Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
18 KiB
PROGRESS · 天空封面第三轮(2026-09-28)
任务书:docs/tasks/TASK-birth-sky-polish-20260928.md。产品授权直接执行(Claude 子代理)。
- 基线:
origin/staging=84df1299(任务书提交,纯文档);代码基线be9e4dbe(天空封面五个提交 + 验收文档,已部署 staging)。分支建自be9e4dbe后 rebase 到84df1299;提交前origin/staging又前进到1a4f7758(纯文档:TASK-self-edit-avatar-menu-20260928,它写明要等本单推上 staging 后再开工改page.tsx),已 rebase 上去,docs/tasks/README.md索引冲突按两行都保留解决。 - 分支 / 工作树:
codex/birth-sky-pacing-20260928/.worktrees/birth-sky-pacing-20260928(本地提交,未推送,由 Claude 验收后推)。 - 环境:Node v22.14.0(
/exec-daemon/node);无.venv,Python 用系统python3;无 Docker;无头google-chrome151。 - BUG 编号:开工时最大 BUG-1079;本单记 BUG-1080。
- 同日其它改
page.tsx的分支:开工时未发现。
结论
| 项 | 结果 |
|---|---|
| P1 星盘页弹层只留图 | 完成 |
| P2 封面印「日期 时刻 · 城市」 | 完成(BFF 只下发 caption 字符串) |
| P3 过场放慢 | 完成(3 + 2.5 + 1 秒) |
| P4 兑换弹窗改时机 + 过场 portal | 完成;BUG-1080 investigating(Chrome 下「弹窗盖住过场」已排除,确切原因待真机) |
| P5 文档 | 完成(DESIGN §17 与弹窗 Placement 条、VOICE、CHANGELOG、BUG_HISTORY、真机清单 31–42、任务索引) |
| 让步 | 未用:兑换弹窗文案已改;海外城市有保守规则(不是一律不带) |
P1 · 弹层只留图
components/birth-sky/birth-sky-dialog.tsx:删掉<header>(标题「那一刻的天空」+「关闭」文字按钮)与面板 / 框 / 页脚包裹;原生<dialog>只含右上角图标 ×(aria-label="关闭",lucideX)、<img>、「保存图片」一行(保存失败提示仍在它上方)。弹层无障碍名称改为aria-label="那一刻的天空"。点 dialog 本身(图和按钮以外)、Esc、× 关闭。birth-sky.css重写:dialog 透明、铺满视口、flex 纵向居中;::backdrop=color-mix(in srgb, #1a1a19 90%, transparent)(night palette 页底色);图 3:4,max-width: 100%、max-height: calc(100dvh − 44px − 3 × --space-4 − 安全区);「保存图片」与 × 用 night 墨色#f2f0ea(细描边 / 透明底,悬停 10% 底),均 ≥ 44px。删除.birth-sky-panel/-header/-frame/-footer与 ≥860px 桌面面板变体(全仓无其它引用)。- 样张(无头 Chrome,真实
BirthSkyDialog组件 + 真实birth-sky.css,样稿虚构资料 04:50 晨昏图,宋体切片以 data URL 内联;背景是示意页):/tmp/claude-1000/-workspace-Jyotisha/fb88e01e-1ced-4bcc-92fe-44c5bdcc878e/scratchpad/polish/dialog/dialog-390x844.png(DPR 2;图 358×477 @ y153,按钮 y647)- 同目录
dialog-1280x900.png(图 606×808,按钮 y840) - 同目录
dialog-1280x600.png(图 381×508,按钮 y540) - 三张图都完整可见,图下没有空白面板。
P2 · caption
- 新文件
lib/birth-sky/caption.ts:birthSkyCity(label)与birthSkyCaption({ date, time, placeLabel })。规则(写在函数注释里,各有测试):- 直辖市与港澳(
北京市 · 朝阳区、香港特别行政区)→北京、香港; - 中国
省 · 市 · 区→ 第二段以「市」结尾时取它、去掉「市」(河北省 · 邯郸市 · 峰峰矿区→邯郸);盟 / 地区 / 自治州 / 县 / 林区、只有省(含台湾省)→ 不带城市; - 海外世界表标签
Country · Region · City:恰好三段且不含汉字 → 第三段(英文原名);两段(只到州 / 省)→ 不带; - 其它(空、一段、自由文本如
浙江省杭州市、Hangzhou, Zhejiang, China)→ 不带。 日期1994年5月18日(年月日不补零),时刻 24 小时制补零;格式不对返回 null。
- 直辖市与港澳(
contract.ts:birthSkyOkSchema加必填caption: string(1..64);birthSkyFromEngine(payload, timeBasis, caption)。service-core.ts:用服务端prepareChartViewProfile的date/time(即实际算天空用的时刻,采用校正时是校正后的时刻)/placeLabel拼 caption;拼不出(理论上不会,profile 已校验)按 503 处理。响应里仍然没有任何日期、时刻、经纬度、时区、地名、名字字段。sentence.ts:删BIRTH_SKY_REPORTED_FOOTNOTE/birthSkyFootnote,加BIRTH_SKY_ADOPTED_SUFFIX = " · 校正后"与birthSkyCaptionLine(sky)。draw.ts:删footnote选项(全仓无调用方传它);事实句下 y = 712、12.5px、inkSecondary画birthSkyCaptionLine(sky)(位置、字号、颜色与原 footnote 相同)。三处(星盘页图、报告封面、首次过场)共用,自动都有。- golden:引擎输出不变(caption 是 BFF 层拼的),
frontend/tests/fixtures/birth-sky-golden.json无需重新生成;tests/birth-sky-test-support.ts用夹具的times+ 虚构地点浙江省 · 杭州市 · 西湖区经同一个birthSkyCaption生成 caption。 - 海报(无头 Chrome,同一
drawBirthSky,含宋体):/tmp/claude-1000/-workspace-Jyotisha/fb88e01e-1ced-4bcc-92fe-44c5bdcc878e/scratchpad/polish/posters/poster-day-night-reported.png(「1994年5月18日 07:40 · 杭州」)- 同目录
poster-night-night-adopted.png(「1994年5月18日 00:00 · 杭州 · 校正后」) - 同目录
poster-twilight_evening-paper-reported.png(paper palette,「1994年5月18日 19:10 · 杭州」)
P3 · 放慢
converge.ts:汇聚 1.5 → 3 s,文字 1 → 2.5 s,淡出 0.5 → 1 s(总 6.5 s);文字淡入 400 → 600 ms;整层前段淡入 200 → 400 ms;reduced-motion 淡入 400 → 600 ms(淡出随BIRTH_SKY_FADE_MS变 1 s)。3 秒取数预算BIRTH_SKY_REVEAL_TIMEOUT_MS不变。- 「t=0 散开、t=1 真实位置」与「停留帧 = 导出封面(逐调用)」两条测试原样保留且通过。
- 四帧(黑体画,排除字体时序;night 与 day 各一组):
/tmp/claude-1000/-workspace-Jyotisha/fb88e01e-1ced-4bcc-92fe-44c5bdcc878e/scratchpad/polish/birth-sky-converge-night-{t0,t050,t100,hold}.png(0 / 1500 / 3000 / 5400 ms)与birth-sky-converge-day-*.png;停留帧 PNG 与导出封面 PNG 逐字节相同(night、day 都是true,frames/*-export.png对照)。
P4 · 兑换弹窗时机、过场 portal、BUG-1080
无头 Chrome 最小复现(先复现再修)
- 页面
scratchpad/polish/bug1080/repro.html:#inset(模拟SidebarInset)里放过场同款<dialog>;先把兑换弹层 portal 到 body(position: fixed; z-index: 40,输入框已聚焦),再inset.inert = true,300ms 后对 inert 容器里的 dialogshowModal()。驱动scratchpad/polish/bug1080/cdp.mjs(CDP:截图、真实鼠标按下、真实 Esc)。 - 结果(
inert与对照plain两种一致):showModal不抛错、:modal为真;截图bug1080/inert.png里过场在最上层完整可见,兑换弹层看不到;elementFromPoint(中心)= 过场画布;鼠标按下触发 dialog 的pointerdown,Esc 触发cancel。 - 结论:Chrome 151 下「兑换弹窗盖住过场 / inert 让过场点不动」不成立(与 BUG-968 不同:那里的账户弹窗是普通 div,不进顶层)。首页代码里的时序是:保存出生地 → 首页变 starter home 并等开场语打完 → 兑换 effect 立即打开;过场要在 3 秒内拿齐
/api/birth-sky、宋体(最多 1.5 秒)、过场 chunk 才开始。仍可能的原因:3 秒预算内没拿齐被静默跳过(页面上只剩兑换弹窗)、Safari 行为不同、或当时不是在首页 onboarding 保存出生地。写成 BUG-1080investigating。
修复
page.tsx:删掉「starterHomeVisible且 0 点数且无订阅就setOnboardingPaywallOpen(true)」的 effect(−12 行);把现有的onboardingPaywallShownref 与setOnboardingPaywallOpen传给useConsultationRun(+1 行)。page.tsx1375 → 1364 行;Home()的useState/useRef数不变(未新增)。- 新文件
lib/redeem-paywall-once.tstryOpenRedeemPaywall({ uiPreview, shown, open }):本页第一次返回 true 并打开兑换弹窗,之后返回 false。useConsultationRun里包成openRedeemPaywallOnce(),两处点数不足:- 前置余额检查
if (account.credits <= 0 && !account.activeSubscription):if (!openRedeemPaywallOnce()) openAccountDialog("billing", …),仍在清空草稿之前 return,问题留在输入框; - 402(
rejectRun,HTTP 体与流内request_rejected共用):if (status === 402 && !openRedeemPaywallOnce()) openAccountDialog("billing", …);既有 rollback 把草稿退回输入框。
- 前置余额检查
birth-sky-converge.tsx:createPortal(<dialog …/>, document.body),挂到SidebarInset之外。- 兑换弹窗文案(
onboarding-redeem-paywall.tsx,已收进 VOICE):「入门问题已经准备好。填写兑换码,解锁后续咨询、生时校正与完整解读。」→「**问题还在输入框里。**填写兑换码,点数到账就能发送,生时校正与完整解读也能用。」;到账后「点数已到账,可以继续选择问题。」→「点数已到账,回到输入框点发送就行。」。请 Claude / 产品验收这两句。
- 生时校正对话的 402(
lib/rectification-chat-turn-run.ts)未动,有源码断言锁住。
改动的既有断言
| 测试(文件) | 原值 | 新值 | 原因 |
|---|---|---|---|
birth-sky-route.test.ts 「a complete profile returns the contract and leaks no birth data」→ 改名「…birth data leaves only as the caption line」 |
响应 JSON 不含 date/time/place 等键,也不含 1994、07:40、30.27、120.15、虚构、Asia/Shanghai | 键断言不变;caption = 「1994年5月18日 07:40 · 杭州」;任何位置都不含 30.27、120.15、虚构、Asia/Shanghai、西湖区、浙江、UTC;把 caption 置空后其余部分不含 1994、07:40、杭州。夹具地点从「虚构地点」改为 浙江省 · 杭州市 · 西湖区 |
P2 产品决定印日期时刻城市;BFF 只下发 caption 字符串 |
birth-sky-draw.test.ts 「no drawn text carries a date, a time, coordinates, a place or a name」→ 改名「only the caption line carries birth data…」 |
所有画出的文字不含年份、时刻、「杭州」、年 / 月 / 日 | 只有 caption 那一行含日期时刻城市;其它文字仍不含;任何文字不含经纬度、UTC、区县、省、名字 | 同上 |
birth-sky-draw.test.ts 「「按你填写的时间」 appears for a reported time and not for an adopted one」→ 由「the caption replaces the footnote; an adopted time adds 「· 校正后」」取代 |
reported 有「按你填写的时间」,adopted 没有 | y = 712 处 reported 为 caption、adopted 为 caption + 「 · 校正后」;两者都没有「按你填写的时间」;paper palette 的 caption 用 inkSecondary |
P2:caption 替换 footnote 位置 |
birth-sky-sentence.test.ts 「the reported-time footnote appears only when the time was not adopted」→ 由「the caption line appends 「· 校正后」 only for an adopted time」取代 |
birthSkyFootnote reported → 「按你填写的时间」,adopted → null |
birthSkyCaptionLine reported → caption,adopted → caption + 「 · 校正后」 |
同上 |
profile-onboarding-birth-sky-reveal.test.ts 「timeline: converge ~1.5 s, words ~1 s, fade ~0.5 s…」→ 改名「timeline: converge ~3 s, words ~2.5 s, fade ~1 s…」 |
1_500 / 1_000 / 500,总 3_000;检查点 750、1_500、2_400、2_750、3_000 ms;reduced 400ms 淡入 | 3_000 / 2_500 / 1_000,总 6_500;检查点 1_500、3_000、3_300、3_600、5_400、6_000、6_500 ms;新增整层 400ms 与文字 600ms 淡入断言;reduced 600ms 淡入 | P3 产品要求放慢 |
membership-page.test.ts 「finished starter questions open the redeem paywall only for accounts without access」→ 由「the redeem paywall opens on the first send refused for credits, not when the starter home appears」取代 |
page.tsx 有 !starterHomeVisible / credits > 0 / activeSubscription active 守卫的 effect 调 setOnboardingPaywallOpen(true);文案含「入门问题已经准备好」 |
page.tsx 不再调 setOnboardingPaywallOpen(true),只把 ref / setter 传给 hook;hook 用 tryOpenRedeemPaywall;两处点数不足各 1 处「先兑换弹窗、否则 billing」;文案「问题还在输入框里。」且不再有「入门问题已经准备好」;其余(兑换接口、按钮、billing 回调)断言原样保留 |
P4 / BUG-1080 |
chat-navigation-a11y-contract.test.ts(测试名不变) |
/if \(status === 402\) openAccountDialog\("billing", …\)/ |
/if \(status === 402 && !openRedeemPaywallOnce\(\)\) openAccountDialog\("billing", …\)/;「共 2 处 billing 调用」计数不变 |
P4 |
consult-first-frame-and-pacing-20260928.test.tsx(测试名不变) |
rejectRun 里 if (status === 402) openAccountDialog(…) |
if (status === 402 && !openRedeemPaywallOnce()) openAccountDialog(…);计数 2 不变 |
P4 |
测试名对基线 be9e4dbe:消失 6 条(上表 6 条改名 / 取代,均为本系列自己的测试或本单改时机的那条,逐条有替代),新增 18 条。任务书验证项写「不得有测试名消失」,这 6 条是按新决策改名,旧名描述的行为已被产品推翻,保留旧名会与断言内容相反,所以没有保留,请 Claude 定。
验证
基线与交付在同一台机器、同一命令各跑一次(基线用临时 detached worktree be9e4dbe,node_modules 硬链接,用完已删)。
| 项 | 基线 be9e4dbe |
交付 |
|---|---|---|
tsc --noEmit |
0 错 | 0 错 |
npm run lint |
0 error / 127 warning | 0 error / 127 warning(逐条同名;中途多出的 1 条 _caption 未使用已修掉) |
npm test |
tests 4310 · pass 4255 · fail 24 · cancelled 0 · skipped 31 | tests 4322 · pass 4267 · fail 24 · cancelled 0 · skipped 31 |
| 失败清单 | 24 条(Docker / PostgreSQL 数据库与部署套件) | 与基线逐条同名 |
| 测试名对比 | — | 消失 6(见「改动的既有断言」,均改名 / 取代),新增 18 |
python3 -m pytest tests/test_birth_sky.py tests/test_api_server_growth_contract.py tests/test_repo_privacy_markers.py |
— | 全部通过 |
python3 scripts/run_quality_gate.py --profile quick(Node 22 在 PATH) |
— | Python 步骤全过(pytest 997 passed / 1 skipped);末步 npm test 24 条失败与基线逐条同名 → 退出码 1(无 Docker) |
rm -rf .next && next build |
/ ○ Static |
/ ○ Static;路由表与基线逐行相同 |
JyotishAPIHandler / jyotish_api_server.py |
— | 未改 |
page.tsx 行数 / Home() 的 useState + useRef |
1375 / — | 1364 / 未新增 |
首屏 gzip(同一脚本 scratchpad/polish/gzip.sh:预渲染 / 的 index.html 里引用的全部 /_next/static/*.js|css,逐个 gzip -9 求和;两边都是干净全量构建)
| 基线 | 交付 | 变化 | |
|---|---|---|---|
| JS | 650,883 B | 650,972 B | +89 B(+0.01%) |
| CSS | 66,126 B | 66,126 B | 0 |
JS +89 B 是 useConsultationRun 引入的 tryOpenRedeemPaywall 减去 page.tsx 删掉的 effect。caption、弹层、过场改动都在懒 chunk。
偏离任务书(请 Claude 定)
- 兑换弹窗不是「以服务端 402 为准」的唯一入口:
useConsultationRun在发送前本来就有前置余额检查(credits <= 0 && !activeSubscription直接拦下、打开 billing),0 点账号连「你好」都发不出去,根本到不了服务端。本单没有删这个前置检查(改计费行为不在任务书范围),而是让它和 402 走同一个「本页第一次先弹兑换弹窗」。结果:0 点账号发任何第一条消息(含闲聊)都弹兑换弹窗,而不是只有付费消息。任务书决策 4 里「免费闲聊轮照常免费、不会被弹窗拦住」对 0 点账号并不成立(改之前就不成立)。真机清单 34 条请产品看实际表现。 - golden 未重新生成:引擎输出没变,caption 在 BFF 层拼,夹具只加了测试侧的虚构地点。
- 测试名消失 6 条(见上)。
- 弹层无障碍名称用
aria-label(原来是aria-labelledby指向标题,标题删了)。 - 海外城市不是一律不带:世界表的三段标签取第三段英文原名。
环境缺口
- 无登录态与真机:弹层、caption、过场节奏、兑换弹窗新时机只在单测、源码合同、无头 Chrome 样张上验证;真机清单
docs/testing/birth-sky-cover-checklist.md新增 31–42 条。 - BUG-1080 的真实原因需要部署后在真机上重走新账号流程;Safari 未能验证。
- 无 Docker:数据库 / 部署套件 24 条失败与基线逐条同名;quick 门禁最后一步因此退出码 1。
- 未推送、未部署。
Claude 验收(2026-09-29,dec36bce)
独立重跑:tsc 0 错;lint 0 error / 127 warning;npm test 4322 · pass 4267 · fail 24,失败清单与基线逐条同名(Docker / DB / 部署);pytest(birth_sky + 增长合同 + 隐私)85 passed;rm -rf .next && next build / ○ Static;首屏 gzip(Claude 口径,基线 be9e4dbe 同代码实测 650,883 / 66,126)→ 650,972 / 66,126,JS +89 B(+0.01%)。page.tsx 1375 → 1364,useState/useRef 数 70 → 70。看过弹层三视口截图与三张海报:无面板、caption 在事实句下。BFF 只多一个 caption 字符串。
偏离裁定:1(0 点账户首条任何消息都弹兑换,包括闲聊)接受——客户端余额预检本来就拦住所有发送,改它属计费行为,不在本单;2–5 接受;6 新文案接受,待产品真机看。BUG-1080 维持 investigating(Chrome 复现证伪「被遮挡」),真机新账号复核。