Commit 54470be3a4d5a8d78a303cf30b42a3ce74c3ee43

Authored by yibo
1 parent dea683eb

test(p3b): 验收 4 进零注入链路;2/8 由真跑验掉,9 的准确状态写清楚

## 验收 4:从「静态成立」变成真的跑过一次

原状态是 🟡「静态成立,未真跑」—— 人读代码把
fixture verdict → FAILING_VERDICTS → readNodeProduct → callAgentNode 判 fail
这条链读通了,但没有任何一次真跑或真链路证据。

现在 graph-real-chain-smoke ⑥ 走完整条:fixture 产出 verdict="attributed"
→ 节点 fail、fail_reason=input_underspecified、沿 fail 边点亮下游、
判词落在**下游节点**行上(判词的载体是「点亮它的那次状态转换」,不是失败节点自己)。
反向验过:verdict 改成 passed 立刻红。

写 fixture 时踩到一条值得记的:fail_reason 是封闭域 ATTRIBUTION_REASONS,
第一版编了个 contract_invalid,被**按设计降级成 null** 并留一条 timeline ——
引擎不认识的归因一律不信。判据没错,是 fixture 错。

## 验收 2 / 8:由 2026-08-20 那次真跑验掉(§六ter)

· 2 —— 19 次真 agent 调用之后 `git status --porcelain` 完全为空,交付物都在项目根内。
  这正是 §六bis「后半被并行 agent 打破」那条的干净环境重验。
· 8 —— 判据取「它引用了只可能来自提示词的内部 id」:triager 判词原文写着
  「org-coder-impl 私有目录 rn_96a8cd696dcc 下只有 _research.md」,
  rn_96a8cd696dcc 是 graph_run_nodes.id,agent 没有别的途径知道它。

## 验收 9:产出端通了,消费端没等到那个形状

真跑里 triager **确实产出了两半地址**(orgNode + workNode)——
这一条本身有分量:27-P3/P4 §八.22 第 13 处记的正是「引擎四环全通,
但产出方 agent.md 里根本没有这个键」,那半条现在通了。

紧接着「忽略跨层地址」那条**不是缺陷,是判据在正常工作**:这次回退发生在
work 层内部,edge.to 本身就是 agent_call 节点,已是最细粒度,没有跨层可言。
缺的是**那个形状**:一次 org 层回退边、且目标是 subgraph 节点。

别把「机制没验」和「机制没有」混为一谈 —— 定点重置的逻辑侧有
graph-cross-layer-address.test.js 三例注入式覆盖,缺的只是真跑里发生过一次。

## 顺带

· template-verdicts 的样本非空那条补 ENOENT 容忍。它扫**活目录**,
  而 graph-real-chain-smoke 会在 system/agents/{aid}/{cid}/ 下建了又清运行态 ——
  文件在「列出名字」和「读它」之间消失是正常的。walk() 与 violations() 早就容忍了,
  只有这一处漏了,实测偶发红过一次。**扫活目录的守卫,每一处读都要容忍 ENOENT。**
· 真跑 timeline 里发现一条未修的:每个节点的产物索引写入都撞
  UNIQUE idx_graph_files_path 失败并被降级成日志(重试时同路径再写一次)。
  后果是 files.json 产物索引在有重试的 run 上静默不全 —— 归 P4 一起处理,记在 §六ter。

全量:139 文件 / 1297 测试全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/README.md
... ... @@ -42,7 +42,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d
42 42 | [27-e2e-task-execution-flow.md](./todo/system-design/27-e2e-task-execution-flow.md) | 端到端流程总览:建公司 → 下达任务 → org graph 逐节点执行 → work graph 规划与循环 → 验收收敛。含与 DevLoop 既有实现的对应关系分析 |
43 43 | [27-example-dev-company-walkthrough.md](./todo/system-design/27-example-dev-company-walkthrough.md) | 参考场景走查(虚构的「配图工厂」),把架构跑一遍具体的数:各阶段 JSON 形态、成本估算、断点清单 |
44 44 | [27-p3-p4-work-graph-console.md](./todo/system-design/27-p3-p4-work-graph-console.md) | P3+P4(原 P4 回退+Loop-Validator 已并入)图执行引擎与执行控制台 — ✅ 已实现(`c3abf4a`)。**仍留 todo 的原因**:§十 末「四条硬规矩」第 2 条那笔欠账**已还大半** —— `graph-real-chain-smoke.test.js`(零注入、只换掉真 CLI 子进程)已进 CI,`agent_call` 与 `subgraph` 两支都覆盖。剩下的是真 LLM(进不了 CI)、多节点 / 回退边 / 熔断的形状、以及按需 hire 那一段 |
45   -| [27-p3b-real-run-runway.md](./todo/system-design/27-p3b-real-run-runway.md) | P3 收尾之真跑跑道(从 mock 走到真 Claude CLI)— 📝 **仍在推进**。验收 7(work 层本体混进 org graph)✅ 已修;缺陷② 的根因(边界守卫误判并行会话)2026-08-20 修掉。**剩下的是 2/4/8/9 —— 都不是「没修」,是「没在干净环境重验」**。⚠️ 这份文档里 ⛔/🟡 的段落多为**当时的现场记录**,读之前先看头部状态行 |
  45 +| [27-p3b-real-run-runway.md](./todo/system-design/27-p3b-real-run-runway.md) | P3 收尾之真跑跑道(从 mock 走到真 Claude CLI)— 📝 **只剩验收 9**。2026-08-20 第四次真跑(§六ter)验掉 2 与 8,验收 4 由零注入链路验掉,7 早已修好。验收 9 的准确状态是「产出端通了、消费端没等到那个形状」—— 需要一次 **org 层回退边指向 subgraph 节点**。⚠️ 这份文档里 ⛔/🟡 的段落多为**当时的现场记录**,读之前先看头部状态行 |
46 46 | [27-goal-contract-format.md](./todo/system-design/27-goal-contract-format.md) | 验收契约格式(`goal-benchmark` / `goal-test-data` / `goal-test-script` 的产物形态与跨节点传递) |
47 47 | [27-p4-parallel-and-artifacts.md](./todo/system-design/27-p4-parallel-and-artifacts.md) | **P4(回收编号)并行执行与产物索引** — 📝 设计中,未实现。fan-out 真并发 + 同 agent 多实例隔离 + `files.json` 产物索引。三个 Phase,C 可插队 |
48 48 | [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(只有口述、无设计)、任务态清理(源码树无限增长)、`source_kind`/`legacy_path` 删列、三项「已实施未验证」。§五 是**已立起来的守卫清单**(别重复造,也别删) |
... ...
docs/todo/system-design/27-p3b-real-run-runway.md
... ... @@ -8,7 +8,8 @@
8 8 > **状态**(2026-08-20 校准): 📝 **仍在推进**。第一批验收 1/3 已验、2 的后半与 4 未在干净环境重验;
9 9 > 第二批验收 7 ✅ **已修**(`58a2865`「W7 零边图」,守卫 `company-hire-org-layer.test.js` 的
10 10 > `CO_ZEROEDGE` + `W7-fix`)—— 下面 §六bis 缺陷③ 那段是**当时的现场记录**,不是当前状态;
11   -> 第三批 8/9 真 CLI 未验。
  11 +> 验收 2 ✅、验收 8 ✅ 由 2026-08-20 的真跑验掉,验收 4 ✅ 由零注入链路验掉,
  12 +> **验收 9 仍开**(产出端已通、消费端没等到那个形状)—— 逐条证据见 §六ter。
12 13 > §六bis 缺陷② 的**根因**(边界守卫误判并行会话的改动)已于 2026-08-20 修掉,
13 14 > 见 [27-P3C §17.2](../../done/system-design/27-p3c-goal-task-model.md) —— 但**根因修了不等于验收点验了**,那几条仍要重跑一遍。
14 15  
... ... @@ -935,3 +936,65 @@ failingVerdict = verdict === null ? raw !== null : !PASSING_VERDICTS.has(verdict
935 936 (「找不到 FAILING_VERDICTS——取值域防线失去依据,不要让它静默空跑」)。
936 937 真去反转的话,把该测试里 `engineSet('FAILING_VERDICTS')` 换成 `engineSet('PASSING_VERDICTS')`
937 938 并把 `VERDICT_DOMAIN` 的组成对应改一行即可;**不要**把断言删掉了事。
  939 +
  940 +---
  941 +
  942 +## 六ter、第四次真跑复验(2026-08-20)
  943 +
  944 +**跑法**:主仓库、真 Claude CLI、工作区干净(`git status` 空)。公司 `e2e-demo`,
  945 +把 2026-08-18 卡住的那条 run(`run_9168bd24f46c`,做 lemonai 官网)解开后逐节点 `/next` 推到收敛。
  946 +19 次真 agent 调用、177,462 tokens,全树 `completed`,goal → `done`。
  947 +过程记在 [27-P3C §17.5](../../done/system-design/27-p3c-goal-task-model.md)。
  948 +
  949 +### 逐条结果
  950 +
  951 +| 验收 | 结果 | 证据 |
  952 +|---|---|---|
  953 +| **2** 产出落在项目根内、宿主仓库 `git status` 无新增/修改 | ✅ **成立** | 19 次真 agent 调用之后 `git status --porcelain` **完全为空**;交付物(`index.html` / `styles.css` / `app.js` / `assets/img/` / `DELIVERY.md`)都在 `data/companies/e2e-demo/projects/p-bkhylu/` 内。这是 §六bis「后半被并行 agent 打破」那一条的干净环境重验 |
  954 +| **3** 越界被守卫拒绝并留痕 | ✅ 早已成立;误判那一半 2026-08-20 修掉 | 见 [27-P3C §17.2](../../done/system-design/27-p3c-goal-task-model.md) |
  955 +| **4** 非法契约 → validator 判失败且引擎按失败处理 | ✅ **成立**,但**不是真 CLI 验的** | `graph-real-chain-smoke.test.js` ⑥(零注入链路,只换掉 CLI 子进程):fixture 产出 `verdict: "attributed"` → 节点 `fail`、`fail_reason=input_underspecified`、沿 fail 边点亮下游、判词落在下游节点行上。反向验过:verdict 改成 `passed` 立刻红 |
  956 +| **7** work 层本体不混进 org graph | ✅ 已修(`58a2865`) | `CO_ZEROEDGE` / `W7-fix` |
  957 +| **8** validator-triager 的提示词里有「哪个节点失败、第几 attempt、失败摘要」 | ✅ **成立(真 CLI)** | 判据是**它引用了只可能来自提示词的内部 id**:triager 的判词原文写着「`org-coder-impl` 私有目录 `rn_96a8cd696dcc` 下只有 `_research.md`」—— `rn_96a8cd696dcc` 是 `graph_run_nodes.id`,agent 没有任何别的途径知道它,只能来自 `describeFailureContext` 注入的那一段 |
  958 +| **9** 一次真实的跨层回退,两半地址齐全、只重置被点名的节点 | 🟡 **产出端通了,消费端没等到那个形状** | 见下 |
  959 +
  960 +### 验收 9 的准确状态
  961 +
  962 +真跑里 triager **确实产出了两半地址**:
  963 +
  964 +```
  965 +node.triage | 归因 verdict=attributed fail_reason=impl_bug → {"orgNode":"org-coder","workNode":"org-coder-impl"}
  966 +```
  967 +
  968 +这一条本身有分量:[27-P3/P4](./27-p3-p4-work-graph-console.md) §八.22 的**第 13 处**记的正是
  969 +「引擎四环全通,但产出方 `validator-triager` 的 agent.md 里根本没有这个键」——
  970 +**那半条现在通了**,真实 agent 真的写出了 `orgNode` + `workNode`。
  971 +
  972 +但紧接着的一条:
  973 +
  974 +```
  975 +node.triage | 归因点名了 org-coder-impl,但回退目标 org-coder-impl 不是 subgraph 节点,忽略跨层地址
  976 +```
  977 +
  978 +**这不是缺陷,是判据在正常工作**:这次回退发生在 **work 层内部**
  979 +(`org-coder-validator` → `org-coder-impl`),`edge.to` 本身就是 `agent_call` 节点,
  980 +已经是最细粒度,没有「跨层」可言 —— `resolveCrossLayerTarget` 第 ② 关就是拦这个的。
  981 +
  982 +所以验收 9 缺的是**那个形状本身**:需要一次 **org 层的回退边、且目标是 subgraph 节点**。
  983 +今天这条 run 的三个 org 节点重试后一次过,没有 org 层失败。
  984 +
  985 +> **别把「机制没验」和「机制没有」混为一谈**:定点重置的逻辑侧有
  986 +> `graph-cross-layer-address.test.js` 三例注入式覆盖(只写 `target_node` / 只写
  987 +> `target_org_node` / 模板键名逐字对齐)。缺的只是「真跑里发生过一次」。
  988 +
  989 +### 顺带发现的一条缺陷(未修)
  990 +
  991 +真跑的 timeline 里,**每个节点的产物索引写入都失败了**,且被降级成一条日志:
  992 +
  993 +```
  994 +node.output | 产物索引写入失败 contract.json: UNIQUE constraint failed: index 'idx_graph_files_path'
  995 +node.output | 产物索引写入失败 sdd.md: UNIQUE constraint failed: index 'idx_graph_files_path'
  996 +```
  997 +
  998 +形态是重试时同一条产物路径被再写一次撞唯一索引。后果不是崩溃 ——
  999 +**是 `files.json` 产物索引在有重试的 run 上静默不全**,而那正是
  1000 +[27-P4](./27-p4-parallel-and-artifacts.md) 要建的东西。归到 P4 一起处理。
... ...
fixtures/devloop-mock/benchmark-validator.json 0 → 100644
  1 +{
  2 + "_doc": "P3 mock fixture · 判失败的验收节点(27-P3B §六 验收 4)。attempt-{attempt}.json 里 verdict=attributed 落在 graph-engine.FAILING_VERDICTS 里 → readNodeProduct 置 failingVerdict → 节点判 fail。这份 fixture 的**全部意义**就是让「validator 说失败 → 引擎真的按失败处理」这条判据在 CI 里跑得出来:在这之前它只是「静态成立」(人读代码读通了链路),没有任何一次真跑或真链路证据。改 verdict 的取值前先看 graph-engine 的 FAILING_VERDICTS。 fail_reason 必须取自 graph-engine.ATTRIBUTION_REASONS 这个封闭域(impl_bug / input_underspecified / skill_defect / infra_flake)—— 自创的词会被**按设计降级成 null** 并留一条 timeline,不是崩溃,但节点行上就没有归因了。第一版这里写了 contract_invalid,实测就是这个下场。",
  3 + "chunks": [
  4 + "[mock] benchmark-validator 读取被测产物与验收契约。\n",
  5 + "契约形状非法:acceptance.json 缺 test_script 段。\n"
  6 + ],
  7 + "result": "mock-fail benchmark-validator:契约非法,判失败。",
  8 + "files": [
  9 + {
  10 + "path": "attempt-{attempt}.json",
  11 + "content": "{\n \"schema\": \"validator-triager/v1\",\n \"verdict\": \"attributed\",\n \"fail_reason\": \"input_underspecified\",\n \"remediation_note\": \"acceptance.json 缺 test_script 段,补齐后重跑\"\n}\n"
  12 + }
  13 + ],
  14 + "usage": {
  15 + "input": 400,
  16 + "output": 120
  17 + }
  18 +}
... ...
server/tests/unit/graph-real-chain-smoke.test.js
... ... @@ -195,6 +195,66 @@ describe(&#39;蝗セ謇ァ陦檎悄螳樣得霍ッ smoke磯峺豕ィ蜈・悟宵謐「謗臥悄 CLI 蟄占ソ帷ィ具シ
195 195 expect(spent.tokens).toBe(1060); // fixture 謚・ 800 + 260
196 196 }, 60_000);
197 197  
  198 + it('竭・ validator 蛻、螟ア雍・譌カ蠑墓梼逵溽噪謖牙、ア雍・螟炊27-P3B ツァ蜈ュ 鬪梧噺 4梧ュ、蜑榊宵譏ッ縲碁撕諤∵遶九搾シ', async () => {
  199 + // 蜴滄ェ梧噺譬壹碁荳莉ス髱樊ウ募・醍コヲ霍 benchmark-validator壼愛蜀ウ荳コ螟ア雍・荳泌シ墓梼謖牙、ア雍・螟炊
  200 + // **荳肴弍** success峨阪13B 蠖捺慮逧サ楢ョコ譏ッ 泯縲碁撕諤∵遶具シ梧悴逵溯キ代坂披 莠コ隸サ莉」遐∵滑
  201 + // fixture verdict 竊 FAILING_VERDICTS 竊 readNodeProduct 竊 callAgentNode 蛻、 fail
  202 + // 霑呎擅體セ隸サ騾壻コシ御スイ。譛我ササ菴穂ク谺。逵溯キ第逵滄得霍ッ隸∵紺縲
  203 + //
  204 + // 縲瑚ッサ騾壻コ榊柱縲瑚キ鷹壻コ堺ケ矩龍髫皮捩逧ュ」譏ッ霑呎擅 smoke 隕∬。・逧ク懆・ソ壻コァ迚ゥ隕∫悄逧誠逶倥
  205 + // 隕∫悄逧「ォ readNodeProduct 隸サ蛻ー縲」erdict 隕∫悄逧「ォ隶、蜃コ譚・縲ゆササ菴穂ク邇ッ譁ュ莠シ
  206 + // 闃らせ驛ス莨壻サ・ success 謾カ蝨コ 窶披 閠**蛻、髞呎 success 逧サ」莉キ譏ッ荳榊粋譬シ莠ァ迚ゥ逶エ謗・謾セ陦鯉シ
  207 + // 荳比ク咲蕗逞戊ソケ**3B ツァ蜈ォ 驍」谿オ縲御ク、遘埼漠莉」莉キ荳榊ッケ遘ー縲崎ッエ逧ーア譏ッ霑吩クェ譁ケ蜷托シ峨
  208 + const VALIDATOR = 'benchmark-validator';
  209 + // 蜿ヲ蠑荳螳カ蜈ャ蜿ク壻ク螳カ蜈ャ蜿ク蜷梧慮蜿ェ蜈∬ョク荳譚。豢サ霍 root rundx_graph_runs_one_active_root会シ
  210 + // 荳企擇蜃譚。逡吩ク狗噪 run 霑伜惠豢サ霍寔驥鯉シ悟逕ィ蜈ャ蜿ク莨壽寵 RunSlotBusyError 窶披 驍」譏ッ郤ヲ譚溷惠豁」蟶ク蟾・菴懶シ
  211 + // 荳肴弍霑呎擅逕ィ萓玖ヲェ檎噪荳懆・ソ縲
  212 + const CO = `${COMPANY}-v4`;
  213 + const db = wdb.getDb();
  214 + const now = new Date().toISOString();
  215 + db.prepare(`INSERT OR IGNORE INTO system_agents
  216 + (id, name, scope, owner_company_id, source_kind, legacy_path, status, created_at, updated_at)
  217 + VALUES (?, ?, 'public', NULL, 'global_agent', ?, 'active', ?, ?)`)
  218 + .run(VALIDATOR, VALIDATOR, `system/agents/${VALIDATOR}`, now, now);
  219 +
  220 + const g = wdb.createWorkGraph({
  221 + companyId: CO,
  222 + // 蟶ヲ荳譚。 fail 蜈懷コ戊セケ壽イ。譛牙霎ケ譌カ螟ア雍・闃らせ謾カ蝨ィ `blocked`域撃襍キ遲我ココ蟾・会シ
  223 + // 驍」荵滓弍縲梧潔螟ア雍・螟炊縲搾シ御ス恚荳榊**霍ッ逕ア**譛画イ。譛芽オー騾壹ょ刈荳願セケ悟愛謐ョ謇榊ョ梧紛
  224 + // 螟ア雍・ 竊 闃らせ fail 竊 豐ソ fail 霎ケ轤ケ莠ョ荳区クク縲
  225 + nodes: [
  226 + { nodeName: VALIDATOR, systemAgentId: VALIDATOR },
  227 + { nodeName: AGENT, systemAgentId: AGENT },
  228 + ],
  229 + edges: [{ from: VALIDATOR, to: AGENT, condition: 'fail' }],
  230 + });
  231 + expect(g.errors || []).toEqual([]);
  232 + const run = wdb.createRun({ graphKind: 'work', graphId: g.id, companyId: CO, executionMode: 'manual' });
  233 + engine.markRunReady(run.id);
  234 + engine.startRun(run.id);
  235 +
  236 + const out = await engine.nextStep(run.id, { task: '逵溷ョ樣得霍ッ smoke夐ェ御ク莉ス髱樊ウ募・醍コヲ' });
  237 + if (!out.ok) throw new Error(`validator 豐。謗ィ蜉ィ${JSON.stringify(out)}`);
  238 + // 霑吩ク、陦悟ーア譏ッ鬪梧噺 4 逧驛ィ**荳肴弍 success**御ク泌ス貞屏關ス莠コ
  239 + expect(out.node.status).toBe('fail');
  240 + // fail_reason 蜿冶蟆溜蝓 ATTRIBUTION_REASONS 窶披 fixture 驥悟荳ェ閾ェ蛻帷噪隸堺シ夊「ォ
  241 + // 謖芽ョセ隶。髯咲コァ謌 null亥シ墓梼荳崎ョ、隸噪蠖貞屏荳蠕倶ク堺ソ。会シ檎ャャ荳迚亥ーア譏ッ霑吩ケ育コ「逧
  242 + expect(out.node.failReason).toBe('input_underspecified');
  243 + // 霍ッ逕ア荵溯ヲ∬オー騾夲シ壼、ア雍・逧ク倶ク豁・譏ッ豐ソ fail 霎ケ轤ケ莠ョ荳区クク御ク肴弍蜴溷慍荳榊勘
  244 + expect((out.activated || []).map((a) => a.name)).toContain(AGENT);
  245 +
  246 + // 蛻、隸堺ケ溯ヲ∝クヲ荳句悉 窶披 豐。譛牙ョシ御ク倶ク霓ョ agent 荳咲衍驕楢ヲ∽ソョ莉荵茨シ按ァ蜈ォ.14 驍」譚。蝗帷識蜈ィ騾夂噪體セ霍ッ峨
  247 + // 豕ィ諢丞愛隸崎誠蝨ィ**陲ォ轤ケ莠ョ逧ク区クク闃らせ**驍」荳陦鯉シ御ク肴弍螟ア雍・闃らせ閾ェ蟾ア驍」荳陦鯉シ
  248 + // 縲悟愛隸咲噪霓ス菴 = 轤ケ莠ョ螳噪驍」谺。迥カ諤∬スャ謐「縲搾シ瑚ソ呎キ螟壽擅蝗樣霎ケ蟷カ蟄俶慮螟ゥ辟カ荳堺クイ蜿ー縲
  249 + const downstream = wdb.getDb()
  250 + .prepare(`SELECT remediation_note FROM graph_run_nodes
  251 + WHERE run_id = ? AND def_node_id = (SELECT id FROM work_graph_nodes WHERE work_graph_id = ? AND node_name = ?)`)
  252 + .get(run.id, g.id, AGENT);
  253 + expect(downstream.remediation_note).toContain('test_script');
  254 +
  255 + fs.rmSync(path.join(__dirname, '../../../system/agents', VALIDATOR, CO), { recursive: true, force: true });
  256 + }, 60_000);
  257 +
198 258 it('竭「 霎ケ逡悟ョ亥梱蝨ィ霑呎擅霍ッ蠕ク頑弍**蠑逹**逧シ域ウィ蜈・蛛 executor 譌カ螳£荳コ蛛シ碁ぅ譚。蛻髪髮カ隕尠', () => {
199 259 // 蛻、謐ョ蜿冶蠑墓梼閾ェ蟾ア逧シ蜈ウ蜃ス謨ー瑚御ク肴弍蜴サ謨ー timeline 莠倶サカ
200 260 // 螳亥梱蝨ィ螳ソ荳サ莉灘コ灘ケイ蜃譌カ譛ャ譚・蟆ア荳堺コァ逕滉コ倶サカ梧響縲梧イ。譛我コ倶サカ縲榊ス薙梧イ。蠑縲堺シ壼渚霑擂蛻、髞吶
... ...
server/tests/unit/template-verdicts.test.js
No preview for this file type