Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
266 lines
19 KiB
Markdown
266 lines
19 KiB
Markdown
# 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,不得标成完成。
|
||
|
||
## 开工前置命令
|
||
|
||
```bash
|
||
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`。
|