@mneme-ai/xray 2.183.0 → 2.184.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mneme-ai/xray",
3
- "version": "2.183.0",
3
+ "version": "2.184.0",
4
4
  "description": "Mneme Repo X-Ray — a signed, raw-free, deterministic X-Ray of any repo. Every number is reproducible from git/AST/metadata and sealed with an offline-verifiable NOTARY receipt. No source code ever leaves the machine; no LLM guesses anything.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/public/card.js CHANGED
@@ -110,41 +110,41 @@
110
110
  // of truth). Every node/edge is verbatim from the signed report; nothing invented.
111
111
  function mix(a, b, t) { const p = (h) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16)); const A = p(a), B = p(b); return "#" + A.map((v, i) => Math.round(v + (B[i] - v) * t).toString(16).padStart(2, "0")).join(""); }
112
112
  const riskColor = (t) => (t < 0.5 ? mix("#16a34a", "#d97706", t / 0.5) : mix("#d97706", "#e11d48", (t - 0.5) / 0.5));
113
+ // RISK MAP — a ranked KEY-PERSON-RISK bar chart (NOT a bubble cloud). Instantly
114
+ // readable: one row per file, bar length = single-author share (risk if they leave),
115
+ // colour by severity, worst on top, a "↔N" badge for how many files it's coupled to.
116
+ // Every value verbatim from the signed report — no AI guessed it, nothing overlaps.
113
117
  function riskMapHTML(r) {
114
118
  const num = (x) => (Number.isFinite(Number(x)) ? Number(x) : 0), strv = (x) => (typeof x === "string" ? x : "");
115
- const bf = r.busFactor || {}, hs = r.hotspots || {}, cx = r.complexity || {}, cp = r.coupling || {};
116
- const W = 960, H = 520, CAP = 22, PAD = 64, byFile = new Map();
117
- const nodeR = (size) => 17 + Math.min(1, Math.max(0, size)) * 30;
118
- const touch = (f) => { f = strv(f); if (!f) return null; if (!byFile.has(f)) byFile.set(f, { o: 0, c: 0, l: 0 }); return byFile.get(f); };
119
- (bf.fragileFiles || []).forEach((x) => { const n = touch(x && x.file); if (n) n.o = Math.max(n.o, Math.min(1, Math.max(0, num(x.topAuthorShare)))); });
120
- (hs.hotspots || []).forEach((x) => { const n = touch(x && x.file); if (n) { n.c = Math.max(n.c, num(x.changes)); n.l = Math.max(n.l, num(x.loc)); } });
121
- (cx.hotspots || []).forEach((x) => { const n = touch(x && x.file); if (n) n.l = Math.max(n.l, num(x.bodyLines)); });
122
- const pairs = cp.pairs || []; pairs.forEach((p) => { touch(p && p.a); touch(p && p.b); });
123
- if (!byFile.size) return "";
124
- const mc = Math.max(1, ...[...byFile.values()].map((v) => v.c)), ml = Math.max(1, ...[...byFile.values()].map((v) => v.l));
125
- let es = [...byFile.entries()].map(([file, v]) => ({ file, risk: Math.min(1, Math.max(0, v.o)), size: Math.min(1, Math.max(v.c / mc, v.l / ml)), ownerPct: v.o, churn: v.c }));
126
- es.sort((a, b) => (b.risk - a.risk) || (b.size - a.size) || a.file.localeCompare(b.file));
127
- es = es.slice(0, CAP);
128
- const idx = new Map(es.map((e, i) => [e.file, i]));
129
- const cxC = W / 2, cyC = H / 2, GOLD = 2.399963229728653, RX = W / 2 - PAD, RY = H / 2 - PAD, N = es.length;
130
- const nodes = es.map((e, i) => { const frac = N <= 1 ? 0 : Math.sqrt((i + 0.5) / N), ang = i * GOLD; return { ...e, i, r: nodeR(e.size), x: Math.min(W - PAD, Math.max(PAD, cxC + RX * frac * Math.cos(ang))), y: Math.min(H - PAD, Math.max(PAD, cyC + RY * frac * Math.sin(ang))) }; });
131
- const base = (f) => { const p = String(f).split("/"); return p[p.length - 1]; };
132
- const edges = [];
133
- pairs.forEach((p) => { const a = idx.get(strv(p && p.a)), b = idx.get(strv(p && p.b)); if (a === undefined || b === undefined || a === b) return; edges.push({ a, b, w: Math.min(1, Math.max(0, num(p.confidence))), hidden: !!(p && p.hidden) }); });
134
- const edgeSvg = edges.map((e) => { const A = nodes[e.a], B = nodes[e.b], mx = (A.x + B.x) / 2, my = (A.y + B.y) / 2 - 30; return `<path d="M${A.x.toFixed(1)} ${A.y.toFixed(1)} Q${mx.toFixed(1)} ${my.toFixed(1)} ${B.x.toFixed(1)} ${B.y.toFixed(1)}" fill="none" stroke="${e.hidden ? "#e11d48" : "#7c83f6"}" stroke-width="${(1 + e.w * 2.6).toFixed(2)}" stroke-opacity="${(0.22 + e.w * 0.5).toFixed(2)}" stroke-linecap="round"${e.hidden ? ' stroke-dasharray="6 5"' : ""}/>`; }).join("");
135
- // glossy spheres: soft glow + body + top-left highlight + white rim
136
- const nodeSvg = nodes.map((n) => { const R = n.r, col = riskColor(n.risk), showLabel = n.i < 9 || n.risk >= 0.65; const lbl = esc(base(n.file)).slice(0, 24) + (n.ownerPct >= 0.5 ? ` · ${Math.round(n.ownerPct * 100)}%` : ""); const lw = lbl.length * 6.4 + 14, ly = n.y + R + 7;
137
- return `<g><circle cx="${n.x}" cy="${n.y}" r="${(R + 11).toFixed(1)}" fill="${col}" opacity="0.12"/><circle cx="${n.x}" cy="${n.y}" r="${R.toFixed(1)}" fill="${col}" opacity="0.95"/><circle cx="${(n.x - R * 0.3).toFixed(1)}" cy="${(n.y - R * 0.32).toFixed(1)}" r="${(R * 0.52).toFixed(1)}" fill="#ffffff" opacity="0.22"/><circle cx="${n.x}" cy="${n.y}" r="${R.toFixed(1)}" fill="none" stroke="#ffffff" stroke-width="1.6" opacity="0.7"/>${showLabel ? `<g><rect x="${(n.x - lw / 2).toFixed(1)}" y="${ly.toFixed(1)}" width="${lw.toFixed(1)}" height="19" rx="9.5" fill="#ffffff" opacity="0.92" stroke="#ececef"/><text x="${n.x}" y="${(ly + 13).toFixed(1)}" text-anchor="middle" font-size="11.5" fill="#33333b" font-family="ui-monospace,Menlo,monospace">${lbl}</text></g>` : ""}</g>`; }).join("");
138
- const owned = nodes.filter((n) => n.risk >= 0.6).length;
139
- // TOP key-person risks as WORDS (the part a CEO/dev reads) — concrete + actionable
140
- const topRisk = nodes.filter((n) => n.risk >= 0.5).slice(0, 6);
141
- const riskList = topRisk.length ? `<div class="rmlist"><div class="rmlt">⚠️ Top key-person risks — fix these first</div>${topRisk.map((n) => `<div class="rmli"><span class="rmdot" style="background:${riskColor(n.risk)}"></span><b>${esc(base(n.file))}</b><span class="rmwhy">one author owns <b>${Math.round(n.ownerPct * 100)}%</b> of its history — add a reviewer / write docs before they leave</span></div>`).join("")}</div>` : `<div class="rmlist rmok">✓ No single-owner files — knowledge is well spread across the team.</div>`;
119
+ const bf = r.busFactor || {}, hs = r.hotspots || {}, cp = r.coupling || {};
120
+ const base = (f) => { const p = String(f).split("/"); return p[p.length - 1] || f; };
121
+ // coupling degree per file (how many distinct files it changes with)
122
+ const deg = new Map();
123
+ (cp.pairs || []).forEach((p) => { const a = strv(p && p.a), b = strv(p && p.b); if (!a || !b || a === b) return; if (!deg.has(a)) deg.set(a, new Set()); if (!deg.has(b)) deg.set(b, new Set()); deg.get(a).add(b); deg.get(b).add(a); });
124
+ const churn = new Map(); (hs.hotspots || []).forEach((x) => { const f = strv(x && x.file); if (f) churn.set(f, Math.max(churn.get(f) || 0, num(x.changes))); });
125
+ // the key-person list = files with a measured single-author share, worst-first
126
+ let files = (bf.fragileFiles || []).map((x) => ({ file: strv(x && x.file), pct: Math.min(1, Math.max(0, num(x.topAuthorShare))), commits: num(x && x.commits) })).filter((x) => x.file);
127
+ files.sort((a, b) => (b.pct - a.pct) || (b.commits - a.commits) || a.file.localeCompare(b.file));
128
+ files = files.slice(0, 12);
129
+ if (!files.length) return `<div class="riskmap"><div class="rmhead">🔑 Key-person risk</div><div class="rmsub">✓ No single-owner files knowledge is well spread across the team. Nobody is a single point of failure.</div></div>`;
130
+ const sevColor = (p) => (p >= 0.9 ? "#e11d48" : p >= 0.75 ? "#f97316" : p >= 0.6 ? "#eab308" : "#22c55e");
131
+ const sevWord = (p) => (p >= 0.9 ? "critical" : p >= 0.75 ? "high" : p >= 0.6 ? "watch" : "ok");
132
+ const critical = files.filter((f) => f.pct >= 0.9).length;
133
+ const rows = files.map((f) => {
134
+ const c = deg.has(f.file) ? deg.get(f.file).size : 0;
135
+ const col = sevColor(f.pct), pctN = Math.round(f.pct * 100);
136
+ return `<div class="rmbar" title="${esc(f.file)} — ${pctN}% by one author${c ? `, coupled to ${c} file(s)` : ""}">
137
+ <span class="rmbf">${esc(base(f.file))}</span>
138
+ <span class="rmtrack"><span class="rmfill" style="width:${pctN}%;background:${col}"></span></span>
139
+ <span class="rmval" style="color:${col}">${pctN}%</span>
140
+ <span class="rmcouple">${c ? `↔${c}` : ""}</span>
141
+ </div>`;
142
+ }).join("");
142
143
  return `<div class="riskmap">
143
- <div class="rmhead">🗺 Risk map — <b>who holds the keys, and what breaks if they leave</b></div>
144
- <div class="rmsub"><b>How to read:</b> each circle is a file · <b>red</b> = only one person knows it (key-person risk) · <b>bigger</b> = changes more often · <b>lines</b> = files that always change together. Click the map to enlarge. Every value is verbatim from the signed report no AI guessed it.</div>
145
- <div class="rmsvgwrap rmzoom" title="click to enlarge"><svg class="rmsvg" viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="repository risk map">${edgeSvg}${nodeSvg}</svg><span class="rmexpand">⤢ enlarge</span></div>
146
- <div class="rmleg"><span><i style="background:#16a34a"></i>shared / safe</span><span><i style="background:#d97706"></i>concentrated</span><span><i style="background:#e11d48"></i>single-owner</span><span><i class="dash"></i>hidden cross-dir coupling</span><span class="rmsummary">${nodes.length} files · ${edges.length} links${owned ? ` · <b style="color:#be123c">${owned} single-owner</b>` : ""}</span></div>
147
- ${riskList}
144
+ <div class="rmhead">🔑 Key-person risk — <b>if one person is away, what's exposed</b></div>
145
+ <div class="rmsub">Each bar is a file. <b>Longer &amp; redder = more of it was written by a single person</b> so it's riskier if they leave. <b>↔N</b> = it changes together with N other files. Worst on top. Measured from git historynothing invented.</div>
146
+ <div class="rmbars">${rows}</div>
147
+ <div class="rmleg"><span><i style="background:#e11d48"></i>critical ≥90%</span><span><i style="background:#f97316"></i>high ≥75%</span><span><i style="background:#eab308"></i>watch ≥60%</span><span class="rmsummary">${files.length} owned file(s)${critical ? ` · <b style="color:#be123c">${critical} critical</b>` : ""}</span></div>
148
148
  </div>`;
149
149
  }
150
150
 
package/public/index.html CHANGED
@@ -93,21 +93,18 @@
93
93
  .riskmap{padding:18px 30px;border-bottom:1px solid #eef0f2;background:linear-gradient(180deg,#fcfcfd,#fff)}
94
94
  .riskmap .rmhead{font-size:14.5px;color:#0b0b0f}
95
95
  .riskmap .rmsub{font-size:11.5px;color:#8b8f98;margin:4px 0 10px;line-height:1.5}
96
- .riskmap .rmsvg{width:100%;height:auto;display:block;border:1px solid #f0f1f3;border-radius:12px;background:radial-gradient(120% 120% at 50% 30%,#fbfbfe,#fff)}
97
- .riskmap .rmleg{display:flex;flex-wrap:wrap;gap:14px;margin-top:10px;font-size:11.5px;color:#5b6068}
96
+ .riskmap .rmbars{display:flex;flex-direction:column;gap:7px;margin:2px 0 4px}
97
+ .riskmap .rmbar{display:flex;align-items:center;gap:10px;font-size:12.5px}
98
+ .riskmap .rmbf{flex:0 0 38%;max-width:38%;font-family:ui-monospace,Menlo,monospace;color:#0b0b0f;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
99
+ .riskmap .rmtrack{flex:1;height:14px;background:#f1f2f4;border-radius:8px;overflow:hidden}
100
+ .riskmap .rmfill{display:block;height:100%;border-radius:8px;transition:width .6s cubic-bezier(.2,.8,.2,1)}
101
+ .riskmap .rmval{flex:0 0 40px;text-align:right;font-weight:680;font-variant-numeric:tabular-nums}
102
+ .riskmap .rmcouple{flex:0 0 30px;font-size:11px;color:#7c83f6;font-weight:600}
103
+ @media (max-width:560px){.riskmap .rmbf{flex-basis:46%;max-width:46%}}
104
+ .riskmap .rmleg{display:flex;flex-wrap:wrap;gap:14px;margin-top:12px;font-size:11.5px;color:#5b6068}
98
105
  .riskmap .rmleg span{display:inline-flex;align-items:center;gap:6px}
99
106
  .riskmap .rmleg i{width:11px;height:11px;border-radius:50%;display:inline-block}
100
- .riskmap .rmleg i.dash{width:18px;height:0;border-radius:0;border-top:2px dashed #e11d48}
101
107
  .riskmap .rmsummary{margin-left:auto;color:#8b8f98}
102
- .riskmap .rmsvgwrap{position:relative;cursor:zoom-in}
103
- .riskmap .rmexpand{position:absolute;top:10px;right:12px;font-size:11px;color:#5b6068;background:#fff;border:1px solid #ececef;border-radius:8px;padding:3px 9px;opacity:.85}
104
- .riskmap .rmlist{margin-top:14px;border-top:1px solid #eef0f2;padding-top:12px}
105
- .riskmap .rmlt{font-size:12.5px;font-weight:680;color:#be123c;margin-bottom:8px}
106
- .riskmap .rmok{color:#15803d;font-weight:560;font-size:13px;border:0;padding-top:0}
107
- .riskmap .rmli{display:flex;align-items:baseline;gap:9px;padding:6px 0;font-size:13px;flex-wrap:wrap}
108
- .riskmap .rmdot{width:10px;height:10px;border-radius:50%;flex-shrink:0;align-self:center}
109
- .riskmap .rmli b{color:#0b0b0f;font-family:ui-monospace,Menlo,monospace}
110
- .riskmap .rmwhy{color:#5b6068}
111
108
  .rmmodal{display:none;position:fixed;inset:0;z-index:9999;background:rgba(11,11,20,.62);align-items:center;justify-content:center;padding:16px}
112
109
  .rmmodal-box{background:#fff;border-radius:16px;max-width:1100px;width:100%;max-height:92vh;overflow:auto;box-shadow:0 24px 80px rgba(0,0,0,.4)}
113
110
  .rmmodal-bar{display:flex;align-items:center;gap:10px;padding:14px 18px;border-bottom:1px solid #eef0f2;font-size:13px;color:#5b6068;position:sticky;top:0;background:#fff}
@@ -46,21 +46,18 @@
46
46
  .riskmap{padding:18px 30px;border-bottom:1px solid #eef0f2;background:linear-gradient(180deg,#fcfcfd,#fff)}
47
47
  .riskmap .rmhead{font-size:14.5px;color:#0b0b0f}
48
48
  .riskmap .rmsub{font-size:11.5px;color:#8b8f98;margin:4px 0 10px;line-height:1.5}
49
- .riskmap .rmsvg{width:100%;height:auto;display:block;border:1px solid #f0f1f3;border-radius:12px;background:radial-gradient(120% 120% at 50% 30%,#fbfbfe,#fff)}
50
- .riskmap .rmleg{display:flex;flex-wrap:wrap;gap:14px;margin-top:10px;font-size:11.5px;color:#5b6068}
49
+ .riskmap .rmbars{display:flex;flex-direction:column;gap:7px;margin:2px 0 4px}
50
+ .riskmap .rmbar{display:flex;align-items:center;gap:10px;font-size:12.5px}
51
+ .riskmap .rmbf{flex:0 0 38%;max-width:38%;font-family:ui-monospace,Menlo,monospace;color:#0b0b0f;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
52
+ .riskmap .rmtrack{flex:1;height:14px;background:#f1f2f4;border-radius:8px;overflow:hidden}
53
+ .riskmap .rmfill{display:block;height:100%;border-radius:8px;transition:width .6s cubic-bezier(.2,.8,.2,1)}
54
+ .riskmap .rmval{flex:0 0 40px;text-align:right;font-weight:680;font-variant-numeric:tabular-nums}
55
+ .riskmap .rmcouple{flex:0 0 30px;font-size:11px;color:#7c83f6;font-weight:600}
56
+ @media (max-width:560px){.riskmap .rmbf{flex-basis:46%;max-width:46%}}
57
+ .riskmap .rmleg{display:flex;flex-wrap:wrap;gap:14px;margin-top:12px;font-size:11.5px;color:#5b6068}
51
58
  .riskmap .rmleg span{display:inline-flex;align-items:center;gap:6px}
52
59
  .riskmap .rmleg i{width:11px;height:11px;border-radius:50%;display:inline-block}
53
- .riskmap .rmleg i.dash{width:18px;height:0;border-radius:0;border-top:2px dashed #e11d48}
54
60
  .riskmap .rmsummary{margin-left:auto;color:#8b8f98}
55
- .riskmap .rmsvgwrap{position:relative;cursor:zoom-in}
56
- .riskmap .rmexpand{position:absolute;top:10px;right:12px;font-size:11px;color:#5b6068;background:#fff;border:1px solid #ececef;border-radius:8px;padding:3px 9px;opacity:.85}
57
- .riskmap .rmlist{margin-top:14px;border-top:1px solid #eef0f2;padding-top:12px}
58
- .riskmap .rmlt{font-size:12.5px;font-weight:680;color:#be123c;margin-bottom:8px}
59
- .riskmap .rmok{color:#15803d;font-weight:560;font-size:13px;border:0;padding-top:0}
60
- .riskmap .rmli{display:flex;align-items:baseline;gap:9px;padding:6px 0;font-size:13px;flex-wrap:wrap}
61
- .riskmap .rmdot{width:10px;height:10px;border-radius:50%;flex-shrink:0;align-self:center}
62
- .riskmap .rmli b{color:#0b0b0f;font-family:ui-monospace,Menlo,monospace}
63
- .riskmap .rmwhy{color:#5b6068}
64
61
  .rmmodal{display:none;position:fixed;inset:0;z-index:9999;background:rgba(11,11,20,.62);align-items:center;justify-content:center;padding:16px}
65
62
  .rmmodal-box{background:#fff;border-radius:16px;max-width:1100px;width:100%;max-height:92vh;overflow:auto;box-shadow:0 24px 80px rgba(0,0,0,.4)}
66
63
  .rmmodal-bar{display:flex;align-items:center;gap:10px;padding:14px 18px;border-bottom:1px solid #eef0f2;font-size:13px;color:#5b6068;position:sticky;top:0;background:#fff}