@lokascript/framework 2.0.0 → 2.1.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/dist/aot/index.d.ts +1 -0
- package/dist/aot/index.d.ts.map +1 -1
- package/dist/aot/registry-bridge.d.ts +27 -0
- package/dist/aot/registry-bridge.d.ts.map +1 -0
- package/dist/api/create-dsl.d.ts.map +1 -1
- package/dist/api/dispatcher.d.ts +22 -0
- package/dist/api/dispatcher.d.ts.map +1 -1
- package/dist/api/domain-registry.d.ts +41 -0
- package/dist/api/domain-registry.d.ts.map +1 -1
- package/dist/api/index.js +1351 -21
- package/dist/api/index.js.map +1 -1
- package/dist/core/index.js +114 -10
- package/dist/core/index.js.map +1 -1
- package/dist/core/pattern-matching/index.js +74 -10
- package/dist/core/pattern-matching/index.js.map +1 -1
- package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -1
- package/dist/core/types.d.ts +80 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +40 -0
- package/dist/core/types.js.map +1 -1
- package/dist/feedback/confidence-gate.d.ts +28 -0
- package/dist/feedback/confidence-gate.d.ts.map +1 -0
- package/dist/feedback/feedback-formatter.d.ts +21 -0
- package/dist/feedback/feedback-formatter.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +11 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/pattern-tracker.d.ts +47 -0
- package/dist/feedback/pattern-tracker.d.ts.map +1 -0
- package/dist/feedback/types.d.ts +113 -0
- package/dist/feedback/types.d.ts.map +1 -0
- package/dist/generation/index.js +2 -1
- package/dist/generation/index.js.map +1 -1
- package/dist/index.cjs +2685 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2652 -71
- package/dist/index.js.map +1 -1
- package/dist/ir/explicit-parser.d.ts +31 -0
- package/dist/ir/explicit-parser.d.ts.map +1 -0
- package/dist/ir/explicit-renderer.d.ts +17 -0
- package/dist/ir/explicit-renderer.d.ts.map +1 -0
- package/dist/ir/from-interchange.d.ts +51 -0
- package/dist/ir/from-interchange.d.ts.map +1 -0
- package/dist/ir/index.d.ts +29 -0
- package/dist/ir/index.d.ts.map +1 -0
- package/dist/ir/index.js +1557 -0
- package/dist/ir/index.js.map +1 -0
- package/dist/ir/json-schema.d.ts +40 -0
- package/dist/ir/json-schema.d.ts.map +1 -0
- package/dist/ir/protocol-json.d.ts +73 -0
- package/dist/ir/protocol-json.d.ts.map +1 -0
- package/dist/ir/references.d.ts +19 -0
- package/dist/ir/references.d.ts.map +1 -0
- package/dist/ir/types.d.ts +141 -0
- package/dist/ir/types.d.ts.map +1 -0
- package/dist/parsing/index.js +74 -10
- package/dist/parsing/index.js.map +1 -1
- package/dist/prompts/index.d.ts +9 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/prompt-generator.d.ts +49 -0
- package/dist/prompts/prompt-generator.d.ts.map +1 -0
- package/dist/prompts/types.d.ts +64 -0
- package/dist/prompts/types.d.ts.map +1 -0
- package/dist/schema/command-schema.d.ts +17 -0
- package/dist/schema/command-schema.d.ts.map +1 -1
- package/dist/schema/index.js.map +1 -1
- package/dist/testing/index.js +3 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/training/index.d.ts +11 -0
- package/dist/training/index.d.ts.map +1 -0
- package/dist/training/jsonl-writer.d.ts +42 -0
- package/dist/training/jsonl-writer.d.ts.map +1 -0
- package/dist/training/schema-synthesizer.d.ts +20 -0
- package/dist/training/schema-synthesizer.d.ts.map +1 -0
- package/dist/training/types.d.ts +73 -0
- package/dist/training/types.d.ts.map +1 -0
- package/package.json +5 -1
- package/src/aot/index.ts +1 -0
- package/src/aot/registry-bridge.test.ts +104 -0
- package/src/aot/registry-bridge.ts +66 -0
- package/src/api/create-dsl.test.ts +306 -0
- package/src/api/create-dsl.ts +21 -0
- package/src/api/dispatcher.test.ts +66 -0
- package/src/api/dispatcher.ts +77 -9
- package/src/api/domain-registry.ts +120 -4
- package/src/core/pattern-matching/pattern-matcher.test.ts +131 -0
- package/src/core/pattern-matching/pattern-matcher.ts +102 -19
- package/src/core/types.ts +145 -2
- package/src/feedback/confidence-gate.test.ts +111 -0
- package/src/feedback/confidence-gate.ts +70 -0
- package/src/feedback/feedback-formatter.test.ts +198 -0
- package/src/feedback/feedback-formatter.ts +298 -0
- package/src/feedback/index.ts +24 -0
- package/src/feedback/pattern-tracker.test.ts +198 -0
- package/src/feedback/pattern-tracker.ts +140 -0
- package/src/feedback/types.ts +149 -0
- package/src/generation/pattern-generator.ts +2 -2
- package/src/index.ts +22 -0
- package/src/integration/llm-lse-integration.test.ts +457 -0
- package/src/ir/explicit-parser.test.ts +306 -0
- package/src/ir/explicit-parser.ts +419 -0
- package/src/ir/explicit-renderer.test.ts +144 -0
- package/src/ir/explicit-renderer.ts +146 -0
- package/src/ir/from-interchange.test.ts +853 -0
- package/src/ir/from-interchange.ts +594 -0
- package/src/ir/index.ts +66 -0
- package/src/ir/json-schema.test.ts +281 -0
- package/src/ir/json-schema.ts +299 -0
- package/src/ir/protocol-json.test.ts +1185 -0
- package/src/ir/protocol-json.ts +745 -0
- package/src/ir/references.test.ts +38 -0
- package/src/ir/references.ts +33 -0
- package/src/ir/round-trip.test.ts +557 -0
- package/src/ir/types.ts +181 -0
- package/src/prompts/index.ts +15 -0
- package/src/prompts/prompt-generator.test.ts +386 -0
- package/src/prompts/prompt-generator.ts +531 -0
- package/src/prompts/types.ts +89 -0
- package/src/schema/command-schema.ts +19 -0
- package/src/training/index.ts +13 -0
- package/src/training/jsonl-writer.test.ts +138 -0
- package/src/training/jsonl-writer.ts +67 -0
- package/src/training/schema-synthesizer.test.ts +250 -0
- package/src/training/schema-synthesizer.ts +306 -0
- package/src/training/types.ts +103 -0
|
@@ -210,7 +210,8 @@ export class PatternMatcher {
|
|
|
210
210
|
private matchTokenSequence(
|
|
211
211
|
tokens: TokenStream,
|
|
212
212
|
patternTokens: PatternToken[],
|
|
213
|
-
captured: Map<SemanticRole, SemanticValue
|
|
213
|
+
captured: Map<SemanticRole, SemanticValue>,
|
|
214
|
+
parentStopMarkers?: Set<string>
|
|
214
215
|
): boolean {
|
|
215
216
|
// Skip leading conjunctions for Arabic (proclitics: و, ف, ول, وب, etc.)
|
|
216
217
|
// BUT NOT if the pattern explicitly expects a conjunction (proclitic patterns)
|
|
@@ -251,6 +252,11 @@ export class PatternMatcher {
|
|
|
251
252
|
// recognized marker keyword or end of input
|
|
252
253
|
if (patternToken.type === 'role' && patternToken.greedy) {
|
|
253
254
|
const stopMarkers = this.collectStopMarkers(patternTokens, i + 1);
|
|
255
|
+
// Merge parent-level stop markers so greedy roles inside groups
|
|
256
|
+
// know when to stop for tokens outside the group (e.g., SOV verb keywords)
|
|
257
|
+
if (parentStopMarkers) {
|
|
258
|
+
for (const m of parentStopMarkers) stopMarkers.add(m);
|
|
259
|
+
}
|
|
254
260
|
const values: string[] = [];
|
|
255
261
|
while (!tokens.isAtEnd()) {
|
|
256
262
|
const nextToken = tokens.peek();
|
|
@@ -271,10 +277,78 @@ export class PatternMatcher {
|
|
|
271
277
|
}
|
|
272
278
|
}
|
|
273
279
|
|
|
280
|
+
// Group tokens: compute sibling stop markers and forward to inner matching
|
|
281
|
+
if (patternToken.type === 'group') {
|
|
282
|
+
const siblingStopMarkers = this.collectStopMarkers(patternTokens, i + 1);
|
|
283
|
+
if (parentStopMarkers) {
|
|
284
|
+
for (const m of parentStopMarkers) siblingStopMarkers.add(m);
|
|
285
|
+
}
|
|
286
|
+
const groupMatched = this.matchGroupToken(
|
|
287
|
+
tokens,
|
|
288
|
+
patternToken as PatternToken & { type: 'group' },
|
|
289
|
+
captured,
|
|
290
|
+
siblingStopMarkers
|
|
291
|
+
);
|
|
292
|
+
if (groupMatched) {
|
|
293
|
+
prevOptionalMark = null;
|
|
294
|
+
prevOptionalRole = null;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (this.isOptional(patternToken)) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
274
303
|
// Save stream position before attempting optional roles
|
|
275
304
|
const isOptionalRole = patternToken.type === 'role' && patternToken.optional === true;
|
|
276
305
|
const markBefore = isOptionalRole ? tokens.mark() : null;
|
|
277
306
|
|
|
307
|
+
// SOV marker-stealing guard: before matching a required role, check if
|
|
308
|
+
// the current input token would match the immediately-following pattern
|
|
309
|
+
// literal (the role's own postpositional marker). If so, skip consuming
|
|
310
|
+
// this token as a role value — it should be matched as the literal instead.
|
|
311
|
+
const isRoleToken = patternToken.type === 'role';
|
|
312
|
+
if (isRoleToken && !isOptionalRole) {
|
|
313
|
+
const nextPT = i + 1 < patternTokens.length ? patternTokens[i + 1] : null;
|
|
314
|
+
if (nextPT?.type === 'literal') {
|
|
315
|
+
const currentToken = tokens.peek();
|
|
316
|
+
if (currentToken && this.getMatchType(currentToken, nextPT.value) !== 'none') {
|
|
317
|
+
// Current token is the marker literal, not a role value — treat role as failed
|
|
318
|
+
this.logger.debug(
|
|
319
|
+
' >> MARKER-STEAL GUARD: current token matches next literal, skipping role'
|
|
320
|
+
);
|
|
321
|
+
// Fall through to failure handling below
|
|
322
|
+
// (which will trigger backtracking of previous optional role)
|
|
323
|
+
|
|
324
|
+
// Match failed
|
|
325
|
+
this.logger.debug(' >> Token match FAILED');
|
|
326
|
+
|
|
327
|
+
if (this.isOptional(patternToken)) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Required token failed — try backtracking over the previous optional role
|
|
332
|
+
if (prevOptionalMark && prevOptionalRole) {
|
|
333
|
+
this.logger.debug(' >> BACKTRACKING: undoing optional role', prevOptionalRole);
|
|
334
|
+
tokens.reset(prevOptionalMark);
|
|
335
|
+
captured.delete(prevOptionalRole);
|
|
336
|
+
prevOptionalMark = null;
|
|
337
|
+
prevOptionalRole = null;
|
|
338
|
+
|
|
339
|
+
// Retry the current (failed) pattern token from the restored position
|
|
340
|
+
const retryMatched = this.matchPatternToken(tokens, patternToken, captured);
|
|
341
|
+
this.logger.debug(' >> Backtrack retry result:', retryMatched);
|
|
342
|
+
if (retryMatched) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
278
352
|
const matched = this.matchPatternToken(tokens, patternToken, captured);
|
|
279
353
|
this.logger.debug(' >> Match result:', matched);
|
|
280
354
|
|
|
@@ -436,10 +510,7 @@ export class PatternMatcher {
|
|
|
436
510
|
if (possessiveValue) {
|
|
437
511
|
// Validate expected types if specified
|
|
438
512
|
if (patternToken.expectedTypes && patternToken.expectedTypes.length > 0) {
|
|
439
|
-
if (
|
|
440
|
-
!patternToken.expectedTypes.includes(possessiveValue.type) &&
|
|
441
|
-
!patternToken.expectedTypes.includes('expression')
|
|
442
|
-
) {
|
|
513
|
+
if (!isTypeCompatible(possessiveValue.type, patternToken.expectedTypes)) {
|
|
443
514
|
return patternToken.optional || false;
|
|
444
515
|
}
|
|
445
516
|
}
|
|
@@ -451,10 +522,7 @@ export class PatternMatcher {
|
|
|
451
522
|
const methodCallValue = this.tryMatchMethodCallExpression(tokens);
|
|
452
523
|
if (methodCallValue) {
|
|
453
524
|
if (patternToken.expectedTypes && patternToken.expectedTypes.length > 0) {
|
|
454
|
-
if (
|
|
455
|
-
!patternToken.expectedTypes.includes(methodCallValue.type) &&
|
|
456
|
-
!patternToken.expectedTypes.includes('expression')
|
|
457
|
-
) {
|
|
525
|
+
if (!isTypeCompatible(methodCallValue.type, patternToken.expectedTypes)) {
|
|
458
526
|
return patternToken.optional || false;
|
|
459
527
|
}
|
|
460
528
|
}
|
|
@@ -479,10 +547,7 @@ export class PatternMatcher {
|
|
|
479
547
|
const propertyAccessValue = this.tryMatchPropertyAccessExpression(tokens);
|
|
480
548
|
if (propertyAccessValue) {
|
|
481
549
|
if (patternToken.expectedTypes && patternToken.expectedTypes.length > 0) {
|
|
482
|
-
if (
|
|
483
|
-
!patternToken.expectedTypes.includes(propertyAccessValue.type) &&
|
|
484
|
-
!patternToken.expectedTypes.includes('expression')
|
|
485
|
-
) {
|
|
550
|
+
if (!isTypeCompatible(propertyAccessValue.type, patternToken.expectedTypes)) {
|
|
486
551
|
return patternToken.optional || false;
|
|
487
552
|
}
|
|
488
553
|
}
|
|
@@ -928,12 +993,18 @@ export class PatternMatcher {
|
|
|
928
993
|
private matchGroupToken(
|
|
929
994
|
tokens: TokenStream,
|
|
930
995
|
patternToken: PatternToken & { type: 'group' },
|
|
931
|
-
captured: Map<SemanticRole, SemanticValue
|
|
996
|
+
captured: Map<SemanticRole, SemanticValue>,
|
|
997
|
+
parentStopMarkers?: Set<string>
|
|
932
998
|
): boolean {
|
|
933
999
|
const mark = tokens.mark();
|
|
934
1000
|
const capturedBefore = new Set(captured.keys());
|
|
935
1001
|
|
|
936
|
-
const success = this.matchTokenSequence(
|
|
1002
|
+
const success = this.matchTokenSequence(
|
|
1003
|
+
tokens,
|
|
1004
|
+
patternToken.tokens,
|
|
1005
|
+
captured,
|
|
1006
|
+
parentStopMarkers
|
|
1007
|
+
);
|
|
937
1008
|
if (success) return true;
|
|
938
1009
|
|
|
939
1010
|
// Reset from failed attempt
|
|
@@ -965,7 +1036,12 @@ export class PatternMatcher {
|
|
|
965
1036
|
for (let s = 0; s < offset; s++) tokens.advance();
|
|
966
1037
|
|
|
967
1038
|
// Retry group match from marker position
|
|
968
|
-
const retrySuccess = this.matchTokenSequence(
|
|
1039
|
+
const retrySuccess = this.matchTokenSequence(
|
|
1040
|
+
tokens,
|
|
1041
|
+
patternToken.tokens,
|
|
1042
|
+
captured,
|
|
1043
|
+
parentStopMarkers
|
|
1044
|
+
);
|
|
969
1045
|
if (retrySuccess) return true;
|
|
970
1046
|
|
|
971
1047
|
// Retry failed — full reset
|
|
@@ -1050,7 +1126,10 @@ export class PatternMatcher {
|
|
|
1050
1126
|
break;
|
|
1051
1127
|
}
|
|
1052
1128
|
}
|
|
1053
|
-
|
|
1129
|
+
// For optional groups, continue scanning past them so the greedy
|
|
1130
|
+
// capture also sees subsequent required literals (e.g., the SOV verb
|
|
1131
|
+
// keyword after an optional WHERE group).
|
|
1132
|
+
if (!pt.optional) break;
|
|
1054
1133
|
}
|
|
1055
1134
|
}
|
|
1056
1135
|
return markers;
|
|
@@ -1061,8 +1140,12 @@ export class PatternMatcher {
|
|
|
1061
1140
|
*/
|
|
1062
1141
|
private isStopMarker(token: LanguageToken, stopMarkers: Set<string>): boolean {
|
|
1063
1142
|
if (stopMarkers.size === 0) return false;
|
|
1064
|
-
|
|
1065
|
-
|
|
1143
|
+
// Check both raw value and normalized form against stop markers.
|
|
1144
|
+
// Pattern literals use native keywords (e.g., '選択') while tokenizers may
|
|
1145
|
+
// set normalized to the English equivalent (e.g., 'select'). Both must match.
|
|
1146
|
+
if (stopMarkers.has(token.value.toLowerCase())) return true;
|
|
1147
|
+
if (token.normalized && stopMarkers.has(token.normalized.toLowerCase())) return true;
|
|
1148
|
+
return false;
|
|
1066
1149
|
}
|
|
1067
1150
|
|
|
1068
1151
|
/**
|
package/src/core/types.ts
CHANGED
|
@@ -56,7 +56,8 @@ export type SemanticValue =
|
|
|
56
56
|
| SelectorValue
|
|
57
57
|
| ReferenceValue
|
|
58
58
|
| PropertyPathValue
|
|
59
|
-
| ExpressionValue
|
|
59
|
+
| ExpressionValue
|
|
60
|
+
| FlagValue;
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
63
|
* Expected value types for role tokens.
|
|
@@ -93,6 +94,49 @@ export interface ExpressionValue {
|
|
|
93
94
|
readonly raw: string;
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* A boolean flag value — present (+flag) or negated (~flag).
|
|
99
|
+
* Used in declarative domains for no-value attributes like primary-key, not-null.
|
|
100
|
+
*/
|
|
101
|
+
export interface FlagValue {
|
|
102
|
+
readonly type: 'flag';
|
|
103
|
+
readonly name: string;
|
|
104
|
+
readonly enabled: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// Annotations & Diagnostics (v1.2)
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
/** A metadata annotation on a node (v1.2). */
|
|
112
|
+
export interface Annotation {
|
|
113
|
+
readonly name: string;
|
|
114
|
+
readonly value?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A type constraint diagnostic attached to a node (v1.2).
|
|
119
|
+
*
|
|
120
|
+
* Named `ProtocolDiagnostic` to avoid collision with the framework's own
|
|
121
|
+
* `Diagnostic` type (which has `severity: 'error' | 'warning' | 'info'`
|
|
122
|
+
* and `suggestion?: string`).
|
|
123
|
+
*/
|
|
124
|
+
export interface ProtocolDiagnostic {
|
|
125
|
+
readonly level: 'error' | 'warning';
|
|
126
|
+
readonly role: string;
|
|
127
|
+
readonly message: string;
|
|
128
|
+
readonly code: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Async coordination variant (v1.2). */
|
|
132
|
+
export type AsyncVariant = 'all' | 'race';
|
|
133
|
+
|
|
134
|
+
/** A single arm in a match command (v1.2). */
|
|
135
|
+
export interface MatchArm {
|
|
136
|
+
readonly pattern: SemanticValue;
|
|
137
|
+
readonly body: SemanticNode[];
|
|
138
|
+
}
|
|
139
|
+
|
|
96
140
|
// =============================================================================
|
|
97
141
|
// Semantic Nodes
|
|
98
142
|
// =============================================================================
|
|
@@ -106,6 +150,10 @@ export interface SemanticNode {
|
|
|
106
150
|
readonly action: ActionType;
|
|
107
151
|
readonly roles: ReadonlyMap<SemanticRole, SemanticValue>;
|
|
108
152
|
readonly metadata?: SemanticMetadata;
|
|
153
|
+
/** Metadata annotations (v1.2). */
|
|
154
|
+
readonly annotations?: readonly Annotation[];
|
|
155
|
+
/** Type constraint diagnostics (v1.2). */
|
|
156
|
+
readonly diagnostics?: readonly ProtocolDiagnostic[];
|
|
109
157
|
}
|
|
110
158
|
|
|
111
159
|
/**
|
|
@@ -137,9 +185,27 @@ export interface SourcePosition {
|
|
|
137
185
|
|
|
138
186
|
/**
|
|
139
187
|
* A command semantic node - represents a single DSL command.
|
|
188
|
+
*
|
|
189
|
+
* v1.2 adds optional fields for try/catch/finally, async coordination (all/race),
|
|
190
|
+
* and pattern matching (match/arms). These are encoded as fields on command nodes
|
|
191
|
+
* (matching the protocol wire format) rather than new node kinds.
|
|
140
192
|
*/
|
|
141
193
|
export interface CommandSemanticNode extends SemanticNode {
|
|
142
194
|
readonly kind: 'command';
|
|
195
|
+
/** try body — commands to execute in the try block (v1.2). */
|
|
196
|
+
readonly body?: readonly SemanticNode[];
|
|
197
|
+
/** catch branch — commands to execute on error (v1.2). */
|
|
198
|
+
readonly catchBranch?: readonly SemanticNode[];
|
|
199
|
+
/** finally branch — cleanup commands that always execute (v1.2). */
|
|
200
|
+
readonly finallyBranch?: readonly SemanticNode[];
|
|
201
|
+
/** Async coordination variant: all (wait for all) or race (first wins) (v1.2). */
|
|
202
|
+
readonly asyncVariant?: AsyncVariant;
|
|
203
|
+
/** Async body — concurrent commands for all/race (v1.2). */
|
|
204
|
+
readonly asyncBody?: readonly SemanticNode[];
|
|
205
|
+
/** Match arms — pattern/body pairs for match command (v1.2). */
|
|
206
|
+
readonly arms?: readonly MatchArm[];
|
|
207
|
+
/** Default arm — executed when no match arm matches (v1.2). */
|
|
208
|
+
readonly defaultArm?: readonly SemanticNode[];
|
|
143
209
|
}
|
|
144
210
|
|
|
145
211
|
/**
|
|
@@ -177,7 +243,7 @@ export interface ConditionalSemanticNode extends SemanticNode {
|
|
|
177
243
|
export interface CompoundSemanticNode extends SemanticNode {
|
|
178
244
|
readonly kind: 'compound';
|
|
179
245
|
readonly statements: SemanticNode[];
|
|
180
|
-
readonly chainType: 'then' | 'and' | 'async' | 'sequential';
|
|
246
|
+
readonly chainType: 'then' | 'and' | 'async' | 'sequential' | 'pipe';
|
|
181
247
|
}
|
|
182
248
|
|
|
183
249
|
/**
|
|
@@ -444,6 +510,13 @@ export function createExpression(raw: string): ExpressionValue {
|
|
|
444
510
|
return { type: 'expression', raw };
|
|
445
511
|
}
|
|
446
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Create a boolean flag value (+flag or ~flag)
|
|
515
|
+
*/
|
|
516
|
+
export function createFlag(name: string, enabled: boolean = true): FlagValue {
|
|
517
|
+
return { type: 'flag', name, enabled };
|
|
518
|
+
}
|
|
519
|
+
|
|
447
520
|
/**
|
|
448
521
|
* Create a command semantic node
|
|
449
522
|
*/
|
|
@@ -542,6 +615,7 @@ export function createCompoundNode(
|
|
|
542
615
|
* eliminating the need for `as any` casts in domain code generators.
|
|
543
616
|
*/
|
|
544
617
|
export function extractValue(value: SemanticValue): string {
|
|
618
|
+
if (value.type === 'flag') return value.name;
|
|
545
619
|
if ('raw' in value && value.raw !== undefined) return String(value.raw);
|
|
546
620
|
if ('value' in value && value.value !== undefined) return String(value.value);
|
|
547
621
|
if (value.type === 'property-path') return `${extractValue(value.object)}.${value.property}`;
|
|
@@ -587,3 +661,72 @@ export function createLoopNode(
|
|
|
587
661
|
...(metadata && { metadata }),
|
|
588
662
|
};
|
|
589
663
|
}
|
|
664
|
+
|
|
665
|
+
// =============================================================================
|
|
666
|
+
// v1.2 Factory Helpers
|
|
667
|
+
// =============================================================================
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Create a try/catch/finally command node (v1.2).
|
|
671
|
+
*/
|
|
672
|
+
export function createTryNode(
|
|
673
|
+
body: SemanticNode[],
|
|
674
|
+
catchBranch?: SemanticNode[],
|
|
675
|
+
finallyBranch?: SemanticNode[],
|
|
676
|
+
metadata?: SemanticMetadata
|
|
677
|
+
): CommandSemanticNode {
|
|
678
|
+
return {
|
|
679
|
+
kind: 'command',
|
|
680
|
+
action: 'try',
|
|
681
|
+
roles: new Map(),
|
|
682
|
+
body,
|
|
683
|
+
...(catchBranch && catchBranch.length > 0 ? { catchBranch } : {}),
|
|
684
|
+
...(finallyBranch && finallyBranch.length > 0 ? { finallyBranch } : {}),
|
|
685
|
+
...(metadata ? { metadata } : {}),
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Create an async coordination (all/race) command node (v1.2).
|
|
691
|
+
*/
|
|
692
|
+
export function createAsyncNode(
|
|
693
|
+
variant: AsyncVariant,
|
|
694
|
+
asyncBody: SemanticNode[],
|
|
695
|
+
metadata?: SemanticMetadata
|
|
696
|
+
): CommandSemanticNode {
|
|
697
|
+
return {
|
|
698
|
+
kind: 'command',
|
|
699
|
+
action: variant,
|
|
700
|
+
roles: new Map(),
|
|
701
|
+
asyncVariant: variant,
|
|
702
|
+
asyncBody,
|
|
703
|
+
...(metadata ? { metadata } : {}),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Create a match command node with pattern arms (v1.2).
|
|
709
|
+
*/
|
|
710
|
+
export function createMatchNode(
|
|
711
|
+
roles: Record<SemanticRole, SemanticValue> | Map<SemanticRole, SemanticValue>,
|
|
712
|
+
arms: MatchArm[],
|
|
713
|
+
defaultArm?: SemanticNode[],
|
|
714
|
+
metadata?: SemanticMetadata
|
|
715
|
+
): CommandSemanticNode {
|
|
716
|
+
const rolesMap = roles instanceof Map ? roles : new Map(Object.entries(roles));
|
|
717
|
+
return {
|
|
718
|
+
kind: 'command',
|
|
719
|
+
action: 'match',
|
|
720
|
+
roles: rolesMap,
|
|
721
|
+
arms,
|
|
722
|
+
...(defaultArm && defaultArm.length > 0 ? { defaultArm } : {}),
|
|
723
|
+
...(metadata ? { metadata } : {}),
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Wire format envelope with version metadata (v1.2). */
|
|
728
|
+
export interface LSEEnvelope {
|
|
729
|
+
readonly lseVersion: string;
|
|
730
|
+
readonly features?: readonly string[];
|
|
731
|
+
readonly nodes: readonly SemanticNode[];
|
|
732
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Confidence-Aware Disambiguation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { needsDisambiguation, buildDisambiguation } from './confidence-gate';
|
|
7
|
+
import { createCommandNode, createSelector, createLiteral } from '../core/types';
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// needsDisambiguation()
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
describe('needsDisambiguation', () => {
|
|
14
|
+
it('returns false for high confidence', () => {
|
|
15
|
+
expect(needsDisambiguation(0.9)).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns false for very low confidence', () => {
|
|
19
|
+
expect(needsDisambiguation(0.3)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns true for borderline confidence', () => {
|
|
23
|
+
expect(needsDisambiguation(0.6)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns true at exact low threshold', () => {
|
|
27
|
+
expect(needsDisambiguation(0.5)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns false at exact high threshold', () => {
|
|
31
|
+
expect(needsDisambiguation(0.7)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('supports custom thresholds', () => {
|
|
35
|
+
expect(needsDisambiguation(0.45, 0.4, 0.6)).toBe(true);
|
|
36
|
+
expect(needsDisambiguation(0.3, 0.4, 0.6)).toBe(false);
|
|
37
|
+
expect(needsDisambiguation(0.7, 0.4, 0.6)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// buildDisambiguation()
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
describe('buildDisambiguation', () => {
|
|
46
|
+
const nodeA = createCommandNode('toggle', { patient: createSelector('#button') });
|
|
47
|
+
const nodeB = createCommandNode('toggle', { patient: createSelector('.button') });
|
|
48
|
+
|
|
49
|
+
it('builds a disambiguation request', () => {
|
|
50
|
+
const result = buildDisambiguation('toggle button', [
|
|
51
|
+
{ action: 'toggle', confidence: 0.62, node: nodeA, description: 'toggle class on #button' },
|
|
52
|
+
{ action: 'toggle', confidence: 0.58, node: nodeB, description: 'toggle .button class' },
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
expect(result.input).toBe('toggle button');
|
|
56
|
+
expect(result.candidates.length).toBe(2);
|
|
57
|
+
expect(result.question).toContain('0.62');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('sorts candidates by confidence (highest first)', () => {
|
|
61
|
+
const result = buildDisambiguation('test', [
|
|
62
|
+
{ action: 'add', confidence: 0.5, node: nodeB },
|
|
63
|
+
{ action: 'toggle', confidence: 0.65, node: nodeA },
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
expect(result.candidates[0].action).toBe('toggle');
|
|
67
|
+
expect(result.candidates[1].action).toBe('add');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('includes bracket syntax in candidates', () => {
|
|
71
|
+
const result = buildDisambiguation('test', [
|
|
72
|
+
{ action: 'toggle', confidence: 0.6, node: nodeA },
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
expect(result.candidates[0].explicit).toContain('[toggle');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('generates option labels (a, b, c)', () => {
|
|
79
|
+
const result = buildDisambiguation('test', [
|
|
80
|
+
{ action: 'a', confidence: 0.6, node: nodeA },
|
|
81
|
+
{ action: 'b', confidence: 0.55, node: nodeB },
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
expect(result.question).toContain('(a)');
|
|
85
|
+
expect(result.question).toContain('(b)');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('includes Which did you mean? prompt', () => {
|
|
89
|
+
const result = buildDisambiguation('test', [
|
|
90
|
+
{ action: 'toggle', confidence: 0.6, node: nodeA },
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
expect(result.question).toContain('Which did you mean?');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('uses custom description when provided', () => {
|
|
97
|
+
const result = buildDisambiguation('test', [
|
|
98
|
+
{ action: 'toggle', confidence: 0.6, node: nodeA, description: 'Custom description' },
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
expect(result.candidates[0].description).toBe('Custom description');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('generates default description when not provided', () => {
|
|
105
|
+
const result = buildDisambiguation('test', [
|
|
106
|
+
{ action: 'toggle', confidence: 0.6, node: nodeA },
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
expect(result.candidates[0].description).toContain('toggle');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confidence-Aware Disambiguation
|
|
3
|
+
*
|
|
4
|
+
* When parse confidence is borderline (between low and high thresholds),
|
|
5
|
+
* generates a disambiguation request with alternative interpretations
|
|
6
|
+
* instead of blindly accepting or rejecting.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { SemanticNode } from '../core/types';
|
|
10
|
+
import { renderExplicit } from '../ir/explicit-renderer';
|
|
11
|
+
import type { DisambiguationRequest, DisambiguationCandidate } from './types';
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Public API
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check whether a confidence score falls in the disambiguation range.
|
|
19
|
+
*
|
|
20
|
+
* @returns true if the confidence is ambiguous (between low and high thresholds)
|
|
21
|
+
*/
|
|
22
|
+
export function needsDisambiguation(
|
|
23
|
+
confidence: number,
|
|
24
|
+
low: number = 0.5,
|
|
25
|
+
high: number = 0.7
|
|
26
|
+
): boolean {
|
|
27
|
+
return confidence >= low && confidence < high;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a disambiguation request from candidate interpretations.
|
|
32
|
+
*
|
|
33
|
+
* @param input - The original ambiguous input
|
|
34
|
+
* @param candidates - Alternative interpretations with confidence scores
|
|
35
|
+
*/
|
|
36
|
+
export function buildDisambiguation(
|
|
37
|
+
input: string,
|
|
38
|
+
candidates: ReadonlyArray<{
|
|
39
|
+
action: string;
|
|
40
|
+
confidence: number;
|
|
41
|
+
node: SemanticNode;
|
|
42
|
+
description?: string;
|
|
43
|
+
}>
|
|
44
|
+
): DisambiguationRequest {
|
|
45
|
+
const sortedCandidates = [...candidates].sort((a, b) => b.confidence - a.confidence);
|
|
46
|
+
|
|
47
|
+
const disambiguationCandidates: DisambiguationCandidate[] = sortedCandidates.map((c, i) => ({
|
|
48
|
+
action: c.action,
|
|
49
|
+
confidence: c.confidence,
|
|
50
|
+
explicit: renderExplicit(c.node),
|
|
51
|
+
description: c.description || `${c.action} (option ${String.fromCharCode(97 + i)})`,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const optionLines = disambiguationCandidates.map(
|
|
55
|
+
(c, i) =>
|
|
56
|
+
` (${String.fromCharCode(97 + i)}) ${c.explicit} — ${c.description} (conf: ${c.confidence.toFixed(2)})`
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const question = [
|
|
60
|
+
`Your input parsed with confidence ${sortedCandidates[0]?.confidence.toFixed(2) ?? '?'}.`,
|
|
61
|
+
...optionLines,
|
|
62
|
+
'Which did you mean?',
|
|
63
|
+
].join('\n');
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
input,
|
|
67
|
+
candidates: disambiguationCandidates,
|
|
68
|
+
question,
|
|
69
|
+
};
|
|
70
|
+
}
|