Skip to content

fix(plugin-detail): bind record:alert predicates through usePredicateRecordContext - #5688

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-4807-record-alert-predicate-binding
Aug 22, 2026
Merged

fix(plugin-detail): bind record:alert predicates through usePredicateRecordContext#5688
os-sales merged 2 commits into
mainfrom
claude/issue-4807-record-alert-predicate-binding

Conversation

@os-sales

@os-salesos-sales commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Fixes#4807

renderers/record-alert.tsx was the last predicate face in the repo still handing useCondition a root-only { record } bag. Every other row-scoped predicate — the four generic action renderers (#4075) and app-shell's DeclaredActionsBar (#4077) — binds the row through the shared usePredicateRecordContext(record), which resolves the three spellings #5330 ruled on: canonical record.status, the deprecated row-action shorthand status, and deprecated legacy data.status.

The card was unblocked by the B ruling on #5330 (maintainer, 2026-08-20): canon is record.*; the shorthand and data.* are deprecated but kept behind a survey-sized window. So all three must resolve, and the pins name record.* as the canon.

Note on spelling in this description, corrected after measuring it: the first revision of this body was posted through the MCP GitHub tool and came back with two angle-bracket fragments gone — the JSX element names and a <pkg> path placeholder — even though both sat inside backticks. Re-posting the same text through the raw REST API preserves them, so the stripping is on the MCP tool path rather than GitHub's store. The bare spellings below (ActionButton, and * for path placeholders) are kept because they survive either path; the renderer's own source comment is unaffected and still carries the angle-bracketed names.

The defect, measured before it was fixed

Under the root-only bag only the canonical spelling reached the row, and the two others failed in opposite directions — both silently, because this call site is fail-soft:

spellingwhat happenedverdict
record.status == 'in_review'resolvedcorrect on both rows
status == 'in_review'status unbound → evaluator throws → the legacy ${…} path answers a throw with its own source text, a truthy stringSHOWN on every row — the author's gate never once hid the banner
data.status == 'in_review'no throw at all: app-shell's ambient scope (providers/ExpressionProvider.tsx) carries data: {}, so the predicate read that object and compared undefinedHIDDEN on every row

The issue predicted a fail-soft SHOWN for both deprecated spellings. That held for the shorthand; the data.* half is the opposite polarity, for the reason in the table, and the changeset and the test header record it that way rather than the way the card guessed.

The change

Three lines of code:

+ usePredicateRecordContext,
+ const predicateRecord = usePredicateRecordContext(record);
+ const passesPredicate = useCondition(predicateInput, predicateRecord);

Everything else in record-alert.tsx is comment. The header block (the Visibility model section the card calls out as untrue) claimed the predicate evaluated "against the same scope … record, user, objectName, features" as "every ActionButton / ActionBar". Three things were wrong with that and all three are now stated truthfully: the row binding was not the shared one, objectName was never in the predicate scope at all (it is read from useRecordContext() for the metadata lookup and the dismiss key), and the fail-soft error policy — the reason this was user-visible rather than a console line — went unmentioned. The rewritten block also names the second gate one tier up (SchemaRenderer's node chain, with its own deliberate data = adapter binding) and points at the two files that pin the composition.

Evidence

Red-first, through the real render path (SchemaRenderer → registry → RecordAlertRenderer), asserting on what a user sees — is the banner's text in the document — never on computed styles or a predicate's return value.

Unfixed tree — 3 failed | 5 passed (8), and the three failures are exactly the three the fix moves:

× group B > row-action shorthand - "onOtherRow": false + "onOtherRow": true
× group B > legacy `data.*` - "onMatchingRow": true + "onMatchingRow": false
× group C > row-action shorthand - "onOtherRow": false + "onOtherRow": true

Fixed tree — Test Files 2 passed (2) / Tests 16 passed (16): the 8 new pins plus the 8 pre-existing pins in record-alert.visibleWhen.evidence.test.tsx, which is the acceptance baseline #5454 named. Those 8 are untouched and stay green, which is what makes this additive rather than a migration.

Non-vacuity

record:alert has four return null paths (dismissed, empty record, its own props gate, and the node gate above it), so "nothing rendered" is not by itself "the gate said no". Group A is the control: it proves the harness paints the banner, and that this exact channel can hide it in both directions. Both group-A assertions pass on the unfixed code, so the three failures above are a defect and not a harness that renders nothing.

Ablation — two legs, predicted before running

No rebuild leg exists or is needed: the root vitest.config.mts aliases every @object-ui/* specifier at packages/*/src, and the suite imports the registration relatively (import '../../index'), so nothing under any dist/ is on the resolution path. Each leg mutated one line, counted the deleted and the injected text separately on disk (grep -oF … | wc -l — occurrences, not lines; the mutator aborts unless its anchor matches exactly once), ran the pins, then restored from the committed blob and verified the restore by hash. Both legs ran under trap … EXIT INT TERM, and the fix was committed first so the restore could not be confused with an uncommitted edit.

legmutation (on disk: deleted 1→0, injected 0→1)predictedobserved
1 — revert the fixuseCondition(predicateInput, predicateRecord)useCondition(predicateInput, { record })the 3 defect pins red, every control and canon pin green, the pre-existing evidence suite untouched3 failed | 13 passed (16) — exactly those 3
2 — renderer paints nothingif (dismissed) return null;if (dismissed || true) return null;every assertion expecting the banner ON SCREEN goes red, the pure "hidden" ones survive13 failed | 3 passed (16) — the 3 survivors are record:path assertions, a different renderer

Leg 2 is the vacuity probe: it shows the group-A controls can fail, so their passing in leg 1 is a measurement.

Restore verified both times: worktree blob 950f6969f272f4bce125907235baae62f24b6522 == HEAD:packages/plugin-detail/src/renderers/record-alert.tsx. Neither leg was void — both anchors matched exactly once, both mutations were confirmed present on disk before the run and absent after it.

Verification scope — a derived superset, not a package sweep

All runs below are on the branch head 02d9d173d, with a clean working tree.

The changed module has exactly one importer in the repo (packages/plugin-detail/src/index.tsx, which registers it as record:alert and the alert alias) and exactly one changed behaviour: the context bag handed to useCondition inside RecordAlertRenderer. A test can therefore observe this change only by mounting that renderer, which requires the registry key or a direct import. A repo-wide grep over all file types (not just tests, so JSON/TS fixtures are covered) for record:alert, RecordAlertRenderer and type: 'alert' enumerates every file that can do so; the indirect emitters (synth/buildDefaultPageSchema.ts, previews/PagePreview.tsx) are included through their own test files.

That superset: Test Files 23 passed (23) / Tests 398 passed (398) in 61s — the plugin-detail renderer and synth suites, the four app-shell metadata-admin preview/inspector tests that mention record:alert, PagePreview.test.tsx, and the four apps/console tests that mount record blocks. The whole-package fallback (92 files) was not needed.

  • pnpm --filter @object-ui/plugin-detail type-checkexit 0, after building the dependency closure (pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-detail^...' build, exit 0). The run echoes @object-ui/plugin-detail@17.6.0 type-check, so it is not a zero-match no-op.
  • check:control-bytes✅ OK (scanned 4736 tracked text file(s); skipped 85 binary)
  • changeset:check✅ No changeset declares a 'major' bump. (minor, per the fixed-group policy)
  • check:self-import✅ No package names itself inside its own src/.
  • check:phantom-deps✅ Every in-scope import is declared by the package that publishes it.
  • check:spec-symbols✅ spec symbol derivation: 1291 files scanned…
  • check:action-forward-parity — exit 0
  • check:doc-types✅ Every documented component type is registered.
  • node scripts/check-changeset-presence.mjs✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)

Every exit code above was captured before any pipe, and each line quoted is the gate's own verdict line rather than a $? this PR wrote.

Lint: a declared narrowing, with its three pieces of evidence

The repo-wide ESLint scan is CI's run. What is delivered here is a narrowed one that is a measurement, not a sample:

  1. Population — read from ESLint's own config, not guessed: eslint.config.js sets no parserOptions.project and no projectService, so type-aware linting is not enabled; and no rule in eslint-rules/ reads the filesystem (readdirSync / globSync / readFileSync: zero hits). ESLint's verdicts are therefore per-file and independent of the rest of the tree.
  2. File count — from --format json: 2 files linted, exit 0. record-alert.rowBinding.test.tsx 0 errors / 0 warnings; record-alert.tsx 0 errors / 8 warnings.
  3. Invariance for untouched files — with no type-aware rule and no cross-file rule, this diff cannot move any untouched file's verdict.

The 8 warnings are all pre-existing @typescript-eslint/no-explicit-any on lines this PR did not touch: the diff adds zero lines containing any (git diff origin/main … | grep '^+' | grep -c 'any'0), and its only non-comment additions are the three lines quoted above.

Out of scope, filed not fixed

#5687SchemaRenderer's node visibility gate resolves a data.* predicate against the data-source adapter, so a hoisted properties.visible written that way hides the block on every row, without a throw and therefore without the #5454 diagnostic. That is one tier up from this card, on a shared surface, and whether #5330's ruling reaches that tier is a real contract question (binding the row over data there would change ${data.*} interpolation in props bags, which SchemaRenderer's own docblock calls out as deliberate). It is why group C of the new suite covers the canon and the shorthand end-to-end but deliberately does not assert data.* through the plain authored node — the test header says so in place of a pin, so nobody reads group B as "data.* works everywhere". #5687 is not addressed here.


Generated by Claude Code


Generated by Claude Code

Reproduces objectui#4807 through the real render path before touching the
renderer: `record:alert`'s `properties.visible` is evaluated on a root-only
`{ record }` bag, so the two spellings objectui#5330 kept (row-action
shorthand and legacy `data.*`) never reach the row.
Measured on the unfixed tree — 3 failed / 5 passed:
- shorthand `status == 'in_review'`: unbound -> throws -> this fail-soft
call site answers SHOWN, on both rows (`onOtherRow: true`).
- legacy `data.status == 'in_review'`: does NOT throw — app-shell's ambient
`data: {}` answers instead of the row, so the comparison is a constant
false and the banner never appears (`onMatchingRow: false`).
- canon `record.*` and both non-vacuity controls pass, which is what makes
the three failures a defect rather than a harness that renders nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK
…RecordContext
`renderers/record-alert.tsx` was the last predicate face still handing
`useCondition` a root-only `{ record }` bag, so only the canonical `record.*`
spelling reached the row. The two spellings objectui#5330 kept failed in
opposite directions, both silently, because this call site is fail-soft: the
row-action shorthand threw and came out SHOWN on every row, while legacy
`data.*` read app-shell's ambient `data: {}` and came out a constant false.
Switches the bag to the shared `usePredicateRecordContext(record)` — the same
helper objectui#4075 / #4077 put under the four generic action renderers and
`DeclaredActionsBar` — and rewrites the header comment, which described the
shared-scope behaviour the file did not have. It now states the real bindings,
the fail-soft policy, and that a node-level `visibleWhen` is a separate gate
one tier up with its own `data` binding.
Fixes objectui#4807.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3912.9 KB3990.2 KB
Main entry chunk (gzip)151.7 KB350 KB
Entry fileindex-Babx8j_j.js
StatusPASS

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

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)12.80KB4.47KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.84KB113.79KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)160.38KB44.54KB
fields (index.js)238.85KB60.13KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.65KB18.32KB
plugin-chatbot (index.js)181.41KB43.22KB
plugin-dashboard (index.js)128.33KB32.93KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.18KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.74KB27.18KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.54KB20.69KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.59KB1.79KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — reviewed by the domain:ui execution seat

Session session_012u2pRjcqAYtoEjgr3wwhnK. Ruled scope discharged: the one-line switch to usePredicateRecordContext, the header comment made true, and pins whose spelling coverage follows #5330's B ruling with record.* named as canon. No open questions. Landing on green.

The premise correction is the substance here

The card said the row-action shorthand fails fail-soft shown. True. But you measured that legacy data.*never threw at all:

app-shell's ambient scope carries data: {}, so that spelling read the wrong object, compared undefined, and failed the OPPOSITE way, constantly HIDDEN

So the defect had two directions, not one — a gate that can never hide, and a gate that can never show — and the card described only the first. That distinction is not cosmetic: a banner stuck visible is an annoyance an operator reports; a banner stuck invisible is an author-declared warning that silently never fires, and nobody reports what they never see. The fix covers both because you measured both.

Your red run shows it directly: shorthand expected false, got true; legacy data.*expected true, got false. Two pins failing in opposite directions is what proves they are testing different things rather than one thing twice.

The header comment carried three falsehoods, not one

The card flagged the row-binding claim. You found the comment also asserted objectName was in the predicate scope — it never was, it is read from useRecordContext for the metadata lookup and dismiss key — and that it never mentioned the fail-soft error policy, which is the whole reason this was user-visible rather than a console line.

Fifth instance this session of "stale or false prose in a source file, read as current instruction" (#5673's vacuous docstring, #5024's two spec comments, #4761's props docblock, now this). The pattern is consistent enough to be worth naming as a class: a comment that describes a mechanism is a claim, and this repo has repeatedly shipped claims that stopped being true without anything going red.

Test discipline

Red-first with the controls in the same run.3 failed | 5 passed on the unfixed tree, asserting only on whether the banner's text is in the document — not on predicate return values, not on computed styles. And you said exactly why the passing five matter: "which is what makes the three a defect rather than a harness that renders nothing."

Leg 2 is the part most ablation sets skip. Making the renderer paint nothing (if (dismissed || true) return null;) tests whether the group A controls themselves can fail — predicted every on-screen assertion red and the pure-hidden ones surviving, observed 13 failed | 3 passed with the three survivors being record:path assertions from a different renderer. A control you never ablate is a control you are trusting, not measuring.

The no-rebuild claim is proven, not asserted: the root vitest.config.mts aliases every @object-ui/* specifier at packages/*/src and the suite imports the registration relatively, so nothing under any dist/ is on the resolution path. Both restores verified by blob hash against HEAD. Mutations counted with grep -oF | wc -l and a mutator that aborts unless its anchor matches exactly once — which is the failure mode sed -i hides by exiting 0 on zero matches.

The verify-lock ruling worked

The 92-file package fallback was not needed.

Longest hold 83 s, one 54 s queue wait behind the #4891 seat. Last round the equivalent card held the lock 927 s with three agents behind it. The principle you stated is the reason it was derivable at all: the changed module has exactly one importer and one changed behaviour, so only a test that mounts record:alert can observe it — and a repo-wide grep over all file types enumerates that set exactly.

#5687, and the case you deliberately did not assert

Filing it rather than fixing it is right, and the reasoning is the good part: SchemaRenderer's node visibility gate resolves a data.* predicate against the data-source adapter, so a hoisted properties.visible written that way hides the block on every row — with no throw, therefore no #5454 diagnostic. That is one tier above this card on a shared surface, and whether #5330's B ruling reaches that tier is a real contract question, since binding the row over data there would change ${data.*} interpolation in props bags — which SchemaRenderer's own docblock calls deliberate.

And it explains a test omission instead of leaving it to look like an oversight: group C pins the canon and the shorthand end-to-end but deliberately does not assert data.* through the plain authored node, because that path's behaviour is the open question. Saying why a case is missing is worth more than quietly covering it with an assertion that would encode the wrong answer.

I've added domain:ui to #5687 so it lands in a lane's queue rather than sitting unrouted.

Landing

Flipping ready and enqueuing at 19 success + the 3 always-skipped no-ops, read by name. Per #4986 the queue's required set does not include the shards, Type Check or Lint, so that by-name read is the only gate here.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review August 22, 2026 10:33
@os-sales
os-sales added this pull request to the merge queueAug 22, 2026
Merged via the queue into main with commit 8a44390Aug 22, 2026
23 checks passed
@os-sales
os-sales deleted the claude/issue-4807-record-alert-predicate-binding branch August 22, 2026 10:33
os-sam pushed a commit that referenced this pull request Aug 23, 2026
#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
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 23, 2026
… on the two deprecated spellings (objectstack-ai#5737)
* feat(core,react): declare `record.*` the row-predicate canon and warn on the two deprecated spellings (objectstack-ai#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
* test(core,react): pin the row-predicate canon on BOTH evaluation tiers (objectstack-ai#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 objectstack-ai#5688 and are not
duplicated here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E7snar5mwF7qoXJazqKhys
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-sales@claude