docs: fix plan per independent review (B1 emitter fake sync, B2 guard return contract, N1-N5)

This commit is contained in:
2026-09-14 00:10:37 -04:00
parent 929d60e501
commit 6f3a620c73
2 changed files with 48 additions and 9 deletions
@@ -44,7 +44,7 @@ Skill 纪律:T1–T4 行为变更执行 `test-driven-development`(先红后绿
| 修改 | `src/polygateway/ports.py` | `Transport.complete`(:56-66)加 `content_event` 必填 kw + docstring;`TelemetryRecorder.record_llm_call`(:306-344)加 `stall_ms`/`stall_count` 两参 + docstring("36 字段"→"38 字段");`TransportResult` 在 types.py(见下) |
| 修改 | `src/polygateway/types.py` | `TransportResult`(:560-586)增 `stall_ms: float \| None = None`/`stall_count: float \| None = None`(追加在 `applied_effort` 后);`CallStats`(:296-324)在 `hedge_won` 后增 `stall_ms: int \| None = None`/`stall_count: int \| None = None`;`_CallContext`(:328-388)`__slots__`/`__init__` 增两私有位、`record_generation` 增 keyword-only `stall_ms`/`stall_count`(默认 None)、`snapshot` 填两字段;`SourceConfig`(:590-700)在 `effort_fallback` 后追加 `stall_threshold_ms: int = 1000` + `__post_init__` 值域校验(正整数、拒 bool) |
| 修改 | `src/polygateway/transports/openai_compat.py` | `complete`(:427-473)加 `content_event` 透传两路径;`_complete_stream`(:553-616)chunk 循环(:580-590 邻域)记增量 chunk 时间戳 + 每 chunk `set` content_event(None 跳过),装配处(:597-616 邻域)由序列算 stall 两值;模块级纯函数 `_stall_from_chunk_times`;`_complete_once`(:660-705)接收 content_event 但**永不置位**(docstring 明写),stall 两值恒 None(默认) |
| 修改 | `src/polygateway/middleware/retry.py` | `__call__` 入口门(:273-283)改两阈值判定;`_attempt`(:304-400)加 `content_event` 私有 kw、sink 元素改三元组、成功分支取整换算 + `_emit` 传 stall;`_attempt_hedged`(:371-530)Phase 1/4 接线 content_event、Phase 5 汇合复用;`_past_hedge_window`(:533-562)两阶段重构;`_record_generation`/`_register_hedge` 邻域(净口径计算收敛一处);`__init__` 加 `stall_after_s` kw |
| 修改 | `src/polygateway/middleware/retry.py` | `__call__` 入口门(:273-283)改两阈值判定;`_attempt`(:304-400)加 `content_event` 私有 kw、sink 元素改三元组、成功分支取整换算 + `_emit` 传 stall;`_attempt_hedged`(:415-531)Phase 1/4 接线 content_event、Phase 5 汇合复用;`_past_hedge_window`(:533-562)两阶段重构;`_record_generation`/`_register_hedge` 邻域(净口径计算收敛一处);`__init__` 加 `stall_after_s` kw |
| 修改 | `src/polygateway/config.py` | `_RESERVED_SEGMENTS`(:57)加 `"STREAM"`;`GatewaySettings`(:192-196 后)加 `stall_threshold_ms: int = 1000`、`hedge_stall_after_s: float \| None = None`;`_validate_hedge`(:382-392)扩传新参;`from_env`(:398-427)`**_load_stream(...)`;新增 `_load_stream`(:745 `_load_hedge` 邻域);`check_hedge_assembly`(:772-849)加 `stall_after_s` 参与三守卫;`EmbeddingSettings.__post_init__`(:865-869 邻域)与 `OcrSettings`(新增 `__post_init__`)加 B4 对冲键 warning |
| 修改 | `src/polygateway/client.py` | `__init__`(:240 邻域)加 `hedge_stall_after_s`/`stall_threshold_ms` keyword-only 参数,守卫(:252-257)扩传;`stall_threshold_ms` 经 `dataclasses.replace` 分发进各 SourceConfig;RetryMW 构造(:264-283)传 `stall_after_s`;`from_settings`(:485-526)透传 |
| 修改 | `src/polygateway/middleware/telemetry.py` | `_AttemptUsage`(:199-236)增两字段;`emit_attempt`(:396-447)加 `stall_ms`/`stall_count` kw(默认 None)+ 转发;`emit_cache_hit`/`emit_terminal_failure`(:449-540)显式传 None;`_record`(:560-665)加两参并在 :618 调用点透传 |
@@ -106,7 +106,8 @@ Skill 纪律:T1–T4 行为变更执行 `test-driven-development`(先红后绿
| 5 | `tests/unit/test_ports.py:102`(`_DummyRecorder`,**显式全签名**) | +2 参(它镜像协议全签名,不加则与本协议脱节) |
| 6 | `tests/unit/test_telemetry.py:3327/:3349/:3354`(装配闸**负例** fake,故意残缺签名) | **不改**——协议加参后它们仍不满足签名,负例断言照常通过;T3 绿态核对这三条仍绿 |
| 7 | `**fields` fake(13 处):`test_ocr_client.py:149`、`test_pricing.py:131`、`test_openai_compat.py:106`、`test_embedding.py:246`、`test_cache.py:562`、`test_client.py:528/955/968/1516/1842`、`test_usage_source_domain.py:71`、`test_telemetry.py:1191/2059` | **零改动**自动兼容;批次 C 断言落行时用其中一份捕获 fields |
| 8 | 发射器 fake:`tests/unit/test_hedge.py:122`、`tests/unit/test_backpressure.py:233`(`emit_attempt`) | **零改动**(`emit_attempt` 新参默认 None,见 §3.4 取舍) |
| 8a | 发射器 fake:`tests/unit/test_backpressure.py:233`(`*args, **kwargs` 形态) | **零改动**(`emit_attempt` 新参默认 None,见 §3.4 取舍) |
| 8b | 发射器 fake:`tests/unit/test_hedge.py:122`(RecordingEmitter,显式全签名 8 kw,无 `**kwargs`) | **列入 T3 同步清单**(审查 B1):加 `stall_ms=None, stall_count=None` 两 kw——T3 后 `_emit` 显式转发两 kw,不加则 TypeError;"新参默认 None"只护调用方、护不住被调的显式签名 fake |
## 3. 跨任务接口(可执行定义,禁止占位)
@@ -227,8 +228,8 @@ generation_sink.append((wall_ms, stall_ms_i, stall_count_i))
| --- | --- |
| loader | `_load_hedge`(:745-769)扩读 `STALL_AFTER_S`:`_first` + `_cast` + `ensure_call_deadline`(origin 传实际命中键名),返回字典加 `"hedge_stall_after_s"` 键;未设 = None(关闭) |
| 字段 | `GatewaySettings` 加 `hedge_stall_after_s: float \| None = None`;`_validate_hedge`(:382-392)扩传 |
| 守卫(全部落进 `check_hedge_assembly` :772-849,签名加 `stall_after_s` 参,`None` 时值域归一化后交叉守卫不查——同 `after is None` 既有早退形态) | ①值域 = `ensure_call_deadline` 同款(有限正数);②`stall_after_s ≥ min over sources of (inter_token_timeout_s or timeout_s)`(有效最小值,与 openai_compat.py:573 回落同款)→ **ValueError**(对冲窗永不开,装配期直接拒绝;stall 场景无非流式残留收益,warning 论证不足,N5);③`stall_after_s ≥ call_deadline_s`(两者皆设)→ ValueError(与 `after_s` 同款,config.py:825-828 邻域);④单源 scope 设阈值 → warning 允许(1.3.7 §5 同款);⑤**B4**:Embedding/OCR scope 装配 warning 落 `EmbeddingSettings.__post_init__`(:865-869 邻域)与 `OcrSettings`(新增 `__post_init__`):内嵌 gateway 的 `hedge_after_s` **或** `hedge_stall_after_s` 非 None → warning"对冲键仅对 chat scope 生效,本 scope 非流式静默退化"(1.3.7 `AFTER_S` 同病顺带补齐,缺陷修复不扩公共面);chat 侧装配 warning 明确不做(per-call stream,装配期无信号,B4 裁决) |
| client | `GatewayClient.__init__` 加 `hedge_stall_after_s: float \| None = None` keyword-only,:252-257 守卫调用扩传;RetryMW 构造(:264-283)传 `stall_after_s=` 归一化值;`from_settings`(:525 邻域)透传 |
| 守卫(全部落进 `check_hedge_assembly` :772-849,签名加 keyword-only `stall_after_s` 参;**返回契约钉死(审查 B2)**:返回 `tuple[float | None, float | None]` = (归一化 after_s, 归一化 stall_after_s),`config.py:384` 与 `client.py:252` 两处调用点随之解构,`test_config.py:1174/:1184` 列入 T4 同步清单(`is None` 断言按新返回形态改写);**两阈值皆 None 才早退**——"同 `after is None` 早退形态"字面执行会让仅 stall 配置绕过守卫②③④,禁止) | ①值域 = `ensure_call_deadline` 同款(有限正数);②`stall_after_s ≥ min over sources of (inter_token_timeout_s or timeout_s)`(有效最小值,与 openai_compat.py:573 回落同款)→ **ValueError**(对冲窗永不开,装配期直接拒绝;stall 场景无非流式残留收益,warning 论证不足,N5);③`stall_after_s ≥ call_deadline_s`(两者皆设)→ ValueError(与 `after_s` 同款,config.py:825-828 邻域);④单源 scope 设阈值 → warning 允许(1.3.7 §5 同款);⑤**B4**:Embedding/OCR scope 装配 warning 落 `EmbeddingSettings.__post_init__`(:865-869 邻域)与 `OcrSettings`(新增 `__post_init__`):内嵌 gateway 的 `hedge_after_s` **或** `hedge_stall_after_s` 非 None → warning"对冲键仅对 chat scope 生效,本 scope 非流式静默退化"(1.3.7 `AFTER_S` 同病顺带补齐,缺陷修复不扩公共面);chat 侧装配 warning 明确不做(per-call stream,装配期无信号,B4 裁决) |
| client | `GatewayClient.__init__` 加 `hedge_stall_after_s: float \| None = None` keyword-only,:252-257 守卫调用扩传;RetryMW 构造(:264-283)传 `stall_after_s=` 归一化值(取守卫返回二元组第二元,B2);`from_settings`(:525 邻域)透传 |
### 3.6 T4:retry 编排改动(P2 唯一实现形态)
@@ -340,12 +341,13 @@ async def _past_hedge_window(self, primary, first_token, content_event) -> bool:
### T3 → 提交 3 `feat: add stall_ms and stall_count telemetry columns`
- **目标**:遥测两列全链路(schema 五编辑点 + 协议两参 + emitter 透传 + 两 recorder 自动拾取);§2.3 同步清单落地。
- **改动**:`telemetry/schema.py` 五编辑点(§3.4 表)+ 模块头计数 docstring;`ports.py:306-344`;`middleware/telemetry.py:199-236/396-447/449-540/560-665`;`retry.py:_emit`(§3.4);`sqlite.py:145`/`postgres.py:411` docstring 计数;`test_ports.py:102` +2 参;`test_telemetry.py` 冻结清单扩展;`tests/integration/test_postgres_telemetry.py:33-77` 冻结清单扩展。
- **改动**:`telemetry/schema.py` 五编辑点(§3.4 表)+ 模块头计数 docstring;`ports.py:306-344`;`middleware/telemetry.py:199-236/396-447/449-540/560-665` + `:368` docstring 计数("组装 36 字段"→38,审查 N4);`retry.py:_emit`(§3.4);`sqlite.py:145`/`postgres.py:411` docstring 计数;`test_ports.py:102` +2 参;`test_telemetry.py` 冻结清单扩展;`tests/integration/test_postgres_telemetry.py:33-77` 冻结清单扩展。
- **先失败测试清单**(批次 C;先红 = 冻结清单已扩展而 schema 未改 → 列数/列序断言失败;协议未加参 → 装配闸 ValueError):
| 测试名 | 断言点 |
| --- | --- |
| `test_telemetry.py::TestSchemaFreeze::test_columns_and_ddl_are_frozen`(扩) | `_EXPECTED_COLUMNS` 末尾追加两列后:`len(COLUMNS) == 38`;两端 DDL 首现顺序含新列;`PG_BACKFILL[-1]` 变 stall_count 项;占位符 `$38)`/`?`×38 |
| `test_ports.py:313` 与 `test_telemetry.py:748` 硬编码计数(扩,审查 N1) | `len(params)-1 == 36` → 38;`len(physical) == 37` → 39;连带 :650/:743/:756 注释文本同步——两处列入批次 C 清单,防被实施者"发现"而非计划登记 |
| `test_telemetry.py::TestBackendColumnParity::test_new_columns_are_appended_last`(扩) | `COLUMNS[-2:] == ("stall_ms", "stall_count")` |
| `test_telemetry.py::<SQLite 落行新用例>` | emit attempt(stall_ms=3200, stall_count=2)→ SQLite 行两列恰为该值;success 非流式行两列 NULL |
| `test_telemetry.py::<迁移幂等新用例>` | 既有 1.3.5 形态表(37 物理列)开 auto_migrate recorder → ALTER 补两列不炸;**重复初始化第二次**幂等;manual 档缺列告警逐字含两条新 ALTER |
@@ -359,8 +361,8 @@ async def _past_hedge_window(self, primary, first_token, content_event) -> bool:
### T4 → 提交 4 `feat: add opt-in mid-stream stall hedging for chat`
- **目标**:`HEDGE__STALL_AFTER_S` 独立生效的中段对冲 + `STREAM__STALL_THRESHOLD_MS` 装配三层可调 + 全部守卫(B4 含 1.3.7 AFTER_S 同病补齐)。
- **改动**:`config.py:57/192-196/382-392/398-427/745-769/772-849/865-869/OcrSettings 新 __post_init__`(§3.5);`client.py:240/252-257/264-283/485-526`(§3.5);`retry.py:__init__/:273-283/:371-530/:533-562`(§3.6)。
- **先失败测试清单**(批次 D;先红 = 键未识 ValueError/None 断言失败、对冲未触发 `hedges==0`、挂起用例超时):
- **改动**:`config.py:57/192-196/382-392/398-427/745-769/772-849/865-869/OcrSettings 新 __post_init__`(§3.5);`client.py:240/252-257/264-283/485-526`(§3.5);`retry.py:__init__/:273-283/:415-531/:533-562`(§3.6)。
- **先失败测试清单**(批次 D;先红 = 键未识时 3 段键被 `config.py:431` 静默跳过、实际红因 AttributeError/TypeError(审查 N3 措辞修正,红仍成立)、None 断言失败、对冲未触发 `hedges==0`、挂起用例超时):
| 测试名(新建 `tests/unit/test_stall_hedge.py`,设施复用 test_hedge.py 范式:两源、event-driven 假 transport、真实 loop 钟、阈值 0.05s、容差 4–10×) | 断言点 |
| --- | --- |
@@ -378,7 +380,7 @@ async def _past_hedge_window(self, primary, first_token, content_event) -> bool:
| `test_config.py::test_stall_after_s_guard_deadline_conflict_raises` | `≥ call_deadline_s` → ValueError |
| `test_config.py::test_stall_after_s_single_source_warns` | 单源 warning 允许(loguru 捕获 fixture,test_config.py:31-33 既有) |
| `test_config.py::test_embedding_ocr_scope_hedge_keys_warn` | Embedding/OCR settings 内嵌 gateway 的 AFTER_S **或** STALL_AFTER_S 非 None → warning(B4,含 1.3.7 同病补齐);chat 侧无此 warning(B4 裁决) |
| 批次 E 默认关闭回归门 | 两键均不设 → `pytest tests/unit tests/contracts -q` 全绿且**不改一行既有断言**(1.3.6/1.3.7 取消/对冲/看门狗语义回归) |
| 批次 E 默认关闭回归门 | 两键均不设 → `pytest tests/unit tests/contracts -q` 全绿且**不改一行既有语义断言**(例外登记——审查 B1/B2/N1:fake 签名同步、`test_config.py:1174/:1184` 返回形态断言、两处硬编码计数,均属签名/清单同步,非语义断言改动) |
- **证据出口**:`tests/outputs/138/t4/red-batch-d.log` + `green-unit.log` + `green-contracts.log` + `lint.log`。
- **验收命令**:`pytest tests/unit/test_stall_hedge.py tests/unit/test_config.py -q` → `pytest tests/unit tests/contracts -q` → `make lint`。
@@ -408,7 +410,7 @@ async def _past_hedge_window(self, primary, first_token, content_event) -> bool:
| D | 中段对冲编排(8 用例)+ 配置守卫(6 用例) | T4 | test_stall_hedge.py(新)/ test_config.py |
| E | 默认关闭回归门(不改一行既有断言) | T4 门 | tests/unit + tests/contracts 全套件 |
**设施复用核对(动手前必读)**:`test_retry.py:120-127` FakeSleep 只记录不推进时钟(推进须用例自带闭包);`test_hedge.py:41-69` 事件驱动假 transport(content_event 身份逐次记录,同款扩展);`test_config.py:31-33` loguru WARNING 捕获 fixture(caplog 抓不到 loguru);取消窗口用 `entered` Event 范式(`test_retry.py:79-89`);真实 loop 钟阈值 0.05s、容差 4–10×(`test_streaming.py` 既有范式,不标 slow);`test_telemetry.py:53-99` 冻结清单的扩展属"先红"手段(先改清单 → schema 未改即红)。
**设施复用核对(动手前必读)**:`test_retry.py:120-127` FakeSleep 只记录不推进时钟(推进须用例自带闭包);`test_hedge.py:41-69` 事件驱动假 transport(content_event 身份逐次记录,同款扩展);`test_config.py:31-33` loguru WARNING 捕获 fixture(caplog 抓不到 loguru);取消窗口用 `entered` Event 范式(`test_retry.py:79-89`);真实 loop 钟阈值 0.05s、容差 4–10×(`test_streaming.py` 既有范式,不标 slow);**命名区隔**(审查 N5):`test_hedge.py:150` 既有 `stall_window_s`(StallClock 预算)与新 `stall_after_s`(对冲触发阈值)是不同概念,`test_stall_hedge.py` 复用设施时注释明写;`test_telemetry.py:53-99` 冻结清单的扩展属"先红"手段(先改清单 → schema 未改即红)。
## 6. 风险节与验收门