@bongos/core 1.21.112 → 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 (265) hide show
  1. package/.bongos-core.json +616 -186
  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 +4 -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 +128 -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-run.js +20 -5
  180. package/scripts/gds/autobongos-tweak-lane.js +262 -0
  181. package/scripts/gds/autobongos-verify.js +21 -2
  182. package/scripts/gds/claim.js +2 -0
  183. package/scripts/gds/government-walk.js +638 -4
  184. package/scripts/gds/pulse.js +9 -6
  185. package/scripts/gds/ship-escalation.js +22 -3
  186. package/scripts/gds/ship-finish.js +5 -4
  187. package/scripts/gds/ship-merge-gate.js +18 -6
  188. package/scripts/gds/tweak-halt.js +1 -0
  189. package/scripts/hall-preview/README.md +41 -1
  190. package/scripts/hall-preview/pref-writes.js +140 -0
  191. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  192. package/scripts/hall-preview/scrub-identity.js +366 -7
  193. package/scripts/hall-preview/server.js +27 -1
  194. package/scripts/render-diagrams.sh +4 -2
  195. package/src/bongos/auth-idp.js +6 -1
  196. package/src/bongos/project-settings.js +41 -0
  197. package/src/bongos/route-rank-check.js +27 -0
  198. package/src/bongos/routes/me.js +68 -7
  199. package/src/bongos/serve-internal.js +5 -1
  200. package/src/module-api.js +7 -1
  201. package/src/platform-server.js +10 -0
  202. package/tests/application_lifecycle.mjs +47 -5
  203. package/tests/autobongos_tweak_lane.mjs +208 -0
  204. package/tests/conductor_main_e2e.mjs +21 -0
  205. package/tests/connections_api.mjs +18 -0
  206. package/tests/craft_mode_nav.mjs +5 -3
  207. package/tests/craft_mode_tallies.mjs +52 -16
  208. package/tests/discord_approvals.mjs +11 -0
  209. package/tests/discord_channels.mjs +20 -0
  210. package/tests/fixtures/gov-seated.mjs +64 -0
  211. package/tests/fixtures/government-ask-cards.json +1284 -0
  212. package/tests/government_act_alone_buildings.mjs +1 -0
  213. package/tests/government_agent_asks.mjs +322 -0
  214. package/tests/government_asks.mjs +408 -0
  215. package/tests/government_build_rights.mjs +253 -0
  216. package/tests/government_buildouts.mjs +382 -0
  217. package/tests/government_counts.mjs +143 -0
  218. package/tests/government_decision_rules.mjs +5 -1
  219. package/tests/government_laws.mjs +488 -0
  220. package/tests/government_laws_session.mjs +321 -0
  221. package/tests/government_leads.mjs +375 -0
  222. package/tests/government_positions.mjs +127 -0
  223. package/tests/government_presets.mjs +215 -0
  224. package/tests/government_routes.mjs +8 -1
  225. package/tests/government_routing.mjs +118 -0
  226. package/tests/government_seed.mjs +5 -0
  227. package/tests/government_treasury.mjs +394 -0
  228. package/tests/governor_docket.mjs +26 -2
  229. package/tests/hall_buildouts.mjs +208 -0
  230. package/tests/hall_city.mjs +14 -6
  231. package/tests/hall_city_living.mjs +617 -0
  232. package/tests/hall_gov_counts.mjs +336 -0
  233. package/tests/hall_gov_living.mjs +636 -0
  234. package/tests/hall_gov_real_site.mjs +655 -0
  235. package/tests/hall_matter_card.mjs +6 -1
  236. package/tests/hall_my_agents.mjs +481 -0
  237. package/tests/hall_nav.mjs +107 -4
  238. package/tests/hall_page_gate_map.mjs +4 -0
  239. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  240. package/tests/hall_preview_scrub.mjs +59 -1
  241. package/tests/hall_preview_security.mjs +169 -0
  242. package/tests/hall_reading_world.mjs +44 -0
  243. package/tests/hall_roster_world.mjs +6 -2
  244. package/tests/hall_street.mjs +18 -2
  245. package/tests/hall_street_amend.mjs +1 -1
  246. package/tests/hall_street_deck.mjs +609 -0
  247. package/tests/hall_street_depth.mjs +559 -0
  248. package/tests/hall_street_desk.mjs +1 -1
  249. package/tests/hall_street_inside.mjs +29 -5
  250. package/tests/hall_tweak_editor.mjs +4 -1
  251. package/tests/hub_account_page.mjs +94 -3
  252. package/tests/hub_connections_page.mjs +507 -0
  253. package/tests/hub_invite_badge.mjs +1 -1
  254. package/tests/idp_device_exchange.mjs +18 -0
  255. package/tests/landing_legal_pages.mjs +8 -5
  256. package/tests/landing_page.mjs +23 -4
  257. package/tests/nav_permission_atoms.mjs +4 -1
  258. package/tests/profile_connections_ui.mjs +88 -25
  259. package/tests/projects_hub.mjs +15 -0
  260. package/tests/render_check.mjs +2 -2
  261. package/tests/ship_broadcast_routing.mjs +57 -16
  262. package/tests/ship_broadcast_server.mjs +40 -7
  263. package/tests/ship_merge_gate.mjs +19 -0
  264. package/tests/ui_design_kit.mjs +2 -2
  265. package/tests/watch_applications_queue.mjs +23 -0
@@ -0,0 +1,394 @@
1
+ // tests/government_treasury.mjs — THE TREASURY: a pot per goal, the reserve,
2
+ // and the borrow a dry pot asks for (task 1004712, goal 1000111; ADR 0379).
3
+ //
4
+ // What this pins (the task's done-when lines, and the walls behind them):
5
+ // 1. POTS ARE FILLED FROM REAL SPEND. A pot's spend is this month's cost_log
6
+ // rows on the goal's tasks; its budget is the stored one its holder gave
7
+ // it; the reserve is last month's real spend no pot was given, less loans.
8
+ // 2. A DRY POT FILES A BORROW ASK. fileBorrows (the poller's one call) files
9
+ // an open borrow for every dry pot with nothing open and no "no" this
10
+ // month, and stores its ask in the ask shape on treasury_spending:<id>,
11
+ // waiting on lead:treasury. The docket carries it at the Treasury, routed
12
+ // to the Treasury's holder (the owner while it is unseated).
13
+ // 3. THE CAP SETTING EXISTS AND IS OFF. project_settings['treasury.spend_cap_usd'];
14
+ // no row is off; the treasury says "no spend limit".
15
+ // 4. THE WALLS. Only whoever answers for the Treasury sets a pot (403
16
+ // not_the_holder); only the seat a borrow waits on answers it (403
17
+ // not_yours); a borrow is answered once (409, and the trigger); a loan is
18
+ // never deleted.
19
+ // DB-free: the pool's query is replaced by an in-memory fake that answers the
20
+ // treasury's own statements by name, and refuses any it does not know.
21
+ //
22
+ // Run: node --test tests/government_treasury.mjs
23
+
24
+ import { strict as assert } from 'node:assert';
25
+ import { test, after } from 'node:test';
26
+ import http from 'node:http';
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import { createRequire } from 'node:module';
30
+ import { fileURLToPath } from 'node:url';
31
+
32
+ const require = createRequire(import.meta.url);
33
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
34
+ const GOV = path.join(ROOT, 'modules', 'government');
35
+ const T = require(path.join(GOV, 'treasury.js'));
36
+ const asks = require(path.join(GOV, 'asks.js'));
37
+ const routing = require(path.join(GOV, 'routing.js'));
38
+ const {
39
+ SETTING_KEYS, MAX_SPEND_CAP_USD, parseSpendCap, resolveSpendCap, setSpendCap, describeSpendCap,
40
+ } = require('../src/bongos/project-settings.js');
41
+ const { pool } = require(path.join(ROOT, 'src', 'bongos', 'pool.js'));
42
+
43
+ const MIGRATION = fs.readFileSync(path.join(GOV, 'migrations', 'government_031_treasury.sql'), 'utf8');
44
+ const NOW = Date.parse('2026-10-10T12:00:00.000Z'); // day 10 of 31
45
+
46
+ // ── an in-memory project: goals, tasks' real spend, pots, borrows, asks, settings ──
47
+ function project({ pots = [], borrows = [], cap = null, spend = {}, lastMonth = 0 } = {}) {
48
+ const st = {
49
+ goals: [{ id: 1, title: 'Project creation', status: 'open' }, { id: 2, title: 'Maintenance', status: 'open' }, { id: 3, title: 'Old goal', status: 'done' }],
50
+ pots: pots.map((p) => ({ set_by: null, set_at: '2026-10-01T00:00:00.000Z', ...p })),
51
+ borrows: borrows.map((b) => ({ ...b })),
52
+ asks: new Map(),
53
+ settings: cap == null ? new Map() : new Map([['treasury.spend_cap_usd', String(cap)]]),
54
+ spend, lastMonth, seq: 100, seen: [],
55
+ };
56
+ const q = async (sql, params = []) => {
57
+ const s = String(sql).replace(/\s+/g, ' ').trim();
58
+ st.seen.push(s);
59
+ if (/^SELECT key, value, updated_at, updated_by FROM project_settings WHERE key = \$1/.test(s)) {
60
+ const v = st.settings.get(params[0]);
61
+ return { rows: v == null ? [] : [{ key: params[0], value: v, updated_at: null, updated_by: null }] };
62
+ }
63
+ if (/^INSERT INTO project_settings/.test(s)) { st.settings.set(params[0], params[1]); return { rows: [{ key: params[0], value: params[1], updated_at: null, updated_by: params[2] }] }; }
64
+ if (/^DELETE FROM project_settings WHERE key = \$1/.test(s)) { st.settings.delete(params[0]); return { rows: [], rowCount: 1 }; }
65
+ if (/^SELECT to_char\(recorded_at, 'YYYY-MM'\) AS month/.test(s)) {
66
+ const thisMonth = Object.values(st.spend).reduce((t, v) => t + v, 0) + 50; // + untasked spend
67
+ return { rows: [{ month: params[0], total_usd: thisMonth }, ...(st.lastMonth ? [{ month: params[1], total_usd: st.lastMonth }] : [])] };
68
+ }
69
+ if (/^SELECT t\.goal_id, COALESCE\(SUM\(cl\.amount_usd\), 0\) AS total_usd FROM cost_log cl JOIN tasks t/.test(s)) {
70
+ assert.equal(params[0], '2026-10');
71
+ return { rows: Object.entries(st.spend).map(([goal_id, total_usd]) => ({ goal_id, total_usd })) };
72
+ }
73
+ if (/^SELECT goal_id, budget_usd, set_by, set_at FROM government_pots/.test(s)) return { rows: st.pots };
74
+ if (/^SELECT id, title, status FROM goals WHERE status = 'open'/.test(s)) return { rows: st.goals.filter((g) => g.status === 'open') };
75
+ if (/^SELECT id, title, status FROM goals WHERE id = \$1/.test(s)) return { rows: st.goals.filter((g) => String(g.id) === String(params[0])) };
76
+ if (/FROM government_pot_borrows WHERE month = \$1 OR state = 'open'/.test(s)) return { rows: st.borrows.filter((b) => b.month === params[0] || b.state === 'open') };
77
+ if (/^INSERT INTO government_pot_borrows/.test(s)) {
78
+ if (st.borrows.some((b) => String(b.goal_id) === String(params[0]) && b.state === 'open')) return { rows: [] };
79
+ const row = { id: ++st.seq, goal_id: params[0], month: params[1], amount_usd: params[2], budget_usd: params[3], spent_usd: params[4], state: 'open', filed_at: '2026-10-10T12:00:00.000Z', lent_usd: null, answer_key: null };
80
+ st.borrows.push(row);
81
+ return { rows: [{ id: row.id }] };
82
+ }
83
+ if (/^INSERT INTO government_asks/.test(s)) {
84
+ const row = { matter_id: params[0], question: params[1], options: JSON.parse(params[2]), recommend_key: params[3], recommend_why: params[4], waits_on: params[5], if_nobody_answers: params[6], detail: params[7], written_at: null };
85
+ st.asks.set(params[0], row);
86
+ return { rows: [row] };
87
+ }
88
+ if (/FROM government_asks WHERE matter_id = ANY/.test(s)) return { rows: params[0].map((id) => st.asks.get(id)).filter(Boolean) };
89
+ if (/FROM government_pot_borrows b JOIN goals g ON g\.id = b\.goal_id WHERE b\.state = 'open'/.test(s)) {
90
+ return { rows: st.borrows.filter((b) => b.state === 'open').map((b) => ({ ...b, title: st.goals.find((g) => String(g.id) === String(b.goal_id)).title })) };
91
+ }
92
+ if (/FROM government_pot_borrows b JOIN goals g ON g\.id = b\.goal_id WHERE b\.id = \$1/.test(s)) {
93
+ const b = st.borrows.find((x) => String(x.id) === String(params[0]));
94
+ return { rows: b ? [{ ...b, title: st.goals.find((g) => String(g.id) === String(b.goal_id)).title }] : [] };
95
+ }
96
+ if (/^UPDATE government_pot_borrows SET state = \$2/.test(s)) {
97
+ const b = st.borrows.find((x) => String(x.id) === String(params[0]) && x.state === 'open');
98
+ if (!b) return { rows: [] };
99
+ Object.assign(b, { state: params[1], lent_usd: params[2], answer_key: params[3], answered_by: params[4], answered_at: '2026-10-10T13:00:00.000Z' });
100
+ return { rows: [b] };
101
+ }
102
+ if (/^INSERT INTO government_pots/.test(s)) {
103
+ const row = { goal_id: params[0], budget_usd: params[1], set_by: params[2], set_at: '2026-10-10T12:00:00.000Z' };
104
+ st.pots = st.pots.filter((p) => String(p.goal_id) !== String(params[0])).concat(row);
105
+ return { rows: [row] };
106
+ }
107
+ if (/^DELETE FROM government_pots WHERE goal_id/.test(s)) {
108
+ const n = st.pots.length;
109
+ st.pots = st.pots.filter((p) => String(p.goal_id) !== String(params[0]));
110
+ return { rows: [], rowCount: n - st.pots.length };
111
+ }
112
+ throw new Error('the fake does not know this statement: ' + s.slice(0, 120));
113
+ };
114
+ return { st, q };
115
+ }
116
+ async function withPool(q, fn) {
117
+ const real = pool.query;
118
+ pool.query = q;
119
+ try { return await fn(); } finally { pool.query = real; }
120
+ }
121
+
122
+ const treasuryDb = require(path.join(GOV, 'treasury-db.js'));
123
+
124
+ // ── 1. pots are filled from real spend ──────────────────────────────────────
125
+
126
+ test('a pot is its stored budget against this month\'s REAL spend on the goal\'s tasks; the reserve is last month\'s spend no pot was given', async () => {
127
+ const { q } = project({ pots: [{ goal_id: 1, budget_usd: 300 }, { goal_id: 2, budget_usd: 500 }, { goal_id: 3, budget_usd: 100 }], spend: { 1: 412.5, 2: 120.25 }, lastMonth: 2000 });
128
+ const t = await withPool(q, () => treasuryDb.readTreasury({ now: NOW }));
129
+ assert.equal(t.month, '2026-10');
130
+ assert.deepEqual(t.pots.map((p) => [p.goal_id, p.spent_usd, p.budget_usd, p.dry]), [['1', 412.5, 300, true], ['2', 120.25, 500, false]], 'spend is cost_log\'s, per goal; a closed goal has no pot');
131
+ assert.equal(t.spend.month_usd, 582.75, 'this month: every real dollar, tasked or not');
132
+ assert.equal(t.spend.last_month_usd, 2000);
133
+ assert.equal(t.reserve.usd, 1200, 'last month\'s $2000, less the $800 given to pots');
134
+ assert.equal(t.pots[0].left_usd, -112.5);
135
+ });
136
+
137
+ test('a goal without a stored budget has no pot (a goal MAY have one); no month behind it leaves the reserve unknown, never invented', async () => {
138
+ const { q } = project({ pots: [], spend: { 1: 50 } });
139
+ const t = await withPool(q, () => treasuryDb.readTreasury({ now: NOW }));
140
+ assert.deepEqual(t.pots, []);
141
+ assert.equal(t.reserve.usd, null);
142
+ assert.match(t.reserve.line, /No month of spend/);
143
+ });
144
+
145
+ test('a loan this month tops its pot up and comes out of the reserve; a pot is dry at spend >= budget + lent', () => {
146
+ const t = T.buildTreasury({
147
+ now: NOW, goals: [{ id: 1, title: 'A', status: 'open' }], pots: [{ goal_id: 1, budget_usd: 300 }],
148
+ spend: { month_usd: 400, last_month_usd: 1000, by_goal: { 1: 400 } },
149
+ borrows: [{ id: 7, goal_id: 1, month: '2026-10', amount_usd: 200, budget_usd: 300, spent_usd: 320, state: 'lent', lent_usd: 200, answer_key: 'lend' }],
150
+ });
151
+ assert.equal(t.pots[0].lent_usd, 200);
152
+ assert.equal(t.pots[0].dry, false);
153
+ assert.equal(t.reserve.usd, 500, '$1000 less $300 given less $200 lent');
154
+ assert.equal(t.pots[0].borrow.state, 'lent');
155
+ const last = T.buildTreasury({
156
+ now: NOW, goals: [{ id: 1, title: 'A', status: 'open' }], pots: [{ goal_id: 1, budget_usd: 300 }],
157
+ spend: { month_usd: 400, last_month_usd: 1000, by_goal: { 1: 400 } },
158
+ borrows: [{ id: 6, goal_id: 1, month: '2026-09', amount_usd: 200, budget_usd: 300, spent_usd: 320, state: 'lent', lent_usd: 200, answer_key: 'lend' }],
159
+ });
160
+ assert.equal(last.pots[0].lent_usd, 0, 'last month\'s loan does not carry into this month');
161
+ assert.equal(last.pots[0].dry, true);
162
+ });
163
+
164
+ // ── 3. the cap setting exists and is off ────────────────────────────────────
165
+
166
+ test('the spend cap is a project setting, and it is OFF by default: no limit', async () => {
167
+ assert.equal(SETTING_KEYS.SPEND_CAP, 'treasury.spend_cap_usd');
168
+ const off = await withPool(project().q, () => resolveSpendCap());
169
+ assert.deepEqual([off.on, off.usd, off.source], [false, null, 'default'], 'no row: off');
170
+ const t = await withPool(project().q, () => treasuryDb.readTreasury({ now: NOW }));
171
+ assert.equal(t.cap.on, false);
172
+ assert.match(t.cap.line, /no spend limit/);
173
+ const on = await withPool(project({ cap: 500 }).q, () => resolveSpendCap());
174
+ assert.deepEqual([on.on, on.usd], [true, 500], 'a stored value turns it on');
175
+ const junk = await withPool(project({ cap: 'lots' }).q, () => resolveSpendCap());
176
+ assert.equal(junk.on, false, 'an unusable stored value reads off, and says why');
177
+ assert.ok(junk.invalid);
178
+ assert.deepEqual(parseSpendCap(0).reason ? 'refused' : 'ok', 'refused', '$0 is not a cap; clearing is how it turns off');
179
+ assert.ok(parseSpendCap(MAX_SPEND_CAP_USD + 1).reason, 'past the ceiling is refused');
180
+ // set it, read it on, turn it off again: off is no row
181
+ const p = project();
182
+ await withPool(p.q, async () => {
183
+ await setSpendCap(800, 3);
184
+ const r = await resolveSpendCap();
185
+ assert.deepEqual([r.on, r.usd], [true, 800]);
186
+ assert.match(describeSpendCap(r), /^On:/);
187
+ await setSpendCap(null, 3);
188
+ assert.equal(p.st.settings.size, 0, 'turning it off deletes the row');
189
+ assert.match(describeSpendCap(await resolveSpendCap()), /no spend limit/);
190
+ });
191
+ });
192
+
193
+ // ── 2. a dry pot files a borrow ask ─────────────────────────────────────────
194
+
195
+ test('a dry pot files ONE borrow, its ask stored in the ask shape on treasury_spending:<id>, waiting on the Treasury', async () => {
196
+ const { st, q } = project({ pots: [{ goal_id: 1, budget_usd: 300 }, { goal_id: 2, budget_usd: 500 }], spend: { 1: 412.5, 2: 120 }, lastMonth: 2000 });
197
+ const filed = await withPool(q, () => treasuryDb.fileBorrows({ now: NOW }));
198
+ assert.equal(filed.length, 1, 'only the dry pot asks');
199
+ assert.equal(filed[0].goal_id, '1');
200
+ // its pace: $412.50 over 10 of 31 days is ~$1279 for the month; it has $300 → ask $1000 (rounded up to $50)
201
+ assert.equal(filed[0].amount_usd, 1000);
202
+ const ask = st.asks.get('treasury_spending:' + filed[0].borrow_id);
203
+ assert.ok(ask, 'the ask is stored on the docket id');
204
+ assert.equal(ask.waits_on, 'lead:treasury');
205
+ assert.equal(ask.question, 'Lend Project creation $1000 from the reserve?');
206
+ assert.ok(asks.checkAsk(asks.fromRow(ask)).ok, 'it is a valid ask, by the server\'s own rules');
207
+ assert.deepEqual(asks.fromRow(ask).options.map((o) => o.act), ['answer:lend', 'answer:half', 'answer:no']);
208
+ // a second sweep files nothing: one open borrow per pot
209
+ const again = await withPool(q, () => treasuryDb.fileBorrows({ now: NOW }));
210
+ assert.deepEqual(again, []);
211
+ assert.equal(st.borrows.length, 1);
212
+ });
213
+
214
+ test('a "no" this month stands until the month turns; a refusal last month does not stop this month\'s ask', () => {
215
+ const pot = { goal_id: '1', dry: true };
216
+ assert.equal(T.needsBorrow(pot, [], '2026-10'), true);
217
+ assert.equal(T.needsBorrow(pot, [{ goal_id: 1, state: 'open', month: '2026-09' }], '2026-10'), false, 'one open at a time, whatever its month');
218
+ assert.equal(T.needsBorrow(pot, [{ goal_id: 1, state: 'refused', month: '2026-10' }], '2026-10'), false);
219
+ assert.equal(T.needsBorrow(pot, [{ goal_id: 1, state: 'refused', month: '2026-09' }], '2026-10'), true);
220
+ assert.equal(T.needsBorrow(pot, [{ goal_id: 1, state: 'lent', month: '2026-10' }], '2026-10'), true, 'lent and dry again: it asks again');
221
+ assert.equal(T.needsBorrow({ goal_id: '1', dry: false }, [], '2026-10'), false);
222
+ });
223
+
224
+ test('fileBorrows honours a "no": a pot the treasury refused this month files nothing until the month turns', async () => {
225
+ const refused = { id: 5, goal_id: 1, month: '2026-10', amount_usd: 400, budget_usd: 300, spent_usd: 320, state: 'refused', lent_usd: null, answer_key: 'no' };
226
+ const { st, q } = project({ pots: [{ goal_id: 1, budget_usd: 300 }], spend: { 1: 412.5 }, lastMonth: 2000, borrows: [refused] });
227
+ assert.deepEqual(await withPool(q, () => treasuryDb.fileBorrows({ now: NOW })), []);
228
+ assert.equal(st.borrows.length, 1);
229
+ const next = project({ pots: [{ goal_id: 1, budget_usd: 300 }], spend: { 1: 412.5 }, lastMonth: 2000, borrows: [{ ...refused, month: '2026-09' }] });
230
+ assert.equal((await withPool(next.q, () => treasuryDb.fileBorrows({ now: NOW }))).length, 1, 'a refusal from last month does not hold this month');
231
+ });
232
+
233
+ test('the borrow ask is never slop: a long or marked-up goal title falls back to "this goal"; past a cap it recommends no', () => {
234
+ const long = T.borrowAsk({ title: 'The very long goal title that would never fit a question', amount_usd: 400, spent_usd: 900, budget_usd: 750 });
235
+ assert.equal(long.question, 'Lend this goal $400 from the reserve?');
236
+ assert.equal(T.borrowAsk({ title: 'Working area 1 — Project creation', amount_usd: 400, spent_usd: 900, budget_usd: 750 }).question, 'Lend Project creation $400 from the reserve?', 'a working-area prefix is dropped');
237
+ assert.ok(asks.checkAsk(long).ok);
238
+ assert.equal(long.recommend.key, 'lend', 'no limit: lend by default');
239
+ const capped = T.borrowAsk({ title: 'Ops', amount_usd: 400, spent_usd: 900, budget_usd: 750, cap: { on: true, usd: 1000 }, month_usd: 800 });
240
+ assert.equal(capped.recommend.key, 'no');
241
+ assert.ok(asks.checkAsk(capped).ok);
242
+ });
243
+
244
+ test('the poller files through fileBorrows and is quiet when nothing is dry; a failed read is retried, never thrown', async () => {
245
+ const poller = require(path.join(GOV, 'pollers', 'treasury-borrow.js'));
246
+ const lines = [];
247
+ const quiet = await poller.runSweep({ fileFn: async () => [], log: (m) => lines.push(m) });
248
+ assert.deepEqual([quiet.filed, lines.length], [0, 0]);
249
+ const one = await poller.runSweep({ fileFn: async () => [{ goal_id: '1', borrow_id: '9', amount_usd: 400 }], log: (m) => lines.push(m) });
250
+ assert.equal(one.filed, 1);
251
+ assert.match(lines[0], /goal 1's pot is dry → borrow 9 asks the reserve for \$400/);
252
+ const errs = [];
253
+ const bad = await poller.runSweep({ fileFn: async () => { throw new Error('db down'); }, error: (m) => errs.push(m) });
254
+ assert.equal(bad.failed, true);
255
+ assert.match(errs[0], /next sweep retries/);
256
+ const manifest = JSON.parse(fs.readFileSync(path.join(GOV, 'module.json'), 'utf8'));
257
+ assert.ok(manifest.contributes.pollers.includes('treasury-borrow'), 'the module runs it');
258
+ });
259
+
260
+ // ── 4. the walls, in the table ──────────────────────────────────────────────
261
+
262
+ test('government_031: a borrow is filed open, answered once, what it asked is never edited, and it is never deleted', () => {
263
+ const code = MIGRATION.replace(/--[^\n]*/g, '');
264
+ assert.match(code, /CREATE UNIQUE INDEX IF NOT EXISTS government_pot_borrows_one_open\s+ON government_pot_borrows \(goal_id\) WHERE state = 'open'/);
265
+ assert.match(code, /IF TG_OP = 'DELETE' THEN\s+RAISE EXCEPTION/);
266
+ assert.match(code, /IF OLD\.state <> 'open' THEN\s+RAISE EXCEPTION/);
267
+ assert.match(code, /NEW\.amount_usd IS DISTINCT FROM OLD\.amount_usd/);
268
+ assert.match(code, /BEFORE INSERT OR UPDATE OR DELETE ON government_pot_borrows/);
269
+ assert.match(code, /state IN \('open', 'lent', 'refused'\)/);
270
+ assert.doesNotMatch(code, /INSERT INTO/, 'nothing is seeded');
271
+ });
272
+
273
+ // ── the routes, through the real router ─────────────────────────────────────
274
+
275
+ const api = require(path.join(ROOT, 'src', 'module-api.js'));
276
+ const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
277
+ const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
278
+ const resolver = require(path.join(GOV, 'resolver.js'));
279
+ const db = require(path.join(GOV, 'db.js'));
280
+ const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
281
+ const express = require('express');
282
+
283
+ const realRequireBuilder = api.requireBuilder;
284
+ const realResolve = resolver.resolveBuilderPermissions;
285
+ const realLeads = db.listLeads;
286
+ const realArchons = db.listActiveArchons;
287
+ let caller = 3;
288
+ let held = ['board.vote.cast'];
289
+ let leads = [];
290
+ api.requireBuilder = (req, res, next) => { req.builder = { id: caller, rank: caller === 3 ? 'archon' : 'metic' }; next(); };
291
+ resolver.resolveBuilderPermissions = async () => held;
292
+ db.listLeads = async () => leads;
293
+ db.listActiveArchons = async () => [{ id: '3', github_login: 'ada', display_name: 'Ada' }];
294
+ seams._reset();
295
+ seams.registerProvider('government', { resolveBuilderPermissions: async () => held });
296
+ const buildGovernmentRouter = require(path.join(GOV, 'routes', 'government.js'));
297
+ const app = express();
298
+ app.use(express.json());
299
+ app.use(attachFail);
300
+ app.use(buildGovernmentRouter());
301
+ const server = http.createServer(app);
302
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
303
+ const BASE = `http://127.0.0.1:${server.address().port}`;
304
+ after(() => {
305
+ server.close(); api.requireBuilder = realRequireBuilder; resolver.resolveBuilderPermissions = realResolve;
306
+ db.listLeads = realLeads; db.listActiveArchons = realArchons; seams._reset();
307
+ });
308
+ async function call(method, p, body) {
309
+ const res = await fetch(`${BASE}${p}`, { method, headers: body === undefined ? {} : { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
310
+ let json = null; try { json = await res.json(); } catch { /* none */ }
311
+ return { status: res.status, json };
312
+ }
313
+ const TREASURY_LEAD = { id: '9', building: 'treasury', builder_id: '45', charter: 'The money.', appointed_by_item_id: '40', appointed_at: '2026-10-09T12:00:00.000Z', removed_at: null, github_login: 'dee', display_name: 'Dee' };
314
+
315
+ test('the treasury routes are pinned where the room reads; the walls are inside', () => {
316
+ const pins = rrc.MODULE_EXPECTED_RANKS.government;
317
+ assert.equal(pins['GET /government/treasury'], 'perm:board.vote.cast');
318
+ assert.equal(pins['PUT /government/treasury/pots/:goalId'], 'perm:board.vote.cast');
319
+ assert.equal(pins['POST /government/treasury/borrows/:borrowId/answer'], 'perm:board.vote.cast');
320
+ });
321
+
322
+ test('GET /government/treasury: the real treasury, the cap off, and who answers for it (the owner while unseated)', async () => {
323
+ const { q } = project({ pots: [{ goal_id: 1, budget_usd: 300 }], spend: { 1: 412.5 }, lastMonth: 1000 });
324
+ await withPool(q, async () => {
325
+ const r = await call('GET', '/government/treasury');
326
+ assert.equal(r.status, 200, JSON.stringify(r.json));
327
+ assert.equal(r.json.pots[0].dry, true);
328
+ assert.equal(r.json.cap.on, false);
329
+ assert.deepEqual([r.json.answers.seat, r.json.answers.yours, r.json.answers.fell_to_owner], ['owner', true, true]);
330
+ });
331
+ held = [];
332
+ try { assert.equal((await call('GET', '/government/treasury')).status, 403); } finally { held = ['board.vote.cast']; }
333
+ });
334
+
335
+ test('PUT /government/treasury/pots/:goalId: only whoever answers for the Treasury; a Lead seated there takes it from the owner', async () => {
336
+ const { st, q } = project();
337
+ await withPool(q, async () => {
338
+ caller = 3; // the owner, Treasury unseated
339
+ const ok = await call('PUT', '/government/treasury/pots/1', { budget_usd: 250 });
340
+ assert.equal(ok.status, 200, JSON.stringify(ok.json));
341
+ assert.equal(ok.json.pot.budget_usd, 250);
342
+ assert.equal(st.pots.length, 1);
343
+ assert.equal((await call('PUT', '/government/treasury/pots/1', { budget_usd: -5 })).status, 400);
344
+ assert.equal((await call('PUT', '/government/treasury/pots/3', { budget_usd: 50 })).json.error.code, 'goal_not_open');
345
+ assert.equal((await call('PUT', '/government/treasury/pots/99', { budget_usd: 50 })).status, 404);
346
+ leads = [TREASURY_LEAD];
347
+ const owner = await call('PUT', '/government/treasury/pots/1', { budget_usd: 999 });
348
+ assert.equal(owner.status, 403, 'Dee leads the Treasury now: the owner does not set pots');
349
+ assert.equal(owner.json.error.code, 'not_the_holder');
350
+ caller = 45;
351
+ const dee = await call('PUT', '/government/treasury/pots/1', { budget_usd: null });
352
+ assert.equal(dee.status, 200);
353
+ assert.equal(dee.json.removed, true, 'null takes the pot away');
354
+ assert.equal(st.pots.length, 0);
355
+ });
356
+ leads = []; caller = 3;
357
+ });
358
+
359
+ test('a filed borrow is a docket matter at the Treasury, with acts only for its seat; answering it writes the loan once', async () => {
360
+ const { st, q } = project({ pots: [{ goal_id: 1, budget_usd: 300 }], spend: { 1: 412.5 }, lastMonth: 2000 });
361
+ await withPool(q, async () => {
362
+ const [f] = await treasuryDb.fileBorrows({ now: NOW });
363
+ const docket = require(path.join(GOV, 'docket.js'));
364
+ const found = await docket.matterWithSeats({ id: 3, rank: 'archon' }, 'treasury_spending:' + f.borrow_id);
365
+ assert.ok(found.matter, 'the docket carries it');
366
+ assert.equal(found.matter.building, 'treasury');
367
+ assert.equal(found.matter.state, 'needs');
368
+ assert.equal(found.matter.ask.waits_on, 'lead:treasury');
369
+ assert.deepEqual(found.matter.acts[0].options.map((o) => o.key), ['lend', 'half', 'no']);
370
+ assert.equal(routing.routeMatter(found.matter, '3', found.seats).yours, true, 'unseated: the owner answers');
371
+ caller = 45; // not the owner, and the Treasury has no Lead
372
+ const notYours = await call('POST', `/government/treasury/borrows/${f.borrow_id}/answer`, { key: 'lend' });
373
+ assert.equal(notYours.status, 403);
374
+ assert.equal(notYours.json.error.code, 'not_yours');
375
+ const other = await docket.matterWithSeats({ id: 45, rank: 'metic' }, 'treasury_spending:' + f.borrow_id);
376
+ assert.equal(other.matter.acts, undefined, 'no acts for a viewer who does not sit there');
377
+ caller = 3;
378
+ assert.equal((await call('POST', `/government/treasury/borrows/${f.borrow_id}/answer`, { key: 'maybe' })).status, 400);
379
+ const half = await call('POST', `/government/treasury/borrows/${f.borrow_id}/answer`, { key: 'half' });
380
+ assert.equal(half.status, 200, JSON.stringify(half.json));
381
+ assert.deepEqual([half.json.borrow.state, half.json.borrow.lent_usd], ['lent', 500]);
382
+ const twice = await call('POST', `/government/treasury/borrows/${f.borrow_id}/answer`, { key: 'no' });
383
+ assert.equal(twice.status, 409);
384
+ const t = await treasuryDb.readTreasury({ now: NOW });
385
+ assert.deepEqual([t.pots[0].lent_usd, t.pots[0].dry], [500, false], 'the loan tops the pot up');
386
+ assert.equal(st.borrows[0].answered_by, 3);
387
+ });
388
+ });
389
+
390
+ test('the docket declares the treasury\'s spending matters as a live source, no longer "coming"', () => {
391
+ const docket = require(path.join(GOV, 'docket.js'));
392
+ assert.ok(docket.EXPECTED_SOURCES.some((s) => s.source === 'treasury_spending' && s.building === 'treasury'));
393
+ assert.ok(!docket.COMING_SOURCES.some((s) => s.source === 'treasury_spending'));
394
+ });
@@ -287,7 +287,9 @@ test('nothing is invented: coming / off / unavailable are each reported, with no
287
287
  assert.match(by.core_update.reason, /in time/);
288
288
  assert.equal(by.access_requests.status, 'off', 'no contributor registered → its module is off');
289
289
  assert.equal(by.module_rechecks.status, 'off', 'module re-checks are a real source now, never coming');
290
- for (const k of ['grade_appeals', 'standing_rules', 'treasury_spending']) {
290
+ // task 1004712: the treasury's spending matters are a real source now (a dry pot's borrow)
291
+ assert.equal(by.treasury_spending.status, 'off', 'treasury spending is real now: with no contributor its module is off, never coming');
292
+ for (const k of ['grade_appeals', 'standing_rules']) {
291
293
  assert.equal(by[k].status, 'coming', k);
292
294
  assert.ok(by[k].reason, `${k} says why`);
293
295
  }
@@ -307,7 +309,9 @@ test('the response shape: counts, line, street-ordered buildings, sorted matters
307
309
  c.readings.set('flow', { reading: 'flow', gate: { anyBuilder: true }, read: async () => ({ value: 3, figure: '3 stuck', line: 'longest wait 4 days, waiting for review', hot: true }) });
308
310
  const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast', 'security.report.adjudicate'], contributions: c, now: NOW });
309
311
 
310
- assert.deepEqual(Object.keys(d).sort(), ['buildings', 'counts', 'desks', 'floors', 'generated_at', 'history', 'history_sources', 'line', 'matters', 'readings', 'sources', 'viewer', 'vocabulary'].sort());
312
+ assert.deepEqual(Object.keys(d).sort(), ['buildings', 'counts', 'desks', 'floors', 'generated_at', 'history', 'history_sources', 'line', 'matters', 'readings', 'routed', 'sources', 'viewer', 'vocabulary'].sort());
313
+ assert.equal(d.routed, false, 'no seats were read, so nothing is routed (task 1004705)');
314
+ assert.ok(d.matters.every((m) => !('route' in m)));
311
315
  assert.deepEqual(d.viewer, { id: '7' });
312
316
  // board 11 (needs) + board 12 (settled, closed this week) + blocker + report; board 13 is too old
313
317
  assert.deepEqual(d.counts, { needs: 2, moving: 1, settled: 1 });
@@ -334,6 +338,26 @@ test('the response shape: counts, line, street-ordered buildings, sorted matters
334
338
  assert.deepEqual(d.vocabulary.rules.map((x) => x.key), ['decide', 'first', 'consent', 'majority', 'unanimous', 'signoff', 'rule']);
335
339
  });
336
340
 
341
+ test('the direction desk: with the seats read, every matter carries its route and the line counts only what is yours (task 1004705)', async () => {
342
+ boardView = { open: [item()], closed: [] };
343
+ const c = none();
344
+ c.sources.set('blockers', contrib({ building: 'planning', read: async () => [{ id: 4, title: 'Which payment provider', state: 'needs', opened_at: '2026-09-20T00:00:00Z' }] }));
345
+ c.sources.set('redteam_reports', contrib({ building: 'disputes', read: async () => [{ id: 2, title: 'Security report: sign-in limits', state: 'needs', rule: 'decide', opened_at: '2026-09-29T00:00:00Z' }] }));
346
+ const seats = (ownerIds) => ({ leads: [{ building: 'planning', builder_id: 9 }], owner: { builders: ownerIds.map((id) => ({ id })) }, holders: {} });
347
+ const build = (ownerIds) => docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: c, now: NOW, seats: seats(ownerIds) });
348
+ const d = await build([7]);
349
+ assert.equal(d.routed, true);
350
+ const route = (id) => d.matters.find((m) => m.id === id).route;
351
+ assert.deepEqual([route('blockers:4').seat, route('blockers:4').yours], ['lead:planning', false], 'Planning has a Lead, and it is not the viewer');
352
+ assert.deepEqual([route('redteam_reports:2').seat, route('redteam_reports:2').fell_to_owner, route('redteam_reports:2').yours], ['owner', true, true], 'Disputes is unseated: the owner seat, which the viewer holds');
353
+ assert.deepEqual([route('board:11').seat, route('board:11').yours], ['board:charter', true], 'the sitting awaits the viewer\'s vote');
354
+ assert.deepEqual(d.counts, { needs: 3, moving: 0, settled: 0, yours: 2 });
355
+ assert.equal(d.line, '2 matters wait on you', 'the home line counts what is the viewer\'s, not all that needs somebody');
356
+ const other = await build([3]);
357
+ assert.equal(other.counts.yours, 1);
358
+ assert.equal(other.line, '1 matter waits on you');
359
+ });
360
+
337
361
  test('fairness is not_yours when the viewer can see no Disputes source (never a false "0 open")', async () => {
338
362
  boardView = { open: [], closed: [] };
339
363
  const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: none(), now: NOW });