Files
Jyotisha/docs/tasks/TASK-rectification-precision-gate-guided-collect-20260916.md
T
Jesse_ChenandClaude Fable 5.1 20435a1916 docs(tasks): 校正出卡加精度门槛 + 引导式补经历任务书
产品 2026-09-16 拍板:宽度 ≤10 分钟且不并列才出卡;用户说没有了仍按现行规则出卡;
门槛未达改为系统点名逐题问 + 年/月选择器录入,删自由文本邀请;跳过的线换问法再问一次;
时间点题答没发生不关领域、存在性题问整个领域、七条线一张表不按领域写死。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-16 08:50:35 +00:00

178 lines
22 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.
# 任务书 · 出卡加精度门槛 + 引导式补经历(2026-09-16
## 0. 基线
- 基线:`origin/staging` @ `071317f7`staging 部署 `7061d0d1`(含 BUG-689 邀请语)。
- 分支:`codex/rectification-precision-gate-guided-collect-20260916`worktree `.worktrees/rectification-precision-gate-guided-collect-20260916`
- 校正 Skill 当前 `10.0.26``skills/jyotish-birth-time-rectification/SKILL.md`)。本单要 bump 到 `10.0.27`
- 与本单同文件的待执行单:`TASK-rectification-collection-redesign-v2`BUG-661663`da048676`)、BUG-626/627/628、631/632、633/634。**若那些单仍未领取,本单优先;执行方开工前 `git log origin/staging` 核对它们是否已合入,已合入的以 staging 为准 rebase,不得回退它们。**
## 1. 事故实证(2026-09-16 真机,脱敏)
30 分钟搜索窗,用户给了 5 件带年月的事、3 个领域(学业、事业、居所),其中 3 件落在同一年 6 个月内(同一段大运,等于 1 件证据)。家人、婚恋、收入三条线全部关闭。6 道带年月点选题 + 1 道性格题答完后,流程直接出交付卡:
| 项目 | 数值 |
| --- | ---: |
| 交付区间宽度 | 20 分钟 |
| 区间内候选 | 5 |
| 前三名相对可能性 | 26% / 26% / 20% |
| 5 件经历对每个候选的吻合 | 5/5(零区分) |
卡下是 BUG-689 的自由文本邀请:「你要是还记得确切哪一天的事,不限领域,说出来我接着算」。产品负责人反馈:**精度没达到要求就出卡了;之后也不该让用户随便打字,要引导用户填。**
代码定位(行号按符号):
1. `frontend/src/lib/rectification-agentic/core/rectification-decision.ts` `decideRectification`:分支 `stopClass.kind === "exhausted" && stopClass.reason === "tied_first" && !probe && input.targetedCollectExhausted !== false` 直接 `deliverRange(..., "exhausted")`。**没有任何宽度或领先度条件**。`classifyStop``tied_first` 来自 `candidate-separation.ts``tiedForFirst = top.score === runnerUp.score``stillNeedNarrowing` 只看 `refreshExhausted` / `targetedCollectExhausted`
2. `core/credible-range.ts` `unionStillValidRange`:区间 = 落后头名不足 `MIN_SEPARATION_LEAD = 8` 分的簇并集。区间宽度从未参与出卡判断。
3. `v9/collection-question-pool.ts` `targetedDomainClosed` / `remainingTargetedDomains``declined``skipped` 同样永久关闭该线;`targetedCollectExhausted` 只看这个池是否为空。池空 → `rangeNarrowHint` 落到 `user-copy.ts` `rangeDeliveryCollectClosed`(自由文本邀请)。
4. `v9/answer-choice.ts` `isTargetedCollectExistenceSchema` 分支:B「没有发生过」→ `declined`;C「记不太清楚」/ D「这条先跳过」→ `skipped``choice-card.ts` 给 D 的 answer class 是 `weak_yes`)。`targetedCollectExistenceAck``declined` 回「记下了,这方面先跳过」,`skipped` 回「记下了,这题先放着」。**文案与语义对调**:答「没有发生过」的用户看到的是「先跳过」。这次真机三条线的回执都是「这方面先跳过」,按代码等于用户点的是 B;若用户点的其实是 D,则是落库错位,执行方按 T6 核实。
5. `scripts/rectification/event_probes.py` `_boundary_windows` / `_union_boundary_dates`:候选间大运边界差 `< MIN_BOUNDARY_DAYS = 45` 天不出题。窗口收到 20 分钟时边界位移约 22 天,**自动点选题池必然为空**,这是「问完了」的算术原因(`docs/tasks/PROGRESS-rectification-precision-gate-research-20260914.md` 已量过:放宽这个闸门对自动是非题无收益,本单不动它)。
6. 未定性观察:性格题(只做 ±1 排序微调、不淘汰)答完后,范围从 20 分钟弹到 28 分钟,下一题又缩回。疑似某簇卡在 `lead = 8` 边界,被 ±1 推过线。见 T6。
7. **时间点题答「没发生」之后整个领域被关掉**(产品负责人补充的实证):先问「某年某月前后,有没有开始一段认真关系、分手或结婚」,用户答「明确没有发生」(那个时间点确实没有);随后定向线问的是 `TARGETED_EXISTENCE_PROMPT.relationship = "结过婚或订过婚吗?"`,用户如实答「没有发生过」→ `declined` → 该领域永久关闭。用户在**别的时间**有过认真关系,从头到尾没被问到。同一张表里其余六条线同样是「领域名 + 两个例子」缩成一句是非题:`family` 只问「添丁或长辈住院」、`finance` 只问「收入变过或大笔进出」,答「没有」就等于整个领域没有。另外 `remainingTargetedDomains(layers, …)` 只从 `remainingLayers`(分盘上还能切开候选的层)映射领域,某层切不开候选时,那条线根本不问;但事件按大运时点计分与分盘层无关,这条限制没有依据。
## 2. 根因
- 出口条件里没有精度项:七条线关闭 + 自动题池空 + 头名并列 = 出卡,宽度多少都出。
- 自动题池空之后没有第二个**有方向**的题源;剩下的只有一句让用户自由发挥的邀请。
- 「跳过」和「没有发生过」在数据层同义,用户想先放一放的线再也回不来。
- 定向线的是非题只覆盖领域里的一两个例子,却按整个领域记「没有」;时间点题的「没发生」也没有接一句「那别的时间有没有」。领域被关掉的不是用户的经历,是题干。
研究定论(`docs/research/rectification_minute_resolution_closure_2026_09_14.md`):调打分拉不开区间内并列,**唯一被证明有效的信息源是新的带年月经历**。所以本单只改出口条件和采集方式,不改打分。
## 3. 决策记录(产品负责人 2026-09-16 拍板)
| # | 决策 | 推翻 / 修改的既有口径 |
| --- | --- | --- |
| D1 | **出卡门槛**:交付区间宽度 ≤ 10 分钟,且头名比第二名的相对可能性至少多 3 个百分点(`RANGE_DELIVERY_TIE_PERCENT`)、`tiedForFirst = false`。三项同时满足才出交付卡。 | 现行「七条线关 + 题池空即出卡」 |
| D2 | **用户确实补不出来时**(明确说「没有了 / 就这些」,或引导题源也空了):**按现在的规则出卡**,卡片、采用按钮、三句正文都不变。不加「未达门槛」标注。 | 保留 09-10「永远给结果」 |
| D3 | **引导式补经历**:门槛未达时,系统点名逐题问,一次只问一件,题目指定「哪几年几月之间、哪一类事」。用户用**类型选项 + 年 / 月选择器(日可选)**录入,不再出现自由文本邀请句。输入框不禁用(BUG-551),用户打字仍然照记,但系统不再主动邀请打字。 | BUG-689 的自由文本邀请(卡上那句删除) |
| D4 | **跳过的线再问一次**:答「记不太清楚」或「这条先跳过」的线,在引导阶段换一种更具体的问法再问一次;再次跳过才永久关闭。答「没有发生过」的线不再问。 | Skill §流程「用户已拒绝或跳过的目标不得换词重问」改为只限「拒绝 / 没有发生过」;BUG-687 防复发条改为「不得再列已答没有发生过的线」 |
| D5 | **时间点的「没发生」不等于领域没有**(产品负责人会话中补充):某年某月的是非题答「明确没有发生」只关闭那个时间点,领域保持开放;领域的存在性问题必须问整个领域(任何时间、该领域全部事件类型),「没有发生过」的选项文案要写成「这类事都没有过」。**规则对七条线用同一张表驱动,不得因为这次是感情线就只改感情线,也不得给任何单个领域写死特殊流程。** | 现行「时间点题 + 窄题干答没有 = 领域关闭」;`remainingTargetedDomains` 只问分盘层还能切开候选的领域 |
不改的:打分尺度、`MIN_SEPARATION_LEAD = 8`、自动是非题的 45 天闸门、`MIN_ACCEPTANCE_DOMAINS = 2`、记录优先(BUG-691)、性格题 ±1 只排序不淘汰、「不用生日推年份」。
## 4. 硬红线
1. 不得为了让门槛更容易达到而改打分、改 lead、改簇合并;门槛只是出口条件。
2. 引导题的年份、月份区间必须来自引擎的候选边界,不得由模型或前端发明;不得用生日推年份(BUG-648)。
3. 引导问法不得列已答「没有发生过」的线(BUG-687 改后的口径);跳过的线最多再问一次。
4. 引导录入的证据必须走现有带年月证据路径(同 `batch` / `set-focus` 落库、改账本指纹、快照过期重算),不得另开一条评分旁路。
5. 用户说「没有了 / 就这些」立即出卡,不得追问第二遍(D2)。
6. 不得在同一回复里一边要引导题一边给采用按钮(Skill 既有红线)。
7. `page.tsx` 不增长;新组件进 `frontend/src/components/`,新逻辑进 `lib/rectification-agentic/`
8. Skill bump 后必须验证历史校正会话仍能打开(BUG-621 教训)。
9. 不得把研究里的离线命中率写成线上承诺;用户文案不得承诺「再补几件就能定到分钟」。
10. **七条线一张表**:题干、例子、重问句、录入芯片都从 `CollectKind` 的表里取;决策逻辑里不得出现 `if (domain === "relationship")` 这类按领域分叉。时间点题的负答案对任何领域都不关闭领域。
## 5. 任务分解
### T1 出卡门槛(前端决策层 + 策略常量)
- `references/rectification_policy.v1.json``deliveryMaxWidthMinutes: 10``scripts/rectification_policy.py` 与 TS 侧各读一次,常量只定义在这一个文件里。
- `decideFromDossier` 计算 `precisionGateMet``credible_range` 宽度 ≤ 10 且交付列前两名 `probability_percent` 差 > 3 且 `!separation.tiedForFirst`
- `decideRectification``tied_first` 耗尽分支、`!separation.sufficient` 的耗尽 / offer 分支、`stopClass.kind === "exhausted"` 分支,都加同一条判断:`precisionGateMet || input.userStopped || input.guidedCollectExhausted``deliverRange`;否则 `collect(...)` 并带上 T3 的引导题。`user_uncertainty_too_high` 分支保持原样(那是用户不配合的止损)。
- `withholdAdoptOnUnnarrowedRange``waitToNarrowCapability` 不动。
- 验收:
- 单测:20 分钟 / 5 候选并列 → `collect` + 引导题,`canOfferRange = false`8 分钟 / 前两名差 5 → `deliverRange`8 分钟并列 → `collect`;门槛未达但 `guidedCollectExhausted = true` → 现行交付卡(含采用);`userStopped` → 现行交付卡。
- 既有测试改断言的,写「原值 / 新值 / 原因」三栏。
### T2 引导题源(后端,`scripts/rectification/event_probes.py`,新函数不改旧函数)
- 新增 `guided_collect_windows(request, built, *, candidate_times, today)`:复用 `_representative_pairs` + `_union_boundary_dates`,参数 `min_boundary_days = 0``include_pratyantar = True``varga_narayana = True`,得到剩余候选之间大运 / 中运 / 小运边界不一致的日期。每个窗口输出 `{year, month_lo, month_hi, domain, split: {left: n, right: n}}`
- `month_lo` / `month_hi` 为该边界在所有剩余候选上的最早与最晚月份(跨年拆两条);
- `domain``_probe_domains` 的层序取该层对应领域,且剔除用户已答「没有发生过」的领域(请求里带 `declined_domains`);
- `split` 是把剩余候选按边界前 / 后分成两组的人数,**按两组越接近一半越靠前**排序,其次按年份靠近今天;上限 6 条。
- 挂进 `refinement_packet.py` / `decision_policy.py` / `api_service.py` 的 packet,键名 `guided_collect_windows`,与 `discriminating_event_probes` 并列;自动题池非空时该列表可以为空(自动题优先)。
- 不改 `MIN_BOUNDARY_DAYS`、不改 `_discriminating_event_probe_lists`
- 验收:
- `tests/test_event_probes_guided_windows.py`(新):golden 用真实引擎输出(不得手造);20 分钟窗至少产出 1 条窗口;`declined_domains` 被剔除;排序按 split 均衡;跨年拆分正确。
-`references/real_case_calibration/minute_rectification_holdout_v4.json` 离线回放:每例在六题后按 `guided_collect_windows` 顺序注入真值方向的带年月事件,记录**达到 D1 门槛所需件数**的中位数与 20 例中达标数(三档半径)。这组数字进进度记录,**不是合入门槛**,但必须报出来,产品据此决定门槛是否要调。
- `.venv/bin/python -m pytest tests/test_event_probes*.py tests/test_candidate_discriminator_contract.py tests/test_rectification_refresh_r3_r4.py` 全绿;快速门 quick 与基线逐条一致。
### T3 引导问法与选择器录入(前端)
- `collection-question-pool.ts` 新题型 `guided`,排在自动题之后、定向七条线之前:
1. **边界窗口题**(来自 T2):题干「YYYY 年 M 到 M 月之间,有没有<领域口语>?」,领域口语沿用 `USER_COLLECT_QUESTION` 的短语,不得复述服务器标签。选项:A「有,我来填时间」/ B「这段没有」/ C「记不清」。B、C 只关闭这一条窗口,不关闭领域。
2. **跳过线重问**T4)。
3. **未覆盖领域的开放时间题**(含时间点题答「没发生」后的领域,T4b):七条线里尚无带年月证据、也没答过「这类事都没有过」的领域,问「<领域全称问法>,哪一年都算」,选项同上。
- A 之后出**录入卡**:类型芯片(沿用七条线的 `CollectKind`,默认选中题目的领域,可改)+ 年 / 月选择器(日可选)。新组件 `frontend/src/components/event-date-picker.tsx`,复用 `birth-date-picker.tsx``Calendar` / `Popover`,但以月为粒度、日可留空;年份范围出生年到今年。提交 → 走现有带年月证据落库路径(与 `TARGETED_YEAR_PROMPT` 答案落库同一条:`collect_kind` + 年月)→ 改账本指纹 → 快照过期 → 重算。
- `rangeNarrowHint`:门槛未达时**不再拼** `rangeDeliveryCollectClosed`;卡下提示改为「现在还剩 HH:MM–HH:MM 里 N 个候选,再对照几件经历会更准」。门槛已达或 D2 出卡时,卡片与现在完全一样(`rangeDeliveryShowsOpenCollectInvite` 那条自由文本邀请一并删除,`RANGE_DELIVERY_OPEN_COLLECT_*` 常量删掉,不留死代码)。
- 用户说「没有了 / 就这些 / 想不起来了」:由现有分类器判为 `userStopped`(不靠正则,BUG-641~643 口径)→ D2 出卡。
- 常驻区间条(`ca020498` 加的代表分钟与已答题数)加一段「已对照 N 件」即可,不加进度条、不写「还差几件」(`COLLECT_FLOW_BANNED_PHRASES` 照旧)。
- 验收:
- `tsc --noEmit` 0 错;`npm run lint` 0 error`npm test` fail 数与基线逐条一致(无 Docker 的 27 条照旧)。
- 新测试:引导题排序(自动题 > 边界窗口 > 跳过重问 > 未覆盖领域);A 后出录入卡;录入卡提交后证据落库、指纹变化、`snapshotCurrent = false`;B / C 只关窗口不关领域;门槛未达时 hint 不含邀请句、不含 `RANGE_DELIVERY_OPEN_COLLECT_*` 任何例子。
- 合同测试 fixture 来自真实引擎响应(golden)。
- `next build``/` 仍 Static;首屏 gzip ±2%(选择器组件按需加载,不进首屏)。
- `frontend/DESIGN.md` 增录入卡与选择器条目;文案对照 `frontend/docs/VOICE.md`
### T4 跳过的线再问一次
- `targetedDomainClosed``declined` 永久关闭;`skipped` 且尚无 `retry` 标记的线**不算关闭**,进 T3 第 2 类题源,题干用 `TARGETED_EXISTENCE_PROMPT_RETRY[domain]`(新表,每条比第一次更具体,例如家人线改问「父母或祖辈有没有住过院、做过手术,或者家里添过小孩」);重问后再答 C / D → 写 `retry: true` 并永久关闭。
- 修文案:B「没有发生过」回「记下了,这条按没有发生过记」;C / D 回「记下了,这题先放着,后面换个问法再问一次」。两句先过 VOICE。
### T4b 领域存在性问题问整个领域(D5,七条线同一张表)
- `TARGETED_EXISTENCE_PROMPT` 重写为「领域 + 全部事件类型 + 任何时间」的问法,例如感情线「感情上有没有过开始一段认真关系、分手、订婚或结婚,哪一年都算?」;七条线都按这个格式改,表驱动,不许某条线特殊。B 选项文案改为「这类事都没有过」,`TARGETED_COLLECT_OPTION_B` 一处改全局生效。
- **时间点题(`source = event_probe` 的年月是非题)答「明确没有发生」不得写入任何会让 `targetedDomainClosed` / `declinedKinds` / `coveredCollectKinds` 判定领域关闭的状态**;它只是那一个探针的答案。若该领域尚无带年月证据,引导阶段紧接着出该领域的开放时间题(T3 第 3 类),最多一次。
- `remainingTargetedDomains` 不再以 `remainingLayers` 过滤:七个 `CollectKind``COLLECT_KIND_ORDER` 全部轮到,只剔除 `declined` 与已覆盖的。`remainingLayers` 仍可用于排序(能切开候选的层先问),不能用于剔除。
- 验收:单测「时间点题答 C → 领域仍在引导池」「窄题干旧文案不再出现(`agent-voice-copy-contract` 断言七条新题干)」「某层切不开候选时该领域仍被问到」;测试用表遍历七个领域,不许只测感情线。
- Skill 与 BUG-687 防复发条按 §3 D4 改写。
- 验收:单测覆盖「skipped 一次 → 重问 → 再 skipped → 关闭」「declined → 不重问」;`agent-voice-copy-contract` 跟上;`docs/BUG_HISTORY.md` BUG-687 条目补「2026-09-16 产品修改防复发口径」并链接本单。
### T5 Skill 10.0.27
- `skills/jyotish-birth-time-rectification/SKILL.md`
- §流程那段(「带年月池空时先按剩余候选刷新一批…所有线问完或用户说没有了后才交付」)改为:自动题池空 → 按 `guided_collect_windows` 逐条问 → 跳过线重问 → 未覆盖领域;**出卡须 `precision_gate_met = true` 或用户说没有了**;引导题用选项 + 选择器录入,不得邀请自由打字。
- 「已拒绝或跳过的目标不得换词重问」改为「已拒绝(没有发生过)的不得重问;跳过的按服务器计划最多重问一次」。
- 出牌三句正文不变。
- bump `version: 10.0.27`,复制到 `versions/10.0.27/`,更新 `skills/skill-package-registry.json``CHANGELOG.md` 写明 bump。
- 验收:Skill 合同测试绿;**历史校正会话能打开**(`open` RPC 与版本绑定的既有测试 + 手工清单一条)。
### T6 顺带核实(不阻塞 T1~T5 合入)
- **回执文案对调**(§1 第 4 条):确认 B / C / D 三个选项各自落什么状态、回什么话;若 D 落成 `declined`,是 BUG;若只是文案对调,随 T4 修并记 BUG。
- **性格题后范围回弹**(§1 第 6 条):查 `varga_style` 的 ±1 是否写进 `posterior_score` 并进入 `unionStillValidRange`。若是,区间并集改用不含风格微调的分数(风格题只影响排序,本就不该改范围)。能复现写 `resolved` 并附回归测试;不能复现写 `investigating`,附已排除的路径。
- 两条各占一个 BUG 号。
### T7 记录
- `docs/BUG_HISTORY.md`:出卡无精度门槛(1 条)、自由文本邀请无方向(可并入前一条)、跳过与拒绝同义(1 条)、T6 两条。编号从 **BUG-740** 起,开工时核对最大号。
- `CHANGELOG.md``frontend/DESIGN.md``frontend/docs/VOICE.md`(新句)、`docs/testing/rectification-guided-collect-20260916.md`(真机清单:门槛未达不出卡、引导题带年月区间、选择器录入后范围变化、说「没有了」出卡、跳过线重问一次、历史会话可开)。
- `docs/tasks/README.md` 状态板那一行随合入一起改。
## 6. 让步顺序
1. T2 的 `include_pratyantar = True` 若单次重算超过 `JYOTISH_HEAVY_COMPUTE` 现有预算(进度记录里给毫秒数),先只用大运 + 中运边界,小运留 BLOCKED。
2. T3 选择器若让首屏 gzip 超 ±2% 且按需加载解决不了,退成「系统点名的题 + 年月文本框(只收 YYYY-MM)」,仍然不出自由文本邀请。
3. T2 的离线回放若超过 3 小时,只跑 ±10 与 ±30 两档,±60 写未测。
4. T6 可留 `investigating`,不得为了收口编根因。
5. 不得让步的:D1 三个条件、D2 立即出卡、D4 跳过只重问一次、Skill bump。
## 7. 开工前置命令
```bash
git fetch origin --prune
git status -sb # 确认不在别人的分支上
git worktree add -b codex/rectification-precision-gate-guided-collect-20260916 \
.worktrees/rectification-precision-gate-guided-collect-20260916 origin/staging
grep -o "^## BUG-[0-9]*" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1 # 应为 BUG-739
grep -n "分不开\|跳过" skills/jyotish-birth-time-rectification/SKILL.md | head
cd frontend && ./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -5 # 记基线失败清单
cd .. && .venv/bin/python -m pytest tests/test_event_probes*.py tests/test_candidate_discriminator_contract.py -q
```
Bug 检索(§5 硬约束):`BUG-687``BUG-689``BUG-646``648``BUG-651``BUG-653``BUG-654``BUG-621``BUG-641``643``BUG-594``BUG-623`,全部读完再动手。
## 8. 验收口径(Claude 事后逐条)
- T1~T5 每条验收标准通过 / 未通过 / 环境缺口三选一。
- 前端:tsc 0 错、lint 0 error、测试失败清单与基线逐条一致、`/` Static、gzip ±2%。
- Python:定向测试全绿、quick 门与基线一致。
- 部署:`/api/health``deployment.gitCommit` = 本单最后一次含门禁路径的 staging 提交;staging 若需迁移先 Migrate。
- 真机清单交产品负责人。
## 9. BUG 编号起点
**BUG-740**(基线最大 BUG-739)。开工时重新核对。