@lokascript/framework 4.6.0 → 4.8.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 (116) hide show
  1. package/CHANGELOG.md +121 -1
  2. package/dist/api/create-dsl.d.ts +18 -2
  3. package/dist/api/create-dsl.d.ts.map +1 -1
  4. package/dist/api/index.d.ts +1 -0
  5. package/dist/api/index.d.ts.map +1 -1
  6. package/dist/api/index.js +1799 -713
  7. package/dist/api/index.js.map +1 -1
  8. package/dist/api/translation-loss.d.ts +84 -0
  9. package/dist/api/translation-loss.d.ts.map +1 -0
  10. package/dist/core/index.d.ts +1 -0
  11. package/dist/core/index.d.ts.map +1 -1
  12. package/dist/core/index.js +1057 -684
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/core/matching/confidence-model.d.ts +54 -0
  15. package/dist/core/matching/confidence-model.d.ts.map +1 -0
  16. package/dist/core/matching/index.d.ts +12 -0
  17. package/dist/core/matching/index.d.ts.map +1 -0
  18. package/dist/core/matching/match-skeleton.d.ts +114 -0
  19. package/dist/core/matching/match-skeleton.d.ts.map +1 -0
  20. package/dist/core/matching/stock-confidence.d.ts +15 -0
  21. package/dist/core/matching/stock-confidence.d.ts.map +1 -0
  22. package/dist/core/matching/stock-reader.d.ts +63 -0
  23. package/dist/core/matching/stock-reader.d.ts.map +1 -0
  24. package/dist/core/matching/stock-reading.d.ts +44 -0
  25. package/dist/core/matching/stock-reading.d.ts.map +1 -0
  26. package/dist/core/matching/value-reader.d.ts +62 -0
  27. package/dist/core/matching/value-reader.d.ts.map +1 -0
  28. package/dist/core/pattern-matching/index.js +561 -660
  29. package/dist/core/pattern-matching/index.js.map +1 -1
  30. package/dist/core/pattern-matching/pattern-matcher.d.ts +0 -117
  31. package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -1
  32. package/dist/core/types.d.ts +53 -0
  33. package/dist/core/types.d.ts.map +1 -1
  34. package/dist/core/types.js.map +1 -1
  35. package/dist/generation/index.d.ts +2 -0
  36. package/dist/generation/index.d.ts.map +1 -1
  37. package/dist/generation/index.js +593 -1
  38. package/dist/generation/index.js.map +1 -1
  39. package/dist/generation/pattern-render.d.ts +72 -0
  40. package/dist/generation/pattern-render.d.ts.map +1 -0
  41. package/dist/generation/schema-patterns.d.ts +152 -0
  42. package/dist/generation/schema-patterns.d.ts.map +1 -0
  43. package/dist/index.cjs +2016 -904
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.js +1998 -903
  46. package/dist/index.js.map +1 -1
  47. package/dist/ir/index.js.map +1 -1
  48. package/dist/parsing/index.js +561 -660
  49. package/dist/parsing/index.js.map +1 -1
  50. package/dist/testing/gate/index.d.ts +8 -0
  51. package/dist/testing/gate/index.d.ts.map +1 -0
  52. package/dist/testing/gate/index.js +664 -0
  53. package/dist/testing/gate/index.js.map +1 -0
  54. package/dist/testing/gate/lint/format.d.ts +7 -0
  55. package/dist/testing/gate/lint/format.d.ts.map +1 -0
  56. package/dist/testing/gate/lint/index.d.ts +30 -0
  57. package/dist/testing/gate/lint/index.d.ts.map +1 -0
  58. package/dist/testing/gate/lint/rules/extractor-coverage.d.ts +18 -0
  59. package/dist/testing/gate/lint/rules/extractor-coverage.d.ts.map +1 -0
  60. package/dist/testing/gate/lint/rules/index.d.ts +14 -0
  61. package/dist/testing/gate/lint/rules/index.d.ts.map +1 -0
  62. package/dist/testing/gate/lint/rules/keyword-classification.d.ts +19 -0
  63. package/dist/testing/gate/lint/rules/keyword-classification.d.ts.map +1 -0
  64. package/dist/testing/gate/lint/rules/keyword-coverage.d.ts +13 -0
  65. package/dist/testing/gate/lint/rules/keyword-coverage.d.ts.map +1 -0
  66. package/dist/testing/gate/lint/rules/marker-tokenization.d.ts +20 -0
  67. package/dist/testing/gate/lint/rules/marker-tokenization.d.ts.map +1 -0
  68. package/dist/testing/gate/lint/rules/position-ordering.d.ts +25 -0
  69. package/dist/testing/gate/lint/rules/position-ordering.d.ts.map +1 -0
  70. package/dist/testing/gate/lint/rules/renderer-coherence.d.ts +24 -0
  71. package/dist/testing/gate/lint/rules/renderer-coherence.d.ts.map +1 -0
  72. package/dist/testing/gate/lint/rules/schema-rules.d.ts +10 -0
  73. package/dist/testing/gate/lint/rules/schema-rules.d.ts.map +1 -0
  74. package/dist/testing/gate/lint/types.d.ts +74 -0
  75. package/dist/testing/gate/lint/types.d.ts.map +1 -0
  76. package/dist/testing/gate/round-trip.d.ts +184 -0
  77. package/dist/testing/gate/round-trip.d.ts.map +1 -0
  78. package/package.json +6 -2
  79. package/src/__test__/fixtures/sql-dsl.ts +8 -2
  80. package/src/api/create-dsl-engine.test.ts +150 -0
  81. package/src/api/create-dsl.test.ts +7 -0
  82. package/src/api/create-dsl.ts +173 -22
  83. package/src/api/index.ts +1 -0
  84. package/src/api/translation-loss.test.ts +89 -0
  85. package/src/api/translation-loss.ts +128 -0
  86. package/src/core/index.ts +1 -0
  87. package/src/core/matching/confidence-model.ts +58 -0
  88. package/src/core/matching/index.ts +12 -0
  89. package/src/core/matching/match-skeleton.test.ts +179 -0
  90. package/src/core/matching/match-skeleton.ts +621 -0
  91. package/src/core/matching/stock-confidence.ts +238 -0
  92. package/src/core/matching/stock-reader.test.ts +265 -0
  93. package/src/core/matching/stock-reader.ts +205 -0
  94. package/src/core/matching/stock-reading.ts +681 -0
  95. package/src/core/matching/value-reader.ts +85 -0
  96. package/src/core/pattern-matching/pattern-matcher.ts +11 -876
  97. package/src/core/types.ts +53 -0
  98. package/src/generation/index.ts +2 -0
  99. package/src/generation/pattern-render.ts +378 -0
  100. package/src/generation/schema-patterns.test.ts +139 -0
  101. package/src/generation/schema-patterns.ts +974 -0
  102. package/src/testing/gate/index.ts +8 -0
  103. package/src/testing/gate/lint/format.ts +23 -0
  104. package/src/testing/gate/lint/index.ts +73 -0
  105. package/src/testing/gate/lint/lint.test.ts +131 -0
  106. package/src/testing/gate/lint/rules/extractor-coverage.ts +63 -0
  107. package/src/testing/gate/lint/rules/index.ts +37 -0
  108. package/src/testing/gate/lint/rules/keyword-classification.ts +52 -0
  109. package/src/testing/gate/lint/rules/keyword-coverage.ts +38 -0
  110. package/src/testing/gate/lint/rules/marker-tokenization.ts +129 -0
  111. package/src/testing/gate/lint/rules/position-ordering.ts +87 -0
  112. package/src/testing/gate/lint/rules/renderer-coherence.ts +100 -0
  113. package/src/testing/gate/lint/rules/schema-rules.ts +35 -0
  114. package/src/testing/gate/lint/types.ts +83 -0
  115. package/src/testing/gate/round-trip.test.ts +208 -0
  116. package/src/testing/gate/round-trip.ts +503 -0
@@ -4,14 +4,24 @@
4
4
 
5
5
  import type {
6
6
  SemanticNode,
7
+ LanguageToken,
7
8
  LanguageTokenizer,
8
9
  LanguagePattern,
9
10
  PatternMatchResult,
11
+ RolePatternToken,
12
+ TokenStream,
10
13
  } from '../core/types';
14
+ import { extractRoleValue } from '../core/types';
11
15
  import type { CommandSchema } from '../schema';
12
- import { PatternMatcher, type PatternMatcherProfile } from '../core/pattern-matching';
16
+ import { PatternMatcher } from '../core/pattern-matching';
17
+ import { MatchSkeleton, createStockReader, stockConfidenceModel } from '../core/matching';
13
18
  import { generatePattern, type PatternGenLanguageProfile } from '../generation/pattern-generator';
14
19
  import { createDomainRenderer, type DomainRenderFn } from '../generation/renderer';
20
+ import {
21
+ createSchemaPatternGenerator,
22
+ type SchemaPatternProfile,
23
+ } from '../generation/schema-patterns';
24
+ import { renderPattern, selectRenderPattern } from '../generation/pattern-render';
15
25
  import { GrammarTransformer, type LanguageProfile as GrammarProfile } from '../grammar';
16
26
  import {
17
27
  type Dictionary,
@@ -174,6 +184,20 @@ export interface DSLConfig {
174
184
 
175
185
  /** Custom patterns to supplement generated ones */
176
186
  readonly customPatterns?: LanguagePattern[];
187
+
188
+ /**
189
+ * What reads and writes the DSL (domains rethink Phase 4.7). `'classic'`, the
190
+ * default: framework's first pattern generator, matcher and schema renderer.
191
+ * `'core'`: the core semantic's hyperscript parser runs — patterns from
192
+ * `createSchemaPatternGenerator` (positions descending, as a domain's schemas
193
+ * read), matched by `MatchSkeleton` with the stock reader and confidence
194
+ * model, and rendered through those patterns where the domain's own renderer
195
+ * declines. Parsing's whole-input rule, the refusals, translation's
196
+ * read-back, compile, explicit syntax and extensions are the same on both.
197
+ * The domains move to `'core'` one at a time against their gate (Phase 5);
198
+ * `'classic'` is removed in a later major, once no domain uses it.
199
+ */
200
+ readonly engine?: 'classic' | 'core';
177
201
  }
178
202
 
179
203
  /**
@@ -206,7 +230,9 @@ export interface CompileResult {
206
230
  /**
207
231
  * Thrown by `parse` (and so `compile`, `validate`, `translate`) when the input reads as a command
208
232
  * only by leaving some of it unread: `select name from users banana` is not `select name from users`.
209
- * `reading` is what the longest such match read, `unread` the text after it.
233
+ * `reading` is what the longest such match read, `unread` the text after it. `hint`, when there is
234
+ * one, says how the input might read: where a bare word read as a value is followed by another, the
235
+ * two may be one value written without quotes (todo `add buy milk to groceries`).
210
236
  */
211
237
  export class UnreadInputError extends Error {
212
238
  readonly code = 'unread-input';
@@ -215,9 +241,13 @@ export class UnreadInputError extends Error {
215
241
  readonly input: string,
216
242
  readonly language: string,
217
243
  readonly reading: SemanticNode,
218
- readonly unread: string
244
+ readonly unread: string,
245
+ readonly hint?: string
219
246
  ) {
220
- super(`"${input}" (${language}) reads as ${reading.action} only by leaving "${unread}" unread`);
247
+ super(
248
+ `"${input}" (${language}) reads as ${reading.action} only by leaving "${unread}" unread` +
249
+ (hint ? `; ${hint}` : '')
250
+ );
221
251
  this.name = 'UnreadInputError';
222
252
  }
223
253
  }
@@ -308,7 +338,11 @@ class DSLRegistry {
308
338
  * subset of the DSL's languages. A built-in command missing a keyword stays
309
339
  * an error, since that is a domain-authoring mistake.
310
340
  */
311
- constructor(config: DSLConfig, partiallyTranslatedActions: ReadonlySet<string> = new Set()) {
341
+ constructor(
342
+ config: DSLConfig,
343
+ partiallyTranslatedActions: ReadonlySet<string>,
344
+ private readonly generate: DSLEngine['generate']
345
+ ) {
312
346
  this.schemas = config.schemas;
313
347
  this.partiallyTranslatedActions = partiallyTranslatedActions;
314
348
 
@@ -331,7 +365,7 @@ class DSLRegistry {
331
365
  ) {
332
366
  continue; // this extension command was not translated into this language
333
367
  }
334
- const pattern = generatePattern(schema, lang.patternProfile);
368
+ const pattern = this.generate(schema, lang.patternProfile);
335
369
  patterns.push(pattern);
336
370
  }
337
371
 
@@ -356,7 +390,7 @@ class DSLRegistry {
356
390
  */
357
391
  class MultilingualDSLImpl implements MultilingualDSL {
358
392
  private registry: DSLRegistry;
359
- private matcher: PatternMatcher;
393
+ private match: DSLEngine['match'];
360
394
  private transformer: GrammarTransformer;
361
395
  private profileProvider: ProfileProvider;
362
396
  private codeGenerator?: CodeGenerator;
@@ -369,10 +403,11 @@ class MultilingualDSLImpl implements MultilingualDSL {
369
403
  config: DSLConfig,
370
404
  registry: DSLRegistry,
371
405
  transformer: GrammarTransformer,
372
- profileProvider: ProfileProvider
406
+ profileProvider: ProfileProvider,
407
+ engine: DSLEngine
373
408
  ) {
374
409
  this.registry = registry;
375
- this.matcher = new PatternMatcher();
410
+ this.match = engine.match;
376
411
  this.transformer = transformer;
377
412
  this.profileProvider = profileProvider;
378
413
  if (config.codeGenerator) {
@@ -390,10 +425,7 @@ class MultilingualDSLImpl implements MultilingualDSL {
390
425
  // Last resort for render(): covers extension commands with no custom
391
426
  // renderer, and any built-in action the domain renderer declines. Lazy, so
392
427
  // a DSL that never renders pays nothing.
393
- this.schemaFallbackRenderer = createDomainRenderer({
394
- schemas: config.schemas,
395
- profiles: config.languages.map(l => l.patternProfile),
396
- });
428
+ this.schemaFallbackRenderer = engine.fallbackRenderer(config, registry);
397
429
 
398
430
  // Build SchemaLookup from config schemas for explicit syntax validation
399
431
  const schemaMap = new Map(config.schemas.map(s => [s.action, s]));
@@ -430,11 +462,6 @@ class MultilingualDSLImpl implements MultilingualDSL {
430
462
  // Get patterns for language
431
463
  const patterns = this.registry.getPatterns(language);
432
464
 
433
- // Create profile for matcher
434
- const profile: PatternMatcherProfile = {
435
- code: language,
436
- };
437
-
438
465
  // Every pattern is tried, and only a match that reads the WHOLE input counts; of those, the
439
466
  // highest priority wins, then the highest confidence, then the earliest pattern. The first match
440
467
  // used to win however much it read, so a trailing word was dropped without a word (the domain
@@ -445,7 +472,7 @@ class MultilingualDSLImpl implements MultilingualDSL {
445
472
  let longestPartial: PatternMatchResult | null = null;
446
473
  for (const pattern of patterns) {
447
474
  const mark = tokens.mark();
448
- const match = this.matcher.matchPattern(tokens, pattern, profile);
475
+ const match = this.match(tokens, pattern, language);
449
476
  tokens.reset(mark);
450
477
  if (!match) continue;
451
478
  if (match.consumedTokens < total) {
@@ -471,16 +498,37 @@ class MultilingualDSLImpl implements MultilingualDSL {
471
498
  .slice(longestPartial.consumedTokens)
472
499
  .map(t => t.value)
473
500
  .join(' ');
501
+ const hint = this.quoteHint(longestPartial, tokens.tokens);
474
502
  throw new UnreadInputError(
475
503
  input,
476
504
  language,
477
505
  this.nodeOf(longestPartial, input, language),
478
- unread
506
+ unread,
507
+ ...(hint ? [hint] : [])
479
508
  );
480
509
  }
481
510
  throw new Error(`No pattern matched for input: ${input}`);
482
511
  }
483
512
 
513
+ /**
514
+ * `if "buy milk" is one value, quote it`, where the last token a match read is a bare word it took
515
+ * as a role's value (a role with no word list) and the first it left is a bare word too.
516
+ */
517
+ private quoteHint(
518
+ match: PatternMatchResult,
519
+ tokens: readonly LanguageToken[]
520
+ ): string | undefined {
521
+ const last = tokens[match.consumedTokens - 1];
522
+ const next = tokens[match.consumedTokens];
523
+ if (last?.kind !== 'identifier' || next?.kind !== 'identifier') return undefined;
524
+ const schema = this.schemaLookup.getSchema(match.pattern.command);
525
+ const role = [...match.captured].find(
526
+ ([, v]) => (v as { raw?: unknown }).raw === last.value
527
+ )?.[0];
528
+ if (!role || schema?.roles.find(r => r.role === role)?.words) return undefined;
529
+ return `if "${last.value} ${next.value}" is one value, quote it`;
530
+ }
531
+
484
532
  private nodeOf(match: PatternMatchResult, input: string, language: string): SemanticNode {
485
533
  return {
486
534
  kind: 'command',
@@ -830,6 +878,109 @@ export function createMultilingualDSL(config: DSLConfig): MultilingualDSL {
830
878
  });
831
879
 
832
880
  // Create registry and implementation
833
- const registry = new DSLRegistry(effectiveConfig, extensionActions);
834
- return new MultilingualDSLImpl(effectiveConfig, registry, transformer, profileProvider);
881
+ const engine = engineFor(config);
882
+ const registry = new DSLRegistry(effectiveConfig, extensionActions, engine.generate);
883
+ return new MultilingualDSLImpl(effectiveConfig, registry, transformer, profileProvider, engine);
884
+ }
885
+
886
+ // =============================================================================
887
+ // The engines: how a DSL turns its schemas into patterns and reads with them
888
+ // =============================================================================
889
+
890
+ /**
891
+ * What differs between the engines {@link DSLConfig.engine} names: how each
892
+ * command's pattern is generated, how input is matched against it, and the
893
+ * renderer `render()` falls back to. Parsing's whole-input rule, refusals,
894
+ * translation's read-back, compile, explicit syntax and extensions are shared.
895
+ */
896
+ interface DSLEngine {
897
+ generate(schema: CommandSchema, profile: PatternGenLanguageProfile): LanguagePattern;
898
+ match(tokens: TokenStream, pattern: LanguagePattern, language: string): PatternMatchResult | null;
899
+ fallbackRenderer(config: DSLConfig, registry: DSLRegistry): DomainRenderFn;
900
+ }
901
+
902
+ /**
903
+ * The engine `config.engine` names. An unknown name throws rather than reading
904
+ * with the default: a domain that misspells `'core'` would otherwise measure
905
+ * the classic engine against its gate and take the result for the core's.
906
+ */
907
+ function engineFor(config: DSLConfig): DSLEngine {
908
+ const name = config.engine ?? 'classic';
909
+ if (name === 'classic') return classicEngine();
910
+ if (name === 'core') return coreEngine();
911
+ throw new Error(`Unknown DSL engine "${String(name)}" (expected "classic" or "core")`);
912
+ }
913
+
914
+ /** framework's first generator and matcher, and its schema renderer (`engine: 'classic'`). */
915
+ function classicEngine(): DSLEngine {
916
+ const matcher = new PatternMatcher();
917
+ return {
918
+ generate: (schema, profile) => generatePattern(schema, profile),
919
+ match: (tokens, pattern, language) => matcher.matchPattern(tokens, pattern, { code: language }),
920
+ fallbackRenderer: config =>
921
+ createDomainRenderer({
922
+ schemas: config.schemas,
923
+ profiles: config.languages.map(l => l.patternProfile),
924
+ }),
925
+ };
926
+ }
927
+
928
+ /**
929
+ * A domain profile as the schema-pattern generator reads it: every role marker
930
+ * with a side (the word order's default where the profile names none).
931
+ */
932
+ function schemaProfileOf(profile: PatternGenLanguageProfile): SchemaPatternProfile {
933
+ const side = profile.wordOrder === 'SOV' ? 'after' : 'before';
934
+ const roleMarkers: Record<string, NonNullable<SchemaPatternProfile['roleMarkers'][string]>> = {};
935
+ for (const [role, marker] of Object.entries(profile.roleMarkers ?? {})) {
936
+ roleMarkers[role] = {
937
+ primary: marker.primary,
938
+ ...(marker.alternatives ? { alternatives: marker.alternatives } : {}),
939
+ position: marker.position ?? side,
940
+ };
941
+ }
942
+ return {
943
+ code: profile.code,
944
+ wordOrder: profile.wordOrder,
945
+ keywords: profile.keywords,
946
+ roleMarkers,
947
+ };
948
+ }
949
+
950
+ /**
951
+ * The core (domains rethink Phase 4, `engine: 'core'`): the schema-pattern
952
+ * generator, the skeleton, the stock reader.
953
+ */
954
+ function coreEngine(): DSLEngine {
955
+ const generator = createSchemaPatternGenerator({ positionOrder: 'descending' });
956
+ const matcher = new MatchSkeleton(createStockReader(), stockConfidenceModel);
957
+ return {
958
+ generate: (schema, profile) => generator.generatePattern(schema, schemaProfileOf(profile)),
959
+ match: (tokens, pattern) => matcher.matchPattern(tokens, pattern),
960
+ fallbackRenderer: (config, registry) => {
961
+ const schemas = new Map(config.schemas.map(s => [s.action, s]));
962
+ const hooks = {
963
+ hasRealRole: (node: SemanticNode, role: string) => {
964
+ const value = node.roles.get(role) as { implicit?: unknown } | undefined;
965
+ return value !== undefined && value.implicit !== true;
966
+ },
967
+ writeRole: (token: RolePatternToken, node: SemanticNode) => {
968
+ if (!node.roles.has(token.role)) return null;
969
+ let value = extractRoleValue(node, token.role);
970
+ const spec = schemas.get(node.action)?.roles.find(r => r.role === token.role);
971
+ if (spec?.quoteMultiword && /\s/.test(value) && !/^(["']).*\1$/.test(value)) {
972
+ value = `"${value}"`;
973
+ }
974
+ return value || null;
975
+ },
976
+ };
977
+ return (node, language) => {
978
+ const patterns = registry.getPatterns(language).filter(p => p.command === node.action);
979
+ const pattern = selectRenderPattern(node, patterns, hooks);
980
+ if (!pattern) return null;
981
+ const text = renderPattern(node, pattern, hooks).join(' ');
982
+ return text || null;
983
+ };
984
+ },
985
+ };
835
986
  }
package/src/api/index.ts CHANGED
@@ -5,3 +5,4 @@
5
5
  export * from './create-dsl';
6
6
  export * from './domain-registry';
7
7
  export * from './dispatcher';
8
+ export * from './translation-loss';
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The translation-loss checks, with rules of a toy grammar: each check in its
3
+ * place in the order, and the error a refusing translation throws.
4
+ */
5
+ import { describe, expect, it } from 'vitest';
6
+ import {
7
+ findTranslationLoss,
8
+ LossyTranslationError,
9
+ unconsumedSpans,
10
+ type TranslationLossRules,
11
+ } from './translation-loss';
12
+
13
+ interface Node {
14
+ readonly action: string;
15
+ readonly diagnostics?: ReadonlyArray<{ readonly code?: string; readonly message: string }>;
16
+ }
17
+
18
+ const unread = (text: string): Node => ({
19
+ action: 'go',
20
+ diagnostics: [{ code: 'unconsumed-input', message: `input not read: "${text}"` }],
21
+ });
22
+
23
+ const rules: TranslationLossRules<Node> = {
24
+ unwritable: node => (node.action === 'legacy' ? ['legacy'] : []),
25
+ differences: [(written, back) => (written.action === back.action ? [] : [`-${written.action}`])],
26
+ missingValues: (input, output) => (input.includes('#a') && !output.includes('#a') ? ['#a'] : []),
27
+ };
28
+
29
+ describe('findTranslationLoss', () => {
30
+ const ok: Node = { action: 'go' };
31
+
32
+ it('finds nothing in a translation that reads back with its values', () => {
33
+ expect(findTranslationLoss('go #a', ok, 've #a', () => ok, rules)).toBeUndefined();
34
+ });
35
+
36
+ it('checks in order: truncation, core-only, read-back, invariant', () => {
37
+ expect(findTranslationLoss('go #a x', unread('x'), '', () => ok, rules)).toEqual({
38
+ kind: 'truncation',
39
+ lost: ['x'],
40
+ });
41
+ expect(findTranslationLoss('x', { action: 'legacy' }, '', () => ok, rules)?.kind).toBe(
42
+ 'core-only'
43
+ );
44
+ expect(
45
+ findTranslationLoss(
46
+ 'go #a',
47
+ ok,
48
+ '?',
49
+ () => {
50
+ throw new Error('no pattern');
51
+ },
52
+ rules
53
+ )
54
+ ).toEqual({ kind: 'read-back', lost: ['the output does not read: no pattern'] });
55
+ expect(findTranslationLoss('go #a', ok, 've #a y', () => unread('y'), rules)).toEqual({
56
+ kind: 'read-back',
57
+ lost: ['y'],
58
+ });
59
+ expect(findTranslationLoss('go #a', ok, 'stop #a', () => ({ action: 'stop' }), rules)).toEqual({
60
+ kind: 'read-back',
61
+ lost: ['-go'],
62
+ });
63
+ expect(findTranslationLoss('go #a', ok, 've', () => ok, rules)).toEqual({
64
+ kind: 'invariant',
65
+ lost: ['#a'],
66
+ });
67
+ });
68
+
69
+ it('compares the read-back with what was written', () => {
70
+ const written: TranslationLossRules<Node> = { ...rules, written: () => ({ action: 'stop' }) };
71
+ expect(
72
+ findTranslationLoss('go #a', ok, 'stop #a', () => ({ action: 'stop' }), written)
73
+ ).toBeUndefined();
74
+ });
75
+ });
76
+
77
+ describe('unconsumedSpans and LossyTranslationError', () => {
78
+ it('reads the unread text out of the diagnostics', () => {
79
+ expect(unconsumedSpans(unread('banana'))).toEqual(['banana']);
80
+ expect(unconsumedSpans({ action: 'go' })).toEqual([]);
81
+ });
82
+
83
+ it('says what the translation loses', () => {
84
+ const error = new LossyTranslationError('ve', { kind: 'invariant', lost: ['#a'] }, 'en', 'es');
85
+ expect(error.name).toBe('LossyTranslationError');
86
+ expect(error.partial).toBe('ve');
87
+ expect(error.message).toBe('translation en → es would lose part of the script (invariant): #a');
88
+ });
89
+ });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Fail-loud translation: what a translation checks before it returns (domains
3
+ * rethink Phase 4.5, `docs-internal/DOMAINS_RETHINK.md`). Moved from
4
+ * `@lokascript/semantic`'s `explicit/lossy.ts`, whose `translate` runs it with
5
+ * hyperscript's rules; `createMultilingualDSL`'s `translate` keeps its own
6
+ * read-back (`TranslationReadBackError`).
7
+ *
8
+ * A parse that matches a prefix of a script and leaves the rest unread used to
9
+ * come back from `translate()` as though it were the whole: `toggle .a .b`
10
+ * became `toggle .a`. A surface that cannot carry a script now says so: the
11
+ * translation throws a {@link LossyTranslationError}, which carries the output
12
+ * it would have returned and what it loses. Four checks, cheapest first:
13
+ *
14
+ * - `truncation` the parse of the input left input bound to no role (its
15
+ * `unconsumed-input` diagnostics);
16
+ * - `core-only` the input holds a form the grammar's writer has no spelling
17
+ * for (hyperscript: a form only `@hyperfixi/core` ran, which
18
+ * upstream reads with another meaning) — `rules.unwritable`;
19
+ * - `read-back` the output, read in the target language, does not read,
20
+ * leaves input unread, or differs from what was written
21
+ * (`rules.differences`, in order);
22
+ * - `invariant` a value the translation must write verbatim is missing
23
+ * from the output (`rules.missingValues`).
24
+ *
25
+ * None of them can say a translation is right. Each says one way it is wrong.
26
+ */
27
+
28
+ export type TranslationLossKind = 'truncation' | 'core-only' | 'read-back' | 'invariant';
29
+
30
+ /** One way a translation loses part of its input. */
31
+ export interface TranslationLoss {
32
+ kind: TranslationLossKind;
33
+ /**
34
+ * What is lost: the input left unread (`truncation`); the forms nothing
35
+ * writes (`core-only`, as `swap <strategy>`); the output left unread or what
36
+ * it reads back without or with in excess (`read-back`, as `-cmd` / `+cmd`);
37
+ * or the verbatim values missing from the output (`invariant`).
38
+ */
39
+ lost: string[];
40
+ }
41
+
42
+ export class LossyTranslationError extends Error {
43
+ override readonly name = 'LossyTranslationError';
44
+
45
+ constructor(
46
+ /** The output `translate()` would have returned: NOT the whole script. */
47
+ readonly partial: string,
48
+ readonly loss: TranslationLoss,
49
+ readonly from: string,
50
+ readonly to: string
51
+ ) {
52
+ super(
53
+ `translation ${from} → ${to} would lose part of the script (${loss.kind}): ` +
54
+ loss.lost.join(', ')
55
+ );
56
+ }
57
+ }
58
+
59
+ /** What the checks read of a parse: its diagnostics. */
60
+ export interface LossCheckedNode {
61
+ readonly diagnostics?: ReadonlyArray<{ readonly code?: string; readonly message: string }>;
62
+ }
63
+
64
+ /** The spans a parse left bound to no role (its top node carries them all). */
65
+ export function unconsumedSpans(node: LossCheckedNode): string[] {
66
+ const out: string[] = [];
67
+ for (const d of node.diagnostics ?? []) {
68
+ if (d.code !== 'unconsumed-input') continue;
69
+ out.push(/"(.*)"\s*$/s.exec(d.message)?.[1] ?? d.message);
70
+ }
71
+ return out;
72
+ }
73
+
74
+ /** A grammar's rules for `findTranslationLoss`, for its node type `N`. */
75
+ export interface TranslationLossRules<N extends LossCheckedNode> {
76
+ /** The forms in `node` the grammar's writer has no spelling for (`core-only`). */
77
+ unwritable?(node: N): string[];
78
+ /**
79
+ * `node` as it was written into the target language, to compare its
80
+ * read-back with (hyperscript: in upstream's spelling for English). Default:
81
+ * `node` itself.
82
+ */
83
+ written?(node: N): N;
84
+ /**
85
+ * How the read-back differs from what was written, checked in order: the
86
+ * first that finds a difference is the loss (hyperscript: commands, then
87
+ * roles, then clauses kept as written).
88
+ */
89
+ readonly differences: ReadonlyArray<(written: N, back: N) => string[]>;
90
+ /** The values `input` holds that `output` must write verbatim and does not (`invariant`). */
91
+ missingValues?(input: string, output: string): string[];
92
+ }
93
+
94
+ /**
95
+ * How a translation of `input` (parsed as `node`) into `output` loses part of
96
+ * it, or undefined. `readBack` parses the output in the target language; it
97
+ * throws when the output does not read.
98
+ */
99
+ export function findTranslationLoss<N extends LossCheckedNode>(
100
+ input: string,
101
+ node: N,
102
+ output: string,
103
+ readBack: (output: string) => N,
104
+ rules: TranslationLossRules<N>
105
+ ): TranslationLoss | undefined {
106
+ const unread = unconsumedSpans(node);
107
+ if (unread.length) return { kind: 'truncation', lost: unread };
108
+ const unwritable = rules.unwritable?.(node) ?? [];
109
+ if (unwritable.length) return { kind: 'core-only', lost: unwritable };
110
+
111
+ let back: N;
112
+ try {
113
+ back = readBack(output);
114
+ } catch (e) {
115
+ return { kind: 'read-back', lost: [`the output does not read: ${(e as Error).message}`] };
116
+ }
117
+ const misread = unconsumedSpans(back);
118
+ if (misread.length) return { kind: 'read-back', lost: misread };
119
+ const written = rules.written ? rules.written(node) : node;
120
+ for (const difference of rules.differences) {
121
+ const lost = difference(written, back);
122
+ if (lost.length) return { kind: 'read-back', lost };
123
+ }
124
+
125
+ const values = rules.missingValues?.(input, output) ?? [];
126
+ if (values.length) return { kind: 'invariant', lost: values };
127
+ return undefined;
128
+ }
package/src/core/index.ts CHANGED
@@ -5,3 +5,4 @@
5
5
  export * from './types';
6
6
  export * from './tokenization';
7
7
  export * from './pattern-matching';
8
+ export * from './matching';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The matcher's confidence model: an injectable score for a pattern's match
3
+ * (domains rethink Phase 4.3). The interface moved here with the skeleton
4
+ * (`match-skeleton.ts`), which calls `calculate` once per matched pattern;
5
+ * hyperscript's model is `@lokascript/semantic`'s `DefaultConfidenceModel`.
6
+ */
7
+
8
+ import type { LanguagePattern, SemanticRole, SemanticValue } from '../types';
9
+
10
+ /**
11
+ * Context passed to confidence model methods.
12
+ */
13
+ export interface ConfidenceContext {
14
+ /** The pattern being matched */
15
+ readonly pattern: LanguagePattern;
16
+ /** Captured semantic roles */
17
+ readonly captured: ReadonlyMap<SemanticRole, SemanticValue>;
18
+ /** Number of stem (morphological) keyword matches */
19
+ readonly stemMatchCount: number;
20
+ /** Total keyword matches attempted */
21
+ readonly totalKeywordMatches: number;
22
+ }
23
+
24
+ /**
25
+ * Breakdown of confidence scoring factors (for diagnostics/debugging).
26
+ */
27
+ export interface ConfidenceBreakdown {
28
+ /** Role coverage score (0-1) */
29
+ readonly roleCoverage: number;
30
+ /** Stem match penalty applied (0 to -0.15) */
31
+ readonly stemPenalty: number;
32
+ /** Language-specific boost (e.g., VSO verb-first) */
33
+ readonly languageBoost: number;
34
+ /** Language-specific adjustment (e.g., preposition disambiguation) */
35
+ readonly languageAdjustment: number;
36
+ /** Final clamped confidence (0-1) */
37
+ readonly final: number;
38
+ }
39
+
40
+ /**
41
+ * Injectable confidence scoring model.
42
+ *
43
+ * Each method computes one factor of the confidence score.
44
+ * Override individual methods to customize scoring without rewriting the pipeline.
45
+ */
46
+ export interface ConfidenceModel {
47
+ /**
48
+ * Calculate the full confidence score.
49
+ * Default implementation calls each factor method and combines them.
50
+ */
51
+ calculate(ctx: ConfidenceContext): number;
52
+
53
+ /**
54
+ * Calculate confidence with a detailed breakdown.
55
+ * Useful for diagnostics and debugging.
56
+ */
57
+ calculateWithBreakdown(ctx: ConfidenceContext): ConfidenceBreakdown;
58
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The pattern matcher's skeleton and its value-reader seam (domains rethink
3
+ * Phase 4.3): the matcher semantic's hyperscript parser runs, with
4
+ * hyperscript's reading behind `ValueReader`.
5
+ */
6
+
7
+ export { MatchSkeleton } from './match-skeleton';
8
+ export type { MatchView, RoleSlot, ValueReader } from './value-reader';
9
+ export type { ConfidenceBreakdown, ConfidenceContext, ConfidenceModel } from './confidence-model';
10
+ export { StockValueReader, createStockReader, greedyStopMarkers, readGreedy } from './stock-reader';
11
+ export { readRoleValue, type ReadingProfile } from './stock-reading';
12
+ export { stockConfidenceModel } from './stock-confidence';