@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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/local/agentWorkflow.js +81 -0
  4. package/dist/local/aiGraph/links.js +635 -0
  5. package/dist/local/analyze/analyzer.js +2129 -0
  6. package/dist/local/analyze/behaviorContracts.js +169 -0
  7. package/dist/local/analyze/boilerplate.js +42 -0
  8. package/dist/local/analyze/callGraph.js +458 -0
  9. package/dist/local/analyze/classify.js +219 -0
  10. package/dist/local/analyze/clustering.js +357 -0
  11. package/dist/local/analyze/confirm.js +2422 -0
  12. package/dist/local/analyze/coverage.js +518 -0
  13. package/dist/local/analyze/coverageArtifacts.js +607 -0
  14. package/dist/local/analyze/frameworks.js +115 -0
  15. package/dist/local/analyze/linkage/conventions.js +160 -0
  16. package/dist/local/analyze/parseCache.js +164 -0
  17. package/dist/local/analyze/selfAssert.js +53 -0
  18. package/dist/local/analyze/symbols.js +430 -0
  19. package/dist/local/analyze/testLayer.js +135 -0
  20. package/dist/local/analyze/treeSitter/engine.js +1253 -0
  21. package/dist/local/analyze/treeSitter/languages.js +101 -0
  22. package/dist/local/autoProve.js +620 -0
  23. package/dist/local/cli.js +1468 -0
  24. package/dist/local/cliArgs.js +112 -0
  25. package/dist/local/corpusScope.js +162 -0
  26. package/dist/local/enrich/csv.js +348 -0
  27. package/dist/local/enrich/index.js +43 -0
  28. package/dist/local/enrich/markdown.js +193 -0
  29. package/dist/local/explain/explain.js +91 -0
  30. package/dist/local/exportCli.js +26 -0
  31. package/dist/local/flows/flowWalker.js +215 -0
  32. package/dist/local/flows/llmFlowDiscovery.js +567 -0
  33. package/dist/local/freshness/changed.js +280 -0
  34. package/dist/local/freshness/manifest.js +35 -0
  35. package/dist/local/freshness/status.js +30 -0
  36. package/dist/local/gaps/gaps.js +114 -0
  37. package/dist/local/generate/buckets.js +73 -0
  38. package/dist/local/generate/compareJudge.js +124 -0
  39. package/dist/local/generate/compareReport.js +538 -0
  40. package/dist/local/generate/compareScore.js +105 -0
  41. package/dist/local/generate/deriveImports.js +91 -0
  42. package/dist/local/generate/generator.js +2586 -0
  43. package/dist/local/generate/prompt.js +144 -0
  44. package/dist/local/generate/promptV5.js +438 -0
  45. package/dist/local/generate/providers.js +400 -0
  46. package/dist/local/generate/runHints.js +304 -0
  47. package/dist/local/graph/citations.js +73 -0
  48. package/dist/local/graph/confirmable.js +72 -0
  49. package/dist/local/graph/factories.js +210 -0
  50. package/dist/local/graph/ontology.js +18 -0
  51. package/dist/local/interactive.js +53 -0
  52. package/dist/local/jobs/jobStore.js +80 -0
  53. package/dist/local/jobs/notify.js +29 -0
  54. package/dist/local/jobs/runner.js +75 -0
  55. package/dist/local/ledger.js +117 -0
  56. package/dist/local/localConfig.js +112 -0
  57. package/dist/local/mcp.js +548 -0
  58. package/dist/local/operations.js +1749 -0
  59. package/dist/local/pack/coverageReport.js +192 -0
  60. package/dist/local/pack/exporter.js +195 -0
  61. package/dist/local/pack/schema.js +128 -0
  62. package/dist/local/pack/summary.js +127 -0
  63. package/dist/local/pack/validate.js +25 -0
  64. package/dist/local/proofRunnability.js +366 -0
  65. package/dist/local/recipe/dbSqljs.js +255 -0
  66. package/dist/local/reprove/paths.js +13 -0
  67. package/dist/local/reprove/scoped.js +136 -0
  68. package/dist/local/resolve/barrelWalker.js +178 -0
  69. package/dist/local/resolve/exportIndex.js +270 -0
  70. package/dist/local/resolve/importGraph.js +347 -0
  71. package/dist/local/resolve/resolver.js +122 -0
  72. package/dist/local/resolve/resolverCache.js +117 -0
  73. package/dist/local/rtm.js +413 -0
  74. package/dist/local/score/coverage.js +99 -0
  75. package/dist/local/score/doctor.js +67 -0
  76. package/dist/local/score/risk.js +362 -0
  77. package/dist/local/score/score.js +182 -0
  78. package/dist/local/types.js +1 -0
  79. package/dist/local/util/hash.js +16 -0
  80. package/dist/local/util/ids.js +16 -0
  81. package/dist/local/util/progress.js +8 -0
  82. package/dist/local/util/redact.js +39 -0
  83. package/dist/local/util/time.js +1 -0
  84. package/dist/local/util/walk.js +174 -0
  85. package/dist/local/viz/behaviorReportData.js +367 -0
  86. package/dist/local/viz/behaviorReportHtml.js +664 -0
  87. package/dist/local/viz/d3.bundle.js +3 -0
  88. package/dist/local/viz/html.js +1152 -0
  89. package/dist/local/viz/payload.js +525 -0
  90. package/dist/local/workspace.js +99 -0
  91. package/docs/agent-workflow.md +167 -0
  92. package/docs/agents/claude-code.md +43 -0
  93. package/docs/agents/codex.md +52 -0
  94. package/docs/agents/cursor.md +39 -0
  95. package/docs/agents/opencode.md +43 -0
  96. package/docs/agents/vscode.md +34 -0
  97. package/docs/local-proof-kit.md +269 -0
  98. package/package.json +92 -0
  99. package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
  100. package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
  101. package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
  102. package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
  103. package/scripts/spikes/failure-summary.mjs +29 -0
@@ -0,0 +1,400 @@
1
+ const DEFAULT_TIMEOUT_MS = 60_000;
2
+ async function postJson(fetchImpl, url, headers, body, timeoutMs = DEFAULT_TIMEOUT_MS) {
3
+ const controller = new AbortController();
4
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
5
+ try {
6
+ const res = await fetchImpl(url, {
7
+ method: "POST",
8
+ signal: controller.signal,
9
+ headers: { "Content-Type": "application/json", ...headers },
10
+ body: JSON.stringify(body)
11
+ });
12
+ const text = await res.text();
13
+ if (!res.ok) {
14
+ const err = new Error(`Model provider HTTP ${res.status}: ${text.slice(0, 300)}`);
15
+ err.status = res.status;
16
+ err.bodyText = text;
17
+ throw err;
18
+ }
19
+ return text ? JSON.parse(text) : {};
20
+ }
21
+ catch (e) {
22
+ // Map the cryptic AbortError ("This operation was aborted") to an actionable
23
+ // timeout message — the #1 cause is a reasoning model thinking past the cap.
24
+ if (e?.name === "AbortError") {
25
+ throw new Error(`Model call timed out after ${Math.round(timeoutMs / 1000)}s. Reasoning models (gpt-5/o-series) can spend ` +
26
+ `minutes on hidden reasoning before responding; retry, or try a smaller --limit or a different model.`);
27
+ }
28
+ throw e;
29
+ }
30
+ finally {
31
+ clearTimeout(timer);
32
+ }
33
+ }
34
+ // Newer OpenAI models (GPT-5.x, o-series reasoning models) require
35
+ // `max_completion_tokens` instead of `max_tokens` and reject a non-default
36
+ // `temperature`. Used only to SEED the request; the adapter still self-corrects
37
+ // from the API's own error, so this list need not be exhaustive or current.
38
+ const REASONING_MODEL = /(?:gpt-5|^o[0-9]|[-/]o[0-9])/i;
39
+ /**
40
+ * Reasoning models spend minutes on hidden reasoning before the (non-streaming)
41
+ * response returns — the 60s default killed live gpt-5 runs mid-generation
42
+ * ("This operation was aborted"), and the starvation retry's 4x budget makes
43
+ * calls even longer. 10 minutes is a ceiling, not an expectation.
44
+ */
45
+ const REASONING_TIMEOUT_MS = 600_000;
46
+ /** Per-call timeout: generous for reasoning models, default for the rest. */
47
+ export function providerTimeoutMs(model) {
48
+ return REASONING_MODEL.test(model) ? REASONING_TIMEOUT_MS : DEFAULT_TIMEOUT_MS;
49
+ }
50
+ /**
51
+ * From a 400, determine which token param the model actually WANTS. OpenAI's error
52
+ * is explicit — "'max_tokens' is not supported ... Use 'max_completion_tokens'
53
+ * instead" — so we honor the "Use X instead" directive instead of guessing from the
54
+ * offending param name. That avoids flipping a reasoning model onto `max_tokens`
55
+ * (which always fails) when the first `max_completion_tokens` call errored for an
56
+ * unrelated reason. Returns null when the 400 is not about the token param.
57
+ */
58
+ function wantedTokenParam(e) {
59
+ const err = e;
60
+ if (err?.status !== 400 || !err.bodyText)
61
+ return null;
62
+ const text = err.bodyText;
63
+ if (/use\s+['"`]?max_completion_tokens/i.test(text))
64
+ return "max_completion_tokens";
65
+ if (/use\s+['"`]?max_tokens\b/i.test(text))
66
+ return "max_tokens";
67
+ return null;
68
+ }
69
+ /** True when a 400 says the model rejects a non-default `temperature`. */
70
+ function temperatureRejected(e) {
71
+ const err = e;
72
+ if (err?.status !== 400 || !err.bodyText)
73
+ return false;
74
+ return /\btemperature\b/i.test(err.bodyText) && /unsupported|does not support|only the default/i.test(err.bodyText);
75
+ }
76
+ /** OpenAI-compatible Chat Completions adapter (OpenAI, Azure-compatible, local gateways). */
77
+ export class OpenAICompatibleProvider {
78
+ cfg;
79
+ fetchImpl;
80
+ providerName = "openai";
81
+ constructor(cfg, fetchImpl = fetch) {
82
+ this.cfg = cfg;
83
+ this.fetchImpl = fetchImpl;
84
+ }
85
+ get modelName() {
86
+ return this.cfg.model;
87
+ }
88
+ async complete(req) {
89
+ const modern = REASONING_MODEL.test(this.cfg.model);
90
+ // Reasoning models spend tokens on hidden reasoning, so give them more room.
91
+ let maxTokens = req.maxTokens ?? (modern ? 4000 : 900);
92
+ // Seed from the model name, then self-correct from the API's explicit directive.
93
+ let tokenParam = modern ? "max_completion_tokens" : "max_tokens";
94
+ let sendTemperature = !modern;
95
+ const tried = new Set();
96
+ for (;;) {
97
+ const body = {
98
+ model: this.cfg.model,
99
+ [tokenParam]: maxTokens,
100
+ messages: [
101
+ { role: "system", content: req.system },
102
+ { role: "user", content: req.user }
103
+ ]
104
+ };
105
+ if (sendTemperature)
106
+ body.temperature = req.temperature ?? 0.2;
107
+ try {
108
+ const data = (await postJson(this.fetchImpl, `${this.cfg.baseUrl}/chat/completions`, { Authorization: `Bearer ${this.cfg.apiKey ?? ""}` }, body, providerTimeoutMs(this.cfg.model)));
109
+ const choice = data.choices?.[0];
110
+ const content = choice?.message?.content ?? "";
111
+ // Reasoning starvation: the model spent the ENTIRE completion budget on
112
+ // hidden reasoning and returned no visible content (finish_reason
113
+ // "length"). Long grounded prompts trigger this on gpt-5/o-series at the
114
+ // 4000-token seed. Retry ONCE with 4x the budget; if it still comes back
115
+ // empty, return "" and let the caller refuse to emit an empty test.
116
+ if (!content.trim() && choice?.finish_reason === "length" && !tried.has("starved")) {
117
+ tried.add("starved");
118
+ maxTokens = maxTokens * 4;
119
+ continue;
120
+ }
121
+ return content;
122
+ }
123
+ catch (e) {
124
+ // Switch to the token param the API explicitly asked for (once) — never guess,
125
+ // so a reasoning model is never flipped onto max_tokens by an unrelated 400.
126
+ const want = wantedTokenParam(e);
127
+ if (want && want !== tokenParam && !tried.has("token-swap")) {
128
+ tried.add("token-swap");
129
+ tokenParam = want;
130
+ continue;
131
+ }
132
+ if (temperatureRejected(e) && sendTemperature && !tried.has("temp-drop")) {
133
+ tried.add("temp-drop");
134
+ sendTemperature = false;
135
+ continue;
136
+ }
137
+ throw e;
138
+ }
139
+ }
140
+ }
141
+ }
142
+ /** Ollama local-model adapter (no API key). */
143
+ export class OllamaProvider {
144
+ cfg;
145
+ fetchImpl;
146
+ providerName = "ollama";
147
+ constructor(cfg, fetchImpl = fetch) {
148
+ this.cfg = cfg;
149
+ this.fetchImpl = fetchImpl;
150
+ }
151
+ get modelName() {
152
+ return this.cfg.model;
153
+ }
154
+ async complete(req) {
155
+ const data = (await postJson(this.fetchImpl, `${this.cfg.baseUrl}/api/chat`, {}, {
156
+ model: this.cfg.model,
157
+ stream: false,
158
+ messages: [
159
+ { role: "system", content: req.system },
160
+ { role: "user", content: req.user }
161
+ ]
162
+ }));
163
+ return data.message?.content ?? "";
164
+ }
165
+ }
166
+ /** Anthropic Messages API adapter. */
167
+ export class AnthropicProvider {
168
+ cfg;
169
+ fetchImpl;
170
+ providerName = "anthropic";
171
+ constructor(cfg, fetchImpl = fetch) {
172
+ this.cfg = cfg;
173
+ this.fetchImpl = fetchImpl;
174
+ }
175
+ get modelName() {
176
+ return this.cfg.model;
177
+ }
178
+ async complete(req) {
179
+ const data = (await postJson(this.fetchImpl, `${this.cfg.baseUrl}/messages`, { "x-api-key": this.cfg.apiKey ?? "", "anthropic-version": "2023-06-01" }, {
180
+ model: this.cfg.model,
181
+ max_tokens: req.maxTokens ?? 900,
182
+ temperature: req.temperature ?? 0.2,
183
+ system: req.system,
184
+ messages: [{ role: "user", content: req.user }]
185
+ }));
186
+ return (data.content ?? []).map((c) => c.text ?? "").join("");
187
+ }
188
+ }
189
+ /**
190
+ * Deterministic, offline provider. Used when no BYOK credentials are present
191
+ * (tests + offline internal validation). It renders the grounded prompt into a
192
+ * framework-hinted scaffold whose specificity tracks the evidence it was given —
193
+ * so a grounded prompt yields concrete assertions while a bare raw-prompt
194
+ * baseline collapses to a generic smoke test. Clearly labeled as deterministic.
195
+ */
196
+ export class DeterministicProvider {
197
+ providerName = "deterministic";
198
+ modelName = "orangepro-local-deterministic-v0";
199
+ async complete(req) {
200
+ return renderDeterministic(req.user);
201
+ }
202
+ }
203
+ function extractSection(text, label) {
204
+ const lines = text.split(/\r?\n/);
205
+ const out = [];
206
+ let inSection = false;
207
+ for (const line of lines) {
208
+ if (new RegExp(`^${label}:`, "i").test(line.trim())) {
209
+ inSection = true;
210
+ const inline = line.split(":").slice(1).join(":").trim();
211
+ if (inline)
212
+ out.push(inline);
213
+ continue;
214
+ }
215
+ if (inSection) {
216
+ // A section ends at the next ALL-CAPS header — with or without a trailing
217
+ // parenthetical (e.g. "EXISTING TESTS (already covered ...):"). Without the
218
+ // `(` alternative, prompt-v1 sections bled into the previous extraction.
219
+ if (/^[A-Z][A-Z _]+\s*[(:]/.test(line.trim()))
220
+ break;
221
+ const item = line.replace(/^[-*\d.\s]+/, "").trim();
222
+ if (item)
223
+ out.push(item);
224
+ }
225
+ }
226
+ return out;
227
+ }
228
+ function snakeIdent(text) {
229
+ const s = text.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
230
+ return s || "behavior";
231
+ }
232
+ function pascalIdent(text) {
233
+ const s = text
234
+ .split(/[^a-z0-9]+/i)
235
+ .filter(Boolean)
236
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
237
+ .join("");
238
+ return /^[A-Za-z]/.test(s) ? s : "Behavior";
239
+ }
240
+ function goStringLiteral(text) {
241
+ return JSON.stringify(text);
242
+ }
243
+ function camelIdent(text) {
244
+ const p = pascalIdent(text);
245
+ return p.charAt(0).toLowerCase() + p.slice(1);
246
+ }
247
+ function renderDeterministic(user) {
248
+ const behavior = extractSection(user, "BEHAVIOR")[0] || "the target behavior";
249
+ const framework = (extractSection(user, "FRAMEWORK")[0] || "vitest").toLowerCase();
250
+ const criteria = extractSection(user, "ACCEPTANCE CRITERIA");
251
+ const assertions = criteria.length
252
+ ? criteria
253
+ : ["the primary expected outcome is observable"]; // generic fallback for raw baseline
254
+ return renderSkeleton(framework, behavior, assertions);
255
+ }
256
+ /**
257
+ * The offline stand-in's body: VALID, runnable framework code (NOT Markdown).
258
+ * It is a grounded skeleton — names the behavior, lists the acceptance criteria as
259
+ * a comment, and includes one trivially-passing placeholder assertion so the file
260
+ * compiles and runs. A real BYOK model replaces the placeholder with concrete
261
+ * assertions; imports for the grounded arm are prepended by synthesizeImports.
262
+ */
263
+ function renderSkeleton(framework, behavior, assertions) {
264
+ // Escape for embedding in a double-quoted JS string: backslash FIRST, then quote,
265
+ // so titles with Windows paths / regex (\d, \U) or quotes can't emit invalid code.
266
+ const esc = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
267
+ const title = esc(behavior);
268
+ const first = esc(assertions[0] ?? "behaves as specified");
269
+ const ac = assertions.join("; ");
270
+ if (framework.includes("pytest") || framework.includes("python")) {
271
+ return [
272
+ `def test_${snakeIdent(behavior)}():`,
273
+ // Behavior + acceptance go in line comments (no docstring) so backslashes or
274
+ // embedded quotes in the title can never break the Python source.
275
+ ` # ${behavior}`,
276
+ ` # acceptance: ${ac}`,
277
+ " # TODO: arrange + act on the subject under test, then assert the real outcome.",
278
+ " assert True # placeholder — replace with a concrete assertion",
279
+ ""
280
+ ].join("\n");
281
+ }
282
+ if (framework.includes("go")) {
283
+ return [
284
+ "package main",
285
+ "",
286
+ "import \"testing\"",
287
+ "",
288
+ `func Test${pascalIdent(behavior)}(t *testing.T) {`,
289
+ `\t// ${behavior}`,
290
+ `\t// acceptance: ${ac}`,
291
+ "\t// TODO: arrange + act on the subject under test, then assert the real outcome.",
292
+ "\tif false {",
293
+ `\t\tt.Fatalf("expected %q", ${goStringLiteral(first)})`,
294
+ "\t}",
295
+ "}",
296
+ ""
297
+ ].join("\n");
298
+ }
299
+ if (framework.includes("junit") || framework.includes("java")) {
300
+ const className = `${pascalIdent(behavior)}Test`;
301
+ if (framework.includes("junit4")) {
302
+ return [
303
+ "import org.junit.Test;",
304
+ "",
305
+ "import static org.junit.Assert.assertTrue;",
306
+ "",
307
+ `class ${className} {`,
308
+ " @Test",
309
+ ` public void ${camelIdent(behavior)}() {`,
310
+ ` // ${behavior}`,
311
+ ` // acceptance: ${ac}`,
312
+ " // TODO: arrange + act on the subject under test, then assert the real outcome.",
313
+ ` assertTrue(${JSON.stringify(`expected ${first}`)}, true);`,
314
+ " }",
315
+ "}",
316
+ ""
317
+ ].join("\n");
318
+ }
319
+ return [
320
+ "import org.junit.jupiter.api.Test;",
321
+ "",
322
+ "import static org.junit.jupiter.api.Assertions.assertTrue;",
323
+ "",
324
+ `class ${className} {`,
325
+ " @Test",
326
+ ` void ${camelIdent(behavior)}() {`,
327
+ ` // ${behavior}`,
328
+ ` // acceptance: ${ac}`,
329
+ " // TODO: arrange + act on the subject under test, then assert the real outcome.",
330
+ ` assertTrue(true, ${JSON.stringify(`expected ${first}`)});`,
331
+ " }",
332
+ "}",
333
+ ""
334
+ ].join("\n");
335
+ }
336
+ if (framework.includes("playwright")) {
337
+ return [
338
+ `test("${title}", async ({ page }) => {`,
339
+ ` // acceptance: ${ac}`,
340
+ " // TODO: navigate (page.goto), interact, then assert the real outcome.",
341
+ " expect(true).toBeTruthy(); // placeholder — replace with a concrete assertion",
342
+ "});",
343
+ ""
344
+ ].join("\n");
345
+ }
346
+ if (framework.includes("cypress")) {
347
+ return [
348
+ `describe("${title}", () => {`,
349
+ ` it("${first}", () => {`,
350
+ ` // acceptance: ${ac}`,
351
+ " // TODO: drive the UI with cy.* and assert the real outcome.",
352
+ ' cy.wrap(true).should("eq", true); // placeholder — replace with a concrete assertion',
353
+ " });",
354
+ "});",
355
+ ""
356
+ ].join("\n");
357
+ }
358
+ if (framework.includes("ava")) {
359
+ return [
360
+ 'import test from "ava";',
361
+ "",
362
+ `test("${title}", (t) => {`,
363
+ ` // acceptance: ${ac}`,
364
+ " // TODO: arrange + act on the subject under test, then assert the real outcome.",
365
+ " t.true(true); // placeholder — replace with a concrete assertion",
366
+ "});",
367
+ ""
368
+ ].join("\n");
369
+ }
370
+ // jest / vitest / mocha / generic
371
+ return [
372
+ `describe("${title}", () => {`,
373
+ ` it("${first}", () => {`,
374
+ ` // acceptance: ${ac}`,
375
+ " // TODO: arrange + act on the subject under test, then assert the real outcome.",
376
+ " expect(true).toBe(true); // placeholder — replace with a concrete assertion",
377
+ " });",
378
+ "});",
379
+ ""
380
+ ].join("\n");
381
+ }
382
+ /**
383
+ * Construct a real BYOK provider from a resolved config. Requires a non-null
384
+ * config and throws on an unknown provider — it never silently falls back to the
385
+ * deterministic stand-in. The deterministic provider must be requested
386
+ * explicitly (`new DeterministicProvider()` at the call site) so the BYOK
387
+ * contract cannot be reintroduced by accident.
388
+ */
389
+ export function buildProvider(cfg, fetchImpl = fetch) {
390
+ switch (cfg.provider) {
391
+ case "openai":
392
+ return new OpenAICompatibleProvider(cfg, fetchImpl);
393
+ case "ollama":
394
+ return new OllamaProvider(cfg, fetchImpl);
395
+ case "anthropic":
396
+ return new AnthropicProvider(cfg, fetchImpl);
397
+ default:
398
+ throw new Error(`Unknown model provider: ${String(cfg.provider)}. Use provider="deterministic" for the offline stand-in.`);
399
+ }
400
+ }