From e44bdfab5179ff17e05b83c3d6998c00c8cf2d1d Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sat, 31 Jan 2026 19:50:28 +1300 Subject: [PATCH] feat(35-02): implement extractSubgraph for selection-aware context - Implement extractSubgraph function with full selection logic - Early return for no selection (return all nodes/edges, isScoped=false) - Filter nodes into selected vs unselected with O(1) Set lookup - Classify edges: fully within selection, boundary, or outside - Build type breakdown from unselected nodes - Identify boundary connections with direction (incoming/outgoing) - All 8 tests passing (GREEN phase) Co-Authored-By: Claude Opus 4.5 --- src/lib/chat/subgraphExtractor.ts | 86 ++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/src/lib/chat/subgraphExtractor.ts b/src/lib/chat/subgraphExtractor.ts index 5456bb79..1831ae40 100644 --- a/src/lib/chat/subgraphExtractor.ts +++ b/src/lib/chat/subgraphExtractor.ts @@ -49,6 +49,88 @@ export function extractSubgraph( edges: WorkflowEdge[], selectedNodeIds: string[] ): SubgraphResult { - // TODO: Implement - throw new Error("Not implemented"); + // Early return for no selection - return everything + if (selectedNodeIds.length === 0) { + return { + selectedNodes: nodes, + selectedEdges: edges, + restSummary: null, + isScoped: false, + }; + } + + // Build selectedSet for O(1) lookup + const selectedSet = new Set(selectedNodeIds); + + // Filter nodes into selected vs unselected + const selectedNodes: WorkflowNode[] = []; + const unselectedNodes: WorkflowNode[] = []; + + for (const node of nodes) { + if (selectedSet.has(node.id)) { + selectedNodes.push(node); + } else { + unselectedNodes.push(node); + } + } + + // Filter edges: both endpoints in selectedSet -> selectedEdges + const selectedEdges: WorkflowEdge[] = []; + const boundaryEdges: WorkflowEdge[] = []; + + for (const edge of edges) { + const sourceSelected = selectedSet.has(edge.source); + const targetSelected = selectedSet.has(edge.target); + + if (sourceSelected && targetSelected) { + // Both endpoints selected -> fully within selection + selectedEdges.push(edge); + } else if (sourceSelected || targetSelected) { + // Exactly one endpoint selected -> boundary edge + boundaryEdges.push(edge); + } + // else: neither selected -> ignore + } + + // Build type breakdown from unselected nodes + const typeBreakdown: Record = {}; + for (const node of unselectedNodes) { + const type = node.type; + typeBreakdown[type] = (typeBreakdown[type] || 0) + 1; + } + + // Build boundaryConnections from boundary edges with direction + const boundaryConnections: BoundaryConnection[] = boundaryEdges.map((edge) => { + const sourceSelected = selectedSet.has(edge.source); + const targetSelected = selectedSet.has(edge.target); + + if (targetSelected) { + // Target is selected, source is not -> incoming + return { + direction: "incoming" as const, + selectedNodeId: edge.target, + otherNodeId: edge.source, + handleType: edge.targetHandle || "unknown", + }; + } else { + // Source is selected, target is not -> outgoing + return { + direction: "outgoing" as const, + selectedNodeId: edge.source, + otherNodeId: edge.target, + handleType: edge.sourceHandle || "unknown", + }; + } + }); + + return { + selectedNodes, + selectedEdges, + restSummary: { + nodeCount: unselectedNodes.length, + typeBreakdown, + boundaryConnections, + }, + isScoped: true, + }; }