ccqa 1.4.0 → 1.6.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 +218 -118
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -3161,12 +3161,45 @@ function hashTriageUserPrompt(text) {
|
|
|
3161
3161
|
*/
|
|
3162
3162
|
const CHANGED_FILE_DIFF_TOOL = "mcp__diff__changed_file_diff";
|
|
3163
3163
|
function buildFailureAnalysisPrompt(input) {
|
|
3164
|
-
const { script, specYaml, failureLog, liveTranscriptExcerpt, diffPatch, changedFiles, baseRef, driftIssues, outputLanguage = "auto", triageUserPrompt, customPrompt } = input;
|
|
3164
|
+
const { script, specYaml, failureLog, liveTranscriptExcerpt, diffPatch, changedFiles, baseRef, baseSource = null, range = null, driftIssues, outputLanguage = "auto", triageUserPrompt, customPrompt } = input;
|
|
3165
|
+
const lastGreen = baseSource === "last-green";
|
|
3165
3166
|
const triageUserPromptBlock = buildTriageUserPromptBlock(triageUserPrompt);
|
|
3166
3167
|
const customPromptBlock = buildCustomPromptBlock(customPrompt);
|
|
3167
|
-
|
|
3168
|
+
const languageBlock = outputLanguageBlock(outputLanguage, "`reasoning`, `detail`", "label names (TEST_DRIFT, etc.)");
|
|
3169
|
+
const executionBlock = buildExecutionEvidenceBlock(script, failureLog, liveTranscriptExcerpt);
|
|
3170
|
+
const baseLabel = lastGreen ? `this spec's last passing commit${baseRef && baseRef !== "last-green" ? ` (${baseRef})` : ""}` : baseRef ?? "base";
|
|
3171
|
+
const rangeNote = range ? ` — spans ${range.commitCount} commit${range.commitCount === 1 ? "" : "s"} over ${range.days} day${range.days === 1 ? "" : "s"}` : "";
|
|
3172
|
+
let diffBlock;
|
|
3173
|
+
if (diffPatch === null) diffBlock = `## Source changes
|
|
3168
3174
|
|
|
3169
|
-
|
|
3175
|
+
No diff context is available (the base ref could not be resolved, or there are no changes). Classify from the failure log, the spec, and what you can read in the repository — and be correspondingly more conservative: prefer UNKNOWN over a confident SPEC_CHANGE/PRODUCT_BUG call without diff evidence.
|
|
3176
|
+
`;
|
|
3177
|
+
else if (diffPatch.length === 0) diffBlock = `## Source changes since ${baseLabel}${rangeNote}
|
|
3178
|
+
|
|
3179
|
+
### Changed files (name-status)
|
|
3180
|
+
${changedFiles && changedFiles.length > 0 ? changedFiles : "(no changes in range)"}
|
|
3181
|
+
|
|
3182
|
+
No changed file matches this spec's relatedPaths, so no hunks are inlined. "No related change" is a real signal — but before concluding, scan the name-status list for anything that could plausibly reach this spec and fetch its hunk with \`${CHANGED_FILE_DIFF_TOOL}\`.
|
|
3183
|
+
`;
|
|
3184
|
+
else diffBlock = `## Source changes since ${baseLabel}${rangeNote} (git diff, scoped to this spec's relatedPaths, may be truncated)
|
|
3185
|
+
|
|
3186
|
+
### Changed files (name-status)
|
|
3187
|
+
${changedFiles ?? "(unavailable)"}
|
|
3188
|
+
|
|
3189
|
+
### Patch
|
|
3190
|
+
\`\`\`diff
|
|
3191
|
+
${diffPatch}
|
|
3192
|
+
\`\`\`
|
|
3193
|
+
`;
|
|
3194
|
+
const driftBlock = driftIssues && driftIssues.length > 0 ? `## Spec↔code drift audit findings
|
|
3195
|
+
|
|
3196
|
+
A separate read-only audit compared the spec against the current source. Treat these as hints, not verdicts:
|
|
3197
|
+
|
|
3198
|
+
${driftIssues.map((i) => `- [${i.severity}] (${DRAFT_CATEGORY_LABEL[i.category]}${i.stepId ? `, step ${i.stepId}` : ""}) ${i.message}${i.detail ? ` — ${i.detail}` : ""}`).join("\n")}
|
|
3199
|
+
` : "";
|
|
3200
|
+
return `You are analyzing a failing E2E regression test against the source changes since a known-good baseline. Your job is a root-cause CALL, not a fix: decide which of three categories explains the failure, using the source diff as your primary context.
|
|
3201
|
+
|
|
3202
|
+
${languageBlock}## The three categories
|
|
3170
3203
|
|
|
3171
3204
|
The question that separates them: **is the behavior the spec describes still what the product intends?**
|
|
3172
3205
|
|
|
@@ -3189,10 +3222,13 @@ You have **up to 12 tool turns**. Do NOT write, edit, run shell commands, or hit
|
|
|
3189
3222
|
|
|
3190
3223
|
## Decision guidance
|
|
3191
3224
|
|
|
3225
|
+
${lastGreen ? `The baseline is the commit where THIS spec last passed, so the range strictly covers the window in which it broke: the cause is either inside these changes or outside the code entirely (flaky timing, environment, an external service, test data). The range may mix several unrelated merges — most of the diff is noise; what matters is the specific change you can tie to the failing step.` : `The baseline is a fixed ref (typically the PR base): the spec is NOT guaranteed to have passed there, so the range is not guaranteed to contain the cause.`}
|
|
3226
|
+
|
|
3192
3227
|
- Diff touches only attributes/identifiers the test selects on (labels, testids, class names, timing) while the user-visible flow is intact → TEST_DRIFT.
|
|
3193
3228
|
- Diff intentionally removes/reworks the UI or flow that a spec step verifies (component deleted, page restructured, copy redefined, feature flag flipped) → SPEC_CHANGE.
|
|
3194
3229
|
- Diff UNINTENTIONALLY breaks behavior the spec still intends — e.g. a refactor that drops a side effect, an inverted condition, a regression hiding inside a cleanup commit — → PRODUCT_BUG, citing the diff hunk as evidence. A product bug is often introduced BY the diff; what separates it from SPEC_CHANGE is intent: does the change read as a deliberate redesign of what the spec verifies, or as collateral damage?
|
|
3195
|
-
- Diff is unrelated to the failing step (or there is no relevant diff) and the test was passing before → lean PRODUCT_BUG; first rule out timing/data flakiness and infrastructure errors (daemon not running, network down, missing credentials) — those read as UNKNOWN with low confidence, not PRODUCT_BUG
|
|
3230
|
+
${lastGreen ? `- No change in the range explains the failing step (after checking the inline patch, the name-status list, and any hunks you fetched) → the cause is outside the code: answer UNKNOWN with low confidence and name the suspected external cause (flaky timing, environment, external service, test data). Do NOT default to PRODUCT_BUG here — under this baseline a product regression must be tied to an in-range change.` : `- Diff is unrelated to the failing step (or there is no relevant diff) and the test was passing before → lean PRODUCT_BUG; first rule out timing/data flakiness and infrastructure errors (daemon not running, network down, missing credentials) — those read as UNKNOWN with low confidence, not PRODUCT_BUG.`}${range ? `
|
|
3231
|
+
- This range spans ${range.commitCount} commit${range.commitCount === 1 ? "" : "s"} over ${range.days} day${range.days === 1 ? "" : "s"}. The wider the range, the more unrelated changes are mixed in: SPEC_CHANGE and TEST_DRIFT still require citing the specific hunk — do not infer intent from the bulk of a large diff, and lower confidence when the evidence is spread thin.` : ""}
|
|
3196
3232
|
- The drift audit findings (when present) flag spec↔code mismatches; an ERROR there usually supports TEST_DRIFT or SPEC_CHANGE over PRODUCT_BUG.
|
|
3197
3233
|
|
|
3198
3234
|
## Sub-diagnosis vocabulary
|
|
@@ -3232,32 +3268,15 @@ Your **final** assistant message must start with \`{\` and end with \`}\` — a
|
|
|
3232
3268
|
- 0.4-0.7: plausible but another category could explain it
|
|
3233
3269
|
- < 0.4: answer UNKNOWN instead of guessing
|
|
3234
3270
|
|
|
3235
|
-
Evidence rules: TEST_DRIFT and SPEC_CHANGE require at least one concrete \`file\` reference (diff hunk or file:line you actually read). PRODUCT_BUG should explain why the diff does NOT account for the failure.
|
|
3271
|
+
Evidence rules: TEST_DRIFT and SPEC_CHANGE require at least one concrete \`file\` reference (diff hunk or file:line you actually read). PRODUCT_BUG should cite the in-range change that unintentionally broke the behavior when one exists; ${lastGreen ? "under this last-green baseline, if no in-range change explains the failure, that is UNKNOWN (external cause), not PRODUCT_BUG" : "when no such change exists, explain why the diff does NOT account for the failure"}.
|
|
3236
3272
|
|
|
3237
3273
|
## Test Spec (spec.yaml)
|
|
3238
3274
|
${specYaml}
|
|
3239
3275
|
|
|
3240
|
-
${
|
|
3241
|
-
|
|
3242
|
-
${diffPatch ? `## Source changes since ${baseRef ?? "base"} (git diff, may be truncated)
|
|
3243
|
-
|
|
3244
|
-
### Changed files (name-status)
|
|
3245
|
-
${changedFiles ?? "(unavailable)"}
|
|
3246
|
-
|
|
3247
|
-
### Patch
|
|
3248
|
-
\`\`\`diff
|
|
3249
|
-
${diffPatch}
|
|
3250
|
-
\`\`\`
|
|
3251
|
-
` : `## Source changes
|
|
3252
|
-
|
|
3253
|
-
No diff context is available (the base ref could not be resolved, or there are no changes). Classify from the failure log, the spec, and what you can read in the repository — and be correspondingly more conservative: prefer UNKNOWN over a confident SPEC_CHANGE/PRODUCT_BUG call without diff evidence.
|
|
3254
|
-
`}
|
|
3255
|
-
${driftIssues && driftIssues.length > 0 ? `## Spec↔code drift audit findings
|
|
3256
|
-
|
|
3257
|
-
A separate read-only audit compared the spec against the current source. Treat these as hints, not verdicts:
|
|
3276
|
+
${executionBlock}
|
|
3258
3277
|
|
|
3259
|
-
${
|
|
3260
|
-
|
|
3278
|
+
${diffBlock}
|
|
3279
|
+
${driftBlock}`;
|
|
3261
3280
|
}
|
|
3262
3281
|
/**
|
|
3263
3282
|
* Render the execution-evidence section the model needs to classify the
|
|
@@ -3671,17 +3690,20 @@ function splitPatchByFile(patch) {
|
|
|
3671
3690
|
/**
|
|
3672
3691
|
* Scope a full patch down to the files a spec depends on, then truncate so
|
|
3673
3692
|
* the analysis prompt stays bounded. `relatedPaths` null/empty means the
|
|
3674
|
-
* spec is unscoped — keep the whole patch (still truncated).
|
|
3675
|
-
*
|
|
3693
|
+
* spec is unscoped — keep the whole patch (still truncated). When
|
|
3694
|
+
* relatedPaths are declared but nothing in the diff matches, the result is
|
|
3695
|
+
* the empty string: "no related change" is itself a signal the prompt
|
|
3696
|
+
* renders explicitly, and the model can inspect any unmatched file's hunk
|
|
3697
|
+
* via the on-demand diff tool — inlining the full unrelated diff (the old
|
|
3698
|
+
* fallback) just burned the prompt budget, especially under wide last-green
|
|
3699
|
+
* baselines. Callers scoping the same patch for many specs can pass
|
|
3700
|
+
* pre-split sections instead.
|
|
3676
3701
|
*/
|
|
3677
3702
|
function scopePatchForSpec(patch, relatedPaths, caps = {}) {
|
|
3678
3703
|
const perFile = caps.perFile ?? 8192;
|
|
3679
3704
|
const total = caps.total ?? 49152;
|
|
3680
3705
|
let sections = typeof patch === "string" ? splitPatchByFile(patch) : patch;
|
|
3681
|
-
if (relatedPaths && relatedPaths.length > 0)
|
|
3682
|
-
const scoped = sections.filter((s) => isPathAffectedBy(s.path, relatedPaths));
|
|
3683
|
-
if (scoped.length > 0) sections = scoped;
|
|
3684
|
-
}
|
|
3706
|
+
if (relatedPaths && relatedPaths.length > 0) sections = sections.filter((s) => isPathAffectedBy(s.path, relatedPaths));
|
|
3685
3707
|
const parts = [];
|
|
3686
3708
|
let used = 0;
|
|
3687
3709
|
let droppedFiles = 0;
|
|
@@ -3716,6 +3738,40 @@ function lookupFileDiff(sections, path) {
|
|
|
3716
3738
|
if (section.body.length <= 16384) return section.body;
|
|
3717
3739
|
return `${section.body.slice(0, FILE_DIFF_RESPONSE_CAP)}\n[truncated: ${section.body.length - FILE_DIFF_RESPONSE_CAP} more chars — Read the file for its full current state]`;
|
|
3718
3740
|
}
|
|
3741
|
+
/**
|
|
3742
|
+
* Best-effort width of the base..HEAD range. Two-dot rev-list matches what
|
|
3743
|
+
* the three-dot diff shows: commits on the HEAD side since the merge base.
|
|
3744
|
+
*/
|
|
3745
|
+
async function measureRange(sha, cwd) {
|
|
3746
|
+
try {
|
|
3747
|
+
const [{ stdout: count }, { stdout: baseTime }, { stdout: headTime }] = await Promise.all([
|
|
3748
|
+
execFileP("git", [
|
|
3749
|
+
"rev-list",
|
|
3750
|
+
"--count",
|
|
3751
|
+
`${sha}..HEAD`
|
|
3752
|
+
], { cwd }),
|
|
3753
|
+
execFileP("git", [
|
|
3754
|
+
"log",
|
|
3755
|
+
"-1",
|
|
3756
|
+
"--format=%ct",
|
|
3757
|
+
sha
|
|
3758
|
+
], { cwd }),
|
|
3759
|
+
execFileP("git", [
|
|
3760
|
+
"log",
|
|
3761
|
+
"-1",
|
|
3762
|
+
"--format=%ct",
|
|
3763
|
+
"HEAD"
|
|
3764
|
+
], { cwd })
|
|
3765
|
+
]);
|
|
3766
|
+
const seconds = Number(headTime.trim()) - Number(baseTime.trim());
|
|
3767
|
+
return {
|
|
3768
|
+
commitCount: Number(count.trim()),
|
|
3769
|
+
days: Math.max(0, Math.round(seconds / 86400))
|
|
3770
|
+
};
|
|
3771
|
+
} catch {
|
|
3772
|
+
return null;
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3719
3775
|
function createDiffProvider(args) {
|
|
3720
3776
|
const { resolveBase, cwd } = args;
|
|
3721
3777
|
const captures = /* @__PURE__ */ new Map();
|
|
@@ -3724,17 +3780,19 @@ function createDiffProvider(args) {
|
|
|
3724
3780
|
const cached = captures.get(sha);
|
|
3725
3781
|
if (cached) return cached;
|
|
3726
3782
|
const pending = (async () => {
|
|
3727
|
-
const result = await capturePrDiff(sha, cwd);
|
|
3783
|
+
const [result, range] = await Promise.all([capturePrDiff(sha, cwd), measureRange(sha, cwd)]);
|
|
3728
3784
|
if (!result.ok) return {
|
|
3729
3785
|
sections: null,
|
|
3730
3786
|
nameStatus: null,
|
|
3731
|
-
error: result.error
|
|
3787
|
+
error: result.error,
|
|
3788
|
+
range
|
|
3732
3789
|
};
|
|
3733
3790
|
const { patch, nameStatus } = result.diff;
|
|
3734
3791
|
return {
|
|
3735
3792
|
sections: patch.length > 0 ? splitPatchByFile(patch) : [],
|
|
3736
3793
|
nameStatus,
|
|
3737
|
-
error: null
|
|
3794
|
+
error: null,
|
|
3795
|
+
range
|
|
3738
3796
|
};
|
|
3739
3797
|
})();
|
|
3740
3798
|
captures.set(sha, pending);
|
|
@@ -3760,6 +3818,7 @@ function createDiffProvider(args) {
|
|
|
3760
3818
|
patch: sections ? scopePatchForSpec(sections, scope) : null,
|
|
3761
3819
|
nameStatus: captured.nameStatus,
|
|
3762
3820
|
error: captured.error,
|
|
3821
|
+
range: captured.range,
|
|
3763
3822
|
fileDiff: (path) => sections ? lookupFileDiff(sections, path) : null
|
|
3764
3823
|
};
|
|
3765
3824
|
} };
|
|
@@ -5831,7 +5890,6 @@ async function runLiveSpecs(specs, opts) {
|
|
|
5831
5890
|
const userPromptSuffix = userPromptBundle?.text ?? null;
|
|
5832
5891
|
const diffProvider = opts.diffProvider ?? null;
|
|
5833
5892
|
const failureAnalysisEnabled = diffProvider != null;
|
|
5834
|
-
const driftAuditEnabled = failureAnalysisEnabled && opts.driftAudit !== false;
|
|
5835
5893
|
const auth = failureAnalysisEnabled ? driftAuthAvailable() : {
|
|
5836
5894
|
ok: false,
|
|
5837
5895
|
reason: "disabled"
|
|
@@ -5859,8 +5917,7 @@ async function runLiveSpecs(specs, opts) {
|
|
|
5859
5917
|
const row = await buildLiveReportRow(outcome, {
|
|
5860
5918
|
auth,
|
|
5861
5919
|
diffProvider,
|
|
5862
|
-
reportDir
|
|
5863
|
-
driftAuditEnabled
|
|
5920
|
+
reportDir
|
|
5864
5921
|
}, opts, cwd);
|
|
5865
5922
|
await opts.report?.upsert(row);
|
|
5866
5923
|
return {
|
|
@@ -5894,7 +5951,7 @@ async function buildLiveReportRow(r, ctx, opts, cwd) {
|
|
|
5894
5951
|
result: r.result,
|
|
5895
5952
|
reportDir: ctx.reportDir
|
|
5896
5953
|
});
|
|
5897
|
-
const driftForSpec = ctx.
|
|
5954
|
+
const driftForSpec = ctx.diffProvider && r.result.status === "failed" ? await runDriftAuditOne(r, opts, cwd) : null;
|
|
5898
5955
|
const analysis = ctx.diffProvider && r.result.status === "failed" ? await analyzeOneLiveFailure(r, ctx.diffProvider, driftForSpec, ctx.auth, opts, cwd) : void 0;
|
|
5899
5956
|
return {
|
|
5900
5957
|
...base,
|
|
@@ -6146,6 +6203,8 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
|
|
|
6146
6203
|
diffPatch: specDiff.patch,
|
|
6147
6204
|
changedFiles: specDiff.nameStatus,
|
|
6148
6205
|
baseRef: specDiff.base.ref,
|
|
6206
|
+
baseSource: specDiff.base.source,
|
|
6207
|
+
range: specDiff.range,
|
|
6149
6208
|
driftIssues: driftForSpec,
|
|
6150
6209
|
...opts.language ? { outputLanguage: opts.language } : {},
|
|
6151
6210
|
...opts.triageUserPrompt ? { triageUserPrompt: opts.triageUserPrompt } : {},
|
|
@@ -9494,7 +9553,6 @@ async function executeRun(targets, opts) {
|
|
|
9494
9553
|
...typeof opts.retry === "number" ? { retry: opts.retry } : {},
|
|
9495
9554
|
concurrency: opts.concurrency ?? 1,
|
|
9496
9555
|
...opts.profile ? { profile: opts.profile } : {},
|
|
9497
|
-
...opts.driftAudit !== false ? { driftAudit: true } : {},
|
|
9498
9556
|
diffProvider,
|
|
9499
9557
|
hubContext: hubCtx,
|
|
9500
9558
|
customPrompt,
|
|
@@ -9724,7 +9782,6 @@ function failedSpec(s) {
|
|
|
9724
9782
|
*/
|
|
9725
9783
|
async function analyzeDeterministicSummaries(summaries, opts, cwd, reportDir, customPrompt, triageUserPrompt, diffProvider) {
|
|
9726
9784
|
const failureAnalysisEnabled = diffProvider != null;
|
|
9727
|
-
const driftAuditEnabled = failureAnalysisEnabled && opts.driftAudit !== false;
|
|
9728
9785
|
const auth = failureAnalysisEnabled ? driftAuthAvailable() : {
|
|
9729
9786
|
ok: false,
|
|
9730
9787
|
reason: "skipped by flags"
|
|
@@ -9735,7 +9792,7 @@ async function analyzeDeterministicSummaries(summaries, opts, cwd, reportDir, cu
|
|
|
9735
9792
|
const specInfoByKey = new Map(tree.flatMap((f) => f.specs.map((sp) => [`${f.featureName}/${sp.specName}`, sp])));
|
|
9736
9793
|
const findSpecInfo = (s) => specInfoByKey.get(`${s.featureName}/${s.specName}`) ?? null;
|
|
9737
9794
|
let driftResults = [];
|
|
9738
|
-
if (
|
|
9795
|
+
if (failureAnalysisEnabled && auth.ok && failed.length > 0) {
|
|
9739
9796
|
const targets = failed.map((s) => {
|
|
9740
9797
|
const spec = findSpecInfo(s);
|
|
9741
9798
|
if (!spec) return null;
|
|
@@ -9823,6 +9880,8 @@ async function analyzeDeterministicSummaries(summaries, opts, cwd, reportDir, cu
|
|
|
9823
9880
|
diffPatch: diffExcerpt,
|
|
9824
9881
|
changedFiles: specDiffResult.nameStatus,
|
|
9825
9882
|
baseRef: specDiffResult.base.ref,
|
|
9883
|
+
baseSource: specDiffResult.base.source,
|
|
9884
|
+
range: specDiffResult.range,
|
|
9826
9885
|
driftIssues,
|
|
9827
9886
|
...opts.language ? { outputLanguage: opts.language } : {},
|
|
9828
9887
|
...triageUserPrompt ? { triageUserPrompt } : {},
|
|
@@ -9888,7 +9947,7 @@ function buildReportEnvelope(args) {
|
|
|
9888
9947
|
},
|
|
9889
9948
|
model: opts.model ?? null,
|
|
9890
9949
|
language: opts.language ?? null,
|
|
9891
|
-
promptVersion: "
|
|
9950
|
+
promptVersion: "6",
|
|
9892
9951
|
customPromptVersion,
|
|
9893
9952
|
...triageUserPromptHash !== null ? { triageUserPromptHash } : {}
|
|
9894
9953
|
};
|
|
@@ -10212,7 +10271,7 @@ function installTeardownSignalHandlers(teardown) {
|
|
|
10212
10271
|
}
|
|
10213
10272
|
//#endregion
|
|
10214
10273
|
//#region src/cli/run.ts
|
|
10215
|
-
const runCommand = addHubOptions(addProfileOption(addLanguageOption(new Command("run").argument("[targets...]", "Specs to run, space-separated: each '<feature>/<spec>', '<feature>', or omit for all. Duplicates are de-duped.").description("Run specs. Each spec's execution mode comes from its spec.yaml `mode:` field (default deterministic; set `mode: live` to have Claude drive agent-browser live per step). Deterministic specs replay the recorded test.spec.ts under vitest. A structured report (report.json + evidence) is always written; use --push-report to also stream it to a hub.").option("--report [dir]", `Directory for the structured run results (report.json + evidence PNGs) that are always written. Default: ${DEFAULT_REPORT_DIR}/. Pass this only to change the location.`).option("--push-report", "Incrementally push the run report to the hub as the run progresses (open → patch per spec → finalize). Requires --hub-url/--hub-token (or CCQA_HUB_URL/CCQA_HUB_TOKEN). Without it, hub credentials are used only to fetch variables/sessions/prompts, not to push.").option("--changed [base]", "Restrict execution to specs whose relatedPaths intersect the git diff against [base]. Without a value the base comes from $GITHUB_BASE_REF (pull_request CI); elsewhere pass it explicitly (e.g. --changed=origin/main). Cannot be combined with an explicit spec id.").option("--failure-analysis [base]", "Classify each failure (TEST_DRIFT / SPEC_CHANGE / PRODUCT_BUG) against the source diff since [base]. Without a value the base comes from $GITHUB_BASE_REF (pull_request CI); elsewhere pass it explicitly (e.g. --failure-analysis=origin/main), or pass 'last-green' to diff each spec against the commit where it last passed (per-spec baselines from the hub; requires a hub connection). Off by default — no Claude calls without it.").option("--
|
|
10274
|
+
const runCommand = addHubOptions(addProfileOption(addLanguageOption(new Command("run").argument("[targets...]", "Specs to run, space-separated: each '<feature>/<spec>', '<feature>', or omit for all. Duplicates are de-duped.").description("Run specs. Each spec's execution mode comes from its spec.yaml `mode:` field (default deterministic; set `mode: live` to have Claude drive agent-browser live per step). Deterministic specs replay the recorded test.spec.ts under vitest. A structured report (report.json + evidence) is always written; use --push-report to also stream it to a hub.").option("--report [dir]", `Directory for the structured run results (report.json + evidence PNGs) that are always written. Default: ${DEFAULT_REPORT_DIR}/. Pass this only to change the location.`).option("--push-report", "Incrementally push the run report to the hub as the run progresses (open → patch per spec → finalize). Requires --hub-url/--hub-token (or CCQA_HUB_URL/CCQA_HUB_TOKEN). Without it, hub credentials are used only to fetch variables/sessions/prompts, not to push.").option("--changed [base]", "Restrict execution to specs whose relatedPaths intersect the git diff against [base]. Without a value the base comes from $GITHUB_BASE_REF (pull_request CI); elsewhere pass it explicitly (e.g. --changed=origin/main). Cannot be combined with an explicit spec id.").option("--failure-analysis [base]", "Classify each failure (TEST_DRIFT / SPEC_CHANGE / PRODUCT_BUG) against the source diff since [base]. Without a value the base comes from $GITHUB_BASE_REF (pull_request CI); elsewhere pass it explicitly (e.g. --failure-analysis=origin/main), or pass 'last-green' to diff each spec against the commit where it last passed (per-spec baselines from the hub; requires a hub connection). Off by default — no Claude calls without it.").option("--cwd <path>", "Working directory containing the .ccqa/ tree (monorepo support). Defaults to the current directory.").option("--format <fmt>", "Additional output format alongside HTML when --report is set: 'text' (default), 'json' (writes report.json), 'github' (GitHub Actions annotations on stdout).", (raw) => {
|
|
10216
10275
|
if (REPORT_FORMATS.includes(raw)) return raw;
|
|
10217
10276
|
throw new Error(`--format must be one of ${REPORT_FORMATS.join(" | ")}`);
|
|
10218
10277
|
}, "text").option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--no-evidence", `(deterministic only) Skip step-boundary evidence capture (PNG + meta JSON written to ${DEFAULT_REPORT_DIR}/${EVIDENCE_SUBDIR}/ by default).`).option("--retry <n>", "(live only) Retry each failed step up to N more times before recording failure. Default 0.", (raw) => {
|
|
@@ -15041,21 +15100,25 @@ const HTML_BODY = `
|
|
|
15041
15100
|
<div class="rd-head" id="rd-head"></div>
|
|
15042
15101
|
|
|
15043
15102
|
<!-- Triage first: grading + learning is the most important action, so it
|
|
15044
|
-
sits above the spec list rather than being buried below it.
|
|
15103
|
+
sits above the spec list rather than being buried below it. The
|
|
15104
|
+
heading row carries the graded counter; one card below it holds
|
|
15105
|
+
the confusion matrix (or its empty state) with the learn CTA as
|
|
15106
|
+
the card footer — grades are the learning job's input, so
|
|
15107
|
+
grade → tally → learn reads top to bottom. -->
|
|
15045
15108
|
<div class="triage-head" id="triage-head">
|
|
15046
15109
|
<h3 style="font-size:14px" data-i18n="detail.triage">Triage</h3>
|
|
15047
15110
|
<span class="triage-summary" id="triage-summary"></span>
|
|
15048
15111
|
</div>
|
|
15049
|
-
<div class="card" id="
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
|
|
15057
|
-
|
|
15058
|
-
|
|
15112
|
+
<div class="card triage-card" id="triage-card">
|
|
15113
|
+
<div id="matrix-card"></div>
|
|
15114
|
+
<div class="learn-cta" id="learn-cta" hidden>
|
|
15115
|
+
<div class="learn-cta-text">
|
|
15116
|
+
<div class="t" data-i18n="learn.cta.title">Learn from these grades</div>
|
|
15117
|
+
<div class="d" data-i18n="learn.cta.desc">Turn the graded cases into a custom prompt that calibrates future failure classification.</div>
|
|
15118
|
+
</div>
|
|
15119
|
+
<div class="learn-cta-actions">
|
|
15120
|
+
<button class="btn primary sm" id="learn-run" data-i18n="learn.cta.run">Learn</button>
|
|
15121
|
+
</div>
|
|
15059
15122
|
</div>
|
|
15060
15123
|
</div>
|
|
15061
15124
|
|
|
@@ -15379,8 +15442,6 @@ const CSS = `
|
|
|
15379
15442
|
.badge-det { background: var(--surface-3); color: var(--muted); border-color: var(--border); }
|
|
15380
15443
|
/* which generation target ran the spec (agent-browser / playwright / runn) */
|
|
15381
15444
|
.badge-target { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: var(--radius-sm); font-size: 11px; font-family: var(--mono); background: var(--surface-3); color: var(--muted); border: 1px solid var(--border); }
|
|
15382
|
-
.badge.drift-warn { background: var(--amber-bg); color: var(--amber); border-color: var(--amber-border); }
|
|
15383
|
-
.badge.drift-warn .d { background: var(--amber); }
|
|
15384
15445
|
.badge-drift { background: var(--violet-bg); color: var(--violet); border-color: var(--violet-border); }
|
|
15385
15446
|
.chip { display: inline-flex; align-items: center; padding: 1px 8px; border-radius: 6px; background: var(--surface-3); border: 1px solid var(--border); color: var(--fg-dim); font-size: 12px; font-family: var(--mono); }
|
|
15386
15447
|
/* Below .chip in source order so these override its background/border/color
|
|
@@ -15423,11 +15484,18 @@ const CSS = `
|
|
|
15423
15484
|
.spec-card-head .spacer { flex: 1; }
|
|
15424
15485
|
.spec-card-body { padding: 0 20px 16px; }
|
|
15425
15486
|
/* Tier2 verdict block */
|
|
15426
|
-
|
|
15487
|
+
/* The diagnosis card: a bordered sub-surface so the model's verdict + the
|
|
15488
|
+
grading zone read as one unit, distinct from the execution details
|
|
15489
|
+
(steps/assertions) below it. */
|
|
15490
|
+
.analysis-box { display: flex; flex-direction: column; gap: 12px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-2); padding: 14px 16px; }
|
|
15491
|
+
.analysis-box .acc > summary:hover { background: var(--surface-3); }
|
|
15427
15492
|
.analysis-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
15428
|
-
.analysis-
|
|
15429
|
-
.analysis-
|
|
15430
|
-
.analysis-
|
|
15493
|
+
.analysis-kv { display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; font-size: 13.5px; }
|
|
15494
|
+
.analysis-kv .k { font-size: 11px; font-weight: 600; color: var(--muted); padding-top: 3px; white-space: nowrap; }
|
|
15495
|
+
.analysis-kv .v { color: var(--fg-dim); line-height: 1.55; }
|
|
15496
|
+
.analysis-kv .v.headline { color: var(--fg); font-weight: 600; }
|
|
15497
|
+
/* Model-evidence rows reuse the drift-row list shape; only the file ref needs its own style. */
|
|
15498
|
+
.ev-file { font-size: 12px; color: var(--fg-dim); }
|
|
15431
15499
|
.analysis-reasoning { font-size: 13px; color: var(--fg-dim); white-space: pre-wrap; line-height: 1.6; }
|
|
15432
15500
|
.analysis-inline-reason { font-size: 13px; color: var(--fg-dim); line-height: 1.55; }
|
|
15433
15501
|
/* Tier3 accordion (real header bar + rotating chevron, replaces the tiny ▸) */
|
|
@@ -15493,11 +15561,12 @@ const CSS = `
|
|
|
15493
15561
|
|
|
15494
15562
|
/* triage grading — an explicit question + a segmented single-select, framed
|
|
15495
15563
|
as an action ("tell us the real cause"), not a data readout. */
|
|
15496
|
-
|
|
15564
|
+
/* Embedded at the bottom of the diagnosis card: a divider separates the
|
|
15565
|
+
human's grading zone from the model's output above it, without breaking
|
|
15566
|
+
the two out of the shared context. */
|
|
15567
|
+
.grade { margin-top: 2px; padding: 12px 0 0; border-top: 1px solid var(--border); }
|
|
15497
15568
|
.grade-top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
15498
15569
|
.grade-q { font-size: 13px; font-weight: 600; color: var(--fg); }
|
|
15499
|
-
.grade-pred { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; font-size: 12px; color: var(--muted); }
|
|
15500
|
-
.grade-pred .grade-arrow { color: var(--muted-2); }
|
|
15501
15570
|
.grade-bottom { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
15502
15571
|
.grade-seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--radius-md); overflow: hidden; background: var(--surface); }
|
|
15503
15572
|
.grade-seg .seg { height: 34px; padding: 0 14px; border: 0; background: transparent; color: var(--muted); font-size: 13px; font-weight: 500; border-right: 1px solid var(--border); display: inline-flex; align-items: center; gap: 5px; }
|
|
@@ -15635,7 +15704,7 @@ const CSS = `
|
|
|
15635
15704
|
.ro-tag { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
|
|
15636
15705
|
color: var(--muted-2); background: var(--surface-3); border: 1px solid var(--border); border-radius: 999px; padding: 1px 8px; margin-left: 4px; }
|
|
15637
15706
|
|
|
15638
|
-
.learn-cta { display: flex; align-items: center; gap: 16px;
|
|
15707
|
+
.learn-cta { display: flex; align-items: center; gap: 16px; padding: 14px 20px; border-top: 1px solid var(--border); background: var(--surface-2); border-radius: 0 0 var(--radius-md) var(--radius-md); }
|
|
15639
15708
|
.learn-cta-text { flex: 1; min-width: 0; }
|
|
15640
15709
|
.learn-cta-text .t { font-size: 13.5px; font-weight: 600; color: var(--fg); }
|
|
15641
15710
|
.learn-cta-text .d { font-size: 12px; color: var(--muted); margin-top: 3px; }
|
|
@@ -15756,26 +15825,25 @@ const CLIENT_JS = `
|
|
|
15756
15825
|
"meta.branch": "Branch", "meta.specs": "Specs", "meta.prompt": "Prompt",
|
|
15757
15826
|
"meta.created": "Created", "meta.passed": "passed", "meta.profile": "Profile",
|
|
15758
15827
|
"meta.drift": "Drift",
|
|
15759
|
-
"
|
|
15828
|
+
"diag.cause": "Cause", "diag.fix": "Fix",
|
|
15760
15829
|
"acc.reasoning": "Reasoning", "acc.evidence": "Evidence", "acc.steps": "Live run steps",
|
|
15761
15830
|
"acc.assertions": "Assertions", "acc.drift": "Drift audit",
|
|
15762
15831
|
"acc.artifacts": "Artifacts",
|
|
15763
15832
|
"art.open": "Open", "art.loadFailed": "could not load (it may have been omitted from the push)",
|
|
15764
15833
|
"acc.assertions.hint": "Test cases from the recorded spec run",
|
|
15765
15834
|
"spec.kind.live": "Live", "spec.kind.det": "Deterministic",
|
|
15766
|
-
"
|
|
15835
|
+
"det.steps": "Steps",
|
|
15767
15836
|
"kind.run": "Test run", "kind.drift": "Drift audit",
|
|
15768
15837
|
"drift.summary.issues": "Issues", "drift.summary.errors": "Errors",
|
|
15769
15838
|
"drift.summary.warnings": "Warnings", "drift.summary.specsWithIssues": "Specs with issues",
|
|
15770
15839
|
"drift.clean": "No drift issues",
|
|
15771
|
-
"grade.question": "What was the real cause?",
|
|
15840
|
+
"grade.question": "What was the real cause?",
|
|
15772
15841
|
"grade.ungraded": "ungraded", "grade.matches": "saved · matches",
|
|
15773
15842
|
"grade.corrected": "saved · corrected", "grade.saving": "saving…",
|
|
15774
15843
|
"grade.error": "couldn't save — retry",
|
|
15775
|
-
"matrix.empty": "No
|
|
15844
|
+
"matrix.empty": "No grades yet. Pick the real cause on a failed spec's diagnosis card below and it is tallied here.",
|
|
15776
15845
|
"matrix.predicted": "predicted \\\\ actual", "matrix.accuracy": "Accuracy",
|
|
15777
15846
|
"matrix.accSuffix": "of graded cases match the prediction", "matrix.graded": "graded",
|
|
15778
|
-
"matrix.progress": "Recorded actual cause: {n} / {total} failing specs",
|
|
15779
15847
|
"learn.cta.title": "Learn from these grades",
|
|
15780
15848
|
"learn.cta.desc": "Learn from what you graded so ccqa classifies failure causes the same way next time.",
|
|
15781
15849
|
"learn.cta.run": "Learn",
|
|
@@ -15846,26 +15914,25 @@ const CLIENT_JS = `
|
|
|
15846
15914
|
"meta.branch": "ブランチ", "meta.specs": "スペック", "meta.prompt": "プロンプト",
|
|
15847
15915
|
"meta.created": "作成", "meta.passed": "合格", "meta.profile": "プロファイル",
|
|
15848
15916
|
"meta.drift": "ドリフト",
|
|
15849
|
-
"
|
|
15917
|
+
"diag.cause": "原因", "diag.fix": "対処",
|
|
15850
15918
|
"acc.reasoning": "推論", "acc.evidence": "根拠", "acc.steps": "実行ステップ",
|
|
15851
15919
|
"acc.assertions": "アサーション", "acc.drift": "ドリフト監査",
|
|
15852
15920
|
"acc.artifacts": "成果物",
|
|
15853
15921
|
"art.open": "開く", "art.loadFailed": "読み込めませんでした(push時に省略された可能性があります)",
|
|
15854
15922
|
"acc.assertions.hint": "記録したスペック実行のテストケース",
|
|
15855
15923
|
"spec.kind.live": "ライブ", "spec.kind.det": "決定的",
|
|
15856
|
-
"
|
|
15924
|
+
"det.steps": "ステップ",
|
|
15857
15925
|
"kind.run": "テスト実行", "kind.drift": "ドリフト監査",
|
|
15858
15926
|
"drift.summary.issues": "問題数", "drift.summary.errors": "エラー",
|
|
15859
15927
|
"drift.summary.warnings": "警告", "drift.summary.specsWithIssues": "問題のあるスペック",
|
|
15860
15928
|
"drift.clean": "ドリフトの問題なし",
|
|
15861
|
-
"grade.question": "実際の原因は何でしたか?",
|
|
15929
|
+
"grade.question": "実際の原因は何でしたか?",
|
|
15862
15930
|
"grade.ungraded": "未評価", "grade.matches": "保存済み · 一致",
|
|
15863
15931
|
"grade.corrected": "保存済み · 修正", "grade.saving": "保存中…",
|
|
15864
15932
|
"grade.error": "保存に失敗 — 再試行",
|
|
15865
|
-
"matrix.empty": "
|
|
15933
|
+
"matrix.empty": "まだ採点がありません。下の失敗スペックの診断カードで実際の原因を選ぶと、ここに集計されます。",
|
|
15866
15934
|
"matrix.predicted": "予測 \\\\ 実際", "matrix.accuracy": "正解率",
|
|
15867
15935
|
"matrix.accSuffix": "件の採点が予測と一致", "matrix.graded": "採点済み",
|
|
15868
|
-
"matrix.progress": "実際の原因を記録: {total} 件中 {n} 件の失敗スペック",
|
|
15869
15936
|
"learn.cta.title": "この採点から学習",
|
|
15870
15937
|
"learn.cta.desc": "採点した内容をもとに、ccqaが次回から同じように失敗の原因を分類できるよう学習します。",
|
|
15871
15938
|
"learn.cta.run": "学習",
|
|
@@ -16465,27 +16532,61 @@ const CLIENT_JS = `
|
|
|
16465
16532
|
|
|
16466
16533
|
// ── run detail: spec cards ──────────────────────────────────────────
|
|
16467
16534
|
|
|
16468
|
-
//
|
|
16469
|
-
//
|
|
16470
|
-
//
|
|
16535
|
+
// The diagnosis card: one surface for everything about a failure's cause.
|
|
16536
|
+
// Verdict (label + confidence), then the cause→fix pair as labelled rows —
|
|
16537
|
+
// headline and recommendation are one causal unit, so they read as one.
|
|
16538
|
+
// subDiagnosis is deliberately NOT shown: it is a machine vocabulary for
|
|
16539
|
+
// accuracy stratification and learning, not for humans. The caller appends
|
|
16540
|
+
// the evidence/reasoning accordions and the grading zone into this box.
|
|
16471
16541
|
function analysisSection(runId, r) {
|
|
16472
16542
|
var wrap = el("div", "analysis-box");
|
|
16473
16543
|
var a = r.analysis;
|
|
16474
16544
|
var head = el("div", "analysis-head");
|
|
16475
16545
|
head.appendChild(labelChip(a.label));
|
|
16476
16546
|
head.appendChild(el("span", "conf", Math.round(a.confidence * 100) + "%"));
|
|
16477
|
-
if (a.subDiagnosis && a.subDiagnosis !== "NONE") head.appendChild(el("span", "muted", a.subDiagnosis));
|
|
16478
16547
|
wrap.appendChild(head);
|
|
16479
|
-
|
|
16548
|
+
var kv = el("div", "analysis-kv");
|
|
16549
|
+
if (a.headline) {
|
|
16550
|
+
kv.appendChild(el("div", "k", t("diag.cause")));
|
|
16551
|
+
kv.appendChild(el("div", "v headline", a.headline));
|
|
16552
|
+
}
|
|
16480
16553
|
if (a.recommendation) {
|
|
16481
|
-
|
|
16482
|
-
|
|
16483
|
-
rec.appendChild(document.createTextNode(a.recommendation));
|
|
16484
|
-
wrap.appendChild(rec);
|
|
16554
|
+
kv.appendChild(el("div", "k", t("diag.fix")));
|
|
16555
|
+
kv.appendChild(el("div", "v", a.recommendation));
|
|
16485
16556
|
}
|
|
16557
|
+
if (kv.childNodes.length > 0) wrap.appendChild(kv);
|
|
16486
16558
|
return wrap;
|
|
16487
16559
|
}
|
|
16488
16560
|
|
|
16561
|
+
// 根拠: the model's evidence items (file + what it proves) merged with the
|
|
16562
|
+
// drift-audit findings. The audit is an input hint TO the classifier, so
|
|
16563
|
+
// its findings belong here as supporting evidence — not as a sibling
|
|
16564
|
+
// section that reads like an independent feature.
|
|
16565
|
+
function analysisEvidenceSection(r) {
|
|
16566
|
+
var wrap = el("div");
|
|
16567
|
+
var count = 0;
|
|
16568
|
+
var items = r.analysis && r.analysis.evidence ? r.analysis.evidence : [];
|
|
16569
|
+
items.forEach(function (e) {
|
|
16570
|
+
// Same list shape as the drift findings below, so the merged 根拠 list
|
|
16571
|
+
// reads as one.
|
|
16572
|
+
var row = el("div", "drift-row");
|
|
16573
|
+
if (e.file) {
|
|
16574
|
+
var head = el("div", "drift-head");
|
|
16575
|
+
head.appendChild(el("code", "ev-file", e.file));
|
|
16576
|
+
row.appendChild(head);
|
|
16577
|
+
}
|
|
16578
|
+
row.appendChild(el("div", "drift-msg", e.detail));
|
|
16579
|
+
wrap.appendChild(row);
|
|
16580
|
+
count++;
|
|
16581
|
+
});
|
|
16582
|
+
if (r.driftIssues && r.driftIssues.length > 0) {
|
|
16583
|
+
wrap.appendChild(el("div", "section-label", t("acc.drift")));
|
|
16584
|
+
wrap.appendChild(driftSection(r.driftIssues));
|
|
16585
|
+
count += r.driftIssues.length;
|
|
16586
|
+
}
|
|
16587
|
+
return { node: wrap, count: count };
|
|
16588
|
+
}
|
|
16589
|
+
|
|
16489
16590
|
function evidenceSection(runId, evidence) {
|
|
16490
16591
|
var grid = el("div", "evidence-grid");
|
|
16491
16592
|
evidence.forEach(function (e) {
|
|
@@ -16717,25 +16818,22 @@ const CLIENT_JS = `
|
|
|
16717
16818
|
return det;
|
|
16718
16819
|
}
|
|
16719
16820
|
|
|
16720
|
-
// The grading action: an explicit question ("What was the real cause?")
|
|
16721
|
-
//
|
|
16722
|
-
//
|
|
16723
|
-
//
|
|
16724
|
-
//
|
|
16725
|
-
//
|
|
16821
|
+
// The grading action: an explicit question ("What was the real cause?"), a
|
|
16822
|
+
// segmented single-select over the failure labels, and a status chip
|
|
16823
|
+
// (ungraded / saved·matches / saved·corrected). One tap grades it.
|
|
16824
|
+
// Optimistic PUT with rollback; on success it refreshes the confusion
|
|
16825
|
+
// matrix. The English label value is what's sent and stored; the segment
|
|
16826
|
+
// just shows its localized name.
|
|
16726
16827
|
function triageGradeControl(runId, r, triageState) {
|
|
16727
16828
|
var key = r.feature + "/" + r.spec;
|
|
16728
16829
|
var predicted = r.analysis ? r.analysis.label : "UNKNOWN";
|
|
16729
16830
|
|
|
16730
16831
|
var wrap = el("div", "grade");
|
|
16731
16832
|
|
|
16833
|
+
// No "predicted →" chip here: the control lives inside the diagnosis
|
|
16834
|
+
// card, directly under the prediction it grades — repeating it is noise.
|
|
16732
16835
|
var top = el("div", "grade-top");
|
|
16733
16836
|
top.appendChild(el("span", "grade-q", t("grade.question")));
|
|
16734
|
-
var pred = el("span", "grade-pred");
|
|
16735
|
-
pred.appendChild(document.createTextNode(t("grade.predicted")));
|
|
16736
|
-
pred.appendChild(el("span", "grade-arrow", "→"));
|
|
16737
|
-
pred.appendChild(labelChip(predicted));
|
|
16738
|
-
top.appendChild(pred);
|
|
16739
16837
|
wrap.appendChild(top);
|
|
16740
16838
|
|
|
16741
16839
|
var bottom = el("div", "grade-bottom");
|
|
@@ -16819,13 +16917,6 @@ const CLIENT_JS = `
|
|
|
16819
16917
|
else if (r.liveRun) head.appendChild(el("span", "badge-live", t("spec.kind.live")));
|
|
16820
16918
|
else if (!external) head.appendChild(el("span", "badge-det", t("spec.kind.det")));
|
|
16821
16919
|
head.appendChild(statusBadge(r.status));
|
|
16822
|
-
var hasDriftError = r.driftIssues && r.driftIssues.some(function (d) { return d.severity === "ERROR"; });
|
|
16823
|
-
if (hasDriftError) {
|
|
16824
|
-
var w = el("span", "badge drift-warn");
|
|
16825
|
-
w.appendChild(el("span", "d"));
|
|
16826
|
-
w.appendChild(document.createTextNode(" " + t("spec.driftWarn")));
|
|
16827
|
-
head.appendChild(w);
|
|
16828
|
-
}
|
|
16829
16920
|
card.appendChild(head);
|
|
16830
16921
|
|
|
16831
16922
|
var body = el("div", "spec-card-body");
|
|
@@ -16836,17 +16927,23 @@ const CLIENT_JS = `
|
|
|
16836
16927
|
any = true;
|
|
16837
16928
|
}
|
|
16838
16929
|
|
|
16839
|
-
|
|
16840
|
-
|
|
16841
|
-
|
|
16842
|
-
|
|
16843
|
-
//
|
|
16930
|
+
var hasAnalysis = r.status === "failed" && r.analysis;
|
|
16931
|
+
if (hasAnalysis) {
|
|
16932
|
+
// The diagnosis card: verdict + cause/fix, then evidence and reasoning
|
|
16933
|
+
// as accordions, then the grading zone — one surface for the whole
|
|
16934
|
+
// "why did this fail and was the call right" story.
|
|
16935
|
+
var box = analysisSection(runId, r);
|
|
16936
|
+
var ev = analysisEvidenceSection(r);
|
|
16937
|
+
if (ev.count > 0) box.appendChild(detailsBlock(t("acc.evidence"), ev.count, ev.node));
|
|
16938
|
+
// Reasoning: fold it as an accordion, but only when it carries real
|
|
16844
16939
|
// content. A one-char/empty reasoning behind a disclosure reads as broken
|
|
16845
16940
|
// (the old "▸ r"), so drop it entirely below the threshold.
|
|
16846
16941
|
var reasoning = r.analysis.reasoning ? String(r.analysis.reasoning).trim() : "";
|
|
16847
16942
|
if (reasoning.length > 2) {
|
|
16848
|
-
|
|
16943
|
+
box.appendChild(detailsBlock(t("acc.reasoning"), null, el("div", "analysis-reasoning", reasoning)));
|
|
16849
16944
|
}
|
|
16945
|
+
box.appendChild(triageGradeControl(runId, r, triageState));
|
|
16946
|
+
body.appendChild(box);
|
|
16850
16947
|
any = true;
|
|
16851
16948
|
} else if (r.status === "failed" && r.analysisSkipped) {
|
|
16852
16949
|
body.appendChild(el("div", "muted", "Analysis skipped: " + r.analysisSkipped));
|
|
@@ -16878,7 +16975,10 @@ const CLIENT_JS = `
|
|
|
16878
16975
|
any = true;
|
|
16879
16976
|
}
|
|
16880
16977
|
|
|
16881
|
-
|
|
16978
|
+
// Drift findings render standalone only when they aren't already folded
|
|
16979
|
+
// into the diagnosis card's evidence: drift-kind runs (the audit IS the
|
|
16980
|
+
// content) and failed rows whose analysis was skipped.
|
|
16981
|
+
if (!hasAnalysis && r.driftIssues && r.driftIssues.length > 0) {
|
|
16882
16982
|
body.appendChild(detailsBlock(t("acc.drift"), r.driftIssues.length, driftSection(r.driftIssues)));
|
|
16883
16983
|
any = true;
|
|
16884
16984
|
}
|
|
@@ -16913,12 +17013,9 @@ const CLIENT_JS = `
|
|
|
16913
17013
|
var cases = Object.keys(triageState.byKey).map(function (k) { return triageState.byKey[k]; })
|
|
16914
17014
|
.filter(function (c) { return c.predicted && c.actual; });
|
|
16915
17015
|
|
|
16916
|
-
//
|
|
16917
|
-
//
|
|
17016
|
+
// The "graded m / n" counter in the header is the single progress
|
|
17017
|
+
// readout — recomputed here so it stays in sync after each grade.
|
|
16918
17018
|
var total = typeof triageState.total === "number" ? triageState.total : Object.keys(triageState.byKey).length;
|
|
16919
|
-
document.getElementById("triage-progress").textContent =
|
|
16920
|
-
t("matrix.progress").replace("{n}", cases.length).replace("{total}", total);
|
|
16921
|
-
|
|
16922
17019
|
var summary = document.getElementById("triage-summary");
|
|
16923
17020
|
|
|
16924
17021
|
if (cases.length === 0) {
|
|
@@ -16989,7 +17086,12 @@ const CLIENT_JS = `
|
|
|
16989
17086
|
renderMatrix(triageState);
|
|
16990
17087
|
onLoaded(triageState);
|
|
16991
17088
|
}).catch(function (err) {
|
|
16992
|
-
|
|
17089
|
+
// Surface the load failure inside the triage card so it isn't silent.
|
|
17090
|
+
var card = document.getElementById("matrix-card");
|
|
17091
|
+
clear(card);
|
|
17092
|
+
var wrap = el("div", "matrix-wrap");
|
|
17093
|
+
wrap.appendChild(el("div", "muted", "Error loading triage: " + err.message));
|
|
17094
|
+
card.appendChild(wrap);
|
|
16993
17095
|
onLoaded({ byKey: {} });
|
|
16994
17096
|
});
|
|
16995
17097
|
}
|
|
@@ -17015,7 +17117,6 @@ const CLIENT_JS = `
|
|
|
17015
17117
|
clear(document.getElementById("rd-head"));
|
|
17016
17118
|
clear(document.getElementById("matrix-card"));
|
|
17017
17119
|
document.getElementById("detail-spec-count").textContent = "";
|
|
17018
|
-
document.getElementById("triage-progress").textContent = "";
|
|
17019
17120
|
document.getElementById("triage-summary").textContent = "";
|
|
17020
17121
|
|
|
17021
17122
|
apiFetch("/api/v1/runs/" + encodeURIComponent(runId)).then(function (run) {
|
|
@@ -17029,8 +17130,7 @@ const CLIENT_JS = `
|
|
|
17029
17130
|
// as the other, and neither escapes its own catch.
|
|
17030
17131
|
var isDrift = report.kind === "drift";
|
|
17031
17132
|
document.getElementById("triage-head").hidden = isDrift;
|
|
17032
|
-
document.getElementById("
|
|
17033
|
-
document.getElementById("triage-progress").hidden = isDrift;
|
|
17133
|
+
document.getElementById("triage-card").hidden = isDrift;
|
|
17034
17134
|
renderSpecCards(runId, report.results, { byKey: {} }, isDrift);
|
|
17035
17135
|
if (isDrift) return; // drift runs have no triage: skip loadTriage entirely
|
|
17036
17136
|
loadTriage(runId, function (triageState) {
|
|
@@ -18409,7 +18509,7 @@ function createLearningWorker(deps) {
|
|
|
18409
18509
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
18410
18510
|
const customPrompt = {
|
|
18411
18511
|
schemaVersion: 1,
|
|
18412
|
-
basePromptVersion: "
|
|
18512
|
+
basePromptVersion: "6",
|
|
18413
18513
|
customPromptVersion: `${generatedAt}-c${cases.length}`,
|
|
18414
18514
|
generatedAt,
|
|
18415
18515
|
guidance
|
package/dist/package.json
CHANGED