ccqa 1.1.0 → 1.2.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
@@ -429,26 +429,6 @@ async function removeLegacyPerspectivesFiles(cwd) {
429
429
  for (const path of candidates) if (await unlink(path).then(() => true).catch(() => false)) removed.push(path);
430
430
  return removed;
431
431
  }
432
- /**
433
- * Replace (or insert) the `relatedPaths` key in the spec. Preserves every
434
- * other top-level field and the entire steps array. Returns the absolute
435
- * path that was written, or null if the spec file does not exist.
436
- */
437
- async function updateSpecRelatedPaths(featureName, specName, relatedPaths, cwd) {
438
- const specPath = join(getSpecDir(featureName, specName, cwd), SPEC_FILE);
439
- const existing = await readFile(specPath, "utf-8").catch(() => null);
440
- if (existing === null) return null;
441
- await writeFile(specPath, stringify(stripUndefined({
442
- ...parseTestSpec(existing, specPath),
443
- relatedPaths: relatedPaths.length > 0 ? relatedPaths : void 0
444
- }), { lineWidth: 0 }), "utf-8");
445
- return specPath;
446
- }
447
- function stripUndefined(obj) {
448
- const out = {};
449
- for (const [k, v] of Object.entries(obj)) if (v !== void 0) out[k] = v;
450
- return out;
451
- }
452
432
  const LEGACY_RECORDING_FILES = ["actions.json", "route.md"];
453
433
  async function saveRecording(featureName, specName, actions, cwd) {
454
434
  const specDir = getSpecDir(featureName, specName, cwd);
@@ -1891,7 +1871,7 @@ ${formatBlockList(blocks)}
1891
1871
 
1892
1872
  1. Read the codebase under cwd to find concrete strings: routes, button labels, aria-labels, page titles, placeholders. Use those exact strings in \`expected\`.
1893
1873
  2. If you use \`include:\` steps, verify each \`params\` key matches a declared param of the block (see the Available blocks list above).
1894
- 3. Populate \`relatedPaths\` with **provisional** glob patterns pointing at the source files this spec touches: the route/page file for each URL the spec visits, plus the component files (or their parent feature directory) that render the aria-labels, placeholders, or visible texts the spec asserts on. Prefer directory globs (e.g. \`src/features/tasks/**\`) when several files in one area are involved. Be conservative — include a path if you're unsure rather than omit it. \`ccqa trace\` will refine this list later from real browser observations.
1874
+ 3. Populate \`relatedPaths\` with glob patterns pointing at the source files this spec depends on — this is the spec's final, authoritative list (nothing else updates it later, so get it right here): the route/page file for each URL the spec visits, plus the component files (or their parent feature directory) that render the aria-labels, placeholders, or visible texts the spec asserts on. Prefer directory globs (e.g. \`src/features/tasks/**\`) when several files in one area are involved. Confirm each path actually exists via \`Glob\`/\`Read\` before listing it — never guess a path by convention. Be conservative — when unsure whether a real path is relevant, include it rather than omit it (a missing path means a code change silently escapes \`drift --changed\`).
1895
1875
  4. Validate the (current or proposed) spec on four axes — emit one issue per finding:
1896
1876
  - **assertable**: each \`expected\` can be verified against a string/URL/state that exists in code.
1897
1877
  - **blocks**: every \`include\` resolves to a real block; every \`params\` key is declared on that block; every required param is provided.
@@ -5527,7 +5507,7 @@ async function runLiveSpecs(specs, opts) {
5527
5507
  if (userPromptBundle !== null) meta("prompt", userPromptBundle.loaded.join(" + "));
5528
5508
  const userPromptSuffix = userPromptBundle?.text ?? null;
5529
5509
  const failureAnalysisEnabled = opts.failureAnalysis !== false;
5530
- const driftBySpec = failureAnalysisEnabled && opts.driftAudit !== false ? await runDriftAudit(specs, opts, cwd) : /* @__PURE__ */ new Map();
5510
+ const driftAuditEnabled = failureAnalysisEnabled && opts.driftAudit !== false;
5531
5511
  const auth = failureAnalysisEnabled ? driftAuthAvailable() : {
5532
5512
  ok: false,
5533
5513
  reason: "disabled"
@@ -5562,12 +5542,12 @@ async function runLiveSpecs(specs, opts) {
5562
5542
  row: null
5563
5543
  };
5564
5544
  const row = await buildLiveReportRow(outcome, {
5565
- driftBySpec,
5566
5545
  auth,
5567
5546
  diff,
5568
5547
  baseRef,
5569
5548
  reportDir,
5570
- failureAnalysisEnabled
5549
+ failureAnalysisEnabled,
5550
+ driftAuditEnabled
5571
5551
  }, opts, cwd);
5572
5552
  await opts.report?.upsert(row);
5573
5553
  return {
@@ -5587,13 +5567,13 @@ async function runLiveSpecs(specs, opts) {
5587
5567
  };
5588
5568
  }
5589
5569
  /**
5590
- * Build one spec's report row: the live-run base row plus drift findings and
5591
- * (for a failed spec) the failure-analysis fields. Runs inside the pool worker
5592
- * so the row can be upserted incrementally the moment the spec finishes.
5570
+ * Build one spec's report row: the live-run base row plus (for a failed spec)
5571
+ * the drift audit and failure-analysis fields. Runs inside the pool worker so
5572
+ * the row can be upserted incrementally the moment the spec finishes. The
5573
+ * drift audit only runs for failed specs — passing specs get no driftIssues,
5574
+ * matching the deterministic path.
5593
5575
  */
5594
5576
  async function buildLiveReportRow(r, ctx, opts, cwd) {
5595
- const key = `${r.featureName}/${r.specName}`;
5596
- const driftForSpec = ctx.driftBySpec.get(key) ?? null;
5597
5577
  const base = await liveRunToReportResult({
5598
5578
  featureName: r.featureName,
5599
5579
  specName: r.specName,
@@ -5601,6 +5581,7 @@ async function buildLiveReportRow(r, ctx, opts, cwd) {
5601
5581
  result: r.result,
5602
5582
  reportDir: ctx.reportDir
5603
5583
  });
5584
+ const driftForSpec = ctx.driftAuditEnabled && r.result.status === "failed" ? await runDriftAuditOne(r, opts, cwd) : null;
5604
5585
  const analysis = ctx.failureAnalysisEnabled && r.result.status === "failed" ? await analyzeOneLiveFailure(r, ctx.diff, ctx.baseRef, driftForSpec, ctx.auth, opts, cwd) : void 0;
5605
5586
  return {
5606
5587
  ...base,
@@ -5624,39 +5605,31 @@ function analysisFieldsFor(a, status, failureAnalysisEnabled) {
5624
5605
  return {};
5625
5606
  }
5626
5607
  /**
5627
- * Run `analyzeDrift` against every spec and return a
5628
- * `featureName/specName driftIssues` map. This is a static spec↔code audit,
5629
- * so it takes the spec list directly and runs before execution — each worker
5630
- * then has its spec's drift context in hand for the failure-analysis prompt.
5631
- * Drift findings are advisory — they show in the report (report.json / hub UI)
5632
- * but do not change the live-run exit code.
5608
+ * Run `analyzeDrift` for one failed spec, used as evidence for its
5609
+ * failure-analysis prompt and shown in its report row. Mirrors the
5610
+ * deterministic path (src/run/pipeline.ts), which also scopes the audit to
5611
+ * failed specs only. Drift findings are advisory they never change the
5612
+ * live-run exit code.
5633
5613
  */
5634
- async function runDriftAudit(specs, opts, cwd) {
5635
- const targets = specs.map((s) => ({
5636
- featureName: s.featureName,
5637
- specName: s.specName
5638
- }));
5639
- const out = /* @__PURE__ */ new Map();
5640
- if (targets.length === 0) return out;
5641
- blank();
5642
- info(`drift audit: ${targets.length} spec${targets.length > 1 ? "s" : ""}`);
5643
- const results = await analyzeDrift({
5644
- targets,
5614
+ async function runDriftAuditOne(r, opts, cwd) {
5615
+ const key = `${r.featureName}/${r.specName}`;
5616
+ info(`drift audit: ${key}`);
5617
+ const blocks = await loadAvailableBlocks(cwd);
5618
+ const [result] = await analyzeDrift({
5619
+ targets: [{
5620
+ featureName: r.featureName,
5621
+ specName: r.specName
5622
+ }],
5645
5623
  cwd,
5646
- blocks: await loadAvailableBlocks(cwd),
5624
+ blocks,
5647
5625
  ...opts.model ? { model: opts.model } : {},
5648
- ...opts.language ? { language: opts.language } : {},
5649
- onSpecStart: (t) => info(` checking ${t.featureName}/${t.specName}`)
5626
+ ...opts.language ? { language: opts.language } : {}
5650
5627
  });
5651
- for (const r of results) {
5652
- const key = `${r.target.featureName}/${r.target.specName}`;
5653
- if (r.ok) out.set(key, r.issues.length > 0 ? r.issues : null);
5654
- else {
5655
- warn(`drift audit failed for ${key}: ${r.error}`);
5656
- out.set(key, null);
5657
- }
5628
+ if (!result || !result.ok) {
5629
+ warn(`drift audit failed for ${key}: ${result?.error ?? "no result"}`);
5630
+ return null;
5658
5631
  }
5659
- return out;
5632
+ return result.issues.length > 0 ? result.issues : null;
5660
5633
  }
5661
5634
  /**
5662
5635
  * Resolve `spec.session` names to a single state file to restore, fetching
@@ -10009,7 +9982,6 @@ function buildTraceSystemPrompt(input) {
10009
9982
  const stepsText = input.steps.map((step) => `### ${step.id} [${step.source}]
10010
9983
  - **Instruction**: ${step.instruction}
10011
9984
  - **Expected**: ${step.expected}`).join("\n\n");
10012
- const relatedPathsBlock = buildRelatedPathsInstruction();
10013
9985
  return `You are an expert QA engineer executing a browser E2E test. Execute each step precisely and record every browser action as a structured log line.
10014
9986
 
10015
9987
  ## Session
@@ -10279,6 +10251,9 @@ CCQA_STEP=<step-id> CCQA_ASSERT=url_contains:/dashboard agent-browser --session
10279
10251
  - A marked command that exits non-zero records nothing — fix the check and re-run it.
10280
10252
  - A marker that doesn't match its command (e.g. \`CCQA_ASSERT=1\` on a \`click\`) is ignored with a warning — never do that.
10281
10253
  - \`get count\` and \`get url\` exit 0 regardless of what they print. **Read the output**: if the printed count / URL contradicts the marker, the signal did NOT verify — treat it as a failed verification (emit \`ASSERTION_FAILED\` if the step cannot be confirmed another way).
10254
+ - **\`url_contains\` is opt-in, not a habit.** Emit it ONLY when a step's own \`expected\` explicitly asks about the URL or path. Do NOT add a \`url_contains\` to "prove" a login succeeded, a page loaded, or a navigation happened — confirm those with \`text_visible\` / \`element_visible\` on something the destination page renders. An unrequested URL assertion adds no coverage the visible-content assert doesn't already give, and is the single most common way an environment gets baked into a test.
10255
+ - **When you do assert a URL, the substring may come from ONE place only:** a \`\${VAR}\` URL that *this step's own instruction* opened, written as that \`\${VAR}\` followed by the literal tail after it. If the step opens \`\${APP_URL}/policies\`, assert \`\${APP_URL}/policies\` (or the tail \`/policies\`); the recorder resolves \`\${APP_URL}\` per environment. Never assert on a URL you merely *observed* — login redirects, identity-provider pages, and OAuth callbacks all live on a **different, environment-named origin** than the app, so any substring of them (host, origin, OR path) names the environment.
10256
+ - **A leading slash does NOT make a substring safe.** \`/auth-staging\`, \`/env-qa\`, \`/tenant-acme\` look like paths but are environment labels — the first segment of an identity-provider or tenant URL, not an application route. If a substring contains an environment name, a stage token (\`dev\`, \`stg\`, \`prod\`), a tenant/org name, or any fragment of a hostname, it is forbidden even with a leading slash. The only safe path substrings are stable *application* routes off the app's own origin (\`/dashboard\`, \`/policies/new\`), taken from a \`\${VAR}\` you opened — not from a redirect you watched.
10282
10257
 
10283
10258
  **Fallback text protocol** — ONLY for assert types the markers cannot express
10284
10259
  (\`element_enabled\`, \`element_disabled\`, \`element_checked\`,
@@ -10389,7 +10364,7 @@ RUN_COMPLETED|passed|<summary>
10389
10364
  RUN_COMPLETED|failed|<summary>
10390
10365
  \`\`\`
10391
10366
 
10392
- ${relatedPathsBlock}## Start
10367
+ ## Start
10393
10368
 
10394
10369
  Begin by clearing cookies, then proceed straight to the first step's instruction.
10395
10370
 
@@ -10408,130 +10383,12 @@ AB_ACTION|cookies_clear
10408
10383
  Then emit \`STEP_START|step-01|...\` and execute the first step, prefixing
10409
10384
  every one of its agent-browser commands with \`CCQA_STEP=step-01\`. The first
10410
10385
  step is responsible for opening the initial URL.
10411
- `;
10412
- }
10413
- function buildRelatedPathsInstruction() {
10414
- return `## Post-run: emit \`relatedPaths\` block
10415
-
10416
- After all steps are complete (regardless of pass/fail) and **before** \`RUN_COMPLETED\`, you MUST emit a single \`RELATED_PATHS\` block. The host (not you) writes these paths into the spec — your only job is to emit the block.
10417
-
10418
- \`relatedPaths\` is a list of glob patterns identifying the source files this spec depends on. CI uses them to decide whether a code change should trigger a drift check for this spec.
10419
-
10420
- **Do NOT modify any source files.** You have only \`Read\`, \`Grep\`, and \`Glob\` for source inspection. The block you emit is the only output the host uses to update the spec.
10421
-
10422
- **Inputs to consider:**
10423
- - The URLs you opened (\`AB_ACTION|open|...\`)
10424
- - The aria-labels, placeholders, and visible texts you clicked / filled / waited on
10425
- - The component / page / route files that render those strings (find them with \`Grep\`/\`Read\`/\`Glob\`)
10426
-
10427
- **How to choose paths:**
10428
- 1. For each URL the test navigates to, locate the route/page file and include it (e.g. \`src/app/tasks/page.tsx\`, \`src/pages/tasks/index.tsx\`).
10429
- 2. For each unique aria-label / placeholder / visible text you interacted with, \`Grep\` the codebase, find the defining component, and include either the file or its parent feature directory.
10430
- 3. Prefer **directory globs** (e.g. \`src/features/tasks/**\`) over individual files when several related components live in the same area. Otherwise list specific files.
10431
- 4. Skip third-party files (\`node_modules/\`), build output (\`dist/\`, \`.next/\`), and generated code.
10432
- 5. Be conservative — false positives (extra paths) are fine; false negatives (missing paths) cause drift to be missed in CI. When unsure whether a path is relevant, include it.
10433
- 6. **Path base:** a path inside this working directory MUST be relative to it — the same base your \`Glob\`/\`Grep\` tools use (\`src/features/tasks/**\`, NOT the repo-root form \`apps/foo/src/features/tasks/**\`). For a file in a monorepo sibling package this spec genuinely depends on, use its **repo-root-relative** path (e.g. \`packages/ui-kit/src/FilePicker.tsx\`); never emit \`../\` forms.
10434
- 7. **Shared / design-system components:** include one only when the spec's checks depend on that component's specific behaviour (e.g. the flow exercises its file picker). Do NOT list generic primitives every screen uses (buttons, tags, dialogs, layout) — they would scope this spec into almost every UI change.
10435
-
10436
- **Output format (STRICT — one line per path, no leading dashes, no commentary inside the block):**
10437
-
10438
- \`\`\`
10439
- RELATED_PATHS_BEGIN
10440
- src/features/tasks/**
10441
- src/app/tasks/page.tsx
10442
- RELATED_PATHS_END
10443
- \`\`\`
10444
-
10445
- Emit the block outside any other code block, on its own lines. If the test could not exercise the feature at all (e.g. blocked early), emit the block anyway with whatever paths you can identify; emit \`RELATED_PATHS_BEGIN\` immediately followed by \`RELATED_PATHS_END\` only if you genuinely could not identify any related file.
10446
-
10447
10386
  `;
10448
10387
  }
10449
10388
  function buildTracePrompt(title) {
10450
10389
  return `Execute the test for "${title}". Each step's instruction includes the URL or selector context it needs.`;
10451
10390
  }
10452
10391
  //#endregion
10453
- //#region src/drift/parse-related-paths.ts
10454
- /**
10455
- * Pull a `RELATED_PATHS_BEGIN ... RELATED_PATHS_END` block out of the trace
10456
- * agent's combined text output. Lines inside the block become entries; blank
10457
- * lines, bullet markers, and code fences are tolerated. Returns null when the
10458
- * agent did not emit a block at all so the caller can warn instead of silently
10459
- * clearing the spec's existing relatedPaths.
10460
- */
10461
- function parseRelatedPathsBlock(text) {
10462
- const match = text.match(/RELATED_PATHS_BEGIN\s*\n?([\s\S]*?)\n?RELATED_PATHS_END/);
10463
- if (!match || match[1] === void 0) return null;
10464
- const seen = /* @__PURE__ */ new Set();
10465
- const out = [];
10466
- for (const raw of match[1].split("\n")) {
10467
- const line = raw.replace(/^```.*$/, "").trim();
10468
- if (!line) continue;
10469
- const cleaned = line.replace(/^[-*]\s+/, "").trim();
10470
- if (!cleaned || seen.has(cleaned)) continue;
10471
- seen.add(cleaned);
10472
- out.push(cleaned);
10473
- }
10474
- return out;
10475
- }
10476
- /**
10477
- * Normalize parsed entries to the canonical bases the drift matcher expects:
10478
- * app-relative for files inside the working directory, repo-root-relative
10479
- * for files outside it. Models sometimes emit an in-app path in repo-root
10480
- * form (`apps/foo/src/...`) or an outside-package path in `../` form — both
10481
- * would silently never match.
10482
- *
10483
- * `cwdPrefix` is the working directory's path relative to the repo root
10484
- * ("" when they coincide, unknown → pass `null` to skip prefix handling).
10485
- */
10486
- function normalizeRelatedPaths(paths, cwdPrefix) {
10487
- const out = [];
10488
- const warnings = [];
10489
- const seen = /* @__PURE__ */ new Set();
10490
- const push = (p) => {
10491
- if (!seen.has(p)) {
10492
- seen.add(p);
10493
- out.push(p);
10494
- }
10495
- };
10496
- for (const raw of paths) {
10497
- const p = raw.replace(/^\.\//, "");
10498
- if (cwdPrefix !== null && cwdPrefix !== "" && p.startsWith(`${cwdPrefix}/`)) {
10499
- push(p.slice(cwdPrefix.length + 1));
10500
- continue;
10501
- }
10502
- if (p.startsWith("../")) {
10503
- if (cwdPrefix === null || cwdPrefix === "") {
10504
- warnings.push(`dropped relatedPaths entry "${raw}" — it points outside the repository`);
10505
- continue;
10506
- }
10507
- const segments = [...cwdPrefix.split("/"), ...p.split("/")];
10508
- const resolved = [];
10509
- let escaped = false;
10510
- for (const seg of segments) {
10511
- if (seg === "" || seg === ".") continue;
10512
- if (seg === "..") {
10513
- if (resolved.length === 0) {
10514
- escaped = true;
10515
- break;
10516
- }
10517
- resolved.pop();
10518
- } else resolved.push(seg);
10519
- }
10520
- if (escaped || resolved.length === 0) {
10521
- warnings.push(`dropped relatedPaths entry "${raw}" — it points outside the repository`);
10522
- continue;
10523
- }
10524
- push(resolved.join("/"));
10525
- continue;
10526
- }
10527
- push(p);
10528
- }
10529
- return {
10530
- paths: out,
10531
- warnings
10532
- };
10533
- }
10534
- //#endregion
10535
10392
  //#region src/runtime/replay-validate.ts
10536
10393
  function isPollCheck(x) {
10537
10394
  return x !== null && !Array.isArray(x) && x.kind === "poll-present";
@@ -11036,7 +10893,11 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
11036
10893
  const expanded = expandSpec(spec, { blocks: await loadAllBlocks(opts.cwd) });
11037
10894
  const envScrub = buildSpecEnvScrub(spec, expanded);
11038
10895
  const envScrubMap = envScrub.map;
11039
- if (envScrub.unresolved.length > 0) warn(`spec references env var(s) with empty/unset values: ${envScrub.unresolved.join(", ")} — their literal trace-time values will be baked into ir.json`);
10896
+ if (envScrub.unresolved.length > 0) {
10897
+ warn(`spec references env var(s) that are unset at record time: ${envScrub.unresolved.join(", ")}`);
10898
+ warn("their concrete trace-time values (e.g. navigate URLs) will be baked into ir.json instead of kept as ${VAR}, so the recording won't switch across environments.");
10899
+ hint("load these vars before recording — pass --profile <name> (hub) or define them in .env — then re-record.");
10900
+ }
11040
10901
  meta("spec", spec.title);
11041
10902
  meta("steps", expanded.length);
11042
10903
  const includes = collectIncludedBlockNames(spec);
@@ -11058,7 +10919,6 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
11058
10919
  let overallStatus = "passed";
11059
10920
  const traceActions = [];
11060
10921
  const stepTracker = createStepTracker();
11061
- let relatedPathsBuffer = null;
11062
10922
  const withStepId = (action, stepId) => {
11063
10923
  if (!action) return null;
11064
10924
  return stepId ? {
@@ -11105,13 +10965,7 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
11105
10965
  if (msg.type !== "assistant") return;
11106
10966
  for (const block of msg.message.content ?? []) {
11107
10967
  if (block.type !== "text" || !block.text) continue;
11108
- const text = block.text;
11109
- if (relatedPathsBuffer !== null) relatedPathsBuffer += text + "\n";
11110
- else {
11111
- const idx = text.indexOf("RELATED_PATHS_BEGIN");
11112
- if (idx !== -1) relatedPathsBuffer = text.slice(idx) + "\n";
11113
- }
11114
- for (const line of text.split("\n")) {
10968
+ for (const line of block.text.split("\n")) {
11115
10969
  const trimmed = line.trim();
11116
10970
  const status = parseStatusLine(line);
11117
10971
  if (status) {
@@ -11136,13 +10990,6 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
11136
10990
  meta("saved", recordingPath);
11137
10991
  meta("actions", validatedActions.length);
11138
10992
  meta("status", overallStatus.toUpperCase());
11139
- const parsedRelatedPaths = relatedPathsBuffer !== null ? parseRelatedPathsBlock(relatedPathsBuffer) : null;
11140
- if (parsedRelatedPaths !== null) {
11141
- const { paths: relatedPaths, warnings } = normalizeRelatedPaths(parsedRelatedPaths, await resolveCwdPrefix(opts.cwd));
11142
- for (const w of warnings) warn(w);
11143
- const written = await updateSpecRelatedPaths(featureName, specName, relatedPaths, opts.cwd);
11144
- if (written) meta("relatedPaths", `${relatedPaths.length} path(s) written to ${written}`);
11145
- } else warn("trace did not emit a RELATED_PATHS block; drift --changed cannot scope this spec");
11146
10993
  hint(`run 'ccqa generate ${featureName}/${specName}' to generate a test script`);
11147
10994
  return {
11148
10995
  status: overallStatus,
@@ -11436,23 +11283,6 @@ function parseStatusLine(text) {
11436
11283
  }
11437
11284
  return null;
11438
11285
  }
11439
- /**
11440
- * The working directory's path relative to the git repo root ("" when they
11441
- * coincide), used to pin relatedPaths bases. Null when git is unavailable —
11442
- * normalization then skips prefix handling rather than guessing.
11443
- */
11444
- async function resolveCwdPrefix(cwd) {
11445
- const { execFile } = await import("node:child_process");
11446
- const { promisify } = await import("node:util");
11447
- const { relative, resolve } = await import("node:path");
11448
- const dir = resolve(cwd ?? process.cwd());
11449
- try {
11450
- const { stdout } = await promisify(execFile)("git", ["rev-parse", "--show-toplevel"], { cwd: dir });
11451
- return relative(stdout.trim(), dir);
11452
- } catch {
11453
- return null;
11454
- }
11455
- }
11456
11286
  //#endregion
11457
11287
  //#region src/prompts/perspectives.ts
11458
11288
  /**
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.1.0",
3
+ "version": "1.2.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.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {