fix(rectification): keep hour-window tail clusters and lock the search window (BUG-623, BUG-624, BUG-625)
Independent Staging Quality Gate / validate (push) Successful in 17m15s
Independent Staging Quality Gate / publish (push) Successful in 2m15s

Hour windows no longer drop later signature clusters. Credible range uses cluster coverage, and a mid-session spoken birth window gets a fixed reply without calling the model.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-09 20:45:11 +08:00
co-authored by Cursor
parent a31a5e2426
commit 6008c07c81
41 changed files with 1164 additions and 44 deletions
+48
View File
@@ -9667,3 +9667,51 @@
- 相关记录:BUG-614、BUG-615、BUG-620
- 复发自:无
- 修复版本:待发布
## BUG-623 | 一小时窗口按时间取前 12 簇,后段真实出生时间从未进入候选
- 状态:resolved
- 首次发现:2026-09-09
- 最近更新:2026-09-09
- 影响面:`scripts/rectification/candidate_contrast.py` `select_signature_representatives``MAX_PUBLIC_CLUSTERS`、公开候选集
- 用户现象:intake 填一小时窗后,第一次比较给出的范围停在窗口前段(例如 14:00–15:00 只收到约 14:04–14:43)。用户知道的更晚时段从头到尾不在候选里。
- 触发条件:分钟网格上签名簇超过 12 个。半小时窗(±15)通常 ≤12,所以以前测不到;「前后半小时」的 60 分钟窗必现。
- 根因:`cluster_contexts_by_signature` 按代表分钟时间排序后,循环里 `len(representatives) >= MAX_PUBLIC_CLUSTERS` 直接 `break`。多出的整簇被丢掉,而且永远是窗口尾部。这发生在证据评分之前。
- 修复:不再按时间截断。安全上限改为 64;超过 64 时合并相邻低分簇,不丢尾部。公开候选仍是每簇一个代表分钟。
- 验证:`tests/test_rectification_v5_services.py` 的 17 簇夹具(含 14:46–14:51)与 65 簇相邻合并保尾。
- 防复发:禁止再对时间排序后的簇做 `break` 截断;超上限只能按分数合并相邻簇。
- 相关记录:BUG-560(以前「分钟级≈随机」的校准是在 ±15 窗口上得出,一小时窗还叠加了本截断)
- 复发自:无
- 修复版本:待发布
## BUG-624 | 可信区间按代表分钟跨度算,簇内其余分钟被视觉淘汰
- 状态:resolved
- 首次发现:2026-09-09
- 最近更新:2026-09-09
- 影响面:`build_candidate_decisions`、推断层 `cluster_range``credible-range.ts`
- 用户现象:即便某簇还活着,区间右端只写到代表分钟。例如 14:40–14:45 这一簇的代表是 14:40 时,读数停在 14:40。
- 触发条件:签名簇覆盖连续多分钟,代表分钟不是簇的末端。
- 根因:引擎候选只带 `time`。推断层用换升时刻在代表分钟之间重新聚类,多数变成单分钟;`unionStillValidRange` 再取这些点的跨度。
- 修复:每个公开候选带 `cluster_times` / `cluster_start` / `cluster_end`。推断层优先用引擎簇覆盖;换升聚类只作缺字段时的兜底。可信区间是仍有效簇覆盖的并集。时间轴实心/空心点仍画代表分钟。
- 验证:Python 簇 14:4014:45、代表 14:40 → `cluster_end=14:45``frontend/tests/rectification-window-cluster-cap-20260909.test.ts` 区间右端 14:45。
- 防复发:可信区间不得只用代表分钟;缺 `cluster_times` 才允许换升兜底。
- 相关记录:BUG-623
- 复发自:无
- 修复版本:待发布
## BUG-625 | 中途说出出生时段时助手口头答应改窗口,但什么都没改
- 状态:resolved
- 首次发现:2026-09-09
- 最近更新:2026-09-09
- 影响面:`frontend/src/app/api/rectification/agent/route.ts` 自由文本、`declared-window-utterance.ts`、Skill 10.0.20、`agent-run` 正文守卫
- 用户现象:校正做到一半,用户说「我的出生时间是 14 点 45 到 14 点 50」。助手回答「明白了,以你说的为准」,搜索窗口和候选集不变。下一句「继续吧」被当成没听懂的点选题回答。
- 触发条件:点选或采集焦点下,用户用自由文本申报钟点范围或「HH:MM 左右」。
- 根因:15 个工具里没有改搜索窗口的入口;opening brief 还禁止改窗口。自由文本进意图分类器,枚举没有「申报时间段」,落 `unclear`。模型在采集焦点下会自己答应。产品否决了中途改窗口。
- 修复:自由文本在分类器和选模型之前做确定性解析。命中则落库固定回复,不调模型,不改 `candidate_range`,当前焦点保持。正文守卫删除「以你说的…为准」。Skill 写明不得口头改窗口。intake 自定义更小范围等产品答复,本单不做。
- 验证:`frontend/tests/rectification-window-cluster-cap-20260909.test.ts`(解析、路由在选模型前拦截、簇覆盖区间、禁用口头承认)。
- 防复发:中途申报时段不得进模型、不得改窗口;助手不得写「以你说的为准」。
- 相关记录:BUG-623、BUG-572
- 复发自:无
- 修复版本:待发布
@@ -0,0 +1,42 @@
# PROGRESS · 窗口簇上限(2026-09-09
工作树:`.worktrees/rectification-window-cluster-cap-20260909`
分支:`codex/rectification-window-cluster-cap-20260909`
任务书:`docs/tasks/TASK-rectification-window-cluster-cap-20260909.md`
基线:开工时 `15cede97`;推 staging 前接到 `origin/staging` @ `a31a5e24`BUG-621
## 开工回执
- 目标:BUG-623 去掉按时间截断;BUG-624 簇覆盖进可信区间;BUG-625 中途说出时段固定回复、不改窗口、不进模型。
- 顺序:4.1 → 4.2 → 4.3 → 4.4 → 4.6。4.5 intake 自定义范围等产品,本单不做。
- 最大风险:一小时窗 17 簇依赖签名而不是分数;前端若不把 `cluster_times` 写进快照,区间仍会缩成代表分钟。
## 实测簇数(虚构盘 1997-08-08`minute_step=1`,与任务书 §1 同盘)
| 窗口 | 分钟数 | 签名簇 | 公开候选 | 覆盖 |
| --- | --- | --- | --- | --- |
| 14:0015:00 | 61 | **17** | 17 | 14:00 … 15:00 |
| 14:3015:00 | 31 | 9 | 9 | 14:30 … 15:00 |
| 12:0016:0014:00 ±120 | 241 | 64 | 64 | 12:00 … 16:00 |
| 12:4716:4714:47 ±120 | 241 | **66** | **64**(相邻低分合并) | 12:47 … 16:47 |
`MAX_PUBLIC_CLUSTERS = 64` 对 ±120 够用:66 簇会合并相邻低分,窗口两端仍在。没有按时间 `break`
## 已完成
- **4.1 / BUG-623**`select_signature_representatives` 不再按时间截断;超 64 走 `cap_clusters_by_adjacent_merge`。夹具:17 簇含 14:46–14:51;65 簇合并后仍覆盖 `01:04`
- **4.2 / BUG-624**:每个公开候选带 `cluster_times` / `cluster_start` / `cluster_end`,经引擎 → 快照 JSONB → 推断层;`engineClusterRange` 优先于换升聚类。簇 14:40–14:45、代表 14:40 → 区间右端 14:45。无新迁移。
- **4.3 / BUG-625**`parseDeclaredBirthWindow` 在选模型之前拦截自由文本。命中则 `persistV9DeterministicTurn` 固定回复,不调模型、不改窗口、不换焦点。不含钟点的句子不命中。
- **4.4**Skill **10.0.20**live + `versions/10.0.20`sha256 `9c09867591cc9e8726f3577346230339b5a6dd4b4f3d634a9cda3e66170d6ac6`)。§4 禁止「以你说的为准」。词表删除该短语;只剩标点时回落到固定回复。
- **4.5**:不做。
- **4.6**BUG-623 / 624 / 625 已写入 `docs/BUG_HISTORY.md``CHANGELOG.md`;走查 `docs/testing/rectification-window-cluster-cap-20260909.md`(固定回复,不是确认卡)。
固定回复原文:「搜索范围是开始时按你的资料定的,校正过程中不改。想按别的时间段重来,请先到资料里改出生时间,再新建一次校正。」
## 验收命令
- `.venv/bin/python -m pytest tests/test_rectification_v5_services.py -q` → 全过
- `frontend``tsc --noEmit` 过;`npm run lint` 0 error(既有 warning 未动)
- `tsx --test` 任务书所列 `rectification-*.test.ts` + `birth-time-intake*.test.ts`(排除 database)→ **1073 pass / 0 fail**
本单状态:**待验收**。推 `origin HEAD:staging`
+2 -2
View File
@@ -88,8 +88,8 @@
| `TASK-rectification-conversation-economy-20260909.md` | `PROGRESS-rectification-conversation-economy-20260909.md` | 对照竞品后产品拍板三条:开场三句讲做法 + 一次收多件(推翻 opening brief「不要一次说完/不举例」与 SKILL L52);采集题「没有 / 记不清」按钮(不做示例骨架条);每轮只留一句(方法句进活动记录、点选旁白去领先落后、证据轮正文一句复述 + 服务端裁剪);Skill 10.0.19 | 已验收通过(2 条 P3 备注) | `aa7ccb30` + `1453fb16`BUG-604606Skill 10.0.19 |
| `TASK-consultation-daily-empty-answer-20260909.md` | `PROGRESS-consultation-daily-empty-answer-20260909.md` | 首页「深入看今日」计算完成却 `empty_answer`:入口定的 `timing` 被模型改成 `general``canonicalDomainPlan` 以模型为准),分段写作标题与今日格式错位,分段 `maxSteps=1` 且工具仍可调 → 模型在分段里再调工具、零正文;思考流按 chunk 过滤漏出缺词英文 | 待验收 | `codex/consultation-daily-empty-answer-20260909`BUG-612613);走查 `docs/testing/consultation-daily-empty-answer-20260909.md` |
| `TASK-rectification-compare-card-polish-20260909.md` | `PROGRESS-rectification-compare-card-polish-20260909.md` | 三列卡两列经历对照 0、三列无未来窗:引擎按引擎前三算 by_time,卡片按推断前三查表,键不匹配(BUG-614);交付旁白被 BUG-606 裁成一句(BUG-615);设计减法:相同性格句只写一次、经历对照说人话、删小标题、右栏本命宫位改北印星盘、换升时刻默认折叠 | 待验收 | `codex/rectification-compare-card-polish-20260909`BUG-614615 |
| `TASK-rectification-history-open-20260909.md` | `PROGRESS-rectification-history-open-20260909.md` | 历史生时校正打不开:`open_agentic_rectification_case_v2` 把 Case 绑定 Skill 身份与当前注册版本做相等比较,两天内 10.0.15→10.0.19 五次 bump 让所有旧 Case 被拒;错误码未映射成 500;错误只画在首页卡下 → 表现为点了没反应。决策:session 打开用绑定身份(v1 取 Case → 读身份 → 注册表核验 → v2),不改迁移 | 待验收 | `codex/rectification-history-open-20260909`BUG-621 |
| `TASK-rectification-window-cluster-cap-20260909.md` | `PROGRESS-rectification-window-cluster-cap-20260909.md` | **P0** 真实用户:一小时窗口分 17 个签名簇,`select_signature_representatives` 按时间取前 12 个,14:40 之后整簇丢弃(本机复现表);区间按代表分钟跨度而非簇覆盖;中途说出真实时段时助手口头答应却未改 → 改为固定回复"范围开始时按资料定、中途不改"(产品否决口头改范围);intake 自定义范围等产品答复 | 待执行 | `codex/rectification-window-cluster-cap-20260909`BUG-623625Skill 10.0.20 |
| `TASK-rectification-history-open-20260909.md` | `PROGRESS-rectification-history-open-20260909.md` | 历史生时校正打不开:`open_agentic_rectification_case_v2` 把 Case 绑定 Skill 身份与当前注册版本做相等比较,两天内 10.0.15→10.0.19 五次 bump 让所有旧 Case 被拒;错误码未映射成 500;错误只画在首页卡下 → 表现为点了没反应。决策:session 打开用绑定身份(v1 取 Case → 读身份 → 注册表核验 → v2),不改迁移 | 待验收 | `a31a5e24`BUG-621 |
| `TASK-rectification-window-cluster-cap-20260909.md` | `PROGRESS-rectification-window-cluster-cap-20260909.md` | **P0** 真实用户:一小时窗口分 17 个签名簇,`select_signature_representatives` 按时间取前 12 个,14:40 之后整簇丢弃(本机复现表);区间按代表分钟跨度而非簇覆盖;中途说出真实时段时助手口头答应却未改 → 改为固定回复"范围开始时按资料定、中途不改"(产品否决口头改范围);intake 自定义范围等产品答复 | 待验收 | `codex/rectification-window-cluster-cap-20260909`BUG-623625Skill 10.0.20;走查 `docs/testing/rectification-window-cluster-cap-20260909.md` |
### 聊天主链路与首页
@@ -0,0 +1,29 @@
# 生时校正窗口簇上限 · 走查清单(2026-09-09
分支 `codex/rectification-window-cluster-cap-20260909`,任务书 `TASK-rectification-window-cluster-cap-20260909.md`。不要把真实案例里的姓名、地点或精确出生资料写进记录。
自动化已锁:`tests/test_rectification_v5_services.py`(17 簇夹具、簇覆盖、超 64 合并保尾)、`frontend/tests/rectification-window-cluster-cap-20260909.test.ts`(解析、路由不进模型、区间右端、禁用口头承认)。
## 1. ±30 窗口候选必须覆盖整窗
用虚构盘,家人档选「差不多准」(±15,半小时窗)和新开一次「前后半小时」(一小时窗)。
- [ ] 半小时窗:比较后的候选范围左端不晚于窗口左端、右端不早于窗口右端(允许因证据收到中间,但不得在第一次比较前就丢掉尾巴)。
- [ ] 一小时窗:第一次比较不得只收到窗口前 2/3。若窗口是 14:00–15:00,14:46 之后必须仍可能成为候选。
## 2. 中途说出时段必须固定回复,不得改窗口
点选题还挂着时输入「我的出生时间是 14 点 45 到 14 点 50」。采集题挂着时再试一次「14:47 左右」。
- [ ] 立刻出现固定回复:搜索范围开始时按资料定、过程中不改;要换时间段先改资料再新建校正。
- [ ] 顶部搜索窗口不变。
- [ ] 原来的点选题或采集题还在,没有被换成别的题。
- [ ] 这一句没有调用模型(网络面板不应出现对话补全;助手气泡应瞬间出现)。
- [ ] 不含钟点的普通句子(「继续吧」「没有」)不走这句固定回复。
## 3. 助手不得口头答应改时间
若模型仍被问到出生时段(回归:关掉拦截再试,或看历史事故会话)。
- [ ] 不得出现「明白了,以你说的为准」或改写搜索窗口。
- [ ] Skill 10.0.20 写明不得口头承认。