@bongos/core 1.19.182 → 1.19.184
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 +81 -76
- package/docs/api/openapi.json +9 -9
- package/modules/{governance → government}/catalog.js +1 -1
- package/modules/{governance → government}/config.js +6 -6
- package/modules/{governance → government}/db.js +43 -43
- package/modules/government/migrations/government_001_rename_from_governance.sql +213 -0
- package/modules/government/module.json +22 -0
- package/modules/{governance → government}/protected-surfaces.json +2 -1
- package/modules/{governance/rank-roles.js → government/ranks.js} +9 -9
- package/modules/{governance → government}/routes/governance.js +5 -5
- package/modules/{governance/routes/role-sync.js → government/routes/rank-sync.js} +7 -7
- package/modules/lifecycle/db.js +6 -6
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +11 -1
- package/scripts/gds/migration-namespace.js +15 -3
- package/scripts/gds/publish-manifest.js +5 -3
- package/scripts/gds/seed-governance-tasks.js +2 -2
- package/src/bongos/auth.js +9 -9
- package/src/bongos/db.js +6 -6
- package/src/bongos/module-scope-map.js +22 -1
- package/src/bongos/permission-path-check.js +3 -3
- package/src/bongos/route-rank-check.js +40 -21
- package/src/module-api.js +6 -2
- package/tests/auth_page_gate.mjs +2 -2
- package/tests/criterion_authoring_gate.mjs +1 -1
- package/tests/governance_abuse_matrix.mjs +7 -7
- package/tests/governance_catalog.mjs +1 -1
- package/tests/governance_claim_eligibility.mjs +1 -1
- package/tests/governance_config.mjs +7 -7
- package/tests/governance_fold.mjs +4 -4
- package/tests/governance_live_proof.mjs +2 -2
- package/tests/governance_ownership_scope.mjs +1 -1
- package/tests/governance_page_gates.mjs +6 -6
- package/tests/governance_parity.mjs +3 -3
- package/tests/governance_principals.mjs +1 -1
- package/tests/governance_protected_surfaces.mjs +63 -7
- package/tests/governance_require_permission.mjs +6 -6
- package/tests/governance_resolver.mjs +2 -2
- package/tests/governance_role_sync.mjs +14 -14
- package/tests/governance_routes.mjs +6 -6
- package/tests/governance_seed.mjs +11 -3
- package/tests/governance_session_scope.mjs +1 -1
- package/tests/governance_vanilla_boot.mjs +4 -4
- package/tests/hall_page_gate_map.mjs +1 -1
- package/tests/memory_isolation.mjs +2 -2
- package/tests/module_loader.mjs +1 -1
- package/tests/onboarding_observability.mjs +2 -2
- package/tests/rank_tier_single_source.mjs +2 -2
- package/tests/route_permission_check.mjs +2 -2
- package/modules/governance/module.json +0 -11
- /package/config/{governance.neutral.json → government.neutral.json} +0 -0
- /package/modules/{governance → government}/CLAUDE.md +0 -0
- /package/modules/{governance → government}/migrations/governance_001_tables.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_002_seed_and_backfill.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_003_task_abandon_metic.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_004_criterion_create.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_005_resync_builder_rank_roles.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_006_version_create.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_007_delegate_archon_ops_to_metic.sql +0 -0
- /package/modules/{governance → government}/protected-surfaces.js +0 -0
- /package/modules/{governance → government}/resolver.js +0 -0
- /package/modules/{governance → government}/session-scopes.js +0 -0
- /package/modules/{governance → government}/session-scopes.json +0 -0
|
@@ -176,7 +176,7 @@ try {
|
|
|
176
176
|
// …the resolver reports it honestly (it is a defined catalog key; matrix V10
|
|
177
177
|
// proves no route consumes it via requirePermission, so it admits nothing
|
|
178
178
|
// in-app)…
|
|
179
|
-
const resolver = require('../modules/
|
|
179
|
+
const resolver = require('../modules/government/resolver.js');
|
|
180
180
|
const perms = await resolver.resolveBuilderPermissions(THETES);
|
|
181
181
|
ok(perms.includes(OPERATIONAL_KEY), 'the resolved set includes the operational grant');
|
|
182
182
|
console.log(` (resolved set ${perms.includes(SYSTEM_KEY) ? 'includes' : 'omits'} the forced '${SYSTEM_KEY}' — in-app it guards no route either way)`);
|
|
@@ -187,7 +187,7 @@ try {
|
|
|
187
187
|
// protected-path diff while the forced grant row sits in the database.
|
|
188
188
|
const { checkPermissionPaths } = require('../src/bongos/permission-path-check.js');
|
|
189
189
|
const sub = checkPermissionPaths({
|
|
190
|
-
changedFiles: ['migrations/999_glp_demo_evil.sql', 'modules/
|
|
190
|
+
changedFiles: ['migrations/999_glp_demo_evil.sql', 'modules/government/catalog.js'],
|
|
191
191
|
builderRank: 'thetes',
|
|
192
192
|
});
|
|
193
193
|
ok(sub.hardFail === true && sub.findings.length === 2,
|
|
@@ -30,7 +30,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
30
30
|
|
|
31
31
|
const require = createRequire(import.meta.url);
|
|
32
32
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
-
const GOV = path.join(ROOT, 'modules', '
|
|
33
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
34
34
|
const catalog = require(path.join(GOV, 'catalog.js'));
|
|
35
35
|
const resolver = require(path.join(GOV, 'resolver.js'));
|
|
36
36
|
const db = require(path.join(GOV, 'db.js'));
|
|
@@ -45,8 +45,8 @@ dbKernel.lookupSession = async () => {
|
|
|
45
45
|
|
|
46
46
|
const auth = require('../src/bongos/auth.js');
|
|
47
47
|
const seams = require('../src/module-seams.js');
|
|
48
|
-
const catalog = require('../modules/
|
|
49
|
-
const resolver = require('../modules/
|
|
48
|
+
const catalog = require('../modules/government/catalog.js');
|
|
49
|
+
const resolver = require('../modules/government/resolver.js');
|
|
50
50
|
const wandering = require('../modules/builder-settings/wandering-prefs.js');
|
|
51
51
|
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
52
52
|
|
|
@@ -126,7 +126,7 @@ async function runGate(name, { rank = null, resolve, port = true } = {}) {
|
|
|
126
126
|
lookupThrows = false;
|
|
127
127
|
seams._reset();
|
|
128
128
|
if (port) {
|
|
129
|
-
seams.registerProvider('
|
|
129
|
+
seams.registerProvider('government', {
|
|
130
130
|
// The real seed for that rank — the rows governance_002 gives the one
|
|
131
131
|
// rank-role a builder holds. The INPUT, not the oracle.
|
|
132
132
|
resolveBuilderPermissions: resolve || (async () => catalog.seedForRank(rank)),
|
|
@@ -144,7 +144,7 @@ async function runGate(name, { rank = null, resolve, port = true } = {}) {
|
|
|
144
144
|
|
|
145
145
|
test('A0: the hand-written audience table agrees with the real threshold function', () => {
|
|
146
146
|
// A second, catalog-free oracle for the literal above: auth.rankMeetsThreshold
|
|
147
|
-
// is the production rank predicate and reads nothing from modules/
|
|
147
|
+
// is the production rank predicate and reads nothing from modules/government/.
|
|
148
148
|
for (const gate of GATES) {
|
|
149
149
|
for (const rank of LIVE_RANKS) {
|
|
150
150
|
assert.equal(
|
|
@@ -203,7 +203,7 @@ test('B1: a session-lookup error fails CLOSED (redirect to sign-in, no page)', a
|
|
|
203
203
|
for (const gate of GATES) {
|
|
204
204
|
nextSession = null;
|
|
205
205
|
seams._reset();
|
|
206
|
-
seams.registerProvider('
|
|
206
|
+
seams.registerProvider('government', { resolveBuilderPermissions: async () => catalog.seedForRank('archon') });
|
|
207
207
|
lookupThrows = true;
|
|
208
208
|
const res = fakeRes();
|
|
209
209
|
let nexted = false;
|
|
@@ -244,7 +244,7 @@ test('B4: the page gates resolve per request — no memoised authority (ADR 0016
|
|
|
244
244
|
nextSession = { builder_id: 7, rank: 'archon' };
|
|
245
245
|
lookupThrows = false;
|
|
246
246
|
seams._reset();
|
|
247
|
-
seams.registerProvider('
|
|
247
|
+
seams.registerProvider('government', {
|
|
248
248
|
resolveBuilderPermissions: async () => { calls += 1; return keys; },
|
|
249
249
|
});
|
|
250
250
|
const req = { headers: { cookie: 'gds_session=validtoken' } };
|
|
@@ -57,8 +57,8 @@ const { RANKS } = require(path.join(ROOT, 'src', 'bongos', 'db-kernel.js'));
|
|
|
57
57
|
const routeRank = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
|
|
58
58
|
const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
59
59
|
const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
|
|
60
|
-
const catalog = require(path.join(ROOT, 'modules', '
|
|
61
|
-
const resolver = require(path.join(ROOT, 'modules', '
|
|
60
|
+
const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
61
|
+
const resolver = require(path.join(ROOT, 'modules', 'government', 'resolver.js'));
|
|
62
62
|
|
|
63
63
|
const { PERMISSIONS, RANK_ORDER, seedForRank } = catalog;
|
|
64
64
|
const FLOORED = PERMISSIONS.filter((p) => p.floor !== null);
|
|
@@ -287,7 +287,7 @@ function fakeRes() {
|
|
|
287
287
|
// not the logic — resolvedFor() is the real resolver over the real seed.
|
|
288
288
|
async function permissionAdmits(key, rank) {
|
|
289
289
|
seams._reset();
|
|
290
|
-
seams.registerProvider('
|
|
290
|
+
seams.registerProvider('government', { resolveBuilderPermissions: async () => resolvedFor(rank) });
|
|
291
291
|
let nexted = false;
|
|
292
292
|
await auth.requirePermission(key)({ builder: { id: 7, rank } }, fakeRes(), () => { nexted = true; });
|
|
293
293
|
seams._reset();
|
|
@@ -22,7 +22,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
22
22
|
|
|
23
23
|
const require = createRequire(import.meta.url);
|
|
24
24
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
-
const GOV = path.join(ROOT, 'modules', '
|
|
25
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
26
26
|
const catalog = require(path.join(GOV, 'catalog.js'));
|
|
27
27
|
const resolver = require(path.join(GOV, 'resolver.js'));
|
|
28
28
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// The load-bearing test is BEHAVIOR-IDENTITY. R101 moved WHERE the truth lives
|
|
10
10
|
// (a hardcoded PROTECTED_GLOBS constant in src/bongos/permission-path-check.js →
|
|
11
|
-
// governance-owned data in modules/
|
|
11
|
+
// governance-owned data in modules/government/protected-surfaces.json). It did
|
|
12
12
|
// NOT change WHAT is protected. EXPECTED_GLOBS_BEFORE_R101 below is the literal
|
|
13
13
|
// pre-change constant, pasted verbatim; the registry must reproduce it exactly,
|
|
14
14
|
// in order.
|
|
@@ -22,7 +22,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
22
22
|
|
|
23
23
|
const require = createRequire(import.meta.url);
|
|
24
24
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
-
const GOV = path.join(ROOT, 'modules', '
|
|
25
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
26
26
|
|
|
27
27
|
const ppc = require(path.join(ROOT, 'src', 'bongos', 'permission-path-check.js'));
|
|
28
28
|
const registry = require(path.join(GOV, 'protected-surfaces.js'));
|
|
@@ -83,14 +83,70 @@ const EXPECTED_GLOBS_BEFORE_R101 = [
|
|
|
83
83
|
'.claude/settings.json',
|
|
84
84
|
];
|
|
85
85
|
|
|
86
|
+
// Globs added to the registry SINCE R101, each with the change that added it.
|
|
87
|
+
// Kept separate from the verbatim pre-R101 array above so that array stays what
|
|
88
|
+
// it claims to be — the literal pre-change constant. Overwriting it to make a
|
|
89
|
+
// later addition pass would destroy the only evidence that R101 moved WHERE the
|
|
90
|
+
// truth lives without changing WHAT is protected.
|
|
91
|
+
//
|
|
92
|
+
// Each entry is { after, glob, why }: `after` is the glob it is inserted
|
|
93
|
+
// directly below, so the expected list is reconstructed in order rather than
|
|
94
|
+
// re-pasted (a re-paste is how the two lists drift apart).
|
|
95
|
+
const ADDED_SINCE_R101 = [
|
|
96
|
+
{
|
|
97
|
+
after: 'modules/governance/',
|
|
98
|
+
glob: 'modules/government/',
|
|
99
|
+
why: 'goal 1000068 / task 1003003 (ADR 0174) — the authz core is renamed '
|
|
100
|
+
+ 'governance→government. The new glob is registered BEFORE the folder '
|
|
101
|
+
+ 'moves because `protected` DERIVES from this registry: without it the '
|
|
102
|
+
+ 'new module key computes protected:false and both the Archon gate on '
|
|
103
|
+
+ 'goal creation and the pre-push block on these files would silently lapse.',
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
// The list the registry must produce TODAY: the pre-R101 set with each
|
|
108
|
+
// documented addition spliced in at its declared position.
|
|
109
|
+
const EXPECTED_GLOBS = (() => {
|
|
110
|
+
const out = [...EXPECTED_GLOBS_BEFORE_R101];
|
|
111
|
+
for (const a of ADDED_SINCE_R101) {
|
|
112
|
+
const at = out.indexOf(a.after);
|
|
113
|
+
assert.notEqual(at, -1, `ADDED_SINCE_R101 anchors on '${a.after}', which is not in the list`);
|
|
114
|
+
out.splice(at + 1, 0, a.glob);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
})();
|
|
118
|
+
|
|
86
119
|
// ---------- behavior identity: the same globs, at the same floor ----------
|
|
87
120
|
|
|
88
|
-
test('
|
|
89
|
-
|
|
90
|
-
|
|
121
|
+
test('nothing protected before R101 has been unprotected since', () => {
|
|
122
|
+
// The load-bearing half. Additions are ordinary policy; a REMOVAL silently
|
|
123
|
+
// unprotects a trust surface, so it is asserted separately and must never be
|
|
124
|
+
// satisfied by editing the pre-R101 array.
|
|
125
|
+
for (const glob of EXPECTED_GLOBS_BEFORE_R101) {
|
|
126
|
+
assert.ok(
|
|
127
|
+
registry.PROTECTED_GLOBS.includes(glob),
|
|
128
|
+
`'${glob}' was protected before R101 and is no longer in the registry — `
|
|
129
|
+
+ 'a protected surface cannot be dropped without an ADR saying why.',
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('the registry reproduces the expected glob list exactly, in order', () => {
|
|
135
|
+
assert.deepEqual(registry.PROTECTED_GLOBS, EXPECTED_GLOBS);
|
|
136
|
+
assert.deepEqual(ppc.PROTECTED_GLOBS, EXPECTED_GLOBS,
|
|
91
137
|
'the kernel matcher must expose the same derived list the registry declares');
|
|
92
138
|
});
|
|
93
139
|
|
|
140
|
+
test('every glob added since R101 carries a documented reason', () => {
|
|
141
|
+
for (const a of ADDED_SINCE_R101) {
|
|
142
|
+
assert.ok(a.why && a.why.length > 40,
|
|
143
|
+
`${a.glob} must record WHY it became protected — an undocumented addition `
|
|
144
|
+
+ 'is indistinguishable from a typo.');
|
|
145
|
+
assert.ok(registry.PROTECTED_GLOBS.includes(a.glob),
|
|
146
|
+
`${a.glob} is declared as added but is not in the registry`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
94
150
|
test('every surface floors at metic — the ADR 0043 floor, unchanged by the move', () => {
|
|
95
151
|
for (const s of registry.SURFACES) {
|
|
96
152
|
assert.equal(s.floor, 'metic', `${s.glob} must keep the Metic floor`);
|
|
@@ -128,7 +184,7 @@ test('the registry loads synchronously from the repo with no DB and no server',
|
|
|
128
184
|
"const p=require('./src/bongos/permission-path-check');" +
|
|
129
185
|
"process.stdout.write(String(p.PROTECTED_GLOBS.length)+':'+String(p.floorFor('migrations/001.sql')));",
|
|
130
186
|
], { cwd: ROOT, encoding: 'utf8', env: { ...process.env, DATABASE_URL: '', PGHOST: '' } });
|
|
131
|
-
assert.equal(out, `${
|
|
187
|
+
assert.equal(out, `${EXPECTED_GLOBS.length}:metic`);
|
|
132
188
|
});
|
|
133
189
|
|
|
134
190
|
test('the registry data file carries no DB/network dependency', () => {
|
|
@@ -178,7 +234,7 @@ test('a known path is protected at the right floor; an unknown path is not', ()
|
|
|
178
234
|
assert.equal(ppc.floorFor('src/bongos/auth.js'), 'metic');
|
|
179
235
|
assert.equal(ppc.permissionFor('src/bongos/auth.js'), 'path.protected.modify');
|
|
180
236
|
assert.equal(registry.isProtected('migrations/999_x.sql'), true, 'subtree globs still match');
|
|
181
|
-
assert.equal(registry.floorFor('modules/
|
|
237
|
+
assert.equal(registry.floorFor('modules/government/catalog.js'), 'metic');
|
|
182
238
|
|
|
183
239
|
assert.equal(ppc.floorFor('public/game/main.js'), null);
|
|
184
240
|
assert.equal(ppc.permissionFor('docs/session-logs/2026-07-30-x.md'), null);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// tests/governance_require_permission.mjs — the requirePermission gate (BV1.R97, ADR 0151).
|
|
2
2
|
//
|
|
3
3
|
// DB-free. The gate reaches the resolver through the `governance` KERNEL PORT
|
|
4
|
-
// (the one-way rule — core never requires modules/
|
|
4
|
+
// (the one-way rule — core never requires modules/government/), so a stub
|
|
5
5
|
// provider registered on the seam is the whole test double: no Postgres, no
|
|
6
6
|
// router, no live module. That is also the point being asserted — if auth.js ever
|
|
7
7
|
// went back to importing the module directly, these tests could not stub it.
|
|
@@ -23,7 +23,7 @@ const auth = require(path.join(ROOT, 'src', 'bongos', 'auth.js'));
|
|
|
23
23
|
const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
24
24
|
const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
|
|
25
25
|
const { classify } = require(path.join(ROOT, 'src', 'bongos', 'api-errors.js'));
|
|
26
|
-
const catalog = require(path.join(ROOT, 'modules', '
|
|
26
|
+
const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
27
27
|
|
|
28
28
|
// Minimal res double capturing status + body, with res.fail() installed exactly
|
|
29
29
|
// as the router installs it (mirrors tests/rank_gate.mjs).
|
|
@@ -42,7 +42,7 @@ function fakeRes() {
|
|
|
42
42
|
// resolveBuilderPermissions; pass null to leave the port UNREGISTERED.
|
|
43
43
|
async function run(gate, req, resolve) {
|
|
44
44
|
seams._reset();
|
|
45
|
-
if (resolve !== null) seams.registerProvider('
|
|
45
|
+
if (resolve !== null) seams.registerProvider('government', { resolveBuilderPermissions: resolve });
|
|
46
46
|
const res = fakeRes();
|
|
47
47
|
let nexted = false;
|
|
48
48
|
await gate(req, res, () => { nexted = true; });
|
|
@@ -89,7 +89,7 @@ test('the denial names the floor rank, not just the atom (task 1002613, map P-1)
|
|
|
89
89
|
// this test moves WITH a floor change instead of pinning stale prose — the
|
|
90
90
|
// D-16/N-3 failure shape the fix exists to avoid.
|
|
91
91
|
seams._reset();
|
|
92
|
-
seams.registerProvider('
|
|
92
|
+
seams.registerProvider('government', {
|
|
93
93
|
resolveBuilderPermissions: holding('idea.triage'),
|
|
94
94
|
permissionFloor: (key) => (catalog.byKey(key) || {}).floor || null,
|
|
95
95
|
});
|
|
@@ -143,7 +143,7 @@ test('an UNREGISTERED governance port denies, it does not admit', async () => {
|
|
|
143
143
|
|
|
144
144
|
test('a malformed port (no resolveBuilderPermissions) denies', async () => {
|
|
145
145
|
seams._reset();
|
|
146
|
-
seams.registerProvider('
|
|
146
|
+
seams.registerProvider('government', { isSystem: () => true });
|
|
147
147
|
const res = fakeRes();
|
|
148
148
|
let nexted = false;
|
|
149
149
|
await auth.requirePermission('idea.triage')({ builder: BUILDER }, res, () => { nexted = true; });
|
|
@@ -175,7 +175,7 @@ test('the gate resolves per request — no memoised authority (ADR 0016)', async
|
|
|
175
175
|
let calls = 0;
|
|
176
176
|
let keys = ['idea.triage'];
|
|
177
177
|
seams._reset();
|
|
178
|
-
seams.registerProvider('
|
|
178
|
+
seams.registerProvider('government', { resolveBuilderPermissions: async () => { calls += 1; return keys; } });
|
|
179
179
|
const gate = auth.requirePermission('idea.triage');
|
|
180
180
|
const first = fakeRes();
|
|
181
181
|
let admitted = false;
|
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
16
16
|
|
|
17
17
|
const require = createRequire(import.meta.url);
|
|
18
18
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
-
const GOV = path.join(ROOT, 'modules', '
|
|
19
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
20
20
|
const resolver = require(path.join(GOV, 'resolver.js'));
|
|
21
21
|
const catalog = require(path.join(GOV, 'catalog.js'));
|
|
22
22
|
const { effectivePermissions, resolveBuilderPermissions, builderHasPermissions } = resolver;
|
|
@@ -79,6 +79,6 @@ test('the kernel port exposes the resolver without core importing the module', (
|
|
|
79
79
|
assert.equal(typeof governancePort.builderHasPermissions, 'function');
|
|
80
80
|
assert.equal(typeof governancePort.isSystem, 'function');
|
|
81
81
|
const manifest = JSON.parse(fs.readFileSync(path.join(GOV, 'module.json'), 'utf8'));
|
|
82
|
-
assert.ok(manifest.provides.includes('
|
|
82
|
+
assert.ok(manifest.provides.includes('government'), 'module.json must declare the government port');
|
|
83
83
|
assert.ok(manifest.contributes.routes.includes('governance'), 'the port registers from the route factory');
|
|
84
84
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// tests/governance_role_sync.mjs — rank→role assignment stays in sync (BV1.R95b, ADR 0151).
|
|
2
2
|
//
|
|
3
|
-
// DB-free. The decision the writer executes is a pure function (modules/
|
|
4
|
-
//
|
|
5
|
-
// than a re-implementation: db.
|
|
3
|
+
// DB-free. The decision the writer executes is a pure function (modules/government/
|
|
4
|
+
// ranks.js planRankSync), so these exercise PRODUCTION logic directly rather
|
|
5
|
+
// than a re-implementation: db.syncRank reads the two row sets, calls this exact
|
|
6
6
|
// function, and executes its verdict. The SQL halves — the DELETE/INSERT the writer
|
|
7
7
|
// issues and the repair migration — are asserted structurally.
|
|
8
8
|
//
|
|
@@ -21,8 +21,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
21
21
|
|
|
22
22
|
const require = createRequire(import.meta.url);
|
|
23
23
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
|
-
const GOV = path.join(ROOT, 'modules', '
|
|
25
|
-
const {
|
|
24
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
25
|
+
const { planRankSync, isSeededRank } = require(path.join(GOV, 'ranks.js'));
|
|
26
26
|
|
|
27
27
|
// The seeded rank-roles exactly as governance_002 writes them.
|
|
28
28
|
const SEEDED = [
|
|
@@ -35,7 +35,7 @@ const seededRole = (rank) => ({ role_key: rank, seeded_rank: rank, is_system: tr
|
|
|
35
35
|
// An Archon-authored custom role from the R110 tab: no seeded_rank, not a system role.
|
|
36
36
|
const customRole = (key) => ({ role_key: key, seeded_rank: null, is_system: false });
|
|
37
37
|
|
|
38
|
-
const plan = (rank, currentRoles) =>
|
|
38
|
+
const plan = (rank, currentRoles) => planRankSync({ rank, currentRoles, seededRoles: SEEDED });
|
|
39
39
|
|
|
40
40
|
// ---------------------------------------------------------------------------
|
|
41
41
|
// registration — a builder with no roles gets the one matching their rank
|
|
@@ -118,7 +118,7 @@ test('a custom role is not mistaken for the rank-role even when its key matches
|
|
|
118
118
|
// role_key is UNIQUE so this cannot happen today, but the predicate must key on
|
|
119
119
|
// is_system + seeded_rank rather than on the key's spelling.
|
|
120
120
|
const impostor = { role_key: 'archon', seeded_rank: null, is_system: false };
|
|
121
|
-
assert.equal(
|
|
121
|
+
assert.equal(isSeededRank(impostor), false);
|
|
122
122
|
const p = plan('metic', [impostor]);
|
|
123
123
|
assert.deepEqual([...p.remove], [], 'a non-system role is never rotated away');
|
|
124
124
|
});
|
|
@@ -147,7 +147,7 @@ test('the sync is idempotent — running it twice changes nothing the second tim
|
|
|
147
147
|
const first = plan('metic', [seededRole('archon')]);
|
|
148
148
|
assert.ok(first.add.length + first.remove.length > 0, 'the first run must do work');
|
|
149
149
|
const after = first.next.map((key) => (SEEDED.find((s) => s.role_key === key) || customRole(key)));
|
|
150
|
-
const second =
|
|
150
|
+
const second = planRankSync({ rank: 'metic', currentRoles: after, seededRoles: SEEDED });
|
|
151
151
|
assert.deepEqual([...second.add], []);
|
|
152
152
|
assert.deepEqual([...second.remove], []);
|
|
153
153
|
assert.deepEqual([...second.next], [...first.next]);
|
|
@@ -175,15 +175,15 @@ const repairSql = fs.readFileSync(path.join(migDir, 'governance_005_resync_build
|
|
|
175
175
|
const stripSqlComments = (sql) => sql.replace(/--[^\n]*/g, '');
|
|
176
176
|
|
|
177
177
|
test('the writer DELETEs the superseded assignments, not just INSERTs the new one', () => {
|
|
178
|
-
assert.match(dbSrc, /DELETE FROM
|
|
179
|
-
'
|
|
180
|
-
assert.match(dbSrc, /INSERT INTO
|
|
178
|
+
assert.match(dbSrc, /DELETE FROM government_builder_ranks/,
|
|
179
|
+
'syncRank must delete superseded rank-roles — an additive-only writer preserves privilege retention');
|
|
180
|
+
assert.match(dbSrc, /INSERT INTO government_builder_ranks[\s\S]*ON CONFLICT \(builder_id, rank_key\) DO NOTHING/,
|
|
181
181
|
'the assignment insert must be idempotent');
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
test('the writer runs on the CALLER\'s transaction client when given one', () => {
|
|
185
|
-
assert.match(dbSrc, /async function
|
|
186
|
-
'
|
|
185
|
+
assert.match(dbSrc, /async function syncRank\(client,/,
|
|
186
|
+
'syncRank must take the caller\'s client so the rotation is atomic with the rank write');
|
|
187
187
|
assert.match(dbSrc, /const q = client \|\| pool;/,
|
|
188
188
|
'and fall back to the pool only when there is no surrounding transaction');
|
|
189
189
|
});
|
|
@@ -197,7 +197,7 @@ test('both rank-change twins rotate the role inside their transaction', () => {
|
|
|
197
197
|
assert.ok(upd !== -1, `${rel}: expected the rank UPDATE in _applyRankChangeTx`);
|
|
198
198
|
assert.ok(sync !== -1, `${rel}: the rank write must rotate the seeded rank-role`);
|
|
199
199
|
assert.ok(sync > upd, `${rel}: the rotation must follow the rank write inside the same transaction`);
|
|
200
|
-
assert.match(src, /resolveOptional\('
|
|
200
|
+
assert.match(src, /resolveOptional\('government\.rankSync'\)/,
|
|
201
201
|
`${rel}: must reach governance through the kernel port, never by importing the module`);
|
|
202
202
|
}
|
|
203
203
|
});
|
|
@@ -33,13 +33,13 @@ const require = createRequire(import.meta.url);
|
|
|
33
33
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
34
34
|
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
35
35
|
const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
36
|
-
const catalog = require(path.join(ROOT, 'modules', '
|
|
37
|
-
const govDb = require(path.join(ROOT, 'modules', '
|
|
36
|
+
const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
37
|
+
const govDb = require(path.join(ROOT, 'modules', 'government', 'db.js'));
|
|
38
38
|
const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
|
|
39
39
|
const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
|
|
40
40
|
const express = require('express');
|
|
41
41
|
|
|
42
|
-
const ROUTES_SRC = path.join(ROOT, 'modules', '
|
|
42
|
+
const ROUTES_SRC = path.join(ROOT, 'modules', 'government', 'routes', 'governance.js');
|
|
43
43
|
|
|
44
44
|
// ===========================================================================
|
|
45
45
|
// Layer 1 — gate classification + route-rank-check pins
|
|
@@ -88,7 +88,7 @@ test('every declared /governance route is pinned, and every pin exists (both dir
|
|
|
88
88
|
|
|
89
89
|
test('checkRouteRanks passes over the real tree (no governance findings)', () => {
|
|
90
90
|
const result = rrc.checkRouteRanks();
|
|
91
|
-
const ours = result.findings.filter((f) => String(f.file).includes('modules/
|
|
91
|
+
const ours = result.findings.filter((f) => String(f.file).includes('modules/government/'));
|
|
92
92
|
assert.deepEqual(ours, [], `governance findings: ${JSON.stringify(ours, null, 2)}`);
|
|
93
93
|
});
|
|
94
94
|
|
|
@@ -131,11 +131,11 @@ api.requireBuilder = (req, res, next) => { req.builder = { id: 42, rank: 'archon
|
|
|
131
131
|
|
|
132
132
|
function grant(keys) {
|
|
133
133
|
seams._reset();
|
|
134
|
-
seams.registerProvider('
|
|
134
|
+
seams.registerProvider('government', { resolveBuilderPermissions: async () => keys });
|
|
135
135
|
}
|
|
136
136
|
grant(['governance.manage']);
|
|
137
137
|
|
|
138
|
-
const buildGovernanceRouter = require(path.join(ROOT, 'modules', '
|
|
138
|
+
const buildGovernanceRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'governance.js'));
|
|
139
139
|
const app = express();
|
|
140
140
|
app.use(express.json());
|
|
141
141
|
app.use(attachFail);
|
|
@@ -15,9 +15,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
17
17
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
-
const { RANK_ROLE_SEED, RANK_ORDER } = require(path.join(ROOT, 'modules', '
|
|
18
|
+
const { RANK_ROLE_SEED, RANK_ORDER } = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
19
19
|
|
|
20
|
-
const MIG_DIR = path.join(ROOT, 'modules', '
|
|
20
|
+
const MIG_DIR = path.join(ROOT, 'modules', 'government', 'migrations');
|
|
21
21
|
const seedSql = fs.readFileSync(path.join(MIG_DIR, 'governance_002_seed_and_backfill.sql'), 'utf8');
|
|
22
22
|
const tablesSql = fs.readFileSync(path.join(MIG_DIR, 'governance_001_tables.sql'), 'utf8');
|
|
23
23
|
|
|
@@ -28,8 +28,16 @@ const tablesSql = fs.readFileSync(path.join(MIG_DIR, 'governance_001_tables.sql'
|
|
|
28
28
|
// ADR 0152), and what must equal RANK_ROLE_SEED is the union of them all, not any one
|
|
29
29
|
// file. Read in filename order; every file is scanned for `SELECT '<role>', unnest(…)`
|
|
30
30
|
// blocks, so a new grant migration needs no change here.
|
|
31
|
+
// SCHEMA migrations — they shape the tables, they do not grant. Excluded by NAME
|
|
32
|
+
// rather than by "does it INSERT?" so that `a grant migration must INSERT something`
|
|
33
|
+
// below stays a real assertion: a file we believe grants but which inserts nothing is
|
|
34
|
+
// a bug worth failing on, and a content-sniffing filter would quietly excuse it.
|
|
35
|
+
const SCHEMA_MIGRATIONS = new Set([
|
|
36
|
+
'governance_001_tables.sql', // creates the three tables
|
|
37
|
+
'government_001_rename_from_governance.sql', // ADR 0174 rename (task 1003004)
|
|
38
|
+
]);
|
|
31
39
|
const grantMigrationFiles = fs.readdirSync(MIG_DIR)
|
|
32
|
-
.filter((f) => f.endsWith('.sql') && f
|
|
40
|
+
.filter((f) => f.endsWith('.sql') && !SCHEMA_MIGRATIONS.has(f))
|
|
33
41
|
.sort();
|
|
34
42
|
const grantSqlByFile = grantMigrationFiles.map((f) => [f, fs.readFileSync(path.join(MIG_DIR, f), 'utf8')]);
|
|
35
43
|
|
|
@@ -38,7 +38,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
38
38
|
|
|
39
39
|
const require = createRequire(import.meta.url);
|
|
40
40
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
41
|
-
const GOV = path.join(ROOT, 'modules', '
|
|
41
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
42
42
|
|
|
43
43
|
// auth.js destructures lookupSession at load, so the stub must be installed before
|
|
44
44
|
// ANYTHING pulls auth.js in (src/module-api does, via the governance route file).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// tests/governance_vanilla_boot.mjs — the VANILLA BOOT proof (BV1.R102, ADR 0151).
|
|
2
2
|
//
|
|
3
3
|
// The core/host split only holds if a FRESH instance — one that has never been
|
|
4
|
-
// branded, with no config/
|
|
4
|
+
// branded, with no config/government.json on disk and no rows in
|
|
5
5
|
// governance_builder_roles — comes up sane, anonymous, and powerless. This file is
|
|
6
6
|
// that proof. It is deliberately scoped to what R96's tests/governance_config.mjs
|
|
7
7
|
// does NOT cover: that file pins the config CONTRACT (precedence, per-field merge,
|
|
@@ -37,7 +37,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
37
37
|
|
|
38
38
|
const require = createRequire(import.meta.url);
|
|
39
39
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
40
|
-
const GOV = path.join(ROOT, 'modules', '
|
|
40
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
41
41
|
const catalog = require(path.join(GOV, 'catalog.js'));
|
|
42
42
|
const { loadGovernanceConfig, resolveGovernanceConfig, NEUTRAL_PATH, INSTANCE_PATH } = require(path.join(GOV, 'config.js'));
|
|
43
43
|
const { effectivePermissions, resolveBuilderPermissions } = require(path.join(GOV, 'resolver.js'));
|
|
@@ -127,10 +127,10 @@ test('the identity guard is not a no-op — a planted host brand in the neutral
|
|
|
127
127
|
// trip it, in the exact file this task is about.
|
|
128
128
|
for (const brand of ['Off the Boats', 'amazonprimea.com']) {
|
|
129
129
|
const poisoned = NEUTRAL_TEXT.replace('"roles"', `"instance": "${brand}",\n "roles"`);
|
|
130
|
-
const found = fitness.scanPromptIdentity('config/
|
|
130
|
+
const found = fitness.scanPromptIdentity('config/government.neutral.json', poisoned, '.json');
|
|
131
131
|
assert.ok(found.length >= 1, `a "${brand}" literal in the neutral pack must be caught`);
|
|
132
132
|
}
|
|
133
|
-
assert.deepEqual(fitness.scanPromptIdentity('config/
|
|
133
|
+
assert.deepEqual(fitness.scanPromptIdentity('config/government.neutral.json', NEUTRAL_TEXT, '.json'), [], 'the real pack is clean');
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
// ── (c) the trust boundary on a vanilla boot ────────────────────────────────────
|
|
@@ -32,7 +32,7 @@ const SRC = fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'ut
|
|
|
32
32
|
// correct for the genuinely public pages and a BUG anywhere else, so every such
|
|
33
33
|
// row is spelled out rather than left to a default.
|
|
34
34
|
//
|
|
35
|
-
// Audiences (the floors live in modules/
|
|
35
|
+
// Audiences (the floors live in modules/government/catalog.js; the gate
|
|
36
36
|
// functions in src/bongos/auth.js):
|
|
37
37
|
// requireBuilderPage page.view.public any signed-in builder (xenos+)
|
|
38
38
|
// requireGovernancePage page.view.governance metic+ (ADR 0157)
|
|
@@ -336,8 +336,8 @@ app.use(attachFail);
|
|
|
336
336
|
// currently impersonating, so the cases below exercise the PERMISSION decision rather
|
|
337
337
|
// than the port-missing fallback.
|
|
338
338
|
const seams = require('../src/module-seams.js');
|
|
339
|
-
const catalog = require('../modules/
|
|
340
|
-
seams.registerProvider('
|
|
339
|
+
const catalog = require('../modules/government/catalog.js');
|
|
340
|
+
seams.registerProvider('government', {
|
|
341
341
|
resolveBuilderPermissions: async () => catalog.seedForRank(
|
|
342
342
|
(fixture.session && fixture.session.rank) || 'xenos',
|
|
343
343
|
),
|
package/tests/module_loader.mjs
CHANGED
|
@@ -197,7 +197,7 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
|
|
|
197
197
|
// That is precisely how the agents regression reached an open PR (task 1002485).
|
|
198
198
|
loader._reset();
|
|
199
199
|
const snap = loader.loaderSnapshot();
|
|
200
|
-
assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'dev-box', 'discord', 'economy', '
|
|
200
|
+
assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'dev-box', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui']);
|
|
201
201
|
assert.deepEqual(snap.errors, []);
|
|
202
202
|
assert.deepEqual(snap.skipped, []);
|
|
203
203
|
loader._reset();
|
|
@@ -43,8 +43,8 @@ async function t(name, fn) {
|
|
|
43
43
|
// src/bongos/routes, so we patch the resolved module before requiring the
|
|
44
44
|
// routes.
|
|
45
45
|
const authPath = require.resolve('../src/bongos/auth.js');
|
|
46
|
-
const catalog = require('../modules/
|
|
47
|
-
// Mirrors the canonical ladder in modules/
|
|
46
|
+
const catalog = require('../modules/government/catalog.js');
|
|
47
|
+
// Mirrors the canonical ladder in modules/government/catalog.js — omitting a rung
|
|
48
48
|
// would resolve its floor to Infinity and deny everyone, including archon. Kept at
|
|
49
49
|
// module scope (BV1.R104) so requireRank and requirePermission below share ONE
|
|
50
50
|
// ladder; R105's branch still had the stale 3-rung inline copy that dropped thetes.
|
|
@@ -40,7 +40,7 @@ const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
|
|
|
40
40
|
const sequence = require(path.join(ROOT, 'scripts', 'gds', 'sequence.js'));
|
|
41
41
|
const boxConnect = require(path.join(ROOT, 'scripts', 'gds', 'box-connect-lib.js'));
|
|
42
42
|
const boxes = require(path.join(ROOT, 'modules', 'dev-box', 'boxes.js'));
|
|
43
|
-
const catalog = require(path.join(ROOT, 'modules', '
|
|
43
|
+
const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
44
44
|
|
|
45
45
|
const LADDER = kernel.LIVE_RANK_LADDER;
|
|
46
46
|
const TIER = kernel.RANK_TIER;
|
|
@@ -142,7 +142,7 @@ const MAP_ALLOWLIST = new Set([
|
|
|
142
142
|
// Where a rank-ladder array literal is allowed to exist.
|
|
143
143
|
const LADDER_ALLOWLIST = new Set([
|
|
144
144
|
'src/bongos/db-kernel.js', // the canonical source itself
|
|
145
|
-
'modules/
|
|
145
|
+
'modules/government/catalog.js',
|
|
146
146
|
'src/bongos/route-rank-check.js',
|
|
147
147
|
...STATIC_LADDER_FILES,
|
|
148
148
|
]);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// a downgrade that reads as legitimate, and a published API doc that misstates authority.
|
|
10
10
|
//
|
|
11
11
|
// THE ORACLE PROBLEM (the trap R103 fell into). The classifier DERIVES a permission's
|
|
12
|
-
// rank from modules/
|
|
12
|
+
// rank from modules/government/catalog.js, so asserting it back against catalog.floor
|
|
13
13
|
// would move both sides together and prove nothing. Every expectation below is therefore
|
|
14
14
|
// either a HAND-WRITTEN literal (a floor read off the catalog once, by a human, and
|
|
15
15
|
// frozen here — so a floor edit reds this file and has to be argued with) or
|
|
@@ -67,7 +67,7 @@ const HAND_WRITTEN_FLOORS = {
|
|
|
67
67
|
// oracle, not a tautology.
|
|
68
68
|
|
|
69
69
|
test('P0: the parsed floor map matches the catalog module exactly (key set and floors)', () => {
|
|
70
|
-
const catalog = require(path.join(ROOT, 'modules', '
|
|
70
|
+
const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
71
71
|
const parsed = rrc.permissionFloors();
|
|
72
72
|
const evaluated = new Map(catalog.PERMISSIONS.map((p) => [p.key, p.floor]));
|
|
73
73
|
assert.equal(parsed.size, evaluated.size,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"key": "governance",
|
|
3
|
-
"title": "Governance (configurable roles & permissions)",
|
|
4
|
-
"description": "Discord-style configurable roles and permissions (ADR 0151, BONGOS-V1 goal 1000030). Permissions are the atomic authority unit and the four ranks (xenos/thetes/metic/archon) become seeded roles; this module owns the code-defined permission catalog (each key flagged `system` = trust-boundary, non-grantable, or operational = delegable) plus the compatibility seed that reproduces today's rank thresholds exactly. A core-domain module, default-on: a Cloud Bongos instance ships configurable roles the way it ships its status page. This first slice (BV1.R94) is the catalog + seed as code constants — the single source the resolver (R95), the requirePermission gate (R97), and the Archon tab (R110/R111) all read. Preserves ADR 0016 (authority resolved server-side, per request, uncached).",
|
|
5
|
-
"coreVersion": "^1.18.0",
|
|
6
|
-
"default": true,
|
|
7
|
-
"maintenance": { "status": "core-maintained" },
|
|
8
|
-
"contributes": { "migrations": true, "routes": ["governance", "role-sync"] },
|
|
9
|
-
"provides": ["governance", "governance.roleSync"],
|
|
10
|
-
"consumes": []
|
|
11
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/modules/{governance → government}/migrations/governance_005_resync_builder_rank_roles.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|