UNPKG

paqad-ai

Version:

Spec-driven development framework — AI agents that think before they type

162 lines (139 loc) • 5.29 kB
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 ?? [])]; }