@bongos/core 1.19.704 → 1.19.705

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.704",
6
- "core_contract": "1.19.704",
7
- "source_commit": "86a7901eed7ffd687d8366c14b70b6dbce1a36f8",
5
+ "core_version": "1.19.705",
6
+ "core_contract": "1.19.705",
7
+ "source_commit": "ee446c148a7c0bc6e13028a538e32f2288b74802",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-13T04:51:44.117Z",
9
+ "built_at": "2026-09-13T05:14:27.400Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 477,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2132,
14
+ "functional_verbatim": 2134,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2633,
20
- "tree_sha256": "6a38a87e7ab24df8fa7020189772ed67bc02d6271601eda7a94aeddf84bfc12d",
19
+ "file_count": 2635,
20
+ "tree_sha256": "b975cdb11e18889435478808545314cf6b5f9d2a3f17411eb4cd63d3d294e047",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2802,7 +2802,7 @@
2802
2802
  {
2803
2803
  "path": "docs/module-api-changelog.md",
2804
2804
  "mode": "0000644",
2805
- "sha256": "339e97fa485b7a1b3090c5964a23d6d30d3863781e549e30af68610fb3eedb91"
2805
+ "sha256": "64a2ef09f0be002b9c3af200c125b35b09775adb665a273b44e235a79b7265b9"
2806
2806
  },
2807
2807
  {
2808
2808
  "path": "docs/modules-contract.md",
@@ -7782,12 +7782,12 @@
7782
7782
  {
7783
7783
  "path": "package-lock.json",
7784
7784
  "mode": "0000644",
7785
- "sha256": "8eb4b18a9a0ee8ec64bab2712698fca515ff92277ebeed12adbb7126fe30e3e2"
7785
+ "sha256": "b74ccfc9d9bcd1da47c4bdf0010e1516b48e5213cc435f10069bb30454b6e4fc"
7786
7786
  },
7787
7787
  {
7788
7788
  "path": "package.json",
7789
7789
  "mode": "0000644",
7790
- "sha256": "6c55beca8912927a2d103ef0efee212341daa09be17e346d4d298377e3958d19"
7790
+ "sha256": "59663e88e9591b8ef999f69c31757eac4275b7aa1376b9497ad6467223a21b3a"
7791
7791
  },
7792
7792
  {
7793
7793
  "path": "public-docs/index.html",
@@ -8199,6 +8199,11 @@
8199
8199
  "mode": "0000644",
8200
8200
  "sha256": "2fcc3f97558ed6a653638df96d585dc7a1ef7dd884657a63d3a9545dbdb03e92"
8201
8201
  },
8202
+ {
8203
+ "path": "scripts/gds/exec-path-guard.js",
8204
+ "mode": "0000644",
8205
+ "sha256": "e568575a922de8c4538f11fa0e67ca0a97ab75317ff59b6aea27e6020a87313c"
8206
+ },
8202
8207
  {
8203
8208
  "path": "scripts/gds/feedback-latest.js",
8204
8209
  "mode": "0000644",
@@ -8252,7 +8257,7 @@
8252
8257
  {
8253
8258
  "path": "scripts/gds/fitness.js",
8254
8259
  "mode": "0000644",
8255
- "sha256": "d6120a5427f9053529639cf15a59bad0afae9602d5a1e8301017f3321493038b"
8260
+ "sha256": "418376cea128d2b1683e65017d74c453eb170e163bd131c1252ec79c6baf0920"
8256
8261
  },
8257
8262
  {
8258
8263
  "path": "scripts/gds/gate-review.js",
@@ -9557,7 +9562,7 @@
9557
9562
  {
9558
9563
  "path": "src/module-api.js",
9559
9564
  "mode": "0000644",
9560
- "sha256": "0954b91956641ff68caa5c9373be6d33d3ac3fdacda102846de75a339326fc86"
9565
+ "sha256": "d5b14e265925245fcd8ff4c280c68d0162a3bec6d58ee5358047e43708bdeaed"
9561
9566
  },
9562
9567
  {
9563
9568
  "path": "src/module-loader/catalog.js",
@@ -10574,6 +10579,11 @@
10574
10579
  "mode": "0000644",
10575
10580
  "sha256": "d6a78357533fdeea3bc33fe97cfcd27b1d04747c600d0a7800ddfca5199dbb1f"
10576
10581
  },
10582
+ {
10583
+ "path": "tests/exec_path_guard.mjs",
10584
+ "mode": "0000644",
10585
+ "sha256": "478075d19af9592d87ef9419c06496e72c132f4c045e19a1ebf821ab6ff7074b"
10586
+ },
10577
10587
  {
10578
10588
  "path": "tests/executed_closure.mjs",
10579
10589
  "mode": "0000644",
@@ -1867,5 +1867,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1867
1867
  landed since 1.19.702 with no explicit bump. run 34736566183. (task 1002620)
1868
1868
  1.19.704 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1869
1869
  landed since 1.19.703 with no explicit bump. run 34738864605. (task 1002620)
1870
+ 1.19.705 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1871
+ landed since 1.19.704 with no explicit bump. run 34739785273. (task 1002620)
1870
1872
  ---------------------------------------------------------------------------
1871
1873
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.704",
3
+ "version": "1.19.705",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.704",
9
+ "version": "1.19.705",
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.704",
3
+ "version": "1.19.705",
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,564 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/exec-path-guard.js — server-side code must not EXECUTE a path it does
3
+ // not statically control (task 1003371, audit ref B50, 2026-08-29 security audit).
4
+ //
5
+ // THE BLIND SPOT THIS EXISTS TO CLOSE. Every other gate asks who may CALL something:
6
+ // route-rank-check.js checks who may reach a route, fitness Check 4 checks the gates
7
+ // exist, permission-path-check.js checks who may EDIT a file. Nothing asked what the
8
+ // server RUNS, or out of whose tree. All three CRITICALs in the 2026-08-29 audit lived
9
+ // in that one gap, and none of them were caught by anything.
10
+ //
11
+ // THE DEFECT SHAPE. modules/lifecycle/conflict-resolve.js clones a builder's branch to
12
+ // a temp dir and runs `execFile(process.execPath, [path.join(work, rel)])` — a script
13
+ // out of the CLONE, with the live server's DATABASE_URL and BUILDER_SECRET_KEY
14
+ // inherited. The path is assembled from `work`, a runtime value, so no amount of
15
+ // reading the call site tells you what binary actually runs.
16
+ //
17
+ // THE RULE. An executed path must be STATICALLY ROOTED AT THE CORE CHECKOUT: built only
18
+ // from string literals, __dirname/__filename, process.execPath, and module-level consts
19
+ // that are themselves so built. A path that reaches the call through a parameter, a
20
+ // let, or any other runtime value is flagged. That is a deliberately blunt rule — it
21
+ // cannot tell a safe runtime path from an unsafe one, so it insists the question never
22
+ // arises. Where a runtime path is genuinely correct, EXEMPTIONS below records why, and
23
+ // the exemption is the review.
24
+ //
25
+ // WHY STATIC AND NOT A RUNTIME GUARD. The dangerous call already ran with full
26
+ // privilege by the time a runtime check could see it, and the sites are few and rarely
27
+ // touched — this is exactly the shape a build-time scan handles well.
28
+ //
29
+ // THE RESIDUAL ASSUMPTION, stated so it can be re-checked rather than rediscovered.
30
+ // This is a LINE/BRACKET scan, not a parse (the repo ships no JS parser — see
31
+ // route-rank-check.js, which reads route files the same way for the same reason). It
32
+ // resolves child_process bindings per file, so a regex's `.exec()` is never mistaken
33
+ // for the child_process one, and it balances brackets to read a multi-line call. It
34
+ // does NOT follow a value across files or through a function return. A path laundered
35
+ // through a helper in another module would read as a bare identifier here and be
36
+ // FLAGGED, not missed — the scan fails toward noise, never toward silence.
37
+
38
+ 'use strict';
39
+
40
+ const fs = require('node:fs');
41
+ const path = require('node:path');
42
+ const { execFileSync } = require('node:child_process');
43
+
44
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
45
+ const NAME = 'no server-side exec/spawn runs a path that is not statically rooted at the core checkout (task 1003371)';
46
+
47
+ // The child_process members that start a process. `fork` included: it runs a module
48
+ // path the same way execFile does.
49
+ const SPAWNERS = new Set([
50
+ 'exec', 'execSync', 'execFile', 'execFileSync', 'spawn', 'spawnSync', 'fork',
51
+ ]);
52
+
53
+ // Identifiers an executed path may be built from without the scan knowing more. These
54
+ // are fixed at module load and cannot be steered by a request or a branch.
55
+ const STATIC_ROOTS = new Set([
56
+ '__dirname', '__filename', 'process', 'path', 'require',
57
+ 'String', 'Number', 'JSON', 'Boolean', 'Array', 'Object',
58
+ ]);
59
+
60
+ // Reviewed exemptions. Each is a call the scan flags where a runtime path is the
61
+ // correct behaviour, with the reason it is safe. An entry is a REVIEW, not a mute:
62
+ // adding one is the moment to argue the case, which is why `why` is asserted non-empty
63
+ // by tests/exec_path_guard.mjs. Keyed `<repo-relative file>:<line>` is deliberately
64
+ // brittle — a line shift re-opens the review rather than carrying a stale approval.
65
+ const EXEMPTIONS = [
66
+ {
67
+ at: 'modules/lifecycle/conflict-resolve.js:382',
68
+ why: 'THE site this check was written for (audit ref B1), and the one place a runtime path is '
69
+ + 'REQUIRED rather than tolerated. Each generator derives its repo root from its own '
70
+ + '__dirname, so running the SERVER\'s copy would regenerate the deploy tree instead of the '
71
+ + 'throwaway clone — rooting this statically would break the feature and corrupt the live '
72
+ + 'checkout. It is safe because of a control OUTSIDE this call: task 1003367 made '
73
+ + 'resolveBranchConflicts refuse to auto-resolve any branch touching '
74
+ + 'computeExecutedClosure() (steps 5b/4b), so this line is unreachable for a branch that '
75
+ + 'edited executed code, and task 1003366 put all eight execution roots at the Metic floor. '
76
+ + 'If that refusal is ever removed this exemption dies with it.',
77
+ },
78
+ {
79
+ at: 'modules/grading/generated-provenance.js:156',
80
+ why: '`dir` defaults to path.join(__dirname, ..., \'scripts\', \'gds\') — the SERVER\'s own tree — '
81
+ + 'and `script` comes from the module-level GENERATORS table, which holds string literals '
82
+ + 'only. The `scriptDir` seam exists so the unit test never spawns a process and is never '
83
+ + 'set in production. The untrusted input here is the CWD, not the script: this runs a '
84
+ + 'trusted generator against a suspect tree, which is the inverse of the B50 shape.',
85
+ },
86
+ {
87
+ at: 'modules/grading/generated-provenance.js:223',
88
+ why: 'The async twin of the call at :156 — same `dir` default, same literal GENERATORS table, '
89
+ + 'same test-only `scriptDir` seam. Listed separately rather than folded in because an '
90
+ + 'exemption is keyed to a line: if one of the two calls changes shape, only that one '
91
+ + 're-opens for review.',
92
+ },
93
+ {
94
+ at: 'src/module-loader/loader.js:190',
95
+ why: 'The plugin loader, and the second site the audit named (B50 "motivating sites"). It '
96
+ + 'require()s a discovered module\'s route file, so the specifier is a runtime path BY '
97
+ + 'DESIGN — a plugin system whose module paths are compile-time constants is not a plugin '
98
+ + 'system. What bounds it is not this call but WHICH ROOTS are discovered: DEFAULT_ROOTS is '
99
+ + 'the core package\'s modules/ plus the instance\'s own, both operator-controlled, never a '
100
+ + 'builder branch. The residual risk — an instance accepting an untrusted module — is a '
101
+ + 'different boundary with its own control: ADR 0107 designates an accept-time module '
102
+ + 'security review, tracked as task 1003400. Exempt here so that work is not silently '
103
+ + 'absorbed into this check.',
104
+ },
105
+ {
106
+ at: 'src/module-loader/loader.js:293',
107
+ why: 'The poller twin of the route require at :190 — same DEFAULT_ROOTS bound, same plugin-by-'
108
+ + 'design rationale, same ADR 0107 / task 1003400 follow-on. Listed on its own line so a '
109
+ + 'change to either call re-opens only that one.',
110
+ },
111
+ {
112
+ at: 'modules/ui-design/scripts/validate-design.js:222',
113
+ why: 'A developer validation script, not a server path: it require()s each adapter\'s index.js '
114
+ + 'to check the adapter contract, and loading the thing under test is the whole point. '
115
+ + 'indexPath is path.join(ADAPTERS_DIR, adapterName, \'index.js\') where ADAPTERS_DIR is '
116
+ + '__dirname-rooted and adapterName comes from reading that same directory — so it cannot '
117
+ + 'escape the core checkout. Flagged only because the scan does not model a directory '
118
+ + 'listing as a constrained source.',
119
+ },
120
+ {
121
+ at: 'src/bongos/llm-cache.js:116',
122
+ why: 'Not an executed path at all. `cmd` is a `git rev-parse HEAD:<p>` command string built in '
123
+ + 'scopeShaFor from a code-defined paths list; the binary is git, resolved from PATH, and '
124
+ + 'nothing is run out of a builder tree. Flagged because the check reads arg 0 of every '
125
+ + 'spawner and cannot tell a command string from a path — the honest cost of a rule blunt '
126
+ + 'enough to have no gaps. NOTE for a future reader: interpolating `p` into a shell string '
127
+ + 'is a real smell of a DIFFERENT class (shell metacharacters, not tree provenance). It is '
128
+ + 'not exploitable today because every caller passes a literal, and it is out of scope for '
129
+ + 'B50 — but it is the thing to fix if `paths` ever becomes request-derived.',
130
+ },
131
+ ];
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // binding resolution — which local names in THIS file are child_process spawners
135
+ // ---------------------------------------------------------------------------
136
+
137
+ const REQUIRE_CP_RE = /(?:const|let|var)\s+(\{[^}]*\}|[A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"](?:node:)?child_process['"]\s*\)/g;
138
+
139
+ // ESM: `import { execFile, spawn as sp } from 'node:child_process'`, and the namespace
140
+ // form. The repo is CJS today, but a check that only understands CJS quietly stops
141
+ // working the day a file converts — and stops without a signal, which is the failure
142
+ // mode this file exists to avoid.
143
+ const IMPORT_CP_RE = /import\s+(\*\s*as\s*[A-Za-z_$][\w$]*|\{[^}]*\}|[A-Za-z_$][\w$]*)\s*from\s*['"](?:node:)?child_process['"]/g;
144
+
145
+ // The UNBOUND form: `require('node:child_process').execFileSync(...)` called straight off
146
+ // the require. It binds no name, so binding resolution alone never sees it — and the
147
+ // first draft of this check short-circuited the WHOLE FILE on "no bindings", so a file
148
+ // spawning this way was skipped entirely rather than scanned. That is the exact
149
+ // inversion of this scan's stated "fails toward noise, never toward silence" contract,
150
+ // and the form is not hypothetical: scripts/gds/autonomy-gate.js and
151
+ // scripts/gds/ship-land.js both already use it.
152
+ const INLINE_CP_RE = new RegExp(
153
+ `require\\(\\s*['"](?:node:)?child_process['"]\\s*\\)\\s*\\.\\s*(?:${[...SPAWNERS].join('|')})\\s*\\(`,
154
+ 'g',
155
+ );
156
+
157
+ // A require() whose specifier is not a literal: the server LOADS AND RUNS that module in
158
+ // its own process, which is the same question this check asks of exec/spawn through a
159
+ // different mechanism. src/module-loader/loader.js is the motivating site the audit named
160
+ // alongside conflict-resolve.js.
161
+ const REQUIRE_CALL_RE = /(?<![.\w$])require\s*\(/g;
162
+
163
+ // Returns { direct:Set<name>, namespaces:Set<name> } — `direct` are names bound to a
164
+ // spawner (destructuring or ESM named import, renames included), `namespaces` are
165
+ // whole-module bindings used as `cp.execFile(...)`.
166
+ function resolveBindings(src) {
167
+ const direct = new Set();
168
+ const namespaces = new Set();
169
+ const takeList = (lhs) => {
170
+ if (!lhs.startsWith('{')) {
171
+ namespaces.add(lhs.replace(/^\*\s*as\s*/, '').trim());
172
+ return;
173
+ }
174
+ for (const part of lhs.slice(1, -1).split(',')) {
175
+ const [orig, alias] = part.split(/:|\bas\b/).map((s) => s.trim());
176
+ if (orig && SPAWNERS.has(orig)) direct.add(alias || orig);
177
+ }
178
+ };
179
+ for (const re of [REQUIRE_CP_RE, IMPORT_CP_RE]) {
180
+ re.lastIndex = 0;
181
+ let m;
182
+ while ((m = re.exec(src))) takeList(m[1].trim());
183
+ }
184
+ return { direct, namespaces };
185
+ }
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // argument extraction — read a call's arguments, balancing brackets and strings
189
+ // ---------------------------------------------------------------------------
190
+
191
+ // From the index of a call's '(', return the raw argument text, or null if unbalanced.
192
+ function readCallArgs(src, openIdx) {
193
+ let depth = 0;
194
+ let quote = null;
195
+ for (let i = openIdx; i < src.length; i += 1) {
196
+ const c = src[i];
197
+ if (quote) {
198
+ if (c === '\\') { i += 1; continue; }
199
+ if (c === quote) quote = null;
200
+ continue;
201
+ }
202
+ if (c === "'" || c === '"' || c === '`') { quote = c; continue; }
203
+ if (c === '(' || c === '[' || c === '{') depth += 1;
204
+ else if (c === ')' || c === ']' || c === '}') {
205
+ depth -= 1;
206
+ if (depth === 0) return src.slice(openIdx + 1, i);
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+
212
+ // Split argument text on TOP-LEVEL commas only.
213
+ function splitArgs(argText) {
214
+ const out = [];
215
+ let depth = 0;
216
+ let quote = null;
217
+ let start = 0;
218
+ for (let i = 0; i < argText.length; i += 1) {
219
+ const c = argText[i];
220
+ if (quote) {
221
+ if (c === '\\') { i += 1; continue; }
222
+ if (c === quote) quote = null;
223
+ continue;
224
+ }
225
+ if (c === "'" || c === '"' || c === '`') { quote = c; continue; }
226
+ if (c === '(' || c === '[' || c === '{') depth += 1;
227
+ else if (c === ')' || c === ']' || c === '}') depth -= 1;
228
+ else if (c === ',' && depth === 0) { out.push(argText.slice(start, i)); start = i + 1; }
229
+ }
230
+ out.push(argText.slice(start));
231
+ return out.map((s) => s.trim()).filter((s) => s.length);
232
+ }
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // staticness — is an expression built only from roots fixed at module load?
236
+ // ---------------------------------------------------------------------------
237
+
238
+ // Blank string CONTENT so it cannot contribute identifiers — with one exception that
239
+ // decides whether this check works at all.
240
+ //
241
+ // A template literal's `${...}` holes are CODE, not content. Blanking a whole template
242
+ // the way a quoted string is blanked leaves `${work}/gen.js` with no free identifiers,
243
+ // and isStatic()'s every() over an empty set is vacuously TRUE — so the path reads as
244
+ // static and the call is never flagged. That is not a missed edge: it is a one-character
245
+ // bypass of the entire check (write a backtick instead of calling path.join) for exactly
246
+ // the runtime-derived path B1/B50 are about. Found by the grader, reproduced, pinned by
247
+ // tests/exec_path_guard.mjs.
248
+ //
249
+ // So a template is reduced to a call-shaped list of its interpolated expressions —
250
+ // `${a}/x/${b}` becomes `(a,b)` — which keeps every identifier the hole referenced and
251
+ // discards the literal text around it. Templates are handled BEFORE the quoted forms so
252
+ // a quote inside a hole cannot be mistaken for a string opener.
253
+ const TEMPLATE_RE = /`(?:[^`\\]|\\.)*`/g;
254
+ const HOLE_RE = /\$\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g;
255
+
256
+ function stripStrings(expr) {
257
+ return expr
258
+ .replace(TEMPLATE_RE, (lit) => {
259
+ const holes = [];
260
+ HOLE_RE.lastIndex = 0;
261
+ let m;
262
+ while ((m = HOLE_RE.exec(lit))) holes.push(m[1]);
263
+ return holes.length ? `(${holes.join(',')})` : '``';
264
+ })
265
+ .replace(/'(?:[^'\\]|\\.)*'/g, "''")
266
+ .replace(/"(?:[^"\\]|\\.)*"/g, '""');
267
+ }
268
+
269
+ // Free identifiers: tokens not preceded by '.' (property names) and not object keys.
270
+ function freeIdentifiers(expr) {
271
+ const bare = stripStrings(expr);
272
+ const out = new Set();
273
+ const re = /(^|[^.\w$])([A-Za-z_$][\w$]*)/g;
274
+ let m;
275
+ while ((m = re.exec(bare))) out.add(m[2]);
276
+ return out;
277
+ }
278
+
279
+ // Module-level `const NAME = <expr>;` declarations — the only indirection the scan
280
+ // follows. Column 0 is the test for module level; a const nested in a function is
281
+ // re-assigned per call and is not a fixed root.
282
+ function moduleConsts(src) {
283
+ const out = new Map();
284
+ const re = /^const\s+([A-Za-z_$][\w$]*)\s*=\s*([^\n]*?);?\s*$/gm;
285
+ let m;
286
+ while ((m = re.exec(src))) out.set(m[1], m[2]);
287
+ return out;
288
+ }
289
+
290
+ // The safe set: STATIC_ROOTS plus module consts that resolve to safe expressions.
291
+ // Iterated to a fixpoint so `const A = __dirname; const B = path.join(A, 'x')` works.
292
+ function safeIdentifiers(src) {
293
+ const consts = moduleConsts(src);
294
+ const safe = new Set(STATIC_ROOTS);
295
+ for (let pass = 0; pass < 5; pass += 1) {
296
+ let grew = false;
297
+ for (const [name, expr] of consts) {
298
+ if (safe.has(name)) continue;
299
+ if ([...freeIdentifiers(expr)].every((id) => safe.has(id))) { safe.add(name); grew = true; }
300
+ }
301
+ if (!grew) break;
302
+ }
303
+ return safe;
304
+ }
305
+
306
+ function isStatic(expr, safe) {
307
+ return [...freeIdentifiers(expr)].every((id) => safe.has(id));
308
+ }
309
+
310
+ // ---------------------------------------------------------------------------
311
+ // masking — blank comments, string bodies and regex literals before matching
312
+ // ---------------------------------------------------------------------------
313
+
314
+ // Call sites are matched against a MASKED copy of the source, never the raw text.
315
+ // Without this the scan reads prose: modules/grading/grader.js carries the words
316
+ // "before worker spawn (GRADER_BYPASS_ENABLED / runOpts.bypass)" inside a note string,
317
+ // and a bare /spawn\s*\(/ matches it and reports the sentence as an executed path.
318
+ // Masking preserves LENGTH and NEWLINES, so offsets and line numbers computed on the
319
+ // mask still address the original text and argument text is read back from it.
320
+ //
321
+ // Regex literals are detected by what precedes them — after an operator or an opening
322
+ // bracket a `/` starts a pattern, after a value it is division. That heuristic is the
323
+ // standard one and it can be wrong; being wrong here masks or unmasks a few characters
324
+ // and can only add a finding, never hide one, which is the direction this scan fails in
325
+ // everywhere else too.
326
+ function maskNonCode(src) {
327
+ const out = src.split('');
328
+ const blank = (i) => { if (out[i] !== '\n') out[i] = ' '; };
329
+ let i = 0;
330
+ let prev = '';
331
+ while (i < src.length) {
332
+ const c = src[i];
333
+ const next = src[i + 1];
334
+ if (c === '/' && next === '/') {
335
+ while (i < src.length && src[i] !== '\n') { blank(i); i += 1; }
336
+ continue;
337
+ }
338
+ if (c === '/' && next === '*') {
339
+ blank(i); blank(i + 1); i += 2;
340
+ while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) { blank(i); i += 1; }
341
+ if (i < src.length) { blank(i); blank(i + 1); i += 2; }
342
+ continue;
343
+ }
344
+ if (c === "'" || c === '"' || c === '`') {
345
+ i += 1; // keep the opening quote so string-shaped args stay recognisable
346
+ while (i < src.length && src[i] !== c) {
347
+ if (src[i] === '\\') { blank(i); i += 1; }
348
+ if (i < src.length) { blank(i); i += 1; }
349
+ }
350
+ if (i < src.length) i += 1;
351
+ prev = c;
352
+ continue;
353
+ }
354
+ if (c === '/' && /[(,=:[!&|?{};+\-*%<>~^]/.test(prev)) {
355
+ i += 1;
356
+ while (i < src.length && src[i] !== '/' && src[i] !== '\n') {
357
+ if (src[i] === '\\') { blank(i); i += 1; }
358
+ if (i < src.length) { blank(i); i += 1; }
359
+ }
360
+ if (i < src.length && src[i] === '/') i += 1;
361
+ prev = '/';
362
+ continue;
363
+ }
364
+ if (!/\s/.test(c)) prev = c;
365
+ i += 1;
366
+ }
367
+ return out.join('');
368
+ }
369
+
370
+ // ---------------------------------------------------------------------------
371
+ // the scan
372
+ // ---------------------------------------------------------------------------
373
+
374
+ // Which argument carries the executed path? Always arg 0. When arg 0 is the node
375
+ // binary, the SCRIPT is the first element of the args array, so that is checked too —
376
+ // `execFile(process.execPath, [path.join(work, rel)])` is the exact defect and its
377
+ // arg 0 is perfectly static.
378
+ function executedExpressions(args) {
379
+ const out = [];
380
+ if (!args.length) return out;
381
+ out.push({ expr: args[0], role: 'executable' });
382
+ const isNode = /process\.execPath/.test(args[0]) || /^['"](?:node|node\.exe)['"]$/.test(args[0].trim());
383
+ if (isNode && args[1] && args[1].trim().startsWith('[')) {
384
+ const first = splitArgs(args[1].trim().slice(1, -1))[0];
385
+ if (first) out.push({ expr: first, role: 'script' });
386
+ }
387
+ return out;
388
+ }
389
+
390
+ // Record one finding if the expression is not statically rooted.
391
+ function consider(findings, { rel, masked, src, idx, callText, role, expr, safe }) {
392
+ if (expr == null || isStatic(expr, safe)) return;
393
+ findings.push({
394
+ file: rel,
395
+ line: src.slice(0, idx).split('\n').length,
396
+ role,
397
+ expr: expr.replace(/\s+/g, ' ').slice(0, 120),
398
+ call: callText,
399
+ });
400
+ }
401
+
402
+ function scanFile(rel, src) {
403
+ // Everything regex-matched below runs against `masked`; argument TEXT is read back
404
+ // from `src` at the same offsets, which masking keeps aligned.
405
+ const masked = maskNonCode(src);
406
+ // Bindings are resolved from the ORIGINAL text: masking blanks string bodies, and the
407
+ // require specifier 'node:child_process' IS a string body — reading bindings off the
408
+ // mask finds none, and the scan silently passes every file (caught in review).
409
+ const { direct, namespaces } = resolveBindings(src);
410
+ const safe = safeIdentifiers(masked);
411
+ const findings = [];
412
+
413
+ // Every way a spawner can be reached: a bound name, a namespace member, and the
414
+ // unbound require('child_process').x() form. NOT short-circuited on "no bindings" —
415
+ // the inline form has none by construction.
416
+ const forms = [
417
+ ...[...direct].map((n) => `(?<![.\\w$])${n}\\s*\\(`),
418
+ ...[...namespaces].map((ns) => `(?<![.\\w$])${ns}\\s*\\.\\s*(?:${[...SPAWNERS].join('|')})\\s*\\(`),
419
+ ];
420
+
421
+ // Bound spawner calls are matched against the MASK, so prose can never be one.
422
+ const hits = [];
423
+ if (forms.length) {
424
+ const bound = new RegExp(forms.join('|'), 'g');
425
+ let b;
426
+ while ((b = bound.exec(masked))) hits.push(b);
427
+ }
428
+
429
+ // The UNBOUND form is matched against the ORIGINAL text, because the thing that
430
+ // identifies it IS a specifier string and masking blanks string bodies — the same trap
431
+ // that once made binding resolution see nothing at all. Prose is still excluded
432
+ // without re-reading the raw text wholesale: had the construct sat inside a comment or
433
+ // a string, its trailing '(' would have been blanked too, so the mask is consulted for
434
+ // that one character.
435
+ INLINE_CP_RE.lastIndex = 0;
436
+ let im;
437
+ while ((im = INLINE_CP_RE.exec(src))) {
438
+ if (masked[im.index + im[0].length - 1] === '(') hits.push(im);
439
+ }
440
+
441
+ for (const m of hits) {
442
+ const openIdx = m.index + m[0].length - 1;
443
+ const argText = readCallArgs(src, openIdx);
444
+ if (argText == null) continue;
445
+ const args = splitArgs(argText);
446
+ const callText = m[0].slice(0, -1).trim().replace(/\s+/g, '');
447
+ for (const { expr, role } of executedExpressions(args)) {
448
+ consider(findings, { rel, masked, src, idx: m.index, callText, role, expr, safe });
449
+ }
450
+ }
451
+
452
+ // Dynamic require(): the server loads and RUNS that module in-process. Same question,
453
+ // different mechanism — src/module-loader/loader.js is the site the audit named.
454
+ REQUIRE_CALL_RE.lastIndex = 0;
455
+ let r;
456
+ while ((r = REQUIRE_CALL_RE.exec(masked))) {
457
+ const openIdx = r.index + r[0].length - 1;
458
+ const argText = readCallArgs(src, openIdx);
459
+ if (argText == null) continue;
460
+ const spec = splitArgs(argText)[0];
461
+ consider(findings, {
462
+ rel, masked, src, idx: r.index, callText: 'require', role: 'module', expr: spec, safe,
463
+ });
464
+ }
465
+
466
+ return findings;
467
+ }
468
+
469
+ // Every tracked first-party server file. Browser assets under public/ never spawn.
470
+ function trackedServerFiles() {
471
+ try {
472
+ // DIRECTORY pathspecs, filtered by extension here, rather than '**/*.js' globs.
473
+ // NOT a bug fix — the glob form enumerated the same 351 files, because a git pathspec
474
+ // '*' crosses '/' (fnmatch without FNM_PATHNAME), so 'src/*.js' already matched every
475
+ // nested file and 'src/**/*.js' — which matches only 63 — was pure redundancy. That
476
+ // equivalence is a non-obvious property of git that a reader has to know to trust the
477
+ // line, and the cost of being wrong about it is coverage that narrows with no signal.
478
+ // A directory pathspec needs no such knowledge. Pinned by the enumeration test below.
479
+ const out = execFileSync('git', ['ls-files', '--', 'src', 'modules'],
480
+ { cwd: REPO_ROOT, encoding: 'utf8' });
481
+ return [...new Set(out.split('\n').map((x) => x.trim()).filter(Boolean))]
482
+ .filter((f) => f.endsWith('.js'))
483
+ .filter((f) => !f.includes('/public/') && !f.includes('/app/'));
484
+ } catch {
485
+ return [];
486
+ }
487
+ }
488
+
489
+ function exemptionFor(f) {
490
+ return EXEMPTIONS.find((e) => e.at === `${f.file}:${f.line}`) || null;
491
+ }
492
+
493
+ function checkExecPathsStatic({ files, read } = {}) {
494
+ const list = files || trackedServerFiles();
495
+ if (!list.length) {
496
+ return {
497
+ name: NAME, ok: false, hardFail: true,
498
+ violations: ['scan defect — enumerated 0 tracked server files; a broken enumeration, not a clean result.'],
499
+ warnings: [], note: 'static scan of src/ + modules/ for executed paths.',
500
+ };
501
+ }
502
+ const reader = read || ((rel) => {
503
+ try { return fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8'); } catch { return null; }
504
+ });
505
+
506
+ const violations = [];
507
+ // Exemptions are NOT warnings. Four reviewed entries emitted as warnings make this check
508
+ // read WARN on every single run, and a check that is permanently yellow is one people stop
509
+ // reading. They ride in `exempt` instead: counted in the note, listed in full by the
510
+ // standalone run (node scripts/gds/exec-path-guard.js).
511
+ const exempt = [];
512
+ let scanned = 0;
513
+ let callSites = 0;
514
+ for (const rel of list) {
515
+ const src = reader(rel);
516
+ if (src == null) continue;
517
+ scanned += 1;
518
+ for (const f of scanFile(rel, src)) {
519
+ callSites += 1;
520
+ const ex = exemptionFor(f);
521
+ if (ex) { exempt.push(`${f.file}:${f.line} exempt — ${ex.why}`); continue; }
522
+ violations.push(
523
+ `${f.file}:${f.line} ${f.call}() runs a ${f.role} path that is not statically rooted at the core `
524
+ + `checkout: \`${f.expr}\`. The server executes it with its OWN environment — DATABASE_URL, `
525
+ + 'BUILDER_SECRET_KEY — so a path assembled at runtime is a path an untrusted tree can steer. '
526
+ + 'Root it at __dirname or a module-level const, or add a reviewed EXEMPTIONS entry in '
527
+ + 'scripts/gds/exec-path-guard.js saying why a runtime path is correct here.',
528
+ );
529
+ }
530
+ }
531
+
532
+ return {
533
+ name: NAME,
534
+ ok: violations.length === 0,
535
+ hardFail: violations.length > 0,
536
+ violations,
537
+ warnings: [],
538
+ exempt,
539
+ note: `${scanned} server file(s) scanned for child_process calls; ${callSites} non-static executed `
540
+ + `path(s) found, ${EXEMPTIONS.length} reviewed exemption(s). An executed path must be built only `
541
+ + 'from literals, __dirname/__filename, process.execPath and module-level consts — the server runs '
542
+ + 'it with its own credentials, so "what runs" must be answerable by reading the call site.',
543
+ };
544
+ }
545
+
546
+ function main() {
547
+ const r = checkExecPathsStatic();
548
+ for (const v of r.violations) console.error(`✗ ${v}`);
549
+ for (const w of r.exempt) console.log(` - ${w}`);
550
+ console.log(`${r.hardFail ? 'FAIL' : 'PASS'} ${r.note}`);
551
+ process.exit(r.hardFail ? 1 : 0);
552
+ }
553
+
554
+ if (require.main === module) main();
555
+
556
+ // Exported surface = what fitness.js, the standalone main and the tests actually use.
557
+ // stripStrings/maskNonCode/isStatic are exported because the template-literal bypass
558
+ // above is the kind of defect that needs a test at ITS OWN altitude, not only through
559
+ // scanFile — a unit-level probe is how that regression stays visible.
560
+ module.exports = {
561
+ checkExecPathsStatic, scanFile, resolveBindings, trackedServerFiles,
562
+ stripStrings, maskNonCode, isStatic, safeIdentifiers,
563
+ EXEMPTIONS, NAME,
564
+ };
@@ -1406,7 +1406,7 @@ const CHECKS = [
1406
1406
  // migrations: same shared, monotonic, human-allocated number line, and until this
1407
1407
  // landed only one of the two was enforced. An ADR number is a citation target, so a
1408
1408
  // duplicate is wrong forever (ADR 0195).
1409
- require('./adr-namespace.js').checkAdrNumbering,
1409
+ require('./adr-namespace.js').checkAdrNumbering, require('./exec-path-guard.js').checkExecPathsStatic, // Check 32 — task 1003371 / B50: what the server EXECUTES, out of whose tree — the blind spot all three 2026-08-29 CRITICALs shared. Rationale + reviewed exemptions live in that file. Sharing Check 31's line (the 19c/19d + 23/24 precedent above): fitness.js is AT the 1,500 ratchet and a new check cannot afford a line until task 1003825 buys it back.
1410
1410
  ];
1411
1411
 
1412
1412
  function runAll() {
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.704'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.705'; // 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');
@@ -0,0 +1,248 @@
1
+ // tests/exec_path_guard.mjs — task 1003371, audit ref B50 (2026-08-29 security audit).
2
+ //
3
+ // WHAT THE CHECK IS FOR. Nothing in the repo asked what the SERVER EXECUTES, or out of
4
+ // whose tree. route-rank-check asks who may call a route; permission-path-check asks who
5
+ // may edit a file; neither looks at execution. All three CRITICALs in the audit lived in
6
+ // that gap.
7
+ //
8
+ // WHAT THESE TESTS PIN. Two things, and the second is the one that matters:
9
+ // 1. the check CATCHES the defect shape — a script path assembled from a runtime value;
10
+ // 2. it does not catch everything that merely LOOKS like one. A scan that fires on
11
+ // `/re/.exec(s)` or on the word "spawn (" inside a sentence gets exempted into
12
+ // uselessness within a month, so the negative cases are pinned as hard as the
13
+ // positive ones. Both of those false positives were real during development.
14
+ import assert from 'node:assert/strict';
15
+ import { test } from 'node:test';
16
+ import { createRequire } from 'node:module';
17
+ import path from 'node:path';
18
+ import { execFileSync } from 'node:child_process';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ // fileURLToPath, not URL.pathname. Both a test (tests/test_path_guard.mjs) and a
22
+ // fitness check (scripts/gds/test-path-guard.js, Check 30) reject the pathname form.
23
+ const require = createRequire(import.meta.url);
24
+ const ROOT = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
25
+ const g = require(path.join(ROOT, 'scripts', 'gds', 'exec-path-guard.js'));
26
+
27
+ const scan = (src, rel = 'modules/probe/x.js') => g.scanFile(rel, src);
28
+ const CP = "const { execFile, execFileSync, spawn, execSync } = require('node:child_process');\n";
29
+
30
+ // ---- the defect shape it must catch ---------------------------------------
31
+
32
+ test('a script path built from a runtime value is flagged', () => {
33
+ // conflict-resolve.js:382 in miniature: arg 0 is perfectly static (process.execPath)
34
+ // and the DANGER is in the args array, which is why the check reads inside it.
35
+ const f = scan(CP + "execFile(process.execPath, [path.join(work, rel)], { cwd: work });");
36
+ assert.equal(f.length, 1, 'the clone-rooted script path must be reported');
37
+ assert.equal(f[0].role, 'script', 'the finding must name the args-array script, not arg 0');
38
+ assert.match(f[0].expr, /work/);
39
+ });
40
+
41
+ test('a non-static EXECUTABLE is flagged too, not just the script', () => {
42
+ const f = scan(CP + 'spawn(bin, args);');
43
+ assert.equal(f.length, 1);
44
+ assert.equal(f[0].role, 'executable');
45
+ });
46
+
47
+ test('a path laundered through a helper still reads as non-static (fails toward noise)', () => {
48
+ // The scan does not follow values across functions. The honest consequence is that an
49
+ // indirect path is FLAGGED rather than missed — pinned so the direction of failure is
50
+ // a decision on record, not an accident.
51
+ const f = scan(CP + 'execFile(process.execPath, [resolveScript()], {});');
52
+ assert.equal(f.length, 1);
53
+ });
54
+
55
+ // ---- what it must NOT catch ------------------------------------------------
56
+
57
+ test("a regex's .exec() is never mistaken for child_process.exec", () => {
58
+ // The dominant false positive: `.exec(` outnumbers real spawner calls ~5:1 in this repo.
59
+ const src = "const m = /^Bearer\\s+(\\w+)$/.exec(header);\nconst k = RE.exec(line);";
60
+ assert.deepEqual(scan(src), [], 'a file that never requires child_process has no call sites');
61
+ });
62
+
63
+ test('the word "spawn (" inside a string is not a call site', () => {
64
+ // Real regression: modules/grading/grader.js carries "before worker spawn
65
+ // (GRADER_BYPASS_ENABLED / runOpts.bypass)" in a note string, and the first draft
66
+ // reported that sentence as an executed path.
67
+ const src = CP + "const note = 'Grader bypassed before worker spawn (GRADER_BYPASS_ENABLED / x) — none ran.';";
68
+ assert.deepEqual(scan(src), []);
69
+ });
70
+
71
+ test('a call site inside a comment is not a call site', () => {
72
+ assert.deepEqual(scan(CP + '// execFile(process.execPath, [path.join(work, rel)]);'), []);
73
+ assert.deepEqual(scan(CP + '/* spawn(bin, args); */'), []);
74
+ });
75
+
76
+ test('a literal binary and a __dirname-rooted script are static', () => {
77
+ assert.deepEqual(scan(CP + "execSync('git rev-parse HEAD');"), []);
78
+ assert.deepEqual(scan(CP + "execFile(process.execPath, [path.join(__dirname, 'serve.js')]);"), []);
79
+ });
80
+
81
+ test('a module-level const rooted at __dirname is static, transitively', () => {
82
+ const src = CP
83
+ + "const KIT_DIR = path.join(__dirname, 'kit');\n"
84
+ + "const ENTRY = path.join(KIT_DIR, 'serve.js');\n"
85
+ + 'spawn(process.execPath, [ENTRY]);';
86
+ assert.deepEqual(scan(src), [], 'const → const → __dirname must resolve to static');
87
+ });
88
+
89
+ test('a const declared INSIDE a function is not a fixed root', () => {
90
+ // Module level is column 0. A const rebound per call is a runtime value however it reads.
91
+ const src = CP + 'function run(work) {\n const entry = path.join(work, "gen.js");\n spawn(process.execPath, [entry]);\n}';
92
+ assert.equal(scan(src).length, 1);
93
+ });
94
+
95
+ // ---- template literals: the one-character bypass (grade round 2) ----------
96
+
97
+ test('a path built from a TEMPLATE LITERAL is flagged, not silently static', () => {
98
+ // The bypass: stripStrings() used to blank a whole template, leaving zero free
99
+ // identifiers, and isStatic()'s every() over an empty set is vacuously true. Writing a
100
+ // backtick instead of calling path.join defeated the entire check for exactly the
101
+ // runtime-derived path B1/B50 are about.
102
+ const f = scan(CP + 'execFile(process.execPath, [`${work}/gen.js`]);');
103
+ assert.equal(f.length, 1, 'a template-literal script path must be reported');
104
+ assert.match(f[0].expr, /work/);
105
+ });
106
+
107
+ test('a template-literal require() specifier is flagged too', () => {
108
+ assert.equal(scan('const m = require(`${dir}/routes.js`);').length, 1);
109
+ });
110
+
111
+ test('stripStrings keeps interpolated expressions and drops literal text', () => {
112
+ // Pinned at unit altitude: a regression here is invisible through scanFile alone,
113
+ // because the symptom is a finding that quietly stops appearing.
114
+ assert.match(g.stripStrings('`${a}/x/${b}`'), /a/);
115
+ assert.match(g.stripStrings('`${a}/x/${b}`'), /b/);
116
+ assert.doesNotMatch(g.stripStrings('`${a}/secretword/${b}`'), /secretword/);
117
+ assert.equal(g.isStatic('`${work}/gen.js`', new Set(['path', '__dirname'])), false);
118
+ assert.equal(g.isStatic('`no-holes-here`', new Set(['path'])), true, 'a hole-less template is a constant');
119
+ });
120
+
121
+ test('a template rooted at __dirname is still static', () => {
122
+ assert.deepEqual(scan(CP + 'execFile(process.execPath, [`${__dirname}/serve.js`]);'), []);
123
+ });
124
+
125
+ // ---- the unbound + ESM forms (grade round 1: the check's own contract) -----
126
+
127
+ test('an UNBOUND require(...).execFileSync(runtimePath) is caught', () => {
128
+ // The gap that failed the first grade. This form binds no name, and the first draft
129
+ // short-circuited the whole file on "no bindings" — so a file spawning this way was
130
+ // skipped entirely. Not hypothetical: scripts/gds/autonomy-gate.js and
131
+ // scripts/gds/ship-land.js already use exactly this shape.
132
+ const f = scan("require('node:child_process').execFileSync(process.execPath, [path.join(work, rel)]);");
133
+ assert.equal(f.length, 1, 'the unbound spawner form must not escape the scan');
134
+ assert.equal(f[0].role, 'script');
135
+ });
136
+
137
+ test('a file with NO child_process binding is still scanned, not skipped wholesale', () => {
138
+ // The severity of the round-1 finding was not the missed call — it was that ONE
139
+ // unbound call made the scan abandon the ENTIRE file.
140
+ const src = "require('child_process').exec(userCmd);\nconst x = 1;";
141
+ assert.equal(scan(src).length, 1);
142
+ });
143
+
144
+ test('an ESM named import of a spawner is tracked, renames included', () => {
145
+ const src = "import { execFile as run } from 'node:child_process';\nrun(process.execPath, [path.join(work, 'x.js')]);";
146
+ assert.equal(scan(src).length, 1, 'the repo is CJS today; a check that only reads CJS dies silently on conversion');
147
+ });
148
+
149
+ // ---- dynamic require(): same question, different mechanism -----------------
150
+
151
+ test('a require() of a runtime path is reported as an executed module', () => {
152
+ // src/module-loader/loader.js:190 is the second site the audit named. The server
153
+ // LOADS AND RUNS that module in-process, which is execution by another name.
154
+ const f = scan("const m = require(path.join(dir, 'routes', rk + '.js'));");
155
+ assert.equal(f.length, 1);
156
+ assert.equal(f[0].role, 'module');
157
+ });
158
+
159
+ test('an ordinary literal require is not a finding', () => {
160
+ // The whole repo is built on these; firing on them would make the check unusable.
161
+ assert.deepEqual(scan("const path = require('node:path');\nconst db = require('../db');"), []);
162
+ });
163
+
164
+ // ---- enumeration coverage --------------------------------------------------
165
+
166
+ test('enumeration is complete for src/ and modules/, and not glob-dependent', () => {
167
+ // Round-1 finding: the scan enumerated via 'src/**/*.js'-style pathspecs, whose
168
+ // completeness rests on git matching '*' ACROSS '/' — true, but a property a reader
169
+ // must already know to trust the line. This pins the result rather than the spelling:
170
+ // whatever the implementation, it must see every tracked .js under src/ and modules/
171
+ // that is not a browser or desktop-app asset.
172
+ const listed = new Set(g.trackedServerFiles());
173
+ const expected = execFileSync('git', ['ls-files', '--', 'src', 'modules'], { cwd: ROOT, encoding: 'utf8' })
174
+ .split('\n').map((s) => s.trim()).filter(Boolean)
175
+ .filter((f) => f.endsWith('.js'))
176
+ .filter((f) => !f.includes('/public/') && !f.includes('/app/'));
177
+ const missing = expected.filter((f) => !listed.has(f));
178
+ assert.deepEqual(missing, [], 'every tracked server .js must be scanned — silent narrowing is the one failure this check cannot survive');
179
+ assert.ok(expected.length > 300, `enumeration collapsed to ${expected.length} files`);
180
+ });
181
+
182
+ // ---- binding resolution ----------------------------------------------------
183
+
184
+ test('only names actually bound to child_process count as spawners', () => {
185
+ // `spawn` here is a local helper with nothing to do with processes.
186
+ const src = "const spawn = (n) => new Array(n);\nspawn(itemCount);";
187
+ assert.deepEqual(scan(src), []);
188
+ });
189
+
190
+ test('a renamed destructured binding is still tracked', () => {
191
+ const src = "const { execFile: ef } = require('node:child_process');\nef(process.execPath, [path.join(work, 'x.js')]);";
192
+ assert.equal(scan(src).length, 1, 'an alias must not escape the scan');
193
+ });
194
+
195
+ test('a namespace binding (cp.execFile) is tracked', () => {
196
+ const src = "const cp = require('child_process');\ncp.execFile(process.execPath, [path.join(work, 'x.js')]);";
197
+ assert.equal(scan(src).length, 1);
198
+ });
199
+
200
+ test('bindings survive masking — the require specifier is a string body', () => {
201
+ // Regression: resolving bindings from the MASKED source blanked
202
+ // 'node:child_process' itself, so no file had bindings and the whole scan passed
203
+ // vacuously. A check that silently passes everything is worse than no check.
204
+ const { direct } = g.resolveBindings(CP);
205
+ assert.ok(direct.has('execFile') && direct.has('spawn'), 'bindings must resolve from real source');
206
+ });
207
+
208
+ // ---- the exemption list is a review, not a mute ----------------------------
209
+
210
+ test('every exemption names a file:line and carries a substantive reason', () => {
211
+ for (const e of g.EXEMPTIONS) {
212
+ assert.match(e.at, /^[\w./-]+:\d+$/, `${e.at} must be a repo-relative file:line`);
213
+ assert.ok(e.why && e.why.length > 80,
214
+ `${e.at} must record WHY a runtime path is correct there — a bare entry is a mute, not a review.`);
215
+ }
216
+ });
217
+
218
+ test('the motivating CRITICAL is on the list with its compensating control named', () => {
219
+ // If conflict-resolve.js:382 ever drops off this list silently, the audit finding has
220
+ // been forgotten rather than fixed.
221
+ const e = g.EXEMPTIONS.find((x) => x.at.startsWith('modules/lifecycle/conflict-resolve.js'));
222
+ assert.ok(e, 'the B1 site must be accounted for explicitly');
223
+ assert.match(e.why, /computeExecutedClosure|1003367/,
224
+ 'the exemption must name the control that makes it safe, not merely assert it is');
225
+ });
226
+
227
+ // ---- the check runs clean on the real tree --------------------------------
228
+
229
+ test('the repo passes, and the scan is not vacuous', () => {
230
+ const r = g.checkExecPathsStatic();
231
+ assert.equal(r.hardFail, false, `unexempted executed paths:\n${r.violations.join('\n')}`);
232
+ assert.ok(/\b(\d{2,})\s+server file\(s\) scanned/.test(r.note),
233
+ 'a scan reporting a handful of files has broken its enumeration');
234
+ assert.equal(r.exempt.length, g.EXEMPTIONS.length,
235
+ 'every exemption must still match a real finding — a stale one means the code moved and the '
236
+ + 'review was silently carried forward');
237
+ });
238
+
239
+ test('exemptions never surface as warnings — a permanently-yellow check stops being read', () => {
240
+ const r = g.checkExecPathsStatic();
241
+ assert.deepEqual(r.warnings, [], 'reviewed exemptions belong in `exempt`, not `warnings`');
242
+ assert.ok(r.exempt.length > 0, 'and they must still be reported somewhere');
243
+ });
244
+
245
+ test('an empty file list is a scan defect, not a pass', () => {
246
+ const r = g.checkExecPathsStatic({ files: [] });
247
+ assert.equal(r.hardFail, true, 'zero files enumerated must fail closed');
248
+ });