Files
Jyotisha/docs/tasks/TASK-rectification-validation-integrity-20260920.md
T
Jesse_ChenandClaude Opus 5 5049e7847b docs(tasks): 生时校正验证体系补缺任务书(BUG-978~980)
会议要求把「推断真实出生时间」与「用户认可的参考盘」分开证明。逐条核对
后:产品口径侧四条已落地(accepted≠confirmed 两条写入路径、确认门
fail-closed、采用不写 reported_birth_time、无把采用率当准确率的指标),
缺口全在评测本身。

三条实证:封存契约里三个打分哈希互不相同且 official_eval_trial_count 为
0,当前实现从未产出过有效官方盲测;全部离线评测的候选窗以真值为圆心而
生产以申报时间为圆心,真值掉出窗外这一失败模式从不可见;v3 封存但每例
仅 3 事件、v4 事件够却已被看过并用于调参,六题回放的高分是真值方向最优
答的上界。

任务分解:申报偏差敏感性 sweep、封存盲测有效重跑 + 契约对齐 + 防复发
测试、v5 采集协议(只出协议)、记录。硬红线:不改 12 个打分文件、不用
封存集调参、不得把 status 改 ready。家庭信息在拿到基线数字前不开工。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
2026-09-20 11:05:46 +08:00

176 lines
18 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.
# TASK · 生时校正验证体系补缺:申报偏差评测与封存盲测有效重跑(2026-09-20)
> 状态:待领取。**纯离线评测与契约对齐,不改任何打分逻辑、不改产品行为、不动确认门。**
> 起因:产品会议对「我们究竟在证明什么」提出边界要求(真实出生时间 vs 用户认可的参考盘必须分开证明)。Claude 2026-09-20 对代码、评测数据与线上口径做了逐条核对,结论见 §1。
> **产品口径侧的四条要求(accepted≠confirmed、不承诺精确分钟、不覆盖原始资料、不拿用户认可当验证)核对下来已经落地**,本单不碰那一层;缺口全部在评测本身。
## 0. 基线与交付
- 基线:`origin/staging` = `fcad0637`(2026-09-20 实测核对)。
- worktree `.worktrees/rectification-validation-20260920`,分支 `codex/rectification-validation-20260920`。
- 改动落点:`scripts/research/**`、`tests/**`、`references/rectification_sealed_holdout.v1.json`、`docs/**`。前三项在 `deploy/gated-paths.txt` 内,推 staging 会触发门禁并重新发布镜像。
- **前置依赖(不在本单,不阻塞开工)**:`TASK-owner-case-purge-20260919` 的三个提交 `fe9489e6` / `36c443bf` / `497798ac` 已验收通过但**尚未合入 `staging`**(已核:三者均不是 `origin/staging` 的祖先)。那条分支带着仓库级隐私守卫 `tests/test_repo_privacy_markers.py`。本单不依赖它即可执行,但**在它合入之前,任何「拿自己的案例试一下」的结果都不具备独立性**,不得当作验证证据。放行由产品负责人决定。
## 1. 事故实证
基线 `fcad0637`,行号按符号定位。
### 1.1 三个打分实现哈希互不相同,任何成绩都无法归属(BUG-978)
`references/real_case_calibration/minute_rectification_holdout_v3.json` 的 `frozen_scoring` 对 12 个打分文件做 SHA-256 封存。三处哈希实测:
| 来源 | 哈希前缀 |
| --- | --- |
| 封存值 `frozen_scoring.implementation_sha256` | `f41c298dd6cdcebe` |
| 契约文件记录的当前树 `current_tree_scorer.implementation_sha256` | `99730c84c6434f52` |
| **基线树实测**(`implementation_sha256(frozen_scoring.files)`) | `b15d9ea15227cd58` |
三者两两不等——契约文件 `references/rectification_sealed_holdout.v1.json` 里的 `current_tree_scorer` 块本身已经过期。同一文件另有两处与现实不符:`source_audit_status` 仍写 `invalidated_after_replay`,而 v3 数据集在 2026-09-14 已改为 `corrected_known_date_errors`;`evaluated_on` 仍停在 `2026-07-21`。
后果记在该文件自己的字段里:`official_eval_implementation_hash_matches: false`、`official_eval_trial_count: 0`——**当前打分实现从未在封存集上产出过一次有效的官方盲测成绩**。
- 唯一跑过的那次(2026-07-21):top-1 `0.15`、top-3 `0.25`、MAE `6.95` 分钟、确认覆盖率 `0.0`,报告状态 `invalidated_after_source_audit`(资料审计发现两处错年,已在 `docs/research/holdout_v4_build_2026_09_14.md` 更正)。
- 现在唯一能看到的较新数字(top-1 `0.45` / top-3 `0.50` / MAE `5.8`)来自 `current_tree_unfrozen_diagnostic` 块,该块自带 `is_blind_evaluation: false`、`results_already_seen: true`、`must_not_claim_as_release_metrics: true`、`must_not_use_for_tuning: true`。
预注册发布门(`minute_rectification_holdout_v3.json` 的 `release_metrics`):top-1 ≥ `0.60`、top-3 ≥ `0.85`、MAE ≤ `2.0` 分钟、误确认率 ≤ `0.05`、信息不足正确拒绝率 ≥ `0.90`。
**计算成本不是障碍**:基线树实测,20 例全量打分 ±10 档 `1.6 s`、±60 档 `8.6 s`。
### 1.2 所有离线评测的候选窗以真值为圆心,生产以申报时间为圆心(BUG-979)
- `scripts/minute_rectification_blind_eval.py` 的 `_candidate_moments()` 以 `case["birth"]["time"]`(Astro-Databank AA 记录的真实分钟)为中心,向两侧各取 `candidate_radius_minutes` 分钟。
- `scripts/minute_rectification_fact_blind_eval_v4.py` 直接 import 复用同一函数。
- `scripts/research/probe_supply_after_six.py` 的 `request_from_case()` 同样以真值为心,局部变量名就叫 `true_time`,`start_time` / `end_time` 由它 ± 半径得到;`scripts/research/minute_resolution_sweep.py` 的 `scoring_request_for()` 调的就是它。
生产不是这样:`frontend/src/lib/rectification-agentic/v9/search-window.ts` 的 `ENGINE_SEARCH_RADIUS_MINUTES = 15`,窗以用户**申报**时间为中心(`frontend/src/lib/rectification-agentic/birth-time-provenance.ts` 文件头注释明写 "Scoring still uses reportedBirthTime as the search-window centre"),可放宽到 30 / 60 / 120。
后果:离线评测里真值**永远在窗正中心、永远在候选集里**。「申报偏差大于半径 → 真值根本不在候选里」这一失败模式在现有全部评测中不可见,其发生率从未被测量。全仓检索偏移窗相关键在评测脚本内 0 命中。
### 1.3 现无「口径干净且事件数贴近真实会话」的封存集(BUG-980)
| 数据集 | 是否封存 | 每例事件 | 每例领域 | 可否当发布门 |
| --- | --- | ---: | ---: | --- |
| `minute_rectification_holdout_v3.json` | 是(`truth_hidden_from_ranker: true`) | **3** | 2–3 | 可,但事件数远低于真实会话 |
| `minute_rectification_holdout_v4.json` | **否**(`truth_hidden_from_ranker: false`) | ≥7 | ≥4 | **不可**,结果已被看过且用于 R1–R5 调参 |
`docs/research/holdout_v4_build_2026_09_14.md` 自己写明 v4「是开放评价集,用来看尺度,不是第二份封存盲测」;真实会话每例 5–18 件事。
另:`docs/research/minute_resolution_2026_09_14.md` 记载六题回放用「**真值方向最优答**」。因此 0.80 / 0.55 / 0.35(±10 / ±30 / ±60)是**上帝视角上界**,不是产品表现;其中 ±30 与 ±60 两档仍低于 `0.60` 的发布门,而生产默认半径 ±15 正落在两档之间。
### 1.4 产品口径侧已经达标(不构成 BUG,记此以免重复排查)
- `accepted` 与 `confirmed` 是两个状态、两条写入路径(`frontend/supabase/migrations/20260813010000_agentic_rectification_v9_agent_api.sql` 的 `accept_agentic_rectification_candidate_for_case` 与 confirm 函数),`selection_kind` 区分 `user_accepted` / `engine_confirmed`。
- 采用候选写 `active_birth_time` / `birth_time`,**不写 `reported_birth_time`**——原始申报保留,且仍是搜索窗圆心。
- 确认门 fail-closed:`scripts/rectification/decision_policy.py` 的 `apply_confirmation_decision()` 要求四闸同时通过,其一是 `scripts/rectification/sealed_holdout.py` 的 `holdout_passed()`。封存契约 `status: not_ready` 且 `confirmation_coverage_rate: 0.0`,因此 `confirmation_allowed` 今天在任何情况下都不可能为真。
- 交付卡自带标签 `candidate_range_not_birth_time_truth` 与「采用只会把代表性时间写入当前排盘,不是已确认的唯一出生分钟」。
- 全仓无「把采用率 / 满意度当准确率」的指标;无任何按特定生日走捷径的运行时分支(上游 `PL9_BAV_CELL_OVERRIDES`、`_known_case_regression_hint` 在本仓 0 命中)。
**关联记录:`BUG-098`**(公开候选门禁未明确独立 holdout 边界)。其防复发条写明「任何 holdout 完成声明必须先有稳定分区持久化与校准验收证据」。本单三条与之同源:那条措施**仍然有效,但从未被执行到底**,且没有任何测试守着它。
## 2. 根因
1. 封存契约文件 `references/rectification_sealed_holdout.v1.json` 是**人工维护**的(`scripts/rectification/sealed_holdout.py` 文件头注释:「A human updates ... after a valid passing evaluation」),没有任何测试断言它与数据集、与当前树哈希一致 —— 打分代码每改一次,它就更旧一次,而没有人会被提醒。
2. 评测脚本是从「给定真实出生时间,能否把它排到第一」出发写的;产品解决的是「给定用户申报时间,真实时间在哪」。两者差一个偏移量,建评测时没有人把这个差异写成一项指标。
3. v4 建立时就定性为开放评价集,此后没有人补上对应的封存孪生集,于是「事件数真实」与「口径干净」只能二选一。
## 3. 决策记录
- **2026-09-20 产品授权出本单**,覆盖核对结论中的两条行动:补申报偏差评测、重跑有效封存盲测;外加一条只出协议的封存集规划。
- **本单只产出数字与结论,不改打分逻辑、不调参、不改产品行为。** 即便重跑成绩过了发布门,执行方**不得**把 `status` 改成 `ready`、不得让确认门打开 —— 过门是产品决策,另立单。
- **家庭信息(父母职业、父母出生年份、兄弟姐妹)在本单产出基线数字之前不开工。** 核对已确认:现有七个领域全部是带日期事件,打分输入只认 `{id, domain, date, precision}` 四个字段(见 `scripts/minute_rectification_blind_eval.py` 的 `_request()`),静态家庭属性**没有输入口** —— 加它不是多问一道题,是新开一条打分通道。若产品另有决定,请在领取前更新本段。
- 上游清除分支的放行是产品动作,不在本单(§0)。
## 4. 硬红线
1. **不得修改 `frozen_scoring.files` 里那 12 个打分文件中的任何一个**,也不得修改 `scripts/rectification/decision_policy.py`、`scripts/rectification/sealed_holdout.py` 的判定逻辑。本单是量,不是改。
2. **不得把封存集 v3 用于任何调参**;不得因成绩难看而更换数据集、更换半径或剔除案例。剔除只允许发生在 `minute_rectification_holdout_validator.validate()` 判定为 `invalid_cases` 的情况下,且必须列出被剔案例与理由。
3. **不得让 `references/rectification_sealed_holdout.v1.json` 的 `status` 变成 `ready`**,不得抬高 `confirmation_coverage_rate`。
4. 新增的偏差评测必须与现有评测**并存**;不得改写 `_candidate_moments()` 与 `request_from_case()` 的现有行为(改了会让两轮历史结论不可比)。
5. 任何数字进文档必须带口径:ayanamsa / node mode / 半径 / 步长 / 数据集 / 打分实现哈希前 16 位。**不得把非盲诊断写成发布指标。**
6. 不得写入真实用户资料;案例只用已登记的公开 AA 名人;不得在任何文档里写出生分钟或坐标。
7. 不顺手升级依赖、不修不在本单内的 warning。
8. `frontend/src/app/page.tsx` 与 `scripts/jyotish_api_server.py` 均不得增长(本单本不该碰前端与主 API,碰了说明跑偏)。
## 5. 任务分解
### T1 · 申报偏差评测(BUG-979)
新增 `scripts/research/reported_offset_sweep.py`:在现有取候选方式之外,增加一个「模拟申报时间」的圆心。
- 对每个案例,把窗心从真值平移 `offset ∈ {0, ±3, ±5, ±8, ±10, ±15, ±20, ±30}` 分钟,半径取产品档位 `{15, 30, 60}`。
- 每个(案例, 偏移, 半径)组合输出四项:**真值是否落在窗内**、真值在候选中的排名、头名与真值的分钟距离、交付区间是否覆盖真值。
- 汇总成一张表:横轴偏移、纵轴半径,格子里是「真值在窗内的比例 / 头名命中 / 区间覆盖」。
- 真值仍然**排完名之后**才揭晓;并列沿用现有哈希打破方式(`_opaque_winner`),**不得按接近真值挑选**。
验收标准:
- `python3 scripts/research/reported_offset_sweep.py --json` 在基线树跑通,输出含上述四项与完整口径块(含打分实现哈希前 16 位)。
- 新增 `tests/test_reported_offset_research.py`,至少断言:(a) 偏移为 0 时窗心等于真值且「真值在窗内」为 `1.0`;(b) 偏移绝对值大于半径时「真值在窗内」为 `0`;(c) 口径块字段齐全;(d) 并列打破不依赖真值。
- 结论写进 `docs/research/reported_offset_2026_09_20.md`,**必须明确回答一句**:在半径 ±15 下,申报偏差多大时真值开始掉出窗外;以及现有放宽路径(30 / 60 / 120)能把这个比例救回多少。
- 本任务**不需要**知道真实用户的申报偏差分布;它给的是「如果偏差是 X,会怎样」的敏感性曲线。真实分布怎么拿,进 T4 的遗留项。
### T2 · 封存盲测有效重跑与契约对齐(BUG-978)
1. 冻结当前打分实现:把基线树实测哈希(`b15d9ea15227cd58…`,执行方自己重算确认)连同 12 个文件清单写进一次新的评测记录;**不改 v3 的 `frozen_scoring` 原值**(那是历史封存值,要留着做对照)。
2. 在 v3 封存集上跑一次完整盲测,产出五项预注册指标:top-1、top-3、MAE、误确认率、信息不足正确拒绝率。
3. 把 `references/rectification_sealed_holdout.v1.json` 更新到与现实一致:`source_audit_status` 取 v3 数据集当前值;`current_tree_scorer` 刷新到本次实测哈希与成绩;新增一个块记录本次重跑,注明它是**首次口径干净的官方盲测**,以及「v3 每例仅 3 件事,是下限口径,不代表真实会话」。`status` 保持 `not_ready`(§4.3)。
4. 新增 `tests/test_sealed_holdout_contract_freshness.py`:断言契约文件的 `current_tree_scorer.implementation_sha256` 等于当前树实测值、`source_audit_status` 等于 v3 数据集里的值。**这条测试就是防复发** —— 它一红就说明打分代码改了而契约没跟上。
验收标准:
- `.venv/bin/python -m pytest tests/test_sealed_holdout_contract_freshness.py tests/test_minute_rectification_holdout_validator.py tests/test_minute_rectification_fact_blind_eval_v4.py -q` 通过。
- 契约文件三处过期字段全部对齐,且 `status` 仍为 `not_ready`、`confirmation_coverage_rate` 仍为 `0.0`。
- 五项指标与发布门逐条对照写进 `docs/research/sealed_holdout_rerun_2026_09_20.md`,每项标「通过 / 未通过 / 不适用」。
- **确认门行为零变化**:跑 `frontend/tests/rectification-confirmation-gate.test.ts` 与相关 pytest,结果与基线逐条相同。
- 快速门 `python3 scripts/run_quality_gate.py --profile quick` 与基线逐条比对。
### T3 · 封存集 v5 采集协议(BUG-980,只出协议、不采集)
写 `docs/research/sealed_holdout_v5_protocol_2026_09_20.md`:
- v5 目标:**封存 + 每例 ≥7 件带年月事件 + ≥4 个领域**,即 v4 的口径、v3 的纪律。
- 人物名单必须与 v3 / v4 的 20 例**不重叠**(那 20 例成绩已被看过)。列出 ≥25 个候选公开 AA 人物作为备选池,只写人物与资料来源类型,**不写出生分钟或坐标**。
- 标注协议沿用 `holdout_v4_build_2026_09_14.md` 的四条(AA 记录、事件来自公开页且 `independent_of_birth_source=true`、工作日期须核实、三档半径写在数据集上),并补一条:**v5 建成后,在产出第一份成绩之前不得被任何人读取事件内容做调参**。
- 工作量估算:按每例 7 件事、每件需一处可核实公开来源计,给出人工小时数区间。
- 明确写出:「在 v5 就绪之前,任何对外的分钟级准确率说法都没有依据」,并指明 T2 的数字是下限口径、T1 的数字是敏感性而非准确率。
验收标准:文档含上述全部要素;候选池 ≥25 人且与现有 20 例无交集;不含任何出生分钟、坐标或真实用户资料。
### T4 · 记录
- `docs/BUG_HISTORY.md` 新增 `BUG-978` / `BUG-979` / `BUG-980`,状态按实际填(T1 / T2 完成的可写 `resolved` 并附证据;`BUG-980` 只出协议,保持 `investigating` 直到 v5 就绪)。三条都必须**关联 `BUG-098`**,并说明那条防复发措施为何没拦住。
- `BLOCKED.md` 记一条:真实用户的申报偏差分布目前拿不到(需要有独立出生记录的真实用户样本,无受控账号),T1 只能给敏感性曲线。
- `docs/research/ACTIVE_FRONTS.md` 加一节索引本单三份文档,并指回 `rectification_minute_resolution_closure_2026_09_14.md`。
- `docs/tasks/README.md` 状态板加一行;**实现合入 staging 的同一次推送里改状态**。
- `CHANGELOG.md` 不写(无用户可感知行为变化)。
## 6. 让步顺序
时间不够时按此顺序砍,砍掉的写进进度记录:
1. **最先保 T2** —— 它最便宜(±10 档全量 1.6 s),且产出的是第一份能对着发布门看的数字。
2. 其次保 T1 的 ±15 一档与 `{0, ±5, ±10, ±15, ±20}` 五个偏移;其余半径与偏移可后补。
3. T3 候选池可从 25 人降到 15 人,但**协议正文与那句「在 v5 就绪之前没有依据」不得删**。
4. **不得为赶工跳过 T2.4 那条防复发测试** —— 没有它,这三条明天会重新长出来(`BUG-098` 就是这么回来的)。
## 7. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-validation-20260920 \
.worktrees/rectification-validation-20260920 origin/staging
cd .worktrees/rectification-validation-20260920
git log --oneline -1 # 必须是 fcad0637 或其后代
python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45
```
本单属 `AGENTS.md` §9 的引擎 / 评测类任务,`pre_work_check.py` 必跑,并先读 `docs/research/pre_work_error_ledger.md`。
**另必读** `docs/research/rectification_minute_resolution_closure_2026_09_14.md` —— 它已经证伪了两条假设(加权重能拉开候选、簇合并把候选并没了),本单不得重走;它的第 6 节三条提醒(先确认指标口径、不要从 docstring 推断行为、真值覆盖率优先于区间宽度)对本单全部适用。
环境备忘:核对机上无 `.venv`,系统 `python3`(3.13)可直接 `import swisseph`;若执行方环境有 `.venv`,按 `AGENTS.md` §10 用 `.venv/bin/python`。
## 8. BUG 编号起点
`docs/BUG_HISTORY.md` 当前最大号 **975**;`TASK-consult-smalltalk-fastpath-20260920.md` 已占用 **976 / 977**。本单从 **BUG-978** 起,连续三条。开工时重新核对最大号,若已被占用则顺延并在进度记录里说明。