Skip to content

Commit 3ac024a

Browse files
huangyiireneclaude
andauthored
docs: the manifest surface no longer describes itself as an open object (#16327)
* docs(spec): retire the "ManifestSchema is an open object" claim across five sites #14192 closed ManifestSchema with strictObject. Five prose sites still taught the old open-object posture; each is rewritten to teach the current refusal rather than merely to stop teaching the old permission. Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno Co-authored-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(changeset): patch for the manifest open-object prose sweep Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno Co-authored-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4998efa commit 3ac024a

6 files changed

Lines changed: 61 additions & 14 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/core": patch
4+
---
5+
6+
Documentation: the manifest surface no longer describes itself as an open object.
7+
8+
`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:
9+
10+
- `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.
11+
- 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.
12+
- 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.
13+
14+
No schema, behaviour or export changed; `check:api-surface` and the generated reference pages are unmoved.

content/docs/protocol/kernel/plugin-spec.mdx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,11 @@ required; the optional fields are `namespace`, `defaultDatasource`, `scope`,
172172
`loading` keys are **retired tombstones** (ADR-0049 enforce-or-remove): nothing ever
173173
read any of them, so authoring one is now a compile error and a parse error carrying
174174
the migration prescription. Any other key is refused at parse — the schema is closed
175-
against unknown keys, at the manifest root and inside `contributes`, `engine` and
176-
`engines` — with a message naming the key and, for a near miss, the declared spelling.
175+
against unknown keys at the manifest root, inside `contributes`, inside each entry of
176+
`contributes.kinds[]`, and inside `engine` and `engines` — with a message naming the
177+
key and, for a near miss, the declared spelling. The closure is inherited, not just
178+
declared: `AssembledPackageBodySchema` extends this schema, so an assembled artifact
179+
body is refused on an undeclared key too.
177180

178181
The `displayName` / `author` / `license` / `homepage` / `optionalDependencies` /
179182
`peerDependencies` / `metadata` / `config` / `marketplace` keys above are

docs/audits/2026-07-unknown-key-strictness-ledger.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1615,7 +1615,13 @@ carefully was not tightening it.**
16151615
Whole-spec context (fourteen directories, out of the ruling's forced scope, same
16161616
instrument): 1940 → 1722 sites, strict 15 → 361, strip 1903 → 1338. The untriaged
16171617
directories are coarse-classified in the section above and are not this campaign's
1618-
debt; `api/`, `system/`, `kernel/` and `cloud/` are wire surface by construction.
1618+
debt; of them only `cloud/` is wire surface by construction. `api/`, `system/` and
1619+
`kernel/` all read **mixed** in that table, each with an authored file named in its
1620+
own row — `endpoint.zod.ts` (split at #5384), the manifest/datasource blocks, and
1621+
`manifest.zod.ts` (split at #14192, `strictObject` since). ⛔ Do not read this
1622+
paragraph as a blanket wire verdict over those three: the rows above are the
1623+
finding, and twice already a flat `wire` cell was correct when written and then
1624+
silently expired.
16191625

16201626
### The three closing claims, confirmed
16211627

packages/cli/src/commands/compile.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,16 @@ function artifactPackages(parsed: Record<string, unknown>): Array<{
8585
* "manifest fields" and "collections" would need a second copy of the key set
8686
* `AssembledPackageBodySchema` derives, and the rules do not need the split —
8787
* they read collections off the top level (already there) and identity off
88-
* `manifest` (a superset of the manifest, and `ManifestSchema` is an open
89-
* object).
88+
* `manifest`.
89+
*
90+
* That `manifest` is a SUPERSET of a real manifest — it is the whole body — and
91+
* that is safe here for one reason only: nothing parses it. `runAuthoringRules`
92+
* reads fields off this object and never hands it to a schema. ⛔ Do not start
93+
* parsing it against `ManifestSchema`, and do not reach for a widened schema to
94+
* make that possible: `ManifestSchema` is `strictObject` since #14192, so it
95+
* would REFUSE, by name, every collection key this superset deliberately puts
96+
* under `manifest` — and re-opening it to stop the refusal would re-open the
97+
* real manifest surface with it.
9098
*/
9199
function packageBodyAsStack(body: Record<string, unknown>): Record<string, unknown> {
92100
return { ...body, manifest: body };

packages/core/src/artifact-packages.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,17 @@
7777
* ⛔ The body handed to `registerApp` is still the caller's original
7878
* `entry.manifest`, never `verdict.data.manifest`. The parse is a GATE, and the
7979
* reason is unchanged by road B: `ManifestSchema` carries defaults
80-
* (`defaultDatasource: 'default'`, `scope: 'project'`) and Zod strips
81-
* undeclared keys, so registering a parsed clone would put different bytes into
82-
* the registry than the singular-`manifest` branch does for the same authored
83-
* package. D7 pins that those two branches do not disagree.
80+
* (`defaultDatasource: 'default'`, `scope: 'project'`), so registering a parsed
81+
* clone would put different bytes into the registry than the singular-`manifest`
82+
* branch does for the same authored package. D7 pins that those two branches do
83+
* not disagree.
84+
*
85+
* ⛔ The other half of that reason — "and Zod strips undeclared keys" — is GONE,
86+
* not merely reworded. `ManifestSchema` is `strictObject` since #14192 and
87+
* `AssembledPackageBodySchema` inherits the closed posture through `.extend()`,
88+
* so an undeclared key on an entry is REFUSED by this very parse, by name, and
89+
* never reaches a clone to be dropped from. Defaults are what still move bytes;
90+
* the drop that used to is now a loud rejection.
8491
*
8592
* ## Ordering reuses the ONE sorter (D5)
8693
*

packages/spec/src/stack.zod.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,11 +1141,20 @@ function assembledPackageBodyShape(): Pick<typeof STACK_DEFINITION_COLLECTIONS_S
11411141
* assembled stage overrides therefore has no expression in an assembled body —
11421142
* write it in the package's own stack, where the collection form is read.
11431143
*
1144-
* NOT `strictObject`: `ManifestSchema` is an open object, and this schema is
1145-
* that surface plus collections rather than a new door. The gate it enables is
1146-
* the one #14242 asked for — a body whose collections are the wrong SHAPE is
1147-
* refused, loudly, at the seam that registers it — not a new unknown-key
1148-
* refusal on a manifest that has never had one.
1144+
* NO `strictObject` spelling appears here, and none is needed. This schema is
1145+
* `ManifestSchema.extend(...)`, and `.extend()` carries the base's unknown-key
1146+
* posture: #14192 closed `ManifestSchema`, so an assembled body is closed too,
1147+
* BY INHERITANCE — an undeclared key on one is REFUSED, by name and with the
1148+
* declared spelling offered for a near miss. ⛔ Do not read the absence of the
1149+
* `strictObject` spelling as a declined posture; it is an inherited one.
1150+
*
1151+
* Two gates therefore land at the seam that registers a body: that inherited
1152+
* unknown-key refusal, and the collection-SHAPE gate #14242 asked for — a body
1153+
* whose collections are the wrong shape (globs where definitions belong) is
1154+
* refused, loudly, there rather than registered and silently owning nothing.
1155+
* ⛔ Never re-open this surface with `.loose()` or a `.catchall()` to make an
1156+
* assembled body tolerant: that would leave this declaration the one door in
1157+
* the chain accepting what the manifest it extends refuses.
11491158
*/
11501159
/*
11511160
* ANNOTATED, not inferred — and annotated with a STRUCTURAL type, not a named

0 commit comments

Comments
 (0)