feat: M2 — 写→审(一致性)→裁决→验收(事务);未决冲突禁验收

- 续审 Agent 声明(AgentSpec) + 结构化输出契约(ContinuityReview/Conflict 五类)
- LangGraph 并行审子图(可扩四审) + collect 落 chapter_reviews 留痕 + review SSE(section/conflict)
- 验收-side Repository:章节 accepted 版本晋升 + digest append-only + 审稿留痕/裁决
- API:review(SSE) + reviews 历史 + accept(单原子事务:晋升 version + 终稿 digest + 裁决留痕)
- 冲突 gate:未决裁决拦截(CONFLICT_UNRESOLVED);digest 从终稿提炼(不变量#4)
- 前端:审稿报告页 + 冲突就地标注 + 裁决(采纳/忽略/手改) + 未决禁验收 + 「本次将更新」清单
- M2 E2E:真实 DB + 多档位 mock 网关零 token 走通 写→审→裁决→验收→摘要入库
- 多 agent 协同台账(PROGRESS.md) + 共享记忆(memory/contracts·decisions·gotchas)
This commit is contained in:
Yaojia Wang
2026-06-18 11:38:28 +02:00
parent b523b4fd21
commit 68f194a043
36 changed files with 3881 additions and 0 deletions

49
memory/decisions.md Normal file
View File

@@ -0,0 +1,49 @@
# 实现决策记录append-only
> 实现期做出、而规格未覆盖的决策 + 理由。一条一决策,最新在最上。
> 与规格冲突的不要写这里——去改规格(见 CLAUDE.md「Conventions」。重大架构选择见 `ARCHITECTURE.md §1.2 ADR`。
格式:
```
## [YYYY-MM-DD] <决策标题> — @skill
- 背景:为什么要决策
- 选择:选了什么
- 理由 / 取舍:
- 影响:动到哪些契约/模块/任务
```
---
## [2026-06-18] 网关结构化输出经可注入 `StructuredClient` 缝instructor— @llm
- 背景:`OpenAICompatAdapter` 需接 instructor 产结构化输出C1 类型预留 `output_schema`/`parsed` 但 M1 未接线),但测试绝不可联网/碰真实 LLM。
- 选择:定义 `StructuredClient` Protocol`create_with_completion(*, messages, response_model, **kw) -> (parsed, raw)`,即 `instructor.AsyncInstructor` 的形adapter 构造可选注入 `structured_client`,未注入时懒构建 `instructor.from_openai(self._client)``run()` 透传 `result.parsed → LlmResponse.parsed`
- 理由 / 取舍:保留既有「注入 client 便于测试」风格,结构化路径同样可注 fake。`ProviderResult.text` 对结构化路径置为 `parsed.model_dump_json()`(便于日志/留痕),程序消费走 `parsed`
- 影响:契约 C1`run()``parsed`;见 contracts 变更日志)、`ProviderResult``parsed` 字段T2.2 续审节点据此消费。
## [2026-06-18] 审稿页终稿来源:页内可编辑 textarea无 GET draft 端点)— @frontend (T2.6)
- 背景:审稿/验收都需「终稿」文本,但当前无 `GET .../draft` 端点拉取已存草稿。
- 选择:审稿页用可折叠 textarea 编辑终稿(`initialDraft=''`),重审传它为 `{draft}`(空→后端回退已存草稿),验收 `final_text`=它。
- 理由 / 取舍:对齐不变量#4(摘要从作者裁决/改稿后的终稿提炼作者可在审稿页改稿。代价刷新页不自动回填草稿正文M2 可接受,后续可加 GET draft
- 影响仅前端T2.7 E2E 用 `#final-text` 注入终稿。
## [2026-06-18] 验收事务边界digest 提炼在事务外、单事务三写一次提交 — @backend (T2.4)
- 背景§5.5 要求验收「单事务」含「从终稿提炼 digestLLM 调用)」,但不能在持开 DB 事务里跨网络调 LLM占锁
- 选择digest 提炼 `extract_digest_facts`(tier=light`ChapterDigestFacts` schema经网关 `run().parsed`) 在 `run_accept_transaction` **之前**调用R2结果作 `digest_facts:dict` 传入;事务内只三次纯 DB 写(`promote_to_accepted``digest.append``set_decisions`+ 末尾一次 `session.commit()`,任一步失败整体回滚。
- 理由 / 取舍兼顾「digest 从终稿(不变量#4)」与「事务不跨网络」;提炼那次网关调用的 usage 随事务提交落 1 条 usage_ledger。digest 用 light、续审用 analyst 档位,统一经新 `build_gateway_for_tier(session, store, tier)``build_writer_gateway` 退化为 writer 特例。§5.5 步骤 3人物 latest_state/伏笔更新)留 `TODO(M3)` 占位,不引入 M3 表逻辑。
- 影响:契约 C3accept/review/reviews 三端点T2.6 消费 `AcceptResponse`/`CONFLICT_UNRESOLVED`T2.7 E2E 验证事务原子性+digest 来源+ledger。
## [2026-06-18] 验收-side 写侧 Repository 命名与提交边界 — @backend (T2.3)
- 背景:`chapter_digests` 已有读侧 repo`memory/``DigestRepo.recent`,供 assembleM2 需写侧 append。同名易混。
- 选择:写侧命名 `DigestAppendRepo`/`SqlDigestAppendRepo`,落 `domain/digest_repo.py`**共用 `DigestView`**;读侧仍在 `memory/`。新增 `review_repo.py`(`record`/`list_for_chapter` 新→旧/`set_decisions`) 与 `chapter_repo``max_version`/`promote_to_accepted`/`latest_accepted`
- 理由 / 取舍:读写分离避免同名歧义;写侧三 repo 的写方法**只 `flush()``commit()`**,提交归 T2.4 验收事务单次完成(对齐「写库副作用在编排/事务层」不变量);`save_draft` 仍自 commitM1 自动保存语义)。
- 影响T2.4 在单事务里组合 `promote_to_accepted``digest.append``review.set_decisions``commit()`R2/R3/R4/R5 决策落在这些接口点。
- 背景ARCH §5.3 伪码里 `assemble` 直接返回 `LlmRequest`,会让 `packages/core/memory` 反向依赖 `packages/llm_gateway` 的类型,且使 M1 的 T1.2 强依赖 T1.1(无法并行)。
- 选择:`assemble(project_id, chapter_no) -> AssembledContext(stable_core: str, volatile: str, selection: SelectionTrace)`。稳定内核/易变各为**已确定性排序、无时间戳/UUID 的字符串**write 节点(T1.3)再据此构造 `LlmRequest(system=[Block(text=stable_core, cache=True)], input=volatile)``SelectionTrace` 记录每个实体的入选理由,供 UX 注入透明面板(T1.6)。
- 理由 / 取舍:记忆服务只经 DB 通信、不应知道网关请求类型(对齐不变量①②);解耦后 T1.1‖T1.2 可并行。§5.3 伪码视为示意,真正的缝是「确定性选择 → 序列化的 stable/volatile 文本」。
- 影响:契约 C5 输出形 = `AssembledContext`(非 `LlmRequest`C1 的 `Block`/`LlmRequest` 仅 gateway+orchestrator 使用。回写 ARCH §5.3 待 M1 落地后由 @docs 注一句。
## [2026-06-18] CLAUDE.md 不复制 spec 的枚举值,只重述跨文档规则 — @all
- 背景CLAUDE.md 原先内联了 SSE 事件名、LLM 调用日志字段、错误码/envelope 形状等"会变的具体值"ARCHITECTURE 一改即静默过期,使 CLAUDE.md 自身成为最大漂移源。
- 选择CLAUDE.md 只保留跨文档、易错的**规则**(含 9 条架构不变量);所有枚举型/具体值改为指向 ARCHITECTURE/PRODUCT_SPEC 对应 §(唯一真源)。新增"冲突裁决"段ARCHITECTURE > UX_SPEC/DEV_PLAN > PRODUCT_SPEC矛盾时回写上游。
- 理由 / 取舍:消除文档间漂移面、降低每 session 上下文成本;代价是查具体值需多跳一层到 ARCHITECTURE可接受
- 影响:编辑 CLAUDE.md 的任何 agent 遵循此纪律——契约/枚举值落在 spec 与 `memory/contracts.md`,不在 CLAUDE.md。