Skip to content

Commit b15d260

Browse files
os-litantclaude
andauthored
fix(client): bind the five in-repo return res.json() methods erased to Promise< any >, and measure the 38 third-party ones (#13082)
* fix(client): bind the five in-repo `return res.json()` methods erased 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 * gate follow-through: resolve the three driven producers from SOURCE, 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 * changeset: state the SDK narrowing in the disposition its three siblings 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d48929e commit b15d260

9 files changed

Lines changed: 696 additions & 14 deletions
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
"@objectstack/client": minor
3+
---
4+
5+
fix(client): bind the five in-repo `return res.json()` methods, whose published type was `Promise< any >` (part of #12104)
6+
7+
**Return-type narrowing on a published SDK (clause-②).** No runtime change: the
8+
value each method resolves to is byte-identical before and after. Only the
9+
DECLARED type moved, off `any` — which is exactly why a runtime test cannot
10+
observe it and the pins for it are type-level.
11+
12+
> ⓘ Angle brackets are spaced throughout (`Promise< any >`) on purpose —
13+
> GitHub's body sanitizer strips tag-shaped spans, backticks and fenced code
14+
> included.
15+
16+
Each of the five carried no return annotation and ended `return res.json()`, so
17+
its published type was `Promise< any >`, inherited from `lib.dom`'s
18+
`Response.json(): Promise< any >`. The method text names neither `any` nor
19+
`Promise` nor `unwrapResponse`, which is why the class was invisible to the
20+
greps two earlier censuses used.
21+
22+
## What each method declares now
23+
24+
| method | declared before | declares now | why |
25+
|---|---|---|---|
26+
| `client.analytics.query` | `any` | `BaseResponse & { data: AnalyticsResult }` | dispatcher-served; `deps.success(v)` wraps and `res.json()` strips nothing |
27+
| `client.analytics.meta` | `any` | `AnalyticsMetadataResponse` | same envelope; `data` is the bare `CubeMeta[]` projection |
28+
| `client.analytics.explain` | `any` | `AnalyticsSqlResponse` | same envelope; `data` is `{ sql, params }` |
29+
| `client.automation.trigger` | `any` | `BaseResponse & { data: AutomationResult }` | same envelope, over the payload its sibling `automation.execute` unwraps |
30+
| `client.analytics.queryDataset` | `any` | `AnalyticsResult` | served by `@objectstack/rest`, which ends `res.json(result)` — no envelope |
31+
32+
`any` is assignable to everything and admits every property read, so a
33+
consumer's code can stop compiling where it previously did not. Concretely:
34+
35+
- **Reading a payload key off one of the four ENVELOPED results.**
36+
`(await client.analytics.query(q)).rows` compiled and was `undefined` at
37+
runtime; the read the wire always required is `.data.rows`. Same for
38+
`.data` on `meta` / `explain`, and `.data.runId` / `.data.screen` on
39+
`automation.trigger`.
40+
- **Reading `.data` off `queryDataset`**, which is served bare — likewise
41+
`undefined` today, likewise refused now.
42+
- Assigning any of the five results to an unrelated annotation, or forwarding
43+
one to a differently-typed parameter.
44+
45+
That break is the point: those call sites are already wrong at runtime and the
46+
`any` is what hid it. The compiler is the channel that reaches every affected
47+
consumer, and it is strictly more precise than a release note.
48+
49+
## How the shapes were established
50+
51+
By DRIVING the real producers — a real `AnalyticsService`, a real
52+
`AutomationEngine`, the real `HttpDispatcher` and the real `RestServer`, with
53+
only the socket stood in for — not by reading source and not by asserting
54+
against a mock. Two spec response types that look like the right binding are
55+
NARROWER than the contract their route relays
56+
(`AnalyticsResultResponseSchema.data.fields` and
57+
`TriggerFlowResponseSchema.data`), so those two annotations bind the producer's
58+
contract instead; the near-miss is pinned so a later sweep cannot retarget them.
59+
60+
## Scope
61+
62+
The five families whose producers live in this repo. The 38 better-auth-backed
63+
`auth.*` / `organizations.*` / `oauth.*` methods of the same class are untouched
64+
and keep their erased `any` — they are exactly as permissive as before, and no
65+
consumer loses anything by that.
66+
67+
No ADR-0087 ledger entry: nothing here is a metadata surface. No Zod schema, no
68+
`packages/spec` declaration and no stored representation changed — the erasure
69+
lived only in a TypeScript return annotation — so `objectstack migrate meta` has
70+
nothing to rewrite and an entry would have no artifact to project into. This is
71+
the disposition #8140, #11925 and #12034 recorded for the same class of SDK
72+
return-type narrowing.

packages/client/exported-any-returns.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22
"$comment": "Exported callables of @objectstack/client whose AWAITED return type resolves to `any` (#11927). Judged against the BUILT dist by `pnpm --filter @objectstack/client check:exported-any-returns`, because the erasure is invisible in source text when a method carries no return annotation. SHRINK-ONLY and EXACT in both directions: a site here that no longer resolves to `any` is RED until its entry is deleted, and a site NOT here that resolves to `any` is RED — that unlisted case is the everyday one and the reason this file exists. There is deliberately NO --update flag: every entry is debt with a name on it, and a reason a tool wrote is a silencer rather than a worklist. SCOPE, and the one exclusion worth stating out loud: a return type that CONTAINS `any` (`{ packages: any[]; total: number }`, `Promise<Record<string, any>>`) is not listed, because it is not flagged — the gate asks whether the type IS `any`, the same line packages/spec's check:exported-any draws, and admitting the broader question costs the gate its zero-false-positive property. That is why 21 of #11925's 38 unannotated methods are absent here: they are `any`-CONTAINING, and they remain #11925's to close. Nothing is silently absorbed in either direction. A caller-supplied `<T = any>` is likewise never listed: the record type and the action payload really are the caller's, and flagging them is the pressure that turns a correct generic into a wrong concrete type.",
33
"entries": {
44
"ObjectStackClient.meta.migrateStored": "#11925 — no return annotation; the published type comes from `this.unwrapResponse<any>(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
5-
"ObjectStackClient.analytics.query": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise<any>`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
6-
"ObjectStackClient.analytics.meta": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise<any>`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
7-
"ObjectStackClient.analytics.explain": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise<any>`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
8-
"ObjectStackClient.analytics.queryDataset": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise<any>`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
95
"ObjectStackClient.organizations.create": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise<any>`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
106
"ObjectStackClient.organizations.update": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise<any>`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
117
"ObjectStackClient.organizations.setActive": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise<any>`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
@@ -45,7 +41,6 @@
4541
"ObjectStackClient.auth.twoFactor.disable": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise<any>`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
4642
"ObjectStackClient.auth.twoFactor.verifyBackupCode": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise<any>`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
4743
"ObjectStackClient.auth.accounts.unlink": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise<any>`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
48-
"ObjectStackClient.automation.trigger": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise<any>`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
4944
"ObjectStackClient.automation.create": "#11924 — DELIBERATE `Promise<any>`: `POST /automation` ends `deps.success(body)`, echoing the caller's own unvalidated bytes, and `IAutomationService.registerFlow` returns nothing, so the service contract has no return shape to relay. This needs a DECISION (keep echoing, or answer the registered `FlowParsed`), not an annotation.",
5045
"ObjectStackClient.automation.update": "#11924 — DELIBERATE `Promise<any>`: `PUT /automation/:name` ends `deps.success(definition)` where `definition = body.definition ?? body`. Same missing contract as `automation.create`, and the two should be answered together since they are one route class."
5146
}

packages/client/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@
3333
"@objectstack/metadata-protocol": "workspace:*",
3434
"@objectstack/objectql": "workspace:*",
3535
"@objectstack/plugin-hono-server": "workspace:*",
36+
"@objectstack/rest": "workspace:*",
3637
"@objectstack/runtime": "workspace:*",
38+
"@objectstack/service-analytics": "workspace:*",
39+
"@objectstack/service-automation": "workspace:*",
3740
"tsx": "^4.23.12",
3841
"typescript": "^6.0.3",
3942
"vitest": "^4.1.10"

0 commit comments

Comments
 (0)