You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(spec): strictObject, the first registered-type conversions, and the check that found two live 422s (#4001) (#4514)
Three things, in the order they forced each other.
1. `strictObject` — closing a shape is one call.
The #4001 wiring was four parts per schema plus a drift test: a hand-transcribed
`const X_KEYS = [...]`, a `strictUnknownKeyError({ knownKeys: X_KEYS, … })` call,
the `{ error }` argument, `.strict()`, and an "accepts every declared key" probe
to catch the array drifting from the shape it describes. 34 key arrays and 16
probe files, with most of the authorable surface still ahead.
The array was never necessary. `knownKeys` feeds one thing — the edit-distance
fallback — and the shape object is at the call site. `strictObject` reads the
keys from `shape`, which also retires the per-schema probe: a list read from the
shape cannot disagree with it. `aliases` and `guidance` stay hand-written and
stay OPTIONAL; they carry judgement rather than transcription, and treating
curation as a precondition is part of why this ratchet moved slowly.
2. A sharper target than "the five authorable directories".
That triage answers "is this authorable?" but not "is this PARSED?" — and after
#4410 the second question decides whether a flip enforces anything at all.
`BUILTIN_METADATA_TYPE_SCHEMAS` answers both: every entry is author-written and
parsed on three paths (`defineStack()`, `/api/v1/meta/types/:type`, the Studio
form). Ten had no strictness; `seed` and `doc` are the first two converted. Five
of the ten live in `system/`, which the directory triage never covered — the two
lenses miss different things, so the ledger now carries both.
3. The check, and the two live bugs it found immediately.
`MetadataPlugin`'s loader stamps `_packageId` / `_provenance` on every
registered type, so a strict schema that does not declare
`MetadataProtectionFields` rejects its own loader's output — a hard 422 on the
ADR-0094 overlay path. That defect had been found three times by hand
(`permission`, `position`, then `seed`/`doc` here). Finding one thing three
times is evidence the check is missing, so this adds it as an invariant over the
registry.
It found the fourth and fifth on its first run: `hook` and `datasource` had both
gone strict in the #4001 data step WITHOUT the envelope, and were sitting on
`main` in the hard-422 class. Three prior hand-searches for exactly this defect
had walked past them. Both now declare it; no registered type rejects the
envelope any more. The test asserts that case unconditionally — no exemption
list — and tracks the quieter strip case (`field` only) separately, since each
entry there becomes a rejection the day its schema closes.
Also: the ledger gate caught its own blind spot again. `strictObject(` did not
match its `z.object(` counting method, so the first conversion read as a site
disappearing. Counting only `z.object(` would make "this directory got solved"
and "this directory got deleted" produce the same number, so the method now
counts both.
Authoring impact on `seed` / `doc`: a key the schema never declared is rejected
instead of silently discarded — it was already ignored, so no working behavior
changes. Rejections name the surface, echo the key and suggest the closest
declared one (`rows` → `records`, `body` → `content`), with tombstones for
`path` / `slug` on `doc`. Published JSON Schema unchanged: output-mode conversion
already emitted `additionalProperties: false` for these shapes.
`validation` is the remaining registered type with the envelope gap — a
`z.lazy()` discriminated union whose variants `.extend()` a shared base, so it
needs per-variant conversion rather than one call. Tracked in the test's debt
list and the ledger.
Verified: spec 282 files / 7115 tests, `tsc --noEmit` clean, all 8 generated
artifacts current, all 15 `check:*` gates green. Example-app seed definitions and
doc frontmatter checked directly against the new shapes — only declared keys.
Claude-Session: https://claude.ai/code/session_01WnqGjQFQMqd5k81LYV8SCY
Co-authored-by: Claude <noreply@anthropic.com>
`strictObject` makes closing an authoring shape one call; `seed` and `doc` are the first two registered metadata types converted with it; and a new invariant test found two live protection-envelope bugs on `hook` and `datasource`.
6
+
7
+
**The helper.** The #4001 wiring was four parts per schema plus a drift test: a hand-transcribed `const X_KEYS = [...]` array, a `strictUnknownKeyError({ knownKeys: X_KEYS, … })` call, the `{ error }` argument, `.strict()`, and an "accepts every declared key" probe to catch the array drifting from the shape it describes. That was 34 key arrays and 16 probe files with most of the authorable surface still ahead — and the array was never necessary: `knownKeys` feeds only the edit-distance suggestion, and the shape object is right there at the call site. `strictObject({ surface, history, aliases?, guidance? }, shape)` derives it, which also removes the per-schema drift probe: a key list read from the shape cannot disagree with it. `aliases` and `guidance` stay hand-written and stay **optional** — they carry judgement rather than transcription, and treating curation as a precondition is part of why the ratchet moved slowly.
8
+
9
+
**A sharper targeting rule.** The five-directory triage answers "is this authorable?" but not "is this parsed?" — and after #4410 that second question decides whether a flip enforces anything. `BUILTIN_METADATA_TYPE_SCHEMAS` answers both: every entry is author-written and parsed on three paths (`defineStack()`, `/api/v1/meta/types/:type`, the Studio form). Ten had no strictness at all; `seed` and `doc` are the first two converted. Five of the ten live in `system/`, which the directory triage never covered — the two lenses miss different things.
10
+
11
+
**Two live bugs, found by a check rather than by reading.**`MetadataPlugin`'s artifact loader stamps `_packageId` / `_provenance` on every registered type, so a strict schema that does not declare `MetadataProtectionFields` rejects its own loader's output — a hard 422 on the ADR-0094 overlay path. That defect had been found three times by hand (`permission`, `position`, then `seed`/`doc`). A new invariant test over the registered-type registry found it twice more on its first run: **`hook` and `datasource` had both gone strict in the #4001 data step without the envelope.** Both now declare it. The test asserts the hard case (rejects) unconditionally with no exemption list, and tracks the quieter case (silently strips, currently only `field`) separately.
12
+
13
+
**Ledger.**`strictObject` replaces the old wiring recipe as the standard, and the gate's site-counting method now counts `strictObject(` alongside `z.object(` — counting only the latter would have made every conversion look like surface disappearing, so "solved" and "deleted" would read the same. The gate caught that itself on the first conversion.
14
+
15
+
Authoring impact: on `seed` and `doc`, a key the schema never declared is now rejected instead of silently discarded — it was already being ignored, so no working behavior changes. The rejection names the surface, echoes the key and suggests the closest declared one (`rows` → `records`, `body` → `content`), with tombstones for `path` / `slug` on `doc`. The published JSON Schema is unchanged: `build-schemas.ts` converts with `io: 'output'`, which already emitted `additionalProperties: false` for these shapes. `validation` is the remaining registered type with a known envelope gap; it is a `z.lazy()` discriminated union whose variants `.extend()` a shared base, so it needs per-variant conversion rather than one call.
0 commit comments