@bongos/core 1.24.71 → 1.24.73

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 (35) hide show
  1. package/.bongos-core.json +57 -37
  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/api/openapi.json +36 -2
  8. package/docs/api-reference.md +3 -2
  9. package/docs/copy-inventory.md +4 -4
  10. package/docs/copy-registry.json +4 -4
  11. package/docs/module-api-changelog.md +4 -0
  12. package/docs/page-readings.json +84 -84
  13. package/modules/government/board.js +19 -7
  14. package/modules/government/config.js +21 -9
  15. package/modules/government/constitution.js +224 -0
  16. package/modules/government/db.js +28 -0
  17. package/modules/government/migrations/government_037_constitution.sql +33 -0
  18. package/modules/government/module.json +2 -1
  19. package/modules/government/pollers/constitution-restore.js +45 -0
  20. package/modules/government/routes/government.js +7 -7
  21. package/modules/hall-ui/public/board-room.js +7 -3
  22. package/modules/hall-ui/public/street-card.js +2 -0
  23. package/modules/hall-ui/public/style.css +1 -1
  24. package/package-lock.json +2 -2
  25. package/package.json +1 -1
  26. package/release-notes.json +12 -0
  27. package/scripts/bongos/government-walk.js +7 -0
  28. package/src/bongos/route-rank-check.js +4 -0
  29. package/src/module-api.js +1 -1
  30. package/tests/genesis_government.mjs +11 -0
  31. package/tests/government_board_amendment.mjs +10 -0
  32. package/tests/government_charter_forms.mjs +10 -0
  33. package/tests/government_constitution_durable.mjs +304 -0
  34. package/tests/government_seed.mjs +1 -0
  35. package/tests/hall_tokens.mjs +19 -0
@@ -76,13 +76,23 @@ async function underMonarchy(fn) {
76
76
  closeBoardItem: async ({ outcome }) => ({ id: '21', subject_type: 'constitutional_amendment', subject_id: '5', opened_by: FOUNDER, closed_at: 'T', outcome }),
77
77
  insertAuthorRatifyKarma: async () => true,
78
78
  getAmendment: async () => ({ id: '5', proposed: seen.filed.proposed }),
79
+ // The durable row (task 1005140) — written first, read back after.
80
+ saveConstitutionRow: async (args) => { seen.row = { board: args.board }; return seen.row; },
81
+ getConstitutionRow: async () => seen.row || null,
79
82
  };
80
83
  for (const [k, v] of Object.entries(stubs)) { originals[k] = db[k]; db[k] = v; }
81
84
  const originalApply = config.applyBoardAmendment;
82
85
  config.applyBoardAmendment = (proposed) => { seen.applied.push(proposed); return proposed; };
86
+ // The read-back after the apply (task 1005140) sees what was applied.
87
+ const originalLoad = config.loadGovernmentConfig;
88
+ config.loadGovernmentConfig = (opts = {}) => {
89
+ const out = originalLoad(opts);
90
+ return seen.applied.length ? { ...out, board: config.sanitizeBoard(seen.applied[seen.applied.length - 1]) } : out;
91
+ };
83
92
  try { await fn(seen); } finally {
84
93
  for (const [k, v] of Object.entries(originals)) db[k] = v;
85
94
  config.applyBoardAmendment = originalApply;
95
+ config.loadGovernmentConfig = originalLoad;
86
96
  }
87
97
  return seen;
88
98
  }
@@ -111,6 +121,7 @@ for (const form of ['timocratic-republic', 'direct-democracy']) {
111
121
  assert.equal((await board.maybeCloseAfterVote(out.charter.item_id)).outcome, 'passed');
112
122
  });
113
123
  assert.equal(seen.applied.length, 1, 'the ratified constitution is applied once');
124
+ assert.equal(charter.readCharter(seen.row.board).form, form, 'and recorded in the durable row');
114
125
  assert.equal(charter.readCharter(seen.applied[0]).form, form, 'the constitution in force is the government the founder picked');
115
126
  assert.match(seen.filed.rationaleMd, /^Charter: /, 'the record says in words what was chosen');
116
127
  });
@@ -207,10 +207,14 @@ test('the monarchy act end to end: the sole Archon ratifies their OWN amendment
207
207
  // applyBoardAmendment by property lookup at call time for exactly this.
208
208
  const origApply = config.applyBoardAmendment;
209
209
  config.applyBoardAmendment = (proposed) => origApply(proposed, { instancePath: tmp });
210
+ // ...and the read-back after it (task 1005140) reads the same temp file.
211
+ const origLoad = config.loadGovernmentConfig;
212
+ config.loadGovernmentConfig = (opts = {}) => origLoad({ ...opts, instancePath: tmp });
210
213
  try {
211
214
  const karma = [];
212
215
  let votes = [];
213
216
  let authorAsked = 0;
217
+ let savedRow = null;
214
218
  await withDb({
215
219
  getBoardItemForVote: async () => amendmentItem(),
216
220
  builderHoldsRankActive: async (builderId, rankKeys) => builderId === '42' && rankKeys.includes('archon'),
@@ -228,6 +232,9 @@ test('the monarchy act end to end: the sole Archon ratifies their OWN amendment
228
232
  // reached ONLY by the post-close ratify karma, never by the vote's
229
233
  // anti-self (the carve-out) — the counter proves the order.
230
234
  getBoardItemAuthor: async () => { authorAsked += 1; return '42'; },
235
+ // The durable row (task 1005140) — written first, read back after.
236
+ saveConstitutionRow: async (args) => { savedRow = { board: args.board, source: args.source }; return savedRow; },
237
+ getConstitutionRow: async () => savedRow,
231
238
  }, async () => {
232
239
  // The PROPOSER casts on their own amendment — legal (§7) — and it closes
233
240
  // immediately under first_ratifier.
@@ -240,13 +247,16 @@ test('the monarchy act end to end: the sole Archon ratifies their OWN amendment
240
247
  assert.ok(closed.applied_constitution, 'the amendment was applied on close');
241
248
  assert.equal(closed.applied_constitution.pass_rule, 'consent', 'the project now runs consent');
242
249
  assert.equal(closed.applied_constitution.window_minutes, 90);
250
+ assert.equal(closed.constitution_not_applied, undefined, 'read back: it is in force');
243
251
  assert.ok(authorAsked >= 1, 'the ratify karma DID resolve the author, after the close');
244
252
  });
245
253
  assert.equal(karma.length, 1, 'the proposer earned the ratification karma (+3, same act as an idea ratify)');
254
+ assert.equal(savedRow && savedRow.board.pass_rule, 'consent', 'the durable row carries it too, so a restart cannot lose it');
246
255
  const onDisk = JSON.parse(fs.readFileSync(tmp, 'utf8'));
247
256
  assert.equal(onDisk.board.pass_rule, 'consent', 'the TEMP instance pack carries the new constitution');
248
257
  } finally {
249
258
  config.applyBoardAmendment = origApply;
259
+ config.loadGovernmentConfig = origLoad;
250
260
  try { fs.unlinkSync(tmp); } catch (_) { /* not created */ }
251
261
  }
252
262
  });
@@ -200,7 +200,11 @@ test('the founder picks Direct Democracy and ratifies it: the amendment path wri
200
200
  const originalApply = config.applyBoardAmendment;
201
201
  const applied = [];
202
202
  config.applyBoardAmendment = (proposed) => { applied.push(proposed); fs.writeFileSync(target, JSON.stringify({ board: proposed })); return proposed; };
203
+ // The read-back after the apply (task 1005140) reads the same temp file.
204
+ const originalLoad = config.loadGovernmentConfig;
205
+ config.loadGovernmentConfig = (opts = {}) => originalLoad({ ...opts, instancePath: target });
203
206
  let filed = null;
207
+ let savedRow = null;
204
208
  const votes = [];
205
209
  try {
206
210
  await withDb({
@@ -216,6 +220,9 @@ test('the founder picks Direct Democracy and ratifies it: the amendment path wri
216
220
  closeBoardItem: async ({ outcome }) => ({ id: '21', subject_type: 'constitutional_amendment', subject_id: '5', opened_by: '42', closed_at: 'T', outcome }),
217
221
  insertAuthorRatifyKarma: async () => true,
218
222
  getAmendment: async () => ({ id: '5', proposed: filed.proposed }),
223
+ // The durable row (task 1005140) — written first, read back after.
224
+ saveConstitutionRow: async (args) => { savedRow = { board: args.board }; return savedRow; },
225
+ getConstitutionRow: async () => savedRow,
219
226
  }, async () => {
220
227
  const out = await board.proposeCharter({ form: 'direct-democracy', proposedBy: '42' });
221
228
  assert.equal(out.ok, true);
@@ -227,11 +234,14 @@ test('the founder picks Direct Democracy and ratifies it: the amendment path wri
227
234
  await board.castVote({ itemId: '21', voterId: '42', direction: 'yes' });
228
235
  const closed = await board.maybeCloseAfterVote('21');
229
236
  assert.equal(closed.outcome, 'passed');
237
+ assert.equal(closed.constitution_not_applied, undefined, 'read back: it is in force');
230
238
  });
231
239
  assert.deepEqual(applied, [filed.proposed], 'the ratified text is exactly what was filed');
232
240
  assert.equal(JSON.parse(fs.readFileSync(target, 'utf8')).board.membership, 'rank:xenos+');
241
+ assert.equal(savedRow && savedRow.board.membership, 'rank:xenos+', 'the durable row carries it too');
233
242
  } finally {
234
243
  config.applyBoardAmendment = originalApply;
244
+ config.loadGovernmentConfig = originalLoad;
235
245
  fs.rmSync(dir, { recursive: true, force: true });
236
246
  }
237
247
  });
@@ -0,0 +1,304 @@
1
+ // tests/government_constitution_durable.mjs — a ratified constitution survives a
2
+ // restart (task 1005140).
3
+ //
4
+ // Found live on toast, 2026-11-07: the board ratified a Timocratic Republic, the
5
+ // amendment was written to <instance root>/config/government.json, and the next
6
+ // container start (deploy, restart, or an idle wake on a free plan) erased the
7
+ // file. The project went back to the day-one monarchy without an error.
8
+ //
9
+ // DB-free: the constitution row lives in an in-memory stand-in for db.js's two
10
+ // functions; the instance pack is a REAL temp file, and "a restart" is deleting
11
+ // it. The invariants:
12
+ //
13
+ // 1. A restart does not lose a ratified constitution — the row restores it.
14
+ // 2. A file with no row is ADOPTED into the row (nobody loses one on upgrade).
15
+ // 3. The row wins over a stale file.
16
+ // 4. The row is written BEFORE the file, and a write that did not take is
17
+ // REPORTED at the close (constitution_not_applied), never absorbed.
18
+ // 5. The operator's re-apply only ever re-applies a PASSED constitutional
19
+ // amendment — it cannot invent a block.
20
+ // 6. The restore runs at boot, immediately.
21
+ import assert from 'node:assert/strict';
22
+ import { test } from 'node:test';
23
+ import { createRequire } from 'node:module';
24
+ import fs from 'node:fs';
25
+ import os from 'node:os';
26
+ import path from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
+ const GOV = path.join(ROOT, 'modules', 'government');
32
+ const db = require(path.join(GOV, 'db.js'));
33
+ const config = require(path.join(GOV, 'config.js'));
34
+ const constitution = require(path.join(GOV, 'constitution.js'));
35
+ const board = require(path.join(GOV, 'board.js'));
36
+
37
+ const MONARCHY = Object.freeze({ membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true });
38
+ const TIMOCRACY = Object.freeze({ membership: 'rank:metic+', pass_rule: 'consent', window_minutes: null, close_early_on_full_turnout: true });
39
+ const quiet = { log: () => {}, error: () => {} };
40
+
41
+ let seq = 0;
42
+ const tmpPack = () => path.join(os.tmpdir(), `gov-durable-${process.pid}-${Date.now()}-${seq++}`, 'config', 'government.json');
43
+
44
+ // One harness: an in-memory row, and the config's instance path redirected to
45
+ // a temp file for both the write and the read — what a real process does with
46
+ // its own INSTANCE_PATH.
47
+ async function withInstance(fn, { row = null, saveFails = false, getFails = null, history } = {}) {
48
+ const pack = tmpPack();
49
+ const store = { row, saves: [] };
50
+ const orig = {
51
+ get: db.getConstitutionRow, save: db.saveConstitutionRow,
52
+ apply: config.applyBoardAmendment, load: config.loadGovernmentConfig,
53
+ list: db.listAmendmentHistory,
54
+ };
55
+ if (history !== undefined) db.listAmendmentHistory = async () => history;
56
+ db.getConstitutionRow = async () => { if (getFails) throw getFails; return store.row; };
57
+ db.saveConstitutionRow = async (args) => {
58
+ if (saveFails) throw new Error('connection refused');
59
+ store.saves.push(args);
60
+ store.row = { board: args.board, source: args.source, amendment_item_id: args.itemId };
61
+ return store.row;
62
+ };
63
+ config.applyBoardAmendment = (proposed, opts = {}) => orig.apply(proposed, { instancePath: opts.instancePath || pack });
64
+ config.loadGovernmentConfig = (opts = {}) => orig.load({ ...opts, instancePath: opts.instancePath || pack });
65
+ try { return await fn({ pack, store, live: () => orig.load({ instancePath: pack }).board }); }
66
+ finally {
67
+ db.getConstitutionRow = orig.get; db.saveConstitutionRow = orig.save; db.listAmendmentHistory = orig.list;
68
+ config.applyBoardAmendment = orig.apply; config.loadGovernmentConfig = orig.load;
69
+ fs.rmSync(path.dirname(path.dirname(pack)), { recursive: true, force: true });
70
+ }
71
+ }
72
+
73
+ const restart = (pack) => fs.rmSync(pack, { force: true });
74
+
75
+ // ── 1. the bug itself ────────────────────────────────────────────────────────
76
+
77
+ test('a ratified constitution survives a container restart (the toast bug)', async () => {
78
+ await withInstance(async ({ pack, store, live }) => {
79
+ const out = await constitution.recordRatified(TIMOCRACY, { itemId: '7' });
80
+ assert.equal(out.failure, null, `the ratified amendment took: ${JSON.stringify(out.failure)}`);
81
+ assert.deepEqual({ ...live() }, TIMOCRACY, 'in force right after the close');
82
+ assert.deepEqual(store.row.board, TIMOCRACY, 'the durable row holds it');
83
+ assert.equal(store.row.source, 'amendment');
84
+
85
+ restart(pack); // the container's filesystem is wiped
86
+ assert.deepEqual({ ...live() }, MONARCHY, 'fixture: with the file gone the process alone falls back to monarchy');
87
+
88
+ const r = await constitution.reconcile({ instancePath: pack, ...quiet });
89
+ assert.equal(r.action, 'restored');
90
+ assert.deepEqual({ ...live() }, TIMOCRACY, 'after the boot restore the board\'s decision is in force again');
91
+ });
92
+ });
93
+
94
+ // ── 2 + 3. adopt on first read; the row wins ─────────────────────────────────
95
+
96
+ test('a file with no row is adopted into the database and the log says so', async () => {
97
+ await withInstance(async ({ pack, store }) => {
98
+ fs.mkdirSync(path.dirname(pack), { recursive: true });
99
+ fs.writeFileSync(pack, JSON.stringify({ ranks: { metic: { label: 'Trusted' } }, board: TIMOCRACY }));
100
+ const lines = [];
101
+ const r = await constitution.reconcile({ instancePath: pack, log: (m) => lines.push(m), error: () => {} });
102
+ assert.equal(r.action, 'adopted');
103
+ assert.deepEqual(store.row.board, TIMOCRACY);
104
+ assert.equal(store.row.source, 'adopted_file');
105
+ assert.ok(lines.some((l) => /adopted/.test(l)), 'the adoption is logged');
106
+ const again = await constitution.reconcile({ instancePath: pack, ...quiet });
107
+ assert.equal(again.action, 'in_step', 'a second pass changes nothing');
108
+ });
109
+ });
110
+
111
+ test('no row and no file is the vanilla instance: nothing is written', async () => {
112
+ await withInstance(async ({ pack, store }) => {
113
+ const r = await constitution.reconcile({ instancePath: pack, ...quiet });
114
+ assert.equal(r.action, 'none');
115
+ assert.equal(store.saves.length, 0);
116
+ assert.equal(fs.existsSync(pack), false, 'no file is invented for a vanilla instance');
117
+ }, { history: [] });
118
+ });
119
+
120
+ test('the toast case: no row, no file, a passed amendment — the first boot puts it back in force (part C)', async () => {
121
+ const history = [
122
+ { item_id: 30, amendment_id: 9, outcome: 'returned', proposed: MONARCHY, kind: 'constitution' },
123
+ { item_id: 21, amendment_id: 1, outcome: 'passed', proposed: TIMOCRACY, kind: 'constitution' },
124
+ ];
125
+ await withInstance(async ({ pack, store, live }) => {
126
+ assert.deepEqual({ ...live() }, MONARCHY, 'fixture: the erased instance answers the day-one monarchy');
127
+ const lines = [];
128
+ const r = await constitution.reconcile({ instancePath: pack, log: (m) => lines.push(m), error: () => {} });
129
+ assert.equal(r.action, 'recovered', JSON.stringify(r));
130
+ assert.equal(r.item_id, '21', 'the PASSED amendment, never the newer returned one');
131
+ assert.deepEqual({ ...live() }, TIMOCRACY, 'the board decision is in force again');
132
+ assert.deepEqual(store.row.board, TIMOCRACY, 'and durable, so the next restart keeps it');
133
+ assert.ok(lines.some((l) => /put back passed amendment 1/.test(l)), 'the log says so');
134
+ assert.equal((await constitution.reconcile({ instancePath: pack, ...quiet })).action, 'in_step');
135
+ }, { history });
136
+ });
137
+
138
+ test('the row wins over a stale file, and the file keeps its other keys', async () => {
139
+ await withInstance(async ({ pack, live }) => {
140
+ fs.mkdirSync(path.dirname(pack), { recursive: true });
141
+ fs.writeFileSync(pack, JSON.stringify({ ranks: { metic: { label: 'Trusted' } }, board: MONARCHY }));
142
+ const r = await constitution.reconcile({ instancePath: pack, ...quiet });
143
+ assert.equal(r.action, 'restored');
144
+ assert.deepEqual({ ...live() }, TIMOCRACY);
145
+ assert.equal(JSON.parse(fs.readFileSync(pack, 'utf8')).ranks.metic.label, 'Trusted', 'rank templates survive the restore');
146
+ }, { row: { board: TIMOCRACY, source: 'amendment' } });
147
+ });
148
+
149
+ test('an unmigrated database (no table) and an unreadable one never throw and never touch the file', async () => {
150
+ const missing = Object.assign(new Error('relation "government_constitution" does not exist'), { code: '42P01' });
151
+ await withInstance(async ({ pack }) => {
152
+ assert.equal((await constitution.reconcile({ instancePath: pack, ...quiet })).action, 'no_table');
153
+ assert.equal(fs.existsSync(pack), false);
154
+ }, { getFails: missing });
155
+ await withInstance(async ({ pack }) => {
156
+ assert.equal((await constitution.reconcile({ instancePath: pack, ...quiet })).action, 'read_failed');
157
+ }, { getFails: new Error('timeout') });
158
+ });
159
+
160
+ // ── 4. row first, and a write that did not take is reported ──────────────────
161
+
162
+ test('the row is written before the file: a failed save leaves the old rule and SAYS so', async () => {
163
+ await withInstance(async ({ pack, live }) => {
164
+ const out = await constitution.recordRatified(TIMOCRACY, { itemId: '7' });
165
+ assert.equal(out.applied, null);
166
+ assert.ok(out.failure, 'reported, not absorbed');
167
+ assert.equal(out.failure.problems[0].where, 'database');
168
+ assert.equal(fs.existsSync(pack), false, 'nothing was put in force that would vanish at the next restart');
169
+ assert.equal(out.failure.problems[0].reason, 'save_failed');
170
+ assert.doesNotMatch(JSON.stringify(out.failure), /connection refused/, 'a voter sees a reason word, never the driver message');
171
+ assert.deepEqual({ ...live() }, MONARCHY);
172
+ }, { saveFails: true });
173
+ });
174
+
175
+ test('a file write that silently did not take is caught by the read-back', async () => {
176
+ await withInstance(async ({ store }) => {
177
+ config.applyBoardAmendment = (proposed) => ({ ...proposed }); // claims success, writes nothing
178
+ const out = await constitution.recordRatified(TIMOCRACY, { itemId: '7' });
179
+ assert.ok(out.failure, 'the verify caught it');
180
+ assert.deepEqual(out.failure.problems.map((p) => p.where), ['in_force']);
181
+ assert.deepEqual(out.failure.problems[0].fields, ['membership', 'pass_rule']);
182
+ assert.deepEqual(store.row.board, TIMOCRACY, 'the durable row still holds the decision, so a restore can finish the job');
183
+ });
184
+ });
185
+
186
+ test('closeItem hands a constitution that did not take back to the caller (and the vote route forwards it)', async () => {
187
+ const item = { id: '21', subject_type: 'constitutional_amendment', subject_id: '5', opened_by: '42', closed_at: null, expired: false, constitution: MONARCHY };
188
+ const stubs = {
189
+ getVotesForItem: async () => [{ id: '1', voter_id: '42', direction: 'yes' }],
190
+ closeBoardItem: async ({ outcome }) => ({ ...item, closed_at: 'T', outcome }),
191
+ getAmendment: async () => ({ id: '5', proposed: TIMOCRACY, kind: 'constitution' }),
192
+ insertAuthorRatifyKarma: async () => true,
193
+ getBoardItemAuthor: async () => '42',
194
+ };
195
+ const orig = {};
196
+ for (const [k, v] of Object.entries(stubs)) { orig[k] = db[k]; db[k] = v; }
197
+ try {
198
+ await withInstance(async () => {
199
+ const closed = await board.closeItem(item, { cause: 'vote' });
200
+ assert.equal(closed.outcome, 'passed');
201
+ assert.ok(closed.constitution_not_applied, 'the close itself reports it');
202
+ assert.equal(closed.constitution_not_applied.problems[0].where, 'database');
203
+ }, { saveFails: true });
204
+ await withInstance(async ({ live }) => {
205
+ const closed = await board.closeItem(item, { cause: 'vote' });
206
+ assert.equal(closed.constitution_not_applied, undefined, 'a clean apply reports nothing');
207
+ assert.deepEqual({ ...closed.applied_constitution }, TIMOCRACY);
208
+ assert.deepEqual({ ...live() }, TIMOCRACY);
209
+ });
210
+ } finally { for (const [k, v] of Object.entries(orig)) db[k] = v; }
211
+
212
+ const routeSrc = fs.readFileSync(path.join(GOV, 'routes', 'government.js'), 'utf8');
213
+ assert.match(routeSrc, /closure && closure\.constitution_not_applied \? \{ constitution_not_applied:/,
214
+ 'the vote route forwards it to the page the voter is standing on');
215
+ for (const page of ['board-room.js', 'street-card.js']) {
216
+ const src = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', page), 'utf8');
217
+ assert.match(src, /constitution_not_applied/, `${page} says so instead of "in force"`);
218
+ }
219
+ });
220
+
221
+ // ── 5. the operator's re-apply ───────────────────────────────────────────────
222
+
223
+ test('re-apply puts the newest PASSED constitutional amendment back, skipping returned ones and laws', async () => {
224
+ const history = [
225
+ { item_id: 30, amendment_id: 12, outcome: 'returned', proposed: MONARCHY, kind: 'constitution' },
226
+ { item_id: 29, amendment_id: 11, outcome: 'passed', proposed: { enact: [] }, kind: 'laws' },
227
+ { item_id: 21, amendment_id: 1, outcome: 'passed', proposed: TIMOCRACY, kind: 'constitution' },
228
+ ];
229
+ const origList = db.listAmendmentHistory;
230
+ db.listAmendmentHistory = async () => history;
231
+ try {
232
+ await withInstance(async ({ store, live }) => {
233
+ const out = await constitution.reapplyNewestPassed({ updatedBy: '3' });
234
+ assert.equal(out.ok, true, JSON.stringify(out));
235
+ assert.equal(out.item_id, '21');
236
+ assert.deepEqual({ ...live() }, TIMOCRACY);
237
+ assert.equal(store.row.source, 'reapply');
238
+ assert.equal(store.saves[0].updatedBy, '3');
239
+ });
240
+ db.listAmendmentHistory = async () => history.slice(0, 2);
241
+ await withInstance(async ({ store }) => {
242
+ const out = await constitution.reapplyNewestPassed({});
243
+ assert.deepEqual(out, { ok: false, reason: 'no_passed_amendment' });
244
+ assert.equal(store.saves.length, 0, 'it never invents a constitution');
245
+ });
246
+ } finally { db.listAmendmentHistory = origList; }
247
+ });
248
+
249
+ test('the re-apply route is pinned to the owner atom and takes no block from the request', () => {
250
+ const { GOVERNMENT_PINS } = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
251
+ const pins = GOVERNMENT_PINS || {};
252
+ const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'), 'utf8');
253
+ assert.match(src, /'POST \/government\/constitution\/reapply': 'perm:government\.manage'/);
254
+ if (Object.keys(pins).length) assert.equal(pins['POST /government/constitution/reapply'], 'perm:government.manage');
255
+ const routeSrc = fs.readFileSync(path.join(GOV, 'routes', 'government.js'), 'utf8');
256
+ assert.match(routeSrc, /router\.post\('\/government\/constitution\/reapply', api\.requireBuilder, api\.requirePermission\('government\.manage'\), constitution\.reapplyRoute\(log\)\)/,
257
+ 'the route exists, gated on the owner atom');
258
+ assert.doesNotMatch(constitution.reapplyRoute.toString(), /req\.body/, 'the block comes from a passed amendment, never the request');
259
+ });
260
+
261
+ test('the re-apply handler answers 404 with nothing passed and 200 with the board it put back', async () => {
262
+ const call = async () => {
263
+ const res = { code: 200, body: null,
264
+ status(c) { this.code = c; return this; }, json(b) { this.body = b; return this; },
265
+ fail(code, status, extra) { this.code = typeof status === 'number' ? status : status.status; this.body = { code, ...extra }; return this; } };
266
+ await constitution.reapplyRoute({ error: () => {} })({ builder: { id: '3' }, body: { board: MONARCHY } }, res);
267
+ return res;
268
+ };
269
+ const origList = db.listAmendmentHistory;
270
+ try {
271
+ db.listAmendmentHistory = async () => [];
272
+ await withInstance(async () => {
273
+ const res = await call();
274
+ assert.equal(res.code, 404);
275
+ assert.equal(res.body.code, 'no_passed_amendment');
276
+ });
277
+ db.listAmendmentHistory = async () => [{ item_id: 21, amendment_id: 1, outcome: 'passed', proposed: TIMOCRACY, kind: 'constitution' }];
278
+ await withInstance(async ({ live }) => {
279
+ const res = await call();
280
+ assert.equal(res.code, 200);
281
+ assert.deepEqual({ ...res.body.board }, TIMOCRACY, 'the passed amendment, not the request body');
282
+ assert.deepEqual({ ...live() }, TIMOCRACY);
283
+ });
284
+ } finally { db.listAmendmentHistory = origList; }
285
+ });
286
+
287
+ // ── 6. at boot ───────────────────────────────────────────────────────────────
288
+
289
+ test('the restore runs the moment the poller starts, not after a delay', () => {
290
+ const poller = require(path.join(GOV, 'pollers', 'constitution-restore.js'));
291
+ let calls = 0;
292
+ poller.start({ reconcileFn: async () => { calls += 1; return { action: 'in_step' }; } });
293
+ try {
294
+ return new Promise((resolve) => setImmediate(() => { assert.equal(calls, 1); resolve(); }));
295
+ } finally { poller.stop(); }
296
+ });
297
+
298
+ test('the module declares the poller and the migration creates the one-row table', () => {
299
+ const manifest = JSON.parse(fs.readFileSync(path.join(GOV, 'module.json'), 'utf8'));
300
+ assert.ok(manifest.contributes.pollers.includes('constitution-restore'));
301
+ const sql = fs.readFileSync(path.join(GOV, 'migrations', 'government_037_constitution.sql'), 'utf8');
302
+ assert.match(sql, /CREATE TABLE IF NOT EXISTS government_constitution/);
303
+ assert.match(sql, /CHECK \(id = 1\)/, 'one row, by construction');
304
+ });
@@ -53,6 +53,7 @@ const NON_GRANT_MIGRATIONS = new Set([
53
53
  'government_031_treasury.sql', // the treasury: a pot per goal and the loans a dry pot asks the reserve for (task 1004712)
54
54
  'government_032_ask_flags.sql', // the reword list: matters a reader flagged as unclear (owner 2026-10-23)
55
55
  'government_036_access_holders.sql', // who holds the keys outside Bongos + the access:<key> ask seat (task 1005000)
56
+ 'government_037_constitution.sql', // the constitution in force as a durable row (task 1005140)
56
57
  ]);
57
58
 
58
59
  // Permission keys RENAMED after the grant migrations that first granted them.
@@ -406,3 +406,22 @@ test('the reduced-motion kill switch is present and closes the token layer', ()
406
406
  assert.equal(src.split('@media (prefers-reduced-motion: reduce)').length - 1, 1,
407
407
  'exactly one kill switch — a second copy is a place for the rule to rot');
408
408
  });
409
+
410
+ test('ink set on an --accent fill is readable on it in both modes', () => {
411
+ // Task 1005103: .genesis__step.is-now .genesis__step-n painted --bg on the
412
+ // accent (light ~2.2:1). Any rule that fills with var(--accent) and names its
413
+ // own ink token must measure that ink against the fill, in both modes.
414
+ const fails = [];
415
+ const plain = src.replace(/\/\*[\s\S]*?\*\//g, '');
416
+ for (const m of plain.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
417
+ const body = m[2];
418
+ if (!/background(?:-color)?\s*:\s*var\(--accent\)\s*[;}]?/.test(body)) continue;
419
+ const ink = body.match(/(?:^|[;\s])color\s*:\s*var\((--[\w-]+)\)/);
420
+ if (!ink) continue;
421
+ for (const mode of ['light', 'dark']) {
422
+ const r = ratio(ink[1], '--accent', mode);
423
+ if (r < 4.5) fails.push(`${mode} ${m[1].trim()}: ${ink[1]} on --accent = ${r.toFixed(2)}:1`);
424
+ }
425
+ }
426
+ assert.deepEqual(fails, [], 'ink on an accent fill needs 4.5:1');
427
+ });