Files
PolyGateway/research-wiki/plans/2026-09-09-134-thinking-contracts.md
T

388 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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.
---
type: plan
node_id: plan:2026-09-09-134-thinking-contracts
title: "1.3.4 推理契约实施计划"
date: 2026-09-09
---
# 1.3.4 推理契约与测试证据实施计划
> 日期:2026-09-09。状态:**自审及 Codex 独立计划审查通过(复审 run ea38c3a7-12ef-4bf0-bb04-257ce37eb96f),T0T7 已实现并通过确定性验证;T8 文档已同步,独立验证/集成/live 与 T9 待执行**。
> 设计:`research-wiki/designs/2026-09-09-134-thinking-contracts-design.md`,用户已正式批准。
> 目标:解决 #21 的受管推理语义漏洞、#25 的测试归因漏洞、#26 的客户端遥测守卫缺口,不扩展生产端口或遥测 schema。
> 方案:在既有推理决策层添加窄校验并接入工厂/默认 transport;测试侧独立保留请求与响应证据,按明确命题判定覆盖。缓存仍由下游显式迁移,生产治理循环不重写。
> 技术:Python 3.12+、asyncio、httpx hooksMockTransport、frozen dataclass、pytest、临时 SQLite、ruff、import-linter;不新增依赖。
本计划不涉及参考实现迁移,保真校验不适用;不得变更 Redis Lua、429/stall、取消结算、结构化重试或 #19#23#24 的生产机制。
## 1. 基线、授权与执行纪律
| 项目 | 固定边界 |
| --- | --- |
| 分支/历史 | `feature/1.3.4-thinking-contracts`;保留已有 `758a127``6a09054`,不重写 main 历史;开始时记录实际 HEAD 与 origin/main |
| D1 | 已登记 AUTO 必须为清单成员;未知 AUTO 保留尽力+warning,空 wire 可能不发送推理字节,不保证开启 |
| D2 | 受管意图非 None 时,两层 raw 推理控制同值/被遮蔽也拒绝;raw-only 保留,不反向推断实际档 |
| D3 | 不添加 fallback 指纹、语义 revision、能力表版本或新缓存前置解析;显式更换 namespace/salt 是操作前置,未迁移可能回放旧语义 |
| 实施权限 | 一工作区仅一 writer;父会话负责前台委派与审核。用户已授权门通过后自行合并 main、测试并发布 1.3.X,无需逐步请示;1.4、新公共决策、验证豁免须停下确认 |
| 证据与秘密 | 不打印 `.env`、token、Authorization、私有提示词;不提交 `.pi/`、运行报告或 reference;命令输出只记录安全路径、状态、退出码 |
T0 开始调用 `writing-plans`T1T7 行为测试执行 `test-driven-development` 并阅读其 testing-anti-patternsT1T5 落日志前执行 `structured-logging`。每次提交执行 `commit` skill(英文祈使标题、无 AI 签名、显式路径暂存),T8 前执行 `requesting-code-review``verification-before-completion`,收到意见执行 `receiving-code-review`;异常先用 `systematic-debugging` 定根因。
用户自主发布授权涵盖既有发布清单的真实 slow 套件,沿既有配置/轮次/并发执行,不重复索取这一授权。超出既有测试矩阵的新研究实验先提交型号、轮次、并发和费用预算;禁止借研究名义追加裸 HTTP 对照。设计要求的新增覆盖先用现有矩阵表达,无法表达且增加调用量时升级该预算决策。
## 2. 文件职责与不变接缝
| 创建/修改 | 精确路径 | 职责 |
| --- | --- | --- |
| 修改 | `src/polygateway/thinking.py` | AUTO 成员检查、nearest 边界、wire 与 raw 冲突纯校验、错误及未知告警文案 |
| 修改 | `src/polygateway/providers.py` | MiniMax on_base 改空;修正空 wire docstring,不在声明层引入决策依赖 |
| 修改 | `src/polygateway/client.py` | `_guard_thinking` 校验源 raw`chat` 请求显式档+已知 raw 冲突前置校验;指纹不改 |
| 修改 | `src/polygateway/transports/openai_compat.py` | `_build_payload` 完整守卫,两层浅覆盖次序不改,沿 complete 的异常翻译 |
| 修改 | `tests/unit/test_thinking.py``tests/unit/test_providers.py` | 纯解析、声明、已知/未知/自定义 wire、告警与边界 |
| 修改 | `tests/unit/test_client.py``tests/unit/test_config.py``tests/unit/test_openai_compat.py``tests/unit/test_retry.py` | 工厂、请求前置、真实 transport、无 HTTP 拒绝与治理收尾;离线兼容 |
| 修改 | `tests/unit/test_cache.py` | 显式迁移和未迁移风险回归;保留旧键黄金值 |
| 修改 | `tests/unit/test_embedding.py``tests/unit/test_ocr_client.py``tests/unit/test_telemetry.py``tests/unit/test_monkey_ocr.py` | 三入口 NULL、阳性、SQLite 与 wire;不改变生产 emitterclient 循环 |
| 新建 | `tests/live_evidence.py` | 测试专用 frozen 证据、有限归因、身份与覆盖判据、逐轮安全报告;无环境自读取 |
| 新建 | `tests/e2e/conftest.py` | 测试侧 hooks、任务局部关联、薄 transport 委托与配置装配;不复制生产 payload/重试算法 |
| 新建 | `tests/unit/test_live_evidence.py` | 分类、hooks/委托器和报告离线反例;导入新 conftest 中无副作用定义,不导入读 .env 的 live 模块 |
| 修改 | `tests/e2e/test_smoke_gateway.py``tests/e2e/test_compat_projects.py``tests/e2e/test_embed_probe.py``tests/e2e/test_thinking_live.py` | 迁入窄证据通道、逐轮完整性与命题分流,保留必须真实执行的行为断言 |
| 修改 | `README.md``CHANGELOG.md``.env.example``research-wiki/ARCHITECTURE.md``research-wiki/designs/2026-09-04-reasoning-effort-design.md` | 用户可达迁移说明、架构同步、旧设计被替代指针;不追改历史实验事实 |
| 修改/登记 | `research-wiki/schemas/llm-calls.md``research-wiki/metrics/call-telemetry-coverage.md``research-wiki/graph/edges.json``research-wiki/index.md``research-wiki/log.md` | 复用既有实体,登记本计划与四种遥测口径;只接受工具对相关实体的必要索引更新 |
| 新建(验收时) | `research-wiki/findings/2026-09-09-134-thinking-contracts-validation.md` | 红绿、变异、失败与豁免索引,≤300 行;原始输出留 `tests/outputs/134/` |
| 修改(发布时) | `pyproject.toml``src/polygateway/__init__.py` | 两处版本一致到 1.3.4,不改变依赖或导出面 |
生产不修改 `ports.py``types.py``errors.py`、cachetelemetry 实现及 embedding/OCR 循环;若实际实现需要突破该清单,先说明设计要求与最小原因,由父会话核定,不顺手改动。
## 3. 跨任务接口(内部实现约定,不新增公共导出)
### 3.1 推理守卫
新函数置于 `thinking.py`,其余模块显式 import;保持决策方向 `client/transport → thinking → providers/types``Mapping``Any``Effort``ThinkingWire` 均为既有类型。函数体由 T2 实现,以下固定消费者签名:
```python
def validate_thinking_wire(wire: ThinkingWire, *, model: str) -> None:
"""拒绝 on_base 偷带已知强度,抛 ThinkingUnsupportedError。"""
def validate_thinking_raw(
raw: Mapping[str, Any], *, effort: Effort | None,
wire: ThinkingWire | None, origin: str,
) -> None:
"""effort 表态时拒绝 raw 控制;wire=None 只检查标准词表。"""
```
签名中的 wire 必填但可 None,None 是 chat 前置看不到实际 profile 的事实,不是容错默认;origin 只取固定位置名/源名,不含 raw 值。两函数无 I/O,不改变输入,抛现有 `ThinkingUnsupportedError`ValueError 子类),不新建错误类。`validate_thinking_wire``resolve_thinking` 的 None 早退之前验证声明结构;不会要求无意图时 wire 必须已知,只拒绝结构上偷带强度。
标准 raw 根:`reasoning_effort``enable_thinking``thinking``thinking_budget``reasoning``thinkingConfig``output_config` 为 Mapping 且有 `effort` 时冲突。wire 可见时并入 on_baseoff 全部顶层根与 effort_key,点号只是字面键。
wire 校验只拒绝 on_base 中自己的非 None effort_key、标准 reasoning_effort、标准嵌套 output_config.effort(含值为 auto/None);不解析任意私有方言。未知/非当前方向的形态检查继续由现有 `_wire_unknown_for` 负责,不改变 off-only 可用性。
### 3.2 测试证据与归因
`tests/live_evidence.py` 不 import e2e conftest、不读环境、不发网络。上下文中的密钥只做内存比较,不进下列值类型。一个 attempt 可以记录零/一/多 HTTP 事件;不能用 len(attempts) 冒充 HTTP 数。
```python
@dataclass(frozen=True)
class HttpEvidence:
call_id: str
request_checks: tuple[tuple[str, bool], ...]
status_code: int
error_body: bytes | None
raw_identity: tuple[bool, str | None]
```
`raw_identity` 是 T5 生产、T6 消费的**成功非流式原始身份快照**:(False, None) 表示未取证,(True, None) 表示已独立解析完整 JSON 对象且 model 缺失/为 null(True, str) 表示原始字符串。由 response hook 保存的响应引用在该次 complete 结束后读取已缓冲 content,独立 JSON 解码(拒绝重复键)取得;不得从 TransportResultLLMResponse.model_reported 回填。未缓冲、无可配对响应、非成功非流式、JSON 非对象/非法或 model 非字符串非 null 均不生成肯定证据,并保存原因供 FAIL;不把解析异常解释成身份缺失。成功 SSE 始终 (False, None),不新增捕获器、不预读流。
`request_checks` 必须完整包含 methodoriginpathmodelstreamembedding 为 input_shape)/authorizationcontrolmessages_digest,缺项不算全过;error_body 只允许 ≤65536 字节已缓冲完整错误体,超限或未缓冲为 None,并在安全报告写证据不足,不把截断文本拿来解析。记录只存在测试内存,不能直接 asdict 后落盘。
```python
@dataclass(frozen=True)
class AttemptEvidence:
call_id: str
http: tuple[HttpEvidence, ...]
error: Exception | None
```
```python
@dataclass(frozen=True)
class LiveVerdict:
status: Literal["PASS", "FAIL", "UNCOVERED"]
reason: str
```
消费者固定为 T5→T6,纯函数与报告出口如下;参数所用 Path、Mapping、Sequence、ThinkingObservation、Effort 为标准库/现有领域类型:
```python
def classify_live_failure(
error: Exception, attempts: Sequence[AttemptEvidence],
) -> LiveVerdict:
"""异常分类仅 FAIL/UNCOVERED;取消不交给此函数。"""
def write_live_round(
output_dir: Path, *, run_id: str, matrix_id: str, round_index: int,
safe_fields: Mapping[str, Any],
) -> Path:
"""只接受已脱敏报告字段,唯一文件写失败必须冒泡。"""
```
身份与命题判定继续放 `tests/live_evidence.py`,避免 e2e 中四份条件分支:
```python
def assess_model_identity(
*, requested: str, aliases: frozenset[str], reported: str | None,
raw_identity: tuple[bool, str | None], request_valid: bool,
) -> LiveVerdict:
"""raw_identity[0] 表示有独立原始身份取证;无证据不得归因上游。"""
def assess_thinking_coverage(
observations: Sequence[ThinkingObservation], *, planned_rounds: int,
proposition: Literal["enabled", "disabled", "cannot_disable"],
) -> LiveVerdict:
"""输入须先过请求/身份资格;缺轮与 UNKNOWN 不补足证明。"""
```
拒绝能力测试不送入以上观测函数,按预声明异常类型/状态/机器字段独立断言。`cannot_disable` 保留 T10“实际未关闭与声明比较”的反证形态:完整合格轮次有 OBSERVED 可支持本条件下不可关闭;全 ABSENT 证伪声明;无 OBSERVED 但有 UNKNOWN 只能未覆盖。不得扩大成“证明所有私有上游参数都无法关闭”。
### 3.3 测试侧取证装配
`tests/e2e/conftest.py``ObservedTransport` 包裹**同一个**真实 `OpenAICompatTransport`completeembed 签名逐字保持 `ports.py`,参数原样传递。每次调用将 call_id 绑定实例持有的 ContextVarfinally reset;异常原样上抛,CancelledError 不转普通错误。不得在委托器做治理重试或 payload 修正。
```python
class LiveCapture:
def __init__(self, *, expectations: Mapping[str, Mapping[str, Any]]) -> None:
"""按源名持有测试矩阵显式预期,不含凭据或真实响应。"""
def round_context(self, *, session_id: str, parent_call_id: str) -> AbstractContextManager[None]:
"""外围绑定逻辑轮次,finally 复位,嵌套任务不串线。"""
def client_factory(self, source: SourceConfig) -> httpx.AsyncClient:
"""按实际源构造带 hooks 客户端,沿生产 timeout/trust_env。"""
def attempts(self, *, session_id: str, parent_call_id: str) -> tuple[AttemptEvidence, ...]:
"""返回本轮快照,含零 HTTP 的尝试,不从最终异常猜前序。"""
def raw_identity(self, *, session_id: str, parent_call_id: str, call_id: str) -> tuple[bool, str | None]:
"""精确读取本逻辑轮次和成功 attempt 唯一 HTTP 事件的原始身份快照。"""
```
`LiveCapture.expectations` 由调用者按源名提供,内层必需键为 model、origin、path、streamembedding 用 input_shape)、control、messages_digest;缺键直接测试配置错误,不自动从待测 payload 补齐。结构化预期片段放 control 的显式预期对象,输出路径由 write_live_round 单独接收;并发异构轮次使用各自 capture 实例,不共享可变“当前预期”。预期不调用生产 `_build_payload` 生成。实际请求 hooks 逐项比较,凭据不进 repr/序列化;可保留失败事实后由轮次出口 FAIL,不能改写实发请求“修正”它。
取证 live 使用 `GatewayClient(...)` 全量注入。conftest 可复用 `client.py` 现有 `_build_limiter``_build_breaker``_build_selector``_build_structured` 等装配函数,但不新增生产注入口、不复制它们实现;自己创建的组件用 ExitStack/显式 finally 关闭,注入 GatewayClient 不会代关。工厂行为单独离线验证。未获得取证通道的工厂 live,异常一律保存后 FAIL。
T5 按 `(session_id, parent_call_id) → AttemptEvidence.call_id → HttpEvidence` 保存快照;T6 使用最终 `LLMResponse.call_id` 调用 `capture.raw_identity(...)`,将返回值交给 `assess_model_identity(raw_identity=...)`,不可取本轮最后一条响应猜关联。查找必须精确匹配本轮且只有一条成功 HTTP 事件;重复/跨轮 call_id、多个候选响应是取证契约错误,报告后 FAIL,不降成外因未覆盖。未发 HTTP 或没有独立身份快照时返回 (False, None)。
成功非流式原始 model 通过上述快照接口交付;成功 SSE 不加捕获器、不预读流。原始身份无从确认而公共结果缺失/不符时 FAIL;原始正确而结果丢失/改错也 FAIL。只要公共身份合格且请求合格,正常能力测试不要求新增成功 SSE 的原始副本。
## 4. 任务与提交点
### T0:基线、计划审查与文档回滚点
- [x] 修改设计批准状态,新增本计划;父会话自审后前台 Codex 独立审,具体问题修正后方可执行 T1。计划无需再走人类门,不能把“已生成”当“已审”。
- [x] 记录 `git status --short --branch``git log --oneline origin/main..HEAD`、实际 HEAD;确认源代码零差异,保存未跟踪文件清单,禁止暂存 `.pi/`
- [x] 执行基线:`make check``conda run -n PolyGateway pytest tests/unit/test_thinking.py tests/unit/test_providers.py tests/unit/test_client.py tests/unit/test_config.py tests/unit/test_openai_compat.py tests/unit/test_cache.py tests/unit/test_embedding.py tests/unit/test_ocr_client.py tests/unit/test_telemetry.py -q`。记录实际失败,不能先改期待绕过;本步骤预期现有非 slow 测试通过。
- [x]`research-wiki` 工具登记 designplan 节点及 implements 边,现有同路径文档不可被 add_entity 模板覆盖;先读工具已有文件处理行为,再登记、重建索引、检查生成 diff。只在本计划 writer 移交后由父会话执行这些额外文件写入。
- [x] 调用 commit skill,提交点 `docs: record approved thinking contracts and implementation plan`,形成生产修改前回滚点。
### T1AUTO 成员语义与默认 MiniMax wire
**文件**`thinking.py``providers.py``tests/unit/test_thinking.py``tests/unit/test_providers.py`,路径均按 §2。
1. 先添加/替换 `test_auto_never_trips_phase5`,以已登记不含 AUTO 的空/非空 on_base 为反例;调用 resolve_thinking 的 errornearest 都明确拒绝且不提示 nearest。先跑新测试,旧实现因未拒绝而红;再修改 `_settle_tier``_tier_unsupported`,避免 AUTO 进入 `EFFORT_ORDER.index`
2. 保留强度→纯开关 AUTO、等距弱侧、NONE 不自动映射、None 不表态、未知空/非空 wire 尽力警告。删除 default MiniMax 的 medium 并修正注释;先测试 M3 AUTO 拒绝、M3 medium payload、M2.5M2.7 AUTO 空 payload 与 applied=AUTO。默认能力表成员不增删。
3. 用 loguru sink 检查未知告警确实说明不保证生效,default transport 实例节流不改;不引入新日志通道,不输出 raw 密钥。按 structured-logging 明确这是既有 warning 与既有列的修正。
**验证**`conda run -n PolyGateway pytest tests/unit/test_thinking.py tests/unit/test_providers.py -q`;新拒绝和 wire 回归先红后绿,其余保留行为通过。若旧下游形态测试依赖 M3 True,需要在 T3 明确改为已批准迁移样本,不能暗改能力表让它绿。
- [x] 提交点:`fix: enforce registered auto reasoning capabilities`
### T2:纯 wireraw 所有权校验
**文件**`src/polygateway/thinking.py``tests/unit/test_thinking.py`。实现 §3.1 两个签名;`resolve_thinking` 集中校验 wireproviders 不反向 import thinking。
| 红绿组 | 最小反例/保留不变量 |
| --- | --- |
| 标准控制 | 六个顶层根+output_config.effortAUTO 空片段仍拒绝 raw highNONE/糖/未知同测 |
| 双来源 | 同值仍拒绝;分别传源与请求 raw 验证,被后层遮蔽也拒绝;不修改两个 Mapping |
| 自定义 | on_baseoff 根并集、effort_key 自定义字面键、点号不解释路径;on_base 偷带自己的键或标准强度值(含 None)拒绝 |
| 嵌套 | 替换 thinking 整个根即拒绝;profile 不拥有 output_config 时仅 format 可过、effort 不可过;拥有根时 format 也不可覆写 |
| 不误伤 | effort=None 时 raw 原样允许;temperatureseedresponse_format 不属词表,合法普通采样保持;off-only 形态及当前方向未知语义保持 |
新校验首次未实现导致的 import 错误不算行为红;可先在隔离基线把同输入经现有 payload 路径表现记录为“覆盖成功但本应拒绝”,或待 T3 在旧实现回放其失败断言,补齐语义红证据。纯函数自身还需逐例断言异常及未修改输入。
**验证**`conda run -n PolyGateway pytest tests/unit/test_thinking.py -q`,每类目标反例有有效红绿,保留行为绿。
- [x] 提交点:`fix: validate ownership of managed reasoning parameters`
### T3:接入工厂、请求入口和默认 transport
**文件**`src/polygateway/client.py``src/polygateway/transports/openai_compat.py``tests/unit/test_client.py``test_config.py``test_openai_compat.py``test_retry.py`
先在旧路径跑源 HIGH+相同 raw HIGH、本次 AUTOraw HIGH 的行为反例,确认旧实现实际发出 raw 参数而未拒绝。再接入两守卫:工厂先求 effective_effort 并校验 source.extra_bodychat coerce 后调用 wire=None 的已知词表检查;transport 对当前 profile 和 effective_effort 分别检查 extra_bodyoverlay,再保持原浅 update 顺序。
| 接缝 | 验收 |
| --- | --- |
| 工厂 | SourceConfig 仍能表达 raw-onlyfrom_envfrom_settings 对受管源拒绝发生在 limiterHTTP client 创建之前;记录构建计数零,不以网络偶然没发代替 |
| 请求前置 | 显式请求档与 overlay 已知键冲突,ValueError 且 handler/准入未触发;源级意图或自定义根留 transport 再查 |
| 全量注入 | 真实 OpenAICompatTransport 翻译为 RequestRejectedErrorMockTransport 记录零 HTTPRetryMW 不换源不重试、limiter inflight=0、已有探针收尾路径正常 |
| 参数保真 | raw-only 允许,applied=None;普通采样源<请求<结构化 overlay 的现状保留;显式档/nearest 成功 payload、TransportResultLLMResponse applied 与真实成功遥测相符 |
| 多源/并发 | 每次以选中源 profile 校验,不因另一个源清单不同提前判整个池死;共享 client 无“最后档”串线;错误不包含 raw 值 |
工厂将来被请求覆盖不能救活一个已拒绝源,这是已批行为。不要为全量注入自定义 transport 添加 preflight 端口。已有 fixture 需要调整时,只将不再合法的受管+raw 双来源改为显式单来源,新增拒绝反例保留迁移证明。
**新增生产默认 HTTP factory 离线守卫**:现有 `tests/unit/test_openai_compat.py` 没有 authtimeouttrust_env 构造断言,不能写作“保留”。新增 `TestDefaultClientFactory`,直接调用生产 `_default_client_factory(source)` 返回真实 AsyncClient,不使用 T5 的测试 factory,也不 mock 整个 AsyncClient。用两组不同假 api_key、非默认 timeout_s、trust_env=TrueFalse 参数化;不发送网络,finally aclose。节点为 `test_authorization_uses_source_api_key`(检查 client.headers 及 build_request 生成的 Authorization)、`test_timeout_uses_source_timeout_for_all_phases`connect/read/write/pool 全部等于输入 timeout_s)、`test_trust_env_uses_source_setting`(检查 client.trust_env)。在隔离副本逐个删除 Authorization 传入、遗漏 timeout 参数、遗漏 trust_env 参数/写死 True,指定节点必须因值不符红,再恢复通过;当前实现本来正确,以这些语义变异作为红证据,不改生产 factory 凑红。
独立命令:`conda run -n PolyGateway pytest tests/unit/test_openai_compat.py::TestDefaultClientFactory -q`,原始实现绿、每个遗漏变异被对应断言杀死、恢复绿;这套测试与 T5 hooks 校验分别验证生产装配和测试取证两条路径。
**验证**`conda run -n PolyGateway pytest tests/unit/test_client.py tests/unit/test_config.py tests/unit/test_openai_compat.py tests/unit/test_retry.py -q`,加 T1/T2 的测试一起跑;工厂/请求/全量注入拒绝均有旧实现红、新实现绿。
- [x] 提交点:`fix: reject conflicting raw reasoning overrides before sending`
### T4:显式缓存迁移和四种遥测口径回归
**文件**`tests/unit/test_cache.py``tests/unit/test_client.py``tests/unit/test_telemetry.py`;不改生产 cache、指纹或 emitter。
使用真实 InMemoryCache、GatewayClient、默认 transportMockTransport 构造两个客户端。旧语义 payload 可按 1.3.3 真实序列化形态预写(历史数据夹具,不需要在当前生产放回漏洞);同版本 nearest→error 则运行真实客户端写入。
| 场景 | 断言 |
| --- | --- |
| 旧 AUTO/raw 记录 | 旧身份可回放是已知风险;换全新 namespace 或 salt 后 miss,实际进入新拒绝,异常不缓存 |
| nearest→error | 源不表态、请求 medium、模型 glm-5.3nearest 写入后 error 同身份可命中;error 换身份后零 HTTP 拒绝,不添加 fallback 指纹 |
| 能力表变化 | 新增 `TestExplicitCacheMigration::test_capability_change_requires_explicit_identity`:相同源配置、请求 AUTO 和 wire,两客户端注入同一测试模型的不同能力表(旧含 AUTO+HIGH,新仅 HIGH),源级不表态以允许装配。旧客户端真实写入后新客户端同身份回放且无新 HTTP;换全新 namespace 或 salt(参数化)后 miss,进入新能力表并 RequestRejected、无新 HTTP、不写失败值。只用局部测试能力表,不修改 DEFAULT |
| 自定义 wire 变化 | 新增 `TestExplicitCacheMigration::test_custom_wire_change_requires_explicit_identity`:相同源/模型/能力表及请求 HIGH,分别注入同名自定义 profile 的旧/新 effort_key(例如 depth_adepth_b),on_base 均为空。旧客户端写缓存,新客户端同身份回放旧值且无新 HTTP;新 namespace 或 salt 后 missMockTransport 必须收到 depth_b=high 且无 depth_a,返回可区分的新结果,旧身份仍能回放旧值。profile 仅局部注入,不改源配置让现有指纹意外变化 |
| 入口与范围 | 工厂默认 namespace、per-call 覆盖默认、构造全量注入、共享多源 scope、两个租户原前缀保留;只改默认无法覆盖 per-call,需专门反例 |
| 并行/回滚 | 旧新身份可并行且不覆盖对方;回到旧身份确实重见旧值;未受影响调用 key 黄金值逐字不变 |
| 四行口径 | 真实成功=applied、失败尝试=effective 意图、cache_hitscope 终态=本次请求级;缓存不读取历史 applied 作本次遥测档 |
该任务多数是已有正确行为的守卫,不人为改生产获得红:隔离变异遗漏 namespacesalt、将 cache_hit 遥测改读历史 applied,要求相应行为断言红,恢复后绿。T3 新拒绝路径旧实现红绿可复用,但不能只报它替代迁移维度证据。
**验证**`conda run -n PolyGateway pytest tests/unit/test_cache.py tests/unit/test_client.py tests/unit/test_telemetry.py -q`。两项新增能力/wire 迁移节点均置于 `tests/unit/test_cache.py::TestExplicitCacheMigration`,单跑 `conda run -n PolyGateway pytest tests/unit/test_cache.py::TestExplicitCacheMigration -q`;分别在隔离副本去掉其 namespace/salt 隔离输入,必须因没有新拒绝/新 wire 而红,恢复后绿,不能只以 nearest→error 的测试代替这两类。两客户端的源指纹必须断言相等,生产指纹算法一字不改。
- [x] 提交点:`test: pin explicit cache migration and reasoning row semantics`
### T5:有限测试归因和独立取证
**文件**:新增 `tests/live_evidence.py``tests/e2e/conftest.py``tests/unit/test_live_evidence.py`。按 §3.2/3.3 实现;不读 .env 的模块可被日常单测安全 import。执行 structured-logging:记录内容按设计 §6,不另建库表。
1. 纯分类默认 FAIL。仅一条完整 HTTP 错误、请求检查齐全、无别的 attempt 异常、404、完整无重复键 JSON 的 error.type 精确匹配、外抛 RequestRejectedError 且 status 一致,才 UNCOVERED;多次尝试/多 HTTP、不同 call_id、空检查元组、重复 JSON 键均 FAIL。
2. hooks 不预读成功 SSE,不将 summary 当 JSONresponse 引用等该次 transport 完成后检查 content 是否已缓冲。64 KiB 上限、0 字节、非对象 error、重复键、坏编码各有反例。薄委托器 finally 恢复上下文,零 HTTP 尝试也保存。
3. 身份函数区分 raw 取证缺失和原始响应明确缺 model;增加 `test_raw_identity_snapshot_reaches_round_consumer`,用真实默认 transportMockTransport 非流式响应依次覆盖正确 model、缺失/null,以及 JSON 非对象/非法/重复键,断言 §3.3 accessor 的来源和区别;故意让公共响应丢 model 时原始快照仍保留正确串并判 FAIL。并发两逻辑轮次+一次重试验证按 sessionparent/成功 call_id 精确选择,不回放前次失败的身份;成功 SSE 快照未取证且公共身份异常时必须 FAIL。覆盖函数按 enableddisabledcannot_disable 命题判断,UNKNOWN 不假绿;预期 400 负向契约单独测。
4. 用 MockTransport 驱动 requestresponse hooks:改错 model、Authorization、端点、SSEJSON 解析→FAIL;成功 SSE 不被提前消费;原始 model 正确但公共字段错误→FAIL。交错并发及取消证明 context reset、凭据不泄露、资源释放;薄委托器不额外调用一次 HTTP。
5. 安全报告每轮独立文件,采用 run UUID+轮次与矩阵安全标识;只接受白名单 safe_fields,拒绝原始异常/HttpEvidence 对象直接序列化。第二轮失败仍可读第一轮;写入失败是 FAIL;最终汇总统计 PASSFAILUNCOVERED 和缺轮,不能只数 pytest 退出码。
对旧策略红证据:用合成记录隔离执行现有“整类 skip/正文子串/UNKNOWN 安静”判据,目标测试要求 FAIL/UNCOVERED,确认语义不符;恢复新纯函数后通过。新文件缺失造成 import error 不计红。
**验证**`conda run -n PolyGateway pytest tests/unit/test_live_evidence.py -q`。安全测试使用假的唯一 sentinel 凭据/私有提示词,逐文件检查不出现 sentinel,不能拿真实密钥做输出搜索。
- [x] 实现及离线证据完成:窄分类/hooks/独立身份/逐轮安全报告;与 T6 合并提交。
### T6:迁移四个 live 文件并离线化装配断言
**文件**:四个 `tests/e2e/test_*.py` 路径见 §2`tests/e2e/conftest.py``tests/unit/test_live_evidence.py``tests/unit/test_client.py``tests/unit/test_config.py`
| 原接缝 | 改动与离线验收 |
| --- | --- |
| smokecompat chat | 每轮 session_idparent_call_id,委托原参数,先报告再 skip/raise;仍验证流/非流、结构化 JSON/模型。断言异常也必须留报告,不只包 await 的异常 |
| compat 平铺键 | 移到 test_config.py 的完整合成 env,删除逐源 TIMEOUT_S 才能验证 LLM_TIMEOUT 回落;不靠真实配置“恰好已有覆盖”过测。无 HTTP、无可达 Redis/PG,明确其仅是本库兼容契约 |
| compat Protocol | 既有真实外部 Protocol 缺包时记录未覆盖;合成 runtime Protocol 和本库调用签名在 test_client.py 无 slow 执行,不能宣称缺失仓库原测试通过 |
| embedding probe | 走同一薄 embed 委托和窄分类,所有路径 finally 关闭;model_not_found 不写成“网关不支持 embeddings”;timeouttrust_env 取已校验源配置,不用 30s 硬编码压紧生产预算 |
| L1L9 | M3 True 拒绝单独离线/本地断言,真实开启用显式 medium,保留未登记/未知 wire 场景;L8 装配拒绝只计本地契约,不算 live 能力 |
| T10 | 预声明 NONE 可关闭/不可关闭/档位预期拒绝;每轮结束即留证。去掉“仅保留可用轮降低分母”、completion 长短提升 UNKNOWN 的成功逻辑;模型部分档未覆盖不可汇总全 PASS |
保持原 `_MODEL_PROVIDER`/显式别名表,不新增 AUTO 成员。`_run_rounds``_probe_effort` 返回路径不许遗漏失败轮;默认基线也走同一证据出口。T10 临时能力表仅为探测绕过清单,不写回 DEFAULT;其控制字段预期由矩阵声明,不能调用被测 resolver 产生预期。
既有 `_tier_settings` 将 stall 强制压到 60s,迁移时去掉该临时缩小值,沿已校验生产配置;不得因持续 429 慢而修改 #22 算法。保留既有轮次/并发设置;先收集矩阵和预计调用数,额外研究不自动展开。M2.5/M2.7 AUTO 复用 T10 现有登记档,M3 medium 流/非流复用对应原开启用例,不以新增多轮研究暗增预算。
测试工厂替代仅改变取证装配,不能靠调用生产私有 `_client_factory` 的同一实现来证明鉴权构造正确;生产默认 factory 的头/timeouttrust_env 离线守卫由 T3 **新增** `TestDefaultClientFactory`,T6 验证时一并运行,不宣称旧源码已有覆盖。各能力轮次用 §3.3 的 `raw_identity(session_id=..., parent_call_id=..., call_id=resp.call_id)` 给身份函数提供独立快照,缺失来源不得猜测。live 无取证通道的失败按 FAIL,不为凑分类额外开生产接口。
**验证**`conda run -n PolyGateway pytest tests/unit/test_live_evidence.py tests/unit/test_client.py tests/unit/test_config.py tests/unit/test_openai_compat.py::TestDefaultClientFactory -q``conda run -n PolyGateway pytest tests/e2e/ -m slow --collect-only -q`(只采集,不视作真实通过)。在离线注入旧整类 skip、丢第一轮、UNKNOWN→PASS、identity 丢失→skip 变异,分别红;新实现恢复绿。
- [x] 实现及日常离线验证/90节点collect-only完成;未执行live,不代表能力覆盖通过。
### T7:无推理路径真链路与四类变异
**文件**`tests/unit/test_embedding.py``tests/unit/test_ocr_client.py``tests/unit/test_telemetry.py``tests/unit/test_monkey_ocr.py`;生产不改。
复用 `_embed_client``_client`/脚本 transport/内存 recorder,参数化 embed、recognize_text、parse_layout 与源 TrueHIGH,两次尝试(Transient→成功)必须恰有 2 行、一错一成、所有 reasoning_effort None。再覆盖 RequestRejected 一行和耗尽非零失败行;不要求新增不存在的逻辑终态。SQL 锚点用真实 `SQLiteRecorder(tmp_path / "reasonless.sqlite", auto_migrate=True)`,三入口分别走 client→emitter→SQLite,查询总数/失败数/NULL 数,finally 同步 close 注入 recorder。
chat 阳性走真实 RetryMWemitterTrue 糖失败 auto、显式请求失败保留意图、nearest 成功为实际映射档。四行遥测继续沿 T4 口径。共享 recorder 并发用测试 sessionparent 配对,attempt call_id 唯一且集合不相交。embedding 默认 transport 和 MonkeyOCR textlayout 真实 MockTransport 回包验证 wire 无推理键,不仅断言 emitter 的 False 实参。
| 隔离变异 | 必须被哪些断言杀死 |
| --- | --- |
| `embedding.py::_emit` False→True | 误配 TrueHIGH 的失败尝试 NULL 断言 |
| `ocr.py::_emit` False→True | text 和 layout 各一个独立节点均因错误行非 NULL 红 |
| `middleware/retry.py::_emit` True→False | chat 阳性实际档/请求档断言,不是签名 TypeError |
| `middleware/telemetry.py::_attempt_effort` 去掉 applies 短路 | 无推理真实失败行断言;确认不是全空数据或假 recorder |
工作方法:以 T7 当前提交建仓库外临时副本(仅 src/tests/必要工程文件,不复制 `.env`reference.pi),用 `PYTHONPATH=<副本>/src` 和副本 cwd 执行 conda pytest;先检查 `polygateway.__file__` 指向副本。逐个变异、跑指定节点记录 exit 1 和目标断言、恢复文件校验散列,再跑 exit 0。绝不在主工作区改 False 假装先红。
**验证**`conda run -n PolyGateway pytest tests/unit/test_embedding.py tests/unit/test_ocr_client.py tests/unit/test_telemetry.py tests/unit/test_monkey_ocr.py tests/unit/test_retry.py -q`,再执行上表隔离变异;原实现绿、四类有效红、还原绿。
- [x] 提交点:`test: guard reasoning-free telemetry through real client paths`
### T8:文档、日志登记与独立验证
**文件**:§2 列出的用户文档/架构/旧设计/schema/metric/知识索引,以及验收 finding;不新增运行时字段。
同步设计 M1–M9 到 README 可执行迁移节与 `.env.example` 注释,保留型号证据来源;CHANGELOG 未发布段点名 AUTO 新拒绝、未知尽力、raw 同值拒绝、显式缓存身份迁移、UNKNOWN/SKIP 限制。schema 既有 reasoning_effort “实际发出”总括修成四种行来源,不修改 DDL;metric 复用既有 call-telemetry-coverage,记录三个无推理入口错误行 NULL/chat 阳性为 100% 契约,真实覆盖基线留待首次实际运行,不能填伪百分比。
由父会话前台派全新 verifier:只给批准设计、计划、分支 diff、验证命令,不给实现自评。至少覆盖正确性/回归和测试归因/范围两个角度;Critical/Important 清零。审查先核对实际路径与仓库语言,不接受不存在文件的结果。补丁回到单 writer,重跑受影响红绿及静态门。
| 检查 | 命令/证据要求 |
| --- | --- |
| 静态与边界 | `make check``git diff --check``conda run -n PolyGateway python -m compileall -q src/polygateway tests/live_evidence.py tests/e2e/conftest.py` |
| 日常全量 | `make test`,保存真实退出码/coverage ≥80%,不能只运行改动文件;连接依赖 skip 单列 |
| LSP | 若会话已有 LSP diagnostics 工具,对四个生产变更文件和新增测试支持文件取诊断;本轮检查 conda 内 pyrightbasedpyright 均未安装且工程无其配置,不安装新依赖或虚报 LSP 通过。可用时命令 `conda run -n PolyGateway pyright src/polygateway/thinking.py src/polygateway/providers.py src/polygateway/client.py src/polygateway/transports/openai_compat.py tests/live_evidence.py tests/e2e/conftest.py`,不可用明确记未执行,ruffimport-lintercompileall 是实际既有静态门,不冒称等价 LSP |
| 真实采集清单 | `conda run -n PolyGateway pytest tests/ -m slow --collect-only -q`,先列必需节点、型号/模式/轮次/并发/所用配置身份(不含秘密) |
| 真实执行 | `conda run --no-capture-output -n PolyGateway pytest tests/ -m slow -ra`;保持生产超时,检查每项报告而非仅 exit 0 |
| 反回归 | 四类 #26 变异+T4 缓存迁移+T5/T6 假绿反例全部有独立失败断言与还原通过,finding 引用原始报告路径 |
长跑用 tmux`PYTHONUNBUFFERED=1`,命令 stdoutstderr 重定向到 `tests/outputs/134/`,原命令后立刻独立保存 `$?`;不得接 `tail` 管道改写退出码。父会话等待准确 tmux 完成信号/PID,不能 pgrep 完整命令自匹配。不把初次失败覆盖成重跑后的单一绿日志。
- [ ] 提交点:`docs: document reasoning ownership and explicit cache migration`;必要修复各自按 commit skill 提交,不把 verifier 自动反馈当授权扩范围。
### T9:发布准备、合并后复验与 1.3.4 发布
仅在 T8 无未处理阻塞后执行。用户已授权所有本节动作,无需为 merge/push/上传再请示;发现 1.3.4 已存在不可覆盖,停下协调版本,不能私自跳到 1.4。
| 顺序 | 精确动作与完成证据 |
| --- | --- |
| 文档先行 | 更新 README 安装约束与能力说明;CHANGELOG 定版为 1.3.4(实际日期),pyproject 和包 `__version__` 同步;本计划复选框只能按已得证据勾选 |
| 发版提交 | 执行 commit skill,标题 `chore: prepare release 1.3.4`,先核对测试报告和 staged 无秘密;运行 `conda run -n PolyGateway pytest tests/unit/test_package.py -q`,不改变公共字段计数 |
| 合并 | `git fetch origin`,确认远端未出现未审变更;`git switch main``git merge --no-ff feature/1.3.4-thinking-contracts`。保留已有两个本地提交,禁止 resetforce push |
| 合并后门 | main 上重新 `make lint``make test``conda run --no-capture-output -n PolyGateway pytest tests/ -m slow -ra`;若 lint --fix 改代码,重新审 diff、提交并重跑,不把脏代码与 tag 分离 |
| 推送与 tag | `git push origin main``git tag -a v1.3.4 -m "Release 1.3.4"``git push origin v1.3.4`,核对远端 tag 指向最终已验证提交 |
| 构建 | 核实 cwd 后按 CLAUDE 清除旧 dist 产物;`conda run -n PolyGateway python -m build``conda run -n PolyGateway python -m twine check dist/*`;缺构建工具先报告环境缺项,不更改核心依赖 |
| 上传 | 从既有 tea 配置安全取 token,仅放 TWINE_PASSWORD 环境变量;`conda run -n PolyGateway python -m twine upload --repository-url https://gitea.iomgaa.online/api/packages/iomgaa/pypi dist/*`TWINE_USERNAME 沿已有账号;不在 argv/日志输出 token,不把命令成功当最终发布完成 |
| 下载检查 | `conda run -n PolyGateway pip download --no-deps --index-url https://gitea.iomgaa.online/api/packages/iomgaa/pypi/simple/ polygateway==1.3.4 -d <临时目录>`;解包核对新守卫与 MiniMax wire、版本、README 元数据;从仓库外使用该环境 Python 将 wheel 安装到独立 target 并验证 import 来源及拒绝行为 |
| 外部可见 | 建 Gitea v1.3.4 Release(正文来自定版 CHANGELOG);调用 `POST /api/v1/packages/iomgaa/pypi/polygateway/-/link/PolyGateway`;查看 Releaseregistry 包页面正文、仓库链接、下载产物,逐项记录 URL 与实际结果 |
测试不在 wheel 内,下载后以无网络小调用核对已安装 `resolve_thinking` 和冲突守卫;不要从工作树 src import 后宣称发布包通过。只在外部结果确认后评论/关闭 #21/#25/#26,正文引用各自验证与迁移边界,不能称所有渠道故障已自动识别。若上传成功但页面/下载校验失败,记录部分发布状态,不重发同版本不同字节。
- [ ] 提交/发布点:main 的发布提交与 `v1.3.4` 对齐;Release 与 registry 外部验证全部成立。
## 5. 阻塞矩阵:哪些可以执行,哪些不能冒充通过
| 缺口 | 本库可完成 | 不可自行宣称/处置 |
| --- | --- | --- |
| 下游工作区缺失 | 本库完整合成 env、runtime Protocol、M1M9 与显式缓存迁移回归 | GovDoc/CHS 实际配置未取证、Video-Tree 已退出迁移但历史兼容面仍可测;三者均不能虚构实测。提前向父会话登记缺口,发布前须拿到相关负责人脱敏配置与验证证据,或人类明确豁免缺失项;不阻止独立离线实现继续 |
| M2 空 wire AUTO | 默认 wire 单测、实际既有 T10 型号档位复验 | 真实缺身份/无信号/不可达不能当已验证;需有效重测或人类具名豁免,不补回 medium 或无证据改表 |
| M3 非流式 UNKNOWN | 可验证 payload、响应形态、UNKNOWN 不假绿 | 不把长度差当开启/关闭证明;必需能力单元无法满足时保留未覆盖并走人类决策,不新增临时“通过”阈值 |
| 429/5xx/网络失败 | 完整证据保存,库回归用离线契约定位 | 本批归因默认 FAIL 是设计批准范围,不能为了 #25 关闭率改宽 skip;外部证据由人类决定发布豁免 |
| 研究新增预算 | 原有 slow 套件按已有授权跑,已有配置保持 | 额外模型/轮次/对照实验需预算批准;不得将批准设计偷换成无限研究调用授权 |
| 设计外漏洞 | 独立记录实际文件与反例,父会话核定是否阻塞 | 不顺手实施 #19#22#23#24、新 schema 或新 deadline;无强制单源分支 |
## 6. 自审与验收映射
| 设计节/需求 | 任务 |
| --- | --- |
| §4.14.2 AUTO 与 MiniMax、未知尽力 | T1T3 双入口,T6/T8 真实证据 |
| §4.3/4.4 raw 同值/嵌套/自定义/时机 | T2、T3;无公共端口新增、无深合并 |
| §5 D3 与 M1–M9 | T4、T8;未迁移风险显式保留,绝不补指纹 |
| §6 归因/身份/UNKNOWN/负向命题 | T5、T6;完整请求证据、默认 FAIL、逐轮持久化 |
| §7 无推理路径与变异 | T7;三入口真 clientSQLite,四类隔离变异 |
| §8 四种行口径与日志 | T1T4T7T8;既有 schemaemitter,不新增数据面 |
| §9/10 文档、下游与发布门 | T8/T9及阻塞矩阵;外部结果与测试缺口不冒充通过 |
自审已核对:生产守卫所有消费者在 §3 定义;新增测试文件有确定路径;conftest 当前不存在故明确新建;默认工厂不支持 transport 注入故使用已批准全量注入而非偷扩 API;缓存不改指纹;无从公共 model_reported 倒推上游身份;所有命令均在 conda 环境;未执行的测试不写为已通过。
计划审查由父会话组织,完成后直接实施,不新增人类计划审批门。执行中本文件任务勾选与 finding 保持实际状态一致;本次计划编写未运行 pytest、变异或真实模型调用。
## 本轮实施证据
T0–T4/T7 的命令、实际失败与修复、11 个隔离变异及 1241 项单测通过,见 `findings/2026-09-09-134-thinking-contracts-validation.md`。T5/T6 已续作:1357单元通过、8个隔离假绿变异exit1/还原0、e2e 90节点仅采集;T8文档同步完成。未执行live、集成、独立verifier和发布。具体节点及残余见同一finding续作节。
### T5/T6续作决策记录
父会话确认无已批准型号→400机器type白名单:不编造,缺机器证据400默认FAIL;精确预期拒绝契约离线守卫,具体live负向缺基线记录未验证。不可关闭命题完整合格轮次有OBSERVED支持本条件下未关闭,全ABSENT证伪,无OBSERVED但UNKNOWN未覆盖。T8复选框保持未勾选,因为独立verifier与全量/live证据门未执行;本轮仅其文档同步部分完成,禁止发布。
T5/T6实现提交:`73008ad`。最终日常单元1357、受影响含factory331、make check、compileall、e2e collect-only90通过;完整T8/T9仍未执行。日志路径及8项红→还原绿详见同一finding。