@bongos/core 1.24.34 → 1.24.36

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 (99) hide show
  1. package/.bongos-core.json +102 -147
  2. package/.claude/skills/merge-mode/SKILL.md +1 -1
  3. package/docs/adr/0228-the-reading-room-owes-a-reference-content-rules-not-a-second-archetype.md +1 -1
  4. package/docs/copy-inventory.md +136 -192
  5. package/docs/copy-registry.json +186 -752
  6. package/docs/module-api-changelog.md +4 -0
  7. package/docs/onboarding/diagrams/README.md +1 -2
  8. package/docs/onboarding/primer.md +66 -22
  9. package/docs/page-inventory.json +7 -101
  10. package/docs/page-readings.json +947 -1413
  11. package/docs/recipes/ui-look-before-you-ship.md +2 -2
  12. package/modules/design-styles/skills/style/SKILL.md +2 -2
  13. package/modules/hall-ui/public/ai-studio.html +2 -2
  14. package/modules/hall-ui/public/atlas.html +2 -2
  15. package/modules/hall-ui/public/blockers.html +2 -2
  16. package/modules/hall-ui/public/board-room.html +2 -2
  17. package/modules/hall-ui/public/builders.js +8 -6
  18. package/modules/hall-ui/public/city.html +2 -2
  19. package/modules/hall-ui/public/collab.html +2 -2
  20. package/modules/hall-ui/public/commands.html +2 -2
  21. package/modules/hall-ui/public/copy-desk.html +2 -2
  22. package/modules/hall-ui/public/craft-mode.js +1 -1
  23. package/modules/hall-ui/public/deploy.html +2 -2
  24. package/modules/hall-ui/public/diagram-viewer.css +1 -1
  25. package/modules/hall-ui/public/diagram-viewer.js +5 -5
  26. package/modules/hall-ui/public/fleet.html +2 -2
  27. package/modules/hall-ui/public/founding-mark.js +3 -2
  28. package/modules/hall-ui/public/gate.html +2 -2
  29. package/modules/hall-ui/public/goal-map.html +2 -2
  30. package/modules/hall-ui/public/goals.html +2 -2
  31. package/modules/hall-ui/public/government.html +2 -2
  32. package/modules/hall-ui/public/hall-render.js +62 -1
  33. package/modules/hall-ui/public/hall-widgets.js +11 -1
  34. package/modules/hall-ui/public/idea.html +2 -2
  35. package/modules/hall-ui/public/ideas.html +2 -2
  36. package/modules/hall-ui/public/index.html +4 -4
  37. package/modules/hall-ui/public/market-watch-item.html +2 -2
  38. package/modules/hall-ui/public/market-watch-rules.html +2 -2
  39. package/modules/hall-ui/public/market-watch.html +2 -2
  40. package/modules/hall-ui/public/modules.html +2 -2
  41. package/modules/hall-ui/public/my-agents.html +2 -2
  42. package/modules/hall-ui/public/palette.js +15 -0
  43. package/modules/hall-ui/public/primer-live.js +188 -0
  44. package/modules/hall-ui/public/primer.html +11 -6
  45. package/modules/hall-ui/public/primer.js +101 -22
  46. package/modules/hall-ui/public/primer.states.json +54 -7
  47. package/modules/hall-ui/public/profile.html +2 -2
  48. package/modules/hall-ui/public/project-settings.html +2 -2
  49. package/modules/hall-ui/public/roadmap.html +2 -2
  50. package/modules/hall-ui/public/room.css +23 -121
  51. package/modules/hall-ui/public/roster.html +2 -2
  52. package/modules/hall-ui/public/sessions.html +2 -2
  53. package/modules/hall-ui/public/settings.html +2 -2
  54. package/modules/hall-ui/public/shell.js +14 -21
  55. package/modules/hall-ui/public/street.html +2 -2
  56. package/modules/hall-ui/public/studio.html +2 -2
  57. package/modules/hall-ui/public/style.css +8 -55
  58. package/modules/hall-ui/public/task.html +2 -2
  59. package/modules/hall-ui/public/thinking.html +2 -2
  60. package/modules/hall-ui/public/tweak-editor.html +2 -2
  61. package/modules/hall-ui/public/watch.html +2 -2
  62. package/modules/hall-ui/public/work.html +2 -2
  63. package/modules/hall-ui/records/founding-mark.md +4 -4
  64. package/modules/hall-ui/records/reading-rooms.md +2 -0
  65. package/modules/hall-ui/records/reference-room.md +2 -0
  66. package/modules/onboarding/founding-mode.js +6 -2
  67. package/modules/ui-design/kit/render.js +1 -1
  68. package/modules/ui-design/styles/README.md +2 -2
  69. package/package-lock.json +2 -2
  70. package/package.json +1 -1
  71. package/release-notes.json +12 -0
  72. package/scripts/bongos/gen-hall-sitemap.js +15 -2
  73. package/scripts/hall-preview/server.js +6 -11
  74. package/src/bongos/serve-internal.js +28 -37
  75. package/src/module-api.js +1 -1
  76. package/tests/craft_mode_home.mjs +6 -2
  77. package/tests/founding_builders.mjs +6 -5
  78. package/tests/founding_mode.mjs +7 -4
  79. package/tests/hall_board_world.mjs +44 -1
  80. package/tests/hall_fullscreen.mjs +1 -1
  81. package/tests/hall_nav.mjs +24 -7
  82. package/tests/hall_page_gate_map.mjs +14 -5
  83. package/tests/hall_reading_room.mjs +426 -0
  84. package/tests/hall_reading_world.mjs +36 -30
  85. package/tests/hall_sitemap.mjs +25 -0
  86. package/tests/nav_permission_atoms.mjs +3 -2
  87. package/tests/rank_tier_single_source.mjs +0 -1
  88. package/tests/ui_design_kit.mjs +1 -1
  89. package/modules/hall-ui/public/diagrams.css +0 -45
  90. package/modules/hall-ui/public/diagrams.html +0 -106
  91. package/modules/hall-ui/public/diagrams.states.json +0 -28
  92. package/modules/hall-ui/public/drachmae.html +0 -110
  93. package/modules/hall-ui/public/drachmae.js +0 -431
  94. package/modules/hall-ui/public/drachmae.states.json +0 -58
  95. package/modules/hall-ui/public/ranks-founding.states.json +0 -12
  96. package/modules/hall-ui/public/ranks.html +0 -83
  97. package/modules/hall-ui/public/ranks.js +0 -229
  98. package/modules/hall-ui/public/ranks.states.json +0 -15
  99. package/tests/hall_reference_world.mjs +0 -352
@@ -0,0 +1,188 @@
1
+ // primer-live.js — the two LIVE sections of the Primer's "Credits and karma"
2
+ // chapter: the project's treasury (what it has spent, and what it has awarded
3
+ // builders for that spend) and the published security bounty rates.
4
+ //
5
+ // Task 1005046 (owner decision 2026-11-03, idea 1001311): the Economy page
6
+ // (/drachmae) folded into the Primer so credits and karma are explained once.
7
+ // Its prose became the chapter; these two sections are the part that was never
8
+ // prose — numbers read from the server when the reader opens the page — so they
9
+ // moved here whole, from drachmae.js (tasks 2174 and 1003446), as a small
10
+ // module primer.js calls rather than more lines in primer.js.
11
+ //
12
+ // The contract each section keeps, unchanged from the Economy page:
13
+ // * it reads an endpoint that already existed — /public/cost-summary (the
14
+ // status page's treasury chart) and /public/bounty-table — no new route;
15
+ // * every mark is drawn by the shared chart kit (hall-kit.js, window.OTBKit);
16
+ // * the currency word is the branding pack's, never a hardcoded unit;
17
+ // * a feed that cannot be read degrades to NOTHING — the section is removed,
18
+ // never left as an empty panel or an outline entry that leads nowhere.
19
+ // What is new: each section is placed the moment the article renders, with a
20
+ // Loading… line, so the reader sees that something is on its way.
21
+
22
+ (() => {
23
+ 'use strict';
24
+
25
+ const BOUNTY_URL = '/api/bongos/public/bounty-table';
26
+ // NOT a new endpoint: the same /public/cost-summary the signed-out status page
27
+ // draws as a cumulative line. Its `daily[]` is "the treasury chart's
28
+ // cumulative line"; the series was in the payload all along.
29
+ const TREASURY_URL = '/api/bongos/public/cost-summary';
30
+
31
+ // The two sections' addresses. Kept from the Economy page, so a link someone
32
+ // copied there (/drachmae#the-treasury) names the same section here.
33
+ const SECTIONS = {
34
+ treasury: { id: 'the-treasury', title: 'The treasury, so far' },
35
+ bounty: { id: 'security-bounty-rates', title: 'Security bounty rates' },
36
+ };
37
+
38
+ const esc = (s) => window.OTB.escapeHtml(s);
39
+ const currencyLabel = () => (window.__BRANDING__ && window.__BRANDING__.currency
40
+ && window.__BRANDING__.currency.label) || 'credits';
41
+ const caps = (s) => s.charAt(0).toUpperCase() + s.slice(1);
42
+
43
+ // A section with its heading and a Loading… line, ready to be placed. The h3
44
+ // carries the address from the start, so a deep link has somewhere to land
45
+ // even before the numbers arrive.
46
+ function headingHtml(kind) {
47
+ const def = SECTIONS[kind];
48
+ return `<h3 id="${esc(def.id)}">${esc(def.title)}</h3>`;
49
+ }
50
+ function placeholder(kind) {
51
+ const section = document.createElement('section');
52
+ section.className = 'primer-live';
53
+ section.dataset.live = kind;
54
+ section.setAttribute('aria-busy', 'true');
55
+ section.innerHTML = headingHtml(kind) + '<p class="hall-loading" role="status">Loading…</p>';
56
+ return section;
57
+ }
58
+
59
+ // Replace the Loading… line with the real body, or take the section away.
60
+ function settle(section, bodyHtml) {
61
+ if (!bodyHtml) { section.remove(); return false; }
62
+ section.innerHTML = headingHtml(section.dataset.live) + bodyHtml;
63
+ section.removeAttribute('aria-busy');
64
+ return true;
65
+ }
66
+
67
+ async function readJson(url) {
68
+ try {
69
+ const r = await fetch(url, { credentials: 'same-origin' });
70
+ if (!r.ok) throw new Error(`${url} → ${r.status}`);
71
+ return await r.json();
72
+ } catch (err) {
73
+ console.warn('[primer] live section unavailable:', err.message);
74
+ return null;
75
+ }
76
+ }
77
+
78
+ // ---- the security bounty rate card (task 2174) ---------------------------
79
+
80
+ function bountyHtml(payload) {
81
+ const rows = Array.isArray(payload && payload.table) ? payload.table : [];
82
+ if (!rows.length) return '';
83
+ const currency = payload.currency || currencyLabel();
84
+ return `<p>Find a flaw in the system’s own walls and report it: rewards are paid in `
85
+ + `${esc(currency)} by severity. These are the live numbers, read from the server `
86
+ + 'when you opened this page, so a tuned rate shows up here immediately.</p>'
87
+ + '<table><thead><tr><th>Severity</th>'
88
+ + `<th>${esc(caps(currency))}</th>`
89
+ + '<th>What counts</th></tr></thead><tbody>'
90
+ + rows.map((r) => {
91
+ // `overridden` means this project tuned the tier off its default.
92
+ const tuned = r.overridden ? ' <em>(tuned)</em>' : '';
93
+ return `<tr><td><strong>${esc(r.severity || '')}</strong></td>`
94
+ + `<td>${esc(String(r.drachmae ?? ''))}${tuned}</td>`
95
+ + `<td>${esc(r.blurb || '')}</td></tr>`;
96
+ }).join('')
97
+ + '</tbody></table>';
98
+ }
99
+
100
+ async function fillBounty(section) {
101
+ return settle(section, bountyHtml(await readJson(BOUNTY_URL)));
102
+ }
103
+
104
+ // ---- the treasury (task 1003446) -----------------------------------------
105
+
106
+ // One cumulative figure: the plot, the number it ends on, and the table twin
107
+ // that keeps it out of picture-only territory.
108
+ function treasuryFigure({ id, title, points, fmt, foot }) {
109
+ const kit = window.OTBKit;
110
+ if (!points.length) return '';
111
+ const last = points[points.length - 1];
112
+ const series = kit.timeStepSeriesHtml(points, { x0: points[0].x, x1: last.x, yMax: last.value });
113
+ const { endX, endY } = series.geometry;
114
+ const day = (t) => new Date(t).toISOString().slice(0, 10);
115
+ // Every 7th day plus the last: one row per calendar day would be a table
116
+ // nobody finishes reading.
117
+ const twinRows = points.filter((p, i) => i % 7 === 0 || i === points.length - 1);
118
+ return `<figure class="pchart" id="${esc(id)}">
119
+ <figcaption class="pchart__title">${esc(title)}</figcaption>
120
+ ${kit.plotSvgHtml({
121
+ series: series.body,
122
+ // No readout is wired here, so no focus stop and no dead crosshair.
123
+ interactive: false,
124
+ ariaLabel: `${title}: ${fmt(last.value)} as of ${day(last.x)}`,
125
+ label: kit.plotValueLabelHtml(endX, endY - 8, fmt(last.value), 'end'),
126
+ leftTick: day(points[0].x),
127
+ rightTick: day(last.x),
128
+ })}
129
+ <details class="pchart__twin">
130
+ <summary>Read it as a table</summary>
131
+ <table class="ledger pchart__table"><thead><tr><th>Day</th><th>${esc(title)}</th></tr></thead><tbody>
132
+ ${twinRows.map((p) => `<tr><td>${esc(day(p.x))}</td><td>${esc(fmt(p.value))}</td></tr>`).join('')}
133
+ </tbody></table>
134
+ </details>
135
+ <p class="pchart__foot">${esc(foot)}</p>
136
+ </figure>`;
137
+ }
138
+
139
+ function treasuryHtml(payload) {
140
+ const kit = window.OTBKit;
141
+ if (!payload || !kit) return '';
142
+ const currency = currencyLabel();
143
+ const usd = (n) => `$${Number(n || 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
144
+ const num = (n) => Number(n || 0).toLocaleString();
145
+ // The server already ran generate_series, so every calendar day is present
146
+ // and a flat run is a real flat run rather than a missing sample.
147
+ const spend = kit.cumulateByDay(payload.daily, { valueOf: (r) => r.amount_usd });
148
+ const earned = kit.cumulateByDay(payload.daily_credits, { valueOf: (r) => r.credits });
149
+ // Both series empty is a treasury with nothing in it yet: no section at all.
150
+ if (!spend.length && !earned.length) return '';
151
+ const figures = [
152
+ treasuryFigure({
153
+ id: 'treasury-spend', title: 'Spent, cumulative', points: spend, fmt: usd,
154
+ foot: 'Every recorded cost — API, infra, compute, art — added up day by day.',
155
+ }),
156
+ treasuryFigure({
157
+ id: 'treasury-earned', title: `${caps(currency)} awarded, cumulative`, points: earned, fmt: num,
158
+ foot: `What the project has awarded builders, in ${currency}, day by day.`,
159
+ }),
160
+ ].filter(Boolean).join('');
161
+ // Where the money went, as the kit's bar series. Categories arrive sorted
162
+ // biggest-first.
163
+ const byCategory = (payload.by_category || []).slice(0, 8);
164
+ const categories = byCategory.length
165
+ ? '<h4 id="treasury-by-category">Where it went</h4>'
166
+ + kit.barSeriesHtml(
167
+ byCategory.map((c) => ({ label: c.category, value: c.total_usd, valueLabel: usd(c.total_usd) })),
168
+ { list: true, ariaLabel: 'Spend by category', valueWidth: '84px' },
169
+ )
170
+ : '';
171
+ return '<p>These are this project’s real numbers, read from the server when you '
172
+ + `opened this page. It has spent <strong>${esc(usd(payload.total_usd))}</strong> `
173
+ + `and awarded <strong>${esc(num(payload.total_value_credits))} ${esc(currency)}</strong> `
174
+ + 'for the work that money bought.</p>'
175
+ + `<div class="pcharts">${figures}</div>`
176
+ + categories;
177
+ }
178
+
179
+ async function fillTreasury(section) {
180
+ if (!window.OTBKit) {
181
+ console.warn('[primer] OTBKit missing — the treasury section needs the chart primitive');
182
+ return settle(section, '');
183
+ }
184
+ return settle(section, treasuryHtml(await readJson(TREASURY_URL)));
185
+ }
186
+
187
+ window.OTBPrimerLive = { SECTIONS, placeholder, fillBounty, fillTreasury, bountyHtml, treasuryHtml };
188
+ })();
@@ -4,14 +4,14 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Primer — {{worldName}}</title>
7
- <meta name="description" content="The guide a new builder reads on day one. What {{productName}} is, how the build system works, the claim-to-ship lifecycle, the rank ladder, and {{currency}} and karma.">
7
+ <meta name="description" content="The guide a new builder reads on day one. What {{productName}} is, how the build system works, the claim-to-ship lifecycle, the rank ladder, {{currency}} and karma, and how versions organise the work, each with its diagram.">
8
8
  <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
11
  <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
12
12
  <link rel="stylesheet" href="/builders/room.css">
13
13
  <link rel="stylesheet" href="/builders/primer.css?v=2026-07-02-shell1">
14
- <link rel="stylesheet" href="/builders/diagram-viewer.css?v=2026-06-14-diagviewer-1">
14
+ <link rel="stylesheet" href="/builders/diagram-viewer.css?v=2026-11-04-reading-room">
15
15
  </head>
16
16
  <!-- `room` opts this page into the reading-room archetype's composition tokens
17
17
  (room.css). They sit on the body, never in a page :root — the branding
@@ -112,15 +112,20 @@
112
112
 
113
113
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
114
114
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
115
- <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
116
- <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
115
+ <script src="/builders/craft-mode.js?v=2026-11-04-reading-room"></script>
116
+ <script src="/builders/shell.js?v=2026-11-04-reading-room"></script>
117
117
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
118
118
  <script src="/builders/palette.js" defer></script>
119
119
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
120
120
  it rather than carrying a second copy of the grammar. -->
121
121
  <script src="/builders/md-reader.js" defer></script>
122
- <script src="/builders/primer.js?v=2026-05-29-primer-1" defer></script>
123
- <script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" defer></script>
122
+ <!-- The credits chapter's live sections (task 1005046, from the retired Economy
123
+ page): the chart kit first, then the module that draws with it, both before
124
+ primer.js, which places them once the article renders. -->
125
+ <script src="/builders/hall-kit.js" defer></script>
126
+ <script src="/builders/primer-live.js?v=2026-11-04-reading-room" defer></script>
127
+ <script src="/builders/primer.js?v=2026-11-04-reading-room-lazy" defer></script>
128
+ <script src="/builders/diagram-viewer.js?v=2026-11-04-reading-room" defer></script>
124
129
  <script src="/builders/copy-on-click.js?v=2026-06-01-copy-1" defer></script>
125
130
  </body>
126
131
  </html>
@@ -2,7 +2,9 @@
2
2
  //
3
3
  // Fetches /builders/primer.md (canonical markdown from docs/onboarding/),
4
4
  // renders it through the SHARED reader (md-reader.js / window.OTBMdReader),
5
- // inlines the five onboarding diagrams at matching sections, builds a TOC from
5
+ // inlines the five onboarding diagrams in their chapters, adds the credits
6
+ // chapter's two live sections (primer-live.js), lands a deep link once all of
7
+ // that is in (task 1005046: the hall's one reading room), builds a TOC from
6
8
  // h2s with a scrollspy, drives a reading-progress hairline, and soft-gates the
7
9
  // footer ack button until the reader has scrolled past ~90% of the article.
8
10
  // Acking POSTs to /api/bongos/builders/me/onboarding/primer-read (same endpoint
@@ -37,17 +39,29 @@
37
39
  let repoBlobBase = null;
38
40
  let repoTreeBase = null;
39
41
 
40
- // Diagrams to inline at matching h2 sections. The keys are LOWERCASED h2
41
- // text (matched as substrings) — keep them tight so the wrong heading
42
- // doesn't pick up the wrong picture. The image paths resolve at
43
- // /builders/diagrams/* (mounted in server.js).
44
- const DIAGRAM_AT_HEADING = [
45
- { match: 'claim → ship lifecycle', src: '/builders/diagrams/01-task-lifecycle.png', alt: 'The full task lifecycle from backlog through shipped' },
46
- { match: 'rank ladder', src: '/builders/diagrams/02-rank-ladder.png', alt: 'The rank ladder — the ranks a builder can hold and how to rise' },
47
- { match: 'drachmae and karma', src: '/builders/diagrams/03-drachmae-karma.png', alt: 'How the reward currency and karma differ — pay for shipped work vs. reputation signals' },
48
- { match: 'get set up', src: '/builders/diagrams/04-architecture.png', alt: 'The complete architecture: your machine holds the worker, the code, and your local memory, the server is the authority and production, GitHub holds the repo — and how code travels from your machine to prod' },
42
+ // The five onboarding diagrams, each inlined in the chapter it illustrates
43
+ // (task 1005046: the Diagrams page folded into this one, so this table is now
44
+ // the ONLY place a diagram is shown in the hall). Keyed by the chapter's h2 id
45
+ // — the slug md-reader.js gives it — not by a substring of its words, so a
46
+ // retitled chapter fails loudly in tests/hall_reading_world.mjs instead of
47
+ // silently losing its picture (the credits diagram did exactly that when the
48
+ // chapter's heading stopped matching 'drachmae and karma'). The image paths
49
+ // resolve at /builders/diagrams/* (serve-internal.js mounts the directory).
50
+ const DIAGRAM_AT_CHAPTER = [
51
+ { id: 'the-claim-ship-lifecycle', src: '/builders/diagrams/01-task-lifecycle.png', alt: 'The task lifecycle from backlog to shipped, with the step where credits land' },
52
+ { id: 'the-rank-ladder', src: '/builders/diagrams/02-rank-ladder.png', alt: 'The rank ladder: which ranks are live today and what each may do' },
53
+ { id: 'credits-and-karma', src: '/builders/diagrams/03-drachmae-karma.png', alt: 'Where credits and karma come from, and where each shows up' },
54
+ { id: 'how-versions-organise-the-work', src: '/builders/diagrams/05-versioning.png', alt: 'How a planning session seeds a version\'s task pool, and how a version closes' },
55
+ { id: 'get-set-up-on-your-machine', src: '/builders/diagrams/04-architecture.png', alt: 'The complete architecture: your machine holds the worker, the code and your local memory; the server is the authority and production; GitHub holds the repo — and the path a change takes to production' },
49
56
  ];
50
57
 
58
+ // The chapter the two live sections close (task 1005046, moved from the
59
+ // retired /drachmae page): what the treasury has actually spent and paid, and
60
+ // the published security bounty rates. They go at the end of this chapter,
61
+ // ahead of the next one, as h3s — so the table of contents stays one line per
62
+ // chapter and a deep link (#the-treasury, #security-bounty-rates) still works.
63
+ const LIVE_CHAPTER_ID = 'credits-and-karma';
64
+
51
65
  // -----------------------------------------------------------------------
52
66
  // Tiny utilities
53
67
  // -----------------------------------------------------------------------
@@ -57,17 +71,25 @@
57
71
  const MD = window.OTBMdReader;
58
72
  const escapeHtml = (s) => window.OTB.escapeHtml(s); // task 1004876: the old fallback escaped nothing
59
73
 
60
- // The shared resolver, plus the one rewrite that is the primer's alone: the
61
- // economy doc has an in-hall reader page (task 1472), so a link to it stays in
62
- // the hall instead of bouncing the reader out to GitHub.
74
+ // The shared resolver, plus the rewrites that are the primer's alone: a link
75
+ // written as a hall address (/builders/…) is a page in this hall, so it stays
76
+ // here instead of being dressed as a repo path on GitHub (task 1005046: the
77
+ // rank ladder sends the reader to their own standing). And a link to a hall
78
+ // page's own file (../../modules/hall-ui/public/<page>.html) opens that page
79
+ // here (task 1005069); on GitHub the same link shows the file.
63
80
  const resolveLinkHref = (() => {
81
+ const HALL_PAGE = /^(?:\.\.\/)+modules\/hall-ui\/public\/([a-z0-9-]+)\.html$/;
64
82
  if (!MD) return (h) => h;
65
83
  const base = MD.makeResolveLinkHref({
66
84
  baseDir: PRIMER_DIR,
67
85
  getBlobBase: () => repoBlobBase,
68
86
  getTreeBase: () => repoTreeBase,
69
87
  });
70
- return (href) => (href === 'drachmae-and-karma.md' ? '/builders/drachmae' : base(href));
88
+ return (href) => {
89
+ if (/^\/builders(?:[/#?]|$)/.test(String(href || ''))) return href;
90
+ const page = HALL_PAGE.exec(href || '');
91
+ return page ? '/builders/' + page[1] : base(href);
92
+ };
71
93
  })();
72
94
 
73
95
  function toast(msg, type) {
@@ -90,19 +112,17 @@
90
112
  // -----------------------------------------------------------------------
91
113
 
92
114
  function inlineDiagrams(articleEl) {
93
- const h2s = articleEl.querySelectorAll('h2');
94
- for (const h of h2s) {
95
- const txt = h.textContent.toLowerCase();
96
- const match = DIAGRAM_AT_HEADING.find((d) => txt.includes(d.match));
97
- if (!match) continue;
115
+ for (const d of DIAGRAM_AT_CHAPTER) {
116
+ const h = articleEl.querySelector(`h2[id="${d.id}"]`);
117
+ if (!h) continue;
98
118
  const fig = document.createElement('figure');
99
119
  fig.className = 'primer-figure';
100
120
  const img = document.createElement('img');
101
- img.src = match.src;
102
- img.alt = match.alt;
121
+ img.src = d.src;
122
+ img.alt = d.alt;
103
123
  img.loading = 'lazy';
104
124
  const cap = document.createElement('figcaption');
105
- cap.textContent = match.alt;
125
+ cap.textContent = d.alt;
106
126
  fig.appendChild(img);
107
127
  fig.appendChild(cap);
108
128
  // Place the figure AFTER the h2's first paragraph so it sits inside
@@ -117,6 +137,63 @@
117
137
  }
118
138
  }
119
139
 
140
+ // The live sections of the credits chapter (primer-live.js). Placed at the
141
+ // END of that chapter — ahead of the next h2 — each with a Loading… line, and
142
+ // removed outright if its feed cannot be read, so the chapter never shows an
143
+ // empty panel or an outline entry that leads nowhere.
144
+ //
145
+ // FETCHED ON APPROACH, not on load. The Primer is the hall's most-read page
146
+ // and three old URLs now land on it, while most readers never scroll to the
147
+ // treasury; firing /public/cost-summary (a day-by-day cumulative query) and
148
+ // /public/bounty-table on every open would spend that on readers who never
149
+ // see it. So the two reads start when the reader comes within a screen of
150
+ // the sections — or at once when the address points at the credits chapter
151
+ // or either section (an old /drachmae link), so a deep link lands on their
152
+ // final layout. Resolves when there is nothing left to wait for before
153
+ // landing: after the fills on a deep link, at once otherwise.
154
+ const LIVE_HASHES = new Set([LIVE_CHAPTER_ID, 'the-treasury', 'security-bounty-rates']);
155
+ function insertLiveSections(articleEl) {
156
+ const live = window.OTBPrimerLive;
157
+ const h = articleEl.querySelector(`h2[id="${LIVE_CHAPTER_ID}"]`);
158
+ if (!live || !h) return Promise.resolve();
159
+ let next = h.nextElementSibling;
160
+ while (next && next.tagName !== 'H2') next = next.nextElementSibling;
161
+ const place = (el) => { if (next) articleEl.insertBefore(el, next); else articleEl.appendChild(el); };
162
+ const treasury = live.placeholder('treasury');
163
+ const bounty = live.placeholder('bounty');
164
+ place(treasury);
165
+ place(bounty);
166
+ let started = null;
167
+ const fill = () => {
168
+ if (!started) started = Promise.allSettled([live.fillTreasury(treasury), live.fillBounty(bounty)]);
169
+ return started;
170
+ };
171
+ let hash = '';
172
+ try { hash = decodeURIComponent(window.location.hash.slice(1)); } catch (_) { hash = ''; }
173
+ if (LIVE_HASHES.has(hash) || typeof window.IntersectionObserver !== 'function') return fill();
174
+ const io = new window.IntersectionObserver((entries) => {
175
+ if (!entries.some((e) => e.isIntersecting)) return;
176
+ io.disconnect();
177
+ fill();
178
+ }, { rootMargin: '100% 0px' });
179
+ io.observe(treasury);
180
+ io.observe(bounty);
181
+ return Promise.resolve();
182
+ }
183
+
184
+ // A deep link — /primer#the-rank-ladder, or an old /ranks, /drachmae or
185
+ // /diagrams URL that serve-internal.js 302s here — arrives before the article
186
+ // exists, so the browser's own jump found nothing. Once the document and its
187
+ // live sections are in, scroll the target into view. After the fonts: a swap
188
+ // after the jump moves everything above the target.
189
+ function landOnHash() {
190
+ let id = '';
191
+ // a malformed percent-escape in the fragment throws; there is nothing to land on
192
+ try { id = decodeURIComponent(window.location.hash.slice(1)); } catch (_) { return; }
193
+ const el = id && document.getElementById(id);
194
+ if (el) el.scrollIntoView({ block: 'start' });
195
+ }
196
+
120
197
  // -----------------------------------------------------------------------
121
198
  // TOC — built from h2 headings. The numeral is drawn by room.css's counter,
122
199
  // so the markup carries only the title.
@@ -423,12 +500,14 @@
423
500
  article.removeAttribute('aria-busy');
424
501
  show('article');
425
502
  inlineDiagrams(article);
503
+ const live = insertLiveSections(article);
426
504
  const tocSections = renderToc(headings);
427
505
  setupTocToggle();
428
506
  setupScrollspy(tocSections);
429
507
  setupProgressBar(article, onArticleProgress);
430
508
  setupAck();
431
509
  checkAlreadyAcked();
510
+ live.then(() => (document.fonts ? document.fonts.ready : null)).then(landOnHash);
432
511
  }
433
512
 
434
513
  if (document.readyState === 'loading') {
@@ -2,7 +2,9 @@
2
2
  "_": "The hall's primer through modules/ui-design/kit/render.js (task 1003306). Written with the reading-room re-skin so the room's failure state and its table of contents are MEASURED at 1440/390/320 in both modes, not just looked at.",
3
3
  "page": "/builders/primer",
4
4
  "surface": "hall-ui",
5
- "stub": { "prefix": "/builders/" },
5
+ "stub": {
6
+ "prefix": "/builders/"
7
+ },
6
8
  "modeQuery": false,
7
9
  "_feeds": "The kit's stub carries the public-landing fixtures, not the hall's, and it serves no /builders/primer.md at all — so the room renders its SKELETON and then its FAILURE note here, which is the point: the two states before the document are the ones nobody looks at, and they have to hold the floors too. The populated document is the hall-preview harness's job: node scripts/hall-preview/server.js --port 4612 --fixture-me, then this same file with --base-in/--base-out pointed at it.",
8
10
  "ignoreRequests": [
@@ -11,7 +13,10 @@
11
13
  "/api/(gds|bongos)/goals",
12
14
  "/api/(gds|bongos)/inbox/",
13
15
  "/api/(gds|bongos)/public/repo-info",
14
- "/builders/primer.md"
16
+ "/builders/primer.md",
17
+ "/api/(gds|bongos)/public/cost-summary",
18
+ "/api/(gds|bongos)/public/bounty-table",
19
+ "/builders/diagrams/"
15
20
  ],
16
21
  "ignoreConsole": [
17
22
  "/api/(gds|bongos)/",
@@ -25,14 +30,56 @@
25
30
  ],
26
31
  "_expect": "The expectations name only what is true under BOTH bases. Under the kit stub there is no primer.md, so the failure note renders; under the harness the real document does. Pinning either one would make this file pass in one place and fail in the other, which is how a states file stops being run against the harness at all.",
27
32
  "states": {
28
- "in": { "auth": true, "expect": { "visible": ["#hall-main", "#primer-toc"] } },
33
+ "in": {
34
+ "auth": true,
35
+ "expect": {
36
+ "visible": [
37
+ "#hall-main",
38
+ "#primer-toc"
39
+ ]
40
+ }
41
+ },
29
42
  "toc-open": {
30
43
  "auth": true,
31
- "actions": [["clickIfVisible", "#primer-toc-toggle"], ["wait", 250]],
44
+ "actions": [
45
+ [
46
+ "clickIfVisible",
47
+ "#primer-toc-toggle"
48
+ ],
49
+ [
50
+ "wait",
51
+ 250
52
+ ]
53
+ ],
32
54
  "_": "the room's one disclosure. Below 760px the contents collapse behind this toggle; above it the toggle is not rendered at all, so clickIfVisible is the right verb and the state is a no-op at 1440.",
33
- "expect": { "visible": ["#primer-toc"] }
55
+ "expect": {
56
+ "visible": [
57
+ "#primer-toc"
58
+ ]
59
+ }
60
+ },
61
+ "jump": {
62
+ "auth": true,
63
+ "url": "/builders/primer#credits-and-karma",
64
+ "actions": [
65
+ [
66
+ "wait",
67
+ 800
68
+ ]
69
+ ],
70
+ "_": "where an old /drachmae link lands (task 1005046: it 302s here). Under the harness the credits chapter is in view with its diagram and the two live sections below; under the kit stub it is the failure note again, which the floors must hold in too.",
71
+ "expect": {
72
+ "visible": [
73
+ "#hall-main",
74
+ "#primer-toc"
75
+ ]
76
+ }
34
77
  },
35
- "out": { "auth": false }
78
+ "out": {
79
+ "auth": false
80
+ }
36
81
  },
37
- "reducedMotion": { "auth": true }
82
+ "reducedMotion": {
83
+ "auth": true
84
+ }
38
85
  }
@@ -207,8 +207,8 @@
207
207
  chrome exists before the page script runs. -->
208
208
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
209
209
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
210
- <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
211
- <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
210
+ <script src="/builders/craft-mode.js?v=2026-11-04-reading-room"></script>
211
+ <script src="/builders/shell.js?v=2026-11-04-reading-room"></script>
212
212
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
213
213
  <script src="/builders/palette.js" defer></script>
214
214
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
@@ -86,8 +86,8 @@
86
86
 
87
87
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
88
88
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
89
- <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
90
- <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
89
+ <script src="/builders/craft-mode.js?v=2026-11-04-reading-room"></script>
90
+ <script src="/builders/shell.js?v=2026-11-04-reading-room"></script>
91
91
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
92
92
  <script src="/builders/palette.js" defer></script>
93
93
  <script src="/builders/hall-kit.js"></script>
@@ -43,8 +43,8 @@
43
43
 
44
44
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
45
45
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
46
- <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
47
- <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
46
+ <script src="/builders/craft-mode.js?v=2026-11-04-reading-room"></script>
47
+ <script src="/builders/shell.js?v=2026-11-04-reading-room"></script>
48
48
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
49
49
  <script src="/builders/palette.js" defer></script>
50
50
  <!-- The chart kit and the timeline's series builders (task 1003447). Both MUST