@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 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.218",
6
- "core_contract": "1.19.218",
7
- "source_commit": "a00e409d98973f59f934543251e819a2b9deabd5",
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-18T01:00:37.333Z",
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": 1302,
14
+ "functional_verbatim": 1303,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1634,
20
- "tree_sha256": "fe7773c1c51b74af666fd1bd0db3495ae383d717ca65af9a4ef310f9e252efb0",
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": "9357b423836c4e8d2cd3c9144d294c3cd3b498ac30755a9e6f62b5bbaa2add5e"
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": "199faf243475315d256fab355abdc1bfa2547a1d42d0ac623080b44be144de26"
4295
+ "sha256": "352bfdf3484cd27c33da65632970d50a1b99e164008d45b2689cb365fee16720"
4291
4296
  },
4292
4297
  {
4293
4298
  "path": "package.json",
4294
4299
  "mode": "0000644",
4295
- "sha256": "81b98db9ae28ac2606efa211fe791632c1857e796c4d7e00cddbcf26daf76362"
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": "559f382c12c7f37bc85227630cd5670d9f4b7749e9994036d85c221a5d3d3b43"
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": "694153e377a78d4325aa4ff9b947f691d71362f843da25815b9740821903802c"
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": "e7f980f02abfb057d725beaf5730fdd71f4e8249af2180bf4a355632481330d6"
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) and DROP the file globs — every file glob in the map sits under a BASE
175
- // directory (src/bongos, scripts) or is a top-level/ancestor file cone mode pulls
176
- // automatically, so base already materializes it. (A future non-cone sparse move
177
- // could pull individual files; out of scope — cone mode stays.) Deterministic:
178
- // BASE in declared order, then the module dirs sorted + deduped.
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.endsWith('/')) continue; // dirs only (cone mode)
190
- const dir = glob.replace(/\/+$/, '');
191
- if (!seen.has(dir)) { seen.add(dir); extra.push(dir); }
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.218",
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.218",
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.218",
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.218'; // CI auto-patch carrier (ADR 0161); see changelog above
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');
@@ -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 contributes only its real DIR globs (config, src/module-loader)', () => {
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
- assert.ok(out.includes('src/module-loader'), 'src/module-loader/ is a real dir not in BASE');
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)) {