@bongos/core 1.24.35 → 1.24.37

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 (112) hide show
  1. package/.bongos-core.json +128 -168
  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 +164 -219
  5. package/docs/copy-registry.json +323 -892
  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 +39 -133
  10. package/docs/page-readings.json +882 -1346
  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 +2 -2
  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-costs.js +3 -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/money.html +92 -0
  42. package/modules/hall-ui/public/money.js +302 -0
  43. package/modules/hall-ui/public/money.states.json +15 -0
  44. package/modules/hall-ui/public/my-agents.html +2 -2
  45. package/modules/hall-ui/public/palette.js +22 -0
  46. package/modules/hall-ui/public/primer-live.js +101 -0
  47. package/modules/hall-ui/public/primer.html +10 -6
  48. package/modules/hall-ui/public/primer.js +93 -23
  49. package/modules/hall-ui/public/primer.states.json +54 -7
  50. package/modules/hall-ui/public/profile.html +2 -2
  51. package/modules/hall-ui/public/project-settings.css +2 -1
  52. package/modules/hall-ui/public/project-settings.html +6 -16
  53. package/modules/hall-ui/public/project-settings.js +2 -142
  54. package/modules/hall-ui/public/roadmap.html +2 -2
  55. package/modules/hall-ui/public/room.css +23 -121
  56. package/modules/hall-ui/public/roster.html +2 -2
  57. package/modules/hall-ui/public/sessions.html +2 -2
  58. package/modules/hall-ui/public/settings.html +2 -2
  59. package/modules/hall-ui/public/shell.js +21 -26
  60. package/modules/hall-ui/public/street.html +2 -2
  61. package/modules/hall-ui/public/studio.html +2 -2
  62. package/modules/hall-ui/public/style.css +8 -55
  63. package/modules/hall-ui/public/task.html +2 -2
  64. package/modules/hall-ui/public/thinking.html +2 -2
  65. package/modules/hall-ui/public/tweak-editor.html +2 -2
  66. package/modules/hall-ui/public/watch.html +2 -2
  67. package/modules/hall-ui/public/work.html +2 -2
  68. package/modules/hall-ui/records/founding-mark.md +4 -4
  69. package/modules/hall-ui/records/reading-rooms.md +2 -0
  70. package/modules/hall-ui/records/reference-room.md +2 -0
  71. package/modules/onboarding/founding-mode.js +6 -2
  72. package/modules/ui-design/kit/render.js +1 -1
  73. package/modules/ui-design/styles/README.md +2 -2
  74. package/package-lock.json +2 -2
  75. package/package.json +1 -1
  76. package/release-notes.json +12 -0
  77. package/scripts/bongos/gen-hall-sitemap.js +3 -0
  78. package/scripts/hall-preview/server.js +6 -11
  79. package/src/bongos/auth.js +0 -8
  80. package/src/bongos/serve-internal.js +43 -43
  81. package/src/module-api.js +1 -1
  82. package/tests/auth_resolve_failure.mjs +5 -4
  83. package/tests/craft_mode_home.mjs +6 -2
  84. package/tests/fleet_cost_view.mjs +2 -18
  85. package/tests/founding_builders.mjs +6 -5
  86. package/tests/founding_mode.mjs +7 -4
  87. package/tests/hall_board_world.mjs +44 -1
  88. package/tests/hall_fullscreen.mjs +1 -1
  89. package/tests/hall_loading.mjs +1 -1
  90. package/tests/hall_money_page.mjs +272 -0
  91. package/tests/hall_nav.mjs +29 -11
  92. package/tests/hall_oversight_world.mjs +1 -1
  93. package/tests/hall_page_gate_map.mjs +17 -7
  94. package/tests/hall_reading_room.mjs +385 -0
  95. package/tests/hall_reading_world.mjs +36 -30
  96. package/tests/nav_permission_atoms.mjs +3 -2
  97. package/tests/rank_tier_single_source.mjs +0 -1
  98. package/tests/ui_design_kit.mjs +1 -1
  99. package/modules/hall-ui/public/diagrams.css +0 -45
  100. package/modules/hall-ui/public/diagrams.html +0 -106
  101. package/modules/hall-ui/public/diagrams.states.json +0 -28
  102. package/modules/hall-ui/public/drachmae.html +0 -110
  103. package/modules/hall-ui/public/drachmae.js +0 -431
  104. package/modules/hall-ui/public/drachmae.states.json +0 -58
  105. package/modules/hall-ui/public/fleet.html +0 -69
  106. package/modules/hall-ui/public/fleet.js +0 -41
  107. package/modules/hall-ui/public/fleet.states.json +0 -14
  108. package/modules/hall-ui/public/ranks-founding.states.json +0 -12
  109. package/modules/hall-ui/public/ranks.html +0 -83
  110. package/modules/hall-ui/public/ranks.js +0 -229
  111. package/modules/hall-ui/public/ranks.states.json +0 -15
  112. package/tests/hall_reference_world.mjs +0 -352
@@ -0,0 +1,385 @@
1
+ // tests/hall_reading_room.mjs — ONE reading room (task 1005046, owner decision
2
+ // 2026-11-03, idea 1001311).
3
+ //
4
+ // Four Docs pages explained the same three topics — the task lifecycle, the rank
5
+ // ladder, and credits and karma — so their copies drifted. Ranks, Economy
6
+ // (/drachmae) and Diagrams became chapters of the Primer, each with its diagram
7
+ // beside it. This file holds what makes that true and keeps it true:
8
+ //
9
+ // * EVERY OLD URL LANDS ON ITS CHAPTER. /ranks, /drachmae and /diagrams 302 to
10
+ // the Primer chapter that replaced each — run against the real middleware,
11
+ // on both host shapes — and the diagram IMAGES under /builders/diagrams/*
12
+ // still serve (behind sign-in) rather than being swallowed by the redirect.
13
+ // * THE CHAPTERS EXIST, AND EACH CARRIES ITS DIAGRAM. Every id primer.js
14
+ // inlines a diagram at, every redirect target and every palette jump is a
15
+ // real h2/h3 slug of docs/onboarding/primer.md, computed by the SAME slugify
16
+ // the page runs — so retitling a chapter fails here, not silently in a
17
+ // browser (the credits diagram was lost that way once).
18
+ // * THE ECONOMY PAGE'S LIVE SECTION MOVED WHOLE. The bounty rate card
19
+ // (primer-live.js, from drachmae.js, task 2174) is RUN in a vm against the
20
+ // checked-in fixture: a Loading… line first, the real numbers after, and
21
+ // NOTHING — not an empty panel — when the feed fails. (The treasury beside
22
+ // it went to the money page in task 1005047: tests/hall_money_page.mjs.)
23
+ // * THE SHARED READER the economy doc leaned on (h4, linked headings, nested
24
+ // emphasis) stays pinned: drachmae-and-karma.md is still the full reference.
25
+ //
26
+ // Moved here from tests/hall_reference_world.mjs, whose reference-room rules
27
+ // (room--reference, the h3 outline, the heading anchors) went with the page.
28
+
29
+ import { test } from 'node:test';
30
+ import assert from 'node:assert/strict';
31
+ import fs from 'node:fs';
32
+ import http from 'node:http';
33
+ import path from 'node:path';
34
+ import vm from 'node:vm';
35
+ import { createRequire } from 'node:module';
36
+ import { fileURLToPath } from 'node:url';
37
+ import { realOTB } from './helpers.mjs';
38
+
39
+ const require = createRequire(import.meta.url);
40
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
41
+ const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
42
+ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
43
+
44
+ const roomCss = read('room.css');
45
+ const primerHtml = read('primer.html');
46
+ const primerJs = read('primer.js');
47
+ const liveJs = read('primer-live.js');
48
+ const PRIMER_MD = fs.readFileSync(path.join(ROOT, 'docs', 'onboarding', 'primer.md'), 'utf8');
49
+
50
+ const decomment = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
51
+ const rule = (css, selector) => {
52
+ const esc = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
53
+ const m = decomment(css).match(new RegExp(`(^|[},])\\s*${esc}\\s*\\{([^}]*)\\}`, 'm'));
54
+ return m ? m[2] : null;
55
+ };
56
+
57
+ function loadReader() {
58
+ const sandbox = { window: { OTB: realOTB() }, location: { hostname: 'builders.example.com', href: 'https://builders.example.com/primer' } };
59
+ vm.createContext(sandbox);
60
+ vm.runInContext(read('md-reader.js'), sandbox, { filename: 'md-reader.js' });
61
+ return sandbox.window.OTBMdReader;
62
+ }
63
+
64
+ // The primer's headings exactly as the page renders them: parsed by the shared
65
+ // reader, ids from its slugify.
66
+ function primerHeadings() {
67
+ return loadReader().parseMarkdown(PRIMER_MD, (h) => h).headings;
68
+ }
69
+ const h2Ids = () => primerHeadings().filter((h) => h.level === 2).map((h) => h.id);
70
+
71
+ // ---- 1. every old URL lands on its chapter ---------------------------------
72
+
73
+ // The real middleware, mounted on a bare app. Nothing here needs a database:
74
+ // the redirect runs before any gate, and the gate it hands an image to only has
75
+ // to redirect an anonymous request into sign-in.
76
+ async function withHall(fn) {
77
+ const express = require('express');
78
+ const { mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
79
+ const app = express();
80
+ mountInternalSurfaces(app);
81
+ const server = app.listen(0, '127.0.0.1');
82
+ await new Promise((r) => server.once('listening', r));
83
+ const { port } = server.address();
84
+ const get = (p, host) => new Promise((resolve, reject) => {
85
+ http.get({ host: '127.0.0.1', port, path: p, headers: { host } }, (res) => {
86
+ res.resume();
87
+ res.on('end', () => resolve({ status: res.statusCode, location: res.headers.location || '' }));
88
+ }).on('error', reject);
89
+ });
90
+ try { await fn(get); } finally { server.close(); }
91
+ }
92
+
93
+ const CHAPTER_OF = {
94
+ ranks: 'the-rank-ladder',
95
+ drachmae: 'credits-and-karma',
96
+ diagrams: 'the-claim-ship-lifecycle',
97
+ };
98
+
99
+ test('RUN: /ranks, /drachmae and /diagrams each 302 to the Primer chapter that replaced them', async () => {
100
+ await withHall(async (get) => {
101
+ for (const [page, chapter] of Object.entries(CHAPTER_OF)) {
102
+ for (const form of [`/${page}`, `/${page}/`, `/${page}.html`]) {
103
+ const r = await get(form, 'builders.127.0.0.1');
104
+ assert.equal(r.status, 302, `${form} is a temporary redirect, so a later re-point is never cached past`);
105
+ assert.equal(r.location, `/primer#${chapter}`, `${form} lands on #${chapter}`);
106
+ }
107
+ }
108
+ // A query string rides along; the chapter is the fragment after it.
109
+ const q = await get('/drachmae?from=mail', 'builders.127.0.0.1');
110
+ assert.equal(q.location, '/primer?from=mail#credits-and-karma');
111
+ });
112
+ });
113
+
114
+ test('RUN: the diagram images are NOT swallowed by the redirect, and stay behind sign-in', async () => {
115
+ await withHall(async (get) => {
116
+ const img = await get('/diagrams/01-task-lifecycle.png', 'builders.127.0.0.1');
117
+ assert.equal(img.status, 302);
118
+ assert.match(img.location, /\/auth\/web\/start\?return=/, 'an anonymous image request goes to sign-in, not to the Primer');
119
+ const primer = await get('/primer', 'builders.127.0.0.1');
120
+ assert.match(primer.location, /\/auth\/web\/start\?return=/, 'and the Primer itself keeps its sign-in gate');
121
+ });
122
+ });
123
+
124
+ test('every redirect target is a real chapter of the Primer', () => {
125
+ const ids = h2Ids();
126
+ for (const chapter of Object.values(CHAPTER_OF)) assert.ok(ids.includes(chapter), `primer.md has an h2 slugged ${chapter}`);
127
+ const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'), 'utf8');
128
+ const map = src.slice(src.indexOf('const RETIRED_DOC_CHAPTER'), src.indexOf('const RETIRED_DOC_PAGE_RE'));
129
+ for (const [page, chapter] of Object.entries(CHAPTER_OF)) {
130
+ assert.match(map, new RegExp(`${page}: '${chapter}'`), `serve-internal.js sends /${page} to #${chapter}`);
131
+ }
132
+ });
133
+
134
+ // ---- 2. the chapters, each with its diagram --------------------------------
135
+
136
+ const diagramTable = () => [...primerJs.matchAll(/\{ id: '([a-z0-9-]+)',\s*src: '\/builders\/diagrams\/([\w.-]+)'/g)]
137
+ .map((m) => ({ id: m[1], file: m[2] }));
138
+
139
+ test('all five diagrams are inlined, each at a chapter that exists', () => {
140
+ const table = diagramTable();
141
+ assert.deepEqual(table.map((d) => d.file.replace(/\.png$/, '')).sort(),
142
+ fs.readdirSync(path.join(ROOT, 'docs', 'onboarding', 'diagrams')).filter((f) => f.endsWith('.mmd')).map((f) => f.replace(/\.mmd$/, '')).sort(),
143
+ 'one figure per diagram source, none left without a chapter');
144
+ const ids = h2Ids();
145
+ for (const d of table) assert.ok(ids.includes(d.id), `${d.file} is inlined at #${d.id}, which primer.md must have`);
146
+ assert.equal(new Set(table.map((d) => d.id)).size, table.length, 'and no chapter carries two');
147
+ });
148
+
149
+ test('the lifecycle, the rank ladder and credits and karma are each explained ONCE', () => {
150
+ const ids = h2Ids();
151
+ for (const chapter of ['the-claim-ship-lifecycle', 'the-rank-ladder', 'credits-and-karma']) {
152
+ assert.equal(ids.filter((id) => id === chapter).length, 1, `one #${chapter} chapter`);
153
+ }
154
+ // The ladder chapter carries what the Ranks page said: every live rank, and
155
+ // how each promotion happens.
156
+ const ladder = PRIMER_MD.slice(PRIMER_MD.indexOf('## The rank ladder'), PRIMER_MD.indexOf('## Credits and karma'));
157
+ for (const rank of ['Xenos', 'Thetes', 'Metic', 'Archon']) assert.match(ladder, new RegExp(`\\*\\*${rank}\\*\\*`));
158
+ assert.match(ladder, /### How you advance/);
159
+ assert.match(ladder, /\/builders#accomplishments-progress/, 'and it points the reader at their own standing');
160
+ });
161
+
162
+ test('the two stale primer lines are gone', () => {
163
+ assert.doesNotMatch(PRIMER_MD, /touches the game/, 'the core has no game');
164
+ assert.doesNotMatch(PRIMER_MD, /Entry to the project is \*\*approval-gated\*\*/);
165
+ assert.match(PRIMER_MD, /\*\*depends on the project\*\*/, 'entry depends on the project');
166
+ });
167
+
168
+ test('the primer keeps a hall address in the hall, and sends repo paths to GitHub', () => {
169
+ const sandbox = { window: { OTB: realOTB() }, document: { readyState: 'complete', querySelector: () => null } };
170
+ vm.createContext(sandbox);
171
+ vm.runInContext(read('md-reader.js'), sandbox, { filename: 'md-reader.js' });
172
+ // Lift the resolver out of primer.js without booting the page.
173
+ const src = primerJs.slice(primerJs.indexOf(' const resolveLinkHref = (() => {'), primerJs.indexOf(' function toast('));
174
+ const fn = vm.runInContext(`(() => { const MD = window.OTBMdReader; const PRIMER_DIR = 'docs/onboarding/'; let repoBlobBase = 'https://gh/blob/main'; let repoTreeBase = 'https://gh/tree/main';\n${src}\nreturn resolveLinkHref; })()`, sandbox);
175
+ assert.equal(fn('/builders#accomplishments-progress'), '/builders#accomplishments-progress');
176
+ assert.equal(fn('drachmae-and-karma.md'), 'https://gh/blob/main/docs/onboarding/drachmae-and-karma.md',
177
+ 'the full reference is a repo doc now that its hall page is a chapter');
178
+ assert.equal(fn('../adr/'), 'https://gh/tree/main/docs/adr');
179
+ assert.equal(fn('/buildersx/'), 'https://gh/tree/main/buildersx', 'only a real /builders address is kept');
180
+ });
181
+
182
+ // Lift insertLiveSections out of primer.js (with the two consts it reads) and run
183
+ // it against a fake article, a fake OTBPrimerLive and a controllable observer.
184
+ function liftInsertLive({ hash = '', withObserver = true }) {
185
+ const src = primerJs.slice(primerJs.indexOf(' const LIVE_HASHES'), primerJs.indexOf(' // A deep link — /primer#the-rank-ladder'));
186
+ const fills = [];
187
+ const observers = [];
188
+ const window = {
189
+ location: { hash },
190
+ OTBPrimerLive: {
191
+ placeholder: (kind) => ({ kind }),
192
+ fillBounty: async (s) => { fills.push(s.kind); return true; },
193
+ },
194
+ };
195
+ if (withObserver) {
196
+ window.IntersectionObserver = class {
197
+ constructor(cb) { this.cb = cb; this.targets = []; observers.push(this); }
198
+ observe(t) { this.targets.push(t); }
199
+ disconnect() { this.gone = true; }
200
+ };
201
+ }
202
+ const next = { tagName: 'H2' };
203
+ const h = { tagName: 'H2', nextElementSibling: { tagName: 'P', nextElementSibling: next } };
204
+ const placed = [];
205
+ const article = {
206
+ querySelector: (sel) => (sel === 'h2[id="credits-and-karma"]' ? h : null),
207
+ insertBefore: (el, ref) => placed.push([el.kind, ref === next]),
208
+ appendChild: (el) => placed.push([el.kind, false]),
209
+ };
210
+ const ctx = { window, Promise };
211
+ vm.createContext(ctx);
212
+ const fn = vm.runInContext(`(() => { const LIVE_CHAPTER_ID = 'credits-and-karma';\n${src}\nreturn insertLiveSections; })()`, ctx);
213
+ return { run: () => fn(article), fills, observers, placed };
214
+ }
215
+
216
+ test('RUN: the live reads wait until the reader comes near, so an ordinary Primer visit costs nothing', async () => {
217
+ const t = liftInsertLive({ hash: '#the-rank-ladder' });
218
+ await t.run();
219
+ assert.deepEqual(t.placed, [['bounty', true]], 'placed at the end of the credits chapter, ahead of the next one (the treasury is on /money since task 1005047)');
220
+ assert.deepEqual(t.fills, [], 'no fetch on load');
221
+ assert.equal(t.observers.length, 1);
222
+ t.observers[0].cb([{ isIntersecting: false }]);
223
+ assert.deepEqual(t.fills, [], 'nor while they are still far away');
224
+ t.observers[0].cb([{ isIntersecting: true }]);
225
+ t.observers[0].cb([{ isIntersecting: true }]);
226
+ await Promise.resolve();
227
+ assert.deepEqual(t.fills, ['bounty'], 'one read, once, when the reader approaches');
228
+ assert.equal(t.observers[0].gone, true, 'and the watch is dropped');
229
+ });
230
+
231
+ test('RUN: an old /drachmae link (or a section address) fills at once, so the landing is on the final layout', async () => {
232
+ for (const hash of ['#credits-and-karma', '#security-bounty-rates']) {
233
+ const t = liftInsertLive({ hash });
234
+ await t.run();
235
+ assert.deepEqual(t.fills, ['bounty'], `${hash} reads it before landing`);
236
+ assert.equal(t.observers.length, 0);
237
+ }
238
+ const old = liftInsertLive({ withObserver: false });
239
+ await old.run();
240
+ assert.deepEqual(old.fills, ['bounty'], 'a browser with no observer still gets the section');
241
+ });
242
+
243
+ test('a deep link lands after the article, its live sections and the fonts', () => {
244
+ const boot = primerJs.slice(primerJs.indexOf('async function boot()'));
245
+ assert.match(boot, /const live = insertLiveSections\(article\);/);
246
+ assert.match(boot, /live\.then\(\(\) => \(document\.fonts \? document\.fonts\.ready : null\)\)\.then\(landOnHash\);/);
247
+ });
248
+
249
+ test('the states file shoots a redirected deep link, not just the top of the page', () => {
250
+ const states = JSON.parse(read('primer.states.json'));
251
+ assert.match(states.states.jump.url, /#credits-and-karma$/);
252
+ });
253
+
254
+ // ---- 3. the live section, RUN ---------------------------------------------
255
+
256
+ const bountyFixture = JSON.parse(
257
+ fs.readFileSync(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures', 'public__bounty-table.json'), 'utf8'),
258
+ );
259
+
260
+ // Just enough of an element for primer-live.js: it only ever writes a section's
261
+ // innerHTML, its data-live and aria-busy, and removes it.
262
+ function fakeSection() {
263
+ return {
264
+ className: '', dataset: {}, attrs: {}, innerHTML: '', removed: false,
265
+ setAttribute(k, v) { this.attrs[k] = String(v); },
266
+ removeAttribute(k) { delete this.attrs[k]; },
267
+ remove() { this.removed = true; },
268
+ };
269
+ }
270
+
271
+ async function loadLive({ responses, branding = { currency: { label: 'drachmae' } } }) {
272
+ const warnings = [];
273
+ const sandbox = {
274
+ window: { OTB: realOTB(), __BRANDING__: branding },
275
+ document: { createElement: () => fakeSection() },
276
+ console: { warn: (...a) => warnings.push(a.join(' ')) },
277
+ fetch: async (url) => {
278
+ const r = responses[url];
279
+ if (r instanceof Error) throw r;
280
+ if (!r) return { ok: false, status: 404, json: async () => ({}) };
281
+ return { ok: true, status: 200, json: async () => r };
282
+ },
283
+ };
284
+ vm.createContext(sandbox);
285
+ vm.runInContext(liveJs, sandbox, { filename: 'primer-live.js' });
286
+ return { live: sandbox.window.OTBPrimerLive, warnings };
287
+ }
288
+
289
+ const BOUNTY = '/api/bongos/public/bounty-table';
290
+
291
+ test('RUN: a section says Loading… the moment it is placed, under the heading a deep link needs', async () => {
292
+ const { live } = await loadLive({ responses: {} });
293
+ for (const [kind, id] of [['bounty', 'security-bounty-rates']]) {
294
+ const s = live.placeholder(kind);
295
+ assert.match(s.innerHTML, new RegExp(`^<h3 id="${id}">`), `${kind} carries its address from the start`);
296
+ assert.match(s.innerHTML, /<p class="hall-loading" role="status">Loading…<\/p>/);
297
+ assert.equal(s.attrs['aria-busy'], 'true');
298
+ }
299
+ });
300
+
301
+ test('RUN: the bounty rate card renders the live table, tuned tiers marked', async () => {
302
+ const tuned = { ...bountyFixture, table: bountyFixture.table.map((r, i) => (i === 0 ? { ...r, overridden: true } : r)) };
303
+ const { live } = await loadLive({ responses: { [BOUNTY]: tuned }, branding: {} });
304
+ const s = live.placeholder('bounty');
305
+ assert.equal(await live.fillBounty(s), true);
306
+ assert.match(s.innerHTML, /^<h3 id="security-bounty-rates">Security bounty rates<\/h3>/);
307
+ assert.equal((s.innerHTML.match(/<tr><td><strong>/g) || []).length, bountyFixture.table.length, 'one row per severity');
308
+ assert.match(s.innerHTML, /<em>\(tuned\)<\/em>/);
309
+ assert.match(s.innerHTML, /<th>Credits<\/th>/, 'the neutral currency word when the pack names none');
310
+ });
311
+
312
+ test('RUN: a bounty feed that fails leaves no section', async () => {
313
+ for (const responses of [{}, { [BOUNTY]: { table: [] } }]) {
314
+ const { live } = await loadLive({ responses });
315
+ const s = live.placeholder('bounty');
316
+ assert.equal(await live.fillBounty(s), false);
317
+ assert.equal(s.removed, true);
318
+ }
319
+ });
320
+
321
+ test('the live section reads an endpoint that already existed, and the treasury is gone to /money', () => {
322
+ assert.match(liveJs, /BOUNTY_URL = '\/api\/bongos\/public\/bounty-table'/);
323
+ assert.doesNotMatch(liveJs, /cost-summary|fillTreasury/, 'task 1005047: a money figure is drawn on the money page alone');
324
+ assert.doesNotMatch(liveJs, /<(?:path|polyline|rect)\b[^>]*\b(?:d|points)="\$\{/, 'no hand-built SVG geometry');
325
+ });
326
+
327
+ test('the page loads the live module, then the page script', () => {
328
+ const loaded = [...primerHtml.matchAll(/<script[^>]+src="([^"?]+)/g)].map((m) => m[1].split('/').pop());
329
+ const at = (f) => loaded.indexOf(f);
330
+ assert.ok(at('primer-live.js') > -1 && at('primer.js') > -1);
331
+ assert.ok(at('primer-live.js') < at('primer.js'));
332
+ // task 1005047: the chart kit drew the treasury, which is on the money page now.
333
+ assert.equal(at('hall-kit.js'), -1, 'nothing on the Primer draws a chart');
334
+ // The figure chrome is shared (style.css), so the Primer needs no profile sheet.
335
+ assert.match(primerHtml, /style\.css/);
336
+ assert.doesNotMatch(primerHtml, /profile\.css/);
337
+ });
338
+
339
+ // ---- 4. the shared reader, actually run -------------------------------------
340
+
341
+ test('the shared parser reaches h4 — the reference doc\'s earning sources are ####', () => {
342
+ const MD = loadReader();
343
+ const { html, headings } = MD.parseMarkdown('#### 4. Achievements\n\nUnlocking one pays its bonus.\n', (h) => h);
344
+ assert.equal(headings.length, 1);
345
+ assert.equal(headings[0].level, 4);
346
+ assert.match(html, /<h4 id="4-achievements">4\. Achievements<\/h4>/);
347
+ assert.doesNotMatch(html, /####/);
348
+ });
349
+
350
+ test('a paragraph breaks before an h4 the way it breaks before an h3', () => {
351
+ const MD = loadReader();
352
+ const { html } = MD.parseMarkdown('a line\n#### A head\n', (h) => h);
353
+ assert.match(html, /<p>a line<\/p>\s*<h4/);
354
+ });
355
+
356
+ test('a heading that carries a link slugs by its text, so the copied address stays readable', () => {
357
+ const MD = loadReader();
358
+ const withLink = '2. Per-task credits, adjusted by *kind* ([ADR 0023](../adr/0023-kind-multipliers-and-idea-bonus.md))';
359
+ assert.equal(MD.slugify(withLink), MD.slugify('2. Per-task credits, adjusted by *kind* (ADR 0023)'));
360
+ assert.doesNotMatch(MD.slugify(withLink), /adr0023|md$/, 'the target path is not part of the address');
361
+ });
362
+
363
+ test('emphasis inside a bold run renders — the API list-price line showed its asterisks', () => {
364
+ const MD = loadReader();
365
+ const { html } = MD.parseMarkdown('- **It is the API *list* price, deliberately** and not the rest\n', (h) => h);
366
+ assert.match(html, /<strong>It is the API <em>list<\/em> price, deliberately<\/strong> and not the rest/);
367
+ const two = MD.parseMarkdown('**a** and **b**\n', (h) => h).html;
368
+ assert.match(two, /<strong>a<\/strong> and <strong>b<\/strong>/, 'and two bold runs on one line stay two');
369
+ });
370
+
371
+ test('the room gives the fourth level a rule, the measure and the anchor offset', () => {
372
+ const h4 = rule(roomCss, '.primer-article h4');
373
+ assert.ok(h4, 'room.css styles h4 (the credits reference doc uses them)');
374
+ assert.match(h4, /scroll-margin-top/, 'a deep link to an h4 clears the top bar');
375
+ const measured = decomment(roomCss).match(/\.primer-article\s*>\s*p,[\s\S]*?\{([^}]*)\}/);
376
+ assert.ok(measured, 'the measured-children list exists');
377
+ assert.match(measured[0], /\.primer-article\s*>\s*h4/, 'and the h4 is measured with the prose');
378
+ });
379
+
380
+ test('the reference rules left with the reference page', () => {
381
+ assert.doesNotMatch(decomment(roomCss), /room--reference/, 'no rule keyed on a body class no page wears');
382
+ for (const f of fs.readdirSync(HALL).filter((n) => n.endsWith('.html'))) {
383
+ assert.doesNotMatch(read(f), /room--reference/, `${f} wears no reference modifier`);
384
+ }
385
+ });
@@ -68,16 +68,13 @@ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
68
68
  const styleCss = read('style.css');
69
69
  const roomCss = read('room.css');
70
70
  const primerCss = read('primer.css');
71
- const diagramsCss = read('diagrams.css');
72
71
  const roadmapCss = read('roadmap.css');
73
72
  const viewerCss = read('diagram-viewer.css');
74
73
  const primerJs = read('primer.js');
75
74
  const viewerJs = read('diagram-viewer.js');
76
75
  const mdReaderJs = read('md-reader.js');
77
76
  const primerHtml = read('primer.html');
78
- const diagramsHtml = read('diagrams.html');
79
77
  const roadmapHtml = read('roadmap.html');
80
- const ranksHtml = read('ranks.html');
81
78
 
82
79
  const decomment = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
83
80
  // The body of the first rule matching `selector {`, comments stripped. Naive on
@@ -94,7 +91,6 @@ const rule = (css, selector) => {
94
91
  const RESKINNED = [
95
92
  ['room.css', roomCss],
96
93
  ['primer.css', primerCss],
97
- ['diagrams.css', diagramsCss],
98
94
  ['roadmap.css', roadmapCss],
99
95
  ['diagram-viewer.css', viewerCss],
100
96
  ];
@@ -143,14 +139,18 @@ test('no re-skinned reading-room sheet spends a compat alias', () => {
143
139
  'the aliases exist for pages nobody has re-skinned; spending one here quietly opts the sheet back out of the world');
144
140
  });
145
141
 
146
- test('the ranks page section of style.css spends the real names too', () => {
147
- const card = rule(styleCss, '.rank-card');
148
- assert.ok(card, '.rank-card has a rule');
149
- for (const alias of ALIASES) {
150
- assert.doesNotMatch(card, new RegExp(`var\\(\\s*${alias}\\s*[,)]`), `.rank-card no longer spends ${alias}`);
142
+ // task 1005046: the Ranks page folded into the Primer; its ladder cards went with
143
+ // it and "Where you stand" (Accomplishments) kept the standing rules, which hold
144
+ // the same line.
145
+ test('the "Where you stand" section of style.css spends the real names too', () => {
146
+ for (const sel of ['.ranks-standing', '.ranks-standing__blurb', '.ranks-standing__next', '.ranks-rise__body']) {
147
+ const body = rule(styleCss, sel);
148
+ assert.ok(body, `${sel} has a rule`);
149
+ for (const alias of ALIASES) {
150
+ assert.doesNotMatch(body, new RegExp(`var\\(\\s*${alias}\\s*[,)]`), `${sel} spends no ${alias}`);
151
+ }
151
152
  }
152
- assert.match(card, /border:\s*var\(--line\)\s+solid\s+transparent/,
153
- 'the edge is reserved as transparent so a modifier can draw one — collab.css\'s .btn--quiet is the precedent');
153
+ assert.equal(rule(styleCss, '.rank-card'), null, 'the ladder cards left with the page');
154
154
  });
155
155
 
156
156
  test('the alias guard would CATCH --surface without matching --surface-2', () => {
@@ -223,8 +223,11 @@ test('primer.html loads the reader BEFORE the page script', () => {
223
223
  });
224
224
 
225
225
  test('the primer keeps the one rewrite that is its own', () => {
226
- assert.match(primerJs, /drachmae-and-karma\.md.*\/builders\/drachmae/s,
227
- 'the economy doc has an in-hall reader page (task 1472); a link to it must stay in the hall, not bounce to GitHub');
226
+ // task 1005046: the economy doc's in-hall page became a Primer chapter, so the
227
+ // rewrite that kept a link to it in the hall is gone; what is the primer's own
228
+ // now is a /builders address (run in tests/hall_reading_room.mjs).
229
+ assert.doesNotMatch(primerJs, /'\/builders\/drachmae'/, 'no link is sent to the retired Economy page');
230
+ assert.match(primerJs, /\/\^\\\/builders/, 'a hall address stays in the hall');
228
231
  assert.match(primerJs, /baseDir:\s*PRIMER_DIR/, 'and resolves its relative links against docs/onboarding/');
229
232
  });
230
233
 
@@ -237,7 +240,7 @@ test('the room styles what the shared parser emits that the old copy never did',
237
240
 
238
241
  test('primer.js still injects the diagrams — the one parser-adjacent job that is the primer\'s', () => {
239
242
  assert.match(primerJs, /function inlineDiagrams/);
240
- assert.match(primerJs, /DIAGRAM_AT_HEADING/);
243
+ assert.match(primerJs, /DIAGRAM_AT_CHAPTER/, 'keyed by chapter id (tests/hall_reading_room.mjs checks each id exists)');
241
244
  assert.match(primerJs, /fig\.className = 'primer-figure'/,
242
245
  'the injected figure takes the class diagram-viewer.js upgrades and room.css frames');
243
246
  });
@@ -285,17 +288,19 @@ test('the fullscreen scroll lock is on the root, where the shell already locks',
285
288
  assert.match(decomment(styleCss), /html\.drawer-open/, 'and that is the convention it is matching');
286
289
  });
287
290
 
288
- test('the room frames BOTH figures the viewer upgrades', () => {
289
- assert.match(viewerJs, /figure\.diagram img, figure\.primer-figure img/,
290
- 'one selector, two classes — the viewer has always treated them as one object');
291
- const frame = decomment(roomCss).match(/\.primer-figure,\s*figure\.diagram\s*\{([^}]*)\}/);
292
- assert.ok(frame, 'and the room frames them in one rule, so they cannot drift apart');
293
- assert.match(frame[1], /var\(--glass\)/);
291
+ test('the room frames the figure the viewer upgrades', () => {
292
+ // task 1005046: the diagrams index's `figure.diagram` was the second class; with
293
+ // that page folded into the primer, the primer's figure is the only one.
294
+ assert.match(viewerJs, /SELECTOR = 'figure\.primer-figure img'/, 'the viewer upgrades the primer\'s figures');
295
+ assert.doesNotMatch(decomment(roomCss), /figure\.diagram/, 'and the room keeps no rule for a class no page draws');
296
+ const frame = rule(roomCss, '.primer-figure');
297
+ assert.ok(frame, 'the room frames the figure');
298
+ assert.match(frame, /var\(--glass\)/);
294
299
  // the canvas is inserted INSIDE the figure, so exactly one of the two draws
295
300
  // the frame or the diagram lands in nested boxes
296
- const inner = decomment(roomCss).match(/\.primer-figure \.dgv,\s*figure\.diagram \.dgv\s*\{([^}]*)\}/);
301
+ const inner = rule(roomCss, '.primer-figure .dgv');
297
302
  assert.ok(inner, 'the inserted canvas gives up its own frame');
298
- assert.match(inner[1], /border:\s*0/);
303
+ assert.match(inner, /border:\s*0/);
299
304
  });
300
305
 
301
306
  // ---- 7b. the figure follows the theme (task 1003639) ----------------------
@@ -326,8 +331,8 @@ test('dark turns the canvas figure, in both twins, byte-identical', () => {
326
331
  assert.doesNotMatch(rule(viewerCss, '.dgv__pan svg'), /filter/, 'light keeps the render exactly as mermaid drew it');
327
332
  });
328
333
 
329
- test('the fallback PNG turns with it, on BOTH figures the viewer upgrades', () => {
330
- const both = (pre) => `${pre} .primer-figure img, ${pre} figure.diagram img`;
334
+ test('the fallback PNG turns with it, on the figure the viewer upgrades', () => {
335
+ const both = (pre) => `${pre} .primer-figure img`;
331
336
  const sw = rule(roomCss, both('html[data-theme="dark"]'));
332
337
  const media = mediaRule(roomCss, both('html:not([data-theme])'));
333
338
  assert.ok(sw && media, 'the PNG shows before the SVG arrives and whenever it cannot — a dark reader must not get a white flash');
@@ -344,7 +349,8 @@ test('the viewer drops the white ground mermaid bakes into the SVG', () => {
344
349
 
345
350
  // ---- 8. every room has exactly one h1 -------------------------------------
346
351
 
347
- const ROOMS = [['primer.html', primerHtml], ['diagrams.html', diagramsHtml], ['roadmap.html', roadmapHtml], ['ranks.html', ranksHtml]];
352
+ // task 1005046: diagrams.html and ranks.html folded into the primer's chapters.
353
+ const ROOMS = [['primer.html', primerHtml], ['roadmap.html', roadmapHtml]];
348
354
 
349
355
  // An HTML comment that mentions <h1> is prose, not markup — and these pages each
350
356
  // carry one explaining why the heading is there at all.
@@ -358,8 +364,8 @@ test('every reading room renders exactly one h1, and it is the page head\'s', ()
358
364
  }
359
365
  });
360
366
 
361
- test('the three DOCS rooms take the room voice; the deck takes the board\'s', () => {
362
- for (const [name, html] of [['primer.html', primerHtml], ['diagrams.html', diagramsHtml], ['ranks.html', ranksHtml]]) {
367
+ test('the DOCS room takes the room voice; the deck takes the board\'s', () => {
368
+ for (const [name, html] of [['primer.html', primerHtml]]) {
363
369
  assert.match(html, /class="page-head page-head--room"/, `${name} is a reading room`);
364
370
  }
365
371
  assert.match(roadmapHtml, /class="page-head page-head--board"/,
@@ -375,7 +381,7 @@ test('the document\'s own h1 is suppressed, or the article would render a second
375
381
  // ---- 9. the archetype's shape ---------------------------------------------
376
382
 
377
383
  test('the room opts in from its markup, and the archetype sheet is what carries it', () => {
378
- for (const [name, html] of [['primer.html', primerHtml], ['diagrams.html', diagramsHtml]]) {
384
+ for (const [name, html] of [['primer.html', primerHtml]]) {
379
385
  assert.match(html, /<body class="room[^"]*"/, `${name} opts into the room`);
380
386
  assert.match(html, /href="\/builders\/room\.css"/, `${name} loads the archetype`);
381
387
  }
@@ -724,13 +730,13 @@ test('the timeline module is a pure builder, so it is testable and cannot boot i
724
730
  // reader sees, and a script writing into one is a message nobody gets.
725
731
  test('every Docs page head holds only its h1, and no script writes into it', () => {
726
732
  const pub = path.join(ROOT, 'modules/hall-ui/public');
727
- for (const page of ['primer', 'ranks', 'drachmae', 'commands', 'diagrams', 'atlas', 'modules']) {
733
+ for (const page of ['primer', 'commands', 'atlas', 'modules']) {
728
734
  const html = dehtmlcomment(fs.readFileSync(path.join(pub, `${page}.html`), 'utf8'));
729
735
  const head = html.match(/<div class="page-head[^"]*">([\s\S]*?)<\/div>/);
730
736
  assert.ok(head, `${page}.html keeps its head for the h1`);
731
737
  assert.doesNotMatch(head[1], /page-head__eyebrow|page-head__sub/, `${page}.html's hidden head holds no eyebrow or sub-line`);
732
738
  }
733
- for (const [file, id] of [['ranks.js', 'ranks-sub'], ['modules.js', 'modules-sub']]) {
739
+ for (const [file, id] of [['modules.js', 'modules-sub']]) {
734
740
  assert.ok(!fs.readFileSync(path.join(pub, file), 'utf8').includes(id), `${file} no longer writes into #${id}`);
735
741
  }
736
742
  });
@@ -94,8 +94,9 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
94
94
  // "Upgraded to core X" notice after a move; task 1004826: Standing is Accomplishments;
95
95
  // task 1004846: the new logo (its bump landed late, in task 1004852); task 1004938: the
96
96
  // Market Watch settings nav item and its fuel glyph; task 1005041: the Update pill replaced the
97
- // core-update banner; task 1005045: the Market Watch settings item left (one sidebar item).
98
- const SHELL_SHA = '4a4b9795cdea422c';
97
+ // core-update banner; task 1005045: the Market Watch settings item left (one sidebar item);
98
+ // task 1005046: Ranks, Economy and Diagrams left Docs (one reading room, the Primer).
99
+ const SHELL_SHA = '662e036a340ad33f';
99
100
 
100
101
  test('shell.js has not changed without its cache-bust being bumped', () => {
101
102
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
@@ -92,7 +92,6 @@ const EXACT_LADDER_RE = /\[\s*'xenos',\s*'thetes',\s*'metic',\s*'archon'\s*\]/;
92
92
  const STATIC_LADDER_FILES = [
93
93
  'modules/discord/discord-channels.js',
94
94
  'modules/memory/search.js',
95
- 'modules/hall-ui/public/ranks.js',
96
95
  'modules/hall-ui/public/profile.js',
97
96
  'modules/hall-ui/public/watch.js',
98
97
  // task 1003094: the Constitution panel and the Board Room's sitting card both
@@ -34,7 +34,7 @@ test('the platform floors are a pinned list — a floor cannot vanish quietly',
34
34
  });
35
35
 
36
36
  test('the shipped states files validate and sit beside the page they name', () => {
37
- for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/projects-empty.states.json', 'modules/public-landing/public/projects-dns-pending.states.json', 'modules/public-landing/public/projects-hall-live.states.json','modules/public-landing/public/index.states.json', 'modules/public-landing/public/index-cli.states.json', 'modules/public-landing/public/account.states.json', 'modules/public-landing/public/account-private.states.json', 'modules/public-landing/public/guild.states.json', 'modules/public-landing/public/invitations.states.json', 'modules/public-landing/public/invitations-empty.states.json', 'modules/public-landing/public/invitations-picker.states.json', 'modules/hall-ui/public/ranks.states.json']) {
37
+ for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/projects-empty.states.json', 'modules/public-landing/public/projects-dns-pending.states.json', 'modules/public-landing/public/projects-hall-live.states.json','modules/public-landing/public/index.states.json', 'modules/public-landing/public/index-cli.states.json', 'modules/public-landing/public/account.states.json', 'modules/public-landing/public/account-private.states.json', 'modules/public-landing/public/guild.states.json', 'modules/public-landing/public/invitations.states.json', 'modules/public-landing/public/invitations-empty.states.json', 'modules/public-landing/public/invitations-picker.states.json', 'modules/hall-ui/public/primer.states.json']) {
38
38
  const json = readJson(rel);
39
39
  const v = lib.validateStatesFile(json);
40
40
  assert.deepEqual(v.errors, [], `${rel}: ${v.errors.join('; ')}`);
@@ -1,45 +0,0 @@
1
- /* diagrams.css — what the DIAGRAMS index has that the other reading rooms do
2
- not (task 1003306, goal 1000081).
3
-
4
- The room — the framed figure, the reading measure, the prose rhythm — is the
5
- shared archetype in room.css, which this page loads first. What is left here
6
- is the legend strip: the three-state key (live / planned / reserved) that
7
- tells a reader how to read the five generated diagrams. It is genuinely this
8
- page's alone, so it is this page's sheet.
9
-
10
- Promoted out of a 16-line inline <style> block, which sat one line over the
11
- hall's default budget with its own tests/hall_audit.mjs exception. The
12
- exception retires with it, and the hall now carries no substantive inline
13
- style at all.
14
-
15
- The diagrams themselves are GENERATED (scripts/bongos/gen-diagrams.js writes the
16
- .svg/.png pair and assertions.json in lockstep) — never hand-edit a .mmd. */
17
-
18
- /* The key sits above the figures on the ground of its own panel, wrapping to a
19
- column on a phone rather than scrolling sideways. */
20
- .legend {
21
- display: flex;
22
- flex-wrap: wrap;
23
- gap: var(--sp-xs) var(--sp-md);
24
- margin: 0;
25
- }
26
- .legend__item {
27
- display: flex;
28
- align-items: center;
29
- gap: 8px;
30
- min-height: 24px;
31
- font-size: 13.5px;
32
- color: var(--ink-soft);
33
- }
34
- /* The three states are the hall's fixed status vocabulary, not accent-derived:
35
- a diagram's "planned" must not be the same salmon as a claim button. */
36
- .legend__swatch {
37
- font-family: var(--label-face);
38
- font-size: var(--label-size);
39
- letter-spacing: var(--label-space);
40
- font-weight: 400;
41
- white-space: nowrap;
42
- }
43
- .legend__swatch--live { color: var(--ok); }
44
- .legend__swatch--planned { color: var(--warn); }
45
- .legend__swatch--reserved { color: var(--ink-faint); }