@orangepro/orangepro-mcp 0.2.36 → 0.2.38
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(root).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,28 @@ 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
|
+
return {
|
|
741
|
+
hash: loaded.hash,
|
|
742
|
+
warnings: loaded.warnings,
|
|
743
|
+
overridesActive: cfg.overrides.length,
|
|
744
|
+
suppressed,
|
|
745
|
+
rankExcludePaths: cfg.classification.rank_exclude_paths,
|
|
746
|
+
floor: cfg.tuning.irreversibility_floor,
|
|
747
|
+
silence: cfg.tuning.silence_multiplier
|
|
748
|
+
};
|
|
749
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { homedir } from "node:os";
|
|
11
12
|
export const DEFAULT_RISK_CONFIG = {
|
|
12
13
|
classification: { test_support_paths: [], scheduled_entry_paths: [], destructive_sinks: [], sensitivity_ignore: [], rank_exclude_paths: [] },
|
|
13
14
|
tuning: { irreversibility_floor: true, silence_multiplier: true },
|
|
@@ -21,41 +22,50 @@ export function globToRegExp(glob) {
|
|
|
21
22
|
const esc = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\u0000").replace(/\*/g, "[^/]*").replace(/\u0000/g, ".*");
|
|
22
23
|
return new RegExp(`^${esc}$`);
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
cfg.tuning.irreversibility_floor = tun.irreversibility_floor;
|
|
40
|
-
if (typeof tun.silence_multiplier === "boolean")
|
|
41
|
-
cfg.tuning.silence_multiplier = tun.silence_multiplier;
|
|
42
|
-
for (const o of Array.isArray(raw.overrides) ? raw.overrides : []) {
|
|
43
|
-
const ov = o;
|
|
44
|
-
if (typeof ov.symbol !== "string" || !["suppress", "pin", "reclassify"].includes(ov.action ?? "")) {
|
|
45
|
-
warnings.push(`config: override ignored (needs symbol + action): ${JSON.stringify(o).slice(0, 80)}`);
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (typeof ov.reason !== "string" || ov.reason.trim().length < 8) {
|
|
49
|
-
warnings.push(`config: override for ${ov.symbol} ignored — a reason (≥8 chars) is required so it can be shown on the report.`);
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
cfg.overrides.push({ symbol: ov.symbol, action: ov.action, sensitivity: ov.sensitivity, reason: ov.reason.trim() });
|
|
53
|
-
}
|
|
25
|
+
/** User-level defaults: ~/.orangepro/config.json (override with ORANGEPRO_USER_CONFIG for tests/CI).
|
|
26
|
+
* Applied FIRST; the analyzed repo's .orangepro/config.json wins on every key it sets.
|
|
27
|
+
* The hash covers the merged result, so provenance still tells the truth. */
|
|
28
|
+
export function userConfigPath() {
|
|
29
|
+
return process.env.ORANGEPRO_USER_CONFIG ?? join(homedir(), ".orangepro", "config.json");
|
|
30
|
+
}
|
|
31
|
+
function applyFile(cfg, file, warnings, label) {
|
|
32
|
+
if (!existsSync(file))
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
36
|
+
const cls = (raw.classification ?? {});
|
|
37
|
+
for (const k of ["test_support_paths", "scheduled_entry_paths", "destructive_sinks", "sensitivity_ignore", "rank_exclude_paths"]) {
|
|
38
|
+
if (Array.isArray(cls[k]))
|
|
39
|
+
cfg.classification[k] = asStringArray(cls[k]);
|
|
54
40
|
}
|
|
55
|
-
|
|
56
|
-
|
|
41
|
+
const tun = (raw.tuning ?? {});
|
|
42
|
+
if (typeof tun.irreversibility_floor === "boolean")
|
|
43
|
+
cfg.tuning.irreversibility_floor = tun.irreversibility_floor;
|
|
44
|
+
if (typeof tun.silence_multiplier === "boolean")
|
|
45
|
+
cfg.tuning.silence_multiplier = tun.silence_multiplier;
|
|
46
|
+
for (const o of Array.isArray(raw.overrides) ? raw.overrides : []) {
|
|
47
|
+
const ov = o;
|
|
48
|
+
if (typeof ov.symbol !== "string" || !["suppress", "pin", "reclassify"].includes(ov.action ?? "")) {
|
|
49
|
+
warnings.push(`config (${label}): override ignored (needs symbol + action): ${JSON.stringify(o).slice(0, 80)}`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (typeof ov.reason !== "string" || ov.reason.trim().length < 8) {
|
|
53
|
+
warnings.push(`config (${label}): override for ${ov.symbol} ignored — a reason (≥8 chars) is required so it can be shown on the report.`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
cfg.overrides.push({ symbol: ov.symbol, action: ov.action, sensitivity: ov.sensitivity, reason: ov.reason.trim() });
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
warnings.push(`config (${label}): unreadable for risk settings (${err.message}); skipped.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function loadRiskConfig(repoRoot) {
|
|
64
|
+
const warnings = [];
|
|
65
|
+
const cfg = JSON.parse(JSON.stringify(DEFAULT_RISK_CONFIG));
|
|
66
|
+
applyFile(cfg, userConfigPath(), warnings, "user defaults");
|
|
67
|
+
if (repoRoot)
|
|
68
|
+
applyFile(cfg, join(repoRoot, ".orangepro", "config.json"), warnings, "repo");
|
|
59
69
|
const canonical = JSON.stringify(cfg, Object.keys(cfg).sort());
|
|
60
70
|
const hash = createHash("sha256").update(JSON.stringify(cfg)).digest("hex").slice(0, 12);
|
|
61
71
|
void canonical;
|
|
@@ -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: {
|
|
@@ -169,6 +169,24 @@ nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
|
|
|
169
169
|
/* RISKS */
|
|
170
170
|
.risk-card{background:var(--s1);border:1px solid var(--bd);border-radius:9px;padding:14px 16px;margin-bottom:10px}
|
|
171
171
|
.risk-tools{display:flex;align-items:center;gap:8px;margin:0 0 12px;flex-wrap:wrap}
|
|
172
|
+
/* WORKLISTS — two views of one ranking */
|
|
173
|
+
.wl-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:0 0 14px}
|
|
174
|
+
.wl-card{background:var(--s1);border:1px solid var(--bd);border-radius:9px;padding:12px 14px}
|
|
175
|
+
.wl-title{font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;margin:0 0 2px}
|
|
176
|
+
.wl-title.wl-change{color:var(--amber)} .wl-title.wl-irrev{color:var(--red)}
|
|
177
|
+
.wl-sub{font-size:11px;color:var(--muted);margin:0 0 8px;line-height:1.45}
|
|
178
|
+
.wl-row{display:flex;justify-content:space-between;gap:8px;padding:4px 0;border-bottom:1px solid var(--bd);font-family:var(--mono);font-size:11px;cursor:pointer}
|
|
179
|
+
.wl-row:last-child{border-bottom:0}
|
|
180
|
+
.wl-row:hover .wl-path{color:var(--ink)}
|
|
181
|
+
.wl-path{color:var(--ink2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
182
|
+
.wl-meta{color:var(--faint);flex-shrink:0;max-width:46%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
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}
|
|
189
|
+
@media(max-width:800px){.wl-grid{grid-template-columns:1fr}}
|
|
172
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}
|
|
173
191
|
.risk-filter:hover{border-color:var(--bd2);color:var(--ink2)}
|
|
174
192
|
.risk-filter[aria-pressed="true"]{border-color:var(--orange);background:var(--obg);color:var(--orange)}
|
|
@@ -423,6 +441,8 @@ body[data-mode="expert"] .simple-only{display:none!important}
|
|
|
423
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>
|
|
424
442
|
<p class="bridge" id="risk-cap-note" style="font-size:12px;opacity:.75"></p>
|
|
425
443
|
<div class="risk-tools" id="risk-tools"></div>
|
|
444
|
+
<div id="config-disclosure"></div>
|
|
445
|
+
<div id="worklists"></div>
|
|
426
446
|
<div id="risk-list"></div>
|
|
427
447
|
</section>
|
|
428
448
|
|
|
@@ -1008,6 +1028,41 @@ riskTools.addEventListener("click",e=>{
|
|
|
1008
1028
|
});
|
|
1009
1029
|
renderRiskFilters();
|
|
1010
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 tuned=C.overridesActive>0||(C.rankExcludePaths||[]).length>0||!C.floor||!C.silence;
|
|
1038
|
+
const bits=["config <b>"+esc(C.hash)+"</b>","overrides active <b>"+C.overridesActive+"</b>","suppressed <b>"+(C.suppressed||[]).length+"</b>"];
|
|
1039
|
+
if((C.rankExcludePaths||[]).length)bits.push("ranking excludes <b>"+esc(C.rankExcludePaths.join(", "))+"</b>");
|
|
1040
|
+
if(!C.floor)bits.push("<b>irreversibility floor OFF</b>");
|
|
1041
|
+
if(!C.silence)bits.push("<b>silence multiplier OFF</b>");
|
|
1042
|
+
let html="<div>"+bits.join(" · ")+"</div>";
|
|
1043
|
+
(C.suppressed||[]).forEach(s=>{html+='<span class="cfg-row">suppressed by config: <b>'+esc(s.symbol)+'</b> — '+esc(s.reason)+'</span>';});
|
|
1044
|
+
(C.warnings||[]).forEach(w=>{html+='<span class="cfg-row">⚠ '+esc(w)+'</span>';});
|
|
1045
|
+
host.className="cfg"+(warn?" cfg-warn":tuned?" cfg-tuned":"");
|
|
1046
|
+
host.innerHTML=html;
|
|
1047
|
+
})();
|
|
1048
|
+
// ── Worklists: two views of ONE ranking. A single P×I×D top-20 cannot hold
|
|
1049
|
+
// "changing fast" and "irreversible but stable" together; both are shown here.
|
|
1050
|
+
(function(){
|
|
1051
|
+
const W=D.worklists, host=$("#worklists");
|
|
1052
|
+
if(!W||!host)return;
|
|
1053
|
+
const topPaths=new Set(D.risks.map(r=>r.path));
|
|
1054
|
+
const row=(path,meta)=>{
|
|
1055
|
+
const inTop=topPaths.has(path);
|
|
1056
|
+
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>\`;
|
|
1057
|
+
};
|
|
1058
|
+
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>';
|
|
1059
|
+
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>';
|
|
1060
|
+
host.innerHTML=\`<div class="wl-grid">
|
|
1061
|
+
<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>
|
|
1062
|
+
<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>
|
|
1063
|
+
</div>\`;
|
|
1064
|
+
host.addEventListener("click",e=>{const r=e.target.closest("[data-path]");if(r&&topPaths.has(r.getAttribute("data-path")))scrollToRisk(r.getAttribute("data-path"));});
|
|
1065
|
+
})();
|
|
1011
1066
|
// toggle test expand
|
|
1012
1067
|
document.addEventListener('click',e=>{
|
|
1013
1068
|
const h=e.target.closest('.gen-test-head');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangepro/orangepro-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.38",
|
|
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",
|