import type { Journey, JourneyNode, JourneyEdge, JourneyNodeId, JourneyEdgeId, } from './journey-store.js'; import type { Guideline, GuidelineId, Criticality } from './guideline-store.js'; export interface ProjectedGuideline { id: GuidelineId; content: { condition: string; action: string | null; description: string | null; }; criticality: Criticality; creationUtc: string; enabled: boolean; tags: string[]; labels: string[]; metadata: Record; } function formatNodeGuidelineId(nodeId: JourneyNodeId, edgeId?: JourneyEdgeId | null): GuidelineId { return `journey_node:${nodeId}${edgeId ? `:${edgeId}` : ''}` as GuidelineId; } export function projectJourneyToGuidelines( journey: Journey, nodes: JourneyNode[], edges: JourneyEdge[], ): ProjectedGuideline[] { const nodeMap = new Map(); for (const n of nodes) nodeMap.set(n.id, n); const edgeMap = new Map(); for (const e of edges) edgeMap.set(e.id, e); const nodeEdges = new Map(); for (const e of edges) { const list = nodeEdges.get(e.source) || []; list.push(e); nodeEdges.set(e.source, list); } const guidelines: Map = new Map(); const nodeIndexes = new Map(); let index = 0; const queue: Array<{ edgeId: JourneyEdgeId | null; nodeId: JourneyNodeId }> = []; const visited = new Set(); queue.push({ edgeId: null, nodeId: journey.rootId }); while (queue.length > 0) { const { edgeId, nodeId } = queue.shift()!; const visitKey = `${edgeId || ''}:${nodeId}`; if (visited.has(visitKey)) continue; visited.add(visitKey); const node = nodeMap.get(nodeId); if (!node) continue; if (!nodeIndexes.has(nodeId)) { index++; nodeIndexes.set(nodeId, index); } const edge = edgeId ? edgeMap.get(edgeId) : undefined; const baseJourneyNode: Record = { follow_ups: [], index: String(nodeIndexes.get(nodeId)), journey_id: journey.id, labels: node.labels, tool_ids: node.tools, }; const edgeJourneyNode = (edge?.metadata?.['journey_node'] as Record) || {}; const nodeJourneyNode = (node.metadata?.['journey_node'] as Record) || {}; const mergedJourneyNode = { ...baseJourneyNode, ...nodeJourneyNode, ...edgeJourneyNode }; const metadata: Record = { journey_node: mergedJourneyNode, }; for (const [k, v] of Object.entries(node.metadata)) { if (k !== 'journey_node') metadata[k] = v; } if (edge) { for (const [k, v] of Object.entries(edge.metadata)) { if (k !== 'journey_node') metadata[k] = v; } } const gid = formatNodeGuidelineId(nodeId, edgeId); const guideline: ProjectedGuideline = { id: gid, content: { condition: (edge?.condition) || '', action: node.action, description: node.description, }, criticality: 'high' as Criticality, creationUtc: new Date().toISOString(), enabled: true, tags: journey.tags, labels: [...(node.labels || [])], metadata, }; guidelines.set(gid, guideline); const childEdges = nodeEdges.get(nodeId) || []; for (const childEdge of childEdges) { if (visited.has(`${childEdge.id}:${childEdge.target}`)) continue; queue.push({ edgeId: childEdge.id, nodeId: childEdge.target }); const childGid = formatNodeGuidelineId(childEdge.target, childEdge.id); const followUps = (guideline.metadata['journey_node'] as Record)['follow_ups'] as string[]; if (!followUps.includes(childGid)) { followUps.push(childGid); } } } return [...guidelines.values()]; } export interface BacktrackCheck { journeyId: string; currentNodeId: JourneyNodeId; previousNodeId: JourneyNodeId; isBacktrack: boolean; recommendation: string | null; } export function detectJourneyBacktrack( journey: Journey, nodes: JourneyNode[], edges: JourneyEdge[], currentNodeId: JourneyNodeId, previousNodeId: JourneyNodeId, ): BacktrackCheck { const nodeMap = new Map(); for (const n of nodes) nodeMap.set(n.id, n); const adjacency = new Map(); for (const e of edges) { const list = adjacency.get(e.source) || []; list.push(e.target); adjacency.set(e.source, list); } const isInForwardPath = (from: JourneyNodeId, target: JourneyNodeId): boolean => { const visitedInner = new Set(); const queueInner: JourneyNodeId[] = [from]; while (queueInner.length > 0) { const current = queueInner.shift()!; if (current === target) return true; if (visitedInner.has(current)) continue; visitedInner.add(current); for (const next of adjacency.get(current) || []) { if (!visitedInner.has(next)) queueInner.push(next); } } return false; }; const fromCurToPrev = isInForwardPath(currentNodeId, previousNodeId); const fromPrevToCur = isInForwardPath(previousNodeId, currentNodeId); const isBacktrack = !fromPrevToCur && !fromCurToPrev; let recommendation: string | null = null; if (isBacktrack && nodeMap.has(previousNodeId)) { const prevNode = nodeMap.get(previousNodeId)!; recommendation = `Detected potential backtrack from '${currentNodeId}' to '${previousNodeId}' (${prevNode.action || 'no action'}). Consider whether this regression is intentional.`; } return { journeyId: journey.id, currentNodeId, previousNodeId, isBacktrack, recommendation, }; }