@kernlang/core 3.5.9-canary.220.1.c398cd95 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/capability-matrix.js +8 -6
  2. package/dist/capability-matrix.js.map +1 -1
  3. package/dist/closure-eligibility.d.ts +104 -0
  4. package/dist/closure-eligibility.js +443 -0
  5. package/dist/closure-eligibility.js.map +1 -0
  6. package/dist/closure-python-lowering.d.ts +26 -0
  7. package/dist/closure-python-lowering.js +132 -22
  8. package/dist/closure-python-lowering.js.map +1 -1
  9. package/dist/codegen/body-ts.js +13 -0
  10. package/dist/codegen/body-ts.js.map +1 -1
  11. package/dist/codegen/type-system.js +50 -3
  12. package/dist/codegen/type-system.js.map +1 -1
  13. package/dist/codegen-core.js +29 -0
  14. package/dist/codegen-core.js.map +1 -1
  15. package/dist/codegen-expression.js +7 -0
  16. package/dist/codegen-expression.js.map +1 -1
  17. package/dist/constructor-super.d.ts +34 -0
  18. package/dist/constructor-super.js +122 -0
  19. package/dist/constructor-super.js.map +1 -0
  20. package/dist/core-contracts/boolean.d.ts +147 -0
  21. package/dist/core-contracts/boolean.js +110 -0
  22. package/dist/core-contracts/boolean.js.map +1 -0
  23. package/dist/core-contracts/function.d.ts +6 -0
  24. package/dist/core-contracts/function.js +7 -0
  25. package/dist/core-contracts/function.js.map +1 -0
  26. package/dist/core-contracts/index.d.ts +1032 -0
  27. package/dist/core-contracts/index.js +30 -0
  28. package/dist/core-contracts/index.js.map +1 -0
  29. package/dist/core-contracts/list.d.ts +87 -0
  30. package/dist/core-contracts/list.js +54 -0
  31. package/dist/core-contracts/list.js.map +1 -0
  32. package/dist/core-contracts/nullish.d.ts +12 -0
  33. package/dist/core-contracts/nullish.js +13 -0
  34. package/dist/core-contracts/nullish.js.map +1 -0
  35. package/dist/core-contracts/number.d.ts +291 -0
  36. package/dist/core-contracts/number.js +214 -0
  37. package/dist/core-contracts/number.js.map +1 -0
  38. package/dist/core-contracts/record.d.ts +60 -0
  39. package/dist/core-contracts/record.js +32 -0
  40. package/dist/core-contracts/record.js.map +1 -0
  41. package/dist/core-contracts/schema.d.ts +71 -0
  42. package/dist/core-contracts/schema.js +98 -0
  43. package/dist/core-contracts/schema.js.map +1 -0
  44. package/dist/core-contracts/semantics.d.ts +7 -0
  45. package/dist/core-contracts/semantics.js +221 -0
  46. package/dist/core-contracts/semantics.js.map +1 -0
  47. package/dist/core-contracts/string.d.ts +414 -0
  48. package/dist/core-contracts/string.js +335 -0
  49. package/dist/core-contracts/string.js.map +1 -0
  50. package/dist/core-runtime/contract-adapter.d.ts +8 -0
  51. package/dist/core-runtime/contract-adapter.js +73 -0
  52. package/dist/core-runtime/contract-adapter.js.map +1 -0
  53. package/dist/core-runtime/index.d.ts +109 -0
  54. package/dist/core-runtime/index.js +1946 -0
  55. package/dist/core-runtime/index.js.map +1 -0
  56. package/dist/core-runtime/shape-validator.d.ts +61 -0
  57. package/dist/core-runtime/shape-validator.js +564 -0
  58. package/dist/core-runtime/shape-validator.js.map +1 -0
  59. package/dist/core-runtime/value-brand.d.ts +2 -0
  60. package/dist/core-runtime/value-brand.js +6 -0
  61. package/dist/core-runtime/value-brand.js.map +1 -0
  62. package/dist/decompiler.js +46 -0
  63. package/dist/decompiler.js.map +1 -1
  64. package/dist/eligibility-taxonomy.d.ts +58 -0
  65. package/dist/eligibility-taxonomy.generated.d.ts +472 -0
  66. package/dist/eligibility-taxonomy.generated.js +566 -0
  67. package/dist/eligibility-taxonomy.generated.js.map +1 -0
  68. package/dist/eligibility-taxonomy.js +55 -0
  69. package/dist/eligibility-taxonomy.js.map +1 -0
  70. package/dist/index.d.ts +19 -2
  71. package/dist/index.js +17 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/instanceof-rhs.d.ts +53 -0
  74. package/dist/instanceof-rhs.js +74 -0
  75. package/dist/instanceof-rhs.js.map +1 -0
  76. package/dist/ir/semantics/lambda.js +9 -1
  77. package/dist/ir/semantics/lambda.js.map +1 -1
  78. package/dist/native-eligibility-ast.d.ts +5 -0
  79. package/dist/native-eligibility-ast.js +242 -63
  80. package/dist/native-eligibility-ast.js.map +1 -1
  81. package/dist/parser-expression.d.ts +1 -1
  82. package/dist/parser-expression.js +90 -1
  83. package/dist/parser-expression.js.map +1 -1
  84. package/dist/rag-assertions.d.ts +3 -0
  85. package/dist/rag-assertions.js +15 -0
  86. package/dist/rag-assertions.js.map +1 -0
  87. package/dist/rag-runtime.d.ts +169 -0
  88. package/dist/rag-runtime.js +823 -0
  89. package/dist/rag-runtime.js.map +1 -0
  90. package/dist/schema.d.ts +1 -0
  91. package/dist/schema.js +197 -2
  92. package/dist/schema.js.map +1 -1
  93. package/dist/semantic-substrate.d.ts +108 -0
  94. package/dist/semantic-substrate.js +263 -0
  95. package/dist/semantic-substrate.js.map +1 -0
  96. package/dist/semantic-validator.d.ts +332 -0
  97. package/dist/semantic-validator.js +3387 -0
  98. package/dist/semantic-validator.js.map +1 -1
  99. package/dist/spec.d.ts +2 -2
  100. package/dist/spec.js +15 -1
  101. package/dist/spec.js.map +1 -1
  102. package/dist/value-ir.d.ts +11 -1
  103. package/dist/value-ir.js.map +1 -1
  104. package/package.json +5 -1
@@ -14,17 +14,34 @@
14
14
  * when the source is local, and resolver-enriched re-exports must name
15
15
  * symbols that the resolver proved exist.
16
16
  */
17
+ import { hasDirectSuperCtorCall } from './constructor-super.js';
18
+ import { collectCoreShapeFacts, } from './core-runtime/shape-validator.js';
17
19
  import { collectExternalImportSymbols } from './external-symbols.js';
18
20
  import { importRegistryOf } from './import-metadata.js';
21
+ import { parseExpression } from './parser-expression.js';
22
+ import { RAG_ASSERTION_KIND_SET, RAG_ASSERTION_KINDS } from './rag-assertions.js';
19
23
  /**
20
24
  * Run semantic validation on an IR tree.
21
25
  * Returns an empty array when the tree is valid.
22
26
  */
23
27
  export function validateSemantics(root) {
24
28
  const violations = [];
29
+ validateClassGraph(root, violations);
30
+ validateRagGraph(root, violations);
31
+ validateEnumAccess(root, violations);
25
32
  validateNode(root, violations, [], []);
26
33
  return violations;
27
34
  }
35
+ export function validateClassSemantics(root) {
36
+ const violations = [];
37
+ validateClassGraphRoots(Array.isArray(root) ? root : [root], violations);
38
+ return violations;
39
+ }
40
+ export function validateRagSemantics(root) {
41
+ const violations = [];
42
+ validateRagGraphRoots(Array.isArray(root) ? root : [root], violations);
43
+ return violations;
44
+ }
28
45
  // True when the *innermost* handler ancestor is opted into native body-
29
46
  // statement mode (`lang="kern"`). Body statements like `let`/`assign`/`do`/
30
47
  // `if`/`try` nest freely inside that scope, so the let-parent rule has to
@@ -393,6 +410,3376 @@ function validateNode(node, violations, ancestry, ancestorNodes) {
393
410
  }
394
411
  }
395
412
  }
413
+ function validateRagGraph(root, violations) {
414
+ validateRagGraphRoots([root], violations);
415
+ }
416
+ function validateRagGraphRoots(roots, violations) {
417
+ const infos = collectRagInfosForRoots(roots);
418
+ if (infos.corpora.length === 0 &&
419
+ infos.sources.length === 0 &&
420
+ infos.chunking.length === 0 &&
421
+ infos.embeds.length === 0 &&
422
+ infos.retrievers.length === 0 &&
423
+ infos.pipelines.length === 0 &&
424
+ infos.groundings.length === 0 &&
425
+ infos.evals.length === 0 &&
426
+ infos.cases.length === 0 &&
427
+ infos.asserts.length === 0 &&
428
+ infos.answerContracts.length === 0 &&
429
+ infos.answerSpans.length === 0 &&
430
+ infos.mcpRetrievals.length === 0 &&
431
+ infos.mcpResources.length === 0 &&
432
+ infos.mcpTools.length === 0 &&
433
+ infos.mcpPrompts.length === 0) {
434
+ return;
435
+ }
436
+ const corpusByName = new Map(infos.corpora.map((info) => [info.name, info]));
437
+ const embedByName = new Map(infos.embeds.map((info) => [info.name, info]));
438
+ const retrieverByName = new Map(infos.retrievers.map((info) => [info.name, info]));
439
+ const ragByName = new Map(infos.pipelines.map((info) => [info.name, info]));
440
+ const mcpResourcesByName = collectRagMcpSymbolsByName(infos.mcpResources);
441
+ const mcpCallableByName = new Map([
442
+ ...infos.mcpTools.map((info) => [info.name, info]),
443
+ ...infos.mcpPrompts.map((info) => [info.name, info]),
444
+ ]);
445
+ const citationRequiredRagNames = collectRagCitationRequiredNames(infos.pipelines, infos.groundings);
446
+ const sourceNamesByCorpus = collectRagSourceNamesByCorpus(infos.sources);
447
+ const globalSourceNames = new Set(infos.sources.map((info) => info.name).filter((name) => !!name));
448
+ validateRagUniqueNames(infos, violations);
449
+ for (const source of infos.sources) {
450
+ validateRagSource(source, mcpResourcesByName, mcpCallableByName, violations);
451
+ }
452
+ for (const chunking of infos.chunking) {
453
+ validateRagChunking(chunking, corpusByName, sourceNamesByCorpus, globalSourceNames, violations);
454
+ }
455
+ for (const embed of infos.embeds) {
456
+ validateRagEmbed(embed, corpusByName, violations);
457
+ }
458
+ for (const retriever of infos.retrievers) {
459
+ validateRagRetriever(retriever, corpusByName, embedByName, violations);
460
+ }
461
+ for (const pipeline of infos.pipelines) {
462
+ validateRagPipeline(pipeline, retrieverByName, infos.groundings, violations);
463
+ }
464
+ for (const grounding of infos.groundings) {
465
+ validateRagGrounding(grounding, ragByName, violations);
466
+ }
467
+ for (const evaluation of infos.evals) {
468
+ validateRagEval(evaluation, ragByName, violations);
469
+ }
470
+ for (const evaluationCase of infos.cases) {
471
+ validateRagCase(evaluationCase, citationRequiredRagNames, violations);
472
+ }
473
+ for (const assertion of infos.asserts) {
474
+ validateRagAssert(assertion, citationRequiredRagNames, violations);
475
+ }
476
+ for (const contract of infos.answerContracts) {
477
+ validateRagAnswerContract(contract, infos.answerSpans, ragByName, citationRequiredRagNames, violations);
478
+ }
479
+ for (const span of infos.answerSpans) {
480
+ validateRagAnswerSpan(span, violations);
481
+ }
482
+ validateRagMcpRetrievalDuplicates(infos.mcpRetrievals, violations);
483
+ for (const retrieval of infos.mcpRetrievals) {
484
+ validateRagMcpRetrieval(retrieval, retrieverByName, ragByName, citationRequiredRagNames, violations);
485
+ }
486
+ }
487
+ function collectRagInfosForRoots(roots) {
488
+ const out = {
489
+ corpora: [],
490
+ sources: [],
491
+ chunking: [],
492
+ embeds: [],
493
+ retrievers: [],
494
+ pipelines: [],
495
+ groundings: [],
496
+ evals: [],
497
+ cases: [],
498
+ asserts: [],
499
+ answerContracts: [],
500
+ answerSpans: [],
501
+ mcpRetrievals: [],
502
+ mcpResources: [],
503
+ mcpTools: [],
504
+ mcpPrompts: [],
505
+ };
506
+ for (const [rootIndex, root] of roots.entries()) {
507
+ collectRagInfos(root, rootIndex, out);
508
+ }
509
+ return out;
510
+ }
511
+ function collectRagInfos(root, rootIndex, out) {
512
+ function visit(node, nearestCorpusName, nearestRagName, nearestRagEvalName, nearestRagCaseName, nearestRagEvalBound = false, nearestRagCaseBound = false, nearestRagEvalNode, nearestRagCaseNode, nearestRagAnswerContractName, nearestRagAnswerContractNode, nearestRagAnswerContractBound = false, nearestMcpContainer, nearestMcpName) {
513
+ const nextCorpusName = node.type === 'corpus' ? stringProp(node, 'name') || nearestCorpusName : nearestCorpusName;
514
+ const nextRagName = node.type === 'rag'
515
+ ? stringProp(node, 'name') || nearestRagName
516
+ : node.type === 'ragEval'
517
+ ? stringProp(node, 'rag') || nearestRagName
518
+ : nearestRagName;
519
+ const nextRagEvalName = node.type === 'ragEval' ? stringProp(node, 'name') : nearestRagEvalName;
520
+ const nextRagCaseName = node.type === 'ragCase' ? stringProp(node, 'name') : nearestRagCaseName;
521
+ const nextRagEvalBound = node.type === 'ragEval' || nearestRagEvalBound;
522
+ const nextRagCaseBound = node.type === 'ragCase' || nearestRagCaseBound;
523
+ const nextRagEvalNode = node.type === 'ragEval' ? node : nearestRagEvalNode;
524
+ const nextRagCaseNode = node.type === 'ragCase' ? node : nearestRagCaseNode;
525
+ const nextRagAnswerContractName = node.type === 'ragAnswerContract' ? stringProp(node, 'name') : nearestRagAnswerContractName;
526
+ const nextRagAnswerContractNode = node.type === 'ragAnswerContract' ? node : nearestRagAnswerContractNode;
527
+ const nextRagAnswerContractBound = node.type === 'ragAnswerContract' || nearestRagAnswerContractBound;
528
+ const nextMcpName = node.type === 'mcp' ? stringProp(node, 'name') || '' : nearestMcpName;
529
+ const nextMcpContainer = node.type === 'tool' || node.type === 'prompt'
530
+ ? ragMcpContainerInfo(node, rootIndex, node.type === 'tool' ? 'tool' : 'prompt')
531
+ : nearestMcpContainer;
532
+ if (node.type === 'corpus') {
533
+ const name = stringProp(node, 'name');
534
+ if (name)
535
+ out.corpora.push({ node, rootIndex, name });
536
+ }
537
+ else if (node.type === 'source') {
538
+ out.sources.push({ node, rootIndex, name: stringProp(node, 'name'), corpusName: nearestCorpusName });
539
+ }
540
+ else if (node.type === 'chunking') {
541
+ out.chunking.push({
542
+ node,
543
+ rootIndex,
544
+ name: stringProp(node, 'name'),
545
+ corpusName: stringProp(node, 'corpus') || nearestCorpusName,
546
+ sourceName: stringProp(node, 'source'),
547
+ });
548
+ }
549
+ else if (node.type === 'embed') {
550
+ const name = stringProp(node, 'name');
551
+ const corpusName = stringProp(node, 'corpus') || nearestCorpusName;
552
+ if (name && corpusName)
553
+ out.embeds.push({ node, rootIndex, name, corpusName });
554
+ }
555
+ else if (node.type === 'retriever') {
556
+ const name = stringProp(node, 'name');
557
+ const corpusName = stringProp(node, 'corpus');
558
+ if (name && corpusName) {
559
+ out.retrievers.push({ node, rootIndex, name, corpusName, embedName: stringProp(node, 'embed') });
560
+ }
561
+ }
562
+ else if (node.type === 'rag') {
563
+ const name = stringProp(node, 'name');
564
+ const retrieverName = stringProp(node, 'retriever');
565
+ if (name && retrieverName)
566
+ out.pipelines.push({ node, rootIndex, name, retrieverName });
567
+ }
568
+ else if (node.type === 'grounding') {
569
+ out.groundings.push({ node, rootIndex, ragName: stringProp(node, 'rag') || nearestRagName });
570
+ }
571
+ else if (node.type === 'ragEval') {
572
+ out.evals.push({ node, rootIndex, ragName: stringProp(node, 'rag') || nearestRagName });
573
+ }
574
+ else if (node.type === 'ragCase') {
575
+ out.cases.push({
576
+ node,
577
+ rootIndex,
578
+ name: stringProp(node, 'name'),
579
+ query: stringProp(node, 'query'),
580
+ ragName: nearestRagName,
581
+ evalName: nearestRagEvalName,
582
+ evalNode: nearestRagEvalNode,
583
+ evalBound: nearestRagEvalBound,
584
+ });
585
+ }
586
+ else if (node.type === 'ragAssert') {
587
+ out.asserts.push({
588
+ node,
589
+ rootIndex,
590
+ ragName: nearestRagName,
591
+ evalName: nearestRagEvalName,
592
+ caseName: nearestRagCaseName,
593
+ evalNode: nearestRagEvalNode,
594
+ caseNode: nearestRagCaseNode,
595
+ evalBound: nearestRagEvalBound,
596
+ caseBound: nearestRagCaseBound,
597
+ });
598
+ }
599
+ else if (node.type === 'ragAnswerContract') {
600
+ out.answerContracts.push({
601
+ node,
602
+ rootIndex,
603
+ name: stringProp(node, 'name'),
604
+ ragName: stringProp(node, 'rag') || nearestRagName,
605
+ });
606
+ }
607
+ else if (node.type === 'answerSpan') {
608
+ out.answerSpans.push({
609
+ node,
610
+ rootIndex,
611
+ contractName: nearestRagAnswerContractName,
612
+ contractNode: nearestRagAnswerContractNode,
613
+ contractBound: nearestRagAnswerContractBound,
614
+ });
615
+ }
616
+ else if (node.type === 'retrieve') {
617
+ out.mcpRetrievals.push({ node, rootIndex, container: nearestMcpContainer });
618
+ }
619
+ else if (nextMcpName !== undefined &&
620
+ (node.type === 'resource' || node.type === 'tool' || node.type === 'prompt')) {
621
+ const name = stringProp(node, 'name');
622
+ if (name) {
623
+ const kind = node.type === 'resource' ? 'resource' : node.type === 'tool' ? 'tool' : 'prompt';
624
+ const info = { node, rootIndex, kind, name };
625
+ if (node.type === 'resource')
626
+ out.mcpResources.push(info);
627
+ else if (node.type === 'tool')
628
+ out.mcpTools.push(info);
629
+ else
630
+ out.mcpPrompts.push(info);
631
+ }
632
+ }
633
+ for (const child of node.children ?? [])
634
+ visit(child, nextCorpusName, nextRagName, nextRagEvalName, nextRagCaseName, nextRagEvalBound, nextRagCaseBound, nextRagEvalNode, nextRagCaseNode, nextRagAnswerContractName, nextRagAnswerContractNode, nextRagAnswerContractBound, nextMcpContainer, nextMcpName);
635
+ }
636
+ visit(root);
637
+ }
638
+ function ragMcpContainerInfo(node, rootIndex, kind) {
639
+ const name = stringProp(node, 'name');
640
+ const paramNames = new Set();
641
+ for (const child of node.children ?? []) {
642
+ if (child.type !== 'param')
643
+ continue;
644
+ const paramName = stringProp(child, 'name');
645
+ if (paramName)
646
+ paramNames.add(paramName);
647
+ }
648
+ return { node, rootIndex, kind, ...optionalStringValue('name', name), paramNames };
649
+ }
650
+ function collectRagSourceNamesByCorpus(sources) {
651
+ const out = new Map();
652
+ for (const source of sources) {
653
+ if (!source.corpusName || !source.name)
654
+ continue;
655
+ const names = out.get(source.corpusName) ?? new Set();
656
+ names.add(source.name);
657
+ out.set(source.corpusName, names);
658
+ }
659
+ return out;
660
+ }
661
+ function collectRagMcpSymbolsByName(symbols) {
662
+ const out = new Map();
663
+ for (const symbol of symbols) {
664
+ const matches = out.get(symbol.name) ?? [];
665
+ matches.push(symbol);
666
+ out.set(symbol.name, matches);
667
+ }
668
+ return out;
669
+ }
670
+ function validateRagUniqueNames(infos, violations) {
671
+ validateRagUniqueNameSet('corpus', infos.corpora, violations);
672
+ validateRagUniqueSourceNames(infos.sources, violations);
673
+ validateRagUniqueNameSet('embed', infos.embeds, violations);
674
+ validateRagUniqueNameSet('retriever', infos.retrievers, violations);
675
+ validateRagUniqueNameSet('rag', infos.pipelines, violations);
676
+ validateRagUniqueEvalNames(infos.evals, violations);
677
+ validateRagUniqueCaseNames(infos.cases, violations);
678
+ validateRagUniqueAnswerContractNames(infos.answerContracts, violations);
679
+ }
680
+ function validateRagUniqueNameSet(kind, infos, violations) {
681
+ const seen = new Map();
682
+ for (const info of infos) {
683
+ const prev = seen.get(info.name);
684
+ if (prev) {
685
+ pushRagViolation(violations, `rag-duplicate-${kind}-name`, info.node, `Duplicate RAG ${kind} named '${info.name}' — first defined at line ${prev.loc?.line ?? '?'}.`);
686
+ }
687
+ else {
688
+ seen.set(info.name, info.node);
689
+ }
690
+ }
691
+ }
692
+ function validateRagUniqueSourceNames(sources, violations) {
693
+ const seen = new Map();
694
+ for (const source of sources) {
695
+ if (!source.name || !source.corpusName)
696
+ continue;
697
+ const key = `${source.corpusName}:${source.name}`;
698
+ const prev = seen.get(key);
699
+ if (prev) {
700
+ pushRagViolation(violations, 'rag-duplicate-source-name', source.node, `Duplicate RAG source named '${source.name}' in corpus '${source.corpusName}' — first defined at line ${prev.loc?.line ?? '?'}.`);
701
+ }
702
+ else {
703
+ seen.set(key, source.node);
704
+ }
705
+ }
706
+ }
707
+ function validateRagUniqueEvalNames(evals, violations) {
708
+ const seen = new Map();
709
+ for (const evaluation of evals) {
710
+ const name = stringProp(evaluation.node, 'name');
711
+ if (!name || !evaluation.ragName)
712
+ continue;
713
+ const key = `${evaluation.ragName}:${name}`;
714
+ const prev = seen.get(key);
715
+ if (prev) {
716
+ pushRagViolation(violations, 'rag-duplicate-eval-name', evaluation.node, `Duplicate RAG eval named '${name}' in rag '${evaluation.ragName}' — first defined at line ${prev.loc?.line ?? '?'}.`);
717
+ }
718
+ else {
719
+ seen.set(key, evaluation.node);
720
+ }
721
+ }
722
+ }
723
+ function validateRagUniqueCaseNames(cases, violations) {
724
+ const seen = new Map();
725
+ for (const evaluationCase of cases) {
726
+ if (!evaluationCase.name || !evaluationCase.evalNode)
727
+ continue;
728
+ const evalCases = seen.get(evaluationCase.evalNode) ?? new Map();
729
+ const prev = evalCases.get(evaluationCase.name);
730
+ if (prev) {
731
+ pushRagViolation(violations, 'rag-duplicate-case-name', evaluationCase.node, `Duplicate RAG eval case named '${evaluationCase.name}' in eval '${evaluationCase.evalName ?? '?'}' — first defined at line ${prev.loc?.line ?? '?'}.`);
732
+ }
733
+ else {
734
+ evalCases.set(evaluationCase.name, evaluationCase.node);
735
+ seen.set(evaluationCase.evalNode, evalCases);
736
+ }
737
+ }
738
+ }
739
+ function validateRagUniqueAnswerContractNames(contracts, violations) {
740
+ const seen = new Map();
741
+ for (const contract of contracts) {
742
+ if (!contract.name || !contract.ragName)
743
+ continue;
744
+ const key = `${contract.ragName}:${contract.name}`;
745
+ const prev = seen.get(key);
746
+ if (prev) {
747
+ pushRagViolation(violations, 'rag-duplicate-answer-contract-name', contract.node, `Duplicate RAG answer contract named '${contract.name}' in rag '${contract.ragName}' — first defined at line ${prev.loc?.line ?? '?'}.`);
748
+ }
749
+ else {
750
+ seen.set(key, contract.node);
751
+ }
752
+ }
753
+ }
754
+ function validateRagSource(source, mcpResourcesByName, mcpCallableByName, violations) {
755
+ if (!source.corpusName) {
756
+ pushRagViolation(violations, 'rag-source-missing-corpus', source.node, 'RAG source must be nested under a corpus.');
757
+ }
758
+ const kind = stringProp(source.node, 'kind');
759
+ const resourceName = stringProp(source.node, 'resource');
760
+ if (kind === 'mcp') {
761
+ if (!resourceName) {
762
+ pushRagViolation(violations, 'rag-source-mcp-resource-required', source.node, 'RAG source kind=mcp requires resource=<mcp resource name>.');
763
+ }
764
+ else {
765
+ const resources = mcpResourcesByName.get(resourceName) ?? [];
766
+ if (resources.length > 1) {
767
+ pushRagViolation(violations, 'rag-source-mcp-resource-ambiguous', source.node, `RAG source resource '${resourceName}' is ambiguous because multiple MCP resources use that name.`);
768
+ }
769
+ else if (resources.length === 0) {
770
+ const callable = mcpCallableByName.get(resourceName);
771
+ if (callable) {
772
+ pushRagViolation(violations, 'rag-source-mcp-resource-kind', source.node, `RAG source resource '${resourceName}' resolves to MCP ${callable.kind}, expected MCP resource.`);
773
+ }
774
+ else {
775
+ pushRagViolation(violations, 'rag-source-mcp-resource-unknown', source.node, `RAG source references unknown MCP resource '${resourceName}'.`);
776
+ }
777
+ }
778
+ }
779
+ }
780
+ else if (resourceName) {
781
+ pushRagViolation(violations, 'rag-source-resource-requires-mcp-kind', source.node, 'RAG source resource=<name> is only valid with kind=mcp.');
782
+ }
783
+ const uri = stringProp(source.node, 'uri');
784
+ if (uri !== undefined && uri.trim() === '') {
785
+ pushRagViolation(violations, 'rag-source-uri-empty', source.node, "RAG source 'uri=' must be a non-empty document location.");
786
+ }
787
+ }
788
+ function validateRagChunking(chunking, corpusByName, sourceNamesByCorpus, globalSourceNames, violations) {
789
+ if (!chunking.corpusName) {
790
+ pushRagViolation(violations, 'rag-chunking-missing-corpus', chunking.node, 'RAG chunking must be nested under a corpus or declare corpus=<name>.');
791
+ }
792
+ if (chunking.corpusName && !corpusByName.has(chunking.corpusName)) {
793
+ pushRagViolation(violations, 'rag-chunking-unknown-corpus', chunking.node, `RAG chunking references unknown corpus '${chunking.corpusName}'. Declare a corpus before chunking it.`);
794
+ }
795
+ if (chunking.sourceName) {
796
+ const sourceNames = chunking.corpusName ? sourceNamesByCorpus.get(chunking.corpusName) : undefined;
797
+ const sourceKnown = chunking.corpusName
798
+ ? Boolean(sourceNames?.has(chunking.sourceName))
799
+ : globalSourceNames.has(chunking.sourceName);
800
+ if (!sourceKnown) {
801
+ pushRagViolation(violations, 'rag-chunking-unknown-source', chunking.node, `RAG chunking references unknown source '${chunking.sourceName}'. Declare a named source in the same corpus.`);
802
+ }
803
+ }
804
+ const maxTokens = numberProp(chunking.node, 'maxTokens');
805
+ if (invalidNumberProp(chunking.node, 'maxTokens') ||
806
+ (maxTokens !== undefined && (!Number.isInteger(maxTokens) || maxTokens <= 0))) {
807
+ pushRagViolation(violations, 'rag-chunking-max-tokens-invalid', chunking.node, 'RAG chunking maxTokens must be a positive integer.');
808
+ }
809
+ const overlap = numberProp(chunking.node, 'overlap');
810
+ if (invalidNumberProp(chunking.node, 'overlap') ||
811
+ (overlap !== undefined && (!Number.isInteger(overlap) || overlap < 0))) {
812
+ pushRagViolation(violations, 'rag-chunking-overlap-invalid', chunking.node, 'RAG chunking overlap must be a non-negative integer.');
813
+ }
814
+ else if (overlap !== undefined && maxTokens !== undefined && overlap >= maxTokens) {
815
+ pushRagViolation(violations, 'rag-chunking-overlap-invalid', chunking.node, 'RAG chunking overlap must be smaller than maxTokens.');
816
+ }
817
+ }
818
+ function validateRagEmbed(embed, corpusByName, violations) {
819
+ if (!corpusByName.has(embed.corpusName)) {
820
+ pushRagViolation(violations, 'rag-embed-unknown-corpus', embed.node, `RAG embed '${embed.name}' references unknown corpus '${embed.corpusName}'.`);
821
+ }
822
+ const dims = numberProp(embed.node, 'dims');
823
+ if (invalidNumberProp(embed.node, 'dims') || (dims !== undefined && (!Number.isInteger(dims) || dims <= 0))) {
824
+ pushRagViolation(violations, 'rag-embed-dims-invalid', embed.node, 'RAG embed dims must be a positive integer.');
825
+ }
826
+ }
827
+ function validateRagRetriever(retriever, corpusByName, embedByName, violations) {
828
+ if (!corpusByName.has(retriever.corpusName)) {
829
+ pushRagViolation(violations, 'rag-retriever-unknown-corpus', retriever.node, `RAG retriever '${retriever.name}' references unknown corpus '${retriever.corpusName}'.`);
830
+ }
831
+ if (retriever.embedName) {
832
+ const embed = embedByName.get(retriever.embedName);
833
+ if (!embed) {
834
+ pushRagViolation(violations, 'rag-retriever-unknown-embed', retriever.node, `RAG retriever '${retriever.name}' references unknown embed '${retriever.embedName}'.`);
835
+ }
836
+ else if (embed.corpusName !== retriever.corpusName) {
837
+ pushRagViolation(violations, 'rag-retriever-embed-corpus-mismatch', retriever.node, `RAG retriever '${retriever.name}' uses embed '${retriever.embedName}' for corpus '${embed.corpusName}', not '${retriever.corpusName}'.`);
838
+ }
839
+ }
840
+ const topK = numberProp(retriever.node, 'topK');
841
+ if (invalidNumberProp(retriever.node, 'topK') || (topK !== undefined && (!Number.isInteger(topK) || topK <= 0))) {
842
+ pushRagViolation(violations, 'rag-retriever-topk-invalid', retriever.node, 'RAG retriever topK must be a positive integer.');
843
+ }
844
+ const minScore = numberProp(retriever.node, 'minScore');
845
+ if (invalidNumberProp(retriever.node, 'minScore') || (minScore !== undefined && (minScore < 0 || minScore > 1))) {
846
+ pushRagViolation(violations, 'rag-retriever-minscore-invalid', retriever.node, 'RAG retriever minScore must be between 0 and 1.');
847
+ }
848
+ }
849
+ function validateRagPipeline(pipeline, retrieverByName, groundings, violations) {
850
+ if (!retrieverByName.has(pipeline.retrieverName)) {
851
+ pushRagViolation(violations, 'rag-unknown-retriever', pipeline.node, `RAG pipeline '${pipeline.name}' references unknown retriever '${pipeline.retrieverName}'.`);
852
+ }
853
+ if (ragBooleanProp(pipeline.node, 'citations')) {
854
+ const hasCitationGrounding = groundings.some((grounding) => grounding.ragName === pipeline.name && ragBooleanProp(grounding.node, 'requireCitations'));
855
+ if (!hasCitationGrounding) {
856
+ pushRagViolation(violations, 'rag-citations-require-grounding', pipeline.node, `RAG pipeline '${pipeline.name}' requires citations but has no grounding requireCitations=true policy.`);
857
+ }
858
+ }
859
+ }
860
+ function validateRagGrounding(grounding, ragByName, violations) {
861
+ if (!grounding.ragName) {
862
+ pushRagViolation(violations, 'rag-grounding-missing-rag', grounding.node, 'RAG grounding must be nested under a rag pipeline or declare rag=<name>.');
863
+ }
864
+ if (grounding.ragName && !ragByName.has(grounding.ragName)) {
865
+ pushRagViolation(violations, 'rag-grounding-unknown-rag', grounding.node, `RAG grounding references unknown rag '${grounding.ragName}'.`);
866
+ }
867
+ const maxContext = numberProp(grounding.node, 'maxContext');
868
+ if (invalidNumberProp(grounding.node, 'maxContext') ||
869
+ (maxContext !== undefined && (!Number.isInteger(maxContext) || maxContext <= 0))) {
870
+ pushRagViolation(violations, 'rag-grounding-max-context-invalid', grounding.node, 'RAG grounding maxContext must be a positive integer.');
871
+ }
872
+ }
873
+ function validateRagEval(evaluation, ragByName, violations) {
874
+ if (!evaluation.ragName) {
875
+ pushRagViolation(violations, 'rag-eval-missing-rag', evaluation.node, 'RAG eval must be nested under a rag pipeline or declare rag=<name>.');
876
+ }
877
+ if (evaluation.ragName && !ragByName.has(evaluation.ragName)) {
878
+ pushRagViolation(violations, 'rag-eval-unknown-rag', evaluation.node, `RAG eval references unknown rag '${evaluation.ragName}'.`);
879
+ }
880
+ const threshold = numberProp(evaluation.node, 'threshold');
881
+ if (invalidNumberProp(evaluation.node, 'threshold') ||
882
+ (threshold !== undefined && (threshold < 0 || threshold > 1))) {
883
+ pushRagViolation(violations, 'rag-eval-threshold-invalid', evaluation.node, 'RAG eval threshold must be between 0 and 1.');
884
+ }
885
+ const mode = stringProp(evaluation.node, 'mode');
886
+ const hasCases = (evaluation.node.children ?? []).some((child) => child.type === 'ragCase');
887
+ if (hasCases && !stringProp(evaluation.node, 'name')) {
888
+ pushRagViolation(violations, 'rag-eval-name-required', evaluation.node, 'RAG eval with ragCase children must declare name=<id> for stable eval facts.');
889
+ }
890
+ if (hasCases && !mode) {
891
+ pushRagViolation(violations, 'rag-eval-mode-required', evaluation.node, 'RAG eval with ragCase children must declare mode=contract.');
892
+ }
893
+ if (mode && mode !== 'contract') {
894
+ pushRagViolation(violations, 'rag-eval-mode-invalid', evaluation.node, "RAG eval mode only supports 'contract' in this slice.");
895
+ }
896
+ }
897
+ function validateRagCase(evaluationCase, citationRequiredRagNames, violations) {
898
+ if (!evaluationCase.evalBound) {
899
+ pushRagViolation(violations, 'rag-case-missing-eval', evaluationCase.node, 'RAG eval case must be nested under ragEval.');
900
+ }
901
+ if (!evaluationCase.name) {
902
+ pushRagViolation(violations, 'rag-case-name-required', evaluationCase.node, 'RAG eval case requires name=<id>.');
903
+ }
904
+ if (!evaluationCase.query) {
905
+ pushRagViolation(violations, 'rag-case-query-required', evaluationCase.node, 'RAG eval case requires query=<text>.');
906
+ }
907
+ const topK = numberProp(evaluationCase.node, 'topK');
908
+ if (invalidNumberProp(evaluationCase.node, 'topK') ||
909
+ (topK !== undefined && (!Number.isInteger(topK) || topK <= 0))) {
910
+ pushRagViolation(violations, 'rag-case-topk-invalid', evaluationCase.node, 'RAG eval case topK must be a positive integer.');
911
+ }
912
+ const minScore = numberProp(evaluationCase.node, 'minScore');
913
+ if (invalidNumberProp(evaluationCase.node, 'minScore') ||
914
+ (minScore !== undefined && (minScore < 0 || minScore > 1))) {
915
+ pushRagViolation(violations, 'rag-case-minscore-invalid', evaluationCase.node, 'RAG eval case minScore must be between 0 and 1.');
916
+ }
917
+ const chunkCount = numberProp(evaluationCase.node, 'chunkCount');
918
+ if (invalidNumberProp(evaluationCase.node, 'chunkCount') ||
919
+ (chunkCount !== undefined && (!Number.isInteger(chunkCount) || chunkCount < 0))) {
920
+ pushRagViolation(violations, 'rag-case-chunk-count-invalid', evaluationCase.node, 'RAG eval case chunkCount must be a non-negative integer.');
921
+ }
922
+ if (stringProp(evaluationCase.node, 'sources') &&
923
+ (!evaluationCase.ragName || !citationRequiredRagNames.has(evaluationCase.ragName))) {
924
+ pushRagViolation(violations, 'rag-case-sources-require-citations', evaluationCase.node, 'RAG eval case sources=<...> requires a citation-grounded rag.');
925
+ }
926
+ }
927
+ function validateRagAssert(assertion, citationRequiredRagNames, violations) {
928
+ if (!assertion.evalBound) {
929
+ pushRagViolation(violations, 'rag-assert-missing-eval', assertion.node, 'RAG assert must be nested under ragEval.');
930
+ }
931
+ if (!assertion.caseBound) {
932
+ pushRagViolation(violations, 'rag-assert-missing-case', assertion.node, 'RAG assert must be nested under ragCase.');
933
+ }
934
+ const kind = stringProp(assertion.node, 'kind');
935
+ if (!kind || !RAG_ASSERTION_KIND_SET.has(kind)) {
936
+ pushRagViolation(violations, 'rag-assert-kind-invalid', assertion.node, `RAG assert kind must be one of ${RAG_ASSERTION_KINDS.join(', ')}.`);
937
+ return;
938
+ }
939
+ if (['factId', 'chunkHash', 'contains', 'sourceEq', 'sourceGlob'].includes(kind) &&
940
+ !stringProp(assertion.node, 'value')) {
941
+ pushRagViolation(violations, 'rag-assert-value-required', assertion.node, `RAG assert kind=${kind} requires value=<text>.`);
942
+ }
943
+ const chunkHash = kind === 'chunkHash' ? stringProp(assertion.node, 'value') : undefined;
944
+ if (chunkHash && !/^[a-fA-F0-9]{32,128}$/.test(chunkHash)) {
945
+ pushRagViolation(violations, 'rag-assert-chunk-hash-invalid', assertion.node, 'RAG assert kind=chunkHash value must be a 32-128 character hex hash.');
946
+ }
947
+ if (kind === 'scoreGte' || kind === 'scoreLte') {
948
+ const threshold = numberProp(assertion.node, 'threshold');
949
+ if (threshold === undefined && !invalidNumberProp(assertion.node, 'threshold')) {
950
+ pushRagViolation(violations, 'rag-assert-threshold-required', assertion.node, `RAG assert kind=${kind} requires threshold=<number>.`);
951
+ }
952
+ else if (invalidNumberProp(assertion.node, 'threshold') ||
953
+ threshold === undefined ||
954
+ threshold < 0 ||
955
+ threshold > 1) {
956
+ pushRagViolation(violations, 'rag-assert-threshold-invalid', assertion.node, `RAG assert kind=${kind} threshold must be between 0 and 1.`);
957
+ }
958
+ }
959
+ if (kind === 'citesRequired' &&
960
+ assertion.caseBound &&
961
+ (!assertion.ragName || !citationRequiredRagNames.has(assertion.ragName))) {
962
+ pushRagViolation(violations, 'rag-assert-citations-require-grounding', assertion.node, 'RAG assert kind=citesRequired requires a citation-grounded rag.');
963
+ }
964
+ if (kind === 'uniqueSourcesGte' || kind === 'chunkCountEq') {
965
+ const count = numberProp(assertion.node, 'count');
966
+ if (invalidNumberProp(assertion.node, 'count') || count === undefined || !Number.isInteger(count) || count < 0) {
967
+ pushRagViolation(violations, 'rag-assert-count-invalid', assertion.node, `RAG assert kind=${kind} requires a non-negative integer count.`);
968
+ }
969
+ }
970
+ if (kind === 'latencyLte') {
971
+ const valueMs = numberProp(assertion.node, 'valueMs');
972
+ if (invalidNumberProp(assertion.node, 'valueMs') ||
973
+ valueMs === undefined ||
974
+ !Number.isInteger(valueMs) ||
975
+ valueMs < 0) {
976
+ pushRagViolation(violations, 'rag-assert-value-ms-invalid', assertion.node, 'RAG assert kind=latencyLte requires a non-negative integer valueMs.');
977
+ }
978
+ }
979
+ }
980
+ function validateRagAnswerContract(contract, spans, ragByName, citationRequiredRagNames, violations) {
981
+ if (!contract.ragName) {
982
+ pushRagViolation(violations, 'rag-answer-contract-missing-rag', contract.node, 'RAG answer contract must be nested under a rag pipeline or declare rag=<name>.');
983
+ }
984
+ else if (!ragByName.has(contract.ragName)) {
985
+ pushRagViolation(violations, 'rag-answer-contract-unknown-rag', contract.node, `RAG answer contract references unknown rag '${contract.ragName}'.`);
986
+ }
987
+ if (!contract.name) {
988
+ pushRagViolation(violations, 'rag-answer-contract-name-required', contract.node, 'RAG answer contract requires name=<id>.');
989
+ }
990
+ if (!stringProp(contract.node, 'query')) {
991
+ pushRagViolation(violations, 'rag-answer-contract-query-required', contract.node, 'RAG answer contract requires query=<text>.');
992
+ }
993
+ if (!stringProp(contract.node, 'answer')) {
994
+ pushRagViolation(violations, 'rag-answer-contract-answer-required', contract.node, 'RAG answer contract requires answer=<text>.');
995
+ }
996
+ const minGroundingCoverage = numberProp(contract.node, 'minGroundingCoverage');
997
+ if (invalidNumberProp(contract.node, 'minGroundingCoverage') ||
998
+ (minGroundingCoverage !== undefined && (minGroundingCoverage < 0 || minGroundingCoverage > 1))) {
999
+ pushRagViolation(violations, 'rag-answer-contract-min-grounding-coverage-invalid', contract.node, 'RAG answer contract minGroundingCoverage must be between 0 and 1.');
1000
+ }
1001
+ validateRagAnswerContractCoverage(contract, spans, minGroundingCoverage, violations);
1002
+ if (ragBooleanProp(contract.node, 'requireCitations') &&
1003
+ (!contract.ragName || !citationRequiredRagNames.has(contract.ragName))) {
1004
+ pushRagViolation(violations, 'rag-answer-contract-citations-require-grounding', contract.node, 'RAG answer contract requireCitations=true requires a citation-grounded rag.');
1005
+ }
1006
+ }
1007
+ function validateRagAnswerContractCoverage(contract, spans, minGroundingCoverage, violations) {
1008
+ const answer = stringProp(contract.node, 'answer');
1009
+ if (!answer)
1010
+ return;
1011
+ const contractSpans = spans.filter((span) => span.contractNode === contract.node);
1012
+ const grounded = new Array(answer.length).fill(false);
1013
+ for (const span of contractSpans) {
1014
+ const start = numberProp(span.node, 'start');
1015
+ const end = numberProp(span.node, 'end');
1016
+ if (start !== undefined &&
1017
+ end !== undefined &&
1018
+ Number.isInteger(start) &&
1019
+ Number.isInteger(end) &&
1020
+ start >= 0 &&
1021
+ end > start &&
1022
+ end <= answer.length) {
1023
+ for (let index = start; index < end; index += 1)
1024
+ grounded[index] = true;
1025
+ }
1026
+ else if (end !== undefined && end > answer.length) {
1027
+ pushRagViolation(violations, 'rag-answer-span-range-invalid', span.node, 'RAG answer span end must not exceed the parent answer length.');
1028
+ }
1029
+ }
1030
+ if (minGroundingCoverage === undefined || minGroundingCoverage < 0 || minGroundingCoverage > 1)
1031
+ return;
1032
+ const answerChars = countRagAnswerChars(answer);
1033
+ const groundedChars = countRagGroundedAnswerChars(answer, grounded);
1034
+ const coverage = answerChars === 0 ? 0 : groundedChars / answerChars;
1035
+ if (answerChars > 0 && coverage < minGroundingCoverage) {
1036
+ pushRagViolation(violations, 'rag-answer-contract-grounding-coverage-insufficient', contract.node, `RAG answer contract grounding coverage ${coverage.toFixed(3)} is below minGroundingCoverage ${minGroundingCoverage.toFixed(3)}.`);
1037
+ }
1038
+ }
1039
+ function validateRagAnswerSpan(span, violations) {
1040
+ if (!span.contractBound) {
1041
+ pushRagViolation(violations, 'rag-answer-span-missing-contract', span.node, 'RAG answer span must be nested under ragAnswerContract.');
1042
+ }
1043
+ const start = numberProp(span.node, 'start');
1044
+ const end = numberProp(span.node, 'end');
1045
+ if (invalidNumberProp(span.node, 'start') ||
1046
+ invalidNumberProp(span.node, 'end') ||
1047
+ start === undefined ||
1048
+ end === undefined ||
1049
+ !Number.isInteger(start) ||
1050
+ !Number.isInteger(end) ||
1051
+ start < 0 ||
1052
+ end <= start) {
1053
+ pushRagViolation(violations, 'rag-answer-span-range-invalid', span.node, 'RAG answer span start/end must be non-negative integers with start < end.');
1054
+ }
1055
+ if (splitRagList(stringProp(span.node, 'chunks')).length === 0) {
1056
+ pushRagViolation(violations, 'rag-answer-span-chunks-required', span.node, 'RAG answer span requires chunks=<chunk-id[,chunk-id...]>.');
1057
+ }
1058
+ }
1059
+ function validateRagMcpRetrievalDuplicates(retrievals, violations) {
1060
+ const seen = new Map();
1061
+ for (const retrieval of retrievals) {
1062
+ const containerNode = retrieval.container?.node;
1063
+ if (!containerNode)
1064
+ continue;
1065
+ const prev = seen.get(containerNode);
1066
+ if (prev) {
1067
+ pushRagViolation(violations, 'mcp-retrieve-duplicate', retrieval.node, `MCP ${retrieval.container?.kind} '${retrieval.container?.name ?? '<unnamed>'}' cannot declare more than one retrieve binding — first defined at line ${prev.loc?.line ?? '?'}.`);
1068
+ }
1069
+ else {
1070
+ seen.set(containerNode, retrieval.node);
1071
+ }
1072
+ }
1073
+ }
1074
+ function validateRagMcpRetrieval(retrieval, retrieverByName, ragByName, citationRequiredRagNames, violations) {
1075
+ if (!retrieval.container) {
1076
+ pushRagViolation(violations, 'mcp-retrieve-missing-container', retrieval.node, 'MCP retrieve must be nested under a tool or prompt.');
1077
+ }
1078
+ const retrieverName = stringProp(retrieval.node, 'retriever');
1079
+ const ragName = stringProp(retrieval.node, 'rag');
1080
+ if (!retrieverName && !ragName) {
1081
+ pushRagViolation(violations, 'mcp-retrieve-target-required', retrieval.node, 'MCP retrieve must declare retriever=<name> or rag=<name>.');
1082
+ }
1083
+ if (retrieverName && ragName) {
1084
+ pushRagViolation(violations, 'mcp-retrieve-target-exclusive', retrieval.node, 'MCP retrieve cannot combine retriever=<name> and rag=<name>.');
1085
+ }
1086
+ if (retrieverName && !retrieverByName.has(retrieverName)) {
1087
+ pushRagViolation(violations, 'mcp-retrieve-unknown-retriever', retrieval.node, `MCP retrieve references unknown retriever '${retrieverName}'.`);
1088
+ }
1089
+ if (ragName && !ragByName.has(ragName)) {
1090
+ pushRagViolation(violations, 'mcp-retrieve-unknown-rag', retrieval.node, `MCP retrieve references unknown rag '${ragName}'.`);
1091
+ }
1092
+ const queryParam = stringProp(retrieval.node, 'queryParam');
1093
+ const query = expressionPropText(retrieval.node.props?.query);
1094
+ if (!queryParam && !query) {
1095
+ pushRagViolation(violations, 'mcp-retrieve-query-required', retrieval.node, 'MCP retrieve must declare queryParam=<param> or query={{...}}.');
1096
+ }
1097
+ if (queryParam && query) {
1098
+ pushRagViolation(violations, 'mcp-retrieve-query-exclusive', retrieval.node, 'MCP retrieve cannot combine queryParam=<param> and query={{...}}.');
1099
+ }
1100
+ if (queryParam && retrieval.container && !retrieval.container.paramNames.has(queryParam)) {
1101
+ pushRagViolation(violations, 'mcp-retrieve-query-param-unknown', retrieval.node, `MCP retrieve queryParam '${queryParam}' is not declared on ${retrieval.container.kind} '${retrieval.container.name ?? '<unnamed>'}'.`);
1102
+ }
1103
+ const topK = numberProp(retrieval.node, 'topK');
1104
+ if (invalidNumberProp(retrieval.node, 'topK') || (topK !== undefined && (!Number.isInteger(topK) || topK <= 0))) {
1105
+ pushRagViolation(violations, 'mcp-retrieve-topk-invalid', retrieval.node, 'MCP retrieve topK must be a positive integer.');
1106
+ }
1107
+ const minScore = numberProp(retrieval.node, 'minScore');
1108
+ if (invalidNumberProp(retrieval.node, 'minScore') || (minScore !== undefined && (minScore < 0 || minScore > 1))) {
1109
+ pushRagViolation(violations, 'mcp-retrieve-minscore-invalid', retrieval.node, 'MCP retrieve minScore must be between 0 and 1.');
1110
+ }
1111
+ if (ragName && ragBooleanPropIsFalse(retrieval.node, 'requireGrounding')) {
1112
+ const requiresCitations = citationRequiredRagNames.has(ragName);
1113
+ if (requiresCitations) {
1114
+ pushRagViolation(violations, 'mcp-retrieve-citations-require-grounding', retrieval.node, `MCP retrieve references citation-grounded rag '${ragName}' but sets requireGrounding=false.`);
1115
+ }
1116
+ }
1117
+ validateRagMcpRetrievalOutput(retrieval, ragName, citationRequiredRagNames, violations);
1118
+ }
1119
+ function validateRagMcpRetrievalOutput(retrieval, ragName, citationRequiredRagNames, violations) {
1120
+ const outputShape = stringProp(retrieval.node, 'output');
1121
+ const provenance = stringProp(retrieval.node, 'provenance');
1122
+ const citationField = stringProp(retrieval.node, 'citationField');
1123
+ const sourceField = stringProp(retrieval.node, 'sourceField');
1124
+ const scoreField = stringProp(retrieval.node, 'scoreField');
1125
+ const hasRequireCitations = Object.hasOwn(retrieval.node.props ?? {}, 'requireCitations');
1126
+ const hasOutputField = Boolean(provenance || citationField || sourceField || scoreField);
1127
+ if (outputShape === RAG_MCP_RETRIEVE_OUTPUT_ITEM_SHAPE) {
1128
+ pushRagViolation(violations, 'mcp-retrieve-output-array-required', retrieval.node, 'MCP retrieve output must be RetrievedChunk[] because retrieval bindings expose ranked context sets.');
1129
+ }
1130
+ else if (outputShape && outputShape !== RAG_MCP_RETRIEVE_OUTPUT_SHAPE) {
1131
+ pushRagViolation(violations, 'mcp-retrieve-output-unknown', retrieval.node, `MCP retrieve output '${outputShape}' is not supported; use RetrievedChunk[] for this slice.`);
1132
+ }
1133
+ if (!outputShape && hasOutputField) {
1134
+ pushRagViolation(violations, 'mcp-retrieve-output-field-without-output', retrieval.node, 'MCP retrieve output fields require output=RetrievedChunk[].');
1135
+ }
1136
+ if (!outputShape && hasRequireCitations) {
1137
+ pushRagViolation(violations, 'mcp-retrieve-output-required', retrieval.node, 'MCP retrieve requireCitations=<bool> requires output=RetrievedChunk[].');
1138
+ }
1139
+ const targetRequiresCitations = ragName ? citationRequiredRagNames.has(ragName) : false;
1140
+ if (ragBooleanPropIsFalse(retrieval.node, 'requireCitations') && targetRequiresCitations) {
1141
+ pushRagViolation(violations, 'mcp-retrieve-output-citations-cannot-weaken-rag', retrieval.node, `MCP retrieve references citation-grounded rag '${ragName}' but sets requireCitations=false.`);
1142
+ }
1143
+ if (outputShape !== RAG_MCP_RETRIEVE_OUTPUT_SHAPE)
1144
+ return;
1145
+ const explicitRequiresCitations = ragBooleanProp(retrieval.node, 'requireCitations');
1146
+ const effectiveRequiresCitations = explicitRequiresCitations || targetRequiresCitations;
1147
+ if (effectiveRequiresCitations && !citationField) {
1148
+ pushRagViolation(violations, 'mcp-retrieve-output-citation-field-required', retrieval.node, 'MCP retrieve output requires citationField=<field> when citations are required.');
1149
+ }
1150
+ if (effectiveRequiresCitations && !sourceField && provenance !== 'source') {
1151
+ pushRagViolation(violations, 'mcp-retrieve-output-source-required', retrieval.node, 'MCP retrieve output requires sourceField=<field> or provenance=source when citations are required.');
1152
+ }
1153
+ }
1154
+ function collectRagCitationRequiredNames(pipelines, groundings) {
1155
+ const out = new Set();
1156
+ for (const pipeline of pipelines) {
1157
+ if (ragBooleanProp(pipeline.node, 'citations'))
1158
+ out.add(pipeline.name);
1159
+ }
1160
+ for (const grounding of groundings) {
1161
+ if (grounding.ragName && ragBooleanProp(grounding.node, 'requireCitations'))
1162
+ out.add(grounding.ragName);
1163
+ }
1164
+ return out;
1165
+ }
1166
+ function pushRagViolation(violations, rule, node, message) {
1167
+ violations.push({ rule, nodeType: node.type, message, line: node.loc?.line, col: node.loc?.col });
1168
+ }
1169
+ export function collectRagSemanticFacts(root) {
1170
+ const roots = Array.isArray(root) ? root : [root];
1171
+ const infos = collectRagInfosForRoots(roots);
1172
+ const corpusNames = new Set(infos.corpora.map((info) => info.name));
1173
+ const embedNames = new Set(infos.embeds.map((info) => info.name));
1174
+ const retrieverNames = new Set(infos.retrievers.map((info) => info.name));
1175
+ const ragNames = new Set(infos.pipelines.map((info) => info.name));
1176
+ const citationRequiredRagNames = collectRagCitationRequiredNames(infos.pipelines, infos.groundings);
1177
+ const mcpResourcesByName = collectRagMcpSymbolsByName(infos.mcpResources);
1178
+ const mcpCallableNames = new Set([
1179
+ ...infos.mcpTools.map((info) => info.name),
1180
+ ...infos.mcpPrompts.map((info) => info.name),
1181
+ ]);
1182
+ const sourceNamesByCorpus = collectRagSourceNamesByCorpus(infos.sources);
1183
+ const globalSourceNames = new Set(infos.sources.map((info) => info.name).filter((name) => !!name));
1184
+ return {
1185
+ corpora: infos.corpora.map((info) => ragCorpusFact(info, infos)),
1186
+ retrievers: infos.retrievers.map(ragRetrieverFact),
1187
+ pipelines: infos.pipelines.map((info) => ragPipelineFact(info, infos.groundings, infos.evals, infos.cases, infos.asserts, infos.answerContracts, infos.answerSpans)),
1188
+ mcpRetrievals: infos.mcpRetrievals.map((info) => ragMcpRetrievalFact(info, citationRequiredRagNames)),
1189
+ resourceFeedsCorpora: infos.sources
1190
+ .filter((info) => stringProp(info.node, 'kind') === 'mcp' &&
1191
+ (mcpResourcesByName.get(stringProp(info.node, 'resource') ?? '')?.length ?? 0) === 1)
1192
+ .map(ragResourceFeedFact),
1193
+ unresolvedCorpusRefs: sortedUnique([
1194
+ ...infos.chunking
1195
+ .map((info) => info.corpusName)
1196
+ .filter((name) => !!name && !corpusNames.has(name)),
1197
+ ...infos.embeds.map((info) => info.corpusName).filter((name) => !corpusNames.has(name)),
1198
+ ...infos.retrievers.map((info) => info.corpusName).filter((name) => !corpusNames.has(name)),
1199
+ ]),
1200
+ unresolvedRetrieverRefs: sortedUnique([
1201
+ ...infos.pipelines.map((info) => info.retrieverName),
1202
+ ...infos.mcpRetrievals.map((info) => stringProp(info.node, 'retriever')),
1203
+ ].filter((name) => !!name && !retrieverNames.has(name))),
1204
+ unresolvedEmbedRefs: sortedUnique(infos.retrievers.map((info) => info.embedName).filter((name) => !!name && !embedNames.has(name))),
1205
+ unresolvedRagRefs: sortedUnique([
1206
+ ...infos.groundings.map((info) => info.ragName),
1207
+ ...infos.evals.map((info) => info.ragName),
1208
+ ...infos.answerContracts.map((info) => info.ragName),
1209
+ ...infos.mcpRetrievals.map((info) => stringProp(info.node, 'rag')),
1210
+ ].filter((name) => !!name && !ragNames.has(name))),
1211
+ unresolvedSourceRefs: sortedUnique(infos.chunking
1212
+ .filter((info) => {
1213
+ if (!info.sourceName)
1214
+ return false;
1215
+ const sourceNames = info.corpusName ? sourceNamesByCorpus.get(info.corpusName) : undefined;
1216
+ return info.corpusName ? !sourceNames?.has(info.sourceName) : !globalSourceNames.has(info.sourceName);
1217
+ })
1218
+ .map((info) => info.sourceName)
1219
+ .filter((name) => !!name)),
1220
+ unresolvedResourceRefs: sortedUnique(infos.sources
1221
+ .filter((info) => stringProp(info.node, 'kind') === 'mcp')
1222
+ .map((info) => stringProp(info.node, 'resource'))
1223
+ .filter((name) => !!name && !mcpResourcesByName.has(name) && !mcpCallableNames.has(name))),
1224
+ };
1225
+ }
1226
+ function ragCorpusFact(info, all) {
1227
+ return {
1228
+ name: info.name,
1229
+ ...optionalStringFact(info.node, 'title', 'title'),
1230
+ ...optionalStringFact(info.node, 'tenant', 'tenant'),
1231
+ ...optionalStringFact(info.node, 'refresh', 'refresh'),
1232
+ sources: all.sources.filter((source) => source.corpusName === info.name).map(ragSourceFact),
1233
+ chunking: all.chunking.filter((chunking) => chunking.corpusName === info.name).map(ragChunkingFact),
1234
+ embeds: all.embeds.filter((embed) => embed.corpusName === info.name).map(ragEmbedFact),
1235
+ ...(info.node.loc ? { loc: ragLocation(info.node) } : {}),
1236
+ };
1237
+ }
1238
+ function ragSourceFact(info) {
1239
+ return {
1240
+ ...optionalStringValue('name', info.name),
1241
+ ...optionalStringValue('corpusName', info.corpusName),
1242
+ ...optionalStringFact(info.node, 'kind', 'kind'),
1243
+ uri: stringProp(info.node, 'uri') ?? '',
1244
+ ...optionalStringFact(info.node, 'resource', 'resourceName'),
1245
+ ...optionalStringFact(info.node, 'media', 'media'),
1246
+ ...optionalStringFact(info.node, 'acl', 'acl'),
1247
+ ...(info.node.loc ? { loc: ragLocation(info.node) } : {}),
1248
+ };
1249
+ }
1250
+ function ragResourceFeedFact(info) {
1251
+ return {
1252
+ ...optionalStringValue('corpusName', info.corpusName),
1253
+ ...optionalStringValue('sourceName', info.name),
1254
+ resourceName: stringProp(info.node, 'resource') ?? '',
1255
+ uri: stringProp(info.node, 'uri') ?? '',
1256
+ ...(info.node.loc ? { loc: ragLocation(info.node) } : {}),
1257
+ };
1258
+ }
1259
+ function ragChunkingFact(info) {
1260
+ return {
1261
+ ...optionalStringValue('name', info.name),
1262
+ ...optionalStringValue('corpusName', info.corpusName),
1263
+ ...optionalStringValue('sourceName', info.sourceName),
1264
+ ...optionalStringFact(info.node, 'strategy', 'strategy'),
1265
+ ...optionalNumberFact(info.node, 'maxTokens', 'maxTokens'),
1266
+ ...optionalNumberFact(info.node, 'overlap', 'overlap'),
1267
+ ...optionalStringFact(info.node, 'unit', 'unit'),
1268
+ ...(info.node.loc ? { loc: ragLocation(info.node) } : {}),
1269
+ };
1270
+ }
1271
+ function ragEmbedFact(info) {
1272
+ return {
1273
+ name: info.name,
1274
+ corpusName: info.corpusName,
1275
+ ...optionalStringFact(info.node, 'model', 'model'),
1276
+ ...optionalNumberFact(info.node, 'dims', 'dims'),
1277
+ ...optionalStringFact(info.node, 'metric', 'metric'),
1278
+ ...(info.node.loc ? { loc: ragLocation(info.node) } : {}),
1279
+ };
1280
+ }
1281
+ function ragRetrieverFact(info) {
1282
+ return {
1283
+ name: info.name,
1284
+ corpusName: info.corpusName,
1285
+ ...optionalStringValue('embedName', info.embedName),
1286
+ ...optionalStringFact(info.node, 'mode', 'mode'),
1287
+ ...optionalNumberFact(info.node, 'topK', 'topK'),
1288
+ ...optionalNumberFact(info.node, 'minScore', 'minScore'),
1289
+ ...optionalStringFact(info.node, 'rerank', 'rerank'),
1290
+ ...(info.node.loc ? { loc: ragLocation(info.node) } : {}),
1291
+ };
1292
+ }
1293
+ function ragPipelineFact(info, groundings, evals, cases, asserts, answerContracts, answerSpans) {
1294
+ return {
1295
+ name: info.name,
1296
+ retrieverName: info.retrieverName,
1297
+ ...optionalStringFact(info.node, 'prompt', 'prompt'),
1298
+ ...optionalStringFact(info.node, 'answer', 'answer'),
1299
+ citations: ragBooleanProp(info.node, 'citations'),
1300
+ groundings: groundings.filter((grounding) => grounding.ragName === info.name).map(ragGroundingFact),
1301
+ evals: evals
1302
+ .filter((evaluation) => evaluation.ragName === info.name)
1303
+ .map((evaluation) => ragEvalFact(evaluation, cases, asserts)),
1304
+ answerContracts: answerContracts
1305
+ .filter((contract) => contract.ragName === info.name)
1306
+ .map((contract) => ragAnswerContractFact(contract, answerSpans)),
1307
+ ...(info.node.loc ? { loc: ragLocation(info.node) } : {}),
1308
+ };
1309
+ }
1310
+ function ragGroundingFact(info) {
1311
+ return {
1312
+ ...optionalStringFact(info.node, 'name', 'name'),
1313
+ ...optionalStringValue('ragName', info.ragName),
1314
+ requireCitations: ragBooleanProp(info.node, 'requireCitations'),
1315
+ ...optionalStringFact(info.node, 'policy', 'policy'),
1316
+ ...optionalNumberFact(info.node, 'maxContext', 'maxContext'),
1317
+ ...(info.node.loc ? { loc: ragLocation(info.node) } : {}),
1318
+ };
1319
+ }
1320
+ function ragEvalFact(info, cases, asserts) {
1321
+ const evalCases = cases.filter((evaluationCase) => evaluationCase.evalNode === info.node);
1322
+ const caseFacts = evalCases.map((evaluationCase) => ragEvalCaseFact(evaluationCase, asserts));
1323
+ return {
1324
+ ...optionalStringFact(info.node, 'name', 'name'),
1325
+ ...optionalStringValue('ragName', info.ragName),
1326
+ ...optionalStringFact(info.node, 'metric', 'metric'),
1327
+ ...optionalNumberFact(info.node, 'threshold', 'threshold'),
1328
+ ...optionalStringFact(info.node, 'mode', 'mode'),
1329
+ caseCount: caseFacts.length,
1330
+ assertCount: caseFacts.reduce((count, evaluationCase) => count + evaluationCase.asserts.length, 0),
1331
+ cases: caseFacts,
1332
+ ...(info.node.loc ? { loc: ragLocation(info.node) } : {}),
1333
+ };
1334
+ }
1335
+ function ragEvalCaseFact(info, asserts) {
1336
+ const caseAsserts = asserts.filter((assertion) => assertion.caseNode === info.node);
1337
+ return {
1338
+ name: info.name ?? '',
1339
+ ...optionalStringValue('ragName', info.ragName),
1340
+ ...optionalStringValue('evalName', info.evalName),
1341
+ query: info.query ?? '',
1342
+ tags: splitRagList(stringProp(info.node, 'tags')),
1343
+ expected: {
1344
+ ...optionalNumberFact(info.node, 'topK', 'topK'),
1345
+ ...optionalNumberFact(info.node, 'minScore', 'minScore'),
1346
+ ...optionalNumberFact(info.node, 'chunkCount', 'chunkCount'),
1347
+ ...(stringProp(info.node, 'sources') ? { sources: splitRagList(stringProp(info.node, 'sources')) } : {}),
1348
+ },
1349
+ asserts: caseAsserts.map(ragEvalAssertFact),
1350
+ ...(info.node.loc ? { loc: ragLocation(info.node) } : {}),
1351
+ };
1352
+ }
1353
+ function ragEvalAssertFact(info) {
1354
+ const kind = stringProp(info.node, 'kind') ?? '';
1355
+ return {
1356
+ ...optionalStringValue('ragName', info.ragName),
1357
+ ...optionalStringValue('evalName', info.evalName),
1358
+ ...optionalStringValue('caseName', info.caseName),
1359
+ kind,
1360
+ target: ragAssertTarget(kind),
1361
+ op: ragAssertOp(kind),
1362
+ ...ragAssertValueFact(info.node, kind),
1363
+ required: ragBooleanProp(info.node, 'required'),
1364
+ ...(info.node.loc ? { loc: ragLocation(info.node) } : {}),
1365
+ };
1366
+ }
1367
+ function ragAnswerContractFact(info, spans) {
1368
+ const contractSpans = spans.filter((span) => span.contractNode === info.node);
1369
+ return {
1370
+ name: info.name ?? '',
1371
+ ...optionalStringValue('ragName', info.ragName),
1372
+ query: stringProp(info.node, 'query') ?? '',
1373
+ answer: stringProp(info.node, 'answer') ?? '',
1374
+ ...optionalStringFact(info.node, 'prompt', 'prompt'),
1375
+ requireCitations: ragBooleanProp(info.node, 'requireCitations'),
1376
+ ...optionalNumberFact(info.node, 'minGroundingCoverage', 'minGroundingCoverage'),
1377
+ spans: contractSpans.map(ragAnswerSpanFact),
1378
+ ...(info.node.loc ? { loc: ragLocation(info.node) } : {}),
1379
+ };
1380
+ }
1381
+ function ragAnswerSpanFact(info) {
1382
+ return {
1383
+ start: numberProp(info.node, 'start') ?? 0,
1384
+ end: numberProp(info.node, 'end') ?? 0,
1385
+ chunkIds: splitRagList(stringProp(info.node, 'chunks')),
1386
+ required: ragBooleanProp(info.node, 'required'),
1387
+ ...(info.node.loc ? { loc: ragLocation(info.node) } : {}),
1388
+ };
1389
+ }
1390
+ function ragAssertTarget(kind) {
1391
+ if (kind === 'uniqueSourcesGte' || kind === 'chunkCountEq')
1392
+ return 'retrieved-chunks';
1393
+ if (kind === 'latencyLte')
1394
+ return 'latency';
1395
+ if (kind === 'citesRequired')
1396
+ return 'grounding';
1397
+ return 'retrieved-chunk';
1398
+ }
1399
+ function ragAssertOp(kind) {
1400
+ switch (kind) {
1401
+ case 'scoreGte':
1402
+ case 'uniqueSourcesGte':
1403
+ return 'gte';
1404
+ case 'scoreLte':
1405
+ case 'latencyLte':
1406
+ return 'lte';
1407
+ case 'contains':
1408
+ return 'contains';
1409
+ case 'sourceGlob':
1410
+ return 'glob';
1411
+ case 'citesRequired':
1412
+ return 'present';
1413
+ default:
1414
+ return 'eq';
1415
+ }
1416
+ }
1417
+ function ragAssertValueFact(node, kind) {
1418
+ if (kind === 'scoreGte' || kind === 'scoreLte') {
1419
+ const threshold = numberProp(node, 'threshold');
1420
+ return threshold === undefined ? {} : { value: threshold };
1421
+ }
1422
+ if (kind === 'uniqueSourcesGte' || kind === 'chunkCountEq') {
1423
+ const count = numberProp(node, 'count');
1424
+ return count === undefined ? {} : { value: count };
1425
+ }
1426
+ if (kind === 'latencyLte') {
1427
+ const valueMs = numberProp(node, 'valueMs');
1428
+ return valueMs === undefined ? {} : { value: valueMs };
1429
+ }
1430
+ if (kind === 'citesRequired')
1431
+ return { value: true };
1432
+ return optionalStringFact(node, 'value', 'value');
1433
+ }
1434
+ function splitRagList(value) {
1435
+ if (!value)
1436
+ return [];
1437
+ return value
1438
+ .split(',')
1439
+ .map((item) => item.trim())
1440
+ .filter((item) => item.length > 0);
1441
+ }
1442
+ function countRagAnswerChars(answer) {
1443
+ let count = 0;
1444
+ for (let index = 0; index < answer.length; index += 1) {
1445
+ if (!/\s/u.test(answer[index] ?? ''))
1446
+ count += 1;
1447
+ }
1448
+ return count;
1449
+ }
1450
+ function countRagGroundedAnswerChars(answer, grounded) {
1451
+ let count = 0;
1452
+ for (let index = 0; index < answer.length; index += 1) {
1453
+ if (grounded[index] && !/\s/u.test(answer[index] ?? ''))
1454
+ count += 1;
1455
+ }
1456
+ return count;
1457
+ }
1458
+ function ragMcpRetrievalFact(info, citationRequiredRagNames) {
1459
+ const ragName = stringProp(info.node, 'rag');
1460
+ const retrieverName = stringProp(info.node, 'retriever');
1461
+ const targetKind = ragName ? 'rag' : 'retriever';
1462
+ const targetName = ragName || retrieverName || '';
1463
+ const outputShape = stringProp(info.node, 'output');
1464
+ const targetRequiresCitations = ragName ? citationRequiredRagNames.has(ragName) : false;
1465
+ const explicitRequiresCitations = ragBooleanProp(info.node, 'requireCitations');
1466
+ return {
1467
+ ...(info.container ? { containerKind: info.container.kind, containerName: info.container.name ?? '' } : {}),
1468
+ targetKind,
1469
+ targetName,
1470
+ ...optionalStringFact(info.node, 'name', 'name'),
1471
+ ...optionalStringFact(info.node, 'queryParam', 'queryParam'),
1472
+ ...optionalStringValue('query', expressionPropText(info.node.props?.query)),
1473
+ ...optionalStringFact(info.node, 'as', 'as'),
1474
+ ...optionalNumberFact(info.node, 'topK', 'topK'),
1475
+ ...optionalNumberFact(info.node, 'minScore', 'minScore'),
1476
+ requireGrounding: ragMcpRetrieveRequiresGrounding(info.node, ragName, citationRequiredRagNames),
1477
+ ...optionalStringValue('outputShape', outputShape),
1478
+ ...(outputShape === RAG_MCP_RETRIEVE_OUTPUT_SHAPE ? { outputItemShape: RAG_MCP_RETRIEVE_OUTPUT_ITEM_SHAPE } : {}),
1479
+ ...(Object.hasOwn(info.node.props ?? {}, 'requireCitations')
1480
+ ? { requireCitations: explicitRequiresCitations }
1481
+ : {}),
1482
+ effectiveRequiresCitations: explicitRequiresCitations || targetRequiresCitations,
1483
+ ...optionalStringFact(info.node, 'provenance', 'provenance'),
1484
+ ...optionalStringFact(info.node, 'citationField', 'citationField'),
1485
+ ...optionalStringFact(info.node, 'sourceField', 'sourceField'),
1486
+ ...optionalStringFact(info.node, 'scoreField', 'scoreField'),
1487
+ contractStatus: ragMcpRetrieveContractStatus(info.node, targetRequiresCitations),
1488
+ ...(info.node.loc ? { loc: ragLocation(info.node) } : {}),
1489
+ };
1490
+ }
1491
+ function ragMcpRetrieveContractStatus(node, targetRequiresCitations) {
1492
+ const outputShape = stringProp(node, 'output');
1493
+ const hasRequireCitations = Object.hasOwn(node.props ?? {}, 'requireCitations');
1494
+ const hasOutputField = ['provenance', 'citationField', 'sourceField', 'scoreField'].some((prop) => Boolean(stringProp(node, prop)));
1495
+ if (!outputShape)
1496
+ return hasOutputField || hasRequireCitations ? 'invalid' : 'absent';
1497
+ if (outputShape !== RAG_MCP_RETRIEVE_OUTPUT_SHAPE)
1498
+ return 'invalid';
1499
+ if (ragBooleanPropIsFalse(node, 'requireCitations') && targetRequiresCitations)
1500
+ return 'invalid';
1501
+ if (ragBooleanProp(node, 'requireCitations') || targetRequiresCitations) {
1502
+ const citationField = stringProp(node, 'citationField');
1503
+ const sourceField = stringProp(node, 'sourceField');
1504
+ const provenance = stringProp(node, 'provenance');
1505
+ if (!citationField)
1506
+ return 'invalid';
1507
+ if (!sourceField && provenance !== 'source')
1508
+ return 'invalid';
1509
+ }
1510
+ return 'valid';
1511
+ }
1512
+ function ragMcpRetrieveRequiresGrounding(node, ragName, citationRequiredRagNames) {
1513
+ if (ragBooleanPropIsFalse(node, 'requireGrounding'))
1514
+ return false;
1515
+ if (ragBooleanProp(node, 'requireGrounding'))
1516
+ return true;
1517
+ return ragName ? citationRequiredRagNames.has(ragName) : false;
1518
+ }
1519
+ function ragLocation(node) {
1520
+ return node.loc ? { line: node.loc.line, col: node.loc.col } : undefined;
1521
+ }
1522
+ function optionalStringFact(node, prop, factName) {
1523
+ return optionalStringValue(factName, stringProp(node, prop));
1524
+ }
1525
+ function optionalStringValue(factName, value) {
1526
+ return value ? { [factName]: value } : {};
1527
+ }
1528
+ function optionalNumberFact(node, prop, factName) {
1529
+ const value = numberProp(node, prop);
1530
+ return value === undefined ? {} : { [factName]: value };
1531
+ }
1532
+ function numberProp(node, prop) {
1533
+ const raw = node.props?.[prop];
1534
+ if (typeof raw === 'number')
1535
+ return Number.isFinite(raw) ? raw : undefined;
1536
+ if (typeof raw !== 'string' || raw.trim() === '')
1537
+ return undefined;
1538
+ const value = Number(raw);
1539
+ return Number.isFinite(value) ? value : undefined;
1540
+ }
1541
+ function invalidNumberProp(node, prop) {
1542
+ const raw = node.props?.[prop];
1543
+ if (raw === undefined || raw === null || raw === '')
1544
+ return false;
1545
+ if (typeof raw === 'number')
1546
+ return !Number.isFinite(raw);
1547
+ if (typeof raw === 'string')
1548
+ return raw.trim() !== '' && !Number.isFinite(Number(raw));
1549
+ return true;
1550
+ }
1551
+ function ragBooleanProp(node, prop) {
1552
+ const raw = node.props?.[prop];
1553
+ return raw === true || (typeof raw === 'string' && raw.trim().toLowerCase() === 'true');
1554
+ }
1555
+ function ragBooleanPropIsFalse(node, prop) {
1556
+ const raw = node.props?.[prop];
1557
+ return raw === false || (typeof raw === 'string' && raw.trim().toLowerCase() === 'false');
1558
+ }
1559
+ function sortedUnique(values) {
1560
+ return [...new Set(values)].sort();
1561
+ }
1562
+ const BUILTIN_CLASS_BASES = new Set(['Error']);
1563
+ export const RAG_MCP_RETRIEVE_OUTPUT_SHAPE = 'RetrievedChunk[]';
1564
+ export const RAG_MCP_RETRIEVE_OUTPUT_ITEM_SHAPE = 'RetrievedChunk';
1565
+ const BODY_EXPRESSION_PROPS = [
1566
+ 'value',
1567
+ 'expr',
1568
+ 'target',
1569
+ 'cond',
1570
+ 'on',
1571
+ 'in',
1572
+ 'from',
1573
+ 'to',
1574
+ 'initial',
1575
+ 'source',
1576
+ 'sources',
1577
+ 'cleanup',
1578
+ 'min',
1579
+ 'max',
1580
+ ];
1581
+ function validateClassGraph(root, violations) {
1582
+ validateClassGraphRoots([root], violations);
1583
+ }
1584
+ function validateClassGraphRoots(roots, violations) {
1585
+ const classesByRoot = roots.map((root, rootIndex) => collectClassInfos(root, rootIndex));
1586
+ const classes = classesByRoot.flat();
1587
+ if (classes.length === 0)
1588
+ return;
1589
+ const interfaces = roots.flatMap((root, rootIndex) => collectInterfaceInfos(root, rootIndex));
1590
+ const interfaceByName = new Map();
1591
+ for (const info of interfaces) {
1592
+ if (!interfaceByName.has(info.name))
1593
+ interfaceByName.set(info.name, info);
1594
+ }
1595
+ const protocolShapeContext = collectClassProtocolShapeContext(roots);
1596
+ const classByName = new Map();
1597
+ const declaredClassNames = new Set();
1598
+ for (const info of classes) {
1599
+ const prev = classByName.get(info.name);
1600
+ if (!prev) {
1601
+ classByName.set(info.name, info);
1602
+ }
1603
+ declaredClassNames.add(info.name);
1604
+ }
1605
+ const visibleNamesByRoot = roots.map((root) => {
1606
+ const visibleNames = collectVisibleClassBaseNames(root);
1607
+ for (const className of declaredClassNames)
1608
+ visibleNames.add(className);
1609
+ return visibleNames;
1610
+ });
1611
+ const visibleProtocolNamesByRoot = roots.map((root) => collectVisibleProtocolNames(root));
1612
+ for (const info of classes) {
1613
+ validateClassBaseReference(info, visibleNamesByRoot[info.rootIndex] ?? declaredClassNames, violations);
1614
+ validateClassImplements(info, interfaceByName, visibleProtocolNamesByRoot[info.rootIndex] ?? new Set(), protocolShapeContext, classByName, violations);
1615
+ validateClassConstructors(info, violations);
1616
+ validateClassMemberConflicts(info, violations);
1617
+ validateClassSuperUsage(info, classByName, violations);
1618
+ validateClassAbstractMembers(info, classByName, violations);
1619
+ }
1620
+ validateClassInheritanceCycles(classes, classByName, violations);
1621
+ validateClassOverrides(classes, classByName, violations);
1622
+ validateClassShapeUsage(classes, classByName, violations);
1623
+ validateAbstractInstantiations(roots, classByName, visibleNamesByRoot, violations);
1624
+ }
1625
+ export function collectClassInfos(root, rootIndex = 0) {
1626
+ const out = [];
1627
+ walkSemanticTree(root, (node) => {
1628
+ if (node.type !== 'class')
1629
+ return;
1630
+ const name = stringProp(node, 'name');
1631
+ if (!name)
1632
+ return;
1633
+ out.push({
1634
+ node,
1635
+ rootIndex,
1636
+ name,
1637
+ baseName: classBaseName(node.props?.extends),
1638
+ implementsNames: classReferenceNames(node.props?.implements, 'class implements='),
1639
+ implementsMalformed: classReferenceListMalformed(node.props?.implements, 'class implements='),
1640
+ members: collectClassMembers(node, name),
1641
+ constructors: (node.children ?? []).filter((child) => child.type === 'constructor'),
1642
+ });
1643
+ });
1644
+ return out;
1645
+ }
1646
+ function collectInterfaceInfos(root, rootIndex = 0) {
1647
+ const out = [];
1648
+ walkSemanticTree(root, (node) => {
1649
+ if (node.type !== 'interface')
1650
+ return;
1651
+ const name = stringProp(node, 'name');
1652
+ if (!name)
1653
+ return;
1654
+ out.push({
1655
+ node,
1656
+ rootIndex,
1657
+ name,
1658
+ extendsNames: classReferenceNames(node.props?.extends, 'interface extends='),
1659
+ fields: collectInterfaceFields(node),
1660
+ methods: collectInterfaceMethods(node),
1661
+ });
1662
+ });
1663
+ return out;
1664
+ }
1665
+ function collectInterfaceFields(node) {
1666
+ const fields = [];
1667
+ for (const child of node.children ?? []) {
1668
+ if (child.type !== 'field')
1669
+ continue;
1670
+ const name = stringProp(child, 'name');
1671
+ if (!name)
1672
+ continue;
1673
+ fields.push({
1674
+ name,
1675
+ ...(stringProp(child, 'type') ? { type: stringProp(child, 'type') } : {}),
1676
+ optional: isTrueFlag(child.props?.optional),
1677
+ static: isTrueFlag(child.props?.static),
1678
+ });
1679
+ }
1680
+ return fields;
1681
+ }
1682
+ function collectInterfaceMethods(node) {
1683
+ const methods = [];
1684
+ for (const child of node.children ?? []) {
1685
+ if (child.type !== 'method')
1686
+ continue;
1687
+ const name = stringProp(child, 'name');
1688
+ if (!name)
1689
+ continue;
1690
+ methods.push({
1691
+ name,
1692
+ ...(stringProp(child, 'returns') ? { returns: stringProp(child, 'returns') } : {}),
1693
+ paramTypes: memberParamTypes(child),
1694
+ arity: memberArity(child),
1695
+ async: isTrueFlag(child.props?.async),
1696
+ stream: isTrueFlag(child.props?.stream),
1697
+ generator: isTrueFlag(child.props?.generator),
1698
+ static: isTrueFlag(child.props?.static),
1699
+ });
1700
+ }
1701
+ return methods;
1702
+ }
1703
+ function collectClassProtocolShapeContext(roots) {
1704
+ const facts = collectCoreShapeFacts(roots);
1705
+ const shapeByName = new Map();
1706
+ for (const shape of facts.interfaces)
1707
+ shapeByName.set(shape.name, shape);
1708
+ const diagnosticsByName = new Map();
1709
+ for (const diagnostic of facts.validationDiagnostics) {
1710
+ if (!diagnostic.interfaceName)
1711
+ continue;
1712
+ const diagnostics = diagnosticsByName.get(diagnostic.interfaceName) ?? [];
1713
+ diagnostics.push(diagnostic);
1714
+ diagnosticsByName.set(diagnostic.interfaceName, diagnostics);
1715
+ }
1716
+ return { shapeByName, diagnosticsByName };
1717
+ }
1718
+ function collectClassMembers(node, owner) {
1719
+ const members = [];
1720
+ for (const child of node.children ?? []) {
1721
+ if (!isClassMemberNode(child))
1722
+ continue;
1723
+ const name = stringProp(child, 'name');
1724
+ if (!name)
1725
+ continue;
1726
+ members.push({
1727
+ node: child,
1728
+ owner,
1729
+ name,
1730
+ kind: child.type,
1731
+ static: isTrueFlag(child.props?.static),
1732
+ private: isTrueFlag(child.props?.private),
1733
+ async: isTrueFlag(child.props?.async),
1734
+ stream: isTrueFlag(child.props?.stream),
1735
+ generator: isTrueFlag(child.props?.generator),
1736
+ ...(stringProp(child, 'type') ? { type: stringProp(child, 'type') } : {}),
1737
+ ...(stringProp(child, 'returns') ? { returns: stringProp(child, 'returns') } : {}),
1738
+ paramTypes: memberParamTypes(child),
1739
+ arity: memberArity(child),
1740
+ });
1741
+ }
1742
+ return members;
1743
+ }
1744
+ export function collectClassSemanticFacts(root) {
1745
+ const roots = Array.isArray(root) ? root : [root];
1746
+ const classes = roots.flatMap((candidate, rootIndex) => collectClassInfos(candidate, rootIndex));
1747
+ const interfaces = roots.flatMap((candidate, rootIndex) => collectInterfaceInfos(candidate, rootIndex));
1748
+ const classByName = new Map();
1749
+ for (const info of classes) {
1750
+ if (!classByName.has(info.name))
1751
+ classByName.set(info.name, info);
1752
+ }
1753
+ const interfaceByName = new Map();
1754
+ for (const info of interfaces) {
1755
+ if (!interfaceByName.has(info.name))
1756
+ interfaceByName.set(info.name, info);
1757
+ }
1758
+ const protocolShapeContext = collectClassProtocolShapeContext(roots);
1759
+ const visibleNamesByRoot = roots.map((candidate) => collectVisibleClassBaseNames(candidate));
1760
+ const visibleProtocolNamesByRoot = roots.map((candidate) => collectVisibleProtocolNames(candidate));
1761
+ const inheritanceEdges = [];
1762
+ const unresolvedBases = new Set();
1763
+ for (const info of classes) {
1764
+ if (!info.baseName)
1765
+ continue;
1766
+ const resolved = classByName.has(info.baseName) || (visibleNamesByRoot[info.rootIndex] ?? BUILTIN_CLASS_BASES).has(info.baseName);
1767
+ const builtin = BUILTIN_CLASS_BASES.has(info.baseName);
1768
+ inheritanceEdges.push({
1769
+ from: info.name,
1770
+ to: info.baseName,
1771
+ relation: 'extends',
1772
+ resolved,
1773
+ builtin,
1774
+ });
1775
+ if (!resolved)
1776
+ unresolvedBases.add(info.baseName);
1777
+ }
1778
+ const implementsEdges = [];
1779
+ const unresolvedImplements = new Set();
1780
+ for (const info of classes) {
1781
+ for (const interfaceName of info.implementsNames) {
1782
+ const external = !interfaceByName.has(interfaceName) &&
1783
+ (visibleProtocolNamesByRoot[info.rootIndex] ?? new Set()).has(interfaceName);
1784
+ const resolved = interfaceByName.has(interfaceName) || external;
1785
+ implementsEdges.push({
1786
+ from: info.name,
1787
+ to: interfaceName,
1788
+ relation: 'implements',
1789
+ resolved,
1790
+ external,
1791
+ });
1792
+ if (!resolved)
1793
+ unresolvedImplements.add(interfaceName);
1794
+ }
1795
+ }
1796
+ return {
1797
+ classes: classes.map((info) => classSemanticFact(info, classByName)),
1798
+ constructorFacts: classes.map((info) => classConstructorSemanticFact(info)),
1799
+ inheritanceEdges,
1800
+ implementsEdges,
1801
+ overrides: collectClassOverrideFacts(classes, classByName),
1802
+ unresolvedBases: [...unresolvedBases].sort(),
1803
+ unresolvedImplements: [...unresolvedImplements].sort(),
1804
+ protocolConformance: collectClassProtocolConformanceFacts(classes, interfaceByName, visibleProtocolNamesByRoot, protocolShapeContext, classByName),
1805
+ cycles: collectClassCycleFacts(classes, classByName),
1806
+ };
1807
+ }
1808
+ function classSemanticFact(info, classByName) {
1809
+ return {
1810
+ name: info.name,
1811
+ ...(info.baseName ? { baseName: info.baseName } : {}),
1812
+ hasConstructor: info.constructors.length > 0,
1813
+ constructorCount: info.constructors.length,
1814
+ members: info.members.map((member) => classMemberSemanticFact(member)),
1815
+ effectiveMembers: effectiveClassMemberFacts(info, classByName),
1816
+ ...(info.node.loc ? { loc: semanticLocation(info.node) } : {}),
1817
+ };
1818
+ }
1819
+ function classConstructorSemanticFact(info) {
1820
+ const superDiagnostics = constructorSuperDiagnostics(info);
1821
+ const superCallCount = info.constructors.reduce((count, ctor) => count + constructorBodyStatements(ctor).reduce((sum, statement) => sum + superCallCountInNode(statement), 0), 0);
1822
+ const declaredFields = declaredInstanceFieldNames(info);
1823
+ const declaredFieldSet = new Set(declaredFields);
1824
+ const initializedFields = sortedUnique([...fieldInitializerNames(info), ...constructorThisAssignmentNames(info)].filter((name) => declaredFieldSet.has(name)));
1825
+ return {
1826
+ className: info.name,
1827
+ hasConstructor: info.constructors.length > 0,
1828
+ constructorCount: info.constructors.length,
1829
+ hasBase: Boolean(info.baseName),
1830
+ requiresSuper: Boolean(info.baseName) && info.constructors.length > 0,
1831
+ superStatus: constructorSuperStatus(info, superDiagnostics),
1832
+ superCallCount,
1833
+ thisBeforeSuper: superDiagnostics.some((diagnostic) => diagnostic.rule === 'class-constructor-this-before-super'),
1834
+ declaredFields,
1835
+ initializedFields,
1836
+ uninitializedRequiredFields: uninitializedRequiredFieldNames(info, initializedFields),
1837
+ provenance: 'static-analysis',
1838
+ ...(info.node.loc ? { loc: semanticLocation(info.node) } : {}),
1839
+ };
1840
+ }
1841
+ function classMemberSemanticFact(member, className = member.owner, inheritedFrom) {
1842
+ return {
1843
+ className,
1844
+ owner: member.owner,
1845
+ name: member.name,
1846
+ kind: member.kind,
1847
+ static: member.static,
1848
+ ...(member.private ? { private: true } : {}),
1849
+ ...(member.async ? { async: true } : {}),
1850
+ ...(member.stream ? { stream: true } : {}),
1851
+ ...(member.generator ? { generator: true } : {}),
1852
+ ...(member.type ? { type: member.type } : {}),
1853
+ ...(member.returns ? { returns: member.returns } : {}),
1854
+ ...(member.kind === 'method' && member.paramTypes.length > 0 ? { paramTypes: member.paramTypes } : {}),
1855
+ arity: member.arity,
1856
+ readable: member.kind === 'field' || member.kind === 'getter' || member.kind === 'method',
1857
+ writable: member.kind === 'field' || member.kind === 'setter',
1858
+ ...(inheritedFrom ? { inheritedFrom } : {}),
1859
+ ...(member.node.loc ? { loc: semanticLocation(member.node) } : {}),
1860
+ };
1861
+ }
1862
+ function constructorSuperDiagnostics(info) {
1863
+ const violations = [];
1864
+ if (!info.baseName)
1865
+ return violations;
1866
+ for (const ctor of info.constructors)
1867
+ validateDerivedConstructorDiscipline(info, ctor, violations);
1868
+ return violations;
1869
+ }
1870
+ function constructorSuperStatus(info, diagnostics) {
1871
+ if (!info.baseName || info.constructors.length === 0)
1872
+ return 'not-required';
1873
+ const rules = new Set(diagnostics.map((diagnostic) => diagnostic.rule));
1874
+ if (rules.has('class-constructor-this-before-super'))
1875
+ return 'this-before-super';
1876
+ if (rules.has('class-constructor-double-super'))
1877
+ return 'double';
1878
+ if (rules.has('class-constructor-conditional-super'))
1879
+ return 'conditional';
1880
+ if (rules.has('class-constructor-missing-super'))
1881
+ return 'missing';
1882
+ return 'satisfied';
1883
+ }
1884
+ function declaredInstanceFieldNames(info) {
1885
+ return sortedUnique(info.members.filter((member) => member.kind === 'field' && !member.static).map((member) => member.name));
1886
+ }
1887
+ function requiredInstanceFieldNames(info) {
1888
+ return sortedUnique(info.members
1889
+ .filter((member) => member.kind === 'field' && !member.static && !isTrueFlag(member.node.props?.optional))
1890
+ .map((member) => member.name));
1891
+ }
1892
+ function fieldInitializerNames(info) {
1893
+ return sortedUnique(info.members
1894
+ .filter((member) => member.kind === 'field' &&
1895
+ !member.static &&
1896
+ (Object.hasOwn(member.node.props ?? {}, 'value') || Object.hasOwn(member.node.props ?? {}, 'default')))
1897
+ .map((member) => member.name));
1898
+ }
1899
+ function constructorThisAssignmentNames(info) {
1900
+ if (info.constructors.length === 0)
1901
+ return [];
1902
+ const constructorAssignments = info.constructors.map((ctor) => [
1903
+ ...definiteThisAssignmentsInStatements(constructorBodyStatements(ctor)),
1904
+ ]);
1905
+ const [first = [], ...rest] = constructorAssignments;
1906
+ return sortedUnique([...rest.reduce((common, names) => setIntersection(common, new Set(names)), new Set(first))]);
1907
+ }
1908
+ function definiteThisAssignmentsInStatements(statements, initial = new Set()) {
1909
+ const states = thisAssignmentPathStatesInStatements(statements, [new Set(initial)]);
1910
+ const [first = new Set(), ...rest] = states.exited.concat(states.continuing);
1911
+ return rest.reduce((common, names) => setIntersection(common, names), new Set(first));
1912
+ }
1913
+ function thisAssignmentPathStatesInStatements(statements, initialStates) {
1914
+ let continuing = initialStates.map((state) => new Set(state));
1915
+ const exited = [];
1916
+ for (let index = 0; index < statements.length; index += 1) {
1917
+ const statement = statements[index];
1918
+ if (statement.type === 'else')
1919
+ continue;
1920
+ const nextContinuing = [];
1921
+ if (statement.type === 'if') {
1922
+ const maybeElse = statements[index + 1]?.type === 'else' ? statements[index + 1] : undefined;
1923
+ for (const state of continuing) {
1924
+ const thenStates = thisAssignmentPathStatesInStatements(statement.children ?? [], [state]);
1925
+ const elseStates = maybeElse
1926
+ ? thisAssignmentPathStatesInStatements(maybeElse.children ?? [], [state])
1927
+ : { continuing: [new Set(state)], exited: [] };
1928
+ nextContinuing.push(...thenStates.continuing, ...elseStates.continuing);
1929
+ exited.push(...thenStates.exited, ...elseStates.exited);
1930
+ }
1931
+ continuing = nextContinuing;
1932
+ if (maybeElse)
1933
+ index += 1;
1934
+ continue;
1935
+ }
1936
+ if (statement.type === 'try') {
1937
+ const tryStates = thisAssignmentTryPathStates(statement, continuing);
1938
+ continuing = tryStates.continuing;
1939
+ exited.push(...tryStates.exited);
1940
+ continue;
1941
+ }
1942
+ if (statement.type === 'return') {
1943
+ exited.push(...continuing.map((state) => new Set(state)));
1944
+ continuing = [];
1945
+ continue;
1946
+ }
1947
+ if (statement.type === 'throw' || statement.type === 'break' || statement.type === 'continue') {
1948
+ continuing = [];
1949
+ continue;
1950
+ }
1951
+ if (statement.type === 'while' || statement.type === 'for' || statement.type === 'each') {
1952
+ continue;
1953
+ }
1954
+ for (const state of continuing) {
1955
+ const next = new Set(state);
1956
+ const directName = statement.type === 'assign' && isSimpleAssignment(statement)
1957
+ ? thisMemberName(expressionPropText(statement.props?.target))
1958
+ : undefined;
1959
+ if (directName)
1960
+ next.add(directName);
1961
+ nextContinuing.push(next);
1962
+ }
1963
+ continuing = nextContinuing;
1964
+ }
1965
+ return { continuing, exited };
1966
+ }
1967
+ function thisAssignmentTryPathStates(statement, initialStates) {
1968
+ const children = statement.children ?? [];
1969
+ const catchNode = children.find((child) => child.type === 'catch');
1970
+ const finallyNode = children.find((child) => child.type === 'finally');
1971
+ const tryChildren = children.filter((child) => child.type !== 'catch' && child.type !== 'finally');
1972
+ const tryStates = thisAssignmentPathStatesInStatements(tryChildren, initialStates);
1973
+ const catchStates = catchNode
1974
+ ? thisAssignmentPathStatesInStatements(catchNode.children ?? [], initialStates)
1975
+ : { continuing: [], exited: [] };
1976
+ const continuing = [...tryStates.continuing, ...catchStates.continuing];
1977
+ const exited = [...tryStates.exited, ...catchStates.exited];
1978
+ if (!finallyNode)
1979
+ return { continuing, exited };
1980
+ const continuingAfterFinally = thisAssignmentPathStatesInStatements(finallyNode.children ?? [], continuing);
1981
+ const exitingAfterFinally = thisAssignmentPathStatesInStatements(finallyNode.children ?? [], exited);
1982
+ return {
1983
+ continuing: continuingAfterFinally.continuing,
1984
+ exited: [...continuingAfterFinally.exited, ...exitingAfterFinally.continuing, ...exitingAfterFinally.exited],
1985
+ };
1986
+ }
1987
+ function isSimpleAssignment(statement) {
1988
+ const op = statement.props?.op;
1989
+ return op === undefined || op === null || op === '' || op === '=';
1990
+ }
1991
+ function setIntersection(left, right) {
1992
+ const out = new Set();
1993
+ for (const value of left) {
1994
+ if (right.has(value))
1995
+ out.add(value);
1996
+ }
1997
+ return out;
1998
+ }
1999
+ function uninitializedRequiredFieldNames(info, initializedFields) {
2000
+ const initialized = new Set(initializedFields);
2001
+ return requiredInstanceFieldNames(info).filter((name) => !initialized.has(name));
2002
+ }
2003
+ function thisMemberName(text) {
2004
+ if (!text)
2005
+ return undefined;
2006
+ try {
2007
+ const value = parseExpression(text);
2008
+ if (value.kind === 'member' && value.object.kind === 'ident' && value.object.name === 'this') {
2009
+ return value.property;
2010
+ }
2011
+ if (value.kind === 'index' &&
2012
+ value.object.kind === 'ident' &&
2013
+ value.object.name === 'this' &&
2014
+ value.index.kind === 'strLit') {
2015
+ return value.index.value;
2016
+ }
2017
+ return undefined;
2018
+ }
2019
+ catch {
2020
+ return undefined;
2021
+ }
2022
+ }
2023
+ function superCallCountInNode(node) {
2024
+ let count = 0;
2025
+ walkSemanticTreeUntil(node, (candidate) => {
2026
+ if (candidate !== node && candidate.type === 'class')
2027
+ return 'stop';
2028
+ for (const prop of BODY_EXPRESSION_PROPS) {
2029
+ const text = expressionPropText(candidate.props?.[prop]);
2030
+ if (!text)
2031
+ continue;
2032
+ try {
2033
+ count += valueIRSuperConstructorCallCount(parseExpression(text));
2034
+ }
2035
+ catch {
2036
+ // Unparseable expression text contributes no super() calls.
2037
+ }
2038
+ }
2039
+ return 'continue';
2040
+ });
2041
+ return count;
2042
+ }
2043
+ function valueIRSuperConstructorCallCount(value) {
2044
+ if (value.kind === 'lambda')
2045
+ return 0;
2046
+ const own = value.kind === 'call' && value.callee.kind === 'ident' && value.callee.name === 'super' ? 1 : 0;
2047
+ return own + valueIRChildren(value).reduce((count, child) => count + valueIRSuperConstructorCallCount(child), 0);
2048
+ }
2049
+ function effectiveClassMemberFacts(info, classByName, seen = new Set()) {
2050
+ const effective = new Map();
2051
+ if (seen.has(info.name) || classInfoParticipatesInCycle(info, classByName)) {
2052
+ return info.members.map((member) => classMemberSemanticFact(member, info.name));
2053
+ }
2054
+ const nextSeen = new Set(seen);
2055
+ nextSeen.add(info.name);
2056
+ const base = info.baseName ? classByName.get(info.baseName) : undefined;
2057
+ if (base) {
2058
+ for (const member of effectiveClassMemberFacts(base, classByName, nextSeen)) {
2059
+ effective.set(classMemberEffectiveKey(member), {
2060
+ ...member,
2061
+ className: info.name,
2062
+ inheritedFrom: member.inheritedFrom ?? member.owner,
2063
+ });
2064
+ }
2065
+ }
2066
+ const ownGroups = new Map();
2067
+ for (const member of info.members) {
2068
+ const group = ownGroups.get(classMemberShapeKey(member)) ?? [];
2069
+ group.push(member);
2070
+ ownGroups.set(classMemberShapeKey(member), group);
2071
+ }
2072
+ for (const [shapeKey, members] of ownGroups) {
2073
+ const first = members[0];
2074
+ if (!first)
2075
+ continue;
2076
+ for (const key of [...effective.keys()]) {
2077
+ if (classMemberShapeKey(effective.get(key) ?? first) === shapeKey)
2078
+ effective.delete(key);
2079
+ }
2080
+ for (const member of members) {
2081
+ effective.set(classMemberEffectiveKey(member), classMemberSemanticFact(member, info.name));
2082
+ }
2083
+ }
2084
+ return [...effective.values()];
2085
+ }
2086
+ function classMemberShapeKey(member) {
2087
+ return `${member.static ? 'static' : 'instance'}:${member.name}`;
2088
+ }
2089
+ function classMemberEffectiveKey(member) {
2090
+ return `${classMemberShapeKey(member)}:${member.kind}`;
2091
+ }
2092
+ function classInfoParticipatesInCycle(info, classByName) {
2093
+ const seen = new Set();
2094
+ let current = info;
2095
+ while (current) {
2096
+ if (seen.has(current.name))
2097
+ return true;
2098
+ seen.add(current.name);
2099
+ current = current.baseName ? classByName.get(current.baseName) : undefined;
2100
+ }
2101
+ return false;
2102
+ }
2103
+ // ── Abstract-class contract enforcement ──────────────────────────────────────
2104
+ // KERN owns its abstract contract at the VALIDATOR layer (codegen/runtime stay
2105
+ // the loud backstop): a concrete class must implement every abstract member it
2106
+ // inherits, and an abstract class may never be instantiated. The validator runs
2107
+ // before codegen, so enforcement is parity-free — TS and Python reject the same
2108
+ // programs by construction.
2109
+ //
2110
+ // PR3 convention: an "abstract member" is a handler-less method/getter/setter
2111
+ // declared under an `abstract=true` class. Fields always carry a value, so they
2112
+ // are never abstract.
2113
+ function isAbstractClassNode(node) {
2114
+ const raw = node.props?.abstract;
2115
+ return raw === true || raw === 'true';
2116
+ }
2117
+ function memberHasHandler(node) {
2118
+ return (node.children ?? []).some((child) => child.type === 'handler');
2119
+ }
2120
+ function abstractObligationKey(member) {
2121
+ return `${member.static ? 'static' : 'instance'}:${member.name}:${member.kind}`;
2122
+ }
2123
+ // Walk the lineage base→derived and return the abstract members still owed by
2124
+ // `info`. Keyed by (static, name, kind) so a getter override never clears the
2125
+ // sibling setter obligation, and a same-name different-kind member never erases
2126
+ // an inherited abstract member (the exact soundness hole that drove this off
2127
+ // `effectiveClassMemberFacts`, which collapses members by name+static only).
2128
+ function collectAbstractObligations(info, classByName, seen = new Set()) {
2129
+ // Inheritance cycles carry their own primary diagnostic; do not also walk a
2130
+ // cyclic chain here (it would never terminate cleanly nor add signal).
2131
+ if (seen.has(info.name) || classInfoParticipatesInCycle(info, classByName))
2132
+ return [];
2133
+ const nextSeen = new Set(seen);
2134
+ nextSeen.add(info.name);
2135
+ const obligations = new Map();
2136
+ const base = info.baseName ? classByName.get(info.baseName) : undefined;
2137
+ if (base) {
2138
+ for (const obligation of collectAbstractObligations(base, classByName, nextSeen)) {
2139
+ obligations.set(abstractObligationKey(obligation), obligation);
2140
+ }
2141
+ }
2142
+ const ownIsAbstract = isAbstractClassNode(info.node);
2143
+ for (const member of info.members) {
2144
+ if (member.kind === 'field')
2145
+ continue; // fields are never abstract
2146
+ const key = abstractObligationKey(member);
2147
+ if (memberHasHandler(member.node)) {
2148
+ // A concrete definition for this exact (static,name,kind) satisfies the
2149
+ // obligation — same-kind only.
2150
+ obligations.delete(key);
2151
+ }
2152
+ else if (ownIsAbstract) {
2153
+ // Handler-less member under an abstract owner declares an obligation.
2154
+ obligations.set(key, {
2155
+ name: member.name,
2156
+ kind: member.kind,
2157
+ static: member.static,
2158
+ declaredIn: info.name,
2159
+ });
2160
+ }
2161
+ // A handler-less member under a CONCRETE owner neither satisfies nor
2162
+ // declares: any inherited obligation stands and is flagged below.
2163
+ }
2164
+ return [...obligations.values()].sort((a, b) => abstractObligationKey(a).localeCompare(abstractObligationKey(b)));
2165
+ }
2166
+ function validateClassAbstractMembers(info, classByName, violations) {
2167
+ // Abstract classes are allowed to carry (and inherit) abstract members.
2168
+ if (isAbstractClassNode(info.node))
2169
+ return;
2170
+ for (const obligation of collectAbstractObligations(info, classByName)) {
2171
+ violations.push({
2172
+ rule: 'class-abstract-member-unimplemented',
2173
+ nodeType: info.node.type,
2174
+ message: `Concrete class '${info.name}' must implement abstract ${obligation.kind} '${obligation.name}' inherited from '${obligation.declaredIn}'.`,
2175
+ line: info.node.loc?.line,
2176
+ col: info.node.loc?.col,
2177
+ });
2178
+ }
2179
+ }
2180
+ // Resolve the class a `new` expression constructs. KERN parses `new` greedily
2181
+ // (the argument is a full postfix chain), and codegen prefixes `new ` to the
2182
+ // emitted chain, so KERN follows JS `new` precedence:
2183
+ // new Shape -> Shape
2184
+ // new Shape() -> Shape
2185
+ // new Shape().area() -> Shape (new binds to Shape(); `.area()` is after)
2186
+ // new pkg.Shape() -> pkg.Shape (qualified) -> not a bare local class, skip
2187
+ // new makeShape()() -> makeShape (head ident; not a class -> skipped on lookup)
2188
+ // We descend the spine to the head ident and skip qualified constructors (a head
2189
+ // reached as a member's object, e.g. `pkg.Shape`).
2190
+ function newExpressionClassName(argument) {
2191
+ let node = argument;
2192
+ let edge = 'root';
2193
+ while (true) {
2194
+ switch (node.kind) {
2195
+ case 'ident':
2196
+ // A member-object head (`pkg.Shape`) is a qualified constructor; every
2197
+ // other head (root, or a called ident) is a bare construction target.
2198
+ return edge === 'object' ? undefined : node.name;
2199
+ case 'call':
2200
+ node = node.callee;
2201
+ edge = 'callee';
2202
+ continue;
2203
+ case 'member':
2204
+ node = node.object;
2205
+ edge = 'object';
2206
+ continue;
2207
+ case 'index':
2208
+ node = node.object;
2209
+ edge = 'object';
2210
+ continue;
2211
+ case 'nonNull':
2212
+ node = node.expression;
2213
+ continue;
2214
+ default:
2215
+ return undefined; // dynamic / non-resolvable constructor
2216
+ }
2217
+ }
2218
+ }
2219
+ // `default` is an executable initializer site (field `default=` and
2220
+ // `param default=`) that is NOT in BODY_EXPRESSION_PROPS — field initializers
2221
+ // treat `value` and `default` equivalently and both lower to runtime code, so a
2222
+ // `new Abstract()` in a default must be checked too. Scanned local to this pass
2223
+ // so the shared super-detection / shape-usage walks are unaffected. A non-`new`
2224
+ // default just parses to a harmless expression that matches nothing.
2225
+ const INSTANTIATION_EXPRESSION_PROPS = [...BODY_EXPRESSION_PROPS, 'default'];
2226
+ // ── Enum reverse-index / iteration gate (symmetric, fail-closed) ──────────
2227
+ //
2228
+ // KERN enums lower to plain int/str members on BOTH targets — a TS `enum`
2229
+ // (reverse-map only on NON-const enums) and a Python namespace class (no
2230
+ // reverse map, no iteration protocol). Two enum operations are therefore NOT
2231
+ // representable identically and are rejected at compile time on BOTH targets so
2232
+ // neither silently diverges:
2233
+ // 1. REVERSE-INDEX `Status[0]` — TS non-const enums build a numeric→name
2234
+ // reverse map; the Python namespace class has none. (const enums forbid it
2235
+ // on TS already.)
2236
+ // 2. ITERATION `Object.keys(Status)` / `Object.values(Status)` /
2237
+ // `Object.entries(Status)` — TS enumerates BOTH forward and reverse keys;
2238
+ // the Python class would enumerate only the forward names (plus dunders).
2239
+ //
2240
+ // Scope honesty (documented in the diagnostic): this rejects DYNAMIC-key access
2241
+ // on enums in v1. Static member access (`Status.Pending`) is unaffected.
2242
+ const ENUM_REFLECTION_CALLEES = new Set(['keys', 'values', 'entries']);
2243
+ function validateEnumAccess(root, violations) {
2244
+ const enumNames = new Set();
2245
+ walkSemanticTree(root, (node) => {
2246
+ if (node.type === 'enum') {
2247
+ const name = typeof node.props?.name === 'string' ? node.props.name : undefined;
2248
+ if (name)
2249
+ enumNames.add(name);
2250
+ }
2251
+ });
2252
+ if (enumNames.size === 0)
2253
+ return;
2254
+ // ZERO-FP shadowing guard (kern-codex probe): a value binding of the same
2255
+ // name ANYWHERE in the program (`let Status = [10, 20]`) makes `Status[0]`
2256
+ // plain indexing in that scope. This pass is scope-blind, so it must be
2257
+ // conservative: drop a shadowed name from the gate entirely rather than
2258
+ // false-reject legal code. Real scope tracking is a later slice.
2259
+ const SHADOWING_DECL_TYPES = new Set(['let', 'const', 'param']);
2260
+ walkSemanticTree(root, (node) => {
2261
+ if (!SHADOWING_DECL_TYPES.has(node.type))
2262
+ return;
2263
+ const name = typeof node.props?.name === 'string' ? node.props.name : undefined;
2264
+ if (name)
2265
+ enumNames.delete(name);
2266
+ });
2267
+ if (enumNames.size === 0)
2268
+ return;
2269
+ walkSemanticTree(root, (node) => {
2270
+ // An enum `member`'s value prop is an initializer literal, not a body
2271
+ // expression — scanning it false-flagged quoted string values like
2272
+ // `value="Object.keys(E)"` as enum-iteration (kern-codex probe). Members
2273
+ // carry no body expressions, so skip the node entirely.
2274
+ if (node.type === 'member')
2275
+ return;
2276
+ for (const prop of INSTANTIATION_EXPRESSION_PROPS) {
2277
+ const text = expressionPropText(node.props?.[prop]);
2278
+ if (!text)
2279
+ continue;
2280
+ let value;
2281
+ try {
2282
+ value = parseExpression(text);
2283
+ }
2284
+ catch {
2285
+ continue;
2286
+ }
2287
+ collectEnumAccessViolations(value, node, enumNames, violations);
2288
+ }
2289
+ });
2290
+ }
2291
+ function collectEnumAccessViolations(value, node, enumNames, violations) {
2292
+ // Reverse-index: `Status[<anything>]` where the object is a bare enum ident.
2293
+ if (value.kind === 'index' && value.object.kind === 'ident' && enumNames.has(value.object.name)) {
2294
+ violations.push({
2295
+ rule: 'enum-reverse-index',
2296
+ nodeType: node.type,
2297
+ message: `Reverse-index access on enum '${value.object.name}' (e.g. \`${value.object.name}[...]\`) is not supported in v1 — it diverges between targets (TS builds a numeric→name reverse map; the Python namespace class has none). Use a static member like '${value.object.name}.MemberName' instead.`,
2298
+ line: node.loc?.line,
2299
+ col: node.loc?.col,
2300
+ });
2301
+ }
2302
+ // Iteration: `Object.keys(Status)` / `.values(...)` / `.entries(...)`.
2303
+ if (value.kind === 'call' &&
2304
+ value.callee.kind === 'member' &&
2305
+ value.callee.object.kind === 'ident' &&
2306
+ value.callee.object.name === 'Object' &&
2307
+ ENUM_REFLECTION_CALLEES.has(value.callee.property)) {
2308
+ const firstArg = value.args[0];
2309
+ if (firstArg && firstArg.kind === 'ident' && enumNames.has(firstArg.name)) {
2310
+ violations.push({
2311
+ rule: 'enum-iteration',
2312
+ nodeType: node.type,
2313
+ message: `Enum iteration via \`Object.${value.callee.property}(${firstArg.name})\` is not supported in v1 — it diverges between targets (TS enumerates both forward and reverse-map keys; the Python namespace class enumerates only forward member names). Dynamic-key access on enums is not available in v1.`,
2314
+ line: node.loc?.line,
2315
+ col: node.loc?.col,
2316
+ });
2317
+ }
2318
+ }
2319
+ for (const child of valueIRChildren(value)) {
2320
+ collectEnumAccessViolations(child, node, enumNames, violations);
2321
+ }
2322
+ }
2323
+ // Module-wide pass: reject `new <AbstractClass>(...)` anywhere — including inside
2324
+ // the abstract class's own static factory (KERN matches TS: abstract is not
2325
+ // self-instantiable). Conservative by design — non-ident callees, names not
2326
+ // resolving to a visible local class, and (consistent with every other
2327
+ // class-name resolution in this validator) names rebound by a local binding are
2328
+ // not pursued; the validator does not track lexical shadowing for any class
2329
+ // reference, so abstract instantiation follows the same name+visibility rule.
2330
+ // Multi-root note: visibleNamesByRoot unions every root's declared class names
2331
+ // (as extends/implements resolution already does), so this resolves classes
2332
+ // across roots; all production callers validate a single root, so the
2333
+ // cross-root union is not a false-positive surface in practice.
2334
+ function validateAbstractInstantiations(roots, classByName, visibleNamesByRoot, violations) {
2335
+ roots.forEach((root, rootIndex) => {
2336
+ const visible = visibleNamesByRoot[rootIndex];
2337
+ walkSemanticTree(root, (node) => {
2338
+ for (const prop of INSTANTIATION_EXPRESSION_PROPS) {
2339
+ const text = expressionPropText(node.props?.[prop]);
2340
+ if (!text)
2341
+ continue;
2342
+ let value;
2343
+ try {
2344
+ value = parseExpression(text);
2345
+ }
2346
+ catch {
2347
+ continue;
2348
+ }
2349
+ collectAbstractInstantiations(value, node, visible, classByName, violations);
2350
+ }
2351
+ });
2352
+ });
2353
+ }
2354
+ function collectAbstractInstantiations(value, node, visible, classByName, violations) {
2355
+ if (value.kind === 'new') {
2356
+ const name = newExpressionClassName(value.argument);
2357
+ if (name && (!visible || visible.has(name))) {
2358
+ const target = classByName.get(name);
2359
+ if (target && isAbstractClassNode(target.node)) {
2360
+ violations.push({
2361
+ rule: 'class-abstract-instantiation',
2362
+ nodeType: node.type,
2363
+ message: `Cannot instantiate abstract class '${name}'.`,
2364
+ line: node.loc?.line,
2365
+ col: node.loc?.col,
2366
+ });
2367
+ }
2368
+ }
2369
+ }
2370
+ for (const child of valueIRChildren(value)) {
2371
+ collectAbstractInstantiations(child, node, visible, classByName, violations);
2372
+ }
2373
+ }
2374
+ function collectClassOverrideFacts(classes, classByName) {
2375
+ const overrides = [];
2376
+ for (const info of classes) {
2377
+ for (const member of info.members) {
2378
+ const baseMember = findBaseMember(info, member, classByName);
2379
+ if (!baseMember)
2380
+ continue;
2381
+ overrides.push({
2382
+ className: info.name,
2383
+ memberName: member.name,
2384
+ static: member.static,
2385
+ kind: member.kind,
2386
+ arity: member.arity,
2387
+ baseClassName: baseMember.owner,
2388
+ baseKind: baseMember.kind,
2389
+ baseArity: baseMember.arity,
2390
+ status: classOverrideStatus(member, baseMember, classByName),
2391
+ ...(member.node.loc ? { loc: semanticLocation(member.node) } : {}),
2392
+ });
2393
+ }
2394
+ }
2395
+ return overrides;
2396
+ }
2397
+ function classOverrideStatus(member, baseMember, classByName) {
2398
+ if (!sameOverrideKind(member, baseMember))
2399
+ return 'kind-mismatch';
2400
+ if (member.kind === 'method' && baseMember.kind === 'method' && member.arity !== baseMember.arity) {
2401
+ return 'arity-mismatch';
2402
+ }
2403
+ const variance = checkOverrideVariance(member, baseMember, classByName);
2404
+ if (variance)
2405
+ return variance;
2406
+ return 'compatible';
2407
+ }
2408
+ /**
2409
+ * Liskov substitutability check for a member override against its base member.
2410
+ *
2411
+ * Runs ONLY when kinds are strictly equal (method/method, getter/getter,
2412
+ * setter/setter). Mixed accessor pairs (getter overriding setter, or vice
2413
+ * versa) and fields return null (skip) to preserve existing behavior. For
2414
+ * methods, it assumes arity has already matched (arity-mismatch fires first).
2415
+ *
2416
+ * Variance rules:
2417
+ * - Return position is COVARIANT: an override may narrow the return type
2418
+ * (override.returns must be a subtype of base.returns). A non-subtype is a
2419
+ * 'return-mismatch'.
2420
+ * - Param positions are CONTRAVARIANT: an override may widen a param type
2421
+ * (base.paramTypes[i] must be a subtype of override.paramTypes[i]). A
2422
+ * non-subtype is a 'param-mismatch'.
2423
+ *
2424
+ * 'unknown' subtype results (gradual typing — primitives, unannotated, or
2425
+ * non-class names) are skipped, so the check produces zero false positives.
2426
+ */
2427
+ export function checkOverrideVariance(member, baseMember, classByName) {
2428
+ if (member.kind !== baseMember.kind)
2429
+ return null;
2430
+ if (member.kind === 'field')
2431
+ return null;
2432
+ if (member.kind === 'method') {
2433
+ if (member.arity !== baseMember.arity)
2434
+ return null;
2435
+ if (isNominalSubtype(member.returns, baseMember.returns, classByName) === false) {
2436
+ return 'return-mismatch';
2437
+ }
2438
+ for (let index = 0; index < member.paramTypes.length; index += 1) {
2439
+ if (isNominalSubtype(baseMember.paramTypes[index], member.paramTypes[index], classByName) === false) {
2440
+ return 'param-mismatch';
2441
+ }
2442
+ }
2443
+ return null;
2444
+ }
2445
+ if (member.kind === 'getter') {
2446
+ if (isNominalSubtype(member.returns, baseMember.returns, classByName) === false) {
2447
+ return 'return-mismatch';
2448
+ }
2449
+ return null;
2450
+ }
2451
+ // setter: param position 0 only, same contravariant direction.
2452
+ if (isNominalSubtype(baseMember.paramTypes[0], member.paramTypes[0], classByName) === false) {
2453
+ return 'param-mismatch';
2454
+ }
2455
+ return null;
2456
+ }
2457
+ /**
2458
+ * Nominal subtype check: is `sub` a (non-strict) subtype of `sup`?
2459
+ * - undefined on either side → 'unknown' (gradual: caller skips).
2460
+ * - sub === sup → true.
2461
+ * - either name not a known class in classByName → 'unknown' (primitives /
2462
+ * external / unresolved types are not compared).
2463
+ * - else cycle-safe walk of sub's baseName chain; reaching sup → true; chain
2464
+ * ends or cycles without reaching sup → false.
2465
+ */
2466
+ export function isNominalSubtype(sub, sup, classByName) {
2467
+ if (sub === undefined || sup === undefined)
2468
+ return 'unknown';
2469
+ if (sub === sup)
2470
+ return true;
2471
+ if (!classByName.has(sub) || !classByName.has(sup))
2472
+ return 'unknown';
2473
+ let current = classByName.get(sub);
2474
+ const visited = new Set();
2475
+ while (current) {
2476
+ if (current.name === sup)
2477
+ return true;
2478
+ if (visited.has(current.name))
2479
+ return false;
2480
+ visited.add(current.name);
2481
+ current = current.baseName ? classByName.get(current.baseName) : undefined;
2482
+ }
2483
+ return false;
2484
+ }
2485
+ function collectClassCycleFacts(classes, classByName) {
2486
+ const cycles = [];
2487
+ const emitted = new Set();
2488
+ for (const info of classes) {
2489
+ const path = [];
2490
+ const seen = new Set();
2491
+ let current = info;
2492
+ while (current) {
2493
+ if (seen.has(current.name)) {
2494
+ const cycleStart = path.indexOf(current.name);
2495
+ const cycleNames = path.slice(cycleStart);
2496
+ const cycleKey = normalizedCycleKey(cycleNames);
2497
+ if (!emitted.has(cycleKey)) {
2498
+ emitted.add(cycleKey);
2499
+ cycles.push([...cycleNames, current.name]);
2500
+ }
2501
+ break;
2502
+ }
2503
+ seen.add(current.name);
2504
+ path.push(current.name);
2505
+ current = current.baseName ? classByName.get(current.baseName) : undefined;
2506
+ }
2507
+ }
2508
+ return cycles;
2509
+ }
2510
+ function collectClassProtocolConformanceFacts(classes, interfaceByName, visibleProtocolNamesByRoot, protocolShapeContext, classByName) {
2511
+ const facts = [];
2512
+ for (const info of classes) {
2513
+ for (const interfaceName of info.implementsNames) {
2514
+ const protocol = interfaceByName.get(interfaceName);
2515
+ if (!protocol) {
2516
+ const visible = (visibleProtocolNamesByRoot[info.rootIndex] ?? new Set()).has(interfaceName);
2517
+ facts.push({
2518
+ className: info.name,
2519
+ interfaceName,
2520
+ status: visible ? 'external' : 'unknown-interface',
2521
+ missingMembers: [],
2522
+ satisfiedMembers: [],
2523
+ missingStaticMembers: [],
2524
+ satisfiedStaticMembers: [],
2525
+ ...(info.node.loc ? { loc: semanticLocation(info.node) } : {}),
2526
+ });
2527
+ continue;
2528
+ }
2529
+ const result = classInterfaceConformance(info, protocol, protocolShapeContext, classByName, interfaceByName);
2530
+ facts.push({
2531
+ className: info.name,
2532
+ interfaceName,
2533
+ status: result.status,
2534
+ missingMembers: result.missingMembers,
2535
+ satisfiedMembers: result.satisfiedMembers,
2536
+ missingStaticMembers: result.missingStaticMembers,
2537
+ satisfiedStaticMembers: result.satisfiedStaticMembers,
2538
+ ...(result.diagnostics.length > 0 ? { diagnostics: result.diagnostics } : {}),
2539
+ ...(result.unsupportedReasons.length > 0 ? { unsupportedReasons: result.unsupportedReasons } : {}),
2540
+ ...(info.node.loc ? { loc: semanticLocation(info.node) } : {}),
2541
+ });
2542
+ }
2543
+ }
2544
+ return facts;
2545
+ }
2546
+ function classInterfaceConformance(info, protocol, protocolShapeContext, classByName, interfaceByName) {
2547
+ const shape = protocolShapeContext.shapeByName.get(protocol.name);
2548
+ const diagnostics = (protocolShapeContext.diagnosticsByName.get(protocol.name) ?? []).map((diagnostic) => diagnostic.code);
2549
+ if (diagnostics.length > 0) {
2550
+ return {
2551
+ status: 'invalid-interface',
2552
+ missingMembers: [],
2553
+ satisfiedMembers: [],
2554
+ missingStaticMembers: [],
2555
+ satisfiedStaticMembers: [],
2556
+ diagnostics: sortedUnique(diagnostics),
2557
+ unsupportedReasons: [],
2558
+ };
2559
+ }
2560
+ if (shape && (shape.indexers.length > 0 || !shape.validatorAvailable)) {
2561
+ return {
2562
+ status: 'unsupported-protocol',
2563
+ missingMembers: [],
2564
+ satisfiedMembers: [],
2565
+ missingStaticMembers: [],
2566
+ satisfiedStaticMembers: [],
2567
+ diagnostics: [],
2568
+ unsupportedReasons: sortedUnique([
2569
+ ...shape.unsupportedReasons,
2570
+ ...(shape.indexers.length > 0 ? ['indexer'] : []),
2571
+ ]),
2572
+ };
2573
+ }
2574
+ const effectiveMembers = effectiveClassMemberFacts(info, classByName);
2575
+ const fields = effectiveInterfaceFields(protocol, interfaceByName);
2576
+ const requiredFields = fields.filter((field) => !field.optional && !field.static);
2577
+ const requiredStaticFields = fields.filter((field) => !field.optional && field.static);
2578
+ const requiredMethods = effectiveInterfaceMethods(protocol, interfaceByName);
2579
+ const requiredInstanceMethods = requiredMethods.filter((method) => !method.static);
2580
+ const requiredStaticMethods = requiredMethods.filter((method) => method.static);
2581
+ const missingMembers = [];
2582
+ const satisfiedMembers = [];
2583
+ const missingStaticMembers = [];
2584
+ const satisfiedStaticMembers = [];
2585
+ for (const field of requiredFields) {
2586
+ if (classHasReadableMember(effectiveMembers, field, false)) {
2587
+ satisfiedMembers.push(field.name);
2588
+ }
2589
+ else {
2590
+ missingMembers.push(field.name);
2591
+ }
2592
+ }
2593
+ for (const field of requiredStaticFields) {
2594
+ if (classHasReadableMember(effectiveMembers, field, true)) {
2595
+ satisfiedStaticMembers.push(field.name);
2596
+ }
2597
+ else {
2598
+ missingStaticMembers.push(field.name);
2599
+ }
2600
+ }
2601
+ for (const method of requiredInstanceMethods) {
2602
+ if (classHasCallableMethod(effectiveMembers, method, false)) {
2603
+ satisfiedMembers.push(method.name);
2604
+ }
2605
+ else {
2606
+ missingMembers.push(method.name);
2607
+ }
2608
+ }
2609
+ for (const method of requiredStaticMethods) {
2610
+ if (classHasCallableMethod(effectiveMembers, method, true)) {
2611
+ satisfiedStaticMembers.push(method.name);
2612
+ }
2613
+ else {
2614
+ missingStaticMembers.push(method.name);
2615
+ }
2616
+ }
2617
+ const missing = sortedUnique(missingMembers);
2618
+ const satisfied = sortedUnique(satisfiedMembers);
2619
+ const missingStatic = sortedUnique(missingStaticMembers);
2620
+ const satisfiedStatic = sortedUnique(satisfiedStaticMembers);
2621
+ return {
2622
+ status: missing.length > 0 || missingStatic.length > 0 ? 'missing-members' : 'satisfied',
2623
+ missingMembers: missing,
2624
+ satisfiedMembers: satisfied,
2625
+ missingStaticMembers: missingStatic,
2626
+ satisfiedStaticMembers: satisfiedStatic,
2627
+ diagnostics: [],
2628
+ unsupportedReasons: [],
2629
+ };
2630
+ }
2631
+ function effectiveInterfaceFields(protocol, interfaceByName, seen = new Set()) {
2632
+ if (seen.has(protocol.name))
2633
+ return [];
2634
+ const nextSeen = new Set(seen);
2635
+ nextSeen.add(protocol.name);
2636
+ const fields = new Map();
2637
+ for (const baseName of protocol.extendsNames) {
2638
+ const base = interfaceByName.get(baseName);
2639
+ if (!base)
2640
+ continue;
2641
+ for (const field of effectiveInterfaceFields(base, interfaceByName, nextSeen)) {
2642
+ fields.set(interfaceMemberShapeKey(field), field);
2643
+ }
2644
+ }
2645
+ for (const field of protocol.fields)
2646
+ fields.set(interfaceMemberShapeKey(field), field);
2647
+ return [...fields.values()];
2648
+ }
2649
+ function effectiveInterfaceMethods(protocol, interfaceByName, seen = new Set()) {
2650
+ if (seen.has(protocol.name))
2651
+ return [];
2652
+ const nextSeen = new Set(seen);
2653
+ nextSeen.add(protocol.name);
2654
+ const methods = new Map();
2655
+ for (const baseName of protocol.extendsNames) {
2656
+ const base = interfaceByName.get(baseName);
2657
+ if (!base)
2658
+ continue;
2659
+ for (const method of effectiveInterfaceMethods(base, interfaceByName, nextSeen)) {
2660
+ methods.set(interfaceMemberShapeKey(method), method);
2661
+ }
2662
+ }
2663
+ for (const method of protocol.methods)
2664
+ methods.set(interfaceMemberShapeKey(method), method);
2665
+ return [...methods.values()];
2666
+ }
2667
+ function interfaceMemberShapeKey(member) {
2668
+ return `${member.static ? 'static' : 'instance'}:${member.name}`;
2669
+ }
2670
+ function classHasReadableMember(members, field, staticOnly) {
2671
+ return members.some((member) => {
2672
+ if (member.name !== field.name || member.static !== staticOnly || member.private)
2673
+ return false;
2674
+ if (member.kind !== 'field' && member.kind !== 'getter')
2675
+ return false;
2676
+ const actualType = member.kind === 'getter' ? member.returns : member.type;
2677
+ return !field.type || actualType === field.type;
2678
+ });
2679
+ }
2680
+ function classHasCallableMethod(members, method, staticOnly) {
2681
+ return members.some((member) => {
2682
+ if (member.name !== method.name || member.static !== staticOnly || member.private || member.kind !== 'method') {
2683
+ return false;
2684
+ }
2685
+ if (member.arity !== method.arity)
2686
+ return false;
2687
+ if (!methodParamTypesCompatible(member.paramTypes ?? [], method.paramTypes))
2688
+ return false;
2689
+ if ((member.async === true) !== method.async)
2690
+ return false;
2691
+ if ((member.stream === true) !== method.stream)
2692
+ return false;
2693
+ if ((member.generator === true) !== method.generator)
2694
+ return false;
2695
+ return methodReturnTypesCompatible(member.returns, {
2696
+ async: member.async === true,
2697
+ stream: member.stream === true,
2698
+ generator: member.generator === true,
2699
+ }, method.returns, method);
2700
+ });
2701
+ }
2702
+ function methodParamTypesCompatible(actual, expected) {
2703
+ return expected.every((type, index) => !type || normalizeProtocolType(actual[index]) === normalizeProtocolType(type));
2704
+ }
2705
+ function normalizeProtocolType(type) {
2706
+ return compactProtocolTypeWhitespace(type);
2707
+ }
2708
+ function compactProtocolTypeWhitespace(type) {
2709
+ let out = '';
2710
+ let quote = null;
2711
+ for (let index = 0; index < (type ?? '').length; index += 1) {
2712
+ const ch = (type ?? '')[index];
2713
+ if (quote !== null) {
2714
+ out += ch;
2715
+ if (ch === '\\' && index + 1 < (type ?? '').length)
2716
+ out += (type ?? '')[++index];
2717
+ else if (ch === quote)
2718
+ quote = null;
2719
+ continue;
2720
+ }
2721
+ if (ch === '"' || ch === "'" || ch === '`') {
2722
+ quote = ch;
2723
+ out += ch;
2724
+ continue;
2725
+ }
2726
+ if (!/\s/.test(ch))
2727
+ out += ch;
2728
+ }
2729
+ return out;
2730
+ }
2731
+ function methodReturnTypesCompatible(actual, actualFlags, expected, expectedFlags) {
2732
+ return normalizeMethodReturnType(actual, actualFlags) === normalizeMethodReturnType(expected, expectedFlags);
2733
+ }
2734
+ function normalizeMethodReturnType(returns, flags) {
2735
+ if (flags.stream) {
2736
+ if (returns?.startsWith('AsyncGenerator<'))
2737
+ return returns;
2738
+ return `AsyncGenerator<${returns || 'unknown'}>`;
2739
+ }
2740
+ if (flags.generator) {
2741
+ if (returns?.startsWith('Generator<') || returns?.startsWith('AsyncGenerator<'))
2742
+ return returns;
2743
+ return `${flags.async ? 'AsyncGenerator' : 'Generator'}<${returns || 'unknown'}>`;
2744
+ }
2745
+ return !returns || returns === 'void' ? 'void' : returns;
2746
+ }
2747
+ function semanticLocation(node) {
2748
+ return node.loc ? { line: node.loc.line, col: node.loc.col } : undefined;
2749
+ }
2750
+ function isClassMemberNode(node) {
2751
+ return node.type === 'field' || node.type === 'method' || node.type === 'getter' || node.type === 'setter';
2752
+ }
2753
+ function validateClassBaseReference(info, visibleNames, violations) {
2754
+ if (!info.baseName)
2755
+ return;
2756
+ if (visibleNames.has(info.baseName) || BUILTIN_CLASS_BASES.has(info.baseName))
2757
+ return;
2758
+ violations.push({
2759
+ rule: 'class-extends-unknown',
2760
+ nodeType: 'class',
2761
+ message: `Class '${info.name}' extends unknown base '${info.baseName}'. Declare or import the base class before extending it.`,
2762
+ line: info.node.loc?.line,
2763
+ col: info.node.loc?.col,
2764
+ });
2765
+ }
2766
+ function validateClassImplements(info, interfaceByName, visibleProtocolNames, protocolShapeContext, classByName, violations) {
2767
+ if (info.implementsMalformed) {
2768
+ violations.push({
2769
+ rule: 'class-implements-invalid-reference-list',
2770
+ nodeType: 'class',
2771
+ message: `Class '${info.name}' has an invalid implements= reference list. Use a comma-separated list of interface names.`,
2772
+ line: info.node.loc?.line,
2773
+ col: info.node.loc?.col,
2774
+ });
2775
+ }
2776
+ for (const interfaceName of info.implementsNames) {
2777
+ const protocol = interfaceByName.get(interfaceName);
2778
+ if (!protocol) {
2779
+ if (!visibleProtocolNames.has(interfaceName)) {
2780
+ violations.push({
2781
+ rule: 'class-implements-unknown',
2782
+ nodeType: 'class',
2783
+ message: `Class '${info.name}' implements unknown interface '${interfaceName}'. Declare or import the interface before implementing it.`,
2784
+ line: info.node.loc?.line,
2785
+ col: info.node.loc?.col,
2786
+ });
2787
+ }
2788
+ continue;
2789
+ }
2790
+ const conformance = classInterfaceConformance(info, protocol, protocolShapeContext, classByName, interfaceByName);
2791
+ if (conformance.status === 'invalid-interface') {
2792
+ violations.push({
2793
+ rule: 'class-implements-invalid-interface',
2794
+ nodeType: 'class',
2795
+ message: `Class '${info.name}' implements invalid interface '${interfaceName}' (${conformance.diagnostics.join(', ')}). Fix the interface shape before relying on protocol conformance.`,
2796
+ line: info.node.loc?.line,
2797
+ col: info.node.loc?.col,
2798
+ });
2799
+ continue;
2800
+ }
2801
+ if (conformance.status === 'unsupported-protocol') {
2802
+ violations.push({
2803
+ rule: 'class-implements-unsupported-protocol',
2804
+ nodeType: 'class',
2805
+ message: `Class '${info.name}' implements interface '${interfaceName}' whose shape is not class-satisfiable in protocol v1 (${conformance.unsupportedReasons.join(', ')}).`,
2806
+ line: info.node.loc?.line,
2807
+ col: info.node.loc?.col,
2808
+ });
2809
+ continue;
2810
+ }
2811
+ if (conformance.missingMembers.length === 0 && conformance.missingStaticMembers.length === 0)
2812
+ continue;
2813
+ const missingParts = [
2814
+ ...(conformance.missingMembers.length > 0
2815
+ ? [`instance member(s): ${conformance.missingMembers.join(', ')}`]
2816
+ : []),
2817
+ ...(conformance.missingStaticMembers.length > 0
2818
+ ? [`static member(s): ${conformance.missingStaticMembers.join(', ')}`]
2819
+ : []),
2820
+ ];
2821
+ violations.push({
2822
+ rule: 'class-implements-missing-member',
2823
+ nodeType: 'class',
2824
+ message: `Class '${info.name}' does not satisfy interface '${interfaceName}'. Missing readable ${missingParts.join('; ')}.`,
2825
+ line: info.node.loc?.line,
2826
+ col: info.node.loc?.col,
2827
+ });
2828
+ }
2829
+ }
2830
+ function validateClassConstructors(info, violations) {
2831
+ if (info.constructors.length <= 1)
2832
+ return;
2833
+ for (const extra of info.constructors.slice(1)) {
2834
+ violations.push({
2835
+ rule: 'class-single-constructor-only',
2836
+ nodeType: 'constructor',
2837
+ message: `Class '${info.name}' declares more than one constructor. KERN classes have exactly one construction path.`,
2838
+ line: extra.loc?.line,
2839
+ col: extra.loc?.col,
2840
+ });
2841
+ }
2842
+ }
2843
+ function validateClassMemberConflicts(info, violations) {
2844
+ const seen = new Map();
2845
+ for (const member of info.members) {
2846
+ const key = `${member.static ? 'static' : 'instance'}:${member.name}`;
2847
+ const prev = seen.get(key) ?? [];
2848
+ const next = [...prev, member];
2849
+ if (isAllowedMemberGroup(next)) {
2850
+ seen.set(key, next);
2851
+ continue;
2852
+ }
2853
+ const first = prev[0] ?? member;
2854
+ violations.push({
2855
+ rule: 'class-member-conflict',
2856
+ nodeType: member.node.type,
2857
+ message: `Class '${info.name}' has conflicting ${member.static ? 'static' : 'instance'} member '${member.name}' (${first.kind} and ${member.kind}). Use one field/method/accessor surface per name.`,
2858
+ line: member.node.loc?.line,
2859
+ col: member.node.loc?.col,
2860
+ });
2861
+ seen.set(key, next);
2862
+ }
2863
+ }
2864
+ function validateClassSuperUsage(info, classByName, violations) {
2865
+ const hasBase = Boolean(info.baseName);
2866
+ const argRequiringBaseName = hasBase ? argRequiringEffectiveBaseName(info, classByName) : undefined;
2867
+ for (const ctor of info.constructors) {
2868
+ if (hasBase) {
2869
+ validateDerivedConstructorSuper(info, ctor, argRequiringBaseName, violations);
2870
+ }
2871
+ if (!hasBase && nodeBodyUsesSuper(ctor)) {
2872
+ violations.push({
2873
+ rule: 'class-super-without-base',
2874
+ nodeType: 'constructor',
2875
+ message: `Class '${info.name}' uses \`super\` but does not extend a base class.`,
2876
+ line: ctor.loc?.line,
2877
+ col: ctor.loc?.col,
2878
+ });
2879
+ }
2880
+ }
2881
+ if (!hasBase) {
2882
+ for (const member of info.members) {
2883
+ if (!nodeBodyUsesSuper(member.node))
2884
+ continue;
2885
+ violations.push({
2886
+ rule: 'class-super-without-base',
2887
+ nodeType: member.node.type,
2888
+ message: `Class '${info.name}' member '${member.name}' uses \`super\` but the class does not extend a base class.`,
2889
+ line: member.node.loc?.line,
2890
+ col: member.node.loc?.col,
2891
+ });
2892
+ }
2893
+ }
2894
+ }
2895
+ // DESCRIPTIVE analyzer — feeds the `superStatus` substrate fact (via
2896
+ // `constructorSuperDiagnostics`), NOT user-facing violations. It still classifies
2897
+ // an omitted super as `missing` and a pre-super `this` access as `this-before-super`
2898
+ // so the FACT keeps describing the constructor's structure faithfully. The
2899
+ // user-facing legality judgment lives in `validateDerivedConstructorSuper`, which
2900
+ // applies KERN's Option-C semantics on top of this description.
2901
+ function validateDerivedConstructorDiscipline(info, ctor, violations) {
2902
+ const ctx = {
2903
+ info,
2904
+ violations,
2905
+ sawSuper: false,
2906
+ emittedConditionalSuper: false,
2907
+ };
2908
+ const analysis = analyzeConstructorStatements(constructorBodyStatements(ctor), 'uninit', ctx);
2909
+ if (analysis.state !== 'init') {
2910
+ if (ctx.sawSuper) {
2911
+ emitConstructorConditionalSuper(ctx, ctor);
2912
+ }
2913
+ else {
2914
+ violations.push({
2915
+ rule: 'class-constructor-missing-super',
2916
+ nodeType: 'constructor',
2917
+ message: `Class '${info.name}' extends '${info.baseName}' but its constructor does not call \`super(...)\`. Derived constructors must initialize the base class explicitly.`,
2918
+ line: ctor.loc?.line,
2919
+ col: ctor.loc?.col,
2920
+ });
2921
+ }
2922
+ }
2923
+ }
2924
+ /**
2925
+ * User-facing derived-constructor validation under KERN's Option-C super
2926
+ * semantics. The mode is decided by the canonical `hasDirectSuperCtorCall`
2927
+ * predicate — shared verbatim with the runtime and both codegen targets so all
2928
+ * four layers agree on whether a constructor opted into explicit-super mode:
2929
+ *
2930
+ * - No direct `super(...)` call (implicit mode): KERN injects base init at
2931
+ * constructor entry, so omitting super is LEGAL and `this`/super-member access
2932
+ * is always safe. The only error is when the base constructor REQUIRES
2933
+ * arguments — an arg-less implicit super cannot satisfy it.
2934
+ * - A direct `super(...)` call exists (explicit mode): the author owns its
2935
+ * placement, so the full discipline applies — reject double-super,
2936
+ * conditional-super (not on every path), and `this`/super before super.
2937
+ *
2938
+ * `class-constructor-missing-super` is intentionally unreachable here: an omitted
2939
+ * super is no longer an error, and an explicit super means a direct call exists.
2940
+ */
2941
+ function validateDerivedConstructorSuper(info, ctor, argRequiringBaseName, violations) {
2942
+ if (!hasDirectSuperCtorCall(ctor)) {
2943
+ if (argRequiringBaseName) {
2944
+ // Name the class whose constructor actually requires args — which may be a
2945
+ // transitive ancestor reached through constructor-less bases, not the
2946
+ // immediate base — so the diagnostic points the author at the real source.
2947
+ violations.push({
2948
+ rule: 'class-constructor-implicit-super-needs-args',
2949
+ nodeType: 'constructor',
2950
+ message: `Class '${info.name}' omits \`super(...)\` but base class '${argRequiringBaseName}' has a constructor that requires arguments. Call \`super(...)\` explicitly to pass them.`,
2951
+ line: ctor.loc?.line,
2952
+ col: ctor.loc?.col,
2953
+ });
2954
+ }
2955
+ return;
2956
+ }
2957
+ // Explicit-super mode: replay the discipline analysis. Its walk emits
2958
+ // double-super / this-before-super as side effects; the tail covers "super
2959
+ // present but not on every path" (conditional-super).
2960
+ const ctx = {
2961
+ info,
2962
+ violations,
2963
+ sawSuper: false,
2964
+ emittedConditionalSuper: false,
2965
+ };
2966
+ const analysis = analyzeConstructorStatements(constructorBodyStatements(ctor), 'uninit', ctx);
2967
+ if (analysis.state !== 'init')
2968
+ emitConstructorConditionalSuper(ctx, ctor);
2969
+ }
2970
+ /**
2971
+ * The name of the EFFECTIVE base class whose constructor an implicit no-arg
2972
+ * `super()` would reach and fail to satisfy — i.e. the first ancestor that
2973
+ * declares a constructor with a required (no-default) parameter — or `undefined`
2974
+ * when implicit init succeeds. The effective base ctor is found by walking up the
2975
+ * inheritance chain through constructor-less bases, exactly as the runtime does:
2976
+ * `initializeClassLayer` forwards `[]` through a base that has no constructor
2977
+ * (`base && !ctor`) to ITS base, so the first ancestor that actually declares a
2978
+ * constructor is the one invoked with no args. Checking only the immediate base
2979
+ * would let `C extends B extends A` (B ctor-less, A arg-requiring) pass validation
2980
+ * yet throw at runtime — re-creating the validator/runtime split this
2981
+ * reconciliation closes. Returning the name (not a bool) lets the diagnostic point
2982
+ * at the real source rather than the immediate base. Mirrors the runtime's
2983
+ * required-arg rule (a param is required unless it carries a `value`/`default`); a
2984
+ * chain with no constructor anywhere (or an unresolved base) needs no args.
2985
+ */
2986
+ function argRequiringEffectiveBaseName(info, classByName) {
2987
+ const seen = new Set();
2988
+ let current = info.baseName ? classByName.get(info.baseName) : undefined;
2989
+ while (current && !seen.has(current.name)) {
2990
+ seen.add(current.name);
2991
+ const ctor = current.constructors[0];
2992
+ if (ctor) {
2993
+ const requiresArgs = (ctor.children ?? []).some((child) => child.type === 'param' &&
2994
+ !Object.hasOwn(child.props ?? {}, 'value') &&
2995
+ !Object.hasOwn(child.props ?? {}, 'default'));
2996
+ return requiresArgs ? current.name : undefined;
2997
+ }
2998
+ // Constructor-less base: the runtime forwards [] to its base — keep walking.
2999
+ current = current.baseName ? classByName.get(current.baseName) : undefined;
3000
+ }
3001
+ return undefined;
3002
+ }
3003
+ function analyzeConstructorStatements(statements, initialState, ctx) {
3004
+ let state = initialState;
3005
+ let sawSuper = false;
3006
+ for (let index = 0; index < statements.length; index += 1) {
3007
+ const statement = statements[index];
3008
+ if (statement.type === 'else')
3009
+ continue;
3010
+ const maybeElse = statement.type === 'if' && statements[index + 1]?.type === 'else' ? statements[index + 1] : undefined;
3011
+ const result = analyzeConstructorStatement(statement, maybeElse, state, ctx);
3012
+ state = result.state;
3013
+ sawSuper = sawSuper || result.sawSuper;
3014
+ if (maybeElse)
3015
+ index += 1;
3016
+ }
3017
+ return { state, sawSuper };
3018
+ }
3019
+ function analyzeConstructorStatement(statement, maybeElse, state, ctx) {
3020
+ if (statement.type === 'class')
3021
+ return { state, sawSuper: false };
3022
+ const directSuper = directSuperConstructorCall(statement);
3023
+ if (directSuper) {
3024
+ scanValueIRForPreSuperAccess(directSuper, state, ctx, statement);
3025
+ ctx.sawSuper = true;
3026
+ if (state === 'init' || state === 'maybe' || directSuper.args.some(valueIRCallsSuperConstructor)) {
3027
+ emitConstructorDoubleSuper(ctx, statement);
3028
+ }
3029
+ if (state === 'maybe')
3030
+ emitConstructorConditionalSuper(ctx, statement);
3031
+ return { state: 'init', sawSuper: true };
3032
+ }
3033
+ if (statement.type === 'if')
3034
+ return analyzeConstructorIf(statement, maybeElse, state, ctx);
3035
+ const sawSuper = scanConstructorStatementExpressions(statement, state, ctx);
3036
+ if (sawSuper && state === 'init')
3037
+ emitConstructorDoubleSuper(ctx, statement);
3038
+ if (sawSuper && state !== 'init')
3039
+ emitConstructorConditionalSuper(ctx, statement);
3040
+ return { state, sawSuper };
3041
+ }
3042
+ function analyzeConstructorIf(statement, maybeElse, state, ctx) {
3043
+ const cond = expressionPropText(statement.props?.cond);
3044
+ if (cond)
3045
+ scanExpressionForConstructorEffects(cond, state, ctx, statement);
3046
+ const thenResult = analyzeConstructorStatements(statement.children ?? [], state, ctx);
3047
+ const elseResult = maybeElse
3048
+ ? analyzeConstructorStatements(maybeElse.children ?? [], state, ctx)
3049
+ : { state, sawSuper: false };
3050
+ const merged = mergeConstructorStates(thenResult.state, elseResult.state);
3051
+ const sawSuper = thenResult.sawSuper || elseResult.sawSuper;
3052
+ if (sawSuper && merged !== 'init')
3053
+ emitConstructorConditionalSuper(ctx, statement);
3054
+ return { state: merged, sawSuper };
3055
+ }
3056
+ function mergeConstructorStates(left, right) {
3057
+ if (left === 'init' && right === 'init')
3058
+ return 'init';
3059
+ if (left === 'uninit' && right === 'uninit')
3060
+ return 'uninit';
3061
+ return 'maybe';
3062
+ }
3063
+ function constructorBodyStatements(node) {
3064
+ const handler = node.children?.find((child) => child.type === 'handler');
3065
+ const body = handler ? (handler.children ?? []) : (node.children ?? []);
3066
+ return body.filter((child) => child.type !== 'param' && child.type !== 'decorator');
3067
+ }
3068
+ function directSuperConstructorCall(node) {
3069
+ if (node.type !== 'do')
3070
+ return undefined;
3071
+ const text = expressionPropText(node.props?.value);
3072
+ if (!text)
3073
+ return undefined;
3074
+ try {
3075
+ const value = parseExpression(text);
3076
+ return value.kind === 'call' && value.callee.kind === 'ident' && value.callee.name === 'super' ? value : undefined;
3077
+ }
3078
+ catch {
3079
+ return undefined;
3080
+ }
3081
+ }
3082
+ function scanConstructorStatementExpressions(node, state, ctx) {
3083
+ let sawSuper = false;
3084
+ walkSemanticTreeUntil(node, (candidate) => {
3085
+ if (candidate !== node && candidate.type === 'class')
3086
+ return 'stop';
3087
+ for (const prop of BODY_EXPRESSION_PROPS) {
3088
+ const text = expressionPropText(candidate.props?.[prop]);
3089
+ if (!text)
3090
+ continue;
3091
+ sawSuper = scanExpressionForConstructorEffects(text, state, ctx, candidate) || sawSuper;
3092
+ }
3093
+ return 'continue';
3094
+ });
3095
+ return sawSuper;
3096
+ }
3097
+ function scanExpressionForConstructorEffects(text, state, ctx, node) {
3098
+ try {
3099
+ const value = parseExpression(text);
3100
+ scanValueIRForPreSuperAccess(value, state, ctx, node);
3101
+ const sawSuper = valueIRCallsSuperConstructor(value);
3102
+ if (sawSuper)
3103
+ ctx.sawSuper = true;
3104
+ return sawSuper;
3105
+ }
3106
+ catch {
3107
+ return false;
3108
+ }
3109
+ }
3110
+ function scanValueIRForPreSuperAccess(value, state, ctx, node) {
3111
+ if (state === 'init')
3112
+ return;
3113
+ if (!valueIRUsesThisOrSuperMember(value))
3114
+ return;
3115
+ ctx.violations.push({
3116
+ rule: 'class-constructor-this-before-super',
3117
+ nodeType: node.type,
3118
+ message: `Class '${ctx.info.name}' constructor uses \`this\` or \`super\` member access before \`super(...)\`. Derived constructors must initialize the base class first.`,
3119
+ line: node.loc?.line,
3120
+ col: node.loc?.col,
3121
+ });
3122
+ }
3123
+ function emitConstructorDoubleSuper(ctx, node) {
3124
+ ctx.violations.push({
3125
+ rule: 'class-constructor-double-super',
3126
+ nodeType: node.type,
3127
+ message: `Class '${ctx.info.name}' constructor calls \`super(...)\` more than once. Derived constructors may initialize the base class once.`,
3128
+ line: node.loc?.line,
3129
+ col: node.loc?.col,
3130
+ });
3131
+ }
3132
+ function emitConstructorConditionalSuper(ctx, node) {
3133
+ if (ctx.emittedConditionalSuper)
3134
+ return;
3135
+ ctx.emittedConditionalSuper = true;
3136
+ ctx.violations.push({
3137
+ rule: 'class-constructor-conditional-super',
3138
+ nodeType: node.type,
3139
+ message: `Class '${ctx.info.name}' constructor must call \`super(...)\` definitely on every path before using derived state. Move \`super(...)\` to a straight-line statement or cover every branch.`,
3140
+ line: node.loc?.line,
3141
+ col: node.loc?.col,
3142
+ });
3143
+ }
3144
+ function validateClassInheritanceCycles(classes, classByName, violations) {
3145
+ const emitted = new Set();
3146
+ for (const info of classes) {
3147
+ const path = [];
3148
+ const seen = new Set();
3149
+ let current = info;
3150
+ while (current) {
3151
+ if (seen.has(current.name)) {
3152
+ const cycleStart = path.indexOf(current.name);
3153
+ const cycleNames = path.slice(cycleStart);
3154
+ const cycleKey = normalizedCycleKey(cycleNames);
3155
+ const cycle = [...cycleNames, current.name].join(' -> ');
3156
+ if (!emitted.has(cycleKey)) {
3157
+ emitted.add(cycleKey);
3158
+ violations.push({
3159
+ rule: 'class-inheritance-cycle',
3160
+ nodeType: 'class',
3161
+ message: `Class inheritance cycle detected: ${cycle}.`,
3162
+ line: current.node.loc?.line,
3163
+ col: current.node.loc?.col,
3164
+ });
3165
+ }
3166
+ break;
3167
+ }
3168
+ seen.add(current.name);
3169
+ path.push(current.name);
3170
+ current = current.baseName ? classByName.get(current.baseName) : undefined;
3171
+ }
3172
+ }
3173
+ }
3174
+ function validateClassOverrides(classes, classByName, violations) {
3175
+ for (const info of classes) {
3176
+ for (const member of info.members) {
3177
+ const baseMember = findBaseMember(info, member, classByName);
3178
+ if (!baseMember)
3179
+ continue;
3180
+ if (!sameOverrideKind(member, baseMember)) {
3181
+ violations.push({
3182
+ rule: 'class-override-kind-mismatch',
3183
+ nodeType: member.node.type,
3184
+ message: `Class '${info.name}' member '${member.name}' overrides base ${baseMember.kind} with ${member.kind}. Overrides must preserve field/method/accessor kind.`,
3185
+ line: member.node.loc?.line,
3186
+ col: member.node.loc?.col,
3187
+ });
3188
+ continue;
3189
+ }
3190
+ if (member.kind === 'method' && baseMember.kind === 'method' && member.arity !== baseMember.arity) {
3191
+ violations.push({
3192
+ rule: 'class-override-arity-mismatch',
3193
+ nodeType: member.node.type,
3194
+ message: `Class '${info.name}' method '${member.name}' overrides a base method with ${baseMember.arity} parameter(s), but declares ${member.arity}.`,
3195
+ line: member.node.loc?.line,
3196
+ col: member.node.loc?.col,
3197
+ });
3198
+ }
3199
+ const variance = checkOverrideVariance(member, baseMember, classByName);
3200
+ if (variance === 'return-mismatch') {
3201
+ violations.push({
3202
+ rule: 'class-override-return-mismatch',
3203
+ nodeType: member.node.type,
3204
+ message: `Class '${info.name}' member '${member.name}' overrides a base member returning '${baseMember.returns}' with return type '${member.returns}'. Overrides must be covariant in their return type (the override's return must be a subtype of the base's).`,
3205
+ line: member.node.loc?.line,
3206
+ col: member.node.loc?.col,
3207
+ });
3208
+ continue;
3209
+ }
3210
+ if (variance === 'param-mismatch') {
3211
+ violations.push({
3212
+ rule: 'class-override-param-mismatch',
3213
+ nodeType: member.node.type,
3214
+ message: `Class '${info.name}' member '${member.name}' narrows a parameter type when overriding a base member. Overrides must be contravariant in their parameter types (the override's parameter must be a supertype of the base's).`,
3215
+ line: member.node.loc?.line,
3216
+ col: member.node.loc?.col,
3217
+ });
3218
+ }
3219
+ }
3220
+ }
3221
+ }
3222
+ function validateClassShapeUsage(classes, classByName, violations) {
3223
+ for (const info of classes) {
3224
+ for (const ctor of info.constructors)
3225
+ validateClassShapeNode(info, ctor, false, classByName, violations);
3226
+ for (const member of info.members)
3227
+ validateClassShapeNode(info, member.node, member.static, classByName, violations);
3228
+ }
3229
+ }
3230
+ function validateClassShapeNode(info, node, staticContext, classByName, violations) {
3231
+ walkSemanticTreeUntil(node, (candidate) => {
3232
+ if (candidate !== node && candidate.type === 'class')
3233
+ return 'stop';
3234
+ if (candidate.type === 'assign') {
3235
+ const target = expressionPropText(candidate.props?.target);
3236
+ if (target && validateClassShapeTarget(info, candidate, target, staticContext, classByName, violations)) {
3237
+ const value = expressionPropText(candidate.props?.value);
3238
+ if (value)
3239
+ validateClassShapeExpression(info, candidate, value, staticContext, classByName, violations);
3240
+ return 'continue';
3241
+ }
3242
+ }
3243
+ for (const prop of BODY_EXPRESSION_PROPS) {
3244
+ const text = expressionPropText(candidate.props?.[prop]);
3245
+ if (!text)
3246
+ continue;
3247
+ validateClassShapeExpression(info, candidate, text, staticContext, classByName, violations);
3248
+ }
3249
+ return 'continue';
3250
+ });
3251
+ }
3252
+ function validateClassShapeTarget(info, node, text, staticContext, classByName, violations) {
3253
+ try {
3254
+ const value = parseExpression(text);
3255
+ if (value.kind !== 'member')
3256
+ return false;
3257
+ if (value.object.kind !== 'ident' || (value.object.name !== 'this' && value.object.name !== 'super'))
3258
+ return false;
3259
+ validateClassShapeAccess(info, node, value.object.name, value.property, 'write', staticContext, classByName, violations);
3260
+ return true;
3261
+ }
3262
+ catch {
3263
+ return false;
3264
+ }
3265
+ }
3266
+ function validateClassShapeExpression(info, node, text, staticContext, classByName, violations) {
3267
+ try {
3268
+ validateClassShapeValueIR(info, node, parseExpression(text), staticContext, classByName, violations);
3269
+ }
3270
+ catch {
3271
+ return;
3272
+ }
3273
+ }
3274
+ function validateClassShapeValueIR(info, node, value, staticContext, classByName, violations) {
3275
+ if (value.kind === 'member' && value.object.kind === 'ident') {
3276
+ if (value.object.name === 'this' || value.object.name === 'super') {
3277
+ validateClassShapeAccess(info, node, value.object.name, value.property, 'read', staticContext, classByName, violations);
3278
+ }
3279
+ }
3280
+ for (const child of valueIRChildren(value)) {
3281
+ validateClassShapeValueIR(info, node, child, staticContext, classByName, violations);
3282
+ }
3283
+ }
3284
+ function validateClassShapeAccess(info, node, receiver, property, accessKind, staticContext, classByName, violations) {
3285
+ const start = receiver === 'super' ? (info.baseName ? classByName.get(info.baseName) : undefined) : info;
3286
+ if (!start)
3287
+ return;
3288
+ const member = findClassShapeMember(start, property, staticContext, classByName, accessKind);
3289
+ if (!member) {
3290
+ violations.push({
3291
+ rule: 'class-member-undeclared',
3292
+ nodeType: node.type,
3293
+ message: `Class '${info.name}' ${receiver}.${property} is not declared on the ${staticContext ? 'static' : 'instance'} class shape.`,
3294
+ line: node.loc?.line,
3295
+ col: node.loc?.col,
3296
+ });
3297
+ return;
3298
+ }
3299
+ if (accessKind === 'read' && member.kind === 'setter') {
3300
+ violations.push({
3301
+ rule: 'class-member-read-not-readable',
3302
+ nodeType: node.type,
3303
+ message: `Class '${info.name}' reads setter-only ${receiver}.${property}. Add a getter or read a declared field.`,
3304
+ line: node.loc?.line,
3305
+ col: node.loc?.col,
3306
+ });
3307
+ }
3308
+ if (accessKind === 'write' && (member.kind === 'getter' || member.kind === 'method')) {
3309
+ violations.push({
3310
+ rule: 'class-member-write-not-writable',
3311
+ nodeType: node.type,
3312
+ message: `Class '${info.name}' writes non-writable ${receiver}.${property}. Declare a field or setter for writes.`,
3313
+ line: node.loc?.line,
3314
+ col: node.loc?.col,
3315
+ });
3316
+ }
3317
+ }
3318
+ function findClassShapeMember(info, property, staticContext, classByName, accessKind) {
3319
+ const precedence = accessKind === 'read' ? ['field', 'getter', 'method', 'setter'] : ['field', 'setter', 'getter', 'method'];
3320
+ for (const kind of precedence) {
3321
+ const found = info.members.find((member) => member.name === property && member.static === staticContext && member.kind === kind);
3322
+ if (found)
3323
+ return found;
3324
+ }
3325
+ const base = info.baseName ? classByName.get(info.baseName) : undefined;
3326
+ return base ? findClassShapeMember(base, property, staticContext, classByName, accessKind) : undefined;
3327
+ }
3328
+ function normalizedCycleKey(cycleNames) {
3329
+ if (cycleNames.length === 0)
3330
+ return '';
3331
+ let best = cycleNames.join('\0');
3332
+ for (let index = 1; index < cycleNames.length; index++) {
3333
+ const rotated = [...cycleNames.slice(index), ...cycleNames.slice(0, index)].join('\0');
3334
+ if (rotated < best)
3335
+ best = rotated;
3336
+ }
3337
+ return best;
3338
+ }
3339
+ function findBaseMember(info, member, classByName) {
3340
+ let current = info.baseName ? classByName.get(info.baseName) : undefined;
3341
+ const visited = new Set();
3342
+ while (current) {
3343
+ if (visited.has(current.name))
3344
+ return undefined;
3345
+ visited.add(current.name);
3346
+ const found = current.members.find((candidate) => candidate.name === member.name && candidate.static === member.static);
3347
+ if (found)
3348
+ return found;
3349
+ current = current.baseName ? classByName.get(current.baseName) : undefined;
3350
+ }
3351
+ return undefined;
3352
+ }
3353
+ function sameOverrideKind(member, baseMember) {
3354
+ if (isAccessorPair(member, baseMember))
3355
+ return true;
3356
+ return member.kind === baseMember.kind;
3357
+ }
3358
+ function isAccessorPair(a, b) {
3359
+ return (a.kind === 'getter' && b.kind === 'setter') || (a.kind === 'setter' && b.kind === 'getter');
3360
+ }
3361
+ function isAllowedMemberGroup(members) {
3362
+ if (members.length <= 1)
3363
+ return true;
3364
+ if (members.length > 2)
3365
+ return false;
3366
+ if (!members.every((member) => member.kind === 'getter' || member.kind === 'setter'))
3367
+ return false;
3368
+ return isAccessorPair(members[0], members[1]);
3369
+ }
3370
+ function collectVisibleClassBaseNames(root) {
3371
+ const names = new Set(BUILTIN_CLASS_BASES);
3372
+ walkSemanticTree(root, (node) => {
3373
+ const name = stringProp(node, 'name');
3374
+ if (name && isVisibleClassBaseDeclaration(node.type))
3375
+ names.add(name);
3376
+ if (node.type === 'import') {
3377
+ for (const binding of importLocalBindings(node))
3378
+ names.add(binding.name);
3379
+ }
3380
+ if (node.type === 'use') {
3381
+ for (const child of node.children ?? []) {
3382
+ if (child.type !== 'from')
3383
+ continue;
3384
+ if (!isUseClassBaseBinding(child))
3385
+ continue;
3386
+ const localName = stringProp(child, 'as') ?? stringProp(child, 'name');
3387
+ if (localName)
3388
+ names.add(localName);
3389
+ }
3390
+ }
3391
+ });
3392
+ return names;
3393
+ }
3394
+ function collectVisibleProtocolNames(root) {
3395
+ const names = new Set();
3396
+ walkSemanticTree(root, (node) => {
3397
+ const name = stringProp(node, 'name');
3398
+ if (name && node.type === 'interface')
3399
+ names.add(name);
3400
+ if (node.type === 'import') {
3401
+ for (const binding of importLocalBindings(node))
3402
+ names.add(binding.name);
3403
+ }
3404
+ if (node.type === 'use') {
3405
+ for (const child of node.children ?? []) {
3406
+ if (child.type !== 'from')
3407
+ continue;
3408
+ const kind = stringProp(child, 'kind');
3409
+ if (kind && kind !== 'interface' && kind !== 'type')
3410
+ continue;
3411
+ const localName = stringProp(child, 'as') ?? stringProp(child, 'name');
3412
+ if (localName)
3413
+ names.add(localName);
3414
+ }
3415
+ }
3416
+ });
3417
+ return names;
3418
+ }
3419
+ function isVisibleClassBaseDeclaration(nodeType) {
3420
+ return nodeType === 'class' || nodeType === 'error';
3421
+ }
3422
+ function isUseClassBaseBinding(node) {
3423
+ const kind = stringProp(node, 'kind');
3424
+ return !kind || kind === 'class' || kind === 'error';
3425
+ }
3426
+ function memberArity(node) {
3427
+ const childParams = node.children?.filter((child) => child.type === 'param').length ?? 0;
3428
+ if (childParams > 0)
3429
+ return childParams;
3430
+ const params = node.props?.params;
3431
+ if (typeof params !== 'string' || !params.trim())
3432
+ return 0;
3433
+ try {
3434
+ return splitSemanticParamList(params, `${node.type} params=`).length;
3435
+ }
3436
+ catch {
3437
+ return 0;
3438
+ }
3439
+ }
3440
+ function memberParamTypes(node) {
3441
+ const childParams = node.children?.filter((child) => child.type === 'param') ?? [];
3442
+ if (childParams.length > 0) {
3443
+ return childParams.map((param) => stringProp(param, 'type') ?? '');
3444
+ }
3445
+ const params = node.props?.params;
3446
+ if (typeof params !== 'string' || !params.trim())
3447
+ return [];
3448
+ try {
3449
+ return splitSemanticParamList(params, `${node.type} params=`).map((part) => {
3450
+ const typeIndex = part.indexOf(':');
3451
+ if (typeIndex < 0)
3452
+ return '';
3453
+ const typeAndMaybeDefault = part.slice(typeIndex + 1);
3454
+ const defaultIndex = paramDefaultSeparatorIndex(typeAndMaybeDefault);
3455
+ return (defaultIndex >= 0 ? typeAndMaybeDefault.slice(0, defaultIndex) : typeAndMaybeDefault).trim();
3456
+ });
3457
+ }
3458
+ catch {
3459
+ return [];
3460
+ }
3461
+ }
3462
+ function paramDefaultSeparatorIndex(value) {
3463
+ let depth = 0;
3464
+ let quote = null;
3465
+ for (let index = 0; index < value.length; index += 1) {
3466
+ const ch = value[index];
3467
+ if (quote !== null) {
3468
+ if (ch === '\\' && index + 1 < value.length)
3469
+ index += 1;
3470
+ else if (ch === quote)
3471
+ quote = null;
3472
+ continue;
3473
+ }
3474
+ if (ch === '"' || ch === "'" || ch === '`') {
3475
+ quote = ch;
3476
+ continue;
3477
+ }
3478
+ if (ch === '<' || ch === '(' || ch === '{' || ch === '[')
3479
+ depth += 1;
3480
+ else if ((ch === '>' || ch === ')' || ch === '}' || ch === ']') && depth > 0)
3481
+ depth -= 1;
3482
+ else if (ch === '=' && depth === 0) {
3483
+ if (value[index + 1] === '>' ||
3484
+ value[index + 1] === '=' ||
3485
+ value[index - 1] === '=' ||
3486
+ value[index - 1] === '<' ||
3487
+ value[index - 1] === '>' ||
3488
+ value[index - 1] === '!') {
3489
+ continue;
3490
+ }
3491
+ return index;
3492
+ }
3493
+ }
3494
+ return -1;
3495
+ }
3496
+ function splitSemanticParamList(raw, propName) {
3497
+ const out = [];
3498
+ let current = '';
3499
+ let depth = 0;
3500
+ let angleDepth = 0;
3501
+ let inDefault = false;
3502
+ let quote = null;
3503
+ for (let index = 0; index < raw.length; index += 1) {
3504
+ const ch = raw[index];
3505
+ if (quote !== null) {
3506
+ current += ch;
3507
+ if (ch === '\\' && index + 1 < raw.length)
3508
+ current += raw[++index];
3509
+ else if (ch === quote)
3510
+ quote = null;
3511
+ continue;
3512
+ }
3513
+ if (ch === '"' || ch === "'" || ch === '`') {
3514
+ quote = ch;
3515
+ current += ch;
3516
+ continue;
3517
+ }
3518
+ if (ch === '(' || ch === '[' || ch === '{')
3519
+ depth += 1;
3520
+ else if (ch === ')' || ch === ']' || ch === '}')
3521
+ depth -= 1;
3522
+ else if (ch === '=' && depth === 0 && angleDepth === 0 && raw[index + 1] !== '>')
3523
+ inDefault = true;
3524
+ else if (ch === '<' && (!inDefault || angleClosesBeforeNextTopLevelComma(raw, index + 1)))
3525
+ angleDepth += 1;
3526
+ else if (!inDefault && ch === '>' && angleDepth > 0)
3527
+ angleDepth -= 1;
3528
+ else if (inDefault && ch === '>' && angleDepth > 0)
3529
+ angleDepth -= 1;
3530
+ if (depth < 0 || angleDepth < 0)
3531
+ throw new Error(`${propName} has unbalanced delimiters.`);
3532
+ if (ch === ',' && depth === 0 && angleDepth === 0) {
3533
+ const part = current.trim();
3534
+ if (part.length === 0)
3535
+ throw new Error(`${propName} contains an empty expression.`);
3536
+ out.push(part);
3537
+ current = '';
3538
+ inDefault = false;
3539
+ continue;
3540
+ }
3541
+ current += ch;
3542
+ }
3543
+ if (quote !== null || depth !== 0 || angleDepth !== 0)
3544
+ throw new Error(`${propName} has unbalanced delimiters.`);
3545
+ const tail = current.trim();
3546
+ if (tail.length === 0 && raw.trim().endsWith(','))
3547
+ throw new Error(`${propName} contains an empty expression.`);
3548
+ if (tail.length > 0)
3549
+ out.push(tail);
3550
+ return out;
3551
+ }
3552
+ function angleClosesBeforeNextTopLevelComma(raw, start) {
3553
+ let depth = 0;
3554
+ let quote = null;
3555
+ for (let index = start; index < raw.length; index += 1) {
3556
+ const ch = raw[index];
3557
+ if (quote !== null) {
3558
+ if (ch === '\\' && index + 1 < raw.length)
3559
+ index += 1;
3560
+ else if (ch === quote)
3561
+ quote = null;
3562
+ continue;
3563
+ }
3564
+ if (ch === '"' || ch === "'" || ch === '`') {
3565
+ quote = ch;
3566
+ continue;
3567
+ }
3568
+ if (ch === '(' || ch === '[' || ch === '{')
3569
+ depth += 1;
3570
+ else if ((ch === ')' || ch === ']' || ch === '}') && depth > 0)
3571
+ depth -= 1;
3572
+ else if (ch === '>' && depth === 0)
3573
+ return true;
3574
+ else if (ch === ',' && depth === 0)
3575
+ return false;
3576
+ }
3577
+ return false;
3578
+ }
3579
+ function nodeBodyUsesSuper(node) {
3580
+ return nodeBodyExpressions(node).some((expr) => {
3581
+ try {
3582
+ return valueIRUsesSuper(parseExpression(expr));
3583
+ }
3584
+ catch {
3585
+ return false;
3586
+ }
3587
+ });
3588
+ }
3589
+ function nodeBodyExpressions(node) {
3590
+ const out = [];
3591
+ walkSemanticTreeUntil(node, (candidate) => {
3592
+ for (const prop of BODY_EXPRESSION_PROPS) {
3593
+ const text = expressionPropText(candidate.props?.[prop]);
3594
+ if (text)
3595
+ out.push(text);
3596
+ }
3597
+ return candidate !== node && candidate.type === 'class' ? 'stop' : 'continue';
3598
+ });
3599
+ return out;
3600
+ }
3601
+ function expressionPropText(value) {
3602
+ if (typeof value === 'string')
3603
+ return value;
3604
+ if (isExpressionObject(value))
3605
+ return value.code;
3606
+ if (typeof value === 'number' || typeof value === 'boolean')
3607
+ return String(value);
3608
+ return undefined;
3609
+ }
3610
+ function valueIRCallsSuperConstructor(value) {
3611
+ if (value.kind === 'call' && value.callee.kind === 'ident' && value.callee.name === 'super')
3612
+ return true;
3613
+ if (value.kind === 'lambda')
3614
+ return false;
3615
+ return valueIRChildren(value).some(valueIRCallsSuperConstructor);
3616
+ }
3617
+ function valueIRUsesSuper(value) {
3618
+ if (value.kind === 'ident' && value.name === 'super')
3619
+ return true;
3620
+ return valueIRChildren(value).some(valueIRUsesSuper);
3621
+ }
3622
+ function valueIRUsesThisOrSuperMember(value) {
3623
+ if (value.kind === 'ident' && value.name === 'this')
3624
+ return true;
3625
+ if ((value.kind === 'member' || value.kind === 'index') &&
3626
+ value.object.kind === 'ident' &&
3627
+ value.object.name === 'super') {
3628
+ return true;
3629
+ }
3630
+ if (value.kind === 'lambda')
3631
+ return false;
3632
+ return valueIRChildren(value).some(valueIRUsesThisOrSuperMember);
3633
+ }
3634
+ function valueIRChildren(value) {
3635
+ switch (value.kind) {
3636
+ case 'call':
3637
+ return [value.callee, ...value.args];
3638
+ case 'member':
3639
+ return [value.object];
3640
+ case 'index':
3641
+ return [value.object, value.index];
3642
+ case 'tmplLit':
3643
+ return [...value.expressions];
3644
+ case 'arrayLit':
3645
+ return [...value.items];
3646
+ case 'objectLit':
3647
+ return value.entries.map((entry) => ('kind' in entry ? entry.argument : entry.value));
3648
+ case 'unary':
3649
+ case 'await':
3650
+ case 'new':
3651
+ case 'spread':
3652
+ case 'propagate':
3653
+ return [value.argument];
3654
+ case 'typeAssert':
3655
+ case 'nonNull':
3656
+ return [value.expression];
3657
+ case 'binary':
3658
+ return [value.left, value.right];
3659
+ case 'conditional':
3660
+ return [value.test, value.consequent, value.alternate];
3661
+ case 'lambda':
3662
+ // Block-bodied arrows carry `bodyBlock` (raw text) instead of an
3663
+ // expression `body`; they contribute no ValueIR children here.
3664
+ return value.body ? [value.body] : [];
3665
+ case 'numLit':
3666
+ case 'strLit':
3667
+ case 'boolLit':
3668
+ case 'nullLit':
3669
+ case 'undefLit':
3670
+ case 'regexLit':
3671
+ case 'ident':
3672
+ return [];
3673
+ }
3674
+ return [];
3675
+ }
3676
+ function classBaseName(value) {
3677
+ if (typeof value !== 'string' || !value.trim())
3678
+ return undefined;
3679
+ const match = /^([A-Za-z_$][\w$]*)/.exec(value.trim());
3680
+ return match?.[1];
3681
+ }
3682
+ function classReferenceNames(value, propName) {
3683
+ if (typeof value !== 'string' || !value.trim())
3684
+ return [];
3685
+ let parts;
3686
+ try {
3687
+ parts = splitClassReferenceList(value, propName);
3688
+ }
3689
+ catch {
3690
+ parts = [];
3691
+ }
3692
+ const names = new Set();
3693
+ for (const part of parts) {
3694
+ const name = classBaseName(part);
3695
+ if (name)
3696
+ names.add(name);
3697
+ }
3698
+ return [...names];
3699
+ }
3700
+ function classReferenceListMalformed(value, propName) {
3701
+ if (typeof value !== 'string' || !value.trim())
3702
+ return false;
3703
+ try {
3704
+ splitClassReferenceList(value, propName);
3705
+ return false;
3706
+ }
3707
+ catch {
3708
+ return true;
3709
+ }
3710
+ }
3711
+ function splitClassReferenceList(raw, propName) {
3712
+ const out = [];
3713
+ let current = '';
3714
+ let depth = 0;
3715
+ let angleDepth = 0;
3716
+ let quote = null;
3717
+ for (let index = 0; index < raw.length; index++) {
3718
+ const ch = raw[index];
3719
+ if (quote !== null) {
3720
+ current += ch;
3721
+ if (ch === '\\' && index + 1 < raw.length)
3722
+ current += raw[++index];
3723
+ else if (ch === quote)
3724
+ quote = null;
3725
+ continue;
3726
+ }
3727
+ if (ch === '"' || ch === "'" || ch === '`') {
3728
+ quote = ch;
3729
+ current += ch;
3730
+ continue;
3731
+ }
3732
+ if (ch === '(' || ch === '[' || ch === '{')
3733
+ depth++;
3734
+ else if (ch === ')' || ch === ']' || ch === '}')
3735
+ depth--;
3736
+ else if (ch === '<')
3737
+ angleDepth++;
3738
+ else if (ch === '>' && angleDepth > 0)
3739
+ angleDepth--;
3740
+ if (depth < 0 || angleDepth < 0)
3741
+ throw new Error(`${propName} has unbalanced delimiters.`);
3742
+ if (ch === ',' && depth === 0 && angleDepth === 0) {
3743
+ const part = current.trim();
3744
+ if (part.length === 0)
3745
+ throw new Error(`${propName} contains an empty reference.`);
3746
+ out.push(part);
3747
+ current = '';
3748
+ continue;
3749
+ }
3750
+ current += ch;
3751
+ }
3752
+ if (quote !== null || depth !== 0 || angleDepth !== 0)
3753
+ throw new Error(`${propName} has unbalanced delimiters.`);
3754
+ const tail = current.trim();
3755
+ if (tail.length === 0 && raw.trim().endsWith(','))
3756
+ throw new Error(`${propName} contains an empty reference.`);
3757
+ if (tail.length > 0)
3758
+ out.push(tail);
3759
+ return out;
3760
+ }
3761
+ function stringProp(nodeOrProps, prop) {
3762
+ const props = nodeOrProps && 'type' in nodeOrProps ? nodeOrProps.props : nodeOrProps;
3763
+ const value = props ? props[prop] : undefined;
3764
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
3765
+ }
3766
+ function walkSemanticTree(node, visit) {
3767
+ visit(node);
3768
+ for (const child of node.children ?? [])
3769
+ walkSemanticTree(child, visit);
3770
+ }
3771
+ function walkSemanticTreeUntil(node, visit) {
3772
+ if (visit(node) === 'stop')
3773
+ return;
3774
+ for (const child of node.children ?? [])
3775
+ walkSemanticTreeUntil(child, visit);
3776
+ }
3777
+ function isExpressionObject(value) {
3778
+ return (typeof value === 'object' &&
3779
+ value !== null &&
3780
+ value.__expr === true &&
3781
+ typeof value.code === 'string');
3782
+ }
396
3783
  function validateModuleExports(node, violations, externalImports) {
397
3784
  const visibleNames = collectModuleVisibleNames(node, externalImports);
398
3785
  for (const child of node.children ?? []) {