@bongos/core 1.21.19 → 1.21.21
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.
- package/.bongos-core.json +84 -74
- package/docs/copy-inventory.md +29 -25
- package/docs/copy-registry.json +87 -30
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +9 -3
- package/docs/page-readings.json +399 -388
- package/modules/autonomy/governor-docket.js +118 -4
- package/modules/economy/cost.js +36 -0
- package/modules/economy/governor-docket.js +81 -0
- package/modules/economy/routes/cost.js +2 -0
- package/modules/government/docket-floors.js +222 -0
- package/modules/government/docket-vocab.js +20 -0
- package/modules/government/docket.js +186 -5
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.css +16 -2
- package/modules/hall-ui/public/city.html +29 -9
- package/modules/hall-ui/public/city.js +25 -2
- package/modules/hall-ui/public/city.states.json +7 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +3 -0
- package/modules/hall-ui/public/street-inside.js +99 -43
- package/modules/hall-ui/public/street.css +17 -4
- package/modules/hall-ui/public/street.html +26 -8
- package/modules/hall-ui/public/street.js +16 -2
- package/modules/hall-ui/public/street.states.json +17 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/ideas/governor-docket.js +42 -1
- package/modules/lifecycle/governor-docket.js +197 -2
- package/modules/security/routes/reports.js +16 -0
- package/modules/security/security.js +5 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +186 -2
- package/tests/hall_audit.mjs +7 -12
- package/tests/hall_city.mjs +44 -4
- package/tests/hall_street.mjs +37 -5
- package/tests/hall_street_inside.mjs +97 -3
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
|
@@ -57,6 +57,37 @@ function rulesReading(settledThisMonth) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Rules · Rules in force (task 1004527): each standing rule and how many
|
|
61
|
+
// matters it settled this month. The project records one standing rule today,
|
|
62
|
+
// the landing rule; a rule put to the council has nowhere to live yet (the
|
|
63
|
+
// docket's standing_rules source says coming). Pure.
|
|
64
|
+
const LANDING_RULE = 'Every filing lands in its goal without a vote.';
|
|
65
|
+
function rulesFloor(settledThisMonth) {
|
|
66
|
+
const n = Number(settledThisMonth) || 0;
|
|
67
|
+
return {
|
|
68
|
+
count: '1',
|
|
69
|
+
line: `1 rule · settled ${n} this month`,
|
|
70
|
+
lines: [[LANDING_RULE, `settled ${n}`], ['A new rule put to the council is coming.', 'coming']],
|
|
71
|
+
hot: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// The Rules record: what the landing rule settled, month by month. Pure.
|
|
75
|
+
function rulesHistory(months) {
|
|
76
|
+
return (months || []).filter((m) => Number(m.n) > 0).map((m) => ({
|
|
77
|
+
id: `landing:${m.month}`, title: `The landing rule settled ${m.n} ${Number(m.n) === 1 ? 'filing' : 'filings'}`,
|
|
78
|
+
at: `${m.month}-01T00:00:00.000Z`, outcome: 'without a vote',
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
async function routedByMonth(months = 6) {
|
|
82
|
+
const { rows } = await api.pool.query(
|
|
83
|
+
`SELECT to_char(created_at, 'YYYY-MM') AS month, count(*)::int AS n FROM ideas_task_lineage
|
|
84
|
+
WHERE origin = 'route' AND created_at >= date_trunc('month', now()) - (($1::int - 1) * interval '1 month')
|
|
85
|
+
GROUP BY month ORDER BY month DESC`,
|
|
86
|
+
[months],
|
|
87
|
+
);
|
|
88
|
+
return rows;
|
|
89
|
+
}
|
|
90
|
+
|
|
60
91
|
// Filings the routing rule landed this month (origin 'route').
|
|
61
92
|
async function countRoutedThisMonth() {
|
|
62
93
|
const { rows } = await api.pool.query(
|
|
@@ -66,6 +97,12 @@ async function countRoutedThisMonth() {
|
|
|
66
97
|
return rows[0] ? rows[0].n : 0;
|
|
67
98
|
}
|
|
68
99
|
|
|
100
|
+
// One read per docket request: the collector hands every read a per-request
|
|
101
|
+
// `once(key, fn)` (docket.js buildDocket), so two floors that need the same rows
|
|
102
|
+
// share one query. Without it (a direct call) the read simply runs.
|
|
103
|
+
const shared = (once, key, fn) => (typeof once === 'function' ? once(key, fn) : fn());
|
|
104
|
+
const routedFor = (once) => shared(once, 'ideas.routedThisMonth', () => countRoutedThisMonth());
|
|
105
|
+
|
|
69
106
|
function contributeIdeasToDocket() {
|
|
70
107
|
api.contribute(POINT, {
|
|
71
108
|
source: 'blockers',
|
|
@@ -81,13 +118,17 @@ function contributeIdeasToDocket() {
|
|
|
81
118
|
api.contribute(POINT, {
|
|
82
119
|
reading: 'rules',
|
|
83
120
|
gate: { anyBuilder: true },
|
|
84
|
-
read: async () => rulesReading(await
|
|
121
|
+
read: async (_ctx, { once } = {}) => rulesReading(await routedFor(once)),
|
|
85
122
|
});
|
|
123
|
+
api.contribute(POINT, { floor: 'rules.in_force', gate: { anyBuilder: true }, read: async (_ctx, { once } = {}) => rulesFloor(await routedFor(once)) });
|
|
124
|
+
api.contribute(POINT, { history: 'rules', building: 'rules', gate: { anyBuilder: true }, read: async () => rulesHistory(await routedByMonth()) });
|
|
86
125
|
}
|
|
87
126
|
|
|
88
127
|
module.exports = {
|
|
89
128
|
contributeIdeasToDocket,
|
|
90
129
|
blockerDocketMatter,
|
|
91
130
|
rulesReading,
|
|
131
|
+
rulesFloor,
|
|
132
|
+
rulesHistory,
|
|
92
133
|
countRoutedThisMonth,
|
|
93
134
|
};
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.21.21",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.21",
|
|
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.
|
|
3
|
+
"version": "1.21.21",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -8870,5 +8870,17 @@
|
|
|
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
|
+
}
|
|
8879
|
+
],
|
|
8880
|
+
"1.21.21": [
|
|
8881
|
+
{
|
|
8882
|
+
"id": "1004544",
|
|
8883
|
+
"text": "The governor city and the street now open inside the normal hall, with the sidebar and top bar around them like every other page. A full-screen button in the corner hides both so the city or the street fills the window again,"
|
|
8884
|
+
}
|
|
8873
8885
|
]
|
|
8874
8886
|
}
|
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.
|
|
78
|
+
const CORE_VERSION = '1.21.21'; // 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;
|