diff --git a/BLOCKED.md b/BLOCKED.md index ccb405a2..076056cb 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -1,17 +1,18 @@ # BLOCKED -## TASK-birth-sky-cover T5:报告封面没有可靠的出生资料绑定(2026-09-28) +## ~~TASK-birth-sky-cover T5:报告封面没有可靠的出生资料绑定(2026-09-28)~~ → 已解除:产品 2026-09-28 选 A -- 任务书要求「只有报告生成时的出生资料与现在一致时才显示封面」。逐项查过,报告侧**没有任何出生资料指纹**: - - `personal_reports` 表列(`20260806010000_personal_reports.sql` 及其后 `add column`):`chart_profile_id`、`request_fingerprint`、`calculation_hash`、`evidence_hash` 等,没有出生资料快照或 `profileFingerprint`。 - - `request_fingerprint` 只哈希 `reportType / presentationMode / depth / themes / sessionId / chartProfileId`(`personal-report-route-core.ts` `computeRequestFingerprint`),不含出生资料。 - - `calculation_hash`:普通报告取引擎 `result_hash`(含计算结果,不是出生资料指纹,且 BUG-693/694 记过哈希含墙钟);长报告取 markdown 全文的 sha256(`personal-report-longform-cover.ts`)。两者都不能和当前 `profileFingerprint` 比。 - - 报告文档 `subject` 只有 `displayName / birthTimeStatus / birthPlaceLabel`;`personal_report_jobs`、`personal_report_sections`、`personal_report_longform_appendices` 也没有出生资料。 - - 仓库里 `result_binding` 只出现在 Python 的 `calculation_profile_contract.py` / `full_report_quality_gate.py`,不进报告行。 -- 按任务书:本单**不显示**报告封面,不拿当前资料冒充当时资料;`reportView` 未改、报告页未改、打印样式未改,旧报告不补算不补写库。 -- 解除需要一个产品决定(二选一,都不改表结构):① 生成报告时把当时的 `profileFingerprint`(`resolveServerOwnedChartBirth(...).fingerprint`)写进报告文档(如 `subject.birthFingerprint`,v2 schema 加可选字段),只对新报告生效,旧报告永远无封面;② 接受「当前资料」作为封面依据并在封面上说明——与任务书红线冲突,需产品推翻。 -- `draw.ts` 的 `paper` palette 已实现并有单测(打印用),解除后报告封面可直接复用。 +- ~~任务书要求「只有报告生成时的出生资料与现在一致时才显示封面」。逐项查过,报告侧**没有任何出生资料指纹**:~~ + - ~~`personal_reports` 表列(`20260806010000_personal_reports.sql` 及其后 `add column`):`chart_profile_id`、`request_fingerprint`、`calculation_hash`、`evidence_hash` 等,没有出生资料快照或 `profileFingerprint`。~~ + - ~~`request_fingerprint` 只哈希 `reportType / presentationMode / depth / themes / sessionId / chartProfileId`(`personal-report-route-core.ts` `computeRequestFingerprint`),不含出生资料。~~ + - ~~`calculation_hash`:普通报告取引擎 `result_hash`(含计算结果,不是出生资料指纹,且 BUG-693/694 记过哈希含墙钟);长报告取 markdown 全文的 sha256(`personal-report-longform-cover.ts`)。两者都不能和当前 `profileFingerprint` 比。~~ + - ~~报告文档 `subject` 只有 `displayName / birthTimeStatus / birthPlaceLabel`;`personal_report_jobs`、`personal_report_sections`、`personal_report_longform_appendices` 也没有出生资料。~~ + - ~~仓库里 `result_binding` 只出现在 Python 的 `calculation_profile_contract.py` / `full_report_quality_gate.py`,不进报告行。~~ +- ~~按任务书:本单**不显示**报告封面,不拿当前资料冒充当时资料;`reportView` 未改、报告页未改、打印样式未改,旧报告不补算不补写库。~~ +- ~~解除需要一个产品决定(二选一,都不改表结构):① 生成报告时把当时的 `profileFingerprint`(`resolveServerOwnedChartBirth(...).fingerprint`)写进报告文档(如 `subject.birthFingerprint`,v2 schema 加可选字段),只对新报告生效,旧报告永远无封面;② 接受「当前资料」作为封面依据并在封面上说明——与任务书红线冲突,需产品推翻。~~ +- ~~`draw.ts` 的 `paper` palette 已实现并有单测(打印用),解除后报告封面可直接复用。~~ - 同单其余环境缺口:无登录态与真机(星盘页按钮、保存 / 分享、首次揭幕按 `docs/testing/birth-sky-cover-checklist.md` 走);无 Docker(24 条 DB / 部署套件失败与基线逐条同名)。未推送、未部署。 +- **已解除(2026-09-28,TASK-birth-sky-followup-20260928 F1–F3)**:产品选方案 A。新生成的报告在 `subject.birthFingerprint` 写入生成时的出生资料指纹(与星盘页 `profileFingerprint` 同一算法、同取存储行);`GET /api/reports/:id` 在服务端比对当前指纹,只下发 `coverSubject`(人物 id 或 null),不下发指纹与出生资料。旧报告没有该字段,永远不显示封面,不补算不回填;不改表结构。 ## TASK-site-button-contrast(BUG-1079):真机、登录态与两条待决发现(2026-09-28) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ee84f59..01551fe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 印度占星 Skill 更新日志 +## 2026-09-28 — 「那一刻的天空」追加:报告封面上线(待验收) + +- 从这次起,新生成的报告会记下生成时用的是哪一套出生资料;打开报告时,只要这套资料和现在一致,正文最上方就有一张「那一刻的天空」封面(深色,和星盘页那张同一种画法)。打印或存 PDF 时封面换成浅色纸面版,单独占第一页。 +- 以前生成的报告没有这份记录,永远不显示封面;改过出生时间或出生地之后,改之前生成的报告也不再显示封面。 +- 封面拿不到就不显示,不留空白、不转圈,报告正文不受影响。浏览器拿不到出生资料和指纹,只知道「要不要画、画谁」。 +- Skill 版本不 bump(Skill 文本未改)。不改数据库结构。 + ## 2026-09-28 — 星盘页新增「那一刻的天空」(待验收) - 星盘页顶栏右侧多一个按钮「那一刻的天空」:出生地、出生那一刻头顶的真实天空,画成一张 3:4 的图。天顶在圆心、东在左,地平线以上的行星和亮星(北斗、仙后、猎户连线)照实画。 diff --git a/contracts/personal-report/report-document.v1.schema.json b/contracts/personal-report/report-document.v1.schema.json index ff595c1d..58fe3632 100644 --- a/contracts/personal-report/report-document.v1.schema.json +++ b/contracts/personal-report/report-document.v1.schema.json @@ -295,6 +295,10 @@ "type": "string", "minLength": 1, "maxLength": 200 + }, + "birthFingerprint": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" } }, "required": ["displayName", "birthTimeStatus", "birthPlaceLabel"] diff --git a/contracts/personal-report/report-document.v2.schema.json b/contracts/personal-report/report-document.v2.schema.json index c1afd565..db13b68e 100644 --- a/contracts/personal-report/report-document.v2.schema.json +++ b/contracts/personal-report/report-document.v2.schema.json @@ -704,6 +704,10 @@ "type": "string", "minLength": 1, "maxLength": 200 + }, + "birthFingerprint": { + "type": "string", + "pattern": "^[0-9a-f]{32}$" } }, "required": [ diff --git a/docs/tasks/PROGRESS-birth-sky-cover-20260928.md b/docs/tasks/PROGRESS-birth-sky-cover-20260928.md index 4e3a0152..04db4a96 100644 --- a/docs/tasks/PROGRESS-birth-sky-cover-20260928.md +++ b/docs/tasks/PROGRESS-birth-sky-cover-20260928.md @@ -109,3 +109,38 @@ JS 的 +773 B 是首页为 T6 引入的 `useBirthSkyReveal`、`startBirthSkyReve - 无 `.venv`:Python 用系统 `python3`。 - 无头 Chrome 的 `--screenshot` 在本机对这页时好时坏(挂起),海报改用 `canvas.toDataURL` + `--dump-dom` 取出,是导出 PNG 的真实像素。 - 未推送、未部署。 + +--- + +## 追加单(`TASK-birth-sky-followup-20260928`) + +- 任务书:`docs/tasks/TASK-birth-sky-followup-20260928.md`(staging-docs)。产品授权直接执行(Claude 子代理)。 +- 基线:开工 `git fetch origin --prune` 后 `origin/staging` 仍是 `d670521f`,无需 rebase;本分支在前三个提交(`d277cbd8`、`028aef27`、`14ef18ac`)之上再加两个提交。 +- BUG 编号:开工时最大 BUG-1079;新功能,未开 BUG。 + +### F1 · 报告写入出生资料指纹 + +- 新模块 `frontend/src/lib/report-birth-binding.ts`:`subjectBirthFingerprint(row)` = `resolveServerOwnedChartBirth(row).fingerprint`。 +- **与星盘页同一种算法、同一种输入**:星盘页的 `profileFingerprint` 来自 `prepareChartViewProfile`,它取的是**存储行**的指纹(`stored.fingerprint`,在内存补全时区之前)。报告两处加载点(`POST /api/reports` 与 worker 的 `loadReportWorkerProfile`)都先 `loadSubjectBirth` 取存储行、再补时区,所以在补全之前用 `stampReportBirthFingerprint(resolved.row, filled)` 把指纹挂在内存行的 `report_birth_fingerprint` 上(只在服务端内存里,不落库、不进引擎输入)。测试证明:缺时区的行,补全前后指纹不同,补全前的等于星盘页。 +- 三条生成路径都写 `subject.birthFingerprint`: + - 路由内联生成(`resolveReportCreate` → `generatePersonalReport`); + - worker writer 路径(`generateWriterReport` → `generatePersonalReport`,单次调用与分章两种组装都在 guard 之前写入); + - worker 长报告路径(`generatePersonalReportLongform` → `buildLongformCoverDocument`)。 +- 指纹**不进证据包**(bundle 会被发给模型),只在组装后的文档上写:`generatePersonalReport` 新增可选 `birthFingerprint`,在 `applyReportGuard` 之前写到候选文档上,随后照常走最终 server parse。 +- Schema:TS `subjectSchema`(v1/v2 共用)、`contracts/personal-report/report-document.v1|v2.schema.json`、Python `scripts/personal_report_contract.py` 都加可选 `birthFingerprint`(`^[0-9a-f]{32}$`)。旧文档不含该字段照常解析;格式不对的拒绝。 +- 测试:`frontend/tests/report-birth-binding.test.ts`(7 条:同算法 / 补时区前取;worker 加载器写入;三条路径接线;长报告写入与缺省;旧文档解析与坏格式;coverSubject 判定;GET 路由在服务端比对)、`personal-report-api.test.ts` 追加「inline generation writes subject.birthFingerprint」、`personal-report-generation-v2.test.ts` 追加「single-call and sectioned generation write subject.birthFingerprint only when given」(并断言分章调用发给模型的 bundle 不含指纹)、`tests/test_personal_report_contract.py` 追加 1 条。 + +### F2 · 服务端比对 + +- `reportView(row, job, failure, sections, coverSubject?)`:新增第 5 个可选参数,传入时输出 `coverSubject`。`resolveReportRead` 只在 `ready` 分支计算:读 `row.reportDocument.subject.birthFingerprint` → 没有就直接 `null`(不查库)→ 有则调新依赖 `loadCurrentBirthFingerprint(userId, subjectId)`(`subjectId` = `chart_profile_id ?? "self"`)→ 相等给 subject id,否则 `null`;加载器抛错(不是自己的人物、已删除、读不到)一律 `null`。 +- `GET /api/reports/:id` 用 `loadSubjectBirth(supabase, userId, subjectId)`(与星盘页同一个人物加载器,按 `user_id` 限定)取存储行再算指纹。 +- 响应里不含指纹:`reportDocument` 走 `projectOrdinaryReportDocument`,本来就不带 `subject`。 +- 测试(`personal-report-api.test.ts` 追加 4 条):一致(本人 `self`、家人给人物 id,序列化响应不含指纹、出生日期、经纬度、时区、地名);不一致;缺字段(且不查库);非本人(加载器抛错 / 返回空)。 + +### F3 · 报告封面 + +- `classifyReportEnvelope` 在 ready 态读 `report.coverSubject`(非空字符串才带上);`PersonalReportPage` 在 `.personal-report-reader-body` 正文最上方渲染 ``(`next/dynamic`,`ssr: false`)。 +- `components/birth-sky/report-sky-cover.tsx` + `report-sky-cover.css`:挂载后(正文已上屏)才取数;`lib/birth-sky/report-cover.ts` `loadReportSkyCover` 取天空并用同一个 `renderBirthSkyPng` 画 `night`(屏幕)与 `paper`(打印)两张;任何失败返回 `null`,组件什么也不渲染。无按钮、无占位、无等待态。 +- 打印:`.report-sky-cover-screen` 隐藏、`.report-sky-cover-print` 显示;`.report-sky-cover { break-after: page }`,图高 246mm 居中(A4 减报告 `@page` 边距后版心 186 × 270mm,3:4 在 186mm 宽时 248mm 高)。 +- `BLOCKED.md` 的 T5 条目已划掉并注明「→ 已解除:产品 2026-09-28 选 A」。 +- 测试:`frontend/tests/report-sky-cover.test.ts`(5 条:coverSubject 有 / 无;封面在正文之上且懒加载、无按钮无等待;night + paper 两张;取数失败 / 空 / 画图失败 / abort 都返回 null;打印 CSS 的 paper 与分页)。 diff --git a/docs/testing/birth-sky-cover-checklist.md b/docs/testing/birth-sky-cover-checklist.md index 8501af3f..a004252f 100644 --- a/docs/testing/birth-sky-cover-checklist.md +++ b/docs/testing/birth-sky-cover-checklist.md @@ -27,12 +27,18 @@ ## 报告封面 -本期**未上线**(报告与出生资料没有可靠绑定,见 `BLOCKED.md`)。验收时确认:打开任何报告,正文最上方**没有**天空封面、也没有空白占位;打印 PDF 第一页仍是原来的报告封面。 +~~本期**未上线**(报告与出生资料没有可靠绑定,见 `BLOCKED.md`)。验收时确认:打开任何报告,正文最上方**没有**天空封面、也没有空白占位;打印 PDF 第一页仍是原来的报告封面。~~ → 追加单(`TASK-birth-sky-followup-20260928`)已上线报告封面,原 11、12 条作废,改按下表。 | # | 设备 / 浏览器 | 操作 | 期望 | 结果 | | --- | --- | --- | --- | --- | -| 11 | 桌面 Chrome | 打开「我的报告」任一份 | 没有天空封面,没有占位 | | -| 12 | 桌面 Chrome | 该报告「打印 → 存为 PDF」 | 第一页与上线前相同 | | +| ~~11~~ | ~~桌面 Chrome~~ | ~~打开「我的报告」任一份~~ | ~~没有天空封面,没有占位~~ | 作废 | +| ~~12~~ | ~~桌面 Chrome~~ | ~~该报告「打印 → 存为 PDF」~~ | ~~第一页与上线前相同~~ | 作废 | +| 18 | 桌面 Chrome | 部署后**新生成**一份报告,打开 | 正文最上方一张深色 3:4 天空封面,宽与正文一致,下面没有按钮;正文先出来,封面随后出现,其间没有占位或转圈 | | +| 19 | 桌面 Chrome | 该报告「打印 → 存为 PDF」 | 第一页只有一张浅色纸面版天空封面;正文从第二页开始 | | +| 20 | 桌面 Chrome | 打开部署**之前**生成的旧报告 | 没有封面,没有空白占位 | | +| 21 | 任一 | 新生成一份报告 → 设置里改自己的出生时间(或出生地)→ 回来打开那份报告 | 没有封面;改完后再新生成的报告有封面 | | +| 22 | 桌面 Chrome | 打开有封面的报告,DevTools → Network 看 `/api/reports/` 的响应 | 响应里只有 `coverSubject`(`"self"` 或人物 id),没有 `birthFingerprint`、没有出生日期 / 时刻 / 经纬度 / 地名 | | +| 23 | 桌面 Chrome | 打开有封面的报告前,在 Request blocking 加一条 `*api/birth-sky*` | 报告正文照常显示,没有封面,也没有报错 | | ## 首次建盘揭幕 diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 6ac9ff34..838976d5 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -1150,9 +1150,15 @@ D40 / D45 / D60 的表格上方,出生时间不是「已校正」时多一句 - 弹层、画布与导出代码都在懒加载 chunk 里(`next/dynamic` + 动态 `import()`),首页 `/` 不带它们。按钮本身是星盘页里的一个普通 `