@adia-ai/a2ui-mcp 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.
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * multi-turn-test.mjs — End-to-end multi-turn generation test.
5
+ *
6
+ * Runs a sequence of related prompts through Pro mode, evaluating:
7
+ * - Output quality (component count, types, validation score)
8
+ * - Context continuity (prior components preserved across turns)
9
+ * - Intent alignment (does each turn actually do what was asked)
10
+ * - Diff efficiency (how many components changed vs carried forward)
11
+ *
12
+ * Usage: node packages/a2ui/mcp/scripts/multi-turn-test.mjs
13
+ */
14
+
15
+ import '../../../../scripts/load-env.mjs';
16
+ import { generateUI } from '../../compose/engine/generator.js';
17
+ import { validateSchema } from '../../validator/validator.js';
18
+
19
+ // ── Test scenarios ──────────────────────────────────────────────────
20
+
21
+ const SCENARIOS = [
22
+ {
23
+ name: 'Kanban Board Build',
24
+ turns: [
25
+ {
26
+ intent: 'Create a kanban board with 3 columns (To Do, In Progress, Done). Each column has 3-4 synthetic task cards. Each task card should have a title, a category tag (Design, Engineering, Marketing, QA), a priority badge (High, Medium, Low), and an assignee avatar. Include a board header with the project name "Project Aurora" and a button to add new tasks.',
27
+ expect: {
28
+ minComponents: 30,
29
+ requiredTypes: ['Grid', 'Column', 'Card', 'Badge', 'Avatar', 'Button', 'Text'],
30
+ requiredContent: ['To Do', 'In Progress', 'Done', 'Aurora'],
31
+ },
32
+ },
33
+ {
34
+ intent: 'Add a toolbar above the board with: a search input to filter tasks by title, a select dropdown to filter by category (All, Design, Engineering, Marketing, QA), and a select dropdown to sort by (Priority, Date Created, Assignee). Keep all existing board content.',
35
+ expect: {
36
+ minComponents: 35,
37
+ requiredTypes: ['Input', 'Select'],
38
+ requiredContent: ['search', 'filter', 'sort'],
39
+ preserveFromPrior: ['Grid', 'Badge', 'Avatar'],
40
+ },
41
+ },
42
+ {
43
+ intent: 'Add a toggle switch in the toolbar labeled "Compact View". When compact mode is conceptually on, the task cards should show only the title text and category badge — no avatar, no priority badge, no description. Show the board in compact mode as an example of what it would look like.',
44
+ expect: {
45
+ minComponents: 25,
46
+ requiredTypes: ['Switch', 'Toggle'],
47
+ requiredContent: ['Compact'],
48
+ preserveFromPrior: ['Input', 'Select'],
49
+ },
50
+ },
51
+ ],
52
+ },
53
+ ];
54
+
55
+ // ── Helpers ──────────────────────────────────────────────────────────
56
+
57
+ function extractComponents(result) {
58
+ return result.messages?.flatMap(m => m.components || []) || [];
59
+ }
60
+
61
+ function getTypes(components) {
62
+ return [...new Set(components.map(c => c.component).filter(Boolean))];
63
+ }
64
+
65
+ function getTextContent(components) {
66
+ return components
67
+ .filter(c => c.textContent || c.text || c.label || c.placeholder)
68
+ .map(c => c.textContent || c.text || c.label || c.placeholder)
69
+ .join(' ')
70
+ .toLowerCase();
71
+ }
72
+
73
+ function checkExpectations(components, expect, priorTypes) {
74
+ const issues = [];
75
+ const types = getTypes(components);
76
+ const text = getTextContent(components);
77
+
78
+ // Min components
79
+ if (expect.minComponents && components.length < expect.minComponents) {
80
+ issues.push(`TOO FEW: ${components.length} components (expected ${expect.minComponents}+)`);
81
+ }
82
+
83
+ // Required types
84
+ if (expect.requiredTypes) {
85
+ for (const t of expect.requiredTypes) {
86
+ // Allow flexible matching — Switch OR Toggle, etc
87
+ const alternatives = {
88
+ 'Switch': ['Switch', 'Toggle'],
89
+ 'Toggle': ['Switch', 'Toggle'],
90
+ 'Input': ['Input', 'Search'],
91
+ };
92
+ const alts = alternatives[t] || [t];
93
+ if (!alts.some(a => types.includes(a))) {
94
+ issues.push(`MISSING TYPE: ${t} (have: ${types.join(', ')})`);
95
+ }
96
+ }
97
+ }
98
+
99
+ // Required content
100
+ if (expect.requiredContent) {
101
+ for (const c of expect.requiredContent) {
102
+ if (!text.includes(c.toLowerCase())) {
103
+ issues.push(`MISSING CONTENT: "${c}" not found in text`);
104
+ }
105
+ }
106
+ }
107
+
108
+ // Continuity — types from prior turn still present
109
+ if (expect.preserveFromPrior && priorTypes) {
110
+ for (const t of expect.preserveFromPrior) {
111
+ if (!types.includes(t)) {
112
+ issues.push(`CONTINUITY BREAK: ${t} was in prior turn but missing now`);
113
+ }
114
+ }
115
+ }
116
+
117
+ return issues;
118
+ }
119
+
120
+ // ── Runner ──────────────────────────────────────────────────────────
121
+
122
+ async function runScenario(scenario) {
123
+ console.log(`\n${'═'.repeat(70)}`);
124
+ console.log(` SCENARIO: ${scenario.name}`);
125
+ console.log(`${'═'.repeat(70)}\n`);
126
+
127
+ let executionId = undefined;
128
+ let priorTypes = null;
129
+ let priorComponentCount = 0;
130
+ const turnResults = [];
131
+
132
+ for (let i = 0; i < scenario.turns.length; i++) {
133
+ const turn = scenario.turns[i];
134
+ const turnNum = i + 1;
135
+
136
+ console.log(`── Turn ${turnNum}/${scenario.turns.length} ${'─'.repeat(50)}`);
137
+ console.log(` Intent: ${turn.intent.slice(0, 120)}...`);
138
+ console.log(` ExecutionId: ${executionId || '(new)'}`);
139
+ console.log('');
140
+
141
+ const startTime = Date.now();
142
+ let result;
143
+ try {
144
+ result = await generateUI({
145
+ intent: turn.intent,
146
+ mode: 'pro',
147
+ executionId,
148
+ });
149
+ } catch (err) {
150
+ console.log(` ✗ GENERATION FAILED: ${err.message}`);
151
+ turnResults.push({ turn: turnNum, error: err.message });
152
+ continue;
153
+ }
154
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
155
+
156
+ const components = extractComponents(result);
157
+ const types = getTypes(components);
158
+ const validation = validateSchema(result.messages, { intent: turn.intent });
159
+ const text = getTextContent(components);
160
+
161
+ // Store executionId for next turn
162
+ if (!executionId) executionId = result.executionId;
163
+
164
+ // Check expectations
165
+ const issues = checkExpectations(components, turn.expect, priorTypes);
166
+
167
+ // Diff analysis vs prior turn
168
+ let diffReport = '';
169
+ if (priorTypes) {
170
+ const newTypes = types.filter(t => !priorTypes.includes(t));
171
+ const lostTypes = priorTypes.filter(t => !types.includes(t));
172
+ const delta = components.length - priorComponentCount;
173
+ diffReport = ` Delta: ${delta >= 0 ? '+' : ''}${delta} components`;
174
+ if (newTypes.length) diffReport += ` New types: ${newTypes.join(', ')}`;
175
+ if (lostTypes.length) diffReport += ` Lost types: ${lostTypes.join(', ')}`;
176
+ }
177
+
178
+ // Print results
179
+ console.log(` Components: ${components.length} | Score: ${validation.score}/100 | Time: ${elapsed}s`);
180
+ console.log(` Types (${types.length}): ${types.join(', ')}`);
181
+ if (diffReport) console.log(diffReport);
182
+
183
+ // Text content sample
184
+ const contentWords = text.split(/\s+/).slice(0, 30).join(' ');
185
+ console.log(` Content sample: ${contentWords}...`);
186
+
187
+ // Validation details
188
+ const failedChecks = validation.checks?.filter(c => !c.passed) || [];
189
+ if (failedChecks.length) {
190
+ console.log(` Failed checks: ${failedChecks.map(c => c.name + ': ' + (c.detail || '').slice(0, 60)).join('; ')}`);
191
+ }
192
+
193
+ // Expectations
194
+ if (issues.length === 0) {
195
+ console.log(` ✓ All expectations met`);
196
+ } else {
197
+ for (const issue of issues) {
198
+ console.log(` ✗ ${issue}`);
199
+ }
200
+ }
201
+ console.log('');
202
+
203
+ // Store for next turn comparison
204
+ priorTypes = types;
205
+ priorComponentCount = components.length;
206
+ turnResults.push({
207
+ turn: turnNum,
208
+ components: components.length,
209
+ types: types.length,
210
+ score: validation.score,
211
+ elapsed: parseFloat(elapsed),
212
+ issues: issues.length,
213
+ issueDetails: issues,
214
+ });
215
+ }
216
+
217
+ // Summary
218
+ console.log(`── Summary ${'─'.repeat(55)}`);
219
+ const totalIssues = turnResults.reduce((s, t) => s + (t.issues || 0), 0);
220
+ const avgScore = Math.round(turnResults.reduce((s, t) => s + (t.score || 0), 0) / turnResults.length);
221
+ const totalTime = turnResults.reduce((s, t) => s + (t.elapsed || 0), 0).toFixed(1);
222
+
223
+ console.log(` Turns: ${turnResults.length} | Avg score: ${avgScore} | Total time: ${totalTime}s`);
224
+ console.log(` Issues: ${totalIssues}`);
225
+ for (const t of turnResults) {
226
+ const icon = t.error ? '✗' : t.issues === 0 ? '✓' : '△';
227
+ console.log(` ${icon} Turn ${t.turn}: ${t.components || 0} comps, score ${t.score || 0}, ${t.elapsed || 0}s${t.issues ? ` (${t.issues} issues)` : ''}`);
228
+ }
229
+ console.log('');
230
+
231
+ return { scenario: scenario.name, turns: turnResults, totalIssues, avgScore };
232
+ }
233
+
234
+ // ── Main ────────────────────────────────────────────────────────────
235
+
236
+ const results = [];
237
+ for (const scenario of SCENARIOS) {
238
+ const result = await runScenario(scenario);
239
+ results.push(result);
240
+ }
241
+
242
+ console.log(`${'═'.repeat(70)}`);
243
+ console.log(' FINAL VERDICT');
244
+ console.log(`${'═'.repeat(70)}`);
245
+ const totalIssues = results.reduce((s, r) => s + r.totalIssues, 0);
246
+ console.log(` Scenarios: ${results.length} | Total issues: ${totalIssues}`);
247
+ process.exit(totalIssues > 0 ? 1 : 0);
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Smoke test for the Phase 5 engine registry dispatcher.
4
+ * Verifies:
5
+ * - registry.pick() returns different functions for different engines
6
+ * - generateUI({ engine: 'monolithic', mode: 'instant' }) works
7
+ * - generateUI({ engine: 'zettel' }) works
8
+ * - shape invariants hold on both paths
9
+ */
10
+ import '../../../../scripts/load-env.mjs';
11
+ import { pick, listEngines, ENGINES } from '../../compose/engines/registry.js';
12
+ import { generateUI } from '../../compose/engine/generator.js';
13
+
14
+ console.log('[smoke] engines registered:', listEngines().join(', '));
15
+
16
+ // pick() sanity
17
+ const monoInstant = pick({ engine: 'monolithic', mode: 'instant' });
18
+ const monoPro = pick({ engine: 'monolithic', mode: 'pro' });
19
+ const zettel = pick({ engine: 'zettel' });
20
+ console.log('[smoke] pick monolithic/instant:', monoInstant === ENGINES['monolithic-instant'] ? 'ok' : 'FAIL');
21
+ console.log('[smoke] pick monolithic/pro: ', monoPro === ENGINES['monolithic-pro'] ? 'ok' : 'FAIL');
22
+ console.log('[smoke] pick zettel: ', zettel === ENGINES.zettel ? 'ok' : 'FAIL');
23
+ console.log('[smoke] pick unknown → fallback:', pick({ engine: 'xxx', mode: 'xxx' }) === ENGINES['monolithic-instant'] ? 'ok' : 'FAIL');
24
+
25
+ const intent = 'login form with email and password';
26
+
27
+ // Monolithic instant
28
+ const t1 = Date.now();
29
+ const r1 = await generateUI({ intent, engine: 'monolithic', mode: 'instant' });
30
+ console.log(`\n[mono/instant] ${Date.now() - t1}ms msgs=${r1.messages?.length} valid=${r1.validation?.valid} score=${r1.validation?.score}`);
31
+
32
+ // Zettel
33
+ const t2 = Date.now();
34
+ const r2 = await generateUI({ intent, engine: 'zettel' });
35
+ console.log(`[zettel] ${Date.now() - t2}ms msgs=${r2.messages?.length} valid=${r2.validation?.valid} score=${r2.validation?.score} strategy=${r2.strategy} engine=${r2.engine}`);
36
+
37
+ // Shape invariants
38
+ const ok =
39
+ Array.isArray(r1.messages) && r1.executionId && r1.validation &&
40
+ Array.isArray(r2.messages) && r2.validation;
41
+ console.log(`\n[smoke] shape invariants: ${ok ? 'ok' : 'FAIL'}`);
42
+
43
+ if (!ok) process.exit(1);
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP smoke test — exercises the merged a2ui-mcp server via a real MCP client.
4
+ * Verifies: boot, tool listing, generate_ui with engine=monolithic, generate_ui with engine=zettel, get_graph.
5
+ */
6
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
7
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { dirname, join } from 'node:path';
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const serverPath = join(__dirname, '..', 'server.js');
13
+
14
+ const transport = new StdioClientTransport({
15
+ command: 'node',
16
+ args: [serverPath],
17
+ stderr: 'inherit',
18
+ });
19
+
20
+ const client = new Client({ name: 'smoke-merged', version: '0.1.0' }, { capabilities: {} });
21
+ await client.connect(transport);
22
+
23
+ const tools = await client.listTools();
24
+ const toolNames = tools.tools.map((t) => t.name).sort();
25
+ console.log(`TOOLS (${toolNames.length}):`, toolNames.join(', '));
26
+
27
+ // 1. Monolithic engine
28
+ const mono = await client.callTool({
29
+ name: 'generate_ui',
30
+ arguments: { intent: 'login form with email and password', engine: 'monolithic', mode: 'instant' },
31
+ });
32
+ const monoData = JSON.parse(mono.content[0].text);
33
+ console.log(`MONOLITHIC: engine=${monoData.engine} msgs=${monoData.messages?.length} score=${monoData.validation?.score}`);
34
+
35
+ // 2. Zettel engine
36
+ const zettel = await client.callTool({
37
+ name: 'generate_ui',
38
+ arguments: { intent: 'login form with email and password', engine: 'zettel', mode: 'instant' },
39
+ });
40
+ const zettelData = JSON.parse(zettel.content[0].text);
41
+ console.log(`ZETTEL: engine=${zettelData.engine} strategy=${zettelData.strategy} msgs=${zettelData.messages?.length} score=${zettelData.validation?.score} frags=${zettelData.fragments_used?.length}`);
42
+
43
+ // 3. Zettel-only tool
44
+ const graph = await client.callTool({ name: 'get_graph', arguments: {} });
45
+ const graphData = JSON.parse(graph.content[0].text);
46
+ console.log(`GRAPH: fragments=${Object.keys(graphData.fragments || {}).length} compositions=${Object.keys(graphData.compositions || {}).length}`);
47
+
48
+ await client.close();
49
+ console.log('OK');
50
+ process.exit(0);
@@ -0,0 +1,51 @@
1
+ // Smoke test for OD-5 plugin engine registry.
2
+ import { registerEngine, unregisterEngine, pick, listEngines, ENGINES } from '../../compose/engines/registry.js';
3
+
4
+ let pass = 0, fail = 0;
5
+ const t = (label, ok, detail = '') => {
6
+ if (ok) { console.log(` ✓ ${label}`); pass++; }
7
+ else { console.log(` ✗ ${label} ${detail}`); fail++; }
8
+ };
9
+
10
+ // Baseline
11
+ t('four built-ins registered', listEngines().length === 4);
12
+
13
+ // Happy path
14
+ let customCalled = null;
15
+ registerEngine('my-test', async (ctx) => {
16
+ customCalled = ctx;
17
+ return { executionId: ctx.executionId, messages: [], validation: { score: 100 }, strategy: 'test', engine: 'my-test' };
18
+ });
19
+ t('custom engine registered', listEngines().includes('my-test'));
20
+ t('pick returns custom engine', pick({ engine: 'my-test' }) === ENGINES['my-test']);
21
+
22
+ const res = await ENGINES['my-test']({ intent: 'hi', executionId: 'e1', storeId: null, llmAdapter: null });
23
+ t('custom engine invocable', res.validation.score === 100 && res.strategy === 'test');
24
+
25
+ // Reserved guard
26
+ try { registerEngine('zettel', () => {}); t('reserved names rejected', false, 'zettel should have thrown'); }
27
+ catch (e) { t('reserved names rejected (zettel)', /reserved/.test(e.message)); }
28
+
29
+ try { registerEngine('monolithic-instant', () => {}); t('reserved rejected (monolithic-instant)', false); }
30
+ catch (e) { t('reserved rejected (monolithic-instant)', /reserved/.test(e.message)); }
31
+
32
+ // Type guards
33
+ try { registerEngine('', () => {}); t('empty name rejected', false); }
34
+ catch (e) { t('empty name rejected', /non-empty/.test(e.message)); }
35
+
36
+ try { registerEngine('ok-name', null); t('non-function rejected', false); }
37
+ catch (e) { t('non-function rejected', /function/.test(e.message)); }
38
+
39
+ // Unregister
40
+ t('unregister custom ok', unregisterEngine('my-test') === true && !listEngines().includes('my-test'));
41
+
42
+ try { unregisterEngine('zettel'); t('unregister reserved rejected', false); }
43
+ catch (e) { t('unregister reserved rejected', /reserved/.test(e.message)); }
44
+
45
+ // Dispatch precedence — re-register + verify priority
46
+ registerEngine('monolithic-alt', async () => ({ engine: 'monolithic-alt' }));
47
+ t('custom with monolithic- prefix works', pick({ engine: 'monolithic-alt' }) === ENGINES['monolithic-alt']);
48
+ unregisterEngine('monolithic-alt');
49
+
50
+ console.log(`\n${pass} passed, ${fail} failed`);
51
+ process.exit(fail ? 1 : 0);
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Pure-logic smoke for <select-ui searchable> filter behavior.
4
+ *
5
+ * Rather than shim full DOM/popover machinery (JSDOM path was 50KB of shims),
6
+ * this test constructs the filter directly by parsing select.js's logic
7
+ * and confirms the substring-match contract against a canned option list.
8
+ *
9
+ * Full browser behavior is verified via `npm run dev` + manual QA;
10
+ * this is the CI-able minimum.
11
+ */
12
+ import { readFileSync } from 'node:fs';
13
+
14
+ const src = readFileSync(new URL('../../../web-components/components/select/select.js', import.meta.url), 'utf8');
15
+
16
+ // Assertions on source-level contract (what shipped) rather than runtime
17
+ const checks = [
18
+ [/searchable:\s*\{\s*type:\s*Boolean/, 'searchable property declared'],
19
+ [/freeText:\s*\{\s*type:\s*Boolean/, 'freeText property declared'],
20
+ [/attribute:\s*'free-text'/, 'free-text attribute wiring'],
21
+ [/#query\s*=\s*''/, '#query state field initialized'],
22
+ [/#applyFilter\(\)/, '#applyFilter() method defined'],
23
+ [/#onSearchInput\s*=/, '#onSearchInput handler defined'],
24
+ [/data-filtered-out/, 'data-filtered-out attribute used for hidden options'],
25
+ [/toLowerCase\(\)/, 'case-insensitive match'],
26
+ [/input\.addEventListener\('input'/, 'input listener wired'],
27
+ [/e\.key\s*===\s*'Escape'[\s\S]{0,200}this\.#query/, 'Escape clears query first'],
28
+ [/this\.freeText\s*&&\s*this\.#query/, 'free-text commit branch on Enter'],
29
+ [/\[role="option"\]:not\(\[aria-disabled\]\):not\(\[data-filtered-out\]\)/, 'focusOption excludes filtered'],
30
+ ];
31
+
32
+ let pass = 0, fail = 0;
33
+ for (const [re, label] of checks) {
34
+ if (re.test(src)) { console.log(` ✓ ${label}`); pass++; }
35
+ else { console.log(` ✗ ${label}`); fail++; }
36
+ }
37
+
38
+ console.log(`\n${pass} passed, ${fail} failed`);
39
+ process.exit(fail === 0 ? 0 : 1);
@@ -0,0 +1,59 @@
1
+ // Direct test of zettel generator with LLM adapter — no HTTP server needed.
2
+ import '../../../../scripts/load-env.mjs';
3
+ import { generateZettel, clearSession, getTurns } from '../../compose/engines/zettel/generator-adapter.js';
4
+
5
+ // Minimal Anthropic adapter inline (mirrors what server.js makes).
6
+ function makeAdapter() {
7
+ const key = process.env.ANTHROPIC_API_KEY;
8
+ if (!key) return null;
9
+ return {
10
+ name: 'anthropic',
11
+ async complete({ messages, systemPrompt }) {
12
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
13
+ method: 'POST',
14
+ headers: { 'content-type': 'application/json', 'x-api-key': key, 'anthropic-version': '2023-06-01' },
15
+ body: JSON.stringify({
16
+ model: 'claude-haiku-4-5-20251001',
17
+ max_tokens: 4096,
18
+ system: systemPrompt,
19
+ messages,
20
+ }),
21
+ });
22
+ if (!res.ok) throw new Error(`Anthropic ${res.status}: ${await res.text()}`);
23
+ const data = await res.json();
24
+ return { content: data.content?.[0]?.text || '' };
25
+ },
26
+ };
27
+ }
28
+
29
+ const llm = makeAdapter();
30
+ console.log('[smoke] llm adapter:', llm ? 'available' : 'missing');
31
+
32
+ async function run(label, args) {
33
+ const t = Date.now();
34
+ const r = await generateZettel(args);
35
+ const ms = Date.now() - t;
36
+ console.log(`\n--- ${label} ---`);
37
+ console.log(` strategy : ${r.strategy}`);
38
+ console.log(` score : ${r.validation?.score ?? '-'}`);
39
+ console.log(` msgs : ${r.messages?.length || 0}`);
40
+ console.log(` composition : ${r.composition || '-'}`);
41
+ console.log(` fragments_used : ${(r.fragments_used || []).join(', ') || '-'}`);
42
+ console.log(` sessionTurns : ${r.sessionTurns || '-'}`);
43
+ console.log(` elapsed : ${ms}ms`);
44
+ if (r.error) console.log(` error : ${r.error}`);
45
+ return r;
46
+ }
47
+
48
+ // Test 1: strong retrieval match (no LLM needed)
49
+ await run('retrieval (login form)', { intent: 'login form', sessionId: 'smoke-1', llmAdapter: llm });
50
+
51
+ // Test 2: novel intent → LLM synthesis (turn 1, fresh)
52
+ clearSession('smoke-2');
53
+ await run('synthesis (recipe timer)', { intent: 'recipe timer with ingredient steps', sessionId: 'smoke-2', llmAdapter: llm });
54
+
55
+ // Test 3: iteration on prior turn
56
+ await run('iteration (add a logo)', { intent: 'add a logo above the heading', sessionId: 'smoke-2', llmAdapter: llm });
57
+
58
+ console.log('\n[smoke] turns recorded for smoke-2:', getTurns('smoke-2').length);
59
+ console.log('[smoke] done');
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ /** Smoke test: load library, resolve login-form, print resolved template. */
3
+ import { loadAll, getAllFragments, getAllCompositions, searchAll, getGraph, getComposition } from './fragment-library.js';
4
+ import { resolveComposition } from './composer.js';
5
+
6
+ const boot = loadAll();
7
+ console.log('boot:', boot);
8
+
9
+ console.log('\n=== Fragments ===');
10
+ for (const f of getAllFragments()) console.log(` - ${f.name} [${f.semantic_role}]`);
11
+
12
+ console.log('\n=== Compositions ===');
13
+ for (const c of getAllCompositions()) console.log(` - ${c.name} (${c.domain})`);
14
+
15
+ console.log('\n=== Search: "login" ===');
16
+ console.log(searchAll('login form password', { limit: 5 }));
17
+
18
+ console.log('\n=== Search: "card header title" ===');
19
+ console.log(searchAll('card header title', { limit: 5 }));
20
+
21
+ console.log('\n=== Resolve login-form ===');
22
+ const comp = getComposition('login-form');
23
+ const resolved = resolveComposition(comp);
24
+ console.log(`resolved ${resolved.length} nodes:`);
25
+ for (const n of resolved) {
26
+ const kids = n.children ? `→[${n.children.join(',')}]` : '';
27
+ const text = n.textContent ? ` "${n.textContent}"` : '';
28
+ const label = n.label ? ` label="${n.label}"` : '';
29
+ console.log(` ${n.id.padEnd(20)} ${n.component}${label}${text} ${kids}`);
30
+ }
31
+
32
+ console.log('\n=== Graph ===');
33
+ const g = getGraph();
34
+ console.log('fragments with usage:');
35
+ for (const f of g.fragments) {
36
+ if (f.used_by.length) console.log(` ${f.name} ← ${f.used_by.map(u => u.name).join(', ')}`);
37
+ }