Files
Jyotisha/docs/tasks/TASK-rectification-timeline-20260909.md
T

261 lines
17 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 · 生时校正常驻时间轴(2026-09-09)
## 基线
- 基线 commit`origin/staging` = `5565b632`
- 开发分支:`codex/rectification-timeline-20260909`
- 工作树:`.worktrees/rectification-timeline-20260909`
- 本单**不改服务端、不改数据库**。所需字段全部已在客户端可得(见任务 0)。
---
## 1. 现状实证
生时校正是本产品最长的流程,也是最没有进度感的流程。
| 事实 | 出处 |
| --- | --- |
| 单轮模型调用上限 210 秒 | `RECTIFICATION_AGENT_ATTEMPT_TIMEOUT_MS = 210_000``v9/agent-run.ts:104` |
| 一轮完整校正要走七个领域采集 + 区分卡 + 职业题 + 交付卡 | `USER_COLLECT_QUESTION``method-followup.ts` |
| 历史上出现过 27 分钟仍未收敛 | `docs/tasks/TASK-rectification-convergence-exit-20260906.md` |
| 近两周 BUG-558~598 四十余条,全部在补这条流程的边界 | `docs/BUG_HISTORY.md` |
用户在整个过程中能看到的范围信息只有两处:
1. **一句话**:范围变化时服务端写进落库 `assistant_message` 的「范围从 A–B 变为 C–D。」(BUG-588 的防复发要求它必须写在那里)
2. **交付卡**`rectification-range-delivery.tsx`,只在流程末尾出现
也就是说,**「还有多宽、还要多久」这个信息只以句子形式散在消息流里**,滚上去就看不见了;用户在第 12 道题时无法回答「我离结束还有多远」。
## 2. 根因
范围宽度是这条流程里唯一能表达「正在收敛」的量,但它没有常驻的表达形态。BUG-575 曾把输入框上方的步骤条删掉(因为那是套话),删掉之后没有任何东西补上这个位置。
## 3. 决策记录
以下七条是产品负责人在 2026-09-09 会话中的明确决定。**其中第 5 条推翻了本人上一版设计口径,执行方按本节为准。**
1. **做横向时间轴,常驻吸顶,只读。** 产品明确倾向吸顶方案(相对于「按轮插进消息流」)。
2. **候选点不按置信度分级,改二元编码**(在范围内 / 已排除)。依据 BUG-560(状态 **blocked**):候选之间相对支持度是 7–9 分(满分 100),20 例公开 holdout 校准显示分钟级几乎没有区分力,按领先集合取真实分钟 12/20 与随机取 10 分钟(≈48%)相当。按置信度分级显示等于用视觉编码放大统计上不显著的差异,撞 `AGENTS.md` B4 诚实边界。
3. **不做 hover 出详情。** 依据 BUG-575:悬停时间解释因文案改写、撑开布局被整体删除,并留下源码级防复发锁;且移动端无 hover。
4. **越简约越好,避免过度提示。** 吸顶条上只保留两个元素,见 §4.3。
5. **轴锁"当前搜索窗口",不锁"开场窗口",并随窗口变化缩放。** 本人上一版口径写的是"轴锁开场窗口、全程不缩放",**该口径错误,作废**。理由:开场窗口不是定值(±15 / ±30 / ±60 / ±120 / 整天,见 BUG-571、BUG-573),且放宽会顶穿开场窗口(BUG-572:±15 → ±30 → ±60 → ±120)。
6. **时间轴只读,不得成为采用入口。** 采用仍然只在交付卡上完成。依据产品口径「多余入口宁可删除也不修」,以及 BUG-501/502 已经删过一个重复入口。
7. **不删右侧板的 `换升时刻` 段**`rectification-board.tsx:114-158`)。它是分盘边界与逐层 LayerChips 的技术参照,与时间轴的"搜索收敛"不是同一份信息。桌面端两者同屏是否显得重复,留给真人走查判断,见任务 7。
## 4. 硬红线
### 4.1 用 grid 行,不用 `position: sticky`;不得修改滚动跟随
`.rectification-workspace__chat` 已经是两行网格(`globals.css:2672-2677`):
```css
.rectification-workspace__chat {
min-width: 0; min-height: 0;
display: grid;
grid-template-rows: minmax(0, 1fr) auto; /* 滚动行 + 输入框行 */
}
```
**时间轴作为新增的第一个 grid 行,放在滚动行之上,位于滚动容器之外。**
滚动容器是 `<section className="conversation is-rectification">``rectification-agentic-chat.tsx:1523-1528`),其唯一直接子元素是 `.message-list`:1530)。
为什么必须这样而不是 sticky
| | grid 行(本单方案) | `position: sticky`(禁止) |
| --- | --- | --- |
| `scrollHeight` | 不变 | 改变 |
| 贴底距离 `scrollHeight - scrollTop - clientHeight``use-conversation-scroll-anchor.ts:19`,阈值 96 见 :6、:23) | 公式仍精确 | 漂移 |
| `ResizeObserver`(只观察 `element.children`,:99-102) | 观察不到,不触发多余跟随 | 会被观察到 |
| `useConversationScrollAnchor` | **一行都不用改** | 必须改 |
**`frontend/src/hooks/use-conversation-scroll-anchor.ts` 本单一行不得改。** 不得新增任何滚动跟随逻辑(`AGENTS.md` §6:一律 `useConversationScrollAnchor` + `JumpToLatestButton`)。
`JumpToLatestButton` 不会被遮挡:它在 `.composer-wrap` 内、`bottom: 100%``globals.css:1523-1531`),而校正面把 `.composer-wrap` 覆写成 `position: relative``globals.css:2689-2691`),它挂在输入框上沿。合同测试 `chat-notice-and-scroll-contract.test.ts:127``doesNotMatch(wrapRule, /sticky/)` 只作用于 `.jump-to-latest`,新条不触雷——**但也不得为新条引入任何 sticky**。
### 4.2 条的高度必须固定,且从挂载起就占位
**这是正确性要求,不是视觉偏好。** 条在滚动容器之外,高度变化会改 `clientHeight`,而 `ResizeObserver` 不观察它(:99-102)——贴底状态下内容会悄悄滑出视野底部,没有任何机制把它拉回。
同源隐患:若条在 Case 载入后才挂载,`clientHeight` 骤减,原本 `distance` 在 40–96 之间的读者会被推过 96 阈值而解除贴底,「跳到最新」凭空弹出。**从首次渲染起就占住最终高度**(未就绪时渲染空骨架占位,不得用 spinner —— `AGENTS.md` §6 揭幕后禁止 spinner)。
### 4.3 吸顶条上只有两个元素
1. **轴本体**:区间带 + 候选标记
2. **当前区间与宽度**`05:0705:09 · 2 分钟`
**明令不得出现在吸顶条上**
| 砍掉的东西 | 理由 |
| --- | --- |
| 「范围从 A–B 变为 C–D」 | 服务端已写进落库助手消息,BUG-588 防复发**要求**它在那里;条上再写=同句同屏两次 |
| 「仍在范围内 N / 已排除 N」计数与图例 | 实心=在内、空心=已排除,答完第一题即自明;图例是过度提示的典型形态 |
| 阶段标签(时段 / 分钟) | `block_scan` / `minute` 是后台词汇,贴近 BUG-043 红线 |
| 「已对照 N 件经历」 | 归交付卡,那里每列已带经历对照计数(`rectification-range-delivery.tsx:70-74` |
| 「时间轴只读」注脚 | 在解释一个不存在的东西;没有元素看起来可点,就不需要声明不可点 |
| 预计剩余时间、按定时器推进的插值动画 | 与 BUG-601 同一条原则:不得演出后端没做的动作 |
### 4.4 其余红线
1. 二元编码:候选标记一律同样大小,只区分在范围内 / 已排除。**不得**按 `relativeSupport` / `probability_percent` 分级(尺寸、深浅、粗细都不行)。
2. 不得使用 `onMouseEnter` / `onMouseOver` 驱动的信息展示(BUG-575 防复发)。
3. 时间轴内不得有采用、确认、跳转类操作;不得渲染 `relativeSupport` 数值。
4. 移动端条高 **≤ 56px**(依据见任务 6)。
5. 若使用 `backdrop-filter`,必须同时把新选择器加进 `globals.css:608``prefers-reduced-transparency: reduce`)与 `:613``prefers-contrast: more`)两个分支——现有 `.chat-header, .composer-wrap, .personal-report-actions` 已在其中,漏加会在该设置下糊成一片。
6. `AGENTS.md` §7 通用红线照常:`tsc --noEmit` 0 错;`npm run lint` **0 error**;测试总数不得低于开工实测;改 UI 同提交更新 `frontend/DESIGN.md`;新文案对照 `frontend/docs/VOICE.md`;不改数据库、不顺手升依赖。
---
## 5. 任务分解
### 任务 0 · 数据源确认(开工第一件事)
确认以下字段在校正会话进行中(非交付时)就能拿到,并写进进度记录:
| 用途 | 字段 | 已知出处 |
| --- | --- | --- |
| 轴的两端 | `candidate_range` | `api/rectification/cases/[caseId]/route.ts:125` |
| 阶段 | `stage``"minute" \| "block_scan"` | 同上 :126`v9/interview-state.ts:65` |
| 区间带 | `credibleRange` | `rectification-candidate-result.ts:95` |
| 候选标记 | `candidates[].time` | 同上 :71、:307 |
**验收标准**:进度记录里逐条写明"可得 / 不可得"。若某项在会话中途拿不到,**停下来报告,不要自行新增服务端字段**——本单范围内服务端不改。
### 任务 1 · 时间轴组件(只读)
新建组件(建议 `frontend/src/components/rectification-timeline.tsx`)与纯函数刻度/位置计算(建议 `frontend/src/lib/rectification-timeline-scale.ts`,与组件分离以便测试)。
- 轴的两端 = `candidate_range`;区间带 = `credibleRange`;两者都按当前轴跨度换算百分比
- 刻度步长随跨度自适应(跨度越大步长越粗),标签用 `HH:MM`
- 候选标记:`stage === "minute"` 时为等大圆点,在范围内 / 已排除两态;`stage === "block_scan"` 时为区块(不是点)
- 组件不接收任何回调,不渲染任何按钮
**验收标准**
- 纯函数测试覆盖:跨度 30 分钟 / 4 小时 / 24 小时三档的刻度与位置换算;区间带落在轴内;候选点在范围边界上的归属(闭区间)
- 源码锁测试:组件源码不出现 `onMouseEnter``onClick``relativeSupport``probability`
- 组件不含 `position: sticky`
### 任务 2 · 接入为第三个 grid 行
`rectification-agentic-chat.tsx` 的布局与 `globals.css``.rectification-workspace__chat`:网格改为三行(时间轴行 / 滚动行 / 输入框行),时间轴行 `auto` 且高度固定。
**验收标准**
- `use-conversation-scroll-anchor.ts``git diff` 为空
- 源码锁测试:时间轴不是 `.conversation` 的后代
- 合同测试:`.rectification-workspace__chat``grid-template-rows` 为三行;时间轴行有固定高度声明(不是 `auto` 内容撑高)
### 任务 3 · 两阶段两套标记
`stage === "block_scan"` 时标记是时段/子段区块,`"minute"` 时是候选分钟点。阶段切换时轴通常同时缩放(见任务 4)。
**验收标准**:两种 stage 各一条渲染测试;`block_scan` 下不渲染分钟点,`minute` 下不渲染区块。
### 任务 4 · 轴缩放
`candidate_range` 变化时(放宽窗口 BUG-572,或时段选定后收窄),轴重新对到新窗口,候选标记与区间带位置随之过渡。
- 过渡用 CSS transition**不得**用 JS 定时器插值
- 必须遵守 `prefers-reduced-motion: reduce``globals.css:594` 已有分支)
**验收标准**
- 测试覆盖轴两端变化前后的位置换算
- 源码锁:时间轴相关源码不出现 `setInterval` / `setTimeout` 驱动的位置更新
- 放宽场景(±15 → ±30)下区间带仍完整落在轴内
### 任务 5 · 空态与未就绪态
Case 未载入、`candidate_range` 缺失、或 `stage` 未知时,条渲染为**等高的空骨架**(无 spinner、无「正在加载」文案,`AGENTS.md` §6)。
**验收标准**:未就绪态与就绪态高度一致的合同测试。
### 任务 6 · 移动端
紧凑模式在 768px 以下触发(`rectification-board-model.ts:14``rectification-agentic-chat.tsx:494`)。移动端 `.chat-panel.is-rectification``calc(64px + env(safe-area-inset-top))` 头部 + `minmax(0,1fr)``globals.css:1822`),`--composer-reserve: 116px`:1839)。667px 视口下滚动区约 467px。
条高上限 **56px**,取值依据是它等于本界面已有的 `--rectification-jump-clearance: calc(44px + var(--space-3))``globals.css:2679`)——沿用同一套由 44px 触控尺寸导出的节奏,不新造常数。超过 64px 等于在手机上吃掉一整条消息气泡。
**验收标准**:合同测试锁死移动端断点下条高 ≤ 56px。
### 任务 7 · 记录与真人清单
- `frontend/DESIGN.md`:新增一节,把 §4.1grid 行非 sticky)、§4.2(固定高度)、§4.3(只有两个元素)三条写死
- `docs/BUG_HISTORY.md`:本单是新增能力不是修 Bug,**若实现中发现既有缺陷**才按 §5 开条目;否则不强开
- `CHANGELOG.md`:日期 + 一句话标题 + 用户可感知变更
- `docs/testing/rectification-timeline-20260909.md`:真人走查清单,**必须包含**下面两条本环境判断不了的:
1. 桌面端时间轴与右侧板 `换升时刻` 段同屏,是否显得重复?(决策记录第 7 条留下的问题)
2. 移动端键盘弹出后,条 + 输入框 + 至少一条完整消息是否仍同屏可见?
**验收标准**:四份文件都在同一批提交里。
---
## 6. 让步顺序
被挡住时按此顺序让步,每让一步都写进进度记录:
1. **先让阶段区块(任务 3**:若 `block_scan` 的区块边界在会话中途拿不到,该阶段先只画区间带与轴,不画区块。分钟阶段不受影响。
2. **再让轴缩放动画(任务 4**:过渡做不好就先做无过渡的直接切换,功能正确优先。
3. **最后让移动端呈现(任务 6**:768px 以下先整体隐藏时间轴(而不是做一个挤坏布局的版本)。桌面端先落地。
**不得让步的**:§4.1grid 行、不改 hook)、§4.2(固定高度)、§4.3(只有两个元素)、§4.4.1(二元编码)、§4.4.3(只读)。这五条任何一条做不到,停下来报告,不要自行变通。
---
## 7. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-timeline-20260909 \
.worktrees/rectification-timeline-20260909 origin/staging
cd .worktrees/rectification-timeline-20260909
git status -sb | head -1 # 确认在自己的分支上
# 记下基线数字,§4.4.6 要用
cd frontend && npm test 2>&1 | grep -E "^# (tests|pass|fail)"
./node_modules/.bin/tsc --noEmit && npm run lint
```
必读:`AGENTS.md` §2 §3 §6 §7、`frontend/DESIGN.md``frontend/docs/VOICE.md`、本文件第 3 与第 4 节。
`AGENTS.md` §5 检索 `docs/BUG_HISTORY.md`:关键词 `滚动``吸顶``sticky``进度``步骤条`,并完整读完 BUG-043、BUG-560、BUG-572、BUG-575、BUG-588、BUG-589。
---
## 8. BUG 编号起点
开工时以 `docs/BUG_HISTORY.md` 实际最大号为准。写本任务书时最大号是 **601**,预期从 **602** 起。
**特别提醒**:本仓近期出现过编号竞争——2026-09-09 有三个会话同时占号,`BUG-599` / `600` 被先落地的会话拿走,后到的一份在 rebase 时才发现并顺延到 601。**交付前 rebase 到最新 `origin/staging` 后,务必重新核对编号是否仍然可用。**
---
## 9. 交付
- 本地提交,**推 staging 前先回报**,由 Claude 独立验收
- 推送用快进:`git push origin HEAD:staging`;推送后核对远端 SHA`AGENTS.md` §2.6
- 做不了的(无登录态、无 Chrome、无 Docker)如实写进 `BLOCKED.md``docs/testing/`**不得写成"通过"**
## 验收(Claude2026-09-09`origin/staging` @ `724a1215`staging 已部署同 SHA
| 门 | 结果 |
| --- | --- |
| tsc | 0 错 |
| lint | 0 error / 108 warning |
| 前端 rectification + consultation + session + voice + skill-registry(非 DB | 1331 / 0 |
| Python v5_services + event_probes + growth contract | 全绿 |
| `next build` / 首屏 gzip | 采信执行方:`/` StaticCSS gzip +0.95% |
| 项 | 结论 |
| --- | --- |
| 任务 0 数据源 | 通过。只读已在线字段,服务端零改动 |
| §4.1 grid 行、不改滚动跟随 | 通过。`use-conversation-scroll-anchor.ts` diff 为空 |
| §4.2 固定条高、骨架占位 | 通过。64 / 56px,`data-state="pending"` 等高 |
| §4.3 只有两个元素 | 通过。源码锁住不得出现的文案 |
| §4.4 二元编码、无 hover、transform-only | 通过。执行方为避开 width 过渡红线改用 transform,比原写法好 |
| 让步 1 时段阶段不画区块 | 接受 |
| **宽度读数** | **未通过**:按差值算(04:51–04:59 显示 8 分钟),与 BUG-593 定下的含两端口径(9 分钟)冲突。任务书示例本身就是差值口径,责任在任务书 |
| **空心点** | **未通过**:客户端候选投影只含 active 分钟(`inference-adapter.ts` L292),被排除的分钟到不了条上,DESIGN §10 的"留在原地变空心"在真实数据下画不出来;测试用的是合成输入 |
结论:整体通过,两处不一致写成 `TASK-rectification-timeline-fix-20260909.md`BUG-602/603)。真人走查清单未做,沿用执行方的 6 节清单。