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
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Node, SourceFile } from "ts-morph";
|
|
2
|
+
/**
|
|
3
|
+
* Line-number-free identity of a single AST node, stable across the
|
|
4
|
+
* read-modify-write lifecycle of a shared source file. See module header.
|
|
5
|
+
*/
|
|
6
|
+
export interface AnchorKey {
|
|
7
|
+
/** Absolute path of the file the node lives in. */
|
|
8
|
+
filePath: string;
|
|
9
|
+
/**
|
|
10
|
+
* Name of the variable / property / object-property that owns the
|
|
11
|
+
* target node (e.g. "submitBtn"). Used for content-addressed recovery
|
|
12
|
+
* when the structural index drifts. Empty string when the node has no
|
|
13
|
+
* named owner.
|
|
14
|
+
*/
|
|
15
|
+
symbolName: string;
|
|
16
|
+
/**
|
|
17
|
+
* Kind-indexed path from the SourceFile root to the node, e.g.
|
|
18
|
+
* "SyntaxList[0]/VariableStatement[2]/.../CallExpression[0]".
|
|
19
|
+
* Immune to pure line shifts.
|
|
20
|
+
*/
|
|
21
|
+
structuralPath: string;
|
|
22
|
+
/** sha256 hex of the target node's `getText()` at flag-time. */
|
|
23
|
+
nodeTextSha: string;
|
|
24
|
+
/** ts-morph SyntaxKind NAME of the target node (e.g. "CallExpression"). */
|
|
25
|
+
kind: string;
|
|
26
|
+
/**
|
|
27
|
+
* Name of the enclosing test/hook block — `test("...")` title,
|
|
28
|
+
* or hook name like "beforeEach" — used to disambiguate identical
|
|
29
|
+
* declarations across multiple test blocks (§1.3 fork detection).
|
|
30
|
+
* Undefined when the node is module-scoped.
|
|
31
|
+
*/
|
|
32
|
+
enclosingFnName?: string;
|
|
33
|
+
}
|
|
34
|
+
export type MutationStrategy = "SOURCE_MUTATE" | "FORK_AT_TEST" | "CALLSITE_PATCH" | "WARN_NONBLOCKING" | "PROMPT_INTERACTIVE";
|
|
35
|
+
/** What broke at runtime and what the healed selector should become. */
|
|
36
|
+
export interface RuntimeEvidence {
|
|
37
|
+
/** The selector chain that failed at runtime. */
|
|
38
|
+
failedSelector: string;
|
|
39
|
+
/** The healed selector the engine proposes to write. */
|
|
40
|
+
proposedSelector: string;
|
|
41
|
+
/**
|
|
42
|
+
* For chained cascades (§1.1): the narrowest selector segment that
|
|
43
|
+
* actually resolved to 0 / ambiguous elements. Undefined when unknown.
|
|
44
|
+
*/
|
|
45
|
+
brokenSegment?: string;
|
|
46
|
+
/** Element count the broken segment resolved to at fail-time, if known. */
|
|
47
|
+
resolvedCount?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Worker-emitted record. Workers NEVER write source (§3.1) — they serialise
|
|
51
|
+
* MutationProposals; the single-writer main loop resolves + applies them.
|
|
52
|
+
*/
|
|
53
|
+
export interface MutationProposal {
|
|
54
|
+
anchor: AnchorKey;
|
|
55
|
+
evidence: RuntimeEvidence;
|
|
56
|
+
/** Strategy the worker-side analysis recommends; main loop may override. */
|
|
57
|
+
strategyHint: MutationStrategy;
|
|
58
|
+
/** 0..1 confidence used for routing (§5.3). */
|
|
59
|
+
confidence: number;
|
|
60
|
+
/** Emitting worker's pid — provenance + collision diagnostics. */
|
|
61
|
+
workerPid: number;
|
|
62
|
+
/**
|
|
63
|
+
* ISO timestamp of when the proposal was created. Passed in by the caller
|
|
64
|
+
* (core stays clock-free so it is deterministic / resumable).
|
|
65
|
+
*/
|
|
66
|
+
createdAtIso: string;
|
|
67
|
+
}
|
|
68
|
+
/** Character offsets into the file text — `[start, end)`. */
|
|
69
|
+
export interface SourceSpan {
|
|
70
|
+
start: number;
|
|
71
|
+
end: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Output of the trace-back engine: a concrete, ready-to-apply edit.
|
|
75
|
+
* `targetSpan` is recomputed from the anchor at apply-time against the
|
|
76
|
+
* fresh file (see resolveAnchor) — never trusted from flag-time.
|
|
77
|
+
*/
|
|
78
|
+
export interface MutationPlan {
|
|
79
|
+
targetFile: string;
|
|
80
|
+
targetSpan: SourceSpan;
|
|
81
|
+
replacementText: string;
|
|
82
|
+
strategy: MutationStrategy;
|
|
83
|
+
confidence: number;
|
|
84
|
+
anchor: AnchorKey;
|
|
85
|
+
}
|
|
86
|
+
export type ResolveAnchorResult = {
|
|
87
|
+
status: "resolved";
|
|
88
|
+
node: Node;
|
|
89
|
+
via: "structural" | "content";
|
|
90
|
+
} | {
|
|
91
|
+
status: "stale";
|
|
92
|
+
node: Node;
|
|
93
|
+
reason: string;
|
|
94
|
+
} | {
|
|
95
|
+
status: "ambiguous";
|
|
96
|
+
matches: number;
|
|
97
|
+
reason: string;
|
|
98
|
+
} | {
|
|
99
|
+
status: "not-found";
|
|
100
|
+
reason: string;
|
|
101
|
+
};
|
|
102
|
+
/** sha256 hex of an arbitrary string. Stable across processes. */
|
|
103
|
+
export declare function sha256(text: string): string;
|
|
104
|
+
/**
|
|
105
|
+
* Build an AnchorKey for `node`. The inverse of resolveAnchor: feeding the
|
|
106
|
+
* result back into resolveAnchor against the SAME (or line-shifted) file
|
|
107
|
+
* must re-locate `node`.
|
|
108
|
+
*/
|
|
109
|
+
export declare function computeAnchor(node: Node): AnchorKey;
|
|
110
|
+
/**
|
|
111
|
+
* Dedup key for collapsing identical proposals (§3.5). Two proposals that
|
|
112
|
+
* target the same node identity in the same file share this key regardless
|
|
113
|
+
* of which worker emitted them.
|
|
114
|
+
*/
|
|
115
|
+
export declare function anchorDedupeKey(a: AnchorKey): string;
|
|
116
|
+
/**
|
|
117
|
+
* Re-locate the node an AnchorKey points to, in a freshly-parsed
|
|
118
|
+
* SourceFile. PURE — no writes, never throws.
|
|
119
|
+
*
|
|
120
|
+
* Resolution order:
|
|
121
|
+
* 1. Structural path. If it lands on a node of the recorded kind whose
|
|
122
|
+
* text sha matches → resolved("structural"). This is the fast path and
|
|
123
|
+
* the ONLY path needed for pure line shifts.
|
|
124
|
+
* 2. Content-addressed recovery. When the structural index drifted (a new
|
|
125
|
+
* sibling was inserted ahead of the target), search the file for nodes
|
|
126
|
+
* of the recorded kind whose sha matches, then narrow by symbolName and
|
|
127
|
+
* enclosingFnName. Exactly one → resolved("content"); more than one →
|
|
128
|
+
* ambiguous.
|
|
129
|
+
* 3. Stale. The structural path resolved to a node of the right kind but
|
|
130
|
+
* the bytes changed (someone edited the selector) and no content match
|
|
131
|
+
* exists → the node moved/changed under us; refuse to blind-write.
|
|
132
|
+
* 4. not-found. Nothing matches.
|
|
133
|
+
*/
|
|
134
|
+
export declare function resolveAnchor(sourceFile: SourceFile, anchor: AnchorKey): ResolveAnchorResult;
|
|
135
|
+
/**
|
|
136
|
+
* Kind-indexed path from the SourceFile root down to `node`. Each segment is
|
|
137
|
+
* `KindName[i]` where `i` is the node's index among its parent's children
|
|
138
|
+
* OF THE SAME KIND. Indexing per-kind (rather than absolute child index)
|
|
139
|
+
* keeps the path readable and resilient to trivia/token reshuffling.
|
|
140
|
+
*/
|
|
141
|
+
export declare function structuralPathOf(node: Node): string;
|
|
142
|
+
/** Walk a structuralPath from the SourceFile root. Returns null on any miss. */
|
|
143
|
+
export declare function followStructuralPath(sourceFile: SourceFile, structuralPath: string): Node | null;
|
|
144
|
+
/**
|
|
145
|
+
* Name of the nearest declaration owning `node`:
|
|
146
|
+
* const NAME = … | NAME: … (object prop) | NAME = page.locator() (POM prop).
|
|
147
|
+
* Null when there is no named owner (e.g. a bare expression statement).
|
|
148
|
+
*/
|
|
149
|
+
export declare function enclosingSymbolName(node: Node): string | null;
|
|
150
|
+
/**
|
|
151
|
+
* Name of the enclosing test/hook block, used to disambiguate identical
|
|
152
|
+
* declarations across blocks. Returns the string-literal title for
|
|
153
|
+
* `test("title", …)` / `describe`/`it`, or the hook name for
|
|
154
|
+
* `beforeEach(…)` / `afterAll(…)`. Undefined when module-scoped.
|
|
155
|
+
*/
|
|
156
|
+
export declare function enclosingFnName(node: Node): string | undefined;
|
|
157
|
+
//# sourceMappingURL=AnchorResolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnchorResolver.d.ts","sourceRoot":"","sources":["../../src/services/AnchorResolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/services/AnchorResolver.ts
|
|
3
|
+
//
|
|
4
|
+
// Ticket #2 — Stage 1: Core Types & the Identity Anchor.
|
|
5
|
+
//
|
|
6
|
+
// THE PROBLEM (architecture doc §3.3): Playwright runs each worker in its
|
|
7
|
+
// own OS process. Workers flag mutations into `.sela/pending-prompts*`,
|
|
8
|
+
// drained later by the main process. Between flag-time and apply-time the
|
|
9
|
+
// file can shift: Worker A inserts a line at the top → every line below
|
|
10
|
+
// moves down. Any anchor that stored `{ line, col }` now points at the
|
|
11
|
+
// WRONG code.
|
|
12
|
+
//
|
|
13
|
+
// THE FIX: never persist coordinates. Persist an IDENTITY ANCHOR:
|
|
14
|
+
// • structuralPath — kind-indexed path from the SourceFile root to the
|
|
15
|
+
// node. Immune to pure line shifts (adding blank lines / comments above
|
|
16
|
+
// the target does NOT change a node's structural index).
|
|
17
|
+
// • nodeTextSha — sha256 of the target node's text at flag-time. The
|
|
18
|
+
// integrity check: if the bytes under the anchor changed, we KNOW the
|
|
19
|
+
// node drifted and refuse to blind-write (status: "stale").
|
|
20
|
+
// • symbolName / enclosingFnName — content-addressed recovery keys used
|
|
21
|
+
// when the structural index DID drift (a new sibling statement was
|
|
22
|
+
// inserted ahead of the target).
|
|
23
|
+
//
|
|
24
|
+
// resolveAnchor() is PURE and SIDE-EFFECT-FREE: given a freshly-parsed
|
|
25
|
+
// SourceFile + an AnchorKey, it re-locates the node or reports precisely
|
|
26
|
+
// why it could not. It never throws on bad input — every failure mode is a
|
|
27
|
+
// typed result variant so the drain loop can branch without try/catch.
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.sha256 = sha256;
|
|
30
|
+
exports.computeAnchor = computeAnchor;
|
|
31
|
+
exports.anchorDedupeKey = anchorDedupeKey;
|
|
32
|
+
exports.resolveAnchor = resolveAnchor;
|
|
33
|
+
exports.structuralPathOf = structuralPathOf;
|
|
34
|
+
exports.followStructuralPath = followStructuralPath;
|
|
35
|
+
exports.enclosingSymbolName = enclosingSymbolName;
|
|
36
|
+
exports.enclosingFnName = enclosingFnName;
|
|
37
|
+
const crypto_1 = require("crypto");
|
|
38
|
+
const ts_morph_1 = require("ts-morph");
|
|
39
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
40
|
+
// PUBLIC API
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
42
|
+
/** sha256 hex of an arbitrary string. Stable across processes. */
|
|
43
|
+
function sha256(text) {
|
|
44
|
+
return (0, crypto_1.createHash)("sha256").update(text, "utf8").digest("hex");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build an AnchorKey for `node`. The inverse of resolveAnchor: feeding the
|
|
48
|
+
* result back into resolveAnchor against the SAME (or line-shifted) file
|
|
49
|
+
* must re-locate `node`.
|
|
50
|
+
*/
|
|
51
|
+
function computeAnchor(node) {
|
|
52
|
+
const sf = node.getSourceFile();
|
|
53
|
+
return {
|
|
54
|
+
filePath: sf.getFilePath(),
|
|
55
|
+
symbolName: enclosingSymbolName(node) ?? "",
|
|
56
|
+
structuralPath: structuralPathOf(node),
|
|
57
|
+
nodeTextSha: sha256(node.getText()),
|
|
58
|
+
kind: node.getKindName(),
|
|
59
|
+
enclosingFnName: enclosingFnName(node),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Dedup key for collapsing identical proposals (§3.5). Two proposals that
|
|
64
|
+
* target the same node identity in the same file share this key regardless
|
|
65
|
+
* of which worker emitted them.
|
|
66
|
+
*/
|
|
67
|
+
function anchorDedupeKey(a) {
|
|
68
|
+
return `${a.filePath}::${a.structuralPath}::${a.symbolName}::${a.kind}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Re-locate the node an AnchorKey points to, in a freshly-parsed
|
|
72
|
+
* SourceFile. PURE — no writes, never throws.
|
|
73
|
+
*
|
|
74
|
+
* Resolution order:
|
|
75
|
+
* 1. Structural path. If it lands on a node of the recorded kind whose
|
|
76
|
+
* text sha matches → resolved("structural"). This is the fast path and
|
|
77
|
+
* the ONLY path needed for pure line shifts.
|
|
78
|
+
* 2. Content-addressed recovery. When the structural index drifted (a new
|
|
79
|
+
* sibling was inserted ahead of the target), search the file for nodes
|
|
80
|
+
* of the recorded kind whose sha matches, then narrow by symbolName and
|
|
81
|
+
* enclosingFnName. Exactly one → resolved("content"); more than one →
|
|
82
|
+
* ambiguous.
|
|
83
|
+
* 3. Stale. The structural path resolved to a node of the right kind but
|
|
84
|
+
* the bytes changed (someone edited the selector) and no content match
|
|
85
|
+
* exists → the node moved/changed under us; refuse to blind-write.
|
|
86
|
+
* 4. not-found. Nothing matches.
|
|
87
|
+
*/
|
|
88
|
+
function resolveAnchor(sourceFile, anchor) {
|
|
89
|
+
// Guard: the anchor must belong to this file. Comparing normalised paths
|
|
90
|
+
// avoids "/a/b" vs "/a/./b" false negatives.
|
|
91
|
+
if (!samePath(sourceFile.getFilePath(), anchor.filePath)) {
|
|
92
|
+
return {
|
|
93
|
+
status: "not-found",
|
|
94
|
+
reason: `anchor file "${anchor.filePath}" != source file "${sourceFile.getFilePath()}"`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// ── 1. Structural fast path ──────────────────────────────────────
|
|
98
|
+
//
|
|
99
|
+
// Accept ONLY when kind + sha match AND the enclosing context the anchor
|
|
100
|
+
// recorded still holds. The context check is what stops a drifted path
|
|
101
|
+
// from landing on an identical-sha TWIN in a different declaration / test
|
|
102
|
+
// block (the multi-consumer hazard, §1.3): after a same-kind sibling is
|
|
103
|
+
// inserted ahead, ExpressionStatement[1] may now point at a different
|
|
104
|
+
// block whose duplicate selector hashes identically. Without this guard
|
|
105
|
+
// we would confidently mutate the wrong copy.
|
|
106
|
+
const structuralNode = followStructuralPath(sourceFile, anchor.structuralPath);
|
|
107
|
+
if (structuralNode &&
|
|
108
|
+
structuralNode.getKindName() === anchor.kind &&
|
|
109
|
+
sha256(structuralNode.getText()) === anchor.nodeTextSha &&
|
|
110
|
+
contextMatches(structuralNode, anchor)) {
|
|
111
|
+
return { status: "resolved", node: structuralNode, via: "structural" };
|
|
112
|
+
}
|
|
113
|
+
// ── 2. Content-addressed recovery ────────────────────────────────
|
|
114
|
+
let matches = sourceFile
|
|
115
|
+
.getDescendants()
|
|
116
|
+
.filter((n) => n.getKindName() === anchor.kind &&
|
|
117
|
+
sha256(n.getText()) === anchor.nodeTextSha);
|
|
118
|
+
if (matches.length > 1 && anchor.symbolName) {
|
|
119
|
+
const bySymbol = matches.filter((n) => enclosingSymbolName(n) === anchor.symbolName);
|
|
120
|
+
if (bySymbol.length > 0)
|
|
121
|
+
matches = bySymbol;
|
|
122
|
+
}
|
|
123
|
+
if (matches.length > 1 && anchor.enclosingFnName) {
|
|
124
|
+
const byFn = matches.filter((n) => enclosingFnName(n) === anchor.enclosingFnName);
|
|
125
|
+
if (byFn.length > 0)
|
|
126
|
+
matches = byFn;
|
|
127
|
+
}
|
|
128
|
+
if (matches.length === 1) {
|
|
129
|
+
return { status: "resolved", node: matches[0], via: "content" };
|
|
130
|
+
}
|
|
131
|
+
if (matches.length > 1) {
|
|
132
|
+
return {
|
|
133
|
+
status: "ambiguous",
|
|
134
|
+
matches: matches.length,
|
|
135
|
+
reason: `${matches.length} nodes match kind+sha; symbolName/enclosingFnName insufficient to disambiguate`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// ── 3. Stale: located structurally but the bytes drifted ─────────
|
|
139
|
+
if (structuralNode && structuralNode.getKindName() === anchor.kind) {
|
|
140
|
+
return {
|
|
141
|
+
status: "stale",
|
|
142
|
+
node: structuralNode,
|
|
143
|
+
reason: `node at structural path changed: sha mismatch (expected ${short(anchor.nodeTextSha)})`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// ── 4. Gone ──────────────────────────────────────────────────────
|
|
147
|
+
return {
|
|
148
|
+
status: "not-found",
|
|
149
|
+
reason: `no node of kind "${anchor.kind}" with matching sha; structural path ${structuralNode ? "resolved to wrong kind" : "did not resolve"}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
153
|
+
// STRUCTURAL PATH
|
|
154
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
155
|
+
/**
|
|
156
|
+
* Kind-indexed path from the SourceFile root down to `node`. Each segment is
|
|
157
|
+
* `KindName[i]` where `i` is the node's index among its parent's children
|
|
158
|
+
* OF THE SAME KIND. Indexing per-kind (rather than absolute child index)
|
|
159
|
+
* keeps the path readable and resilient to trivia/token reshuffling.
|
|
160
|
+
*/
|
|
161
|
+
function structuralPathOf(node) {
|
|
162
|
+
const segs = [];
|
|
163
|
+
let cur = node;
|
|
164
|
+
while (cur && !ts_morph_1.Node.isSourceFile(cur)) {
|
|
165
|
+
const parent = cur.getParent();
|
|
166
|
+
if (!parent)
|
|
167
|
+
break;
|
|
168
|
+
const kindName = cur.getKindName();
|
|
169
|
+
// forEachChildAsArray() mirrors the compiler's ts.forEachChild — it
|
|
170
|
+
// excludes synthetic SyntaxList nodes and punctuation tokens, so it is
|
|
171
|
+
// the exact inverse of getParent(). Using getChildren() here would be
|
|
172
|
+
// asymmetric (it injects SyntaxList layers getParent() skips).
|
|
173
|
+
const sameKind = parent
|
|
174
|
+
.forEachChildAsArray()
|
|
175
|
+
.filter((c) => c.getKindName() === kindName);
|
|
176
|
+
const idx = sameKind.indexOf(cur);
|
|
177
|
+
segs.push(`${kindName}[${idx < 0 ? 0 : idx}]`);
|
|
178
|
+
cur = parent;
|
|
179
|
+
}
|
|
180
|
+
segs.reverse();
|
|
181
|
+
return segs.join("/");
|
|
182
|
+
}
|
|
183
|
+
/** Walk a structuralPath from the SourceFile root. Returns null on any miss. */
|
|
184
|
+
function followStructuralPath(sourceFile, structuralPath) {
|
|
185
|
+
if (!structuralPath)
|
|
186
|
+
return null;
|
|
187
|
+
let cur = sourceFile;
|
|
188
|
+
for (const seg of structuralPath.split("/")) {
|
|
189
|
+
const m = /^(.+)\[(\d+)\]$/.exec(seg);
|
|
190
|
+
if (!m)
|
|
191
|
+
return null;
|
|
192
|
+
const kindName = m[1];
|
|
193
|
+
const idx = Number(m[2]);
|
|
194
|
+
const sameKind = cur
|
|
195
|
+
.forEachChildAsArray()
|
|
196
|
+
.filter((c) => c.getKindName() === kindName);
|
|
197
|
+
if (idx < 0 || idx >= sameKind.length)
|
|
198
|
+
return null;
|
|
199
|
+
cur = sameKind[idx];
|
|
200
|
+
}
|
|
201
|
+
return cur;
|
|
202
|
+
}
|
|
203
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
204
|
+
// ENCLOSING-CONTEXT HELPERS
|
|
205
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
206
|
+
/**
|
|
207
|
+
* Name of the nearest declaration owning `node`:
|
|
208
|
+
* const NAME = … | NAME: … (object prop) | NAME = page.locator() (POM prop).
|
|
209
|
+
* Null when there is no named owner (e.g. a bare expression statement).
|
|
210
|
+
*/
|
|
211
|
+
function enclosingSymbolName(node) {
|
|
212
|
+
let cur = node;
|
|
213
|
+
while (cur && !ts_morph_1.Node.isSourceFile(cur)) {
|
|
214
|
+
if (ts_morph_1.Node.isVariableDeclaration(cur))
|
|
215
|
+
return cur.getName();
|
|
216
|
+
if (ts_morph_1.Node.isPropertyAssignment(cur))
|
|
217
|
+
return cur.getName();
|
|
218
|
+
if (ts_morph_1.Node.isPropertyDeclaration(cur))
|
|
219
|
+
return cur.getName();
|
|
220
|
+
if (ts_morph_1.Node.isShorthandPropertyAssignment(cur))
|
|
221
|
+
return cur.getName();
|
|
222
|
+
cur = cur.getParent();
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Name of the enclosing test/hook block, used to disambiguate identical
|
|
228
|
+
* declarations across blocks. Returns the string-literal title for
|
|
229
|
+
* `test("title", …)` / `describe`/`it`, or the hook name for
|
|
230
|
+
* `beforeEach(…)` / `afterAll(…)`. Undefined when module-scoped.
|
|
231
|
+
*/
|
|
232
|
+
function enclosingFnName(node) {
|
|
233
|
+
const HOOK_OR_TEST = /^(test|it|describe|beforeEach|beforeAll|afterEach|afterAll)(\.\w+)?$/;
|
|
234
|
+
let cur = node.getParent();
|
|
235
|
+
while (cur && !ts_morph_1.Node.isSourceFile(cur)) {
|
|
236
|
+
if (ts_morph_1.Node.isCallExpression(cur)) {
|
|
237
|
+
const calleeText = cur.getExpression().getText();
|
|
238
|
+
const base = calleeText.split("(")[0];
|
|
239
|
+
if (HOOK_OR_TEST.test(base)) {
|
|
240
|
+
const firstArg = cur.getArguments()[0];
|
|
241
|
+
if (firstArg && ts_morph_1.Node.isStringLiteral(firstArg)) {
|
|
242
|
+
return firstArg.getLiteralValue();
|
|
243
|
+
}
|
|
244
|
+
if (firstArg &&
|
|
245
|
+
ts_morph_1.Node.isNoSubstitutionTemplateLiteral(firstArg)) {
|
|
246
|
+
return firstArg.getLiteralValue();
|
|
247
|
+
}
|
|
248
|
+
// Hook with no title (beforeEach) — use the hook keyword itself.
|
|
249
|
+
return base;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
cur = cur.getParent();
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
257
|
+
// PRIVATE
|
|
258
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
259
|
+
/**
|
|
260
|
+
* True when `node`'s enclosing symbol/test-block still match what the anchor
|
|
261
|
+
* recorded. Only fields the anchor actually carries are checked — a coarse
|
|
262
|
+
* anchor (no symbolName / no enclosingFnName) imposes no extra constraint.
|
|
263
|
+
*/
|
|
264
|
+
function contextMatches(node, anchor) {
|
|
265
|
+
if (anchor.symbolName) {
|
|
266
|
+
if (enclosingSymbolName(node) !== anchor.symbolName)
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
if (anchor.enclosingFnName !== undefined) {
|
|
270
|
+
if (enclosingFnName(node) !== anchor.enclosingFnName)
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
function samePath(a, b) {
|
|
276
|
+
return normPath(a) === normPath(b);
|
|
277
|
+
}
|
|
278
|
+
function normPath(p) {
|
|
279
|
+
// ts-morph reports posix-style paths; normalise separators + drop "/./"
|
|
280
|
+
// and trailing slashes so equivalent paths compare equal cross-platform.
|
|
281
|
+
return p
|
|
282
|
+
.replace(/\\/g, "/")
|
|
283
|
+
.replace(/\/\.\//g, "/")
|
|
284
|
+
.replace(/\/+$/g, "")
|
|
285
|
+
.toLowerCase();
|
|
286
|
+
}
|
|
287
|
+
function short(sha) {
|
|
288
|
+
return sha.slice(0, 8);
|
|
289
|
+
}
|
|
@@ -20,7 +20,7 @@ exports.ALL_LOCATOR_METHODS = new Set([
|
|
|
20
20
|
// ArgumentTypeAnalyzer
|
|
21
21
|
//
|
|
22
22
|
// Classifies the first argument of a Playwright locator CallExpression.
|
|
23
|
-
// Stateless
|
|
23
|
+
// Stateless - no file I/O, no side effects.
|
|
24
24
|
// ═══════════════════════════════════════════════════════════════════
|
|
25
25
|
class ArgumentTypeAnalyzer {
|
|
26
26
|
/**
|
|
@@ -64,28 +64,28 @@ class ArgumentTypeAnalyzer {
|
|
|
64
64
|
// Private helpers
|
|
65
65
|
// ─────────────────────────────────────────────────────────────────
|
|
66
66
|
static classifyNode(node) {
|
|
67
|
-
// Plain string
|
|
67
|
+
// Plain string - value is self-contained in this node.
|
|
68
68
|
if (ts_morph_1.Node.isStringLiteral(node) ||
|
|
69
69
|
ts_morph_1.Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
70
70
|
return "STRING_LITERAL";
|
|
71
71
|
}
|
|
72
|
-
// Template with ${...} spans
|
|
72
|
+
// Template with ${...} spans - static segments are healable, spans are not.
|
|
73
73
|
if (ts_morph_1.Node.isTemplateExpression(node)) {
|
|
74
74
|
return "TEMPLATE_LITERAL";
|
|
75
75
|
}
|
|
76
|
-
// Bare variable name
|
|
76
|
+
// Bare variable name - must trace to declaration.
|
|
77
77
|
if (ts_morph_1.Node.isIdentifier(node)) {
|
|
78
78
|
return "IDENTIFIER";
|
|
79
79
|
}
|
|
80
|
-
// OBJECT.property
|
|
80
|
+
// OBJECT.property - must trace to object literal property value.
|
|
81
81
|
if (ts_morph_1.Node.isPropertyAccessExpression(node)) {
|
|
82
82
|
return "PROPERTY_ACCESS";
|
|
83
83
|
}
|
|
84
|
-
// fn(...)
|
|
84
|
+
// fn(...) - must enter function body to find return value.
|
|
85
85
|
if (ts_morph_1.Node.isCallExpression(node)) {
|
|
86
86
|
return "CALL_EXPRESSION";
|
|
87
87
|
}
|
|
88
|
-
// Binary expressions, conditionals, spreads, etc.
|
|
88
|
+
// Binary expressions, conditionals, spreads, etc. - not healable here.
|
|
89
89
|
return "COMPLEX";
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BlastRadiusAnalyzer.d.ts","sourceRoot":"","sources":["../../src/services/BlastRadiusAnalyzer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAc,MAAM,UAAU,CAAC;AAMjE,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,IAAI,EAAE,SAAS,CAAC;CACjB;AAWD,qBAAa,mBAAmB;IAC9B,MAAM,CAAC,OAAO,CACZ,eAAe,EAAE,IAAI,EACrB,WAAW,EAAE,UAAU,EACvB,QAAQ,EAAE,OAAO,GAChB,iBAAiB;IAgBpB,OAAO,CAAC,MAAM,CAAC,SAAS;
|
|
1
|
+
{"version":3,"file":"BlastRadiusAnalyzer.d.ts","sourceRoot":"","sources":["../../src/services/BlastRadiusAnalyzer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAc,MAAM,UAAU,CAAC;AAMjE,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,IAAI,EAAE,SAAS,CAAC;CACjB;AAWD,qBAAa,mBAAmB;IAC9B,MAAM,CAAC,OAAO,CACZ,eAAe,EAAE,IAAI,EACrB,WAAW,EAAE,UAAU,EACvB,QAAQ,EAAE,OAAO,GAChB,iBAAiB;IAgBpB,OAAO,CAAC,MAAM,CAAC,SAAS;IA2DxB,OAAO,CAAC,MAAM,CAAC,WAAW;IAY1B,OAAO,CAAC,MAAM,CAAC,UAAU;CAiB1B"}
|
|
@@ -8,7 +8,7 @@ const ts_morph_1 = require("ts-morph");
|
|
|
8
8
|
// Before mutating a definition-site node, determine how many tests
|
|
9
9
|
// reference it. Uses ts-morph's findReferences() for cross-file
|
|
10
10
|
// accuracy. On any analysis failure, defaults to risk:'low' and logs
|
|
11
|
-
// a warning
|
|
11
|
+
// a warning - never blocks healing due to analysis error.
|
|
12
12
|
// ═══════════════════════════════════════════════════════════════════
|
|
13
13
|
class BlastRadiusAnalyzer {
|
|
14
14
|
static analyze(declarationNode, _sourceFile, _project) {
|
|
@@ -35,7 +35,10 @@ class BlastRadiusAnalyzer {
|
|
|
35
35
|
risk: "low",
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
-
const refs = nameNode
|
|
38
|
+
const refs = nameNode
|
|
39
|
+
.getProject()
|
|
40
|
+
.getLanguageService()
|
|
41
|
+
.findReferences(nameNode);
|
|
39
42
|
const affectedFilesSet = new Set();
|
|
40
43
|
let totalRefs = 0;
|
|
41
44
|
// Key = filePath:startLine of the enclosing test() / it() call.
|