@bongos/core 1.24.36 → 1.24.38

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 (73) hide show
  1. package/.bongos-core.json +92 -87
  2. package/docs/copy-inventory.md +65 -64
  3. package/docs/copy-registry.json +170 -173
  4. package/docs/module-api-changelog.md +4 -0
  5. package/docs/onboarding/primer.md +2 -0
  6. package/docs/page-inventory.json +34 -34
  7. package/docs/page-readings.json +687 -684
  8. package/modules/hall-ui/public/ai-studio.html +2 -2
  9. package/modules/hall-ui/public/atlas.html +2 -2
  10. package/modules/hall-ui/public/blockers.html +2 -2
  11. package/modules/hall-ui/public/board-room.html +2 -2
  12. package/modules/hall-ui/public/city.html +2 -2
  13. package/modules/hall-ui/public/collab.html +2 -2
  14. package/modules/hall-ui/public/commands.html +2 -2
  15. package/modules/hall-ui/public/copy-desk.html +2 -2
  16. package/modules/hall-ui/public/craft-mode.js +6 -4
  17. package/modules/hall-ui/public/deploy.html +2 -2
  18. package/modules/hall-ui/public/fleet-costs.js +3 -2
  19. package/modules/hall-ui/public/gate.html +2 -2
  20. package/modules/hall-ui/public/goal-map.html +2 -2
  21. package/modules/hall-ui/public/goals.html +2 -2
  22. package/modules/hall-ui/public/government.html +2 -2
  23. package/modules/hall-ui/public/idea.html +2 -2
  24. package/modules/hall-ui/public/ideas.html +2 -2
  25. package/modules/hall-ui/public/index.html +2 -2
  26. package/modules/hall-ui/public/market-watch-item.html +2 -2
  27. package/modules/hall-ui/public/market-watch-rules.html +2 -2
  28. package/modules/hall-ui/public/market-watch.html +2 -2
  29. package/modules/hall-ui/public/modules.html +2 -2
  30. package/modules/hall-ui/public/money.html +92 -0
  31. package/modules/hall-ui/public/money.js +302 -0
  32. package/modules/hall-ui/public/money.states.json +15 -0
  33. package/modules/hall-ui/public/my-agents.html +2 -2
  34. package/modules/hall-ui/public/palette.js +9 -2
  35. package/modules/hall-ui/public/primer-live.js +18 -105
  36. package/modules/hall-ui/public/primer.html +7 -8
  37. package/modules/hall-ui/public/primer.js +16 -18
  38. package/modules/hall-ui/public/profile.html +2 -2
  39. package/modules/hall-ui/public/project-settings.css +2 -1
  40. package/modules/hall-ui/public/project-settings.html +6 -16
  41. package/modules/hall-ui/public/project-settings.js +2 -142
  42. package/modules/hall-ui/public/roadmap.html +2 -2
  43. package/modules/hall-ui/public/roster.html +2 -2
  44. package/modules/hall-ui/public/sessions.html +2 -2
  45. package/modules/hall-ui/public/settings.html +2 -2
  46. package/modules/hall-ui/public/shell.js +113 -94
  47. package/modules/hall-ui/public/street.html +2 -2
  48. package/modules/hall-ui/public/studio.html +2 -2
  49. package/modules/hall-ui/public/task.html +2 -2
  50. package/modules/hall-ui/public/thinking.html +2 -2
  51. package/modules/hall-ui/public/tweak-editor.html +2 -2
  52. package/modules/hall-ui/public/watch.html +2 -2
  53. package/modules/hall-ui/public/work.html +2 -2
  54. package/package-lock.json +2 -2
  55. package/package.json +1 -1
  56. package/release-notes.json +12 -0
  57. package/src/bongos/auth.js +0 -8
  58. package/src/bongos/serve-internal.js +15 -6
  59. package/src/module-api.js +1 -1
  60. package/tests/auth_resolve_failure.mjs +5 -4
  61. package/tests/craft_mode_nav.mjs +12 -10
  62. package/tests/fleet_cost_view.mjs +2 -18
  63. package/tests/goal_map_page.mjs +5 -2
  64. package/tests/hall_loading.mjs +1 -1
  65. package/tests/hall_money_page.mjs +272 -0
  66. package/tests/hall_nav.mjs +23 -15
  67. package/tests/hall_oversight_world.mjs +1 -1
  68. package/tests/hall_page_gate_map.mjs +3 -2
  69. package/tests/hall_reading_room.mjs +21 -62
  70. package/tests/nav_permission_atoms.mjs +1 -1
  71. package/modules/hall-ui/public/fleet.html +0 -69
  72. package/modules/hall-ui/public/fleet.js +0 -41
  73. package/modules/hall-ui/public/fleet.states.json +0 -14
@@ -243,7 +243,10 @@ test('sidebar groups read by audience, in order', () => {
243
243
  // 'Archon' → 'Governance' in ADR 0157 (the group's pages opened to Metic, so a header
244
244
  // naming the top rank was wrong), then → 'Government' by ADR 0174: a header
245
245
  // reading "Archon" over items a Metic can open would misdescribe the ladder.
246
- assert.deepEqual(nav.map((g) => g.label), ['', 'You', 'Community', 'Docs', 'Government']);
246
+ // task 1005071 (owner decision 2026-11-04, from the sitemap audit): every group is
247
+ // labelled, and the old unlabeled project group split into Work, Make and Govern.
248
+ // Docs became Help; Government became Run the project.
249
+ assert.deepEqual(nav.map((g) => g.label), ['Work', 'Make', 'Community', 'You', 'Govern', 'Run the project', 'Help']);
247
250
  // task 1003113 (R02 of goal 1000074): 'copy-desk' joins the unlabeled group,
248
251
  // after Tasks. It belongs with the project's own work and not in Docs — it is a
249
252
  // QUEUE somebody acts on, not reference material — and not in You or Community,
@@ -285,12 +288,15 @@ test('sidebar groups read by audience, in order', () => {
285
288
  // task 1004939 (goal 1000137): 'market-watch' joins last — the project's scouts on its
286
289
  // market, so its subject is the project. Module-gated: it is dropped wherever the
287
290
  // default-off market-watch module is not enabled (this boot reports no module state).
288
- assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'goal-map', 'work', 'blockers', 'board-room', 'city', 'thinking', 'studio', 'market-watch'],
289
- 'the unlabeled group is the project itself (task 1002425 order)');
291
+ // task 1005071: the project group split three ways. Work is the project's state;
292
+ // Make is where things get made; Govern is the board's rooms.
293
+ assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'work', 'goals', 'goal-map', 'roadmap', 'blockers', 'thinking'],
294
+ 'Work = the project itself (task 1005071 order)');
295
+ assert.deepEqual(nav[1].items.map((i) => i.id), ['studio', 'market-watch'], 'Make = where things get made');
290
296
  // task 1004685 (the living-city prototype, round-3 board 6): 'my-agents' joins You —
291
297
  // how your own Claude works for you, the personal half of "the city steers
292
298
  // everyone, My agents steers yours". Its subject is the viewer's own agents.
293
- assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile', 'my-agents'], 'You = your own numbers and your own agents');
299
+ assert.deepEqual(nav[3].items.map((i) => i.id), ['standing', 'profile', 'my-agents'], 'You = your own numbers and your own agents');
294
300
  // task 1003444: 'people' LEFT the group — the owner deleted the hall's
295
301
  // cross-project directory (ADR 0337 D5); its home is the hub, and /people
296
302
  // redirects to /roster. Builders is the hall's directory now.
@@ -305,29 +311,31 @@ test('sidebar groups read by audience, in order', () => {
305
311
  // 'leaderboard') and is now a real page at /roster labelled Builders. That is
306
312
  // what made this group read as one real page plus a link into the home page.
307
313
  assert.deepEqual(nav[2].items.map((i) => i.id), ['collab', 'roster'], 'Community = everyone else, in this project');
314
+ assert.deepEqual(nav[4].items.map((i) => i.id), ['city', 'board-room'], 'Govern = the board\'s rooms, the city first');
308
315
  assert.ok(!nav.flatMap((g) => g.items).some((i) => /^\/people\b/.test(i.href)), 'no nav item points at the deleted /people');
309
316
  // task 1004473: 'commands' — every skill and CLI verb, read live — is reference
310
317
  // material that reads the same for every builder, so it joins Docs.
311
318
  // task 1005046 (owner decision 2026-11-03, idea 1001311): ONE reading room. Ranks,
312
319
  // Economy and Diagrams explained what the Primer explains; they are its chapters
313
320
  // now, so Docs is the Primer and the four references that are not a document.
314
- assert.deepEqual(nav[3].items.map((i) => i.id),
321
+ assert.deepEqual(nav[6].items.map((i) => i.id),
315
322
  ['primer', 'commands', 'atlas', 'modules', 'apidocs'], 'Docs = reference material');
316
323
  // 'government' (the Permissions tab, BV1.R113) is the group's one archon-gated item.
317
324
  // 'project-settings' joins the Government group at the metic floor (task 1003280,
318
325
  // goal 1000072) — it sits before 'government', which is the one archon-only item.
319
326
  // 'board-room' LED this group from the ADR 0266 carve until task 1003744 moved
320
- // it to the project group — see the note on nav[0]. What is left is rank surface
327
+ // it to the project group — see the note on Govern (nav[4]). What is left is rank surface
321
328
  // throughout, which is what lets the group keep its metic head honestly.
322
329
  // 'deploy' (task 1003159, ADR 0293) is the SECOND archon item, beside 'government':
323
330
  // its gate is the `core.pin.move` atom, archon-floor and system:true. It sits with
324
331
  // the steering surfaces because that is what it is — the owner's third release gate.
325
- // 'fleet' (task 1001949) follows it at the metic floor of `provisioning.fleet.manage`:
326
- // what each hosted project costs is an operator surface, like the rest of this group.
332
+ // 'money' (task 1005047, owner decision 2026-11-03) follows 'project-settings' at the
333
+ // same government floor: the one money page — spend and payouts, the treasury and the
334
+ // hosting bills. It replaced 'fleet' (task 1001949), now its #hosting section.
327
335
  // 'market-watch-settings' (task 1004938) LEFT the group in task 1005045: the owner saw
328
336
  // "Market Watch" twice and had to guess which was which (owner decision 2026-11-03,
329
337
  // idea 1001310). The settings are an owner-only Settings tab on /market-watch now.
330
- assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', 'gate', 'sessions', 'project-settings', 'deploy', 'fleet', 'government']);
338
+ assert.deepEqual(nav[5].items.map((i) => i.id), ['watch', 'gate', 'sessions', 'project-settings', 'money', 'deploy', 'government']);
331
339
  // task 1005045: and no label is left twice anywhere in the sidebar, so the next pair
332
340
  // fails here instead of in front of the owner.
333
341
  const labels = [...boot().sidebarHtml.matchAll(/<a class="nav-item" [^>]*>[\s\S]*?<span class="nav-item__label">([^<]*)<\/span>/g)].map((m) => m[1]);
@@ -354,7 +362,7 @@ test('Ranks, Economy and Diagrams are Primer chapters, not pages', () => {
354
362
  for (const f of ['ranks.html', 'drachmae.html', 'diagrams.html']) {
355
363
  assert.ok(!fs.existsSync(path.join(HALL, f)), `${f} left the hall`);
356
364
  }
357
- const primer = nav.find((g) => g.label === 'Docs').items.find((i) => i.id === 'primer');
365
+ const primer = nav.find((g) => g.label === 'Help').items.find((i) => i.id === 'primer');
358
366
  assert.equal(primer.href, '/primer', 'the Primer is the reading room the three became');
359
367
  });
360
368
 
@@ -563,11 +571,11 @@ test('collapsible groups carry a stable storage key (collapse persistence)', ()
563
571
  // hall-nav-collapsed storage key, so any builder who had collapsed the old "archon"
564
572
  // group gets the new one expanded once — cosmetic, and preferable to keeping a stale
565
573
  // slug that no longer matches the visible label.
566
- for (const g of ['you', 'community', 'docs', 'government']) {
574
+ // task 1005071: a label with spaces hyphenates, so the key stays a valid id.
575
+ for (const g of ['work', 'make', 'community', 'you', 'govern', 'run-the-project', 'help']) {
567
576
  assert.match(html, new RegExp(`data-group="${g}"`), `${g} is collapsible (hall-nav-collapsed key)`);
568
577
  }
569
- assert.ok(!/nav-group--collapsible[^>]*>(?!<button)/.test(html.split('<div class="nav-group')[1] || ''),
570
- 'the unlabeled project group stays always-expanded');
578
+ assert.ok(!/aria-controls="[^"]* /.test(html), 'no aria-controls id carries a space');
571
579
  });
572
580
 
573
581
  // ── a nav item can carry a COUNT badge (task 1003737) ───────────────────────
@@ -779,7 +787,7 @@ test('no glyph path data reads as an IP address to the release leak scan', () =>
779
787
  // block (task 1004713). These drive the real shell and the real gov-positions.js.
780
788
 
781
789
  const tick = () => new Promise((resolve) => setImmediate(resolve));
782
- const GOVERNING = ['board-room', 'city', 'watch', 'gate', 'sessions', 'project-settings', 'deploy', 'fleet', 'government'];
790
+ const GOVERNING = ['city', 'board-room', 'watch', 'gate', 'sessions', 'project-settings', 'money', 'deploy', 'government'];
783
791
  const lead = (building, title) => ({ kind: 'lead', seat: 'lead:' + building, building, title });
784
792
  const block = (held) => ({ held, title: held.length ? held[0].title : null, has_position: held.length > 0 });
785
793
 
@@ -798,7 +806,7 @@ test('POSITION: exactly the governing surfaces carry the flag — the City, the
798
806
  const html = boot().sidebarHtml;
799
807
  const flagged = [...html.matchAll(/id="nav-([^"]+)"[^>]*data-position="1"/g)].map((m) => m[1]);
800
808
  assert.deepEqual(flagged, GOVERNING);
801
- assert.match(html, /class="nav-group__head" data-gate="government" data-position="1" data-group="government"/,
809
+ assert.match(html, /class="nav-group__head" data-gate="government" data-position="1" data-group="run-the-project"/,
802
810
  'the Government head carries it too, so no label is left over an emptied group');
803
811
  const nav = parseNav(html);
804
812
  assert.ok(!nav.find((g) => g.label === 'You').items.some((i) => flagged.includes(i.id)), 'your own pages never need a seat');
@@ -278,7 +278,7 @@ test('RUN: a metric its snapshots never carried is skipped, not drawn as a cliff
278
278
 
279
279
  // ---- task 1004829: the government pages read like the rebuilt Home ----------
280
280
 
281
- const GOV_PAGES = ['watch', 'gate', 'sessions', 'project-settings', 'deploy', 'fleet', 'government'];
281
+ const GOV_PAGES = ['watch', 'gate', 'sessions', 'project-settings', 'money', 'deploy', 'government'];
282
282
 
283
283
  test('every government page head holds only its h1, and no script writes into it', () => {
284
284
  // No page shows its head (task 1004836). A sub-line there is a line nobody
@@ -126,6 +126,9 @@ const ORACLE = {
126
126
  // its neighbours: a steering surface, metic+ per ADR 0157. Its two WRITES gate
127
127
  // independently on `project.settings.manage`, so this row is only about the shell.
128
128
  '/builders/project-settings': 'requireGovernmentPage',
129
+ // The one money page (task 1005047): the same floor as project-settings, whose money
130
+ // section it took. Its hosting section's data keeps its own atom on the route.
131
+ '/builders/money': 'requireGovernmentPage',
129
132
  // The deploy door (task 1003159, ADR 0293) — the NARROWEST gate in this table, on
130
133
  // the `core.pin.move` atom: archon-floor and system:true, the same key its preview
131
134
  // and its move both check. Correct for the one page whose button restarts the live
@@ -133,8 +136,6 @@ const ORACLE = {
133
136
  // its write on purpose: the preview discloses the exact core version the box serves
134
137
  // and how far behind it is, which is a vulnerability window.
135
138
  '/builders/deploy': 'requireCorePinPage',
136
- // The fleet cost page (task 1001949), on the atom its one read checks.
137
- '/builders/fleet': 'requireFleetPage',
138
139
 
139
140
  // Gone (task 1003444, owner call 2026-09-25 — ADR 0337 D5): the cross-project
140
141
  // directory left the hall, and /people now redirects to /roster before this
@@ -15,10 +15,11 @@
15
15
  // real h2/h3 slug of docs/onboarding/primer.md, computed by the SAME slugify
16
16
  // the page runs — so retitling a chapter fails here, not silently in a
17
17
  // browser (the credits diagram was lost that way once).
18
- // * THE ECONOMY PAGE'S LIVE SECTIONS MOVED WHOLE. The treasury and the bounty
19
- // rate card (primer-live.js, from drachmae.js, tasks 1003446 and 2174) are
20
- // RUN in a vm against the checked-in fixtures: a Loading… line first, the
21
- // real numbers after, and NOTHING — not an empty panel — when a feed fails.
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.)
22
23
  // * THE SHARED READER the economy doc leaned on (h4, linked headings, nested
23
24
  // emphasis) stays pinned: drachmae-and-karma.md is still the full reference.
24
25
  //
@@ -188,7 +189,6 @@ function liftInsertLive({ hash = '', withObserver = true }) {
188
189
  location: { hash },
189
190
  OTBPrimerLive: {
190
191
  placeholder: (kind) => ({ kind }),
191
- fillTreasury: async (s) => { fills.push(s.kind); return true; },
192
192
  fillBounty: async (s) => { fills.push(s.kind); return true; },
193
193
  },
194
194
  };
@@ -216,7 +216,7 @@ function liftInsertLive({ hash = '', withObserver = true }) {
216
216
  test('RUN: the live reads wait until the reader comes near, so an ordinary Primer visit costs nothing', async () => {
217
217
  const t = liftInsertLive({ hash: '#the-rank-ladder' });
218
218
  await t.run();
219
- assert.deepEqual(t.placed, [['treasury', true], ['bounty', true]], 'both placed at the end of the credits chapter, ahead of the next one');
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
220
  assert.deepEqual(t.fills, [], 'no fetch on load');
221
221
  assert.equal(t.observers.length, 1);
222
222
  t.observers[0].cb([{ isIntersecting: false }]);
@@ -224,20 +224,20 @@ test('RUN: the live reads wait until the reader comes near, so an ordinary Prime
224
224
  t.observers[0].cb([{ isIntersecting: true }]);
225
225
  t.observers[0].cb([{ isIntersecting: true }]);
226
226
  await Promise.resolve();
227
- assert.deepEqual(t.fills, ['treasury', 'bounty'], 'one read each, once, when the reader approaches');
227
+ assert.deepEqual(t.fills, ['bounty'], 'one read, once, when the reader approaches');
228
228
  assert.equal(t.observers[0].gone, true, 'and the watch is dropped');
229
229
  });
230
230
 
231
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', '#the-treasury', '#security-bounty-rates']) {
232
+ for (const hash of ['#credits-and-karma', '#security-bounty-rates']) {
233
233
  const t = liftInsertLive({ hash });
234
234
  await t.run();
235
- assert.deepEqual(t.fills, ['treasury', 'bounty'], `${hash} reads both before landing`);
235
+ assert.deepEqual(t.fills, ['bounty'], `${hash} reads it before landing`);
236
236
  assert.equal(t.observers.length, 0);
237
237
  }
238
238
  const old = liftInsertLive({ withObserver: false });
239
239
  await old.run();
240
- assert.deepEqual(old.fills, ['treasury', 'bounty'], 'a browser with no observer still gets the sections');
240
+ assert.deepEqual(old.fills, ['bounty'], 'a browser with no observer still gets the section');
241
241
  });
242
242
 
243
243
  test('a deep link lands after the article, its live sections and the fonts', () => {
@@ -251,11 +251,8 @@ test('the states file shoots a redirected deep link, not just the top of the pag
251
251
  assert.match(states.states.jump.url, /#credits-and-karma$/);
252
252
  });
253
253
 
254
- // ---- 3. the live sections, RUN ---------------------------------------------
254
+ // ---- 3. the live section, RUN ---------------------------------------------
255
255
 
256
- const costSummaryFixture = JSON.parse(
257
- fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', 'cost-summary.json'), 'utf8'),
258
- );
259
256
  const bountyFixture = JSON.parse(
260
257
  fs.readFileSync(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures', 'public__bounty-table.json'), 'utf8'),
261
258
  );
@@ -271,7 +268,7 @@ function fakeSection() {
271
268
  };
272
269
  }
273
270
 
274
- async function loadLive({ responses, branding = { currency: { label: 'drachmae' } }, kit = true }) {
271
+ async function loadLive({ responses, branding = { currency: { label: 'drachmae' } } }) {
275
272
  const warnings = [];
276
273
  const sandbox = {
277
274
  window: { OTB: realOTB(), __BRANDING__: branding },
@@ -284,21 +281,16 @@ async function loadLive({ responses, branding = { currency: { label: 'drachmae'
284
281
  return { ok: true, status: 200, json: async () => r };
285
282
  },
286
283
  };
287
- if (kit) {
288
- const kitPath = path.join(HALL, 'hall-kit.js');
289
- sandbox.window.OTBKit = await import(`file://${kitPath.replace(/\\/g, '/')}`).then((m) => m.default);
290
- }
291
284
  vm.createContext(sandbox);
292
285
  vm.runInContext(liveJs, sandbox, { filename: 'primer-live.js' });
293
286
  return { live: sandbox.window.OTBPrimerLive, warnings };
294
287
  }
295
288
 
296
- const TREASURY = '/api/bongos/public/cost-summary';
297
289
  const BOUNTY = '/api/bongos/public/bounty-table';
298
290
 
299
291
  test('RUN: a section says Loading… the moment it is placed, under the heading a deep link needs', async () => {
300
292
  const { live } = await loadLive({ responses: {} });
301
- for (const [kind, id] of [['treasury', 'the-treasury'], ['bounty', 'security-bounty-rates']]) {
293
+ for (const [kind, id] of [['bounty', 'security-bounty-rates']]) {
302
294
  const s = live.placeholder(kind);
303
295
  assert.match(s.innerHTML, new RegExp(`^<h3 id="${id}">`), `${kind} carries its address from the start`);
304
296
  assert.match(s.innerHTML, /<p class="hall-loading" role="status">Loading…<\/p>/);
@@ -306,38 +298,6 @@ test('RUN: a section says Loading… the moment it is placed, under the heading
306
298
  }
307
299
  });
308
300
 
309
- test('RUN: the treasury fills from the payload, and its line ends on the total the prose states', async () => {
310
- const { live } = await loadLive({ responses: { [TREASURY]: costSummaryFixture } });
311
- const s = live.placeholder('treasury');
312
- assert.equal(await live.fillTreasury(s), true);
313
- assert.equal(s.removed, false);
314
- assert.equal(s.attrs['aria-busy'], undefined, 'no longer busy');
315
- assert.doesNotMatch(s.innerHTML, /Loading…/);
316
- assert.match(s.innerHTML, /^<h3 id="the-treasury">The treasury, so far<\/h3>/);
317
- const usd = `$${Number(costSummaryFixture.total_usd).toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
318
- assert.ok(s.innerHTML.includes(`<strong>${usd}</strong>`), 'the spend total is stated');
319
- assert.match(s.innerHTML, /id="treasury-spend"/);
320
- assert.match(s.innerHTML, /id="treasury-earned"/);
321
- assert.match(s.innerHTML, /Drachmae awarded, cumulative/, 'the currency word is the branding pack\'s');
322
- assert.doesNotMatch(s.innerHTML, /NaN|Infinity/, 'a NaN in a path renders as an invisible chart with no error anywhere');
323
- assert.doesNotMatch(s.innerHTML, /tabindex="0"/, 'no focus stop: nothing here wires a readout');
324
- assert.doesNotMatch(s.innerHTML, /&amp;amp;|&amp;#/, 'nothing is escaped twice');
325
- });
326
-
327
- test('RUN: a treasury that cannot be read, or holds nothing yet, leaves NO section behind', async () => {
328
- for (const responses of [{}, { [TREASURY]: new Error('down') }, { [TREASURY]: { daily: [], daily_credits: [] } }]) {
329
- const { live } = await loadLive({ responses });
330
- const s = live.placeholder('treasury');
331
- assert.equal(await live.fillTreasury(s), false);
332
- assert.equal(s.removed, true, 'the Loading… line goes with it — never an empty panel');
333
- }
334
- const noKit = await loadLive({ responses: { [TREASURY]: costSummaryFixture }, kit: false });
335
- const s = noKit.live.placeholder('treasury');
336
- assert.equal(await noKit.live.fillTreasury(s), false, 'without the chart kit there is nothing honest to draw');
337
- assert.equal(s.removed, true);
338
- assert.match(noKit.warnings.join('\n'), /OTBKit missing/);
339
- });
340
-
341
301
  test('RUN: the bounty rate card renders the live table, tuned tiers marked', async () => {
342
302
  const tuned = { ...bountyFixture, table: bountyFixture.table.map((r, i) => (i === 0 ? { ...r, overridden: true } : r)) };
343
303
  const { live } = await loadLive({ responses: { [BOUNTY]: tuned }, branding: {} });
@@ -358,20 +318,19 @@ test('RUN: a bounty feed that fails leaves no section', async () => {
358
318
  }
359
319
  });
360
320
 
361
- test('the live sections read endpoints that already existed, and draw only through the kit', () => {
362
- assert.match(liveJs, /TREASURY_URL = '\/api\/bongos\/public\/cost-summary'/);
321
+ test('the live section reads an endpoint that already existed, and the treasury is gone to /money', () => {
363
322
  assert.match(liveJs, /BOUNTY_URL = '\/api\/bongos\/public\/bounty-table'/);
364
- for (const fn of ['cumulateByDay', 'timeStepSeriesHtml', 'plotSvgHtml', 'plotValueLabelHtml', 'barSeriesHtml']) {
365
- assert.match(liveJs, new RegExp(`kit\\.${fn}\\(`), `the treasury draws through ${fn}`);
366
- }
323
+ assert.doesNotMatch(liveJs, /cost-summary|fillTreasury/, 'task 1005047: a money figure is drawn on the money page alone');
367
324
  assert.doesNotMatch(liveJs, /<(?:path|polyline|rect)\b[^>]*\b(?:d|points)="\$\{/, 'no hand-built SVG geometry');
368
325
  });
369
326
 
370
- test('the page loads the kit, then the live module, then the page script', () => {
327
+ test('the page loads the live module, then the page script', () => {
371
328
  const loaded = [...primerHtml.matchAll(/<script[^>]+src="([^"?]+)/g)].map((m) => m[1].split('/').pop());
372
329
  const at = (f) => loaded.indexOf(f);
373
- assert.ok(at('hall-kit.js') > -1 && at('primer-live.js') > -1 && at('primer.js') > -1);
374
- assert.ok(at('hall-kit.js') < at('primer-live.js') && at('primer-live.js') < at('primer.js'));
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');
375
334
  // The figure chrome is shared (style.css), so the Primer needs no profile sheet.
376
335
  assert.match(primerHtml, /style\.css/);
377
336
  assert.doesNotMatch(primerHtml, /profile\.css/);
@@ -411,7 +370,7 @@ test('emphasis inside a bold run renders — the API list-price line showed its
411
370
 
412
371
  test('the room gives the fourth level a rule, the measure and the anchor offset', () => {
413
372
  const h4 = rule(roomCss, '.primer-article h4');
414
- assert.ok(h4, 'room.css styles h4 (the treasury\'s "Where it went" is one)');
373
+ assert.ok(h4, 'room.css styles h4 (the credits reference doc uses them)');
415
374
  assert.match(h4, /scroll-margin-top/, 'a deep link to an h4 clears the top bar');
416
375
  const measured = decomment(roomCss).match(/\.primer-article\s*>\s*p,[\s\S]*?\{([^}]*)\}/);
417
376
  assert.ok(measured, 'the measured-children list exists');
@@ -96,7 +96,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
96
96
  // Market Watch settings nav item and its fuel glyph; task 1005041: the Update pill replaced the
97
97
  // core-update banner; task 1005045: the Market Watch settings item left (one sidebar item);
98
98
  // task 1005046: Ranks, Economy and Diagrams left Docs (one reading room, the Primer).
99
- const SHELL_SHA = '36c64a1cf84e9db1';
99
+ const SHELL_SHA = 'baf8a15f025eba68';
100
100
 
101
101
  test('shell.js has not changed without its cache-bust being bumped', () => {
102
102
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
@@ -1,69 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Fleet costs — {{worldName}}</title>
7
- <meta name="description" content="What each hosted project costs this platform, and an estimate of what its owner pays for their own hosting.">
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
- <link rel="preconnect" href="https://fonts.googleapis.com">
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-09-23-changelist1">
12
- <link rel="stylesheet" href="/builders/oversight.css">
13
- <link rel="stylesheet" href="/builders/project-settings.css">
14
- </head>
15
- <body data-page="fleet" data-page-title="Fleet costs">
16
-
17
- <div class="app">
18
- <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
19
- <div class="app-scrim" id="app-scrim" hidden></div>
20
- <div class="app-body">
21
- <header class="app-topbar" id="app-topbar"></header>
22
- <div class="app-content">
23
- <!-- The page's one h1, for screen readers: no page shows its head (task
24
- 1004836), so its sub-line went in task 1004829. -->
25
- <div class="page-head page-head--board">
26
- <h1 class="page-head__h">Fleet costs</h1>
27
- </div>
28
-
29
- <main id="hall-main">
30
- <!-- task 1005043: the page loader, shown until any section below is revealed. -->
31
- <p class="hall-loading hall-loading--page hall-loading--wait" role="status">Loading…</p>
32
- <section class="scroll" id="fl-sealed" aria-labelledby="fl-sealed-h" hidden>
33
- <h2 class="scroll__h" id="fl-sealed-h">Restricted</h2>
34
- <p class="scroll__lede">This page opens at a higher rank.</p>
35
- </section>
36
-
37
- <section class="scroll" id="fl-body" aria-labelledby="fl-body-h" hidden>
38
- <h2 class="scroll__h" id="fl-body-h">What each project costs</h2>
39
- <!-- Two different bills, one column each (ADR 0327): a cloud host runs on its
40
- OWNER's account, so it costs this platform nothing and its owner pays their
41
- hosting provider directly. Nothing on this page is charged to anyone. -->
42
- <p class="scroll__lede">
43
- Two different bills, one column each. The first is what a project costs this
44
- platform. The second is an estimate of what its owner pays their own hosting
45
- provider, which bills them directly. A project with no recorded plan reads
46
- "not priced yet", never $0, and is left out of the total. A self-hosted
47
- project runs on its owner's own machine, so its bill is not visible to us.
48
- </p>
49
- <div id="fl-panel"><span class="kit-skel"></span></div>
50
- </section>
51
- </main>
52
-
53
- <footer class="app-foot" id="app-foot"></footer>
54
- </div>
55
- </div>
56
- </div>
57
-
58
- <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
59
-
60
- <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
61
- <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
62
- <script src="/builders/craft-mode.js?v=2026-11-04-reading-room"></script>
63
- <script src="/builders/shell.js?v=2026-11-04-reading-room"></script>
64
- <script src="/builders/palette.js" defer></script>
65
- <script src="/builders/hall-kit.js"></script>
66
- <script src="/builders/fleet-costs.js"></script>
67
- <script src="/builders/fleet.js"></script>
68
- </body>
69
- </html>
@@ -1,41 +0,0 @@
1
- // fleet.js — the fleet cost page (task 1001949, ADR 0327): the chrome around
2
- // fleet-costs.js, which does all the drawing.
3
- //
4
- // One read, GET /provisioning/fleet/cost-ledger, gated on `provisioning.fleet.manage`
5
- // exactly as this page is. A 403 shows the sealed state rather than an empty table,
6
- // so a refusal is never mistaken for a fleet with nothing in it.
7
- (() => {
8
- 'use strict';
9
-
10
- const API = '/api/bongos';
11
- const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
12
- const { escapeHtml } = window.OTB;
13
- const $ = (id) => document.getElementById(id);
14
-
15
- function fail(message) {
16
- $('fl-body').hidden = false;
17
- $('fl-panel').innerHTML = `<p class="ps-invalid">${escapeHtml(message)}</p>`;
18
- }
19
-
20
- async function load() {
21
- let res;
22
- try {
23
- res = await api.request('GET', `${API}/provisioning/fleet/cost-ledger`);
24
- } catch (e) {
25
- return fail('Could not reach the server.');
26
- }
27
- if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); return; }
28
- if (res.status === 403) {
29
- $('fl-sealed').hidden = false;
30
- return;
31
- }
32
- if (!res.ok) {
33
- const err = (res.data && res.data.error) || {};
34
- return fail(err.message && err.message !== err.code ? err.message : 'Could not read the fleet costs.');
35
- }
36
- $('fl-body').hidden = false;
37
- $('fl-panel').innerHTML = window.OTBFleetCosts.ledgerHtml(res.data);
38
- }
39
-
40
- document.addEventListener('DOMContentLoaded', load);
41
- })();
@@ -1,14 +0,0 @@
1
- {
2
- "_": "FLEET COSTS (task 1001949, ADR 0327) through modules/ui-design/kit/render.js. The page reads ONE authenticated route, GET /provisioning/fleet/cost-ledger, gated on provisioning.fleet.manage, so against the kit's own stub it renders its failed-read state — a state the floors must hold in. To look at the populated ledger run it against the hall-preview harness, whose provisioning__fleet__cost-ledger.json fixture carries two cloud hosts with no recorded plan (every owner cost unpriced, the state live is in today):\n\n node scripts/hall-preview/server.js --port 4620 --fixture-me\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/fleet.states.json --base-in http://localhost:4620 --base-out http://localhost:4620",
3
- "page": "/builders/fleet",
4
- "surface": "hall-ui",
5
- "stub": { "prefix": "/builders/" },
6
- "modeQuery": false,
7
- "ignoreRequests": ["/api/(gds|bongos)/"],
8
- "ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b403\\b", "\\b404\\b", "Unauthorized", "Forbidden", "Not Found", "load failed"],
9
- "states": {
10
- "ledger": { "auth": true, "actions": [["wait", 800]], "expect": { "visible": ["#fl-body"] } },
11
- "out": { "auth": false, "actions": [["wait", 600]] }
12
- },
13
- "reducedMotion": { "auth": true, "actions": [["wait", 800]] }
14
- }