@orangepro/orangepro-mcp 0.2.44 → 0.2.46
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 +322 -30
- package/dist/local/analyze/treeSitter/engine.js +1126 -90
- package/dist/local/autoProve.js +208 -102
- package/dist/local/operations.js +82 -18
- package/dist/local/proofDoctor.js +23 -3
- package/dist/local/provenance.js +2 -2
- package/dist/local/score/risk.js +339 -111
- package/dist/local/score/riskConfig.js +21 -1
- package/dist/local/viz/behaviorReportData.js +83 -20
- package/dist/local/viz/behaviorReportHtml.js +13 -2
- package/package.json +1 -1
- package/scripts/spikes/python-dynamic-proof-spike.mjs +399 -103
- package/scripts/spikes/python-mutate.py +72 -24
|
@@ -11,12 +11,18 @@ import { join } from "node:path";
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
export const DEFAULT_RISK_CONFIG = {
|
|
13
13
|
classification: { test_support_paths: [], scheduled_entry_paths: [], destructive_sinks: [], sensitivity_ignore: [], rank_exclude_paths: [] },
|
|
14
|
-
tuning: { irreversibility_floor: true, silence_multiplier: true },
|
|
14
|
+
tuning: { irreversibility_floor: true, silence_multiplier: true, churn_window_days: 180, churn_max_commits: 20_000, churn_timeout_seconds: 300 },
|
|
15
|
+
proof: { python_runner: "auto", attempt_limit: 20, baseline_green_target: 5 },
|
|
15
16
|
overrides: []
|
|
16
17
|
};
|
|
17
18
|
function asStringArray(v) {
|
|
18
19
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
19
20
|
}
|
|
21
|
+
function boundedInt(value, fallback, min, max) {
|
|
22
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
23
|
+
return fallback;
|
|
24
|
+
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
25
|
+
}
|
|
20
26
|
/** Glob → RegExp: `*` matches within a path segment, `**` matches across segments. */
|
|
21
27
|
export function globToRegExp(glob) {
|
|
22
28
|
const esc = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\u0000").replace(/\*/g, "[^/]*").replace(/\u0000/g, ".*");
|
|
@@ -43,6 +49,20 @@ function applyFile(cfg, file, warnings, label) {
|
|
|
43
49
|
cfg.tuning.irreversibility_floor = tun.irreversibility_floor;
|
|
44
50
|
if (typeof tun.silence_multiplier === "boolean")
|
|
45
51
|
cfg.tuning.silence_multiplier = tun.silence_multiplier;
|
|
52
|
+
if (tun.churn_window_days !== undefined)
|
|
53
|
+
cfg.tuning.churn_window_days = boundedInt(tun.churn_window_days, cfg.tuning.churn_window_days, 1, 3_650);
|
|
54
|
+
if (tun.churn_max_commits !== undefined)
|
|
55
|
+
cfg.tuning.churn_max_commits = boundedInt(tun.churn_max_commits, cfg.tuning.churn_max_commits, 100, 200_000);
|
|
56
|
+
if (tun.churn_timeout_seconds !== undefined)
|
|
57
|
+
cfg.tuning.churn_timeout_seconds = boundedInt(tun.churn_timeout_seconds, cfg.tuning.churn_timeout_seconds, 10, 1_800);
|
|
58
|
+
const proof = (raw.proof ?? {});
|
|
59
|
+
if (typeof proof.python_runner === "string" && proof.python_runner.trim() !== "") {
|
|
60
|
+
cfg.proof.python_runner = proof.python_runner.trim();
|
|
61
|
+
}
|
|
62
|
+
if (proof.attempt_limit !== undefined)
|
|
63
|
+
cfg.proof.attempt_limit = boundedInt(proof.attempt_limit, cfg.proof.attempt_limit, 1, 100);
|
|
64
|
+
if (proof.baseline_green_target !== undefined)
|
|
65
|
+
cfg.proof.baseline_green_target = boundedInt(proof.baseline_green_target, cfg.proof.baseline_green_target, 1, 100);
|
|
46
66
|
for (const o of Array.isArray(raw.overrides) ? raw.overrides : []) {
|
|
47
67
|
const ov = o;
|
|
48
68
|
if (typeof ov.symbol !== "string" || !["suppress", "pin", "reclassify"].includes(ov.action ?? "")) {
|
|
@@ -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) {
|
|
@@ -175,21 +178,42 @@ function proofGuidance(ledger, summary, dyn) {
|
|
|
175
178
|
// — behaviors, flows, Statically Linked signals — is unaffected; only the dynamic pass is small.
|
|
176
179
|
if (dyn) {
|
|
177
180
|
if (dyn.attempted === 0) {
|
|
181
|
+
const why = dyn.status === "disabled"
|
|
182
|
+
? " Auto-prove was disabled for this run (--no-auto)."
|
|
183
|
+
: dyn.scopedChangedFiles !== undefined
|
|
184
|
+
? ` The pass was scoped to ${dyn.scopedChangedFiles.toLocaleString()} changed file${dyn.scopedChangedFiles === 1 ? "" : "s"} in the working tree, and none had a provable target with a linked test.`
|
|
185
|
+
: dyn.status === "no-targets"
|
|
186
|
+
? " No provable target with a linked existing test was found."
|
|
187
|
+
: dyn.reason
|
|
188
|
+
? ` ${dyn.reason}`
|
|
189
|
+
: "";
|
|
178
190
|
return {
|
|
179
191
|
state: "not_started",
|
|
180
192
|
title: "0 Dynamically Proven — dynamic proof was not attempted",
|
|
181
|
-
body:
|
|
193
|
+
body: `Dynamic proof was not attempted in this run.${why} Static behavior mapping and Statically Linked signals are still available.`,
|
|
182
194
|
action: HANDOFF_ACTION
|
|
183
195
|
};
|
|
184
196
|
}
|
|
185
197
|
const dom = dominantBlockReason(dyn.needsSetup);
|
|
198
|
+
const environmentClasses = new Set(["environment_unavailable", "collection_error"]);
|
|
199
|
+
const allEnvironmentUnavailable = dyn.attempted > 0
|
|
200
|
+
&& dyn.needsSetup.length >= dyn.attempted
|
|
201
|
+
&& dyn.needsSetup.every((attempt) => environmentClasses.has(attempt.category ?? ""));
|
|
202
|
+
if (allEnvironmentUnavailable) {
|
|
203
|
+
return {
|
|
204
|
+
state: "attempted",
|
|
205
|
+
title: "Proof not run: test environment could not be started (see ledger)",
|
|
206
|
+
body: "Proof not run: test environment could not be started (see ledger)",
|
|
207
|
+
action: HANDOFF_ACTION
|
|
208
|
+
};
|
|
209
|
+
}
|
|
186
210
|
const allBlocked = dyn.needsSetup.length > 0 && dyn.needsSetup.length >= dyn.attempted;
|
|
187
211
|
const plural = dyn.attempted === 1 ? "" : "s";
|
|
188
212
|
if (allBlocked && dom) {
|
|
189
213
|
return {
|
|
190
214
|
state: "attempted",
|
|
191
215
|
title: `0 Dynamically Proven — top ${dyn.attempted} attempted, all setup-blocked`,
|
|
192
|
-
body: `Dynamic proof attempted ${dyn.attempted} target${plural};
|
|
216
|
+
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
217
|
action: nextStepFor(dom) ?? HANDOFF_ACTION
|
|
194
218
|
};
|
|
195
219
|
}
|
|
@@ -204,6 +228,15 @@ function proofGuidance(ledger, summary, dyn) {
|
|
|
204
228
|
// Standalone report regen (no THIS-RUN data) → derive from the ledger only.
|
|
205
229
|
const dynamicAttempts = ledger.records.filter((r) => r.dynamic_proof?.proof_kind === "dynamic_targeted");
|
|
206
230
|
if (dynamicAttempts.length > 0) {
|
|
231
|
+
const unavailableClasses = new Set(["env_unavailable", "collection_error"]);
|
|
232
|
+
if (dynamicAttempts.every((attempt) => unavailableClasses.has(attempt.dynamic_proof?.failure_class ?? ""))) {
|
|
233
|
+
return {
|
|
234
|
+
state: "attempted",
|
|
235
|
+
title: "Proof not run: test environment could not be started (see ledger)",
|
|
236
|
+
body: "Proof not run: test environment could not be started (see ledger)",
|
|
237
|
+
action: HANDOFF_ACTION
|
|
238
|
+
};
|
|
239
|
+
}
|
|
207
240
|
return {
|
|
208
241
|
state: "attempted",
|
|
209
242
|
title: "0 Dynamically Proven — dynamic proof ran, none closed yet",
|
|
@@ -242,6 +275,12 @@ function scanBlock(graph, rows) {
|
|
|
242
275
|
const unit = tests.filter((n) => n.properties.test_layer === "unit" || n.properties.test_layer === "component").length;
|
|
243
276
|
const denominator = graph.analysis?.denominator;
|
|
244
277
|
const runtime = graph.analysis?.runtime_coverage;
|
|
278
|
+
const rankingExclusions = new Map();
|
|
279
|
+
for (const node of graph.nodes) {
|
|
280
|
+
const code = node.kind === "CodeSymbol" && node.denominator_eligible === true ? node.properties.ranking_exclusion_code : undefined;
|
|
281
|
+
if (typeof code === "string" && code)
|
|
282
|
+
rankingExclusions.set(code, (rankingExclusions.get(code) ?? 0) + 1);
|
|
283
|
+
}
|
|
245
284
|
const excludedCount = (denominator?.excluded_boilerplate ?? 0) +
|
|
246
285
|
(denominator?.excluded_infra ?? 0) +
|
|
247
286
|
(denominator?.excluded_generated ?? 0) +
|
|
@@ -269,7 +308,8 @@ function scanBlock(graph, rows) {
|
|
|
269
308
|
count: excludedCount > 0 ? String(excludedCount) : "0",
|
|
270
309
|
text: "non-behavior symbols were excluded from the behavior count — generated code, framework internals, test-inferred flows, and infrastructure plumbing."
|
|
271
310
|
},
|
|
272
|
-
excludedCliCommands: excludedCliCommands(graph)
|
|
311
|
+
excludedCliCommands: excludedCliCommands(graph),
|
|
312
|
+
rankingExclusions: [...rankingExclusions.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([code, count]) => ({ code, count }))
|
|
273
313
|
};
|
|
274
314
|
}
|
|
275
315
|
function behaviorLists(rows, flowIds) {
|
|
@@ -455,7 +495,7 @@ function displayTitle(title, file) {
|
|
|
455
495
|
}
|
|
456
496
|
/** Deterministic 1–2 line behavior context from graph facts only — no LLM.
|
|
457
497
|
* Sensitivity label mirrors deriveDataSensitivity's tiers. */
|
|
458
|
-
function riskContext(risk) {
|
|
498
|
+
export function riskContext(risk, churnWindowDays, churnMeta, sameSinkAs) {
|
|
459
499
|
const sens = (risk.data_sensitivity ?? 1) >= 10 ? "payment/billing-sensitive"
|
|
460
500
|
: (risk.data_sensitivity ?? 1) >= 9 ? "auth/session-sensitive"
|
|
461
501
|
: (risk.data_sensitivity ?? 1) >= 7 ? "order/transaction"
|
|
@@ -469,10 +509,11 @@ function riskContext(risk) {
|
|
|
469
509
|
: "deep in the call graph";
|
|
470
510
|
const sink = risk.sink_callee;
|
|
471
511
|
const scheduled = risk.scheduled_entry === true;
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
512
|
+
const churn = churnMeta.state === "partial"
|
|
513
|
+
? `${risk.git_churn} churn line${risk.git_churn === 1 ? "" : "s"} in file over partial change history (${churnMeta.commitsScanned} commits)`
|
|
514
|
+
: churnMeta.state === "complete" && risk.churn_available !== false
|
|
515
|
+
? `${risk.git_churn} churn line${risk.git_churn === 1 ? "" : "s"} in file over ${churnWindowDays} days`
|
|
516
|
+
: "change history unavailable";
|
|
476
517
|
const tier = risk.detection_tier ?? "";
|
|
477
518
|
const evidence = tier === "candidate"
|
|
478
519
|
? "no test links here (a similarly-named test exists but never calls it)"
|
|
@@ -492,6 +533,7 @@ function riskContext(risk) {
|
|
|
492
533
|
const seen = [
|
|
493
534
|
pos + (sens ? ` on ${sens} paths` : ""),
|
|
494
535
|
...(sink ? [`reaches \`${sink}\` within two calls`] : []),
|
|
536
|
+
...(sink && sameSinkAs ? [`same delete call as \`${sameSinkAs}\` (ranked higher)`] : []),
|
|
495
537
|
...(scheduled ? ["scheduled / queue-triggered"] : []),
|
|
496
538
|
`${risk.fan_out ?? 0} downstream call${(risk.fan_out ?? 0) === 1 ? "" : "s"}`,
|
|
497
539
|
churn,
|
|
@@ -902,13 +944,19 @@ function riskTodo(risk, verb, path, generatedTests) {
|
|
|
902
944
|
}
|
|
903
945
|
return `No test signal exists. Start with one integration test that ${call} and asserts the observable outcome.${sens}`;
|
|
904
946
|
}
|
|
905
|
-
function riskRows(risks, graph) {
|
|
947
|
+
function riskRows(risks, graph, churnWindowDays, churnMeta) {
|
|
906
948
|
const maxRiskScore = risks.reduce((m, r) => Math.max(m, r.risk_score), 0);
|
|
907
949
|
const riskIds = new Set(risks.map((r) => r.id));
|
|
908
950
|
const firstRowForFile = new Map();
|
|
909
951
|
for (const r of risks)
|
|
910
952
|
if (!firstRowForFile.has(r.file))
|
|
911
953
|
firstRowForFile.set(r.file, r.id);
|
|
954
|
+
// R8.10 (display only): rows that reach the SAME destructive call site through
|
|
955
|
+
// forwarding wrappers name the higher-ranked row. Order and membership unchanged.
|
|
956
|
+
const firstRowForSink = new Map();
|
|
957
|
+
for (const r of risks)
|
|
958
|
+
if (r.sink_owner && !firstRowForSink.has(r.sink_owner))
|
|
959
|
+
firstRowForSink.set(r.sink_owner, { id: r.id, title: r.title });
|
|
912
960
|
// Ambiguity-qualified display: identical titles from DIFFERENT files
|
|
913
961
|
// (multi-program repos: 76 x main) get their top-level dir as a prefix.
|
|
914
962
|
// Purely display; single-file titles render unchanged everywhere else.
|
|
@@ -958,7 +1006,9 @@ function riskRows(risks, graph) {
|
|
|
958
1006
|
todo: riskTodo(risk, verb, path, generatedTests)
|
|
959
1007
|
};
|
|
960
1008
|
})(),
|
|
961
|
-
context: riskContext(risk)
|
|
1009
|
+
context: riskContext(risk, churnWindowDays, churnMeta, risk.sink_owner && firstRowForSink.get(risk.sink_owner) && firstRowForSink.get(risk.sink_owner).id !== risk.id
|
|
1010
|
+
? firstRowForSink.get(risk.sink_owner).title
|
|
1011
|
+
: undefined),
|
|
962
1012
|
desc: risk.reasons.join(" · "),
|
|
963
1013
|
tags
|
|
964
1014
|
};
|
|
@@ -1008,11 +1058,22 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
1008
1058
|
provenIds,
|
|
1009
1059
|
includeAssociated: true
|
|
1010
1060
|
}).filter((r) => r.sink_callee).slice(0, 20)
|
|
1011
|
-
.map((r
|
|
1061
|
+
.map((r, _index, rows) => {
|
|
1062
|
+
const first = rows.find((x) => x.sink_owner && x.sink_owner === r.sink_owner);
|
|
1063
|
+
return {
|
|
1064
|
+
path: r.title,
|
|
1065
|
+
file: r.file,
|
|
1066
|
+
score: r.risk_score,
|
|
1067
|
+
sink: r.sink_callee ?? "",
|
|
1068
|
+
...(first && first.id !== r.id ? { sameSinkAs: first.title } : {})
|
|
1069
|
+
};
|
|
1070
|
+
});
|
|
1012
1071
|
const riskHealth = inspectRiskInputHealth(repoRoot);
|
|
1013
|
-
const churnAvailable = riskHealth.churnAvailable && riskGaps.every((risk) => risk.churn_available !== false);
|
|
1014
|
-
const configHash = loadRiskConfig(repoRoot).hash;
|
|
1015
1072
|
const codeFiles = Object.entries(graph.manifest.files).filter(([, file]) => file.kind === "code").map(([file]) => file);
|
|
1073
|
+
const churnMeta = inspectRiskChurn(repoRoot, codeFiles, riskHealth.churnWindow);
|
|
1074
|
+
const churnAvailable = churnMeta.state === "complete" && riskGaps.every((risk) => risk.churn_available !== false);
|
|
1075
|
+
const configDisclosure = configDisclosureFor(graph, repoRoot);
|
|
1076
|
+
const configHash = configDisclosure.hash;
|
|
1016
1077
|
const identity = buildArtifactIdentity(graph, {
|
|
1017
1078
|
configHash,
|
|
1018
1079
|
history: {
|
|
@@ -1028,16 +1089,18 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
1028
1089
|
gitRoot: riskHealth.gitRoot ? path.basename(riskHealth.gitRoot) : null,
|
|
1029
1090
|
commit: riskHealth.commit,
|
|
1030
1091
|
history: riskHealth.history,
|
|
1031
|
-
churn:
|
|
1092
|
+
churn: churnMeta.state === "complete" ? "available" : churnMeta.state,
|
|
1093
|
+
churnState: churnMeta.state,
|
|
1094
|
+
commitsScanned: churnMeta.commitsScanned,
|
|
1032
1095
|
churnWindow: riskHealth.churnWindow,
|
|
1033
1096
|
toolVersion: identity.tool_version,
|
|
1034
1097
|
configHash,
|
|
1035
1098
|
identity,
|
|
1036
1099
|
inputFingerprint: identity.run_fingerprint.replace(/^sha256:/, "").slice(0, 16),
|
|
1037
|
-
reason:
|
|
1100
|
+
reason: churnMeta.state === "complete" ? undefined : (churnMeta.reason ?? riskHealth.reason ?? "error: churn acquisition returned no reason")
|
|
1038
1101
|
};
|
|
1039
1102
|
const lists = behaviorLists(rows, flowIds);
|
|
1040
|
-
const risks = riskRows(riskGaps, graph);
|
|
1103
|
+
const risks = riskRows(riskGaps, graph, configDisclosure.tuning.churn_window_days, churnMeta);
|
|
1041
1104
|
const sortedBehaviors = [...lists.behaviors].sort((a, b) => tierRank(a) - tierRank(b));
|
|
1042
1105
|
const flowRows = flows(graph, rows, riskGaps);
|
|
1043
1106
|
return {
|
|
@@ -1057,7 +1120,7 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
1057
1120
|
candidateFlows: candidateFlows(graph),
|
|
1058
1121
|
risks,
|
|
1059
1122
|
worklists: { changeFrontier, irreversible },
|
|
1060
|
-
configDisclosure
|
|
1123
|
+
configDisclosure,
|
|
1061
1124
|
zeroProofExplainer: summary.proven === 0 ? { title: ZERO_PROOF_EXPLAINER.title, body: [...ZERO_PROOF_EXPLAINER.body] } : null,
|
|
1062
1125
|
mapModel: buildSystemMapModel({ flows: flowRows, risks, behaviors: sortedBehaviors }),
|
|
1063
1126
|
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||(T.churn_max_commits??20000)!==20000||(T.churn_timeout_seconds??300)!==300||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> · history bound <b>'+esc(String(T.churn_max_commits??20000))+' commits / '+esc(String(T.churn_timeout_seconds??300))+' s</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>';});
|
|
@@ -1075,7 +1086,7 @@ renderRisks();
|
|
|
1075
1086
|
return \`<div class="wl-row" data-path="\${esc(path).replace(/"/g,'"')}" title="\${inTop?'in the ranked list below — click to jump':'ranked, but below the top-20 cut'}"><span class="wl-path">\${esc(path)}</span><span class="wl-meta">\${esc(meta)}</span></div>\`;
|
|
1076
1087
|
};
|
|
1077
1088
|
const cf=(W.changeFrontier||[]).map(r=>row(r.path,"changes "+(r.probability>=7?"a lot":r.probability>=4?"often":"some"))).join("")||'<div class="wl-empty">nothing changing fast and unproven</div>';
|
|
1078
|
-
const ir=(W.irreversible||[]).map(r=>row(r.path,"→ "+(r.sink||"").split(".").pop())).join("")||'<div class="wl-empty">no unproven path reaches a delete</div>';
|
|
1089
|
+
const ir=(W.irreversible||[]).map(r=>row(r.path,"→ "+(r.sink||"").split(".").pop()+(r.sameSinkAs?" · same delete as "+r.sameSinkAs:""))).join("")||'<div class="wl-empty">no unproven path reaches a delete</div>';
|
|
1079
1090
|
host.innerHTML=\`<div class="wl-grid">
|
|
1080
1091
|
<div class="wl-card"><p class="wl-title wl-change">Changing fast · unproven</p><p class="wl-sub">Where the code moves most with nothing proving it. The place a bug is most likely to have just arrived.</p>\${cf}</div>
|
|
1081
1092
|
<div class="wl-card"><p class="wl-title wl-irrev">Can destroy data · unproven</p><p class="wl-sub">Paths that reach a delete or purge with nothing proving they do the right thing. Rarely changing — which is why nobody looks.</p>\${ir}</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangepro/orangepro-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.46",
|
|
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",
|