Files
Arcrun/system-dev/docs/5-records/incidents/2026-05-13-nested-foreach-iterable.md
T
uncle6me-web 5d00e71275 chore: D22 落地——docs/SDD/wiki/CLAUDE.md 進 repo(Gitea private 預設全 push)
頂層 D22 決策(leo 2026-07-03 拍板):推什麼由開發環境歸屬決定,
Gitea private=除機敏值/build 產物/.github 外全 push。
解 T1.5 卡點:雲端工人 clone 拿得到 credential-store-migration.md,可就地改寫 SDD。
機敏掃描兩輪通過(新增 189 檔約 2.1MB,node_modules/dist/wasm 照舊排除)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:13:33 +08:00

4.4 KiB
Raw Blame History

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 想做三層樹寫入:

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。

兩個獨立根因

根因 Aresult only, no fallback to context

外層 FOREACH 跑時:

  • create_paragraph output: { data: { id, ... }, success: true }
  • FOREACH 處理 >> 對每個 triplet >> create_tripletiteratorKey = triplet
  • getIterableFromContext(result, 'triplet')result.triplet / result.triplets → 都沒,回 []

paragraph.triplets 早就在 ctx(外層 FOREACH 注入了 paragraph 物件)。

→ FOREACH 該 fallback 找 context。

根因 B:只看 top-level,不看 nested

即使 fallback 找 contextgetIterableFromContext(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

// 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 找 pluralparagraph → 找 paragraphs)。如果 ctx 內的 array 用單數命名(如 items),會找不到。MVP 階段建議 yaml 內 array 用複數,FOREACH 用單數,符合慣例

Reference