From c639e0e4a8824d65965935e5ad5fe6ab83819497 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 06:47:39 +0000 Subject: [PATCH 1/8] ci(dx): re-measure the type-check DEBT/TEST_DEBT ledger on every run (#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 5ab08428 -- 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 Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW --- .changeset/type-check-debt-ledger-ratchet.md | 56 +++ .github/workflows/lint.yml | 41 +- AGENTS.md | 9 + package.json | 1 + scripts/check-type-check-coverage.mjs | 499 +++++++++++++++++-- 5 files changed, 557 insertions(+), 49 deletions(-) create mode 100644 .changeset/type-check-debt-ledger-ratchet.md diff --git a/.changeset/type-check-debt-ledger-ratchet.md b/.changeset/type-check-debt-ledger-ratchet.md new file mode 100644 index 0000000000..fc9739ac42 --- /dev/null +++ b/.changeset/type-check-debt-ledger-ratchet.md @@ -0,0 +1,56 @@ +--- +--- + +ci(dx): `scripts/check-type-check-coverage.mjs` 的 DEBT / TEST_DEBT 台账数字现在会被**重测**——实测 > 记录即红(#5278)。Dev scripts / CI only;releases nothing。 + +原来的闸门只断言「有一条台账、数字为正」: + +```js +if (!entry || typeof entry.errors !== 'number' || entry.errors <= 0) { ... } +``` + +也就是说 `errors: 28` 和 `errors: 1` 对它完全等价,台账**从不复测**。包这一层对新增 debt 是关着的,错误**条数**这一层不是——一个新测试文件带进来的错误没有任何一道闸会看见。AGENTS.md 写着「DEBT is frozen debt, not a permission slip. Every entry below was measured」,而一个已经悄悄漂了 2.25 倍的数字不再描述它声称冻结的那笔债:记 28 的条目读起来像「快毕业了」,实际成本是它的两倍多。 + +本次在 `5ab08428` 上把 34 条台账**全部重测**,漂移比 issue 报的更普遍: + +| | 记录 | 实测 | +|:---|---:|---:| +| `@objectstack/metadata-protocol` | 28 | 63 | +| `@objectstack/spec-monorepo`(仓库根) | 50 | 80 | +| `@objectstack/objectql`(TEST_DEBT) | 219 | 333 | +| `@objectstack/plugin-approvals`(TEST_DEBT) | 467 | 547 | +| `@objectstack/service-analytics` | 3 | 7 | +| `@objectstack/service-automation` | 2 | 5 | +| …… 共 17 条低估 | | | +| `@objectstack/runtime`(TEST_DEBT) | 220 | 218 | +| `@objectstack/driver-mongodb`(TEST_DEBT) | 44 | 43 | + +17 条低估、2 条高估、15 条精确。**没有一条**是因为债在缩小而失真的。 + +## 新的 MEASURED 不变式 + +`--re-measure` 对每条 DEBT 跑该包自己的 `tsc --noEmit -p /tsconfig.json`,对每条 TEST_DEBT 生成一份 `extends` 原配置、只去掉 test 排除项的临时兄弟配置再跑(临时文件在 `finally` 里删除)。判定是**不对称**的,这是本次的核心: + +- 实测 **>** 记录 → **红**。这才是棘轮。 +- 实测 **<** 记录 → 打印一行 `ℹ … can be lowered`,**不红**。修错误不应该还要先改一个记账数字才能让 CI 变绿,否则台账就是在对它本该鼓励的工作收费。 +- 实测 **= 0** → 报告为 graduation candidate(毕业仍然是一次显式 PR:加 `typecheck` script + 删台账条目,由 COVERED / RECONCILED 两个方向共同强制)。 + +## 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 5ab08428」,不编造成分。 + +另外记录两个重测才看得见的事实:`@objectstack/driver-mongodb` 净变化是 -1,但成分换掉了三分之二(老 note 归咎于缺 `types:["node"]` 的 15 条 TS2591 全没了,冒出 7 条 TS1309)——单看数字会以为什么都没发生;`@objectstack/http-conformance` 的 4 条里有 2 条报在 `node_modules` 的 `.d.ts` 上,所以这条会随 lockfile 动而不只随本包代码动,已在 note 里写明。 + +## 落点与成本 + +便宜的结构检查留在原地(只读 package.json / tsconfig.json,亚秒级,跑在 build 之前)。重测这一半需要各包依赖的 `dist/*.d.ts`,所以挂在 `lint.yml` 的 `typecheck` job 里、build 步骤**之后**——这个 job 本来就付了构建的钱。build filter 顺带扩到嵌套包组(`packages/services/*` 等):多数台账包没有 `typecheck` script,从来没进过 turbo 的任务图,它们的依赖也就不会被建。 + +实测重测本身 **~4 分钟**(34 个 project,顺序执行;并行 tsc 是拿 wall clock 换一个刚建完整个 workspace 的 job 上的 OOM 风险)。 + +## 反向验证 + +方向是先定后验的,两个方向都验了:把一条台账改到**低于**实测(`service-analytics` 7 → 4)、另一条改到**高于**实测(`service-automation` 5 → 9),同一次运行 exit=1,恰好 1 条红(前者)+ 恰好 1 条 `ℹ`(后者)——增长判红、缩小不判红、逐条独立,三件事一次落实。改动前的台账(即 origin/main 的数字)在新闸门下是 17 条红,重测后为绿。 + +self-test 新增 11 个用例:6 个钉住三个方向(涨/缩/归零)与逐条独立性,5 个钉住计数器本身——多行 elaboration 缩进行不能被重复计数(一条 TS2322 能打印 5 行),无文件前缀的全局诊断要计数,而正文里出现「error TS」字样但没有错误码的散文不能计数。 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 07b6bb505e..d6aaddde5a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -482,7 +482,10 @@ jobs: # failure: every package either declares `typecheck` (run by the turbo # step below) or carries a measured DEBT/EXEMPT entry in the script's # ledger, reconciled in both directions so the ledger can only shrink. - # Reads package.json files only; no build, sub-second. + # Reads package.json files only; no build, sub-second. The other half of + # this gate — re-running tsc against each ledger number — needs the built + # dist/*.d.ts and therefore lives after the build step, near the bottom of + # this job ("Re-measure the type-check DEBT / TEST_DEBT ledger"). - name: Check every package is type-check covered or ledgered run: pnpm check:type-check-coverage @@ -647,6 +650,42 @@ jobs: - name: Type check workspace packages run: pnpm exec turbo run typecheck --filter='./packages/*' --filter='./packages/*/*' --filter='./apps/*' + # The MEASURED half of the coverage gate (#5278). The cheap structural + # check near the top of this job asserts that a package without a + # `typecheck` script carries a DEBT/TEST_DEBT entry with a positive number + # written down — and, until now, nothing more: `errors: 28` and + # `errors: 1` were equally acceptable to it, because the ledger was never + # re-measured. So a ledgered package's real error count could grow without + # bound while the gate reported success. It had: metadata-protocol + # recorded 28 and reported 63, service-analytics 3 -> 7, service-automation + # 2 -> 5, and the wholesale re-measure this step ships with found 17 of the + # 34 entries understated and not one overstated. A number that has drifted + # 2.25x no longer describes the debt it claims to freeze. + # + # Asymmetric, on purpose: a count ABOVE its recorded number fails, a count + # below prints an informational "can be lowered / graduation candidate" + # line and stays green. Fixing errors must not also require editing a + # bookkeeping number before CI will go green, or the ledger charges a toll + # on exactly the work it exists to encourage. + # + # Here rather than beside its structural half because it runs the real + # compiler over ~34 projects, and tsc resolves workspace imports through + # each dependency's built `dist/*.d.ts` — so it needs the build steps + # above, which this job already pays for. The build filter is widened to + # the nested package groups (packages/services/*, packages/drivers/*, + # packages/plugins/*, …) because most ledgered packages have no + # `typecheck` script and therefore never entered the turbo task graph that + # would otherwise have built their dependencies; it is a superset of what + # the steps above already built, so it is cache hits plus the remainder. + # Measured cost of the re-measure itself: ~4 min, sequential by design + # (parallel tsc processes trade wall clock for an OOM risk on a job that + # has just built the whole workspace). + - name: Build the ledgered packages' dependencies + run: pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' + + - name: Re-measure the type-check DEBT / TEST_DEBT ledger + run: pnpm check:type-check-debt + - name: Type check example apps run: pnpm --filter './examples/*' run typecheck diff --git a/AGENTS.md b/AGENTS.md index d7356b0750..4f5d0164ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,15 @@ workspace package declares a `typecheck` script or carries a measured DEBT/EXEMP in `scripts/check-type-check-coverage.mjs`. New packages must arrive covered; a package that graduates deletes its ledger entry in the same PR. +The ledger numbers are ratcheted too (`pnpm check:type-check-debt`, run in the same CI +job after its build step): every DEBT/TEST_DEBT count is re-run through `tsc --noEmit`, +and a count ABOVE its recorded number fails. Below is only an informational +"can be lowered" line — improvements never owe CI a bookkeeping edit. Before #5278 the +gate asserted only that *some* positive number was written down, so the real counts had +drifted up to 2.25x while it reported success. When a re-measure makes you raise an +entry, rewrite its `note` as well: the composition drifts too, and a note that still +names only the old errors reads as "nearly graduated" to the next author. + **Do not `exclude` `*.test.ts` / `*.spec.ts` from a package's `tsconfig.json`.** `tsc --noEmit` reads that config, so an exclusion there hides the tests from the check the `typecheck` script advertises — a green gate over source nothing read, which is the diff --git a/package.json b/package.json index 9b78044814..2e6556f240 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "check:workflow-status-functions": "node scripts/check-workflow-status-functions.mjs --self-test && node scripts/check-workflow-status-functions.mjs", "check:published-files": "node scripts/check-published-files.mjs --self-test && node scripts/check-published-files.mjs", "check:type-check-coverage": "node scripts/check-type-check-coverage.mjs --self-test && node scripts/check-type-check-coverage.mjs", + "check:type-check-debt": "node scripts/check-type-check-coverage.mjs --self-test && node scripts/check-type-check-coverage.mjs --re-measure", "check:driver-conformance": "node scripts/check-driver-conformance.mjs --self-test && node scripts/check-driver-conformance.mjs", "check:engine-double-contract": "node scripts/check-engine-double-contract.mjs --self-test && node scripts/check-engine-double-contract.mjs", "check:resume-authority-declared": "node scripts/check-resume-authority-declared.mjs --self-test && node scripts/check-resume-authority-declared.mjs", diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index 7efe8b5f93..bb2211b5c7 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -35,7 +35,8 @@ // purpose). Onboarding a package is supposed to make its `typecheck` mean // something, so the casts belong in the diff too. // -// node scripts/check-type-check-coverage.mjs +// node scripts/check-type-check-coverage.mjs # structural, sub-second +// node scripts/check-type-check-coverage.mjs --re-measure # + runs tsc per ledger entry // node scripts/check-type-check-coverage.mjs --self-test // // Invariants, per workspace package (the root workspace package included -- @@ -87,6 +88,32 @@ // RECONCILED in both directions: a DEBT/EXEMPT entry for a package that now // declares `typecheck`, or that no longer exists, is an error. // A ledger that can only accrete rots into a list nobody trusts. +// MEASURED (--re-measure only) every DEBT / TEST_DEBT number is RE-RUN, and +// a package whose real `tsc --noEmit` count now EXCEEDS its +// recorded one is an error. Without this the ledger asserted only +// that a positive number was written down: `errors: 28` and +// `errors: 1` were equally acceptable, so a package's real count +// could grow without bound while the gate reported success +// (#5278). Measured drift at filing time, all in one direction: +// metadata-protocol 28 -> 63, service-analytics 3 -> 7, +// service-automation 2 -> 5. +// +// Asymmetric on purpose. Growth is red -- that is the ratchet. +// SHRINKAGE is an informational line, never red: making a package +// better must not also make CI fail until someone edits a number, +// or the ledger starts charging a toll on exactly the work it +// exists to encourage. A count that reaches 0 is reported as a +// graduation candidate, and graduating is still a deliberate PR +// (add the `typecheck` script, delete the entry -- COVERED and +// RECONCILED are what force the pair). +// +// What drifts is not only the NUMBER but the note's COMPOSITION: +// service-automation's note named `engine.test.ts:2547/2577` as +// the whole debt while three TS2341 in a different file, from an +// unrelated PR, had joined it. So when this invariant makes you +// raise a count, rewrite the note to match what the pile is now +// made of -- and when the delta cannot be attributed, say so in +// the note rather than inventing composition. // // The root is the one asymmetry: its `typecheck` script is the workspace // aggregator, so its OWN top-level TypeScript is covered by a `typecheck:root` @@ -94,11 +121,12 @@ // // DEBT is frozen debt, not a permission slip. Every entry below was measured // by running the package's own `tsc --noEmit` on main (see the issue for the -// code-tier / config-tier / noise split -- raw counts here include all three). -// To onboard a package: fix (or config-fix) its errors, add -// `"typecheck": "tsc --noEmit"` to its package.json, and delete its entry -// here in the same PR. Deleting the entry without the script fails COVERED; -// keeping the entry alongside the script fails RECONCILED. +// code-tier / config-tier / noise split -- raw counts here include all three), +// and every entry is RE-MEASURED by `--re-measure` (the MEASURED invariant), so +// "frozen" is now enforced rather than asserted. To onboard a package: fix (or +// config-fix) its errors, add `"typecheck": "tsc --noEmit"` to its package.json, +// and delete its entry here in the same PR. Deleting the entry without the +// script fails COVERED; keeping the entry alongside the script fails RECONCILED. // // TEST_DEBT is the same discipline for the second hole. The first pass of this // gate (#4324) counted a package covered the moment it declared `typecheck` -- @@ -107,7 +135,8 @@ // a green check. `spec` alone hid 902 across 272 test files. To onboard: drop // the exclusion from tsconfig.json and delete the entry here in the same PR. -import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; import { join, posix, resolve } from 'node:path'; // Anchored to the script, not to cwd: the verdict must not depend on where the @@ -129,8 +158,13 @@ const PIN_DIRECTIVE = /^[ \t]*(?:\/\/|\/\*|\*)[ \t]*@ts-expect-error\b/m; const PIN_ISSUE = 'https://github.com/objectstack-ai/objectstack/issues/5286'; // Package name -> { errors, note? }. `errors` is the raw `tsc --noEmit` count -// measured per package on main @ b07d829 (2026-07-31), re-measured after the -// NodeNext repair below. +// measured per package. Seeded on main @ b07d829 (2026-07-31), re-measured +// after the NodeNext repair below, and re-measured WHOLESALE on main @ 5ab08428 +// (2026-08-06) when #5278 found the ledger had been drifting untested since: +// 7 of these 15 entries were understated, none overstated. Notes carry the +// composition as measured at that sha; where the delta could not be attributed +// (this repo's clone is shallow, so per-file blame was not available) the note +// says so rather than inventing a story for it. // Raw counts include all three of #4311's tiers -- code-tier (real defects), // config-tier (the check itself misconfigured: TS2591/TS2584 missing // `types:["node"]`, TS2835/TS2307 module resolution) and noise (TS7006 @@ -155,8 +189,11 @@ const DEBT = { note: 'code-tier 11 (TS2493 tuple indexing) + 2 config-tier.', }, '@objectstack/core': { - errors: 91, - note: 'code-tier 3; the rest is config-tier (TS2835/TS2347 module resolution) and noise (TS7006).', + errors: 98, + note: 'code-tier 3 (TS18046/TS2739/TS2352); the rest is config-tier 23 (TS2835 x22 / TS2347 module ' + + 'resolution) and noise 72 (TS7006 x71, TS6133). Re-measured 98 at 5ab08428, up from 91: the ' + + 'code-tier count is UNCHANGED at 3, so the whole +7 landed in the NodeNext/implicit-any residue ' + + '-- which is the tier the note at the top of this ledger says to fix first, not last.', }, '@objectstack/hono': { errors: 3, @@ -167,12 +204,20 @@ const DEBT = { note: 'code-tier 3 (TS2353) + 1 config-tier (TS2550 lib).', }, '@objectstack/metadata': { - errors: 87, - note: 'code-tier 31 (TS2345/TS2353); the rest is config-tier (TS2835) and noise (TS7006).', + errors: 92, + note: 'code-tier 34 (TS2345 x30, TS2322 x4); config-tier 24 (TS2835); noise 34 (TS7006 x33, TS6133). ' + + 'Re-measured 92 at 5ab08428, up from 87. Composition moved as well as the count: the note used to ' + + 'name TS2353, which is gone, and TS2322 has taken its place. Two thirds of the pile sits in ' + + 'metadata.test.ts (34) and register-notifies-watchers.test.ts (16).', }, '@objectstack/metadata-protocol': { - errors: 28, - note: 'code-tier 9 (was read as 2 at 21 raw); the rest is config-tier (TS2835) and noise (TS7006).', + errors: 63, + note: 'code-tier 40 (TS2322 x34, TS2532/TS2493 x2 each, TS2353, TS2339); config-tier 10 (TS2835 x9, ' + + 'TS2550); noise 13 (TS7006). Re-measured 63 at 5ab08428 -- the 2.25x drift that opened #5278, and ' + + 'the entry whose note was most misleading: it read "code-tier 9, the rest config-tier and noise", ' + + 'while code-tier alone is now 40. 27 of the TS2322 are in protocol.stored-migration.test.ts and 10 ' + + 'in seed-loader-multi-value-reference.test.ts, so this is concentrated debt in two files rather ' + + 'than a package-wide drizzle -- read it as two repairs, not as forty.', }, '@objectstack/observability': { errors: 11, @@ -183,23 +228,33 @@ const DEBT = { note: 'code-tier 2 (TS2345).', }, '@objectstack/service-analytics': { - errors: 3, - note: 'code-tier 2 (TS7053) + 1 noise.', + errors: 7, + note: 'code-tier 6 (TS2339 x4, TS7053 x2) + 1 noise (TS6133). Re-measured 7 at 5ab08428, up from 3. ' + + 'The 4 TS2339 are all in __tests__/measure-source-field-gate.test.ts, a file added after the entry ' + + 'was written -- the package layer of this gate is closed to new debt, the ERROR-COUNT layer was ' + + 'not, so a new test file walked its errors in past everything (#5278).', }, '@objectstack/service-automation': { - errors: 2, - note: 'code-tier 2 (TS2741: engine.test.ts:2547/2577 build a descriptor literal missing a required ' - + 'field, the #4198 discovery that opened #4311). The missing field TS names moved from ' - + 'resumeAuthority to handlerContract in #5561, which made resumeAuthority optional; both literals ' - + 'omit both, and TS reports one at a time. The count is unchanged by that.', + errors: 5, + note: 'code-tier 5. Two are the TS2741 this note used to describe as the whole debt: ' + + 'engine.test.ts:2547/2577 build a descriptor literal missing a required field, the #4198 discovery ' + + 'that opened #4311 (the missing field TS names moved from resumeAuthority to handlerContract in ' + + '#5561, which made resumeAuthority optional; both literals omit both, and TS reports one at a ' + + 'time). The other 3 are TS2341 in src/nested-region-parity.test.ts, where the tests dot-read the ' + + 'private `engine.flows` -- not `engine[\'flows\']`, not `as any`. Re-measured 5 at 5ab08428. This is ' + + 'the specimen #5278 cites for composition drift: an entry reading "2, two descriptor literals" ' + + 'looks like a free graduation, while the real residue includes a decision about whether tests may ' + + 'read private state at all.', }, '@objectstack/service-cluster': { errors: 1, note: 'code-tier 1 (TS2322).', }, '@objectstack/service-knowledge': { - errors: 8, - note: 'code-tier 3 (TS2339/TS2352/TS2493); the rest config-tier and noise.', + errors: 10, + note: 'code-tier 3 (TS2339/TS2352/TS2493); config-tier 3 (TS2835); noise 4 (TS7006). Re-measured 10 at ' + + '5ab08428, up from 8; code-tier is unchanged at 3, so the +2 is config-tier/noise. 8 of the 10 are ' + + 'in __tests__/knowledge-service.test.ts.', }, '@objectstack/service-settings': { errors: 13, @@ -210,8 +265,14 @@ const DEBT = { note: 'code-tier 5 (TS2339/TS2347); the rest is config-tier (TS2835) and noise (TS7006).', }, '@objectstack/spec-monorepo': { - errors: 50, - note: 'the workspace root itself: code-tier 2 (TS2304); the rest is config-tier (TS2307/TS2591/TS2584 -- the root tsconfig has no `types:["node"]`) and noise.', + errors: 80, + note: 'the workspace root itself: code-tier 4 (TS2304 x2, TS2339 x2); config-tier 68 ' + + '(TS2591 x28 / TS2584 x22 -- the root tsconfig still has no `types:["node"]` -- plus TS2307 x17 ' + + 'and TS2550); noise 8 (TS7006 x7, TS6133). Re-measured 80 at 5ab08428, up from 50. This entry ' + + 'drifts differently from a package: the root program is `scripts/` and the top-level configs ' + + '(everything outside packages/apps/examples), so it grows whenever the repo gains a script -- ' + + 'scripts/check-test-typecheck.mts alone accounts for 29 of the 80, and the analytics-reconcile ' + + 'tree for 32. Almost all of it is one missing `types:["node"]`, not 80 defects.', }, }; @@ -240,30 +301,84 @@ const EXEMPT = { // stronger than the frozen package-level number this ledger could hold. The // number that used to sit here (272 files / 902 errors) was also stale by 23 // files, which is the other argument for a measurement the gate derives. +// +// Re-measured wholesale on main @ 5ab08428 (2026-08-06) with the DEBT ledger +// above, for the same reason (#5278): 10 of these 19 `errors` numbers were +// understated, 2 overstated, 7 exact. `errors` is now re-run by --re-measure; +// `tests` is not, and it had drifted just as far in the same direction (66 -> +// 101 for runtime, 87 -> 125 for objectql) because adding a test file to an +// excluded package is invisible to everything. Both fields are accurate as of +// that sha; only one of them is enforced, which is filed rather than silently +// widened here. const TEST_DEBT = { '@objectstack/plugin-approvals': { - tests: 13, - errors: 467, - note: 'TS2339 x255, TS2345 x188. Larger than driver-sql; src is clean, so the whole pile is test-only and invisible to every gate today.', + tests: 19, + errors: 547, + note: 'TS2339 x296, TS2345 x213, TS2550 x20, TS18048 x10. Re-measured 547 at 5ab08428, up from 467. ' + + 'Still larger than driver-sql ever was, and still entirely test-only (src is clean), so nothing ' + + 'but this ledger has ever seen it. 443 of the 547 are in one file, src/approval-service.test.ts.', + }, + '@objectstack/objectql': { + tests: 125, + errors: 333, + note: 'TS2339 x114, TS2554 x91 (wrong arity), TS7006 x47, TS2345 x19, TS2322 x12, TS2749 x11. ' + + 'Re-measured 333 at 5ab08428, up from 219 -- the largest absolute growth in either ledger. The ' + + 'shape held (TS2339/TS2554/TS7006 still lead) but every number roughly tripled, and the file count ' + + 'went 87 -> 125; src/engine.test.ts alone carries 103.', + }, + '@objectstack/runtime': { + tests: 101, + errors: 218, + note: 'TS18048 x89 (possibly-undefined), TS2345 x26, TS18046 x20, TS2339 x16, TS2493 x15. Src ' + + 'graduated in #4311 (declares `typecheck`); this is purely the hidden test layer. Re-measured 218 ' + + 'at 5ab08428, DOWN from 220 -- one of only two entries that shrank. The TS6133 x25 the old note ' + + 'named is down to x7, so unused-symbol cleanup happened somewhere in the test tree while ' + + 'possibly-undefined grew; the net -2 hides a much larger churn in both directions.', + }, + '@objectstack/rest': { + tests: 56, + errors: 136, + note: 'TS2835 x61 (NodeNext extensions), TS7006 x54, TS2554 x10, TS2550 x7. Also in DEBT. Re-measured ' + + '136 at 5ab08428, up from 105. Read the top-of-ledger NodeNext note before sizing this one: TS2835 ' + + 'and the implicit-any pile it causes are 115 of the 136, and they are one repair, not 115.', + }, + '@objectstack/plugin-auth': { + tests: 34, + errors: 129, + note: 'TS2493 x42 (tuple index out of range), TS18048 x24, TS2740 x19, TS2322 x11, TS2532 x9. ' + + 'Re-measured 129 at 5ab08428, up from 124; composition unchanged in shape. 63 sit in ' + + 'src/auth-manager.test.ts.', + }, + '@objectstack/mcp': { tests: 8, errors: 52, note: 'TS18046 x51 -- `error` is of type unknown, one catch-block idiom repeated. Re-measured 52 at 5ab08428, exact.' }, + '@objectstack/driver-mongodb': { + tests: 15, + errors: 43, + note: 'TS2345 x33, TS1309 x7, TS2550 x3. Re-measured 43 at 5ab08428, DOWN from 44 -- but the ' + + 'composition changed completely: the TS2591 x15 the old note pinned on a missing `types:["node"]` ' + + 'are all gone, and TS1309 (await in a non-async context) has appeared. A -1 delta over a ledger ' + + 'entry that turned over two thirds of its content is exactly why counts alone cannot be trusted ' + + 'to describe debt (#5278).', + }, + '@objectstack/lint': { tests: 61, errors: 30, note: 'TS7006 x20, TS2835 x6, TS6059 x4. Re-measured 30 at 5ab08428, up from 26; the +4 is TS6059 (a file outside rootDir), a class the old note did not list.' }, + '@objectstack/plugin-security': { tests: 34, errors: 21, note: 'TS2739 x8, TS2740 x5, TS2345/TS2322/TS2741 x2 each -- incomplete literals. Re-measured 21 at 5ab08428, up from 20.' }, + '@objectstack/formula': { tests: 16, errors: 17, note: 'TS2591 x6 (`process`), TS2345 x3, TS2352 x3, TS1470 x2, TS2339 x2. Re-measured 17 at 5ab08428, up from 12; the TS2591 half doubled, which is the missing `types:["node"]` again rather than five new defects.' }, + '@objectstack/trigger-record-change': { tests: 5, errors: 9, note: 'TS2353 x9 -- still the one unknown-property shape repeated, now in four files. Re-measured 9 at 5ab08428, up from 8.' }, + '@objectstack/verify': { tests: 4, errors: 8, note: 'TS2835 x4, TS7006 x4. Re-measured 8 at 5ab08428, up from 6; both classes are the NodeNext pair from the top-of-ledger note.' }, + '@objectstack/connector-mcp': { tests: 3, errors: 5, note: 'TS2339 x5. Re-measured 5 at 5ab08428, exact.' }, + '@objectstack/connector-openapi': { tests: 3, errors: 5, note: 'TS2339 x5. Re-measured 5 at 5ab08428, exact.' }, + '@objectstack/http-conformance': { + tests: 2, + errors: 4, + note: 'TS2307 x2, TS2304 x1, TS2740 x1. Re-measured 4 at 5ab08428, up from 1. Worth knowing before ' + + 'anyone tries to graduate it: 2 of the 4 are reported inside node_modules `.d.ts` files ' + + '(@better-auth/core, @better-fetch/fetch), so this entry moves with the lockfile and not only with ' + + 'this package\'s own code. Raw `tsc --noEmit` counts are what every number in these ledgers means, ' + + 'so they are counted here rather than filtered out -- but they are not this package\'s debt to fix.', }, - '@objectstack/runtime': { tests: 66, errors: 220, note: 'TS18048 x81 (possibly-undefined), TS2345 x26, TS6133 x25. Src graduated in #4311 (declares `typecheck`); this is now purely the hidden test layer.' }, - '@objectstack/objectql': { tests: 87, errors: 219, note: 'TS2339 x88, TS2554 x28 (wrong arity), TS7006 x25.' }, - '@objectstack/plugin-auth': { tests: 26, errors: 124, note: 'TS2493 x40 (tuple index out of range), TS18048 x24, TS2740 x18.' }, - '@objectstack/rest': { tests: 35, errors: 105, note: 'TS2835 x43 (NodeNext extensions), TS7006 x42. Also in DEBT.' }, - '@objectstack/mcp': { tests: 8, errors: 52, note: 'TS18046 x51 -- `error` is of type unknown, one catch-block idiom repeated.' }, - '@objectstack/driver-mongodb': { tests: 7, errors: 44, note: 'TS2345 x22, TS2591 x15 (`process` -- the test files need types:["node"] once included).' }, - '@objectstack/lint': { tests: 39, errors: 26, note: 'TS7006 x20, TS2835 x6.' }, - '@objectstack/plugin-security': { tests: 32, errors: 20, note: 'TS2739 x8, TS2740 x5 -- incomplete literals.' }, - '@objectstack/formula': { tests: 13, errors: 12, note: 'TS2345 x3, TS2352 x3, TS2591 x3.' }, - '@objectstack/trigger-record-change': { tests: 4, errors: 8, note: 'TS2353 x8 -- one unknown-property shape repeated.' }, - '@objectstack/verify': { tests: 2, errors: 6, note: 'TS7006 x4, TS2835 x2.' }, - '@objectstack/connector-mcp': { tests: 3, errors: 5, note: 'TS2339 x5.' }, - '@objectstack/connector-openapi': { tests: 3, errors: 5, note: 'TS2339 x5.' }, - '@objectstack/platform-objects': { tests: 8, errors: 3, note: 'TS2339 x2, TS7006 x1.' }, - '@objectstack/plugin-sharing': { tests: 11, errors: 3, note: 'TS6133 x2, TS18048 x1.' }, - '@objectstack/http-conformance': { tests: 2, errors: 1, note: 'TS2740 x1.' }, - '@objectstack/service-sms': { tests: 3, errors: 1, note: 'TS2493 x1.' }, - '@objectstack/connector-rest': { tests: 3, errors: 1, note: 'TS6133 x1.' }, + '@objectstack/platform-objects': { tests: 9, errors: 3, note: 'TS2339 x2, TS7006 x1. Re-measured 3 at 5ab08428, exact.' }, + '@objectstack/plugin-sharing': { tests: 13, errors: 3, note: 'TS6133 x2, TS18048 x1. Re-measured 3 at 5ab08428, exact.' }, + '@objectstack/service-sms': { tests: 3, errors: 1, note: 'TS2493 x1. Re-measured 1 at 5ab08428, exact.' }, + '@objectstack/connector-rest': { tests: 3, errors: 1, note: 'TS6133 x1. Re-measured 1 at 5ab08428, exact.' }, }; // Repo-relative path -> why this test file's `@ts-expect-error` directives are @@ -693,6 +808,177 @@ function evaluate(packages, root, state) { return problems; } +// --------------------------------------------------------------------------- +// MEASURED -- the re-measure half (#5278). +// +// Everything above reads package.json / tsconfig.json and finishes in under a +// second, which is why it sits before the build in lint.yml's typecheck job. +// Everything below runs the real compiler and therefore needs each package's +// DEPENDENCIES built (tsc resolves workspace imports through `dist/*.d.ts`) -- +// so it is opt-in behind `--re-measure` and wired into the same job AFTER its +// build step, rather than made the default and paid for on every gate run. +// --------------------------------------------------------------------------- + +/** + * One `tsc --noEmit` diagnostic line, counted the way every number in the + * ledgers above was originally counted (`| grep -c "error TS"`). A diagnostic's + * elaboration lines are INDENTED and carry no code of their own, so anchoring + * at a non-space start is what keeps a single 5-line TS2322 from counting five + * times. Global diagnostics (`error TS5055: ...`) have no file prefix at all and + * still count -- they are errors the same as any other. + */ +const TSC_ERROR_LINE = /^(?!\s)(?:.*\s)?error TS\d+: /; + +/** + * Diagnostics that mean the MEASUREMENT failed, not that the package has debt. + * A missing/unreadable project or an empty file list would otherwise be counted + * as one tidy little error and silently *lower* a package's number -- a gate + * that measures nothing and reports an improvement is worse than no gate. + */ +const TSC_SETUP_ERROR = /\berror TS(5058|5083|6053|18003|5012)\b/; + +/** Temp project used to lift a tsconfig's own test exclusion. Never committed. */ +const REMEASURE_CONFIG = 'tsconfig.debt-remeasure.json'; +const REMEASURE_ISSUE = 'https://github.com/objectstack-ai/objectstack/issues/5278'; + +/** @param {string} output */ +function countTscErrors(output) { + let n = 0; + for (const line of output.split(/\r?\n/)) if (TSC_ERROR_LINE.test(line)) n++; + return n; +} + +/** + * Run the repo's own tsc over one project and return its raw error count. + * `--pretty false` so the count does not depend on whether a TTY is attached; + * cwd is ROOT so reported paths are repo-relative however the gate was invoked. + * + * @param {string} project repo-relative path to a tsconfig + * @returns {number} + */ +function tscErrorCount(project) { + const bin = join(ROOT, 'node_modules', '.bin', 'tsc'); + if (!existsSync(bin)) { + throw new Error(`--re-measure needs the workspace's own tsc at ${bin}; run \`pnpm install\` first.`); + } + const run = spawnSync(bin, ['--noEmit', '--pretty', 'false', '-p', project], { + cwd: ROOT, + encoding: 'utf8', + maxBuffer: 256 * 1024 * 1024, + }); + if (run.error) throw new Error(`tsc could not be run for ${project}: ${run.error.message}`); + const output = `${run.stdout ?? ''}${run.stderr ?? ''}`; + if (TSC_SETUP_ERROR.test(output)) { + throw new Error(`tsc could not read ${project} -- the measurement is invalid, not zero:\n${output.trim()}`); + } + const errors = countTscErrors(output); + // Exit 0 means a clean program; anything else must have produced diagnostics + // we recognised. If it did not, tsc failed in a way this parser cannot see, + // and reporting 0 would quietly hand the package a graduation certificate. + if (run.status !== 0 && errors === 0) { + throw new Error( + `tsc exited ${run.status} for ${project} but printed no recognisable diagnostics -- ` + + `refusing to record 0:\n${output.trim().slice(0, 2000)}`, + ); + } + return errors; +} + +/** + * The DEBT number: what the package's OWN config reports today, which is exactly + * what `pnpm --filter exec tsc --noEmit` would print for an author sizing + * the package up. + */ +function measureDebt(dir) { + return tscErrorCount(posix.join(dir, 'tsconfig.json') || 'tsconfig.json'); +} + +/** + * The TEST_DEBT number: what the package reports once its tsconfig stops + * steering tsc away from its own tests. Written as a sibling project that + * `extends` the real one and re-declares `exclude` without the test globs -- + * a sibling, in the package's own directory, because tsconfig resolves + * `include`/`outDir`/`rootDir` relative to the file that DECLARES them, so a + * config generated anywhere else would silently repoint every one of them. + * + * Removed in a `finally`: a stray `tsconfig.*.json` left behind would be picked + * up by this very script's own tsconfig scan on the next run. + */ +function measureTestDebt(dir) { + const configPath = join(ROOT, dir, REMEASURE_CONFIG); + const raw = readFileSync(join(ROOT, dir, 'tsconfig.json'), 'utf8').replace(/^\s*\/\/.*$/gm, ''); + const parsed = JSON.parse(raw); + const kept = (parsed.exclude ?? []).filter((pattern) => !TEST_GLOB.test(pattern)); + writeFileSync(configPath, `${JSON.stringify({ extends: './tsconfig.json', exclude: kept }, null, 2)}\n`); + try { + return tscErrorCount(posix.join(dir, REMEASURE_CONFIG)); + } finally { + rmSync(configPath, { force: true }); + } +} + +/** + * Re-run every ledger number. Sequential on purpose: the ledgered packages are + * the big ones, and N parallel tsc processes on a 2-core runner trade wall clock + * for an OOM risk on the job that also just built the whole workspace. + * + * @returns {Array<{ledger: 'DEBT'|'TEST_DEBT', name: string, dir: string, recorded: number, actual: number}>} + */ +function measureLedgers(packages, rootName, state) { + const dirOf = new Map(packages.map((p) => [p.name, p.dir])); + dirOf.set(rootName, ''); // the workspace root is a member like any other + const measurements = []; + for (const [name, entry] of Object.entries(state.debt)) { + const dir = dirOf.get(name); + if (dir === undefined) continue; // RECONCILED already failed on this one + measurements.push({ ledger: 'DEBT', name, dir, recorded: entry.errors ?? 0, actual: measureDebt(dir) }); + } + for (const [name, entry] of Object.entries(state.testDebt)) { + const dir = dirOf.get(name); + if (dir === undefined) continue; + measurements.push({ ledger: 'TEST_DEBT', name, dir, recorded: entry.errors ?? 0, actual: measureTestDebt(dir) }); + } + return measurements; +} + +/** + * MEASURED's verdict, pure over already-taken measurements so the self-test + * pins the semantics without running a compiler. + * + * @param {Array<{ledger: string, name: string, recorded: number, actual: number}>} measurements + * @returns {{problems: string[], notes: string[]}} + */ +function evaluateMeasurements(measurements) { + const problems = []; + const notes = []; + for (const m of measurements) { + if (m.actual > m.recorded) { + problems.push( + `${m.name}: ${m.ledger} records ${m.recorded} raw tsc error(s), \`tsc --noEmit\` now reports ` + + `${m.actual} (+${m.actual - m.recorded}). ${m.ledger} is frozen debt, not a permission slip -- ` + + `the ledger is a ratchet and may only shrink (${REMEASURE_ISSUE}). Fix the new errors, or, if they ` + + `are genuinely irreducible today, raise the entry in ${SELF} AND rewrite its \`note\` to match what ` + + `the pile is now made of: the composition drifts too, and a note still naming only the old errors ` + + `reads as "nearly graduated" to the next author while something else entirely has moved in. ` + + `If the delta cannot be attributed, say that in the note rather than inventing composition.`, + ); + } else if (m.actual === 0 && m.recorded > 0) { + notes.push( + `${m.name}: ${m.ledger} records ${m.recorded}, and tsc now reports 0 -- graduation candidate. ` + + `Onboard it (add \`"typecheck": "tsc --noEmit"\`, or drop the test exclusion, and delete the ` + + `ledger entry in the same PR).`, + ); + } else if (m.actual < m.recorded) { + notes.push( + `${m.name}: ${m.ledger} records ${m.recorded}, tsc now reports ${m.actual} ` + + `(-${m.recorded - m.actual}) -- the entry can be lowered. Not an error: an improvement must not ` + + `have to pay a bookkeeping toll to land.`, + ); + } + } + return { problems, notes }; +} + /** The observed non-fixture state. */ function observed() { const turbo = JSON.parse(readFileSync(join(ROOT, 'turbo.json'), 'utf8')); @@ -946,6 +1232,100 @@ function selfTest() { if (got !== c.expect) failures.push(`configCovers — ${c.label}: expected ${c.expect}, got ${got}`); } + // MEASURED (#5278). The whole point of this invariant is a DIRECTION, so both + // directions are pinned: up is red, down is a note, equal is silence. A + // symmetric implementation would pass a "does it notice a change" test and + // still be wrong -- it would make every improvement red. + const driftCases = [ + { + label: 'a count that grew is red', + measurements: [{ ledger: 'DEBT', name: 'a', recorded: 28, actual: 63 }], + problems: [/a: DEBT records 28 raw tsc error\(s\).*now reports 63 \(\+35\)/s], + notes: [], + }, + { + label: 'a count that grew by one is red too — no tolerance band', + measurements: [{ ledger: 'TEST_DEBT', name: 'a', recorded: 2, actual: 3 }], + problems: [/a: TEST_DEBT records 2 raw tsc error\(s\).*now reports 3 \(\+1\)/s], + notes: [], + }, + { + label: 'a count that shrank is a note, never red', + measurements: [{ ledger: 'DEBT', name: 'a', recorded: 13, actual: 5 }], + problems: [], + notes: [/a: DEBT records 13, tsc now reports 5 \(-8\) -- the entry can be lowered/], + }, + { + label: 'a count that reached zero is a graduation candidate, not a failure', + measurements: [{ ledger: 'DEBT', name: 'a', recorded: 4, actual: 0 }], + problems: [], + notes: [/a: DEBT records 4, and tsc now reports 0 -- graduation candidate/], + }, + { + label: 'an unchanged count is silent — a green run says nothing at all', + measurements: [ + { ledger: 'DEBT', name: 'a', recorded: 91, actual: 91 }, + { ledger: 'TEST_DEBT', name: 'b', recorded: 467, actual: 467 }, + ], + problems: [], + notes: [], + }, + { + label: 'each entry is judged on its own — one growing does not mask another shrinking', + measurements: [ + { ledger: 'DEBT', name: 'grew', recorded: 3, actual: 7 }, + { ledger: 'DEBT', name: 'shrank', recorded: 9, actual: 8 }, + ], + problems: [/grew: DEBT records 3/], + notes: [/shrank: DEBT records 9, tsc now reports 8/], + }, + ]; + for (const c of driftCases) { + const got = evaluateMeasurements(c.measurements); + const ok = + got.problems.length === c.problems.length && + got.notes.length === c.notes.length && + c.problems.every((rx, i) => rx.test(got.problems[i])) && + c.notes.every((rx, i) => rx.test(got.notes[i])); + if (!ok) { + failures.push( + `evaluateMeasurements — ${c.label}: expected ${c.problems.length} problem(s) / ${c.notes.length} note(s) ` + + `matching, got ${JSON.stringify(got)}`, + ); + } + } + + // The counter is the other half that can be silently wrong: over-count and + // main goes red for nothing, under-count and the ratchet hands out free + // headroom. Multi-line elaborations are the trap -- one TS2322 can print five + // lines, four of them indented. + const countCases = [ + { label: 'no output is no errors', output: '', expect: 0 }, + { + label: 'one diagnostic per line', + output: 'packages/a/src/x.ts(1,2): error TS2345: Argument of type X.\npackages/a/src/y.ts(3,4): error TS7006: Parameter implicitly any.', + expect: 2, + }, + { + label: 'indented elaboration lines belong to the diagnostic above them', + output: + "packages/a/src/x.ts(1,2): error TS2322: Type 'A' is not assignable to type 'B'.\n" + + " Type 'A' is not assignable to type 'C'.\n" + + " Types of property 'p' are incompatible.\n", + expect: 1, + }, + { label: 'a global diagnostic with no file prefix still counts', output: 'error TS5055: Cannot write file.', expect: 1 }, + { + label: 'prose mentioning the phrase mid-line without a code does not count', + output: 'Checked 40 files, no error TS reported by the previous run.', + expect: 0, + }, + ]; + for (const c of countCases) { + const got = countTscErrors(c.output); + if (got !== c.expect) failures.push(`countTscErrors — ${c.label}: expected ${c.expect}, got ${got}`); + } + if (failures.length) { console.error(`✗ check:type-check-coverage --self-test — ${failures.length} failure(s)\n`); for (const f of failures) console.error(' • ' + f); @@ -953,7 +1333,8 @@ function selfTest() { } console.log( `✓ check:type-check-coverage --self-test — ${cases.length} semantic case(s) + ` + - `${namedCases.length + coverCases.length} observation case(s) hold.`, + `${namedCases.length + coverCases.length} observation case(s) + ` + + `${driftCases.length + countCases.length} re-measure case(s) hold.`, ); } @@ -985,3 +1366,25 @@ console.log( ` test layer: ${Object.keys(TEST_DEBT).length} package(s) still exclude their own tests ` + `(${testDebtFiles} files, ${testDebtErrors} frozen raw errors in TEST_DEBT).`, ); + +// MEASURED runs only when asked, and only after the structural verdict above is +// clean: a ledger entry naming a package that no longer exists has nothing to +// measure, and a wall of tsc output would bury the real failure. Reported after +// the summary so the two verdicts read in the order they were reached. +if (process.argv.includes('--re-measure')) { + const started = Date.now(); + const measurements = measureLedgers(packages, root.name, state); + const { problems: drift, notes } = evaluateMeasurements(measurements); + const elapsed = ((Date.now() - started) / 1000).toFixed(1); + for (const n of notes) console.log(' ℹ ' + n); + if (drift.length) { + console.error(`\ncheck-type-check-coverage --re-measure: ${drift.length} ledger entr(ies) drifted upward\n`); + for (const p of drift) console.error(' • ' + p); + process.exit(1); + } + const measuredTotal = measurements.reduce((sum, m) => sum + m.actual, 0); + console.log( + `check-type-check-coverage --re-measure: OK — ${measurements.length} ledger entr(ies) re-measured ` + + `in ${elapsed}s, ${measuredTotal} raw tsc error(s) total, none above its recorded number.`, + ); +} From b8433cafd13193cff1be519ae2c4cab2f92d82cd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 07:05:50 +0000 Subject: [PATCH 2/8] =?UTF-8?q?ci(dx):=20=E8=A1=A5=E4=B8=A4=E5=A4=84?= =?UTF-8?q?=E5=8F=B0=E8=B4=A6=20note=20=E4=B8=8E=E6=AD=BB=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=B8=85=E7=90=86=20=E2=80=94=E2=80=94=20=E6=A0=B9?= =?UTF-8?q?=E6=9D=A1=E7=9B=AE=E4=BC=9A=E9=9A=8F=20showcase=20=E5=8A=A8,mea?= =?UTF-8?q?sureDebt=20=E7=9A=84=20||=20=E5=88=86=E6=94=AF=E4=B8=8D?= =?UTF-8?q?=E5=8F=AF=E8=BE=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW --- scripts/check-type-check-coverage.mjs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index bb2211b5c7..cf0e9cd199 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -272,7 +272,11 @@ const DEBT = { + 'drifts differently from a package: the root program is `scripts/` and the top-level configs ' + '(everything outside packages/apps/examples), so it grows whenever the repo gains a script -- ' + 'scripts/check-test-typecheck.mts alone accounts for 29 of the 80, and the analytics-reconcile ' - + 'tree for 32. Almost all of it is one missing `types:["node"]`, not 80 defects.', + + 'tree for 32. Almost all of it is one missing `types:["node"]`, not 80 defects. One wrinkle to ' + + 'know before reading this number as "the root scripts": `exclude` only drops files from the ' + + 'initial walk, so example sources IMPORTED by a script are still pulled into the program -- 4 of ' + + 'the 80 are reported in examples/app-showcase/src, and this entry therefore moves with the ' + + 'showcase as well as with scripts/.', }, }; @@ -890,7 +894,9 @@ function tscErrorCount(project) { * the package up. */ function measureDebt(dir) { - return tscErrorCount(posix.join(dir, 'tsconfig.json') || 'tsconfig.json'); + // `dir` is '' for the workspace root, and posix.join('', 'tsconfig.json') + // is already 'tsconfig.json' -- no special case needed. + return tscErrorCount(posix.join(dir, 'tsconfig.json')); } /** From 738ae3e6b8e4aedad08393e50c14bcbdc5693ac0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 07:41:25 +0000 Subject: [PATCH 3/8] =?UTF-8?q?ci(dx):=20rest=20=E7=9A=84=20TEST=5FDEBT=20?= =?UTF-8?q?=E9=87=8D=E6=B5=8B=E5=88=B0=20143=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=20merge-commit=20=E7=AB=9E=E6=80=81,?= =?UTF-8?q?=E9=82=A3=E6=AD=A3=E6=98=AF=E9=97=B8=E9=97=A8=E8=B5=B7=E4=BD=9C?= =?UTF-8?q?=E7=94=A8=E7=9A=84=E8=AF=81=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW --- scripts/check-type-check-coverage.mjs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index cf0e9cd199..fde0ae6e37 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -115,6 +115,17 @@ // made of -- and when the delta cannot be attributed, say so in // the note rather than inventing composition. // +// One thing to know before re-measuring: a `pull_request` run +// compiles your branch MERGED INTO the current main, not your +// branch. So the number to record is the one measured on a tree +// rebased onto main as of that moment, and a sweep that re-measures +// MANY entries races every PR landing beside it -- #5278's own PR +// went red on `@objectstack/rest` twice for exactly that reason, +// three rest-touching PRs having landed between the sweep and the +// run. That race is a bootstrapping cost, not a standing one: once +// this invariant is on main, the PR that adds the errors is the PR +// that goes red, which is the whole point. +// // The root is the one asymmetry: its `typecheck` script is the workspace // aggregator, so its OWN top-level TypeScript is covered by a `typecheck:root` // script (tsc, invoked from lint.yml) or by a ledger entry like anyone else. @@ -340,11 +351,16 @@ const TEST_DEBT = { + 'possibly-undefined grew; the net -2 hides a much larger churn in both directions.', }, '@objectstack/rest': { - tests: 56, - errors: 136, - note: 'TS2835 x61 (NodeNext extensions), TS7006 x54, TS2554 x10, TS2550 x7. Also in DEBT. Re-measured ' - + '136 at 5ab08428, up from 105. Read the top-of-ledger NodeNext note before sizing this one: TS2835 ' - + 'and the implicit-any pile it causes are 115 of the 136, and they are one repair, not 115.', + tests: 58, + errors: 143, + note: 'TS2835 x64 (NodeNext extensions), TS7006 x56, TS2554 x10, TS2550 x7. Also in DEBT. Re-measured ' + + '136 at 5ab08428, up from 105, and 143 a few hours later at 77adf29 the same day. Read the ' + + 'top-of-ledger NodeNext note before sizing this one: TS2835 and the implicit-any pile it causes ' + + 'are 120 of the 143, and ' + + 'they are one repair, not 120. This is also the fastest-moving entry in either ledger, and it is ' + + 'the one that proved the gate works: #5278\'s own PR went red in CI on it, because a `pull_request` ' + + 'run builds the branch MERGED INTO main and three rest-touching PRs had landed since the sweep. A ' + + 'ledger number is always a number about a moment.', }, '@objectstack/plugin-auth': { tests: 34, From 26de40cc3acd14f90a42927af455db6e09c14702 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 10:27:25 +0000 Subject: [PATCH 4/8] =?UTF-8?q?ci(dx):=20objectql=20=E7=9A=84=20TEST=5FDEB?= =?UTF-8?q?T=20=E6=A0=A1=E5=87=86=E5=88=B0=20335,=E5=B9=B6=E6=8A=8A?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E9=98=9F=E5=88=97=E7=9A=84=E7=AB=9E=E6=80=81?= =?UTF-8?q?=E5=86=99=E8=BF=9B=E4=B8=8D=E5=8F=98=E5=BC=8F=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 队列把本 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 Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW --- scripts/check-type-check-coverage.mjs | 41 +++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index fde0ae6e37..94ec5b7ced 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -118,7 +118,7 @@ // One thing to know before re-measuring: a `pull_request` run // compiles your branch MERGED INTO the current main, not your // branch. So the number to record is the one measured on a tree -// rebased onto main as of that moment, and a sweep that re-measures +// merged with main as of that moment, and a sweep that re-measures // MANY entries races every PR landing beside it -- #5278's own PR // went red on `@objectstack/rest` twice for exactly that reason, // three rest-touching PRs having landed between the sweep and the @@ -126,6 +126,32 @@ // this invariant is on main, the PR that adds the errors is the PR // that goes red, which is the whole point. // +// The MERGE QUEUE sharpens the same edge, and is worth its own +// paragraph because the usual remedy does not work there. The queue +// builds your PR as merged onto the head of the queue, which keeps +// moving as the entries ahead of you land -- so a count frozen even +// minutes earlier can already be stale, and RE-RUNNING the failed +// job cannot fix it (a rerun replays the same merge ref, so it +// re-measures the same stale base). The only repair is a new commit +// carrying a re-measured number. #5278's PR was kicked from the +// queue on `@objectstack/objectql` +2 -- #5802's registry tests and +// #5850's new file -- and while it sat there red-looping, two +// unrelated PRs queued behind it were each kicked once as +// collateral, then landed untouched once it left the queue. So: if +// a re-measure PR goes red in the queue, take it OUT of the queue +// before repairing it, and if you are the one re-measuring, push +// the calibration immediately after measuring rather than batching +// it with other work. +// +// Before treating any such red as base drift, falsify the other +// explanation: run `--re-measure` TWICE on the same tree. Identical +// output means the count is deterministic and calibration is the +// right answer; a count that oscillates would mean tsc itself is +// nondeterministic here, which is a tolerance question for this +// ratchet and NOT a calibration -- take it back to #5278. Measured +// 2026-08-06 on the objectql case: two back-to-back runs were +// byte-identical, so this gate has no known nondeterminism. +// // The root is the one asymmetry: its `typecheck` script is the workspace // aggregator, so its OWN top-level TypeScript is covered by a `typecheck:root` // script (tsc, invoked from lint.yml) or by a ledger entry like anyone else. @@ -334,12 +360,17 @@ const TEST_DEBT = { + 'but this ledger has ever seen it. 443 of the 547 are in one file, src/approval-service.test.ts.', }, '@objectstack/objectql': { - tests: 125, - errors: 333, - note: 'TS2339 x114, TS2554 x91 (wrong arity), TS7006 x47, TS2345 x19, TS2322 x12, TS2749 x11. ' + tests: 126, + errors: 335, + note: 'TS2339 x115, TS2554 x92 (wrong arity), TS7006 x47, TS2345 x19, TS2322 x12, TS2749 x11. ' + 'Re-measured 333 at 5ab08428, up from 219 -- the largest absolute growth in either ledger. The ' + 'shape held (TS2339/TS2554/TS7006 still lead) but every number roughly tripled, and the file count ' - + 'went 87 -> 125; src/engine.test.ts alone carries 103.', + + 'went 87 -> 126; src/engine.test.ts alone carries 103. Re-calibrated to 335 at 909895dc: +1 TS2339 ' + + 'in src/registry.test.ts (#5802 added ~116 lines of registry tests) and +1 TS2554 in the file ' + + '#5850 introduced, src/engine-update-prior-read-scope.test.ts -- both attributed, neither a new ' + + 'class. This is the package that showed what the merge queue does to a frozen number: the queue ' + + 'builds the PR as merged onto the CURRENT main, so a count frozen minutes earlier is already ' + + 'stale, and #5278\'s own PR was kicked from the queue on exactly this +2.', }, '@objectstack/runtime': { tests: 101, From 0d35a11bf09213b679fd05572c1657374765f98e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 11:48:55 +0000 Subject: [PATCH 5/8] =?UTF-8?q?ci(dx):=20objectql=20=E7=9A=84=20TEST=5FDEB?= =?UTF-8?q?T=20=E6=A0=A1=E5=87=86=E5=88=B0=20339=20=E2=80=94=E2=80=94=20+4?= =?UTF-8?q?=20=E5=85=A8=E5=9C=A8=20#5861=20=E6=96=B0=E5=A2=9E=E7=9A=84?= =?UTF-8?q?=E9=82=A3=E4=B8=80=E4=B8=AA=E6=96=87=E4=BB=B6=E9=87=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第三次基漂移,签名同前:335 是在推上校准提交那一刻冻结的,而 #5861 (SaveMetaItemResponseSchema,#5745)几乎同分钟落地。合并 c15fcee4c 后实测 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 Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW --- scripts/check-type-check-coverage.mjs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index 94ec5b7ced..85b5bd20ea 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -360,17 +360,22 @@ const TEST_DEBT = { + 'but this ledger has ever seen it. 443 of the 547 are in one file, src/approval-service.test.ts.', }, '@objectstack/objectql': { - tests: 126, - errors: 335, - note: 'TS2339 x115, TS2554 x92 (wrong arity), TS7006 x47, TS2345 x19, TS2322 x12, TS2749 x11. ' + tests: 127, + errors: 339, + note: 'TS2339 x115, TS2554 x93 (wrong arity), TS7006 x47, TS2345 x19, TS2322 x12, TS2749 x11. ' + 'Re-measured 333 at 5ab08428, up from 219 -- the largest absolute growth in either ledger. The ' + 'shape held (TS2339/TS2554/TS7006 still lead) but every number roughly tripled, and the file count ' - + 'went 87 -> 126; src/engine.test.ts alone carries 103. Re-calibrated to 335 at 909895dc: +1 TS2339 ' - + 'in src/registry.test.ts (#5802 added ~116 lines of registry tests) and +1 TS2554 in the file ' - + '#5850 introduced, src/engine-update-prior-read-scope.test.ts -- both attributed, neither a new ' - + 'class. This is the package that showed what the merge queue does to a frozen number: the queue ' - + 'builds the PR as merged onto the CURRENT main, so a count frozen minutes earlier is already ' - + 'stale, and #5278\'s own PR was kicked from the queue on exactly this +2.', + + 'went 87 -> 127; src/engine.test.ts alone carries 103. Then +2 at 909895dc (+1 TS2339 in ' + + 'src/registry.test.ts, #5802 having added ~116 lines of registry tests; +1 TS2554 in the file ' + + '#5850 introduced, src/engine-update-prior-read-scope.test.ts), and +4 more at c15fcee4c -- ALL ' + + 'FOUR in one file #5861 added, src/save-meta-response-conformance.test.ts: one TS2554 at :115, ' + + 'and at :119 a TS6133 (`LOG` declared, never read) beside two TS2304 (`appendFileSync` and `OUT` ' + + 'are not names in scope). That last line is worth a look by whoever next touches the file -- an ' + + 'unresolved name is a line that cannot run, not a typing nicety -- but it is that PR\'s to fix, ' + + 'not this ledger\'s. This is the package that showed what the merge queue does to a frozen ' + + 'number: the queue builds the PR as merged onto the CURRENT queue head, so a count frozen minutes ' + + 'earlier is already stale, and #5278\'s own PR was kicked on this entry three times before it ' + + 'landed.', }, '@objectstack/runtime': { tests: 101, From 5bccbf1444c7ce279308f2f156a3341e44fd9758 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 01:20:33 +0000 Subject: [PATCH 6/8] =?UTF-8?q?ci(dx):=20=E6=8C=89=E8=A3=81=E5=86=B3(optio?= =?UTF-8?q?n=20A)=E5=9C=A8=E5=90=88=E5=B9=B6=E6=A0=91=E4=B8=8A=E5=85=A8?= =?UTF-8?q?=E9=87=8F=E9=87=8D=E6=A0=A1=E5=87=86,=E4=B8=89=E4=B8=AA?= =?UTF-8?q?=E7=83=AD=E5=8C=85=E8=AE=B0=E6=A1=A3=20+10=20=E4=BD=99=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit merge origin/main 后在合并树 e8db1a230 上重跑 --re-measure:5 条上漂 (service-analytics 7->10、runtime 218->227、rest 143->153、plugin-auth 129->131、lint 30->32)、1 条下降(service-storage 42->41),其余 28 条与 记录值严格相等。 按维护者 2026-08-07 的裁决执行选项 A:今日实证的三个热包 (objectql / lint / rest)按实测 +10 记档,note 逐条写明「bootstrap margin (+10 over N measured at e8db1a230) -- tighten via the ℹ hint immediately after landing」,并在 MEASURED 文档块加一段说明这是台账里唯一的余量、 每轮打印的 ℹ 行就是收紧工单。其余条目一律精确等于实测。 note 成分按实测重写,delta 能归因的逐条归因、不能的明说: - runtime +9 全部是 TS18048,全在本窗口新增的一个文件 src/domains/meta-item-envelope.test.ts(#5563 / PR #5895); - rest +10 中 8 条来自三个新增测试文件,余 2 条落在既有文件、不再编造归因; - plugin-auth +2 中 1 条在新增的 last-admin-guard.test.ts(#5941 / PR #5993); - service-analytics +3 仍全在 measure-source-field-gate.test.ts 同一文件、 同一错误码(#5716 / PR #5963),该包 3 -> 7 -> 10 全程无人看见; - lint +2 两条 TS7006 均在既有文件,pre-merge 逐文件计数未留存,如实记录不归因; - service-storage -1 跟着 #5540 / PR #5983 摘除 IStorageService.list(prefix), 下调而非留着——高于实测又没写明的余量就是暗账。 tests 文件数一并对齐实测(objectql 130、runtime 102、rest 62、plugin-auth 38、 plugin-security 35、service-sms 5)。 合并冲突只有 TEST_DEBT 一处:main 的 #5773 / PR #5982 改过 service-sms 一行,本分支重写了整块——取本分支并把该条的事实并入,两侧都不丢。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5 --- scripts/check-type-check-coverage.mjs | 117 +++++++++++++++++++------- 1 file changed, 85 insertions(+), 32 deletions(-) diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index 47a065d03e..a782d3c4a3 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -152,6 +152,24 @@ // 2026-08-06 on the objectql case: two back-to-back runs were // byte-identical, so this gate has no known nondeterminism. // +// BOOTSTRAP MARGINS (#5278 option A). The two paragraphs above +// describe a race that the exact-calibration loop lost five times +// running -- objectql 333 -> 334 -> 335 -> 339 and lint 30 -> 32, +// each calibration stale within minutes of being pushed, while +// seven unrelated PRs were kicked from the queue as collateral. +// The maintainer's ruling was to land the invariant with a +// DOCUMENTED margin on the packages that proved hottest rather +// than run a sixth lap: `@objectstack/objectql`, +// `@objectstack/lint` and `@objectstack/rest` are recorded at +// their measurement PLUS TEN, and each says so in its own note, +// naming the measured number and the sha. This is the ledger's +// ONLY slack and it is deliberately loud: nothing else here may +// sit above its measurement, and a margin is not a place to hide +// a real increase. Because shrinkage is informational, each of +// the three prints its own `ℹ ... can be lowered` line on every +// run -- that line IS the tightening worklist, and closing it is +// a follow-up PR, not a thing to leave running for months. +// // The root is the one asymmetry: its `typecheck` script is the workspace // aggregator, so its OWN top-level TypeScript is covered by a `typecheck:root` // script (tsc, invoked from lint.yml) or by a ledger entry like anyone else. @@ -265,11 +283,14 @@ const DEBT = { note: 'code-tier 2 (TS2345).', }, '@objectstack/service-analytics': { - errors: 7, - note: 'code-tier 6 (TS2339 x4, TS7053 x2) + 1 noise (TS6133). Re-measured 7 at 5ab08428, up from 3. ' - + 'The 4 TS2339 are all in __tests__/measure-source-field-gate.test.ts, a file added after the entry ' - + 'was written -- the package layer of this gate is closed to new debt, the ERROR-COUNT layer was ' - + 'not, so a new test file walked its errors in past everything (#5278).', + errors: 10, + note: 'code-tier 9 (TS2339 x7, TS7053 x2) + 1 noise (TS6133). Re-measured 10 at e8db1a230, up from 7 ' + + 'at 5ab08428 and 3 before that. All 7 TS2339 sit in __tests__/measure-source-field-gate.test.ts, ' + + 'the same file that carried 4 of them when this entry was last written; the +3 arrived with #5716 ' + + '/ PR #5963 rewriting that gate\'s refusals -- no new file, no new error class. This entry is the ' + + 'standing specimen for why the ERROR-COUNT layer needed a ratchet of its own: the PACKAGE layer ' + + 'of this gate has been closed to new debt the whole time, and the count still walked 3 -> 7 -> 10 ' + + 'unremarked (#5278).', }, '@objectstack/service-automation': { errors: 5, @@ -298,8 +319,13 @@ const DEBT = { note: 'code-tier 12 (TS2345 x7: manifest action handlers called without `namespace`/`actionId`; TS2322) + 1 noise. Was ledgered at 44 with "no code-tier finding" -- wrong in both directions: 31 of those 44 were unresolved imports (see the NodeNext note at the top of this ledger), and the resolution they were blocking is what made the 12 real ones visible.', }, '@objectstack/service-storage': { - errors: 42, - note: 'code-tier 5 (TS2339/TS2347); the rest is config-tier (TS2835) and noise (TS7006).', + errors: 41, + note: 'code-tier 8 (TS2339 x4, TS2347 x4); config-tier 19 (TS2835); noise 14 (TS7006 x12, TS6196, ' + + 'TS6133). Re-measured 41 at e8db1a230, DOWN from 42 -- the -1 follows #5540 / PR #5983 retiring ' + + '`IStorageService.list(prefix)`. Lowered rather than left standing: an entry kept above its own ' + + 'measurement is undocumented slack, and this ledger carries slack in exactly one place -- the ' + + 'three bootstrap margins that say so in their own note. 11 of the 41 are in ' + + 'storage-route-ledger.conformance.test.ts and 7 in storage-service-plugin.test.ts.', }, '@objectstack/spec-monorepo': { errors: 80, @@ -360,8 +386,8 @@ const TEST_DEBT = { + 'but this ledger has ever seen it. 443 of the 547 are in one file, src/approval-service.test.ts.', }, '@objectstack/objectql': { - tests: 127, - errors: 339, + tests: 130, + errors: 349, note: 'TS2339 x115, TS2554 x93 (wrong arity), TS7006 x47, TS2345 x19, TS2322 x12, TS2749 x11. ' + 'Re-measured 333 at 5ab08428, up from 219 -- the largest absolute growth in either ledger. The ' + 'shape held (TS2339/TS2554/TS7006 still lead) but every number roughly tripled, and the file count ' @@ -375,35 +401,52 @@ const TEST_DEBT = { + 'not this ledger\'s. This is the package that showed what the merge queue does to a frozen ' + 'number: the queue builds the PR as merged onto the CURRENT queue head, so a count frozen minutes ' + 'earlier is already stale, and #5278\'s own PR was kicked on this entry three times before it ' - + 'landed.', + + 'landed. Re-measured at e8db1a230 (this PR merged with main after a day of drift): still 339, ' + + 'and the histogram above is unchanged code for code -- the churn in this window missed the one ' + + 'package that had absorbed the most of it. RECORDED 349 is a bootstrap margin (+10 over 339 ' + + 'measured at e8db1a230) -- tighten via the ℹ hint immediately after landing (#5278 option A).', }, '@objectstack/runtime': { - tests: 101, - errors: 218, - note: 'TS18048 x89 (possibly-undefined), TS2345 x26, TS18046 x20, TS2339 x16, TS2493 x15. Src ' - + 'graduated in #4311 (declares `typecheck`); this is purely the hidden test layer. Re-measured 218 ' - + 'at 5ab08428, DOWN from 220 -- one of only two entries that shrank. The TS6133 x25 the old note ' - + 'named is down to x7, so unused-symbol cleanup happened somewhere in the test tree while ' - + 'possibly-undefined grew; the net -2 hides a much larger churn in both directions.', + tests: 102, + errors: 227, + note: 'TS18048 x98 (possibly-undefined), TS2345 x26, TS18046 x20, TS2339 x16, TS2493 x15, TS2835 x11, ' + + 'TS2554 x11. Src ' + + 'graduated in #4311 (declares `typecheck`); this is purely the hidden test layer. Measured 220 -> ' + + '218 (5ab08428, one of only two entries that ever shrank; TS6133 x25 collapsed to x7 while ' + + 'possibly-undefined grew, so that net -2 hid a much larger churn in both directions) -> 227 ' + + '(e8db1a230). The latest +9 is fully attributed and is ONE file: every one of the nine is a ' + + 'TS18048 in src/domains/meta-item-envelope.test.ts, added by #5563 / PR #5895 (the ' + + '`GET /meta/:type/:name` envelope convergence). Nothing else in the package moved.', }, '@objectstack/rest': { - tests: 58, - errors: 143, - note: 'TS2835 x64 (NodeNext extensions), TS7006 x56, TS2554 x10, TS2550 x7. Also in DEBT. Re-measured ' - + '136 at 5ab08428, up from 105, and 143 a few hours later at 77adf29 the same day. Read the ' + tests: 62, + errors: 163, + note: 'TS2835 x67 (NodeNext extensions), TS7006 x57, TS2554 x13, TS2550 x10. Also in DEBT. Measured ' + + '105 -> 136 (5ab08428) -> 143 (77adf29, hours later the same day) -> 153 (e8db1a230). Read the ' + 'top-of-ledger NodeNext note before sizing this one: TS2835 and the implicit-any pile it causes ' - + 'are 120 of the 143, and ' - + 'they are one repair, not 120. This is also the fastest-moving entry in either ledger, and it is ' + + 'are 124 of the 153, and ' + + 'they are one repair, not 124. Of the latest +10, 8 are attributable to three test files this ' + + 'window added -- rest-meta-save-receipt-envelope.test.ts x4 (#5265 / PR #5926), ' + + 'meta-item-envelope.test.ts x2 (#5563 / PR #5895), ' + + 'analytics-dataset-unlisted-refusal-envelope.test.ts x2; the remaining 2 landed in files that ' + + 'already existed and are NOT attributed further, because the pre-merge per-file counts were not ' + + 'retained and a made-up attribution is worse than an admitted gap. This is also the ' + + 'fastest-moving entry in either ledger, and it is ' + 'the one that proved the gate works: #5278\'s own PR went red in CI on it, because a `pull_request` ' + 'run builds the branch MERGED INTO main and three rest-touching PRs had landed since the sweep. A ' - + 'ledger number is always a number about a moment.', + + 'ledger number is always a number about a moment. RECORDED 163 is a bootstrap margin (+10 over 153 ' + + 'measured at e8db1a230) -- tighten via the ℹ hint immediately after landing (#5278 option A).', }, '@objectstack/plugin-auth': { - tests: 34, - errors: 129, - note: 'TS2493 x42 (tuple index out of range), TS18048 x24, TS2740 x19, TS2322 x11, TS2532 x9. ' - + 'Re-measured 129 at 5ab08428, up from 124; composition unchanged in shape. 63 sit in ' - + 'src/auth-manager.test.ts.', + tests: 38, + errors: 131, + note: 'TS2493 x42 (tuple index out of range), TS18048 x24, TS2740 x19, TS2322 x11, TS2532 x9, ' + + 'TS2339 x8, TS2741 x8. ' + + 'Measured 124 -> 129 (5ab08428, composition unchanged in shape) -> 131 (e8db1a230). Half of the ' + + 'latest +2 is a TS2554 in src/last-admin-guard.test.ts, a file added by #5941 / PR #5993 ' + + '(the break-glass delete guard); the other 1 landed in a file that already existed and is not ' + + 'attributed further. 64 of the 131 sit in src/auth-manager.test.ts, 22 in ' + + 'src/admin-import-users.test.ts and 18 in src/admin-user-endpoints.test.ts.', }, '@objectstack/mcp': { tests: 8, errors: 52, note: 'TS18046 x51 -- `error` is of type unknown, one catch-block idiom repeated. Re-measured 52 at 5ab08428, exact.' }, '@objectstack/driver-mongodb': { @@ -415,8 +458,18 @@ const TEST_DEBT = { + 'entry that turned over two thirds of its content is exactly why counts alone cannot be trusted ' + 'to describe debt (#5278).', }, - '@objectstack/lint': { tests: 61, errors: 30, note: 'TS7006 x20, TS2835 x6, TS6059 x4. Re-measured 30 at 5ab08428, up from 26; the +4 is TS6059 (a file outside rootDir), a class the old note did not list.' }, - '@objectstack/plugin-security': { tests: 34, errors: 21, note: 'TS2739 x8, TS2740 x5, TS2345/TS2322/TS2741 x2 each -- incomplete literals. Re-measured 21 at 5ab08428, up from 20.' }, + '@objectstack/lint': { + tests: 61, + errors: 42, + note: 'TS7006 x22, TS2835 x6, TS6059 x4. Measured 26 -> 30 (5ab08428, the +4 being TS6059, a file ' + + 'outside rootDir, a class the pre-#5278 note did not list) -> 32 (e8db1a230). The latest +2 are ' + + 'both TS7006 and both in files that already existed; three lint test files changed in this window ' + + '(#5762 / PR #5952, #5378 / PR #5904) and the pre-merge per-file counts were not retained, so the ' + + 'delta is recorded rather than attributed. 10 of the 32 sit in ' + + 'src/validate-visibility-predicates.test.ts. RECORDED 42 is a bootstrap margin (+10 over 32 ' + + 'measured at e8db1a230) -- tighten via the ℹ hint immediately after landing (#5278 option A).', + }, + '@objectstack/plugin-security': { tests: 35, errors: 21, note: 'TS2739 x8, TS2740 x5, TS2345/TS2322/TS2741 x2 each -- incomplete literals. Re-measured 21 at 5ab08428, up from 20, and still 21 at e8db1a230 across 35 test files rather than 34 -- the file count moved, the error count did not.' }, '@objectstack/formula': { tests: 16, errors: 17, note: 'TS2591 x6 (`process`), TS2345 x3, TS2352 x3, TS1470 x2, TS2339 x2. Re-measured 17 at 5ab08428, up from 12; the TS2591 half doubled, which is the missing `types:["node"]` again rather than five new defects.' }, '@objectstack/trigger-record-change': { tests: 5, errors: 9, note: 'TS2353 x9 -- still the one unknown-property shape repeated, now in four files. Re-measured 9 at 5ab08428, up from 8.' }, '@objectstack/verify': { tests: 4, errors: 8, note: 'TS2835 x4, TS7006 x4. Re-measured 8 at 5ab08428, up from 6; both classes are the NodeNext pair from the top-of-ledger note.' }, @@ -433,7 +486,7 @@ const TEST_DEBT = { }, '@objectstack/platform-objects': { tests: 9, errors: 3, note: 'TS2339 x2, TS7006 x1. Re-measured 3 at 5ab08428, exact.' }, '@objectstack/plugin-sharing': { tests: 13, errors: 3, note: 'TS6133 x2, TS18048 x1. Re-measured 3 at 5ab08428, exact.' }, - '@objectstack/service-sms': { tests: 4, errors: 1, note: 'TS2493 x1, in transports.test.ts. Re-measured 1 at 5ab08428, exact; #5773 then added sms-manifest-providers.contract.test.ts (3 -> 4 files) and the file count moved while the error count did not -- the new file is type-clean with the exclusion lifted.' }, + '@objectstack/service-sms': { tests: 5, errors: 1, note: 'TS2493 x1, in transports.test.ts. Re-measured 1 at 5ab08428 and still 1 at e8db1a230, across 5 test files rather than 3: #5773 added sms-manifest-providers.contract.test.ts and #2814 / PR #6042 added sms-daily-quota.test.ts. The file count moved twice while the error count did not -- both new files are type-clean with the exclusion lifted.' }, '@objectstack/connector-rest': { tests: 3, errors: 1, note: 'TS6133 x1. Re-measured 1 at 5ab08428, exact.' }, }; From 77c7c884b37ed0fb42f42e37866f44054448351e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 01:27:36 +0000 Subject: [PATCH 7/8] =?UTF-8?q?ci(dx):=20=E5=88=A0=E6=8E=89=E7=A9=BA=20fro?= =?UTF-8?q?ntmatter=20changeset,=E6=94=B9=E8=B5=B0=20skip-changeset=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20#5471=20/=20PR=20#6059=20=E5=9C=A8?= =?UTF-8?q?=E9=A3=9E=E8=A1=8C=E9=80=94=E4=B8=AD=E8=90=BD=E5=9C=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新门禁 `scripts/check-empty-changeset.mjs` 明确判定:PR 新增的空 frontmatter changeset 是违规。本 PR 的 `.changeset/type-check-debt-ledger-ratchet.md` 正是 「A added, empty at head」这一行,门禁在合并树上逐字点名了它。 按门禁给的两条路选:本 PR 只动 dev scripts / CI(`scripts/`、`.github/workflows/`、 `package.json`、`AGENTS.md`),不发布任何包 —— 走 route 2:删掉 changeset,改用 `skip-changeset` 标签。空 changeset 名不到任何包,正文到不了任何 CHANGELOG,却是 changesets/action 的真实输入(全空集会让 Release 静默且绿地空跑,即 #4898);标签 不产生输入,因此严格更优。 原 PR body 的「## changeset」一节因此过期,更正写在正文「裁决落地」一节里,原节 按接手协议不改写。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5 --- .changeset/type-check-debt-ledger-ratchet.md | 56 -------------------- 1 file changed, 56 deletions(-) delete mode 100644 .changeset/type-check-debt-ledger-ratchet.md diff --git a/.changeset/type-check-debt-ledger-ratchet.md b/.changeset/type-check-debt-ledger-ratchet.md deleted file mode 100644 index fc9739ac42..0000000000 --- a/.changeset/type-check-debt-ledger-ratchet.md +++ /dev/null @@ -1,56 +0,0 @@ ---- ---- - -ci(dx): `scripts/check-type-check-coverage.mjs` 的 DEBT / TEST_DEBT 台账数字现在会被**重测**——实测 > 记录即红(#5278)。Dev scripts / CI only;releases nothing。 - -原来的闸门只断言「有一条台账、数字为正」: - -```js -if (!entry || typeof entry.errors !== 'number' || entry.errors <= 0) { ... } -``` - -也就是说 `errors: 28` 和 `errors: 1` 对它完全等价,台账**从不复测**。包这一层对新增 debt 是关着的,错误**条数**这一层不是——一个新测试文件带进来的错误没有任何一道闸会看见。AGENTS.md 写着「DEBT is frozen debt, not a permission slip. Every entry below was measured」,而一个已经悄悄漂了 2.25 倍的数字不再描述它声称冻结的那笔债:记 28 的条目读起来像「快毕业了」,实际成本是它的两倍多。 - -本次在 `5ab08428` 上把 34 条台账**全部重测**,漂移比 issue 报的更普遍: - -| | 记录 | 实测 | -|:---|---:|---:| -| `@objectstack/metadata-protocol` | 28 | 63 | -| `@objectstack/spec-monorepo`(仓库根) | 50 | 80 | -| `@objectstack/objectql`(TEST_DEBT) | 219 | 333 | -| `@objectstack/plugin-approvals`(TEST_DEBT) | 467 | 547 | -| `@objectstack/service-analytics` | 3 | 7 | -| `@objectstack/service-automation` | 2 | 5 | -| …… 共 17 条低估 | | | -| `@objectstack/runtime`(TEST_DEBT) | 220 | 218 | -| `@objectstack/driver-mongodb`(TEST_DEBT) | 44 | 43 | - -17 条低估、2 条高估、15 条精确。**没有一条**是因为债在缩小而失真的。 - -## 新的 MEASURED 不变式 - -`--re-measure` 对每条 DEBT 跑该包自己的 `tsc --noEmit -p /tsconfig.json`,对每条 TEST_DEBT 生成一份 `extends` 原配置、只去掉 test 排除项的临时兄弟配置再跑(临时文件在 `finally` 里删除)。判定是**不对称**的,这是本次的核心: - -- 实测 **>** 记录 → **红**。这才是棘轮。 -- 实测 **<** 记录 → 打印一行 `ℹ … can be lowered`,**不红**。修错误不应该还要先改一个记账数字才能让 CI 变绿,否则台账就是在对它本该鼓励的工作收费。 -- 实测 **= 0** → 报告为 graduation candidate(毕业仍然是一次显式 PR:加 `typecheck` script + 删台账条目,由 COVERED / RECONCILED 两个方向共同强制)。 - -## 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 5ab08428」,不编造成分。 - -另外记录两个重测才看得见的事实:`@objectstack/driver-mongodb` 净变化是 -1,但成分换掉了三分之二(老 note 归咎于缺 `types:["node"]` 的 15 条 TS2591 全没了,冒出 7 条 TS1309)——单看数字会以为什么都没发生;`@objectstack/http-conformance` 的 4 条里有 2 条报在 `node_modules` 的 `.d.ts` 上,所以这条会随 lockfile 动而不只随本包代码动,已在 note 里写明。 - -## 落点与成本 - -便宜的结构检查留在原地(只读 package.json / tsconfig.json,亚秒级,跑在 build 之前)。重测这一半需要各包依赖的 `dist/*.d.ts`,所以挂在 `lint.yml` 的 `typecheck` job 里、build 步骤**之后**——这个 job 本来就付了构建的钱。build filter 顺带扩到嵌套包组(`packages/services/*` 等):多数台账包没有 `typecheck` script,从来没进过 turbo 的任务图,它们的依赖也就不会被建。 - -实测重测本身 **~4 分钟**(34 个 project,顺序执行;并行 tsc 是拿 wall clock 换一个刚建完整个 workspace 的 job 上的 OOM 风险)。 - -## 反向验证 - -方向是先定后验的,两个方向都验了:把一条台账改到**低于**实测(`service-analytics` 7 → 4)、另一条改到**高于**实测(`service-automation` 5 → 9),同一次运行 exit=1,恰好 1 条红(前者)+ 恰好 1 条 `ℹ`(后者)——增长判红、缩小不判红、逐条独立,三件事一次落实。改动前的台账(即 origin/main 的数字)在新闸门下是 17 条红,重测后为绿。 - -self-test 新增 11 个用例:6 个钉住三个方向(涨/缩/归零)与逐条独立性,5 个钉住计数器本身——多行 elaboration 缩进行不能被重复计数(一条 TS2322 能打印 5 行),无文件前缀的全局诊断要计数,而正文里出现「error TS」字样但没有错误码的散文不能计数。 From e576141b9c237e764caf79d7d88b2eb194fab2ea Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 01:48:39 +0000 Subject: [PATCH 8/8] =?UTF-8?q?ci(dx):=20=E7=AC=AC=E4=BA=8C=E4=B8=AA?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E7=AA=97=E5=8F=A3=E7=9A=84=E5=86=8D=E6=A0=A1?= =?UTF-8?q?=E5=87=86=20=E2=80=94=E2=80=94=20service-storage=20=E8=BD=AC?= =?UTF-8?q?=E8=AE=B0=E6=A1=A3=E4=BD=99=E9=87=8F,objectql=20=E4=BD=99?= =?UTF-8?q?=E9=87=8F=E6=8C=89=E6=96=B0=E5=AE=9E=E6=B5=8B=E6=8A=AC=E5=9B=9E?= =?UTF-8?q?=20+10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 合并 main 到 dca5bd36a 后再全量重测,余量在一小时内被兑付了两笔,记录如下: - `@objectstack/objectql` 实测 339 -> **345**(+6 全是 TS2554,全在 `src/summary-rollup.test.ts`,由飞行途中落地的 #5749 / PR #6013 扩写)。 记档 349 把它静默吸收了 —— 若按精确值 339 记账,这就是同一场赛跑的第 6 次红。 按裁决「实测 +10」把记录抬到 **355**,恢复满额余量。 - `@objectstack/service-storage` 42 -> 41 -> **42**:`IStorageService.list(prefix)` 的退休被拆成两个 PR,spec 半边(#5540 / PR #5983)减 1、适配器半边 (#5541 / PR #6061)删旧测试(-1 TS7006)又新增 `storage-adapter-list-retirement.test.ts`(+2 TS2835),净 +1。上一轮我按实测 下调到 41,一小时后就被咬红 —— 正是派发令说的「非余量条目被基漂移咬住」, 按同一记档规则给这条加 +10,记 **52**,不开精确校准 lap。 一个值得写进文档块的新形状:**拆成两个 PR 的退休会让计数先降后升**,在两半之间 记下的精确值,推上去之前就已经过期。 `rest` / `lint` 两条实测未动(153 / 32),余量原样,note 补记「一小时后在 77c7c884b 复测仍是该值」。 重测输出:四条记档余量各打印一行 ℹ(各 -10),无一条上漂,exit 0。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5 --- scripts/check-type-check-coverage.mjs | 67 ++++++++++++++++++--------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index a782d3c4a3..2acc535e0d 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -160,15 +160,27 @@ // The maintainer's ruling was to land the invariant with a // DOCUMENTED margin on the packages that proved hottest rather // than run a sixth lap: `@objectstack/objectql`, -// `@objectstack/lint` and `@objectstack/rest` are recorded at -// their measurement PLUS TEN, and each says so in its own note, -// naming the measured number and the sha. This is the ledger's -// ONLY slack and it is deliberately loud: nothing else here may -// sit above its measurement, and a margin is not a place to hide -// a real increase. Because shrinkage is informational, each of -// the three prints its own `ℹ ... can be lowered` line on every -// run -- that line IS the tightening worklist, and closing it is -// a follow-up PR, not a thing to leave running for months. +// `@objectstack/lint`, `@objectstack/rest` and +// `@objectstack/service-storage` are recorded at their +// measurement PLUS TEN, and each says so in its own note, naming +// the measured number and the sha. This is the ledger's ONLY +// slack and it is deliberately loud: nothing else here may sit +// above its measurement, and a margin is not a place to hide a +// real increase. Because shrinkage is informational, each of the +// four prints its own `ℹ ... can be lowered` line on every run -- +// that line IS the tightening worklist, and closing it is a +// follow-up PR, not a thing to leave running for months. +// +// The margins were not a precaution; two of the four were paid +// out inside a single hour of the landing flight. objectql moved +// 339 -> 345 (#5749 / PR #6013 extending summary-rollup.test.ts) +// and service-storage 42 -> 41 -> 42 (the two halves of the +// `IStorageService.list(prefix)` retirement, #5540 / PR #5983 then +// #5541 / PR #6061, landing hours apart). Recorded exactly, both +// would have been red on a base nobody could have measured in +// advance. Note the second shape especially: a retirement split +// across two PRs moves a count DOWN and then back UP, so an exact +// number recorded between the halves is stale before it is pushed. // // The root is the one asymmetry: its `typecheck` script is the workspace // aggregator, so its OWN top-level TypeScript is covered by a `typecheck:root` @@ -319,13 +331,19 @@ const DEBT = { note: 'code-tier 12 (TS2345 x7: manifest action handlers called without `namespace`/`actionId`; TS2322) + 1 noise. Was ledgered at 44 with "no code-tier finding" -- wrong in both directions: 31 of those 44 were unresolved imports (see the NodeNext note at the top of this ledger), and the resolution they were blocking is what made the 12 real ones visible.', }, '@objectstack/service-storage': { - errors: 41, - note: 'code-tier 8 (TS2339 x4, TS2347 x4); config-tier 19 (TS2835); noise 14 (TS7006 x12, TS6196, ' - + 'TS6133). Re-measured 41 at e8db1a230, DOWN from 42 -- the -1 follows #5540 / PR #5983 retiring ' - + '`IStorageService.list(prefix)`. Lowered rather than left standing: an entry kept above its own ' - + 'measurement is undocumented slack, and this ledger carries slack in exactly one place -- the ' - + 'three bootstrap margins that say so in their own note. 11 of the 41 are in ' - + 'storage-route-ledger.conformance.test.ts and 7 in storage-service-plugin.test.ts.', + errors: 52, + note: 'code-tier 8 (TS2339 x4, TS2347 x4); config-tier 21 (TS2835); noise 13 (TS7006 x11, TS6196, ' + + 'TS6133). This entry is the fourth bootstrap margin, and it earned the label the hard way inside ' + + 'one flight: 42 -> 41 at e8db1a230 (the spec half of the `IStorageService.list(prefix)` ' + + 'retirement, #5540 / PR #5983, removed one error, and it was lowered rather than left standing) ' + + '-> 42 again at 77c7c884b an hour later, when the adapter half (#5541 / PR #6061) deleted the ' + + 'old list tests (-1 TS7006) and added storage-adapter-list-retirement.test.ts (+2 TS2835). A ' + + 'two-PR retirement moves a count twice, and an exact number recorded between the halves is stale ' + + 'before it is pushed -- so this one takes the same documented margin as the three proven-hot ' + + 'packages instead of a sixth calibration lap. 11 of the 42 are in ' + + 'storage-route-ledger.conformance.test.ts and 7 in storage-service-plugin.test.ts. RECORDED 52 ' + + 'is a bootstrap margin (+10 over 42 measured at 77c7c884b) -- tighten via the ℹ hint immediately ' + + 'after landing (#5278 option A).', }, '@objectstack/spec-monorepo': { errors: 80, @@ -387,7 +405,7 @@ const TEST_DEBT = { }, '@objectstack/objectql': { tests: 130, - errors: 349, + errors: 355, note: 'TS2339 x115, TS2554 x93 (wrong arity), TS7006 x47, TS2345 x19, TS2322 x12, TS2749 x11. ' + 'Re-measured 333 at 5ab08428, up from 219 -- the largest absolute growth in either ledger. The ' + 'shape held (TS2339/TS2554/TS7006 still lead) but every number roughly tripled, and the file count ' @@ -402,9 +420,12 @@ const TEST_DEBT = { + 'number: the queue builds the PR as merged onto the CURRENT queue head, so a count frozen minutes ' + 'earlier is already stale, and #5278\'s own PR was kicked on this entry three times before it ' + 'landed. Re-measured at e8db1a230 (this PR merged with main after a day of drift): still 339, ' - + 'and the histogram above is unchanged code for code -- the churn in this window missed the one ' - + 'package that had absorbed the most of it. RECORDED 349 is a bootstrap margin (+10 over 339 ' - + 'measured at e8db1a230) -- tighten via the ℹ hint immediately after landing (#5278 option A).', + + 'the histogram unchanged code for code. Then 345 at 77c7c884b ONE HOUR LATER: +6 TS2554 in ' + + 'src/summary-rollup.test.ts, which #5749 / PR #6013 extended while this PR was in flight. That ' + + '+6 is what the bootstrap margin is FOR -- recorded at 349 it was absorbed silently, and an ' + + 'exactly-calibrated 339 would have been the sixth red in the same race. RECORDED 355 is a ' + + 'bootstrap margin (+10 over 345 measured at 77c7c884b) -- tighten via the ℹ hint immediately ' + + 'after landing (#5278 option A).', }, '@objectstack/runtime': { tests: 102, @@ -435,7 +456,8 @@ const TEST_DEBT = { + 'the one that proved the gate works: #5278\'s own PR went red in CI on it, because a `pull_request` ' + 'run builds the branch MERGED INTO main and three rest-touching PRs had landed since the sweep. A ' + 'ledger number is always a number about a moment. RECORDED 163 is a bootstrap margin (+10 over 153 ' - + 'measured at e8db1a230) -- tighten via the ℹ hint immediately after landing (#5278 option A).', + + 'measured at e8db1a230 and re-confirmed at 153 an hour later at 77c7c884b) -- tighten via the ℹ ' + + 'hint immediately after landing (#5278 option A).', }, '@objectstack/plugin-auth': { tests: 38, @@ -467,7 +489,8 @@ const TEST_DEBT = { + '(#5762 / PR #5952, #5378 / PR #5904) and the pre-merge per-file counts were not retained, so the ' + 'delta is recorded rather than attributed. 10 of the 32 sit in ' + 'src/validate-visibility-predicates.test.ts. RECORDED 42 is a bootstrap margin (+10 over 32 ' - + 'measured at e8db1a230) -- tighten via the ℹ hint immediately after landing (#5278 option A).', + + 'measured at e8db1a230 and re-confirmed at 32 an hour later at 77c7c884b) -- tighten via the ℹ ' + + 'hint immediately after landing (#5278 option A).', }, '@objectstack/plugin-security': { tests: 35, errors: 21, note: 'TS2739 x8, TS2740 x5, TS2345/TS2322/TS2741 x2 each -- incomplete literals. Re-measured 21 at 5ab08428, up from 20, and still 21 at e8db1a230 across 35 test files rather than 34 -- the file count moved, the error count did not.' }, '@objectstack/formula': { tests: 16, errors: 17, note: 'TS2591 x6 (`process`), TS2345 x3, TS2352 x3, TS1470 x2, TS2339 x2. Re-measured 17 at 5ab08428, up from 12; the TS2591 half doubled, which is the missing `types:["node"]` again rather than five new defects.' },