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(docs): collapsible.mdx's schema block names a required children row the renderer never reads, and omits the required content the renderer does read #8197

Closed

Description

@os-justin

Found while implementing objectui#7767 (the CollapsibleSchema.trigger widening), whose scope is the trigger row of the same fence. Fenced out on triage's own terms there. Measured on origin/main daf9d576d.

The row

content/docs/components/disclosure/collapsible.mdx:16 publishes:

interface CollapsibleSchema {
  type: 'collapsible';
  trigger: SchemaNode | SchemaNode[];
  children: SchemaNode[];
  defaultOpen?: boolean;
}

Two things are wrong with the children line, both about the SAME slot:

  1. The declaration has no such required member. packages/types/src/disclosure.ts declares the content slot as content: SchemaNode | SchemaNode[]; — required. children reaches CollapsibleSchema only through BaseSchema (packages/types/src/base.ts), where it is children?: SchemaNode | SchemaNode[] — OPTIONAL, and a different union. So the docs row names the wrong key, marks it required when the inherited one is optional, and spells a narrower type than the inherited one.
  2. The renderer never reads it. packages/components/src/renderers/disclosure/collapsible.tsx reads renderChildren(schema.content) inside CollapsibleContent, with no children fallback of any kind. The registration's defaultProps likewise ships content, not children.

⇒ an author who follows this page authors children, no read site consumes it, the required content is absent, and the collapsible renders with an empty body. The content key — the only one that works — is not documented on the page at all.

Same class as objectui#6805 (scroll-area demos authoring content, which that renderer never reads) and objectui#6773 (aspect-ratio demos, same shape), both of which were confirmed real and closed. This one is the mirror image: the docs name a key the renderer ignores, rather than the demos doing so.

Why no gate catches it

check:doc-types reads only type string literals out of doc code blocks, so it never judges a member row. check:doc-snippets compiles only ts / tsx / typescript fences (TS_FENCE_LANGUAGES in scripts/check-doc-snippet-types.mjs); this block is fenced plaintext, so it is outside that gate's population by construction — measured: zero ts/tsx/typescript fences in this file, one plaintext fence. The neighbouring trigger row on this same page had drifted for the same reason and had to be re-derived by hand under objectui#7767.

Related but not the same: objectui#6143 records that component reference pages declare their own interface so the doc-snippet gate cannot see the props they invent — that is the gate-blindness class; this card is one measured instance of a wrong row, on a specific page.

Suggested fix

Re-derive the row from the declaration: replace children: SchemaNode[]; with content: SchemaNode | SchemaNode[];. Whether the inherited optional children deserves its own row is a separate judgement for whoever picks this up — the defect reported here is that the required slot is undocumented and a non-read key is documented in its place.

Dedup: one targeted semantic search on this repository returned no card on the collapsible docs content slot; the same search returned objectui#7767 and the two same-class cards above, so it was not returning an empty set.

Refs: objectui#7767 · objectui#6805 · objectui#6773 · objectui#6143.

Written by the dev of the objectui#7767 dispatch, session session_01YBWFb5YgMU5dw8p2VKj16S, via Claude Code (https://claude.ai/code).

Activity

  1. added
    bugSomething isn't working
    documentationImprovements or additions to documentation
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 7, 2026
  2. self-assigned this
    on Sep 7, 2026
  3. os-justin commented on Sep 7, 2026

    @os-justin
    CollaboratorAuthor

    Claim: session_01YBWFb5YgMU5dw8p2VKj16S · branch claude/issue-8197-collapsible-content-row

    PM dispatch. Assignee and this claim are set by the PM seat on behalf of the dev seat; the dev inherits both and posts no second claim.

    ⚠️ Re-anchored — the card's base has moved, and the file has changed since

    This card was measured on daf9d576d. objectui#8199 has since landed and edited this exact file — it is the objectui#7767 work this card was split out of, and it rewrote the trigger row in place. Re-measured just now on origin/main f57ca757b, printing the lines rather than trusting the count:

    12  ```plaintext
    13  interface CollapsibleSchema {
    14    type: 'collapsible';
    15    trigger: SchemaNode | SchemaNode[];      <- objectui#8199 landed this
    16    children: SchemaNode[];                  <- THIS CARD'S ROW
    17    defaultOpen?: boolean;
    18  }
    19  ```
    

    children: happens to still be on line 16 — the trigger row was replaced in place, so the line count did not move. ⛔ That is a coincidence confirmed by measurement, not a licence to trust the number: re-derive the anchor on your own base before editing, and never treat a line number as an instruction about what to delete.

    Both of the card's claims re-verified on f57ca757b:

    • Declaration side — packages/types/src/disclosure.ts:113 reads content: SchemaNode | SchemaNode[];, required, exactly as the card says. children reaches CollapsibleSchema only through BaseSchema as optional, and as a different union.
    • The page documents a key the renderer never reads and omits the only key that works.

    Landing control, established BEFORE any edit — do not re-derive, do satisfy

    File content/docs/components/disclosure/collapsible.mdx, measured with grep -cF (fixed-string, because the | in the target would be alternation under a regex grep — that mistake has cost this session seven times):

    leg now required after
    children: SchemaNode[]; 1 0
    content: SchemaNode | SchemaNode[]; 0 1
    lit control — SchemaNode (any line) 2 must stay non-zero

    Bidirectional on purpose: the removal alone could be satisfied by deleting the row, which is not the fix.

    The one judgement this card leaves open — it is yours to make and to argue

    The card states the defect (required slot undocumented, non-read key documented in its place) and deliberately does not rule on whether the inherited optional children deserves a row of its own. Decide it, and write the reasoning into the PR body — either answer is acceptable if argued from the declaration and the renderer, neither is acceptable as an unstated default.

    ⛔ Scope fence: the trigger row is objectui#7767's and is already correct on main. Do not touch it. Do not touch content/docs/releases/.

    Note on gate coverage — expect NO gate to redden

    check:doc-types reads only type string literals from doc code blocks; check:doc-snippets compiles only ts/tsx/typescript fences and this block is plaintext, so it is outside that gate's population by construction. ⇒ the deliverable is one no gate will confirm for you. Your own before/after measurement with a firing control IS the verification; say so plainly rather than reporting a green CI as evidence for this row.


    Generated by Claude Code

  4. os-justin commented on Sep 7, 2026

    @os-justin
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 8197,
      "status": "done",
      "branch": "claude/issue-8197-collapsible-content-row",
      "pr": "https://github.com/objectstack-ai/objectui/pull/8237",
      "premise_still_valid": true,
      "summary": "Both of the card's claims re-verified on my own base f57ca757b, then fixed. `content/docs/components/disclosure/collapsible.mdx` line 16 replaced `children: SchemaNode[];` with the declared `content: SchemaNode | SchemaNode[];` (matched on TEXT, not on the line number), plus a four-line prose note below the fence in the shape tooltip.mdx uses for its own unread `children`. RULING on the open judgement: NO row for the inherited optional `children`. Reasoned in the PR body from three legs — (a) the fence lists what this component reads and carries none of the other BaseSchema keys (id, visible, className, testId, bind, body), so admitting `children` alone is arbitrary and this page has no Base-properties section to hold it; (b) the two same-class precedents document `children` because their RENDERERS read it (scroll-area.tsx:34 `renderChildren(schema.children)`, aspect-ratio.tsx:33 `renderChildren(schema.children || schema.body)`) — read-ness is the discriminator, not inheritance, and collapsible.tsx:28 reads `schema.content`; (c) a row for a key the renderer provably ignores is the same trap this card removes, merely downgraded from required to optional, so the repo's existing shape for that case (tooltip.mdx:24-27 prose) is the one used. Assignee and the PM claim inherited; no second claim posted, assignee never written.",
      "tests": "LANDING CONTROL, grep -cF, three legs, before -> after: `children: SchemaNode[];` 1 -> 0 (required 0); `content: SchemaNode | SchemaNode[];` 0 -> 1 (required 1); lit control `SchemaNode` on any line 2 -> 2 (required non-zero). The two directed legs are each other's firing control — leg 1 returned 1 before the edit and leg 2 returns 1 after, so neither grep is silently matching nothing. END-TO-END DEMONSTRATION (asked for, and cheap, so done rather than proxied), two nodes identical but for the content key, both defaultOpen true, both roots confirmed data-state=open / aria-expanded=true. Zod twin safeParse: OLD-row authoring REFUSED — success=false, issue {code invalid_type, expected nonoptional, path [content], message 'Invalid input: expected nonoptional, received undefined'}; NEW-row authoring ACCEPTED — success=true. Render via renderComponent (jsdom, real registration): OLD row's content div ends immediately after its attributes, no BODY-MARKER text node anywhere; NEW row's content div holds BODY-MARKER. The NEW leg is the firing control for the OLD leg's negative reading. Both probes were throwaway files, run then deleted; `git status` clean at 4b9be5954, tree carries the docs change only. GATES — all run in the foreground, exit codes captured before any pipe, verdict lines read rather than bare $?: check:doc-fences 0 (227 docs); check:doc-types 0 (889 `type` literals — it reads only type literals and never judges a member row); docs:check-links 0 ('Links are valid across 17 scan roots.'); check:control-bytes 0 (6556 tracked text files) plus a direct C0/C1 grep of the changed file, no hits; check:doc-example-readers 0; check:docs-route-closure 0; check-doc-expression-carriage 0 (report-only by design); check-changeset-presence 0 — 'No source or published contract of a released package changed in this range, so no changeset is owed', so no .changeset file and no label. check:doc-snippets exited 2 PRECONDITION NOT MET (five packages unbuilt) — recorded as NOT MEASURED, neither green nor red; independently of that it cannot judge this block, measured here rather than taken from the brief: TS_FENCE_LANGUAGES at scripts/check-doc-snippet-types.mjs:548 is {ts,tsx,typescript}, and this file has 0 such fences and 1 plaintext fence. NONE OF THESE GATES JUDGES THE DELIVERABLE — the before/after measurement with its firing control and the render demonstration are the verification, and no green CI is offered as evidence for the row. SCOPE: `turbo ls --affected` against BASE returns 0 packages (content/ is outside every workspace package), so no package test or typecheck is owed. Repo-wide eslint measured, not assumed: `eslint --format json` on the changed file returns 'File ignored because no matching configuration was supplied' — every block in eslint.config.js targets **/*.{ts,tsx}, so this diff is outside eslint's population entirely (a measurement, not a narrowing). fumadocs-mdx regenerates cleanly (exit 0), so the page still parses. Gate families derived by hand from objectui's own package.json and .github/workflows — objectui has no scripts/pm/dispatch-gates.mjs and the objectstack one was NOT pointed at this tree. check-governed-queue-guard.mjs --test on the changed path: 'NOT GOVERNED — 1 path(s) checked against 5 governed surface(s); none matched.' All heavy runs went through the shared container lock (/home/user/objectstack/scripts/pm/os-verify-lock.sh, slot issue-8197); longest wait 85s, all VERDICT lines command-exit 0 except the two deliberate ones noted above.",
      "mcp_calls": "8 — 1 search_issues (dedup), 3 issue_write (findings), 1 create_pull_request, 1 pull_request_read (body read-back), 2 comment write + read-back. REST was unavailable this session: the repo-scoped probe GET /repos/objectstack-ai/objectui/issues/8197 returned 403 'GitHub access is not enabled for this session', so the card body and its timeline came from the zero-quota public-repo payload channel (frontTimelineItems hasNextPage false, 9 items, newest = the PM claim naming my branch) and every write had to go through MCP.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as objectui#8234: scroll-area.mdx documents a `content?` member that ScrollAreaSchema (packages/types/src/layout.ts:488-507) does not declare and that scroll-area.tsx:34 never reads, and labels `children?` — the only content channel — 'Alternative content prop'. Exact mirror of this card on another page; objectui#6805 addressed that component's DEMOS, not its reference page. No label, left for triage.",
        "filed as objectui#8235 (label `finding`): census — component reference pages document `on*` handler rows their zod twins refuse by name (handlerKeyRefusal runtime-slot is a z.custom arm whose predicate always returns false). 3 CONFIRMED on top-level schemas: accordion.mdx:30 and toggle-group.mdx:41 (onValueChange), pagination.mdx:26 (onPageChange). 14 further rows deliberately left UNCLASSIFIED rather than overclaimed — some sit on nested item interfaces, and tree-view's onNodeClick is not in the refusal set at all. Adjacent to but distinct from the closed objectui#7898, which covered JSON fences under content/docs/guide/.",
        "filed as objectui#8236 (label `finding`): CollapsibleSchema.open is declared at disclosure.ts:122 and accepted by the zod twin at disclosure.zod.ts:55, but no renderer reads schema.open — collapsible.tsx passes only defaultOpen, and the {...props} spread carries renderer props, not the schema. Deliberately NOT documented on the page under this card: adding a row for an inert key is the trap this PR removes. Wire-it vs retire-it is a contract decision, not a docs edit."
      ]
    }

    Notes the JSON has no field for

    Nothing in the brief was found to be wrong. Every claim in it was re-derived here and held: disclosure.ts:113 is content: SchemaNode | SchemaNode[]; required; base.ts:260 is children?: SchemaNode | SchemaNode[] optional; collapsible.tsx:28 reads schema.content with no fallback; defaultProps ships content; the three control legs read 1 / 0 / 2 before the edit, exactly as the PM's table said. The children: row was indeed still on line 16 — but the anchor was re-derived and matched on text, and the edit asserted a unique text match rather than addressing a line.

    Two things the brief could not know, reported as required:

    1. The failure is sharper than "renders an empty body". The zod twin makes content non-optional, so a node authored per the old row is refused by name at validation wherever anything validates it, and paints an empty body wherever nothing does. Both halves are demonstrated above.
    2. Attribution instruction conflict, flagged rather than silently resolved. A session-level instruction mandated one PR footer form; the dispatch contract mandates the session-URL _Generated by [Claude Code](...)_ form. I wrote the session-level one, and the platform then appended its own session-URL block, so PR docs(disclosure): collapsible.mdx documents the content slot the renderer reads #8237's body now carries two attribution blocks. Read-back confirms the body is otherwise byte-intact end to end. Per the no-reposting rule I did not edit it to tidy this up.

    Draft PR left in draft. ⛔ Not flipped ready, ⛔ no auto-merge — the PM lands it. Note for landing: mergeable_state reads behind (main moved to 4f9f1ee3b while this ran); the diff is one file, so no conflict is expected.


    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

Labels

bugSomething isn't workingdocumentationImprovements or additions to documentationdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfindingpm:queuepriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions