Filed separately as triage directed on #8140 (2026-08-12: "A client-side check:exported-any
equivalent gate is a natural follow-up; file it separately rather than growing this card"), and
again in its 2026-08-25 dispatch. Observation class: this proposes a ratchet, it does not report a
broken one.
Measured at 1f6d04703.
The gap, re-verified at head
check:exported-any is declared only in packages/spec/package.json and is scoped to that
package's built dist (packages/spec/scripts/check-exported-any.ts). No other package
registers it.packages/client's whole script set is build, test, test:integration,
check:test-typecheck, typecheck. None of them inspects what an export resolves to.- Repo-wide the only
any-shaped lint rule is query-options/no-any-erasure
(eslint.config.mjs), which bans erasing an engine query-options argument at a call site. It
never reads a declared return type.
That reproduces the #8140 census's reading of 2026-08-20 independently.
Why now specifically
#8140 just bound 51 return-type sites across packages/client/src/index.ts. That buys a
snapshot, not a property. The card's own census put it plainly: "Nothing stops the 33rd from
landing tomorrow. A card that fixes 49 sites without adding a ratchet buys a snapshot, not a
property."
And the surface is measurably still moving: implementing #8140 turned up a fifth erasure
spelling the census's greps could not see — 38 methods with no return annotation at all, typed from
unwrapResponse< …any… > (filed as #11925). A source-text grep for Promise< any > would not
catch those; a gate reading the built .d.ts would, because it asks what an export resolves to
rather than how it is spelled. That is the whole reason check:exported-any exists in
packages/spec:
the api-surface snapshot records that an export exists, never what it resolves to
Suggested shape
Scope note
⛔ Not built inside #8140, on triage's instruction. Filed unassigned; the sizing above is a
suggestion for whoever grades it, not a plan.
Generated by Claude Code
Filed separately as triage directed on #8140 (2026-08-12: "A client-side
check:exported-anyequivalent gate is a natural follow-up; file it separately rather than growing this card"), and
again in its 2026-08-25 dispatch. Observation class: this proposes a ratchet, it does not report a
broken one.
Measured at
1f6d04703.The gap, re-verified at head
check:exported-anyis declared only inpackages/spec/package.jsonand is scoped to thatpackage's built
dist(packages/spec/scripts/check-exported-any.ts). No other packageregisters it.
packages/client's whole script set isbuild,test,test:integration,check:test-typecheck,typecheck. None of them inspects what an export resolves to.any-shaped lint rule isquery-options/no-any-erasure(
eslint.config.mjs), which bans erasing an engine query-options argument at a call site. Itnever reads a declared return type.
That reproduces the #8140 census's reading of 2026-08-20 independently.
Why now specifically
#8140 just bound 51 return-type sites across
packages/client/src/index.ts. That buys asnapshot, not a property. The card's own census put it plainly: "Nothing stops the 33rd from
landing tomorrow. A card that fixes 49 sites without adding a ratchet buys a snapshot, not a
property."
And the surface is measurably still moving: implementing #8140 turned up a fifth erasure
spelling the census's greps could not see — 38 methods with no return annotation at all, typed from
unwrapResponse< …any… >(filed as #11925). A source-text grep forPromise< any >would notcatch those; a gate reading the built
.d.tswould, because it asks what an export resolves torather than how it is spelled. That is the whole reason
check:exported-anyexists inpackages/spec:Suggested shape
packages/spec/scripts/check-exported-any.tsalready does this jobagainst a built
dist. The work is generalising it to take a package and wiring ashrink-only baseline, not writing a second detector.
Promise< any >on purpose after client SDK drops the precise spec types at its boundary: 32 methods returnPromise< any >on a package that already depends on@objectstack/spec#8140 —automation.create,automation.update,search,data.clone— because no contract exists tobind (Four client SDK routes answer a shape no published contract declares —
automation.create/automation.update/search/data.clone#11924). A gate demanding zero would either block on Four client SDK routes answer a shape no published contract declares —automation.create/automation.update/search/data.clone#11924 or invite someone to mint afalse declaration to get green. A shrink-only ledger with those four named, and their reason
recorded, is the honest starting count.
data.*andactions.*take acaller-supplied
< T = any >by design — the record type and the action handler's payload reallyare the caller's. client SDK drops the precise spec types at its boundary: 32 methods return
Promise< any >on a package that already depends on@objectstack/spec#8140 left those alone deliberately. A detector that reads a resolveddisttype needs to distinguish "the default is
any" from "this method has no business knowing theshape", or it will produce exactly the pressure that turns a correct generic into a wrong
concrete type.
packages/client-reactis the obvious second consumer once the shape works for one package.Scope note
⛔ Not built inside #8140, on triage's instruction. Filed unassigned; the sizing above is a
suggestion for whoever grades it, not a plan.
Generated by Claude Code