@orangepro/orangepro-mcp 0.2.37 → 0.2.39
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/operations.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadRiskConfig } from "./score/riskConfig.js";
|
|
1
2
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
@@ -1553,6 +1554,10 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
|
|
|
1553
1554
|
}, deps);
|
|
1554
1555
|
const warnings = [...analyze.warnings];
|
|
1555
1556
|
reportProgress("start: deterministic graph is ready", { current: 4, total: 8 });
|
|
1557
|
+
// A per-repo/user risk config that fails to parse must announce itself here, not
|
|
1558
|
+
// silently revert to defaults (round-three review finding #1).
|
|
1559
|
+
for (const w of loadRiskConfig(scanRoot).warnings)
|
|
1560
|
+
warnings.push(w);
|
|
1556
1561
|
const staticSnapshot = writeStartStaticSnapshot(root, opts.baseRef, warnings);
|
|
1557
1562
|
const aiProviderConfigured = deps.aiProvider !== undefined || resolveProviderConfig(providerEnv, providerOpts) !== null;
|
|
1558
1563
|
const resolvedGenerationProvider = deps.aiProvider ?? resolveGenerationProvider(providerEnv, providerOpts);
|
package/dist/local/score/risk.js
CHANGED
|
@@ -337,7 +337,7 @@ function computeRawORS(node, depthCtx, incomingRefs, gitChurn, fanOut, detection
|
|
|
337
337
|
const routeWeight = deriveRouteWeight(node);
|
|
338
338
|
const flowDepth = getFlowDepth(node, depthCtx);
|
|
339
339
|
const flowPosition = Math.max(0, 5 - flowDepth);
|
|
340
|
-
const dataSensitivity = signals.sensitivityIgnored ? 0 : deriveDataSensitivity(node);
|
|
340
|
+
const dataSensitivity = signals.sensitivityOverride !== undefined ? signals.sensitivityOverride : signals.sensitivityIgnored ? 0 : deriveDataSensitivity(node);
|
|
341
341
|
// Irreversibility is impact: a bug on a path that reaches a delete/purge cannot be
|
|
342
342
|
// rolled back. Bounded, additive, graph-derived (Fix C, signal 1).
|
|
343
343
|
const rawI = incomingRefs * 0.3 + routeWeight * 0.3 + flowPosition * 0.2 + dataSensitivity * 0.2;
|
|
@@ -345,7 +345,7 @@ function computeRawORS(node, depthCtx, incomingRefs, gitChurn, fanOut, detection
|
|
|
345
345
|
// queue fails where no request surfaces it (Fix C, signal 2). Proven stays proven.
|
|
346
346
|
const silentFactor = signals.scheduledEntry && detectionTier !== "associated" ? 1.25 : 1;
|
|
347
347
|
const d = DETECTION_MAP[detectionTier] * silentFactor;
|
|
348
|
-
return { p: rawP, i: rawI, d, reachesDestructiveSink: signals.reachesDestructiveSink, scheduledEntry: signals.scheduledEntry, sensitivityIgnored: signals.sensitivityIgnored };
|
|
348
|
+
return { p: rawP, i: rawI, d, reachesDestructiveSink: signals.reachesDestructiveSink, scheduledEntry: signals.scheduledEntry, sensitivityIgnored: signals.sensitivityIgnored, sensitivityOverride: signals.sensitivityOverride };
|
|
349
349
|
}
|
|
350
350
|
function staticTestLinkedIds(graph, candidateIds) {
|
|
351
351
|
const ids = new Set();
|
|
@@ -565,7 +565,10 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
565
565
|
return computeRawORS(s, depthCtx, incoming_refs, git_churn, fan_out, detectionFor(s.external_id), ts, nowSec, {
|
|
566
566
|
reachesDestructiveSink: cfg.tuning.irreversibility_floor && (reachesSink.get(s.external_id) ?? false),
|
|
567
567
|
scheduledEntry: cfg.tuning.silence_multiplier && (isScheduledEntry(s) || (SCHEDULED_ENTRY_NAME_RE.test(s.title || "") && extraScheduled.some((re) => re.test(symbolFile(s))))),
|
|
568
|
-
sensitivityIgnored: sensitivityIgnore.some((re) => re.test(s.title || "") || re.test(s.external_id)) || overrideFor(s.external_id)?.action === "reclassify" && overrideFor(s.external_id)?.sensitivity === "none"
|
|
568
|
+
sensitivityIgnored: sensitivityIgnore.some((re) => re.test(s.title || "") || re.test(s.external_id)) || overrideFor(s.external_id)?.action === "reclassify" && overrideFor(s.external_id)?.sensitivity === "none",
|
|
569
|
+
// reclassify to a named class uses the SAME values deriveDataSensitivity assigns (payment 10, auth 9, pii 8)
|
|
570
|
+
sensitivityOverride: (() => { const o = overrideFor(s.external_id); if (!o || o.action !== "reclassify")
|
|
571
|
+
return undefined; return o.sensitivity === "payment" ? 10 : o.sensitivity === "auth" ? 9 : o.sensitivity === "pii" ? 8 : o.sensitivity === "none" ? 0 : undefined; })()
|
|
569
572
|
});
|
|
570
573
|
});
|
|
571
574
|
const pinnedIds = new Set(symbols.filter((s) => overrideFor(s.external_id)?.action === "pin").map((s) => s.external_id));
|
|
@@ -583,7 +586,7 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
583
586
|
const fan_out = fanOut.get(s.external_id) ?? 0;
|
|
584
587
|
const isEntry = entryPoint.get(s.external_id) ?? false;
|
|
585
588
|
const route_weight = deriveRouteWeight(s);
|
|
586
|
-
const data_sensitivity = rawScores[idx].sensitivityIgnored ? 0 : deriveDataSensitivity(s);
|
|
589
|
+
const data_sensitivity = rawScores[idx].sensitivityOverride !== undefined ? rawScores[idx].sensitivityOverride : rawScores[idx].sensitivityIgnored ? 0 : deriveDataSensitivity(s);
|
|
587
590
|
const flow_position = Math.max(0, 5 - getFlowDepth(s, depthCtx));
|
|
588
591
|
const complexity_proxy = complexityProxy(s);
|
|
589
592
|
const firstTs = firstCommitTs.get(file) ?? 0;
|
|
@@ -719,3 +722,39 @@ export function rankRiskGaps(graph, opts = {}) {
|
|
|
719
722
|
export function rankPriorityGaps(graph, opts = {}) {
|
|
720
723
|
return rankRiskGaps(graph, { ...opts, maxPerFile: 3, maxPerTitle: 1 });
|
|
721
724
|
}
|
|
725
|
+
/** What the per-repo config DID to this ranking — for visible disclosure in the report.
|
|
726
|
+
* A tuned report must never pass as clean: every suppression is listed with its reason. */
|
|
727
|
+
export function configDisclosureFor(graph, repoRoot) {
|
|
728
|
+
const loaded = loadRiskConfig(repoRoot);
|
|
729
|
+
const cfg = loaded.config;
|
|
730
|
+
const symbols = graph.nodes.filter((n) => n.kind === "CodeSymbol" && n.denominator_eligible === true && !n.stale);
|
|
731
|
+
const suppressed = [];
|
|
732
|
+
for (const o of cfg.overrides) {
|
|
733
|
+
if (o.action !== "suppress")
|
|
734
|
+
continue;
|
|
735
|
+
const re = globToRegExp(o.symbol);
|
|
736
|
+
for (const n of symbols)
|
|
737
|
+
if (n.external_id === o.symbol || re.test(n.external_id))
|
|
738
|
+
suppressed.push({ symbol: n.title || n.external_id, reason: o.reason });
|
|
739
|
+
}
|
|
740
|
+
const ignoreRes = cfg.classification.sensitivity_ignore.map(globToRegExp);
|
|
741
|
+
const sensitivityIgnored = ignoreRes.length
|
|
742
|
+
? symbols.filter((n) => ignoreRes.some((re) => re.test(n.title || "") || re.test(n.external_id))).map((n) => n.title || n.external_id).slice(0, 50)
|
|
743
|
+
: [];
|
|
744
|
+
return {
|
|
745
|
+
hash: loaded.hash,
|
|
746
|
+
warnings: loaded.warnings,
|
|
747
|
+
overridesActive: cfg.overrides.length,
|
|
748
|
+
suppressed,
|
|
749
|
+
rankExcludePaths: cfg.classification.rank_exclude_paths,
|
|
750
|
+
floor: cfg.tuning.irreversibility_floor,
|
|
751
|
+
silence: cfg.tuning.silence_multiplier,
|
|
752
|
+
classification: {
|
|
753
|
+
test_support_paths: cfg.classification.test_support_paths,
|
|
754
|
+
scheduled_entry_paths: cfg.classification.scheduled_entry_paths,
|
|
755
|
+
destructive_sinks: cfg.classification.destructive_sinks,
|
|
756
|
+
sensitivity_ignore: cfg.classification.sensitivity_ignore
|
|
757
|
+
},
|
|
758
|
+
sensitivityIgnored
|
|
759
|
+
};
|
|
760
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadRiskConfig } from "../score/riskConfig.js";
|
|
2
|
+
import { configDisclosureFor } from "../score/risk.js";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { buildRtm } from "../rtm.js";
|
|
@@ -936,7 +937,9 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
936
937
|
.sort((a, b) => (b.probability ?? 0) - (a.probability ?? 0) || b.risk_score - a.risk_score || a.id.localeCompare(b.id))
|
|
937
938
|
.slice(0, 20)
|
|
938
939
|
.map((r) => ({ path: r.title, file: r.file, score: r.risk_score, probability: r.probability ?? 0 }));
|
|
939
|
-
|
|
940
|
+
// Fix: search the FULL ranking for sinks, not the top 200 — a stable destructive path
|
|
941
|
+
// ranked 300th is exactly what this panel exists to recover.
|
|
942
|
+
const irreversible = rankRiskGaps(graph, { repoRoot, limit: Number.MAX_SAFE_INTEGER, provenIds }).filter((r) => r.sink_callee).slice(0, 20)
|
|
940
943
|
.map((r) => ({ path: r.title, file: r.file, score: r.risk_score, sink: r.sink_callee ?? "" }));
|
|
941
944
|
const riskHealth = inspectRiskInputHealth(repoRoot);
|
|
942
945
|
const churnAvailable = riskHealth.churnAvailable && riskGaps.every((risk) => risk.churn_available !== false);
|
|
@@ -950,7 +953,7 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
950
953
|
toolVersion: ORANGEPRO_VERSION,
|
|
951
954
|
configHash: loadRiskConfig(repoRoot).hash,
|
|
952
955
|
inputFingerprint: createHash("sha256")
|
|
953
|
-
.update(JSON.stringify({ root: graph.workspace.root_hash, commit: riskHealth.commit, history: riskHealth.history, churn: churnAvailable, window: riskHealth.churnWindow, version: ORANGEPRO_VERSION }))
|
|
956
|
+
.update(JSON.stringify({ root: graph.workspace.root_hash, commit: riskHealth.commit, history: riskHealth.history, churn: churnAvailable, window: riskHealth.churnWindow, version: ORANGEPRO_VERSION, config: loadRiskConfig(repoRoot).hash }))
|
|
954
957
|
.digest("hex")
|
|
955
958
|
.slice(0, 16),
|
|
956
959
|
reason: churnAvailable ? undefined : (riskHealth.reason ?? "Git churn scan did not complete")
|
|
@@ -976,6 +979,7 @@ export function buildBehaviorReportData(graph, ledger, opts = {}) {
|
|
|
976
979
|
candidateFlows: candidateFlows(graph),
|
|
977
980
|
risks,
|
|
978
981
|
worklists: { changeFrontier, irreversible },
|
|
982
|
+
configDisclosure: configDisclosureFor(graph, repoRoot),
|
|
979
983
|
zeroProofExplainer: summary.proven === 0 ? { title: ZERO_PROOF_EXPLAINER.title, body: [...ZERO_PROOF_EXPLAINER.body] } : null,
|
|
980
984
|
mapModel: buildSystemMapModel({ flows: flowRows, risks, behaviors: sortedBehaviors }),
|
|
981
985
|
viewMeta: {
|
|
@@ -181,6 +181,11 @@ nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
|
|
|
181
181
|
.wl-path{color:var(--ink2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
182
182
|
.wl-meta{color:var(--faint);flex-shrink:0;max-width:46%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
183
183
|
.wl-empty{font-size:11.5px;color:var(--faint);padding:6px 0}
|
|
184
|
+
.cfg{margin:0 0 12px;padding:8px 12px;border:1px solid var(--bd);border-radius:7px;background:var(--s1);font:11px/1.55 var(--mono);color:var(--muted)}
|
|
185
|
+
.cfg b{color:var(--ink2);font-weight:600}
|
|
186
|
+
.cfg.cfg-warn{border-color:var(--rbd);background:var(--rbg);color:var(--red)}
|
|
187
|
+
.cfg.cfg-tuned{border-color:var(--obd);background:var(--obg);color:var(--ink2)}
|
|
188
|
+
.cfg-row{display:block;margin-top:3px}
|
|
184
189
|
@media(max-width:800px){.wl-grid{grid-template-columns:1fr}}
|
|
185
190
|
.risk-filter{appearance:none;border:1px solid var(--bd);background:var(--s1);color:var(--muted);border-radius:20px;padding:5px 10px;font:inherit;font-size:12px;font-weight:600;cursor:pointer}
|
|
186
191
|
.risk-filter:hover{border-color:var(--bd2);color:var(--ink2)}
|
|
@@ -436,6 +441,7 @@ body[data-mode="expert"] .simple-only{display:none!important}
|
|
|
436
441
|
<p class="bridge expert-only">This is the <b>priority-gap worklist</b>, ranked by blast radius and test weakness. It is separate from the coverage-status cards above: <b>Reachable · no test signal</b> is one strict coverage bucket, not the number of priority gaps.</p>
|
|
437
442
|
<p class="bridge" id="risk-cap-note" style="font-size:12px;opacity:.75"></p>
|
|
438
443
|
<div class="risk-tools" id="risk-tools"></div>
|
|
444
|
+
<div id="config-disclosure"></div>
|
|
439
445
|
<div id="worklists"></div>
|
|
440
446
|
<div id="risk-list"></div>
|
|
441
447
|
</section>
|
|
@@ -1022,6 +1028,27 @@ riskTools.addEventListener("click",e=>{
|
|
|
1022
1028
|
});
|
|
1023
1029
|
renderRiskFilters();
|
|
1024
1030
|
renderRisks();
|
|
1031
|
+
// ── Config disclosure: what the per-repo config did. Always rendered — a tuned
|
|
1032
|
+
// report must never pass as clean, and a broken config must announce itself.
|
|
1033
|
+
(function(){
|
|
1034
|
+
const C=D.configDisclosure, host=$("#config-disclosure");
|
|
1035
|
+
if(!C||!host)return;
|
|
1036
|
+
const warn=(C.warnings||[]).length>0;
|
|
1037
|
+
const K=C.classification||{};
|
|
1038
|
+
const clsSet=["test_support_paths","scheduled_entry_paths","destructive_sinks","sensitivity_ignore"].filter(k=>(K[k]||[]).length>0);
|
|
1039
|
+
const tuned=C.overridesActive>0||(C.rankExcludePaths||[]).length>0||!C.floor||!C.silence||clsSet.length>0;
|
|
1040
|
+
const bits=["config <b>"+esc(C.hash)+"</b>","overrides active <b>"+C.overridesActive+"</b>","suppressed <b>"+(C.suppressed||[]).length+"</b>"];
|
|
1041
|
+
if((C.rankExcludePaths||[]).length)bits.push("ranking excludes <b>"+esc(C.rankExcludePaths.join(", "))+"</b>");
|
|
1042
|
+
if(!C.floor)bits.push("<b>irreversibility floor OFF</b>");
|
|
1043
|
+
if(!C.silence)bits.push("<b>silence multiplier OFF</b>");
|
|
1044
|
+
let html="<div>"+bits.join(" · ")+"</div>";
|
|
1045
|
+
clsSet.forEach(k=>{html+='<span class="cfg-row">'+esc(k)+': <b>'+esc(K[k].join(", "))+'</b></span>';});
|
|
1046
|
+
if((C.sensitivityIgnored||[]).length)html+='<span class="cfg-row">sensitivity ignored by config (score changed): <b>'+esc(C.sensitivityIgnored.join(", "))+'</b></span>';
|
|
1047
|
+
(C.suppressed||[]).forEach(s=>{html+='<span class="cfg-row">suppressed by config: <b>'+esc(s.symbol)+'</b> — '+esc(s.reason)+'</span>';});
|
|
1048
|
+
(C.warnings||[]).forEach(w=>{html+='<span class="cfg-row">⚠ '+esc(w)+'</span>';});
|
|
1049
|
+
host.className="cfg"+(warn?" cfg-warn":tuned?" cfg-tuned":"");
|
|
1050
|
+
host.innerHTML=html;
|
|
1051
|
+
})();
|
|
1025
1052
|
// ── Worklists: two views of ONE ranking. A single P×I×D top-20 cannot hold
|
|
1026
1053
|
// "changing fast" and "irreversible but stable" together; both are shown here.
|
|
1027
1054
|
(function(){
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangepro/orangepro-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.39",
|
|
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",
|