From 809bdf13e942c329e2a4dd458d00b90028afbcc1 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sun, 6 Sep 2026 21:48:00 +0800 Subject: [PATCH] docs(report): record MD-page push SHA and keep markdown view locks Co-authored-by: Cursor --- ...PROGRESS-report-longform-gaps2-20260906.md | 18 ++- .../tasks/PROGRESS-report-md-page-20260906.md | 45 +++---- .../personal-report-markdown-view.test.ts | 112 ++++++++++++++++++ 3 files changed, 149 insertions(+), 26 deletions(-) create mode 100644 frontend/tests/personal-report-markdown-view.test.ts diff --git a/docs/tasks/PROGRESS-report-longform-gaps2-20260906.md b/docs/tasks/PROGRESS-report-longform-gaps2-20260906.md index 9693231f..d2fa6afa 100644 --- a/docs/tasks/PROGRESS-report-longform-gaps2-20260906.md +++ b/docs/tasks/PROGRESS-report-longform-gaps2-20260906.md @@ -1,14 +1,16 @@ # PROGRESS · 长报告对齐二轮 gaps2(2026-09-06) 工作树:`.worktrees/report-md-page-20260906`(与 MD 详情页串行) -分支:`codex/report-md-page-20260906` +分支:`codex/report-md-page-20260906`(已 push `origin`,HEAD `8b140479`;实现主体 `cfcd369d`) 任务书:`TASK-report-longform-gaps2-20260906.md` ## 任务 0 根因 专业参考路径可以把 `cmd_kp` 收成 `{status: blocked, reason: ...}` 且没有 `planets` / `houses`。渲染层 `_kp_lord_sub_section` 与专题 KP 表遇到该信封曾直接 `return []`,计划段无声消失。三年 KP 月表不走同一信封,所以仍在。 -触发变量是 **API 候选窗 / blocked KP 信封**,不是 Raman 本身。同构虚构盘(候选窗 + raman + full pack)在单元层用 blocked KP 信封复现;本轮未把真实用户盘入库。 +触发变量是 **API 候选窗 / blocked KP 信封**,不是 Raman 本身。真实用户盘未入库。 + +同构虚构盘 pl9-export(1990-01-01 12:00 UTC+8,39.9042/116.4074,raman,full pack;只写 `/tmp`):约 3107 行,KP Lord/Sub、审计表约 71 数据行、力量章六子块均在;约 11s。 ## 任务状态 @@ -20,14 +22,18 @@ | 3 力量章六子块 | 完成 | Bhava Bala 规范化;Yogini/Narayana/Kala periods 从本地模块补装 | | 4 年度回放按年展开 | 完成 | `_annual_series_surface` 保留 replay 键;缺年 blocked | | 4b 时间精度合同 | 完成 | 四粒度:day blocked;week/month parameter_sensitive;year 有年度系列则 partial_verified | -| 4c 出生 provenance | 完成 | `birthplace_label` / `coordinate_source=user_reported` / `time_source` / `uncertainty_minutes`;坐标来源保持 user_reported | -| 5 C 组小段 | 完成(blocked-or-present) | 原始模块索引、Technique Audit、Vargas I 等走显式 heading;分钟排行等与全量 Agent 行数未做真实盘逐行校准 | -| 6 质量门 | 完成 | `tests/test_report_longform_gaps2.py` 与 timing/parity 相关切片 | +| 4c 出生 provenance | 完成 | `birthplace_label` / `coordinate_source=user_reported` / `time_source` / `uncertainty_minutes` | +| 5 C 组小段 | 完成(blocked-or-present) | 原始模块索引、Technique Audit、Vargas I 等走显式 heading | +| 6 质量门 | 完成 | 见下 | ## 质量门 ```bash -.venv/bin/python -m pytest tests/test_report_longform_gaps2.py tests/test_timing_precision_contract.py tests/test_report_longform_parity.py -q +/Users/jesse/Downloads/Copse/astrology/yinduzhanxing/.venv/bin/python -m pytest \ + tests/test_report_longform_gaps2.py \ + tests/test_timing_precision_contract.py \ + tests/test_report_longform_parity.py -q +# 22 passed ``` 未改 `.gitea/workflows/**`,不提升 main。 diff --git a/docs/tasks/PROGRESS-report-md-page-20260906.md b/docs/tasks/PROGRESS-report-md-page-20260906.md index a636cd77..f507e690 100644 --- a/docs/tasks/PROGRESS-report-md-page-20260906.md +++ b/docs/tasks/PROGRESS-report-md-page-20260906.md @@ -2,6 +2,7 @@ 工作树:`.worktrees/report-md-page-20260906` 分支:`codex/report-md-page-20260906` +远端:`origin/codex/report-md-page-20260906` = `8b140479`(实现 `cfcd369d`) 基线:`origin/staging` `e428e9d0` 任务书:`TASK-report-md-page-20260906.md`(硬依赖 gaps2,已在同一 worktree 完成) 未改 `.gitea/workflows/**`,不提升 main。 @@ -10,41 +11,45 @@ - 目标:新报告 worker 只生成 pl9 长报告 MD;详情页渲染该 MD;writer 停用保留;零模型用量必须结算。 - 顺序:gaps2 装配缺口 → MD-only 生成/计费 → 详情页 TOC/懒渲染 → 导出与报告中心 → 质量门。 -- 最大风险:`complete_personal_report_job` 仍要合法 v2 封面文档;计费 `completeUsage` 必须接受 0 token,且不得把 `actualModelId` 写成 `unknown`。 +- 最大风险:`complete_personal_report_job` 仍要合法 v2 封面文档;计费不得把 `actualModelId` 写成 `unknown`。 ## 我替领导拍的板 -- 计费:沿用 `report.full` 预留,结算 `inputTokens=0 outputTokens=0 costMicrousd=0`,`actualModelId` 用创建时的目录默认模型 id。缺少合法模型 id 时 **在报告标 ready 之前** 重试,避免预留悬挂。 -- 封面文档:writer 不再跑;写入一份最小合法 v2 文档(各主题走 blocked disclosure),页面不渲染它。卡片摘要来自生成时对 MD「摘要」的截取,仍落在封面 `executiveSummary.summary`,列表 JSON 路径不变。 -- 旧报告:无附录 MD 即显示「旧版本报告,请重新生成」,不维护五章视图。professional-reference POST 改为只读缓存,缺失返回 410。 -- 导出:详情页用内存中的同一份 MD 下载;文件名 `个人报告-YYYY-MM-DD`。打印为次按钮。 +- 计费:沿用 `report.full` 预留,结算 `inputTokens=0 outputTokens=0 costMicrousd=0`,`actualModelId` 用目录默认模型 id。缺少合法模型 id 时在报告标 ready **之前** 重试。 +- 封面文档:最小合法 v2(主题走 blocked disclosure),页面不渲染。卡片摘要来自 MD「摘要」,写入封面 `executiveSummary.summary`,列表 JSON 路径不变。 +- 旧报告:无附录 MD 显示「旧版本报告,请重新生成」。professional-reference POST 只读缓存,缺失 410。 +- 导出:内存中同一份 MD;文件名 `个人报告-YYYY-MM-DD`。打印为次按钮。 ## 任务状态 | 任务 | 状态 | 说明 | | --- | --- | --- | -| gaps2 | 完成 | 见 `PROGRESS-report-longform-gaps2-20260906.md`;BUG-561 / BUG-562 | -| MD-only 生成 + 计费 | 完成 | `PERSONAL_REPORT_WRITER_ENABLED=false`;0-token settle | -| 详情页 TOC/懒渲染 | 完成 | `react-markdown` + `remark-gfm` + `skipHtml`;首屏导航+摘要;H2 懒渲染 | +| gaps2 | 完成 | `PROGRESS-report-longform-gaps2-20260906.md`;BUG-561 / BUG-562 | +| MD-only 生成 + 计费 | 完成 | `PERSONAL_REPORT_WRITER_ENABLED=false`;附录失败即报告失败 | +| 详情页 TOC/懒渲染 | 完成 | `react-markdown` + `remark-gfm` + `skipHtml`;首屏导航+摘要 | | 导出 / 报告中心 | 完成 | 「导出报告(.md)」;一步式等待文案 | -| 端到端 staging | 待部署后 | 本机质量门先跑;staging 真实创建仍需部署 | +| 端到端 staging | blocked | staging `/api/health` 仍是 `a444c493`,本分支尚未合入/部署 | ## 计费适配 -`complete_usage` 已允许 `input_tokens >= 0`。生产 worker 在长报告路径上: - -1. 先加载模型目录,没有默认模型则 `model_unavailable`(可重试),不跑引擎。 -2. 生成成功后带 `actualModelId=catalog.defaultModelId`、0 token。 -3. worker-core 若看到空/`unknown` 模型 id,在 `completeReportReady` 之前抛错,避免报告已 ready、预留永不结算。 +1. 先加载模型目录,没有默认模型则 `model_unavailable`(可重试)。 +2. 生成成功后 `actualModelId=catalog.defaultModelId`、0 token。 +3. worker-core 若看到空/`unknown` 模型 id,在 `completeReportReady` 之前抛错。 ## 首屏渲染 -本地对含导航/摘要/宽表/诚实标签的样例 Markdown 做 `renderToStaticMarkup`,`frontend/tests/personal-report-longform-md.test.ts` 锁首屏 < 1500ms(本机实测约 30ms)。完整 ~250KB / 2700 行报告的浏览器首屏仍需 staging 部署后补测;H2 分段懒渲染后首屏只挂载导航+摘要。 +`frontend/tests/personal-report-longform-md.test.ts` 锁样例 SSR < 1500ms(本机约 30ms)。完整 ~250KB 报告的浏览器首屏仍待部署后补测。 -## 质量门命令 +## 质量门(实际跑过) -```bash -.venv/bin/python -m pytest tests/test_report_longform_gaps2.py tests/test_timing_precision_contract.py tests/test_report_longform_parity.py -q -cd frontend && ./node_modules/.bin/tsc --noEmit -cd frontend && ./node_modules/.bin/tsx --test tests/personal-report-longform-md.test.ts tests/personal-report-view.test.ts tests/personal-report-export.test.ts tests/personal-report-entry.test.ts tests/report-polling-contract.test.ts tests/professional-report-reference-route.test.ts tests/personal-report-worker.test.ts tests/personal-report-api.test.ts +```text +pytest gaps2/timing/parity: 22 passed +frontend tsc --noEmit: pass +tsx personal-report-* + professional-reference + polling: 137 passed ``` + +## 推送 + +- `cfcd369d` feat(report): render longform Markdown as the report and close gaps2 holes +- `8b140479` docs: record BUG-561/562 fix SHA +- 未提升 `main`,未合入 `staging` diff --git a/frontend/tests/personal-report-markdown-view.test.ts b/frontend/tests/personal-report-markdown-view.test.ts new file mode 100644 index 00000000..cdd6d2f8 --- /dev/null +++ b/frontend/tests/personal-report-markdown-view.test.ts @@ -0,0 +1,112 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { PersonalReportMarkdownView } from "../src/components/personal-report/personal-report-markdown-view.tsx"; +import { + buildLongformOutline, + extractLongformSummary, + personalReportMarkdownFilename, +} from "../src/lib/personal-report-longform-outline.ts"; +import { buildLongformCoverDocument } from "../src/lib/personal-report-longform-cover.ts"; +import { safeParseServerReportDocument } from "../src/lib/personal-report-contract.server-core.ts"; + +Object.assign(globalThis, { React }); + +const SAMPLE = `# 标题 + + + +[外链](https://evil.example) + +## 成品阅读导航 + +导航应立即出现。 + +### 质量验收矩阵 + +| 段落 | 状态 | +| --- | --- | +| KP | blocked | + +## 主题解读与时间展开 + +### 解读摘要 + +这是摘要正文,用于卡片截取。 + +### 本命主轴 + +懒加载段落不应出现在首屏。 + +## 后段 + +后段正文。 +`; + +test("outline keeps 导航 and 摘要 eager and extracts the card summary", () => { + const outline = buildLongformOutline(SAMPLE); + assert.ok(outline.headings.some((heading) => heading.title === "成品阅读导航")); + assert.ok(outline.headings.some((heading) => heading.title === "质量验收矩阵")); + assert.ok(outline.headings.some((heading) => heading.title === "解读摘要")); + assert.match(outline.leadMarkdown, /成品阅读导航/); + assert.match(outline.leadMarkdown, /质量验收矩阵/); + assert.match(outline.leadMarkdown, /解读摘要/); + assert.equal(extractLongformSummary(SAMPLE), "这是摘要正文,用于卡片截取。"); + const lazy = outline.sections.filter((section) => !section.eager); + assert.ok(lazy.some((section) => section.title === "主题解读与时间展开")); + assert.doesNotMatch(lazy.find((section) => section.title === "主题解读与时间展开")?.markdown ?? "", /解读摘要/); +}); + +test("markdown view skips HTML, blocks remote resources, wraps tables, and lazy-renders later H2s", () => { + const started = Date.now(); + const markup = renderToStaticMarkup(React.createElement(PersonalReportMarkdownView, { markdown: SAMPLE })); + const elapsed = Date.now() - started; + assert.ok(elapsed < 1500, `first paint ${elapsed}ms`); + assert.doesNotMatch(markup, /