Skip to content

test(schema-catalog): ratchet deprecated component types, and report why the div sweep cannot run as ruled - #6732

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-3965-catalog-retire-div
Aug 29, 2026
Merged

os-sales merged 1 commit into
mainfrom
claude/issue-3965-catalog-retire-div

Conversation

@claude

@claude claude Bot commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

Part of #3965

Authored by the domain:ui execution seat, session session_01CRJge11jso9TpXRWFt1Z49 (recorded here in prose because a body edit rewrites the footer to its bare form).

Lands the closing lint the 2026-08-29 ruling ordered, in ratchet form, and reports the measurement that says the sweep half cannot run as specified. #3965 remains open as the worklist carrier.

Both of the card's headline numbers are wrong on today's ref

The card's own query was grep -rl '"type": "div"' examples/schema-catalog/src/schemas | wc -l, so the 48 counted FILES. Re-counted in that unit on origin/main @ 9486ac672:

unit card (2026-08-09) measured now
files (the card's unit) 48 27
nodes (occurrences) not stated 85
corpus 423 files 431 files

Positive control in the identical query shape: "type": "card" = 93 nodes / 53 files, the same 93 measured on 08-10, 08-18 and 08-28. So the div drop is a real migration (#4003 moved 106 nodes), not a broken pattern.

The card's stated symptom is also already gone. It says "a deprecation warning per thumbnail". Rendering all 431 entries through the real SchemaRenderer with a console.warn spy active from the first entry:

entries_rendered=431  total_warns=4  div_deprecation_notices=1

One notice for the whole corpus — DivRenderer reports once per module load (#3998) and exempts kind:'html' nodes by provenance (#4000). The other three warns (react-i18next, a visibleWhen predicate, an expression failure) are the positive control that the spy was capturing, so 1 is a measurement rather than a silent spy. ⚠️ The first run of this census read 0, because an earlier test in the same file had already latched the once-per-module-load guard; the number above is from an isolated file. A census that shares a module instance with any other div render is not measuring the corpus.

Where the deprecation is declared, and why no fixture was swept

packages/components/src/renderers/basic/div.tsx, as a console.warn string literal. It names card / flex / container / stack / grid. None of them is a drop-in, measured through the real SchemaRenderer on identical authored input { className: 'p-4', children: [...] }:

(Angle brackets are spelled out as TAG[classes] below — this body is stored through a sanitizer that eats short tag-shaped fragments, backticks included.)

type wrapper class attribute emitted children
div div p-4 — the authored value, verbatim inline
container div w-full max-w-xl mx-auto sm:p-3 md:p-4 p-4 inline
flex div flex flex-row justify-start items-start gap-1.5 sm:gap-2 p-4 inline
stack div flex flex-col justify-start items-stretch gap-1.5 sm:gap-2 p-4 inline
grid div grid grid-cols-2 gap-4 p-4 inline
card div rounded-lg border bg-card text-card-foreground shadow-sm p-4 wrapped in an extra CardContent element, p-6 pt-0
section section p-4 — the authored value, verbatim inline

div is class-transparent; every replacement injects layout. Note container: twMerge does not strip the injected sm:p-3 md:p-4, because those are different responsive variants from the authored p-4 — the box keeps the authored padding at base and silently gets smaller padding from sm up. And none of the 85 nodes authors a single layout prop (the only keys across all 85 are type, className, children, body) or carries a flex/grid display class, so there is no authored intent to carry across — the injection is pure addition.

The second difference is not cosmetic. container / flex / stack / grid render schema.children only; div renders schema.children || schema.body. Four of the 85 nodes author body. Measured on the real basic-sidebar.json, swapping only its div:

as authored (div)   elements=21  text="Menu…Settings Main content area"
swapped -> stack    elements=21  text="Menu…Settings"        <- content GONE
swapped -> flex     elements=21  text="Menu…Settings"        <- content GONE
swapped -> container / grid — same, content GONE

The element count is unchanged at 21, so the loss is invisible to any element-count check, and catalog-gallery-render's drewSomething control still passes because the sidebar beside it draws. That is the silent catalog-wide regression a find-and-replace would have shipped — worse than the warning it removes, exactly as the dispatch warned.

section / article / aside are the one class-transparent family (semantic.tsx renders Tag with className and injects nothing, and reads children || body), so they are technically one-to-one — but they teach landmark elements as generic boxes across 27 exemplars, which is negative on the AI-authoring axis this card exists to protect. Three parties have now independently recommended against it (the 2026-08-09 implementing agent, the PM, and the 2026-08-28 dev); this makes four.

No fixture was touched. Under the 2026-08-25 escape clause the residue is excluded, and the 2026-08-29 ruling's own acceptance criterion — "browser spot-verification that renders are unchanged" — is the thing that cannot be met.

What this PR does land

A catalog-scoped ratchet, examples/schema-catalog/test/deprecated-component-types.test.ts, delivering the ruling's stated purpose ("so the stock never regrows") in the only form that can ship before the sweep.

Nothing stopped the 49th before this. Both gates that touch component types ask only whether the type resolves, never whether it is deprecated — catalog-gallery-render asserts ComponentRegistry.get(type) is truthy (green today with all 85 deprecated nodes present), and check-doc-component-types.mjs walks content/docs and not examples/**. It is worse than the usual declared-but-unenforced shape: the deprecation is not declared anywhere machine-readable — RegistryComponentMetaExtras has no deprecated field, so the only statements of it are the console.warn literal and the label 'Container (Deprecated)'. The gate therefore mirrors the list by hand and pins its own premise.

  • Freezes the stock at 25 files / 80 nodes; refuses a new file, refuses growth inside a baselined file, and refuses removals left un-deducted from the table (stale allowance would silently re-admit what a batch just removed).
  • span is zero-tolerance — 0 nodes in this corpus at every ref measured, so nothing to grandfather.
  • components-basic-div/ (2 files / 5 nodes) is exempt, not baselined: that category is the migration guidance for this deprecation, shipping use-card-instead / flex-layout / grid-layout beside nested-divs / custom-card. Sweeping it would delete the before-and-after. Same class as div.mdx's deliberate legacy examples. The exemption is checked for non-vacuity so it cannot become a hole nothing uses.
  • Pins that div.tsx / span.tsx still declare their notices, so the hand-kept mirror dies loudly rather than outliving the deprecation.

The hand-kept mirror is a symptom, not a design choice: #6674 tracks giving the registry a machine-readable deprecated flag. If that lands, DEPRECATED_TYPES here derives from the registry instead of being mirrored, and the premise pin can be deleted.

27 files / 85 nodes = 25/80 baselined + 2/5 exempt.

Verification

Every arm goes RED against unfixed source — committed first, each mutation proven on disk before reading any result, each restore pinned to HEAD and proven byte-identical:

mutation disk proof gate red arm
new fixture outside the stock authors a div injected-div-count=1 exit 1 no fixture outside the frozen stock…
baselined fixture gains a node div-count 1→2 exit 1 no baselined fixture grows…
baselined fixture loses a node div-count 1→0 exit 1 the ratchet has no slack…
a span appears span-count=1 exit 1 span is zero-tolerance…
notice removed from div.tsx notice-count 1→0 exit 1 the deprecation this ratchet mirrors is still declared

Restore proof: git diff HEAD empty, git status --porcelain empty, and HEAD-blob hash equality for both mutated files (empty hash treated as failure, not "nothing to compare"). Gate re-run on the restored tree: Tests 7 passed (7), exit 0.

Also run, exit code captured before any pipe:

  • pnpm exec vitest run examples/schema-catalog/ — Test Files 15 passed (15), Tests 1853 passed (1853), exit 0.
  • pnpm exec eslint --no-inline-config --format json on the changed file — 1 file, 0 errors, 0 warnings. Declared narrowing: the repo-wide pnpm lint was not run. The population is read from eslint's own config, the file count off its --format json output, and the config is not type-aware (no project: / projectService in eslint.config.js), so this diff cannot move the verdict on any untouched file. CI runs the full farm regardless.
  • regenerate:check — src/index.ts is up to date (431 entries), exit 0.
  • check-changeset-presence.mjs — its own verdict: "No source of a released package changed in this range, so no changeset is owed." check-changeset-no-major.mjs — "No changeset declares a major bump." A changeset is included anyway, in the empty-frontmatter form AGENTS.md blesses for test-only changes, so "no release" is declared rather than inferred.
  • pnpm --filter @object-ui/example-schema-catalog run type-check (objectui spells it with the hyphen) — exit 0, zero error TS lines, after pnpm --workspace-concurrency=2 --filter '@object-ui/example-schema-catalog^...' build (exit 0). Run before that build it exits 2 with 37 errors, 0 of them naming this PR's file — every one is TS2307 / TS2882 "cannot find module @object-ui/*" in files this PR does not touch, i.e. the unbuilt sibling dist/*.d.ts closure of a fresh worktree. Recorded because it is the trap, not a finding. The new file is genuinely in the program rather than silently excluded: tsc -p tsconfig.test.json --listFiles hit = 1.

What is not checked by anything

Thumbnail appearance. That is precisely why the renderer comparison above is the gate here and not the test suite — and why no fixture was swept.


Generated by Claude Code

Every catalog entry is an exemplar — the corpus a human copies from and that
few-shot retrieval hands a generating model — so a fixture authoring a type the
engine deprecates teaches the deprecated spelling at corpus scale. Nothing
stopped the next one: both gates that touch component types ask only whether the
type RESOLVES (catalog-gallery-render asserts ComponentRegistry.get(type) is
truthy; check-doc-component-types walks content/docs and not examples/**), and
the deprecation is not declared anywhere machine-readable at all.

Freezes the measured stock (25 files / 80 nodes on 9486ac6) and refuses growth
in either direction: a new file, a baselined file gaining nodes, or removals left
un-deducted from the table. `span` is zero-tolerance — it has no stock to drain.
`components-basic-div/` is exempt because it IS the migration guidance for this
deprecation, shipping use-card-instead/flex-layout/grid-layout beside the
deprecated demonstrations.

The stock is frozen rather than swept because the replacements the deprecation
notice names are not drop-in; the measurement is recorded in the file's docblock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant