@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.
@@ -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 tries to dynamically prove the top 5 highest-risk behaviors by default. If setup is missing, the report shows the reason, such as missing dependencies, database setup, environment variables, or unsupported test runner configuration."
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}; all were blocked by ${dom.label} (${dom.count}/${dom.total}). This is a sandbox setup gap, not a static-test failure — the Statically Linked signals are still shown.`,
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 churnKnown = risk.churn_available !== false;
473
- const churn = churnKnown
474
- ? (risk.git_churn > 0 ? `${risk.git_churn} line${risk.git_churn === 1 ? "" : "s"} changed in 180 days` : "unchanged in 180 days")
475
- : "change history unavailable";
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: churnAvailable ? "available" : "unavailable",
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: churnAvailable ? undefined : (riskHealth.reason ?? "Git churn scan did not complete")
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: configDisclosureFor(graph, repoRoot),
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.44",
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",