docs: document logical call telemetry and migration impact
Field counts come from inspect, not memory: record_llm_call takes 36 parameters, COLUMNS has 36 entries, the physical table has 37. - README: capability table says 36 fields and names the three row kinds; new section covers reading call_stats, the five SQL migration items, the attribution query and the storage-side upgrade - README/.env.example/ARCHITECTURE: error_body follows the summarize_body limit and the structured-exhaustion error carries its own bounded explanation, so neither is inside PGW_TELEMETRY_TEXT_CAP coverage - ARCHITECTURE 7.8: the ten columns with per-column semantics, the I3/I4 invariants, operation versus exc.operation, and the assembly gate - CHANGELOG: unreleased section listing the four public changes and what downstream must do, in particular counting failures by event_kind and the assembly-time error for custom recorders - schemas/llm-calls: the ten columns plus a three-row-kind section - metrics/call-telemetry-coverage: 1.3.5 coverage contract, real live baselines left unfilled rather than stating a fake percentage Validation record records the T4 evidence: mechanical migration red then green, the four PG acceptance cases, the seven-item mutation matrix with all seven killed and the copy restored to an identical digest, plus the PYTHONPATH pitfall that made the first mutation round silently test the original source. Version numbers and release steps are deliberately untouched.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
---
|
||||
type: finding
|
||||
node_id: finding:2026-09-09-135-call-observability-validation
|
||||
title: "1.3.5 T2/T3 验收证据:36 列遥测、失败终态与取消口径"
|
||||
title: "1.3.5 T2/T3/T4 验收证据:36 列遥测、失败终态、PG 存储兼容与变异矩阵"
|
||||
date: 2026-09-09
|
||||
---
|
||||
|
||||
@@ -118,17 +118,94 @@ TDD 纪律要求"先失败后通过",且红必须是**行为红**而非 import
|
||||
唯一顺带修正:`RetryMW.__init__` 的 `emitter` 注解由 `object | None` 收紧为
|
||||
`TelemetryEmitter | None`(TYPE_CHECKING 导入,同层不破分层契约),因本轮改了它的 `_emit`。
|
||||
|
||||
## 5. 明确缺口(交 T4 另任务,本轮不做)
|
||||
## 5. T4 验收(PG 存储兼容、变异矩阵、文档同步)
|
||||
|
||||
基线 HEAD `393f2bf`(1467 unit 全绿),全部命令在 `PolyGateway` conda 环境执行。
|
||||
|
||||
### 5.1 PG 存储兼容(真实实验室 Postgres,复用 `pg_sandbox`)
|
||||
|
||||
机械迁移:`_EXPECTED_COLUMNS` 27 → 37 列、`_record_minimal` 字段字典补 10 键(与单测同款)、
|
||||
`_PRE_TENANT_COLUMNS` 的缺列集由4 列扩到 14 列。新十列一律由 `_CALL_OBSERVABILITY_COLUMNS` 派生,
|
||||
两处 manual 档告警的逐字断言改成按 `COLUMNS` 序派生的 `_PRE_TENANT_MISSING_NOTICE`——
|
||||
另抄一份列名必然漂移,而漂移的表现是“manual 档没补列”这条断言假绿。
|
||||
|
||||
| 阶段 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 机械迁移前(已知必红) | `pytest tests/integration/test_postgres_telemetry.py -q -x` | **1 failed / 18 passed**:manual 档列序断言 `Right contains 10 more items, first extra item: 'scope'` |
|
||||
| 机械迁移后 | 同上(无 `-x`) | **27 passed**,EXIT=0,`tests/outputs/135/pg-telemetry-after-mechanical.log` |
|
||||
| 新增四项验收后 | 同上 | **30 passed**,EXIT=0,`tests/outputs/135/pg-telemetry-new-cases.log` |
|
||||
|
||||
新增 `TestCallObservabilityColumnsAcceptance`(对应计划 T4 的 PG 四项),共用一张 27 列的 1.3.4 形态旧表(`pre_135_schema`):
|
||||
|
||||
| 验收项 | 用例与关键断言 |
|
||||
| --- | --- |
|
||||
| auto 追加 10 列 + 旧行 NULL | `test_pre_135_table_gains_the_ten_columns_and_old_rows_stay_null`:物理列 27 → 37 且列序 == `_EXPECTED_COLUMNS`;attempt 行与 terminal 行十列取值**整体比对**(不是逐条 in);历史行 `dict.fromkeys(...)` 十列全 NULL |
|
||||
| manual 缺列裁剪 | `test_manual_trims_the_insert_on_a_pre_135_table`:表结构逐字不动(== `_PRE_135_COLUMNS`);裁剪后 26 列逐列等于提交值(防整体错位);恰 1 条告警且含可直接粘贴的首/末列 ALTER;无“写入失败”/“补列失败”,`degraded is False` |
|
||||
| 新旧进程混写 | `test_old_and_new_writers_share_one_table`:新版建表写 36 列 → 旧版进程用 `insert_sql("postgres", 26 列集)` 写入 → 新版再写;三行共存、表结构不变、旧行新列全 NULL、无写入失败 warning |
|
||||
| 下游口径验收 | 同上用例尾部:`WHERE event_kind = 'terminal_failure'` 计得 1;`event_kind IS NULL` 计得 1(混写期旧行既不误计成失败也不误计成成功) |
|
||||
|
||||
纪律:未引用 `assert_no_leftovers`(它是 `test_pg_sandbox.py` 的模块级 fixture,对本文件不可见,上提它要改 `conftest.py`);
|
||||
未新建沙箱设施、未碰共享表 `llm_calls`、未读或打印 DSN。新增的只有一个 `_execute_args`(带参数单语句)与
|
||||
`_minimal_fields`(从 `_record_minimal` 拆出的字段字典,供“旧进程”复用同一份取值)。
|
||||
|
||||
### 5.2 变异矩阵(仓库外副本,主工作区生产代码零改动)
|
||||
|
||||
驱动脚本 `/tmp/pgw-135-mut/run_mutations.py`;每项“还原副本 → 施加单点变异(断言替换命中)→ 跑指定节点 → 还原”。
|
||||
完整日志 `tests/outputs/135/mutations.log`,脚本总退出码 **EXIT=0**。
|
||||
|
||||
| # | 变异 | exit | 被杀断言(节选) |
|
||||
| --- | --- | --- | --- |
|
||||
| M1 | `register_attempt()` 挪到 transport 成功之后 | 1 | `test_failed_retries_are_counted`、`test_budget_free_429_still_counts_as_an_attempt`、`test_retry_exhausted_counts_every_attempt` |
|
||||
| M2 | `ChatRequest` 每次 `replace` 复制出新上下文 | 1 | `TestLogicalCallStats` 4 项 + `test_retry_exhaustion_writes_exactly_one_terminal_row` |
|
||||
| M3 | `_rehydrate` 去掉 `call_stats=None` 覆盖 | 1 | `test_historic_dict_never_impersonates_call_stats` |
|
||||
| M4 | Emitter 入口提前 `str(exc)` 压平 | 1 | 529/503 保真、4 个空超时文案 `cause_type`、类名前缀策略、结构化有界说明等 9 项 |
|
||||
| M5 | 终态行复制最后一次 attempt 的 token/cost | 1 | `test_terminal_rows_never_contribute_to_cost`、`test_terminal_row_costs_nothing` |
|
||||
| M6 | 去掉 `claim_terminal` 去重 | 1 | `test_terminal_row_is_written_once_per_logical_call`、`test_claim_terminal_is_true_once` |
|
||||
| M7 | 装配闸改为捕获 `TypeError` 后 warning | 1 | `test_old_signature_recorder_is_refused_at_assembly`、`test_uninspectable_recorder_is_a_configuration_error` |
|
||||
|
||||
还原校验:副本 `.py` 文件集合散列与纯净态**逐字一致**(`66b58b9a…`),还原后同一批节点 **172 passed**。
|
||||
|
||||
> **方法论陷阱(影响本仓所有变异证据的有效性)**:只设 `PYTHONPATH=<副本>/src` 是**无效的**——
|
||||
> `pyproject.toml` 的 `[tool.pytest.ini_options] pythonpath = ["src"]` 会把**仓库内**的 `src` 抢先塞进 `sys.path[0]`,
|
||||
> 于是测的仍是原代码。首轮实跑七项变异**全部“存活”(exit=0)**就是这个坑;
|
||||
> 改用 `-o pythonpath=<副本>` 覆盖后七项全部被杀。今后做变异必须先证“副本真的被导入”,
|
||||
> 否则“变异存活”会被误读成“测试不够强”,而真相是变异根本没生效。
|
||||
|
||||
### 5.3 文档同步(字段数一律 `inspect` 实测,不凭记忆)
|
||||
|
||||
实测值:`len(inspect.signature(TelemetryRecorder.record_llm_call).parameters) - 1 == 36`,`len(COLUMNS) == 36`,物理列 37。
|
||||
|
||||
| 位置 | 改了什么 |
|
||||
| --- | --- |
|
||||
| `README.md` 能力表 | “必录 26 字段” → 36 字段 + 三类行与诊断列;新增“逻辑调用统计”一行 |
|
||||
| `README.md` 新小节 | 《1.3.5 逻辑调用统计与失败诊断》:`call_stats` 读法、**SQL 迁移五项**、归因查询、存储侧升级 |
|
||||
| `README.md` cap 覆盖面 | 补“`error_body` 沿用 `summarize_body` 上限、结构化说明自带限长,**两者都不在 cap 覆盖内**” |
|
||||
| `ARCHITECTURE.md` §7.8 | 必录字段 26 → 36(十列逐个列出);新增“逻辑调用十列”段(列语义表 + 不变量 I3/I4 + `operation` vs `exc.operation` 两个语义 + 装配闸);cap 段补两处诊断文本不在覆盖面 |
|
||||
| `CHANGELOG.md` | 新建《未发布》:公共面四项变更表 + 下游必须做的事(点名 `WHERE event_kind` 与装配期报错) |
|
||||
| `.env.example` | `PGW_TELEMETRY_TEXT_CAP` 块补 1.3.5 覆盖面例外 |
|
||||
| `schemas/llm-calls.md` | 标题 26 → 36 字段;十列逐行登记;新增《三类行与失败归因口径》(含归因 SQL 与迁移四条) |
|
||||
| `metrics/call-telemetry-coverage.md` | 新增《1.3.5 三类行与逻辑调用覆盖》;live 基线列**不写伪百分比** |
|
||||
|
||||
版本号与发布步骤**未动**(任务边界);`pyproject.toml` / `__init__.py` 仍为 1.3.4。
|
||||
|
||||
### 5.4 本轮收口验证
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 静态 | `make check`(ruff format+lint + import-linter) | 见 §7 实跑记录 |
|
||||
| 全量单测 | `pytest tests/unit -q` | 见 §7 |
|
||||
| 目标集成 | `pytest tests/integration/test_postgres_telemetry.py -q` | **30 passed** |
|
||||
|
||||
## 6. 剩余缺口(不归本任务,不自称已关)
|
||||
|
||||
| 缺口 | 说明 |
|
||||
| --- | --- |
|
||||
| PG 存储兼容 | `tests/integration/test_postgres_telemetry.py` 的 `_EXPECTED_COLUMNS` 与字段字典**尚未补 10 列**;auto 追加 / manual 缺列裁剪 / 旧行 NULL / 新旧进程混写四项未跑(需真实 PG 沙箱) |
|
||||
| 变异证据 | 计划 §4 的七项变异(计数位置、上下文复制、缓存回放、提前压平、终态双计费用、去 `claim_terminal`、闸改 warning)未执行 |
|
||||
| 文档同步 | README「必录 26 字段」、`PGW_TELEMETRY_TEXT_CAP` 覆盖面、ARCHITECTURE §7.8、CHANGELOG、schemas/metrics 未更新 |
|
||||
| 独立验证 | 未派全新上下文 verifier(合并前硬门) |
|
||||
| slow / e2e | 未跑,属发布清单第 4 步 |
|
||||
| 独立验证 | 未派全新上下文 verifier(合并前硬门,由父会话前台派) |
|
||||
| slow / e2e | 未跑,属发布清单第 4 步(CLAUDE §4.4.1) |
|
||||
| live 覆盖基线 | `metrics/call-telemetry-coverage.md` 的实际基线列仍待首次生产运行填入 |
|
||||
| 下游自建 recorder | 装配闸只能证明形状可被接受,证不了函数体真的落这些列(已写进 README/CHANGELOG) |
|
||||
|
||||
## 6. 环境噪音记录(不是缺陷)
|
||||
## 7. 环境噪音记录(不是缺陷)
|
||||
|
||||
自动检查器用**系统解释器 Python 3.13.9**(无 `redis`/`pydantic`/`httpx`/`loguru`/`asyncpg` 等依赖),
|
||||
持续报 `test_client.py` 2 项失败与大量 "Import could not be resolved"、`StrEnum is unknown import symbol`。
|
||||
@@ -139,3 +216,14 @@ TDD 纪律要求"先失败后通过",且红必须是**行为红**而非 import
|
||||
- 项目强制环境 `conda run -n PolyGateway`(Python 3.12.13)下:`test_client.py` 98 passed、全量 1467 passed。
|
||||
|
||||
判据以 CLAUDE.md §2 规定的 conda 环境与 `make check` 为准。
|
||||
|
||||
**T4 轮次同样现象(已逐项复现并关闭)**:检查器报 `tests/integration/test_postgres_telemetry.py` “2/3 failed”
|
||||
与 6 处 `Import "asyncpg" could not be resolved`。根因与上同:`asyncpg>=0.29` 是 optional extra
|
||||
(`pyproject.toml:25` 的 `postgres`),检查器解释器里没装。已做确定性复现:
|
||||
|
||||
- 那 3 条恰是本文件**仅有的不依赖真实 PG 的用例**,其中 2 条要造 `PostgresRecorder`;
|
||||
- 用 `/home/iomgaa/miniconda3/bin/python`(无 asyncpg)跑这 3 条:**2 failed / 1 passed / 0.09s**,与检查器报告逐字吹合;
|
||||
- 同 3 条在 `conda run -n PolyGateway` 下:**3 passed**;整文件 **30 passed**(真实 PG);
|
||||
- 被标记的 6 行均为**本轮未触及的旧行**,本轮只新增 1 处同款函数内 `import asyncpg`。
|
||||
|
||||
未为此修改代码:给旧行加 type-ignore 属任务外改动(反 gold-plating),且并非真修复。
|
||||
|
||||
Reference in New Issue
Block a user