@chrisdudek/yg 5.4.1 → 5.5.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.
@@ -491,6 +491,14 @@ declare class SuppressMarkerError extends Error {
491
491
  file: string;
492
492
  line: number;
493
493
  readonly code = "SUPPRESS_MARKER_MISSING_REASON";
494
+ /**
495
+ * Self-describing what/why/next for the malformed marker. A reasonless marker
496
+ * is a fault in the SOURCE file's marker — NOT in the aspect's check.mjs — so
497
+ * the deterministic runners re-surface this as its own diagnostic instead of an
498
+ * `aspect-check-runtime-error`. (The LLM-sizing paths build their own message
499
+ * from `file`/`line`; only the deterministic consumers read `messageData`.)
500
+ */
501
+ readonly messageData: IssueMessage;
494
502
  constructor(message: string, file: string, line: number);
495
503
  }
496
504
 
package/dist/structure.js CHANGED
@@ -164,6 +164,26 @@ var UndeclaredGraphReadError = class extends Error {
164
164
  }
165
165
  nodePath;
166
166
  };
167
+ function makeGraphFile(repoRelPosixPath, content, bytes, recorder, subjectFiles) {
168
+ if (!recorder || subjectFiles?.has(repoRelPosixPath)) {
169
+ return { path: repoRelPosixPath, content };
170
+ }
171
+ const rec = recorder;
172
+ let recorded = false;
173
+ const file = { path: repoRelPosixPath };
174
+ Object.defineProperty(file, "content", {
175
+ enumerable: true,
176
+ configurable: true,
177
+ get() {
178
+ if (!recorded) {
179
+ rec.recordRead(repoRelPosixPath, bytes);
180
+ recorded = true;
181
+ }
182
+ return content;
183
+ }
184
+ });
185
+ return file;
186
+ }
167
187
  function computeAllowedNodePaths(currentPath, graph) {
168
188
  const allowed = /* @__PURE__ */ new Set([currentPath]);
169
189
  const current = graph.nodes.get(currentPath);
@@ -225,11 +245,8 @@ function createCtxGraph(params) {
225
245
  if (stat2.isFile()) {
226
246
  const bytes = fs2.readFileSync(abs);
227
247
  const content = bytes.toString("utf8");
228
- files.push({ path: p, content });
229
248
  touchedFiles.push(p);
230
- if (recorder && !subjectFiles?.has(p)) {
231
- recorder.recordRead(p, bytes);
232
- }
249
+ files.push(makeGraphFile(p, content, bytes, recorder, subjectFiles));
233
250
  }
234
251
  } catch {
235
252
  }
@@ -734,45 +751,96 @@ var SuppressMarkerError = class extends Error {
734
751
  this.file = file;
735
752
  this.line = line;
736
753
  this.name = "SuppressMarkerError";
754
+ const loc = `${toPosixPath(file)}:${line}`;
755
+ this.messageData = {
756
+ what: `Malformed yg-suppress marker at ${loc} \u2014 it is missing the required reason after the aspect id.`,
757
+ why: `A single-line or disable marker must carry a reason after its closing parenthesis; a reasonless marker cannot be honored. This is a fault in the source file's marker, not in the aspect's check.`,
758
+ next: `Add a reason to the marker at ${loc}, or remove it.`
759
+ };
737
760
  }
738
761
  file;
739
762
  line;
740
763
  code = "SUPPRESS_MARKER_MISSING_REASON";
764
+ /**
765
+ * Self-describing what/why/next for the malformed marker. A reasonless marker
766
+ * is a fault in the SOURCE file's marker — NOT in the aspect's check.mjs — so
767
+ * the deterministic runners re-surface this as its own diagnostic instead of an
768
+ * `aspect-check-runtime-error`. (The LLM-sizing paths build their own message
769
+ * from `file`/`line`; only the deterministic consumers read `messageData`.)
770
+ */
771
+ messageData;
741
772
  };
742
- var RE_SINGLE = /\byg-suppress\(\s*([^)]+?)\s*\)\s*(.+)?$/m;
743
- var RE_DISABLE = /\byg-suppress-disable\(\s*([^)]+?)\s*\)\s*(.+)?$/m;
744
- var RE_ENABLE = /\byg-suppress-enable\(\s*([^)]+?)\s*\)/;
745
- function commentBody(text) {
746
- if (text.startsWith("//")) return text.slice(2).trim();
747
- if (text.startsWith("/*")) return text.replace(/^\/\*+/, "").replace(/\*+\/$/, "").trim();
748
- return text.trim();
773
+ var LEADER_DELIM = String.raw`\/\/[/!]*|\/\*+|\*+|#+|--+|;+|<!--|\{-|\(\*|[!%'"]`;
774
+ var RE_LEADER_OPTIONAL = new RegExp(String.raw`^\s*(${LEADER_DELIM})?\s*`);
775
+ var RE_LEADER_REQUIRED = new RegExp(String.raw`^\s*(${LEADER_DELIM})\s*`);
776
+ var RE_MARKER = /^yg-suppress(-disable|-enable)?\(\s*([^)]*?)\s*\)\s*(.*)$/;
777
+ var RE_ASPECT_ID = /^(?:\*|[A-Za-z0-9_][A-Za-z0-9_./-]*)$/;
778
+ var RE_ANY_CLOSER = /\s*(?:\*+\/|-->|-\}|\*\))\s*$/;
779
+ var RE_CLOSER_CSTYLE = /\s*\*+\/\s*$/;
780
+ var RE_CLOSER_HTML = /\s*-->\s*$/;
781
+ var RE_CLOSER_HASKELL = /\s*-\}\s*$/;
782
+ var RE_CLOSER_OCAML = /\s*\*\)\s*$/;
783
+ function stripCloser(reason, leader) {
784
+ if (leader === void 0) return reason.replace(RE_ANY_CLOSER, "");
785
+ if (leader.startsWith("//")) return reason;
786
+ if (leader.startsWith("/*") || /^\*+$/.test(leader)) return reason.replace(RE_CLOSER_CSTYLE, "");
787
+ if (leader === "<!--") return reason.replace(RE_CLOSER_HTML, "");
788
+ if (leader === "{-") return reason.replace(RE_CLOSER_HASKELL, "");
789
+ if (leader === "(*") return reason.replace(RE_CLOSER_OCAML, "");
790
+ return reason;
749
791
  }
750
792
  function splitAspectList(raw) {
751
793
  return raw.split(",").map((s) => s.trim()).filter(Boolean);
752
794
  }
753
- function makeMarker(kind, match, line, file) {
754
- const aspectIds = splitAspectList(match[1]);
755
- const reason = (match[2] ?? "").trim();
756
- if (reason === "") {
795
+ function matchMarkerLine(line, requireDelimiter) {
796
+ if (line.endsWith("\r")) line = line.slice(0, -1);
797
+ const leader = (requireDelimiter ? RE_LEADER_REQUIRED : RE_LEADER_OPTIONAL).exec(line);
798
+ if (leader === null) return null;
799
+ const m = RE_MARKER.exec(line.slice(leader[0].length));
800
+ if (m === null) return null;
801
+ const aspectIds = splitAspectList(m[2]);
802
+ if (aspectIds.length === 0) return null;
803
+ if (!aspectIds.every((id) => RE_ASPECT_ID.test(id))) return null;
804
+ const kind = m[1] === "-disable" ? "disable" : m[1] === "-enable" ? "enable" : "single";
805
+ const reason = kind === "enable" ? "" : stripCloser(m[3], leader[1]).trim();
806
+ return { kind, aspectIds, reason };
807
+ }
808
+ function parseMarker(lineText, line, file, requireDelimiter) {
809
+ const m = matchMarkerLine(lineText, requireDelimiter);
810
+ if (m === null) return null;
811
+ if (m.kind !== "enable" && m.reason === "") {
757
812
  throw new SuppressMarkerError(
758
- `yg-suppress${kind === "disable" ? "-disable" : ""}(${match[1]}) missing reason at ${file}:${line}`,
813
+ `yg-suppress${m.kind === "disable" ? "-disable" : ""}(${m.aspectIds.join(", ")}) missing reason at ${file}:${line}`,
759
814
  file,
760
815
  line
761
816
  );
762
817
  }
763
- return { kind, aspectIds, reason, line };
818
+ return { kind: m.kind, aspectIds: m.aspectIds, reason: m.reason, line };
764
819
  }
765
- function parseMarker(commentText, line, file) {
766
- const body = commentBody(commentText);
767
- let m = body.match(RE_DISABLE);
768
- if (m) return makeMarker("disable", m, line, file);
769
- m = body.match(RE_ENABLE);
770
- if (m) {
771
- return { kind: "enable", aspectIds: splitAspectList(m[1]), reason: "", line };
772
- }
773
- m = body.match(RE_SINGLE);
774
- if (m) return makeMarker("single", m, line, file);
775
- return null;
820
+ var RE_FENCE = /^ {0,3}((?:`{3,})|(?:~{3,}))[ \t]*(.*)$/;
821
+ var MARKDOWN_EXTS = /* @__PURE__ */ new Set([".md", ".markdown"]);
822
+ function isMarkdownExt(file) {
823
+ return MARKDOWN_EXTS.has(extname3(file).toLowerCase());
824
+ }
825
+ function markdownFencedLines(text) {
826
+ const fenced = /* @__PURE__ */ new Set();
827
+ const lines = text.split("\n");
828
+ let open = null;
829
+ for (let i = 0; i < lines.length; i++) {
830
+ const m = RE_FENCE.exec(lines[i]);
831
+ if (open === null) {
832
+ if (m) {
833
+ open = { char: m[1][0], len: m[1].length };
834
+ fenced.add(i + 1);
835
+ }
836
+ continue;
837
+ }
838
+ fenced.add(i + 1);
839
+ if (m && m[1][0] === open.char && m[1].length >= open.len && m[2].trim() === "") {
840
+ open = null;
841
+ }
842
+ }
843
+ return fenced;
776
844
  }
777
845
  function collectSuppressions(tree, file, totalLines, content) {
778
846
  const hasGrammar = getLanguageForExtension(extname3(file)) !== null;
@@ -780,13 +848,18 @@ function collectSuppressions(tree, file, totalLines, content) {
780
848
  if (hasGrammar && tree) {
781
849
  const comments = findComments({ path: file, ast: tree });
782
850
  for (const c of comments) {
783
- const m = parseMarker(c.text, c.startPosition.row + 1, file);
784
- if (m) markers.push(m);
851
+ const commentLines = c.text.split("\n");
852
+ for (let i = 0; i < commentLines.length; i++) {
853
+ const m = parseMarker(commentLines[i], c.startPosition.row + i + 1, file, false);
854
+ if (m) markers.push(m);
855
+ }
785
856
  }
786
857
  } else if (content !== void 0) {
787
858
  const lines = content.split("\n");
859
+ const fenced = isMarkdownExt(file) ? markdownFencedLines(content) : null;
788
860
  for (let i = 0; i < lines.length; i++) {
789
- const m = parseMarker(lines[i], i + 1, file);
861
+ if (fenced && fenced.has(i + 1)) continue;
862
+ const m = parseMarker(lines[i], i + 1, file, true);
790
863
  if (m) markers.push(m);
791
864
  }
792
865
  } else {
@@ -812,22 +885,26 @@ function collectSuppressions(tree, file, totalLines, content) {
812
885
  for (const id of m.aspectIds) {
813
886
  if (id === "*") {
814
887
  if (openWildcard !== null) {
815
- ranges.push({ aspectIds: /* @__PURE__ */ new Set(["*"]), startLine: openWildcard, endLine: m.line - 1, isWildcard: true });
888
+ if (m.line - 1 >= openWildcard) {
889
+ ranges.push({ aspectIds: /* @__PURE__ */ new Set(["*"]), startLine: openWildcard, endLine: m.line - 1, isWildcard: true });
890
+ }
816
891
  openWildcard = null;
817
892
  }
818
893
  } else {
819
894
  const start = openSpecific.get(id);
820
895
  if (start !== void 0) {
821
- ranges.push({ aspectIds: /* @__PURE__ */ new Set([id]), startLine: start, endLine: m.line - 1, isWildcard: false });
896
+ if (m.line - 1 >= start) {
897
+ ranges.push({ aspectIds: /* @__PURE__ */ new Set([id]), startLine: start, endLine: m.line - 1, isWildcard: false });
898
+ }
822
899
  openSpecific.delete(id);
823
900
  }
824
901
  }
825
902
  }
826
903
  }
827
904
  }
828
- if (openWildcard !== null) ranges.push({ aspectIds: /* @__PURE__ */ new Set(["*"]), startLine: openWildcard, endLine: totalLines, isWildcard: true });
905
+ if (openWildcard !== null && openWildcard <= totalLines) ranges.push({ aspectIds: /* @__PURE__ */ new Set(["*"]), startLine: openWildcard, endLine: totalLines, isWildcard: true });
829
906
  for (const [id, start] of openSpecific) {
830
- ranges.push({ aspectIds: /* @__PURE__ */ new Set([id]), startLine: start, endLine: totalLines, isWildcard: false });
907
+ if (start <= totalLines) ranges.push({ aspectIds: /* @__PURE__ */ new Set([id]), startLine: start, endLine: totalLines, isWildcard: false });
831
908
  }
832
909
  return ranges;
833
910
  }
@@ -838,7 +915,7 @@ function isLineSuppressed(ranges, aspectId, line) {
838
915
  });
839
916
  }
840
917
  function formatSuppressedRangesForAspect(ranges, aspectId) {
841
- return ranges.filter((r) => r.isWildcard || r.aspectIds.has(aspectId)).map((r) => ({ startLine: r.startLine, endLine: r.endLine })).sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
918
+ return ranges.filter((r) => r.isWildcard || r.aspectIds.has(aspectId)).filter((r) => r.startLine <= r.endLine).map((r) => ({ startLine: r.startLine, endLine: r.endLine })).sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
842
919
  }
843
920
 
844
921
  // src/utils/validate-check-module.ts
@@ -1458,6 +1535,7 @@ async function buildUnitCtx(params) {
1458
1535
  }
1459
1536
 
1460
1537
  // src/structure/runner.ts
1538
+ var SUPPRESS_MARKER_MALFORMED_CODE = "STRUCTURE_SUPPRESS_MARKER_MALFORMED";
1461
1539
  async function runStructureAspect(params) {
1462
1540
  const { aspectDir, aspectId, nodePath, graph, projectRoot, subjectScope } = params;
1463
1541
  const ownCache = !params.parseCache;
@@ -1469,11 +1547,18 @@ async function runStructureAspect(params) {
1469
1547
  if (existing !== void 0) return existing;
1470
1548
  const cached = astCache.get(filePath);
1471
1549
  let ranges;
1472
- if (cached) {
1473
- ranges = collectSuppressions(cached.ast, filePath, cached.content.split("\n").length, cached.content);
1474
- } else {
1475
- const content = contentByPath.get(filePath);
1476
- ranges = content !== void 0 ? collectSuppressions(void 0, filePath, content.split("\n").length, content) : null;
1550
+ try {
1551
+ if (cached) {
1552
+ ranges = collectSuppressions(cached.ast, filePath, cached.content.split("\n").length, cached.content);
1553
+ } else {
1554
+ const content = contentByPath.get(filePath);
1555
+ ranges = content !== void 0 ? collectSuppressions(void 0, filePath, content.split("\n").length, content) : null;
1556
+ }
1557
+ } catch (err) {
1558
+ if (err instanceof SuppressMarkerError) {
1559
+ throw new StructureRunnerError(SUPPRESS_MARKER_MALFORMED_CODE, err.messageData);
1560
+ }
1561
+ throw err;
1477
1562
  }
1478
1563
  rangesByFile.set(filePath, ranges);
1479
1564
  return ranges;
@@ -0,0 +1,278 @@
1
+ /*
2
+ * Component styles — the count header, the tree, the node panel, the glossary tooltip,
3
+ * and the command-palette overlay.
4
+ *
5
+ * The reusable surface pieces the views compose. Tokens come from tokens.css; the shell
6
+ * frame from shell.css. Calm, dense, document-like; the only saturated color is a state
7
+ * (resolved through the state classes in tokens.css). This file stays focused on the
8
+ * in-stage components and the ⌘K overlay.
9
+ */
10
+
11
+ /* ── Count header + chips ───────────────────────────────────────────────── */
12
+ .stage-header {
13
+ border-bottom: 1px solid var(--border);
14
+ padding-bottom: 16px;
15
+ margin-bottom: 18px;
16
+ }
17
+ .stage-title {
18
+ font-size: 24px;
19
+ line-height: 32px;
20
+ font-weight: 600;
21
+ margin: 0 0 4px;
22
+ letter-spacing: -0.01em;
23
+ }
24
+ .stage-sub {
25
+ color: var(--text-secondary);
26
+ font-size: 12.5px;
27
+ margin: 0 0 16px;
28
+ }
29
+ .count-bar {
30
+ display: flex;
31
+ flex-wrap: wrap;
32
+ gap: 8px;
33
+ }
34
+ .count-chip {
35
+ display: inline-flex;
36
+ align-items: center;
37
+ gap: 6px;
38
+ padding: 4px 10px;
39
+ border-radius: 7px;
40
+ border: 1px solid var(--border-subtle);
41
+ background: var(--surface-1);
42
+ font-size: 13px;
43
+ }
44
+ .chip-count {
45
+ font-weight: 600;
46
+ }
47
+ .chip-label {
48
+ color: var(--text-secondary);
49
+ }
50
+
51
+ .stage-intro {
52
+ margin-bottom: 14px;
53
+ }
54
+ .stage-notfound {
55
+ margin: 10px 0 0;
56
+ padding: 8px 12px;
57
+ border: 1px solid var(--border);
58
+ border-left: 3px solid var(--unverified);
59
+ border-radius: 8px;
60
+ background: var(--surface-1);
61
+ color: var(--text-secondary);
62
+ font-size: 13px;
63
+ }
64
+ .stage-vtitle {
65
+ font-size: 16px;
66
+ margin: 0 0 2px;
67
+ }
68
+ .stage-vblurb {
69
+ margin: 0;
70
+ color: var(--text-secondary);
71
+ font-size: 13px;
72
+ }
73
+ .stage-note {
74
+ color: var(--text-muted);
75
+ font-size: 12.5px;
76
+ border: 1px dashed var(--border);
77
+ border-radius: 9px;
78
+ padding: 10px 12px;
79
+ margin: 4px 0 18px;
80
+ }
81
+
82
+ /* ── Tree (virtualized) ─────────────────────────────────────────────────── */
83
+ .tree-filter {
84
+ margin: 4px 0 8px;
85
+ }
86
+ .tree-filter-input {
87
+ width: 100%;
88
+ padding: 7px 11px;
89
+ border: 1px solid var(--border);
90
+ border-radius: 8px;
91
+ background: var(--bg);
92
+ color: var(--text-primary);
93
+ font: inherit;
94
+ font-size: 13px;
95
+ }
96
+ /* "You are here" marker for a row revealed by a palette pick / deep-link. */
97
+ .tree-row-revealed {
98
+ background: var(--surface-3);
99
+ box-shadow: inset 3px 0 0 var(--accent);
100
+ }
101
+ .tree-mount {
102
+ margin: 4px 0 18px;
103
+ }
104
+ .tree-scroller {
105
+ position: relative;
106
+ height: 60vh;
107
+ min-height: 280px;
108
+ overflow: auto;
109
+ border: 1px solid var(--border);
110
+ border-radius: 10px;
111
+ background: var(--surface-1);
112
+ }
113
+ .tree-spacer {
114
+ position: relative;
115
+ width: 100%;
116
+ }
117
+ .tree-window {
118
+ position: absolute;
119
+ top: 0;
120
+ left: 0;
121
+ right: 0;
122
+ will-change: transform;
123
+ }
124
+ .tree-row {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 9px;
128
+ height: 28px;
129
+ padding-right: 12px;
130
+ border-bottom: 1px solid var(--surface-2);
131
+ font-size: 13px;
132
+ cursor: pointer;
133
+ }
134
+ .tree-row:hover {
135
+ background: var(--surface-3);
136
+ }
137
+ .tree-name {
138
+ font-weight: 500;
139
+ }
140
+ .tree-type {
141
+ margin-left: auto;
142
+ color: var(--text-muted);
143
+ font-size: 11px;
144
+ }
145
+
146
+ /* ── Node panel ─────────────────────────────────────────────────────────── */
147
+ .panel-title {
148
+ font-size: 13px;
149
+ margin: 0 0 10px;
150
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
151
+ word-break: break-all;
152
+ }
153
+ .panel-head {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 9px;
157
+ font-size: 13px;
158
+ margin-bottom: 8px;
159
+ }
160
+ .panel-sub {
161
+ color: var(--text-secondary);
162
+ font-size: 12.5px;
163
+ margin: 0;
164
+ }
165
+
166
+ /* ── Glossary term tooltip ──────────────────────────────────────────────── */
167
+ .term {
168
+ border-bottom: 1px dotted var(--border-strong);
169
+ cursor: help;
170
+ }
171
+
172
+ /* ── Command palette overlay ────────────────────────────────────────────── */
173
+ .palette-backdrop {
174
+ position: fixed;
175
+ inset: 0;
176
+ display: flex;
177
+ align-items: flex-start;
178
+ justify-content: center;
179
+ padding-top: 60px;
180
+ background: rgba(0, 0, 0, 0.32);
181
+ z-index: 50;
182
+ }
183
+ .palette-box {
184
+ width: 640px;
185
+ max-width: 92vw;
186
+ background: var(--surface-1);
187
+ border: 1px solid var(--border);
188
+ border-radius: 14px;
189
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04), 0 16px 40px -10px rgba(0, 0, 0, 0.45);
190
+ overflow: hidden;
191
+ }
192
+ .palette-input {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 11px;
196
+ padding: 13px 16px;
197
+ border-bottom: 1px solid var(--border-subtle);
198
+ }
199
+ .palette-search-ic {
200
+ color: var(--text-muted);
201
+ }
202
+ .palette-field {
203
+ flex: 1;
204
+ border: 0;
205
+ background: none;
206
+ color: var(--text-primary);
207
+ font: inherit;
208
+ font-size: 16px;
209
+ }
210
+ .palette-field:focus {
211
+ outline: none;
212
+ }
213
+ .palette-results {
214
+ max-height: 380px;
215
+ overflow: auto;
216
+ padding: 6px;
217
+ }
218
+ .palette-empty {
219
+ padding: 16px;
220
+ color: var(--text-muted);
221
+ font-size: 13px;
222
+ }
223
+ .palette-row {
224
+ display: flex;
225
+ align-items: center;
226
+ gap: 11px;
227
+ padding: 8px 12px;
228
+ border-radius: 8px;
229
+ font-size: 13.5px;
230
+ cursor: pointer;
231
+ }
232
+ .palette-row.on {
233
+ background: var(--surface-3);
234
+ }
235
+ .palette-ic {
236
+ width: 18px;
237
+ text-align: center;
238
+ color: var(--text-muted);
239
+ }
240
+ .palette-nm {
241
+ font-weight: 500;
242
+ }
243
+ .palette-sub {
244
+ color: var(--text-muted);
245
+ font-size: 11.5px;
246
+ }
247
+ .palette-kind {
248
+ margin-left: auto;
249
+ font-size: 10px;
250
+ color: var(--text-muted);
251
+ border: 1px solid var(--border-subtle);
252
+ border-radius: 5px;
253
+ padding: 0 6px;
254
+ }
255
+ .palette-foot {
256
+ display: flex;
257
+ gap: 16px;
258
+ padding: 9px 14px;
259
+ border-top: 1px solid var(--border-subtle);
260
+ font-size: 11px;
261
+ color: var(--text-muted);
262
+ }
263
+
264
+ .portal-error {
265
+ color: var(--refused);
266
+ padding: 24px;
267
+ font-size: 14px;
268
+ }
269
+
270
+ @media (prefers-reduced-motion: reduce) {
271
+ *,
272
+ *::before,
273
+ *::after {
274
+ transition: none !important;
275
+ animation: none !important;
276
+ scroll-behavior: auto !important;
277
+ }
278
+ }
@@ -62,7 +62,7 @@
62
62
  var rows = [['metric', 'value']];
63
63
  var keys = [
64
64
  'nodes', 'aspects', 'flows', 'pairsTotal', 'pairsLLM', 'pairsDet',
65
- 'verified', 'refused', 'advisoryRefused', 'unverified', 'noRule', 'draft', 'notApplicable',
65
+ 'verified', 'verifiedDet', 'verifiedLlm', 'refused', 'advisoryRefused', 'unverified', 'noRule', 'draft', 'notApplicable',
66
66
  'suppressed', 'coveredFiles', 'uncoveredFiles', 'totalFiles', 'errors', 'warnings',
67
67
  ];
68
68
  for (var i = 0; i < keys.length; i += 1) rows.push([keys[i], c[keys[i]]]);
@@ -75,8 +75,11 @@
75
75
 
76
76
  // The bar is sized by the real pair STATES (verified / refused / unverified), never by the
77
77
  // expected-pair kind totals — a verified segment must be exactly as wide as the verified
78
- // count, so an unverified pair can never paint green. The verified label states the LLM-vs-
79
- // deterministic makeup of the expected universe honestly without faking a verified split.
78
+ // count, so an unverified pair can never paint green. The verified label states BOTH the
79
+ // LLM-vs-deterministic makeup of the expected universe AND the real split of the verified
80
+ // count itself (verifiedDet / verifiedLlm — tallied off the identical pairs loop `yg check`
81
+ // uses for its own header), so a green bar can never hide how many pairs were machine-checked
82
+ // for free vs actually reviewed by an LLM.
80
83
  // Advisory refusals are a real expected-pair state, but per the honesty model they render
81
84
  // as a NON-BLOCKING warning, never a blocking `refused`. They get their own warning-coloured
82
85
  // segment + key so the bar still accounts for every expected pair without ever showing an
@@ -96,7 +99,12 @@
96
99
  ledger.appendChild(bar);
97
100
 
98
101
  var labels = dom.el('div', 'cov-barlabels');
99
- labels.appendChild(key('verified', c.verified, 'of ' + c.pairsLLM + ' LLM + ' + c.pairsDet + ' deterministic expected'));
102
+ labels.appendChild(key(
103
+ 'verified',
104
+ c.verified,
105
+ 'of ' + c.pairsLLM + ' LLM + ' + c.pairsDet + ' deterministic expected'
106
+ + ' (' + (c.verifiedDet || 0) + ' deterministic, ' + (c.verifiedLlm || 0) + ' LLM verified)',
107
+ ));
100
108
  labels.appendChild(key('refused', c.refused, 'enforced — blocks (== yg check)'));
101
109
  if (advisoryRefused > 0) labels.appendChild(key('warning', advisoryRefused, 'advisory refusal — does not block'));
102
110
  labels.appendChild(key('unverified', c.unverified));