fix(rectification): keep proportional prior after holdout calibration (BUG-560)

Implement offset and softmax relative-support scales and a public holdout calibration script, but leave the default proportional because no scheme passed the coverage-and-width gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-06 23:05:42 +08:00
co-authored by Cursor
parent 3a9ae736e1
commit 62afa521f4
9 changed files with 642 additions and 10 deletions
+48 -1
View File
@@ -8629,6 +8629,54 @@
- 复发自:BUG-553(`a1956deb` 的测试没断言守卫会命中)
- 修复版本:`0102a973`
## BUG-558 | 采集问完落到「也可以再说一件事」,没有交付出口
- 状态:resolved
- 首次发现:2026-09-06
- 最近更新:2026-09-06
- 影响面:`persistExhaustionCollect`、`exhaustionSpokenCollectFollowup`、`ensureNonTerminalTurnExit`、口述采集 UI、`POST /api/rectification/agent`
- 用户现象:七个带年份领域和职业都问过之后,助手仍问「也可以再说一件你记得大概时间的事」,界面只剩不可点的范围小字,没有候选卡、没有停止按钮、没有下一步。
- 触发条件:方法覆盖完成、带年份领域都问过或拒过、职业已覆盖;非收敛出牌或非终止修复走穷尽采集。
- 根因:`exhaustionSpokenCollectFollowup` 在 dated / occupation 之后无条件落到 `otherCollectFollowup`。`domain="other"` 不在剩余采集集合里,也不挡出牌,但作为 `collect:other:*` 焦点被写回后下一轮还回到同一句,没有终止条件。口述采集没有停止按钮;`ask_about_result` 只在点选焦点处理。
- 修复:穷尽采集不再问 `other`。还有 dated / occupation 就继续问;否则 `acceptance_allowed` 且训练门开且可采用则 `adopt_representative`(`stopReason=probe_pool_exhausted`),否则写门槛翻译句、不写焦点,并用 `terminalNote` 满足非终止出口。口述采集在输入框上方放与点选卡同文案的停止按钮;只读范围改为同一停止动作。`ask_about_result` 与 `stop_rectification` 在点选和口述焦点都映射到停止。`keep_collecting` 仍继续采集,不得劫持成出牌。
- 验证:`frontend/tests/rectification-exhaustion-exit-20260906.test.ts`;既有 adopt / spoken-collect / timeline / hidden-e2e 三栏更新。
- 防复发:`exhaustionSpokenCollectFollowup` 不得再返回 `other`。穷尽日志必须有 `rectification_exhaustion_collect` 且不含证据摘要或年份。不得把 `keep_collecting` 改成强制采用。
- 相关记录:BUG-550、BUG-546
- 复发自:无
- 修复版本:`61ea5671`
## BUG-559 | 同年月拆成多道「有没有 X」,同域同年再问,无锚点性格卡
- 状态:mitigated
- 首次发现:2026-09-06
- 最近更新:2026-09-06
- 影响面:`scripts/rectification/event_probes.py`、`asked_probe_keys`、`existenceProbeAsked`、`rankRenderableDiscriminators`
- 用户现象:同一大运边界月按领域各问一次「那时候有没有入职 / 有没有搬家」;已问过某年 5 月后又问同年无月份;没有对应经历的「平时做事更接近哪一种」性格卡也会出。
- 触发条件:引擎按领域扫描边界月;去重只看账本年份不看已问探针;`varga_style` 不要求同域带年月证据。
- 根因:existence 探针按 `(domain, year, month, source)` 独立出题;`_existence_blocked_years` 不并入已问探针年份;性格对照卡没有锚点门槛。
- 修复:请求可带 `asked_probe_keys`(不进结果指纹)。已问 `domain.Y.MM.*` 后,同年与相邻年不再出该领域 existence / activation。`varga_style` 无同域 confirmed 带年月事件则 `dropped_probes(unanchored_varga_style)`;有锚点时 hint 带年月、分数 ×0.8,用户可见题干仍不写年份。邻近账本事件以领域+年月(无摘要)写入 `user_prompt_hint`。B2 同年月合并卡 `choice_kind=boundary_pair` 本单未做。
- 验证:`tests/test_rectification_event_probes.py` 已问同年相邻年不再产出;`frontend/tests/rectification-probe-year-dedupe-20260906.test.ts`。
- 防复发:不得把 `asked_probe_keys` 写入结果指纹。点选答案仍不写证据账本。不得为性格卡在用户可见题干里写年份。
- 相关记录:BUG-540、BUG-390
- 复发自:无
- 修复版本:`770d378d`
## BUG-560 | 相对支持度正比例归一把引擎证据压成 7–9,lead 8 不可达
- 状态:blocked
- 首次发现:2026-09-06
- 最近更新:2026-09-06
- 影响面:`scripts/rectification/decision_policy.py::_relative_support`、公开簇代表 prior、前端 `unionStillValidRange`
- 用户现象:多件带年月经历之后,公开候选相对支持度仍是 7–9,范围几乎不收;之后只靠点选卡 ±2 推分,问尽仍收不窄。
- 触发条件:公开最多 12 个簇代表按原始分正比例把 100 分掉;原始分底座远大于事件极差。
- 根因:`_relative_support` 按原始分正比例归一。`TASK-rectification-provisional-adopt-20260901.md` §2 已看到 lead 停在约 5,当时只改门、四个常数不动,没有改 prior 尺度。
- 修复:实现 offset(分−全网格最低分)与 softmax,由校准脚本在 20 例公开 holdout 上比覆盖与中位宽度。合入门:覆盖 ≥ 旧方案 − 1 且中位宽度更窄。实测无方案过门(offset 覆盖 12/20 < 16;softmax T=2 中位宽度 21 不小于旧值 21)。默认仍 `proportional`,`POLICY_VERSION` / `ALGORITHM_VERSION` 不 bump,旧 Case 不因此自动重算。
- 验证:`scripts/rectification_prior_calibration.py` holdout 表见进度记录;`tests/test_rectification_relative_support.py` 锁定默认 proportional,并断言显式 offset 在拉开的格子上领先不差于 proportional。
- 防复发:不得在校准门未过时启用 offset/softmax 或 bump 政策版本。不得改 `MIN_SEPARATION_LEAD=8` 或采用/确认/熔断常数来「先让分看起来够」。
- 相关记录:BUG-463、`TASK-rectification-provisional-adopt-20260901.md` §2
- 复发自:无
- 修复版本:未启用新尺度;代码与校准脚本见本分支 C 提交
## BUG-561 | 长报告 KP Lord/Sub 与专题 KP 表在失败信封下整段消失
- 状态:resolved
@@ -8694,4 +8742,3 @@
- 修复版本:`e4d16b75`
@@ -0,0 +1,116 @@
# PROGRESS · 生时校正采集出口 / 探针去重 / prior 校准(2026-09-06)
工作树:`.worktrees/rectification-convergence-exit-20260906`
分支:`codex/rectification-convergence-exit-20260906`
基线:任务书 `2dc454c9`(staging 当时代码头 `e2f4b55c`)。执行期间 `origin/staging` 另有文档提交,本分支未 rebase。
未改 `SKILL.md`(仍 10.0.14)、未改 `scripts/jyotish_api_server.py`、未改 `frontend/src/app/page.tsx`。未改采用门 / 确认门 / `MIN_SEPARATION_LEAD=8` / 熔断常数。未把 `.venv` 加入提交。
本单状态:**部分完成**。A 完成;B1+B3 完成、B2 未做;C1 代码保留、C2 未过门故未启用。
| 任务 | 状态 | BUG |
| --- | --- | --- |
| A1 穷尽日志 | 完成 | BUG-558 |
| A2 穷尽即交付 | 完成 | BUG-558 |
| A3 口述停止 + 范围可点 | 完成(同分支,与 A1/A2 同一提交) | BUG-558 |
| A4 事故形状回归 | 完成 | BUG-558 |
| B1 同域同年去重 | 完成 | BUG-559 |
| B2 同年月合并卡 `boundary_pair` | **未做** | BUG-559 |
| B3 varga_style 锚点 | 完成 | BUG-559 |
| 题干邻近账本年月 | 完成 | BUG-559 |
| C1 offset/softmax 实现 | 完成(默认未启用) | BUG-560 |
| C2 holdout 校准门 | 未过门 | BUG-560 |
| C3 存量自动重算 | 路径仍在;本单未 bump 版本,故不会触发 | BUG-560 |
| D 记录 | 完成 | — |
## A · 出口(BUG-558)
- `exhaustionSpokenCollectFollowup` 在 dated + occupation 之后返回 `null`,不再落到 `other`。开场仍走 `!dashaCovered` 的 GENERIC 句;`USER_COLLECT_QUESTION.other` 留在文案表,穷尽路径不再问它。
- `persistExhaustionCollect` 每次入口 `console.warn` 结构化 `rectification_exhaustion_collect`(`case_id`、can_adopt、ceiling、training_gate、stop_class、ranked_count、probe_key、budget、next_domain/source)。不含 evidence summary、不含年份。
- 无剩余采集时:`acceptanceAllowed && trainingGate.open && canAdopt` → 采用,`stopReason=probe_pool_exhausted`,文案「能分开候选的问题已经问完,先按现有材料给你结果。」;否则门槛翻译句 + `terminalNote`,`ensureNonTerminalTurnExit` 见到 `terminalNote` 不抛。
- 口述采集:输入框上方 44px `.rectification-collect-stop`,文案 `CHOICE_STOP_LABEL`。`RectificationReadonlyRange` 改为 button,同一 `submitStop`。`ask_about_result` 与 `stop_rectification` 在点选和口述焦点都进停止。点选卡仍隐藏停止项。
- **A2 纠偏(未合入的中间稿):** 曾把 `keep_collecting` 在方法已覆盖时劫持成采用。证据不足、应继续采集的用例会变成 `completed_with_range`。已改回:`keep_collecting` **一律** `collect(...)`。
### 既有断言改动
| 文件 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `rectification-decision-authority` 出牌 | `offer_provisional_range` | `ready_to_adopt` | 探针空、方法覆盖且可采用时穷尽交付 |
| `rectification-hidden-e2e` 同上 | 同上 | 同上 | 同上 |
| `occupationClosed.next_followup` | `other` | `null` | 穷尽不再问 other |
| adopt-flow / spoken-collect / timeline-adapter 源扫描 | 禁止 `rectification-collect-stop` | 要求出现 | A3 口述停止按钮 |
| `rectification-answer-choice` 点 A | 旁白含范围、不再问家人 | 下一问家人采集 | B3 丢掉无锚点性格卡后按方法轮转到家人;点选仍不写账本 |
## B · 题型(BUG-559)
- Python:`asked_probe_keys` 经 `normalize_rectification_request`(0–200 条、每条 ≤120)。指纹构造 **排除** 该字段。`known_years = 账本年份 ∪ 已问年份`,再进 `_existence_blocked_years`(含 `EXISTENCE_NEARBY_YEARS`)。
- TS:`existenceProbeAsked` 按 `domain.year` 与相邻年匹配。`engine-client` / `rectification-v9-tools` 把已问键传给引擎。
- `nearby_ledger_note` / `nearbyLedgerHint`:只拼领域 + 年月,不写 summary。
- B3:无同域 confirmed 带年月事件的 `varga_style` → `dropped_probes(unanchored_varga_style)`;有则 hint 带年月、`selection_score *= 0.8`。用户可见卡题干仍是「平时做事,你更接近下面哪一种?」(不写年份)。
### B2 偏离
未实现 `choice_kind: "boundary_pair"`,未改 `contracts/probe-question-v1.json`,未改四选项 yes/weak_yes/no/unsure 语义。原因:让步顺序允许一天内做不完则保留 B1+题干锚点;合并卡要同时改 Python 契约、TS `applyRectificationChoice` 双键写入与联合熵,本单先把死胡同和同年复问堵住。
## C · 收敛(BUG-560)
`decision_policy.py` 增加 `_relative_support_offset` / `_relative_support_softmax`;`build_candidate_decisions(..., support_mode=, temperature=)`;floor = **全部分钟网格**量化分的最低值。默认:
- `RELATIVE_SUPPORT_MODE = "proportional"`
- `POLICY_VERSION = "rectification-candidate-policy-v3"`
- `ALGORITHM_VERSION = "rectification-v5-matrix-scoring-7"`
前端 fixture 里写死的 v3 / matrix-scoring-7 缓存身份断言未改。
### C2 holdout(20 例,全部 `excluded_from_tuning`,radius 10)
脚本:`scripts/rectification_prior_calibration.py`。每例只 `score_from_matrix` 一次,再按方案重建公开决策。门:覆盖 ≥ 旧方案 − 1 **且** 中位宽度 < 旧方案。
| scheme | coverage | median width | mean |
| --- | --- | --- | --- |
| proportional(旧 / 仍默认) | 17/20 | 21.0 | 18.55 |
| offset | 12/20 | 10.0 | 10.3 |
| softmax_0.25 | 10/20 | 6.0 | 7.6 |
| softmax_0.5 | 11/20 | 9.0 | 9.55 |
| softmax_1 | 12/20 | 14.5 | 13.1 |
| softmax_2 | 16/20 | 21.0 | 17.15 |
无方案过门:offset 覆盖 12 < 16;softmax_2 中位 21 不小于 21。**未启用 C1。** 产品负责人确认前不得合入启用。
### C3
`storedSnapshotIsCurrent` 仍比较 `scoringPolicyVersion`;失配 → `snapshotCurrent=false`。本单未 bump,旧 Case 打开后 **不会** 只因本单重算。测试清单第 5 条按「不会自动重算」验收。
### C4 虚构 12 簇代表(任务书 §2.3 七件事量级:原始分 10.96–15.52)
单位测试用同一串分,显式调用 offset 时 top-2 领先不低于 proportional;等分时仍拆成 8/9。线上默认仍是 7–9 压平。公开文案「相对支持度 N」未改。
| 方案 | 12 个公开代表(示例) |
| --- | --- |
| proportional(启用) | 约 7–9,top-2 领先 0 |
| offset(未启用) | 拉开,低分可到 0 |
| softmax T=0.5(未启用) | 更尖,低分大量 0 |
## 测试
| 命令 | 结果 |
| --- | --- |
| `npx tsx --test` 13 个先回归文件(含 exhaustion-exit、probe-year-dedupe、answer-choice) | 225 pass / 0 fail |
| 非 DB `tests/rectification-*.test.ts` + `agent-voice-copy-contract.test.ts` | **905 pass / 0 fail** |
| `.venv/bin/python -m pytest` event_probes + engine_convergence + relative_support + v5_services | 70 pass / 0 fail |
| `tests/test_rectification_diagnostics_clusters.py` | 5 pass / 0 fail |
| `tests/test_active_rectification_api.py::test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow` | 本机失败(04:16 vs 05:07);**用 HEAD 的 decision_policy/event_probes 复跑同样失败**,非本单引入 |
| `./node_modules/.bin/tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error(既有 warning 未清) |
| `npx next build --webpack`(worktree `node_modules` 符号链接使默认 turbopack 拒掉) | 通过;`/` 仍是 ○ Static |
| `.venv/bin/python scripts/run_quality_gate.py --profile quick` | Python 段跑完后 `npm test` 失败:Docker 地址池耗尽导致 `*-database.test.ts` 起 postgres 失败;另有无关的 `onboarding-route` deadline 超时。非本单代码回归。本单相关非 DB 校正套件 905/905。 |
排除 `rectification-*-database.test.ts`(docker compose EPERM)。ERR-011:验收命令用 `python3 -m pytest` / `.venv/bin/python -m pytest`,不用裸 `pytest`。未改 `page.tsx`,首页静态化不应因本单变化。
## 三次提交
1. A `61ea5671`:出口、口述停止、A 测试与相关三栏、DESIGN/VOICE;`method-followup` 的 TS 去重/锚点因同文件随 A
2. B `770d378d`:引擎 `asked_probe_keys`、去重、邻近 hint、B 测试
3. C+D:prior 实现与校准脚本、相对支持测试、BUG_HISTORY / CHANGELOG / PROGRESS / testing / README
未 push。未改 `page.tsx`,首页静态化不因本单变化。
+1 -1
View File
@@ -61,7 +61,7 @@
| `TASK-consultation-context-and-cache-20260906.md` | `PROGRESS-consultation-context-and-cache-20260906.md` | 普通对话历史只取每条前 4,000 字(报告结论被砍、无标记)、历史窗口不看模型 `context_window`、无溢出识别;改为检查点式会话摘要 + append-only 尾巴 + 按模型预算 + 一次降级重试;Anthropic 历史断点;共享方法段进系统块;后台用量页显示缓存命中率 | 已验收 | `bf8ad0d1`(BUG-555/556);`test:db` 环境缺口;部署前先 Migrate Staging Database |
| `TASK-session-title-guard-fix-20260906.md` | `PROGRESS-session-title-guard-fix-20260906.md` | BUG-553 修复单:`consult/route.ts` 标题守卫比较的是 RPC 前快照,`append_consultation_question` 已改写标题,守卫恒 0 行,模型标题只靠客户端 PATCH 落库 | 待验收 | `codex/session-title-guard-fix-20260906`(BUG-557) |
| `TASK-rectification-collect-vs-offer-consistency-20260905.md` | `PROGRESS-rectification-collect-vs-offer-consistency-20260905.md` | 带年份采集没问完就出采用卡 + 报告,同一轮又被搬家采集题把卡挤掉:决策层判 `adopt_representative` 而计划层仍有 dated 采集(BUG-546 只修了一半);改为剩余采集未完保持 `collect_evidence`,出牌轮才出卡写报告 | 已验收通过 `ca4e2408`(2026-09-05;staging 部署仍停在 `afd14948`,`deploy-staging` 自 `c295b853` 起连续失败,先解决 `bab07187` 的待迁移) | `codex/rectification-collect-vs-offer-consistency-20260905`(BUG-550) |
| `TASK-rectification-convergence-exit-20260906.md` | `PROGRESS-rectification-convergence-exit-20260906.md` | 采集问完落到「也可以再说一件事」无出口(非收敛出牌/非终止修复兜底 `other`)、同年月多领域拆题与同域同年重复问、`relative_support` 正比例归一把引擎证据压平成 7–9 使 lead 8 不可达;改为穷尽即交付 + 口述态停止按钮、同年月合并为「发生了什么」四选卡 + 同域同年去重 + 性格题须锚点、prior 按 (分−窗口最低分) 归一并过 20 例公开 holdout 校准门 | 待领取 | `codex/rectification-convergence-exit-20260906`(BUG-558~560) |
| `TASK-rectification-convergence-exit-20260906.md` | `PROGRESS-rectification-convergence-exit-20260906.md` | 采集问完落到「也可以再说一件事」无出口(非收敛出牌/非终止修复兜底 `other`)、同年月多领域拆题与同域同年重复问、`relative_support` 正比例归一把引擎证据压平成 7–9 使 lead 8 不可达;改为穷尽即交付 + 口述态停止按钮、同年月合并为「发生了什么」四选卡 + 同域同年去重 + 性格题须锚点、prior 按 (分−窗口最低分) 归一并过 20 例公开 holdout 校准门 | 待验收 | `codex/rectification-convergence-exit-20260906`(BUG-558~560;B2 未做、C1 未启用,见进度) |
| `TASK-api-not-configured-mislabel-20260904.md` | `PROGRESS-api-not-configured-mislabel-20260904.md` | 16 处路由把数据库瞬断(部署切换窗口)兜底翻译成 503「服务尚未配置」;改为仅配置错误用该文案,其余 `service_unavailable`,收敛为共享 helper | 已验收 | `5483649b`(BUG-542);2 条子进程测试留 CI Node 22 复核 |
| `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505~509) |
@@ -0,0 +1,52 @@
# Staging 人肉复核 · 采集问完有出口、同年不去重问(2026-09-06)
给产品负责人。不要把真实案例里的年份、事件或候选分钟写进任何记录。
对应 BUG-558 / BUG-559 / BUG-560。测之前先做第 0 条。本单 **部分完成**:同年月合并卡(B2)未做;相对支持度换尺度未过校准门,线上仍是正比例归一。
## 0. 确认测的是新版本
浏览器打开 `https://staging.jyotisha.chat/api/health`,看 `deployment.gitCommit` 前 8 位是否等于本单合入 staging 后的提交。不一致 = 先别测。
## 1. 七个领域问完后给出结果或门槛句(P0)
新建生时校正,按任务书事故路径走完带年份采集和职业(可说「没有」跳过家人)。不要期待再出现「也可以再说一件你记得大概时间的事」。
- ✅ 预期:出现可点的候选时间卡,或一句能照做的门槛说明(例如还差带月份的经历)。口述采集时输入框上方有「先这样,先看当前范围」。
- ❌ 失败:最后一问仍是「也可以再说一件你记得大概时间的事」;或只有不可点的范围小字、没有下一步。
## 2. 口述采集可以先停下来看范围(P0)
任一口述采集题(家人 / 钱 / 搬家等)出现时:
1. 看输入框上方是否有「先这样,先看当前范围」。
2. 点它,或点范围那一行小字。
- ✅ 预期:两处都可点,动作相同,进入已停并给出当前范围;不是红色报错。
- ❌ 失败:口述态没有该按钮;或范围小字不可点;或点了之后仍停在采集题。
## 3. 同一领域同一年不再问第二次(P0)
若已出现「Y 年 M 月前后有没有入职/换工作」并答过,后续不应再出「Y 年前后工作上有没有入职/换工作」,也不应再出同年相邻年的同一领域存在题。
- ✅ 预期:同域同年(含前后一年)只问一次。
- ❌ 失败:同一领域同一年(只是去掉月份或换成相邻年)再问一遍。
## 4. 邻近经历写进题干提示;合并卡本单没有(P1)
本单 **没有** 做「某年某月发生了什么」四选合并卡(B2)。仍可能按领域各出存在题,但:
- 若账本里 ±2 个月内有另一领域带月份的事,引擎给模型的提示应提到该领域和年月,**不出现事件摘要原文**。
- 没有同域带月份经历时,不应出现「平时做事,你更接近下面哪一种」这类性格卡。
- ✅ 预期:无锚点性格卡不出;有锚点时题干仍可不写年份(年份在内部 hint)。
- ❌ 失败:没有任何对应经历就出性格对照卡。
- 本条不测合并卡。合并卡是后续单。
## 5. 旧 Case 不会因本单自动重算(P1)
相对支持度政策版本仍是 `rectification-candidate-policy-v3`,算法版本仍是 `rectification-v5-matrix-scoring-7`。`snapshotCurrent=false` 的失配路径还在,但本单没有 bump 版本。
- ✅ 预期:打开旧生时校正 Case,不会只因为本单就在第一轮自动重算候选。
- ❌ 失败:没有政策版本变化,却整案重算。
- 若以后校准过门并 bump 版本,那时才应看到「旧 Case 打开后第一轮会重算一次」。