批次执行记录
这是第 11 批起的执行记录摘要:每批写清楚目标 / 关键发现 / 验收读数 / 复查轮挡下了什么 —— 最后一项最有价值。 原始证据(每批的 R1 调研、R2 实施、R3 复查原文,含逐条
path:line与命令输出)已随 2026-11-04 收尾 归档至 git 历史 —— 每批的结论与验收读数已在本页,本页不抄日志。 第 15 / 16 / 17 批连原文都没有 —— 它们的证据是提交号与门禁读数,各节开头已声明。两条口径:① 下文的门禁读数与用例条数都是各批当时的快照,不是现状 —— 数字会变,判据不会; ② 门禁一律
cmd /c "pnpm …"跑、退出码立刻取$LASTEXITCODE(把管道后的退出码当读数会造出假红)。
第 11 批 · 设备身份收尾(B5)
- 目标:B5 第 2 步之后的收尾 —— 硬切需要的 DDL、90 天留存清理、
deviceName退出 Initial 请求面、两个生成物同步。 - 关键发现:硬切路径下
device_id从不被 UPDATE ⇒ 真正需要的 DDL 只有一条形态 CHECK (apps/server/apps/api/migrations/1793390000000-DeviceIdFormCheck.ts:43-46);留存判据不能只用stale, 且心跳行必须同事务删、走 RLS 的 system 通道(apps/server/libs/system/src/device/repo/device.repo.service.ts:117-136)。 - 验收读数:单测
101 文件 / 833 条exit 0;apps/server的types:checkexit 0;migration:run → revert → run两轮实测(约束「在 / 不在」可区分);integration 分片合计65 文件 / 653 条 = 647 passed · 5 failed · 1 skipped。 - R3 挡下的:
8 文件 / 69 条全绿被当成「收尾完成」的依据 —— 其余文件里还有 5 条红:apps/server/apps/api/test/api/platform/throttle.api-spec.ts:34与apps/server/apps/api/test/api/platform/api-rollback.api-spec.ts:284由本轮改动引入(deviceName从「静默忽略」变成 40099,属破坏性 wire 变更)、apps/server/apps/api/test/integration/auth/auth.login-chain.pg.int-spec.ts:632-633漏改 fixture 撞上新 CHECK。 同轮还证伪了一条自述:admin 侧那不是「vue-tsc 机械守卫」——Omit<…>配as断言不做多余属性检查,多带字段时 tsc 一声不响。
第 12 批 · D 组结构债
- 目标:撤掉一个共享日志模块的
@Global、删一个死模块、给「@Global只在消费方模块数 ≥ 2 时允许」与 「非@Global的每个消费方都要能经 imports 看见它」补机械判据、修SKIP_DIRS的裸名logs。 - 关键发现:撤
@Global的落点是拥有者模块apps/server/apps/api/src/modules/auth/auth-http.module.ts:64-67; 真 bootstrap 三态实验里,「落父模块auth.module.ts」与「不接线」报出的是逐字相同的logAuthSharedService不可解析错误 —— 注入器确实解析在宿主 controller 所在模块。 另有一条没被谁预料到的:这条接线也是部分集成套件装配的必需品,拆了爆炸半径是「openapi 导出 + 相关集成套件」。 - 验收读数:单测
101 文件 / 836 条exit 0;types:check/lint/boundaries/openapi:export(零 diff)全 exit 0; 集成分片12 文件 / 176 条exit 0;新判据的 6 个负对照全红,四个「会假绿」的坑逐个确认是修对而不是改松。 - R3 挡下的:「模块扫描序零变化」不成立 —— 真实容器里那个无 guard 节点的扫描位从 156 提前到 98、其后 48 位整体后移 (结论对、证据错:拿走查器的盲区当成了结论);一处未申报的改动把
apps/server/libs/security/src/cap/__tests__/cap.shutdown-order.spec.ts的超时从 5s 放宽到 30s ——「一个本来在负载下会红的用例现在不会红了」,这属于测试严格度悄悄变松;另外「types:check永久红」这条结论也已过期。
第 13 批 · 文档与事实对齐
- 目标:把 B5 之后的事实漂移从活文档里清出去:设备身份语义、计数口径、SKIP ≠ PASS、风控分级入文档。
- 关键发现:
deviceName是每次 initial 重新生成并覆盖(新建与复用都覆盖)——apps/server/libs/system/src/device/device.service.ts:94-96与apps/server/libs/system/src/device/device.basic.repository.ts:46-52两处为证;R1 与旧文档都写成「只在插入时」。 另一处:README.md的分组计数「加起来等于 15」是加法验不出来的假绿 —— 分组本身已经错了。 - 验收读数:
lint:docs-refs/lint:doc-ts/lint:doc-budget/lint:adr/lint:emoji全 exit 0,pnpm --filter @walnut/docs buildexit 0(66.52s,死链即失败)。 - R3 挡下的:
apps/docs/src/zh-CN/content/shared/device.md:169的 mermaid 仍写「deviceName 只在插入时由服务端生成」 —— 与同文件正文、与代码自相矛盾,等于把本批最大的纠错点在同一页上作废;apps/docs/src/zh-CN/content/backend/modules/system-account.md:92-93把并发失败侧写成 40605, 而它自己引用的那份 spec 断言的是 40000 / 42900。
第 14 批 · 残余债收口
- 目标:注释与私有 JSDoc 里的过时断言、
db:seed:verify的「到底有没有门禁在跑它」、门禁第四态(跳过)、 两个死函数、以及配套文档。 - 关键发现:
lint:workflows现在真的退 3 并写「未验证(跳过不等于通过)」,prepush 把该段打成-+[SKIP 未验证]单列; 删那两个死函数不改部署契约 —— 变的是crypto.deviceIdKey成了悬空配置,DEVICE_ID_ENCRYPTION_KEY当时仍是启动期必填, 由apps/server/libs/system/src/device/__tests__/device-issuance.drift.spec.ts的三条断言机械守 (两者 2026-11-04 都已消失:DEVICE_ID_ENCRYPTION_KEY退役、crypto.deviceIdKey随配置骨架删除,见第 17 批)。 - 验收读数:
@walnut/server单测102 文件 / 839 条、@walnut/scripts545 条、@walnut/release206 条全绿;lint:dtsexit 0;pnpm prepushexit 1(失败段:lint / test / openapi / dts —— 逐段归因后没有一条是本批引起)。 - R3 挡下的:那几段红里大部分是假红 —— 单段预算到点时调用的是「杀全部活跃子进程」的
killActiveChild()(packages/tooling/scripts/src/lib/child-run.ts:196-205→packages/tooling/scripts/src/lib/child-tree.ts:124-137),把并发的兄弟段一起 taskkill, 四条失败段的结束时间精确到 0.1s 相同;另有「零代码读者」四处措辞与本批自家 drift spec(配置骨架里的读者)互相打脸。
第 15 批 · 门禁可信度
无批次报告:证据是提交与门禁读数。 这一批没有原文目录,所以下文每一条都落到提交号上。
- 目标:让门禁的读数与事实同形。第 14 批暴露的三条全都属于「门禁在伪造证据」—— 一段超时会把并发中的 兄弟段一起收掉、「超时被杀」与「真失败」同形、发版电池把显式跳过的条目算进「真跑通过」。
- 关键发现:
packages/tooling/scripts/src/lib/child-tree.ts登记的是模块级活跃子进程集合,超时分支 再调无参的killActiveChild()就顺手收掉兄弟段 —— 上一轮 4 条失败里 3 条是这么来的假红。修法不是 「把超时放宽」,而是把 kill 作用域化到本条 gate 的子树(17450ef5)。 - 验收读数:
packages/tooling/scripts的回归用例把两件事钉住 —— 预算的优先级(段表budgetMs> 全局GATE_BUDGET_MS>PREPUSH_BUDGET_MS覆盖)与「兄弟段不再被连带杀掉」;断言写在packages/tooling/scripts/src/ci/__tests__/prepush.test.ts。 - 记账口径:预算是冷跑实测的下界推出来的,不是拍的 ——
GATE_BUDGET_MS= 600s(5dbdb4e8), lint / test 两段单独用SLOW_GATE_BUDGET_MS(17450ef5)。同批--skip-gates不再算「真跑通过」: 发版电池只数传入的 Set,显式跳过与子进程退 3 都单列「跳过(未验证)」并减ranCount(5dbdb4e8)。
第 16 批 · 收尾(关掉带 C 号的待办与剩余的 P2/P3)
无批次报告:证据是提交与门禁读数。
- 目标:把「架构待办里还开着、但已经不缺条件」的条目一次清掉 —— 两条带
C号的(C1/C3)、 两条 P2/P3(两套 ALS 并存、注册面与孤儿模块守卫)、一个死库。 - 关键发现(两条都推翻了自己的最初判断):
C3不是测试 flake:库侧apps/server/libs/db/src/tx.interceptor.ts的失败路径 catch 只 release 不 rollback,而 TypeORM 的release()不结束事务 ⇒ 连接带着未提交事务回池、被下一个请求借走。 负对照在真实 PG 上把 bug 态读成idle in transaction、修复态读成idle(c7f7e11f)。测试侧只是把 种子改成显式提交事务,让残余异常变成可定位诊断(e2dc612d)。C1不是「断言写错了」:真问题是Initial类型取了实体面,改取 wire 面之后两处类型松动各有 真因可修(b382a580)。
- 其余落地:
libs/delay整库删除(死库,41be94ab);libs/context并入libs/als,但两套 ALS 实例不合并 —— 合并实例会让 winston 读不到requestId(add9481b);同提交把注册面守卫补到能机械拦, 并新增孤儿模块守卫apps/server/apps/api/src/common/monorepo/__tests__/orphan-module.drift.spec.ts。 - 测试目录重整(
9238dcc8):单测进同目录__tests__/、接口测试test/api/<模块>/、集成test/integration/<领域>/、e2etest/e2e/。只动位置不动内容(用例集合不变)—— 判据写在 架构待办事项 的「测试目录约定」,本页不复制。 - 验收读数:文档门禁五连(
lint:docs-refs/lint:doc-ts/lint:doc-budget/lint:adr/lint:emoji)与pnpm --filter @walnut/docs build全 exit 0。 - 复查轮改写了什么:本批没有「挡下一处谎」,因为复查直接改写了归因本身 ——
C3从「测试 flake」 改成「库侧真 bug」、C1从「断言问题」改成「类型来源问题」。这两条如果按最初的判断收尾巴, 就会各留一个「已验证」的假结论在账上。
第 17 批 · guard 覆盖与契约诚实化
无批次报告:证据是提交号。 这一批没有原文目录,所以下文每一条都落到提交号上。
- 目标:把「守卫没人测」和「契约在撒谎」分成两件事修 —— 前者补覆盖,后者关掉那个生产谎言的开关。
- 契约诚实化:
defaultNonNullable不是后端的可选性,是生成器的默认值;开着它,openapi-typescript 会把带default的 字段一律抬成必填,前端于是被迫传后端可选项、再用Omit遮住(478个属性回到可选,ed69cb95)。同批还做了三件 「先把死代码清掉、再让语义显形」的事:删verifyHash并给libs/crypto补首份单测(8f409b13)、事务配对收敛到withQueryRunnerTransaction(1d484487)、DEVICE_ID_ENCRYPTION_KEY退役(d3cd2b5c);最后删掉根目录reports/并清死引用(21c3b098/52602682)。 - guard 覆盖:单测新增 device 15 / mfa 13 / ip 11 / security 22 / throttler 7 条;接口新增 sign 12 / jwt 12 / device 5 / cap 9 / throttle-global 5 / user-mfa 6 / user-lock 8 条(快照读数,数字会随用例增删变化)。
- 真正的产物是「首次被覆盖的语义」(判据不是条数):
40111、滑动续期、Sign40113、B5 旧 64hex 形态、 全局限流桶、Optional + 坏 AT 降级 —— 这些语义在补测之前没有任何用例能证明它们存在,补上之后才敢动它们。 - 两个真 bug:①
37d162db—— 90 天留存只删行、不失效SYSTEM:DEVICE:<deviceId>,而该缓存无 TTL ⇒ 被删的设备仍被放行,留存功能形同虚设(负对照apps/server/apps/api/test/integration/device/device.purge.cache.int-spec.ts); ②582f32e9—— Sign 的坏签名(cookie-parser 返回false)被当有值送去createHash().update(false), 崩成HTTP 200 + 50000;现在视同缺失走重签自愈,而「签名合法但值不符」仍是40112。 - 验收读数:
lint:docs-refs/lint:doc-ts/lint:doc-budget/lint:adr/lint:emoji与pnpm --filter @walnut/docs build全 exit 0(构建同时是死链门禁)。 - 这一批的教训:两处「已知风险」都是契约与实现不同形 —— 一处是生成器默认值在制造假必填, 一处是缓存没有自愈窗口、失效只依赖一条写路径。补测把语义钉住,下一轮才敢动它们。
没有原文目录的批次怎么记
本页的门槛原本是「有一份带 path:line 与命令输出的过程原文」:读不到原文的批次这里就不写,不凭计划书编执行记录。 第 15 / 16 / 17 批是同一类例外 —— 这几批的产物就是门禁与提交本身,证据是提交号加门禁读数; 再补一份「过程原文」只会是同一批数字的第二份抄写。所以规则收紧成: 没有原文目录的批次必须在节首声明证据是什么,否则不写。
教训:反复出现的三类失效
一 · 门禁红着,但没人看见
- 红的常常是门禁自己,不是代码:根
package.json少了行尾换行 ⇒lint:root在干净树上必红, 而那个文件不在任何一轮的改动面里,于是谁都不认为它和自己有关(第 14 批补上这一行后,该段在 prepush 里跑成了通过)。 - 默认超时也会这样:vitest 默认 5s,四路并发抢 CPU 时必红 —— 第 12 批那次「放宽到 30s」,其实是承认了这一点。
- 判据:如果一条红是「环境 / 预算 / 默认超时」引起的,它不会被修,只会被下一个人当成噪声忽略。所以要么修根因,要么让它有独立的读数口径。
二 · 跳过与通过同形
lint-workflows探测不到 actionlint 就return(退出码 0),prepush 只看退出码 ⇒ 打勾、汇总里算进「全部通过」。- 第 14 批把它改成第四态:退出码 3 + 汇总单列 + 只有
PREPUSH_STRICT=1才判红。关键不是「多一个退出码」, 而是让「未验证」在输出里占一行 —— 没有那一行,人类读者和 CI 都只会看到绿色。 - 同一类还有发版电池:
--skip-gates跳过的条目原先是算进「真跑通过」的,现在单列并减掉。
三 · 文档里写死的数字必然腐烂,而且加法验不出来
README.md的分组计数里,错误分组 3+3+3+6 与真实分组 3+3+2+1+6 都等于 15 —— 加法是验不出来的, 只能按组枚举(枚举出来才发现有一整组漏登)。packages/tooling/release/src/release/steps.ts里那句写死的「11 段并行」,在段数变了之后一直在说谎: 第 13 批只能先登记,第 14 批才改成判据式措辞并加防服用例(现在的写法是「段数以表为准」,表在packages/tooling/scripts/src/ci/prepush.ts)。- 判据:写「以某处为判据」而不是写那个数;确实要写数字时,写明它是哪一天 / 哪个 commit 的口径 —— 本页就是这么写的。
三条的共同点:门禁和文档都在回答「谁验过什么」。一旦它们的输出与实际不同形(跳过像通过、超时像失败、 数字像现状),验证本身就失效了 —— 而失效是静默的。所以本轮多数改动的真正产物不是功能,而是可区分的读数。