@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.
@@ -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
- const replacementMode = opts.replacement_mode ?? "return-json";
765
- const runner = opts.runner ?? "auto";
766
- if (replacementMode !== "return-json" && replacementMode !== "promise-json") {
767
- throw new Error("prove --replacement-mode must be one of: return-json, promise-json.");
768
- }
769
- if (runner !== "auto" && runner !== "vitest" && runner !== "jest" && runner !== "mocha") {
770
- throw new Error("prove --runner must be one of: auto, vitest, jest, mocha.");
771
- }
772
- const args = [
773
- "--root",
774
- sourceRoot,
775
- "--test",
776
- testRel,
777
- "--target",
778
- targetRel,
779
- "--method",
780
- method,
781
- "--replacement",
782
- opts.replacement,
783
- "--replacement-mode",
784
- replacementMode,
785
- "--runner",
786
- runner,
787
- "--json"
788
- ];
789
- if (opts.timeout_ms !== undefined)
790
- args.push("--timeout-ms", String(opts.timeout_ms));
791
- if (opts.link_node_modules)
792
- args.push("--link-node-modules");
793
- if (opts.vitest_config)
794
- args.push("--vitest-config", toWorkspaceRel(sourceRoot, resolveContained(sourceRoot, opts.vitest_config)));
795
- if (opts.jest_config)
796
- args.push("--jest-config", toWorkspaceRel(sourceRoot, resolveContained(sourceRoot, opts.jest_config)));
797
- for (const entry of opts.test_env ?? [])
798
- args.push("--test-env", entry);
799
- const run = (deps.dynamicProofRunner ?? defaultDynamicProofRunner)(args, { cwd: sourceRoot });
800
- const oracle = parseDynamicProofJson(run.stdout, run.stderr);
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: use prove_run when present for public Proven, otherwise record_run for static diagnostics.`);
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: use prove_run when present for public Proven, otherwise record_run for static diagnostics.`);
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
- const html = renderBehaviorReport(buildBehaviorReportData(graph, loadLedger(root), { repoRoot: root, dynamicProof }));
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). Read-only: no repo writes, no source upload.
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 third file of the
7
- * 3-file contract (graph.html / COVERAGE_REPORT.md / graph.json). It states,
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 graph.html) ----
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;