@bongos/core 1.21.19 → 1.21.20

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.
@@ -123,7 +123,192 @@ function promisesReading(criteria) {
123
123
  return { value: { kept, total }, figure: `${kept} of ${total} kept`, line: 'what the goals say they deliver', hot: false };
124
124
  }
125
125
 
126
+ // ── floors and the record (task 1004527) ─────────────────────────────────────
127
+ // Each is pure over rows the existing reads already return; the registration
128
+ // below gives each the atom of the route that serves those rows (GET /goals,
129
+ // GET /versions, GET /tasks and GET /public/recent-shipped are any builder's).
130
+
131
+ const MAX_LINES = 4;
132
+ const SHIPPED_LIMIT = 200;
133
+ const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
134
+ function monthStart(now) { const d = new Date(now); return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1); }
135
+ function dayWord(at) { const d = new Date(at); return Number.isNaN(d.getTime()) ? '' : `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`; }
136
+ function plural(n, one, many) { return `${n} ${n === 1 ? one : many}`; }
137
+ function more(lines, total) { if (total > lines.length) lines.push([`and ${total - lines.length} more`, '']); return lines; }
138
+
139
+ // Planning · Goals: each open goal with its promises kept of total.
140
+ function goalsFloor(goals, criteria) {
141
+ const open = (goals || []).filter((g) => g.status === 'open');
142
+ const by = new Map();
143
+ for (const c of criteria || []) {
144
+ if (c.goal_id == null) continue;
145
+ const k = String(c.goal_id);
146
+ if (!by.has(k)) by.set(k, { kept: 0, total: 0 });
147
+ const e = by.get(k); e.total += 1; if (c.satisfied === true) e.kept += 1;
148
+ }
149
+ const lines = open.slice(0, MAX_LINES).map((g) => {
150
+ const e = by.get(String(g.id));
151
+ return [String(g.title), e ? `${e.kept} of ${e.total} kept` : 'no promises yet'];
152
+ });
153
+ return { count: String(open.length), line: open.length ? plural(open.length, 'goal open', 'goals open') : 'no goal open', lines: more(lines, open.length), hot: false };
154
+ }
155
+
156
+ // Planning · Priorities: the building version's open goals in their worked order.
157
+ function prioritiesFloor(goals, versionId) {
158
+ const mine = (goals || []).filter((g) => g.status === 'open' && (!versionId || g.version_id === versionId));
159
+ if (!mine.length) return { count: '', line: 'no goal in the order yet', lines: [['No open goal is ordered in the version building now.', '']], hot: false };
160
+ const top = mine.slice(0, 3).map((g, i) => `${i + 1} ${g.title}`).join(' · ');
161
+ return { count: '', line: 'the order goals are worked in', lines: [[top, versionId ? `version ${versionId}` : ''], [`${plural(mine.length, 'goal is', 'goals are')} in the order.`, '']], hot: false };
162
+ }
163
+
164
+ // Planning · Versions: what is building, and the last that shipped.
165
+ function versionsFloor(versions, goals) {
166
+ const all = versions || [];
167
+ const building = all.filter((v) => v.status === 'building');
168
+ const shipped = all.filter((v) => v.status === 'shipped' && v.shipped_at).sort((a, b) => Date.parse(b.shipped_at) - Date.parse(a.shipped_at));
169
+ const goalsIn = (id) => (goals || []).filter((g) => g.version_id === id && g.status !== 'archived').length;
170
+ const lines = building.slice(0, MAX_LINES).map((v) => [`${v.name || v.id} is building.`, plural(goalsIn(v.id), 'goal', 'goals')]);
171
+ if (shipped[0]) lines.push([`${shipped[0].name || shipped[0].id} shipped.`, dayWord(shipped[0].shipped_at)]);
172
+ const head = building[0];
173
+ return {
174
+ count: '',
175
+ line: head ? `${head.name || head.id} building · ${plural(goalsIn(head.id), 'goal', 'goals')} in it` : 'no version building',
176
+ lines: lines.length ? lines : [['No version is recorded yet.', '']],
177
+ hot: false,
178
+ };
179
+ }
180
+
181
+ // Review · Signed off: promises a tester has marked kept, newest first.
182
+ function signedOffFloor(criteria, now) {
183
+ const done = (criteria || []).filter((c) => c.satisfied === true && c.checked_at).sort((a, b) => Date.parse(b.checked_at) - Date.parse(a.checked_at));
184
+ const month = done.filter((c) => Date.parse(c.checked_at) >= monthStart(now)).length;
185
+ return {
186
+ count: String(month),
187
+ line: done.length ? `${month} signed off this month` : 'nothing signed off yet',
188
+ lines: done.slice(0, MAX_LINES).map((c) => [String(c.criterion_md), dayWord(c.checked_at)]),
189
+ hot: false,
190
+ };
191
+ }
192
+
193
+ // Release · Live this month: shipped work, newest first.
194
+ function liveMonthFloor(shipped, now) {
195
+ const month = (shipped || []).filter((t) => t.shipped_at && Date.parse(t.shipped_at) >= monthStart(now));
196
+ return {
197
+ count: String(month.length),
198
+ line: month.length ? `${plural(month.length, 'piece', 'pieces')} of work went live` : 'nothing went live yet this month',
199
+ lines: more(month.slice(0, MAX_LINES).map((t) => [String(t.title), dayWord(t.shipped_at)]), month.length),
200
+ hot: false,
201
+ };
202
+ }
203
+
204
+ // Release · Held back: approved work that cannot go live until it catches up.
205
+ function heldBackFloor(confirmed) {
206
+ const held = (confirmed || []).filter((t) => t.needs_rebase === true);
207
+ return {
208
+ count: String(held.length),
209
+ line: held.length ? `${plural(held.length, 'piece', 'pieces')} held` : 'nothing held',
210
+ lines: more(held.slice(0, MAX_LINES).map((t) => [String(t.title), 'must catch up first']), held.length),
211
+ hot: false,
212
+ };
213
+ }
214
+
215
+ // People · Newcomers: newcomer tasks being built, and the ones waiting.
216
+ function newcomersFloor(active, ready) {
217
+ const building = (active || []).filter((t) => t.newcomer_friendly === true);
218
+ const waiting = (ready || []).filter((t) => t.newcomer_friendly === true);
219
+ return {
220
+ count: String(building.length),
221
+ line: `${plural(building.length, 'newcomer task', 'newcomer tasks')} being built`,
222
+ lines: [...building.slice(0, MAX_LINES - 1).map((t) => [String(t.title), 'being built']), [`${plural(waiting.length, 'newcomer task is', 'newcomer tasks are')} ready to claim.`, '']],
223
+ hot: false,
224
+ };
225
+ }
226
+
227
+ // The record: sign-offs, releases, goals achieved. Pure.
228
+ function signoffHistory(criteria) {
229
+ return (criteria || []).filter((c) => c.satisfied === true && c.checked_at)
230
+ .map((c) => ({ id: c.id, title: String(c.criterion_md), at: c.checked_at, outcome: 'signed off' }));
231
+ }
232
+ function releaseHistory(shipped) {
233
+ return (shipped || []).filter((t) => t.shipped_at).map((t) => ({ id: t.id, title: String(t.title), at: t.shipped_at, outcome: 'live' }));
234
+ }
235
+ function goalHistory(goals) {
236
+ return (goals || []).filter((g) => g.status === 'achieved' && g.updated_at)
237
+ .map((g) => ({ id: g.id, title: `${g.title} achieved`, at: g.updated_at, outcome: 'all promises kept' }));
238
+ }
239
+
240
+ // The criteria of the versions the open goals live in — one read per version,
241
+ // bounded, through the same listCriteria the version page serves.
242
+ async function criteriaFor(versionIds) {
243
+ const ids = [...new Set(versionIds.filter(Boolean))].slice(0, 6);
244
+ return (await Promise.all(ids.map((v) => doneWhen.listCriteria(v)))).flat();
245
+ }
246
+
247
+ // One read per docket request: the collector hands every read a per-request
248
+ // `once(key, fn)` (docket.js buildDocket), so two floors that need the same rows
249
+ // share one query. Without it (a direct call) the read simply runs.
250
+ const shared = (once, key, fn) => (typeof once === 'function' ? once(key, fn) : fn());
251
+ const openGoals = (once) => shared(once, 'lifecycle.openGoals', () => db.listGoals({ status: 'open' }));
252
+ const confirmedTasks = (once) => shared(once, 'lifecycle.confirmedTasks', () => db.listTasks({ status: 'confirmed', limit: STUCK_LIMIT }));
253
+
254
+ function contributeLifecycleFloors() {
255
+ const any = { anyBuilder: true };
256
+ api.contribute(POINT, {
257
+ floor: 'planning.goals', gate: any,
258
+ read: async (_ctx, { once } = {}) => {
259
+ const goals = await openGoals(once);
260
+ return goalsFloor(goals, await criteriaFor(goals.map((g) => g.version_id)));
261
+ },
262
+ });
263
+ api.contribute(POINT, {
264
+ floor: 'planning.priorities', gate: any,
265
+ read: async (_ctx, { once } = {}) => prioritiesFloor(await openGoals(once), await db.currentBuildingVersionId()),
266
+ });
267
+ api.contribute(POINT, {
268
+ floor: 'planning.versions', gate: any,
269
+ read: async () => versionsFloor(await db.listVersions(), await db.listGoals({})),
270
+ });
271
+ api.contribute(POINT, {
272
+ floor: 'review.signed_off', gate: any,
273
+ read: async (_ctx, { now } = {}) => {
274
+ const v = await db.currentBuildingVersionId();
275
+ return signedOffFloor(v ? await doneWhen.listCriteria(v) : [], now);
276
+ },
277
+ });
278
+ api.contribute(POINT, {
279
+ floor: 'release.live_month', gate: any,
280
+ read: async (_ctx, { now } = {}) => liveMonthFloor(await db.recentShippedTasks(SHIPPED_LIMIT, { days: 31 }), now),
281
+ });
282
+ api.contribute(POINT, {
283
+ floor: 'release.held_back', gate: any,
284
+ read: async (_ctx, { once } = {}) => heldBackFloor(await confirmedTasks(once)),
285
+ });
286
+ api.contribute(POINT, {
287
+ floor: 'people.newcomers', gate: any,
288
+ read: async () => {
289
+ const [active, ready] = await Promise.all([db.listTasks({ status: 'active', limit: STUCK_LIMIT }), db.listTasks({ status: 'ready', limit: 200 })]);
290
+ return newcomersFloor(active, ready);
291
+ },
292
+ });
293
+ api.contribute(POINT, {
294
+ history: 'signoffs', building: 'review', gate: any,
295
+ read: async () => {
296
+ const v = await db.currentBuildingVersionId();
297
+ return signoffHistory(v ? await doneWhen.listCriteria(v) : []);
298
+ },
299
+ });
300
+ api.contribute(POINT, {
301
+ history: 'releases', building: 'release', gate: any,
302
+ read: async () => releaseHistory(await db.recentShippedTasks(60, { days: 120 })),
303
+ });
304
+ api.contribute(POINT, {
305
+ history: 'goals', building: 'planning', gate: any,
306
+ read: async () => goalHistory(await db.listGoals({ status: 'achieved' })),
307
+ });
308
+ }
309
+
126
310
  function contributeLifecycleToDocket() {
311
+ contributeLifecycleFloors();
127
312
  api.contribute(POINT, {
128
313
  source: 'override_requests',
129
314
  building: 'release',
@@ -162,10 +347,10 @@ function contributeLifecycleToDocket() {
162
347
  api.contribute(POINT, {
163
348
  reading: 'flow',
164
349
  gate: { anyBuilder: true },
165
- read: async (_ctx, { now } = {}) => {
350
+ read: async (_ctx, { now, once } = {}) => {
166
351
  const [completed, confirmed] = await Promise.all([
167
352
  db.listTasks({ status: 'completed', limit: STUCK_LIMIT }),
168
- db.listTasks({ status: 'confirmed', limit: STUCK_LIMIT }),
353
+ confirmedTasks(once),
169
354
  ]);
170
355
  return flowReading({ completed, confirmed }, now);
171
356
  },
@@ -189,4 +374,14 @@ module.exports = {
189
374
  joinRequestMatter,
190
375
  flowReading,
191
376
  promisesReading,
377
+ goalsFloor,
378
+ prioritiesFloor,
379
+ versionsFloor,
380
+ signedOffFloor,
381
+ liveMonthFloor,
382
+ heldBackFloor,
383
+ newcomersFloor,
384
+ signoffHistory,
385
+ releaseHistory,
386
+ goalHistory,
192
387
  };
@@ -69,6 +69,21 @@ function contributeReportsToDocket() {
69
69
  return lists.flat().map(redteamDocketMatter);
70
70
  },
71
71
  });
72
+ // The Disputes record (task 1004527): every report that was ruled on, dated by
73
+ // its ruling (confirmed or fixed). Same atom as the list route it reads.
74
+ api.contribute('governor.docket', {
75
+ history: 'rulings',
76
+ building: 'disputes',
77
+ gate: { permission: 'security.report.adjudicate' },
78
+ read: async () => rulingHistory((await Promise.all(['confirmed', 'fixed'].map((status) => security.listVulnerabilityReports({ scope: 'all', status, limit: 60 })))).flat()),
79
+ });
80
+ }
81
+
82
+ const RULING_WORDS = Object.freeze({ confirmed: 'confirmed', fixed: 'fixed', disputed: 'disputed', wontfix: 'will not fix' });
83
+ function rulingHistory(reports) {
84
+ return (reports || [])
85
+ .filter((r) => RULING_WORDS[r.status] && (r.fixed_at || r.confirmed_at))
86
+ .map((r) => ({ id: r.id, title: `Security report: ${r.title}`, at: r.fixed_at || r.confirmed_at, outcome: RULING_WORDS[r.status] }));
72
87
  }
73
88
 
74
89
  module.exports = function buildSecurityReportsRouter() {
@@ -302,3 +317,4 @@ module.exports = function buildSecurityReportsRouter() {
302
317
  };
303
318
 
304
319
  module.exports.redteamDocketMatter = redteamDocketMatter;
320
+ module.exports.rulingHistory = rulingHistory;
@@ -155,7 +155,7 @@ async function createVulnerabilityReport({
155
155
  //
156
156
  // `scope: 'own'` REQUIRES builderId; omitting it throws rather than silently
157
157
  // leaking every report. Optional `status` narrows either scope.
158
- async function listVulnerabilityReports({ scope = 'own', builderId = null, status = null } = {}) {
158
+ async function listVulnerabilityReports({ scope = 'own', builderId = null, status = null, limit = null } = {}) {
159
159
  if (scope !== 'own' && scope !== 'all') {
160
160
  throw { code: 'INVALID_SCOPE', scope, allowed: ['own', 'all'] };
161
161
  }
@@ -176,7 +176,10 @@ async function listVulnerabilityReports({ scope = 'own', builderId = null, statu
176
176
  `SELECT ${VULN_COLUMNS} FROM vulnerability_reports` +
177
177
  (where.length ? ` WHERE ${where.join(' AND ')}` : '') +
178
178
  ` ORDER BY reported_at DESC, id DESC`;
179
- const { rows } = await pool.query(sql, params);
179
+ // Bounded when the caller asks (the governor docket's record, task 1004527).
180
+ let tail = '';
181
+ if (Number.isInteger(limit) && limit > 0) { params.push(limit); tail = ` LIMIT $${params.length}`; }
182
+ const { rows } = await pool.query(sql + tail, params);
180
183
  return rows;
181
184
  }
182
185
 
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.19",
3
+ "version": "1.21.20",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.19",
9
+ "version": "1.21.20",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.19",
3
+ "version": "1.21.20",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -8870,5 +8870,11 @@
8870
8870
  "id": "1004530",
8871
8871
  "text": "Inside any building on the street, \"change how this works\" now drafts a real amendment. It shows how the building decides today crossed out, and the proposed change written in. You pick who decides and how from the project's o"
8872
8872
  }
8873
+ ],
8874
+ "1.21.20": [
8875
+ {
8876
+ "id": "1004527",
8877
+ "text": "Inside every Governor City building, the floors now show real numbers instead of \"not recorded yet\". The Charter reads each section from the rules in force. People shows who sits on the council, the testers, the ranks and newc"
8878
+ }
8873
8879
  ]
8874
8880
  }
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.19'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.20'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -82,7 +82,7 @@ const item = (over = {}) => ({
82
82
  expired: false, closed_at: null, outcome: null, constitution: CONSENT, votes: [],
83
83
  ...over,
84
84
  });
85
- const none = () => ({ sources: new Map(), readings: new Map() });
85
+ const none = () => ({ sources: new Map(), readings: new Map(), floors: new Map(), histories: new Map() });
86
86
 
87
87
  // ── 1. the mapper ────────────────────────────────────────────────────────────
88
88
 
@@ -285,7 +285,7 @@ test('the response shape: counts, line, street-ordered buildings, sorted matters
285
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
286
  const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast', 'security.report.adjudicate'], contributions: c, now: NOW });
287
287
 
288
- assert.deepEqual(Object.keys(d).sort(), ['buildings', 'counts', 'generated_at', 'line', 'matters', 'readings', 'sources', 'viewer', 'vocabulary'].sort());
288
+ assert.deepEqual(Object.keys(d).sort(), ['buildings', 'counts', 'floors', 'generated_at', 'history', 'history_sources', 'line', 'matters', 'readings', 'sources', 'viewer', 'vocabulary'].sort());
289
289
  assert.deepEqual(d.viewer, { id: '7' });
290
290
  // board 11 (needs) + board 12 (settled, closed this week) + blocker + report; board 13 is too old
291
291
  assert.deepEqual(d.counts, { needs: 2, moving: 1, settled: 1 });
@@ -512,6 +512,190 @@ test('each contributed source gates on EXACTLY the atom its own list route requi
512
512
  seams._reset();
513
513
  });
514
514
 
515
+ // ── 5b. floors and the record (task 1004527) ────────────────────────────────
516
+
517
+ const floorsDocket = require(path.join(ROOT, 'modules', 'government', 'docket-floors.js'));
518
+ const economyDocket = require(path.join(ROOT, 'modules', 'economy', 'governor-docket.js'));
519
+
520
+ const GOV = {
521
+ board: { membership: 'rank:metic+', pass_rule: 'majority' },
522
+ members: [{ builder_id: '7', github_login: 'ana', display_name: 'Ana' }, { builder_id: '9', github_login: 'theo', display_name: null }],
523
+ decisions: [
524
+ { area: 'people', name: 'People', who: 'whoever may manage ranks', rule: 'decide', rule_words: 'decides alone', as_today: true, decided_by: 'gate', gate: 'government.manage' },
525
+ { area: 'rules', name: 'Rules', who: 'whoever may change the project settings', rule: 'decide', rule_words: 'decides alone', as_today: true, decided_by: 'gate' },
526
+ { area: 'review', name: 'Review', who: 'testers', rule: 'signoff', rule_words: 'signed off by someone who did not build it', as_today: false, decided_by: 'rank:tester', set_by_item_id: '40' },
527
+ { area: 'release', name: 'Release', who: 'no one yet', rule: 'vacant', rule_words: 'no one holds this yet', as_today: false, decided_by: 'nobody' },
528
+ { area: 'treasury', name: 'Treasury', who: 'whoever may set spending limits', rule: 'decide', rule_words: 'decides alone', as_today: true, decided_by: 'gate' },
529
+ ],
530
+ holders: { review: [{ builder_id: '3', github_login: 'lu', display_name: 'Lu' }], release: [], charter: null },
531
+ genesis: { stages: [{ key: 'charter', label: 'the charter', state: 'passed', item_id: '1', closed_at: '2026-07-04T00:00:00Z' }, { key: 'pay', label: 'who can be paid', state: 'sitting', item_id: '2' }] },
532
+ ranks: [{ rank_key: 'metic', label: 'Metic', holders: 4 }, { rank_key: 'archon', label: 'Archon', holders: 1 }],
533
+ amendments: [{ item_id: '40', kind: 'decision_rules', rationale_md: '# Testers hold Review', closed_at: '2026-10-01T09:00:00Z', outcome: 'passed' }, { item_id: '41', kind: 'constitution', rationale_md: '', closed_at: '2026-09-02T09:00:00Z', outcome: 'returned' }],
534
+ closed: [{ id: '50', subject_type: 'full_idea', subject_id: '8', subject: { title: 'A template marketplace' }, closed_at: '2026-09-20T00:00:00Z', outcome: 'passed' }, { id: '40', subject_type: 'constitutional_amendment', closed_at: '2026-10-01T09:00:00Z', outcome: 'passed' }],
535
+ assignments: [{ builder_id: '3', github_login: 'lu', display_name: 'Lu', rank_key: 'tester', rank_label: 'Tester', is_system: false, assigned_at: '2026-09-30T00:00:00Z' }],
536
+ };
537
+
538
+ test('floors: every floor the design names is reported, and nothing is invented for one nobody answers', async () => {
539
+ const c = none();
540
+ let ran = false;
541
+ c.floors.set('release.live_month', { floor: 'release.live_month', gate: { anyBuilder: true }, read: async () => ({ count: 3, line: '3 pieces of work went live', lines: [['Invite flow', 'sep 26'], 'not a line', [''], ['x'.repeat(400), 'm']], hot: 'yes' }) });
542
+ c.floors.set('automation.running', { floor: 'automation.running', gate: { permission: 'autonomy.fence.manage' }, read: async () => { ran = true; return { count: '2' }; } });
543
+ c.floors.set('treasury.hosting', { floor: 'treasury.hosting', gate: { anyBuilder: true }, read: async () => { throw new Error('db down'); } });
544
+ const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: c, now: NOW });
545
+ assert.deepEqual(d.floors.map((f) => f.key), vocab.FLOORS.map((f) => f.key), 'every declared floor, in order');
546
+ const by = Object.fromEntries(d.floors.map((f) => [f.key, f]));
547
+ assert.deepEqual([by['release.live_month'].status, by['release.live_month'].count, by['release.live_month'].hot], ['live', '3', false]);
548
+ assert.deepEqual(by['release.live_month'].lines.map((l) => l[0].length), [11, 200], 'only real [text, meta] lines, bounded');
549
+ assert.deepEqual([by['automation.running'].status, by['automation.running'].count, ran], ['not_yours', '', false], 'a gated floor never runs');
550
+ assert.deepEqual([by['treasury.hosting'].status, by['treasury.hosting'].count], ['unavailable', '']);
551
+ assert.deepEqual([by['charter.purpose'].status, by['charter.purpose'].count], ['coming', '']);
552
+ assert.match(by['charter.purpose'].reason, /no written purpose/);
553
+ assert.deepEqual([by['treasury.grading'].status, by['treasury.grading'].count], ['off', ''], 'no contributor: off, no figure');
554
+ assert.equal(by['charter.council'].status, 'unavailable', 'no government read: unavailable, never an empty council');
555
+ assert.equal(by['planning.ideas'].status, 'live', 'ideas follow the board, which is live here');
556
+ });
557
+
558
+ test('floors: the government\'s own — council, sections, founding, ranks, testers — from the rows it read', async () => {
559
+ boardView = { open: [item({ id: '60', subject_type: 'full_idea', subject_id: '9', subject: { title: 'Weekly digest', author_id: '3' }, constitution: { membership: 'rank:metic+', pass_rule: 'majority' } })], closed: [] };
560
+ const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: none(), now: NOW, decisions: GOV.decisions, government: GOV });
561
+ const by = Object.fromEntries(d.floors.map((f) => [f.key, f]));
562
+ assert.deepEqual([by['charter.council'].count, by['charter.council'].line], ['2', '2 seats · passes on more than half saying yes']);
563
+ assert.deepEqual(by['charter.council'].lines[0], ['Ana and theo.', 'seated now']);
564
+ assert.deepEqual(by['people.council'].lines[2], ['Seats have no term yet. Elections and recalls are coming.', 'coming']);
565
+ assert.deepEqual([by['charter.founding'].line, by['charter.founding'].hot], ['1 of 2 founding steps passed', true]);
566
+ assert.deepEqual(by['charter.review'].lines, [['Held by testers: signed off by someone who did not build it.', 'amended']]);
567
+ assert.deepEqual([by['charter.release'].line, by['charter.release'].lines[0][1]], ['held by no one yet', 'amended']);
568
+ assert.equal(by['charter.money'].line, 'whoever may set spending limits · decides alone');
569
+ assert.match(by['charter.membership'].line, /metic and above/);
570
+ assert.deepEqual([by['people.ranks'].count, by['people.ranks'].lines[0]], ['2', ['Metic', '4 people']]);
571
+ assert.match(by['people.ranks'].lines.at(-1)[0], /Promotion by a vote is coming/);
572
+ assert.deepEqual([by['review.testers'].count, by['review.testers'].line], ['1', 'Lu']);
573
+ assert.deepEqual([by['planning.ideas'].count, by['planning.ideas'].lines[0][0]], ['1', 'Weekly digest']);
574
+ // who holds each building: the people its row seats now
575
+ const b = Object.fromEntries(d.buildings.map((x) => [x.key, x]));
576
+ assert.deepEqual([b.review.holders, b.review.holder_count], [[{ id: '3', login: 'lu', name: 'Lu' }], 1]);
577
+ assert.deepEqual([b.release.holders, b.release.holder_count], [[], 0], 'vacant: no one, said as no one');
578
+ assert.deepEqual([b.charter.holders, b.planning.holders], [null, null], 'unresolved is null, never an invented seat');
579
+ // without the docket's own atom none of it shows
580
+ const out = await docket.buildDocket({ viewer: VIEWER, permissions: [], contributions: none(), now: NOW, decisions: GOV.decisions, government: GOV });
581
+ assert.ok(out.floors.filter((f) => floorsDocket.BUILDERS[f.key]).every((f) => f.status === 'not_yours' && f.count === ''));
582
+ assert.ok(out.buildings.every((x) => x.holders === null), 'holders are not shown without it');
583
+ assert.deepEqual(out.history, []);
584
+ });
585
+
586
+ test('the record: what closed, from every keeper, newest first; gated keepers never run; undated rows dropped', async () => {
587
+ boardView = { open: [], closed: [] };
588
+ const c = none();
589
+ let ran = false;
590
+ c.histories.set('releases', { history: 'releases', building: 'release', gate: { anyBuilder: true }, read: async () => [{ id: 9, title: 'Invite flow', at: '2026-09-26T00:00:00Z', outcome: 'live' }, { id: 10, title: 'No date', at: null }] });
591
+ c.histories.set('rulings', { history: 'rulings', building: 'disputes', gate: { permission: 'security.report.adjudicate' }, read: async () => { ran = true; return []; } });
592
+ const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: c, now: NOW, decisions: GOV.decisions, government: GOV });
593
+ assert.equal(ran, false);
594
+ assert.deepEqual(d.history.map((h) => [h.id, h.building, h.outcome]), [
595
+ ['government:amendment:40', 'charter', 'passed'],
596
+ ['government:rank:3:tester', 'people', 'granted'],
597
+ ['releases:9', 'release', 'live'],
598
+ ['government:board:50', 'planning', 'ratified'],
599
+ ['government:amendment:41', 'charter', 'returned'],
600
+ ['government:founding:1', 'charter', 'passed'],
601
+ ]);
602
+ assert.equal(d.history[4].title, 'A change to the charter', 'an amendment with no rationale reads as one');
603
+ assert.deepEqual(d.history_sources.map((s) => [s.source, s.status]), [['government', 'live'], ['releases', 'live'], ['rulings', 'not_yours']]);
604
+ });
605
+
606
+ test('lifecycle floors: goals kept of total, priorities, versions, signed off, live this month, held back, newcomers', () => {
607
+ const goals = [{ id: 1, title: 'Hall', status: 'open', version_id: 'V2' }, { id: 2, title: 'Payments', status: 'open', version_id: 'V2' }, { id: 3, title: 'Old', status: 'achieved', version_id: 'V1', updated_at: '2026-09-01T00:00:00Z' }];
608
+ const crit = [{ id: 'c1', goal_id: 1, satisfied: true, criterion_md: 'Search works', checked_at: '2026-10-01T00:00:00Z' }, { id: 'c2', goal_id: 1, satisfied: false }, { id: 'c3', goal_id: 9, satisfied: true, criterion_md: 'Old one', checked_at: '2026-09-03T00:00:00Z' }];
609
+ assert.deepEqual(lifecycleDocket.goalsFloor(goals, crit).lines, [['Hall', '1 of 2 kept'], ['Payments', 'no promises yet']]);
610
+ assert.equal(lifecycleDocket.goalsFloor(goals, crit).count, '2');
611
+ assert.deepEqual(lifecycleDocket.prioritiesFloor(goals, 'V2').lines[0], ['1 Hall · 2 Payments', 'version V2']);
612
+ assert.equal(lifecycleDocket.versionsFloor([{ id: 'V2', name: 'V2', status: 'building' }, { id: 'V1', name: 'V1', status: 'shipped', shipped_at: '2026-09-28T00:00:00Z' }], goals).line, 'V2 building · 2 goals in it');
613
+ const so = lifecycleDocket.signedOffFloor(crit, NOW);
614
+ assert.deepEqual([so.count, so.line, so.lines[0]], ['1', '1 signed off this month', ['Search works', 'oct 1']]);
615
+ const shipped = [{ id: 1, title: 'A', shipped_at: '2026-10-01T03:00:00Z' }, { id: 2, title: 'B', shipped_at: '2026-09-30T23:00:00Z' }];
616
+ assert.deepEqual([lifecycleDocket.liveMonthFloor(shipped, NOW).count, lifecycleDocket.liveMonthFloor(shipped, NOW).lines], ['1', [['A', 'oct 1']]], 'only this month');
617
+ assert.deepEqual(lifecycleDocket.heldBackFloor([{ title: 'X', needs_rebase: true }, { title: 'Y', needs_rebase: false }]).count, '1');
618
+ assert.equal(lifecycleDocket.newcomersFloor([{ title: 'N', newcomer_friendly: true }, { title: 'M' }], [{ newcomer_friendly: true }, { newcomer_friendly: true }]).lines.at(-1)[0], '2 newcomer tasks are ready to claim.');
619
+ assert.deepEqual(lifecycleDocket.signoffHistory(crit).map((h) => h.id), ['c1', 'c3']);
620
+ assert.deepEqual(lifecycleDocket.goalHistory(goals).map((h) => h.title), ['Old achieved']);
621
+ assert.deepEqual(lifecycleDocket.releaseHistory(shipped).map((h) => h.at), ['2026-10-01T03:00:00Z', '2026-09-30T23:00:00Z']);
622
+ });
623
+
624
+ test('automation, treasury and disputes floors read their own ledgers, and say so when there is nothing', () => {
625
+ const ag = autonomyDocket.allowedGoalsFloor({ priority_goal_id: 7, goals: [{ goal_id: 7, added_at: '2026-09-20T00:00:00Z' }, { goal_id: 8, added_at: '2026-09-02T00:00:00Z' }] });
626
+ assert.deepEqual([ag.count, ag.lines], ['2', [['goal 7', 'first in line'], ['goal 8', 'since sep 2']]]);
627
+ assert.equal(autonomyDocket.allowedGoalsFloor({ goals: [] }).line, 'no goal allowed');
628
+ const describe = (r) => ({ ok: r.alive, state: r.alive ? 'alive' : 'dead' });
629
+ const run = autonomyDocket.runningFloor([{ display_name: 'Ana', runners: [{ alive: true, working_task_id: '12' }, { alive: false, working_task_id: '13' }, { alive: true, working_task_id: null }] }], NOW, describe);
630
+ assert.deepEqual([run.count, run.lines], ['1', [['task 12', 'Ana']]], 'only a live runner on a task is building');
631
+ assert.deepEqual([autonomyDocket.spendingLimitFloor({ ceilingUsd: 40, spentUsd: 12 }).line, autonomyDocket.spendingLimitFloor({ ceilingUsd: 40, spentUsd: 35 }).hot], ['$40 a month · $12 used', true]);
632
+ const rep = autonomyDocket.reportsFloor([{ routine: 'builder', decision: 'go', created_at: '2026-09-30T00:00:00Z' }, { routine: 'idea-triage', decision: 'no-go', reason: 'nothing open', created_at: '2026-09-01T00:00:00Z' }], NOW);
633
+ assert.deepEqual([rep.count, rep.lines.map((l) => l[0])], ['1', ['The builder went ahead.', 'Idea sorting held back: nothing open.']], 'said in words, not the gate\'s keys');
634
+ const months = [{ month: '2026-10', rewards_credits: 1180, rewarded_builders: 6, grading_usd: 310.4, hosting_usd: 120 }, { month: '2026-09', rewards_credits: 4920, rewarded_builders: 8, grading_usd: 0, hosting_usd: 160 }];
635
+ assert.deepEqual([economyDocket.rewardsFloor(months, NOW).count, economyDocket.rewardsFloor(months, NOW).line], ['$1,180', '$1,180 this month · 6 builders']);
636
+ assert.deepEqual(economyDocket.hostingFloor(months, NOW).lines[1], ['$160 in sep.', 'last month']);
637
+ assert.equal(economyDocket.gradingFloor([], NOW).count, '$0', 'a month with no rows is a real zero, read from the ledger');
638
+ assert.deepEqual(economyDocket.treasuryHistory(months).map((h) => h.title), ['Paid $1,180 to 6 builders', 'AI grading cost $310', 'Hosting cost $120', 'Paid $4,920 to 8 builders', 'Hosting cost $160']);
639
+ assert.deepEqual(reportsRoute.rulingHistory([{ id: 1, title: 'sign-in limits', status: 'fixed', fixed_at: '2026-08-02T00:00:00Z' }, { id: 2, title: 'open one', status: 'open' }, { id: 3, title: 'undated', status: 'wontfix' }]).map((h) => [h.title, h.outcome]), [['Security report: sign-in limits', 'fixed']]);
640
+ });
641
+
642
+ test('rules in force: the landing rule and what it settled; the record by month', () => {
643
+ assert.deepEqual(ideasDocket.rulesFloor(18), { count: '1', line: '1 rule · settled 18 this month', lines: [['Every filing lands in its goal without a vote.', 'settled 18'], ['A new rule put to the council is coming.', 'coming']], hot: false });
644
+ assert.deepEqual(ideasDocket.rulesHistory([{ month: '2026-10', n: 3 }, { month: '2026-09', n: 0 }]).map((h) => [h.title, h.at]), [['The landing rule settled 3 filings', '2026-10-01T00:00:00.000Z']]);
645
+ });
646
+
647
+ test('one ledger read per docket request: the treasury floors and record share it, and so do money and the spending limit', async () => {
648
+ const costMod = require(path.join(ROOT, 'modules', 'economy', 'cost.js'));
649
+ const autoDb = require(path.join(ROOT, 'modules', 'autonomy', 'db.js'));
650
+ const realFence = autoDb.readFence;
651
+ let fences = 0;
652
+ autoDb.readFence = async () => { fences += 1; return { enabled: true, goals: [], updated_at: '2026-09-02T00:00:00Z' }; };
653
+ const realMonths = costMod.treasuryMonths;
654
+ let reads = 0;
655
+ costMod.treasuryMonths = async () => { reads += 1; return [{ month: '2026-10', rewards_credits: 10, rewarded_builders: 1, grading_usd: 2, hosting_usd: 3 }]; };
656
+ let spends = 0;
657
+ seams._reset();
658
+ seams.registerProvider('reward', { autonomousApiSpendMtd: async () => { spends += 1; return 5; } });
659
+ try {
660
+ for (const f of ['modules/economy/routes/cost.js', 'modules/autonomy/routes/autonomy.js']) require(path.join(ROOT, f))();
661
+ const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast', 'autonomy.precheck'], now: NOW + 1 });
662
+ const by = Object.fromEntries(d.floors.map((f) => [f.key, f]));
663
+ assert.deepEqual([by['treasury.rewards'].count, by['treasury.hosting'].count, by['automation.spending_limit'].status], ['$10', '$3', 'live']);
664
+ assert.equal(reads, 1, 'one treasury read for three floors and the record');
665
+ assert.equal(spends, 1, 'one spend read for the money reading and the spending limit');
666
+ assert.equal(fences, 1, 'one fence read for the switch, allowed goals and the switch\'s record');
667
+ await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], now: NOW + 2 });
668
+ assert.equal(reads, 2, 'a new request reads afresh');
669
+ // two requests in flight at once, even at the same instant, each read once
670
+ reads = 0; spends = 0;
671
+ await Promise.all([1, 2, 3].map(() => docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast', 'autonomy.precheck'], now: NOW + 3 })));
672
+ assert.deepEqual([reads, spends], [3, 3], 'one read per request, never shared across requests');
673
+ } finally {
674
+ costMod.treasuryMonths = realMonths;
675
+ autoDb.readFence = realFence;
676
+ seams._reset();
677
+ }
678
+ });
679
+
680
+ test('each floor and record gates on the atom of the route that serves its rows', async () => {
681
+ seams._reset();
682
+ for (const f of ['modules/ideas/routes/blockers.js', 'modules/lifecycle/routes/lifecycle.js', 'modules/security/routes/reports.js', 'modules/autonomy/routes/autonomy.js', 'modules/economy/routes/cost.js']) require(path.join(ROOT, f))();
683
+ const reg = docket.registeredContributions();
684
+ const routePerm = (file, key) => rrc.classifyText(fs.readFileSync(path.join(ROOT, file), 'utf8')).find((x) => x.key === key).perms;
685
+ assert.deepEqual([reg.floors.get('automation.running').gate.permission], routePerm('modules/autonomy/routes/autonomy.js', 'GET /autonomy/runners/all'));
686
+ for (const k of ['automation.spending_limit', 'automation.reports']) assert.deepEqual([reg.floors.get(k).gate.permission], routePerm('modules/autonomy/routes/autonomy.js', 'GET /autonomy/runs'));
687
+ assert.deepEqual([reg.histories.get('rulings').gate.permission], routePerm('modules/security/routes/reports.js', 'GET /security/reports'));
688
+ const anyBuilder = [...reg.floors].filter(([, c]) => c.gate.anyBuilder === true).map(([k]) => k).sort();
689
+ assert.deepEqual(anyBuilder, ['automation.allowed_goals', 'people.newcomers', 'planning.goals', 'planning.priorities', 'planning.versions', 'release.held_back', 'release.live_month', 'review.signed_off', 'rules.in_force', 'treasury.grading', 'treasury.hosting', 'treasury.rewards']);
690
+ // every declared floor has exactly one answer: a contributor, the government, or a coming reason
691
+ const own = new Set([...Object.keys(floorsDocket.BUILDERS), 'planning.ideas']);
692
+ for (const f of vocab.FLOORS) {
693
+ const answers = [reg.floors.has(f.key), own.has(f.key), docket.COMING_FLOORS.some((c) => c.floor === f.key)].filter(Boolean).length;
694
+ assert.equal(answers, 1, `${f.key} has one answer`);
695
+ }
696
+ seams._reset();
697
+ });
698
+
515
699
  // ── 6. the route ─────────────────────────────────────────────────────────────
516
700
 
517
701
  const realRequireBuilder = api.requireBuilder;