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,1091 @@
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.SourceUpdater = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const child_process_1 = require("child_process");
40
+ const atomicWrite_1 = require("../utils/atomicWrite");
41
+ const ASTSourceUpdater_1 = require("./ASTSourceUpdater");
42
+ const DryRunGuard_1 = require("../config/DryRunGuard");
43
+ const PathScopeGuard_1 = require("../config/PathScopeGuard");
44
+ const WorkspaceSnapshotService_1 = require("./WorkspaceSnapshotService");
45
+ const SelaError_1 = require("../errors/SelaError");
46
+ const SelectorSanitizer_1 = require("./SelectorSanitizer");
47
+ const logger_1 = require("../utils/logger");
48
+ // Centralised write gate - every disk-mutating `fs.writeFileSync` call in
49
+ // this module routes through `persistFile` so the SELA_DRY_RUN bypass is
50
+ // enforced in one place, and the Clean-Room WorkspaceSnapshotService
51
+ // records contentBefore/contentAfter for isolated in-memory diffing.
52
+ function persistFile(filePath, content) {
53
+ // Safe path scoping (T1): block mutations of files outside allowedPaths.
54
+ if (!PathScopeGuard_1.PathScopeGuard.enforce(filePath)) {
55
+ return false;
56
+ }
57
+ if (DryRunGuard_1.DryRunGuard.active()) {
58
+ DryRunGuard_1.DryRunGuard.logSkippedWrite("writeFileSync", filePath);
59
+ return false;
60
+ }
61
+ WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.preWrite(filePath);
62
+ // SELA-5: atomic temp+rename so a concurrent / interrupted write can never
63
+ // leave a torn source file on disk. (The cross-file lost-update guard lives
64
+ // in MutationApplier's per-file lock on the primary AST path.)
65
+ (0, atomicWrite_1.atomicWriteFile)(filePath, content);
66
+ WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.postWrite(filePath);
67
+ return true;
68
+ }
69
+ // ─────────────────────────────────────────────────────────────────
70
+ // QUOTE MANAGEMENT
71
+ // ─────────────────────────────────────────────────────────────────
72
+ const astUpdater = new ASTSourceUpdater_1.ASTSourceUpdater();
73
+ function detectOuterQuote(line, target) {
74
+ const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75
+ for (const q of [`"`, `'`, "`"]) {
76
+ if (new RegExp(`${q}${escaped}${q}`).test(line))
77
+ return q;
78
+ }
79
+ return null;
80
+ }
81
+ function sanitizeForInjection(selector, outerQuote) {
82
+ switch (outerQuote) {
83
+ case `"`:
84
+ return selector.replace(/"/g, `'`);
85
+ case `'`:
86
+ return selector.replace(/'/g, `"`);
87
+ case "`":
88
+ return selector.replace(/`/g, "\\`");
89
+ default:
90
+ return selector.replace(/"/g, `'`);
91
+ }
92
+ }
93
+ function replaceStringLiteralInLine(line, target, replacement) {
94
+ const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
95
+ for (const q of [`"`, `'`, "`"]) {
96
+ const regex = new RegExp(`${q}${escaped}${q}`);
97
+ if (regex.test(line)) {
98
+ const safeReplacement = sanitizeForInjection(replacement, q);
99
+ return line.replace(regex, `${q}${safeReplacement}${q}`);
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ // ─────────────────────────────────────────────────────────────────
105
+ // DEDUPLICATION
106
+ // ─────────────────────────────────────────────────────────────────
107
+ function deduplicateSelector(aiSelector, surroundingChain) {
108
+ const aiParts = aiSelector.split(">>").map((s) => s.trim());
109
+ const chainLower = surroundingChain.toLowerCase();
110
+ let startIdx = 0;
111
+ for (let i = 0; i < aiParts.length - 1; i++) {
112
+ const part = aiParts[i].replace(/['"]/g, "").toLowerCase();
113
+ if (chainLower.includes(part)) {
114
+ startIdx = i + 1;
115
+ }
116
+ else {
117
+ break;
118
+ }
119
+ }
120
+ return aiParts.slice(startIdx).join(" >> ").trim();
121
+ }
122
+ // ─────────────────────────────────────────────────────────────────
123
+ // RUNTIME SELECTOR DEDUPLICATION (FIX 2B)
124
+ //
125
+ // Removes duplicate consecutive `>>` segments from a runtime selector.
126
+ // Uses normalised comparison so minor whitespace / quote differences
127
+ // do not prevent deduplication.
128
+ //
129
+ // Example:
130
+ // "#modern-frame-container >> #modern-frame-container >> internal:control=enter-frame >> ..."
131
+ // → "#modern-frame-container >> internal:control=enter-frame >> ..."
132
+ // ─────────────────────────────────────────────────────────────────
133
+ function deduplicateRuntimeSelector(selector) {
134
+ const parts = selector.split(">>").map((s) => s.trim());
135
+ const deduped = [];
136
+ for (const part of parts) {
137
+ const norm = part
138
+ .replace(/['"]/g, "")
139
+ .toLowerCase()
140
+ .replace(/\s+/g, " ")
141
+ .trim();
142
+ const prevNorm = deduped.length > 0
143
+ ? deduped[deduped.length - 1]
144
+ .replace(/['"]/g, "")
145
+ .toLowerCase()
146
+ .replace(/\s+/g, " ")
147
+ .trim()
148
+ : null;
149
+ if (prevNorm !== null && norm === prevNorm) {
150
+ logger_1.logger.debug(`Removed duplicate segment: "${part}"`);
151
+ continue;
152
+ }
153
+ deduped.push(part);
154
+ }
155
+ return deduped.join(" >> ");
156
+ }
157
+ function detectChain(lines, callerLine, oldLeafSelector) {
158
+ const escaped = oldLeafSelector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
159
+ const leafRegex = new RegExp(`\\.locator\\(["'\`]${escaped}["'\`]\\)`);
160
+ let leafLine = -1;
161
+ const scanStart = Math.max(0, callerLine - 15);
162
+ for (let i = callerLine; i >= scanStart; i--) {
163
+ if (leafRegex.test(lines[i])) {
164
+ leafLine = i;
165
+ break;
166
+ }
167
+ }
168
+ if (leafLine === -1)
169
+ return null;
170
+ let chainEnd = leafLine;
171
+ for (let i = leafLine + 1; i <= callerLine; i++) {
172
+ if (/^\s*\.filter\s*\(/.test(lines[i])) {
173
+ chainEnd = i;
174
+ }
175
+ else {
176
+ break;
177
+ }
178
+ }
179
+ let anchorSelector = "";
180
+ for (let i = leafLine - 1; i >= scanStart; i--) {
181
+ const anchorMatch = lines[i].match(/\.locator\(["'`]([^"'`]+)["'`]\)/);
182
+ if (anchorMatch) {
183
+ anchorSelector = anchorMatch[1];
184
+ break;
185
+ }
186
+ }
187
+ return { startLine: leafLine, endLine: chainEnd, anchorSelector };
188
+ }
189
+ function detectChainRoot(lines, callerLine, oldLeafSelector) {
190
+ const escaped = oldLeafSelector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
191
+ const leafPattern = new RegExp(`(?:locator|getByRole|getByLabel|getByText|getByTestId|getByPlaceholder)\\s*\\(\\s*["'\`]${escaped}["'\`]`);
192
+ let leafLine = -1;
193
+ const scanStart = Math.max(0, callerLine - 20);
194
+ for (let i = callerLine; i >= scanStart; i--) {
195
+ if (leafPattern.test(lines[i])) {
196
+ leafLine = i;
197
+ break;
198
+ }
199
+ }
200
+ if (leafLine === -1)
201
+ return null;
202
+ let chainEndLine = leafLine;
203
+ for (let i = leafLine + 1; i <= Math.min(lines.length - 1, leafLine + 10); i++) {
204
+ const trimmed = lines[i].trim();
205
+ if (/^\.(?:filter|first|last|nth)\s*\(/.test(trimmed)) {
206
+ chainEndLine = i;
207
+ }
208
+ else if (trimmed.startsWith(".")) {
209
+ break;
210
+ }
211
+ else {
212
+ break;
213
+ }
214
+ }
215
+ let rootLine = leafLine;
216
+ let rootReceiver = "page";
217
+ for (let i = leafLine - 1; i >= scanStart; i--) {
218
+ const trimmed = lines[i].trim();
219
+ const combined = lines[i];
220
+ const isChainContinuation = trimmed.startsWith(".") ||
221
+ combined.trimEnd().endsWith("(") ||
222
+ combined.trimEnd().endsWith(",");
223
+ if (isChainContinuation) {
224
+ rootLine = i;
225
+ continue;
226
+ }
227
+ const assignMatch = combined.match(/(?:const|let|var)\s+[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*(page|[a-zA-Z_$][a-zA-Z0-9_$]*)/);
228
+ if (assignMatch) {
229
+ rootReceiver = assignMatch[1];
230
+ rootLine = i;
231
+ break;
232
+ }
233
+ const pageMatch = combined.match(/^\s*((?:await\s+)?(?:page|[a-zA-Z_$][a-zA-Z0-9_$]*))\s*\.(?:locator|frameLocator|getBy)/);
234
+ if (pageMatch) {
235
+ rootReceiver = pageMatch[1].replace(/^await\s+/, "").trim();
236
+ rootLine = i;
237
+ break;
238
+ }
239
+ if (!isChainContinuation && i < leafLine - 1)
240
+ break;
241
+ }
242
+ return { rootLine, rootReceiver, leafLine, chainEndLine };
243
+ }
244
+ // ─────────────────────────────────────────────────────────────────
245
+ // TEMPLATE VARIABLE PRESERVATION
246
+ // ─────────────────────────────────────────────────────────────────
247
+ function updateTemplateLiteralPreservingVars(line, oldSelector, newSelector, domAttributeHint) {
248
+ if (!line.includes("`") || !line.includes("${"))
249
+ return null;
250
+ const newLeaf = newSelector.split(">>").pop().trim();
251
+ const varMatches = [...line.matchAll(/\$\{([^}]+)\}/g)];
252
+ if (varMatches.length === 0)
253
+ return null;
254
+ const varExpression = varMatches[0][1];
255
+ const oldAttrInTemplateMatch = line.match(/\[([a-zA-Z-]+)=["']?\$\{[^}]+\}([^"'\]`]*)["']?\]/);
256
+ const oldAttrName = oldAttrInTemplateMatch?.[1] ?? null;
257
+ const oldStaticSuffix = oldAttrInTemplateMatch?.[2] ?? null;
258
+ const newAttrMatch = newLeaf.match(/\[([a-zA-Z-]+)="([^"]*)"\]/);
259
+ const newAttrName = newAttrMatch?.[1] ?? null;
260
+ const newAttrValue = newAttrMatch?.[2] ?? null;
261
+ const oldAttrRuntimeMatch = oldSelector.match(/\[([a-zA-Z-]+)="([^"]*)"\]/);
262
+ const oldAttrRuntimeValue = oldAttrRuntimeMatch?.[2] ?? null;
263
+ let varRuntimeValue = null;
264
+ if (oldAttrRuntimeValue !== null && oldStaticSuffix !== null) {
265
+ if (oldStaticSuffix.length > 0) {
266
+ varRuntimeValue = oldAttrRuntimeValue.endsWith(oldStaticSuffix)
267
+ ? oldAttrRuntimeValue.slice(0, -oldStaticSuffix.length)
268
+ : oldAttrRuntimeValue.split("_")[0];
269
+ }
270
+ else {
271
+ const underscoreIdx = oldAttrRuntimeValue.indexOf("_");
272
+ varRuntimeValue =
273
+ underscoreIdx >= 0
274
+ ? oldAttrRuntimeValue.slice(0, underscoreIdx)
275
+ : oldAttrRuntimeValue;
276
+ }
277
+ }
278
+ if (oldAttrName &&
279
+ newAttrName &&
280
+ oldAttrName === newAttrName &&
281
+ newAttrValue) {
282
+ let newSuffix = "";
283
+ if (varRuntimeValue && newAttrValue.startsWith(varRuntimeValue)) {
284
+ newSuffix = newAttrValue.slice(varRuntimeValue.length);
285
+ }
286
+ else {
287
+ const lastUnderscoreIdx = newAttrValue.lastIndexOf("_");
288
+ newSuffix =
289
+ lastUnderscoreIdx >= 0 ? newAttrValue.slice(lastUnderscoreIdx) : "";
290
+ }
291
+ if (newSuffix === oldStaticSuffix)
292
+ return null;
293
+ const escapedOldSuffix = (oldStaticSuffix ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
294
+ const suffixPattern = new RegExp(`(\\$\\{[^}]+\\})${escapedOldSuffix}(["']?\\])`);
295
+ const updated = line.replace(suffixPattern, `$1${newSuffix}$2`);
296
+ return updated !== line ? updated : null;
297
+ }
298
+ if (oldAttrName && !newAttrName) {
299
+ let newSuffix = "";
300
+ if (domAttributeHint &&
301
+ varRuntimeValue &&
302
+ domAttributeHint.startsWith(varRuntimeValue)) {
303
+ newSuffix = domAttributeHint.slice(varRuntimeValue.length);
304
+ }
305
+ const reconstructedAttr = newSuffix.length > 0
306
+ ? `[${oldAttrName}="\${${varExpression}}${newSuffix}"]`
307
+ : `[${oldAttrName}="\${${varExpression}}"]`;
308
+ const oldAttrPattern = new RegExp(`\\[${oldAttrName.replace(/-/g, "\\-")}=["']?\\$\\{[^}]+\\}[^"'\\]\`]*["']?\\]`);
309
+ if (oldAttrPattern.test(line)) {
310
+ const updated = line.replace(oldAttrPattern, reconstructedAttr);
311
+ return updated !== line ? updated : null;
312
+ }
313
+ return null;
314
+ }
315
+ return null;
316
+ }
317
+ // ─────────────────────────────────────────────────────────────────
318
+ // MAIN CLASS
319
+ // ─────────────────────────────────────────────────────────────────
320
+ // ─────────────────────────────────────────────────────────────────
321
+ // GIT HELPER (used by branch strategy and autoCommit)
322
+ //
323
+ // Design goals:
324
+ // • Lean - no stash, no branch-switching, no complex state management.
325
+ // • Commit-based - write first (inPlace), then commit. If git fails,
326
+ // the fix is still on disk (silent inPlace fallback).
327
+ // • Detached HEAD aware - skips silently in CI environments that check
328
+ // out a specific SHA without a branch ref.
329
+ // ─────────────────────────────────────────────────────────────────
330
+ function runGitOperations(absoluteFilePath, createBranch, relativeFilePath) {
331
+ const filename = path.basename(relativeFilePath);
332
+ // Dry-run gate - suppress branch creation, `git add`, and auto-commit.
333
+ // Mirrors the persistFile() skip so a dry run is fully observable in CI
334
+ // without ever producing a commit.
335
+ if (DryRunGuard_1.DryRunGuard.active()) {
336
+ logger_1.logger.debug(`Dry Run - skip git operations for ${filename}`);
337
+ return;
338
+ }
339
+ try {
340
+ // Bail out if HEAD is detached (CI SHA checkout, rebase mid-flight, etc.)
341
+ try {
342
+ (0, child_process_1.execSync)("git symbolic-ref HEAD", { stdio: "pipe" });
343
+ }
344
+ catch {
345
+ logger_1.logger.warn(`Detached HEAD - git operations skipped (fix is saved to disk)`);
346
+ return;
347
+ }
348
+ if (createBranch) {
349
+ const branchName = `sela/heal-${Date.now()}`;
350
+ (0, child_process_1.execSync)(`git checkout -b ${branchName}`, { stdio: "pipe" });
351
+ logger_1.logger.debug(`Created branch: ${branchName}`);
352
+ }
353
+ (0, child_process_1.execSync)(`git add "${absoluteFilePath}"`, { stdio: "pipe" });
354
+ (0, child_process_1.execSync)(`git commit -m "fix(sela): heal selector in ${filename}"`, {
355
+ stdio: "pipe",
356
+ });
357
+ logger_1.logger.success(`Auto-committed fix in: ${filename}`);
358
+ }
359
+ catch (err) {
360
+ logger_1.logger.warn(`Git operation failed: ${err.message} - fix written to disk (inPlace fallback)`);
361
+ }
362
+ }
363
+ class SourceUpdater {
364
+ // ─────────────────────────────────────────────────────────────
365
+ // PUBLIC ENTRY POINT
366
+ // Handles commentOnly short-circuit and post-write git operations,
367
+ // then delegates all selector-rewriting work to _coreUpdate().
368
+ // ─────────────────────────────────────────────────────────────
369
+ static update(caller, oldSelector, newSelector, domContext, dnaAttrHint, fullSelectorContext, aiSegments, contentChange, smartChainSegments, originalChainHint, updateStrategy, autoCommit) {
370
+ const resolvedPath = SourceUpdater.resolveFilePath(caller.filePath);
371
+ // ── commentOnly: write a TODO comment, skip all AST/regex work ──
372
+ if (updateStrategy === "commentOnly") {
373
+ if (!resolvedPath) {
374
+ return { success: false, reason: `File not found: ${caller.filePath}` };
375
+ }
376
+ const effective = deduplicateRuntimeSelector(newSelector);
377
+ return SourceUpdater.applyCommentOnly(caller, oldSelector, effective, resolvedPath);
378
+ }
379
+ // ── inPlace / branch: run the full rewriting pipeline ──────────
380
+ const result = SourceUpdater._coreUpdate(caller, oldSelector, newSelector, domContext, dnaAttrHint, fullSelectorContext, aiSegments, contentChange, smartChainSegments, originalChainHint);
381
+ // ── Post-write git operations (branch or autoCommit) ───────────
382
+ if (result.success &&
383
+ resolvedPath &&
384
+ (updateStrategy === "branch" || autoCommit)) {
385
+ runGitOperations(resolvedPath, updateStrategy === "branch", caller.filePath);
386
+ }
387
+ return result;
388
+ }
389
+ // ─────────────────────────────────────────────────────────────
390
+ // commentOnly helper - inserts a TODO comment above the failing
391
+ // call site and returns success:false so the engine knows the
392
+ // source was NOT rewritten (no retry suppression needed).
393
+ // ─────────────────────────────────────────────────────────────
394
+ static applyCommentOnly(caller, oldSelector, newSelector, filePath) {
395
+ try {
396
+ const raw = fs.readFileSync(filePath, "utf8");
397
+ const lines = raw.split(/\r?\n/);
398
+ const lineIdx = caller.line - 1; // 0-based
399
+ if (lineIdx < 0 || lineIdx >= lines.length) {
400
+ return { success: false, reason: "COMMENT_ONLY: line out of range" };
401
+ }
402
+ const indent = lines[lineIdx].match(/^(\s*)/)?.[1] ?? "";
403
+ const comment = `${indent}// TODO [Sela]: replace "${oldSelector}" with "${newSelector}"`;
404
+ lines.splice(lineIdx, 0, comment);
405
+ persistFile(filePath, lines.join("\n"));
406
+ logger_1.logger.success(`Comment-only: inserted TODO at line ${caller.line} in ${path.basename(filePath)}`);
407
+ return {
408
+ success: false,
409
+ reason: "COMMENT_ONLY: TODO inserted, source selector not modified",
410
+ healedLocatorString: newSelector,
411
+ };
412
+ }
413
+ catch (err) {
414
+ return {
415
+ success: false,
416
+ reason: `COMMENT_ONLY: write failed - ${err.message}`,
417
+ };
418
+ }
419
+ }
420
+ // ─────────────────────────────────────────────────────────────
421
+ // CORE UPDATE - Layered Healing (AST primary, Regex fallback)
422
+ // Called by update(); never call directly from outside the class.
423
+ // ─────────────────────────────────────────────────────────────
424
+ static _coreUpdate(caller, oldSelector, newSelector, domContext, dnaAttrHint, fullSelectorContext, aiSegments, contentChange, smartChainSegments, originalChainHint) {
425
+ const filePath = SourceUpdater.resolveFilePath(caller.filePath);
426
+ // ── FIX 2B: Deduplicate the runtime selector BEFORE passing it down ──
427
+ // This prevents the engine from writing double-frame selectors like
428
+ // "#modern-frame-container >> #modern-frame-container >> ..."
429
+ const deduplicatedNewSelector = deduplicateRuntimeSelector(newSelector);
430
+ if (deduplicatedNewSelector !== newSelector) {
431
+ logger_1.logger.debug(`Runtime selector deduplicated:\n` +
432
+ ` before: "${newSelector}"\n` +
433
+ ` after: "${deduplicatedNewSelector}"`);
434
+ }
435
+ // Use the deduplicated selector for all subsequent operations
436
+ const effectiveNewSelector = deduplicatedNewSelector;
437
+ logger_1.logger.debug(`Attempting update in file: ${filePath}`);
438
+ logger_1.logger.debug(`Request: "${oldSelector}" -> "${effectiveNewSelector}" (Line: ${caller.line})`);
439
+ if (smartChainSegments?.length) {
440
+ logger_1.logger.debug(`smartChain: ${smartChainSegments.length} segments, ` +
441
+ `hint: ${originalChainHint?.length ?? 0} segments`);
442
+ }
443
+ if (contentChange) {
444
+ logger_1.logger.debug(`contentChange: "${contentChange.oldText}" → "${contentChange.newText}"`);
445
+ }
446
+ if (!filePath) {
447
+ logger_1.logger.error(`File not found: ${caller.filePath}`);
448
+ return { success: false, reason: `File not found: ${caller.filePath}` };
449
+ }
450
+ // ── 1. AST Engine (Primary) ─────────────────────────────────────
451
+ try {
452
+ const astResult = astUpdater.update(caller, oldSelector, effectiveNewSelector, aiSegments, fullSelectorContext, contentChange, smartChainSegments, originalChainHint);
453
+ if (astResult.success) {
454
+ logger_1.logger.debug(`AST Strategy ${astResult.strategy} SUCCESS` +
455
+ (astResult.lineUpdated !== undefined
456
+ ? ` - line ${astResult.lineUpdated + 1}`
457
+ : ""));
458
+ return {
459
+ success: true,
460
+ reason: `AST[${astResult.strategy}]: ${astResult.reason}`,
461
+ lineUpdated: astResult.lineUpdated,
462
+ // Prefer the AST's own healedLocatorString; fall back to the deduplicated input.
463
+ healedLocatorString: astResult.healedLocatorString ?? effectiveNewSelector,
464
+ definitionSite: astResult.definitionSite,
465
+ blastRadius: astResult.blastRadius,
466
+ };
467
+ }
468
+ // Semantic guard means we identified a definition-site shape (IDENTIFIER /
469
+ // PROPERTY_ACCESS / TEMPLATE_LITERAL) but could not safely resolve the new
470
+ // value. Regex cannot do better and will corrupt the definition site.
471
+ if (astResult.strategy === "semantic-guard") {
472
+ logger_1.logger.warn(`Regex blocked - semantic guard fired. ` +
473
+ `Source unchanged. Reason: ${astResult.reason}`);
474
+ return { success: false, reason: astResult.reason };
475
+ }
476
+ logger_1.logger.debug(`AST engine exhausted (${astResult.reason}), falling back to Regex`);
477
+ }
478
+ catch (astError) {
479
+ // Developer pinned the failing statement with `// sela-fail-fast` -
480
+ // propagate the directive instead of swallowing into the Regex
481
+ // fallback chain (which would defeat the whole point of the gate).
482
+ if ((0, SelaError_1.isSelaErrorFrom)(astError, "ASTUpdater") &&
483
+ astError.code ===
484
+ SelaError_1.AST_UPDATER_CODES.HEAL_DISABLED_BY_DIRECTIVE) {
485
+ throw astError;
486
+ }
487
+ logger_1.logger.warn(`AST engine error: ${astError.message}. Falling back to Regex`);
488
+ }
489
+ // ── ABSOLUTE GUARD: internal engine selector ───────────────────
490
+ // The AST engine declined to mutate (could not method-shift). The regex
491
+ // fallback only does RAW string substitution of `oldSelector → newSelector`,
492
+ // which would stamp the private `internal:role=…` engine string into the
493
+ // developer's source (the bug this guard kills). A `.locator()` string
494
+ // cannot express a semantic locator, so there is no safe regex outcome:
495
+ // refuse, leave source untouched. The in-memory proxy swap already keeps
496
+ // this run green; the correct method-shift is the new composeHeal pipeline.
497
+ if ((0, SelectorSanitizer_1.isInternalEngineSelector)(effectiveNewSelector)) {
498
+ logger_1.logger.warn(`Regex blocked - internal engine selector cannot be ` +
499
+ `string-substituted into source. Method-shift required. Source unchanged.`);
500
+ return {
501
+ success: false,
502
+ reason: "INTERNAL_SELECTOR_GUARD: refusing regex string-substitution of an internal engine selector (method-shift required)",
503
+ };
504
+ }
505
+ // ── 2. Regex Fallback ──────────────────────────────────────────
506
+ logger_1.logger.debug(`Using Regex fallback strategies...`);
507
+ const raw = fs.readFileSync(filePath, "utf8");
508
+ const lines = raw.split(/\r?\n/);
509
+ if (caller.line <= 0) {
510
+ logger_1.logger.debug(`line ${caller.line} invalid - running Regex Strategy G only`);
511
+ if (contentChange) {
512
+ const assertResult = SourceUpdater.strategyAssertionHealing(lines, 1, contentChange, true);
513
+ if (assertResult.success) {
514
+ persistFile(filePath, lines.join("\n"));
515
+ logger_1.logger.debug(`Regex Strategy G (Assertion Healing) SUCCESS`);
516
+ return assertResult;
517
+ }
518
+ }
519
+ logger_1.logger.warn(`ALL STRATEGIES FAILED for line-0 heal`);
520
+ return { success: false, reason: "invalid line number (≤0)" };
521
+ }
522
+ const callerLine = caller.line - 1; // 0-based
523
+ if (!lines[callerLine]) {
524
+ logger_1.logger.warn(`Target line ${caller.line} is empty or out of bounds.`);
525
+ }
526
+ else {
527
+ logger_1.logger.debug(`Analyzing line ${caller.line}: "${lines[callerLine].trim()}"`);
528
+ }
529
+ const tryAssertionHealing = () => {
530
+ if (!contentChange)
531
+ return;
532
+ logger_1.logger.debug(`Checking for assertion mismatches...`);
533
+ logger_1.logger.warn(`To fix the assertion at line ${caller.line}, ` +
534
+ `change to: .toHaveText("${contentChange.newText}")`);
535
+ };
536
+ // ── Strategy 0: Layered Frame Healing ──────────────────────
537
+ if (aiSegments && aiSegments.length > 0 && fullSelectorContext) {
538
+ const layeredResult = SourceUpdater.strategyLayeredHealing(lines, callerLine, oldSelector, aiSegments, fullSelectorContext);
539
+ if (layeredResult.success) {
540
+ tryAssertionHealing();
541
+ persistFile(filePath, lines.join("\n"));
542
+ logger_1.logger.debug(`Strategy 0 (Layered Healing) SUCCESS - line ${layeredResult.lineUpdated + 1}`);
543
+ return layeredResult;
544
+ }
545
+ }
546
+ // ── Strategy F: Chain Collapsing (Regex) ───────────────────
547
+ const chainCollapseResult = SourceUpdater.strategyChainCollapse_Regex(lines, callerLine, oldSelector, effectiveNewSelector);
548
+ if (chainCollapseResult.success) {
549
+ tryAssertionHealing();
550
+ persistFile(filePath, lines.join("\n"));
551
+ logger_1.logger.debug(`Strategy F (Chain Collapse) SUCCESS`);
552
+ return chainCollapseResult;
553
+ }
554
+ // ── Strategy 1: Chain Collapse (original leaf-only) ────────
555
+ const chainResult = SourceUpdater.strategyChainCollapse(lines, callerLine, oldSelector, effectiveNewSelector);
556
+ if (chainResult.success) {
557
+ tryAssertionHealing();
558
+ persistFile(filePath, lines.join("\n"));
559
+ logger_1.logger.debug(`Strategy 1 (Chain Collapse) SUCCESS`);
560
+ return chainResult;
561
+ }
562
+ // ── Strategy 2: Direct Literal ─────────────────────────────
563
+ const directResult = SourceUpdater.strategyDirectLiteral(lines, callerLine, oldSelector, effectiveNewSelector);
564
+ if (directResult.success) {
565
+ tryAssertionHealing();
566
+ persistFile(filePath, lines.join("\n"));
567
+ logger_1.logger.debug(`Strategy 2 (Direct Literal) SUCCESS`);
568
+ return directResult;
569
+ }
570
+ // ── Strategy 3: Upstream Variable ──────────────────────────
571
+ const varResult = SourceUpdater.strategyUpstreamVariable(lines, callerLine, oldSelector, effectiveNewSelector);
572
+ if (varResult.success) {
573
+ tryAssertionHealing();
574
+ persistFile(filePath, lines.join("\n"));
575
+ logger_1.logger.debug(`Strategy 3 (Upstream Variable) SUCCESS`);
576
+ return varResult;
577
+ }
578
+ // ── Strategy 4: Chained Dedup ──────────────────────────────
579
+ const chainDedupResult = SourceUpdater.strategyChainedLocator(lines, callerLine, oldSelector, effectiveNewSelector);
580
+ if (chainDedupResult.success) {
581
+ tryAssertionHealing();
582
+ persistFile(filePath, lines.join("\n"));
583
+ logger_1.logger.debug(`Strategy 4 (Chained Dedup) SUCCESS`);
584
+ return chainDedupResult;
585
+ }
586
+ // ── Strategy 5: Function Return ────────────────────────────
587
+ const fnResult = SourceUpdater.strategyFunctionReturn(lines, callerLine, oldSelector, effectiveNewSelector, domContext, dnaAttrHint);
588
+ if (fnResult.success) {
589
+ tryAssertionHealing();
590
+ persistFile(filePath, lines.join("\n"));
591
+ logger_1.logger.debug(`Strategy 5 (Function Return) SUCCESS`);
592
+ return fnResult;
593
+ }
594
+ // ── Strategy 6: Global Scan ────────────────────────────────
595
+ const scanResult = SourceUpdater.strategyGlobalScan(lines, oldSelector, effectiveNewSelector);
596
+ if (scanResult.success) {
597
+ tryAssertionHealing();
598
+ persistFile(filePath, lines.join("\n"));
599
+ logger_1.logger.debug(`Strategy 6 (Global Scan) SUCCESS`);
600
+ return scanResult;
601
+ }
602
+ // ── Strategy G: Assertion Healing standalone ───────────────
603
+ if (contentChange) {
604
+ const assertResult = SourceUpdater.strategyAssertionHealing(lines, callerLine, contentChange);
605
+ if (assertResult.success) {
606
+ persistFile(filePath, lines.join("\n"));
607
+ logger_1.logger.debug(`Strategy G (Assertion Healing) SUCCESS`);
608
+ return assertResult;
609
+ }
610
+ }
611
+ logger_1.logger.warn(`ALL STRATEGIES FAILED for: "${oldSelector}"`);
612
+ return {
613
+ success: false,
614
+ reason: "No strategy matched (including AST and Regex Fallbacks)",
615
+ };
616
+ }
617
+ // ─────────────────────────────────────────────────────────────
618
+ // STRATEGY F (Regex) - Full Chain Collapsing
619
+ // ─────────────────────────────────────────────────────────────
620
+ static strategyChainCollapse_Regex(lines, callerLine, oldSelector, newSelector) {
621
+ logger_1.logger.debug(`Starting Regex Chain Collapse...`);
622
+ const oldLeaf = oldSelector.split(">>").pop().trim();
623
+ const chainRootInfo = detectChainRoot(lines, callerLine, oldLeaf);
624
+ if (!chainRootInfo) {
625
+ return { success: false, reason: "no chain root detected" };
626
+ }
627
+ const { rootLine, rootReceiver, leafLine, chainEndLine } = chainRootInfo;
628
+ if (rootLine === leafLine && chainEndLine === leafLine) {
629
+ return { success: false, reason: "single-line call, not a chain" };
630
+ }
631
+ logger_1.logger.debug(`Chain detected: root=${rootLine + 1}, leaf=${leafLine + 1}, ` +
632
+ `end=${chainEndLine + 1}, receiver="${rootReceiver}"`);
633
+ const newLeaf = newSelector.split(">>").pop().trim();
634
+ const safeNewLeaf = newLeaf.replace(/"/g, "'");
635
+ const newCallText = `.locator("${safeNewLeaf}")`;
636
+ const indentMatch = lines[rootLine].match(/^(\s*)/);
637
+ const indent = indentMatch ? indentMatch[1] : "";
638
+ const newLine = `${indent}${rootReceiver}${newCallText}`;
639
+ if (rootLine !== chainEndLine) {
640
+ const linesToReplace = chainEndLine - rootLine + 1;
641
+ lines.splice(rootLine, linesToReplace, newLine);
642
+ logger_1.logger.debug(`Multi-line chain collapsed (${linesToReplace} lines → 1): "${newLine.trim()}"`);
643
+ return {
644
+ success: true,
645
+ reason: `chain collapsed (${linesToReplace} lines → 1)`,
646
+ lineUpdated: rootLine,
647
+ };
648
+ }
649
+ else {
650
+ const originalLine = lines[rootLine];
651
+ const receiverPattern = new RegExp(`(${rootReceiver.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})` +
652
+ `(?:\\s*\\.\\s*(?:locator|getBy\\w+|frameLocator|filter|first|last|nth)\\s*\\([^)]*\\))+`);
653
+ if (receiverPattern.test(originalLine)) {
654
+ const replaced = originalLine.replace(receiverPattern, `${rootReceiver}${newCallText}`);
655
+ if (replaced !== originalLine) {
656
+ lines[rootLine] = replaced;
657
+ logger_1.logger.debug(`Single-line chain collapsed: "${replaced.trim()}"`);
658
+ return {
659
+ success: true,
660
+ reason: "single-line chain collapsed",
661
+ lineUpdated: rootLine,
662
+ };
663
+ }
664
+ }
665
+ }
666
+ return { success: false, reason: "chain detected but could not replace" };
667
+ }
668
+ // ─────────────────────────────────────────────────────────────
669
+ // STRATEGY G (Regex) - Assertion Healing
670
+ // ─────────────────────────────────────────────────────────────
671
+ static strategyAssertionHealing(lines, callerLine, contentChange, wholeFile = false) {
672
+ const { oldText, newText } = contentChange;
673
+ logger_1.logger.debug(`Assertion Healing: "${oldText}" → "${newText}"`);
674
+ const TEXT_MATCHERS = [
675
+ "toHaveText",
676
+ "toContainText",
677
+ "toHaveValue",
678
+ "toHaveAttribute",
679
+ "toHaveLabel",
680
+ "toHaveTitle",
681
+ ];
682
+ const matcherPattern = new RegExp(`\\.(?:${TEXT_MATCHERS.join("|")})\\s*\\(`);
683
+ const searchStart = wholeFile ? 0 : Math.max(0, callerLine - 2);
684
+ const searchEnd = wholeFile
685
+ ? lines.length - 1
686
+ : Math.min(lines.length - 1, callerLine + 40);
687
+ let healedCount = 0;
688
+ let lastHealed;
689
+ for (let i = searchStart; i <= searchEnd; i++) {
690
+ const line = lines[i];
691
+ if (!matcherPattern.test(line))
692
+ continue;
693
+ for (const q of [`"`, `'`, "`"]) {
694
+ const escapedOld = oldText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
695
+ const valuePattern = new RegExp(`${q}([^${q}]*${escapedOld}[^${q}]*)${q}`);
696
+ const match = valuePattern.exec(line);
697
+ if (match) {
698
+ const oldValue = match[1];
699
+ const newValue = oldValue.replace(oldText, newText);
700
+ const safeNew = sanitizeForInjection(newValue, q);
701
+ const escaped = oldValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
702
+ const replaceRx = new RegExp(`${q}${escaped}${q}`);
703
+ const newLine = line.replace(replaceRx, `${q}${safeNew}${q}`);
704
+ if (newLine !== line) {
705
+ lines[i] = newLine;
706
+ healedCount++;
707
+ lastHealed = i;
708
+ logger_1.logger.debug(`Healed assertion at line ${i + 1}: "${oldValue}" → "${newValue}"`);
709
+ break;
710
+ }
711
+ }
712
+ }
713
+ }
714
+ if (healedCount > 0) {
715
+ return {
716
+ success: true,
717
+ reason: `assertion healing: ${healedCount} assertion(s) updated`,
718
+ lineUpdated: lastHealed,
719
+ };
720
+ }
721
+ return { success: false, reason: "no assertions to heal" };
722
+ }
723
+ // ─────────────────────────────────────────────────────────────
724
+ // STRATEGY 0 - Layered Healing (Upstream Variable Tracing)
725
+ // ─────────────────────────────────────────────────────────────
726
+ static strategyLayeredHealing(lines, callerLine, oldElementSelector, aiSegments, fullSelectorContext) {
727
+ logger_1.logger.debug(`Strategy 0: Starting Layered Healing...`);
728
+ const failureLine = lines[callerLine] ?? "";
729
+ const varUsageMatch = failureLine.match(/^\s*(?:await\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*locator\s*\(/);
730
+ const usedVarName = varUsageMatch?.[1];
731
+ if (usedVarName && usedVarName !== "page") {
732
+ const origin = SourceUpdater.findVariableOrigin(lines, usedVarName, callerLine);
733
+ if (origin) {
734
+ const frameSegments = aiSegments.filter((s) => s.type === "frame");
735
+ if (frameSegments.length === 0) {
736
+ lines[callerLine] = failureLine.replace(`${usedVarName}.locator`, `page.locator`);
737
+ return {
738
+ success: true,
739
+ reason: `Collapsed variable '${usedVarName}' to 'page'`,
740
+ lineUpdated: callerLine,
741
+ };
742
+ }
743
+ let newDeclaration = ` const ${usedVarName} = page`;
744
+ frameSegments.forEach((seg) => {
745
+ newDeclaration += `.frameLocator("${seg.selector}")`;
746
+ });
747
+ newDeclaration += ";";
748
+ const scanEnd = Math.min(lines.length - 1, origin.declarationLine + 5);
749
+ let endOfDeclaration = origin.declarationLine;
750
+ for (let i = origin.declarationLine; i <= scanEnd; i++) {
751
+ if (lines[i].includes(";")) {
752
+ endOfDeclaration = i;
753
+ break;
754
+ }
755
+ }
756
+ lines.splice(origin.declarationLine, endOfDeclaration - origin.declarationLine + 1, newDeclaration);
757
+ return {
758
+ success: true,
759
+ reason: `Reconstructed variable '${usedVarName}' with AI frame path`,
760
+ lineUpdated: origin.declarationLine,
761
+ };
762
+ }
763
+ }
764
+ return { success: false, reason: "no matches found in chain" };
765
+ }
766
+ static findVariableOrigin(lines, varName, startLine) {
767
+ const scanStart = Math.max(0, startLine - 30);
768
+ const declarationPattern = new RegExp(`^\\s*(?:const|let|var)\\s+${varName}\\s*=`);
769
+ for (let i = startLine - 1; i >= scanStart; i--) {
770
+ if (declarationPattern.test(lines[i])) {
771
+ return { varName, declarationLine: i, lineContent: lines[i] };
772
+ }
773
+ }
774
+ return null;
775
+ }
776
+ // ─────────────────────────────────────────────────────────────
777
+ // STRATEGY 1 - Chain Collapse (leaf-only precision replacement)
778
+ // ─────────────────────────────────────────────────────────────
779
+ static strategyChainCollapse(lines, callerLine, oldSelector, newSelector) {
780
+ const oldLeaf = oldSelector
781
+ .split(">>")
782
+ .pop()
783
+ .replace(/\.filter\([^)]*\)/g, "")
784
+ .trim();
785
+ const newLeaf = newSelector.split(">>").pop().trim();
786
+ const chain = detectChain(lines, callerLine, oldLeaf);
787
+ if (!chain)
788
+ return { success: false, reason: "no chain detected" };
789
+ const deduped = chain.anchorSelector
790
+ ? deduplicateSelector(newSelector, chain.anchorSelector)
791
+ : newLeaf;
792
+ const outerQuote = detectOuterQuote(lines[chain.startLine], oldLeaf) ?? `"`;
793
+ const safeDeduped = sanitizeForInjection(deduped, outerQuote);
794
+ const escapedOldLeaf = oldLeaf.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
795
+ const escapedQ = outerQuote.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
796
+ const leafLiteralRegex = new RegExp(`(\\.locator\\(${escapedQ})${escapedOldLeaf}(${escapedQ}\\))`);
797
+ if (leafLiteralRegex.test(lines[chain.startLine])) {
798
+ lines[chain.startLine] = lines[chain.startLine].replace(leafLiteralRegex, `$1${safeDeduped}$2`);
799
+ }
800
+ else {
801
+ const replaced = replaceStringLiteralInLine(lines[chain.startLine], oldLeaf, safeDeduped);
802
+ if (replaced === null)
803
+ return { success: false, reason: "chain leaf literal not found" };
804
+ lines[chain.startLine] = replaced;
805
+ }
806
+ if (chain.endLine > chain.startLine) {
807
+ for (let i = chain.endLine; i > chain.startLine; i--) {
808
+ if (/^\s*\.filter\s*\(/.test(lines[i]))
809
+ lines.splice(i, 1);
810
+ }
811
+ }
812
+ return {
813
+ success: true,
814
+ reason: `chain collapsed: '${oldLeaf}' → '${deduped}'`,
815
+ lineUpdated: chain.startLine,
816
+ };
817
+ }
818
+ // ─────────────────────────────────────────────────────────────
819
+ // STRATEGY 2 - Direct Literal
820
+ // ─────────────────────────────────────────────────────────────
821
+ static strategyDirectLiteral(lines, callerLine, oldSelector, newSelector) {
822
+ const windowSize = 10;
823
+ const start = Math.max(0, callerLine - windowSize);
824
+ const end = Math.min(lines.length - 1, callerLine + windowSize);
825
+ const lastOldPart = oldSelector.split(">>").pop().trim();
826
+ const lastNewPart = newSelector.split(">>").pop().trim();
827
+ for (let i = callerLine; i >= start; i--) {
828
+ const outerQ = detectOuterQuote(lines[i], lastOldPart);
829
+ if (!outerQ)
830
+ continue;
831
+ const safe = sanitizeForInjection(lastNewPart, outerQ);
832
+ const replaced = replaceStringLiteralInLine(lines[i], lastOldPart, safe);
833
+ if (replaced !== null) {
834
+ lines[i] = replaced;
835
+ return { success: true, reason: "direct literal", lineUpdated: i };
836
+ }
837
+ }
838
+ for (let i = callerLine + 1; i <= end; i++) {
839
+ const outerQ = detectOuterQuote(lines[i], lastOldPart);
840
+ if (!outerQ)
841
+ continue;
842
+ const safe = sanitizeForInjection(lastNewPart, outerQ);
843
+ const replaced = replaceStringLiteralInLine(lines[i], lastOldPart, safe);
844
+ if (replaced !== null) {
845
+ lines[i] = replaced;
846
+ return { success: true, reason: "direct literal", lineUpdated: i };
847
+ }
848
+ }
849
+ return { success: false, reason: "no direct literal" };
850
+ }
851
+ // ─────────────────────────────────────────────────────────────
852
+ // STRATEGY 3 - Upstream Variable/Const
853
+ // ─────────────────────────────────────────────────────────────
854
+ static strategyUpstreamVariable(lines, callerLine, oldSelector, newSelector) {
855
+ const callBlock = lines
856
+ .slice(Math.max(0, callerLine - 5), callerLine + 1)
857
+ .join("\n");
858
+ const identifierPattern = /\b([A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*)\b/g;
859
+ const candidates = new Set();
860
+ let m;
861
+ while ((m = identifierPattern.exec(callBlock)) !== null)
862
+ candidates.add(m[1]);
863
+ const lastOldPart = oldSelector.split(">>").pop().trim();
864
+ const lastNewPart = newSelector.split(">>").pop().trim();
865
+ for (let i = 0; i < lines.length; i++) {
866
+ const line = lines[i];
867
+ const outerQ = detectOuterQuote(line, lastOldPart);
868
+ if (!outerQ)
869
+ continue;
870
+ const safe = sanitizeForInjection(lastNewPart, outerQ);
871
+ const replaced = replaceStringLiteralInLine(line, lastOldPart, safe);
872
+ if (replaced === null)
873
+ continue;
874
+ for (const ident of candidates) {
875
+ const baseName = ident.split(".")[0];
876
+ if (line.includes(baseName)) {
877
+ lines[i] = replaced;
878
+ return {
879
+ success: true,
880
+ reason: `upstream variable '${ident}'`,
881
+ lineUpdated: i,
882
+ };
883
+ }
884
+ }
885
+ }
886
+ return { success: false, reason: "no upstream variable" };
887
+ }
888
+ // ─────────────────────────────────────────────────────────────
889
+ // STRATEGY 4 - Chained Locator Dedup
890
+ // ─────────────────────────────────────────────────────────────
891
+ static strategyChainedLocator(lines, callerLine, oldSelector, newSelector) {
892
+ const blockStart = Math.max(0, callerLine - 10);
893
+ const chainBlock = lines.slice(blockStart, callerLine + 1).join("\n");
894
+ const deduped = deduplicateSelector(newSelector, oldSelector + "\n" + chainBlock);
895
+ if (deduped === newSelector)
896
+ return { success: false, reason: "no duplication detected" };
897
+ const lastOldPart = oldSelector.split(">>").pop().trim();
898
+ for (let i = callerLine; i >= blockStart; i--) {
899
+ const outerQ = detectOuterQuote(lines[i], lastOldPart);
900
+ if (!outerQ)
901
+ continue;
902
+ const safe = sanitizeForInjection(deduped, outerQ);
903
+ const replaced = replaceStringLiteralInLine(lines[i], lastOldPart, safe);
904
+ if (replaced !== null) {
905
+ lines[i] = replaced;
906
+ return {
907
+ success: true,
908
+ reason: `chained dedup: '${newSelector}' → '${deduped}'`,
909
+ lineUpdated: i,
910
+ };
911
+ }
912
+ }
913
+ return { success: false, reason: "dedup found but literal not replaced" };
914
+ }
915
+ // ─────────────────────────────────────────────────────────────
916
+ // STRATEGY 5 - Function/Template with Variable Preservation
917
+ // ─────────────────────────────────────────────────────────────
918
+ static strategyFunctionReturn(lines, callerLine, oldSelector, newSelector, domContext, dnaAttrHint) {
919
+ const callBlock = lines
920
+ .slice(Math.max(0, callerLine - 8), callerLine + 1)
921
+ .join("\n");
922
+ const fnCallPattern = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
923
+ const fnNames = new Set();
924
+ let m;
925
+ const playwrightBuiltins = new Set([
926
+ "locator",
927
+ "fill",
928
+ "click",
929
+ "check",
930
+ "uncheck",
931
+ "hover",
932
+ "dblclick",
933
+ "focus",
934
+ "waitFor",
935
+ "isVisible",
936
+ "getAttribute",
937
+ "innerText",
938
+ "innerHTML",
939
+ "textContent",
940
+ "page",
941
+ "filter",
942
+ "expect",
943
+ "selectOption",
944
+ "frameLocator",
945
+ ]);
946
+ while ((m = fnCallPattern.exec(callBlock)) !== null) {
947
+ if (!playwrightBuiltins.has(m[1]))
948
+ fnNames.add(m[1]);
949
+ }
950
+ if (fnNames.size === 0)
951
+ return { success: false, reason: "no function calls found near site" };
952
+ const newLeaf = newSelector.split(">>").pop().trim();
953
+ const oldSelectorAttrMatch = oldSelector.match(/\[([a-zA-Z][a-zA-Z0-9-]*)=/);
954
+ const expectedAttrName = oldSelectorAttrMatch?.[1] ?? null;
955
+ const resolvedAttrHint = dnaAttrHint ??
956
+ SourceUpdater.extractAttrValueFromDom(domContext ?? "", oldSelector) ??
957
+ undefined;
958
+ for (const fnName of fnNames) {
959
+ const defPattern = new RegExp(`(?:const|let|var|function)\\s+${fnName}\\b`);
960
+ for (let i = 0; i < lines.length; i++) {
961
+ if (!defPattern.test(lines[i]))
962
+ continue;
963
+ for (let j = i; j < Math.min(lines.length, i + 8); j++) {
964
+ const line = lines[j];
965
+ if (line.includes("`") && line.includes("${")) {
966
+ if (expectedAttrName && !line.includes(expectedAttrName))
967
+ continue;
968
+ if (!expectedAttrName)
969
+ continue;
970
+ const preserved = updateTemplateLiteralPreservingVars(line, oldSelector, newSelector, resolvedAttrHint);
971
+ if (preserved !== null && preserved !== line) {
972
+ lines[j] = preserved;
973
+ return {
974
+ success: true,
975
+ reason: `function template updated (vars preserved)`,
976
+ lineUpdated: j,
977
+ };
978
+ }
979
+ continue;
980
+ }
981
+ const lastOldPart = oldSelector.split(">>").pop().trim();
982
+ const outerQ = detectOuterQuote(line, lastOldPart);
983
+ if (!outerQ)
984
+ continue;
985
+ const safe = sanitizeForInjection(newLeaf, outerQ);
986
+ const replaced = replaceStringLiteralInLine(line, lastOldPart, safe);
987
+ if (replaced !== null) {
988
+ lines[j] = replaced;
989
+ return {
990
+ success: true,
991
+ reason: `function literal replaced`,
992
+ lineUpdated: j,
993
+ };
994
+ }
995
+ }
996
+ }
997
+ }
998
+ return { success: false, reason: "function strategy exhausted" };
999
+ }
1000
+ // ─────────────────────────────────────────────────────────────
1001
+ // STRATEGY 6 - Global Scan
1002
+ // ─────────────────────────────────────────────────────────────
1003
+ static strategyGlobalScan(lines, oldSelector, newSelector) {
1004
+ const lastOldPart = oldSelector.split(">>").pop().trim();
1005
+ const lastNewPart = newSelector.split(">>").pop().trim();
1006
+ for (let i = 0; i < lines.length; i++) {
1007
+ const deduped = deduplicateSelector(newSelector, lines[i]);
1008
+ const effectiveNew = deduped !== newSelector ? deduped : lastNewPart;
1009
+ const outerQ = detectOuterQuote(lines[i], lastOldPart);
1010
+ if (!outerQ)
1011
+ continue;
1012
+ const safe = sanitizeForInjection(effectiveNew, outerQ);
1013
+ const replaced = replaceStringLiteralInLine(lines[i], lastOldPart, safe);
1014
+ if (replaced !== null) {
1015
+ lines[i] = replaced;
1016
+ return { success: true, reason: "global scan", lineUpdated: i };
1017
+ }
1018
+ }
1019
+ return { success: false, reason: "not found anywhere in file" };
1020
+ }
1021
+ // ─────────────────────────────────────────────────────────────
1022
+ // UTILITIES
1023
+ // ─────────────────────────────────────────────────────────────
1024
+ static extractAttrValueFromDom(domContext, oldSelector) {
1025
+ const attrMatch = oldSelector.match(/\[([a-zA-Z-]+)=/);
1026
+ if (!attrMatch)
1027
+ return null;
1028
+ const attrName = attrMatch[1];
1029
+ const domValueMatch = domContext.match(new RegExp(`${attrName.replace(/-/g, "\\-")}=["']([^"']*)["']`));
1030
+ return domValueMatch?.[1] ?? null;
1031
+ }
1032
+ static resolveFilePath(raw) {
1033
+ if (!raw || typeof raw !== "string")
1034
+ return null;
1035
+ let clean = raw
1036
+ .replace(/^.*?at\s+/, "")
1037
+ .replace(/:\d+:\d+.*$/, "")
1038
+ .trim();
1039
+ if (!clean)
1040
+ return null;
1041
+ if (!path.isAbsolute(clean))
1042
+ clean = path.resolve(process.cwd(), clean);
1043
+ if (!fs.existsSync(clean))
1044
+ return null;
1045
+ // Reject directories: fs.readFileSync on a dir throws EISDIR. This guards
1046
+ // against ctx.filePath being empty → path.resolve(cwd, '') → cwd.
1047
+ const stat = fs.statSync(clean);
1048
+ if (!stat.isFile())
1049
+ return null;
1050
+ return clean;
1051
+ }
1052
+ static updateArgument(caller, action, oldArgument, newArgument) {
1053
+ const filePath = SourceUpdater.resolveFilePath(caller.filePath);
1054
+ if (!filePath)
1055
+ return { success: false, reason: `File not found: ${caller.filePath}` };
1056
+ const raw = fs.readFileSync(filePath, "utf8");
1057
+ const lines = raw.split(/\r?\n/);
1058
+ const callerLine = caller.line - 1;
1059
+ const windowSize = 5;
1060
+ const start = Math.max(0, callerLine - windowSize);
1061
+ const end = Math.min(lines.length - 1, callerLine + windowSize);
1062
+ const escapedAction = action.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1063
+ const escapedOldArg = oldArgument.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1064
+ for (const q of [`"`, `'`]) {
1065
+ const pattern = new RegExp(`(\\.${escapedAction}\\(${q})${escapedOldArg}(${q})`);
1066
+ for (let i = callerLine; i >= start; i--) {
1067
+ if (pattern.test(lines[i])) {
1068
+ const safeNew = sanitizeForInjection(newArgument, q);
1069
+ lines[i] = lines[i].replace(pattern, `$1${safeNew}$2`);
1070
+ persistFile(filePath, lines.join("\n"));
1071
+ return { success: true, reason: "argument updated", lineUpdated: i };
1072
+ }
1073
+ }
1074
+ for (let i = callerLine + 1; i <= end; i++) {
1075
+ if (pattern.test(lines[i])) {
1076
+ const safeNew = sanitizeForInjection(newArgument, q);
1077
+ lines[i] = lines[i].replace(pattern, `$1${safeNew}$2`);
1078
+ persistFile(filePath, lines.join("\n"));
1079
+ return { success: true, reason: "argument updated", lineUpdated: i };
1080
+ }
1081
+ }
1082
+ }
1083
+ logger_1.logger.warn(`Argument "${oldArgument}" not found near line ${caller.line}`);
1084
+ return { success: false, reason: "argument not found" };
1085
+ }
1086
+ /** Flush buffered advisories to console. Call after Playwright prints its test result. */
1087
+ static flushAdvisories() {
1088
+ astUpdater.flushAdvisories();
1089
+ }
1090
+ }
1091
+ exports.SourceUpdater = SourceUpdater;