From 5604c11bb4872e1472da0a0282aab80a22afb4dc Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 30 Sep 2026 16:36:18 +0800 Subject: [PATCH] docs(tasks): report language switch leaves the old edition on screen (BUG-1126) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE --- docs/BUG_HISTORY.md | 14 +++ docs/tasks/README.md | 1 + ...K-report-language-switch-stale-20260930.md | 101 ++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 docs/tasks/TASK-report-language-switch-stale-20260930.md diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 8e756351..4927d05a 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -15031,3 +15031,17 @@ - 防复发:准备阶段里「还不知道」必须是 pending,不得写成 unavailable;首页揭幕后出现的文字一律带进入动效。 - 相关记录:BUG-1040(暖快照)、BUG-1104 / 1109 / 1110(今日句库与预取) - 修复版本:分支 `codex/loading-motion-20260930` + +## BUG-1126 | 报告阅读页切换中文 / English 后正文不换 + +- 状态:investigating(根因已复现并验证修复方向;待执行,任务书 `docs/tasks/TASK-report-language-switch-stale-20260930.md`) +- 首次发现 / 最近更新:2026-09-30 / 2026-09-30 +- 影响面:`frontend/src/components/personal-report/personal-report-page.tsx`(`PersonalReportPage` markdown-ready 分支);所有带英文版的新报告。 +- 现象:点 English 后地址栏变为 `?lang=en`、开关显示已选中,但正文和目录仍是中文;带 `?lang=en` 打开后点中文,正文仍是英文。导出的英文版正常。桌面与 iPhone 均复现。 +- 触发条件:报告同时带核对表(`factTables` / `factTablesEn`,真实报告都有),在阅读页切换语言。 +- 根因:`PersonalReportMarkdownView` 与 `ReportFactTables` 是同级元素,key 都是 `shown`,发生重复;React 清不掉旧节点,旧语言正文留在最上面,新语言正文追加在后面(Claude 在 staging 线上 JS 上复现:正文区块 1→2→3 份,开发版报 `Encountered two children with the same key`)。响应里没有核对表时不复现,所以只测组件的用例没有拦住。 +- 修复:待执行(两处 key 改成互不相同;Claude 已在复现环境验证可行)。 +- 验证:待执行(整页切换回归测试,先红后绿)。 +- 防复发:同一父级下按语言重新挂载的多个子元素,key 必须带各自前缀;语言切换要有整页测试,数据带齐核对表。 +- 相关记录:BUG-1106~1108(跨语言保留章节);引入提交 `b2ad772e` +- 修复版本:— diff --git a/docs/tasks/README.md b/docs/tasks/README.md index b440064d..d81a3ded 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -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 + 整页切换回归测试 | 待领取 | — | ## 命名与归档 diff --git a/docs/tasks/TASK-report-language-switch-stale-20260930.md b/docs/tasks/TASK-report-language-switch-stale-20260930.md new file mode 100644 index 00000000..3bd4c33d --- /dev/null +++ b/docs/tasks/TASK-report-language-switch-stale-20260930.md @@ -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 + +{state.calculationCharts && } +{factTables && } +``` + +两个同级元素的 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 +```