@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 +29 -24
- package/docs/adr/0017-code-simplification-and-repo-cleanliness.md +1 -0
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +2 -0
- package/modules/builder-settings/display-name.js +0 -2
- package/modules/ideas/born-of.js +1 -3
- package/modules/lifecycle/task-classifier.js +0 -2
- package/modules/specialities/db.js +1 -11
- package/modules/specialities/skills.js +1 -7
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/bongos/fitness-ratchets.js +84 -11
- package/scripts/bongos/pulse.js +5 -3
- package/scripts/bongos/repo-metrics.js +5 -2
- package/src/bongos/module-scope-map.js +3 -0
- package/src/module-api.js +1 -1
- package/tests/applicant_profile_boundary.mjs +8 -2
- package/tests/knip_modules_scope.mjs +145 -0
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.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
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:
|
|
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":
|
|
14
|
+
"functional_verbatim": 3538,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
10830
|
+
"sha256": "ee9b6a44618660fc762c163d437234d8113af7f3d58e135a833ba5b1bc24fc2b"
|
|
10831
10831
|
},
|
|
10832
10832
|
{
|
|
10833
10833
|
"path": "package.json",
|
|
10834
10834
|
"mode": "0000644",
|
|
10835
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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,
|
package/modules/ideas/born-of.js
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/scripts/bongos/pulse.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
741
|
-
|
|
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
|
+
});
|