@bongos/core 1.21.113 → 1.21.114

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 (260) hide show
  1. package/.bongos-core.json +603 -183
  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 +343 -262
  25. package/docs/copy-registry.json +1223 -426
  26. package/docs/file-map.md +2 -1
  27. package/docs/module-api-changelog.md +2 -0
  28. package/docs/packs/engineer.md +2 -1
  29. package/docs/page-inventory.json +91 -6
  30. package/docs/page-readings.json +5017 -4458
  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.html +2 -2
  166. package/modules/public-landing/public/projects.probes.json +6 -2
  167. package/modules/public-landing/public/terms.html +2 -2
  168. package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
  169. package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
  170. package/modules/ui-design/kit/fixtures/connections.json +22 -0
  171. package/modules/ui-design/kit/serve.js +95 -0
  172. package/package-lock.json +2 -2
  173. package/package.json +1 -1
  174. package/release-notes.json +122 -0
  175. package/scripts/discord/README.md +8 -5
  176. package/scripts/discord/post.js +12 -1
  177. package/scripts/gds/artifact-audit.js +20 -3
  178. package/scripts/gds/ask.js +164 -0
  179. package/scripts/gds/autobongos-verify.js +21 -2
  180. package/scripts/gds/government-walk.js +638 -4
  181. package/scripts/gds/pulse.js +9 -6
  182. package/scripts/gds/ship-escalation.js +22 -3
  183. package/scripts/gds/ship-finish.js +5 -4
  184. package/scripts/gds/ship-merge-gate.js +18 -6
  185. package/scripts/hall-preview/README.md +41 -1
  186. package/scripts/hall-preview/pref-writes.js +140 -0
  187. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  188. package/scripts/hall-preview/scrub-identity.js +366 -7
  189. package/scripts/hall-preview/server.js +27 -1
  190. package/scripts/render-diagrams.sh +4 -2
  191. package/src/bongos/auth-idp.js +6 -1
  192. package/src/bongos/project-settings.js +41 -0
  193. package/src/bongos/route-rank-check.js +27 -0
  194. package/src/bongos/routes/me.js +68 -7
  195. package/src/bongos/serve-internal.js +5 -1
  196. package/src/module-api.js +7 -1
  197. package/src/platform-server.js +10 -0
  198. package/tests/application_lifecycle.mjs +47 -5
  199. package/tests/conductor_main_e2e.mjs +21 -0
  200. package/tests/connections_api.mjs +18 -0
  201. package/tests/craft_mode_nav.mjs +5 -3
  202. package/tests/craft_mode_tallies.mjs +52 -16
  203. package/tests/discord_approvals.mjs +11 -0
  204. package/tests/discord_channels.mjs +20 -0
  205. package/tests/fixtures/gov-seated.mjs +64 -0
  206. package/tests/fixtures/government-ask-cards.json +1284 -0
  207. package/tests/government_act_alone_buildings.mjs +1 -0
  208. package/tests/government_agent_asks.mjs +322 -0
  209. package/tests/government_asks.mjs +408 -0
  210. package/tests/government_build_rights.mjs +253 -0
  211. package/tests/government_buildouts.mjs +382 -0
  212. package/tests/government_counts.mjs +143 -0
  213. package/tests/government_decision_rules.mjs +5 -1
  214. package/tests/government_laws.mjs +488 -0
  215. package/tests/government_laws_session.mjs +321 -0
  216. package/tests/government_leads.mjs +375 -0
  217. package/tests/government_positions.mjs +127 -0
  218. package/tests/government_presets.mjs +215 -0
  219. package/tests/government_routes.mjs +8 -1
  220. package/tests/government_routing.mjs +118 -0
  221. package/tests/government_seed.mjs +5 -0
  222. package/tests/government_treasury.mjs +394 -0
  223. package/tests/governor_docket.mjs +26 -2
  224. package/tests/hall_buildouts.mjs +208 -0
  225. package/tests/hall_city.mjs +14 -6
  226. package/tests/hall_city_living.mjs +617 -0
  227. package/tests/hall_gov_counts.mjs +336 -0
  228. package/tests/hall_gov_living.mjs +636 -0
  229. package/tests/hall_gov_real_site.mjs +655 -0
  230. package/tests/hall_matter_card.mjs +6 -1
  231. package/tests/hall_my_agents.mjs +481 -0
  232. package/tests/hall_nav.mjs +107 -4
  233. package/tests/hall_page_gate_map.mjs +4 -0
  234. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  235. package/tests/hall_preview_scrub.mjs +59 -1
  236. package/tests/hall_preview_security.mjs +169 -0
  237. package/tests/hall_reading_world.mjs +44 -0
  238. package/tests/hall_roster_world.mjs +6 -2
  239. package/tests/hall_street.mjs +18 -2
  240. package/tests/hall_street_amend.mjs +1 -1
  241. package/tests/hall_street_deck.mjs +609 -0
  242. package/tests/hall_street_depth.mjs +559 -0
  243. package/tests/hall_street_desk.mjs +1 -1
  244. package/tests/hall_street_inside.mjs +29 -5
  245. package/tests/hall_tweak_editor.mjs +4 -1
  246. package/tests/hub_account_page.mjs +94 -3
  247. package/tests/hub_connections_page.mjs +507 -0
  248. package/tests/hub_invite_badge.mjs +1 -1
  249. package/tests/idp_device_exchange.mjs +18 -0
  250. package/tests/landing_legal_pages.mjs +8 -5
  251. package/tests/landing_page.mjs +23 -4
  252. package/tests/nav_permission_atoms.mjs +4 -1
  253. package/tests/profile_connections_ui.mjs +88 -25
  254. package/tests/projects_hub.mjs +15 -0
  255. package/tests/render_check.mjs +2 -2
  256. package/tests/ship_broadcast_routing.mjs +57 -16
  257. package/tests/ship_broadcast_server.mjs +40 -7
  258. package/tests/ship_merge_gate.mjs +19 -0
  259. package/tests/ui_design_kit.mjs +2 -2
  260. package/tests/watch_applications_queue.mjs +23 -0
@@ -0,0 +1,636 @@
1
+ // tests/hall_gov_living.mjs — the living city's model (modules/hall-ui/public/
2
+ // gov-living.js, task 1004685) over the REAL governor fixtures the harness
3
+ // snapshots, and nothing else: task 1004717 deleted the prototype overlay.
4
+ //
5
+ // What it pins:
6
+ // 1. the nine civic buildings, who holds each (a Lead GET /government/leads
7
+ // names, else the owner seat), and that nothing is invented without a read;
8
+ // 2. every real docket matter is exactly one ask, and the queue is needs-you
9
+ // first, then oldest;
10
+ // 3. the ask format refuses what it should and accepts every card in the
11
+ // past-asks corpus; a matter with no stored ask has no card at all;
12
+ // 4. ALL OF IT IS REAL: every `_sources` label in the whole model is real,
13
+ // derived or mixed — never anything else — and an `overlay` handed in
14
+ // changes nothing, field for field;
15
+ // 5. the module is pure: no window, document, fetch or clock in its source, the
16
+ // same input gives the same model, and the inputs are not mutated.
17
+ //
18
+ // Run: node --test --test-reporter=tap tests/hall_gov_living.mjs
19
+
20
+ import { test } from 'node:test';
21
+ import assert from 'node:assert/strict';
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { createRequire } from 'node:module';
25
+ import { fileURLToPath } from 'node:url';
26
+ // five Leads seated, a room and two build-outs, the docket routed to them (test data)
27
+ import { SEATED, leadsRead as seatedLeads, docketRouted } from './fixtures/gov-seated.mjs';
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
+ const FX = path.join(ROOT, 'scripts', 'hall-preview', 'fixtures');
32
+ const SRC = path.join(ROOT, 'modules', 'hall-ui', 'public', 'gov-living.js');
33
+ const G = require(SRC);
34
+ const fx = (name) => JSON.parse(fs.readFileSync(path.join(FX, name + '.json'), 'utf8'));
35
+ // real past asks as the cards they should have been (test data; the hall never reads it)
36
+ const CARDS = JSON.parse(fs.readFileSync(path.join(ROOT, 'tests', 'fixtures', 'government-ask-cards.json'), 'utf8')).cards;
37
+
38
+ const NOW = Date.parse('2026-10-09T14:00:00.000Z');
39
+ function inputs(overrides = {}) {
40
+ return {
41
+ docket: fx('government__docket'),
42
+ constitution: fx('government__constitution'),
43
+ goals: fx('goals'),
44
+ rollups: fx('goals__rollups'),
45
+ blockers: fx('blockers'),
46
+ fence: fx('autonomy__fence'),
47
+ claims: fx('tasks__claimable'),
48
+ tasks: fx('tasks__status-active'),
49
+ moving: fx('moving'),
50
+ sessions: fx('sessions__search'),
51
+ learnings: fx('learnings'),
52
+ cost: fx('public__cost-summary'),
53
+ roster: fx('builders__roster'),
54
+ me: fx('me'),
55
+ laws: fx('government__laws'),
56
+ leads: fx('government__leads'),
57
+ presets: fx('government__presets'),
58
+ // the pots, the reserve and the spend cap (task 1004712)
59
+ treasury: fx('government__treasury'),
60
+ counts: fx('government__counts'),
61
+ now: NOW,
62
+ ...overrides,
63
+ };
64
+ }
65
+ const model = (overrides) => G.livingModel(inputs(overrides));
66
+ // The docket as a server that did not route it serves it (no `routed`, no `route`).
67
+ function unrouted(d = fx('government__docket')) {
68
+ delete d.routed;
69
+ d.matters.forEach((m) => { delete m.route; });
70
+ return d;
71
+ }
72
+ // the kind of thing the deleted prototype overlay said, every concept it held
73
+ const OLD_OVERLAY = {
74
+ _proto: 'Prototype data: concepts not yet in the database',
75
+ owner: { id: '1000025' },
76
+ leads: [{ building: 'rules', lead: '1000301', since: '2026-09-01', charter: { scope: 'x', can_build_without_vote: true }, deputies: ['3'], committees: [{ key: 'c', name: 'C', members: ['1000025'] }] }],
77
+ laws: [{ id: 'design-exact', text: 'Build the approved design exactly', locks: [{ setting: 'wandering', max: 'focused' }] }],
78
+ personal_settings: [{ key: 'parallel_agents', label: 'Agents at once', values: [1, 2, 3], value: 3 }],
79
+ strictness: { current: 'factory', presets: [{ key: 'factory', name: 'Factory', line: 'x' }] },
80
+ pots: { reserve_usd: 999, basis: 'x', items: [{ goal_id: '1000106', budget_usd: 1, spent_usd: 2 }] },
81
+ buildouts: [{ id: 'b', building: 'rules', authorised: true, label: 'Invented', progress: 0.5 }],
82
+ build_rights: { rules: { capsules: { floor: { without_vote: true } } } },
83
+ asks: [{ id: 'criteria_uat:1000184', question: 'An invented rewrite?', options: [{ key: 'a', label: 'A', result: 'a' }, { key: 'b', label: 'B', result: 'b' }], recommend: { key: 'a', why: 'x' }, waits_on: 'owner', if_nobody_answers: 'x' }],
84
+ proto_asks: [{ id: 'proto:pot:1', building: 'treasury', question: 'Lend it?', options: [{ key: 'a', label: 'A', result: 'a' }, { key: 'b', label: 'B', result: 'b' }], recommend: { key: 'a', why: 'x' }, waits_on: 'owner', if_nobody_answers: 'x' }],
85
+ };
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // 1. the buildings
89
+ // ---------------------------------------------------------------------------
90
+
91
+ test('the nine civic buildings are all present, in the docket\'s order, named by it', () => {
92
+ const m = model();
93
+ const docket = fx('government__docket');
94
+ assert.deepEqual(m.buildings.map((b) => b.key), G.BUILDING_KEYS.slice());
95
+ assert.equal(m.buildings.length, 9);
96
+ assert.deepEqual(m.buildings.map((b) => b.key), docket.buildings.map((b) => b.key), 'the same nine the docket serves');
97
+ for (const b of m.buildings) {
98
+ const d = docket.buildings.find((x) => x.key === b.key);
99
+ assert.equal(b.name, d.name);
100
+ assert.equal(b.decision.rule, d.decision.rule, `${b.key}: how it decides is the docket's`);
101
+ assert.ok(b.activity >= 0 && b.activity <= 1 && b.lit >= 0 && b.lit <= 1, `${b.key}: activity and lit are 0..1`);
102
+ }
103
+ // with every feed missing the city still has its nine buildings, and nothing else
104
+ const empty = G.livingModel({});
105
+ assert.deepEqual(empty.buildings.map((b) => b.key), G.BUILDING_KEYS.slice());
106
+ assert.equal(empty.city.asks.length, 0, 'no feed, no asks: nothing is invented');
107
+ assert.equal(empty.districts.length, 0);
108
+ assert.ok(empty.buildings.every((b) => b.lead === null && b.holder === null && b.build_rights === null && b.structure.committees.length === 0 && b.construction.length === 0), 'no Lead, holder, right or build-out is invented');
109
+ assert.equal(empty.owner, null);
110
+ assert.equal(empty.strictness, null, 'no preset read: no preset');
111
+ assert.equal(empty.city.laws.length, 0, 'no law read: no law');
112
+ assert.equal(empty.city.spend.limit, null, 'no treasury read: the limit is unknown, not "none"');
113
+ });
114
+
115
+ test('a building with no Lead falls to the owner seat — its holder, and the seat its asks wait on', () => {
116
+ const read = seatedLeads();
117
+ const m = model({ leads: read, docket: unrouted() });
118
+ const owner = read.owner.builders[0];
119
+ for (const key of ['charter', 'rules', 'disputes', 'automation']) {
120
+ const b = m.buildings.find((x) => x.key === key);
121
+ assert.equal(b.lead, null, `${key}: no Lead`);
122
+ assert.equal(b.held_by, 'owner');
123
+ assert.equal(b.holder.id, String(owner.id), `${key}: the owner holds it`);
124
+ }
125
+ for (const l of SEATED) {
126
+ const b = m.buildings.find((x) => x.key === l.building);
127
+ assert.equal(b.held_by, 'lead');
128
+ assert.equal(b.lead.id, l.lead);
129
+ }
130
+ // on a docket the server did not route, an ask at an unseated building waits on the owner seat
131
+ const ask = m.city.asks.find((a) => a.building === 'disputes');
132
+ assert.equal(ask.waits_on.seat, 'lead:disputes');
133
+ assert.equal(ask.waits_on.fell_to_owner, true);
134
+ assert.equal(ask.waits_on.real, false, 'not the server\'s route');
135
+ assert.equal(ask.needs_you, true, 'the viewer is among the owner seat');
136
+ const review = m.city.asks.find((a) => a.building === 'review' && a.state === 'needs');
137
+ assert.equal(review.waits_on.seat, 'lead:review');
138
+ assert.equal(review.needs_you, false, 'Review has its own Lead');
139
+ });
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // 2. the asks
143
+ // ---------------------------------------------------------------------------
144
+
145
+ test('every real docket matter appears as exactly one ask, with its real id, state and text', () => {
146
+ const m = model();
147
+ const docket = fx('government__docket');
148
+ const count = {};
149
+ m.city.asks.forEach((a) => { count[a.id] = (count[a.id] || 0) + 1; });
150
+ for (const matter of docket.matters) {
151
+ assert.equal(count[matter.id], 1, `${matter.id} is one ask`);
152
+ const ask = m.city.asks.find((a) => a.id === matter.id);
153
+ assert.equal(ask.state, matter.state);
154
+ assert.equal(ask.raw.title, matter.title, 'the raw text is the docket\'s own');
155
+ assert.equal('proto' in ask, false);
156
+ }
157
+ // the open blockers are all docket matters already, so none is added twice
158
+ for (const b of fx('blockers').blockers) assert.equal(count['blockers:' + b.id], 1);
159
+ assert.equal(m.city.asks.length, docket.matters.length, 'nothing beyond the docket\'s matters');
160
+ // every building lists exactly the asks standing at it
161
+ assert.equal(m.buildings.reduce((n, b) => n + b.asks.length, 0), m.city.asks.length);
162
+ });
163
+
164
+ test('the queue is needs-you first, then oldest first, for high volume', () => {
165
+ // the dry pot's borrow, passed on to the People Lead: it needs someone, not you
166
+ const d = fx('government__docket');
167
+ const b = d.matters.find((x) => x.source === 'treasury_spending');
168
+ b.route = { ...b.route, seat: 'lead:people', label: 'the People Lead', asked: 'lead:people', fell_to_owner: false, yours: false };
169
+ const asks = model({ docket: d }).city.asks;
170
+ const tier = (a) => (a.needs_you ? 0 : { needs: 1, moving: 2, settled: 3 }[a.state]);
171
+ assert.ok(asks.some((a) => a.needs_you) && asks.some((a) => !a.needs_you && a.state === 'needs'), 'both tiers are present, so the order is exercised');
172
+ for (let i = 1; i < asks.length; i++) {
173
+ const a = asks[i - 1];
174
+ const c = asks[i];
175
+ assert.ok(tier(a) <= tier(c), `${a.id} before ${c.id}: tiers in order`);
176
+ if (tier(a) !== tier(c)) continue;
177
+ const ta = a.opened_at ? Date.parse(a.opened_at) : null;
178
+ const tb = c.opened_at ? Date.parse(c.opened_at) : null;
179
+ if (ta != null && tb != null) assert.ok(ta <= tb, `${a.id} before ${c.id}: oldest first`);
180
+ else assert.ok(!(ta == null && tb != null), `${a.id} before ${c.id}: a dated ask before an undated one`);
181
+ }
182
+ assert.equal(asks[asks.length - 1].state, 'settled', 'the settled sitting is last');
183
+ // the comparator on its own, with one ask that waits on the viewer
184
+ const mk = (id, state, opened_at, needs_you) => ({ id, state, opened_at, needs_you });
185
+ const sorted = [mk('c', 'needs', '2026-10-01T00:00:00Z', false), mk('a', 'settled', '2026-01-01T00:00:00Z', false), mk('d', 'needs', null, false), mk('b', 'needs', '2026-10-05T00:00:00Z', true), mk('e', 'needs', '2026-09-01T00:00:00Z', false)].sort(G.compareAsks);
186
+ assert.deepEqual(sorted.map((a) => a.id), ['b', 'e', 'c', 'd', 'a']);
187
+ });
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // 3. the ask format
191
+ // ---------------------------------------------------------------------------
192
+
193
+ const GOOD = {
194
+ question: 'Turn unattended building back on?',
195
+ options: [
196
+ { key: 'on', label: 'Turn it on', result: 'Agents work through the night.' },
197
+ { key: 'off', label: 'Keep it off', result: 'Nothing builds unless someone starts a session.' },
198
+ ],
199
+ recommend: { key: 'off', why: 'It was paused for a check.' },
200
+ waits_on: 'owner',
201
+ if_nobody_answers: 'It stays off.',
202
+ };
203
+ const card = (over) => ({ ...JSON.parse(JSON.stringify(GOOD)), ...over });
204
+
205
+ test('askFormat refuses a question over twelve words, and the other ways an ask goes wrong', () => {
206
+ assert.equal(G.askFormat(GOOD).ok, true);
207
+ const long = G.askFormat(card({ question: 'Should we turn the unattended building back on now that the evaluation is done?' }));
208
+ assert.equal(long.ok, false);
209
+ assert.deepEqual(long.errors.map((e) => e.rule), ['too_long']);
210
+ assert.equal(long.question_words, 14);
211
+ const cases = {
212
+ not_a_question: card({ question: 'Turn unattended building back on.' }),
213
+ more_than_one: card({ question: 'It is off. Turn it on?' }),
214
+ markup: card({ question: 'Turn **unattended** building on?' }),
215
+ count: card({ options: [GOOD.options[0]] }),
216
+ label_too_long: card({ options: [{ key: 'on', label: 'Turn it on right now please', result: 'On.' }, GOOD.options[1]] }),
217
+ not_one_line: card({ options: [GOOD.options[0], { key: 'off', label: 'Keep it off', result: 'Off.\nAnd quiet.' }] }),
218
+ duplicate: card({ options: [GOOD.options[0], { ...GOOD.options[1], key: 'on' }], recommend: { key: 'on', why: 'x' } }),
219
+ unknown_key: card({ recommend: { key: 'maybe', why: 'It was paused.' } }),
220
+ no_why: card({ recommend: { key: 'off', why: '' } }),
221
+ };
222
+ for (const [rule, c] of Object.entries(cases)) {
223
+ const r = G.askFormat(c);
224
+ assert.equal(r.ok, false, rule);
225
+ assert.ok(r.errors.some((e) => e.rule === rule), `${rule}: ${JSON.stringify(r.errors)}`);
226
+ }
227
+ assert.equal(G.askFormat(card({ options: [...GOOD.options, GOOD.options[0], GOOD.options[1]] })).ok, false, 'four answers is too many');
228
+ assert.ok(G.askFormat(card({ waits_on: '' })).errors.some((e) => e.field === 'waits_on'));
229
+ assert.ok(G.askFormat(card({ if_nobody_answers: '' })).errors.some((e) => e.field === 'if_nobody_answers'));
230
+ assert.equal(G.askFormat(null).ok, false, 'no card at all is the raw form, refused');
231
+ assert.equal(G.askFormat(card({ question: 'Upgrade the setup service, version 1.20.82, now?' })).ok, true, 'a version number is not a sentence break');
232
+ });
233
+
234
+ test('askFormat accepts every card in the past-asks corpus, and each one reads at a glance', () => {
235
+ assert.ok(CARDS.length >= fx('government__docket').matters.length);
236
+ for (const a of CARDS) {
237
+ const r = G.askFormat(a);
238
+ assert.equal(r.ok, true, `${a.id}: ${JSON.stringify(r.errors)}`);
239
+ assert.ok(r.question_words <= G.ASK_RULES.question_words);
240
+ assert.equal(G.slopScore(a.question).slop, false, `${a.id}: the question is not slop`);
241
+ assert.ok(a.options.some((o) => o.key === a.recommend.key), `${a.id}: recommends an answer it offers`);
242
+ }
243
+ });
244
+
245
+ test('a matter with no stored ask has no card: the board shows its own words, never a made-up rewrite', () => {
246
+ const m = model({ overlay: OLD_OVERLAY });
247
+ assert.ok(m.city.asks.length > 0);
248
+ // (an ask its agent stored in the ask shape is shown: the dry pot's borrow, task 1004712)
249
+ const stored = m.city.asks.filter((a) => a.ask_real);
250
+ assert.deepEqual(stored.map((a) => a.id), ['treasury_spending:1']);
251
+ assert.equal(stored[0].format.ok, true);
252
+ for (const a of m.city.asks.filter((x) => !x.ask_real)) {
253
+ assert.equal(a.human, null, a.id + ': no card is invented');
254
+ assert.equal(a.shown, null);
255
+ assert.equal(a.format.ok, false);
256
+ }
257
+ assert.ok(!m.city.asks.some((a) => a.id.startsWith('proto:')), 'and no sample ask joins the docket\'s');
258
+ // (a red-team report's raw text is withheld in the fixtures, so the long one here is a criterion)
259
+ const criterion = m.city.asks.find((a) => a.id === 'criteria_uat:1000380');
260
+ const s = criterion.raw.slop;
261
+ assert.equal(s.slop, true, 'the raw criterion is slop by every measure');
262
+ assert.ok(s.words > G.SLOP.words && s.sentences > G.SLOP.sentences && s.markup > 0);
263
+ assert.ok(s.score > 0.5 && s.score <= 1);
264
+ assert.deepEqual(G.slopScore(''), { words: 0, sentences: 0, longest_sentence: 0, lines: 0, markup: 0, score: 0, slop: false, why: [] });
265
+ });
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // 4. all of it real
269
+ // ---------------------------------------------------------------------------
270
+
271
+ // every `_sources` label in a model, with where it sits
272
+ function labelsOf(v, where = '$', out = []) {
273
+ if (Array.isArray(v)) v.forEach((x, i) => labelsOf(x, `${where}[${i}]`, out));
274
+ else if (v && typeof v === 'object') {
275
+ if (v._sources) for (const [f, l] of Object.entries(v._sources)) out.push({ at: `${where}.${f}`, label: l });
276
+ for (const [k, x] of Object.entries(v)) if (k !== '_sources') labelsOf(x, `${where}.${k}`, out);
277
+ }
278
+ return out;
279
+ }
280
+ const ALLOWED = new Set(['real', 'derived', 'mixed']);
281
+
282
+ test('every field is real, derived or mixed: no other label anywhere in the model', () => {
283
+ const full = model({ leads: seatedLeads(), docket: docketRouted(), me: meWithLaws() });
284
+ for (const m of [model(), full, model({ docket: unrouted() }), G.livingModel({}), G.livingModel({ docket: fx('government__docket') })]) {
285
+ const all = labelsOf(m);
286
+ const bad = all.filter((x) => !ALLOWED.has(x.label));
287
+ assert.deepEqual(bad, [], 'only real, derived and mixed');
288
+ }
289
+ const n = labelsOf(full).length;
290
+ assert.ok(n > 500, `${n} labels were walked, not a vacuous check`);
291
+ assert.deepEqual(Object.values(G.SOURCES).sort(), ['derived', 'mixed', 'real']);
292
+ // and the walk would catch one that is not (it is not vacuous)
293
+ const lie = JSON.parse(JSON.stringify(full));
294
+ lie.buildings.find((b) => b.key === 'release').structure._sources = { committees: 'proto' };
295
+ assert.deepEqual(labelsOf(lie).filter((x) => !ALLOWED.has(x.label)).map((x) => x.at), ['$.buildings[7].structure.committees']);
296
+ });
297
+
298
+ test('an overlay handed in changes nothing: the model is the same, field for field', () => {
299
+ for (const over of [{}, { leads: seatedLeads(), docket: docketRouted() }, { leads: null, treasury: null, presets: null, laws: null }]) {
300
+ assert.deepEqual(model({ ...over, overlay: OLD_OVERLAY }), model(over));
301
+ }
302
+ assert.equal('overlay_note' in model(), false, 'there is no prototype note to carry');
303
+ });
304
+
305
+ test('the real reads land where the model says: spend, traffic, districts, readings', () => {
306
+ const m = model();
307
+ const cost = fx('public__cost-summary');
308
+ const day = cost.daily.find((d) => d.day.slice(0, 10) === '2026-10-09');
309
+ const month = cost.by_month.find((x) => x.month === '2026-10');
310
+ assert.equal(m.city.spend.today_usd, Math.round(day.amount_usd * 100) / 100);
311
+ assert.equal(m.city.spend.month_usd, Math.round(month.total_usd * 100) / 100);
312
+ assert.equal(m.city.spend.limit, 'none', 'no spend limit: the treasury\'s cap is off');
313
+ const claims = fx('tasks__claimable').active_claims;
314
+ assert.equal(m.city.traffic.agents, claims.length, 'every active claim is an agent on the move');
315
+ assert.equal(m.city.traffic.moving, fx('moving').moving);
316
+ const placedTotal = Object.values(m.city.traffic.by_building).reduce((n, x) => n + x, 0);
317
+ assert.equal(placedTotal + m.city.traffic.unplaced, claims.length);
318
+ assert.equal(m.buildings.reduce((n, b) => n + b.agents, 0), placedTotal);
319
+ const open = fx('goals').goals.filter((g) => g.status === 'open');
320
+ assert.deepEqual(m.districts.map((d) => d.goal_id), open.map((g) => String(g.id)), 'one district per open goal, in the goals\' order');
321
+ const r = fx('goals__rollups').rollups.find((x) => String(x.goal_id) === m.districts[0].goal_id);
322
+ assert.equal(m.districts[0].counts.shipped, r.done);
323
+ const money = fx('government__docket').readings.find((x) => x.key === 'money');
324
+ assert.equal(m.city.spend.unattended.ceiling_usd, money.value.ceiling_usd);
325
+ assert.ok(m.buildings.find((b) => b.key === 'treasury').readings.some((x) => x.key === 'money'));
326
+ });
327
+
328
+ test('the treasury: a dry pot asks to borrow, routed to the Treasury (task 1004712)', () => {
329
+ const m = model();
330
+ const t = fx('government__treasury');
331
+ const dry = m.districts.filter((d) => d.pot && d.pot.dry);
332
+ assert.equal(dry.length, 1, 'one pot is dry');
333
+ assert.equal(dry[0].pot.real, true);
334
+ assert.equal(dry[0]._sources.pot, 'real');
335
+ assert.equal(dry[0].pot.borrow_ask_id, 'treasury_spending:1', 'it asks to borrow from the reserve, on the docket');
336
+ const borrow = m.city.asks.find((a) => a.id === dry[0].pot.borrow_ask_id);
337
+ assert.equal(borrow.building, 'treasury');
338
+ assert.equal(borrow.ask_real, true, 'its ask is stored in the ask shape');
339
+ assert.equal(borrow.waits_on.seat, 'owner', 'the Treasury is unseated, so borrowing falls to the owner');
340
+ assert.equal(borrow.needs_you, true);
341
+ assert.equal(m.city.spend.reserve_usd, t.reserve.usd);
342
+ assert.equal(m.city.spend.cap.on, false);
343
+ // without the read, no district shows a pot, and nothing about the reserve is said
344
+ const none = model({ treasury: null });
345
+ assert.equal(none.districts.filter((d) => d.pot).length, 0);
346
+ assert.deepEqual([none.city.spend.reserve_usd, none.city.spend.pots_basis, none.city.spend.limit, none.city.spend.cap], [null, null, null, null]);
347
+ });
348
+
349
+ test('the build-outs stand at their buildings, from GET /government/leads alone', () => {
350
+ const m = model({ leads: seatedLeads(), docket: docketRouted() });
351
+ const release = m.buildings.find((b) => b.key === 'release').construction;
352
+ const treasury = m.buildings.find((b) => b.key === 'treasury').construction;
353
+ assert.deepEqual(release.map((c) => [c.kind, c.authorised, c.progress]), [['buildout', true, 0.55]], 'a floor going up, at its real progress');
354
+ assert.deepEqual(treasury.map((c) => [c.kind, c.authorised, c.ask_id, c.illustrative]), [['scaffold', false, 'board:77', false]], 'an annex waiting on its sitting');
355
+ assert.deepEqual(model().buildings.map((b) => b.construction.length).reduce((a, x) => a + x, 0), 0, 'the hub has built nothing and no sitting is open: nothing stands');
356
+ });
357
+
358
+ test('my agents: no law in force, nothing locked, whatever the preset; every row is /me\'s own', () => {
359
+ const a = model().myAgents;
360
+ assert.equal(a.strictness, null, 'no preset is in force on the hub');
361
+ assert.equal(a.locked, 0);
362
+ const wander = a.settings.find((s) => s.key === 'wandering');
363
+ assert.equal(wander.value, fx('me').wandering.effective_key, 'the viewer\'s own value');
364
+ assert.equal(wander.effective, wander.value);
365
+ assert.ok(a.settings.every((s) => !s.locked && !s.lockedBy && !s.advisedBy), 'no row is locked or advised by anything but a law in force');
366
+ assert.ok(!a.settings.some((s) => s.key === 'parallel_agents'), 'a setting no table holds is not a row');
367
+ });
368
+
369
+ test('governance is a held position: a viewer with no seat gets no governance tab', () => {
370
+ const m = model();
371
+ assert.deepEqual(m.viewer.tabs, ['city', 'agents', 'governance']);
372
+ const stranger = { builder: { id: '424242', github_login: 'builder-9999', display_name: 'Builder 9999', rank: 'xenos' } };
373
+ const s = model({ me: stranger });
374
+ assert.equal(s.viewer.has_seat, false);
375
+ assert.deepEqual(s.viewer.tabs, ['city', 'agents']);
376
+ assert.equal(s.city.ask_counts.needs_you, 0, 'and nothing waits on them');
377
+ });
378
+
379
+ test('governance is a held position: /me\'s server-resolved positions decide the seats, and they are real (task 1004713)', () => {
380
+ const me = fx('me');
381
+ const m = model({ me });
382
+ assert.deepEqual(m.viewer.seats.map((s) => s.role), me.positions.held.map((p) => p.kind), 'the seats are /me\'s');
383
+ assert.equal(m.viewer._sources.seats, 'real');
384
+ assert.equal(m.viewer._sources.tabs, 'real');
385
+ const none = model({ me: { ...me, positions: { held: [], title: null, has_position: false } } });
386
+ assert.equal(none.viewer.has_seat, false, 'the server said none');
387
+ assert.deepEqual(none.viewer.tabs, ['city', 'agents']);
388
+ const lead = model({ me: { builder: { id: '424242', rank: 'thetes' }, positions: { held: [{ kind: 'lead', seat: 'lead:review', building: 'review', title: 'Review Lead' }], title: 'Review Lead', has_position: true } } });
389
+ assert.deepEqual(lead.viewer.tabs, ['city', 'agents', 'governance'], 'a Lead the server seats gets the governance tab');
390
+ assert.equal(lead.viewer.seats[0].title, 'Review Lead');
391
+ // without /me's positions block, the seats come from the real reads (the owner seat), derived
392
+ const bare = model({ me: { builder: me.builder } });
393
+ assert.equal(bare.viewer._sources.seats, 'derived');
394
+ assert.ok(bare.viewer.seats.some((s) => s.role === 'owner'), 'the viewer is among the owner seat GET /government/leads names');
395
+ });
396
+
397
+ // ---------------------------------------------------------------------------
398
+ // 5. purity
399
+ // ---------------------------------------------------------------------------
400
+
401
+ test('pure: no window, document, fetch or clock in the source', () => {
402
+ const src = fs.readFileSync(SRC, 'utf8');
403
+ // comments may name what the file does not do; code may not
404
+ const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:'"`\\])\/\/.*$/gm, '$1');
405
+ const tail = "typeof window !== 'undefined' ? window : globalThis";
406
+ assert.ok(code.includes(tail), 'the UMD root test is the one mention of window');
407
+ const body = code.replace(tail, '');
408
+ for (const [name, re] of Object.entries({
409
+ window: /\bwindow\b/, document: /\bdocument\b/, fetch: /\bfetch\b/, 'Date.now': /Date\.now/,
410
+ 'new Date()': /new Date\(\s*\)/, XMLHttpRequest: /XMLHttpRequest/, localStorage: /localStorage/, setTimeout: /setTimeout|setInterval/,
411
+ })) assert.doesNotMatch(body, re, `${name} must not appear in gov-living.js`);
412
+ assert.doesNotMatch(body, /\boverlay\b|'proto'/, 'and it reads no overlay and labels nothing prototype');
413
+ assert.match(code, /root\.GovLiving = api/);
414
+ assert.match(code, /module\.exports = api/);
415
+ });
416
+
417
+ test('pure: the same input gives the same model, and the inputs are left alone', () => {
418
+ const a = inputs();
419
+ const before = JSON.stringify(a);
420
+ const one = G.livingModel(a);
421
+ assert.equal(JSON.stringify(a), before, 'livingModel does not mutate its inputs');
422
+ assert.deepEqual(G.livingModel(inputs()), one);
423
+ assert.equal(one.generated_at, new Date(NOW).toISOString(), 'the clock is the one passed in');
424
+ });
425
+
426
+ // ---------------------------------------------------------------------------
427
+ // the law book (task 1004706) and its locks (task 1004707)
428
+ // ---------------------------------------------------------------------------
429
+
430
+ // what the server did to THIS viewer's settings (task 1004707): GET /me's laws.locks
431
+ function meWithLaws() {
432
+ const me = fx('me');
433
+ // the slice as the server answers it under the law: the effective value obeys it
434
+ me.interaction.effective = { ...me.interaction.effective, verbosity: 'concise' };
435
+ me.laws = {
436
+ inject: true,
437
+ contract: 'Project laws — passed by a vote of this project\'s government.\n1. Ask like a human',
438
+ laws: [{ id: '11', no: 1, text: 'Ask like a human', effect_on_ai: null, locks: [{ setting: 'verbosity', value: 'concise' }, { setting: 'wandering', value: 'locked' }, { setting: 'ask_format', value: 'card' }] }],
439
+ locks: [
440
+ { setting: 'verbosity', law_id: '11', law_no: 1, law_text: 'Ask like a human', value: 'concise', route: 'interaction', enforced: true, from: 'detailed', to: 'concise' },
441
+ { setting: 'wandering', law_id: '11', law_no: 1, law_text: 'Ask like a human', value: 'locked', route: 'wandering', enforced: true, from: me.wandering.effective_key, to: me.wandering.effective_key },
442
+ { setting: 'ask_format', law_id: '11', law_no: 1, law_text: 'Ask like a human', value: 'card', enforced: false, reason: 'not_a_personal_setting' },
443
+ ],
444
+ };
445
+ return me;
446
+ }
447
+
448
+ test('a law in force locks a real setting through GET /me, whatever an overlay says', () => {
449
+ const m = model({ me: meWithLaws() });
450
+ const verb = m.myAgents.settings.find((s) => s.key === 'verbosity');
451
+ assert.equal(verb.locked, true);
452
+ assert.equal(verb.effective, 'concise', 'the server\'s effective value');
453
+ assert.equal(verb.value, 'detailed', 'the viewer\'s own choice, still on record');
454
+ assert.deepEqual(verb.lockedBy, { law_id: 'law:11', no: 1, text: 'Ask like a human', real: true });
455
+ for (const f of ['effective', 'locked', 'lockedBy']) assert.equal(verb._sources[f], 'real', f);
456
+ assert.equal(m.myAgents.settings.find((s) => s.key === 'wandering').lockedBy.law_id, 'law:11');
457
+ assert.equal(m.myAgents.locked, 2);
458
+ // a lock the server could not enforce locks nothing (no table holds ask_format yet)
459
+ assert.ok(!m.myAgents.settings.some((s) => s.key === 'ask_format'), 'no claim of an enforcement that is not happening');
460
+ assert.deepEqual(model({ me: meWithLaws(), overlay: OLD_OVERLAY }).myAgents, m.myAgents);
461
+ });
462
+
463
+ test('a law the project passed (GET /government/laws) stands in the book; a repealed one does not', () => {
464
+ const book = {
465
+ laws: [
466
+ { id: '11', text: 'Every screen change carries two shots', effect_on_ai: 'Agents attach a before and an after picture.', locks: [{ setting: 'render', value: 'rich' }], in_force: true, passed_at: '2026-10-09T12:00:00.000Z', passed_by: { item_id: '31', government: 'Timocratic Republic', proposer: { id: '42' }, yes: [] } },
467
+ { id: '9', text: 'An old rule nobody keeps', in_force: false, passed_at: '2026-09-01T00:00:00.000Z', repealed_at: '2026-10-01T00:00:00.000Z', passed_by: { item_id: '20', government: 'Monarchy / BDFL' } },
468
+ ],
469
+ };
470
+ const m = model({ laws: book });
471
+ const real = m.city.laws.find((l) => l.id === 'law:11');
472
+ assert.ok(real, 'the passed law is in the book');
473
+ assert.equal(real.real, true);
474
+ assert.equal(real.origin, 'vote');
475
+ assert.equal(real.text, 'Every screen change carries two shots');
476
+ assert.equal(real.passed_by.name, 'Timocratic Republic', 'passed by the government in force on its day');
477
+ assert.equal(real.passed_at, '2026-10-09T12:00:00.000Z');
478
+ assert.deepEqual(real.locks, [{ setting: 'render', value: 'rich' }]);
479
+ for (const f of ['id', 'text', 'passed_by', 'passed_at', 'effect_on_ai', 'locks']) assert.equal(real._sources[f], 'real', f);
480
+ assert.ok(!m.city.laws.some((l) => l.id === 'law:9'), 'a repealed law is not in force');
481
+ assert.ok(m.city.laws.every((l) => l.real), 'every law in the book is real');
482
+ // the BOOK's locks are not read for My agents: what a lock did to the viewer is GET /me's laws.locks
483
+ assert.deepEqual(m.myAgents.settings.filter((s) => s.lockedBy), []);
484
+ // the hub has passed no law: the book holds only the rule the docket says the project runs
485
+ assert.deepEqual(model().city.laws.map((l) => l.origin), ['docket']);
486
+ assert.deepEqual(model({ laws: null }).city.laws.map((l) => l.id), model().city.laws.map((l) => l.id));
487
+ });
488
+
489
+ // A Lead the government really appointed (task 1004709), as GET /government/leads
490
+ // answers it: Planning led by builder 1000027, every other building unseated, and
491
+ // the owner seat held by the Archons (the founding one first).
492
+ function leadsRead() {
493
+ const read = fx('government__leads');
494
+ const p = read.buildings.find((b) => b.building === 'planning');
495
+ p.lead = { id: '1', building: 'planning', builder: { id: '1000027', login: 'builder-2016', name: 'Builder 2016' }, charter: 'Goals, priorities and the order of work.', since: '2026-10-09T12:00:00.000Z', appointed_by: { item_id: '40', href: '/board-room?item=40' } };
496
+ p.answers = { seat: 'lead:planning', builder_ids: ['1000027'], fell_to_owner: false };
497
+ read.seated = 1;
498
+ return read;
499
+ }
500
+
501
+ test('a Lead the government appointed (GET /government/leads) is on their building; every unseated building falls to the owner', () => {
502
+ const read = leadsRead();
503
+ const m = model({ leads: read });
504
+ assert.equal(m.seats_real, true);
505
+ const planning = m.buildings.find((b) => b.key === 'planning');
506
+ assert.deepEqual(planning.lead, { since: '2026-10-09T12:00:00.000Z', id: '1000027', login: 'builder-2016', name: 'Builder 2016' });
507
+ assert.equal(planning.held_by, 'lead');
508
+ assert.deepEqual(planning.charter, { scope: 'Goals, priorities and the order of work.' });
509
+ assert.deepEqual(planning.answers, { seat: 'lead:planning', ids: ['1000027'], fell_to_owner: false });
510
+ for (const f of ['lead', 'holder', 'charter', 'answers']) assert.equal(planning._sources[f], 'real', f);
511
+ assert.equal(planning._sources.held_by, 'derived');
512
+ const owner = read.owner.builders[0];
513
+ assert.equal(m.owner.id, String(owner.id), 'the owner is the read\'s');
514
+ for (const b of m.buildings.filter((x) => x.key !== 'planning')) {
515
+ assert.equal(b.lead, null, `${b.key}: no Lead`);
516
+ assert.equal(b.held_by, 'owner');
517
+ assert.equal(b.holder.id, String(owner.id), `${b.key}: held by the owner`);
518
+ assert.equal(b.answers.fell_to_owner, true);
519
+ assert.deepEqual(b.answers.ids, read.owner.builders.map((x) => String(x.id)), `${b.key}: the owner seat answers`);
520
+ }
521
+ // without the read, nobody leads anything and nobody is named the owner
522
+ const before = model({ leads: null });
523
+ assert.equal(before.seats_real, false);
524
+ assert.ok(before.buildings.every((b) => b.lead === null && b.holder === null && b.answers === null));
525
+ assert.equal(before.owner, null);
526
+ });
527
+
528
+ test("the hub has seated no Lead yet: with its read every building is the owner's", () => {
529
+ const m = model({ leads: fx('government__leads') });
530
+ assert.ok(m.buildings.every((b) => b.lead === null && b.held_by === 'owner'), 'every building falls to the owner');
531
+ assert.ok(m.buildings.every((b) => b._sources.lead === 'real' && b._sources.holder === 'real'));
532
+ assert.equal(G.countsOf(model({ counts: null })).on_you, fx('government__docket').matters.filter((x) => x.state === 'needs' && x.route.yours).length, 'the docket\'s own routes');
533
+ });
534
+
535
+ // ---------------------------------------------------------------------------
536
+ // 6. the stored ask (task 1004703): the agent's own card
537
+ // ---------------------------------------------------------------------------
538
+
539
+ // A card as GET /government/docket and GET /blockers carry it (modules/government/asks.js).
540
+ const STORED = {
541
+ question: 'Pick the host for the new site?',
542
+ options: [
543
+ { key: 'cheap', label: 'The cheap host', result: 'Ten dollars a month, slower builds.', act: 'go ahead' },
544
+ { key: 'fast', label: 'The fast host', result: 'Forty dollars a month, quick builds.' },
545
+ ],
546
+ recommend: { key: 'cheap', why: 'The site is small for now.' },
547
+ waits_on: 'owner',
548
+ if_nobody_answers: 'The site waits, unbuilt.',
549
+ detail: 'The long story rides along.',
550
+ written_at: '2026-10-09T12:00:00.000Z',
551
+ };
552
+ function docketWithAsk(id) {
553
+ const d = fx('government__docket');
554
+ d.matters.find((m) => m.id === id).ask = JSON.parse(JSON.stringify(STORED));
555
+ return d;
556
+ }
557
+
558
+ test('a matter that carries its stored ask is drawn from it', () => {
559
+ const id = 'override_requests:1000017';
560
+ const m = model({ docket: docketWithAsk(id) });
561
+ const a = m.city.asks.find((x) => x.id === id);
562
+ assert.equal(a.ask_real, true);
563
+ assert.equal(a.human.question, STORED.question);
564
+ assert.equal(a.format.ok, true);
565
+ assert.equal(a.shown, a.human);
566
+ assert.equal(a.raw.title, fx('government__docket').matters.find((x) => x.id === id).title, 'the matter\'s own words stay its raw form');
567
+ assert.deepEqual([a._sources.human, a._sources.format, a._sources.shown], ['real', 'derived', 'derived']);
568
+ assert.equal(a._sources.waits_on, 'real', 'who it waits on is the server\'s route (task 1004705)');
569
+ // a matter without one has none
570
+ const plain = m.city.asks.find((x) => x.source === 'criteria_uat');
571
+ assert.equal(plain.ask_real, false);
572
+ assert.equal(plain.human, null);
573
+ // a stored card that is not one (no question) is ignored, never shown
574
+ const d = fx('government__docket');
575
+ d.matters.find((x) => x.id === id).ask = { options: [] };
576
+ assert.equal(model({ docket: d }).city.asks.find((x) => x.id === id).ask_real, false);
577
+ });
578
+
579
+ // ---------------------------------------------------------------------------
580
+ // the direction desk (task 1004705): who an ask waits on is the server's route
581
+ // ---------------------------------------------------------------------------
582
+
583
+ test('a routed docket decides what needs you: the route\'s seat and its yours', () => {
584
+ const docket = fx('government__docket');
585
+ assert.equal(docket.routed, true, 'the fixture is routed, as the server serves it');
586
+ // half the matters are not the viewer's
587
+ const d = fx('government__docket');
588
+ d.matters.forEach((m, i) => { if (i % 2) m.route = { ...m.route, seat: 'lead:review', label: 'the Review Lead', fell_to_owner: false, yours: false }; });
589
+ const m = model({ docket: d, counts: null });
590
+ assert.equal(m.routed, true);
591
+ for (const matter of d.matters) {
592
+ const a = m.city.asks.find((x) => x.id === matter.id);
593
+ assert.equal(a.waits_on.seat, matter.route.seat, `${a.id}: the route's seat`);
594
+ assert.equal(a.waits_on.real, true);
595
+ assert.equal(a.needs_you, a.state === 'needs' && matter.route.yours, `${a.id}: needs you is the route's word`);
596
+ assert.equal(a._sources.needs_you, 'derived');
597
+ assert.equal(a._sources.waits_on, 'real');
598
+ }
599
+ const c = G.countsOf(m);
600
+ const yours = d.matters.filter((x) => x.state === 'needs' && x.route.yours).length;
601
+ assert.equal(c.on_you, yours, 'to do counts only the routes that are yours');
602
+ assert.equal(c.on_others, d.matters.filter((x) => x.state === 'needs').length - yours, 'the rest waits on others');
603
+ assert.deepEqual([c._sources.on_you, c._sources.on_others], ['derived', 'derived']);
604
+ });
605
+
606
+ test('an ask nobody answers is settled with its own line, on nobody\'s to do', () => {
607
+ const d = fx('government__docket');
608
+ const target = d.matters.find((m) => m.state === 'needs');
609
+ target.route = { ...target.route, yours: false, nobody: { why: 'lapsed', outcome: 'It stays open on the Disputes desk.' } };
610
+ const m = model({ docket: d, counts: null });
611
+ const a = m.city.asks.find((x) => x.id === target.id);
612
+ assert.equal(a.state, 'settled');
613
+ assert.equal(a.needs_you, false);
614
+ assert.deepEqual(a.nobody, { why: 'lapsed', outcome: 'It stays open on the Disputes desk.' });
615
+ assert.equal(G.isOnYou(a) || G.isOnOthers(a), false, 'neither to do nor waiting on others');
616
+ assert.equal(G.countsOf(m).on_you, model({ counts: null }).city.counts.on_you - 1);
617
+ });
618
+
619
+ test('a docket routed for another viewer gives this viewer nothing to do', () => {
620
+ const stranger = { builder: { id: '424242', github_login: 'builder-9999', display_name: 'Builder 9999', rank: 'xenos' } };
621
+ const m = model({ me: stranger, counts: null });
622
+ assert.equal(m.city.asks.filter((a) => a.needs_you).length, 0);
623
+ assert.equal(G.countsOf(m).on_you, 0);
624
+ });
625
+
626
+ test('an open blocker read from GET /blockers carries its stored ask the same way', () => {
627
+ const bl = fx('blockers');
628
+ const docket = fx('government__docket');
629
+ // a blocker the docket does not carry (a viewer without the docket's blockers)
630
+ docket.matters = docket.matters.filter((x) => x.source !== 'blockers');
631
+ bl.blockers[0].ask = JSON.parse(JSON.stringify(STORED));
632
+ const a = model({ docket, blockers: bl }).city.asks.find((x) => x.id === 'blockers:' + bl.blockers[0].id);
633
+ assert.equal(a.ask_real, true);
634
+ assert.equal(a.shown.question, STORED.question);
635
+ assert.equal(a._sources.human, 'real');
636
+ });