diff --git a/package.manifest.json b/package.manifest.json index 1007d19..67ef684 100644 --- a/package.manifest.json +++ b/package.manifest.json @@ -1,6 +1,6 @@ { "name": "@coreplane/whodunit", - "version": "0.2.4", + "version": "0.2.5", "description": "Trace local failures through code, relevant history and agent context.", "type": "module", "license": "MIT", diff --git a/skill/whodunit/scripts/lib/graph.d.ts b/skill/whodunit/scripts/lib/graph.d.ts index af1c1a7..65742df 100644 --- a/skill/whodunit/scripts/lib/graph.d.ts +++ b/skill/whodunit/scripts/lib/graph.d.ts @@ -15,4 +15,16 @@ export interface DebugGraph { kind: "causes" | "supports" | "contradicts"; }[]; } +export interface GraphGeometry { + width: number; + vertical: boolean; + nodes: Record; + edges: DebugGraph["edges"]; +} +export declare function routeGraphEdge(edge: DebugGraph["edges"][number], geometry: GraphGeometry): string; export declare function renderGraph(graph: DebugGraph, root: string, sources: (ids: string[]) => string): string; diff --git a/skill/whodunit/scripts/lib/graph.js b/skill/whodunit/scripts/lib/graph.js index 2d5f730..b38b374 100644 --- a/skill/whodunit/scripts/lib/graph.js +++ b/skill/whodunit/scripts/lib/graph.js @@ -4,6 +4,40 @@ const escapeHtml = (text) => text .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); +// Self-contained: the report embeds this function in its local graph script. +export function routeGraphEdge(edge, geometry) { + const a = geometry.nodes[edge.from], b = geometry.nodes[edge.to]; + if (geometry.vertical && (a.bottom <= b.top || b.bottom <= a.top)) { + const down = b.top >= a.bottom, x = (a.left + a.right) / 2, y = down ? a.bottom : a.top, t = down ? b.top : b.bottom, incoming = geometry.edges.filter((e) => e.to === edge.to), port = incoming.findIndex((e) => e.from === edge.from), u = b.left + ((b.right - b.left) * (port + 1)) / (incoming.length + 1), blocked = Object.entries(geometry.nodes).some(([id, r]) => id !== edge.from && + id !== edge.to && + r.top < Math.max(y, t) && + r.bottom > Math.min(y, t) && + r.left < Math.max(x, u) + 2 && + r.right > Math.min(x, u) - 2); + if (blocked) { + const side = edge.kind === "contradicts" ? 6 : geometry.width - 6, sign = down ? 1 : -1; + return `M ${x} ${y} L ${x} ${y + sign * 12} L ${side} ${y + sign * 12} L ${side} ${t - sign * 12} L ${u} ${t - sign * 12} L ${u} ${t}`; + } + const mid = (y + t) / 2; + return `M ${x} ${y} C ${x} ${mid} ${u} ${mid} ${u} ${t}`; + } + const forward = b.left > a.left, x = forward ? a.right : a.left, y = (a.top + a.bottom) / 2, t = forward ? b.left : b.right, u = (b.top + b.bottom) / 2, mid = (x + t) / 2; + if (Math.abs(a.left - b.left) < 5) { + const down = b.top > a.top, center = (a.left + a.right) / 2, start = down ? a.bottom : a.top, end = down ? b.top : b.bottom, side = edge.kind === "contradicts" ? 6 : geometry.width - 6, sign = down ? 1 : -1; + return `M ${center} ${start} L ${center} ${start + sign * 12} L ${side} ${start + sign * 12} L ${side} ${end - sign * 12} L ${center} ${end - sign * 12} L ${center} ${end}`; + } + const blocked = Object.entries(geometry.nodes).some(([id, r]) => id !== edge.from && + id !== edge.to && + r.left < Math.max(x, t) && + r.right > Math.min(x, t) && + r.top < Math.max(y, u) + 2 && + r.bottom > Math.min(y, u) - 2); + if (blocked) { + const sign = forward ? 1 : -1, floor = Math.max(...Object.values(geometry.nodes).map((r) => r.bottom)) + 10; + return `M ${x} ${y} L ${x + sign * 10} ${y} L ${x + sign * 10} ${floor} L ${t - sign * 10} ${floor} L ${t - sign * 10} ${u} L ${t} ${u}`; + } + return `M ${x} ${y} C ${mid} ${y} ${mid} ${u} ${t} ${u}`; +} export function renderGraph(graph, root, sources) { const selected = graph.focusId ?? graph.nodes[0].id; const columns = [...new Set(graph.nodes.map((n) => n.column))].sort((a, b) => a - b); @@ -24,22 +58,19 @@ export function renderGraph(graph, root, sources) {
${panels}