@lokascript/framework 4.5.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.
- package/CHANGELOG.md +220 -8
- package/dist/api/create-dsl.d.ts +59 -1
- package/dist/api/create-dsl.d.ts.map +1 -1
- package/dist/api/domain-registry.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 +1938 -724
- package/dist/api/index.js.map +1 -1
- package/dist/api/reading.d.ts +8 -0
- package/dist/api/reading.d.ts.map +1 -0
- 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 +1047 -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 +56 -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 +48 -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 +583 -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 +2132 -890
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2110 -887
- 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 +632 -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 +163 -0
- package/dist/testing/gate/round-trip.d.ts.map +1 -0
- package/package.json +7 -3
- package/src/__test__/fixtures/sql-dsl.ts +11 -2
- package/src/__test__/sql-integration.test.ts +2 -2
- package/src/api/create-dsl-engine.test.ts +86 -0
- package/src/api/create-dsl.test.ts +120 -0
- package/src/api/create-dsl.ts +314 -39
- package/src/api/domain-registry.test.ts +70 -5
- package/src/api/domain-registry.ts +82 -1
- package/src/api/index.ts +1 -0
- package/src/api/reading.ts +29 -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 +181 -0
- package/src/core/matching/stock-reader.ts +186 -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 +48 -0
- package/src/generation/index.ts +2 -0
- package/src/generation/pattern-render.ts +378 -0
- package/src/generation/schema-patterns.test.ts +99 -0
- package/src/generation/schema-patterns.ts +962 -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 +118 -0
- package/src/testing/gate/round-trip.ts +438 -0
package/src/core/types.ts
CHANGED
|
@@ -140,6 +140,12 @@ export interface LanguagePattern {
|
|
|
140
140
|
readonly template: PatternTemplate;
|
|
141
141
|
readonly extraction: ExtractionRules;
|
|
142
142
|
readonly constraints?: PatternConstraints;
|
|
143
|
+
/**
|
|
144
|
+
* The pattern reads a form the renderer does not write by choosing it
|
|
145
|
+
* (hyperscript's wait `esperar evento {event}`: the renderer writes the event
|
|
146
|
+
* word itself, before a name no table knows). The renderer skips it.
|
|
147
|
+
*/
|
|
148
|
+
readonly parseOnly?: boolean;
|
|
143
149
|
}
|
|
144
150
|
|
|
145
151
|
export interface PatternTemplate {
|
|
@@ -153,6 +159,13 @@ export interface LiteralPatternToken {
|
|
|
153
159
|
readonly type: 'literal';
|
|
154
160
|
readonly value: string;
|
|
155
161
|
readonly alternatives?: string[];
|
|
162
|
+
/**
|
|
163
|
+
* Parse-only marker: the matcher still consumes this literal, but the
|
|
164
|
+
* renderer skips it. Set when a role declares `renderOverride: { <lang>: '' }`
|
|
165
|
+
* (hyperscript's fetch `from`: required to PARSE `fetch from /api`, absent from
|
|
166
|
+
* the render, since `fetch from "/api"` is not canonical _hyperscript).
|
|
167
|
+
*/
|
|
168
|
+
readonly renderSuppress?: boolean;
|
|
156
169
|
}
|
|
157
170
|
|
|
158
171
|
export interface RolePatternToken {
|
|
@@ -161,12 +174,28 @@ export interface RolePatternToken {
|
|
|
161
174
|
readonly optional?: boolean;
|
|
162
175
|
readonly expectedTypes?: Array<ExpectedType>;
|
|
163
176
|
readonly greedy?: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Mirrors the schema's `RoleSpec.valueShape`: the slot's surface form is
|
|
179
|
+
* unambiguous on its own. Carried onto the pattern token because the value
|
|
180
|
+
* reader needs it too, not just the confidence model (hyperscript's
|
|
181
|
+
* `'keyword'` slots hold a fixed keyword phrase and are exempt from the
|
|
182
|
+
* trailing-slot verb guard).
|
|
183
|
+
*/
|
|
184
|
+
readonly valueShape?: 'time' | 'reference' | 'keyword' | 'object' | 'clause' | 'list';
|
|
164
185
|
}
|
|
165
186
|
|
|
166
187
|
export interface GroupPatternToken {
|
|
167
188
|
readonly type: 'group';
|
|
168
189
|
readonly tokens: PatternToken[];
|
|
169
190
|
readonly optional?: boolean;
|
|
191
|
+
/**
|
|
192
|
+
* The group is optional for PARSING but belongs in canonical output: the
|
|
193
|
+
* marker-only groups a profile's optional case markers produce (Turkish case
|
|
194
|
+
* suffixes may be dropped colloquially, so the parse accepts both forms).
|
|
195
|
+
* The matcher's skeleton also requires such a group's marker after a role it
|
|
196
|
+
* captures (`MatchSkeleton`).
|
|
197
|
+
*/
|
|
198
|
+
readonly renderRequired?: boolean;
|
|
170
199
|
}
|
|
171
200
|
|
|
172
201
|
export interface ExtractionRules {
|
|
@@ -180,6 +209,11 @@ export interface ExtractionRule {
|
|
|
180
209
|
readonly transform?: (raw: string) => SemanticValue;
|
|
181
210
|
readonly default?: SemanticValue;
|
|
182
211
|
readonly value?: string;
|
|
212
|
+
/**
|
|
213
|
+
* The static `value` is an expression, as the English parse types the word
|
|
214
|
+
* (hyperscript's `go back` destination), not a literal.
|
|
215
|
+
*/
|
|
216
|
+
readonly valueIsExpression?: true;
|
|
183
217
|
readonly fromRole?: string;
|
|
184
218
|
}
|
|
185
219
|
|
|
@@ -195,6 +229,20 @@ export interface PatternMatchResult {
|
|
|
195
229
|
readonly captured: ReadonlyMap<SemanticRole, SemanticValue>;
|
|
196
230
|
readonly consumedTokens: number;
|
|
197
231
|
readonly confidence: number;
|
|
232
|
+
/**
|
|
233
|
+
* Stream index at which each captured role's value STARTED, for callers that
|
|
234
|
+
* need to re-read a slot's own span rather than the tail after it. Populated
|
|
235
|
+
* on the same success path as `captured` (`MatchSkeleton`), so a role left
|
|
236
|
+
* behind by a rolled-back group survives in both maps or in neither.
|
|
237
|
+
*/
|
|
238
|
+
readonly roleStarts?: ReadonlyMap<SemanticRole, number>;
|
|
239
|
+
/**
|
|
240
|
+
* A source phrase hyperscript's reader found in an event HEAD (`from #btn`
|
|
241
|
+
* right after the event) rather than in a pattern slot: kept out of
|
|
242
|
+
* `captured`, so a fused pattern's own source group can neither overwrite it
|
|
243
|
+
* nor be overwritten (`ValueReader.resultFields`).
|
|
244
|
+
*/
|
|
245
|
+
readonly eventSource?: SemanticValue;
|
|
198
246
|
}
|
|
199
247
|
|
|
200
248
|
export interface PatternMatchError {
|
package/src/generation/index.ts
CHANGED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering a command through a language's own patterns: the renderer core
|
|
3
|
+
* semantic's hyperscript renderer runs (domains rethink Phase 4.5,
|
|
4
|
+
* `docs-internal/DOMAINS_RETHINK.md`), moved here from `@lokascript/semantic`.
|
|
5
|
+
*
|
|
6
|
+
* A node renders with the pattern its parser would read it back through:
|
|
7
|
+
* `selectRenderPattern` scores a language's patterns for the node (the roles it
|
|
8
|
+
* carries, the slots each pattern has, the values a pattern pins), and
|
|
9
|
+
* `renderPattern` walks the chosen pattern's tokens. What a value looks like in
|
|
10
|
+
* the language, and the grammar's preferences, are `PatternRenderHooks`.
|
|
11
|
+
* (The domains' `renderer.ts` renders from the schema instead, for
|
|
12
|
+
* `createMultilingualDSL`.)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
ExtractionRule,
|
|
17
|
+
GroupPatternToken,
|
|
18
|
+
LanguagePattern,
|
|
19
|
+
PatternToken,
|
|
20
|
+
RolePatternToken,
|
|
21
|
+
SemanticNode,
|
|
22
|
+
} from '../core/types';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* What the core reads of a node: its roles. A grammar's own node type (semantic's
|
|
26
|
+
* `SemanticNode`) qualifies as it is; the hooks receive it unchanged.
|
|
27
|
+
*/
|
|
28
|
+
export interface RenderableNode {
|
|
29
|
+
readonly roles: ReadonlyMap<string, { readonly type: string }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A grammar's part of rendering through patterns, for its node type `N`. */
|
|
33
|
+
export interface PatternRenderHooks<N extends RenderableNode = SemanticNode> {
|
|
34
|
+
/**
|
|
35
|
+
* The patterns a node may render with, from those that are not parse-only
|
|
36
|
+
* (hyperscript: an event handler renders only with a trigger pattern, never a
|
|
37
|
+
* fused `<command>-event-*` one).
|
|
38
|
+
*/
|
|
39
|
+
candidates?(node: N, patterns: readonly LanguagePattern[]): readonly LanguagePattern[];
|
|
40
|
+
/** Whether the node carries `role` as written, not as a default its parser filled in. */
|
|
41
|
+
hasRealRole(node: N, role: string): boolean;
|
|
42
|
+
/** The grammar's own preference between patterns, added to the score. */
|
|
43
|
+
adjustScore?(pattern: LanguagePattern, node: N): number;
|
|
44
|
+
/**
|
|
45
|
+
* Text for token `i` and the tokens after it it covers, when the grammar
|
|
46
|
+
* writes them as one (hyperscript: a reference in its case, es `a mí`);
|
|
47
|
+
* undefined renders the token as usual.
|
|
48
|
+
*/
|
|
49
|
+
span?(
|
|
50
|
+
tokens: readonly PatternToken[],
|
|
51
|
+
i: number,
|
|
52
|
+
node: N,
|
|
53
|
+
language: string,
|
|
54
|
+
topLevel: boolean
|
|
55
|
+
): { text: string; tokens: number } | undefined;
|
|
56
|
+
/** A role slot's text; null writes nothing. */
|
|
57
|
+
writeRole(token: RolePatternToken, node: N, language: string): string | null;
|
|
58
|
+
/**
|
|
59
|
+
* Whether an optional group whose required roles the node carries is written
|
|
60
|
+
* (hyperscript: not for the implicit `me` its parser filled in). Default yes.
|
|
61
|
+
*/
|
|
62
|
+
writeGroup?(token: GroupPatternToken, node: N, language: string): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Add a token's text to `parts` (hyperscript: a marker written onto its
|
|
65
|
+
* word, ko `#out에`). Default: a part of its own.
|
|
66
|
+
*/
|
|
67
|
+
pushPart?(
|
|
68
|
+
parts: string[],
|
|
69
|
+
text: string,
|
|
70
|
+
token: PatternToken,
|
|
71
|
+
previous: PatternToken | undefined,
|
|
72
|
+
node: N,
|
|
73
|
+
language: string
|
|
74
|
+
): void;
|
|
75
|
+
/** Before top-level token `i` (hyperscript: a scroll's direction word before its amount). */
|
|
76
|
+
beforeToken?(
|
|
77
|
+
tokens: readonly PatternToken[],
|
|
78
|
+
i: number,
|
|
79
|
+
parts: string[],
|
|
80
|
+
node: N,
|
|
81
|
+
language: string
|
|
82
|
+
): void;
|
|
83
|
+
/** After a top-level token wrote `parts[part]` (hyperscript: where the event went). */
|
|
84
|
+
afterToken?(token: PatternToken, part: number, node: N): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Score a role slot nested inside an OPTIONAL group. Lower than the top-level
|
|
89
|
+
* bonus on purpose: it must break a tie between two patterns that are otherwise
|
|
90
|
+
* equal (the handcrafted `toggle-{L}-full` versus the generated pattern that
|
|
91
|
+
* also carries `[{duration}]`) without ever outweighing a top-level difference.
|
|
92
|
+
*/
|
|
93
|
+
const NESTED_ROLE_BONUS = 5;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Score against a pattern that has no slot for a role the node carries. Larger
|
|
97
|
+
* than any priority gap between a command's patterns (80–100), so rendering
|
|
98
|
+
* the role always beats a more idiomatic form that drops it.
|
|
99
|
+
*/
|
|
100
|
+
const UNCOVERED_ROLE_PENALTY = 30;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The literal a pattern pins a role to, normalized for comparison: an
|
|
104
|
+
* extraction default, or a fixed value the pattern records when it matches.
|
|
105
|
+
* The second is how `go`'s url variant says what it means (`go url /page`
|
|
106
|
+
* records `method: 'url'`); reading only defaults let that variant render every
|
|
107
|
+
* `go`, so `go back` came out as `go url back`: a navigation to a page named
|
|
108
|
+
* "back" in every language.
|
|
109
|
+
*/
|
|
110
|
+
function pinnedLiteral(rule: ExtractionRule | undefined): string | undefined {
|
|
111
|
+
if (rule?.default?.type === 'literal') return String(rule.default.value).trim().toLowerCase();
|
|
112
|
+
if (rule?.value !== undefined) return String(rule.value).trim().toLowerCase();
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The pattern to render `node` with, or null when there is none.
|
|
118
|
+
*/
|
|
119
|
+
export function selectRenderPattern<N extends RenderableNode>(
|
|
120
|
+
node: N,
|
|
121
|
+
patterns: readonly LanguagePattern[],
|
|
122
|
+
hooks: PatternRenderHooks<N>
|
|
123
|
+
): LanguagePattern | null {
|
|
124
|
+
let candidates: readonly LanguagePattern[] = patterns.filter(pattern => !pattern.parseOnly);
|
|
125
|
+
if (candidates.length === 0) candidates = patterns;
|
|
126
|
+
if (hooks.candidates) candidates = hooks.candidates(node, candidates);
|
|
127
|
+
|
|
128
|
+
// Which literal values does the candidate set pin, per role? A value that is
|
|
129
|
+
// pinned by SOME candidate is a real alternative the pattern set distinguishes
|
|
130
|
+
// (put's manner: before / after / at end of), so choosing a pattern pinned to a
|
|
131
|
+
// different one would silently change meaning. A value nothing pins is not an
|
|
132
|
+
// alternative — it is just the value that came out of the parse — and must not
|
|
133
|
+
// disqualify anything. That distinction is what keeps `repeat until event X`
|
|
134
|
+
// renderable: a zh parse yields loopType `until` while the English pattern that
|
|
135
|
+
// carries the event pins `until-event`, and nothing pins bare `until`.
|
|
136
|
+
const pinnedValues = new Map<string, Set<string>>();
|
|
137
|
+
for (const pattern of candidates) {
|
|
138
|
+
for (const [role, rule] of Object.entries(pattern.extraction ?? {})) {
|
|
139
|
+
const key = pinnedLiteral(rule);
|
|
140
|
+
if (key === undefined) continue;
|
|
141
|
+
const set = pinnedValues.get(role) ?? new Set<string>();
|
|
142
|
+
set.add(key);
|
|
143
|
+
pinnedValues.set(role, set);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Score patterns by how well they match our roles
|
|
148
|
+
const scored = candidates.map(pattern => {
|
|
149
|
+
let score = pattern.priority;
|
|
150
|
+
|
|
151
|
+
// Check each role token in the pattern, INCLUDING the ones nested inside
|
|
152
|
+
// optional groups. Scoring only the top level made a slot the pattern
|
|
153
|
+
// really has invisible: every `[for {duration}]` / `[with {style}]` group
|
|
154
|
+
// lives one level down, so a handcrafted pattern without the slot tied
|
|
155
|
+
// with the generated one that has it and won on order — `toggle .loading
|
|
156
|
+
// for 2s` rendered as `alternar .loading` in es/it/pl/ru/uk/vi/zh, the
|
|
157
|
+
// duration dropped in silence.
|
|
158
|
+
//
|
|
159
|
+
// A role inside a group is scored as PRESENT-only: its absence is what
|
|
160
|
+
// "optional" means, and the group's own role tokens are not consistently
|
|
161
|
+
// flagged `optional` (a handcrafted `[en {destination}]` writes the role
|
|
162
|
+
// bare), so reading the flag there would apply the missing-role penalty to
|
|
163
|
+
// a slot that is optional by construction.
|
|
164
|
+
const hasRealRole = (role: string): boolean => hooks.hasRealRole(node, role);
|
|
165
|
+
const scoreTokens = (tokens: readonly PatternToken[], inGroup: boolean): void => {
|
|
166
|
+
for (const token of tokens) {
|
|
167
|
+
if (token.type === 'group') {
|
|
168
|
+
scoreTokens(token.tokens, true);
|
|
169
|
+
} else if (token.type === 'role') {
|
|
170
|
+
if (hasRealRole(token.role)) {
|
|
171
|
+
// Bonus for patterns that use roles we have. A nested slot scores
|
|
172
|
+
// LESS than a top-level one, so carrying an extra optional slot
|
|
173
|
+
// breaks a tie without ever outweighing a top-level difference —
|
|
174
|
+
// measured: at equal weight it also re-ordered the pl behavior
|
|
175
|
+
// handler and three qu positional rows.
|
|
176
|
+
score += inGroup ? NESTED_ROLE_BONUS : 10;
|
|
177
|
+
} else if (!inGroup && !token.optional) {
|
|
178
|
+
// Heavy penalty for patterns that require roles we DON'T have
|
|
179
|
+
// This prevents selecting "source" patterns when there's no source
|
|
180
|
+
score -= 50;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
scoreTokens(pattern.template.tokens, false);
|
|
186
|
+
|
|
187
|
+
// A pattern with no slot for a role the node really carries drops it in
|
|
188
|
+
// silence, so it loses to one that renders it. The bonuses above only
|
|
189
|
+
// reward coverage, which let a slot-less pattern win on a top-level-vs-
|
|
190
|
+
// nested difference: once show's patient became optional, zh `显示
|
|
191
|
+
// {patient}` beat the generated `显示 [把 {patient}] [用 {style}]` and
|
|
192
|
+
// `show #modal with *opacity` rendered without its strategy (bn/hi/zh).
|
|
193
|
+
const slots = new Set<string>();
|
|
194
|
+
const collectSlots = (tokens: readonly PatternToken[]): void => {
|
|
195
|
+
for (const token of tokens) {
|
|
196
|
+
if (token.type === 'group') collectSlots(token.tokens);
|
|
197
|
+
else if (token.type === 'role') slots.add(token.role);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
collectSlots(pattern.template.tokens);
|
|
201
|
+
for (const role of node.roles.keys()) {
|
|
202
|
+
if (hasRealRole(role) && !slots.has(role)) score -= UNCOVERED_ROLE_PENALTY;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Value-pinned variants. A positional pattern (`put X before Y`, `at end
|
|
206
|
+
// of`, `after`) carries its position as a baked-in literal plus an
|
|
207
|
+
// extraction default that records which value it means:
|
|
208
|
+
// put-es-before -> extraction.manner.default = 'before'
|
|
209
|
+
// put-es-at-end -> extraction.manner.default = 'at end of'
|
|
210
|
+
// while the neutral `put-es-full` pins nothing. So the pattern set already
|
|
211
|
+
// says which surface each value selects; scoring just has to read it.
|
|
212
|
+
//
|
|
213
|
+
// The rule: a pattern that pins a role to a literal may be chosen ONLY when
|
|
214
|
+
// the node carries that exact value. Matching is a strong preference (the
|
|
215
|
+
// pinned form is the whole reason the value exists); mismatching — or the
|
|
216
|
+
// node not carrying the role at all — disqualifies it, so a plain
|
|
217
|
+
// `put X into Y` can never render as `poner X en fin de Y`.
|
|
218
|
+
//
|
|
219
|
+
// This replaces an id-regex that penalized every `-at-end|-before|-after`
|
|
220
|
+
// pattern unconditionally. That kept plain puts safe but made the positional
|
|
221
|
+
// forms unreachable: `put "<p>" before me` rendered with the into-pattern in
|
|
222
|
+
// all 23 languages, losing the distinction the source drew.
|
|
223
|
+
for (const [role, rule] of Object.entries(pattern.extraction ?? {})) {
|
|
224
|
+
const pinnedKey = pinnedLiteral(rule);
|
|
225
|
+
if (pinnedKey === undefined) continue;
|
|
226
|
+
const actual = node.roles.get(role);
|
|
227
|
+
// The value as the pattern would produce it: a literal, or an
|
|
228
|
+
// expression where the rule says so (`go back`).
|
|
229
|
+
const actualKey =
|
|
230
|
+
actual?.type === (rule.valueIsExpression ? 'expression' : 'literal')
|
|
231
|
+
? String(
|
|
232
|
+
actual.type === 'expression'
|
|
233
|
+
? (actual as { raw?: unknown }).raw
|
|
234
|
+
: (actual as { value?: unknown }).value
|
|
235
|
+
)
|
|
236
|
+
.trim()
|
|
237
|
+
.toLowerCase()
|
|
238
|
+
: undefined;
|
|
239
|
+
|
|
240
|
+
if (actualKey === pinnedKey) {
|
|
241
|
+
// The pinned form is the whole reason this value exists — prefer it
|
|
242
|
+
// decisively over the neutral pattern, whatever the parse priorities say.
|
|
243
|
+
score += 60;
|
|
244
|
+
} else if (actualKey === undefined || pinnedValues.get(role)?.has(actualKey)) {
|
|
245
|
+
// Either the node has no such value (so this pattern would invent one:
|
|
246
|
+
// a plain `put X into Y` must never render as `put X at end of Y`), or it
|
|
247
|
+
// has a DIFFERENT value that the pattern set treats as an alternative.
|
|
248
|
+
// Both are wrong surfaces; disqualify.
|
|
249
|
+
score -= 200;
|
|
250
|
+
}
|
|
251
|
+
// Otherwise the node's value is not one of the pinned alternatives, so this
|
|
252
|
+
// pattern is still the best available carrier for it — leave the score alone
|
|
253
|
+
// and let priority and role coverage decide.
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
score += hooks.adjustScore?.(pattern, node) ?? 0;
|
|
257
|
+
|
|
258
|
+
return { pattern, score };
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
scored.sort((a, b) => b.score - a.score);
|
|
262
|
+
|
|
263
|
+
return scored.length > 0 ? scored[0]!.pattern : null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The parts `node` renders to through `pattern`'s tokens, in order: each
|
|
268
|
+
* literal (unless parse-only), each role's text, each group that is written.
|
|
269
|
+
* The grammar joins them (and adds what the pattern does not model).
|
|
270
|
+
*/
|
|
271
|
+
export function renderPattern<N extends RenderableNode>(
|
|
272
|
+
node: N,
|
|
273
|
+
pattern: LanguagePattern,
|
|
274
|
+
hooks: PatternRenderHooks<N>
|
|
275
|
+
): string[] {
|
|
276
|
+
const parts: string[] = [];
|
|
277
|
+
const language = pattern.language;
|
|
278
|
+
const tokens = pattern.template.tokens;
|
|
279
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
280
|
+
hooks.beforeToken?.(tokens, i, parts, node, language);
|
|
281
|
+
const span = hooks.span?.(tokens, i, node, language, true);
|
|
282
|
+
if (span !== undefined) {
|
|
283
|
+
parts.push(span.text);
|
|
284
|
+
i += span.tokens - 1;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const token = tokens[i]!;
|
|
288
|
+
const rendered = renderPatternToken(token, node, language, hooks);
|
|
289
|
+
if (rendered !== null) {
|
|
290
|
+
pushPart(hooks, parts, rendered, token, tokens[i - 1], node, language);
|
|
291
|
+
hooks.afterToken?.(token, parts.length - 1, node);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return parts;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function pushPart<N extends RenderableNode>(
|
|
298
|
+
hooks: PatternRenderHooks<N>,
|
|
299
|
+
parts: string[],
|
|
300
|
+
text: string,
|
|
301
|
+
token: PatternToken,
|
|
302
|
+
previous: PatternToken | undefined,
|
|
303
|
+
node: N,
|
|
304
|
+
language: string
|
|
305
|
+
): void {
|
|
306
|
+
if (hooks.pushPart) hooks.pushPart(parts, text, token, previous, node, language);
|
|
307
|
+
else parts.push(text);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Render a single pattern token.
|
|
312
|
+
*/
|
|
313
|
+
function renderPatternToken<N extends RenderableNode>(
|
|
314
|
+
token: PatternToken,
|
|
315
|
+
node: N,
|
|
316
|
+
language: string,
|
|
317
|
+
hooks: PatternRenderHooks<N>
|
|
318
|
+
): string | null {
|
|
319
|
+
switch (token.type) {
|
|
320
|
+
case 'literal':
|
|
321
|
+
// Parse-only markers (renderSuppress) consume input but never render —
|
|
322
|
+
// e.g. fetch's `from`, which parses `fetch from /api` yet must be absent
|
|
323
|
+
// from output (`fetch from "/api"` is invalid canonical _hyperscript).
|
|
324
|
+
return token.renderSuppress ? null : token.value;
|
|
325
|
+
|
|
326
|
+
case 'role':
|
|
327
|
+
return hooks.writeRole(token, node, language);
|
|
328
|
+
|
|
329
|
+
case 'group': {
|
|
330
|
+
// Check if we have all required roles in the group
|
|
331
|
+
const hasRequired = token.tokens
|
|
332
|
+
.filter(t => t.type === 'role' && !t.optional)
|
|
333
|
+
.every(t => node.roles.has((t as RolePatternToken).role));
|
|
334
|
+
|
|
335
|
+
if (!hasRequired && token.optional) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (hooks.writeGroup && !hooks.writeGroup(token, node, language)) return null;
|
|
340
|
+
|
|
341
|
+
const groupParts: string[] = [];
|
|
342
|
+
let hasRoleValue = false;
|
|
343
|
+
for (let i = 0; i < token.tokens.length; i++) {
|
|
344
|
+
const span = hooks.span?.(token.tokens, i, node, language, false);
|
|
345
|
+
if (span !== undefined) {
|
|
346
|
+
groupParts.push(span.text);
|
|
347
|
+
hasRoleValue = true;
|
|
348
|
+
i += span.tokens - 1;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const subToken = token.tokens[i]!;
|
|
352
|
+
const rendered = renderPatternToken(subToken, node, language, hooks);
|
|
353
|
+
if (rendered !== null) {
|
|
354
|
+
pushPart(hooks, groupParts, rendered, subToken, token.tokens[i - 1], node, language);
|
|
355
|
+
if (subToken.type === 'role') hasRoleValue = true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Don't emit an optional group that DECLARES a role slot and did not
|
|
360
|
+
// fill it — e.g. don't emit a dangling "with" when the style role is
|
|
361
|
+
// absent from "hide #output".
|
|
362
|
+
//
|
|
363
|
+
// …unless the group is `renderRequired`: a MARKER wrapped on its own by
|
|
364
|
+
// `profile.markersOptional` (tr, where the case suffix may be dropped
|
|
365
|
+
// colloquially, so the parse side has to accept both forms). Dropping
|
|
366
|
+
// those cost tr every role marker in every rendered command —
|
|
367
|
+
// `add .selected to #item` came out `#item .selected ekle` where the
|
|
368
|
+
// corpus has `#item e .selected i ekle`, and the marker-less surface
|
|
369
|
+
// re-parses as a one-role `add` with the destination defaulted to `me`.
|
|
370
|
+
if (token.optional && !hasRoleValue && !token.renderRequired) return null;
|
|
371
|
+
|
|
372
|
+
return groupParts.length > 0 ? groupParts.join(' ') : null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
default:
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schema-pattern generator semantic's hyperscript parser runs (domains
|
|
3
|
+
* rethink Phase 4.4), on a schema of its own: the grammar's position order,
|
|
4
|
+
* the variants, and a profile's optional markers.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { defineCommand, defineRole } from '../schema';
|
|
8
|
+
import type { PatternToken } from '../core/types';
|
|
9
|
+
import {
|
|
10
|
+
createSchemaPatternGenerator,
|
|
11
|
+
withoutVariantOnlyRoles,
|
|
12
|
+
type SchemaPatternProfile,
|
|
13
|
+
} from './schema-patterns';
|
|
14
|
+
|
|
15
|
+
const send = defineCommand({
|
|
16
|
+
action: 'send',
|
|
17
|
+
primaryRole: 'patient',
|
|
18
|
+
roles: [
|
|
19
|
+
defineRole({ role: 'patient', required: true, expectedTypes: ['expression'], svoPosition: 1 }),
|
|
20
|
+
defineRole({
|
|
21
|
+
role: 'destination',
|
|
22
|
+
required: false,
|
|
23
|
+
expectedTypes: ['selector'],
|
|
24
|
+
svoPosition: 2,
|
|
25
|
+
sovPosition: 1,
|
|
26
|
+
}),
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const en: SchemaPatternProfile = {
|
|
31
|
+
code: 'en',
|
|
32
|
+
wordOrder: 'SVO',
|
|
33
|
+
keywords: { send: { primary: 'send' } },
|
|
34
|
+
roleMarkers: { destination: { primary: 'to', position: 'before' } },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const tr: SchemaPatternProfile = {
|
|
38
|
+
code: 'tr',
|
|
39
|
+
wordOrder: 'SOV',
|
|
40
|
+
keywords: { send: { primary: 'gönder' } },
|
|
41
|
+
roleMarkers: { destination: { primary: 'e', position: 'after' } },
|
|
42
|
+
markersOptional: true,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** A pattern's tokens as text: literals as themselves, roles as {role}, groups in brackets. */
|
|
46
|
+
function shape(tokens: readonly PatternToken[]): string {
|
|
47
|
+
return tokens
|
|
48
|
+
.map(t =>
|
|
49
|
+
t.type === 'literal'
|
|
50
|
+
? t.value
|
|
51
|
+
: t.type === 'role'
|
|
52
|
+
? `{${t.role}}`
|
|
53
|
+
: `[${shape(t.tokens)}${'renderRequired' in t && t.renderRequired ? '!' : ''}]`
|
|
54
|
+
)
|
|
55
|
+
.join(' ');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('createSchemaPatternGenerator', () => {
|
|
59
|
+
it("sorts roles in the grammar's position order", () => {
|
|
60
|
+
const ascending = createSchemaPatternGenerator({ positionOrder: 'ascending' });
|
|
61
|
+
const descending = createSchemaPatternGenerator({ positionOrder: 'descending' });
|
|
62
|
+
expect(shape(ascending.generatePattern(send, en).template.tokens)).toBe(
|
|
63
|
+
'send {patient} [to {destination}]'
|
|
64
|
+
);
|
|
65
|
+
expect(shape(descending.generatePattern(send, en).template.tokens)).toBe(
|
|
66
|
+
'send [to {destination}] {patient}'
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('builds the simple and SOV verb-first variants', () => {
|
|
71
|
+
const generator = createSchemaPatternGenerator({ positionOrder: 'ascending' });
|
|
72
|
+
const variants = generator.generatePatternVariants(send, tr, {
|
|
73
|
+
basePriority: 100,
|
|
74
|
+
generateSimpleVariants: true,
|
|
75
|
+
});
|
|
76
|
+
expect(variants.map(p => [p.id, p.priority, shape(p.template.tokens)])).toEqual([
|
|
77
|
+
['send-tr-generated', 100, '[{destination} [e!]] {patient} gönder'],
|
|
78
|
+
['send-tr-generated-simple', 95, '{patient} gönder'],
|
|
79
|
+
['send-tr-generated-verb-first', 80, 'gönder {patient}'],
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('drops variant-only roles', () => {
|
|
84
|
+
const scroll = defineCommand({
|
|
85
|
+
action: 'scroll',
|
|
86
|
+
roles: [
|
|
87
|
+
defineRole({ role: 'patient', required: true, expectedTypes: ['selector'] }),
|
|
88
|
+
defineRole({
|
|
89
|
+
role: 'quantity',
|
|
90
|
+
required: false,
|
|
91
|
+
expectedTypes: ['literal'],
|
|
92
|
+
variantOnly: true,
|
|
93
|
+
}),
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
expect(withoutVariantOnlyRoles(scroll).roles.map(r => r.role)).toEqual(['patient']);
|
|
97
|
+
expect(withoutVariantOnlyRoles(send)).toBe(send);
|
|
98
|
+
});
|
|
99
|
+
});
|