sela-core 1.0.6 → 1.0.8

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 (124) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/ErrorHandler.d.ts +1 -1
  3. package/dist/cli/ErrorHandler.js +14 -14
  4. package/dist/cli/commands/bulk.d.ts +1 -1
  5. package/dist/cli/commands/bulk.d.ts.map +1 -1
  6. package/dist/cli/commands/bulk.js +51 -33
  7. package/dist/cli/commands/init.d.ts +1 -1
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +26 -18
  10. package/dist/cli/commands/showReport.d.ts +1 -1
  11. package/dist/cli/commands/showReport.d.ts.map +1 -1
  12. package/dist/cli/commands/showReport.js +12 -10
  13. package/dist/cli/commands/status.d.ts +1 -1
  14. package/dist/cli/commands/status.d.ts.map +1 -1
  15. package/dist/cli/commands/status.js +8 -8
  16. package/dist/cli/commands/sync.d.ts +1 -1
  17. package/dist/cli/commands/sync.d.ts.map +1 -1
  18. package/dist/cli/commands/sync.js +38 -30
  19. package/dist/cli/index.js +9 -9
  20. package/dist/cli/ui/DnaTable.d.ts +1 -1
  21. package/dist/cli/ui/DnaTable.d.ts.map +1 -1
  22. package/dist/cli/ui/DnaTable.js +23 -16
  23. package/dist/cli/ui/RefactorWizard.d.ts +3 -3
  24. package/dist/cli/ui/RefactorWizard.d.ts.map +1 -1
  25. package/dist/cli/ui/RefactorWizard.js +108 -82
  26. package/dist/config/ConfigLoader.d.ts +3 -2
  27. package/dist/config/ConfigLoader.d.ts.map +1 -1
  28. package/dist/config/ConfigLoader.js +23 -13
  29. package/dist/config/DryRunGuard.d.ts +14 -0
  30. package/dist/config/DryRunGuard.d.ts.map +1 -0
  31. package/dist/config/DryRunGuard.js +79 -0
  32. package/dist/config/SelaConfig.d.ts +26 -16
  33. package/dist/config/SelaConfig.d.ts.map +1 -1
  34. package/dist/config/SelaConfig.js +54 -22
  35. package/dist/engine/HealingRegistry.d.ts +1 -1
  36. package/dist/engine/HealingRegistry.js +3 -3
  37. package/dist/engine/SelaEngine.d.ts +13 -10
  38. package/dist/engine/SelaEngine.d.ts.map +1 -1
  39. package/dist/engine/SelaEngine.js +292 -101
  40. package/dist/errors/SelaError.d.ts +132 -0
  41. package/dist/errors/SelaError.d.ts.map +1 -0
  42. package/dist/errors/SelaError.js +154 -0
  43. package/dist/fixtures/expectProxy.d.ts +1 -1
  44. package/dist/fixtures/expectProxy.d.ts.map +1 -1
  45. package/dist/fixtures/expectProxy.js +17 -10
  46. package/dist/fixtures/index.d.ts +7 -1
  47. package/dist/fixtures/index.d.ts.map +1 -1
  48. package/dist/fixtures/index.js +26 -11
  49. package/dist/fixtures/proxyTag.d.ts +12 -0
  50. package/dist/fixtures/proxyTag.d.ts.map +1 -0
  51. package/dist/fixtures/proxyTag.js +45 -0
  52. package/dist/index.d.ts +8 -6
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +9 -4
  55. package/dist/reporter/SelaReporter.d.ts +143 -0
  56. package/dist/reporter/SelaReporter.d.ts.map +1 -0
  57. package/dist/reporter/SelaReporter.js +428 -0
  58. package/dist/services/ASTSourceUpdater.d.ts +20 -1
  59. package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
  60. package/dist/services/ASTSourceUpdater.js +257 -89
  61. package/dist/services/ArgumentTypeAnalyzer.js +7 -7
  62. package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -1
  63. package/dist/services/BlastRadiusAnalyzer.js +5 -2
  64. package/dist/services/ChainValidator.js +22 -22
  65. package/dist/services/CrossFileHealer.js +7 -7
  66. package/dist/services/DefinitionTracer.d.ts +1 -1
  67. package/dist/services/DefinitionTracer.d.ts.map +1 -1
  68. package/dist/services/DefinitionTracer.js +28 -13
  69. package/dist/services/DnaEditorService.d.ts +2 -2
  70. package/dist/services/DnaEditorService.d.ts.map +1 -1
  71. package/dist/services/DnaEditorService.js +81 -62
  72. package/dist/services/DnaIndexService.d.ts +1 -1
  73. package/dist/services/DnaIndexService.d.ts.map +1 -1
  74. package/dist/services/DnaIndexService.js +14 -14
  75. package/dist/services/HealReportService.d.ts +43 -1
  76. package/dist/services/HealReportService.d.ts.map +1 -1
  77. package/dist/services/HealReportService.js +108 -25
  78. package/dist/services/HealingAdvisory.js +1 -1
  79. package/dist/services/HealingCacheService.d.ts +135 -0
  80. package/dist/services/HealingCacheService.d.ts.map +1 -0
  81. package/dist/services/HealingCacheService.js +460 -0
  82. package/dist/services/HealthReportService.d.ts +1 -1
  83. package/dist/services/HealthReportService.d.ts.map +1 -1
  84. package/dist/services/HealthReportService.js +25 -25
  85. package/dist/services/InitializerUpdater.d.ts.map +1 -1
  86. package/dist/services/InitializerUpdater.js +20 -1
  87. package/dist/services/IntentAuditor.d.ts +18 -1
  88. package/dist/services/IntentAuditor.d.ts.map +1 -1
  89. package/dist/services/IntentAuditor.js +50 -26
  90. package/dist/services/InteractiveReview.d.ts +15 -0
  91. package/dist/services/InteractiveReview.d.ts.map +1 -0
  92. package/dist/services/InteractiveReview.js +125 -0
  93. package/dist/services/LLMService.d.ts +18 -2
  94. package/dist/services/LLMService.d.ts.map +1 -1
  95. package/dist/services/LLMService.js +33 -23
  96. package/dist/services/PRAutomationService.d.ts +14 -1
  97. package/dist/services/PRAutomationService.d.ts.map +1 -1
  98. package/dist/services/PRAutomationService.js +287 -92
  99. package/dist/services/PendingPromptLedger.d.ts +44 -0
  100. package/dist/services/PendingPromptLedger.d.ts.map +1 -0
  101. package/dist/services/PendingPromptLedger.js +180 -0
  102. package/dist/services/ReportGenerator.d.ts +70 -0
  103. package/dist/services/ReportGenerator.d.ts.map +1 -0
  104. package/dist/services/ReportGenerator.js +191 -0
  105. package/dist/services/SafetyGuard.d.ts +30 -7
  106. package/dist/services/SafetyGuard.d.ts.map +1 -1
  107. package/dist/services/SafetyGuard.js +85 -116
  108. package/dist/services/SnapshotService.js +5 -5
  109. package/dist/services/SourceLinkService.d.ts +2 -2
  110. package/dist/services/SourceLinkService.d.ts.map +1 -1
  111. package/dist/services/SourceLinkService.js +31 -22
  112. package/dist/services/SourceUpdater.d.ts +1 -1
  113. package/dist/services/SourceUpdater.d.ts.map +1 -1
  114. package/dist/services/SourceUpdater.js +66 -34
  115. package/dist/services/TemplateDiffService.d.ts.map +1 -1
  116. package/dist/services/TemplateDiffService.js +18 -15
  117. package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
  118. package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
  119. package/dist/services/WorkspaceSnapshotService.js +200 -0
  120. package/dist/types/index.d.ts +1 -1
  121. package/dist/utils/IsolatedDiff.d.ts +45 -0
  122. package/dist/utils/IsolatedDiff.d.ts.map +1 -0
  123. package/dist/utils/IsolatedDiff.js +382 -0
  124. package/package.json +71 -67
@@ -37,6 +37,9 @@ exports.ASTSourceUpdater = void 0;
37
37
  const path = __importStar(require("path"));
38
38
  const fs = __importStar(require("fs"));
39
39
  const ts_morph_1 = require("ts-morph");
40
+ const DryRunGuard_1 = require("../config/DryRunGuard");
41
+ const WorkspaceSnapshotService_1 = require("./WorkspaceSnapshotService");
42
+ const SelaError_1 = require("../errors/SelaError");
40
43
  const HealingRegistry_1 = require("../engine/HealingRegistry");
41
44
  const ArgumentTypeAnalyzer_1 = require("./ArgumentTypeAnalyzer");
42
45
  const BlastRadiusAnalyzer_1 = require("./BlastRadiusAnalyzer");
@@ -47,7 +50,7 @@ const CrossFileHealer_1 = require("./CrossFileHealer");
47
50
  const HealingAdvisory_1 = require("./HealingAdvisory");
48
51
  const ChainValidator_1 = require("./ChainValidator");
49
52
  // ═══════════════════════════════════════════════════════════════════
50
- // FIX #1 normalizeSelector utility
53
+ // FIX #1 - normalizeSelector utility
51
54
  // ═══════════════════════════════════════════════════════════════════
52
55
  function normalizeSelector(s) {
53
56
  return s
@@ -67,7 +70,7 @@ function normalizeSelector(s) {
67
70
  // of "varName", or null otherwise.
68
71
  //
69
72
  // Why this matters:
70
- // const shadowBtn = shadowBtn.getByRole(…) ← CIRCULAR must be caught
73
+ // const shadowBtn = shadowBtn.getByRole(…) ← CIRCULAR - must be caught
71
74
  // const shadowBtn = page.getByRole(…) ← CORRECT
72
75
  //
73
76
  // We walk UP the ancestor chain from the call node looking for a
@@ -80,7 +83,7 @@ function getEnclosingVariableDeclaration(node) {
80
83
  if (ts_morph_1.Node.isVariableDeclaration(current)) {
81
84
  return current;
82
85
  }
83
- // Stop climbing past statement boundaries we only want the immediate
86
+ // Stop climbing past statement boundaries - we only want the immediate
84
87
  // enclosing declaration, not a declaration somewhere further up the tree.
85
88
  if (ts_morph_1.Node.isBlock(current) ||
86
89
  ts_morph_1.Node.isSourceFile(current) ||
@@ -118,7 +121,7 @@ function isInsideOwnInitializer(callNode, varName) {
118
121
  function resolveSafeReceiver(proposedReceiver, rootReceiver, callNode) {
119
122
  if (isInsideOwnInitializer(callNode, proposedReceiver)) {
120
123
  console.log(`[ScopeGuard] 🔒 Circular reference prevented: ` +
121
- `"${proposedReceiver}" is being initialized ` +
124
+ `"${proposedReceiver}" is being initialized - ` +
122
125
  `falling back to root receiver "${rootReceiver}"`);
123
126
  return rootReceiver;
124
127
  }
@@ -551,7 +554,7 @@ class MethodMapper {
551
554
  for (const seg of segments) {
552
555
  const callText = MethodMapper.build(currentReceiver, seg);
553
556
  if (callText === null) {
554
- console.warn(`[MethodMapper] Cannot build segment type "${seg.type}" aborting chain`);
557
+ console.warn(`[MethodMapper] Cannot build segment type "${seg.type}" - aborting chain`);
555
558
  return null;
556
559
  }
557
560
  const methodPart = callText.slice(currentReceiver.length);
@@ -614,7 +617,7 @@ function deduplicateChainSegments(aiSegments, contextSegments) {
614
617
  if (overlap === 0)
615
618
  return { deduped: aiSegments, stripped: 0 };
616
619
  console.log(`[ChainDedup] ✂️ Stripping ${overlap}/${contextSegments.length} duplicate leading ` +
617
- `segment(s) context already encodes them`);
620
+ `segment(s) - context already encodes them`);
618
621
  return { deduped: aiSegments.slice(overlap), stripped: overlap };
619
622
  }
620
623
  const PAGE_ROOT_NAMES = new Set([
@@ -723,7 +726,34 @@ function resolveVariableToSegments(sourceFile, varName, beforeLine, depth = 0) {
723
726
  };
724
727
  }
725
728
  // ═══════════════════════════════════════════════════════════════════
726
- // NODE TRANSFORMATION STRATEGY — Scope-Aware, Zero-Redundancy
729
+ // SHARED PERSISTENCE HOOK
730
+ //
731
+ // Module-level write gate shared between `ASTSourceUpdater` and the
732
+ // internal `NodeTransformationStrategy`. Centralises three concerns
733
+ // every `saveSync` call site needs:
734
+ // 1. `DryRunGuard.active()` short-circuit - keeps the mutation in
735
+ // memory but suppresses the on-disk write under SELA_DRY_RUN.
736
+ // 2. `sharedWorkspaceSnapshot.preWrite()` - captures `contentBefore`
737
+ // so the Clean-Room ledger / Local DX rollback can rewind.
738
+ // 3. `sharedWorkspaceSnapshot.postWrite()` - refreshes `contentAfter`
739
+ // so `getMutatedSnapshots()` (driving the pending-prompts ledger
740
+ // and the interactive CLI prompt) sees the new disk state.
741
+ //
742
+ // Strategies do NOT receive the persistence service via DI - they call
743
+ // this helper directly. Same singleton, zero coupling, one code path.
744
+ // ═══════════════════════════════════════════════════════════════════
745
+ function persistSourceFile(sourceFile) {
746
+ if (DryRunGuard_1.DryRunGuard.active()) {
747
+ DryRunGuard_1.DryRunGuard.logSkippedWrite("saveSync", sourceFile.getFilePath());
748
+ return;
749
+ }
750
+ const fp = sourceFile.getFilePath();
751
+ WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.preWrite(fp);
752
+ sourceFile.saveSync();
753
+ WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.postWrite(fp);
754
+ }
755
+ // ═══════════════════════════════════════════════════════════════════
756
+ // NODE TRANSFORMATION STRATEGY - Scope-Aware, Zero-Redundancy
727
757
  // ═══════════════════════════════════════════════════════════════════
728
758
  class NodeTransformationStrategy {
729
759
  chainValidator;
@@ -793,8 +823,10 @@ class NodeTransformationStrategy {
793
823
  // When AI's first segment TYPE matches the receiver variable's defining type
794
824
  // but with a different selector, the AI is replacing the receiver context.
795
825
  // Strategy: update the receiver variable's definition, then use remaining
796
- // segments with the receiver var preserving developer's variable structure.
797
- if (!forcedReceiver && discoveredAstRoot && !PAGE_ROOT_NAMES.has(discoveredAstRoot)) {
826
+ // segments with the receiver var - preserving developer's variable structure.
827
+ if (!forcedReceiver &&
828
+ discoveredAstRoot &&
829
+ !PAGE_ROOT_NAMES.has(discoveredAstRoot)) {
798
830
  const typeCheckRes = resolveVariableToSegments(sourceFile, discoveredAstRoot, caller.line);
799
831
  if (typeCheckRes.segments.length > 0 && workingSegments.length > 0) {
800
832
  const receiverDefType = typeCheckRes.segments[typeCheckRes.segments.length - 1].type;
@@ -893,8 +925,9 @@ class NodeTransformationStrategy {
893
925
  // to prevent. Skip the candidate; the semantic gate owns this case.
894
926
  const firstArg = call.getArguments()[0];
895
927
  if (firstArg &&
896
- (ts_morph_1.Node.isIdentifier(firstArg) || ts_morph_1.Node.isPropertyAccessExpression(firstArg))) {
897
- console.log(`[NodeTransform] 🛑 skip — first arg is ${firstArg.getKindName()} ` +
928
+ (ts_morph_1.Node.isIdentifier(firstArg) ||
929
+ ts_morph_1.Node.isPropertyAccessExpression(firstArg))) {
930
+ console.log(`[NodeTransform] 🛑 skip - first arg is ${firstArg.getKindName()} ` +
898
931
  `"${firstArg.getText()}" (semantic dispatch owns definition-site mutations)`);
899
932
  continue;
900
933
  }
@@ -906,12 +939,12 @@ class NodeTransformationStrategy {
906
939
  // Before we build code, we must verify that the proposed receiver
907
940
  // does not create a circular self-reference.
908
941
  //
909
- // Case A forcedReceiver is set (strip-and-force path):
942
+ // Case A - forcedReceiver is set (strip-and-force path):
910
943
  // Check if the chainTop lives inside the initializer of that var.
911
944
  // If yes, fall back to the actual AST root (e.g. "page").
912
945
  //
913
- // Case B no forced receiver:
914
- // Use astRoot directly it was walked up from the call node and
946
+ // Case B - no forced receiver:
947
+ // Use astRoot directly - it was walked up from the call node and
915
948
  // is always either "page" or a stable variable reference.
916
949
  // ══════════════════════════════════════════════════════════════
917
950
  let effectiveReceiver;
@@ -959,7 +992,7 @@ class NodeTransformationStrategy {
959
992
  ` ${newCode.split("\n")[0]}`);
960
993
  try {
961
994
  chainTop.replaceWithText(newCode);
962
- sourceFile.saveSync();
995
+ persistSourceFile(sourceFile);
963
996
  return {
964
997
  success: true,
965
998
  reason: `scope-aware transform [${effectiveReceiver}]: ${resolvedSegments.length} segments`,
@@ -1146,7 +1179,7 @@ class NodeTransformationStrategy {
1146
1179
  console.log(`[NodeTransform] 🔬 Partial: segment[${changedIdx}] .${currentMethodName}() → ${newCallText}`);
1147
1180
  try {
1148
1181
  targetCall.replaceWithText(newCallText);
1149
- sourceFile.saveSync();
1182
+ persistSourceFile(sourceFile);
1150
1183
  return {
1151
1184
  success: true,
1152
1185
  reason: `partial: seg[${changedIdx}] .${currentMethodName}() → .${newSeg.type}()`,
@@ -1161,7 +1194,7 @@ class NodeTransformationStrategy {
1161
1194
  }
1162
1195
  updateReceiverDefinition(sourceFile, varName, segType, newSelector, callerLine) {
1163
1196
  // Collect all matching declarations, then pick the closest one that
1164
- // appears strictly BEFORE callerLine prevents accidentally updating
1197
+ // appears strictly BEFORE callerLine - prevents accidentally updating
1165
1198
  // a same-named variable in a different test block that happens to be
1166
1199
  // within the 100-line distance window.
1167
1200
  const declarations = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration);
@@ -1201,7 +1234,7 @@ class NodeTransformationStrategy {
1201
1234
  if (!ts_morph_1.Node.isStringLiteral(firstArg))
1202
1235
  continue;
1203
1236
  firstArg.replaceWithText(`"${newSelector}"`);
1204
- sourceFile.saveSync();
1237
+ persistSourceFile(sourceFile);
1205
1238
  return true;
1206
1239
  }
1207
1240
  return false;
@@ -1272,7 +1305,7 @@ class NodeTransformationStrategy {
1272
1305
  .map((d) => `[TS${d.getCode()}] ${d.getMessageText()}`)
1273
1306
  .join(", "));
1274
1307
  if (/\.(getBy\w+|frameLocator)\s*\(/.test(code)) {
1275
- console.warn(`[NodeTransform] ⚠️ Semantic method detected bypassing validation`);
1308
+ console.warn(`[NodeTransform] ⚠️ Semantic method detected - bypassing validation`);
1276
1309
  return true;
1277
1310
  }
1278
1311
  return false;
@@ -1529,6 +1562,112 @@ class ASTSourceUpdater {
1529
1562
  flushAdvisories() {
1530
1563
  this.advisoryBuffer.flush();
1531
1564
  }
1565
+ /**
1566
+ * Centralised write hook for every ts-morph `saveSync` call in this class.
1567
+ * When `SELA_DRY_RUN` is active the AST mutation stays in memory but the
1568
+ * on-disk file is left untouched - the heal pipeline can still compute a
1569
+ * candidate diff via `sourceFile.getFullText()` and the report layer
1570
+ * tags the event with `dryRun: true`.
1571
+ */
1572
+ _persistChanges(sourceFile) {
1573
+ persistSourceFile(sourceFile);
1574
+ }
1575
+ // ─────────────────────────────────────────────────────────────────
1576
+ // FAIL-FAST DIRECTIVE SCAN
1577
+ //
1578
+ // Walks the leading comment ranges of the failing statement (and the
1579
+ // CallExpression node when it differs) looking for the directive
1580
+ // `// sela-fail-fast`. When found, throws `SelaError(ASTUpdater,
1581
+ // AST_HEAL_DISABLED_BY_DIRECTIVE)` so SelaEngine can short-circuit
1582
+ // BEFORE the LLM call and record a FailedEvent in the "Skipped by
1583
+ // Developer" category.
1584
+ //
1585
+ // Implementation note - ts-morph's `Node.getLeadingCommentRanges()`
1586
+ // returns `ts.CommentRange[]` (offsets, not text). We slice the full
1587
+ // source text by `(pos, end)` to recover the raw comment for each
1588
+ // range, matching both `//` line comments and `/* */` block comments
1589
+ // with either `sela-fail-fast` or `@sela-fail-fast` spelling.
1590
+ // ─────────────────────────────────────────────────────────────────
1591
+ scanFailFastDirective(caller) {
1592
+ const filePath = this.resolveFilePath(caller.filePath);
1593
+ if (!filePath)
1594
+ return { failFast: false };
1595
+ let sourceFile = this.project.getSourceFile(filePath);
1596
+ if (sourceFile) {
1597
+ sourceFile.refreshFromFileSystemSync();
1598
+ }
1599
+ else {
1600
+ sourceFile = this.project.addSourceFileAtPath(filePath);
1601
+ }
1602
+ if (caller.line <= 0)
1603
+ return { failFast: false };
1604
+ const failureNode = this.findNodeAtLine(sourceFile, caller.line);
1605
+ if (!failureNode)
1606
+ return { failFast: false };
1607
+ const statement = this.getEnclosingStatement(failureNode);
1608
+ // Collect candidate comment-owning nodes: enclosing statement (catches
1609
+ // pre-statement comments) AND the failure node itself (catches inline
1610
+ // pre-CallExpression comments inside multi-line chains). De-dup by
1611
+ // offset to avoid double-matching.
1612
+ const fullText = sourceFile.getFullText();
1613
+ const seen = new Set();
1614
+ const nodes = [];
1615
+ if (statement)
1616
+ nodes.push(statement);
1617
+ if (failureNode !== statement)
1618
+ nodes.push(failureNode);
1619
+ const DIRECTIVE_RE = /\/[\/*]\s*@?sela-fail-fast\b/i;
1620
+ for (const node of nodes) {
1621
+ let ranges = [];
1622
+ try {
1623
+ ranges = node.getLeadingCommentRanges() ?? [];
1624
+ }
1625
+ catch {
1626
+ ranges = [];
1627
+ }
1628
+ for (const r of ranges) {
1629
+ const pos = r.getPos();
1630
+ const end = r.getEnd();
1631
+ const key = `${pos}:${end}`;
1632
+ if (seen.has(key))
1633
+ continue;
1634
+ seen.add(key);
1635
+ const text = fullText.slice(pos, end);
1636
+ if (DIRECTIVE_RE.test(text)) {
1637
+ const lineNum = sourceFile.getLineAndColumnAtPos(pos).line;
1638
+ return {
1639
+ failFast: true,
1640
+ matchedComment: text.trim(),
1641
+ matchedLine: lineNum,
1642
+ };
1643
+ }
1644
+ }
1645
+ }
1646
+ return { failFast: false };
1647
+ }
1648
+ /**
1649
+ * Convenience helper - runs `scanFailFastDirective` and throws the
1650
+ * canonical SelaError when the directive is present. Used by SelaEngine
1651
+ * as the very first gate inside `heal()`.
1652
+ */
1653
+ enforceFailFastDirective(caller) {
1654
+ const result = this.scanFailFastDirective(caller);
1655
+ if (!result.failFast)
1656
+ return;
1657
+ throw new SelaError_1.SelaError({
1658
+ subsystem: "ASTUpdater",
1659
+ code: SelaError_1.AST_UPDATER_CODES.HEAL_DISABLED_BY_DIRECTIVE,
1660
+ reason: `Heal disabled by developer directive at ${path.basename(caller.filePath)}` +
1661
+ (result.matchedLine ? `:${result.matchedLine}` : ""),
1662
+ context: {
1663
+ filePath: caller.filePath,
1664
+ line: caller.line,
1665
+ directiveLine: result.matchedLine,
1666
+ directiveText: result.matchedComment,
1667
+ category: "Skipped by Developer",
1668
+ },
1669
+ });
1670
+ }
1532
1671
  constructor() {
1533
1672
  this.project = new ts_morph_1.Project({
1534
1673
  useInMemoryFileSystem: false,
@@ -1654,7 +1793,7 @@ class ASTSourceUpdater {
1654
1793
  sourceFile = this.project.addSourceFileAtPath(filePath);
1655
1794
  }
1656
1795
  if (caller.line <= 0) {
1657
- console.log(`[ASTUpdater] ⚠️ line ${caller.line} invalid Attempting Global Template Strategy`);
1796
+ console.log(`[ASTUpdater] ⚠️ line ${caller.line} invalid - Attempting Global Template Strategy`);
1658
1797
  const globalResult = this.strategyTemplateLiteral(sourceFile, caller, oldSelector, newSelector);
1659
1798
  if (globalResult && globalResult.success) {
1660
1799
  if (contentChange)
@@ -1673,6 +1812,14 @@ class ASTSourceUpdater {
1673
1812
  const statement = failureNode
1674
1813
  ? this.getEnclosingStatement(failureNode)
1675
1814
  : undefined;
1815
+ // ─── Developer fail-fast directive ─────────────────────────────
1816
+ // Scan leading comments on the enclosing statement (and the failure
1817
+ // node when they differ) for `// sela-fail-fast`. When present, abort
1818
+ // all heal work with SelaError(AST_HEAL_DISABLED_BY_DIRECTIVE) so the
1819
+ // engine records a FailedEvent in the "Skipped by Developer" bucket.
1820
+ // Defense-in-depth - SelaEngine also calls this at the very start of
1821
+ // heal() to skip the LLM round-trip entirely.
1822
+ this.enforceFailFastDirective(caller);
1676
1823
  const suspectIdentifier = statement
1677
1824
  ? this.identifySuspectIdentifier(statement, caller.line)
1678
1825
  : null;
@@ -1683,7 +1830,7 @@ class ASTSourceUpdater {
1683
1830
  // HARD EXIT: any non-null result (success OR failure) stops all
1684
1831
  // subsequent AST strategies. When the locator argument is an
1685
1832
  // Identifier, PropertyAccess, or TemplateLiteral, the gate owns
1686
- // the mutation NT/H must never override the symbolic reference.
1833
+ // the mutation - NT/H must never override the symbolic reference.
1687
1834
  const semanticResult = this.semanticDispatch(sourceFile, caller, oldSelector, newSelector);
1688
1835
  if (semanticResult !== null) {
1689
1836
  if (semanticResult.success) {
@@ -1692,7 +1839,7 @@ class ASTSourceUpdater {
1692
1839
  }
1693
1840
  return this.broadcastAndReturn(semanticResult, oldSelector, newSelector, caller);
1694
1841
  }
1695
- // Failure (blast-radius block, trace failed, syntax error) hard stop.
1842
+ // Failure (blast-radius block, trace failed, syntax error) - hard stop.
1696
1843
  console.log(`[SemanticDispatch] 🚫 Hard stop (${semanticResult.strategy}): ${semanticResult.reason}`);
1697
1844
  return semanticResult;
1698
1845
  }
@@ -1741,7 +1888,7 @@ class ASTSourceUpdater {
1741
1888
  };
1742
1889
  }
1743
1890
  // ═══════════════════════════════════════════════════════════════
1744
- // STRATEGY H Smart Chain (Legacy fallback)
1891
+ // STRATEGY H - Smart Chain (Legacy fallback)
1745
1892
  // ═══════════════════════════════════════════════════════════════
1746
1893
  strategySmartChain(sourceFile, caller, oldSelector, oldSem, smartSegments, originalChainHint, suspectIdentifier) {
1747
1894
  console.log(`[ASTUpdater] 🔗 Strategy H v2: Smart Chain (${smartSegments.length} segments)`);
@@ -1815,13 +1962,14 @@ class ASTSourceUpdater {
1815
1962
  return b.chainDepth - a.chainDepth;
1816
1963
  });
1817
1964
  for (const { leafCall, chainTop, rootReceiver, chainDepth, originalText, } of candidates) {
1818
- // Identifier/PropertyAccess guard mirrors the NT guard.
1965
+ // Identifier/PropertyAccess guard - mirrors the NT guard.
1819
1966
  // Strategy H must never substitute a hardcoded string for a symbolic
1820
1967
  // reference; the semantic dispatch gate owns definition-site mutations.
1821
1968
  const firstArgH = leafCall.getArguments()[0];
1822
1969
  if (firstArgH &&
1823
- (ts_morph_1.Node.isIdentifier(firstArgH) || ts_morph_1.Node.isPropertyAccessExpression(firstArgH))) {
1824
- console.log(`[StrategyH] 🛑 skip — first arg is ${firstArgH.getKindName()} ` +
1970
+ (ts_morph_1.Node.isIdentifier(firstArgH) ||
1971
+ ts_morph_1.Node.isPropertyAccessExpression(firstArgH))) {
1972
+ console.log(`[StrategyH] 🛑 skip - first arg is ${firstArgH.getKindName()} ` +
1825
1973
  `"${firstArgH.getText()}" (semantic dispatch owns definition-site mutations)`);
1826
1974
  continue;
1827
1975
  }
@@ -1850,7 +1998,7 @@ class ASTSourceUpdater {
1850
1998
  console.log(`[ASTUpdater] 🔗 H: full replace (depth=${chainDepth}) →\n${newCode}`);
1851
1999
  try {
1852
2000
  chainTop.replaceWithText(newCode);
1853
- sourceFile.saveSync();
2001
+ this._persistChanges(sourceFile);
1854
2002
  return {
1855
2003
  success: true,
1856
2004
  reason: `smart chain applied (${resolvedSegments.length} segments, depth=${chainDepth})`,
@@ -1891,8 +2039,9 @@ class ASTSourceUpdater {
1891
2039
  // Block H-partial from substituting a literal for an Identifier/PropertyAccess arg.
1892
2040
  const targetFirstArg = targetCall.getArguments()[0];
1893
2041
  if (targetFirstArg &&
1894
- (ts_morph_1.Node.isIdentifier(targetFirstArg) || ts_morph_1.Node.isPropertyAccessExpression(targetFirstArg))) {
1895
- console.log(`[H-partial] 🛑 skip — arg is ${targetFirstArg.getKindName()} ` +
2042
+ (ts_morph_1.Node.isIdentifier(targetFirstArg) ||
2043
+ ts_morph_1.Node.isPropertyAccessExpression(targetFirstArg))) {
2044
+ console.log(`[H-partial] 🛑 skip - arg is ${targetFirstArg.getKindName()} ` +
1896
2045
  `"${targetFirstArg.getText()}" (semantic dispatch owns this)`);
1897
2046
  return null;
1898
2047
  }
@@ -1914,7 +2063,7 @@ class ASTSourceUpdater {
1914
2063
  console.log(`[ASTUpdater] 🔗 H partial: .${currentMethodName}() → ${newCallText}`);
1915
2064
  try {
1916
2065
  targetCall.replaceWithText(newCallText);
1917
- sourceFile.saveSync();
2066
+ this._persistChanges(sourceFile);
1918
2067
  return {
1919
2068
  success: true,
1920
2069
  reason: `partial chain heal: segment[${changedIdx}] .${currentMethodName}() → .${newSeg.type}()`,
@@ -1928,7 +2077,7 @@ class ASTSourceUpdater {
1928
2077
  }
1929
2078
  }
1930
2079
  // ─────────────────────────────────────────────────────────────
1931
- // STRATEGY F Chain Collapse
2080
+ // STRATEGY F - Chain Collapse
1932
2081
  // ─────────────────────────────────────────────────────────────
1933
2082
  strategyChainCollapse(sourceFile, caller, oldSelector, newSelector, oldSem, newSem) {
1934
2083
  console.log(`[ASTUpdater] ⛓️ Strategy F: Chain Collapse (upward traversal)`);
@@ -1975,7 +2124,7 @@ class ASTSourceUpdater {
1975
2124
  const newCallText = buildCallText(rootReceiver, newSem);
1976
2125
  try {
1977
2126
  chainTop.replaceWithText(newCallText);
1978
- sourceFile.saveSync();
2127
+ this._persistChanges(sourceFile);
1979
2128
  return {
1980
2129
  success: true,
1981
2130
  reason: `chain collapsed atomically (depth=${chainDepth})`,
@@ -1994,7 +2143,7 @@ class ASTSourceUpdater {
1994
2143
  };
1995
2144
  }
1996
2145
  // ─────────────────────────────────────────────────────────────
1997
- // STRATEGY G Assertion Healing
2146
+ // STRATEGY G - Assertion Healing
1998
2147
  // ─────────────────────────────────────────────────────────────
1999
2148
  runStrategyG(sourceFile, caller, locatorVarName, contentChange) {
2000
2149
  if (!contentChange)
@@ -2026,7 +2175,7 @@ class ASTSourceUpdater {
2026
2175
  return null;
2027
2176
  }
2028
2177
  // ─────────────────────────────────────────────────────────────
2029
- // STRATEGY A Frame Upstream Healing
2178
+ // STRATEGY A - Frame Upstream Healing
2030
2179
  // ─────────────────────────────────────────────────────────────
2031
2180
  strategyFrameUpstream(sourceFile, caller, oldElementSelector, aiSegments, fullSelectorContext) {
2032
2181
  console.log(`[ASTUpdater] 🧠 Strategy A: Frame Upstream`);
@@ -2059,7 +2208,7 @@ class ASTSourceUpdater {
2059
2208
  }
2060
2209
  }
2061
2210
  if (anyFixed) {
2062
- sourceFile.saveSync();
2211
+ this._persistChanges(sourceFile);
2063
2212
  return {
2064
2213
  success: true,
2065
2214
  reason: `upstream variable '${varName}' fixed`,
@@ -2070,7 +2219,7 @@ class ASTSourceUpdater {
2070
2219
  for (const change of changedFrames) {
2071
2220
  const result = this.replaceStringLiteralInRange(sourceFile, change.old, change.new, declLine, Math.min(sourceFile.getEndLineNumber(), declLine + 8));
2072
2221
  if (result !== null) {
2073
- sourceFile.saveSync();
2222
+ this._persistChanges(sourceFile);
2074
2223
  return {
2075
2224
  success: true,
2076
2225
  reason: "multi-line frame fixed",
@@ -2085,7 +2234,7 @@ class ASTSourceUpdater {
2085
2234
  for (const change of changedFrames) {
2086
2235
  const result = this.replaceStringLiteralInRange(sourceFile, change.old, change.new, Math.max(1, caller.line - 15), caller.line);
2087
2236
  if (result !== null) {
2088
- sourceFile.saveSync();
2237
+ this._persistChanges(sourceFile);
2089
2238
  return {
2090
2239
  success: true,
2091
2240
  reason: "inline frame fixed",
@@ -2102,7 +2251,7 @@ class ASTSourceUpdater {
2102
2251
  };
2103
2252
  }
2104
2253
  // ─────────────────────────────────────────────────────────────
2105
- // STRATEGY B Deep Symbol Resolution
2254
+ // STRATEGY B - Deep Symbol Resolution
2106
2255
  // ─────────────────────────────────────────────────────────────
2107
2256
  strategyDeepSymbolResolution(sourceFile, caller, oldSelector, newSelector, oldSem, newSem, suspectIdentifier) {
2108
2257
  console.log(`[ASTUpdater] 🔭 Strategy B: Deep Symbol Resolution`);
@@ -2166,10 +2315,10 @@ class ASTSourceUpdater {
2166
2315
  const newCallText = buildCallText(effectiveReceiver, newSem);
2167
2316
  try {
2168
2317
  chainTop.replaceWithText(newCallText);
2169
- sourceFile.saveSync();
2318
+ this._persistChanges(sourceFile);
2170
2319
  const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
2171
2320
  if (cascadePairs.length > 0) {
2172
- sourceFile.saveSync();
2321
+ this._persistChanges(sourceFile);
2173
2322
  HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
2174
2323
  }
2175
2324
  return {
@@ -2208,10 +2357,10 @@ class ASTSourceUpdater {
2208
2357
  const { chainTop: tailChainTop, rootReceiver } = climbToChainTop(tailCall);
2209
2358
  try {
2210
2359
  tailChainTop.replaceWithText(buildCallText(rootReceiver, newSem));
2211
- sourceFile.saveSync();
2360
+ this._persistChanges(sourceFile);
2212
2361
  const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
2213
2362
  if (cascadePairs.length > 0) {
2214
- sourceFile.saveSync();
2363
+ this._persistChanges(sourceFile);
2215
2364
  HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
2216
2365
  }
2217
2366
  return {
@@ -2228,10 +2377,10 @@ class ASTSourceUpdater {
2228
2377
  }
2229
2378
  const fixed = this.replaceChainTailOnly(tailCall, newSem, newSelector);
2230
2379
  if (fixed) {
2231
- sourceFile.saveSync();
2380
+ this._persistChanges(sourceFile);
2232
2381
  const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
2233
2382
  if (cascadePairs.length > 0) {
2234
- sourceFile.saveSync();
2383
+ this._persistChanges(sourceFile);
2235
2384
  HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
2236
2385
  }
2237
2386
  return {
@@ -2253,7 +2402,7 @@ class ASTSourceUpdater {
2253
2402
  if (chainDepth > 0 || isLeafOfChain(call)) {
2254
2403
  try {
2255
2404
  stmtChainTop.replaceWithText(buildCallText(rootReceiver, newSem));
2256
- sourceFile.saveSync();
2405
+ this._persistChanges(sourceFile);
2257
2406
  return {
2258
2407
  success: true,
2259
2408
  reason: "direct statement chain collapse",
@@ -2264,10 +2413,10 @@ class ASTSourceUpdater {
2264
2413
  catch { }
2265
2414
  }
2266
2415
  if (this.replaceChainTailOnly(call, newSem, newSelector)) {
2267
- sourceFile.saveSync();
2416
+ this._persistChanges(sourceFile);
2268
2417
  const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
2269
2418
  if (cascadePairs.length > 0) {
2270
- sourceFile.saveSync();
2419
+ this._persistChanges(sourceFile);
2271
2420
  HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
2272
2421
  }
2273
2422
  return {
@@ -2286,7 +2435,7 @@ class ASTSourceUpdater {
2286
2435
  };
2287
2436
  }
2288
2437
  // ─────────────────────────────────────────────────────────────
2289
- // STRATEGY C Inline Semantic Fix
2438
+ // STRATEGY C - Inline Semantic Fix
2290
2439
  // ─────────────────────────────────────────────────────────────
2291
2440
  strategyInlineSemanticFix(sourceFile, caller, oldSelector, newSelector, oldSem, newSem) {
2292
2441
  console.log(`[ASTUpdater] 🎯 Strategy C: Inline Semantic Fix`);
@@ -2301,10 +2450,10 @@ class ASTSourceUpdater {
2301
2450
  try {
2302
2451
  chainTop.replaceWithText(buildCallText(rootReceiver, newSem));
2303
2452
  const fixLine = chainTop.getStartLineNumber();
2304
- sourceFile.saveSync();
2453
+ this._persistChanges(sourceFile);
2305
2454
  const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
2306
2455
  if (cascadePairs.length > 0) {
2307
- sourceFile.saveSync();
2456
+ this._persistChanges(sourceFile);
2308
2457
  HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
2309
2458
  }
2310
2459
  return {
@@ -2319,10 +2468,10 @@ class ASTSourceUpdater {
2319
2468
  }
2320
2469
  if (this.applySemanticFix(call, newSem, newSelector)) {
2321
2470
  const fixLine = call.getStartLineNumber();
2322
- sourceFile.saveSync();
2471
+ this._persistChanges(sourceFile);
2323
2472
  const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
2324
2473
  if (cascadePairs.length > 0) {
2325
- sourceFile.saveSync();
2474
+ this._persistChanges(sourceFile);
2326
2475
  HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
2327
2476
  }
2328
2477
  return {
@@ -2344,7 +2493,7 @@ class ASTSourceUpdater {
2344
2493
  };
2345
2494
  }
2346
2495
  // ─────────────────────────────────────────────────────────────
2347
- // STRATEGY D Template Literal
2496
+ // STRATEGY D - Template Literal
2348
2497
  // ─────────────────────────────────────────────────────────────
2349
2498
  strategyTemplateLiteral(sourceFile, caller, oldSelector, newSelector) {
2350
2499
  console.log(`[ASTUpdater] 🔤 Strategy D: Template Literal`);
@@ -2385,7 +2534,7 @@ class ASTSourceUpdater {
2385
2534
  if (newText !== fullText) {
2386
2535
  tmpl.replaceWithText(newText.trim());
2387
2536
  const line = tmpl.getStartLineNumber();
2388
- sourceFile.saveSync();
2537
+ this._persistChanges(sourceFile);
2389
2538
  return {
2390
2539
  success: true,
2391
2540
  reason: "template literal suffix updated",
@@ -2401,7 +2550,7 @@ class ASTSourceUpdater {
2401
2550
  };
2402
2551
  }
2403
2552
  // ─────────────────────────────────────────────────────────────
2404
- // STRATEGY E Semantic Fragment Scan
2553
+ // STRATEGY E - Semantic Fragment Scan
2405
2554
  // ─────────────────────────────────────────────────────────────
2406
2555
  strategySemanticFragmentScan(sourceFile, caller, oldSelector, newSelector, oldSem, newSem) {
2407
2556
  console.log(`[ASTUpdater] 🔎 Strategy E: Semantic Fragment Scan`);
@@ -2430,7 +2579,7 @@ class ASTSourceUpdater {
2430
2579
  if (chainDepth > 0 || isLeafOfChain(call)) {
2431
2580
  try {
2432
2581
  chainTop.replaceWithText(buildCallText(rootReceiver, newSem));
2433
- sourceFile.saveSync();
2582
+ this._persistChanges(sourceFile);
2434
2583
  return {
2435
2584
  success: true,
2436
2585
  reason: `fragment scan chain collapse .${callSem.method}() @ line ${call.getStartLineNumber()}`,
@@ -2441,7 +2590,7 @@ class ASTSourceUpdater {
2441
2590
  catch { }
2442
2591
  }
2443
2592
  if (this.replaceChainTailOnly(call, newSem, newSelector)) {
2444
- sourceFile.saveSync();
2593
+ this._persistChanges(sourceFile);
2445
2594
  return {
2446
2595
  success: true,
2447
2596
  reason: `fragment scan fixed .${callSem.method}() @ line ${call.getStartLineNumber()}`,
@@ -2522,7 +2671,7 @@ class ASTSourceUpdater {
2522
2671
  /**
2523
2672
  * Guard: returns false when replacing oldNode with proposedNewText would
2524
2673
  * substitute a hardcoded string literal for an Identifier or
2525
- * PropertyAccessExpression which is the definition-site mutation bug.
2674
+ * PropertyAccessExpression - which is the definition-site mutation bug.
2526
2675
  * All other replacements are allowed.
2527
2676
  */
2528
2677
  static isSafeReplacement(oldNode, proposedNewText) {
@@ -2546,7 +2695,7 @@ class ASTSourceUpdater {
2546
2695
  firstArg.replaceWithText(`"${safe}"`);
2547
2696
  }
2548
2697
  else {
2549
- console.warn(`[ASTUpdater] ⚠️ replaceCallArgument: blocked unsafe replacement ` +
2698
+ console.warn(`[ASTUpdater] ⚠️ replaceCallArgument: blocked unsafe replacement - ` +
2550
2699
  `arg is ${firstArg.getKindName()}, not a string literal. ` +
2551
2700
  `Heal at definition site instead.`);
2552
2701
  return false;
@@ -2618,7 +2767,7 @@ class ASTSourceUpdater {
2618
2767
  const propName = firstArg.getName();
2619
2768
  const result = this.updateObjectProperty(sourceFile, objName, propName, oldSem.primaryArg, newSem.primaryArg);
2620
2769
  if (result !== null) {
2621
- sourceFile.saveSync();
2770
+ this._persistChanges(sourceFile);
2622
2771
  return {
2623
2772
  success: true,
2624
2773
  reason: `'${objName}.${propName}' updated`,
@@ -2848,10 +2997,10 @@ class ASTSourceUpdater {
2848
2997
  shape === "PROPERTY_ACCESS" ||
2849
2998
  shape === "TEMPLATE_LITERAL") {
2850
2999
  console.log(`[SemanticDispatch] 🛑 hard-block: "${node.getText()}" is ${shape} ` +
2851
- `but trace failed preserving call site`);
3000
+ `but trace failed - preserving call site`);
2852
3001
  return {
2853
3002
  success: false,
2854
- reason: `semantic guard: could not trace "${node.getText()}" call site preserved`,
3003
+ reason: `semantic guard: could not trace "${node.getText()}" - call site preserved`,
2855
3004
  strategy: "semantic-guard",
2856
3005
  };
2857
3006
  }
@@ -2874,21 +3023,21 @@ class ASTSourceUpdater {
2874
3023
  if (blast.risk === "high") {
2875
3024
  this.advisoryBuffer.push({
2876
3025
  code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_BLOCKED,
2877
- message: `Mutation blocked ${blast.testFunctionCount} tests reference this constant.`,
3026
+ message: `Mutation blocked - ${blast.testFunctionCount} tests reference this constant.`,
2878
3027
  affectedFile: targetFile.getFilePath(),
2879
3028
  affectedLine: targetNode.getStartLineNumber() - 1,
2880
3029
  suggestedAction: "Review all referencing tests before changing this constant.",
2881
3030
  });
2882
3031
  return {
2883
3032
  success: false,
2884
- reason: `BlastRadius BLOCKED: ${blast.testFunctionCount} tests reference this constant manual review required`,
3033
+ reason: `BlastRadius BLOCKED: ${blast.testFunctionCount} tests reference this constant - manual review required`,
2885
3034
  strategy: "semantic-identifier",
2886
3035
  };
2887
3036
  }
2888
3037
  if (blast.risk === "medium") {
2889
3038
  this.advisoryBuffer.push({
2890
3039
  code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_WARNING,
2891
- message: `Mutated constant used in ${blast.testFunctionCount} tests verify no regressions.`,
3040
+ message: `Mutated constant used in ${blast.testFunctionCount} tests - verify no regressions.`,
2892
3041
  affectedFile: targetFile.getFilePath(),
2893
3042
  affectedLine: targetNode.getStartLineNumber() - 1,
2894
3043
  suggestedAction: "Run the full test suite to confirm all references still pass.",
@@ -2900,7 +3049,7 @@ class ASTSourceUpdater {
2900
3049
  if (!this.validateFileSyntax(targetFile)) {
2901
3050
  return {
2902
3051
  success: false,
2903
- reason: "post-mutation syntax validation failed reverted",
3052
+ reason: "post-mutation syntax validation failed - reverted",
2904
3053
  strategy: "semantic-identifier",
2905
3054
  };
2906
3055
  }
@@ -2910,7 +3059,10 @@ class ASTSourceUpdater {
2910
3059
  strategy: "semantic-identifier",
2911
3060
  lineUpdated: result.lineUpdated,
2912
3061
  healedLocatorString: result.healedLocatorString,
2913
- definitionSite: { file: targetFile.getFilePath(), line: result.lineUpdated },
3062
+ definitionSite: {
3063
+ file: targetFile.getFilePath(),
3064
+ line: result.lineUpdated,
3065
+ },
2914
3066
  blastRadius: blast.testFunctionCount,
2915
3067
  };
2916
3068
  }
@@ -2939,21 +3091,21 @@ class ASTSourceUpdater {
2939
3091
  if (blast.risk === "high") {
2940
3092
  this.advisoryBuffer.push({
2941
3093
  code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_BLOCKED,
2942
- message: `Mutation blocked ${blast.testFunctionCount} tests reference this property.`,
3094
+ message: `Mutation blocked - ${blast.testFunctionCount} tests reference this property.`,
2943
3095
  affectedFile: targetFile.getFilePath(),
2944
3096
  affectedLine: targetNode.getStartLineNumber() - 1,
2945
3097
  suggestedAction: "Review all referencing tests before changing this property.",
2946
3098
  });
2947
3099
  return {
2948
3100
  success: false,
2949
- reason: `BlastRadius BLOCKED: ${blast.testFunctionCount} tests reference this property manual review required`,
3101
+ reason: `BlastRadius BLOCKED: ${blast.testFunctionCount} tests reference this property - manual review required`,
2950
3102
  strategy: "semantic-property-access",
2951
3103
  };
2952
3104
  }
2953
3105
  if (blast.risk === "medium") {
2954
3106
  this.advisoryBuffer.push({
2955
3107
  code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_WARNING,
2956
- message: `Mutated property used in ${blast.testFunctionCount} tests verify no regressions.`,
3108
+ message: `Mutated property used in ${blast.testFunctionCount} tests - verify no regressions.`,
2957
3109
  affectedFile: targetFile.getFilePath(),
2958
3110
  affectedLine: targetNode.getStartLineNumber() - 1,
2959
3111
  suggestedAction: "Run the full test suite to confirm all references still pass.",
@@ -2965,7 +3117,7 @@ class ASTSourceUpdater {
2965
3117
  if (!this.validateFileSyntax(targetFile)) {
2966
3118
  return {
2967
3119
  success: false,
2968
- reason: "post-mutation syntax validation failed reverted",
3120
+ reason: "post-mutation syntax validation failed - reverted",
2969
3121
  strategy: "semantic-property-access",
2970
3122
  };
2971
3123
  }
@@ -2975,7 +3127,10 @@ class ASTSourceUpdater {
2975
3127
  strategy: "semantic-property-access",
2976
3128
  lineUpdated: result.lineUpdated,
2977
3129
  healedLocatorString: result.healedLocatorString,
2978
- definitionSite: { file: targetFile.getFilePath(), line: result.lineUpdated },
3130
+ definitionSite: {
3131
+ file: targetFile.getFilePath(),
3132
+ line: result.lineUpdated,
3133
+ },
2979
3134
  blastRadius: blast.testFunctionCount,
2980
3135
  };
2981
3136
  }
@@ -3019,10 +3174,11 @@ class ASTSourceUpdater {
3019
3174
  }
3020
3175
  }
3021
3176
  }
3022
- if (patchResult.reason.includes("structural") || patchResult.reason.includes("not found")) {
3177
+ if (patchResult.reason.includes("structural") ||
3178
+ patchResult.reason.includes("not found")) {
3023
3179
  this.advisoryBuffer.push({
3024
3180
  code: HealingAdvisory_1.AdvisoryCode.MANUAL_REVIEW_TEMPLATE_COMPLEX,
3025
- message: `Template literal requires structural change automated patch skipped.`,
3181
+ message: `Template literal requires structural change - automated patch skipped.`,
3026
3182
  affectedFile: sourceFile.getFilePath(),
3027
3183
  affectedLine: node.getStartLineNumber() - 1,
3028
3184
  suggestedAction: "Manually update the template literal and its callers.",
@@ -3032,11 +3188,11 @@ class ASTSourceUpdater {
3032
3188
  }
3033
3189
  const lineUpdated = node.getStartLineNumber() - 1;
3034
3190
  node.replaceWithText(patchResult.patchedTemplate);
3035
- sourceFile.saveSync();
3191
+ this._persistChanges(sourceFile);
3036
3192
  if (!this.validateFileSyntax(sourceFile)) {
3037
3193
  return {
3038
3194
  success: false,
3039
- reason: "post-mutation syntax validation failed reverted",
3195
+ reason: "post-mutation syntax validation failed - reverted",
3040
3196
  strategy: "semantic-template",
3041
3197
  };
3042
3198
  }
@@ -3064,18 +3220,22 @@ class ASTSourceUpdater {
3064
3220
  if (ts_morph_1.Node.isCallExpression(init)) {
3065
3221
  return this.handleCallExpressionArg(init, sourceFile, oldSpanVal, newSpanVal);
3066
3222
  }
3067
- if (ts_morph_1.Node.isStringLiteral(init) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(init)) {
3223
+ if (ts_morph_1.Node.isStringLiteral(init) ||
3224
+ ts_morph_1.Node.isNoSubstitutionTemplateLiteral(init)) {
3068
3225
  const result = InitializerUpdater_1.InitializerUpdater.apply(init, sourceFile, newSpanVal);
3069
3226
  if (!result)
3070
3227
  return null;
3071
- sourceFile.saveSync();
3228
+ this._persistChanges(sourceFile);
3072
3229
  return {
3073
3230
  success: true,
3074
3231
  reason: `span literal updated via trace`,
3075
3232
  strategy: "semantic-template",
3076
3233
  lineUpdated: result.lineUpdated,
3077
3234
  healedLocatorString: result.healedLocatorString,
3078
- definitionSite: { file: sourceFile.getFilePath(), line: result.lineUpdated },
3235
+ definitionSite: {
3236
+ file: sourceFile.getFilePath(),
3237
+ line: result.lineUpdated,
3238
+ },
3079
3239
  };
3080
3240
  }
3081
3241
  if (ts_morph_1.Node.isTemplateExpression(init)) {
@@ -3089,7 +3249,7 @@ class ASTSourceUpdater {
3089
3249
  if (fnAnalysis.kind === "complex") {
3090
3250
  this.advisoryBuffer.push({
3091
3251
  code: HealingAdvisory_1.AdvisoryCode.MANUAL_REVIEW_CONDITIONAL_RETURN,
3092
- message: `Function has conditional/complex return automated patch skipped. Reason: ${fnAnalysis.reason}`,
3252
+ message: `Function has conditional/complex return - automated patch skipped. Reason: ${fnAnalysis.reason}`,
3093
3253
  affectedFile: sourceFile.getFilePath(),
3094
3254
  suggestedAction: "Manually update the function's return expression.",
3095
3255
  });
@@ -3102,7 +3262,7 @@ class ASTSourceUpdater {
3102
3262
  if (!this.validateFileSyntax(fnAnalysis.functionFile)) {
3103
3263
  return {
3104
3264
  success: false,
3105
- reason: "post-mutation syntax validation failed reverted",
3265
+ reason: "post-mutation syntax validation failed - reverted",
3106
3266
  strategy: "semantic-call-expression",
3107
3267
  };
3108
3268
  }
@@ -3112,7 +3272,10 @@ class ASTSourceUpdater {
3112
3272
  strategy: "semantic-call-expression",
3113
3273
  lineUpdated: result.lineUpdated,
3114
3274
  healedLocatorString: result.healedLocatorString,
3115
- definitionSite: { file: fnAnalysis.functionFile.getFilePath(), line: result.lineUpdated },
3275
+ definitionSite: {
3276
+ file: fnAnalysis.functionFile.getFilePath(),
3277
+ line: result.lineUpdated,
3278
+ },
3116
3279
  };
3117
3280
  }
3118
3281
  if (fnAnalysis.kind === "template") {
@@ -3122,7 +3285,7 @@ class ASTSourceUpdater {
3122
3285
  if (patchResult.reason.includes("structural")) {
3123
3286
  this.advisoryBuffer.push({
3124
3287
  code: HealingAdvisory_1.AdvisoryCode.MANUAL_REVIEW_TEMPLATE_COMPLEX,
3125
- message: `Function template return requires structural change automated patch skipped.`,
3288
+ message: `Function template return requires structural change - automated patch skipped.`,
3126
3289
  affectedFile: fnAnalysis.functionFile.getFilePath(),
3127
3290
  affectedLine: fnAnalysis.functionLine - 1,
3128
3291
  suggestedAction: "Manually update the function's template literal.",
@@ -3132,11 +3295,11 @@ class ASTSourceUpdater {
3132
3295
  }
3133
3296
  const lineUpdated = fnAnalysis.templateNode.getStartLineNumber() - 1;
3134
3297
  fnAnalysis.templateNode.replaceWithText(patchResult.patchedTemplate);
3135
- fnAnalysis.functionFile.saveSync();
3298
+ this._persistChanges(fnAnalysis.functionFile);
3136
3299
  if (!this.validateFileSyntax(fnAnalysis.functionFile)) {
3137
3300
  return {
3138
3301
  success: false,
3139
- reason: "post-mutation syntax validation failed reverted",
3302
+ reason: "post-mutation syntax validation failed - reverted",
3140
3303
  strategy: "semantic-call-expression",
3141
3304
  };
3142
3305
  }
@@ -3146,7 +3309,10 @@ class ASTSourceUpdater {
3146
3309
  strategy: "semantic-call-expression",
3147
3310
  lineUpdated,
3148
3311
  healedLocatorString: newSelector,
3149
- definitionSite: { file: fnAnalysis.functionFile.getFilePath(), line: lineUpdated },
3312
+ definitionSite: {
3313
+ file: fnAnalysis.functionFile.getFilePath(),
3314
+ line: lineUpdated,
3315
+ },
3150
3316
  };
3151
3317
  }
3152
3318
  return null;
@@ -3162,11 +3328,13 @@ class ASTSourceUpdater {
3162
3328
  return !ALLOWED_CODES.has(d.getCode());
3163
3329
  });
3164
3330
  if (syntaxErrors.length > 0) {
3165
- const msgs = syntaxErrors.map((d) => String(d.getMessageText())).join("; ");
3166
- console.warn(`[SemanticDispatch] ⚠️ syntax errors after mutation: ${msgs} — reverting`);
3331
+ const msgs = syntaxErrors
3332
+ .map((d) => String(d.getMessageText()))
3333
+ .join("; ");
3334
+ console.warn(`[SemanticDispatch] ⚠️ syntax errors after mutation: ${msgs} - reverting`);
3167
3335
  this.advisoryBuffer.push({
3168
3336
  code: HealingAdvisory_1.AdvisoryCode.POST_MUTATION_TYPE_ERROR,
3169
- message: `Post-mutation type errors detected mutation reverted. Errors: ${msgs}`,
3337
+ message: `Post-mutation type errors detected - mutation reverted. Errors: ${msgs}`,
3170
3338
  affectedFile: sourceFile.getFilePath(),
3171
3339
  suggestedAction: "Manually inspect and fix the type errors introduced by this heal.",
3172
3340
  });