@bongos/core 1.19.616 → 1.19.618

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.
@@ -0,0 +1,159 @@
1
+ // tests/government_config_roots.mjs — the constitution's two files come from two
2
+ // different roots (task 1003739, ADR 0268).
3
+ //
4
+ // THE BUG THIS PINS, because it was silent for two weeks and cost a ratified
5
+ // constitutional amendment. `modules/government/config.js` resolved BOTH the
6
+ // neutral starter and the instance pack from `path.resolve(__dirname, '..',
7
+ // '..')`, under a comment saying the two roots coincided "today". True in a
8
+ // single checkout. False on cloudbongos.com, which runs a PINNED core: there
9
+ // __dirname is inside node_modules/@cloudbongos/core, so the INSTANCE pack
10
+ // resolved to a path inside the core package — one no host authors, and one
11
+ // `npm ci` erases. The board ratified consent/metic+/1440 on 2026-08-25,
12
+ // `applyBoardAmendment` wrote it into node_modules, the next core upgrade
13
+ // deleted it, and GET /government/constitution went on answering the day-one
14
+ // monarchy while its own history showed the amendment had passed.
15
+ //
16
+ // NOTHING THREW. Both paths exist, both are writable, and an absent instance
17
+ // pack is a legal state (a vanilla instance has none), so the read fell through
18
+ // to the neutral file exactly as designed. That is why this is a STRUCTURAL
19
+ // test rather than a behavioural one: there is no error to assert on, only the
20
+ // question of which root each path was built from.
21
+ //
22
+ // The single-checkout answers are asserted too, and deliberately: they are what
23
+ // makes this fix byte-identical for a non-split instance, and a future "tidy-up"
24
+ // that re-collapses the two roots must fail here rather than in production two
25
+ // weeks later.
26
+ import assert from 'node:assert/strict';
27
+ import { test } from 'node:test';
28
+ import { createRequire } from 'node:module';
29
+ import fs from 'node:fs';
30
+ import os from 'node:os';
31
+ import path from 'node:path';
32
+ import { fileURLToPath } from 'node:url';
33
+
34
+ const require = createRequire(import.meta.url);
35
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
36
+ const GOV = path.join(ROOT, 'modules', 'government');
37
+ const CONFIG_PATH = path.join(GOV, 'config.js');
38
+ const config = require(CONFIG_PATH);
39
+ const api = require(path.join(ROOT, 'src', 'module-api.js'));
40
+
41
+ // config.js resolves its two paths AT LOAD (deliberately — see its header), so
42
+ // exercising a different instance root means re-loading it with the env set.
43
+ // The cache delete is scoped to this one module; instance-config resolves the
44
+ // env per call, so nothing else needs evicting.
45
+ function loadWithInstanceRoot(dir) {
46
+ const KEY = 'CLOUDBONGOS_INSTANCE_ROOT';
47
+ const before = process.env[KEY];
48
+ process.env[KEY] = dir;
49
+ delete require.cache[require.resolve(CONFIG_PATH)];
50
+ try { return require(CONFIG_PATH); }
51
+ finally {
52
+ if (before === undefined) delete process.env[KEY]; else process.env[KEY] = before;
53
+ delete require.cache[require.resolve(CONFIG_PATH)];
54
+ require(CONFIG_PATH); // restore the module-cache entry every other test shares
55
+ }
56
+ }
57
+
58
+ // ── the doorway ─────────────────────────────────────────────────────────────
59
+
60
+ test('the module doorway exposes BOTH roots — a module cannot tell them apart without it', () => {
61
+ // ADR 0083: a module never requires a core internal, so until this task the
62
+ // only way to reach a root from inside a module was to re-derive it from
63
+ // __dirname — which is precisely the derivation that was wrong.
64
+ assert.equal(typeof api.resolveCoreRoot, 'function');
65
+ assert.equal(typeof api.resolveInstanceRoot, 'function');
66
+ assert.equal(api.resolveCoreRoot(), ROOT, 'the core root is this checkout');
67
+ });
68
+
69
+ // ── the split ───────────────────────────────────────────────────────────────
70
+
71
+ test('the NEUTRAL starter follows the CORE root and the INSTANCE pack follows the INSTANCE root', () => {
72
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gov-roots-'));
73
+ try {
74
+ const split = loadWithInstanceRoot(tmp);
75
+ assert.equal(split.NEUTRAL_PATH, path.join(ROOT, 'config', 'government.neutral.json'),
76
+ 'the neutral starter is CORE content and must not follow the host');
77
+ assert.equal(split.INSTANCE_PATH, path.join(tmp, 'config', 'government.json'),
78
+ 'the instance pack is HOST content — the bug was this landing inside the core package');
79
+ assert.notEqual(path.dirname(split.INSTANCE_PATH), path.dirname(split.NEUTRAL_PATH),
80
+ 'a split instance must resolve the two to DIFFERENT directories');
81
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
82
+ });
83
+
84
+ test('in a single checkout the two roots coincide, exactly as before the fix', () => {
85
+ // The byte-identical claim for every non-split instance, asserted rather than
86
+ // asserted-in-prose. If this ever fails, the fix changed behaviour for the
87
+ // ordinary case and that is a regression, not a refinement.
88
+ assert.equal(config.NEUTRAL_PATH, path.join(ROOT, 'config', 'government.neutral.json'));
89
+ assert.equal(config.INSTANCE_PATH, path.join(ROOT, 'config', 'government.json'));
90
+ });
91
+
92
+ test('a host instance pack at the INSTANCE root is actually READ', () => {
93
+ // The other half of the same bug, and the half a host would have hit first:
94
+ // before the fix, an instance that hand-authored config/government.json in its
95
+ // own repo — the documented way to set a constitution — was silently ignored,
96
+ // because the reader was looking inside node_modules.
97
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gov-roots-'));
98
+ try {
99
+ fs.mkdirSync(path.join(tmp, 'config'));
100
+ fs.writeFileSync(path.join(tmp, 'config', 'government.json'), JSON.stringify({
101
+ board: { membership: 'rank:metic+', pass_rule: 'unanimous', window_minutes: 1440, close_early_on_full_turnout: true },
102
+ }));
103
+ const split = loadWithInstanceRoot(tmp);
104
+ const { board } = split.loadGovernmentConfig({ instancePath: split.INSTANCE_PATH });
105
+ assert.equal(board.pass_rule, 'unanimous');
106
+ assert.equal(board.membership, 'rank:metic+');
107
+ assert.equal(board.window_minutes, 1440);
108
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
109
+ });
110
+
111
+ // ── the write ───────────────────────────────────────────────────────────────
112
+
113
+ test('a ratified amendment creates the config directory rather than dying on ENOENT', () => {
114
+ // Now that the write targets the INSTANCE root, the directory genuinely may
115
+ // not exist: an instance that has never been branded and never amended has no
116
+ // config/ of its own. Before the fix the write always landed beside the core's
117
+ // own config/, which is why this never came up — and a ratified amendment
118
+ // lost to a missing directory is the same failure as the one being fixed.
119
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gov-roots-'));
120
+ try {
121
+ const target = path.join(tmp, 'nested', 'config', 'government.json');
122
+ assert.equal(fs.existsSync(path.dirname(target)), false, 'sanity: the directory is absent');
123
+ const applied = config.applyBoardAmendment(
124
+ { membership: 'rank:archon', pass_rule: 'unanimous', window_minutes: 1440, close_early_on_full_turnout: true },
125
+ { instancePath: target },
126
+ );
127
+ assert.equal(applied.pass_rule, 'unanimous');
128
+ assert.deepEqual(JSON.parse(fs.readFileSync(target, 'utf8')).board, applied,
129
+ 'what was written back is what was returned');
130
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
131
+ });
132
+
133
+ test('applying an amendment preserves the rest of the instance pack', () => {
134
+ // Unchanged behaviour, re-pinned here because the write now happens in a
135
+ // directory the core does not own: an instance pack holds rank templates too,
136
+ // and a constitution change must not take them with it.
137
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gov-roots-'));
138
+ try {
139
+ const target = path.join(tmp, 'config', 'government.json');
140
+ fs.mkdirSync(path.dirname(target));
141
+ fs.writeFileSync(target, JSON.stringify({ ranks: { metic: { label: 'Trusted' } }, board: { pass_rule: 'consent' } }));
142
+ config.applyBoardAmendment(
143
+ { membership: 'rank:archon', pass_rule: 'majority', window_minutes: 1440, close_early_on_full_turnout: true },
144
+ { instancePath: target },
145
+ );
146
+ const after = JSON.parse(fs.readFileSync(target, 'utf8'));
147
+ assert.equal(after.board.pass_rule, 'majority');
148
+ assert.deepEqual(after.ranks, { metic: { label: 'Trusted' } }, 'the rank templates survive');
149
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
150
+ });
151
+
152
+ test('this checkout still ships NO instance pack — the vanilla-state guard', () => {
153
+ // The pre-existing guard (a test once wrote a real config/government.json into
154
+ // the checkout), kept because the write path moved. It is now doubly
155
+ // load-bearing: with the instance root defaulting to process.cwd(), a stray
156
+ // file here would be read by every test that follows.
157
+ assert.equal(fs.existsSync(path.join(ROOT, 'config', 'government.json')), false,
158
+ 'config/government.json must not exist in the core checkout — a test wrote one once');
159
+ });
@@ -0,0 +1,123 @@
1
+ // tests/government_constitution_divergence.mjs — the detector for a ratified
2
+ // amendment that did not take (task 1003739, ADR 0268).
3
+ //
4
+ // The path bug that caused the live failure is fixed at its source, in
5
+ // config.js, and pinned by tests/government_config_roots.mjs. THIS is the
6
+ // detector for the CLASS: any future reason a ratified amendment fails to reach
7
+ // the live constitution — a failed write, a rolled-back deploy, a hand edit that
8
+ // reverts one — now surfaces on the constitution view instead of being invisible.
9
+ //
10
+ // WHY A DETECTOR AND NOT JUST THE FIX. On 2026-08-25 this board ratified an
11
+ // amendment and the constitution went on answering the day-one monarchy. The API
12
+ // had BOTH facts in ONE payload the whole time — a passed amendment in `history`,
13
+ // a contradicting `board` beside it — and simply never compared them. Two weeks
14
+ // of sittings were decided under a rule the board had voted to replace, and the
15
+ // only reason anyone noticed was someone reading the JSON by hand.
16
+ //
17
+ // IT REPORTS, IT NEVER HEALS, and that is a decision rather than an omission.
18
+ // The instance pack is also the file a human editing the constitution touches
19
+ // (config.js §applyBoardAmendment), so a silent re-apply would revert a
20
+ // legitimate hand edit with no way to tell the two apart — trading a visible
21
+ // wrong constitution for an unstoppable one.
22
+ import assert from 'node:assert/strict';
23
+ import { test } from 'node:test';
24
+ import { createRequire } from 'node:module';
25
+ import path from 'node:path';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ const require = createRequire(import.meta.url);
29
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
30
+ const GOV = path.join(ROOT, 'modules', 'government');
31
+ const board = require(path.join(GOV, 'board.js'));
32
+ const db = require(path.join(GOV, 'db.js'));
33
+
34
+ const { constitutionDivergence, constitutionView } = board;
35
+
36
+ const MONARCHY = { membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true };
37
+ // The REAL proposal from cloudbongos.com's amendment 1, item 11 — the sitting
38
+ // that passed and never took. Kept verbatim so this test is the live failure
39
+ // rather than a sketch of it.
40
+ const PASSED_2026_08_25 = {
41
+ item_id: '11', amendment_id: '1', outcome: 'passed', closed_at: '2026-08-25T16:53:45.655Z',
42
+ proposed: { pass_rule: 'consent', membership: 'rank:metic+', window_minutes: 1440, close_early_on_full_turnout: true },
43
+ };
44
+
45
+ test('THE LIVE FAILURE: a passed amendment beside a contradicting constitution is named', () => {
46
+ const d = constitutionDivergence(MONARCHY, [PASSED_2026_08_25]);
47
+ assert.ok(d, 'the divergence must be reported, not null');
48
+ assert.equal(d.item_id, '11');
49
+ assert.equal(d.amendment_id, '1');
50
+ const byField = Object.fromEntries(d.fields.map((f) => [f.field, f]));
51
+ assert.deepEqual(Object.keys(byField).sort(), ['membership', 'pass_rule', 'window_minutes']);
52
+ assert.deepEqual(byField.pass_rule, { field: 'pass_rule', ratified: 'consent', in_force: 'first_ratifier' });
53
+ assert.deepEqual(byField.membership, { field: 'membership', ratified: 'rank:metic+', in_force: 'rank:archon' });
54
+ assert.deepEqual(byField.window_minutes, { field: 'window_minutes', ratified: 1440, in_force: null });
55
+ assert.equal(d.fields.some((f) => f.field === 'close_early_on_full_turnout'), false,
56
+ 'a field that AGREES is not reported — the report is the delta, not the whole block');
57
+ });
58
+
59
+ test('a constitution that IS the last ratified one reports nothing', () => {
60
+ // The healthy answer, and the one the hall renders nothing for. A detector
61
+ // that fires on a working system is a detector people learn to ignore.
62
+ const inForce = { membership: 'rank:metic+', pass_rule: 'consent', window_minutes: 1440, close_early_on_full_turnout: true };
63
+ assert.equal(constitutionDivergence(inForce, [PASSED_2026_08_25]), null);
64
+ });
65
+
66
+ test('a RETURNED amendment is not a decision and never diverges', () => {
67
+ // The whole meaning of 'returned'. Comparing against it would report a
68
+ // divergence on every rejected proposal — i.e. on the system working.
69
+ const returned = { ...PASSED_2026_08_25, outcome: 'returned' };
70
+ assert.equal(constitutionDivergence(MONARCHY, [returned]), null);
71
+ // …and the newest PASSED one still stands, with later returns above it.
72
+ const d = constitutionDivergence(MONARCHY, [returned, PASSED_2026_08_25]);
73
+ assert.ok(d);
74
+ assert.equal(d.item_id, '11');
75
+ });
76
+
77
+ test('a board that has never amended anything reports nothing', () => {
78
+ assert.equal(constitutionDivergence(MONARCHY, []), null);
79
+ assert.equal(constitutionDivergence(MONARCHY, null), null);
80
+ assert.equal(constitutionDivergence(MONARCHY, [{ outcome: 'passed' }]), null, 'a row with no proposal is not a claim');
81
+ });
82
+
83
+ test('the comparison is against the SANITIZED proposal, not the raw one', () => {
84
+ // applyBoardAmendment sanitizes on write, so comparing the RAW proposal would
85
+ // report a phantom divergence on any field the sanitizer legitimately
86
+ // normalises — and a warning that can never be cleared is worse than none.
87
+ // 999999 is past MAX_WINDOW_MINUTES, so it sanitizes to the default (null),
88
+ // which is what is in force: nothing to report.
89
+ const junkWindow = { ...PASSED_2026_08_25, proposed: { ...PASSED_2026_08_25.proposed, window_minutes: 999999 } };
90
+ const inForce = { membership: 'rank:metic+', pass_rule: 'consent', window_minutes: null, close_early_on_full_turnout: true };
91
+ assert.equal(constitutionDivergence(inForce, [junkWindow]), null);
92
+ });
93
+
94
+ test('the newest passed amendment wins — one it superseded is not compared', () => {
95
+ const older = {
96
+ item_id: '4', amendment_id: '2', outcome: 'passed', closed_at: '2026-08-01T00:00:00Z',
97
+ proposed: { pass_rule: 'majority', membership: 'rank:archon', window_minutes: 90, close_early_on_full_turnout: true },
98
+ };
99
+ // listAmendmentHistory returns newest-first, so the 08-25 row is the standing
100
+ // decision and the older one is history, not a claim on the present.
101
+ const d = constitutionDivergence(MONARCHY, [PASSED_2026_08_25, older]);
102
+ assert.equal(d.item_id, '11');
103
+ assert.equal(d.ratified.pass_rule, 'consent', 'not the older majority proposal');
104
+ });
105
+
106
+ test('the constitution view carries it, and this checkout is healthy', async () => {
107
+ // Wired end to end: the view computes the divergence from the history it
108
+ // already read, so there is no second query that could disagree with the rows
109
+ // shown beside it.
110
+ const originals = { listAmendmentHistory: db.listAmendmentHistory, countActiveBuilders: db.countActiveBuilders };
111
+ try {
112
+ Object.assign(db, { listAmendmentHistory: async () => [], countActiveBuilders: async () => 4 });
113
+ const clean = await constitutionView();
114
+ assert.equal(clean.divergence, null, 'a board with no amendments has nothing to warn about');
115
+
116
+ Object.assign(db, { listAmendmentHistory: async () => [PASSED_2026_08_25] });
117
+ const diverged = await constitutionView();
118
+ assert.ok(diverged.divergence, 'the live failure, reproduced through the real view');
119
+ assert.equal(diverged.divergence.item_id, '11');
120
+ assert.equal(diverged.board.pass_rule, 'first_ratifier', 'the view still reports what is actually in force');
121
+ assert.equal(diverged.divergence.ratified.pass_rule, 'consent', 'beside what was ratified');
122
+ } finally { Object.assign(db, originals); }
123
+ });
@@ -34,6 +34,7 @@ const PUBLISHED_SURFACE = [
34
34
  'llmCache', // BV1.R73: shared LLM-cost cache (1.6.0; tracked domain leak)
35
35
  'llmPricing', // BV1.R77: shared token→USD pricing (1.7.0; tracked domain leak — economy carve)
36
36
  'branding', 'clientBranding', 'userAgent', 'resolveEnv', 'readConfigFileSync', 'configHome', 'configPath', // userAgent added 1.12.0 (BV1.R86; github-push UA)
37
+ 'resolveCoreRoot', 'resolveInstanceRoot', // added by task 1003739 (ADR 0268): the two ADR 0108 §1 roots. A module reading a HOST-owned file could not tell them apart without these, and re-deriving one from __dirname is right in a single checkout and silently wrong on a standalone instance — which is how a RATIFIED constitutional amendment came to be written into node_modules and erased by the next core upgrade. Core content → resolveCoreRoot(); host content → resolveInstanceRoot(); src/branding.js is the pattern
37
38
  'resolveStaleClaimHours', 'staleClaimMinutes', // added by task 1003476 (ADR 0226; goal 1000072): the PROJECT's stale-claim timer — the sweep that releases a silent claim and the Archon roster that reports one read the same number through here, after eighteen hours of disagreeing (6 vs 24)
38
39
  'resolveRotDays', 'projectSettings', // added by task 1003279 (ADR 0232; goal 1000072): the ROT timer + the live knob store behind it. A DIFFERENT question from the stale-claim timer above — days not hours, DB not env, work nobody picked up rather than a builder who went quiet, and it only ever ASKS a human where that one ACTS. Both ride the doorway so no module hardcodes either.
39
40
  'joinabilityMode', // added by task 1003044 (ADR 0194; the carrier after 1.19.374): the project's membership door, one reader for the sign-in gate AND the onboarding module's public POST