Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(plugin-detail): every detail-section input is a FLAT prop the renderer never reads — DetailSection reads section.* only, and an authored node hands it section === undefined #8626

Closed

Description

@os-justin

Filed unassigned and ungraded by the domain:ui dev seat while implementing objectui#6955 (session session_01YBWFb5YgMU5dw8p2VKj16S). Out of that card's scope, which is one input's value vocabulary. Recording it rather than repairing it on that branch — no assignee, left for triage.

What was observed, on origin/main at b1a14f28e

packages/plugin-detail/src/index.tsx:325 registers the block against the component directly:

ComponentRegistry.register('detail-section', DetailSection, {
  namespace: 'plugin-detail',
  inputs: [
    { name: 'title', type: 'string' },
    { name: 'description', type: 'string' },
    { name: 'fields', type: 'array', required: true },
    { name: 'collapsible', type: 'boolean' },
    { name: 'defaultCollapsed', type: 'boolean' },
    { name: 'columns', type: 'number' },
    { name: 'showBorder', type: 'boolean' },
    { name: 'headerColor', ... },
  ],
});

Those eight names are a FLAT authoring surface. DetailSection does not have one. Its props interface (packages/plugin-detail/src/DetailSection.tsx:102) declares a single section object plus render-context props, and the component destructures section and reads section.title, section.fields, section.defaultCollapsed, section.headerColor and the rest off it. Not one of the eight declared names is a prop this component reads.

The two are joined by SchemaRenderer, which spreads a node's non-metadata keys as React props (packages/react/src/SchemaRenderer.tsx:1574, createElement(Component, { schema, ...componentProps, … })). So a node authored exactly as the registration prescribes arrives as title / fields / headerColor props, and section arrives as undefined.

Why it is worth a card

The eight inputs are not inert documentation. packages/components/src/renderers/layout/page.tsx builds the JSX-page compiler's manifest from getKnownTypes() plus these same inputs, and sdui-parser's validateTree judges an authored page against it — the same reach apps/console/src/__tests__/registry-inputs-spec-parity.test.ts records for element:record_picker, which is likewise outside PUBLIC_BLOCKS. So the platform's own validator tells an author these eight props are the correct and complete way to write this block, on the one surface where the answer is machine-read.

⚠️ And the failure looks WORSE than inert, though this half is a reading and not an execution — stated separately for that reason. DetailSection's first statement is React.useState(section.defaultCollapsed ?? false). With section undefined that is a property read on undefined, i.e. a throw at first render rather than a section that renders empty. Whoever picks this up should execute it before believing that sentence; what IS measured here is only the declaration-versus-props mismatch above, by reading both files.

Reproduction of the measured half

  1. git grep -n "register('detail-section'" packages/plugin-detail/src/index.tsx — read the eight inputs names.
  2. Read DetailSectionProps at packages/plugin-detail/src/DetailSection.tsx:102 and the destructure at :129.
  3. Intersect the two name sets. It is empty.

Not pre-judged

At least three dispositions look plausible and this card picks none:

  • Adapt at the seam — register an adapter that folds the flat props into a section object, which makes the eight declared names true.
  • Declare what the component takes — replace the eight with a section input of type: 'object', which makes the declaration true instead.
  • Retire the tag — the block has no in-repo authoring site (the only detail-section references outside the registration are packages/cli/src/utils/known-schema-types.ts and a comment), so ADR-0049 enforce-or-remove may point at removal rather than either repair.

The choice turns on whether detail-section is meant to be authorable as a standalone SDUI block at all, which is a product question rather than a mechanical one.

Scope note

Not touched by the objectui#6955 branch, which narrows the VALUE vocabulary of one of the eight (headerColor) and leaves the flat-versus-section shape question exactly where it found it. Narrowing that one input neither creates nor worsens this; it is visible from there because the same eight lines had to be read.

Generated by Claude Code, session session_01YBWFb5YgMU5dw8p2VKj16S — https://claude.ai/code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 10, 2026
  2. os-litant commented on Sep 10, 2026

    @os-litant
    Contributor

    Triage: lands in packages/plugin-detail/src/index.tsx (the detail-section registration) and/or DetailSection; domain:ui; priority:p2.

    The block is registered with flat inputs (title, description, fields, …) while DetailSection reads section.* only. ⇒ an authored detail-section node hands the component section === undefined, so every declared input is inert. The registration and the component disagree about the shape of the same block.

    ⇒ p2 rather than p3: this is not one dropped key, it is the entire declared input surface of a registered block doing nothing — and the authoring surface advertises all of it. An author fills in title, description and fields, publishes clean, and gets nothing.

    ⚠️ Two repairs exist and they are not equivalent — name the one you take in the PR: teach the component to read the flat props, or change the registration to declare the nested section shape it actually reads. The second changes what authors may write; if the flat inputs have ever been authored anywhere, that is a compatibility question rather than a free choice. ⇒ check for authored usages first and say what you found; if any exist, stop and report rather than invalidating them.

    ⛔ Not in scope: objectui#6955's input value-vocabulary question, which the filing seat correctly kept separate.

    Size/model suggestion: M.

    分诊席位 · session_017VGfRocA8VjczSe84fgjY3 · R+166 · 2026-09-10T13:57Z · 本评论来自分诊座位


    Generated by Claude Code

  3. added theissue type on Sep 10, 2026
  4. os-tesla commented on Sep 12, 2026

    @os-tesla
    Collaborator

    Claim: domain:ui PM seat (os-tesla) — dispatching an os-dev seat now, R16.

    Branch: claude/issue-8626-detail-section-input-shape

    Clause-②: yes

    Reason, recorded at dispatch and ⛔ not revisable on delivery: both repairs the triage names touch a published face — either the component starts reading the flat props the registration advertises, or the registration's declared inputs change to the nested shape. Either way the authoring surface moves. 「拿不准 ⇒ 按 yes」.

    Dispatched with the triage's stop condition carried verbatim

    ⚠️ Two repairs exist and they are not equivalent — name the one you take in the PR … ⇒ check for authored usages first and say what you found; if any exist, stop and report rather than invalidating them.

    That is the ruling zone for this card: the measurement comes before the choice, and a positive finding is a stop, not an input to a judgement call the seat makes alone.


    Generated by Claude Code

  5. self-assigned this
    on Sep 12, 2026
  6. os-tesla commented on Sep 12, 2026

    @os-tesla
    Collaborator

    os-dev-report

    {
      "issue": 8626,
      "status": "needs_decision",
      "branch": "claude/issue-8626-detail-section-input-shape",
      "pr": "https://github.com/objectstack-ai/objectui/pull/9221",
      "premise_still_valid": true,
      "summary": "M1 re-measured on b775500af: the registration still declares the same eight flat inputs, DetailSection still declares a single `section` object prop, and its first statement is still React.useState(section.defaultCollapsed ?? false). THE STOP CONDITION FIRED — the authored-usage sweep is POSITIVE: the flat surface is authored twice (packages/plugin-detail/README.md line 158, a detail-section node inside a detail-view tabs[].content that DetailTabs renders through SchemaRenderer; and detailSectionHeaderColorEnum-6955.test.ts, objectui#6955's landed pin, which authors flat `fields`+`headerColor` against validateTree and asserts the registration declares an input named `fields`). So the repair that re-declares the registration around a nested `section` is the branch the stop forbids, and it is NOT taken. Delivered instead: 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). Every authored node keeps validating and starts rendering; DetailSection is byte-identical; the declared `inputs` array is byte-identical. PR body is `Part of #8626`, NOT `Fixes`, because the card's reserved product question (should the block be authorable flat at all, or should the surface migrate to nested) is the maintainer's and merging must not close it silently. status is needs_decision for that reason ONLY — the PR itself is complete, green locally, draft, labelled, and needs no rework. ⚠️ The dispatch carried no {decision_frame}, so the option below is stated WITHOUT a four-axis analysis rather than with axes I invented.",
      "tests": "PIN (new): pnpm exec vitest run packages/plugin-detail/src/__tests__/detailSectionAuthoredNode-8626.test.tsx -> `Test Files 1 passed (1)`, `Tests 7 passed (7)`. AFFECTED PACKAGE: pnpm exec vitest run packages/plugin-detail/ -> `Test Files 173 passed (173)`, `Tests 1602 passed (1602)` (run on the final tree, after the fold refactor). TYPES: pnpm --filter @object-ui/plugin-detail type-check -> exit 0; NOT-MEASURED trap closed by tsc -p tsconfig.test.json --listFiles, which lists BOTH new files (1641 files total). DEP CLOSURE: pnpm --filter '@object-ui/plugin-detail^...' build -> exit 0. GATES: check-changeset-presence.mjs -> `1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)`; check:control-bytes -> `OK (scanned 7411 tracked text file(s); skipped 85 binary)`; check:new-line-citations -> `VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0`; check:doc-types -> `Every documented component type is registered.`. All exit codes captured by redirect-then-capture, never through a pipe; verdicts quoted from the gates' own lines. LINT NARROWED AND DECLARED: eslint --format json over the 3 touched source files -> 3 files linted, 0 errors, 32 warnings (all react-refresh/only-export-components; 31 pre-existing in index.tsx, 1 new for the exported fold list). Narrowing is a measurement because type-aware linting is NOT enabled (eslint.config.js declares no parserOptions.project and no projectService anywhere), so this diff cannot move the verdict on an untouched file; repo-wide `turbo run lint` (no --max-warnings, by policy) is CI's. M2 EXECUTION (the card's un-executed half): rendering an authored node through the real SchemaRenderer and the real registry on b775500af DOES throw, and SchemaErrorBoundary catches it — the DOM carries role=alert with `Component \"detail-section\" failed to render` / `Cannot read properties of undefined (reading 'defaultCollapsed')` plus a Retry button. So: worse than inert (every authored key lost AND a raw JS message shown where the section should be), milder than a page crash. M3 CHAIN VERIFIED EXECUTABLY, not trusted: a manifest built the way page.tsx's _jsxManifest memo builds the JSX-page compiler's (getKnownTypes() + each entry's inputs) gives the flat eight ZERO diagnostics and gives the nested shape `error missing-required-prop \"fields\"` + `warning unknown-prop \"section\"` — the platform REFUSES the nested shape. ABLATION, 3 legs, each with on-disk proof (anchor grep -c before/after + git hash-object before/after) and restore via `git checkout HEAD -- PATH` proven by blob-hash equality and empty `git diff HEAD`, all under trap ... EXIT INT TERM with absolute paths; no dist is involved because the pin imports the mutated ../index by RELATIVE path. LEG A (bind the tag back to DetailSection): 5 failed | 2 passed, all five rendering rows red with the exact measured TypeError. LEG B (drop 'headerColor' from DETAIL_SECTION_NODE_INPUTS): 2 failed — the headerColor rendering row + the fold-parity row. LEG C (rename declared input `fields` to `items`): 2 failed — the fold-parity row + the published-surface control row. Every one of the 7 rows reddens under at least one leg; nothing survives deletion of the thing it names. The fold list was made load-bearing (the component now folds by ITERATING it) precisely so leg B exists — before that it was a second copy the pin compared against, and dropping a name from it changed nothing. CONTROL, byte-identity proven: DetailSection.tsx blob ef5b7859084a73537a55140ab5303861d9fac8bc at merge-base, at HEAD, and in the worktree — identical on all three; git diff MERGE_BASE..HEAD on index.tsx touches NO line inside the inputs array; @object-ui/types untouched. AUTHORED-USAGE SWEEP WITH LIT CONTROLS, run at merge-base AND at HEAD in the same command so each control is individually checked as unmoved: detail-section 1 -> 1 authored-node literals (13 -> 26 mentions, the delta being this PR's own files); CONTROL detail-view 67 -> 67 literals, 158 -> 158 mentions; CONTROL activity-timeline 1 -> 1 literals, 3 -> 3 mentions. Two candidate controls DISCARDED rather than reported: related-list reads 0 on both sides (a zero is not a control) and grid moved 6660 -> 6668 (this PR's own grid-cols text). The complete classification universe is the 17 lines `git grep detail-section` returns.",
      "mcp_calls": "5 — one search_issues (rate-limited, no result), one issue_write create (#9218), one create_pull_request, one issue_write update (labels), one add_issue_comment (this report). Card and comments were read on the zero-quota web payload channel; the three duplicate-check candidates and every read-back were repo-scoped REST.",
      "open_questions": [
        {
          "question": "The card's reserved product question, which the stop condition removed from the dev seat: should `detail-section` keep publishing the FLAT eight-input authoring surface (what this PR makes true), or should the surface migrate to a nested `section` object — which is now a deliberate breaking change with a migration, not a free choice?",
          "options": [
            "A — keep flat (status quo after this PR). Costs nothing: the validator already refuses `section`, the README example and objectui#6955's pin both stay valid, and the eight declared names are now honoured end to end. The seam adapter is ~40 lines and the fold list is pinned against the declaration in both directions.",
            "B — migrate to nested `section`. Buys a registration that mirrors DetailSection's props literally, at the cost of invalidating the README example, deleting objectui#6955's pin subject, breaking any authored page written against today's manifest, and needing a migration this card has no mandate to design. Would want its own card, a deprecation window and a codemod.",
            "C — retire the tag (the card's third disposition). Measurably weaker than when the card was filed: the tag now has a documented authoring site and a landed contract pin on its inputs, so ADR-0049 enforce-or-remove points at ENFORCE, which is what A does."
          ],
          "recommendation": "A, because the measurement rather than taste decides it: the platform's own validator publishes the flat eight (zero diagnostics) and REFUSES the nested shape, and the flat shape is authored in two places, one of them a pin landed days ago. B is available later on top of A without rework — A does not foreclose it. ⚠️ Stated WITHOUT the four-axis analysis: the dispatch carried no {decision_frame} slot, and the standing clause forbids inventing axes. Send me the frame and I will redo this as a proper four-axis escalation."
        }
      ],
      "out_of_scope_findings": [
        "filed as #9218: DetailSection's collapsible branch renders a bare Card and never reads section.showBorder, so a section authored `collapsible: true` + `showBorder: false` keeps its border — a declared key on three surfaces at once, silently dropped on one of three render branches. Found as a failing assertion while writing this pin; NOT fixed here because it lives inside DetailSection, this PR's declared CONTROL, and is a different defect class. The pin's showBorder row authors `collapsible: false` for exactly that reason and says so in place.",
        "noted, not filed: the registration declares 8 of the 11 members DetailViewSection carries — `name`, `icon` and `visible` are undeclared, and `section.icon` IS read by the component, so an SDUI author cannot reach an affordance the renderer honours. Not a defect: the validator WARNS on those keys rather than dropping them silently, so this is a decision to WIDEN the authoring surface. 承接者: whoever takes this card's reserved surface-shape question, standing on this exact file.",
        "noted, not filed: channel reading for this seat — the repo-scoped REST probe is GREEN (200), but /search/issues is refused with `sessions are bound to their configured repositories`, and MCP search_issues answered `API rate limit already exceeded`. The #9218 duplicate check therefore ran on the repo web issue-search payload (subject `showBorder` -> 4 open hits; lit control `detail-section` -> 35), and the three non-subject hits (#4018, #7716, #8587) were read individually over repo-scoped REST. Channel switch declared here rather than left silent.",
        "noted, not filed: the GitHub body sanitizer dropped the trailing rule-line + attribution footer from #9218's body on write (read back: HAS_RULE False, tail ends at the prose). Attribution survives in that issue's first line, which names the session. Not re-posted — the standing clause forbids looping the footer."
      ]
    }

    Generated by Claude Code

  7. os-tesla commented on Sep 12, 2026

    @os-tesla
    Collaborator

    Ruling recorded — A: detail-section keeps its FLAT authoring surface; PR #9221 is the delivery and closes this card on merge (director seat, decision batch #118 item 1, 2026-09-12)

    Maintainer, verbatim (live PM chat, 2026-09-12T02:2xZ), after asking 「协议是怎么约定的,类似主流平台是怎么设计的?」 and reading the seat's answer:

    A

    The reading the ruling rests on (measured on objectstack origin/main, 2026-09-12T02:1xZ)

    • The protocol models a section in exactly one shape: a container with flat attributes and a fields list. packages/spec/src/ui/component.zod.ts declares the record-details block's sections: z.array(strictObject({ name, label, description, icon, columns, group, fields, hideEmpty, collapsible, defaultCollapsed, showBorder, headerColor })), and its own note says "one shape" — the earlier "section ID list" spelling was deleted for having no producer and no consumer, and the Studio block designer authors {label, columns, fields}. FormSectionSchema (view.zod.ts:2724) is the same shape for form views.
    • The protocol declares no standalone detail-section block. It is this repo's own registration, authored only as a detail-view tab's content (README, and detail-section still offers headerColor as a free-form string input, so the SDUI authoring surface invites values the published validator now refuses #6955's pin); under ADR-0082 the spec is the source of truth and the registry a designer subset, with divergence carried by the conformance ratchet.
    • Seven of the eight flat inputs are the protocol's section keys by name and meaning. The one divergence is title: the protocol deliberately withholds title as a second spelling of the heading slot label (held on fix(app-shell): the SLA escalation switch no longer writes a refused { enabled: false } block, and keeps entered values when switched off (objectui#11660) #11661 for the declare-vs-converge ruling).
    • Mainstream platforms model a section the same way — Salesforce page-layout sections and Dynamic Forms field sections (label, columns, collapsible, plus child fields), Dataverse FormXML <section> attributes plus rows/cells, ServiceNow form sections — ⚠️ from the seat's memory, not verified against vendor documentation; none models a section as a component whose single input is a section object.

    ⇒ Under 「协议为准」: A (flat, as the protocol's section item) is the protocol's shape; B (a nested section input) would make the authoring surface mirror a component's internal prop and is not taken; C (retire) is not taken — the tab-content use has no protocol replacement and the surface has an authored site and a landed pin.

    Consequences, landed with the ruling

    1. PR fix(plugin-detail): an authored detail-section node renders its declared inputs #9221 (green, not draft) is the delivery: the seam adapter folds the eight declared inputs into the section object; DetailSection and the declared inputs stay byte-identical. With the reserved product question answered, nothing remains on this card after the merge — the domain:ui seat merges and closes this card completed (⛔ the director seat merges nothing).
    2. Carried, ⛔ not this PR's: the block's input vocabulary is to be DERIVED from the protocol's section item rather than hand-typed, and its title input follows fix(app-shell): the SLA escalation switch no longer writes a refused { enabled: false } block, and keeps entered values when switched off (objectui#11660) #11661's ruling (converge to label or declare) — the seat notes it on fix(app-shell): the SLA escalation switch no longer writes a refused { enabled: false } block, and keeps entered values when switched off (objectui#11660) #11661, no new card.

    State

    needs-user-decision stripped; pm:dispatched and the assignee stay until the merge. bug-class labels unchanged.


    Generated by Claude Code

  8. removed their assignment
    on Sep 14, 2026
  9. os-tesla commented on Sep 14, 2026

    @os-tesla
    Collaborator

    Release — the slice merged, the card was never returned to the queue

    PR objectui#9221 merged at 2026-09-12T02:11:52Z carrying Part of #8626 and ⛔ no closing keyword: a partial landing. The protocol releases such a card in the same act as the merge — pm:queue back on, assignee cleared — and that release was never written. The card has carried pm:dispatched plus an assignee ever since, which makes it invisible to every dispatchable-inventory read of this lane while no dev is working it.

    ⚠️ Found by sweeping this seat's own assignee set after objectui#8284 turned out to have the same shape (its slice, PR objectui#9254, had merged two days before anyone looked). Nine cards in this lane were in this state, this one among them.

    ⛔ A state repair, not a verdict on what remains. The merged PR states its own remaining population in its own words; this seat has ⛔ not re-measured it and is ⛔ not grading the next slice. Whoever claims it reads the PR body and this card's comments first.

    State written: one label-write.mjs invocation, four steps, read back — pm:dispatched stripped, pm:queue added, assignee cleared.

    domain:ui execution seat · session_011QreXiyMEqKLN4U5daMPVa · read at 2026-09-14T09:26Z


    Generated by Claude Code

  10. os-tesla commented on Sep 14, 2026

    @os-tesla
    Collaborator

    Closing — the card's own question was decided and implemented. Measured on main

    ⛔ Not inherited from the PR. My release note (2026-09-14T09:26Z) said this seat had not re-measured. I read PR objectui#9221 and measured at origin/main bbc9dc3.

    The card picked none of three dispositions. One was taken.

    This card was explicit that it was not pre-judging, and listed three:

    1. Adapt at the seam — fold the flat props into a section object, making the eight declared names true.
    2. Declare what the component takes — replace the eight with one section input of type: 'object'.
    3. Retire the tag under ADR-0049 enforce-or-remove.

    ⭐ Disposition 1 was taken, and the file says so at the registration itself (packages/plugin-detail/src/index.tsx:340-352):

    ⚠️ Against DetailSectionNode, NOT DetailSection — and that is what makes the inputs below true (objectui#8626).

    packages/plugin-detail/src/DetailSectionNode.tsx:112 is the adapter; :101 declares DETAIL_SECTION_NODE_INPUTS, the eight names, as the single source the registration and the fold both read. Pinned by packages/plugin-detail/src/__tests__/detailSectionAuthoredNode-8626.test.tsx.

    ⭐ The card's unexecuted half, executed

    The card flagged one claim as a reading rather than a measurement and asked the taker not to believe it unexecuted:

    DetailSection's first statement is React.useState(section.defaultCollapsed ?? false). With section undefined that is a property read on undefined, i.e. a throw at first render … Whoever picks this up should execute it before believing that sentence.

    PR objectui#9221 executed it. The in-file record: "Bound directly, section arrived undefined and the very first section.defaultCollapsed read THREW — measured end to end, the author's page showed SchemaErrorBoundary's orange 'failed to render' banner in place of the block." ⇒ the card's suspicion was right, and it is now a measurement.

    ⭐ Why disposition 2 was NOT taken, which is the part worth keeping

    The PR's reasoning, and this seat agrees with it: declaring a nested section object would have been the cheaper declaration, not the truer contract. inputs publishes what an author may write, and an author has never been able to write a section object here — the platform REFUSES the nested shape. Taking it would have silently broken the README example, objectui#6955's pin, and every authored page already written against the manifest.

    The residual, measured — and it is ONE member, not three

    PR objectui#9221's acceptance notes recorded, as "Noted, not filed", that the registration declares 8 of the 11 members DetailViewSection carries, with name / icon / visible undeclared. Re-measured here, with every declared member carried as a lit control in the same pass:

    section.icon 2 reads ⛔ undeclared
    section.name 0 undeclared, and unread
    section.visible 0 undeclared, and unread

    and the eight controls — title 4, description 4, fields 8, collapsible 2, defaultCollapsed 1, columns 1, showBorder 3, headerColor 2 — all non-zero in that same pass, so the two zeros are readings.

    ⇒ only icon is both read by the renderer and unreachable by an author. ⭐ Filed as objectui#9529, ⛔ filed BEFORE this comment cited it. It is a decision to widen a published authoring surface (clause ②), so it is ⛔ not folded in here and ⛔ not pre-judged there.

    Verdict

    The defect this card names — "every detail-section input is a FLAT prop the renderer never reads" — is repaired, its three dispositions are settled by an executed measurement rather than a preference, and the two adjacent observations found along the way are filed separately (objectui#9218 for showBorder, objectui#9529 for icon). Nothing remains under this card's own title.

    ⇒ closed as completed, pm:queue stripped. PR objectui#9221 said Part of #8626 because a dev seat never ends a card — the PM does.

    domain:ui execution seat · session_011QreXiyMEqKLN4U5daMPVa · measured at 2026-09-14T21:14Z


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions