@giamme/forge 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +106 -1766
  3. package/SKILL.md +22 -3
  4. package/VERSION +1 -1
  5. package/docs/README.md +26 -0
  6. package/docs/add-ons.md +33 -0
  7. package/docs/artifacts.md +84 -0
  8. package/docs/commands.md +365 -0
  9. package/docs/dataset.md +29 -0
  10. package/docs/decomposed-runs.md +310 -0
  11. package/docs/development.md +95 -0
  12. package/docs/forge-ui.md +128 -0
  13. package/docs/fractal.md +181 -0
  14. package/docs/install.md +160 -0
  15. package/docs/memory.md +156 -0
  16. package/docs/models.md +118 -0
  17. package/docs/reviews.md +245 -0
  18. package/docs/safety.md +55 -0
  19. package/docs/troubleshooting.md +54 -0
  20. package/forge +20 -0
  21. package/package.json +3 -2
  22. package/references/dataset.md +18 -1
  23. package/references/decompose.md +20 -2
  24. package/references/fractal.md +1 -1
  25. package/references/harnesses.md +18 -6
  26. package/references/solo.md +1 -1
  27. package/references/ui.md +27 -5
  28. package/scripts/forge-config.py +347 -0
  29. package/scripts/forge-control.py +645 -0
  30. package/scripts/forge-dispatch.sh +7 -1
  31. package/scripts/forge-issues.py +806 -0
  32. package/scripts/forge-lib-task.sh +234 -35
  33. package/scripts/forge-parallel-options.sh +3 -1
  34. package/scripts/forge-parallel.sh +3 -2
  35. package/scripts/forge-route.py +184 -27
  36. package/scripts/forge-runs-rm.py +840 -0
  37. package/scripts/forge-runs.py +139 -0
  38. package/scripts/forge-runtime.py +59 -6
  39. package/scripts/forge-schedule.py +227 -11
  40. package/scripts/forge-ui.py +85 -19
  41. package/scripts/forge-version.py +54 -12
  42. package/scripts/forge_dataset/__init__.py +77 -6
  43. package/scripts/forge_dataset/capture.py +5 -2
  44. package/scripts/forge_dataset/harvest.py +23 -1
  45. package/scripts/forge_dataset/importer.py +420 -18
  46. package/scripts/forge_fractal/__init__.py +2 -1
  47. package/scripts/forge_registry.py +168 -16
  48. package/ui/README.md +116 -23
  49. package/ui/THIRD_PARTY_NOTICES.md +108 -0
  50. package/ui/docs/api.md +283 -23
  51. package/ui/forge_ui/__main__.py +10 -2
  52. package/ui/forge_ui/control.py +575 -0
  53. package/ui/forge_ui/export.py +33 -9
  54. package/ui/forge_ui/fractal.py +1 -2
  55. package/ui/forge_ui/reader.py +121 -8
  56. package/ui/forge_ui/server.py +135 -5
  57. package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
  58. package/ui/forge_ui/static/app.js +358 -131
  59. package/ui/forge_ui/static/cloud3d.js +3 -2
  60. package/ui/forge_ui/static/cloudui.js +8 -5
  61. package/ui/forge_ui/static/controls.js +619 -0
  62. package/ui/forge_ui/static/convo.js +295 -0
  63. package/ui/forge_ui/static/datatable.js +232 -0
  64. package/ui/forge_ui/static/diffview.js +29 -10
  65. package/ui/forge_ui/static/findings3d.js +134 -0
  66. package/ui/forge_ui/static/findingsview.js +412 -0
  67. package/ui/forge_ui/static/graph.js +3 -3
  68. package/ui/forge_ui/static/icons.js +94 -0
  69. package/ui/forge_ui/static/index.html +31 -13
  70. package/ui/forge_ui/static/iso.js +70 -19
  71. package/ui/forge_ui/static/leaderboard.js +44 -48
  72. package/ui/forge_ui/static/panel.js +92 -36
  73. package/ui/forge_ui/static/rundelete.js +388 -0
  74. package/ui/forge_ui/static/runmodel.js +17 -1
  75. package/ui/forge_ui/static/scene3d.js +518 -43
  76. package/ui/forge_ui/static/search.js +2 -1
  77. package/ui/forge_ui/static/settings.js +399 -0
  78. package/ui/forge_ui/static/shell.js +137 -0
  79. package/ui/forge_ui/static/stagectl.js +150 -0
  80. package/ui/forge_ui/static/style.css +1099 -469
  81. package/ui/forge_ui/static/theme.js +25 -0
  82. package/ui/forge_ui/static/uikit.js +161 -0
  83. package/ui/forge_ui/transcripts.py +608 -0
@@ -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
+ })();
@@ -13,39 +13,57 @@
13
13
  } }
14
14
  </script>
15
15
  <link rel="icon" href="favicon.svg" type="image/svg+xml">
16
+ <link rel="preload" href="D-DINCondensed-Bold.woff2" as="font" type="font/woff2" crossorigin>
16
17
  <link rel="stylesheet" href="style.css">
18
+ <script src="theme.js"></script>
19
+ <script src="icons.js" defer></script>
20
+ <script src="uikit.js" defer></script>
21
+ <script src="datatable.js" defer></script>
17
22
  <script src="runmodel.js" defer></script>
18
23
  <script src="cloudmodel.js" defer></script>
19
24
  <script src="modelstats.js" defer></script>
20
25
  <script src="graph.js" defer></script>
21
26
  <script src="gantt.js" defer></script>
27
+ <script src="controls.js" defer></script>
22
28
  <script src="panel.js" defer></script>
23
29
  <script src="diffview.js" defer></script>
30
+ <script src="convo.js" defer></script>
24
31
  <script src="files.js" defer></script>
25
32
  <script src="map3d.js" defer></script>
26
33
  <script src="cloudui.js" defer></script>
27
34
  <script src="leaderboard.js" defer></script>
35
+ <script src="findingsview.js" defer></script>
36
+ <script src="rundelete.js" defer></script>
37
+ <script src="settings.js" defer></script>
28
38
  <script src="search.js" defer></script>
29
39
  <script src="notify.js" defer></script>
30
40
  <script src="app.js" defer></script>
31
41
  </head>
32
- <body>
42
+ <body data-shell="immersive" data-runs="open" data-insp="open" data-tasks="open">
43
+ <script src="shell.js"></script>
33
44
  <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>
45
+ <a class="brand" href="#/" title="forge-ui · local run viewer"><span class="brand-plate">forge</span><span class="brand-sub">ui</span></a>
46
+ <nav class="desk-nav" aria-label="Views">
47
+ <a class="top-link" href="#/">Map</a>
48
+ <a class="top-link" href="#/cloud">Cloud</a>
49
+ <a class="top-link" href="#/models">Models</a>
50
+ <a class="top-link" href="#/settings">Settings</a>
51
+ </nav>
52
+ <button class="top-search" type="button" data-jump title="Jump to a run, task, project, model or command (/)"><span class="top-search-label"><span>Search</span></span> <kbd>Ctrl K</kbd></button>
53
+ <div class="desk-tools">
54
+ <button id="bell" class="bell" type="button" aria-label="Notifications"><span class="bell-count" hidden></span></button>
55
+ <label class="refresh-control" title="Poll the runs for changes"><input id="refresh" type="checkbox" checked> Live</label>
56
+ </div>
42
57
  </header>
43
58
  <div id="errors" role="status" aria-live="polite" hidden></div>
44
59
  <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">
60
+ <aside class="sidebar module" aria-label="Runs" data-overlay>
61
+ <div class="module-head"><h1 class="module-title">Runs</h1><span id="run-count" class="module-count"></span><span class="module-tools"></span></div>
62
+ <div class="runs-tools">
63
+ <label class="visually-hidden" for="filter">Filter by goal, repo, ID, path or model</label>
64
+ <input id="filter" type="search" placeholder="Filter runs…" autocomplete="off">
65
+ <select id="run-sort" aria-label="Sort the runs"><option value="updated">Latest</option><option value="state">State</option><option value="repo">Repo</option></select>
66
+ </div>
49
67
  <nav id="run-list" aria-label="Choose a run"><p class="empty">Loading runs…</p></nav>
50
68
  </aside>
51
69
  <main id="main" tabindex="-1"><p class="empty">Loading…</p></main>
@@ -41,14 +41,14 @@ const W = (x, y, z) => new THREE.Vector3(x * TS, z * TS, y * TS); // plan x ri
41
41
  const MONO3 = 'ui-monospace, "SF Mono", Menlo, Consolas, monospace';
42
42
  const NOISE = `float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }`;
43
43
  const CW = .61; // monospace advance, in em
44
- // same tokens as the iso figures' light and dark themes
44
+ // the iso figures' light and dark themes, tinted to forge-ui's enamel panel; style.css's --stage-* tokens win when set
45
45
  const THEMES = {
46
- dark: { bg: 0x121314, body: 0x1b1c1f, line: 0x4f5357, major: 0x393c3f, edge: 0x27292c, sky: 0xdde4ff, ground: 0x080808, hemi: .7, amb: 0, sun: .7, fill: .2, fadeMin: .45 },
47
- light: { bg: 0xe9eaea, body: 0xf1f1f0, line: 0x5d6266, major: 0x8a8f93, edge: 0xb9bcbf, sky: 0xffffff, ground: 0xe6e6e6, hemi: .9, amb: 1.6, sun: .35, fill: .15, fadeMin: .8 },
46
+ dark: { bg: 0x0e1210, body: 0x161c19, line: 0x4f5b54, major: 0x36413b, edge: 0x242c28, recess: 0x0b0e0c, sky: 0xe4efe6, ground: 0x070908, hemi: .7, amb: 0, sun: .7, fill: .2, fadeMin: .45 },
47
+ light: { bg: 0xc6cec4, body: 0xf3f5f0, line: 0x454f48, major: 0x6f7a72, edge: 0x9aa49b, recess: 0xdce1d9, sky: 0xffffff, ground: 0xdfe5dc, hemi: .9, amb: 1.6, sun: .35, fill: .15, fadeMin: .85 },
48
48
  };
49
49
  const PAL = {
50
- light: { line: '#8e9397', detail: '#c3c6c8', recess: '#dcdddd', ink: '#6a6f73', inkHi: '#16181a', glassOff: '#e4e5e5', glassOn: '#f6ece2', bg: '#f6f6f5' },
51
- dark: { line: '#4f5357', detail: '#2b2e30', recess: '#0f1011', ink: '#868b8f', inkHi: '#e6e8e9', glassOff: '#141516', glassOn: '#22170f', bg: '#121314' },
50
+ light: { line: '#6f7a72', detail: '#a9b2a8', recess: '#dce1d9', ink: '#2c342f', inkHi: '#111613', glassOff: '#e3e7df', glassOn: '#f6ead0', bg: '#f3f5f0' },
51
+ dark: { line: '#4f5b54', detail: '#28302c', recess: '#0b0e0c', ink: '#86928b', inkHi: '#e4e8e2', glassOff: '#121714', glassOn: '#221d0e', bg: '#0e1210' },
52
52
  };
53
53
 
54
54
  /* ---------- text: wrap and fit monospace print into a box (archivision's figure.js) ---------- */
@@ -287,7 +287,7 @@ export async function createEngine(host, { reduceMotion = false } = {}) {
287
287
  function palette() {
288
288
  const C = PAL[theme], light = theme === 'light', hi = TMPC.copy(accent).lerp(new THREE.Color(light ? 0x000000 : 0xffffff), light ? .3 : .42);
289
289
  PALC = { ...C, acc: '#' + accent.getHexString(), accHi: '#' + hi.getHexString(), light };
290
- INKHI.set(C.inkHi); DETAIL.set(light ? '#c3c6c8' : '#34373a'); GLASSON.set(C.glassOn);
290
+ INKHI.set(C.inkHi); DETAIL.set(C.detail); GLASSON.set(C.glassOn);
291
291
  }
292
292
  palette();
293
293
  const setOp = (m, o) => { m.opacity = o; const t = o < .999; if (m.transparent !== t) { m.transparent = t; m.needsUpdate = true; } };
@@ -391,18 +391,25 @@ export async function createEngine(host, { reduceMotion = false } = {}) {
391
391
  const probe = document.createElement('span');
392
392
  probe.className = 'stage-probe'; probe.setAttribute('aria-hidden', 'true'); host.append(probe);
393
393
  const COLORS = new Map();
394
+ // a custom property's colour, resolved by the browser (tokens are light-dark() pairs, so the raw value is not a colour)
394
395
  function cssColor(className, prop) {
395
396
  const key = className + '|' + prop;
396
397
  if (!COLORS.has(key)) {
397
398
  probe.className = 'stage-probe ' + className;
398
- const v = getComputedStyle(probe).getPropertyValue(prop).trim();
399
+ probe.style.setProperty('color', `var(${prop})`);
400
+ const cs = getComputedStyle(probe), v = cs.getPropertyValue(prop).trim() ? cs.color : '';
399
401
  let c = null; try { if (v) c = new THREE.Color(v); } catch (_) { c = null; }
400
402
  COLORS.set(key, c);
401
403
  }
402
404
  return COLORS.get(key);
403
405
  }
404
- const statusColor = s => cssColor('st-' + String(s || 'unknown').toLowerCase().replace(/[^a-z0-9_-]/g, ''), '--st') || new THREE.Color('#888888');
405
- const statusHex = s => '#' + statusColor(s).getHexString();
406
+ // a state's lamp as lit on the plate (--st-lit), and as ink for print on it (--st)
407
+ const stClass = s => 'st-' + String(s || 'unknown').toLowerCase().replace(/[^a-z0-9_-]/g, '');
408
+ const statusColor = s => cssColor(stClass(s), '--st-lit') || cssColor(stClass(s), '--st') || new THREE.Color('#888888');
409
+ const statusHex = s => '#' + (cssColor(stClass(s), '--st') || statusColor(s)).getHexString();
410
+ // a finding severity's colour (--sev on .sev-P0…P3): a ramp of its own, never a status lamp
411
+ const sevColor = p => cssColor('sev-' + String(p).replace(/[^A-Za-z0-9]/g, ''), '--sev') || statusColor('unknown');
412
+ const sevHex = p => '#' + sevColor(p).getHexString();
406
413
 
407
414
  /* ---------- theme: follows the page (prefers-color-scheme), colours from style.css ---------- */
408
415
  const themeHooks = new Set();
@@ -413,9 +420,10 @@ export async function createEngine(host, { reduceMotion = false } = {}) {
413
420
  const bg = cssColor('', '--stage-bg'), acc = cssColor('', '--stage-accent');
414
421
  BG_COL.set(bg || T.bg); scene.background.copy(BG_COL);
415
422
  if (acc) accent.copy(acc);
416
- bodyMat.color.set(T.body);
417
- LINE_COL.set(T.line); MAJOR_COL.set(T.major); EDGE_COL.set(T.edge); appliedKey = '';
418
- recessMat.color.set(light ? 0xdcdddd : 0x0f1011);
423
+ const tok = (name, d) => cssColor('', name) || new THREE.Color(d);
424
+ bodyMat.color.copy(tok('--stage-body', T.body));
425
+ LINE_COL.copy(tok('--stage-line', T.line)); MAJOR_COL.copy(tok('--stage-major', T.major)); EDGE_COL.copy(tok('--stage-edge', T.edge)); appliedKey = '';
426
+ recessMat.color.copy(tok('--stage-recess', T.recess));
419
427
  accentLineMat.color.copy(accent);
420
428
  hemi.color.set(T.sky); hemi.groundColor.set(T.ground); hemi.intensity = T.hemi; ambient.intensity = T.amb; sun.intensity = T.sun; fill.intensity = T.fill;
421
429
  for (const m of glowMats) { m.blending = light ? THREE.NormalBlending : THREE.AdditiveBlending; m.uniforms.light.value = light ? 1 : 0; m.needsUpdate = true; m.visible = !light; }
@@ -425,16 +433,20 @@ export async function createEngine(host, { reduceMotion = false } = {}) {
425
433
  for (const t of TXT) if (!t.gone) t.redraw();
426
434
  invalidate();
427
435
  }
436
+ // the page's theme: forge-ui's own switch (theme.js) when it is there, else the system setting
428
437
  const media = matchMedia('(prefers-color-scheme: light)');
429
- const onScheme = () => setTheme(media.matches ? 'light' : 'dark');
438
+ const resolved = () => window.ForgeTheme ? window.ForgeTheme.resolved() : media.matches ? 'light' : 'dark';
439
+ const onScheme = () => setTheme(resolved());
430
440
  media.addEventListener('change', onScheme);
441
+ window.addEventListener('forge-theme', onScheme);
431
442
 
432
443
  function applyWeight() {
433
444
  const dist = camera.position.distanceTo(controls.target);
434
445
  const ppm = camera === ortho ? VH * ortho.zoom / orthoH : VH / (dist * fovK());
435
446
  const k = THREE.MathUtils.clamp((ppm / 40 - .35) / .65, 0, 1);
436
447
  const fm = THEMES[theme].fadeMin, fade = fm + (1 - fm) * k, dpr = renderer.getPixelRatio();
437
- const key = fade.toFixed(2) + '|' + dpr + '|' + theme + '|' + (camera === ortho); if (key === appliedKey) return; appliedKey = key;
448
+ // orthographic zoom too: a screen sprite's world size follows it
449
+ const key = fade.toFixed(2) + '|' + dpr + '|' + theme + '|' + (camera === ortho ? ortho.zoom.toFixed(4) : 'p'); if (key === appliedKey) return; appliedKey = key;
438
450
  lineMat.linewidth = majorMat.linewidth = dpr; accentLineMat.linewidth = 1.6 * dpr;
439
451
  lineMat.color.copy(BG_COL).lerp(EDGE_COL, fade);
440
452
  majorMat.color.copy(BG_COL).lerp(MAJOR_COL, fade);
@@ -443,6 +455,8 @@ export async function createEngine(host, { reduceMotion = false } = {}) {
443
455
  for (const f of weightHooks) f();
444
456
  }
445
457
  const weightHooks = new Set();
458
+ // screen pixels per plan unit at the point the camera looks at
459
+ const pxPerUnit = () => (camera === ortho ? VH * ortho.zoom / orthoH : VH / (camera.position.distanceTo(controls.target) * fovK())) * TS;
446
460
 
447
461
  /* ---------- camera: frame a box, keeping the user's angle ---------- */
448
462
  const fovK = () => 2 * Math.tan(THREE.MathUtils.degToRad(persp.fov / 2));
@@ -484,14 +498,33 @@ export async function createEngine(host, { reduceMotion = false } = {}) {
484
498
  let D = .5;
485
499
  for (const p0 of pts) {
486
500
  const p = p0.clone().sub(c), px = p.dot(r), py = p.dot(u), pz = p.dot(f);
487
- D = Math.max(D, Math.abs(px) / (th * fw * .96) - pz, Math.abs(py) / (tv * fh * .92) - pz);
501
+ D = Math.max(D, Math.abs(px) / (th * fw * .97) - pz, Math.abs(py) / (tv * fh * .95) - pz);
488
502
  }
489
503
  // what is framed sits in the middle of the uncovered part of the stage
490
504
  c.addScaledVector(r, -(I.l - I.r) / VW * D * th).addScaledVector(u, (I.t - I.b) / VH * D * tv);
491
505
  return { c, D };
492
506
  }
507
+ // the stage covered by modules floating over it (marked data-overlay): a tall one covers a side, a wide one a top or bottom
508
+ function overlayInset() {
509
+ const H = host.getBoundingClientRect(), I = { l: 0, r: 0, t: 0, b: 0 };
510
+ if (!H.width || !H.height) return I;
511
+ const full = document.fullscreenElement;
512
+ for (const n of document.querySelectorAll('[data-overlay]')) {
513
+ if (full && !full.contains(n)) continue; // full screen: the modules are behind the stage
514
+ const R = n.getBoundingClientRect();
515
+ if (!R.width || !R.height || R.right <= H.left || R.left >= H.right || R.bottom <= H.top || R.top >= H.bottom) continue;
516
+ const cl = Math.max(R.left, H.left), cr = Math.min(R.right, H.right), ct = Math.max(R.top, H.top), cb = Math.min(R.bottom, H.bottom);
517
+ if (cb - ct > H.height * .5) { if (cl - H.left < H.right - cr) I.l = Math.max(I.l, cr - H.left); else I.r = Math.max(I.r, H.right - cl); }
518
+ else if (ct - H.top < H.bottom - cb) I.t = Math.max(I.t, cb - H.top); else I.b = Math.max(I.b, H.bottom - ct);
519
+ }
520
+ return I;
521
+ }
522
+ // the last framing, redone when the stage changes size, until the user moves the camera themselves
523
+ let autoFrame = null;
493
524
  function frameBox(bx, dur = 900, minEl = 26, dirOverride = null, inset = null) {
494
525
  resize(); // the host may have just been shown or moved: fit its real size
526
+ autoFrame = { bx, minEl, dirOverride, inset };
527
+ if (!inset) inset = overlayInset();
495
528
  let dir = dirOverride ? dirOverride.clone().normalize() : camera.position.clone().sub(controls.target);
496
529
  dir = dir.lengthSq() > 1e-8 ? dir.normalize() : DIR_ISO.clone();
497
530
  const el = Math.asin(THREE.MathUtils.clamp(dir.y, -1, 1)), lo = THREE.MathUtils.degToRad(minEl);
@@ -515,6 +548,22 @@ export async function createEngine(host, { reduceMotion = false } = {}) {
515
548
  if (k >= 1) { controls.enableDamping = true; CAMT = null; }
516
549
  return true;
517
550
  }
551
+ // closer (k > 1) or farther (k < 1) toward what the camera looks at, as a short glide; the angle stays
552
+ function zoomBy(k, dur = 260) {
553
+ settleCam();
554
+ const off = camera.position.clone().sub(controls.target);
555
+ let zoom = null;
556
+ if (camera === ortho) zoom = THREE.MathUtils.clamp(ortho.zoom * k, .05, 40);
557
+ else off.setLength(THREE.MathUtils.clamp(off.length() / k, controls.minDistance || .01, controls.maxDistance));
558
+ autoFrame = null;
559
+ if (reduceMotion || dur <= 1) {
560
+ camera.position.copy(controls.target).add(off);
561
+ if (zoom) { ortho.zoom = zoom; ortho.updateProjectionMatrix(); }
562
+ invalidate(); return;
563
+ }
564
+ CAMT = { t0: performance.now(), dur, fromT: controls.target.clone(), fromO: camera.position.clone().sub(controls.target), fromZ: zoom ? ortho.zoom : null, to: { t: controls.target.clone(), off, zoom } };
565
+ controls.enableDamping = false; invalidate();
566
+ }
518
567
  const cancelCam = () => { if (CAMT) { controls.enableDamping = true; CAMT = null; } };
519
568
  const settleCam = () => { if (CAMT) { camStep(CAMT.t0 + CAMT.dur + 1); controls.update(); } }; // jump to where a glide ends (tests)
520
569
 
@@ -544,6 +593,7 @@ export async function createEngine(host, { reduceMotion = false } = {}) {
544
593
  else if (!raf) last = 0;
545
594
  }
546
595
  controls.addEventListener('change', () => invalidate());
596
+ controls.addEventListener('start', () => { autoFrame = null; });
547
597
  const onVisibility = () => { visible = document.visibilityState === 'visible'; if (visible) invalidate(); };
548
598
  document.addEventListener('visibilitychange', onVisibility);
549
599
  const io = typeof IntersectionObserver === 'function' ? new IntersectionObserver(es => { onScreen = es.some(e => e.isIntersecting); if (onScreen) invalidate(); }) : null;
@@ -556,6 +606,7 @@ export async function createEngine(host, { reduceMotion = false } = {}) {
556
606
  const res = devRes();
557
607
  outlineMat.uniforms.res.value.copy(res); lineMat.resolution.copy(res); majorMat.resolution.copy(res); accentLineMat.resolution.copy(res);
558
608
  appliedKey = ''; invalidate();
609
+ if (autoFrame) { const f = autoFrame; frameBox(f.bx, 0, f.minEl, f.dirOverride, f.inset); }
559
610
  }
560
611
  const ro = typeof ResizeObserver === 'function' ? new ResizeObserver(resize) : null;
561
612
  if (ro) ro.observe(host);
@@ -740,7 +791,7 @@ export async function createEngine(host, { reduceMotion = false } = {}) {
740
791
  }
741
792
  function dispose() {
742
793
  disposed = true; if (raf) cancelAnimationFrame(raf); raf = 0;
743
- media.removeEventListener('change', onScheme); document.removeEventListener('visibilitychange', onVisibility);
794
+ media.removeEventListener('change', onScheme); window.removeEventListener('forge-theme', onScheme); document.removeEventListener('visibilitychange', onVisibility);
744
795
  if (io) io.disconnect(); if (ro) ro.disconnect();
745
796
  controls.dispose(); disposeGroup(scene); target.dispose(); composer.dispose && composer.dispose(); renderer.dispose();
746
797
  canvas.remove(); probe.remove();
@@ -756,9 +807,9 @@ export async function createEngine(host, { reduceMotion = false } = {}) {
756
807
  glowPoints, softMat, fat, loopL, noRay, rbox, inset, seg, polyLine, cumLen, alongPts, moveGlow, roundRect,
757
808
  mats, lmat, paint, lineCol, setOp, solid, loopAt, roofPlate, cone,
758
809
  tplane, release, roofAt, frontAt, lines, rr, font, fit, wrap, ellipsis, pxIn,
759
- ends, curve, arc, statusColor, statusHex, cssColor,
760
- frameBox, fitFor, cancelCam, settleCam, setOrtho, isOrtho: () => camera === ortho, invalidate, tickers, themeHooks, weightHooks,
761
- pick, pickHit, rayAt, project, handlers, disposeGroup, dispose, reduceMotion, now: () => NOW,
810
+ ends, curve, arc, statusColor, statusHex, sevColor, sevHex, cssColor,
811
+ frameBox, fitFor, zoomBy, cancelCam, settleCam, setOrtho, overlayInset, isOrtho: () => camera === ortho, invalidate, tickers, themeHooks, weightHooks,
812
+ pick, pickHit, rayAt, project, pxPerUnit, handlers, disposeGroup, dispose, reduceMotion, now: () => NOW,
762
813
  setProfile, get profile() { return profile; }, unitSolid, solidFrom, disposeShared, sprite, outlineInstanced, mixColor,
763
814
  LineSegments2, LineSegmentsGeometry, creaseLines,
764
815
  };
@@ -63,8 +63,9 @@ window.ForgeLeaderboard = (() => {
63
63
  function modelCell(alias, ctx, few) {
64
64
  const td = el('td', undefined, 'lb-model');
65
65
  const href = ctx.modelHref ? ctx.modelHref(alias) : null;
66
- if (href) { const a = el('a', alias, 'mono lb-alias'); a.href = href; a.title = 'show its runs in the cloud'; td.append(a); }
67
- else td.append(el('span', alias, 'mono'));
66
+ const plate = window.ForgeIcons ? window.ForgeIcons.modelPlate(alias) : el('span', alias, 'mono');
67
+ if (href) { const a = el('a', undefined, 'lb-alias'); a.href = href; a.title = 'show its runs in the cloud'; a.append(plate); td.append(a); }
68
+ else td.append(plate);
68
69
  if (few) { const s = el('span', 'few', 'lb-few'); s.title = few; td.append(s); }
69
70
  return td;
70
71
  }
@@ -96,11 +97,6 @@ window.ForgeLeaderboard = (() => {
96
97
  td.title = `input ${count(t.input)} (cached ${count(t.cached)}) · output ${count(t.output)} · recorded by ${count(used)} of ${plural(total, unit)}`;
97
98
  return td;
98
99
  }
99
- function head(table, cols) {
100
- const thead = el('thead'), tr = el('tr');
101
- for (const [label, cls, title] of cols) { const th = el('th', label, cls || ''); th.scope = 'col'; if (title) th.title = title; tr.append(th); }
102
- thead.append(tr); table.append(thead);
103
- }
104
100
 
105
101
  /* ---------- implementers ---------- */
106
102
  const IMPL_COLS = [
@@ -116,31 +112,31 @@ window.ForgeLeaderboard = (() => {
116
112
  ['Median model time', 'lb-num', 'Median time in the model phase per attempt'],
117
113
  ['Tokens in / out', 'lb-num', 'Summed over the attempts that recorded tokens'],
118
114
  ];
115
+ const colsOf = (spec, cells) => spec.map(([label, cls, title], i) => ({ ...cells[i], label, cls, title }));
116
+ // a sortable, reorderable table (datatable.js); one set of preferences per kind of table, shared by its copies
117
+ function table(id, label, columns, rows, ctx, rowClass) {
118
+ const host = el('div', undefined, 'lb-dt');
119
+ window.ForgeTable.render(host, { id, label, columns, rows, className: 'lb-table', rowClass,
120
+ prefs: ctx.tablePrefs ? ctx.tablePrefs(id) : null, savePrefs: p => ctx.saveTablePrefs && ctx.saveTablePrefs(id, p) });
121
+ return host;
122
+ }
119
123
  function implementerTable(rows, ctx, minN, caption) {
120
- const wrap = el('div', undefined, 'table-scroll'), table = el('table', undefined, 'lb-table');
121
- table.append(el('caption', caption, 'visually-hidden'));
122
- head(table, IMPL_COLS);
123
- const body = el('tbody');
124
- for (const r of rows) {
125
- const tr = el('tr', undefined, r.small ? 'lb-small' : '');
126
- const reasons = Object.entries(r.first.reasons).map(([k, v]) => `${v} ${k.replace(/_/g, ' ')}`).join(', ');
127
- const tk = r.timeoutKinds;
128
- tr.append(
129
- modelCell(r.alias, ctx, r.small ? `Fewer than ${plural(minN, 'first-attempt outcome')}: too few to rank` : ''),
130
- numCell(r.tasks),
131
- rateCell(r.first),
132
- numCell(r.first.censored, reasons || undefined),
133
- numCell(r.retries),
134
- numCell(r.escalationsAway),
135
- numCell(r.timeouts, r.timeouts ? `implementer ${tk.implementer} · reviewer ${tk.reviewer} · forge ${tk.forge}` : undefined),
136
- numCell(r.gateAccepts),
137
- numCell(r.humanAccepts),
138
- numCell(dur(r.medianModelS), `median wall time per attempt ${dur(r.medianDurationS)}`),
139
- tokenCell(r.tokens, r.attempts.total, 'attempt'));
140
- body.append(tr);
141
- }
142
- table.append(body); wrap.append(table);
143
- return wrap;
124
+ const tk = r => r.timeoutKinds;
125
+ const columns = colsOf(IMPL_COLS, [
126
+ { id: 'model', value: r => r.alias, cell: r => modelCell(r.alias, ctx, r.small ? `Fewer than ${plural(minN, 'first-attempt outcome')}: too few to rank` : '') },
127
+ { id: 'tasks', value: r => r.tasks, cell: r => numCell(r.tasks) },
128
+ { id: 'pass', value: r => r.first.n ? r.first.lo : null, cell: r => rateCell(r.first) },
129
+ { id: 'censored', value: r => r.first.censored, cell: r => numCell(r.first.censored, Object.entries(r.first.reasons).map(([k, v]) => `${v} ${k.replace(/_/g, ' ')}`).join(', ') || undefined) },
130
+ { id: 'retries', value: r => r.retries, cell: r => numCell(r.retries) },
131
+ { id: 'escalated', value: r => r.escalationsAway, cell: r => numCell(r.escalationsAway) },
132
+ { id: 'timeouts', value: r => r.timeouts, cell: r => numCell(r.timeouts, r.timeouts ? `implementer ${tk(r).implementer} · reviewer ${tk(r).reviewer} · forge ${tk(r).forge}` : undefined) },
133
+ { id: 'gate', value: r => r.gateAccepts, cell: r => numCell(r.gateAccepts) },
134
+ { id: 'human', value: r => r.humanAccepts, cell: r => numCell(r.humanAccepts) },
135
+ { id: 'model-time', value: r => num(r.medianModelS), cell: r => numCell(dur(r.medianModelS), `median wall time per attempt ${dur(r.medianDurationS)}`) },
136
+ { id: 'tokens', value: r => (r.tokens.attempts ?? r.tokens.reviews) ? (r.tokens.input || 0) + (r.tokens.output || 0) : null, cell: r => tokenCell(r.tokens, r.attempts.total, 'attempt') },
137
+ ]);
138
+ columns[2].title += '. Sorts by the lower end of the interval, as the ranking does';
139
+ return table('implementers', caption, columns, rows, ctx, r => r.small ? 'lb-small' : '');
144
140
  }
145
141
 
146
142
  /* ---------- reviewers ---------- */
@@ -176,7 +172,7 @@ window.ForgeLeaderboard = (() => {
176
172
  const avg = r.findings[s];
177
173
  if (avg === null) return numCell('—', 'No review reply could be read');
178
174
  const td = numCell(avg === 0 ? 0 : avg.toFixed(avg < 10 ? 2 : 1), `${count(r.findingTotals[s])} confirmed ${s} over ${plural(r.findings.n, 'review')}`);
179
- if (s === 'P0' && avg > 0) td.classList.add('lb-p0', 'st-fail');
175
+ if (s === 'P0' && avg > 0) td.classList.add('lb-p0');
180
176
  return td;
181
177
  }
182
178
  function agreementCell(a) {
@@ -187,25 +183,25 @@ window.ForgeLeaderboard = (() => {
187
183
  return td;
188
184
  }
189
185
  function reviewerTable(rows, ctx) {
190
- const wrap = el('div', undefined, 'table-scroll'), table = el('table', undefined, 'lb-table');
191
- table.append(el('caption', 'Reviewers', 'visually-hidden'));
192
- head(table, REV_COLS);
193
- const body = el('tbody');
194
- for (const r of rows) {
195
- const tr = el('tr');
196
- tr.append(modelCell(r.alias, ctx, ''), numCell(r.reviews.primary), numCell(r.reviews.shadow), numCell(r.reviews.final), verdictCell(r.verdicts),
197
- ...MS.SEVERITIES.map(s => findingCell(r, s)), agreementCell(r.agreement), numCell(dur(r.medianDurationS)), tokenCell(r.tokens, r.reviews.total, 'review'));
198
- body.append(tr);
199
- }
200
- table.append(body); wrap.append(table);
201
- return wrap;
186
+ const columns = colsOf(REV_COLS, [
187
+ { id: 'model', value: r => r.alias, cell: r => modelCell(r.alias, ctx, '') },
188
+ { id: 'primary', value: r => r.reviews.primary, cell: r => numCell(r.reviews.primary) },
189
+ { id: 'shadow', value: r => r.reviews.shadow, cell: r => numCell(r.reviews.shadow) },
190
+ { id: 'final', value: r => r.reviews.final, cell: r => numCell(r.reviews.final) },
191
+ { id: 'verdicts', value: r => { const v = r.verdicts, t = v.PASS + v.FAIL + v.other; return t ? v.PASS / t : null; }, cell: r => verdictCell(r.verdicts) },
192
+ ...MS.SEVERITIES.map(sv => ({ id: 'sev-' + sv, value: r => r.findings[sv], cell: r => findingCell(r, sv) })),
193
+ { id: 'agreement', value: r => r.agreement.n ? r.agreement.p : null, cell: r => agreementCell(r.agreement) },
194
+ { id: 'time', value: r => num(r.medianDurationS), cell: r => numCell(dur(r.medianDurationS)) },
195
+ { id: 'tokens', value: r => (r.tokens.attempts ?? r.tokens.reviews) ? (r.tokens.input || 0) + (r.tokens.output || 0) : null, cell: r => tokenCell(r.tokens, r.reviews.total, 'review') },
196
+ ]);
197
+ return table('reviewers', 'Reviewers', columns, rows, ctx);
202
198
  }
203
199
 
204
200
  /* ---------- header, caveat and filters ---------- */
205
201
  function header(L, O, f, update, withNote) {
206
202
  const t = L.totals, frag = document.createDocumentFragment();
207
203
  const top = el('header', undefined, 'lb-head');
208
- top.append(el('h1', 'Models'), el('p', `${plural(t.runs, 'run')} · ${plural(t.tasks, 'task')} · ${plural(t.attempts, 'attempt')} · ${plural(t.reviews, 'review')} counted`, 'muted lb-summary'));
204
+ top.append(el('h1', 'Models', 'module-title'), el('p', `${plural(t.runs, 'run')} · ${plural(t.tasks, 'task')} · ${plural(t.attempts, 'attempt')} · ${plural(t.reviews, 'review')} counted`, 'muted lb-summary'));
209
205
  const note = el('div', undefined, 'lb-caveat'); note.setAttribute('role', 'note');
210
206
  const p = el('p');
211
207
  p.append(el('strong', 'Observational, not causal. '), document.createTextNode('Forge routes each task to a model by its difficulty and by how models did before, so a model sent the hard tasks looks worse here than it is. Exploratory picks, the tasks where forge drew the model at random, are the fairest comparison.'));
@@ -276,12 +272,12 @@ window.ForgeLeaderboard = (() => {
276
272
  root.append(emptyNote('No tasks or reviews match these filters.', [], reset));
277
273
  } else {
278
274
  const impl = el('section', undefined, 'lb-section');
279
- impl.append(el('h2', 'Implementers'), el('p', 'Each task counts once, for the model that ran its first attempt; retries, escalations and accepts show what happened after. Ranked by the lower end of the 95% interval, so a high rate on few tasks does not jump the table.', 'lb-lead'));
275
+ impl.append(el('h2', 'Implementers', 'plate'), el('p', 'Each task counts once, for the model that ran its first attempt; retries, escalations and accepts show what happened after. Ranked by the lower end of the 95% interval, so a high rate on few tasks does not jump the table.', 'lb-lead'));
280
276
  impl.append(L.implementers.length ? implementerTable(L.implementers, ctx, f.minN, 'Implementers') : el('p', 'No attempts with a recorded model match these filters.', 'muted'));
281
277
  root.append(impl);
282
278
  if (!f.difficulty && L.implementers.length) {
283
279
  const by = el('section', undefined, 'lb-section');
284
- by.append(el('h2', 'By difficulty'), el('p', 'The same table for the tasks of one declared difficulty: the comparison routing distorts least.', 'lb-lead'));
280
+ by.append(el('h2', 'By difficulty', 'plate'), el('p', 'The same table for the tasks of one declared difficulty: the comparison routing distorts least.', 'lb-lead'));
285
281
  for (const d of MS.DIFFS) {
286
282
  const rows = L.byDifficulty[d];
287
283
  if (!rows.length) continue;
@@ -299,7 +295,7 @@ window.ForgeLeaderboard = (() => {
299
295
  root.append(by);
300
296
  }
301
297
  const rev = el('section', undefined, 'lb-section');
302
- rev.append(el('h2', 'Reviewers'), el('p', 'Primary reviews of each attempt, shadow reviews run beside them and final reviews of a whole run. Findings are confirmed findings per review.', 'lb-lead'));
298
+ rev.append(el('h2', 'Reviewers', 'plate'), el('p', 'Primary reviews of each attempt, shadow reviews run beside them and final reviews of a whole run. Findings are confirmed findings per review.', 'lb-lead'));
303
299
  rev.append(L.reviewers.length ? reviewerTable(L.reviewers, ctx) : el('p', 'No reviews with a recorded model match these filters.', 'muted'));
304
300
  root.append(rev);
305
301
  }