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
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Runs 5 sequential guards before any AST mutation occurs:
|
|
8
8
|
//
|
|
9
|
-
// [1] SchemaValidator
|
|
10
|
-
// [2] TypeFlowValidator
|
|
11
|
-
// [3] ReceiverValidator
|
|
12
|
-
// [4] PlacementValidator
|
|
13
|
-
// [5] SemanticUpgrader
|
|
9
|
+
// [1] SchemaValidator - required fields, no unknown segment types
|
|
10
|
+
// [2] TypeFlowValidator - tracks Locator|FrameLocator state
|
|
11
|
+
// [3] ReceiverValidator - root must match original chain's receiver
|
|
12
|
+
// [4] PlacementValidator - filter() / nth() / first() only after Locator
|
|
13
|
+
// [5] SemanticUpgrader - upgrades flat CSS to semantic where possible
|
|
14
14
|
//
|
|
15
15
|
// Each guard returns a ValidationResult. The pipeline short-circuits on
|
|
16
16
|
// the first REJECT, falling back to the original flat-CSS strategy.
|
|
@@ -139,7 +139,7 @@ const VALID_ARIA_ROLES = new Set([
|
|
|
139
139
|
"generic",
|
|
140
140
|
]);
|
|
141
141
|
// ─────────────────────────────────────────────────────────────────
|
|
142
|
-
// GUARD 1
|
|
142
|
+
// GUARD 1 - Schema Validator
|
|
143
143
|
// ─────────────────────────────────────────────────────────────────
|
|
144
144
|
function schemaValidate(segments) {
|
|
145
145
|
if (!Array.isArray(segments) || segments.length === 0) {
|
|
@@ -226,7 +226,7 @@ function schemaValidate(segments) {
|
|
|
226
226
|
return { valid: true };
|
|
227
227
|
}
|
|
228
228
|
// ─────────────────────────────────────────────────────────────────
|
|
229
|
-
// GUARD 2
|
|
229
|
+
// GUARD 2 - Type Flow Validator
|
|
230
230
|
//
|
|
231
231
|
// Simulates the chain execution, tracking whether the current
|
|
232
232
|
// expression is a Locator, FrameLocator, or Page/unknown.
|
|
@@ -266,13 +266,13 @@ function typeFlowValidate(segments, rootType) {
|
|
|
266
266
|
if (currentType === "FrameLocator") {
|
|
267
267
|
return {
|
|
268
268
|
valid: false,
|
|
269
|
-
reason: "chain ends on FrameLocator
|
|
269
|
+
reason: "chain ends on FrameLocator - assertions require a Locator at the end",
|
|
270
270
|
};
|
|
271
271
|
}
|
|
272
272
|
return { valid: true };
|
|
273
273
|
}
|
|
274
274
|
// ─────────────────────────────────────────────────────────────────
|
|
275
|
-
// GUARD 3
|
|
275
|
+
// GUARD 3 - Receiver Validator
|
|
276
276
|
//
|
|
277
277
|
// The first segment's effective receiver must be compatible with
|
|
278
278
|
// the original code's root receiver. Prevents `page.getByRole()`
|
|
@@ -284,14 +284,14 @@ function receiverValidate(segments, originalRootReceiver, proposedRootReceiver)
|
|
|
284
284
|
// If both are "page" or both are the same variable, we're fine
|
|
285
285
|
if (orig === proposed)
|
|
286
286
|
return { valid: true };
|
|
287
|
-
// "page" is a global receiver
|
|
287
|
+
// "page" is a global receiver - the AI sometimes proposes it even when
|
|
288
288
|
// the original chain was scoped. This is only acceptable if the original
|
|
289
289
|
// chain itself started from "page".
|
|
290
290
|
const PAGE_RECEIVERS = new Set(["page", "context", "browser"]);
|
|
291
291
|
if (PAGE_RECEIVERS.has(proposed) && !PAGE_RECEIVERS.has(orig)) {
|
|
292
292
|
return {
|
|
293
293
|
valid: false,
|
|
294
|
-
reason: `receiver mismatch: original chain rooted at "${orig}" but proposed chain starts from "${proposed}"
|
|
294
|
+
reason: `receiver mismatch: original chain rooted at "${orig}" but proposed chain starts from "${proposed}" - this would change element scope`,
|
|
295
295
|
};
|
|
296
296
|
}
|
|
297
297
|
// If the original root is a local variable and proposed is different,
|
|
@@ -299,14 +299,14 @@ function receiverValidate(segments, originalRootReceiver, proposedRootReceiver)
|
|
|
299
299
|
// We allow this since sometimes variables are renamed but semantically
|
|
300
300
|
// equivalent.
|
|
301
301
|
if (!PAGE_RECEIVERS.has(orig) && !PAGE_RECEIVERS.has(proposed)) {
|
|
302
|
-
// Both are custom variables
|
|
303
|
-
console.warn(`[ChainValidator] ⚠️ Receiver mismatch: "${orig}" vs "${proposed}"
|
|
302
|
+
// Both are custom variables - trust the AI's context
|
|
303
|
+
console.warn(`[ChainValidator] ⚠️ Receiver mismatch: "${orig}" vs "${proposed}" - proceeding cautiously`);
|
|
304
304
|
return { valid: true };
|
|
305
305
|
}
|
|
306
306
|
return { valid: true };
|
|
307
307
|
}
|
|
308
308
|
// ─────────────────────────────────────────────────────────────────
|
|
309
|
-
// GUARD 4
|
|
309
|
+
// GUARD 4 - Placement Validator
|
|
310
310
|
//
|
|
311
311
|
// Validates placement-sensitive rules:
|
|
312
312
|
// - filter/first/last/nth must follow a Locator-producing segment
|
|
@@ -322,7 +322,7 @@ function placementValidate(segments) {
|
|
|
322
322
|
if (prev.type === "frameLocator") {
|
|
323
323
|
return {
|
|
324
324
|
valid: false,
|
|
325
|
-
reason: `segment[${i}] .${seg.type}() placed immediately after frameLocator()
|
|
325
|
+
reason: `segment[${i}] .${seg.type}() placed immediately after frameLocator() - invalid`,
|
|
326
326
|
};
|
|
327
327
|
}
|
|
328
328
|
}
|
|
@@ -335,7 +335,7 @@ function placementValidate(segments) {
|
|
|
335
335
|
if (!hasAnyPredicate) {
|
|
336
336
|
return {
|
|
337
337
|
valid: false,
|
|
338
|
-
reason: `segment[${i}] filter() has all-empty predicates
|
|
338
|
+
reason: `segment[${i}] filter() has all-empty predicates - would be a no-op`,
|
|
339
339
|
};
|
|
340
340
|
}
|
|
341
341
|
}
|
|
@@ -343,16 +343,16 @@ function placementValidate(segments) {
|
|
|
343
343
|
return { valid: true };
|
|
344
344
|
}
|
|
345
345
|
// ─────────────────────────────────────────────────────────────────
|
|
346
|
-
// GUARD 5
|
|
346
|
+
// GUARD 5 - Semantic Upgrader
|
|
347
347
|
//
|
|
348
348
|
// Opportunistically upgrades flat CSS locator segments to semantic
|
|
349
349
|
// equivalents following Playwright's priority hierarchy:
|
|
350
350
|
// 1. getByTestId (data-testid)
|
|
351
351
|
// 2. getByRole (aria role inference from CSS)
|
|
352
352
|
// 3. getByLabel (form elements)
|
|
353
|
-
// 4. locator (CSS
|
|
353
|
+
// 4. locator (CSS - unchanged)
|
|
354
354
|
//
|
|
355
|
-
// This guard NEVER rejects
|
|
355
|
+
// This guard NEVER rejects - it only upgrades and returns the
|
|
356
356
|
// possibly-improved segments.
|
|
357
357
|
// ─────────────────────────────────────────────────────────────────
|
|
358
358
|
const ROLE_BY_TAG = {
|
|
@@ -396,12 +396,12 @@ function tryUpgradeToSemantic(seg) {
|
|
|
396
396
|
return { type: "getByRole", role };
|
|
397
397
|
}
|
|
398
398
|
}
|
|
399
|
-
// Priority 3b: button[type="submit"] or button.classname
|
|
399
|
+
// Priority 3b: button[type="submit"] or button.classname - still a button role
|
|
400
400
|
const buttonMatch = css.match(/^button(?:[.[#][^\s]*)?$/);
|
|
401
401
|
if (buttonMatch) {
|
|
402
402
|
return { type: "getByRole", role: "button" };
|
|
403
403
|
}
|
|
404
|
-
// Priority 4: has-text()
|
|
404
|
+
// Priority 4: has-text() - convert to getByText
|
|
405
405
|
const hasTextMatch = css.match(/^.*:has-text\(["']([^"']+)["']\)$/);
|
|
406
406
|
if (hasTextMatch) {
|
|
407
407
|
return { type: "getByText", text: hasTextMatch[1] };
|
|
@@ -506,7 +506,7 @@ class ChainValidator {
|
|
|
506
506
|
// Guard 2: Type flow
|
|
507
507
|
const effectiveRootType = rootType === "unknown" ? "Locator" : rootType;
|
|
508
508
|
// For Page-level (page.xxx), the root is not yet a Locator/FrameLocator
|
|
509
|
-
//
|
|
509
|
+
// - we treat the first segment as the producer.
|
|
510
510
|
const isPageReceiver = originalRootReceiver === "page" ||
|
|
511
511
|
originalRootReceiver === "context" ||
|
|
512
512
|
originalRootReceiver === "browser";
|
|
@@ -45,12 +45,12 @@ const InitializerUpdater_js_1 = require("./InitializerUpdater.js");
|
|
|
45
45
|
// then delegates mutation to InitializerUpdater.
|
|
46
46
|
//
|
|
47
47
|
// Entry point: CrossFileHealer.heal()
|
|
48
|
-
// modulePath
|
|
49
|
-
// symbolName
|
|
50
|
-
// oldValue
|
|
51
|
-
// newValue
|
|
52
|
-
// callerFilePath
|
|
53
|
-
// project
|
|
48
|
+
// modulePath - the import specifier string (e.g. "./constants")
|
|
49
|
+
// symbolName - the exported identifier name (e.g. "SHARED_SEL")
|
|
50
|
+
// oldValue - the current string value (used for verification)
|
|
51
|
+
// newValue - the replacement value
|
|
52
|
+
// callerFilePath - absolute path of the file containing the import
|
|
53
|
+
// project - the shared ASTSourceUpdater Project instance
|
|
54
54
|
// ═══════════════════════════════════════════════════════════════════
|
|
55
55
|
const DEPTH_GUARD = 3;
|
|
56
56
|
class CrossFileHealer {
|
|
@@ -91,7 +91,7 @@ class CrossFileHealer {
|
|
|
91
91
|
if (!traceResult.found) {
|
|
92
92
|
// Follow re-export chains (barrel files).
|
|
93
93
|
if (traceResult.crossFile) {
|
|
94
|
-
console.log(`[CrossFileHealer] 🔄 "${symbolName}" re-exported from "${traceResult.modulePath}"
|
|
94
|
+
console.log(`[CrossFileHealer] 🔄 "${symbolName}" re-exported from "${traceResult.modulePath}" - following chain`);
|
|
95
95
|
return CrossFileHealer.heal(traceResult.modulePath, traceResult.symbolName, newValue, externalPath, project, depth + 1);
|
|
96
96
|
}
|
|
97
97
|
return {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Node, Block } from "ts-morph";
|
|
2
|
+
import { AnchorKey, SourceSpan } from "./AnchorResolver";
|
|
3
|
+
import { TraceTarget } from "./TraceBackEngine";
|
|
4
|
+
export type DecisionStrategy = "MUTATE_IN_PLACE" | "FORK_AT_TEST" | "PROMPT_INTERACTIVE";
|
|
5
|
+
export interface Edit {
|
|
6
|
+
kind: "replace" | "insert";
|
|
7
|
+
span: SourceSpan;
|
|
8
|
+
replacement: string;
|
|
9
|
+
}
|
|
10
|
+
export interface Decision {
|
|
11
|
+
strategy: DecisionStrategy;
|
|
12
|
+
reason: string;
|
|
13
|
+
/** Read-reference count to the declaration (the blast radius). */
|
|
14
|
+
blastRadius: number;
|
|
15
|
+
/** Distinct test/it blocks referencing the declaration. */
|
|
16
|
+
consumerTests: number;
|
|
17
|
+
/** Ordered edits. Apply via applyEdits (high→low offset). */
|
|
18
|
+
edits: Edit[];
|
|
19
|
+
/** When forking, the (possibly fresh) shadow variable name. */
|
|
20
|
+
forkVarName?: string;
|
|
21
|
+
/** 0..1 routing confidence (§5.3). */
|
|
22
|
+
confidence: number;
|
|
23
|
+
anchor: AnchorKey;
|
|
24
|
+
}
|
|
25
|
+
export interface DecideInput {
|
|
26
|
+
target: TraceTarget;
|
|
27
|
+
/** A node inside the FAILING statement — used to locate the test block. */
|
|
28
|
+
failingCallSite: Node;
|
|
29
|
+
/** Healed locator EXPRESSION text, e.g. `page.locator(".btn-v2")`. */
|
|
30
|
+
newLocatorExpr: string;
|
|
31
|
+
/** Runtime: only the failing test needs the new selector. */
|
|
32
|
+
contextSpecific: boolean;
|
|
33
|
+
/** Runtime: the selector rotted for ALL consumers (proven global). */
|
|
34
|
+
globalCorrect: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare class DecisionEngine {
|
|
37
|
+
static decide(input: DecideInput): Decision;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The Block of the nearest enclosing `test(...)` / `it(...)` callback. Hooks
|
|
41
|
+
* (`beforeEach`, etc.) are intentionally NOT matched — we fork into the test
|
|
42
|
+
* that failed, never into the shared hook.
|
|
43
|
+
*/
|
|
44
|
+
export declare function findEnclosingTestBlock(node: Node): Block | null;
|
|
45
|
+
/**
|
|
46
|
+
* Apply edits to `original`, highest offset first so earlier offsets stay
|
|
47
|
+
* valid (the reverse-offset rule from the concurrency design §3.3.4).
|
|
48
|
+
* Throws on overlapping replace spans — never produces torn text.
|
|
49
|
+
*/
|
|
50
|
+
export declare function applyEdits(original: string, edits: Edit[]): string;
|
|
51
|
+
//# sourceMappingURL=DecisionEngine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DecisionEngine.d.ts","sourceRoot":"","sources":["../../src/services/DecisionEngine.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,IAAI,EAAc,KAAK,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEzD,OAAO,EACL,WAAW,EAGZ,MAAM,mBAAmB,CAAC;AAM3B,MAAM,MAAM,gBAAgB,GACxB,iBAAiB,GACjB,cAAc,GACd,oBAAoB,CAAC;AAEzB,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC3B,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,SAAS,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,WAAW,CAAC;IACpB,2EAA2E;IAC3E,eAAe,EAAE,IAAI,CAAC;IACtB,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,eAAe,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,aAAa,EAAE,OAAO,CAAC;CACxB;AAMD,qBAAa,cAAc;IACzB,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,QAAQ;CAuD5C;AAqJD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,CAc/D;AAwCD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,CAsBlE"}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/services/DecisionEngine.ts
|
|
3
|
+
//
|
|
4
|
+
// Ticket #2 — Stage 3: The Decision Tree & Multi-Consumer Forking.
|
|
5
|
+
//
|
|
6
|
+
// Final semantic phase. Given a trace-back TARGET (Stage 2) + the proposed
|
|
7
|
+
// healed locator expression + runtime evidence, decide HOW to write it:
|
|
8
|
+
//
|
|
9
|
+
// refsToDecl <= 1 ............................. MUTATE_IN_PLACE
|
|
10
|
+
// refsToDecl > 1 & globalCorrect ............. MUTATE_IN_PLACE (all rot)
|
|
11
|
+
// refsToDecl > 1 & contextSpecific ........... FORK_AT_TEST
|
|
12
|
+
// refsToDecl > 1 & scope undeterminable ...... PROMPT_INTERACTIVE
|
|
13
|
+
//
|
|
14
|
+
// FORK_AT_TEST injects a collision-safe shadowing `const <name> = <expr>;`
|
|
15
|
+
// at the top of the FAILING test's block so the other consumers of the
|
|
16
|
+
// shared declaration (e.g. a `beforeEach` locator used by 4 other passing
|
|
17
|
+
// tests) are left untouched. Same-name shadow keeps the call-site intact;
|
|
18
|
+
// a real name collision falls back to a fresh name + a call-site rewrite.
|
|
19
|
+
//
|
|
20
|
+
// Reference census is delegated to BlastRadiusAnalyzer (cross-file accurate
|
|
21
|
+
// via the ts-morph language service) — we do not re-implement findReferences.
|
|
22
|
+
//
|
|
23
|
+
// PURE: produces an edit plan; never writes to disk.
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.DecisionEngine = void 0;
|
|
26
|
+
exports.findEnclosingTestBlock = findEnclosingTestBlock;
|
|
27
|
+
exports.applyEdits = applyEdits;
|
|
28
|
+
const ts_morph_1 = require("ts-morph");
|
|
29
|
+
const BlastRadiusAnalyzer_1 = require("./BlastRadiusAnalyzer");
|
|
30
|
+
const TraceBackEngine_1 = require("./TraceBackEngine");
|
|
31
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
32
|
+
// DecisionEngine
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
34
|
+
class DecisionEngine {
|
|
35
|
+
static decide(input) {
|
|
36
|
+
const { target, newLocatorExpr } = input;
|
|
37
|
+
// Inline call-site or non-declaration target: the call-site is unique,
|
|
38
|
+
// there is nothing to share → mutate in place.
|
|
39
|
+
if (target.declKind === "InlineCallSite" || !target.declaration) {
|
|
40
|
+
return mutateInPlace(target, newLocatorExpr, 1, 1, "inline call-site");
|
|
41
|
+
}
|
|
42
|
+
// Reference census (delegated, cross-file accurate).
|
|
43
|
+
const { totalReferences, consumerTests } = census(target);
|
|
44
|
+
// ── refs <= 1 → safe global mutation ────────────────────────────
|
|
45
|
+
if (totalReferences <= 1) {
|
|
46
|
+
return mutateInPlace(target, newLocatorExpr, totalReferences, consumerTests, `single consumer (refs=${totalReferences})`);
|
|
47
|
+
}
|
|
48
|
+
// ── refs > 1 → multi-consumer ───────────────────────────────────
|
|
49
|
+
if (input.globalCorrect) {
|
|
50
|
+
return mutateInPlace(target, newLocatorExpr, totalReferences, consumerTests, `multi-consumer but selector rot is global (refs=${totalReferences})`, 0.85);
|
|
51
|
+
}
|
|
52
|
+
if (input.contextSpecific) {
|
|
53
|
+
const fork = buildFork(input, totalReferences, consumerTests);
|
|
54
|
+
if (fork)
|
|
55
|
+
return fork;
|
|
56
|
+
// Could not locate a test block to fork into → escalate.
|
|
57
|
+
return promptInteractive(target, totalReferences, consumerTests, "context-specific fix but no enclosing test block to fork into");
|
|
58
|
+
}
|
|
59
|
+
// Scope undeterminable → never silently break the other consumers.
|
|
60
|
+
return promptInteractive(target, totalReferences, consumerTests, `multi-consumer (refs=${totalReferences}) and scope undeterminable — global-vs-fork needs a human`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.DecisionEngine = DecisionEngine;
|
|
64
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
65
|
+
// REFERENCE CENSUS
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
67
|
+
function census(target) {
|
|
68
|
+
const decl = target.declaration;
|
|
69
|
+
try {
|
|
70
|
+
const report = BlastRadiusAnalyzer_1.BlastRadiusAnalyzer.analyze(decl, decl.getSourceFile(), decl.getProject());
|
|
71
|
+
return {
|
|
72
|
+
totalReferences: report.totalReferences,
|
|
73
|
+
consumerTests: report.testFunctionCount,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Census failure must not crash the pipeline; treat as single-consumer
|
|
78
|
+
// (the conservative MUTATE_IN_PLACE path the caller can still gate).
|
|
79
|
+
return { totalReferences: 1, consumerTests: 1 };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
83
|
+
// PLAN BUILDERS
|
|
84
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
85
|
+
function mutateInPlace(target, newLocatorExpr, blastRadius, consumerTests, reason, confidence = 0.9) {
|
|
86
|
+
return {
|
|
87
|
+
strategy: "MUTATE_IN_PLACE",
|
|
88
|
+
reason,
|
|
89
|
+
blastRadius,
|
|
90
|
+
consumerTests,
|
|
91
|
+
confidence,
|
|
92
|
+
anchor: target.anchor,
|
|
93
|
+
edits: [
|
|
94
|
+
{
|
|
95
|
+
kind: "replace",
|
|
96
|
+
span: {
|
|
97
|
+
start: target.targetNode.getStart(),
|
|
98
|
+
end: target.targetNode.getEnd(),
|
|
99
|
+
},
|
|
100
|
+
replacement: newLocatorExpr,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function promptInteractive(target, blastRadius, consumerTests, reason) {
|
|
106
|
+
return {
|
|
107
|
+
strategy: "PROMPT_INTERACTIVE",
|
|
108
|
+
reason,
|
|
109
|
+
blastRadius,
|
|
110
|
+
consumerTests,
|
|
111
|
+
confidence: 0.4,
|
|
112
|
+
anchor: target.anchor,
|
|
113
|
+
edits: [], // no auto-edit; the CLI prompts the user
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function buildFork(input, blastRadius, consumerTests) {
|
|
117
|
+
const { target, failingCallSite, newLocatorExpr } = input;
|
|
118
|
+
const testBlock = findEnclosingTestBlock(failingCallSite);
|
|
119
|
+
if (!testBlock)
|
|
120
|
+
return null;
|
|
121
|
+
const baseName = target.symbolName ?? "healedLocator";
|
|
122
|
+
// Collision-safe shadow naming. A plain block-scoped `const` of the SAME
|
|
123
|
+
// name legally shadows the outer (beforeEach/describe) declaration and the
|
|
124
|
+
// call-site needs no rewrite. Only when the failing test block ALREADY
|
|
125
|
+
// declares that name locally do we need a fresh name + call-site rewrite.
|
|
126
|
+
const localNames = collectBlockLocalNames(testBlock);
|
|
127
|
+
let forkVarName = baseName;
|
|
128
|
+
let needsCallSiteRewrite = false;
|
|
129
|
+
if (localNames.has(baseName)) {
|
|
130
|
+
forkVarName = freshName(baseName, localNames);
|
|
131
|
+
needsCallSiteRewrite = true;
|
|
132
|
+
}
|
|
133
|
+
const edits = [];
|
|
134
|
+
// 1. Insert the shadow declaration at the top of the test block.
|
|
135
|
+
const stmts = testBlock.getStatements();
|
|
136
|
+
const indent = stmts[0]?.getIndentationText() ?? " ";
|
|
137
|
+
const insertPos = stmts[0]
|
|
138
|
+
? stmts[0].getStart()
|
|
139
|
+
: testBlock.getStart() + 1; // just after the `{`
|
|
140
|
+
const insertText = stmts[0]
|
|
141
|
+
? `const ${forkVarName} = ${newLocatorExpr};\n${indent}`
|
|
142
|
+
: `\n${indent}const ${forkVarName} = ${newLocatorExpr};\n`;
|
|
143
|
+
edits.push({
|
|
144
|
+
kind: "insert",
|
|
145
|
+
span: { start: insertPos, end: insertPos },
|
|
146
|
+
replacement: insertText,
|
|
147
|
+
});
|
|
148
|
+
// 2. If we had to rename, rewrite the failing call-site receiver.
|
|
149
|
+
if (needsCallSiteRewrite) {
|
|
150
|
+
const top = (0, TraceBackEngine_1.climbToTopCall)(failingCallSite);
|
|
151
|
+
if (top) {
|
|
152
|
+
const { leftmost } = (0, TraceBackEngine_1.getLeftmostReceiver)(top);
|
|
153
|
+
if (ts_morph_1.Node.isIdentifier(leftmost) && leftmost.getText() === baseName) {
|
|
154
|
+
edits.push({
|
|
155
|
+
kind: "replace",
|
|
156
|
+
span: { start: leftmost.getStart(), end: leftmost.getEnd() },
|
|
157
|
+
replacement: forkVarName,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
strategy: "FORK_AT_TEST",
|
|
164
|
+
reason: `multi-consumer (refs=${blastRadius}, tests=${consumerTests}); context-specific fix forked into the failing test as "${forkVarName}"`,
|
|
165
|
+
blastRadius,
|
|
166
|
+
consumerTests,
|
|
167
|
+
confidence: 0.75,
|
|
168
|
+
forkVarName,
|
|
169
|
+
anchor: target.anchor,
|
|
170
|
+
edits,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
174
|
+
// TEST-BLOCK / SCOPE HELPERS
|
|
175
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
176
|
+
/**
|
|
177
|
+
* The Block of the nearest enclosing `test(...)` / `it(...)` callback. Hooks
|
|
178
|
+
* (`beforeEach`, etc.) are intentionally NOT matched — we fork into the test
|
|
179
|
+
* that failed, never into the shared hook.
|
|
180
|
+
*/
|
|
181
|
+
function findEnclosingTestBlock(node) {
|
|
182
|
+
let cur = node;
|
|
183
|
+
while (cur) {
|
|
184
|
+
if (ts_morph_1.Node.isCallExpression(cur) && isTestCall(cur)) {
|
|
185
|
+
for (const arg of cur.getArguments()) {
|
|
186
|
+
if (ts_morph_1.Node.isArrowFunction(arg) || ts_morph_1.Node.isFunctionExpression(arg)) {
|
|
187
|
+
const body = arg.getBody();
|
|
188
|
+
if (body && ts_morph_1.Node.isBlock(body))
|
|
189
|
+
return body;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
cur = cur.getParent();
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
function isTestCall(call) {
|
|
198
|
+
if (!ts_morph_1.Node.isCallExpression(call))
|
|
199
|
+
return false;
|
|
200
|
+
const expr = call.getExpression();
|
|
201
|
+
const base = ts_morph_1.Node.isPropertyAccessExpression(expr)
|
|
202
|
+
? expr.getExpression().getText()
|
|
203
|
+
: expr.getText();
|
|
204
|
+
return base === "test" || base === "it";
|
|
205
|
+
}
|
|
206
|
+
/** Names declared LOCALLY in this block (top-level var statements + fns). */
|
|
207
|
+
function collectBlockLocalNames(block) {
|
|
208
|
+
const names = new Set();
|
|
209
|
+
for (const stmt of block.getStatements()) {
|
|
210
|
+
if (ts_morph_1.Node.isVariableStatement(stmt)) {
|
|
211
|
+
for (const d of stmt.getDeclarations()) {
|
|
212
|
+
const n = d.getNameNode();
|
|
213
|
+
if (ts_morph_1.Node.isIdentifier(n))
|
|
214
|
+
names.add(n.getText());
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (ts_morph_1.Node.isFunctionDeclaration(stmt)) {
|
|
218
|
+
const nm = stmt.getName();
|
|
219
|
+
if (nm)
|
|
220
|
+
names.add(nm);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return names;
|
|
224
|
+
}
|
|
225
|
+
function freshName(base, taken) {
|
|
226
|
+
const candidate = `${base}Healed`;
|
|
227
|
+
if (!taken.has(candidate))
|
|
228
|
+
return candidate;
|
|
229
|
+
let i = 2;
|
|
230
|
+
while (taken.has(`${candidate}${i}`))
|
|
231
|
+
i++;
|
|
232
|
+
return `${candidate}${i}`;
|
|
233
|
+
}
|
|
234
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
235
|
+
// PURE EDIT APPLICATION (used by callers + tests)
|
|
236
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
237
|
+
/**
|
|
238
|
+
* Apply edits to `original`, highest offset first so earlier offsets stay
|
|
239
|
+
* valid (the reverse-offset rule from the concurrency design §3.3.4).
|
|
240
|
+
* Throws on overlapping replace spans — never produces torn text.
|
|
241
|
+
*/
|
|
242
|
+
function applyEdits(original, edits) {
|
|
243
|
+
const sorted = [...edits].sort((a, b) => b.span.start - a.span.start);
|
|
244
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
245
|
+
const prev = sorted[i - 1];
|
|
246
|
+
const cur = sorted[i];
|
|
247
|
+
// prev.start >= cur.start (desc). Overlap if cur.end > prev.start AND
|
|
248
|
+
// both are replacements (inserts are zero-width and may share a point).
|
|
249
|
+
if (cur.span.end > prev.span.start &&
|
|
250
|
+
prev.kind === "replace" &&
|
|
251
|
+
cur.kind === "replace") {
|
|
252
|
+
throw new Error(`[DecisionEngine] overlapping edits at ${cur.span.start}..${cur.span.end} and ${prev.span.start}..${prev.span.end}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
let text = original;
|
|
256
|
+
for (const e of sorted) {
|
|
257
|
+
text = text.slice(0, e.span.start) + e.replacement + text.slice(e.span.end);
|
|
258
|
+
}
|
|
259
|
+
return text;
|
|
260
|
+
}
|
|
@@ -3,7 +3,7 @@ export type TraceResult = {
|
|
|
3
3
|
found: true;
|
|
4
4
|
targetNode: Node;
|
|
5
5
|
targetFile: SourceFile;
|
|
6
|
-
/** The enclosing VariableDeclaration or PropertyAssignment
|
|
6
|
+
/** The enclosing VariableDeclaration or PropertyAssignment - used by BlastRadiusAnalyzer. */
|
|
7
7
|
declarationNode?: Node;
|
|
8
8
|
} | {
|
|
9
9
|
found: false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DefinitionTracer.d.ts","sourceRoot":"","sources":["../../src/services/DefinitionTracer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,UAAU,EAKV,aAAa,EACb,6BAA6B,EAC7B,kBAAkB,EAGnB,MAAM,UAAU,CAAC;AAMlB,MAAM,MAAM,WAAW,GACnB;IACE,KAAK,EAAE,IAAI,CAAC;IACZ,UAAU,EAAE,IAAI,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;IACvB,6FAA6F;IAC7F,eAAe,CAAC,EAAE,IAAI,CAAC;CACxB,GACD;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACzE;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAC1B;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,YAAY,EAAE,kBAAkB,CAAC;IACjC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,YAAY,EAAE,UAAU,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB,GACD;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,WAAW,EAAE,aAAa,GAAG,6BAA6B,CAAC;IAC3D,YAAY,EAAE,UAAU,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB,GACD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAcxC,qBAAa,gBAAgB;IAK3B,MAAM,CAAC,eAAe,CACpB,cAAc,EAAE,IAAI,EACpB,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,MAAM,EAClB,KAAK,SAAI,GACR,WAAW;
|
|
1
|
+
{"version":3,"file":"DefinitionTracer.d.ts","sourceRoot":"","sources":["../../src/services/DefinitionTracer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,UAAU,EAKV,aAAa,EACb,6BAA6B,EAC7B,kBAAkB,EAGnB,MAAM,UAAU,CAAC;AAMlB,MAAM,MAAM,WAAW,GACnB;IACE,KAAK,EAAE,IAAI,CAAC;IACZ,UAAU,EAAE,IAAI,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;IACvB,6FAA6F;IAC7F,eAAe,CAAC,EAAE,IAAI,CAAC;CACxB,GACD;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACzE;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAC1B;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,YAAY,EAAE,kBAAkB,CAAC;IACjC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,YAAY,EAAE,UAAU,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB,GACD;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,WAAW,EAAE,aAAa,GAAG,6BAA6B,CAAC;IAC3D,YAAY,EAAE,UAAU,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB,GACD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAcxC,qBAAa,gBAAgB;IAK3B,MAAM,CAAC,eAAe,CACpB,cAAc,EAAE,IAAI,EACpB,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,MAAM,EAClB,KAAK,SAAI,GACR,WAAW;IA0Fd,MAAM,CAAC,mBAAmB,CACxB,cAAc,EAAE,IAAI,EACpB,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,MAAM,GACjB,WAAW;IA+Ed,MAAM,CAAC,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,WAAW;CAkC5E;AASD,qBAAa,wBAAwB;IACnC,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,GAAG,kBAAkB;CAmF3E"}
|
|
@@ -5,8 +5,8 @@ const ts_morph_1 = require("ts-morph");
|
|
|
5
5
|
// ═══════════════════════════════════════════════════════════════════
|
|
6
6
|
// DefinitionTracer
|
|
7
7
|
//
|
|
8
|
-
// T-5: traceIdentifier
|
|
9
|
-
// T-6: tracePropertyAccess
|
|
8
|
+
// T-5: traceIdentifier - follow bare variable name to its initializer.
|
|
9
|
+
// T-6: tracePropertyAccess - follow OBJECT.prop to the property value.
|
|
10
10
|
//
|
|
11
11
|
// In-file first; signals crossFile when symbol comes from an import.
|
|
12
12
|
// Never does I/O beyond what ts-morph provides on the given SourceFile.
|
|
@@ -42,9 +42,14 @@ class DefinitionTracer {
|
|
|
42
42
|
}
|
|
43
43
|
console.log(`[DefinitionTracer] ✅ traceIdentifier: "${symbolName}" → ` +
|
|
44
44
|
`${init.getKindName()} @ ${sourceFile.getBaseName()}:${init.getStartLineNumber()}`);
|
|
45
|
-
return {
|
|
45
|
+
return {
|
|
46
|
+
found: true,
|
|
47
|
+
targetNode: init,
|
|
48
|
+
targetFile: sourceFile,
|
|
49
|
+
declarationNode: decl,
|
|
50
|
+
};
|
|
46
51
|
}
|
|
47
|
-
// 2. Symbol not in file
|
|
52
|
+
// 2. Symbol not in file - check import declarations.
|
|
48
53
|
const imported = findImportedSymbol(sourceFile, symbolName);
|
|
49
54
|
if (imported) {
|
|
50
55
|
console.log(`[DefinitionTracer] 🌐 traceIdentifier: "${symbolName}" comes from "${imported.modulePath}" (cross-file)`);
|
|
@@ -71,7 +76,7 @@ class DefinitionTracer {
|
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
catch {
|
|
74
|
-
// Language service unavailable
|
|
79
|
+
// Language service unavailable - not an error.
|
|
75
80
|
}
|
|
76
81
|
return {
|
|
77
82
|
found: false,
|
|
@@ -115,7 +120,7 @@ class DefinitionTracer {
|
|
|
115
120
|
reason: `"${objectName}" initializer is not an ObjectLiteralExpression (got ${init?.getKindName() ?? "none"})`,
|
|
116
121
|
};
|
|
117
122
|
}
|
|
118
|
-
// ts-morph getProperty(name)
|
|
123
|
+
// ts-morph getProperty(name) - returns the first matching property.
|
|
119
124
|
const prop = init.getProperty(propName);
|
|
120
125
|
if (!prop || !ts_morph_1.Node.isPropertyAssignment(prop)) {
|
|
121
126
|
return {
|
|
@@ -135,10 +140,15 @@ class DefinitionTracer {
|
|
|
135
140
|
console.log(`[DefinitionTracer] ✅ tracePropertyAccess: "${objectName}.${propName}" → ` +
|
|
136
141
|
`${valueNode.getKindName()} "${valueNode.getText()}" ` +
|
|
137
142
|
`@ ${sourceFile.getBaseName()}:${valueNode.getStartLineNumber()}`);
|
|
138
|
-
return {
|
|
143
|
+
return {
|
|
144
|
+
found: true,
|
|
145
|
+
targetNode: valueNode,
|
|
146
|
+
targetFile: sourceFile,
|
|
147
|
+
declarationNode: prop,
|
|
148
|
+
};
|
|
139
149
|
}
|
|
140
150
|
// ─────────────────────────────────────────────────────────────────
|
|
141
|
-
// traceByName
|
|
151
|
+
// traceByName - used by CrossFileHealer
|
|
142
152
|
//
|
|
143
153
|
// Like traceIdentifier but takes a plain symbol name string instead
|
|
144
154
|
// of an Identifier node. Searches the entire file (no callerLine
|
|
@@ -166,7 +176,12 @@ class DefinitionTracer {
|
|
|
166
176
|
}
|
|
167
177
|
console.log(`[DefinitionTracer] ✅ traceByName: "${symbolName}" → ` +
|
|
168
178
|
`${init.getKindName()} @ ${sourceFile.getBaseName()}:${init.getStartLineNumber()}`);
|
|
169
|
-
return {
|
|
179
|
+
return {
|
|
180
|
+
found: true,
|
|
181
|
+
targetNode: init,
|
|
182
|
+
targetFile: sourceFile,
|
|
183
|
+
declarationNode: decl,
|
|
184
|
+
};
|
|
170
185
|
}
|
|
171
186
|
}
|
|
172
187
|
exports.DefinitionTracer = DefinitionTracer;
|
|
@@ -241,7 +256,7 @@ class FunctionalReturnAnalyzer {
|
|
|
241
256
|
}
|
|
242
257
|
return {
|
|
243
258
|
kind: "complex",
|
|
244
|
-
reason: `"${calleeName}" return expression is ${returnExpr.getKindName()}
|
|
259
|
+
reason: `"${calleeName}" return expression is ${returnExpr.getKindName()} - not directly healable`,
|
|
245
260
|
};
|
|
246
261
|
}
|
|
247
262
|
}
|
|
@@ -316,7 +331,7 @@ function findFunctionBody(sourceFile, fnName) {
|
|
|
316
331
|
const arrow = init;
|
|
317
332
|
const params = arrow.getParameters().map((p) => p.getName());
|
|
318
333
|
const body = arrow.getBody();
|
|
319
|
-
// Concise body: (role) => `...`
|
|
334
|
+
// Concise body: (role) => `...` - body IS the return expression.
|
|
320
335
|
if (!ts_morph_1.Node.isBlock(body)) {
|
|
321
336
|
return { returnExpr: body, params, line: decl.getStartLineNumber() };
|
|
322
337
|
}
|
|
@@ -338,13 +353,13 @@ function findFunctionBody(sourceFile, fnName) {
|
|
|
338
353
|
return null;
|
|
339
354
|
}
|
|
340
355
|
function extractReturnExpression(body) {
|
|
341
|
-
// Concise arrow body
|
|
356
|
+
// Concise arrow body - the node itself is the return value.
|
|
342
357
|
if (!ts_morph_1.Node.isBlock(body))
|
|
343
358
|
return body;
|
|
344
359
|
// Block body: search for ReturnStatement.
|
|
345
360
|
const returnStmts = body.getDescendantsOfKind(ts_morph_1.SyntaxKind.ReturnStatement);
|
|
346
361
|
if (returnStmts.length === 0)
|
|
347
362
|
return null;
|
|
348
|
-
// Use the first one
|
|
363
|
+
// Use the first one - for conditional returns we signal COMPLEX upstream.
|
|
349
364
|
return returnStmts[0].getExpression() ?? null;
|
|
350
365
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ElementSnapshotV2 } from
|
|
1
|
+
import type { ElementSnapshotV2 } from "../types/index.js";
|
|
2
2
|
export interface ValidationResult {
|
|
3
3
|
valid: boolean;
|
|
4
4
|
errors: string[];
|
|
@@ -9,7 +9,7 @@ export interface DnaSnapshotExtended extends ElementSnapshotV2 {
|
|
|
9
9
|
sourceFile?: string;
|
|
10
10
|
sourceLine?: number;
|
|
11
11
|
lastHealed?: string | null;
|
|
12
|
-
status?:
|
|
12
|
+
status?: "healed" | "broken";
|
|
13
13
|
originalSelector?: string;
|
|
14
14
|
healedSelector?: string;
|
|
15
15
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DnaEditorService.d.ts","sourceRoot":"","sources":["../../src/services/DnaEditorService.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAID,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAChD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,gBAAgB,CAAC;CAChD;
|
|
1
|
+
{"version":3,"file":"DnaEditorService.d.ts","sourceRoot":"","sources":["../../src/services/DnaEditorService.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAID,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAChD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,gBAAgB,CAAC;CAChD;AA6BD,qBAAa,gBAAiB,YAAW,iBAAiB;IAC5C,OAAO,CAAC,QAAQ,CAAC,WAAW;gBAAX,WAAW,EAAE,MAAM;IAE1C,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAgB/C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IA2CpD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa1C,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,gBAAgB;CA4H/C"}
|