SysDeck/web/mini-services/fester/cause.ts

87 lines
2.9 KiB
TypeScript

/**
* Fester cause graph — port of backend/analysis/cause_graph.py.
* Builds a causal graph from the event stream so the UI can answer
* "why did this node make this decision?".
*/
import type { FesterEvent } from './events'
export interface CauseNode {
id: string
kind: 'decision' | 'task' | 'failure' | 'cache' | 'policy' | 'stamp'
label: string
detail?: string
}
export interface CauseEdge {
from: string
to: string
label?: string
}
export interface CauseGraph {
nodes: CauseNode[]
edges: CauseEdge[]
}
export function buildCauseGraph(events: FesterEvent[]): CauseGraph {
const nodes = new Map<string, CauseNode>()
const edges: CauseEdge[] = []
const addNode = (n: CauseNode) => {
if (!nodes.has(n.id)) nodes.set(n.id, n)
}
for (const e of events) {
if (e.type === 'task_update' && e.action) {
const id = `task:${e.action}`
addNode({
id,
kind: 'task',
label: short(e.action),
detail: `${e.state}${e.node ? ` @ ${e.node}` : ''}`,
})
// decision → task edge (scheduler chose this node)
if (e.state === 'scheduled' && e.node) {
const decId = `decision:${e.action}`
addNode({ id: decId, kind: 'decision', label: `score ${e.score ?? '?'} → ${e.node}`, detail: e.reason })
edges.push({ from: decId, to: id, label: 'scheduled' })
// node state influenced the decision
const nodeId = `node:${e.node}`
addNode({ id: nodeId, kind: 'policy', label: e.node, detail: 'node state' })
edges.push({ from: nodeId, to: decId, label: 'load/temp' })
}
// dependency edge
if (e.parent) {
const parentId = `task:${e.parent}`
if (nodes.has(parentId) || events.some((x) => x.action === e.parent)) {
addNode({ id: parentId, kind: 'task', label: short(e.parent), detail: 'upstream' })
edges.push({ from: parentId, to: id, label: 'depends' })
}
}
}
if (e.type === 'cache_update' && e.action) {
const id = `cache:${e.action}`
addNode({ id, kind: 'cache', label: `CAS hit ${short(e.action)}`, detail: String(e.meta?.key ?? '') })
edges.push({ from: id, to: `task:${e.action}`, label: 'skipped exec' })
}
if (e.type === 'failure' && e.action) {
const id = `failure:${e.action}`
addNode({ id, kind: 'failure', label: `✗ ${short(e.action)}`, detail: e.reason })
edges.push({ from: `task:${e.action}`, to: id, label: 'rc≠0' })
}
if (e.type === 'btc_stamp' && e.action) {
const id = `stamp:${e.action}`
addNode({ id, kind: 'stamp', label: `stamp ${short(e.action)}`, detail: String(e.meta?.toolchain ?? 'BTC') })
edges.push({ from: `task:${e.action}`, to: id, label: 'verified' })
}
}
return { nodes: [...nodes.values()], edges }
}
function short(name: string): string {
const parts = name.split(':')
return parts.slice(-2).join(':')
}