@bongos/core 1.19.790 → 1.19.792

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 (41) hide show
  1. package/.bongos-core.json +76 -41
  2. package/.claude/skills/builder-reauth/SKILL.md +5 -3
  3. package/README.md +1 -1
  4. package/docs/adr/0206-own-scoped-session-revocation-no-absolute-ceiling.md +2 -0
  5. package/docs/adr/0294-cli-sessions-do-not-expire.md +56 -0
  6. package/docs/adr/README.md +1 -0
  7. package/docs/copy-inventory.md +90 -90
  8. package/docs/copy-registry.json +109 -109
  9. package/docs/module-api-changelog.md +4 -0
  10. package/migrations/core_243_cli_sessions_never_expire.sql +58 -0
  11. package/modules/hall-ui/public/hall-kit.js +7 -65
  12. package/modules/hall-ui/public/hall-render.js +1 -1
  13. package/modules/hall-ui/public/hall-visual.js +85 -0
  14. package/modules/hall-ui/public/index.html +1 -0
  15. package/modules/hall-ui/public/profile.html +1 -0
  16. package/modules/hall-ui/public/profile.js +2 -2
  17. package/modules/hall-ui/public/settings.js +5 -2
  18. package/modules/hall-ui/public/studio.css +1 -1
  19. package/modules/hall-ui/public/studio.html +1 -0
  20. package/modules/hall-ui/public/studio.js +4 -3
  21. package/modules/hall-ui/public/style.css +0 -16
  22. package/modules/hall-ui/public/task.html +1 -0
  23. package/modules/hall-ui/public/task.js +10 -13
  24. package/package-lock.json +2 -2
  25. package/package.json +1 -1
  26. package/scripts/gds/accessibility-replay.js +195 -0
  27. package/scripts/gds/box-auth-check.js +1 -1
  28. package/scripts/gds/cli-lib.js +2 -2
  29. package/scripts/gds/reauth.js +9 -7
  30. package/scripts/gds/run-unit-tests.js +7 -0
  31. package/src/bongos/auth-github.js +2 -0
  32. package/src/bongos/db-kernel.js +29 -11
  33. package/src/bongos/db.js +33 -24
  34. package/src/bongos/routes/auth.js +3 -2
  35. package/src/module-api.js +1 -1
  36. package/tests/accessibility_replay.mjs +201 -0
  37. package/tests/auth_session_expiry_db.mjs +183 -0
  38. package/tests/auth_session_ttl.mjs +124 -11
  39. package/tests/fixtures/accessibility-replay.json +93 -0
  40. package/tests/hall_studio_world.mjs +19 -11
  41. package/tests/task_visuals.mjs +91 -27
@@ -2054,5 +2054,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2054
2054
  landed since 1.19.788 with no explicit bump. run 35159544354. (task 1002620)
2055
2055
  1.19.790 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2056
2056
  landed since 1.19.789 with no explicit bump. run 35163350575. (task 1002620)
2057
+ 1.19.791 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2058
+ landed since 1.19.790 with no explicit bump. run 35165066743. (task 1002620)
2059
+ 1.19.792 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2060
+ landed since 1.19.791 with no explicit bump. run 35166486577. (task 1002620)
2057
2061
  ---------------------------------------------------------------------------
2058
2062
  ```
@@ -0,0 +1,58 @@
1
+ -- core_243 — CLI sessions lose their idle expiry (task 1003956, ADR 0294).
2
+ --
3
+ -- WHAT CHANGED IN CODE. db-kernel.js createSession now records NO expiry when a
4
+ -- caller names none (every such caller is source='cli'), and lookupSession's
5
+ -- refresh-on-use rolls expires_at forward only for a row that already has one.
6
+ -- Both halves are forward-looking: they govern sessions minted or used AFTER the
7
+ -- deploy. Neither touches the dated rows already sitting in the table.
8
+ --
9
+ -- WHY THAT NEEDS A MIGRATION. Without this, the code change would be nearly
10
+ -- invisible on the day it ships. Every live CLI session carries an expires_at
11
+ -- from migration 035's backfill and from every refresh since, so each one would
12
+ -- still lapse on its own 24h clock exactly as before — and each builder would eat
13
+ -- one final forced re-auth to get a row the new default applies to. That is the
14
+ -- precise annoyance the task set out to remove, delivered once more on the way
15
+ -- out. Clearing the column here is what makes the fix arrive for people who are
16
+ -- already signed in.
17
+ --
18
+ -- DIRECTION OF THE GRACE. Migration 035 faced the mirror of this and reasoned the
19
+ -- same way: it backfilled a 24h runway specifically so introducing a TTL would
20
+ -- not kill the session running the migration. This is that courtesy inverted —
21
+ -- 035 pushed expiries out so nobody was signed out by a new timer; this removes
22
+ -- them so nobody is signed out by a timer already gone from the code. In both
23
+ -- cases the migration exists to keep a policy change off the backs of the people
24
+ -- holding live sessions when it lands.
25
+ --
26
+ -- WHY source='cli' AND NOT A BLANKET UPDATE. builder_sessions holds both classes
27
+ -- and only the CLI's expiry was asked for. A web row's expires_at is an explicit
28
+ -- 30-day value its own flow chose (auth-idp.js / auth-github.js pass a real date,
29
+ -- so the new NULL default never reaches them); nulling it here would silently
30
+ -- hand browser cookies an immortality nobody decided on, and the code would not
31
+ -- re-establish it — lookupSession only rolls an expiry it can already see, so a
32
+ -- web row cleared by mistake stays cleared forever. The filter is the whole
33
+ -- safety property of this statement.
34
+ --
35
+ -- WHY revoked_at IS NULL. A revoked row is dead by revoked_at, which lookupSession
36
+ -- checks before it ever reads expires_at, so clearing its expiry could not revive
37
+ -- it. It is excluded anyway: revoked rows are the audit trail of sessions somebody
38
+ -- deliberately killed, and rewriting their columns muddies what that record says.
39
+ -- Touch only what the change is actually about.
40
+ --
41
+ -- WHAT THIS DOES NOT WEAKEN. Revocation is untouched and remains the real remedy
42
+ -- (ADR 0206): POST /me/sessions/revoke and "Sign out everywhere" kill a leaked
43
+ -- bearer immediately, setBuilderRank sweeps every session on demotion, and
44
+ -- requireBuilder refuses a deactivated builder on the hot path regardless of any
45
+ -- live token. None of those consult expires_at.
46
+ --
47
+ -- Idempotent: re-running matches no rows the second time (expires_at is already
48
+ -- NULL) and is a no-op.
49
+
50
+ BEGIN;
51
+
52
+ UPDATE builder_sessions
53
+ SET expires_at = NULL
54
+ WHERE source = 'cli'
55
+ AND revoked_at IS NULL
56
+ AND expires_at IS NOT NULL;
57
+
58
+ COMMIT;
@@ -579,70 +579,14 @@
579
579
  };
580
580
  }
581
581
 
582
- // ---- ship-time task visual (task 1003109, goal 1000072) -------------------
582
+ // ---- ship-time task visual — MOVED OUT (task 1003835) ---------------------
583
583
  //
584
- // A shipped task may carry an optional IMAGE alongside its value summary — the
585
- // same claim in another medium, so a non-engineer can SEE what changed rather
586
- // than read about it. In a FEED the summary is one line in a list, so the image
587
- // appears as a small thumbnail linking to the full-size file; the task page
588
- // renders it properly (task.js taskVisualHtml).
589
- //
590
- // Lives here because three separate feeds render it (Standing's ship ledger,
591
- // a profile's recent ships, the status dashboard) and a fourth copy of an
592
- // HTML-building helper is how the pre-kit drift started.
593
-
594
- // Same-origin, SERVER-MINTED paths only. The URL arrives from our own API, but
595
- // it lands in an href and a src, so it is matched literally against the shape
596
- // modules/lifecycle/task-visuals.js mints rather than trusted. Anything else
597
- // renders nothing at all.
598
- const VISUAL_URL_RE = /^\/api\/(gds|bongos)\/task-visuals\/task-\d+-[0-9a-f]{16}\.(png|jpg|gif|webp)$/;
599
- function isTaskVisualUrl(url) {
600
- return VISUAL_URL_RE.test(String(url ?? ''));
601
- }
602
-
603
- // The compact thumbnail. Returns '' when the row carries no visual — a task
604
- // without one must never look incomplete, so nothing is drawn (no placeholder).
605
- function shipVisualThumbHtml(row) {
606
- const url = String((row && row.visual_url) ?? '');
607
- if (!isTaskVisualUrl(url)) return '';
608
- const alt = String((row && row.visual_alt) || `Visual for task ${(row && row.id) ?? ''}`).trim();
609
- return `<a href="${esc(url)}" target="_blank" rel="noopener" title="${esc(alt)}">` +
610
- `<img class="ship-visual-thumb" src="${esc(url)}" alt="${esc(alt)}" loading="lazy"></a>`;
611
- }
612
-
613
- // The FULL-SIZE form: the picture as the thing being looked at, with
614
- // visual_alt as its caption. The thumbnail above is for a FEED, where the
615
- // summary is one line in a list; this is for a surface whose subject IS the
616
- // image — the task record, and the Studio's review card (task 1003829).
617
- //
618
- // It lives here so the Studio did not become the THIRD hand-rolled <figure>
619
- // for this one image. Be precise about what that did and did not fix: the two
620
- // that already existed — task.js's taskVisualHtml (.tk-visual) and
621
- // task-detail.js's overlay (.td-visual) — are NOT converged onto this yet and
622
- // are untouched by the task that added it (1003829). Neither page loads the
623
- // kit, so migrating them means adding it to task.html, which is a call of its
624
- // own. So: one canonical renderer exists and new callers take it; the two
625
- // legacy copies are a known, named cleanup rather than a claim already met.
626
- //
627
- // `className` is the caller's BEM base, because the size is the surface's
628
- // decision and not the kit's: the task record caps at 52vh inside a record
629
- // column (.tk-visual), the Studio gives the plate the card's whole width.
630
- // The markup, the URL guard and the escaping are the kit's and identical
631
- // everywhere. Returns '' with no visual, like the thumbnail.
632
- function taskVisualFigureHtml(row, { className = 'task-visual' } = {}) {
633
- const url = String((row && row.visual_url) ?? '');
634
- if (!isTaskVisualUrl(url)) return '';
635
- const written = String((row && row.visual_alt) || '').trim();
636
- const alt = written || `Visual for task ${(row && row.id) ?? ''}`;
637
- const base = String(className).replace(/[^A-Za-z0-9_-]/g, '');
638
- // The caption is only drawn when the shipper WROTE one — the fallback alt is
639
- // a machine sentence for a screen reader, not a caption to show a reader.
640
- const caption = written ? `<figcaption class="${base}__caption">${esc(written)}</figcaption>` : '';
641
- return `<figure class="${base}">` +
642
- `<a href="${esc(url)}" target="_blank" rel="noopener">` +
643
- `<img class="${base}__img" src="${esc(url)}" alt="${esc(alt)}" loading="lazy">` +
644
- `</a>${caption}</figure>`;
645
- }
584
+ // isTaskVisualUrl / shipVisualThumbHtml / taskVisualFigureHtml now live in
585
+ // hall-visual.js. They left because the task record page renders a visual and
586
+ // needs nothing else in this file, so converging it here would have loaded 61KB
587
+ // on the hall's most-visited record page for one function. A page that draws a
588
+ // visual loads hall-visual.js; pages that only use the kit no longer carry the
589
+ // renderers at all. Callers read window.OTBVisual, not OTBKit.
646
590
 
647
591
  // ---- charts (task 1003445) ------------------------------------------------
648
592
  //
@@ -1218,8 +1162,6 @@
1218
1162
  plotBox, plotWindow,
1219
1163
  // states
1220
1164
  emptyStateHtml, skeletonRows, collapsibleCard,
1221
- // ship-time task visual (task 1003109; the full-size figure, task 1003829)
1222
- isTaskVisualUrl, shipVisualThumbHtml, taskVisualFigureHtml,
1223
1165
  // age (task 1002955)
1224
1166
  fmtAge, fmtAbsDate, ageChipHtml, AGE_STALE_DAYS,
1225
1167
  };
@@ -1100,7 +1100,7 @@
1100
1100
  <td>
1101
1101
  <div>${escapeHtml(s.title)}</div>
1102
1102
  ${s.value_summary ? `<div style="color:var(--ink-soft);font-size:0.88rem;font-style:italic;margin-top:2px">${escapeHtml(s.value_summary)}</div>` : ''}
1103
- ${s.visual_url ? `<div style="margin-top:6px">${window.OTBKit.shipVisualThumbHtml(s)}</div>` : ''}
1103
+ ${s.visual_url && window.OTBVisual ? `<div style="margin-top:6px">${window.OTBVisual.shipVisualThumbHtml(s)}</div>` : ''}
1104
1104
  </td>
1105
1105
  <td class="ledger__drachmae">${fmtNum(window.OTBBranding?.taskCreditValue?.(s) ?? s.credits_reward)}</td>
1106
1106
  <td class="ledger__date">${fmtDate(s.shipped_at)}</td>
@@ -0,0 +1,85 @@
1
+ // modules/hall-ui/public/hall-visual.js — the ship-time task visual, in one place
2
+ // (task 1003835; the renderers themselves are tasks 1003109 and 1003829).
3
+ //
4
+ // A satellite of hall-kit.js rather than a part of it, and that is the decision
5
+ // this file records. These three functions lived in the kit, which is 61KB of
6
+ // pager/ledger/filter/chart machinery. The task record page (task.html) renders a
7
+ // visual and needs NOTHING else the kit holds, so converging it onto the kit's
8
+ // renderer would have meant loading 61KB on the hall's most-visited record page
9
+ // for one function — a real cost, paid on the wrong page. The alternative was to
10
+ // let task.js keep its own copy, which is how the hall ended up with two.
11
+ //
12
+ // So the helpers moved OUT. Any page that renders a visual loads this file; the
13
+ // kit no longer carries them and the pages that only use the kit no longer pay
14
+ // for them. The owner's call (2026-09-16) was that the kit may grow satellites
15
+ // like this one.
16
+ //
17
+ // Contract, identical to the kit's own: dependency-free (its own esc(), no
18
+ // window.OTB use) so load order only needs "before the page script";
19
+ // node-require-safe, so tests/task_visuals.mjs can require it with no DOM;
20
+ // string-template builders returning HTML, no state, no render loop.
21
+ //
22
+ // CSS: each caller supplies its own BEM base, because SIZE is the surface's
23
+ // decision and not this file's — the task record caps at 52vh in a record column
24
+ // (.tk-visual, style.css/task.css), the Studio gives the plate the card's whole
25
+ // width (.studio-plate). The markup, the URL guard and the escaping are shared.
26
+ (() => {
27
+ 'use strict';
28
+
29
+ // HTML-escape for text interpolated into innerHTML.
30
+ function esc(s) {
31
+ return String(s ?? '').replace(/[&<>"']/g, (c) => ({
32
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
33
+ }[c]));
34
+ }
35
+
36
+ // Same-origin, SERVER-MINTED paths only. The URL arrives from our own API, but
37
+ // it lands in an href and a src, so it is matched literally against the shape
38
+ // modules/lifecycle/task-visuals.js mints rather than trusted. Anything else
39
+ // renders nothing at all.
40
+ const VISUAL_URL_RE = /^\/api\/(gds|bongos)\/task-visuals\/task-\d+-[0-9a-f]{16}\.(png|jpg|gif|webp)$/;
41
+ function isTaskVisualUrl(url) {
42
+ return VISUAL_URL_RE.test(String(url ?? ''));
43
+ }
44
+
45
+ // The compact thumbnail, for a FEED — where the summary is one line in a list,
46
+ // so the image appears small and links to the full-size file. Three feeds draw
47
+ // it (the hall's ship ledger, a profile's recent ships, the status dashboard).
48
+ // Returns '' when the row carries no visual: a task without one must never look
49
+ // incomplete, so nothing is drawn and there is no placeholder.
50
+ function shipVisualThumbHtml(row) {
51
+ const url = String((row && row.visual_url) ?? '');
52
+ if (!isTaskVisualUrl(url)) return '';
53
+ const alt = String((row && row.visual_alt) || `Visual for task ${(row && row.id) ?? ''}`).trim();
54
+ return `<a href="${esc(url)}" target="_blank" rel="noopener" title="${esc(alt)}">` +
55
+ `<img class="ship-visual-thumb" src="${esc(url)}" alt="${esc(alt)}" loading="lazy"></a>`;
56
+ }
57
+
58
+ // The FULL-SIZE form: the picture as the thing being looked at, with visual_alt
59
+ // as its caption. For a surface whose subject IS the image — the task record
60
+ // (.tk-visual) and the Studio's review card (.studio-plate).
61
+ //
62
+ // `className` is the caller's BEM base; it is sanitised because it reaches a
63
+ // class attribute. Returns '' with no visual, like the thumbnail.
64
+ function taskVisualFigureHtml(row, { className = 'task-visual' } = {}) {
65
+ const url = String((row && row.visual_url) ?? '');
66
+ if (!isTaskVisualUrl(url)) return '';
67
+ const written = String((row && row.visual_alt) || '').trim();
68
+ const alt = written || `Visual for task ${(row && row.id) ?? ''}`;
69
+ const base = String(className).replace(/[^A-Za-z0-9_-]/g, '');
70
+ // The caption is only drawn when the shipper WROTE one — the fallback alt is
71
+ // a machine sentence for a screen reader, not a caption to show a reader.
72
+ const caption = written ? `<figcaption class="${base}__caption">${esc(written)}</figcaption>` : '';
73
+ return `<figure class="${base}">` +
74
+ `<a href="${esc(url)}" target="_blank" rel="noopener">` +
75
+ `<img class="${base}__img" src="${esc(url)}" alt="${esc(alt)}" loading="lazy">` +
76
+ `</a>${caption}</figure>`;
77
+ }
78
+
79
+ const api = { isTaskVisualUrl, shipVisualThumbHtml, taskVisualFigureHtml };
80
+
81
+ // Node-require-safe (the dom-utils.js / hall-kit.js pattern): assignment is the
82
+ // only eval-time side effect, and only when a window exists.
83
+ if (typeof window !== 'undefined') window.OTBVisual = api;
84
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
85
+ })();
@@ -110,6 +110,7 @@
110
110
  <script src="/builders/box-panel.js?v=2026-06-14-domutils" defer></script>
111
111
  <!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
112
112
  Loaded BEFORE hall-widgets.js, whose skeletons call OTBKit.pagerBarHtml(). -->
113
+ <script src="/builders/hall-visual.js?v=2026-09-16-visual1" defer></script>
113
114
  <script src="/builders/hall-kit.js" defer></script>
114
115
  <!-- BV1.R75: the CORE hall widgets register into window.OTB's hall-widget registry.
115
116
  Loaded BEFORE builders.js so the registry is populated when builders.js stamps
@@ -196,6 +196,7 @@
196
196
  <!-- hall-kit (window.OTBKit) — profile.js renders ship-time task visuals through
197
197
  its shared thumbnail helper (task 1003109) and builds the section rail with
198
198
  it (task 1003489), so it must load first. -->
199
+ <script src="/builders/hall-visual.js?v=2026-09-16-visual1"></script>
199
200
  <script src="/builders/hall-kit.js?v=2026-08-27-activity"></script>
200
201
  <!-- Connections (section 6). Loaded BEFORE profile.js so window.OTBConnections
201
202
  exists by the time renderProfile runs; profile.js guards on it anyway, so a
@@ -176,7 +176,7 @@
176
176
  const summary = s.value_summary
177
177
  ? `<span style="display:block;margin-top:2px;font-size:12px;color:var(--ink-faint)">${escapeHtml(s.value_summary)}</span>`
178
178
  : '';
179
- const visual = window.OTBKit && window.OTBKit.shipVisualThumbHtml ? window.OTBKit.shipVisualThumbHtml(s) : '';
179
+ const visual = window.OTBVisual ? window.OTBVisual.shipVisualThumbHtml(s) : '';
180
180
  return `<tr>
181
181
  <td class="ledger__num">#${escapeHtml(String(s.id))}</td>
182
182
  <td><a href="${taskHref(s.id)}">${escapeHtml(s.title || `Task ${s.id}`)}</a>${summary}${visual ? `<span style="display:block;margin-top:6px">${visual}</span>` : ''}</td>
@@ -473,7 +473,7 @@
473
473
  const summary = t.value_summary
474
474
  ? `<span style="display:block;margin-top:2px;font-size:12px;color:var(--ink-faint)">${escapeHtml(t.value_summary)}</span>`
475
475
  : '';
476
- const visual = kit.shipVisualThumbHtml ? kit.shipVisualThumbHtml(t) : '';
476
+ const visual = window.OTBVisual ? window.OTBVisual.shipVisualThumbHtml(t) : '';
477
477
  return `<tr>
478
478
  <td class="ledger__num">#${escapeHtml(String(t.id))}</td>
479
479
  <td><a href="${taskHref(t.id)}">${escapeHtml(t.title || `Task ${t.id}`)}</a>${summary}${visual ? `<span style="display:block;margin-top:6px">${visual}</span>` : ''}</td>
@@ -913,7 +913,10 @@
913
913
  if (res.status === 401) { toAuth(); return; }
914
914
  const body = (res.data || {});
915
915
  if (!res.ok) throw new Error(body.message || `HTTP ${res.status}`);
916
- const expires = body.expires_at ? new Date(body.expires_at).toLocaleString() : 'unknown';
916
+ // No expires_at is the NORMAL case for a CLI bearer since ADR 0294 — say so
917
+ // plainly. 'unknown' read as a gap in the page's knowledge; the absence of a
918
+ // date is the fact, and the revoke is what replaced the timer.
919
+ const expires = body.expires_at ? `Expires ${new Date(body.expires_at).toLocaleString()}` : 'Never expires';
917
920
  const pasteCommand = body.paste_command || '';
918
921
  // The secret is QUOTED (settings.css `.quote`): a 64-character token cannot
919
922
  // be a line of prose, so it sits one tonal step down with its own foot. Same
@@ -936,7 +939,7 @@
936
939
  copyBtn.id = 'cli-reissue-copy';
937
940
  copyBtn.textContent = 'Copy';
938
941
  const hint = el('span', 'settings-card__hint');
939
- hint.textContent = `Expires ${expires} · refreshes on use.`;
942
+ hint.textContent = `${expires} · revoke it any time under Active sessions.`;
940
943
  const foot = el('div', 'quote__foot');
941
944
  foot.append(copyBtn, hint);
942
945
  const quote = el('div', 'quote');
@@ -155,7 +155,7 @@
155
155
  IS the card: an artist is being asked to LOOK at something, so it is given the
156
156
  card's full width and as much height as a screen can spare, not the 44px
157
157
  thumbnail a ship feed uses. Markup comes from the kit's one renderer
158
- (OTBKit.taskVisualFigureHtml); only the size is this room's decision.
158
+ (OTBVisual.taskVisualFigureHtml); only the size is this room's decision.
159
159
 
160
160
  max-height in vh, and MEASURED at 1440x900 rather than chosen. The size an
161
161
  artist can judge it at wins over a zero-scroll card — that is the task's own
@@ -72,6 +72,7 @@
72
72
  <!-- The kit, for its ONE renderer: the full-size ship-time visual figure
73
73
  (task 1003829). The copy desk — the artist's other room — already loads it,
74
74
  and a fourth hand-rolled <figure> is what the kit exists to stop. -->
75
+ <script src="/builders/hall-visual.js?v=2026-09-16-visual1"></script>
75
76
  <script src="/builders/hall-kit.js"></script>
76
77
  <script src="/builders/studio.js?v=2026-09-10-studio2"></script>
77
78
  </body>
@@ -132,7 +132,7 @@
132
132
  // ---------------------------------------------------------------------------
133
133
 
134
134
  // THE PLATE — the reviewed work's ship-time visual, at the size the card can
135
- // give it. Through the kit's one renderer (OTBKit.taskVisualFigureHtml, which
135
+ // give it. Through the one renderer (OTBVisual.taskVisualFigureHtml, which
136
136
  // carries the server-minted-URL guard and the escaping) with the Studio's own
137
137
  // class, because the size is this surface's decision: here the picture is the
138
138
  // subject, not an illustration beside a record.
@@ -141,8 +141,9 @@
141
141
  // read, or when the kit is absent — a review with no picture must read as a
142
142
  // review, never as an empty frame (the degrade rule).
143
143
  function plateHtml(p) {
144
- if (!p.shipped || !kit || typeof kit.taskVisualFigureHtml !== 'function') return '';
145
- return kit.taskVisualFigureHtml(p.shipped, { className: 'studio-plate' });
144
+ const vis = window.OTBVisual;
145
+ if (!p.shipped || !vis || typeof vis.taskVisualFigureHtml !== 'function') return '';
146
+ return vis.taskVisualFigureHtml(p.shipped, { className: 'studio-plate' });
146
147
  }
147
148
 
148
149
  // The three weights, and the one field they share. Offered only to a viewer
@@ -2841,22 +2841,6 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
2841
2841
  overflow-y: auto;
2842
2842
  }
2843
2843
  .td-touches { font-size: 12.5px; color: var(--ink-faint); font-family: var(--font-mono); word-break: break-word; margin: 0 0 12px; }
2844
- /* task 1003109: the ship-time VISUAL, rendered directly under the value summary
2845
- it restates. Sized to be READ (this exists so a non-engineer can see a change),
2846
- capped so a tall screenshot can't push the rest of the record off the page, and
2847
- never drawn at all when a task has no visual. */
2848
- .td-visual { margin: 0 0 12px; }
2849
- .td-visual__img {
2850
- display: block;
2851
- max-width: 100%;
2852
- max-height: 52vh;
2853
- width: auto;
2854
- height: auto;
2855
- border: var(--line) solid var(--rule-soft);
2856
- border-radius: var(--radius-sm);
2857
- background: var(--glass);
2858
- }
2859
- .td-visual__caption { font-size: 12.5px; color: var(--ink-faint); margin: 6px 0 0; }
2860
2844
  /* The compact form: a thumbnail beside a value summary in a ship FEED, where the
2861
2845
  summary is one line in a list and a full-width image would drown it. */
2862
2846
  .ship-visual-thumb {
@@ -68,6 +68,7 @@
68
68
  <script src="/builders/task-detail.js?v=2026-07-03-taskdetail2" defer></script>
69
69
  <script src="/builders/brand-apply.js?v=2026-06-19-r56" defer></script>
70
70
  <script src="/builders/md-reader.js" defer></script>
71
+ <script src="/builders/hall-visual.js?v=2026-09-16-visual1" defer></script>
71
72
  <script src="/builders/task.js?v=2026-07-03-taskpage1" defer></script>
72
73
  </body>
73
74
  </html>
@@ -292,20 +292,17 @@
292
292
  // a detail can be opened full-size. The caption (visual_alt) doubles as the alt
293
293
  // text, which is why it is stored: an image with no alt tells a screen reader
294
294
  // nothing, and "a picture of the change" is exactly the audience that needs it.
295
+ // The shared renderer (hall-visual.js), not a copy of it — task 1003835. This
296
+ // page hand-rolled the same <figure> until then; the markup it produced is what
297
+ // OTBVisual emits, so .tk-visual's rules are unchanged. `className` keeps the
298
+ // sizing this page's own CSS owns (52vh inside a record column).
299
+ //
300
+ // Guarded rather than assumed: hall-visual.js is a separate <script>, so a page
301
+ // that forgot it should render no picture, never a broken one.
295
302
  function taskVisualHtml(t) {
296
- const url = String(t.visual_url || '');
297
- // Same-origin, server-generated paths only. The URL comes from our own API,
298
- // but rendering it into an href/src is a place to be literal rather than
299
- // trusting: anything that is not the shape our serve route mints is dropped.
300
- if (!/^\/api\/(gds|bongos)\/task-visuals\/task-\d+-[0-9a-f]{16}\.(png|jpg|gif|webp)$/.test(url)) return '';
301
- const alt = String(t.visual_alt || `Visual for task ${t.id}`);
302
- const caption = t.visual_alt
303
- ? `<figcaption class="tk-visual__caption">${escapeHtml(t.visual_alt)}</figcaption>`
304
- : '';
305
- return `<figure class="tk-visual">` +
306
- `<a href="${escapeHtml(url)}" target="_blank" rel="noopener">` +
307
- `<img class="tk-visual__img" src="${escapeHtml(url)}" alt="${escapeHtml(alt)}" loading="lazy">` +
308
- `</a>${caption}</figure>`;
303
+ const vis = window.OTBVisual;
304
+ if (!vis || typeof vis.taskVisualFigureHtml !== 'function') return '';
305
+ return vis.taskVisualFigureHtml(t, { className: 'tk-visual' });
309
306
  }
310
307
 
311
308
  async function copyText(text) {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.790",
3
+ "version": "1.19.792",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.790",
9
+ "version": "1.19.792",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.790",
3
+ "version": "1.19.792",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",