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
package/dist/fixtures/index.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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 === "
|
|
534
|
+
if (prop === "_selaSelector")
|
|
535
535
|
return currentFullSelector;
|
|
536
|
-
if (prop === "
|
|
536
|
+
if (prop === "_selaPage")
|
|
537
537
|
return rawPage;
|
|
538
|
-
if (prop === "
|
|
538
|
+
if (prop === "_selaTestTitle")
|
|
539
539
|
return testTitle;
|
|
540
|
-
if (prop === "
|
|
540
|
+
if (prop === "_selaActionCounter")
|
|
541
541
|
return actionCounter;
|
|
542
|
-
if (prop === "
|
|
542
|
+
if (prop === "_selaElementSelector")
|
|
543
543
|
return currentElementSelector;
|
|
544
|
-
if (prop === "
|
|
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.
|
|
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.
|
|
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
|
-
// ──
|
|
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 !==
|
|
12
|
+
if (!value || typeof value !== "object")
|
|
13
13
|
return null;
|
|
14
|
-
//
|
|
15
|
-
const proxied = value.
|
|
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 ===
|
|
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(
|
|
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.
|
|
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 "
|
|
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":"AAgBA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAQ/C,OAAO,EAAE,cAAc,EAAgB,MAAM,mBAAmB,CAAC;AAOjE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|