ccqa 1.47.3 → 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
@@ -22806,6 +22806,9 @@ const HTML_BODY = `
22806
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>
22807
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>
22808
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>
22809
22812
  <div class="spacer"></div>
22810
22813
  <span class="muted persp-head" id="persp-deploy-head" hidden></span>
22811
22814
  <div class="sw-wrap" id="persp-profile-wrap">
@@ -23278,6 +23281,8 @@ const CSS = `
23278
23281
  amber as the drift badges (dr-found), not fail-red. */
23279
23282
  .spec-card.drift-found { border-left: 3px solid var(--amber); }
23280
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); }
23281
23286
  .spec-card-head { display: flex; align-items: center; gap: 12px; padding: 16px 20px; }
23282
23287
  .spec-card-head .name { font-weight: 600; font-size: 15px; }
23283
23288
  .spec-card-head .slug { font-family: var(--mono); font-size: 12px; color: var(--muted); margin-top: 2px; }
@@ -23621,6 +23626,10 @@ const CSS = `
23621
23626
  /* The Perspectives toolbar carries search + two filter-chip groups + the
23622
23627
  profile selector, so it wraps instead of overflowing on a narrow window. */
23623
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); }
23624
23633
  .proj-menu.right { left: auto; right: 0; }
23625
23634
  .d-note { margin-top: 12px; max-width: 900px; }
23626
23635
 
@@ -23746,7 +23755,7 @@ const CLIENT_JS = `
23746
23755
  // Every status a run can be in, from the contract that defines them, so the
23747
23756
  // runs filter offers exactly what a row's badge can say.
23748
23757
  var RUN_STATUSES = ${JSON.stringify(RunStatusSchema.options)};
23749
- 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 };
23750
23759
  var knownProfiles = [];
23751
23760
  var TOKEN_KEY = "ccqa-hub-token";
23752
23761
  var LANG_KEY = "ccqa-hub-lang";
@@ -23859,6 +23868,9 @@ const CLIENT_JS = `
23859
23868
  "perspectives.filter.all": "All", "perspectives.filter.deterministic": "Deterministic",
23860
23869
  "perspectives.filter.live": "Live",
23861
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",
23862
23874
  "perspectives.col.verdict": "Verdict", "perspectives.col.audit": "Audit",
23863
23875
  "perspectives.col.run": "Execution",
23864
23876
  "perspectives.audit.state.due": "Audit due",
@@ -24083,6 +24095,9 @@ const CLIENT_JS = `
24083
24095
  "perspectives.filter.all": "すべて", "perspectives.filter.deterministic": "決定的",
24084
24096
  "perspectives.filter.live": "ライブ",
24085
24097
  "perspectives.filter.group.mode": "モード",
24098
+ "perspectives.filter.showDisabled": "無効も表示",
24099
+ "detail.caseNotInRun": "{case} はこの実行に含まれていません。",
24100
+ "perspectives.filter.disabled": "無効",
24086
24101
  "perspectives.col.verdict": "判定", "perspectives.col.audit": "監査",
24087
24102
  "perspectives.col.run": "実行",
24088
24103
  "perspectives.audit.state.due": "監査待ち",
@@ -24691,6 +24706,32 @@ const CLIENT_JS = `
24691
24706
  document.querySelector(".nav-jobs").classList.toggle("disabled", gated);
24692
24707
  }
24693
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
+
24694
24735
  function route() {
24695
24736
  // Disconnected: the full-screen login gate is the only thing to show.
24696
24737
  if (!state.token) { showAuthGate(false); return; }
@@ -24699,8 +24740,8 @@ const CLIENT_JS = `
24699
24740
  // land there (e.g. right after login) instead of an empty Runs list, and
24700
24741
  // gate any deep-linked #/runs or #/secrets to it too.
24701
24742
  if (location.hash === "#/projects" || !state.project) { openProjects(); return; }
24702
- var m = location.hash.match(/^#\\/runs\\/(.+)$/);
24703
- if (m) { openRunDetail(decodeURIComponent(m[1])); return; }
24743
+ var m = parseRunHash(location.hash);
24744
+ if (m) { openRunDetail(m.runId, m.caseKey); return; }
24704
24745
  if (location.hash === "#/perspectives") { openPerspectives(); return; }
24705
24746
  if (location.hash === "#/coverage") { openCoverage(); return; }
24706
24747
  if (location.hash === "#/secrets") { openSecrets(); return; }
@@ -26038,8 +26079,34 @@ const CLIENT_JS = `
26038
26079
  function renderSpecCards(runId, results, triageState, isDrift) {
26039
26080
  var container = document.getElementById("spec-cards");
26040
26081
  clear(container);
26041
- 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
+ });
26042
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
+ }
26043
26110
  }
26044
26111
 
26045
26112
  // ── run detail: triage (confusion matrix) ───────────────────────────
@@ -26237,9 +26304,11 @@ const CLIENT_JS = `
26237
26304
  e.textContent = msg;
26238
26305
  }
26239
26306
 
26240
- function openRunDetail(runId) {
26307
+ function openRunDetail(runId, caseKey) {
26241
26308
  showView("detail");
26242
26309
  state.detailRunId = runId;
26310
+ state.detailCaseKey = caseKey || null;
26311
+ state.detailScrolled = false;
26243
26312
  document.getElementById("detail-title").textContent = runId.slice(0, 8);
26244
26313
  document.getElementById("detail-error").hidden = true;
26245
26314
  document.getElementById("learn-cta").hidden = true;
@@ -26697,7 +26766,8 @@ const CLIENT_JS = `
26697
26766
  var perspState = {
26698
26767
  doc: null, q: "", f: "all",
26699
26768
  rerun: null, rerunSupported: null, runUrls: {}, rerunProfiles: [],
26700
- drift: null
26769
+ drift: null,
26770
+ showDisabled: false
26701
26771
  };
26702
26772
 
26703
26773
  function perspectivesPath() {
@@ -26807,6 +26877,20 @@ const CLIENT_JS = `
26807
26877
  return feature.featureName + "/" + spec.specName;
26808
26878
  }
26809
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
+
26810
26894
  // Shared by rerun and drift, which differ only in which report they read.
26811
26895
  function ledgerEntryFor(report, feature, spec) {
26812
26896
  return report && report.specs ? (report.specs[perspSpecKey(feature, spec)] || null) : null;
@@ -27022,10 +27106,10 @@ const CLIENT_JS = `
27022
27106
  // One ledger entry as "<short sha> · <when>", linking to the hub's run detail
27023
27107
  // and, when that run recorded one, to the CI run. Clicks must not bubble: the
27024
27108
  // table row itself toggles the detail panel.
27025
- function ledgerLine(entry) {
27109
+ function ledgerLine(entry, caseKey) {
27026
27110
  var wrap = el("span");
27027
27111
  var link = el("a", null, shortSha(entry.gitHead) || String(entry.runId).slice(0, 8));
27028
- link.href = "#/runs/" + encodeURIComponent(entry.runId);
27112
+ link.href = runCaseHref(entry.runId, caseKey);
27029
27113
  link.title = t("perspectives.result.openRun");
27030
27114
  link.addEventListener("click", function (e) { e.stopPropagation(); });
27031
27115
  wrap.appendChild(link);
@@ -27153,7 +27237,7 @@ const CLIENT_JS = `
27153
27237
  due: "rr-none", clean: "passed", drifted: "failed", undecided: "rr-unknown"
27154
27238
  };
27155
27239
 
27156
- function perspAuditCell(rr, driftEntry) {
27240
+ function perspAuditCell(rr, driftEntry, caseKey) {
27157
27241
  var td = el("td");
27158
27242
  if (!rr || !rr.audit) {
27159
27243
  td.appendChild(el("span", "muted", "\u2014"));
@@ -27176,7 +27260,7 @@ const CLIENT_JS = `
27176
27260
  // audit's own coordinate, so it comes from the drift ledger.
27177
27261
  if (driftEntry) {
27178
27262
  var sub = el("span", "cellsub");
27179
- sub.appendChild(ledgerLine(driftEntry));
27263
+ sub.appendChild(ledgerLine(driftEntry, caseKey));
27180
27264
  if (driftEntry.graded) {
27181
27265
  sub.appendChild(document.createTextNode(" · "));
27182
27266
  sub.appendChild(el("span", "graded-mark", t("perspectives.drift.graded")));
@@ -27186,7 +27270,7 @@ const CLIENT_JS = `
27186
27270
  return td;
27187
27271
  }
27188
27272
 
27189
- function perspRunCell(rr) {
27273
+ function perspRunCell(rr, caseKey) {
27190
27274
  var td = el("td");
27191
27275
  var runState = perspRunState(rr);
27192
27276
  // No verdict at all: this case is in the document but not in the report
@@ -27220,7 +27304,7 @@ const CLIENT_JS = `
27220
27304
  var last = lastResult(rr);
27221
27305
  if (last) {
27222
27306
  var sub = el("span", "cellsub");
27223
- sub.appendChild(ledgerLine(last.entry));
27307
+ sub.appendChild(ledgerLine(last.entry, caseKey));
27224
27308
  td.appendChild(sub);
27225
27309
  }
27226
27310
  }
@@ -27292,16 +27376,29 @@ const CLIENT_JS = `
27292
27376
  var host = document.getElementById("persp-ov");
27293
27377
  clear(host);
27294
27378
  var records = [];
27379
+ var features = 0;
27380
+ var listed = 0;
27295
27381
  doc.features.forEach(function (feature) {
27382
+ var before = listed;
27296
27383
  feature.specs.forEach(function (spec) {
27297
- 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));
27298
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;
27299
27394
  });
27300
27395
 
27301
27396
  var inv = el("div", "ov-inv");
27302
- 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)));
27303
27400
  inv.appendChild(document.createTextNode(" " + t("perspectives.ov.cases") + " / "));
27304
- inv.appendChild(el("b", null, String(doc.features.length)));
27401
+ inv.appendChild(el("b", null, String(features)));
27305
27402
  inv.appendChild(document.createTextNode(" " + t("perspectives.ov.features")));
27306
27403
  host.appendChild(inv);
27307
27404
  if (!records.length) return;
@@ -27328,6 +27425,12 @@ const CLIENT_JS = `
27328
27425
  // search text always applies: a chip's count has to be the number of rows
27329
27426
  // clicking it actually leaves.
27330
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) {
27331
27434
  if (f === "deterministic" && perspMode(spec) !== "deterministic") return false;
27332
27435
  if (f === "live" && perspMode(spec) !== "live") return false;
27333
27436
  // The verdict chips (same words as RERUN_ORDER/the 判定 column) share
@@ -27347,12 +27450,12 @@ const CLIENT_JS = `
27347
27450
  return true;
27348
27451
  }
27349
27452
 
27350
- function perspFilterCount(f) {
27453
+ function perspCount(pred) {
27351
27454
  var doc = perspState.doc;
27352
27455
  if (!doc) return 0;
27353
27456
  var n = 0;
27354
27457
  doc.features.forEach(function (feature) {
27355
- feature.specs.forEach(function (spec) { if (perspMatches(feature, spec, f)) n += 1; });
27458
+ feature.specs.forEach(function (spec) { if (pred(feature, spec)) n += 1; });
27356
27459
  });
27357
27460
  return n;
27358
27461
  }
@@ -27844,9 +27947,13 @@ const CLIENT_JS = `
27844
27947
  row.tabIndex = 0;
27845
27948
  row.setAttribute("aria-expanded", "false");
27846
27949
 
27950
+ var caseKey = perspSpecKey(feature, spec);
27847
27951
  var titleTd = el("td", "c-title");
27848
27952
  titleTd.appendChild(document.createTextNode(spec.title));
27849
- 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));
27850
27957
  if (spec.summary) titleTd.appendChild(el("span", "csum", spec.summary));
27851
27958
  row.appendChild(titleTd);
27852
27959
 
@@ -27857,8 +27964,8 @@ const CLIENT_JS = `
27857
27964
  if (showRerun) {
27858
27965
  var rr = ledgerEntryFor(perspState.rerun, feature, spec);
27859
27966
  row.appendChild(perspVerdictCell(rr));
27860
- row.appendChild(perspRunCell(rr));
27861
- 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));
27862
27969
  }
27863
27970
 
27864
27971
  var chevTd = el("td", "c-chev");
@@ -27910,13 +28017,23 @@ const CLIENT_JS = `
27910
28017
  // Each chip also carries what it would yield — the mode breakdown the
27911
28018
  // summary row used to spend four tiles on.
27912
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;
27913
28028
  var group = document.getElementById("persp-verdict-chips");
27914
28029
  group.hidden = perspState.rerunSupported !== true;
27915
28030
  if (perspState.rerunSupported !== true && RERUN_ORDER.indexOf(perspState.f) !== -1) perspState.f = "all";
27916
28031
  document.querySelectorAll("#view-perspectives .fchip").forEach(function (b) {
27917
28032
  var f = b.getAttribute("data-f");
27918
28033
  b.setAttribute("aria-pressed", String(f === perspState.f));
27919
- 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
+ : "";
27920
28037
  });
27921
28038
  }
27922
28039
 
@@ -28622,6 +28739,10 @@ const CLIENT_JS = `
28622
28739
  perspState.q = e.target.value.trim().toLowerCase();
28623
28740
  renderPerspectives();
28624
28741
  });
28742
+ document.getElementById("persp-show-disabled").addEventListener("change", function (e) {
28743
+ perspState.showDisabled = e.target.checked;
28744
+ renderPerspectives();
28745
+ });
28625
28746
 
28626
28747
  // Debounced, unlike persp-q: each keystroke rebuilds the whole tree, and
28627
28748
  // 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.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.3",
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": {