@mneme-ai/xray 2.175.0 → 2.177.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/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export { xrayGauntlet, type XRayGauntlet } from "./gauntlet.js";
16
16
  export { isAllowedPublicUrl, isSafeBranch } from "./clone.js";
17
17
  export { listRemoteBranches, remoteRef, reportDelta, trackerTick, trackGauntlet, type RemoteBranch, type ReportDelta, type Drift, type TrackState, type TickResult } from "./track.js";
18
18
  export { TrackerHub, trackId, hubGauntlet, verifyWebhookSig, type SseSink, type BuildFn, type RefFn, type TrackRecord, type HistoryEntry } from "./tracker_server.js";
19
+ export { buildRiskMap, riskMapGauntlet, MAP_W, MAP_H, MAP_CAP, type RiskMap, type RiskNode, type RiskEdge } from "./riskmap.js";
19
20
  export { defaultFetcher, type MetaFetcher } from "./battery/deps.js";
20
21
  export { publishReport, type PublishResult } from "./publish.js";
21
22
  export { createXRayServer } from "./server.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,KAAK,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AACvL,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACtK,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAC3G,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,KAAK,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AACvL,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACtK,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAChI,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAC3G,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ export { xrayGauntlet } from "./gauntlet.js";
16
16
  export { isAllowedPublicUrl, isSafeBranch } from "./clone.js";
17
17
  export { listRemoteBranches, remoteRef, reportDelta, trackerTick, trackGauntlet } from "./track.js";
18
18
  export { TrackerHub, trackId, hubGauntlet, verifyWebhookSig } from "./tracker_server.js";
19
+ export { buildRiskMap, riskMapGauntlet, MAP_W, MAP_H, MAP_CAP } from "./riskmap.js";
19
20
  export { defaultFetcher } from "./battery/deps.js";
20
21
  export { publishReport } from "./publish.js";
21
22
  export { createXRayServer } from "./server.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAqB,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,YAAY,EAAoB,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAqB,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAqF,MAAM,YAAY,CAAC;AACvL,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAA+E,MAAM,qBAAqB,CAAC;AACtK,OAAO,EAAE,cAAc,EAAoB,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAsB,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAkC,MAAM,aAAa,CAAC;AAC3G,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAoB,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAqB,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,YAAY,EAAoB,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAqB,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAqF,MAAM,YAAY,CAAC;AACvL,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAA+E,MAAM,qBAAqB,CAAC;AACtK,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAA8C,MAAM,cAAc,CAAC;AAChI,OAAO,EAAE,cAAc,EAAoB,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAsB,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAkC,MAAM,aAAa,CAAC;AAC3G,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAoB,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * RISK MAP — the "if this person leaves, what breaks?" constellation.
3
+ *
4
+ * A 2D network the CEO grasps in two seconds: each node is a FILE the analysis
5
+ * actually flagged; node colour = key-person risk (single-owner share), node size
6
+ * = churn/size, edges = files that change together (temporal coupling). High-risk
7
+ * single-owner files sit near the centre.
8
+ *
9
+ * ★ACCURACY CONTRACT (so it can't be disputed): every node + edge is taken
10
+ * VERBATIM from the signed X-Ray report — fragileFiles (single-owner %), hotspots
11
+ * (churn × size), coupling pairs. NOTHING is invented; a file with no measured
12
+ * signal is not drawn. The layout is DETERMINISTIC (a golden-angle spiral, no
13
+ * randomness) so the same report always renders the same map.
14
+ *
15
+ * Pure + total: missing/garbage fields never throw; coordinates are always finite
16
+ * and clamped inside the viewBox. Proven over 100,000 random reports (the gauntlet).
17
+ */
18
+ export interface RiskNode {
19
+ id: number;
20
+ file: string;
21
+ risk: number;
22
+ size: number;
23
+ ownerPct: number;
24
+ churn: number;
25
+ x: number;
26
+ y: number;
27
+ }
28
+ export interface RiskEdge {
29
+ a: number;
30
+ b: number;
31
+ weight: number;
32
+ hidden: boolean;
33
+ }
34
+ export interface RiskMap {
35
+ nodes: RiskNode[];
36
+ edges: RiskEdge[];
37
+ maxRisk: number;
38
+ W: number;
39
+ H: number;
40
+ note: string;
41
+ }
42
+ export declare const MAP_W = 920, MAP_H = 460, MAP_CAP = 24;
43
+ /** Build the deterministic risk constellation from a signed report. Total. */
44
+ export declare function buildRiskMap(report: unknown): RiskMap;
45
+ export interface GauntletCheck {
46
+ name: string;
47
+ pass: boolean;
48
+ detail: string;
49
+ }
50
+ export interface RiskMapGauntlet {
51
+ score: number;
52
+ iterations: number;
53
+ checks: GauntletCheck[];
54
+ }
55
+ export declare function riskMapGauntlet(iterations?: number): RiskMapGauntlet;
56
+ //# sourceMappingURL=riskmap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"riskmap.d.ts","sourceRoot":"","sources":["../src/riskmap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,QAAQ;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE;AACzI,MAAM,WAAW,QAAQ;IAAG,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE;AACnF,MAAM,WAAW,OAAO;IAAG,KAAK,EAAE,QAAQ,EAAE,CAAC;IAAC,KAAK,EAAE,QAAQ,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAEtH,eAAO,MAAM,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,OAAO,KAAK,CAAC;AAKpD,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CA+CrD;AAGD,MAAM,WAAW,aAAa;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAC9E,MAAM,WAAW,eAAe;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,aAAa,EAAE,CAAA;CAAE;AAgB/F,wBAAgB,eAAe,CAAC,UAAU,SAAU,GAAG,eAAe,CA0BrE"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * RISK MAP — the "if this person leaves, what breaks?" constellation.
3
+ *
4
+ * A 2D network the CEO grasps in two seconds: each node is a FILE the analysis
5
+ * actually flagged; node colour = key-person risk (single-owner share), node size
6
+ * = churn/size, edges = files that change together (temporal coupling). High-risk
7
+ * single-owner files sit near the centre.
8
+ *
9
+ * ★ACCURACY CONTRACT (so it can't be disputed): every node + edge is taken
10
+ * VERBATIM from the signed X-Ray report — fragileFiles (single-owner %), hotspots
11
+ * (churn × size), coupling pairs. NOTHING is invented; a file with no measured
12
+ * signal is not drawn. The layout is DETERMINISTIC (a golden-angle spiral, no
13
+ * randomness) so the same report always renders the same map.
14
+ *
15
+ * Pure + total: missing/garbage fields never throw; coordinates are always finite
16
+ * and clamped inside the viewBox. Proven over 100,000 random reports (the gauntlet).
17
+ */
18
+ export const MAP_W = 920, MAP_H = 460, MAP_CAP = 24;
19
+ const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
20
+ const num = (x) => (Number.isFinite(Number(x)) ? Number(x) : 0);
21
+ const str = (x) => (typeof x === "string" ? x : "");
22
+ /** Build the deterministic risk constellation from a signed report. Total. */
23
+ export function buildRiskMap(report) {
24
+ const r = (report && typeof report === "object" ? report : {});
25
+ const bf = (r["busFactor"] || {});
26
+ const hs = (r["hotspots"] || {});
27
+ const cx = (r["complexity"] || {});
28
+ const cp = (r["coupling"] || {});
29
+ // 1) gather per-file signals from the report (verbatim)
30
+ const byFile = new Map();
31
+ const touch = (file) => { const f = str(file); if (!f)
32
+ return null; if (!byFile.has(f))
33
+ byFile.set(f, { ownerPct: 0, churn: 0, loc: 0 }); return byFile.get(f); };
34
+ for (const x of (Array.isArray(bf["fragileFiles"]) ? bf["fragileFiles"] : [])) {
35
+ const n = touch(str(x?.["file"]));
36
+ if (n)
37
+ n.ownerPct = Math.max(n.ownerPct, clamp(num(x?.["topAuthorShare"]), 0, 1));
38
+ }
39
+ for (const x of (Array.isArray(hs["hotspots"]) ? hs["hotspots"] : [])) {
40
+ const n = touch(str(x?.["file"]));
41
+ if (n) {
42
+ n.churn = Math.max(n.churn, num(x?.["changes"]));
43
+ n.loc = Math.max(n.loc, num(x?.["loc"]));
44
+ }
45
+ }
46
+ for (const x of (Array.isArray(cx["hotspots"]) ? cx["hotspots"] : [])) {
47
+ const n = touch(str(x?.["file"]));
48
+ if (n)
49
+ n.loc = Math.max(n.loc, num(x?.["bodyLines"]));
50
+ }
51
+ const pairs = (Array.isArray(cp["pairs"]) ? cp["pairs"] : []);
52
+ for (const p of pairs) {
53
+ touch(str(p?.["a"]));
54
+ touch(str(p?.["b"]));
55
+ }
56
+ // 2) score + cap (worst-first: risk then size)
57
+ const maxChurn = Math.max(1, ...[...byFile.values()].map((v) => v.churn));
58
+ const maxLoc = Math.max(1, ...[...byFile.values()].map((v) => v.loc));
59
+ let entries = [...byFile.entries()].map(([file, v]) => {
60
+ const size = clamp(Math.max(v.churn / maxChurn, v.loc / maxLoc), 0, 1);
61
+ const risk = clamp(v.ownerPct, 0, 1);
62
+ return { file, risk, size, ownerPct: v.ownerPct, churn: v.churn };
63
+ });
64
+ entries.sort((a, b) => (b.risk - a.risk) || (b.size - a.size) || a.file.localeCompare(b.file));
65
+ entries = entries.slice(0, MAP_CAP);
66
+ const indexOf = new Map(entries.map((e, i) => [e.file, i]));
67
+ // 3) DETERMINISTIC golden-angle spiral — highest-risk (index 0) nearest centre
68
+ const cxC = MAP_W / 2, cyC = MAP_H / 2, GOLD = 2.399963229728653;
69
+ const nodes = entries.map((e, i) => {
70
+ const ang = i * GOLD;
71
+ const rad = 26 + 30 * Math.sqrt(i);
72
+ const x = clamp(cxC + rad * Math.cos(ang), 40, MAP_W - 40);
73
+ const y = clamp(cyC + rad * Math.sin(ang) * 0.56, 36, MAP_H - 36);
74
+ return { id: i, file: e.file, risk: e.risk, size: e.size, ownerPct: e.ownerPct, churn: e.churn, x: Math.round(x * 10) / 10, y: Math.round(y * 10) / 10 };
75
+ });
76
+ // 4) edges = coupling pairs whose BOTH files are nodes (verbatim confidence)
77
+ const edges = [];
78
+ for (const p of pairs) {
79
+ const a = indexOf.get(str(p?.["a"])), b = indexOf.get(str(p?.["b"]));
80
+ if (a === undefined || b === undefined || a === b)
81
+ continue;
82
+ edges.push({ a, b, weight: clamp(num(p?.["confidence"]), 0, 1), hidden: !!p?.["hidden"] });
83
+ }
84
+ const maxRisk = nodes.reduce((m, n) => Math.max(m, n.risk), 0);
85
+ return { nodes, edges, maxRisk, W: MAP_W, H: MAP_H, note: nodes.length ? `${nodes.length} flagged file(s), ${edges.length} coupling link(s)` : "no per-file risk signals in this repo" };
86
+ }
87
+ /** seeded LCG → deterministic, reproducible "random" reports for the stress test */
88
+ function lcg(seed) { let s = seed >>> 0; return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; }
89
+ function randomReport(rnd) {
90
+ const nF = Math.floor(rnd() * 40), nH = Math.floor(rnd() * 40), nP = Math.floor(rnd() * 40);
91
+ const file = () => `src/${Math.floor(rnd() * 1000)}/${Math.floor(rnd() * 1000)}.ts`;
92
+ return {
93
+ busFactor: { fragileFiles: Array.from({ length: nF }, () => ({ file: rnd() < 0.05 ? "" : file(), topAuthorShare: rnd() < 0.05 ? NaN : rnd() * 1.2, commits: Math.floor(rnd() * 100) })) },
94
+ hotspots: { hotspots: Array.from({ length: nH }, () => ({ file: file(), changes: rnd() < 0.05 ? -5 : Math.floor(rnd() * 500), loc: Math.floor(rnd() * 3000) })) },
95
+ complexity: { hotspots: Array.from({ length: Math.floor(rnd() * 20) }, () => ({ file: file(), bodyLines: Math.floor(rnd() * 800) })) },
96
+ coupling: { pairs: Array.from({ length: nP }, () => ({ a: file(), b: rnd() < 0.1 ? "" : file(), confidence: rnd() < 0.05 ? NaN : rnd(), hidden: rnd() < 0.3 })) },
97
+ };
98
+ }
99
+ export function riskMapGauntlet(iterations = 100_000) {
100
+ const rnd = lcg(1234567);
101
+ let threw = 0, badCoord = 0, badRange = 0, badEdge = 0, overCap = 0;
102
+ for (let i = 0; i < iterations; i++) {
103
+ let m;
104
+ try {
105
+ m = buildRiskMap(randomReport(rnd));
106
+ }
107
+ catch {
108
+ threw++;
109
+ continue;
110
+ }
111
+ if (m.nodes.length > MAP_CAP)
112
+ overCap++;
113
+ for (const n of m.nodes) {
114
+ if (!Number.isFinite(n.x) || !Number.isFinite(n.y) || n.x < 0 || n.x > MAP_W || n.y < 0 || n.y > MAP_H)
115
+ badCoord++;
116
+ if (!(n.risk >= 0 && n.risk <= 1) || !(n.size >= 0 && n.size <= 1))
117
+ badRange++;
118
+ }
119
+ for (const e of m.edges)
120
+ if (e.a < 0 || e.b < 0 || e.a >= m.nodes.length || e.b >= m.nodes.length || e.a === e.b)
121
+ badEdge++;
122
+ }
123
+ // determinism: a fixed report renders identically twice
124
+ const fixed = randomReport(lcg(42));
125
+ const det = JSON.stringify(buildRiskMap(fixed)) === JSON.stringify(buildRiskMap(fixed));
126
+ const checks = [
127
+ { name: "TOTAL", pass: threw === 0, detail: `0 throws over ${iterations.toLocaleString()} random reports (got ${threw})` },
128
+ { name: "COORDS-IN-BOX", pass: badCoord === 0, detail: `every node coordinate finite + inside ${MAP_W}×${MAP_H} (violations ${badCoord})` },
129
+ { name: "RANGE-0-1", pass: badRange === 0, detail: `risk + size always in [0,1] (violations ${badRange})` },
130
+ { name: "EDGES-VALID", pass: badEdge === 0, detail: `every edge references two distinct existing nodes (violations ${badEdge})` },
131
+ { name: "CAP", pass: overCap === 0, detail: `node count never exceeds ${MAP_CAP} (violations ${overCap})` },
132
+ { name: "DETERMINISTIC", pass: det, detail: "same report → byte-identical map" },
133
+ ];
134
+ const passed = checks.filter((c) => c.pass).length;
135
+ return { score: Math.round((passed / checks.length) * 100), iterations, checks };
136
+ }
137
+ //# sourceMappingURL=riskmap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"riskmap.js","sourceRoot":"","sources":["../src/riskmap.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mneme-ai/xray",
3
- "version": "2.175.0",
3
+ "version": "2.177.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
@@ -93,8 +93,52 @@
93
93
  else if (age.vitality === "active") { tone = "ok"; head = `✅ Healthy & actively maintained — safe to build on`; }
94
94
  else { tone = "neutral"; head = `Reviewed — ${T.length} thing${T.length === 1 ? "" : "s"} to know below`; }
95
95
 
96
+ // the concrete RISK ITEMS (what exactly + where) — listed, scrollable if long
97
+ const risks = [];
98
+ (sec.hits || []).forEach((h) => risks.push({ g: "Secret", icon: "🔑", t: `${esc(h.kind)} — ${esc(h.file)}:${h.line}` }));
99
+ (su.destructive || []).forEach((d) => risks.push({ g: "Destructive CI", icon: "💥", t: `${esc(d.where)} — ${esc(String(d.command).slice(0, 80))}` }));
100
+ (dep.atRisk || []).filter((d) => d.band === "dead" || d.band === "moribund").forEach((d) => risks.push({ g: "Dying dep", icon: "📦", t: `${esc(d.name)} (${esc(d.band)})${d.successor ? ` → ${esc(d.successor)}` : ""}` }));
101
+ (dep.licenseFlags || []).forEach((l) => risks.push({ g: "License", icon: "⚖️", t: `${esc(l.name)} — ${esc(l.license)}` }));
102
+ (bf.fragileFiles || []).forEach((f) => risks.push({ g: "Single-owner", icon: "👤", t: `${esc(f.file)} — one author owns ${Math.round((f.topAuthorShare || 0) * 100)}%` }));
103
+ (su.injectionWhere || []).forEach((w) => risks.push({ g: "Prompt-injection", icon: "🧪", t: esc(w) }));
104
+
96
105
  const top = T.slice(0, 5);
97
- return { tone, head, kind, takeaways: top };
106
+ return { tone, head, kind, takeaways: top, risks };
107
+ }
108
+
109
+ // RISK MAP — mirrors packages/xray/src/riskmap.ts (the tested + 100k-stressed source
110
+ // of truth). Every node/edge is verbatim from the signed report; nothing invented.
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
+ const riskColor = (t) => (t < 0.5 ? mix("#16a34a", "#d97706", t / 0.5) : mix("#d97706", "#e11d48", (t - 0.5) / 0.5));
113
+ function riskMapHTML(r) {
114
+ 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 = 920, H = 460, CAP = 24, byFile = new Map();
117
+ 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); };
118
+ (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)))); });
119
+ (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)); } });
120
+ (cx.hotspots || []).forEach((x) => { const n = touch(x && x.file); if (n) n.l = Math.max(n.l, num(x.bodyLines)); });
121
+ const pairs = cp.pairs || []; pairs.forEach((p) => { touch(p && p.a); touch(p && p.b); });
122
+ if (!byFile.size) return "";
123
+ const mc = Math.max(1, ...[...byFile.values()].map((v) => v.c)), ml = Math.max(1, ...[...byFile.values()].map((v) => v.l));
124
+ 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)), churn: v.c }));
125
+ es.sort((a, b) => (b.risk - a.risk) || (b.size - a.size) || a.file.localeCompare(b.file));
126
+ es = es.slice(0, CAP);
127
+ const idx = new Map(es.map((e, i) => [e.file, i]));
128
+ const cxC = W / 2, cyC = H / 2, GOLD = 2.399963229728653;
129
+ const nodes = es.map((e, i) => { const ang = i * GOLD, rad = 26 + 30 * Math.sqrt(i); return { ...e, i, x: Math.min(W - 40, Math.max(40, cxC + rad * Math.cos(ang))), y: Math.min(H - 36, Math.max(36, cyC + rad * Math.sin(ang) * 0.56)) }; });
130
+ const base = (f) => { const p = String(f).split("/"); return p[p.length - 1]; };
131
+ const edges = [];
132
+ 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) }); });
133
+ 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 - 24; 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" : "#9aa0ad"}" stroke-width="${(0.6 + e.w * 2.2).toFixed(2)}" stroke-opacity="${(0.18 + e.w * 0.5).toFixed(2)}"${e.hidden ? ' stroke-dasharray="5 4"' : ""}/>`; }).join("");
134
+ const nodeSvg = nodes.map((n) => { const rad = 7 + n.size * 20, col = riskColor(n.risk), showLabel = n.i < 8 || n.risk >= 0.7; return `<g><circle cx="${n.x}" cy="${n.y}" r="${(rad + 7).toFixed(1)}" fill="${col}" opacity="0.14"/><circle cx="${n.x}" cy="${n.y}" r="${rad.toFixed(1)}" fill="${col}" opacity="0.92"/>${showLabel ? `<text x="${n.x}" y="${(n.y + rad + 12).toFixed(1)}" text-anchor="middle" font-size="11" fill="#5b6068" font-family="ui-monospace,Menlo,monospace">${esc(base(n.file)).slice(0, 22)}${n.ownerPct >= 0.5 ? ` ${Math.round(n.ownerPct * 100)}%` : ""}</text>` : ""}</g>`; }).join("");
135
+ const owned = nodes.filter((n) => n.risk >= 0.6).length;
136
+ return `<div class="riskmap">
137
+ <div class="rmhead">🗺 Risk map — <b>if an owner leaves, the red nodes lose their only expert</b></div>
138
+ <div class="rmsub">${nodes.length} flagged file(s) · ${edges.length} coupling link(s)${owned ? ` · ${owned} single-owner hotspot(s)` : ""} — node colour = key-person risk · size = churn · line = files that change together. Every node is verbatim from the signed report.</div>
139
+ <svg class="rmsvg" viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="repository risk map">${edgeSvg}${nodeSvg}</svg>
140
+ <div class="rmleg"><span><i style="background:#16a34a"></i>shared / low risk</span><span><i style="background:#d97706"></i>concentrated</span><span><i style="background:#e11d48"></i>single-owner (key-person)</span><span><i class="dash"></i>hidden cross-dir coupling</span></div>
141
+ </div>`;
98
142
  }
99
143
 
100
144
  function xrayCardHTML(signed, opts) {
@@ -136,7 +180,10 @@
136
180
  <div class="vhead">${esc(vd.head)}</div>
137
181
  <div class="vkind">${esc(vd.kind)} · what this means for you ↓</div>
138
182
  <ul class="vlist">${vd.takeaways.map((t) => `<li class="vt-${t.t}">${t.x}</li>`).join("")}</ul>
183
+ ${vd.risks.length ? `<details class="vrisks"${vd.risks.length <= 6 ? " open" : ""}><summary>📋 ${vd.risks.length} flagged item${vd.risks.length > 1 ? "s" : ""} — exactly what &amp; where</summary>
184
+ <div class="vrlist">${vd.risks.map((k) => `<div class="vr"><span class="vrg">${k.icon} ${k.g}</span><span class="vrt">${k.t}</span></div>`).join("")}</div></details>` : ""}
139
185
  </div>
186
+ ${riskMapHTML(r)}
140
187
  <div class="membrane">
141
188
  <div class="mp"><span class="mpk">① CAPABILITY</span><span class="mpv">${s.signalsRun} deterministic signals · ${(sec.filesScanned || 0).toLocaleString()} files scanned</span></div>
142
189
  <div class="mp"><span class="mpk">② ATTENTION</span><span class="mpv">${tri.length ? `${tri.length} signal(s) need attention` : `all signals clear`}</span></div>
package/public/index.html CHANGED
@@ -79,6 +79,23 @@
79
79
  .v-warn{background:#fffaf0} .v-warn .vhead{color:#b45309}
80
80
  .v-ok{background:#f0fdf4} .v-ok .vhead{color:#15803d}
81
81
  .v-neutral{background:#fafafb}
82
+ .verdict .vrisks{margin-top:12px;border-top:1px dashed #e5e7eb;padding-top:10px}
83
+ .verdict .vrisks summary{cursor:pointer;font-size:12.5px;font-weight:600;color:#33333b;list-style:none}
84
+ .verdict .vrisks summary::-webkit-details-marker{display:none}
85
+ .verdict .vrlist{margin-top:8px;max-height:188px;overflow-y:auto;border:1px solid #ececef;border-radius:10px;background:#fff}
86
+ .verdict .vrlist::-webkit-scrollbar{width:9px} .verdict .vrlist::-webkit-scrollbar-thumb{background:#d3d6dd;border-radius:8px;border:2px solid #fff} .verdict .vrlist::-webkit-scrollbar-track{background:transparent}
87
+ .verdict .vr{display:flex;gap:10px;padding:7px 12px;border-bottom:1px solid #f3f3f5;font-size:12.5px;align-items:baseline}
88
+ .verdict .vr:last-child{border-bottom:0}
89
+ .verdict .vrg{flex-shrink:0;min-width:104px;font-weight:600;color:#8b8f98}
90
+ .verdict .vrt{color:#33333b;font-family:ui-monospace,Menlo,monospace;word-break:break-all}
91
+ .riskmap{padding:18px 30px;border-bottom:1px solid #eef0f2;background:linear-gradient(180deg,#fcfcfd,#fff)}
92
+ .riskmap .rmhead{font-size:14.5px;color:#0b0b0f}
93
+ .riskmap .rmsub{font-size:11.5px;color:#8b8f98;margin:4px 0 10px;line-height:1.5}
94
+ .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)}
95
+ .riskmap .rmleg{display:flex;flex-wrap:wrap;gap:14px;margin-top:10px;font-size:11.5px;color:#5b6068}
96
+ .riskmap .rmleg span{display:inline-flex;align-items:center;gap:6px}
97
+ .riskmap .rmleg i{width:11px;height:11px;border-radius:50%;display:inline-block}
98
+ .riskmap .rmleg i.dash{width:18px;height:0;border-radius:0;border-top:2px dashed #e11d48}
82
99
  .trustbar{display:flex;align-items:center;gap:14px;padding:13px 30px;background:#f0fdf4;border-bottom:1px solid #dcfce7;flex-wrap:wrap}
83
100
  .hgauge{display:inline-flex;align-items:center;gap:8px;font-weight:680;color:#15803d;font-size:14px;white-space:nowrap}
84
101
  .hdot{width:10px;height:10px;border-radius:50%;background:#16a34a;box-shadow:0 0 0 4px rgba(22,163,74,.16)}
@@ -206,23 +223,26 @@
206
223
  .lscard .pill{display:inline-block;font-size:11px;background:var(--soft);border:1px solid var(--line);border-radius:20px;padding:1px 8px;color:var(--ink2)}
207
224
  .lscard .lsnote{margin-top:10px;font-size:11.5px;color:var(--sub);line-height:1.5;border-top:1px solid var(--line2);padding-top:9px}
208
225
  /* SHOWCASE — selling points; the graphic sits to the SIDE (grid areas, no markup move) */
209
- .showcase{max-width:1060px;margin:64px auto 0;padding:0 20px;text-align:left;
210
- display:grid;column-gap:36px;row-gap:10px;align-items:start;align-content:start;
211
- grid-template-columns:1fr 380px;grid-template-areas:"title art" "sub art" "cards cards"}
212
- .showcase .sctitle{grid-area:title;font-size:30px;font-weight:700;letter-spacing:-.02em;color:var(--ink);margin:0;line-height:1.2}
226
+ /* centered header; then cards (left, 3-across, readable) + graphic (right column, in the white space) */
227
+ .showcase{max-width:1200px;margin:60px auto 0;padding:0 24px;
228
+ display:grid;column-gap:48px;row-gap:6px;align-items:center;
229
+ grid-template-columns:minmax(0,1fr) 340px;grid-template-areas:"title title" "sub sub" "cards art"}
230
+ .showcase .sctitle{grid-area:title;text-align:center;max-width:760px;margin:0 auto;font-size:29px;font-weight:700;letter-spacing:-.02em;color:var(--ink);line-height:1.25}
213
231
  .showcase .sctitle .hl{color:var(--a)}
214
- .showcase .scsub{grid-area:sub;font-size:15px;color:var(--sub);margin:0;line-height:1.6}
215
- .showcase .bigart{grid-area:art;display:flex;justify-content:center;align-self:center;opacity:.95}
216
- .showcase .sc3{grid-area:cards;display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:14px}
217
- .showcase .sccard{background:var(--soft);border:1px solid var(--line);border-radius:16px;padding:20px}
218
- .showcase .scico{font-size:26px;margin-bottom:8px}
219
- .showcase .sccard b{display:block;font-size:15px;color:var(--ink);margin-bottom:6px}
220
- .showcase .sccard span{font-size:13px;color:var(--sub);line-height:1.55}
221
- @media(max-width:820px){
222
- .showcase{grid-template-columns:1fr;grid-template-areas:"title" "sub" "art" "cards"}
223
- .showcase .sc3{grid-template-columns:1fr}
224
- .showcase .bigart svg{width:300px;height:auto}
232
+ .showcase .scsub{grid-area:sub;text-align:center;max-width:660px;margin:10px auto 22px;font-size:15px;color:var(--sub);line-height:1.6}
233
+ .showcase .sc3{grid-area:cards;display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
234
+ .showcase .sccard{background:var(--soft);border:1px solid var(--line);border-radius:16px;padding:18px}
235
+ .showcase .scico{font-size:24px;margin-bottom:8px}
236
+ .showcase .sccard b{display:block;font-size:14.5px;color:var(--ink);margin-bottom:6px}
237
+ .showcase .sccard span{font-size:12.5px;color:var(--sub);line-height:1.5}
238
+ .showcase .bigart{grid-area:art;display:flex;justify-content:center;align-self:center;opacity:.9}
239
+ .showcase .bigart svg{width:100%;height:auto;max-width:320px}
240
+ @media(max-width:1080px){ /* below this the graphic hides + cards go full-width 3-across (readability wins over decoration) */
241
+ .showcase{grid-template-columns:1fr;grid-template-areas:"title" "sub" "cards";max-width:1000px}
242
+ .showcase .bigart{display:none}
243
+ .showcase .sc3{grid-template-columns:repeat(3,1fr)}
225
244
  }
245
+ @media(max-width:720px){ .showcase .sc3{grid-template-columns:1fr;max-width:460px;margin:0 auto} }
226
246
  .picker{display:none;margin-top:12px;border:1px solid var(--line);border-radius:10px;background:#fff;overflow:hidden}
227
247
  .picker.on{display:block}
228
248
  .pkbar{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--line2);font-size:12.5px}
@@ -667,6 +687,14 @@ function renderLocalScan(r){
667
687
  const sec=s.secrets, secCls=sec.totalFindings>0?"bad":"ok";
668
688
  const langs=s.langs.map(([e,n])=>`<span class="pill">${esc(e)} ${n}</span>`).join(" ");
669
689
  const g=s.git, vit = !g.has ? "" : (s.dormantDays>365?"dormant":s.dormantDays>120?"slowing":"active");
690
+ // VERDICT — turn the local numbers into a decision + what-to-do (same idea as the URL report)
691
+ const lt=[]; let ltone="ok", lhead="✅ Quick scan looks clean — see details below";
692
+ if(sec.totalFindings>0){ ltone="bad"; lhead="⚠️ Secret(s) in this folder — fix before sharing"; lt.push(`🔴 ${sec.totalFindings} credential${sec.totalFindings>1?"s":""} in production code — rotate + add a pre-commit secret scan.`); }
693
+ if(g.has && g.authors<=1){ if(ltone==="ok"){ltone="warn"; lhead="✅ Scanned — ⚠️ solo-owned (key-person risk)";} lt.push(`🟠 Bus factor 1 — one author made all ${g.commits} commits. If they leave, this stalls; add reviewers + docs.`); }
694
+ if(g.has && s.dormantDays>365){ if(ltone==="ok"){ltone="warn"; lhead="🪦 Looks unmaintained — last commit over a year ago";} lt.push(`🟠 No activity for ${s.dormantDays} days — may be stale; confirm it is still maintained.`); }
695
+ if(sec.totalFindings===0 && (!g.has || (g.authors>1 && s.dormantDays<=365))) lt.push(`✅ No leaked secrets${g.has?`, actively worked (${g.commits} commits, ${g.authors} authors)`:""} — clean for a quick local check.`);
696
+ if(s.deps.total>0) lt.push(`ℹ️ ${s.deps.total} dependencies declared — run the full report (public URL / bridge) to check which are dying or risky-licensed.`);
697
+ const lverdict = `<div class="verdict v-${ltone}" style="margin:0 -16px 12px;border-radius:0;border-bottom:1px solid #eef0f2"><div class="vhead" style="font-size:15px">${esc(lhead)}</div><ul class="vlist" style="margin-top:8px">${lt.slice(0,4).map(t=>`<li>${t}</li>`).join("")}</ul></div>`;
670
698
  const gitRows = g.has ? `
671
699
  <div class="lsrow"><span class="lsk">Bus factor</span><span class="lsv ${g.authors===1?"bad":""}"><b>${g.authors}</b> author${g.authors===1?" — single point of failure":"s"} · top author ${Math.round(g.topShare*100)}% of ${g.commits} commits</span></div>
672
700
  <div class="lsrow"><span class="lsk">Vitality</span><span class="lsv ${vit==="dormant"?"bad":""}"><b>${vit}</b> · ${s.ageDays}d span · last activity ${s.dormantDays}d ago</span></div>`
@@ -674,6 +702,7 @@ function renderLocalScan(r){
674
702
  out.innerHTML=`<div class="lscard">
675
703
  <div class="lstop"><div class="lsgrade g-${("ABCDEF".includes(s.grade)?s.grade:"C")}">${esc(s.grade)}</div>
676
704
  <div><div class="lsh"><b>📂 ${esc(r.folder)}</b></div><div class="muted" style="font-size:12px">scanned in your browser · ${r.files} files · nothing uploaded</div></div></div>
705
+ ${lverdict}
677
706
  <div class="lsrow"><span class="lsk">Secrets</span><span class="lsv ${secCls}"><b>${sec.totalFindings}</b> in production code${sec.excludedTestHits?` · ${sec.excludedTestHits} in tests (excluded)`:""}</span></div>
678
707
  ${sec.hits.length?`<div class="lshits">${sec.hits.slice(0,6).map(h=>`<div>🔴 ${esc(h.kind)} — ${esc(h.file)}:${h.line}</div>`).join("")}</div>`:`<div class="lsok">✓ no leaked credentials in production code</div>`}
679
708
  ${gitRows}
@@ -32,6 +32,23 @@
32
32
  .v-warn{background:#fffaf0} .v-warn .vhead{color:#b45309}
33
33
  .v-ok{background:#f0fdf4} .v-ok .vhead{color:#15803d}
34
34
  .v-neutral{background:#fafafb}
35
+ .verdict .vrisks{margin-top:12px;border-top:1px dashed #e5e7eb;padding-top:10px}
36
+ .verdict .vrisks summary{cursor:pointer;font-size:12.5px;font-weight:600;color:#33333b;list-style:none}
37
+ .verdict .vrisks summary::-webkit-details-marker{display:none}
38
+ .verdict .vrlist{margin-top:8px;max-height:188px;overflow-y:auto;border:1px solid #ececef;border-radius:10px;background:#fff}
39
+ .verdict .vrlist::-webkit-scrollbar{width:9px} .verdict .vrlist::-webkit-scrollbar-thumb{background:#d3d6dd;border-radius:8px;border:2px solid #fff} .verdict .vrlist::-webkit-scrollbar-track{background:transparent}
40
+ .verdict .vr{display:flex;gap:10px;padding:7px 12px;border-bottom:1px solid #f3f3f5;font-size:12.5px;align-items:baseline}
41
+ .verdict .vr:last-child{border-bottom:0}
42
+ .verdict .vrg{flex-shrink:0;min-width:104px;font-weight:600;color:#8b8f98}
43
+ .verdict .vrt{color:#33333b;font-family:ui-monospace,Menlo,monospace;word-break:break-all}
44
+ .riskmap{padding:18px 30px;border-bottom:1px solid #eef0f2;background:linear-gradient(180deg,#fcfcfd,#fff)}
45
+ .riskmap .rmhead{font-size:14.5px;color:#0b0b0f}
46
+ .riskmap .rmsub{font-size:11.5px;color:#8b8f98;margin:4px 0 10px;line-height:1.5}
47
+ .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)}
48
+ .riskmap .rmleg{display:flex;flex-wrap:wrap;gap:14px;margin-top:10px;font-size:11.5px;color:#5b6068}
49
+ .riskmap .rmleg span{display:inline-flex;align-items:center;gap:6px}
50
+ .riskmap .rmleg i{width:11px;height:11px;border-radius:50%;display:inline-block}
51
+ .riskmap .rmleg i.dash{width:18px;height:0;border-radius:0;border-top:2px dashed #e11d48}
35
52
  .trustbar{display:flex;align-items:center;gap:14px;padding:13px 30px;background:#f0fdf4;border-bottom:1px solid #dcfce7;flex-wrap:wrap}
36
53
  .hgauge{display:inline-flex;align-items:center;gap:8px;font-weight:680;color:#15803d;font-size:14px}
37
54
  .hdot{width:10px;height:10px;border-radius:50%;background:#16a34a;box-shadow:0 0 0 4px rgba(22,163,74,.16)}