@bongos/core 1.19.659 → 1.19.660

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,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.659",
6
- "core_contract": "1.19.659",
7
- "source_commit": "feb248ee00547956060970a0a2b26b7923f1ef47",
5
+ "core_version": "1.19.660",
6
+ "core_contract": "1.19.660",
7
+ "source_commit": "5d0f5acbd9c33a29dfff28dfc344af42a4bf53f3",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-11T01:18:53.775Z",
9
+ "built_at": "2026-09-11T01:39:06.499Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 473,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2610,
20
- "tree_sha256": "d533f43caabc05ab3636390cbbe8f63a140b0c0d84321a438dd2f243041f2301",
20
+ "tree_sha256": "02c8e791b05c6718bd28b1e66840239f3f3b6ef1cd6d7fff3e6f22598468f4db",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2787,7 +2787,7 @@
2787
2787
  {
2788
2788
  "path": "docs/module-api-changelog.md",
2789
2789
  "mode": "0000644",
2790
- "sha256": "b92613f789d9a907511c2b1490773a86e1c8c2793f90560bfbb8ede7c3b24652"
2790
+ "sha256": "4ed79e71c0f7543d8189f2c4d0ee3c83c34429b6785beb7ddeaa529b7f5f7576"
2791
2791
  },
2792
2792
  {
2793
2793
  "path": "docs/modules-contract.md",
@@ -4487,7 +4487,7 @@
4487
4487
  {
4488
4488
  "path": "modules/government/catalog.js",
4489
4489
  "mode": "0000644",
4490
- "sha256": "3d6d6ced85b69bd6086533a0d118e4032037de66a10931cb3fc4a75bd8b6dbdc"
4490
+ "sha256": "ce3dd2f87b0c6fbb1e2c62028bf50679fa8be316546efb6bf398a2afe22a791d"
4491
4491
  },
4492
4492
  {
4493
4493
  "path": "modules/government/config.js",
@@ -7752,12 +7752,12 @@
7752
7752
  {
7753
7753
  "path": "package-lock.json",
7754
7754
  "mode": "0000644",
7755
- "sha256": "d8f97007999a707d1f1b5cce57a4933594b9abe96061396ac0cf09bb5a22fa5d"
7755
+ "sha256": "65abd9c62cd7f323dcc6478d9203b11fcd364e6c86e43e0e11763dfa8812311f"
7756
7756
  },
7757
7757
  {
7758
7758
  "path": "package.json",
7759
7759
  "mode": "0000644",
7760
- "sha256": "6498ec4a5891c341fa302772fd0ca40de911494cfee16009186a43208a27c18f"
7760
+ "sha256": "a71aea9196614109127b129b57630118e53dabf98c168c6e7ec291673d27eb86"
7761
7761
  },
7762
7762
  {
7763
7763
  "path": "public-docs/index.html",
@@ -9512,7 +9512,7 @@
9512
9512
  {
9513
9513
  "path": "src/module-api.js",
9514
9514
  "mode": "0000644",
9515
- "sha256": "b44e546023213b0bedaf19d6685ea25dd9d3da8f61b33296a7d311bb4f459390"
9515
+ "sha256": "5e1cee6085965844a656e2b5791db19d9cad43770691f4ebb07100787bdbf448"
9516
9516
  },
9517
9517
  {
9518
9518
  "path": "src/module-loader/catalog.js",
@@ -10847,7 +10847,7 @@
10847
10847
  {
10848
10848
  "path": "tests/government_catalog.mjs",
10849
10849
  "mode": "0000644",
10850
- "sha256": "74cfbd482b689df40348af3bdaf050f056e216ff18fdd6add4136d4589b7cd92"
10850
+ "sha256": "1058c2c691a05b4db0365fed68794de8cffa2c4af2c97f4e8c91ba197f67cb6d"
10851
10851
  },
10852
10852
  {
10853
10853
  "path": "tests/government_charter_library.mjs",
@@ -10912,7 +10912,7 @@
10912
10912
  {
10913
10913
  "path": "tests/government_parity.mjs",
10914
10914
  "mode": "0000644",
10915
- "sha256": "7e1cc047dc75b9574bd64329974f8d1ae59b474d8719fbaae00314c7b2cc88e5"
10915
+ "sha256": "0c619508691d41f41b0fbbb1d6939ba9ac7fde2ef4ec08ae7622bfa70bfbf624"
10916
10916
  },
10917
10917
  {
10918
10918
  "path": "tests/government_principals.mjs",
@@ -1767,5 +1767,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1767
1767
  landed since 1.19.657 with no explicit bump. run 34547406494. (task 1002620)
1768
1768
  1.19.659 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1769
1769
  landed since 1.19.658 with no explicit bump. run 34550130772. (task 1002620)
1770
+ 1.19.660 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1771
+ landed since 1.19.659 with no explicit bump. run 34551485764. (task 1002620)
1770
1772
  ---------------------------------------------------------------------------
1771
1773
  ```
@@ -221,7 +221,7 @@ const PERMISSIONS = [
221
221
  // route reads is INERT; a route reading a key with no grant rows 403s everyone.
222
222
  // ADR 0174's Consequences record that failure landing on the Government tab
223
223
  // itself. So grants lead, routes follow — inside this goal, not across releases.
224
- { key: 'board.item.open', system: false, floor: 'metic', guards: 'POST /board/items — put a subject to the board (ADR 0175; a Full Idea window opens automatically on grade pass, so in practice this guards an amendment)' },
224
+ { key: 'board.item.open', system: false, floor: 'metic', guards: 'POST /government/board/items — put a subject to the board (ADR 0175; a Full Idea window opens automatically on grade pass, so in practice this guards an amendment)' },
225
225
  // floor 'xenos' is NOT a mistake and must not be "tightened". Who may vote is
226
226
  // the CONSTITUTION's membership predicate, resolved per request from config
227
227
  // (ADR 0175 §6) — today `rank:archon`. Encoding that rank here too would fork
@@ -230,7 +230,7 @@ const PERMISSIONS = [
230
230
  // is. So the atom says only "this route exists for authenticated builders" and
231
231
  // the predicate does the real gating — the `criterion.create` pattern above,
232
232
  // where the floor is real but explicitly not the whole gate.
233
- { key: 'board.vote.cast', system: false, floor: 'xenos', guards: 'POST /board/items/:id/vote — floor is the coarse gate; board membership is the configured predicate (ADR 0175 §6), checked in-handler' },
233
+ { key: 'board.vote.cast', system: false, floor: 'xenos', guards: 'POST /government/board/items/:itemId/votes — floor is the coarse gate; board membership is the configured predicate (ADR 0175 §6), checked in-handler' },
234
234
  { key: 'task.newcomer_restock', system: false, floor: 'metic', guards: 'POST /tasks/newcomer-restock (ADR 0157, was archon)' },
235
235
  { key: 'task.peer_votes.tally', system: false, floor: 'metic', guards: 'POST /tasks/peer-votes/tally (ADR 0157, was archon)' },
236
236
  { key: 'goal.reorder', system: false, floor: 'metic', guards: 'POST /goals/reorder (ADR 0157, was archon)' },
@@ -247,7 +247,25 @@ const PERMISSIONS = [
247
247
  // ── ownership-scoped + baseline own-work (floor thetes / xenos) ───────────────
248
248
  { key: 'task.claim.any', system: false, floor: 'thetes', guards: 'claim the general queue (per-task requires_rank floor still applies)' },
249
249
  { key: 'page.view.builder', system: false, floor: 'thetes', guards: 'authenticated hall pages (requireNonXenosPage / requireBuilderPage)' },
250
- { key: 'goal.manage.own', system: false, floor: 'metic', scope: 'own', resource: 'goal', guards: 'PATCH /goals/:id/members/:b, POST /goals/:id/transfer owner/manager, in-handler (ADR 0106)' },
250
+ // floor 'metic' is real but NOT the whole gatethe `criterion.create` pattern
251
+ // above, and here the floor is the LOOSER half. Both routes carry requireBuilder
252
+ // and decide in-handler (authorizeMembershipKindChange / authorizeOwnershipTransfer,
253
+ // modules/lifecycle/routes/goal-route-authz.js): rank is consulted ONLY as an
254
+ // Archon bypass, so `actorIsOwner || actorIsManager` admits at ANY rank.
255
+ //
256
+ // THE LIVE POPULATION, recorded because a migration must not silently evict it:
257
+ // a goal's owner is its `created_by` (goal-authz.js isGoalOwner), so a builder
258
+ // DEMOTED below metic keeps owner authority over goals they created; and a
259
+ // sub-metic member can be promoted to manager ('lead') on any unprotected goal,
260
+ // reaching this permission without ever holding metic.
261
+ //
262
+ // So when R104-R107 migrates these routes, the gate stays requireBuilder +
263
+ // resolver.authorizeOwned(key, owner) — a bare permission gate on this key would
264
+ // 403 exactly the owners and managers the routes admit today. The floor is
265
+ // deliberately NOT lowered to xenos here (the board.vote.cast option): a floor
266
+ // feeds RANK_SEED and the live reset route, so widening it is an authority change
267
+ // that wants its own decision, not a note in a hardening batch.
268
+ { key: 'goal.manage.own', system: false, floor: 'metic', scope: 'own', resource: 'goal', guards: 'PATCH /goals/:id/members/:b, POST /goals/:id/transfer — requireBuilder + in-handler owner|manager|Archon (ADR 0106); floor is not the whole gate, and sub-metic owners/managers hold it today' },
251
269
  { key: 'task.claim.newcomer', system: false, floor: 'xenos', guards: 'claim a newcomer_friendly task (xenosClaimAllowed; xenos = one active claim)' },
252
270
  { key: 'task.ship', system: false, floor: 'xenos', guards: '/builder-ship own claim' },
253
271
  { key: 'task.act.own', system: false, floor: 'xenos', scope: 'own', resource: 'claim', guards: 'act on OWN active claim (release, cost, notes) — ownership-scoped' },
@@ -261,7 +279,7 @@ const PERMISSIONS = [
261
279
  // marker here. Exactly why scope is declared data: the suffix rule only runs the
262
280
  // safe direction ('.own' ⇒ scope 'own'), never the converse.
263
281
  { key: 'me.wandering.set', system: false, floor: 'xenos', scope: 'own', resource: 'builder_prefs', guards: 'PATCH /me/wandering (clamped by maxLevelForRank) — ownership-scoped' },
264
- { key: 'cost.log', system: false, floor: 'xenos', guards: 'POST /costs' },
282
+ { key: 'cost.log', system: false, floor: 'xenos', guards: 'POST /cost' },
265
283
  { key: 'learning.create', system: false, floor: 'xenos', guards: 'POST /learnings' },
266
284
  { key: 'blocker.file', system: false, floor: 'xenos', guards: 'POST /blockers' },
267
285
  { key: 'idea.file', system: false, floor: 'xenos', guards: 'POST /inbox (own idea capture)' },
@@ -281,7 +299,8 @@ const OWNERSHIP_SCOPES = Object.freeze(['own', 'any']);
281
299
  const _byKey = new Map();
282
300
  const _byPrincipal = new Map();
283
301
  const _byResource = new Map();
284
- for (const p of PERMISSIONS) {
302
+ for (let i = 0; i < PERMISSIONS.length; i++) {
303
+ const p = PERMISSIONS[i];
285
304
  if (typeof p.key !== 'string' || !p.key) throw new Error(`government catalog: permission missing a key`);
286
305
  if (typeof p.system !== 'boolean') throw new Error(`government catalog: ${p.key} missing boolean 'system'`);
287
306
  if (p.floor !== null && !RANK_ORDER.includes(p.floor)) throw new Error(`government catalog: ${p.key} has invalid floor "${p.floor}"`);
@@ -320,6 +339,16 @@ for (const p of PERMISSIONS) {
320
339
  // floor would seed the key into a rank that the substrate then still denies.
321
340
  if (p.floor !== 'archon') throw new Error(`government catalog: ${p.key} is substrate-enforced and must floor at 'archon' (the substrate compares against the top of the ladder)`);
322
341
  }
342
+ // A system:true key is trust-boundary authority (ADR 0016): it is never grantable
343
+ // to a custom rank, so the ONLY way a builder holds it is by sitting on a seeded
344
+ // rank at or above its floor. That makes the floor the whole wall, and the wall
345
+ // belongs at the top of the ladder. Without this, {system:true, floor:'metic'}
346
+ // loads clean and governance_002's reset route seeds Archon-only power to Metic.
347
+ // The two floor:null shapes are the exception and are already partitioned above:
348
+ // a machine principal is held by NO rank at all.
349
+ if (p.system && principal === null && p.floor !== 'archon') {
350
+ throw new Error(`government catalog: ${p.key} is system:true and must floor at 'archon' (a system key is not grantable, so its floor is the whole wall) — got "${p.floor}"`);
351
+ }
323
352
  // ── the OWNERSHIP axis (R100) ─────────────────────────────────────────────
324
353
  const scope = p.scope ?? null;
325
354
  const resource = p.resource ?? null;
@@ -347,7 +376,15 @@ for (const p of PERMISSIONS) {
347
376
  if (scope !== null && p.floor === null) {
348
377
  throw new Error(`government catalog: ${p.key} is ownership-scoped but has no rank floor — the ownership axis narrows a RANK permission, never an identity one`);
349
378
  }
379
+ // ONE object per permission, shared by every reader. The frozen copy goes back
380
+ // into PERMISSIONS[i] as well as the lookup maps, because the alternative —
381
+ // freezing a COPY and leaving the original mutable in the array — splits the
382
+ // catalog into two views that can disagree: `Object.freeze(PERMISSIONS)` below
383
+ // seals the array's SHAPE but not its entries, so a runtime mutation of
384
+ // PERMISSIONS[i] would change the floor seedForRank() reads (and with it the
385
+ // live POST /government/ranks/:rankKey/reset) while byKey() kept the old value.
350
386
  const frozen = Object.freeze({ ...p, principal, scope, resource, substrate });
387
+ PERMISSIONS[i] = frozen;
351
388
  _byKey.set(p.key, frozen);
352
389
  if (principal !== null) _byPrincipal.set(principal, frozen);
353
390
  if (resource !== null) {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.659",
3
+ "version": "1.19.660",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.659",
9
+ "version": "1.19.660",
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.659",
3
+ "version": "1.19.660",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.659'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.660'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -178,3 +178,16 @@ test('byKey returns a frozen record or null', () => {
178
178
  assert.throws(() => { p.system = false; }, /Cannot assign|read only|frozen/i);
179
179
  assert.equal(byKey('nope'), null);
180
180
  });
181
+
182
+ // Freezing the ARRAY only seals its shape. Before task 1003194 the entries inside it
183
+ // stayed mutable and byKey() handed out a frozen COPY, so the catalog had two views
184
+ // that could disagree: mutating PERMISSIONS[i].floor moved what seedForRank() reads —
185
+ // and with it the live POST /government/ranks/:rankKey/reset — while byKey() kept the
186
+ // old value. Identity is the assertion that actually forbids that, not depth.
187
+ test('the catalog is ONE immutable object per permission — array, entries, and byKey identity', () => {
188
+ assert.equal(Object.isFrozen(PERMISSIONS), true, 'the PERMISSIONS array must be frozen');
189
+ for (const p of PERMISSIONS) {
190
+ assert.equal(Object.isFrozen(p), true, `PERMISSIONS entry ${p.key} must be frozen, not just the array`);
191
+ assert.equal(p, byKey(p.key), `${p.key}: PERMISSIONS[i] and byKey() must be the SAME object, not equal copies`);
192
+ }
193
+ });
@@ -163,6 +163,13 @@ const normRoute = (key) => key.replace(/:[A-Za-z0-9_]+/g, ':p'); // :id vs :crit
163
163
  // Leading `METHOD /path` of a guards note. Deliberately conservative: a note that
164
164
  // lists several routes or wraps them in prose/braces simply doesn't participate.
165
165
  const GUARD_ROUTE_RE = /^(GET|POST|PATCH|PUT|DELETE)\s+(\/[^\s,(]+)/;
166
+ // The conservative intent above is enforced HERE, not by the character class: a
167
+ // path carrying `{a,b}` (a brace set), `[/x]` (an optional segment) or `*` (a
168
+ // wildcard) is a PATTERN standing for several routes, not one concrete route, so
169
+ // it cannot be looked up and does not participate. Without this the regex happily
170
+ // captured `/blockers/:id/{resolve` and seven siblings, which then dangled in B0
171
+ // and drowned the three real typos it exists to catch (task 1003194).
172
+ const ROUTE_PATTERN_RE = /[{[*]/;
166
173
  // A route's middleware rank → the LOOSEST floor still consistent with it. Only the
167
174
  // two rank-gated classifications constrain a floor; 'any-builder' / 'public' /
168
175
  // 'unknown' / 'bfg-principal' carry no rank middleware to compare against.
@@ -190,12 +197,17 @@ function crossReference() {
190
197
  const live = liveRoutes();
191
198
  const floorPinned = [];
192
199
  const atomPinned = [];
200
+ const dangling = [];
193
201
  for (const p of FLOORED) {
194
202
  const m = GUARD_ROUTE_RE.exec(p.guards);
195
203
  if (!m) continue;
204
+ if (ROUTE_PATTERN_RE.test(m[2])) continue;
196
205
  const routeKey = `${m[1]} ${m[2]}`;
197
206
  const r = live.get(normRoute(routeKey));
198
- if (!r) continue;
207
+ // A note that names ONE concrete route which does not exist is a typo, and a
208
+ // silent `continue` here is how it stayed invisible: the entry simply dropped
209
+ // out of B1 and B3 while all 27 tests stayed green. B0 asserts this is empty.
210
+ if (!r) { dangling.push(`${p.key} → ${routeKey}`); continue; }
199
211
  if (r.perms) {
200
212
  atomPinned.push({ permission: p.key, floor: p.floor, routeKey, perms: r.perms });
201
213
  continue;
@@ -204,9 +216,27 @@ function crossReference() {
204
216
  if (!minFloor) continue;
205
217
  floorPinned.push({ permission: p.key, floor: p.floor, routeKey, routeRank: r.rank, minFloor });
206
218
  }
207
- return { floorPinned, atomPinned };
219
+ return { floorPinned, atomPinned, dangling };
208
220
  }
209
221
 
222
+ // The oracles B1 and B3 can only judge a permission whose guards note RESOLVES to a
223
+ // live route. A note naming a route that does not exist therefore fails open — it is
224
+ // dropped, not reported, and the permission silently leaves both oracles while the
225
+ // suite stays green. That is not hypothetical: `cost.log` said 'POST /costs' against
226
+ // a real 'POST /cost', and both Board Room keys omitted the '/government' prefix, so
227
+ // the two newest requirePermission routes sat outside B3 — the very oracle written
228
+ // for them. This test is the fail-loud half (task 1003194).
229
+ //
230
+ // There is deliberately NO allowlist. A note that genuinely cannot name one concrete
231
+ // route does not reach here at all: it either fails GUARD_ROUTE_RE (prose, a surface
232
+ // name like 'planning-session surface') or is a pattern caught by ROUTE_PATTERN_RE.
233
+ // An allowlist would be a place to park the next typo.
234
+ test('B0: every guards note that names a concrete route names one that EXISTS', () => {
235
+ const { dangling } = crossReference();
236
+ assert.deepEqual(dangling, [],
237
+ `guards notes point at routes that do not exist — fix the note (or the route), never ignore it:\n ${dangling.join('\n ')}`);
238
+ });
239
+
210
240
  // ONE-DIRECTIONAL, and deliberately so. A floor STRICTER than the route middleware
211
241
  // is legitimate and common: the wall lives in the handler (task.confirm.grade_bypass
212
242
  // on an any-builder POST /tasks/:id/confirm; goal.manage.own's owner|manager check;