Files
writer-work-flow/memory/gotchas.md
Yaojia Wang f6e88d7438 test(qa): AI 对话(聊天记录)E2E + #1/#6 真源围栏守卫(AC-4)
计划 §6/§8。收口 AI-chat-history 功能(AC-1..AC-4 全交付)。

- tests/test_ai_chat_history_e2e.py:真 pg、零 LLM/无网关(纯 CRUD 侧记录),
  覆盖 §6 全 6 用例——缓冲多轮线程一批落库 + GET newest-first/meta 保真、
  作用域分区(本章 ∪ 项目级 NULL)、跨批分页 + kind 过滤、
  append 只写 ai_messages(其它业务表零变 + usage_ledger 恒 0)/GET 只读、
  404 + 五类 422 失败零落库、clarify-abandon(服务端无部分写路径)。
- tests/test_ai_messages_not_in_generation_path.py:AST 静态围栏——
  memory/orchestrator 两目录无一 import/引用 ai_messages,
  MemoryRepos 捆绑与 assemble 形参都不含它;植入违规自证探测器非空,能变红。
- memory/gotchas.md:登记 ai_messages 真源围栏(append-only 侧记录,
  绝不喂 assemble/prompt;接入生成链 = code-review BLOCKER)。

门禁全绿:ruff/format · mypy 238 · alembic 无漂移 · pytest 1002 passed(+10)。
2026-07-09 17:42:45 +02:00

94 lines
52 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
> 实现中发现的坑、易错点、约定俗成——让兄弟 agent 不重复踩。一条一项,最新在最上。
> 只记**非显而易见**的;规格/CLAUDE.md 已写的别重复。
格式:`- [date] @skill <坑/约定> — 缘由 + 怎么做`
---
- [2026-06-20] @frontend **页面重访回显已存内容用 RSC 读 helper`lib/api/server.ts`)作初值种入 client 组件404/错误降级、绝不阻塞进页**(大纲/写作重载):先前大纲页 `initialChapters=[]`、工作台 `useState("")` → 重访空白虽然库里有数据。范式:① **大纲**`fetchOutline(projectId)``getJson<OutlineResponse>``chapters??[]`**try/catch 整体降级为 `[]`**(项目存在无大纲后端返 200 空列表,但任何错误也不该让大纲页 500`useOutline(initial)` 已据 `initial.length>0``status="ready"`,传非空即回显,生成流照常覆盖。② **草稿**`fetchDraft` 直接复用既有 `getJsonOrNull`**404→null**,后端无草稿行正是 404页面 `draft?.content ?? ""``Workbench``initialText`。③ **不要把初值塞进 stream 的 reducer**——`Workbench``useState(initialText)` 起步即可:流式那条 `useEffect` 起始 `stream.state.text` 为空、仅当 `phase∈{streaming,done,aborted}` 且文本变化才 `setText`所以初值不会被空流反扑SSE+AbortController+PUT 自动保存全不用动。④ 测纯加载/降级逻辑:`lib/api/server.test.ts``vi.stubGlobal("fetch", …new Response(JSON, {status}))` + `afterEach(vi.unstubAllGlobals)`node 环境够用,`server.ts` 只依赖 `process.env`/`fetch`),断 200 解包、空/错误→`[]`、404→null。
- [2026-06-19] @qa **Kimi OAuth E2E 要 token 真落 pgoverride `get_session_factory`→`e2e_sm`、但**不能** monkeypatch `kimi_oauth.SqlCredentialStore`K1.3 单测那样会让落库走内存 fake验不到真 pg**K1.5):与 K1.3 单测FakeSession/FakeStore/FakeJobRepo验逻辑不同E2E 要验真持久化——让后台 `work` 自建的 `SqlCredentialStore(session)` + `run_job` 默认 `SqlJobRepo` **保持真**,只 override `get_session_factory``e2e_sm`(后台 work 用真 session 写真 pg。仍须monkeypatch **模块级** `routers.kimi_oauth._default_http_client`→同一 scripted fake后台 work 自建 http 非 dep+ `app.dependency_overrides[kimi_oauth._default_http_client]`→同一 fake端点 device-auth 走 depcall[0]=device-auth、call[1..]=token 轮询顺序共享)+ monkeypatch `routers.kimi_oauth.asyncio.sleep`→no-op。断「加密非明文」`_ACCESS_TOKEN.encode() not in row.oauth_enc`Fernet 密文里找不到明文字节)+ `decrypt_oauth_bundle` 回环。`Gateway` 无公开 adapters accessor → 用 `gateway._adapters[provider]``KimiCodeAdapter._client``.base_url`/`.default_headers`/`.api_key`(同 K1.2 单测。refresh-on-build 直测 `_build_provider_adapter`(非经 HTTP更清晰字符串 target monkeypatch `"ww_api.services.project_deps.httpx.AsyncClient"` + `setattr(project_deps,"kimi_refresh",fake)`。清理:删 `provider_credentials`(kimi-code) + `jobs`(kind=kimi_oauth) + `tier_routing`(project_id 为 NULL 的全局行)。ruff E501 按**显示宽度**算(中文字符占 2 列)——含中文注释/docstring 的行别贴边 100。
- [2026-06-19] @frontend **Kimi Code OAuth 三个端点无 params/body → openapi-fetch 用 `api.POST(PATH, {})` / `api.GET(PATH)`;路径用 `as const` 字面量常量复用**K1.4`POST .../oauth/start``.../oauth/disconnect``GET .../oauth/status` 的生成 schema 全是 `path?: never; ...; requestBody?: never`——`api.POST(START, {})`(传空 init objtypecheck 绿)、`api.GET("/settings/providers/kimi-code/oauth/status")`(无第二参)。把 `start`/`disconnect` 路径抽成模块级 `const START = "/settings/...start"``api.POST` 时,**字符串字面量类型仍被 openapi-fetch 推断为合法 path key**(无需 `as const`,但抽常量去重 DRY。② 连接流复用 `useJobPoll`M4零改`connect``data.job_id``poll.poll(job_id)`;轮询终态用 `useEffect([poll.status])` + `startedRef` 守门(同 `useStyleLearn` 先例,`initialPollState.status` 默认 `"polling"` 不能进页即据此显进度)。③ kimi_oauth job 完成态 result 只 `{connected, provider}`**无 token**)——`jobConnected(job)=job.result?.["connected"]===true`**别**期待/解析任何 token 字段。④ user_code a11y`<output tabIndex={0} className="select-all">`(可读、可全选复制、可聚焦),别用纯 `<span>`。⑤ 档位路由原是只读展示K1.4 改成可编辑per-tier `<select>` provider + model input + 保存 PUT `{tier_routing}`);选 OAuth providerkimi-code`applyProviderChange` 自动套 `defaultModel="kimi-for-coding"`API-key provider 清空 model 待填。
- [2026-06-19] @backend **Kimi OAuth 后台轮询 work 自建 http 走模块级 `_default_http_client()`(非 dep——测试须 monkeypatch 它 + `job_runner.SqlJobRepo`,不能只 override dep**K1.3`POST .../start``http` 是 FastAPI dep端点用但后台 `work``run_job` 独立 session 里调**模块级** `routers.kimi_oauth._default_http_client()` 自建 http请求 dep 已失效——E2E/单测必 `monkeypatch.setattr("ww_api.routers.kimi_oauth._default_http_client", lambda: fake_http)`(同一 scripted fake 实例供端点 device-auth call[0] + work poll call[1...] 顺序共享)+ `monkeypatch.setattr(job_runner,"SqlJobRepo", lambda s: fake_job_repo)`run_job 默认 repo_factory 引模块级 SqlJobRepoFakeSession 无 .execute同 T4.3 gotcha+ `monkeypatch.setattr(routers.kimi_oauth,"SqlCredentialStore", lambda s: shared_store)`work 自建 store 落库)+ override `get_session`→FakeSession/`get_session_factory`→FakeSessionFactory + monkeypatch `routers.kimi_oauth.asyncio.sleep`→no-op不真等 interval 秒。TestClient 同步跑完 background task断言稳定。
- [2026-06-19] @backend **`AsyncHttpClient` 最小 Protocol 故意不含 `aclose`(只 `post`router work 关客户端用 `getattr(http,"aclose",None)`**K1.3service 函数start/poll/refresh只需 `.post`,给 Protocol 加 `aclose` 会逼所有测试 fake 实现它mypy red`httpx.AsyncClient``aclose`,故 router `work``finally``aclose = getattr(http,"aclose",None); if aclose: await aclose()`——既关真客户端、又不污染最小 Protocol、fake 无需实现 aclose。**测试模块级属性 monkeypatch`project_deps.httpx`/`kimi_oauth.asyncio`mypy strict 报 `attr-defined`(模块未 re-export——用字符串 target `monkeypatch.setattr("pkg.mod.attr.sub", val)`mypy 不静态校验字符串)规避,别 `setattr(mod.attr, "sub", val)`。**
- [2026-06-19] @llm **Kimi Code 适配器:伪造头走 `AsyncOpenAI(default_headers=…)`、device_id 走 env-或-`uuid5` 确定性缺省绝不随机UA 验证为 `KimiCLI/1.5` 且参考实现未设 X-Msh-***K1.2):① coding 端点 OpenAI 兼容 → `KimiCodeAdapter` **直接子类化 `OpenAICompatAdapter`**(零重复 complete/stream/结构化逻辑),区别只在工厂构造的客户端带 `default_headers` + coding base_url + access_token 当 api_keySDK 自动发 `Authorization: Bearer`)。② `X-Msh-Device-Id` 必须**跨调用稳定**`kimi_device_id()` = env `KIMI_DEVICE_ID` 优先,否则 `uuid5(NAMESPACE_DNS, "ww.kimi-code.device")` 模块级常量——**别用 `uuid4()` 每次新建**(每次随机 device id 会让 Kimi 侧把每次调用当新设备)。③ **校验对照 `picassio/pi-kimi-coder` `extensions/index.ts`**:它对 coding API 只显式设 `User-Agent: "KimiCLI/1.5"`(精确值,本实现采用)、**没有** X-Msh-* 头——与 PROGRESS K1 契约「缺 X-Msh→403」**不符**。本实现按「UA 必需(已验证)+ X-Msh-* 附带(契约要求、真客户端会发、额外标识无害)」处理;**到底缺 X-Msh 会不会 403 须 K1.3/K1.5 对真 api.kimi.com 联网验证**(含封号风险)。④ 单测断「构造出的 `AsyncOpenAI` 携带正确 `base_url`/`default_headers`/`api_key`」即可,零联网;工厂分派测试访问 `adapter._client` 须先 `isinstance(adapter, KimiCodeAdapter)` 收窄(`build_adapter` 返回 `ProviderAdapter` Protocol 无 `_client`,否则 mypy `attr-defined`)。
- [2026-06-19] @db **K1.1 把 `provider_credentials.api_key_enc` 改 nullable 后,跨边界打穿到 @backend `credentials.py` mypy 报错——这是 K1.3 要吸收的K1.1 不越权修**`models.py``api_key_enc: Mapped[bytes | None]`,但 `apps/api/ww_api/services/credentials.py``StoredCredential.api_key_enc: bytes`dataclass+ `SqlCredentialStore.list_credentials/get_credential``r.api_key_enc`(现 `bytes|None`)→ 2 处 `arg-type` 错。@db 只拥有 `packages/db/`,不改 `apps/api/`(目录所有权)。**K1.3 @backend 行动**`StoredCredential.api_key_enc``bytes | None`(顺带加 `auth_type`/`oauth_enc` 字段 + OAuth 读写mypy 即绿。首个新迁移M1M5 全零建表)= K1 新功能预期内。
- [2026-06-19] @db **新迁移文件 autogenerate 后须手修两处再过 ruff**:① autogenerate 模板把 `from sqlalchemy.dialects import postgresql` **写两遍**(重复 importruff F811/格式炸)→删一行;② 模板用单引号 + import 顺序(`from alembic import op``import sqlalchemy as sa` 前)不合本仓 ruff双引号 + isort→照初始迁移 `220ca2e3d53f` 的样式手排(`import sqlalchemy as sa``from alembic import op``from sqlalchemy.dialects import postgresql`双引号docstring 后空行)。改完跑 `uv run ruff format` + `ruff check` 验。
- [2026-06-19] @llm **真 `anthropic`/`google-genai` SDK 装上后,注入客户端 Protocol 与 SDK 具体类型在 mypy strict 下不互通——在边界 `cast` 解决,别硬改 Protocol**R2 follow-up #2):① `instructor.from_anthropic(self._client)` 的重载只收 SDK 具体 `AsyncAnthropic|...` 联合,`AnthropicClient` Protocol 不匹配 → adapter 内 `cast("AsyncAnthropic", self._client)` 选中 AsyncInstructor 重载(`TYPE_CHECKING` 下 import避免运行时硬依赖 SDK。② `factory.build_adapter` 把**真实** `AsyncAnthropic`/`genai.Client` 传给 adapter 时SDK 客户端的 `messages`/`aio`**read-only 属性**,而 Protocol 成员默认 **settable** → mypy 报「expected settable variable, got read-only attribute」adapter 仅**读取**这些属性,故在工厂里 `cast("AnthropicClient"/"GeminiClient", client)` 跨过约束。**测试替身缝完好**adapter 仍声明 Protocol 形参fake 仍可注入。gemini 无 instructor 那条错(它走 `response_schema` 非 instructor只有 factory 的 read-only 那条。
- [2026-06-19] @backend **Codex 读端点缺口已补(解紧邻下面这条 @frontend gap+ skill impl 迁回 `ww_skills` 包**M5 R1/R3`GET /projects/:id/characters` + `GET /projects/:id/world_entities` 已落(复用 C5 读侧 `SqlCharacterRepo`/`SqlWorldEntityRepo`,反向 JSONB 解包 `{"items":[...]}`→list/`{"text":...}`→str/`{"rules":[...]}`→list无行返空列表@frontend `pnpm gen:api` 后 CodexPage 初始列表可拉**跨会话全量**,去掉「会话内回显」局限。② skill registry/沙箱从 `ww_core.domain` **迁到** `packages/skills/ww_skills/`——`from ww_core.domain import SkillRegistry/SqlSkillRepo/SkillRecord/SkillRepo/partition_writes/filter_reads/validate_declaration/KNOWN_TABLES` **已失效**,改 `from ww_skills import …`。③ 改 `packages/skills/pyproject.toml` deps去 ww-core、补 ww-llm-gateway+sqlalchemy后**必须 `uv sync`** 重建 ww-skills否则按旧元数据解析。④ `build_gateway_for_tier` 现用 `ww_llm_gateway.build_adapter(provider, api_key=…, base_url=…)`(去掉 apps/api 直接 `AsyncOpenAI`/`OpenAICompatAdapter` importanthropic/gemini 传 base_url=None 走原生适配器。
- [2026-06-19] @frontend **【RESOLVED 2026-06-19】Codex 读端点缺口已闭合M5 R1 follow-up #1**@backend`GET /projects/:id/characters` / `GET .../world_entities`C3 扩,反向 JSONB 解包成 API 友好字段)后,前端 `gen:api` 纳入 `CharacterListResponse{characters?:[CharacterCardView]}` / `WorldEntityListResponse{world_entities?:[WorldEntityCardView]}`+`lib/api/types` 别名);`lib/api/server``fetchCharacters`/`fetchWorldEntities`(无行→空列表,非 404仿 `fetchForeshadow`codex routeServer Component`Promise.all` 拉项目+人物+世界观全量传初始数据;`CodexPage` 渲染**跨会话持久化真源**(刷新不再空),本会话新入库卡经 `mergeCharacterCards`(纯逻辑,按 name 去重、持久化优先、node 单测)合并补显(不与真源重复)。世界观本期无入库端点,故只渲染真源 + 生成器预览(无 session 合并)。**「会话内回显」局限已去除**。原 T5.3「生成+本次入库会话内回显」管理入口仍保留generate→ingest 后即时见新行)。
- [2026-06-19] @frontend **入库 409 冲突详情形 = accept gate 的 `details.conflicts`ReviewConflict 形),不是 missing_conflict_indices**T5.3`POST /characters` 的 409 CONFLICT_UNRESOLVED 信封 `details{conflicts:[{type,where,refs,suggestion}], conflict_count}`continuity 预检产出C3 扩 T5.2),区别于 accept 的 `details{missing_conflict_indices, conflict_count}`(裁决覆盖缺口)。前端 `extractIngestConflicts``error.code==="CONFLICT_UNRESOLVED"` + 收窄 `details.conflicts`(复用 `lib/review/sse``ReviewConflict` 形 + history.ts 同款安全收窄);裁决流 = 展示冲突 → 作者确认 → 带 `acknowledge_conflicts=true` 重发(非逐条 conflict_index整体放行对齐后端 gate 语义)。错误码同款 503 LLM_UNAVAILABLE 引导去设置(仿 useRefine
- [2026-06-19] @frontend **命令面板⌘K走全局 keydown + RootLayout 挂载,纯过滤/高亮逻辑抽 `lib/command/palette` node-env 单测**T5.6`CommandPaletteMount`client`usePathname` 注入项目上下文 → `projectIdFromPath` 正则抽 `/projects/<id>`;命令清单(导航 + 生成动作)+ `filterCommands`(title/keywords 大小写无关子串) + `moveHighlight`/`clampHighlight`(循环) 全是纯函数、node 单测。组件层只管 ⌘K/Ctrl+K toggle`window.keydown`)、焦点(打开 `inputRef.focus`、Esc/↑↓/Enter、a11yrole=dialog/listbox/option + aria-selected + motion-safe。生成动作生成角色/世界观)= 跳 `codex?gen=character|world`CodexPage 读 searchParams 直开对应 tab无需独立 modal 路由)。
- [2026-06-19] @backend **测 `build_gateway_for_tier` 多 provider 接线要用真 Fernet key不是 `"x"*44`**T5.2):该函数解密凭据建适配器,凭据是 `encrypt_api_key(..., key=CREDENTIAL_ENC_KEY)` 加的密key 必须是合法 Fernet32 字节 url-safe base64否则 `decrypt_api_key``CredentialKeyError`。生成一个:`uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`,设 `os.environ["CREDENTIAL_ENC_KEY"]=<key>` + `get_settings.cache_clear()`lru_cache。生成/入库**端点**测试不碰真解密override 网关 dep 注 schema-routing fake故那些仍可用 `"x"*44`;只有直接调 `build_gateway_for_tier` 的单测需真 key。
- [2026-06-19] @backend **生成/入库/precheck 端点测试用 schema-routing fake 网关(按 `req.output_schema` 返不同 parsed**T5.2):一次 ingest 请求触发 precheck`output_schema=ContinuityReview`world/character generate 各触发 `WorldGenResult`/`CharacterGenResult`——单一固定-parsed 的 `FakeReviewGateway` 不够。端点测试自带 `_SchemaRoutingGateway({Schema: instance})``run``req.output_schema` 路由,未命中返 None三个网关 depworldbuilder/character_gen/precheckoverride 成同一个。**生成预览端点也要 `commit()`**(网关 ledger add-only → 不 commit 则 usage 静默丢,同 draft 坑)——断言 `session.commits==1` 即便预览不写业务表;冲突 409 路径同样 commit 落 precheck usage。
- [2026-06-19] @llm **回退/熔断要让适配器把瞬时故障翻译成 `TransientProviderError`,否则网关只重试/回退它认得的错**T5.4`Gateway` 的重试谓词 `_is_retryable` 只认 `TransientProviderError``AppError(RATE_LIMITED)`;普通 `Exception`/厂商原生异常**不重试不回退、直接上抛**内容策略拒绝等本就该让作者知情§4.5)。故每个适配器在 `complete`/`stream``except` 里按异常类名(RateLimitError/APITimeoutError/APIConnectionError/InternalServerError/APIStatusError)+`status_code`(429 或 ≥500) 判定瞬时→包成 `TransientProviderError(provider=...)`。**新增适配器必须照做**,否则它的 429/5xx 会穿透回退链变成硬失败。`packages/llm_gateway/tests/fakes_resilience.py``ScriptedAdapter(failures=[...])``TransientProviderError` 模拟可回退失败、用 `AppError(其它码)` 模拟不可回退。
- [2026-06-19] @llm **`Gateway` 构造签名扩了但保留 `resolver=` 兼容——apps/api 的 `build_gateway_for_tier` 无需改即不破,但也因此默认不启用回退链**T5.4`Gateway(adapters, ledger, *, chain_resolver=None, resolver=None, max_retries=2, breaker=None)``resolver`(单路由) 会被自动包成单元素链,故 `project_deps.build_gateway_for_tier` 仍传 `resolver=resolve_route` 照常工作(**未回归**)。但单元素链=无回退——**真正启用回退须 apps/api 改 `build_gateway_for_tier`**:按 DB `tier_routing.fallback`(`StoredRouting.fallback` 已有) 预备多个 provider 适配器进 `adapters` dict + 传 `chain_resolver=lambda tier: chain_from_routing(tier, primary, fallback)`,并可注入跨请求共享的 `CircuitBreaker`(默认每个 Gateway 实例自带一个、进程内不共享,逐请求新建网关则熔断不跨请求累积)。Anthropic/Gemini 适配器真实接线还需 @devops`anthropic`/`google-genai` SDK 依赖(适配器本身懒导入/注入客户端,仅 apps/api 构造真实 client 时需要)。
- [2026-06-19] @llm **character-gen schema 的 `traits`/`speech_tics` 是 `list[str]`,但 DB `characters.traits`/`speech_tics`/`arc` 列是 JSONB **dict**T5.2 ingest 须转形)**T5.1C6 扩 `CharacterCard``traits`/`speech_tics` 设计成 `list[str]`(生成产物天然是列表)、`arc` 设计成 `str`(弧光一句话),但 `packages/db/ww_db/models.py` 的列类型是 `traits: JSONB dict` / `arc: JSONB dict` / `speech_tics: JSONB dict`,而 `tags`/`relations` 才是 JSONB **list**。T5.2 入库映射时list 字段tags/relations直落`traits`/`speech_tics` 须包成 dict`{"items":[...]}` 或按语义拆)或由 @db 调列形;`arc`(str) 同理包 dict。**别假设 schema 字段形 == DB 列形**——schema 贴生成产物、DB 列贴存储ingest 是转换层。`role`/`backstory` 是 Text 列、直落。`WorldEntityCard.rules:list[str]``world_entities.rules` JSONB`{"rules":[...]}` 或裸 list
- [2026-06-19] @llm **入库前 continuity 校验是「编排器追加的一道检查」,复用 `continuity_spec` 而非新建 spec**T5.1ARCH §6.5`precheck_generated_cards` 传入 `continuity_spec`(不是 character-gen 自调、不是新审种),跑生成角色卡 vs 世界观/已有角色真相源、返 `list[Conflict]`。守不变量#1agent 只经 DB/编排器通信、不互调。T5.2 入库端点拿 conflicts 做 gate有冲突→提示作者裁决/调整,**不静默入库**(同 accept 的冲突 gate 精神,但这是生成预览阶段、非 chapter accept。独立生成语义网关失败直接上抛端点处理不做 review 图的失败隔离)。
- [2026-06-19] @qa **学文风 E2E后台 work 自造网关须 monkeypatch `style.build_gateway_for_tier`(不是 override 网关 dep**T4.5`POST /style` 请求阶段的凭据探测走 `get_style_extract_gateway`(可 `dependency_overrides` 注假网关绕过 503但**真正提取**在 `run_job` 自建的独立 session 上跑 `_make_style_learn_work`→其内 `build_gateway_for_tier(session, store, "analyst")` 从凭据建**真 OpenAI 适配器**(与请求 dep 无关。E2E 无真凭据 → 必 `monkeypatch.setattr(ww_api.routers.style, "build_gateway_for_tier", 返真 Gateway 包假适配器)`ledger 绑入参 session=后台真 session。写侧 `SqlStyleFingerprintWriteRepo` + `run_job` 默认 `repo_factory`→真 `SqlJobRepo` **保持不动** → 指纹/job 真落 pg只换 LLM。同 M3override `get_session_factory``e2e_sm`(同测试 engine/loopASGITransport 下 `await client.post` 等 background task 跑完,轮询 `GET /jobs/{id}` 稳定见 done。第四审 E2E只 override `get_review_gateway`analyst即覆盖整个四审图四档位同 deepseek 单适配器),假适配器据 `req.output_schema is StyleDriftReview` 返漂移 parsed回炉假适配器据 `req.output_schema is None` 返改写串writer 纯文本,保证 refined≠original
- [2026-06-19] @frontend **`GET /jobs/{id}` 在 OpenAPI 是裸 `{[k]:unknown}`T0.3 未建命名 schema**T4.4):前端无法直接用生成类型当 JobView须在 `lib/jobs/job.ts` 安全收窄(`narrowJob`status 非法→`queued`、progress 夹取 0..100、result 仅取 object 非数组)。同理 `chapter_reviews.style` 是松散 `{[k]:unknown}|null``normalizeStyleDrift` 收窄成 `{score(缺省100),segments:[{idx,score(缺省100),label:string|null}]}`score 缺省 100 = 后端无指纹降级态)。轮询 reducer/收窄是纯逻辑、node-env 单测hook 只管 setTimeout+fetch。
- [2026-06-19] @frontend **`useJobPoll``initialPollState.status` 默认 `"polling"`,调用方别在挂载即据此显进度**T4.4`useStyleLearn``startedRef` 守门——只在提交过一次 `POST /style` 后才把 `poll.status` 映射进 UI否则文风页一进就误显「提取中…」。轮询「停止」靠 reducer 到 done/failed 终态后不再 `setTimeout`(无 abort 概念,区别于 SSE 的 AbortController。回炉 `useRefine` 503(`error.code==="LLM_UNAVAILABLE"`)→提示去设置,仿 review 流前 503 检出。
- [2026-06-19] @backend **测 `run_job` 后台路径要 monkeypatch `job_runner.SqlJobRepo` 而非 `_default_repo_factory`**T4.3`run_job(..., repo_factory=_default_repo_factory)``repo_factory` 是**默认参数值**,在函数**定义时**绑定——`monkeypatch.setattr(runner_mod, "_default_repo_factory", ...)` 改的是模块属性、改不到已绑定的默认值,无效。`_default_repo_factory` 体内 `return SqlJobRepo(session)` 引用的是**模块全局** `SqlJobRepo`,故 monkeypatch `runner_mod.SqlJobRepo` 才生效(否则后台 `set_running``SqlJobRepo(FakeSession)``.execute` 炸、被 run_job 吞成 job_failed。端点测 BackgroundTask 落库路径override `get_session_factory``FakeSessionFactory` + monkeypatch `style.build_gateway_for_tier`(产 StyleFingerprintResult)/`style.SqlStyleFingerprintWriteRepo`(指共享 fake repo) + `runner.SqlJobRepo`(指 fake job repo)ASGITransport 下 `await client.post` 会等 background task 跑完,断言稳定(同 run_overdue_scan 时序先例)。
- [2026-06-19] @backend **bulk `UPDATE` 取受影响行数要 `cast` 成 `CursorResult`**T4.1 `reap_zombies``await session.execute(update(...))` 静态类型是 `Result[Any]`**无 `.rowcount`** 属性mypy strict 报 `attr-defined`);实际运行时是 `CursorResult`。做法:`from sqlalchemy import CursorResult` + `cast("CursorResult[Any]", result).rowcount``reap_zombies` 用一条 bulk UPDATE非逐行`running→failed` 高效且原子。
- [2026-06-19] @backend **`run_job` 失败置态要开「全新」session**T4.1`work` 抛异常后,原 session 的事务已作废,不能复用它 `fail(job_id, ...)`——`_mark_failed``session_factory()` 再开一个独立 session 写 failed + commit。故 `run_job` 失败路会开 **2 个** session业务一个 + 失败置态一个),单测断言 `len(factory.sessions)==2``run_job` 对 repo 只依赖最小 `JobLifecycleRepo` Protocolset_running/complete/fail——比全 `JobRepo` 窄,单测 fake 不必实现 create/get/reap`run_overdue_scan` 用最小 `OverdueScanRepo` 先例)。
- [2026-06-18] @llm **`SqlAlchemyLedgerSink.record` 改 add-onlyT3.8 修,去 `await flush()`**:原 add+flush 在并行审里 flush 重入炸(见下条)。改为只 `session.add(row)`——`add()` 同步不让步→并行协程不交错;持久化靠端点/事务 `commit()`(自动 flush。draft/review/accept/outline 端点末尾均已 commit记账不丢。**凡新增「并行用网关产 usage」的路径sink 必须保持 add-only不得在并行段 `await flush`。** 旧 gotcha「record 只 flush 不 commit」措辞已过时——现为「add-onlycommit 归调用方」commit 仍会 flush故记账语义不变
- [2026-06-18] @qa/@llm **并行审记账撞 sessionM3 真 bugT3.7 暴露→T3.8 修)**:三审同 LangGraph superstep 并行,各自 `gateway.run()`→共用**请求 session** 的 `SqlAlchemyLedgerSink.record`(`session.add`+`await session.flush()`)。`AsyncSession` 非并发安全→第二/三审 flush 撞 `Session is already flushing`→被 `run_review` 失败隔离吞成 `incomplete`→**foreshadow/pace 静默丢失**SSE 无事件、`chapter_reviews.foreshadow_sug`/`pace` 列空、日志 `review_node_incomplete error='Session is already flushing'`。M2 单审未触发。根因=并行路径里有 `await flush()`(唯一 await 的 DB-IO。修向审稿期记账避免并发 flushadd-only 靠端点 commit / 或缓冲后 collect 串行落 / 或并行审记账用独立 session
- [2026-06-18] @qa **E2E 验「验收后 BackgroundTask 扫描」时序**httpx `ASGITransport``await client.post(...)` 会等 ASGI app 协程(含 Starlette background tasks跑完才返回故 client 上下文退出后断言稳定不 flaky必 override `get_session_factory``e2e_sm`(真 sessionmaker同测试 engine/loop否则默认 `get_sessionmaker()` 另建 engine 绑别的 loop。
- [2026-06-18] @frontend **审稿 seed 扩成三态**`useReviewStream.seed` 入参由 `ReviewConflict[]``ReviewSeed{conflicts,foreshadow,pace}`(进页同时种伏笔建议/节奏留痕,免重审即可看)。`ReviewStreamState``foreshadow:ForeshadowSuggestion[]`(累加) / `pace:PaceReport|null`(**替换非累加**,对齐后端 collect「pace 整 dict 入列」)。
- [2026-06-18] @frontend **伏笔 transition 乐观更新只改 status、回滚存快照**`useForeshadow.transition` 本地先改 statusPATCH 失败 `setItems(snapshot)` 回滚 + 读 `error.details.reason`(duplicate/invalid_transition/empty_update) 映射文案。register **不乐观**(要服务端 code 唯一校验):成功才追加返回行,重复 code→422 友好提示不改 items。大纲页**无 GET 端点**:进页 `initialChapters=[]`,靠 `POST .../outline` 生成填充无凭据→503 引导去设置)。
- [2026-06-18] @backend **端点级测「无凭据→LLM_UNAVAILABLE」不要靠真实 `get_*_gateway` dep 解析**——`FakeSession` 不支持 `.execute``SqlCredentialStore` 会炸成 500。做法override 网关 dep 注一个 `raise AppError(LLM_UNAVAILABLE)` 的 async 函数(等价无凭据行为),与 review/accept/outline 测试一致。
- [2026-06-18] @backend **BackgroundTask 必须自建独立 session**FastAPI BackgroundTasks 在 response 发回、请求 session 关闭后才跑——复用 `Depends(get_session)` 的 session 已关闭会炸。验收后到期扫描经可注入 `SessionFactory`(=`get_sessionmaker()`)`run_overdue_scan``async with factory()` 开新 session 自己 commit再加 `repo_factory` 缝便于单测注 fake、纯函数 await 不起后台线程。**accept 端点新增 `get_session_factory` 依赖→所有 accept 测试 client 需 override 它**(否则 dep 解析会建真 engine
- [2026-06-18] @backend **伏笔登记重复 code / 非法转移 → `VALIDATION`(422) 非 409**:现有唯一 409 码 `CONFLICT_UNRESOLVED` 专指未决冲突禁验收,不复用;`details.reason``duplicate`/`invalid_transition`/`empty_update` 供前端区分。register 捕 SQLAlchemy `IntegrityError`→rollback→`AppError(VALIDATION)`transition 捕 `InvalidTransition`/`LookupError`(404)。
- [2026-06-18] @llm **三审列类型不齐**`chapter_reviews.foreshadow_sug` 是 JSONB **list**`pace` 是 JSONB **dict**——collect 把 `ForeshadowReview{planted,resolved}`(dict) **扁平成单 list**、每条加 `kind:"planted"|"resolved"``PaceReview` 整体入 dict 列。贴合既有 DB 列类型、不改 @db。新审种加入须同步collect 列映射 + `sse._section_result_events` 分支 + `normalize_review` 键名。`sse.py` import `.collect` 的 spec.name 常量做 section 分流无循环collect 不 import sse
- [2026-06-18] @llm **三审并行图测试按 `req.output_schema` 路由 parsed**`SchemaRoutingRunGateway`)——单 `FakeRunGateway` 对所有审返同一 parsed 会让三审拿错 schema。验失败隔离某 schema 不登记→网关抛 KeyError→`run_review` 隔离为 `incomplete`,无需改 gateway。
- [2026-06-18] @backend **伏笔 `record_progress` append JSONB 必须新建 list 重赋值**`row.progress = [*old, entry]`),不可原地 `.append()`——SQLAlchemy 默认不侦测可变 JSONB 原地突变原地改不脏标记→flush 丢失。`scan_overdue` 仅在有变更时 flush空扫描零写。状态机`transition` 同态(current==to)幂等放行、CLOSED 为终态(离开 CLOSED 全非法抛 `InvalidTransition`)`is_overdue` 严格大于(current==expected_close_to 仍在窗口内不逾期)、无 expected_close_to 永不逾期。
- [2026-06-18] @llm **orchestrator 内每模块各自声明 `GatewayRun` Protocol**`review_node.py``outline_node.py` 各一份,按模块最小依赖)——**不跨模块复用、不在 orchestrator `__init__` 重复导出**`__init__` 只导出 review_node 那个,避免 re-export 名冲突outline 的为模块内部用。
- [2026-06-21] @llm **【撤销上条】`GatewayRun` 已收敛为单点定义**`orchestrator/_protocols.py`,仅依赖 `ww_llm_gateway.types` 无环)→ review/generation/outline/style_extract 4 节点 + graph/`__init__` 统一 `from ._protocols import GatewayRun`,删除 4 处重复声明CODE_REVIEW P2 DRY。上条「每模块各自声明、不复用」的理由怕环/re-export 冲突)不成立——单点 Protocol 无环、`__init__` 单点导出无冲突。
- [2026-06-18] @qa **M2 E2E 多档位假适配器**`config.tier_defaults` writer/analyst/light 默认同 provider(deepseek)→单个假适配器(`provider="deepseek"`)即覆盖三档位;据 `req.output_schema is ContinuityReview`(续审)/否则 digest facts schema 分支返回 `parsed`;三档位用不同 `input_tokens` 区分以断言各自落 `usage_ledger`。三端点记账闭环 = review 端点流末 commit + accept 验收事务末 commit 都把网关 ledger flush 真正提交M1 ledger bug 在 M2 无复发)。
- [2026-06-18] @qa **E2E 验证「digest 从终稿非草稿」(#4) 手法**final_text 注入草稿没有的标记串,假 light 适配器把它放进 digest facts 的 `summary`,断言 `chapter_digests.facts["summary"]==标记``标记 not in draft_text`。accept 409 gate 经 ASGITransport 正常返回(`AppError` 不上抛),断言 `resp.json()["error"]["details"]["missing_conflict_indices"]``ErrorCode` StrEnum → `"CONFLICT_UNRESOLVED"`)。
- [2026-06-18] @frontend **审稿历史 `conflicts` 在 OpenAPI 被标松散 `{[k]:unknown}[]`**(后端用 dict/JSONB 列)→ 前端 `lib/review/history.ts` 安全收窄成 `ReviewConflict{type,where,refs,suggestion}`,缺字段给默认、**保序**(顺序=冲突 gate 的 `conflict_index` 身份,不可重排,否则裁决错位)。
- [2026-06-18] @frontend **审稿页重审完成重置裁决草稿用 streaming→done 边沿判定**`wasReviewingRef`):不能用 conflicts 长度变化判(同数不同组会漏重置),也不能在 seedphase=idle进页种历史留痕时误触发。
- [2026-06-18] @backend **冲突 gate 判据accept**:冲突身份 = 「最近一条 `chapter_reviews.conflicts` 列表的下标」;裁决 `ConflictDecision{conflict_index, verdict:accept|ignore|manual, note?}`gate 通过 = 裁决的 `conflict_index` 集合**覆盖** `range(len(conflicts))`缺判→409 `CONFLICT_UNRESOLVED` + `details.missing_conflict_indices`/`conflict_count`;无审稿留痕或零冲突→直通验收。
- [2026-06-18] @backend/@qa **ASGITransport 默认 `raise_app_exceptions=True`**accept 事务回滚测试里repo 抛的非-`AppError`(如 `RuntimeError`)会**上抛到 client 调用方**而非返回 500——测试用 `pytest.raises(RuntimeError)` 包住请求调用、再断言 `session.commits == 0`证明未部分提交。DB 级原子回滚由 T2.7 真 pg 覆盖。
- [2026-06-18] @llm **langgraph 并行节点同写一个 state key 必须配 reducer**:并行四审同 superstep 各写 `{spec.name: ...}``reviews`,无 reducer → LangGraph 抛 `InvalidUpdateError`。用 `Annotated[dict, merge_reviews]`(浅合并、返回新 dict、不可变`ChapterState` 因逐节点填充改 `total=False`
- [2026-06-18] @llm **langgraph `add_node` 重载拒收显式 `Callable` 类型别名**`make_review_node` 返回的具名 `BoundReviewNode` 别名会让 mypy 报 incompatible arg-type。图工厂里改用 **inline `async def` 闭包**spec 经默认参 `_spec=spec` 绑定避开循环晚绑定mypy 才推出精确函数类型匹配重载。`make_review_node` 仍作公共缝(供 T2.5 跑单审)单独保留+单测。
- [2026-06-18] @llm **审稿失败隔离两层**:审级失败在 `run_review` 内标 `incomplete`§5.2 任一审不阻塞其余);归一级意外(畸形 entry`normalize_review` 发一条 `error` 事件后收尾(同 `normalize_deltas` 纪律)。
- [2026-06-18] @llm **instructor 1.15.3 结构化输出接线**:用 `AsyncInstructor.create_with_completion(messages=..., response_model=..., model=..., max_tokens=...)` 同时拿 `(parsed, raw_completion)`——`raw.usage` 用于记账,避免结构化路径丢 usageusage 提取统一走 `_usage_from(raw_usage)`(文本/结构化/流共用。adapter 经可注入 `StructuredClient` Protocol 注 fake测试不联网。结构化路径 `ProviderResult.text = parsed.model_dump_json()`(日志/留痕),消费走 `parsed`。**带 `output_schema``gateway.run(req).parsed` 必非 None**。
- [2026-06-18] @backend **frozen View 的运行时不可变断言因类型分两种**dataclass frozen(`ChapterView`)→赋值抛 `FrozenInstanceError`**mypy 会静态报错**(测试里故意赋值需 `# type: ignore[misc]`Pydantic frozen(`DigestView`/`ReviewView`)→抛 `ValidationError`**mypy 不静态校验****勿加** `# type: ignore`,否则被判 unused-ignoreruff/mypy 红)。
- [2026-06-18] @backend **M2 验收-side repos 只 flush 不 commit**`promote_to_accepted`/`digest.append`/`review.record`/`set_decisions` 均只 `flush()`,交由 T2.4 验收事务单次 `commit()`(对齐「写库副作用在事务/编排层」);唯独 draft `save_draft` 仍自 commitM1 自动保存语义)。`(project_id,chapter_no,version)` 唯一性是 **DB 级**(T0.2 models 定义),纯 fake 单测不断言它(不引 pg 依赖以免 pytest 门禁需起库)→ 由 T2.7 E2E 真实 DB 覆盖。
- [2026-06-18] @backend/@llm **网关 ledger 只 flush、调用方必须 commit**(不变量「写库副作用在编排层不在网关」的代价):`SqlAlchemyLedgerSink.record``flush()``commit()``get_session` 退出时不提交 → 隐式回滚。draft SSE 端点曾因此把 `usage_ledger` 行丢掉T1.9 暴露)。修复:端点在 SSE 流**耗尽后** `await session.commit()`FastAPI 缓存 `Depends(get_session)`,网关 ledger 与端点同一 session。**M2 起凡用网关产 usage 的路径(四审/accept都要确保所在事务最终提交**,否则记账静默丢失。
- [2026-06-18] @frontend **Next 里消费 SSE 用 `fetch`+`ReadableStream` reader不用 `EventSource`**EventSource 不能 POST、不能干净 abort。"停"=`AbortController.abort()`,吞掉 `AbortError`、已收 token 留在 state 并被自动保存。流前错误无凭据→503 `LLM_UNAVAILABLE`**JSON 信封非帧**)经 `!res.ok` 检出、从 `{error:{code,message}}` 解析。
- [2026-06-18] @frontend apps/web 测试环境坑:`server-only` 包未装→别 importServer Component 仅靠约定vitest 是 **2.x**(无 `toHaveBeenCalledExactlyOnceWith`,用 `toHaveBeenCalledTimes`+`toHaveBeenCalledWith`**未装 jsdom/testing-library**→单测走 node env 测纯逻辑SSE reducer/帧缓冲、debounce、向导状态机组件 DOM 渲染留给 T1.9 Playwright。
- [2026-06-18] @backend **stub user 未 seed → FK 风险**`projects.owner_id` / `usage_ledger.owner_id` / `provider_credentials.owner_id` 全 FK→`users.id`,但仓库无 seeded stub user。约定 `STUB_OWNER_ID = uuid.UUID(int=1)`(对齐网关 `Scope.user_id` stub。任何写这些表的路径写章记账、立项、存凭据跑前必须存在该 user 行——T1.4 幂等 seedstartup/lifespanauth 落地后替换为真实 principal。
- [2026-06-18] @backend 含 nullable 列的唯一约束别用 PG `ON CONFLICT``provider_credentials(owner_id,project_id,provider)` / `tier_routing(project_id,tier)``project_id` 可空PG 默认 NULLS DISTINCT → 全局行(`project_id=NULL`)的 `ON CONFLICT` 不去重、会插重复。T1.7 用显式 read-modify-write(`project_id IS NULL`)。若 @db 后续给约束加 `NULLS NOT DISTINCT` 可改回原生 upsert。
- [2026-06-18] @llm langgraph **1.2.5** 实装pyproject 写 `>=0.2.40` 但装了 1.x用 1.x API`from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver``AsyncPostgresSaver.from_conn_string(...)`(async ctx) → `await saver.setup()`(只在 migrations/CI。mypy strict 下:`add_node` 不收 `functools.partial`(用 `async def` 闭包绑定依赖);`StateGraph[...]`/`CompiledStateGraph[...]`/`BaseCheckpointSaver[Any]` 需写全类型参;测试 state dict 标注 `: ChapterState``ainvoke` config 标注 `: RunnableConfig`
- [2026-06-18] @orchestrator **跨包测试同名碰撞**:每包 `tests/`__init__(避免与顶层 `tests` 包撞),但多包并存时 ① pytest 全跑:同名顶层模块 `fakes.py` 撞("import file mismatch")→ 测试替身用**全局唯一**名(`fakes_gateway`/`fakes_providers`/`fakes_orchestrator`);② 聚合 `mypy packages apps`:多个 rootless `conftest.py` 撞成同名模块 → root pyproject `[tool.mypy] exclude=["(^|/)conftest\\.py$"]`conftest 仅 fixtures按包仍受检。test_*.py 保持全局唯一名。
- [2026-06-18] @backend Fernet 凭据 key 取 `settings.credential_enc_key`(env `CREDENTIAL_ENC_KEY`)`get_settings` 是 lru_cache测试改 env 后须 `get_settings.cache_clear()`(在 client fixture 里。list/GET 不解密(只回脱敏占位),仅 probe 按需解密——缩小明文暴露面。
- [2026-06-18] @orchestrator 后台 fork 子代理会因 "stream idle timeout" 早夭T1.1 网关 fork 跑了 8.5 分钟、0 产出、0 文件)。坑:别盲等/盲重启重型 fork。做法fork 完成后先核对**产物文件 + 自跑门禁**再认其结果;早夭则编排者内联实现该任务(已有完整上下文),不再二次 fork 同一关键路径任务。
- [2026-06-18] @backend 记忆选择走**确定性子串名匹配**(在 flatten+sorted 的 beats/facts 文本上 + 显式 entities 列表),非 pg_trgm/向量§3.4 的 pg_trgm/作者 pin 兜底属后续。`selection``render_cards` 均按 `(kind, name)` 排序→输出与 repo 返回顺序无关CJK 名按 **codepoint** 排(乙 U+4E59 < U+7532测试断言按 codepoint 而非甲乙丙语义`latest_state` 只进 `volatile`卡片`stable_core` 故意不含它以保缓存前缀字节稳定
- [2026-06-18] @qa/@llm 包内单测放 `packages/<pkg>/tests/`** __init__.py**避免与顶层 `tests` 包同名冲突测试替身放独立 `fakes.py` 用绝对导入 `from fakes import ...`不能 `from .conftest import`相对导入在无包目录下报 no known parent package`conftest.py` 只放 fixtures门禁按包跑`uv run {ruff check|mypy|pytest} packages/<pkg>`
- [2026-06-18] @frontend pnpm 11 配置已迁出 package.json/.npmrc 只读 `apps/web/pnpm-workspace.yaml``pnpm run <script>` 前会跑 verifyDepsBeforeRun 触发隐式 install `ERR_PNPM_IGNORED_BUILDS`(esbuild/sharp/unrs-resolver 被默认拦截)直接整条命令失败做法 `pnpm-workspace.yaml` `onlyBuiltDependencies:` 白名单 + `verifyDepsBeforeRun: false`
- [2026-06-18] @frontend gen:api 离线管线`scripts/gen-api.mjs` `execFileSync(uv, [...])` 取后端 OpenAPI(不靠运行中的服务) + 直接调 `node_modules/.bin/openapi-typescript`(别用 `pnpm exec`会再触发 deps 检查)。改后端 schema 后跑 `pnpm gen:api` 重生成 `lib/api/schema.d.ts`
- [2026-06-18] @backend async engine 跨事件循环坑`get_sessionmaker` `lru_cache`engine 绑定首个 looppytest-asyncio 每测试新 loop 复用会报 `Connection._cancel never awaited`/连接失败测试里每个 DB 测试 `get_sessionmaker.cache_clear()` 在当前 loop 重建并 `dispose()`
- [2026-06-18] @db mypy strict + 跨包 editable 安装模型里 `from ww_db.base import Base` 会被判成 Any( "cannot subclass Any")需在根 `pyproject.toml` `[tool.mypy] mypy_path=[...各包源码目录...]` + `namespace_packages=true`否则 editable 包解析不到源码
- [2026-06-17] @docs 命名契约后端 Python/Pydantic 一律 **snake_case**字段/schema/JSON前端经 OpenAPI 生成类型消费——别手写 camelCase 共享类型评审里曾因 TS 旧栈遗留 camelCase Pydantic 契约对不上)。
- [2026-06-17] @docs 数据写入只走**验收事务**四审 agent 只读不写任何 AI 产出入库必经 `accept`HITL gate)。别在 agent 节点里直接写库
- [2026-06-19] @qa M5 E2E生成端点的网关`get_worldbuilder_gateway`/`get_character_gen_gateway`/`get_precheck_gateway`**请求 scope FastAPI 依赖**内部虽调 `build_gateway_for_tier`但作为 `Depends` 暴露)→ E2E 直接 `app.dependency_overrides[get_*_gateway]=真Gateway包假适配器` 即可**无需** monkeypatch `build_gateway_for_tier`区别于 M4 学文风那是 `run_job` 后台**自建**网关 monkeypatch 模块级 `style.build_gateway_for_tier`)。
- [2026-06-19] @qa `served_by`(fell_back/degraded) **不出 API**仅观测/记账标注)→ 端到端 HTTP 路径要证回退真发生」,断言 **DB 真源 `usage_ledger.provider` = fallback 名而非 primary**网关记账记实际服务方ARCH §4.5)。HTTP fallback 用真 `Gateway` + `chain_resolver=chain_from_routing(...)` + primary 假适配器每次 `complete` `TransientProviderError` max_retries+1 次失败耗尽重试)→ fallback能力降级路径无支持者经直测 `Gateway.run` `served_by.degraded` 更清晰
- [2026-06-19] @qa `ww_agents.Conflict.type` `ConflictType` **Literal**性格漂移/能力不符/设定违例/地理矛盾/时间线倒错)——构造 precheck 假冲突须用枚举值之一设定违例」),随手写设定冲突 pydantic literal_error
- [2026-06-19] @llm **已被下条推翻**保留作纠错轨迹曾写 Kimi Code coding API 的伪造头只发 `User-Agent: KimiCLI/1.5`不带 X-Msh-*」——该结论源自分歧/错误参考 `picassio/pi-kimi-coder`UA-only导致误把头集裁成 UA-only。**勿采纳本条**见下条
- [2026-06-19] @backend Kimi Code device authorization **必须带 `scope=kimi-code`**`start_device_authorization` 请求体 = `{"client_id": <id>, "scope": "kimi-code"}`。K1.5 联网实测省略 scope 登录能拿到真 JWT但调 `api.kimi.com/coding/v1` `401 Invalid Authentication`——token coding entitlement`scope=kimi-code` coding-agent 文档化 scope`ooojustin/opencode-kimi` `constants.ts` 发送kimi-cli v1.41.0 已不发但服务端仍接受)。scope **只**放 device_authorizationtoken 交换(`poll_token`/刷新(`refresh`**不带** scopedevice flow 惯例)。⚠ 此前 K1.3 单测/decision/contract 省略 scope」(误信 kimi-cli v1.41.0已全部修正
- [2026-06-19] @llm **Kimi Code coding API 伪造头真源 = `ooojustin/opencode-kimi``src/headers.ts`+`src/constants.ts`1:1 镜像 kimi-cli v1.37.0),发完整 7 头、UA `KimiCLI/1.37.0`**K1.2 校正推翻上方 pi-kimi-coder UA-only 那条`picassio/pi-kimi-coder` 是分歧/错误参考只发 UA照它裁成 UA-only 是误修已回退`kimi_code_headers()` `User-Agent=KimiCLI/1.37.0` + `X-Msh-Platform=kimi_cli`字面常量 OS + `X-Msh-Version=1.37.0`==UA 版本+ `X-Msh-Device-Name``socket.gethostname()` ASCII + `X-Msh-Device-Model`macOS=`f"macOS {platform.mac_ver()[0]} {platform.machine()}"``machine()` 原样 `arm64`/`x86_64` 不归一化+ `X-Msh-Os-Version``platform.version()`+ `X-Msh-Device-Id`稳定 32 位无连字符 `uuid4().hex`env `KIMI_DEVICE_ID` 优先否则读/ `~/.kimi/device_id` 复用**绝不每次随机**否则 Kimi 侧每次当新设备)。源码若与此有出入以源码为准本次核对 master 一致)。⚠ HTTP 头值含非 ASCII 会被底层 fetch/httpx host 派生值Device-Name/Model/Os-Version ASCII `asciiHeaderValue` `\x20-\x7e` 之外 + trim空回退 `unknown`)。单测用 env `KIMI_DEVICE_ID` 注入固定 id CI 确定不触真 ~/.kimi)。ruff E501 按显示宽度算中文占 2 含中文 docstring/注释的行别贴边 100
- [2026-06-19] @backend `assemble`C5此前只从 world_entities/characters/style/rules + cards/foreshadow/digests/outline.beats 组装**从不含项目 premise/logline/theme/title也无写章指令** 全新项目只有 premise无世界观/角色/大纲产出 `stable_core=""`+`volatile=""`writer user message 为空LLM `400 "the message at position 0 with role 'user' must not be empty"`修复`MemoryRepos` 加第 8 repo `project:ProjectSpecRepo``spec(project_id)->ProjectSpecView{title,logline?,premise?,theme?}` project_id `projects` owner 过滤——owner 隔离归路由层`assemble` 作品蓝本放进 `stable_core` 首段书级 spec=定型→缓存前缀,不违 #9并让 `volatile` 始终含 `请创作第 {chapter_no} 章的正文。`易变指令)。⚠ 任何手搓 `MemoryRepos(...)` 的地方含测试 fake现在**必须** `project=` 字段否则 dataclass 缺参报错 DDL 变更只读既有 projects 无迁移
- [2026-06-22] @frontend/@orchestrator T6 创作工具箱通用端点的两处契约边界加新生成器时注意):① `ToolGenerateRequest` **固定字段并集** `{brief, chapter_no?, count?, kind?}`——`GeneratorRunner` 只映射落在此集合内的 `input_field` 新生成器若声明了集合外的 `input_field`前端会**静默忽略**该字段新工具的 `input_fields` 须落在 `brief/chapter_no/count/kind` 否则需先扩 `ToolGenerateRequest` 契约+`pnpm gen:api`)。② `ToolInputFieldView.type` 是自由字符串后端可声明 `"select"` descriptor **无 `options` 字段** 前端把 select text 输入渲染若某工具需要真下拉选项须先给 `InputField`/`ToolInputFieldView` `options` 再改前端 `GeneratorRunner`
- [2026-06-23] @backend 多章链网关C2**单档 `build_gateway_for_tier(tier)` 网关不能驱动跨档链**。 `chain_resolver`/`_resolver` 闭包恒返该 tier 的链、**忽略入参 `req.tier`**故拿它跑 review(analyst)/digest(light) 会全部错路由到 writer 档的 provider/model/任何跨档编排须用 `build_chain_gateway`union 三档适配器 + `chain_resolver` `req.tier` 分派)。测试用 mock 网关按 `req.output_schema` 路由 schema=write有=review不受此影响。
- [2026-06-23] @backend langgraph `CompiledStateGraph.ainvoke` mypy 重载`config` 必须是 `RunnableConfig``from langchain_core.runnables import RunnableConfig` `dict[str,dict[str,str]]` 不匹配任何重载 call-overload 返回值是 `dict[str,Any] | Any`传给取 `__interrupt__`/`written` helper 前先 `dict(raw_final)` 收敛为 `dict[str,Any]`interrupt 命中时返回值含 `__interrupt__`Interrupt 对象列表载荷读 `getattr(first,"value",first)`兼容 dict)。
- [2026-06-23] @backend `JobRepo` Protocol 加方法 `set_awaiting`= 契约变更**所有 fake 实现都要同步补**否则 mypy Protocol 缺成员本次漏了 `packages/core/tests/test_job_repo.py::FakeJobRepo` + `apps/api/tests/fakes_projects.py::FakeJobRepo`)。grep `class Fake.*JobRepo` 全仓再补
- [2026-06-24] @llm/@devops Prompt 外置`prompts/*.md` 文件名按 **spec.name连字符** Python 变量名——`style` agent name `"style"``style.md` `style_drift`另有 `character-gen.md`/`golden-finger.md`/`book-title.md`/`fine-outline.md`/`de-ai.md`。** prompt 内容后必须重生成金标准** `uv run python packages/agents/tests/_gen_golden.py`覆盖 `tests/fixtures/prompt_hashes.json`否则 `test_prompt_loader.py` 字节回归红旧常量含反斜杠折行——外迁/比对一律用**运行时值**别用源码文本`.gitattributes` `prompts/*.md text eol=lf`路径含斜杠是**根锚定**必须写完整嵌套路径 `packages/agents/ww_agents/prompts/*.md` 才匹配 `prompts/*.md` 不生效)。
- [2026-06-24] @devops wheel 默认不打非-`.py` 数据文件——`prompts/*.md` 必须在 `packages/agents/pyproject.toml` 显式纳入hatchling `artifacts`**别用 `force-include`**hatchling 已含包目录下全部文件force-include duplicate-path 构建失败源码树 pytest **测不出**漏带fail-fast 仅裸装时触发 CI `agents-wheel-smoke`build裸装`import ww_agents; assert ww_agents.SPECS``ww_agents` import 链拉起 `ww_llm_gateway``types.Tier`)→ `structlog` `packages/{llm_gateway,core}` 必须自声明 `structlog`原靠 apps/api 传递裸装 import ModuleNotFoundError)。
- [2026-06-24] @backend `SpecResolver`内置 name `SPECS` 纯内存、** DB**保留命名空间守卫在 **SkillRegistry 入库校验期** resolver 读路径用户 skill 与内置同名`REVIEW_RESERVED_NAMES set(SPECS)`)→ `AppError(VALIDATION)`name **精确字符串相等**拼错近似名`character_gen` vs `character-gen``output_schema_for` None/不命中是预期行为非 bug
- [2026-07-09] @qa **`ai_messages` append-only 旁路侧记录 `usage_ledger` 同物种绝不喂 `assemble()`/prompt——守不变量 #1/#6计划 §8)。** 它是作者AI 说了什么的真源**不是手稿/审稿真源**无记忆注入(#6)、无裁决(#3/#4)权威生成链`packages/core/ww_core/{memory,orchestrator}`任一模块 import/引用 `AiMessage`/`AiMessageRepo`/`ai_message_repo` = 制造第二个非确定性真源** code-review BLOCKER**。围栏已工具化`tests/test_ai_messages_not_in_generation_path.py`AST 扫两目录 import/Name/Attribute + 断言 `MemoryRepos` 捆绑与 `assemble` 形参都不含它植入违规自证非空守卫)—— ai_message repo 接进 `assemble()`/prompt 会直接变红日后加新 kind 或改 repo 都别越过这道墙