Uh oh!
There was an error while loading. Please reload this page.
fix(console,plugin-form): bind the host predicate scope on the two unbound authored-predicate evaluators - #6261
Merged
yinlianghui merged 3 commits intoAug 25, 2026
Conversation
…aluators (#6110) Red before the fix: 5/8 in the console pin and 3/5 in the wizard pin, in both fallback directions.
…bound authored-predicate evaluators (#6110)
…e form routes and the wizard submit gate (#6110) Adds the changeset, the fixture repair for the whole-module app-shell mock, and drops an unused test import.
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 was referenced Aug 25, 2026
yinlianghui
marked this pull request as ready for review
August 25, 2026 06:23
Uh oh!
There was an error while loading. Please reload this page.
yinlianghui
deleted the
claude/issue-6110-predicate-scope-unbound-evaluators
branch
August 25, 2026 06:35
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 freeto 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#6110
Gate union run on
88a49aeca(the final commit on this branch).What was broken
objectui#6010 bound the host
predicateScopeon the five authored-predicatecall sites in
packages/components/src/renderers/form/form.tsx. Two otherauthored-predicate evaluators still passed
undefinedfor that argument, so thedefect #6010 closed on one chain was still live on them:
apps/console's formrenderer (both routes) and
WizardForm's submit-time required re-check.visibleWhenis evaluated withfallback: true, so an unbound root faultsOPEN — the field or section a
current_usertest was meant to hide wasshown to everyone.
requiredWhenfaults the other way (fallback: false), so acurrent_userrequiredWhen silently stopped applying.The card named four sites across two files. Tracing the scope argument from
where it is available to where it is consumed found two more, and one of them
would have made the whole console half ship inert.
FormPage.tsxisFieldVisible— view-level fieldvisibleWhen:610, now:611)FormPage.tsxisSectionVisible— sectionvisibleWhen:651, now:653)FormPage.tsxresolveRowState→resolveFieldRuleState— the OBJECT-levelrules.{visibleWhen,readonlyWhen,requiredWhen}InternalFormRoute.tsx— nothing published a scope at allWizardForm.tsxresolveFieldRuleState:440, now:456)WizardForm.tsxevalFieldPredicate(visibleOn):471, now:492)Site 4 is the one that matters.
usePredicateScope()returns{}unless anExpressionProvideris mounted above, and this repo mounts one in exactly twoplaces —
AppContent(for the/apps/:appName/*subtree) and app-shell'sRecordFormPage. Neither is above/forms/:name, whichApp.tsxmountsthrough
InternalFormRoute→DefaultHomeLayout. Binding the evaluator callsites alone would have read
{}forever: green pins, unchanged product. Theroute now mounts the provider over
buildExpressionUser, the samenormalisation
AppContentuses — imported rather than re-derived, because it iswhat supplies
positions: [], and an absent key makes'x' in current_user.positionsan unbound-key FAULT (fail-open) instead ofresolving false.
@object-ui/app-shellexports it for that reason.Line numbers re-derived on this branch; the card was measured @
a100f77.⛔ The fork clause on the anonymous route — answered by STRUCTURE, no new surface
The dispatch's fork clause: if the routes can be told apart without inventing
new contract surface, that is the fix; if not, stop and report. It reduces to
the first branch, so it is implemented rather than escalated.
The two routes share one call site but not one mount.
/forms/:namerendersinside
InternalFormRoute, which has an authenticated session and now publishesit.
/f/:slugis mounted bare inApp.tsx, deliberately outsideProtectedRouteso an anonymous visitor can submit it — so no provider sitsabove it and
usePredicateScope()returns{}. That is not a gap leftunfilled: an anonymous form has no principal, and binding an empty scope is
exactly that statement. Nothing new is declared, no key is invented, no mode
flag is read at the evaluator.
publicRouteHasNoPrincipalpins it as behaviour:on the public route the same authored text still faults and still fails open,
unchanged by this PR.
featuresis likewise{}on the internal route rather than fetched.ExpressionProvideralready documents{}as the pre-load state whosepredicates default to visible; wiring a deployment-config fetch into this route
would be a different card.
PM mechanism assumptions — verified, not inherited
named call, with the drift noted in the table above.
visibleWhenbinds nocurrent_user— position-gated visibility works on pages and per-option rules, but silently fail-opens on form fields #6010 really bound the renderer — verified by RUNNING, not reading.vitest run packages/components/src/renderers/form/__tests__/predicate-scope-parity-6010.test.tsx→
Test Files 1 passed (1) · Tests 15 passed (15). That file is the oraclefor what "bound" looks like, and its host-scope shape is transcribed verbatim
into both new pins.
packages/core/src/evaluator/fieldRules.ts:157:fallbackis returned forevery not-ok verdict, and both
visibleWhenreaders passtrue. Measured,not read: the pre-fix run failed with the gated field on screen.
WizardForm.tsxmissingRequiredByStep: "the same one the form renderer and the server'srule-validator use, so a conditionally required/hidden field gets the same
verdict from all three rather than a second, divergent dialect." Since Form section/field
visibleWhenbinds nocurrent_user— position-gated visibility works on pages and per-option rules, but silently fail-opens on form fields #6010it was the divergent one.
form.tsx's legacyconditionsynthesis (filed:1955, now:1952)— ✅ confirmed deliberately
undefined, with the reason in its own comment(the predicate is synthesised from
{field, equals}and can only ever namerecord.). Untouched.#6239 (objectui#6111) was still open and unlanded at branch time. Its file list
is
ObjectForm/ModalForm/DrawerForm/SplitForm+packages/types+ its own pinand changeset —
WizardForm.tsxis not in it, exactly as its report said.This branch is cut from
origin/main@cbc883960, which is also #6239's base.No edit was made across it, and no
apps/consolefile this PR touches is in thei18n census overlap that was flagged (
FormPage.tsxis touched here only in thethree evaluator calls and one hook declaration).
Tests — every case asserts HIDDEN / BLOCKED, never merely SHOWN
Because the predicate fails open, a field being shown is the outcome of
predicate-true, scope-unbound, and predicate-faulted. An "is shown" assertion
distinguishes none of them and is green on unfixed code. Both files therefore
author a
current_userpredicate that is false for the bound principal andassert the field is absent — and, in the other direction, a
requiredWhenthat is true for the principal and assert the requirement applies, which
no fail-open accident can reach.
apps/console/src/components/FormPage.predicateScope.test.tsx— 8 testspackages/plugin-form/src/wizardPredicateScope.test.tsx— 5 testsMeasured before the fix: 5 failed | 3 passed and 3 failed | 2 passed,
each in the predicted direction (fields on screen;
expected 'Notes' to contain '*';createnever called; the wizard never returning to theownerstep).Per-site ablation — direction predicted BEFORE each run, all six confirmed
Every leg mutated one site, proved the mutation on disk (anchor uniqueness
asserted inside the mutator — it aborts rather than hitting the wrong arm;
injected marker grepped to 1, removed text grepped to 0; landing site and
git diff --statprinted), restored undertrap … EXIT INT TERM, and endedwith
git diff HEAD --statempty.InternalFormRouteprovider mount (site 4)hop1SessionPrincipalred1 failed | 7 passedresolveRowStatescope (site 3)2 failed | 6 passedWizardFormresolveFieldRuleState(site 5)visibleWhen+requiredWhenred2 failed | 3 passedWizardFormvisibleOn(site 6)1 failed | 4 passedisFieldVisiblescope (site 1)1 failed | 7 passedisSectionVisiblescope (site 2)hop1SessionPrincipalred2 failed | 6 passedLeg A is the measurement that matters: it is the only one that separates a fix
which binds the call sites from a fix that also publishes a scope, and it
shows the other seven console cases staying green while the real route is
unbound — i.e. exactly the inert shipment a card-faithful four-site fix would
have produced.
Leg F red-ing
hop1SessionPrincipalis expected and not a leak: that case'spredicate is authored on a section, so it must depend on site 2.
No rebuild was needed for these legs, and that is a property of the setup, not
an omission.
vitest.config.mtsaliases every@object-ui/*specifier to thepackage's
src/, so a mutation to a source file reaches the running codedirectly. The six reds are themselves the proof it did.
Stated plainly, as asked.
Console pin (8): the three controls pass on a full revert —
ALLOWED(a true predicate shows the section, which is also what unfixed codedoes),
FAULTED(an unbound root fails open either way, and exists so theDENIED rows mean "evaluated and false" rather than "could not be evaluated"),
and
publicRouteHasNoPrincipal(the anonymous route is unchanged by this PR bydesign, so this row is green in both worlds — it pins the fork answer, it does
not test the fix). 5 of 8 would go red: the three DENIED rows, the
requiredWhenrow, andhop1SessionPrincipal.Wizard pin (5): the two controls pass on a full revert —
ALLOWEDandFAULTED, both of which block the submit before and after. 3 of 5 would gored: the two fail-open rows and the fail-closed
requiredWhenrow.8 of 13 assertions are the deliverable; 5 are controls that are green either
way, and every one of the 5 is named above rather than left to be discovered.
Also updated:
apps/console/src/__tests__/internalFormShell.test.tsx'swhole-module
@object-ui/app-shellmock, which now declares the two symbolsInternalFormRouteconsumes. Stubbed rather than made real, deliberately — thatfile pins #4109's shell nesting and authors no predicate; the binding is
pinned by
hop1SessionPrincipal, which leg A shows goes red the moment themount is removed.
Behaviour change — in the changeset, in words
.changeset/6110-predicate-scope-unbound-evaluators.md,minor(nevermajor). It states that previously-inertcurrent_userpredicates will starthiding fields and sections and start holding submits; that fail-open is why
nobody noticed; that the wizard half is a fix in the submitter's favour (it
stops demanding a field the wizard itself hid); that the public
/f/:slugrouteis deliberately unchanged; and it tells authors to audit their
current_userpredicates before upgrading.
Verification
predicate-scope-parity-6010.test.tsx(assumption 2, by RUNNING)Tests 15 passed (15)FormPage.predicateScope.test.tsx(new)5 failed | 3 passedpre-fixwizardPredicateScope.test.tsx(new)3 failed | 2 passedpre-fixvitest run apps/console/Test Files 77 passed (77) · Tests 877 passed (877)vitest run packages/plugin-form/Test Files 63 passed (63) · Tests 622 passed (622)vitest run packages/app-shell/src/{providers,console}/Test Files 79 passed (79) · Tests 536 passed (536)type-check@object-ui/console (tsc --noEmit && tsc -b … --force)EXIT=0, 0 errorstype-check@object-ui/plugin-form (tsc --noEmit×2 projects)EXIT=0type-check@object-ui/app-shell (tsc --noEmit×2 projects)EXIT=0turbo run lint --filter=console --filter=plugin-form --filter=app-shell --forceTasks: 4 successful, 4 total,Cached: 0 cachedcheck-changeset-no-major.mjsNo changeset declares a major bump.check-changeset-presence.mjs7 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)check-changeset-fixed.mjsAll workspace packages are in the changeset fixed group.check-control-bytes.mjsOK (scanned 5154 tracked text file(s))check-vi-mock-specifiers.mjsOK (… 429 carry a mock …)check-lint-coverage.mjs46/46 packages linted, 0 with outstanding errorscheck-type-check-coverage.mjs45/46 via type-check, 0 errors outstandingcheck:self-importNo package names itself inside its own src/.check:phantom-depsEvery in-scope import is declared by the package that publishes it.check:published-distNo published package's build output carries tooling material.(after a 132s build)Gate set derived by enumerating each CI job's own step list (
ci.yml,lint.yml,changeset-guard.yml,changeset-presence.yml,control-bytes.yml,vi-mock-specifiers.yml), not from top-level script names. Exit codes capturedbefore any pipe.
Two declared narrowings, and one measured trap
vitest run packages/app-shell/was killed at the container's foregroundcap under contention from sibling agents. Replaced by the
providers/+console/subtrees — which is where this PR's only app-shell change lives(one re-export in
index.ts, pointing atconsole/AppContent.ts) — plustsc --noEmitover the whole package, which is the check that actuallyvalidates a re-export. CI runs the full farm exactly once regardless.
pnpm type-check/pnpm lintrepo-wide areturbo run …over all 46packages — repo-scale runs CI owns. The per-package invocations above are the
same compiler and the same
eslint .unit CI runs for the three changedpackages, so for those packages this is the complete CI unit, not a sample.
tscresolves workspace deps throughdist, not source. The firstconsole type-check reported 15 errors that were purely unbuilt sibling packages
(
plugin-gantt/map/markdown/timeline/tree) — and it also caught one realerror in this PR's own test file. Recording it because reading that run as "my
change broke the console" (or as "all noise") would both have been wrong; the
closure was built and it re-ran at
EXIT=0, 0 errors.vitestis the opposite:vitest.config.mtsaliases every@object-ui/*tosrc/, which is why theablation legs needed no rebuild.
⛔ Left as draft — not marked ready, not enqueued, no auto-merge. The PM lands it.
Generated by Claude Code