ccqa 1.30.0 → 1.31.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 CHANGED
@@ -7537,6 +7537,7 @@ z.object({
7537
7537
  const SpecDriftEntrySchema = z.object({
7538
7538
  label: DriftLabelSchema.nullable(),
7539
7539
  surface: DriftSurfaceSchema.optional(),
7540
+ subDiagnosis: DriftSubDiagnosisSchema.optional(),
7540
7541
  specChangeKind: SpecChangeKindSchema.optional(),
7541
7542
  confidence: z.number().optional(),
7542
7543
  headline: z.string().optional(),
@@ -13844,8 +13845,12 @@ function createRunTeardown() {
13844
13845
  * A command must install at most one: a second handler that also exits would
13845
13846
  * race this one and could terminate mid-finalizer. A second signal while
13846
13847
  * tearing down hard-exits immediately rather than waiting.
13848
+ *
13849
+ * `onSignal` runs synchronously before the teardown starts, so a finalizer
13850
+ * can already see which signal is ending the process (e.g. `ccqa record`
13851
+ * seals its hub run with a "terminated by signal" note).
13847
13852
  */
13848
- function installTeardownSignalHandlers(teardown) {
13853
+ function installTeardownSignalHandlers(teardown, onSignal) {
13849
13854
  let handling = false;
13850
13855
  const handler = (sig) => {
13851
13856
  const code = sig === "SIGINT" ? 130 : 143;
@@ -13854,6 +13859,7 @@ function installTeardownSignalHandlers(teardown) {
13854
13859
  return;
13855
13860
  }
13856
13861
  handling = true;
13862
+ onSignal?.(sig);
13857
13863
  teardown.run().finally(() => process.exit(code));
13858
13864
  };
13859
13865
  const onInt = () => handler("SIGINT");
@@ -14018,6 +14024,16 @@ function generateSessionName() {
14018
14024
  */
14019
14025
  function buildTraceSystemPrompt(input) {
14020
14026
  const sessionName = input.sessionName ?? generateSessionName();
14027
+ const callerGuidance = input.instruction ? `## Caller Guidance
14028
+
14029
+ The caller provided extra guidance for this recording — for example, a drift
14030
+ audit's finding about what a previous recording of this spec got wrong. Treat
14031
+ it as advice on what to avoid or verify while recording; the spec's steps
14032
+ above remain the contract for what to do.
14033
+
14034
+ ${input.instruction}
14035
+
14036
+ ` : "";
14021
14037
  const stepsText = input.steps.map((step) => `### ${step.id} [${step.source}]
14022
14038
  - **Instruction**: ${step.instruction}
14023
14039
  - **Expected**: ${step.expected}`).join("\n\n");
@@ -14405,7 +14421,7 @@ RUN_COMPLETED|passed|<summary>
14405
14421
  RUN_COMPLETED|failed|<summary>
14406
14422
  \`\`\`
14407
14423
 
14408
- ## Start
14424
+ ${callerGuidance}## Start
14409
14425
 
14410
14426
  Begin by clearing cookies, then proceed straight to the first step's instruction.
14411
14427
 
@@ -14881,7 +14897,8 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
14881
14897
  const baseSystemPrompt = buildTraceSystemPrompt({
14882
14898
  title: spec.title,
14883
14899
  steps: expanded,
14884
- sessionName
14900
+ sessionName,
14901
+ ...opts.instruction ? { instruction: opts.instruction } : {}
14885
14902
  });
14886
14903
  const promptBundle = await loadPromptBundleFromHub(opts.hubContext ?? null, "record");
14887
14904
  if (promptBundle !== null) meta("prompt", promptBundle.loaded.join(" + "));
@@ -14892,7 +14909,7 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
14892
14909
  const statusLines = [];
14893
14910
  let overallStatus = "passed";
14894
14911
  const traceActions = [];
14895
- const stepTracker = createStepTracker();
14912
+ const stepTracker = createStepTracker(opts.onStep);
14896
14913
  const withStepId = (action, stepId) => {
14897
14914
  if (!action) return null;
14898
14915
  return stepId ? {
@@ -15234,15 +15251,18 @@ function groupCountByStep(actions) {
15234
15251
  }
15235
15252
  return counts;
15236
15253
  }
15237
- function createStepTracker() {
15254
+ function createStepTracker(onChange) {
15238
15255
  let currentStepId;
15256
+ const advance = (stepId) => {
15257
+ if (stepId === currentStepId) return;
15258
+ currentStepId = stepId;
15259
+ onChange?.(stepId);
15260
+ };
15239
15261
  return {
15240
15262
  current: () => currentStepId,
15241
- fromStepStartLine: (stepId) => {
15242
- currentStepId = stepId;
15243
- },
15263
+ fromStepStartLine: advance,
15244
15264
  fromCommand: (stepId) => {
15245
- if (stepId) currentStepId = stepId;
15265
+ if (stepId) advance(stepId);
15246
15266
  return currentStepId;
15247
15267
  }
15248
15268
  };
@@ -15500,7 +15520,7 @@ async function runGenerateCli(specPath, opts) {
15500
15520
  //#endregion
15501
15521
  //#region src/cli/record.ts
15502
15522
  const VALIDATION_MODES = ["lenient", "strict"];
15503
- const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Command("record").argument("<feature/spec>", "Spec id in '<feature>/<spec>' form (resolves to .ccqa/features/<feature>/test-cases/<spec>/)").description("Record a test from a spec: run agent-browser to collect actions (trace), then compile them into runnable code via the spec's target (generate) — a vitest test.spec.ts for agent-browser, a @playwright/test spec for the playwright target. Recording-backed targets only; spec-input targets like runn have no trace step (use `ccqa generate`), and agent-browser live specs need no recording.").optionsGroup("How to record:").option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--trace-validation <mode>", "What to do with actions that fail post-trace validation: 'lenient' (default) tags them; 'strict' drops them.", (raw) => {
15523
+ const recordCommand = addHubOptions(addProfileOption(addLanguageOption(new Command("record").argument("<feature/spec>", "Spec id in '<feature>/<spec>' form (resolves to .ccqa/features/<feature>/test-cases/<spec>/)").description("Record a test from a spec: run agent-browser to collect actions (trace), then compile them into runnable code via the spec's target (generate) — a vitest test.spec.ts for agent-browser, a @playwright/test spec for the playwright target. Recording-backed targets only; spec-input targets like runn have no trace step (use `ccqa generate`), and agent-browser live specs need no recording.").optionsGroup("How to record:").option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--instruction <text>", "Extra guidance for the recording agent — e.g. the drift audit's finding when re-recording a drifted spec.").option("--trace-validation <mode>", "What to do with actions that fail post-trace validation: 'lenient' (default) tags them; 'strict' drops them.", (raw) => {
15504
15524
  if (VALIDATION_MODES.includes(raw)) return raw;
15505
15525
  throw new Error(`--trace-validation must be one of ${VALIDATION_MODES.join(" | ")}`);
15506
15526
  }, "lenient").option("--auto-fix <mode>", "Auto-fix behaviour during script generation: 'interactive' (default, prompt y/N; declines on non-TTY), 'auto' (apply without prompt, for CI), 'skip' (agent-browser: apply only high-confidence fixes; external targets like playwright/runn: no fix pass at all).", parseAutoFixFlag, "interactive").option("--auto-fix-max-retries <n>", "Maximum number of auto-fix retries", "3").option("--trace-only", "Stop after the trace step; do not generate test code").option("--no-session-pin", "Don't pin AGENT_BROWSER_SESSION / capture page snapshots after a failure (debug toggle)").optionsGroup("What to do with the result:").option("--overwrite", "Replace an existing test.spec.ts without warning").option("--report-to-hub", "Leave a run (kind: record) on the hub saying this spec was recorded and what the recording spent on Claude, so a budget summed over the hub's runs sees it. It advances no ledger: a recording verifies nothing.").optionsGroup("Learning:").option("--learn-hub-trace-prompt", "After the trace finishes, ask Claude to refresh the \"record.agent\" prompt on the hub from a summary of the run. Requires a hub connection.").optionsGroup("Environment and connection:").option("--cwd <path>", "Working directory containing the .ccqa/ tree (monorepo support). Defaults to the current directory.").option("--project <name>", "Project name for the hub. Defaults to the current directory's name.")))).action(withUsageErrors(async (specPath, opts) => {
@@ -15557,19 +15577,30 @@ async function runRecord(specPath, opts) {
15557
15577
  if (push) info(`hub: record run opened (${push.runId})`);
15558
15578
  let recorded = false;
15559
15579
  let sealed = true;
15580
+ let tracingStep;
15581
+ let killedBy;
15560
15582
  const teardown = createRunTeardown();
15561
15583
  teardown.onFinalize(async () => {
15562
- if (push) sealed = await sealRecordPush(push, featureName, specName, recorded);
15584
+ if (!push) return;
15585
+ const note = killedBy ? `terminated by signal (${killedBy})${tracingStep ? ` during ${tracingStep}` : ""}` : void 0;
15586
+ sealed = await sealRecordPush(push, featureName, specName, recorded, note);
15587
+ });
15588
+ const disposeSignalHandlers = installTeardownSignalHandlers(teardown, (sig) => {
15589
+ killedBy = sig;
15563
15590
  });
15564
- const disposeSignalHandlers = installTeardownSignalHandlers(teardown);
15565
15591
  try {
15566
15592
  let traceResult = null;
15567
15593
  let generated = true;
15568
15594
  try {
15569
15595
  traceResult = await runTrace(featureName, specName, opts.model, opts.traceValidation ?? "lenient", language, {
15570
15596
  cwd: cwdForProfile,
15571
- hubContext
15597
+ hubContext,
15598
+ ...opts.instruction ? { instruction: opts.instruction } : {},
15599
+ onStep: (stepId) => {
15600
+ tracingStep = stepId;
15601
+ }
15572
15602
  });
15603
+ tracingStep = void 0;
15573
15604
  blank();
15574
15605
  if (!opts.traceOnly) generated = (await runGenerate(featureName, specName, {
15575
15606
  maxRetries: parseInt(opts.autoFixMaxRetries ?? "3", 10),
@@ -15608,15 +15639,23 @@ async function runRecord(specPath, opts) {
15608
15639
  * Close the record run with the one row this command produced, answering
15609
15640
  * whether it closed. One spec is recorded per invocation, so one row is the
15610
15641
  * whole run — enough for the runs list to say what the money bought.
15642
+ *
15643
+ * `failureNote` (e.g. "terminated by signal (SIGTERM) during step-03") rides
15644
+ * in the row's `failureLogExcerpt` — the field a failed `ccqa run` row already
15645
+ * uses for its failure text — so the hub says why a recording died instead of
15646
+ * a bare status:"failed". Ignored on a successful recording.
15611
15647
  */
15612
- async function sealRecordPush(push, featureName, specName, recorded) {
15648
+ async function sealRecordPush(push, featureName, specName, recorded, failureNote) {
15613
15649
  return sealHubRun(push, {
15614
- rows: [emptySpecRow({
15615
- feature: featureName,
15616
- spec: specName,
15617
- title: null,
15618
- status: recorded ? "passed" : "failed"
15619
- })],
15650
+ rows: [{
15651
+ ...emptySpecRow({
15652
+ feature: featureName,
15653
+ spec: specName,
15654
+ title: null,
15655
+ status: recorded ? "passed" : "failed"
15656
+ }),
15657
+ ...!recorded && failureNote ? { failureLogExcerpt: failureNote } : {}
15658
+ }],
15620
15659
  reportMeta: {
15621
15660
  git: {
15622
15661
  head: push.gitHead,
@@ -17457,6 +17496,7 @@ function gradedDriftEntry(ledger, key, runId, label) {
17457
17496
  };
17458
17497
  if (label === null) {
17459
17498
  delete graded.surface;
17499
+ delete graded.subDiagnosis;
17460
17500
  delete graded.headline;
17461
17501
  delete graded.confidence;
17462
17502
  }
@@ -17821,9 +17861,11 @@ async function updateDriftLedger(storage, run, results) {
17821
17861
  if (row.status === "skipped") continue;
17822
17862
  const key = `${row.feature}/${row.spec}`;
17823
17863
  const diagnosis = row.analysis ? normalizeDiagnosis(row.analysis) : null;
17864
+ const subDiagnosis = DriftSubDiagnosisSchema.safeParse(diagnosis?.subDiagnosis);
17824
17865
  ledger.specs[key] = {
17825
17866
  label: diagnosis ? diagnosis.label : null,
17826
17867
  surface: diagnosis?.surface,
17868
+ subDiagnosis: subDiagnosis.success ? subDiagnosis.data : void 0,
17827
17869
  specChangeKind: diagnosis?.specChangeKind,
17828
17870
  confidence: diagnosis?.confidence,
17829
17871
  headline: diagnosis?.headline,
@@ -383,6 +383,11 @@ declare const DriftLedgerResponseSchema: z.ZodObject<{
383
383
  spec: "spec";
384
384
  generated: "generated";
385
385
  }>>;
386
+ subDiagnosis: z.ZodOptional<z.ZodEnum<{
387
+ OVER_ASSERTION: "OVER_ASSERTION";
388
+ SELECTOR_DRIFT: "SELECTOR_DRIFT";
389
+ NONE: "NONE";
390
+ }>>;
386
391
  specChangeKind: z.ZodOptional<z.ZodEnum<{
387
392
  FEATURE_REMOVED: "FEATURE_REMOVED";
388
393
  BEHAVIOUR_CHANGED: "BEHAVIOUR_CHANGED";
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.30.0",
3
+ "version": "1.31.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.30.0",
3
+ "version": "1.31.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {