@lokascript/framework 4.5.0 → 4.6.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 +134 -8
- package/dist/api/create-dsl.d.ts +46 -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.js +187 -19
- 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/index.cjs +189 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +187 -19
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__test__/fixtures/sql-dsl.ts +3 -0
- package/src/__test__/sql-integration.test.ts +2 -2
- package/src/api/create-dsl.test.ts +120 -0
- package/src/api/create-dsl.ts +173 -21
- package/src/api/domain-registry.test.ts +70 -5
- package/src/api/domain-registry.ts +82 -1
- package/src/api/reading.ts +29 -0
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import type { MultilingualDSL, CompileResult, ValidationResult } from './create-dsl';
|
|
34
|
+
import { UnreadInputError, TranslationReadBackError } from './create-dsl';
|
|
35
|
+
import { readingDifference } from './reading';
|
|
36
|
+
import type { SemanticNode } from '../core/types';
|
|
34
37
|
import type { NaturalLanguageRenderer, DomainRenderFn } from '../generation/renderer';
|
|
35
38
|
import type { CommandSchema } from '../schema/command-schema';
|
|
36
39
|
import type { SchemaLookup } from '../ir/types';
|
|
@@ -244,6 +247,9 @@ export class DomainRegistry {
|
|
|
244
247
|
return jsonResponse({ error: `Unknown operation: ${parsed.operation}` }, true);
|
|
245
248
|
}
|
|
246
249
|
} catch (error) {
|
|
250
|
+
// A refusal is not a tool failure: say what was refused and why, as translate_code does.
|
|
251
|
+
if (error instanceof UnreadInputError) return unreadInputResponse(error);
|
|
252
|
+
if (error instanceof TranslationReadBackError) return readBackResponse(error);
|
|
247
253
|
const message = error instanceof Error ? error.message : String(error);
|
|
248
254
|
return jsonResponse({ error: `${descriptor.name} tool error: ${message}` }, true);
|
|
249
255
|
}
|
|
@@ -412,6 +418,9 @@ export class DomainRegistry {
|
|
|
412
418
|
ok: result.ok,
|
|
413
419
|
code: result.code,
|
|
414
420
|
errors: result.errors,
|
|
421
|
+
...(result.errorCode === 'unread-input'
|
|
422
|
+
? { diagnostics: [UNREAD_INPUT(result.errors)] }
|
|
423
|
+
: {}),
|
|
415
424
|
language,
|
|
416
425
|
input,
|
|
417
426
|
});
|
|
@@ -431,6 +440,9 @@ export class DomainRegistry {
|
|
|
431
440
|
return jsonResponse({
|
|
432
441
|
valid: result.valid,
|
|
433
442
|
errors: result.errors,
|
|
443
|
+
...(result.errorCode === 'unread-input'
|
|
444
|
+
? { diagnostics: [UNREAD_INPUT(result.errors)] }
|
|
445
|
+
: {}),
|
|
434
446
|
language,
|
|
435
447
|
[descriptor.inputLabel]: input,
|
|
436
448
|
});
|
|
@@ -478,7 +490,28 @@ export class DomainRegistry {
|
|
|
478
490
|
roles[key] = value;
|
|
479
491
|
}
|
|
480
492
|
|
|
493
|
+
// The rendering must read back as the source, by the rule `translate` uses: the same action and
|
|
494
|
+
// role values, or the same compiled code. A rendering that does not is refused, with the text under
|
|
495
|
+
// `partial`, never `rendered`.
|
|
496
|
+
if (rendered != null) {
|
|
497
|
+
let back: SemanticNode | null = null;
|
|
498
|
+
let why = '';
|
|
499
|
+
try {
|
|
500
|
+
back = dsl.parse(rendered, to);
|
|
501
|
+
why = readingDifference(node, back);
|
|
502
|
+
if (why && compiled.ok) {
|
|
503
|
+
const again = dsl.compile(rendered, to);
|
|
504
|
+
if (again.ok && again.code === compiled.code) why = '';
|
|
505
|
+
}
|
|
506
|
+
} catch (error) {
|
|
507
|
+
why = error instanceof Error ? error.message : String(error);
|
|
508
|
+
}
|
|
509
|
+
if (why)
|
|
510
|
+
return readBackResponse(new TranslationReadBackError(rendered, from, to, node, back, why));
|
|
511
|
+
}
|
|
512
|
+
|
|
481
513
|
return jsonResponse({
|
|
514
|
+
ok: true,
|
|
482
515
|
input: { [descriptor.inputLabel]: input, language: from },
|
|
483
516
|
...(rendered != null && { rendered: { text: rendered, language: to } }),
|
|
484
517
|
semantic: { action: node.action, roles },
|
|
@@ -567,7 +600,7 @@ function generateToolDefinitions(desc: DomainDescriptor): MCPToolDefinition[] {
|
|
|
567
600
|
case 'translate':
|
|
568
601
|
tools.push({
|
|
569
602
|
name: `translate_${desc.name}`,
|
|
570
|
-
description: `Translate ${desc.description} input between natural languages (requires 'from' and 'to' codes). Supports: ${langList}.`,
|
|
603
|
+
description: `Translate ${desc.description} input between natural languages (requires 'from' and 'to' codes). Supports: ${langList}. A rendering that does not read back as the source is REFUSED: ok is false, the diagnostic is TRANSLATION_DOES_NOT_READ_BACK, and its text is under \`partial\`, never \`rendered\`: do not present it as the translation. Input left unread is refused as UNREAD_INPUT.`,
|
|
571
604
|
inputSchema: {
|
|
572
605
|
type: 'object',
|
|
573
606
|
properties: {
|
|
@@ -609,6 +642,54 @@ function getString(args: Record<string, unknown>, name: string, defaultValue = '
|
|
|
609
642
|
return typeof value === 'string' ? value : defaultValue;
|
|
610
643
|
}
|
|
611
644
|
|
|
645
|
+
function readingOf(node: SemanticNode): { action: string; roles: Record<string, unknown> } {
|
|
646
|
+
const roles: Record<string, unknown> = {};
|
|
647
|
+
for (const [key, value] of node.roles) roles[key] = value;
|
|
648
|
+
return { action: node.action, roles };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const UNREAD_INPUT = (errors?: readonly string[]) => ({
|
|
652
|
+
severity: 'error',
|
|
653
|
+
code: 'UNREAD_INPUT',
|
|
654
|
+
message: errors?.[0] ?? 'part of the input was not read',
|
|
655
|
+
suggestion:
|
|
656
|
+
'Part of the input belongs to no role of the command it reads as: remove it, or quote a value of several words.',
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
/** A parse refused because it read the command only by leaving input unread. */
|
|
660
|
+
function unreadInputResponse(error: UnreadInputError): MCPToolResponse {
|
|
661
|
+
return jsonResponse(
|
|
662
|
+
{
|
|
663
|
+
ok: false,
|
|
664
|
+
unread: error.unread,
|
|
665
|
+
reading: readingOf(error.reading),
|
|
666
|
+
diagnostics: [UNREAD_INPUT([error.message])],
|
|
667
|
+
},
|
|
668
|
+
true
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** A translation refused because it does not read back as the source; its text under `partial`. */
|
|
673
|
+
function readBackResponse(error: TranslationReadBackError): MCPToolResponse {
|
|
674
|
+
return jsonResponse(
|
|
675
|
+
{
|
|
676
|
+
ok: false,
|
|
677
|
+
partial: error.partial,
|
|
678
|
+
...(error.reading ? { reading: readingOf(error.reading) } : {}),
|
|
679
|
+
diagnostics: [
|
|
680
|
+
{
|
|
681
|
+
severity: 'error',
|
|
682
|
+
code: 'TRANSLATION_DOES_NOT_READ_BACK',
|
|
683
|
+
message: error.message,
|
|
684
|
+
suggestion:
|
|
685
|
+
'The rendering would not mean the source in the target language: do not present `partial` as the translation.',
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
},
|
|
689
|
+
true
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
612
693
|
function jsonResponse(data: unknown, isError?: boolean): MCPToolResponse {
|
|
613
694
|
return {
|
|
614
695
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a read-back is compared (internal: not exported from the package). `translate` and the
|
|
3
|
+
* registry's translate tool both require a rendering to read back as its source; this is their rule.
|
|
4
|
+
*/
|
|
5
|
+
import type { SemanticNode } from '../core/types';
|
|
6
|
+
|
|
7
|
+
/** A node's role values as strings, by role: what a read-back is compared on. */
|
|
8
|
+
function roleValues(node: SemanticNode): Map<string, string> {
|
|
9
|
+
const out = new Map<string, string>();
|
|
10
|
+
for (const [role, value] of node.roles) {
|
|
11
|
+
const v = value as { value?: unknown; raw?: unknown };
|
|
12
|
+
out.set(role, String(v?.value ?? v?.raw ?? value));
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** How `back` differs from `source`; empty when it reads the same. */
|
|
18
|
+
export function readingDifference(source: SemanticNode, back: SemanticNode): string {
|
|
19
|
+
if (back.action !== source.action) return `reads as ${back.action}`;
|
|
20
|
+
const want = roleValues(source);
|
|
21
|
+
const got = roleValues(back);
|
|
22
|
+
const parts: string[] = [];
|
|
23
|
+
for (const [role, value] of want) {
|
|
24
|
+
if (!got.has(role)) parts.push(`${role} lost`);
|
|
25
|
+
else if (got.get(role) !== value) parts.push(`${role}: ${value} → ${got.get(role)}`);
|
|
26
|
+
}
|
|
27
|
+
for (const role of got.keys()) if (!want.has(role)) parts.push(`${role} added`);
|
|
28
|
+
return parts.join(', ');
|
|
29
|
+
}
|