Files
Jyotisha/docs/tasks/TASK-report-chart-render-20260909.md
T

182 lines
15 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.
# 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/<id>``#### 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` L60126 | **正常**。本地用 `pl9-export` 跑真实引擎,Markdown 2,985 行 / 293 KB,含 **22 个内联 `<svg>`**D1、D9、Moon 各 1 + 图集 19 |
| 脱敏 | `sanitize_professional_report_reference_markdown`(L17530) | 只做路径脱敏,不动 `<svg>` |
| 接口 / 存储 | `scripts/professional_report_reference.py` L108112 返回 `markdown`;前端 `personal-report-longform-generate.ts` L140159 原样入库 | 不丢 |
| **前端渲染** | `frontend/src/components/personal-report/personal-report-markdown-view.tsx` `renderMarkdown`L5566):`react-markdown` + `skipHtml` + `disallowedElements=[script,iframe,object,embed,img]` | **丢在这里**`skipHtml` 把所有内联 HTML 节点静默丢弃,`<svg>…</svg>` 整块消失,标题后只剩空行 |
对照历史: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. 图盘只以 `<svg>` 内联 HTML 的形式存在于 Markdown 里,而报告页的渲染器按设计不渲染任何内联 HTML。两边各自正确,合起来没有一条让图到达页面的通路。
2. 引擎 SVG 本身也不适合直接上页面:南印式、英文缩写、无逆行标记、每张分盘中心都写死 `Rasi Chart (D1)``chart_renderer.py` L118–119)、颜色写死不跟主题。
## 3. 决策记录(产品负责人 2026-09-09 拍板)
| 决策 | 内容 |
| --- | --- |
| D1 | **北印式**。报告页所有图盘用北印式(菱形)布局;不做南印 / 西洋切换,不做「分盘下拉」。 |
| D2 | **全部渲染**。引擎输出了几张就渲染几张(当前 22 张:D1、D9、Moon、图集 D2D60),不挑。 |
| D3 | **前端自己的组件画图**。数据由引擎以结构化 JSON 给出,前端 zod 校验后用 React SVG 组件绘制;**不**引入 `rehype-raw` / `rehype-sanitize``skipHtml` 红线不动。 |
| D4 | 引擎 Markdown 里的 `<svg>` **保留**`.md` 导出与外部阅读器、`tests/run_all.py``'<svg' in svg` 断言仍靠它),在其旁边**追加**一段 ```` ```jyotish-chart ```` 围栏 JSON;报告页只认围栏块。 |
| D5 | 「导出报告(.md)」下载前剥掉围栏 JSON 块(读者不需要看 22 段 JSON),`<svg>` 留在文件里。 |
| 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 里 `<svg>` 的数量、顺序、`#### D1 — Rashi Chart(本命盘)` 等标题文案不变(`tests/test_full_report_quality_gate.py`、`scripts/full_report_quality_gate.py` L232、`tests/run_all.py` L270272 / 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,断言 围栏块数 == `<svg>` 数(当前 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` → 成功则渲染 `<figure className="personal-report-chart-figure"><NorthIndianChartSvg … /></figure>`;失败则渲染一行 `<p className="personal-report-chart-invalid">图盘数据无效</p>`,**不**把源码回显。其他语言的 `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` 里塞 `<img onerror>` 的篡改样本 parse 后不影响输出(标题不上图);坏 JSON / 多余字段 / `degree: 31` / 未知行星名 → `null`。
- [ ] `frontend/tests/personal-report-markdown-view.test.tsx`(新):`renderToStaticMarkup` 一段含围栏块 + 一段原始 `<svg>` 的 Markdown,断言输出含 `<svg`(来自组件,含 `role="img"`)且不含引擎 SVG 的 `viewBox="0 0 420 480"`;坏块输出「图盘数据无效」且不含 `<script`。
- [ ] 下载测试:`stripReportChartBlocks(golden)` 后不含 ```` ```jyotish-chart `````<svg` 数不变。
- [ ] `tsc` / `lint` / `npm test` / `next build` 四项红线。
### 任务 3 · `VedicChartSvg` 改成真正的北印式几何
现组件是 4×4 方格外圈 + 中央装饰菱形,宫位是方格,不是北印式。改为标准北印式:正方形边长 `S`viewBox 400),两条对角线 + 四边中点连成的内菱形,12 宫多边形顶点(`h=S/2`, `q=S/4`):
| 宫 | 形状 | 顶点 |
| --- | --- | --- |
| 1 | 上中菱形 | (h,0) (3q,q) (h,h) (q,q) |
| 2 | 左上三角 | (0,0) (h,0) (q,q) |
| 3 | 左上三角 | (0,0) (q,q) (0,h) |
| 4 | 左中菱形 | (0,h) (q,q) (h,h) (q,3q) |
| 5 | 左下三角 | (0,h) (q,3q) (0,S) |
| 6 | 左下三角 | (0,S) (q,3q) (h,S) |
| 7 | 下中菱形 | (h,S) (q,3q) (h,h) (3q,3q) |
| 8 | 右下三角 | (h,S) (3q,3q) (S,S) |
| 9 | 右下三角 | (S,S) (3q,3q) (S,h) |
| 10 | 右中菱形 | (S,h) (3q,3q) (h,h) (3q,q) |
| 11 | 右上三角 | (S,h) (3q,q) (S,0) |
| 12 | 右上三角 | (S,0) (3q,q) (h,0) |
- 星座序号放在每宫靠内顶点处(菱形宫放中心侧顶点内 12px,三角宫放直角顶点内 14px),muted;行星标签从宫多边形质心起竖排,最多 6 行,超出合并为 `+N`;文字用 `clipPath` 裁到本宫多边形。
- 颜色只用现有 token(线 `--color-border-strong`、文字 `--color-ink`、muted `--color-ink-muted`),暗色主题跟随;不引入新颜色。
- props 类型从 `ChartV1` 放宽为结构类型 `{houses: {houseNumber, sign, occupants}[]; planets?: {name, retrograde}[]}``ChartV1` 可赋值给它),`personal-report-contract.ts` 的 zod 合同不动。
- 保留现有导出名 `VedicChartSvg`、`hasRealChartData`、`findChart``personal-report-document-view.tsx` 不改。
验收:
- [ ] 现有引用 `vedic-chart-svg` 的测试仍绿;若改断言写「原值 / 新值 / 原因」三栏。
- [ ] 12 宫顶点常量有单测:每宫多边形面积之和 == ``(允许浮点误差),且相邻宫不重叠(用采样点 point-in-polygon 各归一宫)。
- [ ] `frontend/DESIGN.md` 「Personal report reader」加一段 chart figure 规范(尺寸、网格、标签口径、打印),同一提交。
### 任务 4 · `chart_renderer.py` 中心标签跟标题走
`render_south_indian_chart` L118119 每张分盘都写 `Rasi Chart (D1)`。改为从 `title` 取 `D\d+` 或 `Moon` 作第二行,第一行改 `Chart`D1 输出不变。
验收:
- [ ] `tests/test_report_chart_block.py` 里加一条:D9 的 `<svg>` 含 `(D9)`、不含 `(D1)`。
### 任务 5 · 记录
- [ ] `docs/BUG_HISTORY.md` 新增一条(编号见 §8):状态 `resolved`,现象 / 触发 / 根因(skipHtml 丢 `<svg>`,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` 已占 602603`TASK-rectification-conversation-economy-20260909.md` 已占 604606。本单从 **BUG-607** 起,开工时重新核对。