feat(audit): persist phase0 backend drift report artifacts

This commit is contained in:
William Valentin
2026-02-27 09:05:25 -08:00
parent 20224f1601
commit e905fe1d56
10 changed files with 367 additions and 38 deletions
+69 -31
View File
@@ -61,6 +61,10 @@ function usage(): string {
' --baseline-tag <value> Baseline artifact tag (default: previous available per backend)',
' --max-age-hours <number> Require candidate artifact freshness (optional)',
' --require-baseline-history Fail when no prior artifact exists',
' --report-tag <YYYY-MM-DD> Drift report tag (default: current UTC date)',
' --write-default-artifacts Write markdown/json drift reports under artifacts dir',
' --summary-json-out <path> Write JSON report to path',
' --summary-md-out <path> Write Markdown report to path',
' --format <markdown|json> Output format (default: markdown)',
' --out <path> Write output to file instead of stdout',
'',
@@ -76,6 +80,10 @@ function usage(): string {
].join('\n');
}
function isoDateTagNow(): string {
return new Date().toISOString().slice(0, 10);
}
function parseCsv(value: string | undefined): string[] | undefined {
if (!value) {
return undefined;
@@ -311,6 +319,10 @@ async function main(): Promise<void> {
'baseline-tag': { type: 'string' },
'max-age-hours': { type: 'string' },
'require-baseline-history': { type: 'boolean' },
'report-tag': { type: 'string' },
'write-default-artifacts': { type: 'boolean' },
'summary-json-out': { type: 'string' },
'summary-md-out': { type: 'string' },
'min-candidate-sampled-events': { type: 'string' },
'min-baseline-sampled-events': { type: 'string' },
'max-sampled-events-drop-pct': { type: 'string' },
@@ -337,11 +349,25 @@ async function main(): Promise<void> {
const candidateTag = values.tag;
const baselineTag = values['baseline-tag'];
const format = parseFormat(values.format);
const reportTag = values['report-tag'] ?? isoDateTagNow();
const writeDefaultArtifacts = Boolean(values['write-default-artifacts']);
const maxAgeHours = parseOptionalNumber(values['max-age-hours'], '--max-age-hours');
if (typeof maxAgeHours === 'number' && maxAgeHours < 0) {
throw new Error('--max-age-hours must be >= 0.');
}
const defaultBaseName = resolve(artifactsDir, `phase0_baseline_live_backend_drift_${reportTag}`);
const summaryJsonOut = values['summary-json-out']
? resolve(values['summary-json-out'])
: writeDefaultArtifacts
? `${defaultBaseName}.json`
: undefined;
const summaryMdOut = values['summary-md-out']
? resolve(values['summary-md-out'])
: writeDefaultArtifacts
? `${defaultBaseName}.md`
: undefined;
const thresholds = buildThresholds(values as Record<string, string | boolean | undefined>);
const allRecords = await readArtifactRecords(artifactsDir);
const nowMs = Date.now();
@@ -396,37 +422,49 @@ async function main(): Promise<void> {
}
const overallPass = results.every((result) => result.pass);
const output = format === 'json'
? JSON.stringify({
generated_at: new Date().toISOString(),
artifacts_dir: artifactsDir,
backends,
candidate_tag: candidateTag,
baseline_tag: baselineTag,
max_age_hours: maxAgeHours,
thresholds,
overall_pass: overallPass,
results: results.map((result) => ({
backend: result.backend,
pass: result.pass,
candidate: {
tag: result.candidate.tag,
path: result.candidate.path,
generated_at: result.candidate.generatedAtIso,
},
baseline: result.baseline
? {
tag: result.baseline.tag,
path: result.baseline.path,
generated_at: result.baseline.generatedAtIso,
}
: null,
comparison: result.comparison,
freshness: result.freshness,
drift_gate: result.driftGate,
})),
}, null, 2)
: renderMarkdown(artifactsDir, backends, thresholds, maxAgeHours, results, overallPass);
const jsonOutput = JSON.stringify({
generated_at: new Date().toISOString(),
artifacts_dir: artifactsDir,
backends,
candidate_tag: candidateTag,
baseline_tag: baselineTag,
report_tag: reportTag,
max_age_hours: maxAgeHours,
thresholds,
overall_pass: overallPass,
reports: {
summary_json_out: summaryJsonOut,
summary_md_out: summaryMdOut,
},
results: results.map((result) => ({
backend: result.backend,
pass: result.pass,
candidate: {
tag: result.candidate.tag,
path: result.candidate.path,
generated_at: result.candidate.generatedAtIso,
},
baseline: result.baseline
? {
tag: result.baseline.tag,
path: result.baseline.path,
generated_at: result.baseline.generatedAtIso,
}
: null,
comparison: result.comparison,
freshness: result.freshness,
drift_gate: result.driftGate,
})),
}, null, 2);
const markdownOutput = renderMarkdown(artifactsDir, backends, thresholds, maxAgeHours, results, overallPass);
const output = format === 'json' ? jsonOutput : markdownOutput;
if (summaryJsonOut) {
await writeOutput(summaryJsonOut, jsonOutput);
}
if (summaryMdOut) {
await writeOutput(summaryMdOut, markdownOutput);
}
if (values.out) {
await writeOutput(resolve(values.out), output);