@bongos/core 1.21.113 → 1.21.115

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 (265) hide show
  1. package/.bongos-core.json +608 -188
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/bongos-feedback/SKILL.md +1 -1
  4. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  5. package/.claude/skills/builder-ship/SKILL.md +1 -0
  6. package/GOVERNMENT.md +128 -11
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +28 -0
  9. package/clients/bongos-client/index.cjs +28 -0
  10. package/clients/bongos-client/index.d.ts +45 -2
  11. package/clients/bongos-client/index.mjs +28 -0
  12. package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
  13. package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
  14. package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
  15. package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
  16. package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
  17. package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
  18. package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
  19. package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
  20. package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
  21. package/docs/adr/README.md +10 -1
  22. package/docs/api/openapi.json +989 -87
  23. package/docs/api-reference.md +18 -4
  24. package/docs/copy-inventory.md +709 -626
  25. package/docs/copy-registry.json +1673 -858
  26. package/docs/file-map.md +2 -1
  27. package/docs/module-api-changelog.md +4 -0
  28. package/docs/packs/engineer.md +2 -1
  29. package/docs/page-inventory.json +91 -6
  30. package/docs/page-readings.json +4875 -4748
  31. package/modules/builder-settings/law-locks.js +202 -0
  32. package/modules/builder-settings/routes/settings.js +66 -6
  33. package/modules/builder-settings/settings.js +7 -0
  34. package/modules/discord/ship-broadcast.js +22 -13
  35. package/modules/government/asks-db.js +54 -0
  36. package/modules/government/asks.js +269 -0
  37. package/modules/government/board-idea-reads.js +68 -0
  38. package/modules/government/board.js +21 -57
  39. package/modules/government/build-rights.js +148 -0
  40. package/modules/government/buildouts.js +160 -0
  41. package/modules/government/counts.js +81 -0
  42. package/modules/government/db.js +242 -0
  43. package/modules/government/decision-board.js +234 -8
  44. package/modules/government/decision-rules.js +2 -0
  45. package/modules/government/docket.js +114 -6
  46. package/modules/government/law-board.js +131 -0
  47. package/modules/government/laws.js +241 -0
  48. package/modules/government/leads.js +170 -0
  49. package/modules/government/migrations/government_027_laws.sql +143 -0
  50. package/modules/government/migrations/government_028_asks.sql +57 -0
  51. package/modules/government/migrations/government_029_leads.sql +135 -0
  52. package/modules/government/migrations/government_030_buildouts.sql +111 -0
  53. package/modules/government/migrations/government_031_treasury.sql +107 -0
  54. package/modules/government/module.json +2 -1
  55. package/modules/government/pollers/treasury-borrow.js +62 -0
  56. package/modules/government/positions.js +107 -0
  57. package/modules/government/presets.js +149 -0
  58. package/modules/government/routes/government.js +425 -2
  59. package/modules/government/routing.js +154 -0
  60. package/modules/government/treasury-db.js +166 -0
  61. package/modules/government/treasury.js +241 -0
  62. package/modules/hall-ui/public/atlas.html +2 -2
  63. package/modules/hall-ui/public/blockers.html +2 -2
  64. package/modules/hall-ui/public/board-room.html +2 -2
  65. package/modules/hall-ui/public/board-room.js +2 -0
  66. package/modules/hall-ui/public/city-draw.js +604 -11
  67. package/modules/hall-ui/public/city.css +157 -1
  68. package/modules/hall-ui/public/city.html +37 -7
  69. package/modules/hall-ui/public/city.js +441 -21
  70. package/modules/hall-ui/public/city.states.json +110 -5
  71. package/modules/hall-ui/public/collab.html +2 -2
  72. package/modules/hall-ui/public/commands.html +2 -2
  73. package/modules/hall-ui/public/copy-desk.html +2 -2
  74. package/modules/hall-ui/public/craft-mode.js +112 -29
  75. package/modules/hall-ui/public/deploy.html +2 -2
  76. package/modules/hall-ui/public/diagram-viewer.css +12 -0
  77. package/modules/hall-ui/public/diagram-viewer.js +4 -0
  78. package/modules/hall-ui/public/diagrams.html +2 -2
  79. package/modules/hall-ui/public/drachmae.html +2 -2
  80. package/modules/hall-ui/public/fleet.html +2 -2
  81. package/modules/hall-ui/public/gate.html +2 -2
  82. package/modules/hall-ui/public/goal-map.html +2 -2
  83. package/modules/hall-ui/public/goals.html +2 -2
  84. package/modules/hall-ui/public/gov-feeds.js +141 -0
  85. package/modules/hall-ui/public/gov-living.js +1002 -0
  86. package/modules/hall-ui/public/gov-positions.js +97 -0
  87. package/modules/hall-ui/public/government.html +2 -2
  88. package/modules/hall-ui/public/hall-render.js +3 -0
  89. package/modules/hall-ui/public/idea.html +2 -2
  90. package/modules/hall-ui/public/ideas.html +2 -2
  91. package/modules/hall-ui/public/index.html +3 -3
  92. package/modules/hall-ui/public/modules.html +2 -2
  93. package/modules/hall-ui/public/my-agents.css +334 -0
  94. package/modules/hall-ui/public/my-agents.html +72 -0
  95. package/modules/hall-ui/public/my-agents.js +603 -0
  96. package/modules/hall-ui/public/my-agents.states.json +37 -0
  97. package/modules/hall-ui/public/primer.html +2 -2
  98. package/modules/hall-ui/public/profile-connections.js +40 -28
  99. package/modules/hall-ui/public/profile.html +2 -2
  100. package/modules/hall-ui/public/project-settings.html +2 -2
  101. package/modules/hall-ui/public/ranks.html +2 -2
  102. package/modules/hall-ui/public/roadmap.html +2 -2
  103. package/modules/hall-ui/public/room.css +8 -0
  104. package/modules/hall-ui/public/roster.html +2 -2
  105. package/modules/hall-ui/public/sessions.html +2 -2
  106. package/modules/hall-ui/public/settings.html +2 -2
  107. package/modules/hall-ui/public/shell.js +116 -16
  108. package/modules/hall-ui/public/street-card.js +18 -1
  109. package/modules/hall-ui/public/street-deck.css +299 -0
  110. package/modules/hall-ui/public/street-deck.js +1145 -0
  111. package/modules/hall-ui/public/street-depth.js +712 -0
  112. package/modules/hall-ui/public/street-desk.js +5 -2
  113. package/modules/hall-ui/public/street-draw.js +109 -16
  114. package/modules/hall-ui/public/street-inside.js +137 -22
  115. package/modules/hall-ui/public/street-office.js +705 -0
  116. package/modules/hall-ui/public/street.css +161 -11
  117. package/modules/hall-ui/public/street.html +41 -11
  118. package/modules/hall-ui/public/street.js +78 -66
  119. package/modules/hall-ui/public/street.states.json +129 -21
  120. package/modules/hall-ui/public/studio.html +2 -2
  121. package/modules/hall-ui/public/style.css +5 -1
  122. package/modules/hall-ui/public/task.html +2 -2
  123. package/modules/hall-ui/public/thinking.html +2 -2
  124. package/modules/hall-ui/public/tweak-editor.html +2 -2
  125. package/modules/hall-ui/public/watch.html +2 -2
  126. package/modules/hall-ui/public/watch.js +14 -1
  127. package/modules/hall-ui/public/work.html +2 -2
  128. package/modules/hall-ui/records/living-city.md +57 -0
  129. package/modules/hall-ui/records/my-agents.md +21 -0
  130. package/modules/hall-ui/records/oversight-family.md +1 -0
  131. package/modules/hall-ui/records/profile-connections.md +10 -0
  132. package/modules/hall-ui/records/reading-rooms.md +2 -0
  133. package/modules/ideas/routes/blockers.js +92 -3
  134. package/modules/lifecycle/module.json +2 -1
  135. package/modules/lifecycle/moving.js +17 -1
  136. package/modules/lifecycle/routes/moving.js +6 -0
  137. package/modules/lifecycle/routes/project-settings.js +23 -0
  138. package/modules/onboarding/approval-broadcast.js +8 -0
  139. package/modules/onboarding/routes/access-requests.js +16 -6
  140. package/modules/platform-identity/connections.js +13 -2
  141. package/modules/platform-identity/routes/connections.js +7 -1
  142. package/modules/public-landing/public/account-private.states.json +2 -2
  143. package/modules/public-landing/public/account.html +65 -5
  144. package/modules/public-landing/public/account.states.json +3 -3
  145. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  146. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  147. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
  148. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  149. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  150. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
  151. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  152. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  153. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
  154. package/modules/public-landing/public/assets/world.css +4 -4
  155. package/modules/public-landing/public/connections-empty.states.json +14 -0
  156. package/modules/public-landing/public/connections-inactive.states.json +14 -0
  157. package/modules/public-landing/public/connections.html +450 -0
  158. package/modules/public-landing/public/connections.probes.json +414 -0
  159. package/modules/public-landing/public/connections.states.json +17 -0
  160. package/modules/public-landing/public/contact.html +2 -2
  161. package/modules/public-landing/public/guild.html +2 -2
  162. package/modules/public-landing/public/index.html +2 -2
  163. package/modules/public-landing/public/invitations.html +2 -2
  164. package/modules/public-landing/public/privacy.html +2 -2
  165. package/modules/public-landing/public/projects-hall-live.states.json +0 -5
  166. package/modules/public-landing/public/projects.html +169 -6
  167. package/modules/public-landing/public/projects.probes.json +10 -6
  168. package/modules/public-landing/public/projects.states.json +13 -13
  169. package/modules/public-landing/public/terms.html +2 -2
  170. package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
  171. package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
  172. package/modules/ui-design/kit/fixtures/connections.json +22 -0
  173. package/modules/ui-design/kit/serve.js +95 -0
  174. package/package-lock.json +2 -2
  175. package/package.json +1 -1
  176. package/release-notes.json +128 -0
  177. package/scripts/discord/README.md +8 -5
  178. package/scripts/discord/post.js +12 -1
  179. package/scripts/gds/artifact-audit.js +20 -3
  180. package/scripts/gds/ask.js +164 -0
  181. package/scripts/gds/autobongos-verify.js +21 -2
  182. package/scripts/gds/government-walk.js +638 -4
  183. package/scripts/gds/pulse.js +9 -6
  184. package/scripts/gds/ship-escalation.js +22 -3
  185. package/scripts/gds/ship-finish.js +5 -4
  186. package/scripts/gds/ship-merge-gate.js +18 -6
  187. package/scripts/hall-preview/README.md +41 -1
  188. package/scripts/hall-preview/pref-writes.js +140 -0
  189. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  190. package/scripts/hall-preview/scrub-identity.js +366 -7
  191. package/scripts/hall-preview/server.js +27 -1
  192. package/scripts/render-diagrams.sh +4 -2
  193. package/src/bongos/auth-idp.js +6 -1
  194. package/src/bongos/project-settings.js +41 -0
  195. package/src/bongos/route-rank-check.js +27 -0
  196. package/src/bongos/routes/me.js +68 -7
  197. package/src/bongos/serve-internal.js +5 -1
  198. package/src/module-api.js +7 -1
  199. package/src/platform-server.js +10 -0
  200. package/tests/application_lifecycle.mjs +47 -5
  201. package/tests/conductor_main_e2e.mjs +21 -0
  202. package/tests/connections_api.mjs +18 -0
  203. package/tests/craft_mode_nav.mjs +5 -3
  204. package/tests/craft_mode_tallies.mjs +52 -16
  205. package/tests/discord_approvals.mjs +11 -0
  206. package/tests/discord_channels.mjs +20 -0
  207. package/tests/fixtures/gov-seated.mjs +64 -0
  208. package/tests/fixtures/government-ask-cards.json +1284 -0
  209. package/tests/government_act_alone_buildings.mjs +1 -0
  210. package/tests/government_agent_asks.mjs +322 -0
  211. package/tests/government_asks.mjs +408 -0
  212. package/tests/government_build_rights.mjs +253 -0
  213. package/tests/government_buildouts.mjs +382 -0
  214. package/tests/government_counts.mjs +143 -0
  215. package/tests/government_decision_rules.mjs +5 -1
  216. package/tests/government_laws.mjs +488 -0
  217. package/tests/government_laws_session.mjs +321 -0
  218. package/tests/government_leads.mjs +375 -0
  219. package/tests/government_positions.mjs +127 -0
  220. package/tests/government_presets.mjs +215 -0
  221. package/tests/government_routes.mjs +8 -1
  222. package/tests/government_routing.mjs +118 -0
  223. package/tests/government_seed.mjs +5 -0
  224. package/tests/government_treasury.mjs +394 -0
  225. package/tests/governor_docket.mjs +26 -2
  226. package/tests/hall_buildouts.mjs +208 -0
  227. package/tests/hall_city.mjs +14 -6
  228. package/tests/hall_city_living.mjs +617 -0
  229. package/tests/hall_gov_counts.mjs +336 -0
  230. package/tests/hall_gov_living.mjs +636 -0
  231. package/tests/hall_gov_real_site.mjs +655 -0
  232. package/tests/hall_matter_card.mjs +6 -1
  233. package/tests/hall_my_agents.mjs +481 -0
  234. package/tests/hall_nav.mjs +107 -4
  235. package/tests/hall_page_gate_map.mjs +4 -0
  236. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  237. package/tests/hall_preview_scrub.mjs +59 -1
  238. package/tests/hall_preview_security.mjs +169 -0
  239. package/tests/hall_reading_world.mjs +44 -0
  240. package/tests/hall_roster_world.mjs +6 -2
  241. package/tests/hall_street.mjs +18 -2
  242. package/tests/hall_street_amend.mjs +1 -1
  243. package/tests/hall_street_deck.mjs +609 -0
  244. package/tests/hall_street_depth.mjs +559 -0
  245. package/tests/hall_street_desk.mjs +1 -1
  246. package/tests/hall_street_inside.mjs +29 -5
  247. package/tests/hall_tweak_editor.mjs +4 -1
  248. package/tests/hub_account_page.mjs +94 -3
  249. package/tests/hub_connections_page.mjs +507 -0
  250. package/tests/hub_invite_badge.mjs +1 -1
  251. package/tests/idp_device_exchange.mjs +18 -0
  252. package/tests/landing_legal_pages.mjs +8 -5
  253. package/tests/landing_page.mjs +23 -4
  254. package/tests/nav_permission_atoms.mjs +4 -1
  255. package/tests/profile_connections_ui.mjs +88 -25
  256. package/tests/projects_hub.mjs +15 -0
  257. package/tests/projects_hub_pre_uat.mjs +4 -2
  258. package/tests/render_check.mjs +2 -2
  259. package/tests/ship_broadcast_routing.mjs +57 -16
  260. package/tests/ship_broadcast_server.mjs +40 -7
  261. package/tests/ship_merge_gate.mjs +19 -0
  262. package/tests/ui_design_kit.mjs +2 -2
  263. package/tests/watch_applications_queue.mjs +23 -0
  264. package/tests/wizard_physics.mjs +1 -1
  265. package/tests/wizard_physics_more.mjs +133 -0
@@ -276,7 +276,7 @@ async function bootPage({ feeds, posts = {} }) {
276
276
  };
277
277
  const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite });
278
278
  ctx.globalThis = ctx;
279
- for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-desk.js', 'street.js']) vm.runInContext(read(f), ctx);
279
+ for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-depth.js', 'street-desk.js', 'street.js']) vm.runInContext(read(f), ctx);
280
280
  await window.GovStreetPage.ready;
281
281
  return { window, byId, calls, focused };
282
282
  }
@@ -409,6 +409,8 @@ const SOURCE_FIXTURES = () => {
409
409
  { id: 'gate_approvals:88', source: 'gate_approvals', building: 'release', rule: 'decide', state: 'needs', title: 'PR 88 waits at the gate', href: '/gate', ref: { type: 'pull_request', id: '88' } },
410
410
  { id: 'autobongos_fence:project', source: 'autobongos_fence', building: 'automation', rule: 'decide', state: 'needs', title: 'Unattended building is off', href: '/autobongos', ref: { type: 'autonomy_fence', id: 'project' }, acts: [{ key: 'switch_on' }] },
411
411
  { id: 'module_rechecks:7', source: 'module_rechecks', building: 'release', rule: 'rule', state: 'needs', title: 'image-tools 1.2.0 failed its security check', href: '/modules', ref: { type: 'module_version', id: 'image-tools@1.2.0' }, acts: [{ key: 'recheck', options: [{ key: 'fixed', label: 'fixed since' }] }] },
412
+ // task 1004712: a dry pot's borrow from the reserve
413
+ { id: 'treasury_spending:5', source: 'treasury_spending', building: 'treasury', rule: 'decide', state: 'needs', title: 'Sign-up: its pot is dry', href: '/goal/7', ref: { type: 'pot_borrow', id: '5' }, acts: [{ key: 'answer', options: [{ key: 'lend', label: 'Lend $400' }, { key: 'half', label: 'Lend $200' }, { key: 'no', label: 'Say no' }] }] },
412
414
  ];
413
415
  const d = docket(ms);
414
416
  d.desks = [{ key: 'release.core', building: 'release', status: 'live', rows: [{ id: 'release.core:41', desk: 'release.core', building: 'release', name: 'Core 1.21.20', fact: 'not checked yet', facts: [], over: 'core_update', ref: { type: 'core_pin', id: '41' }, acts: [{ key: 'check', options: [{ key: '1.21.20', label: '1.21.20' }] }] }] }];
@@ -444,6 +446,9 @@ test('every docket source kind opens a card with the desk row\'s acts when the v
444
446
  // a module version is checked again through the reassess route, with a one-tap reason
445
447
  const rc = D.cardFor('module_rechecks:7', d, { now: NOW });
446
448
  assert.deepEqual(C.request(rc.row.card, 'recheck', { option: 'fixed', label: 'fixed since' }), { method: 'POST', url: '/api/bongos/store/modules/image-tools/versions/1.2.0/reassess', body: { reason: 'Asked on the street: fixed since.' } });
449
+ // a dry pot's borrow is answered at the treasury route, the chip's key as the answer (task 1004712)
450
+ const tb = D.cardFor('treasury_spending:5', d, { now: NOW });
451
+ assert.deepEqual(C.request(tb.row.card, 'answer', { option: 'half', label: 'Lend $200' }), { method: 'POST', url: '/api/bongos/government/treasury/borrows/5/answer', body: { key: 'half' } });
447
452
  // the review queue's pin opens its first piece of work, with that work's acts
448
453
  const q = D.cardFor('queue', d, { room: r, now: NOW });
449
454
  assert.equal(q.key, 'criteria_uat:3');
@@ -0,0 +1,481 @@
1
+ // tests/hall_my_agents.mjs — MY AGENTS and "governing is a position" (task 1004685,
2
+ // the living-city prototype; round-3 boards 6-8).
3
+ //
4
+ // What it pins:
5
+ // 1. gov-positions.js: the seats a viewer holds are /me's server-resolved
6
+ // `positions` block (task 1004713) and nothing else; ?as=nobody only ever
7
+ // takes away; unknown is unknown, never "none"; and it is pure.
8
+ // 2. my-agents.js buildView over the real fixtures: real rows carry /me's own
9
+ // values, laws lock exactly the rows they name under each preset, an
10
+ // `overlay` handed in changes nothing (task 1004717 deleted it), the session-start
11
+ // panel quotes the real contract, and a habit is a real non-default choice.
12
+ // 3. every write goes to a route the builder-settings module really mounts, and
13
+ // the harness recorder answers it with the real shapers.
14
+ // 4. craft-mode.js: no seat → no Governing anywhere, a stored Governing reads as
15
+ // no mode; a seat names the mode; unknown is today's picker.
16
+ // 5. the page itself: in the hall's family, no prototype key, the
17
+ // fifteen tokens only, dark twins byte-identical, motion on transform only and
18
+ // dead under the kill switch.
19
+ //
20
+ // Run: node --test --test-reporter=tap tests/hall_my_agents.mjs
21
+
22
+ import { test } from 'node:test';
23
+ import assert from 'node:assert/strict';
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import vm from 'node:vm';
27
+ import { createRequire } from 'node:module';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ const require = createRequire(import.meta.url);
31
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
32
+ const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
33
+ const FX = path.join(ROOT, 'scripts', 'hall-preview', 'fixtures');
34
+ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
35
+ const fx = (name) => JSON.parse(fs.readFileSync(path.join(FX, name + '.json'), 'utf8'));
36
+
37
+ const GovPositions = require(path.join(HALL, 'gov-positions.js'));
38
+ const GovLiving = require(path.join(HALL, 'gov-living.js'));
39
+ const MyAgents = require(path.join(HALL, 'my-agents.js'));
40
+ const LIBS = { GovLiving, GovPositions };
41
+ const NOW = Date.parse('2026-10-09T14:00:00.000Z');
42
+ // the kind of thing the deleted prototype overlay said: settings no table holds,
43
+ // invented laws and a preset. Handed in, none of it may show.
44
+ const OLD_OVERLAY = {
45
+ personal_settings: [{ key: 'when_stuck', label: 'When stuck', values: ['ask', 'try'], value: 'ask' }, { key: 'agents_at_once', label: 'Agents at once', values: [1, 2, 3], value: 3 }],
46
+ laws: [{ id: 'one-agent', text: 'One agent at a time', locks: [{ setting: 'wandering', max: 'focused' }] }],
47
+ strictness: { current: 'factory', presets: [{ key: 'factory', name: 'Factory', line: 'x' }] },
48
+ leads: [{ building: 'planning', lead: '1000025', charter: { scope: 'x' } }],
49
+ };
50
+
51
+ const viewOf = (over = {}) => MyAgents.buildView({
52
+ me: fx('me'), constitution: fx('government__constitution'), presets: fx('government__presets'),
53
+ skills: fx('me__skill-prefs'), specialities: fx('specialities'), project: 'Cloud Bongos', now: NOW, ...over,
54
+ }, LIBS);
55
+ const rows = (v) => v.groups.flatMap((g) => g.rows);
56
+ const row = (v, key) => rows(v).find((r) => r.key === key);
57
+ const codeOf = (src) => src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:'"`\\])\/\/.*$/gm, '$1');
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // 1. the seats
61
+ // ---------------------------------------------------------------------------
62
+
63
+ const lead = (building) => ({ kind: 'lead', seat: 'lead:' + building, building, title: GovPositions.BUILDING_NAMES[building] + ' Lead' });
64
+ const withSeats = (held, builder = { id: '424242', rank: 'thetes' }) => ({ builder, positions: { held, title: held.length ? held[0].title : null, has_position: held.length > 0 } });
65
+
66
+ test('SEATS: the seats are /me\'s positions block, exactly as the server resolved them', () => {
67
+ const me = fx('me');
68
+ const p = GovPositions.positionsFor({ me });
69
+ assert.equal(p.known, true);
70
+ assert.deepEqual(p.positions.map((s) => s.kind), me.positions.held.map((s) => s.kind));
71
+ assert.equal(p.title, me.positions.held[0].title, 'the first seat names the Governing mode');
72
+ assert.ok(p.positions.every((s) => s.source === 'real'), 'every seat is the server\'s');
73
+ });
74
+
75
+ test('SEATS: a Lead is named by their seat', () => {
76
+ for (const b of Object.keys(GovPositions.BUILDING_NAMES)) {
77
+ const p = GovPositions.positionsFor({ me: withSeats([lead(b)]) });
78
+ assert.equal(p.title, GovPositions.BUILDING_NAMES[b] + ' Lead', b);
79
+ assert.equal(p.has_position, true);
80
+ }
81
+ });
82
+
83
+ test('SEATS: nothing is re-derived here — no rank, craft, constitution or overlay makes a seat', () => {
84
+ const none = withSeats([], { id: '1000025', rank: 'archon', preferred_disciplines: ['governing'] });
85
+ const p = GovPositions.positionsFor({ me: none, overlay: OLD_OVERLAY, constitution: fx('government__constitution') });
86
+ assert.equal(p.known, true);
87
+ assert.equal(p.has_position, false, 'the server said none: an archon who picked every craft, named Planning Lead by an overlay, still holds none');
88
+ const odd = GovPositions.positionsFor({ me: withSeats([{ kind: 'committee', title: 'Made up' }, lead('review')]) });
89
+ assert.deepEqual(odd.positions.map((s) => s.kind), ['lead'], 'a kind the server does not name is dropped');
90
+ });
91
+
92
+ test('SEATS: unknown is unknown — /me with no positions block is not "holds none"', () => {
93
+ const me = fx('me');
94
+ delete me.positions;
95
+ assert.equal(GovPositions.positionsFor({ me }).known, false);
96
+ assert.equal(GovPositions.positionsFor({ me: { ...me, positions: null } }).known, false, 'a null block (unreadable) is unknown');
97
+ assert.equal(GovPositions.positionsFor({}).known, false, 'no viewer, no answer');
98
+ });
99
+
100
+ test('SEATS: ?as=nobody takes every seat away and never raises a rank', () => {
101
+ const p = GovPositions.positionsFor({ me: fx('me'), as: 'nobody' });
102
+ assert.equal(p.known, true);
103
+ assert.equal(p.has_position, false);
104
+ assert.equal(p.sim_rank, 'thetes', 'an archon is shown as a thetes');
105
+ assert.notEqual(p.viewer_id, fx('me').builder.id, 'the stand-in is nobody');
106
+ const xenos = GovPositions.positionsFor({ me: withSeats([lead('planning')], { id: '9', rank: 'xenos' }), as: 'nobody' });
107
+ assert.equal(xenos.sim_rank, 'xenos', 'a xenos stand-in stays a xenos');
108
+ assert.equal(GovPositions.positionsFor({ me: fx('me'), as: 'archon' }).as, null, 'only "nobody" is honoured');
109
+ });
110
+
111
+ test('SEATS: the kinds the hall reads are the kinds the server names', () => {
112
+ const { KINDS } = require(path.join(ROOT, 'modules', 'government', 'positions.js'));
113
+ assert.deepEqual([...GovPositions.KINDS], [...KINDS]);
114
+ });
115
+
116
+ test('SEATS: gov-positions.js is pure — no window, document, fetch or clock', () => {
117
+ const code = codeOf(read('gov-positions.js'));
118
+ const tail = "typeof window !== 'undefined' ? window : globalThis";
119
+ assert.ok(code.includes(tail));
120
+ const body = code.replace(tail, '');
121
+ for (const [name, re] of Object.entries({ window: /\bwindow\b/, document: /\bdocument\b/, fetch: /\bfetch\b/, 'Date.now': /Date\.now/, storage: /Storage/ })) {
122
+ assert.doesNotMatch(body, re, `${name} must not appear in gov-positions.js`);
123
+ }
124
+ });
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // 2. the view
128
+ // ---------------------------------------------------------------------------
129
+
130
+ test('VIEW: a real row carries the viewer\'s own value from /me', () => {
131
+ const v = viewOf();
132
+ const me = fx('me');
133
+ for (const f of me.interaction.fields) assert.equal(row(v, f.field).effective, me.interaction.effective[f.field], f.field);
134
+ assert.equal(row(v, 'wandering').value, me.wandering.effective_key, 'the viewer\'s own wandering is the real one');
135
+ for (const s of me.model_allocation.slots) assert.equal(row(v, 'model.' + s.slot).effective, me.model_allocation.effective[s.slot]);
136
+ assert.equal(row(v, 'render').effective, me.render.level);
137
+ assert.match(row(v, 'skills').fact, new RegExp('^' + fx('me__skill-prefs').effective.length + ' skills'));
138
+ assert.equal(row(v, 'sounds').fact, 'all off for now', 'sounds are off by the owner\'s call');
139
+ assert.ok(rows(v).every((r) => r.name.split(/\s+/).length <= 3), 'a row name is three words at most');
140
+ });
141
+
142
+ test('VIEW: no row is prototype data, and only a real law locks one (task 1004714)', () => {
143
+ const v = viewOf();
144
+ assert.equal(rows(v).some((r) => 'proto' in r), false, 'no row carries a prototype flag');
145
+ const ov = viewOf({ overlay: OLD_OVERLAY });
146
+ for (const s of OLD_OVERLAY.personal_settings) assert.equal(row(ov, s.key), undefined, s.key + ' (no table holds it) is not a row');
147
+ assert.equal(ov.counts.locked, 0, 'the hub has no law in force: nothing is locked, whatever an overlay\'s preset');
148
+ assert.equal(row(ov, 'wandering').effective, fx('me').wandering.effective_key, 'an invented law does not cap wandering');
149
+ assert.equal(row(v, 'wandering').advised, undefined, 'nor advise it');
150
+ for (const preset of ['open', 'guild', 'factory']) {
151
+ assert.equal(viewOf({ preview: preset }).counts.locked, 0, 'previewing ' + preset + ' locks no row');
152
+ }
153
+ assert.ok(rows(v).every((r) => r.lock === null || r.lock.real), 'every lock there is is real');
154
+ assert.deepEqual(v._sources.groups, 'real');
155
+ });
156
+
157
+ // The server's presets read (GET /government/presets) over a law book, as the
158
+ // route builds it — the pure half of modules/government/presets.js.
159
+ const Presets = require(path.join(ROOT, 'modules', 'government', 'presets.js'));
160
+ const bookOf = (key) => {
161
+ const sw = Presets.switchProposal(key, []);
162
+ return sw.proposal.enact.map((l, i) => ({ id: String(i + 1), ...l, repealed_at: null }));
163
+ };
164
+
165
+ test('VIEW: the strictness strip is real (task 1004715): GET /government/presets, counted against your rows', () => {
166
+ const v = viewOf();
167
+ assert.equal(v._sources.strict, 'real');
168
+ assert.equal(v.strict.real, true);
169
+ assert.equal(v.strict.current, null, 'the hub has no law in force: no preset is');
170
+ assert.equal(v.strict.locked, 0, 'and nothing is locked');
171
+ assert.equal(v.strict.total, v.counts.total, 'over the same rows the desk shows');
172
+ const n = Object.fromEntries(v.strict.presets.map((p) => [p.key, p.locks]));
173
+ assert.deepEqual(n, { open: 0, guild: 1, factory: 4 }, 'open locks nothing; guild caps off-task; factory adds the three model caps');
174
+ const factory = viewOf({ preview: 'factory' });
175
+ assert.equal(factory.strict.previewing, true);
176
+ assert.equal(factory.strict.locked, 4);
177
+ assert.equal(factory.strict.locked, factory.strict.pips.filter(Boolean).length, 'one lit pip per row it would hold');
178
+ assert.equal(factory.counts.locked, 0, 'a preview locks no row');
179
+ assert.equal(factory.strict.canSwitch, true, 'a builder who may open a board item may put it to a vote');
180
+ assert.equal(factory.strict.filed, null);
181
+ assert.equal(viewOf({ preview: 'factory', switched: { factory: '77' } }).strict.filed, '77', 'once filed, the strip links the vote');
182
+ const noPerm = viewOf({ preview: 'factory', me: { ...fx('me'), permissions: { 'board.vote.cast': true } } });
183
+ assert.equal(noPerm.strict.canSwitch, false, 'without board.item.open there is no switch');
184
+ assert.equal(v.strict.canSwitch, false, 'no switch without a preview');
185
+ assert.equal(viewOf({ preview: 'no-such' }).strict.previewing, false, 'an unknown preset is no preview');
186
+ });
187
+
188
+ test('VIEW: the preset in force is the one the laws in force amount to; previewing it is no preview', () => {
189
+ const guild = viewOf({ presets: Presets.presetsView(bookOf('guild')) });
190
+ assert.equal(guild.strict.current.key, 'guild');
191
+ assert.equal(guild.strict.locked, 1, 'guild holds off-task');
192
+ assert.equal(guild.strict.pips.filter(Boolean).length, 1);
193
+ assert.equal(viewOf({ presets: Presets.presetsView(bookOf('guild')), preview: 'guild' }).strict.previewing, false);
194
+ assert.equal(viewOf({ presets: null }).strict, null, 'no presets read, no strip');
195
+ });
196
+
197
+ test('VIEW: an overlay handed in changes nothing — the whole view reads the same', () => {
198
+ assert.deepEqual(viewOf({ overlay: OLD_OVERLAY }), viewOf(), 'the overlay is not read');
199
+ assert.equal('governed' in viewOf(), false, 'and nothing says the page is "governed" by one');
200
+ });
201
+
202
+ test('VIEW: what the agent reads first — the project\'s REAL laws (GET /me laws, task 1004707), then the REAL contract text', () => {
203
+ const v = viewOf();
204
+ const me = fx('me');
205
+ // the hub has passed no law: nothing is sent, and an invented law is never "read"
206
+ assert.deepEqual(viewOf({ overlay: OLD_OVERLAY }).reads.laws, [], 'an overlay\'s laws are not what an agent reads');
207
+ assert.deepEqual(v.reads.laws, []);
208
+ assert.equal(v.reads.sent, false);
209
+ assert.equal(v._sources['reads.laws'], 'real');
210
+ // a law in force: exactly the book GET /me carries, in its numbering, sent first
211
+ const withLaw = { ...fx('me'), laws: { inject: true, contract: 'Project laws — …\n1. Ask like a human', laws: [{ id: '11', no: 1, text: 'Ask like a human', effect_on_ai: 'One short card.', locks: [] }], locks: [] } };
212
+ const lv = viewOf({ me: withLaw });
213
+ assert.deepEqual(lv.reads.laws, [{ id: '11', no: 1, text: 'Ask like a human', effect: 'One short card.' }]);
214
+ assert.equal(lv.reads.sent, true);
215
+ const said = v.reads.mine.find((x) => x.key === 'interaction');
216
+ assert.equal(said.text, me.interaction.contract, 'the interaction profile is quoted, not paraphrased');
217
+ assert.equal(said.sent, me.interaction.inject);
218
+ const wander = v.reads.mine.find((x) => x.key === 'wandering');
219
+ assert.equal(wander.sent, false, 'exploratory is not injected (wandering-prefs.js inject:false)');
220
+ assert.match(wander.note, /^Not sent/);
221
+ });
222
+
223
+ test('VIEW: a law in force (GET /me laws.locks) locks the row it names, as real, numbered in the real book', () => {
224
+ const me = fx('me');
225
+ me.interaction.effective = { ...me.interaction.effective, verbosity: 'concise' };
226
+ me.laws = {
227
+ inject: true, contract: 'Project laws — …', laws: [{ id: '11', no: 1, text: 'Ask like a human', effect_on_ai: null, locks: [{ setting: 'verbosity', value: 'concise' }] }],
228
+ locks: [{ setting: 'verbosity', law_id: '11', law_no: 1, law_text: 'Ask like a human', value: 'concise', route: 'interaction', enforced: true, from: 'detailed', to: 'concise' }],
229
+ };
230
+ for (const overlay of [OLD_OVERLAY, null]) {
231
+ const r = row(viewOf({ me, overlay }), 'verbosity');
232
+ assert.equal(r.locked, true);
233
+ assert.deepEqual(r.lock, { law_id: 'law:11', no: 1, text: 'Cloud Bongos: Ask like a human · Law 1', real: true });
234
+ assert.equal(r.fact, 'short', 'the law\'s value');
235
+ assert.equal(r.yours, 'detailed', 'and theirs, still said');
236
+ }
237
+ });
238
+
239
+ // a law that only CAPS a setting (1004707 locked the whole row for it)
240
+ const meCapped = () => {
241
+ const me = fx('me');
242
+ me.wandering = { ...me.wandering, effective_key: 'focused' };
243
+ me.laws = {
244
+ inject: true, contract: 'Project laws — …', laws: [{ id: '12', no: 1, text: 'Stay near the task', effect_on_ai: null, locks: [{ setting: 'wandering', max: 'balanced' }] }],
245
+ locks: [{ setting: 'wandering', law_id: '12', law_no: 1, law_text: 'Stay near the task', max: 'balanced', route: 'wandering', enforced: true, from: 'exploratory', to: 'focused', allowed: ['locked', 'focused', 'balanced'] }],
246
+ };
247
+ return me;
248
+ };
249
+
250
+ test('VIEW: a law that only CAPS a setting offers the values under the cap, not a locked row (task 1004714)', () => {
251
+ for (const overlay of [OLD_OVERLAY, null]) {
252
+ const r = row(viewOf({ me: meCapped(), overlay }), 'wandering');
253
+ assert.equal(r.locked, false, 'a cap is not a lock');
254
+ assert.deepEqual(r.options.map((o) => o.value), ['locked', 'focused', 'balanced'].filter((k) => fx('me').wandering.levels.some((l) => l.key === k && l.allowed !== false)),
255
+ 'the chips are the values under the cap, in order');
256
+ assert.equal(r.options.find((o) => o.pressed).value, 'focused', 'the effective value is the one pressed');
257
+ assert.deepEqual(r.cap, { law_id: 'law:12', no: 1, text: 'Cloud Bongos: Stay near the task · Law 1', max: 'balanced', real: true });
258
+ assert.equal(r.yours, 'roams', 'and their own choice, past the cap, is still said');
259
+ }
260
+ // a cap that leaves a single value is a lock in all but name
261
+ const me = meCapped();
262
+ me.laws.locks[0].allowed = ['locked'];
263
+ me.wandering.effective_key = 'locked';
264
+ assert.equal(row(viewOf({ me }), 'wandering').locked, true);
265
+ // a server that names no allowed values: the safe reading is the whole-row lock
266
+ const old = meCapped();
267
+ delete old.laws.locks[0].allowed;
268
+ assert.equal(row(viewOf({ me: old }), 'wandering').locked, true);
269
+ });
270
+
271
+ test('VIEW: a habit is a real non-default choice no law already holds, offered to who may propose a law', () => {
272
+ const v = viewOf();
273
+ const me = fx('me');
274
+ assert.equal(v.canPropose, true, 'the fixture viewer may open a board item');
275
+ const expected = ['machinery'].concat(me.wandering.override_level != null ? ['wandering'] : []);
276
+ assert.deepEqual(v.habits.map((h) => h.key), expected);
277
+ const mach = v.habits.find((h) => h.key === 'machinery');
278
+ assert.equal(mach.law, 'Agents hide the machinery');
279
+ assert.deepEqual(mach.lock, { setting: 'machinery', value: 'quiet' }, 'the law would lock the setting to their value');
280
+ assert.equal(viewOf({ proposed: { machinery: '77' } }).habits.find((h) => h.key === 'machinery').item, '77', 'a proposed habit points at its vote');
281
+ assert.equal(viewOf({ me: meCapped() }).habits.some((h) => h.key === 'wandering'), false, 'a setting a law already caps is no habit');
282
+ const noPerm = fx('me');
283
+ noPerm.permissions = { ...noPerm.permissions, 'board.item.open': false };
284
+ assert.deepEqual(viewOf({ me: noPerm }).habits, [], 'no right to open a board item, no Make it a law');
285
+ assert.equal(viewOf({ me: noPerm }).canPropose, false);
286
+ const unknown = fx('me');
287
+ delete unknown.permissions;
288
+ assert.equal(viewOf({ me: unknown }).canPropose, false, 'an unreadable permission map offers nothing');
289
+ });
290
+
291
+ test('PROPOSE: Make it a law files a valid laws vote — the server\'s own validator accepts it', () => {
292
+ const laws = require(path.join(ROOT, 'modules', 'government', 'laws.js'));
293
+ const routes = fs.readFileSync(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'), 'utf8');
294
+ assert.match(routes, /router\.post\('\/government\/board\/laws'/, 'the route is mounted');
295
+ const v = viewOf();
296
+ for (const h of v.habits) {
297
+ const body = MyAgents.proposalFor(h, 'someone');
298
+ const checked = laws.validateLawsProposal({ enact: body.enact });
299
+ assert.equal(checked.ok, true, h.key + ': ' + JSON.stringify(checked));
300
+ assert.deepEqual(checked.proposal.enact[0].locks, [h.lock]);
301
+ assert.equal(typeof body.rationale_md, 'string');
302
+ }
303
+ assert.match(read('my-agents.js'), /'\/government\/board\/laws'/, 'the page posts there');
304
+ assert.doesNotMatch(codeOf(read('my-agents.js')), /'\/inbox'/, 'not an idea any more');
305
+ });
306
+
307
+ test('VIEW: buildView is pure and leaves its inputs alone', () => {
308
+ const input = { me: fx('me'), constitution: fx('government__constitution'), project: 'P', now: NOW };
309
+ const before = JSON.stringify(input);
310
+ const one = MyAgents.buildView(input, LIBS);
311
+ assert.equal(JSON.stringify(input), before);
312
+ assert.deepEqual(MyAgents.buildView(input, LIBS), one);
313
+ });
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // 3. the writes
317
+ // ---------------------------------------------------------------------------
318
+
319
+ test('WRITES: every real row writes to a route builder-settings really mounts', () => {
320
+ const routes = fs.readFileSync(path.join(ROOT, 'modules', 'builder-settings', 'routes', 'settings.js'), 'utf8');
321
+ const mounted = new Set([...routes.matchAll(/router\.patch\('([^']+)'/g)].map((m) => m[1]));
322
+ const me = fx('me');
323
+ const v = viewOf();
324
+ for (const r of rows(v).filter((x) => x.control === 'chips')) {
325
+ const w = MyAgents.writeFor(r.key, r.options[0].value, { me });
326
+ assert.ok(w, `${r.key} has a write`);
327
+ assert.equal(w.method, 'PATCH');
328
+ assert.ok(mounted.has(w.path), `${w.path} is mounted`);
329
+ }
330
+ assert.deepEqual(MyAgents.writeFor('wandering', 'focused', { me }).body, { wandering: 1 }, 'wandering writes its level number');
331
+ assert.equal(MyAgents.writeFor('on_stuck', 'skip', { me }), null, 'a setting no table holds has no route');
332
+ assert.ok(mounted.has(MyAgents.writeFor('skill.builder-ship', 'opus', { me }).path));
333
+ });
334
+
335
+ test('WRITES: the harness recorder answers with the real shapers and lays the result over later reads', () => {
336
+ const { createPrefRecorder } = require(path.join(ROOT, 'scripts', 'hall-preview', 'pref-writes.js'));
337
+ const logged = [];
338
+ const rec = createPrefRecorder({ log: (l) => logged.push(l) });
339
+ const me = fx('me');
340
+ const ok = rec.write('me/interaction', { verbosity: 'detailed' }, { me, skillFixture: fx('me__skill-prefs') });
341
+ assert.equal(ok.status, 200);
342
+ assert.equal(ok.body.effective.verbosity, 'detailed');
343
+ assert.match(ok.body.contract, /Verbosity: detailed/, 'the contract text is the real one, regenerated');
344
+ assert.equal(ok.body.effective.machinery, 'quiet', 'the stored fields survive');
345
+ assert.equal(rec.write('me/interaction', { verbosity: 'loud' }, { me }).status, 422, 'a bad value is the real 422');
346
+ const after = rec.overlay('me', me);
347
+ assert.equal(after.interaction.effective.verbosity, 'detailed', 'GET /me shows it');
348
+ assert.notEqual(after.render.level, me.render.level, 'and the derived card level moves with it, as the real one does');
349
+ assert.equal(rec.write('me/wandering', { wandering: 1 }, { me }).body.effective_key, 'focused');
350
+ assert.equal(rec.write('me/skill-prefs', { 'builder-ship': 'opus' }, { me }).body.prefs['builder-ship'], 'opus');
351
+ assert.equal(rec.handles('PATCH', 'me/interaction'), true);
352
+ assert.equal(rec.handles('POST', 'inbox'), false, 'only the five pref routes are recorded');
353
+ assert.ok(logged.every((l) => /^ {2}recorded: PATCH me\//.test(l)));
354
+ });
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // 4. the craft mode
358
+ // ---------------------------------------------------------------------------
359
+
360
+ function bootCraft({ session = {} } = {}) {
361
+ const el = (html = '') => {
362
+ const attrs = {};
363
+ return {
364
+ _html: html, dataset: {}, style: {}, children: [],
365
+ classList: { add() {}, remove() {}, toggle() {}, contains() { return false; } },
366
+ setAttribute(k, v) { attrs[k] = String(v); }, getAttribute(k) { return k in attrs ? attrs[k] : null; },
367
+ hasAttribute(k) { return k in attrs; }, removeAttribute(k) { delete attrs[k]; },
368
+ addEventListener() {}, appendChild(c) { this.children.push(c); return c; }, removeChild() {},
369
+ querySelector() { return null; }, querySelectorAll() { return []; }, focus() {}, contains() { return false; },
370
+ set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; },
371
+ };
372
+ };
373
+ const root = el();
374
+ const body = el();
375
+ const mk = (o) => ({ getItem: (k) => (k in o ? o[k] : null), setItem: (k, v) => { o[k] = String(v); }, removeItem: (k) => { delete o[k]; } });
376
+ const document = {
377
+ documentElement: root, body, activeElement: null,
378
+ getElementById: () => null, querySelector: () => null, querySelectorAll: () => [],
379
+ createElement: () => el(), addEventListener() {}, removeEventListener() {}, dispatchEvent() { return true; },
380
+ };
381
+ const window = { location: { search: '' }, matchMedia: () => ({ matches: false }), sessionStorage: mk(session), localStorage: mk({}) };
382
+ const sandbox = { window, document, console, CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } } };
383
+ sandbox.globalThis = sandbox;
384
+ vm.createContext(sandbox);
385
+ vm.runInContext(read('craft-mode.js'), sandbox, { filename: 'craft-mode.js' });
386
+ return { craft: window.OTBCraftMode, root, body, session };
387
+ }
388
+ const seatOf = (has, title) => ({ known: true, has_position: has, title: has ? title : null });
389
+
390
+ test('CRAFT: with no seat, Governing is not offered anywhere', () => {
391
+ const b = bootCraft();
392
+ b.craft.setPositions(seatOf(false));
393
+ assert.match(b.craft.switchHtml(), /data-craft-opt="governing"[^>]* hidden>/, 'the switch hides it');
394
+ b.craft.choose('governing');
395
+ assert.equal(b.root.dataset.craft, undefined, 'and choosing it does nothing');
396
+ b.craft.open();
397
+ const picker = b.body.children[0];
398
+ assert.ok(picker, 'the picker opened');
399
+ assert.doesNotMatch(picker.innerHTML, /data-craft-pick="governing"/, 'no Governing door');
400
+ assert.match(picker.innerHTML, /data-doors="3"/, 'three doors, laid out as three');
401
+ });
402
+
403
+ test('CRAFT: a stored Governing reads as no mode for a builder known to hold no seat', () => {
404
+ const b = bootCraft({ session: { 'hall-craft': 'governing' } });
405
+ assert.equal(b.craft.current(), 'governing', 'unknown seats: today\'s behaviour');
406
+ b.craft.setPositions(seatOf(false));
407
+ assert.equal(b.craft.current(), null);
408
+ assert.equal(b.session['hall-craft'], undefined, 'and the stored choice is dropped');
409
+ });
410
+
411
+ test('CRAFT: a seat names the Governing mode', () => {
412
+ const b = bootCraft();
413
+ b.craft.setPositions(seatOf(true, 'Treasury Lead'));
414
+ assert.equal(b.craft.seatName(), 'Treasury Lead');
415
+ assert.match(b.craft.switchHtml(), /data-craft-opt="governing"[^>]*>[\s\S]*?Treasury Lead<\/span>/);
416
+ b.craft.choose('governing');
417
+ assert.equal(b.root.dataset.craft, 'governing');
418
+ b.craft.open();
419
+ assert.match(b.body.children[0].innerHTML, /craft-door__name">Treasury Lead</);
420
+ assert.match(b.body.children[0].innerHTML, /data-doors="4"/);
421
+ });
422
+
423
+ test('CRAFT: unknown seats are today\'s picker — four doors and a free Governing', () => {
424
+ const b = bootCraft();
425
+ b.craft.setPositions(null);
426
+ assert.equal(b.craft.seatName(), null);
427
+ assert.doesNotMatch(b.craft.switchHtml(), / hidden>/);
428
+ assert.match(b.craft.switchHtml(), />Governing<\/span>/);
429
+ b.craft.setPositions({ known: false, has_position: false });
430
+ b.craft.choose('governing');
431
+ assert.equal(b.root.dataset.craft, 'governing', 'an unknown answer never takes the mode away');
432
+ });
433
+
434
+ // ---------------------------------------------------------------------------
435
+ // 5. the page
436
+ // ---------------------------------------------------------------------------
437
+
438
+ test('PAGE: in the hall family, with no prototype key', () => {
439
+ const html = read('my-agents.html');
440
+ assert.match(html, /<body data-page="my-agents"/);
441
+ const sheets = [...html.matchAll(/href="\/builders\/([a-z-]+\.css)/g)].map((m) => m[1]);
442
+ assert.deepEqual(sheets, ['style.css', 'oversight.css', 'my-agents.css'], 'rides the oversight archetype, linked before its own sheet');
443
+ const scripts = [...html.matchAll(/src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
444
+ assert.deepEqual(scripts.slice(0, 3), ['shell-trail', 'craft-mode', 'shell']);
445
+ for (const s of ['gov-living', 'gov-positions', 'my-agents']) assert.ok(scripts.includes(s), `loads ${s}.js`);
446
+ assert.doesNotMatch(html, /ma-key|data-proto|Prototype data/, 'nothing on the page is prototype data, so there is no key');
447
+ assert.doesNotMatch(read('my-agents.js'), /governor-overlay/, 'and it reads no overlay');
448
+ assert.match(html, /id="hall-main"/);
449
+ assert.ok(fs.existsSync(path.join(HALL, 'my-agents.states.json')), 'the kit can render it');
450
+ });
451
+
452
+ test('PAGE: the sheet spends the fifteen tokens only, and no :root', () => {
453
+ const css = codeOf(read('my-agents.css'));
454
+ assert.doesNotMatch(css, /(^|[},])\s*:root\s*\{/, 'no page :root');
455
+ assert.doesNotMatch(css, /#[0-9a-f]{3,8}\b/i, 'no hex literal');
456
+ assert.doesNotMatch(css, /\brgba?\(|\bhsla?\(/, 'no rgb/hsl literal');
457
+ assert.doesNotMatch(css, /--accent\b(?!-ink)/, 'the accent is not spent here (salmon means "needs you")');
458
+ });
459
+
460
+ test('PAGE: the dark twins are byte-identical, and motion is transform-only and killed', () => {
461
+ const css = read('my-agents.css');
462
+ const twin = (re) => { const m = re.exec(css); assert.ok(m, 'twin found'); return m[1].replace(/\s+/g, ' ').trim(); };
463
+ assert.equal(
464
+ twin(/html\[data-theme="dark"\] body\[data-page="my-agents"\] \{([^}]*)\}/),
465
+ twin(/html:not\(\[data-theme\]\) body\[data-page="my-agents"\] \{([^}]*)\}/));
466
+ for (const m of css.matchAll(/@keyframes [\w-]+ \{([\s\S]*?)\}\s*\}/g)) {
467
+ const props = [...m[1].matchAll(/([a-z-]+)\s*:/g)].map((x) => x[1]);
468
+ assert.ok(props.every((p) => p === 'transform'), `a keyframe animates transform only (${props.join(', ')})`);
469
+ }
470
+ const transitions = [...css.matchAll(/transition:\s*([^;]+);/g)].map((m) => m[1]).filter((v) => !/^none\b/.test(v));
471
+ assert.ok(transitions.length >= 1, 'the saved tick transitions');
472
+ for (const t of transitions) assert.match(t, /^transform\b/, 'a transition moves transform only');
473
+ assert.match(css, /@media \(prefers-reduced-motion: reduce\) \{\s*body\[data-page="my-agents"\] \*,[\s\S]*animation: none !important; transition: none !important;/);
474
+ });
475
+
476
+ test('PAGE: no animation loop or timer-driven motion in the page script', () => {
477
+ const code = codeOf(read('my-agents.js'));
478
+ assert.doesNotMatch(code, /requestAnimationFrame|setInterval/);
479
+ // the one timer takes the saved tick's attribute back off; CSS plays the motion
480
+ assert.equal((code.match(/setTimeout\(/g) || []).length, 1);
481
+ });