@orangepro/orangepro-mcp 0.1.0

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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/local/agentWorkflow.js +81 -0
  4. package/dist/local/aiGraph/links.js +635 -0
  5. package/dist/local/analyze/analyzer.js +2129 -0
  6. package/dist/local/analyze/behaviorContracts.js +169 -0
  7. package/dist/local/analyze/boilerplate.js +42 -0
  8. package/dist/local/analyze/callGraph.js +458 -0
  9. package/dist/local/analyze/classify.js +219 -0
  10. package/dist/local/analyze/clustering.js +357 -0
  11. package/dist/local/analyze/confirm.js +2422 -0
  12. package/dist/local/analyze/coverage.js +518 -0
  13. package/dist/local/analyze/coverageArtifacts.js +607 -0
  14. package/dist/local/analyze/frameworks.js +115 -0
  15. package/dist/local/analyze/linkage/conventions.js +160 -0
  16. package/dist/local/analyze/parseCache.js +164 -0
  17. package/dist/local/analyze/selfAssert.js +53 -0
  18. package/dist/local/analyze/symbols.js +430 -0
  19. package/dist/local/analyze/testLayer.js +135 -0
  20. package/dist/local/analyze/treeSitter/engine.js +1253 -0
  21. package/dist/local/analyze/treeSitter/languages.js +101 -0
  22. package/dist/local/autoProve.js +620 -0
  23. package/dist/local/cli.js +1468 -0
  24. package/dist/local/cliArgs.js +112 -0
  25. package/dist/local/corpusScope.js +162 -0
  26. package/dist/local/enrich/csv.js +348 -0
  27. package/dist/local/enrich/index.js +43 -0
  28. package/dist/local/enrich/markdown.js +193 -0
  29. package/dist/local/explain/explain.js +91 -0
  30. package/dist/local/exportCli.js +26 -0
  31. package/dist/local/flows/flowWalker.js +215 -0
  32. package/dist/local/flows/llmFlowDiscovery.js +567 -0
  33. package/dist/local/freshness/changed.js +280 -0
  34. package/dist/local/freshness/manifest.js +35 -0
  35. package/dist/local/freshness/status.js +30 -0
  36. package/dist/local/gaps/gaps.js +114 -0
  37. package/dist/local/generate/buckets.js +73 -0
  38. package/dist/local/generate/compareJudge.js +124 -0
  39. package/dist/local/generate/compareReport.js +538 -0
  40. package/dist/local/generate/compareScore.js +105 -0
  41. package/dist/local/generate/deriveImports.js +91 -0
  42. package/dist/local/generate/generator.js +2586 -0
  43. package/dist/local/generate/prompt.js +144 -0
  44. package/dist/local/generate/promptV5.js +438 -0
  45. package/dist/local/generate/providers.js +400 -0
  46. package/dist/local/generate/runHints.js +304 -0
  47. package/dist/local/graph/citations.js +73 -0
  48. package/dist/local/graph/confirmable.js +72 -0
  49. package/dist/local/graph/factories.js +210 -0
  50. package/dist/local/graph/ontology.js +18 -0
  51. package/dist/local/interactive.js +53 -0
  52. package/dist/local/jobs/jobStore.js +80 -0
  53. package/dist/local/jobs/notify.js +29 -0
  54. package/dist/local/jobs/runner.js +75 -0
  55. package/dist/local/ledger.js +117 -0
  56. package/dist/local/localConfig.js +112 -0
  57. package/dist/local/mcp.js +548 -0
  58. package/dist/local/operations.js +1749 -0
  59. package/dist/local/pack/coverageReport.js +192 -0
  60. package/dist/local/pack/exporter.js +195 -0
  61. package/dist/local/pack/schema.js +128 -0
  62. package/dist/local/pack/summary.js +127 -0
  63. package/dist/local/pack/validate.js +25 -0
  64. package/dist/local/proofRunnability.js +366 -0
  65. package/dist/local/recipe/dbSqljs.js +255 -0
  66. package/dist/local/reprove/paths.js +13 -0
  67. package/dist/local/reprove/scoped.js +136 -0
  68. package/dist/local/resolve/barrelWalker.js +178 -0
  69. package/dist/local/resolve/exportIndex.js +270 -0
  70. package/dist/local/resolve/importGraph.js +347 -0
  71. package/dist/local/resolve/resolver.js +122 -0
  72. package/dist/local/resolve/resolverCache.js +117 -0
  73. package/dist/local/rtm.js +413 -0
  74. package/dist/local/score/coverage.js +99 -0
  75. package/dist/local/score/doctor.js +67 -0
  76. package/dist/local/score/risk.js +362 -0
  77. package/dist/local/score/score.js +182 -0
  78. package/dist/local/types.js +1 -0
  79. package/dist/local/util/hash.js +16 -0
  80. package/dist/local/util/ids.js +16 -0
  81. package/dist/local/util/progress.js +8 -0
  82. package/dist/local/util/redact.js +39 -0
  83. package/dist/local/util/time.js +1 -0
  84. package/dist/local/util/walk.js +174 -0
  85. package/dist/local/viz/behaviorReportData.js +367 -0
  86. package/dist/local/viz/behaviorReportHtml.js +664 -0
  87. package/dist/local/viz/d3.bundle.js +3 -0
  88. package/dist/local/viz/html.js +1152 -0
  89. package/dist/local/viz/payload.js +525 -0
  90. package/dist/local/workspace.js +99 -0
  91. package/docs/agent-workflow.md +167 -0
  92. package/docs/agents/claude-code.md +43 -0
  93. package/docs/agents/codex.md +52 -0
  94. package/docs/agents/cursor.md +39 -0
  95. package/docs/agents/opencode.md +43 -0
  96. package/docs/agents/vscode.md +34 -0
  97. package/docs/local-proof-kit.md +269 -0
  98. package/package.json +92 -0
  99. package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
  100. package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
  101. package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
  102. package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
  103. package/scripts/spikes/failure-summary.mjs +29 -0
@@ -0,0 +1,2335 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { pickReportableFailureLine } from "./failure-summary.mjs";
8
+
9
+ const DEFAULT_TIMEOUT_MS = 30_000;
10
+ const MAX_REPLACEMENT_CHARS = 8192;
11
+ const MAX_REPLACEMENT_JSON_DEPTH = 24;
12
+ const REPLACEMENT_MODES = new Set(["return-json", "promise-json"]);
13
+
14
+ function usage() {
15
+ return [
16
+ "Usage: node scripts/spikes/dynamic-proof-spike.mjs --root <repo> --test <rel> --target <rel> --method <name> --replacement <sentinel> [--replacement-mode return-json|promise-json] [--test-env KEY=value] [--runner auto|vitest|jest|mocha] [--vitest-config <rel>] [--jest-config <rel>] [--mocha-bin <path>] [--json] [--link-node-modules]",
17
+ "",
18
+ "Runs a baseline test, mutates the target method body in an isolated copy, reruns the same test, and classifies the result.",
19
+ "--replacement must be inert: either empty or a single return of a JSON literal, e.g. 'return {\"ok\":false};'. Use --replacement-mode promise-json for Promise<T> methods.",
20
+ "--link-node-modules is a trusted-repo speed mode: source files are copied, but node_modules is symlinked and must not be treated as write-isolated.",
21
+ "Repo test lifecycle hooks and runner binaries are trusted in this spike; use it only for local measurement on trusted checkouts.",
22
+ "This is a spike harness only; it does not write graph edges or product artifacts."
23
+ ].join("\n");
24
+ }
25
+
26
+ function parseArgs(argv) {
27
+ const args = { json: false, linkNodeModules: false, runner: "auto", replacementMode: "return-json", testEnv: [] };
28
+ for (let i = 0; i < argv.length; i += 1) {
29
+ const arg = argv[i];
30
+ if (arg === "--json") {
31
+ args.json = true;
32
+ continue;
33
+ }
34
+ if (arg === "--link-node-modules") {
35
+ args.linkNodeModules = true;
36
+ continue;
37
+ }
38
+ if (!arg.startsWith("--")) {
39
+ throw new Error(`Unexpected positional argument: ${arg}`);
40
+ }
41
+ const key = arg.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
42
+ const value = argv[i + 1];
43
+ if (value === undefined || (value.startsWith("--") && key !== "replacement")) {
44
+ throw new Error(`Missing value for ${arg}`);
45
+ }
46
+ if (key === "testEnv") {
47
+ args.testEnv.push(value);
48
+ i += 1;
49
+ continue;
50
+ }
51
+ args[key] = value;
52
+ i += 1;
53
+ }
54
+ for (const required of ["root", "test", "target", "method", "replacement"]) {
55
+ if (!Object.prototype.hasOwnProperty.call(args, required)) {
56
+ throw new Error(`Missing required --${required.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)}`);
57
+ }
58
+ }
59
+ if (!["auto", "vitest", "jest", "mocha"].includes(args.runner)) {
60
+ throw new Error("--runner must be one of: auto, vitest, jest, mocha");
61
+ }
62
+ if (!REPLACEMENT_MODES.has(args.replacementMode)) {
63
+ throw new Error("--replacement-mode must be one of: return-json, promise-json");
64
+ }
65
+ return args;
66
+ }
67
+
68
+ function isSecretEnvKey(key) {
69
+ return /TOKEN|SECRET|PASSWORD|API[_-]?KEY|ACCESS[_-]?KEY|PRIVATE[_-]?KEY|SECRET[_-]?KEY|PASSPHRASE|CREDENTIAL|PIN|AUTH|COOKIE|SESSION/i.test(key);
70
+ }
71
+
72
+ // NODE_OPTIONS is the one --test-env key that can change how the runner Node LAUNCHES, so it
73
+ // is not enough to secret-filter it — every flag it carries must be on a fixed known-safe
74
+ // allowlist. This closes the hole where `--test-env NODE_OPTIONS=--require=/evil.js` (or
75
+ // --loader/--import/--inspect) would ride through the generic secret filter. The check lives
76
+ // at THIS spike boundary so a direct caller (not just autoProve) cannot bypass it.
77
+ const ALLOWED_NODE_OPTION = /^(?:--experimental-sqlite|--no-warnings|--max-old-space-size=\d+)$/;
78
+
79
+ function assertAllowedNodeOptions(value) {
80
+ const flags = String(value).trim().split(/\s+/).filter(Boolean);
81
+ if (flags.length === 0) {
82
+ throw new Error("--test-env NODE_OPTIONS must not be empty");
83
+ }
84
+ for (const flag of flags) {
85
+ if (!ALLOWED_NODE_OPTION.test(flag)) {
86
+ throw new Error(
87
+ `--test-env NODE_OPTIONS only permits --experimental-sqlite, --no-warnings, --max-old-space-size=<n>; rejected: ${redactSecrets(flag)}`
88
+ );
89
+ }
90
+ }
91
+ }
92
+
93
+ function parseTestEnv(entries) {
94
+ const env = {};
95
+ for (const entry of entries ?? []) {
96
+ const index = entry.indexOf("=");
97
+ if (index <= 0) {
98
+ throw new Error("--test-env must be formatted as KEY=value");
99
+ }
100
+ const key = entry.slice(0, index);
101
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
102
+ throw new Error(`Invalid --test-env key: ${key}`);
103
+ }
104
+ if (isSecretEnvKey(key)) {
105
+ throw new Error(`Secret-looking --test-env key is not allowed: ${key}`);
106
+ }
107
+ const value = entry.slice(index + 1);
108
+ if (key === "NODE_OPTIONS") {
109
+ assertAllowedNodeOptions(value);
110
+ }
111
+ env[key] = value;
112
+ }
113
+ return env;
114
+ }
115
+
116
+ function parseTimeoutMs(value) {
117
+ if (value === undefined) {
118
+ return DEFAULT_TIMEOUT_MS;
119
+ }
120
+ const parsed = Number(value);
121
+ if (!Number.isInteger(parsed) || parsed <= 0) {
122
+ throw new Error("--timeout-ms must be a positive integer");
123
+ }
124
+ return parsed;
125
+ }
126
+
127
+ function parseSafeReplacement(replacementBody) {
128
+ const trimmed = replacementBody.trim();
129
+ if (trimmed === "") {
130
+ return { expr: null };
131
+ }
132
+ if (trimmed.length > MAX_REPLACEMENT_CHARS) {
133
+ throw new Error(`--replacement is too large; max ${MAX_REPLACEMENT_CHARS} characters`);
134
+ }
135
+ const match = /^return[ \t]+([^\r\n\u2028\u2029]+)$/.exec(trimmed);
136
+ if (!match) {
137
+ throw new Error("--replacement must be an inert sentinel: empty or a single return of a literal value");
138
+ }
139
+ const expr = match[1].replace(/;$/, "").trim();
140
+ if (!expr) {
141
+ throw new Error("--replacement return must include a literal value");
142
+ }
143
+ let parsed;
144
+ try {
145
+ parsed = JSON.parse(expr);
146
+ } catch {
147
+ throw new Error("--replacement must be a single return of a JSON literal; no statements, calls, comments, or trailing code are allowed");
148
+ }
149
+ assertSafeJsonLiteral(parsed);
150
+ return { expr };
151
+ }
152
+
153
+ function buildReplacementBody(replacementBody, mode) {
154
+ const { expr } = parseSafeReplacement(replacementBody);
155
+ if (expr === null) {
156
+ return "";
157
+ }
158
+ if (mode === "return-json") {
159
+ return `return ${expr};`;
160
+ }
161
+ if (mode === "promise-json") {
162
+ return `return Promise.resolve(${expr});`;
163
+ }
164
+ throw new Error("--replacement-mode must be one of: return-json, promise-json");
165
+ }
166
+
167
+ function assertSafeJsonLiteral(value, depth = 0) {
168
+ if (depth > MAX_REPLACEMENT_JSON_DEPTH) {
169
+ throw new Error(`--replacement JSON literal is too deeply nested; max depth ${MAX_REPLACEMENT_JSON_DEPTH}`);
170
+ }
171
+ if (Array.isArray(value)) {
172
+ for (const item of value) {
173
+ assertSafeJsonLiteral(item, depth + 1);
174
+ }
175
+ return;
176
+ }
177
+ if (value && typeof value === "object") {
178
+ for (const [key, nested] of Object.entries(value)) {
179
+ if (key === "__proto__" || key === "prototype" || key === "constructor") {
180
+ throw new Error("--replacement JSON literal may not include prototype-shaped keys");
181
+ }
182
+ assertSafeJsonLiteral(nested, depth + 1);
183
+ }
184
+ }
185
+ }
186
+
187
+ function resolveInside(root, relOrAbs) {
188
+ const resolved = path.resolve(root, relOrAbs);
189
+ const relative = path.relative(root, resolved);
190
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
191
+ throw new Error(`Path escapes root: ${relOrAbs}`);
192
+ }
193
+ return resolved;
194
+ }
195
+
196
+ // ── M-1: mirror the tsconfig `extends` chain into the sandbox ──────────────────────
197
+ // A monorepo package's tsconfig commonly does `extends: "../../tsconfig.json"`. The
198
+ // isolated single-package copy loses that parent, so vite/oxc throws "Tsconfig not found"
199
+ // and the suite fails to TRANSFORM (0 tests) before any body runs → every baseline red.
200
+ // To let the baseline transform we mirror the minimal monorepo subtree: copy the package
201
+ // to tmpRoot/mono/<rel(R, package)> and copy each RELATIVE parent config's BYTES to
202
+ // tmpRoot/mono/<rel(R, parent)>, where R = the common ancestor. This ONLY changes WHAT is
203
+ // present so compilation can succeed; the proof gate (mutation/baseline/mutant/classify)
204
+ // is untouched. Parents are copied as bytes (never writably symlinked), so a test that
205
+ // writes to a parent path mutates only the disposable sandbox, never the user checkout.
206
+ const TSCONFIG_EXTENDS_MAX_DEPTH = 8;
207
+ const TSCONFIG_MAX_PARENT_FILES = 16;
208
+ const TSCONFIG_MAX_PARENT_UP_LEVELS = 8;
209
+ const TSCONFIG_REFERENCE_MAX_DEPTH = 8;
210
+ const TSCONFIG_MAX_REFERENCE_FILES = 128;
211
+
212
+ class TsconfigMirrorAbort extends Error {}
213
+
214
+ function stripJsonComments(text) {
215
+ let out = "";
216
+ let inString = false;
217
+ let quote = "";
218
+ let inLine = false;
219
+ let inBlock = false;
220
+ for (let i = 0; i < text.length; i += 1) {
221
+ const ch = text[i];
222
+ const next = text[i + 1];
223
+ if (inLine) {
224
+ if (ch === "\n") {
225
+ inLine = false;
226
+ out += ch;
227
+ }
228
+ continue;
229
+ }
230
+ if (inBlock) {
231
+ if (ch === "*" && next === "/") {
232
+ inBlock = false;
233
+ i += 1;
234
+ }
235
+ continue;
236
+ }
237
+ if (inString) {
238
+ out += ch;
239
+ if (ch === "\\") {
240
+ out += next ?? "";
241
+ i += 1;
242
+ continue;
243
+ }
244
+ if (ch === quote) {
245
+ inString = false;
246
+ }
247
+ continue;
248
+ }
249
+ if (ch === '"' || ch === "'") {
250
+ inString = true;
251
+ quote = ch;
252
+ out += ch;
253
+ continue;
254
+ }
255
+ if (ch === "/" && next === "/") {
256
+ inLine = true;
257
+ i += 1;
258
+ continue;
259
+ }
260
+ if (ch === "/" && next === "*") {
261
+ inBlock = true;
262
+ i += 1;
263
+ continue;
264
+ }
265
+ out += ch;
266
+ }
267
+ return out.replace(/,(\s*[}\]])/g, "$1");
268
+ }
269
+
270
+ function readTsconfigExtends(configAbs) {
271
+ // Defensive JSONC parse; on any failure treat as no-extends (fail-safe).
272
+ let raw;
273
+ try {
274
+ raw = readFileSync(configAbs, "utf8");
275
+ } catch {
276
+ return [];
277
+ }
278
+ let parsed;
279
+ try {
280
+ parsed = JSON.parse(stripJsonComments(raw));
281
+ } catch {
282
+ return [];
283
+ }
284
+ const ext = parsed?.extends;
285
+ if (typeof ext === "string") {
286
+ return [ext];
287
+ }
288
+ if (Array.isArray(ext)) {
289
+ return ext.filter(entry => typeof entry === "string");
290
+ }
291
+ return [];
292
+ }
293
+
294
+ function isInsideDir(root, candidate) {
295
+ const relative = path.relative(root, candidate);
296
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
297
+ }
298
+
299
+ function resolveExtendsTarget(fromDir, value) {
300
+ // null → bare module (resolved from node_modules, #167); throws → policy violation.
301
+ if (path.isAbsolute(value)) {
302
+ throw new TsconfigMirrorAbort("absolute extends is not mirrored");
303
+ }
304
+ if (!value.startsWith(".")) {
305
+ return null;
306
+ }
307
+ const base = path.resolve(fromDir, value);
308
+ const candidates = value.endsWith(".json") ? [base] : [`${base}.json`];
309
+ const found = candidates.find(candidate => existsSync(candidate) && lstatSync(candidate).isFile());
310
+ if (!found) {
311
+ throw new TsconfigMirrorAbort(`unresolvable extends: ${value}`);
312
+ }
313
+ return found;
314
+ }
315
+
316
+ function assertWithinUpBound(packageRoot, target) {
317
+ const upLevels = path.relative(packageRoot, target).split(path.sep).filter(segment => segment === "..").length;
318
+ if (upLevels > TSCONFIG_MAX_PARENT_UP_LEVELS) {
319
+ throw new TsconfigMirrorAbort("parent config is above the plausible monorepo root");
320
+ }
321
+ }
322
+
323
+ function collectExtendsParents(packageRoot) {
324
+ // Absolute paths of RELATIVE parent configs OUTSIDE the package, following the extends
325
+ // chain (string or TS5+ array). Throws TsconfigMirrorAbort on any policy violation.
326
+ const entry = path.join(packageRoot, "tsconfig.json");
327
+ if (!existsSync(entry)) {
328
+ return [];
329
+ }
330
+ const externalParents = [];
331
+ const visited = new Set();
332
+ const walk = (configAbs, depth) => {
333
+ if (visited.has(configAbs)) {
334
+ return;
335
+ }
336
+ visited.add(configAbs);
337
+ if (depth > TSCONFIG_EXTENDS_MAX_DEPTH) {
338
+ throw new TsconfigMirrorAbort("extends chain is too deep");
339
+ }
340
+ const configDir = path.dirname(configAbs);
341
+ for (const value of readTsconfigExtends(configAbs)) {
342
+ const target = resolveExtendsTarget(configDir, value);
343
+ if (target === null) {
344
+ continue;
345
+ }
346
+ if (!isInsideDir(packageRoot, target)) {
347
+ assertWithinUpBound(packageRoot, target);
348
+ if (!externalParents.includes(target)) {
349
+ externalParents.push(target);
350
+ if (externalParents.length > TSCONFIG_MAX_PARENT_FILES) {
351
+ throw new TsconfigMirrorAbort("too many parent configs");
352
+ }
353
+ }
354
+ }
355
+ walk(target, depth + 1);
356
+ }
357
+ };
358
+ walk(entry, 0);
359
+ return externalParents;
360
+ }
361
+
362
+ function readTsconfigReferences(configAbs) {
363
+ const refs = readTsconfigObject(configAbs)?.references;
364
+ if (!Array.isArray(refs)) {
365
+ return [];
366
+ }
367
+ return refs
368
+ .map(ref => (ref && typeof ref === "object" && typeof ref.path === "string" ? ref.path : null))
369
+ .filter(Boolean);
370
+ }
371
+
372
+ function resolveProjectReferenceTarget(fromDir, value) {
373
+ if (path.isAbsolute(value)) {
374
+ throw new TsconfigMirrorAbort("absolute project reference is not mirrored");
375
+ }
376
+ if (!value.startsWith(".")) {
377
+ return null;
378
+ }
379
+ const base = path.resolve(fromDir, value);
380
+ const candidates = value.endsWith(".json")
381
+ ? [base]
382
+ : [path.join(base, "tsconfig.json"), `${base}.json`];
383
+ const found = candidates.find(candidate => existsSync(candidate) && lstatSync(candidate).isFile());
384
+ if (!found) {
385
+ throw new TsconfigMirrorAbort(`unresolvable project reference: ${value}`);
386
+ }
387
+ return found;
388
+ }
389
+
390
+ function collectProjectReferenceMetadata(packageRoot, workspaceRoot) {
391
+ // Vite/OXC may load tsconfig project references while transforming setup files. Even when a
392
+ // referenced workspace package is type-only or copied as built output, its tsconfig/package
393
+ // metadata must exist at the mirrored relative path. Copy metadata only; never source/runtime bytes.
394
+ const entry = path.join(packageRoot, "tsconfig.json");
395
+ if (!existsSync(entry)) {
396
+ return [];
397
+ }
398
+ const files = [];
399
+ const visited = new Set();
400
+ const addFile = file => {
401
+ if (isInsideDir(packageRoot, file)) {
402
+ return;
403
+ }
404
+ assertWithinUpBound(packageRoot, file);
405
+ if (!isInsideDir(workspaceRoot, file)) {
406
+ throw new TsconfigMirrorAbort("project reference escapes the workspace root");
407
+ }
408
+ if (!files.includes(file)) {
409
+ files.push(file);
410
+ if (files.length > TSCONFIG_MAX_REFERENCE_FILES) {
411
+ throw new TsconfigMirrorAbort("too many project reference metadata files");
412
+ }
413
+ }
414
+ };
415
+ const walk = (configAbs, depth) => {
416
+ if (visited.has(configAbs)) {
417
+ return;
418
+ }
419
+ visited.add(configAbs);
420
+ if (depth > TSCONFIG_REFERENCE_MAX_DEPTH) {
421
+ throw new TsconfigMirrorAbort("project reference chain is too deep");
422
+ }
423
+ for (const value of readTsconfigReferences(configAbs)) {
424
+ const target = resolveProjectReferenceTarget(path.dirname(configAbs), value);
425
+ if (target === null) {
426
+ continue;
427
+ }
428
+ addFile(target);
429
+ const pkg = path.join(path.dirname(target), "package.json");
430
+ if (existsSync(pkg) && lstatSync(pkg).isFile()) {
431
+ addFile(pkg);
432
+ }
433
+ walk(target, depth + 1);
434
+ }
435
+ };
436
+ walk(entry, 0);
437
+ return files;
438
+ }
439
+
440
+ function commonAncestorDir(absPaths) {
441
+ const splitPaths = absPaths.map(p => p.split(path.sep));
442
+ const first = splitPaths[0];
443
+ let end = first.length;
444
+ for (const segments of splitPaths.slice(1)) {
445
+ end = Math.min(end, segments.length);
446
+ for (let i = 0; i < end; i += 1) {
447
+ if (segments[i] !== first[i]) {
448
+ end = i;
449
+ break;
450
+ }
451
+ }
452
+ }
453
+ const ancestor = first.slice(0, end).join(path.sep);
454
+ return ancestor === "" ? path.sep : ancestor;
455
+ }
456
+
457
+ // ── M-2: honor tsconfig `paths` — copy the aliased sibling SOURCE + inject the runner alias ──────
458
+ // A monorepo package commonly imports a sibling package's SOURCE via a tsconfig `paths` alias
459
+ // (e.g. "@b/*": ["../b/src/*"]). esbuild/vitest/jest do NOT honor tsconfig `paths`, so in the
460
+ // isolated single-package copy the aliased import doesn't resolve → baseline red. M-2 (1) COPIES
461
+ // the referenced sibling source (bytes, into the disposable sandbox mono tree — never a writable
462
+ // symlink into the user checkout) and (2) injects the mapping into the runner resolver
463
+ // (Vitest `resolve.alias` / Jest `moduleNameMapper`) via a generated, MERGED config. This ONLY
464
+ // changes what is present + how imports resolve; the proof gate (mutation/baseline/mutant/classify)
465
+ // is untouched. Any policy violation → TsconfigMirrorAbort → mirror nothing → honest unrunnable.
466
+ const TSCONFIG_MAX_PATH_ENTRIES = 64;
467
+ const TSCONFIG_MAX_ALIAS_FILES = 5000;
468
+ const TSCONFIG_MAX_ALIAS_BYTES = 64 * 1024 * 1024;
469
+ const MIRROR_SKIP_DIRS = new Set(["node_modules", ".git", ".orangepro"]);
470
+ const GENERATED_VITEST_CONFIG = ".opro-dynamic-proof-vitest.config.mjs";
471
+ const GENERATED_JEST_CONFIG = ".opro-dynamic-proof-jest.config.cjs";
472
+ const GENERATED_MOCHA_TSCONFIG = ".opro-dynamic-proof-mocha.tsconfig.json";
473
+
474
+ function readTsconfigObject(configAbs) {
475
+ // Defensive JSONC parse of a whole tsconfig; on any failure return null (fail-safe → no paths).
476
+ let raw;
477
+ try {
478
+ raw = readFileSync(configAbs, "utf8");
479
+ } catch {
480
+ return null;
481
+ }
482
+ try {
483
+ return JSON.parse(stripJsonComments(raw));
484
+ } catch {
485
+ return null;
486
+ }
487
+ }
488
+
489
+ function resolvePathAliasTarget(packageRoot, baseUrl, key, rawTarget) {
490
+ // Resolve a tsconfig `paths` target (relative to baseUrl). Returns null for in-package /
491
+ // node_modules-backed targets (out of M-2 scope). Throws TsconfigMirrorAbort for an external
492
+ // target that can't be resolved on disk or sits above the plausible monorepo root.
493
+ const starIndex = rawTarget.indexOf("*");
494
+ const prefix = starIndex === -1 ? rawTarget : rawTarget.slice(0, starIndex);
495
+ const absTarget = path.resolve(baseUrl, prefix);
496
+ if (absTarget.split(path.sep).includes("node_modules")) {
497
+ return null;
498
+ }
499
+ if (absTarget === packageRoot || isInsideDir(packageRoot, absTarget)) {
500
+ return null;
501
+ }
502
+ assertWithinUpBound(packageRoot, absTarget);
503
+ if (!existsSync(absTarget)) {
504
+ throw new TsconfigMirrorAbort(`unresolvable path alias target: ${key}`);
505
+ }
506
+ return { key, star: starIndex !== -1, absTarget, isFile: lstatSync(absTarget).isFile() };
507
+ }
508
+
509
+ function collectPathAliases(packageRoot) {
510
+ // Walk the same extends chain (package first, then parents) and collect the RELATIVE `paths`
511
+ // targets that resolve to sibling SOURCE OUTSIDE the package. The package's own mapping wins over
512
+ // a parent's for the same key. Throws TsconfigMirrorAbort on any policy violation → mirror nothing.
513
+ const entry = path.join(packageRoot, "tsconfig.json");
514
+ if (!existsSync(entry)) {
515
+ return [];
516
+ }
517
+ const collected = [];
518
+ const seenKeys = new Set();
519
+ const visited = new Set();
520
+ const walk = (configAbs, depth) => {
521
+ if (visited.has(configAbs)) {
522
+ return;
523
+ }
524
+ visited.add(configAbs);
525
+ if (depth > TSCONFIG_EXTENDS_MAX_DEPTH) {
526
+ throw new TsconfigMirrorAbort("extends chain is too deep");
527
+ }
528
+ const configDir = path.dirname(configAbs);
529
+ const compilerOptions = readTsconfigObject(configAbs)?.compilerOptions ?? {};
530
+ const baseUrl = typeof compilerOptions.baseUrl === "string"
531
+ ? path.resolve(configDir, compilerOptions.baseUrl)
532
+ : configDir;
533
+ const paths = compilerOptions.paths;
534
+ if (paths && typeof paths === "object") {
535
+ for (const [key, targets] of Object.entries(paths)) {
536
+ if (seenKeys.has(key) || !Array.isArray(targets)) {
537
+ continue;
538
+ }
539
+ for (const rawTarget of targets) {
540
+ if (typeof rawTarget !== "string") {
541
+ continue;
542
+ }
543
+ const info = resolvePathAliasTarget(packageRoot, baseUrl, key, rawTarget);
544
+ if (info === null) {
545
+ continue;
546
+ }
547
+ seenKeys.add(key);
548
+ collected.push(info);
549
+ if (collected.length > TSCONFIG_MAX_PATH_ENTRIES) {
550
+ throw new TsconfigMirrorAbort("too many path aliases");
551
+ }
552
+ break;
553
+ }
554
+ }
555
+ }
556
+ for (const value of readTsconfigExtends(configAbs)) {
557
+ const target = resolveExtendsTarget(configDir, value);
558
+ if (target !== null) {
559
+ walk(target, depth + 1);
560
+ }
561
+ }
562
+ };
563
+ walk(entry, 0);
564
+ return collected;
565
+ }
566
+
567
+ function assertAliasBudget(aliases) {
568
+ // Pre-scan the unique alias target dirs and abort BEFORE any copy if they exceed the file/byte
569
+ // caps, so an over-cap alias mirrors nothing (never a half-copied sandbox).
570
+ const seen = new Set();
571
+ let files = 0;
572
+ let bytes = 0;
573
+ const visit = source => {
574
+ const stat = lstatSync(source);
575
+ if (stat.isSymbolicLink()) {
576
+ return;
577
+ }
578
+ if (stat.isDirectory()) {
579
+ for (const name of readdirSync(source)) {
580
+ if (!MIRROR_SKIP_DIRS.has(name)) {
581
+ visit(path.join(source, name));
582
+ }
583
+ }
584
+ return;
585
+ }
586
+ if (stat.isFile()) {
587
+ files += 1;
588
+ bytes += stat.size;
589
+ if (files > TSCONFIG_MAX_ALIAS_FILES) {
590
+ throw new TsconfigMirrorAbort("aliased source has too many files");
591
+ }
592
+ if (bytes > TSCONFIG_MAX_ALIAS_BYTES) {
593
+ throw new TsconfigMirrorAbort("aliased source is too large");
594
+ }
595
+ }
596
+ };
597
+ for (const alias of aliases) {
598
+ if (!seen.has(alias.absTarget)) {
599
+ seen.add(alias.absTarget);
600
+ visit(alias.absTarget);
601
+ }
602
+ }
603
+ }
604
+
605
+ function copyAliasTarget(absTarget, dest, isFile) {
606
+ mkdirSync(path.dirname(dest), { recursive: true });
607
+ if (isFile) {
608
+ writeFileSync(dest, readFileSync(absTarget));
609
+ return;
610
+ }
611
+ cpSync(absTarget, dest, {
612
+ recursive: true,
613
+ filter(source) {
614
+ if (lstatSync(source).isSymbolicLink()) {
615
+ return false;
616
+ }
617
+ return !MIRROR_SKIP_DIRS.has(path.basename(source));
618
+ }
619
+ });
620
+ }
621
+
622
+ function copyRuntimeAliasTarget(absTarget, dest, isFile) {
623
+ const isRuntimeFile = file => /\.(?:js|jsx|mjs|cjs|json)$/.test(file);
624
+ mkdirSync(path.dirname(dest), { recursive: true });
625
+ if (isFile) {
626
+ if (isRuntimeFile(absTarget)) {
627
+ writeFileSync(dest, readFileSync(absTarget));
628
+ }
629
+ return;
630
+ }
631
+ cpSync(absTarget, dest, {
632
+ recursive: true,
633
+ filter(source) {
634
+ if (lstatSync(source).isSymbolicLink()) {
635
+ return false;
636
+ }
637
+ const name = path.basename(source);
638
+ if (MIRROR_SKIP_DIRS.has(name)) {
639
+ return false;
640
+ }
641
+ if (lstatSync(source).isDirectory()) {
642
+ return true;
643
+ }
644
+ return isRuntimeFile(source);
645
+ }
646
+ });
647
+ }
648
+
649
+ function copyFixtureRoot(root, label, { linkNodeModules, workspaceRoot }) {
650
+ const tmpRoot = mkdtempSync(path.join(tmpdir(), `opro-dynamic-proof-${label}-`));
651
+ const monoRoot = path.join(tmpRoot, "mono");
652
+ // On any policy violation, mirror nothing → package lands alone at tmpRoot/mono with its
653
+ // dangling extends / unresolved alias → baseline fails to transform/resolve → honest
654
+ // tsconfig_missing / unrunnable (M-4). Extends (M-1) and paths (M-2) share one fail-safe.
655
+ let parents = [];
656
+ let aliases = [];
657
+ try {
658
+ parents = collectExtendsParents(root);
659
+ aliases = collectPathAliases(root);
660
+ assertAliasBudget(aliases);
661
+ } catch (error) {
662
+ if (!(error instanceof TsconfigMirrorAbort)) {
663
+ throw error;
664
+ }
665
+ parents = [];
666
+ aliases = [];
667
+ }
668
+ // M-3 (aspect 2): discover the sibling workspace-dep closure + package-local runner config helpers, each
669
+ // under its OWN fail-safe (a workspace policy violation mirrors no siblings/helpers → honest unrunnable,
670
+ // without discarding the M-1/M-2 tsconfig extends/paths mirror). Sibling enumeration is skipped unless the
671
+ // target declares at least one dep, so single-package / dep-less packages pay nothing.
672
+ let siblingPlans = [];
673
+ try {
674
+ if (workspaceRoot && workspaceRoot !== root && workspaceDepNames(readPackageJson(root), true).length > 0) {
675
+ const members = enumerateWorkspaceMembers(workspaceRoot);
676
+ // planSiblingCopy returns null for a type-only sibling (skipped, not aborted) — drop those.
677
+ siblingPlans = collectWorkspaceSiblings(root, members)
678
+ .map(sibling => planSiblingCopy(sibling.name, sibling.root))
679
+ .filter(Boolean);
680
+ assertSiblingBudget(siblingPlans);
681
+ }
682
+ } catch (error) {
683
+ if (!(error instanceof WorkspaceMirrorAbort)) {
684
+ throw error;
685
+ }
686
+ siblingPlans = [];
687
+ }
688
+ // §4a config-helper collection is INDEPENDENT of sibling resolution (Codex #182): an unresolved/over-cap
689
+ // workspace SIBLING must not suppress a package-local runner config's workspace-root helper, and vice
690
+ // versa. Each fails closed to its own empty mirror with its own reason.
691
+ let configHelpers = [];
692
+ try {
693
+ configHelpers = collectConfigHelpers(root, workspaceRoot ?? root);
694
+ } catch (error) {
695
+ if (!(error instanceof WorkspaceMirrorAbort)) {
696
+ throw error;
697
+ }
698
+ configHelpers = [];
699
+ }
700
+ // TypeScript project references are config metadata, not runtime evidence. Keep them under their own
701
+ // fail-safe so a bad reference cannot discard working sibling/config-helper mirrors.
702
+ let projectReferenceFiles = [];
703
+ try {
704
+ projectReferenceFiles = collectProjectReferenceMetadata(root, workspaceRoot ?? root);
705
+ } catch (error) {
706
+ if (!(error instanceof TsconfigMirrorAbort)) {
707
+ throw error;
708
+ }
709
+ projectReferenceFiles = [];
710
+ }
711
+ // M-3: fold the detected workspace root (+ sibling roots + config-helper/reference paths) into the common ancestor
712
+ // so each is materialized at its own tmpRoot/mono/<rel(R, path)> position ABOVE the package copy
713
+ // (workspaceRoot is an ancestor of root, so this only ever pulls R up; for single-package repos
714
+ // workspaceRoot === root and there are no siblings/helpers → no change).
715
+ const ancestor = commonAncestorDir([
716
+ root,
717
+ ...parents,
718
+ ...aliases.map(alias => alias.absTarget),
719
+ ...siblingPlans.map(plan => plan.siblingRoot),
720
+ ...configHelpers,
721
+ ...projectReferenceFiles,
722
+ workspaceRoot ?? root
723
+ ]);
724
+ const repoRoot = path.join(monoRoot, path.relative(ancestor, root));
725
+ mkdirSync(path.dirname(repoRoot), { recursive: true });
726
+ cpSync(root, repoRoot, {
727
+ recursive: true,
728
+ filter(source) {
729
+ const name = path.basename(source);
730
+ if (source !== root && lstatSync(source).isSymbolicLink()) {
731
+ return false;
732
+ }
733
+ return name !== "node_modules" && name !== ".git" && name !== ".orangepro";
734
+ }
735
+ });
736
+ for (const parent of parents) {
737
+ const dest = path.join(monoRoot, path.relative(ancestor, parent));
738
+ mkdirSync(path.dirname(dest), { recursive: true });
739
+ writeFileSync(dest, readFileSync(parent));
740
+ }
741
+ for (const file of projectReferenceFiles) {
742
+ const dest = path.join(monoRoot, path.relative(ancestor, file));
743
+ mkdirSync(path.dirname(dest), { recursive: true });
744
+ writeFileSync(dest, readFileSync(file));
745
+ }
746
+ // M-2: copy each unique aliased sibling target as bytes and map its key → sandbox location.
747
+ const copiedTargets = new Map();
748
+ const aliasEntries = [];
749
+ const siblingNames = new Set(siblingPlans.map(plan => plan.name));
750
+ const aliasPackageName = key => {
751
+ const clean = String(key ?? "").replace(/\/\*$/, "");
752
+ if (clean.startsWith("@")) {
753
+ const parts = clean.split("/");
754
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : clean;
755
+ }
756
+ return clean.split("/", 1)[0] ?? clean;
757
+ };
758
+ for (const alias of aliases) {
759
+ if (siblingNames.has(aliasPackageName(alias.key))) {
760
+ continue;
761
+ }
762
+ let replacement = copiedTargets.get(alias.absTarget);
763
+ if (replacement === undefined) {
764
+ replacement = path.join(monoRoot, path.relative(ancestor, alias.absTarget));
765
+ copyAliasTarget(alias.absTarget, replacement, alias.isFile);
766
+ copiedTargets.set(alias.absTarget, replacement);
767
+ }
768
+ aliasEntries.push({ key: alias.key, star: alias.star, replacement });
769
+ }
770
+ // M-3 (aspect 2): copy each sibling's built output / source as bytes into its mirrored mono position and
771
+ // inject the package-name resolver aliases. Order matters: the bare entry (exact) and each `exports`
772
+ // subpath (exact) precede the catch-all star so `@pkg` and `@pkg/known` hit the copied file while
773
+ // `@pkg/deep` still resolves relative to the copied root. Injected via the same aliasEntries the M-2
774
+ // resolver consumes, so the COPY wins over the read-only workspace-root node_modules link.
775
+ for (const plan of siblingPlans) {
776
+ const destRoot = path.join(monoRoot, path.relative(ancestor, plan.siblingRoot));
777
+ copyAliasTarget(path.join(plan.siblingRoot, "package.json"), path.join(destRoot, "package.json"), true);
778
+ if (plan.isSource) {
779
+ const tsconfig = path.join(plan.siblingRoot, "tsconfig.json");
780
+ if (existsSync(tsconfig)) {
781
+ copyAliasTarget(tsconfig, path.join(destRoot, "tsconfig.json"), true);
782
+ }
783
+ }
784
+ for (const segment of plan.segments) {
785
+ const source = path.join(plan.siblingRoot, segment);
786
+ if (existsSync(source)) {
787
+ const isFile = lstatSync(source).isFile();
788
+ if (plan.runtimeOnly) {
789
+ copyRuntimeAliasTarget(source, path.join(destRoot, segment), isFile);
790
+ } else {
791
+ copyAliasTarget(source, path.join(destRoot, segment), isFile);
792
+ }
793
+ }
794
+ }
795
+ aliasEntries.push({ key: plan.name, star: false, replacement: path.join(destRoot, plan.entryRel) });
796
+ for (const sub of plan.subpathAliases) {
797
+ aliasEntries.push({ key: `${plan.name}/${sub.subpath}`, star: false, replacement: path.join(destRoot, sub.targetRel) });
798
+ }
799
+ aliasEntries.push({ key: `${plan.name}/*`, star: true, replacement: destRoot });
800
+ }
801
+ // M-3 (aspect 2, §4a): copy the bounded RELATIVE config-helper closure as bytes into the same mirrored
802
+ // positions so a package-local runner config that requires a workspace-root helper loads (never a symlink).
803
+ for (const helper of configHelpers) {
804
+ const dest = path.join(monoRoot, path.relative(ancestor, helper));
805
+ mkdirSync(path.dirname(dest), { recursive: true });
806
+ writeFileSync(dest, readFileSync(helper));
807
+ }
808
+ const sourceNodeModules = path.join(root, "node_modules");
809
+ if (linkNodeModules && existsSync(sourceNodeModules)) {
810
+ symlinkSync(sourceNodeModules, path.join(repoRoot, "node_modules"), "dir");
811
+ }
812
+ // M-3: additionally link the WORKSPACE-ROOT node_modules at its own ancestor position so a HOISTED
813
+ // runner + deps resolve by walking up from the package copy. Same read-only dependency-cache trust
814
+ // class as the package-local link above (#167): symlinked, not write-isolated. No sibling SOURCE is
815
+ // copied — only the dependency cache is exposed at an ancestor directory.
816
+ if (linkNodeModules && workspaceRoot && workspaceRoot !== root) {
817
+ const workspaceNodeModules = path.join(workspaceRoot, "node_modules");
818
+ if (existsSync(workspaceNodeModules)) {
819
+ const workspaceRootDest = path.join(monoRoot, path.relative(ancestor, workspaceRoot));
820
+ mkdirSync(workspaceRootDest, { recursive: true });
821
+ const destNodeModules = path.join(workspaceRootDest, "node_modules");
822
+ if (!existsSync(destNodeModules)) {
823
+ symlinkSync(workspaceNodeModules, destNodeModules, "dir");
824
+ }
825
+ }
826
+ }
827
+ return { tmpRoot, repoRoot, monoRoot, aliases: aliasEntries };
828
+ }
829
+
830
+ function findMatchingBrace(source, openBraceIndex) {
831
+ let depth = 0;
832
+ for (let i = openBraceIndex; i < source.length; i += 1) {
833
+ const ch = source[i];
834
+ if (ch === "{") {
835
+ depth += 1;
836
+ } else if (ch === "}") {
837
+ depth -= 1;
838
+ if (depth === 0) {
839
+ return i;
840
+ }
841
+ }
842
+ }
843
+ throw new Error("Could not find closing brace for target method");
844
+ }
845
+
846
+ function mutateMethod(targetAbs, method, replacementBody) {
847
+ const source = readFileSync(targetAbs, "utf8");
848
+ const escaped = method.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
849
+ // Class methods + function declarations: `<name>(params)[: ret] {`.
850
+ const methodRe = new RegExp(`((?:async\\s+)?${escaped}\\s*(?:<[^\\n\\{]+>)?\\s*\\([^)]*\\)\\s*(?::\\s*[^\\{]+)?\\s*)\\{`, "gm");
851
+ // Name-bound block functions the methodRe cannot see (there is `= …` between name and `(`):
852
+ // arrow-const block — `export const foo = (c) => { … }`
853
+ // function expression — `const foo = function (c) { … }` (anonymous)
854
+ // A NAMED function expression whose inner name matches (`const foo = function foo(c) { … }`) is safely
855
+ // REFUSED as ambiguous: methodRe ALSO matches the inner `foo(c) {`, so the union yields 2 candidates →
856
+ // "Ambiguous method" → unrunnable. Conservative fail-safe, never a false proof.
857
+ // GUARDRAIL 2: the alternation requires EITHER `=>` OR `function` — a bare `const foo = 5;` never matches.
858
+ const freeFnRe = new RegExp(
859
+ `(^|\\n)([ \\t]*(?:export\\s+)?(?:const|let|var)\\s+${escaped}\\s*(?::\\s*[^=\\n]+)?\\s*=\\s*(?:(?:async\\s+)?(?:<[^(\\n{]+>\\s*)?\\([^)]*\\)\\s*(?::\\s*[^={]+)?\\s*=>\\s*|(?:async\\s+)?function\\s*(?:${escaped})?\\s*(?:<[^(\\n{]+>\\s*)?\\([^)]*\\)\\s*(?::\\s*[^={]+)?\\s*)\\{)`,
860
+ "gm"
861
+ );
862
+ // UNION both forms, normalized to { index (declaration start), text (ends at the opening `{`) }, then the
863
+ // SAME ambiguity guard across forms. GUARDRAIL 1: the freeFnRe candidate starts at const/let/var, NOT the
864
+ // captured line separator (group 1), so the indent logic below sees the same slice shape as methodRe.
865
+ const candidates = [
866
+ ...[...source.matchAll(methodRe)].map(m => ({ index: m.index, text: m[0] })),
867
+ ...[...source.matchAll(freeFnRe)].map(m => ({ index: m.index + m[1].length, text: m[2] }))
868
+ ];
869
+ if (candidates.length > 1) {
870
+ throw new Error(`Ambiguous method ${method} in ${targetAbs}: ${candidates.length} matches`);
871
+ }
872
+ const candidate = candidates[0];
873
+ if (!candidate || candidate.index === undefined) {
874
+ throw new Error(`Could not find method ${method} in ${targetAbs}`);
875
+ }
876
+ const openBraceIndex = candidate.index + candidate.text.length - 1;
877
+ const closeBraceIndex = findMatchingBrace(source, openBraceIndex);
878
+ const indentMatch = source.slice(0, candidate.index).match(/(^|\n)([ \t]*)[^\n]*$/);
879
+ const indent = indentMatch?.[2] ?? "";
880
+ const bodyIndent = `${indent} `;
881
+ const replacement = `{\n${bodyIndent}${replacementBody.trim()}\n${indent}}`;
882
+ const mutated = `${source.slice(0, openBraceIndex)}${replacement}${source.slice(closeBraceIndex + 1)}`;
883
+ writeFileSync(targetAbs, mutated);
884
+ }
885
+
886
+ function firstExisting(paths) {
887
+ return paths.find(candidate => existsSync(candidate)) ?? paths[0];
888
+ }
889
+
890
+ function localToolRoot() {
891
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
892
+ return path.resolve(scriptDir, "../..");
893
+ }
894
+
895
+ function defaultVitestBin(root, workspaceRoot) {
896
+ return firstExisting([
897
+ path.join(root, "node_modules/vitest/vitest.mjs"),
898
+ ...(workspaceRoot && workspaceRoot !== root
899
+ ? [path.join(workspaceRoot, "node_modules/vitest/vitest.mjs")]
900
+ : []),
901
+ path.join(localToolRoot(), "node_modules/vitest/vitest.mjs")
902
+ ]);
903
+ }
904
+
905
+ function defaultJestBin(root, workspaceRoot) {
906
+ return firstExisting([
907
+ path.join(root, "node_modules/jest/bin/jest.js"),
908
+ ...(workspaceRoot && workspaceRoot !== root
909
+ ? [path.join(workspaceRoot, "node_modules/jest/bin/jest.js")]
910
+ : []),
911
+ path.join(localToolRoot(), "node_modules/jest/bin/jest.js")
912
+ ]);
913
+ }
914
+
915
+ function defaultMochaBin(root, workspaceRoot) {
916
+ return firstExisting([
917
+ path.join(root, "node_modules/mocha/bin/mocha.js"),
918
+ ...(workspaceRoot && workspaceRoot !== root
919
+ ? [path.join(workspaceRoot, "node_modules/mocha/bin/mocha.js")]
920
+ : []),
921
+ path.join(localToolRoot(), "node_modules/mocha/bin/mocha.js")
922
+ ]);
923
+ }
924
+
925
+ function hasMochaConfig(root) {
926
+ const pkg = readPackageJson(root);
927
+ return Boolean(pkg?.mocha) || hasAnyFile(root, [
928
+ ".mocharc.js",
929
+ ".mocharc.cjs",
930
+ ".mocharc.mjs",
931
+ ".mocharc.json"
932
+ ]);
933
+ }
934
+
935
+ function dynamicProofReporterPath() {
936
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
937
+ return path.join(scriptDir, "dynamic-proof-vitest-reporter.mjs");
938
+ }
939
+
940
+ function dynamicProofJestReporterPath() {
941
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
942
+ return path.join(scriptDir, "dynamic-proof-jest-reporter.cjs");
943
+ }
944
+
945
+ function dynamicProofMochaReporterPath() {
946
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
947
+ return path.join(scriptDir, "dynamic-proof-mocha-reporter.cjs");
948
+ }
949
+
950
+ function readPackageJson(root) {
951
+ try {
952
+ return JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
953
+ } catch {
954
+ return null;
955
+ }
956
+ }
957
+
958
+ function hasAnyFile(root, names) {
959
+ return names.some(name => existsSync(path.join(root, name)));
960
+ }
961
+
962
+ function packageText(pkg) {
963
+ if (!pkg) {
964
+ return "";
965
+ }
966
+ return JSON.stringify({
967
+ scripts: pkg.scripts ?? {},
968
+ dependencies: pkg.dependencies ?? {},
969
+ devDependencies: pkg.devDependencies ?? {},
970
+ jest: pkg.jest,
971
+ vitest: pkg.vitest
972
+ });
973
+ }
974
+
975
+ function hasPackageDependency(pkg, names) {
976
+ const deps = {
977
+ ...(pkg?.dependencies ?? {}),
978
+ ...(pkg?.devDependencies ?? {}),
979
+ ...(pkg?.peerDependencies ?? {}),
980
+ ...(pkg?.optionalDependencies ?? {})
981
+ };
982
+ return names.some(name => Object.prototype.hasOwnProperty.call(deps, name));
983
+ }
984
+
985
+ // ── M-3 (aspect 1): detect the TS/JS workspace root so a hoisted runner + dependency cache resolve ──
986
+ // A focused package inside a monorepo (e.g. a Medusa package) carries no package-local node_modules —
987
+ // the runner (jest/vitest) and its deps are HOISTED to the workspace root. Walk UP from the package
988
+ // root to the nearest ancestor that declares a workspace (npm/yarn/bun `workspaces`, or a
989
+ // pnpm-workspace.yaml); fall back to the package root when none is found within bounds (single-package
990
+ // repos like Hono → no-op). Detection only relocates WHERE node_modules is linked/resolved; a wrong
991
+ // guess makes the dependency fail to resolve → honest unrunnable, never a false proof, so workspace
992
+ // glob-membership need not be checked here.
993
+ const WORKSPACE_DETECT_MAX_UP_LEVELS = 12;
994
+
995
+ function packageDeclaresWorkspaces(dir) {
996
+ const pkg = readPackageJson(dir);
997
+ const ws = pkg?.workspaces;
998
+ if (Array.isArray(ws)) {
999
+ return ws.length > 0;
1000
+ }
1001
+ // yarn classic shape: { "workspaces": { "packages": [...] } }
1002
+ if (ws && typeof ws === "object" && Array.isArray(ws.packages)) {
1003
+ return ws.packages.length > 0;
1004
+ }
1005
+ return false;
1006
+ }
1007
+
1008
+ function isWorkspaceRootDir(dir) {
1009
+ return (
1010
+ packageDeclaresWorkspaces(dir) ||
1011
+ existsSync(path.join(dir, "pnpm-workspace.yaml")) ||
1012
+ existsSync(path.join(dir, "pnpm-workspace.yml")) ||
1013
+ existsSync(path.join(dir, "lerna.json"))
1014
+ );
1015
+ }
1016
+
1017
+ function detectWorkspaceRoot(packageRoot) {
1018
+ let dir = path.dirname(packageRoot);
1019
+ for (let level = 0; level < WORKSPACE_DETECT_MAX_UP_LEVELS; level += 1) {
1020
+ if (isWorkspaceRootDir(dir)) {
1021
+ return dir;
1022
+ }
1023
+ const parent = path.dirname(dir);
1024
+ if (parent === dir) {
1025
+ break;
1026
+ }
1027
+ dir = parent;
1028
+ }
1029
+ return packageRoot;
1030
+ }
1031
+
1032
+ // ── M-3 (aspect 2): resolve TS/JS WORKSPACE PACKAGE deps — copy sibling output/source + config helpers ──
1033
+ // A focused monorepo package imports SIBLING workspace packages by package NAME (e.g. Medusa's
1034
+ // `@medusajs/*`). In the isolated single-package sandbox those bare imports don't resolve (no local
1035
+ // node_modules, and the name is not a published package), so the baseline fails to resolve → red. Aspect-2
1036
+ // (1) discovers the target's DECLARED workspace-dep closure — direct deps + a bounded transitive closure of
1037
+ // workspace-dep-of-workspace-dep, NEVER a published npm package; (2) for each sibling copies its BUILT
1038
+ // output (dist subtree + package.json) when the runtime entry is built, else its SOURCE (src + tsconfig),
1039
+ // as bytes into the disposable sandbox mono tree; and (3) injects a package-name resolver alias (bare entry
1040
+ // + exact `exports` subpaths + catch-all) — reusing M-2's Vitest resolve.alias / Jest moduleNameMapper
1041
+ // injection — so the COPY resolves and WINS over the read-only workspace-root node_modules link (runner
1042
+ // aliases resolve before node_modules). It also copies a bounded closure of RELATIVE helpers required by
1043
+ // package-local runner CONFIG files (the Medusa `jest.config.js → ../../../define_jest_config` shape).
1044
+ // This ONLY changes what is present + how imports resolve; the proof gate (mutation/baseline/mutant/
1045
+ // classify) is untouched. Any cap/ambiguity/cycle/unresolved entry → WorkspaceMirrorAbort → mirror nothing
1046
+ // → the bare import stays unresolved → honest unrunnable, never a false proof. The credited mutation still
1047
+ // touches ONLY the target file, in the mutant copy.
1048
+ const SIBLING_MAX_PACKAGES = 32;
1049
+ const SIBLING_MAX_DEPTH = 3;
1050
+ const SIBLING_MAX_FILES = 20_000;
1051
+ const SIBLING_MAX_BYTES = 128 * 1024 * 1024;
1052
+ const WORKSPACE_GLOB_MAX_DIRS = 4096;
1053
+ const CONFIG_HELPER_MAX_DEPTH = 3;
1054
+ const CONFIG_HELPER_MAX_FILES = 32;
1055
+ const CONFIG_HELPER_MAX_BYTES = 2 * 1024 * 1024;
1056
+ const RUNTIME_EXPORT_CONDITIONS = ["node", "import", "require", "default", "module"];
1057
+ const RUNNER_CONFIG_FILES = [
1058
+ "package.json",
1059
+ "tsconfig.json",
1060
+ "tsconfig.spec.json",
1061
+ ".mocharc.js", ".mocharc.cjs", ".mocharc.mjs", ".mocharc.json",
1062
+ "jest.config.js", "jest.config.cjs", "jest.config.mjs", "jest.config.ts", "jest.config.json",
1063
+ "vitest.config.js", "vitest.config.cjs", "vitest.config.mjs", "vitest.config.ts", "vitest.config.mts",
1064
+ "vite.config.js", "vite.config.cjs", "vite.config.mjs", "vite.config.ts", "vite.config.mts"
1065
+ ];
1066
+ const HELPER_RESOLVE_EXTS = ["", ".js", ".cjs", ".mjs", ".ts", ".cts", ".mts", ".json"];
1067
+
1068
+ class WorkspaceMirrorAbort extends Error {}
1069
+
1070
+ function safeReaddir(dir) {
1071
+ try {
1072
+ return readdirSync(dir);
1073
+ } catch {
1074
+ return [];
1075
+ }
1076
+ }
1077
+
1078
+ function isRealDir(candidate) {
1079
+ try {
1080
+ return lstatSync(candidate).isDirectory();
1081
+ } catch {
1082
+ return false;
1083
+ }
1084
+ }
1085
+
1086
+ function isRealFile(candidate) {
1087
+ try {
1088
+ return lstatSync(candidate).isFile();
1089
+ } catch {
1090
+ return false;
1091
+ }
1092
+ }
1093
+
1094
+ function resolveFileWithin(root, rel) {
1095
+ // Resolve rel against root; return null (fail-safe) if it escapes root.
1096
+ const abs = path.resolve(root, rel);
1097
+ const relative = path.relative(root, abs);
1098
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
1099
+ return null;
1100
+ }
1101
+ return abs;
1102
+ }
1103
+
1104
+ function workspaceDepNames(pkg, includeDev) {
1105
+ // Direct deps consider devDependencies too (a package can import a sibling only in its tests); the
1106
+ // transitive walk uses runtime deps only (dependencies + peerDependencies).
1107
+ const deps = {
1108
+ ...(pkg?.dependencies ?? {}),
1109
+ ...(pkg?.peerDependencies ?? {}),
1110
+ ...(includeDev ? (pkg?.devDependencies ?? {}) : {})
1111
+ };
1112
+ return Object.keys(deps);
1113
+ }
1114
+
1115
+ function readWorkspacePatterns(workspaceRoot) {
1116
+ const pkg = readPackageJson(workspaceRoot);
1117
+ const ws = pkg?.workspaces;
1118
+ if (Array.isArray(ws)) {
1119
+ return ws.filter(entry => typeof entry === "string");
1120
+ }
1121
+ if (ws && typeof ws === "object" && Array.isArray(ws.packages)) {
1122
+ return ws.packages.filter(entry => typeof entry === "string");
1123
+ }
1124
+ for (const name of ["pnpm-workspace.yaml", "pnpm-workspace.yml"]) {
1125
+ const file = path.join(workspaceRoot, name);
1126
+ if (existsSync(file)) {
1127
+ return parsePnpmWorkspacePackages(file);
1128
+ }
1129
+ }
1130
+ const lernaFile = path.join(workspaceRoot, "lerna.json");
1131
+ if (existsSync(lernaFile)) {
1132
+ try {
1133
+ const lerna = JSON.parse(readFileSync(lernaFile, "utf8"));
1134
+ if (Array.isArray(lerna.packages)) {
1135
+ return lerna.packages.filter(entry => typeof entry === "string");
1136
+ }
1137
+ } catch {
1138
+ return [];
1139
+ }
1140
+ }
1141
+ return [];
1142
+ }
1143
+
1144
+ function parsePnpmWorkspacePackages(file) {
1145
+ // Minimal YAML: collect the list items under the top-level `packages:` key. Fail-safe → [].
1146
+ let raw;
1147
+ try {
1148
+ raw = readFileSync(file, "utf8");
1149
+ } catch {
1150
+ return [];
1151
+ }
1152
+ const patterns = [];
1153
+ let inPackages = false;
1154
+ for (const line of raw.split(/\r?\n/)) {
1155
+ if (/^packages\s*:\s*$/.test(line)) {
1156
+ inPackages = true;
1157
+ continue;
1158
+ }
1159
+ if (!inPackages) {
1160
+ continue;
1161
+ }
1162
+ const item = /^\s*-\s*['"]?([^'"#\r\n]+?)['"]?\s*(?:#.*)?$/.exec(line);
1163
+ if (item) {
1164
+ patterns.push(item[1].trim());
1165
+ continue;
1166
+ }
1167
+ if (/^\S/.test(line)) {
1168
+ break; // a new top-level key ends the packages list
1169
+ }
1170
+ }
1171
+ return patterns;
1172
+ }
1173
+
1174
+ function collectDirsRecursive(base, out, tick) {
1175
+ out.push(base);
1176
+ for (const name of safeReaddir(base)) {
1177
+ if (MIRROR_SKIP_DIRS.has(name)) {
1178
+ continue;
1179
+ }
1180
+ const child = path.join(base, name);
1181
+ if (isRealDir(child)) {
1182
+ tick();
1183
+ collectDirsRecursive(child, out, tick);
1184
+ }
1185
+ }
1186
+ }
1187
+
1188
+ function expandWorkspaceGlob(workspaceRoot, pattern) {
1189
+ // Support literal segments, `*` (one dir), and `**` (any depth). Returns member dirs that contain a
1190
+ // package.json. Symlinked dirs are excluded (isRealDir). Throws WorkspaceMirrorAbort past the dir cap.
1191
+ const segments = pattern.split("/").filter(Boolean);
1192
+ let frontier = [workspaceRoot];
1193
+ let scanned = 0;
1194
+ const tick = () => {
1195
+ scanned += 1;
1196
+ if (scanned > WORKSPACE_GLOB_MAX_DIRS) {
1197
+ throw new WorkspaceMirrorAbort("workspace glob expands too many directories");
1198
+ }
1199
+ };
1200
+ for (const segment of segments) {
1201
+ const next = [];
1202
+ for (const base of frontier) {
1203
+ if (segment === "**") {
1204
+ collectDirsRecursive(base, next, tick);
1205
+ } else if (segment === "*") {
1206
+ for (const name of safeReaddir(base)) {
1207
+ if (MIRROR_SKIP_DIRS.has(name)) {
1208
+ continue;
1209
+ }
1210
+ const child = path.join(base, name);
1211
+ if (isRealDir(child)) {
1212
+ tick();
1213
+ next.push(child);
1214
+ }
1215
+ }
1216
+ } else {
1217
+ const child = path.join(base, segment);
1218
+ if (isRealDir(child)) {
1219
+ next.push(child);
1220
+ }
1221
+ }
1222
+ }
1223
+ frontier = next;
1224
+ }
1225
+ return frontier.filter(dir => existsSync(path.join(dir, "package.json")));
1226
+ }
1227
+
1228
+ function enumerateWorkspaceMembers(workspaceRoot) {
1229
+ // Map packageName → packageRoot for members matched by the workspace globs. Negation globs (!glob) are
1230
+ // ignored. Throws WorkspaceMirrorAbort on a duplicate member name (ambiguous → fail closed).
1231
+ const members = new Map();
1232
+ const dirs = new Set();
1233
+ for (const pattern of readWorkspacePatterns(workspaceRoot)) {
1234
+ if (pattern.startsWith("!")) {
1235
+ continue;
1236
+ }
1237
+ for (const dir of expandWorkspaceGlob(workspaceRoot, pattern)) {
1238
+ dirs.add(dir);
1239
+ }
1240
+ }
1241
+ for (const dir of dirs) {
1242
+ const name = readPackageJson(dir)?.name;
1243
+ if (typeof name !== "string" || name === "") {
1244
+ continue;
1245
+ }
1246
+ const existing = members.get(name);
1247
+ if (existing !== undefined && existing !== dir) {
1248
+ throw new WorkspaceMirrorAbort(`ambiguous workspace member name: ${name}`);
1249
+ }
1250
+ members.set(name, dir);
1251
+ }
1252
+ return members;
1253
+ }
1254
+
1255
+ function collectWorkspaceSiblings(packageRoot, members) {
1256
+ // BFS the DECLARED workspace-dep closure. A dep whose name is not a workspace member is a published
1257
+ // package → never copied. Throws WorkspaceMirrorAbort past the package cap. Returns [{ name, root }].
1258
+ const targetPkg = readPackageJson(packageRoot);
1259
+ if (!targetPkg) {
1260
+ return [];
1261
+ }
1262
+ const chosen = new Map();
1263
+ const queue = workspaceDepNames(targetPkg, true)
1264
+ .filter(name => members.has(name))
1265
+ .map(name => ({ name, depth: 1 }));
1266
+ while (queue.length > 0) {
1267
+ const { name, depth } = queue.shift();
1268
+ if (chosen.has(name)) {
1269
+ continue;
1270
+ }
1271
+ const root = members.get(name);
1272
+ if (root === undefined) {
1273
+ continue;
1274
+ }
1275
+ chosen.set(name, root);
1276
+ if (chosen.size > SIBLING_MAX_PACKAGES) {
1277
+ throw new WorkspaceMirrorAbort("too many workspace siblings");
1278
+ }
1279
+ if (depth >= SIBLING_MAX_DEPTH) {
1280
+ continue; // don't traverse deeper; a needed dep past the cap stays unresolved → honest unrunnable
1281
+ }
1282
+ for (const childName of workspaceDepNames(readPackageJson(root), false)) {
1283
+ if (members.has(childName) && !chosen.has(childName)) {
1284
+ queue.push({ name: childName, depth: depth + 1 });
1285
+ }
1286
+ }
1287
+ }
1288
+ return [...chosen.entries()].map(([name, root]) => ({ name, root }));
1289
+ }
1290
+
1291
+ function resolveExportsCondition(value) {
1292
+ // Resolve an `exports` value (string or conditions object) to a RUNTIME target string. `types` is never
1293
+ // a runtime condition and is skipped. Returns null when unresolvable / blocked (a null export).
1294
+ if (typeof value === "string") {
1295
+ return value;
1296
+ }
1297
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1298
+ for (const condition of RUNTIME_EXPORT_CONDITIONS) {
1299
+ if (Object.prototype.hasOwnProperty.call(value, condition)) {
1300
+ const resolved = resolveExportsCondition(value[condition]);
1301
+ if (resolved) {
1302
+ return resolved;
1303
+ }
1304
+ }
1305
+ }
1306
+ }
1307
+ return null;
1308
+ }
1309
+
1310
+ function runtimeEntryRel(pkg) {
1311
+ // The sibling's RUNTIME entry: exports "." (or a bare conditions object), else main, else module, else
1312
+ // node's index.js default. `types` is type-only and is never the runtime entry.
1313
+ const exp = pkg?.exports;
1314
+ if (exp !== undefined) {
1315
+ if (typeof exp === "string") {
1316
+ return exp;
1317
+ }
1318
+ if (exp && typeof exp === "object" && !Array.isArray(exp)) {
1319
+ if (Object.prototype.hasOwnProperty.call(exp, ".")) {
1320
+ return resolveExportsCondition(exp["."]);
1321
+ }
1322
+ const keys = Object.keys(exp);
1323
+ if (keys.length > 0 && !keys.some(key => key.startsWith("."))) {
1324
+ return resolveExportsCondition(exp);
1325
+ }
1326
+ }
1327
+ }
1328
+ if (typeof pkg?.main === "string") {
1329
+ return pkg.main;
1330
+ }
1331
+ if (typeof pkg?.module === "string") {
1332
+ return pkg.module;
1333
+ }
1334
+ return "index.js";
1335
+ }
1336
+
1337
+ function topLevelSegment(rel) {
1338
+ return rel.split("/").filter(Boolean)[0] ?? "";
1339
+ }
1340
+
1341
+ function sourcePackageSegments(siblingRoot) {
1342
+ const skip = new Set([
1343
+ "node_modules",
1344
+ ".git",
1345
+ ".orangepro",
1346
+ "test",
1347
+ "tests",
1348
+ "__tests__",
1349
+ "coverage",
1350
+ "dist"
1351
+ ]);
1352
+ return safeReaddir(siblingRoot).filter(name => {
1353
+ if (skip.has(name)) {
1354
+ return false;
1355
+ }
1356
+ const abs = path.join(siblingRoot, name);
1357
+ if (isRealDir(abs)) {
1358
+ return true;
1359
+ }
1360
+ return /\.(?:ts|tsx|cts|mts|js|jsx|cjs|mjs|json)$/.test(name);
1361
+ });
1362
+ }
1363
+
1364
+ function isTypeOnlyPackage(pkg) {
1365
+ // A TYPE-ONLY workspace package declares type information (`types`/`typings`) and NO usable runtime
1366
+ // entry. BLANK runtime fields do NOT count as a runtime entry — Medplum's @medplum/fhirtypes ships
1367
+ // `{ "main": "", "types": "dist/index.d.ts" }`, and `main: ""` / `module: ""` / `exports: null` all mean
1368
+ // "no runtime entry". Conservative: a NON-empty runtime field ⇒ not type-only, so a genuinely broken
1369
+ // runtime package (e.g. `main: "dist/missing.js"`) still fails closed.
1370
+ const isNonEmpty = value => typeof value === "string" && value.trim() !== "";
1371
+ const hasTypes = isNonEmpty(pkg?.types) || isNonEmpty(pkg?.typings);
1372
+ const hasRuntimeField =
1373
+ isNonEmpty(pkg?.main) || isNonEmpty(pkg?.module) || (pkg?.exports !== undefined && pkg?.exports !== null);
1374
+ return hasTypes && !hasRuntimeField;
1375
+ }
1376
+
1377
+ function packageDeclaresRuntimeEntry(pkg) {
1378
+ const isNonEmpty = value => typeof value === "string" && value.trim() !== "";
1379
+ return isNonEmpty(pkg?.main) || isNonEmpty(pkg?.module) || (pkg?.exports !== undefined && pkg?.exports !== null);
1380
+ }
1381
+
1382
+ function planSiblingCopy(name, siblingRoot) {
1383
+ // Resolve the runtime entry (built vs source), the byte-copy targets, and the resolver aliases for one
1384
+ // sibling. Returns null to SKIP a type-only package (no runtime mirror); throws WorkspaceMirrorAbort
1385
+ // when a package that declares a runtime entry can't be resolved on disk (fail closed).
1386
+ const pkg = readPackageJson(siblingRoot);
1387
+ const declaredRuntime = packageDeclaresRuntimeEntry(pkg);
1388
+ let entryRel = runtimeEntryRel(pkg);
1389
+ let normalizedEntry = typeof entryRel === "string" ? entryRel.replace(/^\.\//, "") : "";
1390
+ let entryAbs = normalizedEntry ? resolveFileWithin(siblingRoot, normalizedEntry) : null;
1391
+ if (!declaredRuntime && (normalizedEntry === "" || entryAbs === null || !isRealFile(entryAbs))) {
1392
+ for (const candidate of ["index.ts", "index.tsx", "index.mts", "index.cts", "src/index.ts", "src/index.tsx", "src/index.mts", "src/index.cts"]) {
1393
+ const candidateAbs = resolveFileWithin(siblingRoot, candidate);
1394
+ if (candidateAbs !== null && isRealFile(candidateAbs)) {
1395
+ entryRel = candidate;
1396
+ normalizedEntry = candidate;
1397
+ entryAbs = candidateAbs;
1398
+ break;
1399
+ }
1400
+ }
1401
+ }
1402
+ if (normalizedEntry === "" || entryAbs === null || !isRealFile(entryAbs)) {
1403
+ // No resolvable RUNTIME entry. A TYPE-ONLY package (types but no main/module/exports) is not a
1404
+ // runtime dependency — a `import type` of it is erased at transform, and a value import still
1405
+ // resolves via the read-only node_modules link. Skip it WITHOUT aborting the whole sibling mirror
1406
+ // (so real runtime siblings like @medplum/core still copy). It is NEVER runtime evidence.
1407
+ if (isTypeOnlyPackage(pkg)) {
1408
+ return null;
1409
+ }
1410
+ // A package that DECLARES a runtime entry we cannot resolve → genuine blocker → fail closed.
1411
+ throw new WorkspaceMirrorAbort(`workspace_package_unresolved: ${name}`);
1412
+ }
1413
+ const isSource = /\.(?:ts|tsx|cts|mts)$/.test(entryAbs);
1414
+ const rootEntry = !normalizedEntry.includes("/");
1415
+ const segments = new Set(
1416
+ (isSource && !declaredRuntime) || rootEntry
1417
+ ? sourcePackageSegments(siblingRoot)
1418
+ : [topLevelSegment(normalizedEntry)]
1419
+ );
1420
+ const subpathAliases = [];
1421
+ const exp = pkg?.exports;
1422
+ if (exp && typeof exp === "object" && !Array.isArray(exp)) {
1423
+ for (const [key, value] of Object.entries(exp)) {
1424
+ if (key === "." || !key.startsWith("./") || key.includes("*")) {
1425
+ continue; // wildcard subpath patterns fall through to the catch-all alias
1426
+ }
1427
+ const target = resolveExportsCondition(value);
1428
+ if (typeof target !== "string" || !target.startsWith(".")) {
1429
+ continue;
1430
+ }
1431
+ const normalizedTarget = target.replace(/^\.\//, "");
1432
+ const targetAbs = resolveFileWithin(siblingRoot, normalizedTarget);
1433
+ if (targetAbs === null || !isRealFile(targetAbs)) {
1434
+ continue; // unresolved subpath → let the import fail → honest unrunnable
1435
+ }
1436
+ segments.add(topLevelSegment(normalizedTarget));
1437
+ subpathAliases.push({ subpath: key.slice(2), targetRel: normalizedTarget });
1438
+ }
1439
+ }
1440
+ return {
1441
+ name,
1442
+ siblingRoot,
1443
+ entryRel: normalizedEntry,
1444
+ isSource,
1445
+ runtimeOnly: rootEntry && !isSource && declaredRuntime,
1446
+ segments: [...segments].filter(Boolean),
1447
+ subpathAliases
1448
+ };
1449
+ }
1450
+
1451
+ function assertSiblingBudget(plans) {
1452
+ // Pre-scan the sibling copy targets and abort BEFORE any copy if they exceed the file/byte caps, so an
1453
+ // over-cap closure mirrors nothing (never a half-copied sandbox).
1454
+ let files = 0;
1455
+ let bytes = 0;
1456
+ const seen = new Set();
1457
+ const visit = source => {
1458
+ let stat;
1459
+ try {
1460
+ stat = lstatSync(source);
1461
+ } catch {
1462
+ return;
1463
+ }
1464
+ if (stat.isSymbolicLink()) {
1465
+ return;
1466
+ }
1467
+ if (stat.isDirectory()) {
1468
+ for (const name of readdirSync(source)) {
1469
+ if (!MIRROR_SKIP_DIRS.has(name)) {
1470
+ visit(path.join(source, name));
1471
+ }
1472
+ }
1473
+ return;
1474
+ }
1475
+ if (stat.isFile()) {
1476
+ files += 1;
1477
+ bytes += stat.size;
1478
+ if (files > SIBLING_MAX_FILES) {
1479
+ throw new WorkspaceMirrorAbort("workspace siblings have too many files");
1480
+ }
1481
+ if (bytes > SIBLING_MAX_BYTES) {
1482
+ throw new WorkspaceMirrorAbort("workspace siblings are too large");
1483
+ }
1484
+ }
1485
+ };
1486
+ for (const plan of plans) {
1487
+ const targets = ["package.json", ...(plan.isSource ? ["tsconfig.json"] : []), ...plan.segments];
1488
+ for (const rel of targets) {
1489
+ const source = path.join(plan.siblingRoot, rel);
1490
+ if (seen.has(source) || !existsSync(source)) {
1491
+ continue;
1492
+ }
1493
+ seen.add(source);
1494
+ visit(source);
1495
+ }
1496
+ }
1497
+ }
1498
+
1499
+ function relativeImportSpecifiers(file) {
1500
+ // Heuristic scan of a runner CONFIG file for RELATIVE require()/import specifiers. Not a full parser; a
1501
+ // specifier that doesn't resolve to a real file is skipped (→ the real config load fails → honest
1502
+ // unrunnable). ponytail: regex heuristic, upgrade to an AST walk if a real config confuses it.
1503
+ let raw;
1504
+ try {
1505
+ raw = readFileSync(file, "utf8");
1506
+ } catch {
1507
+ return [];
1508
+ }
1509
+ const specifiers = new Set();
1510
+ const patterns = [
1511
+ /require\(\s*["']([^"']+)["']\s*\)/g,
1512
+ /import\s+(?:[^"';]*?\sfrom\s+)?["']([^"']+)["']/g,
1513
+ /import\(\s*["']([^"']+)["']\s*\)/g,
1514
+ /export\s+(?:\*|\{[^}]*\})\s+from\s+["']([^"']+)["']/g
1515
+ ];
1516
+ for (const pattern of patterns) {
1517
+ let match;
1518
+ while ((match = pattern.exec(raw)) !== null) {
1519
+ if (match[1].startsWith(".")) {
1520
+ specifiers.add(match[1]);
1521
+ }
1522
+ }
1523
+ }
1524
+ return [...specifiers];
1525
+ }
1526
+
1527
+ function packageJsonMochaRequireSpecifiers(file) {
1528
+ if (path.basename(file) !== "package.json") {
1529
+ return [];
1530
+ }
1531
+ let pkg;
1532
+ try {
1533
+ pkg = JSON.parse(readFileSync(file, "utf8"));
1534
+ } catch {
1535
+ return [];
1536
+ }
1537
+ const raw = pkg?.mocha?.require;
1538
+ const entries = Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : [];
1539
+ return entries
1540
+ .filter(entry => typeof entry === "string")
1541
+ .filter(entry => entry.startsWith(".") || (!entry.startsWith("@") && !entry.startsWith("node_modules/") && isRealFile(path.resolve(path.dirname(file), entry))))
1542
+ .map(entry => entry.startsWith(".") ? entry : `./${entry}`);
1543
+ }
1544
+
1545
+ function resolveHelperSpecifier(fromDir, specifier) {
1546
+ // Resolve a RELATIVE config-helper specifier to concrete file(s). Files-only, plus an explicit directory
1547
+ // module that carries a package.json `main`. Returns { entry, files } or null (unresolvable → skip).
1548
+ const base = path.resolve(fromDir, specifier);
1549
+ for (const ext of HELPER_RESOLVE_EXTS) {
1550
+ const candidate = ext === "" ? base : `${base}${ext}`;
1551
+ if (isRealFile(candidate)) {
1552
+ return { entry: candidate, files: [candidate] };
1553
+ }
1554
+ }
1555
+ if (isRealDir(base)) {
1556
+ const pkgPath = path.join(base, "package.json");
1557
+ const main = readPackageJson(base)?.main;
1558
+ if (isRealFile(pkgPath) && typeof main === "string") {
1559
+ const mainAbs = resolveFileWithin(base, main.replace(/^\.\//, ""));
1560
+ if (mainAbs !== null && isRealFile(mainAbs)) {
1561
+ return { entry: mainAbs, files: [pkgPath, mainAbs] };
1562
+ }
1563
+ }
1564
+ }
1565
+ return null;
1566
+ }
1567
+
1568
+ function collectConfigHelpers(packageRoot, workspaceRoot) {
1569
+ // Follow RELATIVE requires/imports from package-local runner CONFIG files to a bounded closure of helper
1570
+ // files that resolve OUTSIDE the package but INSIDE the workspace root (the Medusa
1571
+ // `jest.config.js → ../../../define_jest_config` shape). Returns absolute file paths to byte-copy.
1572
+ // Throws WorkspaceMirrorAbort (→ mirror nothing → honest unrunnable) on a cap breach or a relative
1573
+ // import that escapes the workspace root.
1574
+ if (workspaceRoot === packageRoot) {
1575
+ return [];
1576
+ }
1577
+ const toCopy = new Set();
1578
+ const visited = new Set();
1579
+ const queue = [];
1580
+ for (const name of RUNNER_CONFIG_FILES) {
1581
+ const configAbs = path.join(packageRoot, name);
1582
+ if (isRealFile(configAbs)) {
1583
+ queue.push({ file: configAbs, depth: 0 });
1584
+ }
1585
+ const workspaceConfigAbs = path.join(workspaceRoot, name);
1586
+ if (workspaceConfigAbs !== configAbs && isRealFile(workspaceConfigAbs)) {
1587
+ toCopy.add(workspaceConfigAbs);
1588
+ queue.push({ file: workspaceConfigAbs, depth: 0 });
1589
+ }
1590
+ }
1591
+ let files = 0;
1592
+ let bytes = 0;
1593
+ while (queue.length > 0) {
1594
+ const { file, depth } = queue.shift();
1595
+ if (visited.has(file)) {
1596
+ continue;
1597
+ }
1598
+ visited.add(file);
1599
+ for (const specifier of [...relativeImportSpecifiers(file), ...packageJsonMochaRequireSpecifiers(file)]) {
1600
+ const resolved = resolveHelperSpecifier(path.dirname(file), specifier);
1601
+ if (resolved === null) {
1602
+ continue;
1603
+ }
1604
+ if (resolved.entry === packageRoot || isInsideDir(packageRoot, resolved.entry)) {
1605
+ continue; // inside the package: already copied with the package
1606
+ }
1607
+ for (const helperFile of resolved.files) {
1608
+ if (!isInsideDir(workspaceRoot, helperFile)) {
1609
+ throw new WorkspaceMirrorAbort("workspace_config_helper_missing: relative import escapes the workspace root");
1610
+ }
1611
+ if (toCopy.has(helperFile)) {
1612
+ continue;
1613
+ }
1614
+ toCopy.add(helperFile);
1615
+ files += 1;
1616
+ let size = 0;
1617
+ try {
1618
+ size = lstatSync(helperFile).size;
1619
+ } catch {
1620
+ size = 0;
1621
+ }
1622
+ bytes += size;
1623
+ if (files > CONFIG_HELPER_MAX_FILES) {
1624
+ throw new WorkspaceMirrorAbort("workspace_config_helper_missing: too many helper files");
1625
+ }
1626
+ if (bytes > CONFIG_HELPER_MAX_BYTES) {
1627
+ throw new WorkspaceMirrorAbort("workspace_config_helper_missing: helper closure too large");
1628
+ }
1629
+ }
1630
+ if (depth + 1 <= CONFIG_HELPER_MAX_DEPTH) {
1631
+ queue.push({ file: resolved.entry, depth: depth + 1 });
1632
+ }
1633
+ }
1634
+ }
1635
+ return [...toCopy];
1636
+ }
1637
+
1638
+ function detectRunner(root, args) {
1639
+ if (args.runner !== "auto") {
1640
+ return args.runner;
1641
+ }
1642
+ const pkg = readPackageJson(root);
1643
+ const workspacePkg = args.workspaceRoot && args.workspaceRoot !== root ? readPackageJson(args.workspaceRoot) : null;
1644
+ const text = packageText(pkg);
1645
+ const workspaceText = packageText(workspacePkg);
1646
+ const hasJestPackage = hasPackageDependency(pkg, [
1647
+ "jest",
1648
+ "jest-cli",
1649
+ "@jest/core",
1650
+ "babel-jest",
1651
+ "ts-jest",
1652
+ "@swc/jest",
1653
+ "jest-environment-node",
1654
+ "jest-environment-jsdom"
1655
+ ]) || hasPackageDependency(workspacePkg, [
1656
+ "jest",
1657
+ "jest-cli",
1658
+ "@jest/core",
1659
+ "babel-jest",
1660
+ "ts-jest",
1661
+ "@swc/jest",
1662
+ "jest-environment-node",
1663
+ "jest-environment-jsdom"
1664
+ ]);
1665
+ const hasJest = hasAnyFile(root, [
1666
+ "jest.config.js",
1667
+ "jest.config.cjs",
1668
+ "jest.config.mjs",
1669
+ "jest.config.ts",
1670
+ "jest.config.json"
1671
+ ]) || hasJestPackage || /(?:^|["\s:@/])jest(?:["\s:]|$)/.test(`${text}\n${workspaceText}`) || Boolean(pkg?.jest) || Boolean(workspacePkg?.jest);
1672
+ const hasVitestPackage = hasPackageDependency(pkg, ["vitest", "@vitest/ui", "@vitest/coverage-v8"])
1673
+ || hasPackageDependency(workspacePkg, ["vitest", "@vitest/ui", "@vitest/coverage-v8"]);
1674
+ const hasVitest = hasAnyFile(root, [
1675
+ "vitest.config.js",
1676
+ "vitest.config.cjs",
1677
+ "vitest.config.mjs",
1678
+ "vitest.config.ts"
1679
+ ]) || hasVitestPackage || /(?:^|["\s:@/])vitest(?:["\s:]|$)/.test(`${text}\n${workspaceText}`) || Boolean(pkg?.vitest) || Boolean(workspacePkg?.vitest);
1680
+ const hasMochaPackage = hasPackageDependency(pkg, ["mocha", "@types/mocha", "chai", "@types/chai"])
1681
+ || hasPackageDependency(workspacePkg, ["mocha", "@types/mocha", "chai", "@types/chai"]);
1682
+ const hasMocha = hasAnyFile(root, [
1683
+ ".mocharc.js",
1684
+ ".mocharc.cjs",
1685
+ ".mocharc.mjs",
1686
+ ".mocharc.json",
1687
+ "mocha.opts"
1688
+ ]) || hasMochaPackage || /(?:^|["\s:@/])mocha(?:["\s:]|$)/.test(`${text}\n${workspaceText}`);
1689
+ const detected = [
1690
+ hasJest ? "jest" : null,
1691
+ hasVitest ? "vitest" : null,
1692
+ hasMocha ? "mocha" : null
1693
+ ].filter(Boolean);
1694
+ return detected.length === 1 ? detected[0] : "unknown";
1695
+ }
1696
+
1697
+ function assertRunnerBin(runner, binPath) {
1698
+ if (!existsSync(binPath)) {
1699
+ throw new Error(`${runner} runner binary not found: ${binPath}`);
1700
+ }
1701
+ }
1702
+
1703
+ function sanitizedEnv(extra = {}) {
1704
+ const env = {
1705
+ PATH: process.env.PATH ?? "",
1706
+ HOME: process.env.HOME ?? "",
1707
+ TMPDIR: process.env.TMPDIR ?? tmpdir(),
1708
+ TEMP: process.env.TEMP ?? tmpdir(),
1709
+ TMP: process.env.TMP ?? tmpdir(),
1710
+ NODE_ENV: "test",
1711
+ CI: "1",
1712
+ FORCE_COLOR: "0",
1713
+ NO_COLOR: "1",
1714
+ ...extra
1715
+ };
1716
+ for (const key of Object.keys(env)) {
1717
+ if (isSecretEnvKey(key)) {
1718
+ delete env[key];
1719
+ }
1720
+ }
1721
+ return env;
1722
+ }
1723
+
1724
+ function escapeRegexLiteral(text) {
1725
+ return text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
1726
+ }
1727
+
1728
+ function detectRunnerConfig(repoRoot, candidates) {
1729
+ return candidates.find(name => existsSync(path.join(repoRoot, name))) ?? null;
1730
+ }
1731
+
1732
+ function vitestAliasArrayExpr(aliases) {
1733
+ // Vite string `find` prefix-matches `find` and `find/...`, so a wildcard "@b/*" maps by find="@b".
1734
+ // A non-wildcard key is matched exactly via /^key$/ so it cannot shadow sibling imports.
1735
+ const entries = aliases.map(alias => {
1736
+ const find = alias.star
1737
+ ? JSON.stringify(alias.key.replace(/\/\*$/, ""))
1738
+ : `/^${escapeRegexLiteral(alias.key)}$/`;
1739
+ return `{ find: ${find}, replacement: ${JSON.stringify(alias.replacement)} }`;
1740
+ });
1741
+ return `[${entries.join(", ")}]`;
1742
+ }
1743
+
1744
+ function writeVitestAliasConfig(repoRoot, monoRoot, baseConfigRel, aliases) {
1745
+ // Generate a config that ONLY adds resolve.alias (+ server.fs.allow for the sandbox mono tree,
1746
+ // since the copied sibling source sits outside the package root). If the repo already has a
1747
+ // config, mergeConfig it so plugins/reporters/rootDir are preserved (never clobbered). The
1748
+ // generated config adds RESOLUTION only — it does not touch the test file, the CLI reporter, or
1749
+ // the CLI --root. Returns the generated config's rel path.
1750
+ const base = baseConfigRel ?? detectRunnerConfig(repoRoot, [
1751
+ "vitest.config.ts", "vitest.config.mts", "vitest.config.js", "vitest.config.mjs", "vitest.config.cjs",
1752
+ "vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs", "vite.config.cjs"
1753
+ ]);
1754
+ const aliasExpr = vitestAliasArrayExpr(aliases);
1755
+ const allowExpr = JSON.stringify([monoRoot, repoRoot]);
1756
+ let source;
1757
+ if (base) {
1758
+ const baseSpecifier = `./${base.split(path.sep).join("/")}`;
1759
+ source = [
1760
+ `import { mergeConfig } from "vitest/config";`,
1761
+ `import base from ${JSON.stringify(baseSpecifier)};`,
1762
+ `const aliasConfig = { resolve: { alias: ${aliasExpr} }, server: { fs: { allow: ${allowExpr} } } };`,
1763
+ `export default typeof base === "function"`,
1764
+ ` ? async env => mergeConfig((await base(env)) ?? {}, aliasConfig)`,
1765
+ ` : mergeConfig(base ?? {}, aliasConfig);`,
1766
+ ``
1767
+ ].join("\n");
1768
+ } else {
1769
+ source = `export default { resolve: { alias: ${aliasExpr} }, server: { fs: { allow: ${allowExpr} } } };\n`;
1770
+ }
1771
+ writeFileSync(path.join(repoRoot, GENERATED_VITEST_CONFIG), source);
1772
+ return GENERATED_VITEST_CONFIG;
1773
+ }
1774
+
1775
+ function jestModuleNameMapperExpr(aliases) {
1776
+ const entries = aliases.map(alias => {
1777
+ const key = alias.star
1778
+ ? `^${escapeRegexLiteral(alias.key.replace(/\/\*$/, ""))}/(.*)$`
1779
+ : `^${escapeRegexLiteral(alias.key)}$`;
1780
+ const value = alias.star
1781
+ ? `${alias.replacement.split(path.sep).join("/")}/$1`
1782
+ : alias.replacement.split(path.sep).join("/");
1783
+ return `${JSON.stringify(key)}: ${JSON.stringify(value)}`;
1784
+ });
1785
+ return `{ ${entries.join(", ")} }`;
1786
+ }
1787
+
1788
+ function writeJestAliasConfig(repoRoot, baseConfigRel, aliases) {
1789
+ // Extend the detected jest config's moduleNameMapper. Only require-loadable base shapes
1790
+ // (.js/.cjs/.json, package.json `jest` field, or no base) are merged cleanly; a .ts/.mjs base
1791
+ // isn't cleanly requireable here, so injection is SKIPPED (→ honest unrunnable) rather than
1792
+ // clobber the repo config. Returns the generated config's rel path, or null when skipped.
1793
+ let base = baseConfigRel;
1794
+ let usePackageField = false;
1795
+ if (!base) {
1796
+ const configFile = detectRunnerConfig(repoRoot, ["jest.config.js", "jest.config.cjs", "jest.config.json"]);
1797
+ if (configFile) {
1798
+ base = configFile;
1799
+ } else if (existsSync(path.join(repoRoot, "package.json")) && readPackageJson(repoRoot)?.jest) {
1800
+ base = "package.json";
1801
+ usePackageField = true;
1802
+ }
1803
+ }
1804
+ if (base && !usePackageField && !/\.(?:js|cjs|json)$/.test(base)) {
1805
+ return null;
1806
+ }
1807
+ const mapperExpr = jestModuleNameMapperExpr(aliases);
1808
+ const lines = [];
1809
+ if (usePackageField) {
1810
+ lines.push(`const resolved = require("./package.json").jest || {};`);
1811
+ } else if (base) {
1812
+ const baseSpecifier = `./${base.split(path.sep).join("/")}`;
1813
+ lines.push(`const loaded = require(${JSON.stringify(baseSpecifier)});`);
1814
+ lines.push(`const resolved = (loaded && loaded.default) || loaded || {};`);
1815
+ } else {
1816
+ lines.push(`const resolved = {};`);
1817
+ }
1818
+ const rootDir = base ? `resolved.rootDir` : JSON.stringify(repoRoot);
1819
+ lines.push(
1820
+ `module.exports = { ...resolved, rootDir: ${rootDir}${base ? ` || ${JSON.stringify(repoRoot)}` : ""}, ` +
1821
+ `moduleNameMapper: { ...(resolved.moduleNameMapper || {}), ...${mapperExpr} } };`
1822
+ );
1823
+ writeFileSync(path.join(repoRoot, GENERATED_JEST_CONFIG), `${lines.join("\n")}\n`);
1824
+ return GENERATED_JEST_CONFIG;
1825
+ }
1826
+
1827
+ function mochaTsconfigPaths(aliases, cwd) {
1828
+ const paths = {};
1829
+ for (const alias of aliases) {
1830
+ const key = alias.key;
1831
+ const rel = path.relative(cwd, alias.replacement).split(path.sep).join("/");
1832
+ const target = rel.startsWith(".") ? rel : `./${rel}`;
1833
+ paths[key] = [alias.star ? `${target.replace(/\/$/, "")}/*` : target];
1834
+ }
1835
+ return paths;
1836
+ }
1837
+
1838
+ function writeMochaAliasTsconfig(repoRoot, monoRoot, aliases) {
1839
+ const cwd = monoRoot ?? repoRoot;
1840
+ const baseAbs = [path.join(cwd, "tsconfig.json"), path.join(repoRoot, "tsconfig.json")].find(isRealFile);
1841
+ const base = baseAbs ? (readTsconfigObject(baseAbs) ?? {}) : {};
1842
+ const compilerOptions = {
1843
+ ...(base.compilerOptions ?? {}),
1844
+ baseUrl: ".",
1845
+ paths: {
1846
+ ...(base.compilerOptions?.paths ?? {}),
1847
+ ...mochaTsconfigPaths(aliases, cwd)
1848
+ }
1849
+ };
1850
+ const generated = { ...base, compilerOptions };
1851
+ const out = path.join(cwd, GENERATED_MOCHA_TSCONFIG);
1852
+ writeFileSync(out, `${JSON.stringify(generated, null, 2)}\n`);
1853
+ return out;
1854
+ }
1855
+
1856
+ function runVitest({ repoRoot, monoRoot, testRel, vitestBin, vitestConfigRel, timeoutMs, testEnv, aliases }) {
1857
+ assertRunnerBin("vitest", vitestBin);
1858
+ const started = performance.now();
1859
+ const reportPath = path.join(repoRoot, `.opro-dynamic-proof-report-${process.pid}-${Date.now()}.json`);
1860
+ const configRel = aliases && aliases.length > 0
1861
+ ? writeVitestAliasConfig(repoRoot, monoRoot, vitestConfigRel, aliases)
1862
+ : vitestConfigRel;
1863
+ const vitestArgs = [vitestBin, "run", testRel, "--root", repoRoot, `--reporter=${dynamicProofReporterPath()}`];
1864
+ if (configRel) {
1865
+ vitestArgs.push("--config", path.join(repoRoot, configRel));
1866
+ }
1867
+ const result = spawnSync(process.execPath, vitestArgs, {
1868
+ cwd: repoRoot,
1869
+ encoding: "utf8",
1870
+ timeout: timeoutMs,
1871
+ env: sanitizedEnv({ ...testEnv, OPRO_DYNAMIC_PROOF_REPORT: reportPath })
1872
+ });
1873
+ const elapsedMs = Math.round(performance.now() - started);
1874
+ const report = existsSync(reportPath) ? parseVitestJsonReport(readFileSync(reportPath, "utf8")) : null;
1875
+ rmSync(reportPath, { force: true });
1876
+ return {
1877
+ exitCode: result.status ?? 1,
1878
+ signal: result.signal ?? null,
1879
+ timedOut: Boolean(result.error && result.error.code === "ETIMEDOUT"),
1880
+ stdout: result.stdout ?? "",
1881
+ stderr: result.stderr ?? "",
1882
+ report,
1883
+ elapsedMs,
1884
+ cwd: repoRoot
1885
+ };
1886
+ }
1887
+
1888
+ function runJest({ repoRoot, testRel, jestBin, jestConfigRel, timeoutMs, testEnv, aliases }) {
1889
+ assertRunnerBin("jest", jestBin);
1890
+ const started = performance.now();
1891
+ const reportPath = path.join(repoRoot, `.opro-dynamic-proof-report-${process.pid}-${Date.now()}.json`);
1892
+ const generatedConfig = aliases && aliases.length > 0
1893
+ ? writeJestAliasConfig(repoRoot, jestConfigRel, aliases)
1894
+ : null;
1895
+ const jestArgs = [
1896
+ jestBin,
1897
+ "--runTestsByPath",
1898
+ testRel,
1899
+ "--runInBand",
1900
+ "--no-coverage",
1901
+ `--reporters=${dynamicProofJestReporterPath()}`
1902
+ ];
1903
+ if (generatedConfig) {
1904
+ jestArgs.push("--config", path.join(repoRoot, generatedConfig));
1905
+ } else if (jestConfigRel) {
1906
+ jestArgs.push("--config", path.join(repoRoot, jestConfigRel));
1907
+ } else {
1908
+ jestArgs.push("--rootDir", repoRoot);
1909
+ }
1910
+ const result = spawnSync(process.execPath, jestArgs, {
1911
+ cwd: repoRoot,
1912
+ encoding: "utf8",
1913
+ timeout: timeoutMs,
1914
+ env: sanitizedEnv({ ...testEnv, OPRO_DYNAMIC_PROOF_REPORT: reportPath })
1915
+ });
1916
+ const elapsedMs = Math.round(performance.now() - started);
1917
+ const report = existsSync(reportPath) ? parseVitestJsonReport(readFileSync(reportPath, "utf8")) : null;
1918
+ rmSync(reportPath, { force: true });
1919
+ return {
1920
+ exitCode: result.status ?? 1,
1921
+ signal: result.signal ?? null,
1922
+ timedOut: Boolean(result.error && result.error.code === "ETIMEDOUT"),
1923
+ stdout: result.stdout ?? "",
1924
+ stderr: result.stderr ?? "",
1925
+ report,
1926
+ elapsedMs,
1927
+ cwd: repoRoot
1928
+ };
1929
+ }
1930
+
1931
+ function runMocha({ repoRoot, monoRoot, testRel, mochaBin, timeoutMs, testEnv, aliases }) {
1932
+ assertRunnerBin("mocha", mochaBin);
1933
+ const started = performance.now();
1934
+ const cwd = monoRoot ?? repoRoot;
1935
+ const testArg = path.relative(cwd, path.join(repoRoot, testRel)).split(path.sep).join("/");
1936
+ const reportPath = path.join(cwd, `.opro-dynamic-proof-report-${process.pid}-${Date.now()}.json`);
1937
+ const generatedTsconfig = aliases && aliases.length > 0 ? writeMochaAliasTsconfig(repoRoot, monoRoot, aliases) : null;
1938
+ const preferTsExts = /\.[cm]?tsx?$/.test(testRel);
1939
+ const mochaArgs = [];
1940
+ if (/\.[cm]?tsx?$/.test(testRel)) {
1941
+ mochaArgs.push("--loader", "ts-node/esm");
1942
+ }
1943
+ mochaArgs.push(
1944
+ mochaBin,
1945
+ testArg,
1946
+ "--reporter",
1947
+ dynamicProofMochaReporterPath(),
1948
+ "--timeout",
1949
+ String(timeoutMs),
1950
+ "--no-color"
1951
+ );
1952
+ const result = spawnSync(process.execPath, mochaArgs, {
1953
+ cwd,
1954
+ encoding: "utf8",
1955
+ timeout: timeoutMs,
1956
+ env: sanitizedEnv({
1957
+ ...testEnv,
1958
+ OPRO_DYNAMIC_PROOF_REPORT: reportPath,
1959
+ ...(preferTsExts ? { TS_NODE_PREFER_TS_EXTS: "true" } : {}),
1960
+ ...(generatedTsconfig ? { TS_NODE_PROJECT: generatedTsconfig, TS_CONFIG_PATHS_PROJECT: generatedTsconfig } : {})
1961
+ })
1962
+ });
1963
+ const elapsedMs = Math.round(performance.now() - started);
1964
+ const report = existsSync(reportPath) ? parseVitestJsonReport(readFileSync(reportPath, "utf8")) : parseVitestJsonReport(result.stdout ?? "");
1965
+ rmSync(reportPath, { force: true });
1966
+ return {
1967
+ exitCode: result.status ?? 1,
1968
+ signal: result.signal ?? null,
1969
+ timedOut: Boolean(result.error && result.error.code === "ETIMEDOUT"),
1970
+ stdout: result.stdout ?? "",
1971
+ stderr: result.stderr ?? "",
1972
+ report,
1973
+ elapsedMs,
1974
+ cwd
1975
+ };
1976
+ }
1977
+
1978
+ function runTest({ runner, repoRoot, monoRoot, testRel, vitestBin, jestBin, mochaBin, vitestConfigRel, jestConfigRel, timeoutMs, testEnv, aliases }) {
1979
+ if (runner === "vitest") {
1980
+ return runVitest({ repoRoot, monoRoot, testRel, vitestBin, vitestConfigRel, timeoutMs, testEnv, aliases });
1981
+ }
1982
+ if (runner === "jest") {
1983
+ return runJest({ repoRoot, testRel, jestBin, jestConfigRel, timeoutMs, testEnv, aliases });
1984
+ }
1985
+ if (runner === "mocha") {
1986
+ return runMocha({ repoRoot, monoRoot, testRel, mochaBin, timeoutMs, testEnv, aliases });
1987
+ }
1988
+ return {
1989
+ exitCode: 1,
1990
+ signal: null,
1991
+ timedOut: false,
1992
+ stdout: "",
1993
+ stderr: "Unsupported or unknown test runner",
1994
+ report: null,
1995
+ elapsedMs: 0,
1996
+ cwd: repoRoot
1997
+ };
1998
+ }
1999
+
2000
+ function parseVitestJsonReport(stdout) {
2001
+ const trimmed = stdout.trim();
2002
+ if (!trimmed) {
2003
+ return null;
2004
+ }
2005
+ try {
2006
+ return JSON.parse(trimmed);
2007
+ } catch {
2008
+ return null;
2009
+ }
2010
+ }
2011
+
2012
+ function stackFrames(message) {
2013
+ return String(message).split("\n").slice(1).filter(line => /^\s+at\s+/.test(line));
2014
+ }
2015
+
2016
+ function parseStackFrame(frame) {
2017
+ const trimmed = frame.trim();
2018
+ const match = /(?:\()?(.*?):(\d+):(\d+)\)?$/.exec(trimmed);
2019
+ if (!match) {
2020
+ return null;
2021
+ }
2022
+ let file = match[1].replace(/^at\s+/, "").trim();
2023
+ const paren = file.lastIndexOf("(");
2024
+ if (paren !== -1) {
2025
+ file = file.slice(paren + 1);
2026
+ }
2027
+ if (file.startsWith("file://")) {
2028
+ file = fileURLToPath(file);
2029
+ }
2030
+ return {
2031
+ file,
2032
+ line: Number(match[2]),
2033
+ column: Number(match[3])
2034
+ };
2035
+ }
2036
+
2037
+ function lineHasAssertion(sourceLines, lineNumber) {
2038
+ const index = lineNumber - 1;
2039
+ const window = sourceLines.slice(Math.max(0, index - 2), index + 1).join("\n");
2040
+ return /\bexpect\s*\(|\bassert(?:\.\w+)?\s*\(/.test(window);
2041
+ }
2042
+
2043
+ function lineIsInsideLifecycleHook(sourceLines, lineNumber) {
2044
+ const index = lineNumber - 1;
2045
+ if (index < 0 || index >= sourceLines.length) {
2046
+ return false;
2047
+ }
2048
+ const start = Math.max(0, index - 30);
2049
+ let nearestHook = -1;
2050
+ let nearestTest = -1;
2051
+ for (let i = start; i <= index; i += 1) {
2052
+ const line = sourceLines[i] ?? "";
2053
+ if (/\b(?:beforeAll|beforeEach|afterAll|afterEach)\s*\(/.test(line)) {
2054
+ nearestHook = i;
2055
+ }
2056
+ if (/\b(?:it|test)\s*(?:\.\w+)?\s*\(/.test(line)) {
2057
+ nearestTest = i;
2058
+ }
2059
+ }
2060
+ return nearestHook !== -1 && nearestHook > nearestTest;
2061
+ }
2062
+
2063
+ function hasStructuredMatcherSignal(detail) {
2064
+ return hasVitestMatcherSignal(detail) || hasJestMatcherSignal(detail) || hasMochaAssertionSignal(detail);
2065
+ }
2066
+
2067
+ function hasVitestMatcherSignal(detail) {
2068
+ return detail?.name === "AssertionError"
2069
+ && Object.prototype.hasOwnProperty.call(detail, "actual")
2070
+ && Object.prototype.hasOwnProperty.call(detail, "expected")
2071
+ && typeof detail.operator === "string"
2072
+ && detail.showDiff === true
2073
+ && detail.ok === false
2074
+ && typeof detail.diff === "string";
2075
+ }
2076
+
2077
+ function hasJestMatcherSignal(detail) {
2078
+ const matcherResult = detail?.matcherResult;
2079
+ if (!matcherResult || typeof matcherResult !== "object") {
2080
+ return false;
2081
+ }
2082
+ const errorName = detail.name ?? detail.constructorName ?? "";
2083
+ return /^(|Object|Error|JestAssertionError)$/.test(errorName)
2084
+ && Object.prototype.hasOwnProperty.call(matcherResult, "actual")
2085
+ && Object.prototype.hasOwnProperty.call(matcherResult, "expected")
2086
+ && matcherResult.pass === false;
2087
+ }
2088
+
2089
+ function hasMochaAssertionSignal(detail) {
2090
+ const errorName = detail?.name ?? detail?.constructorName ?? "";
2091
+ return errorName === "AssertionError"
2092
+ && Object.prototype.hasOwnProperty.call(detail, "actual")
2093
+ && Object.prototype.hasOwnProperty.call(detail, "expected")
2094
+ && (
2095
+ typeof detail.operator === "string"
2096
+ || detail.showDiff === true
2097
+ || typeof detail.generatedMessage === "boolean"
2098
+ || detail.code === "ERR_ASSERTION"
2099
+ );
2100
+ }
2101
+
2102
+ function hasJestLegacyMessageSignal(message) {
2103
+ return /^Error: expect\(/.test(String(message).split("\n", 1)[0] ?? "");
2104
+ }
2105
+
2106
+ function canonicalPath(filePath) {
2107
+ try {
2108
+ return realpathSync(filePath);
2109
+ } catch {
2110
+ return path.resolve(filePath);
2111
+ }
2112
+ }
2113
+
2114
+ function resolveFrameFile(file, frameRoot) {
2115
+ if (path.isAbsolute(file)) {
2116
+ return file;
2117
+ }
2118
+ return path.resolve(frameRoot, file);
2119
+ }
2120
+
2121
+ function isAssertionFailureMessage(message, detail, testRel, repoRoot, frameRoot) {
2122
+ if (!hasStructuredMatcherSignal(detail) && !hasJestLegacyMessageSignal(message)) {
2123
+ return false;
2124
+ }
2125
+ const text = String(message);
2126
+ const firstLine = text.split("\n", 1)[0] ?? "";
2127
+ if (!/^AssertionError(?:\b|:|\s+\[)/.test(firstLine) && !/^(Error|JestAssertionError):/.test(firstLine)) {
2128
+ return false;
2129
+ }
2130
+ const testAbs = path.resolve(repoRoot, testRel);
2131
+ const sourceLines = readFileSync(testAbs, "utf8").split(/\r?\n/);
2132
+ return stackFrames(text).some(rawFrame => {
2133
+ const frame = parseStackFrame(rawFrame);
2134
+ return frame
2135
+ && canonicalPath(resolveFrameFile(frame.file, frameRoot)) === canonicalPath(testAbs)
2136
+ && !lineIsInsideLifecycleHook(sourceLines, frame.line)
2137
+ && lineHasAssertion(sourceLines, frame.line);
2138
+ });
2139
+ }
2140
+
2141
+ function assertionIdentity(assertion) {
2142
+ const fullName = typeof assertion?.fullName === "string" ? assertion.fullName.trim() : "";
2143
+ if (fullName) {
2144
+ return fullName;
2145
+ }
2146
+ const title = typeof assertion?.title === "string" ? assertion.title.trim() : "";
2147
+ const ancestors = Array.isArray(assertion?.ancestorTitles)
2148
+ ? assertion.ancestorTitles.filter(item => typeof item === "string" && item.trim()).map(item => item.trim())
2149
+ : [];
2150
+ return [...ancestors, title].filter(Boolean).join(" ");
2151
+ }
2152
+
2153
+ function uniquelyPassedAssertionIdentities(run) {
2154
+ const counts = new Map();
2155
+ const passed = new Set();
2156
+ const results = Array.isArray(run.report?.testResults) ? run.report.testResults : [];
2157
+ for (const suite of results) {
2158
+ const assertions = Array.isArray(suite.assertionResults) ? suite.assertionResults : [];
2159
+ for (const assertion of assertions) {
2160
+ const id = assertionIdentity(assertion);
2161
+ if (!id) {
2162
+ continue;
2163
+ }
2164
+ counts.set(id, (counts.get(id) ?? 0) + 1);
2165
+ if (assertion.status !== "passed") {
2166
+ continue;
2167
+ }
2168
+ passed.add(id);
2169
+ }
2170
+ }
2171
+ const ids = new Set();
2172
+ for (const id of passed) {
2173
+ if (counts.get(id) === 1) {
2174
+ ids.add(id);
2175
+ }
2176
+ }
2177
+ return ids;
2178
+ }
2179
+
2180
+ function isAssertionFailure(run, testRel, repoRoot, baselineRun = null) {
2181
+ const baselinePassedIds = baselineRun ? uniquelyPassedAssertionIdentities(baselineRun) : null;
2182
+ const results = Array.isArray(run.report?.testResults) ? run.report.testResults : [];
2183
+ for (const suite of results) {
2184
+ const assertions = Array.isArray(suite.assertionResults) ? suite.assertionResults : [];
2185
+ for (const assertion of assertions) {
2186
+ if (assertion.status !== "failed") {
2187
+ continue;
2188
+ }
2189
+ if (assertion.failurePhase === "hook") {
2190
+ continue;
2191
+ }
2192
+ if (baselinePassedIds) {
2193
+ const id = assertionIdentity(assertion);
2194
+ if (!id || !baselinePassedIds.has(id)) {
2195
+ continue;
2196
+ }
2197
+ }
2198
+ const messages = Array.isArray(assertion.failureMessages) ? assertion.failureMessages : [];
2199
+ const details = Array.isArray(assertion.failureDetails) ? assertion.failureDetails : [];
2200
+ if (messages.some((message, index) => isAssertionFailureMessage(message, details[index], testRel, repoRoot, run.cwd ?? repoRoot))) {
2201
+ return true;
2202
+ }
2203
+ }
2204
+ }
2205
+ return false;
2206
+ }
2207
+
2208
+ function failureSummary(run) {
2209
+ const results = Array.isArray(run.report?.testResults) ? run.report.testResults : [];
2210
+ for (const suite of results) {
2211
+ if (typeof suite.message === "string" && suite.message.trim()) {
2212
+ return redactSecrets(pickReportableFailureLine(suite.message));
2213
+ }
2214
+ const assertions = Array.isArray(suite.assertionResults) ? suite.assertionResults : [];
2215
+ for (const assertion of assertions) {
2216
+ const messages = Array.isArray(assertion.failureMessages) ? assertion.failureMessages : [];
2217
+ const message = messages.find(item => typeof item === "string" && item.trim());
2218
+ if (message) {
2219
+ return redactSecrets(pickReportableFailureLine(message));
2220
+ }
2221
+ }
2222
+ }
2223
+ const stderr = String(run.stderr ?? "").trim();
2224
+ if (stderr) {
2225
+ return redactSecrets(pickReportableFailureLine(stderr));
2226
+ }
2227
+ return null;
2228
+ }
2229
+
2230
+ function redactSecrets(text) {
2231
+ return String(text)
2232
+ .replace(/([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API[_-]?KEY|ACCESS[_-]?KEY|PRIVATE[_-]?KEY|SECRET[_-]?KEY|PASSPHRASE|CREDENTIAL|PIN|AUTH|COOKIE|SESSION)[A-Z0-9_]*=)[^\s'"]+/gi, "$1[REDACTED]")
2233
+ .replace(/(:\/\/[^:/@\s]+:)[^@/\s]+(@)/g, "$1[REDACTED]$2")
2234
+ .replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[REDACTED]")
2235
+ .replace(/(sk-[A-Za-z0-9_-]{8})[A-Za-z0-9_-]+/g, "$1[REDACTED]");
2236
+ }
2237
+
2238
+ function classify({ baseline, mutant, testRel, repoRoot }) {
2239
+ if (baseline.exitCode !== 0 || baseline.timedOut) {
2240
+ return {
2241
+ status: "unrunnable",
2242
+ proven: false,
2243
+ reason: "baseline test did not pass"
2244
+ };
2245
+ }
2246
+ if (mutant.exitCode === 0 && !mutant.timedOut) {
2247
+ return {
2248
+ status: "associated_survived",
2249
+ proven: false,
2250
+ reason: "mutated target did not change the test outcome"
2251
+ };
2252
+ }
2253
+ if (isAssertionFailure(mutant, testRel, repoRoot, baseline)) {
2254
+ return {
2255
+ status: "proven",
2256
+ proven: true,
2257
+ reason: "baseline passed and mutant failed at an assertion"
2258
+ };
2259
+ }
2260
+ return {
2261
+ status: "associated_non_assertion_failure",
2262
+ proven: false,
2263
+ reason: "mutant failed, but not with a trusted assertion failure"
2264
+ };
2265
+ }
2266
+
2267
+ function main() {
2268
+ const args = parseArgs(process.argv.slice(2));
2269
+ const root = path.resolve(args.root);
2270
+ const testAbs = resolveInside(root, args.test);
2271
+ const targetAbs = resolveInside(root, args.target);
2272
+ const testRel = path.relative(root, testAbs);
2273
+ const targetRel = path.relative(root, targetAbs);
2274
+ const vitestConfigRel = args.vitestConfig ? path.relative(root, resolveInside(root, args.vitestConfig)) : null;
2275
+ const jestConfigRel = args.jestConfig ? path.relative(root, resolveInside(root, args.jestConfig)) : null;
2276
+ const timeoutMs = parseTimeoutMs(args.timeoutMs);
2277
+ const replacementBody = buildReplacementBody(args.replacement, args.replacementMode);
2278
+ const testEnv = parseTestEnv(args.testEnv);
2279
+ const workspaceRoot = detectWorkspaceRoot(root);
2280
+ const runner = detectRunner(root, { ...args, workspaceRoot });
2281
+ const vitestBin = args.vitestBin ? path.resolve(args.vitestBin) : defaultVitestBin(root, workspaceRoot);
2282
+ const jestBin = args.jestBin ? path.resolve(args.jestBin) : defaultJestBin(root, workspaceRoot);
2283
+ const mochaBin = args.mochaBin ? path.resolve(args.mochaBin) : defaultMochaBin(root, workspaceRoot);
2284
+
2285
+ const baselineCopy = copyFixtureRoot(root, "baseline", { linkNodeModules: args.linkNodeModules, workspaceRoot });
2286
+ const mutantCopy = copyFixtureRoot(root, "mutant", { linkNodeModules: args.linkNodeModules, workspaceRoot });
2287
+ try {
2288
+ const baseline = runTest({ runner, repoRoot: baselineCopy.repoRoot, monoRoot: baselineCopy.monoRoot, testRel, vitestBin, jestBin, mochaBin, vitestConfigRel, jestConfigRel, timeoutMs, testEnv, aliases: baselineCopy.aliases });
2289
+ mutateMethod(path.join(mutantCopy.repoRoot, targetRel), args.method, replacementBody);
2290
+ const mutant = runTest({ runner, repoRoot: mutantCopy.repoRoot, monoRoot: mutantCopy.monoRoot, testRel, vitestBin, jestBin, mochaBin, vitestConfigRel, jestConfigRel, timeoutMs, testEnv, aliases: mutantCopy.aliases });
2291
+ const verdict = classify({ baseline, mutant, testRel, repoRoot: mutantCopy.repoRoot });
2292
+ const output = {
2293
+ ...verdict,
2294
+ runner,
2295
+ replacementMode: args.replacementMode,
2296
+ vitestConfig: vitestConfigRel,
2297
+ jestConfig: jestConfigRel,
2298
+ testEnv: Object.keys(testEnv).sort(),
2299
+ test: testRel,
2300
+ target: targetRel,
2301
+ method: args.method,
2302
+ baseline: {
2303
+ exitCode: baseline.exitCode,
2304
+ timedOut: baseline.timedOut,
2305
+ elapsedMs: baseline.elapsedMs,
2306
+ failureSummary: failureSummary(baseline)
2307
+ },
2308
+ mutant: {
2309
+ exitCode: mutant.exitCode,
2310
+ timedOut: mutant.timedOut,
2311
+ elapsedMs: mutant.elapsedMs,
2312
+ assertionFailure: isAssertionFailure(mutant, testRel, mutantCopy.repoRoot, baseline),
2313
+ failureSummary: failureSummary(mutant)
2314
+ },
2315
+ medianProofMs: Math.round((baseline.elapsedMs + mutant.elapsedMs) / 2)
2316
+ };
2317
+ if (args.json) {
2318
+ process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
2319
+ } else {
2320
+ process.stdout.write(`${output.status}: ${output.reason}\n`);
2321
+ process.stdout.write(`baseline=${baseline.exitCode} mutant=${mutant.exitCode} median_ms=${output.medianProofMs}\n`);
2322
+ }
2323
+ process.exitCode = output.status === "unrunnable" ? 2 : 0;
2324
+ } finally {
2325
+ rmSync(baselineCopy.tmpRoot, { recursive: true, force: true });
2326
+ rmSync(mutantCopy.tmpRoot, { recursive: true, force: true });
2327
+ }
2328
+ }
2329
+
2330
+ try {
2331
+ main();
2332
+ } catch (error) {
2333
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${usage()}\n`);
2334
+ process.exitCode = 1;
2335
+ }