@orangepro/orangepro-mcp 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +195 -251
- package/dist/local/analyze/analyzer.js +46 -6
- package/dist/local/analyze/treeSitter/engine.js +172 -17
- package/dist/local/autoProve.js +402 -28
- 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 +396 -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 +1 -1
- package/docs/agents/opencode.md +1 -1
- package/docs/agents/vscode.md +1 -1
- 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 +244 -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,67 @@ 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
|
+
function goModuleRoot(sourceRoot, targetRel) {
|
|
68
|
+
let dir = dirname(resolve(sourceRoot, targetRel));
|
|
69
|
+
const stop = resolve(sourceRoot);
|
|
70
|
+
for (;;) {
|
|
71
|
+
if (existsSync(join(dir, "go.mod")))
|
|
72
|
+
return dir;
|
|
73
|
+
if (dir === stop)
|
|
74
|
+
break;
|
|
75
|
+
const parent = dirname(dir);
|
|
76
|
+
if (parent === dir)
|
|
77
|
+
break;
|
|
78
|
+
dir = parent;
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`No go.mod found for Go target ${targetRel} under ${sourceRoot}.`);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve the Java MODULE root: the nearest `pom.xml` (or `build.gradle` /
|
|
84
|
+
* `build.gradle.kts`) ancestor of the target file, searched from the target's
|
|
85
|
+
* directory up to (and including) sourceRoot. Mirrors goModuleRoot — for a
|
|
86
|
+
* single-module project this is sourceRoot itself. Confined to sourceRoot so the
|
|
87
|
+
* spike never sandboxes a directory outside the trusted checkout.
|
|
88
|
+
* ponytail: nearest-ancestor build file; multi-module reactor edge cases resolve to J-INT-3.
|
|
89
|
+
*/
|
|
90
|
+
function javaModuleRoot(sourceRoot, targetRel) {
|
|
91
|
+
let dir = dirname(resolve(sourceRoot, targetRel));
|
|
92
|
+
const stop = resolve(sourceRoot);
|
|
93
|
+
for (;;) {
|
|
94
|
+
if (existsSync(join(dir, "pom.xml")) || existsSync(join(dir, "build.gradle")) || existsSync(join(dir, "build.gradle.kts"))) {
|
|
95
|
+
return dir;
|
|
96
|
+
}
|
|
97
|
+
if (dir === stop)
|
|
98
|
+
break;
|
|
99
|
+
const parent = dirname(dir);
|
|
100
|
+
if (parent === dir)
|
|
101
|
+
break;
|
|
102
|
+
dir = parent;
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`No pom.xml or build.gradle found for Java target ${targetRel} under ${sourceRoot}.`);
|
|
105
|
+
}
|
|
106
|
+
/** Route to the per-language spike. TS/JS keeps the original path; native profiles use their own mechanisms. */
|
|
107
|
+
function dynamicProofSpikePathFor(language) {
|
|
108
|
+
if (language === "go" || language === "java" || language === "python") {
|
|
109
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
110
|
+
const script = language === "go"
|
|
111
|
+
? "go-dynamic-proof-spike.mjs"
|
|
112
|
+
: language === "java"
|
|
113
|
+
? "java-dynamic-proof-spike.mjs"
|
|
114
|
+
: "python-dynamic-proof-spike.mjs";
|
|
115
|
+
return resolve(here, "..", "..", "scripts", "spikes", script);
|
|
116
|
+
}
|
|
117
|
+
return dynamicProofSpikePath();
|
|
118
|
+
}
|
|
59
119
|
function defaultDynamicProofRunner(args, opts = {}) {
|
|
60
|
-
const script = dynamicProofSpikePath();
|
|
120
|
+
const script = opts.scriptPath ?? dynamicProofSpikePath();
|
|
61
121
|
if (!existsSync(script)) {
|
|
62
122
|
throw new Error(`Dynamic proof spike runner not found at ${script}. Run this command from an OrangePro source checkout with scripts/spikes available.`);
|
|
63
123
|
}
|
|
@@ -83,6 +143,107 @@ function parseDynamicProofJson(stdout, stderr) {
|
|
|
83
143
|
throw new Error(`Dynamic proof runner did not return JSON.${detail ? ` stderr/stdout: ${detail}` : ""}`);
|
|
84
144
|
}
|
|
85
145
|
}
|
|
146
|
+
/** The label recorded as the cert `sentinel` for a Go proof (Go derives a zero-value return). */
|
|
147
|
+
const GO_SENTINEL_LABEL = "go-zero-return";
|
|
148
|
+
/**
|
|
149
|
+
* Map the Go spike's JSON onto the SAME language-agnostic DynamicProofOracleSummary the
|
|
150
|
+
* unchanged `dynamicProofSucceeded` and cert block read. This is the ONE trust-adjacent
|
|
151
|
+
* seam, so mapping is STRICT: `assertionFailure` is true ONLY when the Go spike genuinely
|
|
152
|
+
* returned `mutant.trustedAssertion === true`. A skipped/refused mutation, a survived
|
|
153
|
+
* mutation, or any absent field maps to `assertionFailure = false` — never a false Proven.
|
|
154
|
+
*/
|
|
155
|
+
function mapGoOracle(go) {
|
|
156
|
+
const mutant = go.mutant;
|
|
157
|
+
// A refused/skipped mutation co-occurs with status "unrunnable"; it must never close.
|
|
158
|
+
const skipped = Boolean(mutant && "skipped" in mutant && mutant.skipped === true);
|
|
159
|
+
const runMutant = !skipped && mutant ? mutant : undefined;
|
|
160
|
+
return {
|
|
161
|
+
status: go.status,
|
|
162
|
+
proven: go.proven === true,
|
|
163
|
+
reason: go.reason,
|
|
164
|
+
runner: "go",
|
|
165
|
+
replacementMode: GO_SENTINEL_LABEL,
|
|
166
|
+
test: go.testRun,
|
|
167
|
+
target: go.target,
|
|
168
|
+
method: go.func,
|
|
169
|
+
baseline: go.baseline
|
|
170
|
+
? { exitCode: go.baseline.exitCode, timedOut: go.baseline.timedOut, failureSummary: go.baseline.failureSummary ?? null }
|
|
171
|
+
: undefined,
|
|
172
|
+
// STRICT: trustedAssertion must be exactly true AND the mutant must carry a NUMERIC non-zero
|
|
173
|
+
// exit code. A missing/undefined/zero exitCode ⇒ assertionFailure=false ⇒ non-close, so the
|
|
174
|
+
// shared gate (which asserts `exitCode !== 0`, where `undefined !== 0` is truthy) can never
|
|
175
|
+
// false-close on an absent exit code. skipped/absent/false ⇒ false ⇒ non-close.
|
|
176
|
+
mutant: runMutant
|
|
177
|
+
? {
|
|
178
|
+
exitCode: runMutant.exitCode,
|
|
179
|
+
timedOut: runMutant.timedOut,
|
|
180
|
+
assertionFailure: runMutant.trustedAssertion === true &&
|
|
181
|
+
typeof runMutant.exitCode === "number" &&
|
|
182
|
+
runMutant.exitCode !== 0
|
|
183
|
+
}
|
|
184
|
+
: { assertionFailure: false },
|
|
185
|
+
medianProofMs: go.medianProofMs
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** Test-only handle on the Go→oracle mapper (the one trust-adjacent seam). */
|
|
189
|
+
export function __mapGoOracleForTest(go) {
|
|
190
|
+
return mapGoOracle(go);
|
|
191
|
+
}
|
|
192
|
+
/** The label recorded as the cert `sentinel` for a Java proof (Java derives a typed sentinel from the return type). */
|
|
193
|
+
const JAVA_SENTINEL_LABEL = "java-typed-sentinel";
|
|
194
|
+
/**
|
|
195
|
+
* Map the Java spike's JSON onto the SAME language-agnostic DynamicProofOracleSummary the
|
|
196
|
+
* unchanged `dynamicProofSucceeded` and cert block read. This is the ONE trust-adjacent
|
|
197
|
+
* seam, so mapping is STRICT: `assertionFailure` is true ONLY when the Java spike genuinely
|
|
198
|
+
* signalled a trusted JUnit assertion failure — the mutant ran (not skipped), the SAME
|
|
199
|
+
* target test FAILED, AND surefire classified it as a trusted assertion (`isAssertion`).
|
|
200
|
+
* A skipped/refused mutation, a survived (associated_survived) mutation, a compile failure,
|
|
201
|
+
* a non-assertion error, or any absent field maps to `assertionFailure = false` — never a
|
|
202
|
+
* false Proven. The spike itself already gates `status: "proven"` on all of this; the mapper
|
|
203
|
+
* re-derives the assertion signal independently so the cert's `mutant_failed_assertion` never
|
|
204
|
+
* trusts the spike's verdict alone.
|
|
205
|
+
*/
|
|
206
|
+
function mapJavaOracle(java) {
|
|
207
|
+
const mutant = java.mutant;
|
|
208
|
+
// A refused/skipped mutation co-occurs with status "unrunnable"; it must never close.
|
|
209
|
+
const skipped = Boolean(mutant && "skipped" in mutant && mutant.skipped === true);
|
|
210
|
+
const runMutant = !skipped && mutant
|
|
211
|
+
? mutant
|
|
212
|
+
: undefined;
|
|
213
|
+
return {
|
|
214
|
+
status: java.status,
|
|
215
|
+
proven: java.proven === true,
|
|
216
|
+
reason: java.reason,
|
|
217
|
+
runner: "junit",
|
|
218
|
+
replacementMode: JAVA_SENTINEL_LABEL,
|
|
219
|
+
test: java.testClass && java.testMethod ? `${java.testClass}#${java.testMethod}` : java.testMethod,
|
|
220
|
+
target: java.target,
|
|
221
|
+
method: java.method,
|
|
222
|
+
baseline: java.baseline
|
|
223
|
+
? { exitCode: java.baseline.exitCode, timedOut: java.baseline.timedOut, failureSummary: java.baseline.failureSummary ?? null }
|
|
224
|
+
: undefined,
|
|
225
|
+
// STRICT: the mutant must have RUN, FAILED the same target test, the failure must be a trusted
|
|
226
|
+
// JUnit assertion, AND the mutant must carry a NUMERIC non-zero exit code. A missing/undefined/
|
|
227
|
+
// zero exitCode ⇒ assertionFailure=false ⇒ non-close, so the shared gate (which asserts
|
|
228
|
+
// `exitCode !== 0`, where `undefined !== 0` is truthy) can never false-close on an absent exit
|
|
229
|
+
// code. skipped/survived/compile-fail/non-assertion/absent ⇒ false ⇒ non-close.
|
|
230
|
+
mutant: runMutant
|
|
231
|
+
? {
|
|
232
|
+
exitCode: runMutant.exitCode,
|
|
233
|
+
timedOut: runMutant.timedOut,
|
|
234
|
+
assertionFailure: runMutant.targetTestFailed === true &&
|
|
235
|
+
runMutant.isAssertion === true &&
|
|
236
|
+
typeof runMutant.exitCode === "number" &&
|
|
237
|
+
runMutant.exitCode !== 0
|
|
238
|
+
}
|
|
239
|
+
: { assertionFailure: false },
|
|
240
|
+
medianProofMs: java.medianProofMs
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/** Test-only handle on the Java→oracle mapper (the one trust-adjacent seam). */
|
|
244
|
+
export function __mapJavaOracleForTest(java) {
|
|
245
|
+
return mapJavaOracle(java);
|
|
246
|
+
}
|
|
86
247
|
function dynamicProofSucceeded(oracle) {
|
|
87
248
|
return (oracle.status === "proven" &&
|
|
88
249
|
oracle.proven === true &&
|
|
@@ -617,6 +778,17 @@ export function opDoctor(root) {
|
|
|
617
778
|
const graph = loadGraph(workspacePaths(root).graphPath);
|
|
618
779
|
return doctorGraph(graph, scoreGraph(graph));
|
|
619
780
|
}
|
|
781
|
+
/**
|
|
782
|
+
* G1 — proof-focused doctor: why are top targets not Dynamically Proven?
|
|
783
|
+
* Read-only: consumes the canonical RTM judgment (buildRtm) plus the last run's
|
|
784
|
+
* redacted proof-attempts sidecar. Mints nothing, mutates no ledger, writes no
|
|
785
|
+
* files — under staleness or ambiguity it fails closed to "re-run".
|
|
786
|
+
*/
|
|
787
|
+
export function opProofDoctor(root) {
|
|
788
|
+
const graph = loadGraph(workspacePaths(root).graphPath);
|
|
789
|
+
const rtm = buildRtm(graph, loadLedger(root));
|
|
790
|
+
return buildProofDoctor(graph, rtm, loadProofAttempts(root));
|
|
791
|
+
}
|
|
620
792
|
export function opGaps(root, opts = {}) {
|
|
621
793
|
const graph = loadGraph(workspacePaths(root).graphPath);
|
|
622
794
|
const gaps = findGaps(graph, opts);
|
|
@@ -745,59 +917,183 @@ export function opDynamicProof(root, opts, deps = defaultDeps()) {
|
|
|
745
917
|
throw new Error("prove requires --target-symbol sym:<file>#<Symbol>, or a target id that resolves to exactly one CodeSymbol.");
|
|
746
918
|
}
|
|
747
919
|
const language = targetLanguage(target);
|
|
748
|
-
if (language !== "typescript") {
|
|
749
|
-
throw new Error(`prove currently supports JavaScript/TypeScript targets only; found ${language}.`);
|
|
920
|
+
if (language !== "typescript" && language !== "go" && language !== "java" && language !== "python") {
|
|
921
|
+
throw new Error(`prove currently supports JavaScript/TypeScript, Go, Java, and Python targets only; found ${language}.`);
|
|
750
922
|
}
|
|
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
923
|
const sourceRoot = resolve(opts.source ?? root);
|
|
756
924
|
assertProofSourceMatchesGraph(sourceRoot, graph);
|
|
757
925
|
const symbolTarget = symbolTargetParts(target);
|
|
758
926
|
const providedTargetRel = opts.target_path ? toWorkspaceRel(sourceRoot, resolveContained(sourceRoot, opts.target_path)) : undefined;
|
|
759
927
|
assertProofTargetMatchesSymbol({ ...opts, target_path: providedTargetRel }, symbolTarget);
|
|
760
|
-
const testRel = toWorkspaceRel(sourceRoot, resolveContained(sourceRoot, opts.test_path));
|
|
761
928
|
const targetRel = symbolTarget.file;
|
|
762
929
|
const method = symbolTarget.method;
|
|
763
930
|
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
|
-
|
|
931
|
+
// Per-language routing produces the spike args + a mapped oracle in the SAME
|
|
932
|
+
// DynamicProofOracleSummary shape. Everything from `closed` onward (the trust gate,
|
|
933
|
+
// the cert, appendLedgerRecord, the return summary) is language-agnostic and unchanged.
|
|
934
|
+
let oracle;
|
|
935
|
+
let testRel;
|
|
936
|
+
let replacementMode;
|
|
937
|
+
let runner;
|
|
938
|
+
if (language === "go") {
|
|
939
|
+
// Go selects the target test by NAME (`go test -run ^TestX$`), derives its own
|
|
940
|
+
// zero-value sentinel, and always uses `go test`. No --replacement/--runner/
|
|
941
|
+
// --vitest-config/--jest-config/--test-env/--link-node-modules apply.
|
|
942
|
+
if (!opts.test_run)
|
|
943
|
+
throw new Error("prove requires --test-run '^TestName$' for Go targets.");
|
|
944
|
+
if (!/^\^.+\$$/.test(opts.test_run)) {
|
|
945
|
+
throw new Error("prove --test-run must be a fully-anchored test name, e.g. '^TestName$'.");
|
|
946
|
+
}
|
|
947
|
+
const goRoot = goModuleRoot(sourceRoot, targetRel);
|
|
948
|
+
const args = [
|
|
949
|
+
"--root",
|
|
950
|
+
goRoot,
|
|
951
|
+
"--target",
|
|
952
|
+
relative(goRoot, resolve(sourceRoot, targetRel)).split(sep).join("/"),
|
|
953
|
+
"--func",
|
|
954
|
+
method,
|
|
955
|
+
"--test-run",
|
|
956
|
+
opts.test_run,
|
|
957
|
+
"--json"
|
|
958
|
+
];
|
|
959
|
+
if (opts.timeout_ms !== undefined)
|
|
960
|
+
args.push("--timeout-ms", String(opts.timeout_ms));
|
|
961
|
+
// Slice 2: bind a runtime-named subtest's mutant failure to the exact assertion line.
|
|
962
|
+
if (opts.go_assertion_line !== undefined)
|
|
963
|
+
args.push("--go-assertion-line", String(opts.go_assertion_line));
|
|
964
|
+
const run = (deps.dynamicProofRunner ?? defaultDynamicProofRunner)(args, {
|
|
965
|
+
cwd: goRoot,
|
|
966
|
+
scriptPath: dynamicProofSpikePathFor("go")
|
|
967
|
+
});
|
|
968
|
+
oracle = mapGoOracle(parseDynamicProofJson(run.stdout, run.stderr));
|
|
969
|
+
testRel = opts.test_run;
|
|
970
|
+
replacementMode = GO_SENTINEL_LABEL;
|
|
971
|
+
runner = "go";
|
|
972
|
+
}
|
|
973
|
+
else if (language === "java") {
|
|
974
|
+
// Java selects the target test by class#method (`mvn test -Dtest=Class#method`),
|
|
975
|
+
// derives its own type-compatible sentinel from the return type, and always uses
|
|
976
|
+
// Surefire. The single `test_run` field carries `Class#method` (FQCN or simple
|
|
977
|
+
// class both accepted by the spike). No --replacement/--runner/--vitest-config/
|
|
978
|
+
// --jest-config/--test-env/--link-node-modules apply.
|
|
979
|
+
if (!opts.test_run)
|
|
980
|
+
throw new Error("prove requires --test-run 'TestClass#testMethod' for Java targets.");
|
|
981
|
+
const hash = opts.test_run.lastIndexOf("#");
|
|
982
|
+
if (hash <= 0 || hash === opts.test_run.length - 1) {
|
|
983
|
+
throw new Error("prove --test-run must be 'TestClass#testMethod' for Java targets, e.g. 'CalculatorTest#addsTwoNumbers'.");
|
|
984
|
+
}
|
|
985
|
+
const testClass = opts.test_run.slice(0, hash);
|
|
986
|
+
const testMethod = opts.test_run.slice(hash + 1);
|
|
987
|
+
const javaRoot = javaModuleRoot(sourceRoot, targetRel);
|
|
988
|
+
const args = [
|
|
989
|
+
"--root",
|
|
990
|
+
javaRoot,
|
|
991
|
+
"--test-class",
|
|
992
|
+
testClass,
|
|
993
|
+
"--test-method",
|
|
994
|
+
testMethod,
|
|
995
|
+
"--target",
|
|
996
|
+
relative(javaRoot, resolve(sourceRoot, targetRel)).split(sep).join("/"),
|
|
997
|
+
"--method",
|
|
998
|
+
method,
|
|
999
|
+
"--json"
|
|
1000
|
+
];
|
|
1001
|
+
if (opts.timeout_ms !== undefined)
|
|
1002
|
+
args.push("--timeout-ms", String(opts.timeout_ms));
|
|
1003
|
+
const run = (deps.dynamicProofRunner ?? defaultDynamicProofRunner)(args, {
|
|
1004
|
+
cwd: javaRoot,
|
|
1005
|
+
scriptPath: dynamicProofSpikePathFor("java")
|
|
1006
|
+
});
|
|
1007
|
+
oracle = mapJavaOracle(parseDynamicProofJson(run.stdout, run.stderr));
|
|
1008
|
+
testRel = opts.test_run;
|
|
1009
|
+
replacementMode = JAVA_SENTINEL_LABEL;
|
|
1010
|
+
runner = "junit";
|
|
1011
|
+
}
|
|
1012
|
+
else if (language === "python") {
|
|
1013
|
+
if (!opts.test_path)
|
|
1014
|
+
throw new Error("prove requires --test <path>.");
|
|
1015
|
+
if (opts.replacement === undefined)
|
|
1016
|
+
throw new Error("prove requires --replacement <sentinel>.");
|
|
1017
|
+
testRel = toWorkspaceRel(sourceRoot, resolveContained(sourceRoot, opts.test_path));
|
|
1018
|
+
replacementMode = opts.replacement_mode ?? "return-json";
|
|
1019
|
+
runner = opts.runner ?? "auto";
|
|
1020
|
+
if (replacementMode !== "return-json") {
|
|
1021
|
+
throw new Error("prove --replacement-mode promise-json is not supported for Python targets.");
|
|
1022
|
+
}
|
|
1023
|
+
if (runner !== "auto" && runner !== "pytest") {
|
|
1024
|
+
throw new Error("prove Python targets require --runner auto or --runner pytest.");
|
|
1025
|
+
}
|
|
1026
|
+
if ((opts.test_env?.length ?? 0) > 0) {
|
|
1027
|
+
throw new Error("prove --test-env is not supported for Python targets yet.");
|
|
1028
|
+
}
|
|
1029
|
+
const args = [
|
|
1030
|
+
"--root",
|
|
1031
|
+
sourceRoot,
|
|
1032
|
+
"--test",
|
|
1033
|
+
testRel,
|
|
1034
|
+
"--target",
|
|
1035
|
+
targetRel,
|
|
1036
|
+
"--func",
|
|
1037
|
+
method,
|
|
1038
|
+
"--mode",
|
|
1039
|
+
"sentinel",
|
|
1040
|
+
"--json"
|
|
1041
|
+
];
|
|
1042
|
+
if (opts.timeout_ms !== undefined)
|
|
1043
|
+
args.push("--timeout-ms", String(opts.timeout_ms));
|
|
1044
|
+
const run = (deps.dynamicProofRunner ?? defaultDynamicProofRunner)(args, {
|
|
1045
|
+
cwd: sourceRoot,
|
|
1046
|
+
scriptPath: dynamicProofSpikePathFor("python")
|
|
1047
|
+
});
|
|
1048
|
+
oracle = parseDynamicProofJson(run.stdout, run.stderr);
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
if (!opts.test_path)
|
|
1052
|
+
throw new Error("prove requires --test <path>.");
|
|
1053
|
+
if (opts.replacement === undefined)
|
|
1054
|
+
throw new Error("prove requires --replacement <sentinel>.");
|
|
1055
|
+
testRel = toWorkspaceRel(sourceRoot, resolveContained(sourceRoot, opts.test_path));
|
|
1056
|
+
replacementMode = opts.replacement_mode ?? "return-json";
|
|
1057
|
+
runner = opts.runner ?? "auto";
|
|
1058
|
+
if (replacementMode !== "return-json" && replacementMode !== "promise-json") {
|
|
1059
|
+
throw new Error("prove --replacement-mode must be one of: return-json, promise-json.");
|
|
1060
|
+
}
|
|
1061
|
+
if (runner === "pytest") {
|
|
1062
|
+
throw new Error("prove --runner pytest requires a Python target.");
|
|
1063
|
+
}
|
|
1064
|
+
if (runner !== "auto" && runner !== "vitest" && runner !== "jest" && runner !== "mocha") {
|
|
1065
|
+
throw new Error("prove --runner must be one of: auto, vitest, jest, mocha.");
|
|
1066
|
+
}
|
|
1067
|
+
const args = [
|
|
1068
|
+
"--root",
|
|
1069
|
+
sourceRoot,
|
|
1070
|
+
"--test",
|
|
1071
|
+
testRel,
|
|
1072
|
+
"--target",
|
|
1073
|
+
targetRel,
|
|
1074
|
+
"--method",
|
|
1075
|
+
method,
|
|
1076
|
+
"--replacement",
|
|
1077
|
+
opts.replacement,
|
|
1078
|
+
"--replacement-mode",
|
|
1079
|
+
replacementMode,
|
|
1080
|
+
"--runner",
|
|
1081
|
+
runner,
|
|
1082
|
+
"--json"
|
|
1083
|
+
];
|
|
1084
|
+
if (opts.timeout_ms !== undefined)
|
|
1085
|
+
args.push("--timeout-ms", String(opts.timeout_ms));
|
|
1086
|
+
if (opts.link_node_modules)
|
|
1087
|
+
args.push("--link-node-modules");
|
|
1088
|
+
if (opts.vitest_config)
|
|
1089
|
+
args.push("--vitest-config", toWorkspaceRel(sourceRoot, resolveContained(sourceRoot, opts.vitest_config)));
|
|
1090
|
+
if (opts.jest_config)
|
|
1091
|
+
args.push("--jest-config", toWorkspaceRel(sourceRoot, resolveContained(sourceRoot, opts.jest_config)));
|
|
1092
|
+
for (const entry of opts.test_env ?? [])
|
|
1093
|
+
args.push("--test-env", entry);
|
|
1094
|
+
const run = (deps.dynamicProofRunner ?? defaultDynamicProofRunner)(args, { cwd: sourceRoot });
|
|
1095
|
+
oracle = parseDynamicProofJson(run.stdout, run.stderr);
|
|
1096
|
+
}
|
|
801
1097
|
const closed = dynamicProofSucceeded(oracle);
|
|
802
1098
|
const baselineGreen = oracle.baseline?.exitCode === 0 && oracle.baseline?.timedOut !== true;
|
|
803
1099
|
const mutantFailedAssertion = oracle.mutant?.assertionFailure === true && oracle.mutant?.timedOut !== true;
|
|
@@ -927,8 +1223,8 @@ export function opProveLoop(root, opts, deps = defaultDeps()) {
|
|
|
927
1223
|
throw new Error("prove requires --target-symbol sym:<file>#<Symbol>, or a target id that resolves to exactly one CodeSymbol.");
|
|
928
1224
|
}
|
|
929
1225
|
const language = targetLanguage(target);
|
|
930
|
-
if (language !== "typescript") {
|
|
931
|
-
throw new Error(`prove currently supports JavaScript/TypeScript targets only; found ${language}.`);
|
|
1226
|
+
if (language !== "typescript" && language !== "go" && language !== "java" && language !== "python") {
|
|
1227
|
+
throw new Error(`prove currently supports JavaScript/TypeScript, Go, Java, and Python targets only; found ${language}.`);
|
|
932
1228
|
}
|
|
933
1229
|
const setupCommands = (opts.setup_commands ?? []).map((command, index) => validateSetupCommand(command, index));
|
|
934
1230
|
const sourceRoot = resolve(opts.source ?? root);
|
|
@@ -947,6 +1243,8 @@ export function opProveLoop(root, opts, deps = defaultDeps()) {
|
|
|
947
1243
|
target_id: opts.target_id,
|
|
948
1244
|
source: opts.source,
|
|
949
1245
|
test_path: opts.test_path,
|
|
1246
|
+
test_run: opts.test_run,
|
|
1247
|
+
go_assertion_line: opts.go_assertion_line,
|
|
950
1248
|
target_path: opts.target_path,
|
|
951
1249
|
method: opts.method,
|
|
952
1250
|
replacement: opts.replacement,
|
|
@@ -1183,6 +1481,7 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1183
1481
|
noAuto: opts.noAuto,
|
|
1184
1482
|
provider: providerOpts.provider,
|
|
1185
1483
|
model: providerOpts.model,
|
|
1484
|
+
prompt_version: opts.promptVersion,
|
|
1186
1485
|
changedFiles: autoProveChangedScope(loadGraph(workspacePaths(root).graphPath), changed, opts.baseRef)
|
|
1187
1486
|
}, { ...providerDeps, proveLoop: opProveLoop });
|
|
1188
1487
|
for (const skip of autoProveResult.skipped)
|
|
@@ -1203,6 +1502,19 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1203
1502
|
};
|
|
1204
1503
|
warnings.push(`auto-prove skipped: ${reason}`);
|
|
1205
1504
|
}
|
|
1505
|
+
// G1: persist the distilled, already-redacted attempt classifications so
|
|
1506
|
+
// `opro doctor --proof` and standalone report regens can explain blockers
|
|
1507
|
+
// after this process exits. Sidecar only — never read by the oracle, RTM,
|
|
1508
|
+
// or ledger paths; a write failure must never fail start.
|
|
1509
|
+
try {
|
|
1510
|
+
writeProofAttempts(root, distillProofAttempts(autoProveResult, {
|
|
1511
|
+
generatedAt: deps.clock(),
|
|
1512
|
+
graph: loadGraph(workspacePaths(root).graphPath)
|
|
1513
|
+
}));
|
|
1514
|
+
}
|
|
1515
|
+
catch (error) {
|
|
1516
|
+
warnings.push(`proof-attempts sidecar not written: ${error instanceof Error ? error.message : String(error)}`);
|
|
1517
|
+
}
|
|
1206
1518
|
let coverageReport;
|
|
1207
1519
|
try {
|
|
1208
1520
|
reportProgress("artifacts: writing coverage report", { current: 6, total: 8 });
|
|
@@ -1274,10 +1586,10 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1274
1586
|
if (rtm.rows.length < rtm.summary.total)
|
|
1275
1587
|
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
1588
|
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:
|
|
1589
|
+
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
1590
|
}
|
|
1279
1591
|
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:
|
|
1592
|
+
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
1593
|
}
|
|
1282
1594
|
else {
|
|
1283
1595
|
nextActions.push("No deterministic gap target was found; inspect the RTM and graph before generating tests.");
|
|
@@ -1365,6 +1677,16 @@ export async function opGenerate(root, opts = {}, deps = defaultDeps()) {
|
|
|
1365
1677
|
updated_at: deps.clock()
|
|
1366
1678
|
};
|
|
1367
1679
|
saveGraph(paths.graphPath, next);
|
|
1680
|
+
// Keep the behavior report in sync with the freshly persisted generated
|
|
1681
|
+
// tests — analyze/start would REBUILD the graph and drop them, so this is
|
|
1682
|
+
// the only command that can surface them. Display-only refresh; a render
|
|
1683
|
+
// failure must never fail generate.
|
|
1684
|
+
try {
|
|
1685
|
+
opBehaviorCoverageHtml(root, `${WORKSPACE_DIR}/behavior-coverage.html`);
|
|
1686
|
+
}
|
|
1687
|
+
catch {
|
|
1688
|
+
/* best-effort report refresh */
|
|
1689
|
+
}
|
|
1368
1690
|
}
|
|
1369
1691
|
// Validate each test's grounding citations against the graph the tests cite.
|
|
1370
1692
|
// This is the keyless grounding contract: provenance must be verifiable, and a
|
|
@@ -1553,11 +1875,33 @@ export function opGraphHtml(root, outputPath = "orangepro-graph.html") {
|
|
|
1553
1875
|
/** Write the self-contained offline behavior-coverage view (deterministic, metadata only). */
|
|
1554
1876
|
export function opBehaviorCoverageHtml(root, outputPath = "orangepro-behavior-coverage.html", dynamicProof) {
|
|
1555
1877
|
const graph = loadGraph(workspacePaths(root).graphPath);
|
|
1556
|
-
|
|
1878
|
+
// Standalone regens have no this-run outcome: fall back to the persisted
|
|
1879
|
+
// proof-attempts sidecar ONLY when it anchors to the current graph+commit
|
|
1880
|
+
// (stale evidence is dropped — fail closed; display copy only, no tier math).
|
|
1881
|
+
const dyn = dynamicProof ?? sidecarDynamicProof(root, graph);
|
|
1882
|
+
const html = renderBehaviorReport(buildBehaviorReportData(graph, loadLedger(root), { repoRoot: root, dynamicProof: dyn }));
|
|
1557
1883
|
const htmlPath = resolve(root, outputPath);
|
|
1558
1884
|
writeFileSync(htmlPath, html, "utf8");
|
|
1559
1885
|
return { behavior_coverage_path: htmlPath };
|
|
1560
1886
|
}
|
|
1887
|
+
/** Fresh-only sidecar view for report regens; unreadable or stale ⇒ undefined. */
|
|
1888
|
+
function sidecarDynamicProof(root, graph) {
|
|
1889
|
+
try {
|
|
1890
|
+
const attempts = loadProofAttempts(root);
|
|
1891
|
+
if (!attempts || !proofAttemptsFresh(attempts, graph))
|
|
1892
|
+
return undefined;
|
|
1893
|
+
return {
|
|
1894
|
+
attempted: attempts.attempted,
|
|
1895
|
+
proven: attempts.proven,
|
|
1896
|
+
needsSetup: attempts.attempts
|
|
1897
|
+
.filter((a) => a.classification === "needs_setup")
|
|
1898
|
+
.map((a) => ({ category: a.category, reason: a.reason }))
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
catch {
|
|
1902
|
+
return undefined;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1561
1905
|
/** Phase 5.2 — write the human-readable COVERAGE_REPORT.md (3-file contract). */
|
|
1562
1906
|
export function opCoverageReport(root, outputPath = "COVERAGE_REPORT.md") {
|
|
1563
1907
|
const graph = loadGraph(workspacePaths(root).graphPath);
|
|
@@ -1663,7 +2007,7 @@ const NO_CODE_CHANGES_GUIDANCE = "The diff vs the base ref only touched docs (.m
|
|
|
1663
2007
|
* `base_ref` defaults to `main`. The diff runs from `git merge-base <base> HEAD`
|
|
1664
2008
|
* to the working tree: the branch's own commits AND uncommitted edits, never
|
|
1665
2009
|
* upstream churn on the base (falls back to the base tip when no merge-base
|
|
1666
|
-
* exists).
|
|
2010
|
+
* exists). This diff resolver is read-only: it does not write repo files or upload source.
|
|
1667
2011
|
*/
|
|
1668
2012
|
export function resolveDiffContext(graph, baseRefInput) {
|
|
1669
2013
|
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;
|