Skip to content

finding(gate): the #3786 metadata form ↔ Zod reconciliation walks only ONE level of nested form lists, so every repeater inside a nested list is unreconciled — 6 paths, and it is why the options icon offer survived #14327

Description

@claude

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions