docs(tasks): report language switch leaves the old edition on screen (BUG-1126)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
Jesse_Chen
2026-09-30 16:36:18 +08:00
co-authored by Claude Opus 5.5
parent da3ed2f3ab
commit 5604c11bb4
3 changed files with 116 additions and 0 deletions
+1
View File
@@ -382,6 +382,7 @@
| `TASK-rectification-midnight-final-grok-20260921.md` | `PROGRESS-rectification-midnight-date-anchor-20260920.md` | **日期锚点最终验收接续(Grok)**:测试契约复验、扩展身份四模块、真实新 freeze 与 20/900、final-3 全门。不 commit/push/deploy。 | **已验收**(并入 `b85c4a68`,见下一行):隔离 Linux 门通过(tsc/lint0、前端3649、DB56、quick、AA 21/21、0 新增 Python 失败);四既存失败仍在;真人缺口见 BLOCKED | `b85c4a68`(分支 `codex/rectification-midnight-date-anchor-20260920` 提交后合入 staging) |
| `TASK-rectification-midnight-date-anchor-20260920.md` | `PROGRESS-rectification-midnight-date-anchor-20260920.md` | **跨午夜的日期锚点与簇跨度(BUG-982 + BUG-983 合并一单)**:两条同根——线上契约只传钟点、日期不在契约里,后端按钟点大小**猜**哪端跨日。**BUG-983 是静默算错盘**:`_candidate_datetimes()` 把起始钟点无条件绑在申报日期上,前端零补偿。Claude 实测(申报日 2000-06-15):申报 `00:10` → 候选 `06-15 23:55`…`06-16 00:25`,**申报分钟本身落在 `06-16 00:10`,+1 天**;`00:02` 同样 +1 天;`12:00` 对照正确。受影响:申报落在午夜后半径内者(±15≈1.0%、±30≈2.1%、±60≈4.2%、±120≈8.3%);`late_night`(23:00–03:59) 更重,其 `00:00`–`03:59` 共 240 分钟**全部落在用户没申报过的日期上**。**BUG-982 影响面比原记录大**:同缺陷在链路上出现三处(`_cluster_span` / `unionStillValidRange` / `indistinguishableWidthMinutes`),原记录只写了 Python 两处。实测跨午夜簇 `23:58/23:59/00:00` 报告宽度 **1440 分钟**(真实 3)、`23:50/23:55/00:05` 报 **1431**(真实 16);该链经 `reportWidth` 直达 **交付卡「范围 X 分钟」**,深夜用户会看到候选其实差几分钟却被告知范围一千多分钟。**与 BUG-981 的关系**:981 让每个候选按自己日期算 Dasha 是对的,但 983 给的日期本身就错,**983 不修则 981 在申报近午夜的场景等于没上线**。**产品 2026-09-20 三点全部拍板**:D1 `late_night` **先问用户「午夜前还是午夜后」,答不上退「同日两段」`[D 00:00–03:59] ∪ [D 23:00–23:59]`,一律不得跨到 `D+1`**(连锁范围:退路产生**不连续候选集**,枚举/聚类/簇跨度/交付区间/宽度/交付卡都要支持两段,**不得用「取两段最小到最大」糊成一段**——那正是 982 的同型错误);D2 **允许窗口跨到前一天**,但交付层必须说明「若落在 23:5x 则出生日期是前一天」,不得静默改用户的出生日期(依据 Part B B4 真值覆盖率优先);D3 **加窗口相对序号**,排序/取首尾/算跨度一律用序号、钟点只作展示 —— 序号入契约会改候选身份与缓存指纹,**必须核对 BUG-984 的 `scoringIdentityMatches` 并确认历史结果只读不被重标**。硬红线:不调打分常数/确认门阈值;不得以排除真值换窄宽度;不得只修 `_cluster_span` 就宣称 982 已修;非跨午夜必须逐位不变且用同机 A/B(不得写死跨机浮点哈希,见 BUG-985)。沿用 BUG-982/983,不新开号 | **已验收通过并合入 staging**(2026-09-21,`b85c4a68`);已部署 staging(run 2831);真人验收待完成 | Claude 独立复核(用当初复现两条 Bug 的同一探针重跑):**BUG-983** 新契约下申报 `00:10`/`00:02` 的申报分钟落在**申报日**(+0 天),`late_night` 300 个候选**全在同一日**、两段;**BUG-982** 跨午夜 3 分钟簇 → **3 分钟**(原 1440),同日两段 → **300 分钟**(未糊成 1440)。三处(`decision_policy.py`/`credible-range.ts`/`candidate-plateau.ts`)均已改。**非跨午夜分数同机 A/B 6/6 逐位相同**;新测试在基线 6 条红、修复后全绿;Python `test_rectification_*` 基线 216 → **272 passed / 0 failed**;`tsc --noEmit` 退出 0。红线全清:打分常数零改动、`status=not_ready`、coverage 0、`holdout_passed()` False、Skill 未动、`official_eval_trial_count` 仍 0。D1 追问 4 选项含「不知道」「跳过」,均退同日两段、从不去 `D+1`,明写不计分可跳过;D2 交付层显示「(前一天)」;D3 用 `window_index`/`segment_index`,`interval_union_width` 按段求和(注释 “never fill a declared segment gap”)。超出要求的设计:`candidate_intervals` 拒绝重复钟点;engine-client 对 dated 路径 fail-closed 抛 `engine_invalid_candidate_decisions`。环境缺口:3 条前端测试在 Claude 机器红,但**同样的 `@/` 别名错误在既有测试上一模一样复现**(`ephemeris-route`、`api-service-unavailable`),非回归;无 Docker 故 `test:db` 未跑。**遗留待确认**:无 `cluster_intervals` 的行仍走旧按钟点算法(宽度仍 1440),应确认除修复前旧缓存外没有别的生产路径会产生无日期行 |
| (直接执行,无任务书) | `PROGRESS-notice-loading-polish-20260930.md` | 性别只在档案(1119)、星盘气泡频闪与表格圆角(1120)、全站顶部浮层提示(1121/1122)、我的报告提速与分页(1123/1124)、宫位点亮等待与今日一句不跳(1125) | **已验收,推 staging**;真机清单 `docs/testing/notice-loading-polish-20260930.md` 待产品 | 合并分支 `codex/polish-integrate-20260930` |
| `TASK-report-language-switch-stale-20260930.md` | — | 报告阅读页切换中文 / English 后正文不换:正文与核对表同级重复 key(`key={shown}`),旧语言节点残留(BUG-1126);改 key + 整页切换回归测试 | 待领取 | — |
## 命名与归档
@@ -0,0 +1,101 @@
# TASK · 报告阅读页切换语言后正文不换 · 2026-09-30
> 执行方:coding agent。验收:Claude。
> 分支 `codex/report-language-switch-stale-20260930`,worktree `.worktrees/report-language-switch-stale-20260930`,基线 `origin/staging`。
> BUG 编号:**BUG-1126**(Claude 写单时已登记为 investigating;开工时核对 `docs/BUG_HISTORY.md` 最大号,若已被占用顺延并同步改本单)。
> 纯前端改动,不需要 AGENTS §9 预检。
## 0. 基线
- 写单时 `origin/staging` = `da3ed2f3`(staging `/api/health` `deployment.gitCommit` 同为 `da3ed2f3`)。
- 开工前先 `git fetch origin --prune`,以实测 `origin/staging` 为准,写进 `docs/tasks/PROGRESS-report-language-switch-stale-20260930.md`。
## 1. 事故实证
产品 2026-09-30 在 staging 实测,桌面 Dia 与 iPhone 两台设备都能复现:
- 进报告页后点「English」:地址栏会变成 `?lang=en`,开关也显示 English 已选中,但正文、目录(标题仍是「目录」)还是中文版。
- 直接带 `?lang=en` 打开:正文是英文;这时点「中文」,开关会变,正文仍是英文。
- 导出面板里的「中文 / English」和导出的英文文件都正常,说明英文版数据已经完整送到浏览器。
Claude 复现情况(staging 线上 JS + 无头 Chrome;报告接口用虚构出生资料的真实引擎输出替换,也伪造了登录态):
| 响应里有什么 | 点切换后 |
| --- | --- |
| 只有 `longformMarkdown` / `longformMarkdownEn` | 正常:正文区块始终 1 份,目录变 Contents |
| 再加上 `factTables` / `factTablesEn`(真实报告都有) | **复现**:`.personal-report-md-layout` 1 → 2 → 3 份,旧语言那份一直留在最上面;开发版 React 报 `Encountered two children with the same key` |
## 2. 根因
`frontend/src/components/personal-report/personal-report-page.tsx` → `PersonalReportPage` 的 markdown-ready 分支,在 `.personal-report-reader-body` 下同级渲染:
```tsx
<PersonalReportMarkdownView key={shown} … />
{state.calculationCharts && <ReportCalculationCharts … />}
{factTables && <ReportFactTables key={shown} … />}
```
两个同级元素的 key 都是 `shown`("zh" / "en"),发生重复。每次换语言,React 按 key 对齐时旧节点清不掉,旧语言的正文留在页面上,新语言的正文追加到后面,所以读者看到的一直是第一次渲染的那个语言。开关读的是同一个 `shown`,所以只有开关在变。
- 引入提交:`b2ad772e`(英文版上线,两处 `key={shown}` 同时加入)。
- 既有测试没拦住:`frontend/tests/report-english-edition.test.tsx` 只单独测了 `ReportActions`(开关)、`classifyReportEnvelope` 和导出面板,没有把整页连同核对表一起渲染再切换。
**Claude 已验证修复方向**:把两个 key 改成互不相同(例如 `markdown-${shown}` / `facts-${shown}`)后,同一复现环境里切换一次就换成英文、正文区块 1 份、0 个含汉字的章节。验证后已还原,没有提交任何代码。
## 3. 决策记录
1. 产品 2026-09-30 反馈「切换不管用」。修复只动 key,不改英文版的生成、存储、接口,也不改切换的交互。
2. 沿用 `TASK-report-english-edition-20260929.md` 决策 6:切换只换正文、目录、图盘标签、核对表,应用外壳保持中文。本单不推翻任何既有决策。
## 4. 硬红线
1. 只改 `personal-report-page.tsx` 里这两处 key,加测试和记录;不顺手重构阅读页,不动 `PersonalReportMarkdownView` 的懒渲染和保留章节逻辑(BUG-1106~1108)。
2. 不改 `report-english-edition.test.tsx` 的既有断言;如果必须改,写「原值 / 新值 / 原因」三栏。
3. 测试 fixture 用仓库里已有的真实引擎虚构盘 golden(`report-reader-main-fictional*.json`、`report-density-fictional-engine.json`),不手造形状(AGENTS §7-4)。
4. 不动数据库、不升级依赖、不改 workflow。
## 5. 任务分解
### T1 · 去掉重复 key
`PersonalReportMarkdownView` 与 `ReportFactTables` 的 key 改成互不相同,且仍随语言变化(保证换语言时两者都会重新挂载)。同一父级下其他带 key 的子元素一并检查。
验收:`personal-report-page.tsx` 中 `.personal-report-reader-body` 的直接子元素没有重复 key。
### T2 · 整页切换回归测试(先红后绿)
新增测试:渲染 `PersonalReportPage`,mock `fetch` 返回 ready 报告,同时带 `longformMarkdown`、`longformMarkdownEn`、`factTables`、`factTablesEn`(用 golden 组装),然后:
1. 以 `initialLanguage="zh"` 打开 → 点 English → 断言正文区块只有 1 份、目录标题是 Contents、第一个 h2 是英文、正文不含汉字;
2. 再点中文 → 正文区块只有 1 份、目录是「目录」;
3. 以 `initialLanguage="en"` 打开 → 点中文 → 同样断言。
验收:在未修复的基线上这条测试是**红**的(进度记录贴失败输出),修复后是绿的。如果现有测试台(`react-client-lifecycle-test-support`)跑不了整页,说明原因并换可行的方案,不得退回只测组件。
### T3 · 记录
- `docs/BUG_HISTORY.md` 的 BUG-1126 补齐修复、验证、防复发,状态改为 resolved(待部署、真机待产品)。
- `CHANGELOG.md` 加一条:报告阅读页中英切换恢复正常。
- `docs/testing/report-english-edition-20260929.md` 追加一条真机步骤:先中文打开,切 English,再切回中文,每次正文与目录都要跟着变,正文不能出现两份。
- 本单不改 UI 外观,`DESIGN.md` 不用更新。
## 6. 验收口径
- `./node_modules/.bin/tsc --noEmit` 0 错;`npm run lint` 0 error。
- `npm test` 失败清单与基线逐条一致,总数不少于基线加新增条数。
- `next build` 通过,`/` 仍是 Static;首屏 gzip 在 ±2% 内(预期不变)。
- 交付:快进推 `origin/staging`,核对远端 SHA;部署后 `/api/health` 的 `deployment.gitCommit` 等于该提交。
## 7. 让步顺序
时间不够时按这个顺序保:T1 → T2 → T3。T1 和 T2 必须一起交付,不接受只改代码不加回归测试。
## 8. 开工前置命令
```bash
cd /workspace/Jyotisha && git status -sb | head -1
git fetch origin --prune
git worktree add -b codex/report-language-switch-stale-20260930 .worktrees/report-language-switch-stale-20260930 origin/staging
cd .worktrees/report-language-switch-stale-20260930/frontend && npm ci # 各 worktree 需自己装依赖;Node 22 在 /exec-daemon/node
grep -o "BUG-[0-9]\{3,4\}" ../docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1
```