Repository navigation
fix(console): make invalid_type decisive in refusesKind, so union-typed members are judged by what the contract refused (objectui#8204) - #8210
Merged
Conversation
…-typed members are judged by what the contract refused (objectui#8204)
`registry-inputs-spec-parity`'s kind classifier asked "is EVERY complaint at this
node a kind complaint". Zod keeps running a schema's checks after its type check
has already failed, so a node can carry both at once: for `z.string().min(1)` the
probe `[]` comes back as `invalid_type` AND `too_small`. Read by `every`, that
pair is a CONTENT refusal — and the value it describes is an array the contract
refused for being an array.
Measured on the installed `@objectstack/spec` 17.3.0 artifact, whose
`RecordActivityProps.types` is `z.array(z.union([FeedItemType, z.string().min(1)]))`:
the `[]` probe alone put `array` into that key's accepted kind set, and the
derivation gate reported `record:activity.types → contract accepts {string,array}`
for a key whose array members `safeParse` measurably rejects.
`invalid_type` at a node is now decisive, ahead of the `every`: a value cannot be
simultaneously the right kind and the wrong one, so whatever else the node says
about a value it has already refused for its kind is downstream of that refusal,
not a second opinion. That also makes the `invalid_type` arm of the `every`
unreachable, so it is gone.
The card's first suspected mechanism — "`refusesKind` does not recurse into
`z.union`" — was measured and is FALSE: the recursion runs and is what carries
branch verdicts up. The per-branch reading underneath it was the defect. The
second — `acceptedMemberKinds` folding `refuses-content` into "accepted" — is
INTENDED, is the same fold `judgeMembers` and the arm direction already make, and
is now documented as such at the helper with the 17.2.0 measurement that shows
dropping it would break the enum case the coarse-kind ceiling exists for.
Test-only. No expectation literal, calibration control or `it(...)` assertion is
edited; the two logic lines are inside `refusesKind`, the rest is prose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114Ytxr5sM1vdW19Y9WAx6E
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #8204
Test-only. One logic line added and one removed, both inside
refusesKind; everythingelse in the diff is prose and a changeset.
What was actually wrong — the first suspected mechanism is FALSE
objectui#8204 suspected that
refusesKinddoes not recurse intoz.union. Measured ondisk: it does. The
invalid_unionarm at the bottom of theeveryruns, and it iswhat carries branch verdicts up. The defect was one level in — in how a single branch's
issues were read.
refusesKindasked "is EVERY complaint at this node a kind complaint". Zod keepsrunning a schema's checks after its type check has already failed, so one node can carry
both kinds of complaint at once. For
z.string().min(1)the probe[]comes back asinvalid_type(expected string) andtoo_small([].lengthis 0). Read byevery,that pair is a CONTENT refusal — and the value it describes is an array the contract
refused for being an array.
The cleanest evidence is two probes on one key, differing only by the artefact issue.
@objectstack/spec17.3.0 declaresRecordActivityProps.types = z.array(z.union([FeedItemType, z.string().min(1)])):Same key, same coarse kind, opposite verdicts.
[]is the first array probe and theaccepted-kind walk is a
some, so that one probe alone putarrayinto the key'saccepted set.
The fix
invalid_typeat a node is now decisive, ahead of theevery. This is a statement aboutthe contract, not about Zod:
invalid_typesays the value is not of that node's type, anda value cannot be simultaneously the right kind and the wrong one, so whatever else the
node says about a value it has already refused for its kind is downstream of that refusal
rather than a second opinion competing with it.
too_smallon a non-string is exactlysuch an artefact — "too short" is not a claim anyone would make about
[]against astring contract. The change also makes the
invalid_typearm of theeveryunreachable,so it is gone.
The second suspected mechanism is INTENDED, and is now documented
acceptedMemberKindscounting a kind when the verdict isacceptsorrefuses-contentis deliberate, is the same foldjudgeMembersand the arm directionalready make, and is load-bearing. Measured on 17.2.0, where
record:activity.typesis abare
z.enum([...])of strings: every string probe is refused asinvalid_value,'Account'among them, so anaccepts-only reading answers{}for a key whoseof: 'string'was derived from that very enum, and the one-kind assertion would red on adeclaration that is right. The fold was never the leak; it was the amplifier. A comment at
the helper now says so, as objectui#8204 asked.
Measurements
Verdicts are
safeParseagainst the installed@objectstack/specartifact, and thekind sets are the gate's own, from its own failure message.
The CI list is not derived — three of its four names are clean. CI on PR #7685 head
f1e02a88fnamedelement:record_picker.sort,object-grid.searchableFieldsandpage:accordion.itemsalongsiderecord:activity.types. They appear because thatassertion's message prints every judged row; the received array held exactly one
element. Measured on 17.3.0:
record:activity.types{string,array}{string}element:record_picker.sort{object}{object}object-grid.searchableFields{string}{string}page:accordion.items{object}{object}Positive control — a genuine multi-kind member stays multi-kind.
record:highlights.fieldsis a real member union (a field NAME or an inline field object)and is the one key
MULTI_KIND_MEMBER_CONTRACTSpins. It reads{string,object}beforeand after, on 17.2.0 and on 17.3.0. So the fix did not collapse members to one kind.
Across all sixteen keys checked on both spec versions, exactly one row moves:
record:activity.typeson 17.3.0.The gate itself, run against a real 17.3.0 artifact (the installed 17.2.0 package
swapped for a registry copy of 17.3.0 under the repository's own zod 4.4.3, then restored
and the restore proven by sha256 of
dist/ui/index.mjs):The failure that disappears is
member declarations are derived from single-kind member contracts— this card's. No new failure appears.The remaining six are not this card's, and one of them is red on purpose.
the member judge reds on the drift that started thisfails atexpect(specMemberVerdict('record:activity', 'types', 'array', 'Account')).toBe('refuses-content')—
acceptsreceived, because 17.3.0's open vocabulary genuinely accepts it. That isfailure (1) of objectui#8137, it sits on the manual floor, it is undecided, and it is
untouched here. The other five are 17.3.0 spec-drift ledger failures around
object-grid.defaultSort, which belong to objectui#7122 / PR #7685.Gates
All at
65b2e6451;git statusis empty at that commit, so every figure below wasmeasured on that tree.
pnpm exec vitest run apps/console/src/__tests__/registry-inputs-spec-parity.test.tson theinstalled 17.2.0 —
Test Files 1 passed (1),Tests 181 passed (181), unchanged fromthe pre-fix baseline.
pnpm exec vitest run apps/console/—Test Files 89 passed (89),Tests 1052 passed (1052).pnpm --filter @object-ui/console run lint— 0 errors (208 pre-existing warnings, nonein this file;
eslint --format jsonon the changed file reports 0/0).pnpm --filter @object-ui/console run type-check— exit 0, afterpnpm --filter '@object-ui/console^...' build.tsc --listFilesconfirms the changedfile is in the program, so the green is a measurement of it and not an
exclude.node scripts/check-changeset-presence.mjs— OK, empty-frontmatter declaration.node scripts/check-control-bytes.mjs— OK.Coordination with objectui#8176
Per the file/region granularity ruling (maintainer, 2026-09-05), the regions touched are:
refusesKindand its doc comment — the shared classifier in the ARM directionhelper block (
objectui#4971), lines ~1124-1180.acceptedMemberKindsinside the MEMBER direction(
objectui#8067) testmember declarations are derived from single-kind member contracts,line ~2705.
objectui#8176 works the REVERSE direction (
objectui#3808, eager-registration judging),whose block begins at line 2189 and whose helpers sit above line 700. No overlap: nothing
in this diff falls inside the reverse-direction block, and no reverse-direction assertion
or helper is read or written here. There is no collision to merge around.
Scope bans, honoured
MULTI_KIND_MEMBER_CONTRACTSentry added.it(...)assertion edited. The diffcontains no
expect(,toBe(,toEqual(, or probe-table line on either side.not be.
Generated by Claude Code