diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 52250f4f..6d8b8cba 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -83,6 +83,7 @@ | `TASK-rectification-candidate-compare-columns-20260908.md` | `PROGRESS-rectification-candidate-compare-columns-20260908.md` | 产品决定交付卡改一行三列:每列相对可能性、D9/D10/月宿性格处事、经历对照计数、往后 12 个月事件窗、「更像这个」即采用;引擎按候选分钟各算 ledger 与窗口;顺带 BUG-598 点选题成年下限在 inspect 回退路径缺失(需核对)、采集题「没有」按钮(产品可否决);Skill 10.0.18 | 待验收 | `codex/rectification-candidate-compare-columns-20260908`(BUG-597~598) | | `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-505~509) | +| `TASK-rectification-timeline-20260909.md` | — | 常驻吸顶时间轴:轴锁**当前**搜索窗口并随放宽缩放、时段/分钟两套标记、候选点二元编码不分置信度(BUG-560 blocked)、无 hover(BUG-575)、只读不可采用。实现用第三个 grid 行而非 `position: sticky`,`useConversationScrollAnchor` 一行不改;条高固定是正确性要求;吸顶条只留区间与宽度两个元素 | 待领取 | — | ### 聊天主链路与首页 @@ -114,6 +115,7 @@ | `TASK-report-longform-gaps2-20260906.md`(仓库根) | `PROGRESS-report-longform-gaps2-20260906.md` | 长报告真实参数组合下的装配缺口 | 待验收 | `codex/report-longform-gaps2-20260906`(BUG-561/562 `cfcd369d`;补洞 BUG-564 `e4d16b75`) | | `TASK-report-md-page-20260906.md` | `PROGRESS-report-md-page-20260906.md` | 长报告 Markdown 直接作为报告页 | 已合入 | `cfcd369d` / `809bdf13`(BUG-563 lint 随后修) | | `TASK-report-list-500-20260907.md`(仓库根) | `PROGRESS-report-list-500-20260907.md` | 列表 PostgREST JSON 路径 500 | 待验收 | `b466a6fc`(BUG-574) | +| — | `PROGRESS-report-progress-20260909.md` | 生成等待屏只有 spinner 与秒表:后端 `progressPercent` / `progressPhase` 与分章行已产出,前端解析后一字未渲染,且分章行在 `generating` 时根本不出服务端。改为按章分格进度条 + 章节清单,停滞 90 秒改「用时较长,仍在写」;不画百分比条、不做插值动画、不报预计剩余 | 已验收 | `848e39e6`、`5565b632`(BUG-601) | ### 前端基础与工程 diff --git a/docs/tasks/TASK-rectification-timeline-20260909.md b/docs/tasks/TASK-rectification-timeline-20260909.md new file mode 100644 index 00000000..460714b0 --- /dev/null +++ b/docs/tasks/TASK-rectification-timeline-20260909.md @@ -0,0 +1,237 @@ +# 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 行,放在滚动行之上,位于滚动容器之外。** + +滚动容器是 `
`(`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: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 其余红线 + +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.1(grid 行非 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.1(grid 行、不改 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/`,**不得写成"通过"**