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
@@ -24,8 +24,24 @@ export declare class DOMUtils {
24
24
  */
25
25
  private static _getNeighborhoodFromFrame;
26
26
  /**
27
- * Runs the neighborhood-DOM extraction script inside the given execution
28
- * context (either a Page or a Frame).
27
+ * Runs the "Smart Neighborhood" DOM-extraction script inside the given
28
+ * execution context (a Page or a Frame). Implements asymmetric scope:
29
+ *
30
+ * - FULL semantic ancestry spine (anchor → root, crossing shadow→host)
31
+ * is always kept — this is cheap and carries the critical context
32
+ * (form/fieldset/dialog/landmark/id anchors).
33
+ * - SIBLINGS are pruned to a fixed K-window around the spine child; the
34
+ * number dropped is surfaced via `data-sibling-total` so the AI knows
35
+ * pruning occurred (and never assumes a positional selector is safe).
36
+ * - GLOBAL uniqueness is probed in the SAME evaluate pass; when the
37
+ * anchor's primary signal matches >1 element page-wide, the count is
38
+ * attached as `fx-global-matches` — this defeats the "neighborhood is
39
+ * unique but the page is not" trap without shipping the whole DOM.
40
+ * - Char-budget (cheap proxy for tokens) bounds the final payload.
41
+ *
42
+ * Generic wrappers (div/span) with no semantic signal are collapsed; visual
43
+ * noise (class/style) is stripped; script/style/svg and hidden elements are
44
+ * dropped. Shadow DOM and same-origin iframe content are pierced and inlined.
29
45
  */
30
46
  private static _evaluateNeighborhood;
31
47
  static getElementDNA: (page: Page, selector: string) => Promise<ElementDNA>;
@@ -1 +1 @@
1
- {"version":3,"file":"DOMUtils.d.ts","sourceRoot":"","sources":["../../src/utils/DOMUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,qBAAa,QAAQ;IACnB;;;;OAIG;IACH,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAKjD;;;;OAIG;IACH,MAAM,CAAC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE;WAM5B,kBAAkB,CACpC,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAerD;;;;OAIG;mBACkB,yBAAyB;IAoD9C;;;OAGG;mBACkB,qBAAqB;IAmF1C,MAAM,CAAC,aAAa,GAClB,MAAM,IAAI,EACV,UAAU,MAAM,KACf,OAAO,CAAC,UAAU,CAAC,CAqCpB;CACH"}
1
+ {"version":3,"file":"DOMUtils.d.ts","sourceRoot":"","sources":["../../src/utils/DOMUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,qBAAa,QAAQ;IACnB;;;;OAIG;IACH,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAKjD;;;;OAIG;IACH,MAAM,CAAC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE;WAM5B,kBAAkB,CACpC,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAerD;;;;OAIG;mBACkB,yBAAyB;IAoD9C;;;;;;;;;;;;;;;;;;;OAmBG;mBACkB,qBAAqB;IAoW1C,MAAM,CAAC,aAAa,GAClB,MAAM,IAAI,EACV,UAAU,MAAM,KACf,OAAO,CAAC,UAAU,CAAC,CAqCpB;CACH"}
@@ -76,68 +76,354 @@ class DOMUtils {
76
76
  }
77
77
  }
78
78
  /**
79
- * Runs the neighborhood-DOM extraction script inside the given execution
80
- * context (either a Page or a Frame).
79
+ * Runs the "Smart Neighborhood" DOM-extraction script inside the given
80
+ * execution context (a Page or a Frame). Implements asymmetric scope:
81
+ *
82
+ * - FULL semantic ancestry spine (anchor → root, crossing shadow→host)
83
+ * is always kept — this is cheap and carries the critical context
84
+ * (form/fieldset/dialog/landmark/id anchors).
85
+ * - SIBLINGS are pruned to a fixed K-window around the spine child; the
86
+ * number dropped is surfaced via `data-sibling-total` so the AI knows
87
+ * pruning occurred (and never assumes a positional selector is safe).
88
+ * - GLOBAL uniqueness is probed in the SAME evaluate pass; when the
89
+ * anchor's primary signal matches >1 element page-wide, the count is
90
+ * attached as `fx-global-matches` — this defeats the "neighborhood is
91
+ * unique but the page is not" trap without shipping the whole DOM.
92
+ * - Char-budget (cheap proxy for tokens) bounds the final payload.
93
+ *
94
+ * Generic wrappers (div/span) with no semantic signal are collapsed; visual
95
+ * noise (class/style) is stripped; script/style/svg and hidden elements are
96
+ * dropped. Shadow DOM and same-origin iframe content are pierced and inlined.
81
97
  */
82
98
  static async _evaluateNeighborhood(context, selector) {
83
- console.log(`[DOM-EXTRACT] 🧠 Force-Extracting full context DOM for: "${selector}"`);
84
- const result = await context.evaluate(() => {
85
- let indexCounter = 0;
86
- const processNode = (node) => {
87
- // 1. טיפול בטקסט
99
+ console.log(`[DOM-EXTRACT] 🧠 Smart-Neighborhood extraction for: "${selector}"`);
100
+ const result = await context.evaluate((cfg) => {
101
+ const { selector, K, maxText, budget } = cfg;
102
+ // ---- Tag taxonomies ------------------------------------------------
103
+ const SKIP = new Set([
104
+ "script",
105
+ "style",
106
+ "link",
107
+ "svg",
108
+ "noscript",
109
+ "template",
110
+ "meta",
111
+ ]);
112
+ // Kept verbatim but never directly selectable → no fx-id index.
113
+ const CONTAINER = new Set([
114
+ "html",
115
+ "body",
116
+ "form",
117
+ "nav",
118
+ "section",
119
+ "header",
120
+ "footer",
121
+ "main",
122
+ "article",
123
+ "aside",
124
+ "ul",
125
+ "ol",
126
+ "dl",
127
+ "table",
128
+ "thead",
129
+ "tbody",
130
+ "tfoot",
131
+ "tr",
132
+ "colgroup",
133
+ "fieldset",
134
+ "figure",
135
+ "figcaption",
136
+ "caption",
137
+ "menu",
138
+ "details",
139
+ ]);
140
+ // Generic layout wrappers: collapsed when they carry no semantic signal.
141
+ const COLLAPSIBLE = new Set(["div", "span"]);
142
+ const SIGNAL_ATTRS = ["id", "data-testid", "name", "role", "aria-label"];
143
+ // Output attribute allow-list (class/style intentionally excluded).
144
+ const PRESERVED = [
145
+ "id",
146
+ "data-testid",
147
+ "name",
148
+ "role",
149
+ "type",
150
+ "href",
151
+ "alt",
152
+ "title",
153
+ "placeholder",
154
+ "value",
155
+ "aria-label",
156
+ "aria-labelledby",
157
+ "aria-describedby",
158
+ "for",
159
+ "list",
160
+ "src",
161
+ "srcdoc",
162
+ ];
163
+ const isTransparent = (tag) => tag === "body" || tag === "html";
164
+ // ---- Deep query: pierce shadow roots + same-origin iframes ---------
165
+ const deepQuery = (root, sel) => {
166
+ let found = null;
167
+ try {
168
+ found = root.querySelector(sel);
169
+ }
170
+ catch {
171
+ found = null;
172
+ }
173
+ if (found)
174
+ return found;
175
+ let nodes = [];
176
+ try {
177
+ nodes = Array.from(root.querySelectorAll("*"));
178
+ }
179
+ catch {
180
+ nodes = [];
181
+ }
182
+ for (const e of nodes) {
183
+ if (e.shadowRoot) {
184
+ const r = deepQuery(e.shadowRoot, sel);
185
+ if (r)
186
+ return r;
187
+ }
188
+ if (e.tagName === "IFRAME") {
189
+ try {
190
+ const d = e.contentDocument;
191
+ if (d) {
192
+ const r = deepQuery(d, sel);
193
+ if (r)
194
+ return r;
195
+ }
196
+ }
197
+ catch {
198
+ /* cross-origin frame — skip */
199
+ }
200
+ }
201
+ }
202
+ return null;
203
+ };
204
+ // ---- Visibility ----------------------------------------------------
205
+ const isHidden = (el) => {
206
+ const e = el;
207
+ if (e.getAttribute && e.getAttribute("aria-hidden") === "true")
208
+ return true;
209
+ if (e.hasAttribute && e.hasAttribute("hidden"))
210
+ return true;
211
+ if (el.tagName === "INPUT" && e.getAttribute("type") === "hidden")
212
+ return true;
213
+ if (e.style &&
214
+ (e.style.display === "none" || e.style.visibility === "hidden"))
215
+ return true;
216
+ try {
217
+ const view = el.ownerDocument?.defaultView || window;
218
+ const cs = view.getComputedStyle(e);
219
+ if (cs && (cs.display === "none" || cs.visibility === "hidden"))
220
+ return true;
221
+ }
222
+ catch {
223
+ /* getComputedStyle across docs may throw — ignore */
224
+ }
225
+ return false;
226
+ };
227
+ const hasSignal = (el) => SIGNAL_ATTRS.some((a) => !!el.getAttribute?.(a));
228
+ const containsHeader = (el) => el.tagName === "TH" || !!el.querySelector?.("th");
229
+ // ---- Anchor + ancestry spine --------------------------------------
230
+ const anchor = deepQuery(document, selector);
231
+ const spine = new Set();
232
+ // Walk an element + all its ancestors into the spine, crossing shadow
233
+ // boundaries (ShadowRoot, nodeType 11 → host element).
234
+ const addLineage = (start) => {
235
+ let cur = start;
236
+ while (cur) {
237
+ spine.add(cur);
238
+ if (!cur.parentNode)
239
+ break;
240
+ cur =
241
+ cur.parentNode.nodeType === 11
242
+ ? cur.parentNode.host || null
243
+ : cur.parentNode;
244
+ }
245
+ };
246
+ if (anchor) {
247
+ addLineage(anchor);
248
+ // Detached semantic refs (#14-16): the accessible name/description
249
+ // may live anywhere in the document, linked only by id. Pull those
250
+ // nodes (and their lineage) into the spine so pruning never drops
251
+ // the context the AI needs for getByLabel / aria-based selectors.
252
+ const refIds = [];
253
+ ["aria-labelledby", "aria-describedby", "list"].forEach((a) => {
254
+ const v = anchor.getAttribute(a);
255
+ if (v)
256
+ refIds.push(...v.split(/\s+/).filter(Boolean));
257
+ });
258
+ refIds.forEach((id) => {
259
+ const e = document.getElementById(id);
260
+ if (e)
261
+ addLineage(e);
262
+ });
263
+ // Reverse link: <label for="anchorId"> sitting elsewhere in the DOM.
264
+ const aid = anchor.getAttribute("id");
265
+ if (aid) {
266
+ try {
267
+ document
268
+ .querySelectorAll(`label[for="${aid.replace(/(["\\])/g, "\\$1")}"]`)
269
+ .forEach((l) => addLineage(l));
270
+ }
271
+ catch {
272
+ /* ignore malformed id */
273
+ }
274
+ }
275
+ }
276
+ // ---- Global uniqueness probe (same evaluate pass) -----------------
277
+ const globalMatches = (el) => {
278
+ const esc = (v) => v.replace(/(["\\])/g, "\\$1");
279
+ const testid = el.getAttribute("data-testid");
280
+ const id = el.getAttribute("id");
281
+ const name = el.getAttribute("name");
282
+ const role = el.getAttribute("role");
283
+ const tag = el.tagName.toLowerCase();
284
+ let sel = "";
285
+ if (testid)
286
+ sel = `[data-testid="${esc(testid)}"]`;
287
+ else if (id)
288
+ sel = `#${window.CSS?.escape ? CSS.escape(id) : id}`;
289
+ else if (name)
290
+ sel = `${tag}[name="${esc(name)}"]`;
291
+ else if (role)
292
+ sel = `${tag}[role="${esc(role)}"]`;
293
+ else {
294
+ // No stable attr — count same-tag elements sharing trimmed text.
295
+ const txt = (el.textContent || "").trim();
296
+ if (!txt)
297
+ return 1;
298
+ let n = 0;
299
+ document.querySelectorAll(tag).forEach((c) => {
300
+ if ((c.textContent || "").trim() === txt)
301
+ n++;
302
+ });
303
+ return n;
304
+ }
305
+ try {
306
+ return document.querySelectorAll(sel).length;
307
+ }
308
+ catch {
309
+ return 1;
310
+ }
311
+ };
312
+ const counter = { v: 0 };
313
+ const truncate = (s) => s.length > maxText ? s.slice(0, maxText) + "…" : s;
314
+ const renderNode = (node) => {
315
+ // --- Text node ---
88
316
  if (node.nodeType === Node.TEXT_NODE) {
89
- const text = node.textContent?.trim();
90
- return text ? text : null;
317
+ const t = node.textContent ? node.textContent.trim() : "";
318
+ return t ? truncate(t) : null;
91
319
  }
92
- // 2. סינון אלמנטים לא רלוונטיים
93
- if (node.nodeType !== Node.ELEMENT_NODE &&
94
- !(node instanceof ShadowRoot))
320
+ const isShadow = typeof ShadowRoot !== "undefined" && node instanceof ShadowRoot;
321
+ if (node.nodeType !== Node.ELEMENT_NODE && !isShadow)
95
322
  return null;
96
- const el = node instanceof ShadowRoot
323
+ const el = isShadow
97
324
  ? node.host
98
325
  : node;
99
- const tag = node instanceof ShadowRoot ? "shadow-root" : el.tagName.toLowerCase();
100
- if (["script", "style", "link", "svg"].includes(tag))
326
+ const tag = isShadow ? "shadow-root" : el.tagName.toLowerCase();
327
+ if (SKIP.has(tag))
101
328
  return null;
102
- // 3. איסוף אטריביוטים
329
+ if (!isShadow && isHidden(el))
330
+ return null;
331
+ // --- Gather children: shadow root + light DOM + same-doc iframe ---
332
+ let childNodes = [];
333
+ if (!isShadow && el.shadowRoot) {
334
+ childNodes.push(...Array.from(el.shadowRoot.childNodes));
335
+ }
336
+ childNodes.push(...Array.from(node.childNodes));
337
+ if (tag === "iframe") {
338
+ try {
339
+ const d = el.contentDocument;
340
+ if (d && d.body)
341
+ childNodes.push(...Array.from(d.body.childNodes));
342
+ }
343
+ catch {
344
+ /* cross-origin frame — handled by the >> drill instead */
345
+ }
346
+ }
347
+ // --- Asymmetric scope: K-window siblings around the spine child ---
348
+ // Header rows (containing <th>) are always pinned so table column
349
+ // context survives even when data rows are pruned.
350
+ let siblingTotal = -1;
351
+ if (anchor && spine.has(el) && el !== anchor) {
352
+ const elemKids = childNodes.filter((n) => n.nodeType === Node.ELEMENT_NODE);
353
+ const pivot = elemKids.findIndex((k) => spine.has(k));
354
+ if (pivot >= 0) {
355
+ const keep = new Set();
356
+ let pruned = false;
357
+ elemKids.forEach((k, i) => {
358
+ if (spine.has(k) ||
359
+ (i >= pivot - K && i <= pivot + K) ||
360
+ containsHeader(k))
361
+ keep.add(k);
362
+ else
363
+ pruned = true;
364
+ });
365
+ if (pruned) {
366
+ siblingTotal = elemKids.length;
367
+ childNodes = childNodes.filter((n) => n.nodeType !== Node.ELEMENT_NODE ||
368
+ keep.has(n));
369
+ }
370
+ }
371
+ }
372
+ // Generic wrappers with no semantic signal collapse to their
373
+ // children; transparent roots (body/html) emit children only.
374
+ // Neither emits a tag or consumes an fx-id index.
375
+ const collapses = COLLAPSIBLE.has(tag) && !hasSignal(el);
376
+ if (collapses || isTransparent(tag)) {
377
+ return childNodes.map(renderNode).filter(Boolean).join("") || null;
378
+ }
379
+ // Reserve this element's fx-id BEFORE rendering children so the
380
+ // index reflects document order (parent indexed before its kids).
381
+ const isContainer = CONTAINER.has(tag);
382
+ const fxId = isContainer ? -1 : counter.v++;
383
+ const childHtml = childNodes.map(renderNode).filter(Boolean).join("");
384
+ // --- Build attributes ---
103
385
  const attrs = [];
104
- const preserved = [
105
- "id",
106
- "class",
107
- "data-testid",
108
- "name",
109
- "role",
110
- "src",
111
- "srcdoc",
112
- ];
113
- preserved.forEach((a) => {
386
+ if (fxId >= 0)
387
+ attrs.push(`fx-id="${fxId}"`);
388
+ if (anchor && el === anchor) {
389
+ const n = globalMatches(el);
390
+ if (n > 1)
391
+ attrs.push(`fx-global-matches="${n}"`);
392
+ }
393
+ if (siblingTotal > 0)
394
+ attrs.push(`data-sibling-total="${siblingTotal}"`);
395
+ for (const a of PRESERVED) {
114
396
  const v = el.getAttribute?.(a);
115
- if (v) {
116
- // שומרים מספיק מ-srcdoc כדי שה-AI יזהה מה יש בפנים
117
- const val = a === "srcdoc" ? v.substring(0, 200) : v;
118
- attrs.push(`${a}="${val}"`);
397
+ if (v)
398
+ attrs.push(`${a}="${a === "srcdoc" ? v.substring(0, 200) : v}"`);
399
+ }
400
+ // Image filename fallback: even without alt/title the basename of the
401
+ // src often carries intent (e.g. trash-can-icon.svg).
402
+ if (tag === "img") {
403
+ const src = el.getAttribute("src");
404
+ if (src) {
405
+ const base = src
406
+ .split("?")[0]
407
+ .split("#")[0]
408
+ .split("/")
409
+ .filter(Boolean)
410
+ .pop();
411
+ if (base)
412
+ attrs.push(`data-src-name="${base}"`);
119
413
  }
120
- });
121
- attrs.push(`fx-id="${indexCounter++}"`);
122
- // 4. איסוף ילדים (כולל Shadow DOM)
123
- const childrenArr = [];
124
- if (el instanceof HTMLElement && el.shadowRoot) {
125
- childrenArr.push(...Array.from(el.shadowRoot.childNodes));
126
- }
127
- childrenArr.push(...Array.from(node.childNodes));
128
- // 5. עיבוד ילדים וטקסט פנימי
129
- const childrenHtml = childrenArr
130
- .map(processNode)
131
- .filter(Boolean)
132
- .join("");
133
- // אם אין ילדים אבל יש טקסט (כמו בכפתור), ניקח אותו
134
- const innerContent = childrenHtml || node.textContent?.trim() || "";
135
- const attrStr = attrs.length > 0 ? " " + attrs.join(" ") : "";
136
- return `<${tag}${attrStr}>${innerContent}</${tag}>`;
414
+ }
415
+ const inner = childHtml ||
416
+ (node.textContent ? truncate(node.textContent.trim()) : "");
417
+ const attrStr = attrs.length ? " " + attrs.join(" ") : "";
418
+ return `<${tag}${attrStr}>${inner}</${tag}>`;
137
419
  };
138
420
  const root = document.body || document.documentElement;
139
- return processNode(root) || "";
140
- });
421
+ let out = renderNode(root) || "";
422
+ // Char-budget hard cap (cheap token proxy) — degrade, never overflow.
423
+ if (out.length > budget)
424
+ out = out.slice(0, budget) + "<!--fx-truncated-->";
425
+ return out;
426
+ }, { selector, K: 3, maxText: 200, budget: 25000 });
141
427
  return result;
142
428
  }
143
429
  static getElementDNA = async (page, selector) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sela-core",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "AI self-healing Playwright wrapper - drop-in replacement for @playwright/test",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",