diff --git a/gitnexus/bench/impact-pdg/README.md b/gitnexus/bench/impact-pdg/README.md index 845debfa2..a7bbac3ce 100644 --- a/gitnexus/bench/impact-pdg/README.md +++ b/gitnexus/bench/impact-pdg/README.md @@ -227,10 +227,14 @@ analyze via a temp `GITNEXUS_HOME`, mock-free**. Per fixture: first, keeping the source tree clean). 2. **Shell out** to the real CLI as a child process — child-process isolation sidesteps `process.exit`; real `saveMeta` + `registerRepo` land in the temp - home; parse workers spawn from `dist/` (so the harness needs a built `dist/`): + home. The harness prefers the built `dist/` CLI (plain JS, no tsx; the parse + workers it spawns also load from `dist/`), so it needs a built `dist/`; it + falls back to tsx's own CLI over `src/` for build-free local runs. (`node + --import tsx src/cli/index.ts` is avoided: Node ≥22.18 native type-stripping + breaks the `.ts` entry's `./lazy-action.js`→`.ts` import resolution.) ``` - node --import tsx src/cli/index.ts analyze --pdg --skip-git --index-only + node dist/cli/index.js analyze --pdg --skip-git --index-only ``` 3. `new LocalBackend(); await init()` resolves the fixture via the **real** registry (the parent process sets `GITNEXUS_HOME` too, so `init()` reads the diff --git a/gitnexus/bench/impact-pdg/gate-mutation-recall.mjs b/gitnexus/bench/impact-pdg/gate-mutation-recall.mjs index 5d3e44a0d..2ab46072a 100644 --- a/gitnexus/bench/impact-pdg/gate-mutation-recall.mjs +++ b/gitnexus/bench/impact-pdg/gate-mutation-recall.mjs @@ -17,7 +17,14 @@ const floor = Number(process.env.MUTATION_RECALL_FLOOR ?? '0.5'); const report = JSON.parse(fs.readFileSync(reportPath, 'utf8')); const checks = Array.isArray(report?.mutation?.checks) ? report.mutation.checks : []; -const scored = checks.filter((c) => typeof c.recall === 'number'); +// Gate only the checks the oracle marked recall-gated. measure.mjs sets +// `recallGated: false` for cases a forward value-diff oracle cannot fairly +// score against the PDG slice: UPSTREAM fixtures (the oracle runs in its native +// downstream sense, so its behavioral AIS can never intersect a reverse slice — +// recall is 0 by construction) and id-discrimination corroboration fixtures. +// Those still carry a numeric `recall` for the report, so the legacy +// `typeof c.recall === 'number'` filter wrongly tripped the floor on them. +const scored = checks.filter((c) => c.recallGated === true && typeof c.recall === 'number'); const recalls = scored.map((c) => c.recall); const min = recalls.length ? Math.min(...recalls) : null; const mean = recalls.length ? recalls.reduce((a, b) => a + b, 0) / recalls.length : null; @@ -39,6 +46,17 @@ if (process.env.GITHUB_STEP_SUMMARY) { } process.stdout.write(summary + '\n'); +// A report that produced checks but gated NONE of them has no recall signal: +// the floor check below would pass vacuously (`min === null`). Fail loudly so a +// degenerate corpus, or a harvest that silently emptied every behavioral AIS, +// surfaces as a red run instead of a green "scored cases: 0 of N". +if (checks.length > 0 && scored.length === 0) { + console.error( + `Mutation gate has no signal: 0 of ${checks.length} checks were recall-gated — refusing to pass.`, + ); + process.exit(1); +} + if (min !== null && min < floor) { console.error(`Mutation recall regression: min realized recall ${fmt(min)} < floor ${floor}`); process.exit(1); diff --git a/gitnexus/bench/impact-pdg/measure.mjs b/gitnexus/bench/impact-pdg/measure.mjs index 65d3a7675..f6158f1da 100644 --- a/gitnexus/bench/impact-pdg/measure.mjs +++ b/gitnexus/bench/impact-pdg/measure.mjs @@ -25,11 +25,16 @@ * `repo-manager.getGlobalDir()` — it roots the registry; the per-repo DB * lands in `/.gitnexus/`, so fixtures are copied to a temp * working dir to keep the source tree clean); - * 2. SHELL OUT to the real CLI as a child process: - * node --import tsx src/cli/index.ts analyze --pdg --skip-git --index-only - * (child-process isolation sidesteps `process.exit`; real `saveMeta` + - * `registerRepo` land in the temp home; workers spawn from `dist/`, so the - * harness builds `dist/` first — run `node scripts/build.js`); + * 2. SHELL OUT to the real CLI as a child process (see `cliChildArgs`): + * node dist/cli/index.js analyze --pdg --skip-git --index-only + * preferring the BUILT `dist/` CLI when present — plain JS, no tsx, and the + * parse workers it spawns also load from `dist/`. The mutation workflow + * builds `dist/` first (`node scripts/build.js`); `node --import tsx + * src/cli/index.ts` is NOT used because Node >=22.18 native type-stripping + * breaks the `.js`->`.ts` entry resolution (ERR_MODULE_NOT_FOUND on + * `lazy-action.js`). Build-free runs fall back to tsx's own CLI over src. + * (Child-process isolation sidesteps `process.exit`; real `saveMeta` + + * `registerRepo` land in the temp home); * 3. `new LocalBackend(); await init()` resolves the fixture via the REAL * registry (the parent process ALSO sets `GITNEXUS_HOME` so init reads the * temp registry, not the user's ~/.gitnexus); @@ -53,6 +58,7 @@ import os from 'node:os'; import path from 'node:path'; import crypto from 'node:crypto'; import { spawnSync } from 'node:child_process'; +import { createRequire } from 'node:module'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { @@ -95,6 +101,33 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..'); // gitnexus/ const FIXTURES_DIR = path.join(__dirname, 'fixtures'); const BASELINE_PATH = path.join(__dirname, 'baselines.json'); const CLI_ENTRY = path.join(REPO_ROOT, 'src', 'cli', 'index.ts'); +// Shipped CLI entry (package.json `bin`). PREFERRED for the child analyze: it's +// plain compiled JS, so the analyze process — AND the parse workers it spawns, +// which resolve relative to the running entry — load from `dist/` with no tsx in +// the loop. The build-free path below stays as a fallback. +const DIST_CLI = path.join(REPO_ROOT, 'dist', 'cli', 'index.js'); +// Build-free fallback: tsx's OWN cli entry (resolved from this package), NOT +// `node --import tsx .ts`. On Node >=22.18 native TypeScript type- +// stripping is enabled by default and intercepts the `.ts` entry before tsx's +// `--import` resolve hook applies; native stripping does NOT remap `./foo.js` +// specifiers to `foo.ts` (tsx does), so `node --import tsx src/cli/index.ts` +// crashes resolving `./lazy-action.js` (ERR_MODULE_NOT_FOUND) on newer Node. +// The tsx CLI takes over module loading and is version-agnostic across the +// declared engines range (node >=22.0, where `--no-experimental-strip-types` +// is not a universally-recognized flag). Workers still spawn from src via tsx on +// this path, so it is only robust on the older Node devs run locally. +const TSX_CLI = createRequire(import.meta.url).resolve('tsx/cli'); + +/** + * Build the argv that runs the real CLI as a child of `process.execPath`. + * Prefers the built `dist/` CLI (production-faithful, no tsx, dist workers) when + * present — this is what the mutation workflow uses (it builds dist first). Falls + * back to the tsx CLI over src for build-free local runs. Returns the args AFTER + * the node binary, i.e. ready for `spawnSync(process.execPath, [...args])`. + */ +function cliChildArgs(rest) { + return fs.existsSync(DIST_CLI) ? [DIST_CLI, ...rest] : [TSX_CLI, CLI_ENTRY, ...rest]; +} const SCOPES = ['intra', 'inter', 'mixed']; const MODES = ['callgraph', 'pdg']; @@ -138,7 +171,7 @@ async function analyzeAndImpact(fx, home, { pdgOn = true } = {}) { fs.cpSync(path.join(fx.dir, 'src'), path.join(work, 'src'), { recursive: true }); const env = { ...process.env, GITNEXUS_HOME: home }; - const args = ['--import', 'tsx', CLI_ENTRY, 'analyze', work, '--skip-git', '--index-only']; + const args = cliChildArgs(['analyze', work, '--skip-git', '--index-only']); if (pdgOn) args.push('--pdg'); const an = spawnSync(process.execPath, args, { env, diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index b28f005d7..927fe93c3 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -52,6 +52,10 @@ "gitnexus": "dist/cli/index.js" }, "devDependencies": { + "@babel/generator": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@types/busboy": "^1.5.4", "@types/cli-progress": "^3.11.6", "@types/cors": "^2.8.17", @@ -76,10 +80,59 @@ "typescript": "^6.0.3" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -87,9 +140,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -97,13 +150,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -112,15 +165,49 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1128,6 +1215,17 @@ "node": ">=18.0.0" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1471,9 +1569,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1491,9 +1586,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1511,9 +1603,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1531,9 +1620,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1551,9 +1637,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1571,9 +1654,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3487,6 +3567,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-bignum": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz", @@ -3668,9 +3761,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3692,9 +3782,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3716,9 +3803,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3740,9 +3824,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/gitnexus/package.json b/gitnexus/package.json index 6fa0c9007..82f2cabfd 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -94,6 +94,10 @@ "uuid": "^14.0.0" }, "devDependencies": { + "@babel/generator": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@types/busboy": "^1.5.4", "@types/cli-progress": "^3.11.6", "@types/cors": "^2.8.17",