Skip to content

feat(lint): every field-bearing prop on a React page block resolves against the object it names (#4340) - #4353

Merged
os-zhuang merged 2 commits into
mainfrom
claude/react-page-blocks-props-validation-1bxgac
Jul 31, 2026
Merged

feat(lint): every field-bearing prop on a React page block resolves against the object it names (#4340)#4353
os-zhuang merged 2 commits into
mainfrom
claude/react-page-blocks-props-validation-1bxgac

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #4340. Follow-up to #4329 / #4338.

#4329 closed one prop — <ListView searchableFields> — by exporting the metadata rule's core and running it from the gate that owns React block dataProps. That prop was an instance, not the class. This closes the class.

What now resolves

block · prop resolves against how
<ListView> fields columns sort grouping userFilters hiddenFields fieldOrder filterableFields bound object REACT_FIELD_SPECS
<ListView filters>, <ObjectChart filter> bound object filter positions, gating — see below
<ObjectForm> fields, initialValues keys, sections[].fields[] (+ legacy groups) bound object REACT_FIELD_SPECS
<ObjectForm subforms> columns relationshipField amountField / totalField each entry's own childObject / the form's object subformFieldRefs
<RecordHighlights> <RecordDetails> <RecordPath> <RecordRelatedList> the block's objectName (related list: the child, plus its Add-picker's own object) the shared COMPONENT_FIELD_SPECS table, keyed by schemaType
<Block type="…"> ditto, by the type the author writes same table — the escape hatch is checked, not a hole

The record:* blocks are not re-described anywhere: they are the components validate-page-field-bindings already walks, so that rule's extraction (componentFieldRefs, relatedListFieldRefs, checkFieldRefs, indexObjectFields) is now exported and reused. A prop added to that table is checked on both surfaces at once — the two agree by construction rather than by two lists that happen to match, which is the drift #4330 had just finished removing from the system-field lists.

Findings carry the metadata rule's id page-field-unknown at its advisory severity, because the consumer behaves the same way: the unknown name is skipped and the rest renders.

A filter position gates instead

<ListView filters> / <ObjectChart filter> name fields in a query. An unknown column there is not a skipped column: the predicate can never match, SqlDriver's unknown-column ladder only retries the projection and the sort so it falls through to return [], and the surface renders an empty list indistinguishable from "there is no data" — the silent zero filter-token-unknown and validate-flow-template-paths' own filter-position call both gate on. Reported as error, per the issue's severity note.

Filter positions are also resolved independently of one another, unlike every other value this gate reads. filters={['status', '=', stage]} — a static field beside React state — is the shape a react page actually writes, and the all-or-nothing static reader bailed on the whole array including the one position that was knowable. filterFieldRefs reads position 0 only when position 1 is a recognised operator, using the spec's own VALID_AST_OPERATORS so it cannot drift from isFilterAST.

The contract conflict the issue flagged as blocking

RecordRelatedListProps.objectName is the related (child) object. The React overlay declared objectName a second time and glossed it "The parent object", and mergeProps publishes the overlay's description in place of the schema's — so the react surface both contradicted the spec and lost any way to name the object it renders. The one live instance authored against the gloss (objectName="showcase_account" with invoice columns) resolved total against an account.

Settled toward the spec (Prime Directive #12), because every other reading disagrees with itself:

  • the spec schema says "Related object name";
  • every metadata instance passes the child (sys_user.page.ts, sys_position.page.ts, …);
  • validate-page-field-bindings already resolves this component's columns against properties.objectName as the related object;
  • there is one registry component behind both surfaces (ADR-0081: the schema is "the authoritative source, do not re-author"), so only one meaning can be implemented;
  • showcase_invoice.account even declares relatedList: 'primary' with relatedListColumns: ['name','status','total'] — the exact list the page was trying to render.

No new prop was invented: the parent record stays bound by recordId, which is all the default relationshipValueField: 'id' needs.

Closing the class, not the next instance: REACT_OVERLAY_SHADOWS in @objectstack/spec/ui ledgers every overlay prop that restates a spec-schema prop (ListView.navigation, RecordRelatedList.objectName), each with its reason, and react-blocks.test.ts asserts the ledger equals the real collision set in both directions. The next overlay entry that silently redefines a schema prop fails a test instead of shipping a second dialect.

Also

Deliberately not checked, with reason

  • <RecordRelatedList relationshipValueField> — it names a field on the parent, and the react surface binds the parent record (recordId) but never its object. The one field-bearing prop left unresolved, and the reason is a missing binding rather than a missing rule. The metadata twin has the page's object and checks it there.
  • <ObjectChart> axes — they name the aggregate's result columns, not fields; checkObjectChart already owns them.
  • Anything non-static, behind a spread, cross-package, without an authored field map, or a dotted relationship path — unchanged ADR-0072 D1 skips.

Verification

  • packages/lint 632 tests pass (43 new); packages/spec tests pass (new react-blocks.test.ts).
  • pnpm --filter @objectstack/spec check:generated — all 8 artifacts current (gen:react-blocks + gen:api-surface regenerated and committed).
  • typecheck clean on @objectstack/lint, @objectstack/spec, @objectstack/example-showcase; eslint clean on the changed files.
  • os validate clean on both example apps. Re-introducing the old objectName spelling on the showcase page makes the new rule report both stale positions (total, account) — so the corpus is green and the rule is proven to fire on the real bug.

⚠️ For a reviewer with ../objectui in reach: the renderer side was not booted — objectui is not present in this environment. The objectName reading above is argued from the spec, the metadata corpus, the existing lint, and the one-component design, not from watching the block render. Worth a glance at the record:related_list registry component to confirm it reads objectName as the child object.


Generated by Claude Code

…gainst the object it names (#4340)

#4329 closed ONE of them — <ListView searchableFields> — by running the metadata
rule's core from the gate that owns React block props. That prop was an
instance, not the class: every other prop a kind:'react' page binds BY FIELD
NAME shipped exactly as typed, the same silent drift page-field-unknown already
closes for the page-component `properties` bag one surface over.

validate-react-page-props now resolves all of them: ListView
fields/columns/sort/grouping/userFilters/hiddenFields/fieldOrder/filterableFields,
ObjectForm fields + initialValues KEYS + sections[].fields[] + subforms (each
entry against its own childObject, totalField against the form's), and the
record:* blocks via the SAME COMPONENT_FIELD_SPECS table the metadata surface
uses, keyed by the block's schemaType — so the two surfaces agree by
construction rather than by two lists that happen to match. <Block type="…">
reaches that table by the type the author writes, so the escape hatch is
checked instead of being a hole. Findings carry page-field-unknown at its
advisory severity, because the consumer behaves the same way.

A FILTER position gates instead. <ListView filters> / <ObjectChart filter> name
fields in a QUERY: an unknown column there is not a skipped column, the
predicate can never match, SqlDriver swallows "no such column" and returns [],
and the surface renders an empty list indistinguishable from "there is no data"
— the silent zero filter-token-unknown and validate-flow-template-paths' own
filter-position call both gate on. Filter positions are also resolved
INDEPENDENTLY of one another, unlike every other value this gate reads:
filters={['status','=',stage]} is the shape a react page actually writes, and
the all-or-nothing static reader skipped the whole array including the one
position that was knowable.

<RecordRelatedList objectName> is settled as the RELATED (child) object, which
is what the spec schema always said, what record:related_list means on every
metadata surface, and what validate-page-field-bindings already resolves its
columns against. The React overlay had declared objectName a second time and
glossed it "The parent object"; the generated contract publishes the overlay's
description in place of the schema's, so the react surface both contradicted
the spec and lost any way to name the object it renders — and the one live page
authored against the gloss listed the wrong object. The class is closed too:
REACT_OVERLAY_SHADOWS ledgers every overlay prop that restates a spec-schema
prop, and a test asserts the ledger equals the real collision set.

Also: the shared sort reader judges the LEGACY bare-string form ("amount desc")
by its head instead of reporting the whole string as an unknown field — a
finding no author could act on, removed from both surfaces at once.

Verified against the live corpus: `os validate` is clean on app-showcase and
app-crm, and re-introducing the old objectName spelling makes the new rule
report both stale positions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rK2McPogrTJBKdUsThrGs
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 31, 2026 7:54am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests protocol:ui tooling size/xl labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

… fix a related-list example that named keys nothing reads

Two hand-written docs the docs-drift check flagged, both about the surface this
PR changes.

`deployment/validating-metadata.mdx` enumerates what `os validate` catches, and
after this PR it missed the largest new class. §10 covers it: which block props
resolve, that the record:* blocks ride the SAME descriptor table as §5 (so a
component is described once and checked wherever it is authored), that
<RecordRelatedList objectName> is the related child object, and why a filter
position gates when everything else advises. §5 gains one line pointing at it.

`ui/pages.mdx` authored its record:related_list example as
`properties: { object, relationship }`. Neither key exists on
RecordRelatedListProps and no conversion-layer alias maps them, so `properties`
being unvalidated is the only reason the example parses — an author copying it
gets a related list bound to nothing. Corrected to `objectName` /
`relationshipField`, with a comment naming which object each one is: the same
confusion this PR settles in the contract, one doc over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012rK2McPogrTJBKdUsThrGs
@os-zhuang
os-zhuang marked this pull request as ready for review July 31, 2026 11:00
@os-zhuang
os-zhuang merged commit 3eb1b2b into main Jul 31, 2026
26 of 27 checks passed
@os-zhuang
os-zhuang deleted the claude/react-page-blocks-props-validation-1bxgac branch July 31, 2026 11:01

Copy link
Copy Markdown
Contributor Author

合并后补上这个 PR 正文里那条 ⚠️ 的答案——objectui(HEAD c785740)已经拉下来读过渲染器了。

objectName 的判定确认无误。 packages/plugin-detail/src/renderers/record-related-list.tsx:64const objectName = schema.objectName,随后:

<RelatedList
  api={objectName}                           // 被查询/列出的对象 = 子对象
  referenceField={schema.relationshipField}   // 该对象上的外键
  parentId={parentLinkValue}                  // 父值,另走 RecordContext
  columns={filteredColumns}                   // objectName 的列
/>

父记录从来不来自 objectNamecolumns/sort 解析到的正是渲染器实际查询的那个对象,所以这个 PR 里的 lint 校验的对象是对的。另外 react-page.tsxbuildComponentScope 确认是把 JSX props 直接摊进 SDUI schema bag、复用同一个注册组件——没有第二套语义,ADR-0081 的设计如实成立。

但正文里有一句需要更正。 我写了「the parent record stays bound by recordId」——那是契约这么说,渲染器读的是 useRecordContext(),从不读 schema.recordId;而 react-page 只包 SchemaRendererProvider,不包 RecordContextProvider。所以 parentLinkValue 恒为 nullRelatedList 直接拒绝 fetch(objectui 自己的 RelatedList.parentscope.test.tsx:58 钉着这个行为)。<RecordDetails> / <RecordHighlights> / <RecordPath> 更彻底——连 objectName 都走 context。

也就是说 relationshipValueField 被 ledger 为 skipped-with-reason 时我给的理由(「绑父记录但不绑父对象」)说轻了:父记录也没绑上

这属于渲染器/契约层的另一个问题,不影响本 PR 已合入的内容,已按 Prime Directive #10 开出 #4413 跟踪(含完整证据链、分档成本、以及「补 record context」还是「把这四个 block 从 react-tier 契约摘掉」两条路的取舍)。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lint: field-bearing props on React page blocks are unchecked against the bound object — close the class, not the next instance (#4329 follow-up)

2 participants