sela-core 1.0.8 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +27 -17
  2. package/dist/cli/commands/init.js +1 -1
  3. package/dist/cli/commands/merge.d.ts +13 -0
  4. package/dist/cli/commands/merge.d.ts.map +1 -0
  5. package/dist/cli/commands/merge.js +119 -0
  6. package/dist/cli/index.js +4 -1
  7. package/dist/config/SelaConfig.d.ts +1 -1
  8. package/dist/config/SelaConfig.d.ts.map +1 -1
  9. package/dist/config/SelaConfig.js +1 -1
  10. package/dist/engine/SelaEngine.d.ts.map +1 -1
  11. package/dist/engine/SelaEngine.js +54 -2
  12. package/dist/errors/SelaError.d.ts +8 -0
  13. package/dist/errors/SelaError.d.ts.map +1 -1
  14. package/dist/errors/SelaError.js +8 -0
  15. package/dist/fixtures/expectProxy.d.ts +1 -1
  16. package/dist/fixtures/expectProxy.js +6 -6
  17. package/dist/fixtures/index.d.ts +18 -2
  18. package/dist/fixtures/index.d.ts.map +1 -1
  19. package/dist/fixtures/index.js +34 -14
  20. package/dist/fixtures/moduleExpect.js +7 -7
  21. package/dist/fixtures/proxyTag.js +1 -1
  22. package/dist/services/ASTSourceUpdater.d.ts +45 -0
  23. package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
  24. package/dist/services/ASTSourceUpdater.js +201 -47
  25. package/dist/services/AnchorResolver.d.ts +157 -0
  26. package/dist/services/AnchorResolver.d.ts.map +1 -0
  27. package/dist/services/AnchorResolver.js +289 -0
  28. package/dist/services/DecisionEngine.d.ts +51 -0
  29. package/dist/services/DecisionEngine.d.ts.map +1 -0
  30. package/dist/services/DecisionEngine.js +260 -0
  31. package/dist/services/HealReportService.d.ts +31 -55
  32. package/dist/services/HealReportService.d.ts.map +1 -1
  33. package/dist/services/HealReportService.js +863 -1243
  34. package/dist/services/HealingAdvisory.d.ts +9 -1
  35. package/dist/services/HealingAdvisory.d.ts.map +1 -1
  36. package/dist/services/HealingAdvisory.js +8 -0
  37. package/dist/services/InitializerUpdater.d.ts.map +1 -1
  38. package/dist/services/InitializerUpdater.js +10 -0
  39. package/dist/services/InteractiveReview.d.ts.map +1 -1
  40. package/dist/services/InteractiveReview.js +4 -1
  41. package/dist/services/MutationApplier.d.ts +55 -0
  42. package/dist/services/MutationApplier.d.ts.map +1 -0
  43. package/dist/services/MutationApplier.js +322 -0
  44. package/dist/services/PendingPromptLedger.d.ts +7 -0
  45. package/dist/services/PendingPromptLedger.d.ts.map +1 -1
  46. package/dist/services/PendingPromptLedger.js +25 -0
  47. package/dist/services/ReportGenerator.d.ts +116 -30
  48. package/dist/services/ReportGenerator.d.ts.map +1 -1
  49. package/dist/services/ReportGenerator.js +150 -63
  50. package/dist/services/ReportMergeService.d.ts +95 -0
  51. package/dist/services/ReportMergeService.d.ts.map +1 -0
  52. package/dist/services/ReportMergeService.js +0 -0
  53. package/dist/services/SafetyGuard.d.ts +1 -1
  54. package/dist/services/SafetyGuard.d.ts.map +1 -1
  55. package/dist/services/SelectorSanitizer.d.ts +52 -0
  56. package/dist/services/SelectorSanitizer.d.ts.map +1 -0
  57. package/dist/services/SelectorSanitizer.js +318 -0
  58. package/dist/services/SnapshotService.js +1 -1
  59. package/dist/services/SourceUpdater.d.ts.map +1 -1
  60. package/dist/services/SourceUpdater.js +17 -0
  61. package/dist/services/TraceBackEngine.d.ts +67 -0
  62. package/dist/services/TraceBackEngine.d.ts.map +1 -0
  63. package/dist/services/TraceBackEngine.js +672 -0
  64. package/dist/utils/DOMUtils.d.ts +18 -2
  65. package/dist/utils/DOMUtils.d.ts.map +1 -1
  66. package/dist/utils/DOMUtils.js +335 -49
  67. package/package.json +1 -1
@@ -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
+ }
@@ -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"}