…lared inputs
The registration declares eight FLAT inputs — `title`, `description`, `fields`,
`collapsible`, `defaultCollapsed`, `columns`, `showBorder`, `headerColor` —
while `DetailSection` declares a single `section` OBJECT prop and reads
`section.*` only. `SchemaRenderer` spreads a node's non-metadata keys as React
props, so an authored node arrived as `title` / `fields` / … and `section`
arrived `undefined`.
Not merely inert. Measured end to end through the real `SchemaRenderer` and the
real registry: `DetailSection`'s first statement is
`React.useState(section.defaultCollapsed ?? false)`, so the render THREW,
`SchemaErrorBoundary` caught it, and the page showed
Component "detail-section" failed to render
Cannot read properties of undefined (reading 'defaultCollapsed')
in place of the block.
Register the tag against `DetailSectionNode`, a seam adapter that folds the
eight declared inputs into the `section` object the component reads — the same
shape of repair this package already uses for `field:permission-facet-link`
(`withFieldCarrier`, objectui#3307).
The authoring surface does not move, deliberately. The other repair — declaring
a nested `section` input — would change what authors may write, and the flat
shape is both published and authored: a manifest built the way `PageRenderer`
builds the JSX-page compiler's gives the flat eight ZERO diagnostics and REFUSES
`section`, this package's README documents a flat `detail-section` node inside
`tabs[].content`, and objectui#6955's landed pin asserts that same flat surface.
`DetailSection` is byte-identical: every in-repo caller passes `section={…}` as
a direct JSX child and goes nowhere near the registry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
Part of #8626
Part of, notFixes, and that is the stop condition firing — see "The measurement came before the choice" below. The card reserves ONE question for the maintainer (isdetail-sectionmeant to be authorable as a flat SDUI block at all, or should the registration be re-declared around the nestedsectionshape). This PR takes the repair that invalidates nothing and leaves that question open rather than closing it by merge.The measurement came before the choice
The triage's stop condition, verbatim:
The sweep is POSITIVE. The flat surface is authored, twice.
The complete universe is the 17 lines
git grep detail-sectionreturns onb775500af, hand-classified. Two of them are authored nodes:packages/plugin-detail/README.md:158content: { type: 'detail-section', fields: [...] }inside adetail-viewtabs[]DetailTabsrenderstab.contentthroughSchemaRenderer— a live authored node, and one this package documentspackages/plugin-detail/src/__tests__/detailSectionHeaderColorEnum-6955.test.tsvalidateTree({ type: TAG, fields: [{ name: 'amount' }], ...props })withTAG = 'detail-section', plusexpect(inputs.map(i => i.name)).toContain('fields')The other 15 are prose (
ROADMAP.md, fourCHANGELOG.mds, the #6955 changeset), a comment inLookupField.tsx, the CLI's known-type list (packages/cli/src/utils/known-schema-types.ts:111,324), and the registration itself.Lit controls, in the same command. The sweep counts
type: 'TAG'literals, and is run at the merge-base and at HEAD so each control is individually checked as unmoved:Two candidate controls were tried and discarded rather than reported:
related-listreads 0 on both sides (a zero is not a control), andgridmoved 6660 -> 6668 (this PR's owngrid-colstext), so it cannot witness anything.⇒ re-declaring the registration around a nested
sectionwould invalidate both authored sites, and would delete the very surface objectui#6955 had just narrowed. That is the branch the stop condition forbids.The repair taken, and what the other would have cost
TAKEN — adapt at the seam.
detail-sectionis now registered againstDetailSectionNode, which folds the eight declared inputs into thesectionobjectDetailSectionreads. Same shape of repair this package already uses forfield:permission-facet-link(withFieldCarrier, objectui#3307). The declaredinputsarray is byte-identical; every authored node keeps validating and starts rendering.NOT TAKEN — declare the nested shape. It would have cost, concretely:
fieldsinput exists and diagnoses a flatheaderColor— the card the filing seat correctly kept separate (finding(plugin-detail): everydetail-sectioninput is a FLAT prop the renderer never reads —DetailSectionreadssection.*only, and an authored node hands itsection === undefined#8626's own out-of-scope note);It would have BOUGHT a registration that describes
DetailSection's props literally. That is the cheaper declaration, not the truer contract:inputspublishes what an AUTHOR may write, and the author has never been able to write asectionobject here.M2 — the card's second half, EXECUTED (not restated)
The card flagged "a reading and not an execution". Executed, through the real
SchemaRendererand the real registry onb775500af, rendering a node authored exactly as the registration prescribes (type: 'detail-section'withtitleandfields):It does throw — and the throw does not escape.
SchemaErrorBoundarycatches it and paints the block's slot orange:⇒ the severity is worse than the card's "inert" and milder than "breaks the page": every authored key is lost AND the author is shown a raw JavaScript message where their section should be.
errorBannerAbsent()in the pin asserts that exact face is gone.M3 — the reach, verified rather than trusted
packages/components/src/renderers/layout/page.tsx(the_jsxManifestmemo) builds the JSX-page compiler's manifest fromComponentRegistry.getKnownTypes()plus each entry'sinputs, andsdui-parser'svalidateTreejudges an authored page against it — the same reachapps/console/src/__tests__/registry-inputs-spec-parity.test.tsrecords forelement:record_picker, likewise outsidePUBLIC_BLOCKS.Driven against a manifest built that same way, from the live registry:
[]— zero diagnostics{ section: { title, fields } }error missing-required-prop "fields"+warning unknown-prop "section"⇒ the platform does not merely permit the flat shape; it refuses the nested one. That is the reading that decides between the two repairs, and it is pinned as this PR's control row.
Ablation matrix — three legs, every row covered
Each leg: mutate, prove the mutation reached disk (anchor count +
git hash-objectbefore/after), run, restore viagit checkout HEAD -- PATH, prove restoration by blob-hash equality and an emptygit diff HEAD. Restoration ran from atrap … EXIT INT TERMwith absolute paths. No build step is involved: the pin imports the mutated../indexby relative path, so there is nodistbetween the mutation and the assertion.DetailSectionagain (undo the repair)TypeError: Cannot read properties of undefined (reading 'defaultCollapsed')'headerColor'fromDETAIL_SECTION_NODE_INPUTShonours headerColor as the header tint class+folds exactly the inputs the registration declaresfieldstoitemsfolds exactly the inputs…+leaves the published authoring surface where authors already write itEvery one of the 7 rows reddens under at least one leg — no assertion survives deletion of the thing it names. The two rows that survive leg A survive it correctly: they name the DECLARATION, not the binding, and legs B and C are the mutations of what they do name.
The fold list was made load-bearing for leg B:
DETAIL_SECTION_NODE_INPUTSwas a list the pin merely compared against the registration while a hand-written destructure did the folding. Two copies, so the list could keep agreeing with the declaration while the fold drifted away from both. The component now folds by iterating that list.CONTROL — what did not move, proven
The registration's
inputsarray:git diff MERGE_BASE..HEAD -- packages/plugin-detail/src/index.tsxcontains no line inside it.@object-ui/typesis untouched. Five in-repoDetailSectioncall sites (DetailViewx4,SectionGroup) passsection={…}as direct JSX children and go nowhere near the registry, so none of them is on this diff.Verification
Diffed against
git merge-base origin/main HEAD=b775500af(4 files, +424/-2).pnpm exec vitest run packages/plugin-detail/src/__tests__/detailSectionAuthoredNode-8626.test.tsxTest Files 1 passed,Tests 7 passedpnpm exec vitest run packages/plugin-detail/Test Files 173 passed (173),Tests 1602 passed (1602)pnpm --filter @object-ui/plugin-detail type-checktsc -p tsconfig.test.json --listFileslists BOTH new files, so the test leg really covers thempnpm --filter '@object-ui/plugin-detail^...' buildnode scripts/check-changeset-presence.mjs✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)pnpm check:control-bytes✅ OK (scanned 7411 tracked text file(s))pnpm check:new-line-citationsVERDICT new-cross-file-line-citations: 0 new citation(s) -> exit 0pnpm check:doc-types✅ Every documented component type is registered.Lint is narrowed, and the narrowing is declared.
eslint --format jsonover the three touched source files: 3 files linted, 0 errors, 32 warnings, allreact-refresh/only-export-components(31 pre-existing inindex.tsx, 1 new for the exported fold list). The narrowing is safe to read as a measurement because type-aware linting is not enabled —eslint.config.jsdeclares noparserOptions.projectand noprojectServiceanywhere — so this diff cannot move the verdict on any file it does not contain. The repo-wide run (turbo run lint, which sets no--max-warningsby deliberate policy) is CI's.Acceptance notes
DetailSection's collapsible branch never readssection.showBorder, so a declaredshowBorder: falsekeeps its border #9218 —DetailSection's collapsible branch never readssection.showBorder, so a declaredshowBorder: falsekeeps its border. Found as a failing assertion while writing this pin. NOT fixed here: it lives insideDetailSection, which is this PR's declared CONTROL, and it is a different defect class. The pin'sshowBorderrow authorscollapsible: falsefor exactly that reason and says so in place.DetailViewSectioncarries.name,iconandvisibleare undeclared, andsection.iconIS read by the component, so an SDUI author cannot reach an affordance the renderer honours. Not filed: the validator WARNS on those keys rather than dropping them silently, so it is a decision to WIDEN the authoring surface (a product question), not a defect. Whoever picks up finding(plugin-detail): everydetail-sectioninput is a FLAT prop the renderer never reads —DetailSectionreadssection.*only, and an authored node hands itsection === undefined#8626's reserved question will be standing on this exact file.🤖 Generated with Claude Code
https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
Generated by Claude Code