# TASK · 长报告页渲染全部星盘(北印式、前端自绘)— 2026-09-09 - 基线:`origin/staging` @ `54adb0d1`(2026-09-09)。开工前 `git fetch origin --prune`,以远端为准。 - 分支:`codex/report-chart-render-20260909`,worktree `.worktrees/report-chart-render-20260909`。 - 涉及:Python 引擎(`scripts/jyotish_engine.py` 图盘输出、`scripts/chart_renderer.py`)+ 前端报告页(`personal-report-markdown-view.tsx`、`vedic-chart-svg.tsx`、下载)。**不改** api_server、不改数据库、不改写作 agent。 - 串行:同日无其他任务书改这几个文件。`TASK-report-*` 系列在 09-09 只有本单。 ## 1. 事故实证 产品负责人对照竞品(同类产品报告页每张分盘都有图,北印/南印可切)发现:我们的长报告页 `/reports/` 里 `#### D1 — Rashi Chart(本命盘)`、`#### D9 — Navamsa(婚盘)`、`#### Moon Chart(月亮参考盘)` 以及「Vargas I」图集下的 19 个 `#### D2…D60` 标题下面**全部是空白**,只有标题没有图。 链路逐段核过(基线 SHA 上按符号定位): | 段 | 位置 | 结论 | | --- | --- | --- | | 引擎渲染 | `scripts/jyotish_engine.py` `render_pl9_markdown` 内 `_render_south_chart`(L4184)→ `chart_renderer.render_south_indian_chart`(`scripts/chart_renderer.py` L60–126) | **正常**。本地用 `pl9-export` 跑真实引擎,Markdown 2,985 行 / 293 KB,含 **22 个内联 ``**(D1、D9、Moon 各 1 + 图集 19) | | 脱敏 | `sanitize_professional_report_reference_markdown`(L17530) | 只做路径脱敏,不动 `` | | 接口 / 存储 | `scripts/professional_report_reference.py` L108–112 返回 `markdown`;前端 `personal-report-longform-generate.ts` L140–159 原样入库 | 不丢 | | **前端渲染** | `frontend/src/components/personal-report/personal-report-markdown-view.tsx` `renderMarkdown`(L55–66):`react-markdown` + `skipHtml` + `disallowedElements=[script,iframe,object,embed,img]` | **丢在这里**。`skipHtml` 把所有内联 HTML 节点静默丢弃,`` 整块消失,标题后只剩空行 | 对照历史:09-06 之前的「五章写作稿」阅读页(`personal-report-document-view.tsx`,现已退役、仅存储不渲染)是用 React 组件 `VedicChartSvg` 画北印式图的;09-06 `TASK-report-md-page-20260906.md` 把阅读页换成 Markdown 直渲后,图盘随 `skipHtml` 一起没了。**这是产品回归,不是引擎缺能力。** `skipHtml` 本身不是错:`--birthplace-label` 等用户字符串会进 Markdown 正文,放开原始 HTML(`rehype-raw`)等于把用户输入当 HTML 渲染,09-06 任务书 §渲染安全把它列为红线,本单**维持**这条红线。 ## 2. 根因 1. 图盘只以 `` 内联 HTML 的形式存在于 Markdown 里,而报告页的渲染器按设计不渲染任何内联 HTML。两边各自正确,合起来没有一条让图到达页面的通路。 2. 引擎 SVG 本身也不适合直接上页面:南印式、英文缩写、无逆行标记、每张分盘中心都写死 `Rasi Chart (D1)`(`chart_renderer.py` L118–119)、颜色写死不跟主题。 ## 3. 决策记录(产品负责人 2026-09-09 拍板) | 决策 | 内容 | | --- | --- | | D1 | **北印式**。报告页所有图盘用北印式(菱形)布局;不做南印 / 西洋切换,不做「分盘下拉」。 | | D2 | **全部渲染**。引擎输出了几张就渲染几张(当前 22 张:D1、D9、Moon、图集 D2–D60),不挑。 | | D3 | **前端自己的组件画图**。数据由引擎以结构化 JSON 给出,前端 zod 校验后用 React SVG 组件绘制;**不**引入 `rehype-raw` / `rehype-sanitize`,`skipHtml` 红线不动。 | | D4 | 引擎 Markdown 里的 `` **保留**(`.md` 导出与外部阅读器、`tests/run_all.py` 的 `'` 留在文件里。 | | D6 | 现有 `VedicChartSvg` 的 4×4 方格 + 装饰菱形**不是**北印式,要改成真正的北印式几何(§5 任务 3 给了顶点表)。退役的 document view 仍引用该组件,随之一起换几何即可,不需要单独适配。 | | D7 | 标签语言:宫内行星用一字中文(日 月 火 水 木 金 土 罗 计)+ 整数度(`日 12°`),逆行加「逆」(`土逆 3°`);每宫角上标星座序号(1=白羊 … 12=双鱼,北印惯例);图下不再加标题(Markdown 里的 `####` 标题就是标题)。 | 无推翻既有红线。`TASK-report-md-page-20260906.md` §渲染安全(skipHtml、disallowedElements、urlTransform)保持。 ## 4. 硬红线 1. 不引入 `rehype-raw`、`rehype-sanitize`、`dangerouslySetInnerHTML`;图盘的每一个文本节点都必须来自 zod 校验过的 JSON 字段,且行星名 / 星座名只能是白名单枚举映射后的中文,不得把 JSON 里的字符串原样画到 SVG 上。 2. `skipHtml`、`disallowedElements`、`urlTransform` 三项不改。 3. `scripts/jyotish_api_server.py` 不增行。引擎侧新逻辑进新模块 `scripts/report_chart_block.py`,`jyotish_engine.py` 只在 `_render_south_chart` 处接一行。 4. 现有 Markdown 里 `` 的数量、顺序、`#### D1 — Rashi Chart(本命盘)` 等标题文案不变(`tests/test_full_report_quality_gate.py`、`scripts/full_report_quality_gate.py` L232、`tests/run_all.py` L270–272 / L600 依赖)。 5. 前端 `tsc --noEmit` 0 错、`npm run lint` 0 error、测试总数不低于开工实测、`next build` 后 `/` 仍 Static、首屏 gzip ±2%(报告页是独立路由,首屏不该动)。 6. 合同测试 fixture 必须是真实引擎输出(golden),不得手造形状;fixture 用公开名人或虚构出生资料。 7. 不顺手改 `chart_renderer.py` 的南印布局、颜色;只修 §5 任务 4 那一处标签。 ## 5. 任务分解 ### 任务 1 · 引擎:每张图旁边输出结构化围栏块 新模块 `scripts/report_chart_block.py`: ```python def build_chart_block(chart_id: str, title: str, positions: dict, ascendant_row: dict, retrograde_by_planet: dict[str, bool]) -> str ``` 返回: ```` ```jyotish-chart {"version":1,"id":"D9","title":"D9 — Navamsa(婚盘)","layout":"north", "ascendant":{"sign":"Leo","degree":12.34}, "planets":[{"name":"Sun","sign":"Leo","degree":3.21,"retrograde":false}, …]} ``` ```` 规则: - `id` 从 varga key 取 `D` 前缀部分(`D2_Hora`→`D2`,`D1`,月亮盘固定 `MOON`);`title` 与现有 `####` 标题一致。 - `sign` 用引擎英文星座名(Aries…Pisces);`degree` 是宫内度 `degree_in_sign`,保留两位小数;行星固定九曜顺序。 - `retrograde` 分盘里没有,一律从 `packet['core_chart']['planets'][name]['retrograde']` 取;Rahu/Ketu 按引擎原值。 - 在 `_render_south_chart`(L4184)里 SVG 之后拼接:`svg + '\n\n' + block`。`_图盘生成失败_` 分支不追加块。 - JSON 单行、`ensure_ascii=False`、`json.dumps` 生成;不得把任何用户输入字符串(地名、备注、报告 ID)放进块。 验收: - [ ] `.venv/bin/python -m pytest tests/test_report_chart_block.py`:用真实引擎(`build_professional_report_reference_packet` → `render_pl9_markdown`,与 `tests/test_full_report_quality_gate.py` 同一条路径)跑一份 golden,断言 围栏块数 == `` 数(当前 22);每块 JSON 可解析、`planets` ≤ 9、`degree ∈ [0,30)`、`sign` 在 12 枚举内;`D1` 块的 `ascendant.sign` == `core_chart.ascendant.sign`;每块每颗星的 `retrograde` == `core_chart` 同名值;块内不含 golden 请求里的地名字符串。 - [ ] `tests/test_full_report_quality_gate.py`、`tests/run_all.py` 相关断言仍绿。 - [ ] 快速门 `run_quality_gate.py --profile quick` 通过。 ### 任务 2 · 前端:围栏块 → zod → 北印式组件 新文件 `frontend/src/lib/report-chart-block.ts`: - `reportChartBlockSchema`(zod `strictObject`):`version: literal(1)`;`id: /^(D\d{1,3}|MOON)$/`;`title: string ≤120`;`layout: literal("north")`;`ascendant: {sign: enum(12 英文), degree: number ≥0 <30}`;`planets: array(≤9) of {name: enum(Sun…Ketu), sign, degree, retrograde: boolean}`。 - `parseReportChartBlock(source: string): ReportChartBlock | null`:`JSON.parse` 失败或 zod 失败均返回 `null`,不抛。 - `toNorthIndianChart(block)`:按整宫制从上升星座序号推 12 宫(第 i 宫星座 = `(ascIndex + i) % 12`,参考 `personal-report-generation.ts` `deriveVargaHousesFromEngine` L394 的做法),每宫 `occupants` 为行星显示标签(§3 D7 口径),行星→中文与星座→序号用本文件内的常量表,**不**复用 `SAFE_CELESTIAL_NAMES`(那是英文)。 - `stripReportChartBlocks(markdown: string): string`:删除所有 ```` ```jyotish-chart ```` 围栏块(含前后各一空行)。 `personal-report-markdown-view.tsx`: - `markdownComponents` 增加 `pre` 渲染器:用 `markdown-code-block.tsx` 同样的 `language-xxx` 判法取围栏语言;语言为 `jyotish-chart` → `parseReportChartBlock` → 成功则渲染 `
`;失败则渲染一行 `

图盘数据无效

`,**不**把源码回显。其他语言的 `pre` 保持现状(当前报告 Markdown 里没有别的围栏块)。 - 图形容器:最大宽 360px、居中、上下 `--space-*` 间距;图集 19 张在 ≥720px 视口按两列网格排(`.personal-report-chart-grid`,由 H3 section 内相邻 figure 自动成组即可,不改 outline);打印保持单列。 `personal-report-longform-download.ts` / 报告中心「导出报告(.md)」:调用 `downloadMarkdownReport` 前先 `stripReportChartBlocks`。 验收: - [ ] `frontend/tests/report-chart-block.test.ts`:fixture `frontend/tests/fixtures/report-chart-blocks-golden.json` 由任务 1 的真实引擎输出提取(脚本或手工,进度记录写来源命令);断言 22 块全部 parse 成功;D1 块第 1 宫星座 == ascendant,第 12 宫 == ascendant 前一个;逆行行星标签以「逆」结尾;`title` 里塞 `` 的篡改样本 parse 后不影响输出(标题不上图);坏 JSON / 多余字段 / `degree: 31` / 未知行星名 → `null`。 - [ ] `frontend/tests/personal-report-markdown-view.test.tsx`(新):`renderToStaticMarkup` 一段含围栏块 + 一段原始 `` 的 Markdown,断言输出含 `` 含 `(D9)`、不含 `(D1)`。 ### 任务 5 · 记录 - [ ] `docs/BUG_HISTORY.md` 新增一条(编号见 §8):状态 `resolved`,现象 / 触发 / 根因(skipHtml 丢 ``,09-06 直渲回归)/ 修复 / 验证 / 防复发(任务 2 的 markdown-view 测试)/ 关联 `TASK-report-md-page-20260906.md`。 - [ ] `CHANGELOG.md`:「报告页恢复星盘,改北印式,全部分盘可见」。Skill 版本不 bump(引擎输出只加围栏块,SKILL.md 不改)。 - [ ] `docs/tasks/PROGRESS-report-chart-render-20260909.md`:门禁四项实测数字、测试总数开工 / 收工、golden 来源命令、gzip 前后。 - [ ] `docs/testing/report-chart-render-20260909.md`:真人清单(生成一份新报告 → 22 张图可见、暗色主题可读、手机单列、打印单列、导出 `.md` 打开无 JSON 块、旧报告(无围栏块)仍显示标题不报错)。 ## 6. 让步顺序 做不完时按这个顺序砍,砍了写进 PROGRESS: 1. 任务 3 的两列网格与 `+N` 折叠(先单列、先截断)。 2. 任务 4(中心标签)。 3. 任务 2 的下载剥块(先允许 `.md` 里带 JSON 块)。 4. **不可砍**:任务 1、任务 2 的渲染与测试、任务 3 的北印几何、任务 5。 ## 7. 开工前置命令 ```bash git -C /workspace/Jyotisha status -sb | head -1 # 确认不在别人的分支上 git fetch origin --prune git worktree add -b codex/report-chart-render-20260909 .worktrees/report-chart-render-20260909 origin/staging cd .worktrees/report-chart-render-20260909 .venv/bin/python -m pytest tests/test_full_report_quality_gate.py -q # 基线 cd frontend && npm ci && ./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -5 # 记录开工测试总数 ``` 引擎改动只在 `render_pl9_markdown` 输出层与 `chart_renderer.py` 一行标签,不涉及运行入口 / 镜像 / 外部 oracle,§9 预检不要求。 ## 8. BUG 编号起点 `docs/BUG_HISTORY.md` 当前最大 `BUG-601`;`TASK-rectification-timeline-fix-20260909.md` 已占 602–603,`TASK-rectification-conversation-economy-20260909.md` 已占 604–606。本单从 **BUG-607** 起,开工时重新核对。