@orangepro/orangepro-mcp 0.1.0 → 0.2.1

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.
@@ -22,14 +22,83 @@ import { buildProvider } from "./generate/providers.js";
22
22
  import { resolveContained } from "./reprove/paths.js";
23
23
  import { buildRtm } from "./rtm.js";
24
24
  import { loadLedger } from "./ledger.js";
25
+ import { reportProgress } from "./util/progress.js";
25
26
  import { loadGraph, workspacePaths } from "./workspace.js";
26
27
  import { systemClock } from "./util/time.js";
27
28
  import { redactSecrets } from "./util/redact.js";
28
29
  import { classifyBaselineFailure, EXPERIMENTAL_SQLITE_TEST_ENV, IMPORT_TIME_CATEGORIES, isNeedsSetupCategory, readEnginesNode, targetNeedsExperimentalSqlite } from "./proofRunnability.js";
29
- /** Real dynamic proof is TS/JS-only; only these targets are attemptable. */
30
+ /** Real dynamic proof is profile-gated; only wired runner targets are attemptable. */
30
31
  function isTsJsFile(file) {
31
32
  return /\.[cm]?[jt]sx?$/i.test(file);
32
33
  }
34
+ function isGoFile(file) {
35
+ return /\.go$/i.test(file);
36
+ }
37
+ function isJavaFile(file) {
38
+ return /\.java$/i.test(file);
39
+ }
40
+ function isPythonFile(file) {
41
+ return /\.py$/i.test(file);
42
+ }
43
+ function replacementForTarget(file) {
44
+ return isPythonFile(file) ? "return 0" : "return null;";
45
+ }
46
+ function codeSymbolFile(node) {
47
+ return typeof node.properties.file === "string" ? node.properties.file : node.external_id.replace(/^sym:/, "").split("#")[0];
48
+ }
49
+ function isRunnablePythonTestPath(testRel) {
50
+ const file = testRel.split("::", 1)[0] ?? testRel;
51
+ return /(^|\/)(test_[^/]+|[^/]+_test)\.py$/i.test(file);
52
+ }
53
+ const PYTHON_TEST_NODEID_SUFFIX_RE = /^(?:Test[A-Za-z0-9_]*::)?test_[A-Za-z0-9_]+$/;
54
+ function isRunnableTestForTarget(node, testRel) {
55
+ const file = codeSymbolFile(node);
56
+ if (isPythonFile(file))
57
+ return isRunnablePythonTestPath(testRel);
58
+ return true;
59
+ }
60
+ function pytestNodeidsForFile(sourceRoot, testRel) {
61
+ if (testRel.includes("::"))
62
+ return [testRel];
63
+ if (!isRunnablePythonTestPath(testRel))
64
+ return [testRel];
65
+ let text = "";
66
+ try {
67
+ text = readFileSync(resolve(sourceRoot, testRel), "utf8");
68
+ }
69
+ catch {
70
+ return [testRel];
71
+ }
72
+ const out = [];
73
+ let currentClass = null;
74
+ let classIndent = 0;
75
+ for (const line of text.split(/\r?\n/)) {
76
+ const indent = line.match(/^\s*/)?.[0].length ?? 0;
77
+ const classMatch = /^(\s*)class\s+(Test[A-Za-z0-9_]*)\b/.exec(line);
78
+ if (classMatch) {
79
+ currentClass = classMatch[2];
80
+ classIndent = classMatch[1].length;
81
+ continue;
82
+ }
83
+ if (currentClass && indent <= classIndent && line.trim() !== "" && !line.startsWith(" "))
84
+ currentClass = null;
85
+ const fnMatch = /^(\s*)def\s+(test_[A-Za-z0-9_]*)\s*\(/.exec(line);
86
+ if (!fnMatch)
87
+ continue;
88
+ const fnIndent = fnMatch[1].length;
89
+ if (currentClass && fnIndent > classIndent)
90
+ out.push(`${testRel}::${currentClass}::${fnMatch[2]}`);
91
+ else
92
+ out.push(`${testRel}::${fnMatch[2]}`);
93
+ }
94
+ return out.length ? out.slice(0, 25) : [testRel];
95
+ }
96
+ function pytestNodeidsForTarget(sourceRoot, testRel, testName) {
97
+ if (testName && PYTHON_TEST_NODEID_SUFFIX_RE.test(testName) && !testRel.includes("::")) {
98
+ return [`${testRel}::${testName}`];
99
+ }
100
+ return pytestNodeidsForFile(sourceRoot, testRel);
101
+ }
33
102
  /**
34
103
  * SOLE trust barrier for auto-prove target selection. `opDynamicProof` has NO
35
104
  * eligibility guard — `resolveTargetSymbol` resolves ANY CodeSymbol and the prove
@@ -37,7 +106,17 @@ function isTsJsFile(file) {
37
106
  * Proven against plumbing. Auto-prove must therefore refuse anything that is not an
38
107
  * entry-point-adjacent behavior surface: eligible (top-level `denominator_eligible`),
39
108
  * `behavior_surface === "entrypoint_adjacent"`, and carrying NO `denominator_reason_code`
40
- * (infra_behavior_surface / not_entry_point_adjacent). TS/JS file, always.
109
+ * (infra_behavior_surface / not_entry_point_adjacent).
110
+ *
111
+ * Language: TS/JS (unchanged), Go (G-INT-2), OR Java (J-INT-2). Go dynamic proof
112
+ * (G-1) proves FREE FUNCTIONS ONLY — a Go method classifies `unrunnable` and never
113
+ * mints, so admitting one is safe (never a false Proven) but wastes an attempt; when
114
+ * the clean `symbol_kind === "method"` signal is present we exclude it up front. Java
115
+ * dynamic proof (J-1) is the INVERSE — it proves single-top-level-return METHODS, so
116
+ * we admit Java methods; a non-J-1-shape method (void/constructor/nested/generic) just
117
+ * classifies `unrunnable` and never mints, safe by construction. Everything downstream
118
+ * of `closed`/the cert is language-agnostic and mints Go/Java only through the
119
+ * unchanged G-INT-1/J-INT-1 gates.
41
120
  */
42
121
  export function isEligibleProvableTarget(node) {
43
122
  if (!node || node.kind !== "CodeSymbol")
@@ -48,8 +127,218 @@ export function isEligibleProvableTarget(node) {
48
127
  return false;
49
128
  if (node.properties.denominator_reason_code != null)
50
129
  return false;
51
- const file = typeof node.properties.file === "string" ? node.properties.file : node.external_id.replace(/^sym:/, "").split("#")[0];
52
- return isTsJsFile(file);
130
+ return matchesProvableLanguageShape(node);
131
+ }
132
+ /**
133
+ * The LANGUAGE + oracle-SHAPE half of eligibility, factored out so both the strict
134
+ * `isEligibleProvableTarget` (which layers the entry-point-adjacent SCOPE guards on top)
135
+ * and the relaxed hard-edge existing-tests path share ONE definition of "a shape the
136
+ * oracle can prove". CodeSymbol + a TS/JS file, a Go free function, a Java method, or a
137
+ * Python function/method.
138
+ */
139
+ function matchesProvableLanguageShape(node) {
140
+ if (node.kind !== "CodeSymbol")
141
+ return false;
142
+ const file = codeSymbolFile(node);
143
+ if (isTsJsFile(file))
144
+ return true;
145
+ // Go: free functions only. A method is out of scope for the Go oracle (G-1 refuses it).
146
+ if (isGoFile(file))
147
+ return node.properties.symbol_kind !== "method";
148
+ // Java: METHODS only (J-1 proves single-top-level-return methods). A non-J-1-shape
149
+ // method classifies `unrunnable` and never mints, so admitting all Java methods is
150
+ // safe (never a false Proven); a non-method Java symbol (a class container) is out
151
+ // of scope. No graph return-shape signal exists to pre-filter, so we admit broadly
152
+ // and let the Java oracle refuse non-J-1 shapes at run time.
153
+ if (isJavaFile(file))
154
+ return node.properties.symbol_kind === "method";
155
+ if (isPythonFile(file)) {
156
+ const symbolKind = typeof node.properties.symbol_kind === "string" ? node.properties.symbol_kind : "";
157
+ if (symbolKind !== "function" && symbolKind !== "method")
158
+ return false;
159
+ const member = node.external_id.split("#")[1] ?? "";
160
+ if (/^__.*__$/.test(member))
161
+ return false;
162
+ return true;
163
+ }
164
+ return false;
165
+ }
166
+ /**
167
+ * Relaxed eligibility for the existing-tests HARD-edge path ONLY. A symbol carrying a
168
+ * HARD `TESTED_BY`/`COVERS` proof edge (a real, analyzer-derived test) is admitted even
169
+ * when it is `not_entry_point_adjacent` — a Formatter/Converter SPI method like
170
+ * `PetTypeFormatter#print` is a genuine behavior the repo's own test exercises, but it
171
+ * sits below the entry-point-adjacent denominator bar. Only the `not_entry_point_adjacent`
172
+ * SCOPE guard is dropped, NOT a trust guard: a relaxed pick is only ever proven-or-refused
173
+ * by the frozen oracle, never false-Proven. Guards deliberately KEPT:
174
+ * - `infra_behavior_surface` still excludes plumbing (getters/registry accessors) — a
175
+ * hard edge does not buy an infra symbol an attempt (waste, and preserves the #4 bar);
176
+ * - the language + oracle-shape filter (never hand a class container / Go method down).
177
+ * Used solely on the hard lane; weak MAY_* fan-out stays strict.
178
+ */
179
+ function isEligibleHardExistingTarget(node) {
180
+ if (!node)
181
+ return false;
182
+ if (node.properties.denominator_reason_code === "infra_behavior_surface")
183
+ return false;
184
+ return matchesProvableLanguageShape(node);
185
+ }
186
+ /** Go `_test.go` top-level test-name regex (matches `extractTestNames`'s Go pattern). */
187
+ const GO_TEST_NAME_RE = /^Test[A-Za-z0-9_]+$/;
188
+ /**
189
+ * Edge `test_name` may also be a literal-named subtest path (`TestX/sub`, `TestX/a/b`).
190
+ * The analyzer only records `[A-Za-z0-9_]` segments (a runtime `tc.Name` or a literal
191
+ * needing Go's `-run` sanitization is dropped to the bare parent), so each segment is
192
+ * `-run`-safe verbatim and this stays a strict superset of `GO_TEST_NAME_RE`.
193
+ */
194
+ const GO_TEST_PATH_RE = /^Test[A-Za-z0-9_]+(\/[A-Za-z0-9_]+)*$/;
195
+ /**
196
+ * Anchor a Go test-name path into a fully-anchored `-run` pattern the oracle binds to
197
+ * EXACTLY one test: `TestX` → `^TestX$`; `TestX/sub` → `^TestX$/^sub$` (every segment
198
+ * anchored so no segment can prefix-match a sibling). Mirrors the spike's `targetTestName`.
199
+ */
200
+ function anchorGoTestRun(name) {
201
+ return name.split("/").map((seg) => `^${seg}$`).join("/");
202
+ }
203
+ /**
204
+ * G-INT-2: resolve a Go CodeSymbol target to the single anchored `^TestName$` that
205
+ * exercises it, or null when it cannot be resolved uniquely. The Go oracle selects
206
+ * its target test BY NAME (`go test -run ^TestX$`), so auto-drive must derive that
207
+ * name — G-INT-1 took it as an explicit input.
208
+ *
209
+ * The link is the analyzer's HARD `TESTED_BY`/`COVERS` proof edge (Go free-fn sym ↔
210
+ * `test:<file>_test.go`), emitted only for an eligible Go symbol whose test genuinely
211
+ * asserts on it. PRIMARY: the edge carries `properties.test_name` — the EXACT enclosing
212
+ * `func TestXxx` where the assertion witnessed THIS target (structural metadata, never
213
+ * proof) — which disambiguates even a `_test.go` file with many tests. FALLBACK (old
214
+ * graphs / no edge metadata): the linked TestCase node's file-level `test_names[]`,
215
+ * usable only when it lists exactly ONE Go test. Either way we return a name only when
216
+ * it resolves UNIQUELY across all associated tests; zero/ambiguous ⇒ null (SKIP). A
217
+ * wrong name never mints a false Proven — the mutant survives an unrelated test and
218
+ * classifies unproven — but it wastes a run and the spike refuses a broad pattern anyway.
219
+ */
220
+ export function goTestRunForTarget(nodeById, graph, symId) {
221
+ const names = new Set();
222
+ const testIds = new Set();
223
+ for (const e of graph.edges) {
224
+ const isTestedBy = e.from_external_id === symId && e.relationship_type === "TESTED_BY";
225
+ const isCovers = e.to_external_id === symId && e.relationship_type === "COVERS";
226
+ if (!isTestedBy && !isCovers)
227
+ continue;
228
+ testIds.add(isTestedBy ? e.to_external_id : e.from_external_id);
229
+ const edgeName = e.properties?.test_name;
230
+ if (typeof edgeName === "string" && GO_TEST_PATH_RE.test(edgeName))
231
+ names.add(edgeName);
232
+ }
233
+ if (names.size >= 1)
234
+ return names.size === 1 ? anchorGoTestRun([...names][0]) : null;
235
+ // Fallback: no per-edge test_name (older graph) — a file that holds exactly one
236
+ // (bare, top-level) test; file-level `test_names[]` never carries subtest paths.
237
+ for (const testId of testIds) {
238
+ const tc = nodeById.get(testId);
239
+ if (!tc || tc.kind !== "TestCase")
240
+ continue;
241
+ const testNames = Array.isArray(tc.properties.test_names) ? tc.properties.test_names : [];
242
+ for (const n of testNames)
243
+ if (typeof n === "string" && GO_TEST_NAME_RE.test(n))
244
+ names.add(n);
245
+ }
246
+ return names.size === 1 ? anchorGoTestRun([...names][0]) : null;
247
+ }
248
+ /**
249
+ * Slice 2: the assertion source line for the edge whose `test_name` anchors to `testRun`.
250
+ * Returned only when EXACTLY ONE such edge carries a numeric `assertion_line` (unique-else-
251
+ * undefined, mirroring `goTestRunForTarget`'s discipline). Undefined ⇒ the spike keeps its
252
+ * exact-name behavior. Never widens trust — a wrong line makes the oracle refuse, not prove.
253
+ */
254
+ export function goAssertionLineForTarget(graph, symId, testRun) {
255
+ const lines = new Set();
256
+ for (const e of graph.edges) {
257
+ const isTestedBy = e.from_external_id === symId && e.relationship_type === "TESTED_BY";
258
+ const isCovers = e.to_external_id === symId && e.relationship_type === "COVERS";
259
+ if (!isTestedBy && !isCovers)
260
+ continue;
261
+ const edgeName = e.properties?.test_name;
262
+ const line = e.properties?.assertion_line;
263
+ if (typeof edgeName === "string" && GO_TEST_PATH_RE.test(edgeName) && anchorGoTestRun(edgeName) === testRun && typeof line === "number") {
264
+ lines.add(line);
265
+ }
266
+ }
267
+ return lines.size === 1 ? [...lines][0] : undefined;
268
+ }
269
+ /** A JUnit test method identifier (any Java identifier — JUnit does not require a Test* prefix). */
270
+ const JAVA_TEST_METHOD_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
271
+ /** The simple test-class name for a Java `test:<relPath>` id — the file basename without `.java`. */
272
+ function javaTestClassOf(testRelId) {
273
+ if (!testRelId.startsWith("test:"))
274
+ return null;
275
+ const rel = testRelId.slice("test:".length);
276
+ if (!/\.java$/i.test(rel))
277
+ return null;
278
+ const base = rel.split("/").pop() ?? rel;
279
+ return base.replace(/\.java$/i, "") || null;
280
+ }
281
+ /**
282
+ * J-INT-2: resolve a Java CodeSymbol target to the single `Class#method` JUnit test
283
+ * that exercises it, or null when it cannot be resolved uniquely. The Java oracle
284
+ * selects its target test with `mvn test -Dtest=Class#method`, so auto-drive must
285
+ * derive that selector — J-INT-1 took it as an explicit input.
286
+ *
287
+ * The link is the analyzer's HARD `TESTED_BY`/`COVERS` proof edge (Java method sym ↔
288
+ * `test:<...>Test.java`), emitted only for a Java symbol a JUnit test genuinely
289
+ * asserts on. The test CLASS is the test file's simple class name (its basename, per
290
+ * Java's one-public-class-per-file convention; the spike accepts a simple class).
291
+ * The test METHOD is, PRIMARY: the edge's `properties.test_name` — the exact enclosing
292
+ * `@Test` method where the assertion witnessed THIS target (structural metadata, never
293
+ * proof) — which disambiguates a test class with many @Test methods. FALLBACK (older
294
+ * graphs / no edge metadata): the linked TestCase node's file-level `test_names[]`,
295
+ * usable only when it lists exactly ONE test. We return a `Class#method` only when the
296
+ * PAIR resolves UNIQUELY across all associated tests; zero/ambiguous ⇒ null (SKIP). A
297
+ * wrong selector never mints a false Proven — the mutant survives an unrelated test and
298
+ * classifies unproven — but it wastes a run, so we refuse to guess.
299
+ */
300
+ export function javaTestForTarget(nodeById, graph, symId) {
301
+ const selectors = new Set();
302
+ // Per test id, the enclosing @Test method names named by proof edges (primary source).
303
+ const edgeMethodsByTest = new Map();
304
+ const testIds = new Set();
305
+ for (const e of graph.edges) {
306
+ const isTestedBy = e.from_external_id === symId && e.relationship_type === "TESTED_BY";
307
+ const isCovers = e.to_external_id === symId && e.relationship_type === "COVERS";
308
+ if (!isTestedBy && !isCovers)
309
+ continue;
310
+ const testId = isTestedBy ? e.to_external_id : e.from_external_id;
311
+ testIds.add(testId);
312
+ const edgeName = e.properties?.test_name;
313
+ if (typeof edgeName === "string" && JAVA_TEST_METHOD_RE.test(edgeName)) {
314
+ const set = edgeMethodsByTest.get(testId) ?? new Set();
315
+ set.add(edgeName);
316
+ edgeMethodsByTest.set(testId, set);
317
+ }
318
+ }
319
+ for (const testId of testIds) {
320
+ const cls = javaTestClassOf(testId);
321
+ if (!cls)
322
+ continue;
323
+ for (const m of edgeMethodsByTest.get(testId) ?? [])
324
+ selectors.add(`${cls}#${m}`);
325
+ }
326
+ if (selectors.size >= 1)
327
+ return selectors.size === 1 ? [...selectors][0] : null;
328
+ // Fallback: no per-edge test_name (older graph) — a test class holding exactly one test.
329
+ for (const testId of testIds) {
330
+ const cls = javaTestClassOf(testId);
331
+ if (!cls)
332
+ continue;
333
+ const tc = nodeById.get(testId);
334
+ if (!tc || tc.kind !== "TestCase")
335
+ continue;
336
+ const testNames = Array.isArray(tc.properties.test_names) ? tc.properties.test_names : [];
337
+ const valid = testNames.filter((n) => typeof n === "string" && JAVA_TEST_METHOD_RE.test(n));
338
+ if (valid.length === 1)
339
+ selectors.add(`${cls}#${valid[0]}`);
340
+ }
341
+ return selectors.size === 1 ? [...selectors][0] : null;
53
342
  }
54
343
  const GENERATED_HEADER = "// Generated by OrangePro — do not edit";
55
344
  // "Static map first, dynamically prove top 5": ONE unified dynamic-proof budget spans
@@ -67,6 +356,9 @@ const GEN_WINDOW = 5;
67
356
  // a provable hard-edge symbol is tried. Small K; promote to a flag if a repo needs a wider sweep.
68
357
  const EXISTING_LANE_MAX_WEAK_PER_SYMBOL = 3;
69
358
  export const NO_KEY_MESSAGE = "No provider key; auto-prove skipped — add OPENAI_API_KEY / ANTHROPIC_API_KEY, or use the OrangePro MCP in your coding agent.";
359
+ export function isRoastSurvivor(attempt) {
360
+ return attempt.classification === "non_killing" && attempt.mutant_status === "associated_survived";
361
+ }
70
362
  /**
71
363
  * Resolve the contained absolute path for a generated file, REJECTING any name that
72
364
  * escapes `<sourceRoot>/orangepro_generated/`. Exported so the guardrail is unit-tested.
@@ -89,7 +381,8 @@ const REL_IMPORT_RE = /(\bfrom\s*|\bimport\s*|\brequire\s*\(\s*|\bimport\s*\(\s*
89
381
  * against the target source file's directory, then re-express it relative to
90
382
  * `orangepro_generated/` (POSIX, leading `./`) — e.g. `./order.service` →
91
383
  * `../src/order.service`. Only import/require positions are touched, never plain
92
- * string literals. TS/JS only (dynamic proof is TS/JS-only).
384
+ * string literals. TS/JS only this rewrite serves the TS/JS generation lane;
385
+ * Go/Java/Python proofs route through their own language spikes.
93
386
  */
94
387
  function rewriteRelativeImports(body, targetFileRel, generatedDir) {
95
388
  const baseDir = posix.dirname(targetFileRel.split(sep).join("/"));
@@ -142,6 +435,11 @@ function classifyProof(result, ctx) {
142
435
  }
143
436
  return { classification: "non_killing", reason: record.reason ?? "Mutant survived; the test does not assert on the target's real behavior." };
144
437
  }
438
+ function mutantStatusOf(result) {
439
+ if ("status" in result && result.status === "unrunnable")
440
+ return "unrunnable";
441
+ return result.record.dynamic_proof?.mutant_status;
442
+ }
145
443
  /**
146
444
  * R-1 sibling-dedup key: a baseline-red import-time failure is a deterministic property of
147
445
  * loading the TARGET FILE with a given runner, independent of which test runs it — so
@@ -222,31 +520,34 @@ export function existingAssociatedTests(graph, nodeById) {
222
520
  // `hard` = TESTED_BY/COVERS (the confirmer's structural links); weak = MAY_* candidate
223
521
  // edges. Hard is recorded before weak (graph.edges scanned first), so a test already
224
522
  // linked hard is never downgraded; a later weak dup only upgrades an existing weak to hard.
225
- const add = (symId, testRel, hard) => {
226
- if (!isEligibleProvableTarget(nodeById.get(symId)))
523
+ const add = (symId, testRel, hard, testName) => {
524
+ // A HARD TESTED_BY/COVERS edge is a real derivable test; admit it even when the symbol
525
+ // is not_entry_point_adjacent (relaxed shape-only guard). Weak MAY_* fan-out stays strict.
526
+ const node = nodeById.get(symId);
527
+ if (!(hard ? isEligibleHardExistingTarget(node) : isEligibleProvableTarget(node)))
227
528
  return;
228
529
  const list = out.get(symId);
229
530
  if (!list) {
230
- out.set(symId, [{ test: testRel, hard }]);
531
+ out.set(symId, [{ test: testRel, hard, ...(testName ? { testName } : {}) }]);
231
532
  return;
232
533
  }
233
- const existing = list.find((t) => t.test === testRel);
534
+ const existing = list.find((t) => t.test === testRel && t.testName === testName);
234
535
  if (existing) {
235
536
  if (hard)
236
537
  existing.hard = true;
237
538
  return;
238
539
  }
239
- list.push({ test: testRel, hard });
540
+ list.push({ test: testRel, hard, ...(testName ? { testName } : {}) });
240
541
  };
241
542
  // A sym↔test edge joins one TestCase endpoint to one CodeSymbol endpoint; resolve
242
543
  // whichever side is the symbol so both edge directions are handled uniformly.
243
- const link = (a, b, hard) => {
544
+ const link = (a, b, hard, testName) => {
244
545
  const tb = testFileOf(b);
245
546
  if (tb && nodeById.get(a)?.kind === "CodeSymbol")
246
- return add(a, tb, hard);
547
+ return add(a, tb, hard, testName);
247
548
  const ta = testFileOf(a);
248
549
  if (ta && nodeById.get(b)?.kind === "CodeSymbol")
249
- add(b, ta, hard);
550
+ add(b, ta, hard, testName);
250
551
  };
251
552
  // Eligible symbols grouped by source file, for the file-level MAY_RELATE_TO expansion.
252
553
  const eligibleByFile = new Map();
@@ -267,8 +568,10 @@ export function existingAssociatedTests(graph, nodeById) {
267
568
  return n && n.kind === "TestCase" ? relPath : null;
268
569
  };
269
570
  for (const e of graph.edges) {
270
- if (e.relationship_type === "TESTED_BY" || e.relationship_type === "COVERS")
271
- link(e.from_external_id, e.to_external_id, true);
571
+ if (e.relationship_type === "TESTED_BY" || e.relationship_type === "COVERS") {
572
+ const testName = typeof e.properties?.test_name === "string" ? e.properties.test_name : undefined;
573
+ link(e.from_external_id, e.to_external_id, true, testName);
574
+ }
272
575
  }
273
576
  for (const e of graph.candidate_edges ?? []) {
274
577
  if (e.review_status === "ai_suggested")
@@ -302,7 +605,7 @@ export function orderExistingAttempts(testsBySymbol, maxWeakPerSymbol = EXISTING
302
605
  let weakCount = 0;
303
606
  for (const t of tests) {
304
607
  if (t.hard)
305
- hard.push({ symId, testRel: t.test, hard: true });
608
+ hard.push({ symId, testRel: t.test, hard: true, ...(t.testName ? { testName: t.testName } : {}) });
306
609
  else if (weakCount++ < maxWeakPerSymbol)
307
610
  weak.push({ symId, testRel: t.test, hard: false });
308
611
  }
@@ -313,11 +616,13 @@ export function orderExistingAttempts(testsBySymbol, maxWeakPerSymbol = EXISTING
313
616
  * PR 1.5 lane — prove the repo's OWN existing tests, NO provider key. For each eligible
314
617
  * target with an existing associated test, run the UNCHANGED `opProveLoop` with the test
315
618
  * IN ITS ORIGINAL LOCATION (never copied/relocated — that avoids the generation lane's
316
- * import-grounding pitfall) and a null-sentinel mutant. Closes Proven; survives /
317
- * crashes-pre-assert / setup-fails honest skip (never Proven, and #162 best-ever
318
- * selection means it can never clobber a prior Proven). Consumes from the SHARED unified
319
- * budget (`budget`) the generation lane gets whatever this lane leaves unspent, so
320
- * TOTAL attempts (existing + generation) never exceed the budget.
619
+ * import-grounding pitfall). TS/JS uses a null-sentinel mutant; Go (G-INT-2) selects the
620
+ * test by its derived `^TestName$` and lets the Go oracle compute its own zero-value
621
+ * sentinel. Closes Proven; survives / crashes-pre-assert / setup-fails / (Go)
622
+ * unresolvable-test-name honest skip (never Proven, and #162 best-ever selection means
623
+ * it can never clobber a prior Proven). Consumes from the SHARED unified budget (`budget`)
624
+ * — the generation lane gets whatever this lane leaves unspent, so TOTAL attempts
625
+ * (existing + generation) never exceed the budget.
321
626
  */
322
627
  function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, proveLoop, proveDeps, alreadyProven, importTimeBlocked, budget) {
323
628
  const attempts = [];
@@ -329,13 +634,14 @@ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, p
329
634
  const reader = fileReaderFor(sourceRoot); // R-2: source scan for the node:sqlite env profile
330
635
  // Fix 3: hard TESTED_BY/COVERS pairs first, weak MAY_* pairs after and capped per symbol.
331
636
  const queue = orderExistingAttempts(existingAssociatedTests(graph, nodeById));
332
- for (const { symId, testRel } of queue) {
637
+ for (const { symId, testRel, hard, testName } of queue) {
333
638
  if (attempted >= budget)
334
639
  break;
335
640
  const node = nodeById.get(symId);
336
641
  // Redundant with existingAssociatedTests' own filter, but the eligibility barrier is
337
642
  // the sole guard against handing plumbing to the guard-less prove path — assert it here too.
338
- if (!isEligibleProvableTarget(node))
643
+ // A hard-edge pick uses the relaxed shape-only guard (mirrors the add() decision above).
644
+ if (!(hard ? isEligibleHardExistingTarget(node) : isEligibleProvableTarget(node)))
339
645
  continue;
340
646
  if (changed && !changed.has(symbolFileOf(node)))
341
647
  continue;
@@ -351,29 +657,75 @@ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, p
351
657
  // (engine_mismatch: runner Node outside the declared engines range). Every sibling in this
352
658
  // file fails baseline identically → mark it
353
659
  // needs_setup WITHOUT re-running (and WITHOUT consuming the attempt budget).
660
+ const isPython = isPythonFile(targetFileRel);
661
+ const candidateTestRels = isPython
662
+ ? pytestNodeidsForTarget(sourceRoot, testRel, testName).filter((candidate) => isRunnableTestForTarget(node, candidate))
663
+ : [testRel];
664
+ if (candidateTestRels.length === 0)
665
+ continue;
666
+ const proofTestRel = candidateTestRels[0];
354
667
  const blocked = importTimeBlocked.get(dedupKey(undefined, targetFileRel));
355
668
  if (blocked) {
356
- const attempt = dedupedAttempt(symId, testRel, targetFileRel, blocked);
669
+ const attempt = dedupedAttempt(symId, proofTestRel, targetFileRel, blocked);
357
670
  attempts.push(attempt);
358
671
  needsSetup.push(attempt);
359
672
  continue;
360
673
  }
674
+ // Go and Java targets select the test BY NAME and let their oracle derive its own
675
+ // typed sentinel — no test_path/replacement/link_node_modules/test_env apply. Go
676
+ // uses `go test -run ^TestX$`; Java uses `mvn test -Dtest=Class#method`. Resolve the
677
+ // exact selector; if it can't be resolved uniquely, SKIP without spending the attempt
678
+ // budget (a broad/wrong selector is refused/unmatched by the oracle and wastes a run).
679
+ const isGo = isGoFile(targetFileRel);
680
+ const isJava = isJavaFile(targetFileRel);
681
+ let nativeTestRun = null;
682
+ let goAssertionLine;
683
+ if (isGo) {
684
+ nativeTestRun = goTestRunForTarget(nodeById, graph, symId);
685
+ if (!nativeTestRun)
686
+ continue;
687
+ goAssertionLine = goAssertionLineForTarget(graph, symId, nativeTestRun);
688
+ }
689
+ else if (isJava) {
690
+ nativeTestRun = javaTestForTarget(nodeById, graph, symId);
691
+ if (!nativeTestRun)
692
+ continue;
693
+ }
361
694
  attempted++;
362
695
  // R-2: inject NODE_OPTIONS=--experimental-sqlite via the existing test_env path when the
363
696
  // target references node:sqlite. Makes the baseline runnable only; never mints Proven.
364
- const testEnv = experimentalSqliteTestEnv(reader, targetFileRel);
697
+ const testEnv = isGo || isJava || isPython ? undefined : experimentalSqliteTestEnv(reader, targetFileRel);
698
+ const displayTest = nativeTestRun ?? proofTestRel;
699
+ // G5: name the plan before the attempt — detected runner/selector + target —
700
+ // so a detection miss is a visible, named thing instead of a mysterious 0.
701
+ reportProgress(`proof plan: ${symId} → ${isGo
702
+ ? `go test -run '${nativeTestRun}'`
703
+ : isJava
704
+ ? `mvn test -Dtest=${nativeTestRun}`
705
+ : isPython
706
+ ? `pytest ${displayTest}`
707
+ : `js test file ${displayTest}`}`);
365
708
  let result;
366
709
  try {
367
- result = proveLoop(root,
368
- // link_node_modules: the isolated proof copy excludes node_modules; without linking,
369
- // any target/test importing a repo dependency fails baseline → needs_setup. Linking only
370
- // makes real tests runnable Proven still requires the dynamic oracle's sentinel kill.
371
- { target_symbol: symId, source: sourceRoot, test_path: testRel, replacement: "return null;", link_node_modules: true, ...(testEnv ? { test_env: testEnv } : {}), run_id: `auto-prove-existing-${attempted}` }, proveDeps);
710
+ result = proveLoop(root, nativeTestRun
711
+ ? { target_symbol: symId, source: sourceRoot, test_run: nativeTestRun, ...(goAssertionLine !== undefined ? { go_assertion_line: goAssertionLine } : {}), run_id: `auto-prove-existing-${attempted}` }
712
+ // link_node_modules: the isolated proof copy excludes node_modules; without linking,
713
+ // any target/test importing a repo dependency fails baseline needs_setup. Linking only
714
+ // makes real tests runnable Proven still requires the dynamic oracle's sentinel kill.
715
+ : {
716
+ target_symbol: symId,
717
+ source: sourceRoot,
718
+ test_path: proofTestRel,
719
+ replacement: replacementForTarget(targetFileRel),
720
+ link_node_modules: true,
721
+ ...(testEnv ? { test_env: testEnv } : {}),
722
+ run_id: `auto-prove-existing-${attempted}`
723
+ }, proveDeps);
372
724
  }
373
725
  catch (e) {
374
726
  const attempt = {
375
727
  target_symbol: symId,
376
- test_path: testRel,
728
+ test_path: displayTest,
377
729
  classification: "needs_setup",
378
730
  reason: `Proof could not run: ${redactSecrets(errMsg(e))}`
379
731
  };
@@ -382,7 +734,14 @@ function proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, p
382
734
  continue;
383
735
  }
384
736
  const { classification, reason, category } = classifyProof(result, { sourceRoot, targetFileRel });
385
- const attempt = { target_symbol: symId, test_path: testRel, classification, reason, category };
737
+ const attempt = {
738
+ target_symbol: symId,
739
+ test_path: displayTest,
740
+ classification,
741
+ reason,
742
+ category,
743
+ mutant_status: mutantStatusOf(result)
744
+ };
386
745
  attempts.push(attempt);
387
746
  if (classification === "proven") {
388
747
  proven++;
@@ -442,6 +801,20 @@ export async function autoProve(root, opts, deps) {
442
801
  const autoLimit = Math.max(1, Math.min(MAX_AUTO_LIMIT, Math.floor(opts.autoLimit ?? DEFAULT_AUTO_LIMIT)));
443
802
  // ── Lane 1: existing associated tests — NO key required, runs FIRST (PR 1.5). ──
444
803
  const ex = proveExistingAssociatedTests(root, graph, sourceRoot, nodeById, opts, proveLoop, proveDeps, alreadyProven, importTimeBlocked, autoLimit);
804
+ if (opts.existingOnly) {
805
+ const status = ex.proven > 0 ? "proven-run" : ex.attempted > 0 ? "ran-no-proof" : "no-targets";
806
+ return {
807
+ ran: ex.attempted > 0,
808
+ status,
809
+ reason: "existing-tests-only: generation disabled.",
810
+ attempted: ex.attempted,
811
+ proven: ex.proven,
812
+ needs_setup: ex.needsSetup,
813
+ skipped: [],
814
+ generated_files: [],
815
+ attempts: ex.attempts
816
+ };
817
+ }
445
818
  // Key gate applies ONLY to the generation lane. No provider key ⇒ generation is skipped
446
819
  // (no files, no fake proof) with explicit guidance; the existing-tests lane still counts.
447
820
  const providerConfig = resolveProviderConfig(deps.env, { provider: opts.provider, model: opts.model });
@@ -489,7 +862,7 @@ export async function autoProve(root, opts, deps) {
489
862
  const window = candidates.slice(start, start + GEN_WINDOW);
490
863
  const need = genBudget - attempted;
491
864
  const windowIds = window.map((g) => g.id);
492
- const gen = await generate(graph, { target_ids: windowIds, limit: Math.min(windowIds.length, need) }, provider, reader, clock);
865
+ const gen = await generate(graph, { target_ids: windowIds, limit: Math.min(windowIds.length, need), ...(opts.prompt_version ? { prompt_version: opts.prompt_version } : {}) }, provider, reader, clock);
493
866
  const tests = gen.generated_tests;
494
867
  // Global start offset so filenames stay unique across windows — runHintsFor
495
868
  // otherwise resets its index to 0 per window and same-slug targets collide.
@@ -498,13 +871,26 @@ export async function autoProve(root, opts, deps) {
498
871
  const test = tests[i];
499
872
  const hint = hints[i];
500
873
  if (!hint.prove_run) {
501
- // Non-TS/JS target or no resolvable code symbol not attemptable.
874
+ // No JS prove_run. Go and Java are dynamically provable only through their OWN
875
+ // test in the target's package/module (the existing-tests lane): the Go oracle
876
+ // runs `go test -run ^TestX$ ./<pkgdir>` and the Java oracle runs
877
+ // `mvn test -Dtest=Class#method` in the target's Maven module, so a freshly
878
+ // generated test written to orangepro_generated/ is in the wrong package/module
879
+ // and can never be reached. Skip generated Go/Java here with an honest reason
880
+ // (the existing-tests lane covers both).
881
+ const genFile = hint.target_symbol_external_id ? symbolFile(hint.target_symbol_external_id) : "";
882
+ const goGen = genFile ? isGoFile(genFile) : false;
883
+ const javaGen = genFile ? isJavaFile(genFile) : false;
502
884
  skipped.push({
503
885
  target_symbol: hint.target_symbol_external_id,
504
886
  title: test.title,
505
- reason: hint.target_symbol_external_id
506
- ? "Target is not TS/JS; dynamic proof supports TS/JS CodeSymbol targets only."
507
- : "No resolvable TS/JS code-symbol target to prove."
887
+ reason: goGen
888
+ ? "Go dynamic proof runs the target package's own test; a generated Go test is out of that package — proven via the existing-tests lane instead."
889
+ : javaGen
890
+ ? "Java dynamic proof runs the target module's own test; a generated Java test is out of that module — proven via the existing-tests lane instead."
891
+ : hint.target_symbol_external_id
892
+ ? "Target is not TS/JS; dynamic proof supports TS/JS CodeSymbol targets only."
893
+ : "No resolvable TS/JS code-symbol target to prove."
508
894
  });
509
895
  continue;
510
896
  }
@@ -556,6 +942,8 @@ export async function autoProve(root, opts, deps) {
556
942
  attempted++;
557
943
  // R-2: inject the node:sqlite env profile when the TARGET source references the builtin.
558
944
  const testEnv = experimentalSqliteTestEnv(reader, targetFileRel);
945
+ // G5: plan line for the generated-test attempt (runner from the run hint).
946
+ reportProgress(`proof plan: ${hint.prove_run.args.target_symbol} → ${hint.prove_run.args.runner ?? "auto"} on ${writeRel}`);
559
947
  let result;
560
948
  try {
561
949
  result = proveLoop(root, {
@@ -582,7 +970,14 @@ export async function autoProve(root, opts, deps) {
582
970
  continue;
583
971
  }
584
972
  const { classification, reason, category } = classifyProof(result, { sourceRoot, targetFileRel });
585
- const attempt = { target_symbol, test_path: writeRel, classification, reason, category };
973
+ const attempt = {
974
+ target_symbol,
975
+ test_path: writeRel,
976
+ classification,
977
+ reason,
978
+ category,
979
+ mutant_status: mutantStatusOf(result)
980
+ };
586
981
  attempts.push(attempt);
587
982
  if (classification === "proven")
588
983
  proven++;