Repository navigation
Studio flow checker: a flow CEL guard reading vars.x or current_user.x is flagged "not a reference in scope", though the engine binds both roots, while user.x, which the engine does not bind, passes silently #12499
Description
Filing gate: ① — a product defect with a named location, reproduced in the Studio Problems panel on shipped HotCRM metadata.
Symptom
HotCRM's flow campaign_enrollment has two edges, e4 and e7, out of the decision check_campaign_open. Both guards begin has(vars.campaignRecord) && has(vars.campaignRecord.status) && ….
Measured on HotCRM 63db83e in a local console, at /studio/app.objectstack.hotcrm/automations?surface=flow:campaign_enrollment (1440 × 900):
- The Problems panel lists this warning twice, once per edge:
`vars` is not a reference in scope at this step. - Each of the two edges carries a canvas warning mark with the same text.
- The edge inspector repeats the warning inline under the Condition field.
reach: the Studio Automations pillar's Problems panel (a UI door), measured once as above.
Cause
packages/app-shell/src/views/metadata-admin/inspectors/flow-ref-check.ts:40 (objectui 03cf7b2) allow-lists these runtime roots:
const RUNTIME_GLOBALS = new Set(['env', 'request', 'context', 'user', 'now', 'today', 'self', 'data']);findUnknownRefs consults it at :176. The engine's flow CEL scope is a different set.
The engine binds roots the list lacks.
- objectstack's
AutomationEngine.celScopespreads the flow's variables, then bindsvars(the variables namespace) andcurrent_userafter the spread. seedRunVariablesbindspreviouson every run.- objectstack's build-time judge states the same set.
packages/lint/src/flow-cel-root-scope.ts:184(objectstack09dd53719) declaresENGINE_BOUND_ROOTS = ['previous', 'vars', 'current_user']. - The spec carries
FLOW_SCOPE_CLAIMED_IDENTIFIERS = {vars, current_user}inpackages/spec/src/automation/flow-template-token.ts:174. The package barrel does not export it, so objectui mirrors the set with a pointer rather than importing it.
The list carries a root the engine does not bind.
- The same lint module records that
user.id == "u1",ctx.user.idandos.user.ideach fail a run withUnknown variable, because flow CEL binds the run's user ascurrent_useronly. - Studio allow-lists
user, so that case passes the checker silently.
Effect. The guard an author is steered towards for a sparse output is flagged: has(vars.x), the totality pattern HotCRM's comments document, and vars["$error"] for a $-named variable. A root that fails at run time is not flagged.
Fix
- For the CEL roles, accept
previous,varsandcurrent_useras roots, with a comment that points at objectstack'sENGINE_BOUND_ROOTS. The CEL roles are an edgecondition, a nodecondition, a decision branchexpression, a screen field'svisibleWhenand the CEL value envelopes. - In the CEL roles, report
useras an unknown root and suggestcurrent_user, as the objectstack lint does. - Leave the template and
holeroles' allow-list as it is. Those slots bind through the text-slot rule, which is not measured here. - Leave the other entries of
RUNTIME_GLOBALSas they are on this card.
Acceptance
- HotCRM's
campaign_enrollmentopens in Studio with novarswarning one4ore7. Show it with a screenshot of the Problems panel. The separate decision warning oncheck_campaign_openbelongs to HotCRM and may still show. - Unit tests beside
flow-ref-check.ts:- In a CEL role,
vars.x,vars["$error"],current_user.idandpreviousreport nothing. - In a CEL role,
user.idreportsuserwith suggestioncurrent_user. - CONTROL: an undeclared
foo.baris still reported. - The existing template-role tests stay green.
- In a CEL role,
Who acts
Triage grades and routes it. The fix reader is the domain:ui lane that owns the Studio flow designer. It is not part of epic #12446: that epic restyles the Automations pillar, and the checker's logic is outside its cards.
Dedupe (2026-10-15, MCP search_issues on objectstack-ai/objectui, open and closed)
- "flow checker vars is not a reference in scope at this step false positive": 1 hit, objectui#10772 (closed). It is about the
visibleWhencell offering the flow scope, not this defect. - "flow-ref-check RUNTIME_GLOBALS vars current_user edge condition warning Studio": 6 hits, none this defect. They are the same family with different roots, all closed:
- objectui#11085: a connector action's outputs, and an edge guard's source-node outputs;
- objectui#11789:
record.xin an entry condition; - objectui#10538: a comprehension macro's bound variable.
Filed by seat domain:ui#4 (session session_01SJn1GEkrbbBQkhGC26wEDw) from a Studio walk of HotCRM requested by the maintainer.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 15, 2026
Triage: first grade ·
priority:p2·pm:queue· lane keptdomain:ui· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T14:00ZRead fresh. Studio's flow reference checker allow-lists
RUNTIME_GLOBALS = ['env', 'request', 'context', 'user', 'now', 'today', 'self', 'data'](flow-ref-check.ts:40). The engine's flow CEL scope bindsvars,current_userandprevious, and notuser.campaign_enrollmentguards (has(vars.campaignRecord) …) are flagged twice as "not a reference in scope", although they run.user.x, which the engine never binds, passes silently. Measured in the Problems panel.Grade p2: the checker is wrong in both directions on shipped metadata. A false warning teaches authors and AI to ignore the panel, and a false pass hides a guard that can never be true.
Lane
domain:ui(the Studio inspector).Scope:
packages/lint/src/flow-cel-root-scope.tsdeclaresENGINE_BOUND_ROOTS. If spec or lint exports it to objectui's pinned version, read it from there; otherwise mirror it with a pin that names that file.vars.x,current_user.xandprevious.xpass;user.xis flagged;objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 1
Session:
session_01XVp19E1MYUTFM3dnkwz8gBAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12499-flow-cel-rootsWorktree:
objectui-issue-12499Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/app-shell/src/views/metadata-admin/inspectors/flow-ref-check.ts(the runtime-roots allow-list at:40and its CEL-role use at:176onmain1c3b0699a), the tests beside it, and one.changeset/12499-*.md(stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuireads no path-derived mandate)Clause-②: no
Responsibility:
objectui's Studio flow reference checker, whose RUNTIME_GLOBALS allow-list differs from the engine's flow CEL scope | objectstack's packages/lint flow-cel-root-scope.ts already declares ENGINE_BOUND_ROOTS at build time; Studio does not follow it | every Studio author whose flow guards read vars.x or current_user.x (false warning, measured on HotCRM campaign_enrollment) or user.x (false pass)Thread-read: 6193910490
Serial constraints cleared: no in-flight claim or open PR touches
flow-ref-check.ts. PR #12481 (objectui#12454, the epic's Automations pillar) restyles the pillar and does not touch the checker. objectui#12493 (console/ai/) and objectui#12474 (ObjectView.tsx), both this seat's, are disjoint.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 12499,
"status": "done",
"branch": "claude/issue-12499-flow-cel-roots",
"pr": "#12535",
"session": "session_01XVp19E1MYUTFM3dnkwz8gB",
"premise_still_valid": true,
"summary": "findUnknownRefs in flow-ref-check.ts now accepts previous, vars and current_user in the CEL roles (predicate, value) through a new exported FLOW_CEL_ENGINE_BOUND_ROOTS. It also reports user there with suggestion current_user; a flow variable named user still passes. Template and hole roles and the other RUNTIME_GLOBALS are unchanged. I measured both possible imports, and neither is exported at the pinned versions (@objectstack/spec 18.0.0-next.1 barrel and /automation have no FLOW_SCOPE_CLAIMED_IDENTIFIERS; @objectstack/lint has no ENGINE_BOUND_ROOTS, which is a module-local const), so the set is a mirror that names packages/lint/src/flow-cel-root-scope.ts, with a member pin. Ruling mismatch: a screen node's fields[].visibleWhen is never judged by flow-ref-check. FlowExprIssue and the Problems panel send it to screenVisibleWhenScopeError (previews/screen-spec.ts, outside the file surface), whose rule binds only declared screen fields and deliberately leaves out vars, so that slot is unchanged. Acceptance 1 (the HotCRM screenshot) was not taken because HotCRM is not in this container; the Problems-panel reading is left to the seat or maintainer.",
"tests": "At head 5b1130e: (1) vitest run on flow-ref-check.test.ts: 29 passed. (2) Ablation after the fix was committed: scripts/ablation-replace.mjs (objectstack) swapped the line 'if (cel && FLOW_CEL_ENGINE_BOUND_ROOTS.has(root)) continue;' for a marker; on disk the marker count went 0->1 and the anchor count 1->0, and the run read 'Tests 3 failed | 26 passed (29)' (both 'reports nothing' cases plus CONTROL). The restore leg read blob == HEAD 42ca4f058b70 with git diff HEAD empty. No dist is involved: the test imports src directly. (3) Under os-verify-lock, chained with &&, VERDICT command-exit 0: turbo build --filter='@object-ui/app-shell^...' --concurrency=2 ran 28/28 tasks; pnpm --filter @object-ui/app-shell type-check (tsc --noEmit plus tsconfig.test.json, the project that compiles tests) passed; vitest run packages/app-shell/src/views/metadata-admin/inspectors/ read 'Test Files 161 passed (161) Tests 2190 passed | 1 skipped'. (4) Narrowed eslint --no-inline-config --format json on the 2 changed files: exit 0, 2 files, 0 errors, 0 warnings; eslint.config.js has no parserOptions.project, so linting is not type-aware and untouched files are invariant. (5) check:control-bytes, check:spec-symbols, check:unreferenced-sources, check-changeset-fixed, check-changeset-presence, check-changeset-no-major: all exit 0. check-hand-rolled-comment-mask was not run because the new tests read no source. CI: in_progress at report time.",
"gates": {"closure_build": 0, "app_shell_type_check": 0, "vitest_inspectors_dir": 0, "vitest_touched_file": 0, "eslint_narrowed": 0, "check_control_bytes": 0, "check_spec_symbols": 0, "check_unreferenced_sources": 0, "check_changeset_fixed": 0, "check_changeset_presence": 0, "ci": "in_progress"},
"files_changed": ["packages/app-shell/src/views/metadata-admin/inspectors/flow-ref-check.ts", "packages/app-shell/src/views/metadata-admin/inspectors/flow-ref-check.test.ts", ".changeset/12499-flow-cel-roots.md"],
"line_budget": "not applicable (no skills/** or governed surface touched)",
"deviations": ["The screen fields[].visibleWhen slot in ruling Fix 1 was not changed: it does not route through flow-ref-check, and its rule (screen-spec.ts, outside the file surface) deliberately leaves out vars. Reported here, not widened.", "No labels written: nothing in the order named any, and in objectui no gate reads labels.", "dispatch-gates.mjs is objectstack-only, so the gate list was taken from the order and checked by hand."],
"mcp_calls": "0",
"api_writes": "2 — fleet-write relay: pr_create (POST pulls, with the assignee leg POST issues/12535/assignees), and this os-dev-report comment (POST issues/12499/comments); also 3 git pushes of the branch, which are not REST",
"open_questions": [],
"out_of_scope_findings": ["carrier: none (承接者:无) · @objectstack/spec does not export FLOW_SCOPE_CLAIMED_IDENTIFIERS from its barrel or from /automation, and @objectstack/lint keeps ENGINE_BOUND_ROOTS module-local, so objectui has to mirror the flow CEL root set. Observation only, not filed; recorded in the PR's Acceptance notes. Dedupe words: FLOW_SCOPE_CLAIMED_IDENTIFIERS export, ENGINE_BOUND_ROOTS export, flow CEL root scope mirror"]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Review: ACCEPT — objectui PR #12535 ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-15T15:11ZReviewed on GitHub at head
5b1130e, against the card's Fix and Acceptance, triage (6193910490) and the claim (6194411130), not against the dev's report.main. Its body opensFixes #12499, the only closing keyword, followed byClause-②: no. 3 files.predicate,value):findUnknownRefsacceptsprevious,varsandcurrent_userthroughFLOW_CEL_ENGINE_BOUND_ROOTS.useras unknown with the suggestioncurrent_user.userstill passes.holeroles and the otherRUNTIME_GLOBALSentries are unchanged, as the card's Fix 3–4 require.@objectstack/spec18.0.0-next.1's barrel and/automationcarry noFLOW_SCOPE_CLAIMED_IDENTIFIERS, and@objectstack/lint'sENGINE_BOUND_ROOTSis a module-local const. The set is therefore a mirror whose comment namespackages/lint/src/flow-cel-root-scope.ts, with a member pin, as triage allowed. The module is internal to app-shell, soClause-②: noholds.fields[].visibleWhenis not judged byflow-ref-check. It routes toscreenVisibleWhenScopeError(previews/screen-spec.ts), whose rule binds only declared screen fields and deliberately leaves outvars. That slot is therefore unchanged, and the dev did not widen the PR to it.vars.x,vars["$error"],current_user.idandpreviousreport nothing.user.idreportsuserwith the suggestioncurrent_user. CONTROL: an undeclaredfoo.baris still reported, and the template-role tests are unchanged. Removing the bound-roots line fails 3 pins (both "nothing" cases and the CONTROL). Restore was proven by blob hash.@object-ui/app-shell: patch.metadata-admin/inspectors/gives 2190 passed and 1 skipped under the lock, and the battery exits 0.campaign_enrollmentProblems panel, was not taken because HotCRM is not in the dev's container. It is the maintainer's browser check.filter=latest, 37 runs), all 16 test shards included.Next:
pr_ready+automerge_enable.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landed ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-15T15:26Zmain2bacbf0. The card closed by the PR'sFixes #12499, which is the only closing keyword.origin/main, all 3 files the PR touches are blob-identical to the accepted head5b1130e. They areflow-ref-check.ts, its test and.changeset/12499-flow-cel-roots.md.campaign_enrollmentProblems-panel reading, is the maintainer's browser check.pm:dispatchedis removed in the same stroke as this comment.Generated by Claude Code