feat(rectification): 少问几道就出卡,交付卡以范围为主(D1–D4)

- D1 引导窗口题每个校正最多 2 道(前端 GUIDED_WINDOW_CASE_LIMIT;引擎
  GUIDED_COLLECT_LIMIT 未动:event_probes.py 属冻结评分身份,改它需重新冻结)
- D2 七条定向线与跳过线重问问完即出卡,没问到的引导窗口不再挡卡,出卡后也不再挂窗口题
- D3 卡头加副标题「最可能 HH:MM」
- D4 前两列相差 ≥5 个百分点才显示相对可能性,否则一句「这几个时刻目前区分不开……」
- 离线回放 scripts/research/fewer_probes_card_replay.py:真值不降、宽度中位 ±1 分钟、提问 11.4→7.8
- Skill 10.0.29 → 10.0.30;DESIGN / VOICE / CHANGELOG / PROGRESS / 真机清单

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-09-26 14:27:12 +08:00
co-authored by Claude Opus 5.5
parent 7ddce2c96a
commit 4e6e8d87ef
48 changed files with 7885 additions and 54 deletions
@@ -0,0 +1,135 @@
# PROGRESS · 生时校正:减少无效追问 + 交付卡以区间为主(2026-09-26)
- 任务书:`docs/tasks/TASK-rectification-fewer-probes-card-20260926.md`
- 分支:`codex/rectification-fewer-probes-card-20260926`,基线 `origin/staging` `abb05b67`(代码与线上 `8a409434` 相同)
- 执行:Claude 子代理(直接执行模式,产品授权)。未推送。
- 产品调整,不开 BUG 号。Skill 10.0.29 → **10.0.30**。
## 结论先行
| 决策 | 做了什么 | 状态 |
| --- | --- | --- |
| D1 引导补经历最多 2 条 | 前端题池按 **每个校正最多 2 道** 引导窗口题(`GUIDED_WINDOW_CASE_LIMIT = 2`)。引擎 `GUIDED_COLLECT_LIMIT` **没有改**,见下方「偏离」 | 完成(实现位置偏离任务书字面) |
| D2 定向题问完直接出卡 | 能挡住出卡的线只剩「七条定向线 + 跳过线一次重问 + 进行中的年月追问」;没问到的引导窗口题不再挡卡(`cardHoldingLinesExhausted`)。区间、淘汰、采用门、确认门、刷新未试不出卡(BUG-654/656)都不变 | 完成 |
| D3 区间为主标题 | 主标题「目前范围 05:00–05:15(对照了 N 件经历)」不变,下面加副标题「最可能 05:07」(代表分钟) | 完成 |
| D4 百分比只在差距明显时显示 | 第一名比第二名高 ≥5 个百分点才每列写「相对可能性 N%」;否则不写数字,卡上一句「这几个时刻目前区分不开,补一件带年月的经历能帮助分开。」只有一列时两样都不写 | 完成 |
| D5 不做 | 没改打分、淘汰阈值、确认门、45 天闸门 | 遵守 |
| 硬红线 1 离线回放 | 真值在区间内的比例**没有下降**(±30、±60 各多保住 1 例);区间宽度中位差 **1 分钟**(±10 窄 1,±30、±60 宽 1);平均提问 **11.4 → 7.8** | **字面「不变」未完全满足**,数字如实列在下面,请验收方裁决 |
## 偏离:D1 为什么不改 `GUIDED_COLLECT_LIMIT`
任务书写「`GUIDED_COLLECT_LIMIT` 由 6 改为 2」。这个常量在 `scripts/rectification/event_probes.py`,而该文件属于**冻结评分身份**(`scripts/research/sealed_holdout_rerun.py` `PRODUCTION_FILES`)。实测改这一行后 Python 快速门 4 条失败(`tests/test_rectification_validation_integrity_gate.py`:`frozen_record_mismatch` / `production_scoring_sha256` 不等),且记忆化 golden 也要改。BUG-1047 的防复发写明:改冻结评分文件(即使输出不变)必须在任务书里列出重新冻结验证记录。本任务书没有授权重新冻结,所以:
- 引擎仍按 6 条排好序给窗口;前端题池在同一个校正里只问**前两道**没问过的窗口,第三道起不再出(新一轮引擎回执带来的新窗口也不会变成第三道)。
- 对用户效果与「上限 2」相同,而且比「每张回执 2 条」更严(原来答完一题后引擎换了候选集,会再冒出新窗口)。
- `event_probes.py`、golden 零改动;验证完整性门禁 4 条保持绿。
若产品仍要引擎侧也改成 2,需要另开一单写明 sealed holdout / reported offset 重新冻结。
## 离线回放(硬红线 1)
脚本 `scripts/research/fewer_probes_card_replay.py`,结果 `docs/research/fewer_probes_card_replay_2026_09_26.json`。
**口径与 `guided_collect_holdout_2026_09_16.json` 同源**:同一份 v4 开放集(20 例公开 AA 名人数据)、同样六道辨别题按真值作答、同样把引导窗口事件注入在真值候选自己的边界日期上(`truth`,理想作答),另跑 `opposite`(离真值最远的剩余候选的边界)做对照;三档半径 ±10 / ±30 / ±60。直接复用 `guided_collect_holdout_replay.py` 的 `posterior_state`、`precision_gate`、`window_boundary_dates`、`synthetic_event`。
**与 09-16 脚本的唯一区别**:09-16 在第一次达到精度门槛时就停(量「几件能达标」);这里按上线规则比较**最终交付区间**:
- 改前:引导池问完才出卡(BUG-751),回执里的窗口(上限 6)全部问。
- 改后:一个校正最多问前两道窗口(D1),七条定向线问完就出卡(D2)。
- 七条定向线两边相同,不建模;提问数 = 6 道辨别题 + 问到的引导窗口题。
### truth(理想作答)
| 半径 | 六题后:真值在区间内 / 宽度中位 | 改前:真值在区间内 / 宽度中位 / 平均提问 | 改后:真值在区间内 / 宽度中位 / 平均提问 | 区间完全相同的例数 |
| --- | --- | --- | --- | --- |
| ±10 | 20/20 · 15 | 20/20 · 15 · 11.4 | 20/20 · 14 · 7.8 | 18/20 |
| ±30 | 20/20 · 33 | 19/20 · 33 · 11.4 | 20/20 · 34 · 7.8 | 13/20 |
| ±60 | 20/20 · 56 | 19/20 · 52 · 11.4 | 20/20 · 53 · 7.8 | 14/20 |
### opposite(对照)
| 半径 | 改前:真值在区间内 / 宽度中位 / 平均提问 | 改后:真值在区间内 / 宽度中位 / 平均提问 | 区间完全相同 |
| --- | --- | --- | --- |
| ±10 | 20/20 · 15 · 11.4 | 20/20 · 15 · 7.8 | 19/20 |
| ±30 | 20/20 · 33 · 11.4 | 20/20 · 34 · 7.8 | 17/20 |
| ±60 | 20/20 · 54 · 11.4 | 20/20 · 53 · 7.8 | 14/20 |
- 平均引导窗口题:改前 5.4 → 改后 1.8(有的例子引擎给不满 6 / 2 条)。
- 精度门槛达标例数两边相同(±10:2/20;±30:1/20;±60:0/20),与 09-16「引导补件新增达标 0 例」一致。
- 120 行里 25 行最终区间不同:改后更窄 6 行、更宽 19 行。
- **改前丢了真值的两例都是多问出来的**:±60 一例被收成 13 分钟、±30 一例被收成 5 分钟,真值都落在外面;改后这两例都保住真值(区间 55 / 35 分钟)。也就是说,多问的窗口题有时把区间收窄到错误的位置。
- 与红线的差距:宽度中位差 1 分钟、真值比例只升不降。字面「均不变」不成立,只能说「真值不降、宽度中位 ±1 分钟」。不改代码去凑这个数。
### 方法局限(继承自 09-16 同口径,未修)
- 窗口年份上限取 `today.year`,不看月份,所以注入的事件可能落在 2026-09-16 之后(09-16 JSON 里就有 `2026-10-27` 这类注入日期)。两边同口径,比较仍成立,但它不是真实用户能报出的事。
- 改前模型只问一张回执里的窗口;真实线上答完一题后引擎会换回执、可能再冒出新窗口,所以改前真实提问数 ≥ 11.4,节省幅度只会更大。
- 七条定向线不建模,提问数只算辨别题 + 引导题。
## 实现清单
| 文件 | 改动 |
| --- | --- |
| `frontend/src/lib/rectification-agentic/v9/collection-question-pool.ts` | 新增 `GUIDED_WINDOW_CASE_LIMIT = 2`,`guidedWindowPool` 在本校正已问满 2 道窗口时返回空;新增 `cardHoldingLinesExhausted`(进行中的年月追问 + 跳过线重问 + 七条定向线,不含窗口) |
| `frontend/src/lib/rectification-agentic/v9/decision-from-dossier.ts` | `narrowingExhaustion` 的 `guidedCollectExhausted` 改用 `cardHoldingLinesExhausted`(仍要求刷新已试) |
| `frontend/src/lib/rectification-agentic/v9/method-followup.ts` | 出卡状态(`sessionOutcomeAllowsDelivery`)下,计划的收尾补问不再带引导窗口——否则卡出来了,下面还挂着一道「某年某月之间有没有什么事」、「更像这个」被锁。跳过线重问与七条定向线照旧 |
| `frontend/src/lib/rectification-agentic/core/rectification-decision.ts` | 只改注释:`mayDeliverOnPrecision` 逻辑不变 |
| `frontend/src/lib/rectification-agentic/user-copy.ts` | `RANGE_DELIVERY_PERCENT_MIN_GAP = 5`、`rangeDeliveryShowsPercents`、`rangeDeliveryMostLikely`、`rangeDeliveryIndistinct`,并登记到 `listUserVisibleCopy` |
| `frontend/src/components/rectification-range-delivery.tsx` | 副标题「最可能 HH:MM」;按 D4 显示百分比或那一句 |
| `frontend/src/app/globals.css` | `.rectification-range-delivery__most-likely`、`.rectification-range-delivery__indistinct` |
| `frontend/DESIGN.md` / `frontend/docs/VOICE.md` | 交付卡一节、出卡时机、D3/D4 文案 |
| `skills/jyotish-birth-time-rectification/**`、`versions/10.0.30/`、`skills/skill-package-registry.json`、`case-status.ts` | Skill bump 10.0.30(sha256 `926db1ab…3a13`,10.0.29 标 deprecated、快照保留,历史校正按绑定版本打开不受影响——BUG-621) |
| `scripts/research/fewer_probes_card_replay.py`、`docs/research/fewer_probes_card_replay_2026_09_26.json` | 离线回放 |
Skill 为什么要 bump:原 SKILL.md 写「所有线(含引导窗口题、跳过线重问、未覆盖领域题)问完后……才交付目前范围」,`candidate-comparison.md` 写「每列写相对可能性」,都与 D2 / D4 相反,不改会让 Agent 读到错误规则。改了三处:SKILL.md 出卡句 + 引导题「一次校正最多两道」;`conversation-strategy.md` 同步(它还残留 10.0.27 的「出卡须 `precision_gate_met`」旧句,一并改正);`candidate-comparison.md` 卡片结构与百分比规则。
## 测试
### 前端(Node 22.14.0)
| 项 | 结果 |
| --- | --- |
| `tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error(126 warnings,均为既有;改动文件无新增 warning) |
| `npm test` | 4012 条 / 24 fail / 27 skip;基线(同代码 `origin/staging`,Node 22)4002 / 24 / 27 |
| 失败清单对比 | 24 条与基线**逐条同名**(全部是 Docker / DB 套件),新增失败 0 |
| 测试名单对比 | 消失 0;新增 10(`frontend/tests/rectification-fewer-probes-card-20260926.test.ts`) |
| `npm run build -- --webpack` | 通过;`/` 仍 `○ (Static)` |
| 首屏 gzip(`rootMainFiles` 4 个文件) | 130933 B,与基线 130933 B 相同(0%) |
新增 10 条测试覆盖:D4 阈值(5 点、顺序无关、单列不显示);D3 标题与副标题;D4 差距 ≥5 显示三列百分比且无那句;差距 <5 无任何数字、只有一句、三列与排序不变;文案登记与 VOICE 一致;D1 每校正 2 道(`:next` 重问算同一道、进行中的不算、满额后落到定向线);D2 窗口未问不挡卡、跳过线重问与进行中的年月追问仍挡卡;D2 门槛未达也按现行规则出卡、确认门不开;D2 出卡状态(completed_with_range / provisional_range / adopt_representative)的下一问不是引导窗口题,采集状态仍会问。
### 改动的既有断言(三栏已写在测试文件里)
只有 Skill 版本号:15 个文件里的 `RECTIFICATION_SKILL_VERSION` / `skill_version` / 包路径 `"10.0.29"` → `"10.0.30"`,4 个文件里的 `/^version: 10\.0\.29$/` → `10\.0\.30`,`skill-registry.test.ts` 的版本与 sha256。每处上方一行「原值 / 新值 / 原因:D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)」。没有其他既有断言被改动或弱化。
### Python
| 项 | 结果 |
| --- | --- |
| 快速门 pytest 集(`gate-pytest-args`) | 948 passed / 1 skipped,与基线相同 |
| `tests/test_event_probes_guided_windows.py`、`tests/test_rectification_engine_memoization.py` | 通过(`event_probes.py` 与 golden 未改) |
### 截图(真实 Chrome 151 headless,CDP,虚构数据)
组件用 `renderToStaticMarkup` 渲染、挂 `next build` 产出的正式 CSS,在真实浏览器里截:`docs/testing/rectification-fewer-probes-card-20260926/`
| 文件 | 内容 |
| --- | --- |
| `gap-wide-1280.png` / `gap-wide-390.png` | 45% / 30% / 25%:副标题「最可能 05:07」,三列都有「相对可能性」 |
| `gap-narrow-1280.png` / `gap-narrow-390.png` | 35% / 33% / 32%:无任何百分比,卡上一句「这几个时刻目前区分不开……」 |
两种宽度页面 `scrollWidth` 等于视口(无横向滚动),「更像这个」最小高度 44px。这不是登录态整页走查——完整对话里的出卡时机与文案要真机看,清单见 `docs/testing/rectification-fewer-probes-card-20260926.md`。
## 观察(未改,留给产品)
1. 差距 <5 时卡上可能同时出现三句「分不开」:说明行里的「这几个候选按现有信息分不开。」(线都问完时)、参考题用过时的「这两分钟按现有信息分不开,参考题已经用过。」和新加的 D4 句。本单按任务书只加 D4 句,没删另外两句。
2. 副标题「最可能 HH:MM」在差距 <5 时仍显示(任务书 D3 无条件)。它和「目前区分不开」并存,读起来略拧;边界句「这只是代表性候选……」仍在卡底。
3. 送卡时若本校正还没问满 2 道引导窗口,交付旁白末尾仍会写「现在还剩 …… 再对照几件经历会更准」,这是邀请用户自己补,不是系统还会追问。
4. 候选已经明显分开(`separation.sufficient`)那条老路径不经过精度门槛,采集计划里若还有引导窗口题(本校正不足 2 道时)仍会先问——这不是「为门槛追问」,且受每校正 2 道上限约束,本单未改。
## 环境缺口
- 无 Docker:24 条 DB / 部署套件与基线同样失败,已逐条比对。
- 无登录态与模型凭据:真实对话里的出卡时机、Agent 旁白是否遵守新 Skill 句,留待部署后按 `docs/testing/` 清单真机走查。
- 未推送、未部署。
+1 -1
View File
@@ -35,7 +35,7 @@
| 任务书 | 进度 | 主题 | 状态 | 落点 |
| --- | --- | --- | --- | --- |
| `TASK-rectification-fewer-probes-card-20260926.md` | — | **减少无效追问 + 卡片区间为主**:引导补件离线新增达标 0 → 上限 6→2、定向题问完门槛未达直接出卡;卡片区间为主标题、代表分钟副标题,第一二名差距 ≥5 个百分点才显示百分比,否则写「目前区分不开」。不放宽任何置信度。先做 | 待领取 | — |
| `TASK-rectification-fewer-probes-card-20260926.md` | [PROGRESS](PROGRESS-rectification-fewer-probes-card-20260926.md) | **减少无效追问 + 卡片区间为主**:引导补件离线新增达标 0 → 上限 6→2、定向题问完门槛未达直接出卡;卡片区间为主标题、代表分钟副标题,第一二名差距 ≥5 个百分点才显示百分比,否则写「目前区分不开」。不放宽任何置信度。先做 | 待验收 | `codex/rectification-fewer-probes-card-20260926`(未推送;D1 改在前端每校正 2 道,引擎常量因冻结评分身份未动;回放真值不降、宽度中位 ±1 分钟、提问 11.4→7.8;Skill 10.0.30) |
| `TASK-rectification-telemetry-20260926.md` | — | **匿名聚合统计**:每会话一行只存数字 / 枚举(题数分类、宽度、差距、停止原因、门槛达标、耗时、版本),管理后台只看汇总、保留 180 天;动表须 test:db。排在 fewer-probes 后 | 待领取 | — |
| `TASK-rectification-offline-research-20260926.md` | — | **三项离线研究**:答错 1–2 题的容错、V1/V2 分盘配权正确重跑、改正「1 分钟≈1.1 天」(实测中位 3.8 天)并核实 `_representative_pairs` 推断。不改线上 | 待领取 | — |
| `TASK-rectification-code-split-20260926.md` | — | **代码拆分(只搬不改)**:聊天组件 2043 行 / 35 useState、`POST` 926 行、`runV9AgentTurn` 1047 行,269 处切源码测试;拆分 + 增长合同 + 切片测试改调用函数。排在 fewer-probes、telemetry 之后 | 待领取 | — |