@lokascript/framework 2.0.0 → 2.2.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 (138) 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 +1438 -21
  11. package/dist/api/index.js.map +1 -1
  12. package/dist/core/index.js +247 -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/tokenization/index.js +133 -0
  18. package/dist/core/tokenization/index.js.map +1 -1
  19. package/dist/core/tokenization/morphology/base-normalizer.d.ts +94 -0
  20. package/dist/core/tokenization/morphology/base-normalizer.d.ts.map +1 -0
  21. package/dist/core/tokenization/morphology/index.d.ts +3 -1
  22. package/dist/core/tokenization/morphology/index.d.ts.map +1 -1
  23. package/dist/core/types.d.ts +81 -2
  24. package/dist/core/types.d.ts.map +1 -1
  25. package/dist/core/types.js +40 -0
  26. package/dist/core/types.js.map +1 -1
  27. package/dist/feedback/confidence-gate.d.ts +28 -0
  28. package/dist/feedback/confidence-gate.d.ts.map +1 -0
  29. package/dist/feedback/feedback-formatter.d.ts +21 -0
  30. package/dist/feedback/feedback-formatter.d.ts.map +1 -0
  31. package/dist/feedback/index.d.ts +11 -0
  32. package/dist/feedback/index.d.ts.map +1 -0
  33. package/dist/feedback/pattern-tracker.d.ts +47 -0
  34. package/dist/feedback/pattern-tracker.d.ts.map +1 -0
  35. package/dist/feedback/types.d.ts +113 -0
  36. package/dist/feedback/types.d.ts.map +1 -0
  37. package/dist/generation/index.js +2 -1
  38. package/dist/generation/index.js.map +1 -1
  39. package/dist/index.cjs +3264 -132
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.ts +8 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +3223 -132
  44. package/dist/index.js.map +1 -1
  45. package/dist/ir/explicit-parser.d.ts +86 -0
  46. package/dist/ir/explicit-parser.d.ts.map +1 -0
  47. package/dist/ir/explicit-renderer.d.ts +34 -0
  48. package/dist/ir/explicit-renderer.d.ts.map +1 -0
  49. package/dist/ir/from-interchange.d.ts +51 -0
  50. package/dist/ir/from-interchange.d.ts.map +1 -0
  51. package/dist/ir/index.d.ts +31 -0
  52. package/dist/ir/index.d.ts.map +1 -0
  53. package/dist/ir/index.js +1934 -0
  54. package/dist/ir/index.js.map +1 -0
  55. package/dist/ir/json-schema.d.ts +40 -0
  56. package/dist/ir/json-schema.d.ts.map +1 -0
  57. package/dist/ir/protocol-json.d.ts +73 -0
  58. package/dist/ir/protocol-json.d.ts.map +1 -0
  59. package/dist/ir/references.d.ts +19 -0
  60. package/dist/ir/references.d.ts.map +1 -0
  61. package/dist/ir/to-runtime-ast.d.ts +53 -0
  62. package/dist/ir/to-runtime-ast.d.ts.map +1 -0
  63. package/dist/ir/types.d.ts +151 -0
  64. package/dist/ir/types.d.ts.map +1 -0
  65. package/dist/parsing/index.js +74 -10
  66. package/dist/parsing/index.js.map +1 -1
  67. package/dist/prompts/index.d.ts +9 -0
  68. package/dist/prompts/index.d.ts.map +1 -0
  69. package/dist/prompts/prompt-generator.d.ts +49 -0
  70. package/dist/prompts/prompt-generator.d.ts.map +1 -0
  71. package/dist/prompts/types.d.ts +64 -0
  72. package/dist/prompts/types.d.ts.map +1 -0
  73. package/dist/schema/command-schema.d.ts +17 -0
  74. package/dist/schema/command-schema.d.ts.map +1 -1
  75. package/dist/schema/index.js.map +1 -1
  76. package/dist/testing/index.js +8673 -8260
  77. package/dist/testing/index.js.map +1 -1
  78. package/dist/training/index.d.ts +11 -0
  79. package/dist/training/index.d.ts.map +1 -0
  80. package/dist/training/jsonl-writer.d.ts +42 -0
  81. package/dist/training/jsonl-writer.d.ts.map +1 -0
  82. package/dist/training/schema-synthesizer.d.ts +20 -0
  83. package/dist/training/schema-synthesizer.d.ts.map +1 -0
  84. package/dist/training/types.d.ts +73 -0
  85. package/dist/training/types.d.ts.map +1 -0
  86. package/package.json +5 -1
  87. package/src/aot/index.ts +1 -0
  88. package/src/aot/registry-bridge.test.ts +104 -0
  89. package/src/aot/registry-bridge.ts +66 -0
  90. package/src/api/create-dsl.test.ts +306 -0
  91. package/src/api/create-dsl.ts +21 -0
  92. package/src/api/dispatcher.test.ts +66 -0
  93. package/src/api/dispatcher.ts +77 -9
  94. package/src/api/domain-registry.ts +120 -4
  95. package/src/core/pattern-matching/pattern-matcher.test.ts +131 -0
  96. package/src/core/pattern-matching/pattern-matcher.ts +102 -19
  97. package/src/core/tokenization/morphology/base-normalizer.ts +249 -0
  98. package/src/core/tokenization/morphology/index.ts +3 -1
  99. package/src/core/types.ts +147 -2
  100. package/src/feedback/confidence-gate.test.ts +111 -0
  101. package/src/feedback/confidence-gate.ts +70 -0
  102. package/src/feedback/feedback-formatter.test.ts +198 -0
  103. package/src/feedback/feedback-formatter.ts +298 -0
  104. package/src/feedback/index.ts +24 -0
  105. package/src/feedback/pattern-tracker.test.ts +198 -0
  106. package/src/feedback/pattern-tracker.ts +140 -0
  107. package/src/feedback/types.ts +149 -0
  108. package/src/generation/pattern-generator.ts +2 -2
  109. package/src/index.ts +29 -0
  110. package/src/integration/llm-lse-integration.test.ts +457 -0
  111. package/src/ir/conformance.test.ts +120 -0
  112. package/src/ir/explicit-parser.test.ts +612 -0
  113. package/src/ir/explicit-parser.ts +793 -0
  114. package/src/ir/explicit-renderer.test.ts +229 -0
  115. package/src/ir/explicit-renderer.ts +198 -0
  116. package/src/ir/from-interchange.test.ts +853 -0
  117. package/src/ir/from-interchange.ts +594 -0
  118. package/src/ir/index.ts +77 -0
  119. package/src/ir/json-schema.test.ts +281 -0
  120. package/src/ir/json-schema.ts +299 -0
  121. package/src/ir/protocol-json.test.ts +1187 -0
  122. package/src/ir/protocol-json.ts +751 -0
  123. package/src/ir/references.test.ts +38 -0
  124. package/src/ir/references.ts +33 -0
  125. package/src/ir/round-trip.test.ts +557 -0
  126. package/src/ir/to-runtime-ast.ts +203 -0
  127. package/src/ir/types.ts +191 -0
  128. package/src/prompts/index.ts +15 -0
  129. package/src/prompts/prompt-generator.test.ts +386 -0
  130. package/src/prompts/prompt-generator.ts +531 -0
  131. package/src/prompts/types.ts +89 -0
  132. package/src/schema/command-schema.ts +19 -0
  133. package/src/training/index.ts +13 -0
  134. package/src/training/jsonl-writer.test.ts +138 -0
  135. package/src/training/jsonl-writer.ts +67 -0
  136. package/src/training/schema-synthesizer.test.ts +250 -0
  137. package/src/training/schema-synthesizer.ts +306 -0
  138. 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
  /**
@@ -0,0 +1,249 @@
1
+ /**
2
+ * BaseMorphologicalNormalizer — Shared base class for language normalizers.
3
+ *
4
+ * Provides the common suffix/prefix stripping loop, reflexive verb handling,
5
+ * and normalize() pipeline. Language-specific normalizers extend this class
6
+ * and provide their conjugation rules.
7
+ *
8
+ * Phase 3.1 of parser-ecosystem-plan-v3.
9
+ */
10
+
11
+ import type {
12
+ MorphologicalNormalizer,
13
+ NormalizationResult,
14
+ ConjugationType,
15
+ SuffixRule,
16
+ PrefixRule,
17
+ } from './types';
18
+ import { noChange, normalized } from './types';
19
+
20
+ /**
21
+ * Conjugation ending rule for verb classes (Romance languages etc.)
22
+ * Broader than SuffixRule — includes the replacement stem (e.g., strip -ando, add -ar).
23
+ */
24
+ export interface ConjugationEnding {
25
+ readonly ending: string;
26
+ readonly stem: string;
27
+ readonly confidence: number;
28
+ readonly type: ConjugationType;
29
+ }
30
+
31
+ /**
32
+ * Configuration for BaseMorphologicalNormalizer.
33
+ * Subclasses provide this in their constructor.
34
+ */
35
+ export interface NormalizerConfig {
36
+ /** Language code */
37
+ readonly language: string;
38
+
39
+ /** Minimum word length to attempt normalization */
40
+ readonly minWordLength?: number;
41
+
42
+ /** Minimum stem length after stripping (default: 2) */
43
+ readonly minStemLength?: number;
44
+
45
+ /** Conjugation endings sorted longest-first */
46
+ readonly endings?: readonly ConjugationEnding[];
47
+
48
+ /** Suffix rules (for SuffixRule-style normalizers) */
49
+ readonly suffixRules?: readonly SuffixRule[];
50
+
51
+ /** Prefix rules */
52
+ readonly prefixRules?: readonly PrefixRule[];
53
+
54
+ /** Reflexive suffixes (for Romance languages) */
55
+ readonly reflexiveSuffixes?: readonly string[];
56
+
57
+ /** Infinitive endings (for checking if already normalized) */
58
+ readonly infinitiveEndings?: readonly string[];
59
+ }
60
+
61
+ /**
62
+ * Abstract base class for morphological normalizers.
63
+ *
64
+ * Subclasses must implement `isNormalizable()` and can override any
65
+ * normalization step. The default `normalize()` pipeline is:
66
+ *
67
+ * 1. Check if already in dictionary form → noChange
68
+ * 2. Try reflexive normalization (if reflexiveSuffixes configured)
69
+ * 3. Try conjugation endings (if endings configured)
70
+ * 4. Try suffix rules (if suffixRules configured)
71
+ * 5. Try prefix rules (if prefixRules configured)
72
+ * 6. Return noChange
73
+ */
74
+ export abstract class BaseMorphologicalNormalizer implements MorphologicalNormalizer {
75
+ readonly language: string;
76
+ protected readonly config: NormalizerConfig;
77
+
78
+ constructor(config: NormalizerConfig) {
79
+ this.language = config.language;
80
+ this.config = {
81
+ minWordLength: 3,
82
+ minStemLength: 2,
83
+ ...config,
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Check if a word can be normalized. Subclasses must implement this
89
+ * with language-specific script/character detection.
90
+ */
91
+ abstract isNormalizable(word: string): boolean;
92
+
93
+ /**
94
+ * Standard normalization pipeline. Override for custom behavior.
95
+ */
96
+ normalize(word: string): NormalizationResult {
97
+ const lower = word.toLowerCase();
98
+
99
+ // Check if already in dictionary form
100
+ if (this.isAlreadyNormalized(lower)) {
101
+ return noChange(word);
102
+ }
103
+
104
+ // Try reflexive normalization (Romance languages)
105
+ if (this.config.reflexiveSuffixes) {
106
+ const reflexive = this.tryReflexiveNormalization(lower);
107
+ if (reflexive) return reflexive;
108
+ }
109
+
110
+ // Try conjugation endings
111
+ if (this.config.endings) {
112
+ const conjugation = this.tryConjugationEndings(lower);
113
+ if (conjugation) return conjugation;
114
+ }
115
+
116
+ // Try suffix rules
117
+ if (this.config.suffixRules) {
118
+ const suffix = this.trySuffixRules(lower);
119
+ if (suffix) return suffix;
120
+ }
121
+
122
+ // Try prefix rules
123
+ if (this.config.prefixRules) {
124
+ const prefix = this.tryPrefixRules(lower);
125
+ if (prefix) return prefix;
126
+ }
127
+
128
+ return noChange(word);
129
+ }
130
+
131
+ /**
132
+ * Check if word is already in dictionary form (e.g., ends in -ar/-er/-ir).
133
+ * Override for language-specific checks.
134
+ */
135
+ protected isAlreadyNormalized(word: string): boolean {
136
+ if (this.config.infinitiveEndings) {
137
+ return this.config.infinitiveEndings.some(e => word.endsWith(e));
138
+ }
139
+ return false;
140
+ }
141
+
142
+ /**
143
+ * Try to strip reflexive suffixes and normalize the remainder.
144
+ * Common in Romance languages (Spanish, Portuguese, French).
145
+ */
146
+ protected tryReflexiveNormalization(word: string): NormalizationResult | null {
147
+ const suffixes = this.config.reflexiveSuffixes;
148
+ if (!suffixes) return null;
149
+
150
+ for (const suffix of suffixes) {
151
+ if (!word.endsWith(suffix)) continue;
152
+ const remainder = word.slice(0, -suffix.length);
153
+
154
+ // Check if remainder is already an infinitive
155
+ if (this.isAlreadyNormalized(remainder)) {
156
+ return normalized(remainder, 0.88, {
157
+ removedSuffixes: [suffix],
158
+ conjugationType: 'reflexive',
159
+ });
160
+ }
161
+
162
+ // Try to normalize the remainder
163
+ const inner = this.tryConjugationEndings(remainder) || this.trySuffixRules(remainder);
164
+ if (inner && inner.stem !== remainder) {
165
+ return normalized(inner.stem, inner.confidence * 0.95, {
166
+ removedSuffixes: [suffix, ...(inner.metadata?.removedSuffixes || [])],
167
+ conjugationType: 'reflexive',
168
+ });
169
+ }
170
+ }
171
+
172
+ return null;
173
+ }
174
+
175
+ /**
176
+ * Try conjugation endings (verb class endings like -ar/-er/-ir patterns).
177
+ * Endings must be pre-sorted longest-first.
178
+ */
179
+ protected tryConjugationEndings(word: string): NormalizationResult | null {
180
+ const endings = this.config.endings;
181
+ if (!endings) return null;
182
+
183
+ const minStem = this.config.minStemLength ?? 2;
184
+
185
+ for (const rule of endings) {
186
+ if (!word.endsWith(rule.ending)) continue;
187
+
188
+ const stemBase = word.slice(0, -rule.ending.length);
189
+ if (stemBase.length < minStem) continue;
190
+
191
+ const infinitive = stemBase + rule.stem;
192
+ return normalized(infinitive, rule.confidence, {
193
+ removedSuffixes: [rule.ending],
194
+ conjugationType: rule.type,
195
+ });
196
+ }
197
+
198
+ return null;
199
+ }
200
+
201
+ /**
202
+ * Try SuffixRule-style normalization.
203
+ * Rules must be pre-sorted longest-first.
204
+ */
205
+ protected trySuffixRules(word: string): NormalizationResult | null {
206
+ const rules = this.config.suffixRules;
207
+ if (!rules) return null;
208
+
209
+ const defaultMinStem = this.config.minStemLength ?? 2;
210
+
211
+ for (const rule of rules) {
212
+ if (!word.endsWith(rule.pattern)) continue;
213
+
214
+ const stem = word.slice(0, -rule.pattern.length);
215
+ const minStem = rule.minStemLength ?? defaultMinStem;
216
+ if (stem.length < minStem) continue;
217
+
218
+ const result = stem + (rule.replacement || '');
219
+ return normalized(result, rule.confidence, {
220
+ removedSuffixes: [rule.pattern],
221
+ ...(rule.conjugationType && { conjugationType: rule.conjugationType }),
222
+ });
223
+ }
224
+
225
+ return null;
226
+ }
227
+
228
+ /**
229
+ * Try PrefixRule-style normalization.
230
+ */
231
+ protected tryPrefixRules(word: string): NormalizationResult | null {
232
+ const rules = this.config.prefixRules;
233
+ if (!rules) return null;
234
+
235
+ for (const rule of rules) {
236
+ if (!word.startsWith(rule.pattern)) continue;
237
+
238
+ const remainder = word.slice(rule.pattern.length);
239
+ const minRemaining = rule.minRemaining ?? this.config.minStemLength ?? 2;
240
+ if (remainder.length < minRemaining) continue;
241
+
242
+ return normalized(remainder, 1.0 - rule.confidencePenalty, {
243
+ removedPrefixes: [rule.pattern],
244
+ });
245
+ }
246
+
247
+ return null;
248
+ }
249
+ }
@@ -1,5 +1,7 @@
1
1
  /**
2
- * Morphological normalization types and interfaces
2
+ * Morphological normalization types, interfaces, and base class
3
3
  */
4
4
 
5
5
  export * from './types';
6
+ export { BaseMorphologicalNormalizer } from './base-normalizer';
7
+ export type { ConjugationEnding, NormalizerConfig } from './base-normalizer';
package/src/core/types.ts CHANGED
@@ -8,6 +8,8 @@
8
8
  * These types are domain-agnostic - they work for any DSL (SQL, animations, etc.)
9
9
  */
10
10
 
11
+ import type { Diagnostic } from '../generation/diagnostics';
12
+
11
13
  // =============================================================================
12
14
  // Action Types (Generic)
13
15
  // =============================================================================
@@ -56,7 +58,8 @@ export type SemanticValue =
56
58
  | SelectorValue
57
59
  | ReferenceValue
58
60
  | PropertyPathValue
59
- | ExpressionValue;
61
+ | ExpressionValue
62
+ | FlagValue;
60
63
 
61
64
  /**
62
65
  * Expected value types for role tokens.
@@ -93,6 +96,49 @@ export interface ExpressionValue {
93
96
  readonly raw: string;
94
97
  }
95
98
 
99
+ /**
100
+ * A boolean flag value — present (+flag) or negated (~flag).
101
+ * Used in declarative domains for no-value attributes like primary-key, not-null.
102
+ */
103
+ export interface FlagValue {
104
+ readonly type: 'flag';
105
+ readonly name: string;
106
+ readonly enabled: boolean;
107
+ }
108
+
109
+ // =============================================================================
110
+ // Annotations & Diagnostics (v1.2)
111
+ // =============================================================================
112
+
113
+ /** A metadata annotation on a node (v1.2). */
114
+ export interface Annotation {
115
+ readonly name: string;
116
+ readonly value?: string;
117
+ }
118
+
119
+ /**
120
+ * A type constraint diagnostic attached to a node (v1.2).
121
+ *
122
+ * Named `ProtocolDiagnostic` to avoid collision with the framework's own
123
+ * `Diagnostic` type (which has `severity: 'error' | 'warning' | 'info'`
124
+ * and `suggestion?: string`).
125
+ */
126
+ export interface ProtocolDiagnostic {
127
+ readonly level: 'error' | 'warning';
128
+ readonly role: string;
129
+ readonly message: string;
130
+ readonly code: string;
131
+ }
132
+
133
+ /** Async coordination variant (v1.2). */
134
+ export type AsyncVariant = 'all' | 'race';
135
+
136
+ /** A single arm in a match command (v1.2). */
137
+ export interface MatchArm {
138
+ readonly pattern: SemanticValue;
139
+ readonly body: SemanticNode[];
140
+ }
141
+
96
142
  // =============================================================================
97
143
  // Semantic Nodes
98
144
  // =============================================================================
@@ -106,6 +152,10 @@ export interface SemanticNode {
106
152
  readonly action: ActionType;
107
153
  readonly roles: ReadonlyMap<SemanticRole, SemanticValue>;
108
154
  readonly metadata?: SemanticMetadata;
155
+ /** Metadata annotations (v1.2). */
156
+ readonly annotations?: readonly Annotation[];
157
+ /** Diagnostics from parsing, validation, and schema checks (v1.2, extended v1.2.1). */
158
+ readonly diagnostics?: readonly Diagnostic[];
109
159
  }
110
160
 
111
161
  /**
@@ -137,9 +187,27 @@ export interface SourcePosition {
137
187
 
138
188
  /**
139
189
  * A command semantic node - represents a single DSL command.
190
+ *
191
+ * v1.2 adds optional fields for try/catch/finally, async coordination (all/race),
192
+ * and pattern matching (match/arms). These are encoded as fields on command nodes
193
+ * (matching the protocol wire format) rather than new node kinds.
140
194
  */
141
195
  export interface CommandSemanticNode extends SemanticNode {
142
196
  readonly kind: 'command';
197
+ /** try body — commands to execute in the try block (v1.2). */
198
+ readonly body?: readonly SemanticNode[];
199
+ /** catch branch — commands to execute on error (v1.2). */
200
+ readonly catchBranch?: readonly SemanticNode[];
201
+ /** finally branch — cleanup commands that always execute (v1.2). */
202
+ readonly finallyBranch?: readonly SemanticNode[];
203
+ /** Async coordination variant: all (wait for all) or race (first wins) (v1.2). */
204
+ readonly asyncVariant?: AsyncVariant;
205
+ /** Async body — concurrent commands for all/race (v1.2). */
206
+ readonly asyncBody?: readonly SemanticNode[];
207
+ /** Match arms — pattern/body pairs for match command (v1.2). */
208
+ readonly arms?: readonly MatchArm[];
209
+ /** Default arm — executed when no match arm matches (v1.2). */
210
+ readonly defaultArm?: readonly SemanticNode[];
143
211
  }
144
212
 
145
213
  /**
@@ -177,7 +245,7 @@ export interface ConditionalSemanticNode extends SemanticNode {
177
245
  export interface CompoundSemanticNode extends SemanticNode {
178
246
  readonly kind: 'compound';
179
247
  readonly statements: SemanticNode[];
180
- readonly chainType: 'then' | 'and' | 'async' | 'sequential';
248
+ readonly chainType: 'then' | 'and' | 'async' | 'sequential' | 'pipe';
181
249
  }
182
250
 
183
251
  /**
@@ -444,6 +512,13 @@ export function createExpression(raw: string): ExpressionValue {
444
512
  return { type: 'expression', raw };
445
513
  }
446
514
 
515
+ /**
516
+ * Create a boolean flag value (+flag or ~flag)
517
+ */
518
+ export function createFlag(name: string, enabled: boolean = true): FlagValue {
519
+ return { type: 'flag', name, enabled };
520
+ }
521
+
447
522
  /**
448
523
  * Create a command semantic node
449
524
  */
@@ -542,6 +617,7 @@ export function createCompoundNode(
542
617
  * eliminating the need for `as any` casts in domain code generators.
543
618
  */
544
619
  export function extractValue(value: SemanticValue): string {
620
+ if (value.type === 'flag') return value.name;
545
621
  if ('raw' in value && value.raw !== undefined) return String(value.raw);
546
622
  if ('value' in value && value.value !== undefined) return String(value.value);
547
623
  if (value.type === 'property-path') return `${extractValue(value.object)}.${value.property}`;
@@ -587,3 +663,72 @@ export function createLoopNode(
587
663
  ...(metadata && { metadata }),
588
664
  };
589
665
  }
666
+
667
+ // =============================================================================
668
+ // v1.2 Factory Helpers
669
+ // =============================================================================
670
+
671
+ /**
672
+ * Create a try/catch/finally command node (v1.2).
673
+ */
674
+ export function createTryNode(
675
+ body: SemanticNode[],
676
+ catchBranch?: SemanticNode[],
677
+ finallyBranch?: SemanticNode[],
678
+ metadata?: SemanticMetadata
679
+ ): CommandSemanticNode {
680
+ return {
681
+ kind: 'command',
682
+ action: 'try',
683
+ roles: new Map(),
684
+ body,
685
+ ...(catchBranch && catchBranch.length > 0 ? { catchBranch } : {}),
686
+ ...(finallyBranch && finallyBranch.length > 0 ? { finallyBranch } : {}),
687
+ ...(metadata ? { metadata } : {}),
688
+ };
689
+ }
690
+
691
+ /**
692
+ * Create an async coordination (all/race) command node (v1.2).
693
+ */
694
+ export function createAsyncNode(
695
+ variant: AsyncVariant,
696
+ asyncBody: SemanticNode[],
697
+ metadata?: SemanticMetadata
698
+ ): CommandSemanticNode {
699
+ return {
700
+ kind: 'command',
701
+ action: variant,
702
+ roles: new Map(),
703
+ asyncVariant: variant,
704
+ asyncBody,
705
+ ...(metadata ? { metadata } : {}),
706
+ };
707
+ }
708
+
709
+ /**
710
+ * Create a match command node with pattern arms (v1.2).
711
+ */
712
+ export function createMatchNode(
713
+ roles: Record<SemanticRole, SemanticValue> | Map<SemanticRole, SemanticValue>,
714
+ arms: MatchArm[],
715
+ defaultArm?: SemanticNode[],
716
+ metadata?: SemanticMetadata
717
+ ): CommandSemanticNode {
718
+ const rolesMap = roles instanceof Map ? roles : new Map(Object.entries(roles));
719
+ return {
720
+ kind: 'command',
721
+ action: 'match',
722
+ roles: rolesMap,
723
+ arms,
724
+ ...(defaultArm && defaultArm.length > 0 ? { defaultArm } : {}),
725
+ ...(metadata ? { metadata } : {}),
726
+ };
727
+ }
728
+
729
+ /** Wire format envelope with version metadata (v1.2). */
730
+ export interface LSEEnvelope {
731
+ readonly lseVersion: string;
732
+ readonly features?: readonly string[];
733
+ readonly nodes: readonly SemanticNode[];
734
+ }