@adia-ai/a2ui-retrieval 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,50 @@
1
+ # Changelog — @adia-ai/a2ui-retrieval
2
+
3
+ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
4
+ [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [Unreleased]
7
+
8
+ _Nothing yet._
9
+
10
+ ---
11
+
12
+ ## [0.0.1] - 2026-04-24
13
+
14
+ First public release. Extracted from
15
+ [`@adia-ai/a2ui-compose`](../compose/) during the 2026-04-24
16
+ consolidation so non-compose tooling (tests, MCP validator tools,
17
+ CI gates) can depend on retrieval without pulling the full
18
+ generator graph.
19
+
20
+ ### Included
21
+
22
+ - **Pattern library** (`pattern-library.js`) — indexed search over the
23
+ catalog's patterns + fragments; domain-aware ranking; loads patterns
24
+ from `@adia-ai/a2ui-corpus` at runtime.
25
+ - **Intent gate** (`intent-gate.js`) — classifies UI-generating vs
26
+ conversational intents based on domain-keyword signal. Zero-signal
27
+ defaults to conversational (prevents hallucinated UI from
28
+ motivational / abstract text).
29
+ - **Domain router** (`domain-router.js`) — free-form text → catalog
30
+ domain (`forms`, `data`, `agent`, `navigation`, `layout`, etc.).
31
+ - **Anti-pattern detector** (`anti-patterns.js`) — flags invented
32
+ components + bad attribute combos against the registry.
33
+ - **Clarity analyzer** (`clarity.js`) — scores intent specificity;
34
+ drives clarification prompts when signal is too weak.
35
+ - **Context assembly** (`context.js`) — stitches retrieval results
36
+ into prompt context for LLM adapters.
37
+ - **Embedding retriever** (`embedding-retriever.js`) — dense-vector
38
+ pattern retrieval (optional; gated on the presence of embeddings
39
+ in `@adia-ai/a2ui-corpus`).
40
+ - **Prompt analyzer** (`prompt-analyzer.js`) — prompt-side features
41
+ feeding domain + clarity scoring.
42
+ - **Dialog recorder** (`dialog-recorder.js`) — opt-in JSONL capture
43
+ of LLM round-trips for offline replay + tuning (gated on
44
+ `ADIA_LOG_DIALOGS=1`).
45
+ - **Synthetic data** (`synthetic-data.js`) — deterministic
46
+ pattern-match-only generator used by the `instant` engine mode.
47
+
48
+ ### Dependencies
49
+
50
+ - `@adia-ai/a2ui-utils` — A2UI registry + runtime primitives.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # `@adia-ai/a2ui-retrieval`
2
+
3
+ Retrieval layer for the A2UI (Agent-to-UI) protocol — catalog lookup,
4
+ intent classification, domain routing, pattern + anti-pattern matching,
5
+ clarity + context assembly. Consumed by
6
+ [`@adia-ai/a2ui-compose`](../compose/) and any A2UI-protocol tooling
7
+ that needs to reason about user intent against the catalog.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @adia-ai/a2ui-retrieval
13
+ ```
14
+
15
+ ## What's here
16
+
17
+ - **Pattern library** — indexed search over the catalog's patterns and
18
+ fragments; domain-aware ranking.
19
+ - **Intent gate** — classifies a user intent as UI-generating vs
20
+ conversational based on domain-keyword signal.
21
+ - **Domain router** — maps free-form text to catalog domains
22
+ (`forms`, `data`, `agent`, `navigation`, etc.).
23
+ - **Anti-pattern detector** — flags compositions that violate invariants
24
+ (invented components, bad attribute combos).
25
+ - **Clarity analyzer** — scores how specific an intent is; drives
26
+ clarification prompts when the signal is too weak to generate.
27
+ - **Context assembly** — stitches retrieval results into the prompt
28
+ context consumed by the LLM adapters.
29
+
30
+ ## Runtime
31
+
32
+ Framework-agnostic JS. Depends only on
33
+ [`@adia-ai/a2ui-utils`](https://www.npmjs.com/package/@adia-ai/a2ui-utils)
34
+ for the A2UI registry + runtime primitives.
35
+
36
+ ## Links
37
+
38
+ - Repo: [`adiahealth/gen-ui-kit`](https://github.com/adiahealth/gen-ui-kit)
39
+ - Architecture: [`docs/specs/package-architecture.md`](https://github.com/adiahealth/gen-ui-kit/blob/main/docs/specs/package-architecture.md)
40
+ - CHANGELOG: [`CHANGELOG.md`](./CHANGELOG.md)
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Anti-Pattern Registry — known bad patterns from AdiaUI best practices.
3
+ *
4
+ * Each anti-pattern has:
5
+ * - name: machine identifier
6
+ * - description: what the anti-pattern is
7
+ * - check: regex or function to detect it in HTML strings
8
+ * - fix: guidance on how to correct it
9
+ */
10
+
11
+ const antiPatterns = [
12
+ {
13
+ name: 'noBareDivs',
14
+ description: 'All elements should be AdiaUI components, not bare <div> elements.',
15
+ check: /<div[\s>]/i,
16
+ fix: 'Replace <div> with the appropriate AdiaUI layout component: Row, Column, Grid, Card, or Section.',
17
+ },
18
+ {
19
+ name: 'noBareInputs',
20
+ description: 'Form controls should use component wrappers, not native <input>/<select>/<textarea>.',
21
+ check: /<(input|select|textarea)[\s>]/i,
22
+ fix: 'Use input-ui, select-ui, textarea-ui, check-ui, switch-ui, slider-ui, or datetime-ui instead.',
23
+ },
24
+ {
25
+ name: 'cardStructure',
26
+ description: 'Card children must follow the header/section/footer anatomy. Header and footer are direct children of card.',
27
+ check: (html) => {
28
+ // Detect section > header or section > footer (wrong nesting)
29
+ return /<section[^>]*>\s*<header/i.test(html) || /<section[^>]*>\s*<footer/i.test(html);
30
+ },
31
+ fix: 'Header and Footer must be direct children of Card, not nested inside Section. Structure: Card > Header + Section + Footer.',
32
+ },
33
+ {
34
+ name: 'flatAdjacency',
35
+ description: 'AdiaUI uses flat component lists with ID references, not deeply nested component trees.',
36
+ check: (html) => {
37
+ // Heuristic: detect 5+ levels of nesting with AdiaUI components
38
+ const nested = html.match(/<[a-z]+-(?:n|ui)[^>]*>\s*<[a-z]+-(?:n|ui)[^>]*>\s*<[a-z]+-(?:n|ui)[^>]*>\s*<[a-z]+-(?:n|ui)[^>]*>\s*<[a-z]+-(?:n|ui)/i);
39
+ return !!nested;
40
+ },
41
+ fix: 'Use a flat adjacency list with unique IDs and children arrays referencing IDs.',
42
+ },
43
+ {
44
+ name: 'columnWrap',
45
+ description: 'Content inside Section must be wrapped in a Column (col-ui), not placed directly.',
46
+ check: (html) => {
47
+ // Detect section > text-ui or section > p (without col-ui wrapper)
48
+ return /<section[^>]*>\s*<(text-ui|p |p>|h[1-6])/i.test(html);
49
+ },
50
+ fix: 'Wrap section content in <col-ui>: Section > Column > Text/content.',
51
+ },
52
+ {
53
+ name: 'noHardcodedColors',
54
+ description: 'Use design token colors (--a-*) instead of hardcoded hex/rgb/hsl values.',
55
+ check: /(style\s*=\s*"[^"]*(?:color|background)\s*:\s*(?:#[0-9a-f]{3,8}|rgb|hsl))/i,
56
+ fix: 'Use token-based colors via variant attributes or CSS custom properties (--a-canvas-*, --a-accent-*, etc.).',
57
+ },
58
+ {
59
+ name: 'noInlineLayout',
60
+ description: 'Use declarative layout attributes instead of inline display/flex/grid styles.',
61
+ check: /(style\s*=\s*"[^"]*(?:display\s*:|flex|grid-template))/i,
62
+ fix: 'Use Row, Column, Grid components with gap/columns attributes instead of inline CSS layout.',
63
+ },
64
+ {
65
+ name: 'noInventedComponents',
66
+ description: 'Only use components from the AdiaUI registry. Do not invent custom component tags.',
67
+ check: (html) => {
68
+ const tags = html.match(/<([a-z]+-(?:n|ui))\b/gi) || [];
69
+ const known = new Set([
70
+ 'row-ui', 'col-ui', 'list-ui', 'grid-ui', 'text-ui', 'image-ui',
71
+ 'icon-ui', 'divider-ui', 'badge-ui', 'avatar-ui', 'progress-ui',
72
+ 'skeleton-ui', 'input-ui', 'input-ui', 'check-ui', 'switch-ui',
73
+ 'slider-ui', 'select-ui', 'datetime-ui', 'search-ui', 'upload-ui',
74
+ 'button-ui', 'card-ui', 'tabs-ui', 'tab-ui', 'pane-ui', 'modal-ui',
75
+ 'drawer-ui', 'toast-ui', 'popover-ui', 'stream-ui', 'form-container-ui',
76
+ 'table-ui', 'chart-ui', 'embed-ui', 'stack-ui', 'block-ui', 'code-ui',
77
+ 'textarea-ui', 'radio-ui', 'radio-ui', 'tag-ui', 'accordion-ui',
78
+ 'modal-ui', 'alert-ui', 'tooltip-ui', 'menu-ui', 'breadcrumb-ui',
79
+ 'nav-n', 'pagination-ui', 'avatar-group-ui', 'segmented-ui',
80
+ 'segment-ui', 'command-ui', 'command-item-ui', 'command-group-ui',
81
+ 'calendar-picker-ui', 'color-picker-ui', 'kbd-ui', 'toolbar-ui',
82
+ 'otp-input-ui',
83
+ 'theme-ui', 'router-ui', 'adia-root', 'noodles-ui',
84
+ 'listbox-ui', 'icon-provider',
85
+ ]);
86
+ for (const match of tags) {
87
+ const tag = match.slice(1).toLowerCase();
88
+ if (!known.has(tag)) return true;
89
+ }
90
+ return false;
91
+ },
92
+ fix: 'Only use registered AdiaUI types. Check the registry for available components.',
93
+ },
94
+ {
95
+ name: 'slotOnContainer',
96
+ description: 'Slot attributes (slot="heading", slot="description", slot="action") must go on the content children inside Header/Footer, not on Header/Footer elements themselves.',
97
+ check: (html) => {
98
+ return /<header[^>]*\bslot\s*=/i.test(html) || /<footer[^>]*\bslot\s*=/i.test(html);
99
+ },
100
+ fix: 'Move slot="heading" from <header> to the Text/heading child inside it. Header is a grid container — slots go on its children.',
101
+ },
102
+ ];
103
+
104
+ /**
105
+ * Get all registered anti-patterns.
106
+ * @returns {Array<{name: string, description: string, fix: string}>}
107
+ */
108
+ export function getAntiPatterns() {
109
+ return antiPatterns.map(({ name, description, fix }) => ({ name, description, fix }));
110
+ }
111
+
112
+ /**
113
+ * Check an HTML string against a specific anti-pattern.
114
+ *
115
+ * @param {string} name — Anti-pattern name
116
+ * @param {string} html — HTML string to check
117
+ * @returns {{ violated: boolean, name: string, description: string, fix: string } | null}
118
+ * Returns the violation object if detected, null if the pattern name is unknown.
119
+ */
120
+ export function checkAntiPattern(name, html) {
121
+ const pattern = antiPatterns.find(p => p.name === name);
122
+ if (!pattern) return null;
123
+
124
+ const violated = typeof pattern.check === 'function'
125
+ ? pattern.check(html)
126
+ : pattern.check.test(html);
127
+
128
+ return {
129
+ violated,
130
+ name: pattern.name,
131
+ description: pattern.description,
132
+ fix: pattern.fix,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Check HTML against all anti-patterns. Returns only violations.
138
+ *
139
+ * @param {string} html — HTML string to check
140
+ * @returns {Array<{name: string, description: string, fix: string}>}
141
+ */
142
+ export function checkAllAntiPatterns(html) {
143
+ return antiPatterns
144
+ .filter(p => {
145
+ return typeof p.check === 'function' ? p.check(html) : p.check.test(html);
146
+ })
147
+ .map(({ name, description, fix }) => ({ name, description, fix }));
148
+ }
package/catalog.js ADDED
@@ -0,0 +1,215 @@
1
+ /**
2
+ * CatalogManifest — Structured index of all AdiaUI components.
3
+ *
4
+ * Reads from the per-component a2ui.json sidecars co-located with each
5
+ * component (packages/web-components/components/<c>/<c>.a2ui.json), generated
6
+ * from the YAML single-source-of-truth by scripts/build-components.mjs.
7
+ */
8
+
9
+ import { registry } from '@adia-ai/a2ui-utils';
10
+
11
+ // ── Schema loading ──
12
+ const schemaByTag = new Map();
13
+ let schemasLoaded = false;
14
+
15
+ function tagFromSchema(schema) {
16
+ return schema?.['x-adiaui']?.tag || null;
17
+ }
18
+
19
+ async function loadSchemas() {
20
+ if (schemasLoaded) return;
21
+ schemasLoaded = true;
22
+
23
+ if (typeof process !== 'undefined' && process.versions?.node) {
24
+ try {
25
+ const { readdir, readFile, stat } = await import(/* @vite-ignore */ 'node:fs/promises');
26
+ const { fileURLToPath } = await import(/* @vite-ignore */ 'node:url');
27
+ const { dirname, join } = await import(/* @vite-ignore */ 'node:path');
28
+ const compsDir = join(dirname(fileURLToPath(import.meta.url)), '../../web-components/components');
29
+ const entries = await readdir(compsDir, { withFileTypes: true });
30
+ for (const e of entries) {
31
+ if (!e.isDirectory()) continue;
32
+ const path = join(compsDir, e.name, `${e.name}.a2ui.json`);
33
+ try {
34
+ const raw = await readFile(path, 'utf8');
35
+ const schema = JSON.parse(raw);
36
+ const tag = tagFromSchema(schema);
37
+ if (tag) schemaByTag.set(tag, schema);
38
+ } catch { /* skip missing / malformed */ }
39
+ }
40
+ } catch { /* components dir missing */ }
41
+ } else {
42
+ try {
43
+ const modules = import.meta.glob('../../web-components/components/*/*.a2ui.json', { query: '?raw', import: 'default' });
44
+ for (const [, loader] of Object.entries(modules)) {
45
+ try {
46
+ const raw = await loader();
47
+ const schema = JSON.parse(raw);
48
+ const tag = tagFromSchema(schema);
49
+ if (tag) schemaByTag.set(tag, schema);
50
+ } catch { /* skip invalid */ }
51
+ }
52
+ } catch { /* glob not available */ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Register a JSON schema for a component tag (runtime).
58
+ */
59
+ export function registerSchema(tag, schema) {
60
+ schemaByTag.set(tag, schema);
61
+ }
62
+
63
+ // ── Category classification ──
64
+ const categoryMap = {
65
+ 'row-ui': 'layout', 'col-ui': 'layout', 'list-ui': 'layout', 'grid-ui': 'layout',
66
+ 'stack-ui': 'layout', 'block-ui': 'layout', 'toolbar-ui': 'layout',
67
+ 'text-ui': 'display', 'image-ui': 'display', 'icon-ui': 'display',
68
+ 'divider-ui': 'display', 'badge-ui': 'display', 'avatar-ui': 'display',
69
+ 'avatar-group-ui': 'display', 'progress-ui': 'display', 'skeleton-ui': 'display',
70
+ 'code-ui': 'display', 'kbd-ui': 'display', 'tag-ui': 'display',
71
+ 'stat-ui': 'display', 'empty-state-ui': 'display',
72
+ 'input-ui': 'input', 'check-ui': 'input', 'switch-ui': 'input',
73
+ 'slider-ui': 'input', 'select-ui': 'input', 'search-ui': 'input',
74
+ 'upload-ui': 'input', 'textarea-ui': 'input', 'radio-ui': 'input',
75
+ 'otp-input-ui': 'input', 'calendar-picker-ui': 'input', 'color-picker-ui': 'input',
76
+ 'range-ui': 'input',
77
+ 'button-ui': 'action',
78
+ 'card-ui': 'container', 'tabs-ui': 'container', 'tab-ui': 'container',
79
+ 'pane-ui': 'container', 'modal-ui': 'container', 'drawer-ui': 'container',
80
+ 'toast-ui': 'container', 'popover-ui': 'container', 'accordion-ui': 'container',
81
+ 'alert-ui': 'container', 'tooltip-ui': 'container', 'menu-ui': 'container',
82
+ 'command-ui': 'container',
83
+ 'section': 'card-child', 'header': 'card-child', 'footer': 'card-child',
84
+ 'stream-ui': 'agent', 'table-ui': 'agent', 'chart-ui': 'agent',
85
+ 'embed-ui': 'agent', 'noodles-ui': 'agent',
86
+ 'breadcrumb-ui': 'navigation', 'nav-n': 'navigation', 'pagination-ui': 'navigation',
87
+ 'segmented-ui': 'navigation', 'segment-ui': 'navigation', 'router-ui': 'navigation',
88
+ 'toggle-group-ui': 'navigation', 'toggle-option-ui': 'navigation',
89
+ };
90
+
91
+ // ── Trait registry ──
92
+ const traitRegistry = [
93
+ { name: 'pressable', category: 'input-interaction', description: 'Normalizes click/tap/keyboard into a single "press" event' },
94
+ { name: 'focusable', category: 'input-interaction', description: 'Keyboard-only focus ring, ignores pointer focus' },
95
+ { name: 'hoverable', category: 'input-interaction', description: 'Pointer hover enter/leave state tracking' },
96
+ { name: 'active-state', category: 'input-interaction', description: 'Tracks pointer down / active interaction state' },
97
+ { name: 'disabled-state', category: 'input-interaction', description: 'Enforces disabled behavior and interaction blocking' },
98
+ { name: 'keyboard-nav', category: 'keyboard-navigation', description: 'Arrow keys, Enter, Escape — semantic navigation events' },
99
+ { name: 'roving-tabindex', category: 'keyboard-navigation', description: 'Focus management within composite widgets' },
100
+ { name: 'typeahead', category: 'keyboard-navigation', description: 'Incremental search within a collection' },
101
+ { name: 'hotkey', category: 'keyboard-navigation', description: 'Global or scoped keyboard shortcuts' },
102
+ { name: 'focus-trap', category: 'keyboard-navigation', description: 'Traps Tab/Shift+Tab within a container' },
103
+ { name: 'form-associated', category: 'forms-data', description: 'ElementInternals integration for form participation' },
104
+ { name: 'validation', category: 'forms-data', description: 'Validation rules: required, minlength, pattern, email' },
105
+ { name: 'value-sync', category: 'forms-data', description: 'Syncs value between attribute/property/controller' },
106
+ { name: 'dirty-state', category: 'forms-data', description: 'Tracks modified vs initial value' },
107
+ { name: 'resettable', category: 'forms-data', description: 'Responds to form reset events' },
108
+ { name: 'resize-observer-trait', category: 'layout-measurement', description: 'Reacts to element size changes' },
109
+ { name: 'intersection-observer-trait', category: 'layout-measurement', description: 'Visibility detection (viewport)' },
110
+ { name: 'anchor-positioning', category: 'layout-measurement', description: 'Positions relative to an anchor element' },
111
+ { name: 'portal', category: 'layout-measurement', description: 'Renders content in a different DOM root' },
112
+ { name: 'scroll-lock', category: 'layout-measurement', description: 'Disables background scrolling' },
113
+ { name: 'draggable', category: 'motion-positioning', description: 'Pointer drag to reposition' },
114
+ { name: 'tossable', category: 'motion-positioning', description: 'Flick with momentum + viewport bounce' },
115
+ { name: 'resizable', category: 'motion-positioning', description: 'Drag edges/corners to resize' },
116
+ { name: 'inertia-drag', category: 'motion-positioning', description: 'Momentum-based dragging, smooth deceleration' },
117
+ { name: 'snap-to-grid', category: 'motion-positioning', description: 'Snaps position to configurable grid' },
118
+ { name: 'drag-ghost', category: 'motion-positioning', description: 'Ghost clone at origin during drag' },
119
+ { name: 'ripple', category: 'animation-feedback', description: 'Material-style press ripple effect' },
120
+ { name: 'spring-animate', category: 'animation-feedback', description: 'Spring-based motion transitions' },
121
+ { name: 'fade-presence', category: 'animation-feedback', description: 'Enter/exit fade with lifecycle' },
122
+ { name: 'scale-press', category: 'animation-feedback', description: 'Subtle scale transform on press' },
123
+ { name: 'tilt-hover', category: 'animation-feedback', description: 'Tilt based on pointer position' },
124
+ { name: 'glow-focus', category: 'visual-dynamics', description: 'Animated pulsing glow on focus' },
125
+ { name: 'gradient-shift', category: 'visual-dynamics', description: 'Animated rainbow gradient backgrounds' },
126
+ { name: 'parallax', category: 'visual-dynamics', description: 'Layered motion relative to pointer' },
127
+ { name: 'shimmer-loading', category: 'visual-dynamics', description: 'Skeleton shimmer effect' },
128
+ { name: 'noise-texture', category: 'visual-dynamics', description: 'Procedural grain overlay' },
129
+ { name: 'magnetic-hover', category: 'interaction-delight', description: 'Element subtly follows cursor' },
130
+ { name: 'confetti', category: 'interaction-delight', description: 'Radial particle burst on press' },
131
+ { name: 'confetti-burst', category: 'interaction-delight', description: 'Upward fountain particle burst' },
132
+ { name: 'sound-feedback', category: 'audio-haptics-sensory', description: 'Synthesized tones via Web Audio API' },
133
+ { name: 'haptic-feedback', category: 'audio-haptics-sensory', description: 'Vibration API feedback' },
134
+ { name: 'typewriter', category: 'audio-haptics-sensory', description: 'Animated text reveal character by character' },
135
+ { name: 'count-up', category: 'audio-haptics-sensory', description: 'Animated numeric transitions' },
136
+ { name: 'attention-pulse', category: 'audio-haptics-sensory', description: 'Periodic pulse to draw attention' },
137
+ ];
138
+
139
+ // ── Alias tracking ──
140
+ const aliases = new Set([
141
+ 'Select', 'LoadingIndicator', 'ErrorContainer',
142
+ 'Keyboard', 'DatePicker', 'CommandPalette', 'Segmented', 'OTP', 'SideNav', 'IconSource',
143
+ ]);
144
+
145
+ // ── Build the catalog ──
146
+ let catalog = null;
147
+
148
+ async function buildCatalog() {
149
+ if (catalog) return catalog;
150
+ await loadSchemas();
151
+
152
+ const entries = new Map();
153
+
154
+ for (const [type, tag] of registry) {
155
+ if (aliases.has(type)) continue;
156
+ // Skip -ui alias entries (lowercase with dash)
157
+ if (type.includes('-')) continue;
158
+
159
+ const schema = schemaByTag.get(tag) || null;
160
+ const category = categoryMap[tag] || 'other';
161
+
162
+ entries.set(type, {
163
+ type,
164
+ tag,
165
+ category,
166
+ description: schema?.description || '',
167
+ schema,
168
+ });
169
+ }
170
+
171
+ catalog = {
172
+ version: '0.1.0',
173
+ totalTypes: entries.size,
174
+ entries,
175
+ };
176
+
177
+ return catalog;
178
+ }
179
+
180
+ export async function getCatalog() {
181
+ return buildCatalog();
182
+ }
183
+
184
+ export async function getComponent(typeName) {
185
+ const cat = await buildCatalog();
186
+ if (cat.entries.has(typeName)) return cat.entries.get(typeName);
187
+ const tag = registry.get(typeName);
188
+ if (!tag) return null;
189
+ for (const entry of cat.entries.values()) {
190
+ if (entry.tag === tag) return entry;
191
+ }
192
+ return null;
193
+ }
194
+
195
+ export async function getComponentsByCategory(category) {
196
+ const cat = await buildCatalog();
197
+ return [...cat.entries.values()].filter(e => e.category === category);
198
+ }
199
+
200
+ export function getTraits() {
201
+ return traitRegistry;
202
+ }
203
+
204
+ export function getTraitsByCategory(category) {
205
+ return traitRegistry.filter(t => t.category === category);
206
+ }
207
+
208
+ export async function getFullCatalog() {
209
+ const cat = await buildCatalog();
210
+ return {
211
+ ...cat,
212
+ totalTraits: traitRegistry.length,
213
+ traits: traitRegistry,
214
+ };
215
+ }
package/clarity.js ADDED
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Clarity Assessment — Evaluates whether a user intent is clear enough
3
+ * to generate quality UI, or needs clarifying questions first.
4
+ *
5
+ * Scores intents across 5 dimensions:
6
+ * domain — Is the domain clear? (forms, data, layout, etc.)
7
+ * scope — Is the scope defined? (how many items, what sections?)
8
+ * content — Is the content specified? (labels, values, data?)
9
+ * layout — Is the layout preference stated? (grid, stack, sidebar?)
10
+ * action — Are actions/interactions mentioned? (buttons, forms, links?)
11
+ *
12
+ * Returns a clarity score (0-1) and targeted questions for what's missing.
13
+ */
14
+
15
+ import { classifyIntent } from './domain-router.js';
16
+ import { searchPatterns } from './pattern-library.js';
17
+
18
+ // ── Dimension detectors ──────────────────────────────────────────────────
19
+
20
+ /** Scope indicators: quantity, enumeration, specificity */
21
+ const SCOPE_SIGNALS = [
22
+ /\b\d+\b/, // contains a number
23
+ /\b(three|two|four|five|six)\b/i, // spelled-out numbers
24
+ /\b(with|containing|including|showing|displaying)\b/i, // enumerates content
25
+ /\b(and|plus|also)\b/i, // multiple items
26
+ /\b(columns?|rows?|items?|cards?|sections?|fields?|buttons?|tabs?|steps?)\b/i, // structural counts
27
+ ];
28
+
29
+ /** Content specificity: labels, values, named data */
30
+ const CONTENT_SIGNALS = [
31
+ /\b(name|email|password|title|description|price|date|status|role|avatar)\b/i,
32
+ /\b(revenue|users|growth|sales|orders|metrics|analytics)\b/i,
33
+ /\b(todo|done|in progress|pending|active|completed)\b/i,
34
+ /\b(bleed|margin|trim|crop|preview|artwork|brand|design system)\b/i,
35
+ /\b(approved|approval|production|settings|configure|preference)\b/i,
36
+ /["'][\w\s]+["']/, // quoted strings (specific labels)
37
+ /\$[\d,.]+/, // dollar amounts
38
+ /\d+%/, // percentages
39
+ ];
40
+
41
+ /** Layout preferences */
42
+ const LAYOUT_SIGNALS = [
43
+ /\b(grid|row|column|sidebar|split|horizontal|vertical|stack|centered)\b/i,
44
+ /\b(full.?width|responsive|mobile|compact|wide|narrow)\b/i,
45
+ /\b(\d+.?col(umn)?s?)\b/i, // "3 columns", "2-col"
46
+ /\b(left|right|top|bottom|center)\b/i,
47
+ ];
48
+
49
+ /** Action/interaction indicators */
50
+ const ACTION_SIGNALS = [
51
+ /\b(button|submit|cancel|save|delete|edit|close|open|toggle|click)\b/i,
52
+ /\b(form|input|select|search|filter|sort|upload|download)\b/i,
53
+ /\b(login|signup|register|checkout|confirm|approve|reject)\b/i,
54
+ /\b(navigate|link|redirect|route)\b/i,
55
+ /\b(drag|drop|resize|expand|collapse)\b/i,
56
+ ];
57
+
58
+ /**
59
+ * Assess clarity of a user intent for UI generation.
60
+ *
61
+ * @param {string} intent — Raw user input
62
+ * @param {{ domain: string, confidence: number, matchedSignals: string[] }} [classification] — Pre-computed classification
63
+ * @returns {{
64
+ * clear: boolean,
65
+ * score: number,
66
+ * dimensions: { domain: number, scope: number, content: number, layout: number, action: number },
67
+ * questions: { text: string, dimension: string, priority: number }[],
68
+ * summary: string,
69
+ * }}
70
+ */
71
+ export function assessClarity(intent, classification) {
72
+ const text = (intent ?? '').trim();
73
+ if (!text) {
74
+ return {
75
+ clear: false,
76
+ score: 0,
77
+ dimensions: { domain: 0, scope: 0, content: 0, layout: 0, action: 0 },
78
+ questions: [{ text: 'What would you like me to build?', dimension: 'domain', priority: 1 }],
79
+ summary: 'No intent provided',
80
+ };
81
+ }
82
+
83
+ const cls = classification || classifyIntent(text);
84
+ const patterns = searchPatterns(text);
85
+ const lower = text.toLowerCase();
86
+ const wordCount = text.split(/\s+/).length;
87
+
88
+ // ── Score each dimension ──
89
+
90
+ // Domain: how confident is the domain classification?
91
+ const domainScore = cls.confidence >= 0.3 ? 1 : cls.confidence >= 0.15 ? 0.6 : cls.matchedSignals.length > 0 ? 0.3 : 0;
92
+
93
+ // Scope: is the scope specific?
94
+ const scopeHits = SCOPE_SIGNALS.filter(r => r.test(text)).length;
95
+ const scopeScore = Math.min(1, scopeHits / 2);
96
+
97
+ // Content: are concrete labels/values mentioned?
98
+ const contentHits = CONTENT_SIGNALS.filter(r => r.test(text)).length;
99
+ const contentScore = Math.min(1, contentHits / 2);
100
+
101
+ // Layout: is a layout preference stated?
102
+ const layoutHits = LAYOUT_SIGNALS.filter(r => r.test(text)).length;
103
+ const layoutScore = Math.min(1, layoutHits);
104
+
105
+ // Action: are interactions mentioned?
106
+ const actionHits = ACTION_SIGNALS.filter(r => r.test(text)).length;
107
+ const actionScore = Math.min(1, actionHits / 2);
108
+
109
+ // ── Overall score (weighted) ──
110
+ const score = (
111
+ domainScore * 0.25 +
112
+ scopeScore * 0.25 +
113
+ contentScore * 0.20 +
114
+ layoutScore * 0.15 +
115
+ actionScore * 0.15
116
+ );
117
+
118
+ // ── Bonus: pattern match adds confidence ──
119
+ const patternBonus = patterns.length > 0 ? 0.15 : 0;
120
+ // Bonus: long intents are usually more specific
121
+ const lengthBonus = wordCount > 10 ? 0.1 : wordCount > 6 ? 0.05 : 0;
122
+
123
+ const finalScore = Math.min(1, score + patternBonus + lengthBonus);
124
+
125
+ // ── Generate targeted questions for weak dimensions ──
126
+ const questions = [];
127
+
128
+ if (domainScore < 0.3) {
129
+ questions.push({
130
+ text: 'What type of UI is this? (e.g., a form, dashboard, profile card, settings page)',
131
+ dimension: 'domain',
132
+ priority: 1,
133
+ });
134
+ }
135
+
136
+ if (scopeScore < 0.5) {
137
+ const domainQuestions = {
138
+ forms: 'What fields should the form include?',
139
+ data: 'What data should be displayed? How many items or metrics?',
140
+ layout: 'How many sections or cards should it have?',
141
+ agent: 'What actions should the assistant support?',
142
+ navigation: 'What pages or sections should be navigable?',
143
+ };
144
+ questions.push({
145
+ text: domainQuestions[cls.domain] || 'Can you be more specific about what it should contain?',
146
+ dimension: 'scope',
147
+ priority: 2,
148
+ });
149
+ }
150
+
151
+ if (contentScore < 0.3 && scopeScore >= 0.3) {
152
+ const contentQuestions = {
153
+ forms: 'What labels should the fields have? (e.g., "Email", "Password", "Full Name")',
154
+ data: 'What metrics or values should be shown? (e.g., revenue, users, growth rate)',
155
+ layout: 'What content goes in each section?',
156
+ agent: 'What kind of messages or responses should be shown?',
157
+ navigation: 'What should the menu items or links be labeled?',
158
+ };
159
+ questions.push({
160
+ text: contentQuestions[cls.domain] || 'What specific content should be displayed?',
161
+ dimension: 'content',
162
+ priority: 3,
163
+ });
164
+ }
165
+
166
+ if (layoutScore < 0.3 && scopeScore >= 0.5) {
167
+ questions.push({
168
+ text: 'Any layout preference? (e.g., grid of cards, single column, sidebar + content)',
169
+ dimension: 'layout',
170
+ priority: 4,
171
+ });
172
+ }
173
+
174
+ if (actionScore < 0.3 && domainScore >= 0.3) {
175
+ questions.push({
176
+ text: 'What actions should users be able to take? (e.g., submit, edit, delete, filter)',
177
+ dimension: 'action',
178
+ priority: 5,
179
+ });
180
+ }
181
+
182
+ // Sort by priority, limit to 3
183
+ questions.sort((a, b) => a.priority - b.priority);
184
+ const topQuestions = questions.slice(0, 3);
185
+
186
+ // ── Determine if clear enough to proceed ──
187
+ // Clear only if dimensional score meets threshold
188
+ const clear = finalScore >= 0.4;
189
+
190
+ const summary = clear
191
+ ? `Intent is ${finalScore >= 0.7 ? 'clear' : 'adequate'} (${Math.round(finalScore * 100)}%)`
192
+ : `Intent needs clarification (${Math.round(finalScore * 100)}% — below 40% threshold)`;
193
+
194
+ return {
195
+ clear,
196
+ score: Math.round(finalScore * 100) / 100,
197
+ dimensions: {
198
+ domain: Math.round(domainScore * 100) / 100,
199
+ scope: Math.round(scopeScore * 100) / 100,
200
+ content: Math.round(contentScore * 100) / 100,
201
+ layout: Math.round(layoutScore * 100) / 100,
202
+ action: Math.round(actionScore * 100) / 100,
203
+ },
204
+ questions: topQuestions,
205
+ summary,
206
+ };
207
+ }