# Format-extension decisions, when a component's contract can't express something

A component's A2UI JSON contract sometimes can't carry a content shape the
authored HTML has, a table cell with mixed prose and inline `<code>`, a
label needing an icon, a slot needing arbitrary nested markup. Read this
before extending any component's declarative contract to close a gap like
that; it doesn't cover corpus/chunk decisions (see
[leverage-rules](leverage-rules.md)), it's about the component's own
prop/construct surface.

## The three-tier ladder, in order of preference

1. **Extend an existing declarative registry the component already has**, e.g. `table-ui`'s `components/table/cell-types.js` (`badge`, `progress`,
   `link`, …). Add a new named type; the component dispatches rendering
   through the registry it already owns. **Smallest blast radius**: no new
   A2UI construct, no runtime/validator changes, no registry entry in
   `packages/gen-ui/a2ui/registry.js`. **Ceiling**: only covers formats the
   registry's render contract can express (inline-safe content, not
   arbitrary nested block markup). Building the new registry entry is a
   web-components primitive edit, hand off to `primitive-authoring` for the
   implementation half; this reference only decides the tier.
2. **New A2UI primitive(s)**, each slot/cell a normal construct subtree (any
   registered component nestable inside it), matches how `Column`/`Row`
   already compose. **Most expressive**, no ceiling on what can nest.
   **Largest blast radius**: new constructs need transpiler synthesis,
   runtime rendering support, and validator rules; changes what the
   containing component MEANS in the A2UI graph model. Reserve for cases
   tier 1 genuinely can't reach.
3. **Raw-HTML/markup passthrough**, wired through an existing visual-only
   escape hatch (e.g. `table-ui[raw]`) registered as an A2UI construct
   carrying sanitized markup as a string prop. **Fastest to ship**, reuses
   infrastructure verbatim. **Weakest fit for a generative pipeline**: an
   LLM composing pages would need to emit raw HTML strings instead of
   constructs, undermining the JSON-graph value the A2UI format exists for.
   Acceptable ONLY as a narrowly-scoped escape hatch (e.g. a docs-migration
   pipeline), never as a pattern taught to the generation engines.

**Default to tier 1.** Climb to tier 2 or 3 only when a concrete case proves
tier 1's ceiling, don't pre-emptively build the more expressive, more
invasive tier for a gap tier 1 can still close.

## Worked precedent, TKT-0008 (table-ui rich cells, 2026-07-13)

`table-ui`'s `{columns, data}` contract only carried plain-string cells;
docs reference tables with inline `<code>` in cells (e.g. `` `variant` `` in
a props table) fell back to a preserved-HTML `Code` block instead of a
`Table` component, the table never reached the A2UI graph. All three tiers
were scoped (rich-cell vocabulary / new `TableRow`/`TableCell` primitives /
raw-HTML passthrough via the existing `table-ui[raw]` prop); tier 1 was
ratified and built: a new `markdown` cell type in `cell-types.js`, rendered
via `core/markdown.js`'s `inline()` (exported for this reuse, the exact
client-side counterpart to the docs-transpiler's own `convertInline()`).
Measured result: the targeted failure class (`structural:tables` in the
strict re-verification sweep) dropped 39→7 pages out of 98 measured; the
residual 7 carry cells genuinely outside tier 1's reach (block content,
component tags) and are an open tier-2/3 follow-up
(`docs/tickets/TKT-0008-table-ui-cannot-express-rich-cells.md`).

## Why this generalizes

The same ladder applies to any AdiaUI primitive with a per-item/per-cell
render dispatch (table columns, list-item variants, chart series formats,
…): check for an existing registry FIRST, size the ceiling honestly against
the concrete case at hand, and only pay the blast-radius cost of a new
construct or a raw-passthrough when the registry provably can't reach it.
