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
@@ -34,7 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.createHealingExpect = exports.test = void 0;
37
- exports.resolveFixwrightProxy = resolveFixwrightProxy;
37
+ exports.resolveSelaProxy = resolveSelaProxy;
38
38
  exports.createFrameLocatorProxy = createFrameLocatorProxy;
39
39
  exports.createLocatorProxy = createLocatorProxy;
40
40
  const test_1 = require("@playwright/test");
@@ -47,10 +47,10 @@ const fs = __importStar(require("fs"));
47
47
  const path = __importStar(require("path"));
48
48
  const registry = HealingRegistry_1.HealingRegistry.getInstance();
49
49
  const proxyToActiveLocator = new WeakMap();
50
- // Module-level resolver - moduleExpect.ts uses this to unwrap a Fixwright
50
+ // Module-level resolver - moduleExpect.ts uses this to unwrap a Sela
51
51
  // locator proxy back to its live underlying Playwright Locator without
52
52
  // needing access to the WeakMap directly.
53
- function resolveFixwrightProxy(value) {
53
+ function resolveSelaProxy(value) {
54
54
  if (!value || typeof value !== "object")
55
55
  return null;
56
56
  const getActive = proxyToActiveLocator.get(value);
@@ -475,7 +475,7 @@ function createFrameLocatorProxy(rawFrameLocator, frameSelector, rawPage, testTi
475
475
  // born after a heal has already broadcast).
476
476
  // 2. Subscribes to registry so any future broadcast swaps
477
477
  // activeLocator on the same tick - before the next await.
478
- // 3. Exposes _fixwrightUnsubscribe so the fixture can clean up.
478
+ // 3. Exposes _selaUnsubscribe so the fixture can clean up.
479
479
  // ─────────────────────────────────────────────────────────────────
480
480
  function createLocatorProxy(rawLocator, selector, rawPage, testTitle, actionCounter, elementSelectorOnly) {
481
481
  // Defensive: Proxy ctor throws "Cannot create proxy with a non-object as target"
@@ -531,17 +531,17 @@ function createLocatorProxy(rawLocator, selector, rawPage, testTitle, actionCoun
531
531
  if (typeof prop !== "string")
532
532
  return Reflect.get(target, prop, receiver);
533
533
  // ── Internal accessors ───────────────────────────────────
534
- if (prop === "_fixwrightSelector")
534
+ if (prop === "_selaSelector")
535
535
  return currentFullSelector;
536
- if (prop === "_fixwrightPage")
536
+ if (prop === "_selaPage")
537
537
  return rawPage;
538
- if (prop === "_fixwrightTestTitle")
538
+ if (prop === "_selaTestTitle")
539
539
  return testTitle;
540
- if (prop === "_fixwrightActionCounter")
540
+ if (prop === "_selaActionCounter")
541
541
  return actionCounter;
542
- if (prop === "_fixwrightElementSelector")
542
+ if (prop === "_selaElementSelector")
543
543
  return currentElementSelector;
544
- if (prop === "_fixwrightUnsubscribe")
544
+ if (prop === "_selaUnsubscribe")
545
545
  return unsubscribe;
546
546
  // ── getBy* ───────────────────────────────────────────────
547
547
  if (prop in GET_BY_METHODS) {
@@ -661,6 +661,21 @@ function createLocatorProxy(rawLocator, selector, rawPage, testTitle, actionCoun
661
661
  // ─────────────────────────────────────────────────────────────────
662
662
  // ─── expect - healing proxy replacing the plain baseExpect proxy ──────────────
663
663
  exports.test = test_1.test.extend({
664
+ // ── Worker-scoped flush anchor (auto) ───────────────────────────────
665
+ // Setup is a no-op; the teardown after `use()` is the suite-end flush.
666
+ // Aggregation spans the ENTIRE worker run because commitUpdates() is no
667
+ // longer called per test - the report buffer, registry heal-map, and
668
+ // workspace snapshot accumulate across all tests and flush once here.
669
+ _selaWorkerLifecycle: [
670
+ async ({}, use) => {
671
+ await use();
672
+ // Single suite-wide commit: saves DNA snapshots, flushes the Insights
673
+ // report + .sela-history.json ONCE, and writes the DX-review ledger
674
+ // with the full set of healed events from the whole worker run.
675
+ await singleton_1.sharedEngine.commitUpdates();
676
+ },
677
+ { scope: "worker", auto: true },
678
+ ],
664
679
  page: async ({ page }, use, testInfo) => {
665
680
  page.on("console", (msg) => {
666
681
  if (msg.text().includes("[DOM-EXTRACT]")) {
@@ -683,7 +698,7 @@ exports.test = test_1.test.extend({
683
698
  if (prop === "locator") {
684
699
  return (selector) => {
685
700
  const p = createLocatorProxy(target.locator(selector), selector, rawPage, testInfo.title, actionCounter, selector);
686
- const u = p._fixwrightUnsubscribe;
701
+ const u = p._selaUnsubscribe;
687
702
  if (u)
688
703
  unsubscribers.push(u);
689
704
  return p;
@@ -696,7 +711,7 @@ exports.test = test_1.test.extend({
696
711
  return (...args) => {
697
712
  const semanticSelector = GET_BY_METHODS[prop](args);
698
713
  const p = createLocatorProxy(target[prop](...args), semanticSelector, rawPage, testInfo.title, actionCounter, semanticSelector);
699
- const u = p._fixwrightUnsubscribe;
714
+ const u = p._selaUnsubscribe;
700
715
  if (u)
701
716
  unsubscribers.push(u);
702
717
  return p;
@@ -706,11 +721,16 @@ exports.test = test_1.test.extend({
706
721
  },
707
722
  });
708
723
  await use(pageProxy);
709
- // ── Teardown ─────────────────────────────────────────────────
724
+ // ── Per-test teardown ─────────────────────────────────────────
725
+ // Release this test's proxy registry subscriptions and drop per-test
726
+ // listeners. We deliberately DO NOT call engine.commitUpdates() here -
727
+ // flushing per test is what produced the per-test report writes, the
728
+ // duplicated .sela-history.json entries (each "healed":1), and the
729
+ // last-test-only DX diff. The suite-wide flush lives in the
730
+ // worker-scoped `_selaWorkerLifecycle` teardown above.
710
731
  for (const u of unsubscribers)
711
732
  u();
712
733
  registry.clearForTest();
713
- await singleton_1.sharedEngine.commitUpdates();
714
734
  },
715
735
  expect: async ({ page }, use, testInfo) => {
716
736
  const healingExpect = (0, expectProxy_1.createHealingExpect)(singleton_1.sharedEngine, page, testInfo.file, 0,
@@ -9,14 +9,14 @@ const singleton_js_1 = require("../engine/singleton.js");
9
9
  const expectProxy_js_1 = require("./expectProxy.js");
10
10
  const index_js_1 = require("./index.js");
11
11
  function extractPage(value) {
12
- if (!value || typeof value !== 'object')
12
+ if (!value || typeof value !== "object")
13
13
  return null;
14
- // Fixwright locator proxy exposes _fixwrightPage directly
15
- const proxied = value._fixwrightPage;
14
+ // Sela locator proxy exposes _selaPage directly
15
+ const proxied = value._selaPage;
16
16
  if (proxied)
17
17
  return proxied;
18
18
  // Playwright public API: Locator.page()
19
- if (typeof value.page === 'function') {
19
+ if (typeof value.page === "function") {
20
20
  try {
21
21
  return value.page();
22
22
  }
@@ -27,7 +27,7 @@ function extractPage(value) {
27
27
  return null;
28
28
  }
29
29
  function getCallerFile() {
30
- const lines = new Error().stack?.split('\n') ?? [];
30
+ const lines = new Error().stack?.split("\n") ?? [];
31
31
  // Match BOTH frame shapes V8 emits:
32
32
  // " at fnName (D:\\path\\file.spec.ts:17:20)"
33
33
  // " at D:\\path\\file.spec.ts:17:20"
@@ -42,13 +42,13 @@ function getCallerFile() {
42
42
  if (m && m[1])
43
43
  return m[1].trim();
44
44
  }
45
- return '';
45
+ return "";
46
46
  }
47
47
  function expect(value) {
48
48
  const page = extractPage(value);
49
49
  if (page) {
50
50
  const filePath = getCallerFile();
51
- return (0, expectProxy_js_1.createHealingExpect)(singleton_js_1.sharedEngine, page, filePath, 0, index_js_1.resolveFixwrightProxy)(value);
51
+ return (0, expectProxy_js_1.createHealingExpect)(singleton_js_1.sharedEngine, page, filePath, 0, index_js_1.resolveSelaProxy)(value);
52
52
  }
53
53
  return (0, test_1.expect)(value);
54
54
  }
@@ -6,7 +6,7 @@
6
6
  // Every Sela-created Proxy (locator, frame-locator, page, expect) honors a
7
7
  // `get` trap on this symbol and returns the kind string. Consumers - the
8
8
  // surface-diff test, internal helpers, debuggers - can identify Sela
9
- // proxies without touching the brittle "_fixwrightSelector" heuristic.
9
+ // proxies without touching the brittle "_selaSelector" heuristic.
10
10
  //
11
11
  // We use Symbol.for() (a registered symbol) so the identity survives
12
12
  // across module boundaries when multiple bundles end up in the same
@@ -1,6 +1,9 @@
1
1
  import { SelectorSegment } from "./LLMService";
2
2
  import { AdvisoryBuffer } from "./HealingAdvisory";
3
3
  import type { SmartChainSegment } from "./LLMService";
4
+ import { TraceOutcome } from "./TraceBackEngine";
5
+ import { Decision } from "./DecisionEngine";
6
+ import { FileBatch, ApplyOutcome, ApplyOptions } from "./MutationApplier";
4
7
  export interface ASTUpdateResult {
5
8
  success: boolean;
6
9
  reason: string;
@@ -21,10 +24,51 @@ interface CallerContext {
21
24
  filePath: string;
22
25
  line: number;
23
26
  }
27
+ export interface ComposeHealInput {
28
+ filePath: string;
29
+ /** 1-based line of the failing call-site. */
30
+ line: number;
31
+ /** 0-based column; when omitted the first non-whitespace char of the line is used. */
32
+ column?: number;
33
+ /**
34
+ * Healed locator EXPRESSION text, e.g. `page.locator(".btn-v2")`. When
35
+ * omitted, it is BUILT from `newSelector` + the traced target's root
36
+ * receiver (internal selectors → idiomatic method form).
37
+ */
38
+ newLocatorExpr?: string;
39
+ /** Raw healed selector string (may be an `internal:` engine selector). */
40
+ newSelector?: string;
41
+ /** Runtime: only the failing test needs the new selector. */
42
+ contextSpecific?: boolean;
43
+ /** Runtime: selector rot is global across all consumers. */
44
+ globalCorrect?: boolean;
45
+ /** When true, write the result via MutationApplier (with the gate). */
46
+ apply?: boolean;
47
+ applyOptions?: ApplyOptions;
48
+ }
49
+ export interface ComposeHealResult {
50
+ kind: "decision" | "abort";
51
+ trace: TraceOutcome;
52
+ decision?: Decision;
53
+ batch?: FileBatch;
54
+ outcome?: ApplyOutcome;
55
+ /** 1-based line of the mutated target node (for report/diff). */
56
+ writtenLine?: number;
57
+ }
24
58
  export declare class ASTSourceUpdater {
25
59
  private project;
26
60
  readonly advisoryBuffer: AdvisoryBuffer;
27
61
  flushAdvisories(): void;
62
+ /**
63
+ * Trace the failing call-site back to its source declaration, decide the
64
+ * mutation strategy (in-place / fork / prompt), and — when `apply` is set —
65
+ * write it to disk through the type-safety gate.
66
+ */
67
+ static composeHeal(input: ComposeHealInput): ComposeHealResult;
68
+ /** Drain-loop entry: apply pre-built batches through the write gate. */
69
+ static applyMutationBatches(batches: FileBatch[], opts?: ApplyOptions): ApplyOutcome[];
70
+ /** Convert a DecisionEngine result into an anchored MutationApplier batch. */
71
+ static decisionToBatch(filePath: string, decision: Decision): FileBatch;
28
72
  /**
29
73
  * Centralised write hook for every ts-morph `saveSync` call in this class.
30
74
  * When `SELA_DRY_RUN` is active the AST mutation stays in memory but the
@@ -86,6 +130,7 @@ export declare class ASTSourceUpdater {
86
130
  private lineToOffset;
87
131
  resolveFilePath(raw: string): string | null;
88
132
  private semanticDispatch;
133
+ private applyDefinitionSiteHeal;
89
134
  private handleIdentifierArg;
90
135
  private handlePropertyAccessArg;
91
136
  private handleTemplateLiteralArg;
@@ -1 +1 @@
1
- {"version":3,"file":"ASTSourceUpdater.d.ts","sourceRoot":"","sources":["../../src/services/ASTSourceUpdater.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ASTSourceUpdater.d.ts","sourceRoot":"","sources":["../../src/services/ASTSourceUpdater.ts"],"names":[],"mappings":"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"}
@@ -49,6 +49,10 @@ const InitializerUpdater_1 = require("./InitializerUpdater");
49
49
  const CrossFileHealer_1 = require("./CrossFileHealer");
50
50
  const HealingAdvisory_1 = require("./HealingAdvisory");
51
51
  const ChainValidator_1 = require("./ChainValidator");
52
+ const TraceBackEngine_1 = require("./TraceBackEngine");
53
+ const DecisionEngine_1 = require("./DecisionEngine");
54
+ const MutationApplier_1 = require("./MutationApplier");
55
+ const SelectorSanitizer_1 = require("./SelectorSanitizer");
52
56
  // ═══════════════════════════════════════════════════════════════════
53
57
  // FIX #1 - normalizeSelector utility
54
58
  // ═══════════════════════════════════════════════════════════════════
@@ -1553,15 +1557,141 @@ function findAssertionsInWindow(sourceFile, locatorVarName, callerLine, windowLi
1553
1557
  }
1554
1558
  return results;
1555
1559
  }
1556
- // ═══════════════════════════════════════════════════════════════════
1557
- // MAIN CLASS
1558
- // ═══════════════════════════════════════════════════════════════════
1560
+ /**
1561
+ * Build a locator EXPRESSION from a raw healed selector + chain root.
1562
+ * Internal engine selectors become their idiomatic method form
1563
+ * (`page.getByRole(...)`); otherwise a `root.locator("selector")` call.
1564
+ */
1565
+ function buildLocatorExpr(rawSelector, rootReceiver) {
1566
+ const root = rootReceiver || "page";
1567
+ if (!rawSelector)
1568
+ return `${root}.locator("")`;
1569
+ if ((0, SelectorSanitizer_1.isInternalEngineSelector)(rawSelector)) {
1570
+ const method = (0, SelectorSanitizer_1.internalToMethodExpression)(rawSelector, root);
1571
+ if (method)
1572
+ return method;
1573
+ const publicStr = (0, SelectorSanitizer_1.internalToPublicSelectorString)(rawSelector);
1574
+ return `${root}.locator(${JSON.stringify(publicStr ?? rawSelector)})`;
1575
+ }
1576
+ return `${root}.locator(${JSON.stringify(rawSelector)})`;
1577
+ }
1578
+ /** 0-based column of the first non-whitespace char on a 1-based line. */
1579
+ function firstNonWhitespaceColumn(text, line1Based) {
1580
+ const lines = text.split(/\r?\n/);
1581
+ const idx = line1Based - 1;
1582
+ if (idx < 0 || idx >= lines.length)
1583
+ return 0;
1584
+ const m = lines[idx].match(/^(\s*)/);
1585
+ return m ? m[1].length : 0;
1586
+ }
1559
1587
  class ASTSourceUpdater {
1560
1588
  project;
1561
1589
  advisoryBuffer = new HealingAdvisory_1.AdvisoryBuffer();
1562
1590
  flushAdvisories() {
1563
1591
  this.advisoryBuffer.flush();
1564
1592
  }
1593
+ // ═══════════════════════════════════════════════════════════════════
1594
+ // STAGE 4 — end-to-end trace → decide → apply (Stages 1-3 wired in)
1595
+ //
1596
+ // Additive entry point. The legacy heal paths below are untouched; this
1597
+ // is the new write-disciplined pipeline that composes AnchorResolver,
1598
+ // TraceBackEngine, DecisionEngine and MutationApplier.
1599
+ // ═══════════════════════════════════════════════════════════════════
1600
+ /**
1601
+ * Trace the failing call-site back to its source declaration, decide the
1602
+ * mutation strategy (in-place / fork / prompt), and — when `apply` is set —
1603
+ * write it to disk through the type-safety gate.
1604
+ */
1605
+ static composeHeal(input) {
1606
+ const original = fs.readFileSync(input.filePath, "utf8");
1607
+ const project = new ts_morph_1.Project({
1608
+ useInMemoryFileSystem: true,
1609
+ compilerOptions: {
1610
+ allowJs: true,
1611
+ noEmit: true,
1612
+ skipLibCheck: true,
1613
+ noResolve: true,
1614
+ },
1615
+ });
1616
+ const sf = project.createSourceFile(input.filePath, original, {
1617
+ overwrite: true,
1618
+ });
1619
+ // Resolve a robust position: when no explicit column is given, use the
1620
+ // first non-whitespace char of the line (avoids landing on indentation).
1621
+ const column = input.column ?? firstNonWhitespaceColumn(original, input.line);
1622
+ const trace = TraceBackEngine_1.TraceBackEngine.traceFromPosition(sf, input.line, column);
1623
+ if (trace.status !== "target") {
1624
+ return { kind: "abort", trace };
1625
+ }
1626
+ // Re-resolve the node at the failing position for fork test-block lookup.
1627
+ let failingNode;
1628
+ try {
1629
+ failingNode = sf.getDescendantAtPos(sf.compilerNode.getPositionOfLineAndCharacter(input.line - 1, column));
1630
+ }
1631
+ catch {
1632
+ failingNode = undefined;
1633
+ }
1634
+ if (!failingNode)
1635
+ failingNode = trace.target.targetNode;
1636
+ // Build the replacement EXPRESSION. Explicit newLocatorExpr wins; else
1637
+ // synthesise it from the raw healed selector + the traced root receiver,
1638
+ // translating internal engine selectors to their idiomatic method form.
1639
+ const newLocatorExpr = input.newLocatorExpr ??
1640
+ buildLocatorExpr(input.newSelector ?? "", trace.target.rootReceiver);
1641
+ const decision = DecisionEngine_1.DecisionEngine.decide({
1642
+ target: trace.target,
1643
+ failingCallSite: failingNode,
1644
+ newLocatorExpr,
1645
+ contextSpecific: input.contextSpecific ?? false,
1646
+ globalCorrect: input.globalCorrect ?? false,
1647
+ });
1648
+ const batch = ASTSourceUpdater.decisionToBatch(input.filePath, decision);
1649
+ const result = {
1650
+ kind: "decision",
1651
+ trace,
1652
+ decision,
1653
+ batch,
1654
+ writtenLine: trace.target.targetNode.getStartLineNumber(),
1655
+ };
1656
+ if (input.apply && batch.edits.length > 0) {
1657
+ result.outcome = MutationApplier_1.MutationApplier.applyFileBatch(batch, input.applyOptions ?? {});
1658
+ }
1659
+ return result;
1660
+ }
1661
+ /** Drain-loop entry: apply pre-built batches through the write gate. */
1662
+ static applyMutationBatches(batches, opts = {}) {
1663
+ return MutationApplier_1.MutationApplier.applyBatches(batches, opts);
1664
+ }
1665
+ /** Convert a DecisionEngine result into an anchored MutationApplier batch. */
1666
+ static decisionToBatch(filePath, decision) {
1667
+ const edits = decision.edits.map((e) => {
1668
+ if (e.kind === "insert") {
1669
+ return {
1670
+ kind: "insert",
1671
+ replacement: e.replacement,
1672
+ span: e.span,
1673
+ requireLocator: false,
1674
+ };
1675
+ }
1676
+ // MUTATE_IN_PLACE replace → anchor-resolved + Locator-type-gated.
1677
+ if (decision.strategy === "MUTATE_IN_PLACE") {
1678
+ return {
1679
+ kind: "replace",
1680
+ replacement: e.replacement,
1681
+ anchor: decision.anchor,
1682
+ requireLocator: true,
1683
+ };
1684
+ }
1685
+ // FORK call-site receiver rewrite → identifier, not a locator.
1686
+ return {
1687
+ kind: "replace",
1688
+ replacement: e.replacement,
1689
+ span: e.span,
1690
+ requireLocator: false,
1691
+ };
1692
+ });
1693
+ return { filePath, edits };
1694
+ }
1565
1695
  /**
1566
1696
  * Centralised write hook for every ts-morph `saveSync` call in this class.
1567
1697
  * When `SELA_DRY_RUN` is active the AST mutation stays in memory but the
@@ -3012,6 +3142,72 @@ class ASTSourceUpdater {
3012
3142
  return null;
3013
3143
  }
3014
3144
  }
3145
+ // ═══════════════════════════════════════════════════════════════════
3146
+ // DEFINITION-SITE WRITE (with internal→public Method-Shift fallback)
3147
+ //
3148
+ // A `const X = "selector"` consumed by `.locator(X)` cannot hold a semantic
3149
+ // locator (page.getByRole(...) would reference `page` out of scope at the
3150
+ // declaration). So when the AI heals to an internal engine selector
3151
+ // (`internal:role=…`), we DON'T abort and we DON'T leak the engine string:
3152
+ // we translate it to a VALID PUBLIC selector string (`role=button[name="…"]`)
3153
+ // and write THAT into the constant — keeping the developer's architecture
3154
+ // intact — while surfacing the cleaner semantic method form (getByRole) as
3155
+ // a suggested upgrade in the advisory/report.
3156
+ // ═══════════════════════════════════════════════════════════════════
3157
+ applyDefinitionSiteHeal(targetNode, targetFile, newSelector, strategy, blastRadius) {
3158
+ const wasInternal = (0, SelectorSanitizer_1.isInternalEngineSelector)(newSelector);
3159
+ let writeValue = newSelector;
3160
+ if (wasInternal) {
3161
+ const publicStr = (0, SelectorSanitizer_1.internalToPublicSelectorString)(newSelector);
3162
+ if (!publicStr) {
3163
+ // No public string engine (e.g. label association) → a .locator()
3164
+ // string constant genuinely cannot express it. Preserve the call site.
3165
+ return {
3166
+ success: false,
3167
+ reason: `semantic guard: internal selector "${newSelector.slice(0, 48)}" has no public ` +
3168
+ `.locator() string form - call site preserved (method-shift required)`,
3169
+ strategy: "semantic-guard",
3170
+ };
3171
+ }
3172
+ writeValue = publicStr;
3173
+ console.log(`[SemanticDispatch] 🔁 Method-Shift fallback: internal selector → public string "${writeValue}"`);
3174
+ }
3175
+ const result = InitializerUpdater_1.InitializerUpdater.apply(targetNode, targetFile, writeValue);
3176
+ if (!result)
3177
+ return null;
3178
+ if (!this.validateFileSyntax(targetFile)) {
3179
+ return {
3180
+ success: false,
3181
+ reason: "post-mutation syntax validation failed - reverted",
3182
+ strategy,
3183
+ };
3184
+ }
3185
+ if (wasInternal) {
3186
+ const methodExpr = (0, SelectorSanitizer_1.internalToMethodExpression)(newSelector, "page");
3187
+ this.advisoryBuffer.push({
3188
+ code: HealingAdvisory_1.AdvisoryCode.SEMANTIC_UPGRADE_SUGGESTED,
3189
+ message: `Healed constant to public selector string "${writeValue}". ` +
3190
+ `Cleaner semantic form: ${methodExpr ?? "a getBy* method"}.`,
3191
+ affectedFile: targetFile.getFilePath(),
3192
+ affectedLine: result.lineUpdated,
3193
+ suggestedAction: `For more resilient code, rewrite the call site to ${methodExpr ?? "the semantic locator"} ` +
3194
+ `instead of the string constant.`,
3195
+ });
3196
+ }
3197
+ return {
3198
+ success: true,
3199
+ reason: `definition-site heal via ${strategy} trace` +
3200
+ (wasInternal ? " (internal→public string; semantic upgrade suggested)" : ""),
3201
+ strategy,
3202
+ lineUpdated: result.lineUpdated,
3203
+ healedLocatorString: result.healedLocatorString,
3204
+ definitionSite: {
3205
+ file: targetFile.getFilePath(),
3206
+ line: result.lineUpdated,
3207
+ },
3208
+ blastRadius,
3209
+ };
3210
+ }
3015
3211
  handleIdentifierArg(node, sourceFile, caller, newSelector) {
3016
3212
  const traceResult = DefinitionTracer_1.DefinitionTracer.traceIdentifier(node, sourceFile, caller.line);
3017
3213
  if (traceResult.found) {
@@ -3043,28 +3239,7 @@ class ASTSourceUpdater {
3043
3239
  suggestedAction: "Run the full test suite to confirm all references still pass.",
3044
3240
  });
3045
3241
  }
3046
- const result = InitializerUpdater_1.InitializerUpdater.apply(targetNode, targetFile, newSelector);
3047
- if (!result)
3048
- return null;
3049
- if (!this.validateFileSyntax(targetFile)) {
3050
- return {
3051
- success: false,
3052
- reason: "post-mutation syntax validation failed - reverted",
3053
- strategy: "semantic-identifier",
3054
- };
3055
- }
3056
- return {
3057
- success: true,
3058
- reason: `definition-site heal via identifier trace`,
3059
- strategy: "semantic-identifier",
3060
- lineUpdated: result.lineUpdated,
3061
- healedLocatorString: result.healedLocatorString,
3062
- definitionSite: {
3063
- file: targetFile.getFilePath(),
3064
- line: result.lineUpdated,
3065
- },
3066
- blastRadius: blast.testFunctionCount,
3067
- };
3242
+ return this.applyDefinitionSiteHeal(targetNode, targetFile, newSelector, "semantic-identifier", blast.testFunctionCount);
3068
3243
  }
3069
3244
  if (!traceResult.found && traceResult.crossFile) {
3070
3245
  const xResult = CrossFileHealer_1.CrossFileHealer.heal(traceResult.modulePath, traceResult.symbolName, newSelector, caller.filePath, this.project);
@@ -3111,28 +3286,7 @@ class ASTSourceUpdater {
3111
3286
  suggestedAction: "Run the full test suite to confirm all references still pass.",
3112
3287
  });
3113
3288
  }
3114
- const result = InitializerUpdater_1.InitializerUpdater.apply(targetNode, targetFile, newSelector);
3115
- if (!result)
3116
- return null;
3117
- if (!this.validateFileSyntax(targetFile)) {
3118
- return {
3119
- success: false,
3120
- reason: "post-mutation syntax validation failed - reverted",
3121
- strategy: "semantic-property-access",
3122
- };
3123
- }
3124
- return {
3125
- success: true,
3126
- reason: `definition-site heal via property access trace`,
3127
- strategy: "semantic-property-access",
3128
- lineUpdated: result.lineUpdated,
3129
- healedLocatorString: result.healedLocatorString,
3130
- definitionSite: {
3131
- file: targetFile.getFilePath(),
3132
- line: result.lineUpdated,
3133
- },
3134
- blastRadius: blast.testFunctionCount,
3135
- };
3289
+ return this.applyDefinitionSiteHeal(targetNode, targetFile, newSelector, "semantic-property-access", blast.testFunctionCount);
3136
3290
  }
3137
3291
  if (!traceResult.found && traceResult.crossFile) {
3138
3292
  const xResult = CrossFileHealer_1.CrossFileHealer.heal(traceResult.modulePath, traceResult.symbolName, newSelector, caller.filePath, this.project);