Files
Jyotisha/docs/tasks/PROGRESS-birth-sky-cover-20260928.md
T
Jesse_ChenandClaude Opus 5.5 be9e4dbee5
Independent Staging Quality Gate / validate (push) Successful in 13m10s
Independent Staging Quality Gate / publish (push) Successful in 4m5s
docs(tasks): Claude acceptance for birth-sky cover + followup
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-09-28 22:19:40 +08:00

242 lines
29 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.
# PROGRESS · 「那一刻的天空」封面(2026-09-28)
任务书:`docs/tasks/TASK-birth-sky-cover-20260928.md`。产品授权直接执行(Claude 子代理)。
- 基线:`origin/staging` = `d670521f`(任务书写作时为 `dc640931`;`TASK-serif-headings` 与 BUG-1079 按钮配色都已在基线里)
- 分支 / 工作树:`codex/birth-sky-cover-20260928` / `.worktrees/birth-sky-cover-20260928`(三个本地提交,**未推送**,由 Claude 验收后推)
- 环境:Node v22.14.0(`/exec-daemon/node`);无 `.venv`,Python 一律系统 `python3`(3.13,pyswisseph 2.10.03);无 Docker;有 `/usr/bin/google-chrome`(无头)。
- BUG 编号:开工时最大 BUG-1079。新功能,本单未开 BUG。
## 结论
| 项 | 结果 | 提交 |
| --- | --- | --- |
| T1 天空计算模块 | 完成 | 1 |
| T2 BFF 路由与合同 | 完成 | 1 |
| T3 封面渲染与星盘页入口 | 完成 | 1 |
| T4 保存图片 | 完成 | 1 |
| T7 文档 | 完成(DESIGN §17、VOICE、CONTEXT、CHANGELOG、真机清单、任务索引) | 1(后两提交各自补本期段落) |
| T5 报告封面 | **blocked,未实现**:报告没有出生资料绑定,按任务书不显示,写进 `BLOCKED.md` | 2 |
| T6 首次建盘揭幕 | 完成 | 3 |
| 让步 | 未砍 T6;T5 是 blocked 不是砍;三组连线都保留 | — |
## T1 · `scripts/birth_sky.py`
- `compute_birth_sky(date, time, tz_offset_hours, lat, lon)`:本地时刻减时区得 UT,`swe.julday` 得 JD。
- 行星:`swe.calc_ut`(默认旗标,回归黄道视位置)→ `swe.azalt(jd, ECL2HOR, (lon, lat, 0), 0, 10, …)`,取几何高度(`atpress = 0`,不加折射)。方位 = swe 方位 + 180°(swe 自正南起算;注释写明)。七颗都返回(月亮在地平线下也要进事实句),前端只画地平线以上的。
- 恒星:`scripts/data/bright_stars.json`,耶鲁亮星表第 5 版(VizieR V/50,`https://cdsarc.cds.unistra.fr/ftp/V/50/catalog.gz`,sha256 `3dc44b1e90be8fbe5bcc7656032560f51275f985c7e3f783c9028e1838ec7bed`;NASA ADC 产品,CDS 无限制分发,公有领域)。V ≤ 4.0 共 518 颗,J2000,不加自行。生成脚本 `scripts/build_bright_stars.py` 校验 sha256,原始星表不进仓。IAU 1976(Lieske)岁差换到出生历元,再 `swe.azalt(EQU2HOR)`;只返回地平线以上的星。连线用 HR 号:北斗(含 Megrez–Dubhe 闭合)、仙后 W、猎户(肩、腰带、脚、头)。
- `phase`:太阳高度 > 0 `day`;−18°~0° 按太阳本地时角分上午 / 下午(真太阳时,0~180° 为下午);< −18° `night`。
- 返回不带任何输入字段。`POST /api/birth_sky` 在 `do_POST` 里 7 行薄注册(与 `/api/ephemeris_events` 同样式):`JyotishAPIHandler` 方法数 218 不变,`__new__` 计数不变,文件 10935 → 10942 行(上限 11224)。不是重计算路径,不进 `HEAVY_COMPUTE_PATHS`。
- `tests/test_birth_sky.py`(14 条):样稿 golden(太阳 31.71° / 84.69°,土星 50.31° / 191.14°,月亮、木星在地平线下);三个纬度 / 日期的真太阳时正午高度 = 90 − |纬度 − 赤纬|(±0.5°,赤纬取 swe 赤道坐标,逐分钟扫最高点);北极星高度 30.8°(±1°)、方位近正北;四种 phase;输出不含输入;星表来源与许可;岁差对 Meeus 例 21.b 精确到 1e-5°;参数校验;HTTP 薄注册 200 / 400。已加入 quick 门禁列表(`run_quality_gate.py` +2 行)。
- golden 生成脚本 `scripts/generate_birth_sky_golden.py` → `frontend/tests/fixtures/birth-sky-golden.json`(样稿虚构资料四个时刻的真实引擎输出,前端测试原样读取)。
## T2 · `GET /api/birth-sky?subject=`
- `lib/birth-sky/service-core.ts` `assembleBirthSky`:`loadSubjectBirth` → `prepareChartViewProfile`(与星盘页同一条路)→ 引擎 → `birthSkyFromEngine`(zod 合同 `lib/birth-sky/contract.ts`)。`timeBasis` = `birthTimeStatus` 为 `accepted` / `confirmed` 时 `adopted`,否则 `reported`。
- 状态:未登录 401 `unauthenticated`;别人的 / 不存在的 subject 404 `not_found`;资料不全 200 `birth_profile_incomplete`;引擎失败、坏载荷、时区解析失败 503 `unavailable`;每用户每分钟 20 次,超了 429 `unavailable`。只读 `?subject=`,不接受客户端日期经纬度;不计费、不写库;响应 `no-store`。
- `frontend/tests/birth-sky-route.test.ts`(6 条):401、404、incomplete(本人缺坐标 / 家人 `subject_incomplete`)、正常返回过合同且 JSON 不含 `latitude/longitude/date/time/timezone/placeLabel/name` 键与 1994、07:40、30.27、120.15、虚构名等值;adopted 标注;引擎抛错与坏载荷 503;路由源码只读 `subject`、服务端取出生资料、无计费与写库。
## T3 / T4 · 渲染、入口、保存
- `lib/birth-sky/sentence.ts`:四句主句表与事实句规则原样照任务书;样稿资料得「太阳在东,月亮在脚下,火星正悬在东南」。
- `lib/birth-sky/draw.ts` `drawBirthSky(ctx, sky, { palette, width, height, footnote?, serif? })`:Canvas 2D 纯函数,600 × 800 设计格按宽度缩放,导出 1080 × 1440。两套 palette 取 DESIGN token(`night` 深色主题页底 / 墨 / 细线 / 深色 action 画太阳;`paper` 报告纸色 / 墨 / `--report-rule` / `--report-accent`)。行星名做了简单避让(四个候选位)。
- `lib/birth-sky/render.ts`:`document.fonts.load('600 30px "Jyotisha Serif SC"', 主句+顶部小字)`,最多等 1.5 秒,超时或失败用黑体画;离屏 canvas → `toBlob("image/png")`。
- `lib/birth-sky/export.ts`:文件名 `jyotisha-sky.png`;`navigator.canShare({ files })` 真 → `navigator.share`;`AbortError`(用户取消)返回 `cancelled` 不提示;其余走 Blob + `a.download`。
- `hooks/use-birth-sky-cover.ts`:星盘页 `ok` 后按 `subjectId:profileFingerprint` 预取并画好 PNG,画好才返回;切人物 / 改资料键变即重取,旧请求 abort、旧 blob URL 释放。
- `ChartPageView` 新增可选 `birthSky` prop,`view.status === "ok" && birthSky` 时往 `actions` 放 `BirthSkyEntry`(「那一刻的天空」描边小按钮)。弹层 `components/birth-sky/birth-sky-dialog.tsx`(原生 `<dialog>`,窄屏全屏、≥860px 居中 520px;`<img>` + 「保存图片」),与其 CSS、画布、导出一起懒加载。
- 测试:`birth-sky-sentence.test.ts`(6)、`birth-sky-draw.test.ts`(8:1080×1440、全部文字不含日期/时刻/经纬度/地名/名字、footnote 两态、东在左且真实太阳画在圆心左侧、地平线下不画、宋体与超时退回黑体、paper palette、连线只连可见星)、`birth-sky-export.test.ts`(5:share 可用 / 不可用 / 用户取消 / share 失败回落下载 / 字体等待超时)、`chart-page-view.test.tsx` 追加 2(就绪才有按钮;未就绪、失败、资料不全、视图未到都无按钮;预取键与懒加载源码合同)。
海报(样稿虚构资料,真实引擎 golden → 同一个 `drawBirthSky` → 无头 Chrome 里 `canvas.toDataURL` 取出的 PNG,含宋体切片):
- `/tmp/claude-1000/-workspace-Jyotisha/d49dfa04-4e7a-47b2-80a7-8529c4c38f86/scratchpad/birth-sky-poster-day-night.png`(07:40,白天,night palette,「按你填写的时间」)
- 同目录 `birth-sky-poster-night-night.png`(00:00,夜里,adopted,北斗与仙后可见)
- 同目录 `birth-sky-poster-twilight_evening-paper.png`(19:10,傍晚,paper palette,猎户可见)
- 同目录 `dialog-390x844.png`、`dialog-1280x900.png`(弹层版式静态样张,截在淡入途中)
## T5 · 报告封面:blocked
证据与解除方案见 `BLOCKED.md`「TASK-birth-sky-cover T5」。要点:`personal_reports` 行、报告文档、job / section / appendix 表都没有出生资料指纹;`request_fingerprint` 不含出生资料;`calculation_hash` 是引擎结果哈希或 markdown 哈希。按任务书不显示封面,`reportView`、报告页、打印样式都未改。`paper` palette 已在 T3 实现并有单测,解除后可复用。
## T6 · 首次建盘揭幕
- `lib/birth-sky/reveal.ts` `startBirthSkyReveal`:3 秒计时;期限内拿到画好的封面才 `onReady`;超时 abort 请求、迟到的封面释放 URL;失败静默,不重试。
- `hooks/use-birth-sky-reveal.ts` 自持 `cover` 状态;`useProfileOnboarding` 新增可选参数 `onOwnBirthPlaceSaved`,只在 `saveOnboardingPlace` 保存成功、且下一步已设好之后调用(fire-and-forget)。`page.tsx` 只接线:+1 个自定义 hook 调用、+1 个参数、+1 个 `<BirthSkyReveal>`、+2 行 import(1370 → 1375 行;`Home()` 的 `useState` / `useRef` 数不变)。
- 弹层与星盘页同一个组件,`reveal` 时加 `is-reveal`(600ms 透明度淡入,`prefers-reduced-motion` 关闭)与「继续」。关闭只清 reveal 自己的状态。
- `frontend/tests/profile-onboarding-birth-sky-reveal.test.ts`(5):期限内出现、关闭不碰 onboarding;超时无弹层且迟到封面被释放、请求被 abort;失败无弹层无重试;只由本人 onboarding 出生地保存成功触发(`saveProfile`、星盘档案、`chart-profile-form` 不触发);弹层「继续」/ Esc / 遮罩与淡入 CSS。
## 验证
| 项 | 基线 `d670521f` | 提交 1 `d277cbd8` | 交付(提交 3) |
| --- | --- | --- | --- |
| `tsc --noEmit` | 0 错 | 0 错 | 0 错 |
| `npm run lint` | 0 error / 127 warning | — | 0 error / 127 warning |
| `npm test` | tests 4255 · pass 4200 · fail 24 · cancelled 0 · skipped 31 | tests 4282 · pass 4227 · fail 24 | tests 4287 · pass 4232 · fail 24 · cancelled 0 · skipped 31 |
| 失败清单 | 24 条(全是需要 Docker / PostgreSQL 的数据库与部署套件) | 与基线逐条同名 | 与基线逐条同名 |
| 测试名对比(`frontend/AGENTS.md` 方法) | — | 消失 0 | 消失 0,新增 32 |
| `python3 -m pytest tests/test_birth_sky.py tests/test_api_server_growth_contract.py tests/test_readonly_chart_endpoints.py` | — | — | 全部通过 |
| `python3 scripts/run_quality_gate.py --profile quick`(Node 22 在 PATH) | — | — | 所有 Python 步骤通过(pytest 997 passed / 1 skipped,含新加的 `tests/test_birth_sky.py`);最后一步 `npm test` 的 24 条失败与基线逐条同名(无 Docker)→ 退出码 1,属环境缺口 |
| `python3 -m pytest tests/test_repo_privacy_markers.py -q` | — | — | 通过 |
| `next build`(`rm -rf .next` 后全量) | `/` ○ Static | — | `/` ○ Static;路由表与基线逐行相同,只多一行 `ƒ /api/birth-sky` |
### 首屏 gzip(`index.html` 引用的全部 js / css,gzip -9 求和,两边都是干净全量构建)
| | 基线 | 交付 | 变化 |
| --- | ---: | ---: | ---: |
| JS | 651,590 B | 652,363 B | +773 B(+0.12%) |
| CSS | 66,346 B | 66,346 B | 0 |
JS 的 +773 B 是首页为 T6 引入的 `useBirthSkyReveal`、`startBirthSkyReveal`、`loadBirthSkyCover` / `fetchBirthSky` 与 `next/dynamic` 占位;弹层、画布、导出、`birth-sky.css` 都在懒 chunk。
## 改动的既有断言
无。`chart-page-view.test.tsx` 只追加了两条测试和一个 import,未改任何既有断言。
## 偏离任务书(请 Claude 定)
1. **星盘页按钮本身不懒加载**:`BirthSkyEntry` 是星盘页 chunk 里一个普通 `<Button>`(只在图画好后渲染),弹层、CSS、画布、导出都懒加载。原因:按钮要能在 SSR 测试里断言「就绪才出现」,且它不在首页 `/`。
2. **事实句里的「最高的一颗可见行星」同时排除太阳与月亮**(月亮已有自己的一段)。任务书写「除太阳外」;样稿结果不变。
3. **晨昏按太阳本地时角分上午 / 下午**(真太阳时),不按钟表时间。
4. **顶部小字的字距用逐字绘制实现**,不用 canvas `letterSpacing`(部分 Safari 不支持;无头 Chrome 里设了又复位后后续文字仍带字距)。
5. **BFF 增加每用户每分钟 20 次的限流**(与星盘页同量级),任务书未写。
6. **宽屏弹层 520px**(报告导出抽屉是 720px),3:4 的图在 720 宽时会被高度卡住、两侧空白。
7. **T6 的 3 秒包含取数与画 PNG**(含最多 1.5 秒字体等待)。
8. VOICE 对照:四句主句与事实句未发现与 VOICE 其余条款冲突(短句、无 emoji、无揣测、不讲运势);主句带句号、事实句不带,照样稿。新增一句保存失败文案「没能保存。长按或右键上面的图,也能存下来。」已收进 VOICE。
## 环境缺口
- 无登录态与真机:星盘页按钮、弹层、分享 / 下载、首次揭幕只在单元测试、SSR 渲染、无头 Chrome 静态样张上验证;真机清单 `docs/testing/birth-sky-cover-checklist.md`(17 条)。
- 无 Docker:24 条 DB / 部署套件失败与基线逐条同名;quick 门禁最后一步 `npm test` 因此退出码 1。
- 无 `.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` 正文最上方渲染 `<ReportSkyCover subjectId=…/>`(`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 与分页)。
### F4 · 首次建盘群星汇聚过场(替换 T6 弹窗)
- 触发不变:仍只由 `saveOnboardingPlace` 成功后的 `onOwnBirthPlaceSaved?.()` 触发;`saveProfile`、星盘档案、`chart-profile-form` 不触发(原合同测试保留)。`page.tsx` 未改一行(接线 `<BirthSkyReveal cover=… onClose=…/>` 与 `onOwnBirthPlaceSaved: birthSkyReveal.begin` 原样),`Home()` 的 `useState` / `useRef` 数不变。
- 不等待:`startBirthSkyReveal` 改成泛型,仍是 3 秒预算;预算内要到齐三样——`/api/birth-sky?subject=self`、宋体(`waitForHeadingSerif`,最多 1.5 秒)、过场 chunk(`import("@/components/birth-sky/birth-sky-converge")`)。任一失败或超时就静默跳过,无 spinner / 骨架。
- 纯函数 `lib/birth-sky/converge.ts`:`birthSkyScatter`(种子取自这张盘可见天体的投影坐标,mulberry32,均匀撒在地平线圆 95% 半径内)、`birthSkyPositionsAt(sky, scatter, t)`、`interpolateSkyPoint`(在 `draw.ts`,t ≥ 1 精确返回真实点)、`easeOutCubic`、`birthSkyRevealAt(elapsed, reduced)` 时间轴:0–1.5 s 汇聚(ease-out)+ 地平线 / 高度圈 / 方位字 / 连线 / 行星名淡入、整层前 200ms 淡入;1.5–2.5 s 文字 400ms 淡入后停留;2.5–3 s 整层淡出;reduced-motion 不汇聚,400ms 淡入、停留、同时刻淡出。
- `drawBirthSky(ctx, sky, options, frame?)`:新增可选 `frame`,只对位置插值、对透明度乘系数;不传或全为 1 时调用序列与原来逐条相同。
- 过场组件 `components/birth-sky/birth-sky-converge.tsx` + `.css`(懒加载):原生 `<dialog>` 全屏、底色取 `night` palette;画布取视口内最大 3:4 区域 × `devicePixelRatio`;`requestAnimationFrame` 每帧调 `drawBirthSky`;`pointerdown`(点击 / 触摸)与 Esc(`cancel`)立即结束;卸载时 `cancelAnimationFrame`、摘监听、`close()`。无按钮。
- 结束提示:`useBirthSkyReveal.close` 只在过场真的放过后执行一次(`playing` ref 守卫),`toast("这片天空在星盘页可以保存。")`,复用 `layout.tsx` 已挂的 sonner Toaster;这句收进 VOICE.md。失败 / 超时没有过场也没有提示。
- 删除:`BirthSkyDialog` 的 `reveal` 属性、「继续」按钮、`birth-sky.css` 的 `is-reveal` 样式与 `@keyframes birth-sky-reveal`。星盘页弹窗其余不动。
- 四帧 PNG(样稿虚构资料的真实引擎 golden → 同一个 `drawBirthSky`,1080 × 1440,无头 Chrome `canvas.toDataURL` + `--dump-dom` 取出;为排除字体加载时序,这组帧与对照导出都用黑体画):
- `/tmp/claude-1000/-workspace-Jyotisha/fb88e01e-1ced-4bcc-92fe-44c5bdcc878e/scratchpad/birth-sky-converge-night-t0.png`(0 ms,散开)
- 同目录 `birth-sky-converge-night-t050.png`(750 ms,汇聚过半,地平线与方位字半透明)
- 同目录 `birth-sky-converge-night-t100.png`(1500 ms,到位,文字未出)
- 同目录 `birth-sky-converge-night-hold.png`(2400 ms,停留)
- 另有 `day` 一组同名四张。**停留帧与 `drawBirthSky` 导出封面的 PNG dataURL 逐字节相同**(night、day 两组都是 `true`)。
- 测试:`profile-onboarding-birth-sky-reveal.test.ts` 由 5 条改为 10 条(见下「改动的既有断言」)。
### 改动的既有断言(本分支第 3 个提交 `14ef18ac` 新增的测试;基线 `d670521f` 里没有这些测试)
| 测试 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| saved in time…(改名 → 「the convergence starts once its data is ready; ending touches nothing else」) | 载荷类型 `BirthSkyCover`,名字说 dialog | 载荷类型 `BirthSkyRevealSky`(天空 + 宋体结论),断言不变 | F4 把弹窗换成过场,载荷不再是画好的 PNG |
| timeout…(改名 → 「no overlay, the late data is dropped…」) | 同上 | 同上,断言不变 | 同上 |
| failure…(改名 → 「no overlay, no retry, no error surfaced」) | `doesNotMatch(revealHook, /toast\|setError\|console…/)` | `doesNotMatch(revealHook, /setError\|console…/)`,另加:hook 里 `toast(` 恰好 1 处,且不在 `onReady` 里 | F4 要求过场结束后提示一次;失败路径仍然不提示(`close` 只在 `playing` 为真时执行,另有新测试锁住) |
| only the reader's own onboarding place save triggers it… | `revealHook` 匹配 `loadBirthSkyCover("self:reveal", "self", signal)` | 匹配 `loadBirthSkyRevealSky(signal)` | 过场不再需要 PNG,只取天空与字体 |
| the reveal is the chart page's dialog plus 「继续」, faded in 600ms…(**删除**) | 断言弹窗 `reveal` 变体、「继续」、`is-reveal` 600ms 淡入 CSS | 由「overlay: full screen, no buttons…」「timeline…」「the chart page dialog no longer has a reveal variant」三条取代 | 任务书 F4 明令删除弹窗 reveal 变体 |
基线 `d670521f` 的既有断言:无改动。
## 追加单验证(两个提交完成后)
| 项 | 基线 `d670521f` | 交付(第 5 个提交) |
| --- | --- | --- |
| `tsc --noEmit` | 0 错 | 0 错 |
| `npm run lint` | 0 error / 127 warning | 0 error / 127 warning |
| `npm test` | tests 4255 · pass 4200 · fail 24 · cancelled 0 · skipped 31 | tests 4310 · pass 4255 · fail 24 · cancelled 0 · skipped 31 |
| 测试名对比(`frontend/AGENTS.md` 方法) | — | 对基线:消失 0;对本分支第 3 个提交:消失 4(上表 4 条改名 / 删除,均有替代),新增 28 |
| 失败清单 | 24 条(Docker / PostgreSQL 数据库与部署套件) | 与基线逐条同名 |
| `python3 -m pytest tests/test_birth_sky.py tests/test_api_server_growth_contract.py tests/test_personal_report_contract.py` | — | 63 passed |
| `python3 scripts/run_quality_gate.py --profile quick`(Node 22 在 PATH) | — | 所有 Python 步骤通过(pytest 997 passed / 1 skipped);最后一步 `npm test` 的 24 条失败与基线逐条同名 → 退出码 1,属无 Docker 环境缺口 |
| `python3 -m pytest tests/test_repo_privacy_markers.py -q` | — | 通过 |
| `rm -rf .next && next build` | `/` ○ Static | `/` ○ Static;路由表与第 3 个提交交付时逐行相同 |
| `JyotishAPIHandler` 方法数 / `__new__` 计数 | 218 / 不变 | 未改 Python API 服务文件 |
| `Home()` 的 `useState` / `useRef` 数 | — | `page.tsx` 未改 |
### 首屏 gzip(`index.html` 引用的全部 js / css,gzip -9 求和,干净全量构建)
| | 基线 | 第 3 个提交交付 | 本次交付 | 对基线变化 |
| --- | ---: | ---: | ---: | ---: |
| JS | 651,590 B | 652,363 B | 652,605 B | +1,015 B(+0.16%) |
| CSS | 66,346 B | 66,346 B | 66,346 B | 0 |
比第 3 个提交多的 242 B 是首页 hook 里新增的 `sonner` 提示调用、泛型 `startBirthSkyReveal`、`loadBirthSkyRevealSky`;过场组件、`converge.ts`、`draw.ts`、样式都在懒 chunk。报告封面组件在报告页的懒 chunk,不进首页。
## 追加单偏离任务书(请 Claude 定)
1. **指纹只写进报告文档,不写进 v2 evidence bundle。** 任务书写「v2 evidence bundle / 报告文档的 schema 加这个可选字段」。bundle 会原样发给模型(分章写作),把指纹放进去等于把一个出生资料派生值送给供应商,也会改变 bundle 哈希;所以只在组装后的文档(guard 之前)写入,TS / JSON Schema / Python 三处文档合同都加了可选字段。测试断言分章调用发给模型的 bundle 不含指纹。
2. **指纹覆盖姓名。** 同一算法(`resolveServerOwnedChartBirth`)的指纹里本来就含 `name`、岁差等字段,所以改了人物名字或岁差之后,旧报告也会失去封面。这是「与星盘页同一种算法」的直接结果,未另造一个只含出生时刻 / 地点的指纹。
3. **`coverSubject` 只在 ready 读取时下发**(创建响应、replay、失败 / 生成中都不带该键)。
4. **过场用原生 `<dialog>`** 而不是普通 `div`:保证在兑换弹窗(同为顶层)之上,Esc 走 `cancel` 事件;无按钮、无焦点陷阱内容。
5. **整层前 200ms 淡入**(任务书未写),避免保存后页面瞬间变黑;reduced-motion 下整体淡入 400ms。
6. **四帧的「t=0.5」取时间过半(750 ms)**,此时 ease-out 位置进度约 0.875;纯函数测试里的 t 是位置插值参数本身(t=0 散开、t=1 真实)。
7. **报告封面打印高 246mm**(版心 186 × 270mm,3:4 满宽是 248mm,留 2mm 余量防止溢出到第二页)。
## 追加单环境缺口
- 无登录态与真机:报告封面(屏幕 / 打印 PDF)、过场(真机点击跳过、减弱动态效果、慢网)只在单测、源码合同与无头 Chrome 画布取帧上验证;真机清单 `docs/testing/birth-sky-cover-checklist.md` 新增 18–30 条(11、12、13、14、16 作废)。
- 无 Docker:24 条 DB / 部署套件失败与基线逐条同名;quick 门禁最后一步因此退出码 1。本单不改表结构、不加迁移。
- 无模型凭据:真实报告生成(含写入指纹的三条路径)只在注入假代理的单测里跑过,部署后需新生成一份报告复核(清单第 18 条)。
- 未推送、未部署(按任务书由 Claude 验收后整体快进推 staging)。
## Claude 验收(2026-09-28,分支五个提交,`731c3b36`)
在同一工作树上独立重跑,不采信执行方数字。
| 项 | 结论 | 证据 |
| --- | --- | --- |
| `tsc --noEmit` | 通过 | 0 错 |
| `npm run lint` | 通过 | 0 error / 127 warning(与基线同) |
| `npm test` | 通过(环境缺口除外) | tests 4310 · pass 4255 · fail 24 · skipped 31;24 条失败逐条同名于基线,全部是 Docker / PostgreSQL / 部署套件 |
| pytest `test_birth_sky` + `test_api_server_growth_contract` + `test_personal_report_contract` + `test_repo_privacy_markers` | 通过 | 全部 passed |
| quick 门禁 | Python 部分通过 | 997 passed / 1 skipped;末步 `npm test` 因无 Docker 退出码 1 |
| `rm -rf .next && next build` | 通过 | `/` ○ Static |
| 首屏 gzip | 通过 | Claude 口径(`index.html` 里 `/_next/static/*.js|css` 逐个 gzip -9 求和)另建基线 `d670521f` 对比:JS 649,819 → 650,883 B(+0.16%),CSS 66,126 → 66,126 B(0)。与执行方口径绝对值不同、相对变化一致 |
| 硬红线 1 指纹不下发 | 通过 | 报告详情只下发 `coverSubject`;`reportDocument` 走白名单投影不含 `subject`;测试断言响应体不含指纹与出生字段;指纹不进模型 bundle |
| 硬红线 2 不改表 | 通过 | 无迁移、无 schema 目录改动 |
| 硬红线 3 增长 | 通过 | `jyotish_api_server.py` 本系列仅 T2 的 7 行薄注册;`page.tsx` 追加单未改 |
| 硬红线 4 不等待 | 通过 | 3 秒预算含取数 / 字体 / chunk,超时静默;报告封面失败返回 null 不占位 |
| 硬红线 5 末帧一致 | 通过 | `interpolateSkyPoint` t≥1 精确返回真实点、alpha 乘 1;测试逐条比对画布调用序列;执行方取帧停留帧与导出 PNG dataURL 相同 |
| 四帧 | 已看 | scratchpad `birth-sky-converge-night-{t0,t050,t100,hold}.png`:散开 → 半程 → 到位无字 → 停留有字 |
偏离裁定:1(指纹不进 bundle)采纳,比任务书更稳;2(指纹含姓名 / 岁差,改名后旧报告失去封面)接受,属「同一种算法」的直接后果,已写给产品;3–7 接受。
环境缺口不变:真机 / 登录态(清单 18–30 条)、Docker、模型凭据。