M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。 M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变); 网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则; 前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。 K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径; coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。 本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400); GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化; 字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。 门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
25 KiB
25 KiB
实现决策记录(append-only)
实现期做出、而规格未覆盖的决策 + 理由。一条一决策,最新在最上。 与规格冲突的不要写这里——去改规格(见 CLAUDE.md「Conventions」)。重大架构选择见
ARCHITECTURE.md §1.2 ADR。
格式:
## [YYYY-MM-DD] <决策标题> — @skill
- 背景:为什么要决策
- 选择:选了什么
- 理由 / 取舍:
- 影响:动到哪些契约/模块/任务
[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 执行。