# 2026-05-13 cypher-executor 巢狀 FOREACH 內層找不到 iterable > **總耗時**:約 30 分鐘 > **根因**:`getIterableFromContext()` 只看當前節點 result + 只看 top-level,巢狀 FOREACH 內層拿不到外層注入的 nested array > **修法**:fallback 找 context;掃 ctx 內 object 取 nested key > **影響**:任何想做「FOREACH X → 每個 X 內再 FOREACH Y」結構的 workflow 都壞 --- ## 症狀 mira `wiki_synthesis` 想做三層樹寫入: ```yaml flow: - "classify >> ON_SUCCESS >> create_wiki_page" - "create_wiki_page >> 對每個 paragraph >> create_paragraph" - "create_paragraph >> 對每個 triplet >> create_triplet" ``` classify 回 `{ entity, paragraphs: [{ facet, content, triplets: [...] }, ...] }`。 - 外層 FOREACH(對每個 paragraph):✓ 正常跑 N 次 - 內層 FOREACH(對每個 triplet):✗ 跑 0 次 KBDB 內結果:N 個 wiki-paragraph 建好,0 個 triplet。 ## 兩個獨立根因 ### 根因 A:`result` only, no fallback to context 外層 FOREACH 跑時: - `create_paragraph` output: `{ data: { id, ... }, success: true }` - FOREACH 處理 `>> 對每個 triplet >> create_triplet`:iteratorKey = `triplet` - `getIterableFromContext(result, 'triplet')` 找 `result.triplet` / `result.triplets` → 都沒,回 `[]` 但 `paragraph.triplets` 早就在 ctx(外層 FOREACH 注入了 `paragraph` 物件)。 → FOREACH 該 fallback 找 context。 ### 根因 B:只看 top-level,不看 nested 即使 fallback 找 context,`getIterableFromContext(ctx, 'triplet')` 找 `ctx.triplet` / `ctx.triplets`,**找 top-level**。 但 triplets 在 `ctx.paragraph.triplets`(外層 FOREACH 把 paragraph 整個物件注入 ctx 的 `paragraph` key)。 → `getIterableFromContext` 該掃一層 nested。 ## 修法 `cypher-executor/src/graph-executor.ts`: ```typescript // FOREACH case let items = getIterableFromContext(result, iteratorKey); if (items.length === 0) { items = getIterableFromContext(context, iteratorKey); // ← A. fallback } // getIterableFromContext function getIterableFromContext(context: unknown, key: string): unknown[] { if (!context || typeof context !== 'object') return []; const plural = key + 's'; const obj = context as Record; let items = obj[plural] ?? obj[key]; if (!Array.isArray(items)) { for (const v of Object.values(obj)) { // ← B. 掃 nested if (v !== null && typeof v === 'object' && !Array.isArray(v)) { const nested = (v as Record)[plural] ?? (v as Record)[key]; if (Array.isArray(nested)) { items = nested; break; } } } } return Array.isArray(items) ? items : []; } ``` ## 驗證 mira wiki_synthesis 跑「物理 AI」raw → KBDB 內出現: ``` wiki-page "物理 AI" (ef644ec3) ├─ paragraph 00d8f819 │ ├─ triplet: 物理 AI >> 對立於 >> 純數位空間的 AI │ └─ triplet: 物理 AI >> 提出者 >> Andrej Karpathy └─ paragraph 9a5b11b7 ├─ triplet: leo >> 支持 >> 物理 AI └─ triplet: 物理 AI >> 需要 >> 傳感器與機器人協同 ``` 4 個 triplet 都正確接到對應 paragraph parent_id。 ## 為什麼這個沒早被踩到 cypher binding 之前的用例都是「一層 FOREACH」(commit e8fca33 wiki workflow 範例就是單層)。mira V2 wiki 結構是首個真正用巢狀 FOREACH 的,所以才剛踩到。 ## 未來避免 1. **設計 FOREACH 時假設 iterable 可能在 ctx 任何深度** —— 這次的「掃一層 nested」其實還不夠通用,未來如果有三層 FOREACH(A → B → C → D),可能要遞迴掃。MVP 先一層,需要時擴 2. **驗證 yaml 應該支援巢狀 FOREACH 測試** —— CLI validator 沒擋住巢狀,但「能 parse」不等於「能跑」。未來加 e2e 測試 3. **`對每個 X` 命名建議用單數** —— iteratorKey 自動補 `s` 找 plural(`paragraph` → 找 `paragraphs`)。如果 ctx 內的 array 用單數命名(如 `items`),會找不到。MVP 階段建議 yaml 內 array 用複數,FOREACH 用單數,符合慣例 ## Reference - 對應 SDD:`matrix/arcrun/.agents/specs/arcrun/arcrun.md` P0 #10 補完 C 段 - 同日先解: - [2026-05-13-cypher-outbound-522.md](./2026-05-13-cypher-outbound-522.md) - [2026-05-13-chain-ctx-propagation.md](./2026-05-13-chain-ctx-propagation.md) - 受影響 SDD:`polaris/mira/.agents/specs/mira-app/tasks.md` 7B.3e (V2 樹狀結構)