# Common gotchas, authoring traps

Seven concrete failure modes: five from a billing-overview rebuild + multi-demo grandfather-elimination cycle (CSS/composition-layering traps, §§1–5, composite authoring specifically), one from the site-a2ui migration's router-race root cause (async-lifecycle sequencing, §6, any primitive or module with a multi-await lifecycle method), plus one from minting `anchor-bar-ui` (generated-artifact regeneration ordering, §7, any change that mints/renames a component and touches a demo using it in the same pass). Each is the kind of bug that:

- Renders visually broken without console errors
- Passes existing audits silently
- Is fixed in one place but recurs in others until pattern-corrected

Composite authors: read §§1–5 BEFORE Phase 3 sketch. Anyone adding an async lifecycle method (fetch, dynamic `import()`, any multi-`await` sequence) to any primitive or module: read §6. Anyone minting or renaming a component whose PR also regenerates a demo using it: read §7. Each entry includes the pattern, the detector (if any), and the fix.

## Contents

1. [Component used without reading its CSS](#1-component-used-without-reading-its-css--and-especially-without-reading-its-composition-grammar)
2. [Parent CSS overriding child component's intrinsic display](#2-parent-css-overriding-child-components-intrinsic-display)
3. [Mixed sizes across form/control groups](#3-mixed-sizes-across-formcontrol-groups)
4. [minmax(min, 1fr) inside repeat() fighting container queries](#4-minmaxmin-1fr-inside-repeat-fighting-container-queries)
5. [Nested `<!-- ... -->` inside design-plan canonical-sketch fenced blocks](#5-nested----inside-design-plan-canonical-sketch-fenced-blocks)
6. [Async load/render function completing out of order](#6-async-loadrender-function-completing-out-of-order--a-guard-at-the-checkpoint-isnt-enough)
7. [Minting a wrapper-shaped component before its registry.js entry lands](#7-minting-a-wrapper-shaped-component-before-its-registryjs-entry-lands--the-transpiler-silently-deletes-the-node-not-just-mis-types-it)

---

## 1. Component used without reading its CSS, and especially without reading its composition grammar

**Pattern**: Stamping `<X-ui>` and relying on attributes/slots without opening `X-ui.css`. The primitive's `@scope` rules ARE part of its API contract, not implementation details. Particularly load-bearing: composition grammars (which children the primitive expects + how it lays them out).

**Example**: `payment-method-list-ui` stamped three sibling `<div data-brand>/<div data-meta>/<div data-actions>` inside `<card-ui>` and re-implemented the 3-column grid via custom `@scope` rules. Bypassed card-ui's canonical `<header>` + `[slot=icon|heading|description|action]` grammar entirely. Result: visual debt downstream (50% icon-to-frame ratio, off-rhythm tag placement, fragile chrome).

**Detector**: the component-literacy step in [composite-demo-protocol.md](composite-demo-protocol.md) (Phase 2) has you read each used primitive's `.css` before locking the choice. `audit:card-structure` catches the specific card-ui bypass; analogous strict audits don't exist yet for avatar-ui / drawer-ui / aside-ui.

**Fix**: Read the primitive's `.css` end-to-end. Identify its expected child structure (slot grammar). USE it; never invent a parallel layer.

---

## 2. Parent CSS overriding child component's intrinsic display

**Pattern**: A parent composite hides/shows an embedded child via `display: none` ↔ `display: block` toggling. The `display: block` override beats the child's `:scope { display: flex }` from its own `@scope` (specificity 0,2,0 vs 0,1,0). Child's intrinsic layout silently collapses.

**Example**: 4 billing composites all had:

```css
/* WRONG, clobbers empty-state-ui's flex column */
:scope > [data-empty] { display: none; }
:scope[empty] > [data-empty] { display: block; }
```

`empty-state-ui` declares its own `:scope { display: flex; flex-direction: column; align-items: center }`. The parent's `display: block` removed that, making icon + heading + description flow inline: **`⊡ No payment methodsAdd a method to get started.`**

**Detector**: None today. Caught by user visual review.

**Fix**, invert visibility toggle so no display value is set when shown:

```css
/* RIGHT, child's :scope display remains intact */
:scope:not([empty]) > [data-empty] { display: none; }
```

---

## 3. Mixed sizes across form/control groups

**Pattern**: A composite stamps multiple form/control primitives in the same visual row (toolbar, button cluster, filter strip) without coordinating `size` attributes. Defaults differ, buttons might default `sm`, inputs default to a larger size, search-ui doesn't forward `size` to its inner input.

**Example**: an invoice-history toolbar had buttons at `size='sm'` (24px), filter chips at `size='sm'` (24px), search input at default (~36px). Same row, mismatched baseline.

**Detector**: None today. Caught by user visual review.

**Fix**:

- When stamping a control group, set the SAME `size` attribute on every control explicitly.
- Wrapper primitives (search-ui wraps input-ui; select-ui wraps native select) MUST forward `[size]` to their inner control. If a wrapper doesn't forward, file a fix in the wrapper rather than working around it in the consumer.

---

## 4. minmax(min, 1fr) inside repeat() fighting container queries

**Pattern**: A grid uses `repeat(N, minmax(<min>, 1fr))` with a hardcoded minimum, BUT the container also has `@container` queries that collapse columns at breakpoints. The minmax fights the breakpoints, when the container narrows, columns hit the floor and overflow before the breakpoint reduces column count.

**Example**: a dashboard-layout KPI grid was `repeat(4, minmax(16em, 1fr))` plus `@container ≤48em → 2 cols` and `≤32em → 1 col`. Redundant + conflicted. Removed the minmax; container queries own the responsive collapse cleanly.

**Detector**: None today.

**Fix**: When a grid has container-query breakpoints, use plain `repeat(N, 1fr)`. The breakpoints handle responsive behavior; minmax is for grids WITHOUT container queries.

---

## 5. Nested `<!-- ... -->` inside design-plan canonical-sketch fenced blocks

**Pattern**: The `<!-- design-plan: ... -->` block contains a fenced ` ```canonical-sketch ... ``` ` body. Authors sometimes paste HTML examples with inner `<!-- ... -->` comments into the sketch. HTML comments DON'T NEST, the inner `-->` closes the OUTER `<!-- design-plan: -->`. Trailing ` ``` --> ` then leaks as visible text on the page.

**Example**: a billing-overview.examples.html had two inner comments inside its canonical-sketch (annotations + a drawer composition example). Stray ` ``` --> ` rendered above the page header.

**Detector**: ✓ caught by `npm run audit:demo-pattern-source`, emits `phase_3_sketch contains an inner <!-- ... --> comment` finding.

**Fix**: Remove inner HTML comments from the canonical-sketch. Use plain text annotations or remove the doc-noise entirely.

---

## 6. Async load/render function completing out of order, a guard AT the checkpoint isn't enough

**Pattern**: A lifecycle method does asynchronous work (fetch, dynamic `import()`, any `await`) BEFORE reaching a sequence/resolver checkpoint that's guarded against stale calls. The guard only checks identity/sequence AT that one checkpoint, it doesn't protect the awaits that come after it. A call that started earlier but is slow can resume, pass every checkpoint it reaches (each one, in isolation, looks current), and finish writing state AFTER a faster, later call already completed, clobbering the newer result with stale content. The bug is invisible per-checkpoint because each individual guard check "passes"; the invariant that breaks is the ORDER completions land in, not any single check's correctness.

**Example**: `router-ui`'s `#loadContent` (`packages/web-components/core/provider.js`) fetched content, then ran it through the template resolver, which itself carried the only staleness guard (checked at resolver entry). A navigation to `/site/components/button` that fetched slowly could resume after a faster later navigation to `/site/dashboard` had already rendered, the resolver's own guard had nothing to check against by the time the stale call reached it, since the fetch (before the resolver) was itself unguarded. Reproduced deterministically with a 2-second-delayed fetch: the DOM showed the Dashboard's content under `data-route-path="button"`. Three separate flake classes across the site-a2ui migration all traced back to this one mechanism (`site-a2ui/FINDINGS.md`, "Router race fully root-caused: late-resuming #loadContent steals the sequence").

**Detector**: None generic, a per-page timing repro (delay the async step past the next call's completion, then assert final state matches the LAST call issued, not the first-guarded one) is how this was actually caught; no static check flags it.

**Fix**: Claim a monotonic sequence token as the FIRST line of the function, before any `await`, not at the first checkpoint reached. Re-check the token after EVERY subsequent `await` (not just once), and return early, without writing any state or emitting any event, the moment it no longer matches. A checkpoint guard placed only where you happen to already have a natural pause point (a resolver, a render call) is not equivalent to this; it only catches staleness FOR CALLS THAT REACH THAT SPECIFIC POINT before the pause, which a slow-but-eventually-arriving call always will.

**Illustrative** (simplified from the real fix, see `provider.js:230,245,250,286` for the actual code):

```javascript
async #loadContent(route) {
  const nav = ++this.#navSeq;              // claimed before any await
  const content = await fetchContent(route);
  if (nav !== this.#navSeq) return;        // re-checked after EVERY await
  const resolved = await this.#templateResolver.resolve(content);
  if (nav !== this.#navSeq) return;
  // ...write state / emit route-loaded only here
}
```

**Generalizes to**: any lifecycle method with more than one `await` where a caller can re-invoke it before the previous call finishes (route changes, search-as-you-type, tab switches, any "latest wins" async UI update), not just routing.

---

## 7. Minting a wrapper-shaped component before its registry.js entry lands, the transpiler silently deletes the node, not just mis-types it

**Pattern**: a tag is gated first and only, for `*-ui` tags, by `packages/gen-ui/a2ui/registry.js`'s hand-maintained `registry` map, inverted into `reverseRegistry` at `transpiler-maps.js`'s module init, consulted first thing in `compose/transpiler/transpiler.js:149-150`. `registry.js` is hand-edited, not generated by `node scripts/build/components.mjs` (that script writes sidecars/prop-catalog data, consumed only for prop-extraction fidelity on tags the transpiler ALREADY resolved, `transpiler-maps.js:22-26`); a runtime `registerType()` call doesn't rescue a stale row either, `reverseRegistry` is a one-time init snapshot, not live. Transpile a demo using a component minted in the SAME change, before its `registry.js` line lands (e.g. the chunk harvester, `node scripts/build/harvest-chunks.mjs`, or any other engine-transpiler consumer), and the tag falls through to `transpiler.js`'s "Unknown → Column" branch (line 180-183), same mechanism as gh#535's toolbar-group breakage, which at least rendered visibly-wrong. A NEW component is usually wrapper-shaped (one child, author-defined attributes like `anchor="bottom"` the transpiler doesn't map to any real A2UI prop). That shape trips a SECOND, separate rule right after, "single-child container chains flatten" (`transpiler.js:282-285`): a retyped Column with exactly one child and zero recognized props is discarded outright, and its child is spliced directly into the PARENT's children in its place. The wrapper's own id and node are never pushed to the tree at all, not visible-but-wrong, just gone. The row is then internally self-consistent (content hash matches source) so `check:chunks-fresh` reports clean.

**Example (historical, the illustrating consumer has since retired):** minting `anchor-bar-ui` (gh#495, PR #569) and regenerating the `bulk-action-toolbar` pattern's site-a2ui row (site-a2ui itself retired 2026-08-31, ADR-0072 Decision 2 / gh#2410, the underlying registry-gating hazard below is unchanged, only that particular consumer is gone) before the worktree's `registry.js` entry for it existed. Git-verified on the pre-fix commit (`ebf71832d`): the converted artifact contained zero occurrences of `pat-bulk-float-bar` (the anchor-bar-ui's own authored id) anywhere, not retyped-and-visible, genuinely absent, while its single child (the toolbar content) survived, reparented one level up. The site-a2ui freshness gate of the day reported clean regardless, for the exact reason the Detector below still explains.

**Detector**: none generic, a same-source freshness check can't catch this (the artifact IS fresh relative to its source, it transpiled correctly against a registry that was itself incomplete). The only catch is rendering the actual consuming surface and confirming the new tag's node count is nonzero, or re-running the transpile after `registry.js` is updated and diffing the output for the new component name. A non-wrapper-shaped new component (multiple children, or attributes that happen to map to real props) is lower-risk here, it survives as a visible-but-wrong Column, the gh#535 class, which at least has a visual tell.

**Fix**: the `registry.js` entry is what gates resolution, land it (not just run `components.mjs`, which is necessary for prop fidelity but not sufficient to avoid the retype) before transpiling anything that uses the new tag. When gating a dispatched agent's PR that did this out of order, re-run the transpile on the merged tree and confirm the tag actually appears in the output, never trust a freshness gate's green alone for a surface touching a component minted in the same change.

**Generalizes to**: any hand-maintained resolution map (not build-generated) that a later regeneration step reads through, regenerating before the map is updated produces an internally-consistent-but-wrong artifact that passes a same-source freshness check; if the misresolved shape also happens to trip a downstream simplification/collapse rule, the failure escalates from "renders wrong" to "renders nothing," with no visual tell at all.

---

## Meta-pattern across gotchas 1–5

**Composites and primitives have layered contracts. The parent's CSS shouldn't reach into the child's layout territory. The child's CSS shouldn't fight its parent's container queries. The audit should detect the rendering hazard, not just the parsing structure.**

The structural defense for #1 (composition-grammar bypass) is `npm run audit:card-structure[:strict]` / `npm run audit:avatar-structure` / `npm run audit:alert-structure` (HTML + JS `createElement` scan) plus `npm run audit:sketch-grammar` at Phase 3. The component-literacy read is a hint, not a gate: the mechanical defenses above are the proximate fix. See [composite-demo-protocol.md](composite-demo-protocol.md) Phase 2. Gotchas #2–4 are caught only by visual review until corresponding audits are added. #6 is a distinct axis (async-lifecycle correctness, not CSS layering), see its own Detector/Fix above. #7 is a third axis (a hand-maintained resolution map read by a downstream regeneration step, not CSS or async ordering), its own Detector/Fix above; no audit catches it, only a browser probe of the specific route touched.
