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.
Files changed (67) hide show
  1. package/README.md +27 -17
  2. package/dist/cli/commands/init.js +1 -1
  3. package/dist/cli/commands/merge.d.ts +13 -0
  4. package/dist/cli/commands/merge.d.ts.map +1 -0
  5. package/dist/cli/commands/merge.js +119 -0
  6. package/dist/cli/index.js +4 -1
  7. package/dist/config/SelaConfig.d.ts +1 -1
  8. package/dist/config/SelaConfig.d.ts.map +1 -1
  9. package/dist/config/SelaConfig.js +1 -1
  10. package/dist/engine/SelaEngine.d.ts.map +1 -1
  11. package/dist/engine/SelaEngine.js +54 -2
  12. package/dist/errors/SelaError.d.ts +8 -0
  13. package/dist/errors/SelaError.d.ts.map +1 -1
  14. package/dist/errors/SelaError.js +8 -0
  15. package/dist/fixtures/expectProxy.d.ts +1 -1
  16. package/dist/fixtures/expectProxy.js +6 -6
  17. package/dist/fixtures/index.d.ts +18 -2
  18. package/dist/fixtures/index.d.ts.map +1 -1
  19. package/dist/fixtures/index.js +34 -14
  20. package/dist/fixtures/moduleExpect.js +7 -7
  21. package/dist/fixtures/proxyTag.js +1 -1
  22. package/dist/services/ASTSourceUpdater.d.ts +45 -0
  23. package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
  24. package/dist/services/ASTSourceUpdater.js +201 -47
  25. package/dist/services/AnchorResolver.d.ts +157 -0
  26. package/dist/services/AnchorResolver.d.ts.map +1 -0
  27. package/dist/services/AnchorResolver.js +289 -0
  28. package/dist/services/DecisionEngine.d.ts +51 -0
  29. package/dist/services/DecisionEngine.d.ts.map +1 -0
  30. package/dist/services/DecisionEngine.js +260 -0
  31. package/dist/services/HealReportService.d.ts +31 -55
  32. package/dist/services/HealReportService.d.ts.map +1 -1
  33. package/dist/services/HealReportService.js +863 -1243
  34. package/dist/services/HealingAdvisory.d.ts +9 -1
  35. package/dist/services/HealingAdvisory.d.ts.map +1 -1
  36. package/dist/services/HealingAdvisory.js +8 -0
  37. package/dist/services/InitializerUpdater.d.ts.map +1 -1
  38. package/dist/services/InitializerUpdater.js +10 -0
  39. package/dist/services/InteractiveReview.d.ts.map +1 -1
  40. package/dist/services/InteractiveReview.js +4 -1
  41. package/dist/services/MutationApplier.d.ts +55 -0
  42. package/dist/services/MutationApplier.d.ts.map +1 -0
  43. package/dist/services/MutationApplier.js +322 -0
  44. package/dist/services/PendingPromptLedger.d.ts +7 -0
  45. package/dist/services/PendingPromptLedger.d.ts.map +1 -1
  46. package/dist/services/PendingPromptLedger.js +25 -0
  47. package/dist/services/ReportGenerator.d.ts +116 -30
  48. package/dist/services/ReportGenerator.d.ts.map +1 -1
  49. package/dist/services/ReportGenerator.js +150 -63
  50. package/dist/services/ReportMergeService.d.ts +95 -0
  51. package/dist/services/ReportMergeService.d.ts.map +1 -0
  52. package/dist/services/ReportMergeService.js +0 -0
  53. package/dist/services/SafetyGuard.d.ts +1 -1
  54. package/dist/services/SafetyGuard.d.ts.map +1 -1
  55. package/dist/services/SelectorSanitizer.d.ts +52 -0
  56. package/dist/services/SelectorSanitizer.d.ts.map +1 -0
  57. package/dist/services/SelectorSanitizer.js +318 -0
  58. package/dist/services/SnapshotService.js +1 -1
  59. package/dist/services/SourceUpdater.d.ts.map +1 -1
  60. package/dist/services/SourceUpdater.js +17 -0
  61. package/dist/services/TraceBackEngine.d.ts +67 -0
  62. package/dist/services/TraceBackEngine.d.ts.map +1 -0
  63. package/dist/services/TraceBackEngine.js +672 -0
  64. package/dist/utils/DOMUtils.d.ts +18 -2
  65. package/dist/utils/DOMUtils.d.ts.map +1 -1
  66. package/dist/utils/DOMUtils.js +335 -49
  67. 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(), "fixwright-snapshots");
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;AA4BtD,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;IAkT1B,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"}
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"}