@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
@@ -21,8 +21,67 @@
21
21
  * instead of a generic "ineligible". */
22
22
  import ts from 'typescript';
23
23
  import { supportedCompoundAssignmentOperator } from './assignment-operators.js';
24
+ import { classifyClosureBlock } from './closure-eligibility.js';
24
25
  import { emitTypeAnnotation } from './codegen/emitters.js';
26
+ import { deterministicRows, loadEligibilityTaxonomy } from './eligibility-taxonomy.js';
27
+ import { instanceofRhsRejectReasonForName } from './instanceof-rhs.js';
25
28
  import { parseExpression } from './parser-expression.js';
29
+ let taxonomyIndexCache = null;
30
+ function taxonomyIndex() {
31
+ if (taxonomyIndexCache !== null)
32
+ return taxonomyIndexCache;
33
+ const exact = new Set();
34
+ const prefixes = [];
35
+ for (const row of deterministicRows(loadEligibilityTaxonomy())) {
36
+ if (row.verdict !== 'ineligible' || row.reason === undefined)
37
+ continue;
38
+ if (row.construct.endsWith('-')) {
39
+ prefixes.push(row.reason);
40
+ }
41
+ else {
42
+ exact.add(row.reason);
43
+ }
44
+ }
45
+ taxonomyIndexCache = { exactReasonCodes: exact, reasonFamilyPrefixes: prefixes };
46
+ return taxonomyIndexCache;
47
+ }
48
+ /** Coherence-violation collector. `reject()` records (fail-safe, never throws)
49
+ * any reason string with no deterministic taxonomy row — exact or family — so
50
+ * the coherence-gate tests can assert the live classifier never emits an
51
+ * un-taxonomied reason. Production never reads this; tests do, via the getter
52
+ * and the reset below. A Set, not an array: violations are reason CODES, so
53
+ * duplicates add nothing and growth is bounded by the (finite) code space
54
+ * even if an un-taxonomied reason ships and fires on a hot path (agon
55
+ * review, codex 0.96). */
56
+ const coherenceViolationsInternal = new Set();
57
+ /** A snapshot of the recorded coherence violations (reasons emitted with no
58
+ * taxonomy row). Empty in a coherent build. */
59
+ export function coherenceViolations() {
60
+ return [...coherenceViolationsInternal];
61
+ }
62
+ /** Test-only: clear the recorded coherence violations between runs. */
63
+ export function resetCoherenceViolations() {
64
+ coherenceViolationsInternal.clear();
65
+ }
66
+ /** Fail-safe taxonomy AUTHORITY gate for an emitted ineligible reason.
67
+ *
68
+ * Pure indirection: ALWAYS returns `reason` UNCHANGED, never throws, never
69
+ * alters classifier behavior. Its ONLY side effect is recording a coherence
70
+ * violation when `reason` has no deterministic taxonomy row (exact match or a
71
+ * matching dynamic-family prefix). The classifier wraps every ineligible
72
+ * reason in `reject(...)`; the recorded violations are surfaced only to the
73
+ * coherence-gate tests. */
74
+ function reject(reason) {
75
+ const index = taxonomyIndex();
76
+ if (index.exactReasonCodes.has(reason))
77
+ return reason;
78
+ for (const prefix of index.reasonFamilyPrefixes) {
79
+ if (reason.startsWith(prefix))
80
+ return reason;
81
+ }
82
+ coherenceViolationsInternal.add(reason);
83
+ return reason;
84
+ }
26
85
  /** True when `exprText` parses cleanly under KERN's parser-expression.
27
86
  *
28
87
  * Multi-line input is accepted: the parser itself is whitespace-insensitive,
@@ -174,9 +233,9 @@ export function isValidKernTypeAnnotation(typeText) {
174
233
  */
175
234
  function classifyParseFailureBoundary(bodyText) {
176
235
  if (/\{\{\s*[A-Za-z_$][\w$.-]*\s*\}\}/.test(bodyText))
177
- return 'template-placeholder';
236
+ return reject('template-placeholder');
178
237
  if (isObjectFragmentBody(bodyText))
179
- return 'foreign-by-design';
238
+ return reject('foreign-by-design');
180
239
  return null;
181
240
  }
182
241
  function isObjectFragmentBody(bodyText) {
@@ -512,21 +571,132 @@ function isSimpleTrailingStmt(node) {
512
571
  }
513
572
  return false;
514
573
  }
574
+ /** Collect the raw text (braces included) of every block-bodied arrow
575
+ * (`x => { … }`) inside a statement subtree. TS-AST walk, never string
576
+ * scanning — `arrow.body.getText(sf)` yields the exact `{ … }` source. */
577
+ function collectBlockArrowRaws(node, sf) {
578
+ const raws = [];
579
+ const visit = (n) => {
580
+ if (ts.isArrowFunction(n) && ts.isBlock(n.body)) {
581
+ raws.push(n.body.getText(sf));
582
+ // Don't descend into the block body: the v1 closure gate already rejects
583
+ // nested arrows, so a nested block arrow can't be independently eligible.
584
+ return;
585
+ }
586
+ ts.forEachChild(n, visit);
587
+ };
588
+ visit(node);
589
+ return raws;
590
+ }
591
+ /** Eligibility verdict for any block-bodied arrows in a statement (slices
592
+ * 0+1+2). Returns a reject reason or `null` if the statement's block arrows
593
+ * are all gate-passing:
594
+ * - any arrow whose `classifyClosureBlock` is non-null → that gate reason
595
+ * (the statement is ineligible for the same reason the closure is).
596
+ * - otherwise `null` (eligible — funnels through isValidKernExpression for
597
+ * the statement's own expression validity, which now parses the arrow).
598
+ *
599
+ * Slice 2 lifted the former `closure-in-loop` reject: a gate-passing block
600
+ * arrow inside a loop is now eligible. The Python lowerer pins per-iteration
601
+ * captures via default args (`def __kern_closure_N(p, x=x):`) so JS
602
+ * by-reference / per-iteration capture semantics are preserved across both
603
+ * targets. The classifier therefore no longer consults `ctx.loopDepth`. */
604
+ function classifyBlockArrows(stmt, sf) {
605
+ const raws = collectBlockArrowRaws(stmt, sf);
606
+ if (raws.length === 0)
607
+ return null;
608
+ for (const raw of raws) {
609
+ const gateReason = classifyClosureBlock(raw);
610
+ if (gateReason !== null)
611
+ return reject(gateReason);
612
+ }
613
+ return null;
614
+ }
615
+ /** Scan a statement subtree for an `instanceof` whose RHS KERN cannot lower
616
+ * (eligible ≡ lowerable, spec §3). Returns the first reject reason or `null`:
617
+ * - RHS is a bare ident in the reject set →
618
+ * `instanceof-rhs-wrapper-rejected` (String/Number/Boolean) or
619
+ * `instanceof-rhs-unsupported-builtin` (Object/Function/Date/RegExp/
620
+ * Promise/Map/Set/Symbol/BigInt).
621
+ * - RHS is NOT an identifier (call/literal/binary/etc.) →
622
+ * `instanceof-rhs-not-a-type-name`. A member-access RHS (`a.b.C`) is a
623
+ * qualified type name and stays accepted (emits as-is, like a user class).
624
+ * Accepted host idents (`Array`/`Error`/`TypeError`) and user-class /
625
+ * member RHS pass (return `null`); they emit as-is or via the host map.
626
+ *
627
+ * Detection is on the TS AST (`ts.isBinaryExpression` + `InstanceOfKeyword`),
628
+ * mirroring the Python emitter's fail-closed reject so the gate and the
629
+ * lowerer share one source of truth (`instanceof-rhs.ts`). The whole subtree
630
+ * is scanned so a rejected `instanceof` nested in any expression position
631
+ * (ternary arm, call arg, arrow body, …) is caught. */
632
+ // TODO(taxonomy-fold): the `instanceof` RHS reject set in instanceof-rhs.ts
633
+ // (INSTANCEOF_RHS_WRAPPER_REJECT / INSTANCEOF_RHS_BUILTIN_REJECT) is the target
634
+ // declarative shape for a future deterministic-row extraction slice — fold those
635
+ // RHS-name → reason mappings into the taxonomy so this dynamic reason source is
636
+ // itself authority-backed, not just family-validated at runtime. Deferred (phase 2
637
+ // scope is the AUTHORITY inversion; instanceof-rhs.ts fold-in is a separate slice).
638
+ function classifyInstanceofRhs(node) {
639
+ let reason = null;
640
+ const visit = (n) => {
641
+ if (reason !== null)
642
+ return;
643
+ if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) {
644
+ // Unwrap parens (`x instanceof (Error)`) before classification — the TS
645
+ // AST preserves them and a parenthesized bare ident is still a type
646
+ // name (agon review, 3-engine convergence). KERN's expression parser
647
+ // unwraps parens at parse time, so the emitter side never sees them.
648
+ let rhs = n.right;
649
+ while (ts.isParenthesizedExpression(rhs))
650
+ rhs = rhs.expression;
651
+ if (ts.isIdentifier(rhs)) {
652
+ const rhsReason = instanceofRhsRejectReasonForName(rhs.text);
653
+ reason = rhsReason === null ? null : reject(rhsReason);
654
+ }
655
+ else if (!ts.isPropertyAccessExpression(rhs)) {
656
+ // Member RHS (`a.b.C`) is a qualified type name → accepted (emit
657
+ // as-is). Anything else (call/literal/parenthesized/binary) is not a
658
+ // type name and cannot be lowered.
659
+ reason = reject('instanceof-rhs-not-a-type-name');
660
+ }
661
+ if (reason !== null)
662
+ return;
663
+ }
664
+ ts.forEachChild(n, visit);
665
+ };
666
+ visit(node);
667
+ return reason;
668
+ }
515
669
  /** Classify a single statement. Returns null if the migrator can emit it,
516
670
  * otherwise a kebab-case reason. Recurses through if/try branches. */
517
671
  function classifyStmt(stmt, sf, ctx) {
672
+ // Eligible ≡ lowerable (spec §3): a statement carrying an `instanceof` with a
673
+ // RHS the Python emitter fail-closes on is ineligible, with a reason that
674
+ // names the exact RHS problem. Run before the per-shape checks so the
675
+ // instanceof reason wins over a generic `*-bad-expr`.
676
+ const instanceofReason = classifyInstanceofRhs(stmt);
677
+ if (instanceofReason !== null)
678
+ return instanceofReason;
679
+ // Slices 0+1+2 — a statement containing a block-bodied arrow is eligible IFF
680
+ // every such arrow passes the v1 closure gate. Slice 2 lifted the former
681
+ // in-loop reject (the Python lowerer now pins per-iteration captures), so the
682
+ // loop context no longer matters here. The statement's own expression
683
+ // validity is still checked below via isValidKernExpression — the block arrow
684
+ // inside it parses now.
685
+ const closureReason = classifyBlockArrows(stmt, sf);
686
+ if (closureReason !== null)
687
+ return closureReason;
518
688
  if (ts.isVariableStatement(stmt)) {
519
689
  const flags = stmt.declarationList.flags;
520
690
  const isConst = (flags & ts.NodeFlags.Const) !== 0;
521
691
  const isLet = (flags & ts.NodeFlags.Let) !== 0;
522
692
  if (!isConst && !isLet)
523
- return 'var-non-const';
693
+ return reject('var-non-const');
524
694
  const decls = stmt.declarationList.declarations;
525
695
  if (decls.length !== 1)
526
- return 'var-multi-decl';
696
+ return reject('var-multi-decl');
527
697
  const decl = decls[0];
528
698
  if (decl.type && !isValidKernTypeAnnotation(decl.type.getText(sf)))
529
- return 'var-bad-type';
699
+ return reject('var-bad-type');
530
700
  if (!decl.initializer) {
531
701
  // `let x;` migrates to `let name=x kind=let` (the body emitter handles
532
702
  // missing `value=` by emitting `let x = undefined;`, matching TS
@@ -534,7 +704,7 @@ function classifyStmt(stmt, sf, ctx) {
534
704
  // TS parse error in practice, but defensively reject them anyway since
535
705
  // the migrator can only emit identifier-named lets in this branch.
536
706
  if (!ts.isIdentifier(decl.name))
537
- return 'var-destructure';
707
+ return reject('var-destructure');
538
708
  return null;
539
709
  }
540
710
  if (!ts.isIdentifier(decl.name))
@@ -552,13 +722,13 @@ function classifyStmt(stmt, sf, ctx) {
552
722
  const raw = decl.initializer.getText(sf);
553
723
  const body = raw.slice(1, -1);
554
724
  if (body.includes('\n'))
555
- return 'var-template-multiline';
725
+ return reject('var-template-multiline');
556
726
  if (hasTsOnlyTemplateEscape(body))
557
- return 'var-template-escapes';
727
+ return reject('var-template-escapes');
558
728
  return null;
559
729
  }
560
730
  if (!isValidKernExpression(decl.initializer.getText(sf)))
561
- return 'var-bad-expr';
731
+ return reject('var-bad-expr');
562
732
  return null;
563
733
  }
564
734
  if (ts.isReturnStatement(stmt)) {
@@ -570,41 +740,43 @@ function classifyStmt(stmt, sf, ctx) {
570
740
  const raw = stmt.expression.getText(sf);
571
741
  const body = raw.slice(1, -1);
572
742
  if (body.includes('\n'))
573
- return 'return-template-multiline';
743
+ return reject('return-template-multiline');
574
744
  if (hasTsOnlyTemplateEscape(body))
575
- return 'return-template-escapes';
745
+ return reject('return-template-escapes');
576
746
  return null;
577
747
  }
578
748
  if (!isValidKernExpression(stmt.expression.getText(sf)))
579
- return 'return-bad-expr';
749
+ return reject('return-bad-expr');
580
750
  return null;
581
751
  }
582
752
  if (ts.isThrowStatement(stmt)) {
753
+ // `throw;` is structurally unreachable (always a TS parse error) and has no
754
+ // taxonomy row — NOT routed through reject() (it would never validate).
583
755
  if (!stmt.expression)
584
756
  return 'throw-no-expr';
585
757
  if (!isValidKernExpression(stmt.expression.getText(sf)))
586
- return 'throw-bad-expr';
758
+ return reject('throw-bad-expr');
587
759
  return null;
588
760
  }
589
761
  if (ts.isBreakStatement(stmt)) {
590
762
  if (stmt.label)
591
- return 'break-labeled';
592
- return ctx.loopDepth > 0 ? null : 'break-outside-loop';
763
+ return reject('break-labeled');
764
+ return ctx.loopDepth > 0 ? null : reject('break-outside-loop');
593
765
  }
594
766
  if (ts.isContinueStatement(stmt)) {
595
767
  if (stmt.label)
596
- return 'continue-labeled';
597
- return ctx.loopDepth > 0 ? null : 'continue-outside-loop';
768
+ return reject('continue-labeled');
769
+ return ctx.loopDepth > 0 ? null : reject('continue-outside-loop');
598
770
  }
599
771
  if (ts.isIfStatement(stmt)) {
600
772
  if (!isValidKernExpression(stmt.expression.getText(sf)))
601
- return 'if-bad-cond';
773
+ return reject('if-bad-cond');
602
774
  // Body emitters (`emitNativeKernBodyTS` / `emitNativeKernBodyPython`) always
603
775
  // wrap `if` bodies in braces / indented blocks. A raw `if (cond) stmt;`
604
776
  // would migrate to `if cond=… → { stmt; }` and lose byte-equivalence under
605
777
  // `--verify`. Mirror the `for-of-non-block` / `while-non-block` guards.
606
778
  if (!ctx.allowNonBlock && !ts.isBlock(stmt.thenStatement))
607
- return 'if-non-block-then';
779
+ return reject('if-non-block-then');
608
780
  const thenReason = classifyBranch(stmt.thenStatement, sf, ctx);
609
781
  if (thenReason !== null)
610
782
  return thenReason;
@@ -615,7 +787,7 @@ function classifyStmt(stmt, sf, ctx) {
615
787
  // 88c06dcc on dev). classifyBranch handles the nested IfStatement
616
788
  // by re-entering classifyStmt, so the recursion is automatic.
617
789
  if (!ctx.allowNonBlock && !ts.isIfStatement(stmt.elseStatement) && !ts.isBlock(stmt.elseStatement)) {
618
- return 'if-non-block-else';
790
+ return reject('if-non-block-else');
619
791
  }
620
792
  const elseReason = classifyBranch(stmt.elseStatement, sf, ctx);
621
793
  if (elseReason !== null)
@@ -629,12 +801,14 @@ function classifyStmt(stmt, sf, ctx) {
629
801
  // (body-ts.ts:286-292 / codegen-body-python.ts:316-323), and the schema
630
802
  // permits `finally` as a `try` child. The only remaining requirement is
631
803
  // the TS-level shape — at least one of `catch`/`finally` must be present.
804
+ // `try {}` without catch|finally is structurally unreachable (always a TS
805
+ // parse error) and has no taxonomy row — NOT routed through reject().
632
806
  if (!stmt.catchClause && !stmt.finallyBlock)
633
807
  return 'try-no-catch';
634
808
  if (stmt.catchClause) {
635
809
  const cc = stmt.catchClause;
636
810
  if (cc.variableDeclaration && !ts.isIdentifier(cc.variableDeclaration.name))
637
- return 'try-destruct-catch';
811
+ return reject('try-destruct-catch');
638
812
  const catchReason = classifyBranch(cc.block, sf, ctx);
639
813
  if (catchReason !== null)
640
814
  return catchReason;
@@ -659,12 +833,12 @@ function classifyStmt(stmt, sf, ctx) {
659
833
  const op = stmt.expression.operatorToken.kind;
660
834
  if (op >= ts.SyntaxKind.FirstAssignment && op <= ts.SyntaxKind.LastAssignment) {
661
835
  if (op !== ts.SyntaxKind.EqualsToken && !supportedCompoundAssignmentOperator(op)) {
662
- return 'expr-stmt-assignment';
836
+ return reject('expr-stmt-assignment');
663
837
  }
664
838
  if (!isValidKernAssignmentTarget(stmt.expression.left.getText(sf)))
665
- return 'expr-stmt-bad-assign-target';
839
+ return reject('expr-stmt-bad-assign-target');
666
840
  if (!isValidKernAssignmentValue(stmt.expression.right.getText(sf)))
667
- return 'expr-stmt-bad-assign-value';
841
+ return reject('expr-stmt-bad-assign-value');
668
842
  return null;
669
843
  }
670
844
  }
@@ -680,7 +854,7 @@ function classifyStmt(stmt, sf, ctx) {
680
854
  // raw-handler authors get a clear reason and the migrator never rewrites
681
855
  // bytes it cannot reproduce.
682
856
  if (!isValidKernAssignmentTarget(stmt.expression.operand.getText(sf))) {
683
- return 'expr-stmt-bad-assign-target';
857
+ return reject('expr-stmt-bad-assign-target');
684
858
  }
685
859
  return null;
686
860
  }
@@ -688,32 +862,32 @@ function classifyStmt(stmt, sf, ctx) {
688
862
  if (ts.isPrefixUnaryExpression(stmt.expression)) {
689
863
  const op = stmt.expression.operator;
690
864
  if (op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken)
691
- return 'expr-stmt-mutation';
865
+ return reject('expr-stmt-mutation');
692
866
  }
693
867
  if (!isValidKernExpression(stmt.expression.getText(sf)))
694
- return 'expr-stmt-bad-expr';
868
+ return reject('expr-stmt-bad-expr');
695
869
  return null;
696
870
  }
697
871
  if (ts.isForOfStatement(stmt)) {
698
872
  if (!ts.isVariableDeclarationList(stmt.initializer))
699
- return 'for-of-non-decl';
873
+ return reject('for-of-non-decl');
700
874
  if (!(stmt.initializer.flags & ts.NodeFlags.Const))
701
- return 'for-of-non-const';
875
+ return reject('for-of-non-const');
702
876
  const decls = stmt.initializer.declarations;
703
877
  if (decls.length !== 1)
704
- return 'for-of-multi-decl';
878
+ return reject('for-of-multi-decl');
705
879
  const decl = decls[0];
706
880
  if (decl.initializer)
707
- return 'for-of-init';
881
+ return reject('for-of-init');
708
882
  if (!ts.isIdentifier(decl.name)) {
709
883
  const entryBinding = parseForOfEntryBinding(decl.name);
710
884
  if (entryBinding === null)
711
- return 'for-of-destructure';
885
+ return reject('for-of-destructure');
712
886
  if (stmt.awaitModifier && canonicalObjectEntriesSource(stmt.expression, sf) !== null) {
713
- return 'for-of-async-object-entries';
887
+ return reject('for-of-async-object-entries');
714
888
  }
715
889
  if (stmt.awaitModifier && entryBinding.kind !== 'pair')
716
- return 'for-of-async-entry';
890
+ return reject('for-of-async-entry');
717
891
  // KERN-GAPS: sync pair iteration (`for (const [k, v] of expr)`) lifts to
718
892
  // `each pairKey=k pairValue=v in=expr` regardless of whether `expr` is
719
893
  // `Object.entries(...)` — Map.entries(), arrays-of-pairs, and generators
@@ -723,64 +897,68 @@ function classifyStmt(stmt, sf, ctx) {
723
897
  if (!stmt.awaitModifier &&
724
898
  entryBinding.kind !== 'pair' &&
725
899
  canonicalObjectEntriesSource(stmt.expression, sf) === null) {
726
- return 'for-of-sync-pair';
900
+ return reject('for-of-sync-pair');
727
901
  }
728
902
  if (decl.type)
729
- return 'for-of-destructure-type';
903
+ return reject('for-of-destructure-type');
730
904
  }
731
905
  else if (decl.type && !isValidKernTypeAnnotation(decl.type.getText(sf))) {
732
- return 'for-of-bad-type';
906
+ return reject('for-of-bad-type');
733
907
  }
734
908
  if (!isValidKernExpression(stmt.expression.getText(sf)))
735
- return 'for-of-bad-expr';
909
+ return reject('for-of-bad-expr');
736
910
  // Only block-shaped loops are currently migratable. `each` always emits
737
911
  // braces, so migrating `for (const x of xs) do(x);` would drift under
738
912
  // --verify even though it is semantically close.
739
913
  if (!ctx.allowNonBlock && !ts.isBlock(stmt.statement))
740
- return 'for-of-non-block';
914
+ return reject('for-of-non-block');
741
915
  if (ts.isBlock(stmt.statement) && stmt.statement.statements.length === 0)
742
- return 'for-of-empty-body';
916
+ return reject('for-of-empty-body');
743
917
  return classifyBranch(stmt.statement, sf, { ...ctx, loopDepth: ctx.loopDepth + 1 });
744
918
  }
745
919
  if (ts.isWhileStatement(stmt)) {
746
920
  if (!isValidKernExpression(stmt.expression.getText(sf)))
747
- return 'while-bad-cond';
921
+ return reject('while-bad-cond');
748
922
  if (!ctx.allowNonBlock && !ts.isBlock(stmt.statement))
749
- return 'while-non-block';
923
+ return reject('while-non-block');
750
924
  if (ts.isBlock(stmt.statement) && stmt.statement.statements.length === 0)
751
- return 'while-empty-body';
925
+ return reject('while-empty-body');
752
926
  return classifyBranch(stmt.statement, sf, { ...ctx, loopDepth: ctx.loopDepth + 1 });
753
927
  }
754
928
  if (ts.isForStatement(stmt) || ts.isForInStatement(stmt))
755
- return 'for-stmt';
929
+ return reject('for-stmt');
756
930
  if (ts.isDoStatement(stmt))
757
- return 'do-while-stmt';
931
+ return reject('do-while-stmt');
758
932
  if (ts.isSwitchStatement(stmt))
759
- return 'switch-stmt';
933
+ return reject('switch-stmt');
760
934
  if (ts.isBlock(stmt))
761
- return 'bare-block';
935
+ return reject('bare-block');
762
936
  // Fallback — the TS SyntaxKind name surfaces in diagnostics so users have
763
937
  // a starting point when they hit something exotic (label, with, debugger).
764
- return `unsupported-stmt-${ts.SyntaxKind[stmt.kind]}`;
938
+ // Validated against the `unsupported-stmt-` family prefix row.
939
+ return reject(`unsupported-stmt-${ts.SyntaxKind[stmt.kind]}`);
765
940
  }
766
941
  function classifyDestructureDecl(decl, sf) {
942
+ // `var-no-init` is structurally unreachable (an uninitialised destructure
943
+ // short-circuits to `var-destructure` before this branch is reached) and has
944
+ // no taxonomy row — NOT routed through reject().
767
945
  if (!decl.initializer)
768
946
  return 'var-no-init';
769
947
  if (!isValidKernExpression(decl.initializer.getText(sf)))
770
- return 'var-destructure-bad-expr';
948
+ return reject('var-destructure-bad-expr');
771
949
  const name = decl.name;
772
950
  if (ts.isObjectBindingPattern(name)) {
773
951
  if (name.elements.length === 0)
774
- return 'var-destructure-empty';
952
+ return reject('var-destructure-empty');
775
953
  for (const element of name.elements) {
776
954
  if (element.dotDotDotToken)
777
- return 'var-destructure-rest';
955
+ return reject('var-destructure-rest');
778
956
  if (element.initializer)
779
- return 'var-destructure-default';
957
+ return reject('var-destructure-default');
780
958
  if (!ts.isIdentifier(element.name))
781
- return 'var-destructure-nested';
959
+ return reject('var-destructure-nested');
782
960
  if (element.propertyName && !ts.isIdentifier(element.propertyName))
783
- return 'var-destructure-computed';
961
+ return reject('var-destructure-computed');
784
962
  }
785
963
  return null;
786
964
  }
@@ -791,17 +969,17 @@ function classifyDestructureDecl(decl, sf) {
791
969
  continue;
792
970
  concreteElements++;
793
971
  if (element.dotDotDotToken)
794
- return 'var-destructure-rest';
972
+ return reject('var-destructure-rest');
795
973
  if (element.initializer)
796
- return 'var-destructure-default';
974
+ return reject('var-destructure-default');
797
975
  if (!ts.isIdentifier(element.name))
798
- return 'var-destructure-nested';
976
+ return reject('var-destructure-nested');
799
977
  }
800
978
  if (concreteElements === 0)
801
- return 'var-destructure-empty';
979
+ return reject('var-destructure-empty');
802
980
  return null;
803
981
  }
804
- return 'var-destructure';
982
+ return reject('var-destructure');
805
983
  }
806
984
  function parseForOfEntryBinding(name) {
807
985
  if (!ts.isArrayBindingPattern(name))
@@ -855,18 +1033,19 @@ export function classifyHandlerBodyAst(rawBody, opts) {
855
1033
  // public type — the migrator reads it the same way.
856
1034
  const diags = sf.parseDiagnostics;
857
1035
  if (diags && diags.length > 0) {
1036
+ // `classifyParseFailureBoundary` already routes its reasons through reject().
858
1037
  const parseBoundary = classifyParseFailureBoundary(rawBody);
859
1038
  if (parseBoundary !== null)
860
1039
  return { eligible: false, reason: parseBoundary };
861
1040
  if (hasComments(rawBody) && !hasOnlyMigratableComments(rawBody)) {
862
- return { eligible: false, reason: 'comments-present' };
1041
+ return { eligible: false, reason: reject('comments-present') };
863
1042
  }
864
- return { eligible: false, reason: 'ts-parse-error' };
1043
+ return { eligible: false, reason: reject('ts-parse-error') };
865
1044
  }
866
1045
  if (isHostInteropBody(sf))
867
- return { eligible: false, reason: 'foreign-by-design' };
1046
+ return { eligible: false, reason: reject('foreign-by-design') };
868
1047
  if (hasComments(rawBody) && !hasOnlyMigratableComments(rawBody)) {
869
- return { eligible: false, reason: 'comments-present' };
1048
+ return { eligible: false, reason: reject('comments-present') };
870
1049
  }
871
1050
  for (const stmt of sf.statements) {
872
1051
  const r = classifyStmt(stmt, sf, { loopDepth: 0, allowNonBlock: opts?.allowNonBlock });