@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,287 @@
1
+ /*
2
+ * V9 Start here — the new-joiner on-ramp.
3
+ *
4
+ * A short scripted walk assembled live from the committed graph (§3a V9), so it is rot-proof:
5
+ * every word is derived from the resolved PortalData, never hardcoded. Five plain-language
6
+ * steps: (1) what this system is, (2) the big areas of the codebase, (3) one business process
7
+ * end to end, (4) one component explained, (5) reading the colours (the honest key). The front
8
+ * door of the pro-adoption rebalance.
9
+ *
10
+ * The step is in-view state with no hash grammar of its own, so the wizard re-renders its own
11
+ * stage in place on Next/Back — it never round-trips through the router for the step (the steps
12
+ * that OPEN a real surface — the structure tree, a flow, a node's panel — route normally). Every
13
+ * state treatment (the area dots, the colour key) is read from the one shared honest-state model.
14
+ *
15
+ * Browser globals only — reads the already-resolved PortalData; no network, no Node.
16
+ */
17
+ (function () {
18
+ 'use strict';
19
+
20
+ var Yg = (window.YgPortal = window.YgPortal || {});
21
+ var dom = Yg.dom;
22
+ Yg.views = Yg.views || {};
23
+
24
+ var STEP_TITLES = [
25
+ 'What this system is',
26
+ 'The big areas of this system',
27
+ 'One process, end to end',
28
+ 'One component, explained',
29
+ 'Reading the colours',
30
+ ];
31
+
32
+ /** Top-level component nodes (the "big areas") — derived from the graph, never hardcoded. */
33
+ function bigAreas(data) {
34
+ var nodes = data.nodes || [];
35
+ var top = nodes.filter(function (n) {
36
+ return !n.parent;
37
+ });
38
+ // If there is a single synthetic root, descend one level to the real areas.
39
+ if (top.length === 1) {
40
+ var rootPath = top[0].path;
41
+ top = nodes.filter(function (n) {
42
+ return n.parent === rootPath;
43
+ });
44
+ }
45
+ return top.slice(0, 9);
46
+ }
47
+
48
+ /**
49
+ * The honest area state — an area is a container, so its badge is the bottom-up
50
+ * rollupState (the worst REAL state anywhere in its subtree). rollupState is already
51
+ * honest for an empty container: with nothing verdict-bearing under it, the rollup is
52
+ * no-rule (unguarded, never green); a refused/unverified descendant bubbles up. We must
53
+ * NOT gate on the node's own `checked` here — that would suppress a real subtree state
54
+ * on a source-less container area (the exact green-over-empty defect).
55
+ */
56
+ function areaState(node) {
57
+ return node.rollupState;
58
+ }
59
+
60
+ /** A representative node to explain in step 4: the first checked node with relations, else any. */
61
+ function representativeNode(data) {
62
+ var nodes = data.nodes || [];
63
+ var withRules = nodes.filter(function (n) {
64
+ return n.checked && (n.effectiveAspects || []).length > 0;
65
+ });
66
+ return withRules[0] || nodes.filter(function (n) { return n.sourceFileCount > 0; })[0] || nodes[0] || null;
67
+ }
68
+
69
+ function stepRail(step) {
70
+ var rail = dom.el('div', 'st-steps');
71
+ for (var i = 0; i < 5; i += 1) {
72
+ var seg = dom.el('div', 'st-step' + (i < step ? ' st-step-done' : i === step ? ' st-step-now' : ''));
73
+ rail.appendChild(seg);
74
+ }
75
+ return rail;
76
+ }
77
+
78
+ function roadmapChips(step) {
79
+ var box = dom.el('div', 'st-roadmap');
80
+ for (var i = 0; i < 5; i += 1) {
81
+ var chip = dom.el('span', 'st-chip' + (i === step ? ' st-chip-now' : ''), (i + 1) + ' · ' + STEP_TITLES[i]);
82
+ box.appendChild(chip);
83
+ }
84
+ return box;
85
+ }
86
+
87
+ // ── Per-step bodies (each derived from live PortalData) ──────────────────────
88
+
89
+ function stepWhatIs(data) {
90
+ var c = data.meta.counts;
91
+ var body = dom.el('div');
92
+ body.appendChild(
93
+ dom.el(
94
+ 'p',
95
+ 'st-lead',
96
+ 'This project is guarded by continuous architecture checks. A set of rules describes how the code should be built; a reviewer checks the real code against them and records a verdict. Green means a reviewer actually checked something and it passed — nothing else is green.',
97
+ ),
98
+ );
99
+ var stats = dom.el('div', 'st-areas');
100
+ stats.appendChild(factCard(String(c.nodes), 'components mapped'));
101
+ stats.appendChild(factCard(String(c.aspects), 'rules in the book'));
102
+ stats.appendChild(factCard(String(c.flows), 'business processes'));
103
+ body.appendChild(stats);
104
+ body.appendChild(
105
+ dom.el('p', 'st-lead st-lead-sm', 'Right now ' + c.verified + ' of ' + c.pairsTotal + ' expected checks are verified. The rest are not failures by default — some are simply unchecked or unguarded. You will learn to read that difference by the end of this walk.'),
106
+ );
107
+ return body;
108
+ }
109
+
110
+ function factCard(big, label) {
111
+ var card = dom.el('div', 'st-area');
112
+ card.appendChild(dom.el('b', 'st-fact', big));
113
+ card.appendChild(dom.el('span', null, label));
114
+ return card;
115
+ }
116
+
117
+ function stepBigAreas(data, nav) {
118
+ var body = dom.el('div');
119
+ body.appendChild(dom.el('p', 'st-lead', 'This codebase is organized into a handful of areas. Each groups the files that do a related job — and each carries its own rules. Here is the map you will be working in.'));
120
+ var areas = bigAreas(data);
121
+ var grid = dom.el('div', 'st-areas');
122
+ for (var i = 0; i < areas.length; i += 1) {
123
+ grid.appendChild(areaCard(areas[i], nav));
124
+ }
125
+ body.appendChild(grid);
126
+ body.appendChild(
127
+ dom.el('p', 'st-lead st-lead-sm', 'The badge on each area is its state. Green means a reviewer checked it and it passed; the grey "no rule" badge means nothing checks it yet — not broken, just unguarded. You will learn the rest of the colours in the last step.'),
128
+ );
129
+ return body;
130
+ }
131
+
132
+ function areaCard(node, nav) {
133
+ var state = areaState(node);
134
+ var card = dom.el('button', 'st-area st-area-link');
135
+ card.type = 'button';
136
+ var top = dom.el('div', 'st-area-top');
137
+ top.appendChild(Yg.states.badge(state));
138
+ top.appendChild(dom.el('b', 'mono', node.name || node.path));
139
+ card.appendChild(top);
140
+ card.appendChild(dom.el('span', null, node.description ? shorten(node.description) : node.type));
141
+ card.addEventListener('click', function () {
142
+ nav({ view: 'tree', node: node.path });
143
+ });
144
+ return card;
145
+ }
146
+
147
+ function stepOneFlow(data, nav) {
148
+ var body = dom.el('div');
149
+ var flows = data.flows || [];
150
+ var flow = flows[0];
151
+ if (!flow) {
152
+ body.appendChild(dom.el('p', 'st-lead', 'No business processes are defined yet. When they are, this step walks one end to end.'));
153
+ return body;
154
+ }
155
+ body.appendChild(dom.el('p', 'st-lead', 'A "flow" is a real-world process that crosses several components. Here is one, named in plain business language, with the components it touches.'));
156
+ var card = dom.el('div', 'st-flowcard');
157
+ card.appendChild(dom.el('b', null, flow.name));
158
+ if (flow.description) card.appendChild(dom.el('p', 'st-flow-d', flow.description));
159
+ card.appendChild(dom.el('div', 'st-flow-meta', flow.participants.length + ' components take part. A flow is never just green: if nothing checks its components it reads "nothing-checked", and any unverified component makes the whole process need attention.'));
160
+ var open = dom.el('button', 'st-inline-link');
161
+ open.type = 'button';
162
+ open.textContent = 'See this process in full →';
163
+ open.addEventListener('click', function () {
164
+ nav({ view: 'flows', flow: flow.name });
165
+ });
166
+ card.appendChild(open);
167
+ body.appendChild(card);
168
+ return body;
169
+ }
170
+
171
+ function stepOneNode(data, nav) {
172
+ var body = dom.el('div');
173
+ var node = representativeNode(data);
174
+ if (!node) {
175
+ body.appendChild(dom.el('p', 'st-lead', 'No components are mapped yet. When they are, this step explains one in full.'));
176
+ return body;
177
+ }
178
+ body.appendChild(dom.el('p', 'st-lead', 'Each component has an identity, the rules in force on it, and what it depends on. Open one and you can read exactly what a reviewer checked — and what is still unguarded.'));
179
+ var card = dom.el('div', 'st-flowcard');
180
+ var top = dom.el('div', 'st-area-top');
181
+ // node.state is already the honest own state — a node with no real verdict-bearing
182
+ // pair is no-rule, never green (the backend `checked` gate handles that), so we render
183
+ // it directly rather than re-gating on `checked`.
184
+ top.appendChild(Yg.states.badge(node.state));
185
+ top.appendChild(dom.el('b', 'mono', node.name || node.path));
186
+ card.appendChild(top);
187
+ if (node.description) card.appendChild(dom.el('p', 'st-flow-d', shorten(node.description)));
188
+ var ruleCount = (node.effectiveAspects || []).length;
189
+ card.appendChild(dom.el('div', 'st-flow-meta', ruleCount > 0 ? ruleCount + ' rule(s) are in force here. Each carries its own honest verdict — verified, refused, or simply not-yet-checked.' : 'No rule is in force here yet — it is unguarded, not approved.'));
190
+ var open = dom.el('button', 'st-inline-link');
191
+ open.type = 'button';
192
+ open.textContent = 'Open this component →';
193
+ open.addEventListener('click', function () {
194
+ nav({ view: 'tree', node: node.path });
195
+ });
196
+ card.appendChild(open);
197
+ body.appendChild(card);
198
+ return body;
199
+ }
200
+
201
+ function stepColours() {
202
+ var body = dom.el('div');
203
+ body.appendChild(dom.el('p', 'st-lead', 'This is the whole key. Every colour means exactly one thing, and only one of them is green. Absence of red is not a pass — an unchecked or unguarded surface is its own distinct colour, never green.'));
204
+ var grid = dom.el('div', 'st-key');
205
+ for (var i = 0; i < Yg.states.ORDER.length; i += 1) {
206
+ var state = Yg.states.ORDER[i];
207
+ var item = dom.el('div', 'st-key-item ' + Yg.states.cssClass(state));
208
+ item.appendChild(Yg.states.badge(state));
209
+ var t = dom.el('div', 'st-key-text');
210
+ t.appendChild(dom.el('b', null, Yg.states.label(state)));
211
+ t.appendChild(dom.el('span', null, Yg.states.plain(state)));
212
+ item.appendChild(t);
213
+ grid.appendChild(item);
214
+ }
215
+ body.appendChild(grid);
216
+ return body;
217
+ }
218
+
219
+ function shorten(text) {
220
+ var t = String(text).replace(/\s+/g, ' ').trim();
221
+ return t.length > 90 ? t.slice(0, 87) + '…' : t;
222
+ }
223
+
224
+ function renderStep(stage, data, nav, step, setStep) {
225
+ dom.clear(stage);
226
+
227
+ stage.appendChild(stepRail(step));
228
+ stage.appendChild(dom.el('div', 'st-stepno mono', 'Step ' + (step + 1) + ' of 5'));
229
+
230
+ var card = dom.el('div', 'st-card');
231
+ card.appendChild(dom.el('h1', 'st-h1', STEP_TITLES[step]));
232
+
233
+ var bodies = [stepWhatIs(data), stepBigAreas(data, nav), stepOneFlow(data, nav), stepOneNode(data, nav), stepColours()];
234
+ card.appendChild(bodies[step]);
235
+
236
+ card.appendChild(roadmapChips(step));
237
+
238
+ var navRow = dom.el('div', 'st-nav');
239
+ var back = dom.el('button', 'st-btn');
240
+ back.type = 'button';
241
+ back.textContent = '← Back';
242
+ back.disabled = step === 0;
243
+ back.addEventListener('click', function () {
244
+ if (step > 0) setStep(step - 1);
245
+ });
246
+ navRow.appendChild(back);
247
+
248
+ var next = dom.el('button', 'st-btn st-btn-primary');
249
+ next.type = 'button';
250
+ if (step < 4) {
251
+ next.textContent = 'Next: ' + STEP_TITLES[step + 1].toLowerCase() + ' →';
252
+ next.addEventListener('click', function () {
253
+ setStep(step + 1);
254
+ });
255
+ } else {
256
+ next.textContent = 'Done — go to the overview';
257
+ next.addEventListener('click', function () {
258
+ nav({ view: 'overview' });
259
+ });
260
+ }
261
+ navRow.appendChild(next);
262
+
263
+ var skip = dom.el('button', 'st-skip');
264
+ skip.type = 'button';
265
+ skip.textContent = 'Skip the tour';
266
+ skip.addEventListener('click', function () {
267
+ nav({ view: 'overview' });
268
+ });
269
+ navRow.appendChild(skip);
270
+
271
+ card.appendChild(navRow);
272
+ stage.appendChild(card);
273
+ }
274
+
275
+ Yg.views.start = function (stage, route, data, ctx) {
276
+ var nav = ctx && ctx.navigate ? ctx.navigate : function () {};
277
+ var frame = dom.el('div', 'st-frame');
278
+ stage.appendChild(frame);
279
+
280
+ var step = 0;
281
+ function setStep(s) {
282
+ step = s;
283
+ renderStep(frame, data, nav, step, setStep);
284
+ }
285
+ renderStep(frame, data, nav, step, setStep);
286
+ };
287
+ })();
@@ -0,0 +1,187 @@
1
+ /*
2
+ * V8 Suppressions — the risk-first waiver inventory.
3
+ *
4
+ * Suppressed is NOT verified (§3.6, §3a V8): the permanent, surfaced ledger of every deliberate
5
+ * hole and which are dangerous. Every active waiver marker on mapped source as a table —
6
+ * file:line, the waived aspect, the reason text, and a RISK flag resolved by the CLI's own
7
+ * suppress-range resolver (not ad-hoc regex): wildcard (`*`, all aspects off), unbounded (runs
8
+ * to end of file / a long range), inert (the aspect is draft, so the waiver is a no-op), or typo
9
+ * (the aspect-id matches no known aspect). Sorted risk-first; a standing banner if any marker is
10
+ * wildcard or unbounded. A waiver NEVER hides the real verdict.
11
+ *
12
+ * The waived state is read from the one shared honest-state model (the `suppressed` treatment),
13
+ * so a waiver can never read as a green pass.
14
+ *
15
+ * Transitions (§3a V8): a marker's location → its node's attestation panel (SHELL-panel); the
16
+ * waived aspect → V5 (the rulebook). Browser globals only; reads the resolved PortalData; no Node.
17
+ */
18
+ (function () {
19
+ 'use strict';
20
+
21
+ var Yg = (window.YgPortal = window.YgPortal || {});
22
+ var dom = Yg.dom;
23
+ Yg.views = Yg.views || {};
24
+
25
+ // Risk → severity rank (lower = more dangerous, sorted first) + a plain label.
26
+ var RISK_RANK = { wildcard: 0, unbounded: 1, inert: 2, typo: 3 };
27
+ var RISK_LABEL = {
28
+ wildcard: 'WILDCARD — all aspects off',
29
+ unbounded: 'RANGE — runs unbounded',
30
+ inert: 'INERT — aspect is draft, waiver is a no-op',
31
+ typo: 'TYPO — aspect-id matches no known rule',
32
+ };
33
+
34
+ function riskRank(s) {
35
+ return s.risk && Object.prototype.hasOwnProperty.call(RISK_RANK, s.risk) ? RISK_RANK[s.risk] : 9;
36
+ }
37
+
38
+ /** A keyboard-operable export trigger (a native <button>, focusable + Enter/Space-activatable). */
39
+ function exportBtn(label, aria, onClick) {
40
+ var b = dom.el('button', 'exp-btn', label);
41
+ b.type = 'button';
42
+ b.setAttribute('aria-label', aria);
43
+ b.addEventListener('click', onClick);
44
+ return b;
45
+ }
46
+
47
+ /** Map the node owning a source file path, for the location → SHELL-panel transition. */
48
+ function nodeForFile(data, file) {
49
+ var nodes = data.nodes || [];
50
+ var best = null;
51
+ for (var i = 0; i < nodes.length; i += 1) {
52
+ var globs = nodes[i].mapping || [];
53
+ for (var g = 0; g < globs.length; g += 1) {
54
+ // A cheap, dependency-free containment: the node's mapping prefix (before any glob char)
55
+ // is a path prefix of the file. The exact owner is authoritative in the panel itself.
56
+ var prefix = String(globs[g]).split(/[*?[]/)[0];
57
+ if (prefix && file.indexOf(prefix) === 0) {
58
+ if (!best || prefix.length > best.prefix) best = { path: nodes[i].path, prefix: prefix.length };
59
+ }
60
+ }
61
+ }
62
+ return best ? best.path : null;
63
+ }
64
+
65
+ function riskCell(sup) {
66
+ if (!sup.risk) {
67
+ var ok = dom.el('span', 'sup-flag sup-flag-ok', 'bounded · single rule');
68
+ return ok;
69
+ }
70
+ var cls = sup.risk === 'wildcard' ? 'sup-flag-wild' : sup.risk === 'unbounded' ? 'sup-flag-unb' : 'sup-flag-other';
71
+ return dom.el('span', 'sup-flag ' + cls, RISK_LABEL[sup.risk] || sup.risk);
72
+ }
73
+
74
+ function markerRow(sup, data, nav) {
75
+ var tr = dom.el('tr', 'sup-row');
76
+
77
+ // Location — the waived state badge + file:line, routing to the owning node's panel.
78
+ var locCell = dom.el('td', 'sup-loc');
79
+ var locBtn = dom.el('button', 'sup-locbtn');
80
+ locBtn.type = 'button';
81
+ locBtn.appendChild(Yg.states.badge('suppressed'));
82
+ locBtn.appendChild(dom.el('span', 'mono sup-locpath', sup.file + ':' + sup.line));
83
+ var owner = nodeForFile(data, sup.file);
84
+ locBtn.addEventListener('click', function () {
85
+ if (owner) nav({ view: 'tree', node: owner });
86
+ });
87
+ if (!owner) locBtn.disabled = true;
88
+ locCell.appendChild(locBtn);
89
+ tr.appendChild(locCell);
90
+
91
+ // The waived aspect — routes to its detail in the rulebook.
92
+ var aspCell = dom.el('td');
93
+ var aspBtn = dom.el('button', 'sup-asp mono');
94
+ aspBtn.type = 'button';
95
+ aspBtn.textContent = sup.aspectId;
96
+ aspBtn.addEventListener('click', function () {
97
+ nav({ view: 'rulebook', aspect: sup.aspectId });
98
+ });
99
+ aspCell.appendChild(aspBtn);
100
+ tr.appendChild(aspCell);
101
+
102
+ tr.appendChild(dom.el('td', null, riskCell(sup)));
103
+
104
+ var reasonCell = dom.el('td', 'sup-reason');
105
+ reasonCell.textContent = '"' + (sup.reason || '') + '" — a waiver, not a pass.';
106
+ tr.appendChild(reasonCell);
107
+
108
+ return tr;
109
+ }
110
+
111
+ function banner(suppressions) {
112
+ var dangerous = suppressions.filter(function (s) {
113
+ return s.risk === 'wildcard' || s.risk === 'unbounded';
114
+ });
115
+ if (!dangerous.length) return null;
116
+ var wild = dangerous.filter(function (s) {
117
+ return s.risk === 'wildcard';
118
+ }).length;
119
+ var unb = dangerous.length - wild;
120
+ var msg = '';
121
+ if (wild) msg += wild + ' wildcard marker' + (wild === 1 ? '' : 's') + ' (waives every rule on that line) ';
122
+ if (unb) msg += (msg ? '· ' : '') + unb + ' unbounded range' + (unb === 1 ? '' : 's') + ' ';
123
+ var box = dom.el('div', 'sup-banner');
124
+ box.appendChild(Yg.states.badge('suppressed'));
125
+ box.appendChild(dom.el('span', null, msg + '— review whether each is intended.'));
126
+ return box;
127
+ }
128
+
129
+ function summary(suppressions) {
130
+ var files = {};
131
+ for (var i = 0; i < suppressions.length; i += 1) files[suppressions[i].file] = true;
132
+ var fileCount = Object.keys(files).length;
133
+ return suppressions.length + ' active waiver' + (suppressions.length === 1 ? '' : 's') + ' across ' + fileCount + ' file' + (fileCount === 1 ? '' : 's') + ' — a suppressed line is NOT verified; the reviewer was told to skip it. Sorted risk-first.';
134
+ }
135
+
136
+ Yg.views.suppressions = function (stage, route, data, ctx) {
137
+ var nav = ctx && ctx.navigate ? ctx.navigate : function () {};
138
+ var suppressions = (data.suppressions || []).slice().sort(function (a, b) {
139
+ var r = riskRank(a) - riskRank(b);
140
+ if (r !== 0) return r;
141
+ return (a.file + ':' + a.line).localeCompare(b.file + ':' + b.line, 'en');
142
+ });
143
+
144
+ stage.appendChild(dom.el('p', 'view-lead', 'Every deliberate waiver, risk-first. A waived line is not verified — someone told the reviewer to skip a rule there, on purpose, with a reason. A waiver never hides the real verdict; it is a documented hole, not a pass.'));
145
+
146
+ // Export the inventory as a portable audit artifact (CSV / JSON) — built in-page, no network.
147
+ if (Yg.exporter) {
148
+ var bar = dom.el('div', 'exp-bar');
149
+ bar.appendChild(exportBtn('Export CSV', 'Download the suppression inventory as CSV', function () {
150
+ Yg.exporter.exportSuppressionsCsv(data);
151
+ }));
152
+ bar.appendChild(exportBtn('Export JSON', 'Download the full audit bundle (suppressions, residue, coverage) as JSON', function () {
153
+ Yg.exporter.exportJson(data);
154
+ }));
155
+ stage.appendChild(bar);
156
+ }
157
+
158
+ if (!suppressions.length) {
159
+ var empty = dom.el('div', 'sup-empty');
160
+ empty.appendChild(Yg.states.badge('suppressed'));
161
+ empty.appendChild(dom.el('b', null, 'No active waivers.'));
162
+ empty.appendChild(dom.el('p', null, 'Nothing is being skipped on current inputs. That is the honest empty state — not a green, just an empty ledger.'));
163
+ stage.appendChild(empty);
164
+ return;
165
+ }
166
+
167
+ stage.appendChild(dom.el('div', 'rb-sub', summary(suppressions)));
168
+ var b = banner(suppressions);
169
+ if (b) stage.appendChild(b);
170
+
171
+ var table = dom.el('table', 'sup-table');
172
+ var thead = dom.el('thead');
173
+ var htr = dom.el('tr');
174
+ ['location', 'waived rule', 'risk', 'reason'].forEach(function (h) {
175
+ htr.appendChild(dom.el('td', null, h));
176
+ });
177
+ thead.appendChild(htr);
178
+ table.appendChild(thead);
179
+
180
+ var tbody = dom.el('tbody');
181
+ for (var i = 0; i < suppressions.length; i += 1) {
182
+ tbody.appendChild(markerRow(suppressions[i], data, nav));
183
+ }
184
+ table.appendChild(tbody);
185
+ stage.appendChild(table);
186
+ };
187
+ })();
@@ -0,0 +1,68 @@
1
+ /*
2
+ * V3 Structure — the node tree view.
3
+ *
4
+ * The where-does-X-live spine (§3a V3). It reuses the shared DOM-virtualized node tree
5
+ * (Yg.tree) so the hierarchy stays responsive at 300–1000+ nodes, colors each row by its
6
+ * own honest state (read from the one shared honest-state model — never an invented green),
7
+ * and routes a row click to that node's attestation panel via the §3a "tree row → SHELL-panel"
8
+ * transition. A short honest preface states what the coloring means so the absence of red is
9
+ * never mistaken for a pass.
10
+ *
11
+ * Browser globals only — reads the already-resolved PortalData; no network, no Node.
12
+ */
13
+ (function () {
14
+ 'use strict';
15
+
16
+ var Yg = (window.YgPortal = window.YgPortal || {});
17
+ var dom = Yg.dom;
18
+ Yg.views = Yg.views || {};
19
+
20
+ /**
21
+ * Render the structure tree into `stage`. `ctx.onSelect(path)` routes a row to the node's
22
+ * attestation panel (§3a: tree row → SHELL-panel). The shared count header + honest legend
23
+ * are rendered by the dispatcher around this body.
24
+ */
25
+ Yg.views.tree = function (stage, route, data, ctx) {
26
+ var intro = dom.el('p', 'view-lead');
27
+ intro.appendChild(
28
+ document.createTextNode('The component hierarchy. Each row carries its '),
29
+ );
30
+ var ownState = Yg.glossary ? Yg.glossary.term('verified', 'own state') : dom.el('span', null, 'own state');
31
+ intro.appendChild(ownState);
32
+ intro.appendChild(
33
+ document.createTextNode(
34
+ ' — kept separate from a roll-up over its children, so one refused leaf never falsely reddens an ancestor. Click a row to open it.',
35
+ ),
36
+ );
37
+ stage.appendChild(intro);
38
+
39
+ // In-tree filter: at hundreds of nodes, "where does X live" needs a filter that prunes the
40
+ // hierarchy to matches (plus their ancestors) in place — the ⌘K palette only jumps to a panel.
41
+ var filterWrap = dom.el('div', 'tree-filter');
42
+ var filterInput = dom.el('input', 'tree-filter-input');
43
+ filterInput.type = 'search';
44
+ filterInput.placeholder = 'Filter components by name or path…';
45
+ filterInput.setAttribute('aria-label', 'Filter the structure tree by component name or path');
46
+ filterWrap.appendChild(filterInput);
47
+ stage.appendChild(filterWrap);
48
+
49
+ var mount = dom.el('section', 'tree-mount');
50
+ stage.appendChild(mount);
51
+ var controller = Yg.tree.render(mount, data, function (path) {
52
+ if (ctx && ctx.onSelect) ctx.onSelect(path);
53
+ });
54
+
55
+ var debounce;
56
+ filterInput.addEventListener('input', function () {
57
+ var v = filterInput.value;
58
+ if (debounce) clearTimeout(debounce);
59
+ debounce = setTimeout(function () {
60
+ if (controller && controller.filter) controller.filter(v);
61
+ }, 120);
62
+ });
63
+
64
+ // Reveal-on-select: a palette pick or a deep-link to a node scrolls the tree to it and marks
65
+ // it, so the user lands ON the node in the hierarchy, not only in the side panel.
66
+ if (route && route.node && controller && controller.reveal) controller.reveal(route.node);
67
+ };
68
+ })();