Uh oh!
There was an error while loading. Please reload this page.
feat(core,react): declare record.* the row-predicate canon and warn on the two deprecated spellings - #5737
Merged
Merged
Conversation
… on the two deprecated spellings (#5330) Implements the maintainer ruling of 2026-08-20 (option B): the canon is `record.*`; the bare shorthand and `data.*` enter a deprecation window, warned about now and removable only after a stored-metadata survey. The canon states the SERVER's accept set — the ruling's stated first measurement. Measured on `@objectstack/formula@17.1.0`: `buildScope({ record })` mounts exactly `['record']`, so a bare field faults `Unknown variable: status` and `data.*` faults `Unknown variable: data`. The renderer's three-way binding has no server counterpart. `data.*` is lint-silent and runtime-fatal: `data` IS in SCOPE_ROOTS (a generous "never faults" lint baseline, not the runtime accept set), so it passes every authoring gate and then binds nothing — a constant false, which for `visible` is a button that silently never appears. No spelling is removed; no predicate changes verdict. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E7snar5mwF7qoXJazqKhys
#5330) The two tiers are separate evaluation entries and a warning wired into only one misses the surfaces this card is about: the generic action renderers and `record:alert` go through `usePredicateRecordContext` + `useCondition`, not through `evalRowPredicate`. Each tier's pins assert both directions — the binding still resolves all three spellings (no removal before the survey) AND the two deprecated ones now report. Ablation-checked on both tiers: removing the core call turns 3 of 15 red, removing the react call turns 2 of 7 red, and in both cases the "stays silent" pins correctly stay green. `record:alert`'s own three-spelling pins landed with PR #5688 and are not duplicated here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E7snar5mwF7qoXJazqKhys
Contributor
✅ 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
|
This was referenced Aug 23, 2026
os-sam
marked this pull request as ready for review
August 23, 2026 03:23
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 23, 2026
os-sam pushed a commit
that referenced
this pull request
Aug 23, 2026
…e canon (#5738) Phase 0 of the objectui#5330 row-predicate deprecation: stop TEACHING a spelling the Phase-1 warning (PR #5737) now flags. `packages/plugin-form/README.md` illustrated a field-level conditional rule as `requiredWhen: 'status == "scheduled"'` — the bare shorthand. The same README's own table two hundred lines up says these are "CEL predicates over the live record, evaluated by `@objectstack/formula` — the same engine and dialect the server enforces", and on that engine `buildScope({ record })` mounts exactly `['record']`: the bare root faults there with `Unknown variable: status`. `requiredWhen` is one of the two rules enforced client AND server, so this was not merely non-canonical — it was the one arm the server refuses outright, handed to authors as the example. Confirmed with the shipped oracle rather than a regex: the exported `detectNonCanonicalRowSpelling` reports `bare-shorthand → record.status` on the old text and reports nothing on the new, so this cannot disagree with the warning authors are seeing in the console. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E7snar5mwF7qoXJazqKhys
This was referenced Aug 23, 2026
akarma-synetal pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Aug 23, 2026
…e canon (objectstack-ai#5738) (objectstack-ai#5758) Phase 0 of the objectui#5330 row-predicate deprecation: stop TEACHING a spelling the Phase-1 warning (PR objectstack-ai#5737) now flags. `packages/plugin-form/README.md` illustrated a field-level conditional rule as `requiredWhen: 'status == "scheduled"'` — the bare shorthand. The same README's own table two hundred lines up says these are "CEL predicates over the live record, evaluated by `@objectstack/formula` — the same engine and dialect the server enforces", and on that engine `buildScope({ record })` mounts exactly `['record']`: the bare root faults there with `Unknown variable: status`. `requiredWhen` is one of the two rules enforced client AND server, so this was not merely non-canonical — it was the one arm the server refuses outright, handed to authors as the example. Confirmed with the shipped oracle rather than a regex: the exported `detectNonCanonicalRowSpelling` reports `bare-shorthand → record.status` on the old text and reports nothing on the new, so this cannot disagree with the warning authors are seeing in the console. Claude-Session: https://claude.ai/code/session_01E7snar5mwF7qoXJazqKhys Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#5330
Implements the maintainer ruling of 2026-08-20 (option B): the canon is
record.*; the bare shorthand anddata.*enter a deprecation window. No spelling is removed, and none may be before a stored-metadata survey sizes the window.The first measurement: the server's accept set
The ruling made this the dev's first measurement, because the canon statement must state the server's accept set, not the client's. Measured against
@objectstack/formula@17.1.0— the engine the server evaluates with, and the onepackages/core'sfieldRules.tsalready delegates to:buildScope+celEngine)firstUndeclaredReference)record.status{ ok: true, value: true }null)statusUnknown variable: status'status')data.statusUnknown variable: datanull)buildScope({ record })mounts exactly['record']—datais never bound and the row's fields are never flattened to top level. The server acceptsrecord.*and nothing else. The renderer's three-way binding has no server counterpart, which is precisely why warning about it is the client's job.data.*is the dangerous one, and the reason this warning is worth shipping.datais in@objectstack/formula'sSCOPE_ROOTS, so the server's bare-identifier oracle wavesdata.statusthrough — that list is a deliberately generous "never faults" lint baseline, not the runtime accept set. Adata.*row predicate therefore passes every authoring gate the platform has and then binds nothing at runtime: not an error, a constantfalse. Avisiblethat is constantly false is a button that silently never appears — the #4075 fail-closed signature, which is exactly the family this card descends from.The deprecation is layer-scoped, not platform-wide
The card's framing calls
data.*"legacy". The measurement says otherwise:datais the canonical root one layer over, in a metadata-editing form. objectstack'sCANONICAL_ROOT_BY_LAYERreads{ runtime: 'record', metadata: 'data' }(ADR-0089 D3), and objectui's ownapp-shellmetadata-adminSchemaFormbinds{ data: row }on purpose. Declaringdata.*deprecated unqualified would have contradicted ADR-0089 D3 and broken the metadata-editing layer's contract, so the canon here is scoped to the runtime record layer and the detector stands down elsewhere.Where the warning lives, and who sees it
usePredicateRecordContextsees the row but never the predicate text, so it structurally cannot detect a spelling. The detection has to sit where row and text meet — and there are two such places, not one:evalRowPredicate(@object-ui/core) — list/row surfaces, row kebabs, the bulk bar, kanban formatting,page:header.useCondition(@object-ui/react) — the four generic action renderers andrecord:alert, viausePredicateRecordContext.A warning wired into only the first would have missed exactly the surfaces this card is about while reading as covered, so both tiers call one shared detector in
packages/core/src/evaluator/rowPredicateCanon.ts. It surfaces in the dev console at first evaluation — the same channel as the existing legacy-dialect and failed-predicate warnings, and the same channel the sibling #4356 deprecation uses. Production is gated off, matchingdashboard-filters.ts: a deprecation warning that floods a production console is a warning that gets muted.Detection reuses the server's own oracles (
collectCelRootIdentifiers,firstUndeclaredReference) rather than a regex, so no second dialect judgement is invented client-side; an unparseable source is not this module's verdict to give.Three conservative stand-downs, each of which can only remove a report — a false deprecation warning sends an author to rewrite a predicate that was correct:
SCOPE_ROOTSwould otherwise read as a bare field);data.*is reported only whendataactually names the row (arowlessdialog or metadata-editing form is left alone);${…}predicates are never reported — in that dialectdata.*is the correct spelling, anduseCondition's own documented example is'${data.status === "active"}'. This is the case that would have made the warning unshippable if it were wrong.Prose homes
The card's premise named two; there are three, and one was already correct.
packages/plugin-detail/.../record-alert.tsx:40-42already states the canon and cites the 2026-08-20 ruling — written by PR #5688 — so it is untouched. The two that presented the three spellings as peers now declare the canon:packages/core/src/evaluator/listConditional.ts(evalRowPredicate) andpackages/react/src/hooks/useExpression.ts(usePredicateRecordContext, plususeRowPredicate's summary, which taught the shorthand without qualification).What #5688 already covers (not duplicated)
PR #5688 (
8a44390, merged 2026-08-22) touchedplugin-detail/src/renderers/record-alert.tsx, itsrecord-alert.rowBinding.test.tsx, and a changeset. Its three-spelling pins forrecord:alertare not re-pinned here; this PR pins the shared evaluator tiers those renderers sit on, plus the detector itself.Verification
Union re-run on the final commit
a0ba08e:vitest run packages/core/ packages/react/Test Files 148 passed (148)·Tests 2669 passed (2669)type-check(core, react)packages/core type-check: Done·packages/react type-check: Donepnpm lint(whole repo, 47 packages)Tasks: 47 successful, 47 total· 0 errorscheck-control-bytes✅ OK (scanned 4788 tracked text file(s); skipped 85 binary)check-changeset-presence✅ 5 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)Consumer sweep —
plugin-detail+components/renderers/action:104 passed (104)files,1117 passed (1117)tests, confirming the new warning breaks no consumer that asserts onconsole.warn.Lint is the full repo run, not a narrowed one, so no narrowing argument is needed. The 9
no-explicit-anywarnings onuseExpression.tsare pre-existing (9 at base8549453, 9 at head; zeroanyin added lines).Ablation, both tiers. Each mutation was confirmed on disk before the run (call-site count 1→0 plus an injected marker present exactly once), each script carried a
trap … EXIT INT TERMrestore, and each restore leg was verified (marker absent, call site back, tree clean, suite green again). vitest aliases@object-ui/coretopackages/core/src, so no build is in the resolution path for either tier and no rebuild leg applies.In both cases the reds are exactly the positive warning assertions, and the "stays silent" pins correctly stayed green — a warning that is removed cannot make a silence assertion fail.
Scope
Clause ② does not trip: the accept/reject set does not move, the three-way binding is unchanged, and the warn is a warning rather than a refusal. No file outside the declared surface was touched —
content/docs/api/schema-reference.md(fenced to #5667) andpackages/components/src/renderers/basic/**(fenced to #5717) are untouched, as are allusePredicateRecordContextconsumers, which were read but not edited.The survey this warning enables
Removal is Phase 2 and is deliberately not attempted here. The warning is what makes the survey possible at all (ADR-0078 — a tolerance nothing ever reports can never be retired, because nothing would ever show that the last document carrying it is gone). A sizing survey needs to sample, per stored
visible/disabled/enabledpredicate on a runtime record surface: the CEL root each predicate uses (record/data/ bare), bucketed by tenant and by whether the metadata is customer-authored or ships in an example app.detectNonCanonicalRowSpellingis exported precisely so that sweep can run offline over stored metadata rather than waiting for a render.⛔ Phase 0 is not done and is out of scope for this card: objectui's own docs and example apps have not been checked for places that still teach the shorthand. Warning authors while the docs still teach the form is the contradiction users report as a bug — the #4356 precedent shipped Phase 0 in the same PR. Filed separately rather than absorbed here.
Generated by Claude Code
Generated by Claude Code