what_if_revert
Simulates removing specific nodes from the head graph to compute behavioral diff and risk scores. Determines if reverting a commit or function restores prior workflow without executing the revert.
Instructions
Counterfactual reasoning: simulates removing the named nodes from the head graph, then recomputes the behavioral diff and risk scores against the actual base. Answers 'what behaviors recover if I revert this commit / function / class?'. Useful for triaging post-incident commits — quickly see whether a revert restores prior workflow shape without actually running the revert.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| nodeIds | Yes | Node identifiers to remove from the head graph for the counterfactual simulation. Format: 'filepath::SymbolName' or 'filepath::Class::method'. Get them from export_behavioral_graph. |
Implementation Reference
- src/engine/CounterfactualEngine.ts:34-80 (handler)Core implementation: computes actual diff+risk between base and head, then counterfactually removes specified nodes from head, re-runs diff+risk, and returns delta metrics plus a narrative.
public whatIfRevert( baseGraph: BehavioralGraph, headGraph: BehavioralGraph, removeNodeIds: string[] ): CounterfactualResult { const remove = new Set(removeNodeIds); // Actual profile const actualDiff = this.diff.computeDiff(baseGraph, headGraph); const actualRisks = this.risk.assessRisk(actualDiff, headGraph); const actualAvg = actualRisks.length === 0 ? 0 : actualRisks.reduce((s, r) => s + r.score.overallRisk, 0) / actualRisks.length; // Counterfactual: head minus the specified nodes const cfGraph = new BehavioralGraph(); for (const n of headGraph.getNodes()) if (!remove.has(n.id)) cfGraph.addNode(n); for (const e of headGraph.getEdges()) { if (!remove.has(e.sourceId) && !remove.has(e.targetId)) cfGraph.addEdge(e); } const cfDiff = this.diff.computeDiff(baseGraph, cfGraph); const cfRisks = this.risk.assessRisk(cfDiff, cfGraph); const cfAvg = cfRisks.length === 0 ? 0 : cfRisks.reduce((s, r) => s + r.score.overallRisk, 0) / cfRisks.length; // Risks present in actual but absent or reduced in counterfactual const cfRiskMap = new Map(cfRisks.map(r => [r.nodeId, r])); const avoided = actualRisks.filter(r => { const cf = cfRiskMap.get(r.nodeId); return !cf || cf.score.overallRisk < r.score.overallRisk - 5; }); const delta = actualAvg - cfAvg; const impactedDelta = actualDiff.impactedNodes.length - cfDiff.impactedNodes.length; const narrative = buildCounterfactualNarrative(removeNodeIds.length, actualAvg, cfAvg, delta, impactedDelta, avoided.length); return { removedNodeIds: [...remove], actualHeadRisk: parseFloat(actualAvg.toFixed(2)), counterfactualRisk: parseFloat(cfAvg.toFixed(2)), delta: parseFloat(delta.toFixed(2)), actualImpacted: actualDiff.impactedNodes.length, counterfactualImpacted: cfDiff.impactedNodes.length, impactedDelta, risksAvoided: avoided.slice(0, 10), narrative }; } - CounterfactualResult interface defining the output schema: removedNodeIds, actual/counterfactual risk, delta, impacted counts, risksAvoided, and narrative.
export interface CounterfactualResult { removedNodeIds: string[]; actualHeadRisk: number; counterfactualRisk: number; delta: number; // positive = risk drops if reverted actualImpacted: number; counterfactualImpacted: number; impactedDelta: number; risksAvoided: RiskReport[]; narrative: string; }