import type { ParsedTriplets } from './triplet-parser'; import { toEdgeType } from './triplet-parser'; import type { SearchResult } from './search-nodes'; /** 從 nodeResults + parsed 組成可直接送入 /execute 的 ExecutionGraph * * config 格式(來自 workflow YAML 的 config 欄位): * { node_name: { component: "cmp_xxxxxxxx" | "rec_xxxxxxxx" | canonical_id, ...params } } * * 若 config[node].component 存在,以它覆蓋 searchNodes 偵測到的 componentId。 * config[node] 的其他欄位作為節點靜態參數(node.data),合併進執行 context。 */ export function buildExecutionGraph( parsed: ParsedTriplets, nodeResults: SearchResult['nodeResults'], graphId: string, graphName: string, config?: Record>, ) { const nodes = [...parsed.nodeNames].map(name => { const nr = nodeResults[name]!; const id = name.toLowerCase().replace(/\s+/g, '-'); const nodeConfig = config?.[name] ?? {}; // config[name].component 可以是 hash 或 canonical_id,覆蓋自動偵測的 componentId const componentId = (nodeConfig.component as string | undefined) ?? nr.componentId; // 其他 config 欄位作為 node.data(靜態參數) const { component: _component, ...staticParams } = nodeConfig; const data = Object.keys(staticParams).length > 0 ? staticParams : undefined; return { id, type: nr.type, componentId, label: name, data }; }); const edges = parsed.edges.map(e => { // 「對每個 X」label 抽 iterator:cypher binding 表達 FOREACH 的迭代變數 // 例:'A >> 對每個 paragraph >> B' → type=FOREACH, iterator='paragraph' // getIterableFromContext 會找 ctx.paragraphs(複數)或 ctx.paragraph let iterator: string | undefined; let label = e.label; const foreachMatch = label.match(/^(?:對每個|FOREACH)\s+(\w+)$/i); if (foreachMatch) { iterator = foreachMatch[1]; label = '對每個'; // 改回標準 label 走 SEMANTIC_EDGE_MAP } // 「ON_BRANCH(標籤)」抽 branch:意圖語法表達具名分支(SDD workflow-discovery 3.11) // 例:'my_switch >> ON_BRANCH(branch_active) >> 處理啟用' → type=ON_BRANCH, branch='branch_active' // 沒有這段的話,帶括號的 label 會落到 toEdgeType 的預設值 PIPE ⇒ 分支靜默失效 // (即「教了語法但引擎不收」——比沒做更糟,故與 skill 文件同批補上) let branch: string | undefined; const branchMatch = label.match(/^(?:ON_BRANCH|分支)\s*[((]\s*([\w-]+)\s*[))]$/i); if (branchMatch) { branch = branchMatch[1]; label = 'ON_BRANCH'; } const edge: { from: string; to: string; type: ReturnType; iterator?: string; branch?: string; } = { from: e.from.toLowerCase().replace(/\s+/g, '-'), to: e.to.toLowerCase().replace(/\s+/g, '-'), type: toEdgeType(label), }; if (iterator) edge.iterator = iterator; if (branch) edge.branch = branch; return edge; }); return { id: graphId, name: graphName, nodes, edges }; }