Files
writer-work-flow/memory/decisions.md
Yaojia Wang 5fb7bfb1de feat: M3 — 伏笔账本 + 节奏引擎 + 大纲(含并发记账 bugfix)
- 伏笔账本:纯函数状态机(OPEN/PARTIAL/CLOSED/OVERDUE) + ForeshadowLedger repo;验收后到期扫描(BackgroundTask 自建 session 置 OVERDUE);登记/状态变更端点
- 节奏 + 三审齐:foreshadow-analyst + pace-checker 并入 LangGraph 并行审(REVIEW_SPECS),collect 分列落 chapter_reviews(conflicts/foreshadow_sug/pace),review SSE 加 foreshadow/pace 事件
- 大纲:outliner Agent 产 OutlineResult(含 foreshadow_windows),POST /outline 逐章 upsert outline 表;GET /foreshadow?status= 看板
- 前端:伏笔四泳道看板(OVERDUE 琥珀) + 大纲编辑器(窗口徽标) + 节奏节拍图(▁▃▅) + 审稿页消费 foreshadow/pace SSE
- bugfix(T3.8):并行三审共用请求 session 记账触发 'Session is already flushing' → foreshadow/pace 静默丢失;SqlAlchemyLedgerSink.record 改 add-only(靠端点/事务 commit),加并发回归测试
- M3 E2E:真实 DB + mock 网关零 token 走通 埋设→进展→验收后扫描 OVERDUE→看板 + 大纲含窗口 + 三审齐 SSE/留痕;E2E 暴露并钉住上述 bug
- 门禁绿:mypy 111 / pytest 228(0 xfailed) / alembic 无漂移;前端 gen:api/lint/tsc/vitest 69/build
2026-06-18 14:21:17 +02:00

11 KiB
Raw Blame History

实现决策记录append-only

实现期做出、而规格未覆盖的决策 + 理由。一条一决策,最新在最上。 与规格冲突的不要写这里——去改规格(见 CLAUDE.md「Conventions」。重大架构选择见 ARCHITECTURE.md §1.2 ADR

格式:

## [YYYY-MM-DD] <决策标题> — @skill
- 背景:为什么要决策
- 选择:选了什么
- 理由 / 取舍:
- 影响:动到哪些契约/模块/任务

[2026-06-18] 网关结构化输出经可注入 StructuredClientinstructor— @llm

  • 背景:OpenAICompatAdapter 需接 instructor 产结构化输出C1 类型预留 output_schema/parsed 但 M1 未接线),但测试绝不可联网/碰真实 LLM。
  • 选择:定义 StructuredClient Protocolcreate_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
  • 影响:契约 C1run()parsed;见 contracts 变更日志)、ProviderResultparsed 字段T2.2 续审节点据此消费。

[2026-06-18] 并行审记账并发安全选 add-only去网关 ledger flush— @llm (T3.8)

  • 背景:三审同 superstep 并行共用请求 sessionSqlAlchemyLedgerSink.recordawait flush() 是唯一让步点→第二/三审 flush 重入「Session is already flushing」→被 run_review 吞成 incomplete、foreshadow/pace 静默丢失T3.7 暴露)。
  • 选择:recordadd-only(去 await flush(),只 session.add(row))。session.add() 同步不让步→并行协程不交错;持久化仍靠端点/事务末尾 commit()(自动 flush 待决行)。
  • 理由 / 取舍:最小改动、单 owner只动 ledger.py),不牵动 apps/@backend。前提已校验①并行审区间无 DB 查询触发 autoflushreview_context 取自 state、适配器不碰 session②draft/review/accept/outline 端点末尾均已 commit。缓冲/独立 session 方案跨 owner、更重不取。
  • 影响C1 边界语义不变commit 责任方未变);凡新增「并行用网关产 usage」路径sink 必须保持 add-only、不得在并行段 await flush。新增并发回归测试 test_ledger_concurrency.py

[2026-06-18] 节拍图用定高 CSS 柱 + 文本 sparkline不引图表库 — @frontend (T3.6)

  • 选择:beat_map(int 序列) 按本序列 min..max 线性归一到 7 档 ▁..▇(全相等→居中档,最低柱给 12% 可见高度),渲染定高 CSS 柱 + aria-label="爽点节拍图 ▁▃▅…"。静态无动画→天然满足 prefers-reduced-motion。
  • 理由KISS避免图表库依赖。影响T3.7 节拍图 role="img" selector。

[2026-06-18] 大纲 beats 存储形 + 写侧 repo 命名 — @backend (T3.5)

  • 背景:outline.beats DB 列是 JSONB dict但 outliner 产 list[str];读侧 OutlineRepo(C5/memory)已占名。
  • 选择:写侧 SqlOutlineWriteRepo 把 list 包成 {"beats":[...]} 落库(读侧同形 round-trip 一致API 出参 OutlineChapterView.beats 解包成裸 list[str]。写侧命名加 Write 前缀避撞 C5 读侧(同 DigestAppendRepo/ForeshadowLedgerRepo 先例)。volume 由端点提供schema 无逐章卷号M3 默认全卷 1get_outline_gateway 复用 build_gateway_for_tier(...,"analyst")、独立缝便于测试 override。
  • 影响C3 扩(outline 端点)T3.6 渲染 beats 裸 list + 窗口徽标T3.7 断言 outline 行 beats 列形。

[2026-06-18] 验收后伏笔到期扫描挂 BackgroundTask + 重复code/非法转移映射 VALIDATION(422) — @backend (T3.2)

  • 背景§5.5 步骤3 TODO(M3) 要验收后置 OVERDUE登记/状态端点需映射 DB 唯一冲突与非法状态机转移到错误信封。
  • 选择:(1) accept 端点 commit 成功后经 FastAPI BackgroundTasks 登记 run_overdue_scan(自建独立 session因请求 session 已关闭);扫描抽纯 async 函数 + 可注入 repo_factory/session_factory 缝,单测直接 await 注 fake、不起后台线程。(2) 重复 code(IntegrityError)/InvalidTransition 都映射 ErrorCode.VALIDATION(422) 非 409——现有 409 码 CONFLICT_UNRESOLVED 语义=未决冲突,复用不符;未新增 shared 错误码。
  • 影响C3 扩(foreshadow 登记/状态端点)§7.4 持久性局限(进程内/重启丢任务)原型接受、不引 jobs 表T3.5 看板/outline 续接本 routerT3.7 真 pg 断言 OVERDUE。

[2026-06-18] 伏笔写侧 Repository 加 Ledger 前缀避免与读侧同名 — @backend (T3.1)

  • 背景:读侧 ForeshadowRepo/ForeshadowView/SqlForeshadowRepodomain/repositories.py+memory/)已被 assemble(C5 稳定)占用且 View 最小(无 importance/links/progress。M3 写侧账本需更全 View + 写方法。
  • 选择:写侧命名 ForeshadowLedgerRepo/SqlForeshadowLedgerRepo/ForeshadowLedgerView,落 domain/foreshadow_repo.py;读侧不动。状态机纯函数落 domain/foreshadow_state.pyForeshadowStatus StrEnum + transition/is_overdue/apply_overdue_scan/InvalidTransition)。
  • 理由:同 T2.3 DigestAppendRepo vs 读侧 DigestRepo 先例,读写分离免歧义、不破坏 C5。写方法只 flush 不 commitcommit 归 T3.2 扫描任务 / T3.5 端点)。
  • 影响T3.2 验收后扫描 / T3.5 看板端点消费 ForeshadowLedger*

[2026-06-18] 大纲节点不接进 review 图、不做失败隔离 — @llm (T3.4)

  • 背景:续审在 run_review 内把网关失败隔离为 incomplete§5.2 任一审不阻塞其余)。
  • 选择:大纲是独立生成(不在写章/审稿流水线),run_outline 裸函数、网关失败直接上抛由 T3.5 端点处理;带 output_schemaparsedOutlineResultValueErrorC1 违约即显错,不返回空大纲)。
  • 影响C6 扩outliner spec + OutlineResult + run_outline 缝T3.5 端点调 run_outline 持久化 outline 表。

[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=lightChapterDigestFacts schema经网关 run().parsed) 在 run_accept_transaction 之前调用R2结果作 digest_facts:dict 传入;事务内只三次纯 DB 写(promote_to_accepteddigest.appendset_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_UNRESOLVEDT2.7 E2E 验证事务原子性+digest 来源+ledger。

[2026-06-18] 验收-side 写侧 Repository 命名与提交边界 — @backend (T2.3)

  • 背景:chapter_digests 已有读侧 repomemory/DigestRepo.recent,供 assembleM2 需写侧 append。同名易混。
  • 选择:写侧命名 DigestAppendRepo/SqlDigestAppendRepo,落 domain/digest_repo.py共用 DigestView;读侧仍在 memory/。新增 review_repo.py(record/list_for_chapter 新→旧/set_decisions) 与 chapter_repomax_version/promote_to_accepted/latest_accepted
  • 理由 / 取舍:读写分离避免同名歧义;写侧三 repo 的写方法flush()commit(),提交归 T2.4 验收事务单次完成(对齐「写库副作用在编排/事务层」不变量);save_draft 仍自 commitM1 自动保存语义)。
  • 影响T2.4 在单事务里组合 promote_to_accepteddigest.appendreview.set_decisionscommit()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(非 LlmRequestC1 的 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。