sela-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +93 -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 +140 -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 +133 -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/refactor.d.ts +3 -0
  19. package/dist/cli/commands/refactor.d.ts.map +1 -0
  20. package/dist/cli/commands/refactor.js +30 -0
  21. package/dist/cli/commands/status.d.ts +3 -0
  22. package/dist/cli/commands/status.d.ts.map +1 -0
  23. package/dist/cli/commands/status.js +51 -0
  24. package/dist/cli/commands/sync.d.ts +3 -0
  25. package/dist/cli/commands/sync.d.ts.map +1 -0
  26. package/dist/cli/commands/sync.js +123 -0
  27. package/dist/cli/index.d.ts +2 -0
  28. package/dist/cli/index.d.ts.map +1 -0
  29. package/dist/cli/index.js +42 -0
  30. package/dist/cli/ui/DnaTable.d.ts +3 -0
  31. package/dist/cli/ui/DnaTable.d.ts.map +1 -0
  32. package/dist/cli/ui/DnaTable.js +70 -0
  33. package/dist/cli/ui/LocatorPicker.d.ts +8 -0
  34. package/dist/cli/ui/LocatorPicker.d.ts.map +1 -0
  35. package/dist/cli/ui/LocatorPicker.js +33 -0
  36. package/dist/cli/ui/ProgressReporter.d.ts +11 -0
  37. package/dist/cli/ui/ProgressReporter.d.ts.map +1 -0
  38. package/dist/cli/ui/ProgressReporter.js +33 -0
  39. package/dist/cli/ui/RefactorWizard.d.ts +26 -0
  40. package/dist/cli/ui/RefactorWizard.d.ts.map +1 -0
  41. package/dist/cli/ui/RefactorWizard.js +269 -0
  42. package/dist/config/ConfigLoader.d.ts +15 -0
  43. package/dist/config/ConfigLoader.d.ts.map +1 -0
  44. package/dist/config/ConfigLoader.js +174 -0
  45. package/dist/config/SelaConfig.d.ts +67 -0
  46. package/dist/config/SelaConfig.d.ts.map +1 -0
  47. package/dist/config/SelaConfig.js +57 -0
  48. package/dist/config/defineConfig.d.ts +3 -0
  49. package/dist/config/defineConfig.d.ts.map +1 -0
  50. package/dist/config/defineConfig.js +9 -0
  51. package/dist/engine/FixwrightEngine.d.ts +24 -0
  52. package/dist/engine/FixwrightEngine.d.ts.map +1 -0
  53. package/dist/engine/FixwrightEngine.js +403 -0
  54. package/dist/engine/HealingRegistry.d.ts +40 -0
  55. package/dist/engine/HealingRegistry.d.ts.map +1 -0
  56. package/dist/engine/HealingRegistry.js +98 -0
  57. package/dist/engine/singleton.d.ts +3 -0
  58. package/dist/engine/singleton.d.ts.map +1 -0
  59. package/dist/engine/singleton.js +5 -0
  60. package/dist/fixtures/expectProxy.d.ts +12 -0
  61. package/dist/fixtures/expectProxy.d.ts.map +1 -0
  62. package/dist/fixtures/expectProxy.js +228 -0
  63. package/dist/fixtures/index.d.ts +6 -0
  64. package/dist/fixtures/index.d.ts.map +1 -0
  65. package/dist/fixtures/index.js +688 -0
  66. package/dist/fixtures/moduleExpect.d.ts +2 -0
  67. package/dist/fixtures/moduleExpect.d.ts.map +1 -0
  68. package/dist/fixtures/moduleExpect.js +46 -0
  69. package/dist/index.d.ts +7 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +28 -0
  72. package/dist/services/ASTSourceUpdater.d.ts +79 -0
  73. package/dist/services/ASTSourceUpdater.d.ts.map +1 -0
  74. package/dist/services/ASTSourceUpdater.js +3177 -0
  75. package/dist/services/ArgumentTypeAnalyzer.d.ts +26 -0
  76. package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -0
  77. package/dist/services/ArgumentTypeAnalyzer.js +92 -0
  78. package/dist/services/BlastRadiusAnalyzer.d.ts +15 -0
  79. package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -0
  80. package/dist/services/BlastRadiusAnalyzer.js +103 -0
  81. package/dist/services/ChainValidator.d.ts +76 -0
  82. package/dist/services/ChainValidator.d.ts.map +1 -0
  83. package/dist/services/ChainValidator.js +569 -0
  84. package/dist/services/CrossFileHealer.d.ts +6 -0
  85. package/dist/services/CrossFileHealer.d.ts.map +1 -0
  86. package/dist/services/CrossFileHealer.js +134 -0
  87. package/dist/services/DefinitionTracer.d.ts +41 -0
  88. package/dist/services/DefinitionTracer.d.ts.map +1 -0
  89. package/dist/services/DefinitionTracer.js +350 -0
  90. package/dist/services/DnaEditorService.d.ts +31 -0
  91. package/dist/services/DnaEditorService.d.ts.map +1 -0
  92. package/dist/services/DnaEditorService.js +198 -0
  93. package/dist/services/DnaIndexService.d.ts +24 -0
  94. package/dist/services/DnaIndexService.d.ts.map +1 -0
  95. package/dist/services/DnaIndexService.js +131 -0
  96. package/dist/services/HealingAdvisory.d.ts +22 -0
  97. package/dist/services/HealingAdvisory.d.ts.map +1 -0
  98. package/dist/services/HealingAdvisory.js +42 -0
  99. package/dist/services/HealthReportService.d.ts +10 -0
  100. package/dist/services/HealthReportService.d.ts.map +1 -0
  101. package/dist/services/HealthReportService.js +84 -0
  102. package/dist/services/InitializerUpdater.d.ts +16 -0
  103. package/dist/services/InitializerUpdater.d.ts.map +1 -0
  104. package/dist/services/InitializerUpdater.js +37 -0
  105. package/dist/services/IntentAuditor.d.ts +39 -0
  106. package/dist/services/IntentAuditor.d.ts.map +1 -0
  107. package/dist/services/IntentAuditor.js +302 -0
  108. package/dist/services/LLMService.d.ts +100 -0
  109. package/dist/services/LLMService.d.ts.map +1 -0
  110. package/dist/services/LLMService.js +439 -0
  111. package/dist/services/SafetyGuard.d.ts +65 -0
  112. package/dist/services/SafetyGuard.d.ts.map +1 -0
  113. package/dist/services/SafetyGuard.js +524 -0
  114. package/dist/services/SnapshotService.d.ts +11 -0
  115. package/dist/services/SnapshotService.d.ts.map +1 -0
  116. package/dist/services/SnapshotService.js +349 -0
  117. package/dist/services/SourceLinkService.d.ts +26 -0
  118. package/dist/services/SourceLinkService.d.ts.map +1 -0
  119. package/dist/services/SourceLinkService.js +156 -0
  120. package/dist/services/SourceUpdater.d.ts +45 -0
  121. package/dist/services/SourceUpdater.d.ts.map +1 -0
  122. package/dist/services/SourceUpdater.js +1021 -0
  123. package/dist/services/TemplateDiffService.d.ts +25 -0
  124. package/dist/services/TemplateDiffService.d.ts.map +1 -0
  125. package/dist/services/TemplateDiffService.js +331 -0
  126. package/dist/services/types.d.ts +59 -0
  127. package/dist/services/types.d.ts.map +1 -0
  128. package/dist/services/types.js +2 -0
  129. package/dist/storage/SnapshotManager.d.ts +5 -0
  130. package/dist/storage/SnapshotManager.d.ts.map +1 -0
  131. package/dist/storage/SnapshotManager.js +31 -0
  132. package/dist/types/index.d.ts +95 -0
  133. package/dist/types/index.d.ts.map +1 -0
  134. package/dist/types/index.js +7 -0
  135. package/dist/utils/DOMUtils.d.ts +33 -0
  136. package/dist/utils/DOMUtils.d.ts.map +1 -0
  137. package/dist/utils/DOMUtils.js +179 -0
  138. package/dist/utils/StackUtils.d.ts +11 -0
  139. package/dist/utils/StackUtils.d.ts.map +1 -0
  140. package/dist/utils/StackUtils.js +120 -0
  141. package/dist/vendor/enquirer.d.ts +33 -0
  142. package/dist/vendor/enquirer.d.ts.map +1 -0
  143. package/dist/vendor/enquirer.js +11 -0
  144. package/package.json +67 -0
@@ -0,0 +1,439 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.LLMService = void 0;
40
+ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
41
+ const dotenv = __importStar(require("dotenv"));
42
+ dotenv.config();
43
+ // ─────────────────────────────────────────────────────────────────
44
+ // SYSTEM PROMPT
45
+ // ─────────────────────────────────────────────────────────────────
46
+ const SYSTEM_PROMPT_TEXT = `### ROLE
47
+ You are an expert Playwright Automation Engineer specializing in self-healing test selectors (Project Sela).
48
+ You deeply understand Playwright's locator API and always prefer semantic, resilient selectors
49
+ over brittle CSS or XPath expressions.
50
+
51
+ ### STRICT RULES
52
+ 1. **NO PSEUDO-CLASSES**: NEVER use ':first-of-type', ':nth-child', etc., unless it is the ONLY way.
53
+ 2. **PRIORITIZE ATTRIBUTES**: Search DOM for stable attributes: id, data-testid, aria-label, name, role.
54
+ 3. **FRAME ACCURACY**: If a frame's ID changed, use the NEW ID from the DOM.
55
+ 4. **NO FX-ID**: Never return 'fx-id'.
56
+ 5. **VALID METHODS ONLY**: Only use real Playwright Locator methods.
57
+ 6. **RUNTIME SYNC (CRITICAL)**: The flat selector in "segments" MUST match the specificity of "chainSegments".
58
+ 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('...')").
59
+ 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.
60
+ NEVER simplify "input[data-role='old-value']" to just "input".
61
+ Instead update the attribute value: "input[data-role='new-value']".
62
+ Even if the DOM context appears incomplete, when the DNA shows an attribute value changed, include the updated attribute value in your response.
63
+
64
+ ### VALID PLAYWRIGHT CHAIN METHODS
65
+ Locator producers: locator, frameLocator, getByRole, getByLabel, getByText,
66
+ getByPlaceholder, getByTestId, getByAltText, getByTitle
67
+ Narrowing: filter, first, last, nth
68
+
69
+ ### SMART CHAIN RESPONSE FORMAT
70
+ You MUST return a "chainSegments" array for the permanent code fix AND a matching "segments" array for the runtime proxy.
71
+
72
+ Build the chain in this priority order:
73
+ PRIORITY 1 — getByTestId
74
+ PRIORITY 2 — getByRole (ALWAYS include "name" if it exists in the DOM to ensure uniqueness)
75
+ PRIORITY 3 — getByLabel
76
+ PRIORITY 4 — getByText (use exact: true if possible)
77
+ NARROWING — Use filter({ hasText: "..." }) to isolate an element within a repeated container.
78
+
79
+ ### PROXY SELECTOR CONSTRUCTION (segments field)
80
+ The "segments[0].selector" string is used for an immediate "live" retry.
81
+ - AVOID generic selectors like "internal:role=button".
82
+ - PREFER specific selectors: ".user-item >> internal:has-text=\"Bob\" >> internal:role=button[name=\"Edit\"i]"
83
+ - This prevents "Strict Mode" violations where Playwright finds multiple matches and times out.
84
+
85
+ ### DEVELOPER INTENT PRESERVATION
86
+ Preserve the original code's structural pattern from "originalChainHint":
87
+ - If the original used a chain, return a chain.
88
+ - If the original was specific, stay specific.
89
+
90
+ ### TEMPLATE LITERAL / FUNCTION SELECTOR PRESERVATION
91
+ When the failing selector originates from a Template Literal or a function returning a template,
92
+ preserve the attribute structure — update the value inside the attribute rather than removing it.
93
+ For chain selectors: if the original was "#section-B >> input[data-role='user_legacy']"
94
+ and the DNA shows data-role changed to "user_active", return "#section-B >> input[data-role='user_active']".
95
+ NEVER simplify to "#section-B >> input" — that loses the attribute and breaks automated source healing.
96
+ The healing system depends on the attribute value being present in your response to patch the template variable.
97
+
98
+ ### TYPE SAFETY RULES
99
+ - A chain MUST end on a Locator (not FrameLocator).
100
+ - Never call frameLocator() on a Locator.
101
+
102
+ ### CONTENT CHANGE DETECTION
103
+ If the element's text changed, you MUST include:
104
+ "contentChange": { "oldText": "<previous>", "newText": "<current>" }
105
+
106
+ ### MANDATORY OUTPUT FORMAT
107
+ Return ONLY valid JSON.
108
+
109
+ Example output — Identifying a specific button in a list row:
110
+ {
111
+ "status": "FIXED",
112
+ "chainSegments": [
113
+ { "type": "locator", "selector": ".user-item" },
114
+ { "type": "filter", "hasText": "Bob Smith" },
115
+ { "type": "getByRole", "role": "button", "name": "Modify Bob" }
116
+ ],
117
+ "segments": [
118
+ {
119
+ "type": "element",
120
+ "selector": ".user-item >> internal:has-text=\"Bob Smith\"i >> internal:role=button[name=\"Modify Bob\"i]"
121
+ }
122
+ ],
123
+ "explanation": "Updated container to .user-item and targeted the button by its new semantic name 'Modify Bob' to ensure a unique match."
124
+ }`;
125
+ // ─────────────────────────────────────────────────────────────────
126
+ // HELPERS
127
+ // ─────────────────────────────────────────────────────────────────
128
+ /**
129
+ * Reverse-engineer a SmartChainSegment hint from a flat selector
130
+ * string. This gives the AI context about how the original code
131
+ * was structured so it can preserve developer intent.
132
+ */
133
+ function inferOriginalChainHint(selector) {
134
+ const parts = selector.split(" >> ").map((s) => s.trim());
135
+ const segments = [];
136
+ for (const part of parts) {
137
+ // internal:role=button[name="..."]
138
+ const roleMatch = part.match(/^internal:role=([a-zA-Z]+)(?:\[name="([^"]+)"(?:i)?\])?/);
139
+ if (roleMatch) {
140
+ const seg = {
141
+ type: "getByRole",
142
+ role: roleMatch[1],
143
+ };
144
+ if (roleMatch[2])
145
+ seg.name = roleMatch[2];
146
+ segments.push(seg);
147
+ continue;
148
+ }
149
+ // internal:label=
150
+ const labelMatch = part.match(/^internal:label=["']?([^"'\]]+)["']?/);
151
+ if (labelMatch) {
152
+ segments.push({ type: "getByLabel", text: labelMatch[1].trim() });
153
+ continue;
154
+ }
155
+ // internal:text=
156
+ const textMatch = part.match(/^internal:text=["']([^"']+)["'](i)?/);
157
+ if (textMatch) {
158
+ segments.push({
159
+ type: "getByText",
160
+ text: textMatch[1],
161
+ exact: !textMatch[2],
162
+ });
163
+ continue;
164
+ }
165
+ // internal:attr=[placeholder=...]
166
+ const placeholderMatch = part.match(/internal:attr=\[placeholder=["']([^"']+)["']\]/);
167
+ if (placeholderMatch) {
168
+ segments.push({ type: "getByPlaceholder", text: placeholderMatch[1] });
169
+ continue;
170
+ }
171
+ // [data-testid=...]
172
+ const testIdMatch = part.match(/\[data-testid=["']([^"']+)["']\]/);
173
+ if (testIdMatch) {
174
+ segments.push({ type: "getByTestId", testId: testIdMatch[1] });
175
+ continue;
176
+ }
177
+ // iframe / frame selectors
178
+ if (part.includes("frame") ||
179
+ part.startsWith("iframe") ||
180
+ part.match(/^#[a-z-]*frame[a-z-]*/i)) {
181
+ segments.push({ type: "frameLocator", selector: part });
182
+ continue;
183
+ }
184
+ // Fallback: CSS locator
185
+ segments.push({ type: "locator", selector: part });
186
+ }
187
+ return segments;
188
+ }
189
+ // ─────────────────────────────────────────────────────────────────
190
+ // LLM SERVICE
191
+ // ─────────────────────────────────────────────────────────────────
192
+ class LLMService {
193
+ anthropic;
194
+ constructor() {
195
+ const apiKey = process.env.ANTHROPIC_API_KEY || "";
196
+ if (!apiKey)
197
+ throw new Error("ANTHROPIC_API_KEY not found in .env");
198
+ this.anthropic = new sdk_1.default({ apiKey });
199
+ }
200
+ cleanJson(text) {
201
+ let cleaned = text
202
+ .replace(/```json/g, "")
203
+ .replace(/```/g, "")
204
+ .trim();
205
+ const firstBrace = cleaned.indexOf("{");
206
+ const lastBrace = cleaned.lastIndexOf("}");
207
+ if (firstBrace !== -1 && lastBrace !== -1) {
208
+ return cleaned.substring(firstBrace, lastBrace + 1);
209
+ }
210
+ return cleaned;
211
+ }
212
+ /**
213
+ * Attempts to infer contentChange when the AI did not return the field.
214
+ *
215
+ * Strategies (in order of confidence):
216
+ *
217
+ * 1. previousState.text vs has-text() in new selector
218
+ * 2. has-text() in old selector vs new selector
219
+ * 3. intentDescription text vs new selector
220
+ */
221
+ inferContentChange(parsed, request) {
222
+ // Extract new text from either chainSegments or flat new_selector
223
+ let newText = null;
224
+ if (parsed.chainSegments) {
225
+ // Look for text in the last getByText / getByRole with name
226
+ for (const seg of [...parsed.chainSegments].reverse()) {
227
+ if ((seg.type === "getByText" ||
228
+ seg.type === "getByLabel" ||
229
+ seg.type === "filter") &&
230
+ seg.text) {
231
+ newText = seg.text;
232
+ break;
233
+ }
234
+ if (seg.type === "getByRole" && seg.name) {
235
+ newText = seg.name;
236
+ break;
237
+ }
238
+ if (seg.type === "filter" && seg.hasText) {
239
+ newText = seg.hasText;
240
+ break;
241
+ }
242
+ }
243
+ }
244
+ if (!newText) {
245
+ const newSelector = parsed.new_selector ?? "";
246
+ const newHasText = newSelector.match(/has-text\(["']([^"']+)["']\)/);
247
+ if (newHasText)
248
+ newText = newHasText[1];
249
+ }
250
+ if (!newText)
251
+ return undefined;
252
+ // Strategy 1: DNA text
253
+ const prevText = request.previousState?.text;
254
+ if (prevText && prevText !== newText) {
255
+ const prevWords = prevText.split(/\s+/);
256
+ const newWords = newText.split(/\s+/);
257
+ const lenDiff = Math.abs(prevWords.length - newWords.length);
258
+ let diffCount = 0;
259
+ const minLen = Math.min(prevWords.length, newWords.length);
260
+ for (let i = 0; i < minLen; i++) {
261
+ if (prevWords[i] !== newWords[i])
262
+ diffCount++;
263
+ }
264
+ if (lenDiff <= 1 && diffCount <= 3 && diffCount > 0) {
265
+ console.log(`[LLMService] 🔍 contentChange inferred from DNA text: "${prevText}" → "${newText}"`);
266
+ return { oldText: prevText, newText };
267
+ }
268
+ }
269
+ // Strategy 2: has-text in old selector
270
+ const oldHasText = request.failedSelector.match(/has-text\(["']([^"']+)["']\)/);
271
+ if (oldHasText && oldHasText[1] !== newText) {
272
+ console.log(`[LLMService] 🔍 contentChange inferred from has-text diff: "${oldHasText[1]}" → "${newText}"`);
273
+ return { oldText: oldHasText[1], newText };
274
+ }
275
+ // Strategy 3: intent description
276
+ const intentTextMatch = request.targetIntent?.match(/with text ["']([^"']+)["']/i);
277
+ if (intentTextMatch && intentTextMatch[1] !== newText) {
278
+ const oldIntentText = intentTextMatch[1];
279
+ const intentWords = oldIntentText.split(/\s+/);
280
+ const newWords = newText.split(/\s+/);
281
+ const lenDiff = Math.abs(intentWords.length - newWords.length);
282
+ let diffCount = 0;
283
+ const minLen = Math.min(intentWords.length, newWords.length);
284
+ for (let i = 0; i < minLen; i++) {
285
+ if (intentWords[i] !== newWords[i])
286
+ diffCount++;
287
+ }
288
+ if (lenDiff <= 1 && diffCount <= 3 && diffCount > 0) {
289
+ console.log(`[LLMService] 🔍 contentChange inferred from intent: "${oldIntentText}" → "${newText}"`);
290
+ return { oldText: oldIntentText, newText };
291
+ }
292
+ }
293
+ return undefined;
294
+ }
295
+ async getFix(request) {
296
+ const dnaSummary = request.previousState;
297
+ // Reverse-engineer the original chain structure for the AI
298
+ const originalChainHint = inferOriginalChainHint(request.failedSelector);
299
+ const chainHintJson = JSON.stringify(originalChainHint, null, 2);
300
+ const userMessage = `
301
+ ### PROBLEM
302
+ A Playwright test failed. Analyze the full selector path and return a Smart Chain fix.
303
+
304
+ ### FAILED SELECTOR (Full Path):
305
+ "${request.failedSelector}"
306
+
307
+ ### ORIGINAL CHAIN STRUCTURE (reverse-engineered from failed selector):
308
+ ${chainHintJson}
309
+ Use this to understand the developer's original intent. Preserve the same structural
310
+ pattern in your "chainSegments" where possible (e.g., if original used a filter + getByRole
311
+ pattern, try to keep filter + getByRole if the element still exists in a list context).
312
+
313
+ ### INTENT:
314
+ ${request.targetIntent}
315
+
316
+ ### PREVIOUS DNA (Element Fingerprint):
317
+ ${dnaSummary && Object.keys(dnaSummary).length > 0
318
+ ? JSON.stringify(dnaSummary, null, 2)
319
+ : "NO DNA (First run — infer from selector and DOM)"}
320
+
321
+ ### CURRENT DOM SNIPPET:
322
+ ${request.currentDom}
323
+
324
+ ### YOUR TASK
325
+ 1. Find the element in the current DOM.
326
+ 2. Build a "chainSegments" array using Playwright's semantic API (priority: getByTestId > getByRole > getByLabel > getByText > filter+narrow > locator).
327
+ 3. Preserve the developer's chain structure (filter patterns, role patterns) when possible.
328
+ 4. Populate "contentChange" if the element's visible text changed.
329
+ 5. Also populate "segments" (legacy format) for backward compatibility.
330
+
331
+ Return ONLY a raw JSON object — no markdown, no preamble.`;
332
+ try {
333
+ const msg = await this.anthropic.messages.create({
334
+ model: "claude-sonnet-4-20250514",
335
+ max_tokens: 1024,
336
+ system: SYSTEM_PROMPT_TEXT +
337
+ "\nIMPORTANT: Respond ONLY with a raw JSON object.",
338
+ messages: [{ role: "user", content: userMessage }],
339
+ });
340
+ const rawText = msg.content[0].type === "text" ? msg.content[0].text : "";
341
+ console.log("[DEBUG] AI Raw Response:", rawText);
342
+ const jsonString = this.cleanJson(rawText);
343
+ if (!jsonString)
344
+ throw new Error("AI did not return a valid JSON object");
345
+ const parsed = JSON.parse(jsonString);
346
+ // ── Normalize chainSegments → segments (legacy) ───────────────────
347
+ if (parsed.chainSegments && Array.isArray(parsed.chainSegments)) {
348
+ // Reconstruct a flat new_selector from chain segments for legacy paths
349
+ const flatParts = [];
350
+ for (const seg of parsed.chainSegments) {
351
+ switch (seg.type) {
352
+ case "locator":
353
+ case "frameLocator":
354
+ flatParts.push(seg.selector);
355
+ break;
356
+ case "getByRole":
357
+ flatParts.push(seg.name
358
+ ? `[role="${seg.role}"]:has-text("${seg.name}")`
359
+ : `[role="${seg.role}"]`);
360
+ break;
361
+ case "getByLabel":
362
+ flatParts.push(`label:has-text("${seg.text}")`);
363
+ break;
364
+ case "getByText":
365
+ flatParts.push(`:has-text("${seg.text}")`);
366
+ break;
367
+ case "getByTestId":
368
+ flatParts.push(`[data-testid="${seg.testId}"]`);
369
+ break;
370
+ case "getByPlaceholder":
371
+ flatParts.push(`[placeholder="${seg.text}"]`);
372
+ break;
373
+ case "filter":
374
+ if (seg.hasText)
375
+ flatParts.push(`:has-text("${seg.hasText}")`);
376
+ break;
377
+ default:
378
+ break;
379
+ }
380
+ }
381
+ if (flatParts.length > 0 && !parsed.new_selector) {
382
+ parsed.new_selector = flatParts.join(" >> ");
383
+ }
384
+ // Also inject into legacy segments if missing
385
+ if (!parsed.segments || parsed.segments.length === 0) {
386
+ const frameSegs = parsed.chainSegments.filter((s) => s.type === "frameLocator");
387
+ const elemSegs = parsed.chainSegments.filter((s) => s.type !== "frameLocator");
388
+ parsed.segments = [
389
+ ...frameSegs.map((s) => ({
390
+ type: "frame",
391
+ selector: s.selector,
392
+ })),
393
+ { type: "element", selector: parsed.new_selector ?? "" },
394
+ ];
395
+ }
396
+ }
397
+ // ── Normalize legacy new_selector → segments ──────────────────────
398
+ if (!parsed.segments && parsed.new_selector) {
399
+ const parts = parsed.new_selector
400
+ .split(" >> ")
401
+ .map((s) => s.trim());
402
+ parsed.segments = parts.map((s, i) => ({
403
+ type: i === parts.length - 1 ? "element" : "frame",
404
+ selector: s,
405
+ }));
406
+ }
407
+ if (!parsed.segments || !Array.isArray(parsed.segments)) {
408
+ throw new Error("AI response missing 'segments' array");
409
+ }
410
+ parsed.new_selector =
411
+ parsed.new_selector ??
412
+ parsed.segments.map((s) => s.selector).join(" >> ");
413
+ // Attach original chain hint for ASTUpdater's partial healing logic
414
+ parsed.originalChainHint = originalChainHint;
415
+ // ── Ensure contentChange is populated ──────────────────────────────
416
+ if (!parsed.contentChange || typeof parsed.contentChange !== "object") {
417
+ const inferred = this.inferContentChange(parsed, request);
418
+ if (inferred) {
419
+ parsed.contentChange = inferred;
420
+ }
421
+ }
422
+ if (parsed.contentChange) {
423
+ console.log(`[LLMService] 📝 contentChange: "${parsed.contentChange.oldText}" → "${parsed.contentChange.newText}"`);
424
+ }
425
+ return parsed;
426
+ }
427
+ catch (error) {
428
+ console.error("[LLMService] Analysis failed:", error.message);
429
+ return {
430
+ status: "NOT_FOUND",
431
+ segments: [],
432
+ new_selector: null,
433
+ confidence: 0,
434
+ explanation: `AI Error: ${error.message}`,
435
+ };
436
+ }
437
+ }
438
+ }
439
+ exports.LLMService = LLMService;
@@ -0,0 +1,65 @@
1
+ import { ResolvedThresholds } from "../config/SelaConfig";
2
+ import { VisibilityInfo, AncestorNode, AnchorInfo } from "../types";
3
+ export type HealMode = "assertion" | "action";
4
+ export interface HealContext {
5
+ mode: HealMode;
6
+ action?: string;
7
+ /** Current git branch; used for occlusion policy (hard-fail on main). */
8
+ branch?: string;
9
+ }
10
+ export interface AIFixSummary {
11
+ confidence: number;
12
+ contentChange?: {
13
+ oldText: string;
14
+ newText: string;
15
+ };
16
+ explanation?: string;
17
+ /** Live innerText of the new selector fetched directly from the browser. */
18
+ liveText?: string;
19
+ /** Baseline text from the DNA snapshot — what the element showed before. */
20
+ dnaBaselineText?: string;
21
+ /** ARIA role from the DNA snapshot — used by the Intent Auditor. */
22
+ dnaRole?: string;
23
+ /** Visibility info captured from the candidate element (new selector). */
24
+ liveVisibility?: VisibilityInfo;
25
+ /** Ancestry chain captured from the candidate element at heal-time. */
26
+ liveAncestry?: AncestorNode[];
27
+ /** Label/row anchors captured from the candidate element. */
28
+ liveAnchors?: AnchorInfo;
29
+ /** Ancestry chain from the DNA snapshot. */
30
+ dnaAncestry?: AncestorNode[];
31
+ /** Label/row anchors from the DNA snapshot. */
32
+ dnaAnchors?: AnchorInfo;
33
+ }
34
+ export type RiskLevel = "SAFE" | "REQUIRES_REVIEW" | "FAIL_HARD" | "BLOCKED";
35
+ export interface SafetyDecision {
36
+ level: RiskLevel;
37
+ reason: string;
38
+ canProceed: boolean;
39
+ /** Structured info for policy-aware log messages in FixwrightEngine. */
40
+ meta?: {
41
+ auditorVerdict?: "CONSISTENT" | "SUSPICIOUS" | "INCONSISTENT";
42
+ auditorConfidence?: number;
43
+ /** Score breakdown forwarded from IntentAuditor for engine-level summary logging. */
44
+ auditScoreBreakdown?: {
45
+ rawConfidence: number;
46
+ penalty: number;
47
+ bonus: number;
48
+ adjustedConfidence: number;
49
+ };
50
+ };
51
+ }
52
+ type FunctionalRole = "SUBMIT" | "CANCEL" | "DELETE" | "NAVIGATE" | "AUTH" | "STATUS_POSITIVE" | "STATUS_NEGATIVE" | "UNKNOWN";
53
+ export declare class SafetyGuard {
54
+ private intentAuditor;
55
+ private thresholds;
56
+ constructor(thresholds?: ResolvedThresholds);
57
+ evaluate(context: HealContext, aiFix: AIFixSummary): Promise<SafetyDecision>;
58
+ private isSemanticOpposite;
59
+ classifyFunctionalRole(text: string): FunctionalRole;
60
+ computeSemanticSimilarity(a: string, b: string): number;
61
+ private levenshteinDistance;
62
+ private jaccardSimilarity;
63
+ }
64
+ export {};
65
+ //# sourceMappingURL=SafetyGuard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SafetyGuard.d.ts","sourceRoot":"","sources":["../../src/services/SafetyGuard.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAMpE,MAAM,MAAM,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,uEAAuE;IACvE,YAAY,CAAC,EAAE,YAAY,EAAE,CAAC;IAC9B,6DAA6D;IAC7D,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,YAAY,EAAE,CAAC;IAC7B,+CAA+C;IAC/C,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,iBAAiB,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;IACpB,wEAAwE;IACxE,IAAI,CAAC,EAAE;QACL,cAAc,CAAC,EAAE,YAAY,GAAG,YAAY,GAAG,cAAc,CAAC;QAC9D,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,qFAAqF;QACrF,mBAAmB,CAAC,EAAE;YACpB,aAAa,EAAE,MAAM,CAAC;YACtB,OAAO,EAAE,MAAM,CAAC;YAChB,KAAK,EAAE,MAAM,CAAC;YACd,kBAAkB,EAAE,MAAM,CAAC;SAC5B,CAAC;KACH,CAAC;CACH;AA6DD,KAAK,cAAc,GACf,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,MAAM,GACN,iBAAiB,GACjB,iBAAiB,GACjB,SAAS,CAAC;AA2Id,qBAAa,WAAW;IACtB,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,UAAU,CAAqB;gBAE3B,UAAU,CAAC,EAAE,kBAAkB;IASrC,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC;IAkTlF,OAAO,CAAC,kBAAkB;IAY1B,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc;IAsBpD,yBAAyB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM;IAiBvD,OAAO,CAAC,mBAAmB;IAyB3B,OAAO,CAAC,iBAAiB;CAY1B"}