@adia-ai/a2ui-compose 0.0.1

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,86 @@
1
+ # Changelog — @adia-ai/a2ui-compose
2
+
3
+ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
4
+
5
+ Scope: generation engines (monolithic + zettel), LLM adapters,
6
+ transpiler, evals. Retrieval + validation now ship as sibling
7
+ packages (`@adia-ai/a2ui-retrieval`, `@adia-ai/a2ui-validator`) so
8
+ non-compose consumers can depend on them without pulling the
9
+ generator graph.
10
+
11
+ ## [Unreleased]
12
+
13
+ _Nothing yet._
14
+
15
+ ---
16
+
17
+ ## [0.0.1] - 2026-04-24
18
+
19
+ First public release. Framework-agnostic compose engine for the
20
+ A2UI protocol: takes natural-language intents + a catalog and
21
+ produces A2UI protocol messages. Two generation strategies ship
22
+ behind a plug-in engine registry.
23
+
24
+ ### Included
25
+
26
+ - **Monolithic engine** (`engine/generator.js`) — single-call
27
+ generator with `instant` / `pro` / `thinking` modes. Anthropic
28
+ and OpenAI LLM adapters included. Pattern-match-only `instant`
29
+ mode for deterministic output; `pro` + `thinking` call the LLM.
30
+ - **Zettel engine** (`engines/zettel/`) — fragment-graph composition
31
+ strategy that assembles UI from reusable fragments + compositions
32
+ via a backlink graph. Framework-agnostic; driven by the same
33
+ intent-gate + domain-router as monolithic.
34
+ - **Engine registry** (`engines/registry.js`) — plug-in API.
35
+ `registerEngine(name, fn)` + `pick(name)` dispatcher;
36
+ reserved names for built-ins.
37
+ - **LLM bridge** (`llm/llm-bridge.js`) — unified interface over
38
+ Anthropic, OpenAI, and Gemini adapters. Streaming + non-streaming
39
+ calls; prompt-cache support for Anthropic.
40
+ - **Transpiler** (`transpiler/transpiler.js`) — A2UI JSON → HTML
41
+ rendering; used by MCP tools and the visual-validate pipeline.
42
+ - **Evals harness** (`evals/harness.mjs`) — V2 cross-engine eval
43
+ runner. Consumed by `eval-diff.mjs` in `@adia-ai/a2ui-mcp`.
44
+ - **Index barrel** (`index.js`) — re-exports `generateUI`,
45
+ `generateUIStream`, `pick`, `listEngines`, `registerEngine`,
46
+ `unregisterEngine`, `ENGINES` for one-import consumer surface.
47
+
48
+ ### Dependencies
49
+
50
+ - `@adia-ai/a2ui-utils` ^0.0.2 — A2UI runtime primitives.
51
+ - `@adia-ai/a2ui-retrieval` ^0.0.1 — retrieval layer.
52
+ - `@adia-ai/a2ui-validator` ^0.0.1 — validation layer.
53
+
54
+ ---
55
+
56
+ ## Pre-0.0.1 history
57
+
58
+ ### Changed
59
+ - **A2UI runtime dependency relocated.** `retrieval/catalog.js`,
60
+ `transpiler/transpiler-maps.js`, and `validation/validator.js` now
61
+ import `registry` and `wiringRegistry` from the new published package
62
+ `@adia-ai/a2ui-utils` instead of `../../web-components/a2ui/`
63
+ (which no longer exists — the runtime moved out of web-components in
64
+ `@adia-ai/web-components@0.0.4`). The scoped import is resolved via
65
+ the workspace symlink in dev and the transitive dependency in
66
+ published installs.
67
+ - `retrieval/anti-patterns.js` — `noInventedComponents` regex broadened from `/<([a-z]+-n)\b/` to `/<([a-z]+-(?:n|ui))\b/` so the check actually fires against the current `-ui` codebase. Allowlist aligned with canonical registry.
68
+ - `transpiler/transpiler.js` and `transpiler/transpiler-maps.js` — stale `-n` tag references updated to `-ui` forms consistent with `packages/web-components/a2ui/registry.js`.
69
+ - `retrieval/catalog.js`, `retrieval/synthetic-data.js`, `engines/monolithic/_shared.js` — internal tag references audited and aligned with the canonical registry.
70
+
71
+ ### Fixed
72
+ - Stale internal-identifier references in comments + doc strings swept to the current naming.
73
+
74
+ ---
75
+
76
+ ## [0.1.0] — internal baseline (unreleased)
77
+
78
+ Initial version at the time the monorepo was established. Contains:
79
+
80
+ - **Monolithic engine** — single-call generator with instant / pro / thinking modes (Anthropic, OpenAI adapters).
81
+ - **Zettel engine** — fragment-graph composition engine using leverage-ranked fragments from the training corpus.
82
+ - **Retrieval layer** — keyword + concept-tag + component-signature scoring. Dialog recorder writes per-turn JSON to `logs/dialogs/`.
83
+ - **Validator** — 15-check weighted schema validator; rejects `_fallback: true` surfaces as score 0.
84
+ - **Prompt caching** — enabled on Anthropic calls as of 2026-04-19.
85
+
86
+ Package name still uses the legacy `@adia-ai/a2ui-compose` scope pending rename (see root CHANGELOG for context).
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # @adia-ai/a2ui-compose
2
+
3
+ Framework-agnostic UI generation engine. Takes a natural-language intent +
4
+ an A2UI component catalog and produces a tree of A2UI protocol messages
5
+ ready for a renderer.
6
+
7
+ > This package is pipeline runtime only. UI components live in
8
+ > [`@adia-ai/web-components`](../web-components); the A2UI runtime (renderer,
9
+ > registry, streams, wiring) in [`@adia-ai/a2ui-utils`](../a2ui/utils);
10
+ > the pattern corpus in [`@adia-ai/a2ui-corpus`](../corpus);
11
+ > the MCP server in [`@adia-ai/a2ui-mcp`](../mcp).
12
+ >
13
+ > Note: this package still lives under the legacy `@adiahealth` scope in its
14
+ > own `package.json` name. The `@adia-ai` scope is the public npm scope
15
+ > (only `@adia-ai/web-components` and `@adia-ai/a2ui-utils` are currently
16
+ > published).
17
+
18
+ ## What it does
19
+
20
+ ```
21
+ intent ─▶ classify ─▶ retrieve ─▶ compose / adapt ─▶ validate ─▶ A2UI
22
+ (concepts) (patterns) (engine-specific) (score ≥70) JSON
23
+ ```
24
+
25
+ One entry point, two generation strategies, pluggable LLM back-end.
26
+
27
+ ```javascript
28
+ import { generateUI } from '@adia-ai/a2ui-compose/engine';
29
+
30
+ const result = await generateUI({
31
+ intent: 'login form with email, password, and remember-me',
32
+ engine: 'zettel', // 'monolithic' | 'zettel'
33
+ mode: 'pro', // monolithic only: instant | pro | thinking
34
+ model: 'claude-sonnet-4-7',
35
+ });
36
+
37
+ // result.components — A2UI message array
38
+ // result.validation — { score, checks, warnings }
39
+ // result.debug — pattern matches, LLM prompt, token usage, …
40
+ ```
41
+
42
+ ## Layout
43
+
44
+ ```
45
+ gen-ui/
46
+ ├── engine/
47
+ │ └── generator.js generateUI() — the one public entry point
48
+
49
+ ├── engines/ pluggable strategies via registerEngine()
50
+ │ ├── registry.js engine selector + reserved-name guard
51
+ │ ├── monolithic/ pattern-match + LLM-adapt (3 modes)
52
+ │ │ ├── generate-instant.js no LLM — pattern-match only
53
+ │ │ ├── generate-pro.js pattern + LLM adaptation, non-streaming
54
+ │ │ └── generate-thinking.js streaming LLM + repair loop
55
+ │ └── zettel/ fragment-graph composition
56
+ │ ├── generator-adapter.js entry point
57
+ │ ├── composer.js assembles fragments → compositions
58
+ │ └── session.js multi-turn state
59
+
60
+ ├── retrieval/
61
+ │ ├── catalog.js loads component schemas from sibling .a2ui.json
62
+ │ ├── pattern-library.js keyword-ranked pattern search (corpus + embeddings)
63
+ │ ├── fragments.js atomic-shape lookup for zettel
64
+ │ ├── anti-patterns.js catalog of canonical anti-patterns
65
+ │ └── feedback-store.js accumulates user feedback → disk
66
+
67
+ ├── llm/
68
+ │ ├── llm-bridge.js unified adapter (Anthropic / OpenAI / Gemini)
69
+ │ ├── env.js Vite + Node env-var routing
70
+ │ └── prompts/ system prompts per engine mode
71
+
72
+ ├── validation/
73
+ │ └── validator.js 15-check A2UI validator, weighted 0–100 score
74
+
75
+ ├── intelligence/ intent classification + concept extraction
76
+ │ ├── classifier.js
77
+ │ ├── concepts.js
78
+ │ └── steelman.js
79
+
80
+ └── evals/
81
+ └── harness.mjs held-out intent benchmark runner
82
+ ```
83
+
84
+ ## Engines
85
+
86
+ **Monolithic** — pattern-match against full-canvas templates, optionally
87
+ adapt via LLM. Three modes:
88
+
89
+ | Mode | LLM? | Speed | Use for |
90
+ |------------|------|--------|-------------------------------------------------|
91
+ | `instant` | no | <50ms | High-confidence intents with exact pattern hit |
92
+ | `pro` | yes | ~2s | Most requests — adapt a template to the intent |
93
+ | `thinking` | yes | ~5s | Complex requests; streams + runs a repair loop |
94
+
95
+ **Zettel** — fragment-graph composition. Retrieves atomic fragments
96
+ (form-field, card-header, action-row, …) by keyword + concept-tag overlap
97
+ and assembles them into compositions. Verbatim retrieval above a threshold
98
+ (score ≥ 40); LLM synthesis from fragments below. Preserves session state
99
+ across multi-turn iterations.
100
+
101
+ ```javascript
102
+ import { registerEngine } from '@adia-ai/a2ui-compose/engines/registry';
103
+
104
+ registerEngine('my-engine', async (ctx) => {
105
+ // ctx: intent, catalog, patterns, concepts, session, llm, …
106
+ return { components: [...], validation: {...}, debug: {...} };
107
+ });
108
+ ```
109
+
110
+ Reserved names: `monolithic`, `monolithic-*`, `zettel`, `mcp`.
111
+
112
+ ## Validation
113
+
114
+ Every generated output runs through `validation/validator.js` — 15 weighted
115
+ checks covering structural validity, card/grid conventions, intent
116
+ alignment (F1), and anti-patterns. Result:
117
+
118
+ ```javascript
119
+ {
120
+ score: 92, // 0-100
121
+ passed: true, // score ≥ 70
122
+ checks: [
123
+ { id: 'structure', ok: true, weight: 10 },
124
+ { id: 'intent-f1', ok: true, weight: 8, value: 0.84 },
125
+ { id: 'card-grid', ok: true, weight: 6 },
126
+ { id: 'anti-pattern', ok: false, weight: 4, hit: 'chart-legend' },
127
+
128
+ ]
129
+ }
130
+ ```
131
+
132
+ `_fallback` surfaces score 0 by design — ensure the engine returns real
133
+ output, not a safety net.
134
+
135
+ ## LLM bridge
136
+
137
+ Multi-provider adapter with a common interface:
138
+
139
+ ```javascript
140
+ import { getAdapter } from '@adia-ai/a2ui-compose/llm';
141
+
142
+ const adapter = getAdapter('anthropic'); // or 'openai', 'gemini'
143
+ const stream = await adapter.streamChat({ model, messages, tools });
144
+ ```
145
+
146
+ Env-var routing via `llm/env.js` — works under Node (`process.env`) and
147
+ Vite (`import.meta.env`). Browser calls proxy through `server.js` at the
148
+ repo root (holds API keys); Node calls go direct.
149
+
150
+ ## Evals
151
+
152
+ ```bash
153
+ npm run evals # held-out intent benchmark
154
+ npm run eval:diff -- --engine zettel # diff against baseline
155
+ ```
156
+
157
+ The held-out fixture lives in `gen-ui-training/evals/held-out.jsonl`.
158
+ Regression thresholds the pipeline must hold:
159
+
160
+ - Zettel coverage **100%**, avgScore **≥ 88**, MRR **≥ 0.94**
161
+ - Monolithic coverage **100%**, avgScore **≥ 95**
162
+ - Fragment reuse ratio **≥ 29.9%** (167 refs / 559 nodes)
163
+
164
+ Full gate sweep: see `AGENTS.md` at repo root, or run `/verification-sweep`.
165
+
166
+ ## Gotchas
167
+
168
+ - **Component catalog is read-only.** `.a2ui.json` sidecars in
169
+ `web-components/components/*/` are build outputs; edit the sibling YAML
170
+ instead.
171
+ - **Zettel loading is lazy.** The corpus is only parsed on first zettel
172
+ call — avoids Node `fs`/`path` imports reaching the browser bundle.
173
+ - **Validator score ≥ 70 is required** for downstream consumers to trust
174
+ the output. Below that, callers should treat the tree as advisory.
175
+ - **Engine name reservations** are enforced at registration time —
176
+ `registerEngine('zettel-v2', …)` passes; `registerEngine('zettel', …)`
177
+ throws.
178
+
179
+ ## License
180
+
181
+ MIT
@@ -0,0 +1,262 @@
1
+ /**
2
+ * ArtifactStore — In-memory artifact storage for pipeline executions.
3
+ *
4
+ * Stores A2UI message sequences keyed by executionId, with multi-turn
5
+ * history (iterations). Each turn records the messages, optional HTML
6
+ * render, a human-readable summary, and the intent that produced it.
7
+ *
8
+ * Drift detection: measures structural divergence between turns to flag
9
+ * scope creep in iterative generation sessions.
10
+ */
11
+
12
+ export class ArtifactStore {
13
+ #artifacts = new Map(); // executionId → { turns: [], metadata }
14
+
15
+ /**
16
+ * Record a turn (iteration) for an execution.
17
+ *
18
+ * @param {string} executionId
19
+ * @param {object} opts
20
+ * @param {object[]} opts.messages — A2UI message sequence
21
+ * @param {string} [opts.html] — Optional rendered HTML snapshot
22
+ * @param {string} [opts.summary] — Human-readable description
23
+ * @param {object} [opts.validation] — Validation result from validateSchema
24
+ * @param {string} [opts.intent] — The user intent for this turn
25
+ */
26
+ record(executionId, { messages, html = null, summary = '', validation = null, intent = '', analysis = null }) {
27
+ let artifact = this.#artifacts.get(executionId);
28
+ if (!artifact) {
29
+ artifact = {
30
+ executionId,
31
+ turns: [],
32
+ metadata: {
33
+ created: Date.now(),
34
+ updated: Date.now(),
35
+ originalIntent: intent || summary || '',
36
+ // Stash the first turn's analyzer output so iteration turns can
37
+ // recover the original brief's concepts/entities/components for
38
+ // canvas-aware downstream decisions (e.g. suggestion generation,
39
+ // drift detection). Iteration turns skip the analyzer, so this is
40
+ // the only durable handle on what the user originally asked for.
41
+ originalAnalysis: analysis || null,
42
+ },
43
+ };
44
+ this.#artifacts.set(executionId, artifact);
45
+ }
46
+
47
+ // Extract structural snapshot for drift tracking
48
+ const components = messages?.flatMap(m => m.components || []) || [];
49
+ const snapshot = _buildStructuralSnapshot(components);
50
+
51
+ artifact.turns.push({
52
+ turn: artifact.turns.length,
53
+ messages,
54
+ html,
55
+ summary,
56
+ validation,
57
+ intent: intent || summary || '',
58
+ snapshot,
59
+ timestamp: Date.now(),
60
+ });
61
+
62
+ artifact.metadata.updated = Date.now();
63
+ }
64
+
65
+ /**
66
+ * Get a specific turn for an execution.
67
+ *
68
+ * @param {string} executionId
69
+ * @param {number} [turn=-1] — Turn index (negative counts from end; -1 = latest)
70
+ * @returns {object|null}
71
+ */
72
+ get(executionId, turn = -1) {
73
+ const artifact = this.#artifacts.get(executionId);
74
+ if (!artifact || artifact.turns.length === 0) return null;
75
+
76
+ const idx = turn < 0 ? artifact.turns.length + turn : turn;
77
+ return artifact.turns[idx] ?? null;
78
+ }
79
+
80
+ /**
81
+ * Get all turns for an execution.
82
+ *
83
+ * @param {string} executionId
84
+ * @returns {object[]|null}
85
+ */
86
+ getAll(executionId) {
87
+ const artifact = this.#artifacts.get(executionId);
88
+ if (!artifact) return null;
89
+ return [...artifact.turns];
90
+ }
91
+
92
+ /**
93
+ * Get the original (first turn) prompt analyzer output for an execution.
94
+ * Used by iteration turns that need to recover the original concepts /
95
+ * implied components / steelman to drive canvas-aware decisions.
96
+ * @param {string} executionId
97
+ * @returns {object|null}
98
+ */
99
+ getOriginalAnalysis(executionId) {
100
+ const artifact = this.#artifacts.get(executionId);
101
+ return artifact?.metadata?.originalAnalysis || null;
102
+ }
103
+
104
+ /**
105
+ * Get the original (first turn) intent for an execution.
106
+ * @param {string} executionId
107
+ * @returns {string|null}
108
+ */
109
+ getOriginalIntent(executionId) {
110
+ const artifact = this.#artifacts.get(executionId);
111
+ return artifact?.metadata?.originalIntent || null;
112
+ }
113
+
114
+ /**
115
+ * Compute drift metrics between the first turn and the latest turn.
116
+ *
117
+ * Returns null if there are fewer than 2 turns.
118
+ *
119
+ * @param {string} executionId
120
+ * @returns {{ componentGrowth: number, sectionGrowth: number, newSections: string[], driftScore: number, turnCount: number, warning: string|null }|null}
121
+ */
122
+ getDriftMetrics(executionId) {
123
+ const artifact = this.#artifacts.get(executionId);
124
+ if (!artifact || artifact.turns.length < 2) return null;
125
+
126
+ const first = artifact.turns[0].snapshot;
127
+ const latest = artifact.turns[artifact.turns.length - 1].snapshot;
128
+ if (!first || !latest) return null;
129
+
130
+ // Component count growth ratio
131
+ const componentGrowth = first.componentCount > 0
132
+ ? latest.componentCount / first.componentCount
133
+ : latest.componentCount;
134
+
135
+ // Section count growth ratio
136
+ const sectionGrowth = first.sectionCount > 0
137
+ ? latest.sectionCount / first.sectionCount
138
+ : latest.sectionCount;
139
+
140
+ // New sections added since turn 0
141
+ const originalSections = new Set(first.sectionNames);
142
+ const newSections = latest.sectionNames.filter(s => !originalSections.has(s));
143
+
144
+ // Component type diversity change
145
+ const originalTypes = new Set(first.componentTypes);
146
+ const newTypes = latest.componentTypes.filter(t => !originalTypes.has(t));
147
+
148
+ // Drift score: 0 = no drift, 1 = extreme drift
149
+ // Weighted: 40% component growth, 30% section growth, 30% new sections
150
+ const growthFactor = Math.min(componentGrowth / 3, 1); // 3x = max
151
+ const sectionFactor = Math.min(sectionGrowth / 3, 1);
152
+ const newSectionFactor = Math.min(newSections.length / 4, 1); // 4+ new = max
153
+ const driftScore = Math.round((growthFactor * 0.4 + sectionFactor * 0.3 + newSectionFactor * 0.3) * 100) / 100;
154
+
155
+ // Warning thresholds
156
+ let warning = null;
157
+ if (driftScore >= 0.7) {
158
+ warning = `High canvas drift detected (score: ${driftScore}). Canvas grew from ${first.componentCount} to ${latest.componentCount} components across ${artifact.turns.length} turns. ${newSections.length} new sections added: ${newSections.join(', ')}. Consider confirming with user that this matches original intent: "${artifact.metadata.originalIntent}"`;
159
+ } else if (driftScore >= 0.4) {
160
+ warning = `Moderate canvas drift (score: ${driftScore}). Canvas: ${first.componentCount} → ${latest.componentCount} components. ${newSections.length} new sections added.`;
161
+ }
162
+
163
+ return {
164
+ componentGrowth: Math.round(componentGrowth * 100) / 100,
165
+ sectionGrowth: Math.round(sectionGrowth * 100) / 100,
166
+ newSections,
167
+ newTypes,
168
+ driftScore,
169
+ turnCount: artifact.turns.length,
170
+ originalIntent: artifact.metadata.originalIntent,
171
+ warning,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * List all execution summaries.
177
+ *
178
+ * @returns {object[]}
179
+ */
180
+ list() {
181
+ const results = [];
182
+ for (const [id, artifact] of this.#artifacts) {
183
+ const latest = artifact.turns[artifact.turns.length - 1];
184
+ results.push({
185
+ executionId: id,
186
+ turnCount: artifact.turns.length,
187
+ latestSummary: latest?.summary ?? '',
188
+ latestScore: latest?.validation?.score ?? null,
189
+ originalIntent: artifact.metadata.originalIntent ?? '',
190
+ created: artifact.metadata.created,
191
+ updated: artifact.metadata.updated,
192
+ });
193
+ }
194
+ return results;
195
+ }
196
+
197
+ /**
198
+ * Delete an execution's artifacts.
199
+ *
200
+ * @param {string} executionId
201
+ * @returns {boolean} — true if deleted
202
+ */
203
+ delete(executionId) {
204
+ return this.#artifacts.delete(executionId);
205
+ }
206
+
207
+ /**
208
+ * Clear all artifacts.
209
+ */
210
+ clear() {
211
+ this.#artifacts.clear();
212
+ }
213
+
214
+ /** Number of stored executions */
215
+ get size() {
216
+ return this.#artifacts.size;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Build a structural snapshot of the canvas for drift comparison.
222
+ * Extracts component count, section names, component types —
223
+ * the metrics that indicate structural changes.
224
+ *
225
+ * @param {object[]} components — flat A2UI component array
226
+ * @returns {{ componentCount: number, sectionCount: number, sectionNames: string[], componentTypes: string[] }}
227
+ */
228
+ function _buildStructuralSnapshot(components) {
229
+ if (!components || components.length === 0) {
230
+ return { componentCount: 0, sectionCount: 0, sectionNames: [], componentTypes: [] };
231
+ }
232
+
233
+ const sectionNames = [];
234
+ for (const c of components) {
235
+ if (c.component === 'Card') {
236
+ // Find the title of this Card section
237
+ const headerChild = components.find(
238
+ h => h.parentId === c.id && (h.component === 'Header' || h.component === 'CardHeader')
239
+ );
240
+ if (headerChild) {
241
+ const titleChild = components.find(
242
+ t => t.parentId === headerChild.id && t.component === 'Text'
243
+ );
244
+ sectionNames.push(titleChild?.text || titleChild?.children || headerChild.title || c.id);
245
+ } else {
246
+ sectionNames.push(c.id);
247
+ }
248
+ }
249
+ }
250
+
251
+ const typeSet = new Set();
252
+ for (const c of components) {
253
+ if (c.component) typeSet.add(c.component);
254
+ }
255
+
256
+ return {
257
+ componentCount: components.length,
258
+ sectionCount: sectionNames.length,
259
+ sectionNames,
260
+ componentTypes: [...typeSet].sort(),
261
+ };
262
+ }
@@ -0,0 +1,78 @@
1
+ # AdiaUI Generation Constitution
2
+
3
+ Rules the generator MUST satisfy. Used by the RLAIF critique-revise stage
4
+ to evaluate and improve generated output. Each rule maps to a validator check.
5
+
6
+ ---
7
+
8
+ ## Card Content Model
9
+
10
+ - Every `Card` MUST have at least one of: `Header`, `Section`, `Footer`.
11
+ - `Section` content MUST be wrapped in a `Column` component, not placed directly.
12
+ - Headings (`h1`-`h6`) belong in `Header`, not `Section`.
13
+ - Action buttons belong in `Footer`, not `Section` or `Header` (unless using `slot="action"`).
14
+ - `Header` supports 4 slots: `slot="icon"` (leading), `slot="heading"` (title), `slot="description"` (subtitle), `slot="action"` (trailing badge/button).
15
+
16
+ ## Typography & Variant System
17
+
18
+ - Use semantic HTML elements with `variant` attribute: `<h1 variant="title">`, `<h3 variant="section">`, `<p variant="body">`.
19
+ - Never use `data-heading` — use `variant` instead.
20
+ - Text hierarchy: display > title > heading > section > subsection > body > deck > caption > kicker > label.
21
+ - Use `color="subtle"` or `color="muted"` for secondary text, not inline styles.
22
+
23
+ ## Layout Primitives
24
+
25
+ - `Column` (col-ui): vertical stack. Use numeric `gap` values: `gap="2"`, `gap="4"`, `gap="6"`.
26
+ - `Row` (row-ui): horizontal. Use `gap`, `justify`, `align` attributes.
27
+ - `Grid` (grid-ui): CSS grid. Use `columns="2|3|4"`.
28
+ - Never use named gaps (`gap="sm"`, `gap="md"`, `gap="lg"`) — always numeric.
29
+ - Never use inline `style` attributes for layout.
30
+ - Never use CSS class names.
31
+
32
+ ## Component Types
33
+
34
+ - Use `Input` for text inputs (not `TextField` or `TextInput`).
35
+ - Use `CheckBox` for checkboxes (not `Checkbox`).
36
+ - Use `Select` for dropdowns (not `ChoicePicker`).
37
+ - Use `Toggle` for switch controls.
38
+ - Use `Button` with `variant="primary"` for main CTA, `variant="outline"` for secondary.
39
+ - Button text via `text` prop, icon via `icon` prop (Phosphor icon name).
40
+
41
+ ## Flat Adjacency Protocol
42
+
43
+ - Every component has a unique `id`.
44
+ - Root component MUST have `id: "root"`.
45
+ - Children referenced by ID array: `children: ["child-1", "child-2"]`.
46
+ - IDs should be short and descriptive: `"hdr"`, `"email-field"`, `"submit-btn"`.
47
+ - No circular references.
48
+ - No orphaned components (every non-root must be referenced by a parent).
49
+
50
+ ## Anti-Patterns (DO NOT)
51
+
52
+ 1. **No bare divs** — never use `<div>` or HTML-only elements. Use AdiaUI components.
53
+ 2. **No flat adjacency violations** — every Text inside Section must be inside a Column wrapper.
54
+ 3. **No heading hierarchy skips** — don't jump from h1 to h3.
55
+ 4. **No duplicate IDs** — every component must have a unique ID.
56
+ 5. **No content directly in Tab** — Tab is a button strip item, not a content container.
57
+ 6. **No invented components** — only use types registered in the A2UI registry.
58
+ 7. **No slot on containers** — `slot="heading"` goes on the heading element, not on Header.
59
+ 8. **No inline styles** — use component props and tokens, not `style="..."`.
60
+
61
+ ## Prose & Content Context
62
+
63
+ - For marketing/content pages, wrap in `<section prose>` to shift to content-optimized typography.
64
+ - Centered page headers use: `<header align="center" size="lg">` → `<col-ui gap="4">` → kicker/display/deck.
65
+ - Use `nomargin` on typography elements inside cards to prevent double-spacing.
66
+
67
+ ## Icon Names
68
+
69
+ - Use Phosphor icon names: `arrow-right`, `check-circle`, `warning-circle`, etc.
70
+ - Brand icons need `-logo` suffix: `google-logo`, `github-logo`, `twitter-logo`.
71
+ - Common aliases are resolved automatically: `send`→`paper-plane-right`, `settings`→`gear`, `mail`→`envelope`.
72
+
73
+ ## Accessibility
74
+
75
+ - Interactive elements must have `aria-label` if no visible text.
76
+ - Icon-only buttons must have `aria-label`.
77
+ - Images must have `alt` text.
78
+ - Form inputs must have `label` prop.