Files
Jyotisha/docs/tasks/PROGRESS-mobile-chart-confirmed-edit-20260929.md
T
Jesse_ChenandClaude Opus 5.5 f51c830b52
Independent Staging Quality Gate / validate (push) Successful in 14m35s
Independent Staging Quality Gate / publish (push) Successful in 3m45s
docs(tasks): Claude acceptance for mobile chart, confirmed edits, sky replay, report cover removal
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-09-29 08:48:41 +08:00

176 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 条)。
- **无模型凭据**:本单不涉及模型输出。
- 未推送、未部署。
## Claude 验收(2026-09-29)
分支已 rebase 到 `origin/staging` `56722063`(另一会话同期写入 BUG-1084~1089,`BUG_HISTORY.md` 冲突按编号顺序合并,BUG-1083 在前)。独立重跑:`tsc` 0 错;lint 0 error / 126 warning;`npm test` 4335 · pass 4280 · fail 24 · cancelled 0,失败清单与上一单逐条同名;门禁同参数 quick 门禁 997 passed、`Quality gate passed.`;隐私制品扫描 pass;`rm -rf .next && next build` `/` ○ Static;首屏 gzip 651,512 / 66,035(基线 651,520 / 66,031,−8 B / +4 B);`src/` 下 grep `ReportSkyCover|report-sky-cover|coverSubject` = 0。
看过截图:`before-BHAVA-390` 复现产品真机现象;`after-{BHAVA,D1}-390` 盘面完整、表格在框内;`replay-390-held` 定格帧有 × 与「分享」。
M2 复核:最新 `guard_birth_time_journey`(`20260804020000`)已不在 `birth_time` 与 `active_birth_time` 间互相同步,因此同时清 `birth_time` 不会连带清掉新写入的 `active_birth_time`。偏离 1–7 接受;偏离 6(首页提示句「这片天空在星盘页可以保存。」)保留,待产品定。