@chrisdudek/yg 5.3.0 → 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 +1860 -373
  2. package/dist/structure.d.ts +7 -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,190 @@
1
+ /*
2
+ * Node tree — a DOM-virtualized, honest-state hierarchy.
3
+ *
4
+ * Builds the nested parent/child tree from the flat PortalNode[] (parent links), lays it
5
+ * out with the vendored d3-hierarchy tidy-tree so depth/order are real (not hand-rolled),
6
+ * and renders it as a DOM list — accessible, find-in-page, keyboard-reachable. To stay
7
+ * responsive on a large real graph (300–1000+ nodes) the list is VIRTUALIZED: only the
8
+ * rows in (and a small overscan around) the scroll viewport are in the DOM; a spacer holds
9
+ * the full scroll height so the scrollbar is honest.
10
+ *
11
+ * buildForest / flattenLayout are PURE over plain data (no DOM) so the tree shape is
12
+ * unit-testable; the virtualization is the thin DOM layer. Browser globals only — the
13
+ * vendored d3 layout is the only dependency, already on the page. No network, no Node.
14
+ */
15
+ (function () {
16
+ 'use strict';
17
+
18
+ var Yg = (window.YgPortal = window.YgPortal || {});
19
+ var dom = Yg.dom;
20
+ var ROW_H = 28; // px per row (matches the token grid)
21
+ var OVERSCAN = 8;
22
+
23
+ /**
24
+ * Build a nested {node, children} forest from the flat PortalNode[] via parent links,
25
+ * under one synthetic root so a multi-root graph lays out as a single tree. Children are
26
+ * kept in input order (the pipeline emits a stable order). Pure — returns plain objects.
27
+ */
28
+ function buildForest(nodes) {
29
+ var byPath = {};
30
+ var list = nodes || [];
31
+ for (var i = 0; i < list.length; i += 1) {
32
+ byPath[list[i].path] = { node: list[i], children: [] };
33
+ }
34
+ var roots = [];
35
+ for (var j = 0; j < list.length; j += 1) {
36
+ var n = list[j];
37
+ var entry = byPath[n.path];
38
+ var parent = n.parent ? byPath[n.parent] : null;
39
+ if (parent) parent.children.push(entry);
40
+ else roots.push(entry);
41
+ }
42
+ return { node: { path: '', name: 'graph', type: 'root', state: 'no-rule' }, children: roots };
43
+ }
44
+
45
+ /**
46
+ * Flatten the forest into an ordered, depth-tagged row list using the vendored
47
+ * d3-hierarchy layout when present (real tidy-tree order), falling back to a plain
48
+ * depth-first walk otherwise. Each row is { node, depth }. Pure over the forest + the
49
+ * (optional) injected d3 — testable by passing a d3 stub or none.
50
+ */
51
+ function flattenLayout(forest, d3) {
52
+ var rows = [];
53
+ if (d3 && d3.hierarchy && d3.tree) {
54
+ var h = d3.hierarchy(forest);
55
+ d3.tree().nodeSize([1, 1])(h);
56
+ h.eachBefore(function (d) {
57
+ if (!d.data.node.path) return; // skip synthetic root
58
+ rows.push({ node: d.data.node, depth: d.depth - 1 });
59
+ });
60
+ return rows;
61
+ }
62
+ walk(forest, -1, rows);
63
+ return rows;
64
+ }
65
+
66
+ function walk(entry, depth, out) {
67
+ if (entry.node.path) out.push({ node: entry.node, depth: depth });
68
+ for (var i = 0; i < entry.children.length; i += 1) walk(entry.children[i], depth + 1, out);
69
+ }
70
+
71
+ /**
72
+ * Render a virtualized tree into `mount` from `data`, calling `onSelect(path)` when a row
73
+ * is activated (click / Enter). Returns a controller exposing `destroy()`. Only the visible
74
+ * window of rows is materialized; scrolling re-materializes the window — so a 1000-node
75
+ * graph paints a couple dozen rows, not a thousand.
76
+ */
77
+ function render(mount, data, onSelect) {
78
+ var d3 = window.d3;
79
+ var nodes = data.nodes || [];
80
+ var allRows = flattenLayout(buildForest(nodes), d3);
81
+ var rows = allRows;
82
+ var revealPath = null;
83
+
84
+ // path -> parent path, so a filter can keep a match's ancestors (its place in the hierarchy).
85
+ var parentOf = {};
86
+ for (var pi = 0; pi < nodes.length; pi += 1) parentOf[nodes[pi].path] = nodes[pi].parent || null;
87
+
88
+ var scroller = dom.el('div', 'tree-scroller');
89
+ scroller.setAttribute('role', 'tree');
90
+ var spacer = dom.el('div', 'tree-spacer');
91
+ spacer.style.height = rows.length * ROW_H + 'px';
92
+ var win = dom.el('div', 'tree-window');
93
+ spacer.appendChild(win);
94
+ scroller.appendChild(spacer);
95
+ mount.appendChild(scroller);
96
+
97
+ function paint() {
98
+ var top = scroller.scrollTop;
99
+ var height = scroller.clientHeight || ROW_H * 24;
100
+ var first = Math.max(0, Math.floor(top / ROW_H) - OVERSCAN);
101
+ var last = Math.min(rows.length, Math.ceil((top + height) / ROW_H) + OVERSCAN);
102
+ dom.clear(win);
103
+ win.style.transform = 'translateY(' + first * ROW_H + 'px)';
104
+ for (var i = first; i < last; i += 1) win.appendChild(buildRow(rows[i], onSelect, revealPath));
105
+ }
106
+
107
+ scroller.addEventListener('scroll', paint);
108
+ paint();
109
+
110
+ /** Prune the tree to rows matching `q` (by name or path) plus their ancestors; empty = all. */
111
+ function filter(q) {
112
+ var query = String(q || '').trim().toLowerCase();
113
+ if (!query) {
114
+ rows = allRows;
115
+ } else {
116
+ var keep = {};
117
+ for (var i = 0; i < allRows.length; i += 1) {
118
+ var nd = allRows[i].node;
119
+ if ((nd.name || '').toLowerCase().indexOf(query) >= 0 || (nd.path || '').toLowerCase().indexOf(query) >= 0) {
120
+ keep[nd.path] = true;
121
+ var p = parentOf[nd.path];
122
+ while (p) {
123
+ keep[p] = true;
124
+ p = parentOf[p];
125
+ }
126
+ }
127
+ }
128
+ rows = allRows.filter(function (r) {
129
+ return keep[r.node.path];
130
+ });
131
+ }
132
+ spacer.style.height = rows.length * ROW_H + 'px';
133
+ scroller.scrollTop = 0;
134
+ paint();
135
+ }
136
+
137
+ /** Scroll the tree to `path` (in the current row set) and mark it, so a palette pick / deep
138
+ link lands the user ON the node in the hierarchy, not only in the side panel. */
139
+ function reveal(path) {
140
+ var idx = -1;
141
+ for (var i = 0; i < rows.length; i += 1) {
142
+ if (rows[i].node.path === path) {
143
+ idx = i;
144
+ break;
145
+ }
146
+ }
147
+ if (idx < 0) return;
148
+ revealPath = path;
149
+ var height = scroller.clientHeight || ROW_H * 24;
150
+ scroller.scrollTop = Math.max(0, idx * ROW_H - Math.floor(height / 2) + ROW_H);
151
+ paint();
152
+ }
153
+
154
+ return {
155
+ rowCount: allRows.length,
156
+ filter: filter,
157
+ reveal: reveal,
158
+ destroy: function () {
159
+ scroller.removeEventListener('scroll', paint);
160
+ if (scroller.parentNode) scroller.parentNode.removeChild(scroller);
161
+ },
162
+ };
163
+ }
164
+
165
+ function buildRow(row, onSelect, revealPath) {
166
+ var n = row.node;
167
+ var li = dom.el('div', 'tree-row ' + Yg.states.cssClass(n.state));
168
+ li.setAttribute('role', 'treeitem');
169
+ li.setAttribute('tabindex', '0');
170
+ li.setAttribute('data-path', n.path);
171
+ if (revealPath && n.path === revealPath) li.classList.add('tree-row-revealed');
172
+ li.style.paddingLeft = 12 + row.depth * 16 + 'px';
173
+ li.appendChild(Yg.states.badge(n.state));
174
+ li.appendChild(dom.el('span', 'tree-name', n.name || n.path));
175
+ li.appendChild(dom.el('span', 'tree-type', n.type || ''));
176
+ function activate() {
177
+ if (onSelect) onSelect(n.path);
178
+ }
179
+ li.addEventListener('click', activate);
180
+ li.addEventListener('keydown', function (e) {
181
+ if (e.key === 'Enter' || e.key === ' ') {
182
+ activate();
183
+ e.preventDefault();
184
+ }
185
+ });
186
+ return li;
187
+ }
188
+
189
+ Yg.tree = { buildForest: buildForest, flattenLayout: flattenLayout, render: render, ROW_H: ROW_H };
190
+ })();
@@ -0,0 +1,247 @@
1
+ /*
2
+ * V2 Coverage & Audit — the full Coverage-Truth Ledger.
3
+ *
4
+ * The precise, honest audit (§3a V2, §3.1): a 100%-width verdict bar over the full
5
+ * expected-pair universe (verified — sub-split billed-LLM vs free-deterministic — / refused /
6
+ * unverified, each color + glyph + label + exact count); a hairline-separated NON-PAIR track
7
+ * (no-rule / draft / not-applicable) that is counted but structurally barred from the
8
+ * coverage fraction; LIVE-badged counters (boundary / validator / log-missing) that equal what
9
+ * yg check enforces; a provenance line; the rule-grouped needs-attention worklist in the
10
+ * honesty-priority order the pipeline already sorted; and a jump-to-next-unresolved that, on
11
+ * an empty worklist, repoints to the top residue item rather than dead-ending. Every count is
12
+ * read from the live PortalData (== yg check); nothing here is a literal, nothing collapses to
13
+ * green.
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
+ /** A bar segment with a non-zero width (collapses to nothing at 0 so the bar stays honest). */
25
+ function barSeg(cls, flex, label) {
26
+ if (flex <= 0) return null;
27
+ var seg = dom.el('div', 'cov-seg ' + cls, label);
28
+ seg.style.flex = String(flex);
29
+ return seg;
30
+ }
31
+
32
+ /** A labelled count key: glyph + count + plain label, all from the shared state model. */
33
+ function key(state, count, suffix) {
34
+ var k = dom.el('span', 'cov-key');
35
+ k.appendChild(Yg.states.badge(state));
36
+ k.appendChild(dom.el('b', null, String(count)));
37
+ k.appendChild(dom.el('span', 'cov-key-lbl', Yg.states.label(state)));
38
+ if (suffix) k.appendChild(dom.el('span', 'cov-key-sub', suffix));
39
+ return k;
40
+ }
41
+
42
+ /**
43
+ * A LIVE-badged counter. `value` is a number read from the live data, or the string 'UNKNOWN'
44
+ * when the underlying check could not run — never a fabricated zero. An explicit number is
45
+ * always rendered (no hidden row); an optional `onClick` routes the chip.
46
+ */
47
+ function liveChip(value, label, onClick) {
48
+ var unknown = value === 'UNKNOWN';
49
+ var chip = dom.el(onClick ? 'button' : 'div', 'cov-live' + (onClick ? ' cov-live-btn' : ''));
50
+ if (onClick) {
51
+ chip.type = 'button';
52
+ chip.addEventListener('click', onClick);
53
+ }
54
+ chip.appendChild(dom.el('span', 'cov-livebadge', 'LIVE'));
55
+ chip.appendChild(dom.el('b', null, String(value)));
56
+ chip.appendChild(dom.el('span', null, label));
57
+ if (unknown) chip.appendChild(dom.el('span', 'cov-key-sub', 'check could not run — not clean, not zero'));
58
+ else if (value === 0) chip.appendChild(dom.el('span', 'cov-key-sub', 'none on current inputs'));
59
+ return chip;
60
+ }
61
+
62
+ function renderBar(stage, data, ctx) {
63
+ var c = data.meta.counts;
64
+ var nav = ctx && ctx.navigate ? ctx.navigate : function () {};
65
+ var boundary = data.boundary || { unknown: false, phantom: [], forbiddenType: [] };
66
+ var ledger = dom.el('div', 'cov-ledger');
67
+
68
+ var head = dom.el('div', 'cov-lhead');
69
+ var frac = dom.el('span', 'cov-frac', c.verified + ' ');
70
+ frac.appendChild(dom.el('span', 'cov-den', '/ ' + c.pairsTotal));
71
+ head.appendChild(frac);
72
+ head.appendChild(dom.el('span', 'cov-lbl', 'expected verdict pairs verified'));
73
+ head.appendChild(dom.el('span', 'cov-right', c.nodes + ' nodes · ' + c.aspects + ' aspects · ' + c.flows + ' flows'));
74
+ ledger.appendChild(head);
75
+
76
+ // The bar is sized by the real pair STATES (verified / refused / unverified), never by the
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.
80
+ // Advisory refusals are a real expected-pair state, but per the honesty model they render
81
+ // as a NON-BLOCKING warning, never a blocking `refused`. They get their own warning-coloured
82
+ // segment + key so the bar still accounts for every expected pair without ever showing an
83
+ // advisory refusal as a blocking red. `refused` here is ENFORCED refusals only (== yg check).
84
+ var advisoryRefused = c.advisoryRefused || 0;
85
+ var bar = dom.el('div', 'cov-bar');
86
+ bar.setAttribute('role', 'group');
87
+ bar.setAttribute('aria-label', 'verified ' + c.verified + ' of ' + c.pairsTotal + ' expected pairs');
88
+ var segs = [
89
+ barSeg('cov-seg-v', c.verified, c.verified > 0 ? c.verified + ' verified' : ''),
90
+ barSeg('cov-seg-r', c.refused, ''),
91
+ barSeg(Yg.states.cssClass('warning'), advisoryRefused, ''),
92
+ barSeg('cov-seg-u', c.unverified, ''),
93
+ ];
94
+ for (var i = 0; i < segs.length; i += 1) if (segs[i]) bar.appendChild(segs[i]);
95
+ if (!bar.firstChild) bar.appendChild(dom.el('div', 'cov-seg cov-seg-empty', 'no expected pairs'));
96
+ ledger.appendChild(bar);
97
+
98
+ var labels = dom.el('div', 'cov-barlabels');
99
+ labels.appendChild(key('verified', c.verified, 'of ' + c.pairsLLM + ' LLM + ' + c.pairsDet + ' deterministic expected'));
100
+ labels.appendChild(key('refused', c.refused, 'enforced — blocks (== yg check)'));
101
+ if (advisoryRefused > 0) labels.appendChild(key('warning', advisoryRefused, 'advisory refusal — does not block'));
102
+ labels.appendChild(key('unverified', c.unverified));
103
+ ledger.appendChild(labels);
104
+
105
+ // The separated non-pair track — counted, shown, barred from the coverage fraction.
106
+ ledger.appendChild(dom.el('div', 'cov-hair'));
107
+ var nonpair = dom.el('div', 'cov-nonpair');
108
+ nonpair.appendChild(dom.el('span', 'cov-nptag', 'not in coverage fraction:'));
109
+ nonpair.appendChild(key('no-rule', c.noRule, 'own source'));
110
+ nonpair.appendChild(key('draft', c.draft));
111
+ nonpair.appendChild(key('not-applicable', c.notApplicable));
112
+ ledger.appendChild(nonpair);
113
+
114
+ // LIVE counters — read from the live data, never a fabricated zero. The boundary count is
115
+ // the real undeclared + forbidden-type violation total (declared-only is legitimate, never
116
+ // counted), or UNKNOWN when the live relation parse could not run; it routes to V4. The
117
+ // blocking-errors count is the live yg-check error total.
118
+ var boundaryValue = boundary.unknown
119
+ ? 'UNKNOWN'
120
+ : (boundary.phantom || []).length + (boundary.forbiddenType || []).length;
121
+ var live = dom.el('div', 'cov-livewrap');
122
+ live.appendChild(
123
+ liveChip(boundaryValue, 'boundary violations', function () {
124
+ nav({ view: 'relations' });
125
+ }),
126
+ );
127
+ live.appendChild(liveChip(c.errors, 'blocking errors (== yg check)', undefined));
128
+ ledger.appendChild(live);
129
+
130
+ ledger.appendChild(
131
+ dom.el(
132
+ 'p',
133
+ 'cov-prov',
134
+ 'Lock read at generation. Deterministic checks and the relation / architecture / mapping / strict-coverage validators are re-run live at generation; the deterministic cache is never trusted. Counts equal what yg check enforces.',
135
+ ),
136
+ );
137
+ stage.appendChild(ledger);
138
+ }
139
+
140
+ function renderWorklist(stage, data, ctx) {
141
+ var nav = ctx && ctx.navigate ? ctx.navigate : function () {};
142
+ var worklist = data.worklist || [];
143
+
144
+ var title = dom.el('div', 'cov-section');
145
+ title.appendChild(dom.el('span', null, 'Needs attention'));
146
+ title.appendChild(dom.el('span', 'cov-section-count', '(' + worklist.length + ')'));
147
+ var jump = dom.el('button', 'cov-jump');
148
+ jump.type = 'button';
149
+ if (worklist.length > 0) {
150
+ jump.textContent = 'Jump to next unresolved →';
151
+ jump.addEventListener('click', function () {
152
+ var first = worklist[0];
153
+ if (first && first.nodes && first.nodes[0]) nav({ view: 'tree', node: first.nodes[0] });
154
+ });
155
+ } else {
156
+ jump.textContent = 'All clear — view the residue →';
157
+ jump.classList.add('cov-jump-residue');
158
+ jump.addEventListener('click', function () {
159
+ nav({ view: 'suppressions' });
160
+ });
161
+ }
162
+ title.appendChild(jump);
163
+ stage.appendChild(title);
164
+
165
+ if (worklist.length === 0) {
166
+ var calm = dom.el('div', 'cov-calm');
167
+ calm.appendChild(dom.el('p', null, 'No refusals and nothing unverified on current inputs. Absence of red is not a pass — the residue above (no-rule nodes, unmapped files, waivers) is still worth a look.'));
168
+ stage.appendChild(calm);
169
+ return;
170
+ }
171
+
172
+ var card = dom.el('div', 'cov-card');
173
+ for (var i = 0; i < worklist.length; i += 1) {
174
+ card.appendChild(worklistRow(worklist[i], nav));
175
+ }
176
+ stage.appendChild(card);
177
+ }
178
+
179
+ function worklistRow(group, nav) {
180
+ var sevState = group.severity === 'error' ? (group.rule === 'unverified' ? 'unverified' : 'refused') : 'warning';
181
+ var row = dom.el('div', 'cov-worow');
182
+ var pill = dom.el('span', 'cov-pill ' + Yg.states.cssClass(sevState));
183
+ pill.appendChild(Yg.states.badge(sevState));
184
+ pill.appendChild(dom.el('span', null, group.severity));
185
+ row.appendChild(pill);
186
+
187
+ var id = dom.el('span', 'cov-worow-id');
188
+ id.appendChild(dom.el('b', 'mono', group.rule));
189
+ id.appendChild(dom.el('span', 'cov-worow-reason', group.why));
190
+ row.appendChild(id);
191
+
192
+ var meta = dom.el('span', 'cov-worow-meta');
193
+ meta.appendChild(dom.el('span', null, group.nodes.length + (group.nodes.length === 1 ? ' node' : ' nodes')));
194
+ var link = dom.el('button', 'cov-deeplink');
195
+ link.type = 'button';
196
+ link.textContent = 'open →';
197
+ link.addEventListener('click', function () {
198
+ if (group.nodes[0]) nav({ view: 'tree', node: group.nodes[0] });
199
+ });
200
+ meta.appendChild(link);
201
+ row.appendChild(meta);
202
+
203
+ var ruleHdr = dom.el('button', 'cov-rulehdr');
204
+ ruleHdr.type = 'button';
205
+ ruleHdr.setAttribute('aria-label', 'open rule ' + group.rule);
206
+ ruleHdr.textContent = '› fix: ' + group.fix;
207
+ ruleHdr.addEventListener('click', function () {
208
+ nav({ view: 'rulebook', aspect: group.rule });
209
+ });
210
+ var wrap = dom.el('div', 'cov-worow-wrap');
211
+ wrap.appendChild(row);
212
+ wrap.appendChild(ruleHdr);
213
+ return wrap;
214
+ }
215
+
216
+ /** A keyboard-operable export trigger (a native <button>, focusable + Enter/Space-activatable). */
217
+ function exportBtn(label, aria, onClick) {
218
+ var b = dom.el('button', 'exp-btn', label);
219
+ b.type = 'button';
220
+ b.setAttribute('aria-label', aria);
221
+ b.addEventListener('click', onClick);
222
+ return b;
223
+ }
224
+
225
+ /** The portable-artifact export bar (CSV of the coverage summary + the no-rule residue, JSON bundle). */
226
+ function renderExport(stage, data) {
227
+ if (!Yg.exporter) return;
228
+ var bar = dom.el('div', 'exp-bar');
229
+ bar.appendChild(dom.el('span', 'exp-lbl', 'Export the audit (in-page, no network):'));
230
+ bar.appendChild(exportBtn('Coverage CSV', 'Download the coverage summary as CSV', function () {
231
+ Yg.exporter.exportCoverageCsv(data);
232
+ }));
233
+ bar.appendChild(exportBtn('Residue CSV', 'Download the no-rule nodes and unmapped files as CSV', function () {
234
+ Yg.exporter.exportResidueCsv(data);
235
+ }));
236
+ bar.appendChild(exportBtn('JSON bundle', 'Download the full audit bundle (coverage, residue, suppressions) as JSON', function () {
237
+ Yg.exporter.exportJson(data);
238
+ }));
239
+ stage.appendChild(bar);
240
+ }
241
+
242
+ Yg.views.coverage = function (stage, route, data, ctx) {
243
+ renderBar(stage, data, ctx);
244
+ renderExport(stage, data);
245
+ renderWorklist(stage, data, ctx);
246
+ };
247
+ })();
@@ -0,0 +1,204 @@
1
+ /*
2
+ * V7 Flows — the business-language lens.
3
+ *
4
+ * The only business-language artifact (§3.7, §3a V7): the bridge for a lead or director who
5
+ * never reads code. A gallery of flows by name + description; selecting one shows its detail —
6
+ * participants (declared PLUS auto-expanded descendants, each marked), the flow-level aspects,
7
+ * and each participant's honest verdict state.
8
+ *
9
+ * CRITICAL HONESTY — a flow state has THREE values, never two, and is NEVER green merely
10
+ * because nothing was checked:
11
+ * - verified — every participant carrying a rule is verified.
12
+ * - attention — any participant pair is refused or unverified (the weakest link).
13
+ * - nothing-checked — no participant carries a rule at all; a flow can never vanity-green on
14
+ * the absence of any rule. Rendered in the distinct no-rule treatment.
15
+ * Every state treatment is read from the one shared honest-state model.
16
+ *
17
+ * Transitions (§3a V7): a flow card → select it (in-view, round-trips via the hash); a
18
+ * participant → its attestation panel (SHELL-panel); a flow aspect → V5 (the rulebook).
19
+ *
20
+ * Browser globals only — reads the already-resolved PortalData; no network, no Node.
21
+ */
22
+ (function () {
23
+ 'use strict';
24
+
25
+ var Yg = (window.YgPortal = window.YgPortal || {});
26
+ var dom = Yg.dom;
27
+ Yg.views = Yg.views || {};
28
+
29
+ // The flow state → the honest render state it maps to. 'attention' is signal (warning),
30
+ // 'nothing-checked' is the distinct no-rule treatment — NEVER verified unless truly all-green.
31
+ var FLOW_RENDER_STATE = {
32
+ verified: 'verified',
33
+ attention: 'warning',
34
+ 'nothing-checked': 'no-rule',
35
+ };
36
+
37
+ // The plain label shown on a flow state pill (the engine's word, said for a human).
38
+ var FLOW_LABEL = {
39
+ verified: 'verified',
40
+ attention: 'weakest-link',
41
+ 'nothing-checked': 'nothing-checked',
42
+ };
43
+
44
+ function flowRenderState(flow) {
45
+ return FLOW_RENDER_STATE[flow.state] || 'no-rule';
46
+ }
47
+
48
+ /** A flow state pill — its color/border comes from the shared honest-state model. */
49
+ function statePill(flow) {
50
+ var rs = flowRenderState(flow);
51
+ var pill = dom.el('span', 'fl-state ' + Yg.states.cssClass(rs));
52
+ pill.appendChild(dom.el('span', null, FLOW_LABEL[flow.state] || flow.state));
53
+ return pill;
54
+ }
55
+
56
+ function gallery(flows, selectedName, nav) {
57
+ var col = dom.el('div', 'fl-gallery');
58
+ for (var i = 0; i < flows.length; i += 1) {
59
+ col.appendChild(flowCard(flows[i], flows[i].name === selectedName, nav));
60
+ }
61
+ return col;
62
+ }
63
+
64
+ function flowCard(flow, selected, nav) {
65
+ var card = dom.el('button', 'fl-card' + (selected ? ' fl-card-sel' : ''));
66
+ card.type = 'button';
67
+ var top = dom.el('div', 'fl-card-top');
68
+ top.appendChild(dom.el('b', null, flow.name));
69
+ top.appendChild(statePill(flow));
70
+ card.appendChild(top);
71
+ if (flow.description) card.appendChild(dom.el('div', 'fl-card-d', flow.description));
72
+ card.addEventListener('click', function () {
73
+ nav({ view: 'flows', flow: flow.name });
74
+ });
75
+ return card;
76
+ }
77
+
78
+ /** Map a node's own state honestly — node.state is already no-rule for an unchecked
79
+ * node (no real verdict-bearing pair), never green, so we read it directly. */
80
+ function nodeState(data, path) {
81
+ var node = (data.nodes || []).filter(function (n) {
82
+ return n.path === path;
83
+ })[0];
84
+ if (!node) return 'no-rule';
85
+ return node.state;
86
+ }
87
+
88
+ /** Was a participant declared on the flow, or auto-included as a descendant? */
89
+ function isDeclared(flow, path, data) {
90
+ // A participant is "declared" if it equals a declared node; descendants are auto-included.
91
+ // The contract carries the expanded set only, so we infer: a participant whose parent is
92
+ // also a participant is a descendant; a participant with no participant-parent is declared.
93
+ var node = (data.nodes || []).filter(function (n) {
94
+ return n.path === path;
95
+ })[0];
96
+ if (!node || !node.parent) return true;
97
+ return flow.participants.indexOf(node.parent) === -1;
98
+ }
99
+
100
+ function participantRow(flow, path, data, nav) {
101
+ var declared = isDeclared(flow, path, data);
102
+ var state = nodeState(data, path);
103
+ var row = dom.el('button', 'fl-part');
104
+ if (!declared) row.classList.add('fl-part-desc');
105
+ row.type = 'button';
106
+ row.appendChild(Yg.states.badge(state));
107
+ row.appendChild(dom.el('span', 'mono fl-part-path', path));
108
+ row.appendChild(dom.el('span', 'fl-part-tag', declared ? 'declared' : 'descendant'));
109
+ row.addEventListener('click', function () {
110
+ nav({ view: 'tree', node: path });
111
+ });
112
+ return row;
113
+ }
114
+
115
+ function detail(flow, data, nav) {
116
+ var box = dom.el('div', 'fl-detail');
117
+
118
+ var head = dom.el('div', 'fl-detail-head');
119
+ head.appendChild(dom.el('h2', null, flow.name));
120
+ head.appendChild(statePill(flow));
121
+ box.appendChild(head);
122
+ if (flow.description) box.appendChild(dom.el('p', 'fl-detail-d', flow.description));
123
+
124
+ var declaredCount = 0;
125
+ var descCount = 0;
126
+ for (var i = 0; i < flow.participants.length; i += 1) {
127
+ if (isDeclared(flow, flow.participants[i], data)) declaredCount += 1;
128
+ else descCount += 1;
129
+ }
130
+ box.appendChild(
131
+ dom.el(
132
+ 'h5',
133
+ 'fl-h5',
134
+ 'Participants — ' + flow.participants.length + ' (' + declaredCount + ' declared · ' + descCount + ' auto-included descendants)',
135
+ ),
136
+ );
137
+ var parts = dom.el('div', 'fl-parts');
138
+ for (var j = 0; j < flow.participants.length; j += 1) {
139
+ parts.appendChild(participantRow(flow, flow.participants[j], data, nav));
140
+ }
141
+ box.appendChild(parts);
142
+
143
+ box.appendChild(dom.el('h5', 'fl-h5', 'Flow aspects (channel 5)'));
144
+ if (flow.aspects && flow.aspects.length) {
145
+ var asp = dom.el('div', 'fl-asps');
146
+ for (var k = 0; k < flow.aspects.length; k += 1) {
147
+ var chip = dom.el('button', 'fl-asp mono');
148
+ chip.type = 'button';
149
+ chip.textContent = flow.aspects[k];
150
+ chip.addEventListener(
151
+ 'click',
152
+ (function (id) {
153
+ return function () {
154
+ nav({ view: 'rulebook', aspect: id });
155
+ };
156
+ })(flow.aspects[k]),
157
+ );
158
+ asp.appendChild(chip);
159
+ }
160
+ box.appendChild(asp);
161
+ } else {
162
+ box.appendChild(dom.el('p', 'fl-rel-none', 'No flow-level aspects — flow membership alone attaches no rule.'));
163
+ }
164
+
165
+ box.appendChild(
166
+ dom.el(
167
+ 'p',
168
+ 'fl-foot',
169
+ 'A flow with no aspects, or one whose participants are all unguarded, shows nothing-checked — never green. Flow membership attaches rules; it is not evidence of correctness.',
170
+ ),
171
+ );
172
+ return box;
173
+ }
174
+
175
+ Yg.views.flows = function (stage, route, data, ctx) {
176
+ var nav = ctx && ctx.navigate ? ctx.navigate : function () {};
177
+ var flows = (data.flows || []).slice();
178
+ var selectedName = route && route.flow ? route.flow : flows.length ? flows[0].name : null;
179
+
180
+ stage.appendChild(dom.el('p', 'view-lead', 'Business processes that span components — the only business-language lens. A flow is never just green or red: a flow whose participants are all unguarded shows nothing-checked, and any refused or unverified participant makes the whole flow need attention. Absence of red is not a pass.'));
181
+ stage.appendChild(dom.el('div', 'rb-sub', flows.length + ' flows · flow state is never just green/red'));
182
+
183
+ if (!flows.length) {
184
+ stage.appendChild(dom.el('p', 'rb-empty', 'No flows are defined yet — no business processes to lens.'));
185
+ return;
186
+ }
187
+
188
+ var layout = dom.el('div', 'fl-layout');
189
+ layout.appendChild(gallery(flows, selectedName, nav));
190
+ var selected = flows.filter(function (f) {
191
+ return f.name === selectedName;
192
+ })[0];
193
+ if (route && route.flow && !selected) {
194
+ // A deep-link to a flow that no longer exists — say so honestly instead of silently
195
+ // presenting a DIFFERENT flow as if the link had resolved.
196
+ var nf = dom.el('div', 'fl-detail');
197
+ nf.appendChild(dom.el('p', 'rb-empty', 'No flow named "' + route.flow + '" — it may have been removed.'));
198
+ layout.appendChild(nf);
199
+ } else {
200
+ layout.appendChild(detail(selected || flows[0], data, nav));
201
+ }
202
+ stage.appendChild(layout);
203
+ };
204
+ })();