Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-detail): bind record:alert predicates through usePredicateRecordContext - #5688
Conversation
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✅ 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
|
os-sales
commented
Aug 22, 2026
ACCEPT — reviewed by the |
Uh oh!
There was an error while loading. Please reload this page.
#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
… 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>
Fixes#4807
renderers/record-alert.tsxwas the last predicate face in the repo still handinguseConditiona root-only{ record }bag. Every other row-scoped predicate — the four generic action renderers (#4075) and app-shell'sDeclaredActionsBar(#4077) — binds the row through the sharedusePredicateRecordContext(record), which resolves the three spellings #5330 ruled on: canonicalrecord.status, the deprecated row-action shorthandstatus, and deprecated legacydata.status.The card was unblocked by the B ruling on #5330 (maintainer, 2026-08-20): canon is
record.*; the shorthand anddata.*are deprecated but kept behind a survey-sized window. So all three must resolve, and the pins namerecord.*as the canon.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:
record.status == 'in_review'status == 'in_review'statusunbound → evaluator throws → the legacy${…}path answers a throw with its own source text, a truthy stringdata.status == 'in_review'providers/ExpressionProvider.tsx) carriesdata: {}, so the predicate read that object and comparedundefinedThe 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:
Everything else in
record-alert.tsxis comment. The header block (theVisibility modelsection 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,objectNamewas never in the predicate scope at all (it is read fromuseRecordContext()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 deliberatedata= 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:Fixed tree —
Test Files 2 passed (2)/Tests 16 passed (16): the 8 new pins plus the 8 pre-existing pins inrecord-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:alerthas fourreturn nullpaths (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.mtsaliases every@object-ui/*specifier atpackages/*/src, and the suite imports the registration relatively (import '../../index'), so nothing under anydist/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 undertrap … EXIT INT TERM, and the fix was committed first so the restore could not be confused with an uncommitted edit.useCondition(predicateInput, predicateRecord)→useCondition(predicateInput, { record })3 failed | 13 passed (16)— exactly those 3if (dismissed) return null;→if (dismissed || true) return null;13 failed | 3 passed (16)— the 3 survivors arerecord:pathassertions, a different rendererLeg 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 asrecord:alertand thealertalias) and exactly one changed behaviour: the context bag handed touseConditioninsideRecordAlertRenderer. 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) forrecord:alert,RecordAlertRendererandtype: '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 — theplugin-detailrenderer and synth suites, the four app-shell metadata-admin preview/inspector tests that mentionrecord:alert,PagePreview.test.tsx, and the fourapps/consoletests that mount record blocks. The whole-package fallback (92 files) was not needed.pnpm --filter @object-ui/plugin-detail type-check— exit 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 0check: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:
eslint.config.jssets noparserOptions.projectand noprojectService, so type-aware linting is not enabled; and no rule ineslint-rules/reads the filesystem (readdirSync/globSync/readFileSync: zero hits). ESLint's verdicts are therefore per-file and independent of the rest of the tree.--format json: 2 files linted, exit 0.record-alert.rowBinding.test.tsx0 errors / 0 warnings;record-alert.tsx0 errors / 8 warnings.The 8 warnings are all pre-existing
@typescript-eslint/no-explicit-anyon lines this PR did not touch: the diff adds zero lines containingany(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
#5687 —
SchemaRenderer's node visibility gate resolves adata.*predicate against the data-source adapter, so a hoistedproperties.visiblewritten 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 overdatathere would change${data.*}interpolation in props bags, whichSchemaRenderer'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 assertdata.*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