Uh oh!
There was an error while loading. Please reload this page.
fix(client): bind the three verifiable methods of the unannotated return-type erasure population (#11925) - #12062
Conversation
…spec contracts (#11925) Re-measured the population at origin/main: 39 methods with no return annotation typed off `unwrapResponse<…any…>`, not the 38 the card recorded (its single-line reproducer cannot see `projects.get`, whose type argument spans several lines). Of the 39, three had a demonstrable published type: packages.list -> { packages: InstalledPackage[]; total: number } packages.update -> InstalledPackage ScopedProjectClient .packages.get -> { package: InstalledPackage } The remaining 36 keep their erased `any` with a docblock naming the reason and the issue carrying it: missing response contracts (#12038), a cross-surface envelope divergence and three declared envelopes no surface emits (#12034), and a cloud control plane that speaks snake_case while the spec cloud rows are camelCase (#12036). Type-level pins added to return-type-precision.test.ts, compiled through tsconfig.test.json — a runtime test cannot observe a return-type narrowing. Two near-miss guards (green in both states, labelled as regression guards) pin PackageRollbackResponse and Environment as the WRONG bindings for packages.rollback and projects.*. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
…re regression guards (#11925) Ablation measured that only ONE of the three `@ts-expect-error` pins is red before the change: `packages.update` was bare `any`, and `any` is assignable to `string`, so its suppression goes unused (TS2578). The other two methods were never bare — they declared an envelope whose MEMBER was the erased part, and an envelope is not assignable to a bare row/array in either state, so their suppressions are used before AND after. The original comment claimed all three were unused-while-`any`, which the measurement falsifies. Counting a green-in-both pin as red-before evidence is the failure this file's header exists to prevent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 2 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 15 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 26c2ea7590b92e3daef95d2512e4d77f6bf31d2d && git checkout 26c2ea7590b92e3daef95d2512e4d77f6bf31d2d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f6587934539b92591e8514efce36278adaea1d9c 4d7e22ab7d5542f48d06032c2a1d081f8d72c1ae && git checkout -B drift-repro f6587934539b92591e8514efce36278adaea1d9c && git merge --no-ff 4d7e22ab7d5542f48d06032c2a1d081f8d72c1ae
node scripts/docs-audit/affected-docs.mjs --json f6587934539b92591e8514efce36278adaea1d9c
|
os-zhuang
commented
Aug 25, 2026
Review — ACCEPTED on substance. ⛔ Not armed: CI has not converged and the Clause-② gate is hung.
⛔ My order's central estimate was wrong, and this PR is the correctionThe card predicted "20 of the 38 plausibly land on a type that exists today", and my claim comment repeated the cloud families as likely bindable. Measured: 3 — and the cloud families are the least bindable of the five groups. That is the sixth dispatch-order figure this lane has had falsified by measurement in two rounds (49→55, 8→14, offsets +6, two misclassifications, a root cause a package deeper, and now 20→3). Every one was caught because the order said report what you measure, do not reconcile. The instruction keeps earning its place; the numbers beside it keep not deserving trust. Population is 39, not 38 — and the extra is not #11929. It is the card's own single-line reproducer's blind spot: The bar you applied is the reason the answer is 3Not "a plausible spec type exists" but "the route this method calls demonstrably sends this shape." That is #8140's false-declaration discipline, and it is what turns 20 into 3. Binding on plausibility would have produced a PR that typechecks, reads well, and lies. Four things it disqualified, each measured: ⭐ [D] A declared envelope that no surface emits — a live defect, not a typing gap. ⭐ [E]/[F] Binding the cloud families would have broken five CLI files while telling them they were wrong. The control plane speaks snake_case; [B] [C] ⭐ The ablation falsified your own first commit, and you corrected the file rather than the storyYour first commit's comment claimed all three direction-2 An instrument that falsifies its operator's own claim, and an operator who edits the claim rather than the framing — that is the whole point of running the ablation, and it is rarely what happens. The legs also differ (0 errors vs 4, all in the pin file, in both predicted modes), so the both-legs-error-identically failure mode is excluded by construction. ⛔ The runtime suite NEVER RAN, and that is stated as NOT MEASUREDFour consecutive The argument for why this is the cheapest check to lose is measured rather than convenient: a runtime test cannot observe a return-type narrowing — #8140's own control kept the client suite fully green 25/25 against a client that still returned Two narrowings argued from evidence, not assertedLint — the population was answered by
The docs check ran its canary first
A sixth spelling, filed and ⛔ not folded inA tsc ground truth (not a grep) walked 273 reachable public methods and found 89 whose awaited return contains ⛔ What gates arming
Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
…entry `#12062` bound `ObjectStackClient.packages.update` to `InstalledPackage`, but its ledger row did not come with it. The ledger is shrink-only and judged EXACTLY in both directions, so the closed gap left `Type Check · consumer gates` red on `main` — reproducing onto the merge ref of every open PR. Verified against the BUILT dist before deleting: the awaited return type of `ObjectStackClient.packages.update` is the concrete `InstalledPackage` shape, `TypeFlags.Any = false`. The seven neighbouring `packages.*` rows still read `any` and stay. Part of #12180
`22c42c9b` (#12062) bound `packages.update`'s return type, closing its any-return under #11925, but `packages/client/exported-any-returns.json` still carried the entry. The ledger is shrink-only and judged EXACTLY, so the lingering entry reds `pnpm --filter @objectstack/client run check:exported-any-returns` for every PR whose gate family includes the client consumer gates. Deletes exactly the `ObjectStackClient.packages.update` entry; every other entry (still-open #11925/#12104 debt) is untouched, as is `$comment`. Fixes#12193 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KWRU3s15AJz7PGW7a7wdCh
Part of #11925
Binds the verifiable part of the fifth erasure spelling and records — on the methods
themselves — exactly why the rest is not bindable yet.
Part of, notFixes: 3 of the 39methods are bound here, and the other 36 are blocked on contract decisions that are now
filed as their own cards. Merging this should not close the card.
The population, re-derived at
origin/main(c804f0ca5)⛔ The card's 38 is stale, and not only because #11929 landed in this file. Its own
reproducer uses a single-line regex, and one type argument spans several lines:
A brace/angle-balanced parse finds 42
unwrap<…any…>sites; the one the single-linegrep cannot see is
cloud projects.getat L1601, whose{ project: any; database?: any; … }argument is written across six lines. Splitting the 42 by whether the enclosing declaration
carries a
): Promise<annotation:39, not 38 — 39 sites in 39 distinct methods (verified distinct; zero attribution
mismatches against a second pass that also looks for class-method enclosures). The card's
family table is right everywhere except cloud
projects.*, which is 9, not 8: it omitsget, the multi-line one.⭐ The asymmetry sanity-check holds exactly as the card describes it:
ScopedProjectClient.packages.list(L5661) carries both an annotation and a type argument,so #8140 bound it to
{ packages: InstalledPackage[]; total: number }; its immediateneighbour
packages.get(L5665) carries neither. Same object literal, opposite treatment.That neighbour is bound here.
A ground-truth cross-check, and a sixth spelling
The spelling census was checked against the TypeScript checker itself — walking the public
members of both classes and resolving each awaited return type:
The 39 are a strict subset of the checker's 85. The residual 46 are a SIXTH spelling —
return res.json(), whose published type isanywith neitherPromisenorunwrapResponsein the text. Out of scope here, filed as #12037.What is bound (3)
The bar was not "a plausible spec type exists" but "the route this method calls
demonstrably sends this shape". Three cleared it:
client.packages.list{ packages: any[]; total: number }{ packages: InstalledPackage[]; total: number }client.packages.updateanyInstalledPackageScopedProjectClient.packages.get{ package: any }{ package: InstalledPackage }packages.list— both mounted surfaces send the same envelope:runtime's/packagesdomain doessuccess({ packages, total: packages.length })(
domains/packages.ts:277) andrestdoessendOk(res, { packages, total: packages.length })(
package-routes.ts:734). The REST rows also carrysource: 'database' | 'registry' | 'both',which the dispatcher rows lack — deliberately left undeclared, since declaring it would be
false on one surface. 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 gave the scoped sibling the same treatment.packages.update—PATCH /packages/:idis dispatcher-only (restmounts no PATCHtwin) and answers the bare row. This method declared no envelope before, so the shape claim
is unchanged and only the erased
anymoved.ScopedProjectClient.packages.get— the scoped mount is served only by the RESTregistrar (
direct-mount-composition.ts:126registersregisterPackageRoutesat both{base}/packagesand{base}/environments/:environmentId/packages), so unlike the globalpackages.getthere is one surface and one shape.What is examined-and-skipped (36), with the reason on each method
Every one carries a docblock at its declaration naming the reason and the card that holds it.
meta.getPublished/listDrafts/migrateStored/getDiagnostics/getReferences/getBookTree/getAudit/rollbackItem/diffItemresponseSchema=None;StoredMigrationReportlives in@objectstack/metadata-protocol, not a dependency of this packagepackages.publish/discardDrafts/listCommits/revertCommit/rollback/export/adoptOrphans/duplicateanycast, so no declared type exists on the pathclient.packages.getsuccess(pkg)(bare row) vs RESTsendOk(res, { package: {…pkg, source} }). No single declaration is truepackages.install/enable/disable{ package; message? }while the only surface serving them answers the bare row — the envelope is already false, and correcting it is a shape decision, not anany-bindingprojects.*(9) +projects.packages.*(6)/api/v1/cloud/*control plane speaks snake_case; the@objectstack/spec/cloudrows are camelCase⭐ On the last row — this is the
SearchResultnear-miss class at family scale, and it ismeasured, not suspected.
EnvironmentSchemadeclaresdisplayName/organizationId/isDefault/databaseUrl(zero snake_case keys across all three cloud row schemas, against63 key lines in the same file). The in-repo CLI consumers of those exact routes read
p.display_name,p.organization_id,p.is_default,res.database.database_url,res.membership.role, and sendorganization_id/display_name/clone_from_environment_id. Binding toEnvironmentwould typecheck, be false, and breakpackages/cli/src/commands/environments/{show,list,create,switch,bind}.tsat compile timewhile telling them they are wrong when they are right.
Clause ② — YES, a narrowing
Agreed with the claim comment against triage's parenthetical.
anyis assignable toeverything and admits every property read, so for each bound method a consumer's code can
stop compiling: assigning to an unrelated annotation, reading an undeclared property, or
forwarding to a differently-typed parameter. Runtime behaviour is identical, which is exactly
why the pins are type-level. The changeset states, per family, what a consumer could stop
compiling against —
sourceon the package rows being the concrete one.needs:contract-reviewis hung on this PR. ⛔ This seat does not clear it on either carrier.Anti-vacuity — the pins are type-level, and the ablation is red
A runtime test cannot observe a return-type narrowing; #8140's control proved it (25/25 green
against a client that still returned
any). The pins live inpackages/client/src/return-type-precision.test.tsand compile throughpackages/client/tsconfig.test.json, whichpackage.json'stypechecknames viacheck:test-typecheck.The pin file imports
./indexrelatively and the config includessrc/**/*, so tsc readsthe source — no
dist/and no rebuild sit in the ablation path.Ablation = revert only
packages/client/src/index.tstoorigin/main, keep the pins.Mutation confirmed on disk with anchored greps in both directions before each run (an editing
tool's exit code is not evidence):
Both legs run the same instrument (
tsc --noEmit -p packages/client/tsconfig.test.json), andthey differ:
direction-2
@ts-expect-errorpins fires:wrongUpdate(line 246), becausepackages.updatewas bare
anyandanyis assignable tostring. ThewrongListandwrongScopedGetsuppressions are used in both states — those two methods were never bare, they declared an
envelope whose member was the erased part, and an envelope is not assignable to a bare
row/array either way. They are regression guards, not evidence the binding was needed, and
the file now says so at each one. The first commit's comment claimed all three were
red-before; the ablation falsified it and the second commit corrects it.
Two further guards are green in both states and labelled as such:
PackageRollbackResponseis pinned as the WRONG type forpackages.rollback(it declares theVERSION rollback
{ success, restoredVersion?, message? }while the method posts{ commitId }and routes to the ADR-0067 COMMIT rollback), and
Environmentas the wrong type forprojects.*. #8140'sSearchResultguard is untouched and still compiles.Consumer census — radius set by where the CONSUMERS live
In-repo callers of the three bound methods are all inside this package's own tests
(
client.test.ts,packages-lifecycle.test.ts,return-type-precision.test.ts). No consumeranywhere reads
.sourceoff a package row, which is the one key the binding drops.Docs fences: none break. Checked by hand, because no gate compiles them — the #11942 class.
Neither reads a property off the result, so neither stops compiling.
packages.updateand thescoped
packages.getappear in no fence at all. The instrument was canaried first — the samegrep finds 23
client.data.hits and 9 files namingObjectStackClient— so its zero for thebound methods is a real absence, not a broken pattern.
Gates
Re-derived from the actual change set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack.All run on the final commit
4d7e22ab7(the tree the union describes), each quoting the gate's own verdict line.Exit codes captured before any pipe.
Path-derived — all 16 green:
Convention-triggered (this change adds test code) — 3 green, 1 NOT MEASURED:
⛔
check:type-check-debtrefused; a refusal is neither pass nor fail. Its own words:"--re-measure cannot run: 25 workspace dependenc(ies) of the ledgered packages have no built type
entry point on disk … measuring now would not fail, it would silently measure a DIFFERENT WORLD."
It needs the full workspace closure built, which needs the shared verify lock; four consecutive
attempts at that lock timed out (below). Declared narrowing, with the reason it is safe:
scripts/check-type-check-coverage.mjs.@objectstack/clientis in neither.packages/client/test-typecheck-debt.jsonis"entries": {}and isuntouched by this diff (
git diff … -- '*debt*'is empty). Its number was measured directly,green, with the closure built:
check:test-typecheck: OK — … 0 file(s) / 0 error(s).@objectstack/client(@objectstack/cli, TEST_DEBT),and it calls none of the three bound methods — so no ledger number can move.
Also run:
check-nul-bytesexit=0 — "OK (scanned 6666 text file(s) … no raw ASCII control bytes)",plus a self-scan of the changed files with
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'— 0 hits.pnpm lint— a measured narrowing, not a skipFull-repo
eslint . --no-inline-configis CI's run. Targeted here, with all three evidences:eslint.config.mjsitself (ESLint#isPathIgnoredovergit ls-files),not by my assumption: 5058 lintable-extension tracked files, 0 ignored, and both changed
.tsfiles reportCHECKED.--format json: 3 paths in, 3 results out —packages/client/src/index.ts0 errors / 0 warnings,return-type-precision.test.ts0 / 0, and thechangeset
.mdcarrying only "File ignored because no matching configuration was supplied", which acontrol changeset (
.changeset/action-doubled-redirect-refusal.md) reproduces identically — aproperty of
.mdunder this config, not of this diff.exit=0.eslint.config.mjsand it "neverenables type-aware linting (no
parserOptions.project, no typed@typescript-eslintrules) for ANYfile, test or not" — its own recorded measurement, made with a positive control. Verified again here:
8
parserOptionsoccurrences, 0 carrying aproject. A return-type narrowing therefore cannotmove any untouched file's lint verdict.
⛔ The runtime suite is NOT MEASURED locally
pnpm --filter @objectstack/client testnever ran: four consecutiveos-verify-lock: VERDICT queue-timeout (exit 99) · never acquired · waited 540s (9m00s)— ~36 minutesqueued behind other agents' holders (
objectstack-11788's full turbo build, thenobjectstack-11772'sverify.sh/gates.sh, then a@objectstack/restprobe). ⛔ 99 is the lock's "no turn"; nothing ran,and it is not a pass. The fourth attempt was already narrowed to the three files that exercise the bound
methods and still got no turn.
This is the one check whose absence costs least here, and that is measurable rather than hopeful:
a runtime test cannot observe a return-type narrowing at all — #8140's control demonstrated it by
keeping the client suite fully green (25/25) against a client that still returned
any. The verificationthat can see this change is the type-level ablation above, and it ran on both legs. CI runs the suite.
⛔
test-typecheck-debt.jsonis untouched:0 file(s) / 0 error(s)before and after.Generated by Claude Code