Reference implementation for the Phoenix Architecture. Work in progress. aicoding.leaflet.pub/
ai coding crazy
at main 97 lines 4.2 kB view raw
1/** 2 * Shadow Pipeline — runs old and new canonicalization pipelines in parallel, 3 * compares output, and classifies the upgrade. 4 */ 5 6import type { CanonicalNode } from './models/canonical.js'; 7import type { ShadowDiffMetrics, ShadowResult, PipelineConfig } from './models/pipeline.js'; 8import { UpgradeClassification } from './models/pipeline.js'; 9 10/** 11 * Compare two sets of canonical nodes produced by different pipeline versions. 12 */ 13export function computeShadowDiff( 14 oldNodes: CanonicalNode[], 15 newNodes: CanonicalNode[], 16): ShadowDiffMetrics { 17 const oldIds = new Set(oldNodes.map(n => n.canon_id)); 18 const newIds = new Set(newNodes.map(n => n.canon_id)); 19 20 const addedNodes = newNodes.filter(n => !oldIds.has(n.canon_id)); 21 const removedNodes = oldNodes.filter(n => !newIds.has(n.canon_id)); 22 const keptNodes = newNodes.filter(n => oldIds.has(n.canon_id)); 23 24 const totalNodes = Math.max(oldNodes.length, 1); 25 const nodeChangePct = ((addedNodes.length + removedNodes.length) / totalNodes) * 100; 26 27 // Edge changes 28 const oldEdges = new Set(oldNodes.flatMap(n => n.linked_canon_ids.map(l => `${n.canon_id}->${l}`))); 29 const newEdges = new Set(newNodes.flatMap(n => n.linked_canon_ids.map(l => `${n.canon_id}->${l}`))); 30 const edgeAdded = [...newEdges].filter(e => !oldEdges.has(e)).length; 31 const edgeRemoved = [...oldEdges].filter(e => !newEdges.has(e)).length; 32 const totalEdges = Math.max(oldEdges.size, 1); 33 const edgeChangePct = ((edgeAdded + edgeRemoved) / totalEdges) * 100; 34 35 // Orphan nodes (new nodes with no links and no source) 36 const orphanNodes = addedNodes.filter( 37 n => n.linked_canon_ids.length === 0 && n.source_clause_ids.length === 0 38 ).length; 39 40 // Risk escalations: nodes that changed type (approximate by statement match) 41 const oldByStmt = new Map(oldNodes.map(n => [n.statement, n])); 42 let riskEscalations = 0; 43 for (const nn of newNodes) { 44 const old = oldByStmt.get(nn.statement); 45 if (old && old.type !== nn.type) riskEscalations++; 46 } 47 48 // Semantic statement drift: how many statements are completely new 49 const oldStmts = new Set(oldNodes.map(n => n.statement)); 50 const driftCount = newNodes.filter(n => !oldStmts.has(n.statement)).length; 51 const semanticDrift = (driftCount / Math.max(newNodes.length, 1)) * 100; 52 53 return { 54 node_change_pct: Math.round(nodeChangePct * 100) / 100, 55 edge_change_pct: Math.round(edgeChangePct * 100) / 100, 56 risk_escalations: riskEscalations, 57 orphan_nodes: orphanNodes, 58 out_of_scope_growth: addedNodes.length - removedNodes.length, 59 semantic_stmt_drift: Math.round(semanticDrift * 100) / 100, 60 }; 61} 62 63/** 64 * Classify a shadow diff as SAFE, COMPACTION_EVENT, or REJECT. 65 */ 66export function classifyShadowDiff(metrics: ShadowDiffMetrics): { 67 classification: UpgradeClassification; 68 reason: string; 69} { 70 if (metrics.orphan_nodes > 0) { 71 return { classification: UpgradeClassification.REJECT, reason: `${metrics.orphan_nodes} orphan nodes detected` }; 72 } 73 if (metrics.semantic_stmt_drift > 50) { 74 return { classification: UpgradeClassification.REJECT, reason: `Semantic drift too high: ${metrics.semantic_stmt_drift}%` }; 75 } 76 if (metrics.node_change_pct <= 3 && metrics.risk_escalations === 0) { 77 return { classification: UpgradeClassification.SAFE, reason: `Node change ${metrics.node_change_pct}% ≤ 3%, no risk escalations` }; 78 } 79 if (metrics.node_change_pct <= 25 && metrics.orphan_nodes === 0) { 80 return { classification: UpgradeClassification.COMPACTION_EVENT, reason: `Node change ${metrics.node_change_pct}% ≤ 25%, no orphans` }; 81 } 82 return { classification: UpgradeClassification.REJECT, reason: `Excessive churn: ${metrics.node_change_pct}% node change` }; 83} 84 85/** 86 * Run a full shadow comparison. 87 */ 88export function runShadowPipeline( 89 oldPipeline: PipelineConfig, 90 newPipeline: PipelineConfig, 91 oldNodes: CanonicalNode[], 92 newNodes: CanonicalNode[], 93): ShadowResult { 94 const metrics = computeShadowDiff(oldNodes, newNodes); 95 const { classification, reason } = classifyShadowDiff(metrics); 96 return { old_pipeline: oldPipeline, new_pipeline: newPipeline, metrics, classification, reason }; 97}