sela-core 0.1.0-alpha.1

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 (267) hide show
  1. package/README.md +103 -0
  2. package/bin/sela.js +3 -0
  3. package/dist/cli/ErrorHandler.d.ts +10 -0
  4. package/dist/cli/ErrorHandler.d.ts.map +1 -0
  5. package/dist/cli/ErrorHandler.js +70 -0
  6. package/dist/cli/commands/bulk.d.ts +3 -0
  7. package/dist/cli/commands/bulk.d.ts.map +1 -0
  8. package/dist/cli/commands/bulk.js +158 -0
  9. package/dist/cli/commands/find.d.ts +3 -0
  10. package/dist/cli/commands/find.d.ts.map +1 -0
  11. package/dist/cli/commands/find.js +51 -0
  12. package/dist/cli/commands/init.d.ts +3 -0
  13. package/dist/cli/commands/init.d.ts.map +1 -0
  14. package/dist/cli/commands/init.js +166 -0
  15. package/dist/cli/commands/list.d.ts +3 -0
  16. package/dist/cli/commands/list.d.ts.map +1 -0
  17. package/dist/cli/commands/list.js +56 -0
  18. package/dist/cli/commands/merge.d.ts +13 -0
  19. package/dist/cli/commands/merge.d.ts.map +1 -0
  20. package/dist/cli/commands/merge.js +119 -0
  21. package/dist/cli/commands/refactor.d.ts +3 -0
  22. package/dist/cli/commands/refactor.d.ts.map +1 -0
  23. package/dist/cli/commands/refactor.js +30 -0
  24. package/dist/cli/commands/showReport.d.ts +3 -0
  25. package/dist/cli/commands/showReport.d.ts.map +1 -0
  26. package/dist/cli/commands/showReport.js +82 -0
  27. package/dist/cli/commands/status.d.ts +3 -0
  28. package/dist/cli/commands/status.d.ts.map +1 -0
  29. package/dist/cli/commands/status.js +51 -0
  30. package/dist/cli/commands/sync.d.ts +3 -0
  31. package/dist/cli/commands/sync.d.ts.map +1 -0
  32. package/dist/cli/commands/sync.js +131 -0
  33. package/dist/cli/index.d.ts +2 -0
  34. package/dist/cli/index.d.ts.map +1 -0
  35. package/dist/cli/index.js +94 -0
  36. package/dist/cli/ui/DnaTable.d.ts +3 -0
  37. package/dist/cli/ui/DnaTable.d.ts.map +1 -0
  38. package/dist/cli/ui/DnaTable.js +77 -0
  39. package/dist/cli/ui/LocatorPicker.d.ts +8 -0
  40. package/dist/cli/ui/LocatorPicker.d.ts.map +1 -0
  41. package/dist/cli/ui/LocatorPicker.js +33 -0
  42. package/dist/cli/ui/ProgressReporter.d.ts +11 -0
  43. package/dist/cli/ui/ProgressReporter.d.ts.map +1 -0
  44. package/dist/cli/ui/ProgressReporter.js +33 -0
  45. package/dist/cli/ui/RefactorWizard.d.ts +26 -0
  46. package/dist/cli/ui/RefactorWizard.d.ts.map +1 -0
  47. package/dist/cli/ui/RefactorWizard.js +295 -0
  48. package/dist/config/ConfigLoader.d.ts +16 -0
  49. package/dist/config/ConfigLoader.d.ts.map +1 -0
  50. package/dist/config/ConfigLoader.js +185 -0
  51. package/dist/config/DryRunGuard.d.ts +14 -0
  52. package/dist/config/DryRunGuard.d.ts.map +1 -0
  53. package/dist/config/DryRunGuard.js +80 -0
  54. package/dist/config/PathScopeGuard.d.ts +42 -0
  55. package/dist/config/PathScopeGuard.d.ts.map +1 -0
  56. package/dist/config/PathScopeGuard.js +192 -0
  57. package/dist/config/SelaConfig.d.ts +178 -0
  58. package/dist/config/SelaConfig.d.ts.map +1 -0
  59. package/dist/config/SelaConfig.js +141 -0
  60. package/dist/config/defineConfig.d.ts +3 -0
  61. package/dist/config/defineConfig.d.ts.map +1 -0
  62. package/dist/config/defineConfig.js +9 -0
  63. package/dist/engine/FixwrightEngine.d.ts +24 -0
  64. package/dist/engine/FixwrightEngine.d.ts.map +1 -0
  65. package/dist/engine/FixwrightEngine.js +403 -0
  66. package/dist/engine/HealingRegistry.d.ts +40 -0
  67. package/dist/engine/HealingRegistry.d.ts.map +1 -0
  68. package/dist/engine/HealingRegistry.js +99 -0
  69. package/dist/engine/SelaEngine.d.ts +57 -0
  70. package/dist/engine/SelaEngine.d.ts.map +1 -0
  71. package/dist/engine/SelaEngine.js +1146 -0
  72. package/dist/engine/singleton.d.ts +3 -0
  73. package/dist/engine/singleton.d.ts.map +1 -0
  74. package/dist/engine/singleton.js +5 -0
  75. package/dist/errors/SelaError.d.ts +146 -0
  76. package/dist/errors/SelaError.d.ts.map +1 -0
  77. package/dist/errors/SelaError.js +168 -0
  78. package/dist/fixtures/expectProxy.d.ts +12 -0
  79. package/dist/fixtures/expectProxy.d.ts.map +1 -0
  80. package/dist/fixtures/expectProxy.js +246 -0
  81. package/dist/fixtures/index.d.ts +30 -0
  82. package/dist/fixtures/index.d.ts.map +1 -0
  83. package/dist/fixtures/index.js +765 -0
  84. package/dist/fixtures/moduleExpect.d.ts +2 -0
  85. package/dist/fixtures/moduleExpect.d.ts.map +1 -0
  86. package/dist/fixtures/moduleExpect.js +54 -0
  87. package/dist/fixtures/proxyTag.d.ts +12 -0
  88. package/dist/fixtures/proxyTag.d.ts.map +1 -0
  89. package/dist/fixtures/proxyTag.js +45 -0
  90. package/dist/index.d.ts +9 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +31 -0
  93. package/dist/reporter/SelaReporter.d.ts +165 -0
  94. package/dist/reporter/SelaReporter.d.ts.map +1 -0
  95. package/dist/reporter/SelaReporter.js +517 -0
  96. package/dist/services/ASTSourceUpdater.d.ts +143 -0
  97. package/dist/services/ASTSourceUpdater.d.ts.map +1 -0
  98. package/dist/services/ASTSourceUpdater.js +3514 -0
  99. package/dist/services/AnchorResolver.d.ts +157 -0
  100. package/dist/services/AnchorResolver.d.ts.map +1 -0
  101. package/dist/services/AnchorResolver.js +289 -0
  102. package/dist/services/ArgumentTypeAnalyzer.d.ts +26 -0
  103. package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -0
  104. package/dist/services/ArgumentTypeAnalyzer.js +92 -0
  105. package/dist/services/BlastRadiusAnalyzer.d.ts +15 -0
  106. package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -0
  107. package/dist/services/BlastRadiusAnalyzer.js +107 -0
  108. package/dist/services/ChainValidator.d.ts +76 -0
  109. package/dist/services/ChainValidator.d.ts.map +1 -0
  110. package/dist/services/ChainValidator.js +570 -0
  111. package/dist/services/CrossFileHealer.d.ts +6 -0
  112. package/dist/services/CrossFileHealer.d.ts.map +1 -0
  113. package/dist/services/CrossFileHealer.js +135 -0
  114. package/dist/services/DecisionEngine.d.ts +51 -0
  115. package/dist/services/DecisionEngine.d.ts.map +1 -0
  116. package/dist/services/DecisionEngine.js +260 -0
  117. package/dist/services/DefinitionTracer.d.ts +41 -0
  118. package/dist/services/DefinitionTracer.d.ts.map +1 -0
  119. package/dist/services/DefinitionTracer.js +366 -0
  120. package/dist/services/DnaEditorService.d.ts +31 -0
  121. package/dist/services/DnaEditorService.d.ts.map +1 -0
  122. package/dist/services/DnaEditorService.js +217 -0
  123. package/dist/services/DnaIndexService.d.ts +24 -0
  124. package/dist/services/DnaIndexService.d.ts.map +1 -0
  125. package/dist/services/DnaIndexService.js +131 -0
  126. package/dist/services/HealBudget.d.ts +32 -0
  127. package/dist/services/HealBudget.d.ts.map +1 -0
  128. package/dist/services/HealBudget.js +85 -0
  129. package/dist/services/HealReportService.d.ts +161 -0
  130. package/dist/services/HealReportService.d.ts.map +1 -0
  131. package/dist/services/HealReportService.js +1039 -0
  132. package/dist/services/HealingAdvisory.d.ts +30 -0
  133. package/dist/services/HealingAdvisory.d.ts.map +1 -0
  134. package/dist/services/HealingAdvisory.js +51 -0
  135. package/dist/services/HealingCacheService.d.ts +135 -0
  136. package/dist/services/HealingCacheService.d.ts.map +1 -0
  137. package/dist/services/HealingCacheService.js +404 -0
  138. package/dist/services/HealthReportService.d.ts +10 -0
  139. package/dist/services/HealthReportService.d.ts.map +1 -0
  140. package/dist/services/HealthReportService.js +84 -0
  141. package/dist/services/InitializerUpdater.d.ts +16 -0
  142. package/dist/services/InitializerUpdater.d.ts.map +1 -0
  143. package/dist/services/InitializerUpdater.js +73 -0
  144. package/dist/services/IntentAuditor.d.ts +56 -0
  145. package/dist/services/IntentAuditor.d.ts.map +1 -0
  146. package/dist/services/IntentAuditor.js +326 -0
  147. package/dist/services/InteractiveReview.d.ts +15 -0
  148. package/dist/services/InteractiveReview.d.ts.map +1 -0
  149. package/dist/services/InteractiveReview.js +128 -0
  150. package/dist/services/LLMService.d.ts +121 -0
  151. package/dist/services/LLMService.d.ts.map +1 -0
  152. package/dist/services/LLMService.js +475 -0
  153. package/dist/services/MutationApplier.d.ts +69 -0
  154. package/dist/services/MutationApplier.d.ts.map +1 -0
  155. package/dist/services/MutationApplier.js +360 -0
  156. package/dist/services/PRAutomationService.d.ts +50 -0
  157. package/dist/services/PRAutomationService.d.ts.map +1 -0
  158. package/dist/services/PRAutomationService.js +760 -0
  159. package/dist/services/PendingPromptLedger.d.ts +51 -0
  160. package/dist/services/PendingPromptLedger.d.ts.map +1 -0
  161. package/dist/services/PendingPromptLedger.js +205 -0
  162. package/dist/services/ReportGenerator.d.ts +156 -0
  163. package/dist/services/ReportGenerator.d.ts.map +1 -0
  164. package/dist/services/ReportGenerator.js +278 -0
  165. package/dist/services/ReportMergeService.d.ts +95 -0
  166. package/dist/services/ReportMergeService.d.ts.map +1 -0
  167. package/dist/services/ReportMergeService.js +0 -0
  168. package/dist/services/SafetyGuard.d.ts +112 -0
  169. package/dist/services/SafetyGuard.d.ts.map +1 -0
  170. package/dist/services/SafetyGuard.js +87 -0
  171. package/dist/services/Sanitizer.d.ts +28 -0
  172. package/dist/services/Sanitizer.d.ts.map +1 -0
  173. package/dist/services/Sanitizer.js +142 -0
  174. package/dist/services/SanitizingAnthropic.d.ts +19 -0
  175. package/dist/services/SanitizingAnthropic.d.ts.map +1 -0
  176. package/dist/services/SanitizingAnthropic.js +84 -0
  177. package/dist/services/SelectorSanitizer.d.ts +52 -0
  178. package/dist/services/SelectorSanitizer.d.ts.map +1 -0
  179. package/dist/services/SelectorSanitizer.js +318 -0
  180. package/dist/services/SnapshotService.d.ts +11 -0
  181. package/dist/services/SnapshotService.d.ts.map +1 -0
  182. package/dist/services/SnapshotService.js +352 -0
  183. package/dist/services/SourceLinkService.d.ts +26 -0
  184. package/dist/services/SourceLinkService.d.ts.map +1 -0
  185. package/dist/services/SourceLinkService.js +165 -0
  186. package/dist/services/SourceUpdater.d.ts +45 -0
  187. package/dist/services/SourceUpdater.d.ts.map +1 -0
  188. package/dist/services/SourceUpdater.js +1091 -0
  189. package/dist/services/TemplateDiffService.d.ts +25 -0
  190. package/dist/services/TemplateDiffService.d.ts.map +1 -0
  191. package/dist/services/TemplateDiffService.js +334 -0
  192. package/dist/services/TraceBackEngine.d.ts +67 -0
  193. package/dist/services/TraceBackEngine.d.ts.map +1 -0
  194. package/dist/services/TraceBackEngine.js +672 -0
  195. package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
  196. package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
  197. package/dist/services/WorkspaceSnapshotService.js +200 -0
  198. package/dist/services/safety/PipelineContext.d.ts +63 -0
  199. package/dist/services/safety/PipelineContext.d.ts.map +1 -0
  200. package/dist/services/safety/PipelineContext.js +34 -0
  201. package/dist/services/safety/roleClassifier.d.ts +4 -0
  202. package/dist/services/safety/roleClassifier.d.ts.map +1 -0
  203. package/dist/services/safety/roleClassifier.js +233 -0
  204. package/dist/services/safety/rules/ConfidenceTierRule.d.ts +3 -0
  205. package/dist/services/safety/rules/ConfidenceTierRule.d.ts.map +1 -0
  206. package/dist/services/safety/rules/ConfidenceTierRule.js +32 -0
  207. package/dist/services/safety/rules/DangerousSinkRule.d.ts +10 -0
  208. package/dist/services/safety/rules/DangerousSinkRule.d.ts.map +1 -0
  209. package/dist/services/safety/rules/DangerousSinkRule.js +53 -0
  210. package/dist/services/safety/rules/FunctionalRoleRule.d.ts +3 -0
  211. package/dist/services/safety/rules/FunctionalRoleRule.d.ts.map +1 -0
  212. package/dist/services/safety/rules/FunctionalRoleRule.js +64 -0
  213. package/dist/services/safety/rules/HardConfidenceRule.d.ts +3 -0
  214. package/dist/services/safety/rules/HardConfidenceRule.d.ts.map +1 -0
  215. package/dist/services/safety/rules/HardConfidenceRule.js +22 -0
  216. package/dist/services/safety/rules/IntentAuditorRule.d.ts +3 -0
  217. package/dist/services/safety/rules/IntentAuditorRule.d.ts.map +1 -0
  218. package/dist/services/safety/rules/IntentAuditorRule.js +97 -0
  219. package/dist/services/safety/rules/SemanticOppositeRule.d.ts +3 -0
  220. package/dist/services/safety/rules/SemanticOppositeRule.d.ts.map +1 -0
  221. package/dist/services/safety/rules/SemanticOppositeRule.js +30 -0
  222. package/dist/services/safety/rules/StructuralOnlyRule.d.ts +3 -0
  223. package/dist/services/safety/rules/StructuralOnlyRule.d.ts.map +1 -0
  224. package/dist/services/safety/rules/StructuralOnlyRule.js +38 -0
  225. package/dist/services/safety/rules/VisibilityRule.d.ts +3 -0
  226. package/dist/services/safety/rules/VisibilityRule.d.ts.map +1 -0
  227. package/dist/services/safety/rules/VisibilityRule.js +53 -0
  228. package/dist/services/safety/rules/ZeroTrustRule.d.ts +3 -0
  229. package/dist/services/safety/rules/ZeroTrustRule.d.ts.map +1 -0
  230. package/dist/services/safety/rules/ZeroTrustRule.js +37 -0
  231. package/dist/services/safety/rules/index.d.ts +3 -0
  232. package/dist/services/safety/rules/index.d.ts.map +1 -0
  233. package/dist/services/safety/rules/index.js +34 -0
  234. package/dist/services/types.d.ts +59 -0
  235. package/dist/services/types.d.ts.map +1 -0
  236. package/dist/services/types.js +2 -0
  237. package/dist/storage/SnapshotManager.d.ts +5 -0
  238. package/dist/storage/SnapshotManager.d.ts.map +1 -0
  239. package/dist/storage/SnapshotManager.js +32 -0
  240. package/dist/types/index.d.ts +95 -0
  241. package/dist/types/index.d.ts.map +1 -0
  242. package/dist/types/index.js +7 -0
  243. package/dist/utils/DOMUtils.d.ts +49 -0
  244. package/dist/utils/DOMUtils.d.ts.map +1 -0
  245. package/dist/utils/DOMUtils.js +466 -0
  246. package/dist/utils/FileLock.d.ts +44 -0
  247. package/dist/utils/FileLock.d.ts.map +1 -0
  248. package/dist/utils/FileLock.js +189 -0
  249. package/dist/utils/IsolatedDiff.d.ts +45 -0
  250. package/dist/utils/IsolatedDiff.d.ts.map +1 -0
  251. package/dist/utils/IsolatedDiff.js +382 -0
  252. package/dist/utils/StackUtils.d.ts +11 -0
  253. package/dist/utils/StackUtils.d.ts.map +1 -0
  254. package/dist/utils/StackUtils.js +120 -0
  255. package/dist/utils/SummaryTable.d.ts +17 -0
  256. package/dist/utils/SummaryTable.d.ts.map +1 -0
  257. package/dist/utils/SummaryTable.js +37 -0
  258. package/dist/utils/atomicWrite.d.ts +9 -0
  259. package/dist/utils/atomicWrite.d.ts.map +1 -0
  260. package/dist/utils/atomicWrite.js +83 -0
  261. package/dist/utils/logger.d.ts +59 -0
  262. package/dist/utils/logger.d.ts.map +1 -0
  263. package/dist/utils/logger.js +123 -0
  264. package/dist/vendor/enquirer.d.ts +33 -0
  265. package/dist/vendor/enquirer.d.ts.map +1 -0
  266. package/dist/vendor/enquirer.js +11 -0
  267. package/package.json +71 -0
@@ -0,0 +1,3514 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ASTSourceUpdater = void 0;
37
+ const path = __importStar(require("path"));
38
+ const fs = __importStar(require("fs"));
39
+ const ts_morph_1 = require("ts-morph");
40
+ const DryRunGuard_1 = require("../config/DryRunGuard");
41
+ const PathScopeGuard_1 = require("../config/PathScopeGuard");
42
+ const WorkspaceSnapshotService_1 = require("./WorkspaceSnapshotService");
43
+ const SelaError_1 = require("../errors/SelaError");
44
+ const HealingRegistry_1 = require("../engine/HealingRegistry");
45
+ const ArgumentTypeAnalyzer_1 = require("./ArgumentTypeAnalyzer");
46
+ const BlastRadiusAnalyzer_1 = require("./BlastRadiusAnalyzer");
47
+ const DefinitionTracer_1 = require("./DefinitionTracer");
48
+ const TemplateDiffService_1 = require("./TemplateDiffService");
49
+ const InitializerUpdater_1 = require("./InitializerUpdater");
50
+ const CrossFileHealer_1 = require("./CrossFileHealer");
51
+ const HealingAdvisory_1 = require("./HealingAdvisory");
52
+ const ChainValidator_1 = require("./ChainValidator");
53
+ const TraceBackEngine_1 = require("./TraceBackEngine");
54
+ const DecisionEngine_1 = require("./DecisionEngine");
55
+ const MutationApplier_1 = require("./MutationApplier");
56
+ const SelectorSanitizer_1 = require("./SelectorSanitizer");
57
+ const logger_1 = require("../utils/logger");
58
+ // ═══════════════════════════════════════════════════════════════════
59
+ // FIX #1 - normalizeSelector utility
60
+ // ═══════════════════════════════════════════════════════════════════
61
+ function normalizeSelector(s) {
62
+ return s
63
+ .replace(/['"``]/g, "")
64
+ .replace(/\s*>>\s*internal:control=[^\s>]*/g, "")
65
+ .replace(/\s+/g, "")
66
+ .toLowerCase()
67
+ .trim();
68
+ }
69
+ // ═══════════════════════════════════════════════════════════════════
70
+ // SCOPE DETECTION ← THE CORE FIX
71
+ //
72
+ // Determines whether a CallExpression node lives inside the
73
+ // VariableDeclaration initializer of a named variable.
74
+ //
75
+ // Returns the VariableDeclaration if the call is inside the initializer
76
+ // of "varName", or null otherwise.
77
+ //
78
+ // Why this matters:
79
+ // const shadowBtn = shadowBtn.getByRole(…) ← CIRCULAR - must be caught
80
+ // const shadowBtn = page.getByRole(…) ← CORRECT
81
+ //
82
+ // We walk UP the ancestor chain from the call node looking for a
83
+ // VariableDeclaration. If we find one whose name matches varName AND
84
+ // the call is inside its initializer subtree, we return it.
85
+ // ═══════════════════════════════════════════════════════════════════
86
+ function getEnclosingVariableDeclaration(node) {
87
+ let current = node.getParent();
88
+ while (current) {
89
+ if (ts_morph_1.Node.isVariableDeclaration(current)) {
90
+ return current;
91
+ }
92
+ // Stop climbing past statement boundaries - we only want the immediate
93
+ // enclosing declaration, not a declaration somewhere further up the tree.
94
+ if (ts_morph_1.Node.isBlock(current) ||
95
+ ts_morph_1.Node.isSourceFile(current) ||
96
+ ts_morph_1.Node.isArrowFunction(current) ||
97
+ ts_morph_1.Node.isFunctionDeclaration(current) ||
98
+ ts_morph_1.Node.isFunctionExpression(current)) {
99
+ return null;
100
+ }
101
+ current = current.getParent();
102
+ }
103
+ return null;
104
+ }
105
+ /**
106
+ * Returns true when `callNode` is inside the initializer of the variable
107
+ * named `varName`. This is the exact condition that would produce a
108
+ * circular reference if we used `varName` as the receiver.
109
+ */
110
+ function isInsideOwnInitializer(callNode, varName) {
111
+ const enclosingDecl = getEnclosingVariableDeclaration(callNode);
112
+ if (!enclosingDecl)
113
+ return false;
114
+ return enclosingDecl.getName() === varName;
115
+ }
116
+ /**
117
+ * Given a forced receiver (e.g. "shadowBtn") and the call node being
118
+ * replaced, decide the safe receiver to use:
119
+ *
120
+ * • If the call is inside shadowBtn's own initializer → use rootReceiver
121
+ * (typically "page") to avoid circular reference.
122
+ * • Otherwise → use the forced receiver as-is.
123
+ *
124
+ * `rootReceiver` is the leftmost identifier in the original chain
125
+ * (resolved by climbToChainTop), which is always a stable root like "page".
126
+ */
127
+ function resolveSafeReceiver(proposedReceiver, rootReceiver, callNode) {
128
+ if (isInsideOwnInitializer(callNode, proposedReceiver)) {
129
+ logger_1.logger.debug(`Circular reference prevented: ` +
130
+ `"${proposedReceiver}" is being initialized - ` +
131
+ `falling back to root receiver "${rootReceiver}"`);
132
+ return rootReceiver;
133
+ }
134
+ return proposedReceiver;
135
+ }
136
+ function parseInternalSelector(selector) {
137
+ const normalized = selector.replace(/\s*i\s*$/, "").trim();
138
+ const roleMatch = normalized.match(/^internal:role=([a-zA-Z]+)(?:\[name="([^"]+)"(?:i)?\])?/);
139
+ if (roleMatch) {
140
+ const result = {
141
+ method: "getByRole",
142
+ primaryArg: roleMatch[1],
143
+ isSemantic: true,
144
+ };
145
+ if (roleMatch[2])
146
+ result.options = { name: roleMatch[2] };
147
+ return result;
148
+ }
149
+ const labelMatch = normalized.match(/^internal:label="([^"]+)"$/);
150
+ if (labelMatch) {
151
+ return {
152
+ method: "getByLabel",
153
+ primaryArg: labelMatch[1],
154
+ isSemantic: true,
155
+ };
156
+ }
157
+ const labelNoQuote = normalized.match(/^internal:label=([^[]+)$/);
158
+ if (labelNoQuote) {
159
+ return {
160
+ method: "getByLabel",
161
+ primaryArg: labelNoQuote[1].trim(),
162
+ isSemantic: true,
163
+ };
164
+ }
165
+ const textMatch = normalized.match(/^internal:text="([^"]+)"(i)?$/);
166
+ if (textMatch) {
167
+ const result = {
168
+ method: "getByText",
169
+ primaryArg: textMatch[1],
170
+ isSemantic: true,
171
+ };
172
+ if (!textMatch[2])
173
+ result.options = { exact: true };
174
+ return result;
175
+ }
176
+ const placeholderMatch = normalized.match(/^internal:attr=\[placeholder="([^"]+)"\]$/);
177
+ if (placeholderMatch) {
178
+ return {
179
+ method: "getByPlaceholder",
180
+ primaryArg: placeholderMatch[1],
181
+ isSemantic: true,
182
+ };
183
+ }
184
+ const altMatch = normalized.match(/^internal:attr=\[alt="([^"]+)"\]$/);
185
+ if (altMatch) {
186
+ return {
187
+ method: "getByAltText",
188
+ primaryArg: altMatch[1],
189
+ isSemantic: true,
190
+ };
191
+ }
192
+ const titleMatch = normalized.match(/^internal:attr=\[title="([^"]+)"\]$/);
193
+ if (titleMatch) {
194
+ return {
195
+ method: "getByTitle",
196
+ primaryArg: titleMatch[1],
197
+ isSemantic: true,
198
+ };
199
+ }
200
+ const testIdMatch = normalized.match(/^\[data-testid="([^"]+)"\]$/);
201
+ if (testIdMatch) {
202
+ return {
203
+ method: "getByTestId",
204
+ primaryArg: testIdMatch[1],
205
+ isSemantic: true,
206
+ };
207
+ }
208
+ return { method: "locator", primaryArg: selector, isSemantic: false };
209
+ }
210
+ function buildCallText(receiver, sem) {
211
+ const safeArg = sem.primaryArg.replace(/"/g, "'");
212
+ switch (sem.method) {
213
+ case "getByRole":
214
+ if (sem.options?.name) {
215
+ return `${receiver}.getByRole("${safeArg}", { name: "${sem.options.name}" })`;
216
+ }
217
+ return `${receiver}.getByRole("${safeArg}")`;
218
+ case "getByLabel":
219
+ return `${receiver}.getByLabel("${safeArg}")`;
220
+ case "getByPlaceholder":
221
+ return `${receiver}.getByPlaceholder("${safeArg}")`;
222
+ case "getByTestId":
223
+ return `${receiver}.getByTestId("${safeArg}")`;
224
+ case "getByText":
225
+ if (sem.options?.exact)
226
+ return `${receiver}.getByText("${safeArg}", { exact: true })`;
227
+ return `${receiver}.getByText("${safeArg}")`;
228
+ case "getByAltText":
229
+ return `${receiver}.getByAltText("${safeArg}")`;
230
+ case "getByTitle":
231
+ return `${receiver}.getByTitle("${safeArg}")`;
232
+ case "locator":
233
+ default:
234
+ return `${receiver}.locator("${sem.primaryArg.replace(/"/g, "'")}")`;
235
+ }
236
+ }
237
+ function extractSemanticFromCall(callExpr) {
238
+ const expr = callExpr.getExpression();
239
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
240
+ return null;
241
+ const method = expr.getName();
242
+ if (!ALL_SELECTOR_METHODS.has(method))
243
+ return null;
244
+ const args = callExpr.getArguments();
245
+ if (args.length === 0)
246
+ return null;
247
+ const firstArg = args[0];
248
+ let primaryArg = null;
249
+ if (ts_morph_1.Node.isStringLiteral(firstArg)) {
250
+ primaryArg = firstArg.getLiteralValue();
251
+ }
252
+ else if (ts_morph_1.Node.isNoSubstitutionTemplateLiteral(firstArg)) {
253
+ primaryArg = firstArg.getLiteralValue();
254
+ }
255
+ if (primaryArg === null)
256
+ return null;
257
+ const options = {};
258
+ if (args.length >= 2 && ts_morph_1.Node.isObjectLiteralExpression(args[1])) {
259
+ for (const prop of args[1].getProperties()) {
260
+ if (ts_morph_1.Node.isPropertyAssignment(prop)) {
261
+ const key = prop.getName();
262
+ const val = prop.getInitializer();
263
+ if (val && ts_morph_1.Node.isStringLiteral(val))
264
+ options[key] = val.getLiteralValue();
265
+ else if (val && ts_morph_1.Node.isTrueLiteral(val))
266
+ options[key] = true;
267
+ else if (val && ts_morph_1.Node.isFalseLiteral(val))
268
+ options[key] = false;
269
+ }
270
+ }
271
+ }
272
+ const isSemantic = method !== "locator" && method !== "frameLocator";
273
+ return {
274
+ method,
275
+ primaryArg,
276
+ options: Object.keys(options).length > 0 ? options : undefined,
277
+ isSemantic,
278
+ };
279
+ }
280
+ function semanticMatch(callSemantic, oldSelector, allowRelated = false) {
281
+ const leaf = oldSelector.split(">>").pop().trim();
282
+ const normalizedLeaf = leaf.replace(/\s*i\s*$/, "").trim();
283
+ const oldSem = parseInternalSelector(normalizedLeaf);
284
+ if (callSemantic.method === "getByRole" && oldSem.method === "getByRole") {
285
+ if (!isRoleCompatible(callSemantic.primaryArg, oldSem.primaryArg))
286
+ return false;
287
+ }
288
+ if (callSemantic.method === oldSem.method &&
289
+ callSemantic.primaryArg === oldSem.primaryArg) {
290
+ return true;
291
+ }
292
+ if (allowRelated) {
293
+ const textualMethods = new Set([
294
+ "getByLabel",
295
+ "getByPlaceholder",
296
+ "getByText",
297
+ "getByAltText",
298
+ "getByTitle",
299
+ ]);
300
+ if (textualMethods.has(callSemantic.method) &&
301
+ textualMethods.has(oldSem.method)) {
302
+ const a = callSemantic.primaryArg.toLowerCase();
303
+ const b = oldSem.primaryArg.toLowerCase();
304
+ if (a === b || a.includes(b) || b.includes(a))
305
+ return true;
306
+ }
307
+ }
308
+ if (oldSem.isSemantic && callSemantic.isSemantic) {
309
+ const oldLeafArg = oldSem.primaryArg.toLowerCase();
310
+ const callArg = callSemantic.primaryArg.toLowerCase();
311
+ if (oldLeafArg === callArg)
312
+ return true;
313
+ if (callArg.includes(oldLeafArg) || oldLeafArg.includes(callArg))
314
+ return true;
315
+ }
316
+ if (allowRelated) {
317
+ const textualMethods = new Set([
318
+ "getByLabel",
319
+ "getByPlaceholder",
320
+ "getByText",
321
+ "getByAltText",
322
+ "getByTitle",
323
+ ]);
324
+ if (textualMethods.has(callSemantic.method) &&
325
+ textualMethods.has(oldSem.method) &&
326
+ callSemantic.method !== oldSem.method) {
327
+ const lower_a = callSemantic.primaryArg.toLowerCase();
328
+ const lower_b = oldSem.primaryArg.toLowerCase();
329
+ const CONCEPT_DICTIONARY = new Map([
330
+ ["email", new Set(["email", "e-mail", "mail", "inbox", "@"])],
331
+ [
332
+ "password",
333
+ new Set(["password", "passwd", "passcode", "pin", "secret"]),
334
+ ],
335
+ [
336
+ "username",
337
+ new Set(["username", "user name", "login", "account name", "handle"]),
338
+ ],
339
+ ["phone", new Set(["phone", "mobile", "cell", "tel", "telephone"])],
340
+ ["address", new Set(["address", "street", "city", "zip", "postal"])],
341
+ ["name", new Set(["first name", "last name", "full name", "surname"])],
342
+ ["search", new Set(["search", "query", "find", "filter", "keyword"])],
343
+ [
344
+ "submit",
345
+ new Set(["submit", "send", "confirm", "execute", "proceed", "save"]),
346
+ ],
347
+ ["cancel", new Set(["cancel", "dismiss", "close", "abort", "back"])],
348
+ ["date", new Set(["date", "day", "month", "year", "time"])],
349
+ ]);
350
+ for (const [, keywords] of CONCEPT_DICTIONARY) {
351
+ let aMatch = false, bMatch = false;
352
+ for (const kw of keywords) {
353
+ if (lower_a.includes(kw))
354
+ aMatch = true;
355
+ if (lower_b.includes(kw))
356
+ bMatch = true;
357
+ }
358
+ if (aMatch && bMatch)
359
+ return true;
360
+ }
361
+ }
362
+ }
363
+ const oldLeaf = oldSelector.split(">>").pop().trim();
364
+ const oldLeafSem = parseInternalSelector(oldLeaf);
365
+ if (callSemantic.method === oldLeafSem.method &&
366
+ callSemantic.primaryArg === oldLeafSem.primaryArg) {
367
+ return true;
368
+ }
369
+ return false;
370
+ }
371
+ function absoluteNodeMatch(callExpr, oldSelector) {
372
+ const callSem = extractSemanticFromCall(callExpr);
373
+ if (!callSem)
374
+ return false;
375
+ const leaf = oldSelector.split(">>").pop().trim();
376
+ const normalizedLeaf = leaf.replace(/\s*i\s*$/, "").trim();
377
+ const oldSem = parseInternalSelector(normalizedLeaf);
378
+ if (callSem.method === oldSem.method &&
379
+ callSem.primaryArg === oldSem.primaryArg) {
380
+ return true;
381
+ }
382
+ if (callSem.method === oldSem.method && oldSem.method !== "locator") {
383
+ const a = normalize(callSem.primaryArg);
384
+ const b = normalize(oldSem.primaryArg);
385
+ if (a === b || a.includes(b) || b.includes(a))
386
+ return true;
387
+ }
388
+ if (callSem.method !== "locator" &&
389
+ callSem.method !== "frameLocator" &&
390
+ oldSem.method !== "locator" &&
391
+ oldSem.method !== "frameLocator") {
392
+ const a = normalize(callSem.primaryArg);
393
+ const b = normalize(oldSem.primaryArg);
394
+ const minLen = Math.min(a.length, b.length);
395
+ if (minLen >= 4 && (a.includes(b) || b.includes(a)))
396
+ return true;
397
+ }
398
+ if (oldSem.method === "locator" && callSem.method !== "locator") {
399
+ const rawValueMatch = normalizedLeaf.match(/"([^"]{3,})"/);
400
+ if (rawValueMatch) {
401
+ const rawValue = normalize(rawValueMatch[1]);
402
+ const astArg = normalize(callSem.primaryArg);
403
+ if (astArg.includes(rawValue) || rawValue.includes(astArg))
404
+ return true;
405
+ }
406
+ }
407
+ return false;
408
+ }
409
+ function normalize(s) {
410
+ return s
411
+ .toLowerCase()
412
+ .replace(/^[-_\s]+|[-_\s]+$/g, "")
413
+ .trim();
414
+ }
415
+ function isRoleCompatible(roleA, roleB) {
416
+ const interactive = new Set([
417
+ "button",
418
+ "textbox",
419
+ "checkbox",
420
+ "radio",
421
+ "combobox",
422
+ "listbox",
423
+ "link",
424
+ "menuitem",
425
+ ]);
426
+ const structural = new Set([
427
+ "heading",
428
+ "banner",
429
+ "main",
430
+ "navigation",
431
+ "complementary",
432
+ "contentinfo",
433
+ ]);
434
+ if (interactive.has(roleA.toLowerCase()) &&
435
+ structural.has(roleB.toLowerCase()))
436
+ return false;
437
+ if (structural.has(roleA.toLowerCase()) &&
438
+ interactive.has(roleB.toLowerCase()))
439
+ return false;
440
+ return true;
441
+ }
442
+ // ═══════════════════════════════════════════════════════════════════
443
+ // METHOD SETS
444
+ // ═══════════════════════════════════════════════════════════════════
445
+ const SEMANTIC_METHODS = new Set([
446
+ "getByRole",
447
+ "getByLabel",
448
+ "getByText",
449
+ "getByTestId",
450
+ "getByPlaceholder",
451
+ "getByAltText",
452
+ "getByTitle",
453
+ ]);
454
+ const ALL_SELECTOR_METHODS = new Set([
455
+ "locator",
456
+ "frameLocator",
457
+ ...SEMANTIC_METHODS,
458
+ ]);
459
+ const CHAIN_CONTINUATION_METHODS = new Set([
460
+ ...ALL_SELECTOR_METHODS,
461
+ "filter",
462
+ "first",
463
+ "last",
464
+ "nth",
465
+ ]);
466
+ // ═══════════════════════════════════════════════════════════════════
467
+ // MethodMapper
468
+ // ═══════════════════════════════════════════════════════════════════
469
+ class MethodMapper {
470
+ static build(receiver, seg) {
471
+ const q = (s) => `"${s.replace(/"/g, "'")}"`;
472
+ switch (seg.type) {
473
+ case "locator": {
474
+ const opts = MethodMapper.buildLocatorOptions(seg);
475
+ return opts
476
+ ? `${receiver}.locator(${q(seg.selector)}, { ${opts} })`
477
+ : `${receiver}.locator(${q(seg.selector)})`;
478
+ }
479
+ case "frameLocator":
480
+ return `${receiver}.frameLocator(${q(seg.selector)})`;
481
+ case "getByRole": {
482
+ const opts = [];
483
+ if (seg.name !== undefined && seg.name !== "")
484
+ opts.push(`name: ${q(seg.name)}`);
485
+ if (seg.exact !== undefined)
486
+ opts.push(`exact: ${seg.exact}`);
487
+ if (seg.level !== undefined)
488
+ opts.push(`level: ${seg.level}`);
489
+ if (seg.checked !== undefined)
490
+ opts.push(`checked: ${seg.checked}`);
491
+ if (seg.pressed !== undefined)
492
+ opts.push(`pressed: ${seg.pressed}`);
493
+ if (seg.expanded !== undefined)
494
+ opts.push(`expanded: ${seg.expanded}`);
495
+ if (seg.selected !== undefined)
496
+ opts.push(`selected: ${seg.selected}`);
497
+ if (seg.disabled !== undefined)
498
+ opts.push(`disabled: ${seg.disabled}`);
499
+ const optStr = opts.length > 0 ? `, { ${opts.join(", ")} }` : "";
500
+ return `${receiver}.getByRole(${q(seg.role)}${optStr})`;
501
+ }
502
+ case "getByLabel":
503
+ return `${receiver}.getByLabel(${q(seg.text)}${seg.exact !== undefined ? `, { exact: ${seg.exact} }` : ""})`;
504
+ case "getByText":
505
+ return `${receiver}.getByText(${q(seg.text)}${seg.exact !== undefined ? `, { exact: ${seg.exact} }` : ""})`;
506
+ case "getByPlaceholder":
507
+ return `${receiver}.getByPlaceholder(${q(seg.text)}${seg.exact !== undefined ? `, { exact: ${seg.exact} }` : ""})`;
508
+ case "getByAltText":
509
+ return `${receiver}.getByAltText(${q(seg.text)}${seg.exact !== undefined ? `, { exact: ${seg.exact} }` : ""})`;
510
+ case "getByTitle":
511
+ return `${receiver}.getByTitle(${q(seg.text)}${seg.exact !== undefined ? `, { exact: ${seg.exact} }` : ""})`;
512
+ case "getByTestId":
513
+ return `${receiver}.getByTestId(${q(seg.testId)})`;
514
+ case "filter":
515
+ return `${receiver}.filter(${MethodMapper.buildFilterOptions(seg)})`;
516
+ case "first":
517
+ return `${receiver}.first()`;
518
+ case "last":
519
+ return `${receiver}.last()`;
520
+ case "nth":
521
+ return `${receiver}.nth(${seg.index})`;
522
+ default:
523
+ logger_1.logger.warn(`Unknown segment type: ${seg.type}`);
524
+ return null;
525
+ }
526
+ }
527
+ static buildFilterOptions(seg) {
528
+ const opts = [];
529
+ if (seg.hasText !== undefined) {
530
+ const ht = seg.hasText;
531
+ if (typeof ht === "string")
532
+ opts.push(`hasText: "${ht.replace(/"/g, "'")}"`);
533
+ else if (ht instanceof RegExp)
534
+ opts.push(`hasText: ${ht.toString()}`);
535
+ }
536
+ if (seg.hasNotText !== undefined) {
537
+ const hnt = seg.hasNotText;
538
+ if (typeof hnt === "string")
539
+ opts.push(`hasNotText: "${hnt.replace(/"/g, "'")}"`);
540
+ }
541
+ return opts.length === 0 ? "{}" : `{ ${opts.join(", ")} }`;
542
+ }
543
+ static buildLocatorOptions(seg) {
544
+ const opts = [];
545
+ if (seg.hasText !== undefined) {
546
+ opts.push(`hasText: "${seg.hasText.replace(/"/g, "'")}"`);
547
+ }
548
+ if (seg.hasNotText !== undefined) {
549
+ opts.push(`hasNotText: "${seg.hasNotText.replace(/"/g, "'")}"`);
550
+ }
551
+ return opts.length > 0 ? opts.join(", ") : null;
552
+ }
553
+ static buildChain(receiver, segments, originalText) {
554
+ if (segments.length === 0)
555
+ return null;
556
+ const baseIndent = MethodMapper.detectIndent(originalText);
557
+ const innerIndent = baseIndent + " ";
558
+ const calls = [];
559
+ let currentReceiver = receiver;
560
+ for (const seg of segments) {
561
+ const callText = MethodMapper.build(currentReceiver, seg);
562
+ if (callText === null) {
563
+ logger_1.logger.warn(`Cannot build segment type "${seg.type}" - aborting chain`);
564
+ return null;
565
+ }
566
+ const methodPart = callText.slice(currentReceiver.length);
567
+ calls.push(methodPart);
568
+ currentReceiver = callText;
569
+ }
570
+ if (segments.length >= 3) {
571
+ return [receiver, ...calls.map((c) => `${innerIndent}${c}`)].join("\n");
572
+ }
573
+ return receiver + calls.join("");
574
+ }
575
+ static detectIndent(text) {
576
+ if (!text)
577
+ return "";
578
+ const match = text.match(/^(\s+)/);
579
+ return match ? match[1] : "";
580
+ }
581
+ }
582
+ // ═══════════════════════════════════════════════════════════════════
583
+ // CHAIN DEDUPLICATION & VARIABLE RESOLUTION ENGINE
584
+ // ═══════════════════════════════════════════════════════════════════
585
+ function normalizeSegmentSelector(s) {
586
+ return normalizeSelector(s);
587
+ }
588
+ function segmentsStructurallyEqual(ctx, ai) {
589
+ if (ctx.type !== ai.type)
590
+ return false;
591
+ if ("selector" in ctx && "selector" in ai) {
592
+ const a = normalizeSelector(ctx.selector);
593
+ const b = normalizeSelector(ai.selector);
594
+ return a === b || a.includes(b) || b.includes(a);
595
+ }
596
+ if ("text" in ctx && "text" in ai) {
597
+ const a = normalizeSelector(ctx.text);
598
+ const b = normalizeSelector(ai.text);
599
+ return a === b;
600
+ }
601
+ if ("role" in ctx && "role" in ai) {
602
+ return ctx.role === ai.role;
603
+ }
604
+ if ("testId" in ctx && "testId" in ai) {
605
+ return (normalizeSelector(ctx.testId) ===
606
+ normalizeSelector(ai.testId));
607
+ }
608
+ return false;
609
+ }
610
+ function deduplicateChainSegments(aiSegments, contextSegments) {
611
+ if (contextSegments.length === 0)
612
+ return { deduped: aiSegments, stripped: 0 };
613
+ let overlap = 0;
614
+ const maxCheck = Math.min(contextSegments.length, aiSegments.length);
615
+ for (let i = 0; i < maxCheck; i++) {
616
+ if (segmentsStructurallyEqual(contextSegments[i], aiSegments[i])) {
617
+ overlap++;
618
+ }
619
+ else {
620
+ break;
621
+ }
622
+ }
623
+ if (overlap === 0)
624
+ return { deduped: aiSegments, stripped: 0 };
625
+ logger_1.logger.debug(`Stripping ${overlap}/${contextSegments.length} duplicate leading ` +
626
+ `segment(s) - context already encodes them`);
627
+ return { deduped: aiSegments.slice(overlap), stripped: overlap };
628
+ }
629
+ const PAGE_ROOT_NAMES = new Set([
630
+ "page",
631
+ "context",
632
+ "browser",
633
+ "playwright",
634
+ "request",
635
+ ]);
636
+ function semToSegment(sem) {
637
+ switch (sem.method) {
638
+ case "frameLocator":
639
+ return { type: "frameLocator", selector: sem.primaryArg };
640
+ case "locator":
641
+ return { type: "locator", selector: sem.primaryArg };
642
+ case "getByText":
643
+ return {
644
+ type: "getByText",
645
+ text: sem.primaryArg,
646
+ exact: sem.options?.exact,
647
+ };
648
+ case "getByLabel":
649
+ return {
650
+ type: "getByLabel",
651
+ text: sem.primaryArg,
652
+ exact: sem.options?.exact,
653
+ };
654
+ case "getByPlaceholder":
655
+ return { type: "getByPlaceholder", text: sem.primaryArg };
656
+ case "getByRole":
657
+ return {
658
+ type: "getByRole",
659
+ role: sem.primaryArg,
660
+ name: sem.options?.name,
661
+ };
662
+ case "getByTestId":
663
+ return { type: "getByTestId", testId: sem.primaryArg };
664
+ case "getByAltText":
665
+ return { type: "getByAltText", text: sem.primaryArg };
666
+ case "getByTitle":
667
+ return { type: "getByTitle", text: sem.primaryArg };
668
+ default:
669
+ return null;
670
+ }
671
+ }
672
+ function resolveVariableToSegments(sourceFile, varName, beforeLine, depth = 0) {
673
+ const empty = {
674
+ segments: [],
675
+ rootName: varName,
676
+ varName,
677
+ bestReceiverVar: null,
678
+ };
679
+ if (depth > 10)
680
+ return empty;
681
+ if (PAGE_ROOT_NAMES.has(varName)) {
682
+ return { segments: [], rootName: varName, varName, bestReceiverVar: null };
683
+ }
684
+ const decl = jumpToDefinitionInFile(sourceFile, varName, beforeLine + 5);
685
+ if (!decl)
686
+ return empty;
687
+ const init = decl.getInitializer();
688
+ if (!init || !ts_morph_1.Node.isCallExpression(init))
689
+ return empty;
690
+ const calls = collectChainCalls(init);
691
+ calls.reverse();
692
+ const segments = [];
693
+ let rootName = varName;
694
+ let receiverVarForRecursion = null;
695
+ for (let ci = 0; ci < calls.length; ci++) {
696
+ const call = calls[ci];
697
+ const sem = extractSemanticFromCall(call);
698
+ if (!sem)
699
+ continue;
700
+ if (ci === 0) {
701
+ const expr = call.getExpression();
702
+ const receiverNode = ts_morph_1.Node.isPropertyAccessExpression(expr)
703
+ ? expr.getExpression()
704
+ : null;
705
+ const receiverName = receiverNode
706
+ ?.getText()
707
+ .replace(/^await\s+/, "")
708
+ .trim() ?? null;
709
+ if (receiverName && !PAGE_ROOT_NAMES.has(receiverName)) {
710
+ receiverVarForRecursion = receiverName;
711
+ const parentRes = resolveVariableToSegments(sourceFile, receiverName, decl.getStartLineNumber(), depth + 1);
712
+ segments.push(...parentRes.segments);
713
+ rootName = parentRes.rootName;
714
+ }
715
+ else if (receiverName) {
716
+ rootName = receiverName;
717
+ }
718
+ }
719
+ const seg = semToSegment(sem);
720
+ if (seg)
721
+ segments.push(seg);
722
+ }
723
+ logger_1.logger.debug(`Variable: ${varName}, Resolved Segments: ${JSON.stringify(segments)}`);
724
+ logger_1.logger.debug(`'${varName}' → root:'${rootName}' ` +
725
+ `via:'${receiverVarForRecursion ?? "direct"}' ` +
726
+ `segs:[${segments.map((s) => s.type).join(",")}]`);
727
+ return {
728
+ segments,
729
+ rootName,
730
+ varName,
731
+ bestReceiverVar: receiverVarForRecursion,
732
+ };
733
+ }
734
+ // ═══════════════════════════════════════════════════════════════════
735
+ // SHARED PERSISTENCE HOOK
736
+ //
737
+ // Module-level write gate shared between `ASTSourceUpdater` and the
738
+ // internal `NodeTransformationStrategy`. Centralises three concerns
739
+ // every `saveSync` call site needs:
740
+ // 1. `DryRunGuard.active()` short-circuit - keeps the mutation in
741
+ // memory but suppresses the on-disk write under SELA_DRY_RUN.
742
+ // 2. `sharedWorkspaceSnapshot.preWrite()` - captures `contentBefore`
743
+ // so the Clean-Room ledger / Local DX rollback can rewind.
744
+ // 3. `sharedWorkspaceSnapshot.postWrite()` - refreshes `contentAfter`
745
+ // so `getMutatedSnapshots()` (driving the pending-prompts ledger
746
+ // and the interactive CLI prompt) sees the new disk state.
747
+ //
748
+ // Strategies do NOT receive the persistence service via DI - they call
749
+ // this helper directly. Same singleton, zero coupling, one code path.
750
+ // ═══════════════════════════════════════════════════════════════════
751
+ function persistSourceFile(sourceFile) {
752
+ // Safe path scoping (T1): block mutations of files outside allowedPaths.
753
+ if (!PathScopeGuard_1.PathScopeGuard.enforce(sourceFile.getFilePath())) {
754
+ return;
755
+ }
756
+ if (DryRunGuard_1.DryRunGuard.active()) {
757
+ DryRunGuard_1.DryRunGuard.logSkippedWrite("saveSync", sourceFile.getFilePath());
758
+ return;
759
+ }
760
+ const fp = sourceFile.getFilePath();
761
+ WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.preWrite(fp);
762
+ sourceFile.saveSync();
763
+ WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.postWrite(fp);
764
+ }
765
+ // ═══════════════════════════════════════════════════════════════════
766
+ // NODE TRANSFORMATION STRATEGY - Scope-Aware, Zero-Redundancy
767
+ // ═══════════════════════════════════════════════════════════════════
768
+ class NodeTransformationStrategy {
769
+ chainValidator;
770
+ constructor(chainValidator) {
771
+ this.chainValidator = chainValidator;
772
+ }
773
+ replaceLocatorNode(sourceFile, caller, oldSelector, newSegments, originalChainHint, suspectIdentifier, searchWindow) {
774
+ logger_1.logger.debug(`Absolute node search for: "${oldSelector}"`);
775
+ // ── Phase A: Variable-Aware Strip-and-Force ─────────────────────
776
+ //
777
+ // Two-pass deduplication, now with scope-aware receiver selection.
778
+ // After dedup we defer the actual receiver safety check to Phase E,
779
+ // where we have the concrete call node and can call resolveSafeReceiver().
780
+ let workingSegments = newSegments;
781
+ let forcedReceiver = null;
782
+ // Track the AST root (page / root var) so Phase E can fall back to it
783
+ // if the proposed receiver turns out to be the variable being initialized.
784
+ let resolvedRootReceiver = "page";
785
+ const tryDedupAgainst = (varName, label) => {
786
+ if (!varName || PAGE_ROOT_NAMES.has(varName))
787
+ return null;
788
+ const resolution = resolveVariableToSegments(sourceFile, varName, caller.line);
789
+ if (resolution.segments.length === 0)
790
+ return null;
791
+ logger_1.logger.debug(`(${label}) AI Segments: ${JSON.stringify(workingSegments)} ` +
792
+ `vs Resolved Var Segments for '${varName}': ${JSON.stringify(resolution.segments)}`);
793
+ const { deduped, stripped } = deduplicateChainSegments(workingSegments, resolution.segments);
794
+ if (stripped === 0)
795
+ return null;
796
+ // Determine the best intermediate receiver
797
+ let receiver = varName;
798
+ if (resolution.bestReceiverVar) {
799
+ const intermRes = resolveVariableToSegments(sourceFile, resolution.bestReceiverVar, caller.line);
800
+ const { stripped: interStripped } = deduplicateChainSegments(newSegments, intermRes.segments);
801
+ if (interStripped === stripped) {
802
+ receiver = resolution.bestReceiverVar;
803
+ }
804
+ }
805
+ logger_1.logger.debug(`Strip-and-Force (${label}): stripped=${stripped}, ` +
806
+ `receiver="${receiver}" ` +
807
+ `remaining=[${deduped.map((s) => s.type).join(",")}]`);
808
+ return { receiver, deduped, rootName: resolution.rootName };
809
+ };
810
+ // Pass 1: suspectIdentifier
811
+ if (suspectIdentifier && !PAGE_ROOT_NAMES.has(suspectIdentifier)) {
812
+ const r = tryDedupAgainst(suspectIdentifier, "suspectId");
813
+ if (r) {
814
+ forcedReceiver = r.receiver;
815
+ workingSegments = r.deduped;
816
+ resolvedRootReceiver = r.rootName;
817
+ }
818
+ }
819
+ // Pass 2: AST root receiver (catches cases where astRoot != suspectIdentifier)
820
+ let discoveredAstRoot = null;
821
+ if (!forcedReceiver) {
822
+ discoveredAstRoot = this.discoverAstRootReceiver(sourceFile, oldSelector, searchWindow, suspectIdentifier, caller.line);
823
+ if (discoveredAstRoot && !PAGE_ROOT_NAMES.has(discoveredAstRoot)) {
824
+ const r = tryDedupAgainst(discoveredAstRoot, "astRoot");
825
+ if (r) {
826
+ forcedReceiver = r.receiver;
827
+ workingSegments = r.deduped;
828
+ resolvedRootReceiver = r.rootName;
829
+ }
830
+ }
831
+ }
832
+ // ── Pass 3: Type-Collision Guard ─────────────────────────────────
833
+ // When AI's first segment TYPE matches the receiver variable's defining type
834
+ // but with a different selector, the AI is replacing the receiver context.
835
+ // Strategy: update the receiver variable's definition, then use remaining
836
+ // segments with the receiver var - preserving developer's variable structure.
837
+ if (!forcedReceiver &&
838
+ discoveredAstRoot &&
839
+ !PAGE_ROOT_NAMES.has(discoveredAstRoot)) {
840
+ const typeCheckRes = resolveVariableToSegments(sourceFile, discoveredAstRoot, caller.line);
841
+ if (typeCheckRes.segments.length > 0 && workingSegments.length > 0) {
842
+ const receiverDefType = typeCheckRes.segments[typeCheckRes.segments.length - 1].type;
843
+ const aiLeadType = workingSegments[0].type;
844
+ if (receiverDefType === aiLeadType) {
845
+ const pageRoot = typeCheckRes.rootName || "page";
846
+ const newReceiverSelector = workingSegments[0].selector;
847
+ const remainingSegments = workingSegments.slice(1);
848
+ logger_1.logger.debug(`TypeCollision: "${discoveredAstRoot}" IS ${receiverDefType}, ` +
849
+ `AI leads with ${aiLeadType}`);
850
+ if (newReceiverSelector && remainingSegments.length > 0) {
851
+ const defUpdated = this.updateReceiverDefinition(sourceFile, discoveredAstRoot, receiverDefType, newReceiverSelector, caller.line);
852
+ if (defUpdated) {
853
+ logger_1.logger.debug(`TypeCollision: updated "${discoveredAstRoot}" def → ` +
854
+ `${receiverDefType}("${newReceiverSelector}")`);
855
+ forcedReceiver = discoveredAstRoot;
856
+ workingSegments = remainingSegments;
857
+ resolvedRootReceiver = pageRoot;
858
+ }
859
+ else {
860
+ logger_1.logger.debug(`TypeCollision: def update failed → page root fallback`);
861
+ forcedReceiver = pageRoot;
862
+ resolvedRootReceiver = pageRoot;
863
+ }
864
+ }
865
+ else {
866
+ forcedReceiver = pageRoot;
867
+ resolvedRootReceiver = pageRoot;
868
+ }
869
+ }
870
+ }
871
+ }
872
+ // ── Phase B: Semantic-First Enforcement ─────────────────────────
873
+ workingSegments = workingSegments.map((seg) => {
874
+ if (seg.type !== "locator")
875
+ return seg;
876
+ const m = seg.selector.match(/^:has-text\(['"](.+)['"]\)$/);
877
+ if (m) {
878
+ logger_1.logger.debug(`:has-text → getByText("${m[1]}")`);
879
+ return { type: "getByText", text: m[1], exact: false };
880
+ }
881
+ return seg;
882
+ });
883
+ // ── Phase C: ChainValidator ─────────────────────────────────────
884
+ const validation = this.chainValidator.validate({
885
+ segments: workingSegments,
886
+ originalRootReceiver: forcedReceiver ?? "page",
887
+ proposedRootReceiver: forcedReceiver ?? "page",
888
+ rootType: "unknown",
889
+ applySemanticUpgrade: true,
890
+ });
891
+ if (!validation.valid) {
892
+ logger_1.logger.warn(`ChainValidator rejected: ${validation.reason}`);
893
+ return null;
894
+ }
895
+ const resolvedSegments = (validation.segments ??
896
+ workingSegments);
897
+ // ── Phase D: Find AST candidate nodes ──────────────────────────
898
+ const strictCandidates = this.findCandidateNodes(sourceFile, oldSelector, searchWindow);
899
+ const identityCandidates = suspectIdentifier
900
+ ? this.findCandidatesInDeclaration(sourceFile, suspectIdentifier, oldSelector, caller.line)
901
+ : [];
902
+ const seenNodes = new WeakSet();
903
+ const candidates = [];
904
+ for (const c of strictCandidates) {
905
+ if (!seenNodes.has(c.call)) {
906
+ seenNodes.add(c.call);
907
+ candidates.push({ ...c, identityConfidence: false });
908
+ }
909
+ }
910
+ for (const c of identityCandidates) {
911
+ if (!seenNodes.has(c.call)) {
912
+ seenNodes.add(c.call);
913
+ candidates.push(c);
914
+ }
915
+ }
916
+ if (candidates.length === 0) {
917
+ logger_1.logger.debug(`No absolute matches found`);
918
+ return null;
919
+ }
920
+ candidates.sort((a, b) => {
921
+ if (a.identityConfidence !== b.identityConfidence) {
922
+ return a.identityConfidence ? 1 : -1;
923
+ }
924
+ return (Math.abs(a.call.getStartLineNumber() - caller.line) -
925
+ Math.abs(b.call.getStartLineNumber() - caller.line));
926
+ });
927
+ // ── Phase E: Build + Validate + Write ──────────────────────────
928
+ for (const { call, chainTop, rootReceiver: astRoot, chainDepth, identityConfidence, } of candidates) {
929
+ // ── Identifier/PropertyAccess guard ────────────────────────────
930
+ // NT must never rewrite a call whose first argument is an Identifier
931
+ // or PropertyAccessExpression (e.g. locator(SUBMIT_SELECTOR) or
932
+ // locator(SELS.submit)). Replacing the whole chain would substitute
933
+ // a hardcoded string literal for the reference, which is exactly the
934
+ // definition-site mutation bug the semantic dispatch gate was built
935
+ // to prevent. Skip the candidate; the semantic gate owns this case.
936
+ const firstArg = call.getArguments()[0];
937
+ if (firstArg &&
938
+ (ts_morph_1.Node.isIdentifier(firstArg) ||
939
+ ts_morph_1.Node.isPropertyAccessExpression(firstArg))) {
940
+ logger_1.logger.debug(`skip - first arg is ${firstArg.getKindName()} ` +
941
+ `"${firstArg.getText()}" (semantic dispatch owns definition-site mutations)`);
942
+ continue;
943
+ }
944
+ const callLine = call.getStartLineNumber();
945
+ const originalText = chainTop.getText();
946
+ // ══════════════════════════════════════════════════════════════
947
+ // SCOPE-AWARE RECEIVER RESOLUTION ← THE CORE FIX
948
+ //
949
+ // Before we build code, we must verify that the proposed receiver
950
+ // does not create a circular self-reference.
951
+ //
952
+ // Case A - forcedReceiver is set (strip-and-force path):
953
+ // Check if the chainTop lives inside the initializer of that var.
954
+ // If yes, fall back to the actual AST root (e.g. "page").
955
+ //
956
+ // Case B - no forced receiver:
957
+ // Use astRoot directly - it was walked up from the call node and
958
+ // is always either "page" or a stable variable reference.
959
+ // ══════════════════════════════════════════════════════════════
960
+ let effectiveReceiver;
961
+ if (forcedReceiver) {
962
+ // resolveSafeReceiver checks isInsideOwnInitializer and falls back
963
+ // to resolvedRootReceiver ("page" etc.) when needed.
964
+ effectiveReceiver = resolveSafeReceiver(forcedReceiver, resolvedRootReceiver || astRoot, chainTop);
965
+ }
966
+ else {
967
+ effectiveReceiver = astRoot;
968
+ }
969
+ // ── Additional safety: if effectiveReceiver still matches the variable
970
+ // name being declared (possible through deep var chains), force page.
971
+ if (suspectIdentifier &&
972
+ effectiveReceiver === suspectIdentifier &&
973
+ isInsideOwnInitializer(chainTop, suspectIdentifier)) {
974
+ logger_1.logger.debug(`Secondary circular guard: ` +
975
+ `effectiveReceiver "${effectiveReceiver}" == suspectIdentifier ` +
976
+ `inside own initializer → forcing root "${astRoot}"`);
977
+ effectiveReceiver = astRoot;
978
+ }
979
+ logger_1.logger.debug(`Receiver: forced=${forcedReceiver ?? "none"} ` +
980
+ `ast=${astRoot} → "${effectiveReceiver}" ` +
981
+ `(scopeSafe=${effectiveReceiver !== forcedReceiver ? "fallback" : "ok"})`);
982
+ const hintForPartial = originalChainHint &&
983
+ originalChainHint.length === resolvedSegments.length
984
+ ? originalChainHint
985
+ : undefined;
986
+ if (!identityConfidence && hintForPartial) {
987
+ const partial = this.attemptPartialHeal(sourceFile, call, chainTop, effectiveReceiver, hintForPartial, resolvedSegments, callLine);
988
+ if (partial)
989
+ return partial;
990
+ }
991
+ const newCode = MethodMapper.buildChain(effectiveReceiver, resolvedSegments, originalText);
992
+ if (!newCode) {
993
+ logger_1.logger.warn(`MethodMapper failed for receiver "${effectiveReceiver}"`);
994
+ continue;
995
+ }
996
+ if (!this.validateGeneratedCode(newCode, sourceFile)) {
997
+ logger_1.logger.warn(`Validation failed for:\n${newCode}`);
998
+ continue;
999
+ }
1000
+ const tag = identityConfidence ? "identity" : "strict full replace";
1001
+ logger_1.logger.debug(`${tag} @line ${callLine} receiver="${effectiveReceiver}":\n` +
1002
+ ` ${newCode.split("\n")[0]}`);
1003
+ try {
1004
+ chainTop.replaceWithText(newCode);
1005
+ persistSourceFile(sourceFile);
1006
+ return {
1007
+ success: true,
1008
+ reason: `scope-aware transform [${effectiveReceiver}]: ${resolvedSegments.length} segments`,
1009
+ strategy: identityConfidence ? "NT-identity" : "NT",
1010
+ lineUpdated: callLine - 1,
1011
+ };
1012
+ }
1013
+ catch (e) {
1014
+ logger_1.logger.warn(`replaceWithText failed: ${e.message}`);
1015
+ }
1016
+ }
1017
+ return null;
1018
+ }
1019
+ discoverAstRootReceiver(sourceFile, oldSelector, window, suspectIdentifier, callerLine) {
1020
+ const allCandidates = [];
1021
+ for (const call of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
1022
+ const line = call.getStartLineNumber();
1023
+ if (line < window.start || line > window.end)
1024
+ continue;
1025
+ const args = call.getArguments();
1026
+ if (args.length > 0 && ts_morph_1.Node.isTemplateExpression(args[0]))
1027
+ continue;
1028
+ if (!absoluteNodeMatch(call, oldSelector))
1029
+ continue;
1030
+ const { rootReceiver } = climbToChainTop(call);
1031
+ if (rootReceiver && !PAGE_ROOT_NAMES.has(rootReceiver)) {
1032
+ allCandidates.push({
1033
+ receiver: rootReceiver,
1034
+ distance: Math.abs(line - callerLine),
1035
+ });
1036
+ }
1037
+ }
1038
+ if (suspectIdentifier && !PAGE_ROOT_NAMES.has(suspectIdentifier)) {
1039
+ const decl = jumpToDefinitionInFile(sourceFile, suspectIdentifier, callerLine + 5);
1040
+ if (decl) {
1041
+ const init = decl.getInitializer();
1042
+ if (init && ts_morph_1.Node.isCallExpression(init)) {
1043
+ for (const call of decl.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
1044
+ const args = call.getArguments();
1045
+ if (args.length > 0 && ts_morph_1.Node.isTemplateExpression(args[0]))
1046
+ continue;
1047
+ if (!absoluteNodeMatch(call, oldSelector))
1048
+ continue;
1049
+ const { rootReceiver } = climbToChainTop(call);
1050
+ if (rootReceiver && !PAGE_ROOT_NAMES.has(rootReceiver)) {
1051
+ allCandidates.push({ receiver: rootReceiver, distance: 0 });
1052
+ }
1053
+ }
1054
+ }
1055
+ }
1056
+ }
1057
+ if (allCandidates.length === 0)
1058
+ return null;
1059
+ allCandidates.sort((a, b) => a.distance - b.distance);
1060
+ const result = allCandidates[0].receiver;
1061
+ logger_1.logger.debug(`Discovered AST root receiver: "${result}"`);
1062
+ return result;
1063
+ }
1064
+ findCandidateNodes(sourceFile, oldSelector, window) {
1065
+ const results = [];
1066
+ for (const call of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
1067
+ const line = call.getStartLineNumber();
1068
+ if (line < window.start || line > window.end)
1069
+ continue;
1070
+ const args = call.getArguments();
1071
+ if (args.length > 0 && ts_morph_1.Node.isTemplateExpression(args[0]))
1072
+ continue;
1073
+ if (!absoluteNodeMatch(call, oldSelector))
1074
+ continue;
1075
+ const { chainTop, rootReceiver, chainDepth } = climbToChainTop(call);
1076
+ results.push({ call, chainTop, rootReceiver, chainDepth });
1077
+ }
1078
+ return results;
1079
+ }
1080
+ findCandidatesInDeclaration(sourceFile, varName, oldSelector, beforeLine) {
1081
+ const results = [];
1082
+ const decl = jumpToDefinitionInFile(sourceFile, varName, beforeLine + 5);
1083
+ if (!decl)
1084
+ return results;
1085
+ for (const call of decl.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
1086
+ const args = call.getArguments();
1087
+ if (args.length > 0 && ts_morph_1.Node.isTemplateExpression(args[0]))
1088
+ continue;
1089
+ if (!absoluteNodeMatch(call, oldSelector))
1090
+ continue;
1091
+ const { chainTop, rootReceiver, chainDepth } = climbToChainTop(call);
1092
+ results.push({
1093
+ call,
1094
+ chainTop,
1095
+ rootReceiver,
1096
+ chainDepth,
1097
+ identityConfidence: false,
1098
+ });
1099
+ }
1100
+ const init = decl.getInitializer();
1101
+ if (init &&
1102
+ ts_morph_1.Node.isCallExpression(init) &&
1103
+ absoluteNodeMatch(init, oldSelector)) {
1104
+ const { chainTop, rootReceiver, chainDepth } = climbToChainTop(init);
1105
+ results.push({
1106
+ call: init,
1107
+ chainTop,
1108
+ rootReceiver,
1109
+ chainDepth,
1110
+ identityConfidence: false,
1111
+ });
1112
+ }
1113
+ if (results.length === 0 && init && ts_morph_1.Node.isCallExpression(init)) {
1114
+ if (this.isPlaywrightLocatorCall(init)) {
1115
+ logger_1.logger.debug(`Identity override: '${varName}' @ line ${decl.getStartLineNumber()}`);
1116
+ const { chainTop, rootReceiver, chainDepth } = climbToChainTop(init);
1117
+ results.push({
1118
+ call: init,
1119
+ chainTop,
1120
+ rootReceiver,
1121
+ chainDepth,
1122
+ identityConfidence: true,
1123
+ });
1124
+ }
1125
+ else {
1126
+ const pwCalls = decl
1127
+ .getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)
1128
+ .filter((c) => {
1129
+ const args = c.getArguments();
1130
+ if (args.length > 0 && ts_morph_1.Node.isTemplateExpression(args[0]))
1131
+ return false;
1132
+ return this.isPlaywrightLocatorCall(c);
1133
+ });
1134
+ if (pwCalls.length > 0) {
1135
+ const innermost = pwCalls[pwCalls.length - 1];
1136
+ const { chainTop, rootReceiver, chainDepth } = climbToChainTop(innermost);
1137
+ results.push({
1138
+ call: innermost,
1139
+ chainTop,
1140
+ rootReceiver,
1141
+ chainDepth,
1142
+ identityConfidence: true,
1143
+ });
1144
+ }
1145
+ }
1146
+ }
1147
+ return results;
1148
+ }
1149
+ isPlaywrightLocatorCall(callExpr) {
1150
+ const expr = callExpr.getExpression();
1151
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
1152
+ return false;
1153
+ return ALL_SELECTOR_METHODS.has(expr.getName());
1154
+ }
1155
+ attemptPartialHeal(sourceFile, leafCall, chainTop, effectiveReceiver, originalSegments, newSegments, callLine) {
1156
+ if (JSON.stringify(originalSegments) === JSON.stringify(newSegments))
1157
+ return null;
1158
+ const changedIndices = [];
1159
+ for (let i = 0; i < originalSegments.length; i++) {
1160
+ if (JSON.stringify(originalSegments[i]) !== JSON.stringify(newSegments[i])) {
1161
+ changedIndices.push(i);
1162
+ }
1163
+ }
1164
+ if (changedIndices.length !== 1)
1165
+ return null;
1166
+ const changedIdx = changedIndices[0];
1167
+ const newSeg = newSegments[changedIdx];
1168
+ const chainCalls = collectChainCalls(chainTop);
1169
+ if (chainCalls.length <= changedIdx)
1170
+ return null;
1171
+ const targetCall = chainCalls[chainCalls.length - 1 - changedIdx];
1172
+ if (!targetCall)
1173
+ return null;
1174
+ const callReceiver = (() => {
1175
+ const expr = targetCall.getExpression();
1176
+ return ts_morph_1.Node.isPropertyAccessExpression(expr)
1177
+ ? expr.getExpression().getText()
1178
+ : effectiveReceiver;
1179
+ })();
1180
+ const currentMethodName = (() => {
1181
+ const expr = targetCall.getExpression();
1182
+ return ts_morph_1.Node.isPropertyAccessExpression(expr) ? expr.getName() : "?";
1183
+ })();
1184
+ const newCallText = MethodMapper.build(callReceiver, newSeg);
1185
+ if (!newCallText)
1186
+ return null;
1187
+ if (!this.validateGeneratedCode(newCallText, sourceFile))
1188
+ return null;
1189
+ logger_1.logger.debug(`Partial: segment[${changedIdx}] .${currentMethodName}() → ${newCallText}`);
1190
+ try {
1191
+ targetCall.replaceWithText(newCallText);
1192
+ persistSourceFile(sourceFile);
1193
+ return {
1194
+ success: true,
1195
+ reason: `partial: seg[${changedIdx}] .${currentMethodName}() → .${newSeg.type}()`,
1196
+ strategy: "NT-partial",
1197
+ lineUpdated: callLine - 1,
1198
+ };
1199
+ }
1200
+ catch (e) {
1201
+ logger_1.logger.warn(`Partial failed: ${e.message}`);
1202
+ return null;
1203
+ }
1204
+ }
1205
+ updateReceiverDefinition(sourceFile, varName, segType, newSelector, callerLine) {
1206
+ // Collect all matching declarations, then pick the closest one that
1207
+ // appears strictly BEFORE callerLine - prevents accidentally updating
1208
+ // a same-named variable in a different test block that happens to be
1209
+ // within the 100-line distance window.
1210
+ const declarations = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration);
1211
+ let bestDecl = null;
1212
+ let bestDistance = Infinity;
1213
+ for (const decl of declarations) {
1214
+ if (decl.getName() !== varName)
1215
+ continue;
1216
+ const declLine = decl.getStartLineNumber();
1217
+ if (declLine >= callerLine)
1218
+ continue; // must be before caller
1219
+ const dist = callerLine - declLine;
1220
+ if (dist < bestDistance) {
1221
+ bestDistance = dist;
1222
+ bestDecl = decl;
1223
+ }
1224
+ }
1225
+ if (!bestDecl)
1226
+ return false;
1227
+ const initializer = bestDecl.getInitializer();
1228
+ if (!initializer)
1229
+ return false;
1230
+ const allCalls = [
1231
+ ...(ts_morph_1.Node.isCallExpression(initializer) ? [initializer] : []),
1232
+ ...initializer.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression),
1233
+ ];
1234
+ for (const call of allCalls) {
1235
+ const expr = call.getExpression();
1236
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
1237
+ continue;
1238
+ if (expr.getName() !== segType)
1239
+ continue;
1240
+ const args = call.getArguments();
1241
+ if (args.length === 0)
1242
+ continue;
1243
+ const firstArg = args[0];
1244
+ if (!ts_morph_1.Node.isStringLiteral(firstArg))
1245
+ continue;
1246
+ firstArg.replaceWithText(`"${newSelector}"`);
1247
+ persistSourceFile(sourceFile);
1248
+ return true;
1249
+ }
1250
+ return false;
1251
+ }
1252
+ validateGeneratedCode(code, _sourceFile) {
1253
+ const receiverPattern = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\./g;
1254
+ const mockCandidates = new Set();
1255
+ const JS_KEYWORDS = new Set([
1256
+ "const",
1257
+ "let",
1258
+ "var",
1259
+ "function",
1260
+ "return",
1261
+ "await",
1262
+ "async",
1263
+ "new",
1264
+ "this",
1265
+ "typeof",
1266
+ "instanceof",
1267
+ "true",
1268
+ "false",
1269
+ "null",
1270
+ "undefined",
1271
+ "if",
1272
+ "else",
1273
+ "for",
1274
+ "while",
1275
+ "do",
1276
+ "switch",
1277
+ "case",
1278
+ "break",
1279
+ "continue",
1280
+ ]);
1281
+ let m;
1282
+ while ((m = receiverPattern.exec(code)) !== null) {
1283
+ const name = m[1];
1284
+ if (!JS_KEYWORDS.has(name) && !ALL_SELECTOR_METHODS.has(name)) {
1285
+ mockCandidates.add(name);
1286
+ }
1287
+ }
1288
+ PAGE_ROOT_NAMES.forEach((n) => mockCandidates.add(n));
1289
+ const bareIdentifierMatch = code.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\./);
1290
+ if (bareIdentifierMatch) {
1291
+ mockCandidates.add(bareIdentifierMatch[1]);
1292
+ }
1293
+ const mockPreamble = [...mockCandidates]
1294
+ .map((id) => `const ${id}: any = {} as any;`)
1295
+ .join("\n");
1296
+ const wrappedCode = `${mockPreamble}\nconst __val = ${code.trim()};`;
1297
+ try {
1298
+ const testProject = new ts_morph_1.Project({
1299
+ useInMemoryFileSystem: true,
1300
+ compilerOptions: { noEmit: true, skipLibCheck: true, noResolve: true },
1301
+ });
1302
+ const testFile = testProject.createSourceFile(`tv_${Date.now()}.ts`, wrappedCode, { overwrite: true });
1303
+ const diags = testFile.getPreEmitDiagnostics();
1304
+ const ALLOWED_CODES = new Set([
1305
+ 2304, 2339, 2345, 2554, 2571, 7006, 7016, 7031, 18004,
1306
+ ]);
1307
+ const syntaxErrors = diags.filter((d) => {
1308
+ if (d.getCategory() !== 1)
1309
+ return false;
1310
+ return !ALLOWED_CODES.has(d.getCode());
1311
+ });
1312
+ if (syntaxErrors.length > 0) {
1313
+ logger_1.logger.warn(`Syntax errors: ` +
1314
+ syntaxErrors
1315
+ .map((d) => `[TS${d.getCode()}] ${d.getMessageText()}`)
1316
+ .join(", "));
1317
+ if (/\.(getBy\w+|frameLocator)\s*\(/.test(code)) {
1318
+ logger_1.logger.warn(`Semantic method detected - bypassing validation`);
1319
+ return true;
1320
+ }
1321
+ return false;
1322
+ }
1323
+ if (testFile.getStatements().length === 0)
1324
+ return false;
1325
+ return true;
1326
+ }
1327
+ catch (e) {
1328
+ logger_1.logger.warn(`Validation exception: ${e.message}, using heuristic`);
1329
+ return /\.(getBy\w+|locator|frameLocator)\s*\(/.test(code);
1330
+ }
1331
+ }
1332
+ }
1333
+ // ═══════════════════════════════════════════════════════════════════
1334
+ // CHAIN ANALYSIS UTILITIES
1335
+ // ═══════════════════════════════════════════════════════════════════
1336
+ function climbToChainTop(startCall) {
1337
+ let chainTop = startCall;
1338
+ let chainDepth = 0;
1339
+ let cursor = startCall;
1340
+ while (true) {
1341
+ const parent = cursor.getParent();
1342
+ if (!parent)
1343
+ break;
1344
+ if (ts_morph_1.Node.isPropertyAccessExpression(parent)) {
1345
+ cursor = parent;
1346
+ continue;
1347
+ }
1348
+ if (ts_morph_1.Node.isCallExpression(parent)) {
1349
+ const parentExpr = parent.getExpression();
1350
+ if (ts_morph_1.Node.isPropertyAccessExpression(parentExpr)) {
1351
+ const methodName = parentExpr.getName();
1352
+ if (CHAIN_CONTINUATION_METHODS.has(methodName)) {
1353
+ chainTop = parent;
1354
+ chainDepth++;
1355
+ cursor = parent;
1356
+ continue;
1357
+ }
1358
+ }
1359
+ break;
1360
+ }
1361
+ break;
1362
+ }
1363
+ const findLeftmost = (n) => {
1364
+ if (ts_morph_1.Node.isCallExpression(n)) {
1365
+ const e = n.getExpression();
1366
+ if (ts_morph_1.Node.isPropertyAccessExpression(e))
1367
+ return findLeftmost(e.getExpression());
1368
+ }
1369
+ if (ts_morph_1.Node.isPropertyAccessExpression(n))
1370
+ return findLeftmost(n.getExpression());
1371
+ if (ts_morph_1.Node.isAwaitExpression(n))
1372
+ return findLeftmost(n.getExpression());
1373
+ return n
1374
+ .getText()
1375
+ .replace(/^await\s+/, "")
1376
+ .trim();
1377
+ };
1378
+ return { chainTop, rootReceiver: findLeftmost(chainTop), chainDepth };
1379
+ }
1380
+ function collectChainCalls(chainTop) {
1381
+ const calls = [];
1382
+ let current = chainTop;
1383
+ while (ts_morph_1.Node.isCallExpression(current)) {
1384
+ calls.push(current);
1385
+ const expr = current.getExpression();
1386
+ if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
1387
+ current = expr.getExpression();
1388
+ }
1389
+ else {
1390
+ break;
1391
+ }
1392
+ }
1393
+ return calls;
1394
+ }
1395
+ function isLeafOfChain(call) {
1396
+ const expr = call.getExpression();
1397
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
1398
+ return false;
1399
+ const receiver = expr.getExpression();
1400
+ if (ts_morph_1.Node.isCallExpression(receiver))
1401
+ return true;
1402
+ if (ts_morph_1.Node.isPropertyAccessExpression(receiver)) {
1403
+ return ts_morph_1.Node.isCallExpression(receiver.getExpression());
1404
+ }
1405
+ return false;
1406
+ }
1407
+ function analyzeVariableInitializerChain(decl) {
1408
+ const initializer = decl.getInitializer();
1409
+ if (!initializer)
1410
+ return null;
1411
+ if (!ts_morph_1.Node.isCallExpression(initializer))
1412
+ return null;
1413
+ const countChainDepth = (n, depth) => {
1414
+ if (ts_morph_1.Node.isCallExpression(n)) {
1415
+ const e = n.getExpression();
1416
+ if (ts_morph_1.Node.isPropertyAccessExpression(e)) {
1417
+ const methodName = e.getName();
1418
+ if (CHAIN_CONTINUATION_METHODS.has(methodName)) {
1419
+ return countChainDepth(e.getExpression(), depth + 1);
1420
+ }
1421
+ }
1422
+ }
1423
+ if (ts_morph_1.Node.isPropertyAccessExpression(n))
1424
+ return countChainDepth(n.getExpression(), depth);
1425
+ return {
1426
+ depth,
1427
+ root: n
1428
+ .getText()
1429
+ .replace(/^await\s+/, "")
1430
+ .trim(),
1431
+ };
1432
+ };
1433
+ const { depth, root } = countChainDepth(initializer, 0);
1434
+ if (depth === 0)
1435
+ return null;
1436
+ return { fullChainNode: initializer, rootReceiver: root, chainDepth: depth };
1437
+ }
1438
+ function jumpToDefinitionInFile(sourceFile, varName, beforeLine) {
1439
+ const text = sourceFile.getFullText();
1440
+ const lines = text.split("\n");
1441
+ let beforeOffset = 0;
1442
+ for (let i = 0; i < Math.min(beforeLine - 1, lines.length); i++) {
1443
+ beforeOffset += lines[i].length + 1;
1444
+ }
1445
+ let best = null;
1446
+ let bestOffset = -1;
1447
+ for (const decl of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration)) {
1448
+ if (decl.getName() !== varName)
1449
+ continue;
1450
+ const offset = decl.getStart();
1451
+ if (offset < beforeOffset && offset > bestOffset) {
1452
+ best = decl;
1453
+ bestOffset = offset;
1454
+ }
1455
+ }
1456
+ if (best) {
1457
+ logger_1.logger.debug(`JumpToDef: '${varName}' @ line ${best.getStartLineNumber()}`);
1458
+ }
1459
+ return best;
1460
+ }
1461
+ // ═══════════════════════════════════════════════════════════════════
1462
+ // ChainValidator integration
1463
+ // ═══════════════════════════════════════════════════════════════════
1464
+ const chainValidator = new ChainValidator_1.ChainValidator();
1465
+ const nodeTransformer = new NodeTransformationStrategy(chainValidator);
1466
+ function buildSmartChainCode(receiver, smartSegments, originalReceiver, rootType = "unknown", originalText) {
1467
+ const segments = smartSegments;
1468
+ const result = chainValidator.validate({
1469
+ segments,
1470
+ originalRootReceiver: originalReceiver,
1471
+ proposedRootReceiver: receiver,
1472
+ rootType,
1473
+ applySemanticUpgrade: true,
1474
+ });
1475
+ if (!result.valid) {
1476
+ logger_1.logger.warn(`ChainValidator rejected segments: ${result.reason}`);
1477
+ return null;
1478
+ }
1479
+ const finalSegments = (result.segments ??
1480
+ segments);
1481
+ const code = MethodMapper.buildChain(receiver, finalSegments, originalText);
1482
+ if (code)
1483
+ return code;
1484
+ const multiLine = finalSegments.length >= 3;
1485
+ return (0, ChainValidator_1.segmentsToCode)(receiver, result.segments ?? segments, multiLine);
1486
+ }
1487
+ const TEXT_MATCHERS = new Set([
1488
+ "toHaveText",
1489
+ "toContainText",
1490
+ "toHaveValue",
1491
+ "toHaveAttribute",
1492
+ "toHaveTitle",
1493
+ "toHaveLabel",
1494
+ ]);
1495
+ function findAssertionsInWindow(sourceFile, locatorVarName, callerLine, windowLines) {
1496
+ const results = [];
1497
+ const scanStart = Math.max(1, callerLine - 2);
1498
+ const scanEnd = Math.min(sourceFile.getEndLineNumber(), callerLine + windowLines);
1499
+ for (const call of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
1500
+ const line = call.getStartLineNumber();
1501
+ if (line < scanStart || line > scanEnd)
1502
+ continue;
1503
+ const expr = call.getExpression();
1504
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
1505
+ continue;
1506
+ if (!TEXT_MATCHERS.has(expr.getName()))
1507
+ continue;
1508
+ let expectCallNode = null;
1509
+ let cur = expr.getExpression();
1510
+ for (let i = 0; i < 8; i++) {
1511
+ if (ts_morph_1.Node.isCallExpression(cur)) {
1512
+ const inner = cur.getExpression();
1513
+ if (ts_morph_1.Node.isIdentifier(inner) && inner.getText() === "expect") {
1514
+ expectCallNode = cur;
1515
+ break;
1516
+ }
1517
+ if (ts_morph_1.Node.isPropertyAccessExpression(inner)) {
1518
+ const nm = inner.getName();
1519
+ if (nm === "not" || nm === "soft" || nm === "poll") {
1520
+ cur = inner.getExpression();
1521
+ continue;
1522
+ }
1523
+ }
1524
+ }
1525
+ if (ts_morph_1.Node.isPropertyAccessExpression(cur)) {
1526
+ cur = cur.getExpression();
1527
+ continue;
1528
+ }
1529
+ break;
1530
+ }
1531
+ if (!expectCallNode)
1532
+ continue;
1533
+ const expectArgs = expectCallNode.getArguments();
1534
+ if (expectArgs.length === 0)
1535
+ continue;
1536
+ const argText = expectArgs[0].getText();
1537
+ if (locatorVarName !== null) {
1538
+ const matches = argText === locatorVarName ||
1539
+ argText.startsWith(locatorVarName + ".") ||
1540
+ argText.startsWith(locatorVarName + " ");
1541
+ if (!matches)
1542
+ continue;
1543
+ }
1544
+ const matcherArgs = call.getArguments();
1545
+ if (matcherArgs.length === 0)
1546
+ continue;
1547
+ const firstMatcherArg = matcherArgs[0];
1548
+ if (ts_morph_1.Node.isRegularExpressionLiteral(firstMatcherArg))
1549
+ continue;
1550
+ let currentValue = null;
1551
+ if (ts_morph_1.Node.isStringLiteral(firstMatcherArg)) {
1552
+ currentValue = firstMatcherArg.getLiteralValue();
1553
+ }
1554
+ if (currentValue === null)
1555
+ continue;
1556
+ results.push({
1557
+ expectCall: expectCallNode,
1558
+ matcherCall: call,
1559
+ matcherName: expr.getName(),
1560
+ currentValue,
1561
+ line,
1562
+ });
1563
+ }
1564
+ return results;
1565
+ }
1566
+ /**
1567
+ * Build a locator EXPRESSION from a raw healed selector + chain root.
1568
+ * Internal engine selectors become their idiomatic method form
1569
+ * (`page.getByRole(...)`); otherwise a `root.locator("selector")` call.
1570
+ */
1571
+ function buildLocatorExpr(rawSelector, rootReceiver) {
1572
+ const root = rootReceiver || "page";
1573
+ if (!rawSelector)
1574
+ return `${root}.locator("")`;
1575
+ if ((0, SelectorSanitizer_1.isInternalEngineSelector)(rawSelector)) {
1576
+ const method = (0, SelectorSanitizer_1.internalToMethodExpression)(rawSelector, root);
1577
+ if (method)
1578
+ return method;
1579
+ const publicStr = (0, SelectorSanitizer_1.internalToPublicSelectorString)(rawSelector);
1580
+ return `${root}.locator(${JSON.stringify(publicStr ?? rawSelector)})`;
1581
+ }
1582
+ return `${root}.locator(${JSON.stringify(rawSelector)})`;
1583
+ }
1584
+ /** 0-based column of the first non-whitespace char on a 1-based line. */
1585
+ function firstNonWhitespaceColumn(text, line1Based) {
1586
+ const lines = text.split(/\r?\n/);
1587
+ const idx = line1Based - 1;
1588
+ if (idx < 0 || idx >= lines.length)
1589
+ return 0;
1590
+ const m = lines[idx].match(/^(\s*)/);
1591
+ return m ? m[1].length : 0;
1592
+ }
1593
+ class ASTSourceUpdater {
1594
+ project;
1595
+ advisoryBuffer = new HealingAdvisory_1.AdvisoryBuffer();
1596
+ flushAdvisories() {
1597
+ this.advisoryBuffer.flush();
1598
+ }
1599
+ // ═══════════════════════════════════════════════════════════════════
1600
+ // STAGE 4 — end-to-end trace → decide → apply (Stages 1-3 wired in)
1601
+ //
1602
+ // Additive entry point. The legacy heal paths below are untouched; this
1603
+ // is the new write-disciplined pipeline that composes AnchorResolver,
1604
+ // TraceBackEngine, DecisionEngine and MutationApplier.
1605
+ // ═══════════════════════════════════════════════════════════════════
1606
+ /**
1607
+ * Trace the failing call-site back to its source declaration, decide the
1608
+ * mutation strategy (in-place / fork / prompt), and — when `apply` is set —
1609
+ * write it to disk through the type-safety gate.
1610
+ */
1611
+ static composeHeal(input) {
1612
+ const original = fs.readFileSync(input.filePath, "utf8");
1613
+ const project = new ts_morph_1.Project({
1614
+ useInMemoryFileSystem: true,
1615
+ compilerOptions: {
1616
+ allowJs: true,
1617
+ noEmit: true,
1618
+ skipLibCheck: true,
1619
+ noResolve: true,
1620
+ },
1621
+ });
1622
+ const sf = project.createSourceFile(input.filePath, original, {
1623
+ overwrite: true,
1624
+ });
1625
+ // Resolve a robust position: when no explicit column is given, use the
1626
+ // first non-whitespace char of the line (avoids landing on indentation).
1627
+ const column = input.column ?? firstNonWhitespaceColumn(original, input.line);
1628
+ const trace = TraceBackEngine_1.TraceBackEngine.traceFromPosition(sf, input.line, column);
1629
+ if (trace.status !== "target") {
1630
+ return { kind: "abort", trace };
1631
+ }
1632
+ // Re-resolve the node at the failing position for fork test-block lookup.
1633
+ let failingNode;
1634
+ try {
1635
+ failingNode = sf.getDescendantAtPos(sf.compilerNode.getPositionOfLineAndCharacter(input.line - 1, column));
1636
+ }
1637
+ catch {
1638
+ failingNode = undefined;
1639
+ }
1640
+ if (!failingNode)
1641
+ failingNode = trace.target.targetNode;
1642
+ // Build the replacement EXPRESSION. Explicit newLocatorExpr wins; else
1643
+ // synthesise it from the raw healed selector + the traced root receiver,
1644
+ // translating internal engine selectors to their idiomatic method form.
1645
+ const newLocatorExpr = input.newLocatorExpr ??
1646
+ buildLocatorExpr(input.newSelector ?? "", trace.target.rootReceiver);
1647
+ const decision = DecisionEngine_1.DecisionEngine.decide({
1648
+ target: trace.target,
1649
+ failingCallSite: failingNode,
1650
+ newLocatorExpr,
1651
+ contextSpecific: input.contextSpecific ?? false,
1652
+ globalCorrect: input.globalCorrect ?? false,
1653
+ });
1654
+ const batch = ASTSourceUpdater.decisionToBatch(input.filePath, decision);
1655
+ const result = {
1656
+ kind: "decision",
1657
+ trace,
1658
+ decision,
1659
+ batch,
1660
+ writtenLine: trace.target.targetNode.getStartLineNumber(),
1661
+ };
1662
+ if (input.apply && batch.edits.length > 0) {
1663
+ result.outcome = MutationApplier_1.MutationApplier.applyFileBatch(batch, input.applyOptions ?? {});
1664
+ }
1665
+ return result;
1666
+ }
1667
+ /** Drain-loop entry: apply pre-built batches through the write gate. */
1668
+ static applyMutationBatches(batches, opts = {}) {
1669
+ return MutationApplier_1.MutationApplier.applyBatches(batches, opts);
1670
+ }
1671
+ /** Convert a DecisionEngine result into an anchored MutationApplier batch. */
1672
+ static decisionToBatch(filePath, decision) {
1673
+ const edits = decision.edits.map((e) => {
1674
+ if (e.kind === "insert") {
1675
+ return {
1676
+ kind: "insert",
1677
+ replacement: e.replacement,
1678
+ span: e.span,
1679
+ requireLocator: false,
1680
+ };
1681
+ }
1682
+ // MUTATE_IN_PLACE replace → anchor-resolved + Locator-type-gated.
1683
+ if (decision.strategy === "MUTATE_IN_PLACE") {
1684
+ return {
1685
+ kind: "replace",
1686
+ replacement: e.replacement,
1687
+ anchor: decision.anchor,
1688
+ requireLocator: true,
1689
+ };
1690
+ }
1691
+ // FORK call-site receiver rewrite → identifier, not a locator.
1692
+ return {
1693
+ kind: "replace",
1694
+ replacement: e.replacement,
1695
+ span: e.span,
1696
+ requireLocator: false,
1697
+ };
1698
+ });
1699
+ return { filePath, edits };
1700
+ }
1701
+ /**
1702
+ * Centralised write hook for every ts-morph `saveSync` call in this class.
1703
+ * When `SELA_DRY_RUN` is active the AST mutation stays in memory but the
1704
+ * on-disk file is left untouched - the heal pipeline can still compute a
1705
+ * candidate diff via `sourceFile.getFullText()` and the report layer
1706
+ * tags the event with `dryRun: true`.
1707
+ */
1708
+ _persistChanges(sourceFile) {
1709
+ persistSourceFile(sourceFile);
1710
+ }
1711
+ // ─────────────────────────────────────────────────────────────────
1712
+ // FAIL-FAST DIRECTIVE SCAN
1713
+ //
1714
+ // Walks the leading comment ranges of the failing statement (and the
1715
+ // CallExpression node when it differs) looking for the directive
1716
+ // `// sela-fail-fast`. When found, throws `SelaError(ASTUpdater,
1717
+ // AST_HEAL_DISABLED_BY_DIRECTIVE)` so SelaEngine can short-circuit
1718
+ // BEFORE the LLM call and record a FailedEvent in the "Skipped by
1719
+ // Developer" category.
1720
+ //
1721
+ // Implementation note - ts-morph's `Node.getLeadingCommentRanges()`
1722
+ // returns `ts.CommentRange[]` (offsets, not text). We slice the full
1723
+ // source text by `(pos, end)` to recover the raw comment for each
1724
+ // range, matching both `//` line comments and `/* */` block comments
1725
+ // with either `sela-fail-fast` or `@sela-fail-fast` spelling.
1726
+ // ─────────────────────────────────────────────────────────────────
1727
+ scanFailFastDirective(caller) {
1728
+ const filePath = this.resolveFilePath(caller.filePath);
1729
+ if (!filePath)
1730
+ return { failFast: false };
1731
+ let sourceFile = this.project.getSourceFile(filePath);
1732
+ if (sourceFile) {
1733
+ sourceFile.refreshFromFileSystemSync();
1734
+ }
1735
+ else {
1736
+ sourceFile = this.project.addSourceFileAtPath(filePath);
1737
+ }
1738
+ if (caller.line <= 0)
1739
+ return { failFast: false };
1740
+ const failureNode = this.findNodeAtLine(sourceFile, caller.line);
1741
+ if (!failureNode)
1742
+ return { failFast: false };
1743
+ const statement = this.getEnclosingStatement(failureNode);
1744
+ // Collect candidate comment-owning nodes: enclosing statement (catches
1745
+ // pre-statement comments) AND the failure node itself (catches inline
1746
+ // pre-CallExpression comments inside multi-line chains). De-dup by
1747
+ // offset to avoid double-matching.
1748
+ const fullText = sourceFile.getFullText();
1749
+ const seen = new Set();
1750
+ const nodes = [];
1751
+ if (statement)
1752
+ nodes.push(statement);
1753
+ if (failureNode !== statement)
1754
+ nodes.push(failureNode);
1755
+ const DIRECTIVE_RE = /\/[\/*]\s*@?sela-fail-fast\b/i;
1756
+ for (const node of nodes) {
1757
+ let ranges = [];
1758
+ try {
1759
+ ranges = node.getLeadingCommentRanges() ?? [];
1760
+ }
1761
+ catch {
1762
+ ranges = [];
1763
+ }
1764
+ for (const r of ranges) {
1765
+ const pos = r.getPos();
1766
+ const end = r.getEnd();
1767
+ const key = `${pos}:${end}`;
1768
+ if (seen.has(key))
1769
+ continue;
1770
+ seen.add(key);
1771
+ const text = fullText.slice(pos, end);
1772
+ if (DIRECTIVE_RE.test(text)) {
1773
+ const lineNum = sourceFile.getLineAndColumnAtPos(pos).line;
1774
+ return {
1775
+ failFast: true,
1776
+ matchedComment: text.trim(),
1777
+ matchedLine: lineNum,
1778
+ };
1779
+ }
1780
+ }
1781
+ }
1782
+ return { failFast: false };
1783
+ }
1784
+ /**
1785
+ * Convenience helper - runs `scanFailFastDirective` and throws the
1786
+ * canonical SelaError when the directive is present. Used by SelaEngine
1787
+ * as the very first gate inside `heal()`.
1788
+ */
1789
+ enforceFailFastDirective(caller) {
1790
+ const result = this.scanFailFastDirective(caller);
1791
+ if (!result.failFast)
1792
+ return;
1793
+ throw new SelaError_1.SelaError({
1794
+ subsystem: "ASTUpdater",
1795
+ code: SelaError_1.AST_UPDATER_CODES.HEAL_DISABLED_BY_DIRECTIVE,
1796
+ reason: `Heal disabled by developer directive at ${path.basename(caller.filePath)}` +
1797
+ (result.matchedLine ? `:${result.matchedLine}` : ""),
1798
+ context: {
1799
+ filePath: caller.filePath,
1800
+ line: caller.line,
1801
+ directiveLine: result.matchedLine,
1802
+ directiveText: result.matchedComment,
1803
+ category: "Skipped by Developer",
1804
+ },
1805
+ });
1806
+ }
1807
+ constructor() {
1808
+ this.project = new ts_morph_1.Project({
1809
+ useInMemoryFileSystem: false,
1810
+ skipAddingFilesFromTsConfig: true,
1811
+ compilerOptions: {
1812
+ allowJs: true,
1813
+ noEmit: true,
1814
+ skipLibCheck: true,
1815
+ noResolve: true,
1816
+ },
1817
+ });
1818
+ }
1819
+ WRAPPER_IDENTIFIERS = new Set([
1820
+ "test",
1821
+ "it",
1822
+ "describe",
1823
+ "beforeEach",
1824
+ "afterEach",
1825
+ "beforeAll",
1826
+ "afterAll",
1827
+ "expect",
1828
+ "page",
1829
+ "await",
1830
+ "step",
1831
+ "context",
1832
+ "suite",
1833
+ ]);
1834
+ STRUCTURAL_ROLES = new Set([
1835
+ "heading",
1836
+ "banner",
1837
+ "main",
1838
+ "navigation",
1839
+ "nav",
1840
+ "complementary",
1841
+ "aside",
1842
+ "contentinfo",
1843
+ "footer",
1844
+ "region",
1845
+ "landmark",
1846
+ "article",
1847
+ "section",
1848
+ "list",
1849
+ "listitem",
1850
+ "term",
1851
+ "definition",
1852
+ "figure",
1853
+ "img",
1854
+ "presentation",
1855
+ "none",
1856
+ "separator",
1857
+ "scrollbar",
1858
+ "log",
1859
+ "marquee",
1860
+ "status",
1861
+ "timer",
1862
+ "alert",
1863
+ "alertdialog",
1864
+ ]);
1865
+ INTERACTIVE_ROLES = new Set([
1866
+ "textbox",
1867
+ "searchbox",
1868
+ "spinbutton",
1869
+ "button",
1870
+ "checkbox",
1871
+ "radio",
1872
+ "switch",
1873
+ "combobox",
1874
+ "listbox",
1875
+ "option",
1876
+ "menuitem",
1877
+ "menuitemcheckbox",
1878
+ "menuitemradio",
1879
+ "link",
1880
+ "tab",
1881
+ "slider",
1882
+ "progressbar",
1883
+ "grid",
1884
+ "gridcell",
1885
+ "row",
1886
+ "rowheader",
1887
+ "columnheader",
1888
+ ]);
1889
+ // ─────────────────────────────────────────────────────────────────
1890
+ // PUBLIC ENTRY POINT
1891
+ // ─────────────────────────────────────────────────────────────────
1892
+ update(caller, oldSelector, newSelector, aiSegments, fullSelectorContext, contentChange, smartChainSegments, originalChainHint) {
1893
+ const filePath = this.resolveFilePath(caller.filePath);
1894
+ if (!filePath) {
1895
+ return {
1896
+ success: false,
1897
+ reason: `File not found: ${caller.filePath}`,
1898
+ strategy: "none",
1899
+ };
1900
+ }
1901
+ logger_1.logger.debug(`═══════════════════════════════`);
1902
+ logger_1.logger.debug(`${path.basename(filePath)} @ line ${caller.line}`);
1903
+ logger_1.logger.debug(`old: "${oldSelector}"`);
1904
+ logger_1.logger.debug(` → new: "${newSelector}"`);
1905
+ if (smartChainSegments) {
1906
+ logger_1.logger.debug(`smart chain segments: ${JSON.stringify(smartChainSegments)}`);
1907
+ }
1908
+ if (contentChange) {
1909
+ logger_1.logger.debug(`content change: "${contentChange.oldText}" → "${contentChange.newText}"`);
1910
+ }
1911
+ logger_1.logger.debug(`═══════════════════════════════`);
1912
+ const oldSem = parseInternalSelector(oldSelector
1913
+ .split(">>")
1914
+ .pop()
1915
+ .trim()
1916
+ .replace(/\s*i\s*$/, ""));
1917
+ const newSem = parseInternalSelector(newSelector
1918
+ .split(">>")
1919
+ .pop()
1920
+ .trim()
1921
+ .replace(/\s*i\s*$/, ""));
1922
+ logger_1.logger.debug(`old sem: ${oldSem.method}("${oldSem.primaryArg}")`);
1923
+ logger_1.logger.debug(`new sem: ${newSem.method}("${newSem.primaryArg}")`);
1924
+ let sourceFile = this.project.getSourceFile(filePath);
1925
+ if (sourceFile) {
1926
+ sourceFile.refreshFromFileSystemSync();
1927
+ }
1928
+ else {
1929
+ sourceFile = this.project.addSourceFileAtPath(filePath);
1930
+ }
1931
+ if (caller.line <= 0) {
1932
+ logger_1.logger.debug(`line ${caller.line} invalid - Attempting Global Template Strategy`);
1933
+ const globalResult = this.strategyTemplateLiteral(sourceFile, caller, oldSelector, newSelector);
1934
+ if (globalResult && globalResult.success) {
1935
+ if (contentChange)
1936
+ this.runStrategyG(sourceFile, null, null, contentChange);
1937
+ return this.broadcastAndReturn(globalResult, oldSelector, newSelector, caller);
1938
+ }
1939
+ if (contentChange)
1940
+ this.runStrategyG(sourceFile, null, null, contentChange);
1941
+ return {
1942
+ success: false,
1943
+ reason: "invalid line number (≤0) and global fallback failed",
1944
+ strategy: "none",
1945
+ };
1946
+ }
1947
+ const failureNode = this.findNodeAtLine(sourceFile, caller.line);
1948
+ const statement = failureNode
1949
+ ? this.getEnclosingStatement(failureNode)
1950
+ : undefined;
1951
+ // ─── Developer fail-fast directive ─────────────────────────────
1952
+ // Scan leading comments on the enclosing statement (and the failure
1953
+ // node when they differ) for `// sela-fail-fast`. When present, abort
1954
+ // all heal work with SelaError(AST_HEAL_DISABLED_BY_DIRECTIVE) so the
1955
+ // engine records a FailedEvent in the "Skipped by Developer" bucket.
1956
+ // Defense-in-depth - SelaEngine also calls this at the very start of
1957
+ // heal() to skip the LLM round-trip entirely.
1958
+ this.enforceFailFastDirective(caller);
1959
+ const suspectIdentifier = statement
1960
+ ? this.identifySuspectIdentifier(statement, caller.line)
1961
+ : null;
1962
+ if (suspectIdentifier) {
1963
+ logger_1.logger.debug(`Primary suspect: '${suspectIdentifier}'`);
1964
+ }
1965
+ // ─── T-10: Semantic Dispatch Gate ──────────────────────────────
1966
+ // HARD EXIT: any non-null result (success OR failure) stops all
1967
+ // subsequent AST strategies. When the locator argument is an
1968
+ // Identifier, PropertyAccess, or TemplateLiteral, the gate owns
1969
+ // the mutation - NT/H must never override the symbolic reference.
1970
+ const semanticResult = this.semanticDispatch(sourceFile, caller, oldSelector, newSelector);
1971
+ if (semanticResult !== null) {
1972
+ if (semanticResult.success) {
1973
+ if (contentChange) {
1974
+ this.runStrategyG(sourceFile, caller, suspectIdentifier, contentChange);
1975
+ }
1976
+ return this.broadcastAndReturn(semanticResult, oldSelector, newSelector, caller);
1977
+ }
1978
+ // Failure (blast-radius block, trace failed, syntax error) - hard stop.
1979
+ logger_1.logger.debug(`Hard stop (${semanticResult.strategy}): ${semanticResult.reason}`);
1980
+ return semanticResult;
1981
+ }
1982
+ // ───────────────────────────────────────────────────────────────
1983
+ const startLine = Math.max(1, caller.line - 40);
1984
+ const endLine = Math.min(sourceFile.getEndLineNumber(), caller.line + 5);
1985
+ const structuralStrategies = [
1986
+ () => smartChainSegments && smartChainSegments.length > 0
1987
+ ? nodeTransformer.replaceLocatorNode(sourceFile, caller, oldSelector, smartChainSegments, originalChainHint, suspectIdentifier, { start: startLine, end: endLine })
1988
+ : null,
1989
+ () => smartChainSegments && smartChainSegments.length > 0
1990
+ ? this.strategySmartChain(sourceFile, caller, oldSelector, oldSem, smartChainSegments, originalChainHint, suspectIdentifier)
1991
+ : null,
1992
+ () => aiSegments && aiSegments.length > 0 && fullSelectorContext
1993
+ ? this.strategyFrameUpstream(sourceFile, caller, oldSelector, aiSegments, fullSelectorContext)
1994
+ : null,
1995
+ () => this.strategyChainCollapse(sourceFile, caller, oldSelector, newSelector, oldSem, newSem),
1996
+ () => statement
1997
+ ? this.strategyDeepSymbolResolution(sourceFile, caller, oldSelector, newSelector, oldSem, newSem, suspectIdentifier)
1998
+ : null,
1999
+ () => this.strategyInlineSemanticFix(sourceFile, caller, oldSelector, newSelector, oldSem, newSem),
2000
+ () => this.strategyTemplateLiteral(sourceFile, caller, oldSelector, newSelector),
2001
+ () => this.strategySemanticFragmentScan(sourceFile, caller, oldSelector, newSelector, oldSem, newSem),
2002
+ ];
2003
+ for (const strategyFn of structuralStrategies) {
2004
+ const result = strategyFn();
2005
+ if (result && result.success) {
2006
+ if (contentChange) {
2007
+ this.runStrategyG(sourceFile, caller, suspectIdentifier, contentChange);
2008
+ }
2009
+ return this.broadcastAndReturn(result, oldSelector, newSelector, caller);
2010
+ }
2011
+ }
2012
+ if (contentChange) {
2013
+ this.runStrategyG(sourceFile, caller, suspectIdentifier, contentChange);
2014
+ return {
2015
+ success: false,
2016
+ reason: "Assertion drift detected - manual review required",
2017
+ strategy: "G",
2018
+ };
2019
+ }
2020
+ return {
2021
+ success: false,
2022
+ reason: "All AST strategies exhausted",
2023
+ strategy: "none",
2024
+ };
2025
+ }
2026
+ // ═══════════════════════════════════════════════════════════════
2027
+ // STRATEGY H - Smart Chain (Legacy fallback)
2028
+ // ═══════════════════════════════════════════════════════════════
2029
+ strategySmartChain(sourceFile, caller, oldSelector, oldSem, smartSegments, originalChainHint, suspectIdentifier) {
2030
+ logger_1.logger.debug(`Strategy H v2: Smart Chain (${smartSegments.length} segments)`);
2031
+ const validationResult = chainValidator.validate({
2032
+ segments: smartSegments,
2033
+ originalRootReceiver: "page",
2034
+ proposedRootReceiver: "page",
2035
+ rootType: "unknown",
2036
+ applySemanticUpgrade: true,
2037
+ });
2038
+ if (!validationResult.valid) {
2039
+ logger_1.logger.warn(`H: ChainValidator pre-check failed: ${validationResult.reason}`);
2040
+ return null;
2041
+ }
2042
+ const resolvedSegments = (validationResult.segments ??
2043
+ smartSegments);
2044
+ const startLine = Math.max(1, caller.line - 40);
2045
+ const endLine = Math.min(sourceFile.getEndLineNumber(), caller.line + 5);
2046
+ const candidates = [];
2047
+ for (const call of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
2048
+ const callLine = call.getStartLineNumber();
2049
+ if (callLine < startLine || callLine > endLine)
2050
+ continue;
2051
+ const expr = call.getExpression();
2052
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
2053
+ continue;
2054
+ const args = call.getArguments();
2055
+ if (args.length > 0 && ts_morph_1.Node.isTemplateExpression(args[0]))
2056
+ continue;
2057
+ const callSem = extractSemanticFromCall(call);
2058
+ if (!callSem)
2059
+ continue;
2060
+ if (!semanticMatch(callSem, oldSelector, true))
2061
+ continue;
2062
+ const { chainTop, rootReceiver, chainDepth } = climbToChainTop(call);
2063
+ const isChained = chainDepth > 0 || isLeafOfChain(call);
2064
+ const effectiveDepth = chainDepth > 0 ? chainDepth : isChained ? 1 : 0;
2065
+ candidates.push({
2066
+ leafCall: call,
2067
+ chainTop,
2068
+ rootReceiver,
2069
+ chainDepth: effectiveDepth,
2070
+ distance: Math.abs(callLine - caller.line),
2071
+ originalText: chainTop.getText(),
2072
+ });
2073
+ }
2074
+ if (suspectIdentifier && candidates.length === 0) {
2075
+ const decl = jumpToDefinitionInFile(sourceFile, suspectIdentifier, caller.line);
2076
+ if (decl) {
2077
+ const initChain = analyzeVariableInitializerChain(decl);
2078
+ if (initChain) {
2079
+ const { chainTop, rootReceiver } = climbToChainTop(initChain.fullChainNode);
2080
+ candidates.push({
2081
+ leafCall: initChain.fullChainNode,
2082
+ chainTop,
2083
+ rootReceiver,
2084
+ chainDepth: initChain.chainDepth,
2085
+ distance: Math.abs(decl.getStartLineNumber() - caller.line),
2086
+ originalText: chainTop.getText(),
2087
+ });
2088
+ }
2089
+ }
2090
+ }
2091
+ if (candidates.length === 0) {
2092
+ logger_1.logger.debug(`H: no matching candidates found`);
2093
+ return null;
2094
+ }
2095
+ candidates.sort((a, b) => {
2096
+ if (a.distance !== b.distance)
2097
+ return a.distance - b.distance;
2098
+ return b.chainDepth - a.chainDepth;
2099
+ });
2100
+ for (const { leafCall, chainTop, rootReceiver, chainDepth, originalText, } of candidates) {
2101
+ // Identifier/PropertyAccess guard - mirrors the NT guard.
2102
+ // Strategy H must never substitute a hardcoded string for a symbolic
2103
+ // reference; the semantic dispatch gate owns definition-site mutations.
2104
+ const firstArgH = leafCall.getArguments()[0];
2105
+ if (firstArgH &&
2106
+ (ts_morph_1.Node.isIdentifier(firstArgH) ||
2107
+ ts_morph_1.Node.isPropertyAccessExpression(firstArgH))) {
2108
+ logger_1.logger.debug(`skip - first arg is ${firstArgH.getKindName()} ` +
2109
+ `"${firstArgH.getText()}" (semantic dispatch owns definition-site mutations)`);
2110
+ continue;
2111
+ }
2112
+ const callLine = leafCall.getStartLineNumber();
2113
+ if (originalChainHint &&
2114
+ originalChainHint.length === resolvedSegments.length) {
2115
+ const partialResult = this.attemptPartialChainHeal(sourceFile, caller, chainTop, rootReceiver, originalChainHint, resolvedSegments, callLine);
2116
+ if (partialResult && partialResult.success)
2117
+ return partialResult;
2118
+ }
2119
+ // ── Scope safety for Strategy H ─────────────────────────────────
2120
+ // If this candidate's chainTop lives inside the own initializer of
2121
+ // suspectIdentifier, we must use rootReceiver (page), not the var.
2122
+ const safeRootReceiver = suspectIdentifier && isInsideOwnInitializer(chainTop, suspectIdentifier)
2123
+ ? (() => {
2124
+ logger_1.logger.debug(`Circular guard: rootReceiver adjusted from ` +
2125
+ `"${rootReceiver}" (inside '${suspectIdentifier}' init) → using AST root`);
2126
+ return rootReceiver; // rootReceiver from climbToChainTop is always page/root
2127
+ })()
2128
+ : rootReceiver;
2129
+ const newCode = buildSmartChainCode(safeRootReceiver, resolvedSegments, safeRootReceiver, "unknown", originalText);
2130
+ if (!newCode) {
2131
+ logger_1.logger.warn(`H: MethodMapper/ChainValidator produced no code`);
2132
+ continue;
2133
+ }
2134
+ logger_1.logger.debug(`H: full replace (depth=${chainDepth}) →\n${newCode}`);
2135
+ try {
2136
+ chainTop.replaceWithText(newCode);
2137
+ this._persistChanges(sourceFile);
2138
+ return {
2139
+ success: true,
2140
+ reason: `smart chain applied (${resolvedSegments.length} segments, depth=${chainDepth})`,
2141
+ strategy: "H",
2142
+ lineUpdated: callLine - 1,
2143
+ };
2144
+ }
2145
+ catch (e) {
2146
+ logger_1.logger.warn(`H: replaceWithText failed: ${e.message}`);
2147
+ }
2148
+ }
2149
+ logger_1.logger.debug(`H: all candidates exhausted`);
2150
+ return null;
2151
+ }
2152
+ attemptPartialChainHeal(sourceFile, caller, chainTop, rootReceiver, originalSegments, newSegments, callLine) {
2153
+ if (JSON.stringify(originalSegments) === JSON.stringify(newSegments))
2154
+ return null;
2155
+ const changedIndices = [];
2156
+ for (let i = 0; i < originalSegments.length; i++) {
2157
+ if (JSON.stringify(originalSegments[i]) !== JSON.stringify(newSegments[i])) {
2158
+ changedIndices.push(i);
2159
+ }
2160
+ }
2161
+ if (changedIndices.length === 0 || changedIndices.length > 1)
2162
+ return null;
2163
+ const changedIdx = changedIndices[0];
2164
+ const oldSeg = originalSegments[changedIdx];
2165
+ const newSeg = newSegments[changedIdx];
2166
+ logger_1.logger.debug(`H partial: segment[${changedIdx}] changed: ` +
2167
+ `${JSON.stringify(oldSeg)} → ${JSON.stringify(newSeg)}`);
2168
+ const chainCalls = collectChainCalls(chainTop);
2169
+ if (chainCalls.length <= changedIdx)
2170
+ return null;
2171
+ const targetCallIdx = chainCalls.length - 1 - changedIdx;
2172
+ const targetCall = chainCalls[targetCallIdx];
2173
+ if (!targetCall)
2174
+ return null;
2175
+ // Block H-partial from substituting a literal for an Identifier/PropertyAccess arg.
2176
+ const targetFirstArg = targetCall.getArguments()[0];
2177
+ if (targetFirstArg &&
2178
+ (ts_morph_1.Node.isIdentifier(targetFirstArg) ||
2179
+ ts_morph_1.Node.isPropertyAccessExpression(targetFirstArg))) {
2180
+ logger_1.logger.debug(`H-partial: skip - arg is ${targetFirstArg.getKindName()} ` +
2181
+ `"${targetFirstArg.getText()}" (semantic dispatch owns this)`);
2182
+ return null;
2183
+ }
2184
+ const callReceiver = (() => {
2185
+ const expr = targetCall.getExpression();
2186
+ if (ts_morph_1.Node.isPropertyAccessExpression(expr))
2187
+ return expr.getExpression().getText();
2188
+ return rootReceiver;
2189
+ })();
2190
+ const currentMethodName = (() => {
2191
+ const expr = targetCall.getExpression();
2192
+ if (ts_morph_1.Node.isPropertyAccessExpression(expr))
2193
+ return expr.getName();
2194
+ return "?";
2195
+ })();
2196
+ const newCallText = MethodMapper.build(callReceiver, newSeg);
2197
+ if (newCallText === null)
2198
+ return null;
2199
+ logger_1.logger.debug(`H partial: .${currentMethodName}() → ${newCallText}`);
2200
+ try {
2201
+ targetCall.replaceWithText(newCallText);
2202
+ this._persistChanges(sourceFile);
2203
+ return {
2204
+ success: true,
2205
+ reason: `partial chain heal: segment[${changedIdx}] .${currentMethodName}() → .${newSeg.type}()`,
2206
+ strategy: "H-partial",
2207
+ lineUpdated: callLine - 1,
2208
+ };
2209
+ }
2210
+ catch (e) {
2211
+ logger_1.logger.warn(`H partial: replaceWithText failed: ${e.message}`);
2212
+ return null;
2213
+ }
2214
+ }
2215
+ // ─────────────────────────────────────────────────────────────
2216
+ // STRATEGY F - Chain Collapse
2217
+ // ─────────────────────────────────────────────────────────────
2218
+ strategyChainCollapse(sourceFile, caller, oldSelector, newSelector, oldSem, newSem) {
2219
+ logger_1.logger.debug(`Strategy F: Chain Collapse (upward traversal)`);
2220
+ const startLine = Math.max(1, caller.line - 40);
2221
+ const endLine = Math.min(sourceFile.getEndLineNumber(), caller.line + 5);
2222
+ const candidates = [];
2223
+ for (const call of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
2224
+ const callLine = call.getStartLineNumber();
2225
+ if (callLine < startLine || callLine > endLine)
2226
+ continue;
2227
+ const callSem = extractSemanticFromCall(call);
2228
+ if (!callSem)
2229
+ continue;
2230
+ if (!semanticMatch(callSem, oldSelector, false))
2231
+ continue;
2232
+ if (this.isBlockedByStructuralWall(call, newSem))
2233
+ continue;
2234
+ const { chainTop, rootReceiver, chainDepth } = climbToChainTop(call);
2235
+ const isChained = chainDepth > 0 || isLeafOfChain(call);
2236
+ if (!isChained)
2237
+ continue;
2238
+ const effectiveDepth = chainDepth > 0 ? chainDepth : 1;
2239
+ candidates.push({
2240
+ leafCall: call,
2241
+ chainTop,
2242
+ rootReceiver,
2243
+ chainDepth: effectiveDepth,
2244
+ distance: Math.abs(callLine - caller.line),
2245
+ });
2246
+ }
2247
+ if (candidates.length === 0) {
2248
+ return {
2249
+ success: false,
2250
+ reason: "no chained candidates found",
2251
+ strategy: "F",
2252
+ };
2253
+ }
2254
+ candidates.sort((a, b) => {
2255
+ if (b.chainDepth !== a.chainDepth)
2256
+ return b.chainDepth - a.chainDepth;
2257
+ return a.distance - b.distance;
2258
+ });
2259
+ for (const { leafCall, chainTop, rootReceiver, chainDepth } of candidates) {
2260
+ const newCallText = buildCallText(rootReceiver, newSem);
2261
+ try {
2262
+ chainTop.replaceWithText(newCallText);
2263
+ this._persistChanges(sourceFile);
2264
+ return {
2265
+ success: true,
2266
+ reason: `chain collapsed atomically (depth=${chainDepth})`,
2267
+ strategy: "F",
2268
+ lineUpdated: leafCall.getStartLineNumber() - 1,
2269
+ };
2270
+ }
2271
+ catch (e) {
2272
+ logger_1.logger.warn(`F: replaceWithText failed: ${e.message}`);
2273
+ }
2274
+ }
2275
+ return {
2276
+ success: false,
2277
+ reason: "chain collapse: all candidates threw",
2278
+ strategy: "F",
2279
+ };
2280
+ }
2281
+ // ─────────────────────────────────────────────────────────────
2282
+ // STRATEGY G - Assertion Healing
2283
+ // ─────────────────────────────────────────────────────────────
2284
+ runStrategyG(sourceFile, caller, locatorVarName, contentChange) {
2285
+ if (!contentChange)
2286
+ return null;
2287
+ const callerLine = caller && caller.line > 0 ? caller.line : 1;
2288
+ const windowLines = caller === null || caller.line <= 0 ? 99999 : 40;
2289
+ const assertions = findAssertionsInWindow(sourceFile, locatorVarName || null, callerLine, windowLines);
2290
+ if (assertions.length === 0)
2291
+ return null;
2292
+ for (const assertion of assertions) {
2293
+ if (assertion.currentValue.includes(contentChange.oldText)) {
2294
+ const lineNum = assertion.line;
2295
+ const filePath = sourceFile.getFilePath();
2296
+ logger_1.logger.warn(`\n${"=".repeat(60)}`);
2297
+ logger_1.logger.warn(`⚠️ ASSERTION DRIFT DETECTED (Manual Review Required)`);
2298
+ logger_1.logger.warn(`📍 Location: ${path.basename(filePath)}:${lineNum}`);
2299
+ logger_1.logger.warn(`❌ Current: .toHaveText("${contentChange.oldText}")`);
2300
+ logger_1.logger.warn(`✅ Suggested: .toHaveText("${contentChange.newText}")`);
2301
+ logger_1.logger.warn(`💡 Reason: The element was found, but its content differs.`);
2302
+ logger_1.logger.warn(`${"=".repeat(60)}\n`);
2303
+ return {
2304
+ success: false,
2305
+ reason: `Assertion review required at line ${lineNum}`,
2306
+ strategy: "G",
2307
+ lineUpdated: undefined,
2308
+ };
2309
+ }
2310
+ }
2311
+ return null;
2312
+ }
2313
+ // ─────────────────────────────────────────────────────────────
2314
+ // STRATEGY A - Frame Upstream Healing
2315
+ // ─────────────────────────────────────────────────────────────
2316
+ strategyFrameUpstream(sourceFile, caller, oldElementSelector, aiSegments, fullSelectorContext) {
2317
+ logger_1.logger.debug(`Strategy A: Frame Upstream`);
2318
+ const originalSegments = fullSelectorContext
2319
+ .split(" >> ")
2320
+ .map((s) => s.trim());
2321
+ const frameSegments = aiSegments.filter((s) => s.type === "frame");
2322
+ const changedFrames = [];
2323
+ frameSegments.forEach((aiSeg, i) => {
2324
+ const orig = originalSegments[i];
2325
+ if (orig && aiSeg.selector !== orig) {
2326
+ changedFrames.push({ old: orig, new: aiSeg.selector });
2327
+ }
2328
+ });
2329
+ if (changedFrames.length === 0) {
2330
+ return { success: false, reason: "no frame changes", strategy: "A" };
2331
+ }
2332
+ const lines = sourceFile.getFullText().split(/\r?\n/);
2333
+ const failureLine = lines[caller.line - 1] ?? "";
2334
+ const varMatch = failureLine.match(/^\s*(?:await\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*(?:locator|getBy)/);
2335
+ const varName = varMatch?.[1];
2336
+ if (varName && varName !== "page") {
2337
+ const varDecl = jumpToDefinitionInFile(sourceFile, varName, caller.line);
2338
+ if (varDecl) {
2339
+ const declLine = varDecl.getStartLineNumber();
2340
+ let anyFixed = false;
2341
+ for (const change of changedFrames) {
2342
+ if (this.replaceFrameLocatorArg(varDecl, change.old, change.new)) {
2343
+ anyFixed = true;
2344
+ }
2345
+ }
2346
+ if (anyFixed) {
2347
+ this._persistChanges(sourceFile);
2348
+ return {
2349
+ success: true,
2350
+ reason: `upstream variable '${varName}' fixed`,
2351
+ strategy: "A",
2352
+ lineUpdated: declLine - 1,
2353
+ };
2354
+ }
2355
+ for (const change of changedFrames) {
2356
+ const result = this.replaceStringLiteralInRange(sourceFile, change.old, change.new, declLine, Math.min(sourceFile.getEndLineNumber(), declLine + 8));
2357
+ if (result !== null) {
2358
+ this._persistChanges(sourceFile);
2359
+ return {
2360
+ success: true,
2361
+ reason: "multi-line frame fixed",
2362
+ strategy: "A",
2363
+ lineUpdated: result,
2364
+ };
2365
+ }
2366
+ }
2367
+ }
2368
+ }
2369
+ else {
2370
+ for (const change of changedFrames) {
2371
+ const result = this.replaceStringLiteralInRange(sourceFile, change.old, change.new, Math.max(1, caller.line - 15), caller.line);
2372
+ if (result !== null) {
2373
+ this._persistChanges(sourceFile);
2374
+ return {
2375
+ success: true,
2376
+ reason: "inline frame fixed",
2377
+ strategy: "A",
2378
+ lineUpdated: result,
2379
+ };
2380
+ }
2381
+ }
2382
+ }
2383
+ return {
2384
+ success: false,
2385
+ reason: "frame upstream: no fix applied",
2386
+ strategy: "A",
2387
+ };
2388
+ }
2389
+ // ─────────────────────────────────────────────────────────────
2390
+ // STRATEGY B - Deep Symbol Resolution
2391
+ // ─────────────────────────────────────────────────────────────
2392
+ strategyDeepSymbolResolution(sourceFile, caller, oldSelector, newSelector, oldSem, newSem, suspectIdentifier) {
2393
+ logger_1.logger.debug(`Strategy B: Deep Symbol Resolution`);
2394
+ const failureNode = this.findNodeAtLine(sourceFile, caller.line);
2395
+ if (!failureNode)
2396
+ return {
2397
+ success: false,
2398
+ reason: "no node at failure line",
2399
+ strategy: "B",
2400
+ };
2401
+ const statement = this.getEnclosingStatement(failureNode);
2402
+ if (!statement)
2403
+ return {
2404
+ success: false,
2405
+ reason: "no enclosing statement",
2406
+ strategy: "B",
2407
+ };
2408
+ const usedVarNames = new Set();
2409
+ statement.getDescendantsOfKind(ts_morph_1.SyntaxKind.Identifier).forEach((id) => {
2410
+ const text = id.getText();
2411
+ if (text !== "page" &&
2412
+ text !== "await" &&
2413
+ text !== "expect" &&
2414
+ /^[a-zA-Z_$]/.test(text) &&
2415
+ !ALL_SELECTOR_METHODS.has(text) &&
2416
+ !this.WRAPPER_IDENTIFIERS.has(text)) {
2417
+ usedVarNames.add(text);
2418
+ }
2419
+ });
2420
+ const sortedVars = [...usedVarNames].sort((a, b) => {
2421
+ if (a === suspectIdentifier)
2422
+ return -1;
2423
+ if (b === suspectIdentifier)
2424
+ return 1;
2425
+ return 0;
2426
+ });
2427
+ for (const varName of sortedVars) {
2428
+ const declaration = jumpToDefinitionInFile(sourceFile, varName, caller.line);
2429
+ if (!declaration)
2430
+ continue;
2431
+ const declLine = declaration.getStartLineNumber();
2432
+ const isSuspect = varName === suspectIdentifier;
2433
+ logger_1.logger.debug(`JumpToDef: '${varName}' → line ${declLine} (suspect=${isSuspect})`);
2434
+ const initChain = analyzeVariableInitializerChain(declaration);
2435
+ if (initChain) {
2436
+ const initCalls = initChain.fullChainNode.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression);
2437
+ const hasMatch = initCalls.some((c) => {
2438
+ const cs = extractSemanticFromCall(c);
2439
+ return cs ? semanticMatch(cs, oldSelector, false) : false;
2440
+ });
2441
+ const outerSem = extractSemanticFromCall(initChain.fullChainNode);
2442
+ const outerMatch = outerSem
2443
+ ? semanticMatch(outerSem, oldSelector, false)
2444
+ : false;
2445
+ if (hasMatch || outerMatch) {
2446
+ if (!isSuspect &&
2447
+ this.isBlockedByStructuralWall(initChain.fullChainNode, newSem))
2448
+ continue;
2449
+ const { chainTop, rootReceiver } = climbToChainTop(initChain.fullChainNode);
2450
+ const effectiveReceiver = rootReceiver || initChain.rootReceiver;
2451
+ const newCallText = buildCallText(effectiveReceiver, newSem);
2452
+ try {
2453
+ chainTop.replaceWithText(newCallText);
2454
+ this._persistChanges(sourceFile);
2455
+ const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
2456
+ if (cascadePairs.length > 0) {
2457
+ this._persistChanges(sourceFile);
2458
+ HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
2459
+ }
2460
+ return {
2461
+ success: true,
2462
+ reason: `initializer chain collapsed in '${varName}' at line ${declLine}`,
2463
+ strategy: "B",
2464
+ lineUpdated: declLine - 1,
2465
+ cascadeFixed: cascadePairs.length,
2466
+ };
2467
+ }
2468
+ catch (e) {
2469
+ logger_1.logger.warn(`B initializer-collapse failed: ${e.message}`);
2470
+ }
2471
+ }
2472
+ }
2473
+ const chainResult = this.findMatchingChainTail(declaration, oldSelector, oldSem);
2474
+ if (!chainResult)
2475
+ continue;
2476
+ const { tailCall } = chainResult;
2477
+ const callSem = extractSemanticFromCall(tailCall);
2478
+ if (!isSuspect && this.isBlockedByStructuralWall(tailCall, newSem))
2479
+ continue;
2480
+ if (!isSuspect && callSem) {
2481
+ const exactMethodMatch = callSem.method === oldSem.method &&
2482
+ callSem.primaryArg === oldSem.primaryArg;
2483
+ const leafMatch = (() => {
2484
+ const oldLeaf = oldSelector.split(">>").pop().trim();
2485
+ const oldLeafSem = parseInternalSelector(oldLeaf);
2486
+ return (callSem.method === oldLeafSem.method &&
2487
+ callSem.primaryArg === oldLeafSem.primaryArg);
2488
+ })();
2489
+ if (!exactMethodMatch && !leafMatch)
2490
+ continue;
2491
+ }
2492
+ if (isLeafOfChain(tailCall)) {
2493
+ const { chainTop: tailChainTop, rootReceiver } = climbToChainTop(tailCall);
2494
+ try {
2495
+ tailChainTop.replaceWithText(buildCallText(rootReceiver, newSem));
2496
+ this._persistChanges(sourceFile);
2497
+ const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
2498
+ if (cascadePairs.length > 0) {
2499
+ this._persistChanges(sourceFile);
2500
+ HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
2501
+ }
2502
+ return {
2503
+ success: true,
2504
+ reason: `tail chain collapsed in '${varName}' at line ${declLine}`,
2505
+ strategy: "B",
2506
+ lineUpdated: tailCall.getStartLineNumber() - 1,
2507
+ cascadeFixed: cascadePairs.length,
2508
+ };
2509
+ }
2510
+ catch (e) {
2511
+ logger_1.logger.warn(`B tail-chain-collapse failed: ${e.message}`);
2512
+ }
2513
+ }
2514
+ const fixed = this.replaceChainTailOnly(tailCall, newSem, newSelector);
2515
+ if (fixed) {
2516
+ this._persistChanges(sourceFile);
2517
+ const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
2518
+ if (cascadePairs.length > 0) {
2519
+ this._persistChanges(sourceFile);
2520
+ HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
2521
+ }
2522
+ return {
2523
+ success: true,
2524
+ reason: `chain tail replaced in '${varName}' at line ${declLine}`,
2525
+ strategy: "B",
2526
+ lineUpdated: tailCall.getStartLineNumber() - 1,
2527
+ cascadeFixed: cascadePairs.length,
2528
+ };
2529
+ }
2530
+ }
2531
+ for (const call of statement.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
2532
+ const callSem = extractSemanticFromCall(call);
2533
+ if (!callSem || !semanticMatch(callSem, oldSelector, false))
2534
+ continue;
2535
+ if (this.isBlockedByStructuralWall(call, newSem))
2536
+ continue;
2537
+ const { chainTop: stmtChainTop, rootReceiver, chainDepth, } = climbToChainTop(call);
2538
+ if (chainDepth > 0 || isLeafOfChain(call)) {
2539
+ try {
2540
+ stmtChainTop.replaceWithText(buildCallText(rootReceiver, newSem));
2541
+ this._persistChanges(sourceFile);
2542
+ return {
2543
+ success: true,
2544
+ reason: "direct statement chain collapse",
2545
+ strategy: "B",
2546
+ lineUpdated: caller.line - 1,
2547
+ };
2548
+ }
2549
+ catch { }
2550
+ }
2551
+ if (this.replaceChainTailOnly(call, newSem, newSelector)) {
2552
+ this._persistChanges(sourceFile);
2553
+ const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
2554
+ if (cascadePairs.length > 0) {
2555
+ this._persistChanges(sourceFile);
2556
+ HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
2557
+ }
2558
+ return {
2559
+ success: true,
2560
+ reason: "direct statement fix",
2561
+ strategy: "B",
2562
+ lineUpdated: caller.line - 1,
2563
+ cascadeFixed: cascadePairs.length,
2564
+ };
2565
+ }
2566
+ }
2567
+ return {
2568
+ success: false,
2569
+ reason: "symbol not resolved (semantic-aware)",
2570
+ strategy: "B",
2571
+ };
2572
+ }
2573
+ // ─────────────────────────────────────────────────────────────
2574
+ // STRATEGY C - Inline Semantic Fix
2575
+ // ─────────────────────────────────────────────────────────────
2576
+ strategyInlineSemanticFix(sourceFile, caller, oldSelector, newSelector, oldSem, newSem) {
2577
+ logger_1.logger.debug(`Strategy C: Inline Semantic Fix`);
2578
+ const startLine = Math.max(1, caller.line - 8);
2579
+ const endLine = Math.min(sourceFile.getEndLineNumber(), caller.line + 2);
2580
+ for (const call of this.findCallExpressionsInRange(sourceFile, startLine, endLine, ALL_SELECTOR_METHODS)) {
2581
+ const callSem = extractSemanticFromCall(call);
2582
+ if (!callSem || !semanticMatch(callSem, oldSelector))
2583
+ continue;
2584
+ const { chainTop, rootReceiver, chainDepth } = climbToChainTop(call);
2585
+ if (chainDepth > 0 || isLeafOfChain(call)) {
2586
+ try {
2587
+ chainTop.replaceWithText(buildCallText(rootReceiver, newSem));
2588
+ const fixLine = chainTop.getStartLineNumber();
2589
+ this._persistChanges(sourceFile);
2590
+ const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
2591
+ if (cascadePairs.length > 0) {
2592
+ this._persistChanges(sourceFile);
2593
+ HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
2594
+ }
2595
+ return {
2596
+ success: true,
2597
+ reason: `inline chain collapse at line ${fixLine}`,
2598
+ strategy: "C",
2599
+ lineUpdated: fixLine - 1,
2600
+ cascadeFixed: cascadePairs.length,
2601
+ };
2602
+ }
2603
+ catch { }
2604
+ }
2605
+ if (this.applySemanticFix(call, newSem, newSelector)) {
2606
+ const fixLine = call.getStartLineNumber();
2607
+ this._persistChanges(sourceFile);
2608
+ const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
2609
+ if (cascadePairs.length > 0) {
2610
+ this._persistChanges(sourceFile);
2611
+ HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
2612
+ }
2613
+ return {
2614
+ success: true,
2615
+ reason: `inline fix at line ${fixLine}`,
2616
+ strategy: "C",
2617
+ lineUpdated: fixLine - 1,
2618
+ cascadeFixed: cascadePairs.length,
2619
+ };
2620
+ }
2621
+ }
2622
+ const propAccessResult = this.fixPropertyAccessDefinition(sourceFile, caller, oldSelector, newSelector, oldSem, newSem);
2623
+ if (propAccessResult.success)
2624
+ return propAccessResult;
2625
+ return {
2626
+ success: false,
2627
+ reason: "no inline semantic match",
2628
+ strategy: "C",
2629
+ };
2630
+ }
2631
+ // ─────────────────────────────────────────────────────────────
2632
+ // STRATEGY D - Template Literal
2633
+ // ─────────────────────────────────────────────────────────────
2634
+ strategyTemplateLiteral(sourceFile, caller, oldSelector, newSelector) {
2635
+ logger_1.logger.debug(`Strategy D: Template Literal`);
2636
+ const oldAttrMatch = oldSelector.match(/\[([a-zA-Z-]+)="([^"]*)"\]/);
2637
+ if (!oldAttrMatch)
2638
+ return {
2639
+ success: false,
2640
+ reason: "oldSelector has no attribute",
2641
+ strategy: "D",
2642
+ };
2643
+ const [, attrName, attrValue] = oldAttrMatch;
2644
+ const newLeaf = newSelector.split(">>").pop().trim();
2645
+ const newAttrMatch = newLeaf.match(/\[([a-zA-Z-]+)="([^"]*)"\]/);
2646
+ if (!newAttrMatch || newAttrMatch[1] !== attrName) {
2647
+ return {
2648
+ success: false,
2649
+ reason: "attribute name mismatch",
2650
+ strategy: "D",
2651
+ };
2652
+ }
2653
+ for (const tmpl of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.TemplateExpression)) {
2654
+ const fullText = tmpl.getFullText();
2655
+ if (!fullText.includes(attrName))
2656
+ continue;
2657
+ const staticSuffixMatch = fullText.match(/\$\{[^}]+\}([^"'`\]]*)\]/);
2658
+ if (!staticSuffixMatch)
2659
+ continue;
2660
+ const oldStaticPart = staticSuffixMatch[1];
2661
+ const varPart = attrValue.replace(new RegExp(oldStaticPart + "$"), "");
2662
+ const newAttrValue = newAttrMatch[2];
2663
+ const newStaticPart = newAttrValue.startsWith(varPart)
2664
+ ? newAttrValue.slice(varPart.length)
2665
+ : "_" + newAttrValue.split("_").pop();
2666
+ if (oldStaticPart === newStaticPart)
2667
+ continue;
2668
+ const escapedOld = oldStaticPart.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2669
+ const newText = fullText.replace(new RegExp(`(\\\$\\{[^}]+\\})${escapedOld}(\\])`), `$1${newStaticPart}$2`);
2670
+ if (newText !== fullText) {
2671
+ tmpl.replaceWithText(newText.trim());
2672
+ const line = tmpl.getStartLineNumber();
2673
+ this._persistChanges(sourceFile);
2674
+ return {
2675
+ success: true,
2676
+ reason: "template literal suffix updated",
2677
+ strategy: "D",
2678
+ lineUpdated: line - 1,
2679
+ };
2680
+ }
2681
+ }
2682
+ return {
2683
+ success: false,
2684
+ reason: "no matching template literal",
2685
+ strategy: "D",
2686
+ };
2687
+ }
2688
+ // ─────────────────────────────────────────────────────────────
2689
+ // STRATEGY E - Semantic Fragment Scan
2690
+ // ─────────────────────────────────────────────────────────────
2691
+ strategySemanticFragmentScan(sourceFile, caller, oldSelector, newSelector, oldSem, newSem) {
2692
+ logger_1.logger.debug(`Strategy E: Semantic Fragment Scan`);
2693
+ const candidates = [];
2694
+ for (const call of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
2695
+ const callSem = extractSemanticFromCall(call);
2696
+ if (!callSem || !semanticMatch(callSem, oldSelector, true))
2697
+ continue;
2698
+ candidates.push({
2699
+ call,
2700
+ callSem,
2701
+ distance: Math.abs(call.getStartLineNumber() - caller.line),
2702
+ });
2703
+ }
2704
+ if (candidates.length === 0)
2705
+ return {
2706
+ success: false,
2707
+ reason: "no fragment candidates",
2708
+ strategy: "E",
2709
+ };
2710
+ candidates.sort((a, b) => a.distance - b.distance);
2711
+ for (const { call, callSem } of candidates) {
2712
+ if (this.isBlockedByStructuralWall(call, newSem))
2713
+ continue;
2714
+ const { chainTop, rootReceiver, chainDepth } = climbToChainTop(call);
2715
+ if (chainDepth > 0 || isLeafOfChain(call)) {
2716
+ try {
2717
+ chainTop.replaceWithText(buildCallText(rootReceiver, newSem));
2718
+ this._persistChanges(sourceFile);
2719
+ return {
2720
+ success: true,
2721
+ reason: `fragment scan chain collapse .${callSem.method}() @ line ${call.getStartLineNumber()}`,
2722
+ strategy: "E",
2723
+ lineUpdated: call.getStartLineNumber() - 1,
2724
+ };
2725
+ }
2726
+ catch { }
2727
+ }
2728
+ if (this.replaceChainTailOnly(call, newSem, newSelector)) {
2729
+ this._persistChanges(sourceFile);
2730
+ return {
2731
+ success: true,
2732
+ reason: `fragment scan fixed .${callSem.method}() @ line ${call.getStartLineNumber()}`,
2733
+ strategy: "E",
2734
+ lineUpdated: call.getStartLineNumber() - 1,
2735
+ };
2736
+ }
2737
+ }
2738
+ return {
2739
+ success: false,
2740
+ reason: "fragment scan: all candidates blocked",
2741
+ strategy: "E",
2742
+ };
2743
+ }
2744
+ // ═══════════════════════════════════════════════════════════════
2745
+ // CHAIN HELPERS
2746
+ // ═══════════════════════════════════════════════════════════════
2747
+ findMatchingChainTail(decl, oldSelector, oldSem) {
2748
+ for (const call of decl.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
2749
+ const callSem = extractSemanticFromCall(call);
2750
+ if (!callSem || !semanticMatch(callSem, oldSelector, false))
2751
+ continue;
2752
+ let outerCall = call;
2753
+ let parent = call.getParent();
2754
+ while (parent) {
2755
+ if (ts_morph_1.Node.isCallExpression(parent)) {
2756
+ outerCall = parent;
2757
+ parent = parent.getParent();
2758
+ }
2759
+ else if (ts_morph_1.Node.isPropertyAccessExpression(parent) ||
2760
+ ts_morph_1.Node.isAwaitExpression(parent)) {
2761
+ parent = parent.getParent();
2762
+ }
2763
+ else {
2764
+ break;
2765
+ }
2766
+ }
2767
+ return { outerCall, tailCall: call };
2768
+ }
2769
+ return null;
2770
+ }
2771
+ replaceChainTailOnly(tailCall, newSem, _newSelector) {
2772
+ const expr = tailCall.getExpression();
2773
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
2774
+ return false;
2775
+ const receiver = expr.getExpression().getText();
2776
+ const newCallText = buildCallText(receiver, newSem);
2777
+ try {
2778
+ tailCall.replaceWithText(newCallText);
2779
+ return true;
2780
+ }
2781
+ catch (e) {
2782
+ logger_1.logger.warn(`replaceChainTailOnly failed: ${e.message}`);
2783
+ return false;
2784
+ }
2785
+ }
2786
+ // ═══════════════════════════════════════════════════════════════
2787
+ // SEMANTIC CORE OPERATIONS
2788
+ // ═══════════════════════════════════════════════════════════════
2789
+ applySemanticFix(callExpr, newSem, newSelector) {
2790
+ const expr = callExpr.getExpression();
2791
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
2792
+ return false;
2793
+ const currentMethod = expr.getName();
2794
+ const receiver = expr.getExpression().getText();
2795
+ if (currentMethod === newSem.method)
2796
+ return this.replaceCallArgument(callExpr, newSem);
2797
+ const newCallText = buildCallText(receiver, newSem);
2798
+ try {
2799
+ callExpr.replaceWithText(newCallText);
2800
+ return true;
2801
+ }
2802
+ catch (e) {
2803
+ logger_1.logger.warn(`applySemanticFix failed: ${e.message}`);
2804
+ return false;
2805
+ }
2806
+ }
2807
+ /**
2808
+ * Guard: returns false when replacing oldNode with proposedNewText would
2809
+ * substitute a hardcoded string literal for an Identifier or
2810
+ * PropertyAccessExpression - which is the definition-site mutation bug.
2811
+ * All other replacements are allowed.
2812
+ */
2813
+ static isSafeReplacement(oldNode, proposedNewText) {
2814
+ if (ts_morph_1.Node.isIdentifier(oldNode) ||
2815
+ ts_morph_1.Node.isPropertyAccessExpression(oldNode)) {
2816
+ return !/^["'`]/.test(proposedNewText.trim());
2817
+ }
2818
+ return true;
2819
+ }
2820
+ replaceCallArgument(callExpr, newSem) {
2821
+ try {
2822
+ const args = callExpr.getArguments();
2823
+ if (args.length === 0)
2824
+ return false;
2825
+ const firstArg = args[0];
2826
+ const safe = newSem.primaryArg.replace(/"/g, "'");
2827
+ if (ts_morph_1.Node.isStringLiteral(firstArg)) {
2828
+ firstArg.setLiteralValue(safe);
2829
+ }
2830
+ else if (ASTSourceUpdater.isSafeReplacement(firstArg, `"${safe}"`)) {
2831
+ firstArg.replaceWithText(`"${safe}"`);
2832
+ }
2833
+ else {
2834
+ logger_1.logger.warn(`replaceCallArgument: blocked unsafe replacement - ` +
2835
+ `arg is ${firstArg.getKindName()}, not a string literal. ` +
2836
+ `Heal at definition site instead.`);
2837
+ return false;
2838
+ }
2839
+ if (newSem.options && Object.keys(newSem.options).length > 0) {
2840
+ const optionsStr = Object.entries(newSem.options)
2841
+ .map(([k, v]) => typeof v === "string" ? `${k}: "${v}"` : `${k}: ${v}`)
2842
+ .join(", ");
2843
+ if (args.length >= 2)
2844
+ args[1].replaceWithText(`{ ${optionsStr} }`);
2845
+ else
2846
+ callExpr.addArgument(`{ ${optionsStr} }`);
2847
+ }
2848
+ return true;
2849
+ }
2850
+ catch (e) {
2851
+ logger_1.logger.warn(`replaceCallArgument failed: ${e.message}`);
2852
+ return false;
2853
+ }
2854
+ }
2855
+ semanticToInternal(sem) {
2856
+ switch (sem.method) {
2857
+ case "getByLabel":
2858
+ return `internal:label=${JSON.stringify(sem.primaryArg)}`;
2859
+ case "getByRole": {
2860
+ const namePart = sem.options?.name
2861
+ ? `[name="${sem.options.name}"i]`
2862
+ : "";
2863
+ return `internal:role=${sem.primaryArg}${namePart}`;
2864
+ }
2865
+ case "getByPlaceholder":
2866
+ return `internal:attr=[placeholder=${JSON.stringify(sem.primaryArg)}]`;
2867
+ case "getByText": {
2868
+ const exact = sem.options?.exact ? "" : "i";
2869
+ return `internal:text=${JSON.stringify(sem.primaryArg)}${exact}`;
2870
+ }
2871
+ case "getByAltText":
2872
+ return `internal:attr=[alt=${JSON.stringify(sem.primaryArg)}]`;
2873
+ case "getByTitle":
2874
+ return `internal:attr=[title=${JSON.stringify(sem.primaryArg)}]`;
2875
+ case "getByTestId":
2876
+ return `[data-testid="${sem.primaryArg}"]`;
2877
+ default:
2878
+ return sem.primaryArg;
2879
+ }
2880
+ }
2881
+ performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem) {
2882
+ const fixed = [];
2883
+ for (const call of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
2884
+ try {
2885
+ const callSem = extractSemanticFromCall(call);
2886
+ if (!callSem || !semanticMatch(callSem, oldSelector, true))
2887
+ continue;
2888
+ const callInternal = this.semanticToInternal(callSem);
2889
+ if (this.applySemanticFix(call, newSem, newSelector)) {
2890
+ fixed.push({ oldSelector: callInternal ?? oldSelector, newSelector });
2891
+ }
2892
+ }
2893
+ catch { }
2894
+ }
2895
+ return fixed;
2896
+ }
2897
+ fixPropertyAccessDefinition(sourceFile, caller, oldSelector, newSelector, oldSem, newSem) {
2898
+ for (const callExpr of this.findCallExpressionsInRange(sourceFile, Math.max(1, caller.line - 5), caller.line + 2, ALL_SELECTOR_METHODS)) {
2899
+ const firstArg = callExpr.getArguments()[0];
2900
+ if (!firstArg || !ts_morph_1.Node.isPropertyAccessExpression(firstArg))
2901
+ continue;
2902
+ const objName = firstArg.getExpression().getText();
2903
+ const propName = firstArg.getName();
2904
+ const result = this.updateObjectProperty(sourceFile, objName, propName, oldSem.primaryArg, newSem.primaryArg);
2905
+ if (result !== null) {
2906
+ this._persistChanges(sourceFile);
2907
+ return {
2908
+ success: true,
2909
+ reason: `'${objName}.${propName}' updated`,
2910
+ strategy: "C",
2911
+ lineUpdated: result,
2912
+ };
2913
+ }
2914
+ }
2915
+ return { success: false, reason: "no property access", strategy: "C" };
2916
+ }
2917
+ isBlockedByStructuralWall(currentCall, newSem) {
2918
+ const newIsInteractive = (newSem.method === "getByRole" &&
2919
+ this.INTERACTIVE_ROLES.has(newSem.primaryArg.toLowerCase())) ||
2920
+ newSem.method === "getByLabel" ||
2921
+ newSem.method === "getByPlaceholder" ||
2922
+ (newSem.method === "locator" &&
2923
+ (newSem.primaryArg.startsWith("#") ||
2924
+ newSem.primaryArg.includes("input") ||
2925
+ newSem.primaryArg.includes("textarea")));
2926
+ if (!newIsInteractive)
2927
+ return false;
2928
+ const callSem = extractSemanticFromCall(currentCall);
2929
+ if (!callSem)
2930
+ return false;
2931
+ if (callSem.method === "getByRole" &&
2932
+ this.STRUCTURAL_ROLES.has(callSem.primaryArg.toLowerCase())) {
2933
+ logger_1.logger.warn(`Structural Wall BLOCKED: ` +
2934
+ `.getByRole("${callSem.primaryArg}") is structural, ` +
2935
+ `refusing to replace with ${newSem.method}("${newSem.primaryArg}")`);
2936
+ return true;
2937
+ }
2938
+ return false;
2939
+ }
2940
+ // ═══════════════════════════════════════════════════════════════
2941
+ // AST NAVIGATION HELPERS
2942
+ // ═══════════════════════════════════════════════════════════════
2943
+ identifySuspectIdentifier(statement, line) {
2944
+ const callsOnLine = statement
2945
+ .getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)
2946
+ .filter((c) => c.getStartLineNumber() <= line && c.getEndLineNumber() >= line);
2947
+ if (callsOnLine.length === 0)
2948
+ return null;
2949
+ let bestIdentifier = null;
2950
+ let bestDepth = -1;
2951
+ for (const call of callsOnLine) {
2952
+ const expr = call.getExpression();
2953
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
2954
+ continue;
2955
+ let current = expr.getExpression();
2956
+ let depth = 0;
2957
+ while (ts_morph_1.Node.isCallExpression(current)) {
2958
+ const inner = current.getExpression();
2959
+ if (!ts_morph_1.Node.isPropertyAccessExpression(inner))
2960
+ break;
2961
+ current = inner.getExpression();
2962
+ depth++;
2963
+ }
2964
+ while (ts_morph_1.Node.isPropertyAccessExpression(current)) {
2965
+ current = current.getExpression();
2966
+ depth++;
2967
+ }
2968
+ if (!ts_morph_1.Node.isIdentifier(current))
2969
+ continue;
2970
+ const name = current.getText();
2971
+ if (this.WRAPPER_IDENTIFIERS.has(name))
2972
+ continue;
2973
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name))
2974
+ continue;
2975
+ if (depth > bestDepth) {
2976
+ bestDepth = depth;
2977
+ bestIdentifier = name;
2978
+ }
2979
+ }
2980
+ return bestIdentifier;
2981
+ }
2982
+ getEnclosingStatement(node) {
2983
+ return (node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.ExpressionStatement) ??
2984
+ node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.VariableStatement) ??
2985
+ node.getParentWhile((n) => !ts_morph_1.Node.isBlock(n) && !ts_morph_1.Node.isSourceFile(n)));
2986
+ }
2987
+ findNodeAtLine(sourceFile, line) {
2988
+ const targetOffset = this.lineToOffset(sourceFile, line);
2989
+ const endOffset = this.lineToOffset(sourceFile, line + 1);
2990
+ return sourceFile.getDescendants().find((n) => {
2991
+ const start = n.getStart();
2992
+ return start >= targetOffset && start < endOffset;
2993
+ });
2994
+ }
2995
+ findCallExpressionsInRange(sourceFile, startLine, endLine, methodNames) {
2996
+ return sourceFile
2997
+ .getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)
2998
+ .filter((call) => {
2999
+ const line = call.getStartLineNumber();
3000
+ if (line < startLine || line > endLine)
3001
+ return false;
3002
+ const expr = call.getExpression();
3003
+ return (ts_morph_1.Node.isPropertyAccessExpression(expr) &&
3004
+ methodNames.has(expr.getName()));
3005
+ });
3006
+ }
3007
+ replaceFrameLocatorArg(varDecl, oldFrame, newFrame) {
3008
+ for (const call of varDecl.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
3009
+ const expr = call.getExpression();
3010
+ if (!ts_morph_1.Node.isPropertyAccessExpression(expr) ||
3011
+ expr.getName() !== "frameLocator")
3012
+ continue;
3013
+ const arg = call.getArguments()[0];
3014
+ if (arg &&
3015
+ ts_morph_1.Node.isStringLiteral(arg) &&
3016
+ arg.getLiteralValue() === oldFrame) {
3017
+ arg.setLiteralValue(newFrame);
3018
+ return true;
3019
+ }
3020
+ }
3021
+ return false;
3022
+ }
3023
+ replaceStringLiteralInRange(sourceFile, oldVal, newVal, startLine, endLine) {
3024
+ const startOffset = this.lineToOffset(sourceFile, startLine);
3025
+ const endOffset = this.lineToOffset(sourceFile, endLine + 1);
3026
+ for (const node of [
3027
+ ...sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.StringLiteral),
3028
+ ...sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral),
3029
+ ]) {
3030
+ const offset = node.getStart();
3031
+ if (offset < startOffset || offset > endOffset)
3032
+ continue;
3033
+ const val = node.getLiteralValue?.() ??
3034
+ node.getLiteralValue?.() ??
3035
+ "";
3036
+ if (val === oldVal) {
3037
+ if (ts_morph_1.Node.isStringLiteral(node)) {
3038
+ node.setLiteralValue(newVal.replace(/"/g, "'"));
3039
+ }
3040
+ else {
3041
+ node.replaceWithText(`\`${newVal}\``);
3042
+ }
3043
+ return node.getStartLineNumber() - 1;
3044
+ }
3045
+ }
3046
+ return null;
3047
+ }
3048
+ updateObjectProperty(sourceFile, objName, propName, oldValue, newValue) {
3049
+ for (const decl of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration)) {
3050
+ if (decl.getName() !== objName)
3051
+ continue;
3052
+ const init = decl.getInitializer();
3053
+ if (!init || !ts_morph_1.Node.isObjectLiteralExpression(init))
3054
+ continue;
3055
+ for (const prop of init.getProperties()) {
3056
+ if (!ts_morph_1.Node.isPropertyAssignment(prop) || prop.getName() !== propName)
3057
+ continue;
3058
+ const valueNode = prop.getInitializer();
3059
+ if (!valueNode || !ts_morph_1.Node.isStringLiteral(valueNode))
3060
+ continue;
3061
+ if (valueNode.getLiteralValue() === oldValue) {
3062
+ valueNode.setLiteralValue(newValue.replace(/"/g, "'"));
3063
+ return valueNode.getStartLineNumber() - 1;
3064
+ }
3065
+ }
3066
+ }
3067
+ return null;
3068
+ }
3069
+ lineToOffset(sourceFile, line) {
3070
+ const text = sourceFile.getFullText();
3071
+ const lines = text.split("\n");
3072
+ let offset = 0;
3073
+ for (let i = 0; i < Math.min(line - 1, lines.length); i++) {
3074
+ offset += lines[i].length + 1;
3075
+ }
3076
+ return offset;
3077
+ }
3078
+ resolveFilePath(raw) {
3079
+ if (!raw || typeof raw !== "string")
3080
+ return null;
3081
+ let clean = raw
3082
+ .replace(/^.*?at\s+/, "")
3083
+ .replace(/:\d+:\d+.*$/, "")
3084
+ .trim();
3085
+ if (!clean)
3086
+ return null;
3087
+ if (!path.isAbsolute(clean))
3088
+ clean = path.resolve(process.cwd(), clean);
3089
+ if (!fs.existsSync(clean))
3090
+ return null;
3091
+ const stat = fs.statSync(clean);
3092
+ if (!stat.isFile())
3093
+ return null;
3094
+ return clean;
3095
+ }
3096
+ // ═══════════════════════════════════════════════════════════════════
3097
+ // T-10 / T-11: Semantic Dispatch Gate + handlers
3098
+ // ═══════════════════════════════════════════════════════════════════
3099
+ semanticDispatch(sourceFile, caller, oldSelector, newSelector) {
3100
+ try {
3101
+ // Expand window: the failure site (caller.line) is often .click()/.fill()
3102
+ // AFTER the locator assignment. Search up to 5 lines back so we catch
3103
+ // page.locator(IDENTIFIER) even when it lives on the previous line.
3104
+ const calls = this.findCallExpressionsInRange(sourceFile, Math.max(1, caller.line - 5), caller.line, ALL_SELECTOR_METHODS);
3105
+ for (const call of calls) {
3106
+ const analysis = ArgumentTypeAnalyzer_1.ArgumentTypeAnalyzer.classify(call);
3107
+ if (!analysis)
3108
+ continue;
3109
+ const { shape, node } = analysis;
3110
+ if (shape === "STRING_LITERAL" || shape === "COMPLEX")
3111
+ continue;
3112
+ let result = null;
3113
+ if (shape === "IDENTIFIER") {
3114
+ result = this.handleIdentifierArg(node, sourceFile, caller, newSelector);
3115
+ }
3116
+ else if (shape === "PROPERTY_ACCESS") {
3117
+ result = this.handlePropertyAccessArg(node, sourceFile, caller, newSelector);
3118
+ }
3119
+ else if (shape === "TEMPLATE_LITERAL") {
3120
+ result = this.handleTemplateLiteralArg(node, sourceFile, caller, oldSelector, newSelector);
3121
+ }
3122
+ else if (shape === "CALL_EXPRESSION") {
3123
+ result = this.handleCallExpressionArg(node, sourceFile, oldSelector, newSelector);
3124
+ }
3125
+ // Hard exit: any result (success OR failure) from a definition-site shape
3126
+ // stops all subsequent strategies. Returning non-null prevents NT, H, and
3127
+ // H-partial from touching the call site when the arg is a symbolic reference.
3128
+ if (result !== null)
3129
+ return result;
3130
+ // Handler returned null (could not trace). For definition-site shapes,
3131
+ // return a hard-block failure so NT/H cannot overwrite the identifier.
3132
+ if (shape === "IDENTIFIER" ||
3133
+ shape === "PROPERTY_ACCESS" ||
3134
+ shape === "TEMPLATE_LITERAL") {
3135
+ logger_1.logger.debug(`hard-block: "${node.getText()}" is ${shape} ` +
3136
+ `but trace failed - preserving call site`);
3137
+ return {
3138
+ success: false,
3139
+ reason: `semantic guard: could not trace "${node.getText()}" - call site preserved`,
3140
+ strategy: "semantic-guard",
3141
+ };
3142
+ }
3143
+ }
3144
+ return null;
3145
+ }
3146
+ catch (e) {
3147
+ logger_1.logger.warn(`fell through: ${e.message}`);
3148
+ return null;
3149
+ }
3150
+ }
3151
+ // ═══════════════════════════════════════════════════════════════════
3152
+ // DEFINITION-SITE WRITE (with internal→public Method-Shift fallback)
3153
+ //
3154
+ // A `const X = "selector"` consumed by `.locator(X)` cannot hold a semantic
3155
+ // locator (page.getByRole(...) would reference `page` out of scope at the
3156
+ // declaration). So when the AI heals to an internal engine selector
3157
+ // (`internal:role=…`), we DON'T abort and we DON'T leak the engine string:
3158
+ // we translate it to a VALID PUBLIC selector string (`role=button[name="…"]`)
3159
+ // and write THAT into the constant — keeping the developer's architecture
3160
+ // intact — while surfacing the cleaner semantic method form (getByRole) as
3161
+ // a suggested upgrade in the advisory/report.
3162
+ // ═══════════════════════════════════════════════════════════════════
3163
+ applyDefinitionSiteHeal(targetNode, targetFile, newSelector, strategy, blastRadius) {
3164
+ const wasInternal = (0, SelectorSanitizer_1.isInternalEngineSelector)(newSelector);
3165
+ let writeValue = newSelector;
3166
+ if (wasInternal) {
3167
+ const publicStr = (0, SelectorSanitizer_1.internalToPublicSelectorString)(newSelector);
3168
+ if (!publicStr) {
3169
+ // No public string engine (e.g. label association) → a .locator()
3170
+ // string constant genuinely cannot express it. Preserve the call site.
3171
+ return {
3172
+ success: false,
3173
+ reason: `semantic guard: internal selector "${newSelector.slice(0, 48)}" has no public ` +
3174
+ `.locator() string form - call site preserved (method-shift required)`,
3175
+ strategy: "semantic-guard",
3176
+ };
3177
+ }
3178
+ writeValue = publicStr;
3179
+ logger_1.logger.debug(`Method-Shift fallback: internal selector → public string "${writeValue}"`);
3180
+ }
3181
+ const result = InitializerUpdater_1.InitializerUpdater.apply(targetNode, targetFile, writeValue);
3182
+ if (!result)
3183
+ return null;
3184
+ if (!this.validateFileSyntax(targetFile)) {
3185
+ return {
3186
+ success: false,
3187
+ reason: "post-mutation syntax validation failed - reverted",
3188
+ strategy,
3189
+ };
3190
+ }
3191
+ if (wasInternal) {
3192
+ const methodExpr = (0, SelectorSanitizer_1.internalToMethodExpression)(newSelector, "page");
3193
+ this.advisoryBuffer.push({
3194
+ code: HealingAdvisory_1.AdvisoryCode.SEMANTIC_UPGRADE_SUGGESTED,
3195
+ message: `Healed constant to public selector string "${writeValue}". ` +
3196
+ `Cleaner semantic form: ${methodExpr ?? "a getBy* method"}.`,
3197
+ affectedFile: targetFile.getFilePath(),
3198
+ affectedLine: result.lineUpdated,
3199
+ suggestedAction: `For more resilient code, rewrite the call site to ${methodExpr ?? "the semantic locator"} ` +
3200
+ `instead of the string constant.`,
3201
+ });
3202
+ }
3203
+ return {
3204
+ success: true,
3205
+ reason: `definition-site heal via ${strategy} trace` +
3206
+ (wasInternal ? " (internal→public string; semantic upgrade suggested)" : ""),
3207
+ strategy,
3208
+ lineUpdated: result.lineUpdated,
3209
+ healedLocatorString: result.healedLocatorString,
3210
+ definitionSite: {
3211
+ file: targetFile.getFilePath(),
3212
+ line: result.lineUpdated,
3213
+ },
3214
+ blastRadius,
3215
+ };
3216
+ }
3217
+ handleIdentifierArg(node, sourceFile, caller, newSelector) {
3218
+ const traceResult = DefinitionTracer_1.DefinitionTracer.traceIdentifier(node, sourceFile, caller.line);
3219
+ if (traceResult.found) {
3220
+ const { targetNode, targetFile, declarationNode } = traceResult;
3221
+ const blastNode = declarationNode ?? targetNode;
3222
+ const blast = BlastRadiusAnalyzer_1.BlastRadiusAnalyzer.analyze(blastNode, sourceFile, this.project);
3223
+ logger_1.logger.debug(`BlastRadius IDENTIFIER: risk=${blast.risk} ` +
3224
+ `(${blast.testFunctionCount} tests, ${blast.affectedFiles.length} files)`);
3225
+ if (blast.risk === "high") {
3226
+ this.advisoryBuffer.push({
3227
+ code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_BLOCKED,
3228
+ message: `Mutation blocked - ${blast.testFunctionCount} tests reference this constant.`,
3229
+ affectedFile: targetFile.getFilePath(),
3230
+ affectedLine: targetNode.getStartLineNumber() - 1,
3231
+ suggestedAction: "Review all referencing tests before changing this constant.",
3232
+ });
3233
+ return {
3234
+ success: false,
3235
+ reason: `BlastRadius BLOCKED: ${blast.testFunctionCount} tests reference this constant - manual review required`,
3236
+ strategy: "semantic-identifier",
3237
+ };
3238
+ }
3239
+ if (blast.risk === "medium") {
3240
+ this.advisoryBuffer.push({
3241
+ code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_WARNING,
3242
+ message: `Mutated constant used in ${blast.testFunctionCount} tests - verify no regressions.`,
3243
+ affectedFile: targetFile.getFilePath(),
3244
+ affectedLine: targetNode.getStartLineNumber() - 1,
3245
+ suggestedAction: "Run the full test suite to confirm all references still pass.",
3246
+ });
3247
+ }
3248
+ return this.applyDefinitionSiteHeal(targetNode, targetFile, newSelector, "semantic-identifier", blast.testFunctionCount);
3249
+ }
3250
+ if (!traceResult.found && traceResult.crossFile) {
3251
+ const xResult = CrossFileHealer_1.CrossFileHealer.heal(traceResult.modulePath, traceResult.symbolName, newSelector, caller.filePath, this.project);
3252
+ if (xResult.success) {
3253
+ this.advisoryBuffer.push({
3254
+ code: HealingAdvisory_1.AdvisoryCode.CROSS_FILE_MUTATION,
3255
+ message: `Definition-site mutation crossed file boundary to "${traceResult.modulePath}".`,
3256
+ affectedFile: caller.filePath,
3257
+ suggestedAction: "Verify no other callers of this symbol were affected.",
3258
+ });
3259
+ }
3260
+ return xResult;
3261
+ }
3262
+ return null;
3263
+ }
3264
+ handlePropertyAccessArg(node, sourceFile, caller, newSelector) {
3265
+ const traceResult = DefinitionTracer_1.DefinitionTracer.tracePropertyAccess(node, sourceFile, caller.line);
3266
+ if (traceResult.found) {
3267
+ const { targetNode, targetFile, declarationNode } = traceResult;
3268
+ const blastNode = declarationNode ?? targetNode;
3269
+ const blast = BlastRadiusAnalyzer_1.BlastRadiusAnalyzer.analyze(blastNode, sourceFile, this.project);
3270
+ logger_1.logger.debug(`BlastRadius PROPERTY_ACCESS: risk=${blast.risk} ` +
3271
+ `(${blast.testFunctionCount} tests, ${blast.affectedFiles.length} files)`);
3272
+ if (blast.risk === "high") {
3273
+ this.advisoryBuffer.push({
3274
+ code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_BLOCKED,
3275
+ message: `Mutation blocked - ${blast.testFunctionCount} tests reference this property.`,
3276
+ affectedFile: targetFile.getFilePath(),
3277
+ affectedLine: targetNode.getStartLineNumber() - 1,
3278
+ suggestedAction: "Review all referencing tests before changing this property.",
3279
+ });
3280
+ return {
3281
+ success: false,
3282
+ reason: `BlastRadius BLOCKED: ${blast.testFunctionCount} tests reference this property - manual review required`,
3283
+ strategy: "semantic-property-access",
3284
+ };
3285
+ }
3286
+ if (blast.risk === "medium") {
3287
+ this.advisoryBuffer.push({
3288
+ code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_WARNING,
3289
+ message: `Mutated property used in ${blast.testFunctionCount} tests - verify no regressions.`,
3290
+ affectedFile: targetFile.getFilePath(),
3291
+ affectedLine: targetNode.getStartLineNumber() - 1,
3292
+ suggestedAction: "Run the full test suite to confirm all references still pass.",
3293
+ });
3294
+ }
3295
+ return this.applyDefinitionSiteHeal(targetNode, targetFile, newSelector, "semantic-property-access", blast.testFunctionCount);
3296
+ }
3297
+ if (!traceResult.found && traceResult.crossFile) {
3298
+ const xResult = CrossFileHealer_1.CrossFileHealer.heal(traceResult.modulePath, traceResult.symbolName, newSelector, caller.filePath, this.project);
3299
+ if (xResult.success) {
3300
+ this.advisoryBuffer.push({
3301
+ code: HealingAdvisory_1.AdvisoryCode.CROSS_FILE_MUTATION,
3302
+ message: `Definition-site mutation crossed file boundary to "${traceResult.modulePath}".`,
3303
+ affectedFile: caller.filePath,
3304
+ suggestedAction: "Verify no other callers of this symbol were affected.",
3305
+ });
3306
+ }
3307
+ return xResult;
3308
+ }
3309
+ return null;
3310
+ }
3311
+ handleTemplateLiteralArg(node, sourceFile, caller, oldSelector, newSelector) {
3312
+ if (!ts_morph_1.Node.isTemplateExpression(node))
3313
+ return null;
3314
+ const patchResult = TemplateDiffService_1.TemplateDiffService.computeNodePatch(node, oldSelector, newSelector);
3315
+ if (!patchResult.ok) {
3316
+ logger_1.logger.debug(`TemplateDiff failed: ${patchResult.reason}`);
3317
+ // Structural simplification fallback: AI proposed a selector that dropped
3318
+ // or rewrote a dynamic span entirely. Try to derive what each span
3319
+ // expression needs to evaluate to, then trace that span back to its
3320
+ // definition and apply the sub-heal there.
3321
+ if (patchResult.newSpanValues) {
3322
+ const templateNode = node;
3323
+ const oldSpanValues = TemplateDiffService_1.TemplateDiffService.extractNodeSpanValues(templateNode, oldSelector);
3324
+ const spans = templateNode.getTemplateSpans();
3325
+ if (oldSpanValues &&
3326
+ oldSpanValues.length === patchResult.newSpanValues.length &&
3327
+ spans.length === oldSpanValues.length) {
3328
+ logger_1.logger.debug(`Template structural fallback: tracing ${spans.length} span(s)`);
3329
+ for (let i = 0; i < spans.length; i++) {
3330
+ const spanExpr = spans[i].getExpression();
3331
+ const traced = this.traceSpanExpression(spanExpr, sourceFile, caller.line, oldSpanValues[i], patchResult.newSpanValues[i]);
3332
+ if (traced)
3333
+ return traced;
3334
+ }
3335
+ }
3336
+ }
3337
+ if (patchResult.reason.includes("structural") ||
3338
+ patchResult.reason.includes("not found")) {
3339
+ this.advisoryBuffer.push({
3340
+ code: HealingAdvisory_1.AdvisoryCode.MANUAL_REVIEW_TEMPLATE_COMPLEX,
3341
+ message: `Template literal requires structural change - automated patch skipped.`,
3342
+ affectedFile: sourceFile.getFilePath(),
3343
+ affectedLine: node.getStartLineNumber() - 1,
3344
+ suggestedAction: "Manually update the template literal and its callers.",
3345
+ });
3346
+ }
3347
+ return null;
3348
+ }
3349
+ const lineUpdated = node.getStartLineNumber() - 1;
3350
+ node.replaceWithText(patchResult.patchedTemplate);
3351
+ this._persistChanges(sourceFile);
3352
+ if (!this.validateFileSyntax(sourceFile)) {
3353
+ return {
3354
+ success: false,
3355
+ reason: "post-mutation syntax validation failed - reverted",
3356
+ strategy: "semantic-template",
3357
+ };
3358
+ }
3359
+ return {
3360
+ success: true,
3361
+ reason: `surgical template patch (segments ${patchResult.changedSegmentIndices.join(",")})`,
3362
+ strategy: "semantic-template",
3363
+ lineUpdated,
3364
+ healedLocatorString: newSelector,
3365
+ definitionSite: { file: sourceFile.getFilePath(), line: lineUpdated },
3366
+ };
3367
+ }
3368
+ traceSpanExpression(spanExpr, sourceFile, callerLine, oldSpanVal, newSpanVal) {
3369
+ if (!ts_morph_1.Node.isIdentifier(spanExpr))
3370
+ return null;
3371
+ const symbol = spanExpr.getSymbol();
3372
+ if (!symbol)
3373
+ return null;
3374
+ for (const decl of symbol.getDeclarations()) {
3375
+ if (!ts_morph_1.Node.isVariableDeclaration(decl))
3376
+ continue;
3377
+ const init = decl.getInitializer();
3378
+ if (!init)
3379
+ continue;
3380
+ if (ts_morph_1.Node.isCallExpression(init)) {
3381
+ return this.handleCallExpressionArg(init, sourceFile, oldSpanVal, newSpanVal);
3382
+ }
3383
+ if (ts_morph_1.Node.isStringLiteral(init) ||
3384
+ ts_morph_1.Node.isNoSubstitutionTemplateLiteral(init)) {
3385
+ const result = InitializerUpdater_1.InitializerUpdater.apply(init, sourceFile, newSpanVal);
3386
+ if (!result)
3387
+ return null;
3388
+ this._persistChanges(sourceFile);
3389
+ return {
3390
+ success: true,
3391
+ reason: `span literal updated via trace`,
3392
+ strategy: "semantic-template",
3393
+ lineUpdated: result.lineUpdated,
3394
+ healedLocatorString: result.healedLocatorString,
3395
+ definitionSite: {
3396
+ file: sourceFile.getFilePath(),
3397
+ line: result.lineUpdated,
3398
+ },
3399
+ };
3400
+ }
3401
+ if (ts_morph_1.Node.isTemplateExpression(init)) {
3402
+ return this.handleTemplateLiteralArg(init, sourceFile, { filePath: sourceFile.getFilePath(), line: callerLine }, oldSpanVal, newSpanVal);
3403
+ }
3404
+ }
3405
+ return null;
3406
+ }
3407
+ handleCallExpressionArg(node, sourceFile, oldSelector, newSelector) {
3408
+ const fnAnalysis = DefinitionTracer_1.FunctionalReturnAnalyzer.analyze(node, sourceFile);
3409
+ if (fnAnalysis.kind === "complex") {
3410
+ this.advisoryBuffer.push({
3411
+ code: HealingAdvisory_1.AdvisoryCode.MANUAL_REVIEW_CONDITIONAL_RETURN,
3412
+ message: `Function has conditional/complex return - automated patch skipped. Reason: ${fnAnalysis.reason}`,
3413
+ affectedFile: sourceFile.getFilePath(),
3414
+ suggestedAction: "Manually update the function's return expression.",
3415
+ });
3416
+ return null;
3417
+ }
3418
+ if (fnAnalysis.kind === "literal") {
3419
+ const result = InitializerUpdater_1.InitializerUpdater.apply(fnAnalysis.literalNode, fnAnalysis.functionFile, newSelector);
3420
+ if (!result)
3421
+ return null;
3422
+ if (!this.validateFileSyntax(fnAnalysis.functionFile)) {
3423
+ return {
3424
+ success: false,
3425
+ reason: "post-mutation syntax validation failed - reverted",
3426
+ strategy: "semantic-call-expression",
3427
+ };
3428
+ }
3429
+ return {
3430
+ success: true,
3431
+ reason: `function return literal patched @ line ${fnAnalysis.functionLine}`,
3432
+ strategy: "semantic-call-expression",
3433
+ lineUpdated: result.lineUpdated,
3434
+ healedLocatorString: result.healedLocatorString,
3435
+ definitionSite: {
3436
+ file: fnAnalysis.functionFile.getFilePath(),
3437
+ line: result.lineUpdated,
3438
+ },
3439
+ };
3440
+ }
3441
+ if (fnAnalysis.kind === "template") {
3442
+ const patchResult = TemplateDiffService_1.TemplateDiffService.computeNodePatch(fnAnalysis.templateNode, oldSelector, newSelector);
3443
+ if (!patchResult.ok) {
3444
+ logger_1.logger.debug(`TemplateDiff (call) failed: ${patchResult.reason}`);
3445
+ if (patchResult.reason.includes("structural")) {
3446
+ this.advisoryBuffer.push({
3447
+ code: HealingAdvisory_1.AdvisoryCode.MANUAL_REVIEW_TEMPLATE_COMPLEX,
3448
+ message: `Function template return requires structural change - automated patch skipped.`,
3449
+ affectedFile: fnAnalysis.functionFile.getFilePath(),
3450
+ affectedLine: fnAnalysis.functionLine - 1,
3451
+ suggestedAction: "Manually update the function's template literal.",
3452
+ });
3453
+ }
3454
+ return null;
3455
+ }
3456
+ const lineUpdated = fnAnalysis.templateNode.getStartLineNumber() - 1;
3457
+ fnAnalysis.templateNode.replaceWithText(patchResult.patchedTemplate);
3458
+ this._persistChanges(fnAnalysis.functionFile);
3459
+ if (!this.validateFileSyntax(fnAnalysis.functionFile)) {
3460
+ return {
3461
+ success: false,
3462
+ reason: "post-mutation syntax validation failed - reverted",
3463
+ strategy: "semantic-call-expression",
3464
+ };
3465
+ }
3466
+ return {
3467
+ success: true,
3468
+ reason: `function return template patched @ line ${fnAnalysis.functionLine}`,
3469
+ strategy: "semantic-call-expression",
3470
+ lineUpdated,
3471
+ healedLocatorString: newSelector,
3472
+ definitionSite: {
3473
+ file: fnAnalysis.functionFile.getFilePath(),
3474
+ line: lineUpdated,
3475
+ },
3476
+ };
3477
+ }
3478
+ return null;
3479
+ }
3480
+ validateFileSyntax(sourceFile) {
3481
+ const ALLOWED_CODES = new Set([
3482
+ 2304, 2307, 2339, 2345, 2554, 2571, 7006, 7016, 7031, 18004,
3483
+ ]);
3484
+ const diags = sourceFile.getPreEmitDiagnostics();
3485
+ const syntaxErrors = diags.filter((d) => {
3486
+ if (d.getCategory() !== 1)
3487
+ return false;
3488
+ return !ALLOWED_CODES.has(d.getCode());
3489
+ });
3490
+ if (syntaxErrors.length > 0) {
3491
+ const msgs = syntaxErrors
3492
+ .map((d) => String(d.getMessageText()))
3493
+ .join("; ");
3494
+ logger_1.logger.warn(`syntax errors after mutation: ${msgs} - reverting`);
3495
+ this.advisoryBuffer.push({
3496
+ code: HealingAdvisory_1.AdvisoryCode.POST_MUTATION_TYPE_ERROR,
3497
+ message: `Post-mutation type errors detected - mutation reverted. Errors: ${msgs}`,
3498
+ affectedFile: sourceFile.getFilePath(),
3499
+ suggestedAction: "Manually inspect and fix the type errors introduced by this heal.",
3500
+ });
3501
+ sourceFile.refreshFromFileSystemSync();
3502
+ return false;
3503
+ }
3504
+ return true;
3505
+ }
3506
+ broadcastAndReturn(result, oldSelector, newSelector, caller) {
3507
+ if (result.success) {
3508
+ HealingRegistry_1.HealingRegistry.getInstance().broadcast({ filePath: caller.filePath, line: caller.line }, oldSelector, newSelector);
3509
+ return { ...result, healedLocatorString: newSelector };
3510
+ }
3511
+ return result;
3512
+ }
3513
+ }
3514
+ exports.ASTSourceUpdater = ASTSourceUpdater;