scripts/verification-record.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';
import { outsideGit } from './lib/private-evidence.mjs';
import { sourceInputs } from './lib/verification-inputs.mjs';
import { templateSpec } from './lib/verification-spec.mjs';
import { appendEvent, createRun, currentSpec, preflight, readPrivateJson, readRun, renderReport, status, updateRun, validateRecordProfile, validateSpec } from './lib/verification-record.mjs';
import { buildCase, buildCleanup, buildOutcome, buildResource } from './lib/verification-record-builders.mjs';
import { familyStatus, readRunFamily, renderFamilyReport } from './lib/verification-record-family.mjs';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const HELP = `Usage: npm run verify:live:record -- COMMAND --run /private/path/run.json
template --mode MODE [--purpose PURPOSE] [--area NAME] --output FILE
case-add --spec FILE --output FILE --case ID --title TEXT --context ID --area AREA --require CAP --proof SURFACE
init --spec FILE [--parent-run FILE] [--original-case CHILD=PARENT]
preflight Show runnable cases, blockers, registry warnings
refresh --spec FILE --reason TEXT Refresh observed capabilities and context
begin --case ID [--reason TEXT] Record an attempt before the action; prints its attemptId
blocked --case ID --reason TEXT [--category CATEGORY] [--evidence FILE]
Record why a case cannot run, without spending an attempt
verdict --case ID --blocks-pr yes|no --reason TEXT
Say whether a failed or blocked case blocks the PR
lesson --text TEXT [--area AREA] [--case ID]
Keep a gotcha for qa/live/features/; the report lists it
record --event FILE Record outcome, cleanup, repair, batch or candidate_transition
resource --case ID --provider ID --kind ID --resource-id ID --ownership TYPE --cleanup-preset PRESET|--expected-file FILE --evidence FILE
finish|resolve --attempt ID --result RESULT --summary TEXT --evidence FILE --proof SURFACE=FILE [--completed-at ISO] [--observed-at ISO|now] [--timing-observation-ms MS]
Proof surfaces: slack, admin, mcp, provider, model. A live case
needs slack, admin or mcp proof. Omit a time you did not observe.
cleanup --resource ID --outcome OUTCOME [--observed-file FILE] --evidence FILE
phase-start --phase NAME [--case ID] [--attempt ID]
phase-stop --phase-id ID
status Resume: open attempts, cleanup, repairs and next work
report [--output FILE] Generate Markdown from the record
All files must be outside Git. The record is private and never runs a browser,
claims/deploys a lane, retries an action, or cleans a resource. See
qa/live/operator/records.md for a spec and event examples. Use existing
verify:regression --record FILE for measured offline check receipts.
`;
/** The defaults are the standalone profile: v1 records and this checkout's
* source. Another profile's coordinator passes { record, source }, where
* record is its validated profile block and source() its current source. */
export function runRecordCli(argv, root = ROOT, io = {}, profile) {
const output = io.stdout ?? ((value) => process.stdout.write(value));
const error = io.stderr ?? ((value) => process.stderr.write(value));
try {
if (profile !== undefined) {
if (typeof profile?.source !== 'function') throw new Error('A record profile needs a source provider.');
validateRecordProfile(profile.record);
}
const descriptor = profile?.record, scope = { profile: descriptor };
const currentSource = () => profile ? profile.source() : sourceInputs(root);
const { values: flags, positionals } = parseArgs({ args: argv, allowPositionals: true, options: {
...Object.fromEntries(['run', 'spec', 'reason', 'case', 'event', 'output', 'mode', 'purpose', 'title', 'text', 'context', 'max-attempts', 'max-wait-ms', 'min-observation-ms', 'original-request', 'expected-outcome', 'variant', 'cleanup-contract', 'provider', 'kind', 'resource-id', 'ownership', 'cleanup-preset', 'expected-file', 'attempt', 'result', 'summary', 'category', 'completed-at', 'observed-at', 'timing-observation-ms', 'cost-usd', 'resource', 'outcome', 'observed-file', 'phase', 'phase-id', 'parent-run', 'blocks-pr'].map((key) => [key, { type: 'string' }])),
area: { type: 'string', multiple: true }, require: { type: 'string', multiple: true }, proof: { type: 'string', multiple: true }, evidence: { type: 'string', multiple: true }, 'original-case': { type: 'string', multiple: true },
'stop-at': { type: 'string' }, 'max-occurrences': { type: 'string' }, family: { type: 'boolean' },
help: { type: 'boolean' },
} });
if (flags.help) { output(HELP); return 0; }
if (positionals.length !== 1) throw new Error('Choose one command. Use --help.');
const command = positionals[0];
const allowed = {
template: ['mode', 'purpose', 'area', 'output'],
'case-add': ['spec', 'output', 'case', 'title', 'context', 'area', 'require', 'proof', 'max-attempts', 'max-wait-ms', 'min-observation-ms', 'original-request', 'expected-outcome', 'variant', 'cleanup-contract'],
init: ['run', 'spec', 'parent-run', 'original-case'], preflight: ['run'], refresh: ['run', 'spec', 'reason'],
begin: ['run', 'case', 'reason'], blocked: ['run', 'case', 'reason', 'category', 'evidence'], lesson: ['run', 'text', 'area', 'case'], verdict: ['run', 'case', 'blocks-pr', 'reason'],
record: ['run', 'event'], status: ['run'], report: ['run', 'output', 'family'],
resource: ['run', 'case', 'provider', 'kind', 'resource-id', 'ownership', 'cleanup-preset', 'expected-file', 'evidence', 'stop-at', 'max-occurrences'],
finish: ['run', 'attempt', 'result', 'summary', 'category', 'evidence', 'proof', 'completed-at', 'observed-at', 'timing-observation-ms', 'cost-usd'],
resolve: ['run', 'attempt', 'result', 'summary', 'category', 'evidence', 'proof', 'completed-at', 'observed-at', 'timing-observation-ms', 'cost-usd'],
cleanup: ['run', 'resource', 'outcome', 'observed-file', 'evidence'],
'phase-start': ['run', 'phase', 'case', 'attempt'], 'phase-stop': ['run', 'phase-id'],
}[command];
if (!allowed || Object.keys(flags).some((key) => !allowed.includes(key))) throw new Error('Unknown command or inapplicable option.');
if (command === 'template') {
if (profile) throw new Error('This profile\'s spec templates come from its planner.');
if (!flags.output) throw new Error('template needs --output.');
const path = outsideGit(flags.output);
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
writeFileSync(path, `${JSON.stringify(templateSpec(flags.mode, flags.area, Date.now(), flags.purpose), null, 2)}\n`, { flag: 'wx', mode: 0o600 });
output(`${JSON.stringify({ output: path, next: 'Resolve observed capabilities, review selected scope, then init. Unavailable fixtures remain blockers.' })}\n`);
return 0;
}
if (command === 'case-add') {
if (!flags.spec || !flags.output) throw new Error('case-add needs --spec and a distinct --output.');
const input = outsideGit(flags.spec), path = outsideGit(flags.output);
if (input === path) throw new Error('case-add output must differ from its input spec.');
const spec = structuredClone(readPrivateJson(input));
const named = { case: flags.case, title: flags.title, context: flags.context, area: flags.area, require: flags.require, proof: flags.proof,
maxAttempts: flags['max-attempts'], maxWaitMs: flags['max-wait-ms'], minObservationMs: flags['min-observation-ms'],
originalRequest: flags['original-request'], expectedOutcome: flags['expected-outcome'], variant: flags.variant, cleanup: flags['cleanup-contract'] };
spec.cases.push(buildCase(spec, named)); validateSpec(spec, descriptor?.areas, { releaseGate: descriptor?.releaseGate });
mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); writeFileSync(path, `${JSON.stringify(spec, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
output(`${JSON.stringify({ output: path, caseId: flags.case })}\n`); return 0;
}
if (!flags.run) throw new Error('Pass --run.');
let result, code = 0;
if (command === 'init') {
if (!flags.spec) throw new Error('init needs --spec.');
let lineage;
if (flags['parent-run']) {
const parentPath = outsideGit(flags['parent-run']), parent = readRun(parentPath, scope);
const family = readRunFamily(parentPath, currentSource(), 32, scope);
if (family.missingAncestor) throw new Error('Parent run family has a missing ancestor.');
const originalCases = {};
for (const mapping of flags['original-case'] ?? []) {
const [child, original, extra] = mapping.split('=');
if (!child || !original || extra !== undefined) throw new Error('Original case must use CHILD=PARENT.');
if (!currentSpec(parent).cases.some((entry) => entry.id === original)) throw new Error('Original parent case was not found in its current spec.');
originalCases[child] = { runId: parent.id, caseId: original };
}
const spec = readPrivateJson(flags.spec);
if (Object.keys(originalCases).some((child) => !spec.cases.some((entry) => entry.id === child))) throw new Error('Original-case child was not found in the new spec.');
lineage = { parent: { path: parentPath, runId: parent.id }, originalCases };
} else if (flags['original-case']?.length) throw new Error('--original-case needs --parent-run.');
result = createRun(flags.run, readPrivateJson(flags.spec), currentSource(), Date.now(), lineage, descriptor);
result = { runId: result.id, preflight: preflight(result) };
} else if (['refresh', 'begin', 'blocked', 'lesson', 'verdict', 'record', 'resource', 'finish', 'resolve', 'cleanup', 'phase-start', 'phase-stop'].includes(command)) {
let event;
if (command === 'record') {
if (!flags.event) throw new Error('record needs --event.');
event = readPrivateJson(flags.event);
} else if (command === 'refresh') {
if (!flags.spec) throw new Error('refresh needs --spec.');
event = { type: 'refresh', spec: readPrivateJson(flags.spec), reason: flags.reason };
} else if (command === 'begin') event = { type: 'begin', caseId: flags.case, ...(flags.reason ? { reason: flags.reason } : {}) };
else if (command === 'verdict') {
if (!flags.case || !flags['blocks-pr'] || !flags.reason) throw new Error('verdict needs --case, --blocks-pr yes|no and --reason.');
event = { type: 'verdict', caseId: flags.case, blocksPr: flags['blocks-pr'], reason: flags.reason };
} else if (command === 'lesson') {
if (!flags.text) throw new Error('lesson needs --text.');
event = { type: 'lesson', text: flags.text, ...(flags.area?.length ? { areas: flags.area } : {}), ...(flags.case ? { caseId: flags.case } : {}) };
} else if (command === 'blocked') {
if (!flags.case || !flags.reason) throw new Error('blocked needs --case and --reason.');
event = { type: 'case_blocked', caseId: flags.case, reason: flags.reason,
...(flags.category ? { category: flags.category } : {}), ...(flags.evidence ? { evidence: flags.evidence } : {}) };
}
else if (command !== 'resource') {
const named = { ...flags, completedAt: flags['completed-at'], observedAt: flags['observed-at'], timingObservationMs: flags['timing-observation-ms'], costUsd: flags['cost-usd'], observedFile: flags['observed-file'], phaseId: flags['phase-id'] };
if (command === 'finish' || command === 'resolve') event = buildOutcome(command, named);
else if (command === 'cleanup') event = buildCleanup(named, readPrivateJson);
else if (command === 'phase-start') event = { type: 'phase_start', phase: flags.phase, ...(flags.case ? { caseId: flags.case } : {}), ...(flags.attempt ? { attemptId: flags.attempt } : {}) };
else event = { type: 'phase_finish', phaseId: flags['phase-id'] };
}
const source = currentSource();
result = updateRun(flags.run, (run) => {
const input = command === 'resource' ? buildResource(run, { ...flags, resourceId: flags['resource-id'], cleanupPreset: flags['cleanup-preset'], expectedFile: flags['expected-file'], stopAt: flags['stop-at'], maxOccurrences: flags['max-occurrences'] }, readPrivateJson) : event;
return appendEvent(run, input, source);
}, scope);
// The full attempt carries every input digest; the operator needs its ID and deadline.
if (command === 'begin') result = { attemptId: result.id, caseId: result.caseId, deadline: result.deadline, id: result.id };
} else {
const run = readRun(flags.run, scope);
if (command === 'preflight') { result = preflight(run); code = result.ready ? 0 : 1; }
else {
const view = status(run, currentSource());
if (command === 'status') result = view;
else {
const report = flags.family ? renderFamilyReport(familyStatus(readRunFamily(flags.run, currentSource(), 32, scope)), renderReport) : renderReport(view);
if (!flags.output) { output(`${report}\n`); return 0; }
const path = outsideGit(flags.output);
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
// A report is disposable. Never replace the record or a retained evidence file.
writeFileSync(path, report, { flag: 'wx', mode: 0o600 });
result = { output: path, complete: view.complete };
}
}
}
output(`${JSON.stringify(result, null, 2)}\n`);
return code;
} catch (cause) {
error(`Record command failed: ${cause instanceof Error && !cause.code?.startsWith('ERR_PARSE_ARGS') ? cause.message : 'Invalid arguments.'}\n`);
return 2;
}
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
process.exitCode = runRecordCli(process.argv.slice(2));
}