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

29 KiB
Raw Blame History

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
硬红线 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、模型凭据。