頂層 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>
4.4 KiB
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。
兩個獨立根因
根因 A:result only, no fallback to context
外層 FOREACH 跑時:
create_paragraphoutput:{ 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:
// 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 的,所以才剛踩到。
未來避免
- 設計 FOREACH 時假設 iterable 可能在 ctx 任何深度 —— 這次的「掃一層 nested」其實還不夠通用,未來如果有三層 FOREACH(A → B → C → D),可能要遞迴掃。MVP 先一層,需要時擴
- 驗證 yaml 應該支援巢狀 FOREACH 測試 —— CLI validator 沒擋住巢狀,但「能 parse」不等於「能跑」。未來加 e2e 測試
對每個 X命名建議用單數 —— iteratorKey 自動補s找 plural(paragraph→ 找paragraphs)。如果 ctx 內的 array 用單數命名(如items),會找不到。MVP 階段建議 yaml 內 array 用複數,FOREACH 用單數,符合慣例
Reference
- 對應 SDD:
matrix/arcrun/.agents/specs/arcrun/arcrun.mdP0 #10 補完 C 段 - 同日先解:
- 受影響 SDD:
polaris/mira/.agents/specs/mira-app/tasks.md7B.3e (V2 樹狀結構)