计划 §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)。
52 KiB
踩坑与约定(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 真落 pg:override
get_session_factory→e2e_sm、但不能** monkeypatchkimi_oauth.SqlCredentialStore(K1.3 单测那样会让落库走内存 fake,验不到真 pg)(K1.5):与 K1.3 单测(FakeSession/FakeStore/FakeJobRepo,验逻辑)不同,E2E 要验真持久化——让后台work自建的SqlCredentialStore(session)+run_job默认SqlJobRepo保持真,只 overrideget_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 走 dep;call[0]=device-auth、call[1..]=token 轮询顺序共享)+ monkeypatchrouters.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 obj,typecheck 绿)、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 provider(kimi-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 引模块级 SqlJobRepo,FakeSession 无 .execute;同 T4.3 gotcha)+monkeypatch.setattr(routers.kimi_oauth,"SqlCredentialStore", lambda s: shared_store)(work 自建 store 落库)+ overrideget_session→FakeSession/get_session_factory→FakeSessionFactory + monkeypatchrouters.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.3):service 函数(start/poll/refresh)只需.post,给 Protocol 加aclose会逼所有测试 fake 实现它(mypy red)。httpx.AsyncClient有aclose,故 routerwork的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)——用字符串 targetmonkeypatch.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_key(SDK 自动发Authorization: Bearer)。②X-Msh-Device-Id必须跨调用稳定*:kimi_device_id()= envKIMI_DEVICE_ID优先,否则uuid5(NAMESPACE_DNS, "ww.kimi-code.device")模块级常量——别用uuid4()每次新建(每次随机 device id 会让 Kimi 侧把每次调用当新设备)。③ 校验对照picassio/pi-kimi-coderextensions/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返回ProviderAdapterProtocol 无_client,否则 mypyattr-defined)。 -
[2026-06-19] @db K1.1 把
provider_credentials.api_key_enc改 nullable 后,跨边界打穿到 @backendcredentials.pymypy 报错——这是 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 即绿。首个新迁移(M1–M5 全零建表)= K1 新功能预期内。 -
[2026-06-19] @db 新迁移文件 autogenerate 后须手修两处再过 ruff:① autogenerate 模板把
from sqlalchemy.dialects import postgresql写两遍(重复 import,ruff 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-genaiSDK 装上后,注入客户端 Protocol 与 SDK 具体类型在 mypy strict 下不互通——在边界cast解决,别硬改 Protocol(R2 follow-up #2):①instructor.from_anthropic(self._client)的重载只收 SDK 具体AsyncAnthropic|...联合,AnthropicClientProtocol 不匹配 → 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,无行返空列表)→ @frontendpnpm 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.tomldeps(去 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/OpenAICompatAdapterimport);anthropic/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 route(Server 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/palettenode-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、a11y(role=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 必须是合法 Fernet(32 字节 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),三个网关 dep(worldbuilder/character_gen/precheck)override 成同一个。生成预览端点也要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:按 DBtier_routing.fallback(StoredRouting.fallback已有) 预备多个 provider 适配器进adaptersdict + 传chain_resolver=lambda tier: chain_from_routing(tier, primary, fallback),并可注入跨请求共享的CircuitBreaker(默认每个 Gateway 实例自带一个、进程内不共享,逐请求新建网关则熔断不跨请求累积)。Anthropic/Gemini 适配器真实接线还需 @devops 加anthropic/google-genaiSDK 依赖(适配器本身懒导入/注入客户端,仅 apps/api 构造真实 client 时需要)。 -
[2026-06-19] @llm character-gen schema 的
traits/speech_tics是list[str],但 DBcharacters.traits/speech_tics/arc列是 JSONB dict(T5.2 ingest 须转形)(T5.1):C6 扩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.rulesJSONB(包{"rules":[...]}或裸 list)。 -
[2026-06-19] @llm 入库前 continuity 校验是「编排器追加的一道检查」,复用
continuity_spec而非新建 spec(T5.1,ARCH §6.5):precheck_generated_cards传入continuity_spec(不是 character-gen 自调、不是新审种),跑生成角色卡 vs 世界观/已有角色真相源、返list[Conflict]。守不变量#1(agent 只经 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)。同 M3:overrideget_session_factory→e2e_sm(同测试 engine/loop),ASGITransport 下await client.post等 background task 跑完,轮询GET /jobs/{id}稳定见 done。第四审 E2E:只 overrideget_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)。回炉useRefine503(error.code==="LLM_UNAVAILABLE")→提示去设置,仿 review 流前 503 检出。 -
[2026-06-19] @backend 测
run_job后台路径要 monkeypatchjob_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,故 monkeypatchrunner_mod.SqlJobRepo才生效(否则后台set_running用SqlJobRepo(FakeSession)触.execute炸、被 run_job 吞成 job_failed)。端点测 BackgroundTask 落库路径:overrideget_session_factory→FakeSessionFactory+ monkeypatchstyle.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.1reap_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 只依赖最小JobLifecycleRepoProtocol(set_running/complete/fail)——比全JobRepo窄,单测 fake 不必实现 create/get/reap(同run_overdue_scan用最小OverdueScanRepo先例)。 -
[2026-06-18] @llm
SqlAlchemyLedgerSink.record改 add-only(T3.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-only,commit 归调用方」(commit 仍会 flush,故记账语义不变)。 -
[2026-06-18] @qa/@llm 并行审记账撞 session(M3 真 bug,T3.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)。修向:审稿期记账避免并发 flush(add-only 靠端点 commit / 或缓冲后 collect 串行落 / 或并行审记账用独立 session)。 -
[2026-06-18] @qa E2E 验「验收后 BackgroundTask 扫描」时序:httpx
ASGITransport下await client.post(...)会等 ASGI app 协程(含 Starlette background tasks)跑完才返回,故 client 上下文退出后断言稳定不 flaky;必 overrideget_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本地先改 status,PATCH 失败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_*_gatewaydep 解析——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 捕 SQLAlchemyIntegrityError→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.pyimport.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_progressappend 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 内每模块各自声明
GatewayRunProtocol(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_defaultswriter/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"](ErrorCodeStrEnum →"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 长度变化判(同数不同组会漏重置),也不能在 seed(phase=idle,进页种历史留痕)时误触发。 -
[2026-06-18] @backend 冲突 gate 判据(accept):冲突身份 = 「最近一条
chapter_reviews.conflicts列表的下标」;裁决ConflictDecision{conflict_index, verdict:accept|ignore|manual, note?};gate 通过 = 裁决的conflict_index集合覆盖range(len(conflicts)),缺判→409CONFLICT_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。图工厂里改用 inlineasync 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用于记账,避免结构化路径丢 usage;usage 提取统一走_usage_from(raw_usage)(文本/结构化/流共用)。adapter 经可注入StructuredClientProtocol 注 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-ignore,ruff/mypy 红)。 -
[2026-06-18] @backend M2 验收-side repos 只 flush 不 commit:
promote_to_accepted/digest.append/review.record/set_decisions均只flush(),交由 T2.4 验收事务单次commit()(对齐「写库副作用在事务/编排层」);唯独 draftsave_draft仍自 commit(M1 自动保存语义)。(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+ReadableStreamreader,不用EventSource(EventSource 不能 POST、不能干净 abort)。"停"=AbortController.abort(),吞掉AbortError、已收 token 留在 state 并被自动保存。流前错误(无凭据→503LLM_UNAVAILABLE是 JSON 信封非帧)经!res.ok检出、从{error:{code,message}}解析。 -
[2026-06-18] @frontend apps/web 测试环境坑:
server-only包未装→别 import(Server 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_idstub)。任何写这些表的路径(写章记账、立项、存凭据)跑前必须存在该 user 行——T1.4 幂等 seed(startup/lifespan);auth 落地后替换为真实 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、ainvokeconfig 标注: RunnableConfig。 -
[2026-06-18] @orchestrator 跨包测试同名碰撞:每包
tests/无 init(避免与顶层tests包撞),但多包并存时 ① pytest 全跑:同名顶层模块fakes.py撞("import file mismatch")→ 测试替身用全局唯一名(fakes_gateway/fakes_providers/fakes_orchestrator);② 聚合mypy packages apps:多个 rootlessconftest.py撞成同名模块 → root pyproject[tool.mypy] exclude=["(^|/)conftest\\.py$"](conftest 仅 fixtures,按包仍受检)。test_*.py 保持全局唯一名。 -
[2026-06-18] @backend Fernet 凭据 key 取
settings.credential_enc_key(envCREDENTIAL_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 绑定首个 loop;pytest-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包假适配器即可,无需 monkeypatchbuild_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是ConflictTypeLiteral(性格漂移/能力不符/设定违例/地理矛盾/时间线倒错)——构造 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-kimiconstants.ts发送;kimi-cli v1.41.0 已不发但服务端仍接受)。scope 只放 device_authorization;token 交换(poll_token)/刷新(refresh)不带 scope(device 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 头、UAKimiCLI/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:envKIMI_DEVICE_ID优先→否则读/建~/.kimi/device_id复用,绝不每次随机,否则 Kimi 侧每次当新设备)。源码若与此有出入以源码为准(本次核对 master 一致)。⚠️ HTTP 头值含非 ASCII 会被底层 fetch/httpx 拒,host 派生值(Device-Name/Model/Os-Version)须 ASCII 化(asciiHeaderValue:裁\x20-\x7e之外 + trim,空回退unknown)。单测用 envKIMI_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 个 repoproject: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 改
JobRepoProtocol 加方法(如set_awaiting)= 契约变更:所有 fake 实现都要同步补,否则 mypy 报 Protocol 缺成员(本次漏了packages/core/tests/test_job_repo.py::FakeJobRepo+apps/api/tests/fakes_projects.py::FakeJobRepo)。grepclass Fake.*JobRepo全仓再补。 -
[2026-06-24] @llm/@devops Prompt 外置:
prompts/*.md文件名按 spec.name(连字符) 非 Python 变量名——styleagent 的 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 仅裸装时触发),靠 CIagents-wheel-smoke(build→裸装→import ww_agents; assert ww_agents.SPECS)守。ww_agentsimport 链拉起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_genvscharacter-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 都别越过这道墙。