feat(audit): persist phase0 backend drift report artifacts
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user