182 lines
15 KiB
Markdown
182 lines
15 KiB
Markdown
# 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` L60–126) | **正常**。本地用 `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` 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 节点静默丢弃,`<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、图集 D2–D60),不挑。 |
|
||
| 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` 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,断言 围栏块数 == `<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 宫顶点常量有单测:每宫多边形面积之和 == `S²`(允许浮点误差),且相邻宫不重叠(用采样点 point-in-polygon 各归一宫)。
|
||
- [ ] `frontend/DESIGN.md` 「Personal report reader」加一段 chart figure 规范(尺寸、网格、标签口径、打印),同一提交。
|
||
|
||
### 任务 4 · `chart_renderer.py` 中心标签跟标题走
|
||
|
||
`render_south_indian_chart` L118–119 每张分盘都写 `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` 已占 602–603,`TASK-rectification-conversation-economy-20260909.md` 已占 604–606。本单从 **BUG-607** 起,开工时重新核对。
|