Skip to content

Commit be7382d

Browse files
fix(client): the four packages READ members declare the stage their door is declared at (#17536) (#19323)
Fixes #17536 Clause-②: yes The four `packages` READ members of `@objectstack/client` — `ObjectStackClient.packages.list` / `.get` and their `ScopedEnvironmentClient` twins — returned `InstalledPackage`, the AUTHORING manifest stage, while both read doors have been declared at EITHER stage since PR #17517. A response the server is declared able to send was one this SDK's own types said could not arrive. `packages/spec` is the one contract between producers and consumers (Prime Directive #12) and `packages/client` is a consumer of it, so the consumer's declaration is what moves. All four now declare `InstalledPackageAtEitherStage` from `@objectstack/spec/api`. Direction ruled by triage at [`5735293092`](#17536 (comment)), taking ① widen the client. ⛔ Narrowing the read door was excluded by rule, not by preference: AGENTS.md rule 13 makes reversing PR #17517's landed decision a new ADR, which is `domain:spec`'s work and not this card's. ⛔ Nothing under `packages/spec` is touched here. ## The semver level, and the reading behind it ⚠️ The dispatch carried a reserved exit: **measure the level honestly, and if it needs `major`, stop and report rather than land.** It was measured, both legs, on this branch. | leg | command | exit | what it says | |---|---|---|---| | `major` on `@objectstack/client` | `node scripts/check-changeset-no-major.mjs --base origin/main` | **1** | ⛔ REFUSED — "Every publishable package is in the Changesets `fixed` (lockstep) group, so a single `major` promotes the ENTIRE monorepo… During the launch window ship breaking changes as `minor` instead." | | `minor` on `@objectstack/client` (what this PR carries) | same command | **0** | accepted | ⇒ **This does not need a `major`; it needs a `minor` plus the two carriers that stand in for the level during the launch window.** `check-changeset-no-major.mjs`'s own header states the convention and its end condition: *"During the launch window it is NOT the carrier… The mandatory information carriers for breaking-ness in the meantime are the BREAKING banner the author writes in the changeset body and the ADR-0087 migration-ledger disposition… THIS GUARD IS WHAT GETS DISARMED AT GA."* The window is open on this tree: there is no `.changeset/pre.json`, so the RC exemption is not in force and the guard is armed — which is what the exit-1 leg above demonstrates rather than assumes. ⛔ The level was not lowered to quiet a gate, and the breaking-ness was not dropped to reach a lower level. Both carriers are present and both were driven: - the changeset body carries a **BREAKING** banner; - `node scripts/check-adr-0087-registration.mjs --base origin/main` exits **0** and reports `[BREAKING] not-required (no-migration-prescription)`. `type-surface-only` is deliberately NOT claimed, and the changeset writes out why it is unavailable on the merits: that category is for a published TYPE-surface NARROWING moving off an erased type, and at the merge base all four members carried a concrete annotation. **Under strict semver this is breaking, and the changeset says so in words.** What the launch-window convention decides is only which field carries that fact. ## What a consumer pays, measured The element is a union of two whole, CLOSED stages that differ in exactly one key, `manifest`. Every other member of the row — `id`, `name`, `version`, `status`, `enabled`, `installedAt`, … — is common to both branches and reads exactly as before, so code that touches only those needs no change. Code that reaches INTO `manifest` separates the stages first: the authoring stage's `objects` are glob STRINGS, the assembled stage's are object DEFINITIONS, and the compiler now says so at the call site instead of letting a glob-shaped read compile against a row carrying definitions. In this repository the consumer cost is **zero sites outside `packages/client` itself**. No other workspace package calls either read member; the only in-tree mentions are `packages/client/README.md` (a bare `await client.packages.list();` with no member read, type-checked green by `check:skill-examples` against the rebuilt declarations) and the pins in this PR. **The three WRITE members did not move.** `install` / `enable` / `disable` answer the row their own request contract produced — `PackageInstallRequestSchema` declares `manifest: ManifestSchema` — and PR #17517 moved the read doors alone. That asymmetry is a measurement, not an oversight, and it is pinned. ## Tests — type-level, with a control that can fail `packages/client/src/return-type-precision.test.ts` is the only place a return-type move CAN be pinned: nothing about the runtime values changed, so a runtime test is green either way. - **direction 1** — all four read members now ADMIT an assembled-stage row; - **the control** — the same assignment against the unmoved `packages.install` is a used `@ts-expect-error`. If `AssembledInstalledPackage` were assignable to `InstalledPackage` after all, that suppression would go unused (TS2578) and direction 1 would be exposed as passing vacuously; - **direction 2** — a `manifest` belonging to NEITHER stage is still refused, so this line reddens if anyone ever "widens" these members to `any`, `unknown` or an open shape. **Ablation** (restore `packages/client/src/index.ts` to blob `c12b554d20` — the blob it carried before `21e6b9887c`, a **fixed** anchor rather than the moving `origin/main`, keep the test file, run `tsc --noEmit -p tsconfig.test.json`): exit **2**, **ten** errors — the five `toEqualTypeOf` pins naming the union (TS2344) and **five** TS2322: the four direction-1 assignments plus the new #19324 gap pin. Zero TS2578 — both controls stay USED in both states. The restore was verified by blob identity against `HEAD` with `git diff HEAD` empty; both `@ts-expect-error` controls stay USED in the ablated run, which is why they are labelled GREEN IN BOTH STATES rather than offered as evidence. ## Gates Derived from the actual diff with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the merged head, every exit code captured to disk before any pipe, and reconciled with `--ran`: ``` 58 derived, 58 run, 0 NOT-MEASURED, 0 UNRUN — all 58 exit 0 pnpm --filter '@objectstack/client^...' build :: exit 0 pnpm --filter @objectstack/client typecheck :: exit 0 (tsc + check:test-typecheck, 0 errors) pnpm --filter @objectstack/client test :: exit 0 (48 files / 566 tests) pnpm lint :: exit 0 — the FULL unnarrowed repo union, at 4fe43c0 ``` Two of the 58 first answered `PREREQUISITE NOT MET` (`check:skill-examples`, `check:dual-build-cjs-loads`), which is NOT MEASURED and not a pass: both read built output and the packages had no `dist`. They were built and re-run, and both then exited 0 on the merits — `check:skill-examples` type-checks 258 prose examples across three surfaces, including the client SDK's 23 blocks, against the rebuilt declarations. `origin/main` was merged once (`e3b3cdd2df`) before this PR opened; build state was refreshed and the whole derived set plus the lint union were re-run on the merged head. ## Acceptance notes ⚠️ **One finding, in `packages/spec`, reported rather than filed or fixed** — this lane does not touch that package and does not file cards. Measured on this branch, and it bears on how much the union actually buys a consumer: `AssembledInstalledPackage`'s `manifest` resolves to an index-signature type (`Record` of `string` to `unknown`) in the PUBLISHED TypeScript type, not to the assembled body's declared shape. The cause is the cast in `packages/spec/src/api/package-api.zod.ts`: `AssembledPackageRecordBodySchema` is built on `(AssembledPackageBodySchema as unknown as z.ZodObject(z.ZodRawShape))`, whose `z.input` is an index signature. Driven with `tsc`: `InstalledPackage` IS assignable to `AssembledInstalledPackage` (an authoring manifest satisfies an index signature), so the assembled arm absorbs the authoring arm at the type level, and a member read off the assembled stage's `manifest` arrives as `unknown` rather than its declared type. The runtime Zod parse is unaffected — the schema still checks the assembled body member by member, exactly as its own docblock says. This is the same failure family `check:exported-any` exists for ("the snapshot records that an export exists, never what it resolves to"), reached by a different spelling. Dedupe words: `AssembledPackageRecordBodySchema`, `ZodRawShape`, `package-api.zod`, `assembled manifest type erosion`, `z.input index signature`. noted, not filed: `@objectstack/client` re-exports neither `InstalledPackage` nor `InstalledPackageAtEitherStage`, so a consumer writing an explicit annotation reaches into `@objectstack/spec/api` for it. That is unchanged by this PR — the SDK has never re-exported the package row — and adding a re-export would be a new published export on a package this card is only correcting. Carrier: none; no queued PR or lane touches this surface. noted, not filed: the stale comment triage measured on `packages.get` (it claimed `GetInstalledPackageResponseSchema` is `data: InstalledPackageSchema`) is corrected in this PR rather than filed, per that ruling's explicit instruction. It is corrected in place with the reading that falsified it, not deleted. --- _Generated by [Claude Code](https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e56112c commit be7382d

3 files changed

Lines changed: 350 additions & 25 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"@objectstack/client": minor
3+
---
4+
5+
fix(client): the four `packages` READ members declare the stage their door is declared at (#17536)
6+
7+
Clause-②: yes
8+
9+
**BREAKING** for TypeScript consumers — a published TYPE-surface WIDENING, shipped as `minor` under the launch-window convention (`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by this banner and the ADR-0087 disposition below, never by the level). No runtime behaviour changes, and none is possible here: only a declaration moved, and the values these four methods resolve to are the values they have always resolved to.
10+
11+
`ObjectStackClient.packages.list` / `.get` and their `ScopedEnvironmentClient` twins returned `InstalledPackage` — the AUTHORING manifest stage, imported from `@objectstack/spec/kernel`. Since PR #17517 both read doors have been declared at EITHER stage: `ListInstalledPackagesResponseSchema.packages` is `z.array(InstalledPackageAtEitherStageSchema)` and `GetInstalledPackageResponseSchema.data` is that same schema. ⇒ A response the server is declared able to send was one this SDK's own types said could not arrive.
12+
13+
`packages/spec` is the one contract between producers and consumers, and `packages/client` is a consumer of it, so the consumer's declaration is what moves. All four now declare `InstalledPackageAtEitherStage` from `@objectstack/spec/api`.
14+
15+
**What the union is.** It is a union over the two manifest stages — `InstalledPackageSchema` and `AssembledInstalledPackageSchema` — which differ in exactly one key, `manifest`. Every other member of the row (`id`, `name`, `version`, `status`, `enabled`, `installedAt`, …) is common to both branches and reads exactly as it did, so code that reads only those members needs no change at all.
16+
17+
**Where the RUNTIME and the TYPE disagree, measured at this head.** The runtime schema is the strict half: `InstalledPackageAtEitherStageSchema.safeParse(row)` answers `success: false` for a row whose `manifest` belongs to neither stage — measured on a `{ bogus: 1, objects: 'not-even-an-array' }` manifest and on an empty `{}` one. The published TYPE is NOT that strict: on the assembled branch `manifest` is declared `Record<string, unknown>`, so both of those same rows COMPILE against the declared return type. ⛔ Do not read this widening as a type-level guarantee about `manifest` — the guarantee is the parse's. The type-level tolerance is a known gap, tracked as **#19324**; its root cause is the deliberate `z.ZodType<Record<string, unknown>, …>` annotation at `packages/spec/src/stack.zod.ts:1283` (#14513 — TS7056 and a declaration-chunk ceiling), and it is ⛔ not this change's to fix. It is recorded as a pin in `packages/client/src/return-type-precision.test.ts`, which reddens the day the gap closes.
18+
19+
**What a consumer does, concretely.** A member read off `manifest` on the union arrives as `unknown` (measured: `pkg.manifest.objects` is `unknown`). ⇒ a caller that reaches INTO `manifest` narrows by PARSING the row with a `packages/spec` schema and reading the parse's output:
20+
21+
```ts
22+
import { AssembledInstalledPackageSchema } from '@objectstack/spec/api';
23+
import { InstalledPackageSchema } from '@objectstack/spec/kernel';
24+
25+
const row = await client.packages.get(id);
26+
const parsed = AssembledInstalledPackageSchema.safeParse(row);
27+
if (parsed.success) {
28+
// parsed.data.manifest — the ASSEMBLED stage, object definitions
29+
} else {
30+
const authoring = InstalledPackageSchema.parse(row);
31+
// authoring.manifest.objects — the AUTHORING stage, glob strings
32+
}
33+
```
34+
35+
⛔ Do NOT narrow with `Array.isArray(pkg.manifest.objects)`, or with any other structural guess. It separates the stages on NEITHER level: at the type level `pkg.manifest` is the same union inside both branches of that `if`, and at runtime BOTH stages' `objects` are arrays — `z.array(z.string())` at the authoring stage against `z.array(ObjectSchema)` at the assembled one. (Measured: a row carrying no `objects` at all parses as either stage, which is the right answer for it — the key such a guess would read is not there.)
36+
37+
In this repository the whole consumer cost is zero sites outside `packages/client` itself: no other workspace package calls either read member.
38+
39+
**The WRITE members did not move** and stay declared at the authoring stage — all four of them: `install`, `enable`, `disable`, `update` still answer `Promise<InstalledPackage>`. `install` answers the row its own request contract produced (`PackageInstallRequestSchema` declares `manifest: ManifestSchema`), and PR #17517 moved the read doors alone. That asymmetry is the measurement, not an oversight, and it is pinned.
40+
41+
The type is reached the same way `InstalledPackage` always was, from `@objectstack/spec` rather than re-exported here: this SDK has never re-exported the package row, and this change does not start.
42+
43+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable moves. No metadata key, no authored property, no config field, no accepted request shape and no stored artifact changes spelling or shape: the edit is four declared RETURN TYPES on one SDK class pair plus their docblocks, so `objectstack migrate meta` has nothing to rewrite, `spec-changes.json` has nothing to project and the upgrade guide has no row to gain. The party addressed is a TYPESCRIPT CONSUMER and the delivery channel is the compiler at their own call site — the audience the ADR-0087 ledger explicitly does not serve. The "what a consumer does" paragraph above is a source-code statement about narrowing a union, not a stored-metadata rewrite, which is the distinction #13080 records this refusal cannot make on its own.
44+
`type-surface-only` is NOT claimed here, and it is unavailable on the merits rather than on a resolution defect — measured by driving the gate, not assumed. That category was added for a published TYPE-surface NARROWING, and its predicate 4 (`narrowed-from-erased`) reads the base annotation through `isErasedType`, whose line is "the type IS `any` / `unknown`". At the merge base all four members carried a CONCRETE annotation (`Promise<InstalledPackage>`, `Promise<{ packages: InstalledPackage[]; total: number }>`), so nothing moved off an erased type; and this change runs in the opposite direction from the one the category names. The **BREAKING** banner is carried rather than dropped — that erosion is exactly what #13080 was filed about. -->

‎packages/client/src/index.ts‎

Lines changed: 118 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,20 @@ import {
123123
PackageExportManifest,
124124
ReassignOrphanedMetadataResponse,
125125
DuplicatePackageResponse,
126+
// [#17536] The element the two `/packages` READ doors are declared to serve.
127+
// `ListInstalledPackagesResponseSchema.packages` is
128+
// `z.array(InstalledPackageAtEitherStageSchema)` and
129+
// `GetInstalledPackageResponseSchema.data` is that same schema
130+
// (`spec/src/api/package-api.zod.ts`) — a union over the two manifest stages,
131+
// authoring (`InstalledPackageSchema`) and assembled
132+
// (`AssembledInstalledPackageSchema`), each a closed RUNTIME declaration. The
133+
// client is a CONSUMER of that contract, so the widest value those doors are
134+
// declared to answer is what they are declared to return here. ⚠️ What the
135+
// published TYPE admits is wider than what the runtime parse accepts — the
136+
// measurement, and what a caller does about it, are on `packages.list` below
137+
// (#19324). The WRITE methods on the same object keep `InstalledPackage`:
138+
// PR #17517 moved the read doors alone.
139+
InstalledPackageAtEitherStage,
126140
} from '@objectstack/spec/api';
127141
import type {
128142
ApprovalRequestRow,
@@ -2453,8 +2467,61 @@ export class ObjectStackClient {
24532467
* deliberately NOT declared here — the dispatcher rows have no such key, so
24542468
* declaring it would be false on that surface. #8140 bound the identical
24552469
* scoped sibling (`ScopedEnvironmentClient.packages.list`) the same way.
2456-
*/
2457-
list: async (filters?: { status?: string; type?: string; enabled?: boolean }): Promise<{ packages: InstalledPackage[]; total: number }> => {
2470+
*
2471+
* ⚠️ [#17536] The ROW type moved, the envelope did not. #11925 bound the
2472+
* element to `InstalledPackage` — the AUTHORING stage — and PR #17517 then
2473+
* declared the door at EITHER stage:
2474+
* `ListInstalledPackagesResponseSchema.packages` is
2475+
* `z.array(InstalledPackageAtEitherStageSchema)`. That left two declarations
2476+
* one layer apart disagreeing, with this one under-declaring: a row a
2477+
* `defineStack()` host installed carries the ASSEMBLED body, the door is
2478+
* declared able to serve it, and this SDK's own types said it could not
2479+
* arrive. The contract is `packages/spec` and this is a consumer of it
2480+
* (Prime Directive #12), so the consumer's declaration is what moves.
2481+
*
2482+
* ⚠️ Clause-② widening, and what it costs a caller: the element is a union
2483+
* of two stages that differ in ONE key, `manifest`. Every other member of
2484+
* the row — `id`, `name`, `version`, `status`, `enabled`, … — is common to
2485+
* both branches and reads exactly as before, so a caller that reads only
2486+
* those members needs no change at all. No runtime behaviour changes; the
2487+
* values were always these.
2488+
*
2489+
* ⚠️ Reaching INTO `manifest` is where the cost is, and the TYPE will not
2490+
* do the narrowing for you. On the assembled branch `manifest` is declared
2491+
* `Record<string, unknown>`, so a member read off the union —
2492+
* `pkg.manifest.objects` — is `unknown` (measured with `tsc` against the
2493+
* published declarations, not inferred from the schemas). Narrow by PARSING
2494+
* the row with a `packages/spec` schema and reading the parse's output:
2495+
*
2496+
* ```ts
2497+
* const parsed = AssembledInstalledPackageSchema.safeParse(pkg); // spec/api
2498+
* if (parsed.success) {
2499+
* // parsed.data.manifest — the ASSEMBLED stage, object definitions
2500+
* } else {
2501+
* const authoring = InstalledPackageSchema.parse(pkg); // spec/kernel
2502+
* // authoring.manifest.objects — the AUTHORING stage, glob strings
2503+
* }
2504+
* ```
2505+
*
2506+
* ⛔ Do NOT narrow with `Array.isArray(pkg.manifest.objects)`, or with any
2507+
* other structural guess. It separates the stages on NEITHER level: at the
2508+
* type level `pkg.manifest` is the same union inside both branches of that
2509+
* `if`, and at runtime BOTH stages' `objects` are arrays —
2510+
* `z.array(z.string())` at the authoring stage against `z.array(ObjectSchema)`
2511+
* at the assembled one. (Measured: a row carrying no `objects` at all parses
2512+
* as either stage, which is the right answer for it — the key such a guess
2513+
* would read is not there.)
2514+
*
2515+
* ⚠️ The RUNTIME half is the strict one, and the asymmetry is a KNOWN GAP
2516+
* rather than a design: `InstalledPackageAtEitherStageSchema.safeParse()`
2517+
* refuses a `manifest` belonging to neither stage, while that same row
2518+
* COMPILES against this declaration. Tracked as #19324, whose root cause is
2519+
* the deliberate `z.ZodType<Record<string, unknown>, …>` annotation at
2520+
* `packages/spec/src/stack.zod.ts:1283` (#14513 — TS7056 and a
2521+
* declaration-chunk ceiling); ⛔ not something this declaration can fix, and
2522+
* ⛔ not a licence to relax either runtime branch to match the type.
2523+
*/
2524+
list: async (filters?: { status?: string; type?: string; enabled?: boolean }): Promise<{ packages: InstalledPackageAtEitherStage[]; total: number }> => {
24582525
const route = this.getRoute('packages');
24592526
const params = new URLSearchParams();
24602527
if (filters?.status) params.set('status', filters.status);
@@ -2463,7 +2530,7 @@ export class ObjectStackClient {
24632530
const qs = params.toString();
24642531
const url = `${this.baseUrl}${route}${qs ? '?' + qs : ''}`;
24652532
const res = await this.fetch(url);
2466-
return this.unwrapResponse<{ packages: InstalledPackage[]; total: number }>(res);
2533+
return this.unwrapResponse<{ packages: InstalledPackageAtEitherStage[]; total: number }>(res);
24672534
},
24682535

24692536
/**
@@ -2489,29 +2556,44 @@ export class ObjectStackClient {
24892556
*
24902557
* That is the SAME projection its `list` neighbour maps over every row
24912558
* (`packages/runtime/src/domains/packages.ts` — one expression, two
2492-
* doors), and `list` is already declared `InstalledPackage[]` directly
2493-
* above. This binding therefore makes two doors of one domain agree
2494-
* rather than making a new claim about either. `packages/spec` has
2495-
* declared the same thing all along and was never the fork's casualty:
2496-
* `GetInstalledPackageResponseSchema` is `data: InstalledPackageSchema`,
2497-
* the bare row.
2559+
* doors), and `list` carries the identical element type directly above.
2560+
* This binding therefore makes two doors of one domain agree rather than
2561+
* making a new claim about either.
2562+
*
2563+
* ⚠️ [#17536] The sentence that closed this paragraph was measured STALE
2564+
* and is corrected rather than deleted, because what it claimed is what
2565+
* made the authoring-stage binding below look settled. It read:
2566+
* «`packages/spec` has declared the same thing all along and was never the
2567+
* fork's casualty: `GetInstalledPackageResponseSchema` is `data:
2568+
* InstalledPackageSchema`, the bare row.» The first half still holds — the
2569+
* door has always answered the BARE row, no envelope. The second half
2570+
* stopped being true when PR #17517 landed: that schema's `data` now reads
2571+
* `InstalledPackageAtEitherStageSchema`, the union of the two closed
2572+
* manifest stages. It was a COMMENT and never a binding, which is why the
2573+
* disagreement it describes could sit here unread by any gate.
24982574
*
24992575
* `source` stays undeclared because there is no longer anything that
25002576
* emits it on this route; `writable` stays undeclared for the reason
25012577
* `list` leaves it undeclared.
25022578
*
2503-
* ⚠️ Clause-② narrowing. `{ package: any }` is what let
2579+
* ⚠️ Clause-② narrowing (#12034). `{ package: any }` is what let
25042580
* `(await client.packages.get(id)).package` compile, and on the only
25052581
* surface that has served this route since #16628 it was `undefined` at
25062582
* runtime — the falsehood was invisible precisely because the member was
25072583
* `any`. Callers read the row itself. Pinned in
25082584
* `return-type-precision.test.ts`, which is the only place it CAN be
25092585
* pinned: a runtime test cannot observe a return-type narrowing at all.
2586+
*
2587+
* ⚠️ Clause-② widening (#17536), the LATER and opposite move on the same
2588+
* member: the row is declared at whichever stage it was installed at, for
2589+
* the reason and at the cost written out on `list` above. Both directions
2590+
* are pinned in the same file, and for the same reason — only a
2591+
* compile-time assertion can observe either.
25102592
*/
2511-
get: async (id: string): Promise<InstalledPackage> => {
2593+
get: async (id: string): Promise<InstalledPackageAtEitherStage> => {
25122594
const route = this.getRoute('packages');
25132595
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}`);
2514-
return this.unwrapResponse<InstalledPackage>(res);
2596+
return this.unwrapResponse<InstalledPackageAtEitherStage>(res);
25152597
},
25162598

25172599
/**
@@ -7896,9 +7978,19 @@ export class ScopedEnvironmentClient {
78967978
* package tests.
78977979
*/
78987980
packages = {
7899-
list: async (): Promise<{ packages: InstalledPackage[]; total: number }> => {
7981+
/**
7982+
* ⚠️ [#17536] Clause-② widening, in step with the global twin
7983+
* `client.packages.list`. This path and that one reach the SAME door: the
7984+
* `@objectstack/hono` catch-all hands `dispatch()` the still-scoped path,
7985+
* `dispatch()` strips the `/environments/:environmentId` prefix, and the
7986+
* `/packages` domain answers. One door cannot be declared at two different
7987+
* element types by two SDK members, so both move together — see
7988+
* `ObjectStackClient.packages.list` for the door reading, the Prime
7989+
* Directive #12 argument and what the union costs a caller.
7990+
*/
7991+
list: async (): Promise<{ packages: InstalledPackageAtEitherStage[]; total: number }> => {
79007992
const res = await this.parent._fetch(this.url('/packages'));
7901-
return this.parent._unwrap<{ packages: InstalledPackage[]; total: number }>(res);
7993+
return this.parent._unwrap<{ packages: InstalledPackageAtEitherStage[]; total: number }>(res);
79027994
},
79037995
/**
79047996
* [#11925 bound it · #12034 corrected the shape] The BARE row, no
@@ -7926,22 +8018,29 @@ export class ScopedEnvironmentClient {
79268018
* site) — and the `/packages` domain answers
79278019
* `success(withWritableVerdict(qlService, toPackageResponse(pkg)))`. That
79288020
* is the identical projection its `list` neighbour above maps over, which
7929-
* is why `list` already declares `InstalledPackage[]` and needed no
8021+
* is why `list` carries the identical element type and needed no
79308022
* correction here.
79318023
*
7932-
* ⚠️ Clause-② narrowing, and the sharper of the two: this member was
7933-
* `InstalledPackage`, not `any`, so `(await scoped.packages.get(id))
8024+
* ⚠️ Clause-② narrowing (#12034), and the sharper of the two: this member
8025+
* was `InstalledPackage`, not `any`, so `(await scoped.packages.get(id))
79348026
* .package` compiled with a REAL type behind it and was `undefined` at
79358027
* runtime.
79368028
*
8029+
* ⚠️ Clause-② widening (#17536), the later and opposite move: the row is
8030+
* declared at whichever manifest stage it was installed at, because that is
8031+
* what `GetInstalledPackageResponseSchema.data` has been declared as since
8032+
* PR #17517. Same door as the global twin, same element type, same cost to
8033+
* a caller that reaches into `manifest` — written out once on
8034+
* `ObjectStackClient.packages.list`.
8035+
*
79378036
* `version` is unchanged and deliberately not touched by this card — it
79388037
* is a request-side question, and it is a live one: see the acceptance
79398038
* notes on #12034.
79408039
*/
7941-
get: async (id: string, version?: string): Promise<InstalledPackage> => {
8040+
get: async (id: string, version?: string): Promise<InstalledPackageAtEitherStage> => {
79428041
const qs = version ? `?version=${encodeURIComponent(version)}` : '';
79438042
const res = await this.parent._fetch(this.url(`/packages/${encodeURIComponent(id)}${qs}`));
7944-
return this.parent._unwrap<InstalledPackage>(res);
8043+
return this.parent._unwrap<InstalledPackageAtEitherStage>(res);
79458044
},
79468045
};
79478046

0 commit comments

Comments
 (0)