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.
- package/README.md +93 -0
- package/bin/sela.js +3 -0
- package/dist/cli/ErrorHandler.d.ts +10 -0
- package/dist/cli/ErrorHandler.d.ts.map +1 -0
- package/dist/cli/ErrorHandler.js +70 -0
- package/dist/cli/commands/bulk.d.ts +3 -0
- package/dist/cli/commands/bulk.d.ts.map +1 -0
- package/dist/cli/commands/bulk.js +140 -0
- package/dist/cli/commands/find.d.ts +3 -0
- package/dist/cli/commands/find.d.ts.map +1 -0
- package/dist/cli/commands/find.js +51 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +133 -0
- package/dist/cli/commands/list.d.ts +3 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +56 -0
- package/dist/cli/commands/refactor.d.ts +3 -0
- package/dist/cli/commands/refactor.d.ts.map +1 -0
- package/dist/cli/commands/refactor.js +30 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +51 -0
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +123 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +42 -0
- package/dist/cli/ui/DnaTable.d.ts +3 -0
- package/dist/cli/ui/DnaTable.d.ts.map +1 -0
- package/dist/cli/ui/DnaTable.js +70 -0
- package/dist/cli/ui/LocatorPicker.d.ts +8 -0
- package/dist/cli/ui/LocatorPicker.d.ts.map +1 -0
- package/dist/cli/ui/LocatorPicker.js +33 -0
- package/dist/cli/ui/ProgressReporter.d.ts +11 -0
- package/dist/cli/ui/ProgressReporter.d.ts.map +1 -0
- package/dist/cli/ui/ProgressReporter.js +33 -0
- package/dist/cli/ui/RefactorWizard.d.ts +26 -0
- package/dist/cli/ui/RefactorWizard.d.ts.map +1 -0
- package/dist/cli/ui/RefactorWizard.js +269 -0
- package/dist/config/ConfigLoader.d.ts +15 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +174 -0
- package/dist/config/SelaConfig.d.ts +67 -0
- package/dist/config/SelaConfig.d.ts.map +1 -0
- package/dist/config/SelaConfig.js +57 -0
- package/dist/config/defineConfig.d.ts +3 -0
- package/dist/config/defineConfig.d.ts.map +1 -0
- package/dist/config/defineConfig.js +9 -0
- package/dist/engine/FixwrightEngine.d.ts +24 -0
- package/dist/engine/FixwrightEngine.d.ts.map +1 -0
- package/dist/engine/FixwrightEngine.js +403 -0
- package/dist/engine/HealingRegistry.d.ts +40 -0
- package/dist/engine/HealingRegistry.d.ts.map +1 -0
- package/dist/engine/HealingRegistry.js +98 -0
- package/dist/engine/singleton.d.ts +3 -0
- package/dist/engine/singleton.d.ts.map +1 -0
- package/dist/engine/singleton.js +5 -0
- package/dist/fixtures/expectProxy.d.ts +12 -0
- package/dist/fixtures/expectProxy.d.ts.map +1 -0
- package/dist/fixtures/expectProxy.js +228 -0
- package/dist/fixtures/index.d.ts +6 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/index.js +688 -0
- package/dist/fixtures/moduleExpect.d.ts +2 -0
- package/dist/fixtures/moduleExpect.d.ts.map +1 -0
- package/dist/fixtures/moduleExpect.js +46 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/services/ASTSourceUpdater.d.ts +79 -0
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -0
- package/dist/services/ASTSourceUpdater.js +3177 -0
- package/dist/services/ArgumentTypeAnalyzer.d.ts +26 -0
- package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -0
- package/dist/services/ArgumentTypeAnalyzer.js +92 -0
- package/dist/services/BlastRadiusAnalyzer.d.ts +15 -0
- package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -0
- package/dist/services/BlastRadiusAnalyzer.js +103 -0
- package/dist/services/ChainValidator.d.ts +76 -0
- package/dist/services/ChainValidator.d.ts.map +1 -0
- package/dist/services/ChainValidator.js +569 -0
- package/dist/services/CrossFileHealer.d.ts +6 -0
- package/dist/services/CrossFileHealer.d.ts.map +1 -0
- package/dist/services/CrossFileHealer.js +134 -0
- package/dist/services/DefinitionTracer.d.ts +41 -0
- package/dist/services/DefinitionTracer.d.ts.map +1 -0
- package/dist/services/DefinitionTracer.js +350 -0
- package/dist/services/DnaEditorService.d.ts +31 -0
- package/dist/services/DnaEditorService.d.ts.map +1 -0
- package/dist/services/DnaEditorService.js +198 -0
- package/dist/services/DnaIndexService.d.ts +24 -0
- package/dist/services/DnaIndexService.d.ts.map +1 -0
- package/dist/services/DnaIndexService.js +131 -0
- package/dist/services/HealingAdvisory.d.ts +22 -0
- package/dist/services/HealingAdvisory.d.ts.map +1 -0
- package/dist/services/HealingAdvisory.js +42 -0
- package/dist/services/HealthReportService.d.ts +10 -0
- package/dist/services/HealthReportService.d.ts.map +1 -0
- package/dist/services/HealthReportService.js +84 -0
- package/dist/services/InitializerUpdater.d.ts +16 -0
- package/dist/services/InitializerUpdater.d.ts.map +1 -0
- package/dist/services/InitializerUpdater.js +37 -0
- package/dist/services/IntentAuditor.d.ts +39 -0
- package/dist/services/IntentAuditor.d.ts.map +1 -0
- package/dist/services/IntentAuditor.js +302 -0
- package/dist/services/LLMService.d.ts +100 -0
- package/dist/services/LLMService.d.ts.map +1 -0
- package/dist/services/LLMService.js +439 -0
- package/dist/services/SafetyGuard.d.ts +65 -0
- package/dist/services/SafetyGuard.d.ts.map +1 -0
- package/dist/services/SafetyGuard.js +524 -0
- package/dist/services/SnapshotService.d.ts +11 -0
- package/dist/services/SnapshotService.d.ts.map +1 -0
- package/dist/services/SnapshotService.js +349 -0
- package/dist/services/SourceLinkService.d.ts +26 -0
- package/dist/services/SourceLinkService.d.ts.map +1 -0
- package/dist/services/SourceLinkService.js +156 -0
- package/dist/services/SourceUpdater.d.ts +45 -0
- package/dist/services/SourceUpdater.d.ts.map +1 -0
- package/dist/services/SourceUpdater.js +1021 -0
- package/dist/services/TemplateDiffService.d.ts +25 -0
- package/dist/services/TemplateDiffService.d.ts.map +1 -0
- package/dist/services/TemplateDiffService.js +331 -0
- package/dist/services/types.d.ts +59 -0
- package/dist/services/types.d.ts.map +1 -0
- package/dist/services/types.js +2 -0
- package/dist/storage/SnapshotManager.d.ts +5 -0
- package/dist/storage/SnapshotManager.d.ts.map +1 -0
- package/dist/storage/SnapshotManager.js +31 -0
- package/dist/types/index.d.ts +95 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/utils/DOMUtils.d.ts +33 -0
- package/dist/utils/DOMUtils.d.ts.map +1 -0
- package/dist/utils/DOMUtils.js +179 -0
- package/dist/utils/StackUtils.d.ts +11 -0
- package/dist/utils/StackUtils.d.ts.map +1 -0
- package/dist/utils/StackUtils.js +120 -0
- package/dist/vendor/enquirer.d.ts +33 -0
- package/dist/vendor/enquirer.d.ts.map +1 -0
- package/dist/vendor/enquirer.js +11 -0
- 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"}
|