Skip to content

The record-scope CEL lint refuses the app root, which app-shell's predicate scope DOES bind — @objectstack/formula's SCOPE_ROOTS has no app #8155

Description

@os-justin

Filed unassigned, unlabelled, by the #7727 dev (PM loop R3, session session_01YBWFb5YgMU5dw8p2VKj16S) for PM triage. Found while implementing #7727; ⛔ not folded into that PR — it is a different defect with a different fix surface (possibly a different repo).

What

packages/app-shell/src/views/metadata-admin/celAuthoring.ts delegates its lint to @objectstack/formula's validateExpression. Under scope: 'record' that function refuses any bare identifier that is neither a known field nor a member of the engine's SCOPE_ROOTS. Measured against @objectstack/formula@17.2.0 (the version this worktree resolves), from packages/app-shell:

validateExpression('predicate', "size(ROOT) >= 0", { objectName:'invoice', fields:['status','amount'], scope:'record' })

ACCEPTED : record previous parent input os current_user user vars features ctx data env settings
REFUSED  : app tenant org zzz

and the refusal message for app is, verbatim:

bare reference app — a formula/validation expression binds the record as the record namespace, not at top level, so app resolves to nothing and the expression silently evaluates to null. Write record.app.

SCOPE_ROOTS (same build) reads:

["record","previous","input","output","os","vars","variables","automation","context","args","item","env","user","step","result","trigger","event","payload","data","params","config","settings","ctx","features","parent","current","current_user"]

There is no app in it.

Why it matters

app IS bound at runtime for exactly the surfaces this lint serves. packages/app-shell/src/providers/ExpressionProvider.tsx:88:

export function buildExpressionScope({ user = {}, app = {}, data = {}, features = {} }: ExpressionScopeInput = {}): Record<string, any> {
  return { current_user: user, user, ctx: { user }, os: { user }, app, data, features };
}

That bag becomes the PredicateScopeProvider scope, which plugin-grid's ObjectGrid.tsx:2086 and plugin-list's ListView.tsx:639 hand to resolveConditionalFormatting / evalRowPredicate. So app.name == 'crm' in a row predicate resolves and evaluates.

The gap was invisible while every row-predicate authoring site linted in the flattened scope, because flattened accepts ANY bare identifier — measured, size(zzz) >= 0 lints clean there. #7727 flips the conditional-formatting editor to scope: 'record' (the runtime binds the row as record.* only since #5741 Phase 2), and that is what makes the gap reachable:

An editor that suggests a root and then refuses it is the declared-vs-enforced shape in the other direction, and it is user-visible.

Fix shape (for triage, not ruled)

Two candidate directions, and they land in different repos — this is the part that needs a decision, not code:

  • A — producer side (objectstack). Add app to @objectstack/formula's SCOPE_ROOTS. Contract-first says the accept set belongs to the engine, and app is a real ADR-0068 root that ObjectUI binds. Cost: the engine's root vocabulary grows for every consumer.
  • B — consumer side (objectui). Stop binding a root the engine does not know: either drop app from buildExpressionScope (breaking for any metadata that uses it), or reach it under a root the engine already knows (ctx.app / os.app), and drop app from ROW_PREDICATE_ROOTS.

⛔ Whichever wins, the wrong answer is a consumer-side suppression of the diagnostic in celAuthoring.ts — that is the lenient-fallback shape AGENTS.md #0.1 bans.

Interim state on main after #7727: the collision is pinned by a characterization test in ConditionalFormattingEditor.test.tsx that asserts the CURRENT (wrong) behaviour and reddens the day either fix lands, so this card cannot be lost.

Related

os has the mirror-image shape and is filed separately — bound by buildExpressionScope, accepted by the engine, but not advertised.

Dedup

search_issues (REST is 403 from this seat class) with a control query that returned #7727 itself in the same session. Nearest hits, none covering this: #7431, #6487, #2413. No open card names the app root against SCOPE_ROOTS.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpackage: app-shellpm:queuepriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions