Found while landing #16781 (deliverable 2). Filed rather than fixed: the remedy is a packages/spec declaration, and #16781 is explicitly routed away from packages/spec.
The contract, verbatim
ListInstalledPackagesResponseSchema (packages/spec/src/api/package-api.zod.ts) types every row of data.packages as InstalledPackageSchema, whose manifest is ManifestSchema. ManifestSchema declares:
objects: z.array(z.string()).optional().describe('Glob patterns for ObjectQL schemas files'),
— i.e. the authoring stage, where manifest.objects names file patterns. ManifestSchema is additionally a strictObject.
What the door actually serves
SchemaRegistry.installPackage records what it is handed, and ObjectQL.registerApp is handed manifest.objects as object definitions — it iterates them and calls registerObject(objDef, …). That is the assembled body, and it is what a defineStack() host produces (examples/app-showcase/objectstack.config.ts: objects: [...Object.values(objects), ExternalCustomer, ExternalOrder]). GET /packages projects those rows through toPackageResponse and serves them.
Measured
Driven against the real SchemaRegistry + the real dispatcher door, after #16781 added the missing hasMore:
| package authored as |
ListInstalledPackagesResponseSchema.safeParse(body) |
glob patterns (objects: ['./src/objects/*.object.yml']) |
success |
defineStack() shape (objects: [{ name, fields }]) |
failure, one issue: data.packages.0.manifest.objects.0 — expected string, received object |
So the shipped open-core path serves a payload its own declared response schema refuses, and the single surviving reason is the manifest stage.
Why this is the same thing as #14242, not a new class
#14242 identified exactly this authoring-vs-assembled split one layer down, and the ruling is quoted in packages/spec/src/stack.zod.ts at ArtifactPackageSchema:
This key is read at LOAD time, so the authoring-time ArtifactPackageEntrySchema (whose manifest.objects are glob patterns) cannot describe it: that mismatch was #14242, and the maintainer's decision (2026-09-02) was to declare the assembled stage rather than widen the authoring one.
#14242 declared the assembled stage for the artifact load path. The read API never got the same treatment: InstalledPackageSchema still wraps the authoring manifest, so the API contract inherits the mismatch.
Consequence today
packages/runtime/src/route-ledger.ts's GET /packages row is left with no responseSchema by #16781, deliberately, and the blank is a measured verdict. The ledger header forbids filling a row without conformance coverage, and a name there would promise conformance the door keeps only for glob-authored packages. The boundary is pinned in both directions in packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts:
- the glob row parses end to end;
- the assembled row does not, and the test asserts the surviving issue list is exactly
['data.packages.0.manifest.objects.0'].
⇒ Whoever declares the assembled stage for this surface gets a red test telling them the ledger row has become fillable. That is the intended pickup path.
Suggested shape (not a ruling)
Follow #14242's ruling one layer up: an assembled-stage counterpart of InstalledPackageSchema in @objectstack/spec/api whose manifest is the assembled body, bound to the GET /packages and GET /packages/:id response declarations. ⛔ Not widening ManifestSchema — that is the option #14242 already rejected.
Related: #14242 (the ruling) · #16781 (where this was measured) · #16628 · #3877 (the responseSchema programme).
Found while landing #16781 (deliverable 2). Filed rather than fixed: the remedy is a
packages/specdeclaration, and #16781 is explicitly routed away frompackages/spec.The contract, verbatim
ListInstalledPackagesResponseSchema(packages/spec/src/api/package-api.zod.ts) types every row ofdata.packagesasInstalledPackageSchema, whosemanifestisManifestSchema.ManifestSchemadeclares:— i.e. the authoring stage, where
manifest.objectsnames file patterns.ManifestSchemais additionally astrictObject.What the door actually serves
SchemaRegistry.installPackagerecords what it is handed, andObjectQL.registerAppis handedmanifest.objectsas object definitions — it iterates them and callsregisterObject(objDef, …). That is the assembled body, and it is what adefineStack()host produces (examples/app-showcase/objectstack.config.ts:objects: [...Object.values(objects), ExternalCustomer, ExternalOrder]).GET /packagesprojects those rows throughtoPackageResponseand serves them.Measured
Driven against the real
SchemaRegistry+ the real dispatcher door, after #16781 added the missinghasMore:ListInstalledPackagesResponseSchema.safeParse(body)objects: ['./src/objects/*.object.yml'])defineStack()shape (objects: [{ name, fields }])data.packages.0.manifest.objects.0—expected string, received objectSo the shipped open-core path serves a payload its own declared response schema refuses, and the single surviving reason is the manifest stage.
Why this is the same thing as #14242, not a new class
#14242 identified exactly this authoring-vs-assembled split one layer down, and the ruling is quoted in
packages/spec/src/stack.zod.tsatArtifactPackageSchema:#14242 declared the assembled stage for the artifact load path. The read API never got the same treatment:
InstalledPackageSchemastill wraps the authoring manifest, so the API contract inherits the mismatch.Consequence today
packages/runtime/src/route-ledger.ts'sGET /packagesrow is left with noresponseSchemaby #16781, deliberately, and the blank is a measured verdict. The ledger header forbids filling a row without conformance coverage, and a name there would promise conformance the door keeps only for glob-authored packages. The boundary is pinned in both directions inpackages/runtime/src/domains/packages-read-delete-response-conformance.test.ts:['data.packages.0.manifest.objects.0'].⇒ Whoever declares the assembled stage for this surface gets a red test telling them the ledger row has become fillable. That is the intended pickup path.
Suggested shape (not a ruling)
Follow #14242's ruling one layer up: an assembled-stage counterpart of
InstalledPackageSchemain@objectstack/spec/apiwhosemanifestis the assembled body, bound to theGET /packagesandGET /packages/:idresponse declarations. ⛔ Not wideningManifestSchema— that is the option #14242 already rejected.Related: #14242 (the ruling) · #16781 (where this was measured) · #16628 · #3877 (the
responseSchemaprogramme).