sela-core 1.0.7 → 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/ErrorHandler.d.ts +1 -1
- package/dist/cli/ErrorHandler.js +14 -14
- package/dist/cli/commands/bulk.d.ts +1 -1
- package/dist/cli/commands/bulk.d.ts.map +1 -1
- package/dist/cli/commands/bulk.js +51 -33
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +26 -18
- 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/commands/showReport.d.ts +1 -1
- package/dist/cli/commands/showReport.d.ts.map +1 -1
- package/dist/cli/commands/showReport.js +12 -10
- package/dist/cli/commands/status.d.ts +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +8 -8
- package/dist/cli/commands/sync.d.ts +1 -1
- package/dist/cli/commands/sync.d.ts.map +1 -1
- package/dist/cli/commands/sync.js +38 -30
- package/dist/cli/index.js +12 -9
- package/dist/cli/ui/DnaTable.d.ts +1 -1
- package/dist/cli/ui/DnaTable.d.ts.map +1 -1
- package/dist/cli/ui/DnaTable.js +23 -16
- package/dist/cli/ui/RefactorWizard.d.ts +3 -3
- package/dist/cli/ui/RefactorWizard.d.ts.map +1 -1
- package/dist/cli/ui/RefactorWizard.js +108 -82
- package/dist/config/ConfigLoader.d.ts +2 -2
- package/dist/config/ConfigLoader.d.ts.map +1 -1
- package/dist/config/ConfigLoader.js +15 -15
- package/dist/config/DryRunGuard.d.ts +1 -1
- package/dist/config/DryRunGuard.js +7 -7
- package/dist/config/SelaConfig.d.ts +13 -17
- package/dist/config/SelaConfig.d.ts.map +1 -1
- package/dist/config/SelaConfig.js +32 -26
- package/dist/engine/HealingRegistry.d.ts +1 -1
- package/dist/engine/HealingRegistry.js +3 -3
- package/dist/engine/SelaEngine.d.ts +1 -1
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +117 -51
- package/dist/errors/SelaError.d.ts +8 -1
- package/dist/errors/SelaError.d.ts.map +1 -1
- package/dist/errors/SelaError.js +14 -7
- package/dist/fixtures/expectProxy.d.ts +2 -2
- package/dist/fixtures/expectProxy.js +16 -16
- package/dist/fixtures/index.d.ts +18 -2
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +47 -27
- package/dist/fixtures/moduleExpect.js +7 -7
- package/dist/fixtures/proxyTag.d.ts +1 -1
- package/dist/fixtures/proxyTag.js +4 -4
- package/dist/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/reporter/SelaReporter.d.ts +45 -12
- package/dist/reporter/SelaReporter.d.ts.map +1 -1
- package/dist/reporter/SelaReporter.js +139 -39
- package/dist/services/ASTSourceUpdater.d.ts +48 -3
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
- package/dist/services/ASTSourceUpdater.js +283 -105
- 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/ArgumentTypeAnalyzer.js +7 -7
- package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -1
- package/dist/services/BlastRadiusAnalyzer.js +5 -2
- package/dist/services/ChainValidator.js +22 -22
- package/dist/services/CrossFileHealer.js +7 -7
- 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/DefinitionTracer.d.ts +1 -1
- package/dist/services/DefinitionTracer.d.ts.map +1 -1
- package/dist/services/DefinitionTracer.js +28 -13
- package/dist/services/DnaEditorService.d.ts +2 -2
- package/dist/services/DnaEditorService.d.ts.map +1 -1
- package/dist/services/DnaEditorService.js +81 -62
- package/dist/services/DnaIndexService.d.ts +1 -1
- package/dist/services/DnaIndexService.d.ts.map +1 -1
- package/dist/services/DnaIndexService.js +14 -14
- package/dist/services/HealReportService.d.ts +31 -55
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +864 -1240
- package/dist/services/HealingAdvisory.d.ts +9 -1
- package/dist/services/HealingAdvisory.d.ts.map +1 -1
- package/dist/services/HealingAdvisory.js +9 -1
- package/dist/services/HealingCacheService.d.ts +8 -8
- package/dist/services/HealingCacheService.d.ts.map +1 -1
- package/dist/services/HealingCacheService.js +24 -24
- package/dist/services/HealthReportService.d.ts +1 -1
- package/dist/services/HealthReportService.d.ts.map +1 -1
- package/dist/services/HealthReportService.js +25 -25
- package/dist/services/InitializerUpdater.d.ts.map +1 -1
- package/dist/services/InitializerUpdater.js +12 -2
- package/dist/services/IntentAuditor.d.ts.map +1 -1
- package/dist/services/IntentAuditor.js +32 -21
- package/dist/services/InteractiveReview.d.ts +8 -17
- package/dist/services/InteractiveReview.d.ts.map +1 -1
- package/dist/services/InteractiveReview.js +34 -124
- package/dist/services/LLMService.d.ts +1 -1
- package/dist/services/LLMService.js +16 -16
- 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/PRAutomationService.d.ts +2 -2
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +43 -39
- package/dist/services/PendingPromptLedger.d.ts +9 -2
- package/dist/services/PendingPromptLedger.d.ts.map +1 -1
- package/dist/services/PendingPromptLedger.js +31 -6
- package/dist/services/ReportGenerator.d.ts +116 -30
- package/dist/services/ReportGenerator.d.ts.map +1 -1
- package/dist/services/ReportGenerator.js +149 -62
- 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 +4 -7
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SafetyGuard.js +29 -106
- 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 +6 -6
- package/dist/services/SourceLinkService.d.ts +2 -2
- package/dist/services/SourceLinkService.d.ts.map +1 -1
- package/dist/services/SourceLinkService.js +31 -22
- package/dist/services/SourceUpdater.d.ts +1 -1
- package/dist/services/SourceUpdater.d.ts.map +1 -1
- package/dist/services/SourceUpdater.js +44 -26
- package/dist/services/TemplateDiffService.d.ts.map +1 -1
- package/dist/services/TemplateDiffService.js +18 -15
- 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/services/WorkspaceSnapshotService.d.ts +6 -6
- package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -1
- package/dist/services/WorkspaceSnapshotService.js +10 -12
- package/dist/types/index.d.ts +1 -1
- 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/dist/utils/IsolatedDiff.d.ts +2 -2
- package/dist/utils/IsolatedDiff.d.ts.map +1 -1
- package/dist/utils/IsolatedDiff.js +13 -10
- package/package.json +2 -2
|
@@ -49,8 +49,12 @@ 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
|
-
// FIX #1
|
|
57
|
+
// FIX #1 - normalizeSelector utility
|
|
54
58
|
// ═══════════════════════════════════════════════════════════════════
|
|
55
59
|
function normalizeSelector(s) {
|
|
56
60
|
return s
|
|
@@ -70,7 +74,7 @@ function normalizeSelector(s) {
|
|
|
70
74
|
// of "varName", or null otherwise.
|
|
71
75
|
//
|
|
72
76
|
// Why this matters:
|
|
73
|
-
// const shadowBtn = shadowBtn.getByRole(…) ← CIRCULAR
|
|
77
|
+
// const shadowBtn = shadowBtn.getByRole(…) ← CIRCULAR - must be caught
|
|
74
78
|
// const shadowBtn = page.getByRole(…) ← CORRECT
|
|
75
79
|
//
|
|
76
80
|
// We walk UP the ancestor chain from the call node looking for a
|
|
@@ -83,7 +87,7 @@ function getEnclosingVariableDeclaration(node) {
|
|
|
83
87
|
if (ts_morph_1.Node.isVariableDeclaration(current)) {
|
|
84
88
|
return current;
|
|
85
89
|
}
|
|
86
|
-
// Stop climbing past statement boundaries
|
|
90
|
+
// Stop climbing past statement boundaries - we only want the immediate
|
|
87
91
|
// enclosing declaration, not a declaration somewhere further up the tree.
|
|
88
92
|
if (ts_morph_1.Node.isBlock(current) ||
|
|
89
93
|
ts_morph_1.Node.isSourceFile(current) ||
|
|
@@ -121,7 +125,7 @@ function isInsideOwnInitializer(callNode, varName) {
|
|
|
121
125
|
function resolveSafeReceiver(proposedReceiver, rootReceiver, callNode) {
|
|
122
126
|
if (isInsideOwnInitializer(callNode, proposedReceiver)) {
|
|
123
127
|
console.log(`[ScopeGuard] 🔒 Circular reference prevented: ` +
|
|
124
|
-
`"${proposedReceiver}" is being initialized
|
|
128
|
+
`"${proposedReceiver}" is being initialized - ` +
|
|
125
129
|
`falling back to root receiver "${rootReceiver}"`);
|
|
126
130
|
return rootReceiver;
|
|
127
131
|
}
|
|
@@ -554,7 +558,7 @@ class MethodMapper {
|
|
|
554
558
|
for (const seg of segments) {
|
|
555
559
|
const callText = MethodMapper.build(currentReceiver, seg);
|
|
556
560
|
if (callText === null) {
|
|
557
|
-
console.warn(`[MethodMapper] Cannot build segment type "${seg.type}"
|
|
561
|
+
console.warn(`[MethodMapper] Cannot build segment type "${seg.type}" - aborting chain`);
|
|
558
562
|
return null;
|
|
559
563
|
}
|
|
560
564
|
const methodPart = callText.slice(currentReceiver.length);
|
|
@@ -617,7 +621,7 @@ function deduplicateChainSegments(aiSegments, contextSegments) {
|
|
|
617
621
|
if (overlap === 0)
|
|
618
622
|
return { deduped: aiSegments, stripped: 0 };
|
|
619
623
|
console.log(`[ChainDedup] ✂️ Stripping ${overlap}/${contextSegments.length} duplicate leading ` +
|
|
620
|
-
`segment(s)
|
|
624
|
+
`segment(s) - context already encodes them`);
|
|
621
625
|
return { deduped: aiSegments.slice(overlap), stripped: overlap };
|
|
622
626
|
}
|
|
623
627
|
const PAGE_ROOT_NAMES = new Set([
|
|
@@ -731,15 +735,15 @@ function resolveVariableToSegments(sourceFile, varName, beforeLine, depth = 0) {
|
|
|
731
735
|
// Module-level write gate shared between `ASTSourceUpdater` and the
|
|
732
736
|
// internal `NodeTransformationStrategy`. Centralises three concerns
|
|
733
737
|
// every `saveSync` call site needs:
|
|
734
|
-
// 1. `DryRunGuard.active()` short-circuit
|
|
738
|
+
// 1. `DryRunGuard.active()` short-circuit - keeps the mutation in
|
|
735
739
|
// memory but suppresses the on-disk write under SELA_DRY_RUN.
|
|
736
|
-
// 2. `sharedWorkspaceSnapshot.preWrite()`
|
|
740
|
+
// 2. `sharedWorkspaceSnapshot.preWrite()` - captures `contentBefore`
|
|
737
741
|
// so the Clean-Room ledger / Local DX rollback can rewind.
|
|
738
|
-
// 3. `sharedWorkspaceSnapshot.postWrite()`
|
|
742
|
+
// 3. `sharedWorkspaceSnapshot.postWrite()` - refreshes `contentAfter`
|
|
739
743
|
// so `getMutatedSnapshots()` (driving the pending-prompts ledger
|
|
740
744
|
// and the interactive CLI prompt) sees the new disk state.
|
|
741
745
|
//
|
|
742
|
-
// Strategies do NOT receive the persistence service via DI
|
|
746
|
+
// Strategies do NOT receive the persistence service via DI - they call
|
|
743
747
|
// this helper directly. Same singleton, zero coupling, one code path.
|
|
744
748
|
// ═══════════════════════════════════════════════════════════════════
|
|
745
749
|
function persistSourceFile(sourceFile) {
|
|
@@ -753,7 +757,7 @@ function persistSourceFile(sourceFile) {
|
|
|
753
757
|
WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.postWrite(fp);
|
|
754
758
|
}
|
|
755
759
|
// ═══════════════════════════════════════════════════════════════════
|
|
756
|
-
// NODE TRANSFORMATION STRATEGY
|
|
760
|
+
// NODE TRANSFORMATION STRATEGY - Scope-Aware, Zero-Redundancy
|
|
757
761
|
// ═══════════════════════════════════════════════════════════════════
|
|
758
762
|
class NodeTransformationStrategy {
|
|
759
763
|
chainValidator;
|
|
@@ -823,8 +827,10 @@ class NodeTransformationStrategy {
|
|
|
823
827
|
// When AI's first segment TYPE matches the receiver variable's defining type
|
|
824
828
|
// but with a different selector, the AI is replacing the receiver context.
|
|
825
829
|
// Strategy: update the receiver variable's definition, then use remaining
|
|
826
|
-
// segments with the receiver var
|
|
827
|
-
if (!forcedReceiver &&
|
|
830
|
+
// segments with the receiver var - preserving developer's variable structure.
|
|
831
|
+
if (!forcedReceiver &&
|
|
832
|
+
discoveredAstRoot &&
|
|
833
|
+
!PAGE_ROOT_NAMES.has(discoveredAstRoot)) {
|
|
828
834
|
const typeCheckRes = resolveVariableToSegments(sourceFile, discoveredAstRoot, caller.line);
|
|
829
835
|
if (typeCheckRes.segments.length > 0 && workingSegments.length > 0) {
|
|
830
836
|
const receiverDefType = typeCheckRes.segments[typeCheckRes.segments.length - 1].type;
|
|
@@ -923,8 +929,9 @@ class NodeTransformationStrategy {
|
|
|
923
929
|
// to prevent. Skip the candidate; the semantic gate owns this case.
|
|
924
930
|
const firstArg = call.getArguments()[0];
|
|
925
931
|
if (firstArg &&
|
|
926
|
-
(ts_morph_1.Node.isIdentifier(firstArg) ||
|
|
927
|
-
|
|
932
|
+
(ts_morph_1.Node.isIdentifier(firstArg) ||
|
|
933
|
+
ts_morph_1.Node.isPropertyAccessExpression(firstArg))) {
|
|
934
|
+
console.log(`[NodeTransform] 🛑 skip - first arg is ${firstArg.getKindName()} ` +
|
|
928
935
|
`"${firstArg.getText()}" (semantic dispatch owns definition-site mutations)`);
|
|
929
936
|
continue;
|
|
930
937
|
}
|
|
@@ -936,12 +943,12 @@ class NodeTransformationStrategy {
|
|
|
936
943
|
// Before we build code, we must verify that the proposed receiver
|
|
937
944
|
// does not create a circular self-reference.
|
|
938
945
|
//
|
|
939
|
-
// Case A
|
|
946
|
+
// Case A - forcedReceiver is set (strip-and-force path):
|
|
940
947
|
// Check if the chainTop lives inside the initializer of that var.
|
|
941
948
|
// If yes, fall back to the actual AST root (e.g. "page").
|
|
942
949
|
//
|
|
943
|
-
// Case B
|
|
944
|
-
// Use astRoot directly
|
|
950
|
+
// Case B - no forced receiver:
|
|
951
|
+
// Use astRoot directly - it was walked up from the call node and
|
|
945
952
|
// is always either "page" or a stable variable reference.
|
|
946
953
|
// ══════════════════════════════════════════════════════════════
|
|
947
954
|
let effectiveReceiver;
|
|
@@ -1191,7 +1198,7 @@ class NodeTransformationStrategy {
|
|
|
1191
1198
|
}
|
|
1192
1199
|
updateReceiverDefinition(sourceFile, varName, segType, newSelector, callerLine) {
|
|
1193
1200
|
// Collect all matching declarations, then pick the closest one that
|
|
1194
|
-
// appears strictly BEFORE callerLine
|
|
1201
|
+
// appears strictly BEFORE callerLine - prevents accidentally updating
|
|
1195
1202
|
// a same-named variable in a different test block that happens to be
|
|
1196
1203
|
// within the 100-line distance window.
|
|
1197
1204
|
const declarations = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration);
|
|
@@ -1302,7 +1309,7 @@ class NodeTransformationStrategy {
|
|
|
1302
1309
|
.map((d) => `[TS${d.getCode()}] ${d.getMessageText()}`)
|
|
1303
1310
|
.join(", "));
|
|
1304
1311
|
if (/\.(getBy\w+|frameLocator)\s*\(/.test(code)) {
|
|
1305
|
-
console.warn(`[NodeTransform] ⚠️ Semantic method detected
|
|
1312
|
+
console.warn(`[NodeTransform] ⚠️ Semantic method detected - bypassing validation`);
|
|
1306
1313
|
return true;
|
|
1307
1314
|
}
|
|
1308
1315
|
return false;
|
|
@@ -1550,19 +1557,145 @@ function findAssertionsInWindow(sourceFile, locatorVarName, callerLine, windowLi
|
|
|
1550
1557
|
}
|
|
1551
1558
|
return results;
|
|
1552
1559
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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
|
+
}
|
|
1556
1587
|
class ASTSourceUpdater {
|
|
1557
1588
|
project;
|
|
1558
1589
|
advisoryBuffer = new HealingAdvisory_1.AdvisoryBuffer();
|
|
1559
1590
|
flushAdvisories() {
|
|
1560
1591
|
this.advisoryBuffer.flush();
|
|
1561
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
|
+
}
|
|
1562
1695
|
/**
|
|
1563
1696
|
* Centralised write hook for every ts-morph `saveSync` call in this class.
|
|
1564
1697
|
* When `SELA_DRY_RUN` is active the AST mutation stays in memory but the
|
|
1565
|
-
* on-disk file is left untouched
|
|
1698
|
+
* on-disk file is left untouched - the heal pipeline can still compute a
|
|
1566
1699
|
* candidate diff via `sourceFile.getFullText()` and the report layer
|
|
1567
1700
|
* tags the event with `dryRun: true`.
|
|
1568
1701
|
*/
|
|
@@ -1579,7 +1712,7 @@ class ASTSourceUpdater {
|
|
|
1579
1712
|
// BEFORE the LLM call and record a FailedEvent in the "Skipped by
|
|
1580
1713
|
// Developer" category.
|
|
1581
1714
|
//
|
|
1582
|
-
// Implementation note
|
|
1715
|
+
// Implementation note - ts-morph's `Node.getLeadingCommentRanges()`
|
|
1583
1716
|
// returns `ts.CommentRange[]` (offsets, not text). We slice the full
|
|
1584
1717
|
// source text by `(pos, end)` to recover the raw comment for each
|
|
1585
1718
|
// range, matching both `//` line comments and `/* */` block comments
|
|
@@ -1643,7 +1776,7 @@ class ASTSourceUpdater {
|
|
|
1643
1776
|
return { failFast: false };
|
|
1644
1777
|
}
|
|
1645
1778
|
/**
|
|
1646
|
-
* Convenience helper
|
|
1779
|
+
* Convenience helper - runs `scanFailFastDirective` and throws the
|
|
1647
1780
|
* canonical SelaError when the directive is present. Used by SelaEngine
|
|
1648
1781
|
* as the very first gate inside `heal()`.
|
|
1649
1782
|
*/
|
|
@@ -1790,7 +1923,7 @@ class ASTSourceUpdater {
|
|
|
1790
1923
|
sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
1791
1924
|
}
|
|
1792
1925
|
if (caller.line <= 0) {
|
|
1793
|
-
console.log(`[ASTUpdater] ⚠️ line ${caller.line} invalid
|
|
1926
|
+
console.log(`[ASTUpdater] ⚠️ line ${caller.line} invalid - Attempting Global Template Strategy`);
|
|
1794
1927
|
const globalResult = this.strategyTemplateLiteral(sourceFile, caller, oldSelector, newSelector);
|
|
1795
1928
|
if (globalResult && globalResult.success) {
|
|
1796
1929
|
if (contentChange)
|
|
@@ -1814,7 +1947,7 @@ class ASTSourceUpdater {
|
|
|
1814
1947
|
// node when they differ) for `// sela-fail-fast`. When present, abort
|
|
1815
1948
|
// all heal work with SelaError(AST_HEAL_DISABLED_BY_DIRECTIVE) so the
|
|
1816
1949
|
// engine records a FailedEvent in the "Skipped by Developer" bucket.
|
|
1817
|
-
// Defense-in-depth
|
|
1950
|
+
// Defense-in-depth - SelaEngine also calls this at the very start of
|
|
1818
1951
|
// heal() to skip the LLM round-trip entirely.
|
|
1819
1952
|
this.enforceFailFastDirective(caller);
|
|
1820
1953
|
const suspectIdentifier = statement
|
|
@@ -1827,7 +1960,7 @@ class ASTSourceUpdater {
|
|
|
1827
1960
|
// HARD EXIT: any non-null result (success OR failure) stops all
|
|
1828
1961
|
// subsequent AST strategies. When the locator argument is an
|
|
1829
1962
|
// Identifier, PropertyAccess, or TemplateLiteral, the gate owns
|
|
1830
|
-
// the mutation
|
|
1963
|
+
// the mutation - NT/H must never override the symbolic reference.
|
|
1831
1964
|
const semanticResult = this.semanticDispatch(sourceFile, caller, oldSelector, newSelector);
|
|
1832
1965
|
if (semanticResult !== null) {
|
|
1833
1966
|
if (semanticResult.success) {
|
|
@@ -1836,7 +1969,7 @@ class ASTSourceUpdater {
|
|
|
1836
1969
|
}
|
|
1837
1970
|
return this.broadcastAndReturn(semanticResult, oldSelector, newSelector, caller);
|
|
1838
1971
|
}
|
|
1839
|
-
// Failure (blast-radius block, trace failed, syntax error)
|
|
1972
|
+
// Failure (blast-radius block, trace failed, syntax error) - hard stop.
|
|
1840
1973
|
console.log(`[SemanticDispatch] 🚫 Hard stop (${semanticResult.strategy}): ${semanticResult.reason}`);
|
|
1841
1974
|
return semanticResult;
|
|
1842
1975
|
}
|
|
@@ -1885,7 +2018,7 @@ class ASTSourceUpdater {
|
|
|
1885
2018
|
};
|
|
1886
2019
|
}
|
|
1887
2020
|
// ═══════════════════════════════════════════════════════════════
|
|
1888
|
-
// STRATEGY H
|
|
2021
|
+
// STRATEGY H - Smart Chain (Legacy fallback)
|
|
1889
2022
|
// ═══════════════════════════════════════════════════════════════
|
|
1890
2023
|
strategySmartChain(sourceFile, caller, oldSelector, oldSem, smartSegments, originalChainHint, suspectIdentifier) {
|
|
1891
2024
|
console.log(`[ASTUpdater] 🔗 Strategy H v2: Smart Chain (${smartSegments.length} segments)`);
|
|
@@ -1959,13 +2092,14 @@ class ASTSourceUpdater {
|
|
|
1959
2092
|
return b.chainDepth - a.chainDepth;
|
|
1960
2093
|
});
|
|
1961
2094
|
for (const { leafCall, chainTop, rootReceiver, chainDepth, originalText, } of candidates) {
|
|
1962
|
-
// Identifier/PropertyAccess guard
|
|
2095
|
+
// Identifier/PropertyAccess guard - mirrors the NT guard.
|
|
1963
2096
|
// Strategy H must never substitute a hardcoded string for a symbolic
|
|
1964
2097
|
// reference; the semantic dispatch gate owns definition-site mutations.
|
|
1965
2098
|
const firstArgH = leafCall.getArguments()[0];
|
|
1966
2099
|
if (firstArgH &&
|
|
1967
|
-
(ts_morph_1.Node.isIdentifier(firstArgH) ||
|
|
1968
|
-
|
|
2100
|
+
(ts_morph_1.Node.isIdentifier(firstArgH) ||
|
|
2101
|
+
ts_morph_1.Node.isPropertyAccessExpression(firstArgH))) {
|
|
2102
|
+
console.log(`[StrategyH] 🛑 skip - first arg is ${firstArgH.getKindName()} ` +
|
|
1969
2103
|
`"${firstArgH.getText()}" (semantic dispatch owns definition-site mutations)`);
|
|
1970
2104
|
continue;
|
|
1971
2105
|
}
|
|
@@ -2035,8 +2169,9 @@ class ASTSourceUpdater {
|
|
|
2035
2169
|
// Block H-partial from substituting a literal for an Identifier/PropertyAccess arg.
|
|
2036
2170
|
const targetFirstArg = targetCall.getArguments()[0];
|
|
2037
2171
|
if (targetFirstArg &&
|
|
2038
|
-
(ts_morph_1.Node.isIdentifier(targetFirstArg) ||
|
|
2039
|
-
|
|
2172
|
+
(ts_morph_1.Node.isIdentifier(targetFirstArg) ||
|
|
2173
|
+
ts_morph_1.Node.isPropertyAccessExpression(targetFirstArg))) {
|
|
2174
|
+
console.log(`[H-partial] 🛑 skip - arg is ${targetFirstArg.getKindName()} ` +
|
|
2040
2175
|
`"${targetFirstArg.getText()}" (semantic dispatch owns this)`);
|
|
2041
2176
|
return null;
|
|
2042
2177
|
}
|
|
@@ -2072,7 +2207,7 @@ class ASTSourceUpdater {
|
|
|
2072
2207
|
}
|
|
2073
2208
|
}
|
|
2074
2209
|
// ─────────────────────────────────────────────────────────────
|
|
2075
|
-
// STRATEGY F
|
|
2210
|
+
// STRATEGY F - Chain Collapse
|
|
2076
2211
|
// ─────────────────────────────────────────────────────────────
|
|
2077
2212
|
strategyChainCollapse(sourceFile, caller, oldSelector, newSelector, oldSem, newSem) {
|
|
2078
2213
|
console.log(`[ASTUpdater] ⛓️ Strategy F: Chain Collapse (upward traversal)`);
|
|
@@ -2138,7 +2273,7 @@ class ASTSourceUpdater {
|
|
|
2138
2273
|
};
|
|
2139
2274
|
}
|
|
2140
2275
|
// ─────────────────────────────────────────────────────────────
|
|
2141
|
-
// STRATEGY G
|
|
2276
|
+
// STRATEGY G - Assertion Healing
|
|
2142
2277
|
// ─────────────────────────────────────────────────────────────
|
|
2143
2278
|
runStrategyG(sourceFile, caller, locatorVarName, contentChange) {
|
|
2144
2279
|
if (!contentChange)
|
|
@@ -2170,7 +2305,7 @@ class ASTSourceUpdater {
|
|
|
2170
2305
|
return null;
|
|
2171
2306
|
}
|
|
2172
2307
|
// ─────────────────────────────────────────────────────────────
|
|
2173
|
-
// STRATEGY A
|
|
2308
|
+
// STRATEGY A - Frame Upstream Healing
|
|
2174
2309
|
// ─────────────────────────────────────────────────────────────
|
|
2175
2310
|
strategyFrameUpstream(sourceFile, caller, oldElementSelector, aiSegments, fullSelectorContext) {
|
|
2176
2311
|
console.log(`[ASTUpdater] 🧠 Strategy A: Frame Upstream`);
|
|
@@ -2246,7 +2381,7 @@ class ASTSourceUpdater {
|
|
|
2246
2381
|
};
|
|
2247
2382
|
}
|
|
2248
2383
|
// ─────────────────────────────────────────────────────────────
|
|
2249
|
-
// STRATEGY B
|
|
2384
|
+
// STRATEGY B - Deep Symbol Resolution
|
|
2250
2385
|
// ─────────────────────────────────────────────────────────────
|
|
2251
2386
|
strategyDeepSymbolResolution(sourceFile, caller, oldSelector, newSelector, oldSem, newSem, suspectIdentifier) {
|
|
2252
2387
|
console.log(`[ASTUpdater] 🔭 Strategy B: Deep Symbol Resolution`);
|
|
@@ -2430,7 +2565,7 @@ class ASTSourceUpdater {
|
|
|
2430
2565
|
};
|
|
2431
2566
|
}
|
|
2432
2567
|
// ─────────────────────────────────────────────────────────────
|
|
2433
|
-
// STRATEGY C
|
|
2568
|
+
// STRATEGY C - Inline Semantic Fix
|
|
2434
2569
|
// ─────────────────────────────────────────────────────────────
|
|
2435
2570
|
strategyInlineSemanticFix(sourceFile, caller, oldSelector, newSelector, oldSem, newSem) {
|
|
2436
2571
|
console.log(`[ASTUpdater] 🎯 Strategy C: Inline Semantic Fix`);
|
|
@@ -2488,7 +2623,7 @@ class ASTSourceUpdater {
|
|
|
2488
2623
|
};
|
|
2489
2624
|
}
|
|
2490
2625
|
// ─────────────────────────────────────────────────────────────
|
|
2491
|
-
// STRATEGY D
|
|
2626
|
+
// STRATEGY D - Template Literal
|
|
2492
2627
|
// ─────────────────────────────────────────────────────────────
|
|
2493
2628
|
strategyTemplateLiteral(sourceFile, caller, oldSelector, newSelector) {
|
|
2494
2629
|
console.log(`[ASTUpdater] 🔤 Strategy D: Template Literal`);
|
|
@@ -2545,7 +2680,7 @@ class ASTSourceUpdater {
|
|
|
2545
2680
|
};
|
|
2546
2681
|
}
|
|
2547
2682
|
// ─────────────────────────────────────────────────────────────
|
|
2548
|
-
// STRATEGY E
|
|
2683
|
+
// STRATEGY E - Semantic Fragment Scan
|
|
2549
2684
|
// ─────────────────────────────────────────────────────────────
|
|
2550
2685
|
strategySemanticFragmentScan(sourceFile, caller, oldSelector, newSelector, oldSem, newSem) {
|
|
2551
2686
|
console.log(`[ASTUpdater] 🔎 Strategy E: Semantic Fragment Scan`);
|
|
@@ -2666,7 +2801,7 @@ class ASTSourceUpdater {
|
|
|
2666
2801
|
/**
|
|
2667
2802
|
* Guard: returns false when replacing oldNode with proposedNewText would
|
|
2668
2803
|
* substitute a hardcoded string literal for an Identifier or
|
|
2669
|
-
* PropertyAccessExpression
|
|
2804
|
+
* PropertyAccessExpression - which is the definition-site mutation bug.
|
|
2670
2805
|
* All other replacements are allowed.
|
|
2671
2806
|
*/
|
|
2672
2807
|
static isSafeReplacement(oldNode, proposedNewText) {
|
|
@@ -2690,7 +2825,7 @@ class ASTSourceUpdater {
|
|
|
2690
2825
|
firstArg.replaceWithText(`"${safe}"`);
|
|
2691
2826
|
}
|
|
2692
2827
|
else {
|
|
2693
|
-
console.warn(`[ASTUpdater] ⚠️ replaceCallArgument: blocked unsafe replacement
|
|
2828
|
+
console.warn(`[ASTUpdater] ⚠️ replaceCallArgument: blocked unsafe replacement - ` +
|
|
2694
2829
|
`arg is ${firstArg.getKindName()}, not a string literal. ` +
|
|
2695
2830
|
`Heal at definition site instead.`);
|
|
2696
2831
|
return false;
|
|
@@ -2992,10 +3127,10 @@ class ASTSourceUpdater {
|
|
|
2992
3127
|
shape === "PROPERTY_ACCESS" ||
|
|
2993
3128
|
shape === "TEMPLATE_LITERAL") {
|
|
2994
3129
|
console.log(`[SemanticDispatch] 🛑 hard-block: "${node.getText()}" is ${shape} ` +
|
|
2995
|
-
`but trace failed
|
|
3130
|
+
`but trace failed - preserving call site`);
|
|
2996
3131
|
return {
|
|
2997
3132
|
success: false,
|
|
2998
|
-
reason: `semantic guard: could not trace "${node.getText()}"
|
|
3133
|
+
reason: `semantic guard: could not trace "${node.getText()}" - call site preserved`,
|
|
2999
3134
|
strategy: "semantic-guard",
|
|
3000
3135
|
};
|
|
3001
3136
|
}
|
|
@@ -3007,6 +3142,72 @@ class ASTSourceUpdater {
|
|
|
3007
3142
|
return null;
|
|
3008
3143
|
}
|
|
3009
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
|
+
}
|
|
3010
3211
|
handleIdentifierArg(node, sourceFile, caller, newSelector) {
|
|
3011
3212
|
const traceResult = DefinitionTracer_1.DefinitionTracer.traceIdentifier(node, sourceFile, caller.line);
|
|
3012
3213
|
if (traceResult.found) {
|
|
@@ -3018,45 +3219,27 @@ class ASTSourceUpdater {
|
|
|
3018
3219
|
if (blast.risk === "high") {
|
|
3019
3220
|
this.advisoryBuffer.push({
|
|
3020
3221
|
code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_BLOCKED,
|
|
3021
|
-
message: `Mutation blocked
|
|
3222
|
+
message: `Mutation blocked - ${blast.testFunctionCount} tests reference this constant.`,
|
|
3022
3223
|
affectedFile: targetFile.getFilePath(),
|
|
3023
3224
|
affectedLine: targetNode.getStartLineNumber() - 1,
|
|
3024
3225
|
suggestedAction: "Review all referencing tests before changing this constant.",
|
|
3025
3226
|
});
|
|
3026
3227
|
return {
|
|
3027
3228
|
success: false,
|
|
3028
|
-
reason: `BlastRadius BLOCKED: ${blast.testFunctionCount} tests reference this constant
|
|
3229
|
+
reason: `BlastRadius BLOCKED: ${blast.testFunctionCount} tests reference this constant - manual review required`,
|
|
3029
3230
|
strategy: "semantic-identifier",
|
|
3030
3231
|
};
|
|
3031
3232
|
}
|
|
3032
3233
|
if (blast.risk === "medium") {
|
|
3033
3234
|
this.advisoryBuffer.push({
|
|
3034
3235
|
code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_WARNING,
|
|
3035
|
-
message: `Mutated constant used in ${blast.testFunctionCount} tests
|
|
3236
|
+
message: `Mutated constant used in ${blast.testFunctionCount} tests - verify no regressions.`,
|
|
3036
3237
|
affectedFile: targetFile.getFilePath(),
|
|
3037
3238
|
affectedLine: targetNode.getStartLineNumber() - 1,
|
|
3038
3239
|
suggestedAction: "Run the full test suite to confirm all references still pass.",
|
|
3039
3240
|
});
|
|
3040
3241
|
}
|
|
3041
|
-
|
|
3042
|
-
if (!result)
|
|
3043
|
-
return null;
|
|
3044
|
-
if (!this.validateFileSyntax(targetFile)) {
|
|
3045
|
-
return {
|
|
3046
|
-
success: false,
|
|
3047
|
-
reason: "post-mutation syntax validation failed — reverted",
|
|
3048
|
-
strategy: "semantic-identifier",
|
|
3049
|
-
};
|
|
3050
|
-
}
|
|
3051
|
-
return {
|
|
3052
|
-
success: true,
|
|
3053
|
-
reason: `definition-site heal via identifier trace`,
|
|
3054
|
-
strategy: "semantic-identifier",
|
|
3055
|
-
lineUpdated: result.lineUpdated,
|
|
3056
|
-
healedLocatorString: result.healedLocatorString,
|
|
3057
|
-
definitionSite: { file: targetFile.getFilePath(), line: result.lineUpdated },
|
|
3058
|
-
blastRadius: blast.testFunctionCount,
|
|
3059
|
-
};
|
|
3242
|
+
return this.applyDefinitionSiteHeal(targetNode, targetFile, newSelector, "semantic-identifier", blast.testFunctionCount);
|
|
3060
3243
|
}
|
|
3061
3244
|
if (!traceResult.found && traceResult.crossFile) {
|
|
3062
3245
|
const xResult = CrossFileHealer_1.CrossFileHealer.heal(traceResult.modulePath, traceResult.symbolName, newSelector, caller.filePath, this.project);
|
|
@@ -3083,45 +3266,27 @@ class ASTSourceUpdater {
|
|
|
3083
3266
|
if (blast.risk === "high") {
|
|
3084
3267
|
this.advisoryBuffer.push({
|
|
3085
3268
|
code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_BLOCKED,
|
|
3086
|
-
message: `Mutation blocked
|
|
3269
|
+
message: `Mutation blocked - ${blast.testFunctionCount} tests reference this property.`,
|
|
3087
3270
|
affectedFile: targetFile.getFilePath(),
|
|
3088
3271
|
affectedLine: targetNode.getStartLineNumber() - 1,
|
|
3089
3272
|
suggestedAction: "Review all referencing tests before changing this property.",
|
|
3090
3273
|
});
|
|
3091
3274
|
return {
|
|
3092
3275
|
success: false,
|
|
3093
|
-
reason: `BlastRadius BLOCKED: ${blast.testFunctionCount} tests reference this property
|
|
3276
|
+
reason: `BlastRadius BLOCKED: ${blast.testFunctionCount} tests reference this property - manual review required`,
|
|
3094
3277
|
strategy: "semantic-property-access",
|
|
3095
3278
|
};
|
|
3096
3279
|
}
|
|
3097
3280
|
if (blast.risk === "medium") {
|
|
3098
3281
|
this.advisoryBuffer.push({
|
|
3099
3282
|
code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_WARNING,
|
|
3100
|
-
message: `Mutated property used in ${blast.testFunctionCount} tests
|
|
3283
|
+
message: `Mutated property used in ${blast.testFunctionCount} tests - verify no regressions.`,
|
|
3101
3284
|
affectedFile: targetFile.getFilePath(),
|
|
3102
3285
|
affectedLine: targetNode.getStartLineNumber() - 1,
|
|
3103
3286
|
suggestedAction: "Run the full test suite to confirm all references still pass.",
|
|
3104
3287
|
});
|
|
3105
3288
|
}
|
|
3106
|
-
|
|
3107
|
-
if (!result)
|
|
3108
|
-
return null;
|
|
3109
|
-
if (!this.validateFileSyntax(targetFile)) {
|
|
3110
|
-
return {
|
|
3111
|
-
success: false,
|
|
3112
|
-
reason: "post-mutation syntax validation failed — reverted",
|
|
3113
|
-
strategy: "semantic-property-access",
|
|
3114
|
-
};
|
|
3115
|
-
}
|
|
3116
|
-
return {
|
|
3117
|
-
success: true,
|
|
3118
|
-
reason: `definition-site heal via property access trace`,
|
|
3119
|
-
strategy: "semantic-property-access",
|
|
3120
|
-
lineUpdated: result.lineUpdated,
|
|
3121
|
-
healedLocatorString: result.healedLocatorString,
|
|
3122
|
-
definitionSite: { file: targetFile.getFilePath(), line: result.lineUpdated },
|
|
3123
|
-
blastRadius: blast.testFunctionCount,
|
|
3124
|
-
};
|
|
3289
|
+
return this.applyDefinitionSiteHeal(targetNode, targetFile, newSelector, "semantic-property-access", blast.testFunctionCount);
|
|
3125
3290
|
}
|
|
3126
3291
|
if (!traceResult.found && traceResult.crossFile) {
|
|
3127
3292
|
const xResult = CrossFileHealer_1.CrossFileHealer.heal(traceResult.modulePath, traceResult.symbolName, newSelector, caller.filePath, this.project);
|
|
@@ -3163,10 +3328,11 @@ class ASTSourceUpdater {
|
|
|
3163
3328
|
}
|
|
3164
3329
|
}
|
|
3165
3330
|
}
|
|
3166
|
-
if (patchResult.reason.includes("structural") ||
|
|
3331
|
+
if (patchResult.reason.includes("structural") ||
|
|
3332
|
+
patchResult.reason.includes("not found")) {
|
|
3167
3333
|
this.advisoryBuffer.push({
|
|
3168
3334
|
code: HealingAdvisory_1.AdvisoryCode.MANUAL_REVIEW_TEMPLATE_COMPLEX,
|
|
3169
|
-
message: `Template literal requires structural change
|
|
3335
|
+
message: `Template literal requires structural change - automated patch skipped.`,
|
|
3170
3336
|
affectedFile: sourceFile.getFilePath(),
|
|
3171
3337
|
affectedLine: node.getStartLineNumber() - 1,
|
|
3172
3338
|
suggestedAction: "Manually update the template literal and its callers.",
|
|
@@ -3180,7 +3346,7 @@ class ASTSourceUpdater {
|
|
|
3180
3346
|
if (!this.validateFileSyntax(sourceFile)) {
|
|
3181
3347
|
return {
|
|
3182
3348
|
success: false,
|
|
3183
|
-
reason: "post-mutation syntax validation failed
|
|
3349
|
+
reason: "post-mutation syntax validation failed - reverted",
|
|
3184
3350
|
strategy: "semantic-template",
|
|
3185
3351
|
};
|
|
3186
3352
|
}
|
|
@@ -3208,7 +3374,8 @@ class ASTSourceUpdater {
|
|
|
3208
3374
|
if (ts_morph_1.Node.isCallExpression(init)) {
|
|
3209
3375
|
return this.handleCallExpressionArg(init, sourceFile, oldSpanVal, newSpanVal);
|
|
3210
3376
|
}
|
|
3211
|
-
if (ts_morph_1.Node.isStringLiteral(init) ||
|
|
3377
|
+
if (ts_morph_1.Node.isStringLiteral(init) ||
|
|
3378
|
+
ts_morph_1.Node.isNoSubstitutionTemplateLiteral(init)) {
|
|
3212
3379
|
const result = InitializerUpdater_1.InitializerUpdater.apply(init, sourceFile, newSpanVal);
|
|
3213
3380
|
if (!result)
|
|
3214
3381
|
return null;
|
|
@@ -3219,7 +3386,10 @@ class ASTSourceUpdater {
|
|
|
3219
3386
|
strategy: "semantic-template",
|
|
3220
3387
|
lineUpdated: result.lineUpdated,
|
|
3221
3388
|
healedLocatorString: result.healedLocatorString,
|
|
3222
|
-
definitionSite: {
|
|
3389
|
+
definitionSite: {
|
|
3390
|
+
file: sourceFile.getFilePath(),
|
|
3391
|
+
line: result.lineUpdated,
|
|
3392
|
+
},
|
|
3223
3393
|
};
|
|
3224
3394
|
}
|
|
3225
3395
|
if (ts_morph_1.Node.isTemplateExpression(init)) {
|
|
@@ -3233,7 +3403,7 @@ class ASTSourceUpdater {
|
|
|
3233
3403
|
if (fnAnalysis.kind === "complex") {
|
|
3234
3404
|
this.advisoryBuffer.push({
|
|
3235
3405
|
code: HealingAdvisory_1.AdvisoryCode.MANUAL_REVIEW_CONDITIONAL_RETURN,
|
|
3236
|
-
message: `Function has conditional/complex return
|
|
3406
|
+
message: `Function has conditional/complex return - automated patch skipped. Reason: ${fnAnalysis.reason}`,
|
|
3237
3407
|
affectedFile: sourceFile.getFilePath(),
|
|
3238
3408
|
suggestedAction: "Manually update the function's return expression.",
|
|
3239
3409
|
});
|
|
@@ -3246,7 +3416,7 @@ class ASTSourceUpdater {
|
|
|
3246
3416
|
if (!this.validateFileSyntax(fnAnalysis.functionFile)) {
|
|
3247
3417
|
return {
|
|
3248
3418
|
success: false,
|
|
3249
|
-
reason: "post-mutation syntax validation failed
|
|
3419
|
+
reason: "post-mutation syntax validation failed - reverted",
|
|
3250
3420
|
strategy: "semantic-call-expression",
|
|
3251
3421
|
};
|
|
3252
3422
|
}
|
|
@@ -3256,7 +3426,10 @@ class ASTSourceUpdater {
|
|
|
3256
3426
|
strategy: "semantic-call-expression",
|
|
3257
3427
|
lineUpdated: result.lineUpdated,
|
|
3258
3428
|
healedLocatorString: result.healedLocatorString,
|
|
3259
|
-
definitionSite: {
|
|
3429
|
+
definitionSite: {
|
|
3430
|
+
file: fnAnalysis.functionFile.getFilePath(),
|
|
3431
|
+
line: result.lineUpdated,
|
|
3432
|
+
},
|
|
3260
3433
|
};
|
|
3261
3434
|
}
|
|
3262
3435
|
if (fnAnalysis.kind === "template") {
|
|
@@ -3266,7 +3439,7 @@ class ASTSourceUpdater {
|
|
|
3266
3439
|
if (patchResult.reason.includes("structural")) {
|
|
3267
3440
|
this.advisoryBuffer.push({
|
|
3268
3441
|
code: HealingAdvisory_1.AdvisoryCode.MANUAL_REVIEW_TEMPLATE_COMPLEX,
|
|
3269
|
-
message: `Function template return requires structural change
|
|
3442
|
+
message: `Function template return requires structural change - automated patch skipped.`,
|
|
3270
3443
|
affectedFile: fnAnalysis.functionFile.getFilePath(),
|
|
3271
3444
|
affectedLine: fnAnalysis.functionLine - 1,
|
|
3272
3445
|
suggestedAction: "Manually update the function's template literal.",
|
|
@@ -3280,7 +3453,7 @@ class ASTSourceUpdater {
|
|
|
3280
3453
|
if (!this.validateFileSyntax(fnAnalysis.functionFile)) {
|
|
3281
3454
|
return {
|
|
3282
3455
|
success: false,
|
|
3283
|
-
reason: "post-mutation syntax validation failed
|
|
3456
|
+
reason: "post-mutation syntax validation failed - reverted",
|
|
3284
3457
|
strategy: "semantic-call-expression",
|
|
3285
3458
|
};
|
|
3286
3459
|
}
|
|
@@ -3290,7 +3463,10 @@ class ASTSourceUpdater {
|
|
|
3290
3463
|
strategy: "semantic-call-expression",
|
|
3291
3464
|
lineUpdated,
|
|
3292
3465
|
healedLocatorString: newSelector,
|
|
3293
|
-
definitionSite: {
|
|
3466
|
+
definitionSite: {
|
|
3467
|
+
file: fnAnalysis.functionFile.getFilePath(),
|
|
3468
|
+
line: lineUpdated,
|
|
3469
|
+
},
|
|
3294
3470
|
};
|
|
3295
3471
|
}
|
|
3296
3472
|
return null;
|
|
@@ -3306,11 +3482,13 @@ class ASTSourceUpdater {
|
|
|
3306
3482
|
return !ALLOWED_CODES.has(d.getCode());
|
|
3307
3483
|
});
|
|
3308
3484
|
if (syntaxErrors.length > 0) {
|
|
3309
|
-
const msgs = syntaxErrors
|
|
3310
|
-
|
|
3485
|
+
const msgs = syntaxErrors
|
|
3486
|
+
.map((d) => String(d.getMessageText()))
|
|
3487
|
+
.join("; ");
|
|
3488
|
+
console.warn(`[SemanticDispatch] ⚠️ syntax errors after mutation: ${msgs} - reverting`);
|
|
3311
3489
|
this.advisoryBuffer.push({
|
|
3312
3490
|
code: HealingAdvisory_1.AdvisoryCode.POST_MUTATION_TYPE_ERROR,
|
|
3313
|
-
message: `Post-mutation type errors detected
|
|
3491
|
+
message: `Post-mutation type errors detected - mutation reverted. Errors: ${msgs}`,
|
|
3314
3492
|
affectedFile: sourceFile.getFilePath(),
|
|
3315
3493
|
suggestedAction: "Manually inspect and fix the type errors introduced by this heal.",
|
|
3316
3494
|
});
|