Found while landing #17431. Filed rather than fixed: the remedy moves AssembledPackageBodySchema's own shape, which is #14242's ruling territory, and #17431 is bound to the read API one layer up.
The contract, verbatim
ADR-0130 D4's own docblock on ArtifactPackageEntrySchema (packages/spec/src/stack.zod.ts) states why plugins and devPlugins are envelope keys that no package body may carry:
An artifact is inert JSON: a plugin written inside packages[i].manifest could never be constructed by a loader, so a reader that resolved it there would register garbage where it used to skip in silence.
AssembledPackageBodySchema — the body half of ArtifactPackageSchema, i.e. one package as assembled into a release artifact — nevertheless carries two collections whose declarations admit values that same sentence says can never exist in an artifact:
functions, whose entry schema FlowFunctionEntrySchema (automation/flow-function.zod.ts) has z.function() as its first union branch;
hooks, whose HookSchema (data/hook.zod.ts) has a z.custom() branch.
A callable in an artifact is exactly the case the quoted sentence excludes: the artifact is JSON on disk, so the branch describes a value the surface cannot hold.
Measured consequence
Of the assembled body's 55 shape members, exactly those two have no JSON Schema form. Measured with z.toJSONSchema(member, { io: 'input' }) over every key of AssembledPackageBodySchema.shape:
functions: Function types cannot be represented in JSON Schema
hooks: Custom types cannot be represented in JSON Schema
and, on the whole body:
| probe |
result |
AssembledPackageBodySchema |
FAIL — Function types cannot be represented in JSON Schema |
.omit({ functions: true }) |
FAIL — Custom types cannot be represented in JSON Schema |
.omit({ hooks: true }) |
FAIL — Function types cannot be represented in JSON Schema |
.omit({ functions: true, hooks: true }) |
OK |
That is why ArtifactPackage and ObjectStackDefinition publish no JSON Schema at all, and it propagates: any published export that embeds the assembled body loses its own JSON Schema and its content/docs/references/** page with it. #17431 hit exactly that — binding the body into ListInstalledPackagesResponseSchema and GetInstalledPackageResponseSchema made both disappear from json-schema/api/, which build-schemas.ts's disappearance ratchet refuses.
So the cost is not local to the artifact schema. It is a standing tax on every future surface that wants to declare the assembled stage — and declaring the assembled stage is the ruled remedy for the #14242 class.
Why this is a contract violation and not a preference
The two branches are not merely unused here: the surface's own declared semantics say they are unreachable. A declaration that admits what its surface cannot hold is a claim nothing enforces, and here it has a measured price paid by unrelated schemas.
⛔ Not proposing the shape. Two obvious directions exist (narrow the two collections at the assembled body, or give artifact-stage variants of the two entry schemas) and both move a declaration #14242 ruled on, so the choice is the maintainer's.
Related: #14242 (the ruling that created AssembledPackageBodySchema) · #17431 (where this was measured) · #11072 (the sibling axis: the same tree also carries a Node-only import, which is a separate matter)
Generated by Claude Code
Found while landing #17431. Filed rather than fixed: the remedy moves
AssembledPackageBodySchema's own shape, which is #14242's ruling territory, and #17431 is bound to the read API one layer up.The contract, verbatim
ADR-0130 D4's own docblock on
ArtifactPackageEntrySchema(packages/spec/src/stack.zod.ts) states whypluginsanddevPluginsare envelope keys that no package body may carry:AssembledPackageBodySchema— the body half ofArtifactPackageSchema, i.e. one package as assembled into a release artifact — nevertheless carries two collections whose declarations admit values that same sentence says can never exist in an artifact:functions, whose entry schemaFlowFunctionEntrySchema(automation/flow-function.zod.ts) hasz.function()as its first union branch;hooks, whoseHookSchema(data/hook.zod.ts) has az.custom()branch.A callable in an artifact is exactly the case the quoted sentence excludes: the artifact is JSON on disk, so the branch describes a value the surface cannot hold.
Measured consequence
Of the assembled body's 55 shape members, exactly those two have no JSON Schema form. Measured with
z.toJSONSchema(member, { io: 'input' })over every key ofAssembledPackageBodySchema.shape:and, on the whole body:
AssembledPackageBodySchemaFunction types cannot be represented in JSON Schema.omit({ functions: true })Custom types cannot be represented in JSON Schema.omit({ hooks: true })Function types cannot be represented in JSON Schema.omit({ functions: true, hooks: true })That is why
ArtifactPackageandObjectStackDefinitionpublish no JSON Schema at all, and it propagates: any published export that embeds the assembled body loses its own JSON Schema and itscontent/docs/references/**page with it. #17431 hit exactly that — binding the body intoListInstalledPackagesResponseSchemaandGetInstalledPackageResponseSchemamade both disappear fromjson-schema/api/, whichbuild-schemas.ts's disappearance ratchet refuses.So the cost is not local to the artifact schema. It is a standing tax on every future surface that wants to declare the assembled stage — and declaring the assembled stage is the ruled remedy for the #14242 class.
Why this is a contract violation and not a preference
The two branches are not merely unused here: the surface's own declared semantics say they are unreachable. A declaration that admits what its surface cannot hold is a claim nothing enforces, and here it has a measured price paid by unrelated schemas.
⛔ Not proposing the shape. Two obvious directions exist (narrow the two collections at the assembled body, or give artifact-stage variants of the two entry schemas) and both move a declaration #14242 ruled on, so the choice is the maintainer's.
Related: #14242 (the ruling that created
AssembledPackageBodySchema) · #17431 (where this was measured) · #11072 (the sibling axis: the same tree also carries a Node-only import, which is a separate matter)Generated by Claude Code