Files
Jyotisha/docs/tasks/PROGRESS-serif-headings-20260928.md
T
Jesse_ChenandClaude Opus 5.5 d670521f47
Independent Staging Quality Gate / validate (push) Successful in 14m16s
Independent Staging Quality Gate / publish (push) Successful in 3m46s
docs(tasks): Claude acceptance for serif headings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
2026-09-28 19:54:05 +08:00

157 lines
17 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-28)
任务书:`docs/tasks/TASK-serif-headings-20260928.md`。产品 2026-09-28 授权直接执行(Claude 子代理)。
- 基线:`origin/staging` = `ebcee584`(任务书写作时为 `aa33399a`,开工时已前进到任务书自身的提交)
- 分支 / 工作树:`codex/serif-headings-20260928` / `.worktrees/serif-headings-20260928`(本地两个提交,**未推送**,由 Claude 验收后推)
- 环境:Node v22.14.0(`/exec-daemon/node`);无 `.venv`,Python 一律 `/usr/bin/python3`(3.13,含 pyswisseph);本机临时装 `fonttools 4.65.0` + `brotli 1.2.0`(`pip --user`,不进仓库依赖);无 Docker;有 `/usr/bin/google-chrome`(无头)。
- BUG 编号:开工时最大 BUG-1079;本单不开新 BUG,只给 BUG-737 追加说明。
## 结论
| 项 | 结果 |
| --- | --- |
| T1 切片、脚本、Python 合同测试 | 完成(提交 1) |
| T2 接入字体栈 | 完成;`serif-sc.css` 由 `site-styles.ts` 引入(偏离,见下) |
| T3 合同测试改写 | 完成;三栏见下 |
| T4 性能与截图 | 未登录页真实测量;已登录首页 / 咨询 / 报告页用构建产物搭的静态页测量(环境缺口,见下) |
| T5 文档 | DESIGN §3、BUG-737、CHANGELOG、真机清单、任务索引已改 |
| 让步 | 未动用。报告页 493 KB(≤ 900 KB),截图已做 |
## T1 · 切片
- 上游:noto-cjk `Serif2.003`(tag SHA `9b0f1436`)的 `Serif/SubsetOTF/SC/NotoSerifSC-SemiBold.otf`,11,771,476 B,sha256 `517d9736…055bf5`,许可证为同 tag 的 `Serif/LICENSE`(OFL 1.1)原样放 `OFL.txt`。完整 URL、版本、sha256、改名方式写在 `frontend/src/app/fonts/serif-sc/SOURCE.txt`。源文件不进仓,脚本校验 sha256。
- 改名:family `Jyotisha Serif SC`、PostScript / CFF 名 `JyotishaSerifSC-SemiBold`;保留版权、设计者、厂商、许可证记录,删掉 Noto 商标记录和上游本地化名。
- 字表:《通用规范汉字表》一级 3500 + 二级 3000,取自 `shengdoushi/common-standard-chinese-characters-table`(commit `d9b599a9`,与 gov.cn 公布的 PDF 同源整理),逐字节原样放 `scripts/fonts/tygfhzb-level-{1,2}.txt`,来源与 sha256 写在 `scripts/fonts/CHARSET_SOURCE.txt`。脚本断言 6500 个互不重复的单字。
- **字频顺序(偏离任务书的「按字表顺序」)**:字表是按笔画排的,不是字频,拿它排前 1000 字首页会下载一堆无关切片。改用 `scripts/fonts/char_order.txt`(进仓,脚本 `--refresh-order` 生成):
1. 本仓语料(git 跟踪的 `frontend/src`、`frontend/docs`、`references/`、`SKILL.md`)用得最多的 300 字排最前——星、宫、盘、运这类标题常用字;
2. 其后按 Google Fonts 自己公开的简体中文字频分档(`googlefonts/nam-files` 的 `slices/simplified-chinese_default.txt`,commit `2a68014b`,Apache-2.0,前 20 档为 FreqRange);
3. 同档内按本仓语料次数,再按字表顺序。只进仓派生结果,不进仓原文件。
只用本仓语料的第一版实测:首页问候 + 人名常落到 5~6 片、最多 495 KB,超 300 KB 上限,所以加了第 2 层。
- 切片(共 30 片,`unicode-range` 互不重叠):
- `00` 基本拉丁、Latin-1 符号(不含带重音字母)、× ÷、通用标点、中文与全角标点、数字与常用符号,358 字,38.5 KB;
- `01`~`19` 有字频信号的 3635 字,按名次每片 192 字,40~62 KB;
- `20`~`29` 无字频信号的 2865 字按码位连续切块,每块约 287 字,72~109 KB。它们的 `unicode-range` 写成「块首到块尾,扣掉 00~19 已占的码位」,这样 CSS 小一半多。代价:6500 字以外、恰好落在某块跨度里的生僻字,会让浏览器下载那一片,但片里没有它的字形,照样退回黑体。
- 任务书写的「前 1000 字切 2~3 片、其余每片 300~400 字」按实测改细:前 1000 字 3 片时,任何有 10 个汉字的标题都会把 3 片(≈241 KB)全拉下来,加上拉丁片就碰 300 KB 上限。
- 单片最大 109,040 B(≤ 120 KB)。
- fontTools 参数:woff2、desubroutinize(CJK CFF 的 woff2 小约 13%)、保留 hinting(Windows 渲染)、默认 layout features、不重算时间戳。**同一输入跑两遍,30 个 woff2 + CSS 的 sha256 逐一相同**。
- `tests/test_serif_font_slices.py`(8 条):字表 6500 字;每字都被覆盖;拉丁 / 数字 / 中文标点被覆盖;各片不重叠;CSS 引用的文件都存在、是 woff2、≤ 120 KB、没有多余文件;family / 字重 / swap;OFL 与 SOURCE 在;装了 fontTools 时再核对每片字形覆盖其字表字且家族名不含 Noto。已加入 `run_quality_gate.py` 的 quick 列表(否则门禁永远不跑它;偏离,见下)。
## T2 · 接入
- `--font-display` 按硬红线第 3 条改为 `"Jyotisha Serif SC", var(--font-inter, Inter), -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif`;`--font-body` 未改。注释改写。
- **引入位置(偏离)**:任务书写「由 `layout.tsx` 引入」。根 `layout.tsx` 同时是后台 `/admin` 的根布局,`site-style-isolation-contract` 明确禁止它引站点样式;所以 `serif-sc.css` 放在 `src/app/site-styles.ts` 里、紧跟 `globals.css`——`(app)/layout.tsx` 与登录页都经它引入,后台不加载。单独文件而不并进 `globals.css`,因为 `@font-face` 的 `unicode-range` 数据 gzip 后约 22 KB,超过硬红线 5 的 +8%。
- 报告标题规则清单(改之前 grep):
| 规则 | 改前 | 处理 |
| --- | --- | --- |
| `.personal-report-cover h1` | `--font-display` | 不动 |
| `.personal-report-section-heading h2` | `--font-display` | 不动 |
| `.personal-report-md-article h2`(长文报告章节) | `--font-display` | 不动 |
| `.personal-report-md-article h1`(长文报告标题,markdown `#`) | 无规则,继承正文黑体 | **新增** `font-family: var(--font-display)`,只加字体不改字号 |
| `.personal-report-subheading h2/h3`、`.personal-report-appendix h3`、`.personal-report-disclaimer h2`、`.personal-report-state h1`、`.personal-report-facts dd`、进度标题 | `--font-display` | 不动 |
| `.personal-report-md-article h3` / `h4` | 无规则,继承正文 | **不动,请 Claude 定** |
| `.personal-report-theme h3` | 显式 `--font-body` | **不动,请 Claude 定**(不在任务书例外清单里,但写法是刻意的) |
| `.personal-report-chart-card h3` | `--font-body` | 例外,保留 |
另:`.membership-*` 标题(点数 / 订阅页)显式 `--font-body` 600,不在例外清单也未改,请 Claude 定。
- 打印:`@media print` 里没有任何 `font-family` 覆盖,打印沿用同一条栈,屏幕上已加载的切片直接可用;没有实际打 PDF(见环境缺口)。
- 例外保留:`.conversational-narrative .message-markdown h2, h3`、`.personal-report-chart-card h3`、后台全部、四个根错误页(内联无衬线栈)。
- `var(--font-display)` 在 `globals.css` 里 40 → 41 处(多出的就是 md h1)。
## T3 · 合同测试
| 文件 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `font-stack-loadable-contract.test.ts` `every quoted family … vendored or a system face` | 带引号 family 只能在 `SYSTEM_FACES` 里 | 也可以是 `serif-sc.css` 里 `@font-face` 声明过、且每个 `url()` 文件都存在的 family | 决策 1、3;「声明的字体必须可加载」这半条保留并扩展 |
| 同文件 `no CJK serif is reachable from the display stack` | 栈里不得有 Songti / STSong / SimSun / Noto Serif / Source Han Serif,不得以 `serif` 结尾 | **删除**,替换为 `the display stack leads with the self-hosted serif and never reaches a system 宋体`:首位是 `Jyotisha Serif SC`;不含 Songti / STSong / SimSun / Noto Serif CJK SC / Noto Serif SC / Source Han Serif;不以通用 `serif` 结尾、以 `sans-serif` 结尾;首位之后与 `--font-body` 完全相同 | 任务书 T3 明确要求删除并替换;BUG-737「不得落到系统宋体」保留 |
| 同文件 `display rank comes from weight, not from a second family` | 注释:共用一条无衬线栈 | 只改注释与报错文案;断言不变 | 字体没到时仍按无衬线显示,字重仍须承担层级 |
| `personal-report-theme-contract.test.ts` | 注释「`--font-display` 现在是无衬线」 | 注释改为历史说法并补 2026-09-28 说明;字重 500 断言不变 | T3 |
| `consultation-entrypoint.test.ts` | 同上 | 补一段注释;断言不变 | T3 |
新增 `frontend/tests/serif-headings-contract.test.ts`(7 条):校正叙述 h2/h3 与报告分盘卡 h3 仍用 `--font-body`(精确选择器,避免被通用 `.message-markdown h2` 蒙混);回答 h2、报告封面 / 章节 / 长文 h1 h2 用 `--font-display`;正文栈不含衬线;后台三文件不出现 heading face,`site-styles.ts` 引 `serif-sc.css` 而根 `layout.tsx` 不引;四个根错误页不引;每个 `@font-face` 是 swap、`500 700`、相对 `url()`(进 Next 打包),`layout.tsx` 无字体 preload。
测试名对比(`frontend/AGENTS.md` 的方法):消失 1 条 = 上表被任务书要求删除的那条;新增 8 条(上面 7 条 + 替换断言 1 条)。
## 验证
| 项 | 基线 `ebcee584` | 交付 |
| --- | --- | --- |
| `tsc --noEmit` | 0 错 | 0 错 |
| `npm run lint` | 0 error / 127 warning | 0 error / 127 warning |
| `npm test` | tests 4248 · pass 4193 · fail 24 · cancelled 0 · skipped 31 | tests 4255 · pass 4200 · fail 24 · cancelled 0 · skipped 31 |
| 失败清单 | 24 条(全是需要 Docker / PostgreSQL 的数据库与部署套件) | 与基线逐条 `diff` 一致 |
| `python3 -m pytest tests/test_serif_font_slices.py` | — | 8 passed |
| `python3 scripts/run_quality_gate.py --profile quick`(Node 22 在 PATH) | — | 所有 Python 步骤通过(pytest 982 passed / 1 skipped);最后一步 `npm test` 的 24 条失败与基线逐条一致(无 Docker)→ 退出码 1,属环境缺口。首次误用 Node 20 跑出 66 条失败(`mock.module`),已换 Node 22 重跑 |
| `python3 -m pytest tests/test_repo_privacy_markers.py -q` | — | 通过 |
| `next build`(`npm run build`,Turbopack) | `/` ○ Static | `/` ○ Static;全部路由的 ○ / ƒ 标记与基线逐行相同 |
构建备忘:Turbopack 在旧 `.next` 上增量构建时,曾输出旧的 `--font-display` 值(CSS chunk 未失效);测量一律 `rm -rf .next` 后全量构建。
### 首屏 gzip(`index.html` 引用的全部 js / css,gzip -9 求和)
| | 基线 | 交付 | 变化 |
| --- | ---: | ---: | ---: |
| JS | 651,590 B | 651,590 B | 0(0%) |
| CSS 合计 | 43,493 B | 66,346 B | +22,853 B(+52.5%) |
| 其中 globals chunk | 42,248 B | 42,268 B | +20 B |
| 其中字体 chunk | 859 B(next/font 的 Inter / 行星符号 `@font-face`) | 23,692 B(上述 + `serif-sc.css`,Turbopack 合成一个 chunk) | +22,833 B |
CSS 超 +8%,按硬红线 5 已拆成单独文件(单独 chunk)。原因:30 片的 `unicode-range` 里 3635 个高频字只能逐个码位列出(字频顺序与码位无关,合不成区间),这是按字频切片、让首页只下 3~5 片的代价;无字频信号的 2865 字已改写成区间。宋体本身不 preload、`swap`,不阻塞首屏;这 23 KB CSS 是渲染阻塞的,4G 下约数十毫秒。
### 宋体切片流量(无头 Chrome,禁缓存,统计 `jyotisha-serif-sc-*.woff2` 的 `encodedDataLength`)
| 页面 | 测法 | 片数 | 字节 |
| --- | --- | ---: | ---: |
| `/`(未登录;本机无后端,显示「暂时无法进入 Jyotisha」卡与侧栏) | `next start` 真页面 | 5 | 226,060 |
| `/login`(未登录落地页) | `next start` 真页面 | 5 | 220,696 |
| 已登录空白首页(品牌字 + 问候「下午好,王芳」,虚构名) | 构建产物 CSS 搭的静态页 | 5 | 235,833 |
| 咨询回答(`h2`「D10 事业盘怎么读」+ `h3`) | 同上 | 4 | 171,696 |
| 完整个人报告(封面 h1 + 虚构读者 fixture `report-density-fictional-reader.json` 的全部 h1/h2,共 24 个标题;长文报告未进视口的章节也会先渲染 h2 占位,所以全部 h2 都算) | 同上 | 10 | 493,058 |
上限:首页 ≤ 300 KB、报告 ≤ 900 KB,均满足。离线估算(按切片成员与文件大小):首页问候池 10 句 × 虚构人名为 131~309 KB,只有「周末愉快,赵磊」一句(6 片)略超 300 KB——问候随机、人名因人而异,首页会在这附近浮动;另一份 fixture `report-reader-main-fictional.json` 的报告为 7 片 ≈ 341 KB。
截图(不进仓):`/tmp/claude-1000/-workspace-Jyotisha/d49dfa04-4e7a-47b2-80a7-8529c4c38f86/scratchpad/` 下 `shot_login.png`(登录页)、`final-home-loggedin.png`(首页问候)、`harness-consult.png`(回答 h2/h3)、`final-report.png`(报告封面与章节)。四张都确认中文标题(含同行的 `D10`、`Jyotisha`)为宋体、正文为黑体。
## T5 · 文档
- `frontend/DESIGN.md` §3:重写 Display 条目(新栈、自托管切片、Next 打包、500~700、swap 不 preload、中英混排、只许可加载字体、例外清单);删除「CJK never takes a serif here」,改为「CJK 标题用自托管 Jyotisha Serif SC;绝不落到系统宋体」;根错误页条目补一句「标题也是刻意无衬线」;修掉表格下「Display headings use the serif stack at weight 400」。
- `docs/BUG_HISTORY.md` BUG-737:追加 2026-09-28 一行(任务书指定文字 + 实现摘要),状态仍 resolved,「最近更新」改为 2026-09-28。
- `CHANGELOG.md`:「标题改用宋体,正文仍是黑体」,Skill 不 bump,不改数据库。
- `docs/testing/serif-headings-checklist.md`:iPhone Safari、Windows Chrome / Edge、安卓 Chrome(至少一台国产机)× 首页标题 / 回答小标题 / 弹窗标题 / 报告封面 / 打印 PDF;SimSun 判别;Slow 4G 先黑体后宋体、无空白字;Network 里只有本站 `/_next/static/media/` 的小切片;生僻字退回黑体。
- `docs/tasks/README.md`:本单状态改为「已实现待验收」。
## 偏离任务书
1. `serif-sc.css` 由 `site-styles.ts` 引入,不在根 `layout.tsx`(理由见 T2:根布局也管后台)。
2. 字频顺序不用字表顺序,用「本仓语料前 300 + Google Fonts 字频分档」(理由见 T1);因此多引用了一份 Apache-2.0 的派生数据,来源已记录。
3. 切片粒度比任务书建议的细:高频 19 片 × 192 字,低频 10 片 × ~287 字(理由见 T1,为满足首页 300 KB)。
4. 拉丁片不收 Latin-1 带重音字母(À–ÿ),只收 Latin-1 符号与 × ÷;任务书要求的「基本拉丁、中文与全角标点、数字与常用符号」都在。
5. 新增 `.personal-report-md-article h1` 用 `--font-display`(长文报告的标题行就是它的「封面」)。
6. `tests/test_serif_font_slices.py` 加进 quick 门禁列表(`scripts/run_quality_gate.py` +2 行)。
## 请 Claude 定
- `.personal-report-md-article h3`(长文报告小节,现继承黑体)、`.personal-report-theme h3`(显式黑体)、`.membership-*` 标题(显式黑体 600)是否跟随改宋体。本单均未改。
## 环境缺口
- 已登录首页、咨询回答、报告页没有登录态与数据库,无法在真页面上测;改用「同一次构建的 CSS chunk + 真实类名 + 虚构内容」的静态页测流量与截图。真页面的最终数字要在 staging 登录后用 DevTools 复核(真机清单第 7 条)。
- 打印 PDF 未实际导出:`@media print` 无字体覆盖,理论上沿用宋体;留给真机清单。
- 真机(iPhone / Windows / 安卓)与 Slow 4G 观感:`docs/testing/serif-headings-checklist.md`。
- 无 Docker:数据库 / 部署 24 条前端测试与基线同样失败;quick 门禁因此退出码 1。
- 未推送:按指示不推 staging、不推分支;部署核对由 Claude 做。
## Claude 独立验收(2026-09-28)
| 项 | 结论 | 证据 |
| --- | --- | --- |
| T1 切片 | 通过 | `tests/test_serif_font_slices.py` 8 passed;字体 name 表 ID1/16 = `Jyotisha Serif SC`,OFL 与 SOURCE 齐全 |
| T2 字体栈 | 通过 | `--font-display` 与硬红线 3 逐字一致;`--font-body` 未动 |
| T3 合同测试 | 通过 | Node 22:tests 4255 / pass 4200 / fail 24 / cancelled 0;24 条均为需 Docker 的数据库/部署套件,与 PROGRESS 基线一致 |
| tsc / lint | 通过 | tsc 0 错;lint 0 error / 127 warning(同基线) |
| `/` Static | 通过 | 干净 `rm -rf .next` 后 `next build`:`○ /` |
| 首屏 CSS gzip | **接受偏离** | +22.9 KB gzip(+52.5%),来自 3,635 个高频字逐字写进 unicode-range;已按任务书拆成独立 CSS。产品要的是全设备一致的宋体标题,这是它的直接成本,不再压缩 |
| 视觉 | 通过(静态) | 回答 h2/h3 与报告标题为宋体、正文黑体(执行方截图);真机清单 `docs/testing/serif-headings-checklist.md` 欠 |
| 执行方提问:`.personal-report-md-article h3`、`.personal-report-theme h3`、`.membership-*` | 维持黑体 | 这些规则本来就不走 `--font-display`,不在任务书范围;产品看真机后再定 |