Skip to content

Commit 23c209f

Browse files
committed
fix(spec): annotate the assembled package body with a STRUCTURAL type so the stack schema's printed declaration carries no named alias (#14439)
A named type alias inside `ObjectStackDefinitionSchema`'s printed type (`manifest: z.ZodType<AssembledPackageBodyParsed, AssembledPackageBody>` on the `packages` element) can only be IMPORTED by the declaration bundler, never inlined. `system/environment-artifact.zod.ts` embeds the stack type, so the bundler turned `stack.zod` into a shared chunk and gave the `environment-artifact` chunk an import edge into it: every consumer of `@objectstack/spec/system` started loading the entire stack schema declaration it never loaded before. Measured on the `qa/http-conformance` TEST_DEBT re-measure program, same shape as the gate builds it, 8 GB cap so the peak is measured rather than hit: base `53d36892` 691,580 lines of definitions / 4,473,321 K heap; PR head `3878a582` 734,202 (+42,622 — the size of the stack schema's declaration) / 4,875,249 K — over the 4096 MB ceiling `scripts/check-type-check-coverage.mjs` pins as CI's, which is the red `Type Check · debt ledger` lane on both earlier pushes. `AssembledPackageBodySchema` is now annotated `z.ZodType<Record<string, unknown>, Record<string, unknown>>` and the two ADR-0122 aliases are derived FROM the schema (`z.input` / `z.infer`), so nothing named can re-enter the stack schema's printed type. The runtime schema is unchanged: manifest fields plus every collection, key set still derived from `COMPOSE_KEY_DISPOSITIONS`. What consumers lose is static field typing inside an assembled body, which the PR's readers (`compile.ts`, `artifact-packages.ts`) never relied on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
1 parent 3878a58 commit 23c209f

3 files changed

Lines changed: 51 additions & 38 deletions

File tree

packages/spec/src/assembled-package-body.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,11 @@ describe("ADR-0130 D4 — `manifest: 'preserve'` assembles each input stack", ()
240240
'com.example.multi.orders',
241241
'com.example.multi.core',
242242
]);
243-
// Collections inside an assembled body are `unknown` at the TYPE level (see
244-
// the note above `AssembledPackageBodySchema` — the element-precise alias
245-
// OOM'd consumer type-checks); narrow at the point of use, as every reader
246-
// of an assembled body does.
243+
// An assembled body is `Record<string, unknown>` at the TYPE level (see the
244+
// note above `AssembledPackageBodySchema` — a named or element-precise
245+
// static type there leaked the whole stack declaration into every
246+
// consumer of `@objectstack/spec/system` and OOM'd their type-checks);
247+
// narrow at the point of use, as every reader of an assembled body does.
247248
const objectNames = (objects: unknown): string[] | undefined =>
248249
(objects as Array<{ name: string }> | undefined)?.map((o) => o.name);
249250
expect(objectNames(parsed[1].manifest.objects)).toEqual(['crm_account']);

packages/spec/src/stack-artifact-packages.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,10 @@ describe('ADR-0130 D4 — `packages` carries N manifests', () => {
202202

203203
expect(result.success).toBe(true);
204204
if (!result.success) return;
205-
expect(result.data.packages?.[0].manifest.engines?.protocol).toBe('>=18 <19');
205+
// An assembled body is `Record<string, unknown>` at the type level (see the
206+
// note above `AssembledPackageBodySchema`); narrow at the point of use.
207+
const body = result.data.packages?.[0].manifest as { engines?: { protocol?: string } } | undefined;
208+
expect(body?.engines?.protocol).toBe('>=18 <19');
206209
});
207210
});
208211

packages/spec/src/stack.zod.ts

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,18 +1026,9 @@ function assembledPackageBodyShape(): Pick<typeof STACK_DEFINITION_COLLECTIONS_S
10261026
* refused, loudly, at the seam that registers it — not a new unknown-key
10271027
* refusal on a manifest that has never had one.
10281028
*/
1029-
export type AssembledPackageBody =
1030-
Omit<z.input<typeof ManifestSchema>, AssembledPackageBodyKey>
1031-
& Partial<Record<AssembledPackageBodyKey, unknown>>;
1032-
1033-
/** Post-parse shape of {@link AssembledPackageBody} — defaults applied, transforms run (ADR-0122). */
1034-
export type AssembledPackageBodyParsed =
1035-
Omit<z.infer<typeof ManifestSchema>, AssembledPackageBodyKey>
1036-
& Partial<Record<AssembledPackageBodyKey, unknown>>;
1037-
10381029
/*
1039-
* ANNOTATED, not inferred, and the annotation is KEY-precise but
1040-
* ELEMENT-loose — both halves measured, neither a preference.
1030+
* ANNOTATED, not inferredand annotated with a STRUCTURAL type, not a named
1031+
* one. Both halves were measured on #14513, neither is a preference.
10411032
*
10421033
* Not inferred: inferring it emits the manifest and all ~35 collection
10431034
* declarations a SECOND time inside `ObjectStackDefinitionSchema`'s own
@@ -1046,33 +1037,51 @@ export type AssembledPackageBodyParsed =
10461037
* TS7056: The inferred type of this node exceeds the maximum length the
10471038
* compiler will serialize. An explicit type annotation is needed.
10481039
*
1049-
* Element-loose: the two aliases above name every collection KEY (the set is
1050-
* still derived from `COMPOSE_KEY_DISPOSITIONS`, see `AssembledPackageBodyKey`)
1051-
* but type each collection as `unknown` rather than mapping
1052-
* `z.input<(typeof STACK_DEFINITION_COLLECTIONS_SHAPE)[K]>` per key. The mapped
1053-
* form was tried first and measured on #14513: referencing the shape const from
1054-
* an EXPORTED type makes the declaration emit write `STACK_DEFINITION_COLLECTIONS_SHAPE`
1055-
* into `stack.zod-*.d.ts` a second time (21,443 lines beside the 42,449 the
1056-
* stack schema already occupies), and every consumer program then re-infers all
1057-
* ~35 collection input/output types once more — the `qa/http-conformance`
1058-
* type-check-debt re-measure went from green to out-of-memory at the 4096 MB
1059-
* ceiling `scripts/check-type-check-coverage.mjs` pins as CI's. With the keys
1060-
* held and the elements `unknown`, nothing here references the shape const, the
1061-
* second copy is not emitted, and the per-key inference does not run.
1062-
*
1063-
* What a consumer loses is only the STATIC element type of a collection inside
1064-
* an assembled body (`body.objects` is `unknown`, not `ObjectDefinition[]`); the
1065-
* RUNTIME schema below still carries every collection's full declaration, so a
1066-
* wrong-shaped body is refused exactly as before. Readers of assembled bodies
1067-
* (`compile.ts`, `artifact-packages.ts`) already treat them as records and
1068-
* narrow at the point of use; that is the honest shape for a payload whose
1069-
* collections are attributed per package only at composition time.
1040+
* Structural, not named: this schema is the element type of the stack
1041+
* schema's `packages` key, so whatever is written here is printed INSIDE
1042+
* `ObjectStackDefinitionSchema`'s declaration — and that declaration is
1043+
* embedded by every schema that carries a stack (`system/environment-artifact.zod.ts`
1044+
* among them). The declaration bundler inlines STRUCTURAL types into each chunk
1045+
* that embeds them, but a NAMED type alias can only be imported from the chunk
1046+
* that declares it. Two earlier shapes of this annotation named an alias
1047+
* (`z.ZodType<AssembledPackageBodyParsed, AssembledPackageBody>`), and the
1048+
* measured consequence was not local: `stack.zod` became a shared chunk that
1049+
* the `environment-artifact` chunk imports, so every consumer of
1050+
* `@objectstack/spec/system` started loading the whole stack schema
1051+
* declaration it never loaded before — `packages/qa/http-conformance`'s
1052+
* type-check program went from 691,580 to 734,202 lines of definitions
1053+
* (+42,622, the size of `ObjectStackDefinitionSchema`'s declaration), from
1054+
* 4.47 GB to 4.88 GB of heap, and past the 4096 MB ceiling
1055+
* `scripts/check-type-check-coverage.mjs` pins as CI's. A first attempt that
1056+
* ALSO referenced `typeof STACK_DEFINITION_COLLECTIONS_SHAPE` per key from the
1057+
* alias emitted the collections shape a second time on top (21,443 lines).
1058+
*
1059+
* `Record<string, unknown>` on both sides is therefore the whole of the static
1060+
* contract: an assembled body is an object. The two aliases below are derived
1061+
* FROM this annotation (ADR-0122), never the other way round, so no name can
1062+
* re-enter the stack schema's printed type. What a consumer loses is the
1063+
* static field typing inside an assembled body; the RUNTIME schema still
1064+
* carries the manifest's every field plus every collection's full declaration
1065+
* (the key set derived from `COMPOSE_KEY_DISPOSITIONS`, see
1066+
* `assembledPackageBodyShape`), so a wrong-shaped body is refused exactly as
1067+
* before. Readers of assembled bodies (`compile.ts`, `artifact-packages.ts`)
1068+
* already treat them as records and narrow at the point of use — the honest
1069+
* shape for a payload whose collections are attributed per package only at
1070+
* composition time.
10701071
*/
1071-
export const AssembledPackageBodySchema: z.ZodType<AssembledPackageBodyParsed, AssembledPackageBody> =
1072+
export const AssembledPackageBodySchema: z.ZodType<Record<string, unknown>, Record<string, unknown>> =
10721073
lazySchema(() =>
10731074
ManifestSchema.extend(assembledPackageBodyShape())
10741075
.describe('One package as assembled into a release artifact (ADR-0130 D4)'));
10751076

1077+
/**
1078+
* The assembled package body as authored — derived from the schema's
1079+
* annotation above, deliberately structural (see the note there).
1080+
*/
1081+
export type AssembledPackageBody = z.input<typeof AssembledPackageBodySchema>;
1082+
/** Post-parse shape of {@link AssembledPackageBody} — defaults applied, transforms run (ADR-0122). */
1083+
export type AssembledPackageBodyParsed = z.infer<typeof AssembledPackageBodySchema>;
1084+
10761085
/**
10771086
* One package carried by a release artifact, in its ASSEMBLED form — the
10781087
* element type of `packages` on {@link ObjectStackDefinitionSchema}.

0 commit comments

Comments
 (0)