@adia-ai/a2ui-validator 0.4.0 → 0.4.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 +17 -0
- package/package.json +2 -2
- package/semantic/classify-intent.js +128 -0
- package/semantic/classify-intent.test.js +192 -0
- package/semantic/intent-specs.js +162 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
|
|
7
7
|
|
|
8
8
|
_No pending changes._
|
|
9
9
|
|
|
10
|
+
## [0.4.1] - 2026-05-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Semantic-validator Phase 3 foundation** (per [docs/specs/semantic-validator.md](../../../docs/specs/semantic-validator.md) § Phase 3 — rule-based fast-path):
|
|
15
|
+
- `semantic/intent-specs.js` — registry of 10 `IntentSpec` entries covering dominant intent classes: `auth.signup`, `auth.signin`, `auth.password-reset`, `dashboard.analytics`, `data-table.list`, `form.settings`, `form.contact`, `chat.conversation`, `commerce.cart`, `errors.not-found`.
|
|
16
|
+
- `semantic/classify-intent.js` — `classifyIntent()` regex classifier + `scoreAgainstSpec()` deterministic A2UI-tree scorer. Returns rule verdict with certainty 0..1 (≥ 0.9 = LLM-skip eligible per spec § 3c hybrid).
|
|
17
|
+
- 18 unit tests covering registry hygiene, regex match against 6 dominant intents, clean-pass / clean-fail / partial-verdict scoring, nested A2UI tree traversal.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Spec status updated in `docs/specs/semantic-validator.md` to reflect Phase 3 foundation shipped (NOT yet wired into `validateSemantics()` — integration deferred until shadow-compare runs prove ≥ 95% agreement with LLM judge).
|
|
22
|
+
|
|
23
|
+
### Notes
|
|
24
|
+
|
|
25
|
+
- The classifier is NOT yet called from `validateSemantics()`. Phase 3 integration requires (a) the shadow-compare runs from the spec's exit criteria, and (b) a `judge.js` extension to accept rule-verdict hints when falling through to LLM. Both planned for a follow-up cut.
|
|
26
|
+
|
|
10
27
|
## [0.4.0] - 2026-05-10
|
|
11
28
|
|
|
12
29
|
### Ride-along (no source changes)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/a2ui-validator",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "AdiaUI A2UI validator
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "AdiaUI A2UI validator — JSON Schema structural validation plus catalog-aware semantic validation (component exists, props match YAML). Split out from the compose engine so non-compose tooling (tests, MCP validator tools, CI gates) can depend on validation without pulling the whole generator graph.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
7
7
|
"exports": {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent classifier — Phase 3 kickoff (semantic-validator)
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/semantic-validator.md § Phase 3
|
|
5
|
+
*
|
|
6
|
+
* Two-stage classification:
|
|
7
|
+
*
|
|
8
|
+
* 1. Find the best-matching IntentSpec from the registry via regex
|
|
9
|
+
* 2. Score the emitted A2UI tree against the spec deterministically
|
|
10
|
+
*
|
|
11
|
+
* High-confidence matches (intent.confidence ≥ 0.9 AND verdict.certainty ≥ 0.9)
|
|
12
|
+
* can short-circuit the LLM judge per § 3c (hybrid).
|
|
13
|
+
*
|
|
14
|
+
* Status: kickoff — not yet wired into validateSemantics(). Use for
|
|
15
|
+
* shadow-compare runs (the test harness in classify-intent.test.js).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { findIntentSpec } from './intent-specs.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Classify an intent prompt against the IntentSpec registry.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} intent — natural-language prompt
|
|
24
|
+
* @returns {{ spec: IntentSpec | null, confidence: number }}
|
|
25
|
+
*/
|
|
26
|
+
export function classifyIntent(intent) {
|
|
27
|
+
return findIntentSpec(intent);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Score an A2UI tree against an IntentSpec deterministically.
|
|
32
|
+
*
|
|
33
|
+
* Returns a SemanticVerdict-shaped object with rule-based certainty.
|
|
34
|
+
* The caller decides whether to skip the LLM judge based on `certainty`.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} a2ui — emitted A2UI tree (root object with `nodes` or
|
|
37
|
+
* equivalent traversable shape)
|
|
38
|
+
* @param {IntentSpec} spec — the classified IntentSpec
|
|
39
|
+
* @returns {RuleVerdict}
|
|
40
|
+
*/
|
|
41
|
+
export function scoreAgainstSpec(a2ui, spec) {
|
|
42
|
+
const { distinct, instances } = collectComponentTypes(a2ui);
|
|
43
|
+
|
|
44
|
+
const requiredMissing = spec.required.filter((t) => !distinct.includes(t));
|
|
45
|
+
const forbiddenPresent = spec.forbidden.filter((t) => distinct.includes(t));
|
|
46
|
+
const componentCount = instances; // total instance count (matches existing validator semantics)
|
|
47
|
+
|
|
48
|
+
// Clean pass: all required present, no forbidden, meets minComponents
|
|
49
|
+
const cleanPass =
|
|
50
|
+
requiredMissing.length === 0 &&
|
|
51
|
+
forbiddenPresent.length === 0 &&
|
|
52
|
+
componentCount >= spec.minComponents;
|
|
53
|
+
|
|
54
|
+
// Clean fail: 2+ required missing (per spec § 3c "cleanly fails")
|
|
55
|
+
const cleanFail = requiredMissing.length >= 2;
|
|
56
|
+
|
|
57
|
+
// Certainty: 1.0 on clean pass/fail; 0.5 on partial (ambiguous)
|
|
58
|
+
let certainty = 0.5;
|
|
59
|
+
let verdict = 'partial';
|
|
60
|
+
if (cleanPass) {
|
|
61
|
+
certainty = 1.0;
|
|
62
|
+
verdict = 'pass';
|
|
63
|
+
} else if (cleanFail) {
|
|
64
|
+
certainty = 1.0;
|
|
65
|
+
verdict = 'fail';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Build a structured findings payload — useful as hints to the LLM
|
|
69
|
+
// judge on fall-through
|
|
70
|
+
return {
|
|
71
|
+
verdict,
|
|
72
|
+
certainty,
|
|
73
|
+
score: cleanPass ? 100 : cleanFail ? 0 : 50,
|
|
74
|
+
spec: spec.id,
|
|
75
|
+
findings: {
|
|
76
|
+
required_missing: requiredMissing,
|
|
77
|
+
forbidden_present: forbiddenPresent,
|
|
78
|
+
component_count: componentCount,
|
|
79
|
+
min_components: spec.minComponents,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Walk an A2UI tree and collect (a) the set of distinct component types
|
|
86
|
+
* and (b) the total instance count. The A2UI tree shape varies — sometimes
|
|
87
|
+
* flat arrays, sometimes nested under `nodes` / `children` / `body`. This
|
|
88
|
+
* walker handles both.
|
|
89
|
+
*
|
|
90
|
+
* @param {*} node — root or nested A2UI shape
|
|
91
|
+
* @returns {{ distinct: string[], instances: number }}
|
|
92
|
+
*/
|
|
93
|
+
function collectComponentTypes(node) {
|
|
94
|
+
const distinct = new Set();
|
|
95
|
+
const counter = { n: 0 };
|
|
96
|
+
walk(node, distinct, counter);
|
|
97
|
+
return { distinct: Array.from(distinct), instances: counter.n };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function walk(node, distinct, counter) {
|
|
101
|
+
if (!node) return;
|
|
102
|
+
if (Array.isArray(node)) {
|
|
103
|
+
for (const item of node) walk(item, distinct, counter);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (typeof node !== 'object') return;
|
|
107
|
+
// Count instances + collect distinct
|
|
108
|
+
if (typeof node.component === 'string') {
|
|
109
|
+
distinct.add(node.component);
|
|
110
|
+
counter.n += 1;
|
|
111
|
+
} else if (typeof node.type === 'string') {
|
|
112
|
+
distinct.add(node.type);
|
|
113
|
+
counter.n += 1;
|
|
114
|
+
}
|
|
115
|
+
// Recurse children
|
|
116
|
+
if (node.children) walk(node.children, distinct, counter);
|
|
117
|
+
if (node.nodes) walk(node.nodes, distinct, counter);
|
|
118
|
+
if (node.body) walk(node.body, distinct, counter);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @typedef {object} RuleVerdict
|
|
123
|
+
* @property {'pass' | 'fail' | 'partial'} verdict
|
|
124
|
+
* @property {number} certainty — 0..1; ≥ 0.9 allows LLM-skip per § 3c
|
|
125
|
+
* @property {number} score — 0..100; aligned with LLM judge scale
|
|
126
|
+
* @property {string} spec — IntentSpec.id that produced this verdict
|
|
127
|
+
* @property {object} findings — required_missing / forbidden_present / counts
|
|
128
|
+
*/
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { INTENT_SPECS, findIntentSpec } from './intent-specs.js';
|
|
3
|
+
import { classifyIntent, scoreAgainstSpec } from './classify-intent.js';
|
|
4
|
+
|
|
5
|
+
describe('intent-specs registry', () => {
|
|
6
|
+
it('exports at least 10 IntentSpec entries', () => {
|
|
7
|
+
expect(INTENT_SPECS.length).toBeGreaterThanOrEqual(10);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('has unique IntentSpec.id values', () => {
|
|
11
|
+
const ids = INTENT_SPECS.map((s) => s.id);
|
|
12
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('every IntentSpec has the required fields', () => {
|
|
16
|
+
for (const spec of INTENT_SPECS) {
|
|
17
|
+
expect(spec.id).toBeTruthy();
|
|
18
|
+
expect(spec.keywords).toBeInstanceOf(RegExp);
|
|
19
|
+
expect(Array.isArray(spec.required)).toBe(true);
|
|
20
|
+
expect(spec.required.length).toBeGreaterThan(0);
|
|
21
|
+
expect(Array.isArray(spec.forbidden)).toBe(true);
|
|
22
|
+
expect(typeof spec.minComponents).toBe('number');
|
|
23
|
+
expect(spec.minComponents).toBeGreaterThan(0);
|
|
24
|
+
expect(typeof spec.domain).toBe('string');
|
|
25
|
+
expect(typeof spec.description).toBe('string');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('findIntentSpec', () => {
|
|
31
|
+
it('matches "create a signup form" to auth.signup', () => {
|
|
32
|
+
const { spec, confidence } = findIntentSpec('create a signup form for new users');
|
|
33
|
+
expect(spec?.id).toBe('auth.signup');
|
|
34
|
+
expect(confidence).toBe(1.0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('matches "sign in page" to auth.signin', () => {
|
|
38
|
+
const { spec, confidence } = findIntentSpec('build a sign in page with email and password');
|
|
39
|
+
expect(spec?.id).toBe('auth.signin');
|
|
40
|
+
expect(confidence).toBe(1.0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('matches "admin dashboard" to dashboard.analytics', () => {
|
|
44
|
+
const { spec, confidence } = findIntentSpec('an admin dashboard with charts and stats');
|
|
45
|
+
expect(spec?.id).toBe('dashboard.analytics');
|
|
46
|
+
expect(confidence).toBe(1.0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('matches "contact form" to form.contact', () => {
|
|
50
|
+
const { spec, confidence } = findIntentSpec('a contact us form with name email and message');
|
|
51
|
+
expect(spec?.id).toBe('form.contact');
|
|
52
|
+
expect(confidence).toBe(1.0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('matches "404 page" to errors.not-found', () => {
|
|
56
|
+
const { spec, confidence } = findIntentSpec('a 404 page not found error');
|
|
57
|
+
expect(spec?.id).toBe('errors.not-found');
|
|
58
|
+
expect(confidence).toBe(1.0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('matches "shopping cart" to commerce.cart', () => {
|
|
62
|
+
const { spec, confidence } = findIntentSpec('a shopping cart with line items');
|
|
63
|
+
expect(spec?.id).toBe('commerce.cart');
|
|
64
|
+
expect(confidence).toBe(1.0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns null + 0 confidence on unknown intent', () => {
|
|
68
|
+
const { spec, confidence } = findIntentSpec('a kaleidoscope of avian taxonomies');
|
|
69
|
+
expect(spec).toBeNull();
|
|
70
|
+
expect(confidence).toBe(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns null + 0 confidence on empty/invalid input', () => {
|
|
74
|
+
expect(findIntentSpec('').spec).toBeNull();
|
|
75
|
+
expect(findIntentSpec(null).spec).toBeNull();
|
|
76
|
+
expect(findIntentSpec(undefined).spec).toBeNull();
|
|
77
|
+
expect(findIntentSpec(42).spec).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('classifyIntent (alias)', () => {
|
|
82
|
+
it('classifies the same as findIntentSpec', () => {
|
|
83
|
+
const a = classifyIntent('sign up new user');
|
|
84
|
+
const b = findIntentSpec('sign up new user');
|
|
85
|
+
expect(a.spec?.id).toBe(b.spec?.id);
|
|
86
|
+
expect(a.confidence).toBe(b.confidence);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('scoreAgainstSpec', () => {
|
|
91
|
+
const signupSpec = INTENT_SPECS.find((s) => s.id === 'auth.signup');
|
|
92
|
+
|
|
93
|
+
it('returns clean pass for an A2UI tree with all required + no forbidden', () => {
|
|
94
|
+
const a2ui = {
|
|
95
|
+
nodes: [
|
|
96
|
+
{ component: 'Card', children: [
|
|
97
|
+
{ component: 'Input' },
|
|
98
|
+
{ component: 'Input' },
|
|
99
|
+
{ component: 'Input' },
|
|
100
|
+
{ component: 'Button' },
|
|
101
|
+
{ component: 'CheckBox' },
|
|
102
|
+
{ component: 'Text' },
|
|
103
|
+
{ component: 'Link' },
|
|
104
|
+
]},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
const verdict = scoreAgainstSpec(a2ui, signupSpec);
|
|
108
|
+
expect(verdict.verdict).toBe('pass');
|
|
109
|
+
expect(verdict.certainty).toBe(1.0);
|
|
110
|
+
expect(verdict.score).toBe(100);
|
|
111
|
+
expect(verdict.findings.required_missing).toEqual([]);
|
|
112
|
+
expect(verdict.findings.forbidden_present).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('returns clean fail when 2+ required missing', () => {
|
|
116
|
+
const a2ui = {
|
|
117
|
+
nodes: [{ component: 'Toast' }], // no Card, Input, Button
|
|
118
|
+
};
|
|
119
|
+
const verdict = scoreAgainstSpec(a2ui, signupSpec);
|
|
120
|
+
expect(verdict.verdict).toBe('fail');
|
|
121
|
+
expect(verdict.certainty).toBe(1.0);
|
|
122
|
+
expect(verdict.score).toBe(0);
|
|
123
|
+
expect(verdict.findings.required_missing.length).toBeGreaterThanOrEqual(2);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('returns partial when 1 required missing (ambiguous — defer to LLM)', () => {
|
|
127
|
+
const a2ui = {
|
|
128
|
+
nodes: [
|
|
129
|
+
{ component: 'Card', children: [
|
|
130
|
+
{ component: 'Input' },
|
|
131
|
+
{ component: 'Input' },
|
|
132
|
+
{ component: 'Input' },
|
|
133
|
+
{ component: 'Input' },
|
|
134
|
+
{ component: 'CheckBox' },
|
|
135
|
+
{ component: 'Text' },
|
|
136
|
+
{ component: 'Link' },
|
|
137
|
+
]},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
const verdict = scoreAgainstSpec(a2ui, signupSpec);
|
|
141
|
+
expect(verdict.verdict).toBe('partial');
|
|
142
|
+
expect(verdict.certainty).toBeLessThan(0.9); // signals LLM fallback
|
|
143
|
+
expect(verdict.findings.required_missing).toEqual(['Button']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('returns partial when forbidden present (even with all required)', () => {
|
|
147
|
+
const a2ui = {
|
|
148
|
+
nodes: [
|
|
149
|
+
{ component: 'Card' },
|
|
150
|
+
{ component: 'Input' },
|
|
151
|
+
{ component: 'Input' },
|
|
152
|
+
{ component: 'Input' },
|
|
153
|
+
{ component: 'Button' },
|
|
154
|
+
{ component: 'Toast' }, // forbidden
|
|
155
|
+
{ component: 'CheckBox' },
|
|
156
|
+
{ component: 'Text' },
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
const verdict = scoreAgainstSpec(a2ui, signupSpec);
|
|
160
|
+
expect(verdict.verdict).toBe('partial');
|
|
161
|
+
expect(verdict.findings.forbidden_present).toContain('Toast');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('returns partial when below minComponents (even with all required)', () => {
|
|
165
|
+
const a2ui = {
|
|
166
|
+
nodes: [
|
|
167
|
+
{ component: 'Card' },
|
|
168
|
+
{ component: 'Input' },
|
|
169
|
+
{ component: 'Button' },
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
const verdict = scoreAgainstSpec(a2ui, signupSpec);
|
|
173
|
+
expect(verdict.verdict).toBe('partial');
|
|
174
|
+
expect(verdict.findings.component_count).toBeLessThan(signupSpec.minComponents);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('walks nested A2UI trees (children + nodes + body)', () => {
|
|
178
|
+
const a2ui = {
|
|
179
|
+
body: {
|
|
180
|
+
children: [
|
|
181
|
+
{ component: 'Card', nodes: [
|
|
182
|
+
{ component: 'Input' },
|
|
183
|
+
{ component: 'Button' },
|
|
184
|
+
]},
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
const verdict = scoreAgainstSpec(a2ui, signupSpec);
|
|
189
|
+
// Card, Input, Button found — but Input only once + below min
|
|
190
|
+
expect(verdict.findings.component_count).toBe(3);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IntentSpec Registry — Phase 3 kickoff (semantic-validator)
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/semantic-validator.md § 5.1, § Phase 3
|
|
5
|
+
*
|
|
6
|
+
* An IntentSpec is a deterministic recipe for one common intent class.
|
|
7
|
+
* The classify-intent.js classifier matches user prompts against these
|
|
8
|
+
* specs; high-confidence matches (confidence ≥ 0.9) can bypass the LLM
|
|
9
|
+
* judge entirely.
|
|
10
|
+
*
|
|
11
|
+
* Status: kickoff — 10 IntentSpec entries covering the dominant intent
|
|
12
|
+
* classes from the existing keyword map in validator.js. NOT YET wired
|
|
13
|
+
* into validateSemantics() (Phase 3 integration deferred to a follow-up
|
|
14
|
+
* cut once shadow-compare runs prove ≥95% agreement with the LLM judge).
|
|
15
|
+
*
|
|
16
|
+
* Schema (subset of § 5.2 in the spec):
|
|
17
|
+
*
|
|
18
|
+
* IntentSpec {
|
|
19
|
+
* id: string // e.g. 'auth.signup'
|
|
20
|
+
* keywords: RegExp // bag-of-words match
|
|
21
|
+
* required: string[] // component types that MUST appear
|
|
22
|
+
* forbidden: string[] // component types that MUST NOT appear
|
|
23
|
+
* minComponents:number // structural floor
|
|
24
|
+
* domain: 'data-entry' | 'data-display' | 'action' | 'identity' | 'navigation' | 'media' | 'layout' | 'notification' | 'overlay'
|
|
25
|
+
* description: string // for catalog / docs
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* Adding a new spec:
|
|
29
|
+
* 1. Author the entry below
|
|
30
|
+
* 2. Add a row to the test file: `classify-intent.test.js`
|
|
31
|
+
* 3. Run shadow-compare for 1 week (Phase 3 exit criteria § Phase 3)
|
|
32
|
+
* 4. Promote to fast-path if rule verdict agrees with LLM verdict ≥ 95%
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** @type {Array<IntentSpec>} */
|
|
36
|
+
export const INTENT_SPECS = [
|
|
37
|
+
{
|
|
38
|
+
id: 'auth.signup',
|
|
39
|
+
keywords: /\b(?:sign[\s-]*up|register|create\s+account|new\s+user)\b/i,
|
|
40
|
+
required: ['Card', 'Input', 'Button'],
|
|
41
|
+
forbidden: ['Toast', 'Alert'], // a signup page should not be primarily a toast/alert
|
|
42
|
+
minComponents: 7,
|
|
43
|
+
domain: 'data-entry',
|
|
44
|
+
description: 'New-user account creation flow — email/password Card with explicit submit Button',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'auth.signin',
|
|
48
|
+
keywords: /\b(?:sign[\s-]*in|log[\s-]*in|login)\b/i,
|
|
49
|
+
required: ['Card', 'Input', 'Button'],
|
|
50
|
+
forbidden: ['Toast'],
|
|
51
|
+
minComponents: 6,
|
|
52
|
+
domain: 'data-entry',
|
|
53
|
+
description: 'Authentication form — credentials Card with submit Button',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'auth.password-reset',
|
|
57
|
+
keywords: /\b(?:reset\s+password|forgot\s+password|recover\s+account)\b/i,
|
|
58
|
+
required: ['Card', 'Input', 'Button'],
|
|
59
|
+
forbidden: ['Toast'],
|
|
60
|
+
minComponents: 5,
|
|
61
|
+
domain: 'data-entry',
|
|
62
|
+
description: 'Password reset / forgot-password flow — single-email-input Card',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'dashboard.analytics',
|
|
66
|
+
keywords: /\b(?:dashboard|analytics|metrics|admin\s+dashboard)\b/i,
|
|
67
|
+
required: ['Grid', 'Card'],
|
|
68
|
+
forbidden: ['Toast', 'Alert'],
|
|
69
|
+
minComponents: 10,
|
|
70
|
+
domain: 'data-display',
|
|
71
|
+
description: 'Metrics dashboard — Grid of Stat/Chart Cards with optional Nav',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'data-table.list',
|
|
75
|
+
keywords: /\b(?:table|list|directory|roster|catalog|index)\b/i,
|
|
76
|
+
required: ['Table'],
|
|
77
|
+
forbidden: ['Toast'],
|
|
78
|
+
minComponents: 4,
|
|
79
|
+
domain: 'data-display',
|
|
80
|
+
description: 'Tabular data list — sortable/filterable Table',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'form.settings',
|
|
84
|
+
keywords: /\b(?:settings|preferences|configuration|account\s+settings|profile\s+settings)\b/i,
|
|
85
|
+
required: ['Form', 'Input', 'Button'],
|
|
86
|
+
forbidden: ['Toast'],
|
|
87
|
+
minComponents: 8,
|
|
88
|
+
domain: 'data-entry',
|
|
89
|
+
description: 'Multi-field settings/preferences Form with explicit save Button',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'form.contact',
|
|
93
|
+
keywords: /\b(?:contact\s+(?:us|form)|reach\s+out|get\s+in\s+touch|inquiry\s+form)\b/i,
|
|
94
|
+
required: ['Form', 'Input', 'TextArea', 'Button'],
|
|
95
|
+
forbidden: ['Toast'],
|
|
96
|
+
minComponents: 6,
|
|
97
|
+
domain: 'data-entry',
|
|
98
|
+
description: 'Contact form — name/email/message TextArea with submit Button',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'chat.conversation',
|
|
102
|
+
keywords: /\b(?:chat|conversation|messaging|messenger)\b/i,
|
|
103
|
+
required: ['Input', 'Button'],
|
|
104
|
+
forbidden: ['Table', 'Grid'],
|
|
105
|
+
minComponents: 4,
|
|
106
|
+
domain: 'data-entry',
|
|
107
|
+
description: 'Conversational chat surface — Input + send Button stream',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'commerce.cart',
|
|
111
|
+
keywords: /\b(?:cart|checkout|basket|shopping\s+cart)\b/i,
|
|
112
|
+
required: ['Card', 'Button'],
|
|
113
|
+
forbidden: ['Toast'],
|
|
114
|
+
minComponents: 6,
|
|
115
|
+
domain: 'data-display',
|
|
116
|
+
description: 'Shopping cart — line-item Cards with checkout Button',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'errors.not-found',
|
|
120
|
+
keywords: /\b(?:404|not\s+found|page\s+not\s+found|page\s+missing)\b/i,
|
|
121
|
+
required: ['Card', 'Button'],
|
|
122
|
+
forbidden: ['Form', 'Table'],
|
|
123
|
+
minComponents: 3,
|
|
124
|
+
domain: 'action',
|
|
125
|
+
description: '404 error page — reassurance Card with back-to-home Button',
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Find the IntentSpec matching the prompt (if any), with a confidence score.
|
|
131
|
+
*
|
|
132
|
+
* Confidence semantics:
|
|
133
|
+
* - 1.0 = exact keyword regex match
|
|
134
|
+
* - 0.0 = no match
|
|
135
|
+
*
|
|
136
|
+
* Returns { spec, confidence } or { spec: null, confidence: 0 }.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} intent — natural-language intent prompt
|
|
139
|
+
* @returns {{ spec: IntentSpec | null, confidence: number }}
|
|
140
|
+
*/
|
|
141
|
+
export function findIntentSpec(intent) {
|
|
142
|
+
if (!intent || typeof intent !== 'string') {
|
|
143
|
+
return { spec: null, confidence: 0 };
|
|
144
|
+
}
|
|
145
|
+
for (const spec of INTENT_SPECS) {
|
|
146
|
+
if (spec.keywords.test(intent)) {
|
|
147
|
+
return { spec, confidence: 1.0 };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { spec: null, confidence: 0 };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @typedef {object} IntentSpec
|
|
155
|
+
* @property {string} id — dot-namespaced identifier
|
|
156
|
+
* @property {RegExp} keywords — pattern that matches user prompts
|
|
157
|
+
* @property {string[]} required — A2UI component types that MUST appear
|
|
158
|
+
* @property {string[]} forbidden — component types that MUST NOT appear
|
|
159
|
+
* @property {number} minComponents — minimum total component count
|
|
160
|
+
* @property {string} domain — semantic category
|
|
161
|
+
* @property {string} description — human-readable summary
|
|
162
|
+
*/
|