Files
Jyotisha/docs/tasks/TASK-birth-sky-cover-20260928.md
T

19 KiB
Raw Blame History

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 授权)

  1. 画面选 C(真实天空),A 年轮、B 一笔画都不做。
  2. 三个位置都做,按顺序:① 星盘页入口 + 预览 + 保存图片;② 个人报告封面;③ 自己第一次建完盘时揭幕一次。
  3. 不在图上印任何出生资料:不印姓名、日期、时刻、经纬度、地名。天空本身会透露大致时段,产品确认可以接受。
  4. 图上的那句话只描述天空,不讲运势:不写大运、上升、性格。句子从下文「句库」按天空状态确定性地选出,同一张盘永远同一句,不调模型、不扣点。
  5. 出生时间未采用校正的也能生成:birthTimeStatus 不是 accepted / confirmed 时,图底加一行小字「按你填写的时间」。
  6. 不另开页面,不加侧栏入口,不做第六个 Tab;星盘页只在标题栏右侧加一个按钮。
  7. 标题用宋体(产品 2026-09-28 追加决定,见 TASK-serif-headings-20260928):封面主句与顶部小字用字体家族 "Jyotisha Serif SC",由那份任务书自托管;事实句、底部说明仍用站内黑体栈。画布绘制前先 await document.fonts.load('600 30px "Jyotisha Serif SC"', 主句),最多等 1.5 秒;超时就用黑体画,不出现等待态。不引入 Google Fonts。
  8. 第一期不接社交平台分享 SDK。保存图片走浏览器原生能力(见 T4)。
  9. 罗睺、计都不是可见天体,不画。月亮在地平线下时不画,只进句库的事实句。

硬红线

  1. scripts/jyotish_api_server.py 只做薄注册:JyotishAPIHandler 类方法数不得增长、__new__ 伪造点不得增长;计算放新模块 scripts/birth_sky.py(AGENTS §6,由 tests/test_api_server_growth_contract.py 执行)。
  2. frontend/src/app/(app)/page.tsx:Home() 的 useState / useRef 数不得增长;揭幕逻辑进 hooks/ 或组件,由它们持有自己的状态,page.tsx 只接线(AGENTS §6,由 home-shell-growth-contract.test.ts 执行)。
  3. 不新增 npm 依赖。PNG 用原生 Canvas 2D 导出。
  4. 封面代码全部懒加载(next/dynamic 或动态 import()):next build 后 / 仍是 ○ Static,首屏 gzip ±2%。
  5. 出生资料只在服务端取(loadSubjectBirth / resolveServerOwnedChartBirth),不接受客户端传来的日期、经纬度,防止借接口给任意时刻地点算图。
  6. 等待遵守 DESIGN §9:不写新 spinner 或骨架。星盘页进入后预取天空数据,点按钮时图已经就绪(见 T3);首次揭幕拿不到数据就静默跳过,不等待(见 T6)。
  7. 恒星表只用公有领域来源(Yale Bright Star Catalogue 5th ed.,HR 编号),来源与许可写进数据文件头和 PROGRESS。不得从有版权的星图软件抓数据。
  8. 不改数据库结构。一次性揭幕不需要「看过」标记(见 T6)。
  9. 测试 fixture 来自真实引擎输出(golden),不手造形状(§7.4)。
  10. 隐私:测试、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:
    1. 样稿虚构资料的 golden:太阳高度 31.7°±0.2°、方位 84.7°±0.3°(自北),土星高度 50.3°±0.2°、方位 191.1°±0.3°,月亮与木星在地平线下。
    2. 独立校验:任意日期当地真太阳时正午,太阳高度 ≈ 90° − |纬度 − 赤纬|,误差 ±0.5°。
    3. 北极星在北纬 30.27° 的高度约 30°(±1°)。
    4. 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 < 圆心)。
  • 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 态在正文最上方加封面块:
    • 屏幕上用 night palette,宽度随正文列、3:4 比例;
    • 封面下方不加按钮,保存入口只在星盘页。
  • 封面必须对得上报告:只有当报告生成时的出生资料与现在的一致时才显示。执行方先查报告行或 result_binding 里是否存了出生资料的指纹:
    • 有指纹:拿它和当前 profileFingerprint 比较,不一致就不显示封面;
    • 找不到可靠的绑定:本单不显示封面,把缺口写进 PROGRESS 和 BLOCKED.md,不要拿当前资料冒充当时资料。
  • 旧报告不补算、不补写库。没有封面时不留占位。
  • 打印 / 保存 PDF:
    • 封面改用 paper palette(DESIGN 规定打印固定浅色);
    • 封面独占 A4 第一页(break-after: page);
    • 不新增打印入口。
  • 封面的取数在报告正文出现后进行,不阻塞正文。取数失败就不显示封面。

验收

  • frontend/tests/personal-report-view.test.ts 追加:
    • 指纹一致时有封面;
    • 不一致或无绑定时无封面;
    • 取数失败时无封面、正文照常。
  • 打印样式的快照断言 paper palette 与分页。

T6 · 首次建盘揭幕(第三期)

  • 触发点:首页 onboarding 里自己的出生地保存成功(saveOnboardingPlace 成功之后)。它按定义每个账户只发生一次,所以不需要「看过」标记。
    • 人物档案里新建家人不触发。
    • 修改资料不触发。
  • 保存成功后立即请求 /api/birth-sky?subject=self:
    • 3 秒内拿到数据,就在当前流程之上淡入同一个弹层。淡入 600ms,prefers-reduced-motion 时直接出现。
    • 超时或失败就静默跳过,onboarding 照常往下走,不显示错误,不重试。
  • 弹层内容与星盘页一致,另加一个「继续」按钮。
    • 点「继续」、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 第一页;
    • 新账号首次建盘揭幕与跳过(可断网模拟)。

让步顺序(时间不够时从后往前砍)

  1. 先砍 T6 首次揭幕。
  2. 再砍 T5 报告封面。
  3. T1~T4 + T7 是第一期,不可砍。
  4. 猎户座、仙后座连线可以砍,只留北斗。

砍掉的部分写进 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。