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,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"}