@orangepro/orangepro-mcp 0.2.44 → 0.2.45
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/dist/local/analyze/analyzer.js +318 -30
- package/dist/local/analyze/treeSitter/engine.js +1058 -90
- package/dist/local/autoProve.js +208 -102
- package/dist/local/operations.js +65 -16
- package/dist/local/proofDoctor.js +20 -3
- package/dist/local/provenance.js +2 -2
- package/dist/local/score/risk.js +284 -104
- package/dist/local/score/riskConfig.js +17 -1
- package/dist/local/viz/behaviorReportData.js +54 -18
- package/dist/local/viz/behaviorReportHtml.js +12 -1
- package/package.json +1 -1
- package/scripts/spikes/python-dynamic-proof-spike.mjs +399 -103
- package/scripts/spikes/python-mutate.py +72 -24
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { loadRiskConfig } from "../score/riskConfig.js";
|
|
2
1
|
import { configDisclosureFor } from "../score/risk.js";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { buildRtm } from "../rtm.js";
|
|
5
|
-
import { inspectRiskInputHealth, isEntryPoint, rankPriorityGaps, rankRiskGaps, riskHistoryFingerprint } from "../score/risk.js";
|
|
4
|
+
import { inspectRiskChurn, inspectRiskInputHealth, isEntryPoint, rankPriorityGaps, rankRiskGaps, riskHistoryFingerprint } from "../score/risk.js";
|
|
6
5
|
import { PROOF_BLOCKER_GUIDE } from "../proofDoctor.js";
|
|
7
6
|
import { classifyGeneratedDraftBlocker } from "../generate/draftGuidance.js";
|
|
8
7
|
import { buildArtifactIdentity, comparisonCompatibility } from "../provenance.js";
|
|
9
8
|
/** Short human phrase per R-1 needs_setup category, for the "blocked because: …" panel copy. */
|
|
10
9
|
const BLOCK_CATEGORY_LABEL = {
|
|
10
|
+
environment_unavailable: "a Python test environment that could not start",
|
|
11
|
+
collection_error: "a Python test collection or import error",
|
|
12
|
+
go_package_build_failure: "a Go package that could not build for proof",
|
|
13
|
+
runner_missing: "a missing test runner in the selected package",
|
|
11
14
|
module_not_found: "a missing module or dependency in the sandbox",
|
|
12
15
|
tsconfig_missing: "a monorepo tsconfig the sandbox can't resolve (a parent config the package extends)",
|
|
13
16
|
experimental_builtin: "an experimental Node builtin that needs a runtime flag",
|
|
@@ -137,7 +140,7 @@ const ZERO_PROOF_EXPLAINER = {
|
|
|
137
140
|
body: [
|
|
138
141
|
"OrangePro mapped behaviors, flows, and static test links without running your app. Dynamic proof requires executing tests in a sandbox and checking whether a test fails when the target behavior is mutated.",
|
|
139
142
|
"The statically linked behaviors have test evidence, but they are not verified yet. Treat them as likely covered, not proven.",
|
|
140
|
-
"OrangePro
|
|
143
|
+
"OrangePro uses a bounded proof pass. For Python, it can examine up to the configured attempt limit to find the configured number of green baselines; environment or collection failures are recorded without being mistaken for failed product tests."
|
|
141
144
|
]
|
|
142
145
|
};
|
|
143
146
|
function pipeline(graph, ledger, summary) {
|
|
@@ -183,13 +186,25 @@ function proofGuidance(ledger, summary, dyn) {
|
|
|
183
186
|
};
|
|
184
187
|
}
|
|
185
188
|
const dom = dominantBlockReason(dyn.needsSetup);
|
|
189
|
+
const environmentClasses = new Set(["environment_unavailable", "collection_error"]);
|
|
190
|
+
const allEnvironmentUnavailable = dyn.attempted > 0
|
|
191
|
+
&& dyn.needsSetup.length >= dyn.attempted
|
|
192
|
+
&& dyn.needsSetup.every((attempt) => environmentClasses.has(attempt.category ?? ""));
|
|
193
|
+
if (allEnvironmentUnavailable) {
|
|
194
|
+
return {
|
|
195
|
+
state: "attempted",
|
|
196
|
+
title: "Proof not run: test environment could not be started (see ledger)",
|
|
197
|
+
body: "Proof not run: test environment could not be started (see ledger)",
|
|
198
|
+
action: HANDOFF_ACTION
|
|
199
|
+
};
|
|
200
|
+
}
|
|
186
201
|
const allBlocked = dyn.needsSetup.length > 0 && dyn.needsSetup.length >= dyn.attempted;
|
|
187
202
|
const plural = dyn.attempted === 1 ? "" : "s";
|
|
188
203
|
if (allBlocked && dom) {
|
|
189
204
|
return {
|
|
190
205
|
state: "attempted",
|
|
191
206
|
title: `0 Dynamically Proven — top ${dyn.attempted} attempted, all setup-blocked`,
|
|
192
|
-
body: `Dynamic proof attempted ${dyn.attempted} target${plural};
|
|
207
|
+
body: `Dynamic proof attempted ${dyn.attempted} target${plural}; none reached a green baseline. Most common block: ${dom.label} (${dom.count}/${dom.total} actual attempts). This is a test-environment/setup gap, not a static-test failure — the Statically Linked signals are still shown.`,
|
|
193
208
|
action: nextStepFor(dom) ?? HANDOFF_ACTION
|
|
194
209
|
};
|
|
195
210
|
}
|
|
@@ -204,6 +219,15 @@ function proofGuidance(ledger, summary, dyn) {
|
|
|
204
219
|
// Standalone report regen (no THIS-RUN data) → derive from the ledger only.
|
|
205
220
|
const dynamicAttempts = ledger.records.filter((r) => r.dynamic_proof?.proof_kind === "dynamic_targeted");
|
|
206
221
|
if (dynamicAttempts.length > 0) {
|
|
222
|
+
const unavailableClasses = new Set(["env_unavailable", "collection_error"]);
|
|
223
|
+
if (dynamicAttempts.every((attempt) => unavailableClasses.has(attempt.dynamic_proof?.failure_class ?? ""))) {
|
|
224
|
+
return {
|
|
225
|
+
state: "attempted",
|
|
226
|
+
title: "Proof not run: test environment could not be started (see ledger)",
|
|
227
|
+
body: "Proof not run: test environment could not be started (see ledger)",
|
|
228
|
+
action: HANDOFF_ACTION
|
|
229
|
+
};
|
|
230
|
+
}
|
|
207
231
|
return {
|
|
208
232
|
state: "attempted",
|
|
209
233
|
title: "0 Dynamically Proven — dynamic proof ran, none closed yet",
|
|
@@ -242,6 +266,12 @@ function scanBlock(graph, rows) {
|
|
|
242
266
|
const unit = tests.filter((n) => n.properties.test_layer === "unit" || n.properties.test_layer === "component").length;
|
|
243
267
|
const denominator = graph.analysis?.denominator;
|
|
244
268
|
const runtime = graph.analysis?.runtime_coverage;
|
|
269
|
+
const rankingExclusions = new Map();
|
|
270
|
+
for (const node of graph.nodes) {
|
|
271
|
+
const code = node.kind === "CodeSymbol" && node.denominator_eligible === true ? node.properties.ranking_exclusion_code : undefined;
|
|
272
|
+
if (typeof code === "string" && code)
|
|
273
|
+
rankingExclusions.set(code, (rankingExclusions.get(code) ?? 0) + 1);
|
|
274
|
+
}
|
|
245
275
|
const excludedCount = (denominator?.excluded_boilerplate ?? 0) +
|
|
246
276
|
(denominator?.excluded_infra ?? 0) +
|
|
247
277
|
(denominator?.excluded_generated ?? 0) +
|
|
@@ -269,7 +299,8 @@ function scanBlock(graph, rows) {
|
|
|
269
299
|
count: excludedCount > 0 ? String(excludedCount) : "0",
|
|
270
300
|
text: "non-behavior symbols were excluded from the behavior count — generated code, framework internals, test-inferred flows, and infrastructure plumbing."
|
|
271
301
|
},
|
|
272
|
-
excludedCliCommands: excludedCliCommands(graph)
|
|
302
|
+
excludedCliCommands: excludedCliCommands(graph),
|
|
303
|
+
rankingExclusions: [...rankingExclusions.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([code, count]) => ({ code, count }))
|
|
273
304
|
};
|
|
274
305
|
}
|
|
275
306
|
function behaviorLists(rows, flowIds) {
|
|
@@ -455,7 +486,7 @@ function displayTitle(title, file) {
|
|
|
455
486
|
}
|
|
456
487
|
/** Deterministic 1–2 line behavior context from graph facts only — no LLM.
|
|
457
488
|
* Sensitivity label mirrors deriveDataSensitivity's tiers. */
|
|
458
|
-
function riskContext(risk) {
|
|
489
|
+
export function riskContext(risk, churnWindowDays, churnMeta) {
|
|
459
490
|
const sens = (risk.data_sensitivity ?? 1) >= 10 ? "payment/billing-sensitive"
|
|
460
491
|
: (risk.data_sensitivity ?? 1) >= 9 ? "auth/session-sensitive"
|
|
461
492
|
: (risk.data_sensitivity ?? 1) >= 7 ? "order/transaction"
|
|
@@ -469,10 +500,11 @@ function riskContext(risk) {
|
|
|
469
500
|
: "deep in the call graph";
|
|
470
501
|
const sink = risk.sink_callee;
|
|
471
502
|
const scheduled = risk.scheduled_entry === true;
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
503
|
+
const churn = churnMeta.state === "partial"
|
|
504
|
+
? `${risk.git_churn} churn line${risk.git_churn === 1 ? "" : "s"} in file over partial change history (${churnMeta.commitsScanned} commits)`
|
|
505
|
+
: churnMeta.state === "complete" && risk.churn_available !== false
|
|
506
|
+
? `${risk.git_churn} churn line${risk.git_churn === 1 ? "" : "s"} in file over ${churnWindowDays} days`
|
|
507
|
+
: "change history unavailable";
|
|
476
508
|
const tier = risk.detection_tier ?? "";
|
|
477
509
|
const evidence = tier === "candidate"
|
|
478
510
|
? "no test links here (a similarly-named test exists but never calls it)"
|
|
@@ -902,7 +934,7 @@ function riskTodo(risk, verb, path, generatedTests) {
|
|
|
902
934
|
}
|
|
903
935
|
return `No test signal exists. Start with one integration test that ${call} and asserts the observable outcome.${sens}`;
|
|
904
936
|
}
|
|
905
|
-
function riskRows(risks, graph) {
|
|
937
|
+
function riskRows(risks, graph, churnWindowDays, churnMeta) {
|
|
906
938
|
const maxRiskScore = risks.reduce((m, r) => Math.max(m, r.risk_score), 0);
|
|
907
939
|
const riskIds = new Set(risks.map((r) => r.id));
|
|
908
940
|
const firstRowForFile = new Map();
|
|
@@ -958,7 +990,7 @@ function riskRows(risks, graph) {
|
|
|
958
990
|
todo: riskTodo(risk, verb, path, generatedTests)
|
|
959
991
|
};
|
|
960
992
|
})(),
|
|
961
|
-
context: riskContext(risk),
|
|
993
|
+
context: riskContext(risk, churnWindowDays, churnMeta),
|
|
962
994
|
desc: risk.reasons.join(" · "),
|
|
963
995
|
tags
|
|
964
996
|
};
|
|
@@ -1010,9 +1042,11 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
1010
1042
|
}).filter((r) => r.sink_callee).slice(0, 20)
|
|
1011
1043
|
.map((r) => ({ path: r.title, file: r.file, score: r.risk_score, sink: r.sink_callee ?? "" }));
|
|
1012
1044
|
const riskHealth = inspectRiskInputHealth(repoRoot);
|
|
1013
|
-
const churnAvailable = riskHealth.churnAvailable && riskGaps.every((risk) => risk.churn_available !== false);
|
|
1014
|
-
const configHash = loadRiskConfig(repoRoot).hash;
|
|
1015
1045
|
const codeFiles = Object.entries(graph.manifest.files).filter(([, file]) => file.kind === "code").map(([file]) => file);
|
|
1046
|
+
const churnMeta = inspectRiskChurn(repoRoot, codeFiles, riskHealth.churnWindow);
|
|
1047
|
+
const churnAvailable = churnMeta.state === "complete" && riskGaps.every((risk) => risk.churn_available !== false);
|
|
1048
|
+
const configDisclosure = configDisclosureFor(graph, repoRoot);
|
|
1049
|
+
const configHash = configDisclosure.hash;
|
|
1016
1050
|
const identity = buildArtifactIdentity(graph, {
|
|
1017
1051
|
configHash,
|
|
1018
1052
|
history: {
|
|
@@ -1028,16 +1062,18 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
1028
1062
|
gitRoot: riskHealth.gitRoot ? path.basename(riskHealth.gitRoot) : null,
|
|
1029
1063
|
commit: riskHealth.commit,
|
|
1030
1064
|
history: riskHealth.history,
|
|
1031
|
-
churn:
|
|
1065
|
+
churn: churnMeta.state === "complete" ? "available" : churnMeta.state,
|
|
1066
|
+
churnState: churnMeta.state,
|
|
1067
|
+
commitsScanned: churnMeta.commitsScanned,
|
|
1032
1068
|
churnWindow: riskHealth.churnWindow,
|
|
1033
1069
|
toolVersion: identity.tool_version,
|
|
1034
1070
|
configHash,
|
|
1035
1071
|
identity,
|
|
1036
1072
|
inputFingerprint: identity.run_fingerprint.replace(/^sha256:/, "").slice(0, 16),
|
|
1037
|
-
reason:
|
|
1073
|
+
reason: churnMeta.state === "complete" ? undefined : (churnMeta.reason ?? riskHealth.reason ?? "error: churn acquisition returned no reason")
|
|
1038
1074
|
};
|
|
1039
1075
|
const lists = behaviorLists(rows, flowIds);
|
|
1040
|
-
const risks = riskRows(riskGaps, graph);
|
|
1076
|
+
const risks = riskRows(riskGaps, graph, configDisclosure.tuning.churn_window_days, churnMeta);
|
|
1041
1077
|
const sortedBehaviors = [...lists.behaviors].sort((a, b) => tierRank(a) - tierRank(b));
|
|
1042
1078
|
const flowRows = flows(graph, rows, riskGaps);
|
|
1043
1079
|
return {
|
|
@@ -1057,7 +1093,7 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
1057
1093
|
candidateFlows: candidateFlows(graph),
|
|
1058
1094
|
risks,
|
|
1059
1095
|
worklists: { changeFrontier, irreversible },
|
|
1060
|
-
configDisclosure
|
|
1096
|
+
configDisclosure,
|
|
1061
1097
|
zeroProofExplainer: summary.proven === 0 ? { title: ZERO_PROOF_EXPLAINER.title, body: [...ZERO_PROOF_EXPLAINER.body] } : null,
|
|
1062
1098
|
mapModel: buildSystemMapModel({ flows: flowRows, risks, behaviors: sortedBehaviors }),
|
|
1063
1099
|
delta: firstRunDelta(),
|
|
@@ -345,6 +345,7 @@ body[data-mode="expert"] .simple-only{display:none!important}
|
|
|
345
345
|
</header>
|
|
346
346
|
<div class="provenance" id="provenance"></div>
|
|
347
347
|
<div class="scope-note" id="cli-exclusions" hidden></div>
|
|
348
|
+
<div class="scope-note" id="ranking-exclusions" hidden></div>
|
|
348
349
|
|
|
349
350
|
<section class="kpis" id="kpis"></section>
|
|
350
351
|
<p class="metric-scope" id="metric-scope"></p>
|
|
@@ -694,6 +695,12 @@ if(excludedCli.length){
|
|
|
694
695
|
note.hidden=false;
|
|
695
696
|
note.textContent="CLI command entries excluded from the behavior count: "+excludedCli.map(x=>x.path+" ("+x.reason+")").join(" · ");
|
|
696
697
|
}
|
|
698
|
+
const rankExclusions=D.scan.rankingExclusions||[];
|
|
699
|
+
if(rankExclusions.length){
|
|
700
|
+
const note=$("#ranking-exclusions");
|
|
701
|
+
note.hidden=false;
|
|
702
|
+
note.textContent="Excluded from ranking only (still counted as behaviors): "+rankExclusions.map(x=>x.code+" "+x.count).join(" · ");
|
|
703
|
+
}
|
|
697
704
|
$("#fw-pill").textContent=D.framework;
|
|
698
705
|
|
|
699
706
|
// bridge text in codebase tab
|
|
@@ -1050,13 +1057,17 @@ renderRisks();
|
|
|
1050
1057
|
if(!C||!host)return;
|
|
1051
1058
|
const warn=(C.warnings||[]).length>0;
|
|
1052
1059
|
const K=C.classification||{};
|
|
1060
|
+
const P=C.proof||{};
|
|
1061
|
+
const T=C.tuning||{};
|
|
1053
1062
|
const clsSet=["test_support_paths","scheduled_entry_paths","destructive_sinks","sensitivity_ignore"].filter(k=>(K[k]||[]).length>0);
|
|
1054
|
-
const tuned=C.overridesActive>0||(C.rankExcludePaths||[]).length>0||!C.floor||!C.silence||clsSet.length>0;
|
|
1063
|
+
const tuned=C.overridesActive>0||(C.rankExcludePaths||[]).length>0||!C.floor||!C.silence||clsSet.length>0||(T.churn_window_days??180)!==180||P.python_runner!=="auto"||P.attempt_limit!==20||P.baseline_green_target!==5;
|
|
1055
1064
|
const bits=["config <b>"+esc(C.hash)+"</b>","overrides active <b>"+C.overridesActive+"</b>","suppressed <b>"+(C.suppressed||[]).length+"</b>"];
|
|
1056
1065
|
if((C.rankExcludePaths||[]).length)bits.push("ranking excludes <b>"+esc(C.rankExcludePaths.join(", "))+"</b>");
|
|
1057
1066
|
if(!C.floor)bits.push("<b>irreversibility floor OFF</b>");
|
|
1058
1067
|
if(!C.silence)bits.push("<b>silence multiplier OFF</b>");
|
|
1059
1068
|
let html="<div>"+bits.join(" · ")+"</div>";
|
|
1069
|
+
html+='<span class="cfg-row">Churn window: <b>'+esc(String(T.churn_window_days??180))+' days</b></span>';
|
|
1070
|
+
html+='<span class="cfg-row">Python proof: runner <b>'+esc(P.python_runner||"auto")+'</b> · attempt limit <b>'+esc(String(P.attempt_limit??20))+'</b> · green baseline target <b>'+esc(String(P.baseline_green_target??5))+'</b></span>';
|
|
1060
1071
|
clsSet.forEach(k=>{html+='<span class="cfg-row">'+esc(k)+': <b>'+esc(K[k].join(", "))+'</b></span>';});
|
|
1061
1072
|
if((C.sensitivityIgnored||[]).length)html+='<span class="cfg-row">sensitivity ignored by config (score changed): <b>'+esc(C.sensitivityIgnored.join(", "))+'</b></span>';
|
|
1062
1073
|
(C.suppressed||[]).forEach(s=>{html+='<span class="cfg-row">suppressed by config: <b>'+esc(s.symbol)+'</b> — '+esc(s.reason)+'</span>';});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangepro/orangepro-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.45",
|
|
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",
|