@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.
- package/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +1854 -2
- package/SKILL.md +122 -0
- package/VERSION +1 -0
- package/forge +74 -0
- package/package.json +26 -4
- package/references/dataset.md +329 -0
- package/references/decompose.md +667 -0
- package/references/fractal-dashboard.md +21 -0
- package/references/fractal.md +307 -0
- package/references/harnesses.md +353 -0
- package/references/jev.md +865 -0
- package/references/memory.md +228 -0
- package/references/planning.md +42 -0
- package/references/ripwire.md +47 -0
- package/references/runtime.md +64 -0
- package/references/solo.md +113 -0
- package/references/ui.md +112 -0
- package/registry.tsv +81 -0
- package/scripts/forge-artifact.sh +78 -0
- package/scripts/forge-contract.py +430 -0
- package/scripts/forge-dataset.py +6 -0
- package/scripts/forge-dispatch.sh +665 -0
- package/scripts/forge-fail-first.py +195 -0
- package/scripts/forge-fractal-dispatch.sh +4 -0
- package/scripts/forge-fractal-options.sh +54 -0
- package/scripts/forge-fractal.py +6 -0
- package/scripts/forge-guard.py +266 -0
- package/scripts/forge-install-fractal.sh +4 -0
- package/scripts/forge-install-ripwire.sh +52 -0
- package/scripts/forge-install.sh +135 -0
- package/scripts/forge-jev-options.sh +259 -0
- package/scripts/forge-jev.py +6 -0
- package/scripts/forge-lib-plans.sh +785 -0
- package/scripts/forge-lib-review.sh +494 -0
- package/scripts/forge-lib-task.sh +302 -0
- package/scripts/forge-lib-verify.sh +30 -0
- package/scripts/forge-memory.sh +575 -0
- package/scripts/forge-metrics.sh +39 -0
- package/scripts/forge-parallel-options.sh +76 -0
- package/scripts/forge-parallel.sh +1717 -0
- package/scripts/forge-plan-check.py +118 -0
- package/scripts/forge-prompt.py +45 -0
- package/scripts/forge-ripwire.py +136 -0
- package/scripts/forge-route.py +344 -0
- package/scripts/forge-runtime.py +533 -0
- package/scripts/forge-schedule.py +312 -0
- package/scripts/forge-shadow.py +305 -0
- package/scripts/forge-solo.sh +347 -0
- package/scripts/forge-ui-options.sh +42 -0
- package/scripts/forge-ui.py +435 -0
- package/scripts/forge-update.sh +71 -0
- package/scripts/forge-verify-coverage.py +956 -0
- package/scripts/forge-version.py +198 -0
- package/scripts/forge_dataset/__init__.py +439 -0
- package/scripts/forge_dataset/capture.py +330 -0
- package/scripts/forge_dataset/checks.py +109 -0
- package/scripts/forge_dataset/cli.py +233 -0
- package/scripts/forge_dataset/export.py +1009 -0
- package/scripts/forge_dataset/harvest.py +716 -0
- package/scripts/forge_dataset/importer.py +412 -0
- package/scripts/forge_dataset/outcomes.py +275 -0
- package/scripts/forge_dataset/routing.py +278 -0
- package/scripts/forge_fractal/__init__.py +83 -0
- package/scripts/forge_fractal/bridge.py +46 -0
- package/scripts/forge_fractal/cli.py +202 -0
- package/scripts/forge_fractal/dashboard.css +138 -0
- package/scripts/forge_fractal/dashboard.html +41 -0
- package/scripts/forge_fractal/dashboard.js +687 -0
- package/scripts/forge_fractal/decomposition.py +262 -0
- package/scripts/forge_fractal/execution.py +659 -0
- package/scripts/forge_fractal/inspection.py +261 -0
- package/scripts/forge_fractal/install.py +158 -0
- package/scripts/forge_fractal/invocation.py +94 -0
- package/scripts/forge_fractal/pipeline.py +124 -0
- package/scripts/forge_fractal/selection.py +179 -0
- package/scripts/forge_jev/__init__.py +376 -0
- package/scripts/forge_jev/backtest.py +322 -0
- package/scripts/forge_jev/cli.py +980 -0
- package/scripts/forge_jev/client.py +418 -0
- package/scripts/forge_jev/corpus.py +156 -0
- package/scripts/forge_jev/coupling.py +244 -0
- package/scripts/forge_jev/features.py +24 -0
- package/scripts/forge_jev/gates.py +100 -0
- package/scripts/forge_jev/memory.py +163 -0
- package/scripts/forge_jev/outcome.py +49 -0
- package/scripts/forge_jev/predict.py +204 -0
- package/scripts/forge_jev/questions.py +661 -0
- package/scripts/forge_jev/review.py +73 -0
- package/scripts/forge_jev/routing.py +271 -0
- package/scripts/forge_jev/subset.py +123 -0
- package/scripts/forge_jev/verify.py +453 -0
- package/scripts/forge_registry.py +140 -0
- package/ui/README.md +287 -0
- package/ui/THIRD_PARTY_NOTICES.md +69 -0
- package/ui/docs/api.md +888 -0
- package/ui/forge_ui/__init__.py +20 -0
- package/ui/forge_ui/__main__.py +135 -0
- package/ui/forge_ui/diffs.py +454 -0
- package/ui/forge_ui/executions.py +436 -0
- package/ui/forge_ui/export.py +204 -0
- package/ui/forge_ui/findings.py +132 -0
- package/ui/forge_ui/fractal.py +583 -0
- package/ui/forge_ui/models.py +216 -0
- package/ui/forge_ui/reader.py +1336 -0
- package/ui/forge_ui/server.py +303 -0
- package/ui/forge_ui/static/app.js +924 -0
- package/ui/forge_ui/static/cloud3d.js +646 -0
- package/ui/forge_ui/static/cloudmodel.js +375 -0
- package/ui/forge_ui/static/cloudui.js +394 -0
- package/ui/forge_ui/static/diffview.js +216 -0
- package/ui/forge_ui/static/favicon.svg +1 -0
- package/ui/forge_ui/static/files.js +776 -0
- package/ui/forge_ui/static/gantt.js +97 -0
- package/ui/forge_ui/static/graph.js +266 -0
- package/ui/forge_ui/static/index.html +55 -0
- package/ui/forge_ui/static/iso.js +765 -0
- package/ui/forge_ui/static/leaderboard.js +315 -0
- package/ui/forge_ui/static/map3d.js +45 -0
- package/ui/forge_ui/static/modelstats.js +249 -0
- package/ui/forge_ui/static/notify.js +179 -0
- package/ui/forge_ui/static/panel.js +526 -0
- package/ui/forge_ui/static/runmodel.js +651 -0
- package/ui/forge_ui/static/scene3d.js +996 -0
- package/ui/forge_ui/static/search.js +119 -0
- package/ui/forge_ui/static/style.css +618 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* forge-ui execution chart (2D): every attempt on wall-clock time in the lane it ran in, with its dwarf and
|
|
2
|
+
review inside, projected attempts for what has not started, and the concurrency strip under the stage.
|
|
3
|
+
Defines window.ForgeGantt.{render, strip}. SVG drawn with classes only (style.css colours it). */
|
|
4
|
+
window.ForgeGantt = (() => {
|
|
5
|
+
'use strict';
|
|
6
|
+
const RM = window.ForgeRunModel;
|
|
7
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
8
|
+
const list = v => Array.isArray(v) ? v : [];
|
|
9
|
+
function svg(name, attrs, parent) {
|
|
10
|
+
const e = document.createElementNS(NS, name);
|
|
11
|
+
for (const k in attrs || {}) e.setAttribute(k, attrs[k]);
|
|
12
|
+
if (parent) parent.append(e);
|
|
13
|
+
return e;
|
|
14
|
+
}
|
|
15
|
+
const stc = s => 'st-' + String(s || 'pending').toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
16
|
+
const LANE = 34, LABEL = 70, TOP = 26;
|
|
17
|
+
|
|
18
|
+
function render(container, run, { now = null, onSelect = null, sel = null, replay = null, critical = null, ghosts = true } = {}) {
|
|
19
|
+
container.replaceChildren();
|
|
20
|
+
const X = RM.execModel(run, { now, ghosts });
|
|
21
|
+
const all = X.items.concat(X.extra);
|
|
22
|
+
if (!all.length) { const p = document.createElement('p'); p.className = 'fg-empty'; p.textContent = 'Nothing has run yet: no attempt timings are recorded for this run.'; container.append(p); return; }
|
|
23
|
+
const width = Math.max(640, container.clientWidth || 900), plot = width - LABEL - 12;
|
|
24
|
+
const xs = t => LABEL + (t - X.t0) / (X.t1 - X.t0) * plot;
|
|
25
|
+
const laneOf = it => typeof it.lane === 'number' ? it.lane : X.lanes + (it.kind === 'verification' ? 0 : X.finishLanes.length > 1 ? 1 : 0);
|
|
26
|
+
const rows = X.lanes + X.finishLanes.length;
|
|
27
|
+
const height = TOP + rows * LANE + 8;
|
|
28
|
+
const root = svg('svg', { class: 'gantt', viewBox: `0 0 ${width} ${height}`, width, height, role: 'group', 'aria-label': 'Attempts on wall-clock time' });
|
|
29
|
+
for (const r of X.ruler) {
|
|
30
|
+
svg('line', { class: 'gt-tick', x1: xs(r.t), x2: xs(r.t), y1: TOP - 6, y2: height - 4 }, root);
|
|
31
|
+
svg('text', { class: 'gt-tick-label', x: xs(r.t) + 3, y: TOP - 10 }, root).textContent = '+' + r.label;
|
|
32
|
+
}
|
|
33
|
+
for (let i = 0; i < rows; i++) {
|
|
34
|
+
const over = i < X.lanes && i >= X.cap, y = TOP + i * LANE;
|
|
35
|
+
svg('rect', { class: 'gt-lane' + (over ? ' gt-over' : ''), x: LABEL, y: y + 2, width: plot, height: LANE - 4, rx: 4 }, root);
|
|
36
|
+
const name = i < X.lanes ? `lane ${i + 1}${over ? ' !' : ''}` : X.finishLanes[i - X.lanes].name.toLowerCase();
|
|
37
|
+
const t = svg('text', { class: 'gt-lane-label' + (over ? ' gt-over' : ''), x: 6, y: y + LANE / 2 + 4 }, root); t.textContent = name;
|
|
38
|
+
if (over) svg('title', {}, t).textContent = `More attempts overlapped than --max-parallel ${X.cap} allows: separate run invocations, or a resumed run.`;
|
|
39
|
+
}
|
|
40
|
+
const critSet = new Set(critical && critical.ids || []);
|
|
41
|
+
for (const it of all) {
|
|
42
|
+
const y = TOP + laneOf(it) * LANE + 5, x = xs(it.start), w = Math.max(3, xs(it.end) - x);
|
|
43
|
+
const future = replay !== null && it.start > replay;
|
|
44
|
+
const g = svg('g', { class: `gt-item ${stc(it.status)}${it.ghost ? ' gt-ghost' : ''}${future ? ' gt-future' : ''}${critSet.has(it.task) && !it.ghost ? ' gt-crit' : ''}${sel && (sel.exec === it.id || (sel.kind === 'task' && sel.id === it.task)) ? ' gt-sel' : ''}`, tabindex: 0, role: 'button' }, root);
|
|
45
|
+
svg('rect', { class: 'gt-box', x, y, width: w, height: LANE - 10, rx: 3 }, g);
|
|
46
|
+
for (const s of it.segs) {
|
|
47
|
+
const sx = xs(s.start), sw = Math.max(1.5, xs(s.end ?? it.end) - sx);
|
|
48
|
+
svg('rect', { class: `gt-seg gt-${s.role} ${stc(s.status)}`, x: sx, y: y + LANE - 17, width: sw, height: 5, rx: 1.5 }, g);
|
|
49
|
+
}
|
|
50
|
+
const label = it.kind === 'verification' ? 'verify' : it.kind === 'review' ? 'review' : `${it.task}${it.attempt ? ' A' + it.attempt : ''}`;
|
|
51
|
+
if (w > label.length * 6.4 + 6) svg('text', { class: 'gt-label', x: x + 4, y: y + 11 }, g).textContent = label;
|
|
52
|
+
svg('title', {}, g).textContent = `${label} · ${it.status} · ${RM.fmtDur(it.end - it.start)}${it.ghost ? ' (projected)' : ''}`;
|
|
53
|
+
const pick = () => {
|
|
54
|
+
if (!onSelect) return;
|
|
55
|
+
if (it.kind === 'review') onSelect({ kind: 'final', name: it.review });
|
|
56
|
+
else if (it.kind === 'verification') onSelect({ kind: 'execution', task: null, exec: it.id });
|
|
57
|
+
else if (it.ghost || String(it.id).startsWith('span:')) onSelect({ kind: 'task', id: it.task });
|
|
58
|
+
else onSelect({ kind: 'execution', task: it.task, id: it.task, exec: it.id });
|
|
59
|
+
};
|
|
60
|
+
g.addEventListener('click', pick);
|
|
61
|
+
g.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(); } });
|
|
62
|
+
}
|
|
63
|
+
const cursorT = replay !== null ? replay : X.live && now !== null ? now : null;
|
|
64
|
+
if (cursorT !== null && cursorT >= X.t0 && cursorT <= X.t1) svg('line', { class: 'gt-now', x1: xs(cursorT), x2: xs(cursorT), y1: TOP - 8, y2: height - 2 }, root);
|
|
65
|
+
container.append(root);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// How many attempts, dwarves and reviewers were busy at each moment, against the concurrency limit.
|
|
69
|
+
function strip(container, run, { now = null, replay = null } = {}) {
|
|
70
|
+
container.replaceChildren();
|
|
71
|
+
const c = RM.concurrency(run, { now }), range = RM.timeRange(run, now);
|
|
72
|
+
if (!c.series.length || !range) return;
|
|
73
|
+
const width = Math.max(640, container.clientWidth || 900), H = 62, plot = width - LABEL - 12;
|
|
74
|
+
const t0 = range.start, t1 = Math.max(range.end, now !== null && list(run.tasks).some(t => t.status === 'RUNNING') ? now : range.end, t0 + 1);
|
|
75
|
+
const xs = t => LABEL + (t - t0) / (t1 - t0) * plot;
|
|
76
|
+
const top = Math.max(c.cap, c.peak, 1), ys = v => H - 8 - v / top * (H - 18);
|
|
77
|
+
const root = svg('svg', { class: 'strip', viewBox: `0 0 ${width} ${H}`, width, height: H, role: 'img', 'aria-label': `Concurrency: peak ${c.peak} attempts at once, limit ${c.cap}` });
|
|
78
|
+
const step = key => {
|
|
79
|
+
let d = `M${xs(c.series[0].t)} ${ys(0)}`, prev = 0;
|
|
80
|
+
for (const p of c.series) { d += ` L${xs(p.t)} ${ys(prev)} L${xs(p.t)} ${ys(p[key])}`; prev = p[key]; }
|
|
81
|
+
return d + ` L${xs(t1)} ${ys(prev)} L${xs(t1)} ${ys(0)} Z`;
|
|
82
|
+
};
|
|
83
|
+
svg('path', { class: 'cc-area cc-tasks', d: step('task') }, root);
|
|
84
|
+
const models = c.series.map(p => ({ t: p.t, m: p.dwarf + p.qa }));
|
|
85
|
+
let d = `M${xs(models[0].t)} ${ys(0)}`, prev = 0;
|
|
86
|
+
for (const p of models) { d += ` L${xs(p.t)} ${ys(prev)} L${xs(p.t)} ${ys(p.m)}`; prev = p.m; }
|
|
87
|
+
svg('path', { class: 'cc-line cc-models', d }, root);
|
|
88
|
+
svg('line', { class: 'cc-cap', x1: LABEL, x2: width - 12, y1: ys(c.cap), y2: ys(c.cap) }, root);
|
|
89
|
+
svg('text', { class: 'gt-lane-label', x: 6, y: 16 }, root).textContent = 'busy';
|
|
90
|
+
svg('text', { class: 'gt-lane-label', x: 6, y: 30 }, root).textContent = `peak ${c.peak}`;
|
|
91
|
+
svg('text', { class: 'gt-lane-label cc-cap-label', x: 6, y: 44 }, root).textContent = `limit ${c.cap}`;
|
|
92
|
+
const cursorT = replay !== null ? replay : null;
|
|
93
|
+
if (cursorT !== null) svg('line', { class: 'gt-now', x1: xs(cursorT), x2: xs(cursorT), y1: 2, y2: H - 2 }, root);
|
|
94
|
+
container.append(root);
|
|
95
|
+
}
|
|
96
|
+
return { render, strip };
|
|
97
|
+
})();
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/* forge-ui graph: tasks as SVG nodes in wave columns, dependencies as edges.
|
|
2
|
+
Defines window.ForgeGraph.render(container, run, {selected, onSelect}). Colours come from style.css
|
|
3
|
+
classes (.fg-box, .fg-bar, .fg-verdict): the page CSP refuses style attributes. */
|
|
4
|
+
(function () {
|
|
5
|
+
'use strict';
|
|
6
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
7
|
+
|
|
8
|
+
var NS = 'http://www.w3.org/2000/svg';
|
|
9
|
+
var NODE_W = 176, NODE_H = 46, COL_GAP = 72, ROW_GAP = 14;
|
|
10
|
+
var PAD = 16, HEADER_H = 26;
|
|
11
|
+
var uid = 0;
|
|
12
|
+
|
|
13
|
+
function el(name, attrs, parent) {
|
|
14
|
+
var e = document.createElementNS(NS, name);
|
|
15
|
+
if (attrs) for (var k in attrs) if (Object.prototype.hasOwnProperty.call(attrs, k)) e.setAttribute(k, attrs[k]);
|
|
16
|
+
if (parent) parent.appendChild(e);
|
|
17
|
+
return e;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function textEl(parent, str, attrs) {
|
|
21
|
+
var t = el('text', attrs, parent);
|
|
22
|
+
t.textContent = str;
|
|
23
|
+
return t;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Conservative per-glyph advance (in em) so wide letters cannot overflow the node.
|
|
27
|
+
function glyphEm(c) {
|
|
28
|
+
if (/[WM@%]/.test(c)) return 1.0;
|
|
29
|
+
if (/[mw]/.test(c)) return 0.9;
|
|
30
|
+
if (/[A-Z0-9_#&$]/.test(c)) return 0.75;
|
|
31
|
+
if (/[il.,:;'|!\[\]() \-]/.test(c)) return 0.4;
|
|
32
|
+
if (c.charCodeAt(0) > 0x7e) return 1.1; // em dash, ellipsis, CJK, emoji: about one em or more
|
|
33
|
+
return 0.64;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Cut s so its estimated width at fontSize (times extra, e.g. 1.1 for bold) fits maxPx.
|
|
37
|
+
function ellipsize(s, maxPx, fontSize, extra) {
|
|
38
|
+
s = String(s);
|
|
39
|
+
var k = fontSize * (extra || 1), w = 0, i, total = 0;
|
|
40
|
+
for (i = 0; i < s.length; i++) total += glyphEm(s.charAt(i)) * k;
|
|
41
|
+
if (total <= maxPx) return s;
|
|
42
|
+
var room = maxPx - glyphEm('…') * k;
|
|
43
|
+
for (i = 0; i < s.length; i++) {
|
|
44
|
+
var cw = glyphEm(s.charAt(i)) * k;
|
|
45
|
+
if (w + cw > room) break;
|
|
46
|
+
w += cw;
|
|
47
|
+
}
|
|
48
|
+
return s.slice(0, Math.max(1, i)) + '…';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function str(v, dflt) {
|
|
52
|
+
return v === null || v === undefined || v === '' ? dflt : String(v);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function message(container, text) {
|
|
56
|
+
var p = document.createElement('p');
|
|
57
|
+
p.className = 'fg-empty';
|
|
58
|
+
p.textContent = text;
|
|
59
|
+
container.appendChild(p);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Column (1-based) of every task: its wave, else one right of its deepest dep.
|
|
63
|
+
function computeColumns(tasks, byId) {
|
|
64
|
+
var col = Object.create(null), i, pass;
|
|
65
|
+
for (i = 0; i < tasks.length; i++) {
|
|
66
|
+
var w = Math.floor(Number(tasks[i].wave));
|
|
67
|
+
col[tasks[i].id] = isFinite(w) && w >= 1 && tasks[i].wave !== null ? w : 0;
|
|
68
|
+
}
|
|
69
|
+
var floating = tasks.filter(function (t) { return col[t.id] === 0; });
|
|
70
|
+
floating.forEach(function (t) { col[t.id] = 1; });
|
|
71
|
+
for (pass = 0; pass < tasks.length; pass++) {
|
|
72
|
+
var changed = false;
|
|
73
|
+
for (i = 0; i < floating.length; i++) {
|
|
74
|
+
var t = floating[i], deepest = 0;
|
|
75
|
+
var deps = Array.isArray(t.deps) ? t.deps : [];
|
|
76
|
+
for (var d = 0; d < deps.length; d++) {
|
|
77
|
+
if (deps[d] === t.id || !byId[deps[d]]) continue;
|
|
78
|
+
if (col[deps[d]] > deepest) deepest = col[deps[d]];
|
|
79
|
+
}
|
|
80
|
+
var want = deepest ? deepest + 1 : 1;
|
|
81
|
+
if (want !== col[t.id]) { col[t.id] = want; changed = true; }
|
|
82
|
+
}
|
|
83
|
+
if (!changed) break;
|
|
84
|
+
}
|
|
85
|
+
return col;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function edgePath(a, b, self) {
|
|
89
|
+
var x1 = a.x + NODE_W, y1 = a.y + NODE_H / 2;
|
|
90
|
+
var x2 = b.x, y2 = b.y + NODE_H / 2;
|
|
91
|
+
if (self) {
|
|
92
|
+
return 'M' + x1 + ' ' + y1 + ' C' + (x1 + 34) + ' ' + (y1 - NODE_H) + ' ' +
|
|
93
|
+
(x2 - 34) + ' ' + (y2 - NODE_H) + ' ' + x2 + ' ' + y2;
|
|
94
|
+
}
|
|
95
|
+
var dx = x2 - x1;
|
|
96
|
+
var k = dx >= 2 * 30 ? dx / 2 : 60; // backward/same-column edges bulge out so they stay visible
|
|
97
|
+
return 'M' + x1 + ' ' + y1 + ' C' + (x1 + k) + ' ' + y1 + ' ' + (x2 - k) + ' ' + y2 + ' ' + x2 + ' ' + y2;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function render(container, run, options) {
|
|
101
|
+
if (!container) return;
|
|
102
|
+
options = options || {};
|
|
103
|
+
while (container.firstChild) container.removeChild(container.firstChild);
|
|
104
|
+
|
|
105
|
+
var tasks = run && Array.isArray(run.tasks) ? run.tasks.filter(function (t) {
|
|
106
|
+
return t && t.id !== null && t.id !== undefined;
|
|
107
|
+
}) : [];
|
|
108
|
+
if (!tasks.length) { message(container, 'No tasks'); return; }
|
|
109
|
+
|
|
110
|
+
var byId = Object.create(null);
|
|
111
|
+
tasks.forEach(function (t) { if (!byId[t.id]) byId[t.id] = t; });
|
|
112
|
+
var col = computeColumns(tasks, byId);
|
|
113
|
+
|
|
114
|
+
// Densify columns: only waves actually used get a column, so a huge wave number can't blow up the layout.
|
|
115
|
+
var used = Object.create(null), waves = [];
|
|
116
|
+
tasks.forEach(function (t) { if (!used[col[t.id]]) { used[col[t.id]] = true; waves.push(col[t.id]); } });
|
|
117
|
+
waves.sort(function (a, b) { return a - b; });
|
|
118
|
+
var idx = Object.create(null);
|
|
119
|
+
waves.forEach(function (w, i) { idx[w] = i + 1; });
|
|
120
|
+
var label = {};
|
|
121
|
+
waves.forEach(function (w) { label[idx[w]] = w; });
|
|
122
|
+
tasks.forEach(function (t) { col[t.id] = idx[col[t.id]]; });
|
|
123
|
+
|
|
124
|
+
var maxCol = 1, rows = Object.create(null), pos = Object.create(null), maxRows = 0;
|
|
125
|
+
tasks.forEach(function (t) { if (col[t.id] > maxCol) maxCol = col[t.id]; });
|
|
126
|
+
tasks.forEach(function (t) {
|
|
127
|
+
if (pos[t.id]) return;
|
|
128
|
+
var c = col[t.id];
|
|
129
|
+
var r = rows[c] = (rows[c] || 0);
|
|
130
|
+
rows[c] = r + 1;
|
|
131
|
+
if (rows[c] > maxRows) maxRows = rows[c];
|
|
132
|
+
pos[t.id] = {
|
|
133
|
+
x: PAD + (c - 1) * (NODE_W + COL_GAP),
|
|
134
|
+
y: PAD + HEADER_H + r * (NODE_H + ROW_GAP)
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
var width = PAD * 2 + maxCol * NODE_W + (maxCol - 1) * COL_GAP;
|
|
139
|
+
var height = PAD * 2 + HEADER_H + maxRows * NODE_H + (maxRows - 1) * ROW_GAP + 20;
|
|
140
|
+
|
|
141
|
+
var svg = el('svg', {
|
|
142
|
+
'class': 'fg-svg', viewBox: '0 0 ' + width + ' ' + height,
|
|
143
|
+
width: width, height: height, role: 'group', 'aria-label': 'Task dependency graph'
|
|
144
|
+
});
|
|
145
|
+
svg.style.maxWidth = 'none';
|
|
146
|
+
|
|
147
|
+
var markerId = 'fg-arrow-' + (++uid);
|
|
148
|
+
var defs = el('defs', null, svg);
|
|
149
|
+
var marker = el('marker', {
|
|
150
|
+
id: markerId, viewBox: '0 0 10 10', refX: 9, refY: 5,
|
|
151
|
+
markerWidth: 7, markerHeight: 7, orient: 'auto'
|
|
152
|
+
}, defs);
|
|
153
|
+
el('path', { d: 'M0 0 L10 5 L0 10 z', fill: 'currentColor', 'fill-opacity': 0.6 }, marker);
|
|
154
|
+
|
|
155
|
+
for (var c = 1; c <= maxCol; c++) {
|
|
156
|
+
textEl(svg, 'wave ' + label[c], {
|
|
157
|
+
'class': 'fg-wave', x: PAD + (c - 1) * (NODE_W + COL_GAP), y: PAD + 10,
|
|
158
|
+
'font-size': 12, fill: 'currentColor', 'fill-opacity': 0.6
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
var edgeLayer = el('g', { 'class': 'fg-edges', color: 'inherit' }, svg);
|
|
163
|
+
var edgesOf = Object.create(null);
|
|
164
|
+
var seen = Object.create(null);
|
|
165
|
+
tasks.forEach(function (t) {
|
|
166
|
+
if (seen[t.id]) return;
|
|
167
|
+
seen[t.id] = true;
|
|
168
|
+
var deps = Array.isArray(t.deps) ? t.deps : [];
|
|
169
|
+
var done = Object.create(null);
|
|
170
|
+
deps.forEach(function (d) {
|
|
171
|
+
if (!byId[d] || done[d]) return;
|
|
172
|
+
done[d] = true;
|
|
173
|
+
var path = el('path', {
|
|
174
|
+
'class': 'fg-edge', d: edgePath(pos[d], pos[t.id], d === t.id),
|
|
175
|
+
fill: 'none', stroke: 'currentColor', 'stroke-opacity': 0.4,
|
|
176
|
+
'stroke-width': 1.5, 'marker-end': 'url(#' + markerId + ')'
|
|
177
|
+
}, edgeLayer);
|
|
178
|
+
var ends = { path: path, from: d, to: t.id };
|
|
179
|
+
(edgesOf[t.id] = edgesOf[t.id] || []).push(ends);
|
|
180
|
+
if (d !== t.id) (edgesOf[d] = edgesOf[d] || []).push(ends);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
var active = Object.create(null); // id -> {hover, focus}
|
|
185
|
+
function highlight(id, kind, flag) {
|
|
186
|
+
var a = active[id] = active[id] || {};
|
|
187
|
+
a[kind] = flag;
|
|
188
|
+
(edgesOf[id] || []).forEach(function (e) {
|
|
189
|
+
var p = e.path;
|
|
190
|
+
var fa = active[e.from], ta = active[e.to];
|
|
191
|
+
var on = !!((fa && (fa.hover || fa.focus)) || (ta && (ta.hover || ta.focus)));
|
|
192
|
+
p.setAttribute('stroke-opacity', on ? 0.95 : 0.4);
|
|
193
|
+
p.setAttribute('stroke-width', on ? 2.5 : 1.5);
|
|
194
|
+
if (on) p.setAttribute('class', 'fg-edge fg-edge-hi');
|
|
195
|
+
else p.setAttribute('class', 'fg-edge');
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
var drawn = Object.create(null);
|
|
200
|
+
tasks.forEach(function (t) {
|
|
201
|
+
if (drawn[t.id]) return;
|
|
202
|
+
drawn[t.id] = true;
|
|
203
|
+
var p = pos[t.id];
|
|
204
|
+
var status = str(t.status, 'pending');
|
|
205
|
+
var stClass = 'st-' + status.toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
206
|
+
var selected = options.selected === t.id;
|
|
207
|
+
var id = String(t.id);
|
|
208
|
+
|
|
209
|
+
var g = el('g', {
|
|
210
|
+
'class': 'fg-node ' + stClass + (selected ? ' fg-selected' : ''),
|
|
211
|
+
transform: 'translate(' + p.x + ' ' + p.y + ')',
|
|
212
|
+
tabindex: 0, role: 'button',
|
|
213
|
+
'aria-label': 'Task ' + id + ', status ' + status,
|
|
214
|
+
'data-id': id
|
|
215
|
+
}, svg);
|
|
216
|
+
g.style.cursor = 'pointer';
|
|
217
|
+
|
|
218
|
+
var tip = [str(t.title, id), 'status: ' + status,
|
|
219
|
+
'dwarf: ' + str(t.dwarf, '—') + ' / qa: ' + str(t.qa, '—')];
|
|
220
|
+
if (t.verdict) tip.push('verdict: ' + t.verdict);
|
|
221
|
+
if (t.explored) tip.push('explored');
|
|
222
|
+
var title = el('title', null, g);
|
|
223
|
+
title.textContent = tip.join('\n');
|
|
224
|
+
|
|
225
|
+
el('rect', {
|
|
226
|
+
'class': 'fg-box', x: 0, y: 0, width: NODE_W, height: NODE_H, rx: 8, ry: 8,
|
|
227
|
+
'stroke-width': selected ? 3.5 : 1.5,
|
|
228
|
+
'stroke-dasharray': t.explored ? '5 3' : 'none'
|
|
229
|
+
}, g);
|
|
230
|
+
el('rect', { 'class': 'fg-bar', x: 0, y: 6, width: 4, height: NODE_H - 12, rx: 2 }, g);
|
|
231
|
+
|
|
232
|
+
var maxPx = NODE_W - 36;
|
|
233
|
+
textEl(g, ellipsize(id, maxPx, 13, 1.1), {
|
|
234
|
+
x: 12, y: 19, 'font-size': 13, 'font-weight': 'bold', fill: 'currentColor'
|
|
235
|
+
});
|
|
236
|
+
var spec = t.explored ? ellipsize(str(t.dwarf, '—'), maxPx - 12, 12) + ' *' : ellipsize(str(t.dwarf, '—'), maxPx, 12);
|
|
237
|
+
textEl(g, spec, {
|
|
238
|
+
x: 12, y: 36, 'font-size': 12, fill: 'currentColor', 'fill-opacity': 0.75
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (t.verdict === 'PASS' || t.verdict === 'FAIL') {
|
|
242
|
+
var pass = t.verdict === 'PASS';
|
|
243
|
+
textEl(g, pass ? '✓' : '✗', {
|
|
244
|
+
'class': 'fg-verdict' + (pass ? ' fg-pass' : ''), x: NODE_W - 8, y: 17, 'text-anchor': 'end', 'font-size': 14, 'font-weight': 'bold'
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function select() { if (typeof options.onSelect === 'function') options.onSelect(t.id); }
|
|
249
|
+
g.addEventListener('click', select);
|
|
250
|
+
g.addEventListener('keydown', function (ev) {
|
|
251
|
+
if (ev.key === 'Enter' || ev.key === ' ' || ev.key === 'Spacebar') {
|
|
252
|
+
ev.preventDefault();
|
|
253
|
+
select();
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
g.addEventListener('mouseenter', function () { highlight(t.id, 'hover', true); });
|
|
257
|
+
g.addEventListener('mouseleave', function () { highlight(t.id, 'hover', false); });
|
|
258
|
+
g.addEventListener('focus', function () { highlight(t.id, 'focus', true); });
|
|
259
|
+
g.addEventListener('blur', function () { highlight(t.id, 'focus', false); });
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
container.appendChild(svg);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
window.ForgeGraph = { render: render };
|
|
266
|
+
})();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<meta name="referrer" content="no-referrer">
|
|
8
|
+
<title>forge-ui</title>
|
|
9
|
+
<script type="importmap">
|
|
10
|
+
{ "imports": {
|
|
11
|
+
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
|
|
12
|
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
|
|
13
|
+
} }
|
|
14
|
+
</script>
|
|
15
|
+
<link rel="icon" href="favicon.svg" type="image/svg+xml">
|
|
16
|
+
<link rel="stylesheet" href="style.css">
|
|
17
|
+
<script src="runmodel.js" defer></script>
|
|
18
|
+
<script src="cloudmodel.js" defer></script>
|
|
19
|
+
<script src="modelstats.js" defer></script>
|
|
20
|
+
<script src="graph.js" defer></script>
|
|
21
|
+
<script src="gantt.js" defer></script>
|
|
22
|
+
<script src="panel.js" defer></script>
|
|
23
|
+
<script src="diffview.js" defer></script>
|
|
24
|
+
<script src="files.js" defer></script>
|
|
25
|
+
<script src="map3d.js" defer></script>
|
|
26
|
+
<script src="cloudui.js" defer></script>
|
|
27
|
+
<script src="leaderboard.js" defer></script>
|
|
28
|
+
<script src="search.js" defer></script>
|
|
29
|
+
<script src="notify.js" defer></script>
|
|
30
|
+
<script src="app.js" defer></script>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<header class="topbar">
|
|
34
|
+
<a class="brand" href="#/">forge-ui</a>
|
|
35
|
+
<a class="top-link" href="#/">Map of all runs</a>
|
|
36
|
+
<a class="top-link" href="#/cloud">Run cloud</a>
|
|
37
|
+
<a class="top-link" href="#/models">Models</a>
|
|
38
|
+
<span class="muted">Local run viewer · read only</span>
|
|
39
|
+
<button class="top-search" type="button" data-jump title="Jump to a run, task, project, model or command (/)">Search <kbd>Ctrl K</kbd></button>
|
|
40
|
+
<button id="bell" class="bell" type="button" aria-label="Notifications"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 3a6 6 0 0 0-6 6v3.6L4.3 15.4A1 1 0 0 0 5.2 17h13.6a1 1 0 0 0 .9-1.6L18 12.6V9a6 6 0 0 0-6-6Zm-2.6 15.5a2.7 2.7 0 0 0 5.2 0Z"/></svg><span class="bell-count" hidden></span></button>
|
|
41
|
+
<label class="refresh-control"><input id="refresh" type="checkbox" checked> Live refresh</label>
|
|
42
|
+
</header>
|
|
43
|
+
<div id="errors" role="status" aria-live="polite" hidden></div>
|
|
44
|
+
<div class="layout">
|
|
45
|
+
<aside class="sidebar" aria-label="Runs">
|
|
46
|
+
<div class="sidebar-heading"><h1>Runs</h1><span id="run-count" class="muted"></span></div>
|
|
47
|
+
<label class="filter-label" for="filter">Filter by goal, repo, ID, path or model</label>
|
|
48
|
+
<input id="filter" type="search" placeholder="Filter runs…" autocomplete="off">
|
|
49
|
+
<nav id="run-list" aria-label="Choose a run"><p class="empty">Loading runs…</p></nav>
|
|
50
|
+
</aside>
|
|
51
|
+
<main id="main" tabindex="-1"><p class="empty">Loading…</p></main>
|
|
52
|
+
</div>
|
|
53
|
+
<div id="live" class="visually-hidden" aria-live="polite"></div>
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|