ccqa 1.34.0 → 1.35.1
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/README.md +20 -0
- package/dist/bin/ccqa.mjs +91 -33
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -197,6 +197,26 @@ tenant, an account, a role — not an environment: ccqa tracks one
|
|
|
197
197
|
verification environment
|
|
198
198
|
([ADR-0013](./docs/adr/0013-one-verification-environment.md)).
|
|
199
199
|
|
|
200
|
+
## Agent skills
|
|
201
|
+
|
|
202
|
+
`skills/` ships guides that let a coding agent (e.g. Claude Code) drive ccqa
|
|
203
|
+
end-to-end on its own:
|
|
204
|
+
|
|
205
|
+
- **ccqa-record** — create a new test case: pin down the behavior, write the
|
|
206
|
+
spec, choose deterministic or live mode, record, run to green.
|
|
207
|
+
- **ccqa-rerecord** — bring a flagged or failing test case back to green:
|
|
208
|
+
read the hub's finding, decide what went stale, repair that, re-record.
|
|
209
|
+
- **ccqa-resolve** — clear everything the hub is holding for a person: read
|
|
210
|
+
the verdict, order the rows, route each to a product fix, a re-recording or
|
|
211
|
+
an environment repair.
|
|
212
|
+
|
|
213
|
+
Install them with the [skills CLI](https://github.com/vercel-labs/skills)
|
|
214
|
+
into a consuming project (or `-g` for all projects):
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
npx skills add <this-repo> --skill ccqa-record --skill ccqa-rerecord --skill ccqa-resolve
|
|
218
|
+
```
|
|
219
|
+
|
|
200
220
|
## Documentation
|
|
201
221
|
|
|
202
222
|
| I want to… | Read |
|
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -89,17 +89,19 @@ function* iterEnvRefNames(value) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
|
-
* Resolve every `$VAR` / `${VAR}` reference against
|
|
92
|
+
* Resolve every `$VAR` / `${VAR}` reference against `overrides`, then the
|
|
93
|
+
* current process env. `overrides` carries values an invoker injects into a
|
|
94
|
+
* child process (e.g. CCQA_RUN_ID), which beat the parent env there.
|
|
93
95
|
*
|
|
94
96
|
* Missing variables expand to the empty string, mirroring `sh` behaviour.
|
|
95
97
|
* Throwing would force ccqa to be invoked with every var set even for
|
|
96
98
|
* unused blocks, which is more user-hostile than letting the test fail
|
|
97
99
|
* downstream with a clearer message ("login form rejected: empty password").
|
|
98
100
|
*/
|
|
99
|
-
function resolveEnvRefs(value) {
|
|
101
|
+
function resolveEnvRefs(value, overrides = {}) {
|
|
100
102
|
return value.replace(ENV_VAR_RE, (_, braced, plain) => {
|
|
101
103
|
const name = braced ?? plain ?? "";
|
|
102
|
-
return process.env[name] ?? "";
|
|
104
|
+
return overrides[name] ?? process.env[name] ?? "";
|
|
103
105
|
});
|
|
104
106
|
}
|
|
105
107
|
/**
|
|
@@ -408,6 +410,7 @@ function collectIncludedBlockNames(spec) {
|
|
|
408
410
|
const CCQA_DIR = ".ccqa";
|
|
409
411
|
const SPEC_FILE = "spec.yaml";
|
|
410
412
|
const RECORDING_FILE = "ir.json";
|
|
413
|
+
const FAILED_RECORDING_FILE = "ir.failed.json";
|
|
411
414
|
const PERSPECTIVES_FILE = "perspectives.yaml";
|
|
412
415
|
const PERSPECTIVES_MD_FILE = "perspectives.md";
|
|
413
416
|
function getCcqaDir(cwd = process.cwd()) {
|
|
@@ -476,9 +479,22 @@ async function saveRecording(featureName, specName, actions, cwd) {
|
|
|
476
479
|
await mkdir(specDir, { recursive: true });
|
|
477
480
|
const recordingPath = join(specDir, RECORDING_FILE);
|
|
478
481
|
await writeFile(recordingPath, JSON.stringify(actions, null, 2), "utf-8");
|
|
479
|
-
await Promise.all(LEGACY_RECORDING_FILES.map((f) => unlink(join(specDir, f)).catch(() => {})));
|
|
482
|
+
await Promise.all([...LEGACY_RECORDING_FILES, FAILED_RECORDING_FILE].map((f) => unlink(join(specDir, f)).catch(() => {})));
|
|
480
483
|
return recordingPath;
|
|
481
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Persist the actions of a trace that FAILED. They go to a side file so a
|
|
487
|
+
* recording that did not demonstrate the spec can never replace one that
|
|
488
|
+
* did — `ir.json` and the generated code stay whatever they were. The next
|
|
489
|
+
* successful {@link saveRecording} deletes the file.
|
|
490
|
+
*/
|
|
491
|
+
async function saveFailedRecording(featureName, specName, actions, cwd) {
|
|
492
|
+
const specDir = getSpecDir(featureName, specName, cwd);
|
|
493
|
+
await mkdir(specDir, { recursive: true });
|
|
494
|
+
const path = join(specDir, FAILED_RECORDING_FILE);
|
|
495
|
+
await writeFile(path, JSON.stringify(actions, null, 2), "utf-8");
|
|
496
|
+
return path;
|
|
497
|
+
}
|
|
482
498
|
function getBlocksDir(cwd) {
|
|
483
499
|
return join(getCcqaDir(cwd), "blocks");
|
|
484
500
|
}
|
|
@@ -1326,10 +1342,12 @@ function opt(key, value) {
|
|
|
1326
1342
|
* `instruction` / `expected` may *also* contain `${ENV}` refs that
|
|
1327
1343
|
* don't go through include params.
|
|
1328
1344
|
*
|
|
1329
|
-
*
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
1345
|
+
* Each ref resolves against `overrides` first, then `process.env` —
|
|
1346
|
+
* `overrides` carries values the invoker injects into the child process,
|
|
1347
|
+
* which beat the parent env there. Only refs that resolve non-empty land in
|
|
1348
|
+
* the map — scrubbing against an empty string would corrupt unrelated empty
|
|
1349
|
+
* strings in the action stream; the rest are returned via `unresolved` so
|
|
1350
|
+
* the caller can warn the user.
|
|
1333
1351
|
*
|
|
1334
1352
|
* Longer values sort first so a `${SHORT}` whose value is a substring of a
|
|
1335
1353
|
* `${LONG}` value doesn't clobber the longer one.
|
|
@@ -1337,7 +1355,7 @@ function opt(key, value) {
|
|
|
1337
1355
|
* `title` is deliberately NOT scanned — it never reaches the recorded action
|
|
1338
1356
|
* stream.
|
|
1339
1357
|
*/
|
|
1340
|
-
function buildSpecEnvScrub(spec, expanded) {
|
|
1358
|
+
function buildSpecEnvScrub(spec, expanded, overrides = {}) {
|
|
1341
1359
|
const refNames = /* @__PURE__ */ new Set();
|
|
1342
1360
|
for (const step of spec.steps) if (isIncludeStep(step)) for (const v of Object.values(step.params ?? {})) collect(v, refNames);
|
|
1343
1361
|
else {
|
|
@@ -1351,7 +1369,7 @@ function buildSpecEnvScrub(spec, expanded) {
|
|
|
1351
1369
|
const map = [];
|
|
1352
1370
|
const unresolved = [];
|
|
1353
1371
|
for (const name of refNames) {
|
|
1354
|
-
const value = process.env[name];
|
|
1372
|
+
const value = overrides[name] ?? process.env[name];
|
|
1355
1373
|
if (typeof value === "string" && value.length > 0) map.push([value, "${" + name + "}"]);
|
|
1356
1374
|
else unresolved.push(name);
|
|
1357
1375
|
}
|
|
@@ -1383,8 +1401,8 @@ const COMMON_PROSE_VALUES = new Set([
|
|
|
1383
1401
|
* command log too, trading that short-value coverage for not building a
|
|
1384
1402
|
* second map.
|
|
1385
1403
|
*/
|
|
1386
|
-
function buildProseEnvScrubMap(spec, expanded) {
|
|
1387
|
-
return buildSpecEnvScrub(spec, expanded).map.filter(([value]) => value.length >= MIN_PROSE_SCRUB_LENGTH && !COMMON_PROSE_VALUES.has(value.toLowerCase()));
|
|
1404
|
+
function buildProseEnvScrubMap(spec, expanded, overrides = {}) {
|
|
1405
|
+
return buildSpecEnvScrub(spec, expanded, overrides).map.filter(([value]) => value.length >= MIN_PROSE_SCRUB_LENGTH && !COMMON_PROSE_VALUES.has(value.toLowerCase()));
|
|
1388
1406
|
}
|
|
1389
1407
|
/**
|
|
1390
1408
|
* Replace every occurrence of an env value with its `${VAR}` placeholder in
|
|
@@ -10570,7 +10588,6 @@ async function runOneSpec(args) {
|
|
|
10570
10588
|
}
|
|
10571
10589
|
const spec = parseTestSpec(specContent);
|
|
10572
10590
|
const expanded = expandSpec(spec, { blocks: await loadAllBlocks(cwd) });
|
|
10573
|
-
const envScrubMap = buildProseEnvScrubMap(spec, expanded);
|
|
10574
10591
|
meta("spec", spec.title);
|
|
10575
10592
|
meta("steps", expanded.length);
|
|
10576
10593
|
const includes = collectIncludedBlockNames(spec);
|
|
@@ -10600,6 +10617,7 @@ async function runOneSpec(args) {
|
|
|
10600
10617
|
}
|
|
10601
10618
|
try {
|
|
10602
10619
|
const runId = buildRunId();
|
|
10620
|
+
const envScrubMap = buildProseEnvScrubMap(spec, expanded, { CCQA_RUN_ID: runId });
|
|
10603
10621
|
const runDir = opts.out ?? join(specDir, "runs", runId);
|
|
10604
10622
|
await mkdir(runDir, { recursive: true });
|
|
10605
10623
|
meta("runDir", runDir);
|
|
@@ -11835,10 +11853,11 @@ async function runVitest(scriptPath, agentBrowserSession) {
|
|
|
11835
11853
|
"--config",
|
|
11836
11854
|
bundledVitestConfigPath(),
|
|
11837
11855
|
scriptPath
|
|
11838
|
-
],
|
|
11856
|
+
], { env: {
|
|
11839
11857
|
...process.env,
|
|
11840
|
-
|
|
11841
|
-
|
|
11858
|
+
CCQA_RUN_ID: buildRunId(),
|
|
11859
|
+
...agentBrowserSession ? { AGENT_BROWSER_SESSION: agentBrowserSession } : {}
|
|
11860
|
+
} });
|
|
11842
11861
|
const currentScript = await readFile(scriptPath, "utf8");
|
|
11843
11862
|
return {
|
|
11844
11863
|
exitCode,
|
|
@@ -14030,6 +14049,10 @@ function createRunTeardown() {
|
|
|
14030
14049
|
untrackSession(name) {
|
|
14031
14050
|
sessions.delete(name);
|
|
14032
14051
|
},
|
|
14052
|
+
async closeTracked(name) {
|
|
14053
|
+
await closeSession(name);
|
|
14054
|
+
sessions.delete(name);
|
|
14055
|
+
},
|
|
14033
14056
|
onFinalize(fn) {
|
|
14034
14057
|
finalizers.push(fn);
|
|
14035
14058
|
},
|
|
@@ -14304,6 +14327,7 @@ CCQA_STEP=<step-id> agent-browser --session SESSION upload "<input[type=file] se
|
|
|
14304
14327
|
- \`@ref\` / \`@e1\` / \`e14\` — reference IDs are session-specific and change every run.
|
|
14305
14328
|
- **Bare tag selectors**: \`button\`, \`a\`, \`div\`, \`td\`, \`tr\`, \`main a\`, \`table tbody tr:nth-child(N)\`. These match every element of that tag and are non-deterministic on replay. **This includes the inner selector inside \`find first/last/nth\`** — see the \`find\` rules below.
|
|
14306
14329
|
- \`[role='button']\` or \`[type='checkbox']\` alone — matches too many elements.
|
|
14330
|
+
- **Playwright-only pseudo-classes**: \`:has-text()\`, \`:text-is()\`, \`:text-matches()\`, \`:visible\`. agent-browser's CSS engine does not implement them — they match nothing and every command using them fails. Use \`text=...\` or plain CSS instead.
|
|
14307
14331
|
- JavaScript execution (\`eval\`, \`js\`) — blocked by the hook layer.
|
|
14308
14332
|
|
|
14309
14333
|
### \`find\` subset (fallback when no ALLOWED CSS uniquely targets the element)
|
|
@@ -14685,9 +14709,9 @@ const ASSERT_TIMEOUT_MS = 1e4;
|
|
|
14685
14709
|
* has no side effect; assert types whose codegen forms aren't directly
|
|
14686
14710
|
* verifiable here fall through to the caller's `unverifiable` fallback).
|
|
14687
14711
|
*/
|
|
14688
|
-
function actionToAbArgs(action, sessionName) {
|
|
14712
|
+
function actionToAbArgs(action, sessionName, envOverrides = {}) {
|
|
14689
14713
|
const base = ["--session", sessionName];
|
|
14690
|
-
const sub = (s) => s === void 0 ? "" : resolveEnvRefs(s);
|
|
14714
|
+
const sub = (s) => s === void 0 ? "" : resolveEnvRefs(s, envOverrides);
|
|
14691
14715
|
switch (action.action) {
|
|
14692
14716
|
case "snapshot": return null;
|
|
14693
14717
|
case "assert": return assertToAbArgs(action, sub, sessionName);
|
|
@@ -14772,8 +14796,8 @@ const NO_STEP_ID = "__no_step__";
|
|
|
14772
14796
|
* `wait <selector>`); everything else spawns the agent-browser argv. A single
|
|
14773
14797
|
* hard-timeout (SIGTERM) retry covers the daemon's occasional under-load drop.
|
|
14774
14798
|
*/
|
|
14775
|
-
function runValidationAction(action, sessionName) {
|
|
14776
|
-
const built = actionToAbArgs(action, sessionName);
|
|
14799
|
+
function runValidationAction(action, sessionName, envOverrides = {}) {
|
|
14800
|
+
const built = actionToAbArgs(action, sessionName, envOverrides);
|
|
14777
14801
|
if (built === null) return {
|
|
14778
14802
|
skipped: true,
|
|
14779
14803
|
ok: false,
|
|
@@ -14817,7 +14841,7 @@ function validateActions(actions, opts) {
|
|
|
14817
14841
|
});
|
|
14818
14842
|
continue;
|
|
14819
14843
|
}
|
|
14820
|
-
const outcome = runValidationAction(action, opts.sessionName);
|
|
14844
|
+
const outcome = runValidationAction(action, opts.sessionName, opts.envOverrides);
|
|
14821
14845
|
if (outcome.skipped) {
|
|
14822
14846
|
kept.push(action);
|
|
14823
14847
|
continue;
|
|
@@ -14894,7 +14918,7 @@ function rescueLostSteps(actions, kept, dropped, opts) {
|
|
|
14894
14918
|
for (const [stepId, drops] of lostStepDrops.entries()) {
|
|
14895
14919
|
let anyForThisStep = false;
|
|
14896
14920
|
for (const d of drops) {
|
|
14897
|
-
const outcome = runValidationAction(d.action, opts.sessionName);
|
|
14921
|
+
const outcome = runValidationAction(d.action, opts.sessionName, opts.envOverrides);
|
|
14898
14922
|
if (outcome.skipped) continue;
|
|
14899
14923
|
if (outcome.ok) {
|
|
14900
14924
|
rescuedIndices.add(d.index);
|
|
@@ -15076,12 +15100,22 @@ function formatUnstableDrop(drop) {
|
|
|
15076
15100
|
}
|
|
15077
15101
|
//#endregion
|
|
15078
15102
|
//#region src/cli/trace.ts
|
|
15103
|
+
/**
|
|
15104
|
+
* Step ids (in spec order) whose kept actions include no assertion. A step
|
|
15105
|
+
* with none produced a test that performs the step but verifies nothing
|
|
15106
|
+
* about its `expected` — surfaced as a per-step warning after validation.
|
|
15107
|
+
*/
|
|
15108
|
+
function stepsWithoutAsserts(stepIds, actions) {
|
|
15109
|
+
const withAssert = new Set(actions.filter((a) => a.action === "assert" && a.stepId !== void 0).map((a) => a.stepId));
|
|
15110
|
+
return stepIds.filter((id) => !withAssert.has(id));
|
|
15111
|
+
}
|
|
15079
15112
|
async function runTrace(featureName, specName, model, validationMode = "lenient", language, opts = {}) {
|
|
15080
15113
|
header("trace", `${featureName}/${specName}`);
|
|
15081
15114
|
await preflightAgentBrowserCommand();
|
|
15082
15115
|
const spec = parseTestSpec(await readSpecFile(featureName, specName, opts.cwd));
|
|
15083
15116
|
const expanded = expandSpec(spec, { blocks: await loadAllBlocks(opts.cwd) });
|
|
15084
|
-
const
|
|
15117
|
+
const sessionName = generateSessionName();
|
|
15118
|
+
const envScrub = buildSpecEnvScrub(spec, expanded, { CCQA_RUN_ID: sessionName });
|
|
15085
15119
|
const envScrubMap = envScrub.map;
|
|
15086
15120
|
if (envScrub.unresolved.length > 0) {
|
|
15087
15121
|
warn(`spec references env var(s) that are unset at record time: ${envScrub.unresolved.join(", ")}`);
|
|
@@ -15093,7 +15127,7 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
|
|
|
15093
15127
|
const includes = collectIncludedBlockNames(spec);
|
|
15094
15128
|
if (includes.length > 0) meta("blocks", includes.join(", "));
|
|
15095
15129
|
blank();
|
|
15096
|
-
|
|
15130
|
+
opts.teardown?.trackSession(sessionName);
|
|
15097
15131
|
const baseSystemPrompt = buildTraceSystemPrompt({
|
|
15098
15132
|
title: spec.title,
|
|
15099
15133
|
steps: expanded,
|
|
@@ -15176,13 +15210,18 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
|
|
|
15176
15210
|
}
|
|
15177
15211
|
});
|
|
15178
15212
|
if (isError) overallStatus = "failed";
|
|
15179
|
-
const
|
|
15180
|
-
const
|
|
15213
|
+
const dedupedActions = dedupAndReport(scrubAndReport(traceActions));
|
|
15214
|
+
const validatedActions = overallStatus === "passed" || opts.validateFailedTrace === true ? validateAndReport(dedupedActions, validationMode, { CCQA_RUN_ID: sessionName }, opts.teardown) : dedupedActions;
|
|
15215
|
+
opts.teardown?.closeTracked(sessionName) ?? closeSession(sessionName);
|
|
15216
|
+
const recordingPath = overallStatus === "passed" ? await saveRecording(featureName, specName, validatedActions, opts.cwd) : await saveFailedRecording(featureName, specName, validatedActions, opts.cwd);
|
|
15181
15217
|
blank();
|
|
15182
15218
|
meta("saved", recordingPath);
|
|
15183
15219
|
meta("actions", validatedActions.length);
|
|
15184
15220
|
meta("status", overallStatus.toUpperCase());
|
|
15185
|
-
|
|
15221
|
+
if (overallStatus === "passed") {
|
|
15222
|
+
for (const stepId of stepsWithoutAsserts(expanded.map((s) => s.id), validatedActions)) warn(`${stepId} recorded no assertion — nothing in the generated test verifies its 'expected'`);
|
|
15223
|
+
hint(`run 'ccqa generate ${featureName}/${specName}' to generate a test script`);
|
|
15224
|
+
} else warn("trace FAILED — the recorded actions were saved beside the spec for diagnosis; the previous ir.json and generated code are left untouched");
|
|
15186
15225
|
return {
|
|
15187
15226
|
status: overallStatus,
|
|
15188
15227
|
statusLines,
|
|
@@ -15323,26 +15362,30 @@ function isAdjacentDuplicate(a, b) {
|
|
|
15323
15362
|
}
|
|
15324
15363
|
/**
|
|
15325
15364
|
* Run the post-trace replay validation and emit user-visible drop reports.
|
|
15326
|
-
* Splitting this out keeps `runTrace` readable;
|
|
15327
|
-
*
|
|
15365
|
+
* Splitting this out keeps `runTrace` readable; side effects are `log.*`,
|
|
15366
|
+
* the agent-browser invocations inside `validateActions`, and the replay
|
|
15367
|
+
* session's registration/close on `teardown`.
|
|
15328
15368
|
*
|
|
15329
15369
|
* In lenient mode (the default) failing actions are NOT removed — they're
|
|
15330
15370
|
* tagged with `replayUnstable: true` and merged back into the output stream
|
|
15331
15371
|
* in their original order so codegen can still emit them (with a `// [warn]`
|
|
15332
15372
|
* comment) and let the auto-fix loop decide what to do.
|
|
15333
15373
|
*/
|
|
15334
|
-
function validateAndReport(actions, mode) {
|
|
15374
|
+
function validateAndReport(actions, mode, envOverrides, teardown) {
|
|
15335
15375
|
if (actions.length === 0) return actions;
|
|
15336
15376
|
const sessionName = `${generateSessionName()}-validate`;
|
|
15377
|
+
teardown?.trackSession(sessionName);
|
|
15337
15378
|
blank();
|
|
15338
15379
|
info(`post-trace validation in ${mode} mode (replaying ${actions.length} recorded action(s))...`);
|
|
15339
15380
|
const { kept, unstable, dropped, rescuedSteps = [] } = validateActions(actions, {
|
|
15340
15381
|
sessionName,
|
|
15341
15382
|
mode,
|
|
15383
|
+
envOverrides,
|
|
15342
15384
|
onProgress: (i, total, action) => {
|
|
15343
15385
|
progress(i, total, validationProgressLabel(action));
|
|
15344
15386
|
}
|
|
15345
15387
|
});
|
|
15388
|
+
teardown?.closeTracked(sessionName) ?? closeSession(sessionName);
|
|
15346
15389
|
progressEnd();
|
|
15347
15390
|
if (rescuedSteps.length > 0) info(`rescued ${rescuedSteps.length} step(s) that had lost every action: ${rescuedSteps.join(", ")}`);
|
|
15348
15391
|
if (mode === "lenient") {
|
|
@@ -15565,6 +15608,10 @@ function resolveTargetOrExit(resolve) {
|
|
|
15565
15608
|
async function runGenerateLocked(featureName, specName, opts, cwd) {
|
|
15566
15609
|
const specYaml = await readSpecFile(featureName, specName, cwd);
|
|
15567
15610
|
const spec = parseTestSpec(specYaml);
|
|
15611
|
+
if (spec.mode === "live") {
|
|
15612
|
+
error(`this spec is 'mode: live' — a live spec runs without generated code. Run 'ccqa run ${featureName}/${specName}' instead`);
|
|
15613
|
+
process.exit(2);
|
|
15614
|
+
}
|
|
15568
15615
|
const config = await loadProjectConfig(cwd);
|
|
15569
15616
|
const target = resolveTargetOrExit(() => opts.targetOverride !== void 0 ? resolveTargetOverride(spec, opts.targetOverride) : resolveTarget(spec, config));
|
|
15570
15617
|
meta("target", target.id + (opts.targetOverride !== void 0 ? " (--target override)" : ""));
|
|
@@ -15737,6 +15784,10 @@ async function runRecord(specPath, opts) {
|
|
|
15737
15784
|
error(`target "${target.id}" does not use a browser recording — run 'ccqa generate ${featureName}/${specName}' instead`);
|
|
15738
15785
|
process.exit(2);
|
|
15739
15786
|
}
|
|
15787
|
+
if (spec.mode === "live") {
|
|
15788
|
+
error(`this spec is 'mode: live' — a live spec runs without a recording. Run 'ccqa run ${featureName}/${specName}' instead`);
|
|
15789
|
+
process.exit(2);
|
|
15790
|
+
}
|
|
15740
15791
|
const project = opts.hubProfile !== void 0 ? resolveProject(opts) : void 0;
|
|
15741
15792
|
if (opts.hubProfile !== void 0) await applyProfileFromOption({
|
|
15742
15793
|
profile: opts.hubProfile,
|
|
@@ -15804,6 +15855,8 @@ async function runRecord(specPath, opts) {
|
|
|
15804
15855
|
const traceResult = await runTrace(featureName, specName, opts.model, opts.traceValidation ?? "lenient", language, {
|
|
15805
15856
|
cwd: cwdForProfile,
|
|
15806
15857
|
hubContext,
|
|
15858
|
+
teardown,
|
|
15859
|
+
validateFailedTrace: opts.learnHubTracePrompt === true,
|
|
15807
15860
|
...opts.instruction ? { instruction: opts.instruction } : {},
|
|
15808
15861
|
onStep: (stepId) => {
|
|
15809
15862
|
tracingStep = stepId;
|
|
@@ -15821,7 +15874,7 @@ async function runRecord(specPath, opts) {
|
|
|
15821
15874
|
...opts.model ? { model: opts.model } : {},
|
|
15822
15875
|
...language ? { language } : {}
|
|
15823
15876
|
});
|
|
15824
|
-
if (!opts.traceOnly) generated = (await runGenerate(featureName, specName, {
|
|
15877
|
+
if (!opts.traceOnly && traceResult.status === "passed") generated = (await runGenerate(featureName, specName, {
|
|
15825
15878
|
maxRetries: parseInt(opts.autoFixMaxRetries ?? "3", 10),
|
|
15826
15879
|
fixMode: toFixMode(opts.autoFix ?? "interactive"),
|
|
15827
15880
|
force: opts.overwrite ?? false,
|
|
@@ -16089,7 +16142,9 @@ Ask whether the **intent** the step describes still exists in the product:
|
|
|
16089
16142
|
- The intent exists, but the string or selector the spec names is gone or renamed → **TEST_DRIFT**. Cite where the replacement lives.
|
|
16090
16143
|
- The intent itself is gone, or deliberately different → **SPEC_CHANGE**. Cite the source that shows the new shape.
|
|
16091
16144
|
|
|
16092
|
-
A renamed button is TEST_DRIFT. A button that no longer exists because the flow was replaced is SPEC_CHANGE. If the source shows a rename you can point at, prefer TEST_DRIFT
|
|
16145
|
+
A renamed button is TEST_DRIFT. A button that no longer exists because the flow was replaced is SPEC_CHANGE. If the source shows a rename you can point at, prefer TEST_DRIFT.
|
|
16146
|
+
|
|
16147
|
+
SPEC_CHANGE is the more expensive answer — it sends a human to rewrite or retire the spec — so it takes the *stronger* evidence, not the weaker. Failing to find where the intent went is not a finding; that is UNKNOWN. Claim SPEC_CHANGE only when you can point at the source that shows the new shape, or at where the implementation would sit if it still existed.
|
|
16093
16148
|
|
|
16094
16149
|
## Which surface drifted
|
|
16095
16150
|
|
|
@@ -16122,6 +16177,8 @@ sit nearby.
|
|
|
16122
16177
|
|
|
16123
16178
|
- **No drift is a claim, not a default.** Make it after picking the concrete strings from *every* surface you were given — the spec's \`expected\` and the generated code's selectors alike — and finding each of them in the source. Clearing the test case because one surface checked out is the most common way to miss a real finding. If you never looked, the honest answer is UNKNOWN.
|
|
16124
16179
|
- **A finding needs a citation.** Every TEST_DRIFT and SPEC_CHANGE must carry at least one \`evidence\` entry with a real \`file\`, and a line where you can give one. A label with no citation is a guess wearing a verdict's clothes — answer UNKNOWN instead.
|
|
16180
|
+
- **A citation must apply to the case at hand.** Finding the string is not the end of it — read what encloses the line before you cite it. A line inside a guard, behind an early \`return\`, or in a branch this spec's steps never enter says nothing about this spec. Name the conditions that must hold for that line to run, and check the spec puts the product in them. A citation that only proves the line exists is not evidence.
|
|
16181
|
+
- **A comment is not the code.** Comments in the product's source say what someone intended, and they rarely restate the conditions they sit under. A line reading "this is not supported", sitting inside a guarded branch, is true only inside that branch. Cite the control flow you traced, not the sentence you found.
|
|
16125
16182
|
- **Do not report style.** Wording you would have phrased differently is not drift. Report only what would make a replay fail, or what asks about something the product no longer does.
|
|
16126
16183
|
- \`confidence\` is about the label: how sure you are it is the right one, not how bad the finding is.
|
|
16127
16184
|
|
|
@@ -16131,6 +16188,7 @@ sit nearby.
|
|
|
16131
16188
|
2. \`Grep\` the source for them, at the page, component or handler the step is about.
|
|
16132
16189
|
3. For \`include\` steps, confirm the block exists under \`.ccqa/blocks/<name>/spec.yaml\` and that every \`params\` key is declared on it.
|
|
16133
16190
|
4. When a string is missing, look for what replaced it before concluding. Where it went is what decides the label.
|
|
16191
|
+
5. Before citing any line, read the block that encloses it. Which conditions must hold for it to run, and does the spec put the product in those conditions? A line that only runs in a case the spec never enters proves nothing about the spec.
|
|
16134
16192
|
|
|
16135
16193
|
${guidance.userPromptBlock ?? ""}${guidance.customPromptBlock ?? ""}## Output (STRICT)
|
|
16136
16194
|
|
|
@@ -16488,7 +16546,7 @@ function determineExitCode(results, threshold) {
|
|
|
16488
16546
|
//#endregion
|
|
16489
16547
|
//#region src/drift/to-report.ts
|
|
16490
16548
|
/** Tracks the drift prompt's own version — the two must never drift apart. */
|
|
16491
|
-
const DRIFT_REPORT_PROMPT_VERSION = "
|
|
16549
|
+
const DRIFT_REPORT_PROMPT_VERSION = "7";
|
|
16492
16550
|
/**
|
|
16493
16551
|
* Spec-level status under the given threshold, mirroring determineExitCode's
|
|
16494
16552
|
* per-spec logic (exit-code.ts) but scoped to a single SpecResult.
|
package/dist/package.json
CHANGED