fix(client): bind the five in-repo return res.json() methods erased to Promise< any >, and measure the 38 third-party ones - #13082
Conversation
… to `Promise< any >`
The sixth erasure spelling: no return annotation and `return res.json()`, whose
published type comes from `lib.dom`'s `Response.json(): Promise< any >`. The
method text names neither `any` nor `Promise` nor `unwrapResponse`, so it is
invisible to every grep the earlier censuses used.
Binds the five whose producers live in this repo, so the true type is
measurable by DRIVING them rather than reading source or asserting a mock:
analytics.query -> BaseResponse & { data: AnalyticsResult }
analytics.meta -> AnalyticsMetadataResponse
analytics.explain -> AnalyticsSqlResponse
automation.trigger -> BaseResponse & { data: AutomationResult }
analytics.queryDataset-> AnalyticsResult (REST-served, no envelope)
`unwrapResponse` strips the `{ success, data }` envelope; `res.json()` does not,
so four of the five resolve to the envelope and the fifth — mounted only by
@objectstack/rest, which ends `res.json(result)` — to the bare payload.
Two spec response types that LOOK like the right binding are narrower than the
contract their route relays (`AnalyticsResultResponseSchema.data.fields` and
`TriggerFlowResponseSchema.data`); binding either would have shipped a false
narrowing, so both annotations bind the producer's contract instead and the
near-miss is pinned.
Deletes exactly the five matching entries from the shrink-only ledger, in the
same commit as the fix, and adds the two halves the pair needs: a driven wire
test and the type-level pins a runtime test cannot express.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
…and answer ADR-0087 `check:test-source-alias` and `check:type-source-resolution` both refuse a new workspace import that resolves through `dist/`, and the driven wire test added three. Aliased in `vitest.config.ts` and pathed in `tsconfig.json`, one anchored bare-name rule each. The reason is stronger here than the general one: that suite's claim is "the annotation matches what the producer sends", so against a stale `dist` it would certify a declaration that is already false. The changeset declares BREAKING, so `check-adr-0087-registration` requires the ledger question answered in writing: `not-required (no-migration-prescription)` — nothing metadata-shaped moves, so there is no tombstone and nothing for `objectstack migrate meta` to rewrite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
…ngs use #8140 / #11925 / #12034 are the same class — a published `@objectstack/client` return-type narrowing with no metadata surface behind it — and all three record the no-ADR-0087-entry disposition as prose rather than as a `**BREAKING**` declaration, which in this repo is the token that routes a change at the metadata migration ledger. The consumer impact is stated in full (FROM/TO per method, and the exact reads that stop compiling); what is dropped is a token that would have claimed a ledger relevance this change does not have. Recorded on the PR: the ADR-0087 gate has no honest disposition for a published TYPE-surface narrowing that carries a consumer source-edit prescription and no metadata migration — filed separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
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 90936c38f5794fb131716a164dfdfbef659ac39b && git checkout 90936c38f5794fb131716a164dfdfbef659ac39b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f887e5249373ccc2d9af36b5c9b02cc99a691c40 1be89532e36c5689b457ca3e76f9fde454c223a6 && git checkout -B drift-repro f887e5249373ccc2d9af36b5c9b02cc99a691c40 && git merge --no-ff 1be89532e36c5689b457ca3e76f9fde454c223a6
node scripts/docs-audit/affected-docs.mjs --json f887e5249373ccc2d9af36b5c9b02cc99a691c40
|
PM 复核 — ACCEPT,两处待裁都按你的建议
⭐ 你证伪了卡的假设,而这正是我要的卡和分诊都写 ⇒ 如果我照卡的措辞派下去("这些需要授权契约"),会开出三张方向错误的卡。R51 这条规矩今天第二次生效。 核过的ledger 精确:只删了那 5 条具名条目,新增 0 行。47 → 42(它在 #12181 / #12034 落地时已从 65 缩到 47)。⇒ 符合"只删本轮真修掉的,不要清空、不要把计数当判据"。 那条决定性区分,我复核了它的重要性: 两个 spec 响应类型比它们路由中继的契约更窄,你绑了生产者契约而不是那个看起来权威的 schema,并把这个近失钉住、另立 #13078。⇒ 处置正确:一个看起来是权威声明的东西未必是。 ⭐ 你在自己的消融里抓到一次假绿第一条 typecheck 腿跑的是裸 ⇒ 这与 #12992 那位在消融里发现 消融 B 首跑答 PREREQUISITE NOT MET(dist 比 src 旧)也记为无读数、重建后重跑 —— 同一条纪律。 ⭐ 而且你区分了"驱动出来的"和"推理出来的"你自己标注:"drive one better-auth route end to end and read the body — that step is the one part of my measurement that is reasoned rather than driven, and I have said so in the PR"。 ⇒ 在一份整体上高度"驱动"的测量里,主动圈出唯一一处是推理的。这直接决定了下面第二问的裁定。 待裁一:
|
Part of #12104
Binds the five methods of the card's 43 whose producers live in this repo — so the true
type is measurable by DRIVING them — and deletes exactly those five entries from #11927's
shrink-only ledger in the same commit. The 38 better-auth-backed methods are measured, not
shipped; that measurement is the second half of this PR and is below.
1. Coordinates, re-located on today's
origin/mainThe card's line numbers predate #13026 and #13060, both of which edited
packages/client/src/index.ts. Every site was re-found ate6fd1caf7rather than trusted:analytics.querysrc/index.tsanalytics = {→queryPOST /analytics/queryanalytics.metametaGET /analytics/metaanalytics.explainexplainPOST /analytics/sqlanalytics.queryDatasetqueryDatasetPOST /analytics/dataset/query@objectstack/restautomation.triggerautomation = {→triggerPOST /automation/trigger/:nameThe ledger keys were unchanged (
ObjectStackClient.analytics.*,ObjectStackClient.automation.trigger).The ledger itself had already shrunk 65 → 47 as #12181 / #12034 landed; it is 42 after this PR.
2. The one fact that decides four of the five bindings
unwrapResponsestrips the{ success, data }envelope.res.json()strips nothing. So ares.json()method resolves to the WHOLE body, and which body that is depends on the surface:deps.success(v)={ success: true, data: v }→ the envelope is the type;
queryDatasetis mounted only by@objectstack/restand endsres.json(result)→ the barepayload is the type.
Binding the payload where the envelope is served (or the reverse) typechecks against
anyandships a false declaration — the census's highest-risk band. Hence one driven case per method
rather than a family-wide assumption.
3. What each method now declares, and the evidence for it
Nothing below was read off source or asserted against a mock. Each is measured in
packages/client/src/analytics-automation-json-erasure.test.ts, which drives a realAnalyticsService, a realAutomationEngine, the realHttpDispatcherand the realRestServer, with only the socket stood in for.analytics.queryBaseResponse & { data: AnalyticsResult }data/meta/success;body.datacompared for equality against a second direct call toanalyticsService.query(...), not against a literalanalytics.metaAnalyticsMetadataResponsebody.dataequalsanalyticsService.getMeta(); a BARE array (nocubeswrapper, #6442), first membercrm_account, measure namedcrm_account.account_countanalytics.explainAnalyticsSqlResponseObject.keys(body.data).sort()is['params','sql'];sqlmatches/SELECT/iautomation.triggerBaseResponse & { data: AutomationResult }data.status === 'paused',typeof data.runId === 'string',data.screen.title === 'Approve the account'analytics.queryDatasetAnalyticsResult'success' in bodyand'data' in bodyare both false;body.rowsis the payload at top levelPlus the premise the four envelope annotations rest on, pinned against the real dispatcher: the
domain wraps exactly once and
res.json()hands that body through untouched.Why two of the five do NOT bind their route's own spec response type
Two spec schemas look like the authoritative binding and are narrower than the contract their
route relays. Binding either would have been a false narrowing — the bound-but-wrong shape
#12034 paid to remove — so both annotations bind the producer contract instead:
AnalyticsResultResponseSchema.data.fieldsdeclares{ name, type };AnalyticsResult.fieldsalso carries
label/format/currency/percentScale, and the result carriestotals.A real
AnalyticsServiceservingfields[].labelis asserted in the suite. (It also has noexported type —
protocol.zod.tskeeps a module-local alias — so it is unnameable anyway.)TriggerFlowResponseSchema.datadeclares{ success, output?, error?, durationMs? };AutomationResultalso carriescode/status/runId/screen/successMessage/errorMessage/summary— the whole automation: bothtriggerroutes still answer HTTP 200 wrapping an inner {success:false} — the same #3962 residue #8684 closed on resume #9378 / bug(service-automation): a retry attempt that PAUSES is recorded as failed and its suspension is never persisted —executeWithoutRetryhas noisSuspendSignalarm #9510 third state, served by the real engineabove, and the very reads the SDK's own
triggerdocblock tells callers to make.Filed as #13078. The other two —
AnalyticsMetadataResponseSchemaandAnalyticsSqlResponseSchema— DO agree with their producer, so this PR uses them as-is(
AnalyticsMetadataResponse['data'][number]is already pinned ≡CubeMetain spec's ownanalytics.test.ts).4. The two halves the pair needs
return-type-precision.test.ts's header states the rule and both halves are here:analytics-automation-json-erasure.test.ts(new, driven) — proves the declarations areTRUE. A type test cannot do this.
return-type-precision.test.ts→returnTypePrecisionPins12104(new function) — provesthe declarations MOVED. A runtime test cannot observe a return-type narrowing at all: the
value is identical whatever the declaration says.
The type half also pins the envelope/payload split from both directions — reading
.rowsoffthe enveloped
query, and reading.dataoff the barequeryDataset, are each a@ts-expect-errorthat goes unused (TS2578) while the method is stillany.5. Reverse verification — direction predicted BEFORE running
Ablation A — remove the annotation from
automation.trigger. Predicted: the ledger gate RED(its entry was deleted, so the site is unlisted), typecheck RED, the wire test GREEN.
Disk proof before any reading: anchor count 1 → 0, injected count 1, blob
4ea488d1→1ad1a161. Rebuilt, andscripts/ablation-dist-preflight.mjsconfirmedtrigger: (triggerName: string, payload: any) => Promise< any >PRESENT in 2 built files.Measured, all three as predicted:
tsc --noEmit, which readstsconfig.json— a program that EXCLUDES the pins — and reportedexit 0. That is NOT MEASURED, not green. Re-run through the package's real
typecheckscript (which names
tsconfig.test.json) it is red, andtsc -p tsconfig.test.json --listFilesconfirms both test files are in that program.
Ablation B — put a deleted ledger entry back, annotation intact. Predicted RED on the
shrink-only direction. Disk proof: injected key count 1, blob
8f6af1e0→a07a04f0.PREREQUISITE NOT MET(distolder thansrc, from ablation A'srestore) — recorded as no reading at all, rebuilt, re-run.
Restore, proven on both:
git checkout HEAD -- < absolute path >under anEXIT INT TERMtrap, then blob-hash equality against the HEAD blob and an emptygit diff HEAD— never an exit code.index.tsback to4ea488d1,exported-any-returns.jsonback to8f6af1e0. The restore leg was also rebuilt andre-preflighted with
--absent, so no mutateddistsurvives into any later measurement.6. The 38 third-party methods — MEASURED, not shipped
⛔ Not one of them is touched by this PR. The card and the triage both said they "may need
contracts authored rather than relayed"; that was treated as a question, and the answer is
mostly no.
Every one of the 38 maps to a live, named, fully-typed better-auth endpoint. The live
AuthManagerwas enumerated at the ledgered plugin configuration (126 endpoints), and 18representative endpoints across the three families were type-probed against better-auth's own
declarations. Not one returned
any:organizations.*(19)createOrganization→{ id, name, slug, createdAt: Date, logo?, metadata? } & { metadata; members[] };getFullOrganization→{ members[], invitations[], teams[] } & … | null;createTeam,createInvitation,leaveOrganization,removeMember,getActiveMember,updateOrganizationlikewise concreteauth.*(14)updateUser/changeEmail/revokeSession/unlinkAccount→{ status: boolean };deleteUser→{ success: boolean; message: string };verifyTOTP→{ token: string; user: UserWithTwoFactor } | …oauth.*(5)registerOAuthClient→OAuthClientRegistrationResponse;getOAuthClient/getOAuthClientPublic→OAuthClient;oauth2Consent→OAuthRedirectResult | { redirect, url }— all NAMED exported typesClass (a) also exists, for the organization family only — and it is not a drop-in.
@objectstack/spec/identityalready declaresOrganization,MemberandInvitation, and itsown docblock says it "aligns with better-auth's organization plugin". Measured mismatches
against what
createOrganizationreturns:OrganizationSchemaREQUIRESupdatedAt, which thatresponse does not carry; the response carries
members, which the schema does not declare.Class (c) — what genuinely has to be decided — is ONE cross-cutting question, not 38
contracts. Every better-auth payload above carries
Datefields. The SDK does not callauth.api; it reads an HTTP body, and JSON has noDate. So a binding derived verbatim fromtypeof auth.api.Xwould declarecreatedAt: Dateon a value that is astringat runtime —a false declaration of exactly the class this card exists to remove. The repo has already
decided this once in the other direction: every date in
spec/identityisz.string().datetime().(a live
AuthManagerenumeration plus a tsc probe of better-auth's declarations). TheDate→stringtransport step is NOT driven here — it rests on JSON representability plusthe repo's own wire-side schema. Whoever takes the follow-up should drive one better-auth
route end to end and read the body before committing to a shape.
Suggested split, on those numbers (PM's call, not taken here): the transport-form decision
first, as one card; then one card per family, each relaying better-auth's declared types through
that decision. Not "author 38 contracts".
7. Local verification, and what is deliberately CI's
Verification union re-run after the final commit, at
1be89532e:pnpm --filter @objectstack/client check:exported-any-returnsno NEW exported callable … resolves to 'any': 317 callables reached …, 42 ledgered site(s) still openpnpm --filter @objectstack/client typecheckcheck:test-typecheck: OK … 0 file(s) / 0 error(s)pnpm --filter @objectstack/client testTest Files 30 passed (30) · Tests 402 passed (402)Named + path-derived gates, all green (
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackre-derived at the final commit):
check:route-envelope,check:query-options-erasure,check:nul-bytes,check:published-files,check:cross-package-test-inputs,check:test-source-alias,check:type-source-resolution,check:undeclared-dep-imports,check:engine-double-contract,check:where-matcher,check:objectql-double-limit,check:slot-lookup,check:page-declaration-shape,check:type-check-coverage,check:override-consistency,check:objectui-changeset,check:changeset-gate-self-tests,check:pm-half-states(self-test),check-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check-changeset-fixed,check-osv-exemptions,check-comment-mask-adoption,check-plugin-teardown-shape,check-keyed-text-bounds,check-ci-filter-parity,check-shard-attestation,check-affected-docs,release-rehearsal-clone --self-test,@objectstack/spec check:skill-examples(260 proseexamples, incl. 23 client-SDK blocks, all type-check against the narrowed declarations).
NOT MEASURED here, and why — none of these is a green:
check:dual-build-cjs-loads— exited 3,Run pnpm build first. ⛔ This is NOT a pass: nothing was measured.Needs the whole workspace built; CI's.check:type-check-debt --re-measure— same prerequisite (full closure build); CI's.check-test-completeness.mjs— takes a turbo test log as an argument; only invocable from CI.node scripts/pm/check-half-states.mjs(the sweep) — exited 3,PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential(
GITHUB_TOKENis a 14-char proxy placeholder). Its self-test half passed.pnpm lint(repo-wideeslint . --no-inline-config) — not run and not narrowed either,so this is a plain omission rather than a measured narrowing. CI runs the farm exactly once.
8. Out-of-scope findings — filed, not fixed here
packages/specroute response schemas declare adataNARROWER than the contract their route relays —AnalyticsResultResponseSchemaandTriggerFlowResponseSchema#13078 — twopackages/specroute response schemas declare adatanarrower than thecontract their route relays (
AnalyticsResultResponseSchema,TriggerFlowResponseSchema).Producer-side; it is what decided two of this PR's five bindings.
client.analytics.query/meta/explainandautomation.triggerhand callers the RAW dispatcher envelope while every sibling unwraps it — one SDK, two calling conventions #13079 —analytics.query/meta/explainandautomation.triggerhand callers theraw dispatcher envelope while every sibling unwraps it.
triggerandexecutecall the SAMEdoor and disagree. Converging them is a runtime breaking change needing a measured caller
population, so it is recorded rather than taken.
**BREAKING**token instead #13080 —check-adr-0087-registrationhas no honest disposition for a publishedTYPE-surface narrowing:
registeredwould be false data in the metadata ledger,unpublished/already-registeredare factually false, andno-migration-prescription/runtime-interface-onlyare both refused when the changeset carries a FROM/TO block. Methead-on while writing this PR's changeset — see below.
9. One judgment call, surfaced rather than buried
The changeset first declared a breaking change with the repo's
**BREAKING**token, which istrue of a
Promise< any >→ concrete narrowing. That token routed it intocheck-adr-0087-registration, whose five dispositions are all unavailable here (#13080). Ratherthan mint a false category or write a ledger entry with no artifact to project into, this
changeset follows the disposition its three immediate siblings used for the identical class —
#8140, #11925, #12034, all
@objectstack/clientreturn-type narrowings, allminor, allrecording "no ADR-0087 ledger entry: nothing here is a metadata surface" as prose. The consumer
impact is stated in full either way: per-method FROM/TO, and the exact reads that stop
compiling. PM should overrule this if the token is wanted regardless.
Generated by Claude Code