Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
19 KiB
TASK · 「那一刻的天空」封面:星盘页入口、报告封面、首次建盘揭幕(2026-09-28)
基线
origin/staging=dc640931(2026-09-28)。开工时git fetch origin --prune,以最新origin/staging为基线,实际 SHA 写进 PROGRESS。- 分支
codex/birth-sky-cover-20260928,工作树.worktrees/birth-sky-cover-20260928。 - 进度记录
docs/tasks/PROGRESS-birth-sky-cover-20260928.md。 - 串行关系:
TASK-serif-headings-20260928先做(两单都动globals.css与 DESIGN.md §3);本单以它合入后的 staging 为基线。它若未合入,本单照写家族名,加载不到时自动退回黑体。TASK-site-button-contrast-20260928(BUG-1079)改全站按钮配色。它若已合入 staging,就以它为基线;若还没合入,本单新增的按钮一律用现有共享按钮类(.button-secondary/<Button variant="outline">),不自己写按钮颜色,这样两单没有文件交集。同日其它任务书与本单无交集。 - 视觉样稿:
docs/tasks/assets/birth-sky-cover-sample-20260928.html,看第三张「C · 那一刻的天空」。A、B 两张是被否掉的方案,只作对照,不实现。
这是什么(产品背景,不是事故)
这是新功能,没有事故实证。产品看了竞品的「人设标签 + 几何徽章」分享海报,要求不照抄,做一张符合本站气质(私人阅览室、安静、诚实、不给人贴标签)的封面。三版样稿里产品选了 C:
出生地、出生那一刻头顶的真实天空。天顶在圆心、东在左,地平线以上的行星与亮星照实画,下方一句话。不讲运势,不贴标签。
现状实证(Claude 2026-09-28 读码,行号按符号定位)
| 事实 | 位置 |
|---|---|
星盘页标题栏已有 actions?: ReactNode 插槽,星盘页目前没传 |
SecondaryPageShell(frontend/src/components/secondary-page-shell.tsx)→ SecondaryHeader;调用方 ChartPageView |
星盘页数据里有 date、time(本地 HH:MM)、timezoneOffset、latitude、longitude、birthTimeStatus,没有 UTC 瞬间 / JD |
chartViewProfileSchema(frontend/src/lib/chart-view-contract.ts) |
看谁的盘由 useCurrentSubject 决定(localStorage,不在 URL);BFF 用 ?subject= + loadSubjectBirth 在服务端取出生资料 |
lib/current-subject.ts、lib/subject-birth.ts、app/api/chart-view/route.ts |
服务端「是否采用了校正时间」只在 resolveServerOwnedChartBirth 的 adoption 里,不下发客户端 |
lib/chart-birth-truth.ts |
Python 侧没有任何 azalt / 赤经赤纬 / 恒星表代码,仓库里也没有 sefstars.txt |
scripts/ 全量 grep |
报告页没有封面:SecondaryHeader → ReportActions → 正文;ReportEnvelopeView 不带出生资料,DB 行有 chart_profile_id,但 reportView 没暴露 |
PersonalReportPage、app/(app)/reports/[reportId]/page.tsx、lib/personal-report-route-core.ts |
首次建盘在首页 onboarding 里完成:saveOnboardingPlace 保存后留在 /,不跳转 |
hooks/use-profile-onboarding.ts、app/(app)/page.tsx |
前端没有任何 SVG / DOM 转 PNG 的代码;唯一下载模式是 Blob + a.download |
lib/consultation-report-export.ts downloadMarkdownReport |
可复用的弹层:原生 <dialog>,窄屏是底部抽屉,宽屏居中 |
ReportExportDrawer、ChartTypePicker;DESIGN.md「报告导出抽屉」一节 |
行数余量:page.tsx 1370 / 上限 1567;jyotish_api_server.py 10935 / 上限 11224 |
home-shell-growth-contract.test.ts、tests/test_api_server_growth_contract.py |
决策记录(产品 2026-09-28 授权)
- 画面选 C(真实天空),A 年轮、B 一笔画都不做。
- 三个位置都做,按顺序:① 星盘页入口 + 预览 + 保存图片;② 个人报告封面;③ 自己第一次建完盘时揭幕一次。
- 不在图上印任何出生资料:不印姓名、日期、时刻、经纬度、地名。天空本身会透露大致时段,产品确认可以接受。
- 图上的那句话只描述天空,不讲运势:不写大运、上升、性格。句子从下文「句库」按天空状态确定性地选出,同一张盘永远同一句,不调模型、不扣点。
- 出生时间未采用校正的也能生成:
birthTimeStatus不是accepted/confirmed时,图底加一行小字「按你填写的时间」。 - 不另开页面,不加侧栏入口,不做第六个 Tab;星盘页只在标题栏右侧加一个按钮。
- 标题用宋体(产品 2026-09-28 追加决定,见
TASK-serif-headings-20260928):封面主句与顶部小字用字体家族"Jyotisha Serif SC",由那份任务书自托管;事实句、底部说明仍用站内黑体栈。画布绘制前先await document.fonts.load('600 30px "Jyotisha Serif SC"', 主句),最多等 1.5 秒;超时就用黑体画,不出现等待态。不引入 Google Fonts。 - 第一期不接社交平台分享 SDK。保存图片走浏览器原生能力(见 T4)。
- 罗睺、计都不是可见天体,不画。月亮在地平线下时不画,只进句库的事实句。
硬红线
scripts/jyotish_api_server.py只做薄注册:JyotishAPIHandler类方法数不得增长、__new__伪造点不得增长;计算放新模块scripts/birth_sky.py(AGENTS §6,由tests/test_api_server_growth_contract.py执行)。frontend/src/app/(app)/page.tsx:Home()的useState/useRef数不得增长;揭幕逻辑进hooks/或组件,由它们持有自己的状态,page.tsx只接线(AGENTS §6,由home-shell-growth-contract.test.ts执行)。- 不新增 npm 依赖。PNG 用原生 Canvas 2D 导出。
- 封面代码全部懒加载(
next/dynamic或动态import()):next build后/仍是○ Static,首屏 gzip ±2%。 - 出生资料只在服务端取(
loadSubjectBirth/resolveServerOwnedChartBirth),不接受客户端传来的日期、经纬度,防止借接口给任意时刻地点算图。 - 等待遵守 DESIGN §9:不写新 spinner 或骨架。星盘页进入后预取天空数据,点按钮时图已经就绪(见 T3);首次揭幕拿不到数据就静默跳过,不等待(见 T6)。
- 恒星表只用公有领域来源(Yale Bright Star Catalogue 5th ed.,HR 编号),来源与许可写进数据文件头和 PROGRESS。不得从有版权的星图软件抓数据。
- 不改数据库结构。一次性揭幕不需要「看过」标记(见 T6)。
- 测试 fixture 来自真实引擎输出(golden),不手造形状(§7.4)。
- 隐私:测试、PROGRESS、CHANGELOG 只用样稿里的虚构资料(1994-05-18 07:40 UTC+8,30.27N 120.15E)或公开名人资料。
任务分解
T1 · 天空计算模块(Python)
新模块 scripts/birth_sky.py,提供纯函数 compute_birth_sky(date, time, tz_offset_hours, lat, lon) -> dict,在 jyotish_api_server.py 里薄注册为 POST /api/birth_sky。
- 瞬间:由本地日期时刻与
tz_offset算 UT 与 JD。 - 行星:太阳、月亮、水、金、火、木、土的真实高度角与方位角(
swe.calc_ut+swe.azalt,取几何高度 true altitude,不加大气折射;golden 按此口径)。方位角统一成「自正北起、向东为正」,注意 swisseph 的方位角自正南起算,要换算并在注释里写明。 - 恒星:
scripts/data/bright_stars.json收录 V 星等 ≤ 4.0 的恒星(HR 编号、J2000 赤经赤纬、星等),按 IAU 1976 岁差换到出生历元,再换算高度方位。只返回地平线以上的星。另附三组连线(北斗七星、仙后座 W、猎户座),用 HR 编号表示。 - 状态:
sunAltitude,以及phase∈day(太阳高度 > 0)/twilight_morning/twilight_evening(−18° 到 0°,按上午或下午区分)/night。 - 返回里不带输入的日期、时刻、经纬度。
验收
tests/test_birth_sky.py:- 样稿虚构资料的 golden:太阳高度 31.7°±0.2°、方位 84.7°±0.3°(自北),土星高度 50.3°±0.2°、方位 191.1°±0.3°,月亮与木星在地平线下。
- 独立校验:任意日期当地真太阳时正午,太阳高度 ≈ 90° − |纬度 − 赤纬|,误差 ±0.5°。
- 北极星在北纬 30.27° 的高度约 30°(±1°)。
phase四种状态各有一个用例。
tests/test_api_server_growth_contract.py通过。- 快速门
run_quality_gate.py --profile quick通过。
T2 · BFF 路由与合同(前端服务端)
frontend/src/app/api/birth-sky/route.ts:GET ?subject=<id>。
- 服务端用与星盘页相同的方式解析这个 subject 的出生资料和
adoption。 - 调用
/api/birth_sky,返回 zod 合同birthSkyResponseSchema(放在lib/birth-sky/contract.ts):phase、sunAltitude、planets[]、stars[]、lines[]、timeBasis: "adopted" | "reported"。 - 资料不全时返回
status: "birth_profile_incomplete",前端据此不显示入口。 - 不计费、不写库。
- 响应里不带出生资料原文。
验收
frontend/tests/birth-sky-route.test.ts:- 未登录返回 401;
- 别人的 subject 返回 404;
- 资料不全返回 incomplete;
- 正常返回符合合同,且序列化后的 JSON 不含
latitude/longitude/date/time/ 地名字段。
T3 · 封面渲染与星盘页入口(第一期)
-
lib/birth-sky/draw.ts提供一个纯函数drawBirthSky(ctx, data, { palette, width, height, footnote }),用 Canvas 2D 画整张 3:4 封面。预览和导出共用这一个函数,避免 SVG 转图片时网页字体丢失。- 投影:天顶在圆心,r = (90 − 高度)/90 × R;北在上、东在左。
- 画面元素:
- 地平线圆,30° / 60° 虚线高度圈;
- 东南西北四个方位字;
- 恒星点,大小按星等;
- 三组连线;
- 行星用小圆点加中文名,太阳用赭色空心圆加光晕。
- 文字排版对照样稿 C:
- 顶部小字
JYOTISHA · 那 一 刻; - 句库主句;
- 底部事实句;
- 最底
仅供自我探索,不替你做决定。; - 需要时加「按你填写的时间」。
- 顶部小字
- 两套 palette:
night:深色卡纸,屏幕用;paper:纸色底、墨色星点,报告打印用。
- 颜色取自 DESIGN.md 现有 token,不新增品牌色。
-
lib/birth-sky/sentence.ts:句库选择,纯函数,见下表。句子只能从这张表里出,执行方不得自己加。条件 主句 day星星都在,只是还看不见。 twilight_morning天快亮了,星星正一颗颗退场。 twilight_evening天刚暗下来,星星正一颗颗出来。 night那一刻,头顶就是这片天。 事实句最多两段,用「,」连接:
- 月亮:在地平线上时写「月亮在{八方位}」,在地平线下时写「月亮在脚下」。
- 除太阳外最高的一颗可见行星:写「{行星}正悬在{八方位}」。
- 八方位是:北、东北、东、东南、南、西南、西、西北。
- 白天另在事实句前加一段「太阳在{八方位}」。
- 按这些规则,样稿 C 那一刻应得「太阳在东,月亮在脚下,火星正悬在东南」。
VOICE.md 要求的开场问候不用「星星、月亮」一类比喻,这一条只管首页问候。封面写的是真实天体,不是比喻,所以不冲突。这四句定稿前请执行方对照 VOICE 的其余条款(短句、无 emoji),有冲突写进 PROGRESS,不要自行改句,由 Claude 验收时定。
-
组件
components/birth-sky/birth-sky-dialog.tsx:- 复用
ReportExportDrawer的原生<dialog>框架;窄屏占满,宽屏居中。 - 内容是一张按 3:4 等比缩放的
<img>(src 是导出的 PNG blob URL,手机上可长按保存),下方一个「保存图片」按钮。
- 复用
-
ChartPageView往SecondaryPageShell的actions传一个按钮,文案「那一刻的天空」。- 只在
view.status === "ok"且天空数据就绪时出现。 - 星盘页 ok 后立即预取
/api/birth-sky,完成后才显示按钮。预取失败就不显示按钮,不报错,也不占位。 - 按钮和弹层代码都懒加载。
- 只在
-
切换人物(
useCurrentSubject变化)时按新 subject 重新预取。给家人的盘也能生成。
验收
frontend/tests/birth-sky-sentence.test.ts:四种 phase 各一例,加事实句组合;样稿资料必须得到上面那句。frontend/tests/birth-sky-draw.test.ts:- 用 mock canvas 记录
fillText调用,断言画出的全部文字不含日期、时刻、经纬度、地名、姓名; - 断言
timeBasis: "reported"时出现「按你填写的时间」,adopted时不出现; - 断言东在左(方位 90° 的点 x < 圆心)。
- 用 mock canvas 记录
frontend/tests/chart-page-view.test.tsx追加:数据就绪才出现按钮;资料不全或预取失败时无按钮。- 导出尺寸是 1080×1440。
T4 · 保存图片
lib/birth-sky/export.ts:canvas.toBlob("image/png"),文件名jyotisha-sky.png,不带人名和日期。- 点「保存图片」:
- 若
navigator.canShare?.({ files: [file] })为真,调navigator.share({ files }),iOS 上会出现系统的「存储图像」; - 否则走 Blob +
a.download,与downloadMarkdownReport同一模式。 - 用户取消分享不算错误,不弹提示。
- 若
- 手机上长按预览图也能直接保存,这是
<img>的原生行为。
验收:单测覆盖 share 可用、share 不可用、用户取消三条分支。真机项写进 docs/testing/(见 T7)。
T5 · 报告封面(第二期)
reportView暴露报告对应的 subject(chart_profile_id,null 表示本人,对外映射成"self")。- 报告页 ready 态在正文最上方加封面块:
- 屏幕上用
nightpalette,宽度随正文列、3:4 比例; - 封面下方不加按钮,保存入口只在星盘页。
- 屏幕上用
- 封面必须对得上报告:只有当报告生成时的出生资料与现在的一致时才显示。执行方先查报告行或
result_binding里是否存了出生资料的指纹:- 有指纹:拿它和当前
profileFingerprint比较,不一致就不显示封面; - 找不到可靠的绑定:本单不显示封面,把缺口写进 PROGRESS 和
BLOCKED.md,不要拿当前资料冒充当时资料。
- 有指纹:拿它和当前
- 旧报告不补算、不补写库。没有封面时不留占位。
- 打印 / 保存 PDF:
- 封面改用
paperpalette(DESIGN 规定打印固定浅色); - 封面独占 A4 第一页(
break-after: page); - 不新增打印入口。
- 封面改用
- 封面的取数在报告正文出现后进行,不阻塞正文。取数失败就不显示封面。
验收
frontend/tests/personal-report-view.test.ts追加:- 指纹一致时有封面;
- 不一致或无绑定时无封面;
- 取数失败时无封面、正文照常。
- 打印样式的快照断言
paperpalette 与分页。
T6 · 首次建盘揭幕(第三期)
- 触发点:首页 onboarding 里自己的出生地保存成功(
saveOnboardingPlace成功之后)。它按定义每个账户只发生一次,所以不需要「看过」标记。- 人物档案里新建家人不触发。
- 修改资料不触发。
- 保存成功后立即请求
/api/birth-sky?subject=self:- 3 秒内拿到数据,就在当前流程之上淡入同一个弹层。淡入 600ms,
prefers-reduced-motion时直接出现。 - 超时或失败就静默跳过,onboarding 照常往下走,不显示错误,不重试。
- 3 秒内拿到数据,就在当前流程之上淡入同一个弹层。淡入 600ms,
- 弹层内容与星盘页一致,另加一个「继续」按钮。
- 点「继续」、Esc、点遮罩都关闭弹层,回到原本的下一步(问候或校正引导),不改变下一步是什么。
- 若下一步会自动进入出生时间校正,弹层关闭后再进入。
- 逻辑放在新 hook
hooks/use-birth-sky-reveal.ts(或组件内),page.tsx只接线。 - 这一次揭幕属于用户操作后的结果展示,不是首页加载等待,不违反 DESIGN §9 的「首页只揭幕一次」。首页加载揭幕的轨道环逻辑不动。
验收
frontend/tests/profile-onboarding*.test.ts追加:- 保存成功且 3 秒内有数据时出现弹层,关闭后进入原下一步;
- 超时时无弹层,流程照常;
- 家人档案不触发。
home-shell-growth-contract.test.ts通过。next build后/仍是○ Static,首屏 gzip ±2%。
T7 · 文档
frontend/DESIGN.md:新增一节「那一刻的天空」,写明:- 投影与方位约定、两套 palette、三个位置;
- 预取规则、揭幕时限;
- 不印出生资料;
- 标题用 Jyotisha Serif SC。
frontend/docs/VOICE.md:收录句库四句与事实句规则,并注明「只描述天空,不讲运势」。CHANGELOG.md:一条,写明 Skill 版本不 bump。CONTEXT.md:若新增「那一刻的天空 / 天空封面」术语,先进 glossary。docs/testing/birth-sky-cover-checklist.md真机清单:- iPhone Safari 预览图长按保存、「保存图片」弹系统分享;
- 安卓 Chrome 下载;
- 桌面 Chrome / Safari 下载;
- 家人档案生成;
- 报告封面屏幕显示与打印 PDF 第一页;
- 新账号首次建盘揭幕与跳过(可断网模拟)。
让步顺序(时间不够时从后往前砍)
- 先砍 T6 首次揭幕。
- 再砍 T5 报告封面。
- T1~T4 + T7 是第一期,不可砍。
- 猎户座、仙后座连线可以砍,只留北斗。
砍掉的部分写进 PROGRESS,不得标成完成。
开工前置命令
cd /workspace/Jyotisha && git status -sb | head -1
git fetch origin --prune
git worktree add -b codex/birth-sky-cover-20260928 .worktrees/birth-sky-cover-20260928 origin/staging
cd .worktrees/birth-sky-cover-20260928
grep -oE "BUG-[0-9]+" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1 # 开工时最大号
cd frontend && npm ci && ./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -5 # 记下测试总数基线
Node 用 22(本机在 /exec-daemon/node)。
BUG 编号
这是新功能,本身不开 BUG 条目。实现中若发现既有缺陷,从开工时 docs/BUG_HISTORY.md 最大号 +1 起编,写作时最大号是 BUG-1079。
交付
- 三期可以在同一分支分三个提交,顺序 T1–T4+T7 → T5 → T6。每个提交独立可测。
- 推 staging 前把 PROGRESS 写全:
- 基线 SHA;
- tsc / lint / 测试数(总数不低于基线);
- Python 定向测试与快速门结果;
/Static 与 gzip 数字;- 砍掉的项;
- 环境缺口。
- 用
git push origin HEAD:staging快进推送,核对远端 SHA 与/api/health的deployment.gitCommit。