Skip to content

docs(plugin-form): Phase 0 of the row-predicate deprecation — stop teaching the bare shorthand (#5738) - #5758

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-5738-row-predicate-phase0
Aug 23, 2026
Merged

docs(plugin-form): Phase 0 of the row-predicate deprecation — stop teaching the bare shorthand (#5738)#5758
os-sam merged 1 commit into
mainfrom
claude/issue-5738-row-predicate-phase0

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#5738

Phase 0 of the row-predicate deprecation ruled on #5330 (2026-08-20, option B): stop teaching a spelling that the Phase-1 warning (PR #5737) now flags in the dev console. The card's own sizing note said this was plausibly zero-diff and that nobody had measured it — so the measurement is the deliverable, and it is reported in full below.

Result

One defect found and fixed, in packages/plugin-form/README.md:

-remind_at: Field.datetime({ requiredWhen: 'status == "scheduled"', defaultValue: 'NOW()' }),+remind_at: Field.datetime({ requiredWhen: 'record.status == "scheduled"', defaultValue: 'NOW()' }),

This one is the bad arm, not merely the non-canonical one. The same README's own table says these rules 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'], so the bare root faults there with Unknown variable: status. requiredWhen is one of the two rules enforced client and server, so the README was handing authors the spelling the server refuses outright.

How the sweep was controlled

Driven by the shipped oracle, not by a regex: detectNonCanonicalRowSpelling, exported from @object-ui/core, is the same detector behind the live warning, so this cannot disagree with what authors see in their console. It reports bare-shorthand on the old text and nothing on the new.

Discovery was key-agnostic rather than a guess at which keys carry predicates — every string literal in the corpus containing a comparison or boolean operator, then classified by root identifier. Roots scanned, with the literal counts that prove the scanner reached them:

rootfilesstring literals parsed
content/docs20218,755
examples (incl. schema-catalog, 427 JSON)46521,787
apps22622,202
packages/*/README.md396,738
skills294,664
docs152,474

That yielded 441 predicate-shaped strings (top roots: record 98, data 56). Positive control: the neighbouring terms record., data., ${, columns and objectql all return hits across the corpus, so a zero in any class is a measured zero rather than a scanner that never arrived. Negative control: the canonical examples and the host-scope roots (current_user.*, previous.*) are run through the detector too and it reports nothing on them.

The schema-catalog corpus is clean: every predicate across its 427 JSON files is a static boolean, a canonical record.*, or a host-scope current_user.*.

The three stand-downs, each verified rather than assumed

The layer rule is the whole difficulty here, and a blanket rewrite of either data. or the bare root would have broken a working tier. Three classes were deliberately left alone:

  1. Legacy ${…}-dialect predicates — in that dialect data.* is the correct spelling, and evalRowPredicate routes those strings to the legacy engine before the warning, so they cannot be what an author is being warned about. All data.* hits in content/docs are of this class.
  2. The schema/widget tier (visibleOn / hiddenOn / disabledOn in the published skills guides) — a different engine (SafeExpressionParser), where data is the widget data scope rather than a row. Verified against the detector both ways: with dataNamesRow: false it reports nothing, and the counterfactual with dataNamesRow: true shows what it would say on a record surface. The stand-down rests on the tier, and that is stated rather than hidden.
  3. The flow tier in apps/console/src/preview-samples.tsvisibleWhen: 'discount > 0' on a flow screen node reads as a bare-shorthand row predicate and is not one. Confirmed in code: isFieldVisibleWhen (previews/screen-spec.ts) evaluates it with evalCondition(normalized, variables) against flow variables, never evalRowPredicate — so the Phase-1 warning cannot fire there, and discount correctly names a sibling screen field. FlowRunner.visibleWhen.test.tsx pins the same shape from HotCRM's real lead-conversion screen. The neighbouring flow trigger, decision-edge and validation-rule conditions in that file are the same story.

Verification

Gate union run after the final commit, on f7074a8, each exit code captured before any pipe:

gateexitits own verdict line
check:control-bytes0OK (scanned 4799 tracked text file(s); skipped 85 binary)
docs:check-links0Links are valid across 13 scan roots.
check:skills-paths0OK (93/94 stated path(s) resolve across 18 guide file(s); 1 baselined)
check:doc-types0Every documented component type is registered.
check:doc-snippets0Every covered documentation snippet compiles against the built types.
check:changeset-presence0No source of a released package changed in this range, so no changeset is owed.
pnpm build043 successful, 43 total

check:doc-snippets needed the build to run at all (it refuses on unbuilt packages) and its self-checks fired — resolution, a ThisNameIsDefinitelyNotExported sentinel producing TS2305, and a positive import producing zero. One honest limit:packages/plugin-form/README.md sits in that gate's ungated ledger (63 documents "declared in this script, NOT verified by it"), so the green above is not coverage of the file this PR edits.

Declared narrowing. Repo-wide pnpm lint was not run. The diff is a single .md file, and eslint's own configuration is the authority on whether that can matter: npx eslint packages/plugin-form/README.md --format json returns File ignored because no matching configuration was supplied. No eslint configuration matches .md at all, so this diff can move neither its own verdict nor any untouched file's. CI runs the full farm regardless.

No changeset, on the repo's own rule rather than an assumption: check-changeset-presence.mjs guards a released package's src tree only, and a README sits outside it — the gate says so itself in the table above.

Out of scope, filed separately

The published skills guide skills/objectui/guides/schema-expressions.md still presents the three-way row binding without noting that two of the three now warn and are slated for retirement. That is stale post-#5737, but skills/** is outside this card's declared file surface and carries its own net-increase budget, so it is filed rather than edited here. See the linked finding.


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3917.1 KB3990.2 KB
Main entry chunk (gzip)152.5 KB350 KB
Entry fileindex-DGuN2Oi9.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 (consoleActionDispatch.js)0.20KB0.19KB
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 (ActiveOrganizationStorage.js)16.66KB6.35KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)33.99KB8.57KB
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)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
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)510.39KB114.67KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)164.55KB45.67KB
fields (index.js)238.40KB59.89KB
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.41KB32.95KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.34KB60.98KB
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.80KB27.20KB
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.61KB20.74KB
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)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
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.88KB1.85KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)3.40KB1.68KB
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-sam
os-sam marked this pull request as ready for review August 23, 2026 05:06
@os-sam
os-sam added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit 702c48aAug 23, 2026
21 checks passed
@os-sam
os-sam deleted the claude/issue-5738-row-predicate-phase0 branch August 23, 2026 05:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationplugin

Projects

None yet

2 participants

@os-sam@claude