@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.
@@ -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
+ }