Exploratory Test Charter Generator — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Exploratory Test Charter Generator (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
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.tsThe foundation of every exploratory session is a well-formed charter. The industry-standard format is the three-part charter statement.
// 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.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 provides the structure around exploratory sessions, making them auditable and measurable.
// 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);
}
}Heuristics are mental models that guide exploratory testers toward productive areas. They are not checklists; they are thinking tools.
// 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.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`,
}),
];
}Structured notes transform exploratory sessions from anecdotal to evidential.
// 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 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;
}
}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[];
};
metricsUpdate: {
sessionDuration: number;
percentageOnCharter: number;
percentageOnBugInvestigation: number;
percentageOnSetup: number;
defectsPerHour: number;
};
};
}
function conductDebrief(session: ExploratorySession): DebriefSession {
const duration = session.actualDuration || session.charter.timeBox.duration;
const hoursSpent = duration / 60;
return {
sessionId: session.id,
charter: `${session.charter.explore} / ${session.charter.with} / ${session.charter.toDiscover}`,
participants: [session.tester],
duration: session.charter.timeBox.debriefTime,
agenda: {
charterReview: {
wasCharterCompleted: session.coverageAreas.length > 0,
completionRatio: `${Math.round((session.coverageAreas.filter(a => a.depth !== 'shallow').length / Math.max(session.coverageAreas.length, 1)) * 100)}%`,
deviations: session.notes.filter(n => n.type === 'idea').map(n => n.content),
reasonsForDeviation: [],
},
findings: {
defects: session.defects,
observations: session.notes.filter(n => n.type === 'observation').map(n => n.content),
risks: session.notes.filter(n => n.type === 'risk').map(n => n.content),
questions: session.questions,
},
coverageAssessment: {
areasExplored: session.coverageAreas.map(a => a.name),
areasNotReached: [],
depthAchieved: Object.fromEntries(session.coverageAreas.map(a => [a.name, a.depth])),
},
nextActions: {
followUpCharters: session.ideas,
defectsToFile: session.defects.filter(d => d.severity === 'critical' || d.severity === 'blocker').map(d => d.title),
risksToEscalate: session.notes.filter(n => n.type === 'risk' && n.severity === 'critical').map(n => n.content),
questionsToResearch: session.questions,
},
metricsUpdate: {
sessionDuration: duration,
percentageOnCharter: session.metrics.testExecutionPercentage,
percentageOnBugInvestigation: session.metrics.bugInvestigationPercentage,
percentageOnSetup: session.metrics.sessionSetupPercentage,
defectsPerHour: hoursSpent > 0 ? Math.round(session.defects.length / hoursSpent * 10) / 10 : 0,
},
},
};
}Measuring exploratory testing effectiveness requires different metrics than scripted testing.
// exploration-dashboard.ts
interface ExploratoryMetrics {
period: { start: Date; end: Date };
sessionMetrics: {
totalSessions: number;
totalHours: number;
averageSessionLength: number;
charterCompletionRate: number;
};
defectMetrics: {
totalDefects: number;
defectsPerSession: number;
defectsPerHour: number;
severityDistribution: Record<string, number>;
categoryDistribution: Record<string, number>;
};
coverageMetrics: {
totalAreas: number;
exploredAreas: number;
highRiskCoverage: number;
averageDepth: string;
};
qualityIndicators: {
defectClusteringIndex: number; // how concentrated defects are
explorationEfficiency: number; // defects found per unit of effort
riskReductionRate: number; // high-risk areas covered over time
sessionProductivity: number; // useful findings per session
};
}
function calculateExploratoryMetrics(sessions: ExploratorySession[]): ExploratoryMetrics {
const completedSessions = sessions.filter(s => s.status === 'completed');
const totalMinutes = completedSessions.reduce((sum, s) => sum + (s.actualDuration || 0), 0);
const totalHours = totalMinutes / 60;
const allDefects = completedSessions.flatMap(s => s.defects);
const severityDist: Record<string, number> = {};
for (const defect of allDefects) {
severityDist[defect.severity] = (severityDist[defect.severity] || 0) + 1;
}
const categoryDist: Record<string, number> = {};
for (const defect of allDefects) {
categoryDist[defect.category] = (categoryDist[defect.category] || 0) + 1;
}
return {
period: {
start: completedSessions.length > 0 ? completedSessions[0].startTime : new Date(),
end: new Date(),
},
sessionMetrics: {
totalSessions: completedSessions.length,
totalHours: Math.round(totalHours * 10) / 10,
averageSessionLength: completedSessions.length > 0
? Math.round(totalMinutes / completedSessions.length)
: 0,
charterCompletionRate: completedSessions.length > 0
? Math.round(
(completedSessions.filter(s => s.coverageAreas.length > 0).length /
completedSessions.length) * 100
)
: 0,
},
defectMetrics: {
totalDefects: allDefects.length,
defectsPerSession: completedSessions.length > 0
? Math.round((allDefects.length / completedSessions.length) * 10) / 10
: 0,
defectsPerHour: totalHours > 0
? Math.round((allDefects.length / totalHours) * 10) / 10
: 0,
severityDistribution: severityDist,
categoryDistribution: categoryDist,
},
coverageMetrics: {
totalAreas: 0,
exploredAreas: 0,
highRiskCoverage: 0,
averageDepth: 'moderate',
},
qualityIndicators: {
defectClusteringIndex: calculateClusteringIndex(completedSessions),
explorationEfficiency: totalHours > 0 ? allDefects.length / totalHours : 0,
riskReductionRate: 0,
sessionProductivity: completedSessions.length > 0
? completedSessions.reduce((sum, s) => sum + s.metrics.totalNotes, 0) / completedSessions.length
: 0,
},
};
}
function calculateClusteringIndex(sessions: ExploratorySession[]): number {
const areaDefectCounts: Record<string, number> = {};
for (const session of sessions) {
for (const area of session.coverageAreas) {
areaDefectCounts[area.name] = (areaDefectCounts[area.name] || 0) + area.defectsFound;
}
}
const counts = Object.values(areaDefectCounts);
if (counts.length === 0) return 0;
const max = Math.max(...counts);
const total = counts.reduce((a, b) => a + b, 0);
return total > 0 ? Math.round((max / total) * 100) / 100 : 0;
}// charter-config.ts
interface CharterConfig {
defaults: {
timeBoxMinutes: number;
setupPercentage: number;
explorationPercentage: number;
debriefPercentage: number;
breakReminderThreshold: number;
};
personas: TestPersona[];
heuristics: {
name: string;
description: string;
elements: string[];
}[];
riskThresholds: {
high: number;
medium: number;
low: number;
};
reportFormat: 'markdown' | 'json' | 'html';
screenshotDirectory: string;
noteAutoSaveInterval: number; // seconds
}
const defaultConfig: CharterConfig = {
defaults: {
timeBoxMinutes: 60,
setupPercentage: 10,
explorationPercentage: 75,
debriefPercentage: 15,
breakReminderThreshold: 60,
},
personas: [
{ name: 'Novice User', description: 'First-time user with no domain knowledge', technicalLevel: 'novice', motivation: 'Complete a task with minimal guidance', commonActions: ['reading help text', 'clicking obvious buttons', 'making mistakes'] },
{ name: 'Power User', description: 'Experienced user who knows shortcuts', technicalLevel: 'expert', motivation: 'Efficiency and speed', commonActions: ['keyboard shortcuts', 'batch operations', 'customization'] },
{ name: 'Malicious User', description: 'User attempting to break or exploit the system', technicalLevel: 'expert', motivation: 'Finding vulnerabilities and bypasses', commonActions: ['injection attempts', 'parameter tampering', 'privilege escalation'] },
{ name: 'Accessibility User', description: 'User relying on assistive technology', technicalLevel: 'intermediate', motivation: 'Using the product with screen reader or keyboard only', commonActions: ['tab navigation', 'screen reader interaction', 'high contrast mode'] },
],
heuristics: [
{ name: 'SFDIPOT', description: 'San Francisco Depot - Product element analysis', elements: ['Structure', 'Function', 'Data', 'Interfaces', 'Platform', 'Operations', 'Time'] },
{ name: 'HICCUPPS', description: 'Consistency oracle heuristics', elements: ['History', 'Image', 'Comparable', 'Claims', 'User expectations', 'Product', 'Purpose', 'Standards'] },
{ name: 'FEW HICCUPS', description: 'Extended consistency heuristics', elements: ['Familiarity', 'Explainability', 'World', 'History', 'Image', 'Comparable', 'Claims', 'User expectations', 'Product', 'Standards'] },
{ name: 'Touring Heuristics', description: 'Tour-based exploration', elements: ['Guidebook tour', 'Money tour', 'Landmark tour', 'Intellectual tour', 'FedEx tour', 'Garbage collector tour', 'Bad neighborhood tour', 'Museum tour'] },
],
riskThresholds: { high: 45, medium: 20, low: 0 },
reportFormat: 'markdown',
screenshotDirectory: './test-artifacts/screenshots',
noteAutoSaveInterval: 30,
};~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.