@giamme/forge 0.9.0 → 0.11.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.
- package/CHANGELOG.md +56 -0
- package/SKILL.md +22 -3
- package/VERSION +1 -1
- package/docs/artifacts.md +28 -2
- package/docs/commands.md +55 -0
- package/docs/dataset.md +22 -0
- package/docs/decomposed-runs.md +54 -2
- package/docs/forge-ui.md +48 -7
- package/docs/memory.md +4 -2
- package/docs/reviews.md +87 -2
- package/forge +31 -1
- package/package.json +1 -1
- package/references/dataset.md +103 -4
- package/references/decompose.md +41 -5
- package/references/harnesses.md +43 -1
- package/references/ui.md +31 -6
- package/scripts/forge-config.py +350 -0
- package/scripts/forge-control.py +649 -0
- package/scripts/forge-dispatch.sh +235 -121
- package/scripts/forge-issues.py +1134 -0
- package/scripts/forge-lib-plans.sh +6 -2
- package/scripts/forge-lib-review.sh +158 -3
- package/scripts/forge-lib-task.sh +358 -41
- package/scripts/forge-parallel-options.sh +3 -1
- package/scripts/forge-parallel.sh +99 -13
- package/scripts/forge-prompt.py +61 -4
- package/scripts/forge-route.py +245 -28
- package/scripts/forge-runs-rm.py +840 -0
- package/scripts/forge-runs.py +19 -1
- package/scripts/forge-runtime.py +47 -1
- package/scripts/forge-schedule.py +227 -11
- package/scripts/forge-ui.py +85 -19
- package/scripts/forge_dataset/__init__.py +84 -8
- package/scripts/forge_dataset/capture.py +5 -2
- package/scripts/forge_dataset/cli.py +44 -1
- package/scripts/forge_dataset/export.py +36 -3
- package/scripts/forge_dataset/forecast.py +355 -0
- package/scripts/forge_dataset/harvest.py +45 -2
- package/scripts/forge_dataset/importer.py +431 -17
- package/scripts/forge_dataset/reviewers.py +145 -0
- package/scripts/forge_dataset/routing.py +1 -0
- package/scripts/forge_jev/__init__.py +7 -3
- package/scripts/forge_registry.py +183 -26
- package/ui/README.md +41 -7
- package/ui/docs/api.md +259 -21
- package/ui/forge_ui/__main__.py +10 -2
- package/ui/forge_ui/control.py +587 -0
- package/ui/forge_ui/export.py +17 -3
- package/ui/forge_ui/reader.py +210 -12
- package/ui/forge_ui/server.py +125 -4
- package/ui/forge_ui/static/app.js +104 -18
- package/ui/forge_ui/static/cloud3d.js +4 -1
- package/ui/forge_ui/static/cloudmodel.js +1 -1
- package/ui/forge_ui/static/cloudui.js +13 -4
- package/ui/forge_ui/static/controls.js +619 -0
- package/ui/forge_ui/static/findingsview.js +438 -0
- package/ui/forge_ui/static/graph.js +6 -5
- package/ui/forge_ui/static/index.html +7 -1
- package/ui/forge_ui/static/leaderboard.js +18 -0
- package/ui/forge_ui/static/modelstats.js +19 -1
- package/ui/forge_ui/static/panel.js +76 -14
- package/ui/forge_ui/static/rundelete.js +388 -0
- package/ui/forge_ui/static/runmodel.js +128 -13
- package/ui/forge_ui/static/scene3d.js +32 -15
- package/ui/forge_ui/static/settings.js +399 -0
- package/ui/forge_ui/static/style.css +184 -5
- package/ui/forge_ui/static/uikit.js +161 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/* forge-ui:
|
|
1
|
+
/* forge-ui: the page. It reads the API; a change to a run goes through the write helpers in uikit.js (see controls.js). All API values enter the DOM as text.
|
|
2
2
|
Views: the map of every run (#/), the run cloud (#/cloud, full screen: cloudui.js over cloud3d.js), and a
|
|
3
3
|
run (#/runs/<key>) with its stage (3D, or the 2D graph and chart), the inspector beside it and the task table under it. The stage is one element that moves between views,
|
|
4
4
|
so the 3D scene (and its WebGL context) survives navigation and refreshes. */
|
|
5
|
-
window.
|
|
5
|
+
window.ForgeApp = (() => {
|
|
6
6
|
'use strict';
|
|
7
7
|
const RM = window.ForgeRunModel;
|
|
8
8
|
// an exported page (forge_ui/export.py): one run and the API responses it needs, no server behind it
|
|
@@ -18,8 +18,9 @@ window.ForgeUI = (() => {
|
|
|
18
18
|
layers: Object.assign({ cables: true, beads: true, lamps: true, roof: true, labels: true, beacons: true, finish: true, districts: true, halos: true, route: true }, store.get('layers', {})),
|
|
19
19
|
replay: null, playing: null, speed: store.get('speed', 60), stage: null, stageFailed: null, stageLoading: false, timer: null,
|
|
20
20
|
lastStatuses: new Map(), announced: 0, rendered: null, cloud: null, cloudUI: null, cloudPrefs: store.get('cloud', {}),
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
settings: false, lastKey: null, settingsRun: null, settingsPickedAt: null, settingsView: null,
|
|
22
|
+
models: false, modelsData: null, modelsText: null, modelsError: null, reviewerOutcomes: null, modelsPrefs: store.get('models', {}),
|
|
23
|
+
files: new Map(), findingsCount: new Map(), filesPrefs: store.get('files', {}), runSort: store.get('run-sort', 'updated'), tablePrefs: store.get('tables', {}),
|
|
23
24
|
findings: new Map() };
|
|
24
25
|
state.stageMode = state.mode === 'exec' ? 'exec' : 'plan'; // the stage's own mode, kept while Files is shown
|
|
25
26
|
if (state.mode === 'files' && !window.ForgeFiles) state.mode = state.stageMode;
|
|
@@ -73,9 +74,11 @@ window.ForgeUI = (() => {
|
|
|
73
74
|
const c = hash.match(/^#\/cloud(?:\/(runs|projects|models)\/([^/]+))?\/?$/);
|
|
74
75
|
if (c) return { key: null, sel: null, cloud: { sel: c[1] === 'runs' ? decodeURIComponent(c[2]) : null, focus: c[1] === 'projects' ? decodeURIComponent(c[2]) : null, model: c[1] === 'models' ? decodeURIComponent(c[2]) : null } };
|
|
75
76
|
if (/^#\/models\/?$/.test(hash)) return { key: null, sel: null, models: true };
|
|
77
|
+
if (/^#\/settings\/?$/.test(hash)) return { key: null, sel: null, settings: true };
|
|
76
78
|
const m = hash.match(/^#\/runs\/([^/]+)(\/.*)?$/);
|
|
77
79
|
if (!m) return { key: null, sel: null };
|
|
78
80
|
const d = decodeURIComponent, rest = m[2] || '';
|
|
81
|
+
if (/^\/findings\/?$/.test(rest)) return { key: d(m[1]), sel: null, findings: true };
|
|
79
82
|
let sel = null, r;
|
|
80
83
|
if ((r = rest.match(/^\/tasks\/([^/]+)\/diff(?:\/(?!file\/|finding\/)([^/]+))?(?:\/finding\/([^/]+))?(?:\/file\/(.+?))?\/?$/))) return { key: d(m[1]), sel: { kind: 'task', id: d(r[1]) }, diff: { kind: 'task', task: d(r[1]), source: r[2] ? d(r[2]) : 'current', finding: r[3] ? d(r[3]) : null, file: r[4] ? d(r[4]) : null } };
|
|
81
84
|
if ((r = rest.match(/^\/tasks\/([^/]+)\/fractal\/([^/]+)\/diff(?:\/file\/(.+?))?\/?$/))) return { key: d(m[1]), sel: { kind: 'task', id: d(r[1]) }, diff: { kind: 'fractal', task: d(r[1]), node: d(r[2]), file: r[3] ? d(r[3]) : null } };
|
|
@@ -103,9 +106,9 @@ window.ForgeUI = (() => {
|
|
|
103
106
|
function link(text, key, sel) { const a = el('a', text); a.href = hashFor(key, sel); return a; }
|
|
104
107
|
|
|
105
108
|
const statuses = new Set(['merged', 'pass', 'running', 'pending', 'fail', 'error', 'infra',
|
|
106
|
-
'timeout', 'unknown', 'invalidated', 'blocked', 'interrupted', 'nochanges', 'conflict']);
|
|
107
|
-
function chip(value) {
|
|
108
|
-
const status = String(value || 'unknown').toLowerCase();
|
|
109
|
+
'timeout', 'unknown', 'invalidated', 'blocked', 'interrupted', 'nochanges', 'conflict', 'accepted', 'outdated']);
|
|
110
|
+
function chip(value, cls) {
|
|
111
|
+
const status = String(cls || value || 'unknown').toLowerCase();
|
|
109
112
|
return el('span', value || '—', 'chip st-' + (statuses.has(status) ? status : 'unknown'));
|
|
110
113
|
}
|
|
111
114
|
function time(value) {
|
|
@@ -171,8 +174,10 @@ window.ForgeUI = (() => {
|
|
|
171
174
|
const active = parent.contains(document.activeElement) ? document.activeElement : null;
|
|
172
175
|
const focusHref = active?.getAttribute('href');
|
|
173
176
|
const own = [parent.scrollLeft, parent.scrollTop];
|
|
177
|
+
const held = window.ForgeControls ? window.ForgeControls.hold(parent) : null; // a control being edited rides through the rebuild
|
|
174
178
|
parent.replaceChildren();
|
|
175
179
|
build();
|
|
180
|
+
if (held) held.restore();
|
|
176
181
|
if (!restore) return;
|
|
177
182
|
for (const node of parent.querySelectorAll('details[data-keep]')) if (open.has(node.dataset.keep)) node.open = open.get(node.dataset.keep);
|
|
178
183
|
for (const node of parent.querySelectorAll('[data-scroll]')) {
|
|
@@ -246,7 +251,8 @@ window.ForgeUI = (() => {
|
|
|
246
251
|
if (!solo) {
|
|
247
252
|
const verification = el('span', undefined, 'verification');
|
|
248
253
|
verification.append(document.createTextNode('Verification '), chip(run.verification_status));
|
|
249
|
-
|
|
254
|
+
const reviewStatus = RM.runReviewStatus(run);
|
|
255
|
+
if (reviewStatus) verification.append(document.createTextNode(' Review '), chip(reviewStatus));
|
|
250
256
|
item.append(verification);
|
|
251
257
|
}
|
|
252
258
|
$('run-list').append(item);
|
|
@@ -333,7 +339,7 @@ window.ForgeUI = (() => {
|
|
|
333
339
|
add('Kind', sel('kind', [['', 'any'], ['plan', 'plan'], ['solo', 'solo']]));
|
|
334
340
|
add('Model', sel('model', [['', 'any'], ...opts.models.map(m => [m, m])]));
|
|
335
341
|
add('Age', sel('ageDays', [[0, 'any'], [1, 'last day'], [7, 'last week'], [30, 'last month']]));
|
|
336
|
-
add('Review', sel('verdict', [['', 'any'], ['PASS', 'passed'], ['FAIL', 'failed'], ['none', 'no verdict']]));
|
|
342
|
+
add('Review', sel('verdict', [['', 'any'], ['PASS', 'passed'], ['FAIL', 'failed'], ['OUTDATED', 'outdated'], ['none', 'no verdict']]));
|
|
337
343
|
const all = el('input'); all.type = 'checkbox'; all.checked = state.showAll; all.dataset.f = 'showAll';
|
|
338
344
|
const allL = el('label', undefined, 'filter check'); allL.append(all, document.createTextNode(' older runs too')); bar.append(allL);
|
|
339
345
|
const reset = el('button', 'Reset'); reset.type = 'button'; reset.addEventListener('click', () => { Object.assign(state.mapFilter, { text: '', state: '', repo: '', kind: '', model: '', ageDays: 0, verdict: '' }); state.showAll = false; filterChanged(true); });
|
|
@@ -363,7 +369,12 @@ window.ForgeUI = (() => {
|
|
|
363
369
|
card.className = `map-card st-${({ live: 'running', failed: 'fail', done: 'merged', pending: 'pending' })[m.state]}${m.match ? '' : ' dim'}`;
|
|
364
370
|
card.append(el('strong', String(r.goal || 'Untitled run').split('\n')[0]), el('span', `${r.run_id || ''}${r.kind === 'solo' ? ' · solo' : ''}`, 'mono muted'));
|
|
365
371
|
const leds = el('span', undefined, 'leds');
|
|
366
|
-
for (const t of m.tasks.slice(0, 60)) {
|
|
372
|
+
for (const t of m.tasks.slice(0, 60)) {
|
|
373
|
+
const status = RM.taskStatus(t);
|
|
374
|
+
const l = el('i', undefined, 'led st-' + String(status || 'pending').toLowerCase());
|
|
375
|
+
l.title = `${t.id}: ${status}`;
|
|
376
|
+
leds.append(l);
|
|
377
|
+
}
|
|
367
378
|
card.append(leds, el('span', relative(r.updated), 'muted'));
|
|
368
379
|
grid.append(card);
|
|
369
380
|
}
|
|
@@ -469,18 +480,61 @@ window.ForgeUI = (() => {
|
|
|
469
480
|
runs: state.runs, prefs: state.modelsPrefs, now: () => Date.now() / 1000,
|
|
470
481
|
savePrefs: p => { state.modelsPrefs = p; store.set('models', p); },
|
|
471
482
|
tablePrefs: id => state.tablePrefs[id], saveTablePrefs: (id, p) => { state.tablePrefs = { ...state.tablePrefs, [id]: p }; store.set('tables', state.tablePrefs); },
|
|
472
|
-
modelHref: alias => '#/cloud/models/' + encodeURIComponent(alias),
|
|
483
|
+
modelHref: alias => '#/cloud/models/' + encodeURIComponent(alias), reviewerOutcomes: state.reviewerOutcomes,
|
|
473
484
|
});
|
|
474
485
|
}
|
|
486
|
+
// How each reviewer's PASSed work held up (forge dataset reviewers). Advisory and optional: read once per
|
|
487
|
+
// change of the models data, and a failure (an export, no dataset) only leaves the column empty.
|
|
488
|
+
function loadReviewerOutcomes() {
|
|
489
|
+
if (SNAP) return;
|
|
490
|
+
fetchJSON('/api/models/reviewers').then(result => { state.reviewerOutcomes = result.data; },
|
|
491
|
+
() => { state.reviewerOutcomes = null; }).then(() => { if (state.models) renderModels(); });
|
|
492
|
+
}
|
|
475
493
|
function loadModels() {
|
|
476
494
|
return request('/api/models', result => {
|
|
477
495
|
state.modelsError = null;
|
|
478
496
|
if (result.text === state.modelsText) return;
|
|
479
497
|
state.modelsText = result.text; state.modelsData = result.data;
|
|
480
498
|
if (state.models) renderModels();
|
|
499
|
+
loadReviewerOutcomes();
|
|
481
500
|
}, 'Could not refresh the model results', error => { state.modelsError = error.message; if (state.models) renderModels(); });
|
|
482
501
|
}
|
|
483
502
|
|
|
503
|
+
/* ---------- the settings (#/settings) ---------- */
|
|
504
|
+
function renderSettings() {
|
|
505
|
+
const main = $('main');
|
|
506
|
+
if (main.dataset.view === 'settings') { state.settingsView?.poll(); return; } // already drawn: its unsaved edits stay
|
|
507
|
+
setView(main, 'settings'); delete main.dataset.run;
|
|
508
|
+
main.replaceChildren();
|
|
509
|
+
const v = el('section', undefined, 'settings-view'); v.id = 'settings-view'; main.append(v);
|
|
510
|
+
state.settingsView = window.ForgeSettings.render(v, {
|
|
511
|
+
// a pick holds only while the current run is the one it was made from: the default is the current or most recent run
|
|
512
|
+
key: state.settingsRun && state.lastKey === state.settingsPickedAt ? state.settingsRun : state.lastKey,
|
|
513
|
+
runs: () => state.runs, onPick: key => { state.settingsRun = key; state.settingsPickedAt = state.lastKey; },
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/* ---------- a run's open findings (#/runs/<key>/findings) ---------- */
|
|
518
|
+
function renderFindings() {
|
|
519
|
+
const main = $('main');
|
|
520
|
+
if (main.dataset.view === 'findings' && main.dataset.run === state.key) return; // already drawn: a toggle elsewhere must not reload it
|
|
521
|
+
setView(main, 'findings'); main.dataset.run = state.key;
|
|
522
|
+
main.replaceChildren();
|
|
523
|
+
const v = el('section', undefined, 'findings-view'); v.id = 'findings-view'; main.append(v);
|
|
524
|
+
window.ForgeFindings.render(v, {
|
|
525
|
+
key: state.key, run: state.cache.get(state.key)?.data, back: '#/runs/' + encodeURIComponent(state.key),
|
|
526
|
+
tablePrefs: id => state.tablePrefs[id], saveTablePrefs: (id, p) => { state.tablePrefs = { ...state.tablePrefs, [id]: p }; store.set('tables', state.tablePrefs); },
|
|
527
|
+
onCount: (n, scope) => { if (scope !== 'run') return; state.findingsCount.set(state.key, n); },
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
function findingsLink(key) {
|
|
531
|
+
const a = el('a', undefined, 'details-btn findings-link'); a.href = '#/runs/' + encodeURIComponent(key) + '/findings';
|
|
532
|
+
a.title = 'The findings this run left unfixed: dismiss them or file them as issues';
|
|
533
|
+
const n = state.findingsCount.get(key);
|
|
534
|
+
a.append(icon('panel-right'), el('span', 'Open findings'), el('span', n === undefined ? '' : String(n), 'findings-count'));
|
|
535
|
+
return a;
|
|
536
|
+
}
|
|
537
|
+
|
|
484
538
|
/* ---------- a run ---------- */
|
|
485
539
|
function viewToggle() {
|
|
486
540
|
const g = el('div', undefined, 'seg'); g.setAttribute('role', 'group'); g.setAttribute('aria-label', 'Rendering');
|
|
@@ -581,14 +635,32 @@ window.ForgeUI = (() => {
|
|
|
581
635
|
rec.append(icon('panel-right'), el('span', 'Run details'));
|
|
582
636
|
rec.addEventListener('click', () => { Shell.toggle('insp', true); if (state.sel) select(null); });
|
|
583
637
|
acts.append(rec);
|
|
638
|
+
if (!SNAP && window.ForgeFindings) acts.append(findingsLink(state.key));
|
|
584
639
|
if (!SNAP) {
|
|
585
640
|
const ex = el('button', undefined, 'export-btn'); ex.type = 'button'; ex.title = 'Download this run as one HTML file to share';
|
|
586
641
|
ex.append(icon('download'), el('span', 'Export…'));
|
|
587
642
|
ex.addEventListener('click', () => { const run = state.cache.get(state.key)?.data; if (run) exportDialog(run); }); acts.append(ex);
|
|
588
643
|
}
|
|
644
|
+
// the delete key stands apart from the routine ones, and only a server that takes writes shows it
|
|
645
|
+
if (!SNAP && window.ForgeRunDelete && window.ForgeUI && window.ForgeUI.controlsEnabled) {
|
|
646
|
+
window.ForgeUI.controlsEnabled().then(on => {
|
|
647
|
+
if (!on || acts.querySelector('.del-key')) return;
|
|
648
|
+
const del = el('button', undefined, 'del-key'); del.type = 'button'; del.title = 'Delete this run, after a dry run and a typed confirmation';
|
|
649
|
+
del.append(el('i', undefined, 'lamp st-fail'), el('span', 'Delete run…'));
|
|
650
|
+
del.addEventListener('click', () => { const run = state.cache.get(state.key)?.data; if (run) window.ForgeRunDelete.open(run, { onDeleted: runDeleted }); });
|
|
651
|
+
acts.append(el('span', undefined, 'del-gap'), del);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
589
654
|
bar.append(rp, acts);
|
|
590
655
|
header.append(bar);
|
|
591
656
|
}
|
|
657
|
+
// after a delete: the run is gone, so the page returns to the run list and drops what it held of the run
|
|
658
|
+
function runDeleted(run) {
|
|
659
|
+
state.cache.delete(run.key);
|
|
660
|
+
if (location.hash !== '#/') location.hash = '#/';
|
|
661
|
+
state.listText = null;
|
|
662
|
+
loadList();
|
|
663
|
+
}
|
|
592
664
|
// the View popover's push buttons: pressed in while their part is drawn; the layers are the 3D stage's own
|
|
593
665
|
function syncLayerKeys() {
|
|
594
666
|
for (const b of document.querySelectorAll('.layer-key')) {
|
|
@@ -683,11 +755,11 @@ window.ForgeUI = (() => {
|
|
|
683
755
|
{ id: 'title', label: 'Title', value: t => t.title },
|
|
684
756
|
{ id: 'difficulty', label: 'Difficulty', value: t => DIFF_RANK[t.difficulty] ?? null, cell: t => display(t.difficulty) },
|
|
685
757
|
{ id: 'wave', label: 'Wave', value: t => Number.isFinite(Number(t.wave)) && t.wave !== null && t.wave !== '' ? Number(t.wave) : null, cell: t => display(t.wave) },
|
|
686
|
-
{ id: 'status', label: 'Status', value: t => t
|
|
758
|
+
{ id: 'status', label: 'Status', value: t => RM.taskStatus(t), cell: t => chip(RM.taskStatus(t)) },
|
|
687
759
|
{ id: 'dwarf', label: 'Dwarf', value: t => t.dwarf, cell: t => window.ForgeIcons.modelPlate(t.dwarf) },
|
|
688
760
|
{ id: 'qa', label: 'QA', value: t => t.qa, cell: t => window.ForgeIcons.modelPlate(t.qa) },
|
|
689
761
|
{ id: 'attempt', label: 'Attempt', value: t => Number(t.attempt) || null, cell: t => display(t.attempt) },
|
|
690
|
-
{ id: 'verdict', label: 'Verdict', value: t => t.
|
|
762
|
+
{ id: 'verdict', label: 'Verdict', value: t => RM.taskVerdict(t).text, cell: t => { const v = RM.taskVerdict(t); return chip(v.text, v.status); } },
|
|
691
763
|
{ id: 'findings', label: 'Findings', title: 'The worst confirmed severity and how many; ~n plausible only',
|
|
692
764
|
value: t => t.findings && t.findings.worst ? SEV_RANK[t.findings.worst] * 1000 - (t.findings[t.findings.worst] || 0) : t.findings && t.findings.plausible ? 4000 : null,
|
|
693
765
|
cell: t => { const f = t.findings, v = sev(f); if (!f || !f.worst) return v; const n = el('span', v, 'sev-chip sev-' + f.worst); return n; } },
|
|
@@ -862,6 +934,8 @@ window.ForgeUI = (() => {
|
|
|
862
934
|
if (!state.key) document.body.classList.remove('no-tasks');
|
|
863
935
|
if (state.cloud) { renderCloud(); return; }
|
|
864
936
|
if (state.models) { renderModels(); return; }
|
|
937
|
+
if (state.settings) { renderSettings(); return; }
|
|
938
|
+
if (state.key && state.findingsPage) { renderFindings(); return; }
|
|
865
939
|
if (!state.key) { renderMap(); return; }
|
|
866
940
|
const cached = state.cache.get(state.key);
|
|
867
941
|
if (cached) renderRun(cached.data);
|
|
@@ -916,16 +990,17 @@ window.ForgeUI = (() => {
|
|
|
916
990
|
if (window.ForgeNotify) window.ForgeNotify.observe(state.runs);
|
|
917
991
|
renderList();
|
|
918
992
|
if (state.cloud) renderCloud();
|
|
919
|
-
else if (
|
|
993
|
+
else if (state.settings) state.settingsView?.poll(); // the picker follows the run list; the page stays
|
|
994
|
+
else if (!state.key && !state.models) renderMap({ rebuildBar: $('map-filters') ? !$('map-filters').contains(document.activeElement) : true });
|
|
920
995
|
}, 'Could not refresh runs');
|
|
921
996
|
}
|
|
922
997
|
function loadRun(key) {
|
|
923
998
|
return request('/api/runs/' + encodeURIComponent(key), result => {
|
|
924
999
|
const changed = state.cache.get(key)?.text !== result.text;
|
|
925
1000
|
state.cache.set(key, result);
|
|
926
|
-
if (state.key === key && changed) renderRun(result.data);
|
|
1001
|
+
if (state.key === key && changed && !state.findingsPage) renderRun(result.data);
|
|
927
1002
|
}, 'Could not refresh run', error => {
|
|
928
|
-
if (state.key !== key || state.cache.has(key)) return;
|
|
1003
|
+
if (state.key !== key || state.cache.has(key) || state.findingsPage) return;
|
|
929
1004
|
if (error.status === 404) state.errors.delete('/api/runs/' + encodeURIComponent(key));
|
|
930
1005
|
setView($('main'), 'waiting');
|
|
931
1006
|
$('main').replaceChildren(el('p', error.status === 404 ?
|
|
@@ -940,7 +1015,7 @@ window.ForgeUI = (() => {
|
|
|
940
1015
|
history.replaceState(null, '', '#/runs/' + encodeURIComponent(SNAP.meta.key));
|
|
941
1016
|
parsed = parseHash(location.hash);
|
|
942
1017
|
}
|
|
943
|
-
const keyChanged = parsed.key !== state.key;
|
|
1018
|
+
const keyChanged = parsed.key !== state.key || (!!state.findingsPage && !parsed.findings); // the findings page replaced the run's DOM
|
|
944
1019
|
state.key = parsed.key;
|
|
945
1020
|
const selWas = JSON.stringify(state.sel || null);
|
|
946
1021
|
state.sel = parsed.sel;
|
|
@@ -951,6 +1026,9 @@ window.ForgeUI = (() => {
|
|
|
951
1026
|
state.convo = parsed.convo || null;
|
|
952
1027
|
state.cloud = parsed.cloud || null;
|
|
953
1028
|
state.models = !!parsed.models;
|
|
1029
|
+
state.settings = !!parsed.settings && !!window.ForgeSettings && !SNAP;
|
|
1030
|
+
if (parsed.key) state.lastKey = parsed.key;
|
|
1031
|
+
state.findingsPage = !!parsed.findings && !!window.ForgeFindings;
|
|
954
1032
|
if (state.cloud && state.cloud.model) { // a model's runs: the cloud, filtered to it
|
|
955
1033
|
cloudFilter({ models: { aliases: [state.cloud.model], role: 'any' } });
|
|
956
1034
|
history.replaceState(null, '', '#/cloud'); state.cloud.model = null;
|
|
@@ -958,13 +1036,15 @@ window.ForgeUI = (() => {
|
|
|
958
1036
|
document.body.classList.toggle('cloud-mode', !!state.cloud);
|
|
959
1037
|
if (!state.key) document.body.classList.remove('no-tasks');
|
|
960
1038
|
for (const a of document.querySelectorAll('.topbar .top-link')) {
|
|
961
|
-
const here = a.getAttribute('href') === (state.cloud ? '#/cloud' : state.models ? '#/models' : !state.key ? '#/' : null);
|
|
1039
|
+
const here = a.getAttribute('href') === (state.cloud ? '#/cloud' : state.models ? '#/models' : state.settings ? '#/settings' : !state.key ? '#/' : null);
|
|
962
1040
|
if (here) a.setAttribute('aria-current', 'page'); else a.removeAttribute('aria-current');
|
|
963
1041
|
}
|
|
964
1042
|
renderErrors();
|
|
965
1043
|
markSelection();
|
|
966
1044
|
if (state.cloud) { renderCloud(); return; }
|
|
967
1045
|
if (state.models) { renderModels(); loadModels(); return; }
|
|
1046
|
+
if (state.settings) { renderSettings(); return; }
|
|
1047
|
+
if (state.key && state.findingsPage) { renderFindings(); return; }
|
|
968
1048
|
if (!state.key) { renderMap(); return; }
|
|
969
1049
|
const cached = state.cache.get(state.key);
|
|
970
1050
|
if (cached) {
|
|
@@ -993,6 +1073,7 @@ window.ForgeUI = (() => {
|
|
|
993
1073
|
loadList();
|
|
994
1074
|
if (state.key) loadRun(state.key);
|
|
995
1075
|
if (state.models) loadModels();
|
|
1076
|
+
if (state.settings) state.settingsView?.poll();
|
|
996
1077
|
for (const node of document.querySelectorAll('[data-epoch]')) { const t = relative(node.dataset.epoch); node.textContent = node.dataset.short !== undefined ? t.replace(/^updated /, '') : t; }
|
|
997
1078
|
}
|
|
998
1079
|
// Poll faster while something runs, slower otherwise; never with refresh off, and a hidden tab only to notify.
|
|
@@ -1008,12 +1089,14 @@ window.ForgeUI = (() => {
|
|
|
1008
1089
|
{ id: 'cmd:cloud', label: 'Open the run cloud', sub: 'every run in 3D, grouped by project', ids: ['cloud'], href: '#/cloud' },
|
|
1009
1090
|
{ id: 'cmd:map', label: 'Open the map of all runs', sub: 'the front page', ids: ['map', 'home'], href: '#/' },
|
|
1010
1091
|
{ id: 'cmd:models', label: 'Open the model leaderboard', sub: 'pass rates, retries, time and tokens per model', ids: ['models', 'leaderboard'], href: '#/models' },
|
|
1092
|
+
...(window.ForgeSettings ? [{ id: 'cmd:settings', label: 'Open the settings', sub: 'model registry, Jev, and the .forge files of a repository', ids: ['settings', 'config', 'verify', 'jev'], href: '#/settings' }] : []),
|
|
1011
1093
|
{ id: 'cmd:notify', label: 'Notifications', sub: 'what finished or failed, and browser notifications', ids: ['alerts', 'bell'], action: () => window.ForgeNotify && window.ForgeNotify.open() },
|
|
1012
1094
|
{ id: 'cmd:dim', label: state.dim === '3d' ? 'Switch to 2D' : 'Switch to 3D', sub: state.dim === '3d' ? 'graphs and charts, no WebGL' : 'loads three.js from cdn.jsdelivr.net', ids: ['2d', '3d'],
|
|
1013
1095
|
action: () => { state.dim = state.dim === '3d' ? '2d' : '3d'; store.set('view', state.dim); if (state.dim === '3d') state.stageFailed = null; renderMain(); } },
|
|
1014
1096
|
{ id: 'cmd:refresh', label: state.paused ? 'Resume live refresh' : 'Pause live refresh', sub: 'polling of the runs', ids: ['refresh', 'pause'],
|
|
1015
1097
|
action: () => { $('refresh').checked = state.paused; state.paused = !state.paused; refresh(); } },
|
|
1016
1098
|
];
|
|
1099
|
+
if (state.key && !SNAP && window.ForgeFindings) out.push({ id: 'cmd:findings', label: 'Open findings of this run…', sub: 'dismiss them or file them as GitHub issues', ids: ['findings', 'issues', 'dismiss'], href: '#/runs/' + encodeURIComponent(state.key) + '/findings' });
|
|
1017
1100
|
if (run && !SNAP) out.push({ id: 'cmd:export', label: 'Export this run as one HTML file…', sub: 'to share; opens without forge-ui', ids: ['export', 'share'], action: () => exportDialog(run) });
|
|
1018
1101
|
if (SNAP) return out.filter(c => c.id === 'cmd:dim').concat(run ? [{ id: 'cmd:mode', label: state.mode === 'plan' ? 'Show the execution' : 'Show the planned waves', sub: '', ids: ['exec', 'plan'], action: () => setMode(state.mode === 'plan' ? 'exec' : 'plan') }] : []);
|
|
1019
1102
|
if (run && window.ForgeFiles) out.push({ id: 'cmd:files', label: 'Show which tasks touched which files', sub: 'the Files view of this run', ids: ['files', 'heatmap'], action: () => setMode('files') });
|
|
@@ -1077,3 +1160,6 @@ window.ForgeUI = (() => {
|
|
|
1077
1160
|
init();
|
|
1078
1161
|
return { refresh, state };
|
|
1079
1162
|
})();
|
|
1163
|
+
// window.ForgeUI is uikit.js's shared helpers; the page's own handles (state, refresh) join it, where the browser
|
|
1164
|
+
// tests and ui/tools/readme_media.py read them.
|
|
1165
|
+
window.ForgeUI = Object.assign(window.ForgeUI || {}, window.ForgeApp);
|
|
@@ -427,9 +427,12 @@ export function createCloud(E, host, cb = {}) {
|
|
|
427
427
|
function info(el) {
|
|
428
428
|
const f = el.f, r = f.run, counts = Object.entries(r.counts || {}).map(([s, n]) => `${s.toLowerCase()} ${n}`).join(' · ');
|
|
429
429
|
const proj = st.L && st.L.projects.find(p => p.id === f.project);
|
|
430
|
+
const verdict = r.final_review && typeof r.final_review === 'object' ? r.final_review.verdict : r.final_review;
|
|
431
|
+
const isOutdated = verdict && RM.runReviewStatus(r) === 'OUTDATED';
|
|
432
|
+
const reviewText = verdict ? (isOutdated ? `${verdict} (outdated)` : verdict) : null;
|
|
430
433
|
return [['meta', `${proj ? proj.name : RM.basename(r.repo)} · ${r.run_id || ''} · ${r.kind}`], ['title', String(r.goal || 'Untitled run').split('\n')[0]],
|
|
431
434
|
['body', `${f.n} task${f.n > 1 ? 's' : ''} · ${counts}`], ['body', `${[...f.dwarf].join(', ') || '—'} → ${[...f.qa].join(', ') || '—'}`],
|
|
432
|
-
['foot', `started ${new Date(f.t * 1000).toLocaleString()}${
|
|
435
|
+
['foot', `started ${new Date(f.t * 1000).toLocaleString()}${reviewText ? ' · review ' + reviewText : ''}${r.verification_status ? ' · verify ' + r.verification_status : ''}`],
|
|
433
436
|
['foot', (store.layouts[st.layout] || {})[el.key] ? 'moved by you · drag to move, Shift = depth, Alt = height' : 'drag to move · double-click to open']];
|
|
434
437
|
}
|
|
435
438
|
function showTip(el, cx, cy) {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
const aliases = specs => new Set(list(specs).map(RM.alias).filter(Boolean));
|
|
64
64
|
return { key: run.key, run, project: hashId(run.repo || '?'), n, bucket: bucket(n), diff, dominant, state, t: startOf(run),
|
|
65
65
|
dwarf: aliases(run.dwarf_models || run.models), qa: aliases(run.qa_models || []), all: aliases(run.models),
|
|
66
|
-
review: run
|
|
66
|
+
review: RM.runReviewStatus(run) || null, verification: run.verification_status || null, decompose: run.decompose_level || null, kind: run.kind };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/* ---------- filters: the cloud shows exactly the runs that match ---------- */
|
|
@@ -254,7 +254,7 @@ window.ForgeCloudUI = (() => {
|
|
|
254
254
|
}
|
|
255
255
|
const more = el('fieldset', undefined, 'cloud-field cloud-more'); more.append(el('legend', 'More'));
|
|
256
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; }));
|
|
257
|
+
more.append(select('Final review', 'review', [['', 'any'], ['PASS', 'passed'], ['FAIL', 'failed'], ['OUTDATED', 'outdated'], ['none', 'no verdict']], () => f.review, v => { f.review = v; }));
|
|
258
258
|
more.append(select('Verification', 'verification', [['', 'any'], ['PASS', 'passed'], ['FAIL', 'failed'], ['none', 'not run']], () => f.verification, v => { f.verification = v; }));
|
|
259
259
|
const dec = Object.keys(O.decompose);
|
|
260
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; }));
|
|
@@ -290,13 +290,22 @@ window.ForgeCloudUI = (() => {
|
|
|
290
290
|
head.append(shut);
|
|
291
291
|
card.append(head, el('h2', String(r.goal || 'Untitled run').split('\n')[0]), el('p', r.run_id || '', 'mono muted'));
|
|
292
292
|
const leds = el('span', undefined, 'leds');
|
|
293
|
-
for (const t of list(r.task_statuses).slice(0, 120)) {
|
|
293
|
+
for (const t of list(r.task_statuses).slice(0, 120)) {
|
|
294
|
+
const st = RM.taskStatus(t);
|
|
295
|
+
const l = el('i', undefined, 'led st-' + String(st || 'pending').toLowerCase());
|
|
296
|
+
l.title = `${t.id}: ${st}${t.title ? ' · ' + t.title : ''}`;
|
|
297
|
+
leds.append(l);
|
|
298
|
+
}
|
|
294
299
|
card.append(leds);
|
|
295
300
|
const dl = el('dl', undefined, 'fields');
|
|
296
301
|
const mix = ['high', 'medium', 'low', 'none'].filter(k => f.diff[k]).map(k => `${f.diff[k]} ${k === 'none' ? 'unset' : k}`).join(' · ');
|
|
297
302
|
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; };
|
|
303
|
+
const verdict = r.final_review && typeof r.final_review === 'object' ? r.final_review.verdict : r.final_review;
|
|
304
|
+
const isOutdated = verdict && RM.runReviewStatus(r) === 'OUTDATED';
|
|
305
|
+
const reviewText = verdict ? (isOutdated ? `${verdict} (outdated)` : verdict) : '—';
|
|
306
|
+
const reviewVal = isOutdated ? el('span', reviewText, 'chip st-outdated') : reviewText;
|
|
298
307
|
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',
|
|
308
|
+
['Started', when(r.created)], ['Updated', when(r.updated)], ['Final review', reviewVal], ['Verification', r.verification_status || '—'], ['Decompose', r.decompose_level || 'off']]) {
|
|
300
309
|
const dd = el('dd', undefined, mono ? 'mono' : ''); dd.append(v instanceof Node ? v : document.createTextNode(String(v))); dl.append(el('dt', k), dd);
|
|
301
310
|
}
|
|
302
311
|
card.append(dl);
|
|
@@ -345,7 +354,7 @@ window.ForgeCloudUI = (() => {
|
|
|
345
354
|
a.href = '#/runs/' + encodeURIComponent(r.key);
|
|
346
355
|
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
356
|
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
|
|
357
|
+
for (const t of list(r.task_statuses).slice(0, 60)) leds.append(el('i', undefined, 'led st-' + String(RM.taskStatus(t) || 'pending').toLowerCase()));
|
|
349
358
|
a.append(leds); grid.append(a);
|
|
350
359
|
}
|
|
351
360
|
g.append(grid); listHost.append(g);
|