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,475 @@
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.LLMService = void 0;
37
+ const dotenv = __importStar(require("dotenv"));
38
+ const SelaError_1 = require("../errors/SelaError");
39
+ const SanitizingAnthropic_1 = require("./SanitizingAnthropic");
40
+ const logger_1 = require("../utils/logger");
41
+ dotenv.config();
42
+ // ─────────────────────────────────────────────────────────────────
43
+ // SYSTEM PROMPT
44
+ // ─────────────────────────────────────────────────────────────────
45
+ const SYSTEM_PROMPT_TEXT = `### ROLE
46
+ You are an expert Playwright Automation Engineer specializing in self-healing test selectors (Project Sela).
47
+ You deeply understand Playwright's locator API and always prefer semantic, resilient selectors
48
+ over brittle CSS or XPath expressions.
49
+
50
+ ### STRICT RULES
51
+ 1. **NO PSEUDO-CLASSES**: NEVER use ':first-of-type', ':nth-child', etc., unless it is the ONLY way.
52
+ 2. **PRIORITIZE ATTRIBUTES**: Search DOM for stable attributes: id, data-testid, aria-label, name, role.
53
+ 3. **FRAME ACCURACY**: If a frame's ID changed, use the NEW ID from the DOM.
54
+ 4. **NO FX-ID**: Never return 'fx-id'.
55
+ 5. **VALID METHODS ONLY**: Only use real Playwright Locator methods.
56
+ 6. **RUNTIME SYNC (CRITICAL)**: The flat selector in "segments" MUST match the specificity of "chainSegments".
57
+ If your chain uses a 'name' or 'text' filter, you MUST include it in the flat string (e.g., using " >> internal:has-text="..."" or ":has-text('...')").
58
+ 7. **DATA-* ATTRIBUTE PRESERVATION (CRITICAL)**: If the original selector used a data-* attribute (e.g., data-role, data-testid, data-id), you MUST preserve that attribute in your response.
59
+ NEVER simplify "input[data-role='old-value']" to just "input".
60
+ Instead update the attribute value: "input[data-role='new-value']".
61
+ Even if the DOM context appears incomplete, when the DNA shows an attribute value changed, include the updated attribute value in your response.
62
+
63
+ ### VALID PLAYWRIGHT CHAIN METHODS
64
+ Locator producers: locator, frameLocator, getByRole, getByLabel, getByText,
65
+ getByPlaceholder, getByTestId, getByAltText, getByTitle
66
+ Narrowing: filter, first, last, nth
67
+
68
+ ### SMART CHAIN RESPONSE FORMAT
69
+ You MUST return a "chainSegments" array for the permanent code fix AND a matching "segments" array for the runtime proxy.
70
+
71
+ Build the chain in this priority order:
72
+ PRIORITY 1 - getByTestId
73
+ PRIORITY 2 - getByRole (ALWAYS include "name" if it exists in the DOM to ensure uniqueness)
74
+ PRIORITY 3 - getByLabel
75
+ PRIORITY 4 - getByText (use exact: true if possible)
76
+ NARROWING - Use filter({ hasText: "..." }) to isolate an element within a repeated container.
77
+
78
+ ### PROXY SELECTOR CONSTRUCTION (segments field)
79
+ The "segments[0].selector" string is used for an immediate "live" retry.
80
+ - AVOID generic selectors like "internal:role=button".
81
+ - PREFER specific selectors: ".user-item >> internal:has-text=\"Bob\" >> internal:role=button[name=\"Edit\"i]"
82
+ - This prevents "Strict Mode" violations where Playwright finds multiple matches and times out.
83
+
84
+ ### DEVELOPER INTENT PRESERVATION
85
+ Preserve the original code's structural pattern from "originalChainHint":
86
+ - If the original used a chain, return a chain.
87
+ - If the original was specific, stay specific.
88
+
89
+ ### TEMPLATE LITERAL / FUNCTION SELECTOR PRESERVATION
90
+ When the failing selector originates from a Template Literal or a function returning a template,
91
+ preserve the attribute structure - update the value inside the attribute rather than removing it.
92
+ For chain selectors: if the original was "#section-B >> input[data-role='user_legacy']"
93
+ and the DNA shows data-role changed to "user_active", return "#section-B >> input[data-role='user_active']".
94
+ NEVER simplify to "#section-B >> input" - that loses the attribute and breaks automated source healing.
95
+ The healing system depends on the attribute value being present in your response to patch the template variable.
96
+
97
+ ### TYPE SAFETY RULES
98
+ - A chain MUST end on a Locator (not FrameLocator).
99
+ - Never call frameLocator() on a Locator.
100
+
101
+ ### CONTENT CHANGE DETECTION
102
+ If the element's text changed, you MUST include:
103
+ "contentChange": { "oldText": "<previous>", "newText": "<current>" }
104
+
105
+ ### CONFIDENCE SCORE (MANDATORY)
106
+ You MUST include a numeric "confidence" field - an INTEGER in the range 0-100 -
107
+ representing how certain you are that the returned chain will resolve the
108
+ intended element on the next test run. Use this rubric:
109
+ - 90-100: stable id, data-testid, or unique role+name - zero ambiguity in the DOM.
110
+ - 70-89: strong semantic match, but some structural drift or competing candidates.
111
+ - 50-69: best-guess match - multiple plausible candidates required disambiguation.
112
+ - 1-49: speculative match - return this only when no better candidate exists.
113
+ NEVER omit this field. NEVER return it as a string, boolean, or null.
114
+ Reports rendered downstream will show "n/a" when this field is missing - which
115
+ is a clear failure signal to the developer reviewing the heal.
116
+
117
+ ### MANDATORY OUTPUT FORMAT
118
+ Return ONLY valid JSON.
119
+
120
+ Example output - Identifying a specific button in a list row:
121
+ {
122
+ "status": "FIXED",
123
+ "confidence": 92,
124
+ "chainSegments": [
125
+ { "type": "locator", "selector": ".user-item" },
126
+ { "type": "filter", "hasText": "Bob Smith" },
127
+ { "type": "getByRole", "role": "button", "name": "Modify Bob" }
128
+ ],
129
+ "segments": [
130
+ {
131
+ "type": "element",
132
+ "selector": ".user-item >> internal:has-text=\"Bob Smith\"i >> internal:role=button[name=\"Modify Bob\"i]"
133
+ }
134
+ ],
135
+ "explanation": "Updated container to .user-item and targeted the button by its new semantic name 'Modify Bob' to ensure a unique match."
136
+ }`;
137
+ // ─────────────────────────────────────────────────────────────────
138
+ // HELPERS
139
+ // ─────────────────────────────────────────────────────────────────
140
+ /**
141
+ * Reverse-engineer a SmartChainSegment hint from a flat selector
142
+ * string. This gives the AI context about how the original code
143
+ * was structured so it can preserve developer intent.
144
+ */
145
+ function inferOriginalChainHint(selector) {
146
+ const parts = selector.split(" >> ").map((s) => s.trim());
147
+ const segments = [];
148
+ for (const part of parts) {
149
+ // internal:role=button[name="..."]
150
+ const roleMatch = part.match(/^internal:role=([a-zA-Z]+)(?:\[name="([^"]+)"(?:i)?\])?/);
151
+ if (roleMatch) {
152
+ const seg = {
153
+ type: "getByRole",
154
+ role: roleMatch[1],
155
+ };
156
+ if (roleMatch[2])
157
+ seg.name = roleMatch[2];
158
+ segments.push(seg);
159
+ continue;
160
+ }
161
+ // internal:label=
162
+ const labelMatch = part.match(/^internal:label=["']?([^"'\]]+)["']?/);
163
+ if (labelMatch) {
164
+ segments.push({ type: "getByLabel", text: labelMatch[1].trim() });
165
+ continue;
166
+ }
167
+ // internal:text=
168
+ const textMatch = part.match(/^internal:text=["']([^"']+)["'](i)?/);
169
+ if (textMatch) {
170
+ segments.push({
171
+ type: "getByText",
172
+ text: textMatch[1],
173
+ exact: !textMatch[2],
174
+ });
175
+ continue;
176
+ }
177
+ // internal:attr=[placeholder=...]
178
+ const placeholderMatch = part.match(/internal:attr=\[placeholder=["']([^"']+)["']\]/);
179
+ if (placeholderMatch) {
180
+ segments.push({ type: "getByPlaceholder", text: placeholderMatch[1] });
181
+ continue;
182
+ }
183
+ // [data-testid=...]
184
+ const testIdMatch = part.match(/\[data-testid=["']([^"']+)["']\]/);
185
+ if (testIdMatch) {
186
+ segments.push({ type: "getByTestId", testId: testIdMatch[1] });
187
+ continue;
188
+ }
189
+ // iframe / frame selectors
190
+ if (part.includes("frame") ||
191
+ part.startsWith("iframe") ||
192
+ part.match(/^#[a-z-]*frame[a-z-]*/i)) {
193
+ segments.push({ type: "frameLocator", selector: part });
194
+ continue;
195
+ }
196
+ // Fallback: CSS locator
197
+ segments.push({ type: "locator", selector: part });
198
+ }
199
+ return segments;
200
+ }
201
+ const DEFAULT_LLM_MODEL = "claude-sonnet-4-20250514";
202
+ class LLMService {
203
+ anthropic;
204
+ model;
205
+ constructor(opts = {}) {
206
+ this.model = opts.model ?? DEFAULT_LLM_MODEL;
207
+ if (opts.anthropic) {
208
+ this.anthropic = opts.anthropic;
209
+ return;
210
+ }
211
+ const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
212
+ if (!apiKey) {
213
+ throw new SelaError_1.SelaError({
214
+ subsystem: "LLMService",
215
+ code: SelaError_1.LLM_SERVICE_CODES.NO_API_KEY,
216
+ reason: "ANTHROPIC_API_KEY not found - pass an `anthropic` client or `apiKey` to LLMService(), or set the env var",
217
+ });
218
+ }
219
+ // Route the default client through the PII/secrets sanitizer (T3) so all
220
+ // outbound DOM/DNA context is scrubbed before leaving the process.
221
+ this.anthropic = (0, SanitizingAnthropic_1.createSanitizingAnthropic)({ apiKey });
222
+ }
223
+ cleanJson(text) {
224
+ let cleaned = text
225
+ .replace(/```json/g, "")
226
+ .replace(/```/g, "")
227
+ .trim();
228
+ const firstBrace = cleaned.indexOf("{");
229
+ const lastBrace = cleaned.lastIndexOf("}");
230
+ if (firstBrace !== -1 && lastBrace !== -1) {
231
+ return cleaned.substring(firstBrace, lastBrace + 1);
232
+ }
233
+ return cleaned;
234
+ }
235
+ /**
236
+ * Attempts to infer contentChange when the AI did not return the field.
237
+ *
238
+ * Strategies (in order of confidence):
239
+ *
240
+ * 1. previousState.text vs has-text() in new selector
241
+ * 2. has-text() in old selector vs new selector
242
+ * 3. intentDescription text vs new selector
243
+ */
244
+ inferContentChange(parsed, request) {
245
+ // Extract new text from either chainSegments or flat new_selector
246
+ let newText = null;
247
+ if (parsed.chainSegments) {
248
+ // Look for text in the last getByText / getByRole with name
249
+ for (const seg of [...parsed.chainSegments].reverse()) {
250
+ if ((seg.type === "getByText" ||
251
+ seg.type === "getByLabel" ||
252
+ seg.type === "filter") &&
253
+ seg.text) {
254
+ newText = seg.text;
255
+ break;
256
+ }
257
+ if (seg.type === "getByRole" && seg.name) {
258
+ newText = seg.name;
259
+ break;
260
+ }
261
+ if (seg.type === "filter" && seg.hasText) {
262
+ newText = seg.hasText;
263
+ break;
264
+ }
265
+ }
266
+ }
267
+ if (!newText) {
268
+ const newSelector = parsed.new_selector ?? "";
269
+ const newHasText = newSelector.match(/has-text\(["']([^"']+)["']\)/);
270
+ if (newHasText)
271
+ newText = newHasText[1];
272
+ }
273
+ if (!newText)
274
+ return undefined;
275
+ // Strategy 1: DNA text
276
+ const prevText = request.previousState?.text;
277
+ if (prevText && prevText !== newText) {
278
+ const prevWords = prevText.split(/\s+/);
279
+ const newWords = newText.split(/\s+/);
280
+ const lenDiff = Math.abs(prevWords.length - newWords.length);
281
+ let diffCount = 0;
282
+ const minLen = Math.min(prevWords.length, newWords.length);
283
+ for (let i = 0; i < minLen; i++) {
284
+ if (prevWords[i] !== newWords[i])
285
+ diffCount++;
286
+ }
287
+ if (lenDiff <= 1 && diffCount <= 3 && diffCount > 0) {
288
+ logger_1.logger.debug(`contentChange inferred from DNA text: "${prevText}" → "${newText}"`);
289
+ return { oldText: prevText, newText };
290
+ }
291
+ }
292
+ // Strategy 2: has-text in old selector
293
+ const oldHasText = request.failedSelector.match(/has-text\(["']([^"']+)["']\)/);
294
+ if (oldHasText && oldHasText[1] !== newText) {
295
+ logger_1.logger.debug(`contentChange inferred from has-text diff: "${oldHasText[1]}" → "${newText}"`);
296
+ return { oldText: oldHasText[1], newText };
297
+ }
298
+ // Strategy 3: intent description
299
+ const intentTextMatch = request.targetIntent?.match(/with text ["']([^"']+)["']/i);
300
+ if (intentTextMatch && intentTextMatch[1] !== newText) {
301
+ const oldIntentText = intentTextMatch[1];
302
+ const intentWords = oldIntentText.split(/\s+/);
303
+ const newWords = newText.split(/\s+/);
304
+ const lenDiff = Math.abs(intentWords.length - newWords.length);
305
+ let diffCount = 0;
306
+ const minLen = Math.min(intentWords.length, newWords.length);
307
+ for (let i = 0; i < minLen; i++) {
308
+ if (intentWords[i] !== newWords[i])
309
+ diffCount++;
310
+ }
311
+ if (lenDiff <= 1 && diffCount <= 3 && diffCount > 0) {
312
+ logger_1.logger.debug(`contentChange inferred from intent: "${oldIntentText}" → "${newText}"`);
313
+ return { oldText: oldIntentText, newText };
314
+ }
315
+ }
316
+ return undefined;
317
+ }
318
+ async getFix(request) {
319
+ const dnaSummary = request.previousState;
320
+ // Reverse-engineer the original chain structure for the AI
321
+ const originalChainHint = inferOriginalChainHint(request.failedSelector);
322
+ const chainHintJson = JSON.stringify(originalChainHint, null, 2);
323
+ const userMessage = `
324
+ ### PROBLEM
325
+ A Playwright test failed. Analyze the full selector path and return a Smart Chain fix.
326
+
327
+ ### FAILED SELECTOR (Full Path):
328
+ "${request.failedSelector}"
329
+
330
+ ### ORIGINAL CHAIN STRUCTURE (reverse-engineered from failed selector):
331
+ ${chainHintJson}
332
+ Use this to understand the developer's original intent. Preserve the same structural
333
+ pattern in your "chainSegments" where possible (e.g., if original used a filter + getByRole
334
+ pattern, try to keep filter + getByRole if the element still exists in a list context).
335
+
336
+ ### INTENT:
337
+ ${request.targetIntent}
338
+
339
+ ### PREVIOUS DNA (Element Fingerprint):
340
+ ${dnaSummary && Object.keys(dnaSummary).length > 0
341
+ ? JSON.stringify(dnaSummary, null, 2)
342
+ : "NO DNA (First run - infer from selector and DOM)"}
343
+
344
+ ### CURRENT DOM SNIPPET:
345
+ ${request.currentDom}
346
+
347
+ ### YOUR TASK
348
+ 1. Find the element in the current DOM.
349
+ 2. Build a "chainSegments" array using Playwright's semantic API (priority: getByTestId > getByRole > getByLabel > getByText > filter+narrow > locator).
350
+ 3. Preserve the developer's chain structure (filter patterns, role patterns) when possible.
351
+ 4. Populate "contentChange" if the element's visible text changed.
352
+ 5. Also populate "segments" (legacy format) for backward compatibility.
353
+
354
+ Return ONLY a raw JSON object - no markdown, no preamble.`;
355
+ try {
356
+ const msg = await this.anthropic.messages.create({
357
+ model: this.model,
358
+ max_tokens: 1024,
359
+ system: SYSTEM_PROMPT_TEXT +
360
+ "\nIMPORTANT: Respond ONLY with a raw JSON object.",
361
+ messages: [{ role: "user", content: userMessage }],
362
+ });
363
+ const rawText = msg.content[0].type === "text" ? msg.content[0].text : "";
364
+ logger_1.logger.debug(`AI Raw Response: ${rawText}`);
365
+ const jsonString = this.cleanJson(rawText);
366
+ if (!jsonString)
367
+ throw new Error("AI did not return a valid JSON object");
368
+ const parsed = JSON.parse(jsonString);
369
+ // ── Normalise confidence: keep only finite numbers in [0,100] ─────
370
+ // Anything else (undefined, NaN, null, string, out-of-range) collapses
371
+ // to `undefined` so reports render "n/a" instead of synthesising 0.
372
+ const rawConf = parsed.confidence;
373
+ if (typeof rawConf === "number" && Number.isFinite(rawConf)) {
374
+ parsed.confidence = Math.max(0, Math.min(100, Math.round(rawConf)));
375
+ }
376
+ else {
377
+ if (rawConf !== undefined) {
378
+ logger_1.logger.warn(`Discarded non-numeric confidence: ${JSON.stringify(rawConf)}`);
379
+ }
380
+ parsed.confidence = undefined;
381
+ }
382
+ // ── Normalize chainSegments → segments (legacy) ───────────────────
383
+ if (parsed.chainSegments && Array.isArray(parsed.chainSegments)) {
384
+ // Reconstruct a flat new_selector from chain segments for legacy paths
385
+ const flatParts = [];
386
+ for (const seg of parsed.chainSegments) {
387
+ switch (seg.type) {
388
+ case "locator":
389
+ case "frameLocator":
390
+ flatParts.push(seg.selector);
391
+ break;
392
+ case "getByRole":
393
+ flatParts.push(seg.name
394
+ ? `[role="${seg.role}"]:has-text("${seg.name}")`
395
+ : `[role="${seg.role}"]`);
396
+ break;
397
+ case "getByLabel":
398
+ flatParts.push(`label:has-text("${seg.text}")`);
399
+ break;
400
+ case "getByText":
401
+ flatParts.push(`:has-text("${seg.text}")`);
402
+ break;
403
+ case "getByTestId":
404
+ flatParts.push(`[data-testid="${seg.testId}"]`);
405
+ break;
406
+ case "getByPlaceholder":
407
+ flatParts.push(`[placeholder="${seg.text}"]`);
408
+ break;
409
+ case "filter":
410
+ if (seg.hasText)
411
+ flatParts.push(`:has-text("${seg.hasText}")`);
412
+ break;
413
+ default:
414
+ break;
415
+ }
416
+ }
417
+ if (flatParts.length > 0 && !parsed.new_selector) {
418
+ parsed.new_selector = flatParts.join(" >> ");
419
+ }
420
+ // Also inject into legacy segments if missing
421
+ if (!parsed.segments || parsed.segments.length === 0) {
422
+ const frameSegs = parsed.chainSegments.filter((s) => s.type === "frameLocator");
423
+ const elemSegs = parsed.chainSegments.filter((s) => s.type !== "frameLocator");
424
+ parsed.segments = [
425
+ ...frameSegs.map((s) => ({
426
+ type: "frame",
427
+ selector: s.selector,
428
+ })),
429
+ { type: "element", selector: parsed.new_selector ?? "" },
430
+ ];
431
+ }
432
+ }
433
+ // ── Normalize legacy new_selector → segments ──────────────────────
434
+ if (!parsed.segments && parsed.new_selector) {
435
+ const parts = parsed.new_selector
436
+ .split(" >> ")
437
+ .map((s) => s.trim());
438
+ parsed.segments = parts.map((s, i) => ({
439
+ type: i === parts.length - 1 ? "element" : "frame",
440
+ selector: s,
441
+ }));
442
+ }
443
+ if (!parsed.segments || !Array.isArray(parsed.segments)) {
444
+ throw new Error("AI response missing 'segments' array");
445
+ }
446
+ parsed.new_selector =
447
+ parsed.new_selector ??
448
+ parsed.segments.map((s) => s.selector).join(" >> ");
449
+ // Attach original chain hint for ASTUpdater's partial healing logic
450
+ parsed.originalChainHint = originalChainHint;
451
+ // ── Ensure contentChange is populated ──────────────────────────────
452
+ if (!parsed.contentChange || typeof parsed.contentChange !== "object") {
453
+ const inferred = this.inferContentChange(parsed, request);
454
+ if (inferred) {
455
+ parsed.contentChange = inferred;
456
+ }
457
+ }
458
+ if (parsed.contentChange) {
459
+ logger_1.logger.debug(`contentChange: "${parsed.contentChange.oldText}" → "${parsed.contentChange.newText}"`);
460
+ }
461
+ return parsed;
462
+ }
463
+ catch (error) {
464
+ logger_1.logger.error("Analysis failed", error.message);
465
+ return {
466
+ status: "NOT_FOUND",
467
+ segments: [],
468
+ new_selector: null,
469
+ confidence: undefined,
470
+ explanation: `AI Error: ${error.message}`,
471
+ };
472
+ }
473
+ }
474
+ }
475
+ exports.LLMService = LLMService;
@@ -0,0 +1,69 @@
1
+ import { AnchorKey, SourceSpan } from "./AnchorResolver";
2
+ /**
3
+ * A single edit. `replace` edits carry an `anchor` and are RE-RESOLVED at
4
+ * apply-time (span recomputed from the fresh parse). `insert` edits (forks)
5
+ * carry a literal `span` computed against the same content being applied.
6
+ */
7
+ export interface AnchoredEdit {
8
+ kind: "replace" | "insert";
9
+ replacement: string;
10
+ /** Replace target — re-resolved via resolveAnchor at apply time. */
11
+ anchor?: AnchorKey;
12
+ /** Literal span for inserts (and as a fallback for anchorless replaces). */
13
+ span?: SourceSpan;
14
+ /**
15
+ * Require the replacement to be a Locator-returning expression. Defaults to
16
+ * true for `replace`. Set false for inserts / non-locator edits.
17
+ */
18
+ requireLocator?: boolean;
19
+ }
20
+ export interface FileBatch {
21
+ filePath: string;
22
+ edits: AnchoredEdit[];
23
+ }
24
+ export type ApplyStatus = "applied" | "rolled-back" | "no-op" | "skipped";
25
+ export interface ApplyOutcome {
26
+ filePath: string;
27
+ status: ApplyStatus;
28
+ reason: string;
29
+ written: boolean;
30
+ /** Content after the run — the new content when applied, else the original. */
31
+ finalContent: string;
32
+ /** Edits that could not be applied (stale anchor, type-incompat, …). */
33
+ skipped: Array<{
34
+ reason: string;
35
+ }>;
36
+ }
37
+ export interface ApplyOptions {
38
+ /** When false, compute + gate but never touch disk. Default true. */
39
+ write?: boolean;
40
+ /** Honour SELA_DRY_RUN by leaving disk untouched. Default true. */
41
+ respectDryRun?: boolean;
42
+ }
43
+ export declare class MutationApplier {
44
+ /** Apply many batches; each file is processed independently. */
45
+ static applyBatches(batches: FileBatch[], opts?: ApplyOptions): ApplyOutcome[];
46
+ /**
47
+ * Apply a file batch under a per-file cross-process lock (SELA-5).
48
+ *
49
+ * The lock makes the read-modify-write of a single source file atomic across
50
+ * concurrent Playwright workers — the real lost-update risk is two specs that
51
+ * heal a SHARED page-object simultaneously (each reads the same baseline and
52
+ * the second write drops the first's fix). The lock dir is keyed by file
53
+ * path, so heals on DIFFERENT files never serialise — worker scalability is
54
+ * preserved. Read-only / dry-run runs skip the lock entirely (no disk RMW).
55
+ *
56
+ * A stale lock (crashed holder) is reclaimed after 30s so a worker crash can
57
+ * never permanently wedge CI.
58
+ */
59
+ static applyFileBatch(batch: FileBatch, opts?: ApplyOptions): ApplyOutcome;
60
+ private static applyFileBatchUnlocked;
61
+ }
62
+ /**
63
+ * Cheap return-type pre-check. The replacement must be an expression whose
64
+ * OUTERMOST call is a Locator-returning Playwright method (locator / getBy* /
65
+ * frameLocator / nth / first / last / filter / and / or). Rejects actions
66
+ * (.click()), string queries (.title(), .textContent()), bare literals, etc.
67
+ */
68
+ export declare function isLocatorReturningExpr(text: string): boolean;
69
+ //# sourceMappingURL=MutationApplier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MutationApplier.d.ts","sourceRoot":"","sources":["../../src/services/MutationApplier.ts"],"names":[],"mappings":"AA2BA,OAAO,EACL,SAAS,EAET,UAAU,EACX,MAAM,kBAAkB,CAAC;AAY1B;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AAED,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,OAAO,GAAG,SAAS,CAAC;AAE1E,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,YAAY,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,OAAO,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,mEAAmE;IACnE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAeD,qBAAa,eAAe;IAC1B,gEAAgE;IAChE,MAAM,CAAC,YAAY,CACjB,OAAO,EAAE,SAAS,EAAE,EACpB,IAAI,GAAE,YAAiB,GACtB,YAAY,EAAE;IAIjB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,cAAc,CACnB,KAAK,EAAE,SAAS,EAChB,IAAI,GAAE,YAAiB,GACtB,YAAY;IAqCf,OAAO,CAAC,MAAM,CAAC,sBAAsB;CA2KtC;AA2CD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAkB5D"}