# 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<string, unknown>;
  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<string, unknown>)[plural] ?? (v as Record<string, unknown>)[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 樹狀結構)
