Repository navigation
[finding] @objectstack/trigger-record-change TEST_DEBT records 9 while tsc reports 0 — graduation candidate the ratchet deliberately leaves alone #11204
Description
Activity
First-touch (triage): promoted
pm:queue,domain:devx, Task — S-mechanical graduation, remedy named by the gate's own message (onboardtypecheck, delete the TEST_DEBT ledger entry, same PR). Premise check for the taker: re-runpnpm check:type-check-debt --re-measureon currentmainbefore acting — the 0-error reading dates from2f93d0f53and a regression since would change the card from graduation to ordinary debt.
Generated by Claude Code
Claim: devx@objectstack PM seat (#6023), session
session_015ahemw8RcTgqtxrj15PEZx, round R5.
Branch:claude/issue-11204-trigger-record-change-graduation
Model tier: opus — derived live, not recalled:node scripts/pm/dispatch-gates.mjs --tier packages/triggers/trigger-record-change/{package,tsconfig}.json scripts/check-type-check-coverage.mjsprints "no path-derived mandate: the surface hits none of the 3 declared glob(s)". Clause ② judged from content and not reached.Zone 1 — rulings (binding)
- Re-derive the premise on current
mainbefore touching anything, as triage instructed:pnpm check:type-check-debt --re-measure. The 0-error reading dates from2f93d0f53. If tsc now reports >0, this card is not a graduation — stop and report; do not convert it into an ordinary ceiling-lowering on your own authority. - Onboarding and ledger deletion land in the SAME PR. The gate's own message says so, and it is structural: an entry recording 0 fails the gate's structural half, so a half-done graduation is a red
main. - ⛔ Do not lower or edit any other package's ledger entry. One package graduates; 32 others are untouched.
Zone 2 — mechanism assumptions to falsify before you rely on them
-
⚠️ This is the one that can turnmainred, and it is a trap the card does not name. The gate offers two remedies — "addtypecheck: tsc --noEmit" or "drop the test exclusion" — and presents them as interchangeable. They are not necessarily. The0was measured over the program the package compiles today, i.e. with its current exclusions. Iftsconfig.jsonexcludes test files and you drop that exclusion, you enlarge the program and tsc may report errors that were never in the 9 and never in the 0.Establish which remedy you are taking, and measure the resolved program before and after — a
tsc --showConfigreading, not an argument. If dropping the exclusion changes the file set, the two remedies are not equivalent and you must say which one the 0 actually licenses.This is not hypothetical: it landed twice in this lane today. [finding] "no ledger entry is stale" in lint-startup-registry-verdict.corpus.test.ts is green by construction while LEDGER is empty — the one case in that file with no standing non-vacuity proof #10913 found
packages/lint/tsconfig.jsonexcluding**/*.test.ts, so a greentypecheckthere bills none of the test files; [finding]packages/qa/dogfood/tsconfig.jsonincludessrc/**/*, a directory the package does not have #10899 found a tsconfigincludenaming a directory that does not exist. A tsconfig's declared surface routinely differs from the surface people believe it has. -
"tsc reports 0" is a zero-hit reading and needs a positive control. A misconfigured invocation reports 0 too. Show the run is looking at real files.
-
Check whether the package already has a
typecheckscript before adding one; the gate's message assumes it does not.
Zone 3 — suggested routes (not binding)
- If both remedies are available and equivalent under measurement, prefer the one that puts MORE code under tsc — the point of graduating is coverage, not a tidier ledger.
- If they are not equivalent, the honest outcome may be "onboard with the exclusion intact, and file the excluded-tests gap as a separate finding". That is a good result, not a failure.
- The ledger's standing
surplus:note and [finding][devx]check:type-check-debt的 ledger 余量会让新写的 pin 变哑:mongodb 曾有 33 条余量吞掉一次真实回退,另有 5 条目前带 4–19 余量 #6376 are the context for why a stale ceiling matters; read them if you need the argument, don't re-derive it.
Generated by Claude Code
- Re-derive the premise on current
{ "issue": 11204, "status": "done", "branch": "claude/issue-11204-trigger-record-change-graduation", "pr": "https://github.com/objectstack-ai/objectstack/pull/11489", "premise_still_valid": true, "summary": "Premise re-derived on current origin/main (7f30b6be): 'TEST_DEBT records 9, and tsc now reports 0 -- graduation candidate' still prints, so this is a graduation, not a ceiling lowering. Zone 2's warning was RIGHT and the hazard is real, but it arrives through a gate neither the card nor the dispatch named: the two remedies are NOT interchangeable for this package. Remedy 'add a typecheck script' was already done years ago (the package has always had `tsc --noEmit`; its hole was TESTS_COVERED, not COVERED). Remedy 'drop the test exclusion' resolves exactly the 10-file program the ledger scored at 0 (measured by tsc --showConfig) and leaves dist/ byte-identical (tsup builds src/index.ts alone), so both obvious checks pass -- yet it takes `check:type-source-resolution` from exit 0 to exit 1, because the 7 re-admitted tests import @objectstack/driver-sql, /formula, /objectql and /service-automation, which the BUILD config's program never held, against a shrink-only registry whose own message rules that widening the entry is not the fix. So the PR takes the third route, the one AGENTS.md and four sibling packages already prescribe: a sibling tsconfig.test.json named by the typecheck script (`tsc --noEmit && tsc --noEmit -p tsconfig.test.json`), which puts the same 10 files in front of tsc while tsconfig.json -- the only config that gate reads -- stays untouched. The TEST_DEBT entry is deleted in the same commit. No other package's entry was touched: total raw tsc errors across the ledger is 1897 before and 1897 after, while surplus goes 23-across-4 to 14-across-3, retiring exactly the 9-error stale ceiling the card was about.", "tests": "All final readings at branch head `1a383fa3` (git rev-parse --short HEAD, captured inside the same locked run). Every heavy command went through scripts/pm/os-verify-lock.sh; exit codes captured before any pipe; verdicts quoted from each gate's own printed line, never $?.\n\nPREMISE (origin/main 7f30b6be, full closure built first: `turbo run build --filter=./packages/* --filter=./packages/*/*` -> 'Tasks: 70 successful, 70 total', VERDICT command-exit 0, 6m36s): `node scripts/check-type-check-coverage.mjs --re-measure` -> 'check-type-check-coverage --re-measure: OK - 33 ledger entr(ies) re-measured in 273.9s, 1897 raw tsc error(s) total, none above its recorded number.' and the candidate line 'TEST_DEBT records 9, and tsc now reports 0 -- graduation candidate'.\n\nPOSITIVE CONTROL #1 (the 0 is a reading, not a zero-hit). File census first: the gate's generated re-measure project puts 10 of this package's files in the program (3 src + all 7 tests) out of 467 total, by `tsc --listFiles`. Then a planted error. NO REBUILD WAS NEEDED FOR THIS LEG and that is a measured property, not an assumption: the mutated file is the package's OWN source, which tsc reads straight from the checkout (it appears by checkout path in the --listFiles output above); only the DEPENDENCY closure resolves through dist/*.d.ts, and that closure was built immediately before. Mutation confirmed on disk by grep -c of the injected marker (2 hits) AND by git hash-object (3672183f -> 312c5bed), not by the editor's exit code. Mutated leg: 'formula-context.test.ts(115,7): error TS2322' / COUNTED=1 TSC_EXIT=2. Restore leg (trap '<restore>' EXIT INT TERM, plus an explicit restore): marker hits 0, hash back to 3672183f byte-identical, COUNTED=0 TSC_EXIT=0. Direction observed = the predicted one (red under mutation, green restored).\n\nHARNESS AGREEMENT: the single-package harness that ran those legs is a faithful reconstruction of the gate's remeasureProject/measureTestDebt, and it was validated by reproducing the gate's own answer exactly (COUNTED=0 against the gate's 'tsc now reports 0') before being trusted as a 5-second proxy for the 4.5-minute run.\n\nREMEDY COMPARISON (tsc --showConfig, the reading Zone 2 asked for): build tsconfig.json = 3 files, all non-test. Gate's re-measure program = 10 files (3 src + 7 tests) -- so the 0 WAS measured over the enlarged program. Remedy A (exclusion dropped) = the same 10 files; its tsup build produced an identical 6-file dist with 0 test artifacts; but `node scripts/check-type-source-resolution.mjs` went exit 0 -> exit 1: 'NEW dist-resolved type import(s) since this entry was measured: @objectstack/driver-sql, @objectstack/formula, @objectstack/objectql, @objectstack/service-automation.' Both Remedy-A probes were run under a restore trap with the mutation confirmed on disk (exclude-glob grep -c 1->0->1, hash f6a1e8ba -> caff4761 -> f6a1e8ba byte-identical). New tsconfig.test.json (Remedy B) = the same 10 files, module nodenext, rootDir ./src, strict true -- byte-for-byte the file set and options the 0 was measured over.\n\nPOSITIVE CONTROL #2 (on the WIRED script, not just the synthetic project): a planted error in src/record-change-trigger.test.ts took `pnpm --filter @objectstack/trigger-record-change typecheck` from exit 0 to TYPECHECK_EXIT=2 with 'src/record-change-trigger.test.ts(954,7): error TS2322'; restored byte-identical (marker hits 0, hash matches), exit back to 0.\n\nFINAL RUNS AT 1a383fa3: `pnpm --filter @objectstack/trigger-record-change typecheck` -> exit 0, and the echoed script line 'tsc --noEmit && tsc --noEmit -p tsconfig.test.json' proves it was not a zero-match pnpm no-op. `pnpm --filter @objectstack/trigger-record-change test` -> 'Test Files 7 passed (7) / Tests 78 passed (78)'. `pnpm lint` (repo-wide, eslint . --no-inline-config, NOT narrowed) -> VERDICT command-exit 0, 1m43s. GATE FLOOR: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (no paths passed; it derived the change set itself: 3 paths vs merge base 7f30b6beb, and confirmed --repo against this checkout's origin) named 17 families; all 17 exit 0, plus check:nul-bytes and a control-byte grep over the PR body. Load-bearing verdict lines: 'check-type-check-coverage: OK - 65/78 workspace packages type-checked'; 'check-type-source-resolution OK - 77 packages with a tsconfig.json scanned; 51 registered as still resolving a workspace dep's types through dist/'; 'check-type-check-coverage --re-measure: OK - 32 ledger entr(ies) re-measured in 291.6s, 1897 raw tsc error(s) total, none above its recorded number.' Ledger deltas 7f30b6be -> 1a383fa3: entries 33->32, packages hiding tests 20->19, hidden files 1048->1041, frozen TEST_DEBT raw errors 1484->1475, surplus 23-across-4 -> 14-across-3, TOTAL raw errors 1897->1897 (the check that no other ceiling moved).", "open_questions": [], "out_of_scope_findings": [ "filed as #11490: `check:type-source-resolution` reads each package's `tsconfig.json` and only that one, so the sibling `tsconfig.test.json` this repo prescribes as its own repair (now 5 packages, including this PR's) is a whole tsc program outside the gate's declared population - measured both spellings of the identical 7 files, one reported and one silent", "filed as #11491 (label `finding`): the TEST_DEBT graduation message joins its two remedies with 'or' and nothing says they can differ - this card is a worked counterexample in both directions, and 19 packages are still queued to read that message" ] }
Generated by Claude Code
ACCEPT — PR #11489, head
1a383fa3.The dispatch's hazard was real, and the dev found it somewhere better than I pointed
Zone 2 said the two advertised remedies might not be interchangeable, and told the dev to measure the resolved program before and after with
tsc --showConfig. That instruction would have cleared the change:--showConfigunder Remedy 2 resolves exactly the 10-file program (3 src + 7 tests) the ledger scored at 0, anddist/comes out byte-identical because tsup buildssrc/index.tsalone. Both readings I named say "safe".The failure lives in a third reading neither the card nor I named:
check:type-source-resolutiongoes exit 0 → exit 1, because the 7 re-admitted tests import@objectstack/driver-sql,/formula,/objectqland/service-automation— four workspace packages the build config's program never contained — against a shrink-only registry whose own message rules that widening the entry is not the fix.So the answer is better than the question: the remedies are non-interchangeable, but not for the reason I predicted. Recording that distinction, because a PM assumption that is right for the wrong reason is still a bad assumption to inherit.
Also worth stating: Remedy 1 was already done years ago. The package has always carried
tsc --noEmit. It was never in DEBT's hole ("src does not check") — it was in TEST_DEBT's ("src checks, tests are hidden"). The gate's first branch had nothing to do here, so the message was 0-for-2 on this package.The third route is the repo's own prescribed repair — verified, not taken on trust
This is the part I checked hardest, because on its face a sibling
tsconfig.test.jsonmakes a red gate green by keeping the program outside that gate's population, and that shape deserves suspicion.- AGENTS.md
:60–65states it: "Neverexclude*.test.ts… from a package'stsconfig.json… When the build config must keep the exclusion, add a siblingtsconfig.test.jsonand name it in thetypecheckscript (thepackages/specpattern); the sibling may carry its own module semantics to match vitest, never its own strictness." - Exactly four sibling packages carry one on
main:client,metadata-core,metadata-fs,spec. Matches the report. - The precondition — "the build config must keep the exclusion" — was measured, not preferred: dropping it reds a required gate whose sanctioned remedy is explicitly a separate card.
And the sibling honours the AGENTS.md constraint precisely:
module,rootDirand strictness are all left inherited, on the stated ground that "a differentmodulewould be a different measurement" — matching the program the0was scored over. That is the right instinct, and it is the opposite of the shortcut (loosen something until it compiles).Rulings, checked against the patch rather than the prose
ruling status 1 — re-derive the premise on current main✅ re-run at 7f30b6be, candidate line still prints2 — onboarding + ledger deletion in the SAME PR ✅ both in the one commit 3 — ⛔ touch no other package's entry ✅ verified from the diff: only the trigger-record-changeline is removed;plugin-security,formula,verify,connector-mcp,connector-openapisit unchanged in the context linesRuling 3 has a second, independent witness: total raw tsc errors 1897 → 1897 while entries go 33 → 32 and surplus goes 23-across-4 → 14-across-3. A zero-error entry left the ledger and nobody else's number moved.
Positive controls, plural
The
0is a reading rather than a zero-hit, established twice — once on the synthetic re-measure project (planted TS2322 →COUNTED=1 TSC_EXIT=2; restored →0/0, blob hash back to3672183f) and again on the wired script (pnpm --filter … typecheckexit 0 → 2 → 0). Every mutation confirmed by grepping the marker and comparing the blob hash, never by an editor's exit code, each under a restore trap. The harness used for the fast legs was itself validated by reproducing the gate's own answer before being trusted as a proxy — a control on the control.The tension, stated plainly
Remedy B leaves those four dist-resolved imports exactly where they were; it just puts the program that reveals them where
check:type-source-resolutiondoes not look. The dev surfaced this themselves rather than banking it, and filed #11490.I'm accepting anyway, and the reasons are worth being explicit about: the route is the documented one, the precondition was measured, the alternative is blocked by a registry whose own message forbids the widening — and, decisively, the tests really are typechecked now. Coverage went up, not sideways: 7 previously hidden test files now sit in a program with strictness inherited and no
test-typecheck-debt.jsonresidue, so any error there is red immediately with no ledger to be added to.⚠️ But #11490 should be weighted accordingly by triage: the repo's own prescribed repair is invisible to the gate that would police it, now across five packages. That is a structural gap in a required gate's declared population, not a nit.CI
By latest run per name, 33 raw runs, 0 failure, 0 cancelled. The two decisive ones are both green:
Type Check · debt ledgersuccess — the re-measure half.Lint & Repo Gatessuccess — wherecheck:type-source-resolutionactually runs. Given the dev measured that this gate flips to exit 1 under Remedy A, it is sensitive to this diff, so its green is evidence rather than background noise. This is why I held the ACCEPT until it reported.
Second finding
#11491 — the graduation message joins its two remedies with "or" and nothing says they can differ, with 19 packages still queued to read it. That is the generalisation of this card, and it is the more valuable of the two: this PR fixes one package, that card would stop the next nineteen takers from being misled.
Landing via the merge queue once the remaining shards settle. Verification by content on
origin/main.
Generated by Claude Code
- AGENTS.md
LANDED — verified by content on
origin/main, not by sha and not by the merge event.origin/main = 951b025e scripts/check-type-check-coverage.mjs '@objectstack/trigger-record-change': { errors: 9 … → 0 hits (gone) '@objectstack/plugin-security': { errors: 11 … → 1 hit (positive control: the grep matches real ledger lines) packages/triggers/trigger-record-change/tsconfig.test.json present package.json typecheck = "tsc --noEmit && tsc --noEmit -p tsconfig.test.json" carried by: 951b025e build(trigger-record-change): graduate the package out of the TEST_DEBT ledger (#11489)The positive control matters here: "the entry is gone" is a zero-hit reading, and a grep that matched nothing would report the same 0. Confirming a sibling entry still matches is what makes the 0 a removal rather than a broken pattern.
It took two queue attempts, and the first failure was not this PR's
The first queue build (
32673631029) was dequeuedCI_FAILURE. Recording the diagnosis, because the shape is instructive:What failed:
@objectstack/example-showcase#test, specifically
test/vitest-console-teardown-race.test.ts > [#10293] … > ABLATION: the fixture still reddens a green run under vitest defaults, with
AssertionError: the instrument stopped reproducing: 8 attempts under vitest defaults produced no "Closing rpc while \"onUserConsoleLog\" was pending".Why it ran at all, which is the part worth writing down: this PR edits
packages/triggers/trigger-record-change/package.json, andexamples/app-showcase → @objectstack/cli ─┐ → @objectstack/plugin-approvals ─┴→ @objectstack/trigger-record-changeso the package-hash change invalidated turbo's task cache for every dependent. 69 of 75 tasks in that run were cache-restored;
example-showcase#testwas one of the 6 that actually executed. Nothing in this diff can affect a vitest run — atypecheckscript, a new tsconfig, and a ledger line — so the diff changed whether the task ran, not how it behaved.Why it is a flake and not a real failure: the test is a non-vacuity guard whose job is to provoke a known vitest console-forwarding race and go red if the fixture stops reproducing it. Provocation is probabilistic — 8 attempts — so a scheduling difference can make it report "the instrument stopped reproducing" when nothing changed. The single sanctioned re-run passed, which is what distinguishes this from a real regression.
⛔ Nothing was skipped, disabled, or quarantined to get here, no empty commit was pushed, and the PR was re-queued exactly once.
⚠️ Worth noting for whoever owns that test: its failure message is, by construction, indistinguishable from the genuinely important case — "the underlying bug is fixed, retire the fixture". A probabilistic guard that cries wolf trains readers to dismiss the one signal it exists to send. Not filed as a card here: the repo's merge-queue-triage bot already aggregates by signature and opens a summary issue once a second PR hits the same one.⚠️ But that bot reported its own 24h count as a lower bound (it could not read the comment ledger to the end of the window), so the aggregation may under-trigger.pm:dispatchedremoved. The card was auto-closed by the merged PR.
Generated by Claude Code
Observed while running the derived gate set for #10746 (full packages closure built, then
pnpm check:type-check-debt --re-measure, 2026-08-23, tree atorigin/mainmerge-base2f93d0f53):The gate's own message names the remedy: onboard the package (add a
typecheckscript or drop the test exclusion) and delete the TEST_DEBT ledger entry in the same PR. Until someone does, the entry holds a 9-error ceiling over a package that is now clean — room for regressions to land in that layer without any gate saying anything (the same surplus class as the ledger's standingsurplus:note, see #6376).Observation only — no defect today; the run itself was green (
check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured … none above its recorded number). Filed unassigned per the out-of-scope-findings rule; searched open issues fortrigger-record-changegraduation first and found none (nearest siblings are per-package surplus findings like #10615, different package).