Skip to content

console: registry-inputs-spec-parity's refusesKind reads a union branch's [invalid_type, too_small] pair as a CONTENT refusal, so an array probe the contract refuses enters the accepted-kind set #8204

Description

@os-sam

⚠️ CORRECTION — read before citing this card

Suspected mechanism 1 below ("refusesKind does not recurse into z.union") is FALSE. It does recurse; the invalid_union arm runs and carries branch verdicts up. Suspected mechanism 2 ("acceptedMemberKinds folds refuses-content into accepted") is INTENDED and load-bearing, not a defect.

The real defect is one level in: zod keeps running a schema's checks after its type check fails, so z.string().min(1) answers the probe [] with both invalid_type and too_small, and refusesKind's every read that pair as a CONTENT refusal — recording an array the contract refused for being an array as an accepted kind.

Measured and fixed in PR #8210; full account in comment 5563643927. The title was amended; the original body is preserved unchanged below, because what the filing seat got wrong is part of the record — I inferred a mechanism from a symptom and wrote it in as this card's headline.


Filed by the domain:spec @ objectui execution seat (session session_0114Ytxr5sM1vdW19Y9WAx6E), R1. Split out of #8137.

Why this is a separate card

#8137 is a decision card carrying two failures of the same test. They are independent, and only one of them is a decision:

failure needs
1 a calibration control asserts 'Account' is refuses-content; 17.3.0 genuinely accepts it ⛔ human ruling — editing a control is on the manual floor. Stays on #8137.
2 the kind classifier reports contract accepts {string, array} for a member whose array form is measurably refused ordinary gate bug — this card

Sequencing them behind one ruling was my error; (2) does not depend on (1). This card is (2) alone.

The defect

apps/console/src/__tests__/registry-inputs-spec-parity.test.ts (and whatever helper module refusesKind / acceptedMemberKinds live in).

@objectstack/spec 17.3.0 declares:

RecordActivityProps.types = z.array(z.union([FeedItemType, z.string().min(1)]))

Measured against the installed artifact via safeParse (not inferred):

['comment'] → ACCEPTS        [42] → refuses: invalid_union @ [0]
['Account'] → ACCEPTS        [[]] → refuses: invalid_union @ [0]

The gate nonetheless reports record:activity.types as contract accepts {string, array} and fails the single-kind direction. Arrays are refused; the gate says they are accepted.

Two mechanisms are suspected, both to be confirmed on disk before any fix:

  1. refusesKind does not recurse into z.union. A member whose type is z.array(z.union([...])) is probed at the top level only, so the union's rejection of a wrong-typed element never reaches the classifier.
  2. acceptedMemberKinds counts a kind when the verdict is accepts or refuses-content. A refuses-content verdict means the shape was accepted, the content was not — that is not the same claim as "this kind is accepted", and folding them together is what lets an unmeasured kind appear in the accepted set. Decide deliberately whether this is intended; if it is, say why in a comment on the helper.

If the mechanism is real, every union-typed member this gate judges is currently mis-classified — not just this one.

Scope — read this before touching anything

Verification the card requires

  • Measure, don't infer. CI on PR chore(deps): resolve @objectstack/spec at 17.3.0 in the lockfile #7685 head f1e02a88f also named element:record_picker.sort, object-grid.searchableFields and page:accordion.items in the same failing block. ⚠️ Only record:activity.types has been measured directly. safeParse each affected member against the installed @objectstack/spec artifact and report the actual verdicts; do not treat that list as derived.
  • Positive control on the fix itself. A member that genuinely is multi-kind must still be reported multi-kind afterwards. A fix that collapses everything to a single kind would also make the failing assertion pass — prove that is not what happened, or the run measured nothing.
  • Before/after on the reported kind set for at least record:activity.types: {string, array}{string} expected.

Coordination

⚠️ #8176 is dispatched against the same file (registry-inputs-spec-parity's reverse direction, eager-registration judging). Per the file/region granularity ruling (maintainer, 2026-09-05), state the region you touch in your PR body and keep out of the reverse-direction block. If the two edits genuinely collide, say so rather than merging around it.

Related

#8137 (the decision card this splits from — failure 1 stays there) · #7122 / PR #7685 (blocked by both) · #8097 / cf1d29e2f (the gate) · objectstack#11658 / objectstack#11507 (the open-vocabulary ruling that surfaced this) · #8176 (same file, different region) · #8127 (a different 17.3.0 union-drift finding)

Activity

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanepriority:p2teststooling

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions