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,569 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/services/ChainValidator.ts
|
|
3
|
+
//
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
5
|
+
// VALIDATION LAYER for AI-generated chain segments
|
|
6
|
+
//
|
|
7
|
+
// Runs 5 sequential guards before any AST mutation occurs:
|
|
8
|
+
//
|
|
9
|
+
// [1] SchemaValidator — required fields, no unknown segment types
|
|
10
|
+
// [2] TypeFlowValidator — tracks Locator|FrameLocator state
|
|
11
|
+
// [3] ReceiverValidator — root must match original chain's receiver
|
|
12
|
+
// [4] PlacementValidator — filter() / nth() / first() only after Locator
|
|
13
|
+
// [5] SemanticUpgrader — upgrades flat CSS to semantic where possible
|
|
14
|
+
//
|
|
15
|
+
// Each guard returns a ValidationResult. The pipeline short-circuits on
|
|
16
|
+
// the first REJECT, falling back to the original flat-CSS strategy.
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.ChainValidator = void 0;
|
|
20
|
+
exports.segmentsToCode = segmentsToCode;
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────
|
|
22
|
+
// CONSTANTS
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────
|
|
24
|
+
const KNOWN_SEGMENT_TYPES = new Set([
|
|
25
|
+
"locator",
|
|
26
|
+
"frameLocator",
|
|
27
|
+
"getByRole",
|
|
28
|
+
"getByLabel",
|
|
29
|
+
"getByText",
|
|
30
|
+
"getByPlaceholder",
|
|
31
|
+
"getByTestId",
|
|
32
|
+
"getByAltText",
|
|
33
|
+
"getByTitle",
|
|
34
|
+
"filter",
|
|
35
|
+
"first",
|
|
36
|
+
"last",
|
|
37
|
+
"nth",
|
|
38
|
+
]);
|
|
39
|
+
// Methods that produce a Locator (not FrameLocator)
|
|
40
|
+
const LOCATOR_PRODUCERS = new Set([
|
|
41
|
+
"locator",
|
|
42
|
+
"getByRole",
|
|
43
|
+
"getByLabel",
|
|
44
|
+
"getByText",
|
|
45
|
+
"getByPlaceholder",
|
|
46
|
+
"getByTestId",
|
|
47
|
+
"getByAltText",
|
|
48
|
+
"getByTitle",
|
|
49
|
+
"filter",
|
|
50
|
+
"first",
|
|
51
|
+
"last",
|
|
52
|
+
"nth",
|
|
53
|
+
]);
|
|
54
|
+
// Methods valid only when current return type is Locator
|
|
55
|
+
const REQUIRES_LOCATOR = new Set(["filter", "first", "last", "nth"]);
|
|
56
|
+
// Methods that can chain on both Locator and FrameLocator (or start fresh)
|
|
57
|
+
const WORKS_ON_FRAME_LOCATOR = new Set([
|
|
58
|
+
"locator",
|
|
59
|
+
"getByRole",
|
|
60
|
+
"getByLabel",
|
|
61
|
+
"getByText",
|
|
62
|
+
"getByPlaceholder",
|
|
63
|
+
"getByTestId",
|
|
64
|
+
"getByAltText",
|
|
65
|
+
"getByTitle",
|
|
66
|
+
"frameLocator",
|
|
67
|
+
]);
|
|
68
|
+
const VALID_ARIA_ROLES = new Set([
|
|
69
|
+
// Interactive
|
|
70
|
+
"button",
|
|
71
|
+
"textbox",
|
|
72
|
+
"searchbox",
|
|
73
|
+
"checkbox",
|
|
74
|
+
"radio",
|
|
75
|
+
"switch",
|
|
76
|
+
"combobox",
|
|
77
|
+
"listbox",
|
|
78
|
+
"option",
|
|
79
|
+
"menuitem",
|
|
80
|
+
"menuitemcheckbox",
|
|
81
|
+
"menuitemradio",
|
|
82
|
+
"link",
|
|
83
|
+
"tab",
|
|
84
|
+
"tabpanel",
|
|
85
|
+
"slider",
|
|
86
|
+
"spinbutton",
|
|
87
|
+
"progressbar",
|
|
88
|
+
"scrollbar",
|
|
89
|
+
"grid",
|
|
90
|
+
"gridcell",
|
|
91
|
+
"row",
|
|
92
|
+
"rowheader",
|
|
93
|
+
"columnheader",
|
|
94
|
+
"cell",
|
|
95
|
+
"treeitem",
|
|
96
|
+
"tree",
|
|
97
|
+
"treegrid",
|
|
98
|
+
"dialog",
|
|
99
|
+
"alertdialog",
|
|
100
|
+
// Structural
|
|
101
|
+
"heading",
|
|
102
|
+
"banner",
|
|
103
|
+
"main",
|
|
104
|
+
"navigation",
|
|
105
|
+
"nav",
|
|
106
|
+
"complementary",
|
|
107
|
+
"aside",
|
|
108
|
+
"contentinfo",
|
|
109
|
+
"footer",
|
|
110
|
+
"region",
|
|
111
|
+
"landmark",
|
|
112
|
+
"article",
|
|
113
|
+
"section",
|
|
114
|
+
"list",
|
|
115
|
+
"listitem",
|
|
116
|
+
"term",
|
|
117
|
+
"definition",
|
|
118
|
+
"figure",
|
|
119
|
+
"img",
|
|
120
|
+
"presentation",
|
|
121
|
+
"none",
|
|
122
|
+
"separator",
|
|
123
|
+
"log",
|
|
124
|
+
"marquee",
|
|
125
|
+
"status",
|
|
126
|
+
"timer",
|
|
127
|
+
"alert",
|
|
128
|
+
"form",
|
|
129
|
+
"group",
|
|
130
|
+
"toolbar",
|
|
131
|
+
"menubar",
|
|
132
|
+
"menu",
|
|
133
|
+
"tooltip",
|
|
134
|
+
"math",
|
|
135
|
+
"note",
|
|
136
|
+
"application",
|
|
137
|
+
"document",
|
|
138
|
+
"feed",
|
|
139
|
+
"generic",
|
|
140
|
+
]);
|
|
141
|
+
// ─────────────────────────────────────────────────────────────────
|
|
142
|
+
// GUARD 1 — Schema Validator
|
|
143
|
+
// ─────────────────────────────────────────────────────────────────
|
|
144
|
+
function schemaValidate(segments) {
|
|
145
|
+
if (!Array.isArray(segments) || segments.length === 0) {
|
|
146
|
+
return { valid: false, reason: "segments must be a non-empty array" };
|
|
147
|
+
}
|
|
148
|
+
for (let i = 0; i < segments.length; i++) {
|
|
149
|
+
const seg = segments[i];
|
|
150
|
+
if (typeof seg !== "object" || seg === null) {
|
|
151
|
+
return { valid: false, reason: `segment[${i}] is not an object` };
|
|
152
|
+
}
|
|
153
|
+
if (!KNOWN_SEGMENT_TYPES.has(seg.type)) {
|
|
154
|
+
return {
|
|
155
|
+
valid: false,
|
|
156
|
+
reason: `segment[${i}] has unknown type "${seg.type}"`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// Per-type required fields
|
|
160
|
+
switch (seg.type) {
|
|
161
|
+
case "locator":
|
|
162
|
+
case "frameLocator":
|
|
163
|
+
if (typeof seg.selector !== "string" || seg.selector.trim() === "") {
|
|
164
|
+
return {
|
|
165
|
+
valid: false,
|
|
166
|
+
reason: `segment[${i}] type="${seg.type}" requires non-empty selector`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
case "getByRole":
|
|
171
|
+
if (typeof seg.role !== "string" || seg.role.trim() === "") {
|
|
172
|
+
return {
|
|
173
|
+
valid: false,
|
|
174
|
+
reason: `segment[${i}] getByRole requires non-empty role`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (!VALID_ARIA_ROLES.has(seg.role.toLowerCase())) {
|
|
178
|
+
return {
|
|
179
|
+
valid: false,
|
|
180
|
+
reason: `segment[${i}] getByRole has invalid ARIA role "${seg.role}"`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
case "getByLabel":
|
|
185
|
+
case "getByText":
|
|
186
|
+
case "getByPlaceholder":
|
|
187
|
+
case "getByAltText":
|
|
188
|
+
case "getByTitle":
|
|
189
|
+
if (typeof seg.text !== "string" || seg.text.trim() === "") {
|
|
190
|
+
return {
|
|
191
|
+
valid: false,
|
|
192
|
+
reason: `segment[${i}] type="${seg.type}" requires non-empty text`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
case "getByTestId":
|
|
197
|
+
if (typeof seg.testId !== "string" || seg.testId.trim() === "") {
|
|
198
|
+
return {
|
|
199
|
+
valid: false,
|
|
200
|
+
reason: `segment[${i}] getByTestId requires non-empty testId`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
case "nth":
|
|
205
|
+
if (typeof seg.index !== "number" || !Number.isInteger(seg.index)) {
|
|
206
|
+
return {
|
|
207
|
+
valid: false,
|
|
208
|
+
reason: `segment[${i}] nth requires integer index`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
case "filter":
|
|
213
|
+
if (seg.hasText === undefined &&
|
|
214
|
+
seg.hasNotText === undefined &&
|
|
215
|
+
seg.has === undefined &&
|
|
216
|
+
seg.hasNot === undefined) {
|
|
217
|
+
return {
|
|
218
|
+
valid: false,
|
|
219
|
+
reason: `segment[${i}] filter requires at least one of: hasText, hasNotText, has, hasNot`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
// first / last have no required fields beyond type
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { valid: true };
|
|
227
|
+
}
|
|
228
|
+
// ─────────────────────────────────────────────────────────────────
|
|
229
|
+
// GUARD 2 — Type Flow Validator
|
|
230
|
+
//
|
|
231
|
+
// Simulates the chain execution, tracking whether the current
|
|
232
|
+
// expression is a Locator, FrameLocator, or Page/unknown.
|
|
233
|
+
// ─────────────────────────────────────────────────────────────────
|
|
234
|
+
function typeFlowValidate(segments, rootType) {
|
|
235
|
+
let currentType = rootType;
|
|
236
|
+
for (let i = 0; i < segments.length; i++) {
|
|
237
|
+
const seg = segments[i];
|
|
238
|
+
if (seg.type === "frameLocator") {
|
|
239
|
+
// frameLocator() is valid on Page or FrameLocator, not Locator
|
|
240
|
+
if (currentType === "Locator") {
|
|
241
|
+
return {
|
|
242
|
+
valid: false,
|
|
243
|
+
reason: `segment[${i}] frameLocator() cannot be called on a Locator (type flow: ${currentType} → invalid)`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
currentType = "FrameLocator";
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (REQUIRES_LOCATOR.has(seg.type)) {
|
|
250
|
+
if (currentType !== "Locator") {
|
|
251
|
+
return {
|
|
252
|
+
valid: false,
|
|
253
|
+
reason: `segment[${i}] .${seg.type}() requires a Locator receiver but current type is "${currentType}"`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
currentType = "Locator";
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (LOCATOR_PRODUCERS.has(seg.type)) {
|
|
260
|
+
// These methods are valid on Page, FrameLocator, or Locator
|
|
261
|
+
currentType = "Locator";
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Final chain must end in a Locator (not FrameLocator) for assertions to work
|
|
266
|
+
if (currentType === "FrameLocator") {
|
|
267
|
+
return {
|
|
268
|
+
valid: false,
|
|
269
|
+
reason: "chain ends on FrameLocator — assertions require a Locator at the end",
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return { valid: true };
|
|
273
|
+
}
|
|
274
|
+
// ─────────────────────────────────────────────────────────────────
|
|
275
|
+
// GUARD 3 — Receiver Validator
|
|
276
|
+
//
|
|
277
|
+
// The first segment's effective receiver must be compatible with
|
|
278
|
+
// the original code's root receiver. Prevents `page.getByRole()`
|
|
279
|
+
// from replacing `userRow.getByRole()`.
|
|
280
|
+
// ─────────────────────────────────────────────────────────────────
|
|
281
|
+
function receiverValidate(segments, originalRootReceiver, proposedRootReceiver) {
|
|
282
|
+
const orig = originalRootReceiver.trim();
|
|
283
|
+
const proposed = proposedRootReceiver.trim();
|
|
284
|
+
// If both are "page" or both are the same variable, we're fine
|
|
285
|
+
if (orig === proposed)
|
|
286
|
+
return { valid: true };
|
|
287
|
+
// "page" is a global receiver — the AI sometimes proposes it even when
|
|
288
|
+
// the original chain was scoped. This is only acceptable if the original
|
|
289
|
+
// chain itself started from "page".
|
|
290
|
+
const PAGE_RECEIVERS = new Set(["page", "context", "browser"]);
|
|
291
|
+
if (PAGE_RECEIVERS.has(proposed) && !PAGE_RECEIVERS.has(orig)) {
|
|
292
|
+
return {
|
|
293
|
+
valid: false,
|
|
294
|
+
reason: `receiver mismatch: original chain rooted at "${orig}" but proposed chain starts from "${proposed}" — this would change element scope`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// If the original root is a local variable and proposed is different,
|
|
298
|
+
// accept only if proposed is also a valid-looking identifier (not page).
|
|
299
|
+
// We allow this since sometimes variables are renamed but semantically
|
|
300
|
+
// equivalent.
|
|
301
|
+
if (!PAGE_RECEIVERS.has(orig) && !PAGE_RECEIVERS.has(proposed)) {
|
|
302
|
+
// Both are custom variables — trust the AI's context
|
|
303
|
+
console.warn(`[ChainValidator] ⚠️ Receiver mismatch: "${orig}" vs "${proposed}" — proceeding cautiously`);
|
|
304
|
+
return { valid: true };
|
|
305
|
+
}
|
|
306
|
+
return { valid: true };
|
|
307
|
+
}
|
|
308
|
+
// ─────────────────────────────────────────────────────────────────
|
|
309
|
+
// GUARD 4 — Placement Validator
|
|
310
|
+
//
|
|
311
|
+
// Validates placement-sensitive rules:
|
|
312
|
+
// - filter/first/last/nth must follow a Locator-producing segment
|
|
313
|
+
// - frameLocator cannot follow a Locator
|
|
314
|
+
// - No duplicate consecutive locator calls without a filter between them
|
|
315
|
+
// when that would be ambiguous (e.g. locator(".a").locator(".a"))
|
|
316
|
+
// ─────────────────────────────────────────────────────────────────
|
|
317
|
+
function placementValidate(segments) {
|
|
318
|
+
for (let i = 0; i < segments.length; i++) {
|
|
319
|
+
const seg = segments[i];
|
|
320
|
+
const prev = i > 0 ? segments[i - 1] : null;
|
|
321
|
+
if (REQUIRES_LOCATOR.has(seg.type) && prev !== null) {
|
|
322
|
+
if (prev.type === "frameLocator") {
|
|
323
|
+
return {
|
|
324
|
+
valid: false,
|
|
325
|
+
reason: `segment[${i}] .${seg.type}() placed immediately after frameLocator() — invalid`,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// filter() must have a non-empty content
|
|
330
|
+
if (seg.type === "filter") {
|
|
331
|
+
const hasAnyPredicate = (seg.hasText && seg.hasText.trim() !== "") ||
|
|
332
|
+
(seg.hasNotText && seg.hasNotText.trim() !== "") ||
|
|
333
|
+
(seg.has && seg.has.trim() !== "") ||
|
|
334
|
+
(seg.hasNot && seg.hasNot.trim() !== "");
|
|
335
|
+
if (!hasAnyPredicate) {
|
|
336
|
+
return {
|
|
337
|
+
valid: false,
|
|
338
|
+
reason: `segment[${i}] filter() has all-empty predicates — would be a no-op`,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return { valid: true };
|
|
344
|
+
}
|
|
345
|
+
// ─────────────────────────────────────────────────────────────────
|
|
346
|
+
// GUARD 5 — Semantic Upgrader
|
|
347
|
+
//
|
|
348
|
+
// Opportunistically upgrades flat CSS locator segments to semantic
|
|
349
|
+
// equivalents following Playwright's priority hierarchy:
|
|
350
|
+
// 1. getByTestId (data-testid)
|
|
351
|
+
// 2. getByRole (aria role inference from CSS)
|
|
352
|
+
// 3. getByLabel (form elements)
|
|
353
|
+
// 4. locator (CSS — unchanged)
|
|
354
|
+
//
|
|
355
|
+
// This guard NEVER rejects — it only upgrades and returns the
|
|
356
|
+
// possibly-improved segments.
|
|
357
|
+
// ─────────────────────────────────────────────────────────────────
|
|
358
|
+
const ROLE_BY_TAG = {
|
|
359
|
+
button: "button",
|
|
360
|
+
a: "link",
|
|
361
|
+
input: "textbox", // overridden for checkboxes etc. below
|
|
362
|
+
textarea: "textbox",
|
|
363
|
+
select: "combobox",
|
|
364
|
+
h1: "heading",
|
|
365
|
+
h2: "heading",
|
|
366
|
+
h3: "heading",
|
|
367
|
+
h4: "heading",
|
|
368
|
+
h5: "heading",
|
|
369
|
+
h6: "heading",
|
|
370
|
+
nav: "navigation",
|
|
371
|
+
main: "main",
|
|
372
|
+
header: "banner",
|
|
373
|
+
footer: "contentinfo",
|
|
374
|
+
aside: "complementary",
|
|
375
|
+
};
|
|
376
|
+
function tryUpgradeToSemantic(seg) {
|
|
377
|
+
if (seg.type !== "locator")
|
|
378
|
+
return seg;
|
|
379
|
+
const css = seg.selector.trim();
|
|
380
|
+
// Priority 1: data-testid
|
|
381
|
+
const testIdMatch = css.match(/\[data-testid=["']([^"']+)["']\]/);
|
|
382
|
+
if (testIdMatch) {
|
|
383
|
+
return { type: "getByTestId", testId: testIdMatch[1] };
|
|
384
|
+
}
|
|
385
|
+
// Priority 2: aria-label
|
|
386
|
+
const ariaLabelMatch = css.match(/\[aria-label=["']([^"']+)["']\]/);
|
|
387
|
+
if (ariaLabelMatch) {
|
|
388
|
+
return { type: "getByLabel", text: ariaLabelMatch[1] };
|
|
389
|
+
}
|
|
390
|
+
// Priority 3: role inference from pure tag selectors
|
|
391
|
+
const tagOnlyMatch = css.match(/^([a-zA-Z][a-zA-Z0-9]*)$/);
|
|
392
|
+
if (tagOnlyMatch) {
|
|
393
|
+
const tag = tagOnlyMatch[1].toLowerCase();
|
|
394
|
+
const role = ROLE_BY_TAG[tag];
|
|
395
|
+
if (role) {
|
|
396
|
+
return { type: "getByRole", role };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Priority 3b: button[type="submit"] or button.classname — still a button role
|
|
400
|
+
const buttonMatch = css.match(/^button(?:[.[#][^\s]*)?$/);
|
|
401
|
+
if (buttonMatch) {
|
|
402
|
+
return { type: "getByRole", role: "button" };
|
|
403
|
+
}
|
|
404
|
+
// Priority 4: has-text() — convert to getByText
|
|
405
|
+
const hasTextMatch = css.match(/^.*:has-text\(["']([^"']+)["']\)$/);
|
|
406
|
+
if (hasTextMatch) {
|
|
407
|
+
return { type: "getByText", text: hasTextMatch[1] };
|
|
408
|
+
}
|
|
409
|
+
// Otherwise leave as CSS locator
|
|
410
|
+
return seg;
|
|
411
|
+
}
|
|
412
|
+
function semanticUpgrade(segments) {
|
|
413
|
+
return segments.map(tryUpgradeToSemantic);
|
|
414
|
+
}
|
|
415
|
+
// ─────────────────────────────────────────────────────────────────
|
|
416
|
+
// CODE GENERATOR
|
|
417
|
+
//
|
|
418
|
+
// Converts validated ChainSegment[] into clean TypeScript source code.
|
|
419
|
+
// Respects developer style: multi-segment chains get line-broken only
|
|
420
|
+
// when depth >= 3 (configurable).
|
|
421
|
+
// ─────────────────────────────────────────────────────────────────
|
|
422
|
+
function segmentsToCode(receiver, segments, multiLine = false) {
|
|
423
|
+
const sep = multiLine ? "\n " : "";
|
|
424
|
+
let code = receiver;
|
|
425
|
+
for (const seg of segments) {
|
|
426
|
+
const prefix = multiLine ? `\n ` : "";
|
|
427
|
+
switch (seg.type) {
|
|
428
|
+
case "locator":
|
|
429
|
+
code += `${prefix}.locator(${q(seg.selector)})`;
|
|
430
|
+
break;
|
|
431
|
+
case "frameLocator":
|
|
432
|
+
code += `${prefix}.frameLocator(${q(seg.selector)})`;
|
|
433
|
+
break;
|
|
434
|
+
case "getByRole": {
|
|
435
|
+
const opts = [];
|
|
436
|
+
if (seg.name !== undefined)
|
|
437
|
+
opts.push(`name: ${q(seg.name)}`);
|
|
438
|
+
if (seg.exact !== undefined)
|
|
439
|
+
opts.push(`exact: ${seg.exact}`);
|
|
440
|
+
const optStr = opts.length > 0 ? `, { ${opts.join(", ")} }` : "";
|
|
441
|
+
code += `${prefix}.getByRole(${q(seg.role)}${optStr})`;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
case "getByLabel": {
|
|
445
|
+
const opts = seg.exact !== undefined ? `, { exact: ${seg.exact} }` : "";
|
|
446
|
+
code += `${prefix}.getByLabel(${q(seg.text)}${opts})`;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
case "getByText": {
|
|
450
|
+
const opts = seg.exact !== undefined ? `, { exact: ${seg.exact} }` : "";
|
|
451
|
+
code += `${prefix}.getByText(${q(seg.text)}${opts})`;
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
case "getByPlaceholder":
|
|
455
|
+
code += `${prefix}.getByPlaceholder(${q(seg.text)})`;
|
|
456
|
+
break;
|
|
457
|
+
case "getByTestId":
|
|
458
|
+
code += `${prefix}.getByTestId(${q(seg.testId)})`;
|
|
459
|
+
break;
|
|
460
|
+
case "getByAltText":
|
|
461
|
+
code += `${prefix}.getByAltText(${q(seg.text)})`;
|
|
462
|
+
break;
|
|
463
|
+
case "getByTitle":
|
|
464
|
+
code += `${prefix}.getByTitle(${q(seg.text)})`;
|
|
465
|
+
break;
|
|
466
|
+
case "filter": {
|
|
467
|
+
const parts = [];
|
|
468
|
+
if (seg.hasText)
|
|
469
|
+
parts.push(`hasText: ${q(seg.hasText)}`);
|
|
470
|
+
if (seg.hasNotText)
|
|
471
|
+
parts.push(`hasNotText: ${q(seg.hasNotText)}`);
|
|
472
|
+
if (seg.has)
|
|
473
|
+
parts.push(`has: ${seg.has}`); // has is a Locator expr
|
|
474
|
+
if (seg.hasNot)
|
|
475
|
+
parts.push(`hasNot: ${seg.hasNot}`);
|
|
476
|
+
code += `${prefix}.filter({ ${parts.join(", ")} })`;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
case "first":
|
|
480
|
+
code += `${prefix}.first()`;
|
|
481
|
+
break;
|
|
482
|
+
case "last":
|
|
483
|
+
code += `${prefix}.last()`;
|
|
484
|
+
break;
|
|
485
|
+
case "nth":
|
|
486
|
+
code += `${prefix}.nth(${seg.index})`;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return code;
|
|
491
|
+
}
|
|
492
|
+
// Helper: quote a string safely, escaping internal double quotes
|
|
493
|
+
function q(s) {
|
|
494
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
495
|
+
}
|
|
496
|
+
class ChainValidator {
|
|
497
|
+
validate(input) {
|
|
498
|
+
const { segments: rawSegments, originalRootReceiver, proposedRootReceiver, rootType = "unknown", applySemanticUpgrade = true, } = input;
|
|
499
|
+
// Guard 1: Schema
|
|
500
|
+
const schemaResult = schemaValidate(rawSegments);
|
|
501
|
+
if (!schemaResult.valid) {
|
|
502
|
+
console.warn(`[ChainValidator] ❌ Schema: ${schemaResult.reason}`);
|
|
503
|
+
return schemaResult;
|
|
504
|
+
}
|
|
505
|
+
let segments = rawSegments;
|
|
506
|
+
// Guard 2: Type flow
|
|
507
|
+
const effectiveRootType = rootType === "unknown" ? "Locator" : rootType;
|
|
508
|
+
// For Page-level (page.xxx), the root is not yet a Locator/FrameLocator
|
|
509
|
+
// — we treat the first segment as the producer.
|
|
510
|
+
const isPageReceiver = originalRootReceiver === "page" ||
|
|
511
|
+
originalRootReceiver === "context" ||
|
|
512
|
+
originalRootReceiver === "browser";
|
|
513
|
+
const typeFlowResult = typeFlowValidate(segments, isPageReceiver ? "unknown" : effectiveRootType);
|
|
514
|
+
if (!typeFlowResult.valid) {
|
|
515
|
+
console.warn(`[ChainValidator] ❌ TypeFlow: ${typeFlowResult.reason}`);
|
|
516
|
+
return typeFlowResult;
|
|
517
|
+
}
|
|
518
|
+
// Guard 3: Receiver compatibility
|
|
519
|
+
const receiverResult = receiverValidate(segments, originalRootReceiver, proposedRootReceiver);
|
|
520
|
+
if (!receiverResult.valid) {
|
|
521
|
+
console.warn(`[ChainValidator] ❌ Receiver: ${receiverResult.reason}`);
|
|
522
|
+
return receiverResult;
|
|
523
|
+
}
|
|
524
|
+
// Guard 4: Placement
|
|
525
|
+
const placementResult = placementValidate(segments);
|
|
526
|
+
if (!placementResult.valid) {
|
|
527
|
+
console.warn(`[ChainValidator] ❌ Placement: ${placementResult.reason}`);
|
|
528
|
+
return placementResult;
|
|
529
|
+
}
|
|
530
|
+
// Guard 5: Semantic upgrade (non-rejecting)
|
|
531
|
+
if (applySemanticUpgrade) {
|
|
532
|
+
segments = semanticUpgrade(segments);
|
|
533
|
+
}
|
|
534
|
+
// Generate code
|
|
535
|
+
const multiLine = segments.length >= 3;
|
|
536
|
+
const code = segmentsToCode(proposedRootReceiver, segments, multiLine);
|
|
537
|
+
console.log(`[ChainValidator] ✅ Valid chain (${segments.length} segments): ${code}`);
|
|
538
|
+
return { valid: true, segments, code, multiLine };
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Lightweight check: does this segment array describe the same
|
|
542
|
+
* semantic intent as the old flat selector string?
|
|
543
|
+
* Used as a sanity check before committing an AST write.
|
|
544
|
+
*/
|
|
545
|
+
intentMatches(segments, oldFlatSelector) {
|
|
546
|
+
const lastSeg = segments[segments.length - 1];
|
|
547
|
+
if (!lastSeg)
|
|
548
|
+
return false;
|
|
549
|
+
const oldLower = oldFlatSelector.toLowerCase();
|
|
550
|
+
switch (lastSeg.type) {
|
|
551
|
+
case "locator":
|
|
552
|
+
return oldLower.includes(lastSeg.selector.toLowerCase());
|
|
553
|
+
case "getByRole":
|
|
554
|
+
return (oldLower.includes(lastSeg.role.toLowerCase()) ||
|
|
555
|
+
oldLower.includes("role="));
|
|
556
|
+
case "getByLabel":
|
|
557
|
+
case "getByText":
|
|
558
|
+
case "getByPlaceholder":
|
|
559
|
+
case "getByAltText":
|
|
560
|
+
case "getByTitle":
|
|
561
|
+
return oldLower.includes(lastSeg.text.toLowerCase());
|
|
562
|
+
case "getByTestId":
|
|
563
|
+
return oldLower.includes(lastSeg.testId.toLowerCase());
|
|
564
|
+
default:
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
exports.ChainValidator = ChainValidator;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Project } from "ts-morph";
|
|
2
|
+
import type { ASTUpdateResult } from "./ASTSourceUpdater.js";
|
|
3
|
+
export declare class CrossFileHealer {
|
|
4
|
+
static heal(modulePath: string, symbolName: string, newValue: string, callerFilePath: string, project: Project, depth?: number): ASTUpdateResult;
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=CrossFileHealer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CrossFileHealer.d.ts","sourceRoot":"","sources":["../../src/services/CrossFileHealer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAGnC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAmB7D,qBAAa,eAAe;IAC1B,MAAM,CAAC,IAAI,CACT,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,OAAO,EAChB,KAAK,SAAI,GACR,eAAe;CAqFnB"}
|