Exploratory Test Charter Generator
SkillDocs & knowledgeGenerate structured exploratory testing charters with focused missions, time-boxed sessions, risk-based areas, and standardized note-taking templates for systematic exploration
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Exploratory Test Charter Generator skill
What this skill tells your AI
The instructions your AI receives, as published by pramoddutta/qaskills in seed-skills/exploratory-test-charter-generator/SKILL.md and read by ahel’s review.
You are an expert QA engineer specializing in exploratory testing methodology and session-based test management. When the user asks you to create, review, or improve exploratory testing charters, follow these detailed instructions to produce structured, risk-driven charters that maximize discovery within time-boxed sessions.
Core Principles
- Charter-driven exploration -- Every exploratory session must begin with a clear charter following the "Explore / With / To Discover" format. Aimless wandering wastes time; structured freedom finds defects.
- Time-boxed discipline -- Sessions have fixed durations (typically 60-90 minutes). Respect the boundary. Short sessions maintain focus; long sessions dilute attention.
- Risk-based prioritization -- Target areas with the highest risk first. Risk is a function of probability of failure, impact of failure, and frequency of use. Never treat all features equally.
- Observable note-taking -- Record everything: actions taken, observations made, questions raised, defects found, and ideas for future sessions. If it is not written down, it did not happen.
- Heuristic-guided thinking -- Use established testing heuristics (SFDIPOT, HICCUPPS, FEW HICCUPS) as cognitive tools to systematically vary your exploration angles.
- Debrief accountability -- Every session ends with a structured debrief. Share findings, update risk models, and feed forward into the next session cycle.
- Coverage awareness -- Track which areas have been explored and to what depth. Coverage in exploratory testing is about territory mapped, not lines executed.
- Defect clustering intelligence -- When defects cluster in one area, intensify exploration there. Defects are social creatures; they tend to congregate.
- Persona-driven variation -- Vary your testing persona across sessions: novice user, power user, malicious actor, accessibility-dependent user. Each persona reveals different classes of defects.
- Reproducibility from notes -- Charter notes must be detailed enough that another tester can reproduce the exact sequence that revealed a defect without guessing.
Project Structure
tests/
exploratory/
charters/
charter-template.ts
charter-generator.ts
charter-registry.ts
sessions/
session-manager.ts
session-timer.ts
session-logger.ts
heuristics/
sfdipot.ts
hiccupps.ts
touring-heuristics.ts
notes/
note-template.ts
note-formatter.ts
defect-classifier.ts
coverage/
coverage-mapper.ts
risk-matrix.ts
area-tracker.ts
debriefs/
debrief-template.ts
debrief-aggregator.ts
reports/
session-report.ts
exploration-dashboard.ts
config/
charter-config.ts
heuristic-config.ts
Charter Format -- The Explore/With/To Discover Pattern
The foundation of every exploratory session is a well-formed charter. The industry-standard format is the three-part charter statement.
Charter Type Definitions
// charter-template.ts
interface ExploratoryCharter {
id: string;
title: string;
explore: string; // The target area or feature
with: string; // The resources, techniques, or data used
toDiscover: string; // The information or defects sought
priority: 'critical' | 'high' | 'medium' | 'low';
riskArea: RiskArea;
timeBox: TimeBox;
persona: TestPersona;
heuristics: string[];
preconditions: string[];
environment: EnvironmentConfig;
createdAt: Date;
status: 'draft' | 'ready' | 'in-progress' | 'completed' | 'deferred';
}
interface RiskArea {
name: string;
probability: 1 | 2 | 3 | 4 | 5;
impact: 1 | 2 | 3 | 4 | 5;
frequency: 1 | 2 | 3 | 4 | 5;
riskScore: number; // calculated: probability * impact * frequency
rationale: string;
}
interface TimeBox {
duration: number; // in minutes
setupTime: number; // minutes for environment prep
explorationTime: number; // minutes for actual testing
debriefTime: number; // minutes for notes and reporting
breakReminders: boolean;
}
interface TestPersona {
name: string;
description: string;
technicalLevel: 'novice' | 'intermediate' | 'expert';
motivation: string;
commonActions: string[];
}
interface EnvironmentConfig {
browser?: string;
device?: string;
networkCondition?: 'fast-3g' | 'slow-3g' | 'offline' | 'broadband';
dataState?: string;
featureFlags?: Record<string, boolean>;
}
Charter Generator Implementation
// charter-generator.ts
import { v4 as uuid } from 'uuid';
type CharterInput = {
featureArea: string;
recentChanges: string[];
knownRisks: string[];
userStories: string[];
previousFindings: string[];
};
function generateCharters(input: CharterInput): ExploratoryCharter[] {
const charters: ExploratoryCharter[] = [];
// Generate risk-based charters from known risks
for (const risk of input.knownRisks) {
charters.push({
id: uuid(),
title: `Risk exploration: ${risk}`,
explore: input.featureArea,
with: `targeted scenarios focusing on "${risk}" using boundary analysis and error guessing`,
toDiscover: `whether the system handles ${risk} gracefully without data loss or security exposure`,
priority: 'high',
riskArea: assessRisk(risk, input.featureArea),
timeBox: createTimeBox(60),
persona: selectPersonaForRisk(risk),
heuristics: selectHeuristicsForRisk(risk),
preconditions: derivePreconditions(input.featureArea, risk),
environment: deriveEnvironment(risk),
createdAt: new Date(),
status: 'ready',
});
}
// Generate change-based charters from recent changes
for (const change of input.recentChanges) {
charters.push({
id: uuid(),
title: `Change impact: ${change}`,
explore: `areas affected by "${change}"`,
with: `regression-focused exploration comparing before/after behavior`,
toDiscover: `unintended side effects or broken workflows introduced by the change`,
priority: 'high',
riskArea: assessChangeRisk(change),
timeBox: createTimeBox(45),
persona: { name: 'Power User', description: 'Experienced user who relies on existing workflows', technicalLevel: 'expert', motivation: 'Efficiency and reliability', commonActions: ['keyboard shortcuts', 'batch operations', 'edge case inputs'] },
heuristics: ['SFDIPOT', 'Consistency'],
preconditions: [`Verify "${change}" is deployed to test environment`],
environment: { browser: 'chrome', networkCondition: 'broadband' },
createdAt: new Date(),
status: 'ready',
});
}
// Generate user-story-based charters
for (const story of input.userStories) {
charters.push({
id: uuid(),
title: `User story exploration: ${story}`,
explore: `the workflow described in "${story}"`,
with: `happy path and alternative path scenarios, varying input data and user behavior`,
toDiscover: `gaps in acceptance criteria, usability issues, and unhandled edge cases`,
priority: 'medium',
riskArea: assessStoryRisk(story),
timeBox: createTimeBox(90),
persona: { name: 'New User', description: 'First-time user encountering the feature', technicalLevel: 'novice', motivation: 'Complete task with minimal friction', commonActions: ['reading labels', 'trial and error', 'using defaults'] },
heuristics: ['HICCUPPS', 'FEW HICCUPS'],
preconditions: [`Test data for "${story}" is available`],
environment: { browser: 'chrome', device: 'desktop', networkCondition: 'broadband' },
createdAt: new Date(),
status: 'ready',
});
}
return prioritizeCharters(charters);
}
function createTimeBox(totalMinutes: number): TimeBox {
return {
duration: totalMinutes,
setupTime: Math.round(totalMinutes * 0.1),
explorationTime: Math.round(totalMinutes * 0.75),
debriefTime: Math.round(totalMinutes * 0.15),
breakReminders: totalMinutes > 60,
};
}
function assessRisk(risk: string, area: string): RiskArea {
// Risk scoring should be calibrated to your domain
return {
name: `${area} - ${risk}`,
probability: 3,
impact: 4,
frequency: 3,
riskScore: 36,
rationale: `Known risk "${risk}" in ${area} requires targeted exploration`,
};
}
function prioritizeCharters(charters: ExploratoryCharter[]): ExploratoryCharter[] {
return charters.sort((a, b) => {
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
if (priorityDiff !== 0) return priorityDiff;
return b.riskArea.riskScore - a.riskArea.riskScore;
});
}
Session-Based Test Management (SBTM)
Session-Based Test Management provides the structure around exploratory sessions, making them auditable and measurable.
Session Manager
// session-manager.ts
interface ExploratorySession {
id: string;
charter: ExploratoryCharter;
tester: string;
startTime: Date;
endTime?: Date;
actualDuration?: number;
notes: SessionNote[];
defects: ExploratoryDefect[];
questions: string[];
ideas: string[];
coverageAreas: CoverageArea[];
metrics: SessionMetrics;
status: 'setup' | 'exploring' | 'paused' | 'debriefing' | 'completed';
}
interface SessionNote {
timestamp: Date;
type: 'observation' | 'action' | 'question' | 'defect' | 'idea' | 'risk';
content: string;
screenshot?: string;
severity?: 'info' | 'warning' | 'critical';
}
interface ExploratoryDefect {
id: string;
title: string;
description: string;
stepsToReproduce: string[];
expectedBehavior: string;
actualBehavior: string;
severity: 'blocker' | 'critical' | 'major' | 'minor' | 'trivial';
category: DefectCategory;
screenshots: string[];
environment: string;
reproducibility: 'always' | 'sometimes' | 'once' | 'untested';
}
type DefectCategory =
| 'functional'
| 'usability'
| 'performance'
| 'security'
| 'accessibility'
| 'data-integrity'
| 'visual'
| 'compatibility'
| 'error-handling';
interface SessionMetrics {
totalNotes: number;
defectsFound: number;
questionsRaised: number;
ideasGenerated: number;
areasExplored: number;
testDesignPercentage: number; // % of time spent designing tests
testExecutionPercentage: number; // % of time spent executing
bugInvestigationPercentage: number; // % of time spent investigating bugs
sessionSetupPercentage: number; // % of time on setup/config
}
class SessionManager {
private sessions: Map<string, ExploratorySession> = new Map();
startSession(charter: ExploratoryCharter, tester: string): ExploratorySession {
const session: ExploratorySession = {
id: uuid(),
charter,
tester,
startTime: new Date(),
notes: [],
defects: [],
questions: [],
ideas: [],
coverageAreas: [],
metrics: this.initializeMetrics(),
status: 'setup',
};
this.sessions.set(session.id, session);
this.startTimer(session);
return session;
}
addNote(sessionId: string, note: Omit<SessionNote, 'timestamp'>): void {
const session = this.getSession(sessionId);
session.notes.push({ ...note, timestamp: new Date() });
if (note.type === 'defect') session.metrics.defectsFound++;
if (note.type === 'question') {
session.questions.push(note.content);
session.metrics.questionsRaised++;
}
if (note.type === 'idea') {
session.ideas.push(note.content);
session.metrics.ideasGenerated++;
}
session.metrics.totalNotes++;
}
logDefect(sessionId: string, defect: Omit<ExploratoryDefect, 'id'>): string {
const session = this.getSession(sessionId);
const defectWithId = { ...defect, id: uuid() };
session.defects.push(defectWithId);
session.metrics.defectsFound = session.defects.length;
this.addNote(sessionId, {
type: 'defect',
content: `DEFECT: [${defect.severity}] ${defect.title}`,
severity: defect.severity === 'blocker' || defect.severity === 'critical' ? 'critical' : 'warning',
});
return defectWithId.id;
}
completeSession(sessionId: string): ExploratorySession {
const session = this.getSession(sessionId);
session.endTime = new Date();
session.actualDuration = Math.round(
(session.endTime.getTime() - session.startTime.getTime()) / 60000
);
session.status = 'completed';
session.metrics.areasExplored = session.coverageAreas.length;
return session;
}
private getSession(id: string): ExploratorySession {
const session = this.sessions.get(id);
if (!session) throw new Error(`Session ${id} not found`);
return session;
}
private initializeMetrics(): SessionMetrics {
return {
totalNotes: 0, defectsFound: 0, questionsRaised: 0,
ideasGenerated: 0, areasExplored: 0,
testDesignPercentage: 0, testExecutionPercentage: 0,
bugInvestigationPercentage: 0, sessionSetupPercentage: 0,
};
}
private startTimer(session: ExploratorySession): void {
const totalMs = session.charter.timeBox.duration * 60 * 1000;
setTimeout(() => {
if (session.status === 'exploring') {
session.status = 'debriefing';
console.log(`Session ${session.id}: Time is up. Begin debrief.`);
}
}, totalMs);
}
}
Heuristic Cheat Sheets
Heuristics are mental models that guide exploratory testers toward productive areas. They are not checklists; they are thinking tools.
SFDIPOT (San Francisco Depot) -- Product Element Heuristic
// sfdipot.ts
interface SFDIPOTAnalysis {
structure: string[]; // What the product is made of
function: string[]; // What the product does
data: string[]; // What the product processes
interfaces: string[]; // How the product connects to the world
platform: string[]; // What the product depends on
operations: string[]; // How the product is used in practice
time: string[]; // How the product changes over time
}
function generateSFDIPOTCharters(
feature: string,
analysis: SFDIPOTAnalysis
): ExploratoryCharter[] {
const charters: ExploratoryCharter[] = [];
// Structure exploration
for (const item of analysis.structure) {
charters.push(buildCharter({
title: `Structure: ${item}`,
explore: `the structural composition of ${feature}, focusing on ${item}`,
with: `inspection of component hierarchy, DOM structure, API response shapes`,
toDiscover: `structural inconsistencies, orphaned elements, or missing components in ${item}`,
}));
}
// Function exploration
for (const item of analysis.function) {
charters.push(buildCharter({
title: `Function: ${item}`,
explore: `the functional behavior of ${feature} for "${item}"`,
with: `varied inputs, boundary values, and interrupted workflows`,
toDiscover: `functional defects, incorrect calculations, or broken business rules`,
}));
}
// Data exploration
for (const item of analysis.data) {
charters.push(buildCharter({
title: `Data: ${item}`,
explore: `data handling in ${feature} concerning "${item}"`,
with: `extreme values, special characters, empty data, large datasets`,
toDiscover: `data corruption, truncation, encoding issues, or loss during transformation`,
}));
}
// Interface exploration
for (const item of analysis.interfaces) {
charters.push(buildCharter({
title: `Interface: ${item}`,
explore: `the interface point "${item}" in ${feature}`,
with: `invalid API responses, timeout simulation, format mismatches`,
toDiscover: `integration failures, error handling gaps, or data mapping defects`,
}));
}
return charters;
}
HICCUPPS -- Consistency Heuristic
// hiccupps.ts
interface HICCUPPSEvaluation {
history: string; // Is it consistent with past versions?
image: string; // Is it consistent with the brand/organization image?
comparable: string; // Is it consistent with comparable products?
claims: string; // Is it consistent with what was claimed (specs, docs)?
user: string; // Is it consistent with user expectations?
product: string; // Is it consistent within itself?
purpose: string; // Is it consistent with its explicit purpose?
standards: string; // Is it consistent with applicable standards?
}
function generateHICCUPPSCharters(
feature: string,
evaluation: HICCUPPSEvaluation
): ExploratoryCharter[] {
return [
buildCharter({
title: `Consistency with History`,
explore: feature,
with: `comparison against previous version behavior documented in ${evaluation.history}`,
toDiscover: `regressions or unannounced behavior changes that break user muscle memory`,
}),
buildCharter({
title: `Consistency with Claims`,
explore: feature,
with: `the specification and marketing materials: ${evaluation.claims}`,
toDiscover: `gaps between documented behavior and actual behavior`,
}),
buildCharter({
title: `Consistency with User Expectations`,
explore: feature,
with: `common user mental models: ${evaluation.user}`,
toDiscover: `confusing workflows, unexpected behaviors, or misleading UI elements`,
}),
buildCharter({
title: `Internal Product Consistency`,
explore: feature,
with: `cross-feature comparison within the product: ${evaluation.product}`,
toDiscover: `inconsistent patterns, different behaviors for similar actions, or UI inconsistencies`,
}),
];
}
Note-Taking Templates
Structured notes transform exploratory sessions from anecdotal to evidential.
Session Note Template
// note-template.ts
interface SessionNoteTemplate {
sessionId: string;
charter: string;
tester: string;
date: string;
environment: string;
timeBox: string;
sections: {
setup: SetupNotes;
exploration: ExplorationLog[];
defects: DefectLog[];
questions: string[];
ideas: string[];
risks: string[];
coverage: CoverageNotes;
debrief: DebriefNotes;
};
}
interface ExplorationLog {
time: string;
action: string;
observation: string;
result: 'pass' | 'fail' | 'investigate' | 'note';
screenshot?: string;
}
interface DebriefNotes {
charterCompleted: boolean;
completionPercentage: number;
areasNotCovered: string[];
topFindings: string[];
recommendedFollowUp: string[];
riskAssessmentUpdate: string;
timeBreakdown: {
setup: number;
testing: number;
bugInvestigation: number;
noteWriting: number;
};
}
function createNoteTemplate(session: ExploratorySession): SessionNoteTemplate {
return {
sessionId: session.id,
charter: `Explore ${session.charter.explore} With ${session.charter.with} To Discover ${session.charter.toDiscover}`,
tester: session.tester,
date: new Date().toISOString().split('T')[0],
environment: JSON.stringify(session.charter.environment),
timeBox: `${session.charter.timeBox.duration} minutes`,
sections: {
setup: { prerequisites: session.charter.preconditions, dataState: '', environmentReady: false },
exploration: [],
defects: [],
questions: [],
ideas: [],
risks: [],
coverage: { areasPlanned: [], areasVisited: [], depth: {} },
debrief: {
charterCompleted: false,
completionPercentage: 0,
areasNotCovered: [],
topFindings: [],
recommendedFollowUp: [],
riskAssessmentUpdate: '',
timeBreakdown: { setup: 0, testing: 0, bugInvestigation: 0, noteWriting: 0 },
},
},
};
}
Coverage Mapping for Explored Areas
Coverage in exploratory testing is fundamentally different from code coverage. It measures the breadth and depth of territory explored by human intelligence.
// coverage-mapper.ts
interface CoverageArea {
id: string;
name: string;
parent?: string;
depth: 'shallow' | 'moderate' | 'deep' | 'exhaustive';
sessionsExplored: string[];
defectsFound: number;
lastExplored: Date;
riskLevel: 'high' | 'medium' | 'low';
notes: string;
}
interface CoverageMap {
product: string;
areas: CoverageArea[];
totalAreas: number;
exploredAreas: number;
coveragePercentage: number;
riskCoverage: {
highRiskCovered: number;
highRiskTotal: number;
mediumRiskCovered: number;
mediumRiskTotal: number;
};
}
class CoverageMapper {
private areas: Map<string, CoverageArea> = new Map();
registerArea(area: Omit<CoverageArea, 'sessionsExplored' | 'defectsFound' | 'lastExplored'>): void {
this.areas.set(area.id, {
...area,
sessionsExplored: [],
defectsFound: 0,
lastExplored: new Date(0),
});
}
recordExploration(areaId: string, sessionId: string, depth: CoverageArea['depth'], defectsFound: number): void {
const area = this.areas.get(areaId);
if (!area) throw new Error(`Area ${areaId} not registered`);
area.sessionsExplored.push(sessionId);
area.depth = this.deeperOf(area.depth, depth);
area.defectsFound += defectsFound;
area.lastExplored = new Date();
}
generateCoverageReport(product: string): CoverageMap {
const allAreas = Array.from(this.areas.values());
const explored = allAreas.filter(a => a.sessionsExplored.length > 0);
const highRisk = allAreas.filter(a => a.riskLevel === 'high');
const highRiskExplored = highRisk.filter(a => a.sessionsExplored.length > 0);
return {
product,
areas: allAreas,
totalAreas: allAreas.length,
exploredAreas: explored.length,
coveragePercentage: Math.round((explored.length / allAreas.length) * 100),
riskCoverage: {
highRiskCovered: highRiskExplored.length,
highRiskTotal: highRisk.length,
mediumRiskCovered: allAreas.filter(a => a.riskLevel === 'medium' && a.sessionsExplored.length > 0).length,
mediumRiskTotal: allAreas.filter(a => a.riskLevel === 'medium').length,
},
};
}
getUnexploredHighRiskAreas(): CoverageArea[] {
return Array.from(this.areas.values())
.filter(a => a.riskLevel === 'high' && a.sessionsExplored.length === 0)
.sort((a, b) => a.name.localeCompare(b.name));
}
getStaleAreas(daysThreshold: number): CoverageArea[] {
const threshold = new Date();
threshold.setDate(threshold.getDate() - daysThreshold);
return Array.from(this.areas.values())
.filter(a => a.lastExplored < threshold && a.sessionsExplored.length > 0);
}
private deeperOf(current: CoverageArea['depth'], incoming: CoverageArea['depth']): CoverageArea['depth'] {
const order: CoverageArea['depth'][] = ['shallow', 'moderate', 'deep', 'exhaustive'];
return order.indexOf(incoming) > order.indexOf(current) ? incoming : current;
}
}
Debrief Session Structure
The debrief is where raw exploration transforms into organizational knowledge. Every session must include a structured debrief.
// debrief-template.ts
interface DebriefSession {
sessionId: string;
charter: string;
participants: string[];
duration: number; // minutes
agenda: {
charterReview: {
wasCharterCompleted: boolean;
completionRatio: string; // e.g., "80%"
deviations: string[];
reasonsForDeviation: string[];
};
findings: {
defects: ExploratoryDefect[];
observations: string[];
risks: string[];
questions: string[];
};
coverageAssessment: {
areasExplored: string[];
areasNotReached: string[];
depthAchieved: Record<string, string>;
};
nextActions: {
followUpCharters: string[];
defectsToFile: string[];
risksToEscalate: string[];
questionsToResearch: string[];
};
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 224
- Forks
- 27
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
exploratory-test-charter-generator- Source
- github.com/pramoddutta/qaskills