diff --git a/CHANGELOG.md b/CHANGELOG.md index ec9a9c7..66698c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ 直接构造 `GatewaySettings` 或对它做 `dataclasses.replace` 时,若上述组合非法,**现在会在构造期抛 `ValueError`**,而不是留到运行时。经 `from_env()` 装配的调用方**不受影响**——那条路本就跑这些守卫。手工拼配置(如从 YAML 读出后构造)的调用方若此前撞上过上述任一故障,升级后会在启动时立即得到点名字段的报错。 -守卫的报错文案改为点字段名(`lease_ttl_s`、`backpressure.stall_window_s`、`breaker.probe_ttl_s`)。原文案只点环境变量键,而不走 env 的调用方从没设过那些键。键名映射见 `.env.example` 与 wiki `参考-配置键`。 +守卫报错文案的**补救建议**改为点字段名(`lease_ttl_s`、`backpressure.stall_window_s`、`breaker.probe_ttl_s`)。原文案已点出字段名,但建议部分给的是环境变量键(如"调大 `PGW_LEASE_TTL_S`"),而不走 env 的调用方从没设过那些键。键名映射见 `.env.example` 与 wiki `参考-配置键`。 ## 1.0.0(2026-07-22) diff --git a/research-wiki/designs/2026-07-29-settings-invariant-guards-design.md b/research-wiki/designs/2026-07-29-settings-invariant-guards-design.md index 0b4de43..9fe5ca8 100644 --- a/research-wiki/designs/2026-07-29-settings-invariant-guards-design.md +++ b/research-wiki/designs/2026-07-29-settings-invariant-guards-design.md @@ -1,6 +1,6 @@ # GatewaySettings 跨字段不变量守卫的生效范围 -- **日期**: 2026-07-29;**状态**: 待人类审批(改变库对下游的构造承诺,强制人类门) +- **日期**: 2026-07-29;**状态**: **已批准并实施**(2026-07-29 人类门通过;§8 结论见文末) - **范围拍板**(用户 2026-07-29): 功能对齐社区 PR#1,但按本库规范重写;顺带销掉 PR#1 遗留的两个缺陷 - **上游依据**: ARCHITECTURE §7.3 契约补强 G6(装配期守卫,"违反直接报错拒绝装配")、§9 配置聚合、CLAUDE.md §4.5(装配只有两条路)、`types.py` 同族 frozen dataclass 的既有校验笔迹 @@ -123,9 +123,29 @@ TDD 顺序:先写测试跑出预期失败(预计 3 条守卫 + 空源 + from_set 合并后应关闭 PR#1 并在其中说明:诊断被采纳,实现按库内规范重写并扩展了覆盖范围。 -## 8. 待人类拍板 +## 8. 人类拍板结论(2026-07-29) -1. **主决策**:接受方案 A(构造期强制,承诺收紧)? -2. **范围**:`probe_ttl_s` 与空 sources 一并纳入(推荐),还是严格只对齐 PR#1 的两条? -3. **消息文案**:去掉 env 键名(推荐,§2.3),还是保留 PR#1 的附注形式? -4. **PR#1 处置**:重写合并后关闭并说明,还是先请提交者按 review 修改? +| 问题 | 结论 | +|---|---| +| 主决策 | **接受方案 A**,构造期强制,承诺收紧 | +| 范围 | **全量**:`probe_ttl_s` 与空 sources 一并纳入 | +| 消息文案 | **去掉 env 键名**,只点字段名(§2.3) | +| 版本 | **1.0.1**(patch);初稿建议的 minor 被否,理由与代偿见 §4 | +| PR#1 处置 | 重写合并后关闭并说明,诊断归功于提交者 | + +## 9. 实施与验证留痕 + +实施于 `fix/settings-invariant-guards`(5 commits)。TDD 证据:新测试类先 **6 failed / 3 passed**(3 条为边界护栏,本就应过),实现后全绿。 + +独立 verifier(全新上下文)核验结论 **可以合并,无阻塞**,其中两项证据值得留档: + +- **变异测试 12/12 全杀**:逐个破坏实现(删各 `_validate_*` 调用、`>`↔`>=`、`<`↔`<=`、删空源检查、`_PROBE_GRACE_S` 归零)均有测试失败,无一存活。边界侧用例(恰好相等必过)对每条守卫都真实有效,差一错误可捕获。 +- **无热路径回归**:库内**没有任何地方**构造或 `replace` `GatewaySettings`(`src/` 中 4 处 `dataclasses.replace` 全在 `middleware/structured.py`,作用于 `ChatRequest`/`LLMResponse`)。单次构造实测 1.45 µs,装配期一次性成本。`pickle`/`deepcopy` 不触发 `__post_init__`,只有 `replace` 触发——序列化往返既无额外开销也不构成二次守卫点。 + +### 9.1 verifier 发现的同族遗漏(范围外,另起任务) + +`GatewaySettings` 仍有 **14 条校验只挂在 `from_env`**,直接构造/`replace` 全部放行,与本设计所修的是同一个 bug 类:`limiter/breaker/cache_backend=redis` 但 `redis_url=None`、`telemetry_backend=sqlite/postgres` 但 path/dsn 为 None、`selector`/`quota_full`/各 backend 的枚举合法性、`structured_max_retries` 负值、`scope` 空串等。 + +严重性高于本次所修的三条,因为 `client.py:262/282/302/312/316` 有 5 处 `assert ... # 内部不变量: config 已校验` **明文依赖这个前提**,而该前提在 `from_settings` 路上为假:断言开启时抛裸 `AssertionError`(不点字段不说原因),`python -O` 下断言消失、错误退化为 redis 库抛出的天书。后者同时违反 CLAUDE.md §4.3"禁止 assert 承担生产校验"。 + +**有意不纳入本次交付**(避免任务外扩张),另起任务处理。