From 17f1e3d41c7b3a9c6ca155d8f839916d30d833a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 06:06:08 +0000 Subject: [PATCH 1/3] docs(spec): record the record-stage body's index-signature type as the accepted static contract RecordStagePackageBodySchema and AssembledInstalledPackageSchema now state, in their published docblocks, that manifest's static type is deliberately Record (an index signature), that the runtime Zod schema is the enforced contract, why the type is not inferred (TS7056 at PackageApiContracts; the named-alias shared-chunk heap failure recorded beside AssembledPackageBodySchema), and that the A2 form is the precise one if the schema depth ever allows it. The ZodRawShape cast gets a note on what it costs and that it is type-only. The client gap pin's two comments cite RecordStagePackageBodySchema by name instead of a stale stack.zod.ts line number, and say the gap is the accepted static contract; the pin itself is unchanged. No type, schema or runtime change. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-authored-by: Claude --- ...4-record-stage-index-signature-docblock.md | 17 +++++++ .../client/src/return-type-precision.test.ts | 33 +++++++----- .../spec/src/api/package-api-assembled.zod.ts | 28 +++++++--- packages/spec/src/stack.zod.ts | 51 ++++++++++++++++++- 4 files changed, 108 insertions(+), 21 deletions(-) create mode 100644 .changeset/19324-record-stage-index-signature-docblock.md diff --git a/.changeset/19324-record-stage-index-signature-docblock.md b/.changeset/19324-record-stage-index-signature-docblock.md new file mode 100644 index 00000000000..07b87807057 --- /dev/null +++ b/.changeset/19324-record-stage-index-signature-docblock.md @@ -0,0 +1,17 @@ +--- +'@objectstack/spec': patch +--- + +`RecordStagePackageBodySchema` and `AssembledInstalledPackageSchema` now say, in their published docblocks, that `manifest`'s static type is deliberately an index signature and that the runtime schema is the enforced contract (#19324) + +Clause-②: no + +`AssembledInstalledPackage['manifest']` is `RecordStagePackageBodySchema`, declared `z.ZodType, Record>`. So its published type is an index signature: an authoring-stage `InstalledPackage` assigns to `AssembledInstalledPackage`, and a row whose `manifest` belongs to neither stage type-checks as an `InstalledPackageAtEitherStage`. The maintainer ruled that this is the accepted static contract (#19324, letter 丙). Both declarations now say so where a TypeScript reader meets them: + +- **The runtime schema is the enforced contract.** `InstalledPackageAtEitherStageSchema.safeParse()` refuses a `manifest` that belongs to neither stage. Tell the two stages apart by parsing, never by the static type. +- **Why the type is not inferred.** `tsc` refuses to print the whole metadata vocabulary into the declarations that embed it (TS7056). Dropping the record and artifact stages' annotations and the `ZodRawShape` cast fails the declaration build with TS7056 at `PackageApiContracts`. A named alias would turn `stack.zod` into a shared declaration chunk, the heap failure #14513 recorded. +- **The precise form, if the schema depth ever allows it,** is the one #19324 measured as A2, with its cost recorded at `RecordStagePackageBodySchema`. + +This settles what the `@objectstack/client` read-door changeset (#17536) calls "a known gap, tracked as #19324". The gap is not closing under #19324: it is the accepted static contract, and the client pin that records it stays. + +⛔ No behaviour changes. No type, schema, accept set, authorable key or export moves. Only TSDoc and source comments change, and they ship: `@objectstack/spec`'s published `files[]` carries `dist` (the TSDoc is emitted into the `.d.ts` / `.d.mts` declarations) and `src/**/*.zod.ts` (both edited files ship as source). diff --git a/packages/client/src/return-type-precision.test.ts b/packages/client/src/return-type-precision.test.ts index b204de42bc8..bc04ff849bc 100644 --- a/packages/client/src/return-type-precision.test.ts +++ b/packages/client/src/return-type-precision.test.ts @@ -686,14 +686,17 @@ declare const assembledRow: AssembledInstalledPackage; * * ⛔ It does NOT measure object-shaped tolerance, and at this head there is * some: on the assembled branch `manifest` is declared `Record` - * (the deliberate annotation at `packages/spec/src/stack.zod.ts:1283`, #14513), - * so an object `manifest` belonging to NEITHER stage compiles against these - * members. The runtime is the half that is correct — + * (the deliberate annotation on `RecordStagePackageBodySchema` in + * `packages/spec/src/stack.zod.ts`, the #14513 pattern), so an object + * `manifest` belonging to NEITHER stage compiles against these members. The + * runtime is the half that is correct — * `InstalledPackageAtEitherStageSchema.safeParse()` refuses that same row, and * that refusal is pinned beside its producer in * `packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts`. - * The type-level gap is #19324's to close; the third pin below records it as the - * behaviour it is, so the day it closes this file says so. + * The type-level gap is the accepted static contract: #19324 was ruled (letter + * 丙) to record it at `RecordStagePackageBodySchema` rather than close it. The + * third pin below records it as the behaviour it is, so the day the body is + * typed precisely this file says so. * * ## Ablation, measured rather than asserted * @@ -761,16 +764,20 @@ export function installedPackageEitherStagePins17536(): void { // ⛔ NOT a guarantee — a measurement, written down so it cannot change in // silence. `AssembledInstalledPackage['manifest']` is // `Record` (from the deliberate - // `z.ZodType, …>` annotation at - // `packages/spec/src/stack.zod.ts:1283`, #14513 — TS7056 and a - // declaration-chunk ceiling), so the assembled branch admits ANY object and - // the assignment below COMPILES at this head. Measured with `tsc` against the - // published declarations; the runtime disagrees and is the correct half: + // `z.ZodType, …>` annotation on + // `RecordStagePackageBodySchema` in `packages/spec/src/stack.zod.ts`, the + // #14513 pattern — TS7056 and a declaration-chunk ceiling), so the + // assembled branch admits ANY object and the assignment below COMPILES at + // this head. Measured with `tsc` against the published declarations; the + // runtime disagrees and is the correct half: // `InstalledPackageAtEitherStageSchema.safeParse()` answers `success: false` - // for this very row. + // for this very row. #19324 was ruled (letter 丙) to keep this as the + // accepted static contract, so this pin stays until the body is typed + // precisely. // - // ⚠️ There is deliberately no `@ts-expect-error` here. The day #19324 types - // the assembled body, tsc reds on THIS line — and that red is the + // ⚠️ There is deliberately no `@ts-expect-error` here. The day the assembled + // body is typed precisely (the A2 form recorded at + // `RecordStagePackageBodySchema`), tsc reds on THIS line — and that red is the // notification this pin exists to deliver: read it as "the gap closed", then // delete this block and tighten the `manifest` guidance on // `ObjectStackClient.packages.list` in `index.ts`, which sends callers diff --git a/packages/spec/src/api/package-api-assembled.zod.ts b/packages/spec/src/api/package-api-assembled.zod.ts index 1be097dfed6..84c731fbcf4 100644 --- a/packages/spec/src/api/package-api-assembled.zod.ts +++ b/packages/spec/src/api/package-api-assembled.zod.ts @@ -79,13 +79,9 @@ import { * C and was REJECTED by name: a union AT THE KEY makes neither stage checkable, * which is the tolerate-at-the-consumer shape Prime Directive #12 refuses. So * `ManifestSchema` is untouched here — still `strictObject`, still globs — and - * the assembled stage gets its own name, built from `AssembledPackageBodySchema` - * (#14242's own declaration) rather than a second transcription of it. - * - * The body half is deliberately typed `Record`; the reason is - * recorded at `AssembledPackageBodySchema` and is not repeated here. The RUNTIME - * schema still carries the manifest's every field plus every collection's full - * declaration, so a wrong-shaped body is refused exactly as it is there. + * the assembled stage gets its own name, built from the same body shape as + * `AssembledPackageBodySchema` (#14242's own declaration) rather than a second + * transcription of it, at the record stage the next section describes. * * ## The row's manifest is the RECORD stage, not the assembled one * @@ -124,6 +120,24 @@ import { * members that need the treatment is MEASURED, never hand-picked — pinned * key-by-key in `./package-api.test.ts`, so a new collection with no JSON form * reddens there, naming itself. + * + * ## `manifest`'s published type is deliberately an index signature + * + * `RecordStagePackageBodySchema` is declared `z.ZodType, Record>`, so this row's `manifest` publishes as an + * index signature: an authoring-stage `InstalledPackage` assigns to + * `AssembledInstalledPackage`, and at the type level this arm absorbs the + * authoring arm of {@link InstalledPackageAtEitherStageSchema}. That is the + * accepted static contract — the maintainer ruling on #19324 (letter 丙) — not + * a gap to tighten in passing. The RUNTIME schema is the enforced contract: it + * still carries the manifest's every field plus every collection's full + * declaration, so `.parse()` refuses a wrong-shaped body — tell the two stages + * apart by parsing, never by the static type. It is not inferred because `tsc` + * refuses to print the whole metadata vocabulary into the declarations that + * embed it (TS7056; #14513 measured it on the assembled body, and on this + * stage it fires at `PackageApiContracts` below). The precise form, if the + * schema depth ever allows it, is A2 — the reasons and A2's measured cost are + * recorded once, at `RecordStagePackageBodySchema` in `../stack.zod`. */ export const AssembledInstalledPackageSchema = lazySchema(() => InstalledPackageSchema.extend({ manifest: RecordStagePackageBodySchema.describe('The ASSEMBLED package body this row carries, at the stage the registry records it'), diff --git a/packages/spec/src/stack.zod.ts b/packages/spec/src/stack.zod.ts index 3c09282f163..dcdfef43bf3 100644 --- a/packages/spec/src/stack.zod.ts +++ b/packages/spec/src/stack.zod.ts @@ -1450,8 +1450,57 @@ export type ArtifactStagePackageBodyParsed = z.infer, Record>`, so the published type of a record-stage body is an index + * signature: any object assigns to it. It is the `manifest` of + * `AssembledInstalledPackageSchema` (`@objectstack/spec/api-assembled`), so an + * authoring-stage `InstalledPackage` assigns to `AssembledInstalledPackage`, + * and a row whose `manifest` belongs to neither stage type-checks as an + * `InstalledPackageAtEitherStage`. That is the accepted static contract — the + * maintainer ruling on #19324 (letter 丙) — not a gap to tighten in passing. + * + * The RUNTIME schema is the enforced contract. It carries the manifest's every + * field, every collection's full declaration and the two lowered members + * above, so `.parse()` refuses a wrong-shaped body member by member. Narrow a + * record-stage body by parsing it, never by trusting its static type. + * + * Why not inferred: the body is the whole metadata vocabulary, and `tsc` + * refuses to print it into the declarations that embed it — TS7056, 「The + * inferred type of this node exceeds the maximum length the compiler will + * serialize」. #14513 measured that on the assembled body; for this stage, + * dropping this annotation, the artifact stage's and the cast below fails the + * declaration build with TS7056 at `PackageApiContracts` (measured on #19324 + * at `d1ca8741dd` and again at `3bd28e2b2e`). Why not a named type: #14513 + * measured that an alias declared in this module turns `stack.zod` into a + * shared declaration chunk its embedders import, and recorded the heap failure + * that followed beside {@link AssembledPackageBodySchema}; for this stage that + * reading is inherited, not re-measured. + * + * The precise form, if the schema depth ever allows it, is the one #19324 + * measured as A2: infer this stage and the artifact stage (drop both + * annotations and the cast), and give compact `typeof`-based annotations to + * the four declarations that embed the body — `PackageApiContracts`, + * `InstalledPackageAtEitherStageSchema`, `ListInstalledPackagesResponseSchema` + * and `GetInstalledPackageResponseSchema`. At `d1ca8741dd` it built, turned + * all five of the gap's measured type readings into compile errors, and left + * the runtime bundles byte-identical. Its cost is declaration size: +20,209 + * lines in the then `./api` entry (one expansion of this stage) and +44,342 in + * the root entry (this stage and the artifact stage). The heaviest type-check + * program under CI's heap ceiling, `qa/http-conformance`'s, was not measured + * under it. + */ +/* + * ANNOTATED structurally — see the note on the artifact stage above, and the + * section of this docblock on the published type for what that costs a + * consumer. The `as unknown as z.ZodObject` cast below is that + * annotation's price: the artifact stage's declared type no longer says it is + * an object, so `.extend()` is reached through a cast. The cast is type-only + * and emits nothing; at runtime `.extend()` runs on the artifact stage's + * object schema. */ -/* ANNOTATED structurally — see the note on the artifact stage above. */ export const RecordStagePackageBodySchema: z.ZodType, Record> = lazySchema(() => (ArtifactStagePackageBodySchema as unknown as z.ZodObject).extend({ From a2be2ff440bbefaf27325437cfaa9dfbe1c60ddb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 07:10:18 +0000 Subject: [PATCH 2/3] docs(client): packages.list says the manifest index signature is the accepted static contract The packages.list TSDoc called the type/runtime asymmetry a known gap tracked on an open card and cited a stale stack.zod.ts line. The card was ruled the other way: the index signature on the record-stage body is the accepted static contract, the runtime Zod schema is the enforced one, and the A2 form is recorded beside RecordStagePackageBodySchema for the day the schema depth allows it. The paragraph now says so and cites the schema by name. Comment text only. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-authored-by: Claude --- packages/client/src/index.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 4f334d3148a..7c470ecb5d8 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -2513,14 +2513,19 @@ export class ObjectStackClient { * as either stage, which is the right answer for it — the key such a guess * would read is not there.) * - * ⚠️ The RUNTIME half is the strict one, and the asymmetry is a KNOWN GAP - * rather than a design: `InstalledPackageAtEitherStageSchema.safeParse()` - * refuses a `manifest` belonging to neither stage, while that same row - * COMPILES against this declaration. Tracked as #19324, whose root cause is - * the deliberate `z.ZodType, …>` annotation at - * `packages/spec/src/stack.zod.ts:1283` (#14513 — TS7056 and a - * declaration-chunk ceiling); ⛔ not something this declaration can fix, and - * ⛔ not a licence to relax either runtime branch to match the type. + * ⚠️ The RUNTIME half is the strict one, and the asymmetry is the ACCEPTED + * static contract, not a gap waiting to close: + * `InstalledPackageAtEitherStageSchema.safeParse()` refuses a `manifest` + * belonging to neither stage, while that same row COMPILES against this + * declaration. The index signature comes from the deliberate + * `z.ZodType, …>` annotation on + * `RecordStagePackageBodySchema` in `@objectstack/spec` (the #14513 pattern — + * TS7056 and a declaration-chunk ceiling), and the maintainer ruled on + * #19324 (letter 丙) to keep it: the runtime Zod schema is the enforced + * contract, so narrow by parsing, as above. The precise form, A2, is + * recorded beside `RecordStagePackageBodySchema` for the day the schema + * depth allows it. ⛔ Not something this declaration can fix, and ⛔ not a + * licence to relax either runtime branch to match the type. */ list: async (filters?: { status?: string; type?: string; enabled?: boolean }): Promise<{ packages: InstalledPackageAtEitherStage[]; total: number }> => { const route = this.getRoute('packages'); From b9b0d32d2f49ab7a6a3eeaee57b0adbdcd2b3377 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 07:23:40 +0000 Subject: [PATCH 3/3] chore(changeset): the client TSDoc rewrite ships, so @objectstack/client takes a patch line Measured on the rebuilt packages/client dist: the rewritten packages.list paragraph lands in index.d.ts, index.d.mts, index.js and index.mjs, and @objectstack/client publishes dist. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-authored-by: Claude --- .../19324-record-stage-index-signature-docblock.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.changeset/19324-record-stage-index-signature-docblock.md b/.changeset/19324-record-stage-index-signature-docblock.md index 07b87807057..8df32023bfc 100644 --- a/.changeset/19324-record-stage-index-signature-docblock.md +++ b/.changeset/19324-record-stage-index-signature-docblock.md @@ -1,17 +1,23 @@ --- '@objectstack/spec': patch +'@objectstack/client': patch --- -`RecordStagePackageBodySchema` and `AssembledInstalledPackageSchema` now say, in their published docblocks, that `manifest`'s static type is deliberately an index signature and that the runtime schema is the enforced contract (#19324) +`RecordStagePackageBodySchema`, `AssembledInstalledPackageSchema` and `ObjectStackClient.packages.list` now say, in their published docblocks, that `manifest`'s static type is deliberately an index signature and that the runtime schema is the enforced contract (#19324) Clause-②: no -`AssembledInstalledPackage['manifest']` is `RecordStagePackageBodySchema`, declared `z.ZodType, Record>`. So its published type is an index signature: an authoring-stage `InstalledPackage` assigns to `AssembledInstalledPackage`, and a row whose `manifest` belongs to neither stage type-checks as an `InstalledPackageAtEitherStage`. The maintainer ruled that this is the accepted static contract (#19324, letter 丙). Both declarations now say so where a TypeScript reader meets them: +`AssembledInstalledPackage['manifest']` is `RecordStagePackageBodySchema`, declared `z.ZodType, Record>`. So its published type is an index signature: an authoring-stage `InstalledPackage` assigns to `AssembledInstalledPackage`, and a row whose `manifest` belongs to neither stage type-checks as an `InstalledPackageAtEitherStage`. The maintainer ruled that this is the accepted static contract (#19324, letter 丙). The three declarations now say so where a TypeScript reader meets them: - **The runtime schema is the enforced contract.** `InstalledPackageAtEitherStageSchema.safeParse()` refuses a `manifest` that belongs to neither stage. Tell the two stages apart by parsing, never by the static type. - **Why the type is not inferred.** `tsc` refuses to print the whole metadata vocabulary into the declarations that embed it (TS7056). Dropping the record and artifact stages' annotations and the `ZodRawShape` cast fails the declaration build with TS7056 at `PackageApiContracts`. A named alias would turn `stack.zod` into a shared declaration chunk, the heap failure #14513 recorded. - **The precise form, if the schema depth ever allows it,** is the one #19324 measured as A2, with its cost recorded at `RecordStagePackageBodySchema`. +**`@objectstack/client`**: the `packages.list` TSDoc used to call this asymmetry "a KNOWN GAP rather than a design", tracked on #19324, and cited a `stack.zod.ts` line number. It now calls it the accepted static contract, cites `RecordStagePackageBodySchema` by name, and keeps its advice unchanged: narrow a row by parsing it with a `@objectstack/spec` schema, and never by `Array.isArray(pkg.manifest.objects)`. + This settles what the `@objectstack/client` read-door changeset (#17536) calls "a known gap, tracked as #19324". The gap is not closing under #19324: it is the accepted static contract, and the client pin that records it stays. -⛔ No behaviour changes. No type, schema, accept set, authorable key or export moves. Only TSDoc and source comments change, and they ship: `@objectstack/spec`'s published `files[]` carries `dist` (the TSDoc is emitted into the `.d.ts` / `.d.mts` declarations) and `src/**/*.zod.ts` (both edited files ship as source). +⛔ No behaviour changes. No type, schema, accept set, authorable key or export moves. Only TSDoc and source comments change, and they ship: + +- `@objectstack/spec`'s published `files[]` carries `dist`, where the TSDoc is emitted into the `.d.ts` / `.d.mts` declarations, and `src/**/*.zod.ts`, so both edited files also ship as source. +- `@objectstack/client`'s published `files[]` carries `dist`, where the rewritten paragraph lands in `index.d.ts`, `index.d.mts`, `index.js` and `index.mjs`.