承 C1 链图(build_chain_graph),落地多章工作流链的 apps/api 壳:
- 3 端点 routers/chain.py:POST .../chains/{key}/run→202 ChainRunAccepted;
POST .../chains/runs/{job_id}/resume→202;GET /jobs/{id} 复用。校验:
count 1..50→422、未知 chain_key→404、resume 非 awaiting→409、无凭据→503。
- schemas/chain.py:ChainRunRequest/ChainRunAccepted/ChainResumeRequest
(ConflictDecision 复用 schemas/projects)。
- services/chain_runner.py:run_chain_job 仿 run_job 壳自建独立 session 驱动链图
(set_running→ainvoke→据 __interrupt__ 置 awaiting_input/done/failed);
build_accept_op 在 apps/api 装配验收事务闭包注入图节点(守 #3/#4);
token 不入 result/日志。
- services/chain_deps.py:get_checkpointer_factory(运行时 AsyncPostgresSaver
上下文 / 测试 MemorySaver)。
- 零迁移(设计 §7):复用 jobs,新增 status="awaiting_input" + JobRepo.set_awaiting,
awaiting 章经 result.awaiting_chapter;新错误码 ErrorCode.CONFLICT(409)。
- project_deps:build_chain_gateway/get_chain_gateway(按请求 tier writer/analyst/light
分派——单档网关恒返该档会错路由 review/digest)+ get_digest_gateway_builder。
单测 apps/api/tests/test_chain.py 12 用例(mock 网关 + MemorySaver + fake session/
accept_op,无 DB/无网络/无真 LLM):run/resume→202、未知 key 404、count 越界 422、
resume 非 awaiting 409、run_chain_job 无冲突→done、冲突→awaiting→resume→done、
错误脱敏、accept_op 冲突缺判→CONFLICT_UNRESOLVED。
门禁绿:ruff/format 干净 · mypy 193 Success · alembic 无漂移 · pytest 600 passed。
守不变量 #1/#3/#4/#5/#9。唯一新增 DDL(langgraph 检查点表)= C3 迁移。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
28 KiB
28 KiB
实现决策记录(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.md:auth/多租户化已 deferred,
users/owner_id是 stub),无公网暴露、无多调用方滥用面;KISS/YAGNI——为零真实风险的场景引入限流中间件 + 新依赖属过早工程。代价:若误暴露到公网,LLM 端点可被滥用刷成本(已记触发条件兜底)。 - 触发(revisit when):引入多租户/鉴权 或 应用对外公开暴露 时,须补轻量限流——按 IP/token 对 LLM 端点 +
/oauth/start+/settings/providers/test加每窗口配额(届时再评估slowapi或反代层限流)。 - 影响:无契约/代码改动;仅本条记录决策与重启条件。
[2026-06-19] Kimi Code OAuth:token 存储包 + 刷新启发式 + 省略 scope + 后台轮询走 jobs — @backend (K1.3)
- 背景:Kimi 订阅 plan 走 device-flow OAuth;需定 token 持久化形、何时刷新、device authorization 是否带 scope、后台轮询如何挂。
- 选择:
- 存储包:
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 结果。 - 刷新启发式:建网关时若 access token 剩余寿命 <
MIN_REFRESH_BUFFER_SECONDS=300(300s 缓冲,对 ~15min access 足够),调refresh换新包并upsert_oauth_credential持久化(下次复用)。任务文档原提max(300, 0.5*expires_in)——本实现采固定 300s 缓冲(不存原始 expires_in,KISS;对 15min access 等价于 0.5*expires_in=450s 略激进但安全)。 - 省略 scope:device authorization 不带
scope(匹配研究确认的 kimi-cli v1.41.0+;server 拒绝时再加scope=kimi-code兜底——本期先不带)。client_id envKIMI_CLIENT_ID可覆盖默认。 - 后台轮询走 jobs:
POST .../start创建jobs(kind="kimi_oauth")返 202 + user_code,复用 K1.1/M4run_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_in(YAGNI)。代价: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 OAuth:device 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-kimiconstants.ts发送之;kimi-cli v1.41.0 已不发但服务端仍接受)。 - 范围:scope 只放 device_authorization 请求;token 交换(
poll_token)/刷新(refresh)不带 scope(OAuth 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/apigeneration.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.tomldeps 需校准——ww_skills直接 importww_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_url,Anthropic/Gemini 传 None 走原生适配器。仅在凭据缺失时返 None(回退链跳过)。保留既有 503/凭据/单 provider 兼容行为。 - 理由 / 取舍:最小改动让回退链支持非 OpenAI-兼容 provider;adapter 选择逻辑收敛到网关包的工厂(单一真源),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_reposdep,不新增 repo、不动 assemble);响应做 T5.2 ingest 形变的逆向解包({"items":[...]}/{"text":...}/{"rules":[...]}→裸 list/str),复用既有_existing_charactershelper。无行返空列表(非 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→ 409CONFLICT_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 形变落写侧 repo(list→{"items"}、arc str→{"text"}、rules→{"rules"})— @backend (T5.2)
- 背景:T5.1 gotcha 指出
CharacterCard.traits/speech_tics(list)/arc(str) 与 DBcharactersJSONB dict 列不同形;WorldEntityCard.rules(list) 与world_entities.rulesJSONB 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}(无指纹→404NOT_FOUND)。(2) 不动 C5 读侧——在写侧SqlStyleFingerprintWriteRepo上加一个latest(project_id)->StyleFingerprintView(含 evidence_json + version 的更全视图),GET /style用它;C5SqlStyleRepo.latest/StyleView(只 dimensions,供 assemble stable_core)保持不变。 - 理由 / 取舍:读写分离 + 各自最小视图(同
DigestAppendRepovs 读侧DigestRepo先例),不破坏 C5 assemble;前端无须靠 jobresult拼凑摘要、可直接拉完整指纹。 - 影响: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)
- 背景:学文风走 jobs(M4-c):
POST /style立即返 202,提取经 BackgroundTaskrun_job在独立 session 上跑(请求 session 已关闭)。但「无凭据→503」需在请求阶段就让用户知道(不能 202 后再静默 fail job)。 - 选择:(1)
work(session)闭包内自建 analyst 网关(build_gateway_for_tier(session, SqlCredentialStore(session), "analyst"))+ 写侧 repo(用run_job传入的独立 session),不依赖 FastAPI dep(dep 绑的是请求 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。 - 选择:定义
StructuredClientProtocol(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 查询触发 autoflush(review_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.beatsDB 列是 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 续接本 router;T3.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(ForeshadowStatusStrEnum +transition/is_overdue/apply_overdue_scan/InvalidTransition)。 - 理由:同 T2.3
DigestAppendRepovs 读侧DigestRepo先例,读写分离免歧义、不破坏 C5。写方法只 flush 不 commit(commit 归 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 要求验收「单事务」含「从终稿提炼 digest(LLM 调用)」,但不能在持开 DB 事务里跨网络调 LLM(占锁)。
- 选择:digest 提炼
extract_digest_facts(tier=light,ChapterDigestFactsschema,经网关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 表逻辑。 - 影响:契约 C3(accept/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,供 assemble);M2 需写侧 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仍自 commit(M1 自动保存语义)。 - 影响: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 才能 import,app/测试/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 执行。
[2026-06-23] @backend C2(多章链)服务/端点接线:零迁移表达 awaiting + 多档分派网关
- 决策 1(零迁移表达 awaiting):链 interrupt 暂停态不新建 jobs 列/枚举——
jobs.status是自由 Text 列(无 DB CHECK),直接新增值"awaiting_input"+JobRepo.set_awaiting;待裁决章号经result.awaiting_chapter表达。jobs.kind="chain"同理零迁移。唯一新增 DDL(langgraph 检查点表)归 C3 迁移。理由:设计 §7 优先零迁移;result 已是 JSONB 足以承载非密进度。 - 决策 2(多档分派网关
build_chain_gateway):一条链 run 跨 writer(write)/analyst(review)/light(digest) 三档,但build_gateway_for_tier(tier)装出的网关chain_resolver恒返该单档链、忽略req.tier——会把 review/digest 错路由到 writer。故链需一个按请求 tier 分派的网关:union 三档所有 provider 适配器 +chain_resolver据req.tier返回对应档链。digest 在 accept 节点自建短事务内按 session 现建 light 档网关(get_digest_gateway_builder)。 - 决策 3(accept_op 在 apps/api 装配):链编排(图/节点)在
ww_core,但具体验收事务(run_accept_transaction/冲突 gate/digest 提炼/伏笔到期扫描)属 apps/api——经build_accept_op闭包注入图节点,守目录所有权 + 不变量 #3/#4。自动链无作者改稿,final_text=chapters该章草稿正文(write 节点所落)。 - 决策 4(新错误码
CONFLICT):resume 非 awaiting 态须 409,但既有 409 码CONFLICT_UNRESOLVED语义专指「未决冲突禁验收」,不宜复用。新增通用ErrorCode.CONFLICT→409(资源状态冲突)。