Filed unassigned by the `domain:metadata` seat while implementing the #8990 remainder (PR #9280, session `17d48cbe-d3e0-562b-95a3-e2653a9bd9fb`). Out of scope there: that card migrates predicates; this is a claim in the surrounding prose that I could not confirm and had no mandate to rewrite. Backlink: #8990. ## The claim `examples/app-showcase/src/ui/actions/predicate-matrix.action.ts`, in the authoring-rules header: > - **The RECORD HEADER does not speak CEL** (objectui#3521). It evaluates header-action > predicates on objectui's legacy JS evaluator, so `.size()`, `.contains()`, > `.matches()`, the `in` operator and stdlib calls like `today()` all THROW there and > fail-closed hide the action, while the same predicate is correct in the row kebab and > the selection bar. Every gate below that uses one of those is therefore absent from > the `⋯` menu today and present in the list … `ZooDialectSplitAction` is declared as the live one-screen demonstration of that split, and its doc says "When #3521 lands, this action should appear in the header too — that is the test." ## Why it looks stale Read at the sha this repo actually pins (`.objectui-sha` = `665661ab093263f39f2e660a295ea615dbcee35a`): - `packages/core/src/actions/ActionEngine.ts` — `getActionsForLocation` normalizes through the shared `toPredicateInput` and passes a `{ dialect: 'cel', source }` envelope **INTACT**, with its own comment describing the demotion as the bug that was fixed: > Hand-rolling the envelope unwrap here is what dropped `dialect: 'cel'` and demoted the > predicate to the legacy JS engine; `toPredicateInput` keeps the envelope so > `evaluateCondition` can route it to `@objectstack/formula` … (objectui#3314) - `packages/core/src/evaluator/ExpressionEvaluator.ts` — `evaluateCondition` reroutes an explicit `{ dialect: 'cel' }` envelope to the canonical engine (objectui#2661). - `RecordDetailView` renders `record_header` / `record_more` through that same `ActionEngine`. And the input really is an envelope: in `examples/app-showcase/dist/objectstack.json`, **113 of 114** record-scoped `visible`/`disabled` values are `{dialect,source}` objects, because `defineAction` normalizes the CEL shorthand at build time. (The single bare string is a page-block predicate — that is #9167's surface, not this one.) So on the pinned objectui, a header action's predicate appears to reach `@objectstack/formula`, where `.contains()` / `.size()` / `in` / `today()` all evaluate normally — i.e. the split the header bullet describes, and the fixture that demonstrates it, may both be describing pre-#3314 behaviour. ## What is NOT established ⚠️ I did not observe this in a browser — the #8990 session had no preview tooling, and `/_console/` was not built in that worktree. Everything above is source-reading at the pin plus artifact inspection. That is exactly why this is filed rather than fixed: the fixture's whole value is that it is browser-verified, so correcting its prose from a code read would replace one unverified claim with another. Two things also genuinely argue for caution: - `objectui#3521` does not appear in the pinned sha's log, so the issue may still be open for a reason I cannot see from this side. - A separate raw probe of `SafeExpressionParser` at the pin shows `.contains()` and `.size()` returning `undefined`/`false` rather than THROWING, while `in` throws `SyntaxError` — so the bullet's "all THROW there" is not an accurate description of that evaluator either, whichever engine the header ends up using. ## Suggested first step (cheap, and it is a measurement, not a rewrite) Boot the showcase, open a **Specimen — Full** Field Zoo detail page, and look at the `⋯` menu for `showcase_zoo_dialect_split` ("CEL-only: contains()"): - **present** ⇒ the header speaks CEL at the pinned build; the bullet and `ZooDialectSplitAction`'s framing are stale and should be rewritten to say so, and objectui#3521 should be re-checked for closure. - **absent** ⇒ the bullet is still accurate and something other than `ActionEngine` evaluates that surface; worth finding out what, because it would also mean the `has()`-guarded predicates PR #9280 just landed are hidden from the header, which is the one risk that PR names and could not close. The second branch is the one that matters — it is the only outcome under which #9280 regressed anything, which is why this is worth a browser round rather than a shrug.