Files
PolyGateway/research-wiki/designs/2026-07-20-m1-core-design.md
T

23 KiB
Raw Blame History

M1 核心里程碑设计:公共签名冻结与治理栈落地

状态: 待 Codex 审 → 人类门。依据: ARCHITECTURE.md D1D14 / ROADMAP §2 / migrations/ 三份;签名证据来自 2026-07-20 对 reference/ 三项目的逐字提取(文中 文件:行号 均指 reference/ 下路径)。 范围: M1 全部交付物(ROADMAP §2 七步),含 2026-07-20 人类拍板:多源完整行为进 M1;GovDoc 与 Video-Tree 双冒烟;Redis 集成测试用实验室远程实例。 本文档冻结的内容: types.py / errors.py / ports.py 公共签名、配置键名、五个开放设计轴的取舍。与 ARCHITECTURE.md 冲突处在 §13 列为反哺修订,经人类批准后先改 ARCHITECTURE 再实施。

1. 目标与非目标

M1 出口:GovDoc 与 Video-Tree 用 GatewayClient.from_env() + chat() 替代各自 GovernedLLMClient 跑通一次真实治理调用;make ci 绿、覆盖 ≥80%。非目标:Redis 限流/熔断后端、背压 stall、Postgres 遥测、pricing、OCR、音频(M2/M3)。

2. 核心类型冻结(types.py)

2.1 LLMResponse

前 11 个字段与两参考项目逐字一致(VT core/types.py:12-29 ≡ GovDoc docagent_core/types.py:8-25),顺序不变、无默认值——三项目测试中按位置构造 fake 的代码零改动。新增字段全部带默认值(ARCHITECTURE §5.1 稳定性约定①):

@dataclass(frozen=True)
class LLMResponse:
    content: str; thinking: str; model: str; provider: str
    prompt_tokens: int; completion_tokens: int; latency_ms: int
    ttft_ms: float | None; max_inter_token_ms: float | None
    cache_hit: bool; call_id: str
    # —— 库新增(只增不删,必带默认值)——
    source_name: str = ""
    cost: float | None = None
    usage_source: str = "measured"      # "measured" | "estimated"
    structured_data: Any | None = None  # D14 阶梯通过后的解析产物
  • structured_data 不参与缓存序列化(pydantic 实例不可 JSON 往返);缓存命中且调用方传了 structured 时,对缓存的 content 重跑阶梯②③(零网络)再填充——schema 变更后旧缓存自动重校验,失败按未命中处理并记 warning。
  • 缓存反序列化防御:未知字段过滤、缺失新字段吃默认值(版本偏移不炸)。

2.2 chat() 签名与 ChatRequest

chat() 按 ARCHITECTURE §5.2 定稿,唯一细化是 structured 的类型:

async def chat(
    self, messages: list[dict[str, Any]], *,
    session_id: str | None = None,
    parent_call_id: str | None = None,
    cache_salt: str | None = None,
    cache_namespace: str | None = None,
    structured: type[BaseModel] | Literal["json"] | None = None,
    stream: bool = True,
) -> LLMResponse: ...

洋葱内部流转 ChatRequest(frozen dataclass):上述全部参数 + overlay: dict(StructuredMW/注册表写入的请求体叠加项,如 response_format、thinking 参数)。中间件用 dataclasses.replace() 派生新请求,不做原地修改(见 §4.1 方案 A1)。

2.3 SourceConfig(CHS config.py:43-83 超集)与辅助类型

字段 类型/默认 说明
name / provider / base_url / api_key / model str,必填 provider 必须是注册表键(§7)
max_concurrency / rpm / tpm int = 0 0 = 该闸不启用(VT/GovDoc 迁移无 TPM 配置)
est_tokens int = 0 TPM 预扣 + usage 缺失兜底;tpm > 0 时必填 > 0(装配期校验)
timeout_s float,必填 须 ≤ permit 租约 TTL(§7.3 装配守卫)
ttft_timeout_s / inter_token_timeout_s float | None = None 不变式 0 < inter < ttft < timeout_s(CHS config.py:66-82)
enable_thinking bool | None = None 三态:None=不注入(模型默认)/ True=注入开启 / False=注入关闭——同时覆盖 VT"注入开启"(llm.py:130-144)与 CHS"注入关闭"(invokers.py:230-238)两种现状
missing_done str = "retry" SSE 缺 [DONE] 语义,见 §6
trust_env bool = True 代理绕行(VT ocr.py:46 教训,M3 OCR 复用)

其余:Usage(prompt_tokens, completion_tokens, usage_source);SourceStats(inflight, rpm_used, tpm_used)(CHS ports.py:396);TransportResult(transport 返回的内部类型:content/thinking/两 token 数/usage_source/ttft_ms/max_inter_token_ms/raw)。韧性配置四件套沿用 CHS 字段:RetryPolicy(max_attempts, backoff_base_s, backoff_max_s)BreakerConfig(fail_threshold, cooldown_s, probe_ttl_s)BackpressurePolicy(stall_window_s, poll_interval_s)(M1 仅 poll 用,stall 判定 M2)、GlobalLimits(max_concurrency, rpm, tpm)

max_attempts 语义统一:= 总尝试次数(含首次)。VT range(max_retries) 同义;CHS fails > max_attempts 为失败数上限,迁移映射时 +1(记入 CHS 迁移文档)。

3. 错误模型冻结(errors.py)+ §6.1 勘误

class PolyGatewayError(Exception):
    def __init__(self, message: str, *, source_name: str | None = None,
                 status_code: int | None = None, operation: str | None = None): ...

class TransientError(PolyGatewayError):        # + retry_after_s: float | None = None
class SourceDeadError(PolyGatewayError): ...
class RequestRejectedError(PolyGatewayError): ...
class ResultInvalidError(PolyGatewayError):
    # + raw_text: str, repair_error: str | None, validation_errors: tuple[str, ...]

class GatewayUnavailableError(PolyGatewayError):
    # + scope: str, reason: str, retry_after_s: float, per_source_reasons: dict[str, str]
class CircuitOpenError(GatewayUnavailableError): ...   # reason 恒 "circuit_open"
class AllSourcesExhausted(GatewayUnavailableError): ...
class GovernanceBackendError(PolyGatewayError): ...    # 限流/熔断后端故障:报错不放行

基类构造形态承 CHS ProviderError(errors.py:82);GatewayUnavailableError.retry_after_s非可选 float(CHS ProviderUnavailableError:155-176 同),业务侧 arq 延期重投(workers/tracking.py:406-428)只需 catch 基类。全部类自 polygateway 顶层导出(§5.1 约定②)。

勘误(反哺 ARCHITECTURE §6.1):该节称 reason 承接"CHS 的 7 种:circuit_open/retry_exhausted/stalled/quota_exhausted/no_sources/backpressure_timeout/probe_pending",但 CHS errors.py:143-153 实际值域是 {network_error, timeout, rate_limited, source_dead, circuit_open, retry_exhausted, stalled}——前者混入了未曾存在的值。重组为两层值域:

值域
scope 级 reason circuit_open / retry_exhausted / stalled(M2 启用)/ quota_exhausted(fail-fast 限流满)/ no_sources
per_source_reasons network_error / timeout / rate_limited / source_dead / circuit_open / cooldown

4. 端口冻结(ports.py,全部 @runtime_checkable Protocol)

4.1 设计轴 A:中间件上下文形态

Middleware 协议:async def __call__(self, request: ChatRequest, call_next: CallNext) -> LLMResponse,CallNext = Callable[[ChatRequest], Awaitable[LLMResponse]]

方案 权衡
A1 不可变 ChatRequest + 派生传递(推荐) 中间件互不污染;衍生请求(结构化反馈重问、overlay 注入)显式 replace();观测数据由 LLMResponse/异常携带,逐次遥测在 RetryMW 内部(它本就拥有尝试编排的全部信息)
A2 可变 CallContext 贯穿 遥测取数方便,但任何层都能偷改状态,重问循环下请求突变不可追踪;三项目 God-method 的病根之一
A3 contextvars 传递 隐式全局,违反纯 asyncio 中立铁律;直接否决

遥测调用点收敛:内部组件 TelemetryEmitter(包装 TelemetryRecorder,从 request+结果/异常组装 18 字段)注入 TelemetryMW 与 RetryMW,是全库唯一调用 record_llm_call 的地方(铁律执行点)。

4.2 设计轴 B:限流端口粒度

方案 权衡
B1 端口在 limiter 层,契约 = CHS 形态(推荐) try_acquire/acquire/source_stats/mark_progress/progress_age_s + Permit(release, settle)(CHS ports.py:494,507);算法在各后端内实现,契约测试一套两后端共跑(CHS tests/contracts_limiter.py 5 项扩充)
B2 端口在存储原语层(incr/zadd 抽象),算法一份在中间件 Redis 六道闸必须单条 Lua 原子执行(§10 原子性),原语端口无法原子组合;否决
B3 无端口,duck-type 两个类 失去 import-linter 可执法的接缝;否决

D3"算法只有一份"在限流处的准确含义 = 语义契约与契约测试只有一份(ARCHITECTURE §7.3 已如此表述);熔断/重试/退避则是字面的算法一份。settle()/release() 幂等、permit 带 TTL 租约、TPM 预扣 est_tokens 结算多退少补,全部进契约测试。

4.3 设计轴 C:熔断端口

方案 权衡
C1 采 CHS gate 契约(推荐):try_enter(source, owner) -> Decisionrecord_success(entry)record_failure(entry, reason, force_open)release_probe(entry)retry_after_s(sources)(provider_gate.py:105-169) 半开探针租约(TTL 防探针死锁)与 epoch fencing(防旧世代写回)在契约内;M2 的 Redis 版零适配移植;内存版以 VT breaker.py 状态机为蓝本按此契约重写(时钟构造注入)
C2 采 VT 简单 API(is_open/record_failure/force_open/record_success) 内存版零改动,但探针 owner 与 epoch 无处安放,M2 Redis 版被迫另立 API → 中间件写两套分支,违反算法一份
C3 两套 API 并存各配各的 同上,直接否决

Decision/Update 数据类(allowed/state/epoch/is_probe/probe_owner/retry_after_ms;applied/state/epoch/failure_count/retry_after_ms)照 CHS ports.py:405,442 冻结。有效阈值 max(configured, concurrency*2) 由装配层自动计算(三项目 .env 注释的手动约定入库)。

4.4 其余端口(无争议,直接冻结)

端口 签名要点 出处/理由
Transport async def complete(*, messages, source: SourceConfig, stream: bool, overlay: dict, call_id: str) -> TransportResult;内部含看门狗、SSE 解析、§6.2 错误翻译 D2;overlay 承接注册表 thinking 参数与 NativeSchema 的 response_format
CacheBackend async get(key) -> str | None / async set(key, value, ttl_s) -> None;key 公式上移至 CacheMW(算法一份),后端是笨 KV VT redis_cache.py 的 key 构造从类中析出;内存版 dict+TTL 供测试与无 Redis 场景
TelemetryRecorder async record_llm_call(*, <18 字段>),keyword-only;15 旧字段名照 VT/GovDoc Protocol 逐字保留 + source_name, usage_source, cost 表名 llm_calls、列只增不删 → VT 既有分析脚本继续可用
SourceSelector def order(sources, stats) -> list[SourceConfig];首发 round_robin / least_inflight CHS selector.py:12 逐字
StructuredOutputStrategy def request_overlay(schema: dict | None) -> dict + def parse(text: str) -> Any(失败抛 ResultInvalidError) 双实现见 §5
时钟/随机注入 有状态组件构造收 now: Callable[[], float]sleeprng(CHS governance.py:85-89 形态) P6 可测试性

5. 设计轴 D:结构化输出阶梯落位(D14 细化)

方案 权衡
D1 独立 StructuredMW,层序 遥测→缓存→结构化→重试→transport(推荐) 重问 = 再次调用 call_next(天然照过限流/熔断门、逐次遥测);缓存在其外 → 只缓存阶梯通过的最终响应(D14"不固化坏结果"自动成立);反馈改 messages 经 replace() 显式派生
D2 循环写在 client.chat() 里(洋葱之上) 重问绕过遥测/缓存中间件,需手工补记账;循环与洋葱两处编排,回到 God-method
D3 策略塞进 transport transport 管协议不管语义;重问跨 attempt,与"transport 无重试"的 D2 职责划分冲突

细则(D14 留给本设计的三件事):

  1. 反馈模板(库内常量,英文、零业务词):重问 = 原 messages + assistant(原始坏输出) + user("Your previous reply was not valid JSON matching the required schema. Errors: {errors}. Reply with ONLY the corrected JSON object."){errors} 取修复/校验错误前 3 条、每条截断 200 字符(防 prompt 膨胀)。
  2. 策略升级:首次尝试用装配时策略(注册表声明 supports_native_schema 则 NativeSchema,否则 JsonRepair);重问一律叠加 NativeSchema(若支持)——修复失败说明 prompt 约定不够,升级到协议约束。
  3. 归一化钩子:VT/GovDoc 的 _normalize_action(loop.py:379-399,DeepSeek 平铺参数收拢)是面向特定业务 schema 的归一化,入库即违反零业务假设。库提供 JsonRepairStrategy(normalize: Callable[[Any], Any] | None = None) 注入点,业务侧自带归一化函数;ResultInvalidError 携带的 raw_text 保证兜底可行。

structured="json" 档 = 仅阶梯②(围栏剥离 → json_repair → json.loads),产物为 dict/list,失败抛 ResultInvalidError 不重问——恰好继任 CHS 被跨模块偷 import 的 _extract_json_object(extractors.py:32-51,其"失败即抛、不修复"语义由 max_structured_retries=0 默认覆盖)。max_structured_retries 默认 1,装配级配置;CHS 迁移设 0。

6. 设计轴 E:SSE 截断语义

现状分歧:VT 缺 [DONE] 一律 _SseAnomaly("truncated_no_done") 重试(llm.py:585-586);CHS 区分 early_eof(零内容,retry)/ missing_done(有内容,打捞)(invokers.py:307-313);GovDoc 无检查(缺陷)。

方案 权衡
E1 一律 strict 重试 语义最干净,但 CHS 迁移丢打捞行为(迁移文档常驻约束:旧版行为不得隐式丢弃)
E2 一律打捞 截断响应可能进缓存被固化,违反缓存防毒化精神
E3 per-source missing_done: "retry" | "salvage",默认 retry(推荐) 默认严格(防缓存固化截断);CHS 迁移配 salvage 保留原行为;early_eof(零内容)恒 retry 不可配

打捞路径强制 usage_source="estimated"(usage 帧通常随 [DONE] 前的末帧丢失)。

7. provider 注册表(providers.py)

@dataclass(frozen=True)
class ProviderProfile:
    name: str
    thinking_on: dict          # enable_thinking=True 时并入请求体
    thinking_off: dict         # enable_thinking=False 时并入
    strip_think_tags: bool     # 响应 content 中 <think> 剥离(qwen)
    supports_native_schema: bool = False

首发注册:qwen({"enable_thinking": True} / {"enable_thinking": False}、剥 think 标签)、deepseek({"thinking": {"type": "enabled"}} / {"thinking": {"type": "disabled"}})、openai(全空,基线)。reasoning_content 增量提取是 OpenAI 兼容 SSE 的通用行为,归 transport 不进 profile。查找按 SourceConfig.provider 精确匹配,未注册即装配期报错(消灭子串猜测);register_provider(profile) 公开,新 provider 一个条目零核心改动(D11)。

8. 配置面定稿(from_env 键名全集)

说明
多源 {SCOPE}__{PROVIDER}__{N}__{FIELD},FIELD ∈ CHS 全集(config.py:95-104)+ ENABLE_THINKING / TTFT_TIMEOUT_S / INTER_TOKEN_TIMEOUT_S / MISSING_DONE / TRUST_ENV 聚合为 list[SourceConfig]
scope 全局闸 {SCOPE}__GLOBAL__MAX_CONCURRENCY / RPM / TPM CHS 同名
per-scope 韧性 {SCOPE}__RETRY__MAX_ATTEMPTS / BACKOFF_BASE_S / BACKOFF_MAX_S;{SCOPE}__BREAKER__FAIL_THRESHOLD / COOLDOWN_S / PROBE_TTL_S;{SCOPE}__BACKPRESSURE__STALL_WINDOW_S / POLL_INTERVAL_S;{SCOPE}__SELECTOR;{SCOPE}__QUOTA_FULL(wait/fail_fast) CHS 键名 + 新增 PROBE_TTL_S、QUOTA_FULL
平铺简写(单 scope) LLM_TIMEOUT / LLM_MAX_RETRIES / LLM_RETRY_BASE_DELAY / LLM_RETRY_MAX_DELAY / LLM_CIRCUIT_BREAKER_THRESHOLD / LLM_CIRCUIT_BREAKER_COOLDOWN / LLM_TTFT_TIMEOUT / LLM_INTER_TOKEN_TIMEOUT VT/GovDoc 现有键零改名迁移;与 scope 键并存时 scope 键优先(ARCHITECTURE §9)
装配选择 PGW_LIMITER_BACKEND / PGW_BREAKER_BACKEND(memory|redis,M1 仅 memory)、PGW_CACHE_BACKEND(redis|memory|none)、PGW_TELEMETRY_BACKEND(sqlite|none)、PGW_TELEMETRY_SQLITE_PATHPGW_CACHE_NAMESPACE(缓存启用时必填)、PGW_CACHE_TTL_S(必填 >0)、PGW_STRUCTURED_MAX_RETRIES 缺关键键直接报错,严禁默认值兜底
复用 REDIS_URL VT 同名

from_env(scope: str = "LLM") 按 scope 装配单 client;多逻辑角色 = 业务侧对每个角色调一次 from_env(scope=...),共享状态后端须显式传入(§7.7 R5)。VT 的 SEARCH_LLM_MODEL 旧式键由其迁移文档映射为 SEARCH__...,库不做旧键兼容层。

9. 旧版行为审计(替换 VT/GovDoc GovernedLLMClient 栈 + CHS 治理循环)

# 旧行为(出处) 处置
1 chat() cache_salt(仅 VT,llm.py:274;调用点 loop.py:336) 保留(GovDoc 无此参 = 超集兼容)
2 熔断半开单探针(仅 VT breaker.py:45-48 内存 flag;GovDoc 每调用放行) 替换为带 TTL 租约探针(C1 契约);GovDoc 无探针行为有意放弃(缺陷)
3 熔断时钟逐调用传 now(VT/GovDoc) 替换为构造注入 now()(等价可测,签名更稳)
4 有效阈值 = max(threshold, concurrency*2)(.env 注释约定) 保留,装配层自动算
5 遥测:VT 持久连接+Lock+close() / GovDoc 每写新建连接 采 VT 形态 + asyncio.to_thread 桥接;GovDoc 形态放弃
6 遥测初始化/写入失败降级不冒泡、INSERT OR IGNORE 幂等、表 llm_calls 15 列 全保留;列只增(source_name/usage_source/cost)
7 缓存:sha256(model+messages[+salt])、TTL>0 校验、读写失败静默降级(VT redis_cache.py) 语义保留;key 公式替换(必填 namespace + 多模态 part 先摘要)→ 迁移后旧缓存一次性冷启动(迁移文档已记);GovDoc 无 salt/无隔离放弃(缺陷)
8 瞬时判定:VT 宽(TimeoutException+TransportError)/ GovDoc 窄 采宽集(GovDoc 窄集放弃——ConnectError 漏网即 bug)
9 429 body insufficient_quota → SourceDead;Retry-After 仅秒数(CHS invokers.py:144-166) 保留
10 usage 缺失按 est_tokens 估算 + usage_source 标注(CHS invokers.py:241) 保留;VT"缺失填 0"放弃
11 thinking:VT/GovDoc 子串猜测+注入开启;CHS 配置驱动+注入关闭 替换为注册表 profile + enable_thinking 三态(§2.3/§7)
12 qwen <think> 剥离(llm.py:147-164)、reasoning_content 增量提取(三家同款) 保留(前者 registry 声明,后者 transport 通用)
13 SSE:ping/空行跳过、usage 帧旁路、畸形 JSON → 瞬时、强制 include_usage 保留(三家同源纯函数移植)
14 缺 [DONE]:VT strict / CHS salvage / GovDoc 无检查 E3 可配默认 strict;GovDoc 无检查放弃(缺陷)
15 重试:退避公式 min(base*2^n, max)*uniform(0.5,1.5) 与 Retry-After 取大;每 attempt 新 call_id;致命 force_open 即抛 保留(D13);两家 jitter 系数差异按 VT 收敛
16 CHS 循环:源冷却备忘、gate_rejections==len(sources) 判 circuit_open、ResultInvalid 记成功、finally 嵌套 settle→release、Cancel 时 release_probe(governance.py:107-266) 全保留(RetryMW 蓝本)
17 限流契约:预扣/结算/退款、release 幂等、mark_progress(CHS limiter + 契约测试 5 项) 保留;M1 内存实现同契约
18 缓存命中独立 cache_call_id + 记遥测(VT llm.py 缓存分支) 保留(cache_hit=True, latency_ms=0)
19 JSON 解析:VT 内 ≥5 套并存、失败语义不一 替换为 JsonRepairStrategy 单实现;业务归一化经 normalize 钩子外置(§5 细则 3)
20 非流式路径:三家均无(硬编码 stream=True) 新增 stream=False 快路径(单 JSON 响应,仅 total 超时)
21 evolve_llm = llm 隐式共享实例(VT main.py) 放弃隐式;共享 = 显式注入同一状态后端(§7.7 R5)

10. 非功能维度(逐条)

维度 回答
并发与取消 同一 client 被 N 协程并发调用为标准形态:所有中间件无实例级可变请求状态(A1);内存 limiter 用 asyncio 原语,断言单事件循环。CancelledError:退避 sleep / 限流等待 / __anext__ 全可取消;取消路径 finally 依次 release_probe(若探针)→ permit.settle(0)+release()(TPM 退款,承 CHS governance.py finally 语义)→ 尽力遥测(error="cancelled",写失败静默)→ 重抛;库内零 except BaseException
降级方向 缓存 get/set 失败、遥测写失败 → warning 静默;limiter/breaker 后端操作失败 → GovernanceBackendError 上抛不放行(M1 内存后端仅内部 bug 会触发,契约照立,M2 Redis 直接继承)
幂等与重复 settle/release/release_probe 幂等(契约测试);遥测 call_id 主键 OR IGNORE;缓存同 key 重写同值无害
持久化与原子性 SQLite WAL + busy_timeout,单持久连接 + to_thread,崩溃最多丢当次记录;缓存写在阶梯通过后(部分写入 = Redis 单 SET,原子);内存后端无持久化(进程死 = 状态清零,单进程语义下正确)

11. 测试策略

内容
unit(纯函数) 看门狗(fake 迭代器+注入时钟,三层超时逐个触发)、SSE 解析(用三项目录制的真实帧样本二次构造,含畸形帧/usage 帧/缺 DONE)、退避公式、缓存 key 公式(namespace 隔离、salt、多模态摘要稳定性)、注册表、json_repair 链、错误翻译表逐行
契约(参数化后端) limiter:CHS 5 项 + settle 幂等 + 装配守卫;breaker:状态机全迁移 + 探针租约过期回收 + force_open;M1 跑 memory,M2 同套跑 Redis
integration 远程 Redis 缓存(命名空间 pgw:test:{uuid},teardown 清理,断连模拟降级方向)、SQLite 并发写、RetryMW×限流×熔断组合(换源/冷却/circuit_open/取消穿透——asyncio.Task.cancel 注入到退避与流式中途)
e2e 真实网关冒烟(流式/非流式/structured 三档),输出落 tests/outputs/;GovDoc 与 Video-Tree 双最小接入冒烟

每步先写失败测试再实现(测试结果门);行为变更的证据 = 本会话内 pytest 先红后绿输出。

12. 纸面兼容验证(调用点反推)

调用点 形态 兼容性
VT loop.py:336 chat(messages, session_id=, cache_salt=) 逐字兼容
VT summarizer.py:236 chat(messages, session_id=, parent_call_id=)
VT evolve.py:566 chat(messages) 纯位置
GovDoc loop.py:377 chat(messages, session_id=, parent_call_id=) ;其 LLMProvider Protocol(protocols.py:15-25)为我方签名子集,结构性满足
CHS tracking.py:406-428 catch ProviderUnavailableErrorexc.retry_after_s/scope/reason GatewayUnavailableError 字段同名(M2 接入)
CHS classifiers.py:12 偷 import _extract_json_object structured="json" 语义覆盖(不修复→修复是增强,失败仍抛异常)

13. 对 ARCHITECTURE.md 的反哺修订(经人类批准后执行)

  1. §6.1 reason 枚举勘误(本文档 §3):scope 级 5 值 + per_source 6 值,替换现文不实的 7 值清单。
  2. §5.1 新增字段清单补 structured_data;§5.2 chat()structured 参数类型定稿为 type[BaseModel] | Literal["json"] | None
  3. §7.1 补 SSE 缺 [DONE] 的 per-source missing_done 语义(本文档 §6,承 CHS salvage 迁移约束)。
  4. §7.9 阶梯④补反馈模板与"重问叠加 NativeSchema"升级细则;归一化钩子外置声明。
  5. §8 模块图 middleware/ 清单补 structured.py(StructuredMW);sources.py 职责注明含选源策略(selector 不单设目录)。