@bongos/core 1.19.188 → 1.19.190
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 +19 -14
- package/docs/adr/0175-the-board-room-full-idea-ratification.md +243 -0
- package/docs/adr/README.md +1 -0
- package/modules/government/routes/government.js +34 -6
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +6 -4
- package/src/module-api.js +5 -1
- package/tests/government_routes.mjs +69 -2
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.190",
|
|
6
|
+
"core_contract": "1.19.190",
|
|
7
|
+
"source_commit": "99bc31cf263be3e12b7260e97be6d86b95d7d210",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-17T00:54:57.379Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 309,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
14
|
"functional_verbatim": 1282,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1612,
|
|
20
|
+
"tree_sha256": "2c7de5d40b0d85e5c39e5f55c8c0f340cdf45df52687019751d2ec3088194d14",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1354,10 +1354,15 @@
|
|
|
1354
1354
|
"mode": "0000644",
|
|
1355
1355
|
"sha256": "c645ce8202fa8e8faa59d3aa00a8167477b5223817b760a733b38864d7625c1d"
|
|
1356
1356
|
},
|
|
1357
|
+
{
|
|
1358
|
+
"path": "docs/adr/0175-the-board-room-full-idea-ratification.md",
|
|
1359
|
+
"mode": "0000644",
|
|
1360
|
+
"sha256": "aaff11b955bcbacaebf2475d01034dce1c47c70c22534195aebbf90767b025f6"
|
|
1361
|
+
},
|
|
1357
1362
|
{
|
|
1358
1363
|
"path": "docs/adr/README.md",
|
|
1359
1364
|
"mode": "0000644",
|
|
1360
|
-
"sha256": "
|
|
1365
|
+
"sha256": "6748701da2559d6019116eb00e9aeeaad947e08d17f9c9cf77e8f45c19e2c69f"
|
|
1361
1366
|
},
|
|
1362
1367
|
{
|
|
1363
1368
|
"path": "docs/api-reference.md",
|
|
@@ -2972,7 +2977,7 @@
|
|
|
2972
2977
|
{
|
|
2973
2978
|
"path": "modules/government/routes/government.js",
|
|
2974
2979
|
"mode": "0000644",
|
|
2975
|
-
"sha256": "
|
|
2980
|
+
"sha256": "c229aec4f02fe970f4a3f798d370eef9b080cd0837f906b574362abda57107ad"
|
|
2976
2981
|
},
|
|
2977
2982
|
{
|
|
2978
2983
|
"path": "modules/government/routes/rank-sync.js",
|
|
@@ -4257,12 +4262,12 @@
|
|
|
4257
4262
|
{
|
|
4258
4263
|
"path": "package-lock.json",
|
|
4259
4264
|
"mode": "0000644",
|
|
4260
|
-
"sha256": "
|
|
4265
|
+
"sha256": "b63d0bd77af72944106d5b482777b258187917342097cae72ad09078747b411c"
|
|
4261
4266
|
},
|
|
4262
4267
|
{
|
|
4263
4268
|
"path": "package.json",
|
|
4264
4269
|
"mode": "0000644",
|
|
4265
|
-
"sha256": "
|
|
4270
|
+
"sha256": "b5675fea41d22048c57fe51577a36330597e91b1864051a1537aa37f18e1bc16"
|
|
4266
4271
|
},
|
|
4267
4272
|
{
|
|
4268
4273
|
"path": "public-docs/index.html",
|
|
@@ -4887,7 +4892,7 @@
|
|
|
4887
4892
|
{
|
|
4888
4893
|
"path": "scripts/gds/fitness.js",
|
|
4889
4894
|
"mode": "0000644",
|
|
4890
|
-
"sha256": "
|
|
4895
|
+
"sha256": "5cc94859033c924884efa60f50f8a1f7810a85382c42d785d62b804181a195f5"
|
|
4891
4896
|
},
|
|
4892
4897
|
{
|
|
4893
4898
|
"path": "scripts/gds/gate-review.js",
|
|
@@ -5867,7 +5872,7 @@
|
|
|
5867
5872
|
{
|
|
5868
5873
|
"path": "src/module-api.js",
|
|
5869
5874
|
"mode": "0000644",
|
|
5870
|
-
"sha256": "
|
|
5875
|
+
"sha256": "3b8d188039825cd1f1a64e7d33246152e293e452df2f2bee8a586acfc22ccfc4"
|
|
5871
5876
|
},
|
|
5872
5877
|
{
|
|
5873
5878
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6847,7 +6852,7 @@
|
|
|
6847
6852
|
{
|
|
6848
6853
|
"path": "tests/government_routes.mjs",
|
|
6849
6854
|
"mode": "0000644",
|
|
6850
|
-
"sha256": "
|
|
6855
|
+
"sha256": "3b2e0ca134737992590132f089d5f93b479ad6cec83a5d2a932412b3edaf4547"
|
|
6851
6856
|
},
|
|
6852
6857
|
{
|
|
6853
6858
|
"path": "tests/government_seed.mjs",
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# 0175 — The Board Room: Full Idea ratification as the first upward-pointing gate
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-08-17
|
|
5
|
+
- **Task:** [#1003014](https://cloudbongos.com/builders#/task/1003014) (R01 of goal 1000069 — the Board Room)
|
|
6
|
+
- **Related:** ADR 0151 (permissions-as-atoms), 0157 (Archon is rank and identity only), 0162 (verdicts advisory, never self-executing), 0172-per-craft (the ideator credit lane — stream 3), 0174 (government, not governance; "role" means a craft), 0037 (Discord channel layout), 0016 (trust boundary), 0086 (goals + scope walls)
|
|
7
|
+
- **Spec:** [`docs/specs/bongos-v1-board-room.md`](../specs/bongos-v1-board-room.md) — the planning session that produced this decision, with the full interview record
|
|
8
|
+
- **Source ideas:** 1000738 (the Board Room), 1000754 (scoping brief)
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
**Every gate in this system points down the rank ladder.** Rank floors, protected paths,
|
|
13
|
+
the grader, Archon approvals — each one constrains someone below the person who set it.
|
|
14
|
+
Nothing points up. An Archon can ship whatever they want, and on 2026-08-16 the owner named
|
|
15
|
+
that as a loophole they can walk through at will:
|
|
16
|
+
|
|
17
|
+
> *"I am a little unsure about just implementing without team buy-in… Otherwise I can just
|
|
18
|
+
> ship and build as I want, finding loopholes."*
|
|
19
|
+
|
|
20
|
+
The most senior person on the project asked for a structure that can slow **them** down. That
|
|
21
|
+
framing is the specification, and it is why the decision below is about *legitimacy* rather
|
|
22
|
+
than about permissions — the permission system (ADR 0151) already works, and adding a rank
|
|
23
|
+
floor would only point the gate downward again.
|
|
24
|
+
|
|
25
|
+
Three constraints shaped what could actually be built.
|
|
26
|
+
|
|
27
|
+
**1. The ADR 0162 discipline.** Verdicts are advisory and never self-executing. A gate that
|
|
28
|
+
blocks a seeding until a quorum is reached would violate that; a gate that merely records an
|
|
29
|
+
opinion would be theatre. The resolution turned out not to be a compromise between them —
|
|
30
|
+
see Decision 1.
|
|
31
|
+
|
|
32
|
+
**2. Franchise cannot be role-derived.** ADR 0174 §5 freed the word "role" for the three
|
|
33
|
+
crafts (Artist, Ideator, Builder), which makes a craft-scoped voting chamber superficially
|
|
34
|
+
attractive: an Ideator chamber for ideas, a Builder chamber for seedings. It is not viable.
|
|
35
|
+
`PATCH /me/disciplines` is deliberately ungated — *"a builder owns their own profile"*
|
|
36
|
+
(`src/bongos/routes/me.js`) — so self-selecting into a chamber would be a one-request
|
|
37
|
+
privilege escalation. Role also gates nothing anywhere in the codebase today; authority runs
|
|
38
|
+
rank → rank-role → permission, end to end. **Franchise therefore derives from rank**, and the
|
|
39
|
+
role-chamber model is rejected.
|
|
40
|
+
|
|
41
|
+
**3. The scale is the "circle" rung.** The governance synthesis this session worked from
|
|
42
|
+
(*The Six Variables of Governance*, Verro Labs, Aug 2026) argues that formality is adopted
|
|
43
|
+
only where informal coordination breaks, and that the thresholds are set by human cognitive
|
|
44
|
+
constants: an executive differentiates around 9 people, written law separates around 50,
|
|
45
|
+
adjudication separates around 150. This project sits at 3–8. A seven-preset Charter Builder
|
|
46
|
+
is a 150-rung constitution, and shipping one now would be building the wrong rung.
|
|
47
|
+
|
|
48
|
+
## Decision
|
|
49
|
+
|
|
50
|
+
### 1. The board's subjects are exactly two, and both are rare by construction
|
|
51
|
+
|
|
52
|
+
The board votes on a **Full Idea** (`modules/ideas`, goal 1000062 — the five-field graded
|
|
53
|
+
deliverable), and on a **change to the constitution itself**. Nothing else. Claims, seedings,
|
|
54
|
+
promotes, ships and grades behave exactly as they do today; Quick Ideas and Sparks keep
|
|
55
|
+
promoting straight to a task, untouched.
|
|
56
|
+
|
|
57
|
+
Idea 1000738 also wanted votes on task-set seedings and *"anything a member puts up"*. The
|
|
58
|
+
owner narrowed it — *"only Full Detail Ideas (E.g. new goals) should need ratification,
|
|
59
|
+
everything else should function as is"* — and then re-widened it by exactly one subject, so
|
|
60
|
+
that the rule constraining the owner cannot be weakened by the owner alone.
|
|
61
|
+
|
|
62
|
+
**This is what dissolves the ADR 0162 tension rather than compromising on it.** A board vote
|
|
63
|
+
gates no build path, so nothing self-executes because there is nothing downstream to execute.
|
|
64
|
+
Ratification confers legitimacy and triggers a credit award; it does **not** auto-create the
|
|
65
|
+
goal (goal 1000062 already settled that a Full Idea is not auto-converted — the conversion
|
|
66
|
+
recommendation stays a human step).
|
|
67
|
+
|
|
68
|
+
Board items carry a `subject_type` (`full_idea` | `constitutional_amendment`) **from day one**.
|
|
69
|
+
A third subject should be a row, not a migration.
|
|
70
|
+
|
|
71
|
+
### 2. A vote is a timed sitting, not a quorum
|
|
72
|
+
|
|
73
|
+
Asking *"how many votes must we collect"* produces deadlock — Linz's problem, no
|
|
74
|
+
constitutional exit. We invert it: members are notified that a vote is wanted within a window,
|
|
75
|
+
and **an unvoted window nulls that member's vote** — they drop out of the denominator. They
|
|
76
|
+
are never counted as a no.
|
|
77
|
+
|
|
78
|
+
**The clock is the deadlock exit.** This is why no shotgun buy-sell or fork-with-treasury-split
|
|
79
|
+
clause is needed at this scale: absence cannot stall anything, so there is no state for such a
|
|
80
|
+
clause to resolve.
|
|
81
|
+
|
|
82
|
+
Default window: **90 minutes**, closing early the moment every member has voted, so latency is
|
|
83
|
+
`min(full turnout, window)`. Stored in minutes and set per project.
|
|
84
|
+
|
|
85
|
+
Stated plainly because it is the owner's explicit intent, not an accident: **a short window
|
|
86
|
+
means whoever is awake governs.** Standing accrues to those actively governing. The window
|
|
87
|
+
length is a governance lever, not a UX detail.
|
|
88
|
+
|
|
89
|
+
### 3. Consent, not majority — and a zero-vote close passes
|
|
90
|
+
|
|
91
|
+
Silence is consent. A **single reasoned objection** returns the idea to its author. A window
|
|
92
|
+
that closes with **no votes at all passes** — the idea already cleared the completeness gate to
|
|
93
|
+
reach the board, and absence never blocks.
|
|
94
|
+
|
|
95
|
+
This makes both halves of the owner's intent true at once: pace is never obstructed, and only
|
|
96
|
+
an active participant can change an outcome. It also makes the feedback mechanism in Decision 4
|
|
97
|
+
load-bearing rather than decorative — an objection must carry a reason and a section flag to
|
|
98
|
+
count at all.
|
|
99
|
+
|
|
100
|
+
Trade-off recorded deliberately: with five members, 4-yes/1-no returns the idea. That is real
|
|
101
|
+
minority-block power. The cost of a wrong objection is one revision cycle, not a kill.
|
|
102
|
+
|
|
103
|
+
### 4. A rejection carries a reason and one of the five sections — and that forces the surface
|
|
104
|
+
|
|
105
|
+
A Full Idea that fails, **whether on completeness grade or on the vote**, returns to its
|
|
106
|
+
originator rather than dying or stalling. Every rejection carries free text **plus exactly one
|
|
107
|
+
of the five Full Idea sections** flagged as the cause. A sub-bar grade flags its weakest
|
|
108
|
+
section automatically, using the per-field scores `completeness.js` already computes — which
|
|
109
|
+
also means a resubmission can be checked against the flagged section.
|
|
110
|
+
|
|
111
|
+
**This requirement is what makes the hall the only possible voting surface.** A Discord
|
|
112
|
+
reaction can express yes or no and nothing else, so reaction-voting would make every
|
|
113
|
+
Discord-cast rejection feedback-free — precisely what this decision exists to prevent. The hall
|
|
114
|
+
is not the *richer* surface; it is the only one that satisfies the rule.
|
|
115
|
+
|
|
116
|
+
Resubmission is unbounded in v1 — the grade gate already bounds spam — and every past rejection
|
|
117
|
+
stays visible on the item's history.
|
|
118
|
+
|
|
119
|
+
### 5. The completeness grade is the floor; the vote is the merit
|
|
120
|
+
|
|
121
|
+
A Full Idea must clear the completeness pass bar (60, ADR 0172-per-craft) before a window can
|
|
122
|
+
open, and a window opens **automatically** on that pass.
|
|
123
|
+
|
|
124
|
+
Two gates doing two jobs: the deterministic scorer asks *is the thought fully expressed*, the
|
|
125
|
+
board asks *is it worth doing*. Board time is never spent on what a free, reproducible check
|
|
126
|
+
could bounce.
|
|
127
|
+
|
|
128
|
+
Consequence to design for, and a direct result of automatic opening at a 90-minute default:
|
|
129
|
+
**concurrent windows are normal.**
|
|
130
|
+
|
|
131
|
+
### 6. Who governs is configuration, and day one is monarchy
|
|
132
|
+
|
|
133
|
+
The membership predicate, the pass rule, and the window live in `config/government.json`,
|
|
134
|
+
resolved **per request and uncached** so the ADR 0016 instant-demotion property survives.
|
|
135
|
+
|
|
136
|
+
Day one this instance runs `membership: rank:archon`, `pass_rule: first_ratifier`, no clock —
|
|
137
|
+
**monarchy, expressed as configuration on the real machinery rather than as a separate code
|
|
138
|
+
path.** The board path therefore executes from the first ratification, and flipping the switch
|
|
139
|
+
later changes numbers, not a code path that has never run. The documented recommended starting
|
|
140
|
+
point for a new project is: all Archons, one vote each, consent, 90-minute window.
|
|
141
|
+
|
|
142
|
+
### 7. Amendments are decided under whatever rule the current constitution names
|
|
143
|
+
|
|
144
|
+
Changing the constitution is a board subject (Decision 1). It is decided by **whatever rule is
|
|
145
|
+
currently in force** — so under monarchy, "the board must ratify" resolves to "the Archon
|
|
146
|
+
ratifies", and flipping *out* of monarchy is a legal act of the monarchy. Every change after
|
|
147
|
+
that belongs to the board.
|
|
148
|
+
|
|
149
|
+
**No bootstrap special case is required**, and none should be added. This is how a
|
|
150
|
+
constitution's own amendment procedure normally works, and inventing an exception here would
|
|
151
|
+
create exactly the loophole this ADR exists to close.
|
|
152
|
+
|
|
153
|
+
### 8. Discord is an outbound notification mirror that casts nothing
|
|
154
|
+
|
|
155
|
+
On window open, the board channel receives an announcement mentioning the members whose vote is
|
|
156
|
+
wanted, carrying a deep link to that item in the hall. State changes — closed, outcome — are
|
|
157
|
+
mirrored. **Discord reads nothing inbound and casts no vote.**
|
|
158
|
+
|
|
159
|
+
The mention *is* the "vote within X" notice, and Discord's own mobile app *is* the mobile push
|
|
160
|
+
— **no mobile surface needs to be built.** The bot has no DM capability today, and a channel is
|
|
161
|
+
the better shape regardless: it is a room, it is auditable, and deliberation sits beside the
|
|
162
|
+
vote. Per-rank view access follows ADR 0037.
|
|
163
|
+
|
|
164
|
+
`modules/discord/discord-approvals.js` (task 1100) is the precedent for **how to broadcast and
|
|
165
|
+
how to re-check a live rank** — *"the bot asserts only who reacted; the GDS decides what they
|
|
166
|
+
may do"* — and explicitly **not** for how to vote, per Decision 4.
|
|
167
|
+
|
|
168
|
+
### 9. Open ballot
|
|
169
|
+
|
|
170
|
+
Member, direction, timestamp, and objection reason are visible to all members. A board whose
|
|
171
|
+
votes are invisible cannot constrain anyone, which is the entire purpose of an upward-pointing
|
|
172
|
+
gate. It also makes the vote-karma in Decision 10 auditable.
|
|
173
|
+
|
|
174
|
+
### 10. Voting earns karma
|
|
175
|
+
|
|
176
|
+
A small award per vote cast in-window (proposed `+1`); the Full Idea's author earns karma on
|
|
177
|
+
ratification, mirroring `KARMA_RATIFY_DELTA` (`+3` — tuned down from `+5` in V3.R28 / task 248;
|
|
178
|
+
do not copy the stale `+5` still printed in `planning-session/SKILL.md` and `openapi.json`).
|
|
179
|
+
|
|
180
|
+
**Decided against the recommendation, with the risk accepted knowingly.** Under consent a *yes*
|
|
181
|
+
is one click while a *no* requires a written reason and a section flag, so equal pay for unequal
|
|
182
|
+
effort tilts toward assent. Accepted because **the item volume is too low to farm** — the
|
|
183
|
+
board's only subjects are Full Ideas and constitutional amendments, both rare by construction.
|
|
184
|
+
Revisit if item volume grows; the award is one constant.
|
|
185
|
+
|
|
186
|
+
### 11. An author may never vote on their own item
|
|
187
|
+
|
|
188
|
+
Server-side, on the `peer_votes` anti-self precedent (migration 058, `UNIQUE(voter, target)`).
|
|
189
|
+
This is the bright line that keeps ratification from being ceremonial, and it is the one rule
|
|
190
|
+
that survived from a much larger scrutiny mechanism the owner cut.
|
|
191
|
+
|
|
192
|
+
## Consequences
|
|
193
|
+
|
|
194
|
+
**Goal 1000067 (IC5, task 1002999) is unblocked by the pass event, and by nothing else.** ADR
|
|
195
|
+
0172-per-craft is explicit that stream 3 *"consumes the Board Room's pass event; invents no
|
|
196
|
+
parallel vote."* There is exactly one vote surface in this system. The event fires once per
|
|
197
|
+
ratified idea and must be idempotent across replays, re-closes, and a resubmission that passes
|
|
198
|
+
again. `economy` is deliberately outside this goal's scope: we emit, 1000067 pays.
|
|
199
|
+
|
|
200
|
+
**The permission keys must land with their grant migration in the same deploy as the code
|
|
201
|
+
constant.** `government_rank_permissions.permission_key` stores the key as text while
|
|
202
|
+
`catalog.js` holds it as a constant; if the rows change while old code is serving, the check
|
|
203
|
+
fails closed — on the very tab an owner would use to investigate (ADR 0174 §Consequences). The
|
|
204
|
+
grant migration is load-bearing, not bookkeeping: `governance_002` is run-once, so an
|
|
205
|
+
already-migrated database never learns a key added only in code.
|
|
206
|
+
|
|
207
|
+
**Board state lives in `modules/government/`, which is protected.** All board work is therefore
|
|
208
|
+
Archon-gated. This was chosen over `modules/ideas/` (subject-ownership) on the grounds that the
|
|
209
|
+
board is an authority mechanism. The rename that created that folder landed on `origin/main` in
|
|
210
|
+
`14dcedd`; note that task 1003005 reads `abandoned` but was *absorbed* into 1003004 and shipped
|
|
211
|
+
there — read the note, not the status.
|
|
212
|
+
|
|
213
|
+
**Goal 1000069 is walled to the `governance` alias, not `government`.** Prod served core
|
|
214
|
+
1.19.179 while the new scope key landed in 1.19.180, so the live server rejected the new key at
|
|
215
|
+
goal-creation time. The alias carries identical globs. **Re-point `scope_modules` once 1.19.180
|
|
216
|
+
is deployed.**
|
|
217
|
+
|
|
218
|
+
**The charter library ships without the Charter Builder.** The Government page presents all
|
|
219
|
+
seven constitutions, the three trajectories and the ladder thresholds, marking which are
|
|
220
|
+
selectable today. This is the reference half only — the configuration machinery is deliberately
|
|
221
|
+
absent, per the scale argument in Context 3. At two builders this is the half that earns its
|
|
222
|
+
keep, and its stated purpose is getting a team up to speed before deciding when to flip the
|
|
223
|
+
switch.
|
|
224
|
+
|
|
225
|
+
**The agora auto-link from idea 1000738 is deferred, not dropped.** Linking discussion *in*
|
|
226
|
+
would mean reading Discord inbound, contradicting Decision 8. Once goal 1000064 makes the Agora
|
|
227
|
+
a hall surface, the link is hall→hall and needs no adapter. Filed as idea 1000757.
|
|
228
|
+
|
|
229
|
+
**Rejected: quadratic vote weighting and the attribution-economy knobs.** The synthesis doc's
|
|
230
|
+
Part V spends its length on meter-weighted franchise — the Solon problem, compressing whales
|
|
231
|
+
with √credits, the drift from timocracy toward isonomia. None of it applies here, because v1's
|
|
232
|
+
franchise is a flat membership rule and is not weighted by credits at all. Hop decay (γ), time
|
|
233
|
+
half-life (τ), the 30% stack cap and vote coupling belong to the ideator credit lane, not to
|
|
234
|
+
this decision.
|
|
235
|
+
|
|
236
|
+
**Rejected: eternity clauses in v1.** Fork rights with an asset-split rule, meter transparency,
|
|
237
|
+
and a dead-man's switch on founder inactivity are real and worth having — Germany's Basic Law
|
|
238
|
+
Art. 79(3) and MolochDAO's ragequit are the precedents — but nothing in v1 depends on them, and
|
|
239
|
+
an unamendable floor written before the amendable part has ever run would be guesswork.
|
|
240
|
+
|
|
241
|
+
**Rejected: any legal-entity integration.** Equity issuance, Rev. Proc. 93-27 profit interests,
|
|
242
|
+
Subchapter T patronage, and the Howey posture are a later version's problem. v1 credits stay
|
|
243
|
+
reputation- and governance-weighted only, which is also what keeps the Howey question asleep.
|
package/docs/adr/README.md
CHANGED
|
@@ -248,5 +248,6 @@ This keeps the decision history honest and traceable.
|
|
|
248
248
|
| 0171 | [**Profile ↔ cross-project rollup: the join runs in-process on the hub; consent is fail-closed account state; the hide flag lives on the account, enforced in the read path** ([task 1002277](https://cloudbongos.com/builders#/task/1002277), R01 of goal 1000045). The C1 cross-project profile reads the platform-identity rollup through the module's **declared-but-never-registered `platform-identity.hub` port**: the module registers a real provider and core `GET /builders/:id/profile` `resolveOptional`s it beside `reward` — zero network, `null` (clean degrade) on every non-hub instance. Federated instances **link out** instead of inlining (amends profile-page spec Decision 1: an instance-side copy is either stale-on-hide or a per-view hub call). Consent: `profile_state` on `platform_identity_accounts`, **fail-closed** — `active` iff touched by a hub-session `/sso/authorize` (device-poll/check-in create `provisional`; backfill = all provisional, lazy upgrade; no core-`builders` join — type-unsafe vs `system:*` ids and a module-migration boundary breach), so the P03 owner decision can only ever widen, never claw back. The C2 hide flag is `hide_stats` on the account (NOT per-instance `behavior_prefs`), enforced **inside** the module's read functions — `listScoutingRollup` itself gains the `active AND NOT hide_stats` predicate so the live Metic-floor `/people`+`/scouting` surfaces honor it by construction. Hub profile responses go `no-store` (hide must beat the 60s cache). Publishing is gated on hardening the self-reported rollup (membership-row requirement, rank-vocab validation, disciplines caps, check-in→rollup sequencing). Found + filed: the `LATERAL unnest` fan-out inflating rollup sums (task 1002911), the unreserved `u` slug (task 1002912), the missing platform T&C artifact (task 1002913), criterion amendments (task 1002914). Rejected: instance→hub per-view pull (always-on dependency), hub→instance mirroring (stale-on-hide), storing the flag in builder-settings, backfilling `active` from hub `builders` rows.](<redacted>.md) | platform identity / profiles / privacy |
|
|
249
249
|
| 0170 | [**The featured projection tells time honestly: `created_at` in, `updated_at` out, owner from the project's own archon** ([task 1002778](https://cloudbongos.com/builders#/task/1002778), goal 1000061). The hub card's metadata pass added `created_at` to the public `/projects/featured` projection, labelled **Listed** — it is the moment the hub catalogued the row, never when the project began, and the C6 card test bans the word "Created". Two recorded deviations: (1) the task brief said `updated_at` was ready to add, but the column is bumped by every client re-registration AND the Archon's feature toggle, so as a public "last updated" it would report curation noise as project activity — it may sort, never project, and the test pins the SELECT list itself (`SELECT *` banned: the route is CORS-open rank:public and a star would leak `owner_github_id`/`status`). (2) The owner fact reads the project's OWN leaderboard archon row (login + avatar, type-checked and capped) from the payload project-stats already fetches — zero extra requests, the 2-fetch pin intact, the advisory `owner_github_id` column still unread per the design contract. Multi-archon: top-credits archon is named; a fallen-off-the-page archon renders as no owner shown, never a wrong one. Rejected: projecting `updated_at` as asked (it lies), reading `owner_github_id` (advisory, contract-banned), a third per-origin fetch for activity recency (moves the test pin for a fact nobody asked for yet).](<redacted>.md) | platform identity / public surface |
|
|
250
250
|
| 0174 | [**Governance is renamed government, its positions are ranks, and "role" is reserved for the three crafts** ([task 1003002](https://cloudbongos.com/builders#/task/1003002), R01 of goal 1000068). Owner directive, 2026-08-16. The word **role** named two unrelated things: the permission system's ladder tiers (`governance_roles`, the "rank-roles") and the three crafts (`tasks.discipline` = engineer/artist/ideator) — and goal 1000066 was actively claiming it for the crafts (`three-roles-only`, `users-hold-roles`), putting the two on a collision course. Resolved at full depth rather than as a label pass, because a labels-only fix leaves the UI and the code disagreeing, which is the condition that produced the collision. `modules/governance/` → `modules/government/`; the kernel module key, the config files, `/api/bongos/governance/*` → `/government/*`, and the permission keys `governance.manage` → `government.manage` (~50 sites) + `page.view.governance` → `page.view.government`. **The entity IS the rank — no second noun**: `governance_roles` → `government_ranks`, `role_key` → `rank_key`, `rank-roles.js` → `ranks.js`; "position" stays prose-only, since introducing it as a code term would recreate the qualifier problem "rank-role" already demonstrates. The cross-module kernel port `governance.roleSync` → `government.rankSync` must land atomically across its provider and both consumers (`modules/lifecycle/db.js`, `src/bongos/db.js`) because it is a transaction participant on the rank-write path. Hand-composed bundles stay in the one table as **custom ranks** — the owner's call over splitting them into a separate "appointments" concept, with the objection recorded (a custom bundle is not a ladder position, and ADR 0172 §4 already treats the two kinds as different). Enforced by a `fitness.js` rule, allowlist shipped with reasons. **Discord's server roles keep the word** — that noun belongs to Discord's API. Amends ADR 0151 + ADR 0172 (the editable-rank-roles one) in **vocabulary only**; ladder slugs, `builders.rank`, `requireRank`, and every floor/grant/assignment semantic are untouched. Rejected: renaming the ladder slugs (turns a bounded rename into a trust-boundary migration), a permanent compat shim (both vocabularies stay correct forever).](<redacted>.md) | government / permissions |
|
|
251
|
+
| 0175 | [**The Board Room: Full Idea ratification is the first upward-pointing gate** ([task 1003014](https://cloudbongos.com/builders#/task/1003014), R01 of goal 1000069). Every existing gate points DOWN the rank ladder; the owner asked for one that could slow THEM down — *"otherwise I can just ship and build as I want, finding loopholes."* The board's subjects are exactly **two**, both rare by construction: a **Full Idea** and a **change to the constitution itself** (`subject_type` from day one). Because the vote gates no build path, the **ADR 0162 tension dissolves rather than being compromised** — nothing self-executes because there is nothing downstream to execute; ratification confers legitimacy and does NOT auto-create the goal. A vote is a **timed sitting, not a quorum**: absent members are *nulled* (dropped from the denominator, never counted as a no), so the clock is the deadlock exit and no shotgun/fork-with-treasury clause is needed at this scale — default 90 minutes, closing early on full turnout, and yes, a short window means whoever is awake governs (the owner's explicit intent). **Consent, not majority**: silence is consent, one reasoned objection returns the idea, and a zero-vote close PASSES. A rejection must carry free text **plus one of the five Full Idea sections** — which is what **forces the hall to be the voting surface**, since a Discord reaction could only ever be feedback-free; Discord is therefore an outbound notification mirror that casts nothing, and its own mobile app is the mobile push (no mobile surface is built). The completeness grade is the **floor** (a window opens automatically on pass), the vote is the **merit**; concurrent windows are normal. Who governs is **configuration** — day one is monarchy expressed as config on the real machinery, never a separate code path — and **amendments are decided under whatever rule is currently in force**, so flipping out of monarchy is a legal monarchy act and no bootstrap exception is needed. Open ballot; an author may never vote on their own item. Franchise derives from **rank, not role**, and that is forced: `PATCH /me/disciplines` is deliberately ungated, so a craft-scoped chamber would be one-request privilege escalation. Unblocks goal 1000067 / IC5, which consumes the pass event and *"invents no parallel vote"*. Rejected: quadratic vote weighting and the attribution-economy knobs (γ, τ, the 30% stack cap — v1's franchise is not credit-weighted at all), eternity clauses, and any legal-entity integration.](<redacted>.md) | government / ideas / governance |
|
|
251
252
|
|
|
252
253
|
> ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
|
|
@@ -185,11 +185,11 @@ module.exports = function buildGovernmentRouter() {
|
|
|
185
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
186
|
const ASSIGN_REASON = 'a seeded rank-rank is assigned by rank (R95b sync); change the builder\'s rank instead';
|
|
187
187
|
|
|
188
|
-
// ── one-release
|
|
188
|
+
// ── the one-release LEGACY path aliases: /governance/* -> /government/* ────
|
|
189
189
|
// ADR 0174 committed to keeping the pre-rename paths answering for exactly one
|
|
190
190
|
// release. The case this actually protects is narrow but real: a browser that
|
|
191
191
|
// still has the OLD hall page open when the instance upgrades will keep calling
|
|
192
|
-
// /governance/* against the new server. Everything else that
|
|
192
|
+
// the legacy /governance/* paths against the new server. Everything else that
|
|
193
193
|
// paths (the hall bundle, the generated client) ships INSIDE the same core
|
|
194
194
|
// package and moves in lockstep.
|
|
195
195
|
//
|
|
@@ -201,11 +201,39 @@ module.exports = function buildGovernmentRouter() {
|
|
|
201
201
|
// REMOVE THIS BLOCK one release after the rename ships (goal 1000068 follow-up).
|
|
202
202
|
// The `alias` counter in the log line is how we tell whether anything still
|
|
203
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.
|
|
204
211
|
const LEGACY_PATH_PREFIX = '/governance';
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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}`;
|
|
209
237
|
console.warn(`[government] legacy path hit: ${req.method} ${req.originalUrl} -> ${target} (ADR 0174 alias; remove one release after the rename)`);
|
|
210
238
|
return res.redirect(308, target);
|
|
211
239
|
});
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.190",
|
|
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.190",
|
|
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.190",
|
|
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
|
@@ -1690,11 +1690,13 @@ const VOCAB_ALLOW = [
|
|
|
1690
1690
|
},
|
|
1691
1691
|
{
|
|
1692
1692
|
file: 'modules/government/routes/government.js',
|
|
1693
|
-
match: '
|
|
1693
|
+
match: 'legacy',
|
|
1694
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.
|
|
1696
|
-
+ '
|
|
1697
|
-
+ '
|
|
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.',
|
|
1698
1700
|
},
|
|
1699
1701
|
{
|
|
1700
1702
|
file: 'modules/government/module.json',
|
package/src/module-api.js
CHANGED
|
@@ -803,8 +803,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
803
803
|
// landed since 1.19.186 with no explicit bump. run 31978548058. (task 1002620)
|
|
804
804
|
// 1.19.188 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
805
805
|
// landed since 1.19.187 with no explicit bump. run 31978931152. (task 1002620)
|
|
806
|
+
// 1.19.189 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
807
|
+
// landed since 1.19.188 with no explicit bump. run 31983108640. (task 1002620)
|
|
808
|
+
// 1.19.190 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
809
|
+
// landed since 1.19.189 with no explicit bump. run 31983360426. (task 1002620)
|
|
806
810
|
// ---------------------------------------------------------------------------
|
|
807
|
-
const CORE_VERSION = '1.19.
|
|
811
|
+
const CORE_VERSION = '1.19.190'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
808
812
|
|
|
809
813
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
810
814
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -151,16 +151,19 @@ after(() => {
|
|
|
151
151
|
seams._reset();
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
async function call(method, p, body) {
|
|
154
|
+
async function call(method, p, body, opts = {}) {
|
|
155
155
|
calls.length = 0;
|
|
156
156
|
const res = await fetch(`${BASE}${p}`, {
|
|
157
157
|
method,
|
|
158
158
|
headers: body === undefined ? {} : { 'content-type': 'application/json' },
|
|
159
159
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
160
|
+
// `redirect: 'manual'` lets a caller assert the 308 itself rather than
|
|
161
|
+
// silently following it — required by the legacy-alias tests at the bottom.
|
|
162
|
+
...(opts.redirect ? { redirect: opts.redirect } : {}),
|
|
160
163
|
});
|
|
161
164
|
let json = null;
|
|
162
165
|
try { json = await res.json(); } catch { /* no body */ }
|
|
163
|
-
return { status: res.status, json };
|
|
166
|
+
return { status: res.status, json, headers: res.headers };
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
// --- the deny wall -----------------------------------------------------------
|
|
@@ -392,3 +395,67 @@ test('DELETE /governance/assignments validates the builder id param', async () =
|
|
|
392
395
|
const r = await call('DELETE', '/government/assignments/not-a-number/triage-helper');
|
|
393
396
|
assert.equal(r.status, 400);
|
|
394
397
|
});
|
|
398
|
+
|
|
399
|
+
// ── the one-release /governance/* compatibility alias (ADR 0174) ──────────────
|
|
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.
|
|
412
|
+
//
|
|
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.
|
|
420
|
+
const aliasApp = express();
|
|
421
|
+
aliasApp.use(express.json());
|
|
422
|
+
aliasApp.use(attachFail);
|
|
423
|
+
aliasApp.use('/api/gds', buildGovernmentRouter());
|
|
424
|
+
const aliasServer = aliasApp.listen(0);
|
|
425
|
+
await new Promise((r) => aliasServer.once('listening', r));
|
|
426
|
+
const ALIAS_BASE = `http://127.0.0.1:${aliasServer.address().port}`;
|
|
427
|
+
after(() => aliasServer.close());
|
|
428
|
+
|
|
429
|
+
const hop = (method, p) =>
|
|
430
|
+
fetch(`${ALIAS_BASE}${p}`, { method, redirect: 'manual' });
|
|
431
|
+
|
|
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'],
|
|
440
|
+
];
|
|
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`);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
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');
|
|
452
|
+
});
|
|
453
|
+
|
|
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.
|
|
457
|
+
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
|
+
});
|