fix(plugins,react,i18n): bound registrations stop requiring objectName, and a node with neither shows a no-object hint (objectui#11605) - #11612
Conversation
…e, and a node with neither shows a no-object hint (objectui#11605) The object-bound registrations whose spec row or binding doc waives `objectName` (object-grid, view:grid, list-view, view:list, object-form, view:form, embeddable-form, object-master-detail-form, object-kanban, object-metric, object-pivot, object-chart, view:chart) drop `required: true` and describe the binding, so the page compile accepts a node bound by `dataSource.object`. `ElementDataSourceGate` gains `requiresObject`, and each member whose neither-node answered with a blank opts in, so that node shows "No object named: set objectName or dataSource.object." (`view.noObject`, ten packs). Pins: the manifest enumeration pin and per-member compile rows, the neither-node hint rows, and the gate's own rows. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
…rce, so a static board shows no no-object hint (objectui#11605) The board keeps a lane's own `cards` when it merges rows into its lanes, so a board authored with card-carrying `columns` and no object draws cards. The kanban call site now opts out of `requiresObject` for it, and the neither-hint pin gains that control. Two fixtures follow the hint: the kanban registration probe names its object, and the html-tier `object-name` spelling (6598 form 8a) now fails loudly through the block's hint rather than the compile. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
…of the binding doc The pin parses `content/docs/guide/data-source.md`'s "which keys each one honours" table, so the markdown-test-inputs ledger records that read and a markdown-only change to that page runs the pin. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
…tName, so it reads the same before and after (objectui#11605) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
…no-object hint (objectui#11605) With the `data` binding stripped, the workbench's list names no object, so the block now draws the "no object named" hint instead of mounting `ListView`. The control's data-call assertions are unchanged; only its "the block has answered" signal moves to the hint. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
… `any` (objectui#11605) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
✅ 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
|
…`bind` as BaseSchema members, with no second `bind` declaration (objectui#11605) `chartNeedsObject` read the record sources through a type literal that re-declared `bind` beside `BaseSchema`'s, which the `bind`-declared-once pin (`base-bind-declared.test.ts`, objectui#6357) refuses. It now reads `data` and `bind` as the `BaseSchema` members they are and `dataset`, this block's own key, by name with `Reflect.get`. The helper also moves above the `ObjectChartBlock` doc comment it had been inserted beneath, so that comment documents the block again. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
… collector, not a node that names no object (objectui#11605) The `object-form` / `view:form` opt-out read non-empty `customFields` only. `ObjectForm` dispatches the `tabbed`, `wizard`, `split`, `drawer` and `modal` variants to renderers that also treat sections whose every field is an inline runtime `FormField` as an inline field source (objectui#10254), so such a node with no object and no binding had the "no object named" hint painted over a working collector. The predicate is now `!hasInlineFieldSource(schema)`, the shared helper those renderers use. The neither-hint pin gains one row per sectioned variant on both tags, red before this change; the published `objectName` description, the plugin-form changeset and the binding doc name inline sections beside `customFields`. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
✅ 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
|
Contract reviewServed-tier: Resume after the HELD draft on What ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Written 2026-10-04T13:24Z. The HELD draft's two open items are cleared on this head: the |
|
Generated by Claude Code |
Fixes #11605
Clause-②: yes
What changes
Every object-bound registration whose spec row, or the documented binding contract, waives
objectNamestops declaring itrequired: true, and each one gains adescriptionthat names the binding. So the page compile (compile()in@object-ui/sdui-parser, whoseokis the save gate) accepts a node whosedataSource.objectnames the object. This is the triage ruling (a), the same move as objectui#11569 (PR objectui#11604).Each member that answered a node with neither key with a blank now shows a visible hint instead: "No object named: set objectName or dataSource.object." The hint comes from one place.
ElementDataSourceGategains an opt-inrequiresObjectprop, and each call site states whether its block has another record source. The hint is drawn after the binding lands, so a bound node never sees it. The string is the newview.noObjectkey in all ten packs, worded aselement.number.noObjectwith the property as a hole.No new manifest vocabulary and no new compile rule (ruling (a), "no (b)").
packages/typesandsdui-parserare not edited.The census, and one evidence line per site
The census was re-run at the base
b92329cthree ways:git grep "name: 'objectName'.*required: true"finds 16 sites.name: 'objectName',alone on a line) on 3 inputs (action:button,action:icon,object-tree), none required. A double-quoted spelling finds 0.sdui.manifest.json,objectNamewas required on 10 public tags: the 9 members below plusrecord:related_list. The other 6 sites are non-public aliases or blocks.Per site, the reading is: does the registered renderer wrap
ElementDataSourceGate(sodataSource.objectlands onobjectName, the default mapping key), and what do the spec row and the binding doc say.plugin-gridGRID_QUERY_INPUTSobject-grid,view:gridObjectGridRenderer)dataSourcebinding can supply the object instead"plugin-kanbanOBJECT_KANBAN_INPUTSobject-kanbanObjectKanbanRenderer)plugin-dashboardobject-metricregistrationobject-metricObjectMetricBlock)plugin-dashboardobject-pivotregistrationobject-pivotObjectPivotBlock)object-pivotplugin-dashboardobject-data-tableregistrationobject-data-table(not public)ObjectDataTableis registered bareplugin-listlist-viewregistrationlist-viewListViewBlock)list-viewrecord-source rule that counts the bindingplugin-listlistregistrationview:list(not public)ListViewBlock)list-viewlist-viewplugin-chartsObjectChart.tsxobject-chartregistration (outside triage's 15)object-chartObjectChartBlock)object-chartplugin-chartsindex.tsxchartregistrationview:chart(not public)ObjectChartBlock)object-chartobject-chartplugin-formobject-formregistrationobject-formObjectFormRenderer)OBJECT_FORM_OBJECT_NAME_INPUT)plugin-formformregistrationview:form(not public)object-formobject-formplugin-formembeddable-formregistrationembeddable-formEmbeddableFormRenderer)formIdstays required)plugin-formobject-master-detail-formregistrationobject-master-detail-formMasterDetailFormRenderer)detailsstays required)plugin-detaildetailregistrationview:detail(not public)DetailViewplugin-detailrelated_listregistrationrecord:related_listobjectNameplugin-viewobject-viewregistrationobject-view(not public)ObjectViewRendererreads no binding"Before" was measured through
SchemaRendererunder a provider with a recording adapter, at the base. Every member made zero data calls, and none threw. After the change, each opted-in member draws the hint and still makes zero data calls (pin rows below).Other record sources opt out of the hint, each read from the block's own fetch guard. Lists take inline
data. Forms (object-form,view:form) take inline fields, answered by the sharedhasInlineFieldSource: non-emptycustomFields, orsectionswhose every field is inline, the collector thetabbed,wizard,split,drawerandmodalvariants render. Kanban takesdata, abindpath, a parent view's rows, or lanes that carry their owncards. Metric takesfallbackValue. Chart takesdata,datasetorbind. Pivot takesdataorbind. Embeddable and master-detail have none.Accept-set changes (for the contract review)
Measured against the manifest the console ships (
buildArtifactsoverdev/manifest-registry.ts). "Before" is the baseb92329c; "after" is this head.dataSource={{ object: "account" }}and its other required props, noobjectNameok: false, onemissing-required-properror namingobjectNameok: true, no diagnostics, the binding site recordedobjectNamenor a bindingok: false, the same errorobjectName; the runtime answers with the hint (or the grid's own error)embeddable-formnamingobjectNamebut noformId(control)formIdrecord:related_listwithoutobjectName(control)objectNameGenerated artifacts move with it, from the same generator: the
sdui.manifest.jsonentries,sdui-intrinsics.d.ts(objectName?: stringon the 9 tags) and thesdui-blocks.mdrequired-props column. None of them is tracked.The zod faces (
objectui validate) were read and not touched. A bound node of each member passessafeValidateSchemaat this head, and the record-source refinements still refuse some neither nodes there.Pins
New:
apps/console/src/__tests__/objectname-binding-required-11605.test.ts, judged against the shippedsdui.manifest.json:dataSourceinput withbinding: 'object'. For each, everyrequired: trueinput must be required by itsComponentPropsMaprow, not supplied by the binding, or in theLEDGERwith a reason. It runs one test per tag, so a new contradiction is red by name. What the binding supplies is read off the binding doc's "which keys each one honours" table: the object key (objectName,childObjectonrecord:line_items,objectonelement:*), pluscolumnswhere a named view fills them.record:related_list.columns(see Acceptance notes). A stale row is red.objectNamenot required with a description namingdataSource.object; the bound node compilesok; the neither node draws noobjectNamediagnostic. Plus the two controls in the table above.apps/console/src/__tests__/objectname-neither-hint-11605.test.tsx, for 11 tags (the 9 public members plusview:list,view:formandview:chart):TESTID-no-objectwith the exact sentence and makes zero data calls.objectName, none when bound bydataSource.object, none with the block's other record source, and none on a kanban whose lanes carrycards.object-formandview:form, each sectionedformType(tabbed,wizard,split,drawer,modal) with fully-inline sections renders its collector, not the hint (10 rows, red before34eda84).packages/react/src/element-data-source/__tests__/ElementDataSourceGate.requiresObject-11605.test.tsx: the gate's own rows.childObjectunderobject: 'childObject').object: false, changes nothing.Reproduced first. The two console pins were run against the base
b92329cin a separate worktree. 47 rows were red: 9 enumeration rows, 9 published-input rows, 9 bound-compile rows, 9 neither-compile rows and 11 hint rows. Each enumeration row named its tag, for example "object-grid declares "objectName" required, but itsdataSourcebinding supplies it and its spec row does not require it". All controls were green. At34eda84all 108 rows pass across the three new files (hint 53, enumeration 48, gate 7).Ablations, on the committed fix, through
ablation-replace.mjs. The console project aliases each@object-ui/plugin-*and@object-ui/reacttosrc, so a mutation reaches the test without a build.required: trueput back onobject-grid'sobjectName(packages/plugin-grid/src/index.tsx, blob103b678daf64becameb146f9f85b4d): 4 rows went red (enumeration, published input, bound compile, neither compile), each namingobject-grid, with 44 passed.object-form, throughOBJECT_FORM_OBJECT_NAME_INPUT(69df173d5b9fbecame4706c474205e): the same 4 rows went red, namingobject-form.requiresObjectpredicate (54919dea475ebecame21d282a24bf0): exactly theobject-kanbanneither-hint row went red, with 42 passed.customFields-only predicate (packages/plugin-form/src/index.tsx, blobb730e588d559becamedb8cd30dafb7): exactly the 10 sectioned-variant rows went red, 43 passed; restored by blob hash.Each restore is proven by blob hash: blob equals HEAD and
git diff HEADis empty. Ablations 1 and 3 each had a first attempt the tool refused before any test ran, because the replacement contained its own anchor. Those attempts restored to HEAD and measured nothing.Reverse type check.
requiresObject={was renamedrequiresObjekt={inplugin-kanban.tsc --noEmitthen failed with TS2322 against the rebuilt@object-ui/reactdeclarations, and the file was restored by blob hash (54919dea475e).Other edits
plugin-kanbanindex.test.ts: "objectName is required" became "declared, not required, namesdataSource.object".plugin-formembeddable-form-arm-inputs-11440.test.ts: the required list is now['formId']. The arm's record-source rule is unchanged.plugin-kanbanregistration.test.tsx: the stub probe now names its object.plugin-listhtmlTierColumnSpellings-6598.test.tsxform 8a:object-namestill fails loudly, now through the block's hint rather than the compile error. The invariant's "(b) fails loudly" names both.sdui-workbench-preview-listview-binding-8510.test.tsx: the stripped-binding control waits on the list's hint. Its data-call assertions are unchanged.guideCrudAppRenderstest headers said the block "declaresobjectNameas required".scripts/markdown-test-inputs.mjs: anADJUDICATEDentry for the new pin's read ofcontent/docs/guide/data-source.md. Its--auditis green, andscripts/__tests__/markdown-test-inputs.test.tspasses.content/docs/guide/data-source.md: one prose paragraph, no code fence. It says the page compile accepts a bound node on theobjectNamerows, withrecord:related_listas the exception. It also states the hint, the grid's own error, and which record sources opt out.packages/react/README.md: one prose paragraph onrequiresDataSourceandrequiresObject, no code fence.minor, each widening paragraph opening**Clause-②: yes (widening)** —:@object-ui/plugin-grid,plugin-kanban,plugin-dashboard,plugin-list,plugin-charts,plugin-form,react(the new optional prop) andi18n(the new key).@object-ui/consolechanges only test files, the same standing objectui#11604's review accepted.Verification (head
34eda84)The patch round,
582982e→34eda84:067f3f0:chartNeedsObjectreadsdataandbindasBaseSchemamembers anddatasetwithReflect.get, so there is no secondbinddeclaration (base-bind-declared.test.tsgreen;plugin-chartssuite 89 files, 1213 passed / 9 skipped; type-check exit 0).34eda84: the form opt-out useshasInlineFieldSource(plugin-formsuite 163 files, 1889 passed / 1 skipped; type-check exit 0).34eda84: 127 package/app files (2576 passed) and 69 scripts files (2816 passed). The gates listed below were re-run at34eda84, each exit 0.582982e.Build:
pnpm exec turbo run build --concurrency=2filtered to the^...closure of the six changed plugins. Exit 0, 14 tasks, 7 cache hits;@object-ui/reactand@object-ui/i18nrebuilt.type-check: each ofreact,i18n,plugin-grid,plugin-kanban,plugin-dashboard,plugin-list,plugin-chartsandplugin-formexits 0 (tsc --noEmit && tsc -p tsconfig.test.json).reactwas re-run at this head.Full package suites (
pnpm exec vitest run packages/PKG/):plugin-dashboard+plugin-charts: 257 files, 2882 passed, 15 skipped.plugin-form: 163 files, 1889 passed, 1 skipped.plugin-grid: 185 files, 1711 passed.plugin-list+plugin-kanban: 178 files, 1663 passed.react+i18n: 187 files, 2689 passed, 13 skipped.plugin-view+examples/schema-catalog: 104 files, 2861 passed.apps/console: 145 files, all pass after the 8510 control above.582982e, apart from three test files andscripts/markdown-test-inputs.mjs. Those were re-run at582982e: 12 files, 538 passed.Narrowed consumer sweep: the 53 test files outside the moved packages that name a moved tag (
app-shell,components,cli,sdui-parser,plugin-detail,plugin-calendar,plugin-map,plugin-gantt), pluselement-data-source-objectname-waiver-11117(it readsdata-source.md) andmarkdown-test-inputs: 55 files, 882 passed. Selected by tag literal and by source-text read of a changed file. A consumer that renders a moved block without naming its tag is outside this selection, and CI covers it.Gates at this head, each exit 0:
changeset:check,check-changeset-presence(8 changesets over 9 released packages),check-changeset-overwrite,check:changeset-claims,check:pending-changeset-literals;check:control-bytes,check:new-line-citations(0 new citation(s));check:i18n-keys,check:i18n-drift(1 key added, 0 en values changed),check:i18n-dead-keys(view.noObjectnot listed);check:element-data-source-declaration,check:test-path-roots,docs:check-links,check:doc-types,markdown-test-inputs --audit.check:handler-key-reads,check:doc-example-readers,check:spec-symbols,check:doc-fences,check:doc-example-ids,check:component-surface-parity,check:registry-bare-names,check:prompt-keys,check:vi-mock-override-shape,check:unreferenced-sources,check:side-effects-array.check-governed-queue-guard --testover the 40 paths: NOT GOVERNED.eslint (
--no-inline-config --format json) over the 30 touched ts/tsx/mjs files: 0 errors. For each modified file, the warning count and the multiset of rule ids are equal at base (fed by--stdin --stdin-filename) and at head. The three new test files carry 0 warnings. The config has no type-aware linting (noproject/projectService), so this diff cannot move the verdict on an untouched file.NOT MEASURED, left to CI:
check:eager-closure,check:eager-locale-cataloguesandcheck:sdui-registration-pinsneed a built console (PREREQUISITE NOT MET). This diff adds one short string to each eager pack and a small component to the gate.check:doc-snippetsandcheck:doc-examplesneed built packages (exit 2). Neither docs edit adds a code fence.check:readme-exportsis judged on built type entries and could not judge any README here. The README edit adds no fence.Acceptance notes
record:related_list.columnsis ledgered, not moved. The registration requirescolumns, but the spec row leaves it optional, and the gate maps a named view's field list onto it (columns: true). So a node bound to a view with its owncolumnsis refused. Measured at this head:record:related_listwithobjectName,relationshipFieldanddataSource={{ object: "task", view: "open_tasks" }}getsok: false,missing-required-propnamingcolumns. It is outside this card'sobjectNamescope, and a node bound byobjectalone still needs columns. The dev report carried it to the seat, which filed it as objectui#11613.object-gridis not opted into the hint. Its own error already answers the neither node.fallbackValueopts out of the hint, because the author chose a static tile. With an adapter in context the tile still shows its dash rather than that value, as before this change. Recorded only.list-view,object-grid,object-kanban,embeddable-formandobject-pivot.simple(or unset)formTypewhose sections are fully inline also opts out of the hint now, becausehasInlineFieldSourceanswers for every variant.ObjectForm's simple path does not read inline sections, so with no object that node still draws an empty form, as it did before this PR. It is a missed hint, not a regression.app-shelltests that name these tags are green.Implemented by the os-dev run under session
https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2, for the claim on the card.Generated by Claude Code