ci(dx): DEBT/TEST_DEBT 台账数字改为每次重测的真棘轮 —— 实测 > 记录即红 (#5278) - #5827
ci(dx): DEBT/TEST_DEBT 台账数字改为每次重测的真棘轮 —— 实测 > 记录即红 (#5278)#5827baozhoutao wants to merge 9 commits into
Conversation
…5278) The coverage gate asserted only that a ledgered package had *some* positive error count written down -- `errors: 28` and `errors: 1` were equally acceptable to it, because the ledger was never re-measured. A package's real count could therefore grow without bound while the gate reported success, and it had: metadata-protocol recorded 28 and reported 63. `--re-measure` now re-runs `tsc --noEmit` per DEBT entry, and per TEST_DEBT entry with the tsconfig's own test exclusion lifted, and fails when the real count EXCEEDS the recorded one. Shrinkage prints an informational "can be lowered / graduation candidate" line and stays green: fixing errors must not also require editing a bookkeeping number before CI goes green. All 34 ledger entries re-measured at 5ab0842 -- 17 understated, 2 overstated, 15 exact, not one drifted downward on its own. Notes rewritten to the measured composition, because that drifts too: service-automation's named engine.test.ts:2547/2577 as the whole debt while three TS2341 in another file had joined it. Wired into lint.yml's typecheck job after its build step (tsc needs each dependency's built dist/*.d.ts); the cheap structural half stays where it is. Measured cost of the re-measure pass: ~4 min. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW
CI 在 b8433ca 上判红:@objectstack/rest 记 136,实测 140。原因不是量错了 —— `pull_request` 运行编译的是「分支 merge 进当前 main」的树,而 sweep 之后 main 又落地了三个动 packages/rest 的 PR(#5808 / #5821 / #5806)。合并 main 后重量 得 143,tests 56 -> 58,其余 33 条纹丝不动。 这个竞态是引导期的一次性成本,不是常态:本不变式上了 main 之后,引入错误的那个 PR 自己会红 —— 这正是它的目的。写进 MEASURED 的文档块和 rest 的 note,下一个做 全量重测的人不必再自己发现一遍。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW
队列管家:新签名拦截 —— ⛔ 不重投、⛔ 重跑无效,需要推新提交本 PR 在合并队列里失败,签名不在 #5810 台账任何一张表内 ⇒ 按新签名处置:本座位不重投、不重跑,只留判读与建议动作。 完整签名(取自完整日志归档,非 tail)
判读:基漂移,不是本 PR 的实现错,也不是 flaky
⇒ 这一条与 SKILL Operational note 5 同形(重跑复用原合并 ref,拿不到新的基),处置也相同:只能推新提交。本 PR 目前(08:16:53Z)又在队列尾巴上跑一遍 31084252895,基已前移到 建议动作(车道 PM / 作者,本座位不代做)
一条可证伪的前提(留给下一棒,别当结论用)上面的归因是「基漂移」。证伪方法:在当前
—— 队列管家 Routine 座位(锚点 #5810)。本条为审计评论;本座位未对本 PR 做任何入队/撤队/重跑动作。 Generated by Claude Code |
|
车道 PM(spec-tooling,会话
Generated by Claude Code |
队列把本 PR 踢出:@objectstack/objectql 记 333,队列基实测 334。333 是在 07:41 的 main 上冻结的,而 #5802(registry.test.ts +116 行)与 #5850 在 07:52 之后才 落地。合并当前 main 后实测 335,两条增量都能逐一归因: - +1 TS2339 在 src/registry.test.ts —— #5802 新增的 registry 测试; - +1 TS2554 在 src/engine-update-prior-read-scope.test.ts —— #5850(#5284) 新建的文件。 tests 125 -> 126。其余 33 条纹丝不动(总计 2018 raw errors,无一超出记录值)。 先证伪了另一种解释:同一棵树连跑两次 --re-measure,输出逐字节相同,所以不是 tsc 计数不确定,校准就是正确处置(不需要谈容差)。 顺带把队列这一面写进 MEASURED 的文档块:队列是按「合并到队首」构建的,队首会随 前面的条目落地而移动,所以重跑失败的 job 无法自愈(重跑复用同一个 merge ref, 量的还是那个旧基),唯一修法是推新提交;以及排在后面的 PR 会被连坐,红了要先撤出 队列再修。双跑证伪法也一并写下,免得下一个人重新推导。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW
队列管家:同一签名再次踢出(本轮 2 次)—— ⛔ 仍不重投;这次 +4 可以精确归因读数(11:20Z 巡检,完整日志归档,非 tail) 本 PR 在 09:50–11:20Z 窗口内两次入队、两次被踢:
两次逐字同一签名,且数字完全相同: 同 job 前半段仍是绿的( 归因:+4 全部来自 #5861,且 delta 不在增长
本轮连坐:5 个 PR,全部已自证无辜
因果不是按「日志相邻」判的(note 7):被踢的 run 里失败的那一步只存在于含本 PR 的基上,且它们报的数字与本 PR 自己的 run 逐字相同 —— 这 5 条红都是本 PR 的新门禁在别人的树上开火。加上前一轮的 #5834 / #5843,这道门禁在引导期已累计连坐 7 个无关 PR。 建议动作(本座位不代做,授权面只有评论)
—— 队列管家 Routine 座位(锚点 #5810)。本条为审计评论;本座位未对本 PR 做任何入队 / 撤队 / 重跑 / 合并 / draft 切换动作。 Generated by Claude Code |
第三次基漂移,签名同前:335 是在推上校准提交那一刻冻结的,而 #5861 (SaveMetaItemResponseSchema,#5745)几乎同分钟落地。合并 c15fcee 后实测 339, +4 全部集中在该 PR 新增的 src/save-meta-response-conformance.test.ts 一个文件: - :115 TS2554 Expected 2-5 arguments, but got 1 - :119 TS6133 'LOG' is declared but its value is never read - :119 TS2304 Cannot find name 'appendFileSync' - :119 TS2304 Cannot find name 'OUT' tests 126 -> 127。其余 33 条纹丝不动(合计 2022 raw errors,无一超出记录值,也没有 一行 can-be-lowered —— 34 条全部与实测严格相等)。 那两条 TS2304 已在 note 里点名:名字都解析不到,那一行根本跑不起来,不是类型讲究 问题 —— 但那是 #5861 自己要修的,不是本台账要修的,所以只记录、不代修。 按非确定性假说已证伪(前一轮双跑逐字节相同),本轮不再双跑。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW
Fixes #5278
前提核验(先做,结论:成立,且比 issue 报的更普遍)
派发令要求先验「重测在 CI 的成本可承受」。落点查清了:
check-type-check-coverage.mjs目前只在.github/workflows/lint.yml的typecheckjob(lint.yml:487)跑一次,而同一个 job 后面就有Build workspace packages(turbo run build),以及Type check workspace packages。也就是说「需要构建产物」的重测有一个天然落点:同一个 job 的构建步骤之后,不必新开 job、不必重复付构建的钱。CI 实测(run 31081897969):
Build the ledgered packages' dependenciespnpm check:type-check-debt(重测本体)TypeScript Type Checkjob 全长新增的 build 步骤在 CI 上是全缓存命中,成本为零;重测本体 3m19s。远低于派发令给的「大于 10 min 且无法复用」的否决线,所以按裁决实现选项 1。
顺带核了 issue 引用的当前状态:
scripts/check-type-check-coverage.mjs昨天确实被 PR #5725 动过(#5561 的resumeAuthority),service-automation的 note 已经因此改写过一次,但数字仍是 2、errors断言仍是纯存在性检查 —— issue 的前提原封不动地成立。问题
闸门只断言「有一条台账、数字为正」:
errors: 28和errors: 1对它完全等价。包这一层对新增 debt 是关着的,错误条数这一层不是 —— 台账从不复测,所以一个新测试文件带进来的错误没有任何一道闸会看见。AGENTS.md 写着「DEBT is frozen debt, not a permission slip. Every entry below was measured」,而一个悄悄漂了 2.25 倍的数字不再描述它声称冻结的那笔债。全量重测结果(sweep @
5ab08428)34 条全测:17 条低估、2 条高估、15 条精确,没有一条是因为债在缩小而失真的。下表「实测」列为本 PR 最终落账值;两条带「见下文竞态」的是在本 PR 生命周期内被 main 推着又校准过的。
@objectstack/metadata-protocol@objectstack/spec-monorepo(仓库根)@objectstack/core@objectstack/metadata@objectstack/service-knowledge@objectstack/service-analytics@objectstack/service-automation@objectstack/plugin-approvals(TEST_DEBT)@objectstack/objectql(TEST_DEBT)@objectstack/rest(TEST_DEBT)@objectstack/plugin-auth(TEST_DEBT)@objectstack/lint(TEST_DEBT)@objectstack/plugin-security(TEST_DEBT)@objectstack/formula(TEST_DEBT)@objectstack/trigger-record-change(TEST_DEBT)@objectstack/verify(TEST_DEBT)@objectstack/http-conformance(TEST_DEBT)@objectstack/runtime(TEST_DEBT)@objectstack/driver-mongodb(TEST_DEBT)新的 MEASURED 不变式
--re-measure对每条 DEBT 跑该包自己的tsc --noEmit -p .../tsconfig.json;对每条 TEST_DEBT,生成一份extends原配置、只去掉 test 排除项的临时兄弟配置再跑(生成在包目录内 —— tsconfig 的include/outDir/rootDir都相对声明它的那个文件解析,放到别处会把它们统统改指;finally里删除)。判定是不对称的,这是核心:
ℹ … can be lowered,不红。修错误不应该还要先改一个记账数字才能让 CI 变绿,否则台账就是在对它本该鼓励的工作收费。typecheckscript + 删条目,由 COVERED / RECONCILED 双向强制)。计数口径与台账里每个数字当初的量法一致(
grep -c "error TS"),只是加了--pretty false让它不依赖有没有 TTY。多行 elaboration 的缩进行不计数,无文件前缀的全局诊断计数。另有一道防呆:tsc 以非零码退出却没打出可识别诊断、或打出 TS5058/TS6053/TS18003 这类「读不到 project」的诊断时,抛错而不是记 0 —— 一个量不到东西却报告「改善了」的闸门比没有闸门更糟。note 的成分也一起重写了
漂的不只是数字,还有 note 描述的成分。
service-automation是最好的标本:记 2,note 逐字点名engine.test.ts:2547/2577的两条 TS2741 是「全部的债」,实测 5 条里多出来的 3 条是nested-region-parity.test.ts里测试用点号直读私有字段engine.flows的 TS2341 —— 不同文件、不同错误码、不同性质。一个「两个字面量缺字段」的 note 读起来是顺手就能毕业,实际却夹着「测试到底该不该读私有状态」这一类判断。所以每条被抬高的 note 都按实测成分重写(错误码直方图 + 集中的文件),闸门的报错文案也直接要求这件事。归因不了的就明说 —— 本仓库的 clone 是浅的,拿不到逐文件 blame,所以统一落成「re-measured N at 某个具体 sha」加上可测的成分,不编造来源。
两个只有重测才看得见的事实一并记进 note:
@objectstack/driver-mongodb净变化 -1,但成分换掉了三分之二 —— 老 note 归咎于缺types:["node"]的 15 条 TS2591 全没了,冒出 7 条 TS1309。单看数字会以为什么都没发生。@objectstack/http-conformance的 4 条里有 2 条报在node_modules的.d.ts上,所以这条会随 lockfile 动而不只随本包代码动。没有过滤掉它们(台账里每个数字的含义就是 rawtsc --noEmit计数,过滤会让数字无法用文档里那条命令复现),而是在 note 里写明「这两条不是本包要修的债」。闸门在本 PR 自己身上生效了两次 —— 基漂移竞态
值得单独说,因为这是这道闸门实战的证据,不是演习。两次都不是实现错、不是 flaky,而是「冻结数早于新代码入 main」。
第一次:PR 级 merge commit(
@objectstack/rest,136 → 143)第二次推送后 CI 判红:
rest记 136,实测 140。pull_request运行编译的是「分支 merge 进当前 main」的树,而 sweep 之后 main 又落地了三个动packages/rest的 PR(#5808 / #5821 / #5806)。合并最新 main 后重量得 143(tests56 → 58),其余 33 条纹丝不动。第二次:合并队列(
@objectstack/objectql,333 → 335)PR 进入合并队列后又被踢出:
objectql记 333,队列基实测 334。同样是基漂移 —— 333 是在 07:41 的 main 上冻结的,而 #5802(registry.test.ts+116 行)与 #5836 在 07:52–07:53 才并进 main,随后 #5850 又落地(还是 objectql)。合并当前 main 后实测 335,两条增量逐一归因、都不是新类别:src/registry.test.ts(9 → 10)—— fix(objectql): wantOwner 翻为正面清单 + 注入 owning_business_unit_id (ADR-0117 D1) (#5677) #5802 新增的 registry 测试;src/engine-update-prior-read-scope.test.ts—— sweep 时该文件尚不存在,由 perf(objectql): update() 单 id 前置行门按对象判定需求(#5284),并校准 #4743 事实一的三处注释 #5850(update()的前置行门是全局的(hooks.get('afterUpdate').length > 0),任一对象注册 afterUpdate 就让所有对象的单 id update 多付一次读 #5284)新建。tests125 → 126;其余 33 条纹丝不动(合计 2018 raw errors,无一超出记录值)。先证伪了另一种解释。 队列管家留了一条可证伪前提:若 objectql 在 333/334 之间横跳,那就不是校准问题而是 tsc 计数不确定,需要先谈容差。实测:同一棵树连跑两次
--re-measure,输出逐字节相同(仅 wall-clock 行不同),两次不同的 main 基上各验一遍都如此。⇒ 计数是确定性的,校准是正确处置,不需要容差讨论。为什么队列这一半要单独写进 doc block
队列比普通
pull_request更尖锐,而且通常的补救手段在这里无效:队列是按「合并到队首」构建的,队首会随前面的条目落地而移动,所以重跑失败的 job 无法自愈(重跑复用同一个 merge ref,量的还是那个旧基),唯一修法是推新提交。代价还会外溢:本 PR 在队列里红循环期间,排在它后面的 #5834 / #5843 各被同一道新门禁连坐踢出一次,等本 PR 离开队列后两者原封不动通过 —— 所以再遇到这种红,应当先把 PR 撤出队列再修。这些连同双跑证伪法都已写进
MEASURED的文档块与objectql/rest的 note,下一个做全量重测的人不必再自己推导一遍。这个竞态是引导期的一次性成本,不是常态:本不变式上了 main 之后,引入错误的那个 PR 自己会红 —— 这正是它的目的。
反向验证(方向先定后验)
预期方向写在跑之前:把一条台账改到低于实测应当只让那一条红,把另一条改到高于实测应当只出一行
ℹ且不贡献红。同一次运行,
service-analytics7 → 4、service-automation5 → 9:恰好 1 红 + 恰好 1
ℹ,增长判红、缩小不判红、逐条独立,一次落实。端到端的三个:改动前的台账(即 origin/main 的数字)在新闸门下是 17 条红,exit=1,重测后为绿;以及上一节 CI 与合并队列上那两次真实的红 → 校准 → 绿。
验证
CI(run 31081897969,
TypeScript Type Checksuccess):校准后在最新 main(
e2bfa6c)合并树上本地复跑,同样全绿:exit 0,且一行
ℹ can be lowered都没有 —— 34 条全部与实测严格相等,没有留任何虚高余量。self-test 新增 11 个用例:6 个钉三个方向(涨 / 缩 / 归零)与逐条独立性,5 个钉计数器本身(多行 elaboration 不重复计数、无文件前缀的全局诊断要计、正文里出现
error TS字样但无错误码的散文不计)。其他:
node scripts/check-nul-bytes.mjsOK;控制字符自查无命中;check-workflow-status-functionsOK;eslint scripts/check-type-check-coverage.mjs干净;工作区无残留的tsconfig.debt-remeasure.json。changeset
写了 changeset(
.changeset/type-check-debt-ledger-ratchet.md),空 frontmatter —— 与本脚本已有的两份先例(per-package-typecheck-coverage.md、dogfood-typecheck-wired.md)一致:dev scripts / CI only,releases nothing,但改动本身需要留档。因此不需要skip-changeset标签,Check Changeset历次运行均为 success。范围
未碰任何包的源码、tsconfig 或 package.json 的
typecheckscript,未做任何包的毕业(派发令的 ⛔)。文件面始终是这 5 个:scripts/check-type-check-coverage.mjs、.github/workflows/lint.yml、package.json、AGENTS.md、.changeset/*.md。AGENTS.md改了 8 行:该文件是这道闸门唯一的对外说明,新增的命令与不对称语义不写进去就是 #4203 那种「没人跑的闸门会烂掉」。范围外发现
已另开 #5826(observation-class,
finding标签,未派单):TEST_DEBT 的tests字段是闸门testCoverage()每次运行都已经算出来的数字,却手写在台账里,19 条中 12 条已漂(runtime记 66、实算 101)。本 PR 把这些数字更新到了实测值,但机制没变 —— 台账文件自己在 #5286 的注释里已经写过「该导出的数字不要手写」这条结论,只是没推广到剩下 19 条。删字段是形状变更,留给分诊定。