sela-core 1.0.6 → 1.0.8
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 +1 -1
- 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/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 +9 -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 +3 -2
- package/dist/config/ConfigLoader.d.ts.map +1 -1
- package/dist/config/ConfigLoader.js +23 -13
- package/dist/config/DryRunGuard.d.ts +14 -0
- package/dist/config/DryRunGuard.d.ts.map +1 -0
- package/dist/config/DryRunGuard.js +79 -0
- package/dist/config/SelaConfig.d.ts +26 -16
- package/dist/config/SelaConfig.d.ts.map +1 -1
- package/dist/config/SelaConfig.js +54 -22
- package/dist/engine/HealingRegistry.d.ts +1 -1
- package/dist/engine/HealingRegistry.js +3 -3
- package/dist/engine/SelaEngine.d.ts +13 -10
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +292 -101
- package/dist/errors/SelaError.d.ts +132 -0
- package/dist/errors/SelaError.d.ts.map +1 -0
- package/dist/errors/SelaError.js +154 -0
- package/dist/fixtures/expectProxy.d.ts +1 -1
- package/dist/fixtures/expectProxy.d.ts.map +1 -1
- package/dist/fixtures/expectProxy.js +17 -10
- package/dist/fixtures/index.d.ts +7 -1
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +26 -11
- package/dist/fixtures/proxyTag.d.ts +12 -0
- package/dist/fixtures/proxyTag.d.ts.map +1 -0
- package/dist/fixtures/proxyTag.js +45 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/reporter/SelaReporter.d.ts +143 -0
- package/dist/reporter/SelaReporter.d.ts.map +1 -0
- package/dist/reporter/SelaReporter.js +428 -0
- package/dist/services/ASTSourceUpdater.d.ts +20 -1
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
- package/dist/services/ASTSourceUpdater.js +257 -89
- 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/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 +43 -1
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +108 -25
- package/dist/services/HealingAdvisory.js +1 -1
- package/dist/services/HealingCacheService.d.ts +135 -0
- package/dist/services/HealingCacheService.d.ts.map +1 -0
- package/dist/services/HealingCacheService.js +460 -0
- 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 +20 -1
- package/dist/services/IntentAuditor.d.ts +18 -1
- package/dist/services/IntentAuditor.d.ts.map +1 -1
- package/dist/services/IntentAuditor.js +50 -26
- package/dist/services/InteractiveReview.d.ts +15 -0
- package/dist/services/InteractiveReview.d.ts.map +1 -0
- package/dist/services/InteractiveReview.js +125 -0
- package/dist/services/LLMService.d.ts +18 -2
- package/dist/services/LLMService.d.ts.map +1 -1
- package/dist/services/LLMService.js +33 -23
- package/dist/services/PRAutomationService.d.ts +14 -1
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +287 -92
- package/dist/services/PendingPromptLedger.d.ts +44 -0
- package/dist/services/PendingPromptLedger.d.ts.map +1 -0
- package/dist/services/PendingPromptLedger.js +180 -0
- package/dist/services/ReportGenerator.d.ts +70 -0
- package/dist/services/ReportGenerator.d.ts.map +1 -0
- package/dist/services/ReportGenerator.js +191 -0
- package/dist/services/SafetyGuard.d.ts +30 -7
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SafetyGuard.js +85 -116
- package/dist/services/SnapshotService.js +5 -5
- 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 +66 -34
- package/dist/services/TemplateDiffService.d.ts.map +1 -1
- package/dist/services/TemplateDiffService.js +18 -15
- package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
- package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
- package/dist/services/WorkspaceSnapshotService.js +200 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/utils/IsolatedDiff.d.ts +45 -0
- package/dist/utils/IsolatedDiff.d.ts.map +1 -0
- package/dist/utils/IsolatedDiff.js +382 -0
- package/package.json +71 -67
|
@@ -37,6 +37,9 @@ exports.ASTSourceUpdater = void 0;
|
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const ts_morph_1 = require("ts-morph");
|
|
40
|
+
const DryRunGuard_1 = require("../config/DryRunGuard");
|
|
41
|
+
const WorkspaceSnapshotService_1 = require("./WorkspaceSnapshotService");
|
|
42
|
+
const SelaError_1 = require("../errors/SelaError");
|
|
40
43
|
const HealingRegistry_1 = require("../engine/HealingRegistry");
|
|
41
44
|
const ArgumentTypeAnalyzer_1 = require("./ArgumentTypeAnalyzer");
|
|
42
45
|
const BlastRadiusAnalyzer_1 = require("./BlastRadiusAnalyzer");
|
|
@@ -47,7 +50,7 @@ const CrossFileHealer_1 = require("./CrossFileHealer");
|
|
|
47
50
|
const HealingAdvisory_1 = require("./HealingAdvisory");
|
|
48
51
|
const ChainValidator_1 = require("./ChainValidator");
|
|
49
52
|
// ═══════════════════════════════════════════════════════════════════
|
|
50
|
-
// FIX #1
|
|
53
|
+
// FIX #1 - normalizeSelector utility
|
|
51
54
|
// ═══════════════════════════════════════════════════════════════════
|
|
52
55
|
function normalizeSelector(s) {
|
|
53
56
|
return s
|
|
@@ -67,7 +70,7 @@ function normalizeSelector(s) {
|
|
|
67
70
|
// of "varName", or null otherwise.
|
|
68
71
|
//
|
|
69
72
|
// Why this matters:
|
|
70
|
-
// const shadowBtn = shadowBtn.getByRole(…) ← CIRCULAR
|
|
73
|
+
// const shadowBtn = shadowBtn.getByRole(…) ← CIRCULAR - must be caught
|
|
71
74
|
// const shadowBtn = page.getByRole(…) ← CORRECT
|
|
72
75
|
//
|
|
73
76
|
// We walk UP the ancestor chain from the call node looking for a
|
|
@@ -80,7 +83,7 @@ function getEnclosingVariableDeclaration(node) {
|
|
|
80
83
|
if (ts_morph_1.Node.isVariableDeclaration(current)) {
|
|
81
84
|
return current;
|
|
82
85
|
}
|
|
83
|
-
// Stop climbing past statement boundaries
|
|
86
|
+
// Stop climbing past statement boundaries - we only want the immediate
|
|
84
87
|
// enclosing declaration, not a declaration somewhere further up the tree.
|
|
85
88
|
if (ts_morph_1.Node.isBlock(current) ||
|
|
86
89
|
ts_morph_1.Node.isSourceFile(current) ||
|
|
@@ -118,7 +121,7 @@ function isInsideOwnInitializer(callNode, varName) {
|
|
|
118
121
|
function resolveSafeReceiver(proposedReceiver, rootReceiver, callNode) {
|
|
119
122
|
if (isInsideOwnInitializer(callNode, proposedReceiver)) {
|
|
120
123
|
console.log(`[ScopeGuard] 🔒 Circular reference prevented: ` +
|
|
121
|
-
`"${proposedReceiver}" is being initialized
|
|
124
|
+
`"${proposedReceiver}" is being initialized - ` +
|
|
122
125
|
`falling back to root receiver "${rootReceiver}"`);
|
|
123
126
|
return rootReceiver;
|
|
124
127
|
}
|
|
@@ -551,7 +554,7 @@ class MethodMapper {
|
|
|
551
554
|
for (const seg of segments) {
|
|
552
555
|
const callText = MethodMapper.build(currentReceiver, seg);
|
|
553
556
|
if (callText === null) {
|
|
554
|
-
console.warn(`[MethodMapper] Cannot build segment type "${seg.type}"
|
|
557
|
+
console.warn(`[MethodMapper] Cannot build segment type "${seg.type}" - aborting chain`);
|
|
555
558
|
return null;
|
|
556
559
|
}
|
|
557
560
|
const methodPart = callText.slice(currentReceiver.length);
|
|
@@ -614,7 +617,7 @@ function deduplicateChainSegments(aiSegments, contextSegments) {
|
|
|
614
617
|
if (overlap === 0)
|
|
615
618
|
return { deduped: aiSegments, stripped: 0 };
|
|
616
619
|
console.log(`[ChainDedup] ✂️ Stripping ${overlap}/${contextSegments.length} duplicate leading ` +
|
|
617
|
-
`segment(s)
|
|
620
|
+
`segment(s) - context already encodes them`);
|
|
618
621
|
return { deduped: aiSegments.slice(overlap), stripped: overlap };
|
|
619
622
|
}
|
|
620
623
|
const PAGE_ROOT_NAMES = new Set([
|
|
@@ -723,7 +726,34 @@ function resolveVariableToSegments(sourceFile, varName, beforeLine, depth = 0) {
|
|
|
723
726
|
};
|
|
724
727
|
}
|
|
725
728
|
// ═══════════════════════════════════════════════════════════════════
|
|
726
|
-
//
|
|
729
|
+
// SHARED PERSISTENCE HOOK
|
|
730
|
+
//
|
|
731
|
+
// Module-level write gate shared between `ASTSourceUpdater` and the
|
|
732
|
+
// internal `NodeTransformationStrategy`. Centralises three concerns
|
|
733
|
+
// every `saveSync` call site needs:
|
|
734
|
+
// 1. `DryRunGuard.active()` short-circuit - keeps the mutation in
|
|
735
|
+
// memory but suppresses the on-disk write under SELA_DRY_RUN.
|
|
736
|
+
// 2. `sharedWorkspaceSnapshot.preWrite()` - captures `contentBefore`
|
|
737
|
+
// so the Clean-Room ledger / Local DX rollback can rewind.
|
|
738
|
+
// 3. `sharedWorkspaceSnapshot.postWrite()` - refreshes `contentAfter`
|
|
739
|
+
// so `getMutatedSnapshots()` (driving the pending-prompts ledger
|
|
740
|
+
// and the interactive CLI prompt) sees the new disk state.
|
|
741
|
+
//
|
|
742
|
+
// Strategies do NOT receive the persistence service via DI - they call
|
|
743
|
+
// this helper directly. Same singleton, zero coupling, one code path.
|
|
744
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
745
|
+
function persistSourceFile(sourceFile) {
|
|
746
|
+
if (DryRunGuard_1.DryRunGuard.active()) {
|
|
747
|
+
DryRunGuard_1.DryRunGuard.logSkippedWrite("saveSync", sourceFile.getFilePath());
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const fp = sourceFile.getFilePath();
|
|
751
|
+
WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.preWrite(fp);
|
|
752
|
+
sourceFile.saveSync();
|
|
753
|
+
WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.postWrite(fp);
|
|
754
|
+
}
|
|
755
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
756
|
+
// NODE TRANSFORMATION STRATEGY - Scope-Aware, Zero-Redundancy
|
|
727
757
|
// ═══════════════════════════════════════════════════════════════════
|
|
728
758
|
class NodeTransformationStrategy {
|
|
729
759
|
chainValidator;
|
|
@@ -793,8 +823,10 @@ class NodeTransformationStrategy {
|
|
|
793
823
|
// When AI's first segment TYPE matches the receiver variable's defining type
|
|
794
824
|
// but with a different selector, the AI is replacing the receiver context.
|
|
795
825
|
// Strategy: update the receiver variable's definition, then use remaining
|
|
796
|
-
// segments with the receiver var
|
|
797
|
-
if (!forcedReceiver &&
|
|
826
|
+
// segments with the receiver var - preserving developer's variable structure.
|
|
827
|
+
if (!forcedReceiver &&
|
|
828
|
+
discoveredAstRoot &&
|
|
829
|
+
!PAGE_ROOT_NAMES.has(discoveredAstRoot)) {
|
|
798
830
|
const typeCheckRes = resolveVariableToSegments(sourceFile, discoveredAstRoot, caller.line);
|
|
799
831
|
if (typeCheckRes.segments.length > 0 && workingSegments.length > 0) {
|
|
800
832
|
const receiverDefType = typeCheckRes.segments[typeCheckRes.segments.length - 1].type;
|
|
@@ -893,8 +925,9 @@ class NodeTransformationStrategy {
|
|
|
893
925
|
// to prevent. Skip the candidate; the semantic gate owns this case.
|
|
894
926
|
const firstArg = call.getArguments()[0];
|
|
895
927
|
if (firstArg &&
|
|
896
|
-
(ts_morph_1.Node.isIdentifier(firstArg) ||
|
|
897
|
-
|
|
928
|
+
(ts_morph_1.Node.isIdentifier(firstArg) ||
|
|
929
|
+
ts_morph_1.Node.isPropertyAccessExpression(firstArg))) {
|
|
930
|
+
console.log(`[NodeTransform] 🛑 skip - first arg is ${firstArg.getKindName()} ` +
|
|
898
931
|
`"${firstArg.getText()}" (semantic dispatch owns definition-site mutations)`);
|
|
899
932
|
continue;
|
|
900
933
|
}
|
|
@@ -906,12 +939,12 @@ class NodeTransformationStrategy {
|
|
|
906
939
|
// Before we build code, we must verify that the proposed receiver
|
|
907
940
|
// does not create a circular self-reference.
|
|
908
941
|
//
|
|
909
|
-
// Case A
|
|
942
|
+
// Case A - forcedReceiver is set (strip-and-force path):
|
|
910
943
|
// Check if the chainTop lives inside the initializer of that var.
|
|
911
944
|
// If yes, fall back to the actual AST root (e.g. "page").
|
|
912
945
|
//
|
|
913
|
-
// Case B
|
|
914
|
-
// Use astRoot directly
|
|
946
|
+
// Case B - no forced receiver:
|
|
947
|
+
// Use astRoot directly - it was walked up from the call node and
|
|
915
948
|
// is always either "page" or a stable variable reference.
|
|
916
949
|
// ══════════════════════════════════════════════════════════════
|
|
917
950
|
let effectiveReceiver;
|
|
@@ -959,7 +992,7 @@ class NodeTransformationStrategy {
|
|
|
959
992
|
` ${newCode.split("\n")[0]}`);
|
|
960
993
|
try {
|
|
961
994
|
chainTop.replaceWithText(newCode);
|
|
962
|
-
sourceFile
|
|
995
|
+
persistSourceFile(sourceFile);
|
|
963
996
|
return {
|
|
964
997
|
success: true,
|
|
965
998
|
reason: `scope-aware transform [${effectiveReceiver}]: ${resolvedSegments.length} segments`,
|
|
@@ -1146,7 +1179,7 @@ class NodeTransformationStrategy {
|
|
|
1146
1179
|
console.log(`[NodeTransform] 🔬 Partial: segment[${changedIdx}] .${currentMethodName}() → ${newCallText}`);
|
|
1147
1180
|
try {
|
|
1148
1181
|
targetCall.replaceWithText(newCallText);
|
|
1149
|
-
sourceFile
|
|
1182
|
+
persistSourceFile(sourceFile);
|
|
1150
1183
|
return {
|
|
1151
1184
|
success: true,
|
|
1152
1185
|
reason: `partial: seg[${changedIdx}] .${currentMethodName}() → .${newSeg.type}()`,
|
|
@@ -1161,7 +1194,7 @@ class NodeTransformationStrategy {
|
|
|
1161
1194
|
}
|
|
1162
1195
|
updateReceiverDefinition(sourceFile, varName, segType, newSelector, callerLine) {
|
|
1163
1196
|
// Collect all matching declarations, then pick the closest one that
|
|
1164
|
-
// appears strictly BEFORE callerLine
|
|
1197
|
+
// appears strictly BEFORE callerLine - prevents accidentally updating
|
|
1165
1198
|
// a same-named variable in a different test block that happens to be
|
|
1166
1199
|
// within the 100-line distance window.
|
|
1167
1200
|
const declarations = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration);
|
|
@@ -1201,7 +1234,7 @@ class NodeTransformationStrategy {
|
|
|
1201
1234
|
if (!ts_morph_1.Node.isStringLiteral(firstArg))
|
|
1202
1235
|
continue;
|
|
1203
1236
|
firstArg.replaceWithText(`"${newSelector}"`);
|
|
1204
|
-
sourceFile
|
|
1237
|
+
persistSourceFile(sourceFile);
|
|
1205
1238
|
return true;
|
|
1206
1239
|
}
|
|
1207
1240
|
return false;
|
|
@@ -1272,7 +1305,7 @@ class NodeTransformationStrategy {
|
|
|
1272
1305
|
.map((d) => `[TS${d.getCode()}] ${d.getMessageText()}`)
|
|
1273
1306
|
.join(", "));
|
|
1274
1307
|
if (/\.(getBy\w+|frameLocator)\s*\(/.test(code)) {
|
|
1275
|
-
console.warn(`[NodeTransform] ⚠️ Semantic method detected
|
|
1308
|
+
console.warn(`[NodeTransform] ⚠️ Semantic method detected - bypassing validation`);
|
|
1276
1309
|
return true;
|
|
1277
1310
|
}
|
|
1278
1311
|
return false;
|
|
@@ -1529,6 +1562,112 @@ class ASTSourceUpdater {
|
|
|
1529
1562
|
flushAdvisories() {
|
|
1530
1563
|
this.advisoryBuffer.flush();
|
|
1531
1564
|
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Centralised write hook for every ts-morph `saveSync` call in this class.
|
|
1567
|
+
* When `SELA_DRY_RUN` is active the AST mutation stays in memory but the
|
|
1568
|
+
* on-disk file is left untouched - the heal pipeline can still compute a
|
|
1569
|
+
* candidate diff via `sourceFile.getFullText()` and the report layer
|
|
1570
|
+
* tags the event with `dryRun: true`.
|
|
1571
|
+
*/
|
|
1572
|
+
_persistChanges(sourceFile) {
|
|
1573
|
+
persistSourceFile(sourceFile);
|
|
1574
|
+
}
|
|
1575
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1576
|
+
// FAIL-FAST DIRECTIVE SCAN
|
|
1577
|
+
//
|
|
1578
|
+
// Walks the leading comment ranges of the failing statement (and the
|
|
1579
|
+
// CallExpression node when it differs) looking for the directive
|
|
1580
|
+
// `// sela-fail-fast`. When found, throws `SelaError(ASTUpdater,
|
|
1581
|
+
// AST_HEAL_DISABLED_BY_DIRECTIVE)` so SelaEngine can short-circuit
|
|
1582
|
+
// BEFORE the LLM call and record a FailedEvent in the "Skipped by
|
|
1583
|
+
// Developer" category.
|
|
1584
|
+
//
|
|
1585
|
+
// Implementation note - ts-morph's `Node.getLeadingCommentRanges()`
|
|
1586
|
+
// returns `ts.CommentRange[]` (offsets, not text). We slice the full
|
|
1587
|
+
// source text by `(pos, end)` to recover the raw comment for each
|
|
1588
|
+
// range, matching both `//` line comments and `/* */` block comments
|
|
1589
|
+
// with either `sela-fail-fast` or `@sela-fail-fast` spelling.
|
|
1590
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1591
|
+
scanFailFastDirective(caller) {
|
|
1592
|
+
const filePath = this.resolveFilePath(caller.filePath);
|
|
1593
|
+
if (!filePath)
|
|
1594
|
+
return { failFast: false };
|
|
1595
|
+
let sourceFile = this.project.getSourceFile(filePath);
|
|
1596
|
+
if (sourceFile) {
|
|
1597
|
+
sourceFile.refreshFromFileSystemSync();
|
|
1598
|
+
}
|
|
1599
|
+
else {
|
|
1600
|
+
sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
1601
|
+
}
|
|
1602
|
+
if (caller.line <= 0)
|
|
1603
|
+
return { failFast: false };
|
|
1604
|
+
const failureNode = this.findNodeAtLine(sourceFile, caller.line);
|
|
1605
|
+
if (!failureNode)
|
|
1606
|
+
return { failFast: false };
|
|
1607
|
+
const statement = this.getEnclosingStatement(failureNode);
|
|
1608
|
+
// Collect candidate comment-owning nodes: enclosing statement (catches
|
|
1609
|
+
// pre-statement comments) AND the failure node itself (catches inline
|
|
1610
|
+
// pre-CallExpression comments inside multi-line chains). De-dup by
|
|
1611
|
+
// offset to avoid double-matching.
|
|
1612
|
+
const fullText = sourceFile.getFullText();
|
|
1613
|
+
const seen = new Set();
|
|
1614
|
+
const nodes = [];
|
|
1615
|
+
if (statement)
|
|
1616
|
+
nodes.push(statement);
|
|
1617
|
+
if (failureNode !== statement)
|
|
1618
|
+
nodes.push(failureNode);
|
|
1619
|
+
const DIRECTIVE_RE = /\/[\/*]\s*@?sela-fail-fast\b/i;
|
|
1620
|
+
for (const node of nodes) {
|
|
1621
|
+
let ranges = [];
|
|
1622
|
+
try {
|
|
1623
|
+
ranges = node.getLeadingCommentRanges() ?? [];
|
|
1624
|
+
}
|
|
1625
|
+
catch {
|
|
1626
|
+
ranges = [];
|
|
1627
|
+
}
|
|
1628
|
+
for (const r of ranges) {
|
|
1629
|
+
const pos = r.getPos();
|
|
1630
|
+
const end = r.getEnd();
|
|
1631
|
+
const key = `${pos}:${end}`;
|
|
1632
|
+
if (seen.has(key))
|
|
1633
|
+
continue;
|
|
1634
|
+
seen.add(key);
|
|
1635
|
+
const text = fullText.slice(pos, end);
|
|
1636
|
+
if (DIRECTIVE_RE.test(text)) {
|
|
1637
|
+
const lineNum = sourceFile.getLineAndColumnAtPos(pos).line;
|
|
1638
|
+
return {
|
|
1639
|
+
failFast: true,
|
|
1640
|
+
matchedComment: text.trim(),
|
|
1641
|
+
matchedLine: lineNum,
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
return { failFast: false };
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Convenience helper - runs `scanFailFastDirective` and throws the
|
|
1650
|
+
* canonical SelaError when the directive is present. Used by SelaEngine
|
|
1651
|
+
* as the very first gate inside `heal()`.
|
|
1652
|
+
*/
|
|
1653
|
+
enforceFailFastDirective(caller) {
|
|
1654
|
+
const result = this.scanFailFastDirective(caller);
|
|
1655
|
+
if (!result.failFast)
|
|
1656
|
+
return;
|
|
1657
|
+
throw new SelaError_1.SelaError({
|
|
1658
|
+
subsystem: "ASTUpdater",
|
|
1659
|
+
code: SelaError_1.AST_UPDATER_CODES.HEAL_DISABLED_BY_DIRECTIVE,
|
|
1660
|
+
reason: `Heal disabled by developer directive at ${path.basename(caller.filePath)}` +
|
|
1661
|
+
(result.matchedLine ? `:${result.matchedLine}` : ""),
|
|
1662
|
+
context: {
|
|
1663
|
+
filePath: caller.filePath,
|
|
1664
|
+
line: caller.line,
|
|
1665
|
+
directiveLine: result.matchedLine,
|
|
1666
|
+
directiveText: result.matchedComment,
|
|
1667
|
+
category: "Skipped by Developer",
|
|
1668
|
+
},
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1532
1671
|
constructor() {
|
|
1533
1672
|
this.project = new ts_morph_1.Project({
|
|
1534
1673
|
useInMemoryFileSystem: false,
|
|
@@ -1654,7 +1793,7 @@ class ASTSourceUpdater {
|
|
|
1654
1793
|
sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
1655
1794
|
}
|
|
1656
1795
|
if (caller.line <= 0) {
|
|
1657
|
-
console.log(`[ASTUpdater] ⚠️ line ${caller.line} invalid
|
|
1796
|
+
console.log(`[ASTUpdater] ⚠️ line ${caller.line} invalid - Attempting Global Template Strategy`);
|
|
1658
1797
|
const globalResult = this.strategyTemplateLiteral(sourceFile, caller, oldSelector, newSelector);
|
|
1659
1798
|
if (globalResult && globalResult.success) {
|
|
1660
1799
|
if (contentChange)
|
|
@@ -1673,6 +1812,14 @@ class ASTSourceUpdater {
|
|
|
1673
1812
|
const statement = failureNode
|
|
1674
1813
|
? this.getEnclosingStatement(failureNode)
|
|
1675
1814
|
: undefined;
|
|
1815
|
+
// ─── Developer fail-fast directive ─────────────────────────────
|
|
1816
|
+
// Scan leading comments on the enclosing statement (and the failure
|
|
1817
|
+
// node when they differ) for `// sela-fail-fast`. When present, abort
|
|
1818
|
+
// all heal work with SelaError(AST_HEAL_DISABLED_BY_DIRECTIVE) so the
|
|
1819
|
+
// engine records a FailedEvent in the "Skipped by Developer" bucket.
|
|
1820
|
+
// Defense-in-depth - SelaEngine also calls this at the very start of
|
|
1821
|
+
// heal() to skip the LLM round-trip entirely.
|
|
1822
|
+
this.enforceFailFastDirective(caller);
|
|
1676
1823
|
const suspectIdentifier = statement
|
|
1677
1824
|
? this.identifySuspectIdentifier(statement, caller.line)
|
|
1678
1825
|
: null;
|
|
@@ -1683,7 +1830,7 @@ class ASTSourceUpdater {
|
|
|
1683
1830
|
// HARD EXIT: any non-null result (success OR failure) stops all
|
|
1684
1831
|
// subsequent AST strategies. When the locator argument is an
|
|
1685
1832
|
// Identifier, PropertyAccess, or TemplateLiteral, the gate owns
|
|
1686
|
-
// the mutation
|
|
1833
|
+
// the mutation - NT/H must never override the symbolic reference.
|
|
1687
1834
|
const semanticResult = this.semanticDispatch(sourceFile, caller, oldSelector, newSelector);
|
|
1688
1835
|
if (semanticResult !== null) {
|
|
1689
1836
|
if (semanticResult.success) {
|
|
@@ -1692,7 +1839,7 @@ class ASTSourceUpdater {
|
|
|
1692
1839
|
}
|
|
1693
1840
|
return this.broadcastAndReturn(semanticResult, oldSelector, newSelector, caller);
|
|
1694
1841
|
}
|
|
1695
|
-
// Failure (blast-radius block, trace failed, syntax error)
|
|
1842
|
+
// Failure (blast-radius block, trace failed, syntax error) - hard stop.
|
|
1696
1843
|
console.log(`[SemanticDispatch] 🚫 Hard stop (${semanticResult.strategy}): ${semanticResult.reason}`);
|
|
1697
1844
|
return semanticResult;
|
|
1698
1845
|
}
|
|
@@ -1741,7 +1888,7 @@ class ASTSourceUpdater {
|
|
|
1741
1888
|
};
|
|
1742
1889
|
}
|
|
1743
1890
|
// ═══════════════════════════════════════════════════════════════
|
|
1744
|
-
// STRATEGY H
|
|
1891
|
+
// STRATEGY H - Smart Chain (Legacy fallback)
|
|
1745
1892
|
// ═══════════════════════════════════════════════════════════════
|
|
1746
1893
|
strategySmartChain(sourceFile, caller, oldSelector, oldSem, smartSegments, originalChainHint, suspectIdentifier) {
|
|
1747
1894
|
console.log(`[ASTUpdater] 🔗 Strategy H v2: Smart Chain (${smartSegments.length} segments)`);
|
|
@@ -1815,13 +1962,14 @@ class ASTSourceUpdater {
|
|
|
1815
1962
|
return b.chainDepth - a.chainDepth;
|
|
1816
1963
|
});
|
|
1817
1964
|
for (const { leafCall, chainTop, rootReceiver, chainDepth, originalText, } of candidates) {
|
|
1818
|
-
// Identifier/PropertyAccess guard
|
|
1965
|
+
// Identifier/PropertyAccess guard - mirrors the NT guard.
|
|
1819
1966
|
// Strategy H must never substitute a hardcoded string for a symbolic
|
|
1820
1967
|
// reference; the semantic dispatch gate owns definition-site mutations.
|
|
1821
1968
|
const firstArgH = leafCall.getArguments()[0];
|
|
1822
1969
|
if (firstArgH &&
|
|
1823
|
-
(ts_morph_1.Node.isIdentifier(firstArgH) ||
|
|
1824
|
-
|
|
1970
|
+
(ts_morph_1.Node.isIdentifier(firstArgH) ||
|
|
1971
|
+
ts_morph_1.Node.isPropertyAccessExpression(firstArgH))) {
|
|
1972
|
+
console.log(`[StrategyH] 🛑 skip - first arg is ${firstArgH.getKindName()} ` +
|
|
1825
1973
|
`"${firstArgH.getText()}" (semantic dispatch owns definition-site mutations)`);
|
|
1826
1974
|
continue;
|
|
1827
1975
|
}
|
|
@@ -1850,7 +1998,7 @@ class ASTSourceUpdater {
|
|
|
1850
1998
|
console.log(`[ASTUpdater] 🔗 H: full replace (depth=${chainDepth}) →\n${newCode}`);
|
|
1851
1999
|
try {
|
|
1852
2000
|
chainTop.replaceWithText(newCode);
|
|
1853
|
-
|
|
2001
|
+
this._persistChanges(sourceFile);
|
|
1854
2002
|
return {
|
|
1855
2003
|
success: true,
|
|
1856
2004
|
reason: `smart chain applied (${resolvedSegments.length} segments, depth=${chainDepth})`,
|
|
@@ -1891,8 +2039,9 @@ class ASTSourceUpdater {
|
|
|
1891
2039
|
// Block H-partial from substituting a literal for an Identifier/PropertyAccess arg.
|
|
1892
2040
|
const targetFirstArg = targetCall.getArguments()[0];
|
|
1893
2041
|
if (targetFirstArg &&
|
|
1894
|
-
(ts_morph_1.Node.isIdentifier(targetFirstArg) ||
|
|
1895
|
-
|
|
2042
|
+
(ts_morph_1.Node.isIdentifier(targetFirstArg) ||
|
|
2043
|
+
ts_morph_1.Node.isPropertyAccessExpression(targetFirstArg))) {
|
|
2044
|
+
console.log(`[H-partial] 🛑 skip - arg is ${targetFirstArg.getKindName()} ` +
|
|
1896
2045
|
`"${targetFirstArg.getText()}" (semantic dispatch owns this)`);
|
|
1897
2046
|
return null;
|
|
1898
2047
|
}
|
|
@@ -1914,7 +2063,7 @@ class ASTSourceUpdater {
|
|
|
1914
2063
|
console.log(`[ASTUpdater] 🔗 H partial: .${currentMethodName}() → ${newCallText}`);
|
|
1915
2064
|
try {
|
|
1916
2065
|
targetCall.replaceWithText(newCallText);
|
|
1917
|
-
|
|
2066
|
+
this._persistChanges(sourceFile);
|
|
1918
2067
|
return {
|
|
1919
2068
|
success: true,
|
|
1920
2069
|
reason: `partial chain heal: segment[${changedIdx}] .${currentMethodName}() → .${newSeg.type}()`,
|
|
@@ -1928,7 +2077,7 @@ class ASTSourceUpdater {
|
|
|
1928
2077
|
}
|
|
1929
2078
|
}
|
|
1930
2079
|
// ─────────────────────────────────────────────────────────────
|
|
1931
|
-
// STRATEGY F
|
|
2080
|
+
// STRATEGY F - Chain Collapse
|
|
1932
2081
|
// ─────────────────────────────────────────────────────────────
|
|
1933
2082
|
strategyChainCollapse(sourceFile, caller, oldSelector, newSelector, oldSem, newSem) {
|
|
1934
2083
|
console.log(`[ASTUpdater] ⛓️ Strategy F: Chain Collapse (upward traversal)`);
|
|
@@ -1975,7 +2124,7 @@ class ASTSourceUpdater {
|
|
|
1975
2124
|
const newCallText = buildCallText(rootReceiver, newSem);
|
|
1976
2125
|
try {
|
|
1977
2126
|
chainTop.replaceWithText(newCallText);
|
|
1978
|
-
|
|
2127
|
+
this._persistChanges(sourceFile);
|
|
1979
2128
|
return {
|
|
1980
2129
|
success: true,
|
|
1981
2130
|
reason: `chain collapsed atomically (depth=${chainDepth})`,
|
|
@@ -1994,7 +2143,7 @@ class ASTSourceUpdater {
|
|
|
1994
2143
|
};
|
|
1995
2144
|
}
|
|
1996
2145
|
// ─────────────────────────────────────────────────────────────
|
|
1997
|
-
// STRATEGY G
|
|
2146
|
+
// STRATEGY G - Assertion Healing
|
|
1998
2147
|
// ─────────────────────────────────────────────────────────────
|
|
1999
2148
|
runStrategyG(sourceFile, caller, locatorVarName, contentChange) {
|
|
2000
2149
|
if (!contentChange)
|
|
@@ -2026,7 +2175,7 @@ class ASTSourceUpdater {
|
|
|
2026
2175
|
return null;
|
|
2027
2176
|
}
|
|
2028
2177
|
// ─────────────────────────────────────────────────────────────
|
|
2029
|
-
// STRATEGY A
|
|
2178
|
+
// STRATEGY A - Frame Upstream Healing
|
|
2030
2179
|
// ─────────────────────────────────────────────────────────────
|
|
2031
2180
|
strategyFrameUpstream(sourceFile, caller, oldElementSelector, aiSegments, fullSelectorContext) {
|
|
2032
2181
|
console.log(`[ASTUpdater] 🧠 Strategy A: Frame Upstream`);
|
|
@@ -2059,7 +2208,7 @@ class ASTSourceUpdater {
|
|
|
2059
2208
|
}
|
|
2060
2209
|
}
|
|
2061
2210
|
if (anyFixed) {
|
|
2062
|
-
|
|
2211
|
+
this._persistChanges(sourceFile);
|
|
2063
2212
|
return {
|
|
2064
2213
|
success: true,
|
|
2065
2214
|
reason: `upstream variable '${varName}' fixed`,
|
|
@@ -2070,7 +2219,7 @@ class ASTSourceUpdater {
|
|
|
2070
2219
|
for (const change of changedFrames) {
|
|
2071
2220
|
const result = this.replaceStringLiteralInRange(sourceFile, change.old, change.new, declLine, Math.min(sourceFile.getEndLineNumber(), declLine + 8));
|
|
2072
2221
|
if (result !== null) {
|
|
2073
|
-
|
|
2222
|
+
this._persistChanges(sourceFile);
|
|
2074
2223
|
return {
|
|
2075
2224
|
success: true,
|
|
2076
2225
|
reason: "multi-line frame fixed",
|
|
@@ -2085,7 +2234,7 @@ class ASTSourceUpdater {
|
|
|
2085
2234
|
for (const change of changedFrames) {
|
|
2086
2235
|
const result = this.replaceStringLiteralInRange(sourceFile, change.old, change.new, Math.max(1, caller.line - 15), caller.line);
|
|
2087
2236
|
if (result !== null) {
|
|
2088
|
-
|
|
2237
|
+
this._persistChanges(sourceFile);
|
|
2089
2238
|
return {
|
|
2090
2239
|
success: true,
|
|
2091
2240
|
reason: "inline frame fixed",
|
|
@@ -2102,7 +2251,7 @@ class ASTSourceUpdater {
|
|
|
2102
2251
|
};
|
|
2103
2252
|
}
|
|
2104
2253
|
// ─────────────────────────────────────────────────────────────
|
|
2105
|
-
// STRATEGY B
|
|
2254
|
+
// STRATEGY B - Deep Symbol Resolution
|
|
2106
2255
|
// ─────────────────────────────────────────────────────────────
|
|
2107
2256
|
strategyDeepSymbolResolution(sourceFile, caller, oldSelector, newSelector, oldSem, newSem, suspectIdentifier) {
|
|
2108
2257
|
console.log(`[ASTUpdater] 🔭 Strategy B: Deep Symbol Resolution`);
|
|
@@ -2166,10 +2315,10 @@ class ASTSourceUpdater {
|
|
|
2166
2315
|
const newCallText = buildCallText(effectiveReceiver, newSem);
|
|
2167
2316
|
try {
|
|
2168
2317
|
chainTop.replaceWithText(newCallText);
|
|
2169
|
-
|
|
2318
|
+
this._persistChanges(sourceFile);
|
|
2170
2319
|
const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
|
|
2171
2320
|
if (cascadePairs.length > 0) {
|
|
2172
|
-
|
|
2321
|
+
this._persistChanges(sourceFile);
|
|
2173
2322
|
HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
|
|
2174
2323
|
}
|
|
2175
2324
|
return {
|
|
@@ -2208,10 +2357,10 @@ class ASTSourceUpdater {
|
|
|
2208
2357
|
const { chainTop: tailChainTop, rootReceiver } = climbToChainTop(tailCall);
|
|
2209
2358
|
try {
|
|
2210
2359
|
tailChainTop.replaceWithText(buildCallText(rootReceiver, newSem));
|
|
2211
|
-
|
|
2360
|
+
this._persistChanges(sourceFile);
|
|
2212
2361
|
const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
|
|
2213
2362
|
if (cascadePairs.length > 0) {
|
|
2214
|
-
|
|
2363
|
+
this._persistChanges(sourceFile);
|
|
2215
2364
|
HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
|
|
2216
2365
|
}
|
|
2217
2366
|
return {
|
|
@@ -2228,10 +2377,10 @@ class ASTSourceUpdater {
|
|
|
2228
2377
|
}
|
|
2229
2378
|
const fixed = this.replaceChainTailOnly(tailCall, newSem, newSelector);
|
|
2230
2379
|
if (fixed) {
|
|
2231
|
-
|
|
2380
|
+
this._persistChanges(sourceFile);
|
|
2232
2381
|
const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
|
|
2233
2382
|
if (cascadePairs.length > 0) {
|
|
2234
|
-
|
|
2383
|
+
this._persistChanges(sourceFile);
|
|
2235
2384
|
HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
|
|
2236
2385
|
}
|
|
2237
2386
|
return {
|
|
@@ -2253,7 +2402,7 @@ class ASTSourceUpdater {
|
|
|
2253
2402
|
if (chainDepth > 0 || isLeafOfChain(call)) {
|
|
2254
2403
|
try {
|
|
2255
2404
|
stmtChainTop.replaceWithText(buildCallText(rootReceiver, newSem));
|
|
2256
|
-
|
|
2405
|
+
this._persistChanges(sourceFile);
|
|
2257
2406
|
return {
|
|
2258
2407
|
success: true,
|
|
2259
2408
|
reason: "direct statement chain collapse",
|
|
@@ -2264,10 +2413,10 @@ class ASTSourceUpdater {
|
|
|
2264
2413
|
catch { }
|
|
2265
2414
|
}
|
|
2266
2415
|
if (this.replaceChainTailOnly(call, newSem, newSelector)) {
|
|
2267
|
-
|
|
2416
|
+
this._persistChanges(sourceFile);
|
|
2268
2417
|
const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
|
|
2269
2418
|
if (cascadePairs.length > 0) {
|
|
2270
|
-
|
|
2419
|
+
this._persistChanges(sourceFile);
|
|
2271
2420
|
HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
|
|
2272
2421
|
}
|
|
2273
2422
|
return {
|
|
@@ -2286,7 +2435,7 @@ class ASTSourceUpdater {
|
|
|
2286
2435
|
};
|
|
2287
2436
|
}
|
|
2288
2437
|
// ─────────────────────────────────────────────────────────────
|
|
2289
|
-
// STRATEGY C
|
|
2438
|
+
// STRATEGY C - Inline Semantic Fix
|
|
2290
2439
|
// ─────────────────────────────────────────────────────────────
|
|
2291
2440
|
strategyInlineSemanticFix(sourceFile, caller, oldSelector, newSelector, oldSem, newSem) {
|
|
2292
2441
|
console.log(`[ASTUpdater] 🎯 Strategy C: Inline Semantic Fix`);
|
|
@@ -2301,10 +2450,10 @@ class ASTSourceUpdater {
|
|
|
2301
2450
|
try {
|
|
2302
2451
|
chainTop.replaceWithText(buildCallText(rootReceiver, newSem));
|
|
2303
2452
|
const fixLine = chainTop.getStartLineNumber();
|
|
2304
|
-
|
|
2453
|
+
this._persistChanges(sourceFile);
|
|
2305
2454
|
const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
|
|
2306
2455
|
if (cascadePairs.length > 0) {
|
|
2307
|
-
|
|
2456
|
+
this._persistChanges(sourceFile);
|
|
2308
2457
|
HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
|
|
2309
2458
|
}
|
|
2310
2459
|
return {
|
|
@@ -2319,10 +2468,10 @@ class ASTSourceUpdater {
|
|
|
2319
2468
|
}
|
|
2320
2469
|
if (this.applySemanticFix(call, newSem, newSelector)) {
|
|
2321
2470
|
const fixLine = call.getStartLineNumber();
|
|
2322
|
-
|
|
2471
|
+
this._persistChanges(sourceFile);
|
|
2323
2472
|
const cascadePairs = this.performCascadeHeal(sourceFile, oldSelector, newSelector, oldSem, newSem);
|
|
2324
2473
|
if (cascadePairs.length > 0) {
|
|
2325
|
-
|
|
2474
|
+
this._persistChanges(sourceFile);
|
|
2326
2475
|
HealingRegistry_1.HealingRegistry.getInstance().broadcastCascade({ filePath: caller.filePath, line: caller.line }, cascadePairs);
|
|
2327
2476
|
}
|
|
2328
2477
|
return {
|
|
@@ -2344,7 +2493,7 @@ class ASTSourceUpdater {
|
|
|
2344
2493
|
};
|
|
2345
2494
|
}
|
|
2346
2495
|
// ─────────────────────────────────────────────────────────────
|
|
2347
|
-
// STRATEGY D
|
|
2496
|
+
// STRATEGY D - Template Literal
|
|
2348
2497
|
// ─────────────────────────────────────────────────────────────
|
|
2349
2498
|
strategyTemplateLiteral(sourceFile, caller, oldSelector, newSelector) {
|
|
2350
2499
|
console.log(`[ASTUpdater] 🔤 Strategy D: Template Literal`);
|
|
@@ -2385,7 +2534,7 @@ class ASTSourceUpdater {
|
|
|
2385
2534
|
if (newText !== fullText) {
|
|
2386
2535
|
tmpl.replaceWithText(newText.trim());
|
|
2387
2536
|
const line = tmpl.getStartLineNumber();
|
|
2388
|
-
|
|
2537
|
+
this._persistChanges(sourceFile);
|
|
2389
2538
|
return {
|
|
2390
2539
|
success: true,
|
|
2391
2540
|
reason: "template literal suffix updated",
|
|
@@ -2401,7 +2550,7 @@ class ASTSourceUpdater {
|
|
|
2401
2550
|
};
|
|
2402
2551
|
}
|
|
2403
2552
|
// ─────────────────────────────────────────────────────────────
|
|
2404
|
-
// STRATEGY E
|
|
2553
|
+
// STRATEGY E - Semantic Fragment Scan
|
|
2405
2554
|
// ─────────────────────────────────────────────────────────────
|
|
2406
2555
|
strategySemanticFragmentScan(sourceFile, caller, oldSelector, newSelector, oldSem, newSem) {
|
|
2407
2556
|
console.log(`[ASTUpdater] 🔎 Strategy E: Semantic Fragment Scan`);
|
|
@@ -2430,7 +2579,7 @@ class ASTSourceUpdater {
|
|
|
2430
2579
|
if (chainDepth > 0 || isLeafOfChain(call)) {
|
|
2431
2580
|
try {
|
|
2432
2581
|
chainTop.replaceWithText(buildCallText(rootReceiver, newSem));
|
|
2433
|
-
|
|
2582
|
+
this._persistChanges(sourceFile);
|
|
2434
2583
|
return {
|
|
2435
2584
|
success: true,
|
|
2436
2585
|
reason: `fragment scan chain collapse .${callSem.method}() @ line ${call.getStartLineNumber()}`,
|
|
@@ -2441,7 +2590,7 @@ class ASTSourceUpdater {
|
|
|
2441
2590
|
catch { }
|
|
2442
2591
|
}
|
|
2443
2592
|
if (this.replaceChainTailOnly(call, newSem, newSelector)) {
|
|
2444
|
-
|
|
2593
|
+
this._persistChanges(sourceFile);
|
|
2445
2594
|
return {
|
|
2446
2595
|
success: true,
|
|
2447
2596
|
reason: `fragment scan fixed .${callSem.method}() @ line ${call.getStartLineNumber()}`,
|
|
@@ -2522,7 +2671,7 @@ class ASTSourceUpdater {
|
|
|
2522
2671
|
/**
|
|
2523
2672
|
* Guard: returns false when replacing oldNode with proposedNewText would
|
|
2524
2673
|
* substitute a hardcoded string literal for an Identifier or
|
|
2525
|
-
* PropertyAccessExpression
|
|
2674
|
+
* PropertyAccessExpression - which is the definition-site mutation bug.
|
|
2526
2675
|
* All other replacements are allowed.
|
|
2527
2676
|
*/
|
|
2528
2677
|
static isSafeReplacement(oldNode, proposedNewText) {
|
|
@@ -2546,7 +2695,7 @@ class ASTSourceUpdater {
|
|
|
2546
2695
|
firstArg.replaceWithText(`"${safe}"`);
|
|
2547
2696
|
}
|
|
2548
2697
|
else {
|
|
2549
|
-
console.warn(`[ASTUpdater] ⚠️ replaceCallArgument: blocked unsafe replacement
|
|
2698
|
+
console.warn(`[ASTUpdater] ⚠️ replaceCallArgument: blocked unsafe replacement - ` +
|
|
2550
2699
|
`arg is ${firstArg.getKindName()}, not a string literal. ` +
|
|
2551
2700
|
`Heal at definition site instead.`);
|
|
2552
2701
|
return false;
|
|
@@ -2618,7 +2767,7 @@ class ASTSourceUpdater {
|
|
|
2618
2767
|
const propName = firstArg.getName();
|
|
2619
2768
|
const result = this.updateObjectProperty(sourceFile, objName, propName, oldSem.primaryArg, newSem.primaryArg);
|
|
2620
2769
|
if (result !== null) {
|
|
2621
|
-
|
|
2770
|
+
this._persistChanges(sourceFile);
|
|
2622
2771
|
return {
|
|
2623
2772
|
success: true,
|
|
2624
2773
|
reason: `'${objName}.${propName}' updated`,
|
|
@@ -2848,10 +2997,10 @@ class ASTSourceUpdater {
|
|
|
2848
2997
|
shape === "PROPERTY_ACCESS" ||
|
|
2849
2998
|
shape === "TEMPLATE_LITERAL") {
|
|
2850
2999
|
console.log(`[SemanticDispatch] 🛑 hard-block: "${node.getText()}" is ${shape} ` +
|
|
2851
|
-
`but trace failed
|
|
3000
|
+
`but trace failed - preserving call site`);
|
|
2852
3001
|
return {
|
|
2853
3002
|
success: false,
|
|
2854
|
-
reason: `semantic guard: could not trace "${node.getText()}"
|
|
3003
|
+
reason: `semantic guard: could not trace "${node.getText()}" - call site preserved`,
|
|
2855
3004
|
strategy: "semantic-guard",
|
|
2856
3005
|
};
|
|
2857
3006
|
}
|
|
@@ -2874,21 +3023,21 @@ class ASTSourceUpdater {
|
|
|
2874
3023
|
if (blast.risk === "high") {
|
|
2875
3024
|
this.advisoryBuffer.push({
|
|
2876
3025
|
code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_BLOCKED,
|
|
2877
|
-
message: `Mutation blocked
|
|
3026
|
+
message: `Mutation blocked - ${blast.testFunctionCount} tests reference this constant.`,
|
|
2878
3027
|
affectedFile: targetFile.getFilePath(),
|
|
2879
3028
|
affectedLine: targetNode.getStartLineNumber() - 1,
|
|
2880
3029
|
suggestedAction: "Review all referencing tests before changing this constant.",
|
|
2881
3030
|
});
|
|
2882
3031
|
return {
|
|
2883
3032
|
success: false,
|
|
2884
|
-
reason: `BlastRadius BLOCKED: ${blast.testFunctionCount} tests reference this constant
|
|
3033
|
+
reason: `BlastRadius BLOCKED: ${blast.testFunctionCount} tests reference this constant - manual review required`,
|
|
2885
3034
|
strategy: "semantic-identifier",
|
|
2886
3035
|
};
|
|
2887
3036
|
}
|
|
2888
3037
|
if (blast.risk === "medium") {
|
|
2889
3038
|
this.advisoryBuffer.push({
|
|
2890
3039
|
code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_WARNING,
|
|
2891
|
-
message: `Mutated constant used in ${blast.testFunctionCount} tests
|
|
3040
|
+
message: `Mutated constant used in ${blast.testFunctionCount} tests - verify no regressions.`,
|
|
2892
3041
|
affectedFile: targetFile.getFilePath(),
|
|
2893
3042
|
affectedLine: targetNode.getStartLineNumber() - 1,
|
|
2894
3043
|
suggestedAction: "Run the full test suite to confirm all references still pass.",
|
|
@@ -2900,7 +3049,7 @@ class ASTSourceUpdater {
|
|
|
2900
3049
|
if (!this.validateFileSyntax(targetFile)) {
|
|
2901
3050
|
return {
|
|
2902
3051
|
success: false,
|
|
2903
|
-
reason: "post-mutation syntax validation failed
|
|
3052
|
+
reason: "post-mutation syntax validation failed - reverted",
|
|
2904
3053
|
strategy: "semantic-identifier",
|
|
2905
3054
|
};
|
|
2906
3055
|
}
|
|
@@ -2910,7 +3059,10 @@ class ASTSourceUpdater {
|
|
|
2910
3059
|
strategy: "semantic-identifier",
|
|
2911
3060
|
lineUpdated: result.lineUpdated,
|
|
2912
3061
|
healedLocatorString: result.healedLocatorString,
|
|
2913
|
-
definitionSite: {
|
|
3062
|
+
definitionSite: {
|
|
3063
|
+
file: targetFile.getFilePath(),
|
|
3064
|
+
line: result.lineUpdated,
|
|
3065
|
+
},
|
|
2914
3066
|
blastRadius: blast.testFunctionCount,
|
|
2915
3067
|
};
|
|
2916
3068
|
}
|
|
@@ -2939,21 +3091,21 @@ class ASTSourceUpdater {
|
|
|
2939
3091
|
if (blast.risk === "high") {
|
|
2940
3092
|
this.advisoryBuffer.push({
|
|
2941
3093
|
code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_BLOCKED,
|
|
2942
|
-
message: `Mutation blocked
|
|
3094
|
+
message: `Mutation blocked - ${blast.testFunctionCount} tests reference this property.`,
|
|
2943
3095
|
affectedFile: targetFile.getFilePath(),
|
|
2944
3096
|
affectedLine: targetNode.getStartLineNumber() - 1,
|
|
2945
3097
|
suggestedAction: "Review all referencing tests before changing this property.",
|
|
2946
3098
|
});
|
|
2947
3099
|
return {
|
|
2948
3100
|
success: false,
|
|
2949
|
-
reason: `BlastRadius BLOCKED: ${blast.testFunctionCount} tests reference this property
|
|
3101
|
+
reason: `BlastRadius BLOCKED: ${blast.testFunctionCount} tests reference this property - manual review required`,
|
|
2950
3102
|
strategy: "semantic-property-access",
|
|
2951
3103
|
};
|
|
2952
3104
|
}
|
|
2953
3105
|
if (blast.risk === "medium") {
|
|
2954
3106
|
this.advisoryBuffer.push({
|
|
2955
3107
|
code: HealingAdvisory_1.AdvisoryCode.BLAST_RADIUS_WARNING,
|
|
2956
|
-
message: `Mutated property used in ${blast.testFunctionCount} tests
|
|
3108
|
+
message: `Mutated property used in ${blast.testFunctionCount} tests - verify no regressions.`,
|
|
2957
3109
|
affectedFile: targetFile.getFilePath(),
|
|
2958
3110
|
affectedLine: targetNode.getStartLineNumber() - 1,
|
|
2959
3111
|
suggestedAction: "Run the full test suite to confirm all references still pass.",
|
|
@@ -2965,7 +3117,7 @@ class ASTSourceUpdater {
|
|
|
2965
3117
|
if (!this.validateFileSyntax(targetFile)) {
|
|
2966
3118
|
return {
|
|
2967
3119
|
success: false,
|
|
2968
|
-
reason: "post-mutation syntax validation failed
|
|
3120
|
+
reason: "post-mutation syntax validation failed - reverted",
|
|
2969
3121
|
strategy: "semantic-property-access",
|
|
2970
3122
|
};
|
|
2971
3123
|
}
|
|
@@ -2975,7 +3127,10 @@ class ASTSourceUpdater {
|
|
|
2975
3127
|
strategy: "semantic-property-access",
|
|
2976
3128
|
lineUpdated: result.lineUpdated,
|
|
2977
3129
|
healedLocatorString: result.healedLocatorString,
|
|
2978
|
-
definitionSite: {
|
|
3130
|
+
definitionSite: {
|
|
3131
|
+
file: targetFile.getFilePath(),
|
|
3132
|
+
line: result.lineUpdated,
|
|
3133
|
+
},
|
|
2979
3134
|
blastRadius: blast.testFunctionCount,
|
|
2980
3135
|
};
|
|
2981
3136
|
}
|
|
@@ -3019,10 +3174,11 @@ class ASTSourceUpdater {
|
|
|
3019
3174
|
}
|
|
3020
3175
|
}
|
|
3021
3176
|
}
|
|
3022
|
-
if (patchResult.reason.includes("structural") ||
|
|
3177
|
+
if (patchResult.reason.includes("structural") ||
|
|
3178
|
+
patchResult.reason.includes("not found")) {
|
|
3023
3179
|
this.advisoryBuffer.push({
|
|
3024
3180
|
code: HealingAdvisory_1.AdvisoryCode.MANUAL_REVIEW_TEMPLATE_COMPLEX,
|
|
3025
|
-
message: `Template literal requires structural change
|
|
3181
|
+
message: `Template literal requires structural change - automated patch skipped.`,
|
|
3026
3182
|
affectedFile: sourceFile.getFilePath(),
|
|
3027
3183
|
affectedLine: node.getStartLineNumber() - 1,
|
|
3028
3184
|
suggestedAction: "Manually update the template literal and its callers.",
|
|
@@ -3032,11 +3188,11 @@ class ASTSourceUpdater {
|
|
|
3032
3188
|
}
|
|
3033
3189
|
const lineUpdated = node.getStartLineNumber() - 1;
|
|
3034
3190
|
node.replaceWithText(patchResult.patchedTemplate);
|
|
3035
|
-
|
|
3191
|
+
this._persistChanges(sourceFile);
|
|
3036
3192
|
if (!this.validateFileSyntax(sourceFile)) {
|
|
3037
3193
|
return {
|
|
3038
3194
|
success: false,
|
|
3039
|
-
reason: "post-mutation syntax validation failed
|
|
3195
|
+
reason: "post-mutation syntax validation failed - reverted",
|
|
3040
3196
|
strategy: "semantic-template",
|
|
3041
3197
|
};
|
|
3042
3198
|
}
|
|
@@ -3064,18 +3220,22 @@ class ASTSourceUpdater {
|
|
|
3064
3220
|
if (ts_morph_1.Node.isCallExpression(init)) {
|
|
3065
3221
|
return this.handleCallExpressionArg(init, sourceFile, oldSpanVal, newSpanVal);
|
|
3066
3222
|
}
|
|
3067
|
-
if (ts_morph_1.Node.isStringLiteral(init) ||
|
|
3223
|
+
if (ts_morph_1.Node.isStringLiteral(init) ||
|
|
3224
|
+
ts_morph_1.Node.isNoSubstitutionTemplateLiteral(init)) {
|
|
3068
3225
|
const result = InitializerUpdater_1.InitializerUpdater.apply(init, sourceFile, newSpanVal);
|
|
3069
3226
|
if (!result)
|
|
3070
3227
|
return null;
|
|
3071
|
-
|
|
3228
|
+
this._persistChanges(sourceFile);
|
|
3072
3229
|
return {
|
|
3073
3230
|
success: true,
|
|
3074
3231
|
reason: `span literal updated via trace`,
|
|
3075
3232
|
strategy: "semantic-template",
|
|
3076
3233
|
lineUpdated: result.lineUpdated,
|
|
3077
3234
|
healedLocatorString: result.healedLocatorString,
|
|
3078
|
-
definitionSite: {
|
|
3235
|
+
definitionSite: {
|
|
3236
|
+
file: sourceFile.getFilePath(),
|
|
3237
|
+
line: result.lineUpdated,
|
|
3238
|
+
},
|
|
3079
3239
|
};
|
|
3080
3240
|
}
|
|
3081
3241
|
if (ts_morph_1.Node.isTemplateExpression(init)) {
|
|
@@ -3089,7 +3249,7 @@ class ASTSourceUpdater {
|
|
|
3089
3249
|
if (fnAnalysis.kind === "complex") {
|
|
3090
3250
|
this.advisoryBuffer.push({
|
|
3091
3251
|
code: HealingAdvisory_1.AdvisoryCode.MANUAL_REVIEW_CONDITIONAL_RETURN,
|
|
3092
|
-
message: `Function has conditional/complex return
|
|
3252
|
+
message: `Function has conditional/complex return - automated patch skipped. Reason: ${fnAnalysis.reason}`,
|
|
3093
3253
|
affectedFile: sourceFile.getFilePath(),
|
|
3094
3254
|
suggestedAction: "Manually update the function's return expression.",
|
|
3095
3255
|
});
|
|
@@ -3102,7 +3262,7 @@ class ASTSourceUpdater {
|
|
|
3102
3262
|
if (!this.validateFileSyntax(fnAnalysis.functionFile)) {
|
|
3103
3263
|
return {
|
|
3104
3264
|
success: false,
|
|
3105
|
-
reason: "post-mutation syntax validation failed
|
|
3265
|
+
reason: "post-mutation syntax validation failed - reverted",
|
|
3106
3266
|
strategy: "semantic-call-expression",
|
|
3107
3267
|
};
|
|
3108
3268
|
}
|
|
@@ -3112,7 +3272,10 @@ class ASTSourceUpdater {
|
|
|
3112
3272
|
strategy: "semantic-call-expression",
|
|
3113
3273
|
lineUpdated: result.lineUpdated,
|
|
3114
3274
|
healedLocatorString: result.healedLocatorString,
|
|
3115
|
-
definitionSite: {
|
|
3275
|
+
definitionSite: {
|
|
3276
|
+
file: fnAnalysis.functionFile.getFilePath(),
|
|
3277
|
+
line: result.lineUpdated,
|
|
3278
|
+
},
|
|
3116
3279
|
};
|
|
3117
3280
|
}
|
|
3118
3281
|
if (fnAnalysis.kind === "template") {
|
|
@@ -3122,7 +3285,7 @@ class ASTSourceUpdater {
|
|
|
3122
3285
|
if (patchResult.reason.includes("structural")) {
|
|
3123
3286
|
this.advisoryBuffer.push({
|
|
3124
3287
|
code: HealingAdvisory_1.AdvisoryCode.MANUAL_REVIEW_TEMPLATE_COMPLEX,
|
|
3125
|
-
message: `Function template return requires structural change
|
|
3288
|
+
message: `Function template return requires structural change - automated patch skipped.`,
|
|
3126
3289
|
affectedFile: fnAnalysis.functionFile.getFilePath(),
|
|
3127
3290
|
affectedLine: fnAnalysis.functionLine - 1,
|
|
3128
3291
|
suggestedAction: "Manually update the function's template literal.",
|
|
@@ -3132,11 +3295,11 @@ class ASTSourceUpdater {
|
|
|
3132
3295
|
}
|
|
3133
3296
|
const lineUpdated = fnAnalysis.templateNode.getStartLineNumber() - 1;
|
|
3134
3297
|
fnAnalysis.templateNode.replaceWithText(patchResult.patchedTemplate);
|
|
3135
|
-
fnAnalysis.functionFile
|
|
3298
|
+
this._persistChanges(fnAnalysis.functionFile);
|
|
3136
3299
|
if (!this.validateFileSyntax(fnAnalysis.functionFile)) {
|
|
3137
3300
|
return {
|
|
3138
3301
|
success: false,
|
|
3139
|
-
reason: "post-mutation syntax validation failed
|
|
3302
|
+
reason: "post-mutation syntax validation failed - reverted",
|
|
3140
3303
|
strategy: "semantic-call-expression",
|
|
3141
3304
|
};
|
|
3142
3305
|
}
|
|
@@ -3146,7 +3309,10 @@ class ASTSourceUpdater {
|
|
|
3146
3309
|
strategy: "semantic-call-expression",
|
|
3147
3310
|
lineUpdated,
|
|
3148
3311
|
healedLocatorString: newSelector,
|
|
3149
|
-
definitionSite: {
|
|
3312
|
+
definitionSite: {
|
|
3313
|
+
file: fnAnalysis.functionFile.getFilePath(),
|
|
3314
|
+
line: lineUpdated,
|
|
3315
|
+
},
|
|
3150
3316
|
};
|
|
3151
3317
|
}
|
|
3152
3318
|
return null;
|
|
@@ -3162,11 +3328,13 @@ class ASTSourceUpdater {
|
|
|
3162
3328
|
return !ALLOWED_CODES.has(d.getCode());
|
|
3163
3329
|
});
|
|
3164
3330
|
if (syntaxErrors.length > 0) {
|
|
3165
|
-
const msgs = syntaxErrors
|
|
3166
|
-
|
|
3331
|
+
const msgs = syntaxErrors
|
|
3332
|
+
.map((d) => String(d.getMessageText()))
|
|
3333
|
+
.join("; ");
|
|
3334
|
+
console.warn(`[SemanticDispatch] ⚠️ syntax errors after mutation: ${msgs} - reverting`);
|
|
3167
3335
|
this.advisoryBuffer.push({
|
|
3168
3336
|
code: HealingAdvisory_1.AdvisoryCode.POST_MUTATION_TYPE_ERROR,
|
|
3169
|
-
message: `Post-mutation type errors detected
|
|
3337
|
+
message: `Post-mutation type errors detected - mutation reverted. Errors: ${msgs}`,
|
|
3170
3338
|
affectedFile: sourceFile.getFilePath(),
|
|
3171
3339
|
suggestedAction: "Manually inspect and fix the type errors introduced by this heal.",
|
|
3172
3340
|
});
|