Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/manifest-open-object-prose-sweep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@objectstack/spec": patch
"@objectstack/core": patch
---

Documentation: the manifest surface no longer describes itself as an open object.

`ManifestSchema` became a `strictObject` when the manifest surface was closed against unknown keys, but five prose sites still described the earlier posture. They shipped, so an author (or an AI writing metadata) reading the declarations was told the manifest tolerates undeclared keys — while the runtime rejects them by name and offers the declared spelling for a near miss. Prose that contradicts a tightened contract teaches exactly the wrong reflex, so each site now states the current refusal rather than merely dropping the old claim:

- `AssembledPackageBodySchema`'s docblock no longer explains its lack of a `strictObject` spelling by calling `ManifestSchema` open. The posture is inherited: the schema is `ManifestSchema.extend(...)`, and `.extend()` carries the base's unknown-key handling, so an undeclared key on an assembled body is refused — measured, with the rename suggestion intact.
- The artifact-registration seam kept the half of its reasoning that still holds (the schema applies defaults, so a parsed clone would not be byte-identical) and retired the half that does not ("Zod strips undeclared keys") — the key is now refused at that parse rather than dropped from the clone.
- The `os compile` per-package rule pass explains why a body may be re-read as its own manifest: nothing parses that superset, and against `ManifestSchema` it would now be refused.

No schema, behaviour or export changed; `check:api-surface` and the generated reference pages are unmoved.
7 changes: 5 additions & 2 deletions content/docs/protocol/kernel/plugin-spec.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,11 @@ required; the optional fields are `namespace`, `defaultDatasource`, `scope`,
`loading` keys are **retired tombstones** (ADR-0049 enforce-or-remove): nothing ever
read any of them, so authoring one is now a compile error and a parse error carrying
the migration prescription. Any other key is refused at parse — the schema is closed
against unknown keys, at the manifest root and inside `contributes`, `engine` and
`engines` — with a message naming the key and, for a near miss, the declared spelling.
against unknown keys at the manifest root, inside `contributes`, inside each entry of
`contributes.kinds[]`, and inside `engine` and `engines` — with a message naming the
key and, for a near miss, the declared spelling. The closure is inherited, not just
declared: `AssembledPackageBodySchema` extends this schema, so an assembled artifact
body is refused on an undeclared key too.

The `displayName` / `author` / `license` / `homepage` / `optionalDependencies` /
`peerDependencies` / `metadata` / `config` / `marketplace` keys above are
Expand Down
8 changes: 7 additions & 1 deletion docs/audits/2026-07-unknown-key-strictness-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -1615,7 +1615,13 @@ carefully was not tightening it.**
Whole-spec context (fourteen directories, out of the ruling's forced scope, same
instrument): 1940 → 1722 sites, strict 15 → 361, strip 1903 → 1338. The untriaged
directories are coarse-classified in the section above and are not this campaign's
debt; `api/`, `system/`, `kernel/` and `cloud/` are wire surface by construction.
debt; of them only `cloud/` is wire surface by construction. `api/`, `system/` and
`kernel/` all read **mixed** in that table, each with an authored file named in its
own row — `endpoint.zod.ts` (split at #5384), the manifest/datasource blocks, and
`manifest.zod.ts` (split at #14192, `strictObject` since). ⛔ Do not read this
paragraph as a blanket wire verdict over those three: the rows above are the
finding, and twice already a flat `wire` cell was correct when written and then
silently expired.

### The three closing claims, confirmed

Expand Down
12 changes: 10 additions & 2 deletions packages/cli/src/commands/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,16 @@ function artifactPackages(parsed: Record<string, unknown>): Array<{
* "manifest fields" and "collections" would need a second copy of the key set
* `AssembledPackageBodySchema` derives, and the rules do not need the split —
* they read collections off the top level (already there) and identity off
* `manifest` (a superset of the manifest, and `ManifestSchema` is an open
* object).
* `manifest`.
*
* That `manifest` is a SUPERSET of a real manifest — it is the whole body — and
* that is safe here for one reason only: nothing parses it. `runAuthoringRules`
* reads fields off this object and never hands it to a schema. ⛔ Do not start
* parsing it against `ManifestSchema`, and do not reach for a widened schema to
* make that possible: `ManifestSchema` is `strictObject` since #14192, so it
* would REFUSE, by name, every collection key this superset deliberately puts
* under `manifest` — and re-opening it to stop the refusal would re-open the
* real manifest surface with it.
*/
function packageBodyAsStack(body: Record<string, unknown>): Record<string, unknown> {
return { ...body, manifest: body };
Expand Down
15 changes: 11 additions & 4 deletions packages/core/src/artifact-packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,17 @@
* ⛔ The body handed to `registerApp` is still the caller's original
* `entry.manifest`, never `verdict.data.manifest`. The parse is a GATE, and the
* reason is unchanged by road B: `ManifestSchema` carries defaults
* (`defaultDatasource: 'default'`, `scope: 'project'`) and Zod strips
* undeclared keys, so registering a parsed clone would put different bytes into
* the registry than the singular-`manifest` branch does for the same authored
* package. D7 pins that those two branches do not disagree.
* (`defaultDatasource: 'default'`, `scope: 'project'`), so registering a parsed
* clone would put different bytes into the registry than the singular-`manifest`
* branch does for the same authored package. D7 pins that those two branches do
* not disagree.
*
* ⛔ The other half of that reason — "and Zod strips undeclared keys" — is GONE,
* not merely reworded. `ManifestSchema` is `strictObject` since #14192 and
* `AssembledPackageBodySchema` inherits the closed posture through `.extend()`,
* so an undeclared key on an entry is REFUSED by this very parse, by name, and
* never reaches a clone to be dropped from. Defaults are what still move bytes;
* the drop that used to is now a loud rejection.
*
* ## Ordering reuses the ONE sorter (D5)
*
Expand Down
19 changes: 14 additions & 5 deletions packages/spec/src/stack.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1141,11 +1141,20 @@ function assembledPackageBodyShape(): Pick<typeof STACK_DEFINITION_COLLECTIONS_S
* assembled stage overrides therefore has no expression in an assembled body —
* write it in the package's own stack, where the collection form is read.
*
* NOT `strictObject`: `ManifestSchema` is an open object, and this schema is
* that surface plus collections rather than a new door. The gate it enables is
* the one #14242 asked for — a body whose collections are the wrong SHAPE is
* refused, loudly, at the seam that registers it — not a new unknown-key
* refusal on a manifest that has never had one.
* NO `strictObject` spelling appears here, and none is needed. This schema is
* `ManifestSchema.extend(...)`, and `.extend()` carries the base's unknown-key
* posture: #14192 closed `ManifestSchema`, so an assembled body is closed too,
* BY INHERITANCE — an undeclared key on one is REFUSED, by name and with the
* declared spelling offered for a near miss. ⛔ Do not read the absence of the
* `strictObject` spelling as a declined posture; it is an inherited one.
*
* Two gates therefore land at the seam that registers a body: that inherited
* unknown-key refusal, and the collection-SHAPE gate #14242 asked for — a body
* whose collections are the wrong shape (globs where definitions belong) is
* refused, loudly, there rather than registered and silently owning nothing.
* ⛔ Never re-open this surface with `.loose()` or a `.catchall()` to make an
* assembled body tolerant: that would leave this declaration the one door in
* the chain accepting what the manifest it extends refuses.
*/
/*
* ANNOTATED, not inferred — and annotated with a STRUCTURAL type, not a named
Expand Down
Loading