Uh oh!
There was an error while loading. Please reload this page.
test(devx): give the ref'd-timer instrument a home every package can reach, and gate the raw probe - #10905
test(devx): give the ref'd-timer instrument a home every package can reach, and gate the raw probe#10905claude[bot] wants to merge 1 commit into
Conversation
…reach, and gate the raw probe The class — "a process-global `getActiveResourcesInfo()` timer count scored across an `await`" — has been found five times over four cards, twice as a shard-only intermittent red (once in CI, once in the merge queue, where it stalls every lane at once). This converts the fifth site, closes the class with a gate, and converts the one leg whose repair could still hide a real leak. The load-bearing decision is where the instrument lives, because `service-automation` could not reach `packages/core/src/refd-timer-probe.testkit.ts` at all. It moves to a private, test-only workspace package `@objectstack/refd-timer-testkit`, reached by a bare specifier. The two alternatives were measured, not argued: a relative import into another package's `src` lands TS6059 in the consumer's own frozen type-check debt (3 -> 4, a shrink-only ledger with no tolerance band), and publishing a subpath on `@objectstack/core` would put a test helper on a runtime package's public npm surface under semver. Closes#10783Closes#10785
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2c73bd691ecfe40874768437c04fea7aec1464a8 && git checkout 2c73bd691ecfe40874768437c04fea7aec1464a8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 58563be069a70d09c4d9cc5f522bb27c5f8a78b2 46fa7102a1ebee531938685eaed7c03516b943aa && git checkout -B drift-repro 58563be069a70d09c4d9cc5f522bb27c5f8a78b2 && git merge --no-ff 46fa7102a1ebee531938685eaed7c03516b943aa
node scripts/docs-audit/affected-docs.mjs --json 58563be069a70d09c4d9cc5f522bb27c5f8a78b2 |
Closes#10783
Closes#10785
The class — a process-global
getActiveResourcesInfo()timer count scored withtoBeacross anawait— has been found five times over four cards. Twice the first signal was a shard-only intermittent red whose message names a timer count rather than the change that caused it: once in CI (expected 2 to be 4), once in the merge queue, where it stalls every lane at once. This PR converts the fifth site, closes the class with a gate, and converts the one leg whose earlier repair could still hide a real leak.The home decision, and the cost accepted
This is the load-bearing part, so it leads.
@objectstack/service-automationcould not reachpackages/core/src/refd-timer-probe.testkit.tsat all —@objectstack/coreexports exactly.and./logger. The three candidate homes were measured, not argued:packages/core/src, declared inCROSS_PACKAGE_TEST_INPUTSTS6059@objectstack/core'sexportsWhy the relative import lost. I built it first: the declaration and the turbo input glob both landed,
check:cross-package-test-inputswent fromOK: 13 package(s)toOK: 14 package(s), and both suites were green. Thentsc -p packages/services/service-automation/tsconfig.json:That package's tests are in its tsc program (its DEBT entry says so and
--listFilesconfirms it), so the count goes3 → 4.check-type-check-coverage.mjscounts TS6059 for a package's own config deliberately — the asymmetry is documented atTSC_ROOTDIR_DIAGNOSTIC— and its own self-test pins “a count that grew by one is red too — no tolerance band”, with the raise marked⛔ MAINTAINER-ONLY. So this route's only exit is a maintainer-only ratchet weakening. It is also not a quirk of one manifest: most packages here setrootDir: src, so it would have been a trap for the next adopter too.Why the
exportssubpath lost. It would put a test helper on a runtime package's public npm surface, under semver, for every consumer of the kernel — capability expansion with no external pull, against the startup scope-discipline principle. It would also need a tsup entry, contradicting the instrument's own documented property that tsup never bundles it intodist.What was chosen, and what it costs.
packages/qa/refd-timer-testkit—private: true, no build,exportsstraight atsrc, consumed as adevDependency. Measured: a bare specifier resolved throughnode_modulesis exempt fromrootDir(verified on a scratch fixture with--noEmitand with emit), so it is reachable from any consumer whatever its own build config says.service-automation's debt is back to exactly 3, its frozen value.The cost accepted is a new workspace package — a manifest, a tsconfig, and one more row in the type-check-coverage census (
64/77 → 65/78, arriving covered, debt unchanged at 436). The instrument also moves out ofpackages/core, so four core suites change one import line each.The one thing this had to not break is turbo's Layer B, because a bare specifier is deliberately invisible to
check:cross-package-test-inputs. Measured rather than assumed, onturbo run test --dry=json:@objectstack/refd-timer-testkit#buildis a real graph node withinputs: ["package.json","src/index.ts","tsconfig.json"], and editing the instrument moves@objectstack/service-automation#test's hashee8253e9cb374faa → 77680cb1d4bdd2d4. No hand-written input glob needed; the dependency edge carries it.⛔ Not considered: forking a second copy into
service-automation. Both cards turn it down, and the gate below would red on it anyway.1. The fifth site (#10783)
packages/services/service-automation/src/engine.test.ts, at:2012–:2025on the base commit — re-derived, matching the card. A localrefdTimers()sampled either side ofawait engine.execute('guarded_flow')and scored withtoBe(before). It was green only becauseregisterInstantScriptresolves on a microtask — a property of code the test does not own. Now:2. The gate (#10785 part 1)
scripts/check-refd-timer-probe.mjs+pnpm check:refd-timer-probe, wired intolint.yml.It bans the raw probe outside one approved module — the cards' cheap, robust formulation. It does not attempt "is there an
awaitbetween these two readings": that is an AST question, and a text scan answering it approximately fails silently on spellings it does not know, which is the failure mode AGENTS.md already records for source-scanning gates. A gate whose default is a silent pass is the failure mode, not a weaker fix.Two design points worth review:
process.receiver. A member access is one route;const { getActiveResourcesInfo } = process, a named import fromnode:processandprocess['…']are others, and matching the receiver leaves a silent hole per route. The one evading spelling — a name assembled at runtime — is stated in the header rather than left to be discovered.js-comment-mask.mjs). The instrument's docblock, four core suites' fake-timer comments and the gate's own header all have to name the banned probe to explain the rule. A gate that forces authors to reword prose to dodge a scanner teaches them the scanner is noise.It also cannot report a vacuous green: "nobody reads the probe" is equally true of a tree where the instrument was deleted or gutted, so the sweep asserts the approved module is present and still reads it.
Registration. A
check:*key in rootpackage.json, the dominant convention (70+ keys), so the remedy the gate prints is a command that exists. The #9465 fence scopes that file's@changesets/clirange and itsversionscript — the parenthetical in the issue body is scoping, not illustrative — not the file. #10785's body says otherwise; that is the over-broad reading. This is not my claim alone: theStep-collector gatecomment landed onmaintoday says the same thing in as many words, while choosing the other spelling on its own merits.3. The
toBeLessThanOrEqualleg (#10785 part 2)packages/core/src/timeout-guard.test.ts. #10661's repair was deliberate and documented, so it is superseded with an explanation, not silently overwritten — the docblock now carries both the original reasoning and why the instrument replaces it. All three legs move torecordGuards/stillPinningTheLoop, and the assertion is an exacttoBe(0)again. The third leg's guard is supposed to fire; what it pins is now stated rather than implied.The file's other probe use —
arms a REF'D timer— is a genuinely synchronous window, so it keeps the raw reading, now via the instrument'srefdTimeouts(), with a comment saying exactly which property makes it sound.Ablations — predictions written down before each run
Every mutation was proven on disk by marker counts (an editing tool's exit code is not evidence), and every restore leg proved
git diff --quiet.A — the converted pin reds on a real leak. Delete
clearTimeout(this.timer)fromTimeoutGuard.reclaim(). Predicted: leg 1 red withexpected 1 to be +0, plus two pre-existing sibling pins. Observed, exactly:B — the fifth site reds on a real leak. Delete
clearTimeout(guard)fromexecuteWithTimeout'sfinally. Predictedexpected 3 to be +0. Observed verbatim:AssertionError: expected 3 to be +0.C — the gate's self-test cannot pass without the rule. Make⚠️ The mechanism differed from my prediction: I expected the red cases to fail as "expected red, got green", but gutting the detector trips the vacuous-green limb first, so most fail as "red as expected, but the message does not name …". Direction right, mechanism not as written — recorded rather than smoothed over.
probeSitesIn()return[]. Predicted exit 1. Observed exit 1, 9 cases failing.D — the card's part-2 arithmetic, reproduced. Same leaking
reclaim(), plus two foreign timers armed to expire inside the window (modelling shard load deterministically instead of waiting for a flaky shard). Scratch harness, deleted;git diff --quietclean after.subject +1, two foreign −2, reading = before − 1⇒toBeLessThanOrEqualpassed on a real leak, while the instrument reported1on the identical tree. That is #10785 part 2, measured.The gate's real-tree positive control. Before the conversions, on the tree as it stood, the gate named all three live sites and exited 1 —
timeout-guard.test.ts:18,timeout-guard.test.ts:162,engine.test.ts:2013. Turning those green is what this PR does.Verification
Gate union re-run after the final commit, at
46fa7102a1, all green — including every familynode scripts/pm/dispatch-gates.mjsderived from the real diff:check:refd-timer-probe·check:cross-package-test-inputs(13 packages, unchanged) ·check:entry-guard·check:parse-guard·check:nul-bytes·check:ratchet-remedy-authority(117 swept; this gate classifies excluded — it hands out no ratchet-expanding remedy, so it needs no control entry) ·check:type-check-coverage(65/78, debt 436 unchanged) ·check:test-source-alias·check:type-source-resolution·check:slot-lookup·check:query-options-erasure·check:engine-double-contract·check:where-matcher·check:kernel-hook-pairs·check:required-contexts·check:shard-attestation·check:workflow-status-functions·check:node-version·check:override-consistency·check-step-collectors·check-ci-filter-parity·check-changeset-fixed·check-plugin-teardown-shape.Tests at the same commit:
@objectstack/core37 files / 887 tests passed,@objectstack/service-automation84 files / 998 tests passed,@objectstack/refd-timer-testkittypecheck clean.Declared narrowing:
check:type-check-debt --re-measurewas not run locally — it refuses without the full ledgered dependency closure built (35 packages). Instead I measured the one package this PR could have moved, directly:tsc -p packages/services/service-automation/tsconfig.jsonreports exactly 3 errors, its frozen value, with the TS6059 gone. CI runs the farm version.No changeset: test files, a private (unpublished) package, a gate script and its registration. No published package's contents change — the two
devDependencieslines are not installed by consumers.skip-changesetapplied.Concurrency check: two siblings were in flight on
scripts/**. Both landed while this branch was open (c9636de1fd,409077e93c, touchingcheck-type-check-coverage.mjsandcheck-dispatcher-error-vocabulary.mjs) and were merged in cleanly; this diff touches neither file. Worth flagging as a near miss:git reset --soft origin/mainre-anchors to whereverorigin/mainpoints now, not to the commit you merged, so both of those landed commits briefly appeared inside this branch as deletions — silently, exit 0. Caught by readinggit statusbefore pushing; the final diff is the 14 files listed above and nothing else.Generated by Claude Code