`apps/console/src/components/FormPage.tsx` is a second, independent form
renderer — it serves both `/f/:slug` (public, anonymous) and `/forms/:name`
(internal) — and it read neither spelling of the FormView field visibility
predicate. A field conditioned on `record.priority == 'urgent'` rendered
unconditionally on both routes: fail-open and silent.
objectui#2212 recorded the same symptom and PR #2214 fixed it on the OTHER
chain (ModalForm -> resolveFormViewLayout -> plugin-form sectionFields.ts ->
components renderers/form/form.tsx), which this file is on at no point, and
that fix's regression pin lives with that chain.
Applies #2212's ruling verbatim rather than inventing a second predicate
semantics: the predicate is routed through the canonical engine,
`evalFieldPredicate` (@object-ui/core, evaluator/fieldRules.ts), with the same
`record.`/`previous.` scope, the same accepted wire shapes (bare CEL string and
`{ dialect, source }`), and the same fail-open-but-loud behaviour. Resolution
is canonical-first — `visibleWhen ?? visibleOn` — matching plugin-form's
`sectionFields.ts` and app-shell's `readVisibility`.
The regression pin lives with THIS renderer, next to the copy it describes: a
pin that cannot see the second copy is how the first gap survived.
Out of scope by triage fence: FormPage is not folded onto the plugin-form
chain. The second-renderer convergence question stays with the #5596 track.
Fixes#5594
Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK
Fixes#5594
Summary
apps/console/src/components/FormPage.tsxis a second, independent form renderer —its own
buildSections, its own JSX — and it serves both the public/f/:slugroute andthe internal
/forms/:nameroute. It read neither spelling of the FormView fieldvisibility predicate, so a field an author conditioned on
record.priority == 'urgent'rendered unconditionally on both routes. Fail-open and silent: the author saw the field
always, with no diagnostic.
This wires the predicate into this renderer and pins it with a regression test that
lives with this renderer. It does not touch the other one.
Why the gap survived
Issue #2212 recorded exactly this symptom, and PR #2214 landed the repair on a
different chain:
ModalForm→resolveFormViewLayout→@object-ui/plugin-formsectionFields.ts→@object-ui/componentsrenderers/form/form.tsx.FormPage.tsxison that chain at no point, and that PR's regression pin lives with the chain it repaired,
so nothing in the suite could see this copy. One contract, two implementations, each only
ever checked against itself.
The ruling applied, verbatim
Per the PM ruling on the card, this inherits #2212's ruling rather than inventing a
second predicate semantics — two form renderers disagreeing about what
visibleWhenmeans would be a worse defect than one renderer ignoring it.
Helper signature verified in the built
dist/*.d.tsthe console'stscactuallyreads, not from memory (
packages/core/dist/evaluator/fieldRules.d.ts:63):Reachable from the package root via
export * from './evaluator/index.js'inpackages/core/dist/index.d.ts:26.So, by construction rather than by agreement:
evalFieldPredicate(@object-ui/core,evaluator/fieldRules.ts).{ dialect, source }.record.*is the LIVE input values (the predicate re-decides as the usertypes),
previous.*is the stored record an edit form started from (type: 'form'action fired from a record opens an empty CREATE form —/forms/:nameignores the?recordId=ActionRunner forwards #4278). The samepair
form.tsxbinds.because a hidden field is one the submitter can neither fill in nor see is missing.
visibleWhen ?? visibleOn, matching both siblingreaders:
sectionFields.ts(fd.visibleWhen ?? fd.visibleOn) and app-shell'sreadVisibility. The deprecated ADR-0089 alias stays honoured because it still haslive producers that never pass through the spec normaliser.
What changed
apps/console/src/components/FormPage.tsxRenderableFieldcarries the predicate (type derived fromFormFieldSpec['visibleWhen'], not restated);buildSectionsresolves it canonical-first; new exportedisFieldVisibleanswers the static flag and the predicate in one verdict; the render filter calls it.apps/console/src/components/FormPage.visibleWhen.test.tsx.changeset/console-formpage-visible-predicates-5594.md@object-ui/console: patch.Two things deliberately did not change, recorded rather than assumed:
rendering rule in both renderers (the plugin-form repair returns
nullat render andclears stale errors, never values). Making it a submit-payload rule would be a new
contract decided once for both, not invented here in the second one. Pinned as a control.
FormPageis not folded onto the plugin-form chain — the triage scope fence. Thesecond-renderer convergence question stays with the [finding] The SAME drift class one level up: FormViewSpec and FormSectionSpec are each hand-declared twice (console + app-shell), and #5542 only converged the leaf #5596 track.
Evidence
The defect probe, and a control at identical scope. A bare
grep -cfor the two keynames in this file returns
1on today'smain— but that one hit is a prose mentioninside a docblock, not a read. Scoped to a property read instead:
The control returns non-zero at identical scope, so the
0is a missing read and not abad search.
Reverse verification — measured, not predicted.
FormPage.tsxreverted to its pre-fixstate with the new pin left in place (mutation confirmed on disk by anchored greps on the
text being changed, in both legs; the script carries a
trap … EXIT INT TERMrestore, andthe tree was verified clean afterwards). No build leg is involved: the test imports the
subject as a relative source path (
./FormPage), not through a packageexports→dist.The one green is the control that has to be green — "a field with NO predicate at all
still renders" — without which every "the field is absent" assertion would be equally
satisfied by a renderer that draws nothing at all. The two cases named CONTROL are red
here because each must first establish that the field IS hidden; their control halves
(fail-open direction, submitted payload) are unaffected. The full per-test breakdown is in
the test file's docblock.
Gates
All run after the final commit, from the repo root (package-cwd
vitestis refused bythe repo's own guard, #3378), at
93187c334with a clean worktree:npx vitest run apps/console --maxWorkers=2Test Files 66 passed (66)·Tests 722 passed (722)pnpm --filter @object-ui/console type-checktsc --noEmit && tsc -b tsconfig.node.json --force, script name echoed — not a zero-match no-op)pnpm --filter @object-ui/console lint201 problems (0 errors, 201 warnings), all pre-existingno-explicit-any; the two touched files contribute 0 errors, and the new test file 0 warningscheck:control-bytesOK (scanned 4673 tracked text file(s); skipped 85 binary)check-changeset-presence2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)check-changeset-fixed/check-changeset-no-majorAll workspace packages are in the changeset fixed group./No changeset declares a major bump.check:phantom-depsEvery in-scope import is declared by the package that publishes it.check:lint-coverage46/46 packages linted, 0 with outstanding errorscheck:type-check-coverage45/46 via type-check, 0 known-brokencheck:eager-closureNo eager-closure report at apps/console/dist/eager-closure.json … This is a broken gauge, not a passing budget.It needs a consolevite buildto write the report. Noted, not "repaired". The one new import isevalFieldPredicatefrom@object-ui/core, a package already eager in this bundle (55 files underpackages/app-shell/srcimport it, andApp.tsximports app-shell statically), so no new package enters the eager closure.No
packages/**file is touched, so the "doesdist/*.d.tsmove?" question does not arisefor this change — the whole diff is
apps/consoleplus one changeset.Follow-up filed, not fixed here — #5627
FormPagestill drops the other conditional-rule surfaces: section-levelvisibleWhen/visibleOn, and the object-level field rulesvisibleWhen/readonlyWhen/requiredWhenthat the sibling chain copies out of the objectschema. Same class, different keys and different user-visible consequences (disabled state
and submit-blocking, not just hiding), and it raises two semantics questions of its own, so
it is filed rather than smuggled in here.
Generated by Claude Code