@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,151 @@
1
+ /*
2
+ * Audit export — turn the honest ledger into a portable artifact (CSV + JSON).
3
+ *
4
+ * The compliance deliverable is a real file, not a screenshot (§0b.5): the suppression
5
+ * inventory, the no-rule residue (nodes that own source but carry no effective rule), and the
6
+ * coverage summary are serialized to CSV and JSON and handed to the browser as a download. The
7
+ * download is built entirely in the page from a Blob / data-URI — NO network, so the offline
8
+ * static export can still produce the artifact. Pure string builders (buildSuppressionsCsv /
9
+ * buildResidueCsv / buildCoverageCsv / buildExportJson) are separated from the trigger so they
10
+ * are directly testable and round-trip back to the same rows; nothing here invents a state or a
11
+ * green — it serializes exactly what the honest PortalData already carries.
12
+ *
13
+ * Browser globals only — reads the already-resolved PortalData; no Node.
14
+ */
15
+ (function () {
16
+ 'use strict';
17
+
18
+ var Yg = (window.YgPortal = window.YgPortal || {});
19
+
20
+ /** Quote one CSV field: wrap in double-quotes and double any embedded quote (RFC 4180). */
21
+ function csvField(value) {
22
+ var s = value === null || value === undefined ? '' : String(value);
23
+ return '"' + s.replace(/"/g, '""') + '"';
24
+ }
25
+
26
+ /** Join an array of row-arrays into a CRLF-terminated CSV string (header row first). */
27
+ function csvRows(rows) {
28
+ return rows
29
+ .map(function (cols) {
30
+ return cols.map(csvField).join(',');
31
+ })
32
+ .join('\r\n');
33
+ }
34
+
35
+ /** CSV of the suppression inventory: every active waiver with its resolved risk flag. */
36
+ function buildSuppressionsCsv(data) {
37
+ var rows = [['file', 'line', 'aspect', 'risk', 'reason']];
38
+ var sups = (data.suppressions || []).slice();
39
+ for (var i = 0; i < sups.length; i += 1) {
40
+ var s = sups[i];
41
+ rows.push([s.file, s.line, s.aspectId, s.risk || 'none', s.reason || '']);
42
+ }
43
+ return csvRows(rows);
44
+ }
45
+
46
+ /** CSV of the no-rule residue: nodes that own source but carry no effective rule. */
47
+ function buildResidueCsv(data) {
48
+ var rows = [['node', 'kind']];
49
+ var res = (data.residue || { noRuleNodes: [], uncoveredFiles: [] });
50
+ (res.noRuleNodes || []).forEach(function (p) {
51
+ rows.push([p, 'no-rule-node']);
52
+ });
53
+ (res.uncoveredFiles || []).forEach(function (f) {
54
+ rows.push([f, 'uncovered-file']);
55
+ });
56
+ return csvRows(rows);
57
+ }
58
+
59
+ /** CSV of the coverage summary: each count from the honest ledger, one metric per row. */
60
+ function buildCoverageCsv(data) {
61
+ var c = data.meta.counts;
62
+ var rows = [['metric', 'value']];
63
+ var keys = [
64
+ 'nodes', 'aspects', 'flows', 'pairsTotal', 'pairsLLM', 'pairsDet',
65
+ 'verified', 'refused', 'advisoryRefused', 'unverified', 'noRule', 'draft', 'notApplicable',
66
+ 'suppressed', 'coveredFiles', 'uncoveredFiles', 'totalFiles', 'errors', 'warnings',
67
+ ];
68
+ for (var i = 0; i < keys.length; i += 1) rows.push([keys[i], c[keys[i]]]);
69
+ return rows.length ? csvRows(rows) : '';
70
+ }
71
+
72
+ /**
73
+ * The combined JSON export object: provenance (project, generation time, lock hash, commit
74
+ * ref) + the three audit tables. Round-trips back to the same rows; never collapses a state.
75
+ */
76
+ function buildExportJson(data) {
77
+ var c = data.meta.counts;
78
+ return {
79
+ provenance: {
80
+ project: data.meta.projectName,
81
+ generatedAt: data.meta.generatedAt,
82
+ lockHash: data.meta.lockHash,
83
+ commitRef: data.meta.commitRef,
84
+ schemaSupported: data.meta.schemaSupported,
85
+ },
86
+ coverage: c,
87
+ suppressions: (data.suppressions || []).slice(),
88
+ residue: data.residue || { noRuleNodes: [], uncoveredFiles: [] },
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Hand `content` to the browser as a download named `filename`, type `mime`. Built from a
94
+ * Blob + object URL when available, else a data: URI — both are in-page, NO network, so the
95
+ * offline static export still produces the artifact. Degrades silently when neither the
96
+ * anchor download nor URL APIs exist (a constrained sandbox); returns true on a real trigger.
97
+ */
98
+ function download(filename, content, mime) {
99
+ try {
100
+ var doc = window.document;
101
+ var a = doc.createElement('a');
102
+ if (typeof a.click !== 'function' && !('download' in a)) return false;
103
+ var href = null;
104
+ if (window.URL && typeof window.URL.createObjectURL === 'function' && typeof window.Blob === 'function') {
105
+ href = window.URL.createObjectURL(new window.Blob([content], { type: mime }));
106
+ } else {
107
+ href = 'data:' + mime + ';charset=utf-8,' + encodeURIComponent(content);
108
+ }
109
+ a.setAttribute('href', href);
110
+ a.setAttribute('download', filename);
111
+ // Append so a real browser fires the navigation; removed straight after.
112
+ if (doc.body && typeof doc.body.appendChild === 'function') doc.body.appendChild(a);
113
+ a.click();
114
+ if (doc.body && typeof doc.body.removeChild === 'function') doc.body.removeChild(a);
115
+ if (href && href.indexOf('blob:') === 0 && window.URL.revokeObjectURL) {
116
+ window.URL.revokeObjectURL(href);
117
+ }
118
+ return true;
119
+ } catch (_e) {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ Yg.exporter = {
125
+ buildSuppressionsCsv: buildSuppressionsCsv,
126
+ buildResidueCsv: buildResidueCsv,
127
+ buildCoverageCsv: buildCoverageCsv,
128
+ buildExportJson: buildExportJson,
129
+ download: download,
130
+ // Convenience triggers used by the views (suppressions / coverage). Each builds the content
131
+ // and offers the download; honest filenames carry the project name when present.
132
+ exportSuppressionsCsv: function (data) {
133
+ return download(slug(data) + '-suppressions.csv', buildSuppressionsCsv(data), 'text/csv');
134
+ },
135
+ exportResidueCsv: function (data) {
136
+ return download(slug(data) + '-residue.csv', buildResidueCsv(data), 'text/csv');
137
+ },
138
+ exportCoverageCsv: function (data) {
139
+ return download(slug(data) + '-coverage.csv', buildCoverageCsv(data), 'text/csv');
140
+ },
141
+ exportJson: function (data) {
142
+ return download(slug(data) + '-audit.json', JSON.stringify(buildExportJson(data), null, 2), 'application/json');
143
+ },
144
+ };
145
+
146
+ /** A filesystem-safe slug from the project name (fallback 'portal'). */
147
+ function slug(data) {
148
+ var name = (data.meta && data.meta.projectName) || 'portal';
149
+ return String(name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'portal';
150
+ }
151
+ })();
@@ -0,0 +1,72 @@
1
+ /*
2
+ * Plain-language glossary — the engine words said simply.
3
+ *
4
+ * The portal must be legible to a human who does not speak the engine's vocabulary, so
5
+ * every internal term (aspect, pair, deterministic vs LLM, relation, flow, reviewer /
6
+ * tier / consensus, no-rule, draft, when-filtered, waived) carries a plain definition
7
+ * that appears as a tooltip wherever the term is shown. The wording here is the same
8
+ * text as the glossary-key reference card, so the in-app tooltip and the key never drift.
9
+ *
10
+ * Browser globals only — the tooltip is a hover/focus popover built from page DOM; no
11
+ * network, no Node.
12
+ */
13
+ (function () {
14
+ 'use strict';
15
+
16
+ var Yg = (window.YgPortal = window.YgPortal || {});
17
+
18
+ // term id -> plain definition. Keyed by a stable lowercase id, not display text.
19
+ var TERMS = {
20
+ aspect: 'A rule the code must satisfy — e.g. “UI must not import the database”.',
21
+ deterministic:
22
+ 'A rule is checked either by a free local script (mechanical, repeatable) or by an AI reviewer reading it (judgment, may cost).',
23
+ llm: 'An AI reviewer reads the rule and judges the code against it — judgment, and it may cost.',
24
+ pair: 'One rule checked against one thing — the unit a verdict is recorded for.',
25
+ relation:
26
+ "A declared dependency between two components (calls / uses / …). The code's real dependencies must match what's declared.",
27
+ flow: 'A business process that spans several components — “place an order”.',
28
+ reviewer:
29
+ 'Which model judged a rule, and how many times it voted — so you can weigh how much a green is worth.',
30
+ tier: 'The named reviewer setting an aspect uses — which model judges it, and how strictly.',
31
+ consensus: 'How many times the reviewer voted on a rule — more votes, more confidence in the verdict.',
32
+ 'no-rule': 'Nothing is checking this part. Not broken — just unguarded. Absence of red is not a pass.',
33
+ draft: 'A rule parked as not-ready — it is removed from the expected set and verifies nothing.',
34
+ 'when-filtered': 'A rule was deliberately filtered out here — it does not apply to this node.',
35
+ waived: 'Someone told the reviewer to skip a rule here, on purpose, with a reason. Not the same as verified.',
36
+ unverified: "The code changed, so no reviewer has confirmed it yet. Not a pass — just “we don’t know”.",
37
+ suppressed: 'A waiver tells the reviewer to skip a rule on specific lines, with a reason. Waived is not verified.',
38
+ cost: 'A free local script costs nothing; an AI reviewer may cost a paid API call each time it judges.',
39
+ // Where a rule comes from — its provenance / attach channel onto this component.
40
+ own: 'This rule is set directly on this component.',
41
+ ancestor: 'This rule is inherited from a parent component.',
42
+ 'own-type': 'This rule applies to every component of this type.',
43
+ 'ancestor-type': "Inherited from a parent component's type.",
44
+ port: 'This rule crosses in from a named contract this component consumes.',
45
+ implied: 'Pulled in by another rule that includes this one.',
46
+ };
47
+
48
+ /** The plain definition for a term id, or null when the term is unknown. */
49
+ function lookup(termId) {
50
+ var key = String(termId || '').toLowerCase();
51
+ return Object.prototype.hasOwnProperty.call(TERMS, key) ? TERMS[key] : null;
52
+ }
53
+
54
+ /**
55
+ * Wrap a piece of text as a glossary term: a <span class="term"> carrying the plain
56
+ * definition both as a native title and as an aria-label, so the meaning is reachable on
57
+ * hover AND by a screen reader. Falls back to plain text when the term is unknown.
58
+ */
59
+ function term(termId, displayText) {
60
+ var def = lookup(termId);
61
+ var text = displayText === undefined ? String(termId) : displayText;
62
+ if (!def) return Yg.dom.el('span', null, text);
63
+ var node = Yg.dom.el('span', 'term', text);
64
+ node.setAttribute('tabindex', '0');
65
+ node.setAttribute('title', def);
66
+ node.setAttribute('aria-label', text + ': ' + def);
67
+ node.setAttribute('data-term', String(termId).toLowerCase());
68
+ return node;
69
+ }
70
+
71
+ Yg.glossary = { lookup: lookup, term: term, _terms: TERMS };
72
+ })();
@@ -0,0 +1,60 @@
1
+ /*
2
+ * Portal namespace + small DOM/util helpers.
3
+ *
4
+ * Every portal browser module attaches to ONE global, `window.YgPortal`, so the
5
+ * inlined static page (a sequence of plain <script> blocks, no module system) can
6
+ * share code without `import`/`require` — neither exists in a single-file offline
7
+ * page. This is the only place the global is created; later modules extend it.
8
+ *
9
+ * Browser globals only: no Node, no network, no secrets. The portal frontend runs
10
+ * inside a browser tab from a self-contained file or a same-origin loopback page.
11
+ */
12
+ (function () {
13
+ 'use strict';
14
+
15
+ /** The shared namespace — created once, extended by every later module. */
16
+ var Yg = (window.YgPortal = window.YgPortal || {});
17
+
18
+ /** Create an element with an optional class and text/child content. */
19
+ function el(tag, cls, content) {
20
+ var node = document.createElement(tag);
21
+ if (cls) node.className = cls;
22
+ if (content === undefined || content === null) return node;
23
+ if (typeof content === 'string' || typeof content === 'number') {
24
+ node.textContent = String(content);
25
+ } else if (content.nodeType) {
26
+ node.appendChild(content);
27
+ } else if (Array.isArray(content)) {
28
+ for (var i = 0; i < content.length; i += 1) {
29
+ var c = content[i];
30
+ if (c == null) continue;
31
+ node.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
32
+ }
33
+ }
34
+ return node;
35
+ }
36
+
37
+ /** Set several attributes at once (skips null/undefined values). */
38
+ function attrs(node, map) {
39
+ for (var k in map) {
40
+ if (!Object.prototype.hasOwnProperty.call(map, k)) continue;
41
+ var v = map[k];
42
+ if (v === null || v === undefined) continue;
43
+ node.setAttribute(k, String(v));
44
+ }
45
+ return node;
46
+ }
47
+
48
+ /** Remove every child of an element (cheap, allocation-free clear). */
49
+ function clear(node) {
50
+ while (node.firstChild) node.removeChild(node.firstChild);
51
+ return node;
52
+ }
53
+
54
+ /** First element matching a selector within `root` (defaults to document). */
55
+ function find(selector, root) {
56
+ return (root || document).querySelector(selector);
57
+ }
58
+
59
+ Yg.dom = { el: el, attrs: attrs, clear: clear, find: find };
60
+ })();
@@ -0,0 +1,176 @@
1
+ /*
2
+ * Command palette overlay — the ⌘K modal DOM, driven by the pure matcher.
3
+ *
4
+ * Builds the search index from PortalData once, then renders a keyboard-first modal:
5
+ * an input, a results list (arrow keys move the active row, Enter routes to it), and a
6
+ * footer of shortcuts. Open with ⌘K / Ctrl-K or the top-bar trigger; close with Esc.
7
+ * Selecting a result hands its route to the router — one navigation code path. The
8
+ * ranking logic lives in palette.js (pure, tested); this file is only the overlay.
9
+ *
10
+ * Browser globals only — no network, no Node.
11
+ */
12
+ (function () {
13
+ 'use strict';
14
+
15
+ var Yg = (window.YgPortal = window.YgPortal || {});
16
+ var dom = Yg.dom;
17
+
18
+ /** Create a palette controller bound to a router and a PortalData index. */
19
+ function create(data, router) {
20
+ var index = Yg.paletteSearch.buildIndex(data);
21
+ var overlay = null;
22
+ var input = null;
23
+ var listEl = null;
24
+ var rows = [];
25
+ var active = 0;
26
+
27
+ function isOpen() {
28
+ return overlay !== null;
29
+ }
30
+
31
+ function renderResults() {
32
+ var query = input ? input.value : '';
33
+ var results = Yg.paletteSearch.search(index, query, 50);
34
+ dom.clear(listEl);
35
+ rows = [];
36
+ active = 0;
37
+ if (results.length === 0) {
38
+ listEl.appendChild(dom.el('div', 'palette-empty', 'No matches.'));
39
+ return;
40
+ }
41
+ for (var i = 0; i < results.length; i += 1) {
42
+ rows.push(appendRow(results[i], i === 0));
43
+ }
44
+ }
45
+
46
+ function appendRow(item, isActive) {
47
+ var row = dom.el('div', 'palette-row' + (isActive ? ' on' : ''));
48
+ row.setAttribute('role', 'option');
49
+ row.setAttribute('aria-selected', isActive ? 'true' : 'false');
50
+ var ic = dom.el('span', 'palette-ic', kindGlyph(item));
51
+ var nm = dom.el('span', 'palette-nm', item.label);
52
+ var sub = dom.el('span', 'palette-sub', item.sub || '');
53
+ var kind = dom.el('span', 'palette-kind', item.kind);
54
+ row.appendChild(ic);
55
+ row.appendChild(nm);
56
+ row.appendChild(sub);
57
+ row.appendChild(kind);
58
+ row.addEventListener('mouseenter', function () {
59
+ setActiveRow(rows.indexOf(row));
60
+ });
61
+ row.addEventListener('click', function () {
62
+ choose(item);
63
+ });
64
+ row._item = item;
65
+ listEl.appendChild(row);
66
+ return row;
67
+ }
68
+
69
+ function kindGlyph(item) {
70
+ if (item.kind === 'node') return Yg.states.glyph(item.state || 'no-rule');
71
+ if (item.kind === 'aspect') return '▤';
72
+ if (item.kind === 'flow') return '⤳';
73
+ return '◧';
74
+ }
75
+
76
+ function setActiveRow(i) {
77
+ if (i < 0 || i >= rows.length) return;
78
+ if (rows[active]) {
79
+ rows[active].className = 'palette-row';
80
+ rows[active].setAttribute('aria-selected', 'false');
81
+ }
82
+ active = i;
83
+ rows[active].className = 'palette-row on';
84
+ rows[active].setAttribute('aria-selected', 'true');
85
+ rows[active].scrollIntoView({ block: 'nearest' });
86
+ }
87
+
88
+ function choose(item) {
89
+ close();
90
+ router.go(item.route);
91
+ }
92
+
93
+ function onKey(e) {
94
+ if (e.key === 'Escape') {
95
+ close();
96
+ e.preventDefault();
97
+ return;
98
+ }
99
+ if (e.key === 'ArrowDown') {
100
+ setActiveRow(Math.min(active + 1, rows.length - 1));
101
+ e.preventDefault();
102
+ return;
103
+ }
104
+ if (e.key === 'ArrowUp') {
105
+ setActiveRow(Math.max(active - 1, 0));
106
+ e.preventDefault();
107
+ return;
108
+ }
109
+ if (e.key === 'Enter') {
110
+ if (rows[active] && rows[active]._item) choose(rows[active]._item);
111
+ e.preventDefault();
112
+ }
113
+ }
114
+
115
+ function open() {
116
+ if (isOpen()) return;
117
+ overlay = dom.el('div', 'palette-backdrop');
118
+ overlay.setAttribute('role', 'dialog');
119
+ overlay.setAttribute('aria-label', 'Command palette');
120
+ var box = dom.el('div', 'palette-box');
121
+ var inputWrap = dom.el('div', 'palette-input');
122
+ inputWrap.appendChild(dom.el('span', 'palette-search-ic', '⌕'));
123
+ input = document.createElement('input');
124
+ input.type = 'text';
125
+ input.className = 'palette-field';
126
+ input.setAttribute('aria-label', 'Search nodes, aspects, flows, and views');
127
+ input.placeholder = 'Search nodes, aspects, flows…';
128
+ inputWrap.appendChild(input);
129
+ listEl = dom.el('div', 'palette-results');
130
+ listEl.setAttribute('role', 'listbox');
131
+ var foot = dom.el('div', 'palette-foot');
132
+ foot.appendChild(dom.el('span', null, '↑↓ navigate'));
133
+ foot.appendChild(dom.el('span', null, '↵ open'));
134
+ foot.appendChild(dom.el('span', null, 'esc close'));
135
+ box.appendChild(inputWrap);
136
+ box.appendChild(listEl);
137
+ box.appendChild(foot);
138
+ overlay.appendChild(box);
139
+
140
+ overlay.addEventListener('click', function (e) {
141
+ if (e.target === overlay) close();
142
+ });
143
+ input.addEventListener('input', renderResults);
144
+ input.addEventListener('keydown', onKey);
145
+
146
+ document.body.appendChild(overlay);
147
+ renderResults();
148
+ input.focus();
149
+ }
150
+
151
+ function close() {
152
+ if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
153
+ overlay = null;
154
+ input = null;
155
+ listEl = null;
156
+ rows = [];
157
+ }
158
+
159
+ function toggle() {
160
+ if (isOpen()) close();
161
+ else open();
162
+ }
163
+
164
+ // Global ⌘K / Ctrl-K binding.
165
+ document.addEventListener('keydown', function (e) {
166
+ if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
167
+ toggle();
168
+ e.preventDefault();
169
+ }
170
+ });
171
+
172
+ return { open: open, close: close, toggle: toggle, isOpen: isOpen };
173
+ }
174
+
175
+ Yg.palette = { create: create };
176
+ })();
@@ -0,0 +1,133 @@
1
+ /*
2
+ * Command palette (⌘K) — fuzzy jump to any node / aspect / flow or view action.
3
+ *
4
+ * One keyboard-first overlay over the whole graph: type a few characters and the palette
5
+ * fuzzy-matches every node, aspect, flow, and view action, ranked; Enter routes to the
6
+ * selection (a node opens its panel, an aspect/flow its detail, an action runs). It is
7
+ * never empty — with no query it shows view actions and a few entities so the door is
8
+ * always open.
9
+ *
10
+ * The index builder and the fuzzy matcher are PURE functions over plain data (no DOM), so
11
+ * the ranking is unit-testable directly; the overlay below is the thin DOM layer. Browser
12
+ * globals only — no network, no Node.
13
+ */
14
+ (function () {
15
+ 'use strict';
16
+
17
+ var Yg = (window.YgPortal = window.YgPortal || {});
18
+
19
+ /**
20
+ * Build the flat searchable index from a PortalData object. Each entry is
21
+ * { id, label, sub, kind, route } — `route` is a router route the selection navigates to.
22
+ * View actions come first so an empty query always has somewhere to go.
23
+ */
24
+ function buildIndex(data) {
25
+ var items = [];
26
+
27
+ // View actions — always present, even on an empty/blank graph.
28
+ var VIEW_ACTIONS = [
29
+ { id: 'view:overview', label: 'Overview', sub: 'the landing', kind: 'view', route: { view: 'overview' } },
30
+ { id: 'view:coverage', label: 'Coverage & audit', sub: 'the full ledger', kind: 'view', route: { view: 'coverage' } },
31
+ { id: 'view:tree', label: 'Structure (tree)', sub: 'the hierarchy', kind: 'view', route: { view: 'tree' } },
32
+ { id: 'view:relations', label: 'Relations & boundaries', sub: 'matrix · hubs · boundary', kind: 'view', route: { view: 'relations' } },
33
+ { id: 'view:rulebook', label: 'Rulebook', sub: "what's enforced", kind: 'view', route: { view: 'rulebook' } },
34
+ { id: 'view:types', label: 'Type model', sub: "what's possible", kind: 'view', route: { view: 'types' } },
35
+ { id: 'view:flows', label: 'Flows', sub: 'what the system does', kind: 'view', route: { view: 'flows' } },
36
+ { id: 'view:suppressions', label: 'Suppressions', sub: 'waiver inventory', kind: 'view', route: { view: 'suppressions' } },
37
+ { id: 'view:start', label: 'Start here', sub: 'the on-ramp', kind: 'view', route: { view: 'start' } },
38
+ ];
39
+ for (var a = 0; a < VIEW_ACTIONS.length; a += 1) items.push(VIEW_ACTIONS[a]);
40
+
41
+ var nodes = (data && data.nodes) || [];
42
+ for (var n = 0; n < nodes.length; n += 1) {
43
+ var node = nodes[n];
44
+ items.push({
45
+ id: 'node:' + node.path,
46
+ label: node.name || node.path,
47
+ sub: node.path + ' · ' + (node.type || ''),
48
+ kind: 'node',
49
+ state: node.state,
50
+ route: { view: 'tree', node: node.path },
51
+ });
52
+ }
53
+
54
+ var aspects = (data && data.aspects) || [];
55
+ for (var s = 0; s < aspects.length; s += 1) {
56
+ var asp = aspects[s];
57
+ items.push({
58
+ id: 'aspect:' + asp.id,
59
+ label: asp.id,
60
+ sub: 'aspect · ' + (asp.kind || '') + ' · ' + (asp.status || ''),
61
+ kind: 'aspect',
62
+ route: { view: 'rulebook', aspect: asp.id },
63
+ });
64
+ }
65
+
66
+ var flows = (data && data.flows) || [];
67
+ for (var f = 0; f < flows.length; f += 1) {
68
+ var flow = flows[f];
69
+ items.push({
70
+ id: 'flow:' + flow.name,
71
+ label: flow.name,
72
+ sub: 'flow · ' + ((flow.participants && flow.participants.length) || 0) + ' participants',
73
+ kind: 'flow',
74
+ route: { view: 'flows', flow: flow.name },
75
+ });
76
+ }
77
+
78
+ return items;
79
+ }
80
+
81
+ /**
82
+ * Subsequence fuzzy score of `query` against `text`. Returns a number (higher = better)
83
+ * or -1 when the query is not a subsequence. Rewards contiguous runs and a match at a word
84
+ * boundary, so "fill" ranks "cli/core/fill" above an incidental scatter of those letters.
85
+ */
86
+ function fuzzyScore(query, text) {
87
+ var q = query.toLowerCase();
88
+ var t = text.toLowerCase();
89
+ if (q.length === 0) return 0;
90
+ var score = 0;
91
+ var ti = 0;
92
+ var prevMatch = -2;
93
+ for (var qi = 0; qi < q.length; qi += 1) {
94
+ var ch = q.charAt(qi);
95
+ var found = t.indexOf(ch, ti);
96
+ if (found === -1) return -1;
97
+ score += 1;
98
+ if (found === prevMatch + 1) score += 3; // contiguous run
99
+ if (found === 0 || /[\/\-_. ]/.test(t.charAt(found - 1))) score += 2; // word boundary
100
+ prevMatch = found;
101
+ ti = found + 1;
102
+ }
103
+ // Prefer shorter targets (a tighter match) and an exact-substring hit.
104
+ if (t.indexOf(q) !== -1) score += 4;
105
+ score -= t.length * 0.01;
106
+ return score;
107
+ }
108
+
109
+ /**
110
+ * Rank index `items` against `query`. An empty query returns the first `limit` items in
111
+ * index order (view actions first — never an empty palette). A non-empty query keeps only
112
+ * subsequence matches, ranked by score, capped at `limit`.
113
+ */
114
+ function search(items, query, limit) {
115
+ var cap = limit || 50;
116
+ var q = String(query || '').trim();
117
+ if (q.length === 0) return items.slice(0, cap);
118
+ var scored = [];
119
+ for (var i = 0; i < items.length; i += 1) {
120
+ var item = items[i];
121
+ var best = Math.max(fuzzyScore(q, item.label), fuzzyScore(q, item.sub || '') - 1);
122
+ if (best >= 0) scored.push({ item: item, score: best });
123
+ }
124
+ scored.sort(function (a, b) {
125
+ return b.score - a.score;
126
+ });
127
+ return scored.slice(0, cap).map(function (s) {
128
+ return s.item;
129
+ });
130
+ }
131
+
132
+ Yg.paletteSearch = { buildIndex: buildIndex, fuzzyScore: fuzzyScore, search: search };
133
+ })();