Tabbed arm of the grouping contract: FormFieldTab gains the ruled predicate slot (#6237) - #6619
Conversation
… FormFieldTab.visibleWhen (#6237) The tabbed modal arm (`ModalForm` `contentLayout: 'tabbed'`) synthesises no section-divider, so the #6236 grouping contract had no slot to carry an authored `FormSection.visibleWhen` — the predicate was dropped one hop before evaluation. `FormFieldTab` now declares the predicate slot the 2026-08-27 maintainer ruling named, and the form renderer evaluates it with the same record assembly and fail-open fallback the field-level rules use. A FALSE verdict draws neither trigger nor panel; not drawing the panel unmounts the claimed fields through the mechanism a field's own false predicate uses, so the ruled semantics are inherited: hidden-tab values still submit, hidden-tab fields skip client-side validation (server is the loud floor), and a mid-session flip clears the members' stale errors. Selection re-derives over visible tabs only (pick → default → first visible; a hidden pick is restored on re-admission), and the arm decision stays structural on the DECLARED tabs, so a predicate hiding one of two tabs filters the strip instead of collapsing the layout mid-interaction. ModalForm's tabbed synthesis site copies the section predicate onto the tab it emits; the #6111 layout matrix gains both tabbed-modal rows and its FAULTED block now genuinely mounts each row's layout (it used to mount the same simple form under every label). New renderer pin file fieldtab-visiblewhen-6237.test.tsx covers DENIED/ALLOWED/FAULTED, both ruled semantics, re-selection, non-collapse, and compat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
… in the 6237 pin header Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
|
ACCEPT on substance (ui execution seat, The PM hypothesis was refuted, and that is the right outcomeMy dispatch order floated that the tabbed arm might reuse the ⭐ A membership claim can hide fields; it cannot hide a TRIGGER. The tab has a button as well as a panel, and no amount of member-name claiming reaches it. Compounding that: the tabbed arm synthesises no divider to carry a claim at all, and predicates are live-record reactive, so evaluation cannot be moved up to the synthesis site. So What makes this implement the ruling rather than re-implement itThe ruled semantics — values still submit, hidden-group fields skip client validation, the server is the loud floor, stale errors clear — are inherited through the same unmount mechanism field rules already use, not re-coded beside it. ⭐ That is the difference between one contract and a second one that happens to agree today. The same-record-assembly, same-fail-open-policy choice is what keeps Both sub-questions resolved as implementation, correctly, with no product fork invented
Neither needed the box, and neither was quietly turned into a product decision. That was the instruction and it was followed. ⭐⭐ The zod call is the one I would most have expected to get wrong
The bounded in-place fix is a genuine anti-vacuity repairThe #6111 matrix's FAULTED loop never used AblationsRenderer ablation predicted 8 red / 4 green and observed exactly that, with the split meaningful in both directions — DENIED red in the SHOWN direction, validation rows red in the BLOCKED direction, and the ALLOWED / FAULTED / compat controls green. ⭐ The producer ablation is the part worth keeping: the first anchor matched two sites (ModalForm also copies
|
✅ 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
|
Part of #6237 — the tabbed MODAL arm is closed here; the TabbedForm and WizardForm arms remain open on that card (measured below), so this PR must not close it.
What landed
The tabbed modal arm (
ModalFormcontentLayout: 'tabbed') synthesises nosection-dividerat all, so the grouping contract from PR #6608 had nothing to stamp an authoredFormSection.visibleWhenonto and no slot to copy it into — the predicate was dropped one hop before evaluation. Per the 2026-08-27 maintainer ruling (propagated on the card: one grouping contract serves sections and tabs; the ruling names the tab predicate slot),FormFieldTabnow declares it:@object-ui/types:FormFieldTab.visibleWhen— same spelling asFormField.visibleWhen(string or dialect envelope). Authorised widening (dispatch Clause-2); nothing else widened.@object-ui/components(renderers/form/form.tsx): the renderer evaluates each tab's predicate with the SAME record assembly the field rules use (ruleRecord/previousRecord/ host predicate scope, Form section/fieldvisibleWhenbinds nocurrent_user— position-gated visibility works on pages and per-option rules, but silently fail-opens on form fields #6010) and the same fail-open fallback. A FALSE verdict draws neither trigger nor panel; not drawing the panel unmounts the claimed fields through the exact mechanism a field's own false predicate uses, so the ruled semantics are inherited rather than re-implemented: a hidden tab's values still submit, its fields skip client-side validation (the server stays the loud floor — the [console 17.0.0-rc.0 @4a4829d0] Tabbed create/edit modal still loses non-active-tab values (#2153 closed but explicit-sections path unfixed) — Radix unmount destroys per-tab form state #2959 trap, ruled, applied, not re-opened), and a mid-session flip clears the members' stale errors.@object-ui/plugin-form:ModalForm's tabbed synthesis site copies the section'svisibleWhenonto the tab it emits.The two sub-questions the dispatch left to this seat (implementation choices inside the ruled frame — no product fork surfaced)
Small rider on the same mechanism: the server-rejection tab activation now skips a predicate-hidden tab (there is no panel to activate; the toast already names the field; recording the pick would later yank the view when the tab is re-admitted).
Whether the slot was needed at all (the PM asked for the measurement, not the assumption)
Reusing the #6608 membership mechanism without a new key was measured and rejected:
FormField.fieldsgates FIELDS via a divider row, but the tabbed arm has no divider to carry the claim (one would render as a stray heading in the unclaimed leading block), and no membership claim can hide the tab TRIGGER — the tab needs its own verdict. Evaluation cannot move to the synthesis site either: predicates are live-record reactive (they flip on keystrokes), and only the renderer holds the rule record. What IS reused is everything behind the slot: the evaluator, the record assembly, the fail-open policy, and the unmount mechanism that carries both ruled semantics. The landed #6608 docblock itself anticipates exactly this slot ("the tabbed arm's predicate slot is objectui#6237").Zod mirror, measured:
FormFieldTabhas no zod counterpart — the zodFormSchemamodels neitherfieldTabsnorfieldPanes/defaultFieldTab/fieldTabsPosition(onlyFormFieldSchemais mirrored and #3090-pinned, untouched here). Nothing to mirror; declaring a new validated surface would be an unauthorised widening.Fence stops (deliberate)
ModalFormcontentLayout: 'tabbed') cannot carry a sectionvisibleWhenat all —FormFieldTabdeclares no predicate slot #6237. Measured:TabbedFormsynthesisesfieldTabsfrom its ownFormSectionConfig, which declares NOvisibleWhenkey (the AuthoredFormSection.visibleWhenis dropped by all four plugin-form layouts — declared, bridged, then never evaluated on the object-view chain #6111 stop-at-the-boundary decision), so the renderer slot is ready but there is nothing to copy; adding that key widens a published@object-ui/plugin-formtype, which Clause-2 does not authorise.WizardFormrenders steps through its own navigation machinery, notfieldTabs, and a hidden STEP needs its own design. HencePart of, not a closing keyword.content/docs, package READMEs) are outside this dispatch's file fence; the FormFieldTab docblock and changeset carry the contract text. Flagged in the report for a docs follow-up.Bounded in-place fix, declared with evidence
sectionVisibleWhen-6111.test.tsx's FAULTED block looped over the layout matrix but mounted the SAME simple form under every row's label (layout.mountwas never called in that block) — five rows named layouts they never exercised. My two new tabbed rows would have inherited that phantom coverage, so the block now genuinely mounts each row's layout with the unbound predicate. All seven FAULTED rows pass — fail-open holds on every real mount. In-fence file, same gate family, no new verification surface.Verification (union re-run at final commit
63a55aa8)pnpm exec vitest run packages/components/src/renderers/form/— 57 files / 386 tests green (includes the newfieldtab-visiblewhen-6237.test.tsx, 12 pins).pnpm exec vitest run packages/plugin-form/ packages/types/— 136 files / 1499 tests green (the 6111 matrix now 22 rows: 7 layouts x 3 verdicts + the flipped measured-scope pin).type-checkgreen for types / components / plugin-form after building the dependency closure;tsc -p tsconfig.test.json --listFilesconfirms the new test file is compiled (positive control: the known-covered 6236 pin appears in the same query).@object-ui/componentscompiledvisibleWhen: 'record.x == 1'and the dialect envelope against the REBUILT d.ts (a stale d.ts rejects the key as excess), and the negative controlvisibleWhen: 42drew exactlyerror TS2322: Type 'number' is not assignable to type 'string | { dialect?: string | undefined; source: string; } | undefined'— the union read back from the compiled declaration.git diff HEAD; suites import the mutated files by source path, so no build step sits between mutation and observation):hiddenFieldTabKeysverdict): predicted 8 red / 4 green — observed exactly that, DENIED rows red in the SHOWN direction, validation rows red in the BLOCKED direction, ALLOWED / FAULTED / compat controls green.visibleWhencopy; unique anchor after a first attempt matched 2 sites and was redone scoped): predicted exactly the two tabbed matrix rows red in the SHOWN direction — observed 2 red / 20 green.eslint --no-inline-configon the five changed files: 0 errors (repo lint deliberately does not gate warnings; the fulleslint .sweep is CI's run — narrowing is sound because this config does no type-aware linting, so one file's diff cannot move another file's verdict).Maintainer ruling applied, quoted on the card (2026-08-27): visibility gates drawing only; hidden-group values still submit; hidden-group fields skip client-side validation; the server is the loud floor.
Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
Generated by Claude Code
Generated by Claude Code