ccqa 1.47.2 → 1.48.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
@@ -16600,6 +16600,8 @@ Selectors in AB_ACTION must follow Selector Rules. \`find_*\` lines use the loca
16600
16600
  - A non-zero exit from agent-browser (selector not found, element not interactable, timeout) → **do NOT emit AB_ACTION** for that attempt. Switch selector and only emit the AB_ACTION for the call that finally succeeded.
16601
16601
  - If you tried several selectors / \`find_*\` locators for the same logical action, emit AB_ACTION for the **last working one only**. Multiple failed attempts in a row will all fail at replay validation and silently delete the step from the generated test.
16602
16602
  - \`AB_ACTION|assert|...\` follows the same rule: only emit assertions you actually verified on the current page in the current snapshot.
16603
+ - **Open only a URL the step names.** A step's \`open\` takes the address its own instruction gives — a \`\${VAR}\` plus the literal tail after it. Never open an address the run produced: the id in \`/items/01H8XZ...\` belongs to the record this run created, and opening it next time visits a record that run does not have (or worse, one it does, silently testing the wrong thing). Reach such a page the way a person does — click through from where the run already is.
16604
+
16603
16605
  - **Environment-failure recovery is not part of the test.** If a session times out, a network blip drops you to login, or the app crashes and you re-login / re-navigate / re-fill to recover, do NOT emit AB_ACTION for the recovery operations.
16604
16606
  - If a step ultimately fails after retries: emit \`ASSERTION_FAILED\` and STOP. Do not leave half-recorded actions in the stream.
16605
16607
 
@@ -22804,6 +22806,9 @@ const HTML_BODY = `
22804
22806
  <button class="fchip" id="persp-chip-manuallyverified" data-f="manuallyVerified" aria-pressed="false" type="button"><span data-i18n="perspectives.rerun.state.manuallyVerified">Manually verified</span><span class="fcount"></span></button>
22805
22807
  <button class="fchip" id="persp-chip-verified" data-f="verified" aria-pressed="false" type="button"><span data-i18n="perspectives.rerun.state.verified">Verified</span><span class="fcount"></span></button>
22806
22808
  </div>
22809
+ <!-- Not a chip: the chips pick which cases to show out of what the
22810
+ project verifies, and this changes what that set is. -->
22811
+ <label class="ftoggle"><input type="checkbox" id="persp-show-disabled"><span data-i18n="perspectives.filter.showDisabled">Show disabled</span><span class="fcount" id="persp-disabled-count"></span></label>
22807
22812
  <div class="spacer"></div>
22808
22813
  <span class="muted persp-head" id="persp-deploy-head" hidden></span>
22809
22814
  <div class="sw-wrap" id="persp-profile-wrap">
@@ -23276,6 +23281,8 @@ const CSS = `
23276
23281
  amber as the drift badges (dr-found), not fail-red. */
23277
23282
  .spec-card.drift-found { border-left: 3px solid var(--amber); }
23278
23283
  .spec-card.passed { border-left: 3px solid var(--pass); }
23284
+ /* Says which card the link landed on; the left rail still carries the verdict. */
23285
+ .spec-card.linked { box-shadow: 0 0 0 2px var(--accent); }
23279
23286
  .spec-card-head { display: flex; align-items: center; gap: 12px; padding: 16px 20px; }
23280
23287
  .spec-card-head .name { font-weight: 600; font-size: 15px; }
23281
23288
  .spec-card-head .slug { font-family: var(--mono); font-size: 12px; color: var(--muted); margin-top: 2px; }
@@ -23619,6 +23626,10 @@ const CSS = `
23619
23626
  /* The Perspectives toolbar carries search + two filter-chip groups + the
23620
23627
  profile selector, so it wraps instead of overflowing on a narrow window. */
23621
23628
  #view-perspectives .toolbar { flex-wrap: wrap; }
23629
+ .ftoggle { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); cursor: pointer; }
23630
+ .ftoggle input { margin: 0; cursor: pointer; }
23631
+ .chip.off { background: var(--surface-2); color: var(--muted-2); border-color: var(--border); }
23632
+ .ftoggle .fcount { margin-left: 2px; font-variant-numeric: tabular-nums; color: var(--muted-2); }
23622
23633
  .proj-menu.right { left: auto; right: 0; }
23623
23634
  .d-note { margin-top: 12px; max-width: 900px; }
23624
23635
 
@@ -23744,7 +23755,7 @@ const CLIENT_JS = `
23744
23755
  // Every status a run can be in, from the contract that defines them, so the
23745
23756
  // runs filter offers exactly what a row's badge can say.
23746
23757
  var RUN_STATUSES = ${JSON.stringify(RunStatusSchema.options)};
23747
- var state = { token: "", project: "", profile: "default", detailRunId: "", jobPollToken: 0, runsLoadToken: 0, spendLoadToken: 0 };
23758
+ var state = { token: "", project: "", profile: "default", detailRunId: "", detailCaseKey: null, detailScrolled: false, jobPollToken: 0, runsLoadToken: 0, spendLoadToken: 0 };
23748
23759
  var knownProfiles = [];
23749
23760
  var TOKEN_KEY = "ccqa-hub-token";
23750
23761
  var LANG_KEY = "ccqa-hub-lang";
@@ -23857,6 +23868,9 @@ const CLIENT_JS = `
23857
23868
  "perspectives.filter.all": "All", "perspectives.filter.deterministic": "Deterministic",
23858
23869
  "perspectives.filter.live": "Live",
23859
23870
  "perspectives.filter.group.mode": "Mode",
23871
+ "perspectives.filter.showDisabled": "Show disabled",
23872
+ "detail.caseNotInRun": "{case} is not in this run.",
23873
+ "perspectives.filter.disabled": "disabled",
23860
23874
  "perspectives.col.verdict": "Verdict", "perspectives.col.audit": "Audit",
23861
23875
  "perspectives.col.run": "Execution",
23862
23876
  "perspectives.audit.state.due": "Audit due",
@@ -24081,6 +24095,9 @@ const CLIENT_JS = `
24081
24095
  "perspectives.filter.all": "すべて", "perspectives.filter.deterministic": "決定的",
24082
24096
  "perspectives.filter.live": "ライブ",
24083
24097
  "perspectives.filter.group.mode": "モード",
24098
+ "perspectives.filter.showDisabled": "無効も表示",
24099
+ "detail.caseNotInRun": "{case} はこの実行に含まれていません。",
24100
+ "perspectives.filter.disabled": "無効",
24084
24101
  "perspectives.col.verdict": "判定", "perspectives.col.audit": "監査",
24085
24102
  "perspectives.col.run": "実行",
24086
24103
  "perspectives.audit.state.due": "監査待ち",
@@ -24689,6 +24706,32 @@ const CLIENT_JS = `
24689
24706
  document.querySelector(".nav-jobs").classList.toggle("disabled", gated);
24690
24707
  }
24691
24708
 
24709
+ // --- pure: run hash ------------------------------------------------------
24710
+ // Lifted out and run by perspectives-visibility.test.ts: the pattern lives
24711
+ // inside a template literal, where a lost backslash fails silently.
24712
+
24713
+ /** A run, optionally opened on one case. */
24714
+ function runCaseHref(runId, caseKey) {
24715
+ var base = "#/runs/" + encodeURIComponent(runId);
24716
+ return caseKey ? base + "?case=" + encodeURIComponent(caseKey) : base;
24717
+ }
24718
+
24719
+ /** The run id and case a #/runs/... hash names, or null when it names no run. */
24720
+ function parseRunHash(hash) {
24721
+ var m = String(hash).match(/^#\\/runs\\/([^?]+)(?:\\?case=(.*))?$/);
24722
+ if (!m) return null;
24723
+ // A truncated paste leaves a half-written %XX, which decodeURIComponent
24724
+ // throws on. Thrown out of route() that reads as "you are disconnected"
24725
+ // and shows the login gate, for a link the reader can see is a run.
24726
+ try {
24727
+ return { runId: decodeURIComponent(m[1]), caseKey: m[2] ? decodeURIComponent(m[2]) : null };
24728
+ } catch (e) {
24729
+ return { runId: m[1], caseKey: m[2] || null };
24730
+ }
24731
+ }
24732
+
24733
+ // --- end pure: run hash --------------------------------------------------
24734
+
24692
24735
  function route() {
24693
24736
  // Disconnected: the full-screen login gate is the only thing to show.
24694
24737
  if (!state.token) { showAuthGate(false); return; }
@@ -24697,8 +24740,8 @@ const CLIENT_JS = `
24697
24740
  // land there (e.g. right after login) instead of an empty Runs list, and
24698
24741
  // gate any deep-linked #/runs or #/secrets to it too.
24699
24742
  if (location.hash === "#/projects" || !state.project) { openProjects(); return; }
24700
- var m = location.hash.match(/^#\\/runs\\/(.+)$/);
24701
- if (m) { openRunDetail(decodeURIComponent(m[1])); return; }
24743
+ var m = parseRunHash(location.hash);
24744
+ if (m) { openRunDetail(m.runId, m.caseKey); return; }
24702
24745
  if (location.hash === "#/perspectives") { openPerspectives(); return; }
24703
24746
  if (location.hash === "#/coverage") { openCoverage(); return; }
24704
24747
  if (location.hash === "#/secrets") { openSecrets(); return; }
@@ -26036,8 +26079,34 @@ const CLIENT_JS = `
26036
26079
  function renderSpecCards(runId, results, triageState, isDrift) {
26037
26080
  var container = document.getElementById("spec-cards");
26038
26081
  clear(container);
26039
- results.forEach(function (r) { container.appendChild(renderSpecCard(runId, r, triageState, isDrift)); });
26082
+ // A slower earlier fetch can land after the reader has moved to another
26083
+ // run. Its cards were always wrong; what is new is that it would also
26084
+ // claim their case is missing from a run that has it.
26085
+ var caseKey = runId === state.detailRunId ? state.detailCaseKey : null;
26086
+ var wanted = null;
26087
+ results.forEach(function (r) {
26088
+ var card = renderSpecCard(runId, r, triageState, isDrift);
26089
+ if (caseKey && r.feature + "/" + r.spec === caseKey) wanted = card;
26090
+ container.appendChild(card);
26091
+ });
26040
26092
  document.getElementById("detail-spec-count").textContent = "· " + results.length;
26093
+ if (!caseKey) return;
26094
+ // Marked as well as scrolled: a page that jumped has to say where it
26095
+ // jumped to, or the reader re-finds the row by hand. Cards are rebuilt
26096
+ // again when triage lands, so the mark is reapplied but the scroll is
26097
+ // not — the reader has been reading since the first one.
26098
+ if (wanted) {
26099
+ wanted.classList.add("linked");
26100
+ if (!state.detailScrolled) {
26101
+ state.detailScrolled = true;
26102
+ wanted.scrollIntoView({ block: "center" });
26103
+ }
26104
+ } else if (!state.detailScrolled) {
26105
+ // The case is not in this run (renamed, or the run predates it). Silent
26106
+ // would read as "it is here somewhere".
26107
+ state.detailScrolled = true;
26108
+ detailError(t("detail.caseNotInRun").replace("{case}", caseKey));
26109
+ }
26041
26110
  }
26042
26111
 
26043
26112
  // ── run detail: triage (confusion matrix) ───────────────────────────
@@ -26235,9 +26304,11 @@ const CLIENT_JS = `
26235
26304
  e.textContent = msg;
26236
26305
  }
26237
26306
 
26238
- function openRunDetail(runId) {
26307
+ function openRunDetail(runId, caseKey) {
26239
26308
  showView("detail");
26240
26309
  state.detailRunId = runId;
26310
+ state.detailCaseKey = caseKey || null;
26311
+ state.detailScrolled = false;
26241
26312
  document.getElementById("detail-title").textContent = runId.slice(0, 8);
26242
26313
  document.getElementById("detail-error").hidden = true;
26243
26314
  document.getElementById("learn-cta").hidden = true;
@@ -26695,7 +26766,8 @@ const CLIENT_JS = `
26695
26766
  var perspState = {
26696
26767
  doc: null, q: "", f: "all",
26697
26768
  rerun: null, rerunSupported: null, runUrls: {}, rerunProfiles: [],
26698
- drift: null
26769
+ drift: null,
26770
+ showDisabled: false
26699
26771
  };
26700
26772
 
26701
26773
  function perspectivesPath() {
@@ -26805,6 +26877,20 @@ const CLIENT_JS = `
26805
26877
  return feature.featureName + "/" + spec.specName;
26806
26878
  }
26807
26879
 
26880
+ // --- pure: perspectives visibility ---------------------------------------
26881
+ // Lifted out and run by perspectives-visibility.test.ts.
26882
+
26883
+ /**
26884
+ * A disabled case is not part of what this project verifies, so it is not in
26885
+ * the answer the page opens on. What the summary counts and what the table
26886
+ * lists both ask here, or the two disagree on how many cases exist.
26887
+ */
26888
+ function perspHidden(spec, showDisabled) {
26889
+ return spec.disabled === true && !showDisabled;
26890
+ }
26891
+
26892
+ // --- end pure: perspectives visibility -----------------------------------
26893
+
26808
26894
  // Shared by rerun and drift, which differ only in which report they read.
26809
26895
  function ledgerEntryFor(report, feature, spec) {
26810
26896
  return report && report.specs ? (report.specs[perspSpecKey(feature, spec)] || null) : null;
@@ -27020,10 +27106,10 @@ const CLIENT_JS = `
27020
27106
  // One ledger entry as "<short sha> · <when>", linking to the hub's run detail
27021
27107
  // and, when that run recorded one, to the CI run. Clicks must not bubble: the
27022
27108
  // table row itself toggles the detail panel.
27023
- function ledgerLine(entry) {
27109
+ function ledgerLine(entry, caseKey) {
27024
27110
  var wrap = el("span");
27025
27111
  var link = el("a", null, shortSha(entry.gitHead) || String(entry.runId).slice(0, 8));
27026
- link.href = "#/runs/" + encodeURIComponent(entry.runId);
27112
+ link.href = runCaseHref(entry.runId, caseKey);
27027
27113
  link.title = t("perspectives.result.openRun");
27028
27114
  link.addEventListener("click", function (e) { e.stopPropagation(); });
27029
27115
  wrap.appendChild(link);
@@ -27151,7 +27237,7 @@ const CLIENT_JS = `
27151
27237
  due: "rr-none", clean: "passed", drifted: "failed", undecided: "rr-unknown"
27152
27238
  };
27153
27239
 
27154
- function perspAuditCell(rr, driftEntry) {
27240
+ function perspAuditCell(rr, driftEntry, caseKey) {
27155
27241
  var td = el("td");
27156
27242
  if (!rr || !rr.audit) {
27157
27243
  td.appendChild(el("span", "muted", "\u2014"));
@@ -27174,7 +27260,7 @@ const CLIENT_JS = `
27174
27260
  // audit's own coordinate, so it comes from the drift ledger.
27175
27261
  if (driftEntry) {
27176
27262
  var sub = el("span", "cellsub");
27177
- sub.appendChild(ledgerLine(driftEntry));
27263
+ sub.appendChild(ledgerLine(driftEntry, caseKey));
27178
27264
  if (driftEntry.graded) {
27179
27265
  sub.appendChild(document.createTextNode(" · "));
27180
27266
  sub.appendChild(el("span", "graded-mark", t("perspectives.drift.graded")));
@@ -27184,7 +27270,7 @@ const CLIENT_JS = `
27184
27270
  return td;
27185
27271
  }
27186
27272
 
27187
- function perspRunCell(rr) {
27273
+ function perspRunCell(rr, caseKey) {
27188
27274
  var td = el("td");
27189
27275
  var runState = perspRunState(rr);
27190
27276
  // No verdict at all: this case is in the document but not in the report
@@ -27218,7 +27304,7 @@ const CLIENT_JS = `
27218
27304
  var last = lastResult(rr);
27219
27305
  if (last) {
27220
27306
  var sub = el("span", "cellsub");
27221
- sub.appendChild(ledgerLine(last.entry));
27307
+ sub.appendChild(ledgerLine(last.entry, caseKey));
27222
27308
  td.appendChild(sub);
27223
27309
  }
27224
27310
  }
@@ -27290,16 +27376,29 @@ const CLIENT_JS = `
27290
27376
  var host = document.getElementById("persp-ov");
27291
27377
  clear(host);
27292
27378
  var records = [];
27379
+ var features = 0;
27380
+ var listed = 0;
27293
27381
  doc.features.forEach(function (feature) {
27382
+ var before = listed;
27294
27383
  feature.specs.forEach(function (spec) {
27295
- records.push(ledgerEntryFor(perspState.rerun, feature, spec));
27384
+ if (perspHidden(spec, perspState.showDisabled)) return;
27385
+ listed += 1;
27386
+ // A disabled case has no ledger entry — the hub does not ask for one
27387
+ // — and an absent entry reads as "needs re-run". Counting it would
27388
+ // put work in the bar that nobody intends to do.
27389
+ if (spec.disabled !== true) records.push(ledgerEntryFor(perspState.rerun, feature, spec));
27296
27390
  });
27391
+ // A feature whose cases are all hidden renders no group below, so
27392
+ // counting it here would put the summary above a table that disagrees.
27393
+ if (listed > before) features += 1;
27297
27394
  });
27298
27395
 
27299
27396
  var inv = el("div", "ov-inv");
27300
- inv.appendChild(el("b", null, String(records.length)));
27397
+ // The inventory counts what the table lists; the bars below judge only the
27398
+ // cases the hub keeps a ledger for, so the two numbers can differ.
27399
+ inv.appendChild(el("b", null, String(listed)));
27301
27400
  inv.appendChild(document.createTextNode(" " + t("perspectives.ov.cases") + " / "));
27302
- inv.appendChild(el("b", null, String(doc.features.length)));
27401
+ inv.appendChild(el("b", null, String(features)));
27303
27402
  inv.appendChild(document.createTextNode(" " + t("perspectives.ov.features")));
27304
27403
  host.appendChild(inv);
27305
27404
  if (!records.length) return;
@@ -27326,6 +27425,12 @@ const CLIENT_JS = `
27326
27425
  // search text always applies: a chip's count has to be the number of rows
27327
27426
  // clicking it actually leaves.
27328
27427
  function perspMatches(feature, spec, f) {
27428
+ if (perspHidden(spec, perspState.showDisabled)) return false;
27429
+ return perspMatchesFilters(feature, spec, f);
27430
+ }
27431
+
27432
+ /** Everything the toolbar filters on except whether the case is disabled. */
27433
+ function perspMatchesFilters(feature, spec, f) {
27329
27434
  if (f === "deterministic" && perspMode(spec) !== "deterministic") return false;
27330
27435
  if (f === "live" && perspMode(spec) !== "live") return false;
27331
27436
  // The verdict chips (same words as RERUN_ORDER/the 判定 column) share
@@ -27345,12 +27450,12 @@ const CLIENT_JS = `
27345
27450
  return true;
27346
27451
  }
27347
27452
 
27348
- function perspFilterCount(f) {
27453
+ function perspCount(pred) {
27349
27454
  var doc = perspState.doc;
27350
27455
  if (!doc) return 0;
27351
27456
  var n = 0;
27352
27457
  doc.features.forEach(function (feature) {
27353
- feature.specs.forEach(function (spec) { if (perspMatches(feature, spec, f)) n += 1; });
27458
+ feature.specs.forEach(function (spec) { if (pred(feature, spec)) n += 1; });
27354
27459
  });
27355
27460
  return n;
27356
27461
  }
@@ -27842,9 +27947,13 @@ const CLIENT_JS = `
27842
27947
  row.tabIndex = 0;
27843
27948
  row.setAttribute("aria-expanded", "false");
27844
27949
 
27950
+ var caseKey = perspSpecKey(feature, spec);
27845
27951
  var titleTd = el("td", "c-title");
27846
27952
  titleTd.appendChild(document.createTextNode(spec.title));
27847
- titleTd.appendChild(el("span", "c-id", perspSpecKey(feature, spec)));
27953
+ // Only reachable with the toggle on, where saying which rows are the
27954
+ // disabled ones is the whole point of having turned it on.
27955
+ if (spec.disabled === true) titleTd.appendChild(el("span", "chip off", t("perspectives.filter.disabled")));
27956
+ titleTd.appendChild(el("span", "c-id", caseKey));
27848
27957
  if (spec.summary) titleTd.appendChild(el("span", "csum", spec.summary));
27849
27958
  row.appendChild(titleTd);
27850
27959
 
@@ -27855,8 +27964,8 @@ const CLIENT_JS = `
27855
27964
  if (showRerun) {
27856
27965
  var rr = ledgerEntryFor(perspState.rerun, feature, spec);
27857
27966
  row.appendChild(perspVerdictCell(rr));
27858
- row.appendChild(perspRunCell(rr));
27859
- row.appendChild(perspAuditCell(rr, ledgerEntryFor(perspState.drift, feature, spec)));
27967
+ row.appendChild(perspRunCell(rr, caseKey));
27968
+ row.appendChild(perspAuditCell(rr, ledgerEntryFor(perspState.drift, feature, spec), caseKey));
27860
27969
  }
27861
27970
 
27862
27971
  var chevTd = el("td", "c-chev");
@@ -27908,13 +28017,23 @@ const CLIENT_JS = `
27908
28017
  // Each chip also carries what it would yield — the mode breakdown the
27909
28018
  // summary row used to spend four tiles on.
27910
28019
  function syncPerspChips() {
28020
+ // What ticking the box would actually add: disabled cases that the filters
28021
+ // in effect would then let through. A flat count of every disabled case
28022
+ // promises rows the active chip or search would drop.
28023
+ var off = perspState.showDisabled ? 0 : perspCount(function (feature, spec) {
28024
+ return spec.disabled === true && perspMatchesFilters(feature, spec, perspState.f);
28025
+ });
28026
+ document.getElementById("persp-disabled-count").textContent = off ? "+" + off : "";
28027
+ document.getElementById("persp-show-disabled").checked = perspState.showDisabled;
27911
28028
  var group = document.getElementById("persp-verdict-chips");
27912
28029
  group.hidden = perspState.rerunSupported !== true;
27913
28030
  if (perspState.rerunSupported !== true && RERUN_ORDER.indexOf(perspState.f) !== -1) perspState.f = "all";
27914
28031
  document.querySelectorAll("#view-perspectives .fchip").forEach(function (b) {
27915
28032
  var f = b.getAttribute("data-f");
27916
28033
  b.setAttribute("aria-pressed", String(f === perspState.f));
27917
- b.querySelector(".fcount").textContent = perspState.doc ? String(perspFilterCount(f)) : "";
28034
+ b.querySelector(".fcount").textContent = perspState.doc
28035
+ ? String(perspCount(function (feature, spec) { return perspMatches(feature, spec, f); }))
28036
+ : "";
27918
28037
  });
27919
28038
  }
27920
28039
 
@@ -28620,6 +28739,10 @@ const CLIENT_JS = `
28620
28739
  perspState.q = e.target.value.trim().toLowerCase();
28621
28740
  renderPerspectives();
28622
28741
  });
28742
+ document.getElementById("persp-show-disabled").addEventListener("change", function (e) {
28743
+ perspState.showDisabled = e.target.checked;
28744
+ renderPerspectives();
28745
+ });
28623
28746
 
28624
28747
  // Debounced, unlike persp-q: each keystroke rebuilds the whole tree, and
28625
28748
  // the universe can hold thousands of files.
@@ -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";
39
38
  run: "run";
40
39
  drift: "drift";
40
+ record: "record";
41
41
  }>>;
42
42
  drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
43
43
  specs: z.ZodNumber;
@@ -811,9 +811,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
811
811
  declare const RunReportDataSchema: z.ZodObject<{
812
812
  schemaVersion: z.ZodLiteral<1>;
813
813
  kind: z.ZodDefault<z.ZodEnum<{
814
- record: "record";
815
814
  run: "run";
816
815
  drift: "drift";
816
+ record: "record";
817
817
  }>>;
818
818
  createdAt: z.ZodString;
819
819
  runId: z.ZodNullable<z.ZodString>;
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.47.2",
3
+ "version": "1.48.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.47.2",
3
+ "version": "1.48.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {