@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.
Files changed (126) hide show
  1. package/dist/aot/index.d.ts +1 -0
  2. package/dist/aot/index.d.ts.map +1 -1
  3. package/dist/aot/registry-bridge.d.ts +27 -0
  4. package/dist/aot/registry-bridge.d.ts.map +1 -0
  5. package/dist/api/create-dsl.d.ts.map +1 -1
  6. package/dist/api/dispatcher.d.ts +22 -0
  7. package/dist/api/dispatcher.d.ts.map +1 -1
  8. package/dist/api/domain-registry.d.ts +41 -0
  9. package/dist/api/domain-registry.d.ts.map +1 -1
  10. package/dist/api/index.js +1351 -21
  11. package/dist/api/index.js.map +1 -1
  12. package/dist/core/index.js +114 -10
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/core/pattern-matching/index.js +74 -10
  15. package/dist/core/pattern-matching/index.js.map +1 -1
  16. package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -1
  17. package/dist/core/types.d.ts +80 -2
  18. package/dist/core/types.d.ts.map +1 -1
  19. package/dist/core/types.js +40 -0
  20. package/dist/core/types.js.map +1 -1
  21. package/dist/feedback/confidence-gate.d.ts +28 -0
  22. package/dist/feedback/confidence-gate.d.ts.map +1 -0
  23. package/dist/feedback/feedback-formatter.d.ts +21 -0
  24. package/dist/feedback/feedback-formatter.d.ts.map +1 -0
  25. package/dist/feedback/index.d.ts +11 -0
  26. package/dist/feedback/index.d.ts.map +1 -0
  27. package/dist/feedback/pattern-tracker.d.ts +47 -0
  28. package/dist/feedback/pattern-tracker.d.ts.map +1 -0
  29. package/dist/feedback/types.d.ts +113 -0
  30. package/dist/feedback/types.d.ts.map +1 -0
  31. package/dist/generation/index.js +2 -1
  32. package/dist/generation/index.js.map +1 -1
  33. package/dist/index.cjs +2685 -71
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.ts +6 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2652 -71
  38. package/dist/index.js.map +1 -1
  39. package/dist/ir/explicit-parser.d.ts +31 -0
  40. package/dist/ir/explicit-parser.d.ts.map +1 -0
  41. package/dist/ir/explicit-renderer.d.ts +17 -0
  42. package/dist/ir/explicit-renderer.d.ts.map +1 -0
  43. package/dist/ir/from-interchange.d.ts +51 -0
  44. package/dist/ir/from-interchange.d.ts.map +1 -0
  45. package/dist/ir/index.d.ts +29 -0
  46. package/dist/ir/index.d.ts.map +1 -0
  47. package/dist/ir/index.js +1557 -0
  48. package/dist/ir/index.js.map +1 -0
  49. package/dist/ir/json-schema.d.ts +40 -0
  50. package/dist/ir/json-schema.d.ts.map +1 -0
  51. package/dist/ir/protocol-json.d.ts +73 -0
  52. package/dist/ir/protocol-json.d.ts.map +1 -0
  53. package/dist/ir/references.d.ts +19 -0
  54. package/dist/ir/references.d.ts.map +1 -0
  55. package/dist/ir/types.d.ts +141 -0
  56. package/dist/ir/types.d.ts.map +1 -0
  57. package/dist/parsing/index.js +74 -10
  58. package/dist/parsing/index.js.map +1 -1
  59. package/dist/prompts/index.d.ts +9 -0
  60. package/dist/prompts/index.d.ts.map +1 -0
  61. package/dist/prompts/prompt-generator.d.ts +49 -0
  62. package/dist/prompts/prompt-generator.d.ts.map +1 -0
  63. package/dist/prompts/types.d.ts +64 -0
  64. package/dist/prompts/types.d.ts.map +1 -0
  65. package/dist/schema/command-schema.d.ts +17 -0
  66. package/dist/schema/command-schema.d.ts.map +1 -1
  67. package/dist/schema/index.js.map +1 -1
  68. package/dist/testing/index.js +3 -0
  69. package/dist/testing/index.js.map +1 -1
  70. package/dist/training/index.d.ts +11 -0
  71. package/dist/training/index.d.ts.map +1 -0
  72. package/dist/training/jsonl-writer.d.ts +42 -0
  73. package/dist/training/jsonl-writer.d.ts.map +1 -0
  74. package/dist/training/schema-synthesizer.d.ts +20 -0
  75. package/dist/training/schema-synthesizer.d.ts.map +1 -0
  76. package/dist/training/types.d.ts +73 -0
  77. package/dist/training/types.d.ts.map +1 -0
  78. package/package.json +5 -1
  79. package/src/aot/index.ts +1 -0
  80. package/src/aot/registry-bridge.test.ts +104 -0
  81. package/src/aot/registry-bridge.ts +66 -0
  82. package/src/api/create-dsl.test.ts +306 -0
  83. package/src/api/create-dsl.ts +21 -0
  84. package/src/api/dispatcher.test.ts +66 -0
  85. package/src/api/dispatcher.ts +77 -9
  86. package/src/api/domain-registry.ts +120 -4
  87. package/src/core/pattern-matching/pattern-matcher.test.ts +131 -0
  88. package/src/core/pattern-matching/pattern-matcher.ts +102 -19
  89. package/src/core/types.ts +145 -2
  90. package/src/feedback/confidence-gate.test.ts +111 -0
  91. package/src/feedback/confidence-gate.ts +70 -0
  92. package/src/feedback/feedback-formatter.test.ts +198 -0
  93. package/src/feedback/feedback-formatter.ts +298 -0
  94. package/src/feedback/index.ts +24 -0
  95. package/src/feedback/pattern-tracker.test.ts +198 -0
  96. package/src/feedback/pattern-tracker.ts +140 -0
  97. package/src/feedback/types.ts +149 -0
  98. package/src/generation/pattern-generator.ts +2 -2
  99. package/src/index.ts +22 -0
  100. package/src/integration/llm-lse-integration.test.ts +457 -0
  101. package/src/ir/explicit-parser.test.ts +306 -0
  102. package/src/ir/explicit-parser.ts +419 -0
  103. package/src/ir/explicit-renderer.test.ts +144 -0
  104. package/src/ir/explicit-renderer.ts +146 -0
  105. package/src/ir/from-interchange.test.ts +853 -0
  106. package/src/ir/from-interchange.ts +594 -0
  107. package/src/ir/index.ts +66 -0
  108. package/src/ir/json-schema.test.ts +281 -0
  109. package/src/ir/json-schema.ts +299 -0
  110. package/src/ir/protocol-json.test.ts +1185 -0
  111. package/src/ir/protocol-json.ts +745 -0
  112. package/src/ir/references.test.ts +38 -0
  113. package/src/ir/references.ts +33 -0
  114. package/src/ir/round-trip.test.ts +557 -0
  115. package/src/ir/types.ts +181 -0
  116. package/src/prompts/index.ts +15 -0
  117. package/src/prompts/prompt-generator.test.ts +386 -0
  118. package/src/prompts/prompt-generator.ts +531 -0
  119. package/src/prompts/types.ts +89 -0
  120. package/src/schema/command-schema.ts +19 -0
  121. package/src/training/index.ts +13 -0
  122. package/src/training/jsonl-writer.test.ts +138 -0
  123. package/src/training/jsonl-writer.ts +67 -0
  124. package/src/training/schema-synthesizer.test.ts +250 -0
  125. package/src/training/schema-synthesizer.ts +306 -0
  126. 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(tokens, patternToken.tokens, captured);
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(tokens, patternToken.tokens, captured);
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
- break;
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
- const value = (token.normalized || token.value).toLowerCase();
1065
- return stopMarkers.has(value);
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
+ }