Files
writer-work-flow/memory/contracts.md
Yaojia Wang 5fb7bfb1de feat: M3 — 伏笔账本 + 节奏引擎 + 大纲(含并发记账 bugfix)
- 伏笔账本:纯函数状态机(OPEN/PARTIAL/CLOSED/OVERDUE) + ForeshadowLedger repo;验收后到期扫描(BackgroundTask 自建 session 置 OVERDUE);登记/状态变更端点
- 节奏 + 三审齐:foreshadow-analyst + pace-checker 并入 LangGraph 并行审(REVIEW_SPECS),collect 分列落 chapter_reviews(conflicts/foreshadow_sug/pace),review SSE 加 foreshadow/pace 事件
- 大纲:outliner Agent 产 OutlineResult(含 foreshadow_windows),POST /outline 逐章 upsert outline 表;GET /foreshadow?status= 看板
- 前端:伏笔四泳道看板(OVERDUE 琥珀) + 大纲编辑器(窗口徽标) + 节奏节拍图(▁▃▅) + 审稿页消费 foreshadow/pace SSE
- bugfix(T3.8):并行三审共用请求 session 记账触发 'Session is already flushing' → foreshadow/pace 静默丢失;SqlAlchemyLedgerSink.record 改 add-only(靠端点/事务 commit),加并发回归测试
- M3 E2E:真实 DB + mock 网关零 token 走通 埋设→进展→验收后扫描 OVERDUE→看板 + 大纲含窗口 + 三审齐 SSE/留痕;E2E 暴露并钉住上述 bug
- 门禁绿:mypy 111 / pytest 228(0 xfailed) / alembic 无漂移;前端 gen:api/lint/tsc/vitest 69/build
2026-06-18 14:21:17 +02:00

129 lines
23 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.

# 契约登记(解耦缝)
> 跨 agent 的契约——多 agent 靠它解耦并行。**契约先行**:定义方先在此登记并标 `稳定`,依赖方才动工;改契约必须在此记一笔 + 在 `PROGRESS.md` 通知依赖任务(前端要重生成 TS 客户端、依赖模块要重同步)。
> 状态:`待定义` → `草拟(@skill)` → `稳定` → `已变更(见日志)`。
---
## C1 · LLM 网关接口 owner @llm 状态: 稳定T1.1, 2026-06-18
- 来源:`ARCHITECTURE.md §4.1``LlmRequest` / `LlmResponse` / `Block` / `Usage`**snake_case**)。
- 消费方:编排器、所有 Agent`Gateway.run` / `Gateway.stream`)。
- 关键不变量agent 只传 `tier`writer/analyst/light不传具体 model。
- **已实现(`packages/llm_gateway/ww_llm_gateway`**
- 类型 `types.py``Block(text,cache=False)``Scope(user_id,project_id?)``LlmRequest(tier,input:str|list[Block],system:list[Block],stream,output_schema?,thinking,max_tokens?,scope)``Usage(provider,model,input_tokens,output_tokens,cache_read_tokens,cost_minor,currency)``ServedBy(provider,model,fell_back)``LlmResponse(text,parsed?,usage,served_by)``Delta(text)``Tier`
- `Gateway(adapters:dict[str,ProviderAdapter], ledger:LedgerSink, resolver=resolve_route)``async run(req)->LlmResponse``stream(req)->AsyncIterator[Delta]`。每次调用落 **1 条** `usage_ledger`(经 `LedgerSink`,可注入内存替身)。
- 适配器 `ProviderAdapter`(Protocol)`provider``capabilities()->Capabilities``async complete(req,model)->ProviderResult``stream(req,model)->AsyncIterator[StreamChunk]`。M1 实现 `OpenAICompatAdapter(provider, client:AsyncOpenAI)`DeepSeek注入 client 便于测试)。
- 档位路由 `resolve_route(tier)->Route(provider,model)``config.tier_defaults`M1 仅全局默认;回退/熔断属 M5/T5.4**未实现**)。
- 记账 `SqlAlchemyLedgerSink(session)``UsageLedger`owner_id=scope.user_id 单用户 stub列名 `cache_read`)。成本表 `pricing.py`(未知 provider/model→0
## C2 · DB schemaSQLAlchemy 模型 owner @db 状态: 待定义
- 来源:`ARCHITECTURE.md §3.1` DDL11 创作表 + chapter_reviews + 运营表无向量列users stub
- 消费方:`@backend`(Repository/记忆服务/验收)、`@llm`(Agent reads/writes)。
## C3 · API / OpenAPI 端点 owner @backend 状态: 稳定M1 端点全落T1.7 settings + T1.4 projects/chapters, 2026-06-18
- 来源:`ARCHITECTURE.md §7.2` 端点清单(章节端点统一 `/projects/:id/chapters/:no/...`;含 `PUT .../draft`(自动保存)、`/refine``/jobs/:id``/reviews`)。
- 消费方:`@frontend`(经 OpenAPI→TS 客户端)。**改端点/字段 → 前端必须 `pnpm gen:api` 重生成客户端。**
- **已落T1.7, 2026-06-18snake_case响应仅脱敏 key**
- `GET /settings/providers``ProvidersResponse{ providers:[ProviderView{provider, masked_key}], tier_routing:[TierRoutingView{tier, provider, model, fallback:list[str]}] }`
- `PUT /settings/providers``ProvidersUpsertRequest{ credentials:[ProviderCredentialInput{provider, api_key}], tier_routing:[TierRoutingInput{tier, provider, model, fallback}] }``ProvidersResponse`(脱敏)
- `POST /settings/providers/test``TestConnectionRequest{provider}``TestConnectionResponse{provider, ok, capabilities:CapabilitiesView{structured_output, prefix_cache, thinking}}`
- **明文 key 永不过边界**probe 经可注入 `ProviderProbe`(测试用 fake不联网
- **已落T1.4, 2026-06-18snake_case**
- `POST /projects``ProjectCreateRequest{title, genre?, logline?, premise?, theme?, selling_points:list, structure?}` → 201 `ProjectResponse{id, title, genre?, logline?, premise?, theme?, selling_points, structure?}`
- `GET /projects``ProjectListResponse{projects:[ProjectResponse]}``GET /projects/{id}``ProjectResponse`404 `NOT_FOUND` 信封)
- `POST /projects/{id}/chapters/{no}/draft`**SSE** `text/event-stream`,帧 `event:<token|done|error>\ndata:<json>\n\n``token{text}`/`done{length}`/`error{code,message,request_id}`);无凭据 → 流前 `LLM_UNAVAILABLE`(503) JSON 信封(非帧)。
- `PUT /projects/{id}/chapters/{no}/draft``DraftSaveRequest{text}` → 200 `DraftResponse{project_id, chapter_no, volume, status, version, length}`幂等version 固定 1、status='draft')。
- 网关注入缝 `get_writer_gateway`(从凭据构 `OpenAICompatAdapter`+`SqlAlchemyLedgerSink`);测试经 `app.dependency_overrides` 注 mock 网关(只需 `.stream(req)`)。
- **@frontend 行动**M1 端点已全在 OpenAPI → Wave D 前先 `cd apps/web && pnpm gen:api` 重生成 `lib/api/schema.d.ts`
- **已落T2.4+T2.5, 2026-06-18snake_case全部挂 `projects.router`、已在 OpenAPI— M2 审/裁/验收三端点**
- `POST /projects/{id}/chapters/{no}/review``ReviewRequest{draft?}`(空/缺→回退已存草稿无草稿→404 `NOT_FOUND`)→ **SSE** `text/event-stream`,帧:`section{name,status:"done"|"incomplete"}`(每审一条M2 仅 `continuity`) / `conflict{type,where,refs:list,suggestion}`(每冲突一条,形=C6 `Conflict` 五类) / `done{length=审项数}` / `error{code,message,request_id}`。无凭据→流前 `LLM_UNAVAILABLE`(503 JSON 信封)。续审网关 tier=analyst`get_review_gateway`)。**端点流耗尽后 `session.commit()`**(网关 ledger + collect 均只 flush
- `GET /projects/{id}/chapters/{no}/reviews``ReviewHistoryResponse{reviews:[ReviewHistoryItem{id,project_id,chapter_no,chapter_version?,conflicts:list,foreshadow_sug:list,style?,pace?,health_score?,decisions?}]}`(新→旧)。
- `POST /projects/{id}/chapters/{no}/accept``AcceptRequest{final_text(min1), decisions:[ConflictDecision{conflict_index:int>=0, verdict:"accept"|"ignore"|"manual", note?}]}``AcceptResponse{project_id,chapter_no,accepted_version:int,digest_added:bool,decisions_recorded:int,review_id?:uuid}`。**冲突 gate**:裁决的 `conflict_index` 集合须覆盖 `range(len(最近一条 review.conflicts))`缺判→409 `CONFLICT_UNRESOLVED` + `details.missing_conflict_indices`/`conflict_count`无留痕或零冲突→直通。digest 提炼 tier=light`get_digest_gateway`在事务外R2单事务 promote(R4)+digest.append(#4)+set_decisions 末尾一次 commitR3
- 注入缝:`build_gateway_for_tier(session, store, tier)`(原 `build_writer_gateway` 退化为 writer 特例) + `get_review_gateway`/`get_digest_gateway`/`get_review_repo`/`get_digest_append_repo`;测试经 `app.dependency_overrides` 注 mock。
- **@frontend 行动**`cd apps/web && pnpm gen:api` 重生成客户端含此三端点T2.6)。
- **已落T3.2, 2026-06-18snake_case— 伏笔登记/状态端点 + 验收后到期扫描**(挂 `foreshadow.router`prefix `/projects`tag `foreshadow`,已注册):
- `POST /projects/{id}/foreshadow``ForeshadowRegisterRequest{code(min1),title(min1),planted_at?,content?,expected_close_from?,expected_close_to?,importance?}` → 201 `ForeshadowView{code,title,status,planted_at?,content?,expected_close_from?,expected_close_to?,importance?,links:list,progress:list}`(status=OPEN)。重复 code(DB 唯一 `(project_id,code)`)→ **422** `VALIDATION`+`details{field:"code",code,reason:"duplicate"}`
- `PATCH /projects/{id}/foreshadow/{code}``ForeshadowTransitionRequest{to_status?,progress_entry?}` → 200 `ForeshadowView`。非法转移→422 `VALIDATION`+`details{reason:"invalid_transition"}`二者都缺→422 `empty_update`code 不存在→404 `NOT_FOUND`。先转移后追加进展,端点写后 `commit()`(repo 只 flush)。
- **错误码取舍**:重复/非法转移映射 `VALIDATION`(422) 非 409——现有唯一 409 码 `CONFLICT_UNRESOLVED` 语义专指未决冲突禁验收,不复用;未动 `packages/shared/errors.py` 契约。若 T3.5/前端需真 409 须先加专用码。
- **验收后到期扫描**`POST .../accept` 单事务 commit 成功后经 `BackgroundTasks` 登记 `run_overdue_scan(session_factory, project_id, chapter_no=刚验收章号)`——**自建独立 session**(请求 session 已关闭)、应用 `current_ch>expected_close_to AND status≠CLOSED→OVERDUE`、有变更才 commit日志 `foreshadow_overdue_scan`(带 request_id/overdue_count/codes)失败吞不冒泡。§7.4 持久性局限(进程内/重启丢任务)原型接受、不引 jobs 表。
- **T3.5 续接**`GET /projects/{id}/foreshadow?status=` 看板加到本 `foreshadow.router`(注入 `get_foreshadow_repo``list_by_status`,建议响应包 `{foreshadow:[...]}``POST .../outline` 落**独立** `routers/outline.py`(别混进 foreshadow.py),调 `run_outline`(C6 扩)+`build_gateway_for_tier(...,"analyst")`,逐章 upsert `outline` 表、端点末尾 commit。
- **@frontend 行动**T3.6 前 `pnpm gen:api` 纳入伏笔登记/状态端点(+ T3.5 看板/outline
- **已落T3.5, 2026-06-18snake_case— 伏笔看板 + 大纲生成端点**
- `GET /projects/{id}/foreshadow?status=`tag `foreshadow`query `status?``OPEN|PARTIAL|CLOSED|OVERDUE`(缺省=全部非法→422 `VALIDATION`+`details{field:"status",reason:"invalid_status"}`) → 200 `ForeshadowBoardResponse{foreshadow:list[ForeshadowView]}`(按 code 升序)。
- `POST /projects/{id}/outline`tag `outline`,独立 `routers/outline.py`)← `OutlineGenerateRequest{volume:int=1,ge=1}`M3 简单分卷:整批同卷)→ 200 `OutlineResponse{chapters:list[OutlineChapterView{no,volume,beats:list[str],foreshadow_windows:list[ForeshadowWindowView{code,plant_chapter?,expected_close_from?,expected_close_to?}]}]}`项目不存在→404 `NOT_FOUND`;无凭据/网关失败→503 `LLM_UNAVAILABLE`(流前 JSON 信封)。调 `run_outline`(analyst 网关 `get_outline_gateway`)→逐章 upsert `outline` 表(写侧 `SqlOutlineWriteRepo`,只 flush)→端点末尾 `commit()`(含 1 条 usage_ledger
- **beats 存储形**DB `outline.beats` 是 JSONB dict→写侧包成 `{"beats":[...]}` 落库API 出参 `OutlineChapterView.beats` 解包成裸 `list[str]`
- **@frontend 行动**T3.6 `pnpm gen:api` 纳入看板/大纲端点。
## C4 · 编排器接口LangGraph 写章图 owner @llm 状态: 稳定M1/T1.3, 2026-06-18M2 扩四审/验收)
- 来源:`ARCHITECTURE.md §5.2`(图)/ §7.3SSE。**M1 仅单 `write` 节点**;并行四审/collect/interrupt(accept) 属 M2。
- 位置:`packages/core/ww_core/orchestrator/`
- 图状态 `ChapterState`(TypedDict, snake_case)`{project_id:UUID, chapter_no:int, user_id:UUID, stable_core:str, volatile:str, draft:str}`——仅控制流+组装上下文+累积草稿(不变量#5resume 从领域表重读。
- 节点缝:
- `build_write_request(*, stable_core, volatile, user_id, project_id) -> LlmRequest`(纯函数;`tier="writer"``system=[Block(stable_core,cache=True)]``input=volatile``stream=True``scope=Scope(user_id,project_id)`)。
- `async stream_chapter_draft(gateway, *, stable_core, volatile, user_id, project_id) -> AsyncIterator[Delta]`——**T1.4 拿来喂 `normalize_deltas` 的底层流缝**。
- `async write_node(state, *, gateway) -> {"draft":str}`——可直接单测(注入 mock 网关)。`GatewayStream` Protocol = 节点对网关最小依赖(只需 `.stream(req)`)。
- **SSE 归一缝T1.4 消费)**`async normalize_deltas(deltas, *, request_id=None) -> AsyncIterator[SseEvent]``SseEvent{event:str, data:dict}`;事件 `token{text}`/`done{length}`/`error{code,message,request_id}`ARCH §7.3 子集M2 加 section/conflict。底层异常→发 `error` 事件后收尾(不上抛);`AppError.code` 透传,未知→`INTERNAL`。HTTP event-stream 编码归 T1.4。
- 图工厂:`build_write_graph(gateway, *, checkpointer=None) -> CompiledStateGraph`START→write→END单测传 `MemorySaver`
- checkpointer setup 入口:`async setup_checkpointer(conn_string) -> None`——**只在 migrations/CI 调**(内部 `AsyncPostgresSaver.from_conn_string` 跑一次 `setup()` DDL懒 import绝不在 app-runtime 跑)。
- 不变量agent 只传 tierDB 唯一 agent 间通道;不可变更新;瞬时重试在网关不在节点。
### C4 扩展T2.2, 2026-06-18· 审稿子图 + review SSE
- 图工厂 `build_review_graph(gateway, review_repo, *, review_specs=(continuity_spec,), checkpointer=None) -> CompiledStateGraph``START →`各 review spec 并行节点`→ collect → END`。M2 默认仅 continuity`review_specs` 可扩M3/M4 加 foreshadow/style/pace。**`build_write_graph` 保留不动**draft 端点仍用它)。
- `ChapterState`(state.py) 新增 `review_context:str``reviews:Annotated[dict, merge_reviews]`(并行分支浅合并 reducer返回新 dictTypedDict 改 `total=False`(按节点逐步填充);仍仅控制流+组装上下文+产物句柄(不变量#5)。
- 节点缝:`run_review(spec, state, *, gateway)->{"reviews":{spec.name:{status,result}}}`(裸函数可单测);`make_review_node(spec, gateway)`(绑 gateway 的公共缝,供 T2.5 跑单审);审稿用 `gateway.run()`(非 stream**只读不写库**(不变量#3);任一审网关失败被隔离为 `{status:"incomplete",result:None}`§5.2),不上抛、不毁图。`GatewayRun` Protocol=节点对网关最小依赖(`.run(req)->LlmResponse`)。
- collect 缝:`collect_reviews(state, *, review_repo)->{}`:抽 continuity 冲突 → `review_repo.record(project_id, chapter_no, chapter_version=None, conflicts=[...])``chapter_reviews` 留痕;**只 flush 不 commit**(提交归端点/T2.4 事务)。`ReviewRecorder` Protocol 形对齐 `domain.review_repo.ReviewRepo.record`
- SSE 新增事件:`section{name,status}`status ∈ started/done/incomplete`conflict{type,where,refs,suggestion}`(对齐 C6 `Conflict`);新归一缝 `normalize_review(reviews, *, request_id=None)->AsyncIterator[SseEvent]`(每审一条 section + 每冲突一条 conflict + `done{length=审项数}`;异常→`error` 不上抛,同 `normalize_deltas` 纪律。HTTP event-stream 编码归 T2.5。
- **accept 不在本图**`interrupt_before=["accept"]`/accept 节点属 T2.4确定性事务代码从领域表重读R3/不变量#5)。
- 消费方行动T2.5 跑 review 子图 + `normalize_review` + 端点**流耗尽后 `await session.commit()`**(网关 ledger + collect 均只 flush不提交则记账/留痕静默丢失,同 M1 draft 坑);`GET .../reviews``review_repo.list_for_chapter`(新→旧)。T2.4 从 `review_repo.list_for_chapter` 读 collect 落的行(`decisions=None`)裁决。
### C4 扩展T3.3, 2026-06-18· 三审齐foreshadow + pace 并入)
- `REVIEW_SPECS = (continuity_spec, foreshadow_spec, pace_spec)``build_review_graph(..., review_specs=REVIEW_SPECS)` 默认三审齐(文风第四审留 M4`run_review`/`make_review_node` 对 spec 泛型(按 `req.output_schema` 产 parsed
- collect 列映射(一次 `review_repo.record(...)` 落齐continuity→`conflicts`(list)foreshadow→`foreshadow_sug`(list`planted`/`resolved` 扁平成单 list、每条加 `kind:"planted"|"resolved"`)pace→`pace`(dict`{water,hook,beat_map}` 入列)style 留 M4。任一审 incomplete→该列空/None不阻塞其余§5.2)。
- `normalize_review` SSE向后兼容`section`/`conflict` 不变)新增:`foreshadow{kind,code?,title,where?,note?}`(每条建议一条)、`pace{water:[{where,reason}],hook:bool,beat_map:[int]}`一条。键按字典序确定性遍历continuity<foreshadow<pace)。incomplete 审仅发 `section` 不发结果
- 消费方前端 T3.6冲突就地标注 / 伏笔看板联动 / 节拍图 ▁▃▅)、E2E T3.7
## C5 · 记忆服务 `assemble` / `select_relevant_entities` owner @backend 状态: 稳定T1.2, 2026-06-18
- 来源`ARCHITECTURE.md §3.4 / §5.3`确定性选择显式+主角+近况渲染卡片缓存断点)。
- 位置`packages/core/ww_core/memory/`+ `domain/repositories.py`)。
- 输出决策中性文本 `LlmRequest` decisions 2026-06-18
- `AssembledContext{ stable_core:str, volatile:str, selection:SelectionTrace }`
- `stable_core`断点前世界硬规则+定型主角( latest_state)+文风指纹+合并规则已排序/无时间戳/ UUID
- `volatile`断点后注入卡片( latest_state)+伏笔窗口+近况摘要+本章 beats)。
- `SelectionTrace{ selected:list[SelectedEntity] }``SelectedEntity{ kind:"character"|"world_entity", name:str, reasons:list[SelectionReason] }``SelectionReason="explicit_beat"|"main_character"|"recent_digest"|"foreshadow_window"`
- 入口`async assemble(repos:MemoryRepos, project_id, chapter_no, recent_k=5)->AssembledContext`纯函数 `select_relevant_entities(*, outline, characters, world_entities, recent_digests)->SelectionTrace``render_cards(selection, characters, world_entities)->str``merge_rules(rules)->list[RuleView]`globalgenrestyleproject)。
- 依赖注入`MemoryRepos` 捆绑 7 ProtocolOutline/Character/WorldEntity/Digest/Foreshadow/Style/Rules单测注入内存 fake运行时 `sql_memory_repos(AsyncSession)`**T1.4 注入点**)。
- 不变量确定性选择(无向量 #6)统一 project_id 过滤3.5)latest_state 严格归 volatile(#9)。
- 消费方**T1.3 write 节点** 构造 `LlmRequest(system=[Block(stable_core, cache=True)], input=volatile)`
## C6 · Agent 声明AgentSpec+ 续审 I/O schema owner @llm 状态: 稳定T2.1, 2026-06-18
- 来源`ARCHITECTURE.md §5.1 / §5.4 / §6.1`位置`packages/agents/ww_agents/``specs.py` / `schemas.py` `ww_agents` 导出)。
- **`AgentSpec`**frozen Pydantic不可变`name:str``tier:Tier`(writer/analyst/light复用 `ww_llm_gateway.types.Tier`——只声明档位不写 model不变量#2)、`system_prompt:str``input_schema:type[BaseModel]|None``output_schema:type[BaseModel]|None`(writer None=纯文本)、`reads:list[str]``writes:list[str]``genre:str|None=None``scope:str="builtin"`
- **`continuity_spec`**`tier="analyst"``reads=["chapter_digests","characters","world_entities"]``writes=[]`(只读不变量#3)、`input_schema=None`(注入材料为序列化文本)、`output_schema=ContinuityReview`
- **`ContinuityReview{ conflicts: list[Conflict] }`**仅冲突digest 不在审稿期产不变量#4)。
- **`Conflict{ type: ConflictType, where:str, refs:list[str]=[], suggestion:str }`**`ConflictType = Literal["性格漂移","能力不符","设定违例","地理矛盾","时间线倒错"]`ARCH §6.1 五类)。
- 续审节点调法`req = LlmRequest(tier="analyst", system=[Block(system_prompt, cache=True)], input=审稿上下文文本, output_schema=ContinuityReview, scope=...)` `resp = await gateway.run(req)` `resp.parsed` `ContinuityReview` 实例 schema 时必非 None)。续审用 `run()` `stream()`只读+产冲突不写库写在 T2.4 验收事务)。
- 消费方编排器(T2.2 续审/collect 节点)、技能运行时(M5)、前端审稿页( C3)。foreshadow/style/pace 三审 spec M3-T3.3/M4 补入本契约
### C6 扩展T3.4, 2026-06-18· outliner Agent + 大纲/伏笔窗口 schema
- 位置`packages/agents/ww_agents/{schemas.py,specs.py}` `ww_agents` 导出节点在 `packages/core/ww_core/orchestrator/outline_node.py` orchestrator 导出)。
- **`outliner_spec`**(frozen `AgentSpec`)`name="outliner"``tier="analyst"``reads=["projects","foreshadow","characters","world_entities"]``writes=["outline"]`(声明式真写库经 T3.5不变量#3)、`input_schema=None``output_schema=OutlineResult`
- **`OutlineResult{chapters:list[OutlineChapter]}`****`OutlineChapter{no:int, beats:list[str], foreshadow_windows:list[ForeshadowWindow]}`****`ForeshadowWindow{code:str, plant_chapter?:int, expected_close_from?:int, expected_close_to?:int}`**(§6.2 关联章与伏笔)。
- 节点缝沿用 review_node `GatewayRun` Protocol `.run(req)->LlmResponse``build_outline_request(spec, *, context, user_id, project_id)->LlmRequest`(`output_schema=OutlineResult`,`stream=False`) + `async run_outline(spec, *, context, gateway, user_id, project_id)->OutlineResult`(`gateway.run().parsed`**只读不写库**大纲独立生成非并行审网关失败**直接上抛**不隔离parsed 违约抛 `ValueError`)。
- 消费方T3.5 端点调 `run_outline` `OutlineResult` 逐章 upsert `outline` `beats`/`foreshadow_windows` JSONB`volume` 由端点分卷逻辑提供schema 不含 volumeanalyst 网关复用 `build_gateway_for_tier(session,store,"analyst")`/`get_review_gateway`记账闭环同 M2端点末尾 `commit()`)。T3.6 前端大纲编辑器消费窗口
### C6 扩展T3.3, 2026-06-18· foreshadow-analyst + pace-checker spec
- `foreshadow_spec`(name="foreshadow", tier="analyst", reads=["foreshadow"], writes=[]只读, output=`ForeshadowReview{planted:[ForeshadowSuggestion],resolved:[ForeshadowSuggestion]}``ForeshadowSuggestion{code?,title,where?,note?}`)。
- `pace_spec`(name="pace", tier="light", reads=["rules"], writes=[]只读, output=`PaceReview{water:[PaceIssue{where,reason}],hook:bool,beat_map:[int]}`)。genre 模板 DSL = pace system_prompt 内描述黄金三章/章末钩子/爽点密度genre 级模板经 review_context 规则合并(`rules`)注入**未新建表/repo**。
- 三审只读留痕经 collectreview_repo写库 commit 归端点)。详细图/SSE 见上C4 扩展T3.3)」。
## C7 · 前端 ↔ 后端类型契约 owner @backend(产出 OpenAPI) / @frontend(生成) 状态: 待定义
- 机制FastAPI OpenAPI `apps/web/lib/api` TS 类型openapi-typescript/orval)。
- 规则后端 schema 任何变更 `gen:api` 重生成不手写共享类型
---
## 契约变更日志append-only
> 格式:`- [date] @skill 改 Cx<改了什么> → 影响 <依赖方/任务>`
- [2026-06-18] @llm C1`Gateway.run()` 现消费 `LlmRequest.output_schema`——`OpenAICompatAdapter.complete` schema 非空时经 **instructor**(`create_with_completion(response_model=...)`) 取已校验 Pydantic 实例并填 `LlmResponse.parsed` schema `parsed is None`纯文本路径不变记账仍 **1 条 usage_ledger**/调用usage raw completion 提取)。`ProviderResult` 新增 `parsed` 字段结构化路径经可注入 `StructuredClient` Protocol fake测试不联网)。→ 影响 T2.2续审节点可直接 `gateway.run(req).parsed`)、未来所有结构化输出 Agent