Commit 9cab41fd7ebafc6d69fe0f5640d7b5e2a2a31253

Authored by yibo
1 parent 91b39388

docs(31/33): 记录 sdd 兜底边修复的真实 E2E 回归证据 + 新发现的 needs 契约误判 bug

用真实 LLM 起了一条新任务(G-mt2r62ia-73af/T-mt2r6fz0-125b)回归验证 83afecb 的修
复:org-planner-sdd 自判失败后沿新补的兜底边自动回退 3 次、零人工介入,最终收敛到
completed,补上 31 号文档 §六此前用确定性测试代替的那步真实验收证据。

顺带在 org-coder 子图撞见一个独立的新 bug:inboundContracts()/requiredInputsMissing()
不按 role 过滤 graph_edge_contracts,把同一条边上声明的 produces(源节点自己产出什么)
也当成目标节点的必需输入,导致 org-coder-validator 被 missing_required_input 反复假阳
性 blocked(agent 自己判定 passed,引擎却强改成 fail)。已定位根因、写清复现证据与建议
修法,记在 33-needs-gate-role-filter-bug.md,留给下次实施。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs/README.md
@@ -48,6 +48,8 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d @@ -48,6 +48,8 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d
48 | [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(设计见 29 号,已实现,见下)、`source_kind`(✅已删)/`legacy_path`(不能删)删列、三项「已实施未验证」。任务态清理(✅已实现,见下)。§五 是**已立起来的守卫清单**(别重复造,也别删) | 48 | [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(设计见 29 号,已实现,见下)、`source_kind`(✅已删)/`legacy_path`(不能删)删列、三项「已实施未验证」。任务态清理(✅已实现,见下)。§五 是**已立起来的守卫清单**(别重复造,也别删) |
49 | [29-dream-design.md](./todo/system-design/29-dream-design.md) | **dream(经验汇流)**— ✅ 已实现。定时整理所有 agent 在所有公司的记忆,提炼后跨公司沉淀(不泄漏公司信息)。四条取舍已拍板并实现:去敏独立判定、四落点分层写入(经验/`SKILL.md`/`soul.md`/`agent.md`,受管区块+三道闸门)、每日定时(串行+空转保护)、引用真伪自动查、REST+控制台入口(`/api/dream/*`、`/dream`)。`innovator-evolver` 真 CLI 跑通一次。**仍缺的**:「结论对不对」只能人抽查;「套进 org/work graph 来跑」评估后用户选了不做(作用域不匹配,见文内 §5.7) | 49 | [29-dream-design.md](./todo/system-design/29-dream-design.md) | **dream(经验汇流)**— ✅ 已实现。定时整理所有 agent 在所有公司的记忆,提炼后跨公司沉淀(不泄漏公司信息)。四条取舍已拍板并实现:去敏独立判定、四落点分层写入(经验/`SKILL.md`/`soul.md`/`agent.md`,受管区块+三道闸门)、每日定时(串行+空转保护)、引用真伪自动查、REST+控制台入口(`/api/dream/*`、`/dream`)。`innovator-evolver` 真 CLI 跑通一次。**仍缺的**:「结论对不对」只能人抽查;「套进 org/work graph 来跑」评估后用户选了不做(作用域不匹配,见文内 §5.7) |
50 | [30-task-delegation-reliability-gaps.md](./todo/system-design/30-task-delegation-reliability-gaps.md) | **任务委派可靠性现状**— 📝 调研记录,非设计方案。DevLoop `runUnit`、Company 招聘装配、Graph 执行三条链路核对结论一致:都是「失败预算耗尽 → 停等人工」,没有「卡住了主动通知人」这一层;DevLoop 当前还默认走 mock,真实 CLI 成功率无实证数据 | 50 | [30-task-delegation-reliability-gaps.md](./todo/system-design/30-task-delegation-reliability-gaps.md) | **任务委派可靠性现状**— 📝 调研记录,非设计方案。DevLoop `runUnit`、Company 招聘装配、Graph 执行三条链路核对结论一致:都是「失败预算耗尽 → 停等人工」,没有「卡住了主动通知人」这一层;DevLoop 当前还默认走 mock,真实 CLI 成功率无实证数据 |
  51 +| [31-work-graph-fail-fallback-coverage.md](./todo/system-design/31-work-graph-fail-fallback-coverage.md) | work graph 兜底边覆盖不全 — ✅ 已完成(除入口节点外强制兜底边,`planner.json`/`executor-fanout.json` 补 6 条边),978/978 单测通过;2026-08-21 又用真实 LLM 起了一条新任务做端到端回归验证,`org-planner-sdd` 自判失败后自动打回 `org-planner-prd`(3 次,零人工介入),修复确认生效 |
  52 +| [33-needs-gate-role-filter-bug.md](./todo/system-design/33-needs-gate-role-filter-bug.md) | 🔴 **待修**。`inboundContracts`/`requiredInputsMissing` 不按 `role` 过滤,把同一条边上的 `produces` 声明也当成下游节点的必需输入——发现于上面 31 号文档的真实回归验证过程中,`org-coder-validator` 被 `missing_required_input` 反复误判 blocked,与兜底边问题无关,是独立新 bug |
51 53
52 ## todo/future/(暂缓,非当前重心) 54 ## todo/future/(暂缓,非当前重心)
53 55
docs/todo/system-design/31-work-graph-fail-fallback-coverage.md
@@ -7,6 +7,15 @@ @@ -7,6 +7,15 @@
7 > 改成用真实引擎代码 + 真实 DB + 只换掉最外层 Claude CLI 子进程的确定性回归测试代替—— 7 > 改成用真实引擎代码 + 真实 DB + 只换掉最外层 Claude CLI 子进程的确定性回归测试代替——
8 > 比一次性真跑更严格(可重复验证),且零真实 LLM 花费。 8 > 比一次性真跑更严格(可重复验证),且零真实 LLM 花费。
9 > **触发**: `e2e-demo` 公司真实任务 `T-mt1qx0c4-95bf`(goal `G-mt1qwgt5-6b77`)卡住排障,见 §一 9 > **触发**: `e2e-demo` 公司真实任务 `T-mt1qx0c4-95bf`(goal `G-mt1qwgt5-6b77`)卡住排障,见 §一
  10 +>
  11 +> **2026-08-21 追加:真实 E2E 回归验证补上了**。§六「尚未做」的那一步(真跑证明「下次系统自己会
  12 +> 补这条边」)当时因为原任务状态不干净作罢,改用确定性测试代替(§7.3)。同一天晚些时候重启服务
  13 +> 加载了本次修复后,起了一条新任务(goal `G-mt2r62ia-73af` / task `T-mt2r6fz0-125b` / root run
  14 +> `run_0e9074815130`)走真实 LLM:`org-planner-sdd` 真实自判失败,沿新补的兜底边**自动**回退 3 次
  15 +> (零 `retryNode` 调用),最终 planner 子图收敛到 `completed`。**修复确认生效,有真实证据**。
  16 +> 同一次验证顺带在 `org-coder` 子图撞见一个**独立的新 bug**(`inboundContracts` 未按 `role`
  17 +> 过滤,把 `produces` 声明误当 `needs`,导致 `missing_required_input` 假阳性),与本文档无关,
  18 +> 已记录在 [33-needs-gate-role-filter-bug.md](./33-needs-gate-role-filter-bug.md)。
10 19
11 --- 20 ---
12 21
docs/todo/system-design/33-needs-gate-role-filter-bug.md 0 → 100644
  1 +# 33-needs-gate-role-filter-bug. `requiredInputsMissing` 未按 role 过滤 —— produces 声明被误判成 needs
  2 +
  3 +> **日期**: 2026-08-21
  4 +> **状态**: 🔴 **待修**(发现于 31 号文档修复的真实 E2E 回归验证过程中,是一个独立的新 bug,
  5 +> 与 31 号文档的兜底边问题无关)
  6 +> **触发**: `e2e-demo` 公司真实回归任务 `T-mt2r6fz0-125b`(goal `G-mt2r62ia-73af`,
  7 +> root run `run_0e9074815130`)——为验证 [31 号文档](./31-work-graph-fail-fallback-coverage.md)
  8 +> 的兜底边修复而起的新任务,链路走到 `org-coder-validator` 时意外反复 blocked
  9 +
  10 +---
  11 +
  12 +## 一、先说 31 号文档的验证结果:修复生效,有真实证据
  13 +
  14 +这条任务本身是为了拿到 31 号文档 §六「尚未做」的那一步真实验收证据(当时因为原任务
  15 +`T-mt1qx0c4-95bf` 状态不干净,改用确定性回归测试代替,见 31 号文档 §7.3)。这次补上了:
  16 +
  17 +- 新起的 `org-planner` 子图(`run_38b3df8fd228`)里,`org-planner-sdd` 真实自判失败
  18 + (下游信息不足),沿新补的兜底边(`org-planner-sdd --fail--> org-planner-prd`,
  19 + edge `wge_ce0f7f05f6dc`)**自动回退了 3 次**(`graph_run_loop_records`:
  20 + `verdict=registered`,`loop_count` 1→2→3),全程**零次 `retryNode` 调用**,最终
  21 + brd→prd→sdd→benchmark→validator 整条链路收敛到 `run_status=completed`。
  22 +- 31 号文档的决策 A(除入口节点外强制兜底边)在真实 LLM 驱动下按设计工作,**结论确认**。
  23 +
  24 +---
  25 +
  26 +## 二、新发现:`org-coder-validator` 被 `missing_required_input` 反复误判
  27 +
  28 +### 2.1 现象
  29 +
  30 +同一条任务往下走到 `org-coder`(`run_21fd814c2f56`,用的是 `executor.json` 骨架)时,
  31 +`org-coder-validator` 连续两轮都以 `fail_reason=missing_required_input` 收场:
  32 +
  33 +- 第一轮(attempt 1,10:01-10:11):沿兜底边 `validator --fail--> impl` 回退两次
  34 + (`wge_1bdb181b14ef` 一次、`wge_aa5bad61fe9a` 一次),第三次撞上 run 树级回退硬顶
  35 + (`backward_count` 全树累计 >5,`LOOP_HARD_CAP_RUN=5`),`force_terminated`,
  36 + 节点 `blocked`。**这一段是硬顶熔断本身在正常工作**,不是这次要报的 bug。
  37 +- 人工 `POST /nodes/:id/retry` 把 `org-coder-validator` 打回 `ready` 后,
  38 + **第二轮**(attempt 2,10:42:48-10:44:01)**又**判了一次 `missing_required_input`,
  39 + 这次连新的 `loop_record` 都没写(回退硬顶已越顶,直接静默 blocked)。
  40 +- 节点自己的 `remediationNote` 写的是「无需整改。……本轮重新独立跑了一遍测试脚本与
  41 + goal-contract,结果与 attempt-1 完全一致,判定 attempt-1 的 passed 结论有效」——
  42 + **agent 自己认为通过了**,是引擎的产出/输入契约闸门(T-mt25ecbd 那次加的机制,
  43 + commit `aecf5c9`)把 success 强行改判成 fail。
  44 +
  45 +### 2.2 复现证据:`graph_files` 里明明有 `artifact` 却仍报缺
  46 +
  47 +查 `T-mt2r6fz0-125b` 的产物索引(`data/graph.sqlite` 的 `graph_files` 表),
  48 +`org-coder-impl`(`rn_e71be3641174`,10:06:51 完成)与 `org-coder-test`
  49 +(`rn_cd3e3a1c51e2`,10:08:52 完成)都成功登记了当前(`superseded_by IS NULL`)
  50 +`kind='artifact'` 的产物行(`_plan.md` / `_research.md` / `coder.log`)。
  51 +`org-coder-validator` 的 attempt 2 在 10:42:48 才跑,**比这些产物晚了半小时以上**,
  52 +不是时序竞争,是稳定复现。
  53 +
  54 +### 2.3 根因:`inboundContracts` 不按 `role` 过滤,把 `produces` 声明也当成 `needs`
  55 +
  56 +`executor.json` 里 `test → validator` 这条边同时声明了两条契约(`work-graph-db.js`
  57 +`declareEdgeContract`):
  58 +
  59 +```json
  60 +{ "from": "{{orgNode}}-test", "to": "{{orgNode}}-validator", "condition": "success",
  61 + "produces": ["log"], "needs": ["artifact"] }
  62 +```
  63 +
  64 +即:`kind=log, role=produces`(test 自己产出的是 log)+ `kind=artifact, role=needs`
  65 +(validator 要吃的是 artifact)。这两条都落进同一张 `graph_edge_contracts` 表,
  66 +`required=1`。
  67 +
  68 +问题出在 `work-graph-db.js:2542` 的 `inboundContracts()`:
  69 +
  70 +```js
  71 +function inboundContracts(edgeLayer, edges, defNodeId) {
  72 + const inbound = (edges || []).filter(
  73 + (e) => String(e.to) === String(defNodeId) && (e.edgeKind || 'forward') !== 'backward'
  74 + );
  75 + const out = [];
  76 + for (const e of inbound) {
  77 + for (const c of edgeContracts(edgeLayer, e.id)) out.push({ ...c, fromNodeId: e.from });
  78 + }
  79 + return out;
  80 +}
  81 +```
  82 +
  83 +它把入边上**所有**契约行原样吐出来,不管 `role` 是 `produces` 还是 `needs`。
  84 +下游 `graph-engine.js:3945` 的 `requiredInputsMissing()` 接着只按 `required` 过滤:
  85 +
  86 +```js
  87 +const requiredKinds = [...new Set(contracts.filter((c) => c.required).map((c) => c.kind))];
  88 +```
  89 +
  90 +于是 `validator` 的「必须在索引里找到实例」的 kind 集合变成了 `{log, artifact}`,
  91 +而不是本该只有的 `{artifact}`——`log` 是 test 声明**自己产出**的 kind(描述 test 的
  92 +输出形态),不是 validator 要读的输入。这个 task 的 `graph_files` 里从来没有任何
  93 +`kind='log'` 的行(`org-coder-test` 实际写的是 `coder.log` 但被索引成
  94 +`kind='artifact'`,不是 `kind='log'`),于是 `requiredKinds - haveKinds = ['log']`,
  95 +`missing.length > 0`,success 被强改成 fail。
  96 +
  97 +**对照**:同一份代码里至少三处其他函数在读同一张 `graph_edge_contracts` 表时都记得
  98 +按 `role` 过滤(`(c.role || 'produces') !== 'needs'` 或 `=== 'produces'`,
  99 +见 `graph-engine.js:968` / `:3811` / `:3866`),唯独 `inboundContracts` /
  100 +`requiredInputsMissing` 这一条链路漏了。判据不一致,不是设计如此。
  101 +
  102 +### 2.4 影响面推测(未逐一核实,留给修的时候一并查)
  103 +
  104 +- 任何在同一条边上**同时声明了 `produces` 和 `needs`** 的节点,只要 `produces`
  105 + 的那个 kind 从未被单独索引过(大多数「描述性」kind,比如这里的 `log`,本来就不
  106 + 一定会被单独记录成同名 kind 的产物行),下游节点的 success 就会被这条误判的
  107 + `missing_required_input` 拦下来,且**每次重试都会复现**(不是一次性的)。
  108 +- `executor.json` / `executor-fanout.json` / `validator.json` 里凡是 `test→validator`
  109 + 这种「同边混写 produces+needs」的写法都要复查一遍(§一 T3/T4 审计脚本可以扩展
  110 + 一个「edge 同时有 produces 与 needs 时,produces 的 kind 是否真的会被索引」的检查项)。
  111 +
  112 +### 2.5 建议修法(留给实施时定案,这里只记方向)
  113 +
  114 +`inboundContracts()` 应该只返回 `role='needs'`(含历史 NULL 按 `produces` 处理的
  115 +存量行需要保留原有兼容语义,但 NULL 不该被当成 `needs`)的契约行,或者
  116 +`requiredInputsMissing()` 在 `contracts.filter(...)` 那一步同时按
  117 +`(c.role || 'produces') === 'needs'` 过滤——两者选一,不建议两处都改(避免出现
  118 +「谁的语义才是权威」的第二份判据)。改完要跑一遍 §一 提到的真实回归任务,确认
  119 +`org-coder-validator` 不再假阳性 blocked。
  120 +
  121 +---
  122 +
  123 +## 三、下一步
  124 +
  125 +- [ ] 定案 §2.5 的修法,改 `inboundContracts` 或 `requiredInputsMissing` 之一
  126 +- [ ] 新增单测:`test→validator` 同边声明 `produces:log` + `needs:artifact`,
  127 + 只有 `artifact` 有索引实例、`log` 没有,validator 的 success 判定不应被拦
  128 +- [ ] 跑一遍 `graph-real-chain-smoke.test.js` 同款「真实引擎代码 + 真实 DB + 换掉最外层
  129 + CLI 子进程」的确定性回归,复现本次撞见的具体场景
  130 +- [ ] 复查 `system/work-graph-templates/*.json` 里其他「同边混写 produces+needs」的边