Files
Jyotisha/docs/tasks/TASK-rectification-window-cluster-cap-20260909.md
T

81 lines
9.4 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 · 一小时窗口后三分之一被 12 簇上限静默丢掉,真实出生时间从一开始就不在候选里;用户中途说出更窄的时间段无人接(2026-09-09)
- 基线:`origin/staging` @ `c41afe26`
- 分支:`codex/rectification-window-cluster-cap-20260909`,基于 `origin/staging`
- 执行方:coding agent;验收:Claude
- 涉及文件:`scripts/rectification/candidate_contrast.py``MAX_PUBLIC_CLUSTERS``select_signature_representatives`)、`scripts/rectification/decision_policy.py``build_candidate_decisions` 候选字段)、`scripts/rectification/api_service.py``frontend/src/lib/rectification-agentic/v9/inference-adapter.ts` / `core/build-state.ts` / `core/credible-range.ts`(簇范围)、`frontend/src/app/api/rectification/agent/route.ts`(自由文本分支)、`frontend/src/lib/rectification-agentic/v9/turn-intent-classifier.ts``v9/answer-choice.ts`(新卡片)、`v9/tool-service.ts`、新迁移 `frontend/supabase/migrations/2026091001xxxx_rectification_set_case_window.sql``frontend/src/lib/birth-time-intake-model.ts` + `components/birth-time-intake.tsx`(自定义范围)、`skills/jyotish-birth-time-rectification/SKILL.md`(不得口头承认改窗口)
- BUG 编号起点:**BUG-623**`docs/BUG_HISTORY.md` 当前最大 BUG-622
- 优先级:**P0**(真实用户:intake 填 14:0015:00,本人知道是 14:4514:50,系统收到 14:04–14:43 并继续出题;用户说出真实时段后助手口头答应却什么都没改)
## 1. 事故实证(2026-09-09 真实用户转录 + 本机引擎复现;不写用户资料)
| 转录 | 事实 |
| --- | --- |
| 第一轮证据写完范围就是 14:02–14:40 | 一小时窗口的第一次 compare 就只给出到 14:40 的候选 |
| 用户:"我的出生时间是 14 点 45 到 14 点 50" → 助手(1 步)"明白了,出生时间以你说的 14:45–14:50 为准" | 助手没有任何工具能改搜索窗口(`rectification-v9-tools.ts` 的 15 个工具里没有);opening brief 还明写"线索仅旁白建议,不得改搜索窗口"(`agent-run.ts` L256)。这是口头答应、实际未改 |
| "继续吧" → "我不太确定这句是不是在回答上面的问题" | 当前焦点是点选题,自由文本进 `classifyRectificationTurnIntent`,枚举里没有"申报时间段"这一意图,落 `unclear` |
| 范围仍 14:04–14:43 | 真实时段从头到尾不在候选集里 |
**本机复现**(虚构盘 1997-08-08`minute_step=1`):
| 窗口 | 分钟数 | 签名簇数 | 保留簇 | 保留到的最后一分钟 | 被丢掉的簇 |
| --- | --- | --- | --- | --- | --- |
| 14:0015:00 | 61 | **17** | 12 | **14:39** | 14:4014:45、14:4614:51、14:5214:53、14:5414:57、14:5815:00 |
| 14:3015:00 | 31 | 9 | 9 | 15:00 | 无 |
| 04:4505:15 | 31 | 9 | 9 | 05:15 | 无 |
半小时窗口刚好压在 12 以内,所以前面十几次实测(都是 ±15)从未暴露;用户一选"前后半小时"(60 分钟窗)就必然丢后段。
## 2. 根因
### 2.1 BUG-623P0):候选簇按时间排序后取前 12 个,多出的整簇丢弃
`candidate_contrast.py::select_signature_representatives``cluster_contexts_by_signature` 把簇**按代表分钟时间排序**,循环里 `if len(representatives) >= MAX_PUBLIC_CLUSTERS: break``MAX_PUBLIC_CLUSTERS = 12`)。超过 12 簇时,被丢的永远是窗口尾部。这发生在任何证据评分之前,后面所有的"范围收到 …"都只在残缺候选集内进行;旁白与时间轴把 14:04–14:43 说成收敛结果,实际上 14:40 之后从未被考虑过。
### 2.2 BUG-624(P1):可信区间按代表分钟的跨度算,不按簇的实际覆盖算
引擎候选只带 `time`(簇里分数最高的那一分钟),不带簇成员;推断层 `cluster_range` 是用换升时刻在代表分钟之间重新聚的,多数是单分钟;`credible-range.ts` 取 still-valid 候选的 `cluster_range``time` 的跨度。于是即便 14:40–14:45 这一簇活着,若代表分钟是 14:40,区间右端也只写到 14:40,簇内其余五分钟被"视觉淘汰"。
### 2.3 BUG-625(P1):中途申报更窄时段无人接,助手还口头答应
没有"申报时间段"意图;没有把窗口改窄的 RPC(`widen_agentic_rectification_case_window` 要求 `v_new_width > v_old_width`);模型在采集焦点下收到这句话就自己"答应"了。intake 的"家人记得大概时间"只能选 ±15 / ±30 / ±60 / ±120,知道 14:45–14:50 这种小范围的用户没有地方填(`customDeclaredRange` 只给 `period_only`)。
## 3. 决策记录
1. **去掉按时间截断。** `select_signature_representatives` 不再 `break``MAX_PUBLIC_CLUSTERS` 改为安全上限 64(±120 窗口 241 分钟实测簇数写进度记录);若真超过 64,按**分数**合并相邻低分簇,不丢尾部。公开候选数变多(一小时 ≈17)不影响四选项合同:探针的 `candidate_ids` 本来就是分钟集合。
2. **候选带簇成员。** `build_candidate_decisions` 每个候选加 `cluster_times: [HH:MM…]`(连续分钟段可写 `cluster_start/cluster_end`);`inference-adapter.ts` 把它作为 `cluster_range` 的来源(换升聚类只作退化兜底);`credible-range.ts` 的区间 = still-valid 簇覆盖的并集跨度。时间轴的实心/空心点仍按候选(代表分钟)画,簇范围只影响区间带。
3. **中途申报时段 → 服务端卡片,不进模型。** `route.ts` 自由文本分支前置一个确定性解析:识别 `HH:MMHH:MM`、"14 点 45 到 14 点 50"、"14:45 左右" 这类表述 → 不调分类器,直接持久化一张选择卡:题干"把搜索范围改成 14:4514:50?",选项 A「改成这个范围」/ B「保持现在的范围」(两选项,不是四选项探针,用 `choice_kind = window_declare`,走 BUG-572 放宽卡同一渲染路径)。A:新 RPC `set_agentic_rectification_case_window(p_start, p_end)`——允许收窄或放宽,但必须落在 intake 窗口内(或与之相交时取交集)、宽度 ≥3 分钟、Case 未采用未终止;使结果失效,`rescoreMinuteAfterWindowChange({keepAnswers:false})`。宽度 ≤5 时下一步直接进交付(现有门允许)。
4. **模型不得口头承认改时间。** SKILL.md §4 加一句:"用户说出出生时间或时段时,不得回答『以你说的为准』或改写搜索窗口;服务端会出确认卡。"并在 `agent-run` 的正文守卫里把"以你说的…为准"列入机器词表删除。
5. **intake 加自定义范围。** "家人记得大概时间"下方增加"我知道一个更小的范围":两个时钟选择器 → `customDeclaredRange`,宽度 3241 分钟;这条路仍是 `minute` 阶段(不是 `period_only` 的时段扫描)。
6. 不动淘汰阈值、`SCORE_DELTA`、采用/确认门、`_relative_support``minute_step`。**新迁移需要 `npm run test:db`Docker**:执行方无 Docker 时写 `BLOCKED.md`,DB 用例照写,验收时由我在有 Docker 的环境跑或标环境缺口,不得写成通过。
## 4. 任务分解
- 4.1 BUG-623:改 `select_signature_representatives``tests/test_rectification_v5_services.py` 加"14:0015:00 虚构盘 17 簇全部保留、候选含 14:46–14:51 簇的代表";用本单 §1 表格做回归夹具(虚构盘,不含真实资料)。
- 4.2 BUG-624`cluster_times` 贯通到 `credible_range`;用例——簇 14:4014:45 存活、代表 14:40 → 区间右端 14:45。
- 4.3 BUG-625:解析器 + 卡片 + RPC + 重算;用例——点选焦点下输入"我的出生时间是 14 点 45 到 14 点 50"→ 出现两选项卡、无模型调用;选 A → `candidate_range` 变 14:4514:50、旧结果 `invalidated_at` 非空、compare 一次、答题账本清空;范围超出 intake 窗口 → 取交集并在卡片题干写明;已采用 Case → 卡片不出、回执"已采用的校正不能再改范围"。
- 4.4 决策 4Skill 10.0.20(只加一句);`agent-voice-copy-contract` 收禁用短语。
- 4.5 决策 5intake 自定义范围;`birth-time-intake*.test.ts` 三栏。
- 4.6 记录:BUG-623P0,关联 BUG-560 校准记录:以前的"分钟级≈随机"结论是在 ±15 窗口上得出,一小时窗口还叠加了截断)、BUG-624、BUG-625`CHANGELOG.md``PROGRESS-…``docs/testing/` 加"±30 窗口下候选必须覆盖整窗;中途说出时段必须出确认卡;助手不得口头答应改时间"。
## 5. 让步顺序
4.1 当天必做并部署(一行改动止血);4.2、4.3 必做;4.4 随 4.34.5 可后置为 P24.6 不可省。
## 6. 给当前这位用户的临时办法(产品可直接转告)
在资料里把出生时间改成 14:47,来源选"家人记得大概时间 · 差不多准"(±15,窗口 14:3215:02,31 分钟、9 簇,不会触发截断),重新开始校正。或者选"有出生证或医院记录"填 14:47。
## 7. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-window-cluster-cap-20260909 .worktrees/rectification-window-cluster-cap-20260909 origin/staging
cd .worktrees/rectification-window-cluster-cap-20260909
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
ln -s /workspace/Jyotisha/.venv .venv
.venv/bin/python -m pytest tests/test_rectification_v5_services.py -q
cd frontend && ./node_modules/.bin/tsc --noEmit; npm run lint 2>&1 | tail -1
ls tests/rectification-*.test.ts tests/birth-time-intake*.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)"
```