Skip to content

ci(devx): type-check ledger 的余量可见化、一键抹平,并拒绝在未构建闭包上测量 (#6376) - #6510

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-6376-ledger-surplus
Aug 8, 2026
Merged

ci(devx): type-check ledger 的余量可见化、一键抹平,并拒绝在未构建闭包上测量 (#6376)#6510
hotlong merged 1 commit into
mainfrom
claude/issue-6376-ledger-surplus

Conversation

@hotlong

@hotlong hotlong commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #6376

结论先行:判据不是阈值,但也不是「有没有第二个看门人」

派单和出处方的分析都建立在一个布尔判据上:测试层在某个 tsc program 里 vs 被 tsconfig 排除。这个论证本身是对的,但实测表明它在账本内部恒为真,因此划不出子集:

结果 为什么是构造性的
TEST_DEBT 19/19 条 唯一看门人 entry 存在的充要条件就是 excludesTests(该包 typecheck script 调用的 tsconfig 没有一个读测试层),否则 TESTS_COVERED / RECONCILED 二者必红
DEBT 15/15 条 唯一看门人 entry 存在的充要条件是该包没有 typecheck script(否则 RECONCILED 红);根条目 @objectstack/spec-monorepo 同理没有 typecheck:root
全仓 tsconfig paths 映射 跨包 import 一律经由依赖的 dist/*.d.ts 解析,别的包的 program 不会把这些源文件拉进去

「有第二个看门人」的那一类,恰好就是「压根没有账本条目」的那一类,而没有条目就不可能有余量。所以方向 3 不是一次收窄,它展开后等于方向 1(全量抹平),也就正好是 #5278 判过不该付的那笔记账过路费。

出处方那句话仍然成立,而且比原文更强:余量从来都是许可额度,没有例外。今天全仓 11 条 entry 合计 310 条余量,占两个账本记录总量(2072)的 15%,其中 199 条集中在一条上。

实测表(main @ 1818998,全量 build 后 --re-measure,两次逐字节一致)

条目 账本 记录 实测 余量 唯一看门人
@objectstack/plugin-approvals TEST_DEBT 547 348 -199
@objectstack/plugin-auth TEST_DEBT 131 106 -25
@objectstack/lint TEST_DEBT 42 19 -23
@objectstack/rest TEST_DEBT 163 144 -19
@objectstack/plugin-security TEST_DEBT 21 9 -12
@objectstack/service-storage DEBT 52 42 -10 ✅(无 typecheck script,源码层无人读)
@objectstack/mcp TEST_DEBT 63 53 -10 ✅(派单点名待确认,已实测确认排除测试层)
@objectstack/runtime TEST_DEBT 227 220 -7
@objectstack/objectql TEST_DEBT 355 353 -2
@objectstack/service-automation DEBT 5 3 -2
@objectstack/http-conformance TEST_DEBT 4 3 -1
其余 23 条 0

⚠️ 正文表格(测于 d8e8d9cbc)已全面失效:5 条变 11 条,最大余量从 -19 变 -199,plugin-approvals / plugin-auth / plugin-security 是新出现的。两条未确认项已实测:service-storage 记的是 DEBT(源码层),它同样没有第二个看门人;@objectstack/mcp 确实排除测试层。

⚠️ 一个比余量更靠底层的发现:这个「实测值」原本是不可复现的

派单方转来同一条 entry(@objectstack/lint)当天在并行 agent 之间流通的三个读数:19 / 39 / 147。我复现了其中的成因:

闭包已构建   19   (TS7006 x11, TS2835 x4, TS6059 x4)
闭包未构建  147   (TS2307 x71 —— cannot find module —— 外加它引发的
                   级联 TS7006 x38, TS18046 x20, ...)

同一棵树、同一个 commit、相差 7.7 倍,而两次运行都没有任何告警。 tsc 经由依赖的 dist/*.d.ts 解析工作区导入,闭包没构建时测量不会失败,只会测到另一个世界;而且偏差在两个方向上都无界 —— 未解析的 import 让每个符号退化为 any,凭空造出 TS2307/TS7006 的同时抹掉真正的结构性不匹配(TS2345/TS2322)。driver-mongodb 那 33 条 TS2345 正是这一类,在未构建的 @objectstack/spec 面前会实测为 0。

这直接决定了方向:一个不可复现的量,撑不起任何以「记录值 vs 实测值」为判据的政策,而方向 1 / 方向 2 都要求人手写下一个实测数字。所以本 PR 先让这个数字变得可信。

同一环境内它是确定性的:两次全量 --re-measure 逐字节一致(1762 total),所以这不是 tsc 的不确定性,是环境/树的差异 —— 正是 ledger 头部要求先证伪的那一条。

本 PR 做了什么(方向:auto-lowering,不判红)

没有把余量判红。#5278 的裁决继续成立,改进仍然全绿落地。做的是三件让余量可见、可信、一键可抹平的事:

  1. 可见 —— 余量被量化并在绿色运行上报告。 每条 ℹ 现在直说这道口子允许多少条回归静默通过,收尾行汇总全仓:

    surplus: 310 raw error(s) across 11 entr(ies) sit BELOW their recorded ceiling
    — that many regressions can land in layers no other gate reads without this one
    saying anything (#6376). Close it with `pnpm check:type-check-debt --lower`.
    

    「看不见的额度」才是伤人的部分。

  2. 可一键抹平 —— --lower 把实测值写回账本。 抹平一条 entry 不再需要人手敲一个实测数字(见上一节:人手敲的数字来自一个可能不是 CI 那个的环境)。实测跑过一次全量:11 条 entry 精确改写 11 处 errors:,notes 与结构零损伤,并正确区分了同名跨账本@objectstack/rest(DEBT 2 未动,TEST_DEBT 163 → 144)。实测值为 0 的条目不写 —— 那是毕业,写 0 会让下一次结构闸门直接红。

  3. 可信 —— 拒绝在未构建的依赖闭包上测量。 前置检查(每个 workspace: 依赖是否有已构建的类型入口),而不是扫 tsc 输出里的 TS2307:账本里本来就有条目把 TS2307 记为真实债务(根条目 note 明写 TS2307 x17),一个分不清「记录在案的缺陷」和「测坏了的测量」的闸门,会去拒绝它本该测量的条目。

钉住缺陷本身的那对用例

自测里最重要的不是新增数量,是这一对 —— 同一棵树、同一次回归,只有天花板不同:

#6376 — a regression SMALLER than the surplus is invisible: 12 errors under a stale 43-ceiling
        recorded 43, actual 12  ->  0 problems(门禁沉默), surplus 31
#6376 — the same regression against the flattened ceiling is red: PR #6356 is why 10, not 43
        recorded 10, actual 12  ->  1 problem  "records 10 ... now reports 12 (+2)"

⚠️ 诚实标注:第一条是对现行行为的 characterisation,不是对某个修复的回归测试 —— 因为我有意没有改这个判决。它的价值是把「余量吞掉回归」变成一条记录在案的决定而不是一次意外:将来若要把余量判红,就是这条断言翻面,而第二条就是它该翻成的样子。

门禁

闸门 结果 证据
pnpm lint(ESLint) pass 无输出
pnpm check:type-check-debt pass 34 ledger entr(ies) re-measured in 199.3s, 1762 raw tsc error(s) total, none above its recorded number.
--self-test pass 23 semantic + 16 observation + 15 re-measure + 12 built-closure + 9 auto-lowering case(s) hold.(新增 21 条 + 4 条 #6376 用例)
pnpm check:nul-bytes pass scanned 6105 tracked text file(s) ... no raw ASCII control bytes
控制字节自扫(超出闸门扫描面) pass grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' → 0 hits
前置检查实测触发 pass 藏掉 packages/formula/dist--re-measure 立即拒绝并点名该包,未启动任何 tsc

反向验证(先声明,后运行)

反转 声明红 实测红 一致
R1 unbuiltClosure() 退回宽容分支 4 4
R2 余量统计退回(永不累加) 3 3
R3 lowerLedgerEntries() 退回恒等 2 4
R4 plannedLowerings() 去掉 0 值守卫 1 1
R5 棘轮本体退回(增长不再红) 5 5

R3 的分歧就是发现,不改声明:我按「负向断言会空过」把 3 条 applied === 0 的用例整体划为空过,实际只有 1 条是(空计划那条)。另外两条断言的是 skipped === 1 —— 「拒绝被记录下来了」是一条正向断言,反转后 skipped 变空数组照样红。教训:空过与否要按每条断言判,不是按用例的方向判。

R5 是专门用来证明 mongodb 那一对不空过的:把棘轮本体反转后,「同一次回归在 10 天花板下判红」立刻红,说明第二条确实在测东西。

R6(不是反转,是相反方向的一次测量):把余量改成判红,会有 4 条既有语义翻面,其中包括 a count that shrank is a note, never red —— 也就是说,#5278「改进不该付过路费」的裁决不只是散文,它是一条被钉住的断言;要把余量判红,就得先把它翻面。这条数据留给维护者决策用。

⛔ 有意没做的事

留给维护者的一个判决

维护者的推进意见是「auto-lowering(或 surplus ceiling),而不是让改进变红」。本 PR 交付了 auto-lowering 的机制,但它是自愿的;而实测给出一个不利于「自愿」的证据:ℹ 提示自 #5278 落地起每次 CI 都在打印,五条 entry 的 note 自己写着「tighten immediately after landing」,至今一条都没被抹平,余量反而从 5 条涨到 11 条。也就是说,纯提示性的机制在这个仓库里已被实测证伪

所以真正的判决是:记录值过期,要不要变成红?

  • A. 维持自愿(本 PR 现状)。 零新增红面、零竞态;但保证不了紧致度,而上面那条证据说它大概率不会被自觉执行。
  • B. 强制 auto-lowering(记录值必须等于实测值,修复即一条命令)。 唯一能保证紧致的形状;代价是改进方要多跑一趟,并且必须先把 11 条抹平(被禁止的攒批)。可行路径是排序而非阈值:先合本 PR,再用 --lower 一条一 PR 抹平,最后再开强制。
  • C. surplus ceiling(余量上限)。 仍是阈值,K 值任意,激励方向同样是反的。

我的建议是 B,但必须按上面的排序执行,且只在 11 条全部抹平后再开;在那之前保持 A。理由:(1)真实业务需求 —— 310 条许可额度里有 199 条压在 plugin-approvals 这个隐藏测试层最大的包上,而 mongodb 已经实测证明这种额度会吞掉真实回退;(2)长期健康 —— 唯一看门人的天花板必须是紧的,否则这两个账本只是在声称冻结;(3)让 AI 写的代码难以出错 —— 强制 + --lower 意味着数字永远由做过测量的工具写下,而不是由人从一个可能不是 CI 的环境里抄来,这正是本 PR 第三项修复要解决的问题。⛔ 但 B 是判决,不是我该替维护者做的猜测,所以本 PR 不实施。


Generated by Claude Code

`DEBT`/`TEST_DEBT` 的「记录值高于实测值」不是记账滞后,而是一张**实打实的许可
额度**:两个账本里的每一条 entry,都是它所度量那一层的**唯一看门人**(DEBT 只
在没有 `typecheck` script 时存在,TEST_DEBT 只在没有任何被调用的 tsconfig 读测
试层时存在),所以余量敞着的时候,任何不超过余量的回归都会全绿落地。
driver-mongodb 记 43 实测 10,把 `aggregate` 签名回退实测出的 12 条整个吞掉 ——
一次正当的改进(#6210 消掉 33 条)顺手把下一个人的 pin 变哑了。

本 PR **不**把余量判红(#5278 的裁决继续成立,改进仍然全绿落地),而是:

- **可见**:每条 ℹ 提示带上「这道口子现在允许多少条回归静默通过」,并在绿色
  收尾行汇总全仓余量。实测 main @ 1818998:11/34 条 entry 合计 310 条余量,占
  两个账本记录总量的 15%,其中 199 条集中在 plugin-approvals 一条上。
- **可一键抹平**:`--lower` 把实测值写回账本,抹平一条 entry 不再需要人手敲一
  个实测数字。实测值降到 0 的条目**不写**(那是毕业,不是更低的天花板)。
- **拒绝在未构建的依赖闭包上测量**:tsc 通过依赖的 `dist/*.d.ts` 解析工作区
  导入,闭包没构建时测量不会失败,只会**测到另一个世界** —— 同一棵树同一个
  commit,packages/lint 构建后实测 19、未构建实测 147(TS2307 x71 + 级联)。
  偏差在两个方向上都无界,所以这里改为**前置拒绝**而不是扫 tsc 输出(账本里
  本来就有条目把 TS2307 记为真实债务)。

自测新增 21 条(built-closure 12 + auto-lowering 9)与 4 条 #6376 用例,其中
driver-mongodb 那一对是把「余量吞掉回归」这件事本身钉下来:同一棵树同一次回
归,天花板 43 时门禁沉默、天花板 10 时判红。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 2:45am

Request Review

@hotlong hotlong added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 8, 2026 — with Claude
@github-actions github-actions Bot added the size/l label Aug 8, 2026
@hotlong
hotlong marked this pull request as ready for review August 8, 2026 02:46
@hotlong
hotlong enabled auto-merge August 8, 2026 02:46
@hotlong
hotlong added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 61478b9 Aug 8, 2026
23 checks passed
@hotlong
hotlong deleted the claude/issue-6376-ledger-surplus branch August 8, 2026 03:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants