@bongos/core 1.19.219 → 1.19.221

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.219",
6
- "core_contract": "1.19.219",
7
- "source_commit": "c74ac31eedb3ec3676f8517b51302df625298931",
5
+ "core_version": "1.19.221",
6
+ "core_contract": "1.19.221",
7
+ "source_commit": "30b2efb16495a941d6a4b70fd03b772cd69bce23",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-18T03:06:20.562Z",
9
+ "built_at": "2026-08-18T03:23:47.936Z",
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": "4da1158ccf79b2a7a5253dad319cc74192493be9bda2e6a8874df8c60f581cc5",
19
+ "file_count": 1635,
20
+ "tree_sha256": "aa35386f9d066c748b4374cf27b4dc5fbf48ccfef32dfb8ef8c60cf44fdb8c96",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1352,7 +1352,7 @@
1352
1352
  {
1353
1353
  "path": "docs/adr/0174-government-terminology-ranks-not-roles.md",
1354
1354
  "mode": "0000644",
1355
- "sha256": "c645ce8202fa8e8faa59d3aa00a8167477b5223817b760a733b38864d7625c1d"
1355
+ "sha256": "2b813490fe355bfdb783d9b28eee01b97bfb6d7d4343a3f59aeeb6fac185b79b"
1356
1356
  },
1357
1357
  {
1358
1358
  "path": "docs/adr/0175-the-board-room-full-idea-ratification.md",
@@ -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",
@@ -2992,7 +2997,7 @@
2992
2997
  {
2993
2998
  "path": "modules/government/routes/government.js",
2994
2999
  "mode": "0000644",
2995
- "sha256": "c229aec4f02fe970f4a3f798d370eef9b080cd0837f906b574362abda57107ad"
3000
+ "sha256": "872f144bcd788e36f8b8bc87e5bd31f2389169ffa5d930e03d74ba24a0264f3b"
2996
3001
  },
2997
3002
  {
2998
3003
  "path": "modules/government/routes/rank-sync.js",
@@ -4287,12 +4292,12 @@
4287
4292
  {
4288
4293
  "path": "package-lock.json",
4289
4294
  "mode": "0000644",
4290
- "sha256": "ac23681c5493a57f1faca551e4896e4a7a4e2dabb6246bf2a84c39ae6e571257"
4295
+ "sha256": "5b55ea236b725452ef02e308616d3827927f68e82a483022a856e4066864dc99"
4291
4296
  },
4292
4297
  {
4293
4298
  "path": "package.json",
4294
4299
  "mode": "0000644",
4295
- "sha256": "7122e5cca352a5abc317d25267eba6c0bab955febf2179bf362a896be424d0bc"
4300
+ "sha256": "ec221145b014962cce2d2c9fc9e231a0b296bb65153ba8079e30f2f4e80b9df7"
4296
4301
  },
4297
4302
  {
4298
4303
  "path": "public-docs/index.html",
@@ -4917,7 +4922,7 @@
4917
4922
  {
4918
4923
  "path": "scripts/gds/fitness.js",
4919
4924
  "mode": "0000644",
4920
- "sha256": "5cc94859033c924884efa60f50f8a1f7810a85382c42d785d62b804181a195f5"
4925
+ "sha256": "a255afac6eb4a2b8269e896032e8eabf10f104aed5a0f1e8d0a7348695625424"
4921
4926
  },
4922
4927
  {
4923
4928
  "path": "scripts/gds/gate-review.js",
@@ -5737,7 +5742,7 @@
5737
5742
  {
5738
5743
  "path": "src/bongos/module-scope-map.js",
5739
5744
  "mode": "0000644",
5740
- "sha256": "808b164783dae50e0be37f35dcbcae7b2d474868746a240a957b89130870f483"
5745
+ "sha256": "277097c686b695688dee35d158c4bb8384b4a5a89c61f30c7bda7d9f4b477bee"
5741
5746
  },
5742
5747
  {
5743
5748
  "path": "src/bongos/module-submissions.js",
@@ -5772,7 +5777,7 @@
5772
5777
  {
5773
5778
  "path": "src/bongos/route-rank-check.js",
5774
5779
  "mode": "0000644",
5775
- "sha256": "422647dc473583a67509644b04e47b8fc2279bb117fa1b6db1030502b5287870"
5780
+ "sha256": "af5bdd9e362923afc1b78a741b5254147678b34cecb7d92f879387cc94a05631"
5776
5781
  },
5777
5782
  {
5778
5783
  "path": "src/bongos/routes.js",
@@ -5907,7 +5912,7 @@
5907
5912
  {
5908
5913
  "path": "src/module-api.js",
5909
5914
  "mode": "0000644",
5910
- "sha256": "d509a7e4f16027ccef999e8aadf1a2cef5368bce5cd0c3bda3b7eb61cbceaa5f"
5915
+ "sha256": "4aa9331871262decd3e4b2a16e1cd9ca58b4e4000228cde02086ad209a1bb4b7"
5911
5916
  },
5912
5917
  {
5913
5918
  "path": "src/module-loader/catalog.js",
@@ -6902,12 +6907,12 @@
6902
6907
  {
6903
6908
  "path": "tests/government_routes.mjs",
6904
6909
  "mode": "0000644",
6905
- "sha256": "3b2e0ca134737992590132f089d5f93b479ad6cec83a5d2a932412b3edaf4547"
6910
+ "sha256": "7f406d5296489d08804eae5b431d07244e1f2f3464513cccccb72e7b33253c14"
6906
6911
  },
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",
@@ -55,3 +55,33 @@ This was the closest call in the directive, and it was decided against the alter
55
55
  **Rejected: renaming the ladder slugs to match.** `xenos`/`thetes`/`metic`/`archon` are referenced by `builders.rank`, `requireRank`, the protected-path enforcers, the Discord role map, and years of session logs. The directive did not ask for it, and bundling it here would turn a bounded rename into a trust-boundary migration.
56
56
 
57
57
  **Rejected: a compatibility shim kept indefinitely.** The old `/governance/*` route paths answer for exactly one release, logging each hit, and R05 records the removal release in this ADR's lineage. A permanent alias would mean both vocabularies stay correct forever, which is the state this ADR exists to end.
58
+
59
+ ## Lineage — the compatibility alias, and when it actually went
60
+
61
+ The Decision above promised the old `/governance/*` route paths would answer for
62
+ **exactly one release**, and that R05 would record the removal release here. That
63
+ record was never written, and the alias outlived the promise by a wide margin.
64
+
65
+ | | |
66
+ |---|---|
67
+ | Alias shipped | task 1003006 (R05), with the rename |
68
+ | Promised life | one release |
69
+ | Actually removed | **task 1003084**, core 1.19.219+ — many releases later |
70
+
71
+ Two source comments (`src/bongos/module-scope-map.js`, and the `VOCAB_ALLOW`
72
+ entry in `scripts/gds/fitness.js`) asserted the route aliases had already gone
73
+ with the module-key alias in R05. They had not. A reader who trusted them —
74
+ and the goal 1000068 criterion review nearly did — would conclude the
75
+ compatibility surface no longer existed while production was still 308-ing on
76
+ it. The alias was found by curling live, the same way all three of its original
77
+ defects were.
78
+
79
+ **The lesson, recorded because it generalises past this ADR:** a removal that is
80
+ promised in prose and pinned by nothing does not happen. The shim had thorough
81
+ tests, but they asserted the shim WORKED — so deleting the shim would have
82
+ deleted its own evidence. Task 1003084 inverts them: `tests/government_routes.mjs`
83
+ now asserts the legacy paths **404**, with a companion assertion that the current
84
+ paths still route so the check cannot pass by everything being broken. A
85
+ time-boxed compatibility surface should ship with the test of its ABSENCE
86
+ written at the same time, disabled until the clock runs out — not with a
87
+ calendar note in a comment.
@@ -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;
@@ -182,61 +182,8 @@ module.exports = function buildGovernmentRouter() {
182
182
  return rank;
183
183
  }
184
184
  const ARCHON = 'archon';
185
- const ARCHON_REASON = 'the Archon rank-rank holds every permission and is immutable — it is the rank that can never be locked out of this tab (ADR 0172)';
186
- const ASSIGN_REASON = 'a seeded rank-rank is assigned by rank (R95b sync); change the builder\'s rank instead';
187
-
188
- // ── the one-release LEGACY path aliases: /governance/* -> /government/* ────
189
- // ADR 0174 committed to keeping the pre-rename paths answering for exactly one
190
- // release. The case this actually protects is narrow but real: a browser that
191
- // still has the OLD hall page open when the instance upgrades will keep calling
192
- // the legacy /governance/* paths against the new server. Everything else that
193
- // paths (the hall bundle, the generated client) ships INSIDE the same core
194
- // package and moves in lockstep.
195
- //
196
- // A 308 preserves the method and body, so a PUT/DELETE grant toggle survives
197
- // the hop. The alias carries NO gate of its own — it redirects, and the real
198
- // route re-runs requireBuilder + requirePermission on the followed request, so
199
- // this cannot become a way around the wall.
200
- //
201
- // REMOVE THIS BLOCK one release after the rename ships (goal 1000068 follow-up).
202
- // The `alias` counter in the log line is how we tell whether anything still
203
- // calls them before deleting.
204
- //
205
- // EXPRESS 4 WILDCARD SYNTAX. This is express 4.22 — the wildcard is a bare `*`
206
- // and the captured tail is req.params[0]. Express 5's named form (`/*rest`) is
207
- // NOT a wildcard here, it is a literal path segment, so it silently matches
208
- // nothing and every legacy path 404s. It shipped that way once (task 1003006)
209
- // and was only caught by curling the live site after the cutover — the module
210
- // parsed fine, so nothing local failed.
211
- const LEGACY_PATH_PREFIX = '/governance';
212
- // The legacy noun -> the current one, as a NAMED DATA PAIR rather than an
213
- // inline regex literal. An inline pattern here was silently rewritten by a
214
- // repo-wide sweep so that both sides matched, turning the translation into an
215
- // identity function that redirected to a path which does not exist. Naming the
216
- // pair and asserting they differ makes that class of damage loud.
217
- //
218
- // Every line in this block that spells the legacy vocabulary carries the word
219
- // LEGACY on purpose: that is what the ADR 0174 fitness allowlist keys on, so
220
- // the shim can name what it is compatible with without excusing the whole file.
221
- const LEGACY_SEGMENT = 'roles'; // legacy
222
- const CURRENT_SEGMENT = 'ranks';
223
- if (LEGACY_SEGMENT === CURRENT_SEGMENT) {
224
- throw new Error('government legacy alias: the rename pair collapsed to an identity — a sweep has eaten it');
225
- }
226
- router.all(`${LEGACY_PATH_PREFIX}/*`, (req, res, next) => {
227
- const tail = req.params[0] || ''; // express 4: bare * captures here
228
- if (!tail) return next();
229
- // Translate the leading segment, keep everything after it, keep the query.
230
- const translated = tail === LEGACY_SEGMENT || tail.startsWith(`${LEGACY_SEGMENT}/`)
231
- ? CURRENT_SEGMENT + tail.slice(LEGACY_SEGMENT.length)
232
- : tail;
233
- const qs = req.originalUrl.includes('?') ? req.originalUrl.slice(req.originalUrl.indexOf('?')) : '';
234
- // req.baseUrl is the router's MOUNT point (/api/gds). Redirecting without it
235
- // sends the caller to /government/... at the site root, which 404s.
236
- const target = `${req.baseUrl}/government/${translated}${qs}`;
237
- console.warn(`[government] legacy path hit: ${req.method} ${req.originalUrl} -> ${target} (ADR 0174 alias; remove one release after the rename)`);
238
- return res.redirect(308, target);
239
- });
185
+ const ARCHON_REASON = 'the Archon rank holds every permission and is immutable — it is the rank that can never be locked out of this tab (ADR 0172)';
186
+ const ASSIGN_REASON = 'a seeded rank is assigned by rank (R95b sync); change the builder\'s rank instead';
240
187
 
241
188
  // rank: perm government.manage — the full rank list with each rank's grants.
242
189
  router.get('/government/ranks', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
@@ -337,7 +284,7 @@ module.exports = function buildGovernmentRouter() {
337
284
  ? 'substrate-enforced (ADR 0043) — enforced outside the app by the pre-push hook / main-audit / the rank floor, so a grant row here would confer nothing. Non-grantable to EVERY rank.'
338
285
  : catalog.isPrincipalKey(permissionKey)
339
286
  ? 'a machine-principal permission — held by an exact builders.system_role identity, never by a rank (ADR 0151 §3)'
340
- : 'a system (trust-boundary) permission is non-grantable to a CUSTOM rank; only a sub-Archon rank-rank may carry one (ADR 0172)',
287
+ : 'a system (trust-boundary) permission is non-grantable to a CUSTOM rank; only a sub-Archon rank may carry one (ADR 0172)',
341
288
  });
342
289
  }
343
290
  const granted = await db.grantRankPermission({ rankKey, permissionKey });
@@ -387,7 +334,7 @@ module.exports = function buildGovernmentRouter() {
387
334
  if (!rank.is_system || !catalog.RANK_ORDER.includes(rank.seeded_rank)) {
388
335
  return res.fail('rank_has_no_default', 400, {
389
336
  rank_key: rankKey,
390
- reason: 'only a seeded rank-rank has a catalog default to reset to; revoke a custom rank\'s grants instead',
337
+ reason: 'only a seeded rank has a catalog default to reset to; revoke a custom rank\'s grants instead',
391
338
  });
392
339
  }
393
340
  // The seed is the floor derivation, which never contains a principal key and
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.219",
3
+ "version": "1.19.221",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.219",
9
+ "version": "1.19.221",
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.219",
3
+ "version": "1.19.221",
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",
@@ -1688,16 +1688,6 @@ const VOCAB_ALLOW = [
1688
1688
  + 'repo, so the old spelling is still read. Dropping it would silently stop '
1689
1689
  + 'honouring a label override someone already configured.',
1690
1690
  },
1691
- {
1692
- file: 'modules/government/routes/government.js',
1693
- match: 'legacy',
1694
- why: 'the one-release 308 alias from the pre-rename route paths (ADR 0174). A '
1695
- + 'compatibility shim has to name the thing it is compatible with. The match is '
1696
- + 'the word LEGACY, and the shim block is written so that every line spelling the '
1697
- + 'old vocabulary carries it — so the exemption is scoped to the shim rather than '
1698
- + 'to the whole file, and a stray "role" elsewhere in this file still reds the '
1699
- + 'build. Delete with the alias block one release after the rename ships.',
1700
- },
1701
1691
  {
1702
1692
  file: 'modules/government/module.json',
1703
1693
  match: 'role',
@@ -171,15 +171,18 @@ const MODULE_GLOBS = {
171
171
  // module. `protected` DERIVES true — its 'modules/governance/' glob is in
172
172
  // PROTECTED_GLOBS (the authz core) — so creating a goal scoped to governance is
173
173
  // Archon-only (BV1.R57 / POST /goals). The config globs are the per-instance
174
- // role templates (R96), listed here as goal territory; the module code lands in
174
+ // rank templates (R96), listed here as goal territory; the module code lands in
175
175
  // R93+ (the carve precedes the feature — ADR 0086 §4). Written as exact paths,
176
176
  // not 'config/governance*.json' — the touches convention has no wildcards. ---
177
177
  // RENAMED governance → government (goal 1000068, ADR 0174). The
178
- // transitional `governance` alias key was removed in R05 (task 1003006)
179
- // together with the /governance/* route aliases — the same compatibility
180
- // window. The three live goals that named the old key (1000030, 1000057,
181
- // 1000068) had it removed from goals.scope_modules in the same change, so
182
- // no stored row references a key the map no longer defines.
178
+ // transitional `governance` alias key was removed in R05 (task 1003006).
179
+ // The /governance/* ROUTE aliases outlived it: this comment claimed they
180
+ // went in the same change, but they kept 308-ing on production for months
181
+ // of releases past the one ADR 0174 allowed them, and the false claim here
182
+ // is part of why nobody noticed. They were removed separately, in task
183
+ // 1003084. The three live goals that named the old key (1000030, 1000057,
184
+ // 1000068) had it removed from goals.scope_modules in R05, so no stored row
185
+ // references a key the map no longer defines.
183
186
  government: [
184
187
  'modules/government/',
185
188
  'config/government.json', 'config/government.neutral.json',
@@ -232,7 +232,11 @@ const EXPECTED_RANKS = {
232
232
  };
233
233
 
234
234
  // The authz-core route pins (ADR 0174). Paths and the gating atom were renamed
235
- // governance→government in R05 (task 1003006); the module-key alias is gone.
235
+ // governance→government in R05 (task 1003006), which also removed the module-key
236
+ // alias. The /governance/* ROUTE aliases did NOT go with it — they kept 308-ing
237
+ // in production for many releases past the single one ADR 0174 allowed, and were
238
+ // removed separately in task 1003084. Nothing under /governance/* is routed now,
239
+ // so every pin below is the only spelling of its surface.
236
240
  const GOVERNMENT_PINS = {
237
241
  'GET /government/ranks': 'perm:government.manage',
238
242
  'GET /government/permissions': 'perm:government.manage',
package/src/module-api.js CHANGED
@@ -865,8 +865,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
865
865
  // landed since 1.19.217 with no explicit bump. run 32086598601. (task 1002620)
866
866
  // 1.19.219 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
867
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)
870
+ // 1.19.221 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
871
+ // landed since 1.19.220 with no explicit bump. run 32095276039. (task 1002620)
868
872
  // ---------------------------------------------------------------------------
869
- const CORE_VERSION = '1.19.219'; // CI auto-patch carrier (ADR 0161); see changelog above
873
+ const CORE_VERSION = '1.19.221'; // CI auto-patch carrier (ADR 0161); see changelog above
870
874
 
871
875
  // A namespaced logger so a module's log lines are attributable + consistent.
872
876
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -396,27 +396,25 @@ test('DELETE /governance/assignments validates the builder id param', async () =
396
396
  assert.equal(r.status, 400);
397
397
  });
398
398
 
399
- // ── the one-release /governance/* compatibility alias (ADR 0174) ──────────────
399
+ // ── the /governance/* alias is GONE (ADR 0174, task 1003084) ──────────────
400
400
  //
401
- // This block exists because the alias shipped BROKEN and nothing caught it.
402
- // Three independent defects, none visible to a parse check or a unit test that
403
- // only imports the module:
404
- // 1. it was written with express 5's named-wildcard syntax (`/*rest`) on an
405
- // express 4.22 router, where that is a literal segment — so it matched
406
- // nothing and every legacy path 404'd;
407
- // 2. the redirect omitted req.baseUrl, sending callers to /government/... at
408
- // the site root instead of under the /api/gds mount;
409
- // 3. a repo-wide roles->ranks sweep rewrote the SOURCE side of the
410
- // /^\/roles/ -> '/ranks' translation, turning it into an identity that
411
- // redirected to a path which does not exist.
401
+ // ADR 0174 gave the pre-rename paths exactly one release. They outlived it by
402
+ // months of releases — still 308-ing on production long after two source
403
+ // comments claimed they had been removed — because nothing asserted the
404
+ // REMOVAL, only the shim's behaviour. Deleting the shim's tests with the shim
405
+ // would have recreated exactly that hole, so the assertions are INVERTED
406
+ // instead: the legacy paths must now 404.
412
407
  //
413
- // All three only surfaced when the running site was curled after the cutover.
414
- // These tests drive a real router so they cannot repeat that.
415
-
416
- // A SECOND server, mounted at /api/gds exactly as production does. The main
417
- // harness above mounts at the root, so it cannot see a missing req.baseUrl —
418
- // which was one of the three real defects. Mounting like production is the
419
- // whole point of these three tests.
408
+ // The shim itself shipped broken three times over (express 5 wildcard syntax on
409
+ // an express 4.22 router; a redirect that dropped req.baseUrl; a repo-wide
410
+ // roles->ranks sweep that rewrote the SOURCE side of the translation into an
411
+ // identity). Every one of those only surfaced when the running site was curled.
412
+ // That is why this block still drives a REAL router mounted the way production
413
+ // mounts it, rather than asserting on source text.
414
+
415
+ // A SECOND server, mounted at /api/gds exactly as production does — the main
416
+ // harness above mounts at the root, so it cannot tell "no route" from "route at
417
+ // the wrong mount point".
420
418
  const aliasApp = express();
421
419
  aliasApp.use(express.json());
422
420
  aliasApp.use(attachFail);
@@ -429,33 +427,37 @@ after(() => aliasServer.close());
429
427
  const hop = (method, p) =>
430
428
  fetch(`${ALIAS_BASE}${p}`, { method, redirect: 'manual' });
431
429
 
432
- test('the legacy /governance/* alias redirects, under the mount, with the noun translated', async () => {
433
- const cases = [
434
- ['GET', '/api/gds/governance/roles', '/api/gds/government/ranks'],
435
- ['GET', '/api/gds/governance/permissions', '/api/gds/government/permissions'],
436
- ['GET', '/api/gds/governance/assignments', '/api/gds/government/assignments'],
437
- ['PUT', '/api/gds/governance/roles/metic/permissions/idea.triage',
438
- '/api/gds/government/ranks/metic/permissions/idea.triage'],
439
- ['POST', '/api/gds/governance/roles/metic/reset', '/api/gds/government/ranks/metic/reset'],
430
+ test('every legacy /governance/* path is gone — no redirect, no route', async () => {
431
+ // The exact paths the removed shim used to translate. A 308 here means the
432
+ // alias is back; a 200 would mean something answers the old vocabulary
433
+ // directly, which is worse.
434
+ const legacy = [
435
+ ['GET', '/api/gds/governance/roles'],
436
+ ['GET', '/api/gds/governance/permissions'],
437
+ ['GET', '/api/gds/governance/assignments'],
438
+ ['PUT', '/api/gds/governance/roles/metic/permissions/idea.triage'],
439
+ ['POST', '/api/gds/governance/roles/metic/reset'],
440
+ ['GET', '/api/gds/governance/ranks'],
440
441
  ];
441
- for (const [method, from, expected] of cases) {
442
- const r = await hop(method, from);
443
- assert.equal(r.status, 308, `${method} ${from} must 308 — a 404 means the wildcard is not matching`);
444
- assert.equal(r.headers.get('location'), expected,
445
- `${method} ${from} must keep the /api/gds mount AND translate roles→ranks`);
442
+ for (const [method, p] of legacy) {
443
+ const r = await hop(method, p);
444
+ assert.equal(r.status, 404, `${method} ${p} must 404 — the one-release alias was removed in task 1003084`);
445
+ assert.equal(r.headers.get('location'), null, `${method} ${p} must not redirect anywhere`);
446
446
  }
447
447
  });
448
448
 
449
- test('the alias preserves the query string', async () => {
450
- const r = await hop('GET', '/api/gds/governance/roles?x=1&y=2');
451
- assert.equal(r.headers.get('location'), '/api/gds/government/ranks?x=1&y=2');
449
+ test('the current /government/* paths still route (the removal took only the alias)', async () => {
450
+ // The guard against over-deletion: proving the old paths 404 is worthless if
451
+ // the new ones 404 too. These reach the real gate and are refused for lack of
452
+ // a session — a 401/403, never a 404.
453
+ for (const p of ['/api/gds/government/ranks', '/api/gds/government/permissions']) {
454
+ const r = await hop('GET', p);
455
+ assert.notEqual(r.status, 404, `${p} must still be routed`);
456
+ }
452
457
  });
453
458
 
454
- test('the alias carries no gate of its own — the followed request re-runs the real one', async () => {
455
- // A 308 preserves method and body, so the wall is enforced on the FOLLOWED
456
- // request by the real route. The alias must never become a way around it.
459
+ test('no source line reintroduces a legacy path alias', () => {
457
460
  const src = fs.readFileSync(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'), 'utf8');
458
- const block = src.slice(src.indexOf('LEGACY_PATH_PREFIX'), src.indexOf('res.redirect(308'));
459
- assert.ok(!/requirePermission|requireBuilder/.test(block),
460
- 'the alias must not carry its own gate — it redirects, and the real route gates the followed request');
461
+ assert.ok(!/LEGACY_PATH_PREFIX/.test(src), 'the alias block is gone — do not restore it');
462
+ assert.ok(!/redirect\(308/.test(src), 'no 308 vocabulary redirect belongs on this surface any more');
461
463
  });
@@ -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)) {