@bongos/core 1.19.635 → 1.19.637

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.635",
6
- "core_contract": "1.19.635",
7
- "source_commit": "c97f3015861c4ca09d2c5a07f78ab33284d7c26c",
5
+ "core_version": "1.19.637",
6
+ "core_contract": "1.19.637",
7
+ "source_commit": "1c08a67a41a3bf7de85545460f6803cf6c68982d",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-10T02:33:59.187Z",
9
+ "built_at": "2026-09-10T04:22:04.368Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 468,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2099,
14
+ "functional_verbatim": 2100,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2591,
20
- "tree_sha256": "c5c9267a793145efe19a63442cfbe05ee6cf03d5c6ff363084857bf151aec4cc",
19
+ "file_count": 2592,
20
+ "tree_sha256": "09c8365d205c14a4ae13cab5ab9eb561213f4e38cc9126033a3942e5dda81e3f",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/backlog-review/SKILL.md",
@@ -2777,7 +2777,7 @@
2777
2777
  {
2778
2778
  "path": "docs/module-api-changelog.md",
2779
2779
  "mode": "0000644",
2780
- "sha256": "baa05168e88c98e3fc35c1bf652bd85ef90dcfb463408c68e5132910316f8c34"
2780
+ "sha256": "3e80809a3e33883a21335d5380c6bf9358d98f09ac1ccf6322c864bbbaa06892"
2781
2781
  },
2782
2782
  {
2783
2783
  "path": "docs/modules-contract.md",
@@ -7707,12 +7707,12 @@
7707
7707
  {
7708
7708
  "path": "package-lock.json",
7709
7709
  "mode": "0000644",
7710
- "sha256": "f40ca54ae7693ccd7a41bb2429fa61f71b147f0aabc821535e4ca6e9055d9f49"
7710
+ "sha256": "262eabcdc5a5a052871db03a5a334721de892e4d927f4c069787ad763d675686"
7711
7711
  },
7712
7712
  {
7713
7713
  "path": "package.json",
7714
7714
  "mode": "0000644",
7715
- "sha256": "1d7c9b3ad4172d3702cd4baa6fd5f30d1c26bedcfdcea6e8c2532d0decb9814e"
7715
+ "sha256": "e62905214d126629233cda9c9bbb2d57e3b45771899b66139237607269e190fb"
7716
7716
  },
7717
7717
  {
7718
7718
  "path": "public-docs/index.html",
@@ -8144,6 +8144,11 @@
8144
8144
  "mode": "0000644",
8145
8145
  "sha256": "a34da5bd47c0410db31344ae042bae318e6daf0e774672999f14213c3c1180c5"
8146
8146
  },
8147
+ {
8148
+ "path": "scripts/gds/fitness-checks-route-pins.js",
8149
+ "mode": "0000644",
8150
+ "sha256": "4bd0d9c3b39a6f8af4561925835ebf9b349170be36aef926c6af2160af0d1ab5"
8151
+ },
8147
8152
  {
8148
8153
  "path": "scripts/gds/fitness-checks-vocabulary.js",
8149
8154
  "mode": "0000644",
@@ -8167,7 +8172,7 @@
8167
8172
  {
8168
8173
  "path": "scripts/gds/fitness.js",
8169
8174
  "mode": "0000644",
8170
- "sha256": "cd9f61f69ef8d017a39bed129e41a3d3944cf53bfaf6ff26eb868e46611b6dc3"
8175
+ "sha256": "610d76cee575fa1a87f2ff7bb84f5576f3c4a8cf2e727e6c372d76ebf9a0a340"
8171
8176
  },
8172
8177
  {
8173
8178
  "path": "scripts/gds/gate-review.js",
@@ -8367,7 +8372,7 @@
8367
8372
  {
8368
8373
  "path": "scripts/gds/main-audit.js",
8369
8374
  "mode": "0000644",
8370
- "sha256": "18e14d00fead3293eb5b1c9d7a756a0a3780147938a40253ff35540740a73f4b"
8375
+ "sha256": "35047f05c6059aee0cb64b13ef1495e4db1f3885b5b9c8cac5c60283069a059d"
8371
8376
  },
8372
8377
  {
8373
8378
  "path": "scripts/gds/me.js",
@@ -9312,7 +9317,7 @@
9312
9317
  {
9313
9318
  "path": "src/bongos/route-rank-check.js",
9314
9319
  "mode": "0000644",
9315
- "sha256": "514f9bb73cc74a2574272b733e0314dfe3b372497874bd7c88b763f22ddab64e"
9320
+ "sha256": "f57293c2d157949ccb5a74511f167902adf096037b4830b0bf667a38baf6ab58"
9316
9321
  },
9317
9322
  {
9318
9323
  "path": "src/bongos/routes.js",
@@ -9452,7 +9457,7 @@
9452
9457
  {
9453
9458
  "path": "src/module-api.js",
9454
9459
  "mode": "0000644",
9455
- "sha256": "79ef2ff7f2ac145450cc27389e4da2edd8079f2887ba7240c057e0e8cb8bb198"
9460
+ "sha256": "b15a7de69ca6e8cd7c57b50dab455d6ec44a40336895c78738ad0f8ec8a25a10"
9456
9461
  },
9457
9462
  {
9458
9463
  "path": "src/module-loader/catalog.js",
@@ -10482,7 +10487,7 @@
10482
10487
  {
10483
10488
  "path": "tests/fitness.mjs",
10484
10489
  "mode": "0000644",
10485
- "sha256": "5e5b53d5a1dce6b7ffacbea7198375aac6f77259ee948c00c24eb157f62a034d"
10490
+ "sha256": "2208a1e4d3df3ce712c4e4dbc7908d20520833375a2c07c3da5d37252dd56b7a"
10486
10491
  },
10487
10492
  {
10488
10493
  "path": "tests/fitness_gds_ratchet.mjs",
@@ -10852,7 +10857,7 @@
10852
10857
  {
10853
10858
  "path": "tests/government_protected_surfaces.mjs",
10854
10859
  "mode": "0000644",
10855
- "sha256": "fd05f72404be0d007222793a6ed3a49904239f8bb354cddab6b052d74288b3b1"
10860
+ "sha256": "cd56facc3522f17cd83b067ee1ae9dcb3e4e03dac13157013c44d7ba23508af2"
10856
10861
  },
10857
10862
  {
10858
10863
  "path": "tests/government_rank_sync.mjs",
@@ -11437,7 +11442,7 @@
11437
11442
  {
11438
11443
  "path": "tests/main_audit.mjs",
11439
11444
  "mode": "0000644",
11440
- "sha256": "9a1813d6036886ea07d1563e71a466e66c28554b75b22605464c795fd9fa2ebb"
11445
+ "sha256": "eff2ce96889306f4898686c3648d302f54fff5f137ee41ddf543866fdf2f807d"
11441
11446
  },
11442
11447
  {
11443
11448
  "path": "tests/maintenance_goal.mjs",
@@ -12202,7 +12207,7 @@
12202
12207
  {
12203
12208
  "path": "tests/route_rank_check.mjs",
12204
12209
  "mode": "0000644",
12205
- "sha256": "9fd3762c6b171ee39b0cee336de2d41547795b2cd4898b178325cfdd321cf28a"
12210
+ "sha256": "755f2c2c1cd929f0a4449ceb1c4bd0ac719aadf64c56484e03b4bed57e6daf7c"
12206
12211
  },
12207
12212
  {
12208
12213
  "path": "tests/route_shadow_guard.mjs",
@@ -1719,5 +1719,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1719
1719
  landed since 1.19.633 with no explicit bump. run 34429621784. (task 1002620)
1720
1720
  1.19.635 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1721
1721
  landed since 1.19.634 with no explicit bump. run 34429906252. (task 1002620)
1722
+ 1.19.636 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1723
+ landed since 1.19.635 with no explicit bump. run 34434517065. (task 1002620)
1724
+ 1.19.637 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1725
+ landed since 1.19.636 with no explicit bump. run 34436870653. (task 1002620)
1722
1726
  ---------------------------------------------------------------------------
1723
1727
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.635",
3
+ "version": "1.19.637",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.635",
9
+ "version": "1.19.637",
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.635",
3
+ "version": "1.19.637",
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",
@@ -0,0 +1,76 @@
1
+ // scripts/gds/fitness-checks-route-pins.js — Check 4b (task 1003192): every
2
+ // MODULE_EXPECTED_RANKS key names a real module directory.
3
+ //
4
+ // Carved out of scripts/gds/fitness.js on the task-1003206 precedent (the R03
5
+ // de-monolith plan): that file sits against the 1,500-line ratchet with two lines
6
+ // of headroom, so a new check lands here rather than tipping it over. fitness.js
7
+ // imports the name straight back, so the CHECKS registry and the module's public
8
+ // surface — which the tests read heavily — are unchanged.
9
+ //
10
+ // WHY THE CHECK EXISTS. src/bongos/route-rank-check.js pins the expected rank of
11
+ // each privileged module route, but pin SCOPE derives from successfully-classified
12
+ // routes: a MODULE_EXPECTED_RANKS key matching no discovered module is dropped and
13
+ // its pins pass VACUOUSLY, while the loader keeps mounting modules/<key>/routes/ in
14
+ // production by the same convention the scan discovers them by. Measured on the real
15
+ // classifier: with modules/security/ renamed, all six security pins went inert and a
16
+ // POST /security/reports downgraded to any-builder drew zero findings — a pinned
17
+ // privileged WRITE route silently unguarded.
18
+ //
19
+ // THIS IS THE SOURCE-REPO HALF of a two-layer guard. checkRouteRanks fires when a
20
+ // module directory is PRESENT but contributed no route (a rename/move under a live
21
+ // module); it deliberately stays silent on a wholly absent directory, because it runs
22
+ // on every ship and an instance may legitimately omit an optional module. Here every
23
+ // module is expected to exist, so a whole-directory rename or a typo'd key — neither
24
+ // of which the other layer can see — hard-fails. The governance→government rename had
25
+ // to hand-carry double-keyed pins precisely because nothing mechanical caught this.
26
+
27
+ 'use strict';
28
+
29
+ const fs = require('node:fs');
30
+ const path = require('node:path');
31
+
32
+ const { REPO_ROOT } = require('./fitness-lib.js');
33
+ const { checkRouteRanks, MODULE_EXPECTED_RANKS } = require('../../src/bongos/route-rank-check');
34
+
35
+ // ============================================================================
36
+ // Check 4 — no route bypasses server-side rank checks (ADR 0016) [reuses route-rank-check]
37
+ // ============================================================================
38
+ // Moved here with Check 4b (task 1003192): the two are the same subject — Check 4 asks
39
+ // whether each route carries a gate, 4b whether the pins that assert those gates are
40
+ // actually in scope — and keeping them together is what buys fitness.js headroom against
41
+ // the 1,500-line ratchet instead of leaving it sitting exactly on the cap.
42
+ function checkRankGuards() {
43
+ const r = checkRouteRanks();
44
+ // hardFail = an ungated WRITE route or a downgraded privileged route. Unclassified read routes
45
+ // are 'concerns' (the Hacker adjudicates intent) — surfaced as warnings, not a hard failure.
46
+ const violations = r.findings.filter((f) => f.severity === 'blocker').map((f) => `${f.route} (${f.file}) — ${f.summary}`);
47
+ const warnings = r.findings.filter((f) => f.severity !== 'blocker').map((f) => `${f.route} (${f.file}) — ${f.summary}`);
48
+ return { name: 'route rank guards (ADR 0016)', ok: !r.hardFail, hardFail: r.hardFail, violations, warnings, note: `${r.routeCount} routes classify to a known rank; no ungated writes, no privileged downgrades.` };
49
+ }
50
+
51
+ // ============================================================================
52
+ // Check 4b — module rank-pin keys resolve to a real modules/<key>/ dir
53
+ // ============================================================================
54
+ function checkModulePinKeysResolve() {
55
+ const violations = [];
56
+ for (const key of Object.keys(MODULE_EXPECTED_RANKS)) {
57
+ if (!fs.existsSync(path.join(REPO_ROOT, 'modules', key))) {
58
+ violations.push(`MODULE_EXPECTED_RANKS.${key} names no modules/${key}/ directory — its `
59
+ + `${Object.keys(MODULE_EXPECTED_RANKS[key]).length} rank pin(s) are INERT and pass vacuously `
60
+ + '(src/bongos/route-rank-check.js). Rename the key to the module\'s current directory, or drop the entry.');
61
+ }
62
+ }
63
+ return {
64
+ name: 'module rank-pin keys resolve (task 1003192)',
65
+ ok: violations.length === 0,
66
+ hardFail: violations.length > 0,
67
+ violations,
68
+ warnings: [],
69
+ note: `${Object.keys(MODULE_EXPECTED_RANKS).length} module pin blocks all name a live modules/<key>/ directory.`,
70
+ };
71
+ }
72
+
73
+ module.exports = {
74
+ checkRankGuards,
75
+ checkModulePinKeysResolve,
76
+ };
@@ -23,7 +23,7 @@
23
23
  const fs = require('node:fs');
24
24
  const path = require('node:path');
25
25
  const { execFileSync } = require('node:child_process');
26
- const { checkRouteRanks } = require('../../src/bongos/route-rank-check');
26
+ const { checkRankGuards, checkModulePinKeysResolve } = require('./fitness-checks-route-pins.js'); // Checks 4 + 4b — carved out by task 1003192 (the 1,500-line ratchet)
27
27
  const genApiDocs = require('./gen-api-docs'); // R12 (#1999): reuse the route model (readsBody/validated) that drives the OpenAPI spec
28
28
  const { parseRoutineFrontmatter } = require('./run-routine'); // System 4 (ADR 0078): deterministic-routine boundary
29
29
  const { filenameViolation, stemCollisions, CORE_NS, INSTANCE_NS } = require('./migration-namespace'); // ADR 0108 §5: cross-repo migration namespacing
@@ -151,18 +151,6 @@ function checkNestedDocsPresent() {
151
151
  return { name: 'nested CLAUDE.md presence', ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings: [], note: `present for: ${HIGH_TRAFFIC.join(', ')}.` };
152
152
  }
153
153
 
154
- // ============================================================================
155
- // Check 4 — no route bypasses server-side rank checks (ADR 0016) [reuses route-rank-check]
156
- // ============================================================================
157
- function checkRankGuards() {
158
- const r = checkRouteRanks();
159
- // hardFail = an ungated WRITE route or a downgraded privileged route. Unclassified read routes
160
- // are 'concerns' (the Hacker adjudicates intent) — surfaced as warnings, not a hard failure.
161
- const violations = r.findings.filter((f) => f.severity === 'blocker').map((f) => `${f.route} (${f.file}) — ${f.summary}`);
162
- const warnings = r.findings.filter((f) => f.severity !== 'blocker').map((f) => `${f.route} (${f.file}) — ${f.summary}`);
163
- return { name: 'route rank guards (ADR 0016)', ok: !r.hardFail, hardFail: r.hardFail, violations, warnings, note: `${r.routeCount} routes classify to a known rank; no ungated writes, no privileged downgrades.` };
164
- }
165
-
166
154
  // ============================================================================
167
155
  // Check 5 — generated artifacts are fresh (Path C enforces Path B; ADR 0063 + ADR 0062 §8)
168
156
  // ============================================================================
@@ -1357,6 +1345,7 @@ const CHECKS = [
1357
1345
  checkSkillFrontmatter,
1358
1346
  checkNestedDocsPresent,
1359
1347
  checkRankGuards,
1348
+ checkModulePinKeysResolve,
1360
1349
  checkGeneratedArtifactsFresh,
1361
1350
  checkDeterministicRoutines,
1362
1351
  checkCoreNeverImportsModule,
@@ -1443,6 +1432,7 @@ module.exports = {
1443
1432
  checkClaudeMdBudget,
1444
1433
  checkNestedDocsPresent,
1445
1434
  checkRankGuards,
1435
+ checkModulePinKeysResolve,
1446
1436
  checkGeneratedArtifactsFresh,
1447
1437
  checkDeterministicRoutines,
1448
1438
  checkCoreNeverImportsModule,
@@ -55,7 +55,14 @@ const { cliClient, API_BASE } = require('./cli-lib');
55
55
  // not a GDS github_login, and every builder shares one commit email — so the
56
56
  // author NAME is the only distinguishing signal, and an unresolved name is
57
57
  // 'needs a human glance', not 'definitely a violation'.
58
- function analyzeCommits(commits, rankByIdentity, { strict = false } = {}) {
58
+ //
59
+ // `registry` is the protected-surface matcher, defaulting to the real one. It
60
+ // is a parameter for the same reason `rankByIdentity` is: this analyzer is pure
61
+ // and its callers supply its inputs. Tests inject a MIXED-FLOOR fixture through
62
+ // it — the real registry floors every surface at 'metic' today, so without the
63
+ // seam the per-hit floor comparison below is untestable and would silently rot
64
+ // back into a global-minimum test. The CLI never passes it.
65
+ function analyzeCommits(commits, rankByIdentity, { strict = false, registry = ppc } = {}) {
59
66
  const findings = [];
60
67
  for (const c of commits || []) {
61
68
  // #863-followup (finding #5): we no longer blanket-skip merges. For a
@@ -67,28 +74,46 @@ function analyzeCommits(commits, rankByIdentity, { strict = false } = {}) {
67
74
  // changes are still attributed to their feature commits (no double-count);
68
75
  // but a merge that smuggles a protected change is now caught and attributed
69
76
  // to the MERGE author.
70
- const hits = ppc.matchProtected(c.files || []);
77
+ const hits = registry.matchProtected(c.files || []);
71
78
  if (hits.length === 0) continue;
72
79
 
73
80
  const rank = resolveRank(c, rankByIdentity);
74
81
  const fileList = hits.map((h) => h.file).join(', ');
75
82
  if (rank == null) {
83
+ const need = strictestFloorLabel(hits, registry);
76
84
  findings.push({
77
85
  severity: strict ? 'blocker' : 'review',
78
86
  sha: c.sha,
79
87
  author: c.authorName,
80
88
  rank: null,
81
89
  files: hits.map((h) => h.file),
82
- summary: `commit ${short(c.sha)} by '${c.authorName}' touched permission-sensitive path(s) [${fileList}] but the author could not be resolved to a GDS rank. Verify it is Metic+ work.`,
90
+ summary: `commit ${short(c.sha)} by '${c.authorName}' touched permission-sensitive path(s) [${fileList}] but the author could not be resolved to a GDS rank. Verify it is ${need}+ work.`,
83
91
  });
84
- } else if (ppc.isBelowFloor(rank)) {
92
+ continue;
93
+ }
94
+
95
+ // Each hit is judged against ITS OWN registry floor — not against the
96
+ // registry's global minimum. `isBelowFloor` tests the lowest floor any
97
+ // surface declares, which is identical only while every surface floors at
98
+ // the same rank (true today). The moment governance RAISES one surface's
99
+ // floor, a global-minimum test silently downgrades that raise back to the
100
+ // minimum here — in the one layer that catches a raw `git push origin main`
101
+ // (task 1003191; the cooperative layers already compare per-hit via
102
+ // checkPermissionPaths). Unknown ranks stay non-blocking, as before: an
103
+ // unresolvable rank is the `rank == null` case above.
104
+ const tier = registry.tierOf(rank);
105
+ const below = tier === null ? [] : hits.filter((h) => tier < registry.RANK_TIER[h.floor]);
106
+ if (below.length > 0) {
107
+ const need = strictestFloorLabel(below, registry);
108
+ const belowList = below.map((h) => `${h.file} (floor ${h.floor})`).join(', ');
85
109
  findings.push({
86
110
  severity: 'blocker',
87
111
  sha: c.sha,
88
112
  author: c.authorName,
89
113
  rank,
90
- files: hits.map((h) => h.file),
91
- summary: `commit ${short(c.sha)} by '${c.authorName}' (rank '${rank}', below Metic) modified permission-sensitive path(s) [${fileList}] — this is the SEC #863 violation class. It must be reverted and re-landed by a Metic+ builder if genuinely needed.`,
114
+ files: below.map((h) => h.file),
115
+ floors: below.map((h) => h.floor),
116
+ summary: `commit ${short(c.sha)} by '${c.authorName}' (rank '${rank}') modified permission-sensitive path(s) [${belowList}] whose registry floor it does not clear — this is the SEC #863 violation class. It must be reverted and re-landed by a ${need}+ builder if genuinely needed.`,
92
117
  });
93
118
  }
94
119
  }
@@ -115,6 +140,16 @@ function short(sha) {
115
140
  return String(sha || '').slice(0, 9);
116
141
  }
117
142
 
143
+ // The highest rank floor demanded by a set of protected hits, title-cased for
144
+ // prose ('Metic', 'Archon'). What a reader has to clear to own the whole commit
145
+ // — so a finding names the floor that actually governs it rather than a
146
+ // hardcoded 'Metic+' that a governance floor raise would make a lie.
147
+ function strictestFloorLabel(hits, registry) {
148
+ const floors = hits.map((h) => h.floor);
149
+ const top = floors.reduce((a, b) => (registry.RANK_TIER[b] > registry.RANK_TIER[a] ? b : a), floors[0]);
150
+ return top ? top.charAt(0).toUpperCase() + top.slice(1) : 'Metic';
151
+ }
152
+
118
153
  // ---------- git + API glue (CLI) ----------
119
154
 
120
155
  function git(args) {
@@ -934,8 +934,34 @@ function checkRouteRanks({ routesDir = ROUTES_DIR, modulesDir } = {}) {
934
934
  if (mm) scopedModules.add(mm[1]);
935
935
  }
936
936
  const effectivePins = { ...EXPECTED_RANKS };
937
+ // task 1003192: the skip above is SILENT — a key matching no discovered module is
938
+ // dropped with no else branch, so its pins pass vacuously while the routes they
939
+ // guard stay live (the loader mounts modules/<key>/routes/ by the same convention
940
+ // this scan discovers them by). Probed: with modules/security/ renamed, all six
941
+ // security pins go inert and a POST /security/reports deliberately downgraded to
942
+ // any-builder draws ZERO findings. The governance→government rename had to
943
+ // hand-carry double-keyed pins precisely because nothing mechanical catches this.
944
+ //
945
+ // Fire only when the module DIRECTORY IS PRESENT but contributed no route: that is
946
+ // unambiguously a rename/move UNDER a live module, never a legitimately-absent
947
+ // optional one. A wholly-missing directory stays silent here on purpose — an
948
+ // instance may ship without an optional module, and this runs on every ship — so
949
+ // that half is caught at the SOURCE repo by fitness.js Check 4b, where every
950
+ // module is expected to exist. The two layers are complementary, not redundant.
937
951
  for (const [mod, pins] of Object.entries(MODULE_EXPECTED_RANKS)) {
938
- if (scopedModules.has(mod)) Object.assign(effectivePins, pins);
952
+ if (scopedModules.has(mod)) { Object.assign(effectivePins, pins); continue; }
953
+ if (!fs.existsSync(path.join(modulesDir, mod))) continue; // optional module absent — legitimate
954
+ findings.push({
955
+ severity: 'major',
956
+ file: `modules/${mod}/`,
957
+ route: `(MODULE_EXPECTED_RANKS.${mod})`,
958
+ method: '(pins)',
959
+ rank: '(inert)',
960
+ summary: `modules/${mod}/ exists but contributed NO route to this scan, so its `
961
+ + `${Object.keys(pins).length} rank pin(s) were skipped and passed vacuously. `
962
+ + 'The routes may still be mounted in production while nothing verifies their rank. '
963
+ + `Point the pins at the module's current route location, or drop the ${mod} entry if the module no longer serves routes.`,
964
+ });
939
965
  }
940
966
  for (const [key, expected] of Object.entries(effectivePins)) {
941
967
  const r = byKey.get(key);
package/src/module-api.js CHANGED
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
55
55
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
56
56
  // the entry to that file. Look for a version's history there, not here.
57
57
  // ---------------------------------------------------------------------------
58
- const CORE_VERSION = '1.19.635'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
58
+ const CORE_VERSION = '1.19.637'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
59
59
 
60
60
  // A namespaced logger so a module's log lines are attributable + consistent.
61
61
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
package/tests/fitness.mjs CHANGED
@@ -68,6 +68,17 @@ test('no route bypasses server-side rank checks (ADR 0016)', () => {
68
68
  assert.equal(r.hardFail, false, r.violations.join('\n'));
69
69
  });
70
70
 
71
+ // task 1003192 — the source-repo half of the inert-pin guard. checkRouteRanks catches a
72
+ // pinned module whose dir is present but yields no route; it stays silent on a wholly
73
+ // absent dir because it runs on every ship and an instance may omit an optional module.
74
+ // Here every module is expected to exist, so a whole-dir rename or a typo'd key — which
75
+ // would park a block of rank pins vacuously while the routes stay mounted — must red.
76
+ test('every MODULE_EXPECTED_RANKS key names a real module dir (task 1003192)', () => {
77
+ const r = fitness.checkModulePinKeysResolve();
78
+ assert.equal(r.hardFail, false, r.violations.join('\n'));
79
+ assert.equal(r.ok, true);
80
+ });
81
+
71
82
  test('generated repo map is fresh (Path C enforces Path B)', () => {
72
83
  const r = fitness.checkGeneratedArtifactsFresh();
73
84
  assert.equal(r.hardFail, false, r.violations.join('\n'));
@@ -172,14 +172,36 @@ test('every glob added since R101 carries a documented reason', () => {
172
172
  }
173
173
  });
174
174
 
175
- test('every surface floors at metic the ADR 0043 floor, unchanged by the move', () => {
175
+ // Metic is a FLOOR, not a fixed value (task 1003191). Pinning every surface to
176
+ // exactly 'metic' made the registry's own contract untestable and, worse,
177
+ // illegal: governance owns this data and raising one surface to 'archon' is a
178
+ // policy edit the enforcers are supposed to honour. What must never happen is a
179
+ // surface dropping BELOW Metic — that is ADR 0043 being weakened by a data edit.
180
+ // So: no surface under Metic, and the derived minimum stays exactly Metic.
181
+ test('no surface floors below Metic — the ADR 0043 floor (a raise above it is legal policy)', () => {
176
182
  for (const s of registry.SURFACES) {
177
- assert.equal(s.floor, 'metic', `${s.glob} must keep the Metic floor`);
183
+ assert.ok(ppc.RANK_TIER[s.floor] >= ppc.RANK_TIER.metic,
184
+ `${s.glob} floors at '${s.floor}', below the Metic floor ADR 0043 sets. Governance may RAISE a floor; it may never lower one under Metic.`);
178
185
  }
179
- assert.equal(registry.FLOOR, 'metic');
186
+ assert.equal(registry.FLOOR, 'metic', 'the lowest declared floor is the Metic floor itself');
180
187
  assert.equal(ppc.MIN_TIER, ppc.RANK_TIER.metic, 'the kernel floor must DERIVE to metic');
181
188
  });
182
189
 
190
+ test('a floor raise flows through to the enforcers, not just the registry', () => {
191
+ // The registry promises a re-mapped floor reaches the substrate. Each layer
192
+ // must read the hit's OWN floor: the cooperative ones through
193
+ // checkPermissionPaths, the authoritative post-push audit through its per-hit
194
+ // comparison (task 1003191 — it used to test the global minimum and silently
195
+ // downgraded every raise).
196
+ const src = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'main-audit.js'), 'utf8');
197
+ assert.ok(!/\bppc\.isBelowFloor\s*\(/.test(src),
198
+ 'main-audit.js must not decide floors with isBelowFloor — that tests the registry '
199
+ + 'GLOBAL MINIMUM, so a governance floor raise is silently downgraded in the one '
200
+ + 'layer that catches a raw `git push origin main` (task 1003191).');
201
+ assert.ok(/RANK_TIER\[h\.floor\]/.test(src),
202
+ 'main-audit.js must compare each hit against its own registry floor');
203
+ });
204
+
183
205
  // ---------- the system-permission mapping flows from the registry ----------
184
206
 
185
207
  test('every entry maps to a permission the catalog defines as system:true', () => {
@@ -134,6 +134,101 @@ t('#863-followup (finding #6): a sub-Metic author NAME still resolves and BLOCKS
134
134
  assert.equal(r.findings[0].rank, 'thetes');
135
135
  });
136
136
 
137
+ // -- per-hit registry floors (task 1003191) --------------------------------
138
+ //
139
+ // The authoritative layer used to decide floors with ppc.isBelowFloor(rank),
140
+ // which tests the registry's GLOBAL MINIMUM floor. Every live surface floors at
141
+ // 'metic', so that was indistinguishable from a per-hit comparison — until
142
+ // governance RAISES one surface, at which point layer 3 silently downgraded the
143
+ // raise back to the minimum while the pre-push hook and the grader (both on
144
+ // checkPermissionPaths, which compares per-hit) still blocked it. Layer 3 is the
145
+ // only one that catches a raw `git push origin main`, so the authoritative net
146
+ // was the loosest of the three.
147
+ //
148
+ // A uniform-floor fixture cannot tell the two implementations apart. This is the
149
+ // shape that can: one surface raised above the minimum, one left at it.
150
+ const ppc = require('../src/bongos/permission-path-check.js');
151
+
152
+ function mixedFloorRegistry() {
153
+ const SURFACES = [
154
+ { glob: 'infra/', floor: 'archon' }, // RAISED above the global minimum
155
+ { glob: 'src/bongos/db.js', floor: 'metic' }, // still at the minimum
156
+ ];
157
+ return {
158
+ RANK_TIER: ppc.RANK_TIER,
159
+ tierOf: (rank) => ppc.RANK_TIER[rank] ?? null,
160
+ matchProtected: (files) => (files || [])
161
+ .map((f) => {
162
+ const s = SURFACES.find((x) => (x.glob.endsWith('/') ? f.startsWith(x.glob) : f === x.glob));
163
+ return s ? { file: f, glob: s.glob, floor: s.floor, permission: 'path.protected.modify' } : null;
164
+ })
165
+ .filter(Boolean),
166
+ };
167
+ }
168
+
169
+ t('a RAISED surface floor is enforced: Metic touching an archon-floored path → BLOCKER', () => {
170
+ const r = analyzeCommits([{
171
+ sha: 'r01', subject: 'touch the substrate', authorName: 'SomeMetic', authorEmail: 'm@x',
172
+ isMerge: false, files: ['infra/deploy.sh'],
173
+ }], RANKS, { registry: mixedFloorRegistry() });
174
+ assert.equal(r.hardFail, true, 'a governance floor raise must reach the authoritative layer');
175
+ assert.equal(r.findings.length, 1);
176
+ assert.deepEqual(r.findings[0].floors, ['archon']);
177
+ assert.match(r.findings[0].summary, /Archon\+ builder/);
178
+ });
179
+
180
+ t('the finding reports only the paths the author actually fails to clear', () => {
181
+ // db.js floors at the minimum this Metic DOES clear; infra/ does not. A
182
+ // per-hit comparison names the second and stays silent about the first.
183
+ const r = analyzeCommits([{
184
+ sha: 'r02', subject: 'mixed', authorName: 'SomeMetic', authorEmail: 'm@x',
185
+ isMerge: false, files: ['src/bongos/db.js', 'infra/deploy.sh'],
186
+ }], RANKS, { registry: mixedFloorRegistry() });
187
+ assert.equal(r.hardFail, true);
188
+ assert.deepEqual(r.findings[0].files, ['infra/deploy.sh']);
189
+ assert.equal(r.findings[0].summary.includes('src/bongos/db.js'), false,
190
+ 'a path the author clears must not be reported as a violation');
191
+ });
192
+
193
+ t('a raised floor does not over-block: Archon touching the same path → no finding', () => {
194
+ const r = analyzeCommits([{
195
+ sha: 'r03', subject: 'x', authorName: 'somearchon', authorEmail: 'l@x',
196
+ isMerge: false, files: ['infra/deploy.sh'],
197
+ }], RANKS, { registry: mixedFloorRegistry() });
198
+ assert.equal(r.hardFail, false);
199
+ assert.equal(r.findings.length, 0);
200
+ });
201
+
202
+ t('a Metic still clears a surface left at the minimum floor', () => {
203
+ const r = analyzeCommits([{
204
+ sha: 'r04', subject: 'x', authorName: 'SomeMetic', authorEmail: 'm@x',
205
+ isMerge: false, files: ['src/bongos/db.js'],
206
+ }], RANKS, { registry: mixedFloorRegistry() });
207
+ assert.equal(r.hardFail, false);
208
+ assert.equal(r.findings.length, 0);
209
+ });
210
+
211
+ t('an unresolved author is told the floor that actually governs, not a hardcoded Metic', () => {
212
+ const r = analyzeCommits([{
213
+ sha: 'r05', subject: 'x', authorName: 'Nobody Known', authorEmail: 'n@x',
214
+ isMerge: false, files: ['infra/deploy.sh'],
215
+ }], RANKS, { registry: mixedFloorRegistry() });
216
+ assert.equal(r.findings.length, 1);
217
+ assert.equal(r.findings[0].severity, 'review');
218
+ assert.match(r.findings[0].summary, /Archon\+ work/);
219
+ });
220
+
221
+ t('the default registry is the real one — no injection needed in production', () => {
222
+ // The seam must not change what the CLI does: infra/ floors at metic today,
223
+ // so a Metic touching it is legal on the live registry.
224
+ const r = analyzeCommits([{
225
+ sha: 'r06', subject: 'x', authorName: 'SomeMetic', authorEmail: 'm@x',
226
+ isMerge: false, files: ['infra/deploy.sh'],
227
+ }], RANKS);
228
+ assert.equal(r.hardFail, false);
229
+ assert.equal(ppc.floorFor('infra/deploy.sh'), 'metic');
230
+ });
231
+
137
232
  // -- the default range window (task 1002566) -------------------------------
138
233
  //
139
234
  // Everything above tests the pure ANALYZER. Nothing tested what FEEDS it, which
@@ -487,6 +487,56 @@ test('EXPECTED_RANKS: a MISSING pinned route trips a blocker (a protected route
487
487
  assert.equal(res.hardFail, true);
488
488
  });
489
489
 
490
+ // ---------- task 1003192: a module pin block must never go SILENTLY inert ----------
491
+ //
492
+ // From the task-1002671 adversarial verification (finding 4). Pin scope derives from
493
+ // successfully-classified routes, and the skip had no else branch: a MODULE_EXPECTED_RANKS
494
+ // key matching no discovered module was dropped without a word, while the loader kept
495
+ // mounting modules/<key>/routes/ by the same convention this scan discovers them by.
496
+ // Measured before the fix, with the security module's directory renamed: all six pins went
497
+ // inert and a POST /security/reports deliberately downgraded to any-builder drew ZERO
498
+ // findings — a pinned privileged WRITE route silently unguarded.
499
+ //
500
+ // The guard is deliberately split. checkRouteRanks fires only when the directory IS
501
+ // PRESENT but contributed no route (a rename/move under a live module); a wholly-absent
502
+ // directory stays silent here because this runs on every ship and an instance may
503
+ // legitimately omit an optional module. fitness.js Check 4b owns that half, at the source
504
+ // repo where every module is expected to exist — see tests/fitness.mjs.
505
+
506
+ test('a module whose dir EXISTS but yields no route surfaces an inert-pins finding [task 1003192]', () => {
507
+ const os = require('node:os');
508
+ const fs = require('node:fs');
509
+ const path = require('node:path');
510
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rrc-inert-'));
511
+ // modules/security/ is present, but its routes live under handlers/ — so the scan
512
+ // discovers nothing for the key and all six security pins would be skipped.
513
+ fs.mkdirSync(path.join(dir, 'security', 'handlers'), { recursive: true });
514
+ fs.writeFileSync(path.join(dir, 'security', 'handlers', 'reports.js'),
515
+ `router.post('/security/reports', auth.requireBuilder, h);\n`);
516
+ const res = rrc.checkRouteRanks({ modulesDir: dir });
517
+ fs.rmSync(dir, { recursive: true, force: true });
518
+
519
+ const inert = res.findings.find((f) => f.rank === '(inert)');
520
+ assert.ok(inert, `a present-but-routeless pinned module must surface a finding; got: ${res.findings.map((f) => f.route).join(', ')}`);
521
+ assert.equal(inert.severity, 'major');
522
+ assert.match(inert.route, /security/);
523
+ assert.match(inert.summary, /passed vacuously/);
524
+ });
525
+
526
+ test('an ABSENT optional module stays silent — no false alarm for an instance without it [task 1003192]', () => {
527
+ // The counter-test, and the reason the guard is split across two layers: this runs on
528
+ // every ship, so a module an instance legitimately does not ship must not red the board.
529
+ const os = require('node:os');
530
+ const fs = require('node:fs');
531
+ const path = require('node:path');
532
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rrc-absent-'));
533
+ // An empty modules dir: no pinned module directory exists at all.
534
+ const res = rrc.checkRouteRanks({ modulesDir: dir });
535
+ fs.rmSync(dir, { recursive: true, force: true });
536
+ assert.equal(res.findings.filter((f) => f.rank === '(inert)').length, 0,
537
+ 'a module directory that is simply absent must not raise an inert-pins finding');
538
+ });
539
+
490
540
  // ---------- LIVE: every real route classifies to a known rank ----------
491
541
 
492
542
  test('LIVE: checkRouteRanks() on src/bongos/routes/ has zero unknowns', () => {