@orangepro/orangepro-mcp 0.2.3 → 0.2.4
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/README.md +29 -9
- package/dist/local/analyze/analyzer.js +61 -2
- package/dist/local/analyze/parseCache.js +1 -1
- package/dist/local/analyze/treeSitter/engine.js +81 -4
- package/dist/local/autoProve.js +39 -21
- package/dist/local/cli.js +6 -0
- package/dist/local/graph/ontology.js +3 -1
- package/dist/local/operations.js +36 -5
- package/dist/local/util/walk.js +5 -4
- package/dist/local/viz/coverageReveal.js +29 -0
- package/dist/local/workspace.js +25 -2
- package/package.json +1 -1
- package/scripts/spikes/go-dynamic-proof-spike.mjs +17 -13
- package/scripts/spikes/go-mutate.go +60 -21
- package/scripts/spikes/java-dynamic-proof-spike.mjs +6 -4
- package/scripts/spikes/java-mutate.mjs +3 -2
package/README.md
CHANGED
|
@@ -2,33 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
**Find the behaviors your tests miss. Generate grounded tests that actually run.**
|
|
4
4
|
|
|
5
|
-
`opro` builds a knowledge graph from your local checkout, maps every behavior in your code, shows which ones are tested and which aren't, and generates integration-level tests grounded in real symbols — not hallucinated imports.
|
|
5
|
+
`opro` builds a knowledge graph from your local checkout, maps every behavior in your code, shows which ones are tested and which aren't, and generates integration-level tests grounded in real symbols — not hallucinated imports. It runs as a CLI and a local stdio MCP server.
|
|
6
|
+
|
|
7
|
+
Install the target repository's dependencies first, then run OrangePro from that repository:
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
npx @orangepro/mcp-server
|
|
9
10
|
cd /path/to/your/repo
|
|
10
|
-
|
|
11
|
+
npm install # or pnpm install / bun install / the repository's package manager
|
|
12
|
+
|
|
13
|
+
# Optional: enables AI candidate links, candidate flows, and test generation.
|
|
14
|
+
export ANTHROPIC_API_KEY="..." # or OPENAI_API_KEY / OLLAMA_BASE_URL
|
|
15
|
+
|
|
16
|
+
npx -y @orangepro/mcp-server@latest start . --prompt-version v5
|
|
17
|
+
open .orangepro/behavior-coverage.html
|
|
11
18
|
```
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
With no model key, the same command still performs deterministic analysis, renders the report, and dynamically proves eligible behaviors using existing tests. With a key, it also discovers AI candidate flows and drafts grounded tests for the highest-risk gaps. AI output never changes evidence tiers; only the mutation-kill oracle can mint **Dynamically Proven**.
|
|
21
|
+
|
|
22
|
+
The command writes:
|
|
14
23
|
|
|
15
24
|
```
|
|
16
25
|
.orangepro/
|
|
17
26
|
├── behavior-coverage.html ← open this: interactive gap report
|
|
27
|
+
├── graph.json ← deterministic evidence graph
|
|
28
|
+
├── COVERAGE_REPORT.md ← coverage and gap summary
|
|
18
29
|
├── rtm.md ← requirements traceability matrix
|
|
19
|
-
└──
|
|
30
|
+
└── ai/ ← candidate AI links/flows when a provider is configured
|
|
31
|
+
|
|
32
|
+
orangepro_generated/ ← contained generated tests; existing source files are untouched
|
|
20
33
|
```
|
|
21
34
|
|
|
35
|
+
Run `opro export` when you want a machine-readable evidence pack.
|
|
36
|
+
<img width="895" height="960" alt="Screenshot 2026-07-08 at 1 18 01 AM" src="https://github.com/user-attachments/assets/73b8a812-2eab-43a1-8aed-4289545e630b" />
|
|
37
|
+
|
|
22
38
|
---
|
|
23
39
|
|
|
24
40
|
## Install
|
|
25
41
|
|
|
26
42
|
```bash
|
|
27
|
-
# No install needed
|
|
28
|
-
npx @orangepro/mcp-server
|
|
43
|
+
# No install needed: run the full local workflow in the current repository
|
|
44
|
+
npx -y @orangepro/mcp-server@latest start . --prompt-version v5
|
|
29
45
|
|
|
30
46
|
# Or global install
|
|
31
47
|
npm install -g @orangepro/orangepro-mcp
|
|
48
|
+
opro start . --prompt-version v5
|
|
32
49
|
|
|
33
50
|
# Or from source
|
|
34
51
|
git clone https://github.com/OrangeproAI/orangepro-mcp.git
|
|
@@ -89,7 +106,6 @@ Add to your client's MCP config:
|
|
|
89
106
|
|
|
90
107
|
| Client | Config location |
|
|
91
108
|
| --- | --- |
|
|
92
|
-
|--------|----------------|
|
|
93
109
|
| Claude Code | `.mcp.json` or `~/.claude.json` |
|
|
94
110
|
| Cursor | `~/.cursor/mcp.json` or Settings → MCP |
|
|
95
111
|
| Codex | Config printed by `opro agent --client codex` or `npx -y @orangepro/mcp-server@latest agent --client codex` |
|
|
@@ -144,6 +160,7 @@ opro rtm # traceability matrix
|
|
|
144
160
|
opro export # metadata-only evidence pack
|
|
145
161
|
opro mcp # run as MCP server (stdio)
|
|
146
162
|
opro doctor # what evidence to add next
|
|
163
|
+
opro doctor --proof # explain why dynamic proof could not close
|
|
147
164
|
opro coverage # ingest runtime coverage
|
|
148
165
|
```
|
|
149
166
|
|
|
@@ -194,6 +211,8 @@ Every behavior gets exactly one tier. Nothing is labeled "tested" on faith.
|
|
|
194
211
|
|
|
195
212
|
> **"Dynamically Proven 0" is normal on first run.** Static analysis always runs. Dynamic proof requires running tests against targeted mutations. That's the trust model — nothing is Dynamically Proven until a real test kills a real mutant.
|
|
196
213
|
|
|
214
|
+
When runtime coverage is available, `opro start` also compares Runtime-covered and Dynamically Proven behaviors over the same deterministic denominator. It never compares source-line coverage with behavior proof or folds off-denominator proofs into that percentage.
|
|
215
|
+
|
|
197
216
|
---
|
|
198
217
|
|
|
199
218
|
## Language support
|
|
@@ -270,9 +289,10 @@ OrangePro separates **analysis** (what your code does) from **proof** (whether t
|
|
|
270
289
|
## Privacy
|
|
271
290
|
|
|
272
291
|
- **No stored source.** Reads code in-process. Never uploads to an OrangePro server.
|
|
273
|
-
- **No source mutation.** Never edits
|
|
292
|
+
- **No existing-source mutation.** Never edits existing source or test files. Writes metadata to `.orangepro/`; keyed auto-drive may write new, reviewable tests under `orangepro_generated/`.
|
|
274
293
|
- **Metadata-only exports.** File paths, names, hashes, scores — not raw source.
|
|
275
294
|
- **Your keys stay yours.** Read from env at call time, never persisted.
|
|
295
|
+
- **BYOK is direct.** When AI lanes are enabled, grounded code context is sent directly to the model provider you configure; OrangePro's hosted service is not in that path.
|
|
276
296
|
|
|
277
297
|
---
|
|
278
298
|
|
|
@@ -1534,6 +1534,40 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
1534
1534
|
const symId = `sym:${targetRel}#${name}`;
|
|
1535
1535
|
return codeSymbolIds.has(symId) ? symId : null;
|
|
1536
1536
|
};
|
|
1537
|
+
/**
|
|
1538
|
+
* ALL package method symbols whose bare name is `bareName` — receiver-qualified
|
|
1539
|
+
* `Recv.M` hits plus bare-named ones (underivable receiver). Go method symbols
|
|
1540
|
+
* are minted qualified (engine `goReceiverBaseName`), so a bare test callee maps
|
|
1541
|
+
* back through this. Same-named FREE functions never appear (kind-filtered):
|
|
1542
|
+
* the mutator's receiver-exact `--recv` match means a free fn can never be
|
|
1543
|
+
* mutated in a method attempt.
|
|
1544
|
+
*/
|
|
1545
|
+
const goPackageMethodHits = (dir, bareName) => {
|
|
1546
|
+
const suffix = `.${bareName}`;
|
|
1547
|
+
const hits = [];
|
|
1548
|
+
for (const f of goFilesByDir.get(dir) ?? []) {
|
|
1549
|
+
if (isNonProductFile(f))
|
|
1550
|
+
continue;
|
|
1551
|
+
for (const n of symbolsByFile.get(f) ?? []) {
|
|
1552
|
+
if ((n === bareName || n.endsWith(suffix)) && symbolKind(f, n) === "method")
|
|
1553
|
+
hits.push({ file: f, qualified: n });
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return hits;
|
|
1557
|
+
};
|
|
1558
|
+
/**
|
|
1559
|
+
* Receiver type pinned from a constructor's DECLARED result: `p := New(...)` →
|
|
1560
|
+
* New's single `T`/`*T` result base name (engine `goCtorResults`, structurally
|
|
1561
|
+
* capturable results only). Undefined — never a guess — when the ctor is not
|
|
1562
|
+
* package-unique or its result was not capturable (multi-return, interface,
|
|
1563
|
+
* qualified, generic): receiver pinning must fail closed.
|
|
1564
|
+
*/
|
|
1565
|
+
const goCtorResultType = (dir, ctorName) => {
|
|
1566
|
+
const ctorFile = uniqueGoPackageSymbol(dir, ctorName);
|
|
1567
|
+
if (!ctorFile)
|
|
1568
|
+
return undefined;
|
|
1569
|
+
return nonTsStructureByFile.get(ctorFile)?.structure.goCtorResults?.[ctorName];
|
|
1570
|
+
};
|
|
1537
1571
|
const eligiblePythonSymbol = (targetRel, name) => {
|
|
1538
1572
|
if (!targetRel || !(proofEligibleSymbolsByFile.get(targetRel) ?? []).includes(name))
|
|
1539
1573
|
return null;
|
|
@@ -1554,13 +1588,38 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
1554
1588
|
const symId = `sym:${targetRel}#${name}`;
|
|
1555
1589
|
return codeSymbolIds.has(symId) ? symId : null;
|
|
1556
1590
|
};
|
|
1557
|
-
const resolveGoProofTarget = (testRel, structure, qualifier, callee, shadowed) => {
|
|
1591
|
+
const resolveGoProofTarget = (testRel, structure, qualifier, callee, shadowed, receiverLocal, receiverCtor) => {
|
|
1558
1592
|
if (!qualifier) {
|
|
1559
1593
|
if (shadowed.has(callee) || structure.packageName?.endsWith("_test"))
|
|
1560
1594
|
return null;
|
|
1561
1595
|
return eligibleGoSymbol(uniqueGoPackageSymbol(dirOf(testRel), callee), callee);
|
|
1562
1596
|
}
|
|
1563
1597
|
const rootQualifier = qualifier.split(".")[0];
|
|
1598
|
+
// Receiver-local METHOD call `p.M()`: the qualifier `p` is a LOCAL by design
|
|
1599
|
+
// (declared `p := New(...)`), so this must be checked BEFORE the qualifier-shadow
|
|
1600
|
+
// guard (which exists to reject package-func names shadowed by locals, not receivers).
|
|
1601
|
+
// Resolve the bare callee among the package's receiver-qualified method symbols
|
|
1602
|
+
// (`Recv.M`):
|
|
1603
|
+
// - ONE method named M package-wide → take it (the #212 unique-name behavior;
|
|
1604
|
+
// the mutator's `--recv` exact match keeps the mutation on that receiver).
|
|
1605
|
+
// - MULTIPLE methods named M → pin the receiver TYPE from the constructor's
|
|
1606
|
+
// DECLARED result (`p := New(...)` → goCtorResultType, structurally provable
|
|
1607
|
+
// results only) and require exactly one `pin.M` hit; no provable pin → refuse
|
|
1608
|
+
// (the pre-pinning fail-closed behavior).
|
|
1609
|
+
// The pin SELECTS only — it never rejects the unique candidate (an interface- or
|
|
1610
|
+
// alias-returning ctor would name an abstraction, not a receiver, and a wrong
|
|
1611
|
+
// selection is fail-safe anyway: the frozen oracle only fails closed in the
|
|
1612
|
+
// survive direction). Callee and ctor both honor shadowing.
|
|
1613
|
+
if (receiverLocal && !shadowed.has(callee) && !structure.packageName?.endsWith("_test")) {
|
|
1614
|
+
const hits = goPackageMethodHits(dirOf(testRel), callee);
|
|
1615
|
+
if (hits.length === 1)
|
|
1616
|
+
return eligibleGoSymbol(hits[0].file, hits[0].qualified);
|
|
1617
|
+
const pinned = receiverCtor && !shadowed.has(receiverCtor) ? goCtorResultType(dirOf(testRel), receiverCtor) : undefined;
|
|
1618
|
+
if (!pinned)
|
|
1619
|
+
return null;
|
|
1620
|
+
const match = hits.filter((h) => h.qualified === `${pinned}.${callee}`);
|
|
1621
|
+
return match.length === 1 ? eligibleGoSymbol(match[0].file, match[0].qualified) : null;
|
|
1622
|
+
}
|
|
1564
1623
|
if (shadowed.has(rootQualifier))
|
|
1565
1624
|
return null;
|
|
1566
1625
|
const binding = structure.imports.find((i) => i.local === rootQualifier && i.kind === "module");
|
|
@@ -1730,7 +1789,7 @@ export function analyzeRepo(root, opts = {}) {
|
|
|
1730
1789
|
const testExternalId = `test:${testRel}`;
|
|
1731
1790
|
for (const proof of structure.goProofCalls ?? []) {
|
|
1732
1791
|
goProofAttempted++;
|
|
1733
|
-
const symId = resolveGoProofTarget(testRel, structure, proof.qualifier, proof.callee, new Set(proof.shadowed));
|
|
1792
|
+
const symId = resolveGoProofTarget(testRel, structure, proof.qualifier, proof.callee, new Set(proof.shadowed), proof.receiverLocal, proof.receiverCtor);
|
|
1734
1793
|
if (!symId)
|
|
1735
1794
|
continue;
|
|
1736
1795
|
const edgeKey = `${testExternalId}|${symId}`;
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
// Swift protocol methods, and Rust trait signatures while dropping Rust aliases.
|
|
55
55
|
// v16: symbol extraction now carries source line spans for runtime coverage
|
|
56
56
|
// report ingestion; warm v15 entries lack the ranges and cannot be mapped.
|
|
57
|
-
export const PARSER_VERSION =
|
|
57
|
+
export const PARSER_VERSION = 17; // 17: Go method symbols receiver-qualified (Recv.M + member_of)
|
|
58
58
|
const SYMBOL_KINDS = new Set(["function", "class", "const", "method"]);
|
|
59
59
|
/** Strict structural validation — persisted data is untrusted FOR SHAPE; reject anything off-shape. */
|
|
60
60
|
function validSymbols(v) {
|
|
@@ -252,8 +252,12 @@ export function extractTreeSitterSymbols(content, language) {
|
|
|
252
252
|
if (kind && shouldEmitSymbol(node, language)) {
|
|
253
253
|
const name = symbolName(node, cfg);
|
|
254
254
|
if (name && !RESERVED_SYMBOL_NAMES.has(name)) {
|
|
255
|
+
// Go methods mint receiver-qualified names (`Recv.M`, mirroring TS/JS
|
|
256
|
+
// `Class.method` + member_of) so same-named methods on different receivers
|
|
257
|
+
// stay distinct symbols; an underivable receiver falls back to the bare name.
|
|
258
|
+
const recv = kind === "method" && language === "go" ? goReceiverBaseName(node) : undefined;
|
|
255
259
|
add(kind === "method"
|
|
256
|
-
? { name, symbol_kind: kind, trivial_accessor: isTrivialAccessorBody(node, language), ...nodeLines(node) }
|
|
260
|
+
? { name: recv ? `${recv}.${name}` : name, ...(recv ? { member_of: recv } : {}), symbol_kind: kind, trivial_accessor: isTrivialAccessorBody(node, language), ...nodeLines(node) }
|
|
257
261
|
: { name, symbol_kind: kind, ...nodeLines(node) });
|
|
258
262
|
}
|
|
259
263
|
}
|
|
@@ -381,6 +385,20 @@ function functionName(node, language) {
|
|
|
381
385
|
return name;
|
|
382
386
|
return name;
|
|
383
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* Base receiver type name for a Go method declaration — `(p *Parser)` → "Parser",
|
|
390
|
+
* `(g Generic[T])` → "Generic". Undefined when the receiver shape is underivable,
|
|
391
|
+
* in which case callers fall back to the bare method name (the pre-qualified
|
|
392
|
+
* behavior). NOT used for test-name extraction — suite test names must stay bare
|
|
393
|
+
* (`^Test` gate at extractGoProofCalls).
|
|
394
|
+
*/
|
|
395
|
+
function goReceiverBaseName(node) {
|
|
396
|
+
const param = node.childForFieldName("receiver")?.namedChild(0);
|
|
397
|
+
let t = param?.childForFieldName("type") ?? undefined;
|
|
398
|
+
if (t && (t.type === "pointer_type" || t.type === "generic_type"))
|
|
399
|
+
t = t.namedChild(0) ?? undefined;
|
|
400
|
+
return t?.type === "type_identifier" ? t.text : undefined;
|
|
401
|
+
}
|
|
384
402
|
function javaPackage(root) {
|
|
385
403
|
const pkg = namedChildren(root).find((n) => n.type === "package_declaration");
|
|
386
404
|
return pkg ? namedChildren(pkg).find((n) => n.type.endsWith("identifier"))?.text : undefined;
|
|
@@ -389,6 +407,29 @@ function goPackage(root) {
|
|
|
389
407
|
const pkg = namedChildren(root).find((n) => n.type === "package_clause");
|
|
390
408
|
return pkg ? namedChildren(pkg).find((n) => n.type === "package_identifier")?.text : undefined;
|
|
391
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* Top-level Go function name → base name of its single declared result type
|
|
412
|
+
* (`func New(n int) *Parser` → "Parser"). ONLY a bare `type_identifier` (optionally
|
|
413
|
+
* behind one `*`) is captured; multi-return (`parameter_list`), interface, qualified
|
|
414
|
+
* (`pkg.T`), and generic results are omitted — constructor-based receiver pinning
|
|
415
|
+
* must fail closed on any result it cannot prove structurally.
|
|
416
|
+
*/
|
|
417
|
+
function extractGoCtorResults(root) {
|
|
418
|
+
const out = {};
|
|
419
|
+
for (const child of namedChildren(root)) {
|
|
420
|
+
if (child.type !== "function_declaration")
|
|
421
|
+
continue;
|
|
422
|
+
const name = child.childForFieldName("name")?.text;
|
|
423
|
+
if (!name)
|
|
424
|
+
continue;
|
|
425
|
+
let r = child.childForFieldName("result") ?? undefined;
|
|
426
|
+
if (r?.type === "pointer_type")
|
|
427
|
+
r = r.namedChild(0) ?? undefined;
|
|
428
|
+
if (r?.type === "type_identifier")
|
|
429
|
+
out[name] = r.text;
|
|
430
|
+
}
|
|
431
|
+
return out;
|
|
432
|
+
}
|
|
392
433
|
function kotlinPackage(root) {
|
|
393
434
|
const pkg = namedChildren(root).find((n) => n.type === "package_header");
|
|
394
435
|
return pkg ? namedChildren(pkg).find((n) => n.type === "identifier")?.text : undefined;
|
|
@@ -860,6 +901,11 @@ function goShortVarCalls(stmt) {
|
|
|
860
901
|
function extractGoProofCalls(root, imports) {
|
|
861
902
|
const out = [];
|
|
862
903
|
const seen = new Set();
|
|
904
|
+
// Var name → constructor name for locals declared in the CURRENT test func by a
|
|
905
|
+
// bare same-package `x := New(...)` (single unqualified call). Reset per top-level
|
|
906
|
+
// test func; marks a qualified proof-call `p.M()` as receiver-local AND carries
|
|
907
|
+
// the ctor name so the analyzer can pin p's receiver type from New's result.
|
|
908
|
+
let receiverLocals = new Map();
|
|
863
909
|
const assertLocals = goAssertLocals(imports);
|
|
864
910
|
const dotAssertMethods = goDotAssertMethods(root);
|
|
865
911
|
const suiteTypes = goCanonicalSuiteTypes(root, goSuiteLocals(imports));
|
|
@@ -869,7 +915,8 @@ function extractGoProofCalls(root, imports) {
|
|
|
869
915
|
if (seen.has(key))
|
|
870
916
|
continue;
|
|
871
917
|
seen.add(key);
|
|
872
|
-
|
|
918
|
+
const receiverCtor = c.via === "qualified" && !!c.qualifier && !c.qualifier.includes(".") ? receiverLocals.get(c.qualifier) : undefined;
|
|
919
|
+
out.push({ caller: testName, testName, ...c, shadowed: [...shadowed], assertion, ...(assertionLine ? { assertionLine } : {}), ...(receiverCtor ? { receiverLocal: true, receiverCtor } : {}) });
|
|
873
920
|
}
|
|
874
921
|
};
|
|
875
922
|
const processBlock = (block, testName, testingParams, shadowed) => {
|
|
@@ -934,7 +981,29 @@ function extractGoProofCalls(root, imports) {
|
|
|
934
981
|
for (const name of sv.names)
|
|
935
982
|
pending.set(name, sv.calls);
|
|
936
983
|
}
|
|
984
|
+
if (stmt.type === "short_var_declaration") {
|
|
985
|
+
// A lone `x := New(...)` makes x a receiver-local: its type comes from a
|
|
986
|
+
// same-package constructor, so `x.M()` targets a same-package method. This
|
|
987
|
+
// check inspects the TOP-LEVEL RHS call directly (not goShortVarCalls, whose
|
|
988
|
+
// single-product-call rule drops `New(strings.NewReader(...))` for its nested
|
|
989
|
+
// arg — the real phcparser shape). Refuses composite literals, qualified/
|
|
990
|
+
// imported calls, and multi-assign — none are receiver-locals.
|
|
991
|
+
const rhsNames = new Set();
|
|
992
|
+
collectNames(stmt.childForFieldName("left") ?? stmt.namedChild(0), rhsNames);
|
|
993
|
+
const rhs = stmt.childForFieldName("right");
|
|
994
|
+
const rhsCalls = rhs ? namedChildren(rhs).filter((n) => n.type === "call_expression") : [];
|
|
995
|
+
if (rhsNames.size === 1 && rhsCalls.length === 1) {
|
|
996
|
+
const cp = callParts(rhsCalls[0], "go");
|
|
997
|
+
if (cp && cp.via === "free")
|
|
998
|
+
receiverLocals.set([...rhsNames][0], cp.callee);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
937
1001
|
else if (stmt.type === "assignment_statement") {
|
|
1002
|
+
// Reassigning a receiver-local invalidates it too.
|
|
1003
|
+
const reassignedRl = new Set();
|
|
1004
|
+
collectNames(stmt.childForFieldName("left") ?? stmt.namedChild(0), reassignedRl);
|
|
1005
|
+
for (const name of reassignedRl)
|
|
1006
|
+
receiverLocals.delete(name);
|
|
938
1007
|
// Plain reassignment kills the binding — the checked value is no longer F's.
|
|
939
1008
|
const reassigned = new Set();
|
|
940
1009
|
collectNames(stmt.childForFieldName("left") ?? stmt.namedChild(0), reassigned);
|
|
@@ -979,6 +1048,7 @@ function extractGoProofCalls(root, imports) {
|
|
|
979
1048
|
const body = child.childForFieldName("body");
|
|
980
1049
|
if (!testingParams.size || !body)
|
|
981
1050
|
continue;
|
|
1051
|
+
receiverLocals = new Map();
|
|
982
1052
|
processBlock(body, name, testingParams, localBindings(child, "go"));
|
|
983
1053
|
}
|
|
984
1054
|
return out;
|
|
@@ -1442,7 +1512,14 @@ export function extractTreeSitterStructure(content, language) {
|
|
|
1442
1512
|
nextInsideFunction = true;
|
|
1443
1513
|
}
|
|
1444
1514
|
else {
|
|
1445
|
-
|
|
1515
|
+
let name = functionName(node, language);
|
|
1516
|
+
// Go method symbols are receiver-qualified — attribute calls to the
|
|
1517
|
+
// qualified caller so Layer-1 edges keep matching the emitted symbol.
|
|
1518
|
+
if (name && language === "go" && node.type === "method_declaration") {
|
|
1519
|
+
const recv = goReceiverBaseName(node);
|
|
1520
|
+
if (recv)
|
|
1521
|
+
name = `${recv}.${name}`;
|
|
1522
|
+
}
|
|
1446
1523
|
if (name) {
|
|
1447
1524
|
nextCaller = name;
|
|
1448
1525
|
nextShadowed = localBindings(node, language);
|
|
@@ -1461,7 +1538,7 @@ export function extractTreeSitterStructure(content, language) {
|
|
|
1461
1538
|
const imports = extractImports(root, language);
|
|
1462
1539
|
const result = {
|
|
1463
1540
|
...(language === "java" ? { packageName: javaPackage(root), javaClasses: javaClassInfos(root), javaProofCalls: extractJavaProofCalls(root, imports) } : {}),
|
|
1464
|
-
...(language === "go" ? { packageName: goPackage(root) } : {}),
|
|
1541
|
+
...(language === "go" ? { packageName: goPackage(root), goCtorResults: extractGoCtorResults(root) } : {}),
|
|
1465
1542
|
...(language === "kotlin" ? { packageName: kotlinPackage(root), topLevelSymbols: kotlinTopLevelSymbols(root) } : {}),
|
|
1466
1543
|
...(language === "php" ? { moduleName: phpNamespace(root) } : {}),
|
|
1467
1544
|
...(language === "csharp" ? { moduleName: csharpNamespace(root) } : {}),
|
package/dist/local/autoProve.js
CHANGED
|
@@ -108,15 +108,18 @@ function pytestNodeidsForTarget(sourceRoot, testRel, testName) {
|
|
|
108
108
|
* `behavior_surface === "entrypoint_adjacent"`, and carrying NO `denominator_reason_code`
|
|
109
109
|
* (infra_behavior_surface / not_entry_point_adjacent).
|
|
110
110
|
*
|
|
111
|
-
* Language: TS/JS (unchanged), Go (G-INT-2), OR Java (J-INT-2).
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
111
|
+
* Language: TS/JS (unchanged), Go (G-INT-2), OR Java (J-INT-2). This STRICT predicate
|
|
112
|
+
* keeps Go to FREE FUNCTIONS ONLY. The Go oracle can also prove receiver METHODS, but
|
|
113
|
+
* a method's no-false-Proven story rests entirely on the analyzer-minted hard
|
|
114
|
+
* receiver-local TESTED_BY/COVERS edge (package-unique method-name gating) — so Go
|
|
115
|
+
* methods are admitted solely by `isEligibleHardExistingTarget` on the hard-edge lane,
|
|
116
|
+
* never here: this predicate also feeds PR/changed-scope selection, weak MAY_*
|
|
117
|
+
* expansion, and generation candidate filtering, where no hard edge backs the pick.
|
|
118
|
+
* Java dynamic proof (J-1) is the INVERSE — it proves single-top-level-return METHODS,
|
|
119
|
+
* so we admit Java methods; a non-J-1-shape method (void/constructor/nested/generic)
|
|
120
|
+
* just classifies `unrunnable` and never mints, safe by construction. Everything
|
|
121
|
+
* downstream of `closed`/the cert is language-agnostic and mints Go/Java only through
|
|
122
|
+
* the unchanged G-INT-1/J-INT-1 gates.
|
|
120
123
|
*/
|
|
121
124
|
export function isEligibleProvableTarget(node) {
|
|
122
125
|
if (!node || node.kind !== "CodeSymbol")
|
|
@@ -133,18 +136,27 @@ export function isEligibleProvableTarget(node) {
|
|
|
133
136
|
* The LANGUAGE + oracle-SHAPE half of eligibility, factored out so both the strict
|
|
134
137
|
* `isEligibleProvableTarget` (which layers the entry-point-adjacent SCOPE guards on top)
|
|
135
138
|
* and the relaxed hard-edge existing-tests path share ONE definition of "a shape the
|
|
136
|
-
* oracle can prove". CodeSymbol + a TS/JS file, a Go free function
|
|
137
|
-
*
|
|
139
|
+
* oracle can prove". CodeSymbol + a TS/JS file, a Go free function (a receiver method
|
|
140
|
+
* only when the caller opts in via `allowGoMethod`), a Java method, or a Python
|
|
141
|
+
* function/method.
|
|
138
142
|
*/
|
|
139
|
-
function matchesProvableLanguageShape(node) {
|
|
143
|
+
function matchesProvableLanguageShape(node, opts) {
|
|
140
144
|
if (node.kind !== "CodeSymbol")
|
|
141
145
|
return false;
|
|
142
146
|
const file = codeSymbolFile(node);
|
|
143
147
|
if (isTsJsFile(file))
|
|
144
148
|
return true;
|
|
145
|
-
// Go: free functions only
|
|
146
|
-
|
|
147
|
-
|
|
149
|
+
// Go: free functions only, unless the caller opts into methods (`allowGoMethod`).
|
|
150
|
+
// Only the hard-edge lane opts in: a method's no-false-Proven story rests on the
|
|
151
|
+
// analyzer's uniqueness-gated receiver-local TESTED_BY/COVERS edge, which the strict
|
|
152
|
+
// callers (changed-scope selection, weak MAY_* expansion, generation candidates)
|
|
153
|
+
// do not have — admitting methods there would break the invariant that a Go method
|
|
154
|
+
// only ever becomes a proof attempt via a hard receiver-local edge.
|
|
155
|
+
if (isGoFile(file)) {
|
|
156
|
+
if (node.properties.symbol_kind === "function")
|
|
157
|
+
return true;
|
|
158
|
+
return opts?.allowGoMethod === true && node.properties.symbol_kind === "method";
|
|
159
|
+
}
|
|
148
160
|
// Java: METHODS only (J-1 proves single-top-level-return methods). A non-J-1-shape
|
|
149
161
|
// method classifies `unrunnable` and never mints, so admitting all Java methods is
|
|
150
162
|
// safe (never a false Proven); a non-method Java symbol (a class container) is out
|
|
@@ -173,7 +185,11 @@ function matchesProvableLanguageShape(node) {
|
|
|
173
185
|
* by the frozen oracle, never false-Proven. Guards deliberately KEPT:
|
|
174
186
|
* - `infra_behavior_surface` still excludes plumbing (getters/registry accessors) — a
|
|
175
187
|
* hard edge does not buy an infra symbol an attempt (waste, and preserves the #4 bar);
|
|
176
|
-
* - the language + oracle-shape filter (never hand a class container
|
|
188
|
+
* - the language + oracle-shape filter (never hand a class container down).
|
|
189
|
+
* This is also the ONLY caller that admits Go receiver METHODS (`allowGoMethod`): it is
|
|
190
|
+
* reached solely behind a hard TESTED_BY/COVERS edge, and the analyzer mints a method
|
|
191
|
+
* edge only for the uniqueness-gated receiver-local shape — the strict predicate keeps
|
|
192
|
+
* Go methods out of every other selection path.
|
|
177
193
|
* Used solely on the hard lane; weak MAY_* fan-out stays strict.
|
|
178
194
|
*/
|
|
179
195
|
function isEligibleHardExistingTarget(node) {
|
|
@@ -181,7 +197,7 @@ function isEligibleHardExistingTarget(node) {
|
|
|
181
197
|
return false;
|
|
182
198
|
if (node.properties.denominator_reason_code === "infra_behavior_surface")
|
|
183
199
|
return false;
|
|
184
|
-
return matchesProvableLanguageShape(node);
|
|
200
|
+
return matchesProvableLanguageShape(node, { allowGoMethod: true });
|
|
185
201
|
}
|
|
186
202
|
/** Go `_test.go` top-level test-name regex (matches `extractTestNames`'s Go pattern). */
|
|
187
203
|
const GO_TEST_NAME_RE = /^Test[A-Za-z0-9_]+$/;
|
|
@@ -206,9 +222,10 @@ function anchorGoTestRun(name) {
|
|
|
206
222
|
* its target test BY NAME (`go test -run ^TestX$`), so auto-drive must derive that
|
|
207
223
|
* name — G-INT-1 took it as an explicit input.
|
|
208
224
|
*
|
|
209
|
-
* The link is the analyzer's HARD `TESTED_BY`/`COVERS` proof edge (Go free
|
|
210
|
-
* `test:<file>_test.go`), emitted only for an eligible Go
|
|
211
|
-
* asserts on it. PRIMARY: the edge carries
|
|
225
|
+
* The link is the analyzer's HARD `TESTED_BY`/`COVERS` proof edge (Go sym — free fn,
|
|
226
|
+
* or receiver-local method — ↔ `test:<file>_test.go`), emitted only for an eligible Go
|
|
227
|
+
* symbol whose test genuinely asserts on it. PRIMARY: the edge carries
|
|
228
|
+
* `properties.test_name` — the EXACT enclosing
|
|
212
229
|
* `func TestXxx` where the assertion witnessed THIS target (structural metadata, never
|
|
213
230
|
* proof) — which disambiguates even a `_test.go` file with many tests. FALLBACK (old
|
|
214
231
|
* graphs / no edge metadata): the linked TestCase node's file-level `test_names[]`,
|
|
@@ -519,7 +536,8 @@ export function existingAssociatedTests(graph, nodeById) {
|
|
|
519
536
|
};
|
|
520
537
|
// `hard` = TESTED_BY/COVERS (the confirmer's structural links); weak = MAY_* candidate
|
|
521
538
|
// edges. Hard is recorded before weak (graph.edges scanned first), so a test already
|
|
522
|
-
// linked hard is never downgraded
|
|
539
|
+
// linked hard is never downgraded, and a later weak dup of the same pair is a no-op —
|
|
540
|
+
// the hard flag can only ever originate from a genuine TESTED_BY/COVERS edge.
|
|
523
541
|
const add = (symId, testRel, hard, testName) => {
|
|
524
542
|
// A HARD TESTED_BY/COVERS edge is a real derivable test; admit it even when the symbol
|
|
525
543
|
// is not_entry_point_adjacent (relaxed shape-only guard). Weak MAY_* fan-out stays strict.
|
package/dist/local/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { parseArgs, collectSetupCommands } from "./cliArgs.js";
|
|
3
3
|
import { opAnalyze, opAiFlows, opAiLinks, opChanged, opCompare, opDoctor, opProofDoctor, opDynamicProof, opExplain, opGaps, opGenerate, opBehaviorCoverageHtml, opCoverageReport, opInit, opProveLoop, opRuntimeCoverage, opScore, opRecordRun, opRtm, opStats, opStatus, opUpdate, opSetModelDefault, opStart, getModelDefault, resolveDiffTargets, resolvePrCheckout, writeCompareReport } from "./operations.js";
|
|
4
4
|
import { dominantBlockReason } from "./viz/behaviorReportData.js";
|
|
5
|
+
import { coverageRevealLine } from "./viz/coverageReveal.js";
|
|
5
6
|
import { autoProve, isRoastSurvivor } from "./autoProve.js";
|
|
6
7
|
import { opRecipeDbSqljs } from "./recipe/dbSqljs.js";
|
|
7
8
|
import { runExportCli } from "./exportCli.js";
|
|
@@ -236,6 +237,11 @@ async function main() {
|
|
|
236
237
|
out(` skipped: ${skip.target_symbol ?? skip.title} — ${skip.reason}`);
|
|
237
238
|
}
|
|
238
239
|
out(` Runtime-covered: ${res.rtm.summary.runtime_covered}`);
|
|
240
|
+
// G6: same-denominator coverage-vs-proof reveal — renders only when runtime
|
|
241
|
+
// coverage was ingested; percentages share summary.total (never mixed scopes).
|
|
242
|
+
const reveal = coverageRevealLine(res.rtm.summary);
|
|
243
|
+
if (reveal)
|
|
244
|
+
out(` ${reveal}`);
|
|
239
245
|
out(` Statically Linked: ${res.rtm.summary.associated} (static test link, not dynamic proof)`);
|
|
240
246
|
out(` No integration signal: ${res.rtm.summary.no_link}`);
|
|
241
247
|
out(` AI-linked: ${res.ai_linked.behaviors} behavior(s), ${res.ai_linked.symbols} symbol(s), ${res.ai_linked.links} weak link(s) — not coverage`);
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* The graph is built directly by OrangePro; it does not depend on any
|
|
10
10
|
* third-party graph product or format.
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
// v2: Go method symbol ids are receiver-qualified (`sym:file.go#Recv.M`) — old
|
|
13
|
+
// graphs hold bare-name method ids and must force-rebuild (loadGraph hard-fails).
|
|
14
|
+
export const LOCAL_GRAPH_SCHEMA_VERSION = "orangepro.local_graph.v2";
|
|
13
15
|
/** Node kinds that map to behaviors/requirements for scoring + gaps + generation. */
|
|
14
16
|
export const BEHAVIOR_KINDS = new Set([
|
|
15
17
|
"Requirement",
|
package/dist/local/operations.js
CHANGED
|
@@ -273,11 +273,14 @@ function symbolTargetParts(symExtId) {
|
|
|
273
273
|
throw new Error(`Cannot derive dynamic proof target from symbol id: ${symExtId}`);
|
|
274
274
|
}
|
|
275
275
|
const [, file, symbolName] = match;
|
|
276
|
-
const
|
|
276
|
+
const segments = symbolName.split(".").filter(Boolean);
|
|
277
|
+
const method = segments.pop();
|
|
277
278
|
if (!file || !method) {
|
|
278
279
|
throw new Error(`Cannot derive dynamic proof target from symbol id: ${symExtId}`);
|
|
279
280
|
}
|
|
280
|
-
|
|
281
|
+
// The owner qualifier of a member id (TS `Class.method`, Go `Recv.M`). The Go
|
|
282
|
+
// lane passes it as --recv so the mutator matches the exact receiver.
|
|
283
|
+
return { file, method, ...(segments.length ? { memberQualifier: segments.join(".") } : {}) };
|
|
281
284
|
}
|
|
282
285
|
function assertProofTargetMatchesSymbol(opts, symbolTarget) {
|
|
283
286
|
if (opts.target_path !== undefined && opts.target_path !== "") {
|
|
@@ -560,8 +563,9 @@ export function opAnalyze(root, opts = {}, deps = defaultDeps()) {
|
|
|
560
563
|
? (!opts.suppressProgress && reportProgress("coverage: generating local runtime coverage before graph build", { current: 2, total: 4 }),
|
|
561
564
|
prepareRuntimeCoverage(scanRoot, { generate: true, timeoutMs: opts.coverageTimeoutMs, runner: deps.coverageRunner }))
|
|
562
565
|
: undefined;
|
|
563
|
-
|
|
564
|
-
|
|
566
|
+
// Idempotent for existing workspaces and also applies conservative migrations
|
|
567
|
+
// to untouched generated workspace files (for example .orangeproignore).
|
|
568
|
+
initWorkspace(root, now);
|
|
565
569
|
if (!opts.suppressProgress) {
|
|
566
570
|
reportProgress("analyze: parsing source and building deterministic graph", {
|
|
567
571
|
current: opts.generateCoverage ? 3 : 2,
|
|
@@ -1007,6 +1011,10 @@ export function opDynamicProof(root, opts, deps = defaultDeps()) {
|
|
|
1007
1011
|
// Slice 2: bind a runtime-named subtest's mutant failure to the exact assertion line.
|
|
1008
1012
|
if (opts.go_assertion_line !== undefined)
|
|
1009
1013
|
args.push("--go-assertion-line", String(opts.go_assertion_line));
|
|
1014
|
+
// Receiver-qualified method target (`sym:file.go#Recv.M`) → the mutator must
|
|
1015
|
+
// match the exact receiver, never a same-named decl on another type.
|
|
1016
|
+
if (symbolTarget.memberQualifier)
|
|
1017
|
+
args.push("--recv", symbolTarget.memberQualifier);
|
|
1010
1018
|
const run = (deps.dynamicProofRunner ?? defaultDynamicProofRunner)(args, {
|
|
1011
1019
|
cwd: goRoot,
|
|
1012
1020
|
scriptPath: dynamicProofSpikePathFor("go")
|
|
@@ -1458,6 +1466,28 @@ export function autoProveChangedScope(graph, changed, baseRef) {
|
|
|
1458
1466
|
});
|
|
1459
1467
|
return hasEligibleTarget ? meaningful : undefined; // no eligible provable target in scope → global top-5
|
|
1460
1468
|
}
|
|
1469
|
+
function writeStartStaticSnapshot(root, baseRef, warnings) {
|
|
1470
|
+
let behaviorCoveragePath;
|
|
1471
|
+
try {
|
|
1472
|
+
reportProgress("artifacts: writing static behavior view (proof still running)", { current: 4, total: 8 });
|
|
1473
|
+
behaviorCoveragePath = opBehaviorCoverageHtml(root, `${WORKSPACE_DIR}/behavior-coverage.html`, {
|
|
1474
|
+
attempted: 0,
|
|
1475
|
+
proven: 0,
|
|
1476
|
+
needsSetup: []
|
|
1477
|
+
}).behavior_coverage_path;
|
|
1478
|
+
}
|
|
1479
|
+
catch (error) {
|
|
1480
|
+
warnings.push(`static behavior view not written: ${error instanceof Error ? error.message : String(error)}`);
|
|
1481
|
+
}
|
|
1482
|
+
try {
|
|
1483
|
+
reportProgress("artifacts: writing static RTM (proof still running)", { current: 4, total: 8 });
|
|
1484
|
+
opRtm(root, { format: "md", baseRef, limit: START_RTM_LIMIT });
|
|
1485
|
+
}
|
|
1486
|
+
catch (error) {
|
|
1487
|
+
warnings.push(`static RTM not written: ${error instanceof Error ? error.message : String(error)}`);
|
|
1488
|
+
}
|
|
1489
|
+
return behaviorCoveragePath ? { behaviorCoveragePath } : {};
|
|
1490
|
+
}
|
|
1461
1491
|
export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
1462
1492
|
const providerOpts = startProviderOverride(root, opts);
|
|
1463
1493
|
const scanRoot = opts.source ? resolve(opts.source) : resolve(root);
|
|
@@ -1475,6 +1505,7 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1475
1505
|
}, deps);
|
|
1476
1506
|
const warnings = [...analyze.warnings];
|
|
1477
1507
|
reportProgress("start: deterministic graph is ready", { current: 4, total: 8 });
|
|
1508
|
+
const staticSnapshot = writeStartStaticSnapshot(root, opts.baseRef, warnings);
|
|
1478
1509
|
const providerConfigured = deps.aiProvider !== undefined || resolveProviderConfig(providerEnv, providerOpts) !== null;
|
|
1479
1510
|
let aiLinks = { status: "skipped", reason: "AI candidate links disabled for this run." };
|
|
1480
1511
|
if (opts.ai !== false) {
|
|
@@ -1611,7 +1642,7 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1611
1642
|
catch (error) {
|
|
1612
1643
|
warnings.push(`coverage report not written: ${error instanceof Error ? error.message : String(error)}`);
|
|
1613
1644
|
}
|
|
1614
|
-
let coverageHtml;
|
|
1645
|
+
let coverageHtml = staticSnapshot.behaviorCoveragePath;
|
|
1615
1646
|
try {
|
|
1616
1647
|
reportProgress("artifacts: writing behavior coverage view", { current: 7, total: 8 });
|
|
1617
1648
|
// Forward THIS-RUN dynamic-proof outcome so the report can name the dominant setup/runnability
|
package/dist/local/util/walk.js
CHANGED
|
@@ -66,20 +66,21 @@ export function loadIgnore(root) {
|
|
|
66
66
|
const line = raw.trim();
|
|
67
67
|
if (!line || line.startsWith("#") || line.startsWith("!"))
|
|
68
68
|
continue;
|
|
69
|
+
const rootAnchored = line.startsWith("/");
|
|
69
70
|
const cleaned = line.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
70
71
|
if (!cleaned)
|
|
71
72
|
continue;
|
|
72
|
-
if (!cleaned.includes("/") && !cleaned.includes("*")) {
|
|
73
|
+
if (!rootAnchored && !cleaned.includes("/") && !cleaned.includes("*")) {
|
|
73
74
|
names.add(cleaned);
|
|
74
75
|
}
|
|
75
76
|
else {
|
|
76
|
-
matchers.push(globToRegExp(cleaned));
|
|
77
|
+
matchers.push(globToRegExp(cleaned, rootAnchored));
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
return { names, matchers };
|
|
81
82
|
}
|
|
82
|
-
function globToRegExp(glob) {
|
|
83
|
+
function globToRegExp(glob, rootAnchored = false) {
|
|
83
84
|
// Use a plain-ASCII sentinel for `**` so the file stays text (no NUL bytes)
|
|
84
85
|
// and `*` substitution does not re-match the globstar.
|
|
85
86
|
const GLOBSTAR = "__ORANGEPRO_GLOBSTAR__";
|
|
@@ -90,7 +91,7 @@ function globToRegExp(glob) {
|
|
|
90
91
|
.split(GLOBSTAR)
|
|
91
92
|
.join(".*")
|
|
92
93
|
.replace(/\?/g, "[^/]");
|
|
93
|
-
return new RegExp(
|
|
94
|
+
return new RegExp(`${rootAnchored ? "^" : "(^|/)"}${escaped}(/|$)`);
|
|
94
95
|
}
|
|
95
96
|
function isIgnored(relPath, baseName, rules) {
|
|
96
97
|
if (rules.names.has(baseName))
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G6 — the coverage-vs-proof reveal line for the start summary.
|
|
3
|
+
*
|
|
4
|
+
* Compares ONLY same-scope numbers: both sides are RTM counts over the SAME
|
|
5
|
+
* denominator (`summary.total`). The proven side is `coverage_confirmed` — the
|
|
6
|
+
* IN-DENOMINATOR proven count — never `summary.proven`, which also counts
|
|
7
|
+
* off-denominator proofs (relaxed hard-edge targets below the entry-point bar)
|
|
8
|
+
* and can exceed `total` (a 5/3 = 167% render). The signature omits `proven`
|
|
9
|
+
* entirely so the off-denominator number cannot be passed by mistake.
|
|
10
|
+
*
|
|
11
|
+
* `runtime_covered` is the DISJOINT runtime tier — behaviors a repo coverage
|
|
12
|
+
* report executed that are NOT dynamically proven. Runtime coverage only proves
|
|
13
|
+
* EXECUTION, and the unproven side may simply be unattempted or unrunnable —
|
|
14
|
+
* the line says so and never blames the tests outright. Source-report
|
|
15
|
+
* line-coverage totals (lcov LF/LH etc.) are NOT retained by ingestion and are
|
|
16
|
+
* never invented here (spec G6 guardrail). Never implies proven SHOULD equal
|
|
17
|
+
* coverage — they measure different things.
|
|
18
|
+
*
|
|
19
|
+
* Null when no runtime coverage was ingested (nothing to reveal) or the
|
|
20
|
+
* denominator is empty.
|
|
21
|
+
*/
|
|
22
|
+
export function coverageRevealLine(summary) {
|
|
23
|
+
if (summary.runtime_covered <= 0 || summary.total <= 0)
|
|
24
|
+
return null;
|
|
25
|
+
const pct = (n) => Math.round((n / summary.total) * 100);
|
|
26
|
+
return (`Coverage vs proof: ${summary.runtime_covered}/${summary.total} behaviors are runtime-covered but not Dynamically Proven ` +
|
|
27
|
+
`(${pct(summary.runtime_covered)}%) vs ${summary.coverage_confirmed}/${summary.total} Dynamically Proven (${pct(summary.coverage_confirmed)}%) — ` +
|
|
28
|
+
`coverage only proves execution; the unproven side may be unattempted, blocked, or covered by tests that never assert these behaviors.`);
|
|
29
|
+
}
|
package/dist/local/workspace.js
CHANGED
|
@@ -21,7 +21,7 @@ export function workspaceInitialized(root) {
|
|
|
21
21
|
export function graphExists(root) {
|
|
22
22
|
return existsSync(workspacePaths(root).graphPath);
|
|
23
23
|
}
|
|
24
|
-
const
|
|
24
|
+
const ORANGEPROIGNORE_PREAMBLE = `# .orangeproignore — paths the OrangePro local proof kit should never read.
|
|
25
25
|
# Same spirit as .gitignore. Secrets and large assets are excluded by default.
|
|
26
26
|
*.env
|
|
27
27
|
*.pem
|
|
@@ -32,7 +32,8 @@ secrets/
|
|
|
32
32
|
|
|
33
33
|
# Product-denominator defaults: example/demo apps are useful references, but
|
|
34
34
|
# they usually should not count as product behavior coverage.
|
|
35
|
-
|
|
35
|
+
`;
|
|
36
|
+
export const LEGACY_ORANGEPROIGNORE_TEMPLATE = `${ORANGEPROIGNORE_PREAMBLE}examples/
|
|
36
37
|
example/
|
|
37
38
|
demos/
|
|
38
39
|
demo/
|
|
@@ -42,6 +43,16 @@ docs/examples/
|
|
|
42
43
|
docs/demo/
|
|
43
44
|
docs/demos/
|
|
44
45
|
`;
|
|
46
|
+
export const ORANGEPROIGNORE_TEMPLATE = `${ORANGEPROIGNORE_PREAMBLE}/examples/
|
|
47
|
+
/example/
|
|
48
|
+
/demos/
|
|
49
|
+
/demo/
|
|
50
|
+
/samples/
|
|
51
|
+
/sample/
|
|
52
|
+
/docs/examples/
|
|
53
|
+
/docs/demo/
|
|
54
|
+
/docs/demos/
|
|
55
|
+
`;
|
|
45
56
|
export function initWorkspace(root, now) {
|
|
46
57
|
const paths = workspacePaths(root);
|
|
47
58
|
mkdirSync(paths.dir, { recursive: true });
|
|
@@ -58,6 +69,18 @@ export function initWorkspace(root, now) {
|
|
|
58
69
|
if (!existsSync(ignorePath)) {
|
|
59
70
|
writeFileSync(ignorePath, ORANGEPROIGNORE_TEMPLATE, "utf8");
|
|
60
71
|
}
|
|
72
|
+
else {
|
|
73
|
+
// Upgrade only the untouched generated template. A user-edited ignore file
|
|
74
|
+
// is configuration and must never be rewritten implicitly.
|
|
75
|
+
try {
|
|
76
|
+
if (readFileSync(ignorePath, "utf8") === LEGACY_ORANGEPROIGNORE_TEMPLATE) {
|
|
77
|
+
writeFileSync(ignorePath, ORANGEPROIGNORE_TEMPLATE, "utf8");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// A read-only or transiently unavailable ignore file must not fail init.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
61
84
|
return { paths, config: loadConfig(paths) };
|
|
62
85
|
}
|
|
63
86
|
export function loadConfig(paths) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangepro/orangepro-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "OrangePro (`opro`) — a local-first, BYOK CLI + MCP server that builds an evidence graph from a local checkout, ingests runtime coverage, and generates grounded tests. Metadata-only exports; no source upload; generated tests stay local.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// go-dynamic-proof-spike.mjs — Go dynamic-proof MECHANISM (G-1).
|
|
3
3
|
//
|
|
4
|
-
// Proves ONE free function 0->1 on a single Go module by
|
|
4
|
+
// Proves ONE free function or receiver method 0->1 on a single Go module by
|
|
5
|
+
// mutation: it byte-copies
|
|
5
6
|
// the module into a hermetic sandbox, runs `go test -json` baseline, replaces the
|
|
6
7
|
// target function body with a signature-derived sentinel (via go-mutate.go), reruns
|
|
7
8
|
// the SAME test, and classifies. It emits a JSON verdict mirroring the TS/JS spike's
|
|
@@ -21,12 +22,13 @@
|
|
|
21
22
|
// assertion call (`t.Error`/`t.Errorf`, or testify `assert.`/`require.`), NOT a
|
|
22
23
|
// `t.Fatal`/`t.Fatalf`/`t.FailNow`/`t.SkipNow` hard-stop or a helper call. A build
|
|
23
24
|
// error, a panic, a t.Fatal precondition, a setup/helper failure, an unbindable
|
|
24
|
-
// failure, and an ambiguous
|
|
25
|
+
// failure, and an ambiguous or no-return name all classify as `unrunnable`,
|
|
25
26
|
// never `proven`. An equivalent-value mutation survives -> `associated_survived`.
|
|
26
27
|
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
28
|
+
// PRODUCT-WIRED: `opro` routes Go dynamic proof through this script (operations.ts
|
|
29
|
+
// dynamicProofSpikePathFor("go")). The script itself writes no graph edges or product
|
|
30
|
+
// artifacts — it emits a JSON verdict; the orchestrator is the sole interpreter and
|
|
31
|
+
// the only place proof is minted.
|
|
30
32
|
import { spawnSync } from "node:child_process";
|
|
31
33
|
import { cpSync, existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync } from "node:fs";
|
|
32
34
|
import { tmpdir } from "node:os";
|
|
@@ -39,11 +41,12 @@ function usage() {
|
|
|
39
41
|
return [
|
|
40
42
|
"Usage: node scripts/spikes/go-dynamic-proof-spike.mjs --root <module> --test-run <^TestName$> --target <rel.go> --func <name> [--json]",
|
|
41
43
|
"",
|
|
42
|
-
"Runs a Go baseline test, mutates the target free function body in an isolated byte-copy, reruns the same test, and classifies the result.",
|
|
44
|
+
"Runs a Go baseline test, mutates the target free function or method body in an isolated byte-copy, reruns the same test, and classifies the result.",
|
|
43
45
|
"--test-run is passed verbatim to `go test -run` and should anchor a single test, e.g. '^TestCompute$'.",
|
|
46
|
+
"--recv <T> (optional): receiver base type — mutate only `func (x T) <name>` / `func (x *T) <name>`, so a same-named method on another receiver (or a free function) can never be the mutation target.",
|
|
44
47
|
"--go-assertion-line <n> (optional): 1-based test-source line of the target's assertion. When set, the mutant's failure must bind to a frame at EXACTLY that line and subtest frames are considered — so a runtime-named subtest can prove while a sibling asserting elsewhere is refused.",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
48
|
+
"Scope: free functions and receiver methods. Without --recv the name must resolve to exactly ONE declaration in the target file; with --recv exactly one declaration on the selected base receiver must match. Ambiguity within the filtered receiver and generic receivers still fail closed. Equivalent-value mutations survive (associated_survived).",
|
|
49
|
+
"Product wiring: opro prove/auto-prove invokes this script for Go targets; it writes no graph edges or product artifacts itself — the caller interprets the JSON verdict."
|
|
47
50
|
].join("\n");
|
|
48
51
|
}
|
|
49
52
|
|
|
@@ -460,9 +463,9 @@ function redactSecrets(text) {
|
|
|
460
463
|
// Run the AST mutator (go run go-mutate.go). Because `go run` collapses any
|
|
461
464
|
// non-zero child status to 1, we classify on the MUTATE_ERROR:<code> marker the
|
|
462
465
|
// helper prints to stderr, not the exit code.
|
|
463
|
-
function mutateFunc({ targetAbs, func, mode, cacheRoot, timeoutMs }) {
|
|
466
|
+
function mutateFunc({ targetAbs, func, recv, mode, cacheRoot, timeoutMs }) {
|
|
464
467
|
const helper = path.join(path.dirname(fileURLToPath(import.meta.url)), "go-mutate.go");
|
|
465
|
-
const result = spawnSync(goBin(), ["run", helper, "--file", targetAbs, "--func", func, "--mode", mode], {
|
|
468
|
+
const result = spawnSync(goBin(), ["run", helper, "--file", targetAbs, "--func", func, ...(recv ? ["--recv", recv] : []), "--mode", mode], {
|
|
466
469
|
encoding: "utf8",
|
|
467
470
|
timeout: timeoutMs,
|
|
468
471
|
env: hermeticEnv(cacheRoot),
|
|
@@ -481,9 +484,9 @@ function mutateFunc({ targetAbs, func, mode, cacheRoot, timeoutMs }) {
|
|
|
481
484
|
|
|
482
485
|
function mutateErrorReason(code) {
|
|
483
486
|
switch (code) {
|
|
484
|
-
case 3: return "target
|
|
485
|
-
case 4: return "target free function was not found";
|
|
486
|
-
|
|
487
|
+
case 3: return "target name is ambiguous (more than one free function or method)";
|
|
488
|
+
case 4: return "target free function or method was not found";
|
|
489
|
+
// code 5 (method out of scope) is retired — methods are mutable now, never emitted.
|
|
487
490
|
case 6: return "target function has no return value (not mutable)";
|
|
488
491
|
default: return "mutation could not be applied";
|
|
489
492
|
}
|
|
@@ -565,6 +568,7 @@ function main() {
|
|
|
565
568
|
const mutation = mutateFunc({
|
|
566
569
|
targetAbs: path.join(mutantCopy.repoRoot, targetRel),
|
|
567
570
|
func: args.func,
|
|
571
|
+
recv: args.recv,
|
|
568
572
|
mode: args.mode,
|
|
569
573
|
cacheRoot: mutantCopy.tmpRoot,
|
|
570
574
|
timeoutMs
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
// go-mutate.go — AST-based body replacer for the Go dynamic-proof spike (G-1).
|
|
4
4
|
//
|
|
5
|
-
// Locates ONE free function `func Name(
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
5
|
+
// Locates ONE function declaration by exact name — a free function `func Name(...)`
|
|
6
|
+
// or a receiver method `func (r Recv) Name(...)` — and replaces its BODY with a
|
|
7
|
+
// signature-derived sentinel, then writes the mutated file. Invoked by
|
|
8
|
+
// go-dynamic-proof-spike.mjs (the product Go proof path); it writes ONLY the
|
|
9
|
+
// mutated file inside the sandbox copy — no graph or product artifacts. The
|
|
10
|
+
// name must resolve to exactly ONE declaration — in the whole file without
|
|
11
|
+
// --recv, or on the selected base receiver with --recv <T> (receiver-exact
|
|
12
|
+
// selection for receiver-qualified `Recv.M` targets — never the wrong decl,
|
|
13
|
+
// so A.M and B.M can coexist and still be individually mutable). Ambiguity
|
|
14
|
+
// within that filter fails(3); generic receivers are refused (not found).
|
|
10
15
|
//
|
|
11
16
|
// Modes:
|
|
12
17
|
// sentinel — replace body with a type-compatible, deliberately-wrong value
|
|
@@ -19,9 +24,10 @@
|
|
|
19
24
|
// Exit codes (distinct, so the Node orchestrator can classify precisely):
|
|
20
25
|
// 0 ok, mutated file written
|
|
21
26
|
// 2 usage / IO / parse error
|
|
22
|
-
// 3 ambiguous: more than one free
|
|
23
|
-
// 4 not found: no free function with that name
|
|
24
|
-
// 5
|
|
27
|
+
// 3 ambiguous: more than one declaration (free and/or method) with that name
|
|
28
|
+
// 4 not found: no free function or method with that name
|
|
29
|
+
// 5 RETIRED — was "method out of scope (G-2)" before methods became mutable;
|
|
30
|
+
// no longer emitted (kept so codes 3/4/6 stay stable for the orchestrator)
|
|
25
31
|
// 6 not mutable: the function has no return values (no signature-derived
|
|
26
32
|
// sentinel is possible) -> fail closed, never mutated
|
|
27
33
|
package main
|
|
@@ -51,7 +57,8 @@ func fail(code int, format string, args ...any) {
|
|
|
51
57
|
|
|
52
58
|
func main() {
|
|
53
59
|
file := flag.String("file", "", "path to the Go source file to mutate")
|
|
54
|
-
fn := flag.String("func", "", "exact name of the free function to mutate")
|
|
60
|
+
fn := flag.String("func", "", "exact name of the free function or method to mutate")
|
|
61
|
+
recv := flag.String("recv", "", "receiver base type name; when set, match only methods on this receiver")
|
|
55
62
|
out := flag.String("out", "", "path to write the mutated file (defaults to --file)")
|
|
56
63
|
mode := flag.String("mode", "sentinel", "sentinel | equivalent")
|
|
57
64
|
flag.Parse()
|
|
@@ -73,31 +80,47 @@ func main() {
|
|
|
73
80
|
fail(2, "parse error: %v", err)
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
// Collect BOTH free functions and methods named *fn. The proof lane only ever
|
|
84
|
+
// targets a method whose name is UNIQUE in its package (the analyzer's
|
|
85
|
+
// uniqueGoPackageSymbol refuses cross-file collisions before an edge is minted);
|
|
86
|
+
// this file-scoped count is the in-file backstop, so free+method or two-method
|
|
87
|
+
// collisions fail(3) as ambiguous — never a mislabeled mutation. A lone decl
|
|
88
|
+
// (free OR method) is mutated identically via the receiver-agnostic sentinel.
|
|
89
|
+
var matches []*ast.FuncDecl
|
|
78
90
|
for _, decl := range astFile.Decls {
|
|
79
91
|
fd, ok := decl.(*ast.FuncDecl)
|
|
80
92
|
if !ok || fd.Name == nil || fd.Name.Name != *fn {
|
|
81
93
|
continue
|
|
82
94
|
}
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
// Refuse generic receivers (r T[U]) — receiver base type is not a bare Ident.
|
|
96
|
+
if fd.Recv != nil && recvBaseIdent(fd) == nil {
|
|
85
97
|
continue
|
|
86
98
|
}
|
|
87
|
-
|
|
99
|
+
// Receiver-exact selection: when --recv is set, only a method on that base
|
|
100
|
+
// receiver type matches — a free function or another receiver never can, so
|
|
101
|
+
// a receiver-qualified target can never mutate the wrong declaration.
|
|
102
|
+
if *recv != "" {
|
|
103
|
+
if fd.Recv == nil {
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
if id := recvBaseIdent(fd); id == nil || id.Name != *recv {
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
matches = append(matches, fd)
|
|
88
111
|
}
|
|
89
112
|
|
|
90
|
-
if len(
|
|
91
|
-
fail(3, "ambiguous: %d
|
|
113
|
+
if len(matches) > 1 {
|
|
114
|
+
fail(3, "ambiguous: %d declarations named %q (free and/or methods)", len(matches), *fn)
|
|
92
115
|
}
|
|
93
|
-
if len(
|
|
94
|
-
if
|
|
95
|
-
fail(
|
|
116
|
+
if len(matches) == 0 {
|
|
117
|
+
if *recv != "" {
|
|
118
|
+
fail(4, "not found: no method named %q on receiver %q", *fn, *recv)
|
|
96
119
|
}
|
|
97
|
-
fail(4, "not found: no free function named %q", *fn)
|
|
120
|
+
fail(4, "not found: no free function or method named %q", *fn)
|
|
98
121
|
}
|
|
99
122
|
|
|
100
|
-
target :=
|
|
123
|
+
target := matches[0]
|
|
101
124
|
results := target.Type.Results
|
|
102
125
|
if results == nil || len(results.List) == 0 {
|
|
103
126
|
fail(6, "not mutable: %q has no return values; no signature-derived sentinel is possible", *fn)
|
|
@@ -160,6 +183,22 @@ func blankUnusedImports(f *ast.File) {
|
|
|
160
183
|
}
|
|
161
184
|
}
|
|
162
185
|
|
|
186
|
+
// recvBaseIdent returns the receiver type's base identifier for a method decl
|
|
187
|
+
// (unwrapping a pointer receiver `*T` to `T`), or nil for a generic/unsupported
|
|
188
|
+
// receiver. Used only to refuse generic receivers; value-vs-pointer is irrelevant
|
|
189
|
+
// to body mutation (Go auto-(de)refs at the call site).
|
|
190
|
+
func recvBaseIdent(fd *ast.FuncDecl) *ast.Ident {
|
|
191
|
+
if fd.Recv == nil || len(fd.Recv.List) != 1 {
|
|
192
|
+
return nil
|
|
193
|
+
}
|
|
194
|
+
t := fd.Recv.List[0].Type
|
|
195
|
+
if star, ok := t.(*ast.StarExpr); ok {
|
|
196
|
+
t = star.X
|
|
197
|
+
}
|
|
198
|
+
id, _ := t.(*ast.Ident)
|
|
199
|
+
return id
|
|
200
|
+
}
|
|
201
|
+
|
|
163
202
|
// sentinelBody builds `{ return <zero>, <zero>, ... }` matching the function's
|
|
164
203
|
// result signature so the mutant COMPILES. Zero values are type-derived and
|
|
165
204
|
// deliberately wrong for any function whose real return is non-zero; when a
|
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
// classify as `unrunnable`, never `proven`. An equivalent-value mutation survives ->
|
|
22
22
|
// `associated_survived`. An ambiguous method name is refused -> `unrunnable`.
|
|
23
23
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// PRODUCT-WIRED: `opro` routes Java dynamic proof through this script (operations.ts
|
|
25
|
+
// dynamicProofSpikePathFor("java")). The script itself writes no graph edges or
|
|
26
|
+
// product artifacts — it emits a JSON verdict; the orchestrator is the sole
|
|
27
|
+
// interpreter and the only place proof is minted. Pin: Maven + Surefire + JUnit 5
|
|
28
|
+
// (the Spring Boot default). Gradle / JUnit4 are later parsers.
|
|
27
29
|
import { spawnSync } from "node:child_process";
|
|
28
30
|
import { cpSync, existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
29
31
|
import { tmpdir } from "node:os";
|
|
@@ -40,7 +42,7 @@ function usage() {
|
|
|
40
42
|
"",
|
|
41
43
|
"Runs ONE Surefire target test on a byte-copy of a single-module Maven + JUnit 5 project, mutates the target method body via a signature-derived sentinel, reruns the SAME test, and classifies from the structured surefire report.",
|
|
42
44
|
"J-1 scope: SIMPLEST SHAPE ONLY — a concrete non-void return, a single top-level return, no generics, no overloads. Equivalent-value mutations survive (associated_survived). Ambiguous names are refused (unrunnable).",
|
|
43
|
-
"
|
|
45
|
+
"Product wiring: opro prove/auto-prove invokes this script for Java targets; it writes no graph edges or product artifacts itself — the caller interprets the JSON verdict."
|
|
44
46
|
].join("\n");
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// by exact name via tree-sitter Java (the SAME grammar the static layer already
|
|
6
6
|
// uses — tree-sitter-wasms + web-tree-sitter, no new dependency) and replaces its
|
|
7
7
|
// BODY with a signature-derived, type-compatible sentinel by splicing the body
|
|
8
|
-
// block's byte range.
|
|
9
|
-
//
|
|
8
|
+
// block's byte range. Invoked by java-dynamic-proof-spike.mjs (the product Java
|
|
9
|
+
// proof path); it writes ONLY the mutated file inside the sandbox copy — no graph
|
|
10
|
+
// or product artifacts.
|
|
10
11
|
//
|
|
11
12
|
// J-1 scope is the SIMPLEST SHAPE ONLY: a concrete non-void, non-type-variable
|
|
12
13
|
// return type, EXACTLY one top-level `return <expr>;` (no nested return in an
|