fix(rectification): count timeline width inclusively and keep eliminated minutes hollow (BUG-602, BUG-603)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-09 13:58:55 +08:00
co-authored by Cursor
parent d925251654
commit 92e3d5e7a3
11 changed files with 338 additions and 25 deletions
+32
View File
@@ -9333,3 +9333,35 @@
- 相关记录:BUG-043(禁止把后台评分状态渲染成用户需要管理的面板。本处边界不同且不冲突:等待屏是只读的,展示的是用户交付物自身的章节结构,`attemptCount`、原始错误码、lease、job id、payload 一律不出服务端)、BUG-576、BUG-574
- 复发自:无
- 修复版本:待发布
## BUG-602 | 时间轴宽度按差值算,卡片与报告按含两端算
- 状态:resolved
- 首次发现:2026-09-09
- 最近更新:2026-09-09
- 影响面:`rectification-timeline-scale.ts` 区间读数 `widthLabel``frontend/DESIGN.md` §10
- 用户现象:同一屏上时间轴写的宽度比三列卡和验证报告少 1 分钟。例如区间 04:51–04:59 条上写「8 分钟」、卡上写「9 分钟」;05:07–05:09 条上写「2 分钟」;整天窗口写「23 小时 59 分」。
- 触发条件:分钟阶段已有 `credible_range`,或时段阶段轴铺满整天窗口。必现。
- 根因:`widthLabel``timelineDurationLabel(bandEnd - bandStart)`,少算两端都计入的那一分钟。BUG-593 已把交付报告与三列卡定为含两端分钟数;任务书示例「05:07–05:09 · 2 分钟」是差值口径,首版时间轴照抄了。
- 修复:改为 `timelineDurationLabel(bandEnd - bandStart + 1)`。整天 00:00–23:59 显示「24 小时」,单分钟显示「1 分钟」。DESIGN.md §10 示例改为「05:0705:09 · 3 分钟」。
- 验证:`frontend/tests/rectification-timeline-20260909.test.ts`(含两端宽度表:04:5104:59 → 9 分钟、05:0705:09 → 3 分钟、04:5104:53 → 3 分钟、单分钟 → 1 分钟、整天 → 24 小时);与 `frontend/tests/rectification-delivery-report-facts.test.ts``width_minutes === 3` 同一形状。
- 防复发:时间轴读数必须与报告/卡片同一套含两端分钟数;禁止再把 `bandEnd - bandStart` 当宽度。
- 相关记录:BUG-593
- 复发自:无
- 修复版本:待发布
## BUG-603 | 被排除的分钟到不了客户端,时间轴空心点画不出来
- 状态:resolved
- 首次发现:2026-09-09
- 最近更新:2026-09-09
- 影响面:`rectification-candidate-result.ts``rectification-timeline-scale.ts``rectification-agentic-chat.tsx``timelineView` 候选来源
- 用户现象:DESIGN.md §10 写「被排除的点留在原地变空心、不消失」;真实会话里答完一题后那些分钟从条上消失,剩下的全是实心。看不到刚才那一答排掉了哪几分钟。
- 触发条件:分钟阶段已有推断层、至少淘汰过一个候选。必现。
- 根因:`inference-adapter.ts` 投影只保留 `status !== "eliminated"` 的候选;时间轴把 `candidateResult.candidates.map(time)` 当点,再按是否落在区间带内判 in/out。到达客户端的点都在带内,全是实心;被淘汰的分钟根本不在列表里。测试里的 `["out","in","in","in","out"]` 是合成输入,不是这条数据路径。
- 修复:`RectificationCandidateResult` 增加只读 `inferenceMarks: [{time, eliminated}]`,从 `decisionReceipt.inference_state.candidates``time` / `status` 解析(组件不读原始 receipt)。`state = status === "eliminated" ? "out" : "in"`,不再按点是否落在带内判断。没有 `inference_state` 时退回引擎候选全部实心。
- 验证:`frontend/tests/rectification-timeline-20260909.test.ts`:推断层 9 个候选、7 个 eliminated、可信区间 04:5104:53 → 9 个点、7 空心 2 实心;再淘汰 1 个 → 8 空心 1 实心,key 不变。`frontend/tests/rectification-candidate-result.test.ts`:有 `inference_state` 时解析出全集,无则 `[]`。聊天源码合同:`timelineView``inferenceMarks`,不读 `decisionReceipt`
- 防复发:时间轴标记必须来自推断层候选全集;禁止再用引擎 active 投影或带内位置当 in/out。空心/实心只看 `status === "eliminated"`
- 相关记录:BUG-560、DESIGN.md §10
- 复发自:无
- 修复版本:待发布
@@ -0,0 +1,58 @@
# PROGRESS · 生时校正时间轴修复单:宽度含两端、空心点来自推断层(2026-09-09)
工作树:`.worktrees/rectification-timeline-fix-20260909`
分支:`codex/rectification-timeline-fix-20260909`
任务书:`docs/tasks/TASK-rectification-timeline-fix-20260909.md`
基线:`origin/staging` @ `d9252516`(任务书写 `724a1215`;本 worktree 按开工指令从当时 `origin/staging` 切出)
本单状态:**待验收**。未 push,未合入 staging/main,未声称生产部署。
预检:本机 `python3 scripts/pre_work_check.py` 失败(系统 Python 3.14 无 pytest、无 swissephERR-078)。不把预检失败写成门禁已过。前端用 worktree 里已有的 `frontend/node_modules` 符号链接。
未改:二元编码、只读、固定条高、grid 行、transform-only、`use-conversation-scroll-anchor.ts``frontend/src/lib/rectification-agentic/v9/*`、Skill / `references/conversation-strategy.md`、composer-wrap / collect 按钮 / vargaSentence / opening / spoken copy。Skill 版本不变。
## 做了什么
- **BUG-602** `widthLabel = timelineDurationLabel(bandEnd - bandStart + 1)`,与 BUG-593 交付报告含两端分钟数同一形状。
- **BUG-603** `RectificationCandidateResult.inferenceMarks``decisionReceipt.inference_state.candidates``time` / `status` 解析;组件只把该数组交给时间轴,不读 raw receipt。`state = eliminated ? "out" : "in"`,不再按点是否落在区间带内判断。无 `inference_state` 时引擎候选全部实心。
`inferenceMarks` 在类型上是可选的:`parseRectificationCandidateResult` **每次都写出数组**(没有推断层则为 `[]`)。若做成必填,会迫使改 `tests/rectification-range-delivery-20260907.test.ts` 里的结果字面量——该文件不在本单所有权内。
## 三栏 · widthLabel
| 用例 | 原值 | 新值 | 理由 |
| --- | --- | --- | --- |
| 05:0705:09 | `2 分钟` | `3 分钟` | 含 05:07、05:08、05:09 三分钟;与 DESIGN §10 / 报告口径一致 |
| 04:5104:59 | `8 分钟` | `9 分钟` | 含两端;与三列卡同一形状 |
| 04:5104:53 | `2 分钟` | `3 分钟` | 与 `rectification-delivery-report-facts.test.ts``width_minutes === 3` 同一形状 |
| 05:0705:07 | `0 分钟` | `1 分钟` | 单分钟窗口 |
| 00:0023:59 整天 | `23 小时 59 分` | `24 小时` | 1440 分钟;时段阶段尚未排除任何分钟 |
| 04:5605:20(放宽后) | `24 分钟` | `25 分钟` | 含两端 |
| 00:3001:30(跨午夜带) | `1 小时` | `1 小时 1 分` | 61 分钟,同一条 `+ 1` 规则 |
## 三栏 · 标记改按 status
| 用例 | 原值 | 新值 | 理由 |
| --- | --- | --- | --- |
| 合成 05:06…05:10,带 05:0705:09,无 inferenceMarks | `["out","in","in","in","out"]`(按是否落在带内) | 全部 `"in"` | 无推断层时引擎候选全部实心,决策 2 |
| 同上,05:08 在带内但 `eliminated` | 05:08 会被判 `"in"` | 05:08 `"out"` | in/out 只看 `status === "eliminated"`,不看带 |
| 推断层 9 点、7 淘汰、带 04:51–04:53,引擎投影只有 2 个 active | 条上 2 个实心点 | 9 个点,7 空心 2 实心 | 客户端以前拿不到被淘汰分钟 |
| 下一轮再淘汰 1 个 | 点消失或 key 重建 | 8 空心 1 实心,key 不变 | 空心留在原地,过渡不重建 |
## 测试
- `frontend` `./node_modules/.bin/tsc --noEmit`exit 0
- `npm run lint`0 error / 108 warning(仓库既有 warning,无新增 error
- 指定切片 `tsx --test tests/rectification-timeline-20260909.test.ts tests/rectification-delivery-report-facts.test.ts tests/rectification-candidate-result.test.ts`**39 passed / 0 failed**(时间轴 22、候选解析 13、交付报告事实 4)
- 浏览器真人走查:无登录态,仍见 `docs/testing/rectification-timeline-20260909.md`;§4 第一条已改为「被排除的分钟变空心且不消失(数据来自推断层)」
## 偏离
1. 任务书基线写 `724a1215`;本 worktree 按开工指令基于当时 `origin/staging` `d9252516`
2. `inferenceMarks` 类型可选,解析器始终赋值。见上文。
3. 跨午夜 00:30–01:30 读数从「1 小时」变为「1 小时 1 分」,任务书未点名,但是同一条含两端规则。
4. 未改 `inference-adapter.ts`:active 投影仍只给卡片;时间轴改走推断层全集。
## 环境缺口
无登录态、无 Chrome、无 Docker。浏览器级空心点/贴底仍欠真实会话。不把合同测试写成已经看见真实像素。
+1 -1
View File
@@ -84,7 +84,7 @@
| `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-505509 |
| `TASK-rectification-timeline-20260909.md` | — | 常驻吸顶时间轴:轴锁**当前**搜索窗口并随放宽缩放、时段/分钟两套标记、候选点二元编码不分置信度(BUG-560 blocked)、无 hoverBUG-575)、只读不可采用。实现用第三个 grid 行而非 `position: sticky``useConversationScrollAnchor` 一行不改;条高固定是正确性要求;吸顶条只留区间与宽度两个元素 | 已验收通过;宽度口径与空心点两处未通过→修复单 | `ce91a0d2` |
| `TASK-rectification-timeline-fix-20260909.md` | `PROGRESS-rectification-timeline-fix-20260909.md` | 时间轴修复单:宽度读数改含两端分钟数(与 BUG-593 报告/卡片一致);标记改读推断层候选全集,被淘汰分钟留在原地变空心(客户端投影只含 active,DESIGN §10 描述在真实数据下画不出) | 待执行 | `codex/rectification-timeline-fix-20260909`BUG-602603 |
| `TASK-rectification-timeline-fix-20260909.md` | `PROGRESS-rectification-timeline-fix-20260909.md` | 时间轴修复单:宽度读数改含两端分钟数(与 BUG-593 报告/卡片一致);标记改读推断层候选全集,被淘汰分钟留在原地变空心(客户端投影只含 active,DESIGN §10 描述在真实数据下画不出) | 待验收 | `codex/rectification-timeline-fix-20260909`BUG-602603 |
| `TASK-rectification-conversation-economy-20260909.md` | `PROGRESS-rectification-conversation-economy-20260909.md` | 对照竞品后产品拍板三条:开场三句讲做法 + 一次收多件(推翻 opening brief「不要一次说完/不举例」与 SKILL L52);采集题「没有 / 记不清」按钮(不做示例骨架条);每轮只留一句(方法句进活动记录、点选旁白去领先落后、证据轮正文一句复述 + 服务端裁剪);Skill 10.0.19 | 待执行 | `codex/rectification-conversation-economy-20260909`BUG-604606 |
### 聊天主链路与首页
@@ -35,7 +35,7 @@ iOS Safari 与 Android Chrome 各走一次,在校正对话里点输入框唤
需要一个「完全不知道出生时间」的 Case 才能走到时段阶段。
- [ ] 开场(整天或 ±120)时:条上是否**不出现**分钟圆点,区间带铺满整条轴,读数写整个窗口的宽度(如「23 小时 59 分」)?
- [ ] 开场(整天或 ±120)时:条上是否**不出现**分钟圆点,区间带铺满整条轴,读数写整个窗口的宽度(如「24 小时」)?
- [ ] 选定时段后:轴是否重新对到新窗口,读数变小?
- [ ] 进入分钟阶段后:候选圆点是否出现?
- [ ] 触发一次放宽(吻合率低且代表分钟贴边缘,BUG-572):轴是否向外扩,区间带**没有**被裁掉或溢出轴外?
@@ -43,11 +43,11 @@ iOS Safari 与 Android Chrome 各走一次,在校正对话里点输入框唤
## 4. 二元编码与只读
- [ ] 答完一道区分题后,被排除的分钟是否**留在原地变成空心**而不是消失
- [ ] 答完一道区分题后,被排除的分钟是否**留在原地变成空心且不消失**(数据来自推断层)
- [ ] 所有圆点是否**一样大**?(不得有任何按可能性大小分级的迹象——BUG-560 状态是 blocked
- [ ] 鼠标悬停在条上任何位置:是否**没有**任何浮层、提示、文案变化?
- [ ] 点击条上任何位置:是否**没有**任何反应?(采用只在交付卡上)
- [ ] 条上是否只有两样东西:轴,和形如 `05:0705:09 · 2 分钟` 的读数?不应出现计数、图例、阶段名、「已对照 N 件经历」或任何注脚。
- [ ] 条上是否只有两样东西:轴,和形如 `05:0705:09 · 3 分钟` 的读数?不应出现计数、图例、阶段名、「已对照 N 件经历」或任何注脚。
## 5. 跨午夜窗口