@adia-ai/adia-ui-forge 0.8.56 → 0.8.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.claude-plugin/plugin.json +3 -2
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +47 -0
  4. package/README.md +3 -3
  5. package/__init__.py +1 -1
  6. package/commands/demo-audit.md +1 -1
  7. package/commands/gen-ui-review.md +1 -1
  8. package/commands/package-release.md +1 -1
  9. package/commands/site-deployment.md +1 -1
  10. package/package.json +2 -2
  11. package/plugin.yaml +1 -1
  12. package/prompts/demo-audit.md +1 -1
  13. package/prompts/gen-ui-review.md +1 -1
  14. package/prompts/package-release.md +1 -1
  15. package/prompts/site-deployment.md +1 -1
  16. package/scripts/forge-lint.mjs +168 -0
  17. package/scripts/lint-rules.generated.mjs +22210 -0
  18. package/scripts/site-postwrite-derivation-gate +23 -127
  19. package/skills/a2ui-maintenance/references/pipeline-overview.md +13 -11
  20. package/skills/demo-audit/references/admin-shell-anatomy.md +9 -1
  21. package/skills/demo-audit/references/app-shell-pitfalls.md +1 -1
  22. package/skills/demo-audit/references/visual-probe-triage.md +4 -1
  23. package/skills/gen-ui-review/SKILL.md +4 -1
  24. package/skills/gen-ui-review/references/loop-protocol.md +6 -5
  25. package/skills/package-release/references/changelog-discipline.md +6 -3
  26. package/skills/package-release/references/cut-procedure.md +40 -13
  27. package/skills/package-release/references/gates-catalog.md +5 -2
  28. package/skills/package-release/references/recovery-paths.md +6 -3
  29. package/skills/package-release/scripts/gate-roster.mjs +18 -7
  30. package/skills/primitive-authoring/SKILL.md +5 -2
  31. package/skills/primitive-authoring/agents/openai.yaml +1 -1
  32. package/skills/primitive-authoring/references/anti-patterns.md +40 -0
  33. package/skills/primitive-authoring/references/common-gotchas.md +6 -6
  34. package/skills/primitive-authoring/references/css-patterns.md +12 -0
  35. package/skills/primitive-authoring/references/form-control-sizing.md +10 -0
  36. package/skills/primitive-authoring/references/shell-patterns.md +8 -1
  37. package/skills/primitive-authoring/references/token-contract.md +19 -1
  38. package/skills/primitive-authoring/references/yaml-contract.md +43 -14
  39. package/skills/site-docs-authoring/SKILL.md +7 -9
  40. package/hooks/hooks.json +0 -44
  41. package/scripts/forge-lint +0 -315
@@ -56,14 +56,17 @@ export const GATE_ROSTER = [
56
56
  // worktree.mjs && npm run dogfood:status` once under the pnpm layout too
57
57
  // before cutting (cut-procedure.md §3.1 states the per-gate layout need).
58
58
  { n: 17, cmd: 'npm run dogfood:status', what: 'P0/P1 dogfood floor (static-only under npm-ci; run once more under bootstrap layout for full coverage, gh#1359)' },
59
- // gh#421: these four live in the `npm run check` aggregate but were never
60
- // in the pre-cut roster, so a gate added to CI silently never reached a
61
- // cut. examples-md + site-a2ui are the two generators an `.examples.html`
62
- // edit invalidates (the v0.8.14 cut lost a CI round-trip to exactly that);
63
- // contrast is the AA gate that gh#427 widened from 42 to 90 pairs;
64
- // token-semantics-sync keeps the token-selection pack's generated half honest.
59
+ // gh#421: these live in the `npm run check` aggregate but were never in
60
+ // the pre-cut roster, so a gate added to CI silently never reached a cut.
61
+ // examples-md is the generator an `.examples.html` edit invalidates (the
62
+ // v0.8.14 cut lost a CI round-trip to exactly that); contrast is the AA
63
+ // gate that gh#427 widened from 42 to 90 pairs; token-semantics-sync keeps
64
+ // the token-selection pack's generated half honest.
65
+ // (Gate 19 was `verify:site-a2ui` — retired with the site-a2ui mechanism
66
+ // itself, ADR-0072 Decision 2 / gh#2410. Number left unassigned rather
67
+ // than renumbering 20+ — gate numbers are cited as identities in shipped
68
+ // release notes, same discipline gate 29's note states.)
65
69
  { n: 18, cmd: 'npm run check:examples-md-fresh', what: '.examples.md vs .examples.html' },
66
- { n: 19, cmd: 'npm run verify:site-a2ui', what: 'site-a2ui converted rows vs source fragments' },
67
70
  { n: 20, cmd: 'npm run verify:contrast', what: 'WCAG AA — canvas-text + text-on-fill pairs' },
68
71
  { n: 21, cmd: 'npm run check:token-semantics-sync', what: 'token-selection generated references vs token sources' },
69
72
  // Operator directive 2026-07-27: demo surfaces must be discoverable —
@@ -152,6 +155,14 @@ export const GATE_ROSTER = [
152
155
  // manifest trees. Appended, not inserted — same numbering discipline as
153
156
  // gate 30/31/32.
154
157
  { n: 33, cmd: 'npm run verify:patterns-index', what: 'pattern-index.md (mcp + adia-ui-factory) vs corpus source' },
158
+ // gh#2829: gh#2808/PR #2815 (ADR-0108 ticket 3) promoted both yaml gates
159
+ // below into `npm run check` but deliberately left this pre-cut roster
160
+ // untouched — a cut could still ship a yaml `events:` block that phantoms
161
+ // or misses a runtime dispatch, or a schema field with no implementation
162
+ // coverage, without either failing the pre-flight. Appended, not
163
+ // inserted — same numbering discipline as gate 30/31/32/33.
164
+ { n: 34, cmd: 'node scripts/release/check-yaml-events-vs-runtime.mjs --strict --strict-details', what: 'yaml events: blocks vs runtime dispatch (no phantom/missing events)' },
165
+ { n: 35, cmd: 'node scripts/release/check-yaml-impl-coverage.mjs --strict', what: 'yaml schema fields vs implementation coverage' },
155
166
  ];
156
167
 
157
168
  // -- CLI ------------------------------------------------------------------
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: primitive-authoring
3
3
  description: >-
4
- Author or modify AdiaUI framework source inside the monorepo — primitives
4
+ Author or modify AdiaUI framework source inside the monorepo — components
5
5
  (packages/web-components), shells/composites (packages/web-modules), yaml
6
- SoTs, demos. Use to add a new primitive, fix a prop/slot/attribute/CSS
6
+ SoTs, demos. Use to add a new component, fix a prop/slot/attribute/CSS
7
7
  variant, update a yaml, build or fix a shell (chat-shell, admin-shell,
8
8
  editor-shell — sidebar/pane/bespoke-tier composition), promote repeated
9
9
  inline content into a shared module, audit a component's four-axis
@@ -100,5 +100,8 @@ then the full sequence — never suppress.
100
100
  [authoring-cycle.md](references/authoring-cycle.md) Step 3 are fixed
101
101
  immediately; contract-neutral pattern drift is proposed in review, not
102
102
  blocked on.
103
+ - **SoT-change / component.md check**: new or changed states or aria
104
+ behavior on a yaml with a `component.md` sibling → component-md-authoring's
105
+ authored `screenReader`/`behavioral` sections may need a pass.
103
106
  - A one-line bug that touches no props, CSS contract, or lifecycle doesn't
104
107
  need this skill's overhead — just read the code and edit.
@@ -1,3 +1,3 @@
1
1
  interface:
2
2
  display_name: "Primitive Authoring"
3
- short_description: "Author or modify AdiaUI framework source inside the monorepo — primitives (packages/web-components), shells/composites (packages/web-modules), yaml SoTs, demos."
3
+ short_description: "Author or modify AdiaUI framework source inside the monorepo — components (packages/web-components), shells/composites (packages/web-modules), yaml SoTs, demos."
@@ -646,6 +646,46 @@ If you author a primitive that programmatically creates another (`document.creat
646
646
 
647
647
  Source: ADR-0027.
648
648
 
649
+ ### AP-R2 · Folding two visually-converging widgets into a shared module (ADR-0101)
650
+
651
+ Two widgets can converge visually — same tab-strip chrome, same header
652
+ band, same "looks like the same component" impression on the catalog page
653
+ — while carrying fundamentally different content models underneath. That
654
+ visual overlap alone is not sufficient reason to extract a shared module
655
+ or fold one into the other.
656
+
657
+ `inspector-ui` (a live devtools pane: four fixed panes, content pushed in
658
+ imperatively and repeatedly after connect via `update()`/`setHTML()`) and
659
+ `preview-ui` (static docs demo chrome: content captured once on connect,
660
+ shown two fixed ways) look like the same tabbed-view widget. They aren't:
661
+ folding one into the other would force the static-capture component to
662
+ grow a second, incompatible imperative-update mode it doesn't need for the
663
+ job it already does correctly everywhere it's used — added surface, not
664
+ removed surface. Extracting a shared tab-view module was evaluated and
665
+ rejected for the same reason: no common substrate exists beneath the
666
+ visual layer that both widgets could share without also merging their
667
+ different content models (`tabs-ui`'s synthesized strip vs. a hand-rolled
668
+ `role="tablist"` div built for single-panel content-swap semantics).
669
+
670
+ **The resolution is a mirror, not a merge:** keep both components on their
671
+ own contracts; restyle the trailing one to match the leading one's chrome
672
+ via scoped CSS only (`@scope (inspector-ui)` overrides copying
673
+ `preview-ui`'s header-tab anatomy, tokens, and typography) — no shared
674
+ component, no deprecation. The tradeoff this accepts: because the
675
+ mirroring is duplicated CSS, not a shared module, a future visual change
676
+ to the pattern's source component doesn't automatically propagate: check
677
+ the mirrored block for parallel drift when you next touch the pattern
678
+ that's being mirrored — the same "primitives that compose primitives
679
+ don't auto-import" discipline ADR-0027 already requires (AP-R1, above),
680
+ now applied to a duplicated CSS block instead of a duplicated import.
681
+
682
+ Before extracting a shared module for two things that "look the same,"
683
+ verify they'd actually share the extracted contract (props, update
684
+ semantics, lifecycle) — not just its rendered chrome. If they wouldn't,
685
+ mirror the chrome and keep both components separate.
686
+
687
+ Source: ADR-0101.
688
+
649
689
  ---
650
690
 
651
691
  ## Rapid-fire anti-patterns (mined from incident history)
@@ -16,7 +16,7 @@ Composite authors: read §§1–5 BEFORE Phase 3 sketch. Anyone adding an async
16
16
  4. [minmax(min, 1fr) inside repeat() fighting container queries](#4-minmaxmin-1fr-inside-repeat-fighting-container-queries)
17
17
  5. [Nested `<!-- ... -->` inside design-plan canonical-sketch fenced blocks](#5-nested----inside-design-plan-canonical-sketch-fenced-blocks)
18
18
  6. [Async load/render function completing out of order](#6-async-loadrender-function-completing-out-of-order--a-guard-at-the-checkpoint-isnt-enough)
19
- 7. [Minting a wrapper-shaped component before its registry.js entry lands](#7-minting-a-wrapper-shaped-component-before-its-registryjs-entry-lands--site-a2ui-silently-deletes-the-node-not-just-mis-types-it)
19
+ 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)
20
20
 
21
21
  ---
22
22
 
@@ -123,15 +123,15 @@ async #loadContent(route) {
123
123
 
124
124
  ---
125
125
 
126
- ## 7. Minting a wrapper-shaped component before its registry.js entry lands — site-a2ui silently deletes the node, not just mis-types it
126
+ ## 7. Minting a wrapper-shaped component before its registry.js entry lands — the transpiler silently deletes the node, not just mis-types it
127
127
 
128
- **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. Regenerate a site-a2ui row for a demo using a component minted in the SAME change, before its `registry.js` line lands, 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`/`verify:site-a2ui` both report clean.
128
+ **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.
129
129
 
130
- **Example**: minting `anchor-bar-ui` (gh#495, PR #569) and regenerating the `bulk-action-toolbar` pattern's site-a2ui row before the worktree's `registry.js` entry for it existed. Git-verified on the pre-fix commit (`ebf71832d`): `site-a2ui/pages/site__patterns__bulk-action-toolbar.a2ui.json` contains 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. `verify:site-a2ui` reported 321/321 clean.
130
+ **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.
131
131
 
132
- **Detector**: none generic — `verify:site-a2ui`'s freshness check can't catch this (the row IS fresh relative to its source, it transpiled correctly against a registry that was itself incomplete). The only catch is rendering the actual route in a browser and confirming the new tag's node count is nonzero, or re-running `node scripts/build/site-a2ui.mjs --page <route>` (or `--stale`) after `registry.js` is updated and diffing the row 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.
132
+ **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.
133
133
 
134
- **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 regenerating any site-a2ui row that uses the new tag. When gating a dispatched agent's PR that did this out of order, re-run `node scripts/build/site-a2ui.mjs --page <route>` on the merged tree and browser-probe the tag actually appears — never trust `verify:site-a2ui`'s green alone for a route touching a component minted in the same change.
134
+ **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.
135
135
 
136
136
  **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.
137
137
 
@@ -62,6 +62,18 @@ Every component CSS file has exactly this shape:
62
62
 
63
63
  When a parent tries to override `--component-bg`, specificity beats them. The zero-specificity layer is the contract.
64
64
 
65
+ ## Region elements never self-style (2026-09-01, ADR-0105)
66
+
67
+ Native `<header>`/`<section>`/`<footer>` and `<header-ui>`/`<section-ui>`/`<footer-ui>` (a CSS-only slot-routing anatomy stub — no JS, no events, no `.css` file of its own) are **region elements**, not components in their own right. They carry zero layout CSS. Every grid, gap, sticky rule, and slot-vocabulary variant a region renders is written in the **closest host pattern's** `@scope (host-name)` block — `page-ui`, `card-ui`, `drawer-ui`, `modal-ui`, `anchor-bar`, the shells — matching the region by tag + attribute selector (`:scope > :where(header, header-ui)`, `:has(> [slot="action"])`, etc.), never in a region-owned stylesheet.
68
+
69
+ **If you're authoring a new host pattern that accepts region children, you MUST ship its `@scope` treatment for every region shape it accepts before it ships.** A host that accepts `<header-ui>` without a matching grid/sticky/slot-vocabulary block is not a lesser-but-valid host — the region element structurally cannot supply the missing layout itself, so the host renders wrong (usually unstyled block flow, or — the observed failure mode — a stale layout that packs content into the wrong shape). gh#2760 is the live example: `page-ui`'s header-band `@scope` block didn't reproduce the retired `admin-page-header`'s action-cluster grid for a multi-item `[slot="action"]` containing a block-level `<tabs-ui>`, so tabs overlapped buttons instead of getting their own row — same markup, incomplete host coverage.
70
+
71
+ **Bare `<header>` vs `<header-ui>`** — both reach a host's tag-matched rules identically (`:where(header, header-ui, …)` gives them equal zero specificity). They diverge only on rules keyed off a named-slot attribute selector (`:has(> [slot="icon"])`, etc.) — and there the divergence isn't mechanical (`slot=` is decorative everywhere, ADR-0033), it's documentation: `header-ui`'s own yaml is the only place the icon/heading/description/action vocabulary is written down and steered toward by `a2ui.rules`. Use bare `<header>`/`<section>`/`<footer>` only for unstructured default-slot content; use the `-ui` stub whenever content decomposes into the slot vocabulary — that's the only path an author or the A2UI generator discovers it exists.
72
+
73
+ A region element acquiring its own `.css` file (layout rules scoped to `header-ui` itself, not the host) is the anti-pattern this section exists to name — see ADR-0105 for the full rationale and the corollary above.
74
+
75
+ Source: [ADR-0105](../../../../../../docs/ops/adr/adr-0105-region-elements-never-self-style.md).
76
+
65
77
  ## Variants vs modes — the decision tree
66
78
 
67
79
  The rule: **variants change tokens; modes change layout.**
@@ -31,6 +31,16 @@ Decision Class 1 removes the 20ch-class field-width floor for
31
31
  floor, generalizing the `button-ui` keep-square pattern already named below
32
32
  to the field-width components.
33
33
 
34
+ **[amended 2026-08-31, gh#2534, ADR-0095]** The square-minimum rule governs
35
+ a primitive's own OUTER inline-size floor against its own block-size only —
36
+ it does not reach a component's INTERNAL grid-column sizing. A token like
37
+ `field-ui[inline]`'s `--field-label-inline-min: 10ch` floors the label
38
+ *column* inside the component's internal grid, to keep sibling rows
39
+ aligned — not to prevent the host itself from collapsing — so it is not
40
+ "a `--*-min-width: 20ch`-class floor" in ADR-0095's sense and is not
41
+ retired by the Decision above. Don't apply the square-minimum rule to an
42
+ internal grid-column token by analogy to the field-width class.
43
+
34
44
  *(Historical, pre-ADR-0095 rule — superseded by the retirement above, kept
35
45
  only for context on what the old convention was.)* A
36
46
  `--*-min-width: 20ch`-class floor (the gh#781/gh#1633 pattern on
@@ -448,6 +448,13 @@ Consumers (CodeMirror layout, canvas redraw, dependent UI) listen on the shell o
448
448
 
449
449
  ### admin cluster (canonical reference)
450
450
 
451
+ **[deprecated 2026-09-01, ADR-0098]** `admin-page`/`admin-page-header`/
452
+ `admin-page-body` are retired deprecate-then-delete; `admin-scroll` is
453
+ renamed wholesale to `page-scroll`. `page-ui[band]` + `page-scroll` are
454
+ now the canonical page-chrome pair — this cluster still describes
455
+ `admin-shell`'s pre-migration internal composition, valid during the
456
+ deprecation window.
457
+
451
458
  - **3 JS-bearing children** — `<admin-shell>` (host coordinator), `<admin-sidebar>` (resize+collapse+persist), `<admin-command>` (Cmd+K palette)
452
459
  - **7 CSS-only structural children** — `<admin-content>`, `<admin-topbar>`, `<admin-statusbar>`, `<admin-scroll>`, `<admin-page>`, `<admin-page-header>`, `<admin-page-body>`
453
460
  - **CSS bridge** — `packages/web-modules/shell/admin-shell/css/admin-shell.bespoke.css` (~240 LOC)
@@ -547,7 +554,7 @@ When you encounter a legacy reference in old documentation or commit history, th
547
554
 
548
555
  - [authoring-cycle.md](authoring-cycle.md) — general 5-step authoring procedure; shell-specific rules layer on top
549
556
  - [api-contract.md](api-contract.md) — prop naming, reflection policy
550
- - [css-patterns.md](css-patterns.md) — two-block `@scope`, variants vs modes
557
+ - [css-patterns.md](css-patterns.md) — two-block `@scope`, variants vs modes; §Region elements never self-style (ADR-0105) — a shell that hosts `<header-ui>`/`<section-ui>`/`<footer-ui>` region children (e.g. a bespoke `<X-header>` wrapping one) owns their `@scope` layout the same way `page-ui`/`card-ui`/`drawer-ui`/`modal-ui` do
551
558
  - [lifecycle-patterns.md](lifecycle-patterns.md) — teardown patterns; the cleanup-closure pile and document-listener rules above layer on top
552
559
  - [token-contract.md](token-contract.md) — zero-raw-color + L3 alias rules apply identically to shells
553
560
  - [module-promotion.md](module-promotion.md) — the _different_ activity of lifting a cross-cluster reusable element (theme panel, command palette) into a shared module
@@ -36,6 +36,23 @@ Every component's `.css` file must follow the **two-block @scope** pattern:
36
36
  }
37
37
  ```
38
38
 
39
+ ### `@scope` donor selectors must be tag-independent when a component can swap its own rendered tag (ADR-0102, 2026-09-01)
40
+
41
+ A `@scope (my-component-ui)` donor selector matches by **literal tag
42
+ name**. That's a live gap for any component whose own contract lets it
43
+ render as a different tag than its own custom-element name — `text-ui`
44
+ already did this coincidentally (`variant="body"` → `<p>`,
45
+ `variant="caption"` → `<small>`), relying on ambient/bare-tag styling
46
+ rather than the component's own `[variant]` rules. ADR-0102 (`text-ui`'s
47
+ `level` prop, promoting the element to a real `<h1>`-`<h6>`) made this a
48
+ first-class case, requiring the donor selector to stay tag-independent —
49
+ e.g. `@scope (text-ui, [data-text])` with the renderer stamping a stable
50
+ marker attribute on every such element regardless of the resolved tag —
51
+ so a promoted `<h2 level="2" variant="heading">` keeps its component
52
+ styling instead of silently falling back to bare-tag `:where(hN)`
53
+ defaults elsewhere in the cascade. Any component that can render under a
54
+ tag other than its own name needs the same tag-independent scope check.
55
+
39
56
  ## Hard rules
40
57
 
41
58
  1. **Zero raw color values** anywhere in component CSS
@@ -139,7 +156,8 @@ mechanics; never work from this summary alone):
139
156
  in a component yaml, check it against the spec's attribute inventory; the
140
157
  only sanctioned collisions live in the spec's §11 Global-Attribute Exemption
141
158
  List (first entries: `swatch-ui[color]` / `noodles-ui[color]`, ADR-0054;
142
- granted since: `qr-code-ui[color]` / `icon-ui[weight]`, ADR-0070).
159
+ granted since: `qr-code-ui[color]` / `icon-ui[weight]`, ADR-0070;
160
+ `badge-ui[weight]` → `icon-ui[weight]`, ADR-0100).
143
161
 
144
162
  Beyond the global grammar, ADR-0063
145
163
  (`docs/ops/adr/adr-0063-attribute-grammar-addendum.md`) ratifies seven
@@ -123,6 +123,15 @@ per gh#2116) across the five opt-out-scoped catalogs (`adia.core.json`,
123
123
  validator enforces them (`UNALLOWED_PARENT`/`UNALLOWED_CHILD`). Module-tier
124
124
  yamls (web-modules) now carry a v1.0 sidecar too and land in `adia.shells`.
125
125
 
126
+ Once a component earns a `component.md` (both `screenReader:` and
127
+ `behavioral:` authored, gh#2615), a non-empty `allowedParents`/
128
+ `allowedChildren` also generates an enforced lint rule (LLD-0016 §C4,
129
+ gh#2647): `scripts/build/gen-composition-rules.mjs` emits `scripts/lint/
130
+ rules/generated/composition/<name>.mjs`, which flags a markup file where
131
+ the tag nests under (or contains) a non-declared tag. Advisory (`warn`)
132
+ until a corpus-wide rollout promotes it (`npm run build:composition-rules`
133
+ / `check:composition-rules-fresh`).
134
+
126
135
  ---
127
136
 
128
137
  ## `examples:` field — a2ui example ids (semantic-id grammar, gh#2492)
@@ -190,14 +199,18 @@ reported repro):
190
199
  { "id": "kpi-revenue", "component": "Card", "children": ["kpi-revenue-label", "kpi-revenue-value"] }
191
200
  ```
192
201
 
193
- **Enforcement**: `scripts/verify/check-example-ids.mjs` walks every yaml's
194
- `examples[].a2ui` nodes and reports ids that fail the grammar (a bare
195
- 1-2 char id, a `^[a-z]\d+` positional pattern like `k1`/`c2`, non-kebab-case,
196
- or a within-example duplicate). gh#2492 Phase 2 swept the corpus-wide 526
197
- violations across 48 files to zero and flipped the gate to `npm run
198
- check:example-ids:strict` in the `npm run check` chain (non-zero exit on
199
- any violation) a new example that violates the grammar now fails the
200
- build immediately, not just on a future sweep. Known gap: the checker's
202
+ **Enforcement**: the `EXAMPLE-ID-GRAMMAR` rule
203
+ (`scripts/lint/rules/shared/example-id-grammar.mjs`, gh#2649 moved out of
204
+ the retired `scripts/verify/check-example-ids.mjs` into the shared lint
205
+ rule bank per LLD-0016 §C3) walks every yaml's `examples[].a2ui` nodes and
206
+ reports ids that fail the grammar (a bare 1-2 char id, a `^[a-z]\d+`
207
+ positional pattern like `k1`/`c2`, non-kebab-case, or a within-example
208
+ duplicate). gh#2492 Phase 2 swept the corpus-wide 526 violations across 48
209
+ files to zero; `npm run check:example-id-grammar` keeps that promotion
210
+ build-blocking in the `npm run check` chain (non-zero exit on any
211
+ violation) even though the rule's own bank-default severity is
212
+ `advisory` — a new example that violates the grammar now fails the build
213
+ immediately, not just on a future sweep. Known gap: the checker's
201
214
  positional-pattern regex catches `letter+digits` (`k1`, `c2`) but not a
202
215
  `letter-digit-letter-digit` chain like `g1i1` — out of scope for this
203
216
  sweep (nothing flagged it), left for a future refinement.
@@ -388,6 +401,22 @@ already has). As of this writing `deriveProps()` still synthesizes only the
388
401
  three props above and `catalog-validator.js` has no trait-name check —
389
402
  gh#2513 tracks the build.
390
403
 
404
+ ### Sentinel-0 defaults and tag-promoting props (ADR-0102, 2026-09-01)
405
+
406
+ Not every escape hatch belongs on `CatalogComponentCommon` — a prop scoped
407
+ to one component's own semantics stays on that component's own yaml, even
408
+ when it changes the rendered tag. `text-ui`'s `level` (integer 0-6,
409
+ default `0`) is the worked example: `0` is the sentinel for "no
410
+ promotion, current behavior" — the same `0 = off` convention `text-ui`'s
411
+ own `lines` prop already used (`lines: 0` = no clamp) — and `1`-`6`
412
+ promotes the rendered element to a real native `<h1>`-`<h6>`, independent
413
+ of `variant` (which stays presentational-only: typography tokens, never a
414
+ tag). `deriveProps()` needs no change for this — a per-sidecar prop like
415
+ `level` already flows into the generated catalog schema generically; only
416
+ a `CatalogComponentCommon` universal (`slot`/`hidden`/`ariaLive`/`traits`,
417
+ above) needs a wire-schema edit. See ADR-0102 for the full contract
418
+ (renderer branch, dead-code removal, non-goals).
419
+
391
420
  ### `required: true` field
392
421
 
393
422
  **When to use**: only for props where omitting them makes the component meaningless or inaccessible.
@@ -502,9 +531,9 @@ section IS that call. The family stays `data-*`, undeclared in any yaml,
502
531
  with its contract documented at the shared source instead: `core/form.js`'s
503
532
  own header JSDoc (the mixin all consumers share) and the canonical
504
533
  `form-system` pattern doc (`packages/web-components/patterns/form-system/
505
- form-system.examples.html`, mirrored at `site-a2ui/pages/
506
- site__patterns__form-system.a2ui.json`) — both already enumerate the full
507
- family with a worked example. A future architectural pass that wants to
534
+ form-system.examples.html`, rendered directly at `/site/patterns/form-system`)
535
+ — both already enumerate the full family with a worked example. A future
536
+ architectural pass that wants to
508
537
  promote this to a declared per-component contract needs its own ADR (the
509
538
  scope is a mixin-wide contract change, not a small-ticket edit); nothing
510
539
  here forecloses that, it only records today's call.
@@ -538,9 +567,9 @@ npm run check:genui-catalog # the drift gate that fails PR CI
538
567
 
539
568
  Any yaml change that alters a sidecar's prop schemas STALES the genui
540
569
  catalog. The main regen chain (`harvest:chunks` → `build:embeddings:chunks`
541
- → `site-a2ui --stale` → `build:patterns-index`) does NOT cover it — it's a
542
- separate derivation with its own gate, and skipping it passes every local
543
- check that isn't `check:genui-catalog` itself.
570
+ → `build:patterns-index`) does NOT cover it — it's a separate derivation
571
+ with its own gate, and skipping it passes every local check that isn't
572
+ `check:genui-catalog` itself.
544
573
 
545
574
  ---
546
575
 
@@ -47,21 +47,19 @@ skill routes to it and enforces it, it does not restate it.
47
47
  ## Verify after any change
48
48
 
49
49
  - `npm run check:links` — intra-repo links across the touched pages.
50
- - `node scripts/build/site-a2ui.mjs --page <route>` for every page whose
51
- **markup** changed regenerates the compiled A2UI artifact the
52
- docs-transpiler produces from this source; a CSS-only fix needs no regen.
50
+ (Before ADR-0072 Decision 2 / gh#2410 retired site-a2ui, a markup change
51
+ also needed `node scripts/build/site-a2ui.mjs --page <route>` to
52
+ regenerate a compiled A2UI artifact. The docs site now renders each
53
+ route's fragment directly — no compile step, no artifact to regenerate.)
53
54
  - `npm run verify:contrast` for any new or changed token used for text or
54
55
  link contrast (that gate covers text/link AA pairs only — a non-text 3:1
55
56
  claim needs a hand-proved, cited ratio; no mechanical runner exists).
56
57
  - `npm run check:lightningcss-build` after any CSS change (`site/site.css`
57
58
  or a component's own `.css`).
58
59
  - **Visually verify any `alert-ui`/rich-slotted-content change in a
59
- browser after regenerating** — a `valid:true` transpiler result and a
60
- correct raw-HTML source do not prove the rendered order is correct;
61
- the compiled `site-a2ui/pages/*.json` artifact is what's served, and
62
- a wrong container tag (`<span>` instead of `<div>`) silently reorders
63
- content while still passing schema validation. This is not optional
64
- for this one content shape.
60
+ browser** — a wrong container tag (`<span>` instead of `<div>`) still
61
+ silently reorders mixed inline content on render even without a compile
62
+ step; this is not optional for this one content shape.
65
63
 
66
64
  A check that cannot run (missing script, no network for a build step) is a
67
65
  named blocker in the report — flag it and stop; never mark the page done on
package/hooks/hooks.json DELETED
@@ -1,44 +0,0 @@
1
- {
2
- "hooks": {
3
- "PreToolUse": [
4
- {
5
- "matcher": "Write|Edit",
6
- "hooks": [
7
- {
8
- "type": "command",
9
- "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/sidecar-prewrite-guard\" --hook"
10
- }
11
- ]
12
- },
13
- {
14
- "matcher": "Bash",
15
- "hooks": [
16
- {
17
- "type": "command",
18
- "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/release-pretag-docs-gate\" --hook",
19
- "timeout": 90
20
- }
21
- ]
22
- }
23
- ],
24
- "PostToolUse": [
25
- {
26
- "matcher": "Write|Edit",
27
- "hooks": [
28
- {
29
- "type": "command",
30
- "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/forge-lint\" --hook"
31
- },
32
- {
33
- "type": "command",
34
- "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/demo-postwrite-pattern-gate\" --hook"
35
- },
36
- {
37
- "type": "command",
38
- "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/site-postwrite-derivation-gate\" --hook"
39
- }
40
- ]
41
- }
42
- ]
43
- }
44
- }