@bongos/core 1.19.218 → 1.19.220
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 +18 -13
- package/modules/dev-box/box-access.js +47 -8
- package/modules/government/migrations/government_005_seeded_rank_descriptions.sql +78 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +5 -1
- package/tests/box_access.mjs +57 -2
- package/tests/government_seed.mjs +30 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.220",
|
|
6
|
+
"core_contract": "1.19.220",
|
|
7
|
+
"source_commit": "c0428ccbca81e3243f981295653e2996e9cda202",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-18T03:14:10.378Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 311,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1303,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1635,
|
|
20
|
+
"tree_sha256": "aac7f97a1131f16c75b768382681a70085520bf75be38b35124497b3d734e097",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2697,7 +2697,7 @@
|
|
|
2697
2697
|
{
|
|
2698
2698
|
"path": "modules/dev-box/box-access.js",
|
|
2699
2699
|
"mode": "0000644",
|
|
2700
|
-
"sha256": "
|
|
2700
|
+
"sha256": "870976966de7c48be4172390dc8e4f7d2eb6ac53e20dfb3fd5d40b08cfcd7850"
|
|
2701
2701
|
},
|
|
2702
2702
|
{
|
|
2703
2703
|
"path": "modules/dev-box/box-credential.js",
|
|
@@ -2964,6 +2964,11 @@
|
|
|
2964
2964
|
"mode": "0000644",
|
|
2965
2965
|
"sha256": "75913af702d6529c9a5cf2e2c42d9c329eea92ae99274fb71f2524520de4cfbf"
|
|
2966
2966
|
},
|
|
2967
|
+
{
|
|
2968
|
+
"path": "modules/government/migrations/government_005_seeded_rank_descriptions.sql",
|
|
2969
|
+
"mode": "0000644",
|
|
2970
|
+
"sha256": "dc570f8871b578a8af965ffbc91fc4e195f2e7fce7d7886166f0025ac2b47bce"
|
|
2971
|
+
},
|
|
2967
2972
|
{
|
|
2968
2973
|
"path": "modules/government/module.json",
|
|
2969
2974
|
"mode": "0000644",
|
|
@@ -4287,12 +4292,12 @@
|
|
|
4287
4292
|
{
|
|
4288
4293
|
"path": "package-lock.json",
|
|
4289
4294
|
"mode": "0000644",
|
|
4290
|
-
"sha256": "
|
|
4295
|
+
"sha256": "352bfdf3484cd27c33da65632970d50a1b99e164008d45b2689cb365fee16720"
|
|
4291
4296
|
},
|
|
4292
4297
|
{
|
|
4293
4298
|
"path": "package.json",
|
|
4294
4299
|
"mode": "0000644",
|
|
4295
|
-
"sha256": "
|
|
4300
|
+
"sha256": "719aa0ff0ff7323f96ad1257c3505c5929943f64d3e00f9cdb6f4fce78dc150e"
|
|
4296
4301
|
},
|
|
4297
4302
|
{
|
|
4298
4303
|
"path": "public-docs/index.html",
|
|
@@ -5907,7 +5912,7 @@
|
|
|
5907
5912
|
{
|
|
5908
5913
|
"path": "src/module-api.js",
|
|
5909
5914
|
"mode": "0000644",
|
|
5910
|
-
"sha256": "
|
|
5915
|
+
"sha256": "6f39901735ba86e7abd2b5770b36146104ac47dec250131ac24eeca6754ef5f2"
|
|
5911
5916
|
},
|
|
5912
5917
|
{
|
|
5913
5918
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6202,7 +6207,7 @@
|
|
|
6202
6207
|
{
|
|
6203
6208
|
"path": "tests/box_access.mjs",
|
|
6204
6209
|
"mode": "0000644",
|
|
6205
|
-
"sha256": "
|
|
6210
|
+
"sha256": "1e4db680c5925e473fff9ad6247e797bd9089fdf3696c3ffb161903bda41dcda"
|
|
6206
6211
|
},
|
|
6207
6212
|
{
|
|
6208
6213
|
"path": "tests/box_auth_check.mjs",
|
|
@@ -6907,7 +6912,7 @@
|
|
|
6907
6912
|
{
|
|
6908
6913
|
"path": "tests/government_seed.mjs",
|
|
6909
6914
|
"mode": "0000644",
|
|
6910
|
-
"sha256": "
|
|
6915
|
+
"sha256": "8240fef8ed5b7e9a4c16e17e8dd5ffa522a99e320d41229f58f2214e00bcf160"
|
|
6911
6916
|
},
|
|
6912
6917
|
{
|
|
6913
6918
|
"path": "tests/government_session_scope.mjs",
|
|
@@ -171,24 +171,63 @@ const BASE_SPARSE_PATHS = [
|
|
|
171
171
|
// globs come in two shapes: directory globs (trailing '/', e.g. 'modules/dev-box/')
|
|
172
172
|
// and file-list globs (exact paths, e.g. the kernel key's 'src/bongos/db.js'). We
|
|
173
173
|
// KEEP the directory globs (normalized to no trailing slash, matching the BASE
|
|
174
|
-
// style)
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
// BASE
|
|
174
|
+
// style). A FILE glob cannot be emitted as-is, so it contributes its PARENT
|
|
175
|
+
// DIRECTORY instead — unless BASE or another emitted dir already covers it.
|
|
176
|
+
//
|
|
177
|
+
// It used to be dropped outright, on the reasoning that "every file glob in the
|
|
178
|
+
// map sits under a BASE directory (src/bongos, scripts) or is a top-level file
|
|
179
|
+
// cone mode pulls automatically". That was true when written and **silently
|
|
180
|
+
// stopped being true**: ADR 0174 gave the `government` key the file globs
|
|
181
|
+
// `config/government.json` + `config/government.neutral.json`, and `config` is
|
|
182
|
+
// not in BASE. The result was a goal walled to `government` whose box could never
|
|
183
|
+
// see two files inside its own scope wall — which released task 1003018 (R05 of
|
|
184
|
+
// goal 1000069), whose entire deliverable was one of them. Rather than restore an
|
|
185
|
+
// invariant that expires again the next time a module claims a config file, the
|
|
186
|
+
// parent is emitted and the class is covered by construction.
|
|
187
|
+
//
|
|
188
|
+
// TRADE-OFF, stated rather than implied: the parent directory is a SUPERSET of
|
|
189
|
+
// the glob — asking for `config/government.json` materializes all of `config/`.
|
|
190
|
+
// The box's view is already a superset of the wall (BASE hands out `scripts`,
|
|
191
|
+
// `docs` and `tests` wholesale regardless of scope), so this is consistent with
|
|
192
|
+
// the existing posture, not a new concession. The WALL is unchanged: what a
|
|
193
|
+
// builder may author is still decided by goal-scope-check against the globs, not
|
|
194
|
+
// by what happens to be on disk.
|
|
195
|
+
//
|
|
196
|
+
// Top-level file globs (no '/') contribute nothing — cone mode includes root
|
|
197
|
+
// files automatically. Deterministic: BASE in declared order, then the module
|
|
198
|
+
// dirs sorted + deduped.
|
|
179
199
|
function sparsePathsForScopeKeys(scopeKeys, moduleScopeMap) {
|
|
180
200
|
const base = [...BASE_SPARSE_PATHS];
|
|
181
201
|
const seen = new Set(base);
|
|
182
202
|
const extra = [];
|
|
203
|
+
// Cone mode materializes a directory and everything under it, so a path is
|
|
204
|
+
// already covered when IT or any ancestor has been emitted. This is what keeps
|
|
205
|
+
// `scripts/gds/box.js` from adding `scripts/gds` — BASE's `scripts` covers it.
|
|
206
|
+
const covered = (dir) => {
|
|
207
|
+
let p = dir;
|
|
208
|
+
for (;;) {
|
|
209
|
+
if (seen.has(p)) return true;
|
|
210
|
+
const cut = p.lastIndexOf('/');
|
|
211
|
+
if (cut < 0) return false;
|
|
212
|
+
p = p.slice(0, cut);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const add = (dir) => {
|
|
216
|
+
if (!dir || covered(dir)) return;
|
|
217
|
+
seen.add(dir);
|
|
218
|
+
extra.push(dir);
|
|
219
|
+
};
|
|
183
220
|
for (const key of Array.isArray(scopeKeys) ? scopeKeys : []) {
|
|
184
221
|
const entry = moduleScopeMap && typeof moduleScopeMap.scopeForModule === 'function'
|
|
185
222
|
? moduleScopeMap.scopeForModule(key)
|
|
186
223
|
: null;
|
|
187
224
|
if (!entry || !Array.isArray(entry.globs)) continue;
|
|
188
225
|
for (const glob of entry.globs) {
|
|
189
|
-
if (typeof glob !== 'string' || !glob
|
|
190
|
-
|
|
191
|
-
|
|
226
|
+
if (typeof glob !== 'string' || !glob) continue;
|
|
227
|
+
if (glob.endsWith('/')) { add(glob.replace(/\/+$/, '')); continue; }
|
|
228
|
+
const cut = glob.lastIndexOf('/');
|
|
229
|
+
if (cut < 0) continue; // a root file — cone mode already includes it
|
|
230
|
+
add(glob.slice(0, cut));
|
|
192
231
|
}
|
|
193
232
|
}
|
|
194
233
|
extra.sort();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
-- government_005_seeded_rank_descriptions.sql — the last "role" on the government
|
|
2
|
+
-- side: the four seeded ranks still DESCRIBE themselves as rank-roles
|
|
3
|
+
-- (task 1003083 / goal 1000068, criterion govt-ranks-not-roles). Decision of
|
|
4
|
+
-- record: ADR 0174.
|
|
5
|
+
--
|
|
6
|
+
-- ── WHAT WAS MISSED, AND WHY ────────────────────────────────────────────────
|
|
7
|
+
-- The rename cleaned the CONFIG TEMPLATE but never the LIVE ROWS.
|
|
8
|
+
-- config/government.neutral.json already reads "Seeded rank — …" for all four,
|
|
9
|
+
-- so the repo looks correct and a fresh instance boots with the right words in
|
|
10
|
+
-- its template. But `GET /api/bongos/government/ranks` reads the DB, and on the
|
|
11
|
+
-- live database every seeded row still carries the text governance_002 wrote:
|
|
12
|
+
--
|
|
13
|
+
-- archon : 'Seeded rank-role — top authority. Adds the admin + system (non-grantable) powers.'
|
|
14
|
+
-- metic : 'Seeded rank-role — the trusted working rank. Adds review/triage/authoring.'
|
|
15
|
+
-- thetes : 'Seeded rank-role — graduated newcomer. Adds the general claim queue.'
|
|
16
|
+
-- xenos : 'Seeded rank-role — the sandboxed entry rank. Own-work + public read.'
|
|
17
|
+
--
|
|
18
|
+
-- government_001 renamed the TABLES and government_002 renamed the permission
|
|
19
|
+
-- KEYS; neither touched this column. The hall's Government tab renders these
|
|
20
|
+
-- strings, so the surface goal 1000068 renamed has been displaying the reserved
|
|
21
|
+
-- word to the Owner ever since.
|
|
22
|
+
--
|
|
23
|
+
-- ── WHY THE FITNESS WALL DID NOT CATCH IT ───────────────────────────────────
|
|
24
|
+
-- Check 19 (checkVocabularyWall, scripts/gds/fitness.js) scans FILES on the
|
|
25
|
+
-- government surface, and only .js/.json/.html/.css at that. It cannot see DB
|
|
26
|
+
-- row text. That is the rule working as designed, not a hole in it — which is
|
|
27
|
+
-- precisely why this needed a migration rather than a lint fix.
|
|
28
|
+
--
|
|
29
|
+
-- ── WHY THIS CORRECTS FORWARD INSTEAD OF EDITING governance_002 ─────────────
|
|
30
|
+
-- governance_002 is applied and run-once (schema_migrations is stem-keyed), so
|
|
31
|
+
-- editing its text would change history without changing any database. It also
|
|
32
|
+
-- stays deliberately spelled the old way: it is the factual record of what was
|
|
33
|
+
-- once true, which is exactly the carve-out ADR 0174 grants the historical
|
|
34
|
+
-- migrations. government_002 (the permission-key rename) is the worked example
|
|
35
|
+
-- this copies. On a FRESH install the two converge — governance_002 seeds the
|
|
36
|
+
-- old text, this rewrites it — so both paths end identically.
|
|
37
|
+
--
|
|
38
|
+
-- ── THE GUARD IS LOAD-BEARING ───────────────────────────────────────────────
|
|
39
|
+
-- The WHERE clause matches the OLD SEEDED TEXT EXACTLY rather than blanket-
|
|
40
|
+
-- updating by rank_key. A host may have relabelled a seeded rank through
|
|
41
|
+
-- config (config/government.json may relabel a seeded rank — it may never
|
|
42
|
+
-- re-grant one), and overwriting a deliberate customisation with our template
|
|
43
|
+
-- would be a silent regression dressed as a typo fix. Matching the exact string
|
|
44
|
+
-- governance_002 wrote touches only rows nobody has edited. It also makes this
|
|
45
|
+
-- migration idempotent: re-running it matches nothing.
|
|
46
|
+
--
|
|
47
|
+
-- NAMING ONLY. No grant, floor, or authority change; is_system and seeded_rank
|
|
48
|
+
-- are untouched; no row is inserted or deleted. Safe to run against a database
|
|
49
|
+
-- where some or all four have already been corrected by hand.
|
|
50
|
+
--
|
|
51
|
+
-- ROLLBACK — reverse the mapping (lossless; restores the governance_002 text):
|
|
52
|
+
-- UPDATE government_ranks SET description = replace(description, 'Seeded rank —', 'Seeded rank-role —')
|
|
53
|
+
-- WHERE is_system AND rank_key IN ('xenos','thetes','metic','archon')
|
|
54
|
+
-- AND description LIKE 'Seeded rank —%';
|
|
55
|
+
|
|
56
|
+
BEGIN;
|
|
57
|
+
|
|
58
|
+
UPDATE government_ranks AS g
|
|
59
|
+
SET description = v.new_description
|
|
60
|
+
FROM (VALUES
|
|
61
|
+
('xenos',
|
|
62
|
+
'Seeded rank-role — the sandboxed entry rank. Own-work + public read.',
|
|
63
|
+
'Seeded rank — the sandboxed entry rank. Own-work + public read.'),
|
|
64
|
+
('thetes',
|
|
65
|
+
'Seeded rank-role — graduated newcomer. Adds the general claim queue.',
|
|
66
|
+
'Seeded rank — graduated newcomer. Adds the general claim queue.'),
|
|
67
|
+
('metic',
|
|
68
|
+
'Seeded rank-role — the trusted working rank. Adds review/triage/authoring.',
|
|
69
|
+
'Seeded rank — the trusted working rank. Adds review/triage/authoring.'),
|
|
70
|
+
('archon',
|
|
71
|
+
'Seeded rank-role — top authority. Adds the admin + system (non-grantable) powers.',
|
|
72
|
+
'Seeded rank — top authority. Adds the admin + system (non-grantable) powers.')
|
|
73
|
+
) AS v(rank_key, old_description, new_description)
|
|
74
|
+
WHERE g.rank_key = v.rank_key
|
|
75
|
+
AND g.is_system
|
|
76
|
+
AND g.description = v.old_description;
|
|
77
|
+
|
|
78
|
+
COMMIT;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.220",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.220",
|
|
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.19.
|
|
3
|
+
"version": "1.19.220",
|
|
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/src/module-api.js
CHANGED
|
@@ -863,8 +863,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
863
863
|
// landed since 1.19.216 with no explicit bump. run 32084927511. (task 1002620)
|
|
864
864
|
// 1.19.218 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
865
865
|
// landed since 1.19.217 with no explicit bump. run 32086598601. (task 1002620)
|
|
866
|
+
// 1.19.219 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
867
|
+
// landed since 1.19.218 with no explicit bump. run 32094215476. (task 1002620)
|
|
868
|
+
// 1.19.220 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
869
|
+
// landed since 1.19.219 with no explicit bump. run 32094701831. (task 1002620)
|
|
866
870
|
// ---------------------------------------------------------------------------
|
|
867
|
-
const CORE_VERSION = '1.19.
|
|
871
|
+
const CORE_VERSION = '1.19.220'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
868
872
|
|
|
869
873
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
870
874
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/box_access.mjs
CHANGED
|
@@ -221,14 +221,69 @@ t('cone-filter: dev-box keeps its DIRS, DROPS its file globs (scripts/gds/box.js
|
|
|
221
221
|
assert.ok(out.includes('devbox-app'), 'the app dir is kept');
|
|
222
222
|
assert.ok(!out.some((p) => p.endsWith('.js') || p.endsWith('.sh')), 'no file globs survive the cone filter');
|
|
223
223
|
assert.ok(!out.includes('scripts/gds/box.js'), 'the box CLI file is dropped (BASE scripts covers it)');
|
|
224
|
+
// …and its PARENT is not added either: BASE's `scripts` already covers it, so
|
|
225
|
+
// the ancestor check must stop `scripts/gds` from appearing. This is the half
|
|
226
|
+
// that keeps the file-glob rescue below from over-sharing.
|
|
227
|
+
assert.ok(!out.includes('scripts/gds'), 'a covered parent is not re-emitted');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// A file glob whose parent is NOT already covered must contribute that parent.
|
|
231
|
+
// Regression for the live failure that released task 1003018 (R05 of goal
|
|
232
|
+
// 1000069): the `government` key's config/government*.json globs were dropped on
|
|
233
|
+
// the reasoning that every file glob sits under a BASE directory — true when
|
|
234
|
+
// written, false once ADR 0174 added them, since `config` is not in BASE. The box
|
|
235
|
+
// could not see two files inside its own scope wall.
|
|
236
|
+
t('file-glob rescue: government contributes `config` for its config/*.json globs', () => {
|
|
237
|
+
const out = access.sparsePathsForScopeKeys(['government'], scopeMap);
|
|
238
|
+
assert.ok(out.includes('modules/government'), 'the module dir is kept');
|
|
239
|
+
assert.ok(out.includes('config'), 'config/government.neutral.json must reach the box via its parent');
|
|
240
|
+
assert.ok(!out.some((p) => p.endsWith('.json')), 'still no file paths — cone mode would reject one');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
t('file-glob rescue holds for EVERY key in the map — no module can silently lose one', () => {
|
|
244
|
+
// Enumerated, never sampled: a module that gains a file glob under a new parent
|
|
245
|
+
// is covered the day it lands, which is exactly how this bug escaped before.
|
|
246
|
+
for (const key of scopeMap.moduleKeys()) {
|
|
247
|
+
const out = access.sparsePathsForScopeKeys([key], scopeMap);
|
|
248
|
+
for (const glob of scopeMap.scopeForModule(key).globs) {
|
|
249
|
+
if (glob.endsWith('/')) continue;
|
|
250
|
+
const cut = glob.lastIndexOf('/');
|
|
251
|
+
if (cut < 0) continue; // a root file — cone mode includes it automatically
|
|
252
|
+
const parent = glob.slice(0, cut);
|
|
253
|
+
const reachable = out.some((p) => parent === p || parent.startsWith(`${p}/`));
|
|
254
|
+
assert.ok(reachable, `${key}: no cone path materializes ${glob} (parent ${parent})`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
t('the emitted set stays directories only, for every key', () => {
|
|
260
|
+
// A leading dot is a DIRECTORY here ('.devcontainer', '.claude'), so the
|
|
261
|
+
// extension test must require a non-dot character before the dot — otherwise
|
|
262
|
+
// every BASE dotfile-dir reads as a file and this assertion fires on healthy
|
|
263
|
+
// input. (It did, on first write.)
|
|
264
|
+
const looksLikeFile = (seg) => /[^.]\.[a-z0-9]+$/i.test(seg);
|
|
265
|
+
for (const key of scopeMap.moduleKeys()) {
|
|
266
|
+
for (const p of access.sparsePathsForScopeKeys([key], scopeMap)) {
|
|
267
|
+
assert.ok(!looksLikeFile(p.split('/').pop()),
|
|
268
|
+
`${key}: ${p} looks like a file — git sparse-checkout set would abort on it`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
224
271
|
});
|
|
225
272
|
|
|
226
|
-
t('cone-filter: the kernel key
|
|
273
|
+
t('cone-filter: the kernel key materializes its real DIR globs (config, src/module-loader)', () => {
|
|
227
274
|
const out = access.sparsePathsForScopeKeys(['kernel'], scopeMap);
|
|
228
275
|
// kernel is mostly individual src/bongos/*.js FILES (dropped — BASE src/bongos
|
|
229
276
|
// covers them via cone ancestor-inclusion) + a few genuine DIR globs.
|
|
230
277
|
assert.ok(out.includes('config'), 'config/ is a real dir the task needs, not in BASE');
|
|
231
|
-
|
|
278
|
+
// src/module-loader/ is a real dir not in BASE, but it is NOT emitted verbatim:
|
|
279
|
+
// kernel also carries the file globs src/instance-config.js, src/branding.js,
|
|
280
|
+
// src/modules.js and src/module-api.js, whose rescued parent is `src` — and once
|
|
281
|
+
// `src` is emitted the ancestor check absorbs every path beneath it, this one
|
|
282
|
+
// included. Asserting literal membership would be asserting emission ORDER, not
|
|
283
|
+
// reachability. What the box must actually get is the DIRECTORY's contents, so
|
|
284
|
+
// assert that, using the same idiom as the enumerated rescue test above.
|
|
285
|
+
const reaches = (dir) => out.some((p) => dir === p || dir.startsWith(`${p}/`));
|
|
286
|
+
assert.ok(reaches('src/module-loader'), 'src/module-loader must be materialized (here via the rescued `src`)');
|
|
232
287
|
assert.ok(!out.includes('src/bongos/db.js'), 'the kernel FILE globs are dropped');
|
|
233
288
|
assert.ok(!out.includes('server.js'), 'top-level files never enter the sparse set (cone auto-includes them)');
|
|
234
289
|
});
|
|
@@ -37,6 +37,7 @@ const NON_GRANT_MIGRATIONS = new Set([
|
|
|
37
37
|
'government_001_rename_from_governance.sql', // ADR 0174 table rename (task 1003004)
|
|
38
38
|
'government_002_rename_permission_keys.sql', // ADR 0174 key rename (task 1003006)
|
|
39
39
|
'government_003_board_tables.sql', // ADR 0175 Board Room schema (task 1003016)
|
|
40
|
+
'government_005_seeded_rank_descriptions.sql', // ADR 0174 seeded-description text fix (task 1003083)
|
|
40
41
|
]);
|
|
41
42
|
|
|
42
43
|
// Permission keys RENAMED after the grant migrations that first granted them.
|
|
@@ -142,6 +143,35 @@ test('later grant migrations are idempotent + transactional', () => {
|
|
|
142
143
|
}
|
|
143
144
|
});
|
|
144
145
|
|
|
146
|
+
// The bug this guards (task 1003083): goal 1000068 renamed the government, and
|
|
147
|
+
// config/government.neutral.json was updated to say "Seeded rank — …". The LIVE
|
|
148
|
+
// government_ranks rows were not, so `GET /government/ranks` — and the hall tab
|
|
149
|
+
// that renders it — kept serving governance_002's "Seeded rank-role — …" long
|
|
150
|
+
// after the rename shipped. Two lists of the same strings, only one maintained:
|
|
151
|
+
// exactly the drift this file exists to prevent, in the one column nobody had
|
|
152
|
+
// pointed a test at. The vocabulary fitness wall cannot see it either — Check 19
|
|
153
|
+
// scans .js/.json/.html/.css files, never DB row text or .sql.
|
|
154
|
+
test('the description correction lands EXACTLY the config template text (no second list that drifts)', () => {
|
|
155
|
+
const correction = fs.readFileSync(path.join(MIG_DIR, 'government_005_seeded_rank_descriptions.sql'), 'utf8');
|
|
156
|
+
const neutral = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'government.neutral.json'), 'utf8'));
|
|
157
|
+
|
|
158
|
+
// Each VALUES row is ('<rank_key>', '<old_description>', '<new_description>').
|
|
159
|
+
const rows = [...stripComments(correction).matchAll(/\(\s*'([a-z]+)'\s*,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)/g)]
|
|
160
|
+
.map(([, rankKey, , newDescription]) => [rankKey, newDescription]);
|
|
161
|
+
|
|
162
|
+
assert.equal(rows.length, RANK_ORDER.length,
|
|
163
|
+
'the correction must cover every seeded rank, not a subset');
|
|
164
|
+
|
|
165
|
+
for (const [rankKey, newDescription] of rows) {
|
|
166
|
+
const template = neutral.ranks[rankKey];
|
|
167
|
+
assert.ok(template, `${rankKey}: not a rank in config/government.neutral.json`);
|
|
168
|
+
assert.equal(newDescription, template.description,
|
|
169
|
+
`${rankKey}: the migration writes text the config template does not carry — the two lists have drifted again`);
|
|
170
|
+
assert.ok(!/\brole/i.test(newDescription),
|
|
171
|
+
`${rankKey}: "role" is reserved for the crafts (ADR 0174) — a rank describes itself as a rank`);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
145
175
|
test('migration objects are all governance_-namespaced (ADR 0083 §5)', () => {
|
|
146
176
|
for (const sql of [tablesSql, seedSql]) {
|
|
147
177
|
for (const m of sql.matchAll(/CREATE (?:TABLE|INDEX)(?: IF NOT EXISTS)? (\w+)/g)) {
|