Files
Jyotisha/docs/tasks/PROGRESS-cend-rectification-20260916.md
T
Jesse_ChenandClaude Opus 5 ca0204984b
Independent Staging Quality Gate / validate (push) Canceled after 3m14s
Independent Staging Quality Gate / publish (push) Canceled after 0s
feat(rectification): 区间常驻条补代表分钟与已答题数,盘面五段折叠收成两段
R7 / 决策 D10。任务书 docs/tasks/TASK-cend-surfaces-claude-alignment-20260916.md。

T7.1 常驻条。任务书 E9 说区间只在盘面标题行,实际读代码:对话区顶部
已有常驻条 RectificationTimeline 且已显示区间与宽度,窄屏 .is-compact 下
也可见。真正只存在于 .rectification-board__clock 的是代表分钟——窄屏盘面
是默认关闭的 overlay,代表分钟就此看不见。所以没有另造第二条(会撞固定条
高这条正确性约束),而是在既有条上补齐缺的两项:

  05:07–05:09 · 3 分钟 · 代表分钟 05:08 · 已答 6 题

代表分钟与盘面标题行是同一次 workingRectificationTime(result) 调用,一个
值两处用,不是两条推导路径。已答题数逐字读服务端的
inference_state.answered_probe_count,投影没给就整项不显示,不补「已答 0 题」。
条上不写确认,边界句仍只在交付/采用旁白里出现一次。读数只增宽不增高:条高
在滚动容器外,变高会让贴底读者的内容无声滑出视野。

收窄进度未做,触发让步顺序第 5 条。没有这个字段(服务端只把这句话拼进旁白
正文),且前端自己减会算错——candidate_range 是当前窗口且会放宽(BUG-572),
拿它当「最初」相减会把一次放宽报成一次收窄。缺口与解除条件写进 BLOCKED.md。

T7.2 盘面折叠。实际有 8 个 details(任务书记的五处漏了交界节奏、错标了两块),
按内容归成「候选分钟」(换升时刻 + 次级候选)与「参数与口径」(宫位表、确认门、
本轮技法、两段本命上升、经历与大运对照含双轨一致性、交界节奏)。每块保留原有
渲染条件,engineMeaningToDisplayCopy() 仍是 5 处,逐分钟行的 is-changed /
当前时间 一字未改。共享组件 technique-audit-disclosure.tsx 未动。

顺带修掉一个静默失效的断言:rectification-agentic-entry 用
board.slice(indexOf(A), indexOf(B)),B 改名后 indexOf 返回 -1,slice(i, -1)
会悄悄退化成几乎整份文件,底下四条 doesNotMatch 照样绿。已换成带越界断言的
sliceBetween()。两处改动的断言都在测试里写了原值/新值/原因。

tsc 0 错;lint 0 error / 118 warning(同基线);npm test 3350 → 3360,失败
仍是 31 条无 Docker 用例且与基线逐条一致;next build exit 0,/ 仍 ○ Static;
产物 CSS gzip 39,017 → 38,973 B(−0.11%)。

窄屏 .is-compact 下常驻条的实际可见性无 Chrome 无法验证,已按任务书写成环境
缺口,未标通过;CSS 上已确认无任何规则在该状态下隐藏该条,并写成测试断言。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-16 06:44:49 +00:00

188 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.
# 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:0705: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 个** `<details className="technique-audit">`:本命宫位表、唯一分钟确认门、本轮技法、换升时刻(内嵌显示层)、两段本命上升、经历与大运对照、交界节奏。任务书把 `: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` / 「当前时间」/「已更新」四处 |
| 键盘可达性不弱于改前 | 通过(静态) | 每块仍是真 `<summary>`,无 `role="button"`、无 `onClick` 开合;测试断言 `<summary>` 数 ≥ 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"` 等高空骨架;新增元素都是静态 `<span>` |
| 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"`,并断言 `<RectificationHouseTableView` 在 snapshot 里 | `snapshot` 切到 `"{hasMinuteDetail && ("`;宫位表改在 `parameterSection` 切片里断言;新增 `assert.match(snapshot, /<VedicChartSvg/)``assert.doesNotMatch(snapshot, /<RectificationHouseTableView/)` | 本命宫位表随 D10 从 `.rectification-snapshot` 移进「参数与口径」,旧终点标记已不存在。测试原意(宫位表在盘面、不在消息流)原样保留 |
**第二条附带修掉一个静默失效。** 旧写法是 `board.slice(indexOf(A), indexOf(B))`:标记 B 改名后 `indexOf` 返回 `-1``slice(i, -1)` 会**静默**退化成「从 A 到文件倒数第二个字符」,底下四条 `doesNotMatch` 就变成在整份文件上断言,测试照样绿。实测确认过:只改组件不改测试时,这条测试**假通过**。已改成带 `assert.ok(start >= 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. **盘面两段折叠的键盘走查未做。** 静态上每块仍是 `<summary>`、默认收起、无假按钮;「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` | 读数行加代表分钟与已答题数两个 `<span>`;轴的 `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` **未改**——它是普通聊天也在用的共享组件,两个 `<details>` 原样嵌进「参数与口径」里。
---
## 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 <stash-sha>` 生成 patch、`patch -p1` 打回,未跟踪的新测试文件用 `git show <untracked-parent>:<path>` 取回,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% 内 |