@adia-ai/a2ui-compose 0.4.6 → 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 CHANGED
@@ -12,6 +12,14 @@ 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
+
15
23
  ## [0.4.6] - 2026-05-12
16
24
 
17
25
  ### Changed — `core/` retirement follow-through (§64, v0.4.6)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/a2ui-compose",
3
- "version": "0.4.6",
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": {
@@ -98,24 +98,43 @@ function adaptV09Component(a2uiData) {
98
98
  };
99
99
  }
100
100
 
101
- // Component prop catalog — loaded lazily for prompt injection
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.
102
110
  let _componentCatalog = null;
103
111
  async function getComponentCatalog() {
104
112
  if (_componentCatalog) return _componentCatalog;
105
113
  try {
106
114
  const IS_NODE = typeof process !== 'undefined' && process.versions?.node;
115
+ let catalogJson;
107
116
  if (IS_NODE) {
108
117
  const fs = await import(/* @vite-ignore */ 'node:fs/promises');
109
118
  const path = await import(/* @vite-ignore */ 'node:path');
110
119
  const url = await import(/* @vite-ignore */ 'node:url');
111
120
  const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
112
- const raw = await fs.readFile(path.join(__dirname, '../../../corpus/patterns/_components.json'), 'utf8');
113
- _componentCatalog = JSON.parse(raw);
121
+ const raw = await fs.readFile(path.join(__dirname, '../../../corpus/catalog-a2ui_0_9.json'), 'utf8');
122
+ catalogJson = JSON.parse(raw);
114
123
  } else {
115
- // Browser: use fetch (works in all Vite modes)
116
- const resp = await fetch(new URL('../../../corpus/patterns/_components.json', import.meta.url));
117
- if (resp.ok) _componentCatalog = await resp.json();
118
- else _componentCatalog = {};
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
+ };
119
138
  }
120
139
  } catch {
121
140
  _componentCatalog = {};
@@ -1,43 +1,51 @@
1
1
  /**
2
- * Composition Library — zettel-style loader for A2UI compositions.
2
+ * Composition Library — zettel-style loader for harvested A2UI chunks.
3
3
  *
4
- * Loads two kinds of records into a single in-memory map:
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
- * 1. `corpus/compositions/<domain>/<name>.json` hand-authored A2UI
7
- * templates. The legacy path.
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
- * 2. `corpus/chunks/<name>.json` harvested from production HTML
10
- * WHEN the chunk carries both `metadata` (from `data-chunk-*`
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
- import fs from 'node:fs';
25
- import path from 'node:path';
26
- import url from 'node:url';
27
-
28
- const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
29
- const COMPOSITIONS_ROOT = path.resolve(__dirname, '../../../corpus/compositions');
30
- const CHUNKS_ROOT = path.resolve(__dirname, '../../../corpus/chunks');
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
- function walk(dir, cb) {
36
- if (!fs.existsSync(dir)) return;
37
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
38
- const p = path.join(dir, entry.name);
39
- if (entry.isDirectory()) walk(p, cb);
40
- else if (entry.name.endsWith('.json') && !entry.name.startsWith('_')) cb(p);
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
 
@@ -68,57 +76,94 @@ function chunkToComposition(chunkDoc, sourcePath) {
68
76
  * + re-build, which is wasted work but not incorrect.
69
77
  */
70
78
  let _autoLoaded = false;
79
+ let _loadStats = { compositionCount: 0, handAuthoredCount: 0, annotatedChunkCount: 0 };
71
80
 
72
- export function loadAll() {
73
- compositions.clear();
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
+ }
74
93
 
75
- // 1. Hand-authored compositions.
76
- walk(COMPOSITIONS_ROOT, (p) => {
77
- const doc = JSON.parse(fs.readFileSync(p, 'utf8'));
78
- if (doc.kind !== 'composition') return;
79
- doc._sourceFile = p;
80
- compositions.set(doc.name, doc);
81
- });
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
+ }
82
110
 
83
- // 2. Annotated chunks (§41). Filter: must have metadata.domain (or
84
- // metadata.keywords) AND a template field — otherwise the harvester
85
- // doesn't have enough to treat the chunk as a retrieval candidate.
86
111
  let annotatedChunkCount = 0;
87
112
  walk(CHUNKS_ROOT, (p) => {
88
113
  const doc = JSON.parse(fs.readFileSync(p, 'utf8'));
89
- if (!doc.metadata) return;
90
- if (!doc.template || !Array.isArray(doc.template) || doc.template.length === 0) return;
91
- const meta = doc.metadata;
92
- if (!meta.domain && !(meta.keywords && meta.keywords.length > 0)) return;
93
-
94
- // Chunk name MUST NOT collide with a hand-authored composition.
95
- // Hand-authored wins; warn so authors can reconcile (rename one or
96
- // delete the composition once the chunk supersedes it).
97
- if (compositions.has(doc.name)) {
98
- console.warn(
99
- `[composition-library] name collision: annotated chunk "${doc.name}" ` +
100
- `shadowed by hand-authored composition. Annotated chunk ignored — ` +
101
- `rename one or retire the composition.`
102
- );
103
- return;
104
- }
105
- compositions.set(doc.name, chunkToComposition(doc, p));
106
- annotatedChunkCount++;
114
+ annotatedChunkCount += registerChunk(doc, p);
107
115
  });
116
+ return annotatedChunkCount;
117
+ }
118
+
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
+ }
108
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();
109
142
  _autoLoaded = true;
110
- return {
143
+ _loadStats = {
111
144
  compositionCount: compositions.size,
112
- handAuthoredCount: compositions.size - annotatedChunkCount,
145
+ handAuthoredCount: 0,
113
146
  annotatedChunkCount,
114
147
  };
148
+ return _loadStats;
115
149
  }
116
150
 
117
151
  // Eager top-level load so synchronous getters (getComposition,
118
152
  // getAllCompositions, searchAll) work for consumers that don't call
119
153
  // loadAll themselves — test harnesses, smoke scripts, and the
120
154
  // retrieval-side helpers migrated off pattern-library in §64.
121
- if (!_autoLoaded) loadAll();
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 */ });
166
+ }
122
167
 
123
168
  // Back-compat shims removed in §38 — fragment-library.js → composition-library.js
124
169
  // rename. The retired `getFragment` / `getAllFragments` exports had zero