@orangepro/orangepro-mcp 0.2.36 → 0.2.37

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.
@@ -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
- export function loadRiskConfig(repoRoot) {
25
- const warnings = [];
26
- const cfg = JSON.parse(JSON.stringify(DEFAULT_RISK_CONFIG));
27
- const file = join(repoRoot, ".orangepro", "config.json");
28
- if (repoRoot && existsSync(file)) {
29
- try {
30
- const raw = JSON.parse(readFileSync(file, "utf8"));
31
- const cls = (raw.classification ?? {});
32
- cfg.classification.test_support_paths = asStringArray(cls.test_support_paths);
33
- cfg.classification.scheduled_entry_paths = asStringArray(cls.scheduled_entry_paths);
34
- cfg.classification.destructive_sinks = asStringArray(cls.destructive_sinks);
35
- cfg.classification.sensitivity_ignore = asStringArray(cls.sensitivity_ignore);
36
- cfg.classification.rank_exclude_paths = asStringArray(cls.rank_exclude_paths);
37
- const tun = (raw.tuning ?? {});
38
- if (typeof tun.irreversibility_floor === "boolean")
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
- catch (err) {
56
- warnings.push(`config: .orangepro/config.json unreadable for risk settings (${err.message}); defaults used.`);
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;
@@ -169,6 +169,19 @@ 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
+ @media(max-width:800px){.wl-grid{grid-template-columns:1fr}}
172
185
  .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
186
  .risk-filter:hover{border-color:var(--bd2);color:var(--ink2)}
174
187
  .risk-filter[aria-pressed="true"]{border-color:var(--orange);background:var(--obg);color:var(--orange)}
@@ -423,6 +436,7 @@ body[data-mode="expert"] .simple-only{display:none!important}
423
436
  <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
437
  <p class="bridge" id="risk-cap-note" style="font-size:12px;opacity:.75"></p>
425
438
  <div class="risk-tools" id="risk-tools"></div>
439
+ <div id="worklists"></div>
426
440
  <div id="risk-list"></div>
427
441
  </section>
428
442
 
@@ -1008,6 +1022,24 @@ riskTools.addEventListener("click",e=>{
1008
1022
  });
1009
1023
  renderRiskFilters();
1010
1024
  renderRisks();
1025
+ // ── Worklists: two views of ONE ranking. A single P×I×D top-20 cannot hold
1026
+ // "changing fast" and "irreversible but stable" together; both are shown here.
1027
+ (function(){
1028
+ const W=D.worklists, host=$("#worklists");
1029
+ if(!W||!host)return;
1030
+ const topPaths=new Set(D.risks.map(r=>r.path));
1031
+ const row=(path,meta)=>{
1032
+ const inTop=topPaths.has(path);
1033
+ return \`<div class="wl-row" data-path="\${esc(path).replace(/"/g,'&quot;')}" 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>\`;
1034
+ };
1035
+ 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>';
1036
+ 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>';
1037
+ host.innerHTML=\`<div class="wl-grid">
1038
+ <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>
1039
+ <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>
1040
+ </div>\`;
1041
+ host.addEventListener("click",e=>{const r=e.target.closest("[data-path]");if(r&&topPaths.has(r.getAttribute("data-path")))scrollToRisk(r.getAttribute("data-path"));});
1042
+ })();
1011
1043
  // toggle test expand
1012
1044
  document.addEventListener('click',e=>{
1013
1045
  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.36",
3
+ "version": "0.2.37",
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",