Found while implementing #13671 (the object.form options repeater offering an icon input SelectOptionSchema refuses). Filed unassigned and NOT fixed there — it is the structural cause of that card rather than that card's scope.
The gate exists, and it could not see the defect
packages/spec/src/system/metadata-form-zod-reconciliation.test.ts (#3786) is the reconciliation between every defineForm layout in METADATA_FORM_REGISTRY and the Zod schema it names keys of. Its own header states the rule that matters here, verbatim:
form-only (the form offers a key the Zod does not accept) is always a defect. There is no design under which an author should be shown a control whose value is discarded. Not ledgerable.
#13671 is precisely that shape — the options repeater offered icon, SelectOptionSchema is strict and never declared it, so every value typed there was an unrecognized_keys refusal at publish. The gate was green throughout.
Why: the traversal is one level deep
nestedLists(form) walks form.sections[].fields[] and collects a nested list only for a top-level entry that carries its own fields array. It never descends into entry.fields[*].fields, so a repeater or composite nested inside another nested list is not reconciled against anything — it is not "reconciled loosely", it is outside the population entirely.
object.form.ts is exactly that shape: the fields record editor is the top-level nested list the gate reconciles, and the options repeater lives one level further in.
Measured
Mirroring the gate's own traversal against a full-depth one over METADATA_FORM_REGISTRY (17 registered forms) gives 6 nested lists at depth 2 or deeper that the gate never reaches, all in object:
| unreconciled path |
inputs it offers |
schema that should judge them |
fields.options |
label, value, color, description |
SelectOptionSchema |
fields.summaryOperations |
object, field, function |
the summary-operation shape on FieldSchema |
lifecycle.retention |
maxAge |
the retention shape on ObjectSchema |
lifecycle.ttl |
field, expireAfter |
the ttl shape |
lifecycle.storage |
strategy, shards, unit |
the storage shape |
lifecycle.archive |
after, to, keep |
the archive shape |
(fields.options is listed post-#13671, i.e. already with icon removed by hand. The other five have never been checked by anything.)
Why it is worth closing as a class
The offer-vs-door class has now been retired by hand three times — #11410 (deleteBehavior offering a value a master_detail refuses), #12868 (the form-face option narrowing) and #13671 (options[].icon) — each time by a person or an agent noticing, never by a gate. The gate that exists to abolish hand-copied key lists is itself blind at depth 2, which is where the repeater lists live. The five unchecked paths above are the current exposure.
Shape of the work
Make nestedLists recursive, keyed by a dotted path, and resolve each level's sub-schema by walking subSchemaOf down the same path (the helper already looks through unions and unwrap already peels arrays and records, which is what a repeater's element needs). The existing LEDGER is already path-keyed, so the subset / omit vocabulary carries over unchanged; expect the five never-checked paths to need triage on first run, and expect at least one of them to be a real finding rather than a ledger entry — that is the point.
Not urgent enough to ride #13671's PR: that one is a ruled single-key removal with Clause-②: no, and widening it into a gate change would put an unrelated failure surface in the same diff.
Refs: #13671 (the instance) · #3786 (the gate) · #5280 (the previous time this gate's predicate was found too narrow, from the other direction — tombstoned keys reading as accepted) · #11410 · #12868.
Generated by Claude Code
Generated by Claude Code
Found while implementing #13671 (the
object.formoptions repeater offering aniconinputSelectOptionSchemarefuses). Filed unassigned and NOT fixed there — it is the structural cause of that card rather than that card's scope.The gate exists, and it could not see the defect
packages/spec/src/system/metadata-form-zod-reconciliation.test.ts(#3786) is the reconciliation between everydefineFormlayout inMETADATA_FORM_REGISTRYand the Zod schema it names keys of. Its own header states the rule that matters here, verbatim:#13671 is precisely that shape — the options repeater offered
icon,SelectOptionSchemais strict and never declared it, so every value typed there was anunrecognized_keysrefusal at publish. The gate was green throughout.Why: the traversal is one level deep
nestedLists(form)walksform.sections[].fields[]and collects a nested list only for a top-level entry that carries its ownfieldsarray. It never descends intoentry.fields[*].fields, so a repeater or composite nested inside another nested list is not reconciled against anything — it is not "reconciled loosely", it is outside the population entirely.object.form.tsis exactly that shape: thefieldsrecord editor is the top-level nested list the gate reconciles, and theoptionsrepeater lives one level further in.Measured
Mirroring the gate's own traversal against a full-depth one over
METADATA_FORM_REGISTRY(17 registered forms) gives 6 nested lists at depth 2 or deeper that the gate never reaches, all inobject:fields.optionsSelectOptionSchemafields.summaryOperationsFieldSchemalifecycle.retentionObjectSchemalifecycle.ttllifecycle.storagelifecycle.archive(
fields.optionsis listed post-#13671, i.e. already withiconremoved by hand. The other five have never been checked by anything.)Why it is worth closing as a class
The offer-vs-door class has now been retired by hand three times — #11410 (
deleteBehavioroffering a value amaster_detailrefuses), #12868 (the form-face option narrowing) and #13671 (options[].icon) — each time by a person or an agent noticing, never by a gate. The gate that exists to abolish hand-copied key lists is itself blind at depth 2, which is where the repeater lists live. The five unchecked paths above are the current exposure.Shape of the work
Make
nestedListsrecursive, keyed by a dotted path, and resolve each level's sub-schema by walkingsubSchemaOfdown the same path (the helper already looks through unions andunwrapalready peels arrays and records, which is what a repeater's element needs). The existingLEDGERis already path-keyed, so thesubset/omitvocabulary carries over unchanged; expect the five never-checked paths to need triage on first run, and expect at least one of them to be a real finding rather than a ledger entry — that is the point.Not urgent enough to ride #13671's PR: that one is a ruled single-key removal with
Clause-②: no, and widening it into a gate change would put an unrelated failure surface in the same diff.Refs: #13671 (the instance) · #3786 (the gate) · #5280 (the previous time this gate's predicate was found too narrow, from the other direction — tombstoned keys reading as accepted) · #11410 · #12868.
Generated by Claude Code
Generated by Claude Code