@specverse/engines 4.1.14 → 4.1.16

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.
Files changed (66) hide show
  1. package/assets/prompts/core/standard/v9/behavior.prompt.yaml +7 -1
  2. package/dist/ai/behavior-ai-service.d.ts +2 -0
  3. package/dist/ai/behavior-ai-service.d.ts.map +1 -1
  4. package/dist/ai/behavior-ai-service.js +2 -0
  5. package/dist/ai/behavior-ai-service.js.map +1 -1
  6. package/dist/ai/prompt-loader.js +2 -2
  7. package/dist/inference/index.d.ts +2 -1
  8. package/dist/inference/index.d.ts.map +1 -1
  9. package/dist/inference/index.js +2 -1
  10. package/dist/inference/index.js.map +1 -1
  11. package/dist/inference/quint-transpiler.d.ts +18 -1
  12. package/dist/inference/quint-transpiler.d.ts.map +1 -1
  13. package/dist/inference/quint-transpiler.js +501 -21
  14. package/dist/inference/quint-transpiler.js.map +1 -1
  15. package/dist/inference/verification.d.ts +78 -0
  16. package/dist/inference/verification.d.ts.map +1 -0
  17. package/dist/inference/verification.js +263 -0
  18. package/dist/inference/verification.js.map +1 -0
  19. package/dist/libs/instance-factories/applications/templates/generic/backend-package-json-generator.js +4 -1
  20. package/dist/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.js +2 -2
  21. package/dist/libs/instance-factories/applications/templates/react/runtime-package-json-generator.js +1 -0
  22. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +111 -27
  23. package/dist/libs/instance-factories/communication/templates/eventemitter/bus-generator.js +2 -3
  24. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +21 -1
  25. package/dist/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.js +10 -2
  26. package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +130 -22
  27. package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +14 -7
  28. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +29 -54
  29. package/dist/libs/instance-factories/services/templates/prisma/service-generator.js +31 -10
  30. package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +1 -1
  31. package/dist/libs/instance-factories/views/templates/react/components-generator.js +40 -10
  32. package/dist/realize/index.d.ts.map +1 -1
  33. package/dist/realize/index.js +123 -25
  34. package/dist/realize/index.js.map +1 -1
  35. package/libs/instance-factories/applications/templates/generic/backend-package-json-generator.ts +4 -1
  36. package/libs/instance-factories/applications/templates/generic/backend-tsconfig-generator.ts +2 -2
  37. package/libs/instance-factories/applications/templates/react/runtime-package-json-generator.ts +6 -1
  38. package/libs/instance-factories/cli/templates/commander/command-generator.ts +134 -27
  39. package/libs/instance-factories/communication/templates/eventemitter/bus-generator.ts +2 -3
  40. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +27 -2
  41. package/libs/instance-factories/scaffolding/templates/generic/tsconfig-generator.ts +23 -2
  42. package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +185 -20
  43. package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +34 -9
  44. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +37 -59
  45. package/libs/instance-factories/services/templates/prisma/service-generator.ts +40 -10
  46. package/libs/instance-factories/services/templates/prisma/step-conventions.ts +4 -1
  47. package/libs/instance-factories/views/templates/react/components-generator.ts +50 -10
  48. package/package.json +1 -1
  49. package/dist/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.js +0 -232
  50. package/dist/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.js +0 -49
  51. package/dist/libs/instance-factories/tools/templates/mcp/static/src/index.js +0 -18
  52. package/dist/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.js +0 -0
  53. package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.js +0 -97
  54. package/dist/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.js +0 -64
  55. package/dist/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.js +0 -182
  56. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.js +0 -1210
  57. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.js +0 -172
  58. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.js +0 -240
  59. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.js +0 -147
  60. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.js +0 -281
  61. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.js +0 -409
  62. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.js +0 -414
  63. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.js +0 -467
  64. package/dist/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.js +0 -135
  65. package/dist/libs/instance-factories/tools/templates/mcp/static/src/types/index.js +0 -0
  66. package/dist/libs/instance-factories/tools/templates/vscode/static/extension.js +0 -965
@@ -39,6 +39,38 @@ export function transpileEntityGuards(entitiesDir) {
39
39
  }
40
40
  return guards;
41
41
  }
42
+ /**
43
+ * Scan Quint behaviour files for module-level `var X: ...` declarations.
44
+ * These are the state variables guards read from; the runtime host
45
+ * populates them via `setSpecSnapshot` before invoking any `check_*`.
46
+ */
47
+ export function extractStateVariables(entitiesDir) {
48
+ const found = new Set();
49
+ const varRe = /^\s*var\s+(\w+)\s*:/gm;
50
+ for (const category of ['core', 'extensions']) {
51
+ const categoryDir = join(entitiesDir, category);
52
+ if (!existsSync(categoryDir))
53
+ continue;
54
+ for (const entity of readdirSync(categoryDir)) {
55
+ const behaviourDir = join(categoryDir, entity, 'behaviour');
56
+ if (!existsSync(behaviourDir))
57
+ continue;
58
+ for (const file of readdirSync(behaviourDir).filter(f => f.endsWith('.qnt'))) {
59
+ const content = readFileSync(join(behaviourDir, file), 'utf8');
60
+ let m;
61
+ varRe.lastIndex = 0;
62
+ while ((m = varRe.exec(content)) !== null) {
63
+ // Skip `fullyExpanded` and similar pure-inference state vars
64
+ // — they're booleans, not array-of-entity collections.
65
+ if (m[1] === 'fullyExpanded')
66
+ continue;
67
+ found.add(m[1]);
68
+ }
69
+ }
70
+ }
71
+ }
72
+ return [...found].sort();
73
+ }
42
74
  /**
43
75
  * Transpile a single Quint file into TypeScript guards.
44
76
  */
@@ -46,6 +78,15 @@ export function transpileQuintFile(content, entity, filename) {
46
78
  const guards = [];
47
79
  const lines = content.split('\n');
48
80
  const moduleName = extractModuleName(content) || entity;
81
+ // Collect file-level `var X: ...` declarations so we know which symbols
82
+ // to treat as dependencies (vs local parameters) when scanning guard
83
+ // bodies.
84
+ const fileStateVars = new Set();
85
+ for (const line of lines) {
86
+ const vm = line.match(/^\s*var\s+(\w+)\s*:/);
87
+ if (vm && vm[1] !== 'fullyExpanded')
88
+ fileStateVars.add(vm[1]);
89
+ }
49
90
  let i = 0;
50
91
  while (i < lines.length) {
51
92
  const line = lines[i].trim();
@@ -64,6 +105,7 @@ export function transpileQuintFile(content, entity, filename) {
64
105
  typescript: `export function ${name}(${tsParams}): ${tsReturn} {\n ${tsBody}\n}`,
65
106
  module: moduleName,
66
107
  entity,
108
+ dependencies: extractGuardDependencies(body, fileStateVars),
67
109
  });
68
110
  i = skipBlock(lines, i);
69
111
  continue;
@@ -79,14 +121,16 @@ export function transpileQuintFile(content, entity, filename) {
79
121
  body += lines[i].trim() + ' ';
80
122
  i++;
81
123
  }
82
- const tsBody = transpileExpression(body.trim());
124
+ const bodyTrim = body.trim();
125
+ const tsBody = transpileExpression(bodyTrim);
83
126
  guards.push({
84
127
  name,
85
128
  kind: 'invariant',
86
- quint: `val ${name}: bool = ${body.trim()}`,
87
- typescript: `export function check_${name}(data: any): boolean {\n return ${tsBody};\n}`,
129
+ quint: `val ${name}: bool = ${bodyTrim}`,
130
+ typescript: `export function check_${name}(_data?: any): boolean {\n return ${tsBody};\n}`,
88
131
  module: moduleName,
89
132
  entity,
133
+ dependencies: extractGuardDependencies(bodyTrim, fileStateVars),
90
134
  });
91
135
  continue;
92
136
  }
@@ -102,20 +146,67 @@ export function transpileQuintFile(content, entity, filename) {
102
146
  typescript: `export function check_${name}(data: any): boolean {\n return ${tsExpr};\n}`,
103
147
  module: moduleName,
104
148
  entity,
149
+ dependencies: extractGuardDependencies(expr.trim(), fileStateVars),
105
150
  });
106
151
  }
107
152
  i++;
108
153
  }
109
154
  return guards;
110
155
  }
156
+ /**
157
+ * Scan a Quint expression (pre-transpile) and return which file-level
158
+ * state variables it reads. Used to decide whether a guard is applicable
159
+ * to a given spec snapshot: if the snapshot doesn't populate one of the
160
+ * guard's dependencies, verify skips the guard.
161
+ */
162
+ function extractGuardDependencies(body, fileStateVars) {
163
+ if (!fileStateVars.size)
164
+ return [];
165
+ const deps = new Set();
166
+ // Match bare identifiers not preceded by a word char or dot (so
167
+ // `m.models` — field access on a lambda param — doesn't count, but
168
+ // `models.forall(...)` does).
169
+ for (const v of fileStateVars) {
170
+ const re = new RegExp(`(?<![A-Za-z0-9_.])${v}\\b`);
171
+ if (re.test(body))
172
+ deps.add(v);
173
+ }
174
+ return [...deps].sort();
175
+ }
111
176
  /**
112
177
  * Generate a TypeScript guards module from transpiled guards.
178
+ *
179
+ * `stateVars` is the list of Quint module-level `var` declarations the
180
+ * guards reference. When omitted, falls back to a minimal hard-coded
181
+ * set — but callers should pass the real list from `extractStateVariables`.
113
182
  */
114
- export function generateGuardsModule(guards) {
183
+ export function generateGuardsModule(guards, stateVars) {
115
184
  if (guards.length === 0)
116
185
  return '// No guards generated\nexport {};\n';
186
+ // Deduplicate by name — multiple modules may define the same invariant
187
+ // (e.g. invariants.qnt and rules.qnt). First occurrence wins.
188
+ const seen = new Set();
189
+ guards = guards.filter(g => {
190
+ const key = `${g.kind}:${g.name}`;
191
+ if (seen.has(key))
192
+ return false;
193
+ seen.add(key);
194
+ return true;
195
+ });
117
196
  const functions = guards.filter(g => g.kind === 'function');
118
197
  const invariants = guards.filter(g => g.kind === 'invariant');
198
+ const candidateStateVars = stateVars && stateVars.length > 0
199
+ ? stateVars
200
+ : ['models', 'controllers', 'services', 'views', 'events', 'deployments', 'commands', 'conventions', 'measures'];
201
+ // Only declare state vars the surviving guards actually reference —
202
+ // otherwise `noUnusedLocals` flags declarations for vars whose only
203
+ // consumer was a deduplicated guard. Exclude dot-prefixed matches
204
+ // (`d.channels` is a field access on a lambda parameter, not the
205
+ // module-level `channels` state variable).
206
+ const guardBodies = guards.map(g => g.typescript).join('\n');
207
+ const resolvedStateVars = candidateStateVars.filter(v => new RegExp(`(?<![\\w.])${v}\\b`).test(guardBodies));
208
+ const declarations = resolvedStateVars.map(v => `let ${v}: any[] = [];`);
209
+ const assignments = resolvedStateVars.map(v => ` ${v} = snapshot?.${v} ?? [];`);
119
210
  const lines = [
120
211
  '/**',
121
212
  ' * Runtime Guards — Transpiled from Quint Specifications',
@@ -128,6 +219,17 @@ export function generateGuardsModule(guards) {
128
219
  ' * Generated by: @specverse/engine-inference quint-transpiler',
129
220
  ' */',
130
221
  '',
222
+ '// Quint module-level state variables — initialized via setSpecSnapshot().',
223
+ '// The guards read from these; a host caller populates them before',
224
+ '// invoking any check_* function. Variable set is auto-discovered from',
225
+ '// the guard bodies.',
226
+ '/* eslint-disable @typescript-eslint/no-unused-vars, prefer-const */',
227
+ ...declarations,
228
+ '',
229
+ 'export function setSpecSnapshot(snapshot: any): void {',
230
+ ...assignments,
231
+ '}',
232
+ '',
131
233
  ];
132
234
  // Group by entity
133
235
  const byEntity = new Map();
@@ -392,10 +494,7 @@ function transpileBody(body) {
392
494
  ts = ts.replace(/\}\s*$/, '');
393
495
  // Normalize — join multiline into single line, collapse whitespace
394
496
  ts = ts.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
395
- // Replace Quint operators with TypeScript
396
- ts = ts.replace(/\band\b/g, '&&');
397
- ts = ts.replace(/\bor\b/g, '||');
398
- ts = ts.replace(/\bnot\b/g, '!');
497
+ ts = applyQuintRewrites(ts);
399
498
  // Clean up comments
400
499
  ts = ts.replace(/\/\/.*?(?=\bif\b|\belse\b|$)/g, '');
401
500
  // Replace if/else chains → ternary (from innermost out)
@@ -409,25 +508,406 @@ function transpileBody(body) {
409
508
  }
410
509
  return `return ${ts.trim()};`;
411
510
  }
412
- function transpileExpression(expr) {
413
- let ts = expr;
414
- // Quint operators TypeScript
511
+ /**
512
+ * Apply the full sequence of Quint → TypeScript rewrites to an expression
513
+ * string. Ordering matters: method-name rewrites (forall, exists, keys, get)
514
+ * must run BEFORE `transpileImplies`, because the implies walker scans
515
+ * backward through method chains to find the left operand and needs to see
516
+ * JS-shaped method names.
517
+ */
518
+ function applyQuintRewrites(ts) {
519
+ // 1. Boolean operators
415
520
  ts = ts.replace(/\band\b/g, '&&');
416
521
  ts = ts.replace(/\bor\b/g, '||');
417
522
  ts = ts.replace(/\bnot\b/g, '!');
418
- // forall .every
419
- ts = ts.replace(/(\w+)\.forall\((\w+)\s*=>\s*/g, '$1.every(($2: any) => ');
420
- // exists.some
421
- ts = ts.replace(/(\w+)\.exists\((\w+)\s*=>\s*/g, '$1.some(($2: any) => ');
422
- // .size() → .length or .size
523
+ // 2. Local `val` bindings inside blocks
524
+ ts = transpileValBindings(ts);
525
+ // 3. Quint collection operations JS equivalents
526
+ // These run before implies so the walker sees valid JS method names.
527
+ // forall / exists with lambda → .every / .some
528
+ ts = ts.replace(/\.forall\(\s*(\w+)\s*=>\s*/g, '.every(($1: any) => ');
529
+ ts = ts.replace(/\.exists\(\s*(\w+)\s*=>\s*/g, '.some(($1: any) => ');
530
+ // filter / map / flatMap: keep the method name but type the lambda param
531
+ ts = ts.replace(/\.filter\(\s*(\w+)\s*=>\s*/g, '.filter(($1: any) => ');
532
+ ts = ts.replace(/\.map\(\s*(\w+)\s*=>\s*/g, '.map(($1: any) => ');
533
+ ts = ts.replace(/\.flatMap\(\s*(\w+)\s*=>\s*/g, '.flatMap(($1: any) => ');
534
+ // Set literal: Set("a", "b") → ["a", "b"]
535
+ ts = ts.replace(/\bSet\(([^)]+)\)/g, '[$1]');
536
+ // subseteq (subset of) → runtime check via .every + .includes.
537
+ // `X.subseteq(Y)` becomes `X.every((__v: any) => Y.includes(__v))`.
538
+ // Uses a balanced walker because X can be an array literal or method chain.
539
+ ts = rewriteSubseteq(ts);
540
+ // Map operations on plain-object data:
541
+ // X.keys() → Object.keys(X)
542
+ // X.get(k) → X[k]
543
+ // Quint maps use .keys()/.get(); when the runtime data is a plain object
544
+ // (which is what inference produces), these need JS-equivalent forms.
545
+ ts = rewriteKeysGet(ts);
546
+ // .size() → .length (works on arrays from .keys() and on plain arrays)
423
547
  ts = ts.replace(/\.size\(\)/g, '.length');
424
- // .contains → .includes (for arrays) or .has (for Sets)
548
+ // .contains → .includes
425
549
  ts = ts.replace(/\.contains\(/g, '.includes(');
426
- // Set("a", "b") → ["a", "b"]
427
- ts = ts.replace(/Set\(([^)]+)\)/g, '[$1]');
428
550
  // != → !==, == → ===
429
- ts = ts.replace(/([^!<>])==/g, '$1===');
430
- ts = ts.replace(/!=/g, '!==');
551
+ ts = ts.replace(/([^!<>])==(?!=)/g, '$1===');
552
+ ts = ts.replace(/!=(?!=)/g, '!==');
553
+ // 4. implies → (!A || B). Runs LAST so the walker sees JS-shaped methods.
554
+ ts = transpileImplies(ts);
431
555
  return ts;
432
556
  }
557
+ /**
558
+ * Transpile `A implies B` → `(!(A) || (B))`.
559
+ * A is the boolean expression immediately to the left of `implies`, which may
560
+ * be parenthesized or a bare comparison/member-access chain.
561
+ * B is the rest of the expression up to the next enclosing boundary.
562
+ */
563
+ function transpileImplies(ts) {
564
+ let out = ts;
565
+ let guard = 50;
566
+ while (guard-- > 0) {
567
+ const m = out.match(/\bimplies\b/);
568
+ if (!m || m.index === undefined)
569
+ break;
570
+ const idx = m.index;
571
+ // --- find left operand start ---
572
+ let i = idx - 1;
573
+ while (i >= 0 && /\s/.test(out[i]))
574
+ i--;
575
+ if (i < 0)
576
+ break;
577
+ let leftStart;
578
+ if (out[i] === ')') {
579
+ // Balanced parenthesized left operand
580
+ let depth = 1;
581
+ let k = i - 1;
582
+ while (k >= 0 && depth > 0) {
583
+ const ch = out[k];
584
+ if (ch === ')')
585
+ depth++;
586
+ else if (ch === '(')
587
+ depth--;
588
+ if (depth === 0)
589
+ break;
590
+ k--;
591
+ }
592
+ if (depth !== 0)
593
+ break;
594
+ // If the paren is a call paren (preceded by identifier or '.'), keep
595
+ // walking backward through the method chain so the left operand
596
+ // includes the receiver as well.
597
+ let kk = k - 1;
598
+ while (kk >= 0 && /[A-Za-z0-9_.]/.test(out[kk]))
599
+ kk--;
600
+ // If we consumed any chars, it's a method/receiver chain — extend.
601
+ // The consumed range may start with '.' (e.g. `.exists` after `.keys()`)
602
+ // so we can't restrict the first char to a letter.
603
+ if (kk + 1 < k) {
604
+ // Extend through chained receivers: `a.b.c(args)`, `a.b(x).c(y)`
605
+ // (parens), and `[...].c(y)` (array literals).
606
+ let prev = kk;
607
+ while (prev >= 0) {
608
+ const ch = out[prev];
609
+ if (/[A-Za-z0-9_.]/.test(ch)) {
610
+ prev--;
611
+ continue;
612
+ }
613
+ if (ch === ')' || ch === ']') {
614
+ const close = ch;
615
+ const open = close === ')' ? '(' : '[';
616
+ let d = 1;
617
+ prev--;
618
+ while (prev >= 0 && d > 0) {
619
+ if (out[prev] === close)
620
+ d++;
621
+ else if (out[prev] === open)
622
+ d--;
623
+ if (d === 0)
624
+ break;
625
+ prev--;
626
+ }
627
+ if (d !== 0)
628
+ break;
629
+ prev--;
630
+ continue;
631
+ }
632
+ break;
633
+ }
634
+ leftStart = prev + 1;
635
+ }
636
+ else {
637
+ leftStart = k;
638
+ }
639
+ }
640
+ else {
641
+ // Bare expression — walk backward until we hit a boundary at depth 0
642
+ let depth = 0;
643
+ let k = i;
644
+ while (k >= 0) {
645
+ const ch = out[k];
646
+ if (ch === ')' || ch === ']' || ch === '}')
647
+ depth++;
648
+ else if (ch === '(' || ch === '[' || ch === '{') {
649
+ if (depth === 0) {
650
+ k++;
651
+ break;
652
+ }
653
+ depth--;
654
+ }
655
+ else if (depth === 0) {
656
+ if (ch === ',' || ch === ';' || ch === '?') {
657
+ k++;
658
+ break;
659
+ }
660
+ if (k > 0) {
661
+ const prev = out[k - 1];
662
+ if ((ch === '&' && prev === '&') || (ch === '|' && prev === '|') || (ch === '>' && prev === '=')) {
663
+ k++;
664
+ break;
665
+ }
666
+ }
667
+ }
668
+ k--;
669
+ }
670
+ if (k < 0)
671
+ k = 0;
672
+ leftStart = k;
673
+ }
674
+ const left = out.substring(leftStart, i + 1).trim();
675
+ // --- find right operand end ---
676
+ let j = idx + 'implies'.length;
677
+ while (j < out.length && /\s/.test(out[j]))
678
+ j++;
679
+ const rightStart = j;
680
+ let rDepth = 0;
681
+ while (j < out.length) {
682
+ const ch = out[j];
683
+ if (ch === '(' || ch === '[' || ch === '{')
684
+ rDepth++;
685
+ else if (ch === ')' || ch === ']' || ch === '}') {
686
+ if (rDepth === 0)
687
+ break;
688
+ rDepth--;
689
+ }
690
+ else if (rDepth === 0) {
691
+ if (ch === ',' || ch === ';')
692
+ break;
693
+ if (j + 1 < out.length) {
694
+ const next = out[j + 1];
695
+ if ((ch === '&' && next === '&') || (ch === '|' && next === '|'))
696
+ break;
697
+ }
698
+ }
699
+ j++;
700
+ }
701
+ const right = out.substring(rightStart, j).trim();
702
+ out = out.substring(0, leftStart) + `(!(${left}) || (${right}))` + out.substring(j);
703
+ }
704
+ return out;
705
+ }
706
+ /**
707
+ * Rewrite `X.subseteq(Y)` → `X.every((__v: any) => Y.includes(__v))`.
708
+ * X can be an array literal (including spaces before the dot) or a method
709
+ * chain; walk backward from `.subseteq(` to find its receiver with balanced
710
+ * brackets.
711
+ */
712
+ function rewriteSubseteq(ts) {
713
+ let out = ts;
714
+ let guard = 50;
715
+ while (guard-- > 0) {
716
+ const m = out.match(/\.\s*subseteq\s*\(/);
717
+ if (!m || m.index === undefined)
718
+ break;
719
+ const dotIdx = m.index;
720
+ const openIdx = dotIdx + m[0].length; // position of `(`
721
+ // Walk backward from dotIdx to find receiver start
722
+ let i = dotIdx - 1;
723
+ // skip whitespace
724
+ while (i >= 0 && /\s/.test(out[i]))
725
+ i--;
726
+ if (i < 0)
727
+ break;
728
+ let receiverStart;
729
+ if (out[i] === ']') {
730
+ // Array literal receiver — balance brackets
731
+ let depth = 1;
732
+ let k = i - 1;
733
+ while (k >= 0 && depth > 0) {
734
+ if (out[k] === ']')
735
+ depth++;
736
+ else if (out[k] === '[')
737
+ depth--;
738
+ if (depth === 0)
739
+ break;
740
+ k--;
741
+ }
742
+ if (depth !== 0)
743
+ break;
744
+ receiverStart = k;
745
+ }
746
+ else if (out[i] === ')') {
747
+ // Call-expression receiver — balance parens
748
+ let depth = 1;
749
+ let k = i - 1;
750
+ while (k >= 0 && depth > 0) {
751
+ if (out[k] === ')')
752
+ depth++;
753
+ else if (out[k] === '(')
754
+ depth--;
755
+ if (depth === 0)
756
+ break;
757
+ k--;
758
+ }
759
+ if (depth !== 0)
760
+ break;
761
+ // Then walk back through any identifier chain preceding the `(`
762
+ let kk = k - 1;
763
+ while (kk >= 0 && /[A-Za-z0-9_.]/.test(out[kk]))
764
+ kk--;
765
+ receiverStart = kk + 1;
766
+ }
767
+ else if (/[A-Za-z0-9_]/.test(out[i])) {
768
+ // Bare identifier receiver
769
+ let k = i;
770
+ while (k >= 0 && /[A-Za-z0-9_.]/.test(out[k]))
771
+ k--;
772
+ receiverStart = k + 1;
773
+ }
774
+ else {
775
+ break;
776
+ }
777
+ const receiver = out.substring(receiverStart, i + 1);
778
+ // Find argument end (balanced parens from openIdx)
779
+ let depth = 1;
780
+ let j = openIdx;
781
+ while (j < out.length && depth > 0) {
782
+ const ch = out[j];
783
+ if (ch === '(')
784
+ depth++;
785
+ else if (ch === ')') {
786
+ depth--;
787
+ if (depth === 0)
788
+ break;
789
+ }
790
+ j++;
791
+ }
792
+ if (depth !== 0)
793
+ break;
794
+ const argStr = out.substring(openIdx, j).trim();
795
+ const replacement = `${receiver}.every((__v: any) => ${argStr}.includes(__v))`;
796
+ out = out.substring(0, receiverStart) + replacement + out.substring(j + 1);
797
+ }
798
+ return out;
799
+ }
800
+ /**
801
+ * Rewrite `X.keys()` → `Object.keys(X)` and `X.get(k)` → `X[k]`.
802
+ * Quint Maps use `.keys()`/`.get()`; when the runtime data is a plain object
803
+ * (which is what the inferred SpecVerse AST produces), these need JS forms.
804
+ * Walks backward from the method to capture the receiver (identifier chain
805
+ * or nested call).
806
+ */
807
+ function rewriteKeysGet(ts) {
808
+ let out = ts;
809
+ // Handle .keys() — no arguments
810
+ let guard = 100;
811
+ while (guard-- > 0) {
812
+ const m = out.match(/\.\s*keys\s*\(\s*\)/);
813
+ if (!m || m.index === undefined)
814
+ break;
815
+ const dotIdx = m.index;
816
+ const endIdx = dotIdx + m[0].length; // one past closing `)`
817
+ // Walk back to receiver start
818
+ const receiverStart = findReceiverStart(out, dotIdx);
819
+ if (receiverStart === -1)
820
+ break;
821
+ const receiver = out.substring(receiverStart, dotIdx);
822
+ out = out.substring(0, receiverStart) + `Object.keys(${receiver})` + out.substring(endIdx);
823
+ }
824
+ // Handle .get(k) — one argument
825
+ guard = 100;
826
+ while (guard-- > 0) {
827
+ const m = out.match(/\.\s*get\s*\(/);
828
+ if (!m || m.index === undefined)
829
+ break;
830
+ const dotIdx = m.index;
831
+ const openIdx = dotIdx + m[0].length; // position after `(`
832
+ // Find closing paren (balanced)
833
+ let depth = 1;
834
+ let j = openIdx;
835
+ while (j < out.length && depth > 0) {
836
+ const ch = out[j];
837
+ if (ch === '(')
838
+ depth++;
839
+ else if (ch === ')') {
840
+ depth--;
841
+ if (depth === 0)
842
+ break;
843
+ }
844
+ j++;
845
+ }
846
+ if (depth !== 0)
847
+ break;
848
+ const argStr = out.substring(openIdx, j).trim();
849
+ // Walk back to receiver start
850
+ const receiverStart = findReceiverStart(out, dotIdx);
851
+ if (receiverStart === -1)
852
+ break;
853
+ const receiver = out.substring(receiverStart, dotIdx);
854
+ out = out.substring(0, receiverStart) + `${receiver}[${argStr}]` + out.substring(j + 1);
855
+ }
856
+ return out;
857
+ }
858
+ /**
859
+ * Walk backward from `dotIdx` (the position of `.`) to find where the
860
+ * receiver expression starts. Returns the index of the first char of the
861
+ * receiver, or -1 if it can't be parsed.
862
+ */
863
+ function findReceiverStart(s, dotIdx) {
864
+ let i = dotIdx - 1;
865
+ // Skip whitespace
866
+ while (i >= 0 && /\s/.test(s[i]))
867
+ i--;
868
+ if (i < 0)
869
+ return -1;
870
+ if (s[i] === ')' || s[i] === ']') {
871
+ // Balanced close-bracket — walk back to matching open
872
+ const close = s[i];
873
+ const open = close === ')' ? '(' : '[';
874
+ let depth = 1;
875
+ let k = i - 1;
876
+ while (k >= 0 && depth > 0) {
877
+ if (s[k] === close)
878
+ depth++;
879
+ else if (s[k] === open)
880
+ depth--;
881
+ if (depth === 0)
882
+ break;
883
+ k--;
884
+ }
885
+ if (depth !== 0)
886
+ return -1;
887
+ // Then walk back through any identifier chain
888
+ let kk = k - 1;
889
+ while (kk >= 0 && /[A-Za-z0-9_.]/.test(s[kk]))
890
+ kk--;
891
+ return kk + 1;
892
+ }
893
+ if (/[A-Za-z0-9_]/.test(s[i])) {
894
+ let k = i;
895
+ while (k >= 0 && /[A-Za-z0-9_.]/.test(s[k]))
896
+ k--;
897
+ return k + 1;
898
+ }
899
+ return -1;
900
+ }
901
+ /**
902
+ * Handle Quint local `val` bindings inside a block expression.
903
+ * `{ val X = CALL REST }` → `{ const X = CALL; return REST; }`
904
+ * Only supports a simple call-expression RHS; the block may contain multiple
905
+ * bindings followed by a single result expression.
906
+ */
907
+ function transpileValBindings(ts) {
908
+ return ts.replace(/\{\s*val\s+(\w+)\s*=\s*([\w.]+\([^()]*\))\s+(.+?)\s*\}/g, '{ const $1 = $2; return $3; }');
909
+ }
910
+ function transpileExpression(expr) {
911
+ return applyQuintRewrites(expr);
912
+ }
433
913
  //# sourceMappingURL=quint-transpiler.js.map