Files
Jyotisha/docs/tasks/TASK-frontend-followup-20260829.md
T
Jesse_ChenandClaude Fable 5.1 8db71aaf81 docs: product-level README, AGENTS.md split into code/reading parts, add CLAUDE.md, move task briefs to docs/tasks
- README.md is now the product/repo front door (architecture, repo map,
  local dev, test tiers, delivery flow, doc map). Engine positioning,
  VedAstro/Codex setup and the oracle/benchmark command reference move
  verbatim to docs/engine/README.md, docs/engine/vedastro-gateway.md and
  docs/benchmark/README.md. Capability badges realigned with the registry
  (91/78/8/0); tests/test_readme_badges.py was red on staging.
- AGENTS.md: Part A (environment truth, delivery, worktrees, record
  placement, bug workflow, growth freeze, frontend red lines, privacy,
  pre-work check, test tiers) and Part B (reading-rigor constraints).
  GitHub issue-tracker/triage boilerplate removed: GitHub is a read-only
  mirror. All strings locked by tests/ are preserved.
- CLAUDE.md added: roles, three working modes, task-brief sections,
  acceptance criteria, session discipline; imports AGENTS.md.
- 50 tracked TASK-*/PROGRESS-* files and 3 never-committed briefs move to
  docs/tasks/ with an index; REPO_LAYOUT.md merged into README.

Docs-only change (no gated path touched).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-03 06:56:06 +00:00

292 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.
# 任务书 · 前端优化九条的收尾(2026-08-29)
复核对象:`origin/staging` @ `3198fb6b`"perf(frontend): split chat streaming, load Inter, isolate admin CSS")。
上一轮交付基本属实,我逐条实测过:`tsc --noEmit` exit 0、`eslint` 0 error、`next build` exit 0、路由模式(`/` `○ Static``/login` `/admin/**``ƒ`)、admin 路由确实不再引用含 `.message-list` 的 216 KB globals chunk、`public/` 只剩 logo、Inter 走 `next/font` 并生成独立 `@font-face` chunk、星盘库 effect 已合并成单 effect 三分支。**本轮不重做这些,只收三个尾巴。**
---
## 硬红线
1. **不得修改任何既有测试断言。** 本轮三条任务都不需要改断言 —— 如果你发现"必须改断言才能过",那是方案错了,停下来登记 `BLOCKED.md`,不要动断言。
2. **不得让 `error.tsx` / `not-found.tsx` 重新 import `globals.css` 或 `site-styles`。** 这两页与 admin 共享根布局段,一旦 import,admin 每条路由会重新背上约 33 KB gzip —— 上一轮 −32 KB 的成果会当场归零。`tests/site-style-isolation-contract.test.ts` 已锁这条,它必须保持绿灯且不被修改。
3. 不得手写 `useCallback` / `useMemo`
4. 不得为迎合 React Compiler 改写代码;本轮不重开 React Compiler(理由见 `BLOCKED.md` 2026-08-17 记录)。
5. 推 staging 前必须 `./node_modules/.bin/tsc --noEmit` 通过(BUG-409 防复发)。**不要用 `npx tsc`**,本仓库环境下会装到空包 `tsc@2.0.4`
6. 测试数不得低于基线 **2205**,且 `fail=0``skipped=0`(在有 Docker 的环境里跑)。
7. 不得改 `.gitea/workflows/**`。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。
让步顺序:功能与测试不回归 > 可验证的修复 > 代码整洁。
## 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/frontend-followup-20260829 \
../.worktrees/frontend-followup-20260829 origin/staging
```
基线必须是 `origin/staging`,不是任何本地 ref。读 `pre_work_error_ledger.md`,跑 `scripts/pre_work_check.py`,读 `frontend/AGENTS.md`(Next.js 版本与你训练数据不同,写代码前先看 `node_modules/next/dist/docs/`)。改前先在 `docs/BUG_HISTORY.md` 里检索是否有同类记录。
---
## 任务 A(P0)· 根 404 / 错误页现在完全没有样式
### 事实
`src/app/not-found.tsx``src/app/error.tsx` 的 markup 是纯 Tailwind 工具类,但上一轮把 globals 从根布局段摘掉后,这两页**一条 CSS 都不加载**。构建产物实证:
```
.next/server/app/_not-found.html
→ 只引用 3b67syn08d_z3.css2,146 B,纯 Inter @font-face
`min-h-svh` / `text-muted-foreground` 的定义只在 0grkt280n67e_.css216 KB),该页不加载它
```
用户看到的是浏览器默认裸 HTML:不居中、无配色、无间距,`<Button>` 塌成一行光秃秃的链接文字。这是真实的用户可见回归,不是可接受的取舍。
`BLOCKED.md` 把唯一出路写成"必须另开一条不与 admin 共享的布局链"—— **这个判断是错的**,请一并订正该条记录。
### 做法
**照抄仓库里已有的同类模式**,不要发明新方案:`src/app/global-error.tsx``src/app/forbidden.tsx` 早就是内联 `style={{...}}` 写法,正是为了应付同一个约束。
具体:
- 模块级常量沿用 `global-error.tsx` 的形式:`const canvas = "var(--color-canvas, #fbfaf7)"` 等。**注意:这两页上 globals 不加载,CSS 变量取不到值,实际生效的是括号里的 fallback。** 这是预期行为,fallback 值必须和 `globals.css` 里的 token 真实值一致(`--color-canvas: #fbfaf7``--color-ink: #1d1d1f``--color-ink-secondary: #5f5f59``--color-border: #d8d6cf``--color-action: #85432f``--color-danger: #9a2f2f`),改前去 `globals.css` 核一遍,不要照抄我这里的数字。
- 移除 `import { Button } from "@/components/ui/button"` —— 它是 Tailwind 组件,在这两页上等于无样式。改用原生 `<button>`error 页的"重试")和 `<Link>`("返回对话"),各自带内联 style。`next/link` 保留,别退化成 `<a href>`,会丢客户端导航。
- `error.tsx``TriangleAlert` 可以留,但把 `className="size-8 text-destructive"` 换成 lucide 的 props`size={32}` + `color={danger}``aria-hidden="true"` 保留。
- `error.tsx` 保留 `"use client"` 和现有的 `useEffect(() => console.error(...))`,不要动错误上报逻辑。
### 无障碍合同(`frontend/DESIGN.md`,不得降级)
- 可点区域 `minHeight: 44px``minWidth: 88px`
- `error.tsx` 的错误块保留 `role="alert"`
- focus-visible 可见描边:沿用 `global-error.tsx` 的做法,页面内嵌一个作用域 `<style>``.xxx-action:focus-visible { outline: 3px solid ...; outline-offset: 2px; }`
- 文案一字不改
### 验收(必须逐条给出证据,不接受"看起来好了")
```bash
./node_modules/.bin/next build
# 1) 根 404 的 body 里不得再出现 Tailwind 工具类
python3 -c "
import re;h=open('.next/server/app/_not-found.html').read()
print(re.search(r'<body.*?</body>',h,re.S).group(0)[:1200])"
# 2) admin 仍不得引用 globals chunk(这条是 P0 的守门条件)
grep -rho '[a-z0-9_-]\{8,\}\.css' .next/server/app/admin/ | sort -u
# 期望:只有 antd/admin 那个 chunk + Inter chunk 两个,
# 绝不能出现含 .message-list 的那个 216 KB chunk
grep -l 'message-list' $(find .next/static -name '*.css')
# 3) 站点路由 CSS 不变
grep -o 'rel="stylesheet"[^>]*href="[^"]*"' .next/server/app/index.html
```
另外:`next start` 后人工看一眼 `/一个不存在的路径`,确认是居中的暖白页而不是裸 HTML,截图或描述写进 PROGRESS。
### 测试
`tests/site-style-isolation-contract.test.ts` 的既有断言**保持原样不动**(它锁的正是"这两页不许 import globals",我们的方案让它继续为真)。在同文件**新增**断言:这两页不含 Tailwind 工具类特征(例如不出现 `className="flex``min-h-svh``text-muted-foreground`),且不再 import `@/components/ui/button`
### 建档
`docs/BUG_HISTORY.md` 追加一条(用户可见回归,必须建档)。**编号前先重新确认远端最大号**(当前本地最大 BUG-430),别抢号:
```bash
git fetch origin --prune && git show origin/staging:docs/BUG_HISTORY.md | grep -o '^## BUG-[0-9]*' | sort -t- -k2 -n | tail -3
```
防复发条写清楚:根布局段(`error.tsx` / `not-found.tsx` / `forbidden.tsx` / `global-error.tsx`)的页面只能用内联样式,不得依赖任何工具类或 `globals.css`
### 止损
改动超过 250 行,或发现必须动布局链 / 移动 `app/page.tsx` 才能完成 —— 停下,把实际卡点写进 `BLOCKED.md`,不要硬推。
---
## 任务 B(P1)· 一条时区依赖的脆测试
### 事实
`frontend/tests/onboarding-presentation.test.ts:43` 用绝对时刻构造:
```ts
const noon = new Date("2026-07-19T12:00:00+08:00");
```
`src/lib/onboarding-client.ts:112` 读的是 `now.getHours()`(**运行机器的本地时区**)。在 UTC 环境下该时刻是 04:00,落进"夜深了"分支,断言 `中午好,周宁。` 必红。
实测:`TZ=Asia/Shanghai` → 3 pass / 0 fail;默认 UTC → 1 fail。跟本轮和上一轮改动都无关,是既有问题。
### 做法
改成本地时区构造 —— **同一个测试文件末尾已经在用这个正确写法**`new Date(2026, 6, 19, hour)`),照它改:
```ts
const noon = new Date(2026, 6, 19, 12);
```
**断言的期望值一个字都不许改**`中午好,周宁。` / `现在最想理清哪件事?` 等全部原样保留)。这不是改断言,是修夹具构造。
同文件第 27 行的 `new Date("2026-07-19T08:00:00+08:00")` 只被断言了名字、不涉及时段,可改可不改;若改,断言同样不许动。
顺手扫一遍是否还有同类:
```bash
grep -rn '+08:00' frontend/tests/ | grep -iv 'iso\|timestamp\|askedAt\|resolvedAt'
```
凡是"绝对时刻 + 断言时段文案"的组合都按同一方式修;只做时间戳比较的不要动。
### 验收
```bash
TZ=UTC ./node_modules/.bin/tsx --test tests/onboarding-presentation.test.ts
TZ=Asia/Shanghai ./node_modules/.bin/tsx --test tests/onboarding-presentation.test.ts
TZ=America/New_York ./node_modules/.bin/tsx --test tests/onboarding-presentation.test.ts
```
三个时区必须全绿。不建档 BUG_HISTORY(测试夹具问题,非产品缺陷),在 PROGRESS 里记一句即可。
---
## 任务 CP2,可止损)· 绕开 readonly 的类型夹具
### 事实
上一轮为了让 `tsc` 变绿,在两个测试里把直接赋值改成了 `Object.assign` 绕 readonly
```ts
// frontend/tests/rectification-answer-choice.test.ts
Object.assign(refreshed, { latestResult: { ... } });
Object.assign(refreshed.conversationSummary, { activeFocus: { ... } });
```
行为没变,但 `Object.assign` 会把真实的类型不匹配一起吞掉,将来这里改坏了 `tsc` 也不会报。
### 做法
按真实类型修:让夹具**一次性构造出正确形状的对象**,而不是先解析再改字段。例如把 `parseV9CaseDossier(...)` 的结果解构后重建一个新对象,或给局部变量一个显式的可变结构类型。
- 禁止 `as any` / `@ts-expect-error` / `@ts-ignore`
- 禁止改任何断言
- 禁止为此修改 `src/**` 的类型定义(那是产品类型,不该为测试让路)
### 止损
如果不改断言、不动产品类型就做不到,**保留现状**,只在 `Object.assign` 上方加一行注释说明为什么(哪个字段是 readonly、为什么夹具需要改它),并在 `BLOCKED.md` 记一条。这条不值得为它冒回归风险。
---
## 任务 D(P1)· 等待动效有七套,同一个视觉被实现了三遍
### 事实
全仓(不含 admin,antd 是独立设计系统,不在范围)现有等待动效清单,均已实测:
| 视觉 | keyframes | 时长 / 曲线 | 出现位置 |
| --- | --- | --- | --- |
| 轨道环 | `app-loading-orbit` | 1.4s linear | `AppLoadingIndicator``page.tsx:3707` 载入账户、`page.tsx:4103` 正在准备问题、`birth-time-assessment-overlay` |
| 打字光标 | `onboarding-caret` | 700ms steps(1,end) | `globals.css:657` 引导语打字流 |
| 文字微光 | `agent-activity-shimmer` | 1.6s linear + `background-clip:text` | `globals.css:322` Agent 活动状态 |
| 圆圈旋转 ① | `birth-place-spin` | **0.8s** linear | `globals.css:369``birth-place-picker.tsx:291` `<LoaderCircle className="is-spinning">` |
| 圆圈旋转 ② | `consultation-run-spin` | **0.8s** linear | `globals.css:999``consultation-run-timeline.tsx:60` `<LoaderCircle>` |
| 圆圈旋转 ③ | Tailwind `animate-spin` | **1s** linear | `personal-report-page.tsx:209``personal-report-center.tsx:78/168/178``reports/[reportId]/loading.tsx:7` |
| 透明度呼吸 | `daily-starlanguage-pending` | 1.6s ease-in-out | `globals.css:1780` ← 今日星语卡 `aria-busy` |
**①②③ 是同一个视觉(旋转的 lucide `LoaderCircle`)、三套实现、两种时长。**
用户说的"初始化结束有三种加载动画"能在 `page.tsx` 同一段 render 里定位到,是连着出现的三种:
- `page.tsx:4028` `<OnboardingChatMessage streaming>` → 打字光标闪
- `page.tsx:4100-4103` `onboardingPending` → 轨道环"正在准备问题"
- `page.tsx:4123` 附近 `aria-busy={dailyStarlanguageBusy}` → 今日星语卡开始透明度呼吸
三个不同节奏(700ms / 1.4s / 1.6s)、三种不同形态,在几秒内依次闪过。这是用户投诉的直接来源。
另有死代码:`globals.css:395``@keyframes pulse` 全仓 0 引用。
### 做法
**目标不是"全部统一成一个"**,而是按"等待的语义"分层,最多留三类,同类必须同实现:
1. **整页 / 整块阻塞等待**(载入账户、正在准备问题、生时评估浮层)→ 继续用轨道环 `AppLoadingIndicator`。**保持现状不动**,这是品牌感的来源。
2. **行内 / 局部等待**(出生地、运行时间线、个人报告)→ **收敛成唯一一个 `<InlineSpinner>` 组件**:一套 keyframes、一个时长、一处尺寸约定。上表 ①②③ 全部改用它,删掉 `birth-place-spin``consultation-run-spin` 两套 keyframes,清掉所有 `animate-spin` 类名。
3. **流式生成中**(打字光标、文字微光)→ **保持**。它们表达的是"内容正在到达",语义与"在等待"不同,不要一并统一掉。
透明度呼吸(今日星语卡)归入第 2 类的语义但形态不同 —— 由你判断:要么并入 `InlineSpinner`,要么保留但必须说明它为什么不能是 spinner。
**初始化结束那一刻,不得让用户连续看到三种形态。** 轨道环消失后今日星语卡立刻开始呼吸,等于换了个动效继续等。要求二选一:让星语的首次拉取在轨道环期间就完成(轨道环消失时卡片已是终态),或卡片首次渲染直接给静态占位文案、不加动效。改完必须按帧说明用户实际看到几种。
### 约束
- 不得引入任何第三方动画库
- 不得修改 `AppLoadingIndicator` 的 DOM 结构(`birth-time-assessment-overlay` 依赖它)
- 所有等待容器现有的 `role="status"` / `aria-live="polite"` / `aria-busy` 一律保留,不得因为换组件丢掉
- **reduced-motion 要特别处理**`globals.css:443` 有全局 `*` 规则 `animation-iteration-count: 1 !important`,旋转类动效在该模式下转一圈就停 —— 视觉上像卡死。新 `InlineSpinner``prefers-reduced-motion: reduce` 下必须退化成静态但语义明确的形态(静止圆点 + 文案),不要留一个停在半途的圆弧
- 顺手删 `globals.css:395` 的死 `@keyframes pulse`
- admin 的 antd `<Spin>` 不在范围,不要动
### 交付前必须写进 `frontend/DESIGN.md`
新增一节"等待与加载",把三类词汇表写死:每一类叫什么、用在什么语义、用哪个组件、时长多少、reduced-motion 下退化成什么。**这一步不能省** —— 不写进 DESIGN.md,下一个人还会再造第四套。
### 验收
```bash
# 行内旋转应只剩一处实现
grep -rn 'animate-spin' frontend/src/ # 期望 0 命中
grep -n 'birth-place-spin\|consultation-run-spin' frontend/src/app/globals.css # 期望 0
grep -c '@keyframes' frontend/src/app/globals.css # 应比现在的 13 少
```
外加:
- 改后动效清单表(每处等待 → 归哪一类 → keyframes → 时长),对照上面那张表
- 完整走一遍初始化:新账号 → 填姓名 → 填出生资料 → 初始化结束 → 落到首页。**按帧描述用户依次看到几种动效**,录屏更好。这是本条唯一真正的验收标准
- 出生地选择器、运行时间线、个人报告三处各自截图或描述,确认视觉一致
- 系统开启"减少动态效果"后再走一遍,确认没有停住的半圈
不建 `BUG_HISTORY`(这是设计收敛,不是缺陷),在 PROGRESS 里单列一节。
### 止损
收敛行内 spinner 若需改动超过 8 个文件,或需要触碰生时校正外壳 —— 只做"三个圆圈旋转合一 + DESIGN.md 词汇表",初始化流程的时序调整登记 `BLOCKED.md` 留下一轮。
---
## 不在本轮范围
- **`page.tsx:1209``visibleSessions`**(每次 render 重算 filter+filter+sort):没有基准证明它是热点,且红线禁止手写 `useMemo`。留到下一轮,届时先出基准。
- **深色主题**:产品决策,等人拍板,不得自行开工。
- **继续拆 `Home`**:上一轮只切了第一刀(`useState` 61→60),剩余切片单独立项。
- **打字光标与文字微光的形态本身**:任务 D 只收敛"等待"类,"内容正在到达"类不动。
- **`web/*.html`**:确认不删,Python API 仍在用。
---
## 收尾
- PROGRESS 文件名写 `PROGRESS-frontend-followup-20260829.md`。**不要写成 `PROGRESS.md`** —— 根目录已有受版本控制的 `progress.md`,大小写不敏感文件系统上会互相覆盖。
- 订正 `BLOCKED.md` 里"`error.tsx`/`not-found.tsx` 只能另开布局链"那条 —— 实际用内联样式即可,两全。
- 三条任务合成**一次** staging 推送(staging push 会触发全量构建+部署,没有路径过滤,不要分多次推)。
- 推送后核对 `https://staging.jyotisha.chat/api/health``.deployment.gitCommit` 等于新 SHA。
- 不自行提升 main。
## 交付物清单
1. `src/app/not-found.tsx``src/app/error.tsx` 改为内联样式,无 Tailwind 依赖
2. `_not-found.html` body 无工具类的构建产物证据
3. admin 路由 CSS chunk 清单证据(仍为 antd/admin + Inter 两个)
4. `site-style-isolation-contract.test.ts` **新增**断言(既有断言未改)
5. `onboarding-presentation.test.ts` 三时区全绿证据
6. 任务 C:修好,或保留 + 注释 + BLOCKED 记录
7. `InlineSpinner` 组件,①②③ 三处旋转全部改用它,`animate-spin` 与两套旧 keyframes 清零
8. `frontend/DESIGN.md` 新增"等待与加载"一节(三类词汇表 + reduced-motion 退化约定)
9. 初始化全流程按帧的动效说明(含 reduced-motion 一遍)
10. `docs/BUG_HISTORY.md` 新条目(任务 A,编号已对远端确认)
11. `BLOCKED.md` 订正(任务 A 那条判断有误)
12. `PROGRESS-frontend-followup-20260829.md`
13. `tsc --noEmit` / `eslint` / `tsx --test`(≥2205fail=0skipped=0/ `next build` 四条命令的实际输出