@lokascript/framework 2.0.0
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/LICENSE +20 -0
- package/README.md +142 -0
- package/dist/aot/aot-orchestrator.d.ts +75 -0
- package/dist/aot/aot-orchestrator.d.ts.map +1 -0
- package/dist/aot/domain-scanner.d.ts +27 -0
- package/dist/aot/domain-scanner.d.ts.map +1 -0
- package/dist/aot/index.d.ts +8 -0
- package/dist/aot/index.d.ts.map +1 -0
- package/dist/aot/types.d.ts +103 -0
- package/dist/aot/types.d.ts.map +1 -0
- package/dist/api/create-dsl.d.ts +91 -0
- package/dist/api/create-dsl.d.ts.map +1 -0
- package/dist/api/dispatcher.d.ts +108 -0
- package/dist/api/dispatcher.d.ts.map +1 -0
- package/dist/api/domain-registry.d.ts +152 -0
- package/dist/api/domain-registry.d.ts.map +1 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +2082 -0
- package/dist/api/index.js.map +1 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +2674 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +32 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/pattern-matching/index.d.ts +6 -0
- package/dist/core/pattern-matching/index.d.ts.map +1 -0
- package/dist/core/pattern-matching/index.js +1239 -0
- package/dist/core/pattern-matching/index.js.map +1 -0
- package/dist/core/pattern-matching/pattern-matcher.d.ts +239 -0
- package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -0
- package/dist/core/pattern-matching/utils/index.d.ts +6 -0
- package/dist/core/pattern-matching/utils/index.d.ts.map +1 -0
- package/dist/core/pattern-matching/utils/possessive-keywords.d.ts +38 -0
- package/dist/core/pattern-matching/utils/possessive-keywords.d.ts.map +1 -0
- package/dist/core/pattern-matching/utils/type-validation.d.ts +63 -0
- package/dist/core/pattern-matching/utils/type-validation.d.ts.map +1 -0
- package/dist/core/tokenization/base-tokenizer.d.ts +344 -0
- package/dist/core/tokenization/base-tokenizer.d.ts.map +1 -0
- package/dist/core/tokenization/char-classifiers.d.ts +56 -0
- package/dist/core/tokenization/char-classifiers.d.ts.map +1 -0
- package/dist/core/tokenization/default-extractors.d.ts +48 -0
- package/dist/core/tokenization/default-extractors.d.ts.map +1 -0
- package/dist/core/tokenization/extractors/index.d.ts +9 -0
- package/dist/core/tokenization/extractors/index.d.ts.map +1 -0
- package/dist/core/tokenization/extractors/operator.d.ts +23 -0
- package/dist/core/tokenization/extractors/operator.d.ts.map +1 -0
- package/dist/core/tokenization/extractors/punctuation.d.ts +22 -0
- package/dist/core/tokenization/extractors/punctuation.d.ts.map +1 -0
- package/dist/core/tokenization/extractors.d.ts +61 -0
- package/dist/core/tokenization/extractors.d.ts.map +1 -0
- package/dist/core/tokenization/index.d.ts +11 -0
- package/dist/core/tokenization/index.d.ts.map +1 -0
- package/dist/core/tokenization/index.js +1345 -0
- package/dist/core/tokenization/index.js.map +1 -0
- package/dist/core/tokenization/morphology/index.d.ts +5 -0
- package/dist/core/tokenization/morphology/index.d.ts.map +1 -0
- package/dist/core/tokenization/morphology/types.d.ts +110 -0
- package/dist/core/tokenization/morphology/types.d.ts.map +1 -0
- package/dist/core/tokenization/token-utils.d.ts +111 -0
- package/dist/core/tokenization/token-utils.d.ts.map +1 -0
- package/dist/core/types.d.ts +382 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +108 -0
- package/dist/core/types.js.map +1 -0
- package/dist/generation/diagnostics.d.ts +120 -0
- package/dist/generation/diagnostics.d.ts.map +1 -0
- package/dist/generation/index.d.ts +7 -0
- package/dist/generation/index.d.ts.map +1 -0
- package/dist/generation/index.js +339 -0
- package/dist/generation/index.js.map +1 -0
- package/dist/generation/pattern-generator.d.ts +48 -0
- package/dist/generation/pattern-generator.d.ts.map +1 -0
- package/dist/generation/renderer.d.ts +115 -0
- package/dist/generation/renderer.d.ts.map +1 -0
- package/dist/grammar/index.d.ts +10 -0
- package/dist/grammar/index.d.ts.map +1 -0
- package/dist/grammar/index.js +391 -0
- package/dist/grammar/index.js.map +1 -0
- package/dist/grammar/transformer.d.ts +56 -0
- package/dist/grammar/transformer.d.ts.map +1 -0
- package/dist/grammar/types.d.ts +236 -0
- package/dist/grammar/types.d.ts.map +1 -0
- package/dist/index.cjs +4454 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4336 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/dictionary.d.ts +82 -0
- package/dist/interfaces/dictionary.d.ts.map +1 -0
- package/dist/interfaces/index.d.ts +10 -0
- package/dist/interfaces/index.d.ts.map +1 -0
- package/dist/interfaces/profile-provider.d.ts +67 -0
- package/dist/interfaces/profile-provider.d.ts.map +1 -0
- package/dist/interfaces/value-extractor.d.ts +168 -0
- package/dist/interfaces/value-extractor.d.ts.map +1 -0
- package/dist/multilingual/index.d.ts +8 -0
- package/dist/multilingual/index.d.ts.map +1 -0
- package/dist/multilingual/index.js +1 -0
- package/dist/multilingual/index.js.map +1 -0
- package/dist/parsing/index.d.ts +8 -0
- package/dist/parsing/index.d.ts.map +1 -0
- package/dist/parsing/index.js +1415 -0
- package/dist/parsing/index.js.map +1 -0
- package/dist/parsing/multi-statement.d.ts +265 -0
- package/dist/parsing/multi-statement.d.ts.map +1 -0
- package/dist/schema/command-schema.d.ts +78 -0
- package/dist/schema/command-schema.d.ts.map +1 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +25 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/test-setup.d.ts +9 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/testing/index.d.ts +50 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +16969 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +122 -0
- package/src/__test__/fixtures/sql-dsl.ts +232 -0
- package/src/__test__/sql-integration.test.ts +189 -0
- package/src/__test__/test-utils.ts +260 -0
- package/src/aot/aot-orchestrator.test.ts +413 -0
- package/src/aot/aot-orchestrator.ts +238 -0
- package/src/aot/domain-scanner.ts +178 -0
- package/src/aot/index.ts +8 -0
- package/src/aot/types.ts +124 -0
- package/src/api/create-dsl.ts +367 -0
- package/src/api/dispatcher.test.ts +336 -0
- package/src/api/dispatcher.ts +222 -0
- package/src/api/domain-registry.test.ts +336 -0
- package/src/api/domain-registry.ts +500 -0
- package/src/api/index.ts +7 -0
- package/src/core/index.ts +7 -0
- package/src/core/logger.ts +130 -0
- package/src/core/pattern-matching/index.ts +6 -0
- package/src/core/pattern-matching/pattern-matcher.test.ts +900 -0
- package/src/core/pattern-matching/pattern-matcher.ts +1548 -0
- package/src/core/pattern-matching/pattern-matcher.ts.backup +1267 -0
- package/src/core/pattern-matching/utils/index.ts +6 -0
- package/src/core/pattern-matching/utils/possessive-keywords.ts +55 -0
- package/src/core/pattern-matching/utils/type-validation.test.ts +316 -0
- package/src/core/pattern-matching/utils/type-validation.ts +134 -0
- package/src/core/tokenization/base-tokenizer.ts +916 -0
- package/src/core/tokenization/char-classifiers.ts +79 -0
- package/src/core/tokenization/create-simple-tokenizer.test.ts +260 -0
- package/src/core/tokenization/default-extractors.ts +69 -0
- package/src/core/tokenization/extractors/index.ts +9 -0
- package/src/core/tokenization/extractors/operator.ts +75 -0
- package/src/core/tokenization/extractors/punctuation.ts +39 -0
- package/src/core/tokenization/extractors.ts +452 -0
- package/src/core/tokenization/index.ts +11 -0
- package/src/core/tokenization/morphology/index.ts +5 -0
- package/src/core/tokenization/morphology/types.ts +211 -0
- package/src/core/tokenization/token-utils.ts +252 -0
- package/src/core/types.ts +589 -0
- package/src/generation/diagnostics.test.ts +171 -0
- package/src/generation/diagnostics.ts +239 -0
- package/src/generation/index.ts +7 -0
- package/src/generation/pattern-generator.test.ts +430 -0
- package/src/generation/pattern-generator.ts +315 -0
- package/src/generation/renderer.test.ts +266 -0
- package/src/generation/renderer.ts +244 -0
- package/src/grammar/index.ts +12 -0
- package/src/grammar/transformer.ts +159 -0
- package/src/grammar/types.ts +630 -0
- package/src/index.ts +157 -0
- package/src/interfaces/dictionary.ts +123 -0
- package/src/interfaces/index.ts +10 -0
- package/src/interfaces/profile-provider.ts +88 -0
- package/src/interfaces/value-extractor.ts +435 -0
- package/src/multilingual/index.ts +9 -0
- package/src/parsing/index.ts +27 -0
- package/src/parsing/multi-statement.test.ts +480 -0
- package/src/parsing/multi-statement.ts +648 -0
- package/src/schema/command-schema.ts +118 -0
- package/src/schema/index.ts +5 -0
- package/src/test-setup.ts +45 -0
- package/src/testing/index.ts +137 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Domain Dispatcher
|
|
3
|
+
*
|
|
4
|
+
* Routes natural-language input to the correct domain by trying all registered
|
|
5
|
+
* domains and picking the highest-confidence match. Supports auto-detection,
|
|
6
|
+
* composite multi-line parsing, and confidence-based routing.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { DomainRegistry, CrossDomainDispatcher } from '@lokascript/framework';
|
|
11
|
+
*
|
|
12
|
+
* const registry = new DomainRegistry();
|
|
13
|
+
* registry.register(sqlDescriptor);
|
|
14
|
+
* registry.register(bddDescriptor);
|
|
15
|
+
*
|
|
16
|
+
* const dispatcher = new CrossDomainDispatcher(registry);
|
|
17
|
+
*
|
|
18
|
+
* // Auto-detect which domain handles the input
|
|
19
|
+
* const result = await dispatcher.detect('select name from users', 'en');
|
|
20
|
+
* // → { domain: 'sql', node: ..., confidence: 0.95, dsl: ... }
|
|
21
|
+
*
|
|
22
|
+
* // Parse multi-line input across domains
|
|
23
|
+
* const composite = await dispatcher.parseComposite(`
|
|
24
|
+
* select name from users
|
|
25
|
+
* given page http://localhost
|
|
26
|
+
* `, 'en');
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { SemanticNode } from '../core/types';
|
|
31
|
+
import type { MultilingualDSL, CompileResult, ValidationResult } from './create-dsl';
|
|
32
|
+
import { DomainRegistry } from './domain-registry';
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Types
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
/** Result of domain auto-detection */
|
|
39
|
+
export interface DispatchResult {
|
|
40
|
+
/** The domain that matched */
|
|
41
|
+
readonly domain: string;
|
|
42
|
+
/** Parsed semantic node */
|
|
43
|
+
readonly node: SemanticNode;
|
|
44
|
+
/** Confidence score (0-1) */
|
|
45
|
+
readonly confidence: number;
|
|
46
|
+
/** The DSL instance that parsed it */
|
|
47
|
+
readonly dsl: MultilingualDSL;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A single statement in a composite parse result */
|
|
51
|
+
export interface CompositeStatement {
|
|
52
|
+
readonly line: number;
|
|
53
|
+
readonly input: string;
|
|
54
|
+
readonly domain: string;
|
|
55
|
+
readonly node: SemanticNode;
|
|
56
|
+
readonly confidence: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** An error from a composite parse */
|
|
60
|
+
export interface CompositeError {
|
|
61
|
+
readonly line: number;
|
|
62
|
+
readonly input: string;
|
|
63
|
+
readonly message: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Result of multi-domain composite parsing */
|
|
67
|
+
export interface CompositeParseResult {
|
|
68
|
+
/** Successfully parsed statements */
|
|
69
|
+
readonly statements: readonly CompositeStatement[];
|
|
70
|
+
/** Lines that could not be matched to any domain */
|
|
71
|
+
readonly errors: readonly CompositeError[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Dispatcher configuration options */
|
|
75
|
+
export interface DispatcherOptions {
|
|
76
|
+
/** Minimum confidence to accept a match (default: 0.5) */
|
|
77
|
+
readonly minConfidence?: number;
|
|
78
|
+
/** Domain priority order — earlier domains win on confidence ties */
|
|
79
|
+
readonly priority?: readonly string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// Comment line detection
|
|
84
|
+
// =============================================================================
|
|
85
|
+
|
|
86
|
+
const COMMENT_PATTERN = /^\s*(\/\/|--|#)/;
|
|
87
|
+
|
|
88
|
+
function isSkippableLine(line: string): boolean {
|
|
89
|
+
const trimmed = line.trim();
|
|
90
|
+
return trimmed === '' || COMMENT_PATTERN.test(trimmed);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// CrossDomainDispatcher
|
|
95
|
+
// =============================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Dispatcher that routes input to the correct domain by confidence scoring.
|
|
99
|
+
*
|
|
100
|
+
* Built on top of DomainRegistry — uses registered domains' DSL instances
|
|
101
|
+
* to try parsing input and picks the highest-confidence match.
|
|
102
|
+
*/
|
|
103
|
+
export class CrossDomainDispatcher {
|
|
104
|
+
private registry: DomainRegistry;
|
|
105
|
+
private minConfidence: number;
|
|
106
|
+
private priority: readonly string[];
|
|
107
|
+
|
|
108
|
+
constructor(registry: DomainRegistry, options?: DispatcherOptions) {
|
|
109
|
+
this.registry = registry;
|
|
110
|
+
this.minConfidence = options?.minConfidence ?? 0.5;
|
|
111
|
+
this.priority = options?.priority ?? [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Auto-detect which domain handles the input.
|
|
116
|
+
* Tries all registered domains and returns the highest-confidence match.
|
|
117
|
+
* Returns null if no domain matches above minConfidence.
|
|
118
|
+
*/
|
|
119
|
+
async detect(input: string, language = 'en'): Promise<DispatchResult | null> {
|
|
120
|
+
const domainNames = this.registry.getDomainNames();
|
|
121
|
+
if (domainNames.length === 0) return null;
|
|
122
|
+
|
|
123
|
+
const candidates: DispatchResult[] = [];
|
|
124
|
+
|
|
125
|
+
for (const name of domainNames) {
|
|
126
|
+
const dsl = await this.registry.getDSLForDomain(name);
|
|
127
|
+
if (!dsl) continue;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const { node, confidence } = dsl.parseWithConfidence(input, language);
|
|
131
|
+
if (confidence >= this.minConfidence) {
|
|
132
|
+
candidates.push({ domain: name, node, confidence, dsl });
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// Domain could not parse this input — skip
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (candidates.length === 0) return null;
|
|
140
|
+
|
|
141
|
+
// Sort by confidence descending, break ties by priority order
|
|
142
|
+
candidates.sort((a, b) => {
|
|
143
|
+
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
144
|
+
return this.getPriorityIndex(a.domain) - this.getPriorityIndex(b.domain);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return candidates[0];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parse multi-line input where each line may belong to a different domain.
|
|
152
|
+
* Empty lines and comment lines (starting with //, --, or #) are skipped.
|
|
153
|
+
*/
|
|
154
|
+
async parseComposite(input: string, language = 'en'): Promise<CompositeParseResult> {
|
|
155
|
+
const lines = input.split('\n');
|
|
156
|
+
const statements: CompositeStatement[] = [];
|
|
157
|
+
const errors: CompositeError[] = [];
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < lines.length; i++) {
|
|
160
|
+
const line = lines[i];
|
|
161
|
+
if (isSkippableLine(line)) continue;
|
|
162
|
+
|
|
163
|
+
const trimmed = line.trim();
|
|
164
|
+
const result = await this.detect(trimmed, language);
|
|
165
|
+
|
|
166
|
+
if (result) {
|
|
167
|
+
statements.push({
|
|
168
|
+
line: i + 1,
|
|
169
|
+
input: trimmed,
|
|
170
|
+
domain: result.domain,
|
|
171
|
+
node: result.node,
|
|
172
|
+
confidence: result.confidence,
|
|
173
|
+
});
|
|
174
|
+
} else {
|
|
175
|
+
errors.push({
|
|
176
|
+
line: i + 1,
|
|
177
|
+
input: trimmed,
|
|
178
|
+
message: 'No domain matched this input',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { statements, errors };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Compile input using auto-detected domain.
|
|
188
|
+
* Returns the compilation result with the domain name.
|
|
189
|
+
*/
|
|
190
|
+
async compile(
|
|
191
|
+
input: string,
|
|
192
|
+
language = 'en'
|
|
193
|
+
): Promise<(CompileResult & { domain: string }) | null> {
|
|
194
|
+
const detected = await this.detect(input, language);
|
|
195
|
+
if (!detected) return null;
|
|
196
|
+
|
|
197
|
+
const result = detected.dsl.compile(input, language);
|
|
198
|
+
return { ...result, domain: detected.domain };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validate input against all domains, returning the best match.
|
|
203
|
+
*/
|
|
204
|
+
async validate(input: string, language = 'en'): Promise<ValidationResult & { domain?: string }> {
|
|
205
|
+
const detected = await this.detect(input, language);
|
|
206
|
+
if (!detected) {
|
|
207
|
+
return { valid: false, errors: ['No domain matched this input'] };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const result = detected.dsl.validate(input, language);
|
|
211
|
+
return { ...result, domain: detected.domain };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Private
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
private getPriorityIndex(domain: string): number {
|
|
219
|
+
const idx = this.priority.indexOf(domain);
|
|
220
|
+
return idx === -1 ? this.priority.length : idx;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { DomainRegistry } from './domain-registry';
|
|
3
|
+
import type { DomainDescriptor, MCPToolDefinition } from './domain-registry';
|
|
4
|
+
import type { MultilingualDSL } from './create-dsl';
|
|
5
|
+
import type { SemanticNode } from '../core/types';
|
|
6
|
+
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Test Helpers
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
function createMockDSL(): MultilingualDSL {
|
|
12
|
+
return {
|
|
13
|
+
parse(input: string, language: string): SemanticNode {
|
|
14
|
+
return {
|
|
15
|
+
kind: 'command',
|
|
16
|
+
action: input.split(/\s+/)[0].toLowerCase(),
|
|
17
|
+
roles: new Map([['raw', { type: 'expression' as const, raw: input }]]),
|
|
18
|
+
metadata: { sourceLanguage: language },
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
parseWithConfidence(input: string, language: string) {
|
|
22
|
+
return { node: this.parse(input, language), confidence: 0.9 };
|
|
23
|
+
},
|
|
24
|
+
validate(input: string, language: string) {
|
|
25
|
+
try {
|
|
26
|
+
this.parse(input, language);
|
|
27
|
+
return { valid: true };
|
|
28
|
+
} catch {
|
|
29
|
+
return { valid: false, errors: ['Parse failed'] };
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
compile(input: string, language: string) {
|
|
33
|
+
try {
|
|
34
|
+
const node = this.parse(input, language);
|
|
35
|
+
return { ok: true, code: `compiled(${input})`, node };
|
|
36
|
+
} catch {
|
|
37
|
+
return { ok: false, errors: ['Compile failed'] };
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
translate(input: string, fromLanguage: string, toLanguage: string) {
|
|
41
|
+
return `[${toLanguage}] ${input}`;
|
|
42
|
+
},
|
|
43
|
+
getSupportedLanguages() {
|
|
44
|
+
return ['en', 'ja'];
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function mockRenderer(node: SemanticNode, language: string): string {
|
|
50
|
+
return `[${language}] ${node.action}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createTestDescriptor(overrides?: Partial<DomainDescriptor>): DomainDescriptor {
|
|
54
|
+
return {
|
|
55
|
+
name: 'test',
|
|
56
|
+
description: 'Test domain',
|
|
57
|
+
languages: ['en', 'ja'],
|
|
58
|
+
inputLabel: 'query',
|
|
59
|
+
inputDescription: 'Test query',
|
|
60
|
+
getDSL: () => createMockDSL(),
|
|
61
|
+
getRenderer: () => mockRenderer,
|
|
62
|
+
...overrides,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// =============================================================================
|
|
67
|
+
// Registration
|
|
68
|
+
// =============================================================================
|
|
69
|
+
|
|
70
|
+
describe('DomainRegistry', () => {
|
|
71
|
+
let registry: DomainRegistry;
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
registry = new DomainRegistry();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('register', () => {
|
|
78
|
+
it('registers a domain', () => {
|
|
79
|
+
registry.register(createTestDescriptor());
|
|
80
|
+
expect(registry.getDomainNames()).toEqual(['test']);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('throws on duplicate registration', () => {
|
|
84
|
+
registry.register(createTestDescriptor());
|
|
85
|
+
expect(() => registry.register(createTestDescriptor())).toThrow(
|
|
86
|
+
'Domain already registered: test'
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('registers multiple domains', () => {
|
|
91
|
+
registry.register(createTestDescriptor({ name: 'sql' }));
|
|
92
|
+
registry.register(createTestDescriptor({ name: 'bdd' }));
|
|
93
|
+
expect(registry.getDomainNames()).toEqual(['sql', 'bdd']);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('getDescriptor', () => {
|
|
98
|
+
it('returns descriptor for registered domain', () => {
|
|
99
|
+
registry.register(createTestDescriptor());
|
|
100
|
+
const desc = registry.getDescriptor('test');
|
|
101
|
+
expect(desc?.name).toBe('test');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns undefined for unknown domain', () => {
|
|
105
|
+
expect(registry.getDescriptor('nope')).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// Tool Definition Generation
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
113
|
+
describe('getToolDefinitions', () => {
|
|
114
|
+
it('generates 4 standard tools per domain', () => {
|
|
115
|
+
registry.register(createTestDescriptor());
|
|
116
|
+
const tools = registry.getToolDefinitions();
|
|
117
|
+
expect(tools).toHaveLength(4);
|
|
118
|
+
|
|
119
|
+
const names = tools.map(t => t.name);
|
|
120
|
+
expect(names).toContain('parse_test');
|
|
121
|
+
expect(names).toContain('compile_test');
|
|
122
|
+
expect(names).toContain('validate_test');
|
|
123
|
+
expect(names).toContain('translate_test');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('uses domain inputLabel in tool schemas', () => {
|
|
127
|
+
registry.register(createTestDescriptor({ inputLabel: 'scenario' }));
|
|
128
|
+
const tools = registry.getToolDefinitions();
|
|
129
|
+
const parseTool = tools.find(t => t.name === 'parse_test')!;
|
|
130
|
+
expect(parseTool.inputSchema.properties).toHaveProperty('scenario');
|
|
131
|
+
expect(parseTool.inputSchema.required).toContain('scenario');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('includes language list in descriptions', () => {
|
|
135
|
+
registry.register(createTestDescriptor({ languages: ['en', 'es', 'ja'] }));
|
|
136
|
+
const tools = registry.getToolDefinitions();
|
|
137
|
+
expect(tools[0].description).toContain('en, es, ja');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('respects tools subset', () => {
|
|
141
|
+
registry.register(createTestDescriptor({ tools: ['parse', 'validate'] }));
|
|
142
|
+
const tools = registry.getToolDefinitions();
|
|
143
|
+
expect(tools).toHaveLength(2);
|
|
144
|
+
const names = tools.map(t => t.name);
|
|
145
|
+
expect(names).toContain('parse_test');
|
|
146
|
+
expect(names).toContain('validate_test');
|
|
147
|
+
expect(names).not.toContain('compile_test');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('generates tools for multiple domains', () => {
|
|
151
|
+
registry.register(createTestDescriptor({ name: 'sql' }));
|
|
152
|
+
registry.register(createTestDescriptor({ name: 'bdd' }));
|
|
153
|
+
const tools = registry.getToolDefinitions();
|
|
154
|
+
expect(tools).toHaveLength(8); // 4 per domain
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// =============================================================================
|
|
159
|
+
// canHandle
|
|
160
|
+
// =============================================================================
|
|
161
|
+
|
|
162
|
+
describe('canHandle', () => {
|
|
163
|
+
it('returns true for registered domain tools', () => {
|
|
164
|
+
registry.register(createTestDescriptor({ name: 'sql' }));
|
|
165
|
+
expect(registry.canHandle('parse_sql')).toBe(true);
|
|
166
|
+
expect(registry.canHandle('compile_sql')).toBe(true);
|
|
167
|
+
expect(registry.canHandle('validate_sql')).toBe(true);
|
|
168
|
+
expect(registry.canHandle('translate_sql')).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('returns false for unregistered domains', () => {
|
|
172
|
+
expect(registry.canHandle('parse_sql')).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('returns false for non-matching tool names', () => {
|
|
176
|
+
registry.register(createTestDescriptor({ name: 'sql' }));
|
|
177
|
+
expect(registry.canHandle('analyze_complexity')).toBe(false);
|
|
178
|
+
expect(registry.canHandle('sql_parse')).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// =============================================================================
|
|
183
|
+
// Tool Call Dispatch
|
|
184
|
+
// =============================================================================
|
|
185
|
+
|
|
186
|
+
describe('handleToolCall', () => {
|
|
187
|
+
it('returns null for unrecognized tool names', async () => {
|
|
188
|
+
registry.register(createTestDescriptor());
|
|
189
|
+
const result = await registry.handleToolCall('unknown_tool', {});
|
|
190
|
+
expect(result).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('returns null for unregistered domains', async () => {
|
|
194
|
+
const result = await registry.handleToolCall('parse_sql', { query: 'test' });
|
|
195
|
+
expect(result).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('parse', () => {
|
|
199
|
+
it('parses input and returns semantic representation', async () => {
|
|
200
|
+
registry.register(createTestDescriptor());
|
|
201
|
+
const result = await registry.handleToolCall('parse_test', {
|
|
202
|
+
query: 'select name',
|
|
203
|
+
language: 'en',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(result).not.toBeNull();
|
|
207
|
+
const data = JSON.parse(result!.content[0].text);
|
|
208
|
+
expect(data.action).toBe('select');
|
|
209
|
+
expect(data.language).toBe('en');
|
|
210
|
+
expect(data.query).toBe('select name');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('returns error for missing input', async () => {
|
|
214
|
+
registry.register(createTestDescriptor());
|
|
215
|
+
const result = await registry.handleToolCall('parse_test', {});
|
|
216
|
+
expect(result!.isError).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('defaults language to en', async () => {
|
|
220
|
+
registry.register(createTestDescriptor());
|
|
221
|
+
const result = await registry.handleToolCall('parse_test', { query: 'test' });
|
|
222
|
+
const data = JSON.parse(result!.content[0].text);
|
|
223
|
+
expect(data.language).toBe('en');
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('compile', () => {
|
|
228
|
+
it('compiles input to target code', async () => {
|
|
229
|
+
registry.register(createTestDescriptor());
|
|
230
|
+
const result = await registry.handleToolCall('compile_test', {
|
|
231
|
+
query: 'select name',
|
|
232
|
+
language: 'en',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const data = JSON.parse(result!.content[0].text);
|
|
236
|
+
expect(data.ok).toBe(true);
|
|
237
|
+
expect(data.code).toBe('compiled(select name)');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('validate', () => {
|
|
242
|
+
it('validates input', async () => {
|
|
243
|
+
registry.register(createTestDescriptor());
|
|
244
|
+
const result = await registry.handleToolCall('validate_test', {
|
|
245
|
+
query: 'select name',
|
|
246
|
+
language: 'en',
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const data = JSON.parse(result!.content[0].text);
|
|
250
|
+
expect(data.valid).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('translate', () => {
|
|
255
|
+
it('translates between languages', async () => {
|
|
256
|
+
registry.register(createTestDescriptor());
|
|
257
|
+
const result = await registry.handleToolCall('translate_test', {
|
|
258
|
+
query: 'select name',
|
|
259
|
+
from: 'en',
|
|
260
|
+
to: 'ja',
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const data = JSON.parse(result!.content[0].text);
|
|
264
|
+
expect(data.input.query).toBe('select name');
|
|
265
|
+
expect(data.input.language).toBe('en');
|
|
266
|
+
expect(data.rendered.text).toBe('[ja] select');
|
|
267
|
+
expect(data.rendered.language).toBe('ja');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('returns error when from/to missing', async () => {
|
|
271
|
+
registry.register(createTestDescriptor());
|
|
272
|
+
|
|
273
|
+
const result1 = await registry.handleToolCall('translate_test', {
|
|
274
|
+
query: 'test',
|
|
275
|
+
to: 'ja',
|
|
276
|
+
});
|
|
277
|
+
expect(result1!.isError).toBe(true);
|
|
278
|
+
|
|
279
|
+
const result2 = await registry.handleToolCall('translate_test', {
|
|
280
|
+
query: 'test',
|
|
281
|
+
from: 'en',
|
|
282
|
+
});
|
|
283
|
+
expect(result2!.isError).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('works without renderer', async () => {
|
|
287
|
+
registry.register(createTestDescriptor({ getRenderer: undefined }));
|
|
288
|
+
const result = await registry.handleToolCall('translate_test', {
|
|
289
|
+
query: 'select name',
|
|
290
|
+
from: 'en',
|
|
291
|
+
to: 'ja',
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const data = JSON.parse(result!.content[0].text);
|
|
295
|
+
expect(data.semantic.action).toBe('select');
|
|
296
|
+
// No rendered field when renderer is missing
|
|
297
|
+
expect(data.rendered).toBeUndefined();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('error handling', () => {
|
|
302
|
+
it('catches and reports domain errors', async () => {
|
|
303
|
+
registry.register(
|
|
304
|
+
createTestDescriptor({
|
|
305
|
+
getDSL: () => {
|
|
306
|
+
throw new Error('Module not found');
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const result = await registry.handleToolCall('parse_test', { query: 'test' });
|
|
312
|
+
expect(result!.isError).toBe(true);
|
|
313
|
+
const data = JSON.parse(result!.content[0].text);
|
|
314
|
+
expect(data.error).toContain('Module not found');
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('DSL caching', () => {
|
|
319
|
+
it('reuses DSL instance across calls', async () => {
|
|
320
|
+
let callCount = 0;
|
|
321
|
+
registry.register(
|
|
322
|
+
createTestDescriptor({
|
|
323
|
+
getDSL: () => {
|
|
324
|
+
callCount++;
|
|
325
|
+
return createMockDSL();
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
await registry.handleToolCall('parse_test', { query: 'a' });
|
|
331
|
+
await registry.handleToolCall('parse_test', { query: 'b' });
|
|
332
|
+
expect(callCount).toBe(1);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|