@bongos/core 1.19.755 → 1.19.756

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.755",
6
- "core_contract": "1.19.755",
7
- "source_commit": "1cc8ad5ec0be2921b0e5cf12464689c86b5c36a6",
5
+ "core_version": "1.19.756",
6
+ "core_contract": "1.19.756",
7
+ "source_commit": "701a77495faed74b1cad4941c564f5a8a1a99158",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-16T02:11:51.276Z",
9
+ "built_at": "2026-09-16T02:21:20.653Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 483,
13
13
  "agent_docs_stubbed": 25,
14
- "functional_verbatim": 2112,
14
+ "functional_verbatim": 2114,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2620,
20
- "tree_sha256": "4960c3809b0d1bd4756c52793ef2167cfc1b70844cd2250f3e35723cd275bc50",
19
+ "file_count": 2622,
20
+ "tree_sha256": "0124e1a9cd241c6125c6a7a1351be80d37c9a40ba36c2f2def3ccb768417105c",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2512,7 +2512,7 @@
2512
2512
  {
2513
2513
  "path": "docs/module-api-changelog.md",
2514
2514
  "mode": "0000644",
2515
- "sha256": "744adc56bff99972f3104823c66db347b9aaef9aee42d06f12bff8da8e559a7d"
2515
+ "sha256": "cbc6427898e270a22852c496e1405b927c41e295a1a8808fb260b4f83f6f6429"
2516
2516
  },
2517
2517
  {
2518
2518
  "path": "docs/modules-contract.md",
@@ -7567,12 +7567,12 @@
7567
7567
  {
7568
7568
  "path": "package-lock.json",
7569
7569
  "mode": "0000644",
7570
- "sha256": "d2721993cfbc4a656aab1c93434a55dd7a440a3bb63048893ed6b43c2b1bbeec"
7570
+ "sha256": "cc3fcdaac4eeb204e96790fe0203978514bb550acc078a68d5577cd02d229f7c"
7571
7571
  },
7572
7572
  {
7573
7573
  "path": "package.json",
7574
7574
  "mode": "0000644",
7575
- "sha256": "23815bafaed5c20104d344967253d871d91b0df5524e5fba825119a316a0784a"
7575
+ "sha256": "0e01d62b067f43577be10f62f417f36dbdcbadc0ba03d290ba902b9447d9ac34"
7576
7576
  },
7577
7577
  {
7578
7578
  "path": "public-docs/index.html",
@@ -8024,11 +8024,21 @@
8024
8024
  "mode": "0000644",
8025
8025
  "sha256": "b8b1a7ed1d8dd217b24757a3ec9f4b3aa58b26a3e2a7b2cef9de217792e5ea43"
8026
8026
  },
8027
+ {
8028
+ "path": "scripts/gds/fitness-checks-error-envelope.js",
8029
+ "mode": "0000644",
8030
+ "sha256": "ab11c75ae177397fdd507505732450fee3531d42b99ba0b34811d06563ebd21c"
8031
+ },
8027
8032
  {
8028
8033
  "path": "scripts/gds/fitness-checks-identity.js",
8029
8034
  "mode": "0000644",
8030
8035
  "sha256": "583b21bf6527b657058ebfab50aff8c51cec96a0b1baa4d01daea55e90d0ccf7"
8031
8036
  },
8037
+ {
8038
+ "path": "scripts/gds/fitness-checks-packaging.js",
8039
+ "mode": "0000644",
8040
+ "sha256": "1f0611c9b8cb30e20119d50f67bc3a1148d6b5d869337361ae0ac58724aaca12"
8041
+ },
8032
8042
  {
8033
8043
  "path": "scripts/gds/fitness-checks-route-pins.js",
8034
8044
  "mode": "0000644",
@@ -8057,7 +8067,7 @@
8057
8067
  {
8058
8068
  "path": "scripts/gds/fitness.js",
8059
8069
  "mode": "0000644",
8060
- "sha256": "69c24b7d4a273520284cf2e07be151ffd70128e495b6bb12aa6da83b7d62cf71"
8070
+ "sha256": "4856485e5bb5bb03e47cd6953659c25214411d0b63287f6ab2faa4c34e1e27bc"
8061
8071
  },
8062
8072
  {
8063
8073
  "path": "scripts/gds/gate-review.js",
@@ -9372,7 +9382,7 @@
9372
9382
  {
9373
9383
  "path": "src/module-api.js",
9374
9384
  "mode": "0000644",
9375
- "sha256": "690fb3d76f0d06fcc41490f58b4a273c42ecfd65c8e9a2acb7cff0e85885f5a4"
9385
+ "sha256": "a4071a82922c3333e62a1657f6ce674b19831dbf0152ab3c6c270904193d3f2c"
9376
9386
  },
9377
9387
  {
9378
9388
  "path": "src/module-loader/catalog.js",
@@ -1984,5 +1984,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1984
1984
  landed since 1.19.753 with no explicit bump. run 35039836268. (task 1002620)
1985
1985
  1.19.755 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1986
1986
  landed since 1.19.754 with no explicit bump. run 35047010813. (task 1002620)
1987
+ 1.19.756 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1988
+ landed since 1.19.755 with no explicit bump. run 35047623582. (task 1002620)
1987
1989
  ---------------------------------------------------------------------------
1988
1990
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.755",
3
+ "version": "1.19.756",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.755",
9
+ "version": "1.19.756",
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.755",
3
+ "version": "1.19.756",
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,134 @@
1
+ // scripts/gds/fitness-checks-error-envelope.js — Check 17: no route hand-rolls a
2
+ // FLAT error (BV1 R03 / task 1990 / ADR 0116).
3
+ //
4
+ // Carved out of scripts/gds/fitness.js by task 1003825 alongside the packaging pair,
5
+ // for the headroom reason documented there. Self-contained: the scan needs only
6
+ // CORE_ROOT / rel / walkJs / walkJsAllRoots from fitness-lib.js, and FLAT_ERROR_BASELINE
7
+ // travels WITH the check rather than staying behind — the baseline and the gate that
8
+ // reads it are one decision, and splitting them is how a ratchet quietly loses its floor.
9
+ //
10
+ // fitness.js requires the four public names straight back (checkErrorEnvelope,
11
+ // scanFlatErrorSites, countFlatErrors, FLAT_ERROR_BASELINE — the last three are read by
12
+ // the R04 sweep and its tests), so nothing outside this file changes shape.
13
+ // stripCommentsForFlat stays PRIVATE here: it was never on fitness.js's exports, and
14
+ // exporting it now to mirror the carve would be a new public name nothing consumes.
15
+
16
+ 'use strict';
17
+
18
+ const fs = require('node:fs');
19
+ const path = require('node:path');
20
+
21
+ const { CORE_ROOT, rel, walkJs, walkJsAllRoots } = require('./fitness-lib.js');
22
+ // ============================================================================
23
+ // Check 17 — no route hand-rolls a FLAT error (BV1 R03 / task 1990 / ADR 0116)
24
+ // ============================================================================
25
+ //
26
+ // The error-envelope contract (ADR 0116) says every failure is the ONE envelope
27
+ // `{error:{code,message,details}}`, emitted via res.fail() or a thrown ApiError
28
+ // (the R02 middleware, task 1989). A hand-rolled flat error —
29
+ // `res.status(n).json({ error: 'x' })` or `.json({ error })` — breaks the
30
+ // contract: the caller gets a bare string where the envelope should be.
31
+ //
32
+ // RATCHET, not big-bang. The R04 sweep (task 1991) migrates the ~562 existing
33
+ // sites; until it finishes they'd all "fail" a strict check, so this HOLDS THE
34
+ // LINE: it fails only when the count rises ABOVE the committed baseline (a NEW
35
+ // hand-rolled flat error), and nudges the baseline DOWN as the sweep shrinks it.
36
+ // When the sweep reaches 0, FLAT_ERROR_BASELINE is 0 and any new flat error
37
+ // hard-fails immediately. Detector is shared with the sweep + its test.
38
+ const FLAT_ERROR_BASELINE = 0; // R04 sweep (task 1991) landed the surface at 0 — the gate is now hard-zero: any new hand-rolled flat error fails the build immediately.
39
+ // A flat error is a `.json({ … })` whose FIRST key is `error` with a value that
40
+ // is NOT an object literal (a string/var/shorthand). The envelope's nested
41
+ // `error: { … }` and `.json(envelope(…))` deliberately do NOT match.
42
+ // The `(?!\s*\{)` lookahead is whitespace-aware: without the inner `\s*` the
43
+ // preceding `\s*` backtracks to zero and the lookahead is tested at the space in
44
+ // `error: {`, so the compliant nested envelope was wrongly flagged (task 2029).
45
+ const FLAT_ERROR_VALUE_RE = /\.json\(\s*\{\s*error\s*:\s*(?!\s*\{)/g; // error: 'x' | error: code
46
+ const FLAT_ERROR_SHORTHAND_RE = /\.json\(\s*\{\s*error\s*[,}]/g; // { error } | { error, … }
47
+
48
+ // Strip // line comments + /* */ block comments AND blank out string/template
49
+ // contents (keeping the delimiters) so neither a DOC example of the anti-pattern
50
+ // — e.g. the res.fail() usage note in middleware/error-envelope.js — nor a
51
+ // pattern quoted inside a string literal is counted as a real flat error once
52
+ // the gate is hard-zero (task 1991). Real flat errors are unaffected: in
53
+ // `res.status(404).json({ error: 'x' })` the `.json({ error:` the detector keys
54
+ // on is CODE (outside the string), so blanking the `'x'` content still matches.
55
+ function stripCommentsForFlat(src) {
56
+ let out = '';
57
+ let i = 0;
58
+ let quote = null;
59
+ while (i < src.length) {
60
+ const c = src[i];
61
+ const d = src[i + 1];
62
+ if (quote) {
63
+ // inside a string/template: drop content, keep only the closing delimiter
64
+ if (c === '\\') { i += 2; continue; }
65
+ if (c === quote) { out += c; quote = null; }
66
+ i++; continue;
67
+ }
68
+ if (c === '"' || c === "'" || c === '`') { quote = c; out += c; i++; continue; }
69
+ if (c === '/' && d === '/') { while (i < src.length && src[i] !== '\n') i++; continue; }
70
+ if (c === '/' && d === '*') { i += 2; while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) i++; i += 2; continue; }
71
+ out += c; i++;
72
+ }
73
+ return out;
74
+ }
75
+
76
+ function countFlatErrors(text) {
77
+ const code = stripCommentsForFlat(text);
78
+ return (code.match(FLAT_ERROR_VALUE_RE) || []).length
79
+ + (code.match(FLAT_ERROR_SHORTHAND_RE) || []).length;
80
+ }
81
+
82
+ // scanFlatErrorSites() — per-file flat-error counts across the API surface
83
+ // (core src/bongos + every module), skipping tests. Shared by the check below, the
84
+ // R04 codemod, and tests/api_error_envelope_buildcheck.mjs.
85
+ function scanFlatErrorSites() {
86
+ const seen = new Set();
87
+ const byFile = {};
88
+ let total = 0;
89
+ const files = [
90
+ ...walkJs(path.join(CORE_ROOT, 'src', 'bongos')),
91
+ ...walkJsAllRoots('modules'),
92
+ ];
93
+ for (const abs of files) {
94
+ const relFile = rel(abs);
95
+ if (seen.has(relFile)) continue;
96
+ seen.add(relFile);
97
+ if (relFile.includes('/tests/') || relFile.endsWith('.test.js')) continue;
98
+ const n = countFlatErrors(fs.readFileSync(abs, 'utf8'));
99
+ if (n) { byFile[relFile] = n; total += n; }
100
+ }
101
+ return { total, byFile };
102
+ }
103
+
104
+ function checkErrorEnvelope() {
105
+ const { total, byFile } = scanFlatErrorSites();
106
+ const violations = [];
107
+ const warnings = [];
108
+ if (total > FLAT_ERROR_BASELINE) {
109
+ const top = Object.entries(byFile).sort((a, b) => b[1] - a[1]).slice(0, 8)
110
+ .map(([f, n]) => `${f} (${n})`).join(', ');
111
+ violations.push(
112
+ `${total} hand-rolled flat-error sites — ABOVE the ${FLAT_ERROR_BASELINE} baseline. A new route emitted a flat \`res.status(n).json({error:'x'})\` instead of \`res.fail('x', n)\` / \`throw new ApiError('x')\` (ADR 0116). Top files: ${top}. Use the envelope helper (src/bongos/middleware/error-envelope.js).`
113
+ );
114
+ } else if (total < FLAT_ERROR_BASELINE) {
115
+ warnings.push(
116
+ `${total} flat-error sites — BELOW the ${FLAT_ERROR_BASELINE} baseline. The sweep tightened; lower FLAT_ERROR_BASELINE to ${total} in fitness.js so the ratchet can't slip back (BV1 R04 / task 1991).`
117
+ );
118
+ }
119
+ return {
120
+ name: 'no route hand-rolls a flat error (ADR 0116 / R03)',
121
+ ok: violations.length === 0,
122
+ hardFail: violations.length > 0,
123
+ violations,
124
+ warnings,
125
+ note: `≤ ${FLAT_ERROR_BASELINE} flat \`.json({error:'…'})\` sites (ratchet → 0 as the R04 sweep lands); currently ${total}. New failures must use res.fail()/ApiError (the R02 envelope).`,
126
+ };
127
+ }
128
+
129
+ module.exports = {
130
+ checkErrorEnvelope,
131
+ scanFlatErrorSites,
132
+ countFlatErrors,
133
+ FLAT_ERROR_BASELINE,
134
+ };
@@ -0,0 +1,132 @@
1
+ // scripts/gds/fitness-checks-packaging.js — Checks 16 + 16b: core code must not
2
+ // HARD-require a boundary-EXCLUDED config or module.
3
+ //
4
+ // Carved out of scripts/gds/fitness.js by task 1003825, on the task-1003192
5
+ // precedent. That file had returned to the 1,500-line ratchet with ONE line of
6
+ // headroom (1,499 of 1,500, fitness-ratchets.js OVERSIZE_LINES), so the next check
7
+ // registered there would have tipped it over and redded the ratchet for whoever
8
+ // wrote it. These two checks left together because they are one subject — the same
9
+ // question asked of a config and of a module — and because the pair depends only on
10
+ // CORE_ROOT / rel / walkJs / walkJsAllRoots, all already public on fitness-lib.js,
11
+ // so the carve adds no import cycle.
12
+ //
13
+ // fitness.js requires both names straight back, so the CHECKS registry and the
14
+ // module surface the tests read are unchanged.
15
+
16
+ 'use strict';
17
+
18
+ const fs = require('node:fs');
19
+ const path = require('node:path');
20
+
21
+ const { CORE_ROOT, rel, walkJs, walkJsAllRoots } = require('./fitness-lib.js');
22
+ // ============================================================================
23
+ // Check 16 — core code must not HARD-require an instance-excluded config/*.json
24
+ // ============================================================================
25
+ //
26
+ // A vanilla self-host boot ships only the PUBLISHABLE tree (publish-manifest.js). An
27
+ // instance-only config (config/devbox-app.json, config/bongos-cli.json, config/branding.json,
28
+ // config/modules.json, …) is boundary-EXCLUDED, so a bare `require('../../config/<excluded>.json')`
29
+ // in core code throws MODULE_NOT_FOUND and CRASHES the boot on a fresh instance — the exact
30
+ // class that shipped in R88 (two hard-requires in src/bongos/serve-internal.js; task 1824). The
31
+ // cure is to READ the config instead of requiring it, tolerating a missing file
32
+ // (tryLoadInstanceConfig in src/bongos/serve-internal.js / fs.existsSync → fail-to-vanilla), which
33
+ // also drops the require() literal so it no longer matches here. Check 1 (core↔host) scans
34
+ // code→code IMPORTS and misses this code→config class. Only an EXCLUDED config is a violation;
35
+ // a publishable neutral.*/schema config (which ships with the core) is fine.
36
+ const CONFIG_REQUIRE_RE = /require\(\s*['"]([^'"]*config\/[^'"]*\.json)['"]\s*\)/g;
37
+
38
+ function checkCoreRequiresExcludedConfig() {
39
+ const { isPublishable } = require('./publish-manifest');
40
+ const violations = [];
41
+ // src/ + scripts/ are CORE; modules ship from BOTH roots (ADR 0108 §Consequences #4). Dedup by
42
+ // repo-relative path so a shared checkout scans each file once.
43
+ const seen = new Set();
44
+ const allAbs = [
45
+ ...['src', 'scripts'].flatMap((d) => walkJs(path.join(CORE_ROOT, d))),
46
+ ...walkJsAllRoots('modules'),
47
+ ];
48
+ for (const abs of allAbs) {
49
+ const relFile = rel(abs);
50
+ if (seen.has(relFile)) continue;
51
+ seen.add(relFile);
52
+ if (!isPublishable(relFile)) continue; // only CORE (publishable) code can crash a vanilla boot
53
+ for (const raw of fs.readFileSync(abs, 'utf8').split('\n')) {
54
+ const trimmed = raw.trim();
55
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
56
+ let m;
57
+ CONFIG_REQUIRE_RE.lastIndex = 0;
58
+ while ((m = CONFIG_REQUIRE_RE.exec(raw))) {
59
+ const relCfg = rel(path.resolve(path.dirname(abs), m[1]));
60
+ if (!isPublishable(relCfg)) {
61
+ violations.push(`${relFile} → require('${m[1]}') hard-requires an instance-excluded config (${relCfg}) — guard the read (tryLoadInstanceConfig in src/bongos/serve-internal.js / fs.existsSync, fail-to-vanilla) so a fresh core boots (task 1824).`);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ return {
67
+ name: 'core never hard-requires an excluded config (task 1824)',
68
+ ok: violations.length === 0,
69
+ hardFail: violations.length > 0,
70
+ violations,
71
+ warnings: [],
72
+ note: "a publishable src/·scripts/·modules/ .js must not require('…config/X.json') when X.json is boundary-excluded — guard the read so a vanilla core boots.",
73
+ };
74
+ }
75
+
76
+ // ============================================================================
77
+ // Check 16b — core code must not require an instance-EXCLUDED module (task 2135)
78
+ // ============================================================================
79
+ //
80
+ // Sibling of Check 16 for the code→MODULE class. A publishable core file that
81
+ // require()s a boundary-EXCLUDED module (modules/game, modules/art-pipeline,
82
+ // modules/character-anim, …) throws MODULE_NOT_FOUND once that module lives in the
83
+ // consumer's instance repo rather than the core package (R89 split). The cure is
84
+ // either to reclassify the module INTO core (if it belongs there — e.g. provisioning,
85
+ // sibling of dev-box) or to exclude the requiring file with the feature it serves
86
+ // (art tooling). Catches BOTH require forms: a direct literal
87
+ // require('../modules/<key>/…') AND a require(path.join(<root>, 'modules/<key>/…')).
88
+ // Only an EXCLUDED module required from a PUBLISHABLE file is a violation; requiring a
89
+ // publishable (core) module is fine, and the loader's DYNAMIC dispatch
90
+ // (path.join(dir,'routes',rk) — no literal modules/<key>/ string) is invisible here.
91
+ const MODULE_PATH_IN_STRING_RE = /['"](?:[^'"]*\/)?modules\/([a-z0-9][a-z0-9-]*)\/[^'"]*['"]/g;
92
+
93
+ function checkCoreRequiresExcludedModule() {
94
+ const { isPublishable } = require('./publish-manifest');
95
+ const violations = [];
96
+ const seen = new Set();
97
+ const allAbs = [
98
+ ...['src', 'scripts'].flatMap((d) => walkJs(path.join(CORE_ROOT, d))),
99
+ ...walkJsAllRoots('modules'),
100
+ ];
101
+ for (const abs of allAbs) {
102
+ const relFile = rel(abs);
103
+ if (seen.has(relFile)) continue;
104
+ seen.add(relFile);
105
+ if (!isPublishable(relFile)) continue; // only CORE (publishable) code can crash a vanilla boot
106
+ for (const raw of fs.readFileSync(abs, 'utf8').split('\n')) {
107
+ const trimmed = raw.trim();
108
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
109
+ if (!/require\s*\(/.test(raw)) continue; // only lines that actually require()
110
+ let m;
111
+ MODULE_PATH_IN_STRING_RE.lastIndex = 0;
112
+ while ((m = MODULE_PATH_IN_STRING_RE.exec(raw))) {
113
+ const key = m[1];
114
+ if (isPublishable(`modules/${key}/module.json`)) continue; // requiring a CORE module is fine
115
+ violations.push(`${relFile} → require(...'modules/${key}/…') pulls in an instance-EXCLUDED module — reclassify it into core (if it belongs there) or exclude ${relFile} with the feature it serves, so a fresh core boots (task 2135).`);
116
+ }
117
+ }
118
+ }
119
+ return {
120
+ name: 'core never requires an excluded module (task 2135)',
121
+ ok: violations.length === 0,
122
+ hardFail: violations.length > 0,
123
+ violations,
124
+ warnings: [],
125
+ note: "a publishable src/·scripts/·modules/ .js must not require('…modules/<key>/…') when that module is boundary-excluded — MODULE_NOT_FOUND post-split (R89).",
126
+ };
127
+ }
128
+
129
+ module.exports = {
130
+ checkCoreRequiresExcludedConfig,
131
+ checkCoreRequiresExcludedModule,
132
+ };
@@ -37,6 +37,8 @@ const { CORE_ROOT, INSTANCE_ROOT, MODULE_ROOTS, REPO_ROOT, rel, walkJs, walkJsAl
37
37
  const { IDENTITY_SCAN_ALLOWED, IDENTITY_SCAN_EXCLUDE, IDENTITY_SCAN_EXTRA_FILES, IDENTITY_SCAN_EXTS, IDENTITY_SCAN_ROOTS, IDENTITY_SCAN_SKIP_DIRS, PROMPT_IDENTITY_LITERALS, checkDesignContract, checkKernelPromptsNoHardcodedIdentity, checkNeutralConfigsNoHostIdentity, collectIdentityFiles, scanPromptIdentity, stripCommentsForIdentity } = require('./fitness-checks-identity.js');
38
38
  const { WRITE_VALIDATION_BASELINE, WRITE_VALIDATION_EXEMPT, checkWriteRoutesValidated, scanUnvalidatedWriteRoutes } = require('./fitness-checks-write-validation.js');
39
39
  const { GOAL_VOCAB_CITATION, GOAL_VOCAB_LINE_SCOPED, GOAL_VOCAB_SURFACE, LIFECYCLE_DB_FILES, PRELAUNCH_SURFACE, RENAME_IDENTITY_RE, VOCAB_ALLOW, VOCAB_BANNED, VOCAB_CITATION, VOCAB_EXTS, VOCAB_HISTORICAL_REF, VOCAB_SURFACE, checkGoalMembershipVocabulary, checkGovernmentRenameIdentity, checkPrelaunchVocabulary, checkVocabularyWall, vocabFilesToScan } = require('./fitness-checks-vocabulary.js');
40
+ const { checkCoreRequiresExcludedConfig, checkCoreRequiresExcludedModule } = require('./fitness-checks-packaging.js'); // Checks 16 + 16b — carved out by task 1003825 (the 1,500-line ratchet)
41
+ const { FLAT_ERROR_BASELINE, checkErrorEnvelope, countFlatErrors, scanFlatErrorSites } = require('./fitness-checks-error-envelope.js'); // Check 17 — carved out by task 1003825
40
42
  // ============================================================================
41
43
  // Check 1 — core ↔ host/game import boundary (ADR 0062 §1 cut-line)
42
44
  // ============================================================================
@@ -1084,219 +1086,6 @@ function checkHallSectionRegistry() {
1084
1086
  };
1085
1087
  }
1086
1088
 
1087
- // ============================================================================
1088
- // Check 16 — core code must not HARD-require an instance-excluded config/*.json
1089
- // ============================================================================
1090
- //
1091
- // A vanilla self-host boot ships only the PUBLISHABLE tree (publish-manifest.js). An
1092
- // instance-only config (config/devbox-app.json, config/bongos-cli.json, config/branding.json,
1093
- // config/modules.json, …) is boundary-EXCLUDED, so a bare `require('../../config/<excluded>.json')`
1094
- // in core code throws MODULE_NOT_FOUND and CRASHES the boot on a fresh instance — the exact
1095
- // class that shipped in R88 (two hard-requires in src/bongos/serve-internal.js; task 1824). The
1096
- // cure is to READ the config instead of requiring it, tolerating a missing file
1097
- // (tryLoadInstanceConfig in src/bongos/serve-internal.js / fs.existsSync → fail-to-vanilla), which
1098
- // also drops the require() literal so it no longer matches here. Check 1 (core↔host) scans
1099
- // code→code IMPORTS and misses this code→config class. Only an EXCLUDED config is a violation;
1100
- // a publishable neutral.*/schema config (which ships with the core) is fine.
1101
- const CONFIG_REQUIRE_RE = /require\(\s*['"]([^'"]*config\/[^'"]*\.json)['"]\s*\)/g;
1102
-
1103
- function checkCoreRequiresExcludedConfig() {
1104
- const { isPublishable } = require('./publish-manifest');
1105
- const violations = [];
1106
- // src/ + scripts/ are CORE; modules ship from BOTH roots (ADR 0108 §Consequences #4). Dedup by
1107
- // repo-relative path so a shared checkout scans each file once.
1108
- const seen = new Set();
1109
- const allAbs = [
1110
- ...['src', 'scripts'].flatMap((d) => walkJs(path.join(CORE_ROOT, d))),
1111
- ...walkJsAllRoots('modules'),
1112
- ];
1113
- for (const abs of allAbs) {
1114
- const relFile = rel(abs);
1115
- if (seen.has(relFile)) continue;
1116
- seen.add(relFile);
1117
- if (!isPublishable(relFile)) continue; // only CORE (publishable) code can crash a vanilla boot
1118
- for (const raw of fs.readFileSync(abs, 'utf8').split('\n')) {
1119
- const trimmed = raw.trim();
1120
- if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
1121
- let m;
1122
- CONFIG_REQUIRE_RE.lastIndex = 0;
1123
- while ((m = CONFIG_REQUIRE_RE.exec(raw))) {
1124
- const relCfg = rel(path.resolve(path.dirname(abs), m[1]));
1125
- if (!isPublishable(relCfg)) {
1126
- violations.push(`${relFile} → require('${m[1]}') hard-requires an instance-excluded config (${relCfg}) — guard the read (tryLoadInstanceConfig in src/bongos/serve-internal.js / fs.existsSync, fail-to-vanilla) so a fresh core boots (task 1824).`);
1127
- }
1128
- }
1129
- }
1130
- }
1131
- return {
1132
- name: 'core never hard-requires an excluded config (task 1824)',
1133
- ok: violations.length === 0,
1134
- hardFail: violations.length > 0,
1135
- violations,
1136
- warnings: [],
1137
- note: "a publishable src/·scripts/·modules/ .js must not require('…config/X.json') when X.json is boundary-excluded — guard the read so a vanilla core boots.",
1138
- };
1139
- }
1140
-
1141
- // ============================================================================
1142
- // Check 16b — core code must not require an instance-EXCLUDED module (task 2135)
1143
- // ============================================================================
1144
- //
1145
- // Sibling of Check 16 for the code→MODULE class. A publishable core file that
1146
- // require()s a boundary-EXCLUDED module (modules/game, modules/art-pipeline,
1147
- // modules/character-anim, …) throws MODULE_NOT_FOUND once that module lives in the
1148
- // consumer's instance repo rather than the core package (R89 split). The cure is
1149
- // either to reclassify the module INTO core (if it belongs there — e.g. provisioning,
1150
- // sibling of dev-box) or to exclude the requiring file with the feature it serves
1151
- // (art tooling). Catches BOTH require forms: a direct literal
1152
- // require('../modules/<key>/…') AND a require(path.join(<root>, 'modules/<key>/…')).
1153
- // Only an EXCLUDED module required from a PUBLISHABLE file is a violation; requiring a
1154
- // publishable (core) module is fine, and the loader's DYNAMIC dispatch
1155
- // (path.join(dir,'routes',rk) — no literal modules/<key>/ string) is invisible here.
1156
- const MODULE_PATH_IN_STRING_RE = /['"](?:[^'"]*\/)?modules\/([a-z0-9][a-z0-9-]*)\/[^'"]*['"]/g;
1157
-
1158
- function checkCoreRequiresExcludedModule() {
1159
- const { isPublishable } = require('./publish-manifest');
1160
- const violations = [];
1161
- const seen = new Set();
1162
- const allAbs = [
1163
- ...['src', 'scripts'].flatMap((d) => walkJs(path.join(CORE_ROOT, d))),
1164
- ...walkJsAllRoots('modules'),
1165
- ];
1166
- for (const abs of allAbs) {
1167
- const relFile = rel(abs);
1168
- if (seen.has(relFile)) continue;
1169
- seen.add(relFile);
1170
- if (!isPublishable(relFile)) continue; // only CORE (publishable) code can crash a vanilla boot
1171
- for (const raw of fs.readFileSync(abs, 'utf8').split('\n')) {
1172
- const trimmed = raw.trim();
1173
- if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
1174
- if (!/require\s*\(/.test(raw)) continue; // only lines that actually require()
1175
- let m;
1176
- MODULE_PATH_IN_STRING_RE.lastIndex = 0;
1177
- while ((m = MODULE_PATH_IN_STRING_RE.exec(raw))) {
1178
- const key = m[1];
1179
- if (isPublishable(`modules/${key}/module.json`)) continue; // requiring a CORE module is fine
1180
- violations.push(`${relFile} → require(...'modules/${key}/…') pulls in an instance-EXCLUDED module — reclassify it into core (if it belongs there) or exclude ${relFile} with the feature it serves, so a fresh core boots (task 2135).`);
1181
- }
1182
- }
1183
- }
1184
- return {
1185
- name: 'core never requires an excluded module (task 2135)',
1186
- ok: violations.length === 0,
1187
- hardFail: violations.length > 0,
1188
- violations,
1189
- warnings: [],
1190
- note: "a publishable src/·scripts/·modules/ .js must not require('…modules/<key>/…') when that module is boundary-excluded — MODULE_NOT_FOUND post-split (R89).",
1191
- };
1192
- }
1193
-
1194
- // ============================================================================
1195
- // Check 17 — no route hand-rolls a FLAT error (BV1 R03 / task 1990 / ADR 0116)
1196
- // ============================================================================
1197
- //
1198
- // The error-envelope contract (ADR 0116) says every failure is the ONE envelope
1199
- // `{error:{code,message,details}}`, emitted via res.fail() or a thrown ApiError
1200
- // (the R02 middleware, task 1989). A hand-rolled flat error —
1201
- // `res.status(n).json({ error: 'x' })` or `.json({ error })` — breaks the
1202
- // contract: the caller gets a bare string where the envelope should be.
1203
- //
1204
- // RATCHET, not big-bang. The R04 sweep (task 1991) migrates the ~562 existing
1205
- // sites; until it finishes they'd all "fail" a strict check, so this HOLDS THE
1206
- // LINE: it fails only when the count rises ABOVE the committed baseline (a NEW
1207
- // hand-rolled flat error), and nudges the baseline DOWN as the sweep shrinks it.
1208
- // When the sweep reaches 0, FLAT_ERROR_BASELINE is 0 and any new flat error
1209
- // hard-fails immediately. Detector is shared with the sweep + its test.
1210
- const FLAT_ERROR_BASELINE = 0; // R04 sweep (task 1991) landed the surface at 0 — the gate is now hard-zero: any new hand-rolled flat error fails the build immediately.
1211
- // A flat error is a `.json({ … })` whose FIRST key is `error` with a value that
1212
- // is NOT an object literal (a string/var/shorthand). The envelope's nested
1213
- // `error: { … }` and `.json(envelope(…))` deliberately do NOT match.
1214
- // The `(?!\s*\{)` lookahead is whitespace-aware: without the inner `\s*` the
1215
- // preceding `\s*` backtracks to zero and the lookahead is tested at the space in
1216
- // `error: {`, so the compliant nested envelope was wrongly flagged (task 2029).
1217
- const FLAT_ERROR_VALUE_RE = /\.json\(\s*\{\s*error\s*:\s*(?!\s*\{)/g; // error: 'x' | error: code
1218
- const FLAT_ERROR_SHORTHAND_RE = /\.json\(\s*\{\s*error\s*[,}]/g; // { error } | { error, … }
1219
-
1220
- // Strip // line comments + /* */ block comments AND blank out string/template
1221
- // contents (keeping the delimiters) so neither a DOC example of the anti-pattern
1222
- // — e.g. the res.fail() usage note in middleware/error-envelope.js — nor a
1223
- // pattern quoted inside a string literal is counted as a real flat error once
1224
- // the gate is hard-zero (task 1991). Real flat errors are unaffected: in
1225
- // `res.status(404).json({ error: 'x' })` the `.json({ error:` the detector keys
1226
- // on is CODE (outside the string), so blanking the `'x'` content still matches.
1227
- function stripCommentsForFlat(src) {
1228
- let out = '';
1229
- let i = 0;
1230
- let quote = null;
1231
- while (i < src.length) {
1232
- const c = src[i];
1233
- const d = src[i + 1];
1234
- if (quote) {
1235
- // inside a string/template: drop content, keep only the closing delimiter
1236
- if (c === '\\') { i += 2; continue; }
1237
- if (c === quote) { out += c; quote = null; }
1238
- i++; continue;
1239
- }
1240
- if (c === '"' || c === "'" || c === '`') { quote = c; out += c; i++; continue; }
1241
- if (c === '/' && d === '/') { while (i < src.length && src[i] !== '\n') i++; continue; }
1242
- if (c === '/' && d === '*') { i += 2; while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) i++; i += 2; continue; }
1243
- out += c; i++;
1244
- }
1245
- return out;
1246
- }
1247
-
1248
- function countFlatErrors(text) {
1249
- const code = stripCommentsForFlat(text);
1250
- return (code.match(FLAT_ERROR_VALUE_RE) || []).length
1251
- + (code.match(FLAT_ERROR_SHORTHAND_RE) || []).length;
1252
- }
1253
-
1254
- // scanFlatErrorSites() — per-file flat-error counts across the API surface
1255
- // (core src/bongos + every module), skipping tests. Shared by the check below, the
1256
- // R04 codemod, and tests/api_error_envelope_buildcheck.mjs.
1257
- function scanFlatErrorSites() {
1258
- const seen = new Set();
1259
- const byFile = {};
1260
- let total = 0;
1261
- const files = [
1262
- ...walkJs(path.join(CORE_ROOT, 'src', 'bongos')),
1263
- ...walkJsAllRoots('modules'),
1264
- ];
1265
- for (const abs of files) {
1266
- const relFile = rel(abs);
1267
- if (seen.has(relFile)) continue;
1268
- seen.add(relFile);
1269
- if (relFile.includes('/tests/') || relFile.endsWith('.test.js')) continue;
1270
- const n = countFlatErrors(fs.readFileSync(abs, 'utf8'));
1271
- if (n) { byFile[relFile] = n; total += n; }
1272
- }
1273
- return { total, byFile };
1274
- }
1275
-
1276
- function checkErrorEnvelope() {
1277
- const { total, byFile } = scanFlatErrorSites();
1278
- const violations = [];
1279
- const warnings = [];
1280
- if (total > FLAT_ERROR_BASELINE) {
1281
- const top = Object.entries(byFile).sort((a, b) => b[1] - a[1]).slice(0, 8)
1282
- .map(([f, n]) => `${f} (${n})`).join(', ');
1283
- violations.push(
1284
- `${total} hand-rolled flat-error sites — ABOVE the ${FLAT_ERROR_BASELINE} baseline. A new route emitted a flat \`res.status(n).json({error:'x'})\` instead of \`res.fail('x', n)\` / \`throw new ApiError('x')\` (ADR 0116). Top files: ${top}. Use the envelope helper (src/bongos/middleware/error-envelope.js).`
1285
- );
1286
- } else if (total < FLAT_ERROR_BASELINE) {
1287
- warnings.push(
1288
- `${total} flat-error sites — BELOW the ${FLAT_ERROR_BASELINE} baseline. The sweep tightened; lower FLAT_ERROR_BASELINE to ${total} in fitness.js so the ratchet can't slip back (BV1 R04 / task 1991).`
1289
- );
1290
- }
1291
- return {
1292
- name: 'no route hand-rolls a flat error (ADR 0116 / R03)',
1293
- ok: violations.length === 0,
1294
- hardFail: violations.length > 0,
1295
- violations,
1296
- warnings,
1297
- note: `≤ ${FLAT_ERROR_BASELINE} flat \`.json({error:'…'})\` sites (ratchet → 0 as the R04 sweep lands); currently ${total}. New failures must use res.fail()/ApiError (the R02 envelope).`,
1298
- };
1299
- }
1300
1089
 
1301
1090
  // ============================================================================
1302
1091
  // Check 2c — every SKILL.md frontmatter parses the way Claude Code's YAML reader reads it (task 1003548)
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.755'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.756'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');