@bongos/core 1.21.19 → 1.21.21

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 (70) hide show
  1. package/.bongos-core.json +84 -74
  2. package/docs/copy-inventory.md +29 -25
  3. package/docs/copy-registry.json +87 -30
  4. package/docs/module-api-changelog.md +4 -0
  5. package/docs/page-inventory.json +9 -3
  6. package/docs/page-readings.json +399 -388
  7. package/modules/autonomy/governor-docket.js +118 -4
  8. package/modules/economy/cost.js +36 -0
  9. package/modules/economy/governor-docket.js +81 -0
  10. package/modules/economy/routes/cost.js +2 -0
  11. package/modules/government/docket-floors.js +222 -0
  12. package/modules/government/docket-vocab.js +20 -0
  13. package/modules/government/docket.js +186 -5
  14. package/modules/hall-ui/public/atlas.html +1 -1
  15. package/modules/hall-ui/public/blockers.html +1 -1
  16. package/modules/hall-ui/public/board-room.html +1 -1
  17. package/modules/hall-ui/public/city.css +16 -2
  18. package/modules/hall-ui/public/city.html +29 -9
  19. package/modules/hall-ui/public/city.js +25 -2
  20. package/modules/hall-ui/public/city.states.json +7 -1
  21. package/modules/hall-ui/public/collab.html +1 -1
  22. package/modules/hall-ui/public/commands.html +1 -1
  23. package/modules/hall-ui/public/copy-desk.html +1 -1
  24. package/modules/hall-ui/public/deploy.html +1 -1
  25. package/modules/hall-ui/public/diagrams.html +1 -1
  26. package/modules/hall-ui/public/drachmae.html +1 -1
  27. package/modules/hall-ui/public/fleet.html +1 -1
  28. package/modules/hall-ui/public/gate.html +1 -1
  29. package/modules/hall-ui/public/goal-map.html +1 -1
  30. package/modules/hall-ui/public/goals.html +1 -1
  31. package/modules/hall-ui/public/government.html +1 -1
  32. package/modules/hall-ui/public/idea.html +1 -1
  33. package/modules/hall-ui/public/ideas.html +1 -1
  34. package/modules/hall-ui/public/index.html +1 -1
  35. package/modules/hall-ui/public/modules.html +1 -1
  36. package/modules/hall-ui/public/primer.html +1 -1
  37. package/modules/hall-ui/public/profile.html +1 -1
  38. package/modules/hall-ui/public/project-settings.html +1 -1
  39. package/modules/hall-ui/public/ranks.html +1 -1
  40. package/modules/hall-ui/public/roadmap.html +1 -1
  41. package/modules/hall-ui/public/roster.html +1 -1
  42. package/modules/hall-ui/public/sessions.html +1 -1
  43. package/modules/hall-ui/public/settings.html +1 -1
  44. package/modules/hall-ui/public/shell.js +3 -0
  45. package/modules/hall-ui/public/street-inside.js +99 -43
  46. package/modules/hall-ui/public/street.css +17 -4
  47. package/modules/hall-ui/public/street.html +26 -8
  48. package/modules/hall-ui/public/street.js +16 -2
  49. package/modules/hall-ui/public/street.states.json +17 -1
  50. package/modules/hall-ui/public/studio.html +1 -1
  51. package/modules/hall-ui/public/task.html +1 -1
  52. package/modules/hall-ui/public/thinking.html +1 -1
  53. package/modules/hall-ui/public/tweak-editor.html +1 -1
  54. package/modules/hall-ui/public/watch.html +1 -1
  55. package/modules/hall-ui/public/work.html +1 -1
  56. package/modules/ideas/governor-docket.js +42 -1
  57. package/modules/lifecycle/governor-docket.js +197 -2
  58. package/modules/security/routes/reports.js +16 -0
  59. package/modules/security/security.js +5 -2
  60. package/package-lock.json +2 -2
  61. package/package.json +1 -1
  62. package/release-notes.json +12 -0
  63. package/src/module-api.js +1 -1
  64. package/tests/governor_docket.mjs +186 -2
  65. package/tests/hall_audit.mjs +7 -12
  66. package/tests/hall_city.mjs +44 -4
  67. package/tests/hall_street.mjs +37 -5
  68. package/tests/hall_street_inside.mjs +97 -3
  69. package/tests/hall_tweak_editor.mjs +1 -1
  70. package/tests/nav_permission_atoms.mjs +1 -1
@@ -375,18 +375,13 @@ test('the module doc still records where the 2179 density pass landed', () => {
375
375
  // else is on the page but the wordmark, a sun/moon switch, start governing and
376
376
  // one line. It still loads style.css (the hall's tokens, so no private palette:
377
377
  // city.css spends var(--bg), var(--ink), var(--accent) …), and reads and writes
378
- // the hall's own 'hall-theme' preference. Whether it should join the chrome
379
- // behind the full-screen switch, as the sky did in task 1004327, is the
380
- // owner's call, named in that task's ship notes.
381
- // THE STREET (task 1004525) follows the city, for the same reason: the
382
- // reference design's street view shows nothing on the page but its own header
383
- // (the wordmark back to the city, needs you, moving, your share, the switch),
384
- // the street and the six readings. Same terms as the city: style.css and the
385
- // hall's tokens, the hall's own 'hall-theme' preference, no private palette.
386
- const STANDALONE_EXEMPT = {
387
- 'city.html': 'the governor city home: nothing on the page but the prototype home (task 1004524)',
388
- 'street.html': 'the governor street: nothing on the page but the prototype street view (task 1004525)',
389
- };
378
+ // the hall's own 'hall-theme' preference. THE STREET (task 1004525) followed
379
+ // it for the same reason. Both left this list in task 1004544 (owner decision
380
+ // 2026-10-02): they sit in the hall chrome now, behind the shell's full-screen
381
+ // switch, exactly as the sky did in task 1004327, so the list is empty again.
382
+ // Neither has a sidebar link (the owner's call): the city is reached by URL and
383
+ // the street from the city's start governing.
384
+ const STANDALONE_EXEMPT = {};
390
385
 
391
386
  test('every hall page loads style.css and shell.js', () => {
392
387
  const missing = [];
@@ -241,10 +241,27 @@ test('the page holds only the wordmark, the switch, the city, one button and one
241
241
  assert.equal((html.match(/<a\b/g) || []).length, 1, 'one door');
242
242
  assert.equal((html.match(/<button\b/g) || []).length, 1, 'one switch');
243
243
  assert.match(html, /class="city-mark">\{\{productName\}\}</);
244
- assert.doesNotMatch(html, /app-sidebar|shell\.js/, 'no hall chrome on the city');
245
244
  assert.equal(City.STREET_HREF, '/builders/street');
246
245
  });
247
246
 
247
+ // task 1004544 (owner decision 2026-10-02): the city sits in the hall chrome
248
+ // behind the shell's full-screen switch, the sky's mechanism (task 1004327),
249
+ // and still has no sidebar link.
250
+ test('the city sits in the hall chrome, behind the shell\'s full-screen switch', () => {
251
+ const html = read('city.html').replace(/<!--[\s\S]*?-->/g, '');
252
+ assert.match(html, /<body data-page="city"[^>]*\sdata-fullscreen-switch>/, 'the bare attribute: the page lays itself out for full screen');
253
+ for (const id of ['app-sidebar', 'app-scrim', 'app-topbar']) assert.match(html, new RegExp(`id="${id}"`), id);
254
+ assert.match(html, /<main id="hall-main" class="city-main">\s*<div class="city-top">/, 'the stage is the one <main>, the strip inside it');
255
+ const scripts = [...html.matchAll(/<script src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
256
+ assert.deepEqual(scripts, ['shell', 'palette', 'city-draw', 'city'], 'the shell first, as on every hall page');
257
+ const css = read('city.css').replace(/\/\*[\s\S]*?\*\//g, '');
258
+ assert.match(css, /\.city-main \{ position: fixed; top: var\(--topbar-h, 56px\); left: var\(--sidebar-w-rail, 64px\);[^}]*contain: layout paint;/);
259
+ assert.match(css, /html\[data-fullscreen="on"\] \.city-main \{ top: 0; left: 0; \}/);
260
+ assert.match(css, /html\[data-fullscreen="on"\] \.city-top \{ right: 184px; \}/, 'the strip steps clear of the corner exit');
261
+ const shell = read('shell.js');
262
+ assert.doesNotMatch(shell, /href:\s*'(\/builders)?\/(city|street)'/, 'no sidebar link (owner: leave it off the sidebar)');
263
+ });
264
+
248
265
  test('motion is CSS only and dies under reduced motion; no page :root; no innerHTML', () => {
249
266
  const css = read('city.css').replace(/\/\*[\s\S]*?\*\//g, '');
250
267
  assert.doesNotMatch(css, /:root/);
@@ -293,7 +310,7 @@ function fakeDom() {
293
310
  return { document, byId, html };
294
311
  }
295
312
 
296
- async function bootPage({ feeds = {}, stored = null, osDark = true } = {}) {
313
+ async function bootPage({ feeds = {}, stored = null, osDark = true, shell = undefined, stage = null } = {}) {
297
314
  const { document, byId, html } = fakeDom();
298
315
  const store = new Map(stored ? [['hall-theme', stored]] : []);
299
316
  const calls = [];
@@ -308,14 +325,18 @@ async function bootPage({ feeds = {}, stored = null, osDark = true } = {}) {
308
325
  const body = feeds[url];
309
326
  return Promise.resolve(body === undefined ? { ok: false, json: async () => ({}) } : { ok: true, json: async () => body });
310
327
  };
311
- const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, console });
328
+ const observers = [];
329
+ class MutationObserver { constructor(fn) { this.fn = fn; observers.push(this); } observe() {} }
330
+ window.OTBShell = shell;
331
+ if (stage) { const m = document.createElement('main'); m.setAttribute('id', 'hall-main'); Object.assign(m, stage); }
332
+ const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, console, MutationObserver });
312
333
  ctx.globalThis = ctx;
313
334
  window.GovCity = undefined;
314
335
  vm.runInContext(read('city-draw.js'), ctx);
315
336
  window.GovCity = ctx.GovCity || ctx.window.GovCity;
316
337
  vm.runInContext(read('city.js'), ctx);
317
338
  await window.GovCityPage.ready;
318
- return { window, byId, html, store, calls };
339
+ return { window, byId, html, store, calls, observers };
319
340
  }
320
341
 
321
342
  const FEEDS = (ms, goals = [goal(1)], rollups = [rollup(1, { done: 2, active: 1 })]) => ({
@@ -363,6 +384,25 @@ test('the switch reads and writes the hall\'s own theme preference', async () =>
363
384
  assert.equal(q.html.attrs['data-theme'], 'dark', 'no stored choice follows the OS');
364
385
  });
365
386
 
387
+ test('in the hall, the city and the top bar\'s theme switch never disagree (task 1004544)', async () => {
388
+ let shellPaints = 0;
389
+ const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: true, shell: { applyTheme() { shellPaints++; } } });
390
+ assert.equal(p.observers.length, 1, 'the page watches html[data-theme]');
391
+ p.byId.get('cityMode').fire('click');
392
+ assert.equal(shellPaints, 1, 'this switch repaints the shell\'s');
393
+ const before = JSON.stringify(p.byId.get('cityFlat').children[0].attrs);
394
+ p.store.set('hall-theme', 'light'); p.html.setAttribute('data-theme', 'light'); // the shell's switch
395
+ p.observers[0].fn();
396
+ assert.equal(p.byId.get('cityMode').attrs['aria-label'], 'Switch to dark mode', 'this switch follows the shell\'s');
397
+ assert.notEqual(JSON.stringify(p.byId.get('cityFlat').children[0].attrs), before, 'and the city redraws in that mode');
398
+ assert.equal(shellPaints, 1, 'following the shell does not echo back to it');
399
+ });
400
+
401
+ test('the artboard fits the stage the hall chrome leaves, not the window (task 1004544)', async () => {
402
+ const p = await bootPage({ feeds: FEEDS([]), stage: { clientWidth: 688, clientHeight: 900 } });
403
+ assert.equal(p.byId.get('cityStage').style['--city-scale'], '0.688');
404
+ });
405
+
366
406
  test('docket text lands as text: a title with markup is never parsed', async () => {
367
407
  const p = await bootPage({ feeds: FEEDS([matter({ title: '<img src=x onerror=alert(1)>', short: '<b>x</b>' })]) });
368
408
  assert.equal(p.byId.get('cityPins').children.length, 1);
@@ -375,12 +375,27 @@ test('the page holds the prototype\'s header, the street, the readings and the c
375
375
  assert.equal((html.match(/<h1\b/g) || []).length, 1);
376
376
  assert.match(html, /<a class="street-mark" id="streetHome" href="\/builders\/city"[^>]*>\{\{productName\}\}</);
377
377
  for (const id of ['hudNeeds', 'hudMoving', 'hudShare', 'streetMode', 'streetSigns', 'streetPins', 'streetDoors', 'streetCard']) assert.match(html, new RegExp(`id="${id}"`), id);
378
- assert.doesNotMatch(html, /app-sidebar|shell\.js/, 'no hall chrome, as the city');
379
378
  const scripts = [...html.matchAll(/<script src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
380
- assert.deepEqual(scripts, ['city-draw', 'street-draw', 'street-card', 'street-inside', 'street-amend', 'street'], 'the shared kit loads first, then the card (task 1004528), the interiors (task 1004526) and the drafted amendment (task 1004530)');
379
+ assert.deepEqual(scripts, ['shell', 'palette', 'city-draw', 'street-draw', 'street-card', 'street-inside', 'street-amend', 'street'], 'the hall shell (task 1004544), the shared kit, then the card (task 1004528), the interiors (task 1004526) and the drafted amendment (task 1004530)');
381
380
  assert.equal(City.STREET_HREF, '/builders/street', 'the city\'s door is this page');
382
381
  });
383
382
 
383
+ // task 1004544 (owner decision 2026-10-02): the street sits in the hall chrome
384
+ // behind the shell's full-screen switch, as the city and the sky (task 1004327),
385
+ // and still has no sidebar link.
386
+ test('the street sits in the hall chrome, behind the shell\'s full-screen switch', () => {
387
+ const html = read('street.html').replace(/<!--[\s\S]*?-->/g, '');
388
+ assert.match(html, /<body data-page="street"[^>]*\sdata-fullscreen-switch>/, 'the bare attribute: the page lays itself out for full screen');
389
+ for (const id of ['app-sidebar', 'app-scrim', 'app-topbar']) assert.match(html, new RegExp(`id="${id}"`), id);
390
+ assert.match(html, /<main id="hall-main" class="street-stage">\s*<div class="street-top">/, 'the stage is the one <main>, the strip inside it');
391
+ assert.match(html, /<div class="street-window" id="streetWindow">/, 'the street band is a <div> inside the stage');
392
+ const css = read('street.css').replace(/\/\*[\s\S]*?\*\//g, '');
393
+ assert.match(css, /\.street-stage \{ position: fixed; top: var\(--topbar-h, 56px\); left: var\(--sidebar-w-rail, 64px\);[^}]*contain: layout paint;/);
394
+ assert.match(css, /html\[data-fullscreen="on"\] \.street-stage \{ top: 0; left: 0; \}/);
395
+ assert.match(css, /html\[data-fullscreen="on"\] \.street-top \{ padding-right: 184px; \}/, 'the strip steps clear of the corner exit');
396
+ assert.doesNotMatch(read('shell.js'), /href:\s*'(\/builders)?\/(city|street)'/, 'no sidebar link (owner: leave it off the sidebar)');
397
+ });
398
+
384
399
  test('motion is CSS only and dies under reduced motion; no page :root; no innerHTML', () => {
385
400
  const css = read('street.css').replace(/\/\*[\s\S]*?\*\//g, '');
386
401
  assert.doesNotMatch(css, /:root/);
@@ -428,7 +443,7 @@ function fakeDom() {
428
443
  return { document, byId, html, focused };
429
444
  }
430
445
 
431
- async function bootPage({ feeds = {}, stored = null, osDark = true, hash = '' } = {}) {
446
+ async function bootPage({ feeds = {}, stored = null, osDark = true, hash = '', shell = undefined } = {}) {
432
447
  const { document, byId, html, focused } = fakeDom();
433
448
  const store = new Map(stored ? [['hall-theme', stored]] : []);
434
449
  const calls = [];
@@ -444,7 +459,10 @@ async function bootPage({ feeds = {}, stored = null, osDark = true, hash = '' }
444
459
  const body = feeds[url];
445
460
  return Promise.resolve(body === undefined ? { ok: false, json: async () => ({}) } : { ok: true, json: async () => body });
446
461
  };
447
- const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN });
462
+ const observers = [];
463
+ class MutationObserver { constructor(fn) { this.fn = fn; observers.push(this); } observe() {} }
464
+ window.OTBShell = shell;
465
+ const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, MutationObserver });
448
466
  ctx.globalThis = ctx;
449
467
  // each file hangs its api on `window`, as the browser loads them
450
468
  vm.runInContext(read('city-draw.js'), ctx);
@@ -453,7 +471,7 @@ async function bootPage({ feeds = {}, stored = null, osDark = true, hash = '' }
453
471
  vm.runInContext(read('street.js'), ctx);
454
472
  await window.GovStreetPage.ready;
455
473
  const key = (t, e) => (listeners[t] || []).forEach((fn) => fn(e));
456
- return { window, byId, html, store, calls, key, focused };
474
+ return { window, byId, html, store, calls, key, focused, observers };
457
475
  }
458
476
 
459
477
  const FEEDS = (ms, goals = [goal(1)], rollups = [rollup(1, { done: 2, active: 1 })]) => ({
@@ -509,6 +527,20 @@ test('the switch reads and writes the hall\'s own theme preference and redraws t
509
527
  assert.notEqual(JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs), before, 'the street redraws in the new mode');
510
528
  });
511
529
 
530
+ test('in the hall, the street and the top bar\'s theme switch never disagree (task 1004544)', async () => {
531
+ let shellPaints = 0;
532
+ const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: true, shell: { applyTheme() { shellPaints++; } } });
533
+ assert.equal(p.observers.length, 1, 'the page watches html[data-theme]');
534
+ p.byId.get('streetMode').fire('click');
535
+ assert.equal(shellPaints, 1, 'this switch repaints the shell\'s');
536
+ const before = JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs);
537
+ p.store.set('hall-theme', 'light'); p.html.setAttribute('data-theme', 'light'); // the shell's switch
538
+ p.observers[0].fn();
539
+ assert.equal(p.byId.get('streetModeWord').textContent, 'dark', 'this switch follows the shell\'s');
540
+ assert.notEqual(JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs), before, 'and the street redraws in that mode');
541
+ assert.equal(shellPaints, 1, 'following the shell does not echo back to it');
542
+ });
543
+
512
544
  test('arriving through a door marks that building, and a failed docket says so', async () => {
513
545
  const p = await bootPage({ feeds: FEEDS([]), hash: '#building=treasury' });
514
546
  const here = p.byId.get('streetDoors').children.filter((a) => a.attrs['aria-current'] === 'location');
@@ -68,6 +68,8 @@ function docket(matters = [], over = {}) {
68
68
  };
69
69
  }
70
70
  const goal = (id, status = 'open', title = `Goal ${id}`) => ({ id: String(id), title, status });
71
+ // a docket floor row in the route's shape (docket.js buildDocket `floors`)
72
+ const floorRow = (key, over = {}) => ({ key, building: key.split('.')[0], status: 'live', count: '3', line: 'three things', lines: [['One thing.', 'today']], hot: false, reason: null, ...over });
71
73
 
72
74
  // ── the drawing is the prototype's ───────────────────────────────────────────
73
75
 
@@ -176,10 +178,12 @@ test('each building keeps the prototype\'s capacity, words and floor order', ()
176
178
 
177
179
  // ── what a floor says comes from the docket, and only from it ───────────────
178
180
 
179
- test('a floor no feed reads stands as not recorded yet: faint, no number, and says so', () => {
180
- const b = I.civicInfo('charter', { docket: docket(), goals: [], now: NOW });
181
+ test('a floor whose engine is coming stands as not recorded yet: faint, no number, and says so', () => {
182
+ const coming = docket([], { floors: I.PLAN.charter.floors.map((f) => floorRow(f[2].floor, { status: 'coming', count: '', lines: [], reason: 'Not yet.' })) });
183
+ const b = I.civicInfo('charter', { docket: coming, goals: [], now: NOW });
181
184
  assert.ok(b.floors.every((f) => f.status === 'coming' && f.count === ''), 'nothing on the charter is read yet');
182
- const bv = I.buildingModel({ key: 'charter', docket: docket(), mode: 'dark', now: NOW });
185
+ assert.deepEqual(b.floors[1].lines, [['Not yet.', 'coming']], 'it says why, in the docket\'s words');
186
+ const bv = I.buildingModel({ key: 'charter', docket: coming, mode: 'dark', now: NOW });
183
187
  const quiet = bv.texts.filter((t) => t.v === 'not recorded yet');
184
188
  assert.equal(quiet.length, 8, 'every used floor says it on the wall');
185
189
  assert.ok(bv.texts.filter((t) => t.s === 14.5).every((t) => t.c === '#8e8e93'), 'its name lettered faint, like a proposed floor');
@@ -265,6 +269,96 @@ test('who holds it: the docket\'s holders when it names them, else an empty seat
265
269
  assert.deepEqual([held.seats, held.who, held.how], [['M'], 'Mira, treasurer', 'more than half must say yes']);
266
270
  });
267
271
 
272
+ // ── real data on every floor (task 1004527) ─────────────────────────────────
273
+
274
+ test('every floor of every building is bound to a docket feed: none is left unwired', () => {
275
+ for (const key of S.CIVIC_KEYS) {
276
+ for (const [name, , bind] of I.PLAN[key].floors) {
277
+ assert.ok(bind && (bind.floor || bind.source || bind.reading || bind.goals), `${key} · ${name} reads something`);
278
+ }
279
+ }
280
+ });
281
+
282
+ test('a live floor shows exactly what the docket says: its count, line and lines; hot is being decided', () => {
283
+ const d = docket([], { floors: [
284
+ floorRow('treasury.rewards', { count: '$2,400', line: '$2,400 this month · 6 builders', lines: [['Paid to builders for shipped work.', 'each ship']] }),
285
+ floorRow('automation.allowed_goals', { count: '2', line: '2 goals it may build', lines: [['goal 7', 'first in line'], ['goal 99', 'since sep 2']] }),
286
+ floorRow('charter.founding', { count: '', line: '1 of 2 founding steps passed', lines: [['The charter.', 'passed']], hot: true }),
287
+ ] });
288
+ const t = I.civicInfo('treasury', { docket: d, now: NOW }).floors.find((f) => f.name === 'Builder rewards');
289
+ assert.deepEqual([t.count, t.line, t.status, t.lines], ['$2,400', '$2,400 this month · 6 builders', 'ok', [['Paid to builders for shipped work.', 'each ship']]]);
290
+ const a = I.civicInfo('automation', { docket: d, goals: [goal(7, 'open', 'Onboarding')], now: NOW }).floors.find((f) => f.name === 'Allowed goals');
291
+ assert.deepEqual(a.lines, [['Onboarding', 'first in line'], ['goal 99', 'since sep 2']], 'a goal it knows is lettered by title; one it does not keeps its id');
292
+ const c = I.buildingModel({ key: 'charter', docket: d, mode: 'dark', now: NOW });
293
+ assert.equal(c.selFloor, 0, 'a founding step being decided is the floor chosen first');
294
+ assert.equal(c.openKicker, 'floor 1 · being decided');
295
+ });
296
+
297
+ test('Rules in force: each rule and what it settled, from its floor; the reading when the floor is absent', () => {
298
+ const d = docket([], { floors: [floorRow('rules.in_force', { count: '1', line: '1 rule · settled 18 this month', lines: [['Every filing lands in its goal without a vote.', 'settled 18']] })] });
299
+ const f = I.civicInfo('rules', { docket: d, now: NOW }).floors[0];
300
+ assert.deepEqual([f.count, f.line, f.lines[0]], ['1', '1 rule · settled 18 this month', ['Every filing lands in its goal without a vote.', 'settled 18']]);
301
+ const old = docket();
302
+ Object.assign(old.readings.find((r) => r.key === 'rules'), { status: 'live', figure: '1 rule', line: 'the landing rule settled 4 filings this month' });
303
+ assert.deepEqual(I.civicInfo('rules', { docket: old, now: NOW }).floors[0].count, '1', 'an older docket still reads the reading');
304
+ const h = I.holderOf('people', { decision: { who: 'metic', rule: 'consent' }, holders: [], holder_count: 3 }, []);
305
+ assert.equal(h.who, 'metic', 'a count with no names never letters undefined');
306
+ });
307
+
308
+ test('a floor the docket cannot answer never shows a number; Goals falls back to the open goals', () => {
309
+ const d = docket([], { floors: [floorRow('people.ranks', { status: 'not_yours', count: '9' }), floorRow('people.council', { status: 'off', count: '5', reason: 'Off here.' }), floorRow('people.newcomers', { status: 'unavailable', count: '2' })] });
310
+ const by = Object.fromEntries(I.civicInfo('people', { docket: d, now: NOW }).floors.map((f) => [f.name, f]));
311
+ for (const [n, st] of [['Ranks', 'not_yours'], ['The council', 'off'], ['Newcomers', 'unavailable'], ['Testers', 'unavailable']]) {
312
+ assert.deepEqual([by[n].status, by[n].count], [st, ''], `${n} says ${st} and no number`);
313
+ }
314
+ const g = I.civicInfo('planning', { docket: docket(), goals: [goal(1), goal(2)], now: NOW }).floors[0];
315
+ assert.deepEqual([g.count, g.line], ['2', '2 goals open'], 'an older docket without floors still counts the open goals');
316
+ const live = I.civicInfo('planning', { docket: docket([], { floors: [floorRow('planning.goals', { count: '1', line: '1 goal open', lines: [['Hall', '5 of 5 kept']] })] }), goals: [goal(1), goal(2)], now: NOW }).floors[0];
317
+ assert.deepEqual([live.count, live.lines], ['1', [['Hall', '5 of 5 kept']]], 'with the floor, its promises kept of total');
318
+ });
319
+
320
+ test('no made-up number: every count on every wall is one the docket sent', () => {
321
+ const ms = [matter({ id: 'access_requests:1', source: 'access_requests', building: 'people' })];
322
+ const floors = [floorRow('people.ranks', { count: '4' }), floorRow('treasury.hosting', { count: '$120' }), floorRow('release.live_month', { count: '7' })];
323
+ const d = docket(ms, { floors });
324
+ Object.assign(d.readings.find((r) => r.key === 'money'), { status: 'live', figure: '$1,100 left', line: 'left' });
325
+ const sent = new Set(['', ...floors.map((f) => f.count), '1', '0', '$1,100']);
326
+ for (const key of S.CIVIC_KEYS) {
327
+ for (const f of I.civicInfo(key, { docket: d, goals: [], now: NOW }).floors) assert.ok(sent.has(f.count), `${key} · ${f.name} shows ${f.count}`);
328
+ }
329
+ });
330
+
331
+ test('who holds it reads the decision table: the people it seats, its rule, you, and a vacant seat', () => {
332
+ const info = { key: 'release', decision: { who: 'whoever may put a release live', rule: 'decide', rule_words: 'decides alone', as_today: true }, holders: [{ id: '95', name: 'Ana' }, { id: '7', login: 'theo' }], holder_count: 2 };
333
+ const h = I.holderOf('release', info, [], '95');
334
+ assert.deepEqual([h.seats, h.who, h.rule, h.how], [['you', 'T'], 'Ana and theo · whoever may put a release live', 'decide', 'one person decides']);
335
+ const many = I.holderOf('people', { decision: { who: 'metic and above', rule: 'consent' }, holders: [{ name: 'A' }, { name: 'B' }, { name: 'C' }], holder_count: 7 }, []);
336
+ assert.equal(many.who, 'A, B, C and 4 more · metic and above');
337
+ const vacant = I.holderOf('review', { decision: { who: 'no one yet', rule: 'vacant' }, holders: [] }, []);
338
+ assert.deepEqual([vacant.seats, vacant.who, vacant.how], [['—'], 'no one holds it yet', 'its matters wait until a seat is filled']);
339
+ const d = docket();
340
+ d.buildings.find((b) => b.key === 'review').decision = { who: 'no one yet', rule: 'vacant' };
341
+ assert.equal(I.civicInfo('review', { docket: d, now: NOW }).vacant, true, 'a vacant row is an unheld Review');
342
+ const held = I.civicInfo('review', { docket: docket(), now: NOW });
343
+ assert.deepEqual(held.floors.map((f) => f.name), ['Testers', 'Being tested', 'Signed off'], 'held, its queue is the prototype\'s Being tested');
344
+ });
345
+
346
+ test('the record drawers read the real history by month, file a sitting once, and cap a full drawer', () => {
347
+ const history = [
348
+ { id: 'government:amendment:12', source: 'government', building: 'charter', title: '§6 Review: testers hold the seat', at: '2026-10-01T09:00:00.000Z', outcome: 'passed' },
349
+ { id: 'government:founding:3', source: 'government', building: 'charter', title: 'Founding: the charter', at: '2026-07-04T09:00:00.000Z', outcome: 'passed' },
350
+ { id: 'releases:9', source: 'releases', building: 'release', title: 'Invite flow', at: '2026-09-26T09:00:00.000Z', outcome: 'live' },
351
+ ];
352
+ const ms = [matter({ id: 'board:12', source: 'board', building: 'charter', state: 'settled', title: '§6 Review: testers hold the seat', ref: { type: 'board_item', id: '12' }, opened_at: '2026-09-28T00:00:00.000Z' })];
353
+ const m = I.buildingModel({ key: 'charter', docket: docket(ms, { history }), mode: 'dark', now: NOW });
354
+ assert.deepEqual(m.drawers.map((x) => [x.label, x.count]), [['oct', 1], ['jul', 1]], 'the settled sitting is filed once, by when it closed');
355
+ assert.deepEqual(m.drawerItems.map((x) => [x.text, x.meta]), [['§6 Review: testers hold the seat', 'passed']]);
356
+ assert.equal(m.onFile, '1 matter on the docket · 2 on the record');
357
+ const lots = Array.from({ length: 15 }, (_, i) => ({ id: `releases:${i}`, building: 'release', title: `R${i}`, at: '2026-09-02T00:00:00.000Z', outcome: 'live' }));
358
+ const r = I.recordOf([], NOW, lots);
359
+ assert.deepEqual([r[0].count, r[0].items.length, r[0].items[12]], [15, 13, ['and 3 more', '']]);
360
+ });
361
+
268
362
  test('the annex lot stands only when the building is full; an unheld Review is the dashed ghost', () => {
269
363
  assert.equal(I.buildingModel({ key: 'charter', docket: docket(), mode: 'dark', now: NOW }).hasAnnex, false, '8 of 9');
270
364
  const save = I.PLAN.rules.cap;
@@ -635,7 +635,7 @@ try {
635
635
  assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
636
636
  assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
637
637
  assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
638
- assert.match(HTML, /shell\.js\?v=2026-10-01-look/);
638
+ assert.match(HTML, /shell\.js\?v=2026-10-02-theme/);
639
639
  assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
640
640
  for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
641
641
  assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
87
87
  // the file and left the query alone, so a returning browser kept the cached copy —
88
88
  // a fix that reaches nobody looks identical to a fix that does not work.
89
89
 
90
- const SHELL_SHA = '51a13ea563542113';
90
+ const SHELL_SHA = 'a8dc2884930ca19a';
91
91
 
92
92
  test('shell.js has not changed without its cache-bust being bumped', () => {
93
93
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);