@adia-ai/a2ui-compose 0.4.5 → 0.4.7
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 +44 -0
- package/core/generator.js +0 -1
- package/core/reference.js +53 -18
- package/package.json +1 -1
- package/strategies/_shared/chunk-loader.js +5 -4
- package/strategies/monolithic/_shared.js +118 -10
- package/strategies/monolithic/generate-instant.js +1 -1
- package/strategies/zettel/composition-library.js +119 -60
- package/core/pattern-export.js +0 -149
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,50 @@ generator graph.
|
|
|
12
12
|
|
|
13
13
|
_No pending changes._
|
|
14
14
|
|
|
15
|
+
## [0.4.7] - 2026-05-12
|
|
16
|
+
|
|
17
|
+
### Changed — `strategies/monolithic/_shared.js` `getComponentCatalog()` reads canonical catalog (§72)
|
|
18
|
+
|
|
19
|
+
The legacy reader of `@adia-ai/a2ui-corpus/patterns/_components.json` has been migrated to read `@adia-ai/a2ui-corpus/catalog-a2ui_0_9.json` (the canonical v0.9 catalog, already the package root export). Per-component aliases are now lifted from `components[name].x-adiaui.synonyms.tags`. Same legacy output shape (`{ <Name>: { aliases: string[] } }`); zero behavior change for downstream callers.
|
|
20
|
+
|
|
21
|
+
Closes the §65 carry-over from v0.4.6 — `@adia-ai/a2ui-corpus` `patterns/` + `compositions/` are now deleted from disk + tarball.
|
|
22
|
+
|
|
23
|
+
## [0.4.6] - 2026-05-12
|
|
24
|
+
|
|
25
|
+
### Changed — `core/` retirement follow-through (§64, v0.4.6)
|
|
26
|
+
|
|
27
|
+
Companion to `@adia-ai/a2ui-retrieval@0.4.6` retiring `pattern-library.js`:
|
|
28
|
+
|
|
29
|
+
- **`compose/core/pattern-export.js`** retired — no consumers post-§64 (the export shape targeted the retired pattern-library surface).
|
|
30
|
+
- **`compose/core/reference.js`** — updated to delegate to `composition-library` instead of the retired `pattern-library`.
|
|
31
|
+
- **`compose/core/generator.js`** — dropped pattern-export wiring; system-prompt path consumes compositions directly via §66's `adaptV09Component()`.
|
|
32
|
+
|
|
33
|
+
### Changed — `buildSystemPrompt` reads canonical `.a2ui.json` sidecars (§66, v0.4.6)
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
**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.
|
|
38
|
+
|
|
39
|
+
**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.).
|
|
40
|
+
|
|
41
|
+
**Empirical fidelity gain** (Button as canonical primitive):
|
|
42
|
+
|
|
43
|
+
| Source | Props surfaced |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `_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) |
|
|
46
|
+
| v0.9 sidecar (canonical, after §66) | 10: `type, aria-label, color, disabled, icon, size, stretch, text, textContent, variant` |
|
|
47
|
+
|
|
48
|
+
**§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.
|
|
49
|
+
|
|
50
|
+
**Pitfalls encoded in the adapter** (5 v0.9-shape gotchas):
|
|
51
|
+
1. `events` is an object `{eventName: {description}}`, not an array. Normalize to `string[]`.
|
|
52
|
+
2. `slots` is also object-shaped. Same fix.
|
|
53
|
+
3. `synonyms.tags` may be null/undefined. Wrap in `Array.isArray()` guard.
|
|
54
|
+
4. `component` and `children` properties on every v0.9 sidecar are discriminator/container fields, not user-facing props. Skip in the adapter loop.
|
|
55
|
+
5. `const` fields (e.g. `{const: 'Button'}`) are discriminator-only. Skip.
|
|
56
|
+
|
|
57
|
+
Source: `packages/a2ui/compose/strategies/monolithic/_shared.js` (+91, -2 lines, commit `afda98f5`).
|
|
58
|
+
|
|
15
59
|
## [0.4.5] - 2026-05-12
|
|
16
60
|
|
|
17
61
|
### Changed — GenUI overhaul prompt-engineering (§56, v0.4.5)
|
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 —
|
|
2
|
+
* Reference Tools — stable generative-system API surface.
|
|
3
3
|
*
|
|
4
|
-
* Thin wrappers
|
|
5
|
-
*
|
|
6
|
-
* to
|
|
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
|
|
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
|
-
|
|
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
|
|
63
|
-
* @param {string} name —
|
|
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
|
|
99
|
+
return normalize(getComposition(name));
|
|
68
100
|
}
|
|
69
101
|
|
|
70
102
|
/**
|
|
71
|
-
* Get all available
|
|
103
|
+
* Get all available compositions.
|
|
72
104
|
* @returns {object[]}
|
|
73
105
|
*/
|
|
74
106
|
export function listPatterns() {
|
|
75
|
-
return
|
|
107
|
+
return getAllCompositions().map(normalize);
|
|
76
108
|
}
|
|
77
109
|
|
|
78
110
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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} [
|
|
83
|
-
* @returns {Promise<{ matches: 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,
|
|
86
|
-
return
|
|
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.
|
|
3
|
+
"version": "0.4.7",
|
|
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
|
|
5
|
-
* gen-UI training chunks at
|
|
6
|
-
* from BOTH server-side
|
|
7
|
-
*
|
|
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,26 +12,129 @@ 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/
|
|
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
|
+
}
|
|
16
81
|
|
|
17
|
-
//
|
|
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
|
+
}
|
|
100
|
+
|
|
101
|
+
// Component prop catalog — loaded lazily for prompt injection.
|
|
102
|
+
//
|
|
103
|
+
// Since §65 (v0.4.7), reads from the canonical v0.9 catalog at
|
|
104
|
+
// `corpus/catalog-a2ui_0_9.json` (assembled from yamls by `npm run
|
|
105
|
+
// components`). Previously read the hand-maintained
|
|
106
|
+
// `corpus/patterns/_components.json` which was retired in §65.
|
|
107
|
+
// Aliases come from `x-adiaui.synonyms.tags` per the v0.9 sidecar shape;
|
|
108
|
+
// migrated in §65 step 1 — 17 yamls populated with synonyms.tags from
|
|
109
|
+
// the prior `_components.json` aliases field.
|
|
18
110
|
let _componentCatalog = null;
|
|
19
111
|
async function getComponentCatalog() {
|
|
20
112
|
if (_componentCatalog) return _componentCatalog;
|
|
21
113
|
try {
|
|
22
114
|
const IS_NODE = typeof process !== 'undefined' && process.versions?.node;
|
|
115
|
+
let catalogJson;
|
|
23
116
|
if (IS_NODE) {
|
|
24
117
|
const fs = await import(/* @vite-ignore */ 'node:fs/promises');
|
|
25
118
|
const path = await import(/* @vite-ignore */ 'node:path');
|
|
26
119
|
const url = await import(/* @vite-ignore */ 'node:url');
|
|
27
120
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
28
|
-
const raw = await fs.readFile(path.join(__dirname, '../../../corpus/
|
|
29
|
-
|
|
121
|
+
const raw = await fs.readFile(path.join(__dirname, '../../../corpus/catalog-a2ui_0_9.json'), 'utf8');
|
|
122
|
+
catalogJson = JSON.parse(raw);
|
|
30
123
|
} else {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
124
|
+
const resp = await fetch(new URL('../../../corpus/catalog-a2ui_0_9.json', import.meta.url));
|
|
125
|
+
catalogJson = resp.ok ? await resp.json() : {};
|
|
126
|
+
}
|
|
127
|
+
// Adapt the v0.9 catalog (JSON Schema with `components: {Name: {x-adiaui: {...}}}`)
|
|
128
|
+
// into the legacy {Name: {aliases, ...}} shape getComponentCatalog consumers
|
|
129
|
+
// expect. Aliases live at `x-adiaui.synonyms.tags`.
|
|
130
|
+
const comps = catalogJson?.components || {};
|
|
131
|
+
_componentCatalog = {};
|
|
132
|
+
for (const [name, def] of Object.entries(comps)) {
|
|
133
|
+
const ext = def?.['x-adiaui'] || {};
|
|
134
|
+
const syns = (ext.synonyms && typeof ext.synonyms === 'object') ? ext.synonyms : null;
|
|
135
|
+
_componentCatalog[name] = {
|
|
136
|
+
aliases: Array.isArray(syns?.tags) ? syns.tags : [],
|
|
137
|
+
};
|
|
35
138
|
}
|
|
36
139
|
} catch {
|
|
37
140
|
_componentCatalog = {};
|
|
@@ -160,8 +263,13 @@ KEY RULES:
|
|
|
160
263
|
return Array.isArray(a) && a.length ? ` (also: ${a.join(', ')})` : '';
|
|
161
264
|
};
|
|
162
265
|
for (const typeName of relevantTypes) {
|
|
163
|
-
//
|
|
164
|
-
|
|
266
|
+
// §66 (v0.4.6): .a2ui.json sidecar is the canonical source of truth.
|
|
267
|
+
// The raw v0.9 shape uses `properties` (JSON Schema); adapt to the
|
|
268
|
+
// legacy `props` shape this prompt builder was originally written
|
|
269
|
+
// for. Falls back to the (soon-retired) `_components.json` only
|
|
270
|
+
// when no sidecar exists.
|
|
271
|
+
const v09Raw = getComponentData(typeName);
|
|
272
|
+
const a2uiData = v09Raw ? adaptV09Component(v09Raw) : null;
|
|
165
273
|
if (a2uiData) {
|
|
166
274
|
const props = a2uiData.props ? Object.entries(a2uiData.props).map(([k, v]) => {
|
|
167
275
|
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 ? '
|
|
166
|
+
source: bestMatch ? 'composition-library' : 'fallback',
|
|
167
167
|
confidence: bestMatch ? 0.95 : 0.4,
|
|
168
168
|
});
|
|
169
169
|
|
|
@@ -1,43 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Composition Library — zettel-style loader for A2UI
|
|
2
|
+
* Composition Library — zettel-style loader for harvested A2UI chunks.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Reads `corpus/chunks/<name>.json` whenever the chunk carries both
|
|
5
|
+
* `metadata` (from `data-chunk-*` source-HTML attrs per §40) AND
|
|
6
|
+
* `template` (from the harvester's transpileHTML pass per §41). Chunks
|
|
7
|
+
* are normalized to composition shape by hoisting `metadata.*` to
|
|
8
|
+
* top level so consumers can search them uniformly.
|
|
5
9
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
10
|
+
* The legacy `corpus/compositions/<domain>/<name>.json` glob was
|
|
11
|
+
* retired in v0.4.7 (§65). The harvested-chunks substrate (~30
|
|
12
|
+
* annotated chunks at landing time) is the canonical retrieval
|
|
13
|
+
* surface; per the project's "source-of-truth = shipped /site/"
|
|
14
|
+
* principle, anything not in a shipped surface should fall through
|
|
15
|
+
* to LLM generation rather than be retrieval-matched from a curated
|
|
16
|
+
* JSON. The 199 MODE-C-MAYBE + 9 KEEP-AS-CHUNK + 3 DELETE
|
|
17
|
+
* composition/pattern files all retired alongside the dir.
|
|
8
18
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* source-HTML attrs per §40) AND `template` (from the harvester's
|
|
12
|
-
* transpileHTML pass per §41). Such chunks are normalized to
|
|
13
|
-
* composition shape by hoisting `metadata.*` to top level, so they
|
|
14
|
-
* compete with hand-authored compositions in the same search.
|
|
15
|
-
*
|
|
16
|
-
* Records from chunks carry `_kind: 'annotated-chunk'` and the
|
|
17
|
-
* source-of-truth path so consumers can distinguish if they care; most
|
|
18
|
-
* shouldn't.
|
|
19
|
+
* Records carry `_kind: 'annotated-chunk'` and the source-of-truth
|
|
20
|
+
* path so consumers can distinguish if they care; most shouldn't.
|
|
19
21
|
*
|
|
20
22
|
* Renamed from fragment-library.js in §38. Fragments retired §37.
|
|
21
|
-
* Annotated-chunk loading added §41.
|
|
23
|
+
* Annotated-chunk loading added §41. Compositions-glob retired §65.
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
import
|
|
26
|
-
import
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
26
|
+
// §72 follow-up (v0.4.7): dual-mode loader so this module is safe to
|
|
27
|
+
// statically import from browser entrypoints. Previously the top-level
|
|
28
|
+
// `import 'node:fs'` poisoned the browser bundle the moment any
|
|
29
|
+
// app/playground reached `core/reference.js` → composition-library.
|
|
30
|
+
// Pattern mirrors `retrieval/component-catalog.js`.
|
|
31
|
+
const IS_NODE =
|
|
32
|
+
typeof process !== 'undefined' &&
|
|
33
|
+
typeof process.versions?.node === 'string';
|
|
31
34
|
|
|
32
35
|
/** @type {Map<string, object>} */
|
|
33
36
|
const compositions = new Map();
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
// Vite resolves this at build time; at runtime in Node the variable is unused.
|
|
39
|
+
let _globChunkModules = null;
|
|
40
|
+
if (!IS_NODE) {
|
|
41
|
+
try {
|
|
42
|
+
_globChunkModules = import.meta.glob('../../../corpus/chunks/*.json', {
|
|
43
|
+
query: '?raw',
|
|
44
|
+
import: 'default',
|
|
45
|
+
eager: false,
|
|
46
|
+
});
|
|
47
|
+
} catch {
|
|
48
|
+
// Not in a Vite context — no chunk data in this realm.
|
|
41
49
|
}
|
|
42
50
|
}
|
|
43
51
|
|
|
@@ -62,48 +70,99 @@ function chunkToComposition(chunkDoc, sourcePath) {
|
|
|
62
70
|
};
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Track whether the eager top-level load has run. Multiple `loadAll`
|
|
75
|
+
* call sites (mcp/server.js bootstrap + any other consumer) re-clear
|
|
76
|
+
* + re-build, which is wasted work but not incorrect.
|
|
77
|
+
*/
|
|
78
|
+
let _autoLoaded = false;
|
|
79
|
+
let _loadStats = { compositionCount: 0, handAuthoredCount: 0, annotatedChunkCount: 0 };
|
|
67
80
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Filter + register an annotated chunk. Returns 1 if it qualified
|
|
83
|
+
* (had `metadata` + non-empty `template` + domain/keywords), else 0.
|
|
84
|
+
*/
|
|
85
|
+
function registerChunk(doc, sourcePath) {
|
|
86
|
+
if (!doc?.metadata) return 0;
|
|
87
|
+
if (!doc.template || !Array.isArray(doc.template) || doc.template.length === 0) return 0;
|
|
88
|
+
const meta = doc.metadata;
|
|
89
|
+
if (!meta.domain && !(meta.keywords && meta.keywords.length > 0)) return 0;
|
|
90
|
+
compositions.set(doc.name, chunkToComposition(doc, sourcePath));
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function _loadAllNode() {
|
|
95
|
+
const fs = await import(/* @vite-ignore */ 'node:fs');
|
|
96
|
+
const path = await import(/* @vite-ignore */ 'node:path');
|
|
97
|
+
const url = await import(/* @vite-ignore */ 'node:url');
|
|
98
|
+
|
|
99
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
100
|
+
const CHUNKS_ROOT = path.resolve(__dirname, '../../../corpus/chunks');
|
|
101
|
+
|
|
102
|
+
function walk(dir, cb) {
|
|
103
|
+
if (!fs.existsSync(dir)) return;
|
|
104
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
105
|
+
const p = path.join(dir, entry.name);
|
|
106
|
+
if (entry.isDirectory()) walk(p, cb);
|
|
107
|
+
else if (entry.name.endsWith('.json') && !entry.name.startsWith('_')) cb(p);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
75
110
|
|
|
76
|
-
// 2. Annotated chunks (§41). Filter: must have metadata.domain (or
|
|
77
|
-
// metadata.keywords) AND a template field — otherwise the harvester
|
|
78
|
-
// doesn't have enough to treat the chunk as a retrieval candidate.
|
|
79
111
|
let annotatedChunkCount = 0;
|
|
80
112
|
walk(CHUNKS_ROOT, (p) => {
|
|
81
113
|
const doc = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
82
|
-
|
|
83
|
-
if (!doc.template || !Array.isArray(doc.template) || doc.template.length === 0) return;
|
|
84
|
-
const meta = doc.metadata;
|
|
85
|
-
if (!meta.domain && !(meta.keywords && meta.keywords.length > 0)) return;
|
|
86
|
-
|
|
87
|
-
// Chunk name MUST NOT collide with a hand-authored composition.
|
|
88
|
-
// Hand-authored wins; warn so authors can reconcile (rename one or
|
|
89
|
-
// delete the composition once the chunk supersedes it).
|
|
90
|
-
if (compositions.has(doc.name)) {
|
|
91
|
-
console.warn(
|
|
92
|
-
`[composition-library] name collision: annotated chunk "${doc.name}" ` +
|
|
93
|
-
`shadowed by hand-authored composition. Annotated chunk ignored — ` +
|
|
94
|
-
`rename one or retire the composition.`
|
|
95
|
-
);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
compositions.set(doc.name, chunkToComposition(doc, p));
|
|
99
|
-
annotatedChunkCount++;
|
|
114
|
+
annotatedChunkCount += registerChunk(doc, p);
|
|
100
115
|
});
|
|
116
|
+
return annotatedChunkCount;
|
|
117
|
+
}
|
|
101
118
|
|
|
102
|
-
|
|
119
|
+
async function _loadAllBrowser() {
|
|
120
|
+
if (!_globChunkModules) return 0;
|
|
121
|
+
let annotatedChunkCount = 0;
|
|
122
|
+
for (const [globPath, loader] of Object.entries(_globChunkModules)) {
|
|
123
|
+
try {
|
|
124
|
+
const raw = await loader();
|
|
125
|
+
const doc = JSON.parse(raw);
|
|
126
|
+
annotatedChunkCount += registerChunk(doc, globPath);
|
|
127
|
+
} catch {
|
|
128
|
+
// skip malformed chunk JSON
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return annotatedChunkCount;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Load all annotated chunks into the in-memory map. Returns a stats
|
|
136
|
+
* snapshot. In Node, runs synchronously via `fs.readFileSync`; in the
|
|
137
|
+
* browser, dispatches the Vite glob loaders in parallel.
|
|
138
|
+
*/
|
|
139
|
+
export async function loadAll() {
|
|
140
|
+
compositions.clear();
|
|
141
|
+
const annotatedChunkCount = IS_NODE ? await _loadAllNode() : await _loadAllBrowser();
|
|
142
|
+
_autoLoaded = true;
|
|
143
|
+
_loadStats = {
|
|
103
144
|
compositionCount: compositions.size,
|
|
104
|
-
handAuthoredCount:
|
|
145
|
+
handAuthoredCount: 0,
|
|
105
146
|
annotatedChunkCount,
|
|
106
147
|
};
|
|
148
|
+
return _loadStats;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Eager top-level load so synchronous getters (getComposition,
|
|
152
|
+
// getAllCompositions, searchAll) work for consumers that don't call
|
|
153
|
+
// loadAll themselves — test harnesses, smoke scripts, and the
|
|
154
|
+
// retrieval-side helpers migrated off pattern-library in §64.
|
|
155
|
+
//
|
|
156
|
+
// In Node we top-level-await the load so the map is populated before
|
|
157
|
+
// any importer reaches the synchronous getters. In the browser the
|
|
158
|
+
// auto-load is fire-and-forget (the map fills as chunk JSONs come back
|
|
159
|
+
// from Vite's glob loaders); callers that need the map populated MUST
|
|
160
|
+
// `await loadAll()` themselves. Documented in §72-follow-up.
|
|
161
|
+
if (IS_NODE && !_autoLoaded) {
|
|
162
|
+
await loadAll();
|
|
163
|
+
} else if (!IS_NODE && !_autoLoaded) {
|
|
164
|
+
// Fire-and-forget; do not block module import on the network round-trips.
|
|
165
|
+
loadAll().catch(() => { /* swallow; browsers that don't need this path won't trip */ });
|
|
107
166
|
}
|
|
108
167
|
|
|
109
168
|
// Back-compat shims removed in §38 — fragment-library.js → composition-library.js
|
package/core/pattern-export.js
DELETED
|
@@ -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
|
-
}
|