feat(report): drop the report sky cover; docs, checklist and progress for M1–M5 (M5+M3)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
Jesse_Chen
2026-09-29 08:41:27 +08:00
co-authored by Claude Opus 5.5
parent 60b5542b7d
commit 447a19b561
16 changed files with 236 additions and 357 deletions
@@ -0,0 +1,167 @@
# PROGRESS · 手机星盘 / 确认时间可改 / 天空定格 / 去掉报告封面(2026-09-29)
任务书:`docs/tasks/TASK-mobile-chart-and-confirmed-edit-20260929.md`(M1–M4 在 `53c0b9ea`,M5 在 `47eda7e1` 追加)。产品授权直接执行(Claude 子代理)。
- 基线:`origin/staging` = `53c0b9ea`(开工时;其后 `47eda7e1` 只加了任务书 M5 段,纯文档,分支未 rebase)
- 分支 / 工作树:`codex/mobile-chart-confirmed-edit-20260929` / `.worktrees/mobile-chart-confirmed-edit-20260929`,**未推送**
- 环境:Node v22.14.0(`/exec-daemon/node`);系统 `python3`;无 Docker;无头 Chrome 151(`--disable-dev-shm-usage`,CDP 设备模拟)
- BUG 编号:开工时最大 BUG-1082;本单 M1 = BUG-1083;M2 是 BUG-264 下的产品修订段,不另开号
## 结论
| 项 | 结果 | 提交 |
| --- | --- | --- |
| M1 手机星盘完整显示(BUG-1083) | 完成 | 1 |
| M2 已确认时间,改声明字段后重置 | 完成(数据库侧未跑 `test:db`,见环境缺口) | 2 |
| M4 「那一刻的天空」重放 → 定格 → 右上角分享 | 完成 | 3 |
| M5 去掉报告星图封面 | 完成 | 4 |
| M3 文档 | 完成(DESIGN、VOICE、CHANGELOG、BUG_HISTORY、BLOCKED、真机清单) | 1–4 各自带 |
## M1 · 根因与修复(BUG-1083)
复现方法:真实组件(`ChartPageView` / `ChartVedicTab` / `AppSidebar` / `SidebarInset`)+ golden 夹具(`chart-view-golden.json`、`chart-view-bhava-golden.json`)SSR 成静态页,套上**本次构建产物**里 `/chart` 引用的全部 CSS,无头 Chrome 用 CDP `Emulation.setDeviceMetricsOverride` 模拟 390×844(新版无头窗口最小 500 宽,`--window-size` 不够用),从盘面 SVG 往上逐层量 `getBoundingClientRect().width`。
修复前(基线 CSS,本命与 Bhava 相同):
| 层 | 宽度 |
| --- | ---: |
| `svg.personal-report-chart-svg` | 1043 |
| `.chart-page-vedic-stage` / `-vedic-layout` / `.chart-page-vedic` / `.chart-page-panel` | 1069 |
| **`.chart-page-body`(第一个超宽祖先)** | **1101** |
| `.secondary-page` | 390 |
| `.secondary-panel` / `main.chat-app` | 390 |
- 根因:`.secondary-page` 是 `display: grid` 且没有 `grid-template-columns`,隐式列是 `auto`;子项 `.chart-page-body` 保持默认 `min-width: auto`,这一列就按正文最小内容宽度定宽(不折行的 23 个盘型 chip、34rem 行星表)。内层表格与盘型条自己的 `min-width: 0` + 横滚都在,撑宽发生在它们外面。面板 `overflow-x: hidden` 把溢出直接裁掉,所以看起来是「盘很大、右边没了」。
- 修复:`.secondary-page { grid-template-columns: minmax(0, 1fr); }`。修在四个次级页共用的这一层。
- 修复后(本次构建 CSS):390×844 下本命 / D9 / Bhava / 月亮 / D10 都是 `documentElement.scrollWidth` = 390、没有超宽祖先、盘面 332px。大运 / 基础信息 / 西洋盘三个 Tab 修复前就不溢出(让步 1:未另改)。
- 桌面:1280×900 下修复前其实也被裁(`.chart-page-vedic-layout` 1069px 塞在约 1024px 的内容区,参数卡右边被切、「≡」看不到);修复后两栏布局不变,盘面 549 → 480px,参数卡与「≡」完整可见。
- 回归测试:`frontend/tests/chart-page-mobile-width-20260929.test.ts`(2 条)。
截图(`/tmp/claude-1000/-workspace-Jyotisha/fb88e01e-1ced-4bcc-92fe-44c5bdcc878e/scratchpad/mobilechart/`):
- 修复前:`before-D1-390.png`、`before-BHAVA-390.png`、`before-D1-1280.png`
- 修复后(本次构建 CSS):`after-D1-390.png`、`after-D9-390.png`、`after-BHAVA-390.png`、`after-MOON-390.png`、`after-D10-390.png`、`after-D1-1280.png`
- 桌面前后对比:`cmp-1280.png`
## M2 · 结论
- 改法:`resolveAccountBirthTimeApplicationPatch` 去掉「confirmed 直接 `{}`」。confirmed(含状态为空、有遗留分钟的 legacy 行)**只有声明字段真的变了**才重置,之后与未确认完全同一套派生:零误差家人时间 → `accepted` + 新分钟;否则 `reported`、`active_birth_time = null`;`rectification_case_id = null`。只改称呼、岁差、性别、头像 → `{}`。
- 跨午夜三列(硬红线 3):
- 写入方:采用 / 确认候选的 RPC(`20260920020000_adopted_birth_date.sql` 起的 dated-v1 路径);读取方:`server-owned-birth-profile.ts`(`active_birth_timezone_offset` 覆盖声明时区)、`chart-birth-truth.ts`(`adoption = "adopted"`、指纹)。
- 数据库触发器 `zz_guard_adopted_birth_date`(BEFORE INSERT/UPDATE)在任一声明字段(日期、时间、来源、时段、范围、误差、经纬度、时区)变化或状态不在 accepted/confirmed 时把三列置空;前端镜像是 `applyAdoptedBirthDateGuard`。
- 本次仍在派生补丁里**显式**写三列为 null(仅当当前行有值),不只依赖触发器。
- 遗留列 `birth_time`:`chart-birth-truth.ts` 与 `server-owned-birth-profile.ts` 在 `active_birth_time` 为空时会回落读 `birth_time`。confirmed / legacy 重置成「只有时段」时,若不清它,星盘仍会按旧分钟排。所以当前行 `birth_time` 非空时一并写 null。
- 触发器会不会改回 confirmed:不会。
- `guard_birth_time_journey` 最新版(`20260804020000`)只在「状态为空且有 active 分钟」时补 `confirmed`,本修订总写明确状态。
- `agentic_rectification_profiles_rebaseline_guard` 与 `invalidate_agentic_rectification_results_on_profile_change`(最新版 `20260827010000`)在声明变化时把进行中的校正 Case 标成 `needs_rebaseline`、把结果标 `invalidated_at`,这是期望行为。
- 没有改触发器、没有加迁移。
- 并发守卫:派生补丁非空时,路由照旧用 `applyAccountProfileConcurrencyGuards` 按当前行做条件更新,confirmed 行现在也会走到这条,能挡住与一次并发采用的竞争。
- 客户端:`/people` 保存本人走 `saveSelfProfile` → `applyPersistedBirthTime`,用路由返回的 `status` / `activeTime`(取自写回的行);新增单测证明「确认 07:52 → 改 08:05」后本地是 `accepted` / `08:05`,改成时段后是 `reported` / 空分钟。缓存失效沿用 BUG-1081 那一套。
- **偏离(请 Claude 定)**:声明比较由 `!==` 改成按值比较(`sameDeclarationValue`:库里 `HH:MM:SS` 与表单 `HH:MM`、数字与数字字符串、空串与 null 视为相同)。原因:`/people` 每次保存都整表重提全部出生字段,库里的 `time` 列回来是 `HH:MM:SS`,原来的 `!==` 会把「只改称呼」判成新声明,于是本单改完后会把已确认分钟清掉。副作用:未确认状态(例如 `accepted`)下「只改称呼」以前也会被误判并退回 `reported`,现在不会了——这是修正,不是新行为。
## M4 · 完成情况
- 纯函数:`birthSkyRevealAt(elapsed, reduced, mode)`,新增 `mode = "hold"`:汇聚与文字与首页过场完全相同,文字淡入完(3.6 s;减少动态效果时 0.6 s)就 `held`,此后永远是定格帧、`layer = 1`、`done = false`。`birthSkyHoldAtMs(reduced)` 给出定格时刻。首页过场(`reveal`)时间轴不变。
- 组件:`BirthSkyConverge` 加 `mode` 与 `controls` 两个可选参数(默认 `reveal`、无控件,首页过场行为不变)。`hold` 模式:
- 定格后停止 `requestAnimationFrame`;resize 会清空画布,所以定格后 resize 时重画一次;
- 点画面(不在控件上)直接跳到定格帧,不关闭;
- Esc 关闭;卸载时取消动画。
- 新组件 `components/birth-sky/birth-sky-replay.tsx`:左上 ×(读屏名「关闭」),右上「分享」(`saveBirthSkyImage(cover.blob)`:能分享文件就调系统分享,不能就下载;用户取消不提示;其他失败提示「没能分享,再试一次。」)。样式在 `birth-sky-converge.css`,44px,顶部让出安全区。入口 `birth-sky-entry.tsx` 懒加载它,每次打开都重放。
- 定格帧 = 海报:定格帧画布调用序列与导出 PNG 逐条相同(单测,三个 phase);并且 `BirthSkyCover` 新增 `serif`(`render.ts` 新增 `paintBirthSkyPng` 同时返回 PNG 与宋体判定),重放用画 PNG 时同一个字体判定,避免「PNG 用黑体、屏幕用宋体」。
- 删除:静态海报弹层 `birth-sky-dialog.tsx` 与 `birth-sky.css`(不再有引用)。
- 无头 Chrome 实测(真实组件 esbuild 打包 + golden 夜盘 + 「中国 · 浙江省 · 杭州市 · 西湖区」标签),390×844 与 1280×900 都通过:
- 1.5 s 在播放;
- 5 s 已定格、不透明;
- 8 s 仍打开;
- 0.7 s 时点画面 → 跳到定格、仍打开;
- Esc → 关闭。
截图:`replay-390-playing.png`、`replay-390-held.png`、`replay-390-tapped.png`、`replay-1280-playing.png`、`replay-1280-held.png`、`replay-1280-tapped.png`、`replay-390-strip.png`(同一目录)。
- 「分享」按钮「PNG 未就绪时禁用」:没有实现禁用态。星盘页只在 PNG 画好后才渲染入口按钮,重放层打开时 PNG 一定已经在手,这一状态不存在(偏离,见下)。
## M5 · 完成情况
- 删除:`components/birth-sky/report-sky-cover.tsx` / `.css`、`lib/birth-sky/report-cover.ts`、`personal-report-page.tsx` 的动态导入与挂载点(连同只给它用的 `next/dynamic` import)、`classifyReportEnvelope` 的 `coverSubject` 解析、`ReportLoadState` / `ReportEnvelopeView` 上的字段。
- 服务端:`reportView` 的 `coverSubject` 参数、`readyReportCoverSubject`、`ReportReadCoreDeps.loadCurrentBirthFingerprint`、详情路由里的 `loadCurrentBirthFingerprint`(以及随之无用的 `subjectBirthFingerprint` / `loadSubjectBirth` import)全部删除;读报告不再多查一次人物资料。
- `lib/report-birth-binding.ts` 只保留 F1 写入用到的:`REPORT_BIRTH_FINGERPRINT_KEY`、`subjectBirthFingerprint`、`stampReportBirthFingerprint`、`readReportBirthFingerprint`、`withReportBirthFingerprint`;删掉 `reportDocumentBirthFingerprint`、`reportSubjectId`、`reportCoverSubject`。
- 保留:三条生成路径写 `subject.birthFingerprint`、TS / JSON Schema / Python 三处 schema 可选字段及其测试。
- `grep -rn "ReportSkyCover\|report-sky-cover\|coverSubject" frontend/src` = 0。
- `BLOCKED.md` T5 保持划掉,追加「→ 2026-09-29 产品决定去掉报告封面」。
## 验证
| 项 | 基线 `53c0b9ea` | 交付 |
| --- | --- | --- |
| `tsc --noEmit` | 0 错 | 0 错 |
| `npm run lint` | 0 error / 126 warning | 0 error / 126 warning |
| `npm test` | tests 4335 · pass 4280 · fail 24 · cancelled 0 · skipped 31 | tests 4335 · pass 4280 · fail 24 · cancelled 0 · skipped 31 |
| 失败清单 | 24 条(Docker / PostgreSQL / 部署) | 与基线逐条同名 |
| 测试名对比(同机同命令跑基线) | — | 消失 15、新增 15(下表逐条) |
| `run_quality_gate.py --profile quick --skip-yoga-logic --skip-frontend-runtime`(门禁同参数) | — | 997 passed / 1 skipped,`Quality gate passed.`,退出 0 |
| `commercial_privacy_artifact_scan.py --json` | — | `status: pass`,`finding_count: 0` |
| pytest `test_repo_privacy_markers` + `test_birth_sky` + `test_api_server_growth_contract` + `test_personal_report_contract` | — | 126 passed |
| `rm -rf .next && next build` | `/` ○ Static | `/` ○ Static |
| `page.tsx` / `jyotish_api_server.py` | — | 未改 |
### 首屏 gzip(`index.html` 引用的全部 `/_next/static/*.js|css` 逐个 gzip -9 求和;同一工作树先在基线代码上全量构建一次,再在交付代码上全量构建)
| | 基线 | 交付 | 变化 |
| --- | ---: | ---: | ---: |
| JS | 651,520 B | 651,512 B | −8 B(−0.001%) |
| CSS | 66,031 B | 66,035 B | +4 B(+0.006%) |
### 消失的测试名(15 条,§7.3)
| 消失的测试名 | 去向 / 原因 |
| --- | --- |
| ordinary declaration edits clear stale candidate application but never overwrite confirmed active time | 改名为 “…and a new declaration drops a confirmed minute too”;产品 2026-09-29 推翻 BUG-264 约定(M2) |
| confirmed ordinary edits keep the active minute and clear a complete adopted tuple together | 改名为 “confirmed declaration edits drop the minute and clear a complete adopted tuple together”;同上(M2) |
| chart page dialog shows only the picture: no title bar, no panel; icon × and 「保存图片」 | 由 “chart page replay stays on the poster: × top left, 「分享」 top right…” 取代;静态弹层删除(M4) |
| the chart page dialog no longer has a reveal variant (「继续」, is-reveal) | 由 “the chart page replay has no 「继续」 and the poster dialog is gone” 取代;读的两个文件已删除(M4) |
| GET /api/reports/:id compares on the server through the chart page's subject loader | 由 “GET /api/reports/:id no longer looks up the subject for a cover (M5)” 取代;产品决定去掉报告封面(M5) |
| coverSubject: only a matching fingerprint yields the subject id, self when chart_profile_id is null | 删除:比对函数 `reportCoverSubject` 已删(M5) |
| core read: coverSubject is the subject id when the stored fingerprint matches the current one | 删除;由 “core read: the detail response has no coverSubject, and never the fingerprint or birth fields” 取代(M5) |
| core read: coverSubject is null when the birth data changed since the report | 同上(M5) |
| core read: coverSubject is null for a legacy report without the field, and nothing is looked up | 同上(M5) |
| core read: coverSubject is null for a subject that is not the reader's (foreign, deleted, unreadable) | 同上(M5) |
| matching fingerprint → the ready state carries coverSubject; mismatch or legacy → no cover | 删除:`report-sky-cover.test.ts` 整个文件随组件删除(M5) |
| the cover sits above the body, lazy, and only when coverSubject is set | 同上(M5) |
| loader paints night for the screen and paper for print with the same sky | 同上(M5) |
| fetch failure, an empty sky or an abort → no cover, and nothing throws into the report | 同上(M5) |
| print: paper picture alone on the first A4 page; the screen picture is hidden | 同上(M5) |
新增 15 条:M1 2 条(`chart-page-mobile-width-20260929.test.ts`);M2 4 条(两条改名 + “confirmed minute follows a new declaration and survives everything else” + “saving a new time over a confirmed minute takes the server's new status and minute”);M4 7 条(`birth-sky-replay-20260929.test.ts` 5 条 + 两条替代);M5 2 条(替代)。总数 4335 不变。
## 改动的既有断言(原值 / 新值 / 原因)
| 测试(文件) | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `account-api.test.ts` 第一条 confirmed + 新声明(两处) | `{}` | `{ active_birth_time: null, birth_time_status: "reported", rectification_case_id: null, birth_time: null }` | M2:新声明丢掉已确认分钟;夹具 `birth_time` 有值,所以一并清 |
| `account-api.test.ts` confirmed / legacy + 新零误差家人时间 | `{}` | `accepted` + `05:40`(legacy 另加 `birth_time: null`) | M2 |
| `account-api.test.ts` “PATCH answers…” confirmed / legacy + 改 `district_code` | `confirmed` / `05:18` | `accepted` / `05:00`(夹具声明是零误差家人时间 05:00);另加两条「只改称呼 / 岁差 → 仍 confirmed 05:18」 | M2:改地点属于新声明 |
| `account-api.test.ts` confirmed + 只改岁差 | `{}` | 不变 | —— |
| `chart-profile-update-consistency.test.ts` 跨午夜 confirmed + 新声明 | `{}`,排盘仍 confirmed 23:55(legacy) | 回到 `reported` 并显式清三列,排盘按新声明 00:20(declared) | M2 |
| `profile-onboarding-birth-sky-reveal.test.ts` overlay | `layer.addEventListener("pointerdown", finish)` | `onPointerDown = mode === "hold" ? skip : finish`,默认 `mode = "reveal"` | M4:定格模式点一下只跳到定格帧 |
| `chart-page-view.test.tsx` 入口懒加载 | `import("./birth-sky-dialog")` | `import("./birth-sky-replay")` | M4 |
| `chart-page-view.test.tsx` 封面夹具 | 无 `serif` | `serif: false` | `BirthSkyCover` 新增字段(夹具形状,断言未变) |
| `report-birth-binding.test.ts` longform cover | 另用 `reportDocumentBirthFingerprint` 读回 | 只看 `subject.birthFingerprint` | M5:读回函数已删;F1 写入断言保留 |
改动的断言都在测试文件里就地写了三栏注释。
## 偏离任务书(请 Claude 定)
1. **M2 声明比较改成按值比较**(见 M2 节)。不改的话,本单会让「只改称呼」清掉已确认分钟,违反决策记录第 1 条。
2. **M2 同时清遗留 `birth_time`**(任务书只写了三列)。原因:图表在 `active_birth_time` 为空时会回落读它。
3. **M4 「分享」没有做禁用态**:PNG 在入口按钮出现前已就绪,这个状态不存在。
4. **M4 失败文案改为「没能分享,再试一次。」**,原「没能保存。长按或右键上面的图,也能存下来。」停用,因为画面是画布,不能长按存图。已收进 VOICE。
5. **M1 修在 `.secondary-page`**(四个次级页共用),而不是只给 `.chart-page-body` 补 `min-width: 0`。桌面宽度下被裁的问题也随之消失。
6. 首页过场结束后的提示「这片天空在星盘页可以保存。」未改;星盘页现在的按钮字是「分享」,保存要经分享面板或下载,句意仍成立。请 Claude 判断是否改句。
7. 分支基于 `53c0b9ea`,未 rebase 到只加了文档的 `47eda7e1`(按协调要求);`docs/tasks/README.md` 索引行的状态未改,留给 Claude 交付时处理,避免冲突。
## 环境缺口
- **无 Docker**:
- 24 条 DB / 部署套件失败,与基线逐条同名;
- 没跑 `npm run test:db`,所以 M2 在真实 PostgreSQL 上的效果(触发器链、条件更新)只做了静态核对。staging 实测见真机清单第 5–7 条。
- **无登录态与真机**:M1 只在无头 Chrome 设备模拟上验证;M4 的系统分享面板、M5 的打印 PDF 没有实测。见 `docs/testing/mobile-chart-confirmed-edit-20260929.md`(18 条)。
- **无模型凭据**:本单不涉及模型输出。
- 未推送、未部署。