@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
@@ -0,0 +1,100 @@
1
+ /**
2
+ * R10: Renderer-table coherence (opt-in).
3
+ *
4
+ * Some domains ship a natural-language renderer that maintains its own
5
+ * per-language keyword/marker tables separate from the profile data. This
6
+ * rule — when the domain passes `input.renderer` — asserts that:
7
+ * - Every `commandKeywords[action][lang]` matches `profile.keywords[action].primary`
8
+ * for that language.
9
+ * - Every `markers[name][lang]` appears as a markerOverride value somewhere
10
+ * in the schemas for that language (can't pin to a specific role since
11
+ * renderer marker names are arbitrary labels).
12
+ *
13
+ * Warnings only. Domains without `input.renderer` get a no-op pass — this
14
+ * rule is inherently opt-in because not every domain has round-trip
15
+ * rendering and not every renderer uses the same table shape.
16
+ *
17
+ * Catches the exact drift we fixed earlier this session: domain-sql's
18
+ * Turkish source marker was `dan` in the `get` schema but `den` in the
19
+ * renderer's MARKERS table. Had this rule existed, it would have warned
20
+ * the moment `get` was added with the wrong Turkish variant.
21
+ */
22
+
23
+ import type { LintFinding, LintRule } from '../types';
24
+
25
+ const RULE_ID = 'renderer-coherence';
26
+
27
+ export const rendererCoherenceRule: LintRule = input => {
28
+ if (!input.renderer) return [];
29
+ const findings: LintFinding[] = [];
30
+
31
+ // commandKeywords ↔ profile primaries
32
+ for (const [action, langMap] of Object.entries(input.renderer.commandKeywords)) {
33
+ for (const [lang, rendererWord] of Object.entries(langMap)) {
34
+ const profile = input.profiles.find(p => p.code === lang);
35
+ if (!profile) continue; // lang not registered; R6 handles that
36
+ const profileEntry = profile.keywords[action];
37
+ if (!profileEntry) {
38
+ findings.push({
39
+ rule: RULE_ID,
40
+ severity: 'warning',
41
+ message: `renderer has '${action}' keyword for ${lang} ("${rendererWord}") but profile has no entry for that action`,
42
+ context: { domain: input.name, lang, action, rendererWord },
43
+ });
44
+ continue;
45
+ }
46
+ const profilePrimary = profileEntry.primary;
47
+ const profileAlts = profileEntry.alternatives ?? [];
48
+ if (rendererWord !== profilePrimary && !profileAlts.includes(rendererWord)) {
49
+ findings.push({
50
+ rule: RULE_ID,
51
+ severity: 'warning',
52
+ message: `renderer '${action}' keyword for ${lang} is "${rendererWord}" but profile has primary="${profilePrimary}"${profileAlts.length ? ` (alternatives: ${profileAlts.join(', ')})` : ''}`,
53
+ context: {
54
+ domain: input.name,
55
+ lang,
56
+ action,
57
+ rendererWord,
58
+ profilePrimary,
59
+ },
60
+ });
61
+ }
62
+ }
63
+ }
64
+
65
+ // markers ↔ schema markerOverride values (loose check — any role in any
66
+ // schema for that lang must have this marker value somewhere)
67
+ const markerByLang = new Map<string, Set<string>>();
68
+ for (const schema of input.schemas) {
69
+ for (const role of schema.roles) {
70
+ if (!role.markerOverride) continue;
71
+ for (const [lang, word] of Object.entries(role.markerOverride)) {
72
+ const set = markerByLang.get(lang) ?? new Set<string>();
73
+ set.add(word);
74
+ markerByLang.set(lang, set);
75
+ }
76
+ }
77
+ }
78
+
79
+ for (const [markerName, langMap] of Object.entries(input.renderer.markers)) {
80
+ for (const [lang, rendererWord] of Object.entries(langMap)) {
81
+ const schemaMarkers = markerByLang.get(lang);
82
+ if (!schemaMarkers) continue; // lang has no markerOverride anywhere; skip
83
+ if (!schemaMarkers.has(rendererWord)) {
84
+ findings.push({
85
+ rule: RULE_ID,
86
+ severity: 'warning',
87
+ message: `renderer marker "${markerName}" in ${lang} is "${rendererWord}" but no schema role has that markerOverride value — likely drift`,
88
+ context: {
89
+ domain: input.name,
90
+ lang,
91
+ markerName,
92
+ rendererWord,
93
+ },
94
+ });
95
+ }
96
+ }
97
+ }
98
+
99
+ return findings;
100
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Schema-structure rules (R1, R2).
3
+ *
4
+ * Delegates to the validator the input brings (`DomainLintInput.validateSchema`): the domains pass
5
+ * `@lokascript/semantic`'s `validateCommandSchema`, whose checks are already solid. We only surface its
6
+ * error- and warning-level items; notes are dropped to keep linter output focused.
7
+ */
8
+
9
+ import type { LintFinding, LintRule } from '../types';
10
+
11
+ const RULE_ID = 'schema-structure';
12
+
13
+ export const schemaStructureRule: LintRule = input => {
14
+ const findings: LintFinding[] = [];
15
+ if (!input.validateSchema) return findings;
16
+
17
+ for (const schema of input.schemas) {
18
+ const result = input.validateSchema(schema);
19
+ for (const item of result.items) {
20
+ if (item.severity === 'note') continue; // skip notes — not actionable at lint time
21
+ findings.push({
22
+ rule: `${RULE_ID}:${item.code}`,
23
+ severity: item.severity,
24
+ message: `[${schema.action}] ${item.message}`,
25
+ context: {
26
+ domain: input.name,
27
+ action: String(schema.action),
28
+ code: item.code,
29
+ },
30
+ });
31
+ }
32
+ }
33
+
34
+ return findings;
35
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Core types for the domain linter.
3
+ */
4
+
5
+ import type { CommandSchema } from '../../../schema';
6
+ import type { LanguageTokenizer } from '../../../core/types';
7
+ import type { PatternGenLanguageProfile } from '../../../generation/pattern-generator';
8
+
9
+ export type Severity = 'error' | 'warning' | 'note';
10
+
11
+ export interface LintFinding {
12
+ readonly rule: string;
13
+ readonly severity: Severity;
14
+ readonly message: string;
15
+ /** Optional structured context: domain, language code, command action, role name, etc. */
16
+ readonly context?: Record<string, string | number | boolean>;
17
+ }
18
+
19
+ export interface LintResult {
20
+ readonly domain: string;
21
+ readonly findings: readonly LintFinding[];
22
+ /** Count helpers — derived from findings. */
23
+ readonly errorCount: number;
24
+ readonly warningCount: number;
25
+ }
26
+
27
+ /**
28
+ * Optional renderer metadata for R10 (renderer-coherence check).
29
+ * Domains without a renderer or without standard table shape omit this field.
30
+ */
31
+ export interface RendererTables {
32
+ /** Keyed by action name → lang code → keyword. */
33
+ readonly commandKeywords: Record<string, Record<string, string>>;
34
+ /** Keyed by marker name → lang code → marker word. */
35
+ readonly markers: Record<string, Record<string, string>>;
36
+ }
37
+
38
+ /**
39
+ * Matcher for an acknowledged-but-unfixed finding. Each entry is an object
40
+ * whose fields are compared against the finding's rule + context. A finding
41
+ * matches a waiver if every field in the waiver matches the corresponding
42
+ * field on the finding (or finding.context). Matched findings are dropped
43
+ * from the returned result entirely.
44
+ *
45
+ * Use sparingly — waivers are real debt that should have a plan to resolve.
46
+ */
47
+ export interface LintWaiver {
48
+ readonly rule: string;
49
+ readonly reason: string; // human-readable; not matched against — for audit
50
+ readonly matches?: Record<string, string | number | boolean>;
51
+ }
52
+
53
+ export interface DomainLintInput {
54
+ readonly name: string;
55
+ readonly schemas: readonly CommandSchema[];
56
+ readonly profiles: readonly PatternGenLanguageProfile[];
57
+ readonly tokenizers: Readonly<Record<string, LanguageTokenizer>>;
58
+ readonly renderer?: RendererTables;
59
+ readonly waivers?: readonly LintWaiver[];
60
+ /**
61
+ * The schema validator R1/R2 (schema-structure) run. The domains pass `@lokascript/semantic`'s
62
+ * `validateCommandSchema`; framework cannot import semantic, which depends on it. Without one, the
63
+ * rule finds nothing.
64
+ */
65
+ readonly validateSchema?: SchemaValidator;
66
+ }
67
+
68
+ /** One finding of a schema validator. */
69
+ export interface SchemaValidationFinding {
70
+ readonly code: string;
71
+ readonly severity: Severity;
72
+ readonly message: string;
73
+ }
74
+
75
+ /** A schema validator: `@lokascript/semantic`'s `validateCommandSchema` has this shape. */
76
+ export type SchemaValidator = (schema: CommandSchema) => {
77
+ readonly items: readonly SchemaValidationFinding[];
78
+ };
79
+
80
+ /**
81
+ * A single rule is a pure function producing findings.
82
+ */
83
+ export type LintRule = (input: DomainLintInput) => LintFinding[];
@@ -0,0 +1,118 @@
1
+ /**
2
+ * The round-trip gate kit on framework's SQL fixture DSL: the commands it writes,
3
+ * the outcomes it reads, and the baseline and snapshot it compares with.
4
+ */
5
+ import { describe, expect, it } from 'vitest';
6
+ import { createSQLDSL, selectCommand } from '../../__test__/fixtures/sql-dsl';
7
+ import {
8
+ commandsFor,
9
+ compare,
10
+ countsOf,
11
+ listingOf,
12
+ runRoundTrip,
13
+ snapshotChanges,
14
+ snapshotOf,
15
+ type GateBaseline,
16
+ type GateFixture,
17
+ } from './round-trip';
18
+
19
+ const fixture: GateFixture = {
20
+ oracle: 'compile',
21
+ values: { columns: ['name', 'email'], source: ['users'], condition: ['age'] },
22
+ };
23
+
24
+ const results = runRoundTrip('sql', createSQLDSL(), [selectCommand], fixture);
25
+
26
+ function baselineOf(listing: ReturnType<typeof listingOf>): GateBaseline {
27
+ return {
28
+ upstream: {},
29
+ counts: { sql: countsOf(results) },
30
+ roundTrip: { sql: listing.roundTrip },
31
+ leftover: { sql: listing.leftover },
32
+ };
33
+ }
34
+
35
+ describe('runRoundTrip', () => {
36
+ it('writes each schema with its required roles, all its roles, and each further fixed word', () => {
37
+ expect(commandsFor([selectCommand], fixture)).toEqual([
38
+ '[select columns:name source:users]',
39
+ '[select columns:email source:users]',
40
+ '[select columns:name source:users condition:age]',
41
+ '[select columns:email source:users condition:age]',
42
+ ]);
43
+ });
44
+
45
+ it('reads every render back, in every language, and tries each with an unknown word after it', () => {
46
+ expect(countsOf(results)).toEqual({
47
+ roundTrip: { cases: 8, pass: 8, refused: 0, silent: 0 },
48
+ // The `where` condition is greedy: it takes the unknown word as text (an open-text role).
49
+ leftover: { cases: 8, pass: 0, refused: 4, silent: 4 },
50
+ });
51
+ expect(results.find(r => r.kind === 'roundTrip' && r.lang === 'es')).toMatchObject({
52
+ text: 'seleccionar name de users',
53
+ read: 'select columns:expression=name source:expression=users',
54
+ });
55
+ });
56
+ });
57
+
58
+ describe('compare', () => {
59
+ it('finds no drift against the listing of the same results', () => {
60
+ expect(compare('sql', results, baselineOf(listingOf(results)))).toEqual({
61
+ worse: [],
62
+ better: [],
63
+ stale: [],
64
+ });
65
+ });
66
+
67
+ it('reports a case worse than listed, one better, and a listed case that is gone', () => {
68
+ const listing = listingOf(results);
69
+ const accepted = Object.keys(listing.leftover)[0]!;
70
+ const better = { ...listing.leftover };
71
+ delete better[accepted];
72
+ const drift = compare('sql', results, {
73
+ ...baselineOf({ roundTrip: listing.roundTrip, leftover: better }),
74
+ roundTrip: {
75
+ sql: {
76
+ '[select columns:name source:users]': {
77
+ es: { outcome: 'refused' },
78
+ },
79
+ '[gone]': { en: { outcome: 'silent' } },
80
+ },
81
+ },
82
+ });
83
+ expect(drift.worse.map(d => [d.kind, d.source, d.lang, d.listed, d.now])).toEqual([
84
+ ['leftover', accepted, 'en', 'refused', 'silent'],
85
+ ['leftover', accepted, 'es', 'refused', 'silent'],
86
+ ]);
87
+ expect(drift.better).toEqual([
88
+ {
89
+ kind: 'roundTrip',
90
+ source: '[select columns:name source:users]',
91
+ lang: 'es',
92
+ listed: 'refused',
93
+ now: 'pass',
94
+ },
95
+ ]);
96
+ expect(drift.stale).toEqual([{ kind: 'roundTrip', source: '[gone]', lang: 'en' }]);
97
+ });
98
+ });
99
+
100
+ describe('the parse snapshot', () => {
101
+ it('shows a parse that moved', () => {
102
+ const parses = snapshotOf(results);
103
+ const source = '[select columns:name source:users]';
104
+ const moved = {
105
+ upstream: {},
106
+ parses: { sql: { ...parses, [source]: { ...parses[source], es: 'select' } } },
107
+ };
108
+ expect(snapshotChanges('sql', results, moved)).toEqual([
109
+ {
110
+ source,
111
+ lang: 'es',
112
+ was: 'select',
113
+ now: 'select columns:expression=name source:expression=users',
114
+ },
115
+ ]);
116
+ expect(snapshotChanges('sql', results, { upstream: {}, parses: { sql: parses } })).toEqual([]);
117
+ });
118
+ });