@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/operations.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { LOCAL_GRAPH_SCHEMA_VERSION } from "./graph/ontology.js";
|
|
6
6
|
import { systemClock } from "./util/time.js";
|
|
@@ -42,6 +42,7 @@ import { confirmedCoverageByLayer } from "./score/coverage.js";
|
|
|
42
42
|
import { prepareRuntimeCoverage } from "./analyze/coverageArtifacts.js";
|
|
43
43
|
import { appendLedgerRecord, canReproveLanguage, loadLedger, ledgerStats, proofEdgesFor, reproveTarget, resolveTargetSymbol, targetFingerprint, targetLanguage } from "./ledger.js";
|
|
44
44
|
import { buildRtm, renderRtmCsv, renderRtmMarkdown } from "./rtm.js";
|
|
45
|
+
import { buildProofDoctor, distillProofAttempts, loadProofAttempts, proofAttemptsFresh, writeProofAttempts } from "./proofDoctor.js";
|
|
45
46
|
import { tryScopedReprove } from "./reprove/scoped.js";
|
|
46
47
|
import { resolveContained, toWorkspaceRel } from "./reprove/paths.js";
|
|
47
48
|
import { hashBuffer } from "./util/hash.js";
|
|
@@ -56,8 +57,81 @@ function dynamicProofSpikePath() {
|
|
|
56
57
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
57
58
|
return resolve(here, "..", "..", "scripts", "spikes", "dynamic-proof-spike.mjs");
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the Go MODULE root: the nearest `go.mod` ancestor of the target file,
|
|
62
|
+
* searched from the target's directory up to (and including) sourceRoot. For a
|
|
63
|
+
* single-module repo this is sourceRoot itself. Confined to sourceRoot so the spike
|
|
64
|
+
* never sandboxes a directory outside the trusted checkout.
|
|
65
|
+
* ponytail: nearest-ancestor go.mod; multi-module edge cases (nested/replace) resolve to G-INT-3.
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* G2 — confinement bound for module-root walk-up: the walk may pass ABOVE the
|
|
69
|
+
* analyzed source path (fixing `opro start ./subpkg` inside a bigger module)
|
|
70
|
+
* but never escapes the INVOCATION root the user ran opro from. If the analyzed
|
|
71
|
+
* path lies outside the invocation root, keep the old sourceRoot confinement.
|
|
72
|
+
* Discovery/scoping only — the proof gate is untouched, and when the chosen
|
|
73
|
+
* module root differs from the analyzed path it is named in progress output
|
|
74
|
+
* and on the result (module_root).
|
|
75
|
+
*/
|
|
76
|
+
function moduleRootBound(sourceRoot, workspaceRoot) {
|
|
77
|
+
const src = resolve(sourceRoot);
|
|
78
|
+
const ws = resolve(workspaceRoot);
|
|
79
|
+
return src === ws || src.startsWith(ws + sep) ? ws : src;
|
|
80
|
+
}
|
|
81
|
+
function goModuleRoot(sourceRoot, targetRel, workspaceRoot) {
|
|
82
|
+
let dir = dirname(resolve(sourceRoot, targetRel));
|
|
83
|
+
const stop = moduleRootBound(sourceRoot, workspaceRoot);
|
|
84
|
+
for (;;) {
|
|
85
|
+
if (existsSync(join(dir, "go.mod")))
|
|
86
|
+
return dir;
|
|
87
|
+
if (dir === stop)
|
|
88
|
+
break;
|
|
89
|
+
const parent = dirname(dir);
|
|
90
|
+
if (parent === dir)
|
|
91
|
+
break;
|
|
92
|
+
dir = parent;
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`No go.mod found for Go target ${targetRel} under ${stop}.`);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the Java MODULE root: the nearest `pom.xml` (or `build.gradle` /
|
|
98
|
+
* `build.gradle.kts`) ancestor of the target file, searched from the target's
|
|
99
|
+
* directory up to (and including) sourceRoot. Mirrors goModuleRoot — for a
|
|
100
|
+
* single-module project this is sourceRoot itself. Confined to sourceRoot so the
|
|
101
|
+
* spike never sandboxes a directory outside the trusted checkout.
|
|
102
|
+
* ponytail: nearest-ancestor build file; multi-module reactor edge cases resolve to J-INT-3.
|
|
103
|
+
*/
|
|
104
|
+
function javaModuleRoot(sourceRoot, targetRel, workspaceRoot) {
|
|
105
|
+
let dir = dirname(resolve(sourceRoot, targetRel));
|
|
106
|
+
const stop = moduleRootBound(sourceRoot, workspaceRoot);
|
|
107
|
+
for (;;) {
|
|
108
|
+
if (existsSync(join(dir, "pom.xml")) || existsSync(join(dir, "build.gradle")) || existsSync(join(dir, "build.gradle.kts"))) {
|
|
109
|
+
return dir;
|
|
110
|
+
}
|
|
111
|
+
if (dir === stop)
|
|
112
|
+
break;
|
|
113
|
+
const parent = dirname(dir);
|
|
114
|
+
if (parent === dir)
|
|
115
|
+
break;
|
|
116
|
+
dir = parent;
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`No pom.xml or build.gradle found for Java target ${targetRel} under ${stop}.`);
|
|
119
|
+
}
|
|
120
|
+
/** Route to the per-language spike. TS/JS keeps the original path; native profiles use their own mechanisms. */
|
|
121
|
+
function dynamicProofSpikePathFor(language) {
|
|
122
|
+
if (language === "go" || language === "java" || language === "python") {
|
|
123
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
124
|
+
const script = language === "go"
|
|
125
|
+
? "go-dynamic-proof-spike.mjs"
|
|
126
|
+
: language === "java"
|
|
127
|
+
? "java-dynamic-proof-spike.mjs"
|
|
128
|
+
: "python-dynamic-proof-spike.mjs";
|
|
129
|
+
return resolve(here, "..", "..", "scripts", "spikes", script);
|
|
130
|
+
}
|
|
131
|
+
return dynamicProofSpikePath();
|
|
132
|
+
}
|
|
59
133
|
function defaultDynamicProofRunner(args, opts = {}) {
|
|
60
|
-
const script = dynamicProofSpikePath();
|
|
134
|
+
const script = opts.scriptPath ?? dynamicProofSpikePath();
|
|
61
135
|
if (!existsSync(script)) {
|
|
62
136
|
throw new Error(`Dynamic proof spike runner not found at ${script}. Run this command from an OrangePro source checkout with scripts/spikes available.`);
|
|
63
137
|
}
|
|
@@ -83,6 +157,107 @@ function parseDynamicProofJson(stdout, stderr) {
|
|
|
83
157
|
throw new Error(`Dynamic proof runner did not return JSON.${detail ? ` stderr/stdout: ${detail}` : ""}`);
|
|
84
158
|
}
|
|
85
159
|
}
|
|
160
|
+
/** The label recorded as the cert `sentinel` for a Go proof (Go derives a zero-value return). */
|
|
161
|
+
const GO_SENTINEL_LABEL = "go-zero-return";
|
|
162
|
+
/**
|
|
163
|
+
* Map the Go spike's JSON onto the SAME language-agnostic DynamicProofOracleSummary the
|
|
164
|
+
* unchanged `dynamicProofSucceeded` and cert block read. This is the ONE trust-adjacent
|
|
165
|
+
* seam, so mapping is STRICT: `assertionFailure` is true ONLY when the Go spike genuinely
|
|
166
|
+
* returned `mutant.trustedAssertion === true`. A skipped/refused mutation, a survived
|
|
167
|
+
* mutation, or any absent field maps to `assertionFailure = false` — never a false Proven.
|
|
168
|
+
*/
|
|
169
|
+
function mapGoOracle(go) {
|
|
170
|
+
const mutant = go.mutant;
|
|
171
|
+
// A refused/skipped mutation co-occurs with status "unrunnable"; it must never close.
|
|
172
|
+
const skipped = Boolean(mutant && "skipped" in mutant && mutant.skipped === true);
|
|
173
|
+
const runMutant = !skipped && mutant ? mutant : undefined;
|
|
174
|
+
return {
|
|
175
|
+
status: go.status,
|
|
176
|
+
proven: go.proven === true,
|
|
177
|
+
reason: go.reason,
|
|
178
|
+
runner: "go",
|
|
179
|
+
replacementMode: GO_SENTINEL_LABEL,
|
|
180
|
+
test: go.testRun,
|
|
181
|
+
target: go.target,
|
|
182
|
+
method: go.func,
|
|
183
|
+
baseline: go.baseline
|
|
184
|
+
? { exitCode: go.baseline.exitCode, timedOut: go.baseline.timedOut, failureSummary: go.baseline.failureSummary ?? null }
|
|
185
|
+
: undefined,
|
|
186
|
+
// STRICT: trustedAssertion must be exactly true AND the mutant must carry a NUMERIC non-zero
|
|
187
|
+
// exit code. A missing/undefined/zero exitCode ⇒ assertionFailure=false ⇒ non-close, so the
|
|
188
|
+
// shared gate (which asserts `exitCode !== 0`, where `undefined !== 0` is truthy) can never
|
|
189
|
+
// false-close on an absent exit code. skipped/absent/false ⇒ false ⇒ non-close.
|
|
190
|
+
mutant: runMutant
|
|
191
|
+
? {
|
|
192
|
+
exitCode: runMutant.exitCode,
|
|
193
|
+
timedOut: runMutant.timedOut,
|
|
194
|
+
assertionFailure: runMutant.trustedAssertion === true &&
|
|
195
|
+
typeof runMutant.exitCode === "number" &&
|
|
196
|
+
runMutant.exitCode !== 0
|
|
197
|
+
}
|
|
198
|
+
: { assertionFailure: false },
|
|
199
|
+
medianProofMs: go.medianProofMs
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/** Test-only handle on the Go→oracle mapper (the one trust-adjacent seam). */
|
|
203
|
+
export function __mapGoOracleForTest(go) {
|
|
204
|
+
return mapGoOracle(go);
|
|
205
|
+
}
|
|
206
|
+
/** The label recorded as the cert `sentinel` for a Java proof (Java derives a typed sentinel from the return type). */
|
|
207
|
+
const JAVA_SENTINEL_LABEL = "java-typed-sentinel";
|
|
208
|
+
/**
|
|
209
|
+
* Map the Java spike's JSON onto the SAME language-agnostic DynamicProofOracleSummary the
|
|
210
|
+
* unchanged `dynamicProofSucceeded` and cert block read. This is the ONE trust-adjacent
|
|
211
|
+
* seam, so mapping is STRICT: `assertionFailure` is true ONLY when the Java spike genuinely
|
|
212
|
+
* signalled a trusted JUnit assertion failure — the mutant ran (not skipped), the SAME
|
|
213
|
+
* target test FAILED, AND surefire classified it as a trusted assertion (`isAssertion`).
|
|
214
|
+
* A skipped/refused mutation, a survived (associated_survived) mutation, a compile failure,
|
|
215
|
+
* a non-assertion error, or any absent field maps to `assertionFailure = false` — never a
|
|
216
|
+
* false Proven. The spike itself already gates `status: "proven"` on all of this; the mapper
|
|
217
|
+
* re-derives the assertion signal independently so the cert's `mutant_failed_assertion` never
|
|
218
|
+
* trusts the spike's verdict alone.
|
|
219
|
+
*/
|
|
220
|
+
function mapJavaOracle(java) {
|
|
221
|
+
const mutant = java.mutant;
|
|
222
|
+
// A refused/skipped mutation co-occurs with status "unrunnable"; it must never close.
|
|
223
|
+
const skipped = Boolean(mutant && "skipped" in mutant && mutant.skipped === true);
|
|
224
|
+
const runMutant = !skipped && mutant
|
|
225
|
+
? mutant
|
|
226
|
+
: undefined;
|
|
227
|
+
return {
|
|
228
|
+
status: java.status,
|
|
229
|
+
proven: java.proven === true,
|
|
230
|
+
reason: java.reason,
|
|
231
|
+
runner: "junit",
|
|
232
|
+
replacementMode: JAVA_SENTINEL_LABEL,
|
|
233
|
+
test: java.testClass && java.testMethod ? `${java.testClass}#${java.testMethod}` : java.testMethod,
|
|
234
|
+
target: java.target,
|
|
235
|
+
method: java.method,
|
|
236
|
+
baseline: java.baseline
|
|
237
|
+
? { exitCode: java.baseline.exitCode, timedOut: java.baseline.timedOut, failureSummary: java.baseline.failureSummary ?? null }
|
|
238
|
+
: undefined,
|
|
239
|
+
// STRICT: the mutant must have RUN, FAILED the same target test, the failure must be a trusted
|
|
240
|
+
// JUnit assertion, AND the mutant must carry a NUMERIC non-zero exit code. A missing/undefined/
|
|
241
|
+
// zero exitCode ⇒ assertionFailure=false ⇒ non-close, so the shared gate (which asserts
|
|
242
|
+
// `exitCode !== 0`, where `undefined !== 0` is truthy) can never false-close on an absent exit
|
|
243
|
+
// code. skipped/survived/compile-fail/non-assertion/absent ⇒ false ⇒ non-close.
|
|
244
|
+
mutant: runMutant
|
|
245
|
+
? {
|
|
246
|
+
exitCode: runMutant.exitCode,
|
|
247
|
+
timedOut: runMutant.timedOut,
|
|
248
|
+
assertionFailure: runMutant.targetTestFailed === true &&
|
|
249
|
+
runMutant.isAssertion === true &&
|
|
250
|
+
typeof runMutant.exitCode === "number" &&
|
|
251
|
+
runMutant.exitCode !== 0
|
|
252
|
+
}
|
|
253
|
+
: { assertionFailure: false },
|
|
254
|
+
medianProofMs: java.medianProofMs
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/** Test-only handle on the Java→oracle mapper (the one trust-adjacent seam). */
|
|
258
|
+
export function __mapJavaOracleForTest(java) {
|
|
259
|
+
return mapJavaOracle(java);
|
|
260
|
+
}
|
|
86
261
|
function dynamicProofSucceeded(oracle) {
|
|
87
262
|
return (oracle.status === "proven" &&
|
|
88
263
|
oracle.proven === true &&
|
|
@@ -617,6 +792,17 @@ export function opDoctor(root) {
|
|
|
617
792
|
const graph = loadGraph(workspacePaths(root).graphPath);
|
|
618
793
|
return doctorGraph(graph, scoreGraph(graph));
|
|
619
794
|
}
|
|
795
|
+
/**
|
|
796
|
+
* G1 — proof-focused doctor: why are top targets not Dynamically Proven?
|
|
797
|
+
* Read-only: consumes the canonical RTM judgment (buildRtm) plus the last run's
|
|
798
|
+
* redacted proof-attempts sidecar. Mints nothing, mutates no ledger, writes no
|
|
799
|
+
* files — under staleness or ambiguity it fails closed to "re-run".
|
|
800
|
+
*/
|
|
801
|
+
export function opProofDoctor(root) {
|
|
802
|
+
const graph = loadGraph(workspacePaths(root).graphPath);
|
|
803
|
+
const rtm = buildRtm(graph, loadLedger(root));
|
|
804
|
+
return buildProofDoctor(graph, rtm, loadProofAttempts(root));
|
|
805
|
+
}
|
|
620
806
|
export function opGaps(root, opts = {}) {
|
|
621
807
|
const graph = loadGraph(workspacePaths(root).graphPath);
|
|
622
808
|
const gaps = findGaps(graph, opts);
|
|
@@ -736,6 +922,33 @@ export function opRecordRun(root, opts, deps = defaultDeps()) {
|
|
|
736
922
|
ts: deps.clock()
|
|
737
923
|
});
|
|
738
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* G2 — Python sandbox NARROWING: the nearest pyproject.toml/setup.py/setup.cfg
|
|
927
|
+
* ancestor of the target, confined to sourceRoot, used only when the test also
|
|
928
|
+
* lives inside it. Falls back to sourceRoot (today's behavior) otherwise —
|
|
929
|
+
* this only ever shrinks the copied sandbox, never widens or breaks it.
|
|
930
|
+
*/
|
|
931
|
+
function pythonModuleRoot(sourceRoot, targetRel, testRel) {
|
|
932
|
+
const stop = resolve(sourceRoot);
|
|
933
|
+
let dir = dirname(resolve(sourceRoot, targetRel));
|
|
934
|
+
let found = null;
|
|
935
|
+
for (;;) {
|
|
936
|
+
if (existsSync(join(dir, "pyproject.toml")) || existsSync(join(dir, "setup.py")) || existsSync(join(dir, "setup.cfg"))) {
|
|
937
|
+
found = dir;
|
|
938
|
+
break;
|
|
939
|
+
}
|
|
940
|
+
if (dir === stop)
|
|
941
|
+
break;
|
|
942
|
+
const parent = dirname(dir);
|
|
943
|
+
if (parent === dir)
|
|
944
|
+
break;
|
|
945
|
+
dir = parent;
|
|
946
|
+
}
|
|
947
|
+
if (!found || found === stop)
|
|
948
|
+
return stop;
|
|
949
|
+
const testAbs = resolve(sourceRoot, testRel);
|
|
950
|
+
return testAbs === found || testAbs.startsWith(found + sep) ? found : stop;
|
|
951
|
+
}
|
|
739
952
|
export function opDynamicProof(root, opts, deps = defaultDeps()) {
|
|
740
953
|
const paths = workspacePaths(root);
|
|
741
954
|
const graph = loadGraph(paths.graphPath);
|
|
@@ -745,59 +958,199 @@ export function opDynamicProof(root, opts, deps = defaultDeps()) {
|
|
|
745
958
|
throw new Error("prove requires --target-symbol sym:<file>#<Symbol>, or a target id that resolves to exactly one CodeSymbol.");
|
|
746
959
|
}
|
|
747
960
|
const language = targetLanguage(target);
|
|
748
|
-
if (language !== "typescript") {
|
|
749
|
-
throw new Error(`prove currently supports JavaScript/TypeScript targets only; found ${language}.`);
|
|
961
|
+
if (language !== "typescript" && language !== "go" && language !== "java" && language !== "python") {
|
|
962
|
+
throw new Error(`prove currently supports JavaScript/TypeScript, Go, Java, and Python targets only; found ${language}.`);
|
|
750
963
|
}
|
|
751
|
-
if (!opts.test_path)
|
|
752
|
-
throw new Error("prove requires --test <path>.");
|
|
753
|
-
if (opts.replacement === undefined)
|
|
754
|
-
throw new Error("prove requires --replacement <sentinel>.");
|
|
755
964
|
const sourceRoot = resolve(opts.source ?? root);
|
|
756
965
|
assertProofSourceMatchesGraph(sourceRoot, graph);
|
|
757
966
|
const symbolTarget = symbolTargetParts(target);
|
|
758
967
|
const providedTargetRel = opts.target_path ? toWorkspaceRel(sourceRoot, resolveContained(sourceRoot, opts.target_path)) : undefined;
|
|
759
968
|
assertProofTargetMatchesSymbol({ ...opts, target_path: providedTargetRel }, symbolTarget);
|
|
760
|
-
const testRel = toWorkspaceRel(sourceRoot, resolveContained(sourceRoot, opts.test_path));
|
|
761
969
|
const targetRel = symbolTarget.file;
|
|
762
970
|
const method = symbolTarget.method;
|
|
763
971
|
assertTargetFileFresh(sourceRoot, targetRel, graph);
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
972
|
+
// Per-language routing produces the spike args + a mapped oracle in the SAME
|
|
973
|
+
// DynamicProofOracleSummary shape. Everything from `closed` onward (the trust gate,
|
|
974
|
+
// the cert, appendLedgerRecord, the return summary) is language-agnostic and unchanged.
|
|
975
|
+
let oracle;
|
|
976
|
+
let proofModuleRoot;
|
|
977
|
+
let testRel;
|
|
978
|
+
let replacementMode;
|
|
979
|
+
let runner;
|
|
980
|
+
if (language === "go") {
|
|
981
|
+
// Go selects the target test by NAME (`go test -run ^TestX$`), derives its own
|
|
982
|
+
// zero-value sentinel, and always uses `go test`. No --replacement/--runner/
|
|
983
|
+
// --vitest-config/--jest-config/--test-env/--link-node-modules apply.
|
|
984
|
+
if (!opts.test_run)
|
|
985
|
+
throw new Error("prove requires --test-run '^TestName$' for Go targets.");
|
|
986
|
+
if (!/^\^.+\$$/.test(opts.test_run)) {
|
|
987
|
+
throw new Error("prove --test-run must be a fully-anchored test name, e.g. '^TestName$'.");
|
|
988
|
+
}
|
|
989
|
+
const goRoot = goModuleRoot(sourceRoot, targetRel, root);
|
|
990
|
+
if (goRoot !== resolve(sourceRoot)) {
|
|
991
|
+
proofModuleRoot = goRoot;
|
|
992
|
+
reportProgress(`proof scope: Go module root ${goRoot} (above the analyzed path)`);
|
|
993
|
+
}
|
|
994
|
+
const args = [
|
|
995
|
+
"--root",
|
|
996
|
+
goRoot,
|
|
997
|
+
"--target",
|
|
998
|
+
relative(goRoot, resolve(sourceRoot, targetRel)).split(sep).join("/"),
|
|
999
|
+
"--func",
|
|
1000
|
+
method,
|
|
1001
|
+
"--test-run",
|
|
1002
|
+
opts.test_run,
|
|
1003
|
+
"--json"
|
|
1004
|
+
];
|
|
1005
|
+
if (opts.timeout_ms !== undefined)
|
|
1006
|
+
args.push("--timeout-ms", String(opts.timeout_ms));
|
|
1007
|
+
// Slice 2: bind a runtime-named subtest's mutant failure to the exact assertion line.
|
|
1008
|
+
if (opts.go_assertion_line !== undefined)
|
|
1009
|
+
args.push("--go-assertion-line", String(opts.go_assertion_line));
|
|
1010
|
+
const run = (deps.dynamicProofRunner ?? defaultDynamicProofRunner)(args, {
|
|
1011
|
+
cwd: goRoot,
|
|
1012
|
+
scriptPath: dynamicProofSpikePathFor("go")
|
|
1013
|
+
});
|
|
1014
|
+
oracle = mapGoOracle(parseDynamicProofJson(run.stdout, run.stderr));
|
|
1015
|
+
testRel = opts.test_run;
|
|
1016
|
+
replacementMode = GO_SENTINEL_LABEL;
|
|
1017
|
+
runner = "go";
|
|
1018
|
+
}
|
|
1019
|
+
else if (language === "java") {
|
|
1020
|
+
// Java selects the target test by class#method (`mvn test -Dtest=Class#method`),
|
|
1021
|
+
// derives its own type-compatible sentinel from the return type, and always uses
|
|
1022
|
+
// Surefire. The single `test_run` field carries `Class#method` (FQCN or simple
|
|
1023
|
+
// class both accepted by the spike). No --replacement/--runner/--vitest-config/
|
|
1024
|
+
// --jest-config/--test-env/--link-node-modules apply.
|
|
1025
|
+
if (!opts.test_run)
|
|
1026
|
+
throw new Error("prove requires --test-run 'TestClass#testMethod' for Java targets.");
|
|
1027
|
+
const hash = opts.test_run.lastIndexOf("#");
|
|
1028
|
+
if (hash <= 0 || hash === opts.test_run.length - 1) {
|
|
1029
|
+
throw new Error("prove --test-run must be 'TestClass#testMethod' for Java targets, e.g. 'CalculatorTest#addsTwoNumbers'.");
|
|
1030
|
+
}
|
|
1031
|
+
const testClass = opts.test_run.slice(0, hash);
|
|
1032
|
+
const testMethod = opts.test_run.slice(hash + 1);
|
|
1033
|
+
const javaRoot = javaModuleRoot(sourceRoot, targetRel, root);
|
|
1034
|
+
if (javaRoot !== resolve(sourceRoot)) {
|
|
1035
|
+
proofModuleRoot = javaRoot;
|
|
1036
|
+
reportProgress(`proof scope: Java module root ${javaRoot} (above the analyzed path)`);
|
|
1037
|
+
}
|
|
1038
|
+
const args = [
|
|
1039
|
+
"--root",
|
|
1040
|
+
javaRoot,
|
|
1041
|
+
"--test-class",
|
|
1042
|
+
testClass,
|
|
1043
|
+
"--test-method",
|
|
1044
|
+
testMethod,
|
|
1045
|
+
"--target",
|
|
1046
|
+
relative(javaRoot, resolve(sourceRoot, targetRel)).split(sep).join("/"),
|
|
1047
|
+
"--method",
|
|
1048
|
+
method,
|
|
1049
|
+
"--json"
|
|
1050
|
+
];
|
|
1051
|
+
if (opts.timeout_ms !== undefined)
|
|
1052
|
+
args.push("--timeout-ms", String(opts.timeout_ms));
|
|
1053
|
+
const run = (deps.dynamicProofRunner ?? defaultDynamicProofRunner)(args, {
|
|
1054
|
+
cwd: javaRoot,
|
|
1055
|
+
scriptPath: dynamicProofSpikePathFor("java")
|
|
1056
|
+
});
|
|
1057
|
+
oracle = mapJavaOracle(parseDynamicProofJson(run.stdout, run.stderr));
|
|
1058
|
+
testRel = opts.test_run;
|
|
1059
|
+
replacementMode = JAVA_SENTINEL_LABEL;
|
|
1060
|
+
runner = "junit";
|
|
1061
|
+
}
|
|
1062
|
+
else if (language === "python") {
|
|
1063
|
+
if (!opts.test_path)
|
|
1064
|
+
throw new Error("prove requires --test <path>.");
|
|
1065
|
+
if (opts.replacement === undefined)
|
|
1066
|
+
throw new Error("prove requires --replacement <sentinel>.");
|
|
1067
|
+
testRel = toWorkspaceRel(sourceRoot, resolveContained(sourceRoot, opts.test_path));
|
|
1068
|
+
replacementMode = opts.replacement_mode ?? "return-json";
|
|
1069
|
+
runner = opts.runner ?? "auto";
|
|
1070
|
+
if (replacementMode !== "return-json") {
|
|
1071
|
+
throw new Error("prove --replacement-mode promise-json is not supported for Python targets.");
|
|
1072
|
+
}
|
|
1073
|
+
if (runner !== "auto" && runner !== "pytest") {
|
|
1074
|
+
throw new Error("prove Python targets require --runner auto or --runner pytest.");
|
|
1075
|
+
}
|
|
1076
|
+
if ((opts.test_env?.length ?? 0) > 0) {
|
|
1077
|
+
throw new Error("prove --test-env is not supported for Python targets yet.");
|
|
1078
|
+
}
|
|
1079
|
+
// G2: narrow the copied sandbox to the owning Python project when both the
|
|
1080
|
+
// target and the test live inside it (bounded copy — no full-repo OOM).
|
|
1081
|
+
const pyRoot = pythonModuleRoot(sourceRoot, targetRel, testRel);
|
|
1082
|
+
if (pyRoot !== resolve(sourceRoot)) {
|
|
1083
|
+
proofModuleRoot = pyRoot;
|
|
1084
|
+
reportProgress(`proof scope: Python project root ${pyRoot} (narrowed from the analyzed path)`);
|
|
1085
|
+
}
|
|
1086
|
+
const args = [
|
|
1087
|
+
"--root",
|
|
1088
|
+
pyRoot,
|
|
1089
|
+
"--test",
|
|
1090
|
+
relative(pyRoot, resolve(sourceRoot, testRel)).split(sep).join("/"),
|
|
1091
|
+
"--target",
|
|
1092
|
+
relative(pyRoot, resolve(sourceRoot, targetRel)).split(sep).join("/"),
|
|
1093
|
+
"--func",
|
|
1094
|
+
method,
|
|
1095
|
+
"--mode",
|
|
1096
|
+
"sentinel",
|
|
1097
|
+
"--json"
|
|
1098
|
+
];
|
|
1099
|
+
if (opts.timeout_ms !== undefined)
|
|
1100
|
+
args.push("--timeout-ms", String(opts.timeout_ms));
|
|
1101
|
+
const run = (deps.dynamicProofRunner ?? defaultDynamicProofRunner)(args, {
|
|
1102
|
+
cwd: pyRoot,
|
|
1103
|
+
scriptPath: dynamicProofSpikePathFor("python")
|
|
1104
|
+
});
|
|
1105
|
+
oracle = parseDynamicProofJson(run.stdout, run.stderr);
|
|
1106
|
+
}
|
|
1107
|
+
else {
|
|
1108
|
+
if (!opts.test_path)
|
|
1109
|
+
throw new Error("prove requires --test <path>.");
|
|
1110
|
+
if (opts.replacement === undefined)
|
|
1111
|
+
throw new Error("prove requires --replacement <sentinel>.");
|
|
1112
|
+
testRel = toWorkspaceRel(sourceRoot, resolveContained(sourceRoot, opts.test_path));
|
|
1113
|
+
replacementMode = opts.replacement_mode ?? "return-json";
|
|
1114
|
+
runner = opts.runner ?? "auto";
|
|
1115
|
+
if (replacementMode !== "return-json" && replacementMode !== "promise-json") {
|
|
1116
|
+
throw new Error("prove --replacement-mode must be one of: return-json, promise-json.");
|
|
1117
|
+
}
|
|
1118
|
+
if (runner === "pytest") {
|
|
1119
|
+
throw new Error("prove --runner pytest requires a Python target.");
|
|
1120
|
+
}
|
|
1121
|
+
if (runner !== "auto" && runner !== "vitest" && runner !== "jest" && runner !== "mocha") {
|
|
1122
|
+
throw new Error("prove --runner must be one of: auto, vitest, jest, mocha.");
|
|
1123
|
+
}
|
|
1124
|
+
const args = [
|
|
1125
|
+
"--root",
|
|
1126
|
+
sourceRoot,
|
|
1127
|
+
"--test",
|
|
1128
|
+
testRel,
|
|
1129
|
+
"--target",
|
|
1130
|
+
targetRel,
|
|
1131
|
+
"--method",
|
|
1132
|
+
method,
|
|
1133
|
+
"--replacement",
|
|
1134
|
+
opts.replacement,
|
|
1135
|
+
"--replacement-mode",
|
|
1136
|
+
replacementMode,
|
|
1137
|
+
"--runner",
|
|
1138
|
+
runner,
|
|
1139
|
+
"--json"
|
|
1140
|
+
];
|
|
1141
|
+
if (opts.timeout_ms !== undefined)
|
|
1142
|
+
args.push("--timeout-ms", String(opts.timeout_ms));
|
|
1143
|
+
if (opts.link_node_modules)
|
|
1144
|
+
args.push("--link-node-modules");
|
|
1145
|
+
if (opts.vitest_config)
|
|
1146
|
+
args.push("--vitest-config", toWorkspaceRel(sourceRoot, resolveContained(sourceRoot, opts.vitest_config)));
|
|
1147
|
+
if (opts.jest_config)
|
|
1148
|
+
args.push("--jest-config", toWorkspaceRel(sourceRoot, resolveContained(sourceRoot, opts.jest_config)));
|
|
1149
|
+
for (const entry of opts.test_env ?? [])
|
|
1150
|
+
args.push("--test-env", entry);
|
|
1151
|
+
const run = (deps.dynamicProofRunner ?? defaultDynamicProofRunner)(args, { cwd: sourceRoot });
|
|
1152
|
+
oracle = parseDynamicProofJson(run.stdout, run.stderr);
|
|
1153
|
+
}
|
|
801
1154
|
const closed = dynamicProofSucceeded(oracle);
|
|
802
1155
|
const baselineGreen = oracle.baseline?.exitCode === 0 && oracle.baseline?.timedOut !== true;
|
|
803
1156
|
const mutantFailedAssertion = oracle.mutant?.assertionFailure === true && oracle.mutant?.timedOut !== true;
|
|
@@ -830,6 +1183,7 @@ export function opDynamicProof(root, opts, deps = defaultDeps()) {
|
|
|
830
1183
|
});
|
|
831
1184
|
return {
|
|
832
1185
|
...result,
|
|
1186
|
+
...(proofModuleRoot ? { module_root: proofModuleRoot } : {}),
|
|
833
1187
|
oracle: {
|
|
834
1188
|
status: oracle.status,
|
|
835
1189
|
proven: oracle.proven,
|
|
@@ -927,8 +1281,8 @@ export function opProveLoop(root, opts, deps = defaultDeps()) {
|
|
|
927
1281
|
throw new Error("prove requires --target-symbol sym:<file>#<Symbol>, or a target id that resolves to exactly one CodeSymbol.");
|
|
928
1282
|
}
|
|
929
1283
|
const language = targetLanguage(target);
|
|
930
|
-
if (language !== "typescript") {
|
|
931
|
-
throw new Error(`prove currently supports JavaScript/TypeScript targets only; found ${language}.`);
|
|
1284
|
+
if (language !== "typescript" && language !== "go" && language !== "java" && language !== "python") {
|
|
1285
|
+
throw new Error(`prove currently supports JavaScript/TypeScript, Go, Java, and Python targets only; found ${language}.`);
|
|
932
1286
|
}
|
|
933
1287
|
const setupCommands = (opts.setup_commands ?? []).map((command, index) => validateSetupCommand(command, index));
|
|
934
1288
|
const sourceRoot = resolve(opts.source ?? root);
|
|
@@ -947,6 +1301,8 @@ export function opProveLoop(root, opts, deps = defaultDeps()) {
|
|
|
947
1301
|
target_id: opts.target_id,
|
|
948
1302
|
source: opts.source,
|
|
949
1303
|
test_path: opts.test_path,
|
|
1304
|
+
test_run: opts.test_run,
|
|
1305
|
+
go_assertion_line: opts.go_assertion_line,
|
|
950
1306
|
target_path: opts.target_path,
|
|
951
1307
|
method: opts.method,
|
|
952
1308
|
replacement: opts.replacement,
|
|
@@ -1183,6 +1539,7 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1183
1539
|
noAuto: opts.noAuto,
|
|
1184
1540
|
provider: providerOpts.provider,
|
|
1185
1541
|
model: providerOpts.model,
|
|
1542
|
+
prompt_version: opts.promptVersion,
|
|
1186
1543
|
changedFiles: autoProveChangedScope(loadGraph(workspacePaths(root).graphPath), changed, opts.baseRef)
|
|
1187
1544
|
}, { ...providerDeps, proveLoop: opProveLoop });
|
|
1188
1545
|
for (const skip of autoProveResult.skipped)
|
|
@@ -1203,6 +1560,19 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1203
1560
|
};
|
|
1204
1561
|
warnings.push(`auto-prove skipped: ${reason}`);
|
|
1205
1562
|
}
|
|
1563
|
+
// G1: persist the distilled, already-redacted attempt classifications so
|
|
1564
|
+
// `opro doctor --proof` and standalone report regens can explain blockers
|
|
1565
|
+
// after this process exits. Sidecar only — never read by the oracle, RTM,
|
|
1566
|
+
// or ledger paths; a write failure must never fail start.
|
|
1567
|
+
try {
|
|
1568
|
+
writeProofAttempts(root, distillProofAttempts(autoProveResult, {
|
|
1569
|
+
generatedAt: deps.clock(),
|
|
1570
|
+
graph: loadGraph(workspacePaths(root).graphPath)
|
|
1571
|
+
}));
|
|
1572
|
+
}
|
|
1573
|
+
catch (error) {
|
|
1574
|
+
warnings.push(`proof-attempts sidecar not written: ${error instanceof Error ? error.message : String(error)}`);
|
|
1575
|
+
}
|
|
1206
1576
|
let coverageReport;
|
|
1207
1577
|
try {
|
|
1208
1578
|
reportProgress("artifacts: writing coverage report", { current: 6, total: 8 });
|
|
@@ -1274,10 +1644,10 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1274
1644
|
if (rtm.rows.length < rtm.summary.total)
|
|
1275
1645
|
nextActions.push("For full machine-readable RTM, run `opro rtm --format json --out .orangepro/rtm-full.json`; avoid opening full Markdown on very large repos.");
|
|
1276
1646
|
if (changed.status === "ok" && changed.affected_behaviors.length > 0) {
|
|
1277
|
-
nextActions.push(`In your coding agent, call orangepro_generate_tests with base_ref=${changed.base_ref}; write runnable tests, then follow each returned handoff:
|
|
1647
|
+
nextActions.push(`In your coding agent, call orangepro_generate_tests with base_ref=${changed.base_ref}; write runnable tests, then follow each returned handoff: call orangepro_prove with returned prove_run args when present for public Proven, otherwise record_run is static diagnostics only.`);
|
|
1278
1648
|
}
|
|
1279
1649
|
else if (gaps.gaps.length > 0) {
|
|
1280
|
-
nextActions.push(`In your coding agent, call orangepro_generate_tests for ${gaps.gaps[0].external_id}; write runnable tests, then follow each returned handoff:
|
|
1650
|
+
nextActions.push(`In your coding agent, call orangepro_generate_tests for ${gaps.gaps[0].external_id}; write runnable tests, then follow each returned handoff: call orangepro_prove with returned prove_run args when present for public Proven, otherwise record_run is static diagnostics only.`);
|
|
1281
1651
|
}
|
|
1282
1652
|
else {
|
|
1283
1653
|
nextActions.push("No deterministic gap target was found; inspect the RTM and graph before generating tests.");
|
|
@@ -1365,6 +1735,16 @@ export async function opGenerate(root, opts = {}, deps = defaultDeps()) {
|
|
|
1365
1735
|
updated_at: deps.clock()
|
|
1366
1736
|
};
|
|
1367
1737
|
saveGraph(paths.graphPath, next);
|
|
1738
|
+
// Keep the behavior report in sync with the freshly persisted generated
|
|
1739
|
+
// tests — analyze/start would REBUILD the graph and drop them, so this is
|
|
1740
|
+
// the only command that can surface them. Display-only refresh; a render
|
|
1741
|
+
// failure must never fail generate.
|
|
1742
|
+
try {
|
|
1743
|
+
opBehaviorCoverageHtml(root, `${WORKSPACE_DIR}/behavior-coverage.html`);
|
|
1744
|
+
}
|
|
1745
|
+
catch {
|
|
1746
|
+
/* best-effort report refresh */
|
|
1747
|
+
}
|
|
1368
1748
|
}
|
|
1369
1749
|
// Validate each test's grounding citations against the graph the tests cite.
|
|
1370
1750
|
// This is the keyless grounding contract: provenance must be verifiable, and a
|
|
@@ -1553,11 +1933,33 @@ export function opGraphHtml(root, outputPath = "orangepro-graph.html") {
|
|
|
1553
1933
|
/** Write the self-contained offline behavior-coverage view (deterministic, metadata only). */
|
|
1554
1934
|
export function opBehaviorCoverageHtml(root, outputPath = "orangepro-behavior-coverage.html", dynamicProof) {
|
|
1555
1935
|
const graph = loadGraph(workspacePaths(root).graphPath);
|
|
1556
|
-
|
|
1936
|
+
// Standalone regens have no this-run outcome: fall back to the persisted
|
|
1937
|
+
// proof-attempts sidecar ONLY when it anchors to the current graph+commit
|
|
1938
|
+
// (stale evidence is dropped — fail closed; display copy only, no tier math).
|
|
1939
|
+
const dyn = dynamicProof ?? sidecarDynamicProof(root, graph);
|
|
1940
|
+
const html = renderBehaviorReport(buildBehaviorReportData(graph, loadLedger(root), { repoRoot: root, dynamicProof: dyn }));
|
|
1557
1941
|
const htmlPath = resolve(root, outputPath);
|
|
1558
1942
|
writeFileSync(htmlPath, html, "utf8");
|
|
1559
1943
|
return { behavior_coverage_path: htmlPath };
|
|
1560
1944
|
}
|
|
1945
|
+
/** Fresh-only sidecar view for report regens; unreadable or stale ⇒ undefined. */
|
|
1946
|
+
function sidecarDynamicProof(root, graph) {
|
|
1947
|
+
try {
|
|
1948
|
+
const attempts = loadProofAttempts(root);
|
|
1949
|
+
if (!attempts || !proofAttemptsFresh(attempts, graph))
|
|
1950
|
+
return undefined;
|
|
1951
|
+
return {
|
|
1952
|
+
attempted: attempts.attempted,
|
|
1953
|
+
proven: attempts.proven,
|
|
1954
|
+
needsSetup: attempts.attempts
|
|
1955
|
+
.filter((a) => a.classification === "needs_setup")
|
|
1956
|
+
.map((a) => ({ category: a.category, reason: a.reason }))
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
catch {
|
|
1960
|
+
return undefined;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1561
1963
|
/** Phase 5.2 — write the human-readable COVERAGE_REPORT.md (3-file contract). */
|
|
1562
1964
|
export function opCoverageReport(root, outputPath = "COVERAGE_REPORT.md") {
|
|
1563
1965
|
const graph = loadGraph(workspacePaths(root).graphPath);
|
|
@@ -1663,7 +2065,7 @@ const NO_CODE_CHANGES_GUIDANCE = "The diff vs the base ref only touched docs (.m
|
|
|
1663
2065
|
* `base_ref` defaults to `main`. The diff runs from `git merge-base <base> HEAD`
|
|
1664
2066
|
* to the working tree: the branch's own commits AND uncommitted edits, never
|
|
1665
2067
|
* upstream churn on the base (falls back to the base tip when no merge-base
|
|
1666
|
-
* exists).
|
|
2068
|
+
* exists). This diff resolver is read-only: it does not write repo files or upload source.
|
|
1667
2069
|
*/
|
|
1668
2070
|
export function resolveDiffContext(graph, baseRefInput) {
|
|
1669
2071
|
const scanRoot = graph.workspace.root;
|
|
@@ -3,8 +3,8 @@ import { resolveCoverage } from "../score/coverage.js";
|
|
|
3
3
|
import { rankRiskGaps } from "../score/risk.js";
|
|
4
4
|
import { buildRtm } from "../rtm.js";
|
|
5
5
|
/**
|
|
6
|
-
* Phase 5.2 — `COVERAGE_REPORT.md`, the human-readable
|
|
7
|
-
*
|
|
6
|
+
* Phase 5.2 — `COVERAGE_REPORT.md`, the human-readable summary beside
|
|
7
|
+
* `behavior-coverage.html`, `graph.json`, and optional graph export. It states,
|
|
8
8
|
* auditably and in one place:
|
|
9
9
|
* - dynamic Proven % over the DENOMINATOR behaviors;
|
|
10
10
|
* - what the denominator is made of (Gate 3 composition line);
|
|
@@ -36,7 +36,7 @@ export function renderCoverageReport(graph, ledger = emptyLedger()) {
|
|
|
36
36
|
lines.push(`_Graph schema: ${graph.schema_version}_`);
|
|
37
37
|
lines.push(`_Generated: ${graph.updated_at || graph.created_at || "(unknown)"}_`);
|
|
38
38
|
lines.push("");
|
|
39
|
-
// ---- Public Proven + denominator: ONE atomic pair (shared with
|
|
39
|
+
// ---- Public Proven + denominator: ONE atomic pair (shared with report views) ----
|
|
40
40
|
const { coverage: staticCov, denominator: comp } = resolveCoverage(graph);
|
|
41
41
|
const rtm = buildRtm(graph, ledger);
|
|
42
42
|
const s = rtm.summary;
|