Files
writer-work-flow/memory/decisions.md
Yaojia Wang f43ccd293f feat(toolbox): T6 创作工具箱通用生成器框架 — 8 新生成器 + 声明驱动落地页 + P2 收尾
通用执行路径驱动全部生成器("加生成器=加一份声明"):
- @llm: ww_agents +7 输出 schema + 7 spec(book-title/blurb/name/golden-finger/
  glossary/opening/fine-outline,只声明 tier)+ build_outline_chapter_context
- @backend: ww_skills GeneratorTool 描述符 + TOOLBOX(11) + get_tool;3 通用端点
  GET /skills/toolbox · POST .../skills/{tool_key}/generate(预览不写库,仅记账) ·
  POST .../ingest(复用 continuity 409 + partition_writes 白名单);纯 context 派发
- @frontend: 工具箱落地页 RSC + 声明驱动 GeneratorRunner + lib/toolbox 纯函数
  + LeftNav「工具箱」+ ⌘K nav-toolbox/action-gen-*;legacy 3 跳现页
- @qa: tests/test_t6_toolbox_e2e.py 5 用例真 pg + mock 网关零 token,无端点 bug
- P2 收尾: 限流→decisions.md 记延后(单用户原型);noopener/Committable 早已修

守不变量 #2(只声明 tier)/#3(预览不写库,入库经验收 gate)/#9(缓存前缀)。无 DB 迁移。
门禁绿: 后端 ruff/format/mypy 195/alembic 无漂移/pytest 583;前端 lint/tsc/vitest 279/build。
spec 回写 PRODUCT_SPEC §7 + ARCHITECTURE §7.2 端点表。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:37:55 +02:00

152 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 实现决策记录append-only
> 实现期做出、而规格未覆盖的决策 + 理由。一条一决策,最新在最上。
> 与规格冲突的不要写这里——去改规格(见 CLAUDE.md「Conventions」。重大架构选择见 `ARCHITECTURE.md §1.2 ADR`。
格式:
```
## [YYYY-MM-DD] <决策标题> — @skill
- 背景:为什么要决策
- 选择:选了什么
- 理由 / 取舍:
- 影响:动到哪些契约/模块/任务
```
---
## [2026-06-22] 限流rate limiting原型期**延后**,记触发条件 — @backend (P2 code-review)
- 背景:代码审查指出触 LLM 的端点draft/review/accept/outline/style/refine 等)+ `/oauth/start` + `/settings/providers/test` 缺速率限制;审查约定「若接受延后,记入 memory/decisions.md」。
- 选择:本期**不加**任何限流代码/依赖(不引 `slowapi` 等)。
- 理由 / 取舍:原型显式为**单用户**CLAUDE.mdauth/多租户化已 deferred`users`/`owner_id` 是 stub无公网暴露、无多调用方滥用面KISS/YAGNI——为零真实风险的场景引入限流中间件 + 新依赖属过早工程。代价若误暴露到公网LLM 端点可被滥用刷成本(已记触发条件兜底)。
- 触发revisit when**引入多租户/鉴权** 或 **应用对外公开暴露** 时,须补轻量限流——按 IP/token 对 LLM 端点 + `/oauth/start` + `/settings/providers/test` 加每窗口配额(届时再评估 `slowapi` 或反代层限流)。
- 影响:无契约/代码改动;仅本条记录决策与重启条件。
## [2026-06-19] Kimi Code OAuthtoken 存储包 + 刷新启发式 + 省略 scope + 后台轮询走 jobs — @backend (K1.3)
- 背景Kimi 订阅 plan 走 device-flow OAuth需定 token 持久化形、何时刷新、device authorization 是否带 scope、后台轮询如何挂。
- 选择:
1. **存储包**`TokenSet{access_token, refresh_token, expires_at}` 序列化为 JSON 串经 Fernet复用 `encrypt_api_key`,工作在 str 上)加密入 `provider_credentials.oauth_enc`C2 扩 K1.1 列)。`expires_at` 存**服务端驱动的绝对 UTC 时刻**(从 `expires_in` 计算入库时刻)。明文 token 绝不进日志/响应/job 结果。
2. **刷新启发式**:建网关时若 access token 剩余寿命 < `MIN_REFRESH_BUFFER_SECONDS=300`300s 缓冲,对 ~15min access 足够),调 `refresh` 换新包并 `upsert_oauth_credential` 持久化(下次复用)。任务文档原提 `max(300, 0.5*expires_in)`——本实现采固定 300s 缓冲(不存原始 expires_inKISS对 15min access 等价于 0.5*expires_in=450s 略激进但安全)。
3. **省略 scope**device authorization 不带 `scope`(匹配研究确认的 kimi-cli v1.41.0+server 拒绝时再加 `scope=kimi-code` 兜底——本期先不带。client_id env `KIMI_CLIENT_ID` 可覆盖默认。
4. **后台轮询走 jobs**`POST .../start` 创建 `jobs(kind="kimi_oauth")` 返 202 + user_code复用 K1.1/M4 `run_job` 在独立 session 上**循环** `poll_token`(每 interval 秒,`authorization_pending` 续轮询、`slow_down` 增大 interval、过期/拒绝抛 AppError 置 job failed成功落加密 token + job done结果只 `{connected, provider}`)。前端轮询 `GET /jobs/{id}`
- 理由 / 取舍:复用既有 jobs/run_job 基建(无新长任务机制);固定 300s 缓冲省去存 expires_inYAGNI。代价refresh-on-build 每次建网关多一次解密 + 可能一次刷新网络调用(仅临近过期时)。
- 影响C3 扩OAuth 3 端点C2 扩 K1.1 的 `StoredCredential` 收口api_key_enc→nullable + auth_type/oauth_enc`_build_provider_adapter` 加 OAuth 分支。K1.4 前端连接流、K1.5 E2E真账号联网验证含封号风险用户自担
## [2026-06-19] Kimi Code OAuthdevice authorization **重新带 `scope=kimi-code`**(实测 401 后修正,超越上条第 3 点)— @backend (K1.3 fix)
- 背景K1.5 真账号联网实测——device-flow 登录成功拿到真 JWT但调 `api.kimi.com/coding/v1` 被拒 `401 Invalid Authentication`。根因device_authorization 省略了 `scope`,拿到的 token 缺 coding entitlement。
- 选择:`start_device_authorization` 的请求体在 `client_id` 之外加 `scope=kimi-code`(模块常量 `KIMI_CODE_SCOPE`)。**修正上条决策第 3 点的「省略 scope」**——`scope=kimi-code` 是 coding-agent 文档化 OAuth scope`ooojustin/opencode-kimi` `constants.ts` 发送之kimi-cli v1.41.0 已不发但服务端仍接受)。
- 范围scope **只**放 device_authorization 请求token 交换(`poll_token`/刷新(`refresh`**不带** scopeOAuth device flow 惯例,参考实现亦然)。
- 影响device-auth 请求体现为 `{"client_id": <id>, "scope": "kimi-code"}`K1.3 单测 `test_start_device_authorization_parses_response_and_sends_scope``data["scope"]=="kimi-code"`。无 OpenAPI 形变,前端无需 re-gen。
## [2026-06-19] Skill registry + 表权限沙箱迁回 `ww_skills` 包(@devops 已纳入 workspace— @backend (M5 R3)
- 背景T5.5 曾因 `packages/skills` 非 uv workspace member 把 `skill_registry`/`skill_permissions` 暂落 `ww_core.domain``ww_skills/__init__` 做再导出门面(见上 T5.5 决策)。@devops 现已把 `packages/skills` 做成真 workspace member`ww_skills` 可 import、有自己的 pyproject、`uv sync` 完成)——前提消除。
- 选择:把两个模块**物理迁入** `packages/skills/ww_skills/{skill_registry,skill_permissions}.py`(内部相对 import 从 `ww_core.domain.skill_permissions` 改为 `ww_skills.skill_permissions``ww_skills/__init__` 改为**直接导出**(不再 re-export ww_core删除 `ww_core.domain` 的两个模块文件 + `__init__` 里的 skill 导出。所有 importer`apps/api` `generation.py`/`project_deps.py` + `test_generation.py`)改 `from ww_skills import …`skill 单测从 `packages/core/tests/` 移到 `packages/skills/tests/`import 也改 `ww_skills`)。
- 理由 / 取舍恢复任务文档原定位skill 逻辑归 skills 包),物理位置与 §5.6 措辞一致ww_core 不再承载 skill 符号(关注点分离)。代价:`packages/skills/pyproject.toml` deps 需校准——`ww_skills` 直接 import `ww_db`/`ww_agents`/`ww_shared`/`ww_llm_gateway`(`Tier`),故去掉不再用的 `ww-core`、补 `ww-llm-gateway` + `sqlalchemy``SqlSkillRepo``select`/`AsyncSession`);改后 `uv sync` 重建 ww-skills。
- 影响C8 实现位置更新contracts.md C8 状态加「M5 R3 迁入 ww_skills 包」);消费方 import 路径从 `ww_core.domain`/`ww_core.domain.skill_registry``ww_skills``ww_core.domain.skill_*` 导入路径**已失效**(凡 grep 到旧路径须改)。`packages/skills/pyproject.toml` 改了 deps@backend 可编辑的 skills 目录内,但属打包元数据)——已 PROGRESS 通知 @devops 复核根锁。
## [2026-06-19] `build_gateway_for_tier` 改用 `build_adapter` 工厂per-provider 适配器)— @backend (M5 R1 #2)
- 背景T5.4 接线只为回退链里的每个 provider 一律建 `OpenAICompatAdapter`——Anthropic/Gemini 即便配进 `tier_routing` 链也因 `_PROVIDER_BASE_URLS` 无 base_url 被跳过、拿不到真实适配器。@llm 加了 `build_adapter(provider, *, api_key, base_url=None)` 工厂C1 扩 follow-up #2)按 provider 选适配器类。
- 选择:`_build_openai_compat_adapter` 改名 `_build_provider_adapter`去掉「base_url 缺失即返 None」分支改调 `build_adapter(provider, api_key=…, base_url=_PROVIDER_BASE_URLS.get(provider))`——OpenAI 兼容 provider 传 base_urlAnthropic/Gemini 传 None 走原生适配器。仅在凭据缺失时返 None回退链跳过。保留既有 503/凭据/单 provider 兼容行为。
- 理由 / 取舍:最小改动让回退链支持非 OpenAI-兼容 provideradapter 选择逻辑收敛到网关包的工厂单一真源apps/api 不再硬编码适配器类。代价:真实 Anthropic/Gemini 仍需 @devops 加 SDK 依赖(适配器懒导入)。
- 影响C3 扩build_gateway_for_tier 接线段更新);`apps/api` 去掉 `AsyncOpenAI`/`OpenAICompatAdapter` 直接 import。T5.7 切 provider E2E 可扩 Anthropic/Gemini 路径。
## [2026-06-19] 设定库 Codex 读端点复用 C5 读侧 + 反向 JSONB 解包 — @backend (M5 R1)
- 背景PROGRESS 余项①——Codex 设定库本应展示 characters/world_entities **读/管理**视图,但后端只有生成/入库写端点,前端只能「生成+本次入库会话内回显」。
- 选择:加 `GET /projects/:id/characters` + `GET /projects/:id/world_entities`**复用 C5 读侧** `SqlCharacterRepo`/`SqlWorldEntityRepo`(经既有 `get_memory_repos` dep不新增 repo、不动 assemble响应做 T5.2 ingest 形变的**逆向**解包(`{"items":[...]}`/`{"text":...}`/`{"rules":[...]}`→裸 list/str复用既有 `_existing_characters` helper。无行返空列表非 404列表语义
- 理由 / 取舍KISS——读端点是写入形变的镜像复用已有读侧 + helper 零新依赖GET 与 POST 同路径靠 method 区分无冲突。代价:读侧 CharacterView 仍是裸 dict端点层做解包与写侧 repo 的包裹对称)。
- 影响C3 扩(两读端点);@frontend `pnpm gen:api` + CodexPage 初始列表拉全量(去掉「会话内」局限,解 gotcha 2026-06-19 @frontend Codex 缺口条)。
## [2026-06-19] 角色入库 continuity gate有冲突→409 CONFLICT_UNRESOLVED作者 acknowledge 后放行 — @backend (T5.2)
- 背景ARCH §6.5 要求生成角色入库前过 continuity 校验(`precheck_generated_cards`但任务文档留了两条路线block / 返回冲突供作者裁决)。要选最简单正确、且守不变量 #3「无 AI 静默写库」。
- 选择:入库端点跑 precheck检出冲突且请求未带 `acknowledge_conflicts=true`**409 `CONFLICT_UNRESOLVED`** + `details{conflicts:[...], conflict_count}`(不写库)。作者在前端查看冲突、裁决后重发带 `acknowledge_conflicts=true` → 放行写库。零冲突 → 直接写库 201。
- 理由 / 取舍:复用既有 `CONFLICT_UNRESOLVED`(409) 码(语义=未决冲突禁写入,与 accept gate 一致),无需新增错误码;显式 `acknowledge` 标志=作者裁决的 HITL gate不静默入库守不变量 #3),比「自动返回冲突 + 另开裁决端点」更省(无新端点、无新状态表,原型 KISS。代价作者确认 = 重发一次请求(带 flag可接受。
- 影响C3 扩POST /characters`partition_writes` 在 gate 后再过写白名单character-gen 只声明 writes=characters。T5.6 前端入库流:预览→点入库→若 409 展示冲突→裁决勾选→带 acknowledge 重发。T5.7 E2E 覆盖两条路径。
## [2026-06-19] 角色卡 schema↔DB 形变落写侧 repolist→{"items"}、arc str→{"text"}、rules→{"rules"})— @backend (T5.2)
- 背景T5.1 gotcha 指出 `CharacterCard.traits`/`speech_tics`(list)/`arc`(str) 与 DB `characters` JSONB **dict** 列不同形;`WorldEntityCard.rules`(list) 与 `world_entities.rules` JSONB dict 列不同形。需定一个固定包裹形。
- 选择:写侧 repo 做转换层——`traits`/`speech_tics` 包成 `{"items":[...]}``arc` 包成 `{"text":...}``rules` 包成 `{"rules":[...]}``tags`/`relations` 是 JSONB list 直落。读回(喂防雷同/precheck反向解包同形`_existing_characters`/`_world_context`)。
- 理由 / 取舍:固定 wrapper key 让 round-trip 确定(写=读互逆),不改 @db 列类型(@db owned。代价assemble/C5 读侧 `CharacterView.traits` 仍是裸 dict本期 assemble 不消费 items 内层,无影响);若后续 assemble 要渲染 traits 文本,按同 wrapper 解包即可。
- 影响C3 扩(写侧 repo 形变契约ww_core.domain 新增 `character_repo`/`world_entity_repo`@db 若日后规整列类型为 list可去 wrapper迁移由届时 owner 执行)。
## [2026-06-19] 顺带补 `GET /projects/:id/style` 读指纹 + 写侧 repo 独立读视图 — @backend (T4.3)
- 背景UX §6.9 要展示完整 16 维指纹 + 证据,但 ARCH §7.2 端点清单**无**读指纹端点(只 `POST /style`。C5 读侧 `SqlStyleRepo.latest` 已存在,但其 `StyleView` **只含 `dimensions`**assemble 只需维度文本,不需证据/版本),扩它会牵动 C5 assemble 契约。
- 选择:(1) **加 `GET /projects/:id/style`** 返最新指纹 `{dimensions, evidence, version}`无指纹→404 `NOT_FOUND`)。(2) 不动 C5 读侧——在写侧 `SqlStyleFingerprintWriteRepo` 上加一个 `latest(project_id)->StyleFingerprintView`(含 evidence_json + version 的更全视图),`GET /style` 用它C5 `SqlStyleRepo.latest`/`StyleView`(只 dimensions供 assemble stable_core保持不变。
- 理由 / 取舍:读写分离 + 各自最小视图(同 `DigestAppendRepo` vs 读侧 `DigestRepo` 先例),不破坏 C5 assemble前端无须靠 job `result` 拼凑摘要、可直接拉完整指纹。
- 影响C3 扩(新增 `GET /style`**待回写规格**ARCH §7.2 端点清单 + PRODUCT_SPEC §6 应补 `GET /projects/:id/style`spec 一致性纪律,@docs 回写。T4.4 前端 FingerprintView 消费此端点。
## [2026-06-19] 学文风 `work` 自建网关+repo不走 FastAPI dep凭据探测前移到请求阶段 — @backend (T4.3)
- 背景:学文风走 jobsM4-c`POST /style` 立即返 202提取经 BackgroundTask `run_job` 在**独立 session** 上跑(请求 session 已关闭。但「无凭据→503」需在请求阶段就让用户知道不能 202 后再静默 fail job
- 选择:(1) `work(session)` 闭包内**自建** analyst 网关(`build_gateway_for_tier(session, SqlCredentialStore(session), "analyst")`+ 写侧 repo`run_job` 传入的独立 session不依赖 FastAPI depdep 绑的是请求 session。(2) 端点仍声明 `get_style_extract_gateway`(analyst) 依赖**仅作凭据探测**——无凭据时 dep 解析阶段抛 503`job_repo.create` 之前拦下;其返回的网关本身不被复用(请求 session 即将关闭)。
- 理由 / 取舍兼顾「BackgroundTask 必须自建 session」gotcha与「无凭据请求阶段即 503」友好、不写注定失败的 job。代价凭据被探测两次请求阶段 + 后台 work原型可接受。
- 影响C3 扩POST /style测试后台路径 monkeypatch `style.build_gateway_for_tier`/`style.SqlStyleFingerprintWriteRepo` + `job_runner.SqlJobRepo`(见 gotcha
## [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] 并行审记账并发安全选 add-only去网关 ledger flush— @llm (T3.8)
- 背景:三审同 superstep 并行共用请求 session`SqlAlchemyLedgerSink.record``await flush()` 是唯一让步点→第二/三审 flush 重入「Session is already flushing」→被 `run_review` 吞成 incomplete、foreshadow/pace 静默丢失T3.7 暴露)。
- 选择:`record`**add-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 默认全卷 1`get_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`/`SqlForeshadowRepo``domain/repositories.py`+`memory/`)已被 assemble(C5 稳定)占用且 View 最小(无 importance/links/progress。M3 写侧账本需更全 View + 写方法。
- 选择:写侧命名 `ForeshadowLedgerRepo`/`SqlForeshadowLedgerRepo`/`ForeshadowLedgerView`,落 `domain/foreshadow_repo.py`;读侧不动。状态机纯函数落 `domain/foreshadow_state.py``ForeshadowStatus` 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_schema``parsed``OutlineResult``ValueError`C1 违约即显错,不返回空大纲)。
- 影响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=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。
## [2026-06-19] T5.5 Skill registry + 表权限沙箱落 `ww_core.domain`,非 `packages/skills/ww_skills` — @backend
- 背景T5.5 任务文档把 registry/沙箱定位在 `packages/skills/ww_skills/`,但该包当前**不是 uv workspace member**(根 `pyproject.toml``[tool.uv.workspace].members` 不含它、mypy_path/ruff src 也不含),无 pyproject → 仅靠 sys.path hack 才能 importapp/测试/mypy 都无法干净消费它。根 pyproject 由 @devops 持有,自己接线会越权 + 阻塞。
- 选择:把实现落在已是 workspace member、且承载「编排/写库 apply 层」的 `ww_core.domain``skill_registry.py` + `skill_permissions.py`,经 `ww_core.domain` 导出)。`ARCHITECTURE §5.6` 明确「强制点在编排/写库层」=ww_core语义上也吻合。`packages/skills/ww_skills/__init__.py` 留**再导出门面**import 自 ww_core保留稳定「skills」导入名。
- 理由 / 取舍非阻塞、门禁可干净覆盖ruff/mypy/pytest 全跑)、不越权改根配置;代价是物理位置与任务文档措辞略偏,但功能/契约C8完整。待 @devops`packages/skills` 纳入 workspace + mypy_path 后,可平滑迁移实现到 `ww_skills` 而不动调用方(只改门面方向)。
- 影响:消费方 import `ww_core.domain``SkillRegistry`/`filter_reads`/`partition_writes`/`validate_declaration`(或经 `ww_skills` 门面)。若 @devops 后续纳入 workspace迁移由届时的 @backend/@llm 执行。