@adia-ai/a2ui-retrieval 0.2.0 → 0.2.2

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
@@ -9,13 +9,61 @@ _Nothing yet._
9
9
 
10
10
  ---
11
11
 
12
+ ## [0.2.2] - 2026-05-02
13
+
14
+ **Lockstep cut + `detectReferences` brand-only filter.** All 8 published `@adia-ai/*` packages bump 0.2.1 → 0.2.2 per [`docs/specs/package-architecture.md` § 15](../../../docs/specs/package-architecture.md#15-versioning-policy). Patch cut — no breaking changes.
15
+
16
+ ### Changed
17
+
18
+ - `version`: `0.2.1` → `0.2.2`.
19
+ - `dependencies["@adia-ai/a2ui-corpus"]`: `^0.2.0` (covers `0.2.2`).
20
+ - `dependencies["@adia-ai/a2ui-utils"]`: `^0.2.0` (covers `0.2.2`).
21
+ - `authoring/web-research.js` is the surface that gained the EXPLICIT/IMPLICIT pattern split; no other retrieval subdirs touched.
22
+
23
+ ### Fixed — `detectReferences` brand-only filter (2026-05-02)
24
+
25
+ `authoring/web-research.js` regex patterns were capturing generic nouns as brand references. For the intent `"tier pricing page"`, the pattern `\b(\w+)\s+(?:pricing|dashboard|landing|login|settings|profile)\s+page\b` captured `"tier"` and seeded a research query for `"tier pricing UI design components"` — useless, since `"tier"` is a generic noun, not a brand reference.
26
+
27
+ Fix: split `REFERENCE_PATTERNS` into two tiers by signal strength:
28
+
29
+ - **`EXPLICIT_PATTERNS`** — the surrounding language declares the captured token IS a brand reference (`"like X"`, `"X-style"`, `"similar to X"`, `"clone X"`). Captures accepted as references.
30
+ - **`IMPLICIT_PATTERNS`** — the surrounding language is a UI-shape verb (`"X pricing page"`). Captures accepted ONLY if in `KNOWN_REFERENCES` (Stripe, Notion, Figma, …); generic nouns dropped.
31
+
32
+ Verified:
33
+ - `"tier pricing page"` → no refs (was: `["tier"]`)
34
+ - `"fancy pricing page"` → no refs
35
+ - `"Stripe pricing page"` → `["stripe"]` ✓
36
+ - `"Notion-style sidebar"` → `["notion"]` ✓
37
+ - `"like Figma"` → `["figma"]` ✓
38
+
39
+ Per memory `feedback_panel_label_reliability.md`. Companion fix in `@adia-ai/a2ui-compose` Unreleased (generator.js panel emissions). Both surfaces feed the same reasoning-panel deception class — five distinct bugs caught + fixed in commit `9986c71e`.
40
+
41
+ ---
42
+
43
+ ## [0.2.1] - 2026-05-02
44
+
45
+ **Lockstep cut.** All 8 published `@adia-ai/*` packages bump 0.2.0 → 0.2.1 per [`docs/specs/package-architecture.md` § 15](../../../docs/specs/package-architecture.md#15-versioning-policy). Patch cut — no breaking changes.
46
+
47
+ ### Changed
48
+
49
+ - `version`: `0.2.0` → `0.2.1`.
50
+ - `dependencies["@adia-ai/a2ui-corpus"]`: `^0.2.0` (covers `0.2.1`).
51
+ - `dependencies["@adia-ai/a2ui-utils"]`: `^0.2.0` (covers `0.2.1`).
52
+ - `embedding/chunk-embedding-retriever.js` + `embedding/embedding-retriever.js` — defensive resolution path: when the `chunk-embeddings.json` / `pattern-embeddings.json` indexes aren't reachable (e.g. inside a `node_modules/@adia-ai/a2ui-corpus/` install layout where the `files:` array now excludes them), the retriever falls back to keyword-only retrieval gracefully instead of throwing on `ENOENT`. Aligns with the `@adia-ai/a2ui-corpus` 0.2.0 tarball-trim decision.
53
+
54
+ ### No source-behavior changes
55
+
56
+ Public API of `a2ui-retrieval` is byte-identical to `0.2.0`. The above embedding-retriever updates only affect the failure path (`ENOENT` graceful fallback) and don't change the contract for callers passing valid index files. The cut bumps version + tracks dep ranges + softens the embedding-index-missing failure mode.
57
+
58
+ ---
59
+
12
60
  ## [0.2.0] - 2026-05-02
13
61
 
14
62
  **Lockstep cut + boundary reorganization.** All 8 published
15
63
  `@adia-ai/*` packages now share one version, governed by
16
64
  [`docs/specs/package-architecture.md` § 15 (Versioning Policy)](../../../docs/specs/package-architecture.md#15-versioning-policy).
17
65
  This release also lands the boundary cleanup from T4 of the
18
- [`docs/plans/packages-architecture-fixes-2026-05-02.md`](../../../docs/plans/packages-architecture-fixes-2026-05-02.md)
66
+ [`docs/plans/packages-architecture-fixes-2026-05-02.md`](../../../.brain/archive/2026-Q2/PLAN-packages-architecture-fixes-2026-05-02.md)
19
67
  plan.
20
68
 
21
69
  ### Changed
@@ -71,8 +119,6 @@ The top-level `index.js` still re-exports the previously-public surface
71
119
 
72
120
  ---
73
121
 
74
- ---
75
-
76
122
  ## [0.0.1] - 2026-04-24
77
123
 
78
124
  First public release. Extracted from
@@ -13,8 +13,25 @@
13
13
 
14
14
  // ── Reference detection ──────────────────────────────────────────────────
15
15
 
16
- /** Patterns that indicate the user is referencing a specific product/design */
17
- const REFERENCE_PATTERNS = [
16
+ /**
17
+ * Patterns split by signal strength:
18
+ *
19
+ * EXPLICIT_PATTERNS — the surrounding language declares the captured
20
+ * token IS a brand reference ("like X", "X-style", "similar to X").
21
+ * The capture is accepted as a reference even if not in KNOWN_REFERENCES.
22
+ *
23
+ * IMPLICIT_PATTERNS — the surrounding language is a UI-shape verb
24
+ * ("X pricing page"); the capture COULD be a brand or COULD be a
25
+ * generic noun. Accept ONLY if in KNOWN_REFERENCES, otherwise drop.
26
+ *
27
+ * Splitting this way fixes the 2026-05-02 bug where "tier pricing page"
28
+ * extracted "tier" as a reference and seeded a research query for it. The
29
+ * regex was working as designed — `\w+ pricing page` captures the prefix
30
+ * word — but a generic noun like "tier" or "subscription" or "fancy"
31
+ * isn't a brand reference, so research and downstream prompt enrichment
32
+ * shouldn't act on it.
33
+ */
34
+ const EXPLICIT_PATTERNS = [
18
35
  /\blike\s+(\w[\w\s]*?)(?:\s*['']s|\s+style|\s+design|\s*$)/i,
19
36
  /\b(\w[\w\s]*?)[\s-]style\b/i,
20
37
  /\b(\w[\w\s]*?)[\s-]inspired\b/i,
@@ -22,6 +39,9 @@ const REFERENCE_PATTERNS = [
22
39
  /\bbased\s+on\s+(\w[\w\s]*?)(?:\s|$|,)/i,
23
40
  /\bcopy\s+(\w[\w\s]*?)(?:\s|$|,)/i,
24
41
  /\bclone\s+(\w[\w\s]*?)(?:\s|$|,)/i,
42
+ ];
43
+
44
+ const IMPLICIT_PATTERNS = [
25
45
  /\b(\w+)\s+(?:pricing|dashboard|landing|login|settings|profile)\s+page\b/i,
26
46
  ];
27
47
 
@@ -44,12 +64,11 @@ export function detectReferences(intent) {
44
64
  const references = [];
45
65
  const lower = intent.toLowerCase();
46
66
 
47
- // Pattern matching
48
- for (const pattern of REFERENCE_PATTERNS) {
67
+ // Explicit-signal patterns — accept the capture as a reference.
68
+ for (const pattern of EXPLICIT_PATTERNS) {
49
69
  const match = intent.match(pattern);
50
70
  if (match) {
51
71
  let ref = (match[1] || '').trim();
52
- // Strip leading articles
53
72
  ref = ref.replace(/^(the|a|an)\s+/i, '');
54
73
  if (ref && ref.length > 1 && ref.length < 30) {
55
74
  references.push(ref);
@@ -57,6 +76,19 @@ export function detectReferences(intent) {
57
76
  }
58
77
  }
59
78
 
79
+ // Implicit-signal patterns — gate by KNOWN_REFERENCES so generic nouns
80
+ // ("tier", "fancy", "simple") don't get treated as brands.
81
+ for (const pattern of IMPLICIT_PATTERNS) {
82
+ const match = intent.match(pattern);
83
+ if (match) {
84
+ let ref = (match[1] || '').trim();
85
+ ref = ref.replace(/^(the|a|an)\s+/i, '');
86
+ if (ref && ref.length > 1 && ref.length < 30 && KNOWN_REFERENCES.has(ref.toLowerCase())) {
87
+ references.push(ref);
88
+ }
89
+ }
90
+ }
91
+
60
92
  // Direct brand name detection (only as whole words)
61
93
  for (const brand of KNOWN_REFERENCES) {
62
94
  const re = new RegExp(`\\b${brand}\\b`, 'i');
@@ -65,7 +97,9 @@ export function detectReferences(intent) {
65
97
  }
66
98
  }
67
99
 
68
- // Filter out non-brand captures (e.g., "inspired" from "X-inspired")
100
+ // Filter out non-brand captures from the explicit patterns — words like
101
+ // "inspired" can sneak through when the intent is "X-inspired" with no
102
+ // X (e.g. "is this AI-inspired?"). Belt and suspenders.
69
103
  const NON_BRANDS = new Set(['inspired', 'style', 'based', 'like', 'similar', 'the']);
70
104
  for (let i = references.length - 1; i >= 0; i--) {
71
105
  if (NON_BRANDS.has(references[i].toLowerCase())) references.splice(i, 1);
package/catalog.js CHANGED
@@ -89,52 +89,57 @@ const categoryMap = {
89
89
  };
90
90
 
91
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
- ];
92
+ //
93
+ // Loaded from packages/web-components/traits/_catalog.json generated
94
+ // by `node scripts/build/traits-catalog.mjs` from the live defineTrait()
95
+ // metadata. Hand-edits here will be overwritten. Treat the trait file
96
+ // itself as the single source of truth.
97
+ let traitRegistry = [];
98
+
99
+ async function loadTraitRegistry() {
100
+ if (typeof process !== 'undefined' && process.versions?.node) {
101
+ try {
102
+ const { readFile } = await import(/* @vite-ignore */ 'node:fs/promises');
103
+ const { fileURLToPath } = await import(/* @vite-ignore */ 'node:url');
104
+ const { dirname, join } = await import(/* @vite-ignore */ 'node:path');
105
+ const path = join(dirname(fileURLToPath(import.meta.url)), '../../web-components/traits/_catalog.json');
106
+ const raw = await readFile(path, 'utf8');
107
+ const data = JSON.parse(raw);
108
+ traitRegistry = data.traits.map(t => ({
109
+ name: t.name,
110
+ category: t.category,
111
+ description: t.description,
112
+ attributes: [...t.attributes],
113
+ events: [...t.events],
114
+ config: [...t.config],
115
+ }));
116
+ } catch (err) {
117
+ console.warn('[a2ui-corpus] could not load trait catalog:', err.message);
118
+ }
119
+ } else {
120
+ // Browser branch — fetch via HTTP, not `import('… .json', { with: { type: 'json' } })`.
121
+ // Vite's dev server transforms JSON imports into JS modules and serves them
122
+ // with `Content-Type: text/javascript`; Chrome's strict MIME check rejects
123
+ // that when the spec-correct `with: { type: 'json' }` attribute is present.
124
+ // (Site pages already use fetch() for the same file — see
125
+ // site/pages/traits/_api-table.js + site/pages/traits/catalog/index.setup.js.)
126
+ try {
127
+ const res = await fetch('/packages/web-components/traits/_catalog.json');
128
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
129
+ const data = await res.json();
130
+ traitRegistry = data.traits.map(t => ({
131
+ name: t.name,
132
+ category: t.category,
133
+ description: t.description,
134
+ attributes: [...t.attributes],
135
+ events: [...t.events],
136
+ config: [...t.config],
137
+ }));
138
+ } catch { /* trait catalog not present in this build context */ }
139
+ }
140
+ }
141
+
142
+ await loadTraitRegistry();
138
143
 
139
144
  // ── Alias tracking ──
140
145
  const aliases = new Set([
@@ -32,9 +32,20 @@ async function _loadIndex() {
32
32
  const fs = await import(/* @vite-ignore */ 'node:fs/promises');
33
33
  const path = await import(/* @vite-ignore */ 'node:path');
34
34
  const url = await import(/* @vite-ignore */ 'node:url');
35
- // packages/a2ui/retrieval/embedding up 2 packages/a2ui corpus/chunk-embeddings.json
36
- const here = path.dirname(url.fileURLToPath(import.meta.url));
37
- const p = path.resolve(here, '../../corpus/chunk-embeddings.json');
35
+ // Try the package-import path first (works under node_modules
36
+ // install layout — `@adia-ai/a2ui-corpus` exports
37
+ // `./chunk-embeddings`). Fall back to the relative source-tree
38
+ // path so a source-checkout monorepo without symlinked deps
39
+ // still resolves.
40
+ let p = null;
41
+ try {
42
+ const { createRequire } = await import(/* @vite-ignore */ 'node:module');
43
+ const require = createRequire(import.meta.url);
44
+ p = require.resolve('@adia-ai/a2ui-corpus/chunk-embeddings');
45
+ } catch {
46
+ const here = path.dirname(url.fileURLToPath(import.meta.url));
47
+ p = path.resolve(here, '../../corpus/chunk-embeddings.json');
48
+ }
38
49
  const raw = await fs.readFile(p, 'utf8');
39
50
  _index = JSON.parse(raw);
40
51
  } else {
@@ -32,9 +32,20 @@ async function _loadIndex() {
32
32
  const fs = await import(/* @vite-ignore */ 'node:fs/promises');
33
33
  const path = await import(/* @vite-ignore */ 'node:path');
34
34
  const url = await import(/* @vite-ignore */ 'node:url');
35
- // packages/a2ui/retrieval/embedding up 2 packages/a2ui corpus/pattern-embeddings.json
36
- const here = path.dirname(url.fileURLToPath(import.meta.url));
37
- const p = path.resolve(here, '../../corpus/pattern-embeddings.json');
35
+ // Try the package-import path first (works under node_modules
36
+ // install layout — `@adia-ai/a2ui-corpus` exports
37
+ // `./pattern-embeddings`). Fall back to the relative source-tree
38
+ // path so a source-checkout monorepo without symlinked deps
39
+ // still resolves.
40
+ let p = null;
41
+ try {
42
+ const { createRequire } = await import(/* @vite-ignore */ 'node:module');
43
+ const require = createRequire(import.meta.url);
44
+ p = require.resolve('@adia-ai/a2ui-corpus/pattern-embeddings');
45
+ } catch {
46
+ const here = path.dirname(url.fileURLToPath(import.meta.url));
47
+ p = path.resolve(here, '../../corpus/pattern-embeddings.json');
48
+ }
38
49
  const raw = await fs.readFile(p, 'utf8');
39
50
  _index = JSON.parse(raw);
40
51
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/a2ui-retrieval",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "AdiaUI A2UI retrieval layer — catalog lookup, intent classification, domain routing, pattern + anti-pattern matching, clarity + context assembly. Consumed by the compose engine and any A2UI-protocol tooling that needs to reason about user intent against the catalog.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -37,5 +37,8 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@adia-ai/a2ui-utils": "^0.2.0"
40
+ },
41
+ "optionalDependencies": {
42
+ "@adia-ai/a2ui-corpus": "^0.2.0"
40
43
  }
41
44
  }