@giamme/forge 0.0.0-stage → 0.8.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 (127) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1854 -2
  4. package/SKILL.md +122 -0
  5. package/VERSION +1 -0
  6. package/forge +74 -0
  7. package/package.json +26 -4
  8. package/references/dataset.md +329 -0
  9. package/references/decompose.md +667 -0
  10. package/references/fractal-dashboard.md +21 -0
  11. package/references/fractal.md +307 -0
  12. package/references/harnesses.md +353 -0
  13. package/references/jev.md +865 -0
  14. package/references/memory.md +228 -0
  15. package/references/planning.md +42 -0
  16. package/references/ripwire.md +47 -0
  17. package/references/runtime.md +64 -0
  18. package/references/solo.md +113 -0
  19. package/references/ui.md +112 -0
  20. package/registry.tsv +81 -0
  21. package/scripts/forge-artifact.sh +78 -0
  22. package/scripts/forge-contract.py +430 -0
  23. package/scripts/forge-dataset.py +6 -0
  24. package/scripts/forge-dispatch.sh +665 -0
  25. package/scripts/forge-fail-first.py +195 -0
  26. package/scripts/forge-fractal-dispatch.sh +4 -0
  27. package/scripts/forge-fractal-options.sh +54 -0
  28. package/scripts/forge-fractal.py +6 -0
  29. package/scripts/forge-guard.py +266 -0
  30. package/scripts/forge-install-fractal.sh +4 -0
  31. package/scripts/forge-install-ripwire.sh +52 -0
  32. package/scripts/forge-install.sh +135 -0
  33. package/scripts/forge-jev-options.sh +259 -0
  34. package/scripts/forge-jev.py +6 -0
  35. package/scripts/forge-lib-plans.sh +785 -0
  36. package/scripts/forge-lib-review.sh +494 -0
  37. package/scripts/forge-lib-task.sh +302 -0
  38. package/scripts/forge-lib-verify.sh +30 -0
  39. package/scripts/forge-memory.sh +575 -0
  40. package/scripts/forge-metrics.sh +39 -0
  41. package/scripts/forge-parallel-options.sh +76 -0
  42. package/scripts/forge-parallel.sh +1717 -0
  43. package/scripts/forge-plan-check.py +118 -0
  44. package/scripts/forge-prompt.py +45 -0
  45. package/scripts/forge-ripwire.py +136 -0
  46. package/scripts/forge-route.py +344 -0
  47. package/scripts/forge-runtime.py +533 -0
  48. package/scripts/forge-schedule.py +312 -0
  49. package/scripts/forge-shadow.py +305 -0
  50. package/scripts/forge-solo.sh +347 -0
  51. package/scripts/forge-ui-options.sh +42 -0
  52. package/scripts/forge-ui.py +435 -0
  53. package/scripts/forge-update.sh +71 -0
  54. package/scripts/forge-verify-coverage.py +956 -0
  55. package/scripts/forge-version.py +198 -0
  56. package/scripts/forge_dataset/__init__.py +439 -0
  57. package/scripts/forge_dataset/capture.py +330 -0
  58. package/scripts/forge_dataset/checks.py +109 -0
  59. package/scripts/forge_dataset/cli.py +233 -0
  60. package/scripts/forge_dataset/export.py +1009 -0
  61. package/scripts/forge_dataset/harvest.py +716 -0
  62. package/scripts/forge_dataset/importer.py +412 -0
  63. package/scripts/forge_dataset/outcomes.py +275 -0
  64. package/scripts/forge_dataset/routing.py +278 -0
  65. package/scripts/forge_fractal/__init__.py +83 -0
  66. package/scripts/forge_fractal/bridge.py +46 -0
  67. package/scripts/forge_fractal/cli.py +202 -0
  68. package/scripts/forge_fractal/dashboard.css +138 -0
  69. package/scripts/forge_fractal/dashboard.html +41 -0
  70. package/scripts/forge_fractal/dashboard.js +687 -0
  71. package/scripts/forge_fractal/decomposition.py +262 -0
  72. package/scripts/forge_fractal/execution.py +659 -0
  73. package/scripts/forge_fractal/inspection.py +261 -0
  74. package/scripts/forge_fractal/install.py +158 -0
  75. package/scripts/forge_fractal/invocation.py +94 -0
  76. package/scripts/forge_fractal/pipeline.py +124 -0
  77. package/scripts/forge_fractal/selection.py +179 -0
  78. package/scripts/forge_jev/__init__.py +376 -0
  79. package/scripts/forge_jev/backtest.py +322 -0
  80. package/scripts/forge_jev/cli.py +980 -0
  81. package/scripts/forge_jev/client.py +418 -0
  82. package/scripts/forge_jev/corpus.py +156 -0
  83. package/scripts/forge_jev/coupling.py +244 -0
  84. package/scripts/forge_jev/features.py +24 -0
  85. package/scripts/forge_jev/gates.py +100 -0
  86. package/scripts/forge_jev/memory.py +163 -0
  87. package/scripts/forge_jev/outcome.py +49 -0
  88. package/scripts/forge_jev/predict.py +204 -0
  89. package/scripts/forge_jev/questions.py +661 -0
  90. package/scripts/forge_jev/review.py +73 -0
  91. package/scripts/forge_jev/routing.py +271 -0
  92. package/scripts/forge_jev/subset.py +123 -0
  93. package/scripts/forge_jev/verify.py +453 -0
  94. package/scripts/forge_registry.py +140 -0
  95. package/ui/README.md +287 -0
  96. package/ui/THIRD_PARTY_NOTICES.md +69 -0
  97. package/ui/docs/api.md +888 -0
  98. package/ui/forge_ui/__init__.py +20 -0
  99. package/ui/forge_ui/__main__.py +135 -0
  100. package/ui/forge_ui/diffs.py +454 -0
  101. package/ui/forge_ui/executions.py +436 -0
  102. package/ui/forge_ui/export.py +204 -0
  103. package/ui/forge_ui/findings.py +132 -0
  104. package/ui/forge_ui/fractal.py +583 -0
  105. package/ui/forge_ui/models.py +216 -0
  106. package/ui/forge_ui/reader.py +1336 -0
  107. package/ui/forge_ui/server.py +303 -0
  108. package/ui/forge_ui/static/app.js +924 -0
  109. package/ui/forge_ui/static/cloud3d.js +646 -0
  110. package/ui/forge_ui/static/cloudmodel.js +375 -0
  111. package/ui/forge_ui/static/cloudui.js +394 -0
  112. package/ui/forge_ui/static/diffview.js +216 -0
  113. package/ui/forge_ui/static/favicon.svg +1 -0
  114. package/ui/forge_ui/static/files.js +776 -0
  115. package/ui/forge_ui/static/gantt.js +97 -0
  116. package/ui/forge_ui/static/graph.js +266 -0
  117. package/ui/forge_ui/static/index.html +55 -0
  118. package/ui/forge_ui/static/iso.js +765 -0
  119. package/ui/forge_ui/static/leaderboard.js +315 -0
  120. package/ui/forge_ui/static/map3d.js +45 -0
  121. package/ui/forge_ui/static/modelstats.js +249 -0
  122. package/ui/forge_ui/static/notify.js +179 -0
  123. package/ui/forge_ui/static/panel.js +526 -0
  124. package/ui/forge_ui/static/runmodel.js +651 -0
  125. package/ui/forge_ui/static/scene3d.js +996 -0
  126. package/ui/forge_ui/static/search.js +119 -0
  127. package/ui/forge_ui/static/style.css +618 -0
@@ -0,0 +1,394 @@
1
+ /* forge-ui run cloud controls: the filter panel, the layout and shape switches, the position tools, the
2
+ legend and the run card, laid over the full-screen cloud (cloud3d.js); and, when 3D is off, the same
3
+ filtered runs as a list. All API values enter the DOM as text.
4
+ window.ForgeCloudUI = { create(root, ctx) }, where ctx = { prefs, save(prefs), openRun(key), route({sel, focus}),
5
+ load3d(), viewToggle() }. */
6
+ window.ForgeCloudUI = (() => {
7
+ 'use strict';
8
+ const CM = window.ForgeCloudModel, RM = window.ForgeRunModel;
9
+ const STATES = [['live', 'running', 'running'], ['failed', 'has failures', 'fail'], ['done', 'all done', 'merged'], ['pending', 'not started', 'pending'], ['stale', 'stalled', 'interrupted']];
10
+ const STATE = Object.fromEntries(STATES.map(([k, label, st]) => [k, { label, st }]));
11
+ const LIST_CAP = 300;
12
+ const list = v => Array.isArray(v) ? v : [];
13
+ function el(tag, text, cls) {
14
+ const n = document.createElement(tag);
15
+ if (text !== undefined && text !== null) n.textContent = String(text);
16
+ if (cls) n.className = cls;
17
+ return n;
18
+ }
19
+ function button(text, cls, onClick, title) {
20
+ const b = el('button', text, cls); b.type = 'button';
21
+ if (title) b.title = title;
22
+ if (onClick) b.addEventListener('click', onClick);
23
+ return b;
24
+ }
25
+ const when = t => Number.isFinite(Number(t)) && t ? new Date(Number(t) * 1000).toLocaleString() : '—';
26
+
27
+ function create(root, ctx) {
28
+ const prefs = {
29
+ filter: CM.normFilter(ctx.prefs && ctx.prefs.filter),
30
+ layout: CM.LAYOUTS.includes(ctx.prefs && ctx.prefs.layout) ? ctx.prefs.layout : 'helix',
31
+ shape: ctx.prefs && ctx.prefs.shape === 'orbs' ? 'orbs' : 'blocks',
32
+ panel: !(ctx.prefs && ctx.prefs.panel === false),
33
+ };
34
+ const ui = el('div', undefined, 'cloud-ui');
35
+ const bar = el('div', undefined, 'cloud-bar'); bar.setAttribute('role', 'toolbar'); bar.setAttribute('aria-label', 'Cloud');
36
+ const panel = el('form', undefined, 'cloud-panel'); panel.id = 'cloud-panel'; panel.setAttribute('aria-label', 'Filter the runs in the cloud');
37
+ panel.addEventListener('submit', e => e.preventDefault());
38
+ const card = el('aside', undefined, 'cloud-card'); card.setAttribute('aria-label', 'Selected run'); card.hidden = true;
39
+ const legend = el('div', undefined, 'cloud-legend');
40
+ const toast = el('div', undefined, 'cloud-toast'); toast.setAttribute('role', 'status'); toast.hidden = true;
41
+ const listHost = el('div', undefined, 'cloud-list'); listHost.hidden = true;
42
+ ui.append(bar, panel, card, legend, toast);
43
+ root.append(ui, listHost);
44
+ const file = el('input'); file.type = 'file'; file.accept = 'application/json,.json'; file.hidden = true; ui.append(file);
45
+
46
+ const S = { runs: [], facts: [], byKey: new Map(), sel: null, focus: null, mode: '2d', stage: null, shown: false, optSig: '', runsRef: null, toastTimer: 0 };
47
+ const cloud = () => S.mode === '3d' && S.stage ? S.stage.cloud : null;
48
+ const save = () => ctx.save && ctx.save({ filter: prefs.filter, layout: prefs.layout, shape: prefs.shape, panel: prefs.panel });
49
+ const nowSec = () => Date.now() / 1000;
50
+
51
+ /* ---------- the bar: counts, layout, shape, positions, view ---------- */
52
+ const count = el('span', '', 'cloud-count'); count.setAttribute('aria-live', 'polite');
53
+ const filtersBtn = button('Filters', 'cloud-filters-btn', () => { prefs.panel = !prefs.panel; save(); syncBar(); });
54
+ filtersBtn.setAttribute('aria-controls', 'cloud-panel');
55
+ const layoutSeg = el('div', undefined, 'seg cloud-layouts'); layoutSeg.setAttribute('role', 'group'); layoutSeg.setAttribute('aria-label', 'Layout');
56
+ for (const name of CM.LAYOUTS) {
57
+ const b = button(CM.LAYOUT_INFO[name].name, '', () => setLayout(name), CM.LAYOUT_INFO[name].blurb + ' (L cycles)');
58
+ b.dataset.layout = name; layoutSeg.append(b);
59
+ }
60
+ const shapeSeg = el('div', undefined, 'seg cloud-shapes'); shapeSeg.setAttribute('role', 'group'); shapeSeg.setAttribute('aria-label', 'Run shape');
61
+ for (const [shape, label, title] of [['blocks', 'Blocks', 'Iso blocks with a lamp per task (B toggles)'], ['orbs', 'Orbs', 'Spheres ringed by their tasks (B toggles)']]) {
62
+ const b = button(label, '', () => setShape(shape), title); b.dataset.shape = shape; shapeSeg.append(b);
63
+ }
64
+ const resetView = button('Reset view', '', () => { const c = cloud(); if (c) c.resetView(); }, 'Frame every shown run (0)');
65
+ const posMenu = el('details', undefined, 'cloud-menu');
66
+ const posSum = el('summary', 'Positions'); posMenu.append(posSum);
67
+ const posBody = el('div', undefined, 'cloud-menu-body');
68
+ const posNote = el('p', '', 'muted');
69
+ const resetPos = button('Reset moved runs', '', () => {
70
+ const c = cloud(); if (!c) return;
71
+ const name = CM.LAYOUT_INFO[prefs.layout].name, undo = c.resetPositions('layout');
72
+ posMenu.open = false; syncBar(); refreshCard();
73
+ say(`Moved runs went back to their places in ${name}.`, 'Undo', () => { undo(); syncBar(); refreshCard(); });
74
+ }, 'Put every run you moved in this layout back where the layout places it');
75
+ const exportPos = button('Export positions…', '', () => {
76
+ const c = cloud(); if (!c) return;
77
+ const blob = new Blob([JSON.stringify(c.exportPositions(), null, 2)], { type: 'application/json' });
78
+ const a = el('a'); a.href = URL.createObjectURL(blob); a.download = 'forge-ui-cloud-layout.json';
79
+ document.body.append(a); a.click(); a.remove();
80
+ setTimeout(() => URL.revokeObjectURL(a.href), 2000);
81
+ posMenu.open = false;
82
+ }, 'Save where you moved runs, in every layout, as a JSON file');
83
+ const importPos = button('Import positions…', '', () => file.click(), 'Load positions from a file exported here');
84
+ posBody.append(posNote, resetPos, exportPos, importPos); posMenu.append(posBody);
85
+ file.addEventListener('change', async () => {
86
+ const f = file.files && file.files[0]; file.value = ''; posMenu.open = false;
87
+ if (!f) return;
88
+ if (f.size > 5 * 1024 * 1024) { say('That file is larger than 5 MB: not a cloud layout.'); return; }
89
+ let obj = null;
90
+ try { obj = JSON.parse(await f.text()); } catch (_) { say('That file is not JSON.'); return; }
91
+ const c = cloud(); if (!c) return;
92
+ const res = c.importPositions(obj);
93
+ if (!res.ok) { say(`Not imported: ${res.errors[0]}`); return; }
94
+ say(`Imported ${res.count} position${res.count === 1 ? '' : 's'}${res.unknown ? `, ${res.unknown} for runs not listed here` : ''}${res.errors.length ? `; ${res.errors.length} skipped` : ''}.`);
95
+ syncBar(); refreshCard();
96
+ });
97
+ const full = button('Full screen', '', () => {
98
+ if (document.fullscreenElement) document.exitFullscreen().catch(() => {});
99
+ else if (root.requestFullscreen) root.requestFullscreen().catch(() => say('Full screen is not available here.'));
100
+ });
101
+ document.addEventListener('fullscreenchange', () => { full.textContent = document.fullscreenElement === root ? 'Exit full screen' : 'Full screen'; });
102
+ const title = el('h1', 'Run cloud');
103
+ const left = el('div', undefined, 'cloud-bar-left'); left.append(title, count);
104
+ const tools = el('div', undefined, 'cloud-bar-tools'); tools.append(filtersBtn, layoutSeg, shapeSeg, resetView, posMenu, full);
105
+ if (ctx.viewToggle) tools.append(ctx.viewToggle());
106
+ bar.append(left, tools);
107
+
108
+ function syncBar() {
109
+ const active = CM.activeCount(prefs.filter);
110
+ filtersBtn.textContent = active ? `Filters (${active})` : 'Filters';
111
+ filtersBtn.setAttribute('aria-expanded', String(prefs.panel));
112
+ panel.hidden = !prefs.panel;
113
+ for (const b of layoutSeg.children) b.setAttribute('aria-pressed', String(b.dataset.layout === prefs.layout));
114
+ for (const b of shapeSeg.children) b.setAttribute('aria-pressed', String(b.dataset.shape === prefs.shape));
115
+ const three = S.mode === '3d';
116
+ for (const n of [layoutSeg, shapeSeg, resetView, posMenu, full]) n.hidden = !three;
117
+ const c = cloud(), n = c ? c.pinnedCount() : 0;
118
+ posSum.textContent = n ? `Positions (${n} moved)` : 'Positions';
119
+ posNote.textContent = n ? `${n} run${n === 1 ? '' : 's'} moved by you in ${CM.LAYOUT_INFO[prefs.layout].name}. Positions are kept in this browser, per layout.` : `Drag a run to move it. Positions are kept in this browser, per layout.`;
120
+ resetPos.disabled = !n;
121
+ insets();
122
+ const visible = c ? c.counts().visible : S.facts.filter(f => CM.match(f, prefs.filter, nowSec())).length;
123
+ count.textContent = `${visible} of ${S.facts.length} run${S.facts.length === 1 ? '' : 's'}`;
124
+ }
125
+ function setLayout(name) {
126
+ if (!CM.LAYOUTS.includes(name)) return;
127
+ prefs.layout = name; save();
128
+ const c = cloud(); if (c) c.setLayout(name);
129
+ syncBar(); legendText(); refreshCard();
130
+ }
131
+ function setShape(shape) {
132
+ prefs.shape = shape === 'orbs' ? 'orbs' : 'blocks'; save();
133
+ const c = cloud(); if (c) c.setShape(prefs.shape);
134
+ syncBar();
135
+ }
136
+
137
+ // what the overlays cover, so the cloud frames into the space left over
138
+ function measure() {
139
+ const R = root.getBoundingClientRect(); if (!R.width) return null;
140
+ const I = { l: 0, r: 0, t: Math.max(0, bar.getBoundingClientRect().bottom - R.top) + 8, b: 0 };
141
+ if (!panel.hidden) { const p = panel.getBoundingClientRect(); if (p.width < R.width * .5) I.l = p.right - R.left + 8; }
142
+ if (!card.hidden) { const k = card.getBoundingClientRect(); if (k.width < R.width * .5) I.r = R.right - k.left + 8; }
143
+ const g = legend.getBoundingClientRect(); if (g.height && g.height < R.height * .3) I.b = R.bottom - g.top + 4;
144
+ return I;
145
+ }
146
+ const insets = () => { const c = cloud(); if (c) c.setInsets(measure()); };
147
+
148
+ /* ---------- the filter panel ---------- */
149
+ let frameTimer = 0;
150
+ function apply({ rebuild = false } = {}) {
151
+ save();
152
+ const c = cloud();
153
+ if (c) {
154
+ const before = c.counts().visible;
155
+ c.setFilter(prefs.filter);
156
+ // the camera follows a filter once it settles (not on every key typed)
157
+ clearTimeout(frameTimer);
158
+ if (c.counts().visible !== before) frameTimer = setTimeout(() => { insets(); c.resetView(); }, 650);
159
+ }
160
+ if (rebuild) buildPanel(true);
161
+ syncBar();
162
+ if (S.mode !== '3d') renderList();
163
+ refreshCard();
164
+ }
165
+ function chips(label, entries, get, set) {
166
+ const box = el('fieldset', undefined, 'cloud-field');
167
+ box.append(el('legend', label));
168
+ const row = el('div', undefined, 'cloud-chips');
169
+ for (const [value, text, cnt, st] of entries) {
170
+ const b = button(cnt === undefined ? text : `${text} ${cnt}`, 'cloud-chip' + (st ? ' st-' + st : ''));
171
+ b.dataset.key = label + ':' + value;
172
+ b.setAttribute('aria-pressed', String(get().includes(value)));
173
+ b.addEventListener('click', () => {
174
+ const cur = get(), on = !cur.includes(value);
175
+ set(on ? cur.concat(value) : cur.filter(v => v !== value));
176
+ b.setAttribute('aria-pressed', String(on)); apply();
177
+ });
178
+ row.append(b);
179
+ }
180
+ box.append(row); return box;
181
+ }
182
+ function select(label, key, entries, get, set) {
183
+ const l = el('label', undefined, 'cloud-select'); l.append(el('span', label));
184
+ const s = el('select'); s.dataset.key = key;
185
+ for (const [v, t] of entries) { const o = el('option', t); o.value = String(v); if (String(get()) === String(v)) o.selected = true; s.append(o); }
186
+ s.addEventListener('change', () => { set(s.value); apply(); });
187
+ l.append(s); return l;
188
+ }
189
+ function checklist(label, items, { get, set, onlyLabel = 'only', searchable = false }) {
190
+ const box = el('fieldset', undefined, 'cloud-field');
191
+ const head = el('legend', label); box.append(head);
192
+ const tools = el('div', undefined, 'cloud-check-tools');
193
+ const all = button('All', 'linkish', () => { set([]); apply({ rebuild: true }); });
194
+ tools.append(all);
195
+ let q = null;
196
+ if (searchable && items.length > 8) {
197
+ q = el('input'); q.type = 'search'; q.placeholder = `Find ${label.toLowerCase()}…`; q.dataset.key = label + ':find'; q.setAttribute('aria-label', `Find ${label.toLowerCase()}`);
198
+ tools.append(q);
199
+ }
200
+ box.append(tools);
201
+ const ul = el('ul', undefined, 'cloud-checks');
202
+ const ids = items.map(i => i.id);
203
+ for (const it of items) {
204
+ const li = el('li'); li.dataset.name = it.name.toLowerCase();
205
+ const lab = el('label');
206
+ const c = el('input'); c.type = 'checkbox'; c.dataset.key = label + ':' + it.id;
207
+ c.checked = !get().length || get().includes(it.id);
208
+ c.addEventListener('change', () => {
209
+ let cur = get().length ? get().slice() : ids.slice();
210
+ cur = c.checked ? [...new Set(cur.concat(it.id))] : cur.filter(x => x !== it.id);
211
+ if (!cur.length) { c.checked = true; say(`One of the ${label.toLowerCase()} stays chosen: "All" shows every one.`); return; }
212
+ set(cur.length === ids.length ? [] : cur);
213
+ apply({ rebuild: !cur.length || cur.length === ids.length });
214
+ });
215
+ lab.append(c, el('span', it.name, 'cloud-check-name'), el('span', String(it.count), 'muted'));
216
+ lab.title = it.title || it.name;
217
+ const only = button(onlyLabel, 'linkish cloud-only', () => { set([it.id]); apply({ rebuild: true }); }, `Show ${it.name} only`);
218
+ li.append(lab, only); ul.append(li);
219
+ }
220
+ if (q) q.addEventListener('input', () => { const t = q.value.trim().toLowerCase(); for (const li of ul.children) li.hidden = !!t && !li.dataset.name.includes(t); });
221
+ box.append(ul); return box;
222
+ }
223
+ function buildPanel(keep) {
224
+ const active = keep && panel.contains(document.activeElement) ? document.activeElement.dataset.key : null, top = panel.scrollTop;
225
+ const f = prefs.filter, O = CM.options(S.facts), P = CM.projects(S.runs);
226
+ panel.replaceChildren();
227
+ const head = el('div', undefined, 'cloud-panel-head');
228
+ head.append(el('h2', 'Filters'), button('Reset', 'linkish', () => { prefs.filter = CM.emptyFilter(); apply({ rebuild: true }); }, 'Show every run'));
229
+ panel.append(head);
230
+ const text = el('input'); text.type = 'search'; text.value = f.text; text.placeholder = 'goal, repo, run or task id, title, model…'; text.dataset.key = 'text';
231
+ text.setAttribute('aria-label', 'Search the runs');
232
+ text.addEventListener('input', () => { f.text = text.value; apply(); });
233
+ panel.append(text);
234
+ panel.append(checklist('Projects', P.map(p => ({ id: p.id, name: p.name, count: p.count, title: p.repo || p.name })), { get: () => f.projects, set: v => { f.projects = v; }, searchable: true }));
235
+ panel.append(chips('State', STATES.filter(([k]) => O.states[k]).map(([k, label, st]) => [k, label, O.states[k], st]), () => f.states, v => { f.states = v; }));
236
+ if (Object.keys(O.kinds).length > 1) panel.append(chips('Kind', Object.entries(O.kinds).map(([k, n]) => [k, k, n]), () => f.kinds, v => { f.kinds = v; }));
237
+ const dBox = chips('Difficulty', ['low', 'medium', 'high', 'none'].filter(k => O.difficulty[k]).map(k => [k, k === 'none' ? 'not set' : k, O.difficulty[k]]), () => f.difficulty.levels, v => { f.difficulty.levels = v; });
238
+ dBox.append(select('Match', 'difficulty-mode', [['contains', 'has a task of'], ['dominant', 'mostly']], () => f.difficulty.mode, v => { f.difficulty.mode = v === 'dominant' ? 'dominant' : 'contains'; }));
239
+ panel.append(dBox);
240
+ const tBox = el('fieldset', undefined, 'cloud-field'); tBox.append(el('legend', 'Tasks per run'));
241
+ const range = el('div', undefined, 'cloud-range');
242
+ for (const [k, label] of [['min', 'at least'], ['max', 'at most']]) {
243
+ const l = el('label'); l.append(el('span', label));
244
+ const i = el('input'); i.type = 'number'; i.min = '1'; i.max = String(Math.max(1, O.maxTasks)); i.step = '1'; i.dataset.key = 'tasks-' + k; i.inputMode = 'numeric';
245
+ i.value = f.tasks[k] == null ? '' : String(f.tasks[k]); i.placeholder = k === 'min' ? '1' : String(O.maxTasks);
246
+ i.addEventListener('input', () => { const v = i.value === '' ? null : Math.max(1, Math.floor(Number(i.value))); f.tasks[k] = Number.isFinite(v) ? v : null; apply(); });
247
+ l.append(i); range.append(l);
248
+ }
249
+ tBox.append(range); panel.append(tBox);
250
+ if (O.aliases.length) {
251
+ const mBox = checklist('Models', O.aliases.map(a => ({ id: a.alias, name: a.alias, count: a.count })), { get: () => f.models.aliases, set: v => { f.models.aliases = v; }, searchable: true });
252
+ mBox.append(select('As', 'models-role', [['any', 'any role'], ['dwarf', 'implementer (dwarf)'], ['qa', 'reviewer (qa)']], () => f.models.role, v => { f.models.role = ['dwarf', 'qa'].includes(v) ? v : 'any'; }));
253
+ panel.append(mBox);
254
+ }
255
+ const more = el('fieldset', undefined, 'cloud-field cloud-more'); more.append(el('legend', 'More'));
256
+ more.append(select('Active', 'active', [[0, 'any time'], [1, 'in the last day'], [7, 'in the last week'], [30, 'in the last month'], [90, 'in the last 3 months']], () => f.activeDays, v => { f.activeDays = Number(v) || 0; }));
257
+ more.append(select('Final review', 'review', [['', 'any'], ['PASS', 'passed'], ['FAIL', 'failed'], ['none', 'no verdict']], () => f.review, v => { f.review = v; }));
258
+ more.append(select('Verification', 'verification', [['', 'any'], ['PASS', 'passed'], ['FAIL', 'failed'], ['none', 'not run']], () => f.verification, v => { f.verification = v; }));
259
+ const dec = Object.keys(O.decompose);
260
+ if (dec.length > 1) more.append(chips('Decompose', dec.map(k => [k, k === 'none' ? 'off' : k, O.decompose[k]]), () => f.decompose, v => { f.decompose = v; }));
261
+ panel.append(more);
262
+ if (active) { const n = [...panel.querySelectorAll('[data-key]')].find(x => x.dataset.key === active); if (n) n.focus({ preventScroll: true }); }
263
+ panel.scrollTop = top;
264
+ }
265
+
266
+ /* ---------- the legend ---------- */
267
+ const legendHow = el('p', '', 'cloud-legend-how');
268
+ function legendText() {
269
+ legendHow.textContent = `${CM.LAYOUT_INFO[prefs.layout].blurb} Size is the number of tasks. Drag a run to move it (Shift: depth, Alt: height); double-click opens it. Keys: arrows orbit, + − zoom, F frame, P projects, [ ] runs, L layout, B shape, 0 reset.`;
270
+ }
271
+ {
272
+ const row = el('ul', undefined, 'cloud-legend-states');
273
+ for (const [, label, st] of STATES) { const li = el('li'); li.append(el('i', undefined, 'led st-' + st), document.createTextNode(label)); row.append(li); }
274
+ legend.append(row, legendHow); legendText();
275
+ }
276
+
277
+ /* ---------- the run card ---------- */
278
+ function refreshCard() {
279
+ const key = S.sel, f = key && S.byKey.get(key);
280
+ card.hidden = !f;
281
+ if (!f) { card.replaceChildren(); insets(); return; }
282
+ const r = f.run, c = cloud(), proj = CM.projects(S.runs).find(p => p.id === f.project);
283
+ const shown = CM.match(f, prefs.filter, nowSec());
284
+ card.replaceChildren();
285
+ const head = el('div', undefined, 'cloud-card-head');
286
+ head.append(el('span', STATE[f.state] ? STATE[f.state].label : f.state, 'chip st-' + (STATE[f.state] || { st: 'unknown' }).st),
287
+ el('span', `${proj ? proj.name : RM.basename(r.repo)} · ${r.kind || 'plan'}`, 'muted'));
288
+ head.append(button('×', 'cloud-card-close', () => ctx.route({ sel: null, focus: S.focus }), 'Close (Esc)'));
289
+ card.append(head, el('h2', String(r.goal || 'Untitled run').split('\n')[0]), el('p', r.run_id || '', 'mono muted'));
290
+ const leds = el('span', undefined, 'leds');
291
+ for (const t of list(r.task_statuses).slice(0, 120)) { const l = el('i', undefined, 'led st-' + String(t.status || 'pending').toLowerCase()); l.title = `${t.id}: ${t.status}${t.title ? ' · ' + t.title : ''}`; leds.append(l); }
292
+ card.append(leds);
293
+ const dl = el('dl', undefined, 'fields');
294
+ const mix = ['high', 'medium', 'low', 'none'].filter(k => f.diff[k]).map(k => `${f.diff[k]} ${k === 'none' ? 'unset' : k}`).join(' · ');
295
+ for (const [k, v, mono] of [['Tasks', `${f.n}${mix ? ' · ' + mix : ''}`], ['Implementers', [...f.dwarf].join(', ') || '—', true], ['Reviewers', [...f.qa].join(', ') || '—', true],
296
+ ['Started', when(r.created)], ['Updated', when(r.updated)], ['Final review', r.final_review || '—'], ['Verification', r.verification_status || '—'], ['Decompose', r.decompose_level || 'off']]) {
297
+ dl.append(el('dt', k), el('dd', v, mono ? 'mono' : ''));
298
+ }
299
+ card.append(dl);
300
+ if (!shown) card.append(el('p', 'Hidden by the current filters.', 'warn'));
301
+ const acts = el('div', undefined, 'cloud-card-actions');
302
+ const open = el('a', 'Open run', 'button primary'); open.href = '#/runs/' + encodeURIComponent(key);
303
+ acts.append(open);
304
+ if (c && shown) acts.append(button('Focus', '', () => c.focusRun(key), 'Bring the camera to this run (F)'));
305
+ if (c && c.isPinned(key)) acts.append(button('Reset position', '', () => { const undo = c.resetPositions('run', key); refreshCard(); syncBar(); say('The run went back to its place.', 'Undo', () => { undo(); refreshCard(); syncBar(); }); }, 'Put this run back where the layout places it'));
306
+ if (!(prefs.filter.projects.length === 1 && prefs.filter.projects[0] === f.project)) acts.append(button('Only this project', '', () => { prefs.filter.projects = [f.project]; apply({ rebuild: true }); }));
307
+ card.append(acts);
308
+ if (c && c.isPinned(key)) card.append(el('p', `Moved by you in ${CM.LAYOUT_INFO[prefs.layout].name}.`, 'muted'));
309
+ insets();
310
+ }
311
+
312
+ /* ---------- messages ---------- */
313
+ function say(text, action, fn) {
314
+ clearTimeout(S.toastTimer);
315
+ toast.replaceChildren(el('span', text));
316
+ if (action) toast.append(button(action, 'linkish', () => { toast.hidden = true; fn(); }));
317
+ toast.hidden = false;
318
+ S.toastTimer = setTimeout(() => { toast.hidden = true; }, action ? 9000 : 5000);
319
+ }
320
+
321
+ /* ---------- without 3D: the same filtered runs as a list ---------- */
322
+ function renderList() {
323
+ listHost.replaceChildren();
324
+ const head = el('div', undefined, 'cloud-list-head');
325
+ if (S.mode === 'failed') head.append(el('p', S.note || '3D view unavailable.', 'warn'));
326
+ else if (S.mode === 'loading') head.append(el('p', 'Loading the 3D view…', 'muted'));
327
+ else {
328
+ head.append(el('p', 'The cloud is a 3D view. These are the runs it would show.', 'muted'));
329
+ head.append(button('Load 3D', 'primary', () => ctx.load3d && ctx.load3d(), 'Load three.js from cdn.jsdelivr.net and show the cloud'));
330
+ }
331
+ listHost.append(head);
332
+ const shown = S.facts.filter(f => CM.match(f, prefs.filter, nowSec())).sort((a, b) => b.t - a.t);
333
+ const P = CM.projects(S.runs);
334
+ let n = 0;
335
+ for (const p of P) {
336
+ const mine = shown.filter(f => f.project === p.id); if (!mine.length) continue;
337
+ const g = el('section', undefined, 'map-repo'); g.append(el('h3', `${p.name} · ${mine.length}`));
338
+ const grid = el('div', undefined, 'map-grid');
339
+ for (const f of mine) {
340
+ if (n++ >= LIST_CAP) break;
341
+ const r = f.run, a = el('a', undefined, 'map-card st-' + (STATE[f.state] || { st: 'unknown' }).st);
342
+ a.href = '#/runs/' + encodeURIComponent(r.key);
343
+ a.append(el('strong', String(r.goal || 'Untitled run').split('\n')[0]), el('span', `${r.run_id || ''} · ${f.n} task${f.n === 1 ? '' : 's'}${r.kind === 'solo' ? ' · solo' : ''}`, 'mono muted'));
344
+ const leds = el('span', undefined, 'leds');
345
+ for (const t of list(r.task_statuses).slice(0, 60)) leds.append(el('i', undefined, 'led st-' + String(t.status || 'pending').toLowerCase()));
346
+ a.append(leds); grid.append(a);
347
+ }
348
+ g.append(grid); listHost.append(g);
349
+ }
350
+ if (n > LIST_CAP) listHost.append(el('p', `${n - LIST_CAP} more: narrow the filters to see them.`, 'muted'));
351
+ if (!shown.length) listHost.append(el('p', S.facts.length ? 'No runs match these filters.' : 'No runs found.', 'empty'));
352
+ }
353
+
354
+ /* ---------- render: data from the app, the stage when there is one ---------- */
355
+ function render({ runs, sel = null, focus = null, mode = '2d', stage = null, note = '' }) {
356
+ const newRuns = runs !== S.runsRef;
357
+ if (newRuns) {
358
+ S.runsRef = runs; S.runs = list(runs);
359
+ S.facts = S.runs.map(r => CM.runFacts(r, nowSec())); S.byKey = new Map(S.facts.map(f => [f.key, f]));
360
+ }
361
+ const modeChanged = mode !== S.mode || stage !== S.stage;
362
+ S.mode = mode; S.stage = stage; S.note = note;
363
+ root.classList.toggle('cloud-3d', mode === '3d');
364
+ listHost.hidden = mode === '3d';
365
+ const optSig = JSON.stringify([CM.projects(S.runs).map(p => [p.id, p.count]), CM.options(S.facts)]);
366
+ if (optSig !== S.optSig) { S.optSig = optSig; buildPanel(true); }
367
+ const selChanged = sel !== S.sel, focusChanged = focus !== S.focus;
368
+ S.sel = sel; S.focus = focus;
369
+ if (mode === '3d' && stage) {
370
+ const c = stage.cloud;
371
+ if (!c || !S.shown || modeChanged || stage.view !== 'cloud') {
372
+ syncBar(); refreshCard();
373
+ stage.showCloud(S.runs, { filter: prefs.filter, layout: prefs.layout, shape: prefs.shape, sel, focus, inset: measure() });
374
+ if (!S.shown && sel) stage.cloud.focusRun(sel);
375
+ S.shown = true;
376
+ } else {
377
+ if (newRuns) c.update(S.runs);
378
+ if (focusChanged) c.focusProject(focus);
379
+ if (selChanged) c.select(sel);
380
+ }
381
+ }
382
+ if (mode !== '3d') renderList();
383
+ syncBar(); refreshCard();
384
+ }
385
+
386
+ // keys and clicks on the stage come back through the app (cloud callbacks)
387
+ return {
388
+ render, setLayout, setShape, refreshCard: () => { syncBar(); refreshCard(); },
389
+ setFilter(f) { prefs.filter = CM.normFilter(f); apply({ rebuild: true }); },
390
+ get prefs() { return prefs; },
391
+ };
392
+ }
393
+ return { create };
394
+ })();
@@ -0,0 +1,216 @@
1
+ /* forge-ui diff viewer: a task's diff (current, previous, committed, or exactly what one reviewer read), a final
2
+ review's combined diff or a Fractal node's candidate, with each review finding shown under the line it points
3
+ at. Defines window.ForgeDiff.render(host, ctx). All API values enter the DOM as text.
4
+ ctx = { run, spec: {kind: 'task'|'final'|'fractal', task, source, file, name, node}, fetchJSON(path),
5
+ rerender(), href(spec) → hash, closeHref }. */
6
+ window.ForgeDiff = (() => {
7
+ 'use strict';
8
+ const list = v => Array.isArray(v) ? v : [];
9
+ const SEV = { P0: 'fail', P1: 'conflict', P2: 'timeout', P3: 'unknown' };
10
+ const RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
11
+ const BUDGET = 4000; // lines drawn before the rest of the files wait for a click
12
+ const cache = new Map(); // url → {data, error, pending, fresh}
13
+ const opened = new Set(); // files shown past the budget, by url + path
14
+ function el(tag, text, cls) {
15
+ const n = document.createElement(tag);
16
+ if (text !== undefined && text !== null) n.textContent = String(text);
17
+ if (cls) n.className = cls;
18
+ return n;
19
+ }
20
+ const chip = (text, st) => el('span', text, 'chip st-' + st);
21
+ const enc = encodeURIComponent;
22
+ const TASK_FILES = new Set(['current', 'prev', 'committed', 'existing']);
23
+ function urlFor(run, spec) {
24
+ const base = `/api/runs/${enc(run.key)}`;
25
+ if (spec.kind === 'final') return `${base}/final-reviews/${enc(spec.name)}/diff`;
26
+ if (spec.kind === 'fractal') return `${base}/tasks/${enc(spec.task)}/fractal/nodes/${enc(spec.node)}/diff`;
27
+ const src = spec.source || 'current';
28
+ if (TASK_FILES.has(src)) return `${base}/tasks/${enc(spec.task)}/diff${src === 'current' ? '' : '/' + src}`;
29
+ return `${base}/executions/${enc(src)}/diff`;
30
+ }
31
+ function load(ctx, url) {
32
+ const c = cache.get(url), fresh = ctx.run.updated;
33
+ if (c && (c.fresh === fresh || c.pending)) return c;
34
+ const entry = { pending: true, fresh, data: c && c.data || null, error: null };
35
+ cache.set(url, entry);
36
+ ctx.fetchJSON(url).then(d => { entry.data = d; entry.error = null; entry.at = performance.now(); }, e => { entry.error = e; }).finally(() => { entry.pending = false; ctx.rerender(); });
37
+ return entry;
38
+ }
39
+ const pathOf = f => f.new || f.old || '';
40
+ const worst = fs => fs.reduce((w, f) => (w === null || RANK[f.severity] < RANK[w] ? f.severity : w), null);
41
+ // what a finding says after its "- [P1][CONFIRMED] path:line —" head
42
+ function said(f) {
43
+ const s = String(f.summary || f.text || '').split('\n')[0].replace(/^\s*-\s*\[P\d\]\s*(\[[A-Z]+\]\s*)?/, '');
44
+ const i = s.indexOf(' — '); return i >= 0 && i < 200 ? s.slice(i + 3) : s;
45
+ }
46
+
47
+ /* ---------- one finding, as a row under its line or in a list ---------- */
48
+ function findingBox(f, where) {
49
+ const box = el('div', undefined, 'diff-finding sev-' + (f.severity || 'P3'));
50
+ const head = el('div', undefined, 'diff-finding-head');
51
+ head.append(chip(f.severity || '?', SEV[f.severity] || 'unknown'));
52
+ if (f.status) head.append(el('span', f.status.toLowerCase(), 'tag'));
53
+ const r = f.review || {};
54
+ head.append(el('span', `${r.kind || 'review'}${r.attempt != null ? ' · attempt ' + r.attempt : ''}${r.spec ? ' · ' + r.spec : ''}`, 'muted'));
55
+ if (where) head.append(el('span', where, 'mono muted'));
56
+ box.append(head, el('p', said(f)));
57
+ const more = String(f.text || '').trim();
58
+ if (more && more !== String(f.summary || '').trim()) { const d = el('details'); d.append(el('summary', 'Details'), el('pre', more)); box.append(d); }
59
+ return box;
60
+ }
61
+
62
+ /* ---------- a file: its header, hunks with line numbers, and pinned findings ---------- */
63
+ function fileBlock(ctx, D, f, fi, pins, state) {
64
+ const sec = el('section', undefined, 'diff-file'); sec.id = 'diff-file-' + fi; sec.dataset.path = pathOf(f);
65
+ const head = el('div', undefined, 'diff-file-head');
66
+ const st = { modified: 'running', added: 'merged', deleted: 'fail', renamed: 'nochanges', copied: 'nochanges' }[f.status] || 'unknown';
67
+ head.append(chip(f.status, st));
68
+ const name = el('a', f.status === 'renamed' || f.status === 'copied' ? `${f.old} → ${f.new}` : pathOf(f), 'mono diff-path');
69
+ name.href = ctx.href({ ...ctx.spec, file: pathOf(f) });
70
+ head.append(name, el('span', `+${f.additions} −${f.deletions}`, 'diff-stat'));
71
+ if (f.binary) head.append(el('span', 'binary', 'tag'));
72
+ if (f.truncated) head.append(el('span', 'cut short', 'tag'));
73
+ sec.append(head);
74
+ const fileLevel = pins.filter(p => p.pin.hunk === null || p.pin.hunk === undefined);
75
+ if (fileLevel.length) {
76
+ const fl = el('div', undefined, 'diff-file-findings');
77
+ for (const p of fileLevel) fl.append(findingBox(p.f, p.f.off_diff === 'line outside the changed hunks' ? `line ${p.pin.line}, outside the changed lines` : 'this file'));
78
+ sec.append(fl);
79
+ }
80
+ if (f.binary) { sec.append(el('p', 'A binary file: no lines to show.', 'muted diff-note')); return sec; }
81
+ const lines = f.hunks.reduce((a, h) => a + h.lines.length, 0), key = D.__url + '\n' + pathOf(f);
82
+ if (state.drawn + lines > BUDGET && !opened.has(key) && state.drawn > 0) {
83
+ const b = el('button', `Show this file (${lines} lines)`, 'diff-more'); b.type = 'button';
84
+ b.addEventListener('click', () => { opened.add(key); ctx.rerender(); });
85
+ sec.append(b); return sec;
86
+ }
87
+ state.drawn += lines;
88
+ const at = new Map(); // "hunk:index" → findings
89
+ for (const p of pins) if (p.pin.hunk != null) { const k = p.pin.hunk + ':' + (p.pin.index ?? 'end'); if (!at.has(k)) at.set(k, []); at.get(k).push(p); }
90
+ const table = el('table', undefined, 'diff-table'); table.setAttribute('role', 'presentation');
91
+ const body = el('tbody');
92
+ f.hunks.forEach((h, hi) => {
93
+ const hr = el('tr', undefined, 'diff-hunk');
94
+ const hc = el('td', `@@ -${h.old_start},${h.old_lines} +${h.new_start},${h.new_lines} @@${h.section ? ' ' + h.section : ''}`); hc.colSpan = 4; hr.append(hc); body.append(hr);
95
+ let o = h.old_start, n = h.new_start;
96
+ h.lines.forEach((ln, li) => {
97
+ const m = ln[0], tr = el('tr', undefined, 'diff-line ' + (m === '+' ? 'add' : m === '-' ? 'del' : m === '\\' ? 'meta' : 'ctx'));
98
+ const on = m === '+' || m === '\\' ? '' : o++, nn = m === '-' || m === '\\' ? '' : n++;
99
+ tr.append(el('td', on, 'ln'), el('td', nn, 'ln'), el('td', m === '\\' ? '' : m, 'mk'), el('td', m === '\\' ? ln.slice(1).trim() : ln.slice(1), 'code'));
100
+ body.append(tr);
101
+ const here = at.get(hi + ':' + li);
102
+ if (here) for (const p of here) body.append(pinRow(p, p.pin.line));
103
+ });
104
+ const tail = at.get(hi + ':end');
105
+ if (tail) for (const p of tail) body.append(pinRow(p, p.pin.line));
106
+ });
107
+ table.append(body);
108
+ const scroll = el('div', undefined, 'diff-scroll'); scroll.append(table); sec.append(scroll);
109
+ return sec;
110
+ }
111
+ function pinRow(p, line) {
112
+ const tr = el('tr', undefined, 'diff-pin'), td = el('td'); td.colSpan = 4;
113
+ const box = findingBox(p.f, `line ${line}`); box.id = 'diff-finding-' + p.i; box.tabIndex = -1; td.append(box); tr.append(td);
114
+ return tr;
115
+ }
116
+
117
+ /* ---------- the viewer ---------- */
118
+ function render(host, ctx) {
119
+ const run = ctx.run, spec = ctx.spec, url = urlFor(run, spec), entry = load(ctx, url);
120
+ // a poll that brought nothing new leaves thousands of rows alone
121
+ const sig = [url, entry.at || 0, !!entry.error, spec.file, opened.size].join('|');
122
+ if (host.diffSig === sig) return;
123
+ host.diffSig = sig;
124
+ host.replaceChildren();
125
+ const D = entry.data;
126
+ const top = el('div', undefined, 'diff-top');
127
+ const what = spec.kind === 'final' ? 'Final review' : spec.kind === 'fractal' ? `${spec.task} · Fractal node ${spec.node}` : spec.task === 'solo' && run.kind === 'solo' ? 'Solo run' : `Task ${spec.task}`;
128
+ const title = el('h2'); title.append(el('span', 'Diff · ', 'muted'), document.createTextNode(what)); top.append(title);
129
+ const close = el('a', 'Close ✕', 'diff-close'); close.href = ctx.closeHref; top.append(close);
130
+ host.append(top);
131
+ if (!D) {
132
+ host.append(el('p', entry.error ? (entry.error.snapshot ? 'This diff is not in this export.' : `Could not load this diff (${entry.error.status === 404 ? 'not found' : entry.error.message}).`) : 'Loading the diff…', entry.error ? 'warn' : 'muted'));
133
+ return;
134
+ }
135
+ D.__url = url;
136
+ // which diff: the files of the task and the diffs each reviewer read
137
+ const sources = list(D.sources);
138
+ if (sources.length > 1) {
139
+ const bar = el('div', undefined, 'diff-sources'); bar.setAttribute('role', 'group'); bar.setAttribute('aria-label', 'Which diff');
140
+ const cur = spec.kind === 'final' ? spec.name : spec.kind === 'fractal' ? 'candidate' : spec.source || 'current';
141
+ for (const s of sources) {
142
+ const a = el('a', s.label || s.id, 'diff-source'); a.setAttribute('aria-current', String(s.id === cur));
143
+ a.href = spec.kind === 'final' ? ctx.href({ kind: 'final', name: s.id }) : ctx.href({ ...spec, source: s.id, file: null });
144
+ if (s.verdict) a.append(document.createTextNode(' '), chip(s.verdict, String(s.verdict).toLowerCase()));
145
+ a.title = `${s.bytes != null ? s.bytes + ' bytes' : ''}${s.kind ? ' · ' + s.kind : ''}`;
146
+ bar.append(a);
147
+ }
148
+ host.append(bar);
149
+ }
150
+ const src = D.source || {};
151
+ const meta = el('p', undefined, 'diff-meta muted');
152
+ const files = list(D.files), adds = files.reduce((a, f) => a + (f.additions || 0), 0), dels = files.reduce((a, f) => a + (f.deletions || 0), 0);
153
+ const finds = list(D.findings).map((f, i) => ({ f, i, pin: f.pin || null }));
154
+ const pinned = finds.filter(p => p.pin && p.pin.file != null && p.pin.hunk != null && !p.f.off_diff);
155
+ meta.textContent = [src.file, src.attempt != null ? `attempt ${src.attempt}` : null, D.available ? `${files.length} file${files.length === 1 ? '' : 's'} · +${adds} −${dels}` : null,
156
+ finds.length ? `${finds.length} finding${finds.length === 1 ? '' : 's'}, ${pinned.length} on a changed line` : null].filter(Boolean).join(' · ');
157
+ host.append(meta);
158
+ if (D.truncated) host.append(el('p', 'This diff was cut short (over 8 MiB, 300 files or 20,000 lines): what is shown is its beginning.', 'warn'));
159
+ if (!D.available) { host.append(el('p', `No diff to show: ${D.reason || 'not available'}.`, 'muted diff-note')); }
160
+ const byFile = new Map();
161
+ for (const p of finds) if (p.pin && p.pin.file != null && (p.f.off_diff === null || p.f.off_diff === 'line outside the changed hunks')) { if (!byFile.has(p.pin.file)) byFile.set(p.pin.file, []); byFile.get(p.pin.file).push(p); }
162
+ const loose = finds.filter(p => !(p.pin && p.pin.file != null && (p.f.off_diff === null || p.f.off_diff === 'line outside the changed hunks')));
163
+
164
+ const body = el('div', undefined, 'diff-body');
165
+ // the file list, with what changed and how many findings point into each
166
+ const nav = el('nav', undefined, 'diff-files'); nav.setAttribute('aria-label', 'Files in this diff');
167
+ const ul = el('ul');
168
+ files.forEach((f, fi) => {
169
+ const li = el('li'), a = el('a', undefined, 'diff-file-link'); a.href = ctx.href({ ...spec, file: pathOf(f) }); a.dataset.fi = String(fi);
170
+ const p = pathOf(f), cut = p.lastIndexOf('/');
171
+ a.append(el('span', cut >= 0 ? p.slice(0, cut + 1) : '', 'diff-dir'), el('span', cut >= 0 ? p.slice(cut + 1) : p, 'diff-base'));
172
+ const stat = el('span', undefined, 'diff-stat'); stat.append(el('span', `+${f.additions}`, 'diff-add'), document.createTextNode(' '), el('span', `−${f.deletions}`, 'diff-del'));
173
+ a.append(stat);
174
+ const ps = byFile.get(fi) || [];
175
+ if (ps.length) { const w = worst(ps.map(x => x.f)); a.append(chip(`${ps.length}`, SEV[w] || 'unknown')); a.title = `${ps.length} finding${ps.length === 1 ? '' : 's'}, worst ${w}`; }
176
+ if (spec.file && spec.file === p) a.setAttribute('aria-current', 'true');
177
+ li.append(a); ul.append(li);
178
+ });
179
+ nav.append(ul);
180
+ if (loose.length) { const a = el('a', `Not on a changed line (${loose.length})`, 'diff-file-link diff-loose-link'); a.href = '#diff-loose'; a.addEventListener('click', e => { e.preventDefault(); host.querySelector('#diff-loose')?.scrollIntoView({ block: 'start' }); }); nav.append(a); }
181
+ const main = el('div', undefined, 'diff-main');
182
+ const state = { drawn: 0 };
183
+ files.forEach((f, fi) => main.append(fileBlock(ctx, D, f, fi, byFile.get(fi) || [], state)));
184
+ if (loose.length) {
185
+ const ls = el('section', undefined, 'diff-loose'); ls.id = 'diff-loose';
186
+ ls.append(el('h3', 'Findings not on a changed line of this diff'));
187
+ for (const p of loose) { const b = findingBox(p.f, `${p.f.location || 'no location'} · ${p.f.off_diff || 'not located'}`); b.id = 'diff-finding-' + p.i; b.tabIndex = -1; ls.append(b); }
188
+ main.append(ls);
189
+ }
190
+ if (list(D.warnings).length) { const d = el('details', undefined, 'diff-warnings'); d.append(el('summary', `Reader warnings (${D.warnings.length})`), el('pre', D.warnings.join('\n'))); main.append(d); }
191
+ if (files.length || loose.length) body.append(nav); else body.classList.add('no-files');
192
+ body.append(main); host.append(body);
193
+
194
+ // keys: n/p findings, j/k files; the hash keeps the file a link points at
195
+ const order = [...main.querySelectorAll('.diff-finding[id]')];
196
+ let fIdx = -1;
197
+ const fileEls = [...main.querySelectorAll('.diff-file')];
198
+ const visibleFile = () => { let i = 0; fileEls.forEach((s, k) => { if (s.getBoundingClientRect().top <= 90) i = k; }); return i; };
199
+ host.tabIndex = -1; host.setAttribute('aria-label', 'Diff: n and p move between findings, j and k between files');
200
+ host.onkeydown = e => {
201
+ if (e.altKey || e.ctrlKey || e.metaKey || /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return;
202
+ if ((e.key === 'n' || e.key === 'p') && order.length) {
203
+ fIdx = (fIdx + (e.key === 'n' ? 1 : -1) + order.length) % order.length;
204
+ order[fIdx].scrollIntoView({ block: 'center' }); order[fIdx].classList.add('flash'); setTimeout(() => order[fIdx] && order[fIdx].classList.remove('flash'), 900); e.preventDefault();
205
+ } else if ((e.key === 'j' || e.key === 'k') && fileEls.length) {
206
+ const i = Math.max(0, Math.min(fileEls.length - 1, visibleFile() + (e.key === 'j' ? 1 : -1)));
207
+ fileEls[i].scrollIntoView({ block: 'start' }); e.preventDefault();
208
+ }
209
+ };
210
+ if (spec.file) {
211
+ const target = fileEls.find(s => s.dataset.path === spec.file);
212
+ if (target && host.dataset.scrolledTo !== url + '\n' + spec.file) { host.dataset.scrolledTo = url + '\n' + spec.file; requestAnimationFrame(() => target.scrollIntoView({ block: 'start' })); }
213
+ }
214
+ }
215
+ return { render, urlFor, cache };
216
+ })();
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#1c222b"/><path d="M32 12 52 23v20L32 54 12 43V23Z" fill="#ff9d3c"/><path d="M32 12 52 23 32 34 12 23Z" fill="#ffc690"/><path d="M32 34v20L12 43V23Z" fill="#d9730d"/></svg>