@orangepro/orangepro-mcp 0.2.1 → 0.2.3
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 +36 -3
- package/dist/local/analyze/treeSitter/engine.js +60 -1
- package/dist/local/cli.js +1 -1
- package/dist/local/flows/llmFlowDiscovery.js +16 -5
- package/dist/local/mcp.js +1 -1
- package/dist/local/operations.js +33 -1
- package/dist/local/viz/behaviorReportData.js +2 -3
- package/dist/local/viz/behaviorReportHtml.js +9 -10
- package/package.json +6 -2
- package/scripts/spikes/go-mutate.go +43 -0
package/README.md
CHANGED
|
@@ -41,7 +41,38 @@ cd orangepro-mcp && npm ci && npm run build && npm link
|
|
|
41
41
|
|
|
42
42
|
OrangePro runs as an MCP server. Any MCP-compatible agent (Cursor, Claude Code, Codex, Copilot, OpenCode) can drive it.
|
|
43
43
|
|
|
44
|
-
###
|
|
44
|
+
### Quick agent setup
|
|
45
|
+
|
|
46
|
+
If you already have `opro` on your PATH, print the exact config for your client:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
opro agent --client codex
|
|
50
|
+
opro agent --client claude-code
|
|
51
|
+
opro agent --client cursor
|
|
52
|
+
opro agent --client opencode
|
|
53
|
+
opro agent --client generic
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
No global install is required. These commands use the published package:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Codex
|
|
60
|
+
npx -y @orangepro/mcp-server@latest agent --client codex
|
|
61
|
+
|
|
62
|
+
# Claude Code
|
|
63
|
+
npx -y @orangepro/mcp-server@latest agent --client claude-code
|
|
64
|
+
|
|
65
|
+
# Cursor
|
|
66
|
+
npx -y @orangepro/mcp-server@latest agent --client cursor
|
|
67
|
+
|
|
68
|
+
# OpenCode
|
|
69
|
+
npx -y @orangepro/mcp-server@latest agent --client opencode
|
|
70
|
+
|
|
71
|
+
# Generic MCP clients, including VS Code/Copilot-style MCP settings
|
|
72
|
+
npx -y @orangepro/mcp-server@latest agent --client generic
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Manual MCP config
|
|
45
76
|
|
|
46
77
|
Add to your client's MCP config:
|
|
47
78
|
|
|
@@ -57,11 +88,13 @@ Add to your client's MCP config:
|
|
|
57
88
|
```
|
|
58
89
|
|
|
59
90
|
| Client | Config location |
|
|
91
|
+
| --- | --- |
|
|
60
92
|
|--------|----------------|
|
|
61
93
|
| Claude Code | `.mcp.json` or `~/.claude.json` |
|
|
62
94
|
| Cursor | `~/.cursor/mcp.json` or Settings → MCP |
|
|
63
|
-
| Codex |
|
|
64
|
-
| VS Code / Copilot | MCP settings |
|
|
95
|
+
| Codex | Config printed by `opro agent --client codex` or `npx -y @orangepro/mcp-server@latest agent --client codex` |
|
|
96
|
+
| VS Code / Copilot | MCP settings; use the `generic` config if your client accepts raw MCP server JSON |
|
|
97
|
+
| OpenCode | Config printed by `opro agent --client opencode` |
|
|
65
98
|
|
|
66
99
|
### The workflow
|
|
67
100
|
|
|
@@ -766,6 +766,17 @@ function goAssertionCalls(node, assertLocals, dotAssertMethods, shadowed) {
|
|
|
766
766
|
return Boolean(fn?.type === "identifier" && !shadowed.has(fn.text) && dotAssertMethods.has(fn.text));
|
|
767
767
|
});
|
|
768
768
|
}
|
|
769
|
+
function goAssertionIdentifierArgs(assertion, testingParams) {
|
|
770
|
+
const sel = callFunctionSelector(assertion);
|
|
771
|
+
const name = sel?.name ?? assertion.childForFieldName("function")?.text;
|
|
772
|
+
const args = namedChildren(assertion.childForFieldName("arguments") ?? assertion).filter((n) => n.type !== "comment");
|
|
773
|
+
if (!name || args.length < 2)
|
|
774
|
+
return [];
|
|
775
|
+
if (!testingParams.has(args[0]?.text ?? ""))
|
|
776
|
+
return [];
|
|
777
|
+
const slots = name === "Equal" || name === "NotEqual" ? [args[1], args[2]] : [args[1]];
|
|
778
|
+
return slots.filter((n) => n?.type === "identifier").map((n) => n.text);
|
|
779
|
+
}
|
|
769
780
|
function goAssertionSubject(assertion, testingParams) {
|
|
770
781
|
const sel = callFunctionSelector(assertion);
|
|
771
782
|
const name = sel?.name ?? assertion.childForFieldName("function")?.text;
|
|
@@ -862,9 +873,36 @@ function extractGoProofCalls(root, imports) {
|
|
|
862
873
|
}
|
|
863
874
|
};
|
|
864
875
|
const processBlock = (block, testName, testingParams, shadowed) => {
|
|
876
|
+
// ONE-HOP block-local dataflow: a standalone `x, err := F(...)` statement is
|
|
877
|
+
// credited when a LATER statement in the SAME block checks a declared name
|
|
878
|
+
// (if-fail condition or assert subject). This covers the dominant real-Go
|
|
879
|
+
// idiom the if-initializer shape misses. Discipline kept: single product
|
|
880
|
+
// call per statement (goShortVarCalls), last write wins, plain reassignment
|
|
881
|
+
// invalidates, never crosses block boundaries. Metadata only — the dynamic
|
|
882
|
+
// oracle still re-verifies every edge before anything is Proven.
|
|
883
|
+
const pending = new Map();
|
|
884
|
+
// Deferred pending credits: witness lines are collected per call and flushed
|
|
885
|
+
// after the block — one witness keeps its exact line (subtest binding),
|
|
886
|
+
// several witnesses drop the line (the oracle's frame-line gate must never
|
|
887
|
+
// refuse a real kill firing at a sibling check).
|
|
888
|
+
const pendingHits = new Map();
|
|
889
|
+
const hitPending = (assertion, calls, line) => {
|
|
890
|
+
for (const c of calls) {
|
|
891
|
+
const key = `${assertion}|${c.qualifier ?? ""}|${c.callee}`;
|
|
892
|
+
const hit = pendingHits.get(key) ?? { assertion, calls: [c], lines: new Set() };
|
|
893
|
+
if (line !== undefined)
|
|
894
|
+
hit.lines.add(line);
|
|
895
|
+
pendingHits.set(key, hit);
|
|
896
|
+
}
|
|
897
|
+
};
|
|
865
898
|
for (const stmt of blockStatements(block)) {
|
|
866
899
|
for (const assertion of goAssertionCalls(stmt, assertLocals, dotAssertMethods, shadowed)) {
|
|
867
|
-
|
|
900
|
+
const subject = goAssertionSubject(assertion, testingParams);
|
|
901
|
+
add(testName, shadowed, "assert_helper", singleGoProductCallIn(subject), assertion.startPosition.row + 1);
|
|
902
|
+
for (const argName of goAssertionIdentifierArgs(assertion, testingParams)) {
|
|
903
|
+
if (pending.has(argName))
|
|
904
|
+
hitPending("assert_helper", pending.get(argName), assertion.startPosition.row + 1);
|
|
905
|
+
}
|
|
868
906
|
}
|
|
869
907
|
if (stmt.type === "if_statement" && hasGoTestingFailure(stmt.childForFieldName("consequence"), testingParams)) {
|
|
870
908
|
const condition = stmt.childForFieldName("condition");
|
|
@@ -876,6 +914,10 @@ function extractGoProofCalls(root, imports) {
|
|
|
876
914
|
const initCalls = init ? goShortVarCalls(init) : null;
|
|
877
915
|
if (initCalls && containsIdentifier(condition, initCalls.names))
|
|
878
916
|
add(testName, shadowed, "testing_fail", initCalls.calls, failLine);
|
|
917
|
+
for (const [name, calls] of pending) {
|
|
918
|
+
if (containsIdentifier(condition, new Set([name])))
|
|
919
|
+
hitPending("testing_fail", calls, failLine);
|
|
920
|
+
}
|
|
879
921
|
}
|
|
880
922
|
for (const child of namedChildren(stmt)) {
|
|
881
923
|
if (child.type === "block")
|
|
@@ -886,6 +928,23 @@ function extractGoProofCalls(root, imports) {
|
|
|
886
928
|
const subTestName = subtest.subName ? `${testName}/${subtest.subName}` : testName;
|
|
887
929
|
processBlock(subtest.body, subTestName, subtest.testingParams, shadowed);
|
|
888
930
|
}
|
|
931
|
+
// Record declarations AFTER uses: a declaration is never its own check.
|
|
932
|
+
const sv = stmt.type === "short_var_declaration" ? goShortVarCalls(stmt) : null;
|
|
933
|
+
if (sv) {
|
|
934
|
+
for (const name of sv.names)
|
|
935
|
+
pending.set(name, sv.calls);
|
|
936
|
+
}
|
|
937
|
+
else if (stmt.type === "assignment_statement") {
|
|
938
|
+
// Plain reassignment kills the binding — the checked value is no longer F's.
|
|
939
|
+
const reassigned = new Set();
|
|
940
|
+
collectNames(stmt.childForFieldName("left") ?? stmt.namedChild(0), reassigned);
|
|
941
|
+
for (const name of reassigned)
|
|
942
|
+
pending.delete(name);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
for (const hit of pendingHits.values()) {
|
|
946
|
+
const line = hit.lines.size === 1 ? [...hit.lines][0] : undefined;
|
|
947
|
+
add(testName, shadowed, hit.assertion, hit.calls, line);
|
|
889
948
|
}
|
|
890
949
|
};
|
|
891
950
|
const processSuiteBlock = (block, testName, receiver, shadowed) => {
|
package/dist/local/cli.js
CHANGED
|
@@ -106,7 +106,7 @@ Usage:
|
|
|
106
106
|
opro ai-links [--all] [--apply] [--provider openai|anthropic|ollama] [--model <name>] [--max-behaviors <n>] [--symbols-per-behavior <n>] [--max-prompt-tokens <n>] [--json]
|
|
107
107
|
# opt-in AI lane: stage weak candidate behavior↔code links in .orangepro/ai/links.json; --apply merges them into candidate_edges only
|
|
108
108
|
opro ai-flows [--apply] [--provider openai|anthropic|ollama] [--model <name>] [--json]
|
|
109
|
-
# opt-in AI lane: stage candidate behavior flows (closed anchor set) in .orangepro/
|
|
109
|
+
# opt-in AI lane: stage candidate behavior flows (closed anchor set) in .orangepro/flows.json; --apply stores them under analysis.candidate_flows only — a verify-these worklist, never evidence
|
|
110
110
|
opro generate [--target REQ-001] [--base <ref>] [--pr <n> [--yes]] [--changed] [--framework playwright] [--limit 3] [--prompt-version v2|v5] [--provider openai|anthropic|ollama|deterministic] [--model <name>] [--single [--raw]] [--background] [--json]
|
|
111
111
|
# default: A/B both arms (prompt-only vs Local KG, same model) scored side by side + writes a fresh report; --single generates one arm only
|
|
112
112
|
# --base <ref>: NON-MUTATING default for PR/branch review — generate only for the behaviors the diff vs <ref> touches (e.g. --base main); read-only \`git diff\`, no checkout
|
|
@@ -19,8 +19,20 @@ const MAX_PROMPT_ENTRIES = 60;
|
|
|
19
19
|
const MAX_PROMPT_SYMBOLS = 300;
|
|
20
20
|
const MAX_COMPLETION_TOKENS = 3000;
|
|
21
21
|
export function aiFlowsPath(root) {
|
|
22
|
+
return join(workspacePaths(root).dir, "flows.json");
|
|
23
|
+
}
|
|
24
|
+
function legacyAiFlowsPath(root) {
|
|
22
25
|
return join(workspacePaths(root).dir, "ai", "flows.json");
|
|
23
26
|
}
|
|
27
|
+
function readAiFlowsArtifact(root) {
|
|
28
|
+
const path = aiFlowsPath(root);
|
|
29
|
+
const current = readArtifact(path);
|
|
30
|
+
if (current.artifact || current.invalid)
|
|
31
|
+
return { ...current, path };
|
|
32
|
+
const legacy = legacyAiFlowsPath(root);
|
|
33
|
+
const fallback = readArtifact(legacy);
|
|
34
|
+
return { ...fallback, path: fallback.artifact || fallback.invalid ? legacy : path };
|
|
35
|
+
}
|
|
24
36
|
function buildAnchorContext(graph) {
|
|
25
37
|
const entries = dedupeEntries([
|
|
26
38
|
...endpointEntries(graph.nodes, graph.edges),
|
|
@@ -210,12 +222,12 @@ export async function generateAiFlows(root, graph, provider, clock) {
|
|
|
210
222
|
node_set_hash: nodeSetHash
|
|
211
223
|
}));
|
|
212
224
|
const path = aiFlowsPath(root);
|
|
213
|
-
const cached =
|
|
225
|
+
const cached = readAiFlowsArtifact(root);
|
|
214
226
|
if (cached.artifact?.cache_key === cacheKey) {
|
|
215
227
|
const hit = cached.artifact;
|
|
216
228
|
return {
|
|
217
229
|
mode: "generate",
|
|
218
|
-
ai_flows_path: path,
|
|
230
|
+
ai_flows_path: cached.path,
|
|
219
231
|
cache_hit: true,
|
|
220
232
|
model_provider: hit.model_provider,
|
|
221
233
|
model_name: hit.model_name,
|
|
@@ -306,8 +318,7 @@ export async function generateAiFlows(root, graph, provider, clock) {
|
|
|
306
318
|
* edges/candidate_edges/nodes/analysis.flows.
|
|
307
319
|
*/
|
|
308
320
|
export function applyAiFlows(root, graph) {
|
|
309
|
-
const path =
|
|
310
|
-
const { artifact, invalid } = readArtifact(path);
|
|
321
|
+
const { path, artifact, invalid } = readAiFlowsArtifact(root);
|
|
311
322
|
if (invalid) {
|
|
312
323
|
throw new Error(`AI flows artifact at ${path} is invalid or corrupted; re-run \`opro ai-flows\` to regenerate it.`);
|
|
313
324
|
}
|
|
@@ -413,7 +424,7 @@ function toCandidateFlow(flow, entriesById, knownEdges, provenance) {
|
|
|
413
424
|
...(flow.rationale ? { rationale: flow.rationale } : {}),
|
|
414
425
|
provenance: {
|
|
415
426
|
source_scope_id: `ai:${provenance.cache_key}`,
|
|
416
|
-
source_ref: ".orangepro/
|
|
427
|
+
source_ref: ".orangepro/flows.json",
|
|
417
428
|
detector: "ai_flows",
|
|
418
429
|
model_provider: provenance.model_provider,
|
|
419
430
|
model_name: provenance.model_name,
|
package/dist/local/mcp.js
CHANGED
|
@@ -407,7 +407,7 @@ export function createLocalServer() {
|
|
|
407
407
|
});
|
|
408
408
|
server.registerTool("orangepro_ai_flows", {
|
|
409
409
|
title: "Stage/apply AI candidate flows",
|
|
410
|
-
description: "Opt-in AI lane: propose candidate behavior-flow chains over existing deterministic entry and CodeSymbol ids. Generate writes .orangepro/
|
|
410
|
+
description: "Opt-in AI lane: propose candidate behavior-flow chains over existing deterministic entry and CodeSymbol ids. Generate writes .orangepro/flows.json only; apply=true stores survivors under analysis.candidate_flows. Candidate flows are a verify-these worklist and never affect Proven, deterministic flow counts, tiers, or coverage.",
|
|
411
411
|
inputSchema: {
|
|
412
412
|
...Workspace,
|
|
413
413
|
apply: z.boolean().optional().describe("Apply staged flows into analysis.candidate_flows. Default false stages flows only."),
|
package/dist/local/operations.js
CHANGED
|
@@ -1560,6 +1560,36 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1560
1560
|
};
|
|
1561
1561
|
warnings.push(`auto-prove skipped: ${reason}`);
|
|
1562
1562
|
}
|
|
1563
|
+
if (!opts.noAuto && providerConfigured && opts.ai !== false) {
|
|
1564
|
+
try {
|
|
1565
|
+
const graphForGeneration = loadGraph(workspacePaths(root).graphPath);
|
|
1566
|
+
const generatedTargets = new Set((graphForGeneration.generated_tests ?? []).map((t) => t.target_symbol_external_id).filter((id) => Boolean(id)));
|
|
1567
|
+
const targetIds = rankRiskGaps(graphForGeneration, { repoRoot: root, limit: START_GENERATE_RISK_LIMIT })
|
|
1568
|
+
.map((gap) => gap.id)
|
|
1569
|
+
.filter((id) => !generatedTargets.has(id));
|
|
1570
|
+
if (targetIds.length) {
|
|
1571
|
+
reportProgress(`generate: drafting tests for top ${targetIds.length} risk target(s)`, { current: 6, total: 8 });
|
|
1572
|
+
let accepted = 0;
|
|
1573
|
+
for (let i = 0; i < targetIds.length; i += START_GENERATE_BATCH_LIMIT) {
|
|
1574
|
+
const batch = targetIds.slice(i, i + START_GENERATE_BATCH_LIMIT);
|
|
1575
|
+
const generated = await opGenerate(root, {
|
|
1576
|
+
...providerOpts,
|
|
1577
|
+
target_ids: batch,
|
|
1578
|
+
limit: batch.length,
|
|
1579
|
+
prompt_version: opts.promptVersion ?? "v5"
|
|
1580
|
+
}, providerDeps);
|
|
1581
|
+
accepted += generated.generated_tests.length;
|
|
1582
|
+
warnings.push(...generated.warnings.map((w) => `generate: ${w}`));
|
|
1583
|
+
}
|
|
1584
|
+
if (accepted === 0)
|
|
1585
|
+
warnings.push("generate: provider returned no accepted tests for the top risk targets.");
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
catch (err) {
|
|
1589
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1590
|
+
warnings.push(`generate skipped: ${reason}`);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1563
1593
|
// G1: persist the distilled, already-redacted attempt classifications so
|
|
1564
1594
|
// `opro doctor --proof` and standalone report regens can explain blockers
|
|
1565
1595
|
// after this process exits. Sidecar only — never read by the oracle, RTM,
|
|
@@ -1680,6 +1710,8 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1680
1710
|
}
|
|
1681
1711
|
const NO_PROVIDER_MESSAGE = 'No model provider configured. Set OPENAI_API_KEY (or OLLAMA_BASE_URL / ANTHROPIC_API_KEY) in your shell environment or a .env.provider.local file to generate with your own model, or pass provider="deterministic" (or set ORANGEPRO_ALLOW_DETERMINISTIC=1) to use the offline deterministic stand-in. No tests were generated.';
|
|
1682
1712
|
const START_RTM_LIMIT = 500;
|
|
1713
|
+
const START_GENERATE_RISK_LIMIT = 20;
|
|
1714
|
+
const START_GENERATE_BATCH_LIMIT = 5;
|
|
1683
1715
|
const EMPTY_EVIDENCE_SUMMARY = {
|
|
1684
1716
|
tests: 0,
|
|
1685
1717
|
tests_with_proof: 0,
|
|
@@ -1711,7 +1743,7 @@ export async function opGenerate(root, opts = {}, deps = defaultDeps()) {
|
|
|
1711
1743
|
// deterministic stand-in is opt-in only; otherwise return setup guidance
|
|
1712
1744
|
// instead of silently degrading.
|
|
1713
1745
|
const providerEnv = loadProviderEnv([root], deps.env);
|
|
1714
|
-
const provider = resolveGenerationProvider(providerEnv, opts);
|
|
1746
|
+
const provider = deps.aiProvider ?? resolveGenerationProvider(providerEnv, opts);
|
|
1715
1747
|
if (!provider) {
|
|
1716
1748
|
return {
|
|
1717
1749
|
run_id: null,
|
|
@@ -375,7 +375,6 @@ function riskRows(risks, graph) {
|
|
|
375
375
|
firstRowForFile.set(r.file, r.id);
|
|
376
376
|
return risks.map((risk, idx) => {
|
|
377
377
|
const methodMatch = risk.title.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i);
|
|
378
|
-
const pathMatch = risk.file.match(/\/api\/(.+)$/);
|
|
379
378
|
const tags = [];
|
|
380
379
|
const bucket = riskBucket(risk.risk_score);
|
|
381
380
|
if (bucket)
|
|
@@ -394,8 +393,8 @@ function riskRows(risks, graph) {
|
|
|
394
393
|
applicableCategories: [...new Set(generatedTests.map((t) => t.concern).filter((c) => Boolean(c)))]
|
|
395
394
|
};
|
|
396
395
|
})(),
|
|
397
|
-
verb: methodMatch?.[1]?.toUpperCase() ??
|
|
398
|
-
path: methodMatch?.[2] ??
|
|
396
|
+
verb: methodMatch?.[1]?.toUpperCase() ?? "BEHAVIOR",
|
|
397
|
+
path: methodMatch?.[2] ?? risk.title,
|
|
399
398
|
desc: risk.reasons.join(" · "),
|
|
400
399
|
tags,
|
|
401
400
|
todo: "Write an integration or behavior test that calls this behavior and asserts the observable outcome."
|
|
@@ -167,7 +167,6 @@ nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
|
|
|
167
167
|
.risk-rank{font-size:10px;color:var(--orange);font-weight:700;margin-bottom:3px}
|
|
168
168
|
.risk-ep{font-family:var(--mono);font-size:13px;margin:0 0 6px}
|
|
169
169
|
.risk-ep .v{color:var(--green);font-weight:700}
|
|
170
|
-
.risk-desc{font-size:12px;color:var(--muted);margin:0 0 8px;max-width:72ch}
|
|
171
170
|
.risk-tags{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:8px}
|
|
172
171
|
.todo{background:var(--gbg);border:1px solid var(--gbd);border-radius:6px;padding:8px 11px;font-size:11.5px;color:var(--ink2)}
|
|
173
172
|
|
|
@@ -471,8 +470,9 @@ if(cf&&cf.flows.length){
|
|
|
471
470
|
}
|
|
472
471
|
|
|
473
472
|
// risks + generated test samples
|
|
474
|
-
let activeRiskFilter="all";
|
|
475
473
|
const riskList=$("#risk-list"),riskTools=$("#risk-tools");
|
|
474
|
+
const generatedRiskCount=D.risks.filter(r=>r.generatedTests&&r.generatedTests.length).length;
|
|
475
|
+
let activeRiskFilter=generatedRiskCount?"generated":"all";
|
|
476
476
|
function riskMatchesFilter(r){
|
|
477
477
|
const hasGenerated=Boolean(r.generatedTests&&r.generatedTests.length);
|
|
478
478
|
if(activeRiskFilter==="generated")return hasGenerated;
|
|
@@ -482,7 +482,7 @@ function riskMatchesFilter(r){
|
|
|
482
482
|
function renderRiskFilters(){
|
|
483
483
|
const options=[
|
|
484
484
|
["all","All",D.risks.length],
|
|
485
|
-
["generated","
|
|
485
|
+
["generated","Flows with tests",generatedRiskCount],
|
|
486
486
|
["missing","No generated tests",D.risks.filter(r=>!(r.generatedTests&&r.generatedTests.length)).length]
|
|
487
487
|
];
|
|
488
488
|
riskTools.innerHTML=options.map(([key,label,count])=>\`<button class="risk-filter" type="button" data-risk-filter="\${key}" aria-pressed="\${key===activeRiskFilter}">\${label} <span class="gc">\${count}</span></button>\`).join("");
|
|
@@ -513,7 +513,6 @@ function riskCardHtml(r){
|
|
|
513
513
|
}
|
|
514
514
|
return \`<div class="risk-rank">#\${r.rank}</div>
|
|
515
515
|
<div class="risk-ep"><span class="v">\${esc(r.verb)}</span> \${esc(r.path)}</div>
|
|
516
|
-
<div class="risk-desc">\${esc(r.desc)}</div>
|
|
517
516
|
<div class="risk-tags">\${tags}</div>
|
|
518
517
|
<div class="todo">\${esc(r.todo)}</div>\${testsHtml}\${catHtml}\`;
|
|
519
518
|
}
|
|
@@ -521,16 +520,16 @@ function renderRisks(){
|
|
|
521
520
|
riskList.innerHTML="";
|
|
522
521
|
D.risks.filter(riskMatchesFilter).forEach(r=>riskList.append(el("div","risk-card",riskCardHtml(r))));
|
|
523
522
|
if(activeRiskFilter==="all"&&D.generatedTotal){
|
|
524
|
-
const
|
|
525
|
-
const remainingRiskFlows=Math.max(0,D.risks.length-
|
|
523
|
+
const hiddenGeneratedFlows=Math.max(0,generatedRiskCount-D.risks.filter(riskMatchesFilter).length);
|
|
524
|
+
const remainingRiskFlows=Math.max(0,D.risks.length-generatedRiskCount);
|
|
526
525
|
riskList.append(el("div","paywall",
|
|
527
|
-
|
|
528
|
-
? \`<div class="paywall-num">\${
|
|
529
|
-
<div class="paywall-txt">OrangePro
|
|
526
|
+
hiddenGeneratedFlows
|
|
527
|
+
? \`<div class="paywall-num">\${hiddenGeneratedFlows} more flows with tests</div>
|
|
528
|
+
<div class="paywall-txt">OrangePro accepted \${D.generatedTotal} runnable generated test\${D.generatedTotal===1?"":"s"} across \${generatedRiskCount} high-risk flow\${generatedRiskCount===1?"":"s"}. Use the “Flows with tests” filter to review them first.</div>
|
|
530
529
|
<a class="paywall-btn" href="https://app.orangepro.ai" target="_blank">View all on OrangePro Platform →</a>\`
|
|
531
530
|
: remainingRiskFlows
|
|
532
531
|
? \`<div class="paywall-num">\${remainingRiskFlows} high-risk flows left</div>
|
|
533
|
-
<div class="paywall-txt">The local MCP accepted \${D.generatedTotal} runnable generated test\${D.generatedTotal===1?"":"s"}
|
|
532
|
+
<div class="paywall-txt">The local MCP accepted \${D.generatedTotal} runnable generated test\${D.generatedTotal===1?"":"s"} across \${generatedRiskCount} high-risk flow\${generatedRiskCount===1?"":"s"}. Generate the remaining high-risk flow tests on OrangePro Platform.</div>
|
|
534
533
|
<a class="paywall-btn" href="https://app.orangepro.ai" target="_blank">Generate remaining tests on Platform →</a>\`
|
|
535
534
|
: \`<div class="paywall-num">All generated tests are shown</div>
|
|
536
535
|
<div class="paywall-txt">OrangePro generated tests for every high-risk flow in this report, and every generated test is visible here.</div>\`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangepro/orangepro-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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",
|
|
@@ -85,7 +85,11 @@
|
|
|
85
85
|
"test:integration:go:auto:slow": "OPRO_SLOW_GO_AUTO=1 vitest run tests/local/autoProveGo.test.ts --pool=forks --poolOptions.forks.maxForks=1",
|
|
86
86
|
"test:integration:java:1": "vitest run tests/local/javaProveIntegration.test.ts -t \"^(?!.*(no-false-Proven|mints the same Proven)).*\" --pool=forks --poolOptions.forks.maxForks=1",
|
|
87
87
|
"test:integration:java:2": "vitest run tests/local/javaProveIntegration.test.ts -t \"no-false-Proven|mints the same Proven\" --pool=forks --poolOptions.forks.maxForks=1",
|
|
88
|
-
"test:integration:java:3": "
|
|
88
|
+
"test:integration:java:3": "npm run test:integration:java:3:unit && npm run test:integration:java:3:prove && npm run test:integration:java:3:survivor && npm run test:integration:java:3:refused",
|
|
89
|
+
"test:integration:java:3:unit": "vitest run tests/local/autoProveJava.test.ts -t \"javaTestForTarget|isEligibleProvableTarget\" --pool=forks --poolOptions.forks.maxForks=1",
|
|
90
|
+
"test:integration:java:3:prove": "vitest run tests/local/autoProveJava.test.ts -t \"mints DP\" --pool=forks --poolOptions.forks.maxForks=1",
|
|
91
|
+
"test:integration:java:3:survivor": "vitest run tests/local/autoProveJava.test.ts -t \"equivalent survivor\" --pool=forks --poolOptions.forks.maxForks=1",
|
|
92
|
+
"test:integration:java:3:refused": "vitest run tests/local/autoProveJava.test.ts -t \"refused-shape\" --pool=forks --poolOptions.forks.maxForks=1",
|
|
89
93
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
90
94
|
"smoke:gap-fill": "npm run build && node scripts/smoke-gap-fill-loop.mjs",
|
|
91
95
|
"smoke:generate-prove": "npm run build && node scripts/smoke-generate-prove-e2e.mjs",
|
|
@@ -35,6 +35,7 @@ import (
|
|
|
35
35
|
"go/printer"
|
|
36
36
|
"go/token"
|
|
37
37
|
"os"
|
|
38
|
+
"strings"
|
|
38
39
|
)
|
|
39
40
|
|
|
40
41
|
// fail prints a stable, machine-readable marker plus a human message and exits
|
|
@@ -104,6 +105,15 @@ func main() {
|
|
|
104
105
|
|
|
105
106
|
if *mode == "sentinel" {
|
|
106
107
|
target.Body = sentinelBody(results)
|
|
108
|
+
// The sentinel body can orphan imports the original body used (Go
|
|
109
|
+
// rejects unused imports, so the mutant would fail to BUILD and the
|
|
110
|
+
// oracle would refuse — an honest but useless verdict for most real
|
|
111
|
+
// functions). Rewrite now-unused imports to blank imports: package
|
|
112
|
+
// init side effects are preserved, no behavior is added, and the
|
|
113
|
+
// repair is fail-safe in both directions — over-blanking a used
|
|
114
|
+
// import still fails the build (refusal, never proof), and a missed
|
|
115
|
+
// unused import is exactly today's behavior.
|
|
116
|
+
blankUnusedImports(astFile)
|
|
107
117
|
}
|
|
108
118
|
// equivalent mode: leave target.Body untouched (semantically identical).
|
|
109
119
|
|
|
@@ -117,6 +127,39 @@ func main() {
|
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
129
|
|
|
130
|
+
// blankUnusedImports renames imports whose package qualifier is no longer
|
|
131
|
+
// referenced anywhere in the file to blank imports (`_ "path"`). Dot imports
|
|
132
|
+
// and existing blank imports are left untouched. Qualifier detection is
|
|
133
|
+
// syntactic (selector bases), which over-approximates "used" — the safe
|
|
134
|
+
// direction: we only ever blank an import nothing references.
|
|
135
|
+
func blankUnusedImports(f *ast.File) {
|
|
136
|
+
used := map[string]bool{}
|
|
137
|
+
ast.Inspect(f, func(n ast.Node) bool {
|
|
138
|
+
if sel, ok := n.(*ast.SelectorExpr); ok {
|
|
139
|
+
if id, ok := sel.X.(*ast.Ident); ok {
|
|
140
|
+
used[id.Name] = true
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return true
|
|
144
|
+
})
|
|
145
|
+
for _, imp := range f.Imports {
|
|
146
|
+
if imp.Name != nil {
|
|
147
|
+
if imp.Name.Name == "_" || imp.Name.Name == "." {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if !used[imp.Name.Name] {
|
|
151
|
+
imp.Name = ast.NewIdent("_")
|
|
152
|
+
}
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
path := strings.Trim(imp.Path.Value, "\"")
|
|
156
|
+
base := path[strings.LastIndex(path, "/")+1:]
|
|
157
|
+
if !used[base] {
|
|
158
|
+
imp.Name = ast.NewIdent("_")
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
120
163
|
// sentinelBody builds `{ return <zero>, <zero>, ... }` matching the function's
|
|
121
164
|
// result signature so the mutant COMPILES. Zero values are type-derived and
|
|
122
165
|
// deliberately wrong for any function whose real return is non-zero; when a
|