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

15 KiB
Raw Blame History

TASK · 长报告页渲染全部星盘(北印式、前端自绘)— 2026-09-09

  • 基线:origin/staging @ 54adb0d12026-09-09)。开工前 git fetch origin --prune,以远端为准。
  • 分支:codex/report-chart-render-20260909worktree .worktrees/report-chart-render-20260909
  • 涉及:Python 引擎(scripts/jyotish_engine.py 图盘输出、scripts/chart_renderer.py+ 前端报告页(personal-report-markdown-view.tsxvedic-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_chartL4184)→ chart_renderer.render_south_indian_chartscripts/chart_renderer.py L60126 正常。本地用 pl9-export 跑真实引擎,Markdown 2,985 行 / 293 KB,含 22 个内联 <svg>D1、D9、Moon 各 1 + 图集 19
脱敏 sanitize_professional_report_reference_markdownL17530 只做路径脱敏,不动 <svg>
接口 / 存储 scripts/professional_report_reference.py L108112 返回 markdown;前端 personal-report-longform-generate.ts L140159 原样入库 不丢
前端渲染 frontend/src/components/personal-report/personal-report-markdown-view.tsx renderMarkdownL5566):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 正文,放开原始 HTMLrehype-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-sanitizeskipHtml 红线不动。
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-rawrehype-sanitizedangerouslySetInnerHTML;图盘的每一个文本节点都必须来自 zod 校验过的 JSON 字段,且行星名 / 星座名只能是白名单枚举映射后的中文,不得把 JSON 里的字符串原样画到 SVG 上。
  2. skipHtmldisallowedElementsurlTransform 三项不改。
  3. scripts/jyotish_api_server.py 不增行。引擎侧新逻辑进新模块 scripts/report_chart_block.pyjyotish_engine.py 只在 _render_south_chart 处接一行。
  4. 现有 Markdown 里 <svg> 的数量、顺序、#### D1 — Rashi Chart(本命盘) 等标题文案不变(tests/test_full_report_quality_gate.pyscripts/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

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_HoraD2D1,月亮盘固定 MOON);title 与现有 #### 标题一致。
  • sign 用引擎英文星座名(Aries…Pisces);degree 是宫内度 degree_in_sign,保留两位小数;行星固定九曜顺序。
  • retrograde 分盘里没有,一律从 packet['core_chart']['planets'][name]['retrograde'] 取;Rahu/Ketu 按引擎原值。
  • _render_south_chartL4184)里 SVG 之后拼接:svg + '\n\n' + block_图盘生成失败_ 分支不追加块。
  • JSON 单行、ensure_ascii=Falsejson.dumps 生成;不得把任何用户输入字符串(地名、备注、报告 ID)放进块。

验收:

  • .venv/bin/python -m pytest tests/test_report_chart_block.py:用真实引擎(build_professional_report_reference_packetrender_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.pytests/run_all.py 相关断言仍绿。
  • 快速门 run_quality_gate.py --profile quick 通过。

任务 2 · 前端:围栏块 → zod → 北印式组件

新文件 frontend/src/lib/report-chart-block.ts

  • reportChartBlockSchemazod strictObject):version: literal(1)id: /^(D\d{1,3}|MOON)$/title: string ≤120layout: 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 | nullJSON.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-chartparseReportChartBlock → 成功则渲染 <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.tsfixture 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 方格外圈 + 中央装饰菱形,宫位是方格,不是北印式。改为标准北印式:正方形边长 SviewBox 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 合同不动。
  • 保留现有导出名 VedicChartSvghasRealChartDatafindChartpersonal-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)。改为从 titleD\d+Moon 作第二行,第一行改 ChartD1 输出不变。

验收:

  • 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. 开工前置命令

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-601TASK-rectification-timeline-fix-20260909.md 已占 602603TASK-rectification-conversation-economy-20260909.md 已占 604606。本单从 BUG-607 起,开工时重新核对。