Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
17 KiB
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 |
用户在整个过程中能看到的范围信息只有两处:
- 一句话:范围变化时服务端写进落库
assistant_message的「范围从 A–B 变为 C–D。」(BUG-588 的防复发要求它必须写在那里) - 交付卡:
rectification-range-delivery.tsx,只在流程末尾出现
也就是说,「还有多宽、还要多久」这个信息只以句子形式散在消息流里,滚上去就看不见了;用户在第 12 道题时无法回答「我离结束还有多远」。
2. 根因
范围宽度是这条流程里唯一能表达「正在收敛」的量,但它没有常驻的表达形态。BUG-575 曾把输入框上方的步骤条删掉(因为那是套话),删掉之后没有任何东西补上这个位置。
3. 决策记录
以下七条是产品负责人在 2026-09-09 会话中的明确决定。其中第 5 条推翻了本人上一版设计口径,执行方按本节为准。
- 做横向时间轴,常驻吸顶,只读。 产品明确倾向吸顶方案(相对于「按轮插进消息流」)。
- 候选点不按置信度分级,改二元编码(在范围内 / 已排除)。依据 BUG-560(状态 blocked):候选之间相对支持度是 7–9 分(满分 100),20 例公开 holdout 校准显示分钟级几乎没有区分力,按领先集合取真实分钟 12/20 与随机取 10 分钟(≈48%)相当。按置信度分级显示等于用视觉编码放大统计上不显著的差异,撞
AGENTS.mdB4 诚实边界。 - 不做 hover 出详情。 依据 BUG-575:悬停时间解释因文案改写、撑开布局被整体删除,并留下源码级防复发锁;且移动端无 hover。
- 越简约越好,避免过度提示。 吸顶条上只保留两个元素,见 §4.3。
- 轴锁"当前搜索窗口",不锁"开场窗口",并随窗口变化缩放。 本人上一版口径写的是"轴锁开场窗口、全程不缩放",该口径错误,作废。理由:开场窗口不是定值(±15 / ±30 / ±60 / ±120 / 整天,见 BUG-571、BUG-573),且放宽会顶穿开场窗口(BUG-572:±15 → ±30 → ±60 → ±120)。
- 时间轴只读,不得成为采用入口。 采用仍然只在交付卡上完成。依据产品口径「多余入口宁可删除也不修」,以及 BUG-501/502 已经删过一个重复入口。
- 不删右侧板的
换升时刻段(rectification-board.tsx:114-158)。它是分盘边界与逐层 LayerChips 的技术参照,与时间轴的"搜索收敛"不是同一份信息。桌面端两者同屏是否显得重复,留给真人走查判断,见任务 7。
4. 硬红线
4.1 用 grid 行,不用 position: sticky;不得修改滚动跟随
.rectification-workspace__chat 已经是两行网格(globals.css:2672-2677):
.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 吸顶条上只有两个元素
- 轴本体:区间带 + 候选标记
- 当前区间与宽度:
05:07–05: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 其余红线
- 二元编码:候选标记一律同样大小,只区分在范围内 / 已排除。不得按
relativeSupport/probability_percent分级(尺寸、深浅、粗细都不行)。 - 不得使用
onMouseEnter/onMouseOver驱动的信息展示(BUG-575 防复发)。 - 时间轴内不得有采用、确认、跳转类操作;不得渲染
relativeSupport数值。 - 移动端条高 ≤ 56px(依据见任务 6)。
- 若使用
backdrop-filter,必须同时把新选择器加进globals.css:608(prefers-reduced-transparency: reduce)与:613(prefers-contrast: more)两个分支——现有.chat-header, .composer-wrap, .personal-report-actions已在其中,漏加会在该设置下糊成一片。 AGENTS.md§7 通用红线照常:tsc --noEmit0 错;npm run lint0 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.1(grid 行非 sticky)、§4.2(固定高度)、§4.3(只有两个元素)三条写死docs/BUG_HISTORY.md:本单是新增能力不是修 Bug,若实现中发现既有缺陷才按 §5 开条目;否则不强开CHANGELOG.md:日期 + 一句话标题 + 用户可感知变更docs/testing/rectification-timeline-20260909.md:真人走查清单,必须包含下面两条本环境判断不了的:- 桌面端时间轴与右侧板
换升时刻段同屏,是否显得重复?(决策记录第 7 条留下的问题) - 移动端键盘弹出后,条 + 输入框 + 至少一条完整消息是否仍同屏可见?
- 桌面端时间轴与右侧板
验收标准:四份文件都在同一批提交里。
6. 让步顺序
被挡住时按此顺序让步,每让一步都写进进度记录:
- 先让阶段区块(任务 3):若
block_scan的区块边界在会话中途拿不到,该阶段先只画区间带与轴,不画区块。分钟阶段不受影响。 - 再让轴缩放动画(任务 4):过渡做不好就先做无过渡的直接切换,功能正确优先。
- 最后让移动端呈现(任务 6):768px 以下先整体隐藏时间轴(而不是做一个挤坏布局的版本)。桌面端先落地。
不得让步的:§4.1(grid 行、不改 hook)、§4.2(固定高度)、§4.3(只有两个元素)、§4.4.1(二元编码)、§4.4.3(只读)。这五条任何一条做不到,停下来报告,不要自行变通。
7. 开工前置命令
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/,不得写成"通过"
验收(Claude,2026-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 |
采信执行方:/ Static,CSS 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 节清单。