@orangepro/orangepro-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +328 -0
- package/dist/local/agentWorkflow.js +81 -0
- package/dist/local/aiGraph/links.js +635 -0
- package/dist/local/analyze/analyzer.js +2129 -0
- package/dist/local/analyze/behaviorContracts.js +169 -0
- package/dist/local/analyze/boilerplate.js +42 -0
- package/dist/local/analyze/callGraph.js +458 -0
- package/dist/local/analyze/classify.js +219 -0
- package/dist/local/analyze/clustering.js +357 -0
- package/dist/local/analyze/confirm.js +2422 -0
- package/dist/local/analyze/coverage.js +518 -0
- package/dist/local/analyze/coverageArtifacts.js +607 -0
- package/dist/local/analyze/frameworks.js +115 -0
- package/dist/local/analyze/linkage/conventions.js +160 -0
- package/dist/local/analyze/parseCache.js +164 -0
- package/dist/local/analyze/selfAssert.js +53 -0
- package/dist/local/analyze/symbols.js +430 -0
- package/dist/local/analyze/testLayer.js +135 -0
- package/dist/local/analyze/treeSitter/engine.js +1253 -0
- package/dist/local/analyze/treeSitter/languages.js +101 -0
- package/dist/local/autoProve.js +620 -0
- package/dist/local/cli.js +1468 -0
- package/dist/local/cliArgs.js +112 -0
- package/dist/local/corpusScope.js +162 -0
- package/dist/local/enrich/csv.js +348 -0
- package/dist/local/enrich/index.js +43 -0
- package/dist/local/enrich/markdown.js +193 -0
- package/dist/local/explain/explain.js +91 -0
- package/dist/local/exportCli.js +26 -0
- package/dist/local/flows/flowWalker.js +215 -0
- package/dist/local/flows/llmFlowDiscovery.js +567 -0
- package/dist/local/freshness/changed.js +280 -0
- package/dist/local/freshness/manifest.js +35 -0
- package/dist/local/freshness/status.js +30 -0
- package/dist/local/gaps/gaps.js +114 -0
- package/dist/local/generate/buckets.js +73 -0
- package/dist/local/generate/compareJudge.js +124 -0
- package/dist/local/generate/compareReport.js +538 -0
- package/dist/local/generate/compareScore.js +105 -0
- package/dist/local/generate/deriveImports.js +91 -0
- package/dist/local/generate/generator.js +2586 -0
- package/dist/local/generate/prompt.js +144 -0
- package/dist/local/generate/promptV5.js +438 -0
- package/dist/local/generate/providers.js +400 -0
- package/dist/local/generate/runHints.js +304 -0
- package/dist/local/graph/citations.js +73 -0
- package/dist/local/graph/confirmable.js +72 -0
- package/dist/local/graph/factories.js +210 -0
- package/dist/local/graph/ontology.js +18 -0
- package/dist/local/interactive.js +53 -0
- package/dist/local/jobs/jobStore.js +80 -0
- package/dist/local/jobs/notify.js +29 -0
- package/dist/local/jobs/runner.js +75 -0
- package/dist/local/ledger.js +117 -0
- package/dist/local/localConfig.js +112 -0
- package/dist/local/mcp.js +548 -0
- package/dist/local/operations.js +1749 -0
- package/dist/local/pack/coverageReport.js +192 -0
- package/dist/local/pack/exporter.js +195 -0
- package/dist/local/pack/schema.js +128 -0
- package/dist/local/pack/summary.js +127 -0
- package/dist/local/pack/validate.js +25 -0
- package/dist/local/proofRunnability.js +366 -0
- package/dist/local/recipe/dbSqljs.js +255 -0
- package/dist/local/reprove/paths.js +13 -0
- package/dist/local/reprove/scoped.js +136 -0
- package/dist/local/resolve/barrelWalker.js +178 -0
- package/dist/local/resolve/exportIndex.js +270 -0
- package/dist/local/resolve/importGraph.js +347 -0
- package/dist/local/resolve/resolver.js +122 -0
- package/dist/local/resolve/resolverCache.js +117 -0
- package/dist/local/rtm.js +413 -0
- package/dist/local/score/coverage.js +99 -0
- package/dist/local/score/doctor.js +67 -0
- package/dist/local/score/risk.js +362 -0
- package/dist/local/score/score.js +182 -0
- package/dist/local/types.js +1 -0
- package/dist/local/util/hash.js +16 -0
- package/dist/local/util/ids.js +16 -0
- package/dist/local/util/progress.js +8 -0
- package/dist/local/util/redact.js +39 -0
- package/dist/local/util/time.js +1 -0
- package/dist/local/util/walk.js +174 -0
- package/dist/local/viz/behaviorReportData.js +367 -0
- package/dist/local/viz/behaviorReportHtml.js +664 -0
- package/dist/local/viz/d3.bundle.js +3 -0
- package/dist/local/viz/html.js +1152 -0
- package/dist/local/viz/payload.js +525 -0
- package/dist/local/workspace.js +99 -0
- package/docs/agent-workflow.md +167 -0
- package/docs/agents/claude-code.md +43 -0
- package/docs/agents/codex.md +52 -0
- package/docs/agents/cursor.md +39 -0
- package/docs/agents/opencode.md +43 -0
- package/docs/agents/vscode.md +34 -0
- package/docs/local-proof-kit.md +269 -0
- package/package.json +92 -0
- package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
- package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
- package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
- package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
- package/scripts/spikes/failure-summary.mjs +29 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { opAnalyze, opAiFlows, opAiLinks, opChanged, opCompare, opDoctor, opDynamicProof, opExplain, opExport, opGaps, opGenerate, opProveLoop, opRecordRun, opRtm, opScore, opStats, opStart, opStatus, opUpdate, resolveDiffTargets } from "./operations.js";
|
|
5
|
+
import { runnableRunHintsFor, AGENT_RUN_WORKFLOW, GROUNDING_CONTRACT } from "./generate/runHints.js";
|
|
6
|
+
import { preloadTreeSitter } from "./analyze/treeSitter/engine.js";
|
|
7
|
+
import { treeSitterLanguages } from "./analyze/treeSitter/languages.js";
|
|
8
|
+
import { redactSecrets } from "./util/redact.js";
|
|
9
|
+
function asText(payload) {
|
|
10
|
+
return { content: [{ type: "text", text: typeof payload === "string" ? payload : JSON.stringify(payload, null, 2) }] };
|
|
11
|
+
}
|
|
12
|
+
function asError(error) {
|
|
13
|
+
const message = redactSecrets(error instanceof Error ? error.message : String(error));
|
|
14
|
+
return { content: [{ type: "text", text: `OrangePro local error: ${message}` }], isError: true };
|
|
15
|
+
}
|
|
16
|
+
const Workspace = {
|
|
17
|
+
workspace: z.string().optional().describe("Local workspace root. Defaults to the current directory.")
|
|
18
|
+
};
|
|
19
|
+
const DEFAULT_MARKDOWN_RTM_LIMIT = 500;
|
|
20
|
+
/**
|
|
21
|
+
* MCP server exposing OrangePro local graph/test tools. Agent-agnostic (Cursor,
|
|
22
|
+
* Codex, Claude, Copilot, …). First slice exposes NO upload or repo-write tools.
|
|
23
|
+
*/
|
|
24
|
+
export function createLocalServer() {
|
|
25
|
+
const server = new McpServer({ name: "orangepro-local", version: "0.1.0" });
|
|
26
|
+
const root = (ws) => ws || process.cwd();
|
|
27
|
+
server.registerTool("orangepro_start", {
|
|
28
|
+
title: "Start OrangePro",
|
|
29
|
+
description: "One-command local setup for a repo or PR: analyze sources, auto-apply weak AI candidate links and candidate flows when a real BYOK provider is configured, write graph.html + rtm.md, summarize changed/gap targets, and return agent next actions. AI lanes stay separate and never affect Proven coverage.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
...Workspace,
|
|
32
|
+
source: z.string().optional().describe("Source path to analyze. Defaults to workspace."),
|
|
33
|
+
base_ref: z.string().optional().describe("Optional PR/diff base ref. Default auto-detects main/master when possible."),
|
|
34
|
+
include_markdown: z.boolean().optional().describe("Enrich requirements-like Markdown docs. Default true."),
|
|
35
|
+
generate_coverage: z.boolean().optional().describe("Run safe local coverage generation before analyze where supported. Default false."),
|
|
36
|
+
coverage_timeout_ms: z.number().int().min(1000).max(600000).optional().describe("Per-command coverage-generation timeout."),
|
|
37
|
+
no_ai: z.boolean().optional().describe("Disable automatic AI weak-link and candidate-flow passes even if a provider key is configured."),
|
|
38
|
+
no_ai_flows: z.boolean().optional().describe("Disable automatic AI candidate-flow discovery while keeping weak AI links enabled."),
|
|
39
|
+
ai_all: z.boolean().optional().describe("Run AI weak-linking over all deterministic behavior nodes instead of gaps-only."),
|
|
40
|
+
provider: z.enum(["openai", "ollama", "anthropic"]).optional().describe("BYOK provider override for the AI passes."),
|
|
41
|
+
model: z.string().optional().describe("Model name override.")
|
|
42
|
+
},
|
|
43
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
44
|
+
}, async (input) => {
|
|
45
|
+
try {
|
|
46
|
+
await preloadTreeSitter(treeSitterLanguages());
|
|
47
|
+
return asText(await opStart(root(input.workspace), {
|
|
48
|
+
source: input.source,
|
|
49
|
+
baseRef: input.base_ref,
|
|
50
|
+
includeMarkdown: input.include_markdown,
|
|
51
|
+
generateCoverage: input.generate_coverage,
|
|
52
|
+
coverageTimeoutMs: input.coverage_timeout_ms,
|
|
53
|
+
ai: !input.no_ai,
|
|
54
|
+
aiAll: input.ai_all,
|
|
55
|
+
aiFlows: !input.no_ai_flows,
|
|
56
|
+
provider: input.provider,
|
|
57
|
+
model: input.model
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return asError(error);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
server.registerTool("orangepro_analyze_sources", {
|
|
65
|
+
title: "Analyze local sources",
|
|
66
|
+
description: "Build or refresh the local OrangePro evidence graph from a local checkout/path plus optional CSV/Markdown enrichers. Metadata-only by default — reads source in-process but never stores or uploads code. Optionally stages/applies AI candidate flows and refreshes the report when ai_flows=true.",
|
|
67
|
+
inputSchema: {
|
|
68
|
+
...Workspace,
|
|
69
|
+
paths: z.array(z.string()).optional().describe("Extra template/doc files (.csv/.md) to enrich the graph."),
|
|
70
|
+
include_markdown: z.boolean().optional().describe("Enrich requirements-like Markdown docs. Default true."),
|
|
71
|
+
ai_flows: z.boolean().optional().describe("Generate and apply AI-suggested candidate flows after analyze, then refresh the behavior report. Requires a BYOK provider; skips without failing analyze if none is configured."),
|
|
72
|
+
provider: z.enum(["openai", "ollama", "anthropic"]).optional().describe("BYOK provider override for ai_flows."),
|
|
73
|
+
model: z.string().optional().describe("Model name override for ai_flows."),
|
|
74
|
+
mode: z.enum(["metadata_only"]).optional().describe("Persistence privacy mode. Only metadata_only is supported."),
|
|
75
|
+
include_source_snippets: z.boolean().optional().describe("Deferred: source snippets are never persisted in the first slice.")
|
|
76
|
+
},
|
|
77
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
78
|
+
}, async (input) => {
|
|
79
|
+
try {
|
|
80
|
+
const workspace = root(input.workspace);
|
|
81
|
+
const analyze = opAnalyze(workspace, { paths: input.paths, includeMarkdown: input.include_markdown });
|
|
82
|
+
if (!input.ai_flows)
|
|
83
|
+
return asText(analyze);
|
|
84
|
+
try {
|
|
85
|
+
const generated = await opAiFlows(workspace, { provider: input.provider, model: input.model });
|
|
86
|
+
const applied = await opAiFlows(workspace, { apply: true });
|
|
87
|
+
return asText({ ...analyze, ai_flows: { status: "applied", generate: generated, apply: applied } });
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
91
|
+
return asText({ ...analyze, ai_flows: { status: "skipped", reason }, warnings: [...analyze.warnings, `AI candidate flows skipped: ${reason}`] });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return asError(error);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
server.registerTool("orangepro_graph_score", {
|
|
99
|
+
title: "Graph readiness score",
|
|
100
|
+
description: "Return the local graph readiness score (0-100), band, per-dimension breakdown, and plain-language missing evidence. Readiness signal, not a proof of test lift.",
|
|
101
|
+
inputSchema: { ...Workspace },
|
|
102
|
+
annotations: { readOnlyHint: true, destructiveHint: false }
|
|
103
|
+
}, async (input) => {
|
|
104
|
+
try {
|
|
105
|
+
return asText(opScore(root(input.workspace)));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return asError(error);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
server.registerTool("orangepro_status", {
|
|
112
|
+
title: "Local workspace status",
|
|
113
|
+
description: "Show local workspace state without generating anything: whether the graph is fresh/stale/missing, source mix, score, and privacy settings (always local, no upload).",
|
|
114
|
+
inputSchema: { ...Workspace },
|
|
115
|
+
annotations: { readOnlyHint: true, destructiveHint: false }
|
|
116
|
+
}, async (input) => {
|
|
117
|
+
try {
|
|
118
|
+
return asText(opStatus(root(input.workspace)));
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return asError(error);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
server.registerTool("orangepro_doctor", {
|
|
125
|
+
title: "Recommend next evidence",
|
|
126
|
+
description: "Recommend the smallest next source that would most improve generated-test quality, with expected score impact. Teaches what data improves output.",
|
|
127
|
+
inputSchema: { ...Workspace, goal: z.string().optional().describe("Optimization goal hint, e.g. 'better_tests'.") },
|
|
128
|
+
annotations: { readOnlyHint: true, destructiveHint: false }
|
|
129
|
+
}, async (input) => {
|
|
130
|
+
try {
|
|
131
|
+
return asText(opDoctor(root(input.workspace)));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
return asError(error);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
server.registerTool("orangepro_find_test_gaps", {
|
|
138
|
+
title: "Find test gaps",
|
|
139
|
+
description: "List requirements/flows with weak or missing test evidence, plus top_risk_gaps for unproven code symbols ranked by the OrangePro Risk Score (Probability × Impact × DetectionDifficulty). Risk is prioritization only; it never changes coverage/proof status.",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
...Workspace,
|
|
142
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max gaps to return. Default 10."),
|
|
143
|
+
min_priority: z.string().optional().describe("Minimum priority to include (low|medium|high|critical).")
|
|
144
|
+
},
|
|
145
|
+
annotations: { readOnlyHint: true, destructiveHint: false }
|
|
146
|
+
}, async (input) => {
|
|
147
|
+
try {
|
|
148
|
+
return asText(opGaps(root(input.workspace), { limit: input.limit, min_priority: input.min_priority }));
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return asError(error);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
server.registerTool("orangepro_record_run", {
|
|
155
|
+
title: "Record gap-fill outcome",
|
|
156
|
+
description: "Record an agent test-writing attempt as static diagnostics. When test_path is provided for TS/JS, OrangePro first tries scoped deterministic re-prove; otherwise it re-analyzes the repo. Static hard COVERS edges render as Associated signal; public Proven requires orangepro_prove.",
|
|
157
|
+
inputSchema: {
|
|
158
|
+
...Workspace,
|
|
159
|
+
target_symbol: z.string().optional().describe("Exact CodeSymbol external id, e.g. sym:svc/add.go#Add. Preferred."),
|
|
160
|
+
target_id: z.string().optional().describe("Optional graph target id that must resolve to exactly one hard-linked CodeSymbol."),
|
|
161
|
+
source: z.string().optional().describe("Source checkout to re-analyze. Defaults to workspace."),
|
|
162
|
+
test_path: z.string().optional().describe("Workspace-relative test file the agent just wrote. Enables scoped deterministic re-prove for TS/JS."),
|
|
163
|
+
agent_pass: z.boolean().optional().describe("Whether the agent-reported test command passed. Advisory only."),
|
|
164
|
+
vacuous: z.boolean().optional().describe("Whether the agent judged the test vacuous/non-assertive. Advisory only."),
|
|
165
|
+
evidence_ids: z.array(z.string()).optional().describe("Generated-test/evidence ids tied to this attempt. Metadata only."),
|
|
166
|
+
provider: z.string().optional().describe("Model provider used by the agent/generator, if any."),
|
|
167
|
+
model: z.string().optional().describe("Model name used by the agent/generator, if any."),
|
|
168
|
+
prompt_version: z.string().optional().describe("Prompt or workflow version used, if any."),
|
|
169
|
+
run_id: z.string().optional().describe("Optional caller-supplied run id. Defaults to a local monotonic id.")
|
|
170
|
+
},
|
|
171
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
172
|
+
}, async (input) => {
|
|
173
|
+
try {
|
|
174
|
+
await preloadTreeSitter(treeSitterLanguages());
|
|
175
|
+
return asText(opRecordRun(root(input.workspace), {
|
|
176
|
+
target_symbol: input.target_symbol,
|
|
177
|
+
target_id: input.target_id,
|
|
178
|
+
source: input.source,
|
|
179
|
+
test_path: input.test_path,
|
|
180
|
+
agent_pass: input.agent_pass,
|
|
181
|
+
vacuous: input.vacuous,
|
|
182
|
+
evidence_ids: input.evidence_ids,
|
|
183
|
+
provider: input.provider,
|
|
184
|
+
model: input.model,
|
|
185
|
+
prompt_version: input.prompt_version,
|
|
186
|
+
run_id: input.run_id
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
return asError(error);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
server.registerTool("orangepro_prove", {
|
|
194
|
+
title: "Run dynamic targeted proof",
|
|
195
|
+
description: "Run the dynamic targeted-proof oracle for a CodeSymbol and append a metadata-only ledger certificate. Public Proven closes only when baseline green plus a valid sentinel mutation of the credited symbol fails the same test at an assertion. Static record_run is diagnostics only.",
|
|
196
|
+
inputSchema: {
|
|
197
|
+
...Workspace,
|
|
198
|
+
target_symbol: z.string().optional().describe("Exact CodeSymbol external id, e.g. sym:src/service.ts#OrderService.createOrder. Preferred."),
|
|
199
|
+
target_id: z.string().optional().describe("Optional graph target id that must resolve to exactly one hard-linked CodeSymbol."),
|
|
200
|
+
source: z.string().optional().describe("Source checkout containing the test and target files. Defaults to workspace."),
|
|
201
|
+
test_path: z.string().describe("Repo-relative test file to run."),
|
|
202
|
+
replacement: z.string().describe("Inert sentinel body, e.g. 'return null;' or 'return {\"ok\":false};'. Must be JSON-only per the oracle."),
|
|
203
|
+
target_file: z.string().optional().describe("Optional consistency check. If provided, must match the file derived from target_symbol."),
|
|
204
|
+
method: z.string().optional().describe("Optional consistency check. If provided, must match the member derived from target_symbol."),
|
|
205
|
+
replacement_mode: z.enum(["return-json", "promise-json"]).optional().describe("Sentinel wrapping mode. Default return-json."),
|
|
206
|
+
runner: z.enum(["auto", "vitest", "jest", "mocha"]).optional().describe("Test runner override. Default auto."),
|
|
207
|
+
timeout_ms: z.number().int().min(1000).max(600000).optional().describe("Per baseline/mutant test timeout."),
|
|
208
|
+
link_node_modules: z.boolean().optional().describe("Trusted-repo speed mode: symlink node_modules into temp copies."),
|
|
209
|
+
vitest_config: z.string().optional().describe("Repo-relative Vitest config path."),
|
|
210
|
+
jest_config: z.string().optional().describe("Repo-relative Jest config path."),
|
|
211
|
+
test_env: z.array(z.string()).optional().describe("Explicit non-secret KEY=value env entries for the test run."),
|
|
212
|
+
run_id: z.string().optional().describe("Optional caller-supplied run id. Defaults to a local monotonic id.")
|
|
213
|
+
},
|
|
214
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
215
|
+
}, async (input) => {
|
|
216
|
+
try {
|
|
217
|
+
await preloadTreeSitter(treeSitterLanguages());
|
|
218
|
+
return asText(opDynamicProof(root(input.workspace), {
|
|
219
|
+
target_symbol: input.target_symbol,
|
|
220
|
+
target_id: input.target_id,
|
|
221
|
+
source: input.source,
|
|
222
|
+
test_path: input.test_path,
|
|
223
|
+
target_path: input.target_file,
|
|
224
|
+
method: input.method,
|
|
225
|
+
replacement: input.replacement,
|
|
226
|
+
replacement_mode: input.replacement_mode,
|
|
227
|
+
runner: input.runner,
|
|
228
|
+
timeout_ms: input.timeout_ms,
|
|
229
|
+
link_node_modules: input.link_node_modules,
|
|
230
|
+
vitest_config: input.vitest_config,
|
|
231
|
+
jest_config: input.jest_config,
|
|
232
|
+
test_env: input.test_env,
|
|
233
|
+
run_id: input.run_id
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
return asError(error);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
server.registerTool("orangepro_prove_loop", {
|
|
241
|
+
title: "Setup, run dynamic proof, refresh report",
|
|
242
|
+
description: "Product wrapper around orangepro_prove: run trusted-local setup_commands in the source checkout, then the UNCHANGED dynamic targeted-proof oracle, then refresh the behavior report. Setup is non-secret repo prep only (build/install/shim); a setup failure returns unrunnable, does not run the oracle, and leaves the ledger untouched (never Proven). Public Proven still closes only through the unchanged certificate.",
|
|
243
|
+
inputSchema: {
|
|
244
|
+
...Workspace,
|
|
245
|
+
target_symbol: z.string().optional().describe("Exact CodeSymbol external id, e.g. sym:src/service.ts#OrderService.createOrder. Preferred."),
|
|
246
|
+
target_id: z.string().optional().describe("Optional graph target id that must resolve to exactly one hard-linked CodeSymbol."),
|
|
247
|
+
source: z.string().optional().describe("Source checkout containing the test and target files. Setup runs here. Defaults to workspace."),
|
|
248
|
+
test_path: z.string().describe("Repo-relative test file to run."),
|
|
249
|
+
replacement: z.string().describe("Inert sentinel body, e.g. 'return null;' or 'return {\"ok\":false};'. Must be JSON-only per the oracle."),
|
|
250
|
+
target_file: z.string().optional().describe("Optional consistency check. If provided, must match the file derived from target_symbol."),
|
|
251
|
+
method: z.string().optional().describe("Optional consistency check. If provided, must match the member derived from target_symbol."),
|
|
252
|
+
replacement_mode: z.enum(["return-json", "promise-json"]).optional().describe("Sentinel wrapping mode. Default return-json."),
|
|
253
|
+
runner: z.enum(["auto", "vitest", "jest", "mocha"]).optional().describe("Test runner override. Default auto."),
|
|
254
|
+
timeout_ms: z.number().int().min(1000).max(600000).optional().describe("Per baseline/mutant test timeout."),
|
|
255
|
+
link_node_modules: z.boolean().optional().describe("Trusted-repo speed mode: symlink node_modules into temp copies."),
|
|
256
|
+
vitest_config: z.string().optional().describe("Repo-relative Vitest config path."),
|
|
257
|
+
jest_config: z.string().optional().describe("Repo-relative Jest config path."),
|
|
258
|
+
test_env: z.array(z.string()).optional().describe("Explicit non-secret KEY=value env entries for the test run."),
|
|
259
|
+
setup_commands: z
|
|
260
|
+
.array(z.object({
|
|
261
|
+
command: z.string().describe("Executable to run in the source checkout, e.g. 'npm'."),
|
|
262
|
+
args: z.array(z.string()).optional().describe("Argument vector, e.g. ['ci'] or ['run','build']."),
|
|
263
|
+
timeout_ms: z.number().int().min(1).optional().describe("Optional per-command timeout override.")
|
|
264
|
+
}))
|
|
265
|
+
.optional()
|
|
266
|
+
.describe("Trusted-local repo prep run in the source checkout before the oracle. Non-secret only (build/install/shim). First non-zero exit returns unrunnable."),
|
|
267
|
+
setup_timeout_ms: z.number().int().min(1).max(600000).optional().describe("Default per-setup-command timeout (ms). Default 30000."),
|
|
268
|
+
run_id: z.string().optional().describe("Optional caller-supplied run id. Defaults to a local monotonic id.")
|
|
269
|
+
},
|
|
270
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
271
|
+
}, async (input) => {
|
|
272
|
+
try {
|
|
273
|
+
return asText(opProveLoop(root(input.workspace), {
|
|
274
|
+
target_symbol: input.target_symbol,
|
|
275
|
+
target_id: input.target_id,
|
|
276
|
+
source: input.source,
|
|
277
|
+
test_path: input.test_path,
|
|
278
|
+
target_path: input.target_file,
|
|
279
|
+
method: input.method,
|
|
280
|
+
replacement: input.replacement,
|
|
281
|
+
replacement_mode: input.replacement_mode,
|
|
282
|
+
runner: input.runner,
|
|
283
|
+
timeout_ms: input.timeout_ms,
|
|
284
|
+
link_node_modules: input.link_node_modules,
|
|
285
|
+
vitest_config: input.vitest_config,
|
|
286
|
+
jest_config: input.jest_config,
|
|
287
|
+
test_env: input.test_env,
|
|
288
|
+
setup_commands: input.setup_commands,
|
|
289
|
+
setup_timeout_ms: input.setup_timeout_ms,
|
|
290
|
+
run_id: input.run_id
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
return asError(error);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
server.registerTool("orangepro_stats", {
|
|
298
|
+
title: "Gap-fill ledger stats",
|
|
299
|
+
description: "Summarize the local gap-fill ledger: attempted, dynamic-reproven, unproven, legacy static-associated, generated-but-unverifiable, and kept rate. Metadata-only.",
|
|
300
|
+
inputSchema: { ...Workspace },
|
|
301
|
+
annotations: { readOnlyHint: true, destructiveHint: false }
|
|
302
|
+
}, async (input) => {
|
|
303
|
+
try {
|
|
304
|
+
return asText(opStats(root(input.workspace)));
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
return asError(error);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
server.registerTool("orangepro_rtm", {
|
|
311
|
+
title: "Deterministic traceability matrix",
|
|
312
|
+
description: "Write and return a deterministic Requirements Traceability Matrix. Status comes only from the local graph and gap-fill ledger: Proven, Runtime-covered, Associated signal, No integration signal, Reproven, or Generated-unverifiable. No LLM calls.",
|
|
313
|
+
inputSchema: {
|
|
314
|
+
...Workspace,
|
|
315
|
+
format: z.enum(["md", "csv", "json"]).optional().describe("Output format. Default md."),
|
|
316
|
+
output_path: z.string().optional().describe("Output path. Default .orangepro/rtm.<format>."),
|
|
317
|
+
base_ref: z.string().optional().describe("Optional PR/diff scope. Restricts rows to behaviors touched vs this ref."),
|
|
318
|
+
status: z.array(z.string()).optional().describe("Optional status filter, e.g. ['no-link','runtime','associated']."),
|
|
319
|
+
limit: z.number().int().min(1).max(50000).optional().describe("Optional max rows to emit. Markdown defaults to 500 rows; JSON/CSV are uncapped unless set.")
|
|
320
|
+
},
|
|
321
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
322
|
+
}, async (input) => {
|
|
323
|
+
try {
|
|
324
|
+
return asText(opRtm(root(input.workspace), {
|
|
325
|
+
format: input.format,
|
|
326
|
+
outputPath: input.output_path,
|
|
327
|
+
baseRef: input.base_ref,
|
|
328
|
+
statuses: input.status,
|
|
329
|
+
limit: input.limit ?? ((input.format ?? "md") === "md" ? DEFAULT_MARKDOWN_RTM_LIMIT : undefined)
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
return asError(error);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
server.registerTool("orangepro_update_graph", {
|
|
337
|
+
title: "Incrementally update graph",
|
|
338
|
+
description: "Refresh the local graph from changed files. Incremental and non-destructive by default — preserves evidence and marks affected generated tests stale.",
|
|
339
|
+
inputSchema: {
|
|
340
|
+
...Workspace,
|
|
341
|
+
force_full_rebuild: z.boolean().optional().describe("Force a full rebuild instead of an incremental update.")
|
|
342
|
+
},
|
|
343
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
344
|
+
}, async (input) => {
|
|
345
|
+
try {
|
|
346
|
+
return asText(opUpdate(root(input.workspace), { force_full_rebuild: input.force_full_rebuild }));
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
return asError(error);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
server.registerTool("orangepro_changed_impact", {
|
|
353
|
+
title: "Changed-file impact",
|
|
354
|
+
description: "DIFF/PR TOOL — only for real code-review situations. Summarizes what changed vs a base git ref and which local graph behaviors/tests are affected (basic changed-file impact; no deep route/API shape analysis). Requires a git repo and a real diff: returns status 'no_diff' / 'no_code_changes' (diff was docs-only) / 'missing_base_ref' / 'not_a_git_repo' with guidance instead of fabricated impact when there is nothing to analyze. For baseline (no-PR) test opportunities use orangepro_find_test_gaps.",
|
|
355
|
+
inputSchema: { ...Workspace, base_ref: z.string().optional().describe("Base git ref to diff against. Default 'main'.") },
|
|
356
|
+
annotations: { readOnlyHint: true, destructiveHint: false }
|
|
357
|
+
}, async (input) => {
|
|
358
|
+
try {
|
|
359
|
+
return asText(opChanged(root(input.workspace), input.base_ref));
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
return asError(error);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
server.registerTool("orangepro_ai_links", {
|
|
366
|
+
title: "Stage/apply AI candidate links",
|
|
367
|
+
description: "Opt-in AI lane: propose weak MAY_RELATE_TO candidate links between existing deterministic behavior and CodeSymbol nodes. Generate writes .orangepro/ai/links.json only; apply=true explicitly merges survivors into candidate_edges. These links never affect proven coverage.",
|
|
368
|
+
inputSchema: {
|
|
369
|
+
...Workspace,
|
|
370
|
+
apply: z.boolean().optional().describe("Apply staged links into candidate_edges. Default false stages links only."),
|
|
371
|
+
all: z.boolean().optional().describe("Link across all behavior nodes instead of the default gaps-only scope."),
|
|
372
|
+
provider: z.enum(["openai", "ollama", "anthropic"]).optional().describe("BYOK provider override."),
|
|
373
|
+
model: z.string().optional().describe("Model name override."),
|
|
374
|
+
max_behaviors: z.number().int().positive().optional().describe("Maximum behavior targets to process in this AI linking run."),
|
|
375
|
+
symbols_per_behavior: z.number().int().positive().optional().describe("Deterministic CodeSymbol shortlist size per behavior."),
|
|
376
|
+
max_prompt_tokens: z.number().int().positive().optional().describe("Approximate per-batch prompt token ceiling.")
|
|
377
|
+
},
|
|
378
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
379
|
+
}, async (input) => {
|
|
380
|
+
try {
|
|
381
|
+
return asText(await opAiLinks(root(input.workspace), {
|
|
382
|
+
apply: input.apply,
|
|
383
|
+
all: input.all,
|
|
384
|
+
provider: input.provider,
|
|
385
|
+
model: input.model,
|
|
386
|
+
maxBehaviors: input.max_behaviors,
|
|
387
|
+
symbolsPerBehavior: input.symbols_per_behavior,
|
|
388
|
+
maxPromptTokens: input.max_prompt_tokens
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
return asError(error);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
server.registerTool("orangepro_ai_flows", {
|
|
396
|
+
title: "Stage/apply AI candidate flows",
|
|
397
|
+
description: "Opt-in AI lane: propose candidate behavior-flow chains over existing deterministic entry and CodeSymbol ids. Generate writes .orangepro/ai/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.",
|
|
398
|
+
inputSchema: {
|
|
399
|
+
...Workspace,
|
|
400
|
+
apply: z.boolean().optional().describe("Apply staged flows into analysis.candidate_flows. Default false stages flows only."),
|
|
401
|
+
provider: z.enum(["openai", "ollama", "anthropic"]).optional().describe("BYOK provider override."),
|
|
402
|
+
model: z.string().optional().describe("Model name override.")
|
|
403
|
+
},
|
|
404
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
405
|
+
}, async (input) => {
|
|
406
|
+
try {
|
|
407
|
+
return asText(await opAiFlows(root(input.workspace), {
|
|
408
|
+
apply: input.apply,
|
|
409
|
+
provider: input.provider,
|
|
410
|
+
model: input.model
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
return asError(error);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
server.registerTool("orangepro_generate_tests", {
|
|
418
|
+
title: "Generate grounded tests",
|
|
419
|
+
description: "Generate a small capped set (1-5) of grounded tests from local evidence using your own model key (BYOK: OpenAI-compatible, Ollama, or Anthropic). Tests are diversified across lightweight LOCAL scenario buckets (happy_path, validation_error, edge_case, integration_flow, security_privacy, regression) chosen from the evidence — unjustified buckets are skipped, never padded. By default it focuses the top-gap behavior and returns up to `limit` bucket-diverse tests for it; pass multiple target_ids to split the budget across them. Each test reports its bucket plus VALIDATED grounding `evidence` (every cited entity resolved against the local graph with its kind, evidence strength, and source_ref) and discloses weak/candidate evidence; `evidence_summary` reports proof coverage and any broken citations, so the grounding is verifiable rather than asserted. If evidence is too thin, returns missing-evidence guidance instead of generic tests. Returns each test's code plus `run_hints` (a suggested file path + run command) and an `agent_workflow`: YOU (the agent) write each file and run it with your shell tools, then report pass/fail + stack traces — OrangePro never writes to or runs anything in the repo. Requires the repo's test framework/deps to already be installed.",
|
|
420
|
+
inputSchema: {
|
|
421
|
+
...Workspace,
|
|
422
|
+
target_ids: z.array(z.string()).optional().describe("Behavior/requirement external ids to target. Default: the top-gap behavior. Pass several to split the budget across them (each gets a test when the budget allows, in priority order)."),
|
|
423
|
+
base_ref: z.string().optional().describe("PR-scoped generation: restrict to the behaviors the diff vs this ref touches (e.g. 'main') — for branch/PR review, target only the changed code. Requires a git repo + a real diff; returns structured guidance (status + guidance) instead of fabricating when there is none. Overrides target_ids."),
|
|
424
|
+
framework: z.string().optional().describe("Framework hint, e.g. 'playwright', 'vitest', 'pytest'."),
|
|
425
|
+
limit: z.number().int().min(1).max(5).optional().describe("Total tests to generate (1-5), spread across local scenario buckets. Default 3."),
|
|
426
|
+
provider: z
|
|
427
|
+
.enum(["openai", "ollama", "anthropic", "deterministic"])
|
|
428
|
+
.optional()
|
|
429
|
+
.describe("BYOK provider override (auto-detected from env if omitted). Use 'deterministic' for an offline stand-in; without any provider configured, generation returns setup guidance instead of degrading."),
|
|
430
|
+
model: z.string().optional().describe("Model name override."),
|
|
431
|
+
prompt_version: z
|
|
432
|
+
.enum(["v2", "v5"])
|
|
433
|
+
.optional()
|
|
434
|
+
.describe("Prompt strategy. Defaults to v2; v5 is opt-in until corpus validation clears it as default."),
|
|
435
|
+
write_files: z.boolean().optional().describe("Deferred/premium: repo file writing is disabled in the first slice; always treated as false."),
|
|
436
|
+
compare: z
|
|
437
|
+
.boolean()
|
|
438
|
+
.optional()
|
|
439
|
+
.describe("Run BOTH arms — prompt-only baseline vs Local KG, same model + system prompt — and return side-by-side scores (completeness, context awareness, accuracy, domain specificity) plus both test suites. Non-persisting testing view.")
|
|
440
|
+
},
|
|
441
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
442
|
+
}, async (input) => {
|
|
443
|
+
try {
|
|
444
|
+
// PR-scoped generation: --base/base_ref restricts targets to the diff.
|
|
445
|
+
let target_ids = input.target_ids;
|
|
446
|
+
if (input.base_ref) {
|
|
447
|
+
const dt = resolveDiffTargets(root(input.workspace), input.base_ref);
|
|
448
|
+
if (dt.status !== "ok") {
|
|
449
|
+
return asText({ status: dt.status, base_ref: dt.base_ref, guidance: dt.guidance, generated_tests: [] });
|
|
450
|
+
}
|
|
451
|
+
if (!dt.target_ids.length) {
|
|
452
|
+
return asText({ status: "no_behaviors", base_ref: dt.base_ref, guidance: dt.guidance, generated_tests: [] });
|
|
453
|
+
}
|
|
454
|
+
target_ids = dt.target_ids;
|
|
455
|
+
}
|
|
456
|
+
if (input.compare) {
|
|
457
|
+
// Each arm already carries run_hints (runnable tests only); attach the
|
|
458
|
+
// write -> run -> report workflow so the agent can run the Local KG arm.
|
|
459
|
+
const cmp = await opCompare(root(input.workspace), {
|
|
460
|
+
target_ids,
|
|
461
|
+
framework: input.framework,
|
|
462
|
+
limit: input.limit,
|
|
463
|
+
provider: input.provider,
|
|
464
|
+
model: input.model,
|
|
465
|
+
prompt_version: input.prompt_version
|
|
466
|
+
});
|
|
467
|
+
return asText({ ...cmp, agent_workflow: AGENT_RUN_WORKFLOW });
|
|
468
|
+
}
|
|
469
|
+
const result = await opGenerate(root(input.workspace), {
|
|
470
|
+
target_ids,
|
|
471
|
+
framework: input.framework,
|
|
472
|
+
limit: input.limit,
|
|
473
|
+
provider: input.provider,
|
|
474
|
+
model: input.model,
|
|
475
|
+
prompt_version: input.prompt_version
|
|
476
|
+
});
|
|
477
|
+
const note = input.write_files
|
|
478
|
+
? { write_files_note: "Repo file writing is a deferred/premium capability; tests were generated in-memory only (write_files=false)." }
|
|
479
|
+
: {};
|
|
480
|
+
// The agent is the test runner: return each RUNNABLE test's suggested write
|
|
481
|
+
// path + run command plus the write -> run -> report workflow. Non-runnable
|
|
482
|
+
// grounded drafts (runnable === false) are excluded from run_hints so the
|
|
483
|
+
// agent never runs a draft with a fabricated/missing import; they remain in
|
|
484
|
+
// `generated_tests` with their `unresolved_reason`. OrangePro never writes
|
|
485
|
+
// to or runs anything in the repo.
|
|
486
|
+
return asText({
|
|
487
|
+
...result,
|
|
488
|
+
agent_workflow: AGENT_RUN_WORKFLOW,
|
|
489
|
+
grounding_contract: GROUNDING_CONTRACT,
|
|
490
|
+
run_hints: runnableRunHintsFor(result.generated_tests, root(input.workspace)),
|
|
491
|
+
...note
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
return asError(error);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
server.registerTool("orangepro_explain_test", {
|
|
499
|
+
title: "Explain a generated test",
|
|
500
|
+
description: "Explain why a generated test exists: which graph evidence anchors support it, source refs, and whether weak/candidate evidence was used. Exposes trust artifacts only, not internal generation logic.",
|
|
501
|
+
inputSchema: {
|
|
502
|
+
...Workspace,
|
|
503
|
+
generated_test_id: z.string().min(1).describe("The generated test id (or title) from orangepro_generate_tests.")
|
|
504
|
+
},
|
|
505
|
+
annotations: { readOnlyHint: true, destructiveHint: false }
|
|
506
|
+
}, async (input) => {
|
|
507
|
+
try {
|
|
508
|
+
return asText(opExplain(root(input.workspace), input.generated_test_id));
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
return asError(error);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
server.registerTool("orangepro_export_evidence_pack", {
|
|
515
|
+
title: "Export evidence pack",
|
|
516
|
+
description: "Export a portable evidence pack (JSON) plus a human-readable Markdown summary for review or later hosted promotion. Includes facts, provenance, generated outputs, and high-level score metadata — never prompts, weights, traces, or raw source. Validates against the local schema.",
|
|
517
|
+
inputSchema: {
|
|
518
|
+
...Workspace,
|
|
519
|
+
output_path: z.string().optional().describe("Output JSON path. Default 'orangepro-evidence-pack.json'."),
|
|
520
|
+
include_generated_bodies: z
|
|
521
|
+
.boolean()
|
|
522
|
+
.optional()
|
|
523
|
+
.describe("Embed generated test bodies in the pack. Default false: bodies stay in the local workspace so the exported pack is metadata-only. Raw source snippets never cross the boundary regardless of this flag."),
|
|
524
|
+
graph_html: z.boolean().optional().describe("Also write a self-contained offline evidence-graph explorer (metadata only, no network).")
|
|
525
|
+
},
|
|
526
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
527
|
+
}, async (input) => {
|
|
528
|
+
try {
|
|
529
|
+
const result = opExport(root(input.workspace), input.output_path || "orangepro-evidence-pack.json", {
|
|
530
|
+
include_generated_bodies: input.include_generated_bodies,
|
|
531
|
+
graph_html: input.graph_html
|
|
532
|
+
});
|
|
533
|
+
return asText(result);
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
return asError(error);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
return server;
|
|
540
|
+
}
|
|
541
|
+
export async function startLocalMcpServer() {
|
|
542
|
+
// Preload configured tree-sitter grammars once so the sync analyzer can extract
|
|
543
|
+
// via AST when the analyze/generate tools run.
|
|
544
|
+
await preloadTreeSitter(treeSitterLanguages());
|
|
545
|
+
const server = createLocalServer();
|
|
546
|
+
const transport = new StdioServerTransport();
|
|
547
|
+
await server.connect(transport);
|
|
548
|
+
}
|