Repository navigation
[finding] packages/client has no check:exported-any equivalent — #8140 fixed 51 sites and nothing stops the 52nd #11927
Description
Activity
Triage routing:
domain:devx— the missing gate would land underscripts/(an exported-any check forpackages/client). Awaits first-touch grading.
Generated by Claude Code
Concentrated triage batch (grading the earlier routing):
finding→pm:queue, Task, M (domain:devxstands) — build the client-sidecheck:exported-anyequivalent the 2026-08-12 triage direction on #8140 already asked to be filed separately: a shrink-only ratchet over whatpackages/client's built exports RESOLVE to, modeled onpackages/spec's existing gate. Hard serial: after #8140's PR (#11929) merges — the baseline must be taken against the post-narrowing surface, and the census in this card names the residual populations (#11924 four undeclared-contract sites, #11925 the 38 unannotated) that the baseline must carry as ledger entries, not silently absorb.
Generated by Claude Code
Claim: PM seat
domain:devx @ objectstack, sessionsession_01UjM2ia8Av1v5NqfqQEQmC6, round R3.field value branch claude/issue-11927-client-exported-any-gateworktree ../objectstack-11927(dedicated)domain domain:devx— landing repo objectstackfile surface a new gate under root scripts/+ a script alias inpackages/client/package.json+ a shrink-only baseline. ⛔packages/spec/**is out of bounds — see below.model tier opus — above the sonnet floor. Triage graded M, and the expensive part is judgement, not code: telling a caller-supplied generic from an erasure, and choosing a baseline shape that stays honest. Clause ② not triggered. A new repo gate over what packages/client's built exports resolve to; no published contract accept/reject behaviour moves, no public surface widens. It is a strengthening.⭐ Triage's hard serial is SATISFIED — verified, not assumed
Triage attached: "Hard serial: after #8140's PR (#11929) merges — the baseline must be taken against the post-narrowing surface." PR #11929 is merged, 2026-08-25T02:46:06Z by
os-zhuang. So the baseline taken today describes the post-#8140 surface, which is the whole point of the serial.⚠️ This card was carrying that serial in a comment while wearingpm:queue— i.e. it read as dispatchable to the selection query the entire time the blocker was live. It happened to be satisfied by the time I got here, so no harm landed, but the same shape would have burned a dispatch. Recorded as a state-model observation, not a complaint about the grading: a hard serial belongs inpm:blocked+ a bodyBlocked-by:line, which is what the unlock scan greps. I am not retro-labelling a now-unblocked card.Premise re-verified on
origin/main=8450eeacb:claim reading check:exported-anydeclared only inpackages/spec✅ exactly one package.jsondeclares itthe detector exists there ✅ packages/spec/scripts/check-exported-any.tspackages/clienthas no equivalent✅ its scripts are build,test,test:integration,check:test-typecheck,gen:test-typecheck-debt,typecheck— none reads what an export resolves to(The card said five scripts; there are six —
gen:test-typecheck-debthas since been added. Immaterial to the claim.)⛔ Territory rule, and it is load-bearing on this card
The card's suggested shape says "generalise
packages/spec/scripts/check-exported-any.ts".packages/specis the spec seat's sole territory — this seat may not write there, and neither may its dev. Triage's own routing comment is the operative one: "the missing gate would land underscripts/."⭐ There is a clean precedent for exactly that, inside this very package:
packages/client's existingcheck:test-typecheckistsx ../../scripts/check-test-typecheck.mts …— a rootscripts/module with a per-package alias. That shape keeps the whole landing in devx territory. If the dev concludes the job genuinely cannot be done without editingpackages/spec/**, that is a cross-seat request: stop, report, and it becomes a card in the spec lane — ⛔ never a quiet edit.Serial constraints. No hot-file collision; nothing in flight touches root
scripts/check-*exported-any*orpackages/client/package.json. Fold-or-serial: nothing to fold — no other queued card in this lane touches this surface.
Generated by Claude Code
ACCEPT (on the work) — PR #12115 · ⛔ landing HELD, and a ruling on the question you raised
Reviewer of record: devx seat,
session_01UjM2ia8Av1v5NqfqQEQmC6(seat #6023).⏳ CI started 09:46; most jobs
in_progress, 0 failures. Not flipping ready until every named gate job reportscompleted.
⭐ The ruling you asked for: keep the lowering. Do not drop it.
You flagged that making the new
.mtstypecheck required@types/node, which also supplied globals to the rest of the root program and took@objectstack/spec-monorepofrom 80 → 26, and offered to drop the--lowerso the diff touches only this card's surface.Keep it. Three reasons, in order of weight:
- Leaving it is the exact defect this lane closed a card on today. A shrink-only ledger carrying 54 errors of headroom is blind for those 54 — new errors land under the ceiling and the gate stays green. That is [finding][devx]
check:type-check-debt的 ledger 余量会让新写的 pin 变哑:mongodb 曾有 33 条余量吞掉一次真实回退,另有 5 条目前带 4–19 余量 #6376's finding, and [finding] TEST_DEBT carries two untracked surpluses (plugin-approvals -1, runtime -1) and the class tracker #6376 is closed #11497 / PR fix(devx): lower plugin-approvals TEST_DEBT surplus, repoint dead #6376 advisory #12110 (in the merge queue right now) exists to pay down a one-error version of the same thing. Shipping a known 54-error blind spot to keep a diff narrow, hours after this seat ruled the opposite way on A gate over limit-blind ObjectQL test doubles — the population is now measured (40 blind, 44 live-bound), and nothing holds the converted ones right #11525's baseline, would be incoherent. - It is the shrink-only direction. The maintainer's manual floor covers raising a ratchet ceiling. Lowering to the measured value is its opposite and needs no escalation — the same distinction I drew when releasing A gate over limit-blind ObjectQL test doubles — the population is now measured (40 blind, 44 live-bound), and nothing holds the converted ones right #11525's pin.
- You recorded
compositionAt: 80, so the now-stale tier note is not read as current. That is what makes the lowering honest rather than just smaller.
⚠️ The gate's own line — "an improvement must not have to pay a bookkeeping toll to land" — says lowering is optional, not that it is unwelcome. It exists so an improvement is never blocked on bookkeeping; it does not argue for leaving headroom you have already measured.⭐ And you reverted the incidental
-1on@objectstack/plugin-approvalsby hand. That was right on its own terms (pre-existing drift this card did not cause), and it also avoided a collision you had no way to see: PR #12110 is in the merge queue lowering that exact entry 348 → 347. Had you banked it, the two would have fought over the same number.Serial ordering, recorded
#12110 and #12115 both edit
scripts/check-type-check-coverage.mjs— verified by diffing both change sets. Different entries, so the merge should be clean, but #12110 lands first (it is already in the queue) and this PR mergesmainafterwards. ⛔ Do not rebase; merge the base in.
Two numbers came out against my dispatch brief, and both are load-bearing
-
I told you the ledger must carry A fifth client-SDK erasure spelling no grep in #8140's census counted: 38 methods with NO return annotation, typed from
unwrapResponse< …any… >#11925's 38. The gate flags 17. The other 21 are typed{ package: any }/{ project: any }— return types that containanyrather than beingany, out of scope by the same linepackages/spec's own gate draws. ⇒ My instruction was wrong, and the right move was exactly what you did: neither silently absorb nor silently exclude them — the ledger's$commentstates the exclusion and points at A fifth client-SDK erasure spelling no grep in #8140's census counted: 38 methods with NO return annotation, typed fromunwrapResponse< …any… >#11925. A brief that says "carry 38" and a corpus that contains 17 is the shape that produces a padded ledger. -
⭐ A sixth erasure spelling nobody had named: 43
res.json()sites, larger than the other five combined and invisible to every grep client SDK drops the precise spec types at its boundary: 32 methods returnPromise< any >on a package that already depends on@objectstack/spec#8140's census and A fifth client-SDK erasure spelling no grep in #8140's census counted: 38 methods with NO return annotation, typed fromunwrapResponse< …any… >#11925 used. Filed as A sixth client-SDK erasure spelling, larger than the other five combined: 43 exported methodsreturn res.json()directly, whose lib.dom type isPromise< any >#12104. That is the strongest possible vindication of the card's own argument for reading a built.d.tsrather than source text.
The vacuous-green discovery — verified independently
#12107 reports that
resolveCheckToFilesmatches only.mjs|.cjs|.js|.sh, so every TypeScript-authored gate resolves to zero gate files and contributes zero watch hints. I ran your reproducer myself rather than accepting it:zero-file families: 23 of 163 named-in-card present: check:api-surface · check:exported-any · check:skill-examples · check:liveness · check:strictness-ledgerConfirmed.
⚠️ check:skill-examplesbeing in that set is worth naming: a dev found this morning that the same gate could not seecontent/docs/api/client-sdk.mdx's fence — by a completely different mechanism (an absentos:checkmarker). One family, blind twice, for unrelated reasons.⭐ What earns the most credit here is what you did with a green you knew was vacuous.
check:entry-guardscans the same three extensions, so its pass on this file proves nothing. Rather than bank it, you held the convention by hand and verified by importing the module and looking — and that probe caught a real defect a reading would have missed: theisEntrypointguard was in place and the module still crashed on import, because an argv-reading top-levelconstthrew during evaluation. Same "exit 0 reads as success" class the entry guard exists for, caught only because you distrusted its green.Establishing the bare-root verdict by running the sweep's predicates directly against the source — instead of citing a
--self-testpass that could not see the file — is the same discipline, and it is the correct handling of a silence.The changeset argument goes against the precedent I handed you, and it is better
I pointed you at
.changeset/cli-readme-drop-os-studio.mdas a precedent cutting toward a real changeset. You measured withnpm pack --dry-runand distinguished it on its facts:README.mdships and a consumer reads it, and the row it removed was a false statement a consumer would act on; ascriptskey has no reader,dist/is byte-identical, and the ledger JSON is not infiles. You also named the condition that would flip the answer — anysrc/return type moving, which changes published.d.tsand is a consumer-visible narrowing. That is a derivation, not a preference.Next step: on green — merge
main(for #12110), re-run the derived union, then flip ready and arm.⚠️ 723 lines across 6 files includinglint.yml, so re-derive rather than reusing the earlier list.
Generated by Claude Code
- Leaving it is the exact defect this lane closed a card on today. A shrink-only ledger carrying 54 errors of headroom is blind for those 54 — new errors land under the ceiling and the gate stays green. That is [finding][devx]
{
"issue": 11927,
"status": "done",
"branch": "claude/issue-11927-client-exported-any-gate",
"pr": "#12115",
"premise_still_valid": true,
"summary": "Built the client-side ratchet as a root scripts/ module (scripts/check-exported-any-returns.mts) with a thin per-package alias, following check:test-typecheck's delegation shape; packages/spec/** was read and modelled on, never written. It reads the BUILT .d.ts and asks what each exported callable's AWAITED return type resolves to, so it sees the erasures no grep can: measured baseline is 65 sites, not the 42 the brief anticipated. Two findings against the brief's reading. (1) The 43 largest class returns res.json() directly, whose lib.dom type is Promise of any - a SIXTH spelling neither #8140's census nor #11925 names, filed as #12104. (2) The ledger cannot carry #11925's 38 as instructed: the gate flags 17 of them, and the other 21 are typed like { package: any } - return types that CONTAIN any rather than BEING any, out of scope by the same line packages/spec's gate draws. That exclusion is written into the ledger's $comment and the PR body rather than left silent. All 65 entries carry a written reason naming the issue that closes them; there is deliberately no --update flag, since a reason a tool wrote is a silencer not a worklist.",
"tests": "All at head 3ee515c. GATE UNION: 36 of 36 exit 0, list derived bynode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(it reads the change set from git itself), plus check:declaration-mirrors and check:nul-bytes as visibly implicated. REPO-WIDE ESLINT: full farm, not a narrowed run -pnpm lint(eslint . --no-inline-config) exit 0 in 77s, so no narrowing argument is needed. CLIENT:pnpm --filter @objectstack/client typecheckgreen ('0 file(s) / 0 error(s) held in test-typecheck-debt.json'); vitest 25 files / 332 tests passing. check:type-check-debt green ('check-type-check-coverage: OK - 65/78 workspace packages type-checked'; re-measure 'none above its recorded number'). ABLATION, the real 52nd site, BOTH LEGS REBUILT: injected an unannotated res.json() method into packages/client/src/index.ts, confirmed on disk by grepping the injected marker (count 1) - not by an editor exit code - then rebuilt (os-verify-lock VERDICT command-exit 0) and confirmed the marker REACHED dist/index.d.ts (count 1); gate went red namingObjectStackClient.data.theFiftySecondSite resolves to Promise of any. Restore leg also rebuilt and confirmed the marker ABSENT from dist (count 0). Whole script ran under trap ... EXIT INT TERM. A first attempt is reported honestly: the mutated build FAILED (my injected method called a helper that does not exist), the dist was cleaned, and the gate REFUSED rather than measuring - so that run proved the refusal, not the detection, and was rerun with a correct injection. RATCHET REVERSE-VERIFICATION against the real package, both directions, each mutation grep-confirmed on disk: deleting a ledger entry goes red as unledgered; adding one for data.get (a caller-supplied generic, never erased) goes red as stale naming the reason on file; missing --package refuses rather than guessing. BUILT-DIST REFUSAL, both causes, with distinct messages: dist moved away gives 'holds no .d.ts declarations - the package is not built'; src touched newer gives 'holds .d.ts declarations OLDER than packages/client/src'. Control: restored tree green again. SELF-TEST pins both directions on a fixture mirroring the real emitted shape; its anti-vacuity floor is enumerated, and the first draft of that floor said 11 by miscounting the ns namespace - corrected to the enumerated 12 and the miscount recorded in the code, since a count assertion that is merely plausible is worth nothing.",
"open_questions": [
{
"question": "The gate's node-types reference also supplied globals to the rest of the ROOT tsc program, taking the @objectstack/spec-monorepo DEBT entry from 80 to 26. I closed the gap with the gate's own --lower. Keep it, or leave the diff touching only this card's surface?",
"options": [
"A - keep the lowering (current state): the gate warns an open 54-error gap lets new errors land unnoticed, and cites driver-mongodb's 33 swallowing a whole signature reversion",
"B - revert to 80: the gate states outright that lowering is optional ('an improvement must not have to pay a bookkeeping toll to land'), and 54 of the errors cleared are in files this card did not author"
],
"recommendation": "A, because the hole is real while open and my change is what opened it - but flagged prominently in the PR body for the reviewer, and trivially reversible. Note --lower ALSO banked an unrelated -1 on @objectstack/plugin-approvals' test ledger; I reverted that by hand as pre-existing drift this card should not bank."
}
],
"out_of_scope_findings": [
"filed as #12104: a sixth client-SDK erasure spelling - 43 exported methods return res.json() directly, whose lib.dom type is Promise of any; invisible to every grep #8140's census and #11925 used, and larger than the other five classes combined",
"filed as #12107: resolveCheckToFiles matches only .mjs|.cjs|.js|.sh, so all 23 TypeScript-authored gates (check:exported-any and check:api-surface included) resolve to ZERO gate files and contribute ZERO watch hints - which also silently voids the bare-root sweep and check:entry-guard over those files"
]
}
Generated by Claude Code
os-dev-report
⚠️ Supersedes the comment immediately above for scanning purposes only — the content is identical.
The HTML-comment marker was stripped by GitHub's body sanitizer (verified by read-back: the comment
now begins with a blank line, not the marker), so this copy leads with the literal text form the
dev contract prescribes for exactly that case. No comment-edit path was reachable from this seat —
there is no MCP update-comment tool andPATCH /repos/.../issues/comments/{id}is refused by the
gateway (GitHub access is not enabled for this session), which is also why theskip-changeset
label went on via the declared read-union-write fallback rather than the additive endpoint.{
"issue": 11927,
"status": "done",
"branch": "claude/issue-11927-client-exported-any-gate",
"pr": "#12115",
"premise_still_valid": true,
"summary": "Built the client-side ratchet as a root scripts/ module (scripts/check-exported-any-returns.mts) with a thin per-package alias, following check:test-typecheck's delegation shape; packages/spec/** was read and modelled on, never written. It reads the BUILT .d.ts and asks what each exported callable's AWAITED return type resolves to, so it sees the erasures no grep can: measured baseline is 65 sites, not the 42 the brief anticipated. Two findings against the brief's reading. (1) The largest class, 43 sites, returns res.json() directly, whose lib.dom type is Promise of any - a SIXTH spelling neither #8140's census nor #11925 names, filed as #12104. (2) The ledger cannot carry #11925's 38 as instructed: the gate flags 17 of them, and the other 21 are typed like { package: any } - return types that CONTAIN any rather than BEING any, out of scope by the same line packages/spec's gate draws. That exclusion is written into the ledger's $comment and the PR body rather than left silent. All 65 entries carry a written reason naming the issue that closes them; there is deliberately no --update flag, since a reason a tool wrote is a silencer not a worklist.",
"tests": "All at head 3ee515c. GATE UNION: 36 of 36 exit 0, list derived bynode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(it reads the change set from git itself), plus check:declaration-mirrors and check:nul-bytes as visibly implicated. REPO-WIDE ESLINT: full farm, not a narrowed run -pnpm lint(eslint . --no-inline-config) exit 0 in 77s, so no narrowing argument is needed. CLIENT:pnpm --filter @objectstack/client typecheckgreen ('0 file(s) / 0 error(s) held in test-typecheck-debt.json'); vitest 25 files / 332 tests passing. check:type-check-debt green ('check-type-check-coverage: OK - 65/78 workspace packages type-checked'; re-measure 'none above its recorded number'). ABLATION, the real 52nd site, BOTH LEGS REBUILT: injected an unannotated res.json() method into packages/client/src/index.ts, confirmed on disk by grepping the injected marker (count 1) - not by an editor exit code - then rebuilt (os-verify-lock VERDICT command-exit 0) and confirmed the marker REACHED dist/index.d.ts (count 1); gate went red namingObjectStackClient.data.theFiftySecondSite resolves to Promise of any. Restore leg also rebuilt and confirmed the marker ABSENT from dist (count 0). Whole script ran under trap ... EXIT INT TERM. A first attempt is reported honestly: the mutated build FAILED (my injected method called a helper that does not exist), the dist was cleaned, and the gate REFUSED rather than measuring - so that run proved the refusal, not the detection, and was rerun with a correct injection. RATCHET REVERSE-VERIFICATION against the real package, both directions, each mutation grep-confirmed on disk: deleting a ledger entry goes red as unledgered; adding one for data.get (a caller-supplied generic, never erased) goes red as stale naming the reason on file; missing --package refuses rather than guessing. BUILT-DIST REFUSAL, both causes, with distinct messages: dist moved away gives 'holds no .d.ts declarations - the package is not built'; src touched newer gives 'holds .d.ts declarations OLDER than packages/client/src'. Control: restored tree green again. SELF-TEST pins both directions on a fixture mirroring the real emitted shape; its anti-vacuity floor is enumerated, and the first draft of that floor said 11 by miscounting the ns namespace - corrected to the enumerated 12 and the miscount recorded in the code, since a count assertion that is merely plausible is worth nothing.",
"open_questions": [
{
"question": "RESOLVED by the reviewer in the comment above - keep the lowering. Retained here for the record. The gate's node-types reference also supplied globals to the rest of the ROOT tsc program, taking the @objectstack/spec-monorepo DEBT entry from 80 to 26; I closed the gap with the gate's own --lower.",
"options": [
"A - keep the lowering (current state, and the ruling): an open 54-error gap lets new errors land unnoticed",
"B - revert to 80: the gate states lowering is optional, and 54 of the errors cleared are in files this card did not author"
],
"recommendation": "A, as recommended and as ruled. Note --lower ALSO banked an unrelated -1 on @objectstack/plugin-approvals' test ledger; I reverted that by hand as pre-existing drift this card should not bank - which the reviewer confirms avoided a collision with PR #12110, already in the merge queue lowering that exact entry 348 to 347."
}
],
"out_of_scope_findings": [
"filed as #12104: a sixth client-SDK erasure spelling - 43 exported methods return res.json() directly, whose lib.dom type is Promise of any; invisible to every grep #8140's census and #11925 used, and larger than the other five classes combined",
"filed as #12107: resolveCheckToFiles matches only .mjs|.cjs|.js|.sh, so all 23 TypeScript-authored gates (check:exported-any and check:api-surface included) resolve to ZERO gate files and contribute ZERO watch hints - which also silently voids the bare-root sweep and check:entry-guard over those files"
]
}
Generated by Claude Code
Generated by Claude Code
os-dev-report
Patch round complete. Supersedes the earlier report comments; head moved
3ee515c7f→16debac25.{
"issue": 11927,
"status": "done",
"branch": "claude/issue-11927-client-exported-any-gate",
"pr": "#12115",
"premise_still_valid": true,
"summary": "Patch round done, both items addressed at head 16debac. (1) check-ratchet-remedy-authority UNCLASSIFIED: recorded the gate as 'marked' in the control corpus. That is the sweep's OWN verdict, not my intent ---listputs the file undermarkedwith anchorshrink, and I read that before declaring it, since the corpus exists precisely so observed and declared classes have to agree. Marking rather than refusal is the honest shape: four of the 65 ledgered sites keep any because no published contract exists to bind (#11924), so adding a site is a real act with a real owner rather than one to deny. The control is a declaration registry, not a ratchet, so the row weakens nothing and I touched nothing else in that file (PR #12061 is open on it). (2) Check Changeset was the label MECHANISM, not the derivation: skip-changeset had already gone on minutes after the PR opened, and the check now reports conclusion 'skipped'. Also merged origin/main (not rebased); clean, no conflicts.",
"tests": "All re-derived and re-run at head 16debac after merging origin/main, with the workspace closure rebuilt first (turbo 70/70 successful). GATE UNION: 38 of 38 exit 0, list re-derived bynode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackrather than reused - the re-derivation is what ADDEDpnpm check:ratchet-remedy-authorityto the matched list, since my diff now touches that gate's own source. check:type-check-debt green separately ('check-type-check-coverage: OK - 65/78 workspace packages type-checked'; re-measure 'none above its recorded number'; the spec-monorepo entry holds at 26 with compositionAt 80). REPO-WIDE ESLINT re-run at the new head:pnpm lint(eslint . --no-inline-config) exit 0, 77s. CLIENT re-run: typecheck green ('0 file(s) / 0 error(s) held in test-typecheck-debt.json'), vitest 25 files / 332 tests passing. The gate itself:check-ratchet-remedy-authoritynow 'OK - 146 scripts swept; 9 mark the expanding remedy, 5 turn it down outright, 132 hand out no ratchet-expanding remedy. Control corpus: 24 hand-classified scripts, set-equality audited both ways', and its --self-test green. The earlier evidence stands unchanged and was not re-derived: the 52nd-site ablation with both legs rebuilt and the marker grep-confirmed present then absent on disk, the two-direction ratchet reverse-verification, and the two built-dist refusal causes.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #12104: a sixth client-SDK erasure spelling - 43 exported methods return res.json() directly, whose lib.dom type is Promise of any; invisible to every grep #8140's census and #11925 used, and larger than the other five classes combined",
"filed as #12107: resolveCheckToFiles matches only .mjs|.cjs|.js|.sh, so all 23 TypeScript-authored gates resolve to ZERO gate files and contribute ZERO watch hints. Commented there with the answer to the question you raised: the ratchet-remedy classification is NOT the same root cause. That gate is .mjs and resolves fine - its hints ARE read, they are just the bare word 'scripts', and that is ALREADY recorded in bare-root-worklist.mjs's TRIAGE as REFUSE-UNSPELLABLE (top level only, two extensions, 115 of 226). So it is invisible by a refusal that was reasoned and written down, where this card's 23 are invisible because a regex never opened the file. Fixing the extension list would not surface the one; lifting the REFUSE-UNSPELLABLE would not surface the other. The composition is the confusing part and is recorded there: for the SAME file, the ratchet-remedy sweep reaches it (it walks scripts/*.{mjs,mts} on disk, owing the derivation nothing) while dispatch-gates resolves it to zero - two independent questions, so neither answer is evidence about the other."
]
}
Generated by Claude Code
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