@lokascript/framework 2.7.2 → 2.9.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 (55) hide show
  1. package/CHANGELOG.md +393 -0
  2. package/dist/api/create-dsl.d.ts +93 -1
  3. package/dist/api/create-dsl.d.ts.map +1 -1
  4. package/dist/api/domain-registry.d.ts +5 -3
  5. package/dist/api/domain-registry.d.ts.map +1 -1
  6. package/dist/api/index.js +238 -20
  7. package/dist/api/index.js.map +1 -1
  8. package/dist/core/index.js +67 -7
  9. package/dist/core/index.js.map +1 -1
  10. package/dist/core/tokenization/base-tokenizer.d.ts +39 -3
  11. package/dist/core/tokenization/base-tokenizer.d.ts.map +1 -1
  12. package/dist/core/tokenization/extractors.d.ts +6 -0
  13. package/dist/core/tokenization/extractors.d.ts.map +1 -1
  14. package/dist/core/tokenization/index.js +67 -7
  15. package/dist/core/tokenization/index.js.map +1 -1
  16. package/dist/core/tokenization/token-utils.d.ts +15 -0
  17. package/dist/core/tokenization/token-utils.d.ts.map +1 -1
  18. package/dist/generation/index.js +75 -48
  19. package/dist/generation/index.js.map +1 -1
  20. package/dist/generation/pattern-generator.d.ts +8 -1
  21. package/dist/generation/pattern-generator.d.ts.map +1 -1
  22. package/dist/generation/renderer.d.ts +53 -1
  23. package/dist/generation/renderer.d.ts.map +1 -1
  24. package/dist/index.cjs +349 -118
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.ts +4 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +345 -118
  29. package/dist/index.js.map +1 -1
  30. package/dist/interfaces/value-extractor.d.ts +5 -0
  31. package/dist/interfaces/value-extractor.d.ts.map +1 -1
  32. package/dist/multilingual/index.js +66 -7
  33. package/dist/multilingual/index.js.map +1 -1
  34. package/dist/testing/index.js +4 -12
  35. package/dist/testing/index.js.map +1 -1
  36. package/package.json +4 -3
  37. package/src/api/create-dsl.test.ts +11 -0
  38. package/src/api/create-dsl.ts +278 -9
  39. package/src/api/domain-registry.ts +15 -10
  40. package/src/api/extensions.test.ts +322 -0
  41. package/src/core/tokenization/base-tokenizer.ts +78 -9
  42. package/src/core/tokenization/colon-qualifier.test.ts +129 -0
  43. package/src/core/tokenization/css-selector-extractor.test.ts +67 -0
  44. package/src/core/tokenization/extractors.ts +6 -0
  45. package/src/core/tokenization/token-utils.ts +18 -0
  46. package/src/generation/domain-renderer.test.ts +172 -0
  47. package/src/generation/pattern-generator.test.ts +102 -0
  48. package/src/generation/pattern-generator.ts +32 -20
  49. package/src/generation/renderer.test.ts +243 -4
  50. package/src/generation/renderer.ts +188 -45
  51. package/src/index.ts +9 -1
  52. package/src/interfaces/value-extractor.ts +50 -0
  53. package/src/ir/protocol-json.test.ts +21 -0
  54. package/src/ir/references.test.ts +5 -2
  55. package/src/prompts/prompt-generator.ts +4 -1
package/dist/api/index.js CHANGED
@@ -1282,10 +1282,12 @@ function buildTokens(schema, profile, keyword, alternatives) {
1282
1282
  addRoleWithMarker(tokens, role, profile);
1283
1283
  }
1284
1284
  } else if (profile.wordOrder === "SOV") {
1285
+ const preVerb = [];
1286
+ const postVerb = [];
1285
1287
  for (const role of sortedRoles) {
1286
- addRoleWithMarker(tokens, role, profile);
1288
+ addRoleWithMarker(role.sovSlot === "postVerb" ? postVerb : preVerb, role, profile);
1287
1289
  }
1288
- tokens.push(keywordToken);
1290
+ tokens.push(...preVerb, keywordToken, ...postVerb);
1289
1291
  } else if (profile.wordOrder === "VSO") {
1290
1292
  tokens.push(keywordToken);
1291
1293
  for (const role of sortedRoles) {
@@ -1368,28 +1370,130 @@ function buildExtractionRules(schema, profile) {
1368
1370
  return rules;
1369
1371
  }
1370
1372
  function buildFormatString(schema, profile, keyword) {
1371
- const parts = [];
1372
- if (profile.wordOrder === "SVO" || profile.wordOrder === "VSO") {
1373
- parts.push(keyword);
1374
- }
1375
- for (const role of schema.roles) {
1373
+ const isSOV = profile.wordOrder === "SOV";
1374
+ const preVerb = [];
1375
+ const postVerb = [];
1376
+ const sortedRoles = sortRolesByWordOrder(schema.roles, profile.wordOrder);
1377
+ for (const role of sortedRoles) {
1376
1378
  const marker = getMarkerForRole(role, profile);
1377
1379
  const roleName = `{${role.role}}`;
1380
+ const bucket = isSOV && role.sovSlot === "postVerb" ? postVerb : preVerb;
1378
1381
  if (marker) {
1379
1382
  const markerInfo = profile.roleMarkers?.[role.role];
1380
1383
  if (markerInfo?.position === "after") {
1381
- parts.push(`${roleName} ${marker}`);
1384
+ bucket.push(`${roleName} ${marker}`);
1382
1385
  } else {
1383
- parts.push(`${marker} ${roleName}`);
1386
+ bucket.push(`${marker} ${roleName}`);
1384
1387
  }
1385
1388
  } else {
1386
- parts.push(roleName);
1389
+ bucket.push(roleName);
1387
1390
  }
1388
1391
  }
1389
- if (profile.wordOrder === "SOV") {
1390
- parts.push(keyword);
1392
+ return (isSOV ? [...preVerb, keyword, ...postVerb] : [keyword, ...preVerb, ...postVerb]).join(
1393
+ " "
1394
+ );
1395
+ }
1396
+
1397
+ // src/generation/renderer.ts
1398
+ function lookupKeyword(keywords, action, language) {
1399
+ return keywords[action]?.[language] ?? action;
1400
+ }
1401
+ function buildPhrase(...parts) {
1402
+ return parts.filter(Boolean).join(" ");
1403
+ }
1404
+ function buildTablesFromProfiles(schemas, profiles) {
1405
+ const keywords = {};
1406
+ const markers = {};
1407
+ for (const profile of profiles) {
1408
+ for (const [action, kw] of Object.entries(profile.keywords)) {
1409
+ if (!keywords[action]) keywords[action] = {};
1410
+ keywords[action][profile.code] = kw.primary;
1411
+ }
1391
1412
  }
1392
- return parts.join(" ");
1413
+ for (const schema of schemas) {
1414
+ for (const role of schema.roles) {
1415
+ if (role.markerOverride) {
1416
+ const markerKey = role.role;
1417
+ if (!markers[markerKey]) markers[markerKey] = {};
1418
+ for (const [lang, markerText] of Object.entries(role.markerOverride)) {
1419
+ markers[markerKey][lang] = markerText;
1420
+ }
1421
+ }
1422
+ }
1423
+ }
1424
+ for (const profile of profiles) {
1425
+ if (profile.roleMarkers) {
1426
+ for (const [markerKey, markerDef] of Object.entries(profile.roleMarkers)) {
1427
+ if (!markers[markerKey]) markers[markerKey] = {};
1428
+ markers[markerKey][profile.code] = markerDef.primary;
1429
+ }
1430
+ }
1431
+ }
1432
+ return { keywords, markers };
1433
+ }
1434
+ function detectWordOrders(profiles) {
1435
+ const sovLanguages = /* @__PURE__ */ new Set();
1436
+ const vsoLanguages = /* @__PURE__ */ new Set();
1437
+ for (const profile of profiles) {
1438
+ if (profile.wordOrder === "SOV") sovLanguages.add(profile.code);
1439
+ if (profile.wordOrder === "VSO") vsoLanguages.add(profile.code);
1440
+ }
1441
+ return { sovLanguages, vsoLanguages };
1442
+ }
1443
+ function createDomainRenderer(config) {
1444
+ const { schemas, profiles, overrides } = config;
1445
+ let tables;
1446
+ return (node, language) => {
1447
+ const override = overrides?.[node.action];
1448
+ if (override) return override(node, language);
1449
+ tables ?? (tables = buildRenderTables(schemas, profiles));
1450
+ return renderFromSchema(node, language, tables);
1451
+ };
1452
+ }
1453
+ function buildRenderTables(schemas, profiles) {
1454
+ const { keywords, markers } = buildTablesFromProfiles(schemas, profiles);
1455
+ const { sovLanguages } = detectWordOrders(profiles);
1456
+ const markerPositions = {};
1457
+ for (const profile of profiles) {
1458
+ for (const [role, markerDef] of Object.entries(profile.roleMarkers ?? {})) {
1459
+ if (!markerDef.position) continue;
1460
+ markerPositions[role] ?? (markerPositions[role] = {});
1461
+ markerPositions[role][profile.code] = markerDef.position;
1462
+ }
1463
+ }
1464
+ const schemaMap = /* @__PURE__ */ new Map();
1465
+ for (const s of schemas) schemaMap.set(s.action, s);
1466
+ return { keywords, markers, markerPositions, sovLanguages, schemaMap };
1467
+ }
1468
+ function renderFromSchema(node, language, tables) {
1469
+ const schema = tables.schemaMap.get(node.action);
1470
+ if (!schema) return null;
1471
+ const keyword = lookupKeyword(tables.keywords, node.action, language);
1472
+ const isSOV = tables.sovLanguages.has(language);
1473
+ const ordered = sortRolesByWordOrder([...schema.roles], isSOV ? "SOV" : "SVO");
1474
+ const preVerb = [];
1475
+ const postVerb = [];
1476
+ for (const role of ordered) {
1477
+ let value = extractRoleValue(node, role.role);
1478
+ if (!value && role.default !== void 0) {
1479
+ value = String(extractValue(role.default));
1480
+ }
1481
+ if (!value) continue;
1482
+ if (role.quoteMultiword && /\s/.test(value) && !/^(["']).*\1$/.test(value)) {
1483
+ value = `"${value}"`;
1484
+ }
1485
+ const markerText = role.renderOverride?.[language] ?? role.markerOverride?.[language] ?? role.renderOverride?.["*"] ?? tables.markers[role.role]?.[language];
1486
+ const markerPosition = role.markerPositionOverride?.[language] ?? role.markerPosition ?? tables.markerPositions[role.role]?.[language] ?? (isSOV ? "after" : "before");
1487
+ const bucket = isSOV && role.sovSlot === "postVerb" ? postVerb : preVerb;
1488
+ if (markerText && markerPosition === "before") {
1489
+ bucket.push(markerText, value);
1490
+ } else if (markerText) {
1491
+ bucket.push(value, markerText);
1492
+ } else {
1493
+ bucket.push(value);
1494
+ }
1495
+ }
1496
+ return isSOV ? buildPhrase(...preVerb, keyword, ...postVerb) : buildPhrase(keyword, ...preVerb, ...postVerb);
1393
1497
  }
1394
1498
 
1395
1499
  // src/grammar/types.ts
@@ -1621,10 +1725,17 @@ import { renderExplicit, renderDocument } from "@lokascript/intent";
1621
1725
 
1622
1726
  // src/api/create-dsl.ts
1623
1727
  var DSLRegistry = class {
1624
- constructor(config) {
1728
+ /**
1729
+ * @param partiallyTranslatedActions Actions permitted to lack a keyword in
1730
+ * some configured languages — extension commands, which may be added for a
1731
+ * subset of the DSL's languages. A built-in command missing a keyword stays
1732
+ * an error, since that is a domain-authoring mistake.
1733
+ */
1734
+ constructor(config, partiallyTranslatedActions = /* @__PURE__ */ new Set()) {
1625
1735
  this.patterns = /* @__PURE__ */ new Map();
1626
1736
  this.tokenizers = /* @__PURE__ */ new Map();
1627
1737
  this.schemas = config.schemas;
1738
+ this.partiallyTranslatedActions = partiallyTranslatedActions;
1628
1739
  for (const lang of config.languages) {
1629
1740
  this.registerLanguage(lang);
1630
1741
  }
@@ -1633,6 +1744,9 @@ var DSLRegistry = class {
1633
1744
  this.tokenizers.set(lang.code, lang.tokenizer);
1634
1745
  const patterns = [];
1635
1746
  for (const schema of this.schemas) {
1747
+ if (this.partiallyTranslatedActions.has(schema.action) && !lang.patternProfile.keywords[schema.action]) {
1748
+ continue;
1749
+ }
1636
1750
  const pattern = generatePattern(schema, lang.patternProfile);
1637
1751
  patterns.push(pattern);
1638
1752
  }
@@ -1649,13 +1763,25 @@ var DSLRegistry = class {
1649
1763
  }
1650
1764
  };
1651
1765
  var MultilingualDSLImpl = class {
1652
- constructor(config, registry, transformer) {
1766
+ constructor(config, registry, transformer, profileProvider) {
1653
1767
  this.registry = registry;
1654
1768
  this.matcher = new PatternMatcher();
1655
1769
  this.transformer = transformer;
1770
+ this.profileProvider = profileProvider;
1656
1771
  if (config.codeGenerator) {
1657
1772
  this.codeGenerator = config.codeGenerator;
1658
1773
  }
1774
+ if (config.renderer) {
1775
+ this.domainRenderer = config.renderer;
1776
+ }
1777
+ this.extensionRenderers = /* @__PURE__ */ new Map();
1778
+ for (const extension of config.extensions ?? []) {
1779
+ if (extension.render) this.extensionRenderers.set(extension.schema.action, extension.render);
1780
+ }
1781
+ this.schemaFallbackRenderer = createDomainRenderer({
1782
+ schemas: config.schemas,
1783
+ profiles: config.languages.map((l) => l.patternProfile)
1784
+ });
1659
1785
  const schemaMap = new Map(config.schemas.map((s) => [s.action, s]));
1660
1786
  this.schemaLookup = {
1661
1787
  getSchema: (action) => schemaMap.get(action)
@@ -1717,6 +1843,13 @@ var MultilingualDSLImpl = class {
1717
1843
  }
1718
1844
  translate(input, fromLanguage, toLanguage) {
1719
1845
  if (isExplicitSyntax(input)) return input;
1846
+ for (const language of [fromLanguage, toLanguage]) {
1847
+ if (!this.profileProvider.getProfile(language)) {
1848
+ throw new Error(
1849
+ `translate() requires a grammar profile for language "${language}", but none is configured. Set 'grammarProfile' on the LanguageConfig for "${language}" in createMultilingualDSL() (parse/validate/compile do not need it), or inject a custom 'profileProvider'.`
1850
+ );
1851
+ }
1852
+ }
1720
1853
  return this.transformer.transform(input, fromLanguage, toLanguage);
1721
1854
  }
1722
1855
  compile(input, language) {
@@ -1745,6 +1878,18 @@ var MultilingualDSLImpl = class {
1745
1878
  };
1746
1879
  }
1747
1880
  }
1881
+ render(node, language) {
1882
+ const extensionRenderer = this.extensionRenderers.get(node.action);
1883
+ if (extensionRenderer) {
1884
+ const rendered = extensionRenderer(node, language);
1885
+ if (rendered != null) return rendered;
1886
+ }
1887
+ if (this.domainRenderer) {
1888
+ const rendered = this.domainRenderer(node, language);
1889
+ if (rendered != null) return rendered;
1890
+ }
1891
+ return this.schemaFallbackRenderer(node, language);
1892
+ }
1748
1893
  getSupportedLanguages() {
1749
1894
  return this.registry.getSupportedLanguages();
1750
1895
  }
@@ -1769,15 +1914,80 @@ function createDefaultProfileProvider(config) {
1769
1914
  }
1770
1915
  return new InMemoryProfileProvider(profiles);
1771
1916
  }
1917
+ function applyExtensions(config) {
1918
+ const extensions = config.extensions;
1919
+ if (!extensions || extensions.length === 0) return config;
1920
+ const configuredLanguages = new Set(config.languages.map((l) => l.code));
1921
+ const actions = new Set(config.schemas.map((s) => s.action));
1922
+ for (const extension of extensions) {
1923
+ const { action } = extension.schema;
1924
+ if (actions.has(action)) {
1925
+ throw new Error(
1926
+ `Extension command "${action}" collides with a command this DSL already defines. Pick a different action name.`
1927
+ );
1928
+ }
1929
+ actions.add(action);
1930
+ for (const code of Object.keys(extension.vocabulary)) {
1931
+ if (!configuredLanguages.has(code)) {
1932
+ throw new Error(
1933
+ `Extension command "${action}" supplies vocabulary for language "${code}", which is not configured on this DSL. Configured languages: ${[...configuredLanguages].join(", ")}.`
1934
+ );
1935
+ }
1936
+ }
1937
+ }
1938
+ const schemas = [...config.schemas, ...extensions.map((e) => e.schema)];
1939
+ const languages = config.languages.map((lang) => {
1940
+ const keywords = { ...lang.patternProfile.keywords };
1941
+ let roleMarkers = lang.patternProfile.roleMarkers;
1942
+ for (const extension of extensions) {
1943
+ const vocabulary = extension.vocabulary[lang.code];
1944
+ if (!vocabulary) continue;
1945
+ keywords[extension.schema.action] = { ...vocabulary.keyword };
1946
+ if (vocabulary.roleMarkers) {
1947
+ roleMarkers = { ...roleMarkers, ...vocabulary.roleMarkers };
1948
+ }
1949
+ }
1950
+ return {
1951
+ ...lang,
1952
+ patternProfile: {
1953
+ ...lang.patternProfile,
1954
+ keywords,
1955
+ ...roleMarkers !== void 0 && { roleMarkers }
1956
+ }
1957
+ };
1958
+ });
1959
+ const extensionGenerators = new Map(
1960
+ extensions.filter((e) => e.generate).map((e) => [e.schema.action, e.generate])
1961
+ );
1962
+ const baseGenerator = config.codeGenerator;
1963
+ const codeGenerator = extensionGenerators.size > 0 ? {
1964
+ generate(node) {
1965
+ const generate = extensionGenerators.get(node.action);
1966
+ if (generate) return generate(node);
1967
+ if (!baseGenerator) {
1968
+ throw new Error(`No code generator for action "${node.action}"`);
1969
+ }
1970
+ return baseGenerator.generate(node);
1971
+ }
1972
+ } : baseGenerator;
1973
+ return {
1974
+ ...config,
1975
+ schemas,
1976
+ languages,
1977
+ ...codeGenerator !== void 0 && { codeGenerator }
1978
+ };
1979
+ }
1772
1980
  function createMultilingualDSL(config) {
1773
- const dictionary = config.dictionary ?? createDefaultDictionary(config);
1774
- const profileProvider = config.profileProvider ?? createDefaultProfileProvider(config);
1981
+ const effectiveConfig = applyExtensions(config);
1982
+ const extensionActions = new Set((config.extensions ?? []).map((e) => e.schema.action));
1983
+ const dictionary = effectiveConfig.dictionary ?? createDefaultDictionary(effectiveConfig);
1984
+ const profileProvider = effectiveConfig.profileProvider ?? createDefaultProfileProvider(effectiveConfig);
1775
1985
  const transformer = new GrammarTransformer({
1776
1986
  dictionary,
1777
1987
  profileProvider
1778
1988
  });
1779
- const registry = new DSLRegistry(config);
1780
- return new MultilingualDSLImpl(config, registry, transformer);
1989
+ const registry = new DSLRegistry(effectiveConfig, extensionActions);
1990
+ return new MultilingualDSLImpl(effectiveConfig, registry, transformer, profileProvider);
1781
1991
  }
1782
1992
 
1783
1993
  // src/prompts/prompt-generator.ts
@@ -1855,7 +2065,9 @@ function buildProtocolSection() {
1855
2065
  3. No spaces around the colon in role:value pairs
1856
2066
  4. Strings with spaces must be quoted: \`patient:"hello world"\`
1857
2067
  5. Selectors start with \`#\`, \`.\`, \`[\`, \`@\`, or \`*\`
1858
- 6. Output must be valid bracket syntax: \`[action role:value ...]\``;
2068
+ 6. A selector containing a space, a combinator (\`>\` \`+\` \`~\`), or a comma must use a selector literal: \`patient:<ul > li/>\`, \`patient:<.a, .b/>\`
2069
+ 7. Inside a structural role (\`body\`, \`then\`, \`else\`, \`condition\`, \`loop-body\`, \`variable\`, \`catch\`, \`finally\`) a \`[...]\` value is always a nested command. Write an attribute selector there as \`condition:<[data-active]/>\`
2070
+ 8. Output must be valid bracket syntax: \`[action role:value ...]\``;
1859
2071
  return {
1860
2072
  id: "protocol",
1861
2073
  title: "LSE Protocol",
@@ -1868,6 +2080,7 @@ function buildValueTypeSection() {
1868
2080
 
1869
2081
  | Type | Syntax | Example |
1870
2082
  |------|--------|---------|
2083
+ | Selector literal | Delimited by \`<\` and \`/>\` | \`<ul > li/>\`, \`<.a, .b/>\`, \`<[data-id]/>\` |
1871
2084
  | Selector | Starts with \`#\` \`.\` \`[\` \`@\` \`*\` | \`#button\`, \`.active\`, \`[data-id]\` |
1872
2085
  | String | Quoted with \`"\` or \`'\` | \`"hello world"\`, \`'json'\` |
1873
2086
  | Boolean | Exact: \`true\` / \`false\` | \`visible:true\` |
@@ -2736,6 +2949,11 @@ var DomainRegistry = class {
2736
2949
  rendered = typeof renderer === "function" ? renderer(node, to) : renderer.render(node, to);
2737
2950
  } catch {
2738
2951
  }
2952
+ } else if (dsl.render) {
2953
+ try {
2954
+ rendered = dsl.render(node, to);
2955
+ } catch {
2956
+ }
2739
2957
  }
2740
2958
  const roles = {};
2741
2959
  for (const [key, value] of node.roles) {