ccqa 1.60.1 → 1.60.3
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 +170 -63
- package/dist/hub-client/index.d.cts +2 -2
- package/dist/hub-client/index.d.mts +2 -2
- 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 [];
|
|
@@ -9302,8 +9344,12 @@ const ViolationsSchema = z.object({ ruleViolations: z.array(z.unknown()) });
|
|
|
9302
9344
|
* treats it as one it could not obtain.
|
|
9303
9345
|
*/
|
|
9304
9346
|
const MAX_REVIEW_TURNS = 80;
|
|
9305
|
-
/**
|
|
9306
|
-
|
|
9347
|
+
/**
|
|
9348
|
+
* A wedged call, not a slow one. A review of a real suite reads the test, its
|
|
9349
|
+
* page objects and many of the suite's files to count conventions, which runs
|
|
9350
|
+
* past ten minutes; one that is still going after twenty is stuck.
|
|
9351
|
+
*/
|
|
9352
|
+
const REVIEW_TIMEOUT_MS = 20 * 6e4;
|
|
9307
9353
|
/** Read-only: a reviewer reads the repository and changes nothing in it. */
|
|
9308
9354
|
const REVIEW_TOOLS = [
|
|
9309
9355
|
"Read",
|
|
@@ -14746,7 +14792,7 @@ const LLM_GEN_ALLOWED_TOOLS = [
|
|
|
14746
14792
|
const LlmFileSchema = z.object({
|
|
14747
14793
|
path: z.string().min(1),
|
|
14748
14794
|
contents: z.string(),
|
|
14749
|
-
kind: z.string().
|
|
14795
|
+
kind: z.string().optional()
|
|
14750
14796
|
});
|
|
14751
14797
|
const LlmOutputSchema = z.object({
|
|
14752
14798
|
files: z.array(LlmFileSchema),
|
|
@@ -14778,21 +14824,29 @@ function parseLlmGenOutput(raw) {
|
|
|
14778
14824
|
const issues = res.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
|
|
14779
14825
|
throw new Error(`reply JSON does not match {files:[{path,contents,kind}],summary}: ${issues.join("; ")}`);
|
|
14780
14826
|
}
|
|
14781
|
-
const kindWarnings = [];
|
|
14782
14827
|
return {
|
|
14783
|
-
files: res.data.files.map((f) => {
|
|
14784
|
-
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14828
|
+
files: res.data.files.map((f) => ({
|
|
14829
|
+
path: f.path,
|
|
14830
|
+
contents: f.contents,
|
|
14831
|
+
kind: f.kind === "support" ? "support" : "test"
|
|
14832
|
+
})),
|
|
14833
|
+
summary: res.data.summary
|
|
14834
|
+
};
|
|
14835
|
+
}
|
|
14836
|
+
/**
|
|
14837
|
+
* The file at the configured test path is the test and every other file is
|
|
14838
|
+
* support — whatever the reply labelled them. The path already says which one
|
|
14839
|
+
* a file is, and a model that labels a page object "test" would otherwise fail
|
|
14840
|
+
* the whole generation over a word.
|
|
14841
|
+
*/
|
|
14842
|
+
function kindByPath(output, policy) {
|
|
14843
|
+
const testAbs = resolve(policy.cwd, policy.testPath);
|
|
14844
|
+
return {
|
|
14845
|
+
...output,
|
|
14846
|
+
files: output.files.map((f) => ({
|
|
14847
|
+
...f,
|
|
14848
|
+
kind: resolve(policy.cwd, f.path) === testAbs ? "test" : "support"
|
|
14849
|
+
}))
|
|
14796
14850
|
};
|
|
14797
14851
|
}
|
|
14798
14852
|
/**
|
|
@@ -14952,7 +15006,6 @@ async function generateWithLlmEngine(req) {
|
|
|
14952
15006
|
requireTestFile: true,
|
|
14953
15007
|
allowEmpty: false
|
|
14954
15008
|
}), "run");
|
|
14955
|
-
warnings.push(...output.kindWarnings);
|
|
14956
15009
|
return finalizeAndVerify({
|
|
14957
15010
|
ctx,
|
|
14958
15011
|
target: req.target,
|
|
@@ -15013,12 +15066,14 @@ async function finalizeAndVerify(p) {
|
|
|
15013
15066
|
review
|
|
15014
15067
|
};
|
|
15015
15068
|
}
|
|
15069
|
+
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
15070
|
/**
|
|
15017
15071
|
* 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
|
|
15072
|
+
* then the review and the run together — and a round is over when all three
|
|
15073
|
+
* are clean. On anything else the output goes to Claude for a corrected set and
|
|
15020
15074
|
* the next round asks all three again: a fix written for a failing run can
|
|
15021
|
-
* break a rule the round before it cleared
|
|
15075
|
+
* break a rule the round before it cleared, and one written for a finding can
|
|
15076
|
+
* break the run.
|
|
15022
15077
|
*
|
|
15023
15078
|
* `fix.mode` mirrors the agent-browser target's fix UX (which only ever
|
|
15024
15079
|
* prompts inside its own fix loop, never for the first write):
|
|
@@ -15030,8 +15085,9 @@ async function finalizeAndVerify(p) {
|
|
|
15030
15085
|
* verify decides pass/fail.
|
|
15031
15086
|
*
|
|
15032
15087
|
* Exhaustion (or a declined / skipped fix) keeps the files on disk and reports
|
|
15033
|
-
* `passed: false
|
|
15034
|
-
*
|
|
15088
|
+
* `passed: false` — unless an earlier round's files passed their run: those
|
|
15089
|
+
* are put back and the generate passes with the findings open. Targets without
|
|
15090
|
+
* a runCommand are generate-only here and pass trivially.
|
|
15035
15091
|
*/
|
|
15036
15092
|
async function runVerificationLoop(p, state) {
|
|
15037
15093
|
const runCommand = p.ctx.targetConfig.runCommand;
|
|
@@ -15061,33 +15117,61 @@ async function runVerificationLoop(p, state) {
|
|
|
15061
15117
|
const askReviewer = maxRetries > 0;
|
|
15062
15118
|
let reviewerGone = false;
|
|
15063
15119
|
if (!askReviewer && p.reading) info("no fix round could act on a review — the reviewer is not asked; the mechanical read still runs");
|
|
15120
|
+
let passing = null;
|
|
15121
|
+
const original = /* @__PURE__ */ new Map();
|
|
15122
|
+
const settle = async (attempt) => {
|
|
15123
|
+
if (passing === null) {
|
|
15124
|
+
await clearCaseRun(p.ctx.ref);
|
|
15125
|
+
return finish(false);
|
|
15126
|
+
}
|
|
15127
|
+
if (passing.attempt !== attempt) {
|
|
15128
|
+
await restoreFiles(state, passing.files, original);
|
|
15129
|
+
review = passing.review;
|
|
15130
|
+
warn("the final round did not pass — restored the files of the last round that did");
|
|
15131
|
+
}
|
|
15132
|
+
warn(FINDINGS_LEFT_OPEN);
|
|
15133
|
+
return finish(true);
|
|
15134
|
+
};
|
|
15135
|
+
if (captures) await clearCaseRun(p.ctx.ref);
|
|
15064
15136
|
for (let attempt = 0;; attempt++) {
|
|
15065
15137
|
let failing = await runCheckCommands(p.ctx);
|
|
15066
15138
|
let asked = [];
|
|
15067
15139
|
if (failing === null) {
|
|
15068
15140
|
await refreshFromDisk(state);
|
|
15069
|
-
const
|
|
15141
|
+
const questions = [
|
|
15142
|
+
readingOfEmitted(p, state, askReviewer && !reviewerGone),
|
|
15143
|
+
reviewOfEmitted(p.ctx, state, p.writeRoots),
|
|
15144
|
+
verifyRun(p, state, runCommand, attempt, captures)
|
|
15145
|
+
];
|
|
15146
|
+
const [reading, mechanical, run] = await Promise.all(questions).catch(async (err) => {
|
|
15147
|
+
await Promise.allSettled(questions);
|
|
15148
|
+
await clearCaseRun(p.ctx.ref);
|
|
15149
|
+
throw err;
|
|
15150
|
+
});
|
|
15070
15151
|
review = reading;
|
|
15071
|
-
if (
|
|
15072
|
-
|
|
15073
|
-
|
|
15152
|
+
if (run.exitCode === 0) passing = {
|
|
15153
|
+
files: new Map(state),
|
|
15154
|
+
review,
|
|
15155
|
+
attempt
|
|
15156
|
+
};
|
|
15157
|
+
const heard = review?.reviewerFailed ? void 0 : review;
|
|
15158
|
+
if (review && !heard) reviewerGone = true;
|
|
15159
|
+
const unchecked = uncheckedSteps(heard, spent);
|
|
15160
|
+
const violations = guideViolations(heard, spent);
|
|
15074
15161
|
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;
|
|
15162
|
+
asked = [...unchecked?.asked ?? [], ...violations?.asked ?? []];
|
|
15163
|
+
if (run.exitCode !== 0) failing = reads === null ? run : allReadings(run, reads) ?? run;
|
|
15164
|
+
else if (reads !== null && attempt < maxRetries) failing = reads;
|
|
15165
|
+
else {
|
|
15166
|
+
if (reads !== null) warn(FINDINGS_LEFT_OPEN);
|
|
15167
|
+
return finish(true);
|
|
15083
15168
|
}
|
|
15084
15169
|
}
|
|
15085
15170
|
const outputTail = scrubEnvValues(tail(failing.output), outputScrub);
|
|
15086
15171
|
if (attempt >= maxRetries) {
|
|
15087
|
-
warn(`verification still failing after ${maxRetries} fix attempt(s)
|
|
15172
|
+
warn(`verification still failing after ${maxRetries} fix attempt(s)`);
|
|
15088
15173
|
logOutputTail(outputTail, warn);
|
|
15089
|
-
|
|
15090
|
-
return finish(false);
|
|
15174
|
+
return settle(attempt);
|
|
15091
15175
|
}
|
|
15092
15176
|
fix(`verification failed (exit ${failing.exitCode}) — requesting a fix (${attempt + 1}/${maxRetries})`);
|
|
15093
15177
|
logOutputTail(outputTail, fix);
|
|
@@ -15125,9 +15209,12 @@ async function runVerificationLoop(p, state) {
|
|
|
15125
15209
|
continue;
|
|
15126
15210
|
}
|
|
15127
15211
|
if (p.ctx.fix.mode === "interactive" && !await confirmFixWrite(output.files, p.ctx.cwd)) {
|
|
15128
|
-
info("fix not applied (declined)
|
|
15129
|
-
|
|
15130
|
-
|
|
15212
|
+
info("fix not applied (declined)");
|
|
15213
|
+
return settle(attempt);
|
|
15214
|
+
}
|
|
15215
|
+
for (const f of output.files) {
|
|
15216
|
+
const rel = relative(p.ctx.cwd, resolve(p.ctx.cwd, f.path));
|
|
15217
|
+
if (!state.has(rel) && !original.has(rel)) original.set(rel, await readFile(resolve(p.ctx.cwd, f.path), "utf8").catch(() => null));
|
|
15131
15218
|
}
|
|
15132
15219
|
await writeGeneratedFiles(p.ctx.cwd, output.files, state);
|
|
15133
15220
|
review = void 0;
|
|
@@ -15135,17 +15222,12 @@ async function runVerificationLoop(p, state) {
|
|
|
15135
15222
|
}
|
|
15136
15223
|
/**
|
|
15137
15224
|
* 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.
|
|
15225
|
+
* consumes. Asked only of files the project's own commands have cleared.
|
|
15140
15226
|
*/
|
|
15141
15227
|
async function verifyRun(p, state, runCommand, attempt, captures) {
|
|
15142
15228
|
const testFiles = [...state.entries()].filter(([, f]) => f.kind === "test").map(([rel]) => rel);
|
|
15143
15229
|
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
|
-
}
|
|
15230
|
+
const evidenceDir = captures ? await mkdtemp(join(tmpdir(), "ccqa-verify-evidence-")) : null;
|
|
15149
15231
|
let command = substituteArtifactsDir(substituteRunCommandFiles(runCommand, testFiles), artifactsDir);
|
|
15150
15232
|
let traceUnavailable;
|
|
15151
15233
|
if (evidenceDir) {
|
|
@@ -15169,6 +15251,8 @@ async function verifyRun(p, state, runCommand, attempt, captures) {
|
|
|
15169
15251
|
warn: (message) => warn(`${p.ctx.ref.id}: ${message}`)
|
|
15170
15252
|
});
|
|
15171
15253
|
if (unavailable) warn(unavailable);
|
|
15254
|
+
await clearCaseRun(p.ctx.ref);
|
|
15255
|
+
await cp(evidenceDir, caseRunDir(p.ctx.ref), { recursive: true });
|
|
15172
15256
|
}
|
|
15173
15257
|
return {
|
|
15174
15258
|
...result,
|
|
@@ -15179,6 +15263,10 @@ async function verifyRun(p, state, runCommand, attempt, captures) {
|
|
|
15179
15263
|
recursive: true,
|
|
15180
15264
|
force: true
|
|
15181
15265
|
});
|
|
15266
|
+
if (evidenceDir) await rm(evidenceDir, {
|
|
15267
|
+
recursive: true,
|
|
15268
|
+
force: true
|
|
15269
|
+
});
|
|
15182
15270
|
}
|
|
15183
15271
|
}
|
|
15184
15272
|
/**
|
|
@@ -15199,6 +15287,10 @@ async function runCheckCommands(ctx) {
|
|
|
15199
15287
|
}
|
|
15200
15288
|
return null;
|
|
15201
15289
|
}
|
|
15290
|
+
function blockStepIds(ctx) {
|
|
15291
|
+
const included = new Set(collectIncludedBlockNames(ctx.spec));
|
|
15292
|
+
return new Set(ctx.steps.filter((s) => included.has(s.source)).map((s) => s.id));
|
|
15293
|
+
}
|
|
15202
15294
|
/**
|
|
15203
15295
|
* The mechanical read of what this attempt wrote, shaped like a failed check
|
|
15204
15296
|
* so the fix loop carries it the same way.
|
|
@@ -15216,6 +15308,7 @@ async function reviewOfEmitted(ctx, state, writeRoots) {
|
|
|
15216
15308
|
...ctx.expectations,
|
|
15217
15309
|
...ctx.cleanupExpectations
|
|
15218
15310
|
],
|
|
15311
|
+
blockSteps: blockStepIds(ctx),
|
|
15219
15312
|
testPath: ctx.testPath
|
|
15220
15313
|
});
|
|
15221
15314
|
if (findings.length === 0) return null;
|
|
@@ -15261,6 +15354,23 @@ async function refreshFromDisk(state) {
|
|
|
15261
15354
|
}
|
|
15262
15355
|
}
|
|
15263
15356
|
/**
|
|
15357
|
+
* Put the files back as `to` holds them. A path written after `to` was taken
|
|
15358
|
+
* goes back to what it held before the loop wrote it, or away if it was new.
|
|
15359
|
+
*/
|
|
15360
|
+
async function restoreFiles(state, to, original) {
|
|
15361
|
+
for (const [rel, f] of state) {
|
|
15362
|
+
if (to.has(rel)) continue;
|
|
15363
|
+
const was = original.get(rel) ?? null;
|
|
15364
|
+
if (was === null) await rm(f.abs, { force: true });
|
|
15365
|
+
else await writeFile(f.abs, was, "utf8");
|
|
15366
|
+
}
|
|
15367
|
+
state.clear();
|
|
15368
|
+
for (const [rel, f] of to) {
|
|
15369
|
+
await writeFile(f.abs, f.contents, "utf8");
|
|
15370
|
+
state.set(rel, f);
|
|
15371
|
+
}
|
|
15372
|
+
}
|
|
15373
|
+
/**
|
|
15264
15374
|
* The reading's findings, shaped like a failed check so the fix loop carries
|
|
15265
15375
|
* them the same way — or null when there is nothing to act on.
|
|
15266
15376
|
*
|
|
@@ -15273,9 +15383,8 @@ async function refreshFromDisk(state) {
|
|
|
15273
15383
|
*
|
|
15274
15384
|
* What the round asks for is verified like any other rewrite, and that can
|
|
15275
15385
|
* 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.
|
|
15386
|
+
* not hold. The loop then goes back to the last version that passed, and the
|
|
15387
|
+
* finding stays open on the record rather than hidden.
|
|
15279
15388
|
*
|
|
15280
15389
|
* A review that could not be obtained is not acted on either: spending a round
|
|
15281
15390
|
* answering a question nobody asked is worse than leaving it unanswered. Nor
|
|
@@ -15421,7 +15530,7 @@ function allReadings(...reports) {
|
|
|
15421
15530
|
const found = reports.filter((r) => r !== null);
|
|
15422
15531
|
if (found.length === 0) return null;
|
|
15423
15532
|
return {
|
|
15424
|
-
exitCode:
|
|
15533
|
+
exitCode: found[0].exitCode,
|
|
15425
15534
|
command: found.map((r) => r.command).join(" + "),
|
|
15426
15535
|
output: found.map((r) => `${r.command}\n\n${r.output}`).join("\n\n")
|
|
15427
15536
|
};
|
|
@@ -15461,8 +15570,7 @@ async function invokeForFiles(p) {
|
|
|
15461
15570
|
model: p.ctx.model
|
|
15462
15571
|
}, () => {});
|
|
15463
15572
|
if (isError) throw new Error(`Claude invocation failed: ${tail(result)}`);
|
|
15464
|
-
const output = parseLlmGenOutput(result);
|
|
15465
|
-
for (const w of output.kindWarnings) warn(w);
|
|
15573
|
+
const output = kindByPath(parseLlmGenOutput(result), p.policy);
|
|
15466
15574
|
const errors = validateOutput(output, p);
|
|
15467
15575
|
if (errors.length > 0) throw new Error(errors.join("; "));
|
|
15468
15576
|
return output;
|
|
@@ -15505,8 +15613,7 @@ function validateOutput(output, p) {
|
|
|
15505
15613
|
if (fileError) errors.push(fileError);
|
|
15506
15614
|
}
|
|
15507
15615
|
const tests = output.files.filter((f) => f.kind === "test");
|
|
15508
|
-
if (p.requireTestFile && tests.length === 0) errors.push(
|
|
15509
|
-
if (tests.length > 1) errors.push(`output contains ${tests.length} "kind": "test" files; a spec has exactly one`);
|
|
15616
|
+
if (p.requireTestFile && tests.length === 0) errors.push(`the test file must be written to ${p.policy.testPath}; the reply has no file there`);
|
|
15510
15617
|
return errors;
|
|
15511
15618
|
}
|
|
15512
15619
|
function toPromptResource(r) {
|
|
@@ -35,9 +35,9 @@ declare const RunSchema: z.ZodObject<{
|
|
|
35
35
|
running: "running";
|
|
36
36
|
}>;
|
|
37
37
|
kind: z.ZodDefault<z.ZodEnum<{
|
|
38
|
+
record: "record";
|
|
38
39
|
run: "run";
|
|
39
40
|
drift: "drift";
|
|
40
|
-
record: "record";
|
|
41
41
|
}>>;
|
|
42
42
|
drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
43
43
|
specs: z.ZodNumber;
|
|
@@ -831,9 +831,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
|
|
|
831
831
|
declare const RunReportDataSchema: z.ZodObject<{
|
|
832
832
|
schemaVersion: z.ZodLiteral<1>;
|
|
833
833
|
kind: z.ZodDefault<z.ZodEnum<{
|
|
834
|
+
record: "record";
|
|
834
835
|
run: "run";
|
|
835
836
|
drift: "drift";
|
|
836
|
-
record: "record";
|
|
837
837
|
}>>;
|
|
838
838
|
createdAt: z.ZodString;
|
|
839
839
|
runId: z.ZodNullable<z.ZodString>;
|
|
@@ -35,9 +35,9 @@ declare const RunSchema: z.ZodObject<{
|
|
|
35
35
|
running: "running";
|
|
36
36
|
}>;
|
|
37
37
|
kind: z.ZodDefault<z.ZodEnum<{
|
|
38
|
+
record: "record";
|
|
38
39
|
run: "run";
|
|
39
40
|
drift: "drift";
|
|
40
|
-
record: "record";
|
|
41
41
|
}>>;
|
|
42
42
|
drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
|
|
43
43
|
specs: z.ZodNumber;
|
|
@@ -831,9 +831,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
|
|
|
831
831
|
declare const RunReportDataSchema: z.ZodObject<{
|
|
832
832
|
schemaVersion: z.ZodLiteral<1>;
|
|
833
833
|
kind: z.ZodDefault<z.ZodEnum<{
|
|
834
|
+
record: "record";
|
|
834
835
|
run: "run";
|
|
835
836
|
drift: "drift";
|
|
836
|
-
record: "record";
|
|
837
837
|
}>>;
|
|
838
838
|
createdAt: z.ZodString;
|
|
839
839
|
runId: z.ZodNullable<z.ZodString>;
|
package/dist/package.json
CHANGED