@kernlang/test 3.4.0 → 3.4.2-canary.3.1.6b4cbb13

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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { generateCoreNode, parseDocumentWithDiagnostics, validateSchema, validateSemantics } from '@kernlang/core';
1
+ import { decompile, generateCoreNode, importTypeScript, parseDocumentWithDiagnostics, validateSchema, validateSemantics, } from '@kernlang/core';
2
2
  import { execFileSync } from 'child_process';
3
3
  import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
4
4
  import { dirname, join, relative, resolve } from 'path';
@@ -16,7 +16,7 @@ const DISCOVERY_SKIP_DIRS = new Set([
16
16
  ]);
17
17
  const NATIVE_TEST_PRESETS = {
18
18
  apisafety: ['duplicateRoutes', 'emptyRoutes', 'unvalidatedRoutes', 'unguardedEffects', 'uncheckedRoutePathParams'],
19
- coverage: ['untestedTransitions', 'untestedGuards'],
19
+ coverage: ['untestedTransitions', 'untestedGuards', 'untestedRoutes', 'untestedTools', 'untestedEffects'],
20
20
  effects: ['unguardedEffects', 'sensitiveEffectsRequireAuth', 'effectWithoutCleanup', 'unrecoveredAsync'],
21
21
  guard: ['invalidGuards', 'weakGuards', 'nonExhaustiveGuards'],
22
22
  machine: ['deadStates', 'duplicateTransitions'],
@@ -68,6 +68,30 @@ const NATIVE_KERN_TEST_RULES = [
68
68
  ruleId: 'runtime:behavior',
69
69
  description: 'Evaluate a constrained pure fn or derive assertion with scoped native test fixtures.',
70
70
  },
71
+ {
72
+ ruleId: 'runtime:route',
73
+ description: 'Evaluate a portable route workflow with request fixtures before backend code generation.',
74
+ },
75
+ {
76
+ ruleId: 'runtime:tool',
77
+ description: 'Evaluate a portable MCP tool workflow with input fixtures before backend code generation.',
78
+ },
79
+ {
80
+ ruleId: 'runtime:effect',
81
+ description: 'Evaluate a deterministic portable effect/recover workflow before backend code generation.',
82
+ },
83
+ {
84
+ ruleId: 'mock:called',
85
+ description: 'Assert how many times a scoped native effect mock was actually invoked.',
86
+ },
87
+ {
88
+ ruleId: 'import',
89
+ description: 'Assert that a TypeScript fixture imports to expected KERN source and optionally recompiles.',
90
+ },
91
+ {
92
+ ruleId: 'has:invariant',
93
+ description: 'Assert that an intentionally bad target KERN file contains the requested native invariant failure.',
94
+ },
71
95
  { ruleId: 'expect:unsupported', description: 'The expect assertion shape is not supported by native kern test.' },
72
96
  { ruleId: 'preset:unknown', description: 'The requested preset name is unknown.' },
73
97
  { ruleId: 'no:schemaviolations', description: 'The target KERN file has no schema violations.' },
@@ -174,6 +198,21 @@ const NATIVE_KERN_TEST_RULES = [
174
198
  description: 'Guards are covered by exhaustive or guard-preset assertions.',
175
199
  presets: ['coverage'],
176
200
  },
201
+ {
202
+ ruleId: 'no:untestedroutes',
203
+ description: 'Routes are covered by native route workflow assertions.',
204
+ presets: ['coverage'],
205
+ },
206
+ {
207
+ ruleId: 'no:untestedtools',
208
+ description: 'MCP tools are covered by native tool workflow assertions.',
209
+ presets: ['coverage'],
210
+ },
211
+ {
212
+ ruleId: 'no:untestedeffects',
213
+ description: 'Deterministic KERN effects are covered by native effect assertions.',
214
+ presets: ['coverage'],
215
+ },
177
216
  ];
178
217
  export function listNativeKernTestRules() {
179
218
  return NATIVE_KERN_TEST_RULES.map((rule) => ({
@@ -221,6 +260,24 @@ function exprPropToRuntimeSource(node, propName) {
221
260
  }
222
261
  return exprToString(value);
223
262
  }
263
+ function rawPropToRuntimeSource(node, propName) {
264
+ const props = getProps(node);
265
+ const value = props[propName];
266
+ if (value === undefined || value === '')
267
+ return '';
268
+ const expr = exprToString(value);
269
+ return expr || String(value);
270
+ }
271
+ function stringLiteralOrExprPropToRuntimeSource(node, propName) {
272
+ const props = getProps(node);
273
+ const value = props[propName];
274
+ if (value === undefined || value === '')
275
+ return '';
276
+ if (value && typeof value === 'object' && '__expr' in value)
277
+ return exprToString(value);
278
+ const source = String(value);
279
+ return isJsStringLiteralSource(source) ? source : JSON.stringify(source);
280
+ }
224
281
  function runtimeExpectedSource(node, propName) {
225
282
  const props = getProps(node);
226
283
  const value = props[propName];
@@ -312,6 +369,9 @@ function isAssertionConfigurationFailure(message) {
312
369
  message.startsWith('Runtime fn assertion ') ||
313
370
  message.startsWith('Runtime derive assertion ') ||
314
371
  message.startsWith('Runtime behavior assertion ') ||
372
+ message.startsWith('Runtime route assertion cannot ') ||
373
+ message.startsWith('Runtime tool assertion cannot ') ||
374
+ message.startsWith('Runtime effect assertion cannot ') ||
315
375
  message.startsWith('Node assertion requires ') ||
316
376
  message.startsWith('Node assertion count ') ||
317
377
  message === 'Unsupported native expect assertion.' ||
@@ -343,6 +403,9 @@ function grepMatches(options, result) {
343
403
  function invariantRuleId(value) {
344
404
  return `no:${normalizeInvariant(value) || 'unknown'}`;
345
405
  }
406
+ function hasInvariantRuleId(value) {
407
+ return `has:${normalizeInvariant(value) || 'unknown'}`;
408
+ }
346
409
  function presetRuleId(value) {
347
410
  return `preset:${normalizeInvariant(value) || 'unknown'}`;
348
411
  }
@@ -491,41 +554,119 @@ function runtimeFixtureBindings(node) {
491
554
  .map((fixture) => runtimeFixtureBinding(fixture))
492
555
  .filter((fixture) => fixture !== undefined);
493
556
  }
557
+ function runtimeEffectMock(node, id) {
558
+ const props = getProps(node);
559
+ const effect = str(props.effect);
560
+ const returns = runtimeExpectedSource(node, 'returns');
561
+ const throws = props.throws === undefined ? undefined : String(props.throws || 'true');
562
+ if (!effect || (returns === undefined && throws === undefined))
563
+ return undefined;
564
+ return { id, effect, returns, throws, line: node.loc?.line, col: node.loc?.col };
565
+ }
494
566
  function collectAssertions(testNode) {
495
567
  const suite = str(getProps(testNode).name) || 'unnamed test';
496
568
  const assertions = [];
497
- function pushExpectation(node, path, fixtures) {
569
+ let mockId = 0;
570
+ const caseMergeProps = new Set([
571
+ 'args',
572
+ 'with',
573
+ 'input',
574
+ 'equals',
575
+ 'returns',
576
+ 'recovers',
577
+ 'fallback',
578
+ 'matches',
579
+ 'throws',
580
+ 'called',
581
+ 'message',
582
+ 'severity',
583
+ ]);
584
+ function runtimeEffectMocks(node) {
585
+ return getChildren(node, 'mock')
586
+ .map((mock) => runtimeEffectMock(mock, `mock:${mockId++}`))
587
+ .filter((mock) => mock !== undefined);
588
+ }
589
+ function testCaseRows(node, fixtures, mocks) {
590
+ return getChildren(node, 'case').map((caseNode, index) => {
591
+ const label = str(getProps(caseNode).name) || `case ${index + 1}`;
592
+ return {
593
+ node: caseNode,
594
+ fixtures: [...fixtures, ...runtimeFixtureBindings(caseNode)],
595
+ mocks: [...mocks, ...runtimeEffectMocks(caseNode)],
596
+ label,
597
+ };
598
+ });
599
+ }
600
+ function expectationForCase(expectNode, caseNode) {
601
+ const mergedProps = { ...getProps(expectNode) };
602
+ const quotedProps = new Set(expectNode.__quotedProps || []);
603
+ const caseQuotedProps = new Set(caseNode.__quotedProps || []);
604
+ for (const [key, value] of Object.entries(getProps(caseNode))) {
605
+ if (!caseMergeProps.has(key))
606
+ continue;
607
+ mergedProps[key] = value;
608
+ if (caseQuotedProps.has(key))
609
+ quotedProps.add(key);
610
+ else
611
+ quotedProps.delete(key);
612
+ }
613
+ return {
614
+ ...expectNode,
615
+ props: mergedProps,
616
+ __quotedProps: [...quotedProps],
617
+ loc: caseNode.loc || expectNode.loc,
618
+ children: (expectNode.children || []).filter((child) => child.type !== 'case'),
619
+ };
620
+ }
621
+ function pushExpectation(node, path, fixtures, mocks, cases = []) {
622
+ if (cases.length > 0) {
623
+ for (const testCase of cases) {
624
+ assertions.push({
625
+ suite,
626
+ caseName: [...path, testCase.label].join(' > '),
627
+ node: expectationForCase(node, testCase.node),
628
+ fixtures: testCase.fixtures,
629
+ mocks: testCase.mocks,
630
+ });
631
+ }
632
+ return;
633
+ }
498
634
  assertions.push({
499
635
  suite,
500
636
  caseName: path.length > 0 ? path.join(' > ') : 'top-level',
501
637
  node,
502
638
  fixtures,
639
+ mocks,
503
640
  });
504
641
  }
505
- function visit(node, path, fixtures) {
642
+ function visit(node, path, fixtures, mocks) {
506
643
  const scopedFixtures = [...fixtures, ...runtimeFixtureBindings(node)];
644
+ const scopedMocks = [...mocks, ...runtimeEffectMocks(node)];
507
645
  if (node.type === 'expect') {
508
- pushExpectation(node, path, scopedFixtures);
646
+ pushExpectation(node, path, scopedFixtures, scopedMocks, testCaseRows(node, scopedFixtures, scopedMocks));
509
647
  return;
510
648
  }
511
649
  if (node.type === 'it') {
512
650
  const nextPath = [...path, str(getProps(node).name) || 'it'];
651
+ const scopedCases = testCaseRows(node, scopedFixtures, scopedMocks);
513
652
  for (const child of node.children || []) {
514
- if (child.type === 'expect')
515
- pushExpectation(child, nextPath, scopedFixtures);
653
+ if (child.type === 'expect') {
654
+ const expectCases = testCaseRows(child, scopedFixtures, scopedMocks);
655
+ pushExpectation(child, nextPath, scopedFixtures, scopedMocks, expectCases.length > 0 ? expectCases : scopedCases);
656
+ }
516
657
  }
517
658
  return;
518
659
  }
519
660
  if (node.type === 'describe') {
520
661
  const nextPath = [...path, str(getProps(node).name) || 'describe'];
521
662
  for (const child of node.children || [])
522
- visit(child, nextPath, scopedFixtures);
663
+ visit(child, nextPath, scopedFixtures, scopedMocks);
523
664
  return;
524
665
  }
525
666
  for (const child of node.children || [])
526
- visit(child, path, scopedFixtures);
667
+ visit(child, path, scopedFixtures, scopedMocks);
527
668
  }
528
- visit(testNode, [], []);
669
+ visit(testNode, [], [], []);
529
670
  return assertions;
530
671
  }
531
672
  function assertionLabel(node) {
@@ -544,15 +685,71 @@ function assertionLabel(node) {
544
685
  const to = str(props.to);
545
686
  const reaches = str(props.reaches);
546
687
  const no = str(props.no);
688
+ const has = str(props.has);
547
689
  const guard = str(props.guard);
548
690
  const expr = exprToString(props.expr);
549
691
  const fn = str(props.fn);
550
692
  const derive = str(props.derive);
693
+ const route = str(props.route);
694
+ const tool = str(props.tool);
695
+ const effect = str(props.effect);
696
+ const mock = str(props.mock);
697
+ const importAssertion = 'import' in props;
698
+ const importSource = importAssertionSourcePath(node);
699
+ const codegen = 'codegen' in props;
700
+ const decompileAssertion = 'decompile' in props;
701
+ const roundtrip = 'roundtrip' in props;
551
702
  const args = exprToString(props.args);
552
703
  const withValue = exprToString(props.with);
704
+ const input = exprToString(props.input);
553
705
  const equals = props.equals === undefined ? '' : exprToString(props.equals) || String(props.equals);
706
+ const returns = props.returns === undefined ? '' : exprToString(props.returns) || String(props.returns);
707
+ const fallback = props.fallback === undefined ? '' : exprToString(props.fallback) || String(props.fallback);
708
+ const recovers = props.recovers === undefined ? '' : String(props.recovers || 'true');
709
+ const contains = props.contains === undefined ? '' : String(props.contains);
710
+ const notContains = props.notContains === undefined ? '' : String(props.notContains);
554
711
  const matches = props.matches === undefined ? '' : String(props.matches);
712
+ const unmapped = props.unmapped === undefined ? '' : String(props.unmapped);
555
713
  const throws = props.throws === undefined ? '' : String(props.throws || 'true');
714
+ const called = props.called === undefined ? '' : String(props.called);
715
+ if (importAssertion) {
716
+ const parts = [`import${importSource ? ` ${importSource}` : ''}`];
717
+ if (contains)
718
+ parts.push(`contains ${contains}`);
719
+ if (notContains)
720
+ parts.push(`notContains ${notContains}`);
721
+ if (matches)
722
+ parts.push(`matches ${matches}`);
723
+ if (roundtrip)
724
+ parts.push('roundtrip');
725
+ if (unmapped)
726
+ parts.push(`unmapped ${unmapped}`);
727
+ if (no === 'unmapped')
728
+ parts.push('no unmapped');
729
+ return parts.join(' ');
730
+ }
731
+ if (decompileAssertion) {
732
+ const parts = ['decompile'];
733
+ if (contains)
734
+ parts.push(`contains ${contains}`);
735
+ if (notContains)
736
+ parts.push(`notContains ${notContains}`);
737
+ if (matches)
738
+ parts.push(`matches ${matches}`);
739
+ return parts.join(' ');
740
+ }
741
+ if (roundtrip)
742
+ return 'roundtrip';
743
+ if (codegen) {
744
+ const parts = ['codegen'];
745
+ if (contains)
746
+ parts.push(`contains ${contains}`);
747
+ if (notContains)
748
+ parts.push(`notContains ${notContains}`);
749
+ if (matches)
750
+ parts.push(`matches ${matches}`);
751
+ return parts.join(' ');
752
+ }
556
753
  if (preset)
557
754
  return `preset ${preset}`;
558
755
  if (nodeType) {
@@ -569,6 +766,9 @@ function assertionLabel(node) {
569
766
  }
570
767
  if (no)
571
768
  return `${machine ? `machine ${machine} ` : ''}no ${no}`;
769
+ if (has) {
770
+ return `${machine ? `machine ${machine} ` : ''}has ${has}${count ? ` count ${count}` : ''}${matches ? ` matches ${matches}` : ''}`;
771
+ }
572
772
  if (guard)
573
773
  return `guard ${guard} exhaustive`;
574
774
  if (machine && transition) {
@@ -605,6 +805,60 @@ function assertionLabel(node) {
605
805
  parts.push(`throws ${throws}`);
606
806
  return parts.join(' ');
607
807
  }
808
+ if (route) {
809
+ const parts = [`route ${route}`];
810
+ if (withValue)
811
+ parts.push(`with ${withValue}`);
812
+ if (input)
813
+ parts.push(`input ${input}`);
814
+ if (returns)
815
+ parts.push(`returns ${returns}`);
816
+ if (equals)
817
+ parts.push(`equals ${equals}`);
818
+ if (matches)
819
+ parts.push(`matches ${matches}`);
820
+ if (throws)
821
+ parts.push(`throws ${throws}`);
822
+ return parts.join(' ');
823
+ }
824
+ if (tool) {
825
+ const parts = [`tool ${tool}`];
826
+ if (withValue)
827
+ parts.push(`with ${withValue}`);
828
+ if (input)
829
+ parts.push(`input ${input}`);
830
+ if (returns)
831
+ parts.push(`returns ${returns}`);
832
+ if (equals)
833
+ parts.push(`equals ${equals}`);
834
+ if (matches)
835
+ parts.push(`matches ${matches}`);
836
+ if (throws)
837
+ parts.push(`throws ${throws}`);
838
+ return parts.join(' ');
839
+ }
840
+ if (effect) {
841
+ const parts = [`effect ${effect}`];
842
+ if (returns)
843
+ parts.push(`returns ${returns}`);
844
+ if (recovers)
845
+ parts.push(`recovers ${recovers}`);
846
+ if (fallback)
847
+ parts.push(`fallback ${fallback}`);
848
+ if (equals)
849
+ parts.push(`equals ${equals}`);
850
+ if (matches)
851
+ parts.push(`matches ${matches}`);
852
+ if (throws)
853
+ parts.push(`throws ${throws}`);
854
+ return parts.join(' ');
855
+ }
856
+ if (mock) {
857
+ const parts = [`mock ${mock}`];
858
+ if (called)
859
+ parts.push(`called ${called}`);
860
+ return parts.join(' ');
861
+ }
608
862
  if (expr && equals)
609
863
  return `expr ${expr} equals ${equals}`;
610
864
  if (expr && matches)
@@ -1189,6 +1443,15 @@ function assertionCoversAnyInvariant(node, invariants) {
1189
1443
  function syntheticTarget(root) {
1190
1444
  return { file: '<coverage>', root, diagnostics: [], schemaViolations: [], semanticViolations: [] };
1191
1445
  }
1446
+ function testNodeContributesCoverage(node) {
1447
+ const raw = getProps(node).coverage;
1448
+ if (raw === undefined || raw === '')
1449
+ return true;
1450
+ if (raw === false)
1451
+ return false;
1452
+ const value = String(raw).toLowerCase();
1453
+ return !['0', 'false', 'off', 'ignore', 'ignored', 'exclude', 'excluded', 'none'].includes(value);
1454
+ }
1192
1455
  function coveredTransitionsFromAssertion(root, assertion) {
1193
1456
  const props = getProps(assertion);
1194
1457
  const machineName = str(props.machine);
@@ -1244,12 +1507,9 @@ function findUntestedGuards(root, context) {
1244
1507
  const assertions = context?.assertions || [];
1245
1508
  if (assertions.some((assertion) => assertionCoversAnyInvariant(assertion.node, guardCoverageInvariants)))
1246
1509
  return [];
1247
- const explicitlyCovered = new Set(assertions.map((assertion) => str(getProps(assertion.node).guard)).filter(Boolean));
1510
+ const covered = coveredGuardKeys(root, assertions);
1248
1511
  return collectNodes(root, 'guard')
1249
- .filter((guard) => {
1250
- const name = str(getProps(guard).name);
1251
- return !name || !explicitlyCovered.has(name);
1252
- })
1512
+ .filter((guard) => !covered.has(runtimeGuardCoverageKey(guard)))
1253
1513
  .map((guard) => {
1254
1514
  const name = str(getProps(guard).name);
1255
1515
  return name
@@ -1257,6 +1517,153 @@ function findUntestedGuards(root, context) {
1257
1517
  : `unnamed guard at line ${guard.loc?.line ?? '?'}`;
1258
1518
  });
1259
1519
  }
1520
+ function coveredGuardKeys(root, assertions) {
1521
+ const target = syntheticTarget(root);
1522
+ const covered = new Set();
1523
+ for (const assertion of assertions) {
1524
+ const props = getProps(assertion.node);
1525
+ const guardName = str(props.guard);
1526
+ if (guardName) {
1527
+ for (const guard of collectNodes(root, 'guard')) {
1528
+ if (str(getProps(guard).name) === guardName)
1529
+ covered.add(runtimeGuardCoverageKey(guard));
1530
+ }
1531
+ continue;
1532
+ }
1533
+ const routeSpec = str(props.route);
1534
+ if (routeSpec) {
1535
+ const guardCalls = new Map();
1536
+ const evaluated = evaluateRuntimeRoute(assertion.node, target, assertion.fixtures, assertion.mocks, new Map(), undefined, guardCalls);
1537
+ if (evaluated.passed) {
1538
+ for (const [key, count] of guardCalls)
1539
+ if (count > 0)
1540
+ covered.add(key);
1541
+ }
1542
+ continue;
1543
+ }
1544
+ const toolName = str(props.tool);
1545
+ if (toolName) {
1546
+ const guardCalls = new Map();
1547
+ const evaluated = evaluateRuntimeTool(assertion.node, target, assertion.fixtures, assertion.mocks, new Map(), undefined, guardCalls);
1548
+ if (evaluated.passed) {
1549
+ for (const [key, count] of guardCalls)
1550
+ if (count > 0)
1551
+ covered.add(key);
1552
+ }
1553
+ }
1554
+ }
1555
+ return covered;
1556
+ }
1557
+ function coveredRouteLabels(root, assertions) {
1558
+ const target = syntheticTarget(root);
1559
+ const covered = new Set();
1560
+ for (const assertion of assertions) {
1561
+ const routeSpec = str(getProps(assertion.node).route);
1562
+ if (!routeSpec)
1563
+ continue;
1564
+ const found = findRuntimeRoute(target, routeSpec);
1565
+ if (!found.route)
1566
+ continue;
1567
+ const evaluated = evaluateRuntimeRoute(assertion.node, target, assertion.fixtures, assertion.mocks, new Map());
1568
+ if (evaluated.passed)
1569
+ covered.add(runtimeRouteLabel(found.route));
1570
+ }
1571
+ return covered;
1572
+ }
1573
+ function routeCoverage(root, assertions) {
1574
+ const covered = coveredRouteLabels(root, assertions);
1575
+ const routes = collectNodes(root, 'route');
1576
+ const uncovered = routes
1577
+ .filter((route) => !covered.has(runtimeRouteLabel(route)))
1578
+ .map((route) => `route ${runtimeRouteLabel(route)} at line ${route.loc?.line ?? '?'}`);
1579
+ return coverageMetric(routes.length, uncovered);
1580
+ }
1581
+ function findUntestedRoutes(root, context) {
1582
+ return routeCoverage(root, context?.assertions || []).uncovered;
1583
+ }
1584
+ function coveredToolNames(root, assertions) {
1585
+ const target = syntheticTarget(root);
1586
+ const covered = new Set();
1587
+ for (const assertion of assertions) {
1588
+ const toolName = str(getProps(assertion.node).tool);
1589
+ if (!toolName)
1590
+ continue;
1591
+ const found = findRuntimeTool(target, toolName);
1592
+ if (!found.tool)
1593
+ continue;
1594
+ const evaluated = evaluateRuntimeTool(assertion.node, target, assertion.fixtures, assertion.mocks, new Map());
1595
+ if (evaluated.passed)
1596
+ covered.add(runtimeToolName(found.tool));
1597
+ }
1598
+ return covered;
1599
+ }
1600
+ function toolCoverage(root, assertions) {
1601
+ const covered = coveredToolNames(root, assertions);
1602
+ const tools = collectNodes(root, 'tool');
1603
+ const uncovered = tools
1604
+ .filter((tool) => !covered.has(runtimeToolName(tool)))
1605
+ .map((tool) => `tool ${runtimeToolName(tool)} at line ${tool.loc?.line ?? '?'}`);
1606
+ return coverageMetric(tools.length, uncovered);
1607
+ }
1608
+ function findUntestedTools(root, context) {
1609
+ return toolCoverage(root, context?.assertions || []).uncovered;
1610
+ }
1611
+ function runtimeEffectNodes(root) {
1612
+ return collectNodes(root, 'effect').filter((effect) => {
1613
+ const name = str(getProps(effect).name);
1614
+ return Boolean(name && getChildren(effect, 'trigger').length > 0);
1615
+ });
1616
+ }
1617
+ function coveredEffectKeys(root, assertions) {
1618
+ const target = syntheticTarget(root);
1619
+ const covered = new Set();
1620
+ for (const assertion of assertions) {
1621
+ const props = getProps(assertion.node);
1622
+ const effectName = str(props.effect);
1623
+ if (effectName) {
1624
+ const found = findRuntimeEffect(target, effectName);
1625
+ if (!found.effect)
1626
+ continue;
1627
+ const evaluated = evaluateRuntimeEffect(assertion.node, target, assertion.fixtures, assertion.mocks, new Map());
1628
+ if (evaluated.passed)
1629
+ covered.add(runtimeEffectCoverageKey(found.effect));
1630
+ continue;
1631
+ }
1632
+ const routeSpec = str(props.route);
1633
+ if (routeSpec) {
1634
+ const effectCalls = new Map();
1635
+ const evaluated = evaluateRuntimeRoute(assertion.node, target, assertion.fixtures, assertion.mocks, new Map(), effectCalls);
1636
+ if (evaluated.passed) {
1637
+ for (const [key, count] of effectCalls)
1638
+ if (count > 0)
1639
+ covered.add(key);
1640
+ }
1641
+ continue;
1642
+ }
1643
+ const toolName = str(props.tool);
1644
+ if (toolName) {
1645
+ const effectCalls = new Map();
1646
+ const evaluated = evaluateRuntimeTool(assertion.node, target, assertion.fixtures, assertion.mocks, new Map(), effectCalls);
1647
+ if (evaluated.passed) {
1648
+ for (const [key, count] of effectCalls)
1649
+ if (count > 0)
1650
+ covered.add(key);
1651
+ }
1652
+ }
1653
+ }
1654
+ return covered;
1655
+ }
1656
+ function effectCoverage(root, assertions) {
1657
+ const covered = coveredEffectKeys(root, assertions);
1658
+ const effects = runtimeEffectNodes(root);
1659
+ const uncovered = effects
1660
+ .filter((effect) => !covered.has(runtimeEffectCoverageKey(effect)))
1661
+ .map((effect) => `effect ${runtimeEffectName(effect)} at line ${effect.loc?.line ?? '?'}`);
1662
+ return coverageMetric(effects.length, uncovered);
1663
+ }
1664
+ function findUntestedEffects(root, context) {
1665
+ return effectCoverage(root, context?.assertions || []).uncovered;
1666
+ }
1260
1667
  function coverageMetric(total, uncovered) {
1261
1668
  const covered = Math.max(0, total - uncovered.length);
1262
1669
  return {
@@ -1279,20 +1686,29 @@ function emptyCoverageSummary() {
1279
1686
  percent: 100,
1280
1687
  transitions: empty,
1281
1688
  guards: empty,
1689
+ routes: empty,
1690
+ tools: empty,
1691
+ effects: empty,
1282
1692
  targets: [],
1283
1693
  };
1284
1694
  }
1285
1695
  function combineCoverageSummaries(summaries) {
1286
1696
  const transitions = combineCoverageMetrics(summaries.map((summary) => summary.transitions));
1287
1697
  const guards = combineCoverageMetrics(summaries.map((summary) => summary.guards));
1288
- const total = transitions.total + guards.total;
1289
- const covered = transitions.covered + guards.covered;
1698
+ const routes = combineCoverageMetrics(summaries.map((summary) => summary.routes));
1699
+ const tools = combineCoverageMetrics(summaries.map((summary) => summary.tools));
1700
+ const effects = combineCoverageMetrics(summaries.map((summary) => summary.effects));
1701
+ const total = transitions.total + guards.total + routes.total + tools.total + effects.total;
1702
+ const covered = transitions.covered + guards.covered + routes.covered + tools.covered + effects.covered;
1290
1703
  return {
1291
1704
  total,
1292
1705
  covered,
1293
1706
  percent: total === 0 ? 100 : Math.round((covered / total) * 10000) / 100,
1294
1707
  transitions,
1295
1708
  guards,
1709
+ routes,
1710
+ tools,
1711
+ effects,
1296
1712
  targets: summaries.flatMap((summary) => summary.targets),
1297
1713
  };
1298
1714
  }
@@ -1338,12 +1754,9 @@ function guardCoverage(root, assertions) {
1338
1754
  if (assertions.some((assertion) => assertionCoversAnyInvariant(assertion.node, guardCoverageInvariants))) {
1339
1755
  return coverageMetric(guards.length, []);
1340
1756
  }
1341
- const explicitlyCovered = new Set(assertions.map((assertion) => str(getProps(assertion.node).guard)).filter(Boolean));
1757
+ const covered = coveredGuardKeys(root, assertions);
1342
1758
  const uncovered = guards
1343
- .filter((guard) => {
1344
- const name = str(getProps(guard).name);
1345
- return !name || !explicitlyCovered.has(name);
1346
- })
1759
+ .filter((guard) => !covered.has(runtimeGuardCoverageKey(guard)))
1347
1760
  .map((guard) => {
1348
1761
  const name = str(getProps(guard).name);
1349
1762
  return name
@@ -1358,25 +1771,37 @@ function coverageForTarget(target, assertions) {
1358
1771
  file: target.file,
1359
1772
  transitions: coverageMetric(0, []),
1360
1773
  guards: coverageMetric(0, []),
1774
+ routes: coverageMetric(0, []),
1775
+ tools: coverageMetric(0, []),
1776
+ effects: coverageMetric(0, []),
1361
1777
  };
1362
1778
  }
1363
1779
  return {
1364
1780
  file: target.file,
1365
1781
  transitions: machineTransitionCoverage(target.root, assertions),
1366
1782
  guards: guardCoverage(target.root, assertions),
1783
+ routes: routeCoverage(target.root, assertions),
1784
+ tools: toolCoverage(target.root, assertions),
1785
+ effects: effectCoverage(target.root, assertions),
1367
1786
  };
1368
1787
  }
1369
1788
  function createCoverageSummary(targets) {
1370
1789
  const transitions = combineCoverageMetrics(targets.map((target) => target.transitions));
1371
1790
  const guards = combineCoverageMetrics(targets.map((target) => target.guards));
1372
- const total = transitions.total + guards.total;
1373
- const covered = transitions.covered + guards.covered;
1791
+ const routes = combineCoverageMetrics(targets.map((target) => target.routes));
1792
+ const tools = combineCoverageMetrics(targets.map((target) => target.tools));
1793
+ const effects = combineCoverageMetrics(targets.map((target) => target.effects));
1794
+ const total = transitions.total + guards.total + routes.total + tools.total + effects.total;
1795
+ const covered = transitions.covered + guards.covered + routes.covered + tools.covered + effects.covered;
1374
1796
  return {
1375
1797
  total,
1376
1798
  covered,
1377
1799
  percent: total === 0 ? 100 : Math.round((covered / total) * 10000) / 100,
1378
1800
  transitions,
1379
1801
  guards,
1802
+ routes,
1803
+ tools,
1804
+ effects,
1380
1805
  targets,
1381
1806
  };
1382
1807
  }
@@ -1395,128 +1820,1801 @@ function findCodegenErrors(root) {
1395
1820
  }
1396
1821
  return failures;
1397
1822
  }
1398
- const RUNTIME_EXPR_TIMEOUT_MS = 100;
1399
- const RUNTIME_ASYNC_PROCESS_TIMEOUT_MS = 1500;
1400
- const RUNTIME_EXPR_UNSAFE_TOKEN = /\b(?:async|class|constructor|Date|delete|do|eval|fetch|for|Function|global|globalThis|import|new|process|prototype|require|setInterval|setTimeout|switch|this|throw|try|while|with|WebSocket|XMLHttpRequest|__proto__)\b/;
1401
- const RUNTIME_FN_UNSAFE_TOKEN = /\b(?:class|constructor|Date|delete|do|eval|fetch|Function|global|globalThis|import|process|prototype|require|setInterval|setTimeout|switch|this|while|with|WebSocket|XMLHttpRequest|__proto__)\b/;
1402
- function unsafeRuntimeExpressionReason(source, options = {}) {
1403
- if (source.length > 2000)
1404
- return 'expression is longer than 2000 characters';
1405
- if (/[\r\n;]/.test(source))
1406
- return 'multi-statement expressions are not supported';
1407
- const unsafeToken = source.match(RUNTIME_EXPR_UNSAFE_TOKEN)?.[0];
1408
- if (unsafeToken)
1409
- return `unsupported token '${unsafeToken}'`;
1410
- if (!options.allowAwait && /\bawait\b/.test(source))
1411
- return "unsupported token 'await'";
1412
- if (/(^|[^=!<>])=(?!=|>)/.test(source))
1413
- return 'assignment is not supported';
1414
- return undefined;
1415
- }
1416
- function unsafeRuntimeFunctionReason(source) {
1417
- if (source.length > 5000)
1418
- return 'function body is longer than 5000 characters';
1419
- const unsafeToken = source.match(RUNTIME_FN_UNSAFE_TOKEN)?.[0];
1420
- if (unsafeToken)
1421
- return `unsupported token '${unsafeToken}'`;
1422
- return undefined;
1423
- }
1424
- function isRuntimeBindingName(value) {
1425
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
1426
- }
1427
- function runtimeBindingSource(node) {
1428
- if (node.type === 'const')
1429
- return { expr: exprPropToRuntimeSource(node, 'value'), kind: 'expr' };
1430
- if (node.type === 'derive' || node.type === 'let') {
1431
- return { expr: exprPropToRuntimeSource(node, 'value') || exprPropToRuntimeSource(node, 'expr'), kind: 'expr' };
1432
- }
1433
- if (node.type === 'fn')
1434
- return { expr: runtimeFunctionExpr(node), kind: 'fn' };
1435
- return undefined;
1436
- }
1437
- function runtimeParamNames(node) {
1438
- const names = [];
1439
- for (const param of getChildren(node, 'param')) {
1440
- const name = str(getProps(param).name);
1441
- if (name)
1442
- names.push(name);
1823
+ function generatedCoreSource(root) {
1824
+ const chunks = [];
1825
+ for (const node of codegenRoots(root)) {
1826
+ try {
1827
+ chunks.push(...generateCoreNode(node));
1828
+ }
1829
+ catch (error) {
1830
+ return {
1831
+ message: `${nodeLabel(node)} at line ${node.loc?.line ?? '?'}: ${error instanceof Error ? error.message : String(error)}`,
1832
+ };
1833
+ }
1443
1834
  }
1444
- if (names.length > 0)
1445
- return names;
1446
- return parseLegacyParamNames(str(getProps(node).params));
1447
- }
1448
- function runtimeFunctionExpr(node) {
1449
- const code = handlerText(node);
1450
- if (!code)
1451
- return '';
1452
- const params = runtimeParamNames(node);
1453
- if (!params.every(isRuntimeBindingName))
1454
- return '';
1455
- const asyncKw = isTruthy(getProps(node).async) ? 'async ' : '';
1456
- return `(${asyncKw}(${params.join(', ')}) => {\n${code.trim()}\n})`;
1835
+ return { code: chunks.join('\n') };
1457
1836
  }
1458
- function collectRuntimeBindings(root) {
1459
- const bindings = [];
1460
- function visit(node) {
1461
- if (node.type === 'const' || node.type === 'derive' || node.type === 'let' || node.type === 'fn') {
1462
- const name = str(getProps(node).name);
1463
- const binding = runtimeBindingSource(node);
1464
- if (name && binding?.expr) {
1465
- bindings.push({
1466
- name,
1467
- expr: binding.expr,
1468
- kind: binding.kind,
1469
- line: node.loc?.line,
1470
- });
1471
- }
1837
+ function decompiledCoreSource(root) {
1838
+ const chunks = [];
1839
+ for (const node of codegenRoots(root)) {
1840
+ try {
1841
+ chunks.push(decompile(node).code);
1842
+ }
1843
+ catch (error) {
1844
+ return {
1845
+ message: `${nodeLabel(node)} at line ${node.loc?.line ?? '?'}: ${error instanceof Error ? error.message : String(error)}`,
1846
+ };
1472
1847
  }
1473
- for (const child of node.children || [])
1474
- visit(child);
1475
1848
  }
1476
- visit(root);
1477
- return bindings;
1849
+ return { code: chunks.filter(Boolean).join('\n') };
1478
1850
  }
1479
- function orderRuntimeBindings(bindings, entryExpr) {
1480
- const byName = new Map();
1481
- for (const binding of bindings) {
1482
- if (!isRuntimeBindingName(binding.name)) {
1483
- return { ordered: [], error: `invalid runtime binding name '${binding.name}' at line ${binding.line ?? '?'}` };
1484
- }
1485
- byName.set(binding.name, [...(byName.get(binding.name) || []), binding]);
1851
+ function evaluateSourceTextAssertion(node, source, label) {
1852
+ const props = getProps(node);
1853
+ const contains = str(props.contains);
1854
+ const notContains = str(props.notContains);
1855
+ const matches = str(props.matches);
1856
+ if (!contains && !notContains && !matches) {
1857
+ return { passed: false, message: `${label} assertion requires contains=, notContains=, or matches=` };
1486
1858
  }
1487
- const ordered = [];
1488
- const visiting = new Set();
1489
- const visited = new Set();
1490
- const stack = [];
1491
- function depsIn(source) {
1492
- return [...byName.keys()].filter((name) => new RegExp(`\\b${escapeRegExp(name)}\\b`).test(source));
1859
+ if (contains && !source.includes(contains)) {
1860
+ return { passed: false, message: `${label} does not contain ${JSON.stringify(contains)}` };
1493
1861
  }
1494
- function bindingFor(name) {
1495
- const candidates = byName.get(name) || [];
1496
- if (candidates.length <= 1)
1497
- return candidates[0];
1498
- const [first, ...rest] = candidates;
1499
- throw new Error(`duplicate runtime binding '${name}' at line ${rest[0].line ?? '?'} (first at line ${first.line ?? '?'})`);
1862
+ if (notContains && source.includes(notContains)) {
1863
+ return { passed: false, message: `${label} unexpectedly contains ${JSON.stringify(notContains)}` };
1500
1864
  }
1501
- function visit(name) {
1502
- if (visited.has(name))
1503
- return undefined;
1504
- if (visiting.has(name)) {
1505
- const start = stack.indexOf(name);
1506
- return `runtime binding cycle: ${[...stack.slice(start), name].join(' -> ')}`;
1507
- }
1508
- let binding;
1865
+ if (matches) {
1509
1866
  try {
1510
- binding = bindingFor(name);
1867
+ if (!new RegExp(matches).test(source))
1868
+ return { passed: false, message: `${label} does not match /${matches}/` };
1511
1869
  }
1512
- catch (error) {
1513
- return error instanceof Error ? error.message : String(error);
1870
+ catch {
1871
+ return { passed: false, message: `Invalid ${label.toLowerCase()} matches regex: ${matches}` };
1514
1872
  }
1515
- if (!binding)
1516
- return undefined;
1517
- visiting.add(name);
1518
- stack.push(name);
1873
+ }
1874
+ return { passed: true };
1875
+ }
1876
+ function evaluateCodegenAssertion(node, target) {
1877
+ const blocking = targetBlockingMessage(target);
1878
+ if (blocking)
1879
+ return { passed: false, message: blocking };
1880
+ if (!target.root)
1881
+ return { passed: false, message: 'Target has no parsed KERN root' };
1882
+ const generated = generatedCoreSource(target.root);
1883
+ if (generated.message || generated.code === undefined) {
1884
+ return { passed: false, message: `Target has codegen error: ${generated.message || 'unknown error'}` };
1885
+ }
1886
+ return evaluateSourceTextAssertion(node, generated.code, 'Generated code');
1887
+ }
1888
+ function evaluateDecompileAssertion(node, target) {
1889
+ const blocking = targetBlockingMessage(target);
1890
+ if (blocking)
1891
+ return { passed: false, message: blocking };
1892
+ if (!target.root)
1893
+ return { passed: false, message: 'Target has no parsed KERN root' };
1894
+ const decompiled = decompiledCoreSource(target.root);
1895
+ if (decompiled.message || decompiled.code === undefined) {
1896
+ return { passed: false, message: `Target has decompile error: ${decompiled.message || 'unknown error'}` };
1897
+ }
1898
+ return evaluateSourceTextAssertion(node, decompiled.code, 'Decompiled KERN');
1899
+ }
1900
+ function evaluateRoundtripAssertion(_node, target) {
1901
+ const blocking = targetBlockingMessage(target);
1902
+ if (blocking)
1903
+ return { passed: false, message: blocking };
1904
+ if (!target.root)
1905
+ return { passed: false, message: 'Target has no parsed KERN root' };
1906
+ const decompiled = decompiledCoreSource(target.root);
1907
+ if (decompiled.message || decompiled.code === undefined) {
1908
+ return { passed: false, message: `Target has decompile error: ${decompiled.message || 'unknown error'}` };
1909
+ }
1910
+ const reparsed = parseDocumentWithDiagnostics(decompiled.code);
1911
+ const parseError = reparsed.diagnostics.find((diagnostic) => diagnostic.severity === 'error');
1912
+ if (parseError) {
1913
+ return {
1914
+ passed: false,
1915
+ message: `Decompiled KERN does not reparse at ${parseError.line}:${parseError.col}: ${parseError.message}`,
1916
+ };
1917
+ }
1918
+ const schemaViolation = validateSchema(reparsed.root)[0];
1919
+ if (schemaViolation) {
1920
+ return {
1921
+ passed: false,
1922
+ message: `Decompiled KERN has schema violation at ${schemaViolation.line ?? 1}:${schemaViolation.col ?? 1}: ${schemaViolation.message}`,
1923
+ };
1924
+ }
1925
+ const semanticViolation = validateSemantics(reparsed.root).filter((violation) => !isMultiSourceTransitionFalsePositive(violation, reparsed.root))[0];
1926
+ if (semanticViolation) {
1927
+ return {
1928
+ passed: false,
1929
+ message: `Decompiled KERN has semantic violation at ${semanticViolation.line ?? 1}:${semanticViolation.col ?? 1}: ${semanticViolation.message}`,
1930
+ };
1931
+ }
1932
+ const originalGenerated = generatedCoreSource(target.root);
1933
+ if (originalGenerated.message || originalGenerated.code === undefined) {
1934
+ return { passed: false, message: `Target has codegen error: ${originalGenerated.message || 'unknown error'}` };
1935
+ }
1936
+ const roundtripGenerated = generatedCoreSource(reparsed.root);
1937
+ if (roundtripGenerated.message || roundtripGenerated.code === undefined) {
1938
+ return {
1939
+ passed: false,
1940
+ message: `Decompiled KERN has codegen error: ${roundtripGenerated.message || 'unknown error'}`,
1941
+ };
1942
+ }
1943
+ if (originalGenerated.code !== roundtripGenerated.code) {
1944
+ return { passed: false, message: 'Round-trip changed generated core code' };
1945
+ }
1946
+ return { passed: true };
1947
+ }
1948
+ function importAssertionSourcePath(node) {
1949
+ const props = getProps(node);
1950
+ const importValue = str(props.import);
1951
+ if (importValue && importValue !== 'true')
1952
+ return importValue;
1953
+ return str(props.from) || '';
1954
+ }
1955
+ function resolveImportAssertionSource(node, context) {
1956
+ const props = getProps(node);
1957
+ const inlineSource = str(props.source);
1958
+ if (inlineSource)
1959
+ return { source: inlineSource, fileName: 'inline.ts' };
1960
+ const sourcePath = importAssertionSourcePath(node);
1961
+ if (!sourcePath)
1962
+ return { message: 'Import assertion requires import=<ts-file> or from=<ts-file>' };
1963
+ const baseDir = context?.testFile ? dirname(context.testFile) : process.cwd();
1964
+ const fileName = resolve(baseDir, sourcePath);
1965
+ try {
1966
+ return { source: readFileSync(fileName, 'utf-8'), fileName };
1967
+ }
1968
+ catch (error) {
1969
+ return {
1970
+ message: `Could not read import source ${sourcePath}: ${error instanceof Error ? error.message : String(error)}`,
1971
+ };
1972
+ }
1973
+ }
1974
+ function evaluateImportedKernRoundtrip(kern, options = {}) {
1975
+ const reparsed = parseDocumentWithDiagnostics(kern);
1976
+ const parseError = reparsed.diagnostics.find((diagnostic) => diagnostic.severity === 'error');
1977
+ if (parseError) {
1978
+ return {
1979
+ passed: false,
1980
+ message: `Imported KERN does not reparse at ${parseError.line}:${parseError.col}: ${parseError.message}`,
1981
+ };
1982
+ }
1983
+ const parseWarning = reparsed.diagnostics.find((diagnostic) => diagnostic.severity === 'warning');
1984
+ if (parseWarning && !options.allowWarnings) {
1985
+ return {
1986
+ passed: false,
1987
+ message: `Imported KERN has parser warning at ${parseWarning.line}:${parseWarning.col}: ${parseWarning.message}`,
1988
+ };
1989
+ }
1990
+ const schemaViolation = validateSchema(reparsed.root)[0];
1991
+ if (schemaViolation) {
1992
+ return {
1993
+ passed: false,
1994
+ message: `Imported KERN has schema violation at ${schemaViolation.line ?? 1}:${schemaViolation.col ?? 1}: ${schemaViolation.message}`,
1995
+ };
1996
+ }
1997
+ const semanticViolation = validateSemantics(reparsed.root).filter((violation) => !isMultiSourceTransitionFalsePositive(violation, reparsed.root))[0];
1998
+ if (semanticViolation) {
1999
+ return {
2000
+ passed: false,
2001
+ message: `Imported KERN has semantic violation at ${semanticViolation.line ?? 1}:${semanticViolation.col ?? 1}: ${semanticViolation.message}`,
2002
+ };
2003
+ }
2004
+ const generated = generatedCoreSource(reparsed.root);
2005
+ if (generated.message || generated.code === undefined) {
2006
+ return { passed: false, message: `Imported KERN has codegen error: ${generated.message || 'unknown error'}` };
2007
+ }
2008
+ return { passed: true };
2009
+ }
2010
+ function formatImportUnmapped(unmapped) {
2011
+ if (unmapped.length === 0)
2012
+ return 'no unmapped TypeScript statements';
2013
+ const shown = unmapped.slice(0, 5).join('; ');
2014
+ const suffix = unmapped.length > 5 ? `; +${unmapped.length - 5} more` : '';
2015
+ return `${unmapped.length} unmapped TypeScript statement(s): ${shown}${suffix}`;
2016
+ }
2017
+ function evaluateImportAssertion(node, context) {
2018
+ const props = getProps(node);
2019
+ const hasTextAssertion = 'contains' in props || 'notContains' in props || 'matches' in props;
2020
+ const hasRoundtripAssertion = 'roundtrip' in props;
2021
+ const hasUnmappedAssertion = 'unmapped' in props || str(props.no) === 'unmapped';
2022
+ if (!hasTextAssertion && !hasRoundtripAssertion && !hasUnmappedAssertion) {
2023
+ return {
2024
+ passed: false,
2025
+ message: 'Import assertion requires contains=, notContains=, matches=, roundtrip=true, unmapped=<count>, or no=unmapped',
2026
+ };
2027
+ }
2028
+ const resolved = resolveImportAssertionSource(node, context);
2029
+ if (resolved.message || resolved.source === undefined) {
2030
+ return { passed: false, message: resolved.message || 'Could not resolve import source' };
2031
+ }
2032
+ let imported;
2033
+ try {
2034
+ imported = importTypeScript(resolved.source, resolved.fileName || 'input.ts');
2035
+ }
2036
+ catch (error) {
2037
+ return {
2038
+ passed: false,
2039
+ message: `TypeScript import failed: ${error instanceof Error ? error.message : String(error)}`,
2040
+ };
2041
+ }
2042
+ if (str(props.no) === 'unmapped' && imported.unmapped.length > 0) {
2043
+ return {
2044
+ passed: false,
2045
+ message: `Import expected no unmapped TypeScript, found ${formatImportUnmapped(imported.unmapped)}`,
2046
+ };
2047
+ }
2048
+ if ('unmapped' in props) {
2049
+ const expected = Number(props.unmapped);
2050
+ if (!Number.isInteger(expected) || expected < 0) {
2051
+ return { passed: false, message: 'Import assertion requires unmapped=<non-negative integer>' };
2052
+ }
2053
+ if (imported.unmapped.length !== expected) {
2054
+ return {
2055
+ passed: false,
2056
+ message: `Import expected ${expected} unmapped TypeScript statement(s), found ${formatImportUnmapped(imported.unmapped)}`,
2057
+ };
2058
+ }
2059
+ }
2060
+ if (hasTextAssertion) {
2061
+ const sourceAssertion = evaluateSourceTextAssertion(node, imported.kern, 'Imported KERN');
2062
+ if (!sourceAssertion.passed)
2063
+ return sourceAssertion;
2064
+ }
2065
+ if (hasRoundtripAssertion) {
2066
+ const roundtrip = evaluateImportedKernRoundtrip(imported.kern, { allowWarnings: isTruthy(props.allowWarnings) });
2067
+ if (!roundtrip.passed)
2068
+ return roundtrip;
2069
+ }
2070
+ return { passed: true };
2071
+ }
2072
+ const RUNTIME_EXPR_TIMEOUT_MS = 100;
2073
+ const RUNTIME_ASYNC_PROCESS_TIMEOUT_MS = 1500;
2074
+ const RUNTIME_EXPR_UNSAFE_TOKEN = /\b(?:async|class|constructor|Date|delete|do|eval|fetch|for|Function|global|globalThis|import|process|prototype|require|setInterval|setTimeout|switch|this|throw|try|while|with|WebSocket|XMLHttpRequest|__proto__)\b/;
2075
+ const RUNTIME_FN_UNSAFE_TOKEN = /\b(?:class|constructor|Date|delete|do|eval|fetch|Function|global|globalThis|import|process|prototype|require|setInterval|setTimeout|switch|this|with|WebSocket|XMLHttpRequest|__proto__)\b/;
2076
+ const RUNTIME_CLASS_UNSAFE_TOKEN = /\b(?:Date|delete|do|eval|fetch|Function|global|globalThis|import|process|prototype|require|setInterval|setTimeout|switch|with|WebSocket|XMLHttpRequest|__proto__)\b/;
2077
+ const RUNTIME_NATIVE_BINDING_UNSAFE_TOKEN = /\b(?:class|constructor|Date|delete|do|eval|fetch|Function|global|globalThis|import|process|prototype|require|setInterval|setTimeout|switch|this|throw|try|while|with|WebSocket|XMLHttpRequest|__proto__)\b/;
2078
+ const RUNTIME_WORKFLOW_BINDING_UNSAFE_TOKEN = /\b(?:class|constructor|Date|delete|do|eval|fetch|Function|global|globalThis|import|process|prototype|require|setInterval|setTimeout|switch|this|while|with|WebSocket|XMLHttpRequest|__proto__)\b/;
2079
+ function unsafeRuntimeExpressionReason(source, options = {}) {
2080
+ if (source.length > 2000)
2081
+ return 'expression is longer than 2000 characters';
2082
+ if (/[\r\n;]/.test(source))
2083
+ return 'multi-statement expressions are not supported';
2084
+ const unsafeToken = source.match(RUNTIME_EXPR_UNSAFE_TOKEN)?.[0];
2085
+ if (unsafeToken)
2086
+ return `unsupported token '${unsafeToken}'`;
2087
+ if (!options.allowAwait && /\bawait\b/.test(source))
2088
+ return "unsupported token 'await'";
2089
+ if (/(^|[^=!<>])=(?!=|>)/.test(source))
2090
+ return 'assignment is not supported';
2091
+ return undefined;
2092
+ }
2093
+ function unsafeRuntimeFunctionReason(source) {
2094
+ if (source.length > 5000)
2095
+ return 'function body is longer than 5000 characters';
2096
+ const unsafeToken = source.match(RUNTIME_FN_UNSAFE_TOKEN)?.[0];
2097
+ if (unsafeToken)
2098
+ return `unsupported token '${unsafeToken}'`;
2099
+ return undefined;
2100
+ }
2101
+ function unsafeRuntimeClassReason(source) {
2102
+ if (source.length > 10000)
2103
+ return 'class body is longer than 10000 characters';
2104
+ const unsafeToken = source.match(RUNTIME_CLASS_UNSAFE_TOKEN)?.[0];
2105
+ if (unsafeToken)
2106
+ return `unsupported token '${unsafeToken}'`;
2107
+ return undefined;
2108
+ }
2109
+ function unsafeRuntimeNativeBindingReason(source) {
2110
+ if (source.length > 5000)
2111
+ return 'native binding expression is longer than 5000 characters';
2112
+ const unsafeToken = source.match(RUNTIME_NATIVE_BINDING_UNSAFE_TOKEN)?.[0];
2113
+ if (unsafeToken)
2114
+ return `unsupported token '${unsafeToken}'`;
2115
+ return undefined;
2116
+ }
2117
+ function unsafeRuntimeWorkflowReason(source) {
2118
+ if (source.length > 10000)
2119
+ return 'workflow expression is longer than 10000 characters';
2120
+ const unsafeToken = source.match(RUNTIME_WORKFLOW_BINDING_UNSAFE_TOKEN)?.[0];
2121
+ if (unsafeToken)
2122
+ return `unsupported token '${unsafeToken}'`;
2123
+ return undefined;
2124
+ }
2125
+ function isRuntimeBindingName(value) {
2126
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
2127
+ }
2128
+ function transformRuntimeCodeSegments(source, transform) {
2129
+ let output = '';
2130
+ let segmentStart = 0;
2131
+ let index = 0;
2132
+ function flush(until) {
2133
+ if (until > segmentStart)
2134
+ output += transform(source.slice(segmentStart, until));
2135
+ }
2136
+ while (index < source.length) {
2137
+ const char = source[index];
2138
+ const next = source[index + 1];
2139
+ if (char === '/' && next === '/') {
2140
+ flush(index);
2141
+ const end = source.indexOf('\n', index + 2);
2142
+ const commentEnd = end === -1 ? source.length : end;
2143
+ output += source.slice(index, commentEnd);
2144
+ index = commentEnd;
2145
+ segmentStart = index;
2146
+ continue;
2147
+ }
2148
+ if (char === '/' && next === '*') {
2149
+ flush(index);
2150
+ const end = source.indexOf('*/', index + 2);
2151
+ const commentEnd = end === -1 ? source.length : end + 2;
2152
+ output += source.slice(index, commentEnd);
2153
+ index = commentEnd;
2154
+ segmentStart = index;
2155
+ continue;
2156
+ }
2157
+ if (char === '"' || char === "'" || char === '`') {
2158
+ flush(index);
2159
+ const quote = char;
2160
+ let end = index + 1;
2161
+ while (end < source.length) {
2162
+ if (source[end] === '\\') {
2163
+ end += 2;
2164
+ continue;
2165
+ }
2166
+ if (source[end] === quote) {
2167
+ end += 1;
2168
+ break;
2169
+ }
2170
+ end += 1;
2171
+ }
2172
+ output += source.slice(index, end);
2173
+ index = end;
2174
+ segmentStart = index;
2175
+ continue;
2176
+ }
2177
+ index += 1;
2178
+ }
2179
+ flush(source.length);
2180
+ return output;
2181
+ }
2182
+ function runtimeJsSource(source) {
2183
+ return transformRuntimeCodeSegments(source, (segment) => segment
2184
+ .replace(/\s+as\s+(?:const|any|unknown|never|[A-Za-z_$][\w$]*(?:<[^;\n(){}=]*>)?(?:\[\])?(?:\s*\|\s*[A-Za-z_$][\w$]*(?:<[^;\n(){}=]*>)?(?:\[\])?)*)/g, '')
2185
+ .replace(/\b(const|let|var)\s+([A-Za-z_$][\w$]*)\s*:\s*[^=;\n]+(?=\s*=)/g, '$1 $2')
2186
+ .replace(/([,(]\s*[A-Za-z_$][\w$]*)\s*:\s*[^,)]+(?=\s*[,)]\s*=>)/g, '$1')
2187
+ .replace(/\bfunction\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*:\s*[^{]+{/g, 'function $1($2) {'));
2188
+ }
2189
+ function runtimeConstHandlerExpr(node) {
2190
+ const code = runtimeJsSource(handlerText(node).trim());
2191
+ if (!code)
2192
+ return '';
2193
+ if (/^\s*(?:return|const|let|var|if|for|while|try|throw)\b/.test(code) || /;\s*$/.test(code)) {
2194
+ return `(() => {\n${code}\n})()`;
2195
+ }
2196
+ return `(${code})`;
2197
+ }
2198
+ function runtimeBindingSource(node) {
2199
+ if (node.type === 'const') {
2200
+ const value = exprPropToRuntimeSource(node, 'value');
2201
+ if (value)
2202
+ return { expr: runtimeJsSource(value), kind: 'expr' };
2203
+ const handlerExpr = runtimeConstHandlerExpr(node);
2204
+ if (handlerExpr)
2205
+ return { expr: handlerExpr, kind: 'native' };
2206
+ return { expr: '', kind: 'expr' };
2207
+ }
2208
+ if (node.type === 'derive' || node.type === 'let') {
2209
+ return {
2210
+ expr: runtimeJsSource(exprPropToRuntimeSource(node, 'value') || exprPropToRuntimeSource(node, 'expr')),
2211
+ kind: 'expr',
2212
+ };
2213
+ }
2214
+ if (node.type === 'fn')
2215
+ return { expr: runtimeFunctionExpr(node), kind: 'fn' };
2216
+ if (node.type === 'class')
2217
+ return { expr: runtimeClassExpr(node), kind: 'class' };
2218
+ if (node.type === 'mapLit')
2219
+ return { expr: runtimeMapLitExpr(node), kind: 'native' };
2220
+ if (node.type === 'setLit')
2221
+ return { expr: runtimeSetLitExpr(node), kind: 'native' };
2222
+ if (node.type === 'collect')
2223
+ return { expr: runtimeCollectBindingExpr(node), kind: 'native' };
2224
+ const arrayExpr = runtimeArrayBindingExpr(node);
2225
+ if (arrayExpr !== undefined)
2226
+ return { expr: arrayExpr, kind: 'native' };
2227
+ return undefined;
2228
+ }
2229
+ function runtimeParamNames(node) {
2230
+ const names = [];
2231
+ for (const param of getChildren(node, 'param')) {
2232
+ const name = str(getProps(param).name);
2233
+ if (name)
2234
+ names.push(name);
2235
+ }
2236
+ if (names.length > 0)
2237
+ return names;
2238
+ return parseLegacyParamNames(str(getProps(node).params));
2239
+ }
2240
+ function runtimeFunctionExpr(node) {
2241
+ const code = runtimeJsSource(handlerText(node));
2242
+ if (!code)
2243
+ return '';
2244
+ const params = runtimeParamNames(node);
2245
+ if (!params.every(isRuntimeBindingName))
2246
+ return '';
2247
+ const asyncKw = isTruthy(getProps(node).async) ? 'async ' : '';
2248
+ return `(${asyncKw}(${params.join(', ')}) => {\n${code.trim()}\n})`;
2249
+ }
2250
+ function runtimeHandlerLines(node, spaces = 4) {
2251
+ const prefix = ' '.repeat(spaces);
2252
+ const code = runtimeJsSource(handlerText(node).trim());
2253
+ if (!code)
2254
+ return [];
2255
+ return code.split('\n').map((line) => `${prefix}${line}`);
2256
+ }
2257
+ function runtimeClassFieldInitializers(node) {
2258
+ const lines = [];
2259
+ for (const field of getChildren(node, 'field')) {
2260
+ const props = getProps(field);
2261
+ if (isTruthy(props.static))
2262
+ continue;
2263
+ const name = str(props.name);
2264
+ if (!isRuntimeBindingName(name))
2265
+ return [];
2266
+ const value = exprPropToRuntimeSource(field, 'value') || rawPropToRuntimeSource(field, 'default');
2267
+ if (value)
2268
+ lines.push(` this.${name} = (${value});`);
2269
+ }
2270
+ return lines;
2271
+ }
2272
+ function runtimeClassMethodLines(node) {
2273
+ const props = getProps(node);
2274
+ const name = str(props.name);
2275
+ if (!isRuntimeBindingName(name))
2276
+ return undefined;
2277
+ const params = runtimeParamNames(node);
2278
+ if (!params.every(isRuntimeBindingName))
2279
+ return undefined;
2280
+ const staticKw = isTruthy(props.static) ? 'static ' : '';
2281
+ const asyncKw = isTruthy(props.async) || isTruthy(props.stream) ? 'async ' : '';
2282
+ const star = isTruthy(props.stream) ? '*' : '';
2283
+ const lines = [` ${staticKw}${asyncKw}${star}${name}(${params.join(', ')}) {`];
2284
+ lines.push(...runtimeHandlerLines(node));
2285
+ lines.push(' }');
2286
+ return lines;
2287
+ }
2288
+ function runtimeClassGetterLines(node) {
2289
+ const props = getProps(node);
2290
+ const name = str(props.name);
2291
+ if (!isRuntimeBindingName(name))
2292
+ return undefined;
2293
+ const staticKw = isTruthy(props.static) ? 'static ' : '';
2294
+ const lines = [` ${staticKw}get ${name}() {`];
2295
+ lines.push(...runtimeHandlerLines(node));
2296
+ lines.push(' }');
2297
+ return lines;
2298
+ }
2299
+ function runtimeClassSetterLines(node) {
2300
+ const props = getProps(node);
2301
+ const name = str(props.name);
2302
+ if (!isRuntimeBindingName(name))
2303
+ return undefined;
2304
+ const params = runtimeParamNames(node);
2305
+ if (!params.every(isRuntimeBindingName))
2306
+ return undefined;
2307
+ const param = params[0] || 'value';
2308
+ const staticKw = isTruthy(props.static) ? 'static ' : '';
2309
+ const lines = [` ${staticKw}set ${name}(${param}) {`];
2310
+ lines.push(...runtimeHandlerLines(node));
2311
+ lines.push(' }');
2312
+ return lines;
2313
+ }
2314
+ function runtimeClassExpr(node) {
2315
+ const name = str(getProps(node).name);
2316
+ if (!isRuntimeBindingName(name))
2317
+ return '';
2318
+ const ctorNode = getChildren(node, 'constructor')[0];
2319
+ const ctorParams = ctorNode ? runtimeParamNames(ctorNode) : [];
2320
+ if (!ctorParams.every(isRuntimeBindingName))
2321
+ return '';
2322
+ const fieldInitializers = runtimeClassFieldInitializers(node);
2323
+ const lines = ['(class {'];
2324
+ if (ctorNode || fieldInitializers.length > 0) {
2325
+ lines.push(` constructor(${ctorParams.join(', ')}) {`);
2326
+ lines.push(...fieldInitializers);
2327
+ if (ctorNode)
2328
+ lines.push(...runtimeHandlerLines(ctorNode));
2329
+ lines.push(' }');
2330
+ }
2331
+ for (const method of getChildren(node, 'method')) {
2332
+ const methodLines = runtimeClassMethodLines(method);
2333
+ if (!methodLines)
2334
+ return '';
2335
+ lines.push(...methodLines);
2336
+ }
2337
+ for (const getter of getChildren(node, 'getter')) {
2338
+ const getterLines = runtimeClassGetterLines(getter);
2339
+ if (!getterLines)
2340
+ return '';
2341
+ lines.push(...getterLines);
2342
+ }
2343
+ for (const setter of getChildren(node, 'setter')) {
2344
+ const setterLines = runtimeClassSetterLines(setter);
2345
+ if (!setterLines)
2346
+ return '';
2347
+ lines.push(...setterLines);
2348
+ }
2349
+ lines.push('})');
2350
+ return lines.join('\n');
2351
+ }
2352
+ function runtimeValuePropSource(node, propName) {
2353
+ return exprPropToRuntimeSource(node, propName) || rawPropToRuntimeSource(node, propName);
2354
+ }
2355
+ function runtimePortableSource(source) {
2356
+ return source.replace(/\b([A-Za-z_$][A-Za-z0-9_$]*)\.result\b/g, '$1');
2357
+ }
2358
+ function runtimePortableValuePropSource(node, propName) {
2359
+ const source = runtimeValuePropSource(node, propName);
2360
+ return source ? runtimePortableSource(source) : '';
2361
+ }
2362
+ function runtimeMapLitExpr(node) {
2363
+ const entries = [];
2364
+ for (const entry of getChildren(node, 'mapEntry')) {
2365
+ const key = runtimeValuePropSource(entry, 'key');
2366
+ const value = runtimeValuePropSource(entry, 'value');
2367
+ if (!key || !value)
2368
+ return '';
2369
+ entries.push(`[${key}, ${value}]`);
2370
+ }
2371
+ return `new Map([${entries.join(', ')}])`;
2372
+ }
2373
+ function runtimeSetLitExpr(node) {
2374
+ const items = [];
2375
+ for (const item of getChildren(node, 'setItem')) {
2376
+ const value = runtimeValuePropSource(item, 'value');
2377
+ if (!value)
2378
+ return '';
2379
+ items.push(value);
2380
+ }
2381
+ return `new Set([${items.join(', ')}])`;
2382
+ }
2383
+ function runtimeSyntheticName(node, prefix) {
2384
+ const line = node.loc?.line ?? 0;
2385
+ const col = node.loc?.col ?? 0;
2386
+ return `__kern${prefix}_${line}_${col}`;
2387
+ }
2388
+ function runtimeDestructureBindings(node) {
2389
+ const source = rawPropToRuntimeSource(node, 'source');
2390
+ if (!source)
2391
+ return [];
2392
+ const bindings = [];
2393
+ for (const child of node.children || []) {
2394
+ const props = getProps(child);
2395
+ const name = str(props.name);
2396
+ if (!isRuntimeBindingName(name))
2397
+ continue;
2398
+ if (child.type === 'element') {
2399
+ const rawIndex = str(props.index);
2400
+ const index = rawIndex && /^-?\d+$/.test(rawIndex) ? rawIndex : String(bindings.length);
2401
+ bindings.push({
2402
+ name,
2403
+ expr: `((${source})[${index}])`,
2404
+ kind: 'native',
2405
+ line: child.loc?.line,
2406
+ });
2407
+ }
2408
+ if (child.type === 'binding') {
2409
+ const key = str(props.key) || name;
2410
+ const accessor = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
2411
+ bindings.push({
2412
+ name,
2413
+ expr: `((${source})${accessor})`,
2414
+ kind: 'native',
2415
+ line: child.loc?.line,
2416
+ });
2417
+ }
2418
+ }
2419
+ return bindings;
2420
+ }
2421
+ function runtimeNamedProp(node, propName, fallback) {
2422
+ const value = str(getProps(node)[propName]);
2423
+ return isRuntimeBindingName(value) ? value : fallback;
2424
+ }
2425
+ function runtimeArrayPredicateBindingExpr(node, method) {
2426
+ const collection = rawPropToRuntimeSource(node, 'in');
2427
+ const predicate = rawPropToRuntimeSource(node, 'where');
2428
+ if (!collection || !predicate)
2429
+ return '';
2430
+ const item = runtimeNamedProp(node, 'item', 'item');
2431
+ return `((${collection}).${method}((${item}) => ${predicate}))`;
2432
+ }
2433
+ function runtimeArrayProjectionBindingExpr(node, method) {
2434
+ const collection = rawPropToRuntimeSource(node, 'in');
2435
+ const body = rawPropToRuntimeSource(node, 'expr');
2436
+ if (!collection || !body)
2437
+ return '';
2438
+ const item = runtimeNamedProp(node, 'item', 'item');
2439
+ return `((${collection}).${method}((${item}) => ${body}))`;
2440
+ }
2441
+ function runtimeArrayValueLookupBindingExpr(node, method) {
2442
+ const collection = rawPropToRuntimeSource(node, 'in');
2443
+ const value = rawPropToRuntimeSource(node, 'value');
2444
+ if (!collection || !value)
2445
+ return '';
2446
+ const from = rawPropToRuntimeSource(node, 'from');
2447
+ const args = from ? `${value}, ${from}` : value;
2448
+ return `((${collection}).${method}(${args}))`;
2449
+ }
2450
+ function runtimeCollectBindingExpr(node, options = {}) {
2451
+ const source = options.portable ? runtimePortableSource : (value) => value;
2452
+ const from = source(rawPropToRuntimeSource(node, 'from'));
2453
+ if (!from)
2454
+ return '';
2455
+ const where = source(rawPropToRuntimeSource(node, 'where'));
2456
+ const order = source(rawPropToRuntimeSource(node, 'order'));
2457
+ const limit = source(rawPropToRuntimeSource(node, 'limit'));
2458
+ let chain = `(${from})`;
2459
+ if (where)
2460
+ chain += `.filter((item) => ${where})`;
2461
+ if (order)
2462
+ chain += `.sort((a, b) => ${order})`;
2463
+ if (limit)
2464
+ chain += `.slice(0, ${limit})`;
2465
+ return `(${chain})`;
2466
+ }
2467
+ function runtimePartitionBindings(node) {
2468
+ const collection = rawPropToRuntimeSource(node, 'in');
2469
+ const predicate = rawPropToRuntimeSource(node, 'where');
2470
+ if (!collection || !predicate)
2471
+ return [];
2472
+ const passName = str(getProps(node).pass);
2473
+ const failName = str(getProps(node).fail);
2474
+ if (!isRuntimeBindingName(passName) || !isRuntimeBindingName(failName))
2475
+ return [];
2476
+ const item = runtimeNamedProp(node, 'item', 'item');
2477
+ const pairName = `__kernPartition_${passName}_${failName}`;
2478
+ const pairExpr = `((${collection}).reduce((acc, ${item}) => { (${predicate} ? acc[0] : acc[1]).push(${item}); return acc; }, [[], []]))`;
2479
+ return [
2480
+ {
2481
+ name: pairName,
2482
+ expr: pairExpr,
2483
+ kind: 'native',
2484
+ line: node.loc?.line,
2485
+ },
2486
+ {
2487
+ name: passName,
2488
+ expr: `${pairName}[0]`,
2489
+ kind: 'native',
2490
+ line: node.loc?.line,
2491
+ },
2492
+ {
2493
+ name: failName,
2494
+ expr: `${pairName}[1]`,
2495
+ kind: 'native',
2496
+ line: node.loc?.line,
2497
+ },
2498
+ ];
2499
+ }
2500
+ function runtimeRespondSource(node, options = {}) {
2501
+ const props = getProps(node);
2502
+ const status = typeof props.status === 'number' ? props.status : Number(str(props.status)) || undefined;
2503
+ const valueSource = options.portable ? runtimePortableValuePropSource : runtimeValuePropSource;
2504
+ const jsonSource = valueSource(node, 'json');
2505
+ if (jsonSource)
2506
+ return status && status !== 200 ? `({ status: ${status}, json: ${jsonSource} })` : jsonSource;
2507
+ const textSource = valueSource(node, 'text');
2508
+ if (textSource)
2509
+ return status && status !== 200 ? `({ status: ${status}, text: ${textSource} })` : textSource;
2510
+ const error = str(props.error);
2511
+ if (error)
2512
+ return `({ status: ${status || 500}, error: ${JSON.stringify(error)} })`;
2513
+ const redirect = str(props.redirect);
2514
+ if (redirect)
2515
+ return `({ status: ${status || 302}, redirect: ${JSON.stringify(redirect)} })`;
2516
+ return `({ status: ${status || 200} })`;
2517
+ }
2518
+ function runtimeScopedObject(names) {
2519
+ const unique = [...new Set(names)].filter(isRuntimeBindingName);
2520
+ if (unique.length === 0)
2521
+ return 'undefined';
2522
+ if (unique.length === 1)
2523
+ return unique[0];
2524
+ return `({ ${unique.join(', ')} })`;
2525
+ }
2526
+ function runtimeWorkflowHelperLines() {
2527
+ return [
2528
+ 'const __kernGuardError = (name, message) => { const error = new Error(message); error.name = name; throw error; };',
2529
+ 'const __kernGuardSyncParam = (name, value) => {',
2530
+ ' if (typeof params === "object" && params !== null) params[name] = value;',
2531
+ ' if (typeof query === "object" && query !== null && name in query) query[name] = value;',
2532
+ ' if (typeof body === "object" && body !== null && name in body) body[name] = value;',
2533
+ ' if (typeof args === "object" && args !== null) args[name] = value;',
2534
+ '};',
2535
+ 'const __kernNormalizePath = (value) => {',
2536
+ ' const text = String(value || "");',
2537
+ ' const absolute = text.startsWith("/") ? text : `/${text}`;',
2538
+ ' const parts = [];',
2539
+ ' for (const part of absolute.split("/")) {',
2540
+ ' if (!part || part === ".") continue;',
2541
+ ' if (part === "..") parts.pop();',
2542
+ ' else parts.push(part);',
2543
+ ' }',
2544
+ ' return `/${parts.join("/")}`;',
2545
+ '};',
2546
+ 'const __kernResolvePath = (value, base) => String(value).startsWith("/") ? __kernNormalizePath(value) : __kernNormalizePath(`${base || ""}/${value}`);',
2547
+ 'const __kernPathWithin = (candidate, roots) => roots.some((root) => candidate === root || candidate.startsWith(root.endsWith("/") ? root : `${root}/`));',
2548
+ 'const __kernByteLength = (value) => {',
2549
+ ' const text = typeof value === "string" ? value : JSON.stringify(value);',
2550
+ ' if (text == null) return 0;',
2551
+ ' let bytes = 0;',
2552
+ ' for (let i = 0; i < text.length; i++) {',
2553
+ ' const code = text.charCodeAt(i);',
2554
+ ' if (code < 0x80) bytes += 1;',
2555
+ ' else if (code < 0x800) bytes += 2;',
2556
+ ' else if (code >= 0xd800 && code <= 0xdbff) { bytes += 4; i++; }',
2557
+ ' else bytes += 3;',
2558
+ ' }',
2559
+ ' return bytes;',
2560
+ '};',
2561
+ 'const __kernUrlHost = (value) => {',
2562
+ ' const match = String(value || "").match(/^[A-Za-z][A-Za-z0-9+.-]*:\\/\\/([^/?#]+)/);',
2563
+ ' if (!match) return "";',
2564
+ ' return match[1].split("@").pop().split(":")[0].toLowerCase();',
2565
+ '};',
2566
+ ];
2567
+ }
2568
+ function runtimeGuardTargetName(node, options) {
2569
+ return guardParam(node) || options.targetParam || '';
2570
+ }
2571
+ function runtimeGuardListProp(node, ...propNames) {
2572
+ const props = getProps(node);
2573
+ for (const propName of propNames) {
2574
+ const value = str(props[propName]);
2575
+ if (value)
2576
+ return parseList(value);
2577
+ }
2578
+ return [];
2579
+ }
2580
+ function runtimeGuardAssignLines(target, source) {
2581
+ return [`${target} = ${source};`, `__kernGuardSyncParam(${JSON.stringify(target)}, ${target});`];
2582
+ }
2583
+ function runtimeGuardStatement(node, options = {}) {
2584
+ const rawExpr = exprPropToRuntimeSource(node, 'expr') || rawPropToRuntimeSource(node, 'expr');
2585
+ const expr = options.portable ? runtimePortableSource(rawExpr) : rawExpr;
2586
+ const props = getProps(node);
2587
+ const coverageLine = options.trackCoverage ? runtimeGuardCoverageCallLine(node) : undefined;
2588
+ const withCoverage = (lines) => (coverageLine ? [coverageLine, ...lines] : lines);
2589
+ if (expr) {
2590
+ const fallback = str(props.else) || str(props.fallback);
2591
+ const numericFallback = fallback && /^\d+$/.test(fallback) ? Number(fallback) : undefined;
2592
+ const result = numericFallback !== undefined
2593
+ ? `({ status: ${numericFallback} })`
2594
+ : fallback
2595
+ ? `({ error: ${JSON.stringify(fallback)} })`
2596
+ : 'false';
2597
+ return withCoverage([`if (!(${expr})) return (${result});`]);
2598
+ }
2599
+ if (!options.portable && !options.targetParam)
2600
+ return [];
2601
+ const kind = guardKind(node);
2602
+ const target = runtimeGuardTargetName(node, options);
2603
+ const lines = [];
2604
+ if (kind === 'sanitize') {
2605
+ if (!target || !isRuntimeBindingName(target))
2606
+ return [];
2607
+ const pattern = str(props.pattern) || '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]';
2608
+ const replacement = str(props.replacement);
2609
+ try {
2610
+ new RegExp(pattern);
2611
+ if (/([+*}])\s*\)\s*[+*{]/.test(pattern)) {
2612
+ return withCoverage([
2613
+ `__kernGuardError("GuardConfigError", ${JSON.stringify(`Unsafe sanitize regex for ${target}: ${pattern}`)});`,
2614
+ ]);
2615
+ }
2616
+ }
2617
+ catch {
2618
+ return withCoverage([
2619
+ `__kernGuardError("GuardConfigError", ${JSON.stringify(`Invalid sanitize regex for ${target}: ${pattern}`)});`,
2620
+ ]);
2621
+ }
2622
+ lines.push(`if (typeof ${target} === "string") {`);
2623
+ for (const line of runtimeGuardAssignLines(target, `${target}.replace(new RegExp(${JSON.stringify(pattern)}, "g"), ${JSON.stringify(replacement)})`)) {
2624
+ lines.push(` ${line}`);
2625
+ }
2626
+ lines.push('}');
2627
+ return withCoverage(lines);
2628
+ }
2629
+ if (kind === 'validate') {
2630
+ if (!target || !isRuntimeBindingName(target))
2631
+ return [];
2632
+ const min = numericProp(props, 'min');
2633
+ const max = numericProp(props, 'max');
2634
+ if (min !== undefined) {
2635
+ lines.push(`if (typeof ${target} === "number" ? ${target} < ${min} : String(${target}).length < ${min}) __kernGuardError("ValidationError", ${JSON.stringify(`${target} below minimum ${min}`)});`);
2636
+ }
2637
+ if (max !== undefined) {
2638
+ lines.push(`if (typeof ${target} === "number" ? ${target} > ${max} : String(${target}).length > ${max}) __kernGuardError("ValidationError", ${JSON.stringify(`${target} above maximum ${max}`)});`);
2639
+ }
2640
+ const regex = str(props.regex) || str(props.pattern);
2641
+ if (regex) {
2642
+ try {
2643
+ new RegExp(regex);
2644
+ if (/([+*}])\s*\)\s*[+*{]/.test(regex)) {
2645
+ lines.push(`__kernGuardError("GuardConfigError", ${JSON.stringify(`Unsafe validate regex for ${target}: ${regex}`)});`);
2646
+ }
2647
+ else {
2648
+ lines.push(`if (typeof ${target} === "string" && !(new RegExp(${JSON.stringify(regex)})).test(${target})) __kernGuardError("ValidationError", ${JSON.stringify(`${target} does not match required pattern`)});`);
2649
+ }
2650
+ }
2651
+ catch {
2652
+ lines.push(`__kernGuardError("GuardConfigError", ${JSON.stringify(`Invalid validate regex for ${target}: ${regex}`)});`);
2653
+ }
2654
+ }
2655
+ return withCoverage(lines);
2656
+ }
2657
+ if (kind === 'pathcontainment' || kind === 'pathcontainmentguard' || kind === 'path') {
2658
+ if (!target || !isRuntimeBindingName(target))
2659
+ return [];
2660
+ const roots = runtimeGuardListProp(node, 'allowlist', 'allow', 'roots', 'root');
2661
+ const base = str(props.baseDir) || str(props.base) || '/';
2662
+ const rootList = roots.length > 0 ? roots : [base || '/'];
2663
+ const rootsSource = `[${rootList.map((root) => JSON.stringify(root)).join(', ')}].map(__kernNormalizePath)`;
2664
+ lines.push(`if (${target} == null || ${target} === "") __kernGuardError("PathContainmentError", ${JSON.stringify(`${target} is required for path containment check`)});`);
2665
+ for (const line of runtimeGuardAssignLines(target, `__kernResolvePath(${target}, ${JSON.stringify(base)})`)) {
2666
+ lines.push(line);
2667
+ }
2668
+ lines.push(`if (!__kernPathWithin(${target}, ${rootsSource})) __kernGuardError("PathContainmentError", ${JSON.stringify(`${target} escapes allowed directories`)});`);
2669
+ return withCoverage(lines);
2670
+ }
2671
+ if (kind === 'sizelimit') {
2672
+ if (!target || !isRuntimeBindingName(target))
2673
+ return [];
2674
+ const maxBytes = numericProp(props, 'maxBytes') ?? numericProp(props, 'max') ?? 1048576;
2675
+ lines.push(`if (__kernByteLength(${target}) > ${maxBytes}) __kernGuardError("SizeLimitError", ${JSON.stringify(`${target} exceeds size limit of ${maxBytes} bytes`)});`);
2676
+ return withCoverage(lines);
2677
+ }
2678
+ if (kind === 'auth') {
2679
+ const tokenTarget = target && isRuntimeBindingName(target) ? target : '';
2680
+ const header = str(props.header) || 'authorization';
2681
+ const tokenExpr = tokenTarget
2682
+ ? tokenTarget
2683
+ : `(typeof headers === "object" && headers !== null ? (headers[${JSON.stringify(header)}] || headers[${JSON.stringify(header.toLowerCase())}] || headers.Authorization) : undefined) || (typeof params === "object" && params !== null ? (params.token || params.authorization) : undefined) || (typeof args === "object" && args !== null ? (args.token || args.authorization) : undefined)`;
2684
+ lines.push(`if (!(${tokenExpr})) __kernGuardError("AuthError", ${JSON.stringify(`Authentication required${tokenTarget ? `: ${tokenTarget}` : ''}`)});`);
2685
+ return withCoverage(lines);
2686
+ }
2687
+ if (kind === 'ratelimit') {
2688
+ const maxRequests = numericProp(props, 'maxRequests') ?? numericProp(props, 'requests') ?? 1;
2689
+ if (maxRequests <= 0) {
2690
+ return withCoverage([`__kernGuardError("RateLimitError", "Rate limit maxRequests must be greater than 0");`]);
2691
+ }
2692
+ return withCoverage([]);
2693
+ }
2694
+ if (kind === 'urlallowlist' || kind === 'urlvalidation' || kind === 'hostallowlist' || kind === 'domainallowlist') {
2695
+ if (!target || !isRuntimeBindingName(target))
2696
+ return [];
2697
+ const allowed = runtimeGuardListProp(node, 'allowlist', 'allow').map((entry) => entry.toLowerCase());
2698
+ if (allowed.length === 0)
2699
+ return [];
2700
+ const allowedSource = `[${allowed.map((entry) => JSON.stringify(entry)).join(', ')}]`;
2701
+ lines.push(`if (!${allowedSource}.includes(__kernUrlHost(${target}))) __kernGuardError("UrlAllowlistError", ${JSON.stringify(`${target} host is not allowlisted`)});`);
2702
+ return withCoverage(lines);
2703
+ }
2704
+ return [];
2705
+ }
2706
+ function runtimeScopedChildProgram(children) {
2707
+ const lines = [];
2708
+ const names = [];
2709
+ let hasReturn = false;
2710
+ for (const child of children) {
2711
+ if (child.type === 'respond') {
2712
+ lines.push(`return (${runtimeRespondSource(child)});`);
2713
+ hasReturn = true;
2714
+ continue;
2715
+ }
2716
+ if (child.type === 'guard') {
2717
+ lines.push(...runtimeGuardStatement(child));
2718
+ continue;
2719
+ }
2720
+ if (child.type === 'destructure') {
2721
+ for (const binding of runtimeDestructureBindings(child)) {
2722
+ lines.push(`const ${binding.name} = (${binding.expr});`);
2723
+ names.push(binding.name);
2724
+ }
2725
+ continue;
2726
+ }
2727
+ if (child.type === 'partition') {
2728
+ const partitionBindings = runtimePartitionBindings(child);
2729
+ for (const binding of partitionBindings) {
2730
+ lines.push(`const ${binding.name} = (${binding.expr});`);
2731
+ names.push(binding.name);
2732
+ }
2733
+ continue;
2734
+ }
2735
+ if (child.type === 'each') {
2736
+ const expr = runtimeEachExecutionExpr(child);
2737
+ if (expr)
2738
+ lines.push(`(${expr});`);
2739
+ continue;
2740
+ }
2741
+ const name = str(getProps(child).name);
2742
+ const binding = runtimeBindingSource(child);
2743
+ if (name && binding?.expr && isRuntimeBindingName(name)) {
2744
+ lines.push(`const ${name} = (${binding.expr});`);
2745
+ names.push(name);
2746
+ }
2747
+ }
2748
+ return { lines, names, hasReturn };
2749
+ }
2750
+ function runtimeBranchBindingExpr(node, options = {}) {
2751
+ const on = rawPropToRuntimeSource(node, 'on');
2752
+ if (!on)
2753
+ return '';
2754
+ const lines = [`(${options.async ? 'async ' : ''}() => {`, ` const __branchValue = (${on});`];
2755
+ const paths = getChildren(node, 'path');
2756
+ for (let index = 0; index < paths.length; index++) {
2757
+ const pathNode = paths[index];
2758
+ const value = str(getProps(pathNode).value);
2759
+ const program = runtimeScopedChildProgram(pathNode.children || []);
2760
+ const keyword = index === 0 ? 'if' : 'else if';
2761
+ lines.push(` ${keyword} (__branchValue === ${JSON.stringify(value)}) {`);
2762
+ for (const line of program.lines)
2763
+ lines.push(` ${line}`);
2764
+ if (!program.hasReturn)
2765
+ lines.push(` return (${runtimeScopedObject(program.names)});`);
2766
+ lines.push(' }');
2767
+ }
2768
+ lines.push(' return undefined;');
2769
+ lines.push('})()');
2770
+ return lines.join('\n');
2771
+ }
2772
+ function runtimeEachExecutionExpr(node, options = {}) {
2773
+ const collection = rawPropToRuntimeSource(node, 'in');
2774
+ const item = str(getProps(node).name) || 'item';
2775
+ if (!collection || !isRuntimeBindingName(item))
2776
+ return '';
2777
+ const index = str(getProps(node).index);
2778
+ if (index && !isRuntimeBindingName(index))
2779
+ return '';
2780
+ const program = runtimeScopedChildProgram(node.children || []);
2781
+ const lines = [`(${options.async ? 'async ' : ''}() => {`, ' const __results = [];'];
2782
+ if (index) {
2783
+ lines.push(` for (const [${index}, ${item}] of (${collection}).entries()) {`);
2784
+ }
2785
+ else {
2786
+ lines.push(` for (const ${item} of ${collection}) {`);
2787
+ }
2788
+ for (const line of program.lines)
2789
+ lines.push(` ${line}`);
2790
+ lines.push(` __results.push(${runtimeScopedObject(program.names)});`);
2791
+ lines.push(' }');
2792
+ lines.push(' return __results;');
2793
+ lines.push('})()');
2794
+ return lines.join('\n');
2795
+ }
2796
+ function runtimeEachExecutionBinding(node) {
2797
+ const expr = runtimeEachExecutionExpr(node);
2798
+ if (!expr)
2799
+ return undefined;
2800
+ return {
2801
+ name: runtimeSyntheticName(node, 'Each'),
2802
+ expr,
2803
+ kind: 'native',
2804
+ eager: true,
2805
+ line: node.loc?.line,
2806
+ };
2807
+ }
2808
+ function runtimeRouteMethod(node) {
2809
+ return (str(getProps(node).method) || 'get').toLowerCase();
2810
+ }
2811
+ function runtimeRoutePath(node) {
2812
+ return str(getProps(node).path);
2813
+ }
2814
+ function runtimeRouteLabel(node) {
2815
+ return `${runtimeRouteMethod(node).toUpperCase()} ${runtimeRoutePath(node) || '<missing-path>'}`;
2816
+ }
2817
+ function parseRuntimeRouteSpec(spec) {
2818
+ const trimmed = spec.trim();
2819
+ const match = trimmed.match(/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+(.+)$/i);
2820
+ if (match)
2821
+ return { method: match[1].toLowerCase(), path: match[2].trim() };
2822
+ if (trimmed.startsWith('/'))
2823
+ return { path: trimmed };
2824
+ return { name: trimmed };
2825
+ }
2826
+ function findRuntimeRoute(target, spec) {
2827
+ const parsed = parseRuntimeRouteSpec(spec);
2828
+ const routes = collectNodes(target.root, 'route').filter((route) => {
2829
+ const props = getProps(route);
2830
+ if (parsed.name) {
2831
+ return str(props.name) === parsed.name || runtimeRouteLabel(route) === parsed.name;
2832
+ }
2833
+ if (parsed.method && runtimeRouteMethod(route) !== parsed.method)
2834
+ return false;
2835
+ return !parsed.path || runtimeRoutePath(route) === parsed.path;
2836
+ });
2837
+ if (routes.length === 1)
2838
+ return { route: routes[0] };
2839
+ if (routes.length === 0)
2840
+ return { message: `Runtime route assertion target not found: ${spec}` };
2841
+ return { message: `Runtime route assertion target is ambiguous: ${spec}` };
2842
+ }
2843
+ function runtimeRoutePathParamNames(path) {
2844
+ const names = [];
2845
+ const matcher = /(?::|\{)([A-Za-z_$][A-Za-z0-9_$]*)(?:\})?/g;
2846
+ let match;
2847
+ while ((match = matcher.exec(path)))
2848
+ names.push(match[1]);
2849
+ return [...new Set(names)];
2850
+ }
2851
+ function runtimeRouteParamItems(route) {
2852
+ const items = [];
2853
+ for (const paramsNode of getChildren(route, 'params')) {
2854
+ const rawItems = getProps(paramsNode).items;
2855
+ if (!Array.isArray(rawItems))
2856
+ continue;
2857
+ for (const item of rawItems) {
2858
+ if (!item || typeof item !== 'object')
2859
+ continue;
2860
+ const itemProps = item;
2861
+ const name = str(itemProps.name);
2862
+ if (!name)
2863
+ continue;
2864
+ const defaultSource = itemProps.default === undefined ? undefined : String(itemProps.default);
2865
+ items.push({ name, defaultSource });
2866
+ }
2867
+ }
2868
+ return items;
2869
+ }
2870
+ function runtimeWorkflowKind(options) {
2871
+ return options.kind || 'route';
2872
+ }
2873
+ function runtimeEffectName(node) {
2874
+ return str(getProps(node).name) || 'effect';
2875
+ }
2876
+ function runtimeEffectCoverageKey(node) {
2877
+ return `${runtimeEffectName(node)}:${node.loc?.line ?? '?'}`;
2878
+ }
2879
+ function runtimeGuardCoverageKey(node) {
2880
+ return `${node.loc?.line ?? '?'}:${node.loc?.col ?? '?'}`;
2881
+ }
2882
+ function runtimeEffectTriggerSource(node, options = {}) {
2883
+ const triggerNode = getChildren(node, 'trigger')[0];
2884
+ if (!triggerNode)
2885
+ return { message: `Runtime effect ${runtimeEffectName(node)} requires trigger expr={{...}}` };
2886
+ const source = exprPropToRuntimeSource(triggerNode, 'expr');
2887
+ if (!source) {
2888
+ return {
2889
+ message: `Runtime effect ${runtimeEffectName(node)} can only simulate trigger expr={{...}}; ` +
2890
+ 'query/url/call triggers need native mocks or a backend integration test.',
2891
+ };
2892
+ }
2893
+ const portableSource = options.portable ? runtimePortableSource(source) : source;
2894
+ const problem = unsafeRuntimeExpressionReason(portableSource, { allowAwait: true });
2895
+ if (problem) {
2896
+ return { message: `Runtime effect ${runtimeEffectName(node)} cannot execute trigger: ${problem}` };
2897
+ }
2898
+ return { source: portableSource };
2899
+ }
2900
+ function runtimeEffectRecoverFallbackSource(node, options = {}) {
2901
+ const recoverNode = getChildren(node, 'recover')[0];
2902
+ if (!recoverNode)
2903
+ return 'null';
2904
+ const source = runtimeValuePropSource(recoverNode, 'fallback') || 'null';
2905
+ return options.portable ? runtimePortableSource(source) : source;
2906
+ }
2907
+ function findRuntimeEffectMock(mocks, effectName) {
2908
+ const matches = (mocks || []).filter((mock) => mock.effect === effectName);
2909
+ if (matches.length === 0)
2910
+ return {};
2911
+ if (matches.length === 1)
2912
+ return { mock: matches[0] };
2913
+ return { message: `Runtime effect ${effectName} has multiple scoped mocks` };
2914
+ }
2915
+ function runtimeEffectMockExecutionExpr(mock, options = {}) {
2916
+ if (!isRuntimeBindingName(mock.effect))
2917
+ return { message: `Runtime mock has invalid effect name: ${mock.effect}` };
2918
+ if (mock.throws !== undefined) {
2919
+ const rawName = mock.throws.trim() && mock.throws !== 'true' ? mock.throws.trim() : 'Error';
2920
+ const message = rawName === 'Error' ? `Mocked effect ${mock.effect} failed` : rawName;
2921
+ const lines = [
2922
+ '(async () => {',
2923
+ ` const __error = new Error(${JSON.stringify(message)});`,
2924
+ ` __error.name = ${JSON.stringify(rawName)};`,
2925
+ ' throw __error;',
2926
+ '})()',
2927
+ ];
2928
+ return { expr: lines.join('\n') };
2929
+ }
2930
+ const source = options.portable ? runtimePortableSource(mock.returns || 'undefined') : mock.returns || 'undefined';
2931
+ const problem = unsafeRuntimeExpressionReason(source, { allowAwait: true });
2932
+ if (problem)
2933
+ return { message: `Runtime mock effect ${mock.effect} cannot execute returns value: ${problem}` };
2934
+ const lines = ['(async () => {', ` const __value = await (${source});`];
2935
+ if (options.meta) {
2936
+ lines.push(' return { result: __value, recovered: false, attempts: 0, mocked: true };');
2937
+ }
2938
+ else {
2939
+ lines.push(' return __value;');
2940
+ }
2941
+ lines.push('})()');
2942
+ return { expr: lines.join('\n') };
2943
+ }
2944
+ function recordRuntimeEffectMockCall(mock, mockCalls) {
2945
+ if (!mockCalls)
2946
+ return;
2947
+ mockCalls.set(mock.id, (mockCalls.get(mock.id) || 0) + 1);
2948
+ }
2949
+ function markRuntimeEffectMockChecked(mock, checkedMocks) {
2950
+ checkedMocks?.add(mock.id);
2951
+ }
2952
+ function runtimeEffectMockCallLine(mock) {
2953
+ const id = JSON.stringify(mock.id);
2954
+ return `__kernMockCalls[${id}] = (__kernMockCalls[${id}] || 0) + 1;`;
2955
+ }
2956
+ function runtimeEffectCoverageCallLine(effect) {
2957
+ const key = JSON.stringify(runtimeEffectCoverageKey(effect));
2958
+ return `__kernEffectCalls[${key}] = (__kernEffectCalls[${key}] || 0) + 1;`;
2959
+ }
2960
+ function runtimeGuardCoverageCallLine(guard) {
2961
+ const key = JSON.stringify(runtimeGuardCoverageKey(guard));
2962
+ return `__kernGuardCalls[${key}] = (__kernGuardCalls[${key}] || 0) + 1;`;
2963
+ }
2964
+ function mergeRuntimeEffectMockCalls(mockCalls, calls) {
2965
+ if (!mockCalls || !calls || typeof calls !== 'object')
2966
+ return;
2967
+ for (const [id, count] of Object.entries(calls)) {
2968
+ const numeric = Number(count);
2969
+ if (!Number.isFinite(numeric) || numeric <= 0)
2970
+ continue;
2971
+ mockCalls.set(id, (mockCalls.get(id) || 0) + numeric);
2972
+ }
2973
+ }
2974
+ function mergeRuntimeEffectCoverageCalls(effectCalls, calls) {
2975
+ if (!effectCalls || !calls || typeof calls !== 'object')
2976
+ return;
2977
+ for (const [key, count] of Object.entries(calls)) {
2978
+ const numeric = Number(count);
2979
+ if (!Number.isFinite(numeric) || numeric <= 0)
2980
+ continue;
2981
+ effectCalls.set(key, (effectCalls.get(key) || 0) + numeric);
2982
+ }
2983
+ }
2984
+ function mergeRuntimeGuardCoverageCalls(guardCalls, calls) {
2985
+ if (!guardCalls || !calls || typeof calls !== 'object')
2986
+ return;
2987
+ for (const [key, count] of Object.entries(calls)) {
2988
+ const numeric = Number(count);
2989
+ if (!Number.isFinite(numeric) || numeric <= 0)
2990
+ continue;
2991
+ guardCalls.set(key, (guardCalls.get(key) || 0) + numeric);
2992
+ }
2993
+ }
2994
+ function runtimeEffectExecutionExpr(node, options = {}) {
2995
+ const name = runtimeEffectName(node);
2996
+ if (!isRuntimeBindingName(name))
2997
+ return { message: `Runtime effect has invalid name: ${name}` };
2998
+ const trigger = runtimeEffectTriggerSource(node, options);
2999
+ if (trigger.message || !trigger.source)
3000
+ return { message: trigger.message || 'Runtime effect has no trigger' };
3001
+ const recoverNode = getChildren(node, 'recover')[0];
3002
+ const hasRecover = recoverNode !== undefined;
3003
+ const fallbackSource = runtimeEffectRecoverFallbackSource(node, options);
3004
+ const fallbackProblem = unsafeRuntimeExpressionReason(fallbackSource, { allowAwait: true });
3005
+ if (fallbackProblem) {
3006
+ return { message: `Runtime effect ${name} cannot execute fallback: ${fallbackProblem}` };
3007
+ }
3008
+ const rawRetry = recoverNode ? getProps(recoverNode).retry : undefined;
3009
+ const retryCount = rawRetry === undefined || rawRetry === '' ? 0 : Number(rawRetry);
3010
+ if (!Number.isInteger(retryCount) || retryCount < 0) {
3011
+ return { message: `Runtime effect ${name} has invalid recover retry=${String(rawRetry)}` };
3012
+ }
3013
+ const attempts = hasRecover && retryCount > 0 ? retryCount : 1;
3014
+ const result = (value, recovered) => options.meta ? `({ result: ${value}, recovered: ${recovered}, attempts: __attempts })` : value;
3015
+ const lines = ['(async () => {', ' let __attempts = 0;'];
3016
+ if (!hasRecover) {
3017
+ lines.push(' __attempts++;');
3018
+ lines.push(` const __value = await (${trigger.source});`);
3019
+ lines.push(` return ${result('__value', false)};`);
3020
+ lines.push('})()');
3021
+ return { expr: lines.join('\n') };
3022
+ }
3023
+ lines.push(` const __fallback = (${fallbackSource});`);
3024
+ lines.push(` for (let __attempt = 0; __attempt < ${attempts}; __attempt++) {`);
3025
+ lines.push(' __attempts++;');
3026
+ lines.push(' try {');
3027
+ lines.push(` const __value = await (${trigger.source});`);
3028
+ lines.push(` return ${result('__value', false)};`);
3029
+ lines.push(' } catch (__error) {');
3030
+ lines.push(` if (__attempt === ${attempts - 1}) return ${result('__fallback', true)};`);
3031
+ lines.push(' }');
3032
+ lines.push(' }');
3033
+ lines.push(` return ${result('__fallback', true)};`);
3034
+ lines.push('})()');
3035
+ return { expr: lines.join('\n') };
3036
+ }
3037
+ function runtimeRouteRequestLines(route, inputSource) {
3038
+ const lines = [
3039
+ `const __request = ((${inputSource}) ?? {});`,
3040
+ 'let params = (__request.params ?? {});',
3041
+ 'let query = (__request.query ?? {});',
3042
+ 'let body = (__request.body ?? {});',
3043
+ 'let headers = (__request.headers ?? {});',
3044
+ ];
3045
+ const declared = new Set();
3046
+ for (const name of runtimeRoutePathParamNames(runtimeRoutePath(route))) {
3047
+ if (!isRuntimeBindingName(name))
3048
+ return { lines: [], message: `Runtime route has invalid path param: ${name}` };
3049
+ if (declared.has(name))
3050
+ continue;
3051
+ declared.add(name);
3052
+ lines.push(`let ${name} = params[${JSON.stringify(name)}];`);
3053
+ lines.push(`__kernGuardSyncParam(${JSON.stringify(name)}, ${name});`);
3054
+ }
3055
+ for (const item of runtimeRouteParamItems(route)) {
3056
+ if (!isRuntimeBindingName(item.name)) {
3057
+ return { lines: [], message: `Runtime route has invalid params entry: ${item.name}` };
3058
+ }
3059
+ if (declared.has(item.name))
3060
+ continue;
3061
+ declared.add(item.name);
3062
+ const fallback = item.defaultSource ?? 'undefined';
3063
+ const key = JSON.stringify(item.name);
3064
+ lines.push(`let ${item.name} = query[${key}] ?? params[${key}] ?? (${fallback});`);
3065
+ lines.push(`__kernGuardSyncParam(${key}, ${item.name});`);
3066
+ }
3067
+ return { lines };
3068
+ }
3069
+ function runtimeRouteChildProgram(children, options = {}) {
3070
+ const lines = [];
3071
+ const workflowKind = runtimeWorkflowKind(options);
3072
+ for (const child of children) {
3073
+ if (child.type === 'handler') {
3074
+ const handler = runtimeWorkflowHandlerLines(child, workflowKind);
3075
+ if (handler.message)
3076
+ return handler;
3077
+ lines.push(...handler.lines);
3078
+ continue;
3079
+ }
3080
+ if (child.type === 'effect') {
3081
+ const effectName = runtimeEffectName(child);
3082
+ if (!isRuntimeBindingName(effectName)) {
3083
+ return { lines: [], message: `Runtime ${workflowKind} effect has invalid name: ${effectName}` };
3084
+ }
3085
+ const scopedMock = findRuntimeEffectMock(options.mocks, effectName);
3086
+ if (scopedMock.message)
3087
+ return { lines: [], message: scopedMock.message };
3088
+ if (scopedMock.mock) {
3089
+ const mocked = runtimeEffectMockExecutionExpr(scopedMock.mock, { portable: true });
3090
+ if (mocked.message || !mocked.expr) {
3091
+ return {
3092
+ lines: [],
3093
+ message: mocked.message || `Runtime ${workflowKind} mock effect ${effectName} cannot be simulated`,
3094
+ };
3095
+ }
3096
+ lines.push(runtimeEffectCoverageCallLine(child));
3097
+ lines.push(runtimeEffectMockCallLine(scopedMock.mock));
3098
+ lines.push(`const ${effectName} = await (${mocked.expr});`);
3099
+ continue;
3100
+ }
3101
+ const effect = runtimeEffectExecutionExpr(child, { portable: true });
3102
+ if (effect.message || !effect.expr) {
3103
+ return {
3104
+ lines: [],
3105
+ message: effect.message || `Runtime ${workflowKind} effect ${effectName} cannot be simulated`,
3106
+ };
3107
+ }
3108
+ lines.push(runtimeEffectCoverageCallLine(child));
3109
+ lines.push(`const ${effectName} = await (${effect.expr});`);
3110
+ continue;
3111
+ }
3112
+ if (child.type === 'respond') {
3113
+ lines.push(`return (${runtimeRespondSource(child, { portable: true })});`);
3114
+ continue;
3115
+ }
3116
+ if (child.type === 'guard') {
3117
+ lines.push(...runtimeGuardStatement(child, { portable: true, trackCoverage: true }));
3118
+ continue;
3119
+ }
3120
+ if (child.type === 'branch') {
3121
+ const expr = runtimeRouteBranchExecutionExpr(child, options);
3122
+ if (!expr)
3123
+ continue;
3124
+ const name = runtimeSyntheticName(child, 'BranchResult');
3125
+ lines.push(`const ${name} = await (${expr});`);
3126
+ lines.push(`if (${name} !== undefined) return ${name};`);
3127
+ continue;
3128
+ }
3129
+ if (child.type === 'each') {
3130
+ const expr = runtimeRouteEachExecutionExpr(child, options);
3131
+ if (expr)
3132
+ lines.push(`await (${expr});`);
3133
+ continue;
3134
+ }
3135
+ if (child.type === 'destructure') {
3136
+ for (const binding of runtimeDestructureBindings(child))
3137
+ lines.push(`const ${binding.name} = (${binding.expr});`);
3138
+ continue;
3139
+ }
3140
+ if (child.type === 'partition') {
3141
+ for (const binding of runtimePartitionBindings(child))
3142
+ lines.push(`const ${binding.name} = (${binding.expr});`);
3143
+ continue;
3144
+ }
3145
+ const name = str(getProps(child).name);
3146
+ const binding = child.type === 'collect'
3147
+ ? { expr: runtimeCollectBindingExpr(child, { portable: true }), kind: 'native' }
3148
+ : runtimeBindingSource(child);
3149
+ if (name && binding?.expr && isRuntimeBindingName(name)) {
3150
+ lines.push(`const ${name} = (${runtimePortableSource(binding.expr)});`);
3151
+ }
3152
+ }
3153
+ lines.push('return undefined;');
3154
+ return { lines };
3155
+ }
3156
+ function runtimeWorkflowHandlerLines(node, workflowKind) {
3157
+ const code = str(getProps(node).code).trim();
3158
+ if (!code)
3159
+ return { lines: [] };
3160
+ const problem = unsafeRuntimeWorkflowReason(code);
3161
+ if (problem) {
3162
+ return {
3163
+ lines: [],
3164
+ message: `Runtime ${workflowKind} assertion cannot execute handler: ${problem}`,
3165
+ };
3166
+ }
3167
+ return { lines: code.split('\n') };
3168
+ }
3169
+ function runtimeRouteBranchExecutionExpr(node, options = {}) {
3170
+ const on = runtimePortableSource(rawPropToRuntimeSource(node, 'on'));
3171
+ if (!on)
3172
+ return '';
3173
+ const lines = ['(async () => {', ` const __branchValue = (${on});`];
3174
+ const paths = getChildren(node, 'path');
3175
+ for (let index = 0; index < paths.length; index++) {
3176
+ const pathNode = paths[index];
3177
+ const value = str(getProps(pathNode).value);
3178
+ const program = runtimeRouteChildProgram(pathNode.children || [], options);
3179
+ if (program.message)
3180
+ return '';
3181
+ const keyword = index === 0 ? 'if' : 'else if';
3182
+ lines.push(` ${keyword} (__branchValue === ${JSON.stringify(value)}) {`);
3183
+ for (const line of program.lines)
3184
+ lines.push(` ${line}`);
3185
+ lines.push(' }');
3186
+ }
3187
+ lines.push(' return undefined;');
3188
+ lines.push('})()');
3189
+ return lines.join('\n');
3190
+ }
3191
+ function runtimeRouteEachExecutionExpr(node, options = {}) {
3192
+ const collection = runtimePortableSource(rawPropToRuntimeSource(node, 'in'));
3193
+ const item = str(getProps(node).name) || 'item';
3194
+ if (!collection || !isRuntimeBindingName(item))
3195
+ return '';
3196
+ const index = str(getProps(node).index);
3197
+ if (index && !isRuntimeBindingName(index))
3198
+ return '';
3199
+ const program = runtimeRouteChildProgram(node.children || [], options);
3200
+ if (program.message)
3201
+ return '';
3202
+ const lines = ['(async () => {', ' const __results = [];'];
3203
+ if (index) {
3204
+ lines.push(` for (const [${index}, ${item}] of (${collection}).entries()) {`);
3205
+ }
3206
+ else {
3207
+ lines.push(` for (const ${item} of ${collection}) {`);
3208
+ }
3209
+ for (const line of program.lines)
3210
+ lines.push(` ${line}`);
3211
+ lines.push(' __results.push(undefined);');
3212
+ lines.push(' }');
3213
+ lines.push(' return __results;');
3214
+ lines.push('})()');
3215
+ return lines.join('\n');
3216
+ }
3217
+ function runtimeRouteExecutionExpr(route, inputSource, options = {}) {
3218
+ const request = runtimeRouteRequestLines(route, inputSource || '{}');
3219
+ if (request.message)
3220
+ return { message: request.message };
3221
+ const program = runtimeRouteChildProgram(route.children || [], options);
3222
+ if (program.message)
3223
+ return { message: program.message };
3224
+ const bodyLines = [...runtimeWorkflowHelperLines(), ...request.lines, ...program.lines];
3225
+ const lines = ['(async () => {', ' const __kernMockCalls = Object.create(null);'];
3226
+ lines.push(' const __kernEffectCalls = Object.create(null);');
3227
+ lines.push(' const __kernGuardCalls = Object.create(null);');
3228
+ if (options.probe) {
3229
+ lines.push(' const __kernRunRoute = async () => {');
3230
+ for (const line of bodyLines)
3231
+ lines.push(` ${line}`);
3232
+ lines.push(' };');
3233
+ lines.push(' try {');
3234
+ lines.push(' const __kernValue = await __kernRunRoute();');
3235
+ lines.push(' return { __kernRouteStatus: "returned", value: __kernValue, calls: __kernMockCalls, effects: __kernEffectCalls, guards: __kernGuardCalls };');
3236
+ lines.push(' } catch (__kernError) {');
3237
+ lines.push(' return {');
3238
+ lines.push(' __kernRouteStatus: "thrown",');
3239
+ lines.push(' error: {');
3240
+ lines.push(' name: __kernError && __kernError.name ? String(__kernError.name) : "Error",');
3241
+ lines.push(' message: __kernError && __kernError.message ? String(__kernError.message) : String(__kernError),');
3242
+ lines.push(' stack: __kernError && __kernError.stack ? String(__kernError.stack) : undefined,');
3243
+ lines.push(' },');
3244
+ lines.push(' calls: __kernMockCalls,');
3245
+ lines.push(' effects: __kernEffectCalls,');
3246
+ lines.push(' guards: __kernGuardCalls,');
3247
+ lines.push(' };');
3248
+ lines.push(' }');
3249
+ }
3250
+ else {
3251
+ for (const line of bodyLines)
3252
+ lines.push(` ${line}`);
3253
+ }
3254
+ lines.push('})()');
3255
+ return { expr: lines.join('\n') };
3256
+ }
3257
+ function runtimeToolName(node) {
3258
+ return str(getProps(node).name) || 'tool';
3259
+ }
3260
+ function findRuntimeTool(target, toolName) {
3261
+ const tools = collectNodes(target.root, 'tool').filter((tool) => runtimeToolName(tool) === toolName);
3262
+ if (tools.length === 1)
3263
+ return { tool: tools[0] };
3264
+ if (tools.length === 0)
3265
+ return { message: `Runtime tool assertion target not found: ${toolName}` };
3266
+ return { message: `Runtime tool assertion target is ambiguous: ${toolName}` };
3267
+ }
3268
+ function runtimeToolParamItems(tool) {
3269
+ const items = [];
3270
+ for (const param of getChildren(tool, 'param')) {
3271
+ const name = str(getProps(param).name);
3272
+ if (!name)
3273
+ continue;
3274
+ const defaultSource = runtimePortableValuePropSource(param, 'value') || runtimePortableValuePropSource(param, 'default');
3275
+ items.push({ name, ...(defaultSource ? { defaultSource } : {}) });
3276
+ }
3277
+ return items;
3278
+ }
3279
+ function runtimeToolInputLines(tool, inputSource) {
3280
+ const lines = [
3281
+ `const __toolInput = ((${inputSource}) ?? {});`,
3282
+ 'let args = __toolInput;',
3283
+ 'let params = __toolInput;',
3284
+ ];
3285
+ const declared = new Set();
3286
+ for (const item of runtimeToolParamItems(tool)) {
3287
+ if (!isRuntimeBindingName(item.name)) {
3288
+ return { lines: [], message: `Runtime tool has invalid param: ${item.name}` };
3289
+ }
3290
+ if (item.name === 'args' || item.name === 'params') {
3291
+ return { lines: [], message: `Runtime tool param name is reserved for native context: ${item.name}` };
3292
+ }
3293
+ if (declared.has(item.name))
3294
+ continue;
3295
+ declared.add(item.name);
3296
+ const fallback = item.defaultSource ?? 'undefined';
3297
+ const key = JSON.stringify(item.name);
3298
+ lines.push(`let ${item.name} = params[${key}] ?? (${fallback});`);
3299
+ lines.push(`__kernGuardSyncParam(${key}, ${item.name});`);
3300
+ const paramNode = paramNodeByName(tool, item.name);
3301
+ for (const guard of paramNode ? getChildren(paramNode, 'guard') : []) {
3302
+ lines.push(...runtimeGuardStatement(guard, { portable: true, targetParam: item.name, trackCoverage: true }));
3303
+ }
3304
+ }
3305
+ return { lines };
3306
+ }
3307
+ function runtimeToolExecutionExpr(tool, inputSource, options = {}) {
3308
+ const input = runtimeToolInputLines(tool, inputSource || '{}');
3309
+ if (input.message)
3310
+ return { message: input.message };
3311
+ const program = runtimeRouteChildProgram(tool.children || [], { mocks: options.mocks, kind: 'tool' });
3312
+ if (program.message)
3313
+ return { message: program.message };
3314
+ const bodyLines = [...runtimeWorkflowHelperLines(), ...input.lines, ...program.lines];
3315
+ const lines = ['(async () => {', ' const __kernMockCalls = Object.create(null);'];
3316
+ lines.push(' const __kernEffectCalls = Object.create(null);');
3317
+ lines.push(' const __kernGuardCalls = Object.create(null);');
3318
+ if (options.probe) {
3319
+ lines.push(' const __kernRunTool = async () => {');
3320
+ for (const line of bodyLines)
3321
+ lines.push(` ${line}`);
3322
+ lines.push(' };');
3323
+ lines.push(' try {');
3324
+ lines.push(' const __kernValue = await __kernRunTool();');
3325
+ lines.push(' return { __kernToolStatus: "returned", value: __kernValue, calls: __kernMockCalls, effects: __kernEffectCalls, guards: __kernGuardCalls };');
3326
+ lines.push(' } catch (__kernError) {');
3327
+ lines.push(' return {');
3328
+ lines.push(' __kernToolStatus: "thrown",');
3329
+ lines.push(' error: {');
3330
+ lines.push(' name: __kernError && __kernError.name ? String(__kernError.name) : "Error",');
3331
+ lines.push(' message: __kernError && __kernError.message ? String(__kernError.message) : String(__kernError),');
3332
+ lines.push(' stack: __kernError && __kernError.stack ? String(__kernError.stack) : undefined,');
3333
+ lines.push(' },');
3334
+ lines.push(' calls: __kernMockCalls,');
3335
+ lines.push(' effects: __kernEffectCalls,');
3336
+ lines.push(' guards: __kernGuardCalls,');
3337
+ lines.push(' };');
3338
+ lines.push(' }');
3339
+ }
3340
+ else {
3341
+ for (const line of bodyLines)
3342
+ lines.push(` ${line}`);
3343
+ }
3344
+ lines.push('})()');
3345
+ return { expr: lines.join('\n') };
3346
+ }
3347
+ function runtimeArrayBindingExpr(node) {
3348
+ const collection = rawPropToRuntimeSource(node, 'in');
3349
+ switch (node.type) {
3350
+ case 'filter':
3351
+ case 'find':
3352
+ case 'some':
3353
+ case 'every':
3354
+ case 'findIndex':
3355
+ return runtimeArrayPredicateBindingExpr(node, node.type);
3356
+ case 'map':
3357
+ case 'flatMap':
3358
+ return runtimeArrayProjectionBindingExpr(node, node.type);
3359
+ case 'reduce': {
3360
+ const body = rawPropToRuntimeSource(node, 'expr');
3361
+ const initial = rawPropToRuntimeSource(node, 'initial');
3362
+ if (!collection || !body || !initial)
3363
+ return '';
3364
+ const acc = runtimeNamedProp(node, 'acc', 'acc');
3365
+ const item = runtimeNamedProp(node, 'item', 'item');
3366
+ return `((${collection}).reduce((${acc}, ${item}) => ${body}, ${initial}))`;
3367
+ }
3368
+ case 'slice': {
3369
+ if (!collection)
3370
+ return '';
3371
+ const start = rawPropToRuntimeSource(node, 'start');
3372
+ const end = rawPropToRuntimeSource(node, 'end');
3373
+ const args = [];
3374
+ if (start)
3375
+ args.push(start);
3376
+ if (end) {
3377
+ if (!start)
3378
+ args.push('0');
3379
+ args.push(end);
3380
+ }
3381
+ return `((${collection}).slice(${args.join(', ')}))`;
3382
+ }
3383
+ case 'flat': {
3384
+ if (!collection)
3385
+ return '';
3386
+ const depth = rawPropToRuntimeSource(node, 'depth');
3387
+ return `((${collection}).flat(${depth}))`;
3388
+ }
3389
+ case 'at': {
3390
+ const index = rawPropToRuntimeSource(node, 'index');
3391
+ return collection && index ? `((${collection}).at(${index}))` : '';
3392
+ }
3393
+ case 'sort': {
3394
+ if (!collection)
3395
+ return '';
3396
+ const compare = rawPropToRuntimeSource(node, 'compare');
3397
+ if (!compare)
3398
+ return `([...(${collection})].sort())`;
3399
+ const a = runtimeNamedProp(node, 'a', 'a');
3400
+ const b = runtimeNamedProp(node, 'b', 'b');
3401
+ return `([...(${collection})].sort((${a}, ${b}) => ${compare}))`;
3402
+ }
3403
+ case 'reverse':
3404
+ return collection ? `([...(${collection})].reverse())` : '';
3405
+ case 'join': {
3406
+ if (!collection)
3407
+ return '';
3408
+ const separator = stringLiteralOrExprPropToRuntimeSource(node, 'separator');
3409
+ return `((${collection}).join(${separator}))`;
3410
+ }
3411
+ case 'includes':
3412
+ case 'indexOf':
3413
+ case 'lastIndexOf':
3414
+ return runtimeArrayValueLookupBindingExpr(node, node.type);
3415
+ case 'concat': {
3416
+ const withArg = rawPropToRuntimeSource(node, 'with');
3417
+ return collection && withArg ? `((${collection}).concat(${withArg}))` : '';
3418
+ }
3419
+ case 'compact':
3420
+ return collection ? `((${collection}).filter(Boolean))` : '';
3421
+ case 'pluck': {
3422
+ const prop = rawPropToRuntimeSource(node, 'prop');
3423
+ if (!collection || !prop)
3424
+ return '';
3425
+ const item = runtimeNamedProp(node, 'item', 'item');
3426
+ return `((${collection}).map((${item}) => ${item}.${prop}))`;
3427
+ }
3428
+ case 'unique':
3429
+ return collection ? `((${collection}).filter((item, index, items) => items.indexOf(item) === index))` : '';
3430
+ case 'uniqueBy': {
3431
+ const by = rawPropToRuntimeSource(node, 'by');
3432
+ if (!collection || !by)
3433
+ return '';
3434
+ const item = runtimeNamedProp(node, 'item', 'item');
3435
+ return `((__seen) => (${collection}).filter((${item}) => { const __k = ${by}; if (__seen.has(__k)) return false; __seen.add(__k); return true; }))(new Set())`;
3436
+ }
3437
+ case 'groupBy': {
3438
+ const by = rawPropToRuntimeSource(node, 'by');
3439
+ if (!collection || !by)
3440
+ return '';
3441
+ const item = runtimeNamedProp(node, 'item', 'item');
3442
+ return `((${collection}).reduce((acc, ${item}) => { const __k = ${by}; (acc[__k] ??= []).push(${item}); return acc; }, Object.create(null)))`;
3443
+ }
3444
+ case 'indexBy': {
3445
+ const by = rawPropToRuntimeSource(node, 'by');
3446
+ if (!collection || !by)
3447
+ return '';
3448
+ const item = runtimeNamedProp(node, 'item', 'item');
3449
+ return `(Object.fromEntries((${collection}).map((${item}) => [${by}, ${item}])))`;
3450
+ }
3451
+ case 'countBy': {
3452
+ const by = rawPropToRuntimeSource(node, 'by');
3453
+ if (!collection || !by)
3454
+ return '';
3455
+ const item = runtimeNamedProp(node, 'item', 'item');
3456
+ return `((${collection}).reduce((acc, ${item}) => { const __k = ${by}; acc[__k] = (acc[__k] ?? 0) + 1; return acc; }, Object.create(null)))`;
3457
+ }
3458
+ case 'chunk': {
3459
+ const size = rawPropToRuntimeSource(node, 'size');
3460
+ return collection && size
3461
+ ? `((__src, __n) => Array.from({ length: Math.ceil(__src.length / __n) }, (_, i) => __src.slice(i * __n, (i + 1) * __n)))((${collection}), (${size}))`
3462
+ : '';
3463
+ }
3464
+ case 'zip': {
3465
+ const right = rawPropToRuntimeSource(node, 'with');
3466
+ if (!collection || !right)
3467
+ return '';
3468
+ const item = runtimeNamedProp(node, 'item', 'item');
3469
+ const indexName = runtimeNamedProp(node, 'index', '__i');
3470
+ return `((__r) => (${collection}).map((${item}, ${indexName}) => [${item}, __r[${indexName}]]))((${right}))`;
3471
+ }
3472
+ case 'range': {
3473
+ const end = rawPropToRuntimeSource(node, 'end');
3474
+ if (!end)
3475
+ return '';
3476
+ const start = rawPropToRuntimeSource(node, 'start') || '0';
3477
+ return `(Array.from({ length: (${end}) - (${start}) }, (_, i) => i + (${start})))`;
3478
+ }
3479
+ case 'take': {
3480
+ const n = rawPropToRuntimeSource(node, 'n');
3481
+ return collection && n ? `((${collection}).slice(0, ${n}))` : '';
3482
+ }
3483
+ case 'drop': {
3484
+ const n = rawPropToRuntimeSource(node, 'n');
3485
+ return collection && n ? `((${collection}).slice(${n}))` : '';
3486
+ }
3487
+ case 'min':
3488
+ return collection
3489
+ ? `((__src) => __src.length === 0 ? undefined : __src.reduce((__a, __b) => __b < __a ? __b : __a))((${collection}))`
3490
+ : '';
3491
+ case 'max':
3492
+ return collection
3493
+ ? `((__src) => __src.length === 0 ? undefined : __src.reduce((__a, __b) => __b > __a ? __b : __a))((${collection}))`
3494
+ : '';
3495
+ case 'minBy':
3496
+ case 'maxBy': {
3497
+ const by = rawPropToRuntimeSource(node, 'by');
3498
+ if (!collection || !by)
3499
+ return '';
3500
+ const item = runtimeNamedProp(node, 'item', 'item');
3501
+ const op = node.type === 'minBy' ? '<' : '>';
3502
+ return `((__src) => { if (__src.length === 0) return undefined; const __key = (${item}) => ${by}; return __src.reduce((__best, __cur) => __key(__cur) ${op} __key(__best) ? __cur : __best); })((${collection}))`;
3503
+ }
3504
+ case 'sum':
3505
+ return collection ? `((${collection}).reduce((acc, n) => acc + n, 0))` : '';
3506
+ case 'avg':
3507
+ return collection
3508
+ ? `((__src) => __src.length === 0 ? Number.NaN : __src.reduce((acc, n) => acc + n, 0) / __src.length)((${collection}))`
3509
+ : '';
3510
+ case 'sumBy': {
3511
+ const by = rawPropToRuntimeSource(node, 'by');
3512
+ if (!collection || !by)
3513
+ return '';
3514
+ const item = runtimeNamedProp(node, 'item', 'item');
3515
+ return `((${collection}).reduce((acc, ${item}) => acc + (${by}), 0))`;
3516
+ }
3517
+ case 'intersect': {
3518
+ const right = rawPropToRuntimeSource(node, 'with');
3519
+ if (!collection || !right)
3520
+ return '';
3521
+ const item = runtimeNamedProp(node, 'item', 'item');
3522
+ return `((__r) => (${collection}).filter((${item}) => __r.has(${item})))(new Set((${right})))`;
3523
+ }
3524
+ default:
3525
+ return undefined;
3526
+ }
3527
+ }
3528
+ function collectRuntimeBindings(root) {
3529
+ const bindings = [];
3530
+ function visit(node) {
3531
+ if (node.type === 'route') {
3532
+ return;
3533
+ }
3534
+ if (node.type === 'branch') {
3535
+ const name = str(getProps(node).name);
3536
+ const expr = runtimeBranchBindingExpr(node);
3537
+ if (name && expr) {
3538
+ bindings.push({
3539
+ name,
3540
+ expr,
3541
+ kind: 'native',
3542
+ line: node.loc?.line,
3543
+ });
3544
+ }
3545
+ return;
3546
+ }
3547
+ if (node.type === 'each') {
3548
+ const binding = runtimeEachExecutionBinding(node);
3549
+ if (binding)
3550
+ bindings.push(binding);
3551
+ return;
3552
+ }
3553
+ if (node.type === 'destructure') {
3554
+ bindings.push(...runtimeDestructureBindings(node));
3555
+ }
3556
+ if (node.type === 'partition') {
3557
+ bindings.push(...runtimePartitionBindings(node));
3558
+ }
3559
+ const name = str(getProps(node).name);
3560
+ const binding = runtimeBindingSource(node);
3561
+ if (name && binding?.expr) {
3562
+ bindings.push({
3563
+ name,
3564
+ expr: binding.expr,
3565
+ kind: binding.kind,
3566
+ line: node.loc?.line,
3567
+ });
3568
+ }
3569
+ for (const child of node.children || [])
3570
+ visit(child);
3571
+ }
3572
+ visit(root);
3573
+ return bindings;
3574
+ }
3575
+ function orderRuntimeBindings(bindings, entryExpr) {
3576
+ const byName = new Map();
3577
+ for (const binding of bindings) {
3578
+ if (!isRuntimeBindingName(binding.name)) {
3579
+ return { ordered: [], error: `invalid runtime binding name '${binding.name}' at line ${binding.line ?? '?'}` };
3580
+ }
3581
+ byName.set(binding.name, [...(byName.get(binding.name) || []), binding]);
3582
+ }
3583
+ const ordered = [];
3584
+ const visiting = new Set();
3585
+ const visited = new Set();
3586
+ const stack = [];
3587
+ function depsIn(source) {
3588
+ return [...byName.keys()].filter((name) => new RegExp(`\\b${escapeRegExp(name)}\\b`).test(source));
3589
+ }
3590
+ function bindingFor(name) {
3591
+ const candidates = byName.get(name) || [];
3592
+ if (candidates.length <= 1)
3593
+ return candidates[0];
3594
+ const [first, ...rest] = candidates;
3595
+ throw new Error(`duplicate runtime binding '${name}' at line ${rest[0].line ?? '?'} (first at line ${first.line ?? '?'})`);
3596
+ }
3597
+ function visit(name) {
3598
+ if (visited.has(name))
3599
+ return undefined;
3600
+ if (visiting.has(name)) {
3601
+ const start = stack.indexOf(name);
3602
+ return `runtime binding cycle: ${[...stack.slice(start), name].join(' -> ')}`;
3603
+ }
3604
+ let binding;
3605
+ try {
3606
+ binding = bindingFor(name);
3607
+ }
3608
+ catch (error) {
3609
+ return error instanceof Error ? error.message : String(error);
3610
+ }
3611
+ if (!binding)
3612
+ return undefined;
3613
+ visiting.add(name);
3614
+ stack.push(name);
1519
3615
  for (const dep of depsIn(binding.expr)) {
3616
+ if (dep === name && binding.kind === 'fn')
3617
+ continue;
1520
3618
  const error = visit(dep);
1521
3619
  if (error)
1522
3620
  return error;
@@ -1527,7 +3625,10 @@ function orderRuntimeBindings(bindings, entryExpr) {
1527
3625
  ordered.push(binding);
1528
3626
  return undefined;
1529
3627
  }
1530
- for (const name of depsIn(entryExpr)) {
3628
+ const initialNames = new Set([...bindings.filter((binding) => binding.eager).map((binding) => binding.name)]);
3629
+ for (const name of depsIn(entryExpr))
3630
+ initialNames.add(name);
3631
+ for (const name of initialNames) {
1531
3632
  const error = visit(name);
1532
3633
  if (error)
1533
3634
  return { ordered: [], error };
@@ -1540,12 +3641,14 @@ function runtimeContext() {
1540
3641
  Boolean,
1541
3642
  Error,
1542
3643
  JSON,
3644
+ Map,
1543
3645
  Math,
1544
3646
  Number,
1545
3647
  Object,
1546
3648
  Promise,
1547
3649
  RangeError,
1548
3650
  ReferenceError,
3651
+ Set,
1549
3652
  String,
1550
3653
  SyntaxError,
1551
3654
  TypeError,
@@ -1572,6 +3675,78 @@ function runtimeValuesEqual(actual, expected) {
1572
3675
  return false;
1573
3676
  }
1574
3677
  }
3678
+ function normalizeRuntimeDiffValue(value) {
3679
+ try {
3680
+ return JSON.parse(JSON.stringify(value));
3681
+ }
3682
+ catch {
3683
+ return value;
3684
+ }
3685
+ }
3686
+ function runtimeValueKind(value) {
3687
+ if (Array.isArray(value))
3688
+ return 'array';
3689
+ if (value === null)
3690
+ return 'null';
3691
+ return typeof value;
3692
+ }
3693
+ function runtimePathProperty(base, key) {
3694
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? `${base}.${key}` : `${base}[${JSON.stringify(key)}]`;
3695
+ }
3696
+ function firstRuntimeValueDifference(expected, actual, path = '$', depth = 0) {
3697
+ if (runtimeValuesEqual(actual, expected))
3698
+ return undefined;
3699
+ if (depth > 16) {
3700
+ return `at ${path}: expected ${formatRuntimeValue(expected)}, received ${formatRuntimeValue(actual)}`;
3701
+ }
3702
+ const expectedKind = runtimeValueKind(expected);
3703
+ const actualKind = runtimeValueKind(actual);
3704
+ if (expectedKind !== actualKind) {
3705
+ return `at ${path}: expected ${expectedKind} ${formatRuntimeValue(expected)}, received ${actualKind} ${formatRuntimeValue(actual)}`;
3706
+ }
3707
+ if (Array.isArray(expected) && Array.isArray(actual)) {
3708
+ const common = Math.min(expected.length, actual.length);
3709
+ for (let i = 0; i < common; i++) {
3710
+ const diff = firstRuntimeValueDifference(expected[i], actual[i], `${path}[${i}]`, depth + 1);
3711
+ if (diff)
3712
+ return diff;
3713
+ }
3714
+ if (expected.length > actual.length) {
3715
+ return `at ${path}[${actual.length}]: missing item; expected ${formatRuntimeValue(expected[actual.length])}`;
3716
+ }
3717
+ return `at ${path}[${expected.length}]: unexpected item ${formatRuntimeValue(actual[expected.length])}`;
3718
+ }
3719
+ if (expected !== null &&
3720
+ actual !== null &&
3721
+ typeof expected === 'object' &&
3722
+ typeof actual === 'object' &&
3723
+ !Array.isArray(expected) &&
3724
+ !Array.isArray(actual)) {
3725
+ const expectedRecord = expected;
3726
+ const actualRecord = actual;
3727
+ const keys = [...new Set([...Object.keys(expectedRecord), ...Object.keys(actualRecord)])].sort();
3728
+ for (const key of keys) {
3729
+ const nextPath = runtimePathProperty(path, key);
3730
+ if (!Object.hasOwn(actualRecord, key)) {
3731
+ return `at ${nextPath}: missing property; expected ${formatRuntimeValue(expectedRecord[key])}`;
3732
+ }
3733
+ if (!Object.hasOwn(expectedRecord, key)) {
3734
+ return `at ${nextPath}: unexpected property ${formatRuntimeValue(actualRecord[key])}`;
3735
+ }
3736
+ const diff = firstRuntimeValueDifference(expectedRecord[key], actualRecord[key], nextPath, depth + 1);
3737
+ if (diff)
3738
+ return diff;
3739
+ }
3740
+ }
3741
+ return `at ${path}: expected ${formatRuntimeValue(expected)}, received ${formatRuntimeValue(actual)}`;
3742
+ }
3743
+ function runtimeMismatchMessage(label, expected, actual, context) {
3744
+ const normalizedExpected = normalizeRuntimeDiffValue(expected);
3745
+ const normalizedActual = normalizeRuntimeDiffValue(actual);
3746
+ const diff = firstRuntimeValueDifference(normalizedExpected, normalizedActual) ||
3747
+ `at $: expected ${formatRuntimeValue(normalizedExpected)}, received ${formatRuntimeValue(normalizedActual)}`;
3748
+ return `${label} expected ${formatRuntimeValue(expected)}, received ${formatRuntimeValue(actual)}${context}\ndiff: ${diff}`;
3749
+ }
1575
3750
  function formatThrownRuntimeError(error) {
1576
3751
  if (error instanceof Error)
1577
3752
  return `${error.name}: ${error.message}`;
@@ -1587,6 +3762,12 @@ function runtimeExpressionContext(expr, fixtures) {
1587
3762
  function runtimeBindingUnsafeReason(binding) {
1588
3763
  if (binding.kind === 'fn')
1589
3764
  return unsafeRuntimeFunctionReason(binding.expr);
3765
+ if (binding.kind === 'class')
3766
+ return unsafeRuntimeClassReason(binding.expr);
3767
+ if (binding.kind === 'workflow')
3768
+ return unsafeRuntimeWorkflowReason(binding.expr);
3769
+ if (binding.kind === 'native')
3770
+ return unsafeRuntimeNativeBindingReason(binding.expr);
1590
3771
  return unsafeRuntimeExpressionReason(binding.expr);
1591
3772
  }
1592
3773
  function thrownRuntimeErrorMatches(error, expected) {
@@ -1686,12 +3867,14 @@ function runtimeContext() {
1686
3867
  Boolean,
1687
3868
  Error,
1688
3869
  JSON,
3870
+ Map,
1689
3871
  Math,
1690
3872
  Number,
1691
3873
  Object,
1692
3874
  Promise,
1693
3875
  RangeError,
1694
3876
  ReferenceError,
3877
+ Set,
1695
3878
  String,
1696
3879
  SyntaxError,
1697
3880
  TypeError,
@@ -1785,7 +3968,7 @@ function evaluateRuntimeThrows(node, target, declarations, expr, fixtures, label
1785
3968
  }
1786
3969
  return { passed: true };
1787
3970
  }
1788
- function evaluateRuntimeSource(node, target, expr, fixtures = [], label = 'Runtime expr') {
3971
+ function evaluateRuntimeSource(node, target, expr, fixtures = [], label = 'Runtime expr', expectedPropName = 'equals') {
1789
3972
  const blocking = targetBlockingMessage(target);
1790
3973
  if (blocking)
1791
3974
  return { passed: false, message: blocking };
@@ -1793,7 +3976,9 @@ function evaluateRuntimeSource(node, target, expr, fixtures = [], label = 'Runti
1793
3976
  const trimmedExpr = expr.trim();
1794
3977
  if (!trimmedExpr)
1795
3978
  return { passed: false, message: `${label} assertion requires an executable expression` };
1796
- const expectedSource = runtimeExpectedSource(node, 'equals');
3979
+ const expectedSource = runtimeExpectedSource(node, expectedPropName) ??
3980
+ (expectedPropName === 'equals' ? undefined : runtimeExpectedSource(node, 'equals'));
3981
+ const expectedLabel = expectedPropName !== 'equals' && expectedPropName in props ? expectedPropName : 'equals';
1797
3982
  const expressionSources = expectedSource ? [trimmedExpr, expectedSource] : [trimmedExpr];
1798
3983
  const declarations = buildRuntimeDeclarations(target, expressionSources, fixtures);
1799
3984
  if ('message' in declarations)
@@ -1814,7 +3999,7 @@ function evaluateRuntimeSource(node, target, expr, fixtures = [], label = 'Runti
1814
3999
  if (!expected.ok) {
1815
4000
  return {
1816
4001
  passed: false,
1817
- message: `Runtime expr assertion cannot execute expected equals value: ${formatThrownRuntimeError(expected.error)}`,
4002
+ message: `Runtime expr assertion cannot execute expected ${expectedLabel} value: ${formatThrownRuntimeError(expected.error)}`,
1818
4003
  };
1819
4004
  }
1820
4005
  return runtimeValuesEqual(actual.value, expected.value)
@@ -1822,7 +4007,7 @@ function evaluateRuntimeSource(node, target, expr, fixtures = [], label = 'Runti
1822
4007
  : {
1823
4008
  passed: false,
1824
4009
  message: str(props.message) ||
1825
- `${label} expected ${formatRuntimeValue(expected.value)}, received ${formatRuntimeValue(actual.value)}${runtimeExpressionContext(trimmedExpr, fixtures)}`,
4010
+ runtimeMismatchMessage(label, expected.value, actual.value, runtimeExpressionContext(trimmedExpr, fixtures)),
1826
4011
  };
1827
4012
  }
1828
4013
  if ('matches' in props) {
@@ -1892,13 +4077,383 @@ function evaluateRuntimeBehavior(node, target, fixtures = []) {
1892
4077
  return { passed: false, message: call.message };
1893
4078
  return evaluateRuntimeSource(node, target, call.expr || '', fixtures, `Runtime fn ${fnName}`);
1894
4079
  }
1895
- if (deriveName) {
1896
- if (!targetHasNamedNode(target, 'derive', deriveName)) {
1897
- return { passed: false, message: `Runtime derive assertion target not found: ${deriveName}` };
1898
- }
1899
- return evaluateRuntimeSource(node, target, deriveName, fixtures, `Runtime derive ${deriveName}`);
4080
+ if (deriveName) {
4081
+ if (!targetHasNamedNode(target, 'derive', deriveName)) {
4082
+ return { passed: false, message: `Runtime derive assertion target not found: ${deriveName}` };
4083
+ }
4084
+ return evaluateRuntimeSource(node, target, deriveName, fixtures, `Runtime derive ${deriveName}`);
4085
+ }
4086
+ return { passed: false, message: 'Runtime behavior assertion requires fn=<name> or derive=<name>' };
4087
+ }
4088
+ function evaluateRuntimeRoute(node, target, fixtures = [], mocks = [], mockCalls, effectCalls, guardCalls) {
4089
+ const blocking = targetBlockingMessage(target);
4090
+ if (blocking)
4091
+ return { passed: false, message: blocking };
4092
+ const props = getProps(node);
4093
+ const routeSpec = str(props.route);
4094
+ if (!routeSpec)
4095
+ return { passed: false, message: 'Runtime route assertion requires route="METHOD /path"' };
4096
+ const found = findRuntimeRoute(target, routeSpec);
4097
+ if (found.message || !found.route)
4098
+ return { passed: false, message: found.message || 'Runtime route target not found' };
4099
+ const inputSource = runtimeValuePropSource(node, 'with') || runtimeValuePropSource(node, 'input') || '{}';
4100
+ const inputProblem = unsafeRuntimeExpressionReason(inputSource, { allowAwait: true });
4101
+ if (inputProblem) {
4102
+ return { passed: false, message: `Runtime route assertion cannot execute request input: ${inputProblem}` };
4103
+ }
4104
+ const routeExpr = runtimeRouteExecutionExpr(found.route, inputSource, { mocks, probe: true });
4105
+ if (routeExpr.message || !routeExpr.expr) {
4106
+ return { passed: false, message: routeExpr.message || 'Runtime route assertion cannot build route workflow' };
4107
+ }
4108
+ const routeBinding = {
4109
+ name: runtimeSyntheticName(node, 'Route'),
4110
+ expr: routeExpr.expr,
4111
+ kind: 'workflow',
4112
+ line: node.loc?.line,
4113
+ };
4114
+ const label = `Runtime route ${runtimeRouteLabel(found.route)}`;
4115
+ const expectedSource = runtimeExpectedSource(node, 'returns') ?? runtimeExpectedSource(node, 'equals');
4116
+ const expectedLabel = 'returns' in props ? 'returns' : 'equals';
4117
+ const expressionSources = expectedSource ? [routeBinding.name, expectedSource] : [routeBinding.name];
4118
+ const declarations = buildRuntimeDeclarations(target, expressionSources, [...fixtures, routeBinding]);
4119
+ if ('message' in declarations)
4120
+ return { passed: false, message: declarations.message };
4121
+ const routeContext = runtimeExpressionContext(routeBinding.name, [...fixtures, routeBinding]);
4122
+ const actual = runRuntimeExpression(target, declarations.source, routeBinding.name);
4123
+ if (!actual.ok) {
4124
+ return {
4125
+ passed: false,
4126
+ message: `${label} threw: ${actual.error instanceof Error ? actual.error.message : String(actual.error)}${routeContext}`,
4127
+ };
4128
+ }
4129
+ const probe = actual.value;
4130
+ if (!probe || typeof probe !== 'object' || typeof probe.__kernRouteStatus !== 'string') {
4131
+ return { passed: false, message: `${label} returned an invalid runtime probe` };
4132
+ }
4133
+ mergeRuntimeEffectMockCalls(mockCalls, probe.calls);
4134
+ mergeRuntimeEffectCoverageCalls(effectCalls, probe.effects);
4135
+ mergeRuntimeGuardCoverageCalls(guardCalls, probe.guards);
4136
+ if ('throws' in props) {
4137
+ const expectedRaw = props.throws === true || props.throws === '' ? 'true' : String(props.throws ?? 'true');
4138
+ if (probe.__kernRouteStatus !== 'thrown') {
4139
+ return {
4140
+ passed: false,
4141
+ message: str(props.message) ||
4142
+ `${label} was expected to throw${expectedRaw && expectedRaw !== 'true' ? ` ${expectedRaw}` : ''}, but returned ${formatRuntimeValue(probe.value)}${routeContext}`,
4143
+ };
4144
+ }
4145
+ const error = decodeRuntimeError(probe.error);
4146
+ if (!thrownRuntimeErrorMatches(error, expectedRaw)) {
4147
+ return {
4148
+ passed: false,
4149
+ message: str(props.message) ||
4150
+ `${label} threw ${formatThrownRuntimeError(error)}, expected ${expectedRaw}${routeContext}`,
4151
+ };
4152
+ }
4153
+ return { passed: true };
4154
+ }
4155
+ if (probe.__kernRouteStatus === 'thrown') {
4156
+ return {
4157
+ passed: false,
4158
+ message: `${label} threw: ${formatThrownRuntimeError(decodeRuntimeError(probe.error))}${routeContext}`,
4159
+ };
4160
+ }
4161
+ if (expectedSource !== undefined) {
4162
+ const expected = runRuntimeExpression(target, declarations.source, expectedSource);
4163
+ if (!expected.ok) {
4164
+ return {
4165
+ passed: false,
4166
+ message: `Runtime route assertion cannot execute expected ${expectedLabel} value: ${formatThrownRuntimeError(expected.error)}${routeContext}`,
4167
+ };
4168
+ }
4169
+ return runtimeValuesEqual(probe.value, expected.value)
4170
+ ? { passed: true }
4171
+ : {
4172
+ passed: false,
4173
+ message: str(props.message) || runtimeMismatchMessage(label, expected.value, probe.value, routeContext),
4174
+ };
4175
+ }
4176
+ if ('matches' in props) {
4177
+ const pattern = runtimePatternValue(node, 'matches') || '';
4178
+ try {
4179
+ const regex = new RegExp(pattern);
4180
+ return regex.test(String(probe.value))
4181
+ ? { passed: true }
4182
+ : {
4183
+ passed: false,
4184
+ message: str(props.message) ||
4185
+ `${label} value ${formatRuntimeValue(probe.value)} does not match /${pattern}/${routeContext}`,
4186
+ };
4187
+ }
4188
+ catch (error) {
4189
+ return {
4190
+ passed: false,
4191
+ message: `Runtime route assertion has invalid matches regex: ${error instanceof Error ? error.message : String(error)}`,
4192
+ };
4193
+ }
4194
+ }
4195
+ return probe.value
4196
+ ? { passed: true }
4197
+ : { passed: false, message: str(props.message) || `${label} evaluated false${routeContext}` };
4198
+ }
4199
+ function evaluateRuntimeTool(node, target, fixtures = [], mocks = [], mockCalls, effectCalls, guardCalls) {
4200
+ const blocking = targetBlockingMessage(target);
4201
+ if (blocking)
4202
+ return { passed: false, message: blocking };
4203
+ const props = getProps(node);
4204
+ const toolName = str(props.tool);
4205
+ if (!toolName)
4206
+ return { passed: false, message: 'Runtime tool assertion requires tool=<name>' };
4207
+ const found = findRuntimeTool(target, toolName);
4208
+ if (found.message || !found.tool) {
4209
+ return { passed: false, message: found.message || 'Runtime tool target not found' };
4210
+ }
4211
+ const inputSource = runtimeValuePropSource(node, 'with') || runtimeValuePropSource(node, 'input') || '{}';
4212
+ const inputProblem = unsafeRuntimeExpressionReason(inputSource, { allowAwait: true });
4213
+ if (inputProblem) {
4214
+ return { passed: false, message: `Runtime tool assertion cannot execute input: ${inputProblem}` };
4215
+ }
4216
+ const toolExpr = runtimeToolExecutionExpr(found.tool, inputSource, { mocks, probe: true });
4217
+ if (toolExpr.message || !toolExpr.expr) {
4218
+ return { passed: false, message: toolExpr.message || 'Runtime tool assertion cannot build tool workflow' };
4219
+ }
4220
+ const toolBinding = {
4221
+ name: runtimeSyntheticName(node, 'Tool'),
4222
+ expr: toolExpr.expr,
4223
+ kind: 'workflow',
4224
+ line: node.loc?.line,
4225
+ };
4226
+ const label = `Runtime tool ${runtimeToolName(found.tool)}`;
4227
+ const expectedSource = runtimeExpectedSource(node, 'returns') ?? runtimeExpectedSource(node, 'equals');
4228
+ const expectedLabel = 'returns' in props ? 'returns' : 'equals';
4229
+ const expressionSources = expectedSource ? [toolBinding.name, expectedSource] : [toolBinding.name];
4230
+ const declarations = buildRuntimeDeclarations(target, expressionSources, [...fixtures, toolBinding]);
4231
+ if ('message' in declarations)
4232
+ return { passed: false, message: declarations.message };
4233
+ const toolContext = runtimeExpressionContext(toolBinding.name, [...fixtures, toolBinding]);
4234
+ const actual = runRuntimeExpression(target, declarations.source, toolBinding.name);
4235
+ if (!actual.ok) {
4236
+ return {
4237
+ passed: false,
4238
+ message: `${label} threw: ${actual.error instanceof Error ? actual.error.message : String(actual.error)}${toolContext}`,
4239
+ };
4240
+ }
4241
+ const probe = actual.value;
4242
+ if (!probe || typeof probe !== 'object' || typeof probe.__kernToolStatus !== 'string') {
4243
+ return { passed: false, message: `${label} returned an invalid runtime probe` };
4244
+ }
4245
+ mergeRuntimeEffectMockCalls(mockCalls, probe.calls);
4246
+ mergeRuntimeEffectCoverageCalls(effectCalls, probe.effects);
4247
+ mergeRuntimeGuardCoverageCalls(guardCalls, probe.guards);
4248
+ if ('throws' in props) {
4249
+ const expectedRaw = props.throws === true || props.throws === '' ? 'true' : String(props.throws ?? 'true');
4250
+ if (probe.__kernToolStatus !== 'thrown') {
4251
+ return {
4252
+ passed: false,
4253
+ message: str(props.message) ||
4254
+ `${label} was expected to throw${expectedRaw && expectedRaw !== 'true' ? ` ${expectedRaw}` : ''}, but returned ${formatRuntimeValue(probe.value)}${toolContext}`,
4255
+ };
4256
+ }
4257
+ const error = decodeRuntimeError(probe.error);
4258
+ if (!thrownRuntimeErrorMatches(error, expectedRaw)) {
4259
+ return {
4260
+ passed: false,
4261
+ message: str(props.message) ||
4262
+ `${label} threw ${formatThrownRuntimeError(error)}, expected ${expectedRaw}${toolContext}`,
4263
+ };
4264
+ }
4265
+ return { passed: true };
4266
+ }
4267
+ if (probe.__kernToolStatus === 'thrown') {
4268
+ return {
4269
+ passed: false,
4270
+ message: `${label} threw: ${formatThrownRuntimeError(decodeRuntimeError(probe.error))}${toolContext}`,
4271
+ };
4272
+ }
4273
+ if (expectedSource !== undefined) {
4274
+ const expected = runRuntimeExpression(target, declarations.source, expectedSource);
4275
+ if (!expected.ok) {
4276
+ return {
4277
+ passed: false,
4278
+ message: `Runtime tool assertion cannot execute expected ${expectedLabel} value: ${formatThrownRuntimeError(expected.error)}${toolContext}`,
4279
+ };
4280
+ }
4281
+ return runtimeValuesEqual(probe.value, expected.value)
4282
+ ? { passed: true }
4283
+ : {
4284
+ passed: false,
4285
+ message: str(props.message) || runtimeMismatchMessage(label, expected.value, probe.value, toolContext),
4286
+ };
4287
+ }
4288
+ if ('matches' in props) {
4289
+ const pattern = runtimePatternValue(node, 'matches') || '';
4290
+ try {
4291
+ const regex = new RegExp(pattern);
4292
+ return regex.test(String(probe.value))
4293
+ ? { passed: true }
4294
+ : {
4295
+ passed: false,
4296
+ message: str(props.message) ||
4297
+ `${label} value ${formatRuntimeValue(probe.value)} does not match /${pattern}/${toolContext}`,
4298
+ };
4299
+ }
4300
+ catch (error) {
4301
+ return {
4302
+ passed: false,
4303
+ message: `Runtime tool assertion has invalid matches regex: ${error instanceof Error ? error.message : String(error)}`,
4304
+ };
4305
+ }
4306
+ }
4307
+ return probe.value
4308
+ ? { passed: true }
4309
+ : { passed: false, message: str(props.message) || `${label} evaluated false${toolContext}` };
4310
+ }
4311
+ function findRuntimeEffect(target, effectName) {
4312
+ const effects = collectNodes(target.root, 'effect').filter((effect) => runtimeEffectName(effect) === effectName);
4313
+ if (effects.length === 1)
4314
+ return { effect: effects[0] };
4315
+ if (effects.length === 0)
4316
+ return { message: `Runtime effect assertion target not found: ${effectName}` };
4317
+ return { message: `Runtime effect assertion target is ambiguous: ${effectName}` };
4318
+ }
4319
+ function runtimeEffectExpectedSource(node) {
4320
+ const fallback = runtimeExpectedSource(node, 'fallback');
4321
+ if (fallback !== undefined)
4322
+ return { source: fallback, label: 'fallback' };
4323
+ const returns = runtimeExpectedSource(node, 'returns');
4324
+ if (returns !== undefined)
4325
+ return { source: returns, label: 'returns' };
4326
+ const equals = runtimeExpectedSource(node, 'equals');
4327
+ if (equals !== undefined)
4328
+ return { source: equals, label: 'equals' };
4329
+ return {};
4330
+ }
4331
+ function evaluateRuntimeEffectRecovery(node, target, effect, fixtures, mocks = [], mockCalls) {
4332
+ const props = getProps(node);
4333
+ const effectName = runtimeEffectName(effect);
4334
+ const scopedMock = findRuntimeEffectMock(mocks, effectName);
4335
+ if (scopedMock.message)
4336
+ return { passed: false, message: scopedMock.message };
4337
+ const effectExpr = scopedMock.mock
4338
+ ? runtimeEffectMockExecutionExpr(scopedMock.mock, { portable: true, meta: true })
4339
+ : runtimeEffectExecutionExpr(effect, { portable: true, meta: true });
4340
+ if (effectExpr.message || !effectExpr.expr) {
4341
+ return { passed: false, message: effectExpr.message || `Runtime effect ${effectName} cannot be simulated` };
4342
+ }
4343
+ const effectBinding = {
4344
+ name: runtimeSyntheticName(node, 'Effect'),
4345
+ expr: effectExpr.expr,
4346
+ kind: 'workflow',
4347
+ line: node.loc?.line,
4348
+ };
4349
+ const expected = runtimeEffectExpectedSource(node);
4350
+ const entryExprs = expected.source ? [effectBinding.name, expected.source] : [effectBinding.name];
4351
+ const declarations = buildRuntimeDeclarations(target, entryExprs, [...fixtures, effectBinding]);
4352
+ if ('message' in declarations)
4353
+ return { passed: false, message: declarations.message };
4354
+ const effectContext = runtimeExpressionContext(effectBinding.name, [...fixtures, effectBinding]);
4355
+ const actual = runRuntimeExpression(target, declarations.source, effectBinding.name);
4356
+ if (scopedMock.mock)
4357
+ recordRuntimeEffectMockCall(scopedMock.mock, mockCalls);
4358
+ if (!actual.ok) {
4359
+ return {
4360
+ passed: false,
4361
+ message: `Runtime effect ${effectName} threw: ${formatThrownRuntimeError(actual.error)}`,
4362
+ };
4363
+ }
4364
+ const meta = actual.value;
4365
+ if (isTruthy(props.recovers) && meta.recovered !== true) {
4366
+ return {
4367
+ passed: false,
4368
+ message: str(props.message) || `Runtime effect ${effectName} was expected to recover, but completed without recovery`,
4369
+ };
4370
+ }
4371
+ if (expected.source !== undefined) {
4372
+ const expectedResult = runRuntimeExpression(target, declarations.source, expected.source);
4373
+ if (!expectedResult.ok) {
4374
+ return {
4375
+ passed: false,
4376
+ message: `Runtime effect assertion cannot execute expected ${expected.label} value: ${formatThrownRuntimeError(expectedResult.error)}`,
4377
+ };
4378
+ }
4379
+ return runtimeValuesEqual(meta.result, expectedResult.value)
4380
+ ? { passed: true }
4381
+ : {
4382
+ passed: false,
4383
+ message: str(props.message) ||
4384
+ runtimeMismatchMessage(`Runtime effect ${effectName}`, expectedResult.value, meta.result, effectContext),
4385
+ };
4386
+ }
4387
+ return { passed: true };
4388
+ }
4389
+ function evaluateRuntimeEffect(node, target, fixtures = [], mocks = [], mockCalls) {
4390
+ const blocking = targetBlockingMessage(target);
4391
+ if (blocking)
4392
+ return { passed: false, message: blocking };
4393
+ const props = getProps(node);
4394
+ const effectName = str(props.effect);
4395
+ if (!effectName)
4396
+ return { passed: false, message: 'Runtime effect assertion requires effect=<name>' };
4397
+ const found = findRuntimeEffect(target, effectName);
4398
+ if (found.message || !found.effect) {
4399
+ return { passed: false, message: found.message || 'Runtime effect target not found' };
4400
+ }
4401
+ if (isTruthy(props.recovers)) {
4402
+ return evaluateRuntimeEffectRecovery(node, target, found.effect, fixtures, mocks, mockCalls);
4403
+ }
4404
+ const scopedMock = findRuntimeEffectMock(mocks, effectName);
4405
+ if (scopedMock.message)
4406
+ return { passed: false, message: scopedMock.message };
4407
+ const effectExpr = scopedMock.mock
4408
+ ? runtimeEffectMockExecutionExpr(scopedMock.mock, { portable: true })
4409
+ : runtimeEffectExecutionExpr(found.effect, { portable: true });
4410
+ if (effectExpr.message || !effectExpr.expr) {
4411
+ return { passed: false, message: effectExpr.message || `Runtime effect ${effectName} cannot be simulated` };
4412
+ }
4413
+ const effectBinding = {
4414
+ name: runtimeSyntheticName(node, 'Effect'),
4415
+ expr: effectExpr.expr,
4416
+ kind: 'workflow',
4417
+ line: node.loc?.line,
4418
+ };
4419
+ const evaluated = evaluateRuntimeSource(node, target, effectBinding.name, [...fixtures, effectBinding], `Runtime effect ${effectName}`, 'returns');
4420
+ if (scopedMock.mock && !isAssertionConfigurationFailure(evaluated.message)) {
4421
+ recordRuntimeEffectMockCall(scopedMock.mock, mockCalls);
4422
+ }
4423
+ return evaluated;
4424
+ }
4425
+ function expectedMockCallCount(node) {
4426
+ const raw = getProps(node).called;
4427
+ const count = Number(raw);
4428
+ if (raw === '' || !Number.isInteger(count) || count < 0) {
4429
+ return { message: `Runtime mock call assertion requires called=<non-negative integer>, got ${String(raw)}` };
1900
4430
  }
1901
- return { passed: false, message: 'Runtime behavior assertion requires fn=<name> or derive=<name>' };
4431
+ return { count };
4432
+ }
4433
+ function evaluateRuntimeMockCall(node, mocks = [], mockCalls, checkedMocks) {
4434
+ const props = getProps(node);
4435
+ const effectName = str(props.mock);
4436
+ if (!effectName)
4437
+ return { passed: false, message: 'Runtime mock call assertion requires mock=<effect>' };
4438
+ if (!('called' in props))
4439
+ return { passed: false, message: 'Runtime mock call assertion requires called=<count>' };
4440
+ const expected = expectedMockCallCount(node);
4441
+ if (expected.message || expected.count === undefined)
4442
+ return { passed: false, message: expected.message };
4443
+ const scopedMock = findRuntimeEffectMock(mocks, effectName);
4444
+ if (scopedMock.message)
4445
+ return { passed: false, message: scopedMock.message };
4446
+ if (!scopedMock.mock)
4447
+ return { passed: false, message: `Runtime mock assertion target not found: ${effectName}` };
4448
+ markRuntimeEffectMockChecked(scopedMock.mock, checkedMocks);
4449
+ const actual = mockCalls?.get(scopedMock.mock.id) || 0;
4450
+ return actual === expected.count
4451
+ ? { passed: true }
4452
+ : {
4453
+ passed: false,
4454
+ message: str(props.message) ||
4455
+ `Native effect mock effect=${effectName} expected called=${expected.count}, received called=${actual}`,
4456
+ };
1902
4457
  }
1903
4458
  function nodeSearchText(node) {
1904
4459
  const props = getProps(node);
@@ -2341,12 +4896,235 @@ function evaluateNoInvariant(node, target, context) {
2341
4896
  return untested.length > 0
2342
4897
  ? {
2343
4898
  passed: false,
2344
- message: `Found untested guards: ${untested.join('; ')}. Add expect guard=<name> exhaustive=true or a guard-wide assertion such as expect preset=guard.`,
4899
+ message: `Found untested guards: ${untested.join('; ')}. Add expect guard=<name> exhaustive=true, a route/tool workflow assertion that executes the guard, or a guard-wide assertion such as expect preset=guard.`,
4900
+ }
4901
+ : { passed: true };
4902
+ }
4903
+ if (invariant === 'untestedroutes' || invariant === 'uncoveredroutes') {
4904
+ const blocking = targetBlockingMessage(target);
4905
+ if (blocking)
4906
+ return { passed: false, message: blocking };
4907
+ const untested = findUntestedRoutes(target.root, context);
4908
+ return untested.length > 0
4909
+ ? {
4910
+ passed: false,
4911
+ message: `Found untested routes: ${untested.join('; ')}. Add expect route="METHOD /path" assertions for each route.`,
4912
+ }
4913
+ : { passed: true };
4914
+ }
4915
+ if (invariant === 'untestedtools' || invariant === 'uncoveredtools') {
4916
+ const blocking = targetBlockingMessage(target);
4917
+ if (blocking)
4918
+ return { passed: false, message: blocking };
4919
+ const untested = findUntestedTools(target.root, context);
4920
+ return untested.length > 0
4921
+ ? {
4922
+ passed: false,
4923
+ message: `Found untested tools: ${untested.join('; ')}. Add expect tool=<name> assertions for each MCP tool.`,
4924
+ }
4925
+ : { passed: true };
4926
+ }
4927
+ if (invariant === 'untestedeffects' || invariant === 'uncoveredeffects') {
4928
+ const blocking = targetBlockingMessage(target);
4929
+ if (blocking)
4930
+ return { passed: false, message: blocking };
4931
+ const untested = findUntestedEffects(target.root, context);
4932
+ return untested.length > 0
4933
+ ? {
4934
+ passed: false,
4935
+ message: `Found untested effects: ${untested.join('; ')}. Add expect effect=<name> assertions for deterministic effects.`,
2345
4936
  }
2346
4937
  : { passed: true };
2347
4938
  }
2348
4939
  return { passed: false, message: `Unsupported native invariant: no=${str(getProps(node).no)}` };
2349
4940
  }
4941
+ function diagnosticFinding(file, issue) {
4942
+ return `${file}:${issue.line ?? 1}:${issue.col ?? 1}: ${issue.message}`;
4943
+ }
4944
+ function nativeInvariantFindings(node, target, context) {
4945
+ if (target.readError)
4946
+ return { message: target.readError };
4947
+ const props = getProps(node);
4948
+ const invariant = normalizeInvariant(str(props.has) || str(props.no));
4949
+ const machineName = str(props.machine) || undefined;
4950
+ if (invariant === 'parseerrors') {
4951
+ return {
4952
+ findings: target.diagnostics
4953
+ .filter((diagnostic) => diagnostic.severity === 'error')
4954
+ .map((diagnostic) => diagnosticFinding(target.file, diagnostic)),
4955
+ };
4956
+ }
4957
+ if (invariant === 'schemaviolations') {
4958
+ return { findings: target.schemaViolations.map((violation) => diagnosticFinding(target.file, violation)) };
4959
+ }
4960
+ if (invariant === 'semanticviolations') {
4961
+ return { findings: target.semanticViolations.map((violation) => diagnosticFinding(target.file, violation)) };
4962
+ }
4963
+ const blocking = targetBlockingMessage(target);
4964
+ if (blocking)
4965
+ return { message: blocking };
4966
+ const root = target.root;
4967
+ if (invariant === 'codegenerrors' || invariant === 'codegenerationerrors' || invariant === 'compileerrors') {
4968
+ return { findings: findCodegenErrors(root) };
4969
+ }
4970
+ if (invariant === 'cycles' || invariant === 'derivecycles') {
4971
+ return { findings: findDeriveCycles(root).map((cycle) => cycle.join(' -> ')) };
4972
+ }
4973
+ if (invariant === 'deadstates' || invariant === 'unreachablestates') {
4974
+ return { findings: findUnreachableStates(root, machineName) };
4975
+ }
4976
+ if (invariant === 'duplicatetransitions') {
4977
+ return { findings: findDuplicateTransitions(root, machineName) };
4978
+ }
4979
+ if (invariant === 'duplicateroutes') {
4980
+ return { findings: findDuplicateRoutes(root) };
4981
+ }
4982
+ if (invariant === 'emptyroutes' || invariant === 'missingroutehandlers' || invariant === 'missingrouteresponses') {
4983
+ return { findings: findEmptyRoutes(root) };
4984
+ }
4985
+ if (invariant === 'duplicatenames' || invariant === 'duplicatesiblingnames') {
4986
+ return { findings: findDuplicateSiblingNames(root) };
4987
+ }
4988
+ if (invariant === 'weakguards' || invariant === 'guardwithoutelse' || invariant === 'guardswithoutelse') {
4989
+ return { findings: findWeakGuards(root) };
4990
+ }
4991
+ if (invariant === 'nonexhaustiveguards' || invariant === 'guardexhaustiveness' || invariant === 'exhaustiveguards') {
4992
+ return { findings: findNonExhaustiveGuards(root) };
4993
+ }
4994
+ if (invariant === 'unguardedeffects') {
4995
+ return { findings: findUnguardedEffects(root) };
4996
+ }
4997
+ if (invariant === 'unvalidatedroutes' || invariant === 'unguardedmutatingroutes') {
4998
+ return { findings: findUnvalidatedMutatingRoutes(root) };
4999
+ }
5000
+ if (invariant === 'rawhandlers' || invariant === 'handlerescapes') {
5001
+ return { findings: findRawHandlerEscapes(root) };
5002
+ }
5003
+ if (invariant === 'invalidguards' || invariant === 'guardmisconfigurations') {
5004
+ return { findings: findInvalidGuards(root) };
5005
+ }
5006
+ if (invariant === 'duplicateparams' || invariant === 'duplicateparameters') {
5007
+ return { findings: findDuplicateParams(root) };
5008
+ }
5009
+ if (invariant === 'unguardedtoolparams' || invariant === 'unguardedrequiredparams') {
5010
+ return { findings: findUnguardedRequiredToolParams(root) };
5011
+ }
5012
+ if (invariant === 'missingpathguards' || invariant === 'pathparamguards') {
5013
+ return { findings: findMissingPathGuards(root) };
5014
+ }
5015
+ if (invariant === 'ssrfrisks' || invariant === 'ssrf') {
5016
+ return { findings: findSsrfRisks(root) };
5017
+ }
5018
+ if (invariant === 'sensitiveeffectsrequireauth' || invariant === 'missingeffectauth' || invariant === 'missingauth') {
5019
+ return { findings: findSensitiveEffectsWithoutAuth(root) };
5020
+ }
5021
+ if (invariant === 'uncheckedroutepathparams' || invariant === 'routepathparams') {
5022
+ return { findings: findUncheckedRoutePathParams(root) };
5023
+ }
5024
+ if (invariant === 'effectwithoutcleanup' || invariant === 'effectcleanup') {
5025
+ return { findings: findEffectsWithoutCleanup(root) };
5026
+ }
5027
+ if (invariant === 'unrecoveredasync' || invariant === 'asyncrecover') {
5028
+ return { findings: findUnrecoveredAsync(root) };
5029
+ }
5030
+ if (invariant === 'untestedtransitions' || invariant === 'uncoveredtransitions') {
5031
+ return { findings: findUntestedTransitions(root, context, machineName) };
5032
+ }
5033
+ if (invariant === 'untestedguards' || invariant === 'uncoveredguards') {
5034
+ return { findings: findUntestedGuards(root, context) };
5035
+ }
5036
+ if (invariant === 'untestedroutes' || invariant === 'uncoveredroutes') {
5037
+ return { findings: findUntestedRoutes(root, context) };
5038
+ }
5039
+ if (invariant === 'untestedtools' || invariant === 'uncoveredtools') {
5040
+ return { findings: findUntestedTools(root, context) };
5041
+ }
5042
+ if (invariant === 'untestedeffects' || invariant === 'uncoveredeffects') {
5043
+ return { findings: findUntestedEffects(root, context) };
5044
+ }
5045
+ const propName = 'has' in props ? 'has' : 'no';
5046
+ return { message: `Unsupported native invariant: ${propName}=${str(props.has) || str(props.no)}` };
5047
+ }
5048
+ function evaluateHasInvariant(node, target, context) {
5049
+ const props = getProps(node);
5050
+ const invariant = str(props.has);
5051
+ const normalized = normalizeInvariant(invariant);
5052
+ const expectedCount = props.count === undefined || props.count === '' ? undefined : Number(props.count);
5053
+ if (expectedCount !== undefined && (!Number.isInteger(expectedCount) || expectedCount < 0)) {
5054
+ return {
5055
+ passed: false,
5056
+ message: `Native has assertion count must be a non-negative integer: ${String(props.count)}`,
5057
+ };
5058
+ }
5059
+ if (expectedCount !== undefined) {
5060
+ const collected = nativeInvariantFindings(node, target, context);
5061
+ if (collected.message)
5062
+ return { passed: false, message: collected.message };
5063
+ const findings = collected.findings || [];
5064
+ if (findings.length !== expectedCount) {
5065
+ const details = findings.length > 0 ? `: ${findings.slice(0, 10).join('; ')}` : '';
5066
+ const extra = findings.length > 10 ? `; +${findings.length - 10} more` : '';
5067
+ return {
5068
+ passed: false,
5069
+ message: `Expected target to have ${invariant || '<missing>'} count ${expectedCount}, found ${findings.length}${details}${extra}`,
5070
+ };
5071
+ }
5072
+ if ('matches' in props) {
5073
+ const pattern = runtimePatternValue(node, 'matches') || '';
5074
+ const message = findings.join('; ');
5075
+ try {
5076
+ const regex = new RegExp(pattern);
5077
+ return regex.test(message)
5078
+ ? { passed: true }
5079
+ : {
5080
+ passed: false,
5081
+ message: `Expected ${invariant || '<missing>'} findings to match /${pattern}/, got: ${message || '<none>'}`,
5082
+ };
5083
+ }
5084
+ catch (error) {
5085
+ return {
5086
+ passed: false,
5087
+ message: `Native has assertion has invalid matches regex: ${error instanceof Error ? error.message : String(error)}`,
5088
+ };
5089
+ }
5090
+ }
5091
+ return { passed: true };
5092
+ }
5093
+ if (!['parseerrors', 'schemaviolations', 'semanticviolations'].includes(normalized)) {
5094
+ const blocking = targetBlockingMessage(target);
5095
+ if (blocking)
5096
+ return { passed: false, message: blocking };
5097
+ }
5098
+ const evaluated = evaluateNoInvariant(nodeWithProps(node, { ...props, no: invariant }), target, context);
5099
+ if (isAssertionConfigurationFailure(evaluated.message)) {
5100
+ return { passed: false, message: evaluated.message };
5101
+ }
5102
+ if (evaluated.passed) {
5103
+ return {
5104
+ passed: false,
5105
+ message: `Expected target to have ${invariant || '<missing>'}, but none was found`,
5106
+ };
5107
+ }
5108
+ if ('matches' in props) {
5109
+ const pattern = runtimePatternValue(node, 'matches') || '';
5110
+ try {
5111
+ const regex = new RegExp(pattern);
5112
+ return regex.test(evaluated.message || '')
5113
+ ? { passed: true }
5114
+ : {
5115
+ passed: false,
5116
+ message: `Expected ${invariant || '<missing>'} message to match /${pattern}/, got: ${evaluated.message || '<none>'}`,
5117
+ };
5118
+ }
5119
+ catch (error) {
5120
+ return {
5121
+ passed: false,
5122
+ message: `Native has assertion has invalid matches regex: ${error instanceof Error ? error.message : String(error)}`,
5123
+ };
5124
+ }
5125
+ }
5126
+ return { passed: true };
5127
+ }
2350
5128
  function evaluateMachineReachability(node, target) {
2351
5129
  const blocking = targetBlockingMessage(target);
2352
5130
  if (blocking)
@@ -2558,6 +5336,54 @@ function evaluatePresetAssertion(node, target, context) {
2558
5336
  }
2559
5337
  function evaluateNativeAssertion(node, target, context) {
2560
5338
  const props = getProps(node);
5339
+ if ('decompile' in props) {
5340
+ const evaluated = evaluateDecompileAssertion(node, target);
5341
+ return [
5342
+ {
5343
+ ruleId: 'decompile',
5344
+ assertion: assertionLabel(node),
5345
+ passed: evaluated.passed,
5346
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
5347
+ ...(evaluated.message ? { message: evaluated.message } : {}),
5348
+ },
5349
+ ];
5350
+ }
5351
+ if ('import' in props) {
5352
+ const evaluated = evaluateImportAssertion(node, context);
5353
+ return [
5354
+ {
5355
+ ruleId: 'import',
5356
+ assertion: assertionLabel(node),
5357
+ passed: evaluated.passed,
5358
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
5359
+ ...(evaluated.message ? { message: evaluated.message } : {}),
5360
+ },
5361
+ ];
5362
+ }
5363
+ if ('roundtrip' in props) {
5364
+ const evaluated = evaluateRoundtripAssertion(node, target);
5365
+ return [
5366
+ {
5367
+ ruleId: 'roundtrip',
5368
+ assertion: assertionLabel(node),
5369
+ passed: evaluated.passed,
5370
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
5371
+ ...(evaluated.message ? { message: evaluated.message } : {}),
5372
+ },
5373
+ ];
5374
+ }
5375
+ if ('codegen' in props) {
5376
+ const evaluated = evaluateCodegenAssertion(node, target);
5377
+ return [
5378
+ {
5379
+ ruleId: 'codegen',
5380
+ assertion: assertionLabel(node),
5381
+ passed: evaluated.passed,
5382
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
5383
+ ...(evaluated.message ? { message: evaluated.message } : {}),
5384
+ },
5385
+ ];
5386
+ }
2561
5387
  if ('preset' in props)
2562
5388
  return evaluatePresetAssertion(node, target, context);
2563
5389
  if ('node' in props) {
@@ -2584,6 +5410,18 @@ function evaluateNativeAssertion(node, target, context) {
2584
5410
  },
2585
5411
  ];
2586
5412
  }
5413
+ if ('has' in props) {
5414
+ const evaluated = evaluateHasInvariant(node, target, context);
5415
+ return [
5416
+ {
5417
+ ruleId: hasInvariantRuleId(str(props.has)),
5418
+ assertion: assertionLabel(node),
5419
+ passed: evaluated.passed,
5420
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
5421
+ ...(evaluated.message ? { message: evaluated.message } : {}),
5422
+ },
5423
+ ];
5424
+ }
2587
5425
  if ('guard' in props) {
2588
5426
  const evaluated = evaluateGuardExhaustiveness(node, target);
2589
5427
  return [
@@ -2632,6 +5470,54 @@ function evaluateNativeAssertion(node, target, context) {
2632
5470
  },
2633
5471
  ];
2634
5472
  }
5473
+ if ('route' in props) {
5474
+ const evaluated = evaluateRuntimeRoute(node, target, context?.fixtures || [], context?.mocks || [], context?.mockCalls);
5475
+ return [
5476
+ {
5477
+ ruleId: 'runtime:route',
5478
+ assertion: assertionLabel(node),
5479
+ passed: evaluated.passed,
5480
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
5481
+ ...(evaluated.message ? { message: evaluated.message } : {}),
5482
+ },
5483
+ ];
5484
+ }
5485
+ if ('tool' in props) {
5486
+ const evaluated = evaluateRuntimeTool(node, target, context?.fixtures || [], context?.mocks || [], context?.mockCalls);
5487
+ return [
5488
+ {
5489
+ ruleId: 'runtime:tool',
5490
+ assertion: assertionLabel(node),
5491
+ passed: evaluated.passed,
5492
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
5493
+ ...(evaluated.message ? { message: evaluated.message } : {}),
5494
+ },
5495
+ ];
5496
+ }
5497
+ if ('effect' in props) {
5498
+ const evaluated = evaluateRuntimeEffect(node, target, context?.fixtures || [], context?.mocks || [], context?.mockCalls);
5499
+ return [
5500
+ {
5501
+ ruleId: 'runtime:effect',
5502
+ assertion: assertionLabel(node),
5503
+ passed: evaluated.passed,
5504
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
5505
+ ...(evaluated.message ? { message: evaluated.message } : {}),
5506
+ },
5507
+ ];
5508
+ }
5509
+ if ('mock' in props || 'called' in props) {
5510
+ const evaluated = evaluateRuntimeMockCall(node, context?.mocks || [], context?.mockCalls, context?.checkedMocks);
5511
+ return [
5512
+ {
5513
+ ruleId: 'mock:called',
5514
+ assertion: assertionLabel(node),
5515
+ passed: evaluated.passed,
5516
+ ...(isAssertionConfigurationFailure(evaluated.message) ? { severity: 'error' } : {}),
5517
+ ...(evaluated.message ? { message: evaluated.message } : {}),
5518
+ },
5519
+ ];
5520
+ }
2635
5521
  if ('expr' in props) {
2636
5522
  const evaluated = evaluateRuntimeExpression(node, target, context?.fixtures || []);
2637
5523
  return [
@@ -2710,6 +5596,8 @@ export function runNativeKernTests(file, options = {}) {
2710
5596
  const testDoc = loadKernDocument(inputPath);
2711
5597
  const results = [];
2712
5598
  const targetFiles = new Set();
5599
+ const coverageExcludedTargetFiles = new Set();
5600
+ const coverageIncludedTargetFiles = new Set();
2713
5601
  if (testDoc.readError) {
2714
5602
  results.push(issueResult(inputPath, testDoc.readError));
2715
5603
  return summarizeNativeTestRun(inputPath, targetFiles, results);
@@ -2727,7 +5615,10 @@ export function runNativeKernTests(file, options = {}) {
2727
5615
  const testNodes = collectNodes(testDoc.root, 'test');
2728
5616
  const targetCache = new Map([[inputPath, testDoc]]);
2729
5617
  const assertionsByTarget = new Map();
2730
- const summarize = () => summarizeNativeTestRun(inputPath, targetFiles, results, createCoverageSummary([...targetFiles].sort().map((targetFile) => {
5618
+ const summarize = () => summarizeNativeTestRun(inputPath, targetFiles, results, createCoverageSummary([...targetFiles]
5619
+ .sort()
5620
+ .filter((targetFile) => !coverageExcludedTargetFiles.has(targetFile) || coverageIncludedTargetFiles.has(targetFile))
5621
+ .map((targetFile) => {
2731
5622
  const target = targetCache.get(targetFile);
2732
5623
  return coverageForTarget(target || {
2733
5624
  file: targetFile,
@@ -2742,12 +5633,25 @@ export function runNativeKernTests(file, options = {}) {
2742
5633
  const targetProp = str(getProps(testNode).target);
2743
5634
  const targetPath = targetProp ? resolve(dirname(inputPath), targetProp) : inputPath;
2744
5635
  targetFiles.add(targetPath);
5636
+ if (testNodeContributesCoverage(testNode)) {
5637
+ coverageIncludedTargetFiles.add(targetPath);
5638
+ }
5639
+ else {
5640
+ coverageExcludedTargetFiles.add(targetPath);
5641
+ }
2745
5642
  let target = targetCache.get(targetPath);
2746
5643
  if (!target) {
2747
5644
  target = loadKernDocument(targetPath);
2748
5645
  targetCache.set(targetPath, target);
2749
5646
  }
2750
5647
  const assertions = collectAssertions(testNode);
5648
+ const declaredMocks = new Map();
5649
+ for (const assertion of assertions) {
5650
+ for (const mock of assertion.mocks)
5651
+ declaredMocks.set(mock.id, mock);
5652
+ }
5653
+ const mockCalls = new Map();
5654
+ const checkedMocks = new Set();
2751
5655
  assertionsByTarget.set(targetPath, [...(assertionsByTarget.get(targetPath) || []), ...assertions]);
2752
5656
  if (assertions.length === 0) {
2753
5657
  results.push({
@@ -2765,7 +5669,14 @@ export function runNativeKernTests(file, options = {}) {
2765
5669
  continue;
2766
5670
  }
2767
5671
  for (const assertion of assertions) {
2768
- const context = { assertions, fixtures: assertion.fixtures };
5672
+ const context = {
5673
+ assertions,
5674
+ testFile: inputPath,
5675
+ fixtures: assertion.fixtures,
5676
+ mocks: assertion.mocks,
5677
+ mockCalls,
5678
+ checkedMocks,
5679
+ };
2769
5680
  const requestedSeverity = severityFromNode(assertion.node);
2770
5681
  for (const evaluated of evaluateNativeAssertion(assertion.node, target, context)) {
2771
5682
  const severity = effectiveSeverity(requestedSeverity, evaluated);
@@ -2789,6 +5700,27 @@ export function runNativeKernTests(file, options = {}) {
2789
5700
  }
2790
5701
  }
2791
5702
  }
5703
+ for (const mock of declaredMocks.values()) {
5704
+ if ((mockCalls.get(mock.id) || 0) > 0 || checkedMocks.has(mock.id))
5705
+ continue;
5706
+ const result = {
5707
+ suite,
5708
+ caseName: 'mock usage',
5709
+ ruleId: 'mock:unused',
5710
+ assertion: `mock effect ${mock.effect}`,
5711
+ severity: 'error',
5712
+ status: 'failed',
5713
+ message: `Native effect mock effect=${mock.effect} was declared but never used`,
5714
+ file: inputPath,
5715
+ line: mock.line,
5716
+ col: mock.col,
5717
+ };
5718
+ if (!grepMatches(options, result))
5719
+ continue;
5720
+ results.push(result);
5721
+ if (options.bail)
5722
+ return summarize();
5723
+ }
2792
5724
  }
2793
5725
  return summarize();
2794
5726
  }
@@ -2934,17 +5866,22 @@ export function formatNativeKernTestCoverage(coverage) {
2934
5866
  `coverage ${coverage.covered}/${coverage.total} (${coverage.percent}%)`,
2935
5867
  coverageLine('transitions', coverage.transitions),
2936
5868
  coverageLine('guards', coverage.guards),
5869
+ coverageLine('routes', coverage.routes),
5870
+ coverageLine('tools', coverage.tools),
5871
+ coverageLine('effects', coverage.effects),
2937
5872
  ];
2938
- const uncoveredTransitions = coverage.transitions.uncovered;
2939
- const uncoveredGuards = coverage.guards.uncovered;
2940
- if (uncoveredTransitions.length > 0) {
2941
- lines.push('uncovered transitions:');
2942
- for (const item of uncoveredTransitions)
2943
- lines.push(` ${item}`);
2944
- }
2945
- if (uncoveredGuards.length > 0) {
2946
- lines.push('uncovered guards:');
2947
- for (const item of uncoveredGuards)
5873
+ const uncoveredSections = [
5874
+ ['transitions', coverage.transitions.uncovered],
5875
+ ['guards', coverage.guards.uncovered],
5876
+ ['routes', coverage.routes.uncovered],
5877
+ ['tools', coverage.tools.uncovered],
5878
+ ['effects', coverage.effects.uncovered],
5879
+ ];
5880
+ for (const [name, uncovered] of uncoveredSections) {
5881
+ if (uncovered.length === 0)
5882
+ continue;
5883
+ lines.push(`uncovered ${name}:`);
5884
+ for (const item of uncovered)
2948
5885
  lines.push(` ${item}`);
2949
5886
  }
2950
5887
  return `${lines.join('\n')}\n`;
@@ -2955,8 +5892,10 @@ function formatNativeKernTestResult(result, summaryFile) {
2955
5892
  ? ` (${relative(process.cwd(), result.file || summaryFile)}:${result.line}:${result.col ?? 1})`
2956
5893
  : '';
2957
5894
  const lines = [`${marker} ${result.suite} > ${result.caseName}: ${result.assertion} [${result.ruleId}]${loc}`];
2958
- if (result.status !== 'passed' && result.message)
2959
- lines.push(` ${result.message}`);
5895
+ if (result.status !== 'passed' && result.message) {
5896
+ for (const line of result.message.split('\n'))
5897
+ lines.push(` ${line}`);
5898
+ }
2960
5899
  return lines;
2961
5900
  }
2962
5901
  export function formatNativeKernTestSummary(summary, options = {}) {