- 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
19 KiB
PROGRESS · React Compiler 接管 page.tsx 重渲染(codex/react-compiler-20260817)
文件名偏离说明(先写在最前面)
任务书要求新建 PROGRESS.md。本仓库根目录已有受版本控制的 progress.md(1025 行),
而本机文件系统大小写不敏感(已实测:ls PROGRESS.md 命中 progress.md,内容相同)。
在根目录写 PROGRESS.md 等于覆盖 progress.md,而 progress.md 不在「只允许改」清单里。
两条约束打架时,优先不破坏清单外的文件,因此进度记录落在
PROGRESS-react-compiler-20260817.md。BLOCKED.md 无此冲突,按原样追加到根目录既有文件。
我理解的目标/顺序/最大风险(任务 0 要求,≤10 行)
- 目标:让 React Compiler(Rust 版)真正编译
frontend/src/app/page.tsx的Home, 使这个 3738 行、24 个 useState / 18 个 useEffect、0 个手写记忆化的组件由编译器自动优化。 - 顺序:任务 0 复核基线 → 任务 1 升 Next 16.3.1(Rust 版编译器的前置) → 任务 2 开启配置并取证 +
反向验证 → 任务 3 测构建代价 → 补
docs/BUG_HISTORY.md并提交。 - 让步顺序:编译器真的生效 > 测试与构建全绿 > 构建速度。
- 最大风险:配置开了、构建绿了,但编译器对着这个超大组件静默放弃(bailout 不报错),
于是「看起来做完了、实际零收益」。所以取证必须落在构建产物上,并用
"use no memo"反向验证 证明取证方法不是恒为真。 - 次要风险:16.3 的其他变更牵连别处(按任务 1 止损条款回滚);构建变慢或首屏体积上涨。
- 硬红线:不手写 useCallback/useMemo;
frontend/src/**只读(注解模式的一行指令除外); 测试与 CI 配置一行不碰;测试数只许 ≥1592 且 skipped=0。
任务 0 · 基线复核
worktree:/Users/jesse/Downloads/Copse/astrology/.worktrees/react-compiler-20260817
(git worktree add -b codex/react-compiler-20260817 ../.worktrees/react-compiler-20260817 origin/staging,
基点 e8d201dd)。frontend/ 已跑一次 npm install(新 worktree 无 node_modules)。
| 复核项 | 任务书给的数 | 实测 | 结论 |
|---|---|---|---|
| 过滤后测试文件数 | 199 | 199 | 对上 |
| 测试通过数 | 1592 | tests 1592 / pass 1592 / fail 0 / skipped 0 / todo 0 | 对上 |
npx tsc --noEmit |
干净 | 退出码 0,无输出 | 对上 |
page.tsx 行数 |
3738 | 3738 | 对上 |
| next 当前版本 | 16.2.10 | 16.2.10 | 对上 |
npm view next version |
16.3.1 | 16.3.1 | 对上 |
babel-plugin-react-compiler |
未装 | 未装(Rust 版不需要) | 对上 |
npx react-compiler-healthcheck 查实:包存在,npm view react-compiler-healthcheck version = 1.0.0。
但它是 Babel 版编译器的体检工具,跑的不是本次实际启用的 Rust 版编译管线,
两者的 bailout 判定可能不一致,healthcheck 通过不等于 Turbopack 里真编译了。
因此取证采用任务 2 的第二种方法(构建产物里定位 Home 的 _c(N) 缓存槽),
它直接落在真实构建产物上,且能被 "use no memo" 反向验证推翻。
锁文件:npm install 如任务书预告的那样整体重排 —— 961 条依赖,键序由非规范变为 npm 规范的字典序,
另有 6 条(@types/react-dom、escape-string-regexp、js-tokens、loose-envify、prop-types、
react-is)去掉了 dev: true(npm 重算了它们的 dev 可达性)。逐条比对确认:
包数量 961→961、无新增、无删除、无 version/resolved/integrity 变化。已单独成一个提交
(3371baca),排在版本升级之前,这样升级那一笔的 diff 只有 490 行且逐行可读。
顺带记下 16.2.10 的构建参考点(升级前的退路数据,不是任务 3 的四个数):
npx next build 退出码 0、real 39.02s,/ 首屏 18 个 JS chunk 共 gzip 490798 B = 479.3 KB。
任务书给的 476.3 KB 参考值与此相差 0.6%,说明体积口径对齐了。
任务 1 · 升级到 Next 16.3.1(完成)
npm install next@16.3.1 --save-exact。锁文件的真实变更 490 行,逐条核对为:
next/@next/env/@next/swc-* 16.2.10→16.3.1,加上 npm 自行重解析的传递依赖
sharp 0.34.5→0.35.3、@img/* 一族、@swc/helpers 0.5.15→0.5.23、
postcss 8.4.31→8.5.23(原先嵌在 @tailwindcss/postcss 下的副本被提升合并,故包数 961→962)。
这些都是升级的传递结果,不是我顺手升的依赖;package.json 只动了 next 一行。
eslint-config-next 仍留在 16.2.10:与 next 版本号不同步,但 npx eslint 实测 0 error
(4 个 warning 全在我没碰过的 src/、tests/ 文件里,升级前就有),
没有出现必须同步升级的理由,按「不许顺手升别的依赖」保持不动,只把这条记在这里备查。
验收:
| 项 | 结果 |
|---|---|
npx tsc --noEmit |
退出码 0,无输出 |
npx next build |
退出码 0(两次,均绿) |
| 测试 | tests 1592 / pass 1592 / fail 0 / cancelled 0 / skipped 0 / todo 0 |
测试第一遍出现过 1 个失败:tests/rectification-v9-database.test.ts 的
「v9 migration applies on a fresh database and re-applies idempotently」报
database migration failed(1 !== 0)。它靠 Docker 起临时 Postgres,与 Next 版本无因果关系。
单独重跑该文件 7/7 全过,全量重跑 1592/1592 全过,判定为全量并发下 Docker 资源争用导致的 flake。
没有改任何测试文件,也没有加 skip:两次原始输出都在对话里贴了。
任务 2 · 开启编译器并取证(结论:编译器拒编 Home,按止损条款回滚配置)
配置本身是对的
reactCompiler 确认是顶层选项、turbopackRustReactCompiler 确认在 experimental 里,
依据是 16.3.1 自带的类型定义 node_modules/next/dist/server/config-shared.d.ts:
第 1399 行 reactCompiler?: boolean | ReactCompilerOptions(NextConfig 顶层)、
第 1092 行 turbopackRustReactCompiler?: boolean(ExperimentalConfig 内)。
任务书这一点是对的,网上 experimental.reactCompiler 的写法确实是 Next 15 旧写法。
按此写入后构建打印 ✓ turbopackRustReactCompiler,authInterrupts、optimizePackageImports、
output、turbopack、outputFileTracingRoot、outputFileTracingIncludes 全部逐项保留。
取证方法
用任务书的第二种取证法(构建产物定位 Home + 找编译器注入的缓存槽),不用 healthcheck,理由见任务 0。
实现细节:Home 在产物里被压缩改名,function Home( 搜不到,所以改用 source map 反查 ——
.next/server/chunks/ssr/_0kxuuj2._.js.map 的 sources 里有 frontend/src/app/page.tsx,
解 VLQ mappings 就能把每个生成位置映射回原始文件与行号。两个脚本都放在 /tmp,不进仓库:
/tmp/locate_home.mjs(定位 page.tsx:1002 的 Home 并打印生成代码)、
/tmp/slot_owners.mjs(把每个缓存槽 _c(N) 归属到原始文件,给出「每文件被编译函数数」表)。
缓存槽在压缩后的真实形态是 (0,X.c)(N) 而不是 _c(N),正则要匹配这个形态。
六次尝试的结果(全部原始输出已贴在对话里)
| # | 配置 | page.tsx 被编译函数 |
Home |
构建 |
|---|---|---|---|---|
| 1 | reactCompiler: true |
2 | 未编译 | 退出码 0 |
| 2 | { panicThreshold: "all_errors" } |
2 | 未编译,且未报任何错 | 退出码 0 |
| 3 | 同上 + page.tsx 文件顶部 "use memo" |
2 | 未编译 | 退出码 0 |
| 4 | { compilationMode: "all" } |
47 | 未编译 | 失败 |
| 5 | { compilationMode: "all", panicThreshold: "all_errors" } |
47 | 未编译,且未报任何错 | 失败 |
| 6 | { compilationMode: "annotation", ... } + Home 体内 "use memo" |
0 | 未编译 | 退出码 0 |
关键读数:
- 默认
infer下全项目共 44 个函数拿到缓存槽,page.tsx内部有 2 个小组件被编译 (原始行 760、815,槽数 11 和 24),但 3738 行文件里那个 2730 行的Home没有。 所以不是「文件没进编译器」,是「Home这一个函数被拒」。 - 第 4/5 次把
compilationMode设成all(绕过组件识别启发式,逼编译器编译每个函数),page.tsx被编译函数从 2 涨到 47,Home仍然没有。既然all模式下不存在「没被识别成组件」 这回事,那就只剩一种解释:编译器尝试了Home并失败,然后按panicThreshold默认值none(文档原话「skips components which cannot be compiled」)静默跳过。 panicThreshold: "all_errors"在 Rust 版下形同虚设:第 2、5 次都设了,构建既不报错也不打印 任何编译器诊断。所以拿不到「Home到底哪一行挡住了编译器」的具体原因。- 第 4/5 次的
compilationMode: "all"还会直接把构建搞坏,不能用:它会去编译模块作用域的普通回调, 于是birth-time-intake.tsx:22的Array.from(..., (_, index) => ...)被插入useMemoCache, 预渲染/时炸TypeError: Cannot read properties of null (reading 'useMemoCache')。 - 第 6 次是领导指定的注解模式退路,也不成立。顺带纠正任务书一处技术细节:
"use memo"是函数体内的指令,不是文件顶部指令,依据是 16.3.1 自带文档node_modules/next/dist/docs/.../reactCompiler.md第 82-96 行的示例 (export default function Page() { 'use memo' ... })。我按正确位置放进Home体内第一行, 结果整个项目 0 个缓存槽、Home依旧未编译 —— 注解模式连原本能编的那 44 个都停了,纯亏。
反向验证(证明取证方法不是恒为真)
在 reactCompiler: true 下,只加/删 page.tsx 文件顶部一行 "use no memo",其余不动:
- 加上:
frontend/src/app/page.tsx compiled functions: 0 - 删掉:
frontend/src/app/page.tsx compiled functions: 2(槽 11、24),全项目 44 个槽
两段有可见差异,且差异只落在 page.tsx 上(其他文件的槽数不变),说明这套取证能真实反映
page.tsx 的编译状态,不是恒为真。同时它在两种状态下都判定 Home 未编译,
所以「Home 没被编译」不是取证方法的假象。反向验证用完已删除该行,page.tsx 与 origin/staging 逐字节一致。
结论与处置
任务 2 的验收(拿出 Home 被编译的证据)没做成,而且不是配置写错,是 Next 16.3.1 的 Rust 版
React Compiler 在所有六种配置下都拒编 Home,包括显式 "use memo" opt-in。
把它编译过去只剩两条路,两条都越界,所以都不做、写进 BLOCKED.md:
- 改
page.tsx的业务代码去迎合编译器 —— 任务书明令禁止(「哪处代码挡住了编译器就写进 BLOCKED.md」)。 - 换 Babel 版编译器(
turbopackRustReactCompiler: false)—— 需要新增依赖babel-plugin-react-compiler(next 的可选 peer,未装,Next 只内置了 runtime 没内置 plugin), 撞「不新增依赖」。
按任务 2 止损条款「连败 3 次改注解模式;仍失败就回滚编译器配置、保留任务 1 的升级、如实汇报」,
已把 next.config.ts 回滚到与 origin/staging 逐字节一致(git diff 为空),保留 Next 16.3.1。
用掉 6 轮验收,未满 12 轮;停下的原因不是轮数,而是不越界的配置空间已经穷尽。
任务 3 · 构建代价(四个数)
体积口径与基线一致:汇总 .next/server/app/index.html 引用的全部 /_next/static/**.js,
去重后逐个 gzip(Node zlib.gzipSync 默认级别)再相加。/ 是静态预渲染路由(构建输出标 ○ /),
index.html 存在,共 21 个 JS chunk。测量脚本 /tmp/measure_first_load.mjs,不进仓库。
每次构建前都 rm -rf .next(同时清掉 .next/cache/turbopack),四次都是冷构建。
四个数(任务书要求「各测 2 次取较快一次」,下表按前 2 次取最快给出):
| 编译器关闭 | 编译器开启 | 变化 | |
|---|---|---|---|
next build 耗时 |
41.29 s | 35.11 s | −6.18 s |
/ 首屏 JS(gzip 汇总) |
481397 B = 470.1 KB | 493428 B = 481.9 KB | +12031 B / +2.50 % |
必须说清的一点:耗时这两个数不可信,同机噪声远大于效应。实际观测到的全部冷构建耗时是 关闭 41.29 / 130.27 / 36.65 / 37.26 s,开启 100.43 / 35.11 / 39.21 s —— 机器安静时两边都落在 35~41 s,偶发的 100 s 与 130 s 是外部负载造成的离群值。 所以正确结论不是「开启后构建更快」,而是Rust 版编译器在本仓库没有可测量的构建变慢, 这与官方「Rust 版为缓解构建变慢而做」的说法方向一致。为避免误导,我补测到每边 4 次才敢这么说。 按任务书口径「取较快一次」,前两次的最快值就是上表的 41.29 与 35.11。
体积是确定性的:关闭态 4 次构建都是 481397 B,开启态 2 次都是 493428 B,逐字节可复现。 首屏涨 2.50 %,低于 5 % 阈值,不触发任务书的强制说明条款。
保留还是回滚的建议:本轮已按任务 2 止损条款回滚。若纯看这三个数,
「构建不变慢、首屏 +2.5 %、换 44 个组件自动记忆化」本身是笔划算的交易,
而且那 44 个里的 app-sidebar(112 槽)、sidebar-session-row(73 槽)、birth-date-picker(58 槽)、
三个引导 hook 都在 / 上渲染,收益落在同一个页面上。
但本轮的目标是 Home,Home 一个槽都没拿到;那 44 个组件的实际收益本轮没有测量(没有渲染基准),
在「收益未测量 + 首屏确定变大 + 止损条款明确要求回滚」三者叠加下,我选择回滚,
把「是否为了这 44 个组件而保留 reactCompiler: true」作为一个独立决策交出去,
而不是顺手替决策者拍板。要重新开启只需在 frontend/next.config.ts 加回两行,
本文件的第 1/6 次尝试记录了确切写法。
该决策已有结论(2026-08-17,交付后确认):维持回滚。 理由是本轮目标是 Home,
而那 44 个组件的运行时收益没有测量数据支撑首屏 +2.50 % 的代价。
若日后要改变这个结论,前置条件应当是先给那 44 个组件测出渲染基准,而不是直接开配置。
追加轮 · 授权新增依赖后的根因诊断(2026-08-17)
用户授权新增依赖,于是把 BLOCKED.md 第一条(拿不到编译器诊断)解掉。做法:临时
npm i -D babel-plugin-react-compiler,用 @babel/core 的 transformSync 单独跑 page.tsx
(parserOpts.plugins = ["jsx", ["typescript", {isTSX:true}]]),给插件传 logger 钩子逐函数收事件。
脚本在 /tmp/rc_diagnose.mjs、/tmp/rc_batch.mjs,不进仓库。
Home 到底被什么挡住
稳定版 1.0.0 对 page.tsx 报 26 个事件:CompileSuccess 2、CompileError 24。
Home(行 1002)的 24 条去重后三类,全部带 Todo: 前缀 —— 这是 React Compiler
标记「该语法尚未实现」的前缀,不是用户代码违规:
| 条数 | 原因 |
|---|---|
| 13 | Todo: (BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause |
| 9 | Todo: (BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch |
| 2 | Todo: (BuildHIR::node.lowerReorderableExpression) Expression type MemberExpression cannot be safely reordered |
那 13 个 finally 做的全是 finally 该做的事:window.clearTimeout(bootstrapTimeout)、
polling = false、rectificationOpenInFlight.current = false、
cancellationRequests.current.delete(requestId),以及 8 处 setProfileSaving(false)/
setAvatarSaving(false)/setCreatingSession(false)/setBirthTimeAssessmentPhase(null) 这类加载态复位。
删掉任何一个,try 块抛错时 UI 就永久卡在加载态 —— 那是拿真 bug 换假优化,所以一行没动。
顺带交叉验证了之前的产物取证:Babel 版报告编译成功的是 BirthLocationFields @ 760 与
ProfileFields @ 815,和先前从 Rust 版构建产物 source map 反查出的槽 11/24(行 760/815)
逐一对上。两条完全独立的证据链互相印证,取证方法没问题。
更新版本能不能救
experimental 版(0.0.0-experimental-a1856f3-20260507)里那 22 条 try/finally 与 throw
错误已被上游修好,CompileError 从 24 降到 2。但 Home 立刻撞上编译器内部断言失败:
Invariant: Expected all references to a variable to be consistently local or context references
(page.tsx:2666,catch (error) 的绑定既被直接使用、又被 setRequestError((current) => ...)
的闭包捕获)。在仓库外的副本上把这一处改掉,又冒出下一个
Invariant: [PruneHoistedContexts] Unexpected hoisted function(page.tsx:1837 的 refreshAccount,
被 1626 行的 useEffect 提前引用)。Invariant: 在 React Compiler 分类里是编译器 bug。
Home 共 50 个函数声明,其中 6 个被声明前引用,要满足编译器得跨千行重排这 6 个定义。
一句话:打地鼠,且每步都由编译器 bug 而非代码质量驱动。
换版本/换实现的全项目收益:零
对 frontend/src 全部 373 个文件跑批量诊断,两版对比:
| 稳定版 1.0.0 | experimental 版 | |
|---|---|---|
| 编译成功的函数数 | 134 | 134 |
| bailout 事件数 | 74 | 48 |
| 有 bailout 的文件数 | 21 | 21 |
成功数一个没多。原因是被上游修掉的错误类别,所在函数都还有别的拦路错误。
所以「升编译器版本」或「改用 Babel 版实现」这两条路由数据关闭,
babel-plugin-react-compiler 诊断完即卸载,锁文件用 npm ci 权威还原(npm uninstall 会留下
3 条 dev → devOptional 的元数据漂移,已还原掉),git diff 为空,不进交付。
这一轮改变了什么结论
任务 2 的验收结论不变(Home 未被编译),但原因的性质变了:
从「不明原因的静默失败」变成「上游编译器缺陷,本仓库代码无错」。
这直接影响后续该怎么做:不该去改 page.tsx 迎合编译器,该做的是按职责拆小 Home。
交付物本身零变化 —— 配置仍是回滚态,frontend/src/** 仍是零改动。
收尾
docs/BUG_HISTORY.md 追加 BUG-255。按 BUG-253 的防复发要求先检索最大编号:本地基点
e8d201dd 的最大编号是 253,但期间 origin/staging 已推进到 fb9eec54 并占用了 254
(标题「22 条记录的正文挂在别人的标题下…」),所以顺延为 255 —— BUG-253 记录的抢号失效模式又发生了一次,
已在记录里注明。提交前 npx tsc --noEmit 无输出、npx eslint 0 error(4 个既有 warning)。
交付内容(相对 merge-base e8d201dd):frontend/package.json(next 一行)、
frontend/package-lock.json(两笔:规范化重排 + 升级)、docs/BUG_HISTORY.md(+15 行)、
BLOCKED.md(+10 行)、本文件(新建)。
frontend/next.config.ts 已回滚、frontend/src/**、frontend/tests/**、tests/**、CI 配置零改动,
git diff 逐项为空。最终测试 tests 1592 / pass 1592 / fail 0 / cancelled 0 / skipped 0 / todo 0。