@bongos/core 1.21.3 → 1.21.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.bongos-core.json +128 -68
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +2 -0
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0362-the-governor-docket-is-collected-from-contributed-sources.md +34 -0
  8. package/docs/adr/README.md +1 -0
  9. package/docs/api/openapi.json +35 -1
  10. package/docs/api-reference.md +3 -2
  11. package/docs/copy-inventory.md +389 -518
  12. package/docs/copy-registry.json +880 -2143
  13. package/docs/module-api-changelog.md +4 -0
  14. package/docs/page-inventory.json +17 -25
  15. package/docs/page-readings.json +1008 -1246
  16. package/migrations/core_272_rename_cn_criterion_slugs.sql +40 -0
  17. package/modules/autonomy/governor-docket.js +101 -0
  18. package/modules/autonomy/routes/autonomy.js +5 -0
  19. package/modules/discord/board-broadcast.js +3 -1
  20. package/modules/discord/craft-broadcast.js +107 -0
  21. package/modules/discord/routes/discord.js +10 -0
  22. package/modules/discord/ship-broadcast.js +30 -2
  23. package/modules/economy/reward.js +3 -0
  24. package/modules/government/board.js +20 -5
  25. package/modules/government/docket-vocab.js +82 -0
  26. package/modules/government/docket.js +458 -0
  27. package/modules/government/routes/government.js +34 -0
  28. package/modules/hall-ui/public/genesis-home.js +9 -10
  29. package/modules/ideas/governor-docket.js +93 -0
  30. package/modules/ideas/routes/blockers.js +5 -0
  31. package/modules/lifecycle/criterion-ref.js +11 -0
  32. package/modules/lifecycle/db-tasks.js +7 -0
  33. package/modules/lifecycle/governor-docket.js +192 -0
  34. package/modules/lifecycle/routes/done-when.js +6 -0
  35. package/modules/lifecycle/routes/gate-approvals.js +78 -33
  36. package/modules/lifecycle/routes/lifecycle.js +3 -0
  37. package/modules/onboarding/routes/access-requests.js +68 -27
  38. package/modules/public-landing/public/assets/cosmos.css +6 -50
  39. package/modules/public-landing/public/projects.html +1170 -1659
  40. package/modules/public-landing/public/projects.probes.json +18 -18
  41. package/modules/public-landing/public/projects.states.json +23 -31
  42. package/modules/security/routes/reports.js +39 -0
  43. package/package-lock.json +2 -2
  44. package/package.json +1 -1
  45. package/release-notes.json +20 -0
  46. package/src/bongos/module-rechecks.js +77 -0
  47. package/src/bongos/route-rank-check.js +3 -0
  48. package/src/bongos/routes/core-update.js +41 -0
  49. package/src/bongos/routes/modules.js +17 -0
  50. package/src/module-api.js +1 -1
  51. package/tests/criterion_goal_attach.mjs +12 -4
  52. package/tests/criterion_ref_parse.mjs +41 -0
  53. package/tests/discord_craft_broadcast.mjs +162 -0
  54. package/tests/genesis_home.mjs +1 -1
  55. package/tests/government_routes.mjs +3 -1
  56. package/tests/governor_docket.mjs +552 -0
  57. package/tests/onboard.mjs +10 -4
  58. package/tests/platform_boot.mjs +1 -1
  59. package/tests/projects_hub.mjs +196 -89
  60. package/tests/projects_hub_app_status.mjs +3 -1
  61. package/tests/projects_hub_app_step.mjs +72 -48
  62. package/tests/projects_hub_dns_ready.mjs +10 -2
  63. package/tests/projects_hub_look.mjs +32 -22
  64. package/tests/projects_hub_module_picker.mjs +112 -62
  65. package/tests/projects_hub_pre_uat.mjs +48 -41
  66. package/tests/public_landing_projects.mjs +12 -6
  67. package/tests/wizard_demo.mjs +37 -11
  68. package/tests/wizard_draft_resume.mjs +34 -22
  69. package/tests/wizard_front_door.mjs +173 -103
  70. package/tests/wizard_intent_resume.mjs +28 -25
  71. package/tests/wizard_physics.mjs +94 -76
  72. package/tests/wizard_physics_more.mjs +409 -277
  73. package/tests/wizard_six_screens.mjs +126 -0
@@ -0,0 +1,552 @@
1
+ // tests/governor_docket.mjs — the governor's docket (task 1004523, goal 1000125):
2
+ // ONE read of every matter awaiting a governor, which the Governor City screens
3
+ // (tasks 1004524-1004528) all draw from. modules/government/docket.js collects
4
+ // sources contributed to the `governor.docket` point by their owning modules.
5
+ //
6
+ // What this pins:
7
+ // 1. THE MAPPER — normalizeMatter's one shape, and its fail-safe defaults (an
8
+ // unknown state is never a false "needs you"; an unknown rule is null).
9
+ // 2. THE BOARD'S PER-VIEWER STATE mirrors the /me summons (pendingVotesFor):
10
+ // needs = member, not cast, not the author, not expired; else moving/settled.
11
+ // 3. RANK SCOPING — a source the viewer may not see returns NO rows and reports
12
+ // `not_yours`; a contribution with no recognisable gate is never run; a
13
+ // gate-less or malformed one cannot widen anything (fail-closed).
14
+ // 4. NOTHING INVENTED — a design-named source with nothing recorded is `coming`,
15
+ // a module that is off is `off`, a read that throws or hangs is `unavailable`.
16
+ // 5. EVERY CONTRIBUTED SOURCE DECLARES ITS OWN LIST ROUTE'S ATOM — read off the
17
+ // route source by the route-rank auditor, so the city can never show a viewer
18
+ // a matter the owning route would refuse them.
19
+ // 6. THE ROUTE — GET /government/docket gates on board.vote.cast and answers the
20
+ // documented shape through the real router.
21
+ //
22
+ // DB-free: db reads are monkeypatched by property; the real seam carries the
23
+ // contributions.
24
+ //
25
+ // Run: node tests/governor_docket.mjs
26
+
27
+ import { strict as assert } from 'node:assert';
28
+ import { test, after } from 'node:test';
29
+ import http from 'node:http';
30
+ import fs from 'node:fs';
31
+ import path from 'node:path';
32
+ import { createRequire } from 'node:module';
33
+ import { fileURLToPath } from 'node:url';
34
+
35
+ const require = createRequire(import.meta.url);
36
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
37
+ const api = require(path.join(ROOT, 'src', 'module-api.js'));
38
+ const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
39
+ const docket = require(path.join(ROOT, 'modules', 'government', 'docket.js'));
40
+ const vocab = require(path.join(ROOT, 'modules', 'government', 'docket-vocab.js'));
41
+ const board = require(path.join(ROOT, 'modules', 'government', 'board.js'));
42
+ const govDb = require(path.join(ROOT, 'modules', 'government', 'db.js'));
43
+ const resolver = require(path.join(ROOT, 'modules', 'government', 'resolver.js'));
44
+ const lifecycleDocket = require(path.join(ROOT, 'modules', 'lifecycle', 'governor-docket.js'));
45
+ const coreUpdateRoute = require(path.join(ROOT, 'src', 'bongos', 'routes', 'core-update.js'));
46
+ const ideasDocket = require(path.join(ROOT, 'modules', 'ideas', 'governor-docket.js'));
47
+ const autonomyDocket = require(path.join(ROOT, 'modules', 'autonomy', 'governor-docket.js'));
48
+ const accessRequestsRoute = require(path.join(ROOT, 'modules', 'onboarding', 'routes', 'access-requests.js'));
49
+ const reportsRoute = require(path.join(ROOT, 'modules', 'security', 'routes', 'reports.js'));
50
+ const moduleRechecks = require(path.join(ROOT, 'src', 'bongos', 'module-rechecks.js'));
51
+ const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
52
+ const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
53
+ const express = require('express');
54
+
55
+ // ── stubs: the board and headcount reads (no DB) ─────────────────────────────
56
+
57
+ const REAL = {
58
+ boardRoomView: board.boardRoomView,
59
+ getActiveBuildersWithRank: govDb.getActiveBuildersWithRank,
60
+ countActiveBuilders: govDb.countActiveBuilders,
61
+ resolveBuilderPermissions: resolver.resolveBuilderPermissions,
62
+ };
63
+ let boardView = { open: [], closed: [] };
64
+ let members = [{ builder_id: 7, github_login: 'ana', display_name: 'Ana' }, { builder_id: 9, github_login: 'theo', display_name: 'Theo' }];
65
+ board.boardRoomView = async () => boardView;
66
+ govDb.getActiveBuildersWithRank = async () => members;
67
+ govDb.countActiveBuilders = async () => 12;
68
+ after(() => {
69
+ board.boardRoomView = REAL.boardRoomView;
70
+ govDb.getActiveBuildersWithRank = REAL.getActiveBuildersWithRank;
71
+ govDb.countActiveBuilders = REAL.countActiveBuilders;
72
+ resolver.resolveBuilderPermissions = REAL.resolveBuilderPermissions;
73
+ seams._reset();
74
+ });
75
+
76
+ const NOW = Date.parse('2026-10-01T12:00:00Z');
77
+ const CONSENT = { membership: 'rank:archon', pass_rule: 'consent' };
78
+ const item = (over = {}) => ({
79
+ id: '11', subject_type: 'constitutional_amendment', subject_id: '3',
80
+ subject: { rationale_md: '# Give Review a seat\nmore', author_id: '9' },
81
+ opened_at: '2026-09-29T00:00:00Z', closes_at: '2026-10-03T00:00:00Z',
82
+ expired: false, closed_at: null, outcome: null, constitution: CONSENT, votes: [],
83
+ ...over,
84
+ });
85
+ const none = () => ({ sources: new Map(), readings: new Map() });
86
+
87
+ // ── 1. the mapper ────────────────────────────────────────────────────────────
88
+
89
+ test('normalizeMatter: the one shape, prefixed id, ISO dates', () => {
90
+ const m = docket.normalizeMatter({
91
+ id: 5, title: 'Admit Kai', rule: 'consent', state: 'needs', opened_at: '2026-09-30T10:00:00Z',
92
+ proposer: { id: 3, login: 'theo' }, tally: { for: 2, against: '1', open: 2, needs: 'x' }, ref: { type: 'a', id: 5 },
93
+ }, { source: 'access_requests', building: 'people' });
94
+ assert.equal(m.id, 'access_requests:5');
95
+ assert.equal(m.building, 'people');
96
+ assert.equal(m.opened_at, '2026-09-30T10:00:00.000Z');
97
+ assert.equal(m.deadline, null);
98
+ assert.deepEqual(m.proposer, { id: '3', login: 'theo', name: null });
99
+ assert.deepEqual(m.tally, { for: 2, against: 1, open: 2, needs: 'x' });
100
+ assert.deepEqual(m.ref, { type: 'a', id: '5' });
101
+ assert.deepEqual(Object.keys(m).sort(), ['building', 'deadline', 'href', 'id', 'opened_at', 'proposer', 'ref', 'rule', 'short', 'source', 'state', 'tally', 'title', 'where'].sort());
102
+ });
103
+
104
+ test('normalizeMatter fails SAFE: unknown state → moving (never a false "needs you"), unknown rule → null', () => {
105
+ const m = docket.normalizeMatter({ id: 1, title: 't', state: 'urgent!!', rule: 'vibes', building: 'castle' }, { source: 's', building: 'review' });
106
+ assert.equal(m.state, 'moving');
107
+ assert.equal(m.rule, null);
108
+ assert.equal(m.building, 'review', 'an unknown building falls back to the source\'s own');
109
+ assert.equal(docket.normalizeMatter({ title: 'no id' }, { source: 's', building: 'review' }), null);
110
+ assert.equal(docket.normalizeMatter({ id: 1 }, { source: 's', building: 'review' }), null);
111
+ });
112
+
113
+ test('gateAllows is fail-closed on anything it does not recognise', () => {
114
+ const ctx = { has: (p) => p === 'x' };
115
+ assert.equal(docket.gateAllows({ permission: 'x' }, ctx), true);
116
+ assert.equal(docket.gateAllows({ permission: 'y' }, ctx), false);
117
+ assert.equal(docket.gateAllows({ anyBuilder: true }, ctx), true);
118
+ for (const g of [null, undefined, {}, { anyBuilder: 'yes' }, { permission: '' }, 'x', { rank: 'archon' }]) {
119
+ assert.equal(docket.gateAllows(g, ctx), false, JSON.stringify(g));
120
+ }
121
+ });
122
+
123
+ test('homeLine speaks the design\'s words', () => {
124
+ assert.equal(docket.homeLine({ needs: 0, moving: 0, settled: 4 }), 'nothing waits on you.');
125
+ assert.equal(docket.homeLine({ needs: 3, moving: 2, settled: 0 }), '3 matters wait on you · 2 moving');
126
+ assert.equal(docket.homeLine({ needs: 1, moving: 0, settled: 0 }), '1 matter waits on you');
127
+ });
128
+
129
+ // ── 2. the board, per viewer ─────────────────────────────────────────────────
130
+
131
+ test('board: a member who has not voted NEEDS to; the same item after their vote is MOVING', () => {
132
+ const viewer = { viewerId: '7', members };
133
+ assert.equal(docket.boardMatter(item(), viewer).state, 'needs');
134
+ const voted = item({ votes: [{ voter_id: '7', direction: 'yes', github_login: 'ana' }] });
135
+ const m = docket.boardMatter(voted, viewer);
136
+ assert.equal(m.state, 'moving');
137
+ assert.deepEqual(m.tally, { for: 1, against: 0, open: 1, needs: 'passes unless someone objects' });
138
+ assert.equal(m.rule, 'consent');
139
+ assert.equal(m.building, 'charter');
140
+ assert.equal(m.title, 'Give Review a seat');
141
+ assert.deepEqual(m.proposer, { id: '9', login: 'theo', name: 'Theo' });
142
+ assert.equal(m.deadline, '2026-10-03T00:00:00Z');
143
+ });
144
+
145
+ test('board: the author is not summoned (the /me summons rule), a non-member sees it settled, an expired one never needs you', () => {
146
+ assert.equal(docket.boardMatter(item(), { viewerId: '9', members }).state, 'moving', 'author');
147
+ assert.equal(docket.boardMatter(item(), { viewerId: '55', members }).state, 'settled', 'not a member');
148
+ assert.equal(docket.boardMatter(item({ expired: true }), { viewerId: '7', members }).state, 'moving', 'expired');
149
+ assert.equal(docket.boardMatter(item({ closed_at: '2026-09-30T00:00:00Z', outcome: 'passed' }), { viewerId: '7', members }).state, 'settled');
150
+ });
151
+
152
+ test('board: a genesis stage has no author to hold back, and a full idea stands at Planning', () => {
153
+ const stage = item({ subject_type: 'genesis_stage', subject_id: '3', subject: { label: 'Charter', author_id: '7' }, opened_by: '7' });
154
+ assert.equal(docket.boardMatter(stage, { viewerId: '7', members }).state, 'needs');
155
+ const idea = item({ subject_type: 'full_idea', subject: { title: 'A template marketplace', author_id: '9' }, constitution: { membership: 'rank:archon', pass_rule: 'unanimous' } });
156
+ const m = docket.boardMatter(idea, { viewerId: '7', members });
157
+ assert.equal(m.building, 'planning');
158
+ assert.equal(m.rule, 'unanimous');
159
+ assert.equal(m.title, 'A template marketplace');
160
+ assert.equal(docket.boardMatter(item({ constitution: { pass_rule: 'nonsense' } }), { viewerId: '7', members }).rule, null);
161
+ });
162
+
163
+ test('the city and the /me summons agree on "needs you": boardMatter and pendingVotesFor over one matrix', async () => {
164
+ // Every combination the rule turns on: membership, cast or not, author or not,
165
+ // and the subject type (a genesis stage has no author to hold back). Each case
166
+ // is one open, unexpired item seen by viewer 7; the two readers must agree.
167
+ const realAwaiting = govDb.openBoardItemsAwaitingVoter;
168
+ const realHolds = govDb.builderHoldsRankActive;
169
+ try {
170
+ for (const member of [true, false]) {
171
+ for (const voted of [true, false]) {
172
+ for (const author of ['7', '9', null]) {
173
+ for (const subjectType of ['constitutional_amendment', 'full_idea', 'genesis_stage']) {
174
+ const label = JSON.stringify({ member, voted, author, subjectType });
175
+ // pendingVotesFor reads the SQL row shape (the genesis author is nulled in SQL).
176
+ govDb.openBoardItemsAwaitingVoter = async () => [{
177
+ id: '11', subject_type: subjectType, subject_id: '3', constitution: CONSENT,
178
+ opened_at: 't', closes_at: null,
179
+ author_id: subjectType === 'genesis_stage' ? null : author,
180
+ already_voted: voted,
181
+ }];
182
+ govDb.builderHoldsRankActive = async () => member;
183
+ const summons = await board.pendingVotesFor('7');
184
+ // boardMatter reads the room's shape.
185
+ const m = docket.boardMatter(item({
186
+ subject_type: subjectType,
187
+ subject: { author_id: author, title: 't', label: 'Charter', rationale_md: 'r' },
188
+ votes: voted ? [{ voter_id: '7', direction: 'yes' }] : [],
189
+ }), { viewerId: '7', members: member ? [{ builder_id: '7' }] : [] });
190
+ assert.equal(m.state === 'needs', summons.count === 1, label);
191
+ }
192
+ }
193
+ }
194
+ }
195
+ } finally {
196
+ govDb.openBoardItemsAwaitingVoter = realAwaiting;
197
+ govDb.builderHoldsRankActive = realHolds;
198
+ }
199
+ });
200
+
201
+ test('awaitsVoter: the one definition, fail-closed', () => {
202
+ const yes = { builderId: '7', authorId: '9', alreadyVoted: false, isMember: true };
203
+ assert.equal(board.awaitsVoter(yes), true);
204
+ assert.equal(board.awaitsVoter({ ...yes, authorId: 7 }), false, 'number and string ids compare equal');
205
+ assert.equal(board.awaitsVoter({ ...yes, authorId: null }), true, 'no author, nobody held back');
206
+ assert.equal(board.awaitsVoter({ ...yes, alreadyVoted: true }), false);
207
+ assert.equal(board.awaitsVoter({ ...yes, isMember: false }), false);
208
+ assert.equal(board.awaitsVoter({ ...yes, expired: true }), false);
209
+ assert.equal(board.awaitsVoter({ ...yes, closed: true }), false);
210
+ assert.equal(board.awaitsVoter({ ...yes, builderId: '' }), false);
211
+ assert.equal(board.awaitsVoter(), false);
212
+ });
213
+
214
+ // ── 3 + 4. scoping, statuses, the collector ──────────────────────────────────
215
+
216
+ const VIEWER = { id: 7, rank: 'metic' };
217
+ const contrib = (over) => ({ building: 'release', gate: { anyBuilder: true }, read: async () => [], ...over });
218
+
219
+ test('a source the viewer may not see returns NO rows and reports not_yours — and its read never runs', async () => {
220
+ boardView = { open: [], closed: [] };
221
+ let ran = false;
222
+ const c = none();
223
+ c.sources.set('override_requests', contrib({ gate: { permission: 'override_request.decide' }, read: async () => { ran = true; return [{ id: 1, title: 'secret', state: 'needs' }]; } }));
224
+ const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: c, now: NOW });
225
+ assert.equal(ran, false);
226
+ assert.equal(d.matters.length, 0);
227
+ assert.equal(d.sources.find((s) => s.source === 'override_requests').status, 'not_yours');
228
+ // The same viewer holding the atom sees it.
229
+ const d2 = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast', 'override_request.decide'], contributions: c, now: NOW });
230
+ assert.equal(d2.matters.length, 1);
231
+ assert.equal(d2.matters[0].id, 'override_requests:1');
232
+ assert.equal(d2.sources.find((s) => s.source === 'override_requests').status, 'live');
233
+ });
234
+
235
+ test('the board itself is scoped too: without board.vote.cast it is not_yours', async () => {
236
+ boardView = { open: [item()], closed: [] };
237
+ const d = await docket.buildDocket({ viewer: VIEWER, permissions: [], contributions: none(), now: NOW });
238
+ assert.equal(d.sources.find((s) => s.source === 'board').status, 'not_yours');
239
+ assert.equal(d.matters.length, 0);
240
+ const d2 = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: none(), now: NOW });
241
+ assert.equal(d2.matters.length, 1);
242
+ assert.equal(d2.matters[0].id, 'board:11');
243
+ });
244
+
245
+ test('a contribution without a recognisable gate is DROPPED, never run (registeredContributions)', () => {
246
+ seams._reset();
247
+ seams.contribute(docket.CONTRIBUTION_POINT, { source: 'blockers', building: 'planning', read: async () => [] });
248
+ seams.contribute(docket.CONTRIBUTION_POINT, { source: 'goal_join_requests', building: 'people', gate: { rank: 'xenos' }, read: async () => [] });
249
+ seams.contribute(docket.CONTRIBUTION_POINT, { source: 'removal_proposals', building: 'disputes', gate: { permission: 'task.promote' } });
250
+ seams.contribute(docket.CONTRIBUTION_POINT, { source: 'criteria_uat', building: 'review', gate: { permission: 'criterion.review' }, read: async () => [] });
251
+ const reg = docket.registeredContributions();
252
+ assert.deepEqual([...reg.sources.keys()], ['criteria_uat']);
253
+ seams._reset();
254
+ });
255
+
256
+ test('nothing is invented: coming / off / unavailable are each reported, with no rows', async () => {
257
+ boardView = { open: [], closed: [] };
258
+ const c = none();
259
+ c.sources.set('blockers', contrib({ read: async () => { throw new Error('db down'); } }));
260
+ c.sources.set('core_update', contrib({ read: () => new Promise(() => {}) })); // never answers
261
+ const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: c, now: NOW, timeoutMs: 30 });
262
+ const by = Object.fromEntries(d.sources.map((s) => [s.source, s]));
263
+ assert.equal(by.blockers.status, 'unavailable');
264
+ assert.equal(by.core_update.status, 'unavailable');
265
+ assert.match(by.core_update.reason, /in time/);
266
+ assert.equal(by.access_requests.status, 'off', 'no contributor registered → its module is off');
267
+ assert.equal(by.module_rechecks.status, 'off', 'module re-checks are a real source now, never coming');
268
+ for (const k of ['grade_appeals', 'standing_rules', 'treasury_spending']) {
269
+ assert.equal(by[k].status, 'coming', k);
270
+ assert.ok(by[k].reason, `${k} says why`);
271
+ }
272
+ assert.equal(d.matters.length, 0);
273
+ // every source the design names is reported, in declared order, once
274
+ const keys = d.sources.map((s) => s.source);
275
+ assert.equal(new Set(keys).size, keys.length);
276
+ for (const e of docket.EXPECTED_SOURCES) assert.ok(keys.includes(e.source), e.source);
277
+ assert.equal(keys[0], 'board');
278
+ });
279
+
280
+ test('the response shape: counts, line, street-ordered buildings, sorted matters, six readings, vocabulary', async () => {
281
+ boardView = { open: [item()], closed: [item({ id: '12', closed_at: '2026-09-30T00:00:00Z', outcome: 'passed' }), item({ id: '13', closed_at: '2026-08-01T00:00:00Z', outcome: 'passed' })] };
282
+ const c = none();
283
+ c.sources.set('blockers', contrib({ building: 'planning', read: async () => [{ id: 4, title: 'Which payment provider', state: 'moving', opened_at: '2026-09-20T00:00:00Z' }] }));
284
+ c.sources.set('redteam_reports', contrib({ building: 'disputes', gate: { permission: 'security.report.adjudicate' }, read: async () => [{ id: 2, title: 'Security report: sign-in limits', state: 'needs', rule: 'decide', opened_at: '2026-09-29T00:00:00Z' }] }));
285
+ 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 }) });
286
+ const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast', 'security.report.adjudicate'], contributions: c, now: NOW });
287
+
288
+ assert.deepEqual(Object.keys(d).sort(), ['buildings', 'counts', 'generated_at', 'line', 'matters', 'readings', 'sources', 'viewer', 'vocabulary'].sort());
289
+ assert.deepEqual(d.viewer, { id: '7' });
290
+ // board 11 (needs) + board 12 (settled, closed this week) + blocker + report; board 13 is too old
291
+ assert.deepEqual(d.counts, { needs: 2, moving: 1, settled: 1 });
292
+ assert.equal(d.line, '2 matters wait on you · 1 moving');
293
+ assert.deepEqual(d.matters.map((m) => m.state), ['needs', 'needs', 'moving', 'settled'], 'needs first');
294
+ assert.equal(d.matters[0].id, 'board:11', 'among needs, the soonest deadline first');
295
+ assert.deepEqual(d.buildings.map((b) => b.key), vocab.BUILDING_KEYS);
296
+ const charter = d.buildings.find((b) => b.key === 'charter');
297
+ assert.deepEqual(charter.counts, { needs: 1, moving: 0, settled: 1 });
298
+ assert.deepEqual(charter.matter_ids.sort(), ['board:11', 'board:12']);
299
+
300
+ const r = Object.fromEntries(d.readings.map((x) => [x.key, x]));
301
+ assert.deepEqual(d.readings.map((x) => x.key), ['flow', 'promises', 'money', 'people', 'fairness', 'rules']);
302
+ assert.equal(r.flow.status, 'live');
303
+ assert.equal(r.flow.figure, '3 stuck');
304
+ assert.equal(r.people.figure, '12 builders');
305
+ assert.equal(r.fairness.status, 'live');
306
+ assert.equal(r.fairness.value, 1, 'one open dispute the viewer can see');
307
+ assert.equal(r.fairness.hot, true);
308
+ assert.equal(r.money.status, 'off', 'no autonomy contributor in this test, never a made-up figure');
309
+ assert.equal(r.rules.status, 'off');
310
+ assert.equal(r.promises.status, 'off', 'no lifecycle contributor in this test');
311
+ assert.deepEqual(d.vocabulary.states, ['needs', 'moving', 'settled']);
312
+ assert.deepEqual(d.vocabulary.rules.map((x) => x.key), ['decide', 'first', 'consent', 'majority', 'unanimous', 'signoff', 'rule']);
313
+ });
314
+
315
+ test('fairness is not_yours when the viewer can see no Disputes source (never a false "0 open")', async () => {
316
+ boardView = { open: [], closed: [] };
317
+ const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: none(), now: NOW });
318
+ assert.equal(d.readings.find((x) => x.key === 'fairness').status, 'not_yours');
319
+ });
320
+
321
+ // ── lifecycle + core mappers ─────────────────────────────────────────────────
322
+
323
+ test('lifecycle mappers: override, removal, UAT, join request', () => {
324
+ const o = lifecycleDocket.overrideMatter({ id: 3, task_id: 99, task_title: 'Refund flow', requested_by_builder_id: 9, requester_login: 'theo', requester_display_name: 'Theo', created_at: 't' }, true);
325
+ assert.equal(o.state, 'needs');
326
+ assert.equal(o.title, 'Override: Refund flow');
327
+ assert.equal(o.href, '/task/99');
328
+ assert.equal(lifecycleDocket.overrideMatter({ id: 3, task_id: 99 }, false).state, 'moving');
329
+ const rm = lifecycleDocket.removalMatter({ proposal_task_id: '50', title: 'Was the old search page removed?', goal_title: 'Hall', filed_at: 't' });
330
+ assert.equal(rm.where, 'disputes · removal · Hall');
331
+ const u = lifecycleDocket.uatMatter({ id: 8, criterion_md: 'Refunds work', goal_id: 4, goal_title: 'Payments', shipped_total: 3 });
332
+ assert.equal(u.rule, 'signoff');
333
+ assert.equal(u.href, '/goals#/goal/4');
334
+ const j = lifecycleDocket.joinRequestMatter({ id: 2, goal_id: 4, goal_title: 'Payments', builder_id: 11, github_login: 'kai', display_name: 'Kai', created_at: 't' }, false);
335
+ assert.equal(j.title, 'Kai asks to join Payments');
336
+ assert.equal(j.state, 'needs');
337
+ assert.equal(lifecycleDocket.joinRequestMatter({ id: 2, goal_id: 4, goal_title: 'Payments' }, true).state, 'moving');
338
+ });
339
+
340
+ test('flow and promises readings', () => {
341
+ const day = 24 * 60 * 60 * 1000;
342
+ const f = lifecycleDocket.flowReading({ completed: [{ updated_at: new Date(NOW - 4 * day).toISOString() }, { updated_at: new Date(NOW - day).toISOString() }], confirmed: [{ updated_at: new Date(NOW).toISOString() }] }, NOW);
343
+ assert.deepEqual(f, { value: 3, figure: '3 stuck', line: 'longest wait 4 days, waiting for review', hot: true });
344
+ assert.equal(lifecycleDocket.flowReading({}, NOW).figure, 'all moving');
345
+ assert.equal(lifecycleDocket.promisesReading([{ satisfied: true }, { satisfied: false }, { satisfied: true }]).figure, '2 of 3 kept');
346
+ assert.equal(lifecycleDocket.promisesReading([]).value, null);
347
+ });
348
+
349
+ test('core update: behind is a matter, current is none, no answer is an error (never "up to date")', () => {
350
+ const m = coreUpdateRoute.coreUpdateDocketMatters({ reading: 'ok', behind: true, latest: '1.21.0', running: '1.20.75' });
351
+ assert.equal(m.length, 1);
352
+ assert.equal(m[0].title, 'Take core 1.21.0 (running 1.20.75)');
353
+ assert.deepEqual(coreUpdateRoute.coreUpdateDocketMatters({ reading: 'ok', behind: false }), []);
354
+ assert.throws(() => coreUpdateRoute.coreUpdateDocketMatters({ reading: 'no_answer', behind: false }));
355
+ });
356
+
357
+ test('blockers: needs only a viewer who may resolve one; the hold count reads in words', () => {
358
+ const b = { id: 4, title: 'Which payment provider', created_at: 't', blocking_task_ids: [1, 2, 3] };
359
+ const m = ideasDocket.blockerDocketMatter(b, true);
360
+ assert.equal(m.state, 'needs');
361
+ assert.equal(m.where, 'planning · holds 3 tasks');
362
+ assert.equal(m.href, '/blockers#blocker-4');
363
+ assert.equal(ideasDocket.blockerDocketMatter({ ...b, blocking_task_ids: [9] }, false).state, 'moving');
364
+ assert.equal(ideasDocket.blockerDocketMatter({ ...b, blocking_task_ids: [9] }, false).where, 'planning · holds 1 task');
365
+ assert.equal(ideasDocket.blockerDocketMatter({ ...b, blocking_task_ids: [] }, true).where, 'planning · waiting on a decision');
366
+ });
367
+
368
+ test('rules at work: the routing rule and what it settled this month', () => {
369
+ assert.deepEqual(ideasDocket.rulesReading(45), {
370
+ value: { rules: 1, settled_this_month: 45 }, figure: '1 rule',
371
+ line: 'the landing rule settled 45 filings this month without a vote', hot: false,
372
+ });
373
+ assert.equal(ideasDocket.rulesReading(1).line, 'the landing rule settled 1 filing this month without a vote');
374
+ assert.equal(ideasDocket.rulesReading(0).line, 'the landing rule settled nothing this month');
375
+ });
376
+
377
+ test('the fence: a matter only when it needs a governor, and only a fence manager is told it needs them', () => {
378
+ assert.deepEqual(autonomyDocket.fenceDocketMatters({ enabled: true, goals: [{ goal_id: 1 }] }, true), []);
379
+ const off = autonomyDocket.fenceDocketMatters({ enabled: false, paused_reason: 'payments touched', goals: [{ goal_id: 1 }], updated_by: 3 }, true);
380
+ assert.equal(off.length, 1);
381
+ assert.equal(off[0].title, 'Unattended building is switched off');
382
+ assert.equal(off[0].where, 'automation · the switch · payments touched');
383
+ assert.equal(off[0].state, 'needs');
384
+ assert.equal(autonomyDocket.fenceDocketMatters({ enabled: false, goals: [] }, false)[0].state, 'settled');
385
+ assert.equal(autonomyDocket.fenceDocketMatters({ enabled: true, goals: [] }, true)[0].title, 'Unattended building is on, but no goal is allowed');
386
+ });
387
+
388
+ test('money: what unattended building may still spend this month, never negative, hot under a fifth', () => {
389
+ const m = autonomyDocket.moneyReading({ ceilingUsd: 30, spentUsd: 12.5 });
390
+ assert.equal(m.figure, '$17.50 left');
391
+ assert.equal(m.line, 'unattended building this month · $12.50 of $30 spent');
392
+ assert.equal(m.value.scope, 'unattended building, this month');
393
+ assert.equal(m.hot, false);
394
+ assert.equal(autonomyDocket.moneyReading({ ceilingUsd: 30, spentUsd: 27 }).hot, true);
395
+ assert.equal(autonomyDocket.moneyReading({ ceilingUsd: 30, spentUsd: 41 }).figure, '$0 left');
396
+ });
397
+
398
+ test('access requests and red-team reports map to their buildings words', () => {
399
+ const a = accessRequestsRoute.accessRequestDocketMatter({ id: 5, github_login: 'kai', display_name: 'Kai', vouched: true, created_at: 't' });
400
+ assert.equal(a.title, 'Admit Kai as a builder');
401
+ assert.equal(a.where, 'people · waiting to join · vouched');
402
+ assert.equal(a.state, 'needs');
403
+ assert.equal(accessRequestsRoute.accessRequestDocketMatter({ id: 5, github_login: 'kai', vouched: false }).where, 'people · waiting to join');
404
+ const r = reportsRoute.redteamDocketMatter({ id: 2, title: 'sign-in limits', severity: 'high', status: 'repro_attempted', reporter_builder_id: 8, reported_at: 't' });
405
+ assert.equal(r.title, 'Security report: sign-in limits');
406
+ assert.equal(r.short, 'high report');
407
+ assert.equal(r.where, 'disputes · security report · being reproduced');
408
+ assert.deepEqual(r.proposer, { id: 8 });
409
+ assert.equal(reportsRoute.redteamDocketMatter({ id: 2, title: 'x', status: 'open' }).where, 'disputes · security report');
410
+ });
411
+
412
+ test('module re-checks: a failed gate needs a governor who may ask; a pending part is moving; the query reads the CURRENT signal', async () => {
413
+ const failed = moduleRechecks.moduleRecheckMatter({ version_id: 7, module_key: 'image-tools', version: '1.2.0', security_failed: true, pending_parts: null, measured_at: 't' }, true);
414
+ assert.equal(failed.state, 'needs');
415
+ assert.equal(failed.title, 'image-tools 1.2.0 failed its security check');
416
+ assert.equal(failed.where, 'release · modules · flagged for a re-check');
417
+ assert.equal(moduleRechecks.moduleRecheckMatter({ version_id: 7, module_key: 'm', version: '1.0.0', security_failed: true }, false).state, 'moving');
418
+ const pending = moduleRechecks.moduleRecheckMatter({ version_id: 8, module_key: 'm', version: '1.0.0', security_failed: false, pending_parts: ['tests'] }, true);
419
+ assert.equal(pending.state, 'moving');
420
+ assert.equal(pending.where, 'release · modules · tests pending');
421
+ let sql = null;
422
+ await moduleRechecks.listModuleRechecks({ db: { query: async (q, params) => { sql = q; assert.deepEqual(params, [50]); return { rows: [] }; } } });
423
+ assert.match(sql, /DISTINCT ON \(s\.version_id, s\.part\)/, 'the newest signal per part, not every historical row');
424
+ assert.match(sql, /ORDER BY s\.version_id, s\.part, s\.measured_at DESC, s\.id DESC/);
425
+ assert.match(sql, /outcome = 'failed'/);
426
+ assert.match(sql, /outcome = 'pending'/);
427
+ // The DISTINCT ON walks core_265's own composite index, whose column order is the
428
+ // query's ORDER BY exactly — pinned so a reorder of either side fails here.
429
+ const mig = fs.readFileSync(path.join(ROOT, 'migrations', 'core_265_module_assessments.sql'), 'utf8');
430
+ assert.match(mig, /idx_module_assessment_signals_current\s+ON module_assessment_signals \(version_id, part, measured_at DESC, id DESC\)/);
431
+ });
432
+
433
+ test('the six readings, end to end with every module contributing: each reading is live or names why not', async () => {
434
+ seams._reset();
435
+ for (const f of ['modules/ideas/routes/blockers.js', 'modules/lifecycle/routes/lifecycle.js', 'modules/autonomy/routes/autonomy.js']) require(path.join(ROOT, f))();
436
+ const reg = docket.registeredContributions();
437
+ // Stub each contributed reading's read so no DB is touched; the GATE stays real.
438
+ const stubbed = { sources: new Map(), readings: new Map() };
439
+ const answers = {
440
+ flow: { value: 3, figure: '3 stuck', line: 'l', hot: true },
441
+ promises: { value: { kept: 11, total: 17 }, figure: '11 of 17 kept', line: 'l', hot: false },
442
+ money: { value: {}, figure: '$17.50 left', line: 'l', hot: false },
443
+ rules: { value: {}, figure: '1 rule', line: 'l', hot: false },
444
+ };
445
+ for (const [k, c] of reg.readings) stubbed.readings.set(k, { ...c, read: async () => answers[k] });
446
+ boardView = { open: [], closed: [] };
447
+ const metic = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast', 'autonomy.precheck', 'security.report.adjudicate'], contributions: stubbed, now: NOW });
448
+ const by = Object.fromEntries(metic.readings.map((x) => [x.key, x]));
449
+ for (const k of ['flow', 'promises', 'money', 'people', 'rules']) assert.equal(by[k].status, 'live', k);
450
+ assert.equal(by.money.figure, '$17.50 left');
451
+ // A viewer without the precheck atom does not see the spend ceiling.
452
+ const xenos = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: stubbed, now: NOW });
453
+ assert.equal(xenos.readings.find((x) => x.key === 'money').status, 'not_yours');
454
+ assert.equal(xenos.readings.find((x) => x.key === 'money').figure, null);
455
+ seams._reset();
456
+ });
457
+
458
+ // ── 5. every contributed source declares its own list route's atom ───────────
459
+
460
+ test('each contributed source gates on EXACTLY the atom its own list route requires', async () => {
461
+ seams._reset();
462
+ const factories = [
463
+ 'modules/ideas/routes/blockers.js',
464
+ 'modules/lifecycle/routes/lifecycle.js',
465
+ 'modules/lifecycle/routes/gate-approvals.js',
466
+ 'modules/onboarding/routes/access-requests.js',
467
+ 'modules/security/routes/reports.js',
468
+ 'modules/autonomy/routes/autonomy.js',
469
+ 'src/bongos/routes/core-update.js',
470
+ 'src/bongos/routes/modules.js',
471
+ ];
472
+ for (const f of factories) require(path.join(ROOT, f))();
473
+ const reg = docket.registeredContributions();
474
+ const routePerm = (file, key) => {
475
+ const r = rrc.classifyText(fs.readFileSync(path.join(ROOT, file), 'utf8')).find((x) => x.key === key);
476
+ assert.ok(r, `${key} declared in ${file}`);
477
+ return r.perms;
478
+ };
479
+ const expect = {
480
+ override_requests: ['modules/lifecycle/routes/tasks.js', 'GET /override-requests'],
481
+ removal_proposals: ['modules/lifecycle/routes/removal-proposals.js', 'GET /removal-proposals'],
482
+ criteria_uat: ['modules/lifecycle/routes/done-when.js', 'GET /done-when/pending-review'],
483
+ gate_approvals: ['modules/lifecycle/routes/gate-approvals.js', 'GET /gate-approvals'],
484
+ access_requests: ['modules/onboarding/routes/access-requests.js', 'GET /access-requests'],
485
+ redteam_reports: ['modules/security/routes/reports.js', 'GET /security/reports'],
486
+ core_update: ['src/bongos/routes/core-update.js', 'GET /core-update'],
487
+ };
488
+ for (const [source, [file, key]] of Object.entries(expect)) {
489
+ const c = reg.sources.get(source);
490
+ assert.ok(c, `${source} is contributed`);
491
+ assert.deepEqual([c.gate.permission], routePerm(file, key), `${source} gates on ${key}'s atom`);
492
+ }
493
+ // The own-scoped / public reads say so explicitly, and nothing else is anyBuilder.
494
+ const anyBuilder = [...reg.sources].filter(([, c]) => c.gate.anyBuilder === true).map(([k]) => k).sort();
495
+ assert.deepEqual(anyBuilder, ['autobongos_fence', 'blockers', 'goal_join_requests']);
496
+ for (const [k, c] of reg.sources) {
497
+ const decl = docket.EXPECTED_SOURCES.find((e) => e.source === k);
498
+ assert.ok(decl, `${k} is a declared source`);
499
+ assert.equal(c.building, decl.building, `${k} stands where the docket says`);
500
+ }
501
+ assert.equal(reg.sources.get('module_rechecks').gate.permission, 'module.assessment.rerun',
502
+ 'module re-checks gate on the reassess route atom (there is no assessment read route)');
503
+ assert.deepEqual(routePerm('src/bongos/routes/modules.js', 'POST /store/modules/:key/versions/:version/reassess'), ['module.assessment.rerun']);
504
+ assert.deepEqual([...reg.readings.keys()].sort(), ['flow', 'money', 'promises', 'rules']);
505
+ assert.equal(reg.readings.get('money').gate.permission, 'autonomy.precheck');
506
+ assert.deepEqual(routePerm('modules/autonomy/routes/autonomy.js', 'GET /autonomy/precheck'), ['autonomy.precheck']);
507
+ // Every reading the street draws has a contributor once the modules are mounted.
508
+ for (const r of vocab.READINGS) {
509
+ if (r.key === 'people' || r.key === 'fairness') continue; // answered inside government
510
+ assert.ok(reg.readings.has(r.key), 'reading ' + r.key + ' is contributed');
511
+ }
512
+ seams._reset();
513
+ });
514
+
515
+ // ── 6. the route ─────────────────────────────────────────────────────────────
516
+
517
+ const realRequireBuilder = api.requireBuilder;
518
+ api.requireBuilder = (req, res, next) => { req.builder = { id: 7, rank: 'metic' }; next(); };
519
+ let held = ['board.vote.cast'];
520
+ function grant(keys) {
521
+ held = keys;
522
+ seams._reset();
523
+ seams.registerProvider('government', { resolveBuilderPermissions: async () => held });
524
+ }
525
+ grant(['board.vote.cast']);
526
+ resolver.resolveBuilderPermissions = async () => held;
527
+ const buildGovernmentRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'));
528
+ const app = express();
529
+ app.use(express.json());
530
+ app.use(attachFail);
531
+ app.use(buildGovernmentRouter());
532
+ const server = http.createServer(app);
533
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
534
+ const BASE = `http://127.0.0.1:${server.address().port}`;
535
+ after(() => { server.close(); api.requireBuilder = realRequireBuilder; });
536
+
537
+ test('GET /government/docket: 403 without board.vote.cast, the documented shape with it', async () => {
538
+ boardView = { open: [item()], closed: [] };
539
+ grant([]);
540
+ let res = await fetch(`${BASE}/government/docket`);
541
+ assert.equal(res.status, 403);
542
+ grant(['board.vote.cast']);
543
+ // a hidden source contributed AFTER the router was built is still scoped
544
+ seams.contribute(docket.CONTRIBUTION_POINT, { source: 'access_requests', building: 'people', gate: { permission: 'access_request.review' }, read: async () => [{ id: 1, title: 'Admit Kai', state: 'needs' }] });
545
+ res = await fetch(`${BASE}/government/docket`);
546
+ assert.equal(res.status, 200);
547
+ const body = await res.json();
548
+ assert.equal(body.viewer.id, '7');
549
+ assert.deepEqual(body.matters.map((m) => m.id), ['board:11']);
550
+ assert.equal(body.sources.find((s) => s.source === 'access_requests').status, 'not_yours');
551
+ assert.ok(!JSON.stringify(body).includes('Admit Kai'), 'a hidden source leaks nothing');
552
+ });
package/tests/onboard.mjs CHANGED
@@ -81,10 +81,16 @@ test('every form OFFERS the optional step and names what skipping costs — skil
81
81
  assert.match(cli, /Press Enter to skip/, 'the terminal form offers the skip in the prompt itself');
82
82
  assert.match(cli, /off the public map/, 'and says what a missing description means');
83
83
 
84
- // the web form
85
- const hub = read('modules/public-landing/public/projects.html');
86
- assert.match(hub, /id="panel-detail"/, 'the wizard ships the panel');
87
- assert.match(hub, /id="skipDetail"/, 'with a skip control of its own');
84
+ // the web form — since task 1004504 the optional step is no panel of its own: the
85
+ // sentence is an optional field on "What are you making?" whose label names what
86
+ // leaving it empty costs, and the team shape is the review's Team size row
87
+ const hub = read('modules/public-landing/public/projects.html').replace(/\r\n/g, '\n');
88
+ const what = (hub.match(/<section class="panel" id="panel-what"[\s\S]*?\n {10}<\/section>/) || [''])[0];
89
+ assert.ok(what, 'the wizard ships the screen that carries the sentence');
90
+ assert.match(what, /<label class="wizField"><span>In a sentence <small>\(optional — puts it on the public map\)<\/small><\/span>\s*<input type="text" id="f-desc"/,
91
+ 'the sentence is offered as optional, and its label says what skipping costs');
92
+ assert.match(hub, /<div class="setRow" data-row="team">[\s\S]*?id="teamCards"/, 'the team shape rides the review\'s Team size row');
93
+ assert.doesNotMatch(hub, /id="panel-detail"|id="skipDetail"/, 'the old About panel and its skip control are gone');
88
94
 
89
95
  // the route that receives it
90
96
  const route = read('modules/provisioning/routes/provisioning.js');
@@ -147,7 +147,7 @@ test('/create redirects into the hub wizard, seed intact (task 1002763)', async
147
147
  assert.equal(hub.status, 200);
148
148
  assert.match(hub.headers['content-type'] || '', /text\/html/);
149
149
  assert.match(hub.body, /window\.__BRANDING__=/);
150
- assert.match(hub.body, /Create a project/);
150
+ assert.match(hub.body, /<h1 id="gate-h">Make something\.<\/h1>/); // the wizard's sign-in screen (task 1004504)
151
151
  // the wizard files the request against the provisioning API, nothing else
152
152
  assert.match(hub.body, /provisioning\/instances/);
153
153
  });