@bongos/core 1.21.179 → 1.21.180

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/bongos/package-core.js",
5
- "core_version": "1.21.179",
6
- "core_contract": "1.21.179",
7
- "source_commit": "178a6fa539e3718e93bab31e4ced14262d1457a6",
5
+ "core_version": "1.21.180",
6
+ "core_contract": "1.21.180",
7
+ "source_commit": "36dc85d6b63259779e000aebdf3dc1e0138792b0",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-26T00:44:53.003Z",
9
+ "built_at": "2026-10-26T00:58:50.603Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 616,
13
13
  "agent_docs_stubbed": 29,
14
- "functional_verbatim": 3537,
14
+ "functional_verbatim": 3538,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 4183,
20
- "tree_sha256": "026997dfda1f061de2986ae1e869fbe88be80237fa31d02b7f1d2480dd9d51e4",
19
+ "file_count": 4184,
20
+ "tree_sha256": "fbe7e2684882ff2e854978d9a494f9058adeb5f62571e3e020009f9074f48249",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -472,7 +472,7 @@
472
472
  {
473
473
  "path": "docs/adr/0017-code-simplification-and-repo-cleanliness.md",
474
474
  "mode": "0000644",
475
- "sha256": "6e57ab51a44f245f1691fb9f0de43d78aa05faa0465f12659c2eb89c088b5fb8"
475
+ "sha256": "d841dd656d9678981310f69cdc277fc259fde3dd37252a8ff4612f95e777de42"
476
476
  },
477
477
  {
478
478
  "path": "docs/adr/0018-three-rank-model-goes-live.md",
@@ -2927,7 +2927,7 @@
2927
2927
  {
2928
2928
  "path": "docs/file-map.md",
2929
2929
  "mode": "0000644",
2930
- "sha256": "ad8dd9b3c01736a49e3559a3dc0f5e37120ac1b4de4318a7bba07be3609aaf6b"
2930
+ "sha256": "4e0a9cf41330f62ec1d2bacdc07a7071b4081517e60f6446fe66a33e44175ce9"
2931
2931
  },
2932
2932
  {
2933
2933
  "path": "docs/handoff-template.md",
@@ -2937,7 +2937,7 @@
2937
2937
  {
2938
2938
  "path": "docs/module-api-changelog.md",
2939
2939
  "mode": "0000644",
2940
- "sha256": "5d9dd0fe4686c1dde4914b7b17f35d182ef0e931f4e4e3a682008df73364cc94"
2940
+ "sha256": "82235d195b8ec1dae8f427ce3b500af7abcb84d3c265702ef66589f86abccc64"
2941
2941
  },
2942
2942
  {
2943
2943
  "path": "docs/modules-contract.md",
@@ -4397,7 +4397,7 @@
4397
4397
  {
4398
4398
  "path": "modules/builder-settings/display-name.js",
4399
4399
  "mode": "0000644",
4400
- "sha256": "f959d06834da24a929e22f7159f8451a4e9578e4ffe32b20bfa9301bfd47fbed"
4400
+ "sha256": "9414b08857cf2d1eba6bf43127436f86f04ed5df707a7a04027c556a9ce91a39"
4401
4401
  },
4402
4402
  {
4403
4403
  "path": "modules/builder-settings/event-sound-prefs.js",
@@ -7052,7 +7052,7 @@
7052
7052
  {
7053
7053
  "path": "modules/ideas/born-of.js",
7054
7054
  "mode": "0000644",
7055
- "sha256": "c7458b5d65de75d0eb6bafdbb0b61b156d0f7d7e25c9c82229794494338a6808"
7055
+ "sha256": "bc7e3b1121d63fc0736e0a44894997710f872548cfd0ce66ae9903bb69910bc6"
7056
7056
  },
7057
7057
  {
7058
7058
  "path": "modules/ideas/completeness.js",
@@ -7932,7 +7932,7 @@
7932
7932
  {
7933
7933
  "path": "modules/lifecycle/task-classifier.js",
7934
7934
  "mode": "0000644",
7935
- "sha256": "95907661350fece131d9063b795a10139a2f8777a0a49f7f39c50e61a9568a1d"
7935
+ "sha256": "ec331ba544e199ec80c3cbc08bc4eb6bb9f6fba1e4aa70f9bd02c1402647f7dc"
7936
7936
  },
7937
7937
  {
7938
7938
  "path": "modules/lifecycle/task-list-limits.js",
@@ -10342,7 +10342,7 @@
10342
10342
  {
10343
10343
  "path": "modules/specialities/db.js",
10344
10344
  "mode": "0000644",
10345
- "sha256": "c3ef28d95b6d93b0d302c2705522550b701931deb932016ee2134b03eadff358"
10345
+ "sha256": "4badd0141e04154a73cf1e9bde4a1a794a756e0ea09f5b7d19c1c8dbcfa3fb0b"
10346
10346
  },
10347
10347
  {
10348
10348
  "path": "modules/specialities/migrations/specialities_001_specialities.sql",
@@ -10372,7 +10372,7 @@
10372
10372
  {
10373
10373
  "path": "modules/specialities/skills.js",
10374
10374
  "mode": "0000644",
10375
- "sha256": "be3948128b557774f65a02f5b12497979d4c38334552947b8c4ef9c47e8562a2"
10375
+ "sha256": "eb1663db43e9e7a1b83313c3c1619ea4dabe011edbf86910d55e95bd3e1247c4"
10376
10376
  },
10377
10377
  {
10378
10378
  "path": "modules/specialities/specialities.js",
@@ -10827,12 +10827,12 @@
10827
10827
  {
10828
10828
  "path": "package-lock.json",
10829
10829
  "mode": "0000644",
10830
- "sha256": "a310406cb88d9fa7053aa0002367fc0d02e00f0fa3a566353e7ec4d93700ee0f"
10830
+ "sha256": "ee9b6a44618660fc762c163d437234d8113af7f3d58e135a833ba5b1bc24fc2b"
10831
10831
  },
10832
10832
  {
10833
10833
  "path": "package.json",
10834
10834
  "mode": "0000644",
10835
- "sha256": "78692b77c9c7abc874c9237014f3953b714f1b4d90b7102b323a48cff93c41ed"
10835
+ "sha256": "d1b93e086b2f09ccba18efa17327c3ca20cec70c4b1e6f305ded43643fce1f26"
10836
10836
  },
10837
10837
  {
10838
10838
  "path": "public-docs/index.html",
@@ -10852,7 +10852,7 @@
10852
10852
  {
10853
10853
  "path": "release-notes.json",
10854
10854
  "mode": "0000644",
10855
- "sha256": "9c2abac99e107bc56a08f65863cf2367ceeceb9c7a4823e41db7e9805c829e4c"
10855
+ "sha256": "6863c7358f68a6456d7be9d06d184442c3f3af03e8a27dab308913962649b751"
10856
10856
  },
10857
10857
  {
10858
10858
  "path": "scripts/bongos/CLAUDE.md",
@@ -11397,7 +11397,7 @@
11397
11397
  {
11398
11398
  "path": "scripts/bongos/fitness-ratchets.js",
11399
11399
  "mode": "0000644",
11400
- "sha256": "15c1d62a515cf0ee1b651f33233e22f66144c88a20680bcb6b90ca3b73ddb81f"
11400
+ "sha256": "29c8c99a1380a0df741338feef8cc0465e22b12c45ba7655584a6a24e48987ac"
11401
11401
  },
11402
11402
  {
11403
11403
  "path": "scripts/bongos/fitness.js",
@@ -11967,7 +11967,7 @@
11967
11967
  {
11968
11968
  "path": "scripts/bongos/pulse.js",
11969
11969
  "mode": "0000644",
11970
- "sha256": "c0ae5acf5576014c44c488e7f4c195cf69fbfab420311f4726424a8f9dec024e"
11970
+ "sha256": "908648ffc796e4eecda652333a67626a2ec206dd394038dab4bf981c5a5272d5"
11971
11971
  },
11972
11972
  {
11973
11973
  "path": "scripts/bongos/push-main.js",
@@ -12052,7 +12052,7 @@
12052
12052
  {
12053
12053
  "path": "scripts/bongos/repo-metrics.js",
12054
12054
  "mode": "0000644",
12055
- "sha256": "b68dc127a8ef9eacd4abd7f90eef05ff4bfe788b6770ae2b517759b399c50dd3"
12055
+ "sha256": "c1ee7d69e2ef949ecec2ff7f602c9876a835fb9853585e21aea4c2d215c9aa1f"
12056
12056
  },
12057
12057
  {
12058
12058
  "path": "scripts/bongos/retired-core-skills.json",
@@ -14422,7 +14422,7 @@
14422
14422
  {
14423
14423
  "path": "src/bongos/module-scope-map.js",
14424
14424
  "mode": "0000644",
14425
- "sha256": "93d9e5e91b4caf90bc2f1955be557fa7ba17105e5a8e3f1d94f9651ff4e1925c"
14425
+ "sha256": "b9dd189b2035885f8ccb205de20d45409380f16f1110de50447acd9ff866f52a"
14426
14426
  },
14427
14427
  {
14428
14428
  "path": "src/bongos/module-store.js",
@@ -14677,7 +14677,7 @@
14677
14677
  {
14678
14678
  "path": "src/module-api.js",
14679
14679
  "mode": "0000644",
14680
- "sha256": "622387442066f1d43a8f2514f706770b6c3fb6972e7bb60642da1a7b31117c4d"
14680
+ "sha256": "391a575b2a36039085561dfa8e1b76080d46a678f59dcde76ebb6e1c6a829945"
14681
14681
  },
14682
14682
  {
14683
14683
  "path": "src/module-loader/catalog.js",
@@ -14972,7 +14972,7 @@
14972
14972
  {
14973
14973
  "path": "tests/applicant_profile_boundary.mjs",
14974
14974
  "mode": "0000644",
14975
- "sha256": "f7c94079d3c85cc6513dba591111f81fa45115e25d32469d67e949a8e7abee3d"
14975
+ "sha256": "3bde3c999f6ef1b1cdcc3185450c7e8e03d819d621ee5684ebee94536299b0fd"
14976
14976
  },
14977
14977
  {
14978
14978
  "path": "tests/applicant_profile_read_bounds.mjs",
@@ -18009,6 +18009,11 @@
18009
18009
  "mode": "0000644",
18010
18010
  "sha256": "9448a42c0a10144d9e9cd96aa7ae894db6a2e30319be1ab5013c3b52d2ad7496"
18011
18011
  },
18012
+ {
18013
+ "path": "tests/knip_modules_scope.mjs",
18014
+ "mode": "0000644",
18015
+ "sha256": "6dadd33910d25806110296d916e5a0301c9e7bb4baa69103b71fdfea94ad3ac7"
18016
+ },
18012
18017
  {
18013
18018
  "path": "tests/knip_ratchet_ambient.mjs",
18014
18019
  "mode": "0000644",
@@ -37,6 +37,7 @@ We will adopt **knip** (dev-only) as the one tool the substrate and janitor lean
37
37
  - **It targets the exact failure mode.** Unused exports and unreferenced files are the residue of AI over-writing — code written, then orphaned. Reliable dead-file/dead-export detection is genuinely hard to hand-roll (it requires building and walking the import graph); knip does it.
38
38
  - **Zero production cost.** knip is in `devDependencies`. The droplet deploy runs `npm ci --omit=dev`, so knip and its dependency tree never reach production. No runtime, no monthly cost — consistent with the $5K budget posture. (Its transitive dev-tree does carry npm-audit advisories; these are dev-only and are the concern of the dependency-scan task V3.R38, not prod.)
39
39
  - **Config lives in `package.json`, not a new file.** knip reads a `"knip"` key in `package.json`. Embedding it there — rather than adding a root `knip.json` — keeps the footprint minimal, which is the very ethos this ADR encodes. (The task description floated `knip.json`; the package.json key is the simpler in-scope choice and is what shipped.)
40
+ - **Amended 2026-10-25 (task 1004875):** the config moved to a root `knip.config.js`. Bringing `modules/` into scope needs entry points GENERATED from each `module.json` (routes, pollers, web surfaces) — the module loader finds those files by name, so a static JSON key would have to be a hand list that rots. The package.json key was removed so there is one config, not two.
40
41
 
41
42
  The committed config declares entry points (CLI scripts, tests, the browser `public/game/main.js`, the status/builders pages) and the project globs, and lists `phaser` + `colyseus.js` under `ignoreDependencies` because those are loaded as vendored browser globals via `<script>` tags (`public/index.html`), not statically imported — so knip would otherwise report them as unused. The config was trimmed to remove every pattern knip flagged as redundant; it now emits no configuration hints.
42
43
 
package/docs/file-map.md CHANGED
@@ -14,6 +14,7 @@
14
14
  ├── .impeccable/ ← impeccable skill artifacts: config.json (buildPath: code), surfaces/<route>.md (per-route briefs), mocks/world-assets/ (Higgsfield renders + prompt sidecars; the SOURCE of modules/public-landing/public/assets/world/), mocks/decision/ (A1 comps), design.json (DESIGN.md sidecar), review/ (finish captures)
15
15
  ├── .gitignore
16
16
  ├── package.json / package-lock.json
17
+ ├── knip.config.js ← dead-code scanner config (task 1004875): modules/ entries GENERATED from each module.json + #! CLIs; scored by scripts/bongos/fitness-ratchets.js --knip
17
18
  ├── CODEOWNERS ← owner review on the merge-gate surfaces (real since task 1002611, ADR 0159); inert until branch protection is on (scripts/bongos/gating.js) — keep aligned with scripts/bongos/hard-floor.js HARD_FLOOR_GLOBS
18
19
  ├── server.js ← game boot: Express + Colyseus + the shared internal surface (mountInternalSurfaces), binds 127.0.0.1:3000
19
20
  │
@@ -3195,5 +3195,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3195
3195
  landed since 1.21.177 with no explicit bump. run 40249608959. (task 1002620)
3196
3196
  1.21.179 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3197
3197
  landed since 1.21.178 with no explicit bump. run 40250732318. (task 1002620)
3198
+ 1.21.180 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3199
+ landed since 1.21.179 with no explicit bump. run 40251859318. (task 1002620)
3198
3200
  ---------------------------------------------------------------------------
3199
3201
  ```
@@ -24,7 +24,6 @@
24
24
  // The longest display name we accept, in Unicode code points (so a multi-byte
25
25
  // character counts as one, matching how a human reads the limit).
26
26
  const MAX_LENGTH = 60;
27
- const MIN_LENGTH = 1; // after trimming — empty / whitespace-only is rejected
28
27
 
29
28
  // Count code points, not UTF-16 units, so an emoji or astral character is "1".
30
29
  function codePointLength(str) {
@@ -77,7 +76,6 @@ function validate(name) {
77
76
 
78
77
  module.exports = {
79
78
  MAX_LENGTH,
80
- MIN_LENGTH,
81
79
  codePointLength,
82
80
  hasControlChar,
83
81
  validate,
@@ -26,8 +26,6 @@
26
26
  const api = require('../../src/module-api');
27
27
  const { pool } = api;
28
28
 
29
- function emptyBorn() { return { goals: {}, criteria: {} }; }
30
-
31
29
  // readBornOf — the two reads, folded first-wins into plain objects keyed by the
32
30
  // born thing's id. deps.pool honours the test seam.
33
31
  async function readBornOf(deps = {}) {
@@ -52,4 +50,4 @@ async function readBornOf(deps = {}) {
52
50
  return { goals: firstWins(goals.rows, 'goal_id'), criteria: firstWins(criteria.rows, 'criterion_id') };
53
51
  }
54
52
 
55
- module.exports = { readBornOf, emptyBorn };
53
+ module.exports = { readBornOf };
@@ -71,7 +71,6 @@ function blockerVendorRule() {
71
71
  }
72
72
  return _blockerVendorRe;
73
73
  }
74
- const _resetVendorCacheForTests = () => { _blockerVendorRe = undefined; };
75
74
 
76
75
  function extractKindTag(description) {
77
76
  if (!description) return null;
@@ -549,7 +548,6 @@ module.exports = {
549
548
  classifyTaskKind,
550
549
  classifyTaskDiscipline,
551
550
  buildBlockerVendorRe,
552
- _resetVendorCacheForTests,
553
551
  VALID_KINDS,
554
552
  VALID_DISCIPLINES,
555
553
  WRITABLE_DISCIPLINES,
@@ -113,16 +113,6 @@ async function listDocuments(specialityId, executor) {
113
113
  return r.rows;
114
114
  }
115
115
 
116
- async function getDocument(specialityId, docId, executor) {
117
- const q = exec(executor);
118
- const r = await q(
119
- `SELECT id, speciality_id, title, body_md, source, content_sha, created_at
120
- FROM specialities_documents WHERE speciality_id = $1 AND id = $2`,
121
- [specialityId, docId],
122
- );
123
- return r.rows[0] || null;
124
- }
125
-
126
116
  async function deleteDocument(specialityId, docId, executor) {
127
117
  const q = exec(executor);
128
118
  const r = await q(
@@ -286,7 +276,7 @@ async function listOffered({ builderId, projectIds = [], discipline = null }, ex
286
276
  module.exports = {
287
277
  UPDATABLE, taskDiscipline, setOffered, listOffered,
288
278
  createSpeciality, getSpeciality, listVisibleTo, updateSpeciality,
289
- addDocument, listDocuments, getDocument, deleteDocument,
279
+ addDocument, listDocuments, deleteDocument,
290
280
  linkLearning, countTraining,
291
281
  adopt, abandon, deactivateCraft, activeForCraft, listAdoptions, setEnabledSkills,
292
282
  };
@@ -70,12 +70,6 @@ function readFrontmatter(skillMd) {
70
70
  try { return parseFrontmatter(fs.readFileSync(skillMd, 'utf8')); } catch { return {}; }
71
71
  }
72
72
 
73
- // The description a SKILL.md gives, for the adoption walkthrough (task 1004073).
74
- function readDescription(skillMd) {
75
- const d = readFrontmatter(skillMd).description;
76
- return typeof d === 'string' ? d : '';
77
- }
78
-
79
73
  // The optional PEOPLE-FACING fields (task 1004073). `description` is written to
80
74
  // be MATCHED by a model, so a skill's author may also say, for the person
81
75
  // deciding whether to turn it on: what it does (`plain`), when to reach for it
@@ -193,4 +187,4 @@ function installedSkillsCached(opts, now = Date.now()) {
193
187
  }
194
188
  function _resetCache() { cached = null; }
195
189
 
196
- module.exports = { SKILL_NAME_RE, installedSkills, installedSkillsCached, readDescription, parseFrontmatter, explainSkill, _resetCache };
190
+ module.exports = { SKILL_NAME_RE, installedSkills, installedSkillsCached, parseFrontmatter, explainSkill, _resetCache };
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.179",
3
+ "version": "1.21.180",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.179",
9
+ "version": "1.21.180",
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.21.179",
3
+ "version": "1.21.180",
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",
@@ -10350,5 +10350,11 @@
10350
10350
  "id": "1004877",
10351
10351
  "text": "The Work page no longer re-downloads the whole task list every 20 seconds: it updates within about a second when something actually changes, and an idle tab now asks 5 times in 10 minutes instead of 30."
10352
10352
  }
10353
+ ],
10354
+ "1.21.180": [
10355
+ {
10356
+ "id": "1004875",
10357
+ "text": "The unused-code check now covers modules/, the largest part of the codebase, and fails the build when someone adds a dead file or an unused export there. Five dead exports were removed along the way."
10358
+ }
10353
10359
  ]
10354
10360
  }
@@ -240,6 +240,85 @@ function localKnipPath() {
240
240
  let knipSkipReason = null;
241
241
  function lastKnipSkipReason() { return knipSkipReason; }
242
242
 
243
+ // ---- scoring knip's JSON (task 1004875) ----
244
+ //
245
+ // TWO THINGS THE OLD INLINE COUNT GOT WRONG, both found by putting modules/ in scope:
246
+ //
247
+ // 1. UNUSED FILES WERE NEVER COUNTED. It read a top-level `files` array; knip 6's
248
+ // JSON reporter has none — an unused file is an issue whose own `files` array is
249
+ // non-empty. So a brand-new dead file anywhere scored 0 and the ratchet waved it
250
+ // through. `files` is now one of the per-issue keys (the legacy top-level array is
251
+ // still read, for an older knip).
252
+ //
253
+ // 2. knip DOES NOT CREDIT A CommonJS NAMESPACE USE. Measured on a two-file repro:
254
+ // `const a = require('./a'); a.used()` leaves `used` reported as an unused export;
255
+ // only a destructured `const { used } = require('./a')` counts. src/ is mostly
256
+ // destructured (module-api.js says so), but modules/ is written the namespace way,
257
+ // so knip reports ~1,600 exports there that ARE used. Counting those would make
258
+ // every ordinary new export under modules/ red CI. So for a finding under
259
+ // modules/, an unused export counts only when its NAME appears in no other code
260
+ // file at all (a word match over .js/.mjs/.cjs/.html/.sh). That is the case knip
261
+ // is right about: exported, and nothing else even spells it. It under-counts (a
262
+ // name that happens to be spelled elsewhere hides) and never over-counts, which
263
+ // is the direction a gate that fails CI must err in. src/, scripts/ and tests/
264
+ // keep knip's own verdict, so their part of the baseline is unchanged.
265
+ const KNIP_ISSUE_KEYS = ['files', 'dependencies', 'devDependencies', 'optionalPeerDependencies', 'unlisted', 'binaries', 'unresolved', 'exports', 'types', 'nsExports', 'nsTypes', 'duplicates'];
266
+ const NAME_FILTERED_PREFIX = 'modules/';
267
+ const WORD_SCAN_EXT_RE = /\.(m?js|cjs|html|sh)$/;
268
+ const WORD_SCAN_SKIP = new Set(['node_modules', '.git', 'docs', 'public-docs', 'worktrees']);
269
+ const WORD_RE = /[A-Za-z_$][\w$]*/g;
270
+
271
+ // word -> the one repo-relative file that uses it, or true when 2+ files do.
272
+ function codeWordIndex(root = ROOT) {
273
+ const index = new Map();
274
+ const walk = (dir) => {
275
+ let ents = [];
276
+ try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
277
+ for (const e of ents) {
278
+ if (WORD_SCAN_SKIP.has(e.name)) continue;
279
+ const abs = path.join(dir, e.name);
280
+ if (e.isDirectory()) { walk(abs); continue; }
281
+ if (!WORD_SCAN_EXT_RE.test(e.name)) continue;
282
+ let text;
283
+ try { text = fs.readFileSync(abs, 'utf8'); } catch { continue; }
284
+ const rel = path.relative(root, abs).split(path.sep).join('/');
285
+ for (const w of new Set(text.match(WORD_RE) || [])) {
286
+ const seen = index.get(w);
287
+ if (seen === undefined) index.set(w, rel);
288
+ else if (seen !== true && seen !== rel) index.set(w, true);
289
+ }
290
+ }
291
+ };
292
+ walk(root);
293
+ return index;
294
+ }
295
+
296
+ function nameUsedOutside(index, name, file) {
297
+ const seen = index.get(name);
298
+ return seen === true || (seen !== undefined && seen !== file);
299
+ }
300
+
301
+ // knip JSON -> the integer the ratchet scores. `wordIndex` is built on demand, and
302
+ // only when a modules/ export finding needs it.
303
+ function countKnipFindings(json, { root = ROOT, wordIndex = null } = {}) {
304
+ let total = Array.isArray(json && json.files) ? json.files.length : 0;
305
+ let index = wordIndex;
306
+ for (const issue of (json && json.issues) || []) {
307
+ const filtered = typeof issue.file === 'string' && issue.file.startsWith(NAME_FILTERED_PREFIX);
308
+ for (const key of KNIP_ISSUE_KEYS) {
309
+ const v = issue[key];
310
+ if (!Array.isArray(v)) continue;
311
+ if (filtered && (key === 'exports' || key === 'types')) {
312
+ if (!index) index = codeWordIndex(root);
313
+ total += v.filter((e) => !(e && nameUsedOutside(index, e.name, issue.file))).length;
314
+ } else {
315
+ total += v.length;
316
+ }
317
+ }
318
+ }
319
+ return total;
320
+ }
321
+
243
322
  function knipIssueCount() {
244
323
  knipSkipReason = null;
245
324
  if (!localKnipPath()) {
@@ -259,22 +338,14 @@ function knipIssueCount() {
259
338
  if (e && e.stdout) { raw = e.stdout.toString(); }
260
339
  else { knipSkipReason = `npx knip failed to run in ${ROOT}`; return null; }
261
340
  }
341
+ let j;
262
342
  try {
263
- const j = JSON.parse(raw);
264
- // knip's json reporter: { files: [...], issues: [{ file, owners, exports:[], types:[], ... }] }
265
- const files = Array.isArray(j.files) ? j.files.length : 0;
266
- let items = 0;
267
- for (const issue of j.issues || []) {
268
- for (const key of ['dependencies', 'devDependencies', 'optionalPeerDependencies', 'unlisted', 'binaries', 'unresolved', 'exports', 'types', 'nsExports', 'nsTypes', 'duplicates']) {
269
- const v = issue[key];
270
- items += Array.isArray(v) ? v.length : 0;
271
- }
272
- }
273
- return files + items;
343
+ j = JSON.parse(raw);
274
344
  } catch {
275
345
  knipSkipReason = 'knip produced output this script could not parse as JSON';
276
346
  return null;
277
347
  }
348
+ return countKnipFindings(j);
278
349
  }
279
350
 
280
351
  // ---------- baseline + ratchet ----------
@@ -586,6 +657,8 @@ module.exports = {
586
657
  consoleCallCount,
587
658
  isServerPath,
588
659
  knipIssueCount,
660
+ countKnipFindings,
661
+ codeWordIndex,
589
662
  localKnipPath,
590
663
  lastKnipSkipReason,
591
664
  OVERSIZE_LINES,
@@ -71,12 +71,14 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
71
71
  // droplet machine without Postgres reachable).
72
72
  function loadDeps() {
73
73
  // eslint-disable-next-line global-require
74
- const { pool } = require(path.join(REPO_ROOT, 'src/bongos/pool'));
74
+ const { pool } = require('../../src/bongos/pool');
75
75
  // eslint-disable-next-line global-require
76
76
  // pulse.js (the lib) carved → modules/sessions/ (BV1.R82 / ADR 0093 §1); the CLI
77
77
  // shell stays here and requires the module lib directly (scripts are outside the
78
- // kernel boundary, so a direct module require is fine).
79
- const { sweepStaleClaims } = require(path.join(REPO_ROOT, 'modules/sessions/pulse'));
78
+ // kernel boundary, so a direct module require is fine). A LITERAL path, not
79
+ // path.join: the dead-code scanner (knip) cannot follow a computed require, and
80
+ // reported the lib as an unused file while this was its only caller (task 1004875).
81
+ const { sweepStaleClaims } = require('../../modules/sessions/pulse');
80
82
  return { pool, sweepStaleClaims };
81
83
  }
82
84
 
@@ -121,7 +121,9 @@ function scanDeadCode() {
121
121
  configured =
122
122
  !!pkg.knip ||
123
123
  fs.existsSync(path.join(REPO_ROOT, 'knip.json')) ||
124
- fs.existsSync(path.join(REPO_ROOT, 'knip.jsonc'));
124
+ fs.existsSync(path.join(REPO_ROOT, 'knip.jsonc')) ||
125
+ // The config moved to a JS file so it can generate modules/ entries (task 1004875).
126
+ fs.existsSync(path.join(REPO_ROOT, 'knip.config.js'));
125
127
  } catch (_) {
126
128
  configured = false;
127
129
  }
@@ -163,7 +165,8 @@ function scanDeadCode() {
163
165
  return acc + ex + ty;
164
166
  }, 0);
165
167
  deadFiles = report.issues.reduce(
166
- (acc, issue) => acc + (issue.files === true ? 1 : 0),
168
+ // knip 6 gives an ARRAY here ([{ name }]), not a bool (task 1004875).
169
+ (acc, issue) => acc + (issue.files === true || (Array.isArray(issue.files) && issue.files.length) ? 1 : 0),
167
170
  0
168
171
  );
169
172
  }
@@ -427,6 +427,9 @@ const MODULE_GLOBS = {
427
427
  // scanner config above (task 1004146).
428
428
  '.gitignore', '.env.local.example',
429
429
  'package.json', 'package-lock.json',
430
+ // The dead-code scanner's config, moved out of package.json's "knip" key so it
431
+ // can generate modules/ entries from each module.json (task 1004875).
432
+ 'knip.config.js',
430
433
  'scripts/bongos/fitness.js', 'scripts/bongos/docs-entropy.js',
431
434
  'scripts/bongos/redteam-patrol.js', 'scripts/bongos/audit-gate.js',
432
435
  'scripts/bongos/fitness-ratchets.js', 'scripts/bongos/control-manifest.js',
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.179'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.180'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -737,8 +737,14 @@ test('this proof runs in a lane that actually executes', () => {
737
737
  const listed = runners.some((f) => {
738
738
  try { return fs.readFileSync(path.join(process.cwd(), 'tests', f), 'utf8').includes(self); } catch { return false; }
739
739
  });
740
- const globbed = fs.existsSync(path.join(process.cwd(), 'package.json'))
741
- && /tests\/\*|tests\/\*\*|\*\.mjs/.test(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
740
+ // `npm test` is scripts/bongos/run-unit-tests.js, which readdir()s EVERY
741
+ // tests/*.mjs into the unit or the integration lane — no allowlist. (This used to
742
+ // regex package.json for a tests/** glob, which only ever matched the knip
743
+ // config's entry list; task 1004875 moved that out and the check went red.)
744
+ let testScript = '';
745
+ try { testScript = (JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')).scripts || {}).test || ''; } catch { /* no package.json */ }
746
+ const globbed = /scripts\/bongos\/run-unit-tests\.js/.test(testScript)
747
+ && /readdirSync\(TESTS_DIR\)/.test(fs.readFileSync(path.join(process.cwd(), 'scripts/bongos/run-unit-tests.js'), 'utf8'));
742
748
  assert.ok(listed || globbed,
743
749
  `${self} is in no runner list and no glob — it would assert nothing forever (learning 1000218)`);
744
750
  });
@@ -0,0 +1,145 @@
1
+ // tests/knip_modules_scope.mjs — the dead-code scanner covers modules/ (task 1004875).
2
+ //
3
+ // Three layers:
4
+ // 1. the scorer (countKnipFindings) on synthetic knip JSON — hermetic;
5
+ // 2. the generated entry list (knip.config.js) against the real modules/ tree —
6
+ // every route and poller a manifest declares is an entry, by the loader's rule;
7
+ // 3. REAL knip on a throwaway fixture repo that uses the real config builder: a
8
+ // new unused file, and a new unused export, under modules/ each raise the score
9
+ // and the ratchet then fails. That layer needs knip in THIS checkout's
10
+ // node_modules (CI's `npm ci` provides it); without it, it skips and says why.
11
+ import assert from 'node:assert/strict';
12
+ import { test } from 'node:test';
13
+ import { createRequire } from 'node:module';
14
+ import { execFileSync } from 'node:child_process';
15
+ import fs from 'node:fs';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
+ const fr = require(path.join(ROOT, 'scripts/bongos/fitness-ratchets.js'));
23
+ const knipConfig = require(path.join(ROOT, 'knip.config.js'));
24
+
25
+ const issue = (file, extra) => ({ file, files: [], exports: [], types: [], dependencies: [], unlisted: [], ...extra });
26
+
27
+ test('scorer: an unused FILE counts (knip 6 reports it per issue, not top-level)', () => {
28
+ const json = { issues: [issue('modules/x/orphan.js', { files: [{ name: 'modules/x/orphan.js' }] })] };
29
+ assert.equal(fr.countKnipFindings(json, { wordIndex: new Map() }), 1);
30
+ });
31
+
32
+ test('scorer: a modules/ export named by no other code file counts; one named elsewhere does not', () => {
33
+ const wordIndex = new Map([
34
+ ['usedViaNamespace', true], // two+ files spell it
35
+ ['onlyHere', 'modules/x/lib.js'], // only its own file
36
+ ['spelledInConsumer', 'modules/y/consumer.js'], // one OTHER file
37
+ ]);
38
+ const json = {
39
+ issues: [issue('modules/x/lib.js', {
40
+ exports: [{ name: 'usedViaNamespace' }, { name: 'onlyHere' }, { name: 'spelledInConsumer' }, { name: 'neverSpelled' }],
41
+ })],
42
+ };
43
+ assert.equal(fr.countKnipFindings(json, { wordIndex }), 2);
44
+ });
45
+
46
+ test('scorer: src/ exports keep knip\'s own verdict (no name filter outside modules/)', () => {
47
+ const wordIndex = new Map([['usedViaNamespace', true]]);
48
+ const json = { issues: [issue('src/x.js', { exports: [{ name: 'usedViaNamespace' }] })] };
49
+ assert.equal(fr.countKnipFindings(json, { wordIndex }), 1);
50
+ });
51
+
52
+ test('config: every manifest-declared route and poller is an entry, and the file exists', () => {
53
+ const entries = new Set(knipConfig.generatedModuleEntries(ROOT));
54
+ const modulesDir = path.join(ROOT, 'modules');
55
+ let checked = 0;
56
+ for (const key of fs.readdirSync(modulesDir)) {
57
+ const mf = path.join(modulesDir, key, 'module.json');
58
+ if (!fs.existsSync(mf)) continue;
59
+ const c = JSON.parse(fs.readFileSync(mf, 'utf8')).contributes || {};
60
+ for (const [dir, list] of [['routes', c.routes], ['pollers', c.pollers]]) {
61
+ for (const k of list || []) {
62
+ const rel = `modules/${key}/${dir}/${k}.js`;
63
+ assert.ok(entries.has(rel), `${rel} is declared in ${key}/module.json but is not a knip entry`);
64
+ assert.ok(fs.existsSync(path.join(ROOT, rel)), `${rel} is declared but missing`);
65
+ checked++;
66
+ }
67
+ }
68
+ }
69
+ assert.ok(checked > 50, `expected the real tree's routes+pollers, saw ${checked}`);
70
+ assert.ok(knipConfig.project.includes('modules/**/*.{js,mjs}'), 'modules/ must be in knip "project"');
71
+ assert.ok(entries.has('modules/ui-design/kit/render.js'), 'a #! CLI under modules/ is an entry');
72
+ });
73
+
74
+ // ---------- real knip on a fixture ----------
75
+
76
+ const KNIP_BIN = path.join(ROOT, 'node_modules', 'knip', 'bin', 'knip.js');
77
+ const HAVE_KNIP = fs.existsSync(KNIP_BIN);
78
+
79
+ function write(root, rel, text) {
80
+ const abs = path.join(root, rel);
81
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
82
+ fs.writeFileSync(abs, text);
83
+ }
84
+
85
+ function makeFixture() {
86
+ // realpath: on macOS os.tmpdir() is a /var -> /private/var symlink, and knip then
87
+ // resolves a require to a path outside its own project set and calls the target an
88
+ // unused file.
89
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'knip-modules-')));
90
+ write(dir, 'package.json', JSON.stringify({ name: 'knip-fixture', version: '1.0.0', private: true }));
91
+ // The fixture uses the REAL config builder, pointed at itself.
92
+ write(dir, 'knip.config.js',
93
+ `module.exports = require(${JSON.stringify(path.join(ROOT, 'knip.config.js'))}).buildConfig(__dirname);\n`);
94
+ write(dir, 'modules/demo/module.json', JSON.stringify({ key: 'demo', contributes: { routes: ['demo'] } }));
95
+ // The route requires the lib the namespace way — the shape knip is blind to.
96
+ write(dir, 'modules/demo/routes/demo.js',
97
+ "'use strict';\nconst lib = require('../lib');\nmodule.exports = function route() { return lib.hello(); };\n");
98
+ write(dir, 'modules/demo/lib.js',
99
+ "'use strict';\nfunction hello() { return 'hi'; }\nmodule.exports = { hello };\n");
100
+ return dir;
101
+ }
102
+
103
+ function score(dir) {
104
+ const raw = execFileSync(process.execPath, [KNIP_BIN, '--directory', dir, '--reporter', 'json', '--no-exit-code'], {
105
+ cwd: dir, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'],
106
+ });
107
+ return fr.countKnipFindings(JSON.parse(raw), { root: dir });
108
+ }
109
+
110
+ const ratchetFails = (value, baseline) =>
111
+ fr.compareToBaselines({ knip_issue_count: value }, { knip_issue_count: baseline }).violations.length > 0;
112
+
113
+ test('real knip: a new unused file or export under modules/ fails the ratchet; a namespace-used export does not',
114
+ { skip: HAVE_KNIP ? false : `no knip at ${KNIP_BIN} — run \`npm ci\` (CI always has it)` }, () => {
115
+ const dir = makeFixture();
116
+ try {
117
+ const baseline = score(dir);
118
+ assert.equal(baseline, 0, 'the clean fixture scores 0 — the route is a generated entry, lib.hello is namespace-used');
119
+ assert.equal(ratchetFails(baseline, baseline), false);
120
+
121
+ // A new export that the route uses through the namespace: knip alone flags it;
122
+ // the scorer must not, or every ordinary export under modules/ would red CI.
123
+ write(dir, 'modules/demo/lib.js',
124
+ "'use strict';\nfunction hello() { return 'hi'; }\nfunction greet() { return 'yo'; }\nmodule.exports = { hello, greet };\n");
125
+ write(dir, 'modules/demo/routes/demo.js',
126
+ "'use strict';\nconst lib = require('../lib');\nmodule.exports = function route() { return lib.hello() + lib.greet(); };\n");
127
+ assert.equal(score(dir), baseline, 'a namespace-used export is not a finding');
128
+
129
+ // A new unused EXPORT.
130
+ write(dir, 'modules/demo/lib.js',
131
+ "'use strict';\nfunction hello() { return 'hi'; }\nfunction greet() { return 'yo'; }\n"
132
+ + "function zzDeadExport() { return 0; }\nmodule.exports = { hello, greet, zzDeadExport };\n");
133
+ const withExport = score(dir);
134
+ assert.equal(withExport, baseline + 1);
135
+ assert.equal(ratchetFails(withExport, baseline), true, 'an unused export under modules/ must fail the ratchet');
136
+
137
+ // A new unused FILE.
138
+ write(dir, 'modules/demo/orphan.js', "'use strict';\nmodule.exports = { nobodyCalls() { return 1; } };\n");
139
+ const withFile = score(dir);
140
+ assert.equal(withFile, withExport + 1);
141
+ assert.equal(ratchetFails(withFile, withExport), true, 'an unused file under modules/ must fail the ratchet');
142
+ } finally {
143
+ fs.rmSync(dir, { recursive: true, force: true });
144
+ }
145
+ });