# 33-needs-gate-role-filter-bug. `produces:["log"]` 声明了一个从不会被索引到的 kind —— validator 被 missing_required_input 误判 > **日期**: 2026-08-21 发现,2026-08-22 修复 > **状态**: ✅ **已修复**(发现于 31 号文档修复的真实 E2E 回归验证过程中,是一个独立的新 bug, > 与 31 号文档的兜底边问题无关) > **触发**: `e2e-demo` 公司真实回归任务 `T-mt2r6fz0-125b`(goal `G-mt2r62ia-73af`, > root run `run_0e9074815130`)——为验证 [31 号文档](./31-work-graph-fail-fallback-coverage.md) > 的兜底边修复而起的新任务,链路走到 `org-coder-validator` 时意外反复 blocked > > **2026-08-22 修订**:§2.3 最初写的根因假设(`inboundContracts` 该按 `role` 过滤只留 > `needs`)**是错的**,动手改了之后跑单测直接炸了 9 个用例——`impl→test`、`brd→prd` > 这类只声明 `produces`(没有配对 `needs`)的边占绝大多数,`inboundContracts` 把它们 > 也算进「下游需要的输入」正是**设计如此**,还是 T-mt25ecbd(commit `aecf5c9`)那次 > 产出闸门的直接依赖。已把那次改动撤回,真正的根因和修法见 §2.3(修订版)与 §四。 --- ## 一、先说 31 号文档的验证结果:修复生效,有真实证据 这条任务本身是为了拿到 31 号文档 §六「尚未做」的那一步真实验收证据(当时因为原任务 `T-mt1qx0c4-95bf` 状态不干净,改用确定性回归测试代替,见 31 号文档 §7.3)。这次补上了: - 新起的 `org-planner` 子图(`run_38b3df8fd228`)里,`org-planner-sdd` 真实自判失败 (下游信息不足),沿新补的兜底边(`org-planner-sdd --fail--> org-planner-prd`, edge `wge_ce0f7f05f6dc`)**自动回退了 3 次**(`graph_run_loop_records`: `verdict=registered`,`loop_count` 1→2→3),全程**零次 `retryNode` 调用**,最终 brd→prd→sdd→benchmark→validator 整条链路收敛到 `run_status=completed`。 - 31 号文档的决策 A(除入口节点外强制兜底边)在真实 LLM 驱动下按设计工作,**结论确认**。 --- ## 二、新发现:`org-coder-validator` 被 `missing_required_input` 反复误判 ### 2.1 现象 同一条任务往下走到 `org-coder`(`run_21fd814c2f56`,用的是 `executor.json` 骨架)时, `org-coder-validator` 连续两轮都以 `fail_reason=missing_required_input` 收场: - 第一轮(attempt 1,10:01-10:11):沿兜底边 `validator --fail--> impl` 回退两次 (`wge_1bdb181b14ef` 一次、`wge_aa5bad61fe9a` 一次),第三次撞上 run 树级回退硬顶 (`backward_count` 全树累计 >5,`LOOP_HARD_CAP_RUN=5`),`force_terminated`, 节点 `blocked`。**这一段是硬顶熔断本身在正常工作**,不是这次要报的 bug。 - 人工 `POST /nodes/:id/retry` 把 `org-coder-validator` 打回 `ready` 后, **第二轮**(attempt 2,10:42:48-10:44:01)**又**判了一次 `missing_required_input`, 这次连新的 `loop_record` 都没写(回退硬顶已越顶,直接静默 blocked)。 - 节点自己的 `remediationNote` 写的是「无需整改。……本轮重新独立跑了一遍测试脚本与 goal-contract,结果与 attempt-1 完全一致,判定 attempt-1 的 passed 结论有效」—— **agent 自己认为通过了**,是引擎的产出/输入契约闸门(T-mt25ecbd 那次加的机制, commit `aecf5c9`)把 success 强行改判成 fail。 ### 2.2 复现证据:`graph_files` 里明明有 `artifact` 却仍报缺 查 `T-mt2r6fz0-125b` 的产物索引(`data/graph.sqlite` 的 `graph_files` 表), `org-coder-impl`(`rn_e71be3641174`,10:06:51 完成)与 `org-coder-test` (`rn_cd3e3a1c51e2`,10:08:52 完成)都成功登记了当前(`superseded_by IS NULL`) `kind='artifact'` 的产物行(`_plan.md` / `_research.md` / `coder.log`)。 `org-coder-validator` 的 attempt 2 在 10:42:48 才跑,**比这些产物晚了半小时以上**, 不是时序竞争,是稳定复现。 ### 2.3 根因(修订版):`produces:["log"]` 声明的是一个永远认不出来的 kind `executor.json` 里 `test → validator` 这条边同时声明了两条契约(`work-graph-db.js` `declareEdgeContract`): ```json { "from": "{{orgNode}}-test", "to": "{{orgNode}}-validator", "condition": "success", "produces": ["log"], "needs": ["artifact"] } ``` 即:`kind=log, role=produces`(test 自己产出的是 log,required 默认 true)+ `kind=artifact, role=needs`(validator 真正要吃的是 artifact,中继声明,产出方是 更早的 impl,非相邻依赖,R-19.3 ②)。`executor.json` 头部 `dataContractNotes` 写明了设计意图——「validator 既要看测试记录(紧邻上游 produces log)也要看被验的 交付物本身」,即:`produces` 与 `needs` **都该算进 validator 的输入需求**,这不是 误用,`inboundContracts()` 把两者都吐出来是**故意的、正确的**(详见下方「走过的 弯路」)。 真正的根因在**产出侧**:`test` 类节点(`org-coder-test`、`org-coder-test-a/b`、 `org-coder-integration`)从来不会写出一个能被认作 `kind=log` 的文件。 `indexNodeOutputs()`(`graph-engine.js:3835`)给文件定 kind 的规则是「文件名匹配 声明的 kind → 已知文件名 → 唯一无歧义兜底」三级,而这几类节点实际落盘的是 `_plan.md` / `_research.md` / `coder.log` 这类文件——`coder.log` 恰好属于 **impl** 节点(不是 test),test 自己写的两份文件文件名里都没有 "log",且同时有两个认不出 kind 的文件,触发不了「一对一无歧义」的兜底认领。于是 `graph_files` 里对这个 task **永远**不会出现一行 `kind='log'`——`unsatisfiedRequiredProduces()`(出边产出 闸门,`graph-engine.js:3801`)本身对此是宽容的:它只检查节点私有目录里有没有 **任何**候选文件,不检查文件是否精确匹配声明的 kind,所以 `test` 自己能顺利 `success`;但 `requiredInputsMissing()`(`graph-engine.js:3945`)在 `validator` 结算时是严格的:`declared kind - 索引里查到的 kind` 非空就判 `missing_required_input`, 而 `log` 这个 kind 因为上面那条产出侧的缺口从未出现在索引里——**这不是一次性的 时序问题,是这条边只要还这么声明就永远无法满足的硬伤,每次重试都复现**。 对照 `validator.json`(同一批模板):它的 `needs` 声明本来就写成 `{ "kind": "artifact", "required": false }`——同一份代码库里已经有先例, 知道这类「声明了但未必查得到实例」的依赖要标 `required:false`,只是 `executor.json` / `executor-fanout.json` 里的 `produces:["log"]` 用的是裸数组 写法(`normalizeContracts` 对裸字符串项默认 `required:true`),漏了这一条。 ### 2.4 走过的弯路:为什么「按 role 过滤 inboundContracts」是错的 最初的假设是「`inboundContracts()` 不该把 `produces` 也当成 `needs`,只留 `role==='needs'` 的行」。改完发现 `work-graph-skeletons.test.js` / `org-layer-contracts.test.js` 等 9 个用例炸了:`org-planner-sdd`(入边 `prd→sdd: produces:["prd"]`,无配对 `needs`)、`org-coder-test`(入边 `impl→test: produces:["artifact"]`,无配对 `needs`)等一大批节点的入边**只声明 了 `produces`,从不显式写 `needs`**——这本来就是绝大多数模板边的写法(`declareEdgeContract` 注释原话:「出边声明产出是压倒性多数,needs 必须显式写」)。按 role 过滤会让这些 节点的 `inboundContracts()` 变成空数组,`requiredInputsMissing()` 也就不再校验 它们——而这条链路正是 T-mt25ecbd(commit `aecf5c9`)刚加固的产出闸门本身 依赖的数据源(`org-planner-benchmark` 靠它校验 `needs:[brd,prd,sdd]` 真的都被 索引到)。按 role 过滤表面上修好了 `log` 这一个 kind,实际上会**静默削弱** T-mt25ecbd 那次修复要守住的保护——好在跑单测立刻炸了,没有带着这个错误方向走 下去。**结论**:`inboundContracts()` 本身没有 bug,别再按这个方向改它。 ### 2.5 实际修法 在**声明**这一侧把 `log` 标成 `required:false`,与 `validator.json` 的既有写法 对齐——既保留「validator 能看到 test 的 log(如果将来真的被索引到)」这条文档 价值,又不再把一个永远查不到实例的 kind 当硬闸门: ```diff - { "from": "{{orgNode}}-test", "to": "{{orgNode}}-validator", "condition": "success", - "produces": ["log"], "needs": ["artifact"] }, + { "from": "{{orgNode}}-test", "to": "{{orgNode}}-validator", "condition": "success", + "produces": [{ "kind": "log", "required": false }], "needs": ["artifact"] }, ``` 改了 4 处(`executor.json` 1 处,`executor-fanout.json` 3 处:`test-a→merge`、 `test-b→merge`、`integration→validator`——同一种「produces:log 但 test/integration 类节点从不产出可识别的 log 文件」的形状)。真正必需的 `needs:artifact` (`required:true`)保持不变,继续受硬闸门保护。 --- ## 三、已完成 - [x] 撤回 §2.4 那次错误方向的引擎改动(`work-graph-db.js` 的 `inboundContracts` 恢复原状,加注释记录这条弯路,防止以后有人重新踩一遍) - [x] 按 §2.5 改 `executor.json` / `executor-fanout.json` 的 4 处 `produces:["log"]` 声明为 `required:false` - [x] 新增单测 `graph-files-index.test.js` ⑪:`produces` 声明 `required:false` 且该 kind 从未被索引到时,不拦下游节点 success;同一条边上 `required:true` 的 `needs:artifact` 依旧受硬闸门保护(未被这次改动削弱) - [x] 全量单测 `npx vitest run server/tests/unit`:**998/998 通过**(含新增用例, 零回归) - [x] (2026-08-22 追加)用真实回归任务验证 `org-coder-validator` 不再假阳性 blocked——**已验证**:直接把这次真实卡住的那条 run 的 `graph_edge_contracts` 行(`log` 那条)手动 patch 成 `required=0`,重试 `org-coder-validator` 并 恢复自动驱动,节点从 `blocked` 转 `success`,root run 最终收敛到 `completed`。唯一没有走到的是"由 `workgraph-planning` 全新规划一张图, 模板里 `{kind:'log',required:false}` 的对象写法被正确解析落库"这一环—— 理论上没有风险(`validator.json` 已经在用同样的对象写法且工作正常,见下一条), 但没有用一次全新任务端到端跑过,只是手动 patch 了已有的行。 - [x] (2026-08-22 追加)`validator.json` 的 `needs:[{kind:'artifact',required:false}]` 复查过:**不是同一类潜伏问题,是刻意设计**。它的 `dataContractNotes` 原文 写明:被验的 `artifact` 由**另一张图**(org-coder 等)产出,这里的 `needs` 只是文档意图声明,产物索引按 `company+task` 检索、不按图/边——`required:false` 是因为这条 `artifact` 依赖压根不指望靠这条边找到实例(找的时候看的是任务级 共享索引,边上声明与否不影响检索结果),跟 33 号文档这次的 bug(`log` 声明了 却永远不会被索引到)成因完全不同,不用动。 ## 四、留待下次 - [ ] "由 `workgraph-planning` 全新规划一张图" 这一环的端到端证据仍然缺失 (见上面第五条最后一句)——理论风险低,但没有真跑过