Files
writer-work-flow/memory/decisions.md
Yaojia Wang 57b3183564 docs: 写作台放置规则入 decisions + 登记 UX P2 完成(P2-3,P0/P1/P2 整套完成,P2-4 延后)
decisions.md 落四象限唯一落点规则(导航→左栏/AI 动作→中央/状态→底栏/参考→右栏)防三重复回潮;PROGRESS 登记 P2-1..P2-3 落地、门禁绿(vitest 716/build OK),P2-4 统一对话面板按计划延后。
2026-07-10 18:14:56 +02:00

32 KiB
Raw Permalink Blame History

实现决策记录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/多租户化已 deferredusers/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_encC2 扩 K1.1 列)。expires_at服务端驱动的绝对 UTC 时刻(从 expires_in 计算入库时刻)。明文 token 绝不进日志/响应/job 结果。
    2. 刷新启发式:建网关时若 access token 剩余寿命 < MIN_REFRESH_BUFFER_SECONDS=300300s 缓冲,对 ~15min access 足够),调 refresh 换新包并 upsert_oauth_credential 持久化(下次复用)。任务文档原提 max(300, 0.5*expires_in)——本实现采固定 300s 缓冲(不存原始 expires_inKISS对 15min access 等价于 0.5*expires_in=450s 略激进但安全)。
    3. 省略 scopedevice authorization 不带 scope(匹配研究确认的 kimi-cli v1.41.0+server 拒绝时再加 scope=kimi-code 兜底——本期先不带。client_id env KIMI_CLIENT_ID 可覆盖默认。
    4. 后台轮询走 jobsPOST .../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 scopeooojustin/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_scopedata["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.domainww_skills/__init__ 做再导出门面(见上 T5.5 决策)。@devops 现已把 packages/skills 做成真 workspace memberww_skills 可 import、有自己的 pyproject、uv sync 完成)——前提消除。
  • 选择:把两个模块物理迁入 packages/skills/ww_skills/{skill_registry,skill_permissions}.py(内部相对 import 从 ww_core.domain.skill_permissions 改为 ww_skills.skill_permissionsww_skills/__init__ 改为直接导出(不再 re-export ww_core删除 ww_core.domain 的两个模块文件 + __init__ 里的 skill 导出。所有 importerapps/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 + sqlalchemySqlSkillReposelect/AsyncSession);改后 uv sync 重建 ww-skills。
  • 影响C8 实现位置更新contracts.md C8 状态加「M5 R3 迁入 ww_skills 包」);消费方 import 路径从 ww_core.domain/ww_core.domain.skill_registryww_skillsww_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=true409 CONFLICT_UNRESOLVED + details{conflicts:[...], conflict_count}(不写库)。作者在前端查看冲突、裁决后重发带 acknowledge_conflicts=true → 放行写库。零冲突 → 直接写库 201。
  • 理由 / 取舍:复用既有 CONFLICT_UNRESOLVED(409) 码(语义=未决冲突禁写入,与 accept gate 一致),无需新增错误码;显式 acknowledge 标志=作者裁决的 HITL gate不静默入库守不变量 #3比「自动返回冲突 + 另开裁决端点」更省(无新端点、无新状态表,原型 KISS。代价作者确认 = 重发一次请求(带 flag可接受。
  • 影响C3 扩POST /characterspartition_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 只含 dimensionsassemble 只需维度文本,不需证据/版本),扩它会牵动 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/stylespec 一致性纪律,@docs 回写。T4.4 前端 FingerprintView 消费此端点。

[2026-06-19] 学文风 work 自建网关+repo不走 FastAPI dep凭据探测前移到请求阶段 — @backend (T4.3)

  • 背景:学文风走 jobsM4-cPOST /style 立即返 202提取经 BackgroundTask run_job独立 session 上跑(请求 session 已关闭。但「无凭据→503」需在请求阶段就让用户知道不能 202 后再静默 fail job
  • 选择:(1) work(session) 闭包内自建 analyst 网关(build_gateway_for_tier(session, SqlCredentialStore(session), "analyst")+ 写侧 reporun_job 传入的独立 session不依赖 FastAPI depdep 绑的是请求 session。(2) 端点仍声明 get_style_extract_gateway(analyst) 依赖仅作凭据探测——无凭据时 dep 解析阶段抛 503job_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] 网关结构化输出经可注入 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。

[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.domainskill_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.domainSkillRegistry/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" 同理零迁移。唯一新增 DDLlanggraph 检查点表)归 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_resolverreq.tier 返回对应档链。digest 在 accept 节点自建短事务内按 session 现建 light 档网关(get_digest_gateway_builder)。
  • 决策 3accept_op 在 apps/api 装配):链编排(图/节点)在 ww_core,但具体验收事务(run_accept_transaction/冲突 gate/digest 提炼/伏笔到期扫描)属 apps/api——经 build_accept_op 闭包注入图节点,守目录所有权 + 不变量 #3/#4。自动链无作者改稿final_text = chapters 该章草稿正文write 节点所落)。
  • 决策 4新错误码 CONFLICTresume 非 awaiting 态须 409但既有 409 码 CONFLICT_UNRESOLVED 语义专指「未决冲突禁验收」,不宜复用。新增通用 ErrorCode.CONFLICT→409资源状态冲突

[2026-06-24] Prompt 外置方案A — @llm/@backend/@devops

  • 背景21 个内置 agent 的 system_prompt 散文内联在 specs.py 的 Python 三引号常量里,改稿要碰 Python、diff 脏、加 agent 要改 4 处。设计真源 docs/design/prompt-management.md
  • 选择:① 散文外置 packages/agents/ww_agents/prompts/<spec.name>.mdsystem_prompt=load_prompt(name) import 期读盘+内存缓存+确定性+fail-fast去 BOM utf-8-sig/LF 归一/NFC/rstrip('\n')无运行期插值——需插值的 prompt 不走此路径,属本波范围外);② 金标准 fixture 取 AST/运行时 system_prompt 值算 sha256tests/fixtures/prompt_hashes.json不取源码文本——因旧常量含反斜杠折行,源码物理换行≠运行时换行;导出时 .md 物理换行≡运行时换行;③ 尾换行方案 B.md 允许 ≤1 尾 LFloader rstrip 不补回,文件层断言守「尾 LF≤1」与运行时金标准双层契约④ Pydantic 类型永留 PythonSCHEMA_CATALOG[name] 是 name→output 唯一真相源(收敛为 dict[name,type|None],去恒 None 的 input 槽YAGNI⑤ 内置 nameREVIEW_RESERVED_NAMES=continuity/foreshadow/style/pace为保留命名空间守卫前移至 SkillRegistry 入库校验(非 resolver 读路径),用户 skill 同名→VALIDATION(安全边界,呼应不变量 #3*_spec 兼容期保留且 SPECS[name] is *_spec(同一实例),编排器 + apps/api 3 路由本波不切 resolver。
  • 理由 / 取舍:非工程改稿不碰 Python、缓存断点前块字节稳定不变量 #9守卫前移让内置 get 纯内存零 DB、确定性、不把安全校验耦合进写章热路径。代价git log -p specs.py 追不到旧 prompt 演进(迁移 commit body 注明),后续改 prompt 须同步重生成金标准 fixture。
  • 影响:加 agent = 写 .md + 注册 SPECS/SCHEMA_CATALOGwheel 必须 force-include/artifactsprompts/*.md@devopsCI 冒烟守);packages/{llm_gateway,core} 补声明 structlog(原直接 import 未声明)。后续波次可将编排器/路由切 SpecResolver 后删 *_spec

[2026-07-10] 写作台功能放置规则(防三重复回潮)— @frontend (UX P2-3)

  • 背景UX 探索报告(docs/design/ux-exploration.md §1.D/§4-P2-3诊断出旧写作台把导航/AI 动作/状态/透明度四类东西散落在左栏 + 顶「AI 工具条」+ 底栏三套面上(「大纲」出现 3 次、「审稿」4 次无法学习。P0/P1 已删顶栏、把 AI 动作收进中央输入条 +「或让 AI:」行、底栏瘦成状态。为防新功能再乱塞,落一条放置规则。
  • 选择:四象限唯一落点——① 导航(去哪个页/章)只进左栏components/NavItems.tsx + lib/nav/items.ts 分组,桌面侧栏/移动抽屉共用,是唯一导航面);② AI 动作(对本章动手)只进中央——主输入条「写本章」发送键 + 其下「或让 AI:」次级动词行(续写/润色选段/整章重写/工具箱);③ 章节状态(字数/保存/流式播报/停/审稿前进 CTA只进底栏;④ 参考/透明度(本章注入、写完检查、去审稿)只进右栏「本章参考」(守不变量 #6「看到的=写章用的」,去术语不删除)。
  • 理由 / 取舍:一条稳定规则让「这个按钮做什么」可从位置预判,消灭「同一标签多处含义相反」(旧「写本章」在顶栏是导航、底栏是生成)。代价:新功能须先归类再落位,偶有归属边界(如工具箱既是左栏页入口、又是中央 AI 动词,两处并存但语义一致——一个是「打开工具箱页」、一个是「对本章调工具箱」)。
  • 影响左栏分组P2-1把 10 项归 写作/资料库/审阅/更多;工具箱在分组清单缺列,收进「更多」以不丢入口。后续任何写作台新功能按本规则四选一落点,不再新开第 4 套面。