# PROGRESS · R7 生时校正(2026-09-16) 任务书:`docs/tasks/TASK-cend-surfaces-claude-alignment-20260916.md` 的 **R7**(T7.1 + T7.2,决策 D10)。 - 基线:`origin/staging` = `50ce02c8` - 分支:`codex/cend-rectification-20260916` - worktree:`/workspace/Jyotisha/.claude/worktrees/agent-a8ca21a2f5ced3002`(本会话被隔离在这个 agent worktree,不是任务书里的 `.worktrees/cend-rectification-20260916`) - BUG 编号:**未占用**。本轮是产品改造,未发现需要建档的产品缺陷,`docs/BUG_HISTORY.md` 当前最大号仍是 **BUG-738**(已核对)。 --- ## 1. 开工基线数字 在 `50ce02c8` 上 `npm ci` 后实测: | 项 | 基线 | | --- | --- | | `tsc --noEmit` | 0 错 | | `npm run lint` | **0 error / 118 warning** | | `npm test` | `# tests 3350 / # pass 3304 / # fail 31` | | 失败清单 | 31 条,全部是无 Docker 的数据库 / 部署 / 迁移类;存档在 `base.txt` | | `next build` | exit 0,`/` = `○ Static` | | 产物 CSS gzip | `.next/static/chunks/*.css` 合计 **39,017 B**(raw 251,634 B) | --- ## 2. 逐条验收对照 ### T7.1 可信区间提成常驻条 **先说一个与任务书事故实证不一致的发现,它改变了本轮的做法。** 任务书 E9 写「区间只以 `.rectification-board__clock` 的形式出现在右侧盘面板的标题行」。实际读代码:`rectification-agentic-chat.tsx:1645` 起,对话区顶部**已经有一条常驻条** `RectificationTimeline`(BUG-602/603 那一轮做的),它是 `.rectification-workspace__chat` 的第一个 grid 行,已经在显示**当前区间与宽度**(`view.rangeLabel` / `view.widthLabel`)。窄屏 `.is-compact` 下 `.rectification-workspace__chat` 取 `grid-area: 1 / 1` 且三行俱在,所以这条**本来就可见**。 真正只存在于 `.rectification-board__clock` 的是**代表分钟**——窄屏下盘面变成默认关闭的 overlay,代表分钟就此看不见。这才是 E9 描述的那个死角。 因此本轮没有另造第二条常驻条(那会撞上固定条高这条正确性约束,也等于手写第二套),而是**在既有常驻条上补齐缺的两项**。 | 验收标准 | 结论 | 证据 | | --- | --- | --- | | 窄屏 `.is-compact` 下常驻条仍然可见 | **CSS 上确认,真机未验(环境缺口)** | 见 §5 | | 边界语义不弱化 | 通过 | 条上只写「代表分钟 HH:MM」/「已采用 HH:MM」,不写确认;`REPRESENTATIVE_MINUTE_DISCLAIMER` 与交付/采用旁白一行未改。测试 `an adopted minute is labelled adopted...` 断言组件与 scale 模块都不出现「已确认 / 确认的唯一」 | | 区间数字与 `.rectification-board__clock` 同源 | 通过 | 同一次调用:盘面 `rectification-board.tsx:59` `workingRectificationTime(result)`;条由 `rectification-agentic-chat.tsx` 传 `workingTime: workingRectificationTime(candidateResult)`。不是两条推导路径,是一个值两处用。测试第 1 条锁死这三行源码 | 条上最终四项,一行排完: ``` 05:07–05:09 · 3 分钟 · 代表分钟 05:08 · 已答 6 题 ``` 每项的服务端来源: | 读数 | 来源 | 是否前端口算 | | --- | --- | --- | | 当前区间 | `credible_range`(服务端投影) | 否 | | 宽度 | 该区间自身的闭区间宽度,`timelineDurationLabel()`,既有函数未改 | 否(任务书让步顺序第 5 条明确允许「只显示当前区间与宽度」) | | 代表分钟 | `workingRectificationTime(result)` = `selectedTime ?? representativeTime ?? houseTable.time`,与盘面同源 | 否 | | 已答题数 | `decisionReceipt.inference_state.answered_probe_count`,**服务端自己的计数**,逐字读出 | 否 | | 收窄进度 | — | **未做,触发让步顺序第 5 条,见 §3** | `answered_probe_count` 此前已经在线上(`compactInferenceProjection()` 写进 `inference_state`,随 receipt 下发),只是客户端没解析。本轮加 `parseAnsweredProbeCount()` 读出来,投影没给就整项不显示,不补 `已答 0 题`。 ### T7.2 盘面板折叠块收敛 任务书说五块(`:117/:132/:160/:182/:198`)。实际数下来盘面里有 **8 个** `
`:本命宫位表、唯一分钟确认门、本轮技法、换升时刻(内嵌显示层)、两段本命上升、经历与大运对照、交界节奏。任务书把 `:117` 记成「盘面技法审计」、`:182` 记成「口径披露」,并漏了 `:212` 的交界节奏。按实际内容归的类: | 新段 | 装进去的原块 | | --- | --- | | **候选分钟**(`aria-label="候选分钟"`) | 换升时刻(改为段内组标题 + 原列表)、次级候选(原「显示层」子折叠) | | **参数与口径**(`aria-label="参数与口径"`) | 本命宫位表、唯一分钟确认门、本轮技法、两段本命上升、经历与大运对照(含双轨一致性那句)、交界节奏 | | 验收标准 | 结论 | 证据 | | --- | --- | --- | | 五块内容一条不丢,只换归属 | 通过 | 测试 `the board folds to two sections and loses none of the five blocks` 在两段的源码切片里逐块断言 | | `engineMeaningToDisplayCopy()` 调用点全部保留 | 通过 | 改前 5 处,改后 5 处,测试里 `assert.equal(..., 5)` 锁死 | | 候选分钟列表 `is-changed` / 「当前时间」行为不变 | 通过 | 逐分钟行的 JSX 原样搬运未改;测试 `regrouping changed no minute-row behaviour` 锁 `is-changed` / `is-working` / 「当前时间」/「已更新」四处 | | 键盘可达性不弱于改前 | 通过(静态) | 每块仍是真 ``,无 `role="button"`、无 `onClick` 开合;测试断言 `` 数 ≥ 6 且无 `open`。嵌套多了一层 → 到最深一块要多按一次 Enter,这是收敛的固有代价,不是可达性缺失 | **渲染条件逐块保持原样。** 确认门与本轮技法原本写在 `{table && ...}` 里面,现在仍跟着 `table` 走——没有星盘时它们本来就不出现,改后也一样。不是「一条不丢」的反面,是不多出一条。 ### 硬约束核对 | # | 约束 | 结论 | | --- | --- | --- | | 1 | 数字只来自服务端投影 | 通过,四项来源见上表;收窄进度按让步顺序不做 | | 2 | 边界语义不弱化、候选不得写成 confirmed | 通过 | | 3 | 常驻条区间与盘面 clock 同源 | 通过 | | 4 | 五块一条不丢、`engineMeaningToDisplayCopy()` 全留、`is-changed`/「当前时间」不变 | 通过 | | 5 | 窄屏 `.is-compact` 下常驻条可见 | CSS 确认,真机是缺口(§5) | | 6 | 只动 `rectification-*` 与 `globals.css` 的 `.rectification-*` / `.technique-audit` 区段 | 通过,见 §6 | | 7 | 复用 `ChatComposer` / `useConversationScrollAnchor` | 通过,两者一行未改;未新增输入框与滚动跟随 | | 8 | `tsc` 0 错、`lint` 0 error、warning ≤ 118 | 通过:0 错 / **0 error / 118 warning**(与基线同数) | | 9 | `npm test` 失败清单与基线逐条一致 | 通过:`# tests 3360 / # pass 3314 / # fail 31`,`diff base.txt after.txt` **无差异** | | 10 | 改断言写「原值/新值/原因」 | 通过,见 §4 | | 11 | `next build` 后 `/` 仍 `○ Static` | 通过,exit 0,`┌ ○ /` | | 12 | 揭幕后无 spinner / 骨架 /「正在加载」 | 通过,未改 `data-state="pending"` 等高空骨架;新增元素都是静态 `` | | 13 | 同提交更新 `DESIGN.md`、对照 `VOICE.md` | 通过,§10 与 §12 重写;文案对照见下 | **首屏体积(红线:`/` gzip ±2%)。** Next 16.3.1 的构建输出没有 Size / First Load 两列,沿用 BUG-601 那轮的口径直接量产物:`.next/static/chunks/*.css` 合计 gzip **39,017 → 38,973 B(−44 B,−0.11%)**,在 ±2% 内。raw 从 251,634 涨到 252,540 B(+906 B),gzip 反而略降属正常。新增 JS(`rectification-timeline-scale.ts` 的两个 label 函数、`parseAnsweredProbeCount`)全部落在校正面的 `dynamic()` 分包里,不进 `/` 首屏。 **VOICE.md 对照。** 「代表分钟 05:08」「已采用 05:08」「已答 6 题」三句都是短事实,无公文腔,不含被禁的「概率 / 置信度 / 确定 / 相对支持度」,也不碰 DESIGN.md 里已明令砍掉的「已对照 N 件经历」——那是经历吻合数,归交付卡;条上的「已答 N 题」是问答题数,两个量不同,已在 DESIGN.md 表里写明区别。 --- ## 3. 让步顺序:触发了第 5 条 **让步顺序第 5 条「T7.1 的收窄进度:若服务端投影里没有现成的『已从 N 分钟收到 M 分钟』字段 —— 只显示当前区间与宽度,不要前端口算进度」——已触发。** 依据,两条,第二条比第一条更要紧: 1. **没有这个字段。** 服务端确实能拼出这句话——`user-copy.ts` 的 `progressClause()`,由 `answer-choice.ts:1885/2060` 与 `agent-run.ts:1189` 传 `openingRangeFromDossier(dossier)` 调用——但它只作为**旁白正文**落库,没有结构化出口。客户端要拿到只能解析消息文本。 2. **前端自己减会算错,不只是违规。** 开场窗口在客户端确实有(`case.candidate_range` → `searchWindow`),但它是**当前**搜索窗口,会放宽(BUG-572:±15 → ±30 → ±60 → ±120)。拿它当「最初」去减,一次放宽会被报成一次收窄。这条写进了 `rectification-timeline-scale.ts` 的模块注释,免得下一个人「顺手补上」。 缺口已写进 `BLOCKED.md`(顶部新条目),含解除条件:投影里加一个记**开场**窗口、放宽时不跟着变的宽度/进度字段。`rectification-timeline-scale.ts` 已留好接入位置。**收窄进度这句话服务端照旧写进旁白,用户仍看得到,只是不在常驻条上——不是回归。** 让步顺序第 6 条(T7.2 退到三段)**未触发**,收敛成两段。 --- ## 4. 被改断言:原值 / 新值 / 原因 两处,都在测试文件里就地写了三行注释。 | 文件 | 原值 | 新值 | 原因 | | --- | --- | --- | --- | | `frontend/tests/rectification-board-model.test.ts`("board shows a north-indian chart and folds 换升时刻 by default") | `assert.match(html, /换升时刻 · 2 个/)` | `assert.match(html, /候选分钟/)` + `/2 个时刻/` + `/换升时刻/`,另加 `/参数与口径/` | D10 五段收敛成两段,「换升时刻」从 summary 降为段内组标题,计数移到「候选分钟」的 summary。计数仍是 `grouped.length`,仍默认折叠。**没有弱化**:原来断言 1 个字符串,现在断言 4 个 | | `frontend/tests/rectification-agentic-entry.test.ts`("the natal house table is a live board beside the chat, not a message") | `snapshot` 切到 `"{scoringMinutes.length > 0 \|\| displayMinutes.length > 0"`,并断言 `= 0)` / `assert.ok(end > start)` 的 `sliceBetween()` 辅助函数,标记改名会直接报错并打印找不到的那个标记。 新增测试文件 `frontend/tests/rectification-range-bar-20260916.test.ts`,10 项:同源锁、采用标签与边界、无值不猜、计数来自投影、**无收窄进度口算**、一行四读数与固定条高、窄屏不隐藏、两段收敛不丢块、分钟行行为不变、折叠默认收起且可达。测试总数 3350 → 3360,未降。 --- ## 5. 环境缺口(不得写成通过) 1. **「窄屏 `.is-compact` 下常驻条的实际可见性」——无 Chrome、无登录态,真机未验。** 这是本轮的核心收益,按任务书要求写成缺口。 **CSS 上确认到的事实(不等于真机通过):** `.rectification-workspace.is-compact` 把 `.rectification-workspace__chat` 与 `.rectification-board-overlay` 都放进 `grid-area: 1 / 1` 叠起来;`__chat` 在窄屏保留三行栅格,第一行就是常驻条;overlay 只在 `boardOpen` 时渲染,默认关闭。全表扫过一遍,**没有任何规则**在 `.is-compact` 下对 `.rectification-timeline` 写 `display: none` / `visibility: hidden` / `height: 0`;这条写成了测试断言(遍历所有 `.is-compact ... .rectification-timeline` 规则块)。 **真机仍需确认的:** 四项读数在 375px 实机字体下是否真的一行放得下(算下来约 269px,容器约 343px,留有余量,但中文字宽随字体变);被裁时是不是先裁「已答 N 题」;以及贴底跟随在条变宽后仍然正常(条高未变,理论上不受影响)。 2. **盘面两段折叠的键盘走查未做。** 静态上每块仍是 ``、默认收起、无假按钮;「Tab / Enter 依次展开两段并进到最深一块」需要真人按一遍。 3. **无 Docker。** `npm run test:db` 未跑。本轮未动表、未动迁移。 4. **本轮未推送**(按任务要求)。staging 部署与 `/api/health` 的 `deployment.gitCommit` 核对不在本轮。 前两条建议并入 `docs/testing/` 的校正走查清单;本轮未新建清单文件,避免与 R4–R8 其它轮次的清单重复。 --- ## 6. 改动范围 只碰了这些,没有越界到 R5/R6/R8 的地盘(`.chart-page-*` / `.ephemeris-*` / `.report-center-*` / `.personal-report-*` / `.settings-*` / `.membership-*` 一个字符未动): | 文件 | 改了什么 | | --- | --- | | `frontend/src/components/rectification-timeline.tsx` | 读数行加代表分钟与已答题数两个 ``;轴的 `aria-label` 跟着补齐 | | `frontend/src/lib/rectification-timeline-scale.ts` | 视图加 `workingLabel` / `answeredLabel`;新增 `workingMinuteLabel()` / `answeredProbeLabel()`;模块注释写明为何没有收窄进度 | | `frontend/src/lib/rectification-candidate-result.ts` | 加 `answeredProbeCount` 字段与 `parseAnsweredProbeCount()` | | `frontend/src/components/rectification-agentic-chat.tsx` | 给 `buildRectificationTimeline()` 多传三个参数;import `workingRectificationTime` | | `frontend/src/components/rectification-board.tsx` | 五段折叠收敛成两段;加 `hasMinuteDetail` / `hasParameterDetail` | | `frontend/src/app/globals.css` | `.rectification-timeline__readout` 改 nowrap + 新增 `__working` / `__answered`;`.rectification-board__group`;盘面嵌套 `.technique-audit` 的间距;`.is-compact` 规则加注释 | | `frontend/DESIGN.md` | §10 重写(四项读数、只增宽不增高、代表分钟上条的理由与同源要求、收窄进度为何不在条上、砍掉清单加两行);§12 重写(两段折叠表) | | `BLOCKED.md` | 顶部新增收窄进度字段缺口 | | 三个测试文件 | 见 §4 | `technique-audit-disclosure.tsx` **未改**——它是普通聊天也在用的共享组件,两个 `
` 原样嵌进「参数与口径」里。 --- ## 7. 一件必须交代的事故:共享 stash 栈 量首屏 gzip 需要基线构建,本会话用了 `git stash push -u` → 构建基线 → `git stash pop`。**这是错的**:git 的 stash 栈是整个仓库共用的,不按 worktree 分。同一时刻 R5(`cend-chart-eph`)和 R6(`cend-report`)两个会话也在 stash。结果是我的 `pop` 弹出的不是我自己的 stash,而是 R5 的 chart-page / ephemeris 改动(9 个文件),落进了我的 worktree。 处理:先把 R5 的改动原样备份成 patch,再 `git stash push -u -m "RECOVERED cend-chart-eph work..."` 整份放回栈上,一个字符没丢。查 `git log -g refs/stash` 时发现 R6 会话也对我的 stash 做了同样的事(`RECOVERED cend-r7-wip ...`),互相救了回来。我自己的改动最后用 `git diff ` 生成 patch、`patch -p1` 打回,未跟踪的新测试文件用 `git show :` 取回,11 个文件与 stash 的 diffstat 逐行一致,随后立刻提交,不再留在栈上。 **给后面轮次的教训:多 worktree 并行时不要用 `git stash`。** 要基线构建就另开一个 worktree 或临时 commit,别碰共享栈。 --- ## 8. 最终数字 | 项 | 基线 | 本轮 | 结论 | | --- | --- | --- | --- | | `tsc --noEmit` | 0 错 | 0 错 | 通过 | | `npm run lint` | 0 error / 118 warning | 0 error / 118 warning | 通过 | | `npm test` 总数 | 3350 | 3360(+10) | 未降 | | `npm test` 失败 | 31 | 31 | 清单逐条一致,`diff` 无差异 | | `next build` | exit 0,`/` `○ Static` | exit 0,`/` `○ Static` | 通过 | | 产物 CSS gzip | 39,017 B | 38,973 B | −0.11%,在 ±2% 内 |