架构待办的历史:执行记录 · 核实记录 · 已决调研
📦 归档文档(2026-09-24):这是从 架构待办事项 里 搬出来冻结的历史。待办页现在只回答「还剩什么」,「做过什么」在这里。
⚠️ 文中的待办编号(P/A/F/D/R)是迁移前的口径,其中大部分条目已经做完并移出待办; 要判断某件事现在还算不算数,看待办页与各专题文档,别只看这里。
F2-b 调研结论:文档链接校验用什么(2026-09-23)
需求拆成三块,分别找现成工具,结论是只有第 1 块有零成本方案:
| 需求 | 候选 | 结论 |
|---|---|---|
| ① 站内相对/绝对链接的目标页是否存在 | VitePress 内置 ignoreDeadLinks | ✅ 已启用(见执行记录第 2 批)。零依赖、就是构建本身 |
② #fragment 锚点是否真实存在 | remark-validate-links(v13,离线)lychee + lycheeverse/lychee-actionmarkdown-link-check / linkinator / broken-link-checker | 见下 |
| ③ prose 里反引号引用的仓库路径(约 584 处) | 无 | ❌ 没有现成库。所有链接检查器都只认真正的链接语法,不认反引号里的纯文本。要么不查,要么自己写几十行 |
为什么 VitePress 用不了 remark 插件(这是选型的关键,已核实):VitePress 的 markdown 引擎是 markdown-it,不是 remark/unified —— markdown.config 拿到的是 markdown-it 实例(本仓现在用它装 vitepress-plugin-tabs),所以 remark-validate-links 插不进构建。若退化成 remark-cli 单独一步, 它又要求链接指向真实存在的文件,而本仓 174 条相对链接里有 ~15 条是不带 .md 后缀的(VitePress 允许、会自己补),会直接误报。⇒ 不推荐。
推荐(要用时直接抄):lychee 是唯一能补上锚点校验的现成工具 —— 它支持 --offline(不联网)、--include-fragments(查锚点)、--fallback-extensions md(补 VitePress 的扩展名省略)、 --exclude-path(排除冻结语料)。CI 里用官方 action,不需要本地装二进制,与本仓 lint:workflows(actionlint 没装就跳过、CI 强制)完全同形:
- uses: lycheeverse/lychee-action@v2
with:
args: >-
--offline --include-fragments --no-progress
--fallback-extensions md
--exclude-path 'apps/docs/src/zh-CN/content/archive'
--exclude-path 'apps/docs/src/zh-CN/content/industry-research'
'apps/docs/src/zh-CN/content/**/*.md'为什么现在没装:全站实测 8 条带锚点的站内链接(2026-09-23 手工对着构建产物逐条核过),而 VitePress 内置 已经覆盖了「目标页不存在」这个大头 —— 为 8 条链接引入一个二进制 + 一个 CI 步骤,ROI 说不通。 触发条件:等锚点链接涨到几十条,或出现一次「锚点静默失效」的真实事故。
顺带查出的真实错误(已修):architecture-todo.md 里两个锚点写错了。VitePress 的 slugify 会把 全角括号变成 -:## 搁置(等条件成熟) 生成的是 id="搁置-等条件成熟",而我原来写的是 #搁置等条件成熟(无横杠)。已按构建产物里的真实 id 改正。 📌 记住这条规则:中文标题里的 () : 都会变成 -,手写锚点必须对照构建产物的 id。 (2026-09-23 补记:全角冒号那条是后来才发现的 —— 当时漏掉的正是本页那个 F2-b 自指链接。)
执行记录
追加式历史。只有本表记录「做过什么」,上面的待办表只回答「还剩什么」。
| 日期 | 完成项 |
|---|---|
| 2026-09-24 | 「改了东边坏了西边」那次事故 → 上一条机械门禁(pnpm lint:pre-hooks):起点:你指出 CI 红了。查清红的是上一提交( 98423b5,run #22):把两个 unplugin 的生成物移出跟踪面时,我只给 dev 与 types:check 接了生成步骤,漏了 build —— 打包时 vite 的检查器在插件写盘之前就建好了 TS program(以前靠提交里那份现成的 dts 兜着),于是 Build admin 报一串 Cannot find name 'useAppStoreUserPreference' / watch / useAppUserMonitor。本地复现判据:把该包生成物移走、不经 pre 钩子直接 vue-tsc,报的就是同一批名字。修(e51d341):生成物收敛成一个入口 build/generate/index.ts,predev / prebuild / prebuild:stage / pretypes:check 全部指向它 —— run #23 两个 job 全 success。本轮(你拍板「加」):新门禁 pnpm lint:pre-hooks(bin walnut-check-pre-hooks,9 个用例)。判据:包内有 build/generate/ ⇒ 它跑 vite / vue-tsc 的脚本都必须有 pre<script> 钩子(vite preview 除外;钩子里写了 node <文件> 时那个文件必须存在)。⚠️ 为什么拿「有没有 build/generate/」当判据:@walnut/contract 同样跑 vite build 却没有任何生成物 —— 按「命令里有 vite」一刀切,这条门禁第一天就误报,而误报的门禁等于没有门禁;判据必须来自包自己(加生成器目录 = 主动加入这条规则)。五步接线全走完(gate + bin + scripts 包 bin 登记 + 根脚本 + prepush 表 + ci.yml quality job),并按 gate-wiring.test.ts 的口径补进发版电池与 release.md 的电池表;顺带同步了两处「整表钉住」的期望(prepush 的 id 序列、电池的 argv 逐行表)。负对照真跑过:临时摘掉 admin 的 prebuild ⇒ 门禁 exit 1,报 [vite-script-needs-pre-generate] @walnut/admin 的 \build`(`vite build`)会读生成物,却没有 `prebuild` 钩子(文案里直接点出这次事故);git checkout 还原后即绿。<br>**验证**:pnpm prepush **17 段全绿**(新段 0.8s)、@walnut/scripts **346 用例**、@walnut/release **198 用例**、真仓正面控制(collectFindings()为空 —— 只有 admin 有build/generate/)。<br>⚠️ **过程里的两个坑(都不是新问题,是本仓已记过的规矩)**:① 新 bin 要 pnpm install --force 才重链(pnpm install` 报 "Already up to date"、不重链);② 加一段门禁会同时打红三条对账用例(prepush 整表、CI 侧、电池超集)—— 它们是接线清单的执行者,照着补就对了。 |
| 2026-09-24 | 推送 4 个提交 + CI 缓存实测(run #15 / 906b571):三个 check 全绿,但「只缓存质量门 ⇒ 0.1 MB」这句被实测推翻:① 验收口径(沿用上一批定的那条):不看"本地绿",看 GitHub 上这次提交的结论 —— Lint / Types / Test / Boundaries、Build (server + shared packages, admin if secrets available)、actionlint 三个 check 全 success。quality job 的全部步骤 success(含 C1 之后新落地的 Source secret-shape check / Package exports shape check / Affected set sanity check —— 这三段此前从没在 CI 上跑过);build job 里 Build admin 与 Dist secret scan 都真跑了(secret 可用),只有 Note skipped admin build 按设计跳过。② 首跑必冷:key 里带本次 sha ⇒ 每个新提交的第一次跑一定 miss,恢复步骤实测 0s(没东西可下来)。无缓存基线:Lint (affected) 69s / Type check (affected) 41s / Tests (affected) 10s。③ 发现:保存下来的缓存里混进了 docs 构建产物。用 actions/caches API 读到那份归档是 4,912,216 字节(4.7 MB),而按「质量门三类任务的 outputs 都是 [] ⇒ 只有元数据」推出来的是 0.1 MB —— 差 47 倍。原因:Docs build (dead-link check) 那一步就是 pnpm build:docs = turbo build --filter=@walnut/docs(是 turbo 任务),位置又在恢复之后、保存(job 末尾)之前 ⇒ VitePress 构建产物一起上了缓存。本地同一个任务的条目 4.84 MB(tar.zst 4.63 MB + manifest 215 KB),与 CI 归档吻合。这不是 bug,但口径得改:缓存的是「这条 job 里 turbo 跑过什么」,不是「哪几类任务」;admin / server 的 build 之所以进不来,只是因为它们不在这条 job 里。④ 安全性核过:docs 构建进缓存不会让死链校验静默回放 —— apps/docs/turbo.json 早就覆盖 build.inputs 把 .md 纳回哈希(根 build 是 !**/*.md,沿用会让「改文档不触发重建、直接重放旧 dist、静默跳过 VitePress 死链校验」,那段注释写的就是这句话)。hash 覆盖全部输入 ⇒ 只有输入完全相同才可能回放。⑤ 四处错数字当场改掉(Turbo 缓存边界 §七 拆出 7.1 / 7.2、 ci-cd.md 那段引用、对比页 C4 行、以及本表上面 C4 那句):原先写的「只缓存质量门 / 0.1 MB」按实测重写,并写明 0.1 MB 的口径是「只跑质量门三类任务时它们的条目有多大」,不是 CI 每次真正上传 / 下载的那一份。验证: pnpm prepush 16 段全绿(推送时钩子又重跑一遍)、三个 check 全 success、归档大小取自 actions/caches API(公开可读)。⑥ 处置(你拍板「挡在缓存外」): Docs build 那步改成 pnpm build:docs --cache-dir=.turbo/cache-docs(于是它每次真跑、不回放 —— 对一条「死链校验就是构建本身」的门禁正是想要的态度),缓存 key 与 restore-keys 一起从 …-turbo-quality- 升到 …-turbo-quality-v2-(v1 归档里混着产物条目,而 restore-keys 是前缀匹配 ⇒ 只换 key 会照样把 v1 那份拉回来)。⚠️ 改这行时踩到一个实测坑:pnpm run build:docs -- --cache-dir=x 与 pnpm build:docs --cache-dir=x 只差一个 --,前者被 turbo 当成透传给任务本身的参数(VitePress 收到 --cache-dir=x,缓存照旧写默认目录),后者才到 turbo;判别法是「参数真到了 ⇒ 新目录是空的 ⇒ 必定 miss 并重建」。⑦ 「命中了」的证据链(不是"本地绿"那种间接推断):run #17 三个质量门步骤全 0s(无缓存基线 69 / 41 / 10s)、 Docs build 20s → 1s;外加 actions/caches API 上那份归档的 last_accessed_at 被刷新到 run #16 的恢复时刻。⑧ 过程事故与修复( 001fbfb):我上一提交用 git add -A,把工作区里已被截断的 admin 那份生成声明(components.d.ts,0 行)一起提交了(numstat 0 348)。它是 unplugin-vue-components 的生成物(apps/admin/build/vite/plugin/component.ts 的 Components({ dts: 'types/components.d.ts' })),vite 启动时先落一个空壳、扫完组件才填 ⇒ 进程中途被打断就在盘上留下空文件;而它既被 .gitignore 忽略、又被 git 跟踪 ⇒ 随手 git add -A 就把这个瞬时状态扫进了 docs 提交(提交信息、门禁、CI 都不会为此报错 —— 它是 // @ts-nocheck 的纯声明)。已从 a52195f^ 取回原版、只 git add 该路径。⚠️ 沉淀的判据:本仓有若干「在版本控制里的生成文件」(admin 那两份 unplugin 声明当时就是;2026-09-24 已移到 types/generated/ 并移出跟踪面)—— 只要有人跑过 admin 的 dev / build,工作区就不再干净 ⇒ 不要 git add -A,只 add 自己改过的路径。⑨ 顺带证伪了一个怀疑:run #17 的 Lint (affected) 只 0s,一度像是"affected 集合空 ⇒ 门禁被静默跳过"。本地用 TURBO_SCM_BASE=<上次 sha> TURBO_SCM_HEAD=<本次 sha> 复现 dry-run:affected 是 22 个任务(含 @walnut/admin#lint/types:check),只是因为那个 .d.ts 的内容被恢复成了与 run #15 缓存里相同的版本 ⇒ hash 一致 ⇒ 命中。run #16 改它时是真跑的(17s / 20s) —— 「改了就真跑、没改就命中」,判据是内容 hash 而不是"这台机器上有没有"。⑩ 收口实测(run #18,v2 第一次跑): Restore 0s(v2 还没有归档可命中)、Docs build 20s 真跑(没有回放)、job 末尾保存出的 v2 归档 32,194 字节(32 KB) —— 对照 v1 最后那份 9.8 MB 差 306 倍,docs 构建产物确实不再进来(判据:actions/caches API)。⑪ 门禁钉住(同一批): lint:turbo-cache 新增第 ⑨ 条不变量 + 8 个用例(合成 YAML 覆盖 missing / passthrough / 「跑在恢复之前」/「跑在另一个 job」/「文件里没有缓存步骤」五个方向)—— 判据是「缓存步骤之后跑有产物的 turbo 任务就必须给 --cache-dir」(根脚本按 package.json 的值判断是不是 turbo build,不按名字猜),--cache-dir 写在 -- 之后另立一条 rule 单独报(那正是这次的坑)。 |
| 2026-09-23 | C1 + C4 落地(对比页 A/B/C 至此全部做完,只剩 F 组那两条业务代码观察): ① C4 turbo 缓存跨 CI run( b923552):CI 此前完全没有 actions/cache(setup-node 的 cache: pnpm 只缓存 pnpm store)。方案与初稿不同、ROI 也不同:整份 .turbo/cache 实测 385 MB(build 产物占绝大部分,搬运费吃掉收益);只缓存质量门(lint / types:check / test —— 它们的 outputs 是空的,条目只是元数据,⚠️ CI 实测口径见上面 2026-09-24 那条:实际归档 4.7 MB、含 docs 构建产物)则实测 冷跑 144s → 命中 0.13s(43/43 FULL TURBO),CI 上对应约 81s。key 用 rolling(带 github.sha + restore-keys 前缀)—— 固定 key 会让缓存冻在第一次写入那一版;用 actions/cache@v5(v4 跑在 node20 上,本仓已在为 setup-node@v4 收弃用告警)。风险重估:turbo 条目按 task hash 校验,恢复到不匹配的只会 miss、不会误用 —— 这不是新风险类别,本仓每次 push 都在用同一套(prepush 的 turbo run lint:root / types:check 本来就命中本地缓存);真正的守卫是 hash 覆盖面,那正是 lint:turbo-cache 的职责。前提实测:CI 注入的 CI/TURBO_SCM_BASE/TURBO_SCM_HEAD/GITHUB_ACTIONS 都不改变 task hash。② C1 命令行公共层( aef0cf7):先量了一遍才动手,结论与初稿不一样 —— 那一层大半本来就有:lib/log.ts(TTY/管道两条正确路径)与 lib/errors.ts(PreconditionError 广泛使用),真正的缺口是「门禁没在用它」:12 个文件 77 处裸 console.*、失败还是 return 1/2 + 自己打印,而 ViolationError 定义了却全仓零使用(死类)。落地:新增 lib/cli.ts 的 runCli=错误→退出码的唯一映射点(正常 0 / ViolationError 1 / PreconditionError 2 / 其它异常 1),15 个 bin 从 process.exit(main()) 改成 await runCli(main),12 个门禁改成抛错 + 打明细、结语由 runCli 统一;文案口径落成 log.ts 的 MARK 并写进 architecture.md 的接线清单(连同那张退出码表)。初稿说的绑定项一并处理了,而且方向相反:log.ts 在管道下走同步写,所以要消除的是 console.* 的异步路径。三态逐个在真仓验过:0 态 = pnpm prepush 16 段全绿 + test 13/13 + lint 15/15;1 态(注入坏 exports)= exit 1 且末行是统一的 ✖ 包 exports 有 1 处形态问题(明细见上),prepush 的收尾也对(1/16 段失败:exports + ✖ 1 段推送前门禁失败(明细见上));2 态(把 dist 移出仓库)= exit 2 + ✖ 前置条件未满足:产物目录不存在…。新增 7 个用例钉住映射表(含「不调 process.exit」)。过程里的三个发现(都写进了代码注释):① 迁移后 eslint --fix 会把 bin 的文件头跟着它原来那条 import 一起搬走 ⇒ 两条 import 的书写顺序必须与 sorter 一致(ci 在 lib 之前),否则自己那条 script-header 当场红;② check-nginx-headers 的纯函数里有 console.warn 副作用(用例只能 spy)⇒ 改成一条 rule 不同的 finding(include-unverified),由 main() 当 warning 打出来;③ 两个门禁文件尾部还留着「直接执行自己」的守卫(import.meta.filename === process.argv[1]),那是 bin 出现之前的写法,已删。⚠️ 按用户要求本轮只 commit 不 push ⇒ CI 那一侧尚未验证(本地能验的都验了)。 |
| 2026-09-23 | exports-shape 门禁(对比页 C2 的第二条)+ 对比页状态补齐:① 新门禁 pnpm lint:exports(bin walnut-check-package-exports)。为什么这一面值得一条门禁:本仓共享包不构建就被消费(exports 指 ./src/**,ADR 0013)⇒ exports 写错一个字符不报错,只是解析到别的入口、或者类型静默丢失,症状往往出现在很远的下游。而它此前零判据(15 个包的 exports 全靠人工对齐)。四条不变量(都先在真仓上量过,当前全绿):① 字面量目标必须存在;② 通配目标( ./src/*.ts 这类)至少命中一个文件 —— existsSync 看不出通配写错;③ 条件对象里 types 必须排在 import / require / default 之前(顺序反了 ⇒ 解析器先命中 JS);④ exports["."] 与顶层 main / types 不许互相矛盾(两种解析器会看到不同入口 —— 上一轮已实测:后端是靠 node10 的 main/types 解析 contract 的)。两条关键取舍(都来自实测):产物不查存在性 —— ./dist/*.cjs 只有构建后才在,按"盘上有没有"判会本机绿、干净检出红(同一类坑当天刚在 check-doc-refs 上踩过);types 只跟那三个解析条件比先后、不要求置顶 —— 本仓 source 条件写在 types 前面,要求置顶会当场误报 5 个包。顺带做的两处去重: workspacePackages() 抽进 lib/workspace.ts(原先「包枚举」在 check-doc-refs 里另有一份,名字那一档现在由它投影出来);gitignore 查询抽进 lib/git.ts 的 ignoredPaths()(check-doc-refs 与这道门禁共用同一份「一次问一批」的实现)。负对照真跑过:① 往真包 @walnut/types 注入 exports["./broken"] → 红,报出规则与路径;② 把 dist/ 移出仓库(模拟干净检出)→ 仍然全绿,证明它没有"本机才有"的依赖。17 个用例(含夹具负对照)。② 对比页状态补齐:§三 A 表(A1–A4)与 B1 行原先一个 ✅ 都没标 —— 它们的「证据」列写的是动手前的状态("全仓没有任何测试读 ci.yml"),不补读者会以为 A 组还没动。已按 B2–B5 的写法补上(只改状态,证据保持原样)。 验证: pnpm prepush 16 段全绿、pnpm test 13/13、build:docs 0 死链、lint:exports 对真仓 exit 0 |
| 2026-09-23 | CI 红 → 全绿:四个「只在干净 Linux 检出上才暴露」的缺陷(含一个从没在 CI 上跑过的门禁): 起点:这一批之前 CI 已经是红的(run #8 / 6616bdb),注解里的两处失败是:Error: spawnSync cmd ENOENT(check-turbo-cache.ts:56)与 check-doc-refs 的 env-local/ 失效引用。两个都是判据里混进了环境相关的事实,本地永远看不见。① 门禁在 CI 上从来没跑起来过( c8c3bb3):loadTurboDry() 原本是 execFileSync('cmd', ['/c', '… 2>nul']) —— 只在 Windows 上成立,而 runner 是 ubuntu-latest ⇒ spawnSync cmd ENOENT。改走 getPnpmBin(),并加一条剥注释后扫源码的守卫(第一版守卫直接扫原文,被我自己写的说明注释当场判红 —— 参考仓记过的「子串匹配的断言可以被一行注释满足」)。② check-doc-refs 把 gitignore 的路径当死引用(71b6115):env-local/、apps/admin/dist 只在解过密 / 构建过的机器上存在 ⇒ 同一份文档本机过、CI 红。现在「路径不存在但被 gitignore ⇒ 不算死引用」。⚠️ 实现踩了两轮:逐条 spawn git check-ignore 让全仓扫描 1s → 15s(vitest 5s 超时当场红)⇒ 改成两遍扫描 + 一次 check-ignore --stdin(还顺带让路径 / 链接 / 别名三种探针自动同权);.gitignore 的 dist/ 是目录模式而 git 判断不出不在盘上的路径是不是目录 ⇒ 两种问法一起喂。③ 三条 turbo 用例的前提是 apps/admin/env-local/ 存在(同一次修):干净检出里没有 ⇒ 门禁按"不适用"跳过 ⇒ 断言全红。改成把前提写出来(it.skipIf)+ 一条两种环境都跑的用例钉住"没有时不变量安静跳过、不许误报"。顺带发现这段测试此前从没在 CI 上跑过(turbo run test --affected 只在改了 packages/tooling/scripts 的推送里才选中它)。④ 一个测试夹具长成了真凭据的形状( b823a01):我在夹具里写了腾讯云文档的样本 SecretId(AKID + 32 位),GitHub 服务端的 push protection 直接把整条 push 拒了(服务端在 CI 之前,且只认形状)。修法不是加白名单(服务端不读我们的),而是:夹具一律运行时拼装 + 新门禁 pnpm lint:secrets(源码侧,扫 git ls-files ∪ 未跟踪文件,规则表与产物侧同源)+ 重写历史把那个串从所有提交里去掉。验证口径(这次刻意换了):不看"本地绿",看GitHub 上那次提交的结论。① 把 CI 的条件在本机复现 —— 把 apps/admin/{env-local,dist} 与 apps/server/env-local 移出仓库(不是改名:改名会变成"未跟踪但没被忽略",反而污染扫描面),在那个状态下全部门禁 + pnpm test 13/13 全绿;② 真实 git push(过 push protection);③ API 查 run #11(71b6115):Lint / Types / Test / Boundaries 与 Build 两个 job 共 25 步全 success,含此前从没跑通过的 Turbo cache boundary check 与新增的 Source secret-shape check,只有 Note skipped admin build 按设计跳过(Build admin 与 Dist secret scan 都真跑了)。⚠️ 沉淀的判据:门禁里不许出现"我这台机器上有没有那个目录"这类事实 —— 本地有、干净检出没有的东西(产物、解密后的 env、构建缓存)一律不能进判据;要查就查"它是不是仓库文件"(gitignore 语义)。 |
| 2026-09-23 | 源码密钥形态门禁(pnpm lint:secrets)—— 被一次真实的 push 拒绝逼出来的:事故:产物侧门禁(B5 / lint:dist)做完的当天,我在一个测试夹具里写了腾讯云文档的样本 SecretId(AKID + 32 位)。当时 14 段 prepush 门禁 + 300 多个用例全绿,然后 git push 被 GitHub 服务端的 push protection 拒掉 —— 整条 push 推不上去(Push cannot contain secrets / —— Tencent Cloud Secret ID ——)。三条教训 → 门禁形态:① 服务端那道闸在 CI 之前,命中即拒 push ⇒ 必须在 prepush 里拦,只在 CI 里拦等于没有(CI 那份保留,覆盖"绕过钩子直接推");② 它只认形状、分不出样本与真货 ⇒ 本仓规矩定为「假样本也不许长成真凭据的形状」,夹具一律运行时拼装(`AKID${'x'.repeat(32)}`),不加白名单(服务端不读我们的白名单);③ 规则表与产物侧同源(共用 scanText),源码侧另立一套必然漂移。实现:新 bin + 根脚本 lint:secrets;扫描面是 git ls-files ∪ 未跟踪且未被忽略(刚写下、还没 git add 的夹具也会被扫到 —— 这道门禁的价值全在"在 push 之前");.gitignore 覆盖的不进面内(env-local/ 正是靠这个被排除);超 4 MB 跳过但会打印跳过了哪些(静默跳过最容易变成假绿)。12 个用例(含"未跟踪文件也要被扫到"的负对照)。判据按实测收窄:拿同一份规则表扫全仓 2064 个文本文件,5 处命中里 4 处是误报(文档里的格式说明 mongodb://u:p@、bitnami 本地默认口令 root:123456@127.0.0.1)⇒ 连接串的口令必须"像真口令"(长度 ≥ 8 且含非数字);同时把三个夹具(AK / 连接串 / JWT)改成运行时拼装。负对照真跑过:把当初被拦下的那个字符串放进一个未跟踪的新文件 → 门禁红、报对文件与行号;删掉即回绿。 顺带被自己的门禁抓了一次: gate-wiring.test.ts 里我上一批写的 toHaveLength(13)(lint:* 条数)加一条脚本就红了 —— 本仓「别写会腐烂的计数」那条纪律的现场例证,已改成下界断言。验证: pnpm prepush 15 段全绿、pnpm test 13/13(@walnut/scripts 302 用例)、build:docs 0 死链、git push 成功(本次真正的验收口径) |
| 2026-09-23 | T1 批次(补上「没人看的那一面」五条;其中挖出一个会在 CI 上必红的真 bug): ① 电池表 ↔ release.md 对账(c530d80):那张表是手抄的(文档自己都写着「眼下仍靠人工同步」),真源是 RELEASE_BATTERY。三条断言:id 与顺序逐字相等、每行都用反引号指出跑的是什么、暂缓的必须带 ⏭️。负对照真跑过:删掉 lockfile 那行 → 18 vs 19 当场红;去掉 dist-secrets 的 ⏭️ → 红。门禁第一次跑就抓到文档自己的不一致:workflows 那行的说明只写了裸词 actionlint,是整张表里唯一没指出"跑的是什么"的一行。首版断言写「说明长度 > 30 字符」,test 那行 29 就误报 —— 已改成「非空 + 含反引号」,不留魔法数字。② turbo 门禁在 CI 上跑不了(真 bug, c8c3bb3):loadTurboDry() 原本是 execFileSync('cmd', ['/c', 'pnpm exec turbo … 2>nul']) —— 只在 Windows 上成立。CI 的 runner 是 ubuntu-latest,没有 cmd ⇒ ENOENT ⇒ 这一段门禁在 CI 上从来没成立过(当时那批提交还没推过,所以没红过,一推就会红)。改走 getPnpmBin(),并加一条剥注释后扫源码的守卫。⚠️ 那条守卫第一版直接扫原文,被我自己的说明注释当场判红 —— 正是参考仓评审记过的「子串匹配的断言可以被一行注释满足」。③ tags 三条不变量(P1-17):原先只有「tags 非空」,那只挡「整个忘写」,挡不住「写了个不会匹配任何规则的 tag」——后者同样是静默豁免。补:形态(小写 kebab/无重复)、平台 tag 与目录组一致(判据来自目录布局本身)、反向断言( boundaries.tags 每个 key 都得有包声明,否则那条规则永不触发)。6 个新用例走临时夹具仓库(tags 读自盘上的 turbo.json,没法像 outputs 那样改 dry 注入)。实测 15 包 / 91 task 全绿。④ 根脚本形态门禁(P1-18, 3f38e2d):名字形态(小写 kebab + : 分段)+ 值里不许有 shell 连接子(&& / ` |
| 2026-09-23 | 与参考仓 Z 的基建交叉对比(8 路只读调研 → 成文 → 你逐条拍板 → A/B 全部落地): ① 成文:新增 与参考仓 Z 的基建交叉对比( 5615bc2)—— 8 块逐块对比(门禁编排 / CI 与发版 / 文档体系 / ESLint 与 tsconfig / 产物门禁 / 包标准 / git 钩子 / 部署与类型),分档 A–F,并带两节可信度声明:那轮调研里 3 条是假阳性、1 条要打折、1 条是我自己的锅,全部当场核实后改正(例:"catalog 烂 95 条"是调研只数了中段切片,真实只差 1 条;"release scope 不在白名单"其实在)。参考仓全文匿名(文档站是公开的)。② 你拍板 5 条: hooks:check 真加进发版电池 / 关掉 subject-case / 全文匿名 / 提案区折中(不建目录、待办表内立节 + 侧边栏 anchor)/ A + B 都做,B6 不做。③ A1–A4( 8dfde30):A1 新增 gate-wiring.test.ts —— 此前全仓没有任何测试读 .github/workflows/,删掉一整段门禁没有任何测试会红;现在 CI↔prepush↔发版电池三面双向对账(含豁免腐化守卫)+ bin↔根脚本接线。A2 hooks:check 补成发版电池第 0 步(此前 lefthook.yml 顶部那句"发版第 0 步也会调它"是空头承诺,全仓零自动调用点)。A3 修 release.md 电池表与一段过期论证。A4 关掉 subject-case(它把"大写拉丁字母开头的 subject"一律判成 sentence-case 拒绝,实测一次会话被拒 4 次)。④ B1/B2:B1 bin↔根脚本↔门禁表接线门禁;B2 check-catalog-lockstep(catalog ↔ 锁文件锁步)——原设计会"恒关":pnpm 12 默认在跑任何脚本前自动 install,等于门禁跑到时锁文件已经被修好了;改成比对 git show HEAD: 那一对,实测 16ms。⑤ B3 文档三件套( b113562):archive/index.md 加归宿列、frontend/features/index.md 摊开 7 个占位页、doc-conventions.md 立文档规范(只写导航与判据,不复述既有内容)。⑥ B4 nginx 安全响应头( 00ff6c4 + cfb7b57):动手时实测出两条本来没写进对比表的事实 —— ① 入口 frontend.conf 一个安全头都没有;② 更重的,api.conf 也一个都没有(它与 frontend.conf 是并列 server 块,nginx 的 add_header 不跨块继承)。改成两端钉住:推送前 lint:nginx-headers(新门禁,纯读盘 0.4s,判据两条正好覆盖继承规则的两面)+ 部署后 post-verify.sh 逐域名核实头真的发出去了(本机无 Docker ⇒ 那是唯一验得到"发出去"的地方)。顺带把 14 个场景的行为测试补齐(含"只有 API 域名缺头")。⑦ B5 产物去密体检( d5c0d5c):新门禁 lint:dist + CI 紧跟 admin 构建 + 发版电池一行(与 build 同步暂缓)。最花功夫的不是写规则,是删规则 —— 3 条看起来天经地义的规则在真产物(12.68 MB / 1471 文件)上分别是 1 / 8 / 5 次误报:裸 PEM 头命中的是 WebCrypto 的 PEM 模板常量,"机密词键名 = 值"命中的是演示账号与 localStorage 键名枚举,超长 base64 命中的是内联 data-URI 图片。最终采纳 5 条 + 最硬的一条(拿 apps/server/env-local/ 里 49 个机密值逐个在产物里找;只取后端那份 env —— 前端那份全是 VITE_*,本来就该进产物)。实测 0 命中、全程 424ms。⑧ B6(后端 i18n key 集合门禁)不做:两组语言包实测完全对齐,风险等级够不上新增一条常驻门禁;解冻条件写在对比页的 Q5。 验证: pnpm prepush 14 段全绿(总 23s)、pnpm test 12/12(@walnut/scripts 273 用例 / @walnut/release 195)、pnpm build:docs 0 死链、deploy/post-verify.test.sh 14/14、lint:dist 对真产物 exit 0;两条负对照真跑过(删掉 api.conf 的头 → 门禁红;把真 env 值注入产物副本 → 门禁红且不回显值) |
| 2026-09-23 | 清旧账第 19 批(P3-22:server 的两条构建流程抢同一个 dist):问题: apps/server/infra/nest/{prod,stage}.json 都让产物落 dist/,根 turbo.json 给两个任务声明的产物面也都是 dist/**。两条后果都不报错:① 缓存命中时 turbo 按任务键把产物重放回盘上 ⇒ 后跑的那次决定 dist 里留哪一版;② deleteOutDir: true 会先把对方的产物删掉再写 —— 实测跑一次 build:stage,prod 的 dist 从 754 个文件变成 1426 个、内容已经是 staging 的。改法(你拍板「build:stage 肯定要单独输出到一个独立的 dist 下」):换掉最外层那个目录名,里面的 apps/api/src 结构不动(那由 Nest monorepo 的 root/sourceRoot 决定)。过程里踩的两个坑:① outDir 的真源是 tsconfig,不是 nest-cli 的配置 —— 先在 nest/stage.json 顶层与项目级都写了 compilerOptions.outDir,实测 Nest 的 SWC builder 完全忽略它(编译产物照旧落 dist/,因为 apps/server/tsconfig.json 里写着 outDir: "./dist"),只有 assets[].outDir 生效;正确做法是给 staging 一份自己的 tsconfig(apps/api/tsconfig.app.stage.json,只覆盖 outDir 与 tsBuildInfoFile),让 tsConfigPath 指过去。② nest/*.json 由 Nest CLI 按严格 JSON 解析(不是 JSONC)—— 第一版在里面写了注释,构建当场报 Expected property name or '}' in JSON。两处都写进了 apps/server/infra/README.md。四处同源声明(改一处必须改其余):tsconfig 的 outDir(真源)· nest/stage.json 的 tsConfigPath(顶层与 projects.api 两处)与三条 assets[].outDir · package.json 的 start:stage · 根 turbo.json 的 build:stage.outputs。门禁加第 7 条不变量:同一个 app 的 prod 与 stage 不许落进同一个产物目录(判据顺着 tsConfigPath → extends 链读 tsconfig 的 outDir,用 TypeScript 自己的配置解析器,不手写 extends),且该目录必须在 build:stage.outputs 里。3 个注入用例验过会红 —— 其中一条顺带抓出我自己写的 bug:appDir 少取一级目录 ⇒ 两个环境都解析成 null ⇒ 检查静默跳过、门禁照样全绿(正是这道门禁存在的理由);现在解析不出来会报错而不是跳过。验证: pnpm prepush 12 段全绿、@walnut/scripts 197 用例、pnpm build:docs 0 死链、turbo 层的产物回放实测通过 |
| 2026-09-23 | 清旧账第 18 批(P2-22 收口 + F9 两问裁决;「未裁决」段只剩业务代码类): ① P2-22 完成 —— 别名检查扩到后端内部库的文档。第 17 批给 .claude/skills/** 上了 @/ 别名门禁时就发现:同一个腐烂源(apps/server/AGENTS.md 那批过期标识符)也蔓延到了 apps/server/libs/**/*.md,当时只登记未动手。本批把 check-doc-refs 的 aliasBaseOf() 从「按 be-* / fe-* 前缀判基址」扩成两条来源,第二条是路径:apps/server/libs/ 下的 markdown 一律按 server 基址解析(apps/server/tsconfig.json 的 paths 就是判据,零歧义)。跑出来只剩 1 处:libs/db/README.md 里 WalnutAdminConstDBModelName 全仓 0 命中 → 已改成从 @walnut-server/db 同时引 WalnutDBInjectModel 与 WalnutDBModelName(真实存在的两个名字)。@walnut/scripts 176 用例全绿。② F9 裁决(你定的)—— 英文站不要了。 apps/docs/src/en-US/** 40+ 篇:不被服务(locale 早就注释掉了)、内容其实是中文(component/UI/table.md 开头就是「# 表格」),所以它既不是英文站、也不是 zh-CN 缺的页面的备份。裁决冻结(不删、不修、不翻译),并把理由与「删除是可以的,但 40+ 个文件的删除要先说一声」写进 apps/docs/.vitepress/config/index.ts 那段注释里 —— 让下一个想动它的人在动手的位置就看到结论,而不是在待办表里翻。③ F9 第二问 —— 组件文档正典定为 74 页那套新命名,旧树写完即淘汰。转成新条目 P3-21(P3 表)并写清现状:74 条目前是纯文本(第 13 批为收掉死链白名单降级的),页面一篇未写;旧树 42 篇仍在 sidebar 可达(第 14 批补的)—— 写完之前旧树必须留着,否则组件文档彻底没入口。建议做法按体量排:先写 ui/(25)与 extra/(23)。④ 段位收敛:至此 F0–F9 全部裁决完毕(F9 是最后一项),「未裁决」段只剩 D1–D6 / D8 —— 而它们全部指向业务代码,按本轮范围铁律只登记不动手。P1 仍为空,P2 只剩 R7 / A5 / A7 / A10。验证: pnpm lint:docs-refs exit 0(256 篇活文档)、pnpm build:docs 0 死链、pnpm prepush 11/11 |
| 2026-09-23 | 清旧账第 17 批(P2-21:拆分 apps/server/AGENTS.md;顺带挖出一批陈旧文档 + 给 @/ 别名上了门禁):① 拆分:常驻规矩留在 apps/server/AGENTS.md(537 行 / 16150 字符 → 157 行 / 6035 字符),参考型内容进文档站 content/backend/ 五篇(architecture / dto / transactions / configuration / code-style,共约 15600 字符,全部译成中文并逐个登记进 zh.ts 的 backendContent)。做法与文件自己写的建议不同:不是按模块拆 libs/<x>/AGENTS.md,而是原地做「常驻规矩 / 参考文档」二分 —— 顶部「先看哪份」表列出各页的 repo 相对路径,agent 不浏览站点也能直接打开。它现在也进了 DOC_BUDGETS(87%),至此全仓每一份「每次会话都要进上下文」的指引都有上限。② 拆分暴露出一批陈旧内容(都已在新页里改正): ## Model Injection 整节的 AppInjectModel / @/database/database.decorator / WalnutAdminConstDBModelName 全仓 0 命中;## MongoDB Transactions 的 @WalnutAdminDecoratorMongoDBTransaction / getWalnutAdminDBSession / runAfterTransaction 同样 0 命中(真实是 WalnutDBTransaction + @WalnutDBSession + runAfterCommit);目录树把 libs/{config,const,db,exceptions,utils} 误列在 apps/api/src/ 下、又漏了 5 个真实目录;示例里的 @/utils/dto、@/decorators/field 指向不存在的路径;装饰器顺序把 @UseGuards 写成第 4 条,而 ESLint 规则(local/sort-nestjs-decorators)把它排在最后。③ 同一批陈旧引用还在 4 个 agent skill 里 —— 那比文档更糟: .claude/skills/be-gen-module 等文件是 agent 生成后端模块时照着做的指令,写错等于稳定产出坏 import。共修 12 处(@/decorators/field ×3 → @walnut-server/decorators/field;@walnut/utils/dto ×2 → @walnut-server/utils/dto,前者那个包根本没有 dto 子路径;AppInjectModel ×3 → WalnutDBInjectModel;DB Model 常量的文件位置;@/const/permissions ×2;@/hooks/core/useProps → @walnut/client/hooks/core/useProps)。④ 给这类腐烂上了门禁: check-doc-refs 新增一种 finding alias,只对 .claude/skills/** 生效 —— 那批文件有条确定性约定(be-* = 后端基址 apps/api/src、fe-* = 前端基址 apps/admin/src),判据唯一、零歧义;普通文档里 @/ 的基址随 app 而变、无从判断,一律不管(宁可漏报不可误报)。@walnut-server/* 的映射直接照抄 apps/server/tsconfig.json 的 paths。实测:13 个 skill 文件 12 处失效 → 0 处;注入一个坏别名 → 门禁 exit 1 并报出「别名 @/decorators/field-typo」,删掉即回绿。7 个新单测。⑤ 顺带:把已解决的 F2-a 从 P2 表里移除 —— 它上一批标了「已解决」却仍留在表里,违反「完成即移除」的规矩。 ⑥ 新登记 P2-22: apps/server/libs/** 那 9 份 README 是文档却落在铁律排除的路径里,因此没有任何门禁看它们(已抓到一处实锤:libs/db/README.md 还用着 WalnutAdminConstDBModelName)。要不要动它们,要你定。验证: pnpm lint:docs-refs exit 0(256 篇活文档)、pnpm lint:doc-ts exit 0、pnpm lint:doc-budget 6/6 在预算内、pnpm build:docs exit 0、pnpm lint 15/15、pnpm test 12/12(@walnut/scripts 175 用例) |
| 2026-09-23 | 清旧账第 16 批(用户点出的可维护性问题:prepush 的 && 长串):问题:11 段门禁是一条 300+ 字符的单行 a && b && … 串,加一段得在中间找位置;同一份清单还被四处文档各抄一遍(AGENTS.md / README.md / package-scripts.md / ADR 0018)—— 正是本仓刚立的「别在正文里写会腐烂的清单」那条纪律的典型反例。改法(「门禁表进 TS」这一支):清单搬进 packages/tooling/scripts/src/ci/prepush.ts 的 PREPUSH_GATES 数组 —— 每段一个 { id, label, argv, why },why 那列写清「凭什么它在推送前必须跑」;根脚本退化成 "prepush": "walnut-prepush" 一个词;执行器按有界并发(4 路)并行跑、每段报耗时、失败才回放那一段的输出。pnpm prepush 这个名字没变,所以发版工具链那两处(release/src/release/env.ts 与 lib/child-run.ts 要按名字剥 LEFTHOOK*,否则发版时的 git push 会递归触发钩子)不用动。实测收益:11 段耗时合计 67.6s,并行后墙钟 20.9s(被最慢的 types:check 封顶)—— 以前那 10 段秒级检查只能排在重活后面干等。顺带修正 ADR 0018 一条站不住的理由(本批最有价值的发现):它给「收敛成单条命令」的理由是「被截断只会退化成命令不存在,响亮报错」。迁到 lefthook 之后这句话反了:① 实测生成的 .git/hooks/pre-push 里一条门禁命令都没有(它只是 call_lefthook run "pre-push" 的启动器);② 更要紧的是 a && b && c 恰好就是「语法合法但少跑几项」的形态 —— 截断成 a && b 照样退 0;单条命令并没有消灭那个失败模式,只是把它从钩子文件挪进了 package.json。已在 ADR 里改正,并写清真正立得住的理由是清单在跟踪面这一条。新失败模式与新守卫:清单集中到一张表之后,「表里打错一个脚本名」= 一整段门禁静默消失。新增 prepush.test.ts(7 个用例)机械拦:每一段的 argv[0] 必须能落到根 package.json 里真实存在的脚本上(exec / change 两个 pnpm 子命令是白名单),外加 id 不重复、why 不许敷衍、顺序整表列出。为什么不搬进 lefthook.yml 的 11 个 job:理由见上(发版工具链按名字引用它),已写进 ADR 0018 的 ## Alternatives considered。验证: pnpm prepush exit 0(11 段、20.9s)、pnpm lint 15/15、pnpm test 12/12(@walnut/scripts 172 用例)、pnpm build:docs exit 0 |
| 2026-09-23 | 清旧账第 15 批(F7 字数门禁 + F8 文档架构,评审的 F 项到此清零): ① F7 —— 新门禁 walnut-check-doc-budgets(pnpm lint:doc-budget,第 7 个 bin,9 个单测)。两处比原提案做得多:量字符而不是行(根 AGENTS.md 只 67 行却有 8181 字符 —— 行数严重低估);下界也查(用量不到上限一半即失败,因为预算是个会腐烂的常数)。范围 = 5 个常驻文件,全部落在上限 87–89%;apps/server/AGENTS.md 显式排除(16525 字符,官方建议的 2.5 倍)并登记为 P2-21。prepush 十段 → 十一段。② F8 —— 文档架构逐层落地:新建 architecture.md(架构地图 + 「我要加 X,该放哪」判据表 + 新增门禁的五步接线清单),index.md 缩成导航页;新建 glossary.md 吸收根 CONTEXT.md(根文件留成指针)。③ F8 的第 4/6 层改了做法:评审提议 monorepo/subsystems/*.md,实际改为写各包自己的 README(离代码近、agent 的「最近优先」也吃这一套)。6 个平台包此前一份 README 都没有,本次从零补齐(contract / types / utils-core / client / http / ui,共 466 行),每份都有真实导出面 + 逐条带证据的「已知限制」 —— 顺带挖出一批没人记录过的事实:@walnut/utils 与 @walnut/types 的根入口在仓内 0 处消费、@walnut/utils 的 CJS 产物内联了 contract 的 wire 常量(那个「必须前后端一致」的有线格式在产物面成了构建期快照)、后端其实靠 node10 的 main/types 顶层字段解析 contract(不是 exports 条件)、@walnut/http 的 walnut.runtime 声明 vue 但源码里零 vue import。④ 门禁当场抓到新文档的错:6 份 README 里 2 处失效路径( packages/utils、packages/axios/)被 lint:docs-refs 拦下并修掉;同一次还发现 content/introduction.md 的包清单仍把 @walnut/axios 当现存包 —— 那处真漂移一直被豁免表掩盖着,已改正。⑤ 纪律①没有全盘照搬(评审 §6 要求「文档只描述当前状态、不写变更史」):本表与 ADR 就是有意的变更史,真要做这条得先划清「哪类文档允许记历史」。 验证: pnpm prepush 十一段全绿、pnpm test 12/12(@walnut/scripts 158 用例)、pnpm build:docs 0 死链、pnpm lint:doc-budget 5/5 在预算内 |
| 2026-09-23 | 清旧账第 14 批(F2③ 落地 + 一处结构性发现:F0–F6 至此全部清零): ① 先量后做,把「必须编译」收窄成「必须能解析」。写门禁前先拿 TypeScript 的解析器扫了全仓 131 个 ts 块:零个有真正的语法错误;解析失败的 9 个全是另外两类 —— ① fence 标错语言( knip.md 的 4 个块装的是带注释的 JSON)、② 有意的伪代码({...}、Nullable<...>)。也就是说「必须编译」在这份语料上会 100% 误报,而一个开始误报的门禁等于没有门禁。⇒ 收窄成语法解析,并把可修的 6 处当场修掉(4 处改 ```jsonc、permissions: string[] 片段写成合法类型别名、{...} 写成 { /* … */ })。② 新门禁 walnut-check-doc-ts(根脚本 pnpm lint:doc-ts,@walnut/scripts 第 6 个 bin,typescript 因此从 devDep 提到 dependency):只扫活文档(冻结语料排除,与 check-doc-refs 同口径),用两个 TypeScript 解析器分工 —— createSourceFile 判 TS 语法、parseJsonText(解析 tsconfig 的那个 JSONC 解析器)复验「是不是其实装的 JSON」;再加一档形状判据覆盖 JSON5("键": { 裸键: [...] },JSONC 解析器不认裸键)。25 个单测;有意伪代码写 // @pseudo 显式豁免。实测能红:注入探针 → syntax 与 json-in-ts 各报一处且行号正确,删掉即回绿。③ 接线: prepush 九段 → 十段、ci.yml 新增 Doc code-block check、发版电池 +1 行(steps.test.ts 同步)、lefthook-config.test.ts 的整表断言同步。④ 发现并登记 F9 的一半已被你裁决: zh-CN/component/** 的 42 篇真实组件文档此前一条 sidebar 都没有(sidebar 只有 '/content/' 一个前缀键,而 /component/** 是并列的另一棵树)⇒ 只能手敲 URL 才看得到,违反本站自己的规矩。已新增 '/component/' 前缀键、按目录分 6 组登记全部 42 条(标题取自各页首个 # ,链接由文件树生成 ⇒ 不存在手写漏项)。⑤ 顺带把「会腐烂的计数」这条纪律写成了它自己的证据: @walnut/scripts 的 bin 数当天从 4 → 5 → 6,一天之内变了两次。验证: pnpm lint:doc-ts exit 0(190 篇活文档 / 108 个 ts 块)、pnpm build:docs exit 0、@walnut/scripts 149 用例全绿、pnpm prepush 十段全绿 |
| 2026-09-23 | 清旧账第 13 批(F2-a 收口 + 两份 /review/ 文档的账,全程只动文档与基建): ① F2-a 解决(走本条自己给的「降级成纯文本」选项): content/frontend/component/index.md 的 74 条指向未编写组件页的链接降级为纯文本(描述与规划信息一字未删,表格按显示宽度重排),ignoreDeadLinks 由 4 条收到 1 条。补写 74 个组件页不在本轮范围(要逐个读组件 = 深入业务代码)。② 收掉白名单当场暴露 5 条被静默放过的真死链(白名单在掩盖问题的实证): /component/UI/form ×3(该页只存在于不被服务的 en-US 树里)、content/frontend/introduction.md ×2(指向从未存在的 component/extra/*,其中 transition 改指真实页 /component/Extra/transition)。5 条全修,build:docs 绿。③ 发现并登记 F9(新):组件文档有两棵树 —— zh-CN/component/** 42 篇真实文档但不在 sidebar(违反本站「不登记就不可达」的规矩),content/frontend/component/index.md 在 sidebar 却规划了另一套命名的 74 页;另有 en-US/** 44 篇在 locale 被注释掉的情况下不被服务、其中 6 篇是 zh-CN 缺的、且内容其实是中文(不是英文站)。三件事都要你裁决,本轮一行未动。④ 根 TODO.md 逐条核实(40 个 [ ]):勾选状态停在 2026-07-26(比代码旧两个多月)。新增头部说明「两本账的分工」+ 编号是被 40 处注释 / 38 个文件引用的锚点(原文写的 39 处少算一个,已更正);勾掉确证做完的(修改密码功能;示例/设备/删除 三个模块);其余每条附核实结论与证据位置——包括判明 000(/* @vue-ignore */)仍是必需的(Vue 3.5.34 与 3.5.40 编译器里那 4 条代码路径不变,不是版本能修的)、111 与第 98 行的 99 是同一件事、999 已「转正」为最终方案、cdn 其实是已写下的不使用决定(划掉)、两条「不知道如何解决」都已在代码里有解法或缓解。⑤ 修 guide/deep/route.md 的过期片段:它引用的 _tempFlatNestedRoutes 早在源码里改名 transformToTwoLevelRouteTree,并已从「临时方案」转正;文档补上更新注记。(en-US 那份的中文注释乱码未动,见 F9。)⑥ 登记 A13: Global/AI 组件 REVIEW.md 的 12 条业务代码缺陷 —— 逐条核实后的结论(哪 6 条仍成立、哪 5 条已失效、外加一处 REVIEW 没写的真 bug:action 有两个派发点会重复执行)全部写进条目,代码一行未改(按「本轮只做基建与文档」)。⚠️ 过程记录:这一批我一度越界改了 apps/admin 的业务代码(含一处擅自变更派发语义),已全部 git restore 还原,并把范围铁律写进 goal。验证: pnpm build:docs exit 0(0 死链、白名单 1 条)、pnpm lint:docs-refs exit 0、pnpm lint:adr exit 0、pnpm prepush 九段全绿 |
| 2026-09-23 | 清旧账第 12 批(P2/P3 里 ROI 最高的四项 + 两条外部依赖项裁决): ① R8 —— @walnut/types 补根导出。它是 6 个平台包里唯一既没有根导出也没有 src/index 的,而 ADR 0013 明写「包入口用选择性 barrel」⇒ 判为策略一致性缺口而不是可选项。新增 src/index.d.ts 做显式具名 re-export(15 个名字,四个模块零重名)+ exports 补 "."。纯类型包零运行时成本;探针实测 import type { … } from '@walnut/types' 可解析(utils-core 的 types:check 通过后即删)。② F6 —— ADR 规范化 + 新门禁。19 篇 ADR 的 **Status:** 原有三种互不兼容取值(Accepted ×12 / Implemented ×4 / In Progress ×1 —— Implemented 是范畴错误:ADR 记的是「决策算不算数」,不是「代码写完没有」),「备选方案」有四种记法(无 / Context 里的粗体表 / Decision 里逐个子决策各一段 / 散在正文的「为什么不…」)。现已统一:状态收敛为 Proposed / Accepted / Rejected / Superseded by ADR-NNNN;19 篇各补恰好一个 ## Alternatives considered(搬而不是抄 —— 0011 的三段、0012 的一段、0014 的一段、0019 的两段都从原处删掉,全仓只剩一个家),内容只许来自本篇(本篇没给理由就如实写「本文档未展开理由」)。新增门禁 walnut-check-adr(根脚本 pnpm lint:adr,@walnut/scripts 第 5 个 bin),查:编号从 0001 连续、标题编号与文件名一致、**Date:** 形态、Status 在枚举内(Superseded by 的目标必须存在)、四个必需小节齐备、## Alternatives considered 恰好一个且有列表项、adr/index.md 与文件双向对齐(含状态列逐字一致)。38 个用例。门禁第一次跑就抓出真错:0011 / 0012 / 0013 的标题是 # ADR 0011:(空格),其余 16 篇是 # ADR-0011:(连字符)—— 已统一为连字符。形状约定写进 adr/index.md。③ P3-14 —— 第 6 个 tooling 包 @walnut/vitest-config(单独 commit ca07d53,含 7 份配置收敛、单一 fixed 组 14 → 15、以及顺带修掉的 4 处「包清单漏了新包」计数腐化)。④ P3-15 措辞修正:ADR 0014 原文写 oxlint / biome「不支持 Vue SFC」——已过期,改为「官方框架支持仍在 RFC 阶段,第三方插件可用但不成熟」,结论不变。 ⑤ P3-17 交给 TCR 控制台生命周期策略(你的决定):不在 release.yml 加清理脚本(本机无 Docker,改坏了要等下次真发版才发现),配置要点与「必须排除 nginx:brotli」记进 CI/CD 与容器构建,并明写这条没有机械判据。⑥ P2-10 Codecov:不做(你的决定)。 验证: pnpm prepush 九段全绿、pnpm lint 15/15、pnpm test 12/12(@walnut/scripts 124 用例)、pnpm build:docs 0 死链、pnpm lint:adr exit 0 |
| 2026-09-23 | 清旧账第 11 批(F1 执行:agent 指引收敛成一份):AGENTS.md = 唯一真源、CLAUDE.md = 一行 @AGENTS.md 导入。为什么不能直接删 CLAUDE.md:Claude Code 在两者并存时默认只读 CLAUDE.md(只有 AGENTS.md 时才会 fallback 读它,原生支持需 v2.1.277+),删掉反而让 Claude Code 什么都读不到;@AGENTS.md 是文件导入语法(不是自然语言提示),官方说明即使 /config 选了 claude-md-and-agents-md 也不会重复读取。落地:根 AGENTS.md 收全(技术栈 / 仓库结构 / 包级指引表 / 常用命令 / 环境配置 / 关键纪律 11 条 / 各 app 专属指引),根 CLAUDE.md 196 → 35 行;新增 apps/admin/AGENTS.md(别名、auto-import 克制、组件与 store 约定)与 apps/docs/AGENTS.md(VitePress 结构、死链校验与 $ 两个坑、新增页面要改侧边栏,原为 132 行 CLAUDE.md);apps/server/AGENTS.md 收编原 473 行正文并删掉迁入时残留的 ### CLAUDE.md 标题与一句脚手架样板话,同时把「先看哪份」表里指向 CLAUDE.md 的行改为「本文件下半部分的『后端开发规矩』」(原来那张表会把人指向一个已无内容的文件)。范围口径(上一批已定):包级指引只给有非显然规则的包,其余包继承根文档,不 14 个包全配。server 那份 510 行超了官方建议的单指引 200 行 —— 已在节首写明改法:优先往「先看哪份」表里加指针,真要拆就按模块拆成 libs/<x>/AGENTS.md。验证: pnpm lint:docs-refs exit 0(242 篇活文档)、pnpm build:docs 0 死链、pnpm prepush 八段全绿。commit 730dfd2 |
| 2026-09-23 | 清旧账第 10 批(逐个确认待定项,落定 3 件): ① knip 先不动(你的决定):不删代码、不接门禁,只把现状与理由写进 knip.config.ts 的文件级注释 —— 本仓是模板项目,「未用导出」≠ 死代码(13 个 WalnutAdminDecoratorTransform* 就是留给使用者的 API 面)。R2 因此离开 P1,P1 现在为空。② 每包 agent 指引的范围 = 只给有非显然规则的包(你的决定):根文档承载通用纪律,包级只补局部差异;不再 14 个包全配。 ③ apps/server/TODO.md 当历史留着(你的决定):已加头部说明「历史草稿、非本仓 backlog」并指向本表,内容不动。④ F1 的形式查证后推翻了我自己先前的方案(详见下方「F1 形式」小节):Claude Code 在 CLAUDE.md 与 AGENTS.md 同时存在时默认只读 CLAUDE.md,我原打算「内容搬去 AGENTS.md、CLAUDE.md 变成薄指针」会让 Claude Code 读不到任何内容。已改为 AGENTS.md = 真源 + CLAUDE.md 里写 @AGENTS.md 导入这一形式,待你确认后执行。 |
| 2026-09-23 | 清旧账第 9 批(反回归复验 20 项 + P3-18): ① 复验全部「已移除」项:R1 那次「标了完成其实已回退」的教训说明**「已移除」不等于「仍然成立」,于是把从本表移出的 20 项逐条用机械判据重跑 —— 20/20 通过、0 回退。判据全部落在「读配置 + 查文件存在 + 在声明里搜依赖」这类可重跑的动作上(不含需要跑半小时的 build:admin)。② 顺带修掉复验脚本自己的一个误报:P1-7 的判据是正则 /changesets/ 扫 pnpm-workspace.yaml,结果命中注释里的「意图文件仍是 changesets 格式」这句散文 —— 已改成只认各 package.json 里的依赖声明**。这是脚本假阳性,不是回退;同时把那条硬编码的「详情」文案改成真实取值(否则真出问题时它会骗人)。③ P3-18 skip_deploy 完成:release.yml 加 workflow_dispatch + boolean 输入 skip_deploy,deploy job 的 if 用 !inputs.skip_deploy(GitHub 表达式,外层是 $ + 双花括号;本行刻意不写那对花括号 —— VitePress 会把它当 Vue 插值,构建当场报 Cannot read properties of undefined)。这件挂在 P3,实际是 P1-16 的前置 —— 以前想验证「镜像能不能构建」就必须真打 tag、连带部署生产;现在可以 dispatch 到那个 tag 上勾选它,只跑门禁 + 构建推镜像 + 建 Release。⚠️ 风险与验证边界: inputs 出现在 job 级 if 是允许的(与 secrets 不同),actionlint exit 0;但「tag 推送时 inputs 为空 ⇒ 照常部署」这条运行时语义本地无法验证,只能等第一次真跑 tag 时确认 —— 已写进 ci-cd.md 的触发矩阵。验证:actionlint exit 0、 pnpm lint:workflows exit 0、pnpm prepush 八段全绿 |
| 2026-09-23 | 清旧账第 8 批(R11 收尾 + 门禁再扩一半): ① 门禁扩到 markdown 链接。上一轮的 walnut-check-doc-refs 只看反引号里的路径,看不见真链接 —— 而 apps/server/AGENTS.md 那份 14 行索引每一条都指向不存在的文件(.agents/docs/ 目录从未进过仓库),当时完全没被拦住。现已加 extractLinkTargets / linkResolves(对齐 VitePress 的三种解析:原样 / 补 .md / 当目录找 index.md)。② 边界是实测出来的,与 VitePress 零重叠:埋一条指向不存在 .yaml 的相对链接 → build:docs exit 0(VitePress 不查非 .md 链接);埋一条 .md → exit 1。于是定为:文档站里的 .md 链接交给 VitePress(它有自己那份「冻结语料 + 未编写组件页」白名单,不重复维护),文档站里的非 .md 链接、以及文档站之外的全部链接由本门禁查。③ 又抓出 17 类真失效并全部修掉: apps/server/AGENTS.md ×14(重写为真正的包内导航:指向 CLAUDE.md / 根文档 / ADR / .claude/skills,并写明 .agents/docs/ 从未进仓库这段历史);release.md ×2 条层级写错的非 .md 链接(../../../../../pnpm-workspace.yaml → GitHub 链接,VitePress 查不到这类);apps/admin/.../AI/docs/REVIEW.md ×1 —— 顺带查出该 review 的 A1 条目已过期:deepseek.ts 全盘搜索不存在、源码里也没有任何 DEEPSEEK 引用,已标注关闭。④ R11 收尾: apps/server/README.md 原是未改动的 NestJS 脚手架 README($ npm install / $ npm run start,与本仓 pnpm-only 相悖)→ 重写为真实的后端 README;apps/server/TODO.md(原 server 仓草稿待办,无日期、18 条未完成、零引用)→ 加头部说明它是历史草稿而非本仓 backlog,并指向 architecture-todo.md。验证: @walnut/scripts 92 用例(新增 11 个链接相关)、pnpm lint:docs-refs exit 0、pnpm prepush 八段、pnpm build:docs、actionlint 全绿 |
| 2026-09-23 | 清旧账第 7 批(把「幽灵引用」做成门禁):@walnut/scripts 新增门禁 bin walnut-check-doc-refs(根脚本 pnpm lint:docs-refs),把前两轮靠一次性脚本手工扫出来的东西制度化 —— 校验活文档正文里的 ① @walnut/* 包名 ② 仓库路径引用是否真实存在。判据刻意「宁可漏报不可误报」(门禁一旦有噪声就会被无视,等于没做):包名只认 @walnut/<段>(@walnut-server/ 是内部 lib 命名空间,不查);路径只在 markdown 反引号里查、必须以顶层目录开头、且允许语境解析(仓库根 → 文档自身目录 → apps/server/,这样 env-encrypted/ 这类相对 server 的写法不再误报);ADR 与待办文档排除在路径检查之外 —— 前者合法地引用历史路径(ADR 0017 讲的就是重组),后者的职责就是记录失效引用本身;ADR 里的 markdown 链接仍由 VitePress 内置覆盖。豁免清单每条强制写理由,并有单测拦「清单腐化」(已存在的包名不许再留在豁免里、理由不许过短)。实测豁免收敛到 包名 15 条 / 路径 7 条,全仓 0 未豁免失效引用。 接线: prepush(七段 → 八段)、ci.yml(新步骤 Doc reference check)、发版电池(新行 docs-refs,steps.test.ts 同步,电池 9 → 10 条)。开发中被单测抓出的自身缺陷(都已修):占位符 apps/admin/.../AI/docs/ 被当成路径;pathResolves 把 path.join 的反斜杠透给调用方(Windows 上断言假失败)—— 现已统一为「仓库相对正斜杠」契约;以及两处我自己写错的路径层级断言。验证: @walnut/scripts 81 用例全绿、pnpm lint:docs-refs exit 0、release 195 用例全绿、pnpm prepush 八段全绿、actionlint exit 0 |
| 2026-09-23 | 清旧账第 6 批(路径版漂移扫描):把上一轮「不存在的包名」扫描扩成「不存在的仓库路径」扫描(239 篇活文档 / 335 条反引号路径引用 → 40 条命中),逐条判定后修真错 9 类: ① ADR 0009 / 0015 / 0016 里的 docs/reference/*、docs/decisions/*、docs/adr/* 全是迁移前的老路径(根 docs/ 早已并入文档站)→ 改成站内相对链接;② ADR 0015 的测试配置段与示例树写的是重组前布局(packages/{utils,contract,client}、apps/api/vitest.config.ts)→ 按真实位置改写,并如实补注「__tests__/ 与同级 *.test.ts 两种并存」;③ .claude/skills/be-gen-module/SKILL.md 把 DB Model 常量指到 apps/api/src/const/app/config.ts → 实际在 apps/server/libs/const/src/app/config.ts;④ typescript.md / tsconfig/README.md 的裸 scripts/build-barrel.ts → 补全路径;⑤ migration-guide/ 目录根本不存在(归档评审 2026-09-21 就记过「4 处引用已删除的目录」,本轮清掉最后 3 处文字 + 1 条死掉的 knip 豁免 '**/migration-guide/**')。判定为合法不动: env-encrypted/ / env-local/(相对 apps/server/ 的语境写法)、.changeset/ledger.yaml(发版时才生成)、.changeset/config.json(有意删除)、裸 scripts/(历史叙述)、ADR 0017 里的重组前路径(那篇讲的就是重组)。顺带验证:上一轮修的 turbo 缓存输入缺陷生效了 —— 改 ADR 后 build:docs 这次是 cache miss(真实重建),修之前会重放旧 dist 静默跳过死链校验。验证: pnpm prepush 七段全绿、pnpm build:docs exit 0、零死链 |
| 2026-09-23 | 清旧账第 5 批(机械扫幽灵引用,查出一个真 bug + 两处过期入口文档):写了一次性脚本「枚举真实 workspace 包名 → 扫描全仓引用 → 报出引用了不存在包名的地方」,扫 1840 个文件。结论:15 类幽灵引用,其中真问题三个 —— ① 真 bug(代码): packages/tooling/release/src/release/attribution.ts 的 SCOPE_TO_PACKAGE 里还留着 'tooling': '@walnut/tooling' —— 那个包已拆成 5 个。后果:一条 fix(tooling): … 且没动任何包目录(例如只改根 turbo.json)的提交,会为一个幽灵包写出 pnpm change 意图。已改为把 tooling 归入 NON_PACKAGE_SCOPES(与基础设施 scope 同理:工具链改动按路径归属,路径没命中就不发版),并给日志一句专门说法(原来会误报成「未在册的 scope」)。② 补测试:该模块此前零直接测试。新增 attribution.test.ts(15 个用例,release 包 195 个用例全绿),核心是那条不变量 —— SCOPE_TO_PACKAGE 的每个值都必须是真实存在的 workspace 包(这条守卫就是能提前抓出①的那种)。③ 两处过期入口文档: README.md 的结构块整块虚构(packages/{shared,axios,core} 三个包一个都不存在,还在教 pnpm dev:admin);CONTEXT.md 把 @walnut/axios 当现存包。都已按真实结构校正。验证:release 包 types:check + 195 用例全绿、pnpm prepush 七段全绿 |
| 2026-09-23 | 清旧账第 4 批(F2-b 调研 + 修掉 3 个坏锚点 + 修一个门禁失效):调研「文档链接校验用什么现成工具」,结论见上方小节: ① VitePress 是 markdown-it 不是 remark ⇒ remark-validate-links 插不进构建,且它要求链接指向真实文件(本仓 ~15 条不带 .md 的链接会误报);② lychee(--offline --include-fragments --fallback-extensions md)是唯一能补锚点校验的现成工具,CI 用 lycheeverse/lychee-action 不需要本地装二进制,配方已写进文档;③ 外链检查器(markdown-link-check / linkinator)会因 177 条外链常年红,不用;④ prose 里 584 处反引号仓库路径没有现成库。实测 ROI 后决定暂不安装:全站只有 6 条带锚点的站内链接,为它引入二进制 + CI 步骤说不通。 修掉 3 个真实坏锚点(都在 architecture-todo.md,是我自己写的):VitePress 的 slugify 把全角括号与全角冒号都转成 - —— ## 搁置(等条件成熟) 的真实 id 是 搁置-等条件成熟;已按构建产物的真实 id 逐个改正。顺带修掉一个门禁失效:根 turbo.json 的 build 任务把 !**/*.md 排除在缓存输入外 ⇒ 改文档不会让 docs 构建缓存失效,本地 pnpm build:docs 会重放旧 dist、静默跳过死链校验(CI 无缓存所以不受影响,但本地会骗人)。已在 apps/docs/turbo.json 覆盖 inputs 把 .md 纳入。验证: pnpm build:docs 强制真实构建 exit 0、零死链、页内锚点全部可解析 |
| 2026-09-23 | 清旧账第 3 批(F3 + F4,文档治理): ① F3 删掉重复文档树 —— apps/docs/{zh-CN,en-US}/ 164 个文件全删。删前逐文件核对(不是拍脑袋):164 个全部在 apps/docs/src/ 下有同名文件(内层还多 49 个),其中 157 个逐字节相同,7 个有差异的都是旧修订版(内层 5 个更大、已重写;support.md / frontend/base/vendor.md 2 个外层更大,含已从站点移除的旧段落);srcDir: 'src' 且全仓零引用 ⇒ 外层树从不参与构建。删后 pnpm build:docs exit 0。② F4 孤儿报告归档(不是按评审说的"废除/折进 ADR 0007")—— 核对原文后发现评审的前提有误: apps/server/docs/lib-extraction-recommendations.md 提议的落点是 apps/server/libs/(NestJS 内部 lib),与 ADR 0007「后端 lib 保持内部 lib、不提升为 workspace 包」并不冲突;真正的问题是候选包名用了前端 scope @walnut/*(会把 609722b 修掉的命名碰撞重新引入),以及全仓零引用。因此按本仓既有归档约定("新增过程文档直接写在 content/archive/")迁到 archive/2026-07-26-lib-extraction-recommendations.md:加归档横幅说明未被执行的实情 + 把 21 处 @walnut/<候选> 改为 @walnut-server/<候选>(核验后无残留)+ 登记进 archive 索引;apps/server/docs/ 随之清空。抽取计划本身记为 A12(未排期)。验证: pnpm build:docs exit 0、零死链 |
| 2026-09-23 | 清旧账第 2 批(F2 的第一半:文档链接门禁):apps/docs/.vitepress/config/index.ts 的 ignoreDeadLinks 由 true 改为白名单数组 —— 打开 VitePress 内置死链校验,零新增依赖、零新增脚本。打开后实测暴露 86 条死链,收窄白名单并修掉真错的 9 条后归零:· 真错并已修: monorepo/release.md 的「关键文件」表 5 条相对路径层级数错(../../../../../cliff.toml 之类,且这些是仓库文件、不是站点页面)→ 统一改 GitHub 链接;monorepo/architecture-todo.md 2 条 ./industry-research/… 少了一级 → ../industry-research/…;content.md 的 ./vue/introduction.md → ./content/frontend/introduction.md;frontend/base/vendor.md 的 ../components/vendor.md → ../component/index.md;en-US/guide/configuration.md 的 ../content/monorepo/env-management.md → 根绝对路径。· 白名单只留两类并各写理由:冻结语料( archive/、industry-research/)+ content/frontend/component/index.md 里指向尚未编写的 ~74 个组件页的链接(记为 F2-a)。· ci.yml quality job 新增 Docs build (dead-link check) 步 —— 以后新增死链直接红。验证: pnpm build:docs exit 0(0 死链)、actionlint exit 0 |
| 2026-09-23 | 清旧账第 1 批(R4 / R3 / P3-19,验收口径即核实方式): ① R4 根配置接入类型检查 —— 新增 types:check:root: tsc -p tsconfig.json,接入 prepush(六段 → 七段)、ci.yml(新步骤 Type check root configs)与发版电池(新行 types-root,steps.test.ts 同步);eslint.config.ts / commitlint.config.ts / knip.config.ts 此前没有任何脚本做类型检查。② R3 根 eslint 换 base 预设 —— 根 eslint.config.ts 由 vue 预设改为 base。核实中发现原条目描述的前提是错的:仓内只有 5 份 eslint.config.ts,另外 10 个包经 ESLint 向上查找也用根配置(它们全是 TS-only、零 .vue,base 才是它们该用的预设)。用 eslint --print-config 逐文件比对规则集,确认只丢掉 2 条对 TS 无效的 unocss/*;同时给 base 补上 pnpm: true 与 pnpm/yaml-enforce-settings: off,否则会丢 3~4 条 pnpm/* 规则并让 lint:root 直接报 shellEmulator mismatch。③ P3-19 turbo 产出警告 —— @walnut/{client,http,types,ui} 四个包的 turbo.json 声明 "build": { "outputs": [] };根 turbo.json 的 test 任务把 outputs 从 ["coverage/**"] 改为 [](正常 vitest run 不产出任何文件,6 个有 test 脚本的包此前每次刷警告)。实测 build --force 与 test --force 的 "no output files found" 警告 合计 10 → 0。验证: build(9/9,2m27s)、test(12/12)、lint(14/14)、lint:root、types:check:root、prepush 七段、build:docs、actionlint 全绿 |
| 2026-09-23 | 待办表核实与瘦身(本轮):逐条核实原 P0-P3 / A1-A11 / R1-R11 的 ✅ 标记 → 18 项确认完成并移出、1 项(R1)判定回退后重开;剩余项按 P0-P3 重排,新增「未裁决」段收纳 2026-09-21 评审的 F/D 项(即评审建议 F5「状态移出文档」 的落地);新增 tsx 全局移除后的文档同步(ADR-0019 / AGENTS.md / CLAUDE.md / package-scripts.md / pnpm-workspace-config.md),catalog 243 → 242 |
| 2026-09-23 | 仓内彻底移除 tsx:apps/admin 的 predev / types:check:log 改由 node 原生类型剥离执行(相对导入补 .ts 扩展名、JSON 导入补 with { type: 'json' }),devDependency 与 catalog 条目删除;顺带修掉 .gitignore 的死规则 report/*(带斜杠锚定到仓库根,实际产物在 apps/admin/report/)。详见 commit 3daafc0 |
| 2026-09-23 | 工具链拆包 + tsconfig 预设提取 + 全仓去 .mjs:packages/tooling/ 1 → 5 包;提取 @walnut/tsconfig(base / ts / vue)并删根 tsconfig.base.json;16 个 .mjs → .ts(含 249 行装饰器排序插件补类型);根 devDeps 18 → 19;prepush 五段 → 六段。完整决策与代价见 ADR 0019,commit 2190873 |
| 2026-09-23 | 发版与 Git 钩子迁移:@changesets/cli → pnpm 12 原生 release management(versioning 段为唯一真源、.changeset/ledger.yaml 为台账);changelog 改 git-cliff 逐包渲染;钩子 simple-git-hooks → lefthook(ADR 0018)。详见 发布 & 发版指南 与 commit 4e16d7d |
| 2026-09-23 | CI/CD 与容器构建重构:修复 ci.yml 的非法表达式(steps.if 用 secrets 上下文 → Invalid workflow file,CI 自 2026-08-13 起五周从未运行);拆流水线(commit 只跑质量门禁、tag 才构建镜像、deploy.yml 改可复用纯部署);修 buildx 缓存 scope 冲突(单次上线实测 85 min → 正常);镜像改「薄运行时」并剔除 pnpm deploy --prod 拷进产物的 env-local 明文密钥;新增 actionlint 闸门。详见 CI/CD 与容器构建 |
| 2026-09-23 | 部署后验证(post-verify):新增 deploy/post-verify.sh(轮询 2 分钟检查三容器 running / 镜像 tag 一致 / 后端日志无致命错误 / 前端与入口 nginx 无 5xx / 公网端到端 200);deploy/nginx/Dockerfile 把 nginx 日志软链到 stdout/stderr;新增 deploy/post-verify.test.sh 用假 docker/curl 覆盖 10 个场景 |
| 2026-09-23 | 文档整理:根 docs/ 目录移除;CI/CD 设计重写为长期文档 CI/CD 与容器构建;6 份过程文档迁入 content/archive/ 并加归档横幅 |
| 2026-08-08 | 共享包测试 + peers + AGENTS:@walnut/utils 5 个测试文件、@walnut/contract 12 组契约快照测试 + vitest 配置;peerDependencyRules.allowedVersions 豁免 5 项 peers 错位(该机制已于 2026-09-21 随 pnpm 12 迁移删除 → 见 R1);根 AGENTS.md 重写为分发式导航文档 |
| 2026-08-08 | 文档配套 + 优化池:A4 表格补勾;P1-4 Remote Cache 标记不接入;P3-14 理由更新;新增「架构优化池」R1-R11;修复 turbo preview 任务缺失;release.md / index.md / eslint.md / turbo.md / env-management.md / server+docs 的 CLAUDE.md 同步 |
| 2026-08-08 | 架构 review 第三批:@walnut/client 的 vue/pinia 移入 peerDependencies;DOM lib 下沉到 admin/docs/platform-web;server strict: true(保留 strictPropertyInitialization: false);清理 6 处残留空导入 import { } from '@walnut/contract';build:stage 升级为独立 turbo 任务(废除三重 -- 透传);lint-staged 删除无效的 *.md 条目 |
| 2026-08-08 | 架构 review 第二批:turbo dev/test 接入 dependsOn: ["^build"](修 fresh clone 下 dev:server 的 MODULE_NOT_FOUND);turbo boundaries 接入 pre-push 与 CI;CI workflow 落地(P0-1);根 clean:all glob 修复;@walnut/eslint-config 补 lint 脚本;apps/* 补 private: true;边界验证实测(临时加 app 标签 → 正确报 3 处 shared→app 违规) |
| 2026-08-08 | 架构 review 修复批:修复 production/stage 缺失 USER_ID_ENCRYPTION_KEY / USER_ID_HASH_SALT(生产启动失败 bug);根 pnpm test 可用(server vitest 配置路径、I18nService 依赖、--passWithNoTests、client jsdom);knip 假门禁修复(packages/* → ./packages/*/*,此前静默空操作)+ 清死依赖;server 的 jest/ts-jest/ts-node 残留清理;根 scripts NODE_OPTIONS= 改 cross-env |
| 2026-08-08 | PWA 移除:删 vite-plugin-pwa + workbox-window + @vite-pwa/assets-generator 及全部相关代码。原因:workbox-build 传递链问题多(lru-cache CJS 崩溃、高危漏洞、precaching 陈旧内容),后台场景价值有限 |
| 2026-08-08 | 构建与 dev 验证:P0-3 解决(env 解密流程 + lru-cache override + optimizeDeps 过滤);修复 tsbuildinfo 掩盖的 6 处既有类型错误(Table / ApiSelect / CountryCallingSelect);pnpm build:admin 成功;dev 前端(3100)+ 后端(3000,连 MongoDB/Redis)启动验证通过 |
| 2026-08-08 | ADR 0017 收尾批次:A1 旧目录清理;A2 包 walnut 标签补齐;A3 boundaries 升级为 platform 维度 + 修 10 处 ~build/package 失效 import;A4 client 移除 vue-router 死依赖;A5 admin 硬编码路由 → contract;A6 server 移除 @walnut/utils 死依赖;A7 @walnut/ui POC(3 组件)。lint 9/9 + types:check 9/9 + boundaries 零违规 |
| 2026-08-08 | 文档同步:commitlint(P1-6)实际已完成;新增 ADR 0017 遗留收尾线(A1-A11) |
| 2026-07-30 | syncpack(P2-9)— 依赖版本一致性检测接入 |
| 2026-07-29 | P0 剩余 3 项未完成(CI/CD 流水线、共享包测试、前端构建修复 —— 后均已完成) |
核实记录(已移出待办)
2026-09-23 逐条复核。只留一行索引:记录「当初的验收口径」,便于日后发现回退时对照。
| 原编号 | 事项 | 核实方式与结果 |
|---|---|---|
| P0-1 | CI/CD 流水线 | .github/workflows/{ci,workflow-lint}.yml 均在;ci.yml 无 if: … secrets.*、含 TURBO_SCM_BASE/HEAD 与 Affected set sanity check 步;prepush 含 lint:workflows;actionlint v1.7.7 全 workflow 通过 |
| P0-2 | 共享包测试 | @walnut/utils 5 个 *.test.ts(queue / regex / transformer / persistent×2);@walnut/contract/src/index.test.ts + vitest.config.ts;pnpm test 12 包全绿(utils 29 例 / contract 12 例 / scripts 66 / release 180) |
| P0-3 | 前端构建修复 | pnpm-workspace.yaml 已无 overrides: 段(lru-cache 只剩 catalog 条目,非 override);pnpm build:admin → ✓ built in 1m49s |
| P1-4 | Turbo Remote Cache | 决策为不接入;globalPassThroughEnv 保留 TURBO_TOKEN / TURBO_TEAM;无 remoteCache 配置 |
| P1-5 | 部署流水线 | deploy.yml 是 workflow_call + workflow_dispatch 的纯部署;docker-bake.hcl 每镜像独立 cache scope;backend Dockerfile 含 env-local 剔除 |
| P1-6 | commitlint | 根 commitlint.config.ts + @walnut/commitlint-config + lefthook commit-msg;本轮两次提交均被其真实校验放行 |
| P1-7 | Changeset Bot | 全仓无 @changesets/* 依赖声明、lockfile 无残留、.changeset/config.json 已删(目录只剩 README.md) |
| P2-8 | Docker 多阶段 → 薄镜像 | 三个 Dockerfile 均无 pnpm install / pnpm deploy(只 COPY runner 侧产物) |
| P2-9 | syncpack | pnpm syncpack:lint → exit 0「No issues found」 |
| A1 | 旧目录残留清理 | packages/{axios,client,contract,eslint-config,utils} 均不存在 |
| A2 | walnut 标签补齐 | packages/ 下 11 个包的 package.json 全部带 walnut 字段 |
| A3 | turbo boundaries 升级 | 根 turbo.json 的 boundaries.tags 含 platform-any / platform-node |
| A4 | @walnut/client vue-router 死依赖 | dependencies / devDependencies / peerDependencies 三处均无 vue-router |
| A6 | server 死依赖决策 | apps/server 无 @walnut/utils 声明;git grep @walnut/utils apps/server/{apps,libs} → 0 |
| R5 | AGENTS.md 重写 | 根 AGENTS.md 是分发式导航文档(含「仓库结构」「常用命令」「关键纪律」) |
| R6 | only-allow | 根 package.json 无 preinstall;全仓无 only-allow |
| R9 | turbo preview 任务 | 根 turbo.json 的 tasks 含 preview |
| R10 | CI affected 空集行为 | ci.yml 含名为「Affected set sanity check」的步骤(文件变更非空但受影响包为 0 → 失败) |
| F0 | 修正 hoisting: false | pnpm-workspace.yaml 已是合法的 hoist: false(2026-09-21 pnpm 12 迁移完成) |
| F1 | 根文档单一化 | 4 份 CLAUDE.md(根 + 三个 app)各只剩一行 @AGENTS.md 导入、无正文规则;apps/{admin,docs,server}/AGENTS.md 三份包级指引均在;根 CLAUDE.md 196 → 35 行 |
| R8 | @walnut/types exports 结构 | exports 同时有 "."(→ src/index.d.ts)与 "./*";src/index.d.ts 是具名 re-export(无 export *);探针 import type { … } from '@walnut/types' 在 utils-core 的 types:check 下可解析 |
| F6 | ADR 规范化 | 19 篇 ADR 的 **Status:** 全部是 Accepted(枚举内、无 Implemented/In Progress);标题全部 # ADR-NNNN:(连字符);每篇恰好一个 ## Alternatives considered 且有列表项;pnpm lint:adr exit 0 |
| P3-14 | Vitest 共享 preset | packages/tooling/vitest-config/ 存在且是单一 fixed 组第 15 个成员;7 份 vitest.config*.ts 全部经 defineWalnutVitestConfig;versioning-config.test.ts 6 用例通过 |
| P3-15 | oxlint / biome 措辞 | ADR 0014 Decision 2 的 rationale 不再出现「不支持 Vue SFC」的绝对判断;带 **Last revised:** 2026-09-23 注记 |
| P3-17 | TCR 旧 tag 清理 | ci-cd.md 有「镜像仓库的版本上限与清理策略」一节,写明走控制台生命周期策略 + 必须排除 nginx:brotli + 本条无机械判据 |
| P2-10 | Codecov | 决策为不接入;根 package.json 与各 vitest.config* 均无 codecov 相关配置 |
| F5 | 状态移出文档 | 本轮完成:完成项从待办表移除,只留本核实索引 |
| F2-a | 文档死链白名单 | apps/docs/.vitepress/config/index.ts 的 ignoreDeadLinks 只剩 1 条(冻结语料);content/frontend/component/index.md 里 0 条 ](./ 形态链接;pnpm build:docs exit 0 |
| P2-22 | apps/server/libs/** 的文档纳入别名检查 | check-doc-refs.ts 的 aliasBaseOf 对 apps/server/libs/ 下的 markdown 返回 server;libs/db/README.md 里那个全仓 0 命中的 WalnutAdminConstDBModelName 已删除,改成从 @walnut-server/db 同时引 WalnutDBInjectModel 与 WalnutDBModelName;pnpm lint:docs-refs exit 0 |
| F9 | 组件文档两棵树 + 要不要英文站 | 已裁决(2026-09-23)。裁决前的取证(三件事各自的事实,不是猜测):apps/docs/src/zh-CN/component/ 42 篇真实组件文档、而 zh.ts 的 sidebar 当时没有 component/ 条目(只有 '/content/' 一个前缀键,/component/** 是并列的另一棵树)⇒ 只能手敲 URL 才看得到;zh-CN/content/frontend/component/ 只有 index.md,却规划了另一套命名的 74 页;apps/docs/src/en-US/component/ 44 篇(比 zh-CN 多 6 篇),config 里 en-US locale 处于注释状态 ⇒ 不被服务,且内容其实是中文。裁决:① 不要英文站 → en-US/** 冻结(不删不修不翻译),理由写在 apps/docs/.vitepress/config/index.ts 那段注释里(放在动手的位置,不放在本表);② 正典 = content/frontend/component/index.md 那套 74 页新命名,旧树写完即淘汰 → 落地为 P3-21 |
| P3-22 | server 的 build 与 build:stage 共用一个 dist | apps/api/tsconfig.app.stage.json 存在且 outDir 为 ../../dist-stage;nest/stage.json 的两处 tsConfigPath 指向它、三条 assets[].outDir 也是 dist-stage/apps/api/src;根 turbo.json 的 build:stage.outputs 含 dist-stage/**。实测:turbo run build:stage --filter=@walnut/server 冷跑产出 1465 个文件(含 dist-stage/apps/api/src/main.js)而 prod 的 dist 754 个文件 sha 不变;删掉 dist-stage 再跑 → FULL TURBO 命中并完整回放。门禁第 7 条不变量守着它 |
| P2-21 | 拆分 apps/server/AGENTS.md | apps/server/AGENTS.md = 157 行 / 6035 字符(原 537 / 16150);五篇新页在 apps/docs/src/zh-CN/content/backend/ 且都在 zh.ts 的 backendContent 里;它已进 check-doc-budgets.ts 的 DOC_BUDGETS(pnpm lint:doc-budget 报 87%);prepush 的别名规则在 .claude/skills/** 上 0 失效 |