@chrisdudek/yg 5.2.6 → 5.4.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.
Files changed (29) hide show
  1. package/dist/bin.js +5152 -3063
  2. package/dist/structure.d.ts +9 -0
  3. package/dist/structure.js +10 -5
  4. package/dist/templates/portal/js/bootstrap.js +206 -0
  5. package/dist/templates/portal/js/consumer.js +128 -0
  6. package/dist/templates/portal/js/dispatch.js +181 -0
  7. package/dist/templates/portal/js/export.js +151 -0
  8. package/dist/templates/portal/js/glossary.js +72 -0
  9. package/dist/templates/portal/js/namespace.js +60 -0
  10. package/dist/templates/portal/js/palette-overlay.js +176 -0
  11. package/dist/templates/portal/js/palette.js +133 -0
  12. package/dist/templates/portal/js/router.js +169 -0
  13. package/dist/templates/portal/js/shell.js +264 -0
  14. package/dist/templates/portal/js/state-model.js +142 -0
  15. package/dist/templates/portal/js/tree.js +190 -0
  16. package/dist/templates/portal/js/views/coverage-view.js +247 -0
  17. package/dist/templates/portal/js/views/flows-view.js +204 -0
  18. package/dist/templates/portal/js/views/overview-view.js +167 -0
  19. package/dist/templates/portal/js/views/panel-aspect.js +141 -0
  20. package/dist/templates/portal/js/views/panel-view.js +314 -0
  21. package/dist/templates/portal/js/views/relations-matrix.js +185 -0
  22. package/dist/templates/portal/js/views/relations-view.js +185 -0
  23. package/dist/templates/portal/js/views/rulebook-view.js +174 -0
  24. package/dist/templates/portal/js/views/start-view.js +287 -0
  25. package/dist/templates/portal/js/views/suppressions-view.js +187 -0
  26. package/dist/templates/portal/js/views/tree-view.js +68 -0
  27. package/dist/templates/portal/js/views/types-view.js +154 -0
  28. package/dist/templates/portal/vendor/d3-hierarchy.js +350 -0
  29. package/package.json +5 -2
@@ -0,0 +1,185 @@
1
+ /*
2
+ * V4 part (a) — the allowed-relations matrix (Canvas grid + DOM mirror).
3
+ *
4
+ * The architecture's allowed-relations rules as a node-type × node-type grid: a filled cell
5
+ * means the architecture permits that relation type from the row type to the column type; an
6
+ * empty cell means it permits none (a forbidden pair). This is *allowed*, not *actual* —
7
+ * conformance is the separate live boundary check. The dense grid is drawn on Canvas 2D (the
8
+ * one place §3a sanctions Canvas), with a DOM-list MIRROR beside it so the matrix is not
9
+ * opaque to a screen reader (Canvas alone is). Colors come from CSS custom properties read off
10
+ * the page; this module paints relation presence only — it never paints a verdict state, so it
11
+ * does not (and must not) invent a green.
12
+ *
13
+ * Browser globals only — reads the already-resolved PortalData; no network, no Node.
14
+ */
15
+ (function () {
16
+ 'use strict';
17
+
18
+ var Yg = (window.YgPortal = window.YgPortal || {});
19
+ var dom = Yg.dom;
20
+ Yg.views = Yg.views || {};
21
+ Yg.matrix = {};
22
+
23
+ var CELL = 26;
24
+ var HEADER = 92;
25
+ var ROWLBL = 150;
26
+
27
+ // Relation type → a stable, colorblind-safe hue (presence marker only, not a state).
28
+ var REL_COLOR = {
29
+ calls: '#0d74ce',
30
+ uses: '#208368',
31
+ extends: '#9a6700',
32
+ implements: '#8e4ec6',
33
+ emits: '#d6409f',
34
+ listens: '#d6409f',
35
+ };
36
+
37
+ /** The sorted union of every type that appears as a relation source or target. */
38
+ Yg.matrix.axisTypes = function (types) {
39
+ var ids = (types || []).map(function (t) {
40
+ return t.id;
41
+ });
42
+ return ids.slice().sort();
43
+ };
44
+
45
+ /** The relation types allowed from `rowType` to `colType`, by the architecture matrix. */
46
+ Yg.matrix.allowedBetween = function (typesById, rowType, colType) {
47
+ var row = typesById[rowType];
48
+ if (!row || !row.allowedRelations) return [];
49
+ var out = [];
50
+ for (var rel in row.allowedRelations) {
51
+ if (!Object.prototype.hasOwnProperty.call(row.allowedRelations, rel)) continue;
52
+ var targets = row.allowedRelations[rel] || [];
53
+ if (targets.indexOf(colType) !== -1) out.push(rel);
54
+ }
55
+ return out;
56
+ };
57
+
58
+ function cssVar(name, fallback) {
59
+ try {
60
+ var v = getComputedStyle(document.documentElement).getPropertyValue(name);
61
+ return (v && v.trim()) || fallback;
62
+ } catch (_e) {
63
+ return fallback;
64
+ }
65
+ }
66
+
67
+ function drawCanvas(canvas, axis, typesById) {
68
+ var ctx = canvas.getContext && canvas.getContext('2d');
69
+ if (!ctx) return; // jsdom/test sandbox without canvas — the DOM mirror carries the data
70
+ var n = axis.length;
71
+ var muted = cssVar('--text-secondary', '#60646c');
72
+ var border = cssVar('--border-subtle', '#d9d9e0');
73
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
74
+ ctx.font = '11px ui-sans-serif, system-ui, sans-serif';
75
+ ctx.textBaseline = 'middle';
76
+
77
+ // Column headers (rotated) + row labels.
78
+ for (var c = 0; c < n; c += 1) {
79
+ ctx.save();
80
+ ctx.translate(ROWLBL + c * CELL + CELL / 2, HEADER - 6);
81
+ ctx.rotate(-Math.PI / 3);
82
+ ctx.fillStyle = muted;
83
+ ctx.textAlign = 'left';
84
+ ctx.fillText(axis[c], 0, 0);
85
+ ctx.restore();
86
+ }
87
+ for (var r = 0; r < n; r += 1) {
88
+ ctx.fillStyle = muted;
89
+ ctx.textAlign = 'right';
90
+ ctx.fillText(axis[r], ROWLBL - 8, HEADER + r * CELL + CELL / 2);
91
+ }
92
+
93
+ // Cells.
94
+ ctx.textAlign = 'center';
95
+ for (var ri = 0; ri < n; ri += 1) {
96
+ for (var ci = 0; ci < n; ci += 1) {
97
+ var x = ROWLBL + ci * CELL;
98
+ var y = HEADER + ri * CELL;
99
+ ctx.strokeStyle = border;
100
+ ctx.strokeRect(x, y, CELL, CELL);
101
+ if (ri === ci) {
102
+ ctx.fillStyle = cssVar('--surface-2', '#f0f0f3');
103
+ ctx.fillRect(x + 1, y + 1, CELL - 2, CELL - 2);
104
+ continue;
105
+ }
106
+ var rels = Yg.matrix.allowedBetween(typesById, axis[ri], axis[ci]);
107
+ if (rels.length) {
108
+ ctx.fillStyle = REL_COLOR[rels[0]] || muted;
109
+ ctx.beginPath();
110
+ ctx.arc(x + CELL / 2, y + CELL / 2, 4, 0, Math.PI * 2);
111
+ ctx.fill();
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ /** A DOM-list mirror of the matrix: every allowed (row → rel → col) edge, screen-reader-legible. */
118
+ function buildMirror(axis, typesById) {
119
+ var mirror = dom.el('div', 'mtx-mirror');
120
+ mirror.setAttribute('aria-label', 'Allowed relations, as a list');
121
+ var any = false;
122
+ for (var ri = 0; ri < axis.length; ri += 1) {
123
+ for (var ci = 0; ci < axis.length; ci += 1) {
124
+ if (ri === ci) continue;
125
+ var rels = Yg.matrix.allowedBetween(typesById, axis[ri], axis[ci]);
126
+ if (!rels.length) continue;
127
+ any = true;
128
+ var line = dom.el('div', 'mtx-mirror-row');
129
+ line.appendChild(dom.el('span', 'mono', axis[ri]));
130
+ line.appendChild(dom.el('span', 'mtx-arrow', ' → '));
131
+ line.appendChild(dom.el('span', 'mtx-rels', rels.join(' / ')));
132
+ line.appendChild(dom.el('span', 'mtx-arrow', ' → '));
133
+ line.appendChild(dom.el('span', 'mono', axis[ci]));
134
+ mirror.appendChild(line);
135
+ }
136
+ }
137
+ if (!any) mirror.appendChild(dom.el('p', 'mtx-empty', 'The architecture declares no allowed relations between these types — every pair is a forbidden cell.'));
138
+ return mirror;
139
+ }
140
+
141
+ function legend() {
142
+ var box = dom.el('div', 'mtx-legend');
143
+ for (var rel in REL_COLOR) {
144
+ if (!Object.prototype.hasOwnProperty.call(REL_COLOR, rel)) continue;
145
+ if (rel === 'listens') continue; // emits/listens share one swatch
146
+ var k = dom.el('span', 'mtx-legend-k');
147
+ var sw = dom.el('span', 'mtx-swatch');
148
+ sw.style.background = REL_COLOR[rel];
149
+ k.appendChild(sw);
150
+ k.appendChild(dom.el('span', null, rel === 'emits' ? 'emits/listens' : rel));
151
+ box.appendChild(k);
152
+ }
153
+ var empty = dom.el('span', 'mtx-legend-k mtx-legend-empty', 'empty = forbidden by architecture');
154
+ box.appendChild(empty);
155
+ return box;
156
+ }
157
+
158
+ /** Render the allowed-relations matrix (Canvas + DOM mirror) into `mount`. */
159
+ Yg.matrix.render = function (mount, data) {
160
+ var typesById = {};
161
+ (data.types || []).forEach(function (t) {
162
+ typesById[t.id] = t;
163
+ });
164
+ var axis = Yg.matrix.axisTypes(data.types);
165
+
166
+ mount.appendChild(dom.el('p', 'view-lead', "What's allowed to depend on what — the architecture's node-type × node-type rules. An empty cell means no relation is permitted there. This is allowed, not actual: conformance is the live boundary check below."));
167
+
168
+ // The dense grid keeps its intrinsic pixel size and scrolls WITHIN its own container
169
+ // (overflow-x on .mtx-scroll), so a wide matrix never pushes the page into a horizontal
170
+ // scrollbar — it stays legible and contained instead of being scaled down.
171
+ var scroll = dom.el('div', 'mtx-scroll');
172
+ var canvas = document.createElement('canvas');
173
+ canvas.className = 'mtx-canvas';
174
+ canvas.width = ROWLBL + axis.length * CELL + 4;
175
+ canvas.height = HEADER + axis.length * CELL + 4;
176
+ canvas.setAttribute('role', 'img');
177
+ canvas.setAttribute('aria-label', 'Allowed-relations matrix; a list mirror follows');
178
+ scroll.appendChild(canvas);
179
+ mount.appendChild(scroll);
180
+ drawCanvas(canvas, axis, typesById);
181
+
182
+ mount.appendChild(legend());
183
+ mount.appendChild(buildMirror(axis, typesById));
184
+ };
185
+ })();
@@ -0,0 +1,185 @@
1
+ /*
2
+ * V4 Relations & Boundaries — orchestrator (hubs + the live boundary), with the matrix.
3
+ *
4
+ * Three parts, all MVP (§3.4, §3a V4): (a) the allowed-relations matrix (delegated to
5
+ * Yg.matrix), (b) the fan-in / fan-out hub ranking (the load-bearing nodes), and (c) the LIVE
6
+ * boundary, recomputed at generation, never read from the lock.
7
+ *
8
+ * CRITICAL HONESTY — the boundary has THREE classes, and they are NOT the same thing:
9
+ * - phantom — a real undeclared code dependency. A VIOLATION (boundary state, error).
10
+ * - forbiddenType — a code dependency to a type the architecture forbids. A VIOLATION.
11
+ * - declaredOnly — a DECLARED relation with no static code backing (reflection / DI / HTTP /
12
+ * events). LEGITIMATE by design; rendered NEUTRALLY / informationally,
13
+ * never red, never called a violation, never summed into a violation count.
14
+ * And when the live parse could not run (boundary.unknown), the surface renders UNKNOWN — not
15
+ * clean, not zero — never a fabricated green. Every verdict-state treatment is read from the one
16
+ * shared honest-state model.
17
+ *
18
+ * Browser globals only — reads the already-resolved PortalData; no network, no Node.
19
+ */
20
+ (function () {
21
+ 'use strict';
22
+
23
+ var Yg = (window.YgPortal = window.YgPortal || {});
24
+ var dom = Yg.dom;
25
+ Yg.views = Yg.views || {};
26
+
27
+ function hubColumn(title, cap, entries, nav) {
28
+ var col = dom.el('div', 'rel-hubcol');
29
+ col.appendChild(dom.el('h3', 'rel-hubh', title));
30
+ col.appendChild(dom.el('div', 'rel-hubcap', cap));
31
+ var max = 1;
32
+ for (var i = 0; i < entries.length; i += 1) max = Math.max(max, entries[i].count);
33
+ if (!entries.length) {
34
+ col.appendChild(dom.el('p', 'rel-empty', 'None.'));
35
+ return col;
36
+ }
37
+ for (var j = 0; j < entries.length; j += 1) {
38
+ var e = entries[j];
39
+ var row = dom.el('button', 'rel-hubrow');
40
+ row.type = 'button';
41
+ row.appendChild(dom.el('span', 'rel-hubname mono', e.path));
42
+ var bar = dom.el('span', 'rel-hubbar');
43
+ bar.style.width = Math.round((e.count / max) * 120) + 'px';
44
+ row.appendChild(bar);
45
+ row.appendChild(dom.el('span', 'rel-hubn', String(e.count)));
46
+ row.addEventListener('click', function (p) {
47
+ return function () {
48
+ nav({ view: 'tree', node: p });
49
+ };
50
+ }(e.path));
51
+ col.appendChild(row);
52
+ }
53
+ return col;
54
+ }
55
+
56
+ function summaryCard(count, label, tone) {
57
+ var card = dom.el('div', 'rel-scard' + (tone ? ' rel-scard-' + tone : ''));
58
+ card.appendChild(dom.el('b', null, String(count)));
59
+ card.appendChild(dom.el('div', 'rel-scard-l', label));
60
+ return card;
61
+ }
62
+
63
+ /** A phantom (undeclared) violation row — a real error, with a copy-ready relation stanza. */
64
+ function phantomRow(v, nav) {
65
+ var row = dom.el('div', 'rel-viol rel-viol-phantom');
66
+ row.appendChild(Yg.states.badge('boundary'));
67
+ var link = dom.el('button', 'rel-viol-src mono');
68
+ link.type = 'button';
69
+ link.textContent = v.source + ' → ' + v.target;
70
+ link.addEventListener('click', function () {
71
+ nav({ view: 'tree', node: v.source });
72
+ });
73
+ row.appendChild(link);
74
+ var stanza = dom.el('pre', 'rel-stanza', 'relations:\n - target: ' + v.target + '\n type: <allowed-type>');
75
+ row.appendChild(stanza);
76
+ return row;
77
+ }
78
+
79
+ /** A forbidden-type violation — links to the Type Model / matrix that forbids the pair. */
80
+ function forbiddenRow(v, nav) {
81
+ var row = dom.el('div', 'rel-viol rel-viol-forbidden');
82
+ row.appendChild(Yg.states.badge('boundary'));
83
+ row.appendChild(dom.el('span', 'rel-viol-src mono', v.source + ' → ' + v.target));
84
+ var to = dom.el('button', 'rel-viol-link');
85
+ to.type = 'button';
86
+ to.textContent = 'see the architecture decision →';
87
+ to.addEventListener('click', function () {
88
+ nav({ view: 'types' });
89
+ });
90
+ row.appendChild(to);
91
+ return row;
92
+ }
93
+
94
+ /** A declared-only edge — NEUTRAL / informational. Never red, never a violation. */
95
+ function declaredOnlyRow(v) {
96
+ var row = dom.el('div', 'rel-decl');
97
+ row.appendChild(dom.el('span', 'rel-decl-mark', '↪'));
98
+ row.appendChild(dom.el('span', 'mono', v.source + ' → ' + v.target));
99
+ row.appendChild(dom.el('span', 'rel-decl-note', 'declared, no static backing — legitimate (DI / HTTP / reflection / events)'));
100
+ return row;
101
+ }
102
+
103
+ function renderBoundary(stage, boundary, nav) {
104
+ var head = dom.el('div', 'rel-bhead');
105
+ head.appendChild(dom.el('span', 'cov-livebadge', 'LIVE'));
106
+ head.appendChild(dom.el('span', null, 'recomputed now, never cached · always an error · not suppressible'));
107
+ stage.appendChild(head);
108
+
109
+ // UNKNOWN degraded state — honest, never a fabricated clean.
110
+ if (boundary.unknown) {
111
+ var unk = dom.el('div', 'rel-unknown');
112
+ unk.appendChild(Yg.states.badge('unverified'));
113
+ unk.appendChild(dom.el('b', null, 'UNKNOWN — relation health unknown, not clean'));
114
+ unk.appendChild(dom.el('p', null, 'The live relation parse could not run, so the boundary cannot be computed. This is not a clean result and not zero. Re-run yg check once the parse can complete.'));
115
+ stage.appendChild(unk);
116
+ return;
117
+ }
118
+
119
+ var phantom = boundary.phantom || [];
120
+ var declaredOnly = boundary.declaredOnly || [];
121
+ var forbidden = boundary.forbiddenType || [];
122
+
123
+ var summary = dom.el('div', 'rel-summary');
124
+ summary.appendChild(summaryCard(phantom.length, 'phantom (undeclared) ' + (phantom.length === 0 ? '— clean' : '— error'), phantom.length === 0 ? 'clean' : 'err'));
125
+ summary.appendChild(summaryCard(declaredOnly.length, 'declared-only (informational)', 'info'));
126
+ summary.appendChild(summaryCard(forbidden.length, 'forbidden-type crossings', forbidden.length === 0 ? 'clean' : 'err'));
127
+ stage.appendChild(summary);
128
+
129
+ // Phantom — violations.
130
+ var phSect = dom.el('section', 'rel-class');
131
+ phSect.appendChild(dom.el('h3', 'rel-classh', 'Phantom — code depends on a node it never declared'));
132
+ if (phantom.length === 0) {
133
+ phSect.appendChild(dom.el('p', 'rel-clean', '✓ clean — no undeclared dependency on current inputs.'));
134
+ } else {
135
+ for (var i = 0; i < phantom.length; i += 1) phSect.appendChild(phantomRow(phantom[i], nav));
136
+ }
137
+ stage.appendChild(phSect);
138
+
139
+ // Forbidden-type — violations.
140
+ var fbSect = dom.el('section', 'rel-class');
141
+ fbSect.appendChild(dom.el('h3', 'rel-classh', 'Forbidden-type — a dependency the architecture allows no relation for'));
142
+ if (forbidden.length === 0) {
143
+ fbSect.appendChild(dom.el('p', 'rel-clean', '✓ clean — no forbidden-type crossing on current inputs.'));
144
+ } else {
145
+ for (var f = 0; f < forbidden.length; f += 1) fbSect.appendChild(forbiddenRow(forbidden[f], nav));
146
+ }
147
+ stage.appendChild(fbSect);
148
+
149
+ // Declared-only — NEUTRAL, never red.
150
+ var doSect = dom.el('section', 'rel-class rel-class-info');
151
+ doSect.appendChild(dom.el('h3', 'rel-classh', 'Declared-only — relation declared, no static backing (legitimate, never red)'));
152
+ doSect.appendChild(dom.el('p', 'rel-decl-intro', 'One-directional contract: a declared relation needs no code behind it. These are correct, not dead edges.'));
153
+ if (declaredOnly.length === 0) {
154
+ doSect.appendChild(dom.el('p', 'rel-empty', 'None on current inputs.'));
155
+ } else {
156
+ for (var d = 0; d < declaredOnly.length; d += 1) doSect.appendChild(declaredOnlyRow(declaredOnly[d]));
157
+ }
158
+ stage.appendChild(doSect);
159
+ }
160
+
161
+ Yg.views.relations = function (stage, route, data, ctx) {
162
+ var nav = ctx && ctx.navigate ? ctx.navigate : function () {};
163
+
164
+ // (a) allowed-relations matrix.
165
+ var mtxSect = dom.el('section', 'rel-part');
166
+ mtxSect.appendChild(dom.el('h2', 'rel-parth', 'Allowed relations'));
167
+ Yg.matrix.render(mtxSect, data);
168
+ stage.appendChild(mtxSect);
169
+
170
+ // (b) fan-in / fan-out hubs.
171
+ var hubSect = dom.el('section', 'rel-part');
172
+ hubSect.appendChild(dom.el('h2', 'rel-parth', 'Dependency hubs'));
173
+ var cols = dom.el('div', 'rel-hubcols');
174
+ cols.appendChild(hubColumn('Most depended-on (fan-in)', 'If one of these breaks, a lot breaks with it.', (data.hubs && data.hubs.fanIn) || [], nav));
175
+ cols.appendChild(hubColumn('Depends on the most (fan-out)', 'Wide reach often signals responsibilities to split.', (data.hubs && data.hubs.fanOut) || [], nav));
176
+ hubSect.appendChild(cols);
177
+ stage.appendChild(hubSect);
178
+
179
+ // (c) the live boundary.
180
+ var bSect = dom.el('section', 'rel-part');
181
+ bSect.appendChild(dom.el('h2', 'rel-parth', 'Live boundary'));
182
+ renderBoundary(bSect, data.boundary || { unknown: false }, nav);
183
+ stage.appendChild(bSect);
184
+ };
185
+ })();
@@ -0,0 +1,174 @@
1
+ /*
2
+ * V5 Rulebook — the aspect catalogue + per-aspect node list with HONEST cells.
3
+ *
4
+ * The rulebook a lead screen-shares (§3.5, §3a V5). Every rule the code must satisfy, as a
5
+ * searchable catalogue: id + rule prose, a kind badge (LLM / deterministic / aggregating), a
6
+ * status badge (draft / advisory / enforced), scope (per node / file + `when` presence), how
7
+ * many nodes it lands on, and a coverage micro-summary. The micro-summary has THREE DISTINCT
8
+ * honest renderings the contract keeps apart and this view must never collapse into one green:
9
+ * - normal — a real V/R/U tally over the aspect's expected units (a reviewer/check ran).
10
+ * - aggregate — an aggregating bundle has no own reviewer: it "judges nothing".
11
+ * - vacuous — a rule-bearing aspect with ZERO expected units "verifies nothing", with the
12
+ * resolved reason (draft / no effective node / scope-when excludes all).
13
+ * Selecting an aspect expands the node list it lands on, each node rendered with its honest
14
+ * verdict cell (§3a V5: aspect row → node cell → SHELL-panel). Every state treatment is read
15
+ * from the one shared honest-state model; nothing here invents a green.
16
+ *
17
+ * Browser globals only — reads the already-resolved PortalData; no network, no Node.
18
+ */
19
+ (function () {
20
+ 'use strict';
21
+
22
+ var Yg = (window.YgPortal = window.YgPortal || {});
23
+ var dom = Yg.dom;
24
+ Yg.views = Yg.views || {};
25
+
26
+ /** A kind badge (LLM / deterministic / aggregating) — presentation only, never a verdict. */
27
+ function kindBadge(kind) {
28
+ var label = kind === 'llm' ? 'LLM' : kind === 'aggregate' ? 'aggregating' : 'deterministic';
29
+ return dom.el('span', 'rb-badge rb-badge-' + kind, label);
30
+ }
31
+
32
+ /** A status badge (draft / advisory / enforced) — rendering of how a verdict blocks, not green. */
33
+ function statusBadge(status) {
34
+ return dom.el('span', 'rb-st rb-st-' + status, status);
35
+ }
36
+
37
+ /**
38
+ * The honest coverage cell for one aspect tally. NEVER green for an aspect that judges or
39
+ * verifies nothing: an aggregate reads "judges nothing", a vacuous aspect reads "verifies
40
+ * nothing" with its reason. A normal tally paints a micro-bar sized by the real V/R/U states.
41
+ */
42
+ function tallyCell(tally) {
43
+ var cell = dom.el('td', 'rb-cov');
44
+ if (tally.render === 'aggregate') {
45
+ cell.appendChild(dom.el('span', 'rb-note', 'judges nothing — bundle (state = union of children)'));
46
+ return cell;
47
+ }
48
+ if (tally.render === 'vacuous') {
49
+ var note = dom.el('span', 'rb-note rb-note-vacuous', 'verifies nothing — ' + tally.reason);
50
+ cell.appendChild(note);
51
+ return cell;
52
+ }
53
+ // normal — a micro-bar sized by the real pair states, so an unverified unit never paints
54
+ // green and an advisory refusal paints its own warning segment, never a blocking red.
55
+ var bar = dom.el('div', 'rb-bar');
56
+ var warning = tally.warning || 0;
57
+ var segs = [
58
+ { state: 'verified', n: tally.verified },
59
+ { state: 'refused', n: tally.refused },
60
+ { state: 'warning', n: warning },
61
+ { state: 'unverified', n: tally.unverified },
62
+ ];
63
+ for (var i = 0; i < segs.length; i += 1) {
64
+ if (segs[i].n <= 0) continue;
65
+ var seg = dom.el('i', 'rb-bar-seg ' + Yg.states.cssClass(segs[i].state));
66
+ seg.style.flex = String(segs[i].n);
67
+ bar.appendChild(seg);
68
+ }
69
+ if (!bar.firstChild) bar.appendChild(dom.el('i', 'rb-bar-seg ' + Yg.states.cssClass('no-rule')));
70
+ cell.appendChild(bar);
71
+ var num =
72
+ tally.verified + ' verified / ' + tally.refused + ' refused / ' +
73
+ (warning > 0 ? warning + ' advisory / ' : '') + tally.unverified + ' unverified';
74
+ cell.appendChild(dom.el('div', 'rb-covnum mono', num));
75
+ return cell;
76
+ }
77
+
78
+ /** One aspect row in the catalogue table. Clicking the id toggles its per-node expansion. */
79
+ function aspectRow(a, selected, nav) {
80
+ var tr = dom.el('tr', 'rb-row' + (selected ? ' rb-row-sel' : ''));
81
+
82
+ var idCell = dom.el('td', 'rb-aid');
83
+ var idBtn = dom.el('button', 'rb-idbtn mono');
84
+ idBtn.type = 'button';
85
+ idBtn.textContent = a.id;
86
+ idBtn.addEventListener('click', function () {
87
+ // Toggle: clicking the open aspect collapses it (route back to the bare rulebook).
88
+ nav(selected ? { view: 'rulebook' } : { view: 'rulebook', aspect: a.id });
89
+ });
90
+ idCell.appendChild(idBtn);
91
+ var prose = a.description || (a.ruleProse ? firstLine(a.ruleProse) : '') || a.name;
92
+ if (prose) idCell.appendChild(dom.el('span', 'rb-desc', prose));
93
+ tr.appendChild(idCell);
94
+
95
+ tr.appendChild(dom.el('td', null, kindBadge(a.kind)));
96
+ tr.appendChild(dom.el('td', null, statusBadge(a.status)));
97
+
98
+ var scopeCell = dom.el('td', 'mono rb-scope');
99
+ scopeCell.textContent = a.kind === 'aggregate' ? '—' : 'per:' + a.scope + (a.hasWhen ? ' · when' : '');
100
+ tr.appendChild(scopeCell);
101
+
102
+ var appliesCell = dom.el('td', 'rb-applies');
103
+ appliesCell.textContent = appliesText(a);
104
+ tr.appendChild(appliesCell);
105
+
106
+ tr.appendChild(tallyCell(a.tally));
107
+ return tr;
108
+ }
109
+
110
+ /** "Applies to N nodes" / "via implies" / "0 nodes" — derived honestly from the tally. */
111
+ function appliesText(a) {
112
+ if (a.kind === 'aggregate') return 'via implies';
113
+ if (a.tally.render === 'vacuous') return '0 nodes';
114
+ return a.tally.units + (a.tally.units === 1 ? ' unit' : ' units');
115
+ }
116
+
117
+ /** The first non-heading prose line of a content.md, for a one-line description. */
118
+ function firstLine(prose) {
119
+ var lines = String(prose).split('\n');
120
+ for (var i = 0; i < lines.length; i += 1) {
121
+ var t = lines[i].trim();
122
+ if (t && t.charAt(0) !== '#') return t.length > 140 ? t.slice(0, 137) + '…' : t;
123
+ }
124
+ return '';
125
+ }
126
+
127
+
128
+ function counts(aspects) {
129
+ var llm = 0;
130
+ var det = 0;
131
+ var agg = 0;
132
+ for (var i = 0; i < aspects.length; i += 1) {
133
+ if (aspects[i].kind === 'llm') llm += 1;
134
+ else if (aspects[i].kind === 'aggregate') agg += 1;
135
+ else det += 1;
136
+ }
137
+ return aspects.length + ' rules · ' + llm + ' LLM · ' + det + ' deterministic · ' + agg + ' aggregating';
138
+ }
139
+
140
+ Yg.views.rulebook = function (stage, route, data, ctx) {
141
+ var nav = ctx && ctx.navigate ? ctx.navigate : function () {};
142
+ var aspects = (data.aspects || []).slice();
143
+ var selectedId = route && route.aspect ? route.aspect : null;
144
+
145
+ stage.appendChild(dom.el('p', 'view-lead', 'Every rule the code must satisfy — the rulebook. A green tally means a reviewer actually checked those units; a bundle judges nothing of its own, and a rule with no expected units verifies nothing. Absence of red is not a pass. Click a rule to see every node it lands on.'));
146
+ stage.appendChild(dom.el('div', 'rb-sub', counts(aspects)));
147
+
148
+ var table = dom.el('table', 'rb-table');
149
+ var thead = dom.el('thead');
150
+ var htr = dom.el('tr');
151
+ ['rule', 'kind', 'status', 'scope', 'applies', 'coverage'].forEach(function (h) {
152
+ htr.appendChild(dom.el('td', null, h));
153
+ });
154
+ thead.appendChild(htr);
155
+ table.appendChild(thead);
156
+
157
+ var tbody = dom.el('tbody');
158
+ var selectedAspect = null;
159
+ for (var i = 0; i < aspects.length; i += 1) {
160
+ var isSel = aspects[i].id === selectedId;
161
+ if (isSel) selectedAspect = aspects[i];
162
+ tbody.appendChild(aspectRow(aspects[i], isSel, nav));
163
+ // The selected rule's full detail opens in the shared inspector panel (the aspect-side
164
+ // mirror of the node attestation), reached via the #/aspect/<id> route — not inline here.
165
+ }
166
+ table.appendChild(tbody);
167
+ stage.appendChild(table);
168
+
169
+ if (!selectedAspect && selectedId) {
170
+ // A deep-linked aspect id that no longer exists — honest, never a blank.
171
+ stage.appendChild(dom.el('p', 'rb-empty', 'No rule named "' + selectedId + '" — it may have been removed.'));
172
+ }
173
+ };
174
+ })();