sela-core 1.0.8 → 1.0.9
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 +27 -17
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/merge.d.ts +13 -0
- package/dist/cli/commands/merge.d.ts.map +1 -0
- package/dist/cli/commands/merge.js +119 -0
- package/dist/cli/index.js +4 -1
- package/dist/config/SelaConfig.d.ts +1 -1
- package/dist/config/SelaConfig.d.ts.map +1 -1
- package/dist/config/SelaConfig.js +1 -1
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +54 -2
- package/dist/errors/SelaError.d.ts +8 -0
- package/dist/errors/SelaError.d.ts.map +1 -1
- package/dist/errors/SelaError.js +8 -0
- package/dist/fixtures/expectProxy.d.ts +1 -1
- package/dist/fixtures/expectProxy.js +6 -6
- package/dist/fixtures/index.d.ts +18 -2
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +34 -14
- package/dist/fixtures/moduleExpect.js +7 -7
- package/dist/fixtures/proxyTag.js +1 -1
- package/dist/services/ASTSourceUpdater.d.ts +45 -0
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
- package/dist/services/ASTSourceUpdater.js +201 -47
- package/dist/services/AnchorResolver.d.ts +157 -0
- package/dist/services/AnchorResolver.d.ts.map +1 -0
- package/dist/services/AnchorResolver.js +289 -0
- package/dist/services/DecisionEngine.d.ts +51 -0
- package/dist/services/DecisionEngine.d.ts.map +1 -0
- package/dist/services/DecisionEngine.js +260 -0
- package/dist/services/HealReportService.d.ts +31 -55
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +863 -1243
- package/dist/services/HealingAdvisory.d.ts +9 -1
- package/dist/services/HealingAdvisory.d.ts.map +1 -1
- package/dist/services/HealingAdvisory.js +8 -0
- package/dist/services/InitializerUpdater.d.ts.map +1 -1
- package/dist/services/InitializerUpdater.js +10 -0
- package/dist/services/InteractiveReview.d.ts.map +1 -1
- package/dist/services/InteractiveReview.js +4 -1
- package/dist/services/MutationApplier.d.ts +55 -0
- package/dist/services/MutationApplier.d.ts.map +1 -0
- package/dist/services/MutationApplier.js +322 -0
- package/dist/services/PendingPromptLedger.d.ts +7 -0
- package/dist/services/PendingPromptLedger.d.ts.map +1 -1
- package/dist/services/PendingPromptLedger.js +25 -0
- package/dist/services/ReportGenerator.d.ts +116 -30
- package/dist/services/ReportGenerator.d.ts.map +1 -1
- package/dist/services/ReportGenerator.js +150 -63
- package/dist/services/ReportMergeService.d.ts +95 -0
- package/dist/services/ReportMergeService.d.ts.map +1 -0
- package/dist/services/ReportMergeService.js +0 -0
- package/dist/services/SafetyGuard.d.ts +1 -1
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SelectorSanitizer.d.ts +52 -0
- package/dist/services/SelectorSanitizer.d.ts.map +1 -0
- package/dist/services/SelectorSanitizer.js +318 -0
- package/dist/services/SnapshotService.js +1 -1
- package/dist/services/SourceUpdater.d.ts.map +1 -1
- package/dist/services/SourceUpdater.js +17 -0
- package/dist/services/TraceBackEngine.d.ts +67 -0
- package/dist/services/TraceBackEngine.d.ts.map +1 -0
- package/dist/services/TraceBackEngine.js +672 -0
- package/dist/utils/DOMUtils.d.ts +18 -2
- package/dist/utils/DOMUtils.d.ts.map +1 -1
- package/dist/utils/DOMUtils.js +335 -49
- package/package.json +1 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when `value` is (or contains, in a `>>` chain) a Playwright internal
|
|
3
|
+
* engine selector. These start with the `internal:` engine prefix.
|
|
4
|
+
*/
|
|
5
|
+
export declare function isInternalEngineSelector(value: string): boolean;
|
|
6
|
+
interface MethodDescriptor {
|
|
7
|
+
method: string;
|
|
8
|
+
primaryArg: string;
|
|
9
|
+
options?: Record<string, string | boolean | number>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parse a SINGLE internal selector segment into a method descriptor, or null
|
|
13
|
+
* when it is not a recognised internal form (so callers REFUSE rather than
|
|
14
|
+
* guess). Mirrors the engine→public mapping used by ASTSourceUpdater.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseInternalSelector(selector: string): MethodDescriptor | null;
|
|
17
|
+
export declare function buildMethodCall(receiver: string, desc: MethodDescriptor): string;
|
|
18
|
+
/**
|
|
19
|
+
* Translate an internal selector string into an idiomatic Playwright method
|
|
20
|
+
* expression rooted at `receiver`. Handles `>>` chains by chaining each
|
|
21
|
+
* segment. Returns null when ANY segment is an unrecognised internal form
|
|
22
|
+
* (frame `internal:control=…` plumbing, unknown engines) — the caller must
|
|
23
|
+
* then refuse rather than emit something fragile.
|
|
24
|
+
*/
|
|
25
|
+
export declare function internalToMethodExpression(selector: string, receiver?: string): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Convert an internal engine selector into a VALID PUBLIC `.locator()` string,
|
|
28
|
+
* preserving `>>` chaining. Returns null when ANY internal segment has no
|
|
29
|
+
* public string engine (so the caller must method-shift or refuse instead of
|
|
30
|
+
* stuffing an unusable string into a constant).
|
|
31
|
+
*
|
|
32
|
+
* Example: `internal:role=button[name="Update System"i]`
|
|
33
|
+
* → `role=button[name="Update System"]`
|
|
34
|
+
*/
|
|
35
|
+
export declare function internalToPublicSelectorString(selector: string): string | null;
|
|
36
|
+
export interface SanitizeResult {
|
|
37
|
+
/** The (possibly rewritten) expression text. */
|
|
38
|
+
text: string;
|
|
39
|
+
/** True when the original contained an internal selector that was rewritten. */
|
|
40
|
+
sanitized: boolean;
|
|
41
|
+
/** False when an internal selector remained that could NOT be translated. */
|
|
42
|
+
safe: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Rewrite any `recv.locator("internal:…")` / `recv.frameLocator("internal:…")`
|
|
46
|
+
* inside `expr` to its idiomatic method form. Returns `safe:false` when an
|
|
47
|
+
* internal selector remains that cannot be translated, so the caller refuses
|
|
48
|
+
* to write it.
|
|
49
|
+
*/
|
|
50
|
+
export declare function sanitizeLocatorExpression(expr: string): SanitizeResult;
|
|
51
|
+
export {};
|
|
52
|
+
//# sourceMappingURL=SelectorSanitizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SelectorSanitizer.d.ts","sourceRoot":"","sources":["../../src/services/SelectorSanitizer.ts"],"names":[],"mappings":"AAoCA;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAK/D;AAMD,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC,CAAC;CACrD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CA6D/E;AAWD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAyBhF;AAMD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAAS,GAChB,MAAM,GAAG,IAAI,CAmBf;AAgDD;;;;;;;;GAQG;AACH,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB9E;AAMD,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,SAAS,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,IAAI,EAAE,OAAO,CAAC;CACf;AAID;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAgEtE"}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/services/SelectorSanitizer.ts
|
|
3
|
+
//
|
|
4
|
+
// THE ABSOLUTE RULE (Ticket #2 hardening):
|
|
5
|
+
// Playwright's `internal:` engine selectors (e.g.
|
|
6
|
+
// `internal:role=button[name="Update System"i]`) are a PRIVATE runtime
|
|
7
|
+
// serialization. They must NEVER be written verbatim into user source — not
|
|
8
|
+
// as a `.locator()` string argument, and not into a string-literal constant
|
|
9
|
+
// consumed by `.locator()`. Although the engine happens to accept them at
|
|
10
|
+
// runtime, they are undocumented, version-fragile, and leak internals into
|
|
11
|
+
// the developer's clean code (bad DX, the exact bug this module prevents).
|
|
12
|
+
//
|
|
13
|
+
// This module is the single source of truth for:
|
|
14
|
+
// 1. DETECTING an internal engine selector (`isInternalEngineSelector`).
|
|
15
|
+
// 2. TRANSLATING it to the idiomatic public method form
|
|
16
|
+
// (`internalToMethodExpression`) — `getByRole(...)`, `getByText(...)`, …
|
|
17
|
+
// 3. SANITIZING a full locator expression (`sanitizeLocatorExpression`):
|
|
18
|
+
// rewriting `recv.locator("internal:…")` → `recv.getByRole(…)`, and
|
|
19
|
+
// reporting whether anything internal remains (unconvertible ⇒ unsafe).
|
|
20
|
+
//
|
|
21
|
+
// Pure, dependency-light (ts-morph only for expression rewriting). Every
|
|
22
|
+
// write-head (InitializerUpdater, MutationApplier, SourceUpdater regex gate)
|
|
23
|
+
// consults this so the invariant holds across all heal paths.
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.isInternalEngineSelector = isInternalEngineSelector;
|
|
26
|
+
exports.parseInternalSelector = parseInternalSelector;
|
|
27
|
+
exports.buildMethodCall = buildMethodCall;
|
|
28
|
+
exports.internalToMethodExpression = internalToMethodExpression;
|
|
29
|
+
exports.internalToPublicSelectorString = internalToPublicSelectorString;
|
|
30
|
+
exports.sanitizeLocatorExpression = sanitizeLocatorExpression;
|
|
31
|
+
const ts_morph_1 = require("ts-morph");
|
|
32
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
33
|
+
// DETECTION
|
|
34
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
35
|
+
/**
|
|
36
|
+
* True when `value` is (or contains, in a `>>` chain) a Playwright internal
|
|
37
|
+
* engine selector. These start with the `internal:` engine prefix.
|
|
38
|
+
*/
|
|
39
|
+
function isInternalEngineSelector(value) {
|
|
40
|
+
if (!value)
|
|
41
|
+
return false;
|
|
42
|
+
return value
|
|
43
|
+
.split(">>")
|
|
44
|
+
.some((seg) => seg.trim().toLowerCase().startsWith("internal:"));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parse a SINGLE internal selector segment into a method descriptor, or null
|
|
48
|
+
* when it is not a recognised internal form (so callers REFUSE rather than
|
|
49
|
+
* guess). Mirrors the engine→public mapping used by ASTSourceUpdater.
|
|
50
|
+
*/
|
|
51
|
+
function parseInternalSelector(selector) {
|
|
52
|
+
// Match against the raw (trimmed) selector. The case-insensitivity `i` flag
|
|
53
|
+
// is significant per-pattern (trailing for text/label, inside `[…]` for
|
|
54
|
+
// role), so it must NOT be stripped globally up front.
|
|
55
|
+
const s = selector.trim();
|
|
56
|
+
// internal:role=button[name="X"i][level=2]
|
|
57
|
+
const roleMatch = s.match(/^internal:role=([a-zA-Z]+)(\[.*\])?$/);
|
|
58
|
+
if (roleMatch) {
|
|
59
|
+
const desc = {
|
|
60
|
+
method: "getByRole",
|
|
61
|
+
primaryArg: roleMatch[1],
|
|
62
|
+
};
|
|
63
|
+
const attrs = roleMatch[2] ?? "";
|
|
64
|
+
const nameMatch = attrs.match(/name="([^"]*)"(i)?/);
|
|
65
|
+
const options = {};
|
|
66
|
+
if (nameMatch) {
|
|
67
|
+
options.name = nameMatch[1];
|
|
68
|
+
if (!nameMatch[2])
|
|
69
|
+
options.exact = true; // no `i` flag → exact match
|
|
70
|
+
}
|
|
71
|
+
const levelMatch = attrs.match(/level=(\d+)/);
|
|
72
|
+
if (levelMatch)
|
|
73
|
+
options.level = Number(levelMatch[1]);
|
|
74
|
+
if (Object.keys(options).length)
|
|
75
|
+
desc.options = options;
|
|
76
|
+
return desc;
|
|
77
|
+
}
|
|
78
|
+
// internal:label="X"i | internal:label=X
|
|
79
|
+
const labelQuoted = s.match(/^internal:label="([^"]*)"(i)?$/);
|
|
80
|
+
if (labelQuoted) {
|
|
81
|
+
return labelQuoted[2]
|
|
82
|
+
? { method: "getByLabel", primaryArg: labelQuoted[1] }
|
|
83
|
+
: { method: "getByLabel", primaryArg: labelQuoted[1], options: { exact: true } };
|
|
84
|
+
}
|
|
85
|
+
const labelBare = s.match(/^internal:label=([^[]+)$/);
|
|
86
|
+
if (labelBare) {
|
|
87
|
+
return { method: "getByLabel", primaryArg: labelBare[1].trim() };
|
|
88
|
+
}
|
|
89
|
+
// internal:text="X"i
|
|
90
|
+
const textMatch = s.match(/^internal:text="([^"]*)"(i)?$/);
|
|
91
|
+
if (textMatch) {
|
|
92
|
+
return textMatch[2]
|
|
93
|
+
? { method: "getByText", primaryArg: textMatch[1] }
|
|
94
|
+
: { method: "getByText", primaryArg: textMatch[1], options: { exact: true } };
|
|
95
|
+
}
|
|
96
|
+
// internal:attr=[placeholder="X"] / [alt="X"] / [title="X"]
|
|
97
|
+
const placeholder = s.match(/^internal:attr=\[placeholder="([^"]*)"\]$/);
|
|
98
|
+
if (placeholder)
|
|
99
|
+
return { method: "getByPlaceholder", primaryArg: placeholder[1] };
|
|
100
|
+
const alt = s.match(/^internal:attr=\[alt="([^"]*)"\]$/);
|
|
101
|
+
if (alt)
|
|
102
|
+
return { method: "getByAltText", primaryArg: alt[1] };
|
|
103
|
+
const title = s.match(/^internal:attr=\[title="([^"]*)"\]$/);
|
|
104
|
+
if (title)
|
|
105
|
+
return { method: "getByTitle", primaryArg: title[1] };
|
|
106
|
+
// internal:testid=X | internal:attr=[data-testid="X"]
|
|
107
|
+
const testId = s.match(/^internal:testid=(.+)$/) ||
|
|
108
|
+
s.match(/^internal:attr=\[data-testid="([^"]*)"\]$/);
|
|
109
|
+
if (testId)
|
|
110
|
+
return { method: "getByTestId", primaryArg: testId[1].trim() };
|
|
111
|
+
return null; // unrecognised internal form → caller must refuse
|
|
112
|
+
}
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
114
|
+
// BUILD (method descriptor → idiomatic call text)
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
116
|
+
function q(s) {
|
|
117
|
+
// Prefer double quotes, escaping any embedded ones.
|
|
118
|
+
return `"${s.replace(/"/g, '\\"')}"`;
|
|
119
|
+
}
|
|
120
|
+
function buildMethodCall(receiver, desc) {
|
|
121
|
+
switch (desc.method) {
|
|
122
|
+
case "getByRole": {
|
|
123
|
+
const opts = [];
|
|
124
|
+
if (desc.options?.name !== undefined)
|
|
125
|
+
opts.push(`name: ${q(String(desc.options.name))}`);
|
|
126
|
+
if (desc.options?.exact === true)
|
|
127
|
+
opts.push(`exact: true`);
|
|
128
|
+
if (desc.options?.level !== undefined)
|
|
129
|
+
opts.push(`level: ${desc.options.level}`);
|
|
130
|
+
const optStr = opts.length ? `, { ${opts.join(", ")} }` : "";
|
|
131
|
+
return `${receiver}.getByRole(${q(desc.primaryArg)}${optStr})`;
|
|
132
|
+
}
|
|
133
|
+
case "getByText":
|
|
134
|
+
case "getByLabel": {
|
|
135
|
+
const exact = desc.options?.exact === true ? `, { exact: true }` : "";
|
|
136
|
+
return `${receiver}.${desc.method}(${q(desc.primaryArg)}${exact})`;
|
|
137
|
+
}
|
|
138
|
+
case "getByPlaceholder":
|
|
139
|
+
case "getByAltText":
|
|
140
|
+
case "getByTitle":
|
|
141
|
+
case "getByTestId":
|
|
142
|
+
return `${receiver}.${desc.method}(${q(desc.primaryArg)})`;
|
|
143
|
+
default:
|
|
144
|
+
return `${receiver}.locator(${q(desc.primaryArg)})`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
148
|
+
// TRANSLATE (full internal selector string → method expression)
|
|
149
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
150
|
+
/**
|
|
151
|
+
* Translate an internal selector string into an idiomatic Playwright method
|
|
152
|
+
* expression rooted at `receiver`. Handles `>>` chains by chaining each
|
|
153
|
+
* segment. Returns null when ANY segment is an unrecognised internal form
|
|
154
|
+
* (frame `internal:control=…` plumbing, unknown engines) — the caller must
|
|
155
|
+
* then refuse rather than emit something fragile.
|
|
156
|
+
*/
|
|
157
|
+
function internalToMethodExpression(selector, receiver = "page") {
|
|
158
|
+
const segments = selector
|
|
159
|
+
.split(">>")
|
|
160
|
+
.map((s) => s.trim())
|
|
161
|
+
.filter(Boolean);
|
|
162
|
+
if (segments.length === 0)
|
|
163
|
+
return null;
|
|
164
|
+
let expr = receiver;
|
|
165
|
+
for (const seg of segments) {
|
|
166
|
+
if (seg.toLowerCase().startsWith("internal:")) {
|
|
167
|
+
const desc = parseInternalSelector(seg);
|
|
168
|
+
if (!desc)
|
|
169
|
+
return null; // unconvertible internal form
|
|
170
|
+
expr = buildMethodCall(expr, desc);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// Plain CSS / text segment — stays a public .locator() string.
|
|
174
|
+
expr = `${expr}.locator(${q(seg)})`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return expr;
|
|
178
|
+
}
|
|
179
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
180
|
+
// TRANSLATE (internal selector → valid PUBLIC .locator() string)
|
|
181
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
182
|
+
function escAttr(s) {
|
|
183
|
+
return s.replace(/"/g, '\\"');
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Convert a single internal segment to a valid PUBLIC `.locator()` selector
|
|
187
|
+
* string (the documented selector engines: `role=`, `text=`, css attribute).
|
|
188
|
+
* Returns null for forms that have NO public string engine (label is an
|
|
189
|
+
* associative relationship, not an attribute — only `getByLabel(...)` works).
|
|
190
|
+
*/
|
|
191
|
+
function segmentToPublicSelector(seg) {
|
|
192
|
+
const desc = parseInternalSelector(seg);
|
|
193
|
+
if (!desc)
|
|
194
|
+
return null;
|
|
195
|
+
switch (desc.method) {
|
|
196
|
+
case "getByRole": {
|
|
197
|
+
const attrs = [];
|
|
198
|
+
if (desc.options?.name !== undefined)
|
|
199
|
+
attrs.push(`name="${escAttr(String(desc.options.name))}"`);
|
|
200
|
+
if (desc.options?.level !== undefined)
|
|
201
|
+
attrs.push(`level=${desc.options.level}`);
|
|
202
|
+
const attrStr = attrs.length ? `[${attrs.join("][")}]` : "";
|
|
203
|
+
return `role=${desc.primaryArg}${attrStr}`;
|
|
204
|
+
}
|
|
205
|
+
case "getByText":
|
|
206
|
+
// Quoted text engine = exact-ish; unquoted = substring (i-flag default).
|
|
207
|
+
return desc.options?.exact
|
|
208
|
+
? `text="${escAttr(desc.primaryArg)}"`
|
|
209
|
+
: `text=${desc.primaryArg}`;
|
|
210
|
+
case "getByPlaceholder":
|
|
211
|
+
return `[placeholder="${escAttr(desc.primaryArg)}"]`;
|
|
212
|
+
case "getByAltText":
|
|
213
|
+
return `[alt="${escAttr(desc.primaryArg)}"]`;
|
|
214
|
+
case "getByTitle":
|
|
215
|
+
return `[title="${escAttr(desc.primaryArg)}"]`;
|
|
216
|
+
case "getByTestId":
|
|
217
|
+
return `[data-testid="${escAttr(desc.primaryArg)}"]`;
|
|
218
|
+
case "getByLabel":
|
|
219
|
+
default:
|
|
220
|
+
return null; // no public string-engine equivalent
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Convert an internal engine selector into a VALID PUBLIC `.locator()` string,
|
|
225
|
+
* preserving `>>` chaining. Returns null when ANY internal segment has no
|
|
226
|
+
* public string engine (so the caller must method-shift or refuse instead of
|
|
227
|
+
* stuffing an unusable string into a constant).
|
|
228
|
+
*
|
|
229
|
+
* Example: `internal:role=button[name="Update System"i]`
|
|
230
|
+
* → `role=button[name="Update System"]`
|
|
231
|
+
*/
|
|
232
|
+
function internalToPublicSelectorString(selector) {
|
|
233
|
+
const segments = selector
|
|
234
|
+
.split(">>")
|
|
235
|
+
.map((s) => s.trim())
|
|
236
|
+
.filter(Boolean);
|
|
237
|
+
if (segments.length === 0)
|
|
238
|
+
return null;
|
|
239
|
+
const out = [];
|
|
240
|
+
for (const seg of segments) {
|
|
241
|
+
if (seg.toLowerCase().startsWith("internal:")) {
|
|
242
|
+
const pub = segmentToPublicSelector(seg);
|
|
243
|
+
if (!pub)
|
|
244
|
+
return null;
|
|
245
|
+
out.push(pub);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
out.push(seg); // already a public css/text segment
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return out.join(" >> ");
|
|
252
|
+
}
|
|
253
|
+
let probeCounter = 0;
|
|
254
|
+
/**
|
|
255
|
+
* Rewrite any `recv.locator("internal:…")` / `recv.frameLocator("internal:…")`
|
|
256
|
+
* inside `expr` to its idiomatic method form. Returns `safe:false` when an
|
|
257
|
+
* internal selector remains that cannot be translated, so the caller refuses
|
|
258
|
+
* to write it.
|
|
259
|
+
*/
|
|
260
|
+
function sanitizeLocatorExpression(expr) {
|
|
261
|
+
// Fast path: nothing internal anywhere.
|
|
262
|
+
if (!expr.includes("internal:")) {
|
|
263
|
+
return { text: expr, sanitized: false, safe: true };
|
|
264
|
+
}
|
|
265
|
+
const project = new ts_morph_1.Project({
|
|
266
|
+
useInMemoryFileSystem: true,
|
|
267
|
+
compilerOptions: { allowJs: true, noEmit: true, skipLibCheck: true, noResolve: true },
|
|
268
|
+
});
|
|
269
|
+
let current = expr;
|
|
270
|
+
let sanitized = false;
|
|
271
|
+
// Iteratively rewrite the first internal locator call until none remain.
|
|
272
|
+
// replaceWithText invalidates nodes, so we re-parse each pass.
|
|
273
|
+
for (let pass = 0; pass < 32; pass++) {
|
|
274
|
+
let sf;
|
|
275
|
+
try {
|
|
276
|
+
sf = project.createSourceFile(`__san${probeCounter++}.ts`, `const __x = ${current};`, {
|
|
277
|
+
overwrite: true,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return { text: expr, sanitized, safe: false };
|
|
282
|
+
}
|
|
283
|
+
const call = sf
|
|
284
|
+
.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)
|
|
285
|
+
.find((c) => {
|
|
286
|
+
const callee = c.getExpression();
|
|
287
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(callee))
|
|
288
|
+
return false;
|
|
289
|
+
const m = callee.getName();
|
|
290
|
+
if (m !== "locator" && m !== "frameLocator")
|
|
291
|
+
return false;
|
|
292
|
+
const arg = c.getArguments()[0];
|
|
293
|
+
if (!arg)
|
|
294
|
+
return false;
|
|
295
|
+
const lit = ts_morph_1.Node.isStringLiteral(arg) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(arg)
|
|
296
|
+
? arg.getLiteralValue()
|
|
297
|
+
: null;
|
|
298
|
+
return lit !== null && isInternalEngineSelector(lit);
|
|
299
|
+
});
|
|
300
|
+
if (!call)
|
|
301
|
+
break; // no more internal locator calls
|
|
302
|
+
const callee = call.getExpression();
|
|
303
|
+
const receiver = callee.getExpression().getText();
|
|
304
|
+
const arg = call.getArguments()[0];
|
|
305
|
+
const internalSel = arg.getLiteralValue();
|
|
306
|
+
const methodExpr = internalToMethodExpression(internalSel, receiver);
|
|
307
|
+
if (!methodExpr) {
|
|
308
|
+
// Unconvertible — leave as-is and report unsafe.
|
|
309
|
+
return { text: current, sanitized, safe: false };
|
|
310
|
+
}
|
|
311
|
+
call.replaceWithText(methodExpr);
|
|
312
|
+
current = sf.getVariableDeclaration("__x").getInitializer().getText();
|
|
313
|
+
sanitized = true;
|
|
314
|
+
}
|
|
315
|
+
// Final integrity check: nothing internal must survive.
|
|
316
|
+
const safe = !isInternalEngineSelector(current) && !current.includes("internal:");
|
|
317
|
+
return { text: current, sanitized, safe };
|
|
318
|
+
}
|
|
@@ -39,7 +39,7 @@ const path = __importStar(require("path"));
|
|
|
39
39
|
class SnapshotService {
|
|
40
40
|
baseDir;
|
|
41
41
|
constructor() {
|
|
42
|
-
this.baseDir = path.join(process.cwd(), "
|
|
42
|
+
this.baseDir = path.join(process.cwd(), "sela-snapshots");
|
|
43
43
|
if (!fs.existsSync(this.baseDir)) {
|
|
44
44
|
fs.mkdirSync(this.baseDir, { recursive: true });
|
|
45
45
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SourceUpdater.d.ts","sourceRoot":"","sources":["../../src/services/SourceUpdater.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"SourceUpdater.d.ts","sourceRoot":"","sources":["../../src/services/SourceUpdater.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AA6BtD,UAAU,YAAY;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,cAAc,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,aAAa;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAqZD,qBAAa,aAAa;IAMxB,MAAM,CAAC,MAAM,CACX,MAAM,EAAE,aAAa,EACrB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,MAAM,EACpB,mBAAmB,CAAC,EAAE,MAAM,EAC5B,UAAU,CAAC,EAAE,eAAe,EAAE,EAC9B,aAAa,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EACpD,kBAAkB,CAAC,EAAE,iBAAiB,EAAE,EACxC,iBAAiB,CAAC,EAAE,iBAAiB,EAAE,EACvC,cAAc,CAAC,EAAE,SAAS,GAAG,aAAa,GAAG,QAAQ,EACrD,UAAU,CAAC,EAAE,OAAO,GACnB,YAAY;IAoDf,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAwC/B,OAAO,CAAC,MAAM,CAAC,WAAW;IAsU1B,OAAO,CAAC,MAAM,CAAC,2BAA2B;IA8E1C,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA8EvC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAoErC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAsBjC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IA6DpC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAuCpC,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA6CvC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAqCrC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAqGrC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IA0BjC,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAatC,MAAM,CAAC,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAgBlD,MAAM,CAAC,cAAc,CACnB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,YAAY;IA0Cf,0FAA0F;IAC1F,MAAM,CAAC,eAAe,IAAI,IAAI;CAG/B"}
|
|
@@ -41,6 +41,7 @@ const ASTSourceUpdater_1 = require("./ASTSourceUpdater");
|
|
|
41
41
|
const DryRunGuard_1 = require("../config/DryRunGuard");
|
|
42
42
|
const WorkspaceSnapshotService_1 = require("./WorkspaceSnapshotService");
|
|
43
43
|
const SelaError_1 = require("../errors/SelaError");
|
|
44
|
+
const SelectorSanitizer_1 = require("./SelectorSanitizer");
|
|
44
45
|
// Centralised write gate - every disk-mutating `fs.writeFileSync` call in
|
|
45
46
|
// this module routes through `persistFile` so the SELA_DRY_RUN bypass is
|
|
46
47
|
// enforced in one place, and the Clean-Room WorkspaceSnapshotService
|
|
@@ -475,6 +476,22 @@ class SourceUpdater {
|
|
|
475
476
|
}
|
|
476
477
|
console.warn(`[SourceUpdater] ⚠️ AST engine error: ${astError.message}. Falling back to Regex`);
|
|
477
478
|
}
|
|
479
|
+
// ── ABSOLUTE GUARD: internal engine selector ───────────────────
|
|
480
|
+
// The AST engine declined to mutate (could not method-shift). The regex
|
|
481
|
+
// fallback only does RAW string substitution of `oldSelector → newSelector`,
|
|
482
|
+
// which would stamp the private `internal:role=…` engine string into the
|
|
483
|
+
// developer's source (the bug this guard kills). A `.locator()` string
|
|
484
|
+
// cannot express a semantic locator, so there is no safe regex outcome:
|
|
485
|
+
// refuse, leave source untouched. The in-memory proxy swap already keeps
|
|
486
|
+
// this run green; the correct method-shift is the new composeHeal pipeline.
|
|
487
|
+
if ((0, SelectorSanitizer_1.isInternalEngineSelector)(effectiveNewSelector)) {
|
|
488
|
+
console.warn(`[SourceUpdater] 🛑 Regex blocked - internal engine selector cannot be ` +
|
|
489
|
+
`string-substituted into source. Method-shift required. Source unchanged.`);
|
|
490
|
+
return {
|
|
491
|
+
success: false,
|
|
492
|
+
reason: "INTERNAL_SELECTOR_GUARD: refusing regex string-substitution of an internal engine selector (method-shift required)",
|
|
493
|
+
};
|
|
494
|
+
}
|
|
478
495
|
// ── 2. Regex Fallback ──────────────────────────────────────────
|
|
479
496
|
console.log(`[SourceUpdater] 🔄 Using Regex fallback strategies...`);
|
|
480
497
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Node, SourceFile, CallExpression } from "ts-morph";
|
|
2
|
+
import { AnchorKey } from "./AnchorResolver";
|
|
3
|
+
export type MethodCategory = "selector" | "frame" | "modifier" | "action" | "unknown";
|
|
4
|
+
export declare function categorizeMethod(name: string): MethodCategory;
|
|
5
|
+
export type TraceAbortCode = "PARAMETER" | "FACTORY" | "DYNAMIC_SELECTOR" | "DESTRUCTURED" | "REASSIGNED" | "CROSS_FILE" | "DEFINITION_NOT_FOUND" | "MAX_DEPTH" | "UNSUPPORTED";
|
|
6
|
+
export type TraceSeverity = "soft" | "hard";
|
|
7
|
+
export interface ChainMethod {
|
|
8
|
+
name: string;
|
|
9
|
+
category: MethodCategory;
|
|
10
|
+
}
|
|
11
|
+
export type DeclKind = "VariableDeclaration" | "PropertyDeclaration" | "PropertyAssignment" | "InlineCallSite";
|
|
12
|
+
export interface TraceTarget {
|
|
13
|
+
declKind: DeclKind;
|
|
14
|
+
/**
|
|
15
|
+
* The node to mutate at source: the locator-chain expression (a
|
|
16
|
+
* VariableDeclaration's initializer, a POM property's value, or the inline
|
|
17
|
+
* selector chain with trailing actions stripped).
|
|
18
|
+
*/
|
|
19
|
+
targetNode: Node;
|
|
20
|
+
/** The owning declaration node; null for an inline call-site. */
|
|
21
|
+
declaration: Node | null;
|
|
22
|
+
/** Variable / property name owning the target, or null. */
|
|
23
|
+
symbolName: string | null;
|
|
24
|
+
/** Stable chain root ("page", "context", …). */
|
|
25
|
+
rootReceiver: string;
|
|
26
|
+
/** Stage-1 identity anchor for the target node. */
|
|
27
|
+
anchor: AnchorKey;
|
|
28
|
+
}
|
|
29
|
+
export type TraceOutcome = {
|
|
30
|
+
status: "target";
|
|
31
|
+
target: TraceTarget;
|
|
32
|
+
/** Methods seen on the failing call-site chain, root→leaf. */
|
|
33
|
+
callSiteChain: ChainMethod[];
|
|
34
|
+
/**
|
|
35
|
+
* For chained cascades (§1.1) — every upstream VariableDeclaration the
|
|
36
|
+
* target depends on, root-most first, target last. Single-element when
|
|
37
|
+
* there is no cascade. Runtime evidence later picks the broken link.
|
|
38
|
+
*/
|
|
39
|
+
dependencyChain: TraceTarget[];
|
|
40
|
+
} | {
|
|
41
|
+
status: "abort";
|
|
42
|
+
severity: TraceSeverity;
|
|
43
|
+
code: TraceAbortCode;
|
|
44
|
+
reason: string;
|
|
45
|
+
node?: Node;
|
|
46
|
+
};
|
|
47
|
+
export declare class TraceBackEngine {
|
|
48
|
+
/** Phase 0 convenience: resolve (1-based line, 0-based col) → node → trace. */
|
|
49
|
+
static traceFromPosition(sourceFile: SourceFile, line: number, column: number): TraceOutcome;
|
|
50
|
+
/** Phases 1–5. */
|
|
51
|
+
static traceFromNode(start: Node): TraceOutcome;
|
|
52
|
+
}
|
|
53
|
+
export declare function climbToTopCall(start: Node): CallExpression | null;
|
|
54
|
+
export declare function getLeftmostReceiver(topCall: CallExpression): {
|
|
55
|
+
leftmost: Node;
|
|
56
|
+
chain: ChainMethod[];
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Lexical-scope resolution of `name` as seen from `useOffset`. Walks
|
|
60
|
+
* enclosing scopes outward: parameters of enclosing functions, then
|
|
61
|
+
* variable/function declarations introduced in each enclosing block /
|
|
62
|
+
* source file. Pure compiler-API traversal — no language service.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveValueDeclaration(fromNode: Node, name: string, useOffset: number): Node | null;
|
|
65
|
+
/** Strip trailing action/query calls, returning the locator-chain node. */
|
|
66
|
+
export declare function stripTrailingActions(topCall: CallExpression): Node;
|
|
67
|
+
//# sourceMappingURL=TraceBackEngine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TraceBackEngine.d.ts","sourceRoot":"","sources":["../../src/services/TraceBackEngine.ts"],"names":[],"mappings":"AAwBA,OAAO,EACL,IAAI,EACJ,UAAU,EACV,cAAc,EAIf,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,SAAS,EAAsC,MAAM,kBAAkB,CAAC;AAsFjF,MAAM,MAAM,cAAc,GACtB,UAAU,GACV,OAAO,GACP,UAAU,GACV,QAAQ,GACR,SAAS,CAAC;AAEd,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAM7D;AAMD,MAAM,MAAM,cAAc,GACtB,WAAW,GACX,SAAS,GACT,kBAAkB,GAClB,cAAc,GACd,YAAY,GACZ,YAAY,GACZ,sBAAsB,GACtB,WAAW,GACX,aAAa,CAAC;AAElB,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,MAAM,QAAQ,GAChB,qBAAqB,GACrB,qBAAqB,GACrB,oBAAoB,GACpB,gBAAgB,CAAC;AAErB,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,QAAQ,CAAC;IACnB;;;;OAIG;IACH,UAAU,EAAE,IAAI,CAAC;IACjB,iEAAiE;IACjE,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;IACzB,2DAA2D;IAC3D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,MAAM,EAAE,SAAS,CAAC;CACnB;AAED,MAAM,MAAM,YAAY,GACpB;IACE,MAAM,EAAE,QAAQ,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC;IACpB,8DAA8D;IAC9D,aAAa,EAAE,WAAW,EAAE,CAAC;IAC7B;;;;OAIG;IACH,eAAe,EAAE,WAAW,EAAE,CAAC;CAChC,GACD;IACE,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,IAAI,CAAC;CACb,CAAC;AAQN,qBAAa,eAAe;IAC1B,+EAA+E;IAC/E,MAAM,CAAC,iBAAiB,CACtB,UAAU,EAAE,UAAU,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,YAAY;IAqBf,kBAAkB;IAClB,MAAM,CAAC,aAAa,CAAC,KAAK,EAAE,IAAI,GAAG,YAAY;CAoChD;AAMD,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,GAAG,cAAc,GAAG,IAAI,CA6BjE;AAMD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,cAAc,GAAG;IAC5D,QAAQ,EAAE,IAAI,CAAC;IACf,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB,CA6BA;AAuWD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,IAAI,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,IAAI,GAAG,IAAI,CAmBb;AAmHD,2EAA2E;AAC3E,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAelE"}
|