* Compute workflows using LLM-assigned workflow names per file, * with hub detection for end-to-end shared service inclusion. * Falls back to connectivity-based grouping if no assignments exist.
(nodes: WorkflowNode[], edges: WorkflowEdge[])
| 938 | * Falls back to connectivity-based grouping if no assignments exist. |
| 939 | */ |
| 940 | private computeWorkflows(nodes: WorkflowNode[], edges: WorkflowEdge[]): WorkflowMetadata[] { |
| 941 | // Phase 1: Group nodes by stored workflow name (per-node from LLM analysis) |
| 942 | const workflowGroups = new Map<string, { id: string; name: string; description?: string; nodeIds: string[] }>(); |
| 943 | let hasAnyAssignment = false; |
| 944 | |
| 945 | for (const node of nodes) { |
| 946 | const file = node.source?.file; |
| 947 | if (!file) continue; |
| 948 | const normalizedFile = this.toRelativePath(file); |
| 949 | const cached = this.files[normalizedFile]; |
| 950 | const wfAssignment = cached?.nodeWorkflows?.[node.id]; |
| 951 | if (wfAssignment) { |
| 952 | hasAnyAssignment = true; |
| 953 | const key = wfAssignment.name; |
| 954 | if (!workflowGroups.has(key)) { |
| 955 | workflowGroups.set(key, { |
| 956 | id: wfAssignment.id || `workflow_${workflowGroups.size}`, |
| 957 | name: key, |
| 958 | description: this.workflows[wfAssignment.id || '']?.description, |
| 959 | nodeIds: [] |
| 960 | }); |
| 961 | } |
| 962 | workflowGroups.get(key)!.nodeIds.push(node.id); |
| 963 | } |
| 964 | } |
| 965 | |
| 966 | // Fallback: if no files have workflow assignments, use connectivity-based grouping |
| 967 | if (!hasAnyAssignment) { |
| 968 | return this.computeWorkflowsByConnectivity(nodes, edges); |
| 969 | } |
| 970 | |
| 971 | // Phase 2: Iteratively assign orphan nodes via edges until no more changes |
| 972 | // This handles chains like A→B→C where only A is initially assigned |
| 973 | const assignedNodes = new Set( |
| 974 | [...workflowGroups.values()].flatMap(g => g.nodeIds) |
| 975 | ); |
| 976 | |
| 977 | let changed = true; |
| 978 | let iterations = 0; |
| 979 | const maxIterations = 20; // Prevent infinite loops |
| 980 | |
| 981 | while (changed && iterations < maxIterations) { |
| 982 | changed = false; |
| 983 | iterations++; |
| 984 | |
| 985 | for (const node of nodes) { |
| 986 | if (assignedNodes.has(node.id)) continue; |
| 987 | |
| 988 | // Find any edge connecting this node to an assigned node |
| 989 | const connectedEdge = edges.find( |
| 990 | e => (e.source === node.id && assignedNodes.has(e.target)) || |
| 991 | (e.target === node.id && assignedNodes.has(e.source)) |
| 992 | ); |
| 993 | |
| 994 | if (connectedEdge) { |
| 995 | const assignedId = connectedEdge.source === node.id |
| 996 | ? connectedEdge.target : connectedEdge.source; |
| 997 | for (const wf of workflowGroups.values()) { |
no test coverage detected