ccqa 1.47.3 → 1.48.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/dist/bin/ccqa.mjs CHANGED
@@ -9936,6 +9936,12 @@ const HUB_READ_CONCURRENCY = 4;
9936
9936
  * source that cannot be read warns, and `degraded` flips when the failure
9937
9937
  * leaves absence ambiguous (the ledger itself, or the legacy sources while
9938
9938
  * no ledger answers).
9939
+ *
9940
+ * The ledger is read first and, once it answers, alone. The legacy sources
9941
+ * resolve every retained run against the whole event stream — minutes each on
9942
+ * a hub of any age — and reading them alongside a ledger that answers in one
9943
+ * request meant waiting for the slow source to confirm what the fast one had
9944
+ * already said (and then discarding it, since the ledger wins).
9939
9945
  */
9940
9946
  async function loadCoverageEdges(input) {
9941
9947
  if (input == null) return {
@@ -9947,11 +9953,9 @@ async function loadCoverageEdges(input) {
9947
9953
  const existing = candidates.get(key);
9948
9954
  if (!existing || candidate.measuredAt > existing.measuredAt) candidates.set(key, candidate);
9949
9955
  };
9950
- const [ledger, ...legacy] = await Promise.allSettled([
9951
- collectLedgerEdges(input, merge),
9952
- collectStreamEdges(input, merge),
9953
- collectReportEdges(input, merge)
9954
- ]);
9956
+ const [ledger] = await Promise.allSettled([collectLedgerEdges(input, merge)]);
9957
+ const ledgerAnswered = ledger.status === "fulfilled" && ledger.value;
9958
+ const legacy = ledgerAnswered ? [] : await Promise.allSettled([collectStreamEdges(input, merge), collectReportEdges(input, merge)]);
9955
9959
  let skipped = 0;
9956
9960
  let legacyBroken = false;
9957
9961
  for (const result of legacy) if (result.status === "rejected") {
@@ -9963,7 +9967,6 @@ async function loadCoverageEdges(input) {
9963
9967
  warn(`select-specs: ${skipped} measured run(s) on the hub could not be read; treating the measurements as unreadable rather than absent`);
9964
9968
  }
9965
9969
  if (ledger.status === "rejected") warn(`select-specs: could not read the hub's coverage-edge ledger (${errMessage(ledger.reason)})`);
9966
- const ledgerAnswered = ledger.status === "fulfilled" && ledger.value;
9967
9970
  const degraded = ledger.status === "rejected" || !ledgerAnswered && legacyBroken;
9968
9971
  return {
9969
9972
  edges: new Map([...candidates].map(([key, c]) => [key, {
@@ -22806,6 +22809,9 @@ const HTML_BODY = `
22806
22809
  <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>
22807
22810
  <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>
22808
22811
  </div>
22812
+ <!-- Not a chip: the chips pick which cases to show out of what the
22813
+ project verifies, and this changes what that set is. -->
22814
+ <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>
22809
22815
  <div class="spacer"></div>
22810
22816
  <span class="muted persp-head" id="persp-deploy-head" hidden></span>
22811
22817
  <div class="sw-wrap" id="persp-profile-wrap">
@@ -23278,6 +23284,8 @@ const CSS = `
23278
23284
  amber as the drift badges (dr-found), not fail-red. */
23279
23285
  .spec-card.drift-found { border-left: 3px solid var(--amber); }
23280
23286
  .spec-card.passed { border-left: 3px solid var(--pass); }
23287
+ /* Says which card the link landed on; the left rail still carries the verdict. */
23288
+ .spec-card.linked { box-shadow: 0 0 0 2px var(--accent); }
23281
23289
  .spec-card-head { display: flex; align-items: center; gap: 12px; padding: 16px 20px; }
23282
23290
  .spec-card-head .name { font-weight: 600; font-size: 15px; }
23283
23291
  .spec-card-head .slug { font-family: var(--mono); font-size: 12px; color: var(--muted); margin-top: 2px; }
@@ -23621,6 +23629,10 @@ const CSS = `
23621
23629
  /* The Perspectives toolbar carries search + two filter-chip groups + the
23622
23630
  profile selector, so it wraps instead of overflowing on a narrow window. */
23623
23631
  #view-perspectives .toolbar { flex-wrap: wrap; }
23632
+ .ftoggle { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); cursor: pointer; }
23633
+ .ftoggle input { margin: 0; cursor: pointer; }
23634
+ .chip.off { background: var(--surface-2); color: var(--muted-2); border-color: var(--border); }
23635
+ .ftoggle .fcount { margin-left: 2px; font-variant-numeric: tabular-nums; color: var(--muted-2); }
23624
23636
  .proj-menu.right { left: auto; right: 0; }
23625
23637
  .d-note { margin-top: 12px; max-width: 900px; }
23626
23638
 
@@ -23746,7 +23758,7 @@ const CLIENT_JS = `
23746
23758
  // Every status a run can be in, from the contract that defines them, so the
23747
23759
  // runs filter offers exactly what a row's badge can say.
23748
23760
  var RUN_STATUSES = ${JSON.stringify(RunStatusSchema.options)};
23749
- var state = { token: "", project: "", profile: "default", detailRunId: "", jobPollToken: 0, runsLoadToken: 0, spendLoadToken: 0 };
23761
+ var state = { token: "", project: "", profile: "default", detailRunId: "", detailCaseKey: null, detailScrolled: false, jobPollToken: 0, runsLoadToken: 0, spendLoadToken: 0 };
23750
23762
  var knownProfiles = [];
23751
23763
  var TOKEN_KEY = "ccqa-hub-token";
23752
23764
  var LANG_KEY = "ccqa-hub-lang";
@@ -23859,6 +23871,9 @@ const CLIENT_JS = `
23859
23871
  "perspectives.filter.all": "All", "perspectives.filter.deterministic": "Deterministic",
23860
23872
  "perspectives.filter.live": "Live",
23861
23873
  "perspectives.filter.group.mode": "Mode",
23874
+ "perspectives.filter.showDisabled": "Show disabled",
23875
+ "detail.caseNotInRun": "{case} is not in this run.",
23876
+ "perspectives.filter.disabled": "disabled",
23862
23877
  "perspectives.col.verdict": "Verdict", "perspectives.col.audit": "Audit",
23863
23878
  "perspectives.col.run": "Execution",
23864
23879
  "perspectives.audit.state.due": "Audit due",
@@ -24083,6 +24098,9 @@ const CLIENT_JS = `
24083
24098
  "perspectives.filter.all": "すべて", "perspectives.filter.deterministic": "決定的",
24084
24099
  "perspectives.filter.live": "ライブ",
24085
24100
  "perspectives.filter.group.mode": "モード",
24101
+ "perspectives.filter.showDisabled": "無効も表示",
24102
+ "detail.caseNotInRun": "{case} はこの実行に含まれていません。",
24103
+ "perspectives.filter.disabled": "無効",
24086
24104
  "perspectives.col.verdict": "判定", "perspectives.col.audit": "監査",
24087
24105
  "perspectives.col.run": "実行",
24088
24106
  "perspectives.audit.state.due": "監査待ち",
@@ -24691,6 +24709,32 @@ const CLIENT_JS = `
24691
24709
  document.querySelector(".nav-jobs").classList.toggle("disabled", gated);
24692
24710
  }
24693
24711
 
24712
+ // --- pure: run hash ------------------------------------------------------
24713
+ // Lifted out and run by perspectives-visibility.test.ts: the pattern lives
24714
+ // inside a template literal, where a lost backslash fails silently.
24715
+
24716
+ /** A run, optionally opened on one case. */
24717
+ function runCaseHref(runId, caseKey) {
24718
+ var base = "#/runs/" + encodeURIComponent(runId);
24719
+ return caseKey ? base + "?case=" + encodeURIComponent(caseKey) : base;
24720
+ }
24721
+
24722
+ /** The run id and case a #/runs/... hash names, or null when it names no run. */
24723
+ function parseRunHash(hash) {
24724
+ var m = String(hash).match(/^#\\/runs\\/([^?]+)(?:\\?case=(.*))?$/);
24725
+ if (!m) return null;
24726
+ // A truncated paste leaves a half-written %XX, which decodeURIComponent
24727
+ // throws on. Thrown out of route() that reads as "you are disconnected"
24728
+ // and shows the login gate, for a link the reader can see is a run.
24729
+ try {
24730
+ return { runId: decodeURIComponent(m[1]), caseKey: m[2] ? decodeURIComponent(m[2]) : null };
24731
+ } catch (e) {
24732
+ return { runId: m[1], caseKey: m[2] || null };
24733
+ }
24734
+ }
24735
+
24736
+ // --- end pure: run hash --------------------------------------------------
24737
+
24694
24738
  function route() {
24695
24739
  // Disconnected: the full-screen login gate is the only thing to show.
24696
24740
  if (!state.token) { showAuthGate(false); return; }
@@ -24699,8 +24743,8 @@ const CLIENT_JS = `
24699
24743
  // land there (e.g. right after login) instead of an empty Runs list, and
24700
24744
  // gate any deep-linked #/runs or #/secrets to it too.
24701
24745
  if (location.hash === "#/projects" || !state.project) { openProjects(); return; }
24702
- var m = location.hash.match(/^#\\/runs\\/(.+)$/);
24703
- if (m) { openRunDetail(decodeURIComponent(m[1])); return; }
24746
+ var m = parseRunHash(location.hash);
24747
+ if (m) { openRunDetail(m.runId, m.caseKey); return; }
24704
24748
  if (location.hash === "#/perspectives") { openPerspectives(); return; }
24705
24749
  if (location.hash === "#/coverage") { openCoverage(); return; }
24706
24750
  if (location.hash === "#/secrets") { openSecrets(); return; }
@@ -26038,8 +26082,34 @@ const CLIENT_JS = `
26038
26082
  function renderSpecCards(runId, results, triageState, isDrift) {
26039
26083
  var container = document.getElementById("spec-cards");
26040
26084
  clear(container);
26041
- results.forEach(function (r) { container.appendChild(renderSpecCard(runId, r, triageState, isDrift)); });
26085
+ // A slower earlier fetch can land after the reader has moved to another
26086
+ // run. Its cards were always wrong; what is new is that it would also
26087
+ // claim their case is missing from a run that has it.
26088
+ var caseKey = runId === state.detailRunId ? state.detailCaseKey : null;
26089
+ var wanted = null;
26090
+ results.forEach(function (r) {
26091
+ var card = renderSpecCard(runId, r, triageState, isDrift);
26092
+ if (caseKey && r.feature + "/" + r.spec === caseKey) wanted = card;
26093
+ container.appendChild(card);
26094
+ });
26042
26095
  document.getElementById("detail-spec-count").textContent = "· " + results.length;
26096
+ if (!caseKey) return;
26097
+ // Marked as well as scrolled: a page that jumped has to say where it
26098
+ // jumped to, or the reader re-finds the row by hand. Cards are rebuilt
26099
+ // again when triage lands, so the mark is reapplied but the scroll is
26100
+ // not — the reader has been reading since the first one.
26101
+ if (wanted) {
26102
+ wanted.classList.add("linked");
26103
+ if (!state.detailScrolled) {
26104
+ state.detailScrolled = true;
26105
+ wanted.scrollIntoView({ block: "center" });
26106
+ }
26107
+ } else if (!state.detailScrolled) {
26108
+ // The case is not in this run (renamed, or the run predates it). Silent
26109
+ // would read as "it is here somewhere".
26110
+ state.detailScrolled = true;
26111
+ detailError(t("detail.caseNotInRun").replace("{case}", caseKey));
26112
+ }
26043
26113
  }
26044
26114
 
26045
26115
  // ── run detail: triage (confusion matrix) ───────────────────────────
@@ -26237,9 +26307,11 @@ const CLIENT_JS = `
26237
26307
  e.textContent = msg;
26238
26308
  }
26239
26309
 
26240
- function openRunDetail(runId) {
26310
+ function openRunDetail(runId, caseKey) {
26241
26311
  showView("detail");
26242
26312
  state.detailRunId = runId;
26313
+ state.detailCaseKey = caseKey || null;
26314
+ state.detailScrolled = false;
26243
26315
  document.getElementById("detail-title").textContent = runId.slice(0, 8);
26244
26316
  document.getElementById("detail-error").hidden = true;
26245
26317
  document.getElementById("learn-cta").hidden = true;
@@ -26697,7 +26769,8 @@ const CLIENT_JS = `
26697
26769
  var perspState = {
26698
26770
  doc: null, q: "", f: "all",
26699
26771
  rerun: null, rerunSupported: null, runUrls: {}, rerunProfiles: [],
26700
- drift: null
26772
+ drift: null,
26773
+ showDisabled: false
26701
26774
  };
26702
26775
 
26703
26776
  function perspectivesPath() {
@@ -26807,6 +26880,20 @@ const CLIENT_JS = `
26807
26880
  return feature.featureName + "/" + spec.specName;
26808
26881
  }
26809
26882
 
26883
+ // --- pure: perspectives visibility ---------------------------------------
26884
+ // Lifted out and run by perspectives-visibility.test.ts.
26885
+
26886
+ /**
26887
+ * A disabled case is not part of what this project verifies, so it is not in
26888
+ * the answer the page opens on. What the summary counts and what the table
26889
+ * lists both ask here, or the two disagree on how many cases exist.
26890
+ */
26891
+ function perspHidden(spec, showDisabled) {
26892
+ return spec.disabled === true && !showDisabled;
26893
+ }
26894
+
26895
+ // --- end pure: perspectives visibility -----------------------------------
26896
+
26810
26897
  // Shared by rerun and drift, which differ only in which report they read.
26811
26898
  function ledgerEntryFor(report, feature, spec) {
26812
26899
  return report && report.specs ? (report.specs[perspSpecKey(feature, spec)] || null) : null;
@@ -27022,10 +27109,10 @@ const CLIENT_JS = `
27022
27109
  // One ledger entry as "<short sha> · <when>", linking to the hub's run detail
27023
27110
  // and, when that run recorded one, to the CI run. Clicks must not bubble: the
27024
27111
  // table row itself toggles the detail panel.
27025
- function ledgerLine(entry) {
27112
+ function ledgerLine(entry, caseKey) {
27026
27113
  var wrap = el("span");
27027
27114
  var link = el("a", null, shortSha(entry.gitHead) || String(entry.runId).slice(0, 8));
27028
- link.href = "#/runs/" + encodeURIComponent(entry.runId);
27115
+ link.href = runCaseHref(entry.runId, caseKey);
27029
27116
  link.title = t("perspectives.result.openRun");
27030
27117
  link.addEventListener("click", function (e) { e.stopPropagation(); });
27031
27118
  wrap.appendChild(link);
@@ -27153,7 +27240,7 @@ const CLIENT_JS = `
27153
27240
  due: "rr-none", clean: "passed", drifted: "failed", undecided: "rr-unknown"
27154
27241
  };
27155
27242
 
27156
- function perspAuditCell(rr, driftEntry) {
27243
+ function perspAuditCell(rr, driftEntry, caseKey) {
27157
27244
  var td = el("td");
27158
27245
  if (!rr || !rr.audit) {
27159
27246
  td.appendChild(el("span", "muted", "\u2014"));
@@ -27176,7 +27263,7 @@ const CLIENT_JS = `
27176
27263
  // audit's own coordinate, so it comes from the drift ledger.
27177
27264
  if (driftEntry) {
27178
27265
  var sub = el("span", "cellsub");
27179
- sub.appendChild(ledgerLine(driftEntry));
27266
+ sub.appendChild(ledgerLine(driftEntry, caseKey));
27180
27267
  if (driftEntry.graded) {
27181
27268
  sub.appendChild(document.createTextNode(" · "));
27182
27269
  sub.appendChild(el("span", "graded-mark", t("perspectives.drift.graded")));
@@ -27186,7 +27273,7 @@ const CLIENT_JS = `
27186
27273
  return td;
27187
27274
  }
27188
27275
 
27189
- function perspRunCell(rr) {
27276
+ function perspRunCell(rr, caseKey) {
27190
27277
  var td = el("td");
27191
27278
  var runState = perspRunState(rr);
27192
27279
  // No verdict at all: this case is in the document but not in the report
@@ -27220,7 +27307,7 @@ const CLIENT_JS = `
27220
27307
  var last = lastResult(rr);
27221
27308
  if (last) {
27222
27309
  var sub = el("span", "cellsub");
27223
- sub.appendChild(ledgerLine(last.entry));
27310
+ sub.appendChild(ledgerLine(last.entry, caseKey));
27224
27311
  td.appendChild(sub);
27225
27312
  }
27226
27313
  }
@@ -27292,16 +27379,29 @@ const CLIENT_JS = `
27292
27379
  var host = document.getElementById("persp-ov");
27293
27380
  clear(host);
27294
27381
  var records = [];
27382
+ var features = 0;
27383
+ var listed = 0;
27295
27384
  doc.features.forEach(function (feature) {
27385
+ var before = listed;
27296
27386
  feature.specs.forEach(function (spec) {
27297
- records.push(ledgerEntryFor(perspState.rerun, feature, spec));
27387
+ if (perspHidden(spec, perspState.showDisabled)) return;
27388
+ listed += 1;
27389
+ // A disabled case has no ledger entry — the hub does not ask for one
27390
+ // — and an absent entry reads as "needs re-run". Counting it would
27391
+ // put work in the bar that nobody intends to do.
27392
+ if (spec.disabled !== true) records.push(ledgerEntryFor(perspState.rerun, feature, spec));
27298
27393
  });
27394
+ // A feature whose cases are all hidden renders no group below, so
27395
+ // counting it here would put the summary above a table that disagrees.
27396
+ if (listed > before) features += 1;
27299
27397
  });
27300
27398
 
27301
27399
  var inv = el("div", "ov-inv");
27302
- inv.appendChild(el("b", null, String(records.length)));
27400
+ // The inventory counts what the table lists; the bars below judge only the
27401
+ // cases the hub keeps a ledger for, so the two numbers can differ.
27402
+ inv.appendChild(el("b", null, String(listed)));
27303
27403
  inv.appendChild(document.createTextNode(" " + t("perspectives.ov.cases") + " / "));
27304
- inv.appendChild(el("b", null, String(doc.features.length)));
27404
+ inv.appendChild(el("b", null, String(features)));
27305
27405
  inv.appendChild(document.createTextNode(" " + t("perspectives.ov.features")));
27306
27406
  host.appendChild(inv);
27307
27407
  if (!records.length) return;
@@ -27328,6 +27428,12 @@ const CLIENT_JS = `
27328
27428
  // search text always applies: a chip's count has to be the number of rows
27329
27429
  // clicking it actually leaves.
27330
27430
  function perspMatches(feature, spec, f) {
27431
+ if (perspHidden(spec, perspState.showDisabled)) return false;
27432
+ return perspMatchesFilters(feature, spec, f);
27433
+ }
27434
+
27435
+ /** Everything the toolbar filters on except whether the case is disabled. */
27436
+ function perspMatchesFilters(feature, spec, f) {
27331
27437
  if (f === "deterministic" && perspMode(spec) !== "deterministic") return false;
27332
27438
  if (f === "live" && perspMode(spec) !== "live") return false;
27333
27439
  // The verdict chips (same words as RERUN_ORDER/the 判定 column) share
@@ -27347,12 +27453,12 @@ const CLIENT_JS = `
27347
27453
  return true;
27348
27454
  }
27349
27455
 
27350
- function perspFilterCount(f) {
27456
+ function perspCount(pred) {
27351
27457
  var doc = perspState.doc;
27352
27458
  if (!doc) return 0;
27353
27459
  var n = 0;
27354
27460
  doc.features.forEach(function (feature) {
27355
- feature.specs.forEach(function (spec) { if (perspMatches(feature, spec, f)) n += 1; });
27461
+ feature.specs.forEach(function (spec) { if (pred(feature, spec)) n += 1; });
27356
27462
  });
27357
27463
  return n;
27358
27464
  }
@@ -27844,9 +27950,13 @@ const CLIENT_JS = `
27844
27950
  row.tabIndex = 0;
27845
27951
  row.setAttribute("aria-expanded", "false");
27846
27952
 
27953
+ var caseKey = perspSpecKey(feature, spec);
27847
27954
  var titleTd = el("td", "c-title");
27848
27955
  titleTd.appendChild(document.createTextNode(spec.title));
27849
- titleTd.appendChild(el("span", "c-id", perspSpecKey(feature, spec)));
27956
+ // Only reachable with the toggle on, where saying which rows are the
27957
+ // disabled ones is the whole point of having turned it on.
27958
+ if (spec.disabled === true) titleTd.appendChild(el("span", "chip off", t("perspectives.filter.disabled")));
27959
+ titleTd.appendChild(el("span", "c-id", caseKey));
27850
27960
  if (spec.summary) titleTd.appendChild(el("span", "csum", spec.summary));
27851
27961
  row.appendChild(titleTd);
27852
27962
 
@@ -27857,8 +27967,8 @@ const CLIENT_JS = `
27857
27967
  if (showRerun) {
27858
27968
  var rr = ledgerEntryFor(perspState.rerun, feature, spec);
27859
27969
  row.appendChild(perspVerdictCell(rr));
27860
- row.appendChild(perspRunCell(rr));
27861
- row.appendChild(perspAuditCell(rr, ledgerEntryFor(perspState.drift, feature, spec)));
27970
+ row.appendChild(perspRunCell(rr, caseKey));
27971
+ row.appendChild(perspAuditCell(rr, ledgerEntryFor(perspState.drift, feature, spec), caseKey));
27862
27972
  }
27863
27973
 
27864
27974
  var chevTd = el("td", "c-chev");
@@ -27910,13 +28020,23 @@ const CLIENT_JS = `
27910
28020
  // Each chip also carries what it would yield — the mode breakdown the
27911
28021
  // summary row used to spend four tiles on.
27912
28022
  function syncPerspChips() {
28023
+ // What ticking the box would actually add: disabled cases that the filters
28024
+ // in effect would then let through. A flat count of every disabled case
28025
+ // promises rows the active chip or search would drop.
28026
+ var off = perspState.showDisabled ? 0 : perspCount(function (feature, spec) {
28027
+ return spec.disabled === true && perspMatchesFilters(feature, spec, perspState.f);
28028
+ });
28029
+ document.getElementById("persp-disabled-count").textContent = off ? "+" + off : "";
28030
+ document.getElementById("persp-show-disabled").checked = perspState.showDisabled;
27913
28031
  var group = document.getElementById("persp-verdict-chips");
27914
28032
  group.hidden = perspState.rerunSupported !== true;
27915
28033
  if (perspState.rerunSupported !== true && RERUN_ORDER.indexOf(perspState.f) !== -1) perspState.f = "all";
27916
28034
  document.querySelectorAll("#view-perspectives .fchip").forEach(function (b) {
27917
28035
  var f = b.getAttribute("data-f");
27918
28036
  b.setAttribute("aria-pressed", String(f === perspState.f));
27919
- b.querySelector(".fcount").textContent = perspState.doc ? String(perspFilterCount(f)) : "";
28037
+ b.querySelector(".fcount").textContent = perspState.doc
28038
+ ? String(perspCount(function (feature, spec) { return perspMatches(feature, spec, f); }))
28039
+ : "";
27920
28040
  });
27921
28041
  }
27922
28042
 
@@ -28622,6 +28742,10 @@ const CLIENT_JS = `
28622
28742
  perspState.q = e.target.value.trim().toLowerCase();
28623
28743
  renderPerspectives();
28624
28744
  });
28745
+ document.getElementById("persp-show-disabled").addEventListener("change", function (e) {
28746
+ perspState.showDisabled = e.target.checked;
28747
+ renderPerspectives();
28748
+ });
28625
28749
 
28626
28750
  // Debounced, unlike persp-q: each keystroke rebuilds the whole tree, and
28627
28751
  // the universe can hold thousands of files.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.47.3",
3
+ "version": "1.48.1",
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.3",
3
+ "version": "1.48.1",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {