@bongos/core 1.19.220 → 1.19.222
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 +17 -17
- package/config/government.neutral.json +7 -0
- package/docs/adr/0174-government-terminology-ranks-not-roles.md +30 -0
- package/modules/government/config.js +86 -1
- package/modules/government/routes/government.js +4 -57
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +0 -10
- package/src/bongos/module-scope-map.js +9 -6
- package/src/bongos/route-rank-check.js +5 -1
- package/src/module-api.js +5 -1
- package/tests/government_config.mjs +147 -3
- package/tests/government_routes.mjs +43 -41
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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.222",
|
|
6
|
+
"core_contract": "1.19.222",
|
|
7
|
+
"source_commit": "4425d4ace9a3b996b50be1b59002e4cf85f027db",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-18T03:
|
|
9
|
+
"built_at": "2026-08-18T03:36:13.104Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 311,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1635,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "6bf87d2a8bb013b102f3ae8287a56fd6fe7ed99ed49b48f4607abcb047f59f74",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -397,7 +397,7 @@
|
|
|
397
397
|
{
|
|
398
398
|
"path": "config/government.neutral.json",
|
|
399
399
|
"mode": "0000644",
|
|
400
|
-
"sha256": "
|
|
400
|
+
"sha256": "7ab2f7aff4bc52d09ee1a4aa3b76a7646ff2ee462bbc459941a468a9cc1246eb"
|
|
401
401
|
},
|
|
402
402
|
{
|
|
403
403
|
"path": "config/modules.neutral.json",
|
|
@@ -1352,7 +1352,7 @@
|
|
|
1352
1352
|
{
|
|
1353
1353
|
"path": "docs/adr/0174-government-terminology-ranks-not-roles.md",
|
|
1354
1354
|
"mode": "0000644",
|
|
1355
|
-
"sha256": "
|
|
1355
|
+
"sha256": "2b813490fe355bfdb783d9b28eee01b97bfb6d7d4343a3f59aeeb6fac185b79b"
|
|
1356
1356
|
},
|
|
1357
1357
|
{
|
|
1358
1358
|
"path": "docs/adr/0175-the-board-room-full-idea-ratification.md",
|
|
@@ -2902,7 +2902,7 @@
|
|
|
2902
2902
|
{
|
|
2903
2903
|
"path": "modules/government/config.js",
|
|
2904
2904
|
"mode": "0000644",
|
|
2905
|
-
"sha256": "
|
|
2905
|
+
"sha256": "dc4784b8d1f39c10c62ee935c6ff7d2d85db42f80479750e2ca2b8ca2877ebc6"
|
|
2906
2906
|
},
|
|
2907
2907
|
{
|
|
2908
2908
|
"path": "modules/government/db.js",
|
|
@@ -2997,7 +2997,7 @@
|
|
|
2997
2997
|
{
|
|
2998
2998
|
"path": "modules/government/routes/government.js",
|
|
2999
2999
|
"mode": "0000644",
|
|
3000
|
-
"sha256": "
|
|
3000
|
+
"sha256": "872f144bcd788e36f8b8bc87e5bd31f2389169ffa5d930e03d74ba24a0264f3b"
|
|
3001
3001
|
},
|
|
3002
3002
|
{
|
|
3003
3003
|
"path": "modules/government/routes/rank-sync.js",
|
|
@@ -4292,12 +4292,12 @@
|
|
|
4292
4292
|
{
|
|
4293
4293
|
"path": "package-lock.json",
|
|
4294
4294
|
"mode": "0000644",
|
|
4295
|
-
"sha256": "
|
|
4295
|
+
"sha256": "44f9bf150c24b1bed1d68bcd0a9661b4c659a32e5087a268a21b7b4370d7099a"
|
|
4296
4296
|
},
|
|
4297
4297
|
{
|
|
4298
4298
|
"path": "package.json",
|
|
4299
4299
|
"mode": "0000644",
|
|
4300
|
-
"sha256": "
|
|
4300
|
+
"sha256": "231179c4f9a0fffdd0ecdb48ceb161e99e7aab4e3058296762561d510f15281a"
|
|
4301
4301
|
},
|
|
4302
4302
|
{
|
|
4303
4303
|
"path": "public-docs/index.html",
|
|
@@ -4922,7 +4922,7 @@
|
|
|
4922
4922
|
{
|
|
4923
4923
|
"path": "scripts/gds/fitness.js",
|
|
4924
4924
|
"mode": "0000644",
|
|
4925
|
-
"sha256": "
|
|
4925
|
+
"sha256": "a255afac6eb4a2b8269e896032e8eabf10f104aed5a0f1e8d0a7348695625424"
|
|
4926
4926
|
},
|
|
4927
4927
|
{
|
|
4928
4928
|
"path": "scripts/gds/gate-review.js",
|
|
@@ -5742,7 +5742,7 @@
|
|
|
5742
5742
|
{
|
|
5743
5743
|
"path": "src/bongos/module-scope-map.js",
|
|
5744
5744
|
"mode": "0000644",
|
|
5745
|
-
"sha256": "
|
|
5745
|
+
"sha256": "277097c686b695688dee35d158c4bb8384b4a5a89c61f30c7bda7d9f4b477bee"
|
|
5746
5746
|
},
|
|
5747
5747
|
{
|
|
5748
5748
|
"path": "src/bongos/module-submissions.js",
|
|
@@ -5777,7 +5777,7 @@
|
|
|
5777
5777
|
{
|
|
5778
5778
|
"path": "src/bongos/route-rank-check.js",
|
|
5779
5779
|
"mode": "0000644",
|
|
5780
|
-
"sha256": "
|
|
5780
|
+
"sha256": "af5bdd9e362923afc1b78a741b5254147678b34cecb7d92f879387cc94a05631"
|
|
5781
5781
|
},
|
|
5782
5782
|
{
|
|
5783
5783
|
"path": "src/bongos/routes.js",
|
|
@@ -5912,7 +5912,7 @@
|
|
|
5912
5912
|
{
|
|
5913
5913
|
"path": "src/module-api.js",
|
|
5914
5914
|
"mode": "0000644",
|
|
5915
|
-
"sha256": "
|
|
5915
|
+
"sha256": "e999662a04f1981cbf01b2a9b5d18d33e71034311702fac6e5a6e0a0a209dcc0"
|
|
5916
5916
|
},
|
|
5917
5917
|
{
|
|
5918
5918
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6847,7 +6847,7 @@
|
|
|
6847
6847
|
{
|
|
6848
6848
|
"path": "tests/government_config.mjs",
|
|
6849
6849
|
"mode": "0000644",
|
|
6850
|
-
"sha256": "
|
|
6850
|
+
"sha256": "330372412c80d8d33698eba68a5c19e79daff17b0d820a9a797c883071a5641f"
|
|
6851
6851
|
},
|
|
6852
6852
|
{
|
|
6853
6853
|
"path": "tests/government_fold.mjs",
|
|
@@ -6907,7 +6907,7 @@
|
|
|
6907
6907
|
{
|
|
6908
6908
|
"path": "tests/government_routes.mjs",
|
|
6909
6909
|
"mode": "0000644",
|
|
6910
|
-
"sha256": "
|
|
6910
|
+
"sha256": "7f406d5296489d08804eae5b431d07244e1f2f3464513cccccb72e7b33253c14"
|
|
6911
6911
|
},
|
|
6912
6912
|
{
|
|
6913
6913
|
"path": "tests/government_seed.mjs",
|
|
@@ -17,5 +17,12 @@
|
|
|
17
17
|
"label": "Archon",
|
|
18
18
|
"description": "Seeded rank \u2014 top authority. Adds the admin + system (non-grantable) powers."
|
|
19
19
|
}
|
|
20
|
+
},
|
|
21
|
+
"_board_note": "The CONSTITUTION \u2014 the Board Room's rule set (BV1.R05 of goal 1000069, ADR 0175). Day one runs the real machinery configured to MONARCHY: membership 'rank:archon', pass_rule 'first_ratifier', no clock. That is a set of VALUES, never a separate code path \u2014 the same resolver, tally and window run whatever the numbers say, so flipping to a full board later changes configuration and not a branch that has never executed. RECOMMENDED once a project has a board worth sitting: all Archons, one vote each, pass_rule 'consent', window_minutes 90 \u2014 documented here, deliberately not the default. membership is the predicate 'rank:<key>'; it may name a CUSTOM rank, and WHO HOLDS that rank stays a DB row resolved per request (this file grants nobody anything). pass_rule is one of first_ratifier | consent. window_minutes is null for no clock, else whole minutes up to 7 days. Every unreadable field falls back to ITS OWN default, and the fallbacks are the STRICT end of both axes, so a malformed constitution can never resolve to a board that is wider or easier to pass than this one. To customise, copy to config/government.json and edit there \u2014 not by env var: a label is cosmetic, a constitution is not.",
|
|
22
|
+
"board": {
|
|
23
|
+
"membership": "rank:archon",
|
|
24
|
+
"pass_rule": "first_ratifier",
|
|
25
|
+
"window_minutes": null,
|
|
26
|
+
"close_early_on_full_turnout": true
|
|
20
27
|
}
|
|
21
28
|
}
|
|
@@ -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.
|
|
@@ -126,6 +126,78 @@ function sanitizeRole(key, raw) {
|
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// ── the constitution: who governs, how an item passes, how long a sitting runs ──
|
|
130
|
+
// BV1.R05 of goal 1000069 (ADR 0175). The Board Room's rule set is CONFIGURATION on
|
|
131
|
+
// the real machinery, never a separate code path: day one runs the same resolver,
|
|
132
|
+
// the same tally and the same window as a full consent board, with the numbers set
|
|
133
|
+
// to monarchy (owner decision 1). Flipping the switch later changes values, not a
|
|
134
|
+
// branch that has never executed.
|
|
135
|
+
//
|
|
136
|
+
// It lives here, beside the rank templates, for the same reason ADR 0175 put the
|
|
137
|
+
// board tables inside modules/government/: this is an AUTHORITY question, and
|
|
138
|
+
// authority is this module's territory.
|
|
139
|
+
//
|
|
140
|
+
// STILL NOT A SOURCE OF AUTHORITY TRUTH. Same wall as the rest of this file — the
|
|
141
|
+
// predicate says which RANK governs; who actually holds that rank stays a DB row,
|
|
142
|
+
// resolved per request by R06's membership resolver. Nothing here names a builder,
|
|
143
|
+
// and a config edit cannot make anyone a board member.
|
|
144
|
+
const BOARD_DEFAULTS = Object.freeze({
|
|
145
|
+
membership: 'rank:archon',
|
|
146
|
+
pass_rule: 'first_ratifier',
|
|
147
|
+
window_minutes: null, // null = no clock; monarchy does not sit
|
|
148
|
+
close_early_on_full_turnout: true,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// The pass rules that EXIST, not the ones a project should pick. `consent` is the
|
|
152
|
+
// documented recommendation once a project has a board worth sitting (all Archons,
|
|
153
|
+
// one vote each, consent, a 90-minute window); `first_ratifier` is the day-one
|
|
154
|
+
// default and what an unreadable value falls back to.
|
|
155
|
+
const PASS_RULES = Object.freeze(['first_ratifier', 'consent']);
|
|
156
|
+
|
|
157
|
+
// A membership predicate is `rank:<key>` — the only grammar v1 defines. It is NOT
|
|
158
|
+
// validated against catalog.RANK_ORDER on purpose: a project may govern by a CUSTOM
|
|
159
|
+
// rank, and custom ranks are DB rows this file cannot see (checking here would make
|
|
160
|
+
// config authoritative over a live rank list, which is exactly the inversion the
|
|
161
|
+
// header forbids). R06 resolves the predicate against the live ranks and yields an
|
|
162
|
+
// empty board if it names nothing — fail-closed there, shape-checked here.
|
|
163
|
+
const MEMBERSHIP_RE = /^rank:[a-z][a-z0-9_-]*$/;
|
|
164
|
+
|
|
165
|
+
// A sitting longer than a week is indistinguishable from no clock at all, and a
|
|
166
|
+
// zero/negative window would close every item the instant it opened. Both are far
|
|
167
|
+
// likelier to be a typo than an intent, so both fall back to the default.
|
|
168
|
+
const MAX_WINDOW_MINUTES = 7 * 24 * 60;
|
|
169
|
+
|
|
170
|
+
// Sanitize the board block. Every unreadable field falls back to ITS OWN default
|
|
171
|
+
// independently, so one typo costs one setting rather than the whole constitution.
|
|
172
|
+
//
|
|
173
|
+
// THE INVARIANT THAT MATTERS: a malformed constitution must never resolve to a board
|
|
174
|
+
// that is WIDER or EASIER to pass than the platform default. `rank:archon` is the
|
|
175
|
+
// narrowest membership this project can express, and `first_ratifier` demands an
|
|
176
|
+
// affirmative act by a member where `consent` lets an empty window pass unopposed
|
|
177
|
+
// (owner decision 6). So the fallbacks are the strict end of both axes — a broken
|
|
178
|
+
// config cannot quietly enfranchise anyone or lower the bar.
|
|
179
|
+
function sanitizeBoard(raw) {
|
|
180
|
+
const r = isObj(raw) ? raw : {};
|
|
181
|
+
const membership = typeof r.membership === 'string' && MEMBERSHIP_RE.test(r.membership)
|
|
182
|
+
? r.membership
|
|
183
|
+
: BOARD_DEFAULTS.membership;
|
|
184
|
+
const passRule = PASS_RULES.includes(r.pass_rule) ? r.pass_rule : BOARD_DEFAULTS.pass_rule;
|
|
185
|
+
// null is a MEANINGFUL value (no clock), so it is accepted rather than defaulted.
|
|
186
|
+
// Anything else must be a whole number of minutes inside the sane band.
|
|
187
|
+
const w = r.window_minutes;
|
|
188
|
+
const windowMinutes = w === null ? null
|
|
189
|
+
: (Number.isInteger(w) && w > 0 && w <= MAX_WINDOW_MINUTES ? w : BOARD_DEFAULTS.window_minutes);
|
|
190
|
+
const closeEarly = typeof r.close_early_on_full_turnout === 'boolean'
|
|
191
|
+
? r.close_early_on_full_turnout
|
|
192
|
+
: BOARD_DEFAULTS.close_early_on_full_turnout;
|
|
193
|
+
return Object.freeze({
|
|
194
|
+
membership,
|
|
195
|
+
pass_rule: passRule,
|
|
196
|
+
window_minutes: windowMinutes,
|
|
197
|
+
close_early_on_full_turnout: closeEarly,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
129
201
|
// Pure resolver — all inputs injected, so tests drive it with zero I/O (the
|
|
130
202
|
// resolveBranding pattern).
|
|
131
203
|
function resolveGovernmentConfig({ neutral, instance, env = process.env } = {}) {
|
|
@@ -146,7 +218,16 @@ function resolveGovernmentConfig({ neutral, instance, env = process.env } = {})
|
|
|
146
218
|
for (const key of catalog.RANK_ORDER) {
|
|
147
219
|
if (!ranks[key]) ranks[key] = sanitizeRole(key, {});
|
|
148
220
|
}
|
|
149
|
-
|
|
221
|
+
// The board block merges FIELD BY FIELD, neutral under instance, so a project that
|
|
222
|
+
// wants only a different window keeps the platform's answer for everything else
|
|
223
|
+
// rather than having to restate the whole constitution to change one number.
|
|
224
|
+
//
|
|
225
|
+
// Deliberately NOT env-overridable. applyLabelOverrides exists because a label is
|
|
226
|
+
// cosmetic; the constitution is not, and an env var that could change who governs
|
|
227
|
+
// would be a way to alter authority from outside the checkout AND outside the DB.
|
|
228
|
+
// Changing the constitution is a board act (owner decision 11), not a deploy flag.
|
|
229
|
+
const board = sanitizeBoard({ ...((neutral || {}).board || {}), ...((instance || {}).board || {}) });
|
|
230
|
+
return Object.freeze({ ranks: Object.freeze(ranks), board });
|
|
150
231
|
}
|
|
151
232
|
|
|
152
233
|
// READ, never require(): a bare require('../../config/X.json') of a boundary-excluded
|
|
@@ -179,6 +260,10 @@ module.exports = {
|
|
|
179
260
|
resolveGovernmentConfig,
|
|
180
261
|
loadGovernmentConfig,
|
|
181
262
|
grantableOnly,
|
|
263
|
+
sanitizeBoard,
|
|
264
|
+
BOARD_DEFAULTS,
|
|
265
|
+
PASS_RULES,
|
|
266
|
+
MAX_WINDOW_MINUTES,
|
|
182
267
|
NEUTRAL_PATH,
|
|
183
268
|
INSTANCE_PATH,
|
|
184
269
|
};
|
|
@@ -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
|
|
186
|
-
const ASSIGN_REASON = 'a seeded rank
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "1.19.222",
|
|
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.222",
|
|
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.222",
|
|
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/scripts/gds/fitness.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
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)
|
|
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
|
@@ -867,8 +867,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
867
867
|
// landed since 1.19.218 with no explicit bump. run 32094215476. (task 1002620)
|
|
868
868
|
// 1.19.220 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
869
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)
|
|
872
|
+
// 1.19.222 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
873
|
+
// landed since 1.19.221 with no explicit bump. run 32096034408. (task 1002620)
|
|
870
874
|
// ---------------------------------------------------------------------------
|
|
871
|
-
const CORE_VERSION = '1.19.
|
|
875
|
+
const CORE_VERSION = '1.19.222'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
872
876
|
|
|
873
877
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
874
878
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -18,7 +18,8 @@ const GOV = path.join(ROOT, 'modules', 'government');
|
|
|
18
18
|
const config = require(path.join(GOV, 'config.js'));
|
|
19
19
|
const catalog = require(path.join(GOV, 'catalog.js'));
|
|
20
20
|
const { envPrefix } = require(path.join(ROOT, 'src', 'instance-config'));
|
|
21
|
-
const { resolveGovernmentConfig, loadGovernmentConfig, grantableOnly,
|
|
21
|
+
const { resolveGovernmentConfig, loadGovernmentConfig, grantableOnly, sanitizeBoard,
|
|
22
|
+
BOARD_DEFAULTS, PASS_RULES, MAX_WINDOW_MINUTES, NEUTRAL_PATH, INSTANCE_PATH } = config;
|
|
22
23
|
|
|
23
24
|
const NEUTRAL = JSON.parse(fs.readFileSync(NEUTRAL_PATH, 'utf8'));
|
|
24
25
|
// No env by default — every precedence test injects exactly the env it is asserting on.
|
|
@@ -158,11 +159,20 @@ test('the neutral starter carries no host-specific identity (a vanilla instance
|
|
|
158
159
|
const { scanPromptIdentity } = require(path.join(ROOT, 'scripts', 'gds', 'fitness.js'));
|
|
159
160
|
const raw = fs.readFileSync(NEUTRAL_PATH, 'utf8');
|
|
160
161
|
assert.deepEqual(scanPromptIdentity('config/government.neutral.json', raw, '.json'), []);
|
|
161
|
-
// And it carries only templates — never a live per-builder assignment.
|
|
162
|
-
|
|
162
|
+
// And it carries only templates — never a live per-builder assignment. The key list
|
|
163
|
+
// is an ALLOWLIST on purpose: a new top-level key has to be justified here before it
|
|
164
|
+
// can ship, which is what stops a live grant or assignment arriving unnoticed.
|
|
165
|
+
// `board` joined it in R05 (goal 1000069) — it is the CONSTITUTION, a rule shape, and
|
|
166
|
+
// the assertion below pins that it names no builder. `_`-prefixed keys are prose notes.
|
|
167
|
+
assert.deepEqual(Object.keys(NEUTRAL).filter((k) => !k.startsWith('_')).sort(), ['board', 'ranks']);
|
|
163
168
|
for (const rank of Object.values(NEUTRAL.ranks)) {
|
|
164
169
|
assert.ok(!('permissions' in rank), 'the neutral seeded ranks carry labels only, never grants');
|
|
165
170
|
}
|
|
171
|
+
// The constitution says which RANK governs, never WHO. A builder id, login or handle
|
|
172
|
+
// here would make config authoritative over membership, which is R06's DB question.
|
|
173
|
+
assert.match(NEUTRAL.board.membership, /^rank:/, 'membership is a rank predicate, not a person');
|
|
174
|
+
assert.equal(JSON.stringify(NEUTRAL.board).includes('builder'), false,
|
|
175
|
+
'the neutral constitution must not reference any builder');
|
|
166
176
|
});
|
|
167
177
|
|
|
168
178
|
test('the two config paths are the branding-shaped neutral/instance pair under config/', () => {
|
|
@@ -185,3 +195,137 @@ test('the neutral starter publishes with the core (a packaged vanilla instance n
|
|
|
185
195
|
assert.equal(isPublishable('config/government.neutral.json'), true);
|
|
186
196
|
assert.equal(isPublishable('config/government.json'), false, "a host's filled-in pack is instance content");
|
|
187
197
|
});
|
|
198
|
+
|
|
199
|
+
// ── the constitution (BV1.R05 of goal 1000069, ADR 0175) ────────────────────────
|
|
200
|
+
//
|
|
201
|
+
// The board block is CONFIGURATION on the real machinery, not a second code path.
|
|
202
|
+
// These tests pin the two things that make that safe: the platform default really is
|
|
203
|
+
// monarchy, and a malformed constitution can never resolve to a board that is WIDER
|
|
204
|
+
// or EASIER to pass than that default. A config file is editable by anyone who can
|
|
205
|
+
// write the checkout — the same trust argument the permission tests above make.
|
|
206
|
+
|
|
207
|
+
test('day one is monarchy: the neutral starter ships the Archon-ratifies constitution', () => {
|
|
208
|
+
const out = resolveGovernmentConfig({ neutral: NEUTRAL, env: NO_ENV });
|
|
209
|
+
assert.deepEqual(out.board, {
|
|
210
|
+
membership: 'rank:archon',
|
|
211
|
+
pass_rule: 'first_ratifier',
|
|
212
|
+
window_minutes: null, // no clock — monarchy does not sit
|
|
213
|
+
close_early_on_full_turnout: true,
|
|
214
|
+
}, 'owner decision 1: the real machinery, configured to monarchy');
|
|
215
|
+
assert.deepEqual(NEUTRAL.board, out.board, 'the shipped file and the resolved value must agree');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('a project with no board block at all still gets the full default constitution', () => {
|
|
219
|
+
// The vanilla-boot case, and the upgrade case: an instance whose config predates
|
|
220
|
+
// R05 has no board key. It must resolve to monarchy, not to undefined fields that
|
|
221
|
+
// R06 would then have to guess at.
|
|
222
|
+
const out = resolveGovernmentConfig({ neutral: {}, instance: {}, env: NO_ENV });
|
|
223
|
+
assert.deepEqual(out.board, BOARD_DEFAULTS);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('the instance pack overrides the constitution FIELD BY FIELD', () => {
|
|
227
|
+
// Changing one number must not require restating the whole constitution.
|
|
228
|
+
const out = resolveGovernmentConfig({
|
|
229
|
+
neutral: NEUTRAL,
|
|
230
|
+
instance: { board: { pass_rule: 'consent', window_minutes: 90 } },
|
|
231
|
+
env: NO_ENV,
|
|
232
|
+
});
|
|
233
|
+
assert.equal(out.board.pass_rule, 'consent');
|
|
234
|
+
assert.equal(out.board.window_minutes, 90);
|
|
235
|
+
assert.equal(out.board.membership, 'rank:archon', 'untouched fields keep the neutral answer');
|
|
236
|
+
assert.equal(out.board.close_early_on_full_turnout, true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('the documented recommendation is expressible as pure configuration', () => {
|
|
240
|
+
// "all Archons, one vote each, consent, a 90-minute window" — the spec's recommended
|
|
241
|
+
// starting point. If this could not be reached by config alone, the "values not code
|
|
242
|
+
// paths" claim would be false.
|
|
243
|
+
const out = resolveGovernmentConfig({
|
|
244
|
+
neutral: NEUTRAL,
|
|
245
|
+
instance: { board: { membership: 'rank:archon', pass_rule: 'consent', window_minutes: 90 } },
|
|
246
|
+
env: NO_ENV,
|
|
247
|
+
});
|
|
248
|
+
assert.deepEqual(out.board, {
|
|
249
|
+
membership: 'rank:archon',
|
|
250
|
+
pass_rule: 'consent',
|
|
251
|
+
window_minutes: 90,
|
|
252
|
+
close_early_on_full_turnout: true,
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('a malformed constitution can never widen the franchise or lower the bar', () => {
|
|
257
|
+
// The invariant, enumerated over every junk shape a hand-edited file can take.
|
|
258
|
+
// rank:archon is the narrowest membership expressible; first_ratifier demands an
|
|
259
|
+
// affirmative act by a member where consent lets an empty window pass (decision 6).
|
|
260
|
+
const junk = [
|
|
261
|
+
undefined, null, 42, 'consent', [],
|
|
262
|
+
{ membership: 'everyone' },
|
|
263
|
+
{ membership: 'rank:' },
|
|
264
|
+
{ membership: 'RANK:ARCHON' }, // grammar is lowercase
|
|
265
|
+
{ membership: '' },
|
|
266
|
+
{ membership: { rank: 'archon' } },
|
|
267
|
+
{ pass_rule: 'unanimous' },
|
|
268
|
+
{ pass_rule: 'majority' },
|
|
269
|
+
{ pass_rule: null },
|
|
270
|
+
{ membership: 'rank:xenos; DROP TABLE government_ranks' },
|
|
271
|
+
];
|
|
272
|
+
for (const raw of junk) {
|
|
273
|
+
const b = sanitizeBoard(raw);
|
|
274
|
+
assert.equal(b.membership, BOARD_DEFAULTS.membership, `${JSON.stringify(raw)} must not widen membership`);
|
|
275
|
+
assert.equal(b.pass_rule, BOARD_DEFAULTS.pass_rule, `${JSON.stringify(raw)} must not lower the pass bar`);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('one unreadable field costs one setting, not the whole constitution', () => {
|
|
280
|
+
const b = sanitizeBoard({ membership: 'rank:metic', pass_rule: 'nonsense', window_minutes: 30 });
|
|
281
|
+
assert.equal(b.membership, 'rank:metic', 'a VALID field survives its neighbour being junk');
|
|
282
|
+
assert.equal(b.window_minutes, 30);
|
|
283
|
+
assert.equal(b.pass_rule, BOARD_DEFAULTS.pass_rule, 'only the broken field falls back');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('membership accepts a CUSTOM rank — config cannot be authoritative over the live rank list', () => {
|
|
287
|
+
// Deliberately NOT validated against catalog.RANK_ORDER: custom ranks are DB rows
|
|
288
|
+
// this file cannot see, and checking here would invert the config/DB boundary.
|
|
289
|
+
// R06 resolves the predicate against live ranks and yields an empty board if it
|
|
290
|
+
// names nothing, so the fail-closed step happens where the truth lives.
|
|
291
|
+
assert.equal(sanitizeBoard({ membership: 'rank:triage-helper' }).membership, 'rank:triage-helper');
|
|
292
|
+
assert.equal(sanitizeBoard({ membership: 'rank:board_member' }).membership, 'rank:board_member');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('window_minutes: null means no clock, and only a sane whole-minute band is accepted', () => {
|
|
296
|
+
assert.equal(sanitizeBoard({ window_minutes: null }).window_minutes, null, 'null is MEANINGFUL, not missing');
|
|
297
|
+
assert.equal(sanitizeBoard({ window_minutes: 90 }).window_minutes, 90);
|
|
298
|
+
assert.equal(sanitizeBoard({ window_minutes: MAX_WINDOW_MINUTES }).window_minutes, MAX_WINDOW_MINUTES);
|
|
299
|
+
for (const bad of [0, -5, 1.5, '90', NaN, Infinity, MAX_WINDOW_MINUTES + 1]) {
|
|
300
|
+
assert.equal(sanitizeBoard({ window_minutes: bad }).window_minutes, BOARD_DEFAULTS.window_minutes,
|
|
301
|
+
`window_minutes ${String(bad)} must fall back — a zero window closes on open, a huge one is not a sitting`);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('no env var can change the constitution', () => {
|
|
306
|
+
// applyLabelOverrides exists because a label is cosmetic. Authority is not: an env
|
|
307
|
+
// var that could move the board would be a way to change who governs from outside
|
|
308
|
+
// both the checkout and the DB. Changing it is a board act (owner decision 11).
|
|
309
|
+
const p = envPrefix();
|
|
310
|
+
const hostile = {
|
|
311
|
+
[`${p}_GOVERNMENT_BOARD_MEMBERSHIP`]: 'rank:xenos',
|
|
312
|
+
[`${p}_GOVERNMENT_BOARD_PASS_RULE`]: 'consent',
|
|
313
|
+
[`${p}_GOVERNMENT_BOARD`]: JSON.stringify({ membership: 'rank:xenos' }),
|
|
314
|
+
[`${p}_GOVERNMENT_RANK_LABELS`]: 'archon=Owner',
|
|
315
|
+
};
|
|
316
|
+
const out = resolveGovernmentConfig({ neutral: NEUTRAL, env: hostile });
|
|
317
|
+
assert.deepEqual(out.board, NEUTRAL.board, 'the constitution is untouched by env');
|
|
318
|
+
assert.equal(out.ranks.archon.label, 'Owner', 'while the LABEL override still works — proving env was read');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('the resolved constitution is frozen, like the rest of the config contract', () => {
|
|
322
|
+
const out = resolveGovernmentConfig({ neutral: NEUTRAL, env: NO_ENV });
|
|
323
|
+
assert.ok(Object.isFrozen(out.board));
|
|
324
|
+
assert.throws(() => { 'use strict'; out.board.membership = 'rank:xenos'; });
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('PASS_RULES is the closed set the rest of the goal codes against', () => {
|
|
328
|
+
assert.deepEqual([...PASS_RULES].sort(), ['consent', 'first_ratifier']);
|
|
329
|
+
assert.ok(Object.isFrozen(PASS_RULES));
|
|
330
|
+
assert.ok(PASS_RULES.includes(BOARD_DEFAULTS.pass_rule), 'the default must be a member of the set');
|
|
331
|
+
});
|
|
@@ -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
|
|
399
|
+
// ── the /governance/* alias is GONE (ADR 0174, task 1003084) ──────────────
|
|
400
400
|
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
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
|
-
//
|
|
414
|
-
//
|
|
415
|
-
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
|
|
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('
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
['
|
|
438
|
-
|
|
439
|
-
['
|
|
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,
|
|
442
|
-
const r = await hop(method,
|
|
443
|
-
assert.equal(r.status,
|
|
444
|
-
assert.equal(r.headers.get('location'),
|
|
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
|
|
450
|
-
|
|
451
|
-
|
|
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('
|
|
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
|
-
|
|
459
|
-
assert.ok(!/
|
|
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
|
});
|