@giamme/forge 0.0.0-stage → 0.9.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 (152) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/LICENSE +21 -0
  3. package/README.md +194 -2
  4. package/SKILL.md +123 -0
  5. package/VERSION +1 -0
  6. package/docs/README.md +26 -0
  7. package/docs/add-ons.md +33 -0
  8. package/docs/artifacts.md +69 -0
  9. package/docs/commands.md +315 -0
  10. package/docs/dataset.md +20 -0
  11. package/docs/decomposed-runs.md +268 -0
  12. package/docs/development.md +95 -0
  13. package/docs/forge-ui.md +92 -0
  14. package/docs/fractal.md +181 -0
  15. package/docs/install.md +160 -0
  16. package/docs/memory.md +156 -0
  17. package/docs/models.md +118 -0
  18. package/docs/reviews.md +231 -0
  19. package/docs/safety.md +55 -0
  20. package/docs/troubleshooting.md +54 -0
  21. package/forge +79 -0
  22. package/package.json +27 -4
  23. package/references/dataset.md +329 -0
  24. package/references/decompose.md +667 -0
  25. package/references/fractal-dashboard.md +21 -0
  26. package/references/fractal.md +307 -0
  27. package/references/harnesses.md +365 -0
  28. package/references/jev.md +865 -0
  29. package/references/memory.md +228 -0
  30. package/references/planning.md +42 -0
  31. package/references/ripwire.md +47 -0
  32. package/references/runtime.md +64 -0
  33. package/references/solo.md +113 -0
  34. package/references/ui.md +112 -0
  35. package/registry.tsv +81 -0
  36. package/scripts/forge-artifact.sh +78 -0
  37. package/scripts/forge-contract.py +430 -0
  38. package/scripts/forge-dataset.py +6 -0
  39. package/scripts/forge-dispatch.sh +671 -0
  40. package/scripts/forge-fail-first.py +195 -0
  41. package/scripts/forge-fractal-dispatch.sh +4 -0
  42. package/scripts/forge-fractal-options.sh +54 -0
  43. package/scripts/forge-fractal.py +6 -0
  44. package/scripts/forge-guard.py +266 -0
  45. package/scripts/forge-install-fractal.sh +4 -0
  46. package/scripts/forge-install-ripwire.sh +52 -0
  47. package/scripts/forge-install.sh +135 -0
  48. package/scripts/forge-jev-options.sh +259 -0
  49. package/scripts/forge-jev.py +6 -0
  50. package/scripts/forge-lib-plans.sh +785 -0
  51. package/scripts/forge-lib-review.sh +494 -0
  52. package/scripts/forge-lib-task.sh +302 -0
  53. package/scripts/forge-lib-verify.sh +30 -0
  54. package/scripts/forge-memory.sh +575 -0
  55. package/scripts/forge-metrics.sh +39 -0
  56. package/scripts/forge-parallel-options.sh +76 -0
  57. package/scripts/forge-parallel.sh +1717 -0
  58. package/scripts/forge-plan-check.py +118 -0
  59. package/scripts/forge-prompt.py +45 -0
  60. package/scripts/forge-ripwire.py +136 -0
  61. package/scripts/forge-route.py +344 -0
  62. package/scripts/forge-runs.py +121 -0
  63. package/scripts/forge-runtime.py +586 -0
  64. package/scripts/forge-schedule.py +312 -0
  65. package/scripts/forge-shadow.py +305 -0
  66. package/scripts/forge-solo.sh +347 -0
  67. package/scripts/forge-ui-options.sh +42 -0
  68. package/scripts/forge-ui.py +435 -0
  69. package/scripts/forge-update.sh +71 -0
  70. package/scripts/forge-verify-coverage.py +956 -0
  71. package/scripts/forge-version.py +240 -0
  72. package/scripts/forge_dataset/__init__.py +439 -0
  73. package/scripts/forge_dataset/capture.py +330 -0
  74. package/scripts/forge_dataset/checks.py +109 -0
  75. package/scripts/forge_dataset/cli.py +233 -0
  76. package/scripts/forge_dataset/export.py +1009 -0
  77. package/scripts/forge_dataset/harvest.py +716 -0
  78. package/scripts/forge_dataset/importer.py +412 -0
  79. package/scripts/forge_dataset/outcomes.py +275 -0
  80. package/scripts/forge_dataset/routing.py +278 -0
  81. package/scripts/forge_fractal/__init__.py +84 -0
  82. package/scripts/forge_fractal/bridge.py +46 -0
  83. package/scripts/forge_fractal/cli.py +202 -0
  84. package/scripts/forge_fractal/dashboard.css +138 -0
  85. package/scripts/forge_fractal/dashboard.html +41 -0
  86. package/scripts/forge_fractal/dashboard.js +687 -0
  87. package/scripts/forge_fractal/decomposition.py +262 -0
  88. package/scripts/forge_fractal/execution.py +659 -0
  89. package/scripts/forge_fractal/inspection.py +261 -0
  90. package/scripts/forge_fractal/install.py +158 -0
  91. package/scripts/forge_fractal/invocation.py +94 -0
  92. package/scripts/forge_fractal/pipeline.py +124 -0
  93. package/scripts/forge_fractal/selection.py +179 -0
  94. package/scripts/forge_jev/__init__.py +376 -0
  95. package/scripts/forge_jev/backtest.py +322 -0
  96. package/scripts/forge_jev/cli.py +980 -0
  97. package/scripts/forge_jev/client.py +418 -0
  98. package/scripts/forge_jev/corpus.py +156 -0
  99. package/scripts/forge_jev/coupling.py +244 -0
  100. package/scripts/forge_jev/features.py +24 -0
  101. package/scripts/forge_jev/gates.py +100 -0
  102. package/scripts/forge_jev/memory.py +163 -0
  103. package/scripts/forge_jev/outcome.py +49 -0
  104. package/scripts/forge_jev/predict.py +204 -0
  105. package/scripts/forge_jev/questions.py +661 -0
  106. package/scripts/forge_jev/review.py +73 -0
  107. package/scripts/forge_jev/routing.py +271 -0
  108. package/scripts/forge_jev/subset.py +123 -0
  109. package/scripts/forge_jev/verify.py +453 -0
  110. package/scripts/forge_registry.py +140 -0
  111. package/ui/README.md +347 -0
  112. package/ui/THIRD_PARTY_NOTICES.md +177 -0
  113. package/ui/docs/api.md +970 -0
  114. package/ui/forge_ui/__init__.py +20 -0
  115. package/ui/forge_ui/__main__.py +135 -0
  116. package/ui/forge_ui/diffs.py +454 -0
  117. package/ui/forge_ui/executions.py +436 -0
  118. package/ui/forge_ui/export.py +214 -0
  119. package/ui/forge_ui/findings.py +132 -0
  120. package/ui/forge_ui/fractal.py +582 -0
  121. package/ui/forge_ui/models.py +216 -0
  122. package/ui/forge_ui/reader.py +1424 -0
  123. package/ui/forge_ui/server.py +312 -0
  124. package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
  125. package/ui/forge_ui/static/app.js +1079 -0
  126. package/ui/forge_ui/static/cloud3d.js +647 -0
  127. package/ui/forge_ui/static/cloudmodel.js +375 -0
  128. package/ui/forge_ui/static/cloudui.js +397 -0
  129. package/ui/forge_ui/static/convo.js +295 -0
  130. package/ui/forge_ui/static/datatable.js +232 -0
  131. package/ui/forge_ui/static/diffview.js +235 -0
  132. package/ui/forge_ui/static/favicon.svg +1 -0
  133. package/ui/forge_ui/static/files.js +776 -0
  134. package/ui/forge_ui/static/findings3d.js +134 -0
  135. package/ui/forge_ui/static/gantt.js +97 -0
  136. package/ui/forge_ui/static/graph.js +266 -0
  137. package/ui/forge_ui/static/icons.js +94 -0
  138. package/ui/forge_ui/static/index.html +67 -0
  139. package/ui/forge_ui/static/iso.js +816 -0
  140. package/ui/forge_ui/static/leaderboard.js +311 -0
  141. package/ui/forge_ui/static/map3d.js +45 -0
  142. package/ui/forge_ui/static/modelstats.js +249 -0
  143. package/ui/forge_ui/static/notify.js +179 -0
  144. package/ui/forge_ui/static/panel.js +562 -0
  145. package/ui/forge_ui/static/runmodel.js +667 -0
  146. package/ui/forge_ui/static/scene3d.js +1471 -0
  147. package/ui/forge_ui/static/search.js +120 -0
  148. package/ui/forge_ui/static/shell.js +137 -0
  149. package/ui/forge_ui/static/stagectl.js +150 -0
  150. package/ui/forge_ui/static/style.css +1073 -0
  151. package/ui/forge_ui/static/theme.js +25 -0
  152. package/ui/forge_ui/transcripts.py +608 -0
@@ -0,0 +1,397 @@
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', 'module-title');
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', 'plate'), 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
+ const shut = button('', 'cloud-card-close', () => ctx.route({ sel: null, focus: S.focus }), 'Close (Esc)');
289
+ shut.setAttribute('aria-label', 'Close (Esc)'); if (window.ForgeIcons) shut.append(window.ForgeIcons.icon('close'));
290
+ head.append(shut);
291
+ card.append(head, el('h2', String(r.goal || 'Untitled run').split('\n')[0]), el('p', r.run_id || '', 'mono muted'));
292
+ const leds = el('span', undefined, 'leds');
293
+ 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); }
294
+ card.append(leds);
295
+ const dl = el('dl', undefined, 'fields');
296
+ const mix = ['high', 'medium', 'low', 'none'].filter(k => f.diff[k]).map(k => `${f.diff[k]} ${k === 'none' ? 'unset' : k}`).join(' · ');
297
+ const plates = specs => { if (!specs.length || !window.ForgeIcons) return specs.join(', ') || '—'; const s = el('span', undefined, 'plates'); for (const x of specs) s.append(window.ForgeIcons.modelPlate(x)); return s; };
298
+ for (const [k, v, mono] of [['Tasks', `${f.n}${mix ? ' · ' + mix : ''}`], ['Implementers', plates([...f.dwarf])], ['Reviewers', plates([...f.qa])],
299
+ ['Started', when(r.created)], ['Updated', when(r.updated)], ['Final review', r.final_review || '—'], ['Verification', r.verification_status || '—'], ['Decompose', r.decompose_level || 'off']]) {
300
+ const dd = el('dd', undefined, mono ? 'mono' : ''); dd.append(v instanceof Node ? v : document.createTextNode(String(v))); dl.append(el('dt', k), dd);
301
+ }
302
+ card.append(dl);
303
+ if (!shown) card.append(el('p', 'Hidden by the current filters.', 'warn'));
304
+ const acts = el('div', undefined, 'cloud-card-actions');
305
+ const open = el('a', 'Open run', 'button primary'); open.href = '#/runs/' + encodeURIComponent(key);
306
+ acts.append(open);
307
+ if (c && shown) acts.append(button('Focus', '', () => c.focusRun(key), 'Bring the camera to this run (F)'));
308
+ 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'));
309
+ 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 }); }));
310
+ card.append(acts);
311
+ if (c && c.isPinned(key)) card.append(el('p', `Moved by you in ${CM.LAYOUT_INFO[prefs.layout].name}.`, 'muted'));
312
+ insets();
313
+ }
314
+
315
+ /* ---------- messages ---------- */
316
+ function say(text, action, fn) {
317
+ clearTimeout(S.toastTimer);
318
+ toast.replaceChildren(el('span', text));
319
+ if (action) toast.append(button(action, 'linkish', () => { toast.hidden = true; fn(); }));
320
+ toast.hidden = false;
321
+ S.toastTimer = setTimeout(() => { toast.hidden = true; }, action ? 9000 : 5000);
322
+ }
323
+
324
+ /* ---------- without 3D: the same filtered runs as a list ---------- */
325
+ function renderList() {
326
+ listHost.replaceChildren();
327
+ const head = el('div', undefined, 'cloud-list-head');
328
+ if (S.mode === 'failed') head.append(el('p', S.note || '3D view unavailable.', 'warn'));
329
+ else if (S.mode === 'loading') head.append(el('p', 'Loading the 3D view…', 'muted'));
330
+ else {
331
+ head.append(el('p', 'The cloud is a 3D view. These are the runs it would show.', 'muted'));
332
+ head.append(button('Load 3D', 'primary', () => ctx.load3d && ctx.load3d(), 'Load three.js from cdn.jsdelivr.net and show the cloud'));
333
+ }
334
+ listHost.append(head);
335
+ const shown = S.facts.filter(f => CM.match(f, prefs.filter, nowSec())).sort((a, b) => b.t - a.t);
336
+ const P = CM.projects(S.runs);
337
+ let n = 0;
338
+ for (const p of P) {
339
+ const mine = shown.filter(f => f.project === p.id); if (!mine.length) continue;
340
+ const g = el('section', undefined, 'map-repo'); g.append(el('h3', `${p.name} · ${mine.length}`));
341
+ const grid = el('div', undefined, 'map-grid');
342
+ for (const f of mine) {
343
+ if (n++ >= LIST_CAP) break;
344
+ const r = f.run, a = el('a', undefined, 'map-card st-' + (STATE[f.state] || { st: 'unknown' }).st);
345
+ a.href = '#/runs/' + encodeURIComponent(r.key);
346
+ 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'));
347
+ const leds = el('span', undefined, 'leds');
348
+ for (const t of list(r.task_statuses).slice(0, 60)) leds.append(el('i', undefined, 'led st-' + String(t.status || 'pending').toLowerCase()));
349
+ a.append(leds); grid.append(a);
350
+ }
351
+ g.append(grid); listHost.append(g);
352
+ }
353
+ if (n > LIST_CAP) listHost.append(el('p', `${n - LIST_CAP} more: narrow the filters to see them.`, 'muted'));
354
+ if (!shown.length) listHost.append(el('p', S.facts.length ? 'No runs match these filters.' : 'No runs found.', 'empty'));
355
+ }
356
+
357
+ /* ---------- render: data from the app, the stage when there is one ---------- */
358
+ function render({ runs, sel = null, focus = null, mode = '2d', stage = null, note = '' }) {
359
+ const newRuns = runs !== S.runsRef;
360
+ if (newRuns) {
361
+ S.runsRef = runs; S.runs = list(runs);
362
+ S.facts = S.runs.map(r => CM.runFacts(r, nowSec())); S.byKey = new Map(S.facts.map(f => [f.key, f]));
363
+ }
364
+ const modeChanged = mode !== S.mode || stage !== S.stage;
365
+ S.mode = mode; S.stage = stage; S.note = note;
366
+ root.classList.toggle('cloud-3d', mode === '3d');
367
+ listHost.hidden = mode === '3d';
368
+ const optSig = JSON.stringify([CM.projects(S.runs).map(p => [p.id, p.count]), CM.options(S.facts)]);
369
+ if (optSig !== S.optSig) { S.optSig = optSig; buildPanel(true); }
370
+ const selChanged = sel !== S.sel, focusChanged = focus !== S.focus;
371
+ S.sel = sel; S.focus = focus;
372
+ if (mode === '3d' && stage) {
373
+ const c = stage.cloud;
374
+ if (!c || !S.shown || modeChanged || stage.view !== 'cloud') {
375
+ syncBar(); refreshCard();
376
+ stage.showCloud(S.runs, { filter: prefs.filter, layout: prefs.layout, shape: prefs.shape, sel, focus, inset: measure() });
377
+ if (!S.shown && sel) stage.cloud.focusRun(sel);
378
+ S.shown = true;
379
+ } else {
380
+ if (newRuns) c.update(S.runs);
381
+ if (focusChanged) c.focusProject(focus);
382
+ if (selChanged) c.select(sel);
383
+ }
384
+ }
385
+ if (mode !== '3d') renderList();
386
+ syncBar(); refreshCard();
387
+ }
388
+
389
+ // keys and clicks on the stage come back through the app (cloud callbacks)
390
+ return {
391
+ render, setLayout, setShape, refreshCard: () => { syncBar(); refreshCard(); },
392
+ setFilter(f) { prefs.filter = CM.normFilter(f); apply({ rebuild: true }); },
393
+ get prefs() { return prefs; },
394
+ };
395
+ }
396
+ return { create };
397
+ })();