feat(lint): every field-bearing prop on a React page block resolves against the object it names (#4340) - #4353
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
… 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
|
合并后补上这个 PR 正文里那条
<RelatedList
api={objectName} // 被查询/列出的对象 = 子对象
referenceField={schema.relationshipField} // 该对象上的外键
parentId={parentLinkValue} // 父值,另走 RecordContext
columns={filteredColumns} // objectName 的列
/>父记录从来不来自 但正文里有一句需要更正。 我写了「the parent record stays bound by 也就是说 这属于渲染器/契约层的另一个问题,不影响本 PR 已合入的内容,已按 Prime Directive #10 开出 #4413 跟踪(含完整证据链、分档成本、以及「补 record context」还是「把这四个 block 从 react-tier 契约摘掉」两条路的取舍)。 Generated by Claude Code |
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
<ListView>fieldscolumnssortgroupinguserFiltershiddenFieldsfieldOrderfilterableFieldsREACT_FIELD_SPECS<ListView filters>,<ObjectChart filter><ObjectForm>fields,initialValueskeys,sections[].fields[](+ legacygroups)REACT_FIELD_SPECS<ObjectForm subforms>columnsrelationshipFieldamountField/totalFieldchildObject/ the form's objectsubformFieldRefs<RecordHighlights><RecordDetails><RecordPath><RecordRelatedList>objectName(related list: the child, plus its Add-picker's own object)COMPONENT_FIELD_SPECStable, keyed byschemaType<Block type="…">The
record:*blocks are not re-described anywhere: they are the componentsvalidate-page-field-bindingsalready 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-unknownat 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 toreturn [], and the surface renders an empty list indistinguishable from "there is no data" — the silent zerofilter-token-unknownandvalidate-flow-template-paths' own filter-position call both gate on. Reported aserror, 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.filterFieldRefsreads position 0 only when position 1 is a recognised operator, using the spec's ownVALID_AST_OPERATORSso it cannot drift fromisFilterAST.The contract conflict the issue flagged as blocking
RecordRelatedListProps.objectNameis the related (child) object. The React overlay declaredobjectNamea second time and glossed it "The parent object", andmergePropspublishes 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) resolvedtotalagainst an account.Settled toward the spec (Prime Directive #12), because every other reading disagrees with itself:
sys_user.page.ts,sys_position.page.ts, …);validate-page-field-bindingsalready resolves this component'scolumnsagainstproperties.objectNameas the related object;showcase_invoice.accounteven declaresrelatedList: 'primary'withrelatedListColumns: ['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 defaultrelationshipValueField: 'id'needs.Closing the class, not the next instance:
REACT_OVERLAY_SHADOWSin@objectstack/spec/uiledgers every overlay prop that restates a spec-schema prop (ListView.navigation,RecordRelatedList.objectName), each with its reason, andreact-blocks.test.tsasserts 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
"amount desc") by its head rather than reporting the whole string as an unknown field — a finding no author could act on. Removed from both surfaces at once.SYSTEM_FIELDSderivation) was already done by lint: five hand-copiedSYSTEM_FIELDSlists in packages/lint should derive from the spec's declarations #4330/fix(lint): the seven system-field exemption lists derive from the spec's declarations (#4330) #4339 onmain: both rules import the derived set; no hand-copied list remains in the package.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;checkObjectChartalready owns them.Verification
packages/lint632 tests pass (43 new);packages/spectests pass (newreact-blocks.test.ts).pnpm --filter @objectstack/spec check:generated— all 8 artifacts current (gen:react-blocks+gen:api-surfaceregenerated and committed).typecheckclean on@objectstack/lint,@objectstack/spec,@objectstack/example-showcase; eslint clean on the changed files.os validateclean on both example apps. Re-introducing the oldobjectNamespelling 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.../objectuiin reach: the renderer side was not booted — objectui is not present in this environment. TheobjectNamereading 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 therecord:related_listregistry component to confirm it readsobjectNameas the child object.Generated by Claude Code