ccqa 1.4.0 → 1.5.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 +99 -36
- 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
|
|
3276
|
+
${executionBlock}
|
|
3252
3277
|
|
|
3253
|
-
|
|
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:
|
|
3258
|
-
|
|
3259
|
-
${driftIssues.map((i) => `- [${i.severity}] (${DRAFT_CATEGORY_LABEL[i.category]}${i.stepId ? `, step ${i.stepId}` : ""}) ${i.message}${i.detail ? ` — ${i.detail}` : ""}`).join("\n")}
|
|
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
|
} };
|
|
@@ -6146,6 +6205,8 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
|
|
|
6146
6205
|
diffPatch: specDiff.patch,
|
|
6147
6206
|
changedFiles: specDiff.nameStatus,
|
|
6148
6207
|
baseRef: specDiff.base.ref,
|
|
6208
|
+
baseSource: specDiff.base.source,
|
|
6209
|
+
range: specDiff.range,
|
|
6149
6210
|
driftIssues: driftForSpec,
|
|
6150
6211
|
...opts.language ? { outputLanguage: opts.language } : {},
|
|
6151
6212
|
...opts.triageUserPrompt ? { triageUserPrompt: opts.triageUserPrompt } : {},
|
|
@@ -9823,6 +9884,8 @@ async function analyzeDeterministicSummaries(summaries, opts, cwd, reportDir, cu
|
|
|
9823
9884
|
diffPatch: diffExcerpt,
|
|
9824
9885
|
changedFiles: specDiffResult.nameStatus,
|
|
9825
9886
|
baseRef: specDiffResult.base.ref,
|
|
9887
|
+
baseSource: specDiffResult.base.source,
|
|
9888
|
+
range: specDiffResult.range,
|
|
9826
9889
|
driftIssues,
|
|
9827
9890
|
...opts.language ? { outputLanguage: opts.language } : {},
|
|
9828
9891
|
...triageUserPrompt ? { triageUserPrompt } : {},
|
|
@@ -9888,7 +9951,7 @@ function buildReportEnvelope(args) {
|
|
|
9888
9951
|
},
|
|
9889
9952
|
model: opts.model ?? null,
|
|
9890
9953
|
language: opts.language ?? null,
|
|
9891
|
-
promptVersion: "
|
|
9954
|
+
promptVersion: "6",
|
|
9892
9955
|
customPromptVersion,
|
|
9893
9956
|
...triageUserPromptHash !== null ? { triageUserPromptHash } : {}
|
|
9894
9957
|
};
|
|
@@ -18409,7 +18472,7 @@ function createLearningWorker(deps) {
|
|
|
18409
18472
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
18410
18473
|
const customPrompt = {
|
|
18411
18474
|
schemaVersion: 1,
|
|
18412
|
-
basePromptVersion: "
|
|
18475
|
+
basePromptVersion: "6",
|
|
18413
18476
|
customPromptVersion: `${generatedAt}-c${cases.length}`,
|
|
18414
18477
|
generatedAt,
|
|
18415
18478
|
guidance
|
package/dist/package.json
CHANGED