@lokascript/framework 4.6.0 → 4.7.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 (115) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/dist/api/create-dsl.d.ts +13 -0
  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 +1756 -710
  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 +1047 -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 +56 -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 +48 -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 +583 -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 +1979 -907
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.js +1961 -906
  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 +632 -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 +163 -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 +86 -0
  81. package/src/api/create-dsl.ts +141 -18
  82. package/src/api/index.ts +1 -0
  83. package/src/api/translation-loss.test.ts +89 -0
  84. package/src/api/translation-loss.ts +128 -0
  85. package/src/core/index.ts +1 -0
  86. package/src/core/matching/confidence-model.ts +58 -0
  87. package/src/core/matching/index.ts +12 -0
  88. package/src/core/matching/match-skeleton.test.ts +179 -0
  89. package/src/core/matching/match-skeleton.ts +621 -0
  90. package/src/core/matching/stock-confidence.ts +238 -0
  91. package/src/core/matching/stock-reader.test.ts +181 -0
  92. package/src/core/matching/stock-reader.ts +186 -0
  93. package/src/core/matching/stock-reading.ts +681 -0
  94. package/src/core/matching/value-reader.ts +85 -0
  95. package/src/core/pattern-matching/pattern-matcher.ts +11 -876
  96. package/src/core/types.ts +48 -0
  97. package/src/generation/index.ts +2 -0
  98. package/src/generation/pattern-render.ts +378 -0
  99. package/src/generation/schema-patterns.test.ts +99 -0
  100. package/src/generation/schema-patterns.ts +962 -0
  101. package/src/testing/gate/index.ts +8 -0
  102. package/src/testing/gate/lint/format.ts +23 -0
  103. package/src/testing/gate/lint/index.ts +73 -0
  104. package/src/testing/gate/lint/lint.test.ts +131 -0
  105. package/src/testing/gate/lint/rules/extractor-coverage.ts +63 -0
  106. package/src/testing/gate/lint/rules/index.ts +37 -0
  107. package/src/testing/gate/lint/rules/keyword-classification.ts +52 -0
  108. package/src/testing/gate/lint/rules/keyword-coverage.ts +38 -0
  109. package/src/testing/gate/lint/rules/marker-tokenization.ts +129 -0
  110. package/src/testing/gate/lint/rules/position-ordering.ts +87 -0
  111. package/src/testing/gate/lint/rules/renderer-coherence.ts +100 -0
  112. package/src/testing/gate/lint/rules/schema-rules.ts +35 -0
  113. package/src/testing/gate/lint/types.ts +83 -0
  114. package/src/testing/gate/round-trip.test.ts +118 -0
  115. package/src/testing/gate/round-trip.ts +438 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lokascript/framework",
3
- "version": "4.6.0",
3
+ "version": "4.7.0",
4
4
  "description": "Generic framework for building multilingual DSLs with semantic parsing and grammar transformation",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -56,6 +56,10 @@
56
56
  "types": "./dist/testing/index.d.ts",
57
57
  "import": "./dist/testing/index.js"
58
58
  },
59
+ "./testing/gate": {
60
+ "types": "./dist/testing/gate/index.d.ts",
61
+ "import": "./dist/testing/gate/index.js"
62
+ },
59
63
  "./ir": {
60
64
  "types": "./dist/ir/index.d.ts",
61
65
  "import": "./dist/ir/index.js"
@@ -96,7 +100,7 @@
96
100
  "author": "LokaScript Contributors",
97
101
  "license": "MIT",
98
102
  "dependencies": {
99
- "@lokascript/intent": "^4.6.0"
103
+ "@lokascript/intent": "^4.7.0"
100
104
  },
101
105
  "devDependencies": {
102
106
  "@lokascript/domains": "^4.0.0",
@@ -13,6 +13,7 @@ import {
13
13
  getDefaultExtractors,
14
14
  type SemanticNode,
15
15
  type MultilingualDSL,
16
+ type DSLConfig,
16
17
  } from '../../index';
17
18
 
18
19
  // =============================================================================
@@ -168,7 +169,12 @@ function generateSQL(node: SemanticNode): string {
168
169
  // =============================================================================
169
170
 
170
171
  export function createSQLDSL(): MultilingualDSL {
171
- return createMultilingualDSL({
172
+ return createMultilingualDSL(sqlDSLConfig());
173
+ }
174
+
175
+ /** The fixture's config, for a test that builds the DSL with another factory. */
176
+ export function sqlDSLConfig(): DSLConfig {
177
+ return {
172
178
  name: 'SQL DSL Test Fixture',
173
179
  schemas: [selectCommand],
174
180
  languages: [
@@ -190,7 +196,7 @@ export function createSQLDSL(): MultilingualDSL {
190
196
  codeGenerator: {
191
197
  generate: generateSQL,
192
198
  },
193
- });
199
+ };
194
200
  }
195
201
 
196
202
  // =============================================================================
@@ -0,0 +1,86 @@
1
+ /**
2
+ * `engine: 'core'` (domains rethink Phase 4.7): the same config and DSL as the
3
+ * classic engine, read by the core. framework's SQL fixture reads, compiles and
4
+ * translates as it does on the classic engine, refuses what it does not read,
5
+ * and renders through its own patterns where no renderer is configured.
6
+ */
7
+ import { describe, expect, it, vi } from 'vitest';
8
+ import { createMultilingualDSL, UnreadInputError, type DSLConfig } from './create-dsl';
9
+ import { sqlDSLConfig, testQueries, expectedSQL } from '../__test__/fixtures/sql-dsl';
10
+ import type { SemanticNode } from '../core/types';
11
+ import { generatePattern } from '../generation/pattern-generator';
12
+ import { createSchemaPatternGenerator } from '../generation/schema-patterns';
13
+
14
+ // Pass-through spies. On the SQL fixture the two engines read, compile and render alike, so
15
+ // these are what show which engine a DSL was built on.
16
+ vi.mock('../generation/pattern-generator', async importOriginal => {
17
+ const actual = await importOriginal<typeof import('../generation/pattern-generator')>();
18
+ return { ...actual, generatePattern: vi.fn(actual.generatePattern) };
19
+ });
20
+ vi.mock('../generation/schema-patterns', async importOriginal => {
21
+ const actual = await importOriginal<typeof import('../generation/schema-patterns')>();
22
+ return { ...actual, createSchemaPatternGenerator: vi.fn(actual.createSchemaPatternGenerator) };
23
+ });
24
+
25
+ const core = createMultilingualDSL({ ...sqlDSLConfig(), engine: 'core' });
26
+ const classic = createMultilingualDSL(sqlDSLConfig());
27
+
28
+ const reading = (node: SemanticNode) =>
29
+ [
30
+ node.action,
31
+ ...[...node.roles].map(
32
+ ([role, v]) =>
33
+ `${role}=${String((v as { value?: unknown; raw?: unknown }).value ?? (v as { raw?: unknown }).raw)}`
34
+ ),
35
+ ].join(' ');
36
+
37
+ describe("createMultilingualDSL with engine: 'core'", () => {
38
+ const queries = [
39
+ ['en', testQueries.english.simple, expectedSQL.simple],
40
+ ['en', testQueries.english.withWhere, expectedSQL.withWhere],
41
+ ['es', testQueries.spanish.simple, 'SELECT nombre FROM usuarios'],
42
+ ['es', testQueries.spanish.withWhere, 'SELECT nombre FROM usuarios WHERE edad > 18'],
43
+ ] as const;
44
+
45
+ it.each(queries)('reads and compiles %s `%s` as the classic engine does', (lang, input, sql) => {
46
+ expect(reading(core.parse(input, lang))).toBe(reading(classic.parse(input, lang)));
47
+ expect(core.compile(input, lang).code).toBe(sql);
48
+ });
49
+
50
+ it.each(queries)('translates %s `%s` as the classic engine does', (lang, input) => {
51
+ const to = lang === 'en' ? 'es' : 'en';
52
+ expect(core.translate(input, lang, to)).toBe(classic.translate(input, lang, to));
53
+ });
54
+
55
+ it('refuses input it does not read', () => {
56
+ expect(() => core.parse('select name from users banana', 'en')).toThrow(UnreadInputError);
57
+ expect(core.validate('select name from users banana', 'en')).toMatchObject({
58
+ valid: false,
59
+ errorCode: 'unread-input',
60
+ });
61
+ });
62
+
63
+ it('renders through its patterns, in each language, what it reads back', () => {
64
+ const node = core.parse(testQueries.english.withWhere, 'en');
65
+ expect(core.render?.(node, 'en')).toBe('select name from users where age > 18');
66
+ expect(core.render?.(node, 'es')).toBe('seleccionar name de users donde age > 18');
67
+ expect(reading(core.parse(core.render!(node, 'es')!, 'es'))).toBe(reading(node));
68
+ });
69
+
70
+ it.each([
71
+ ['core', { engine: 'core' }],
72
+ ['classic', { engine: 'classic' }],
73
+ ['classic', {}],
74
+ ] as const)('builds on the %s engine with %o', (expected, option) => {
75
+ vi.mocked(generatePattern).mockClear();
76
+ vi.mocked(createSchemaPatternGenerator).mockClear();
77
+ createMultilingualDSL({ ...sqlDSLConfig(), ...option }).parse(testQueries.english.simple, 'en');
78
+ expect(vi.mocked(createSchemaPatternGenerator).mock.calls.length > 0).toBe(expected === 'core');
79
+ expect(vi.mocked(generatePattern).mock.calls.length > 0).toBe(expected === 'classic');
80
+ });
81
+
82
+ it('throws on an engine it does not know, rather than reading with the classic one', () => {
83
+ const misspelled = { ...sqlDSLConfig(), engine: 'cor' } as unknown as DSLConfig;
84
+ expect(() => createMultilingualDSL(misspelled)).toThrow('Unknown DSL engine "cor"');
85
+ });
86
+ });
@@ -7,11 +7,20 @@ import type {
7
7
  LanguageTokenizer,
8
8
  LanguagePattern,
9
9
  PatternMatchResult,
10
+ RolePatternToken,
11
+ TokenStream,
10
12
  } from '../core/types';
13
+ import { extractRoleValue } from '../core/types';
11
14
  import type { CommandSchema } from '../schema';
12
- import { PatternMatcher, type PatternMatcherProfile } from '../core/pattern-matching';
15
+ import { PatternMatcher } from '../core/pattern-matching';
16
+ import { MatchSkeleton, createStockReader, stockConfidenceModel } from '../core/matching';
13
17
  import { generatePattern, type PatternGenLanguageProfile } from '../generation/pattern-generator';
14
18
  import { createDomainRenderer, type DomainRenderFn } from '../generation/renderer';
19
+ import {
20
+ createSchemaPatternGenerator,
21
+ type SchemaPatternProfile,
22
+ } from '../generation/schema-patterns';
23
+ import { renderPattern, selectRenderPattern } from '../generation/pattern-render';
15
24
  import { GrammarTransformer, type LanguageProfile as GrammarProfile } from '../grammar';
16
25
  import {
17
26
  type Dictionary,
@@ -174,6 +183,20 @@ export interface DSLConfig {
174
183
 
175
184
  /** Custom patterns to supplement generated ones */
176
185
  readonly customPatterns?: LanguagePattern[];
186
+
187
+ /**
188
+ * What reads and writes the DSL (domains rethink Phase 4.7). `'classic'`, the
189
+ * default: framework's first pattern generator, matcher and schema renderer.
190
+ * `'core'`: the core semantic's hyperscript parser runs — patterns from
191
+ * `createSchemaPatternGenerator` (positions descending, as a domain's schemas
192
+ * read), matched by `MatchSkeleton` with the stock reader and confidence
193
+ * model, and rendered through those patterns where the domain's own renderer
194
+ * declines. Parsing's whole-input rule, the refusals, translation's
195
+ * read-back, compile, explicit syntax and extensions are the same on both.
196
+ * The domains move to `'core'` one at a time against their gate (Phase 5);
197
+ * `'classic'` is removed in a later major, once no domain uses it.
198
+ */
199
+ readonly engine?: 'classic' | 'core';
177
200
  }
178
201
 
179
202
  /**
@@ -308,7 +331,11 @@ class DSLRegistry {
308
331
  * subset of the DSL's languages. A built-in command missing a keyword stays
309
332
  * an error, since that is a domain-authoring mistake.
310
333
  */
311
- constructor(config: DSLConfig, partiallyTranslatedActions: ReadonlySet<string> = new Set()) {
334
+ constructor(
335
+ config: DSLConfig,
336
+ partiallyTranslatedActions: ReadonlySet<string>,
337
+ private readonly generate: DSLEngine['generate']
338
+ ) {
312
339
  this.schemas = config.schemas;
313
340
  this.partiallyTranslatedActions = partiallyTranslatedActions;
314
341
 
@@ -331,7 +358,7 @@ class DSLRegistry {
331
358
  ) {
332
359
  continue; // this extension command was not translated into this language
333
360
  }
334
- const pattern = generatePattern(schema, lang.patternProfile);
361
+ const pattern = this.generate(schema, lang.patternProfile);
335
362
  patterns.push(pattern);
336
363
  }
337
364
 
@@ -356,7 +383,7 @@ class DSLRegistry {
356
383
  */
357
384
  class MultilingualDSLImpl implements MultilingualDSL {
358
385
  private registry: DSLRegistry;
359
- private matcher: PatternMatcher;
386
+ private match: DSLEngine['match'];
360
387
  private transformer: GrammarTransformer;
361
388
  private profileProvider: ProfileProvider;
362
389
  private codeGenerator?: CodeGenerator;
@@ -369,10 +396,11 @@ class MultilingualDSLImpl implements MultilingualDSL {
369
396
  config: DSLConfig,
370
397
  registry: DSLRegistry,
371
398
  transformer: GrammarTransformer,
372
- profileProvider: ProfileProvider
399
+ profileProvider: ProfileProvider,
400
+ engine: DSLEngine
373
401
  ) {
374
402
  this.registry = registry;
375
- this.matcher = new PatternMatcher();
403
+ this.match = engine.match;
376
404
  this.transformer = transformer;
377
405
  this.profileProvider = profileProvider;
378
406
  if (config.codeGenerator) {
@@ -390,10 +418,7 @@ class MultilingualDSLImpl implements MultilingualDSL {
390
418
  // Last resort for render(): covers extension commands with no custom
391
419
  // renderer, and any built-in action the domain renderer declines. Lazy, so
392
420
  // a DSL that never renders pays nothing.
393
- this.schemaFallbackRenderer = createDomainRenderer({
394
- schemas: config.schemas,
395
- profiles: config.languages.map(l => l.patternProfile),
396
- });
421
+ this.schemaFallbackRenderer = engine.fallbackRenderer(config, registry);
397
422
 
398
423
  // Build SchemaLookup from config schemas for explicit syntax validation
399
424
  const schemaMap = new Map(config.schemas.map(s => [s.action, s]));
@@ -430,11 +455,6 @@ class MultilingualDSLImpl implements MultilingualDSL {
430
455
  // Get patterns for language
431
456
  const patterns = this.registry.getPatterns(language);
432
457
 
433
- // Create profile for matcher
434
- const profile: PatternMatcherProfile = {
435
- code: language,
436
- };
437
-
438
458
  // Every pattern is tried, and only a match that reads the WHOLE input counts; of those, the
439
459
  // highest priority wins, then the highest confidence, then the earliest pattern. The first match
440
460
  // used to win however much it read, so a trailing word was dropped without a word (the domain
@@ -445,7 +465,7 @@ class MultilingualDSLImpl implements MultilingualDSL {
445
465
  let longestPartial: PatternMatchResult | null = null;
446
466
  for (const pattern of patterns) {
447
467
  const mark = tokens.mark();
448
- const match = this.matcher.matchPattern(tokens, pattern, profile);
468
+ const match = this.match(tokens, pattern, language);
449
469
  tokens.reset(mark);
450
470
  if (!match) continue;
451
471
  if (match.consumedTokens < total) {
@@ -830,6 +850,109 @@ export function createMultilingualDSL(config: DSLConfig): MultilingualDSL {
830
850
  });
831
851
 
832
852
  // Create registry and implementation
833
- const registry = new DSLRegistry(effectiveConfig, extensionActions);
834
- return new MultilingualDSLImpl(effectiveConfig, registry, transformer, profileProvider);
853
+ const engine = engineFor(config);
854
+ const registry = new DSLRegistry(effectiveConfig, extensionActions, engine.generate);
855
+ return new MultilingualDSLImpl(effectiveConfig, registry, transformer, profileProvider, engine);
856
+ }
857
+
858
+ // =============================================================================
859
+ // The engines: how a DSL turns its schemas into patterns and reads with them
860
+ // =============================================================================
861
+
862
+ /**
863
+ * What differs between the engines {@link DSLConfig.engine} names: how each
864
+ * command's pattern is generated, how input is matched against it, and the
865
+ * renderer `render()` falls back to. Parsing's whole-input rule, refusals,
866
+ * translation's read-back, compile, explicit syntax and extensions are shared.
867
+ */
868
+ interface DSLEngine {
869
+ generate(schema: CommandSchema, profile: PatternGenLanguageProfile): LanguagePattern;
870
+ match(tokens: TokenStream, pattern: LanguagePattern, language: string): PatternMatchResult | null;
871
+ fallbackRenderer(config: DSLConfig, registry: DSLRegistry): DomainRenderFn;
872
+ }
873
+
874
+ /**
875
+ * The engine `config.engine` names. An unknown name throws rather than reading
876
+ * with the default: a domain that misspells `'core'` would otherwise measure
877
+ * the classic engine against its gate and take the result for the core's.
878
+ */
879
+ function engineFor(config: DSLConfig): DSLEngine {
880
+ const name = config.engine ?? 'classic';
881
+ if (name === 'classic') return classicEngine();
882
+ if (name === 'core') return coreEngine();
883
+ throw new Error(`Unknown DSL engine "${String(name)}" (expected "classic" or "core")`);
884
+ }
885
+
886
+ /** framework's first generator and matcher, and its schema renderer (`engine: 'classic'`). */
887
+ function classicEngine(): DSLEngine {
888
+ const matcher = new PatternMatcher();
889
+ return {
890
+ generate: (schema, profile) => generatePattern(schema, profile),
891
+ match: (tokens, pattern, language) => matcher.matchPattern(tokens, pattern, { code: language }),
892
+ fallbackRenderer: config =>
893
+ createDomainRenderer({
894
+ schemas: config.schemas,
895
+ profiles: config.languages.map(l => l.patternProfile),
896
+ }),
897
+ };
898
+ }
899
+
900
+ /**
901
+ * A domain profile as the schema-pattern generator reads it: every role marker
902
+ * with a side (the word order's default where the profile names none).
903
+ */
904
+ function schemaProfileOf(profile: PatternGenLanguageProfile): SchemaPatternProfile {
905
+ const side = profile.wordOrder === 'SOV' ? 'after' : 'before';
906
+ const roleMarkers: Record<string, NonNullable<SchemaPatternProfile['roleMarkers'][string]>> = {};
907
+ for (const [role, marker] of Object.entries(profile.roleMarkers ?? {})) {
908
+ roleMarkers[role] = {
909
+ primary: marker.primary,
910
+ ...(marker.alternatives ? { alternatives: marker.alternatives } : {}),
911
+ position: marker.position ?? side,
912
+ };
913
+ }
914
+ return {
915
+ code: profile.code,
916
+ wordOrder: profile.wordOrder,
917
+ keywords: profile.keywords,
918
+ roleMarkers,
919
+ };
920
+ }
921
+
922
+ /**
923
+ * The core (domains rethink Phase 4, `engine: 'core'`): the schema-pattern
924
+ * generator, the skeleton, the stock reader.
925
+ */
926
+ function coreEngine(): DSLEngine {
927
+ const generator = createSchemaPatternGenerator({ positionOrder: 'descending' });
928
+ const matcher = new MatchSkeleton(createStockReader(), stockConfidenceModel);
929
+ return {
930
+ generate: (schema, profile) => generator.generatePattern(schema, schemaProfileOf(profile)),
931
+ match: (tokens, pattern) => matcher.matchPattern(tokens, pattern),
932
+ fallbackRenderer: (config, registry) => {
933
+ const schemas = new Map(config.schemas.map(s => [s.action, s]));
934
+ const hooks = {
935
+ hasRealRole: (node: SemanticNode, role: string) => {
936
+ const value = node.roles.get(role) as { implicit?: unknown } | undefined;
937
+ return value !== undefined && value.implicit !== true;
938
+ },
939
+ writeRole: (token: RolePatternToken, node: SemanticNode) => {
940
+ if (!node.roles.has(token.role)) return null;
941
+ let value = extractRoleValue(node, token.role);
942
+ const spec = schemas.get(node.action)?.roles.find(r => r.role === token.role);
943
+ if (spec?.quoteMultiword && /\s/.test(value) && !/^(["']).*\1$/.test(value)) {
944
+ value = `"${value}"`;
945
+ }
946
+ return value || null;
947
+ },
948
+ };
949
+ return (node, language) => {
950
+ const patterns = registry.getPatterns(language).filter(p => p.command === node.action);
951
+ const pattern = selectRenderPattern(node, patterns, hooks);
952
+ if (!pattern) return null;
953
+ const text = renderPattern(node, pattern, hooks).join(' ');
954
+ return text || null;
955
+ };
956
+ },
957
+ };
835
958
  }
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';