@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.
- package/LICENSE +1 -1
- package/README.md +195 -251
- package/dist/local/analyze/analyzer.js +62 -17
- package/dist/local/analyze/treeSitter/engine.js +185 -22
- package/dist/local/autoProve.js +433 -38
- package/dist/local/cli.js +93 -10
- package/dist/local/cliArgs.js +1 -0
- package/dist/local/generate/runHints.js +9 -4
- package/dist/local/graph/factories.js +5 -2
- package/dist/local/ledger.js +1 -1
- package/dist/local/mcp.js +26 -13
- package/dist/local/operations.js +454 -52
- package/dist/local/pack/coverageReport.js +3 -3
- package/dist/local/proofDoctor.js +312 -0
- package/dist/local/rtm.js +40 -10
- package/dist/local/viz/behaviorReportData.js +79 -7
- package/dist/local/viz/behaviorReportHtml.js +526 -615
- package/dist/local/viz/html.js +1 -1
- package/docs/agent-workflow.md +10 -38
- package/docs/agents/claude-code.md +3 -9
- package/docs/agents/codex.md +3 -20
- package/docs/agents/cursor.md +2 -2
- package/docs/agents/opencode.md +2 -2
- package/docs/agents/vscode.md +2 -2
- package/docs/local-proof-kit.md +52 -19
- package/package.json +39 -6
- package/scripts/spikes/go-dynamic-proof-spike.mjs +637 -0
- package/scripts/spikes/go-mutate.go +182 -0
- package/scripts/spikes/java-dynamic-proof-spike.mjs +571 -0
- package/scripts/spikes/java-mutate.mjs +264 -0
- package/scripts/spikes/python-dynamic-proof-spike.mjs +245 -0
- package/scripts/spikes/python-mutate.py +89 -0
package/dist/local/autoProve.js
CHANGED
|
@@ -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
|
|
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).
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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:
|
|
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 = {
|
|
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
|
-
//
|
|
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:
|
|
506
|
-
? "
|
|
507
|
-
:
|
|
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 = {
|
|
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++;
|