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

17 KiB
Raw Blame History

TASK · 生时校正常驻时间轴(2026-09-09)

基线

  • 基线 commitorigin/staging = 5565b632
  • 开发分支:codex/rectification-timeline-20260909
  • 工作树:.worktrees/rectification-timeline-20260909
  • 本单不改服务端、不改数据库。所需字段全部已在客户端可得(见任务 0)。

1. 现状实证

生时校正是本产品最长的流程,也是最没有进度感的流程。

事实 出处
单轮模型调用上限 210 秒 RECTIFICATION_AGENT_ATTEMPT_TIMEOUT_MS = 210_000v9/agent-run.ts:104
一轮完整校正要走七个领域采集 + 区分卡 + 职业题 + 交付卡 USER_COLLECT_QUESTIONmethod-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):

.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 - clientHeightuse-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: relativeglobals.css:2689-2691),它挂在输入框上沿。合同测试 chat-notice-and-scroll-contract.test.ts:127doesNotMatch(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 分钟

明令不得出现在吸顶条上

砍掉的东西 理由
「范围从 AB 变为 CD」 服务端已写进落库助手消息,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:608prefers-reduced-transparency: reduce)与 :613prefers-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" 同上 :126v9/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 小时三档的刻度与位置换算;区间带落在轴内;候选点在范围边界上的归属(闭区间)
  • 源码锁测试:组件源码不出现 onMouseEnteronClickrelativeSupportprobability
  • 组件不含 position: sticky

任务 2 · 接入为第三个 grid 行

rectification-agentic-chat.tsx 的布局与 globals.css.rectification-workspace__chat:网格改为三行(时间轴行 / 滚动行 / 输入框行),时间轴行 auto 且高度固定。

验收标准

  • use-conversation-scroll-anchor.tsgit diff 为空
  • 源码锁测试:时间轴不是 .conversation 的后代
  • 合同测试:.rectification-workspace__chatgrid-template-rows 为三行;时间轴行有固定高度声明(不是 auto 内容撑高)

任务 3 · 两阶段两套标记

stage === "block_scan" 时标记是时段/子段区块,"minute" 时是候选分钟点。阶段切换时轴通常同时缩放(见任务 4)。

验收标准:两种 stage 各一条渲染测试;block_scan 下不渲染分钟点,minute 下不渲染区块。

任务 4 · 轴缩放

candidate_range 变化时(放宽窗口 BUG-572,或时段选定后收窄),轴重新对到新窗口,候选标记与区间带位置随之过渡。

  • 过渡用 CSS transition不得用 JS 定时器插值
  • 必须遵守 prefers-reduced-motion: reduceglobals.css:594 已有分支)

验收标准

  • 测试覆盖轴两端变化前后的位置换算
  • 源码锁:时间轴相关源码不出现 setInterval / setTimeout 驱动的位置更新
  • 放宽场景(±15 → ±30)下区间带仍完整落在轴内

任务 5 · 空态与未就绪态

Case 未载入、candidate_range 缺失、或 stage 未知时,条渲染为等高的空骨架(无 spinner、无「正在加载」文案,AGENTS.md §6)。

验收标准:未就绪态与就绪态高度一致的合同测试。

任务 6 · 移动端

紧凑模式在 768px 以下触发(rectification-board-model.ts:14rectification-agentic-chat.tsx:494)。移动端 .chat-panel.is-rectificationcalc(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. 开工前置命令

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.mdfrontend/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;推送后核对远端 SHAAGENTS.md §2.6
  • 做不了的(无登录态、无 Chrome、无 Docker)如实写进 BLOCKED.mddocs/testing/不得写成"通过"

验收(Claude2026-09-09origin/staging @ 724a1215staging 已部署同 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 / 56pxdata-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.mdBUG-602/603)。真人走查清单未做,沿用执行方的 6 节清单。