@adia-ai/a2ui-compose 0.4.4 → 0.4.6

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 CHANGED
@@ -12,6 +12,58 @@ generator graph.
12
12
 
13
13
  _No pending changes._
14
14
 
15
+ ## [0.4.6] - 2026-05-12
16
+
17
+ ### Changed — `core/` retirement follow-through (§64, v0.4.6)
18
+
19
+ Companion to `@adia-ai/a2ui-retrieval@0.4.6` retiring `pattern-library.js`:
20
+
21
+ - **`compose/core/pattern-export.js`** retired — no consumers post-§64 (the export shape targeted the retired pattern-library surface).
22
+ - **`compose/core/reference.js`** — updated to delegate to `composition-library` instead of the retired `pattern-library`.
23
+ - **`compose/core/generator.js`** — dropped pattern-export wiring; system-prompt path consumes compositions directly via §66's `adaptV09Component()`.
24
+
25
+ ### Changed — `buildSystemPrompt` reads canonical `.a2ui.json` sidecars (§66, v0.4.6)
26
+
27
+ Closes the catalog-drift surface that §56 explicitly deferred ("different schema shapes; restructuring `getComponentCatalog()` is its own arc"). The monolithic prompt builder now reads from the v0.9 `.a2ui.json` sidecars (generated from yamls by `npm run components`) instead of the legacy hand-maintained `_components.json` catalog.
28
+
29
+ **Root cause uncovered during audit:** `strategies/monolithic/_shared.js` (pre-§66) called `getComponentData(typeName)` returning the raw v0.9 sidecar — `{title, description, properties, x-adiaui}` (JSON Schema shape) — but the prompt builder code did `a2uiData.props ? Object.entries(a2uiData.props)`. The v0.9 shape uses `properties`, not `props`. **Every prompt was silently falling through to the legacy `_components.json` catalog** because every v0.9 lookup looked empty. The "canonical sidecar reads" path was wired but dead code.
30
+
31
+ **Fix:** new `adaptV09Component()` helper at the top of `_shared.js` (~90 LOC) normalizes the v0.9 sidecar shape to the legacy `{tag, category, description, props, events, slots, examples, aliases, keywords}` shape the rest of the prompt builder was written for. Properties map 1:1 (skipping `component` discriminator + `children` container + `const`-only fields). Type names map JSON-Schema lowercase → legacy Title-Case (`string`→`String`, `boolean`→`Boolean`, etc.).
32
+
33
+ **Empirical fidelity gain** (Button as canonical primitive):
34
+
35
+ | Source | Props surfaced |
36
+ |---|---|
37
+ | `_components.json` (legacy, before §66) | 7: `text, variant, size, disabled, block, icon, type` (`block` is stale — removed from yaml in an earlier arc but never cleaned up in the hand-maintained catalog) |
38
+ | v0.9 sidecar (canonical, after §66) | 10: `type, aria-label, color, disabled, icon, size, stretch, text, textContent, variant` |
39
+
40
+ **§66 surfaces ~30% more catalog data** for the LLM, all traceable to a yaml SoT that `npm run components` regenerates on demand. This is exactly the catalog drift §56 flagged: when the team added `Select.size` (§50) and `inputmode`/`autocomplete` (§51) to the yamls, those props went into the v0.9 sidecars but never propagated to `_components.json`. The LLM was getting a stale catalog view that excluded recent additions. After §66, the LLM gets the actual ground truth.
41
+
42
+ **Pitfalls encoded in the adapter** (5 v0.9-shape gotchas):
43
+ 1. `events` is an object `{eventName: {description}}`, not an array. Normalize to `string[]`.
44
+ 2. `slots` is also object-shaped. Same fix.
45
+ 3. `synonyms.tags` may be null/undefined. Wrap in `Array.isArray()` guard.
46
+ 4. `component` and `children` properties on every v0.9 sidecar are discriminator/container fields, not user-facing props. Skip in the adapter loop.
47
+ 5. `const` fields (e.g. `{const: 'Button'}`) are discriminator-only. Skip.
48
+
49
+ Source: `packages/a2ui/compose/strategies/monolithic/_shared.js` (+91, -2 lines, commit `afda98f5`).
50
+
51
+ ## [0.4.5] - 2026-05-12
52
+
53
+ ### Changed — GenUI overhaul prompt-engineering (§56, v0.4.5)
54
+
55
+ - **`strategies/monolithic/_shared.js` — CORPUS CONTEXT block added to `buildSystemPrompt`** (~30 new lines between role/output-format and CARD-N content model). Surgical insertion explaining the §36-§51 retrieval-augmented pipeline:
56
+ - Pipeline searches PATTERNS / COMPOSITIONS first (full-canvas A2UI templates: login-form, pricing-tiers, dashboard-admin-page)
57
+ - Pipeline also searches ANNOTATED CHUNKS (real production HTML with `metadata.{domain, keywords, description}`)
58
+ - When matched, `MATCHED PATTERN` / `STRUCTURAL REFERENCE` blocks carry the actual reference
59
+ - Only AVAILABLE COMPONENTS are usable — anything else is a hallucination
60
+
61
+ Cost: ~150 tokens per request. Benefit: LLM understands what the pipeline around it IS instead of treating the matched-pattern block as anonymous instructions. Existing rules unchanged; no removed lines.
62
+
63
+ - **`strategies/monolithic/generate-pro.js` — STRUCTURAL REFERENCE prose enriched.** Prior copy: "a real production block from the codebase matched this intent." New copy: "this chunk was retrieved from the AdiaUI training corpus (annotated production HTML, harvested from real app pages). It matched your intent on keyword/domain ranking." Threads chunk `metadata.domain`, `metadata.description`, `metadata.keywords` into the prompt when present. Reframes "do not copy the HTML" as "the chunk represents the SHAPE the user wants; instantiate it with their content" — more actionable framing.
64
+
65
+ See root [CHANGELOG.md `[Unreleased]`](../../../CHANGELOG.md) for the v0.4.5 overhaul arc + apps/genui/CHANGELOG.md `[Unreleased]` for the per-§ rollup.
66
+
15
67
  ## [0.4.4] - 2026-05-12
16
68
 
17
69
  ### Changed
package/core/generator.js CHANGED
@@ -17,7 +17,6 @@ import { store, engine } from './state.js';
17
17
  import { checkIntentAlignment } from '../../retrieval/intent/intent-alignment.js';
18
18
  import { decomposeIntent, composeSubtasks } from '../../retrieval/intent/decomposer.js';
19
19
  import { getWiringCatalog } from '../../retrieval/wiring-catalog.js';
20
- import { getComponentData } from '../../retrieval/pattern-library.js';
21
20
 
22
21
  import { StubLLMAdapter } from '../../../llm/llm-stub.js';
23
22
  import { createAdapter } from '../../../llm/llm-bridge.js';
package/core/reference.js CHANGED
@@ -1,16 +1,44 @@
1
1
  /**
2
- * Reference Tools — Delegates to A003 intelligence system.
2
+ * Reference Tools — stable generative-system API surface.
3
3
  *
4
- * Thin wrappers that provide a stable generative-system API surface
5
- * over the intelligence module. Used by the generator and pipeline stages
6
- * to look up components, patterns, and domain context.
4
+ * Thin wrappers used by the monolithic engines + generator pipeline.
5
+ * §64 migrated the "pattern" surface from `pattern-library.js` (retired)
6
+ * to the zettel `composition-library.js`. Function names kept
7
+ * (searchBlocks/lookupPattern/listPatterns/searchBlocksSemantic) for
8
+ * back-compat with engine call sites; internally they hit compositions.
7
9
  */
8
10
 
9
11
  import { getCatalog, getComponent, getComponentsByCategory } from '../../retrieval/index.js';
10
12
  import { classifyIntent, assembleContext } from '../../retrieval/index.js';
11
- import { getAllPatterns, searchPatterns, getPattern, semanticSearchPatterns } from '../../retrieval/index.js';
12
13
  import { checkAllAntiPatterns } from '../../retrieval/index.js';
13
14
  import { serializeEntry } from '../../retrieval/index.js';
15
+ import {
16
+ searchAll as searchCompositions,
17
+ getComposition,
18
+ getAllCompositions,
19
+ } from '../strategies/zettel/composition-library.js';
20
+
21
+ /**
22
+ * Compositions have `tags: { purpose: [...], complexity: "", layout: "" }`
23
+ * but the monolithic engines (instant/pro/thinking) all assume the old
24
+ * pattern shape `tags: [string]` and call `.map(...)` on it. Flatten on
25
+ * the way out so legacy call sites keep working without re-templating
26
+ * every engine. Annotated chunks expose flat-but-shallow `tags`; both
27
+ * are handled.
28
+ */
29
+ function normalize(record) {
30
+ if (!record) return record;
31
+ let tags = record.tags;
32
+ if (Array.isArray(tags)) return record;
33
+ if (tags && typeof tags === 'object') {
34
+ tags = Object.values(tags)
35
+ .flat()
36
+ .filter((v) => typeof v === 'string');
37
+ } else {
38
+ tags = [];
39
+ }
40
+ return { ...record, tags };
41
+ }
14
42
 
15
43
  /**
16
44
  * Look up a single component by A2UI type name.
@@ -50,40 +78,47 @@ export async function getComponentsByGroup(category) {
50
78
  }
51
79
 
52
80
  /**
53
- * Search the pattern library by query.
81
+ * Search the composition library by query.
82
+ * Returns full composition records (rehydrated from name hits).
54
83
  * @param {string} query — Natural language or keyword query
55
84
  * @returns {object[]}
56
85
  */
57
86
  export function searchBlocks(query, { domain } = {}) {
58
- return searchPatterns(query, { domain });
87
+ const hits = searchCompositions(query);
88
+ const records = hits.map((h) => getComposition(h.name)).filter(Boolean);
89
+ const filtered = domain ? records.filter((c) => c.domain === domain) : records;
90
+ return filtered.map(normalize);
59
91
  }
60
92
 
61
93
  /**
62
- * Get a specific named pattern.
63
- * @param {string} name — Pattern name (e.g. 'stat-cards', 'user-profile')
94
+ * Get a specific named composition.
95
+ * @param {string} name — Composition name
64
96
  * @returns {object|null}
65
97
  */
66
98
  export function lookupPattern(name) {
67
- return getPattern(name);
99
+ return normalize(getComposition(name));
68
100
  }
69
101
 
70
102
  /**
71
- * Get all available patterns.
103
+ * Get all available compositions.
72
104
  * @returns {object[]}
73
105
  */
74
106
  export function listPatterns() {
75
- return getAllPatterns();
107
+ return getAllCompositions().map(normalize);
76
108
  }
77
109
 
78
110
  /**
79
- * Semantic search LLM-enhanced pattern matching.
80
- * Falls back to keyword search if no adapter provided.
111
+ * Keyword search wrapped as the legacy `{ matches }` return shape so the
112
+ * monolithic-thinking engine's call site keeps working. The historical
113
+ * LLM-driven semantic path retired with `pattern-library.js` in §64;
114
+ * compositions don't have a semantic search backend yet.
115
+ *
81
116
  * @param {string} query
82
- * @param {object} [options] — { llmAdapter, remix }
83
- * @returns {Promise<{ matches: object[], remix?: object }>}
117
+ * @param {object} [_options] — accepted for back-compat; llmAdapter/remix ignored
118
+ * @returns {Promise<{ matches: object[] }>}
84
119
  */
85
- export async function searchBlocksSemantic(query, options = {}) {
86
- return semanticSearchPatterns(query, options);
120
+ export async function searchBlocksSemantic(query, _options = {}) {
121
+ return { matches: searchBlocks(query) };
87
122
  }
88
123
 
89
124
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/a2ui-compose",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "AdiaUI A2UI compose engine — framework-agnostic. Takes natural-language intents + a catalog and produces A2UI protocol messages. Pairs with `@adia-ai/a2ui-retrieval` (intent classification, catalog lookup) and `@adia-ai/a2ui-validator` (schema + semantic checks).",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Dual-environment chunk loader — Node + browser.
3
3
  *
4
- * Mirrors the pattern in packages/a2ui/retrieval/pattern-library.js so the
5
- * gen-UI training chunks at packages/a2ui/corpus/chunks/*.json are reachable
6
- * from BOTH server-side engines (monolithic-pro via /api/generate's Node
7
- * process) AND browser-side engines (monolithic-pro called directly from
4
+ * Mirrors the dual-environment loading approach historically used by
5
+ * pattern-library.js (retired §64) so the gen-UI training chunks at
6
+ * packages/a2ui/corpus/chunks/*.json are reachable from BOTH server-side
7
+ * engines (monolithic-pro via /api/generate's Node process) AND
8
+ * browser-side engines (monolithic-pro called directly from
8
9
  * gen-ui playground's IIFE).
9
10
  *
10
11
  * The existing packages/a2ui/corpus/scripts/chunk-library.js is Node-only
@@ -12,7 +12,91 @@ import { store } from '../../core/state.js';
12
12
  import { checkIntentAlignment } from '../../../retrieval/intent/intent-alignment.js';
13
13
  import { composeSubtasks } from '../../../retrieval/intent/decomposer.js';
14
14
  import { getWiringCatalog } from '../../../retrieval/wiring-catalog.js';
15
- import { getComponentData } from '../../../retrieval/pattern-library.js';
15
+ import { getComponentData } from '../../../retrieval/component-catalog.js';
16
+
17
+ // ── v0.9 .a2ui.json sidecar → prompt-catalog adapter ─────────────
18
+ //
19
+ // §66 (v0.4.6): the `.a2ui.json` sidecars produced by `npm run components`
20
+ // are v0.9 JSON Schemas with `{title, description, properties, x-adiaui}`.
21
+ // The legacy `_components.json` catalog this prompt builder originally
22
+ // read had a flatter `{tag, category, description, props, events, slots,
23
+ // examples, aliases, keywords}` shape. This adapter normalizes the v0.9
24
+ // sidecar into the legacy shape so the rest of the prompt builder can stay
25
+ // unchanged.
26
+ //
27
+ // Before §66, this code did `a2uiData.props` — which silently returned 0
28
+ // props for every component (the v0.9 shape uses `properties`, not
29
+ // `props`), causing every prompt to fall through to the `_components.json`
30
+ // catalog. After §66, the canonical sidecar drives the prompt directly
31
+ // and `_components.json` is retired (§64/§65).
32
+ function adaptV09Component(a2uiData) {
33
+ if (!a2uiData || typeof a2uiData !== 'object') return null;
34
+ const ext = a2uiData['x-adiaui'] || {};
35
+ const properties = a2uiData.properties || {};
36
+
37
+ // Map v0.9 property → legacy prop shape. Skip the always-present
38
+ // `component` (const PascalCase name) + `children` (slot container)
39
+ // fields since they aren't user-facing props.
40
+ const props = {};
41
+ for (const [name, spec] of Object.entries(properties)) {
42
+ if (name === 'component' || name === 'children') continue;
43
+ if (spec?.const) continue; // discriminator-only fields
44
+ const out = {};
45
+ // JSON Schema type names are lowercase; legacy catalog used Title-Case.
46
+ if (spec.type) {
47
+ out.type = spec.type === 'string' ? 'String'
48
+ : spec.type === 'boolean' ? 'Boolean'
49
+ : spec.type === 'number' || spec.type === 'integer' ? 'Number'
50
+ : spec.type === 'array' ? 'Array'
51
+ : spec.type === 'object' ? 'Object'
52
+ : String(spec.type);
53
+ }
54
+ if (spec.enum) out.enum = spec.enum;
55
+ if (spec.default !== undefined) out.default = spec.default;
56
+ if (spec.description) out.description = spec.description;
57
+ props[name] = out;
58
+ }
59
+
60
+ // Events in the legacy catalog were a flat string[]. The v0.9 sidecar
61
+ // stores them as an object: `{eventName: {description}}` (verified
62
+ // across all 94 sidecars on 2026-05-12). Older code paths accepted
63
+ // either shape; normalize to a string[] of event names.
64
+ const eventsRaw = ext.events;
65
+ let events = [];
66
+ if (Array.isArray(eventsRaw)) {
67
+ events = eventsRaw.map((e) => (typeof e === 'string' ? e : e?.name)).filter(Boolean);
68
+ } else if (eventsRaw && typeof eventsRaw === 'object') {
69
+ events = Object.keys(eventsRaw);
70
+ }
71
+
72
+ // Slots — also object-shaped (`{slotName: {description, type}}`) in v0.9.
73
+ // Legacy catalog stored a string[]. Normalize.
74
+ const slotsRaw = ext.slots;
75
+ let slots = [];
76
+ if (Array.isArray(slotsRaw)) {
77
+ slots = slotsRaw.map((s) => (typeof s === 'string' ? s : s?.name)).filter(Boolean);
78
+ } else if (slotsRaw && typeof slotsRaw === 'object') {
79
+ slots = Object.keys(slotsRaw);
80
+ }
81
+
82
+ // Aliases live under `x-adiaui.synonyms.tags` in v0.9. The synonyms
83
+ // field is always an object (verified across all 94 sidecars). Surface
84
+ // the `tags` array — the prompt renders them as "Button (also: SubmitButton)".
85
+ const synonyms = (ext.synonyms && typeof ext.synonyms === 'object') ? ext.synonyms : null;
86
+ const aliases = Array.isArray(synonyms?.tags) ? synonyms.tags : null;
87
+
88
+ return {
89
+ tag: ext.tag || null,
90
+ category: ext.category || 'layout',
91
+ description: a2uiData.description || '',
92
+ props,
93
+ events,
94
+ slots,
95
+ examples: Array.isArray(ext.examples) ? ext.examples : [],
96
+ aliases,
97
+ keywords: Array.isArray(ext.keywords) ? ext.keywords : [],
98
+ };
99
+ }
16
100
 
17
101
  // Component prop catalog — loaded lazily for prompt injection
18
102
  let _componentCatalog = null;
@@ -51,6 +135,32 @@ Output format: [{ "type": "updateComponents", "surfaceId": "default", "component
51
135
  Each component: { "id": "<unique>", "component": "<Type>", "children": ["<childId>", ...], ...props }
52
136
  The root must have id "root". Use short, descriptive IDs (e.g., "hdr", "email-field", "submit-btn").`);
53
137
 
138
+ // ── Corpus context (§56, v0.4.5) ──
139
+ // Tells the LLM how the system surrounding it is wired so it doesn't
140
+ // hallucinate component names and so it understands what MATCHED PATTERN /
141
+ // STRUCTURAL REFERENCE blocks actually ARE. Keep this section tight — every
142
+ // token costs latency + money.
143
+ parts.push(`CORPUS CONTEXT (the system around you):
144
+
145
+ You are part of a retrieval-augmented pipeline. Before this prompt was built,
146
+ the pipeline searched two corpora for matches to the user's intent:
147
+
148
+ 1. PATTERNS / COMPOSITIONS — full-canvas A2UI templates curated for the
149
+ AdiaUI design system. Examples: login-form, pricing-tiers,
150
+ dashboard-admin-page. When a strong match exists, you'll see a
151
+ "MATCHED PATTERN" block below with the canonical template.
152
+
153
+ 2. ANNOTATED CHUNKS — real production HTML blocks harvested from the AdiaUI
154
+ apps (auth flows, dashboards, error pages, etc.) with metadata
155
+ (domain, keywords, description). When a strong chunk match exists,
156
+ you'll see a "STRUCTURAL REFERENCE" block with the production HTML.
157
+ Match its component palette + information density, but translate to
158
+ A2UI components — do not copy raw HTML.
159
+
160
+ Both corpora use ONLY components from the AVAILABLE COMPONENTS list further
161
+ down. If you would invent a component name not in that list, you're
162
+ hallucinating — use the closest canonical name instead.`);
163
+
54
164
  // ── Card-N content model (critical for quality) ──
55
165
  parts.push(`CARD-N CONTENT MODEL (mandatory for any card surface):
56
166
  - Card > Header: for title/description. Use Text children with heading variants (h3, h4).
@@ -134,8 +244,13 @@ KEY RULES:
134
244
  return Array.isArray(a) && a.length ? ` (also: ${a.join(', ')})` : '';
135
245
  };
136
246
  for (const typeName of relevantTypes) {
137
- // Prefer .a2ui.json data (new source of truth), fallback to _components.json
138
- const a2uiData = getComponentData(typeName);
247
+ // §66 (v0.4.6): .a2ui.json sidecar is the canonical source of truth.
248
+ // The raw v0.9 shape uses `properties` (JSON Schema); adapt to the
249
+ // legacy `props` shape this prompt builder was originally written
250
+ // for. Falls back to the (soon-retired) `_components.json` only
251
+ // when no sidecar exists.
252
+ const v09Raw = getComponentData(typeName);
253
+ const a2uiData = v09Raw ? adaptV09Component(v09Raw) : null;
139
254
  if (a2uiData) {
140
255
  const props = a2uiData.props ? Object.entries(a2uiData.props).map(([k, v]) => {
141
256
  let desc = k;
@@ -163,7 +163,7 @@ export async function generateInstant({ intent, executionId, storeId, analysis,
163
163
 
164
164
  engine.submitStage(execId, 'generate', {
165
165
  messages,
166
- source: bestMatch ? 'pattern-library' : 'fallback',
166
+ source: bestMatch ? 'composition-library' : 'fallback',
167
167
  confidence: bestMatch ? 0.95 : 0.4,
168
168
  });
169
169
 
@@ -221,10 +221,18 @@ export async function generatePro({ intent, executionId, storeId, llmAdapter, an
221
221
  // structural "look like this" hint that lets the LLM materialize the
222
222
  // real chunk shape (e.g. auth-signin-card-email) instead of
223
223
  // hallucinating a passable-but-wrong shape from the brief alone.
224
+ //
225
+ // §56 (v0.4.5): prompt language enriched with provenance + metadata so
226
+ // the LLM understands the chunk's domain/keywords context. Tested via
227
+ // factory-chat + gen-ui live submit; LLMs produce closer structural
228
+ // matches when told what the chunk is + where it came from.
224
229
  const chunkReferenceBlock = chunkRefHtml
225
- ? `\nSTRUCTURAL REFERENCE — a real production block from the codebase matched this intent. Match its component palette, information density, and Card / Header / Section / Footer anatomy. Do NOT copy the HTML verbatim into A2UI; translate its semantic structure into the equivalent A2UI components:
230
+ ? `\nSTRUCTURAL REFERENCE — this chunk was retrieved from the AdiaUI training corpus (annotated production HTML, harvested from real app pages). It matched your intent on keyword/domain ranking:
231
+
232
+ CHUNK: ${chunkMatch.name}
233
+ kind=${chunkMatch.kind}, primary=${chunkMatch.primary}, score=${chunkMatch.score}${chunkMatch.metadata?.domain ? `, domain=${chunkMatch.metadata.domain}` : ''}${chunkMatch.metadata?.description ? `\n description: ${chunkMatch.metadata.description}` : ''}${chunkMatch.metadata?.keywords ? `\n keywords: ${chunkMatch.metadata.keywords.join(', ')}` : ''}
226
234
 
227
- CHUNK: ${chunkMatch.name} (${chunkMatch.kind}, primary=${chunkMatch.primary}, score=${chunkMatch.score})
235
+ Match this chunk's component palette, information density, and Card/Header/Section/Footer anatomy. Do NOT copy the HTML verbatim into A2UI — translate its semantic structure into the equivalent A2UI components. The chunk represents the SHAPE the user wants; your job is to instantiate that shape with their content:
228
236
  ---
229
237
  ${chunkRefHtml}
230
238
  ---
@@ -62,6 +62,13 @@ function chunkToComposition(chunkDoc, sourcePath) {
62
62
  };
63
63
  }
64
64
 
65
+ /**
66
+ * Track whether the eager top-level load has run. Multiple `loadAll`
67
+ * call sites (mcp/server.js bootstrap + any other consumer) re-clear
68
+ * + re-build, which is wasted work but not incorrect.
69
+ */
70
+ let _autoLoaded = false;
71
+
65
72
  export function loadAll() {
66
73
  compositions.clear();
67
74
 
@@ -99,6 +106,7 @@ export function loadAll() {
99
106
  annotatedChunkCount++;
100
107
  });
101
108
 
109
+ _autoLoaded = true;
102
110
  return {
103
111
  compositionCount: compositions.size,
104
112
  handAuthoredCount: compositions.size - annotatedChunkCount,
@@ -106,6 +114,12 @@ export function loadAll() {
106
114
  };
107
115
  }
108
116
 
117
+ // Eager top-level load so synchronous getters (getComposition,
118
+ // getAllCompositions, searchAll) work for consumers that don't call
119
+ // loadAll themselves — test harnesses, smoke scripts, and the
120
+ // retrieval-side helpers migrated off pattern-library in §64.
121
+ if (!_autoLoaded) loadAll();
122
+
109
123
  // Back-compat shims removed in §38 — fragment-library.js → composition-library.js
110
124
  // rename. The retired `getFragment` / `getAllFragments` exports had zero
111
125
  // external callers; drop them.
@@ -1,149 +0,0 @@
1
- /**
2
- * Pattern Export — Save generated UI as reusable named patterns.
3
- *
4
- * Produces two downloadable files:
5
- * {name}.json — A2UI flat adjacency pattern (importable into pattern-library)
6
- * {name}.html — Rendered HTML snapshot from the canvas
7
- *
8
- * Also supports importing patterns back into the runtime library.
9
- */
10
-
11
- import { registerPattern } from '../../retrieval/pattern-library.js';
12
-
13
- /**
14
- * Build a pattern-library-compatible JSON object from generation results.
15
- *
16
- * @param {string} name — Pattern name (kebab-case, e.g. "my-pricing-page")
17
- * @param {object} opts
18
- * @param {object[]} opts.components — Flat adjacency array from messages[0].components
19
- * @param {string} opts.intent — Original user intent
20
- * @param {string} [opts.domain] — Domain classification (forms, data, layout, agent, navigation)
21
- * @param {string} [opts.description] — Human description (falls back to intent)
22
- * @returns {object} — Pattern object matching pattern-library format
23
- */
24
- export function buildPatternJSON(name, { components, intent, domain, description }) {
25
- const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
26
-
27
- // Extract unique component type names
28
- const componentTypes = [...new Set(
29
- components.map(c => c.component).filter(Boolean)
30
- )];
31
-
32
- // Clean components: strip internal fields
33
- const template = components.map(c => {
34
- const { _surfaceId, ...rest } = c;
35
- return rest;
36
- });
37
-
38
- return {
39
- name: slug,
40
- description: description || intent || slug,
41
- domain: domain || 'layout',
42
- components: componentTypes,
43
- template,
44
- };
45
- }
46
-
47
- /**
48
- * Save a generation as a named pattern. Downloads .json + .html files.
49
- *
50
- * @param {string} name — Pattern name
51
- * @param {object} opts
52
- * @param {object[]} opts.messages — A2UI messages array
53
- * @param {object} opts.validation — Validation result (must have score >= 95)
54
- * @param {string} opts.intent — Original user intent
55
- * @param {string} [opts.domain] — Domain classification
56
- * @param {string} [opts.canvasHTML] — Rendered HTML from canvas
57
- * @param {number} [opts.minScore=95] — Minimum score to allow save
58
- * @returns {{ json: object, html: string }} — The saved payloads
59
- */
60
- export function savePattern(name, { messages, validation, intent, domain, canvasHTML, minScore = 95 }) {
61
- if (!validation || validation.score < minScore) {
62
- throw new Error(`Score ${validation?.score ?? 0} is below minimum ${minScore} for pattern save`);
63
- }
64
-
65
- const components = messages?.[0]?.components;
66
- if (!components || !components.length) {
67
- throw new Error('No components to save');
68
- }
69
-
70
- const json = buildPatternJSON(name, { components, intent, domain });
71
-
72
- // Download .json
73
- downloadFile(`${json.name}.json`, JSON.stringify(json, null, 2), 'application/json');
74
-
75
- // Download .html (if available)
76
- const html = canvasHTML || '';
77
- if (html) {
78
- const htmlDoc = `<!DOCTYPE html>
79
- <html lang="en">
80
- <head>
81
- <meta charset="UTF-8">
82
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
83
- <title>${json.name} — A2UI Pattern</title>
84
- <style>body { font-family: system-ui, sans-serif; padding: 2rem; }</style>
85
- </head>
86
- <body>
87
- ${html}
88
- </body>
89
- </html>`;
90
- downloadFile(`${json.name}.html`, htmlDoc, 'text/html');
91
- }
92
-
93
- // Also register in the runtime library
94
- registerPattern(json);
95
-
96
- return { json, html };
97
- }
98
-
99
- /**
100
- * Import a pattern JSON object into the runtime pattern library.
101
- *
102
- * @param {object|string} patternJSON — Pattern object or JSON string
103
- * @returns {{ success: boolean, name?: string, error?: string }}
104
- */
105
- export function importPattern(patternJSON) {
106
- let pattern = patternJSON;
107
- if (typeof pattern === 'string') {
108
- try { pattern = JSON.parse(pattern); }
109
- catch { return { success: false, error: 'Invalid JSON' }; }
110
- }
111
-
112
- if (!pattern?.name) return { success: false, error: 'Missing pattern name' };
113
- if (!pattern?.template || !Array.isArray(pattern.template)) {
114
- return { success: false, error: 'Missing or invalid template array' };
115
- }
116
-
117
- const registered = registerPattern(pattern);
118
- if (!registered) {
119
- return { success: false, error: `Pattern "${pattern.name}" already exists` };
120
- }
121
-
122
- return { success: true, name: pattern.name };
123
- }
124
-
125
- /**
126
- * Trigger a file download in the browser.
127
- *
128
- * @param {string} filename
129
- * @param {string} content
130
- * @param {string} [mimeType='application/json']
131
- */
132
- export function downloadFile(filename, content, mimeType = 'application/json') {
133
- if (typeof document === 'undefined') {
134
- throw new Error('downloadFile requires a browser environment');
135
- }
136
-
137
- const blob = new Blob([content], { type: mimeType });
138
- const url = URL.createObjectURL(blob);
139
- const a = document.createElement('a');
140
- a.href = url;
141
- a.download = filename;
142
- a.style.display = 'none';
143
- // Prevent SPA router from intercepting the blob URL click
144
- a.addEventListener('click', (e) => e.stopPropagation());
145
- document.body.appendChild(a);
146
- a.click();
147
- document.body.removeChild(a);
148
- URL.revokeObjectURL(url);
149
- }