ccqa 1.60.1 → 1.60.2
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 +139 -41
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -1650,10 +1650,10 @@ async function saveRecording(ref, actions, cleanup = [], stepDigests) {
|
|
|
1650
1650
|
*
|
|
1651
1651
|
* One directory, not one per attempt: a project whose tests belong to its own
|
|
1652
1652
|
* runner never calls `ccqa run`, so this stands in for the run report the
|
|
1653
|
-
* evidence table reads — and what stands in for it is the attempt that passed
|
|
1654
|
-
*
|
|
1655
|
-
*
|
|
1656
|
-
*
|
|
1653
|
+
* evidence table reads — and what stands in for it is the attempt that passed
|
|
1654
|
+
* last, the version generation keeps. `ccqa generate` replaces it only on a
|
|
1655
|
+
* pass and removes it when none passed, because screenshots of a failing run
|
|
1656
|
+
* are a debugging aid and this table presents them as the case working.
|
|
1657
1657
|
*/
|
|
1658
1658
|
function caseRunDir(ref) {
|
|
1659
1659
|
return join(ref.dir, EVIDENCE_DIR);
|
|
@@ -6535,7 +6535,7 @@ function parseStepComment(line) {
|
|
|
6535
6535
|
* every step then reads as deciding nothing and every rewrite is rejected.
|
|
6536
6536
|
*/
|
|
6537
6537
|
function stepLines(source) {
|
|
6538
|
-
return source.replace(STEP_CALL, (call) => call.replace(/\s*\n\s*/g, " ")).split("\n");
|
|
6538
|
+
return source.replace(STEP_CALL, (call) => "\n".repeat(call.split("\n").length - 1) + call.replace(/\s*\n\s*/g, " ")).split("\n");
|
|
6539
6539
|
}
|
|
6540
6540
|
/**
|
|
6541
6541
|
* The step id a `test.step` title names — the block form only.
|
|
@@ -8338,6 +8338,31 @@ function exportedNames(source) {
|
|
|
8338
8338
|
}
|
|
8339
8339
|
//#endregion
|
|
8340
8340
|
//#region src/targets/emitted-review.ts
|
|
8341
|
+
/**
|
|
8342
|
+
* A mechanical read of the code a generation just wrote, before it is called
|
|
8343
|
+
* done.
|
|
8344
|
+
*
|
|
8345
|
+
* The fix loop already runs the project's own test command and checks, so
|
|
8346
|
+
* what reaches review is code that compiles, lints and passes. None of that
|
|
8347
|
+
* answers whether it is code a reviewer would take: an assertion scoped to
|
|
8348
|
+
* nothing passes, a locator named for a row that matches page-wide text
|
|
8349
|
+
* passes, a test that checks five things the case never asked about passes.
|
|
8350
|
+
* Those are what this reads for, and its findings go back through the same
|
|
8351
|
+
* fix loop rather than to a human.
|
|
8352
|
+
*
|
|
8353
|
+
* Every rule here was calibrated against a large hand-written suite: a rule
|
|
8354
|
+
* that fires on code people wrote and reviewed is a rule that is wrong,
|
|
8355
|
+
* however reasonable it sounds. Two candidates died that way —
|
|
8356
|
+
* "a locator named `…Row` must use `getByRole("row")`" fired across a
|
|
8357
|
+
* sizable share of the hand-written page objects, because that suite names
|
|
8358
|
+
* elements for what they look like rather than for their ARIA role, and
|
|
8359
|
+
* "never `.first()` in an assertion" fired on deliberate uses that each
|
|
8360
|
+
* carried the comment their own guidelines ask for. A third died against
|
|
8361
|
+
* ccqa's own output: the step comment a generated test opens each step with
|
|
8362
|
+
* looked like narration, and is what the evidence table reads back to say
|
|
8363
|
+
* which assertion belongs to which step. Keep that bar for anything added
|
|
8364
|
+
* here.
|
|
8365
|
+
*/
|
|
8341
8366
|
function reviewEmittedFiles(input) {
|
|
8342
8367
|
const locators = collectLocators(input.files);
|
|
8343
8368
|
const said = input.caseText.join("\n");
|
|
@@ -8346,7 +8371,7 @@ function reviewEmittedFiles(input) {
|
|
|
8346
8371
|
...containerOfPageText(file, source),
|
|
8347
8372
|
...unjustifiedFirst(file, source),
|
|
8348
8373
|
...describeEcho(file, source),
|
|
8349
|
-
...unaskedAssertions(file, source, locators, said),
|
|
8374
|
+
...unaskedAssertions(file, source, locators, said, input.blockSteps),
|
|
8350
8375
|
...weakerTwin(file, source),
|
|
8351
8376
|
...unreached(file, source, input)
|
|
8352
8377
|
])];
|
|
@@ -8601,9 +8626,26 @@ function assertedStrings(text, locators) {
|
|
|
8601
8626
|
* precondition broke rather than four steps later, is worth doing and is
|
|
8602
8627
|
* written down when it is.
|
|
8603
8628
|
*/
|
|
8604
|
-
function unaskedAssertions(file, source, locators, said) {
|
|
8629
|
+
function unaskedAssertions(file, source, locators, said, blockSteps = /* @__PURE__ */ new Set()) {
|
|
8605
8630
|
const all = lines(source);
|
|
8631
|
+
const boundaries = stepLines(source);
|
|
8632
|
+
let step = null;
|
|
8633
|
+
let depth = null;
|
|
8606
8634
|
return all.flatMap((text, i) => {
|
|
8635
|
+
const opened = parseStepComment(boundaries[i] ?? "");
|
|
8636
|
+
if (opened !== null) {
|
|
8637
|
+
step = opened;
|
|
8638
|
+
depth = boundaries[i].includes("test.step(") ? 0 : null;
|
|
8639
|
+
}
|
|
8640
|
+
if (depth !== null) {
|
|
8641
|
+
const before = depth;
|
|
8642
|
+
depth += (text.match(/\{/g)?.length ?? 0) - (text.match(/\}/g)?.length ?? 0);
|
|
8643
|
+
if (before > 0 && depth <= 0) {
|
|
8644
|
+
step = null;
|
|
8645
|
+
depth = null;
|
|
8646
|
+
}
|
|
8647
|
+
}
|
|
8648
|
+
if (step !== null && blockSteps.has(step)) return [];
|
|
8607
8649
|
if (!/\bexpect(?:\.soft)?\(/.test(text)) return [];
|
|
8608
8650
|
const strings = assertedStrings(text, locators);
|
|
8609
8651
|
if (strings.length === 0 || strings.some((s) => said.includes(s))) return [];
|
|
@@ -15013,12 +15055,14 @@ async function finalizeAndVerify(p) {
|
|
|
15013
15055
|
review
|
|
15014
15056
|
};
|
|
15015
15057
|
}
|
|
15058
|
+
const FINDINGS_LEFT_OPEN = "generated with review findings still open — the files are kept and the findings are recorded against the case, but nothing acted on them";
|
|
15016
15059
|
/**
|
|
15017
15060
|
* The runCommand verification loop. One round asks the project's own commands,
|
|
15018
|
-
* then the review
|
|
15019
|
-
* clean. On anything else the output goes to Claude for a corrected set and
|
|
15061
|
+
* then the review and the run together — and a round is over when all three
|
|
15062
|
+
* are clean. On anything else the output goes to Claude for a corrected set and
|
|
15020
15063
|
* the next round asks all three again: a fix written for a failing run can
|
|
15021
|
-
* break a rule the round before it cleared
|
|
15064
|
+
* break a rule the round before it cleared, and one written for a finding can
|
|
15065
|
+
* break the run.
|
|
15022
15066
|
*
|
|
15023
15067
|
* `fix.mode` mirrors the agent-browser target's fix UX (which only ever
|
|
15024
15068
|
* prompts inside its own fix loop, never for the first write):
|
|
@@ -15030,8 +15074,9 @@ async function finalizeAndVerify(p) {
|
|
|
15030
15074
|
* verify decides pass/fail.
|
|
15031
15075
|
*
|
|
15032
15076
|
* Exhaustion (or a declined / skipped fix) keeps the files on disk and reports
|
|
15033
|
-
* `passed: false
|
|
15034
|
-
*
|
|
15077
|
+
* `passed: false` — unless an earlier round's files passed their run: those
|
|
15078
|
+
* are put back and the generate passes with the findings open. Targets without
|
|
15079
|
+
* a runCommand are generate-only here and pass trivially.
|
|
15035
15080
|
*/
|
|
15036
15081
|
async function runVerificationLoop(p, state) {
|
|
15037
15082
|
const runCommand = p.ctx.targetConfig.runCommand;
|
|
@@ -15061,33 +15106,61 @@ async function runVerificationLoop(p, state) {
|
|
|
15061
15106
|
const askReviewer = maxRetries > 0;
|
|
15062
15107
|
let reviewerGone = false;
|
|
15063
15108
|
if (!askReviewer && p.reading) info("no fix round could act on a review — the reviewer is not asked; the mechanical read still runs");
|
|
15109
|
+
let passing = null;
|
|
15110
|
+
const original = /* @__PURE__ */ new Map();
|
|
15111
|
+
const settle = async (attempt) => {
|
|
15112
|
+
if (passing === null) {
|
|
15113
|
+
await clearCaseRun(p.ctx.ref);
|
|
15114
|
+
return finish(false);
|
|
15115
|
+
}
|
|
15116
|
+
if (passing.attempt !== attempt) {
|
|
15117
|
+
await restoreFiles(state, passing.files, original);
|
|
15118
|
+
review = passing.review;
|
|
15119
|
+
warn("the final round did not pass — restored the files of the last round that did");
|
|
15120
|
+
}
|
|
15121
|
+
warn(FINDINGS_LEFT_OPEN);
|
|
15122
|
+
return finish(true);
|
|
15123
|
+
};
|
|
15124
|
+
if (captures) await clearCaseRun(p.ctx.ref);
|
|
15064
15125
|
for (let attempt = 0;; attempt++) {
|
|
15065
15126
|
let failing = await runCheckCommands(p.ctx);
|
|
15066
15127
|
let asked = [];
|
|
15067
15128
|
if (failing === null) {
|
|
15068
15129
|
await refreshFromDisk(state);
|
|
15069
|
-
const
|
|
15130
|
+
const questions = [
|
|
15131
|
+
readingOfEmitted(p, state, askReviewer && !reviewerGone),
|
|
15132
|
+
reviewOfEmitted(p.ctx, state, p.writeRoots),
|
|
15133
|
+
verifyRun(p, state, runCommand, attempt, captures)
|
|
15134
|
+
];
|
|
15135
|
+
const [reading, mechanical, run] = await Promise.all(questions).catch(async (err) => {
|
|
15136
|
+
await Promise.allSettled(questions);
|
|
15137
|
+
await clearCaseRun(p.ctx.ref);
|
|
15138
|
+
throw err;
|
|
15139
|
+
});
|
|
15070
15140
|
review = reading;
|
|
15071
|
-
if (
|
|
15072
|
-
|
|
15073
|
-
|
|
15141
|
+
if (run.exitCode === 0) passing = {
|
|
15142
|
+
files: new Map(state),
|
|
15143
|
+
review,
|
|
15144
|
+
attempt
|
|
15145
|
+
};
|
|
15146
|
+
const heard = review?.reviewerFailed ? void 0 : review;
|
|
15147
|
+
if (review && !heard) reviewerGone = true;
|
|
15148
|
+
const unchecked = uncheckedSteps(heard, spent);
|
|
15149
|
+
const violations = guideViolations(heard, spent);
|
|
15074
15150
|
const reads = allReadings(mechanical, unchecked, violations);
|
|
15075
|
-
|
|
15076
|
-
|
|
15077
|
-
|
|
15078
|
-
|
|
15079
|
-
if (reads !== null) warn(
|
|
15080
|
-
|
|
15081
|
-
if (run.exitCode === 0) return finish(true);
|
|
15082
|
-
failing = run;
|
|
15151
|
+
asked = [...unchecked?.asked ?? [], ...violations?.asked ?? []];
|
|
15152
|
+
if (run.exitCode !== 0) failing = reads === null ? run : allReadings(run, reads) ?? run;
|
|
15153
|
+
else if (reads !== null && attempt < maxRetries) failing = reads;
|
|
15154
|
+
else {
|
|
15155
|
+
if (reads !== null) warn(FINDINGS_LEFT_OPEN);
|
|
15156
|
+
return finish(true);
|
|
15083
15157
|
}
|
|
15084
15158
|
}
|
|
15085
15159
|
const outputTail = scrubEnvValues(tail(failing.output), outputScrub);
|
|
15086
15160
|
if (attempt >= maxRetries) {
|
|
15087
|
-
warn(`verification still failing after ${maxRetries} fix attempt(s)
|
|
15161
|
+
warn(`verification still failing after ${maxRetries} fix attempt(s)`);
|
|
15088
15162
|
logOutputTail(outputTail, warn);
|
|
15089
|
-
|
|
15090
|
-
return finish(false);
|
|
15163
|
+
return settle(attempt);
|
|
15091
15164
|
}
|
|
15092
15165
|
fix(`verification failed (exit ${failing.exitCode}) — requesting a fix (${attempt + 1}/${maxRetries})`);
|
|
15093
15166
|
logOutputTail(outputTail, fix);
|
|
@@ -15125,9 +15198,12 @@ async function runVerificationLoop(p, state) {
|
|
|
15125
15198
|
continue;
|
|
15126
15199
|
}
|
|
15127
15200
|
if (p.ctx.fix.mode === "interactive" && !await confirmFixWrite(output.files, p.ctx.cwd)) {
|
|
15128
|
-
info("fix not applied (declined)
|
|
15129
|
-
|
|
15130
|
-
|
|
15201
|
+
info("fix not applied (declined)");
|
|
15202
|
+
return settle(attempt);
|
|
15203
|
+
}
|
|
15204
|
+
for (const f of output.files) {
|
|
15205
|
+
const rel = relative(p.ctx.cwd, resolve(p.ctx.cwd, f.path));
|
|
15206
|
+
if (!state.has(rel) && !original.has(rel)) original.set(rel, await readFile(resolve(p.ctx.cwd, f.path), "utf8").catch(() => null));
|
|
15131
15207
|
}
|
|
15132
15208
|
await writeGeneratedFiles(p.ctx.cwd, output.files, state);
|
|
15133
15209
|
review = void 0;
|
|
@@ -15135,17 +15211,12 @@ async function runVerificationLoop(p, state) {
|
|
|
15135
15211
|
}
|
|
15136
15212
|
/**
|
|
15137
15213
|
* One verification run of the files as they are now, in the shape the fix loop
|
|
15138
|
-
* consumes.
|
|
15139
|
-
* of files the project's own commands and the review have already cleared.
|
|
15214
|
+
* consumes. Asked only of files the project's own commands have cleared.
|
|
15140
15215
|
*/
|
|
15141
15216
|
async function verifyRun(p, state, runCommand, attempt, captures) {
|
|
15142
15217
|
const testFiles = [...state.entries()].filter(([, f]) => f.kind === "test").map(([rel]) => rel);
|
|
15143
15218
|
const artifactsDir = await mkdtemp(join(tmpdir(), "ccqa-verify-artifacts-"));
|
|
15144
|
-
const evidenceDir = captures ?
|
|
15145
|
-
if (evidenceDir) {
|
|
15146
|
-
await clearCaseRun(p.ctx.ref);
|
|
15147
|
-
await mkdir(evidenceDir, { recursive: true });
|
|
15148
|
-
}
|
|
15219
|
+
const evidenceDir = captures ? await mkdtemp(join(tmpdir(), "ccqa-verify-evidence-")) : null;
|
|
15149
15220
|
let command = substituteArtifactsDir(substituteRunCommandFiles(runCommand, testFiles), artifactsDir);
|
|
15150
15221
|
let traceUnavailable;
|
|
15151
15222
|
if (evidenceDir) {
|
|
@@ -15169,6 +15240,8 @@ async function verifyRun(p, state, runCommand, attempt, captures) {
|
|
|
15169
15240
|
warn: (message) => warn(`${p.ctx.ref.id}: ${message}`)
|
|
15170
15241
|
});
|
|
15171
15242
|
if (unavailable) warn(unavailable);
|
|
15243
|
+
await clearCaseRun(p.ctx.ref);
|
|
15244
|
+
await cp(evidenceDir, caseRunDir(p.ctx.ref), { recursive: true });
|
|
15172
15245
|
}
|
|
15173
15246
|
return {
|
|
15174
15247
|
...result,
|
|
@@ -15179,6 +15252,10 @@ async function verifyRun(p, state, runCommand, attempt, captures) {
|
|
|
15179
15252
|
recursive: true,
|
|
15180
15253
|
force: true
|
|
15181
15254
|
});
|
|
15255
|
+
if (evidenceDir) await rm(evidenceDir, {
|
|
15256
|
+
recursive: true,
|
|
15257
|
+
force: true
|
|
15258
|
+
});
|
|
15182
15259
|
}
|
|
15183
15260
|
}
|
|
15184
15261
|
/**
|
|
@@ -15199,6 +15276,10 @@ async function runCheckCommands(ctx) {
|
|
|
15199
15276
|
}
|
|
15200
15277
|
return null;
|
|
15201
15278
|
}
|
|
15279
|
+
function blockStepIds(ctx) {
|
|
15280
|
+
const included = new Set(collectIncludedBlockNames(ctx.spec));
|
|
15281
|
+
return new Set(ctx.steps.filter((s) => included.has(s.source)).map((s) => s.id));
|
|
15282
|
+
}
|
|
15202
15283
|
/**
|
|
15203
15284
|
* The mechanical read of what this attempt wrote, shaped like a failed check
|
|
15204
15285
|
* so the fix loop carries it the same way.
|
|
@@ -15216,6 +15297,7 @@ async function reviewOfEmitted(ctx, state, writeRoots) {
|
|
|
15216
15297
|
...ctx.expectations,
|
|
15217
15298
|
...ctx.cleanupExpectations
|
|
15218
15299
|
],
|
|
15300
|
+
blockSteps: blockStepIds(ctx),
|
|
15219
15301
|
testPath: ctx.testPath
|
|
15220
15302
|
});
|
|
15221
15303
|
if (findings.length === 0) return null;
|
|
@@ -15261,6 +15343,23 @@ async function refreshFromDisk(state) {
|
|
|
15261
15343
|
}
|
|
15262
15344
|
}
|
|
15263
15345
|
/**
|
|
15346
|
+
* Put the files back as `to` holds them. A path written after `to` was taken
|
|
15347
|
+
* goes back to what it held before the loop wrote it, or away if it was new.
|
|
15348
|
+
*/
|
|
15349
|
+
async function restoreFiles(state, to, original) {
|
|
15350
|
+
for (const [rel, f] of state) {
|
|
15351
|
+
if (to.has(rel)) continue;
|
|
15352
|
+
const was = original.get(rel) ?? null;
|
|
15353
|
+
if (was === null) await rm(f.abs, { force: true });
|
|
15354
|
+
else await writeFile(f.abs, was, "utf8");
|
|
15355
|
+
}
|
|
15356
|
+
state.clear();
|
|
15357
|
+
for (const [rel, f] of to) {
|
|
15358
|
+
await writeFile(f.abs, f.contents, "utf8");
|
|
15359
|
+
state.set(rel, f);
|
|
15360
|
+
}
|
|
15361
|
+
}
|
|
15362
|
+
/**
|
|
15264
15363
|
* The reading's findings, shaped like a failed check so the fix loop carries
|
|
15265
15364
|
* them the same way — or null when there is nothing to act on.
|
|
15266
15365
|
*
|
|
@@ -15273,9 +15372,8 @@ async function refreshFromDisk(state) {
|
|
|
15273
15372
|
*
|
|
15274
15373
|
* What the round asks for is verified like any other rewrite, and that can
|
|
15275
15374
|
* end red: an assertion strengthened to check what the case claims may simply
|
|
15276
|
-
* not hold. The
|
|
15277
|
-
*
|
|
15278
|
-
* (restoring what passed weakly) would hide a case the product does not meet.
|
|
15375
|
+
* not hold. The loop then goes back to the last version that passed, and the
|
|
15376
|
+
* finding stays open on the record rather than hidden.
|
|
15279
15377
|
*
|
|
15280
15378
|
* A review that could not be obtained is not acted on either: spending a round
|
|
15281
15379
|
* answering a question nobody asked is worse than leaving it unanswered. Nor
|
|
@@ -15421,7 +15519,7 @@ function allReadings(...reports) {
|
|
|
15421
15519
|
const found = reports.filter((r) => r !== null);
|
|
15422
15520
|
if (found.length === 0) return null;
|
|
15423
15521
|
return {
|
|
15424
|
-
exitCode:
|
|
15522
|
+
exitCode: found[0].exitCode,
|
|
15425
15523
|
command: found.map((r) => r.command).join(" + "),
|
|
15426
15524
|
output: found.map((r) => `${r.command}\n\n${r.output}`).join("\n\n")
|
|
15427
15525
|
};
|
package/dist/package.json
CHANGED