Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
242 lines
29 KiB
Markdown
242 lines
29 KiB
Markdown
# 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、模型凭据。
|