@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.
- package/CHANGELOG.md +121 -1
- package/dist/api/create-dsl.d.ts +18 -2
- package/dist/api/create-dsl.d.ts.map +1 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +1799 -713
- package/dist/api/index.js.map +1 -1
- package/dist/api/translation-loss.d.ts +84 -0
- package/dist/api/translation-loss.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1057 -684
- package/dist/core/index.js.map +1 -1
- package/dist/core/matching/confidence-model.d.ts +54 -0
- package/dist/core/matching/confidence-model.d.ts.map +1 -0
- package/dist/core/matching/index.d.ts +12 -0
- package/dist/core/matching/index.d.ts.map +1 -0
- package/dist/core/matching/match-skeleton.d.ts +114 -0
- package/dist/core/matching/match-skeleton.d.ts.map +1 -0
- package/dist/core/matching/stock-confidence.d.ts +15 -0
- package/dist/core/matching/stock-confidence.d.ts.map +1 -0
- package/dist/core/matching/stock-reader.d.ts +63 -0
- package/dist/core/matching/stock-reader.d.ts.map +1 -0
- package/dist/core/matching/stock-reading.d.ts +44 -0
- package/dist/core/matching/stock-reading.d.ts.map +1 -0
- package/dist/core/matching/value-reader.d.ts +62 -0
- package/dist/core/matching/value-reader.d.ts.map +1 -0
- package/dist/core/pattern-matching/index.js +561 -660
- package/dist/core/pattern-matching/index.js.map +1 -1
- package/dist/core/pattern-matching/pattern-matcher.d.ts +0 -117
- package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -1
- package/dist/core/types.d.ts +53 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/generation/index.d.ts +2 -0
- package/dist/generation/index.d.ts.map +1 -1
- package/dist/generation/index.js +593 -1
- package/dist/generation/index.js.map +1 -1
- package/dist/generation/pattern-render.d.ts +72 -0
- package/dist/generation/pattern-render.d.ts.map +1 -0
- package/dist/generation/schema-patterns.d.ts +152 -0
- package/dist/generation/schema-patterns.d.ts.map +1 -0
- package/dist/index.cjs +2016 -904
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1998 -903
- package/dist/index.js.map +1 -1
- package/dist/ir/index.js.map +1 -1
- package/dist/parsing/index.js +561 -660
- package/dist/parsing/index.js.map +1 -1
- package/dist/testing/gate/index.d.ts +8 -0
- package/dist/testing/gate/index.d.ts.map +1 -0
- package/dist/testing/gate/index.js +664 -0
- package/dist/testing/gate/index.js.map +1 -0
- package/dist/testing/gate/lint/format.d.ts +7 -0
- package/dist/testing/gate/lint/format.d.ts.map +1 -0
- package/dist/testing/gate/lint/index.d.ts +30 -0
- package/dist/testing/gate/lint/index.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/extractor-coverage.d.ts +18 -0
- package/dist/testing/gate/lint/rules/extractor-coverage.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/index.d.ts +14 -0
- package/dist/testing/gate/lint/rules/index.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/keyword-classification.d.ts +19 -0
- package/dist/testing/gate/lint/rules/keyword-classification.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/keyword-coverage.d.ts +13 -0
- package/dist/testing/gate/lint/rules/keyword-coverage.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/marker-tokenization.d.ts +20 -0
- package/dist/testing/gate/lint/rules/marker-tokenization.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/position-ordering.d.ts +25 -0
- package/dist/testing/gate/lint/rules/position-ordering.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/renderer-coherence.d.ts +24 -0
- package/dist/testing/gate/lint/rules/renderer-coherence.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/schema-rules.d.ts +10 -0
- package/dist/testing/gate/lint/rules/schema-rules.d.ts.map +1 -0
- package/dist/testing/gate/lint/types.d.ts +74 -0
- package/dist/testing/gate/lint/types.d.ts.map +1 -0
- package/dist/testing/gate/round-trip.d.ts +184 -0
- package/dist/testing/gate/round-trip.d.ts.map +1 -0
- package/package.json +6 -2
- package/src/__test__/fixtures/sql-dsl.ts +8 -2
- package/src/api/create-dsl-engine.test.ts +150 -0
- package/src/api/create-dsl.test.ts +7 -0
- package/src/api/create-dsl.ts +173 -22
- package/src/api/index.ts +1 -0
- package/src/api/translation-loss.test.ts +89 -0
- package/src/api/translation-loss.ts +128 -0
- package/src/core/index.ts +1 -0
- package/src/core/matching/confidence-model.ts +58 -0
- package/src/core/matching/index.ts +12 -0
- package/src/core/matching/match-skeleton.test.ts +179 -0
- package/src/core/matching/match-skeleton.ts +621 -0
- package/src/core/matching/stock-confidence.ts +238 -0
- package/src/core/matching/stock-reader.test.ts +265 -0
- package/src/core/matching/stock-reader.ts +205 -0
- package/src/core/matching/stock-reading.ts +681 -0
- package/src/core/matching/value-reader.ts +85 -0
- package/src/core/pattern-matching/pattern-matcher.ts +11 -876
- package/src/core/types.ts +53 -0
- package/src/generation/index.ts +2 -0
- package/src/generation/pattern-render.ts +378 -0
- package/src/generation/schema-patterns.test.ts +139 -0
- package/src/generation/schema-patterns.ts +974 -0
- package/src/testing/gate/index.ts +8 -0
- package/src/testing/gate/lint/format.ts +23 -0
- package/src/testing/gate/lint/index.ts +73 -0
- package/src/testing/gate/lint/lint.test.ts +131 -0
- package/src/testing/gate/lint/rules/extractor-coverage.ts +63 -0
- package/src/testing/gate/lint/rules/index.ts +37 -0
- package/src/testing/gate/lint/rules/keyword-classification.ts +52 -0
- package/src/testing/gate/lint/rules/keyword-coverage.ts +38 -0
- package/src/testing/gate/lint/rules/marker-tokenization.ts +129 -0
- package/src/testing/gate/lint/rules/position-ordering.ts +87 -0
- package/src/testing/gate/lint/rules/renderer-coherence.ts +100 -0
- package/src/testing/gate/lint/rules/schema-rules.ts +35 -0
- package/src/testing/gate/lint/types.ts +83 -0
- package/src/testing/gate/round-trip.test.ts +208 -0
- package/src/testing/gate/round-trip.ts +503 -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,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The round-trip gate kit on framework's SQL fixture DSL: the commands it writes,
|
|
3
|
+
* the outcomes it reads (an unknown word read into a role among them), and the
|
|
4
|
+
* baseline and snapshot it compares with.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { createSQLDSL, selectCommand, sqlDSLConfig } from '../../__test__/fixtures/sql-dsl';
|
|
8
|
+
import { createMultilingualDSL } from '../../api/create-dsl';
|
|
9
|
+
import {
|
|
10
|
+
commandsFor,
|
|
11
|
+
compare,
|
|
12
|
+
countsOf,
|
|
13
|
+
listingOf,
|
|
14
|
+
readsWord,
|
|
15
|
+
runRoundTrip,
|
|
16
|
+
snapshotChanges,
|
|
17
|
+
snapshotOf,
|
|
18
|
+
type GateBaseline,
|
|
19
|
+
type GateFixture,
|
|
20
|
+
} from './round-trip';
|
|
21
|
+
|
|
22
|
+
const fixture: GateFixture = {
|
|
23
|
+
oracle: 'compile',
|
|
24
|
+
values: { columns: ['name', 'email'], source: ['users'], condition: ['age'] },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const results = runRoundTrip('sql', createSQLDSL(), [selectCommand], fixture);
|
|
28
|
+
|
|
29
|
+
function baselineOf(listing: ReturnType<typeof listingOf>): GateBaseline {
|
|
30
|
+
return {
|
|
31
|
+
upstream: {},
|
|
32
|
+
counts: { sql: countsOf(results) },
|
|
33
|
+
roundTrip: { sql: listing.roundTrip },
|
|
34
|
+
leftover: { sql: listing.leftover },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('runRoundTrip', () => {
|
|
39
|
+
it('writes each schema with its required roles, all its roles, and each further fixed word', () => {
|
|
40
|
+
expect(commandsFor([selectCommand], fixture)).toEqual([
|
|
41
|
+
'[select columns:name source:users]',
|
|
42
|
+
'[select columns:email source:users]',
|
|
43
|
+
'[select columns:name source:users condition:age]',
|
|
44
|
+
'[select columns:email source:users condition:age]',
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("writes a fixture's own commands after the generated ones, once each", () => {
|
|
49
|
+
const own = '[select columns:email source:users condition:age]';
|
|
50
|
+
expect(
|
|
51
|
+
commandsFor([selectCommand], { ...fixture, commands: [own, '[select columns:id source:t]'] })
|
|
52
|
+
).toEqual([...commandsFor([selectCommand], fixture), '[select columns:id source:t]']);
|
|
53
|
+
expect(commandsFor([selectCommand], fixture)).toContain(own);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('reads every render back, in every language, and tries each with an unknown word after it', () => {
|
|
57
|
+
expect(countsOf(results)).toEqual({
|
|
58
|
+
roundTrip: { cases: 8, pass: 8, refused: 0, silent: 0 },
|
|
59
|
+
// The `where` condition is greedy: it takes the unknown word as text, into the SQL (`pass`).
|
|
60
|
+
leftover: { cases: 8, pass: 4, refused: 4, silent: 0 },
|
|
61
|
+
});
|
|
62
|
+
expect(results.find(r => r.kind === 'roundTrip' && r.lang === 'es')).toMatchObject({
|
|
63
|
+
text: 'seleccionar name de users',
|
|
64
|
+
read: 'select columns:expression=name source:expression=users',
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('compare', () => {
|
|
70
|
+
it('finds no drift against the listing of the same results', () => {
|
|
71
|
+
expect(compare('sql', results, baselineOf(listingOf(results)))).toEqual({
|
|
72
|
+
worse: [],
|
|
73
|
+
better: [],
|
|
74
|
+
stale: [],
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('reports a case worse than listed, one better, and a listed case that is gone', () => {
|
|
79
|
+
// Two leftover cases accepted that the baseline lists as refused.
|
|
80
|
+
const accepted = '[select columns:name source:users]';
|
|
81
|
+
const now = results.map(r =>
|
|
82
|
+
r.kind === 'leftover' && r.source === accepted ? { ...r, outcome: 'silent' as const } : r
|
|
83
|
+
);
|
|
84
|
+
const listing = listingOf(results);
|
|
85
|
+
const drift = compare('sql', now, {
|
|
86
|
+
...baselineOf(listing),
|
|
87
|
+
roundTrip: {
|
|
88
|
+
sql: {
|
|
89
|
+
'[select columns:name source:users]': {
|
|
90
|
+
es: { outcome: 'refused' },
|
|
91
|
+
},
|
|
92
|
+
'[gone]': { en: { outcome: 'silent' } },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
expect(drift.worse.map(d => [d.kind, d.source, d.lang, d.listed, d.now])).toEqual([
|
|
97
|
+
['leftover', accepted, 'en', 'refused', 'silent'],
|
|
98
|
+
['leftover', accepted, 'es', 'refused', 'silent'],
|
|
99
|
+
]);
|
|
100
|
+
expect(drift.better).toEqual([
|
|
101
|
+
{
|
|
102
|
+
kind: 'roundTrip',
|
|
103
|
+
source: '[select columns:name source:users]',
|
|
104
|
+
lang: 'es',
|
|
105
|
+
listed: 'refused',
|
|
106
|
+
now: 'pass',
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
expect(drift.stale).toEqual([{ kind: 'roundTrip', source: '[gone]', lang: 'en' }]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('an unknown word read into a role', () => {
|
|
114
|
+
const leftover = (rs: typeof results) =>
|
|
115
|
+
rs
|
|
116
|
+
.filter(r => r.kind === 'leftover' && r.source.includes('condition'))
|
|
117
|
+
.map(r => [r.lang, r.outcome, r.why]);
|
|
118
|
+
|
|
119
|
+
it('is read (`pass`) where it extends an open-text role and reaches the code', () => {
|
|
120
|
+
expect(leftover(results)).toEqual([
|
|
121
|
+
['en', 'pass', undefined],
|
|
122
|
+
['es', 'pass', undefined],
|
|
123
|
+
['en', 'pass', undefined],
|
|
124
|
+
['es', 'pass', undefined],
|
|
125
|
+
]);
|
|
126
|
+
expect(results.find(r => r.kind === 'leftover' && r.source.includes('condition'))?.read).toBe(
|
|
127
|
+
'select columns:expression=name condition:expression="age zzqx" source:expression=users'
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('is accepted (`silent`) where it does not reach the code', () => {
|
|
132
|
+
const dropsCondition = createMultilingualDSL({
|
|
133
|
+
...sqlDSLConfig(),
|
|
134
|
+
codeGenerator: { generate: () => 'SELECT name FROM users' },
|
|
135
|
+
});
|
|
136
|
+
const rs = runRoundTrip('sql', dropsCondition, [selectCommand], fixture);
|
|
137
|
+
expect(leftover(rs)[0]).toEqual([
|
|
138
|
+
'en',
|
|
139
|
+
'silent',
|
|
140
|
+
'accepted as select; the word does not reach the code',
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('is refused where the input stops compiling, unless the command never compiles on its own', () => {
|
|
145
|
+
const refusesTheWord = createMultilingualDSL({
|
|
146
|
+
...sqlDSLConfig(),
|
|
147
|
+
codeGenerator: {
|
|
148
|
+
generate: n => {
|
|
149
|
+
const c = n.roles.get('condition') as { raw?: string } | undefined;
|
|
150
|
+
if (c?.raw?.includes('zzqx')) throw new Error('not a condition');
|
|
151
|
+
return 'SELECT';
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const rs = runRoundTrip('sql', refusesTheWord, [selectCommand], fixture);
|
|
156
|
+
expect(leftover(rs)[0]).toEqual(['en', 'refused', 'does not compile']);
|
|
157
|
+
const alone = runRoundTrip('sql', refusesTheWord, [selectCommand], {
|
|
158
|
+
...fixture,
|
|
159
|
+
englishDoesNotCompile: ['select'],
|
|
160
|
+
});
|
|
161
|
+
expect(leftover(alone)[0]).toEqual(['en', 'pass', undefined]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const node = (action: string, roles: Record<string, string>) => ({
|
|
165
|
+
action,
|
|
166
|
+
roles: new Map(Object.entries(roles).map(([k, v]) => [k, { type: 'expression', raw: v }])),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it.each([
|
|
170
|
+
['extends one role', { condition: 'age' }, { condition: 'age zzqx' }, true],
|
|
171
|
+
['fills a role the text left empty', {}, { list: 'zzqx' }, true],
|
|
172
|
+
[
|
|
173
|
+
'moves a value to make room for it (`when clicks zzqx`)',
|
|
174
|
+
{ action: 'clicks' },
|
|
175
|
+
{ actor: 'clicks', action: 'zzqx' },
|
|
176
|
+
false,
|
|
177
|
+
],
|
|
178
|
+
['replaces a value', { quantity: '2' }, { quantity: 'zzqx' }, false],
|
|
179
|
+
['is in two roles', {}, { a: 'zzqx', b: 'zzqx' }, false],
|
|
180
|
+
['is not read at all', { a: 'x' }, { a: 'x' }, false],
|
|
181
|
+
] as const)('a parse that %s is the text read with the word: %s', (_, was, now, want) => {
|
|
182
|
+
expect(readsWord(node('when', was), node('when', now), 'zzqx')).toBe(want);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('a parse as another command is not', () => {
|
|
186
|
+
expect(readsWord(node('when', {}), node('given', { a: 'zzqx' }), 'zzqx')).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('the parse snapshot', () => {
|
|
191
|
+
it('shows a parse that moved', () => {
|
|
192
|
+
const parses = snapshotOf(results);
|
|
193
|
+
const source = '[select columns:name source:users]';
|
|
194
|
+
const moved = {
|
|
195
|
+
upstream: {},
|
|
196
|
+
parses: { sql: { ...parses, [source]: { ...parses[source], es: 'select' } } },
|
|
197
|
+
};
|
|
198
|
+
expect(snapshotChanges('sql', results, moved)).toEqual([
|
|
199
|
+
{
|
|
200
|
+
source,
|
|
201
|
+
lang: 'es',
|
|
202
|
+
was: 'select',
|
|
203
|
+
now: 'select columns:expression=name source:expression=users',
|
|
204
|
+
},
|
|
205
|
+
]);
|
|
206
|
+
expect(snapshotChanges('sql', results, { upstream: {}, parses: { sql: parses } })).toEqual([]);
|
|
207
|
+
});
|
|
208
|
+
});
|