# L2 · Integration Loop > 中间层。当所有 Unit passed 后开闸:跑集成测试 → 若失败则**归因 → 改 SDD → 回 L1**。L2 是「跨 Unit 契约」一致性的守门员。 ## 状态机 ## L2 闸门触发条件 不是「所有 Unit 都跑过一次」,而是「所有 Unit 的**当前 contract_hash 与依赖关系下,状态都是 passed**」。这避免 SDD 改过之后某个旧版 Unit 还停留在 passed 而事实上契约已变。 ```js function l2GateOpen(project) { return project.units.every(u => u.status === "passed" && u.contract_hash === currentSDD(project).contractHashFor(u.id) ); } ``` ## 失败归因 — BugTriager 输出契约 BugTriager 必须输出一个机读 JSON,LoopController 据此自动决定回滚范围。Triager 不直接改文件,只发「建议」。 ```js { "run_id": "run-int-20260512-1031", "root_cause": "auth-be returns 500 on invalid pwd, expected 401; auth-fe error handler relies on 401", "category": "contract_mismatch", "affected_units": ["auth-be", "auth-fe"], "proposed_sdd_patch": { "target_section": "#unit-auth-be", "op": "update", "text": "明确:登录失败必须 401 with {error:'invalid_credentials'}" }, "confidence": 0.82 } ``` ## 归因 → 回写 → 重置 三步 #### ① 归因 BugTriager 读 stderr + diff + units.json + sdd.html,产 `triage.json`。**confidence < 0.5** 时上报 L3 让人类裁决。 #### ② 回写 Architect 接收 `proposed_sdd_patch`,用 section 级 DOM 操作改 `sdd.html`,归档旧版到 `history/sdd-rev-N.html`。 #### ③ 重置 UnitRegistry 把 `affected_units` 的 status 改回 `pending`,并自动重算 `contract_hash`,触发 L1 新一轮。 ## L2 修复预算 同一项目**每个 PRD rev** 内最多触发 3 次 L2 修复轮回。超出后整个项目挂起,发 SSE 事件 `project.l2_exhausted`,等待人类(L3)发新需求或手动干预。 **为什么是 3:**实测 Claude Code Agent 三轮内还修不好的,要么是契约描述本身有歧义(PRD 缺陷),要么是技术选型有根本问题。再加轮次只会浪费 Token。 ## 集成测试套件从哪来? 这是一个设计决策(见 [风险与待决 · R3](plan-risks.html))。初版选 **方案 A**: | 方案 | 谁写集成测试 | 优点 | 缺点 | | --- | --- | --- | --- | | **A · 选中** | Architect 在生成 SDD 时同步生成 `integration/spec.json` | 少一个 Agent,集成测试与 SDD 同步演化 | Architect 职责变重 | | B | 独立 IntegrationAuthor Agent | 关注点分离 | 多一个 Agent,多一次握手 | | C | Integrator 自己写 | 读取代码后再写最贴合 | 容易写一个「永远会通过」的 trivial test |