Skip to content

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

Closed

Description

@objectstack-fleet

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.celScope spreads the flow's variables, then binds vars (the variables namespace) and current_user after the spread.
  • seedRunVariables binds previous on every run.
  • objectstack's build-time judge states the same set. packages/lint/src/flow-cel-root-scope.ts:184 (objectstack 09dd53719) declares ENGINE_BOUND_ROOTS = ['previous', 'vars', 'current_user'].
  • The spec carries FLOW_SCOPE_CLAIMED_IDENTIFIERS = {vars, current_user} in packages/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.id and os.user.id each fail a run with Unknown variable, because flow CEL binds the run's user as current_user only.
  • 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

  1. For the CEL roles, accept previous, vars and current_user as roots, with a comment that points at objectstack's ENGINE_BOUND_ROOTS. The CEL roles are an edge condition, a node condition, a decision branch expression, a screen field's visibleWhen and the CEL value envelopes.
  2. In the CEL roles, report user as an unknown root and suggest current_user, as the objectstack lint does.
  3. Leave the template and hole roles' allow-list as it is. Those slots bind through the text-slot rule, which is not measured here.
  4. Leave the other entries of RUNTIME_GLOBALS as they are on this card.

Acceptance

  1. HotCRM's campaign_enrollment opens in Studio with no vars warning on e4 or e7. Show it with a screenshot of the Problems panel. The separate decision warning on check_campaign_open belongs to HotCRM and may still show.
  2. Unit tests beside flow-ref-check.ts:
    • In a CEL role, vars.x, vars["$error"], current_user.id and previous report nothing.
    • In a CEL role, user.id reports user with suggestion current_user.
    • CONTROL: an undeclared foo.bar is still reported.
    • The existing template-role tests stay green.

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 visibleWhen cell 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.x in 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

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    on Oct 15, 2026
  2. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade · priority:p2 · pm:queue · lane kept domain:ui · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T14:00Z

    Read 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 binds vars, current_user and previous, and not user.

    • So HotCRM's shipped campaign_enrollment guards (has(vars.campaignRecord) …) are flagged twice as "not a reference in scope", although they run.
    • A guard reading 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:

    • The allow-list follows the engine's bound roots.
    • Prefer one source: objectstack's packages/lint/src/flow-cel-root-scope.ts declares ENGINE_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.
    • Pins:
      • vars.x, current_user.x and previous.x pass;
      • user.x is flagged;
      • CONTROL: an undeclared variable is still flagged.
  3. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01XVp19E1MYUTFM3dnkwz8gB
    Account: baozhoutao (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-12499-flow-cel-roots
    Worktree: objectui-issue-12499
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: packages/app-shell/src/views/metadata-admin/inspectors/flow-ref-check.ts (the runtime-roots allow-list at :40 and its CEL-role use at :176 on main 1c3b0699a), 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/objectui reads 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

  4. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  5. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: ACCEPT — objectui PR #12535 · domain:ui seat 2 · session_01XVp19E1MYUTFM3dnkwz8gB · 2026-10-15T15:11Z

    Reviewed on GitHub at head 5b1130e, against the card's Fix and Acceptance, triage (6193910490) and the claim (6194411130), not against the dev's report.

    • Shape: the PR is a draft against main. Its body opens Fixes #12499, the only closing keyword, followed by Clause-②: no. 3 files.
    • The fix, in the CEL roles only (predicate, value):
      • findUnknownRefs accepts previous, vars and current_user through FLOW_CEL_ENGINE_BOUND_ROOTS.
      • It reports user as unknown with the suggestion current_user.
      • Declared flow roots are checked first, so a flow variable that is actually named user still passes.
      • The template and hole roles and the other RUNTIME_GLOBALS entries are unchanged, as the card's Fix 3–4 require.
    • One source, measured: neither set is exported at objectui's pinned versions. @objectstack/spec 18.0.0-next.1's barrel and /automation carry no FLOW_SCOPE_CLAIMED_IDENTIFIERS, and @objectstack/lint's ENGINE_BOUND_ROOTS is a module-local const. The set is therefore a mirror whose comment names packages/lint/src/flow-cel-root-scope.ts, with a member pin, as triage allowed. The module is internal to app-shell, so Clause-②: no holds.
    • Card mismatch, measured: a screen node's fields[].visibleWhen is not judged by flow-ref-check. It routes to screenVisibleWhenScopeError (previews/screen-spec.ts), whose rule binds only declared screen fields and deliberately leaves out vars. That slot is therefore unchanged, and the dev did not widen the PR to it.
    • Pins and ablation: vars.x, vars["$error"], current_user.id and previous report nothing. user.id reports user with the suggestion current_user. CONTROL: an undeclared foo.bar is 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.
    • Changeset: @object-ui/app-shell: patch.
    • Evidence: the closure build and the app-shell type-check exit 0. metadata-admin/inspectors/ gives 2190 passed and 1 skipped under the lock, and the battery exits 0.
    • Acceptance note (not filed): the card's Acceptance 1, a screenshot of HotCRM's campaign_enrollment Problems panel, was not taken because HotCRM is not in the dev's container. It is the maintainer's browser check.
    • CI at head: 34 success, 3 skipped, 0 failures (filter=latest, 37 runs), all 16 test shards included.

    Next: pr_ready + automerge_enable.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:ui seat 2 · session_01XVp19E1MYUTFM3dnkwz8gB · 2026-10-15T15:26Z

    • Merged: PR fix(app-shell): Studio flow CEL checker follows the engine-bound roots (vars, current_user, previous; user flagged) #12535 merged through the merge queue as main 2bacbf0. The card closed by the PR's Fixes #12499, which is the only closing keyword.
    • Verified by content: on origin/main, all 3 files the PR touches are blob-identical to the accepted head 5b1130e. They are flow-ref-check.ts, its test and .changeset/12499-flow-cel-roots.md.
    • Acceptance 1, the HotCRM campaign_enrollment Problems-panel reading, is the maintainer's browser check.
    • Label: pm:dispatched is removed in the same stroke as this comment.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portalbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions