@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.
- package/CHANGELOG.md +205 -0
- package/LICENSE +21 -0
- package/README.md +194 -2
- package/SKILL.md +123 -0
- package/VERSION +1 -0
- package/docs/README.md +26 -0
- package/docs/add-ons.md +33 -0
- package/docs/artifacts.md +69 -0
- package/docs/commands.md +315 -0
- package/docs/dataset.md +20 -0
- package/docs/decomposed-runs.md +268 -0
- package/docs/development.md +95 -0
- package/docs/forge-ui.md +92 -0
- package/docs/fractal.md +181 -0
- package/docs/install.md +160 -0
- package/docs/memory.md +156 -0
- package/docs/models.md +118 -0
- package/docs/reviews.md +231 -0
- package/docs/safety.md +55 -0
- package/docs/troubleshooting.md +54 -0
- package/forge +79 -0
- package/package.json +27 -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 +365 -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 +671 -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-runs.py +121 -0
- package/scripts/forge-runtime.py +586 -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 +240 -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 +84 -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 +347 -0
- package/ui/THIRD_PARTY_NOTICES.md +177 -0
- package/ui/docs/api.md +970 -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 +214 -0
- package/ui/forge_ui/findings.py +132 -0
- package/ui/forge_ui/fractal.py +582 -0
- package/ui/forge_ui/models.py +216 -0
- package/ui/forge_ui/reader.py +1424 -0
- package/ui/forge_ui/server.py +312 -0
- package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
- package/ui/forge_ui/static/app.js +1079 -0
- package/ui/forge_ui/static/cloud3d.js +647 -0
- package/ui/forge_ui/static/cloudmodel.js +375 -0
- package/ui/forge_ui/static/cloudui.js +397 -0
- package/ui/forge_ui/static/convo.js +295 -0
- package/ui/forge_ui/static/datatable.js +232 -0
- package/ui/forge_ui/static/diffview.js +235 -0
- package/ui/forge_ui/static/favicon.svg +1 -0
- package/ui/forge_ui/static/files.js +776 -0
- package/ui/forge_ui/static/findings3d.js +134 -0
- package/ui/forge_ui/static/gantt.js +97 -0
- package/ui/forge_ui/static/graph.js +266 -0
- package/ui/forge_ui/static/icons.js +94 -0
- package/ui/forge_ui/static/index.html +67 -0
- package/ui/forge_ui/static/iso.js +816 -0
- package/ui/forge_ui/static/leaderboard.js +311 -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 +562 -0
- package/ui/forge_ui/static/runmodel.js +667 -0
- package/ui/forge_ui/static/scene3d.js +1471 -0
- package/ui/forge_ui/static/search.js +120 -0
- package/ui/forge_ui/static/shell.js +137 -0
- package/ui/forge_ui/static/stagectl.js +150 -0
- package/ui/forge_ui/static/style.css +1073 -0
- package/ui/forge_ui/static/theme.js +25 -0
- package/ui/forge_ui/transcripts.py +608 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/* forge-ui finding beacons: every review finding as a small mark over the stage. Its shape is its severity
|
|
2
|
+
(P0 spike, P1 diamond, P2 cone, P3 pin, no severity a ring), drawn like the blocks: a hairline in its --sev-*
|
|
3
|
+
colour over a translucent fill of the same, on a short post, its foot on the block it is about; a confirmed
|
|
4
|
+
finding is full size and fuller, a plausible one smaller and paler. Seen from straight above (the top-down
|
|
5
|
+
projection) the marks lie flat, so each keeps its silhouette. scene3d.js says where each one stands (an anchor
|
|
6
|
+
that follows its block as it lifts, opens or travels); this module draws them, one instanced mesh per shape
|
|
7
|
+
with the engine's hairline outline, and picks them. Marks stand still, so an idle stage draws no frames; one
|
|
8
|
+
that arrives (a new finding on a poll, one a replay reaches, one moving onto an opened task) drops in. */
|
|
9
|
+
export const SHAPES = ['P0', 'P1', 'P2', 'P3', 'none'];
|
|
10
|
+
const MAX = 512; // marks per shape
|
|
11
|
+
const SIZE = 18; // plan units per unit of the shapes below: a mark stands ~40 tall
|
|
12
|
+
const MIN_PX = 28; // a mark stands at least this tall on screen while the plate is near
|
|
13
|
+
const FAR_PPU = .5; // screen px per plan unit below which that floor shrinks with the plate, to half
|
|
14
|
+
const TALL = 2.2; // a mark's height in shape units (post and head)
|
|
15
|
+
|
|
16
|
+
export function createBeacons(E, { reduce = false } = {}) {
|
|
17
|
+
const { THREE, W, TS } = E;
|
|
18
|
+
const POST = .3; // the post's height; every head sits on it
|
|
19
|
+
const post = () => new THREE.CylinderGeometry(.035, .035, POST, 6).translate(0, POST / 2, 0);
|
|
20
|
+
const geo = {
|
|
21
|
+
P0: mergeTwo(THREE, new THREE.ConeGeometry(.42, 1.6, 4).translate(0, POST + .8, 0), post()),
|
|
22
|
+
P1: mergeTwo(THREE, new THREE.OctahedronGeometry(.62).scale(1, 1.3, 1).translate(0, POST + .8, 0), post()),
|
|
23
|
+
P2: mergeTwo(THREE, new THREE.ConeGeometry(.62, 1.2, 16).rotateX(Math.PI).translate(0, POST + .6, 0), post()),
|
|
24
|
+
P3: mergeTwo(THREE, new THREE.SphereGeometry(.5, 18, 12).translate(0, POST + .48, 0), post()),
|
|
25
|
+
none: mergeTwo(THREE, new THREE.TorusGeometry(.5, .12, 8, 24).translate(0, POST + .56, 0), post()),
|
|
26
|
+
};
|
|
27
|
+
const group = new THREE.Group(); group.name = 'beacons';
|
|
28
|
+
const meshes = {}, outlines = {};
|
|
29
|
+
for (const k of SHAPES) {
|
|
30
|
+
const m = new THREE.InstancedMesh(geo[k], new THREE.MeshBasicMaterial({ toneMapped: false, transparent: true, depthWrite: false }), MAX);
|
|
31
|
+
m.count = 0; m.frustumCulled = false; m.userData = { beacons: k }; m.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
|
32
|
+
m.setColorAt(0, new THREE.Color()); // allocates the per-instance colours
|
|
33
|
+
const o = new THREE.InstancedMesh(geo[k], E.outlineInstanced(), MAX);
|
|
34
|
+
o.material.side = THREE.BackSide; o.count = 0; o.frustumCulled = false; o.instanceMatrix = m.instanceMatrix; o.raycast = () => {};
|
|
35
|
+
group.add(m, o); meshes[k] = m; outlines[k] = o;
|
|
36
|
+
}
|
|
37
|
+
let unit = SIZE; // plan units per shape unit this frame (marks grow when far away)
|
|
38
|
+
let items = [], byShape = Object.fromEntries(SHAPES.map(k => [k, []])), seen = null, visible = true, hot = null, flat = false;
|
|
39
|
+
const FLAT = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -Math.PI / 2); // the head toward the top of the screen
|
|
40
|
+
const M4 = new THREE.Matrix4(), Q = new THREE.Quaternion(), S = new THREE.Vector3(), P = new THREE.Vector3(), C = new THREE.Color();
|
|
41
|
+
const ez = t => 1 - Math.pow(1 - t, 3);
|
|
42
|
+
|
|
43
|
+
// items: [{key, sev: 'P0'..'P3'|null, plausible, anchor() → [x, y, z, scale?] in plan units | null (hidden), data}]
|
|
44
|
+
function set(next) {
|
|
45
|
+
const now = performance.now(), first = seen === null;
|
|
46
|
+
seen = seen || new Set();
|
|
47
|
+
items = next.map(it => ({ ...it, shape: SHAPES.includes(it.sev) ? it.sev : 'none', born: first || seen.has(it.key) || reduce ? -Infinity : now,
|
|
48
|
+
phase: (hash(it.key) % 628) / 100 }));
|
|
49
|
+
for (const it of items) seen.add(it.key);
|
|
50
|
+
byShape = Object.fromEntries(SHAPES.map(k => [k, []]));
|
|
51
|
+
for (const it of items) if (byShape[it.shape].length < MAX) byShape[it.shape].push(it);
|
|
52
|
+
paint();
|
|
53
|
+
}
|
|
54
|
+
// the fill is the severity's colour seen through: fuller when confirmed, paler when plausible; the hairline is the colour itself
|
|
55
|
+
function paint() {
|
|
56
|
+
const bg = E.BG_COL, light = E.theme === 'light';
|
|
57
|
+
for (const k of SHAPES) {
|
|
58
|
+
const m = meshes[k], list = byShape[k];
|
|
59
|
+
list.forEach((it, i) => {
|
|
60
|
+
C.copy(E.sevColor(it.sev || 'none')).lerp(bg, it.plausible ? .7 : .45);
|
|
61
|
+
if (hot === it.key) C.lerp(E.INKHI, .3);
|
|
62
|
+
m.setColorAt(i, C);
|
|
63
|
+
});
|
|
64
|
+
if (m.instanceColor) m.instanceColor.needsUpdate = true;
|
|
65
|
+
m.material.opacity = light ? .82 : .75;
|
|
66
|
+
outlines[k].material.uniforms.color.value.copy(E.sevColor(k));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// every frame: each mark where its anchor is; one that just arrived falls in. Returns whether anything moves.
|
|
70
|
+
function tick(now) {
|
|
71
|
+
if (!visible) { for (const k of SHAPES) meshes[k].count = outlines[k].count = 0; return false; }
|
|
72
|
+
let moving = false;
|
|
73
|
+
const ppu = Math.max(1e-6, E.pxPerUnit()), least = MIN_PX * Math.min(1, Math.max(.5, ppu / FAR_PPU)) / (ppu * TALL);
|
|
74
|
+
unit = Math.max(SIZE, least);
|
|
75
|
+
for (const k of SHAPES) {
|
|
76
|
+
const m = meshes[k], list = byShape[k];
|
|
77
|
+
let n = 0;
|
|
78
|
+
for (const it of list) {
|
|
79
|
+
const a = it.anchor();
|
|
80
|
+
if (!a) { it.hidden = true; continue; }
|
|
81
|
+
if (it.hidden && !reduce) it.born = now; // shown again: it arrives
|
|
82
|
+
it.hidden = false;
|
|
83
|
+
const age = (now - it.born) / 650, drop = age < 1 ? 1 - ez(Math.max(0, age)) : 0;
|
|
84
|
+
if (drop > 0) moving = true;
|
|
85
|
+
const sc = unit * (a[3] || 1) * (it.plausible ? .8 : 1) * (hot === it.key ? 1.2 : 1);
|
|
86
|
+
const lie = flat ? .7 * sc : 0; // lying flat, it floats its own thickness over the roof
|
|
87
|
+
P.copy(W(a[0], a[1], a[2] + drop * 60 + lie));
|
|
88
|
+
it.head = flat ? [a[0], a[1] - 1.15 * sc, a[2] + lie] : [a[0], a[1], a[2] + drop * 60 + 1.15 * sc]; // where its head is drawn, in plan units
|
|
89
|
+
Q.setFromAxisAngle(UP, it.phase); if (flat) Q.premultiply(FLAT);
|
|
90
|
+
S.setScalar(sc * TS);
|
|
91
|
+
M4.compose(P, Q, S); m.setMatrixAt(n, M4); it.slot = n; n++;
|
|
92
|
+
}
|
|
93
|
+
m.count = outlines[k].count = n; m.instanceMatrix.needsUpdate = true;
|
|
94
|
+
m.boundingSphere = null; // three.js keeps the first one it computes: picking needs today's
|
|
95
|
+
}
|
|
96
|
+
return moving;
|
|
97
|
+
}
|
|
98
|
+
const UP = new THREE.Vector3(0, 1, 0);
|
|
99
|
+
// the item under the pointer, or null
|
|
100
|
+
function pick(cx, cy) {
|
|
101
|
+
if (!visible) return null;
|
|
102
|
+
const h = E.pickHit(cx, cy, SHAPES.map(k => meshes[k]).filter(m => m.count));
|
|
103
|
+
if (!h || h.instanceId == null) return null;
|
|
104
|
+
const k = h.object.userData.beacons;
|
|
105
|
+
return byShape[k].find(it => it.slot === h.instanceId && it.anchor()) || null;
|
|
106
|
+
}
|
|
107
|
+
function setHot(key) { if (key !== hot) { hot = key; paint(); } }
|
|
108
|
+
E.themeHooks.add(() => paint());
|
|
109
|
+
return {
|
|
110
|
+
group, set, tick, pick, setHot, repaint: paint,
|
|
111
|
+
get count() { return items.length; },
|
|
112
|
+
get unit() { return unit; }, // what scene3d spaces marks by
|
|
113
|
+
get items() { return items; },
|
|
114
|
+
setVisible(v) { visible = !!v; group.visible = visible; },
|
|
115
|
+
setFlat(v) { flat = !!v; }, // the top-down projection
|
|
116
|
+
get flat() { return flat; },
|
|
117
|
+
dispose() { for (const k of SHAPES) { meshes[k].dispose(); outlines[k].dispose(); geo[k].dispose(); } },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function hash(s) { let h = 7; for (const c of String(s)) h = (h * 31 + c.charCodeAt(0)) >>> 0; return h; }
|
|
122
|
+
// two geometries as one (positions and normals), so the pin is one instance; one without an index (the
|
|
123
|
+
// octahedron) draws its vertices in order
|
|
124
|
+
function mergeTwo(THREE, a, b) {
|
|
125
|
+
const out = new THREE.BufferGeometry(), na = a.attributes.position.count, nb = b.attributes.position.count;
|
|
126
|
+
const cat = (x, y, n) => { const r = new Float32Array(x.length + y.length); r.set(x); r.set(y, x.length); return new THREE.BufferAttribute(r, n); };
|
|
127
|
+
out.setAttribute('position', cat(a.attributes.position.array, b.attributes.position.array, 3));
|
|
128
|
+
out.setAttribute('normal', cat(a.attributes.normal.array, b.attributes.normal.array, 3));
|
|
129
|
+
const order = (g, n) => g.index ? Array.from(g.index.array) : Array.from({ length: n }, (_, i) => i);
|
|
130
|
+
const ia = order(a, na), ib = order(b, nb).map(i => i + na);
|
|
131
|
+
out.setIndex(ia.concat(ib));
|
|
132
|
+
a.dispose(); b.dispose();
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
@@ -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
|
+
// a drawn tick or cross, in the verdict's colour
|
|
244
|
+
el('path', { 'class': 'fg-verdict' + (pass ? ' fg-pass' : ''), d: pass ? 'M' + (NODE_W - 20) + ' 12l4 4 7-8' : 'M' + (NODE_W - 19) + ' 8l8 8m0-8l-8 8',
|
|
245
|
+
fill: 'none', 'stroke-width': 1.8, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }, g);
|
|
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,94 @@
|
|
|
1
|
+
/* forge-ui icons: one hairline set drawn for forge-ui on a 24-unit grid, 1.5 stroke, round joins, currentColor.
|
|
2
|
+
States come from CSS (colour, opacity); an icon never carries its own colour. Built with createElementNS: the page CSP
|
|
3
|
+
refuses markup parsed from strings. Also the model name plate, the one way a model is shown everywhere.
|
|
4
|
+
window.ForgeIcons = { icon(name, title?) → <svg class="icon icon-<name>">, modelPlate(spec) → <span class="model-plate">, names } */
|
|
5
|
+
window.ForgeIcons = (() => {
|
|
6
|
+
'use strict';
|
|
7
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
8
|
+
const P = d => ['path', { d }], C = (cx, cy, r) => ['circle', { cx, cy, r }], R = (x, y, width, height, rx = 1.5) => ['rect', { x, y, width, height, rx }];
|
|
9
|
+
const SET = {
|
|
10
|
+
map: [R(3, 4, 18, 16, 2), P('M7 9h4v3H7zM13 9h4v6h-4zM7 14h4v2H7')],
|
|
11
|
+
cloud: [C(12, 12, 2.2), P('M3.5 12c0-2.5 3.8-4.5 8.5-4.5s8.5 2 8.5 4.5-3.8 4.5-8.5 4.5S3.5 14.5 3.5 12Z'), C(18.5, 9.2, 1)],
|
|
12
|
+
models: [P('M5 20V11M10 20V5M15 20v-7M20 20V8M3 20h18')],
|
|
13
|
+
search: [C(10.5, 10.5, 6), P('m15 15 5.5 5.5')],
|
|
14
|
+
bell: [P('M6 16.5V11a6 6 0 0 1 12 0v5.5l1.5 1.5h-15Z'), P('M10 20.5a2.2 2.2 0 0 0 4 0')],
|
|
15
|
+
live: [C(12, 12, 2), P('M7.8 7.8a6 6 0 0 0 0 8.4M16.2 7.8a6 6 0 0 1 0 8.4M5 5a10 10 0 0 0 0 14M19 5a10 10 0 0 1 0 14')],
|
|
16
|
+
sun: [C(12, 12, 3.8), P('M12 3v2M12 19v2M3 12h2M19 12h2M5.6 5.6 7 7M17 17l1.4 1.4M5.6 18.4 7 17M17 7l1.4-1.4')],
|
|
17
|
+
moon: [P('M19.5 14.5A7.5 7.5 0 0 1 9.5 4.5a7.5 7.5 0 1 0 10 10Z')],
|
|
18
|
+
system: [R(3, 4.5, 18, 12, 1.5), P('M9 20h6M12 16.5V20')],
|
|
19
|
+
immersive: [R(3, 4, 18, 16, 2), R(5.5, 6.5, 4, 8, 1), R(14.5, 6.5, 4, 11, 1)],
|
|
20
|
+
workbench: [R(3, 4, 18, 16, 2), P('M8 4v16M16 4v16M8 15h8')],
|
|
21
|
+
'panel-left': [R(3, 4, 18, 16, 2), P('M9 4v16')],
|
|
22
|
+
'panel-right': [R(3, 4, 18, 16, 2), P('M15 4v16')],
|
|
23
|
+
'panel-bottom': [R(3, 4, 18, 16, 2), P('M3 14h18')],
|
|
24
|
+
close: [P('M6 6l12 12M18 6 6 18')],
|
|
25
|
+
'chevron-down': [P('m6 9 6 6 6-6')],
|
|
26
|
+
'chevron-up': [P('m6 15 6-6 6 6')],
|
|
27
|
+
'chevron-right': [P('m9 6 6 6-6 6')],
|
|
28
|
+
'chevron-left': [P('m15 6-6 6 6 6')],
|
|
29
|
+
play: [P('M8 5.5v13l10-6.5Z')],
|
|
30
|
+
pause: [P('M8 5.5v13M16 5.5v13')],
|
|
31
|
+
plan: [P('M4 5v14M10 5v14M16 5v14'), R(5.5, 7, 3, 3, .8), R(11.5, 9, 3, 3, .8), R(11.5, 14, 3, 3, .8), R(17.5, 11, 3, 3, .8)],
|
|
32
|
+
exec: [P('M3 5v14'), R(5, 6, 8, 3, 1), R(9, 10.5, 10, 3, 1), R(6, 15, 6, 3, 1)],
|
|
33
|
+
files: [P('M6 3.5h8l4 4v13H6Z'), P('M14 3.5v4h4M9 12h6M9 15.5h6')],
|
|
34
|
+
cube: [P('M12 3 20 7.5v9L12 21l-8-4.5v-9Z'), P('M4 7.5 12 12l8-4.5M12 12v9')],
|
|
35
|
+
square: [R(5, 5, 14, 14, 1.5), P('M5 12h14M12 5v14')],
|
|
36
|
+
layers: [P('m12 4 9 4.5-9 4.5-9-4.5Z'), P('m3 12.5 9 4.5 9-4.5'), P('m3 16.5 9 4.5 9-4.5')],
|
|
37
|
+
fit: [P('M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5'), R(9, 9, 6, 6, 1)],
|
|
38
|
+
persp: [P('M3.5 19 8.5 5h7l5 14Z'), P('M10.5 5 9.5 19M13.5 5l1 14M6 12h12')],
|
|
39
|
+
iso: [P('M12 4.5 18.5 8v8L12 19.5 5.5 16V8Z'), P('M5.5 8 12 11.5 18.5 8M12 11.5v8')],
|
|
40
|
+
top: [R(4, 4, 16, 16, 2), R(8.5, 8.5, 7, 7, 1)],
|
|
41
|
+
reset: [P('M4.5 12a7.5 7.5 0 1 0 2.2-5.3'), P('M4.5 4.5v3.5H8')],
|
|
42
|
+
'zoom-in': [C(11, 11, 6.5), P('m16 16 4.5 4.5M8.5 11h5M11 8.5v5')],
|
|
43
|
+
'zoom-out': [C(11, 11, 6.5), P('m16 16 4.5 4.5M8.5 11h5')],
|
|
44
|
+
fullscreen: [P('M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5')],
|
|
45
|
+
legend: [C(6, 7, 1.5), C(6, 12, 1.5), C(6, 17, 1.5), P('M10 7h9M10 12h9M10 17h6')],
|
|
46
|
+
help: [C(12, 12, 9), P('M9.5 9.5a2.6 2.6 0 0 1 5 1c0 1.8-2.5 2.2-2.5 3.8'), C(12, 17, .6)],
|
|
47
|
+
sort: [P('M8 5v14M5 8l3-3 3 3M16 19V5M13 16l3 3 3-3')],
|
|
48
|
+
'sort-asc': [P('M12 19V5M7 10l5-5 5 5')],
|
|
49
|
+
'sort-desc': [P('M12 5v14M7 14l5 5 5-5')],
|
|
50
|
+
columns: [R(3, 4, 18, 16, 2), P('M9 4v16M15 4v16')],
|
|
51
|
+
grip: [C(9, 7, .9), C(15, 7, .9), C(9, 12, .9), C(15, 12, .9), C(9, 17, .9), C(15, 17, .9)],
|
|
52
|
+
download: [P('M12 4v11M7.5 10.5 12 15l4.5-4.5M5 19.5h14')],
|
|
53
|
+
flag: [P('M6 21V4M6 4.5h11l-2.5 4 2.5 4H6')],
|
|
54
|
+
check: [P('m5 12.5 4.5 4.5L19 7.5')],
|
|
55
|
+
cross: [P('M6 6l12 12M18 6 6 18')],
|
|
56
|
+
filter: [P('M4 5h16l-6 7.5V19l-4-2v-4.5Z')],
|
|
57
|
+
arrow: [P('M5 12h14M13 6l6 6-6 6')],
|
|
58
|
+
eye: [P('M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z'), C(12, 12, 3)],
|
|
59
|
+
route: [C(5, 17, 2), C(19, 7, 2), P('M7 17h4l2-10h4')],
|
|
60
|
+
keyboard: [R(2.5, 6, 19, 12, 2), P('M6 10h1M9.5 10h1M13 10h1M16.5 10h1M7 14h10')],
|
|
61
|
+
chat: [P('M4 5.5h16v10.5H10l-4.5 3.5V16H4Z'), P('M8 9.5h8M8 12.5h5')],
|
|
62
|
+
terminal: [R(3, 4.5, 18, 15, 2), P('m7 9.5 3 2.5-3 2.5M12.5 15H17')],
|
|
63
|
+
pencil: [P('M4.5 19.5 5.5 15 15.5 5a2.1 2.1 0 0 1 3 3l-10 10Z'), P('m13.5 7 3 3')],
|
|
64
|
+
tool: [P('M14.5 5.5a4 4 0 0 0-5 5L4.5 15.5a2.1 2.1 0 0 0 3 3l5-5a4 4 0 0 0 5-5l-2.4 2.4-2.4-.6-.6-2.4Z')],
|
|
65
|
+
thought: [P('M7 15.5a5 5 0 1 1 8.5-3.5 4 4 0 0 1 2 7.5H8a3 3 0 0 1-1-4Z'), C(6, 20.5, .8)],
|
|
66
|
+
};
|
|
67
|
+
function icon(name, title) {
|
|
68
|
+
const svg = document.createElementNS(NS, 'svg');
|
|
69
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
70
|
+
svg.setAttribute('class', 'icon icon-' + name);
|
|
71
|
+
svg.setAttribute('focusable', 'false');
|
|
72
|
+
if (title) { const t = document.createElementNS(NS, 'title'); t.textContent = title; svg.append(t); svg.setAttribute('role', 'img'); }
|
|
73
|
+
else svg.setAttribute('aria-hidden', 'true');
|
|
74
|
+
for (const [tag, attrs] of SET[name] || SET.square) {
|
|
75
|
+
const n = document.createElementNS(NS, tag);
|
|
76
|
+
for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, String(v));
|
|
77
|
+
svg.append(n);
|
|
78
|
+
}
|
|
79
|
+
return svg;
|
|
80
|
+
}
|
|
81
|
+
// a model's name plate: the alias engraved, the effort beside it ("luna:high" → LUNA | high); a bare id stays whole
|
|
82
|
+
function modelPlate(spec) {
|
|
83
|
+
const s = String(spec || '').trim(), i = s.lastIndexOf(':'), out = document.createElement('span');
|
|
84
|
+
out.className = 'model-plate'; out.title = s;
|
|
85
|
+
if (!s) { out.textContent = '—'; out.className = 'muted'; return out; }
|
|
86
|
+
const name = document.createElement('span'); name.className = 'mp-name'; name.textContent = i > 0 ? s.slice(0, i) : s; out.append(name);
|
|
87
|
+
if (i > 0 && i < s.length - 1) { // the colon stays in the text, for copying and screen readers
|
|
88
|
+
const c = document.createElement('span'); c.className = 'visually-hidden'; c.textContent = ':';
|
|
89
|
+
const e = document.createElement('span'); e.className = 'mp-effort'; e.textContent = s.slice(i + 1); out.append(c, e);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
return { icon, modelPlate, names: Object.keys(SET) };
|
|
94
|
+
})();
|