paqad-ai
Version:
Spec-driven development framework — AI agents that think before they type
162 lines (139 loc) • 5.29 kB
text/typescript
import type { PaqadHudSnapshot, PaqadStageProof, PaqadStageRun } from './types';
export type WordTone = 'pass' | 'fail' | 'acc' | 'att' | 'cau' | 'dim';
export const NOTHING_TO_PROVE = 'Nothing to prove yet';
export const OLD_RECORD_NOTE =
'This record was written before stage proof. Only its recorded gates are shown.';
export const GATE_STATE_WORDS: Readonly<Record<string, { word: string; tone: WordTone }>> = {
pass: { word: 'Pass', tone: 'pass' },
fail: { word: 'Fail', tone: 'fail' },
inconclusive: { word: 'Could not prove', tone: 'cau' },
'did-not-run': { word: 'Did not run', tone: 'cau' },
'not-required': { word: 'Not required', tone: 'dim' },
};
const NOT_JUDGED_WORD = { word: 'Not judged yet', tone: 'dim' as const };
export function gateStateWord(state: string): { word: string; tone: WordTone } {
return GATE_STATE_WORDS[state] ?? NOT_JUDGED_WORD;
}
const STAGE_STATE_WORDS: Readonly<Record<string, { word: string; tone: WordTone }>> = {
complete: { word: 'Done', tone: 'pass' },
redone: { word: 'Done', tone: 'pass' },
running: { word: 'Running', tone: 'acc' },
failed: { word: 'Failed', tone: 'fail' },
inconclusive: { word: 'Could not prove', tone: 'cau' },
skipped: { word: 'Skipped', tone: 'cau' },
'not-applicable': { word: 'Not required', tone: 'dim' },
};
const NOT_YET_WORD = { word: 'Not yet', tone: 'dim' as const };
const DONE_STATES: ReadonlySet<string> = new Set(['complete', 'redone']);
export const DONE_MARK = '✓';
export type StepMark = 'done' | 'failed' | 'not-yet';
export const STEP_MARKS: Readonly<Record<StepMark, string>> = {
done: '✓',
failed: '✕',
'not-yet': '○',
};
export interface TrustStep {
key: string;
mark: StepMark;
words: string;
detail: string | null;
}
export interface TrustGateRow {
key: string;
words: string;
word: string;
tone: WordTone;
}
export interface TrustStage {
key: string;
words: string;
word: string;
tone: WordTone;
mark: string | null;
steps: TrustStep[];
gates: TrustGateRow[];
run: TrustRun | null;
span?: string | null;
}
export interface TrustRun {
before: string;
report: string;
after: string;
tone: WordTone;
footnote: string | null;
}
export const DOCKED_LINE_MAX = 46;
export const NEEDS_A_LOOK = 'Needs a look';
function trustRun(run: PaqadStageRun | undefined): TrustRun | null {
if (!run) return null;
const line = run.line.length <= DOCKED_LINE_MAX ? run.line : run.short_line;
const at = line.indexOf(` · ${run.report} · `);
const count = run.runs > 1 ? ` · ${run.runs} runs` : '';
return {
before: at === -1 ? '' : line.slice(0, at + 3),
report: run.report,
after: `${at === -1 ? '' : line.slice(at + 3 + run.report.length)}${count}`,
tone: run.tone,
footnote: run.footnote,
};
}
export function stageHasEvidence(stage: PaqadStageProof): boolean {
return (
stage.steps.some((step) => step.state !== 'not-yet') ||
stage.gates.some((gate) => gate.state !== 'not-judged') ||
STAGE_STATE_WORDS[stage.state] !== undefined
);
}
export function trustStage(stage: PaqadStageProof): TrustStage {
const look = STAGE_STATE_WORDS[stage.state] ?? NOT_YET_WORD;
const needsLook = stage.run?.needs_look === true && DONE_STATES.has(stage.state);
return {
key: stage.stage,
words: stage.words,
word: needsLook ? NEEDS_A_LOOK : look.word,
tone: needsLook ? 'cau' : look.tone,
mark: DONE_STATES.has(stage.state) && !needsLook ? DONE_MARK : null,
run: trustRun(stage.run),
steps: stage.steps.map((step) => ({
key: `${stage.stage}-${step.id}`,
mark: step.state,
words: step.words,
detail: step.detail,
})),
gates: stage.gates.map((gate) => ({
key: gate.code,
words: gate.words,
...gateStateWord(gate.state),
})),
};
}
const OLD_GATE_WORDS: Readonly<Record<string, string>> = {
'spec-review': 'The spec was reviewed',
'implementation-review': 'The code was reviewed',
'code-tests-lint': 'Tests, types and lint pass',
'quality-ratchet': 'Quality did not drop',
duplication: 'No near-copies added',
'documentation-freshness': 'Docs match the code',
'ac-test-mapping': 'Every criterion has a test',
'change-completeness': 'Nothing left half done',
'mutation-testing': 'Tests catch real mutations',
'module-docs-structure': 'Module docs keep their shape',
'instructions-docs-structure': 'Instruction docs keep their shape',
'extension-surface': 'Public surface stays stable',
'site-map-freshness': 'The site map matches the app',
};
export function oldRecordGates(snapshot: PaqadHudSnapshot): TrustGateRow[] {
return snapshot.gates.flatMap((gate) => {
const words = OLD_GATE_WORDS[gate.name];
const look = GATE_STATE_WORDS[gate.verdict];
if (!words || !look || gate.owner === 'paqad-record') return [];
return [{ key: gate.name, words, ...look }];
});
}
export function repairLine(snapshot: PaqadHudSnapshot): string | null {
const repairs = [...new Set((snapshot.repairs ?? []).map((repair) => repair.what))];
return repairs.length > 0 ? `paqad repaired its own record: ${repairs.join('; ')}` : null;
}
export function recordGapLines(snapshot: PaqadHudSnapshot): string[] {
return [...new Set(snapshot.record_gaps ?? [])];
}