@bongos/core 1.21.180 → 1.21.181

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.180",
6
- "core_contract": "1.21.180",
7
- "source_commit": "36dc85d6b63259779e000aebdf3dc1e0138792b0",
5
+ "core_version": "1.21.181",
6
+ "core_contract": "1.21.181",
7
+ "source_commit": "677ee2b818cf829dddd5434301b158be3408fbd5",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-26T00:58:50.603Z",
9
+ "built_at": "2026-10-26T01:16:25.738Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 616,
13
13
  "agent_docs_stubbed": 29,
14
- "functional_verbatim": 3538,
14
+ "functional_verbatim": 3539,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 4184,
20
- "tree_sha256": "fbe7e2684882ff2e854978d9a494f9058adeb5f62571e3e020009f9074f48249",
19
+ "file_count": 4185,
20
+ "tree_sha256": "e69b48d53a056c99114f3c5904e69828363e91f5f0df70b7b599e61e8e299d1d",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2937,7 +2937,7 @@
2937
2937
  {
2938
2938
  "path": "docs/module-api-changelog.md",
2939
2939
  "mode": "0000644",
2940
- "sha256": "82235d195b8ec1dae8f427ce3b500af7abcb84d3c265702ef66589f86abccc64"
2940
+ "sha256": "4835f21d9d6cf7eaa82821e1bb5eb4c79493267300ef852ad1ca1c05b4a41e65"
2941
2941
  },
2942
2942
  {
2943
2943
  "path": "docs/modules-contract.md",
@@ -10827,12 +10827,12 @@
10827
10827
  {
10828
10828
  "path": "package-lock.json",
10829
10829
  "mode": "0000644",
10830
- "sha256": "ee9b6a44618660fc762c163d437234d8113af7f3d58e135a833ba5b1bc24fc2b"
10830
+ "sha256": "4d3ecd3d1a0b7e6602da53e83e88c204ab82a45ca83ae3d24979746970f15ac9"
10831
10831
  },
10832
10832
  {
10833
10833
  "path": "package.json",
10834
10834
  "mode": "0000644",
10835
- "sha256": "d1b93e086b2f09ccba18efa17327c3ca20cec70c4b1e6f305ded43643fce1f26"
10835
+ "sha256": "2a01fd05d918fc38ad18d1cc6305681129b6115a4eab33f7d1fa4d34b8da2e44"
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": "6863c7358f68a6456d7be9d06d184442c3f3af03e8a27dab308913962649b751"
10855
+ "sha256": "2a6e6fa89b09fd51aa8fae09c92eec30b6d0823dcdf3eaeda63826e7269a46ea"
10856
10856
  },
10857
10857
  {
10858
10858
  "path": "scripts/bongos/CLAUDE.md",
@@ -12052,7 +12052,7 @@
12052
12052
  {
12053
12053
  "path": "scripts/bongos/repo-metrics.js",
12054
12054
  "mode": "0000644",
12055
- "sha256": "c1ee7d69e2ef949ecec2ff7f602c9876a835fb9853585e21aea4c2d215c9aa1f"
12055
+ "sha256": "15dee800bf6fc5aca8a9d332854825b1d0081f2269ccdf69c2b5d3e1010009cb"
12056
12056
  },
12057
12057
  {
12058
12058
  "path": "scripts/bongos/retired-core-skills.json",
@@ -14677,7 +14677,7 @@
14677
14677
  {
14678
14678
  "path": "src/module-api.js",
14679
14679
  "mode": "0000644",
14680
- "sha256": "391a575b2a36039085561dfa8e1b76080d46a678f59dcde76ebb6e1c6a829945"
14680
+ "sha256": "c48862d3a5d62afff6c50b8de356b68ec2ef4f22ced0c0caa36a99d5f135747a"
14681
14681
  },
14682
14682
  {
14683
14683
  "path": "src/module-loader/catalog.js",
@@ -19604,6 +19604,11 @@
19604
19604
  "mode": "0000644",
19605
19605
  "sha256": "5c964c162b87f2e73f6dc3343b1c4be886b7d3e0d29c01d51280dc790fc218e7"
19606
19606
  },
19607
+ {
19608
+ "path": "tests/repo_metrics_dead_exports.mjs",
19609
+ "mode": "0000644",
19610
+ "sha256": "f98a3d9635557d050f428c5ad3258ff1ef7652bf0f4fba87830cc09481b76e9b"
19611
+ },
19607
19612
  {
19608
19613
  "path": "tests/reward_gate.mjs",
19609
19614
  "mode": "0000644",
@@ -3197,5 +3197,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3197
3197
  landed since 1.21.178 with no explicit bump. run 40250732318. (task 1002620)
3198
3198
  1.21.180 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3199
3199
  landed since 1.21.179 with no explicit bump. run 40251859318. (task 1002620)
3200
+ 1.21.181 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3201
+ landed since 1.21.180 with no explicit bump. run 40253320267. (task 1002620)
3200
3202
  ---------------------------------------------------------------------------
3201
3203
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.180",
3
+ "version": "1.21.181",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.180",
9
+ "version": "1.21.181",
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.180",
3
+ "version": "1.21.181",
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",
@@ -10356,5 +10356,11 @@
10356
10356
  "id": "1004875",
10357
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
10358
  }
10359
+ ],
10360
+ "1.21.181": [
10361
+ {
10362
+ "id": "1004892",
10363
+ "text": "The repo health dashboard's dead-code number now counts the same way the CI check does, so it no longer jumps by about 1,800 false alarms from modules/."
10364
+ }
10359
10365
  ]
10360
10366
  }
@@ -37,6 +37,7 @@ const { execSync } = require('node:child_process');
37
37
  const fs = require('node:fs');
38
38
  const path = require('node:path');
39
39
  const { hasFlag } = require('./cli-lib');
40
+ const { countKnipFindings } = require('./fitness-ratchets');
40
41
 
41
42
  const REPO_ROOT = path.resolve(__dirname, '..', '..');
42
43
  const TOP_N_LARGEST = 15;
@@ -151,36 +152,44 @@ function scanDeadCode() {
151
152
  return { dead_exports: null, dead_files: null, knip_status: 'unparseable' };
152
153
  }
153
154
 
154
- // knip v6 JSON shape: { issues: [{ file, files (bool: whole file unused),
155
- // exports:[...], types:[...], ... }] }. A top-level `files` array exists in
156
- // some reporter modes; handle both. dead_exports sums unused exports+types
157
- // across issues; dead_files counts issues whose whole file is unused.
158
- let deadFiles = null;
155
+ const scored = scoreKnipReport(report);
156
+ if (!scored) return { dead_exports: null, dead_files: null, knip_status: 'unexpected_shape' };
157
+ return { ...scored, knip_status: 'ok' };
158
+ }
159
+
160
+ // knip JSON -> { dead_exports, dead_files }, or null on a shape we cannot read.
161
+ //
162
+ // dead_exports goes through the CI ratchet's own scorer, countKnipFindings, fed only
163
+ // the exports/types findings (task 1004892). Counting knip's raw list instead put
164
+ // ~1,800 exports under modules/ on the dashboard that ARE used: knip does not credit
165
+ // a CommonJS namespace use (`lib.x`), and modules/ is written that way. The scorer's
166
+ // name filter drops those, so this number and the ratchet's agree. `opts` passes
167
+ // through to the scorer (a test injects `wordIndex`).
168
+ //
169
+ // COST: the scorer builds its word index once per call (codeWordIndex walks the
170
+ // repo's .js/.mjs/.cjs/.html/.sh text) — measured ~2.3 s on this repo, beside a knip
171
+ // run that already takes far longer. It is deliberately NOT fed from
172
+ // gatherFileSizes()'s reads: that list is git-tracked .js/.mjs only, while the
173
+ // ratchet's index also reads .cjs/.html/.sh and skips docs/ — a different file set
174
+ // gives a different count, and matching CI's number is the whole point.
175
+ function scoreKnipReport(report, opts = {}) {
176
+ if (!report || typeof report !== 'object') return null;
177
+ const hasIssues = Array.isArray(report.issues);
178
+ if (!hasIssues && !Array.isArray(report.files)) return null;
159
179
  let deadExports = null;
160
- try {
161
- if (Array.isArray(report.issues)) {
162
- deadExports = report.issues.reduce((acc, issue) => {
163
- const ex = Array.isArray(issue.exports) ? issue.exports.length : 0;
164
- const ty = Array.isArray(issue.types) ? issue.types.length : 0;
165
- return acc + ex + ty;
166
- }, 0);
167
- deadFiles = report.issues.reduce(
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),
170
- 0
171
- );
172
- }
173
- // Some reporter modes additionally surface a flat unused-file list.
174
- if (Array.isArray(report.files)) {
175
- deadFiles = (deadFiles ?? 0) + report.files.length;
176
- }
177
- if (deadExports == null && deadFiles == null) {
178
- return { dead_exports: null, dead_files: null, knip_status: 'unexpected_shape' };
179
- }
180
- } catch (_) {
181
- return { dead_exports: null, dead_files: null, knip_status: 'unexpected_shape' };
180
+ let deadFiles = null;
181
+ if (hasIssues) {
182
+ const exportsOnly = report.issues.map((issue) => ({ file: issue.file, exports: issue.exports, types: issue.types }));
183
+ deadExports = countKnipFindings({ issues: exportsOnly }, opts);
184
+ deadFiles = report.issues.reduce(
185
+ // knip 6 gives an ARRAY here ([{ name }]), not a bool (task 1004875).
186
+ (acc, issue) => acc + (issue.files === true || (Array.isArray(issue.files) && issue.files.length) ? 1 : 0),
187
+ 0
188
+ );
182
189
  }
183
- return { dead_exports: deadExports, dead_files: deadFiles, knip_status: 'ok' };
190
+ // Some reporter modes additionally surface a flat unused-file list.
191
+ if (Array.isArray(report.files)) deadFiles = (deadFiles ?? 0) + report.files.length;
192
+ return { dead_exports: deadExports, dead_files: deadFiles };
184
193
  }
185
194
 
186
195
  // ---------- branch hygiene ----------
@@ -287,7 +296,7 @@ async function upsertSnapshot(pool, snap) {
287
296
 
288
297
  // ---------- main ----------
289
298
 
290
- (async () => {
299
+ async function main() {
291
300
  const args = process.argv.slice(2);
292
301
  const asJson = hasFlag('--json', args);
293
302
  const noWrite = hasFlag('--no-write', args);
@@ -360,7 +369,15 @@ async function upsertSnapshot(pool, snap) {
360
369
  }
361
370
 
362
371
  process.exit(0);
363
- })().catch((err) => {
364
- console.error('fatal:', err);
365
- process.exit(1);
366
- });
372
+ }
373
+
374
+ module.exports = { scoreKnipReport };
375
+
376
+ // CLI only when run directly, so a test can require this file without it
377
+ // scanning the repo or writing to the DB (task 1004892).
378
+ if (require.main === module) {
379
+ main().catch((err) => {
380
+ console.error('fatal:', err);
381
+ process.exit(1);
382
+ });
383
+ }
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.180'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.181'; // 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');
@@ -0,0 +1,57 @@
1
+ // tests/repo_metrics_dead_exports.mjs — repo-metrics scores dead exports the way the
2
+ // CI knip ratchet does (task 1004892).
3
+ //
4
+ // Once knip scanned modules/ (task 1004875), repo-metrics' raw export count jumped by
5
+ // ~1,800: knip does not credit a CommonJS namespace use (`lib.x`), and modules/ is
6
+ // written that way. The ratchet's scorer (countKnipFindings) filters those by name;
7
+ // repo-metrics must use the same scorer, so the dashboard number matches CI's.
8
+ // Hermetic: synthetic knip JSON and an injected word index, no knip run.
9
+ import assert from 'node:assert/strict';
10
+ import { test } from 'node:test';
11
+ import { createRequire } from 'node:module';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
+ const rm = require(path.join(ROOT, 'scripts/bongos/repo-metrics.js'));
18
+ const fr = require(path.join(ROOT, 'scripts/bongos/fitness-ratchets.js'));
19
+
20
+ const issue = (file, extra) => ({ file, files: [], exports: [], types: [], dependencies: [], unlisted: [], ...extra });
21
+
22
+ test('a modules/ export used through a namespace elsewhere is not a dead export', () => {
23
+ const wordIndex = new Map([
24
+ ['usedViaNamespace', true], // spelled in 2+ files
25
+ ['onlyHere', 'modules/x/lib.js'], // spelled only in its own file
26
+ ]);
27
+ const report = {
28
+ issues: [
29
+ issue('modules/x/lib.js', {
30
+ exports: [{ name: 'usedViaNamespace' }, { name: 'onlyHere' }],
31
+ types: [{ name: 'neverSpelled' }],
32
+ }),
33
+ issue('src/y.js', { exports: [{ name: 'usedViaNamespace' }] }), // outside modules/: knip's verdict stands
34
+ ],
35
+ };
36
+ const scored = rm.scoreKnipReport(report, { wordIndex });
37
+ assert.equal(scored.dead_exports, 3); // onlyHere + neverSpelled + the src/ one
38
+ // ...and that is exactly the ratchet scorer's count for the same export/type findings.
39
+ assert.equal(scored.dead_exports, fr.countKnipFindings(report, { wordIndex }));
40
+ });
41
+
42
+ test('non-export findings (deps, unlisted) do not leak into dead_exports; dead files still count', () => {
43
+ const report = {
44
+ issues: [
45
+ issue('src/a.js', { files: [{ name: 'src/a.js' }], dependencies: [{ name: 'left-pad' }], exports: [{ name: 'x' }] }),
46
+ issue('src/b.js', { unlisted: [{ name: 'chalk' }] }),
47
+ ],
48
+ };
49
+ const scored = rm.scoreKnipReport(report, { wordIndex: new Map() });
50
+ assert.equal(scored.dead_exports, 1);
51
+ assert.equal(scored.dead_files, 1);
52
+ });
53
+
54
+ test('a report with no issues list is an unexpected shape (null), not zero', () => {
55
+ assert.equal(rm.scoreKnipReport({}, { wordIndex: new Map() }), null);
56
+ assert.equal(rm.scoreKnipReport(null, { wordIndex: new Map() }), null);
57
+ });