diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b408866..2944c04d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# 印度占星 Skill 更新日志
+## 2026-09-29 — 个人报告同时有中文版和英文版(待验收)
+
+- 新生成的报告自动带中英两版,不用另外操作。报告页右上角「中文 / English」一键切换,刷新或把链接发给别人时停在同一个版本(地址里带 `?lang=en`)。
+- 英文版不是机器翻译:同一次计算的结果按英文再排一遍,度数、日期、分数与中文版逐项一致;术语用印度占星通行的英文写法。英文版里如果还有一个汉字没翻,这一版就不发,只给中文,报告照常完成。
+- 导出时导出当前看的那一版,英文文件名带 `-EN`。看中文时导出框会提示:要拿去问 ChatGPT、Claude 等 AI,建议切到 English 再导出,并给一个「切到 English」按钮。
+- 这次更新之前生成的报告只有中文版,不显示切换;导出框里提示「重新生成即可获得中英两版」。
+- 瑜伽规则库 477 条补了英文说明(效果、组合条件、强度)。
+- Skill 版本不 bump(Skill 文本未改)。不改数据库结构(英文版存在原有的计算快照里)。
+
## 2026-09-29 — 星盘展示去引擎、能力列收口与图标说明(本地实施完成,未发布)
- 星盘参数不再展示引擎品牌;岁差、交点与准确性边界保留。
diff --git a/CONTEXT.md b/CONTEXT.md
index b4a02c2d..0a745f17 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -36,6 +36,10 @@ _避免使用_:完整聊天记录、错误消息
用户针对一条完整 Agent 回复提交的当前正向或负向质量判断。评价属于具体回复,而不是整个 Agent 会话。
_避免使用_:会话评分、点赞记录
+**报告语言版本**:
+同一份个人报告的中文版与英文版,由同一次引擎计算、同一个数据包分别渲染,数字逐项一致。英文版是从属版本:缺失或未通过检查时报告仍以中文交付;英文版上线前生成的报告只有中文版。
+_避免使用_:翻译版、机翻、英文报告(指另一份报告时)
+
**不满意原因**:
负向回复评价附带的一个或多个原因分类,可包含用户补充说明。
_避免使用_:投诉、差评文本
diff --git a/docs/tasks/PROGRESS-report-english-edition-20260929.md b/docs/tasks/PROGRESS-report-english-edition-20260929.md
new file mode 100644
index 00000000..c9adbd57
--- /dev/null
+++ b/docs/tasks/PROGRESS-report-english-edition-20260929.md
@@ -0,0 +1,35 @@
+# PROGRESS · 报告中英两版 · 2026-09-29
+
+> 任务书:`docs/tasks/TASK-report-english-edition-20260929.md`。执行:Claude(直接执行模式)+ 两个 fork 子代理(词典 E3、引擎 E1/E2),由 Claude 合并与复验。
+> 分支 `codex/report-english-edition-20260929`,基于 reader-polish 合入后的 `origin/staging` = `5f84637e`。
+
+## 交付对照
+
+| 编号 | 结论 | 说明 |
+|---|---|---|
+| E1 引擎两种语言 | 完成 | `/api/professional_report_reference` 接受 `languages: ["zh","en"]`;只算一次 packet,中文照旧渲染,英文由深拷贝 + `report_language='en'` 渲染;英文含汉字或渲染出错时不给 `markdown_en`,改给 `english_unavailable`。不传 `languages` 时响应与改前逐字节相同 |
+| E2 英文渲染 | 完成 | 新模块 `scripts/pl9_reader_english.py` + `pl9_reader_english_terms.py`(固定英文词表);`pl9_reader_export` 只在 `reader_main` 入口按语言分派,中文路径不变;`_dignity_label` 英文取括号里的英文 |
+| E3 词典 | 完成 | `references/yoga_rules.json` 477 条规则补 `effects_en` / `combo_template_en` / `strength_en` / `source_en` / `accuracy_note_en`(去掉 `_en` 键后与原文件相同);`yoga_engine.py` 带出 `combination_en` 等孪生字段,含约 20 处写死的中文默认模板;`reader_dasha_applicability` 加 `language` 参数,中文输出用 201 个合成 packet 逐字节比对不变 |
+| E4 生成链 | 完成 | 新模块 `personal-report-english-edition.ts::acceptEnglishEdition`:英文必须零汉字、原文不含公开投影要删的词、英文核对表零汉字,否则只记原因码。快照升 `version: 2`(`english` 与 `englishUnavailable` 二选一),v1 旧快照照常读。**不动数据库**:英文版放在原有的 `personal_report_sections` 快照 payload 里(该表 payload 无大小约束) |
+| E5 路由 | 完成 | `GET /api/reports/:id` 多给 `englishEdition`(ready / unavailable / legacy)、`longformMarkdownEn`、`factTablesEn`;读英文出错一律视为 unavailable,不影响中文 |
+| E6 阅读页 | 完成 | `?lang=en`(服务端页面读取,`history.replaceState` 写回);切换只在 `englishEdition === "ready"` 时出现;正文 / 目录 / 核对表 / 分盘核对 / 图例随语言;应用外壳保持中文;`buildLongformOutline` 的导航与摘要正则补英文对应 |
+| E7 导出 | 完成 | 导出当前语言,英文文件名 `Personal-Report-<日期>[-excerpt]-EN`;AI 提示 + 「切到 English」;旧报告 / 英文失败各一句说明;文件大小按当前语言计算 |
+| E8 文档 | 完成 | `DESIGN.md`、`VOICE.md`、`CONTEXT.md`(报告语言版本)、`CHANGELOG.md`、真机清单 |
+
+## 三条红线的证据
+
+| 红线 | 证据 |
+|---|---|
+| 中文逐字节不变 | `tests/test_report_english_edition.py`:3 个虚构盘,中文与参照提交渲染逐字节相同;不传 `languages` 的 API 响应相同;前端 `assembleReportFactTables(engine, "zh")` 与默认输出 deepEqual、`projectOrdinaryReportMarkdown(x)` 与 `(x,"zh")` 相同 |
+| 英文零汉字 | Python:子代理用 75 个虚构盘验证零汉字;golden `report-reader-main-fictional-en.json` 汉字数 0;前端再验一遍(生成链与阅读页各一次) |
+| 数字一致 | golden 中英两版行对齐(各 1,834 行),逐行数字序列相同,唯一例外是一行瑜伽**名称**:英文名「Kalatra Dosha (7th Lord in 6/8/12)」写出了宫位,中文名「婚姻障碍(7宫主在凶宫)」没有——这是标签,不是计算值,已在测试里点名固定 |
+
+## 既有断言改动(原值 / 新值 / 原因)
+
+| 文件 | 原值 | 新值 | 原因 |
+|---|---|---|---|
+| `frontend/tests/personal-report-view.test.ts`「the report TOC is a persistent right-hand rail…」 | `` | `` | E6:目录标签随语言;目录仍只从同一 outline 构建 |
+
+## 验证
+
+(见下方,Claude 实跑)
diff --git a/docs/tasks/README.md b/docs/tasks/README.md
index 4674d301..98141520 100644
--- a/docs/tasks/README.md
+++ b/docs/tasks/README.md
@@ -259,7 +259,7 @@
| `TASK-rectification-scoring-research-20260929.md` | `PROGRESS-rectification-scoring-research-20260929.md` | **打分方法研究(离线)**:R-A 似然比校准权重(leave-one-case-out,KP / Pranapada / D60 作为特征由数据定权重、允许负权重)、R-B 缺席证据(口述时间线覆盖时段内的空白年扣分,必测漏说稳健性)、R-C 精度追问可行性(对已说事件追问月份,算在定向追问 2 条额度内)。判定必须在 v5 上做;有收益才立实现单并按 ERR-110 重冻结 | 脚手架已验收(Claude 09-29:对账 60 case-radius 0 差、JSON 复跑逐字节一致、冻结文件零改动、117 测试全绿;已合入 staging);R-A / R-B / R-C 判定待 v5 | 分支 `codex/rectification-scoring-research-20260929`(BUG-1091 investigating) |
| `TASK-rectification-typed-event-scoring-research-20260929.md` | `PROGRESS-rectification-typed-event-research-20260929.md` | **打字经历按选择题规则计分(离线研究,不上线)**:计分通道不对称 + 已入账年份挡题;R0 学业质量题措辞 / 年精度显示成 1 月(冻结文件,需重新冻结) | 已验收(Claude 2026-09-29 合并验收:全量前端 4302/24 与基线同 24 条环境失败、tsc 0、lint 0 error、`/` Static、gzip +0.16%、快速门 pytest 1001 passed、两份回放复跑一致),待部署核对 | BUG-1088、1089 |
| `TASK-report-reader-polish-20260929.md` | `PROGRESS-report-reader-polish-20260929.md` | **报告页打磨**:生成入口挪进页面主体(删标题栏按钮)、详情页到底部按钮(懒渲染一次到底)、导出按钮带文字、目录一级/二级分层、去掉「字段」与 RL/NL 缩写表头、状态列同义重复去重、报告表格淡底色、分块导出显示真实文件大小 | Claude 直接执行并自验(tsc 0、lint 0 error、全量 fail 与基线同 24 条、`/` Static、gzip +7 B、快速门 Python 1000 passed),已推 staging,真机欠 | BUG-1092~1094 |
-| `TASK-report-english-edition-20260929.md` | — | **报告中英两版**:同一次引擎计算渲染 zh/en 两遍(不用模型翻译),瑜伽库 406 条补英文、模板与前端表头英文化;中文逐字节不变、英文零汉字、两版数字序列一致;阅读页 `?lang=en` 切换,导出当前语言 + 「问 AI 建议导出英文版」提示;旧报告不补英文 | 待领取(依赖 reader-polish 合入) | — |
+| `TASK-report-english-edition-20260929.md` | `PROGRESS-report-english-edition-20260929.md` | **报告中英两版**:同一次引擎计算渲染 zh/en 两遍(不用模型翻译),瑜伽库 477 条补英文、模板与前端表头英文化;中文逐字节不变、英文零汉字、两版数字序列一致;阅读页 `?lang=en` 切换,导出当前语言 + 「问 AI 建议导出英文版」提示;旧报告不补英文 | Claude 直接执行(含两个 fork 子代理),自验中 | — |
| `TASK-serif-headings-20260928.md` | `PROGRESS-serif-headings-20260928.md` | **全站标题改用自托管宋体、正文保持黑体**:产品推翻 BUG-737「CJK 不用衬线」结论(保留「声明的字体必须可加载」「不落系统宋体」两条);Noto Serif SC SemiBold 按通用规范汉字表 6500 字 unicode-range 切片自托管(改名 Jyotisha Serif SC),swap 不 preload;首页宋体流量 ≤300 KB;先于天空封面单 | 已实现待验收(分支 `codex/serif-headings-20260928`,未推送) | 不开新 BUG;BUG-737 追加说明 |
| `TASK-cend-ui-claude-alignment-20260916.md` | `PROGRESS-cend-ui-r1/r2/r3-20260916.md` | **C 端界面向 claude.ai 产品界面对齐(三轮串行 R1→R2→R3,都动 `globals.css`,不得并行)**:根因是 `frontend/CLAUDE_DESIGN.md` 扒的是 **claude.com 营销官网**,它自己在 Known Gaps 里写明 claude.ai 产品界面不在范围内,而 `DESIGN.md:3` 把它当成了产品界面的实现契约。**R1**:`--font-display` 里 Tiempos Headline / StyreneB **从未加载**(无 `@font-face`、`public/` 无字体、`layout.tsx` 只 vendor 了 Inter),中文标题全站落到 **宋体 / SimSun**,波及 20 处含助手回答的 h2/h3(BUG-737);亮色强调色 `#85432f` 与暗色 `#d78064` 不同源,产品拍板亮色换 **Claude coral `#cc785c`**,**易漏点**是 `globals.css:16` 的 `--color-ring` 硬编码在 `@theme inline` 里不跟随 `:root`,另有第四个 `:root` 亮色块(`:4358`)必须同步(BUG-738);`.composer-footer` 常驻 44px + 顶栏 68px + `--composer-reserve` 148px,每屏固定吃掉 216px,模型选择器移进输入框内部、删掉底栏、顶栏收到 46px 并删「分析对象」副标题。**R2**:空状态是营销落地页(hero 卡 + 两张 132px 入口大卡 + 3 列 156px 主题卡),输入框被压在 **800px 以上**内容之下,重排成「问候 + 居中输入框 + 两枚入口 pill + 一排 chip」。**R3**:侧栏两个 `` 拍平成一条「最近」、星盘的两个入口(侧栏分组 + 账户菜单)收敛到一处、删掉逐条助手头像。**决策记录 D3 推翻 DESIGN.md「报告强调色与应用同源」一句**(报告刻意保留深棕)。原型图 https://claude.ai/code/artifact/da275da6-2954-4f50-99aa-32bb8694d38b(三套画面 + 明暗,页面标题就是建议字体栈的实际渲染)。环境缺口:无登录态无 Chrome,四项真机观感留 `docs/testing/`。BUG 段 737–738 | 待领取 | — |
| `TASK-cend-surfaces-claude-alignment-20260916.md` | `PROGRESS-cend-shell-20260916.md`、`PROGRESS-cend-report-20260916.md`、`PROGRESS-cend-rectification-20260916.md`、`PROGRESS-cend-chart-eph-20260916.md` | **次级页面对齐(上一单的续篇,R4→R5/R6,R7、R8 可并行)**:星盘 `/chart`、星历 `/ephemeris`、报告 `/reports` **各是脱离 app 外壳的独立全屏页**,顶部只有一个「返回对话」链接、侧栏整个消失,且三家各写了一套一模一样的 `*-shell`/`*-topbar`/`*-hero` 骨架——与上一单 E5 同根因(营销站 band 结构被套到产品界面)。**R4** 抽只读导航外壳 `AppNavRail`(只用现成的 `GET /api/sessions` + `GET /api/account`,会话行走 `sessionHref` 跳 `/?c=`;**刻意不带**重命名/删除/收藏/归档——那套连着 `Home()` 的乐观更新与回滚,搬过来会撞 useState 增长门禁)。**R5** 星盘五 tab 下划线化 + 参数合表 + 行星表横向滚动;星历日期导航改 `‹ 日期 ›`。**R6** 报告中心卡片网格改行式列表;阅读页加常驻目录。**R7** 生时校正把可信区间从盘面板标题行提成常驻条(窄屏 `.is-compact` 下盘面板是 overlay,现在默认看不到区间),五个 `technique-audit` 折叠块收成两段。**R8** 设置内容区收窄(880px 弹窗里表单铺了 690px)、套餐卡三修饰符收敛成两态。**已解锁**:原挡路的设置单已于 `111b4a84`(BUG-698)合入。**两条不得回退**:BUG-698 的 `@supports (height: 1dvh)` 写法(重复声明回退会被 Lightning CSS 折叠)、BUG-616/617 的报告盘面 grid 实现。默认不占 BUG 号 | **R4–R8 全部已实现并验收合入** | R4:抽出 `AppNavRail`(只读,两个 GET,零写操作)+ `SecondaryShell`,三个次级页并入 app 外壳并删掉各自的 shell/topbar/hero;四个路由渲染标记**完全不变**(`/` `/chart` `/ephemeris` 仍 Static);CSS gzip −0.25%。`/reports/[reportId]` 留给 R6 与目录一起做。两处自身健壮性问题被测试抓到:`usePathname()` 可为 null、`fetch` 可能不存在。差点弄丢 BUG-717 的 eyebrow 文案(已放回)。R8:表单分区收窄到 440px(列表分区不变)、套餐卡三修饰符收敛成互斥的 `is-current` / `is-recommended`,`--highlighted` 删除改为滚动定位;手机端 `order:-1` 改挂 `[data-plan-alias]`(版位不是状态)。测试 3350→3354(净增 4),失败清单与基线逐条一致;`/` 仍 Static;我的干净构建实测 CSS gzip −3 字节。**遗留待产品拍板**:`?plan=` 深链现在完全没有视觉指向,只有滚动位置。R6:报告中心卡片网格改行式列表、阅读页并入外壳并把目录挪到右侧常驻。**任务书 E10 过期**——目录在 `cfcd369d` 就已存在,本轮是挪位置定稿而非从零加。挂外壳带出一个真实打印风险已处理:`.chat-app`/`.chat-panel` 是 `height:100%;overflow:hidden`,裸 `window.print()` 会把九节报告裁成一页,阅读页因此多挂一条只在挂载期生效的 print 样式解锁外壳。「生成中的分节进度」做不了——`REPORT_LIST_COLUMNS` 不返回节数,按 VOICE.md 不许前端编。R7:区间常驻条与盘面折叠收敛。**任务书 E9 也不准确**——对话区顶部早有常驻条 `RectificationTimeline` 且窄屏可见,真正只在盘面标题行的是**代表分钟**;因此没另造第二条,在既有条上补齐代表分钟与已答题数(与盘面同一次 `workingRectificationTime()` 调用)。折叠块实际是 **8 个**不是 5 个。**触发让步顺序第 5 条**:收窄进度未做——服务端无该字段,且 `candidate_range` 会放宽(BUG-572),前端相减会把一次放宽报成收窄,已写进 `BLOCKED.md`。顺带修掉一个**静默失效的旧断言**(`slice(indexOf(A), indexOf(B))` 在 B 改名后变成几乎整份文件,四条 `doesNotMatch` 假通过)|
diff --git a/docs/testing/report-english-edition-20260929.md b/docs/testing/report-english-edition-20260929.md
new file mode 100644
index 00000000..8e3e2081
--- /dev/null
+++ b/docs/testing/report-english-edition-20260929.md
@@ -0,0 +1,15 @@
+# 真机清单 · 报告中英两版(2026-09-29)
+
+对应 `docs/tasks/TASK-report-english-edition-20260929.md`。电脑和手机各走一遍。第 1 步要生成一份**新**报告(会用掉一次生成次数)。
+
+| # | 操作 | 应该看到 | 结果 |
+|---|---|---|---|
+| 1 | 「我的报告」→ 生成报告,等它完成后打开 | 操作栏有「中文 / English」切换,默认选中「中文」;正文和改版前的中文报告一样 | |
+| 2 | 点「English」 | 正文、目录、核对表全部变成英文,页面里找不到一个汉字(侧栏、标题栏、「到底部」按钮仍是中文,这是设计如此);地址栏多了 `?lang=en` | |
+| 3 | 在英文版里刷新页面 | 仍停在英文版 | |
+| 4 | 对照看一个数字:中文版「行星主星与状态」表里某颗星的力量比,与英文版「Planet Lords and Dignity」同一行 | 数字完全一样 | |
+| 5 | 在中文版点「导出」 | 弹窗顶部有一条提示「要拿去问 ChatGPT、Claude 等 AI?建议切到 English 再导出…」和「切到 English」按钮 | |
+| 6 | 点「切到 English」 | 弹窗里的章节变成英文章节,说明句末写「导出的是英文版」;点「导出选中」,下载的文件名结尾是 `-EN.md`,打开是英文 | |
+| 7 | 打开一份**这次更新之前**生成的旧报告 | 没有「中文 / English」切换;点导出,弹窗里一行小字「这份报告生成于英文版上线前,重新生成即可获得中英两版。」 | |
+| 8 | 在旧报告地址后面手动加 `?lang=en` 打开 | 仍然显示中文,不报错 | |
+| 9 | 把第 6 步下载的英文文件发给 ChatGPT 或 Claude,让它概括 | 能正常读懂(这一步只是体感,不强求) | |
diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md
index f5af308d..3ec02b9d 100644
--- a/frontend/DESIGN.md
+++ b/frontend/DESIGN.md
@@ -587,6 +587,8 @@ one skeleton, none of them carrying the sidebar.
- **Structure:** inside the app shell, like every other secondary page — `/reports/[reportId]` was the last standalone full-screen route, and opening a report used to drop the reader out of the app entirely. 「个人报告」 sits in the 46px header; below it the reader owns its own scroll boundary, carrying the sticky screen chrome (back and the labelled 「导出」 button) and then the longform Markdown body beside a persistent TOC rail. Every phase — loading, generating, timed-out, and each error — renders inside the same shell, so the way back is always the rail. The five-chapter writer document is storage-only and is not rendered. Reports without Markdown show “旧版本报告,请重新生成”.
- **TOC levels (2026-09-29):** chapters (`##`) are `--color-ink` at weight 600 with `--space-3` above each new chapter; sections (`###`) are indented `--space-5` and stay `--color-ink-secondary`. The two levels read apart without relying on the current-item pill.
+- **中文 / English (2026-09-29):** reports generated since the English edition carry both editions; the action bar shows a two-segment pill (`.report-language-switch`, 40px segments, selected = `--color-canvas-muted` + ink + 600) before 「导出」. Reports without an English edition show no switch at all rather than a disabled one. The edition lives in the URL (`?lang=en`, read by the server page, written with `history.replaceState`), so refresh and shared links keep it. Switching remounts the article, TOC and fact tables for that edition and sets `lang` on the reader body; chrome (header, rail, 到底部) stays Chinese. Charts draw the same glyphs; only the legend reads `Su Mo …` / 「bar = retrograde」.
+- **导出框里的英文版提示:** reading Chinese with an English edition available, the drawer header adds one `--color-canvas-muted` band (`.report-export-ai-hint`) with the AI sentence and an outline 「切到 English」; it stacks below 767px. Old reports get one caption line (`.report-export-edition-note`) instead. The drawer is keyed by edition, so switching resets the selection to that edition's blocks; the file name gains `-EN`.
- **到底部 / 回到顶部:** one floating pill (`.report-edge-jump`, fixed bottom-right, same look as the chat's 跳到最新). It reads 「到底部」 until the reader is within 480px of the end, then 「回到顶部」 — never both. Sections below the fold mount lazily, so the click first mounts every section synchronously (`flushSync`) and then scrolls to `scrollHeight`, landing on the real end in one click. It is not a scroll follower and owns no anchor; it is hidden on paper.
- **TOC rail:** a persistent right-hand column built from the outline's own `##`/`###` ids — no second slugger, and nothing that touches the chart-grid rehype pass. It is sticky under the report's own action bar, highlights the section in view via `IntersectionObserver`, and marks it with `aria-current="location"` plus a 2px `--color-action` bar. Placement is explicit (`grid-column: 2`) rather than DOM-ordered, so the narrow-screen drawer can stay first in the source. Below 860px the rail is gone and what remains is the collapsed 「目录」 drawer.
- **Paper stays paper:** the rail reads in the app palette because it is chrome. `--report-paper` / `--report-rule` / `--report-accent` are the document's own, deliberately out of step with the app accent, and nothing in this surface puts them on chrome or takes chrome colour onto the page. D11, continuing D3.
diff --git a/frontend/docs/VOICE.md b/frontend/docs/VOICE.md
index 0d4ab220..c33502bf 100644
--- a/frontend/docs/VOICE.md
+++ b/frontend/docs/VOICE.md
@@ -141,13 +141,16 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
## 报告分块导出与删除
-- 列表已完成行只留「查看报告」。阅读页操作条只留「返回报告中心」与导出图标;图标读屏名「导出报告」,tooltip「导出」。不再显示「下载原始附录」或「打印」入口文案;浏览器 Ctrl+P 排版仍保留。
+- 列表已完成行只留「查看报告」。阅读页操作条只留「返回报告中心」、「中文 / English」切换(只在有英文版的报告上出现)与带字的「导出」按钮(读屏名「导出报告」,2026-09-29 起不再是只有图标的按钮)。不再显示「下载原始附录」或「打印」入口文案;浏览器 Ctrl+P 排版仍保留。
- 导出对话框标题仍为「分块导出」。说明:「勾选要带走的章节,导出成一个 .md 文件。全选就是整篇。」这里全选包含完整正文及另选的盘 / 表;与旧整篇逐字节相同的套餐是「常用」(全部正文)。
- 套餐:「常用 / 全部分盘 / 仅核对表」;动作:「全选 / 取消全选」「导出选中 (N)」。成功才提示「已导出 N 块」,下载失败或微信限制不提示成功。本轮 D5 保持既有微信系统浏览器提示原文,不借删除打印入口顺带改它。
-- 「共 N 块 · X 字 · 已选 n/N · Y 字」来自实际可导出块及 `Array.from(text).length`;包含安全 SVG / Markdown 标记,不是估计文章字数。此为第 2 条的可直接计算文本统计,不是口算领域数字;列表页仍不杜撰章数或盘数。
+- 「共 N 块 · X 字 · 约 Z KB · 已选 n/N · Y 字 · 约 W KB」:字数来自实际可导出块及 `Array.from(text).length`,大小是实际下载内容的字节数(`TextEncoder`),写「约 186 KB」「约 1.4 MB」,不按字数估;包含安全 SVG / Markdown 标记,不是估计文章字数。此为第 2 条的可直接计算文本统计,不是口算领域数字;列表页仍不杜撰章数或盘数。
- 删除确认:「确定删除这份报告?」;按钮:「删除 / 取消」;请求中:「删除中…」;失败:「报告没能删除,请稍后重试。」失败保留原行。
- 日期中文全写;深度「简版 / 标准深度 / 深度 / 研究级」;主题复用现有中文映射,未知 id 原样回退,不造含义。
+- 英文版提示(2026-09-29):当前看中文、且这份报告有英文版时,导出框顶部写「要拿去问 ChatGPT、Claude 等 AI?建议切到 English 再导出,术语与原典一致,AI 读得更准。」按钮「切到 English」。当前是英文时说明句末加「导出的是英文版。」。英文版上线前的报告写「这份报告生成于英文版上线前,重新生成即可获得中英两版。」;英文版没生成成功的写「这份报告的英文版没有生成成功,只能导出中文版。」
+- 英文版正文的口径:用 BPHS / PL9 通行的英文术语(Exalted、Own Sign、Mooltrikona、Sub lord……),不音译中文、不中英混写;限制说明用固定英文四句(见 `report-public-projection.ts` 的 `ENGLISH_LIMITATION_COPY`)。应用外壳(侧栏、标题、按钮、到底部)始终是中文。
+
## 报告里的限制说明
普通报告只留下人能读的正文、结论、行动建议和必要限制。不写字段名、状态码、评分、权重或执行账本。说不到日期就写「这次说不到具体哪一天。」依据没补上就写「有些依据还没补上,相关说法不能当成确定预测。」判断没闭合就写「有些判断还没闭合,不能写成确定结论。」不出现 `technique_truth`、`workflow_route` 这类键。
diff --git a/frontend/tests/report-english-edition.test.tsx b/frontend/tests/report-english-edition.test.tsx
index 3ae79405..191dfc6b 100644
--- a/frontend/tests/report-english-edition.test.tsx
+++ b/frontend/tests/report-english-edition.test.tsx
@@ -189,3 +189,26 @@ test("the export drawer suggests English for AI only when it can switch", async
assert.ok(h.container.text.includes("重新生成即可获得中英两版"));
} finally { await h.close(); }
});
+
+test("the real engine English golden is accepted whole and matches its Chinese pair", () => {
+ const en = JSON.parse(readFileSync(new URL("./fixtures/report-reader-main-fictional-en.json", import.meta.url), "utf8"));
+ const zh = JSON.parse(readFileSync(new URL("./fixtures/report-reader-main-fictional-zh-pair.json", import.meta.url), "utf8"));
+ assert.equal(en.fixtureProvenance.fictional, true);
+ assert.equal(en.fixtureProvenance.language, "en");
+ const accepted = acceptEnglishEdition(en.markdown, undefined, engine);
+ assert.ok(accepted.english, accepted.englishUnavailable);
+ // Nothing for the projection to cut, so the reader sees the whole edition.
+ assert.equal(projectOrdinaryReportMarkdown(en.markdown, "en"), en.markdown);
+ const zhLines = (zh.markdown as string).split("\n");
+ const enLines = (en.markdown as string).split("\n");
+ assert.equal(enLines.length, zhLines.length, "line-aligned editions");
+ const mismatched = zhLines.flatMap((line, index) => (
+ JSON.stringify(numericTokens(line)) === JSON.stringify(numericTokens(enLines[index])) ? [] : [enLines[index]]
+ ));
+ // The one known difference is a label, not a value: the English yoga name
+ // spells out the houses (「婚姻障碍(7宫主在凶宫)」 / "Kalatra Dosha (7th Lord in 6/8/12)").
+ assert.deepEqual(mismatched.map(line => line.split("|")[1]?.trim()), ["Kalatra Dosha (7th Lord in 6/8/12)"]);
+ const h2 = (markdown: string) => buildReportExportBlocks({ markdown }).filter(block => block.group === "body").length;
+ assert.equal(h2(en.markdown), h2(zh.markdown), "same chapters");
+ assert.equal(containsHan(JSON.stringify(buildReportExportBlocks({ markdown: en.markdown, language: "en" }))), false);
+});