Repository navigation
fix(app-shell): the Studio decides a screen field's visibleWhen live, through the runner's renderer, over the screen's declared fields (objectui#10743) - #10763
Conversation
… through the runner's renderer, over the screen's declared fields The Studio screen preview and the Debug run's screen pause judged each field's `visibleWhen` once, against the run's variables, dropped the fields judged hidden and stripped the predicate from the rest, so a sibling-field predicate (`createOpportunity == true`, `discount > 0`) stayed hidden however the author ticked or typed. `buildScreenSpec` now carries the predicate raw and `ScreenView` decides it live; the Studio's own evaluator for this key (`isFieldVisibleWhen`, `fieldVisibility`, `evalVisibleWhen`) is removed. The Debug run's screen step names, as an error, a predicate that references a name that is not a field declared on this screen, or a shape `registerFlow` refuses; a sibling predicate is no longer reported. `ScreenView` exports `screenPredicateScope`, unchanged. The runner's fallback direction is untouched. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
✅ 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
|
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Two items stop the landing: (1) |
…n over the screen's declared fields, by the Debug run's rule Round-2 review items on the same branch. `flow-expr-problems.ts` judged the `screen` node's `fields[].visibleWhen` column over the FLOW scope, so it warned on a sibling-field predicate and stayed silent on a run variable, the inverse of the Debug run's screen step. It now applies `screenVisibleWhenScopeError` from `screen-spec.ts`, the one rule both surfaces share: the roots are the keys of the renderer's own `screenPredicateScope` plus `record`. A comprehension's iteration variable is bound, not a root (walked over `parseCelToAst`, scoped to the macro's arguments). The screen field inspector's `visibleWhen` placeholder is a sibling-field example. The changeset's server sentence states when the resume door enforces `required`; the `screen-spec.ts` docblock says the server side is to be closed and that `record` is restated by name because the renderer binds it inside `evalFieldPredicate`. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
|
changeset-claim-re-read
|
✅ 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
|
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Nothing in the branch stops the landing: all five round-2 items are delivered and measured true at head, the pins are red on |
…ble half of the inline-cell statement (objectui#10743) Prose only, from the round-2 contract review: the Studio reads the bare roots off the renderer's own scope and restates only `record`, which the renderer binds inside `evalFieldPredicate`; the inline inspector's visibleWhen cell also says nothing on a run variable the Problems panel and the Debug run report. Frontmatter untouched. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
✅ 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
|
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS The delta is the one changeset file, prose only, with both round-2 sentences landed verbatim and true at head; every changed body line re-measures true against head and current main; CI is fully green and the merge is clean. Nothing found that should stop the landing. |
Fixes #10743
Clause-②: no — the Studio's screen preview, Debug run and Problems panel rebind to the runner's own evaluator and its scope; no export on
@object-ui/app-shell's index, no spec shape and no accept set moves (screen-spec.ts,flow-expr-problems.tsandScreenView's scope helper are module-internal)Executes the seat's ruling on objectui#10743 (comment 5853038135, option C: one contract, one scope) with the amended claim 5852879659, the round-1 contract review (comment 5853440854) and the round-2 contract review (comment 5855514425, PASS) on this PR. The server half is objectstack-ai/objectstack#20178 and does not block this. The flow runner (
ScreenView,FlowRunner) has NO behaviour change;ScreenViewonly exports its existingscreenPredicateScope.What changed
screen-spec.ts—buildScreenSpec(node)keeps every authored field and carries itsvisibleWhenraw, as the runtimescreenexecutor sends it to the client; it no longer takesvariablesand no longer decides visibility.ScreenViewdecides it live, over the screen's declared fields and the values the author collects in the preview — the one client evaluator. The Studio's own evaluator for this key is removed:isFieldVisibleWhen,fieldVisibility, and the simulator'sevalVisibleWhen(flow-sim-validate.ts;evalCelPredicatekeeps serving edge guards and decision branches).screenPredicateRoots(node)is the ONE scope rule: the keys of the renderer's ownscreenPredicateScope(spec, {})plusrecord, the namespaceevalFieldPredicatebinds the same values under (that binding happens insideevalFieldPredicate, sorecordis restated by name; the bare names are read off the scope).screenVisibleWhenScopeError(visibleWhen, node)judges one predicate against it — shape refusal, CEL parse (a{var}brace), then the free roots — naming a root that is not a field on this screen with the nearest declared field for a typo.unevaluableVisibleWhen(node)lists the fields it applies to, for the Debug run's screen step (recorded on the step'serror, drawn in rose). A sibling-field predicate (discountabove zero,record.discount) is not reported.["a","b"].exists(t, t == note)no longer reportst. The free roots are walked over the canonicalparseCelToAst, with the binding scoped to the macro's own arguments (all,exists,exists_one,map,filter):["a","b"].exists(t, t == tags)still reportstags, and an outer baretint == 1 && ["a"].exists(t, t == note)is still a root.collectCelRootIdentifiersis the fallback when the parse hands back no AST, so an unexpected shape is never read as "no roots".flow-expr-problems.ts(round 2, the review's blocking item) — the Problems panel judged thescreennode'sfields[].visibleWhencolumn over the FLOW scope: it warned "discountis not a reference in scope at this step" on the sibling-field shape and stayed silent on a run variable, the inverse of the Debug-run step. That column now goes throughscreenVisibleWhenScopeError, the same rule: the sibling shape is clean, and a run variable the flow scope holds (needsApproval == true) is the unknown reference, named by field and root, atwarning— this panel's level for a reference nothing binds, since the runtime accepts such a flow today (theregisterFlowrefusal is objectstack#20178). Brace errors stayerror, ahead of the scope. Every otherexpressionslot keeps the flow scope.hiddenFieldCount(spec, values)is the renderer's own verdict (screenFieldsminusvisibleScreenFields), so the preview's "N fields hidden by their visible when condition" hint follows every tick and keystroke; it moved intoScreenFormPreview, where the values live.inspectors/flow-node-config.ts(round 2) — the screen field inspector'svisibleWhenplaceholder is a sibling-field example,createOpportunity == true(the flows guide's shape); it readstage == "review", a run-variable shape the runner cannot bind. Only that column entry and a comment above it change.ScreenPreview.tsx—variablesnow feed{var}interpolation only; the docblock says so.screen-spec.ts,ScreenPreview.tsx,flow-simulator.ts,flow-sim-validate.ts,packages/app-shell/README.md, and the pending.changeset/10692-debug-run-routing-parity.md(a dated note in the objectui#10533 form; its frontmatter is untouched). Each states that the server's resume door still reads the run's variables until objectstack#20178 lands, andscreen-spec.tsnow says that divergence "is to be closed on the server side". No server behaviour that is not on objectstack main is claimed..changeset/10743-studio-screen-visiblewhen-live.md,'@object-ui/app-shell': patch.Still on the flow scope — stated, not aligned
The inline inspector's
visibleWhencell (FlowObjectListField, anexpressioncolumn fed the flowscopeGroups) is outside the claimed surface and is unchanged. Measured at head with the realFlowObjectListField, thescreennode'sfieldscolumns andresolveFlowScope(flow variableneedsApproval; scope tokensneedsApproval,record,previous): the cell's inline note reads "discountis not a reference in scope at this step." on the sibling-field shape (note,visibleWhendiscountabove zero) and shows nothing forreason(needsApproval == true); its picker offers the flow scope; the placeholder now readscreateOpportunity == true. So the inline cell still says the inverse of the Problems panel and the Debug run on the sibling shape. Stated in the changeset and the README; aligning it (screen-field groups for that one column, fromscreenPredicateRoots) is the follow-up beside objectstack#20178.Pending changesets that name
inspectors/flow-node-config.ts(changeset-claim-re-read, comment 5855257425).changeset/6499-inactive-values-retained.md— still true. Its paragraph namesinactiveRetainedKind(field, node, fields)and theshowWhenpopulation ("all 33 gated fields"); this diff touches neither: at the base and at head the file exportsinactiveRetainedKindonce and carries the same number ofshowWhenoccurrences, and the change is one placeholder string plus a comment on the screenfieldscolumn, which declares noshowWhen..changeset/9278-end-outcome-select.md— still true. Its paragraph describes theendgroup'soutcomecontrol; this diff does not touch theendgroup: at headoutcomeis still theselectovercompleted | refusedwithdefaultValue: 'completed', and the onlysuccess · failureleft in the file is the comment recording that placeholder's removal. No correction, both frontmatters untouched.H1 — the paused-run payload carries no run variables (both sides, from source)
service-automationengine.ts, the screen-pause arm ofexecuteand ofresumeInternal:return { success: true, status: 'paused', runId, durationMs, screen: err.screen };— no variables member; the subflow re-pause arm is the same shape withchildRes.screen.contracts/automation-service.tsAutomationResultmembers: success, output, error, durationMs, code, status, runId, screen, successMessage, errorMessage, refusalMessage, summary — no variables.ScreenSpecmembers: nodeId, title, description, fields, kind, objectName, mode, recordId, defaults, idVariable.domains/automation.ts: the trigger door serves the engine result verbatim,if (isPausedRun(result)) { return { handled: true, response: deps.success(result) }; }; the resume door likewise afterautomationService.resume(parts[2], signal); the screen door answersdeps.success({ runId: parts[2], screen }).variablesfor a paused run is GET runs/RUN_ID (anExecutionLogEntry, objectstack#7639), behindisRunStateReadandrefuseUngrantedRunRead(the sys_automation_run read grant); its own comment: applying the grant to the screen route "would lock out the end user the flow paused for".interpretFlowResponse's paused arm carries{ kind: 'paused', runId, screen, data },judgeFlowLaunchforwards{ kind: 'screen', runId, screen },FlowRunner'sScreenFlowStateis{ flowName, runId, screen }. Nothing is received and dropped.H2 — producers
visibleWhenover a run variable, in either repo. objectstack example flows with screen nodes:examples/app-crmconvert-lead.flow.ts(a waitForInput message screen and two object-form screens, novisibleWhen),examples/app-showcaseshowcase_reassign_wizard(one flat field, novisibleWhen),examples/app-todotask.flow.ts(visibleWhenappears only in a comment).visibleWhenpredicates over a sibling field: the flows guide (opportunityName,createOpportunity == true), the console preview sample (note,discountabove zero), and the objectstack and lint test fixtures.H3 — the ruling, and what the Studio was doing (M1, M2, measured on the base)
Option C: a screen field's
visibleWhenbinds the same screen's declared fields plus the values being collected — the specScreenFieldSpec.visibleWhendocblock ("Bare CEL over the screen's own field names", "evaluated by the CLIENT … not by the server"). The runner already implements it; the Studio, since PR objectui#10736, read the resume door instead. Measured with the realFlowSimulatorandScreenPreviewon the base704e05b09d(probe file, not committed):noteis absent (buildScreenSpecdropped it; the step note read "Hidden because its visibleWhen could not be evaluated … Unknown variable: discount"); typing5into Discount leavesnoteabsent; the controlcommentis drawn throughout. Docs shape:opportunityNameabsent; ticking the checkbox (data-stategoes to checked) leaves it absent; controlplaindrawn. With the flow variable declaredfalsein the run (as the guide recommends): the predicate evaluatedfalse, the field was dropped, the tick changed nothing. ⇒ frozen, exactly the outcome the runtime'sscreenexecutor forwards the predicate raw to avoid.unevaluableVisibleWhenover the run's variables named the sibling predicates (note,opportunityName) as unevaluable, and did NOT name a genuine run-variable predicate when the run held the variable (needsApproval: true→ nothing reported); withneedsApproval: falseseeded,reasonwas hidden silently.@objectstack/formula17.4.0 exportscollectCelRootIdentifiers,parseCelToAstandnearestName; a comprehension arrives in the AST as anrcallwhose third argument opens with theidit binds; on the runner's seeded scope,createOpportunity == truewithfalseevaluatesfalse, anddiscountabove zero withnullfaults ("no such overload"), whichScreenView's fallback then decides — its direction (objectui#8069), not this card's.Pins — red on the base, green on the head, each with a control in the same tree
Files:
ScreenPreview.debugRunScope-10743.test.tsx(new; realFlowSimulator→ realScreenPreview, mounted as the panel mounts them),ScreenPreview.test.tsx(re-judged, plus the round-2 comprehension and one-rule pins),flow-expr-problems.test.ts(round 2: the Problems panel),simulator/__tests__/flow-simulator.parity-10692.test.ts(re-judged).5revealsnote,0hides it;commentdrawn throughout), docs shape (tick revealsopportunityName;plaindrawn throughout; hint appears then goes), docs shape with the flow variable seededfalse;needsApprovaltrueorfalse(plaindrawn in both), and the screen step names"reason"withneedsApprovalas an error whatever the run holds; the sibling pause carries no error and names no field;unevaluableVisibleWhen: sibling,record.sibling, two siblings, no predicate, blank → not reported; run variable, stdlib call over a run variable,varsroot, brace, non-string → reported by field name; typo → nearest declared field named; comprehension (exists,all, nestedmapandexists_one) over siblings → not reported; an undeclared root inside a comprehension body, and the same name used bare outside one → reported;screenPredicateRoots(node)equals the keys ofscreenPredicateScope(buildScreenSpec(node), {})plusrecord;record.sibling, comprehension over a sibling, no predicate → no problem on the screen; a run variable the flow scope holds →warningon the screen node, "Note:needsApprovalis not a field on this screen"; a{var}brace →errorahead of the scope; control: a decision downstream keeps the FLOW scope —needsApprovaland the screen outputdiscountare clean there, and an unknown root warns with the flow wording ("did you meanneedsApproval");buildScreenSpeckeeps every field with itsvisibleWhen;hiddenFieldCount(spec, values)reads 1 then 0.Red legs, each with the head pins over base sources checked out from the base commit, trap-restored with
git checkout HEAD --, blob hashes re-checked against HEAD andgit diff HEADempty: round 1 over704e05b09d(five source files, three pin files):Tests 24 failed | 35 passed (59); round 2 over39fbfac155(screen-spec.ts,flow-expr-problems.ts;ScreenPreview.test.tsx,flow-expr-problems.test.ts):Tests 8 failed | 37 passed (45)— the four Problems-panel pins, the three comprehension pins and the one-rule pin. Green leg atcd8e9713e7, eight files (the four above plusflow-simulator.test.ts,FlowSimulatorPanel.test.tsx,spec-symbol-parity.test.ts,ScreenView.requiredMarker-10367.test.tsx):Test Files 8 passed (8),Tests 158 passed (158).Ablations (each through
ablation-replace.mjsin wrap mode: anchor hit exactly once, blob changed, tests run, restored with blob equal to HEAD andgit diff HEADempty):toScreenFieldsstops carryingvisibleWhen→ 8 red (the live-reveal pins,keeps every fieldin both files);hiddenFieldCountreturns 0 → 3 red (the two hint pins, the count pin);39fbfac, 9 at head (the comprehension-does-not-hide pin joins) — run-variable andvarsrows in both files, the typo pin, the step-error pin; brace and non-string stay reported by the parse and shape legs;unevaluableVisibleWhen→ 6 red (every step-error row, the debug-run step pin);screenPredicateScopeand keep onlyrecord→ 5 red at39fbfac, 9 at head (the three comprehension rows and the one-rule pin join) — sibling not-reported rows, the sibling pause row, the two live-reveal debug-run pins whose steps now carry an error;isScreenVisibleWhenColumnreturns false, so the panel judges the column over the flow scope → 4 red (the sibling and comprehension no-problem pins, the run-variable pin, the flow-scope control's clean half);collectCelRootIdentifiers→ 4 red (the three comprehension pins and the panel's comprehension pin).Round 3 (head
c06451c56b, prose only)screenPredicateScopeclause now reads the record's text: the Studio reads the bare roots a predicate may name off the renderer's own scope; only therecordnamespace, which the renderer binds insideevalFieldPredicate, is restated by name. Its "Still on the flow scope" paragraph adds "and says nothing on a run variable the other two report". Frontmatter byte-identical: md54e8ce55021fe3b14329c8b4cb3f9f354before and after.cd8e9713e7(git diff cd8e9713e7 HEAD --stat: the one changeset file), so the green and red legs, type-check and eslint above stand as measured there. A3 and A6 re-measured at head over the three round-1 pin files:9 failed | 55 passed (64)each, anchor hit once, restored with blob equal to HEAD.c06451c56b:check-changeset-presence✅,check-changeset-claims✅ (report-only),check-pending-changeset-literals✅,check-changeset-no-major✅,check-changeset-overwrite✅,check-control-bytes✅,check-new-line-citations:VERDICT 0 new citation(s).FlowExprIssue.tsxand the inline cell are untouched; the seat files that follow-up.Gates (at
cd8e9713e7; code unchanged atc06451c56b)turbo run build --filter='@object-ui/app-shell^...' --concurrency=2under the verify lock:Tasks: 28 successful, 28 total,VERDICT command-exit 0(round 2: 28 cached, FULL TURBO).pnpm --filter @object-ui/app-shell type-check(script echoed@object-ui/app-shell@17.6.0 type-check,tsc --noEmit && tsc -p tsconfig.test.json): exit 0 in both rounds;tsc -p tsconfig.test.json --listFileslists the new pin file (1).ScreenView.tsx, react-refresh and no-explicit-any, untouched lines).check-changeset-presence✅ (round 2: 11 published source files, 1 changeset declared);check-control-bytes✅;check-vi-mock-specifiers✅;check-vi-mock-override-shape✅;check-test-path-roots✅;check-pending-changeset-literals✅;check-changeset-claims✅ (report-only);check-new-line-citations:VERDICT 0 new citation(s)at both commits.check-readme-exports— exit 1 on a prerequisite (app-shelldist/index.d.tsabsent, "run pnpm build first", 8 packages unbuilt); the README edits are prose and add no fenced import block (the round-1 CI run of README Export Check was green). The repo-levelpnpm lintand fullpnpm testare CI's runs.Serial
Claim time 05:28Z: no open PR touched
metadata-admin/previews/,ScreenView.tsxorFlowRunner.tsx. Re-checked before each push; round 3: 7 other open PRs (10768, 10760, 10759, 10714, 10278, 8941, 5400 — the release PR across all 1732 files, 18 pages), every page of every file list read by REST: none touches the PR's 14 files,metadata-admin/previews/,inspectors/flow-node-config.ts,ScreenView.tsxorFlowRunner.tsx.origin/mainfetched into the private refrefs/issue-10743/main=b5b6f37531before the round-3 push;git merge-tree --write-treeexit 0 (no commit on main since the base touches a file this PR changes; main moved in 7 commits and 56 files since704e05b09d, and the only ones in a directory this PR touches are six unrelated.changeset/entries and six unrelatedmetadata-admin/files —PermissionMatrixEditor.tsxwith its clone test,i18n.ts,permission-set-clone-dispatch.ts,inspectors/ReportDefaultInspector.tsxwith its test). No merge commit was needed.Acceptance notes
ScreenView's fallback the console sample'snoteis drawn before any discount is typed —discountis seedednull, the comparison faults in CEL, andevalFieldPredicatefalls open — and hides once0is typed. The pins read the live decision after typing, not the initial fallback. The inspector's standalone preview has no step, so a scope fault there is named by the Problems panel (now the right shape) and not inline.error(the renderer cannot evaluate it there), the Problems panel atwarning(the runtime accepts the flow today; objectstack#20178 makes it a refusal). Both are English literals like their neighbouring step notes and the panel's other messages are localized throughengine.flowRef.*; the new sentence is not in the i18n catalogue.findUnknownRefs, the panel's flow-scope checker, allow-listsenv,request,context,user,now,today,self,data; the screen column no longer goes through it, souser == "x"on a screen field is reported — the runner binds none of those.screenFieldsis empty for them);interpolateand the{var}title/description path are untouched.@object-ui/app-shell's index; theScreenViewexport is additive and module-level only.requiredonly when the run's variables make the predicate true, and waives it when they make it false or leave it unevaluable.Generated by Claude Code