Commit ab4631c2516e2f2a62bd57ae191867ee2905d4eb
1 parent
9cab41fd
fix(work-graph): executor 模板的 produces:log 声明永不可满足 —— 改成 required:false(33号)
org-coder-validator 在真实回归任务里被 missing_required_input 反复误判 blocked:
executor.json/executor-fanout.json 的 test/integration→validator 边声明
produces:["log"](required 默认 true),但 test 类节点从不写出能被 indexNodeOutputs
认作 kind=log 的文件(coder.log 实际属于 impl,test 自己的 _plan.md/_research.md
认不出 kind 也没有一对一无歧义可猜),这个 kind 因此永远不会被索引到,
requiredInputsMissing 每次都判缺失——不是时序问题,是结构性、每次重试都复现的硬伤。
改成 { kind: 'log', required: false },对齐 validator.json 已有的写法(它的
needs:artifact 早就标了 required:false)。真正必需的 needs:artifact 不变,
仍受硬闸门保护。
中途走了一段弯路:最初以为该改 inboundContracts() 按 role 过滤只留 needs,
改完单测炸了 9 个用例——大多数边只声明 produces(无配对 needs),这条链路
正是 T-mt25ecbd 产出闸门依赖的数据源,按 role 过滤会静默削弱那次修复。
已撤回(work-graph-db.js 这次只加注释,没有逻辑改动),细节记在 33 号文档。
新增回归用例 graph-files-index.test.js ⑪:produces required:false 且该 kind
从未被索引到时不拦下游成功,同边 needs:artifact(required:true) 依旧受保护。
全量单测 998/998 通过。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
6 changed files
with
151 additions
and
71 deletions
docs/README.md
| @@ -49,7 +49,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | @@ -49,7 +49,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | ||
| 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 次,零人工介入),修复确认生效 | | 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 | | 52 | +| [33-needs-gate-role-filter-bug.md](./todo/system-design/33-needs-gate-role-filter-bug.md) | ✅ **已修复**。`executor.json`/`executor-fanout.json` 里 `produces:["log"]` 声明了一个 test/integration 类节点永远不会被索引到的 kind,导致 `org-coder-validator` 被 `missing_required_input` 反复误判 blocked;改成 `required:false`(对齐 `validator.json` 既有写法)。文档记录了一条**走过的弯路**(按 role 过滤 `inboundContracts` 会削弱 T-mt25ecbd 的产出闸门,已撤回),发现于 31 号文档的真实回归验证过程中,与兜底边问题无关 | |
| 53 | 53 | ||
| 54 | ## todo/future/(暂缓,非当前重心) | 54 | ## todo/future/(暂缓,非当前重心) |
| 55 | 55 |
docs/todo/system-design/33-needs-gate-role-filter-bug.md
| 1 | -# 33-needs-gate-role-filter-bug. `requiredInputsMissing` 未按 role 过滤 —— produces 声明被误判成 needs | 1 | +# 33-needs-gate-role-filter-bug. `produces:["log"]` 声明了一个从不会被索引到的 kind —— validator 被 missing_required_input 误判 |
| 2 | 2 | ||
| 3 | -> **日期**: 2026-08-21 | ||
| 4 | -> **状态**: 🔴 **待修**(发现于 31 号文档修复的真实 E2E 回归验证过程中,是一个独立的新 bug, | 3 | +> **日期**: 2026-08-21 发现,2026-08-22 修复 |
| 4 | +> **状态**: ✅ **已修复**(发现于 31 号文档修复的真实 E2E 回归验证过程中,是一个独立的新 bug, | ||
| 5 | > 与 31 号文档的兜底边问题无关) | 5 | > 与 31 号文档的兜底边问题无关) |
| 6 | > **触发**: `e2e-demo` 公司真实回归任务 `T-mt2r6fz0-125b`(goal `G-mt2r62ia-73af`, | 6 | > **触发**: `e2e-demo` 公司真实回归任务 `T-mt2r6fz0-125b`(goal `G-mt2r62ia-73af`, |
| 7 | > root run `run_0e9074815130`)——为验证 [31 号文档](./31-work-graph-fail-fallback-coverage.md) | 7 | > root run `run_0e9074815130`)——为验证 [31 号文档](./31-work-graph-fail-fallback-coverage.md) |
| 8 | > 的兜底边修复而起的新任务,链路走到 `org-coder-validator` 时意外反复 blocked | 8 | > 的兜底边修复而起的新任务,链路走到 `org-coder-validator` 时意外反复 blocked |
| 9 | +> | ||
| 10 | +> **2026-08-22 修订**:§2.3 最初写的根因假设(`inboundContracts` 该按 `role` 过滤只留 | ||
| 11 | +> `needs`)**是错的**,动手改了之后跑单测直接炸了 9 个用例——`impl→test`、`brd→prd` | ||
| 12 | +> 这类只声明 `produces`(没有配对 `needs`)的边占绝大多数,`inboundContracts` 把它们 | ||
| 13 | +> 也算进「下游需要的输入」正是**设计如此**,还是 T-mt25ecbd(commit `aecf5c9`)那次 | ||
| 14 | +> 产出闸门的直接依赖。已把那次改动撤回,真正的根因和修法见 §2.3(修订版)与 §四。 | ||
| 9 | 15 | ||
| 10 | --- | 16 | --- |
| 11 | 17 | ||
| @@ -51,7 +57,7 @@ | @@ -51,7 +57,7 @@ | ||
| 51 | `org-coder-validator` 的 attempt 2 在 10:42:48 才跑,**比这些产物晚了半小时以上**, | 57 | `org-coder-validator` 的 attempt 2 在 10:42:48 才跑,**比这些产物晚了半小时以上**, |
| 52 | 不是时序竞争,是稳定复现。 | 58 | 不是时序竞争,是稳定复现。 |
| 53 | 59 | ||
| 54 | -### 2.3 根因:`inboundContracts` 不按 `role` 过滤,把 `produces` 声明也当成 `needs` | 60 | +### 2.3 根因(修订版):`produces:["log"]` 声明的是一个永远认不出来的 kind |
| 55 | 61 | ||
| 56 | `executor.json` 里 `test → validator` 这条边同时声明了两条契约(`work-graph-db.js` | 62 | `executor.json` 里 `test → validator` 这条边同时声明了两条契约(`work-graph-db.js` |
| 57 | `declareEdgeContract`): | 63 | `declareEdgeContract`): |
| @@ -61,70 +67,87 @@ | @@ -61,70 +67,87 @@ | ||
| 61 | "produces": ["log"], "needs": ["artifact"] } | 67 | "produces": ["log"], "needs": ["artifact"] } |
| 62 | ``` | 68 | ``` |
| 63 | 69 | ||
| 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 | -} | 70 | +即:`kind=log, role=produces`(test 自己产出的是 log,required 默认 true)+ |
| 71 | +`kind=artifact, role=needs`(validator 真正要吃的是 artifact,中继声明,产出方是 | ||
| 72 | +更早的 impl,非相邻依赖,R-19.3 ②)。`executor.json` 头部 `dataContractNotes` | ||
| 73 | +写明了设计意图——「validator 既要看测试记录(紧邻上游 produces log)也要看被验的 | ||
| 74 | +交付物本身」,即:`produces` 与 `needs` **都该算进 validator 的输入需求**,这不是 | ||
| 75 | +误用,`inboundContracts()` 把两者都吐出来是**故意的、正确的**(详见下方「走过的 | ||
| 76 | +弯路」)。 | ||
| 77 | + | ||
| 78 | +真正的根因在**产出侧**:`test` 类节点(`org-coder-test`、`org-coder-test-a/b`、 | ||
| 79 | +`org-coder-integration`)从来不会写出一个能被认作 `kind=log` 的文件。 | ||
| 80 | +`indexNodeOutputs()`(`graph-engine.js:3835`)给文件定 kind 的规则是「文件名匹配 | ||
| 81 | +声明的 kind → 已知文件名 → 唯一无歧义兜底」三级,而这几类节点实际落盘的是 | ||
| 82 | +`_plan.md` / `_research.md` / `coder.log` 这类文件——`coder.log` 恰好属于 **impl** | ||
| 83 | +节点(不是 test),test 自己写的两份文件文件名里都没有 "log",且同时有两个认不出 | ||
| 84 | +kind 的文件,触发不了「一对一无歧义」的兜底认领。于是 `graph_files` 里对这个 | ||
| 85 | +task **永远**不会出现一行 `kind='log'`——`unsatisfiedRequiredProduces()`(出边产出 | ||
| 86 | +闸门,`graph-engine.js:3801`)本身对此是宽容的:它只检查节点私有目录里有没有 | ||
| 87 | +**任何**候选文件,不检查文件是否精确匹配声明的 kind,所以 `test` 自己能顺利 | ||
| 88 | +`success`;但 `requiredInputsMissing()`(`graph-engine.js:3945`)在 `validator` | ||
| 89 | +结算时是严格的:`declared kind - 索引里查到的 kind` 非空就判 `missing_required_input`, | ||
| 90 | +而 `log` 这个 kind 因为上面那条产出侧的缺口从未出现在索引里——**这不是一次性的 | ||
| 91 | +时序问题,是这条边只要还这么声明就永远无法满足的硬伤,每次重试都复现**。 | ||
| 92 | + | ||
| 93 | +对照 `validator.json`(同一批模板):它的 `needs` 声明本来就写成 | ||
| 94 | +`{ "kind": "artifact", "required": false }`——同一份代码库里已经有先例, | ||
| 95 | +知道这类「声明了但未必查得到实例」的依赖要标 `required:false`,只是 | ||
| 96 | +`executor.json` / `executor-fanout.json` 里的 `produces:["log"]` 用的是裸数组 | ||
| 97 | +写法(`normalizeContracts` 对裸字符串项默认 `required:true`),漏了这一条。 | ||
| 98 | + | ||
| 99 | +### 2.4 走过的弯路:为什么「按 role 过滤 inboundContracts」是错的 | ||
| 100 | + | ||
| 101 | +最初的假设是「`inboundContracts()` 不该把 `produces` 也当成 `needs`,只留 | ||
| 102 | +`role==='needs'` 的行」。改完发现 `work-graph-skeletons.test.js` / | ||
| 103 | +`org-layer-contracts.test.js` 等 9 个用例炸了:`org-planner-sdd`(入边 | ||
| 104 | +`prd→sdd: produces:["prd"]`,无配对 `needs`)、`org-coder-test`(入边 | ||
| 105 | +`impl→test: produces:["artifact"]`,无配对 `needs`)等一大批节点的入边**只声明 | ||
| 106 | +了 `produces`,从不显式写 `needs`**——这本来就是绝大多数模板边的写法(`declareEdgeContract` | ||
| 107 | +注释原话:「出边声明产出是压倒性多数,needs 必须显式写」)。按 role 过滤会让这些 | ||
| 108 | +节点的 `inboundContracts()` 变成空数组,`requiredInputsMissing()` 也就不再校验 | ||
| 109 | +它们——而这条链路正是 T-mt25ecbd(commit `aecf5c9`)刚加固的产出闸门本身 | ||
| 110 | +依赖的数据源(`org-planner-benchmark` 靠它校验 `needs:[brd,prd,sdd]` 真的都被 | ||
| 111 | +索引到)。按 role 过滤表面上修好了 `log` 这一个 kind,实际上会**静默削弱** | ||
| 112 | +T-mt25ecbd 那次修复要守住的保护——好在跑单测立刻炸了,没有带着这个错误方向走 | ||
| 113 | +下去。**结论**:`inboundContracts()` 本身没有 bug,别再按这个方向改它。 | ||
| 114 | + | ||
| 115 | +### 2.5 实际修法 | ||
| 116 | + | ||
| 117 | +在**声明**这一侧把 `log` 标成 `required:false`,与 `validator.json` 的既有写法 | ||
| 118 | +对齐——既保留「validator 能看到 test 的 log(如果将来真的被索引到)」这条文档 | ||
| 119 | +价值,又不再把一个永远查不到实例的 kind 当硬闸门: | ||
| 120 | + | ||
| 121 | +```diff | ||
| 122 | +- { "from": "{{orgNode}}-test", "to": "{{orgNode}}-validator", "condition": "success", | ||
| 123 | +- "produces": ["log"], "needs": ["artifact"] }, | ||
| 124 | ++ { "from": "{{orgNode}}-test", "to": "{{orgNode}}-validator", "condition": "success", | ||
| 125 | ++ "produces": [{ "kind": "log", "required": false }], "needs": ["artifact"] }, | ||
| 81 | ``` | 126 | ``` |
| 82 | 127 | ||
| 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。 | 128 | +改了 4 处(`executor.json` 1 处,`executor-fanout.json` 3 处:`test-a→merge`、 |
| 129 | +`test-b→merge`、`integration→validator`——同一种「produces:log 但 test/integration | ||
| 130 | +类节点从不产出可识别的 log 文件」的形状)。真正必需的 `needs:artifact` | ||
| 131 | +(`required:true`)保持不变,继续受硬闸门保护。 | ||
| 120 | 132 | ||
| 121 | --- | 133 | --- |
| 122 | 134 | ||
| 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」的边 | 135 | +## 三、已完成 |
| 136 | + | ||
| 137 | +- [x] 撤回 §2.4 那次错误方向的引擎改动(`work-graph-db.js` 的 `inboundContracts` | ||
| 138 | + 恢复原状,加注释记录这条弯路,防止以后有人重新踩一遍) | ||
| 139 | +- [x] 按 §2.5 改 `executor.json` / `executor-fanout.json` 的 4 处 `produces:["log"]` | ||
| 140 | + 声明为 `required:false` | ||
| 141 | +- [x] 新增单测 `graph-files-index.test.js` ⑪:`produces` 声明 `required:false` | ||
| 142 | + 且该 kind 从未被索引到时,不拦下游节点 success;同一条边上 `required:true` | ||
| 143 | + 的 `needs:artifact` 依旧受硬闸门保护(未被这次改动削弱) | ||
| 144 | +- [x] 全量单测 `npx vitest run server/tests/unit`:**998/998 通过**(含新增用例, | ||
| 145 | + 零回归) | ||
| 146 | + | ||
| 147 | +## 四、留待下次 | ||
| 148 | + | ||
| 149 | +- [ ] 用真实回归任务(同 31 号文档 §一的路数,起一条新 e2e-demo 任务)验证 | ||
| 150 | + `org-coder-validator` 不再假阳性 blocked——这次改动只有单测覆盖,还没有 | ||
| 151 | + 真实 LLM 跑过的证据 | ||
| 152 | +- [ ] `validator.json` 的两条 `needs:[{kind:'artifact',required:false}]` 是否也该 | ||
| 153 | + 核对一遍「这个 kind 到底会不会被索引到」,避免同一类问题在别的模板里潜伏 |
server/services/work-graph-db.js
| @@ -2538,6 +2538,17 @@ function edgeContracts(edgeLayer, edgeId) { | @@ -2538,6 +2538,17 @@ function edgeContracts(edgeLayer, edgeId) { | ||
| 2538 | /** | 2538 | /** |
| 2539 | * 某个节点的**入边**上声明了哪些 kind —— 这是「这个节点需要什么输入」的答案。 | 2539 | * 某个节点的**入边**上声明了哪些 kind —— 这是「这个节点需要什么输入」的答案。 |
| 2540 | * 只看 forward 入边:backward 入边是回退路径,不是数据来源。 | 2540 | * 只看 forward 入边:backward 入边是回退路径,不是数据来源。 |
| 2541 | + * | ||
| 2542 | + * **produces 与 needs 都收**——两者对下游而言都是「要吃的输入」,只是来源不同: | ||
| 2543 | + * produces 是紧邻上游自己的产出(R-19.3 ①「沿边传递」,如 `impl→test: produces:[artifact]`), | ||
| 2544 | + * needs 是中继声明的非相邻依赖(R-19.3 ②,如 `test→validator: needs:[artifact]`, | ||
| 2545 | + * 实际产出方是更早的 impl)。33 号文档最初以为这里该按 role 过滤只留 needs, | ||
| 2546 | + * 但那会把单纯声明 produces(没有配对 needs)的绝大多数边(`impl→test`、`brd→prd`……) | ||
| 2547 | + * 的下游需求判空,且会削弱 T-mt25ecbd 那次刚加固的产出闸门(`org-planner-sdd`/ | ||
| 2548 | + * `org-planner-benchmark` 的 `produces:[sdd]`/`produces:[acceptance]` 就是靠这条链路 | ||
| 2549 | + * 被 requiredInputsMissing 强制校验的)——按 role 过滤是错的,真正的根因和修法在 | ||
| 2550 | + * `system/work-graph-templates/*.json` 那几条 `produces:["log"]` 声明本身 | ||
| 2551 | + * (见 33 号文档修订版)。 | ||
| 2541 | */ | 2552 | */ |
| 2542 | function inboundContracts(edgeLayer, edges, defNodeId) { | 2553 | function inboundContracts(edgeLayer, edges, defNodeId) { |
| 2543 | const inbound = (edges || []).filter( | 2554 | const inbound = (edges || []).filter( |
server/tests/unit/graph-files-index.test.js
| @@ -542,4 +542,50 @@ describe('R-19 产物索引:graph_files / graph_edge_contracts', () => { | @@ -542,4 +542,50 @@ describe('R-19 产物索引:graph_files / graph_edge_contracts', () => { | ||
| 542 | const rows = allFiles(companyId); | 542 | const rows = allFiles(companyId); |
| 543 | expect(rows.map((r) => path.basename(r.rel_path))).toEqual(['brd.md']); | 543 | expect(rows.map((r) => path.basename(r.rel_path))).toEqual(['brd.md']); |
| 544 | }); | 544 | }); |
| 545 | + | ||
| 546 | + // ── ⑪ 33 号文档:produces 声明 required:false 时不拦下游 ───────── | ||
| 547 | + // | ||
| 548 | + // 复现 executor.json 里 test→validator 那条边的真实形状:test 出边同时声明 | ||
| 549 | + // produces:[{kind:'log', required:false}](自己产出的记录,写作文档但不强制) | ||
| 550 | + // 与 needs:[{kind:'artifact', required:true}](中继声明,产出方是更早的 impl)。 | ||
| 551 | + // test 实际写的文件(_plan.md/_research.md 之类)从来认不出 kind=log —— | ||
| 552 | + // 这在真实回归任务 T-mt2r6fz0-125b 里复现过:validator 被 missing_required_input | ||
| 553 | + // 反复误判 blocked,而它自己判定 passed。required:false 让这条声明只留文档价值, | ||
| 554 | + // 不再是硬闸门;真正必需的 needs:artifact 依旧受硬闸门保护(不因这次修复被削弱)。 | ||
| 555 | + | ||
| 556 | + it('⑪ produces 声明 required:false 时,即使这个 kind 从未被索引到,也不拦下游成功', async () => { | ||
| 557 | + const companyId = 'co-files-optional-produces'; | ||
| 558 | + const taskId = 'task-optional-produces'; | ||
| 559 | + const b = buildRun({ | ||
| 560 | + companyId, taskId, | ||
| 561 | + nodes: [{ nodeName: 'w-impl' }, { nodeName: 'w-test' }, { nodeName: 'w-validator' }], | ||
| 562 | + edges: [ | ||
| 563 | + { from: 'w-impl', to: 'w-test', condition: 'success' }, | ||
| 564 | + { from: 'w-test', to: 'w-validator', condition: 'success' }, | ||
| 565 | + ], | ||
| 566 | + }); | ||
| 567 | + wdb.declareEdgeContract({ | ||
| 568 | + edgeLayer: 'work', edgeId: b.edgeId('w-impl', 'w-test'), kind: 'artifact', role: 'produces', | ||
| 569 | + }); | ||
| 570 | + const e2 = b.edgeId('w-test', 'w-validator'); | ||
| 571 | + // 与 executor.json 修复后的形状逐字对应 | ||
| 572 | + wdb.declareEdgeContract({ edgeLayer: 'work', edgeId: e2, kind: 'log', role: 'produces', required: false }); | ||
| 573 | + wdb.declareEdgeContract({ edgeLayer: 'work', edgeId: e2, kind: 'artifact', role: 'needs', required: true }); | ||
| 574 | + | ||
| 575 | + await advance(b.runId, 3, { | ||
| 576 | + writes: { | ||
| 577 | + 'w-impl': { 'impl.md': '# 实现产物' }, | ||
| 578 | + // test 写的文件跟 "log" 毫无关联、彼此也无歧义可猜(同 ④b 的口径), | ||
| 579 | + // 这个 kind 永远不会被索引到——正是修复要覆盖的那种情形 | ||
| 580 | + 'w-test': { '_plan.md': '计划', '_research.md': '调研' }, | ||
| 581 | + }, | ||
| 582 | + }); | ||
| 583 | + | ||
| 584 | + const state = wdb.getRunState(b.runId); | ||
| 585 | + const validator = state.nodes.find((n) => n.name === 'w-validator'); | ||
| 586 | + // 关键断言:log 从未被索引,但它是 required:false,不该拦下游 | ||
| 587 | + expect(allFiles(companyId).some((f) => f.kind === 'log')).toBe(false); | ||
| 588 | + expect(validator.status).toBe('success'); | ||
| 589 | + expect(validator.failReason).toBeFalsy(); | ||
| 590 | + }); | ||
| 545 | }); | 591 | }); |
system/work-graph-templates/executor-fanout.json
| @@ -19,10 +19,10 @@ | @@ -19,10 +19,10 @@ | ||
| 19 | { "from": "{{orgNode}}-split", "to": "{{orgNode}}-impl-b", "condition": "success", "produces": ["contract"] }, | 19 | { "from": "{{orgNode}}-split", "to": "{{orgNode}}-impl-b", "condition": "success", "produces": ["contract"] }, |
| 20 | { "from": "{{orgNode}}-impl-a", "to": "{{orgNode}}-test-a", "condition": "success", "produces": ["artifact"] }, | 20 | { "from": "{{orgNode}}-impl-a", "to": "{{orgNode}}-test-a", "condition": "success", "produces": ["artifact"] }, |
| 21 | { "from": "{{orgNode}}-impl-b", "to": "{{orgNode}}-test-b", "condition": "success", "produces": ["artifact"] }, | 21 | { "from": "{{orgNode}}-impl-b", "to": "{{orgNode}}-test-b", "condition": "success", "produces": ["artifact"] }, |
| 22 | - { "from": "{{orgNode}}-test-a", "to": "{{orgNode}}-merge", "condition": "success", "produces": ["log"], "needs": ["artifact"] }, | ||
| 23 | - { "from": "{{orgNode}}-test-b", "to": "{{orgNode}}-merge", "condition": "success", "produces": ["log"], "needs": ["artifact"] }, | 22 | + { "from": "{{orgNode}}-test-a", "to": "{{orgNode}}-merge", "condition": "success", "produces": [{ "kind": "log", "required": false }], "needs": ["artifact"] }, |
| 23 | + { "from": "{{orgNode}}-test-b", "to": "{{orgNode}}-merge", "condition": "success", "produces": [{ "kind": "log", "required": false }], "needs": ["artifact"] }, | ||
| 24 | { "from": "{{orgNode}}-merge", "to": "{{orgNode}}-integration", "condition": "success", "produces": ["artifact"] }, | 24 | { "from": "{{orgNode}}-merge", "to": "{{orgNode}}-integration", "condition": "success", "produces": ["artifact"] }, |
| 25 | - { "from": "{{orgNode}}-integration", "to": "{{orgNode}}-validator", "condition": "success", "produces": ["log"], "needs": ["artifact"] }, | 25 | + { "from": "{{orgNode}}-integration", "to": "{{orgNode}}-validator", "condition": "success", "produces": [{ "kind": "log", "required": false }], "needs": ["artifact"] }, |
| 26 | { "from": "{{orgNode}}-test-a", "to": "{{orgNode}}-impl-a", "condition": "fail", "edgeKind": "backward" }, | 26 | { "from": "{{orgNode}}-test-a", "to": "{{orgNode}}-impl-a", "condition": "fail", "edgeKind": "backward" }, |
| 27 | { "from": "{{orgNode}}-test-b", "to": "{{orgNode}}-impl-b", "condition": "fail", "edgeKind": "backward" }, | 27 | { "from": "{{orgNode}}-test-b", "to": "{{orgNode}}-impl-b", "condition": "fail", "edgeKind": "backward" }, |
| 28 | { "from": "{{orgNode}}-integration", "to": "{{orgNode}}-merge", "condition": "fail", "edgeKind": "backward" }, | 28 | { "from": "{{orgNode}}-integration", "to": "{{orgNode}}-merge", "condition": "fail", "edgeKind": "backward" }, |
system/work-graph-templates/executor.json
| @@ -11,7 +11,7 @@ | @@ -11,7 +11,7 @@ | ||
| 11 | ], | 11 | ], |
| 12 | "edges": [ | 12 | "edges": [ |
| 13 | { "from": "{{orgNode}}-impl", "to": "{{orgNode}}-test", "condition": "success", "produces": ["artifact"] }, | 13 | { "from": "{{orgNode}}-impl", "to": "{{orgNode}}-test", "condition": "success", "produces": ["artifact"] }, |
| 14 | - { "from": "{{orgNode}}-test", "to": "{{orgNode}}-validator", "condition": "success", "produces": ["log"], "needs": ["artifact"] }, | 14 | + { "from": "{{orgNode}}-test", "to": "{{orgNode}}-validator", "condition": "success", "produces": [{ "kind": "log", "required": false }], "needs": ["artifact"] }, |
| 15 | { "from": "{{orgNode}}-test", "to": "{{orgNode}}-impl", "condition": "fail", "edgeKind": "backward" }, | 15 | { "from": "{{orgNode}}-test", "to": "{{orgNode}}-impl", "condition": "fail", "edgeKind": "backward" }, |
| 16 | { "from": "{{orgNode}}-validator", "to": "{{orgNode}}-impl", "condition": "fail", "edgeKind": "backward" } | 16 | { "from": "{{orgNode}}-validator", "to": "{{orgNode}}-impl", "condition": "fail", "edgeKind": "backward" } |
| 17 | ] | 17 | ] |