ccqa 1.21.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/ccqa.mjs +198 -97
- package/dist/hub-client/index.d.mts +8 -0
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -6842,10 +6842,26 @@ const SpecLedgerEntrySchema = z.object({
|
|
|
6842
6842
|
deployedSha: z.string().nullable().optional(),
|
|
6843
6843
|
deployedShaAmbiguous: z.boolean().optional()
|
|
6844
6844
|
});
|
|
6845
|
+
/**
|
|
6846
|
+
* A red-bucket entry: where the failure happened, plus what it was. The cause
|
|
6847
|
+
* is copied off the run report's `analysis` so a reader learns why a spec is
|
|
6848
|
+
* red without fetching the report of every red spec.
|
|
6849
|
+
*
|
|
6850
|
+
* Only the red bucket carries it. A pass has no cause, so putting these on
|
|
6851
|
+
* `SpecLedgerEntry` would invite writing them where they cannot exist.
|
|
6852
|
+
*
|
|
6853
|
+
* Both fields are optional and mean the same thing by their absence — nothing
|
|
6854
|
+
* is on record. Failure analysis is opt-in (`--on-fail-explain`), and entries
|
|
6855
|
+
* written before this release have neither field.
|
|
6856
|
+
*/
|
|
6857
|
+
const SpecRedLedgerEntrySchema = SpecLedgerEntrySchema.extend({
|
|
6858
|
+
label: PredictedLabelSchema.optional(),
|
|
6859
|
+
headline: z.string().optional()
|
|
6860
|
+
});
|
|
6845
6861
|
z.object({
|
|
6846
6862
|
green: z.record(z.string(), SpecLedgerEntrySchema).default({}),
|
|
6847
6863
|
run: z.record(z.string(), SpecLedgerEntrySchema).default({}),
|
|
6848
|
-
red: z.record(z.string(),
|
|
6864
|
+
red: z.record(z.string(), SpecRedLedgerEntrySchema).default({})
|
|
6849
6865
|
});
|
|
6850
6866
|
/** One deploy, as the consumer's deploy job reported it (ADR-0010). */
|
|
6851
6867
|
const DeployEntrySchema = z.object({
|
|
@@ -7025,7 +7041,7 @@ const SpecRerunSchema = z.object({
|
|
|
7025
7041
|
heldBy: SpecLockSchema.nullable(),
|
|
7026
7042
|
lastRun: SpecLedgerEntrySchema.nullable(),
|
|
7027
7043
|
lastGreen: SpecLedgerEntrySchema.nullable(),
|
|
7028
|
-
lastRed:
|
|
7044
|
+
lastRed: SpecRedLedgerEntrySchema.nullable(),
|
|
7029
7045
|
touchedBy: z.array(z.string()).optional(),
|
|
7030
7046
|
touchedByDeploy: DeployRefSchema.nullable().optional()
|
|
7031
7047
|
});
|
|
@@ -7075,7 +7091,7 @@ z.object({
|
|
|
7075
7091
|
z.object({
|
|
7076
7092
|
entries: z.record(z.string(), SpecLedgerEntrySchema),
|
|
7077
7093
|
lastRun: z.record(z.string(), SpecLedgerEntrySchema).default({}),
|
|
7078
|
-
lastRed: z.record(z.string(),
|
|
7094
|
+
lastRed: z.record(z.string(), SpecRedLedgerEntrySchema).default({})
|
|
7079
7095
|
});
|
|
7080
7096
|
/**
|
|
7081
7097
|
* One spec's last drift audit, as recorded by `ccqa audit --report-to-hub`. Unlike the
|
|
@@ -15299,16 +15315,18 @@ const DEFAULT_CONCURRENCY$1 = 3;
|
|
|
15299
15315
|
* `cli/run` calls this with just the failing specs after vitest.
|
|
15300
15316
|
*/
|
|
15301
15317
|
async function analyzeDrift(input) {
|
|
15302
|
-
const { targets, cwd, blocks, concurrency = DEFAULT_CONCURRENCY$1, model, language, guidance, onSpecStart } = input;
|
|
15318
|
+
const { targets, cwd, blocks, concurrency = DEFAULT_CONCURRENCY$1, model, language, guidance, onSpecStart, onSpecDone } = input;
|
|
15303
15319
|
return runPool(targets, concurrency, async (target) => {
|
|
15304
15320
|
onSpecStart?.(target);
|
|
15305
|
-
|
|
15321
|
+
const result = await checkSpec(target, {
|
|
15306
15322
|
cwd,
|
|
15307
15323
|
blocks,
|
|
15308
15324
|
model,
|
|
15309
15325
|
language,
|
|
15310
15326
|
guidance
|
|
15311
15327
|
});
|
|
15328
|
+
await onSpecDone?.(result);
|
|
15329
|
+
return result;
|
|
15312
15330
|
});
|
|
15313
15331
|
}
|
|
15314
15332
|
async function checkSpec(target, opts) {
|
|
@@ -15497,37 +15515,35 @@ function specStatus(result, threshold) {
|
|
|
15497
15515
|
return "passed";
|
|
15498
15516
|
}
|
|
15499
15517
|
/**
|
|
15500
|
-
*
|
|
15501
|
-
*
|
|
15502
|
-
*
|
|
15503
|
-
*
|
|
15504
|
-
*
|
|
15505
|
-
*
|
|
15518
|
+
* One audited spec as a report row. Browser-execution fields don't apply to an
|
|
15519
|
+
* audit and stay empty — which is why `mode` is carried separately: nothing
|
|
15520
|
+
* ran, but which surfaces were audited is still a fact about the row. The
|
|
15521
|
+
* diagnosis goes into `analysis` because for a `kind: "drift"` report the
|
|
15522
|
+
* diagnosis IS the row's verdict, so it renders through the same card a failed
|
|
15523
|
+
* `ccqa run` spec does.
|
|
15506
15524
|
*
|
|
15507
|
-
*
|
|
15508
|
-
*
|
|
15509
|
-
* card a failed `ccqa run` spec does. `reasoning` has no drift-audit
|
|
15510
|
-
* equivalent (the audit gives one headline, not a deliberation) so it is
|
|
15511
|
-
* filled with an empty string to satisfy `FailureAnalysisSchema`.
|
|
15525
|
+
* Shared by the incremental push and the final report: the hub upserts by
|
|
15526
|
+
* feature/spec, so two mappings would let the closing patch rewrite history.
|
|
15512
15527
|
*/
|
|
15513
|
-
function
|
|
15514
|
-
|
|
15515
|
-
|
|
15516
|
-
|
|
15517
|
-
|
|
15528
|
+
function driftResultToRow(result, threshold) {
|
|
15529
|
+
return {
|
|
15530
|
+
...emptySpecRow({
|
|
15531
|
+
feature: result.target.featureName,
|
|
15532
|
+
spec: result.target.specName,
|
|
15533
|
+
title: result.title ?? null,
|
|
15534
|
+
status: specStatus(result, threshold)
|
|
15535
|
+
}),
|
|
15518
15536
|
...result.live === void 0 ? {} : { mode: result.live ? "live" : "deterministic" },
|
|
15519
|
-
|
|
15520
|
-
|
|
15521
|
-
|
|
15522
|
-
|
|
15523
|
-
|
|
15524
|
-
|
|
15525
|
-
|
|
15526
|
-
|
|
15527
|
-
|
|
15528
|
-
|
|
15529
|
-
liveRun: null
|
|
15530
|
-
}));
|
|
15537
|
+
analysis: result.drift ?? null
|
|
15538
|
+
};
|
|
15539
|
+
}
|
|
15540
|
+
/**
|
|
15541
|
+
* Adapts `ccqa audit` results into the shared RunReportData shape so they can
|
|
15542
|
+
* be pushed to the hub (`ccqa audit --report-to-hub`) and rendered by the same
|
|
15543
|
+
* report UI as `ccqa run`/`ccqa live`.
|
|
15544
|
+
*/
|
|
15545
|
+
function driftResultsToReport(results, meta) {
|
|
15546
|
+
const specResults = results.map((result) => driftResultToRow(result, meta.threshold));
|
|
15531
15547
|
return {
|
|
15532
15548
|
schemaVersion: 1,
|
|
15533
15549
|
kind: "drift",
|
|
@@ -15807,8 +15823,13 @@ async function runAudit(specPath, opts) {
|
|
|
15807
15823
|
requireReportToHubConnection(opts);
|
|
15808
15824
|
let results;
|
|
15809
15825
|
let promptCtx;
|
|
15826
|
+
let push = null;
|
|
15810
15827
|
try {
|
|
15811
15828
|
promptCtx = await resolveAuditPromptContext(opts, cwd);
|
|
15829
|
+
if (opts.reportToHub) {
|
|
15830
|
+
push = await openDriftPush(opts, cwd);
|
|
15831
|
+
if (format === "text") info(`hub: incremental drift run opened (${push.runId})`);
|
|
15832
|
+
}
|
|
15812
15833
|
results = await analyzeDrift({
|
|
15813
15834
|
targets,
|
|
15814
15835
|
cwd,
|
|
@@ -15819,16 +15840,18 @@ async function runAudit(specPath, opts) {
|
|
|
15819
15840
|
guidance: promptCtx.guidance,
|
|
15820
15841
|
onSpecStart: (t) => {
|
|
15821
15842
|
if (format === "text") info(`checking ${t.featureName}/${t.specName}`);
|
|
15843
|
+
},
|
|
15844
|
+
onSpecDone: async (r) => {
|
|
15845
|
+
if (push) await sendDriftRow(push, r, threshold);
|
|
15822
15846
|
}
|
|
15823
15847
|
});
|
|
15824
15848
|
} finally {
|
|
15825
15849
|
if (holder) await releaseSpecs(hub, hubProject, opts.hubProfile, holder);
|
|
15826
15850
|
}
|
|
15827
15851
|
process.stdout.write(renderDrift(results, format, cwd));
|
|
15828
|
-
if (
|
|
15852
|
+
if (push) await sealDriftPush(push, {
|
|
15829
15853
|
results,
|
|
15830
15854
|
threshold,
|
|
15831
|
-
cwd,
|
|
15832
15855
|
opts,
|
|
15833
15856
|
format,
|
|
15834
15857
|
baseRef,
|
|
@@ -15877,59 +15900,92 @@ function requireReportToHubConnection(opts) {
|
|
|
15877
15900
|
process.exit(2);
|
|
15878
15901
|
}
|
|
15879
15902
|
/**
|
|
15880
|
-
*
|
|
15881
|
-
*
|
|
15882
|
-
*
|
|
15883
|
-
*
|
|
15903
|
+
* Open the drift run this sweep patches into. A failure here is fatal, as it
|
|
15904
|
+
* is for `ccqa run`: a job that asked to publish and cannot reach the hub has
|
|
15905
|
+
* not done what it was told, and the audit has no local artifact to fall back
|
|
15906
|
+
* on — the hub is its only output. Raised before any spec is checked, so
|
|
15907
|
+
* nothing is wasted. Not retried: a dropped response after the hub committed
|
|
15908
|
+
* would leave a second orphan running run.
|
|
15884
15909
|
*
|
|
15885
|
-
* `resolveHub` is injectable
|
|
15886
|
-
* a real hub connection; it defaults to the real flag/env resolution.
|
|
15910
|
+
* `resolveHub` is injectable for tests.
|
|
15887
15911
|
*/
|
|
15888
|
-
async function
|
|
15889
|
-
const { results, threshold, cwd, opts, format, baseRef, promptCtx } = args;
|
|
15912
|
+
async function openDriftPush(opts, cwd, resolveHub = resolveHubClient) {
|
|
15890
15913
|
const hub = resolveHub(opts);
|
|
15891
|
-
if (!hub)
|
|
15892
|
-
|
|
15893
|
-
|
|
15914
|
+
if (!hub) throw new RunUsageError("--report-to-hub requires a hub connection (--hub-url/--hub-token or CCQA_HUB_URL/CCQA_HUB_TOKEN)");
|
|
15915
|
+
const project = resolveProject({
|
|
15916
|
+
project: opts.project,
|
|
15917
|
+
cwd
|
|
15918
|
+
});
|
|
15919
|
+
const [branch, gitHead] = await Promise.all([detectBranch(cwd), getGitHead(cwd)]);
|
|
15920
|
+
const ciRunId = githubRunId();
|
|
15921
|
+
const runUrl = githubRunUrl();
|
|
15922
|
+
try {
|
|
15923
|
+
return {
|
|
15924
|
+
hub,
|
|
15925
|
+
runId: (await hub.openRun({
|
|
15926
|
+
project,
|
|
15927
|
+
kind: "drift",
|
|
15928
|
+
...branch ? { branch } : {},
|
|
15929
|
+
...opts.hubProfile ? { profile: opts.hubProfile } : {},
|
|
15930
|
+
...gitHead ? { gitHead } : {},
|
|
15931
|
+
...ciRunId ? { ciRunId } : {},
|
|
15932
|
+
...runUrl ? { runUrl } : {}
|
|
15933
|
+
})).id,
|
|
15934
|
+
gitHead
|
|
15935
|
+
};
|
|
15936
|
+
} catch (err) {
|
|
15937
|
+
throw new RunUsageError(`--report-to-hub: could not open a run on the hub (${errMessage(err)})`);
|
|
15894
15938
|
}
|
|
15939
|
+
}
|
|
15940
|
+
/**
|
|
15941
|
+
* Send one finished spec. A failure is warned and swallowed rather than
|
|
15942
|
+
* thrown: the seal resends every row, so a dropped patch costs freshness for
|
|
15943
|
+
* the rest of the sweep, not the record.
|
|
15944
|
+
*/
|
|
15945
|
+
async function sendDriftRow(push, result, threshold) {
|
|
15895
15946
|
try {
|
|
15896
|
-
|
|
15897
|
-
|
|
15898
|
-
|
|
15947
|
+
await push.hub.patchRun(push.runId, {
|
|
15948
|
+
rows: [driftResultToRow(result, threshold)],
|
|
15949
|
+
reportMeta: { cost: currentReportCost() }
|
|
15899
15950
|
});
|
|
15900
|
-
|
|
15901
|
-
|
|
15902
|
-
|
|
15903
|
-
|
|
15904
|
-
|
|
15905
|
-
|
|
15906
|
-
|
|
15907
|
-
|
|
15908
|
-
|
|
15951
|
+
} catch (err) {
|
|
15952
|
+
warn(`hub: could not push ${result.target.featureName}/${result.target.specName}: ${errMessage(err)}`);
|
|
15953
|
+
}
|
|
15954
|
+
}
|
|
15955
|
+
/**
|
|
15956
|
+
* Close the run with every row and the envelope metadata. Resends all rows
|
|
15957
|
+
* rather than only the unsent ones, so this one call also repairs any row
|
|
15958
|
+
* whose mid-sweep patch failed. Status is left to the hub, which derives it
|
|
15959
|
+
* from the rows.
|
|
15960
|
+
*/
|
|
15961
|
+
async function sealDriftPush(push, args) {
|
|
15962
|
+
const { results, threshold, opts, format, baseRef, promptCtx } = args;
|
|
15963
|
+
const report = driftResultsToReport(results, {
|
|
15964
|
+
threshold,
|
|
15965
|
+
git: {
|
|
15966
|
+
head: push.gitHead,
|
|
15967
|
+
base: baseRef ?? null
|
|
15968
|
+
},
|
|
15969
|
+
customPromptVersion: promptCtx?.customPromptVersion ?? null,
|
|
15970
|
+
triageUserPromptHash: promptCtx?.triageUserPromptHash ?? null
|
|
15971
|
+
});
|
|
15972
|
+
try {
|
|
15973
|
+
await push.hub.patchRun(push.runId, {
|
|
15974
|
+
rows: report.results,
|
|
15975
|
+
done: true,
|
|
15976
|
+
reportMeta: {
|
|
15977
|
+
git: report.git,
|
|
15978
|
+
promptVersion: report.promptVersion,
|
|
15979
|
+
customPromptVersion: report.customPromptVersion,
|
|
15980
|
+
...report.triageUserPromptHash ? { triageUserPromptHash: report.triageUserPromptHash } : {},
|
|
15981
|
+
cost: report.cost
|
|
15982
|
+
}
|
|
15909
15983
|
});
|
|
15910
|
-
const dir = await mkdtemp(join(tmpdir(), "ccqa-drift-push-"));
|
|
15911
|
-
try {
|
|
15912
|
-
await writeFile(join(dir, "report.json"), JSON.stringify(report, null, 2), "utf8");
|
|
15913
|
-
const archive = await packDirToTarGz(dir);
|
|
15914
|
-
const run = await hub.pushRun(archive, {
|
|
15915
|
-
project,
|
|
15916
|
-
...branch ? { branch } : {},
|
|
15917
|
-
kind: "drift"
|
|
15918
|
-
});
|
|
15919
|
-
if (format === "text") info(`pushed drift result to hub: ${(opts.hubUrl ?? process.env.CCQA_HUB_URL ?? "").replace(/\/+$/, "")}/#/runs/${run.id}`);
|
|
15920
|
-
} finally {
|
|
15921
|
-
await rm(dir, {
|
|
15922
|
-
recursive: true,
|
|
15923
|
-
force: true
|
|
15924
|
-
});
|
|
15925
|
-
}
|
|
15926
15984
|
} catch (err) {
|
|
15927
|
-
|
|
15928
|
-
|
|
15929
|
-
process.exit(2);
|
|
15930
|
-
}
|
|
15931
|
-
throw err;
|
|
15985
|
+
error(`hub: could not close the drift run ${push.runId}: ${errMessage(err)}`);
|
|
15986
|
+
process.exit(2);
|
|
15932
15987
|
}
|
|
15988
|
+
if (format === "text") info(`pushed drift result to hub: ${(opts.hubUrl ?? process.env.CCQA_HUB_URL ?? "").replace(/\/+$/, "")}/#/runs/${push.runId}`);
|
|
15933
15989
|
}
|
|
15934
15990
|
/**
|
|
15935
15991
|
* Adapts `AuditOptions` to `resolveHubContext`'s flat option shape. Unlike
|
|
@@ -16755,12 +16811,17 @@ function toLedger(raw) {
|
|
|
16755
16811
|
}
|
|
16756
16812
|
/** Fold `from` into `into` in place, per bucket and key, newest `at` winning. */
|
|
16757
16813
|
function mergeLedgerInto(into, from) {
|
|
16758
|
-
|
|
16759
|
-
|
|
16760
|
-
|
|
16761
|
-
}
|
|
16814
|
+
mergeBucket(into.green, from.green);
|
|
16815
|
+
mergeBucket(into.run, from.run);
|
|
16816
|
+
mergeBucket(into.red, from.red);
|
|
16762
16817
|
return into;
|
|
16763
16818
|
}
|
|
16819
|
+
function mergeBucket(into, from) {
|
|
16820
|
+
for (const [key, entry] of Object.entries(from)) {
|
|
16821
|
+
const prev = into[key];
|
|
16822
|
+
if (!prev || prev.at <= entry.at) into[key] = entry;
|
|
16823
|
+
}
|
|
16824
|
+
}
|
|
16764
16825
|
//#endregion
|
|
16765
16826
|
//#region src/hub/api/validate.ts
|
|
16766
16827
|
/**
|
|
@@ -16971,7 +17032,7 @@ async function updateSpecLedger(storage, run, results) {
|
|
|
16971
17032
|
const key = `${row.feature}/${row.spec}`;
|
|
16972
17033
|
ledger.run[key] = entry;
|
|
16973
17034
|
if (row.status === "passed") ledger.green[key] = entry;
|
|
16974
|
-
else ledger.red[key] = entry;
|
|
17035
|
+
else ledger.red[key] = redEntry(entry, row);
|
|
16975
17036
|
}
|
|
16976
17037
|
if (Object.keys(ledger.run).length === 0) return;
|
|
16977
17038
|
try {
|
|
@@ -16981,6 +17042,23 @@ async function updateSpecLedger(storage, run, results) {
|
|
|
16981
17042
|
}
|
|
16982
17043
|
}
|
|
16983
17044
|
/**
|
|
17045
|
+
* The red bucket's entry: the coordinate every bucket carries, plus what the
|
|
17046
|
+
* failure analysis concluded, so a reader of the ledger learns why a spec is
|
|
17047
|
+
* red without fetching its report.
|
|
17048
|
+
*
|
|
17049
|
+
* A field with nothing behind it is left out rather than written empty:
|
|
17050
|
+
* `analysis` is null when the run did not ask for one (`--on-fail-explain` is
|
|
17051
|
+
* opt-in), and a model that answered with no headline leaves `headline` "".
|
|
17052
|
+
*/
|
|
17053
|
+
function redEntry(entry, row) {
|
|
17054
|
+
if (!row.analysis) return entry;
|
|
17055
|
+
return {
|
|
17056
|
+
...entry,
|
|
17057
|
+
label: row.analysis.label,
|
|
17058
|
+
...row.analysis.headline ? { headline: row.analysis.headline } : {}
|
|
17059
|
+
};
|
|
17060
|
+
}
|
|
17061
|
+
/**
|
|
16984
17062
|
* Advance the drift ledger from a terminal `kind: "drift"` run: each row's
|
|
16985
17063
|
* `analysis` (the diagnosis `driftResultsToReport` put there) becomes that
|
|
16986
17064
|
* spec's newest audit entry. No profile — drift asks whether the spec still
|
|
@@ -17119,23 +17197,20 @@ function createPatchRunHandler(config) {
|
|
|
17119
17197
|
};
|
|
17120
17198
|
});
|
|
17121
17199
|
if (evidence) for (const [relPath, b64] of Object.entries(evidence)) await config.storage.artifacts.putFile(id, relPath, Buffer.from(b64, "base64"));
|
|
17122
|
-
const patch =
|
|
17123
|
-
status: finalStatus ?? (specs.failed > 0 ? "failed" : "passed"),
|
|
17200
|
+
const patch = {
|
|
17124
17201
|
specs,
|
|
17125
17202
|
costUsd,
|
|
17126
17203
|
...run.kind === "drift" ? { drift: summarizeDrift(mergedResults) } : {},
|
|
17127
|
-
...
|
|
17128
|
-
|
|
17129
|
-
|
|
17130
|
-
|
|
17131
|
-
|
|
17132
|
-
|
|
17204
|
+
...done ? {
|
|
17205
|
+
status: finalStatus ?? (specs.failed > 0 ? "failed" : "passed"),
|
|
17206
|
+
...reportMeta?.git?.head ? { gitHead: reportMeta.git.head } : {},
|
|
17207
|
+
...reportMeta?.promptVersion ? { promptVersion: reportMeta.promptVersion } : {},
|
|
17208
|
+
...await deployHeadMovedDuringRun(config.storage, run) ? { deployedShaAmbiguous: true } : {}
|
|
17209
|
+
} : {}
|
|
17133
17210
|
};
|
|
17134
17211
|
const updated = await config.storage.runs.update(id, patch);
|
|
17135
|
-
|
|
17136
|
-
|
|
17137
|
-
await updateDriftLedger(config.storage, updated, mergedResults);
|
|
17138
|
-
}
|
|
17212
|
+
await updateDriftLedger(config.storage, updated, mergedResults);
|
|
17213
|
+
if (done) await updateSpecLedger(config.storage, updated, mergedResults);
|
|
17139
17214
|
sendJson(ctx.res, 200, updated);
|
|
17140
17215
|
};
|
|
17141
17216
|
}
|
|
@@ -22205,6 +22280,17 @@ const CLIENT_JS = `
|
|
|
22205
22280
|
badge.appendChild(document.createTextNode(" " + t("perspectives.run.state." + runState)));
|
|
22206
22281
|
td.appendChild(badge);
|
|
22207
22282
|
|
|
22283
|
+
// What the failure was, as the run's analysis called it — the same place
|
|
22284
|
+
// the audit column names its drift label. Absent on a red entry the run
|
|
22285
|
+
// never analyzed, and on entries written before the ledger carried it.
|
|
22286
|
+
if (runState === "failed" && rr.lastRed && rr.lastRed.label) {
|
|
22287
|
+
var cause = el("span", "cellsub", labelText(rr.lastRed.label));
|
|
22288
|
+
// The one-line conclusion, for a pointer. The detail panel shows it in
|
|
22289
|
+
// full, so nothing here depends on finding it.
|
|
22290
|
+
if (rr.lastRed.headline) cause.title = rr.lastRed.headline;
|
|
22291
|
+
td.appendChild(cause);
|
|
22292
|
+
}
|
|
22293
|
+
|
|
22208
22294
|
// The sub-line is the coordinate of the result being reported — the same
|
|
22209
22295
|
// "when is this from" evidence the audit column carries above, so the two
|
|
22210
22296
|
// axes read as siblings. Why the currency matters belongs to the verdict
|
|
@@ -22423,6 +22509,21 @@ const CLIENT_JS = `
|
|
|
22423
22509
|
return wrap;
|
|
22424
22510
|
}
|
|
22425
22511
|
|
|
22512
|
+
// The failure: which run it was, then what the analysis concluded. The
|
|
22513
|
+
// headline is model output, already localized server-side, so it is shown as
|
|
22514
|
+
// written. A run made without failure analysis carries neither field, and the
|
|
22515
|
+
// row is then the coordinate alone — what it has always been.
|
|
22516
|
+
function rerunFailureValue(entry) {
|
|
22517
|
+
var wrap = el("div");
|
|
22518
|
+
wrap.appendChild(ledgerLine(entry));
|
|
22519
|
+
if (entry.label) {
|
|
22520
|
+
var line = el("div", "d-prose", labelText(entry.label));
|
|
22521
|
+
if (entry.headline) line.appendChild(document.createTextNode(" · " + entry.headline));
|
|
22522
|
+
wrap.appendChild(line);
|
|
22523
|
+
}
|
|
22524
|
+
return wrap;
|
|
22525
|
+
}
|
|
22526
|
+
|
|
22426
22527
|
// Detail row: a definition list of the case's fields plus the note editor.
|
|
22427
22528
|
// Built with createElement/textContent throughout — every field here is
|
|
22428
22529
|
// API-derived, so none of it may go through innerHTML.
|
|
@@ -22456,7 +22557,7 @@ const CLIENT_JS = `
|
|
|
22456
22557
|
var rr = ledgerEntryFor(perspState.rerun, feature, spec);
|
|
22457
22558
|
if (rr) {
|
|
22458
22559
|
row(rerunEvidenceLabelKey(rr), rerunEvidenceValue(rr));
|
|
22459
|
-
if (rerunHasFailure(rr)) row("perspectives.d.lastRed",
|
|
22560
|
+
if (rerunHasFailure(rr)) row("perspectives.d.lastRed", rerunFailureValue(rr.lastRed));
|
|
22460
22561
|
}
|
|
22461
22562
|
frag.appendChild(dl);
|
|
22462
22563
|
|
|
@@ -314,6 +314,14 @@ declare const RerunReportSchema: z.ZodObject<{
|
|
|
314
314
|
at: z.ZodString;
|
|
315
315
|
deployedSha: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
316
316
|
deployedShaAmbiguous: z.ZodOptional<z.ZodBoolean>;
|
|
317
|
+
label: z.ZodOptional<z.ZodEnum<{
|
|
318
|
+
TEST_DRIFT: "TEST_DRIFT";
|
|
319
|
+
SPEC_CHANGE: "SPEC_CHANGE";
|
|
320
|
+
PRODUCT_BUG: "PRODUCT_BUG";
|
|
321
|
+
ENVIRONMENT: "ENVIRONMENT";
|
|
322
|
+
UNKNOWN: "UNKNOWN";
|
|
323
|
+
}>>;
|
|
324
|
+
headline: z.ZodOptional<z.ZodString>;
|
|
317
325
|
}, z.core.$strip>>;
|
|
318
326
|
touchedBy: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
319
327
|
touchedByDeploy: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
package/dist/package.json
CHANGED