@lokascript/framework 2.8.0 → 2.9.1
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 +393 -0
- package/dist/api/create-dsl.d.ts +93 -1
- package/dist/api/create-dsl.d.ts.map +1 -1
- package/dist/api/domain-registry.d.ts +5 -3
- package/dist/api/domain-registry.d.ts.map +1 -1
- package/dist/api/index.js +232 -18
- package/dist/api/index.js.map +1 -1
- package/dist/core/index.js +26 -6
- package/dist/core/index.js.map +1 -1
- package/dist/core/tokenization/base-tokenizer.d.ts +15 -3
- package/dist/core/tokenization/base-tokenizer.d.ts.map +1 -1
- package/dist/core/tokenization/char-classifiers.d.ts +2 -2
- package/dist/core/tokenization/index.js +26 -6
- package/dist/core/tokenization/index.js.map +1 -1
- package/dist/core/tokenization/token-utils.d.ts +15 -0
- package/dist/core/tokenization/token-utils.d.ts.map +1 -1
- package/dist/generation/index.js +73 -47
- package/dist/generation/index.js.map +1 -1
- package/dist/generation/pattern-generator.d.ts +8 -1
- package/dist/generation/pattern-generator.d.ts.map +1 -1
- package/dist/generation/renderer.d.ts +53 -1
- package/dist/generation/renderer.d.ts.map +1 -1
- package/dist/index.cjs +302 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +298 -115
- package/dist/index.js.map +1 -1
- package/dist/interfaces/value-extractor.d.ts +5 -0
- package/dist/interfaces/value-extractor.d.ts.map +1 -1
- package/dist/multilingual/index.js +25 -6
- package/dist/multilingual/index.js.map +1 -1
- package/package.json +4 -3
- package/src/api/create-dsl.test.ts +11 -0
- package/src/api/create-dsl.ts +278 -9
- package/src/api/domain-registry.ts +15 -10
- package/src/api/extensions.test.ts +322 -0
- package/src/core/tokenization/base-tokenizer.ts +23 -8
- package/src/core/tokenization/char-classifiers.ts +2 -2
- package/src/core/tokenization/css-selector-extractor.test.ts +67 -0
- package/src/core/tokenization/token-utils.ts +18 -0
- package/src/generation/domain-renderer.test.ts +172 -0
- package/src/generation/pattern-generator.test.ts +102 -0
- package/src/generation/pattern-generator.ts +27 -19
- package/src/generation/renderer.test.ts +243 -4
- package/src/generation/renderer.ts +188 -45
- package/src/index.ts +9 -1
- package/src/interfaces/value-extractor.ts +50 -0
|
@@ -71,6 +71,108 @@ describe('PatternGenerator', () => {
|
|
|
71
71
|
expect(pattern.template.tokens[1]).toEqual({ type: 'literal', value: 'トグル' });
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
it('should place a sovSlot:postVerb role after the verb in SOV', () => {
|
|
75
|
+
// The renderer buckets by this field (`renderer.ts`), writing voice's
|
|
76
|
+
// page count after the verb: `戻る 2`. Generation has to agree, or the
|
|
77
|
+
// rendered surface re-parses as a bare verb with its argument dropped.
|
|
78
|
+
const schema = defineCommand({
|
|
79
|
+
action: 'back',
|
|
80
|
+
roles: [
|
|
81
|
+
defineRole({
|
|
82
|
+
role: 'quantity',
|
|
83
|
+
required: true,
|
|
84
|
+
expectedTypes: ['expression'],
|
|
85
|
+
sovPosition: 1,
|
|
86
|
+
sovSlot: 'postVerb',
|
|
87
|
+
}),
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const profile: PatternGenLanguageProfile = {
|
|
92
|
+
code: 'ja',
|
|
93
|
+
wordOrder: 'SOV',
|
|
94
|
+
keywords: { back: { primary: '戻る' } },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const pattern = generatePattern(schema, profile);
|
|
98
|
+
|
|
99
|
+
expect(pattern.template.tokens).toHaveLength(2);
|
|
100
|
+
expect(pattern.template.tokens[0]).toEqual({ type: 'literal', value: '戻る' });
|
|
101
|
+
expect(pattern.template.tokens[1]).toMatchObject({ type: 'role', role: 'quantity' });
|
|
102
|
+
expect(pattern.template.format).toBe('戻る {quantity}');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should split pre-verb and post-verb roles around the verb in SOV', () => {
|
|
106
|
+
const schema = defineCommand({
|
|
107
|
+
action: 'fetch',
|
|
108
|
+
roles: [
|
|
109
|
+
defineRole({
|
|
110
|
+
role: 'source',
|
|
111
|
+
required: true,
|
|
112
|
+
expectedTypes: ['expression'],
|
|
113
|
+
sovPosition: 2,
|
|
114
|
+
}),
|
|
115
|
+
defineRole({
|
|
116
|
+
role: 'destination',
|
|
117
|
+
required: true,
|
|
118
|
+
expectedTypes: ['selector'],
|
|
119
|
+
sovPosition: 1,
|
|
120
|
+
sovSlot: 'postVerb',
|
|
121
|
+
markerOverride: { ja: 'に' },
|
|
122
|
+
}),
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const profile: PatternGenLanguageProfile = {
|
|
127
|
+
code: 'ja',
|
|
128
|
+
wordOrder: 'SOV',
|
|
129
|
+
keywords: { fetch: { primary: '取得' } },
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const pattern = generatePattern(schema, profile);
|
|
133
|
+
|
|
134
|
+
expect(pattern.template.tokens).toEqual([
|
|
135
|
+
expect.objectContaining({ type: 'role', role: 'source' }),
|
|
136
|
+
{ type: 'literal', value: '取得' },
|
|
137
|
+
expect.objectContaining({ type: 'role', role: 'destination' }),
|
|
138
|
+
// SOV markers default to following their value
|
|
139
|
+
{ type: 'literal', value: 'に' },
|
|
140
|
+
]);
|
|
141
|
+
// The role PHRASE moves as a unit. Note the marker sits before its role
|
|
142
|
+
// here while the tokens put it after: `format` resolves marker position
|
|
143
|
+
// from the profile alone, missing `markerPosition` and the SOV default
|
|
144
|
+
// that `addRoleWithMarker` applies. Pre-existing and documentation-only
|
|
145
|
+
// (nothing reads `format`), left alone deliberately — fixing it would
|
|
146
|
+
// move every SOV golden entry with a marker and bury this change's diff.
|
|
147
|
+
expect(pattern.template.format).toBe('{source} 取得 に {destination}');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should ignore sovSlot outside SOV languages', () => {
|
|
151
|
+
const schema = defineCommand({
|
|
152
|
+
action: 'back',
|
|
153
|
+
roles: [
|
|
154
|
+
defineRole({
|
|
155
|
+
role: 'quantity',
|
|
156
|
+
required: true,
|
|
157
|
+
expectedTypes: ['expression'],
|
|
158
|
+
svoPosition: 1,
|
|
159
|
+
sovSlot: 'postVerb',
|
|
160
|
+
}),
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
for (const wordOrder of ['SVO', 'VSO'] as const) {
|
|
165
|
+
const pattern = generatePattern(schema, {
|
|
166
|
+
code: 'en',
|
|
167
|
+
wordOrder,
|
|
168
|
+
keywords: { back: { primary: 'back' } },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(pattern.template.tokens[0]).toEqual({ type: 'literal', value: 'back' });
|
|
172
|
+
expect(pattern.template.tokens[1]).toMatchObject({ type: 'role', role: 'quantity' });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
74
176
|
it('should generate a VSO pattern with verb first', () => {
|
|
75
177
|
const schema = defineCommand({
|
|
76
178
|
action: 'toggle',
|
|
@@ -110,15 +110,21 @@ function buildTokens(
|
|
|
110
110
|
addRoleWithMarker(tokens, role, profile);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
-
// For SOV: [patient, destination, source, ..., action]
|
|
113
|
+
// For SOV: [patient, destination, source, ..., action, postVerb roles...]
|
|
114
114
|
else if (profile.wordOrder === 'SOV') {
|
|
115
|
-
//
|
|
115
|
+
// `sovSlot: 'postVerb'` moves a role AFTER the verb, which is what the
|
|
116
|
+
// renderer does (`renderer.ts` buckets by the same field). Generation has
|
|
117
|
+
// to mirror it: a role the renderer writes post-verb but the pattern
|
|
118
|
+
// expects pre-verb produces a faithful surface its own parser drops the
|
|
119
|
+
// argument from — voice's `戻る 2` re-parsing as a bare `back`.
|
|
120
|
+
const preVerb: PatternToken[] = [];
|
|
121
|
+
const postVerb: PatternToken[] = [];
|
|
122
|
+
|
|
116
123
|
for (const role of sortedRoles) {
|
|
117
|
-
addRoleWithMarker(
|
|
124
|
+
addRoleWithMarker(role.sovSlot === 'postVerb' ? postVerb : preVerb, role, profile);
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
|
|
121
|
-
tokens.push(keywordToken);
|
|
127
|
+
tokens.push(...preVerb, keywordToken, ...postVerb);
|
|
122
128
|
}
|
|
123
129
|
// For VSO: [action, destination, patient, source, ...]
|
|
124
130
|
else if (profile.wordOrder === 'VSO') {
|
|
@@ -218,8 +224,11 @@ function getMarkerForRole(
|
|
|
218
224
|
|
|
219
225
|
/**
|
|
220
226
|
* Sort roles by word order for pattern building.
|
|
227
|
+
*
|
|
228
|
+
* Exported so the renderer sorts with exactly this comparator — a rendered
|
|
229
|
+
* surface whose role order differs from the generated pattern would not re-parse.
|
|
221
230
|
*/
|
|
222
|
-
function sortRolesByWordOrder(roles: RoleSpec[], wordOrder: string): RoleSpec[] {
|
|
231
|
+
export function sortRolesByWordOrder(roles: RoleSpec[], wordOrder: string): RoleSpec[] {
|
|
223
232
|
const sorted = [...roles];
|
|
224
233
|
|
|
225
234
|
if (wordOrder === 'SVO') {
|
|
@@ -266,11 +275,9 @@ function buildFormatString(
|
|
|
266
275
|
profile: PatternGenLanguageProfile,
|
|
267
276
|
keyword: string
|
|
268
277
|
): string {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
parts.push(keyword);
|
|
273
|
-
}
|
|
278
|
+
const isSOV = profile.wordOrder === 'SOV';
|
|
279
|
+
const preVerb: string[] = [];
|
|
280
|
+
const postVerb: string[] = [];
|
|
274
281
|
|
|
275
282
|
// Order roles the same way buildTokens does (descending position). Iterating
|
|
276
283
|
// declaration order here would disagree with the token order whenever a
|
|
@@ -279,24 +286,25 @@ function buildFormatString(
|
|
|
279
286
|
for (const role of sortedRoles) {
|
|
280
287
|
const marker = getMarkerForRole(role, profile);
|
|
281
288
|
const roleName = `{${role.role}}`;
|
|
289
|
+
// Same pre/post-verb split buildTokens applies, so the documented format
|
|
290
|
+
// string keeps describing the tokens it is generated alongside.
|
|
291
|
+
const bucket = isSOV && role.sovSlot === 'postVerb' ? postVerb : preVerb;
|
|
282
292
|
|
|
283
293
|
if (marker) {
|
|
284
294
|
const markerInfo = profile.roleMarkers?.[role.role];
|
|
285
295
|
if (markerInfo?.position === 'after') {
|
|
286
|
-
|
|
296
|
+
bucket.push(`${roleName} ${marker}`);
|
|
287
297
|
} else {
|
|
288
|
-
|
|
298
|
+
bucket.push(`${marker} ${roleName}`);
|
|
289
299
|
}
|
|
290
300
|
} else {
|
|
291
|
-
|
|
301
|
+
bucket.push(roleName);
|
|
292
302
|
}
|
|
293
303
|
}
|
|
294
304
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return parts.join(' ');
|
|
305
|
+
return (isSOV ? [...preVerb, keyword, ...postVerb] : [keyword, ...preVerb, ...postVerb]).join(
|
|
306
|
+
' '
|
|
307
|
+
);
|
|
300
308
|
}
|
|
301
309
|
|
|
302
310
|
/**
|
|
@@ -168,6 +168,8 @@ describe('detectWordOrders', () => {
|
|
|
168
168
|
// =============================================================================
|
|
169
169
|
|
|
170
170
|
describe('createSchemaRenderer', () => {
|
|
171
|
+
// Positions sort DESCENDING (higher = earlier), matching pattern generation:
|
|
172
|
+
// columns (2) precedes source (1) → "select name from users".
|
|
171
173
|
const schemas: CommandSchema[] = [
|
|
172
174
|
defineCommand({
|
|
173
175
|
action: 'select',
|
|
@@ -179,13 +181,13 @@ describe('createSchemaRenderer', () => {
|
|
|
179
181
|
role: 'columns',
|
|
180
182
|
required: true,
|
|
181
183
|
expectedTypes: ['expression'],
|
|
182
|
-
svoPosition:
|
|
184
|
+
svoPosition: 2,
|
|
183
185
|
}),
|
|
184
186
|
defineRole({
|
|
185
187
|
role: 'source',
|
|
186
188
|
required: true,
|
|
187
189
|
expectedTypes: ['expression'],
|
|
188
|
-
svoPosition:
|
|
190
|
+
svoPosition: 1,
|
|
189
191
|
markerOverride: { en: 'from', ja: 'から' },
|
|
190
192
|
}),
|
|
191
193
|
],
|
|
@@ -239,7 +241,7 @@ describe('createSchemaRenderer', () => {
|
|
|
239
241
|
role: 'columns',
|
|
240
242
|
required: true,
|
|
241
243
|
expectedTypes: ['expression'],
|
|
242
|
-
svoPosition:
|
|
244
|
+
svoPosition: 3,
|
|
243
245
|
}),
|
|
244
246
|
defineRole({
|
|
245
247
|
role: 'source',
|
|
@@ -252,7 +254,7 @@ describe('createSchemaRenderer', () => {
|
|
|
252
254
|
role: 'condition',
|
|
253
255
|
required: false,
|
|
254
256
|
expectedTypes: ['expression'],
|
|
255
|
-
svoPosition:
|
|
257
|
+
svoPosition: 1,
|
|
256
258
|
markerOverride: { en: 'where' },
|
|
257
259
|
}),
|
|
258
260
|
],
|
|
@@ -263,4 +265,241 @@ describe('createSchemaRenderer', () => {
|
|
|
263
265
|
const node = makeNode('select', { columns: 'name', source: 'users' });
|
|
264
266
|
expect(renderer.render(node, 'en')).toBe('select name from users');
|
|
265
267
|
});
|
|
268
|
+
|
|
269
|
+
describe('absent roles never leave a dangling marker', () => {
|
|
270
|
+
// An "analyze"-shaped schema: `manner` is REQUIRED and marked, so a node
|
|
271
|
+
// missing it used to render "analyze #content as" / "#content として 分析".
|
|
272
|
+
const analyzeSchemas: CommandSchema[] = [
|
|
273
|
+
defineCommand({
|
|
274
|
+
action: 'analyze',
|
|
275
|
+
description: 'Analyze content',
|
|
276
|
+
category: 'llm',
|
|
277
|
+
primaryRole: 'patient',
|
|
278
|
+
roles: [
|
|
279
|
+
defineRole({
|
|
280
|
+
role: 'patient',
|
|
281
|
+
required: true,
|
|
282
|
+
expectedTypes: ['expression'],
|
|
283
|
+
svoPosition: 2,
|
|
284
|
+
}),
|
|
285
|
+
defineRole({
|
|
286
|
+
role: 'manner',
|
|
287
|
+
required: true,
|
|
288
|
+
expectedTypes: ['expression'],
|
|
289
|
+
svoPosition: 1,
|
|
290
|
+
markerOverride: { en: 'as', ja: 'として' },
|
|
291
|
+
}),
|
|
292
|
+
],
|
|
293
|
+
}),
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
const analyzeProfiles: PatternGenLanguageProfile[] = [
|
|
297
|
+
{
|
|
298
|
+
code: 'en',
|
|
299
|
+
wordOrder: 'SVO',
|
|
300
|
+
keywords: { analyze: { primary: 'analyze' } },
|
|
301
|
+
roleMarkers: {},
|
|
302
|
+
},
|
|
303
|
+
{ code: 'ja', wordOrder: 'SOV', keywords: { analyze: { primary: '分析' } }, roleMarkers: {} },
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
it('omits a required-but-absent role and its marker (SVO)', () => {
|
|
307
|
+
const renderer = createSchemaRenderer(analyzeSchemas, analyzeProfiles);
|
|
308
|
+
const node = makeNode('analyze', { patient: '#content' });
|
|
309
|
+
expect(renderer.render(node, 'en')).toBe('analyze #content');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('omits a required-but-absent role and its marker (SOV)', () => {
|
|
313
|
+
const renderer = createSchemaRenderer(analyzeSchemas, analyzeProfiles);
|
|
314
|
+
const node = makeNode('analyze', { patient: '#content' });
|
|
315
|
+
expect(renderer.render(node, 'ja')).toBe('#content 分析');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('still renders the marker when the role has a value', () => {
|
|
319
|
+
const renderer = createSchemaRenderer(analyzeSchemas, analyzeProfiles);
|
|
320
|
+
const node = makeNode('analyze', { patient: '#content', manner: 'sentiment' });
|
|
321
|
+
expect(renderer.render(node, 'en')).toBe('analyze #content as sentiment');
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('role positions', () => {
|
|
326
|
+
// Declaration order deliberately disagrees with the declared positions.
|
|
327
|
+
const outOfOrder: CommandSchema[] = [
|
|
328
|
+
defineCommand({
|
|
329
|
+
action: 'select',
|
|
330
|
+
description: 'Select data',
|
|
331
|
+
category: 'query',
|
|
332
|
+
primaryRole: 'columns',
|
|
333
|
+
roles: [
|
|
334
|
+
defineRole({
|
|
335
|
+
role: 'source',
|
|
336
|
+
required: true,
|
|
337
|
+
expectedTypes: ['expression'],
|
|
338
|
+
svoPosition: 1,
|
|
339
|
+
sovPosition: 1,
|
|
340
|
+
markerOverride: { en: 'from', ja: 'から' },
|
|
341
|
+
}),
|
|
342
|
+
defineRole({
|
|
343
|
+
role: 'columns',
|
|
344
|
+
required: true,
|
|
345
|
+
expectedTypes: ['expression'],
|
|
346
|
+
svoPosition: 2,
|
|
347
|
+
sovPosition: 2,
|
|
348
|
+
}),
|
|
349
|
+
],
|
|
350
|
+
}),
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
it('orders roles by svoPosition, not declaration order', () => {
|
|
354
|
+
const renderer = createSchemaRenderer(outOfOrder, profiles);
|
|
355
|
+
const node = makeNode('select', { columns: 'name', source: 'users' });
|
|
356
|
+
expect(renderer.render(node, 'en')).toBe('select name from users');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('orders roles by sovPosition for SOV languages', () => {
|
|
360
|
+
const renderer = createSchemaRenderer(outOfOrder, profiles);
|
|
361
|
+
const node = makeNode('select', { columns: 'name', source: 'users' });
|
|
362
|
+
expect(renderer.render(node, 'ja')).toBe('name users から 選択');
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('role defaults', () => {
|
|
367
|
+
const withDefault: CommandSchema[] = [
|
|
368
|
+
defineCommand({
|
|
369
|
+
action: 'select',
|
|
370
|
+
description: 'Select data',
|
|
371
|
+
category: 'query',
|
|
372
|
+
primaryRole: 'columns',
|
|
373
|
+
roles: [
|
|
374
|
+
defineRole({
|
|
375
|
+
role: 'columns',
|
|
376
|
+
required: true,
|
|
377
|
+
expectedTypes: ['expression'],
|
|
378
|
+
svoPosition: 2,
|
|
379
|
+
default: { type: 'literal', value: '*' },
|
|
380
|
+
}),
|
|
381
|
+
defineRole({
|
|
382
|
+
role: 'source',
|
|
383
|
+
required: true,
|
|
384
|
+
expectedTypes: ['expression'],
|
|
385
|
+
svoPosition: 1,
|
|
386
|
+
markerOverride: { en: 'from' },
|
|
387
|
+
}),
|
|
388
|
+
],
|
|
389
|
+
}),
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
it("renders a schema default in an absent role's place", () => {
|
|
393
|
+
const renderer = createSchemaRenderer(withDefault, profiles);
|
|
394
|
+
const node = makeNode('select', { source: 'users' });
|
|
395
|
+
expect(renderer.render(node, 'en')).toBe('select * from users');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('prefers the node value over the default', () => {
|
|
399
|
+
const renderer = createSchemaRenderer(withDefault, profiles);
|
|
400
|
+
const node = makeNode('select', { columns: 'name', source: 'users' });
|
|
401
|
+
expect(renderer.render(node, 'en')).toBe('select name from users');
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe('marker placement and formatting', () => {
|
|
406
|
+
const capabilities: CommandSchema[] = [
|
|
407
|
+
defineCommand({
|
|
408
|
+
action: 'summarize',
|
|
409
|
+
description: 'Summarize content',
|
|
410
|
+
category: 'llm',
|
|
411
|
+
primaryRole: 'patient',
|
|
412
|
+
roles: [
|
|
413
|
+
defineRole({
|
|
414
|
+
role: 'patient',
|
|
415
|
+
required: true,
|
|
416
|
+
expectedTypes: ['expression'],
|
|
417
|
+
svoPosition: 3,
|
|
418
|
+
sovPosition: 3,
|
|
419
|
+
quoteMultiword: true,
|
|
420
|
+
}),
|
|
421
|
+
defineRole({
|
|
422
|
+
role: 'quantity',
|
|
423
|
+
required: false,
|
|
424
|
+
expectedTypes: ['expression'],
|
|
425
|
+
svoPosition: 2,
|
|
426
|
+
sovPosition: 2,
|
|
427
|
+
markerOverride: { en: 'in', ja: 'で' },
|
|
428
|
+
// Japanese renders this marker BEFORE its value despite SOV default
|
|
429
|
+
markerPositionOverride: { ja: 'before' },
|
|
430
|
+
}),
|
|
431
|
+
defineRole({
|
|
432
|
+
role: 'manner',
|
|
433
|
+
required: false,
|
|
434
|
+
expectedTypes: ['expression'],
|
|
435
|
+
svoPosition: 1,
|
|
436
|
+
sovPosition: 1,
|
|
437
|
+
markerOverride: { en: 'as', ja: 'として' },
|
|
438
|
+
sovSlot: 'postVerb',
|
|
439
|
+
}),
|
|
440
|
+
defineRole({
|
|
441
|
+
role: 'source',
|
|
442
|
+
required: false,
|
|
443
|
+
expectedTypes: ['expression'],
|
|
444
|
+
svoPosition: 0,
|
|
445
|
+
markerOverride: { en: '' }, // parser-only marker; renders bare
|
|
446
|
+
}),
|
|
447
|
+
],
|
|
448
|
+
}),
|
|
449
|
+
];
|
|
450
|
+
|
|
451
|
+
const capProfiles: PatternGenLanguageProfile[] = [
|
|
452
|
+
{
|
|
453
|
+
code: 'en',
|
|
454
|
+
wordOrder: 'SVO',
|
|
455
|
+
keywords: { summarize: { primary: 'summarize' } },
|
|
456
|
+
roleMarkers: {},
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
code: 'ja',
|
|
460
|
+
wordOrder: 'SOV',
|
|
461
|
+
keywords: { summarize: { primary: '要約' } },
|
|
462
|
+
roleMarkers: {},
|
|
463
|
+
},
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
it('quotes a multi-word value for quoteMultiword roles', () => {
|
|
467
|
+
const renderer = createSchemaRenderer(capabilities, capProfiles);
|
|
468
|
+
const node = makeNode('summarize', { patient: 'the annual report' });
|
|
469
|
+
expect(renderer.render(node, 'en')).toBe('summarize "the annual report"');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('leaves a single-word value unquoted', () => {
|
|
473
|
+
const renderer = createSchemaRenderer(capabilities, capProfiles);
|
|
474
|
+
const node = makeNode('summarize', { patient: '#document' });
|
|
475
|
+
expect(renderer.render(node, 'en')).toBe('summarize #document');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('does not double-quote an already-quoted value', () => {
|
|
479
|
+
const renderer = createSchemaRenderer(capabilities, capProfiles);
|
|
480
|
+
const node = makeNode('summarize', { patient: '"the annual report"' });
|
|
481
|
+
expect(renderer.render(node, 'en')).toBe('summarize "the annual report"');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('honors markerPositionOverride against the word-order default', () => {
|
|
485
|
+
const renderer = createSchemaRenderer(capabilities, capProfiles);
|
|
486
|
+
const node = makeNode('summarize', { patient: '#doc', quantity: '3' });
|
|
487
|
+
// ja default would be "3 で"; the override puts the marker first
|
|
488
|
+
expect(renderer.render(node, 'ja')).toBe('#doc で 3 要約');
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('renders a sovSlot:postVerb role after the verb', () => {
|
|
492
|
+
const renderer = createSchemaRenderer(capabilities, capProfiles);
|
|
493
|
+
const node = makeNode('summarize', { patient: '#doc', manner: 'bullets' });
|
|
494
|
+
expect(renderer.render(node, 'ja')).toBe('#doc 要約 bullets として');
|
|
495
|
+
// SVO is unaffected — sovSlot applies only to SOV languages
|
|
496
|
+
expect(renderer.render(node, 'en')).toBe('summarize #doc as bullets');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("treats renderOverride '' as no marker", () => {
|
|
500
|
+
const renderer = createSchemaRenderer(capabilities, capProfiles);
|
|
501
|
+
const node = makeNode('summarize', { patient: '#doc', source: '#feed' });
|
|
502
|
+
expect(renderer.render(node, 'en')).toBe('summarize #doc #feed');
|
|
503
|
+
});
|
|
504
|
+
});
|
|
266
505
|
});
|