sela-core 1.0.5 → 1.0.7

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 (71) hide show
  1. package/README.md +1 -1
  2. package/dist/config/ConfigLoader.d.ts +1 -0
  3. package/dist/config/ConfigLoader.d.ts.map +1 -1
  4. package/dist/config/ConfigLoader.js +10 -0
  5. package/dist/config/DryRunGuard.d.ts +14 -0
  6. package/dist/config/DryRunGuard.d.ts.map +1 -0
  7. package/dist/config/DryRunGuard.js +79 -0
  8. package/dist/config/SelaConfig.d.ts +15 -1
  9. package/dist/config/SelaConfig.d.ts.map +1 -1
  10. package/dist/config/SelaConfig.js +27 -1
  11. package/dist/engine/SelaEngine.d.ts +26 -6
  12. package/dist/engine/SelaEngine.d.ts.map +1 -1
  13. package/dist/engine/SelaEngine.js +329 -77
  14. package/dist/errors/SelaError.d.ts +133 -0
  15. package/dist/errors/SelaError.d.ts.map +1 -0
  16. package/dist/errors/SelaError.js +155 -0
  17. package/dist/fixtures/expectProxy.d.ts.map +1 -1
  18. package/dist/fixtures/expectProxy.js +7 -0
  19. package/dist/fixtures/index.d.ts +7 -1
  20. package/dist/fixtures/index.d.ts.map +1 -1
  21. package/dist/fixtures/index.js +15 -0
  22. package/dist/fixtures/proxyTag.d.ts +12 -0
  23. package/dist/fixtures/proxyTag.d.ts.map +1 -0
  24. package/dist/fixtures/proxyTag.js +45 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +6 -1
  28. package/dist/reporter/SelaReporter.d.ts +110 -0
  29. package/dist/reporter/SelaReporter.d.ts.map +1 -0
  30. package/dist/reporter/SelaReporter.js +328 -0
  31. package/dist/services/ASTSourceUpdater.d.ts +19 -0
  32. package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
  33. package/dist/services/ASTSourceUpdater.js +173 -29
  34. package/dist/services/HealReportService.d.ts +62 -3
  35. package/dist/services/HealReportService.d.ts.map +1 -1
  36. package/dist/services/HealReportService.js +184 -14
  37. package/dist/services/HealingCacheService.d.ts +135 -0
  38. package/dist/services/HealingCacheService.d.ts.map +1 -0
  39. package/dist/services/HealingCacheService.js +460 -0
  40. package/dist/services/InitializerUpdater.d.ts.map +1 -1
  41. package/dist/services/InitializerUpdater.js +20 -1
  42. package/dist/services/IntentAuditor.d.ts +18 -1
  43. package/dist/services/IntentAuditor.d.ts.map +1 -1
  44. package/dist/services/IntentAuditor.js +19 -6
  45. package/dist/services/InteractiveReview.d.ts +24 -0
  46. package/dist/services/InteractiveReview.d.ts.map +1 -0
  47. package/dist/services/InteractiveReview.js +218 -0
  48. package/dist/services/LLMService.d.ts +23 -2
  49. package/dist/services/LLMService.d.ts.map +1 -1
  50. package/dist/services/LLMService.js +48 -12
  51. package/dist/services/PRAutomationService.d.ts +23 -3
  52. package/dist/services/PRAutomationService.d.ts.map +1 -1
  53. package/dist/services/PRAutomationService.js +325 -79
  54. package/dist/services/PendingPromptLedger.d.ts +44 -0
  55. package/dist/services/PendingPromptLedger.d.ts.map +1 -0
  56. package/dist/services/PendingPromptLedger.js +180 -0
  57. package/dist/services/ReportGenerator.d.ts +70 -0
  58. package/dist/services/ReportGenerator.d.ts.map +1 -0
  59. package/dist/services/ReportGenerator.js +191 -0
  60. package/dist/services/SafetyGuard.d.ts +34 -2
  61. package/dist/services/SafetyGuard.d.ts.map +1 -1
  62. package/dist/services/SafetyGuard.js +65 -13
  63. package/dist/services/SourceUpdater.d.ts.map +1 -1
  64. package/dist/services/SourceUpdater.js +44 -13
  65. package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
  66. package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
  67. package/dist/services/WorkspaceSnapshotService.js +202 -0
  68. package/dist/utils/IsolatedDiff.d.ts +45 -0
  69. package/dist/utils/IsolatedDiff.d.ts.map +1 -0
  70. package/dist/utils/IsolatedDiff.js +379 -0
  71. package/package.json +71 -67
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ // src/services/InteractiveReview.ts
3
+ //
4
+ // Local DX — Interactive CLI review of post-run heals.
5
+ //
6
+ // Runs in the REPORTER (main process) only — workers must never block on
7
+ // stdin. The reporter reads cross-process state from
8
+ // `PendingPromptLedger` and calls `runInteractiveReview()` with the
9
+ // merged entry list.
10
+ //
11
+ // For every entry: prints `[Sela] 🔧 Healed selector in <FileName>`, the
12
+ // isolated unified diff, a clickable `vscode://file/<abs>:<line>`
13
+ // deeplink, then prompts the developer. 'Y' / 'y' / Enter keeps the
14
+ // on-disk fix (the worker already wrote it); 'N' / 'n' rewrites the file
15
+ // back to `contentBefore` (the pre-heal state captured by
16
+ // `WorkspaceSnapshotService.preWrite()` in the worker).
17
+ //
18
+ // All I/O is injectable so this can be unit-tested without a real TTY.
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ var __importDefault = (this && this.__importDefault) || function (mod) {
53
+ return (mod && mod.__esModule) ? mod : { "default": mod };
54
+ };
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.runInteractiveReview = runInteractiveReview;
57
+ const fs = __importStar(require("fs"));
58
+ const path = __importStar(require("path"));
59
+ const readline = __importStar(require("readline"));
60
+ const chalk_1 = __importDefault(require("chalk"));
61
+ const IsolatedDiff_1 = require("../utils/IsolatedDiff");
62
+ const DEFAULT_PROMPT = "[Sela] Apply this fix? (Y/n): ";
63
+ /**
64
+ * Run the post-run interactive review loop sequentially across every
65
+ * pending entry. Returns one outcome per entry so callers can log / test
66
+ * the resulting Apply / Reject decisions.
67
+ *
68
+ * Sequential by design — every prompt `await`s before the next entry is
69
+ * rendered. Concurrent prompts on a single TTY would interleave output.
70
+ */
71
+ async function runInteractiveReview(deps) {
72
+ const log = deps.log ?? ((m) => process.stdout.write(m + "\n"));
73
+ const prompt = deps.prompt ?? defaultPrompt;
74
+ const rollback = deps.rollback ?? defaultRollback;
75
+ const entries = deps.entries ?? [];
76
+ const outcomes = [];
77
+ if (entries.length === 0)
78
+ return outcomes;
79
+ // Headline banner — surfaces even when downstream chalk/readline
80
+ // output is suppressed by the host terminal multiplexer (e.g.
81
+ // Playwright's reporter-level stdio interception).
82
+ log("");
83
+ log(chalk_1.default.bold.bgCyan.black(` [Sela] Local DX Review — ${entries.length} pending fix(es) `));
84
+ for (const entry of entries) {
85
+ renderEntry(entry, log);
86
+ const answer = await prompt(chalk_1.default.cyan(DEFAULT_PROMPT));
87
+ const decision = parseDecision(answer);
88
+ if (decision === "apply") {
89
+ log(chalk_1.default.green(`[Sela] ✅ Fix kept on disk: ${entry.relativePath}`));
90
+ outcomes.push({ file: entry.absolutePath, applied: true });
91
+ }
92
+ else {
93
+ try {
94
+ rollback(entry);
95
+ log(chalk_1.default.yellow(`[Sela] ↩️ Rolled back: ${entry.relativePath} (restored to pre-heal state)`));
96
+ }
97
+ catch (err) {
98
+ log(chalk_1.default.red(`[Sela] ⚠️ Rollback failed for ${entry.relativePath}: ${err.message}`));
99
+ }
100
+ outcomes.push({ file: entry.absolutePath, applied: false });
101
+ }
102
+ }
103
+ log("");
104
+ return outcomes;
105
+ }
106
+ function renderEntry(entry, log) {
107
+ const fileName = path.basename(entry.absolutePath);
108
+ const diff = (0, IsolatedDiff_1.generateUnifiedDiff)(entry.contentBefore, entry.contentAfter, {
109
+ filePath: entry.relativePath,
110
+ });
111
+ log("");
112
+ log(chalk_1.default.bold(`[Sela] 🔧 Healed selector in ${fileName}`));
113
+ log("");
114
+ log(colorizeDiff(diff));
115
+ log("");
116
+ log(`👉 To review and accept this fix directly in VS Code, click here:`);
117
+ log(` ${chalk_1.default.cyan(buildVsCodeLink(entry.absolutePath, entry.lineNumber))}`);
118
+ log("");
119
+ }
120
+ function colorizeDiff(diff) {
121
+ if (!diff)
122
+ return chalk_1.default.dim("(no diff)");
123
+ return diff
124
+ .split("\n")
125
+ .map((row) => {
126
+ if (row.startsWith("+++") ||
127
+ row.startsWith("---") ||
128
+ row.startsWith("diff --git")) {
129
+ return chalk_1.default.bold(row);
130
+ }
131
+ if (row.startsWith("@@"))
132
+ return chalk_1.default.cyan(row);
133
+ if (row.startsWith("+"))
134
+ return chalk_1.default.green(row);
135
+ if (row.startsWith("-"))
136
+ return chalk_1.default.red(row);
137
+ return row;
138
+ })
139
+ .join("\n");
140
+ }
141
+ function buildVsCodeLink(absPath, line) {
142
+ const normalised = absPath.split(path.sep).join("/");
143
+ return line && line > 0
144
+ ? `vscode://file/${normalised}:${line}`
145
+ : `vscode://file/${normalised}`;
146
+ }
147
+ function parseDecision(raw) {
148
+ const trimmed = (raw ?? "").trim().toLowerCase();
149
+ // Empty (Enter) ⇒ apply. 'y' / 'yes' ⇒ apply. 'n' / 'no' ⇒ reject.
150
+ // Anything else ⇒ apply (default-safe — never silently destroy work).
151
+ if (trimmed === "" || trimmed === "y" || trimmed === "yes")
152
+ return "apply";
153
+ if (trimmed === "n" || trimmed === "no")
154
+ return "reject";
155
+ return "apply";
156
+ }
157
+ /**
158
+ * Default rollback — rewrites `contentBefore` to disk. Honours the
159
+ * `createdBySela` flag so files Sela CREATED from nothing get unlinked
160
+ * instead of being replaced with an empty buffer.
161
+ */
162
+ function defaultRollback(entry) {
163
+ if (entry.createdBySela) {
164
+ if (fs.existsSync(entry.absolutePath)) {
165
+ fs.unlinkSync(entry.absolutePath);
166
+ }
167
+ return;
168
+ }
169
+ fs.writeFileSync(entry.absolutePath, entry.contentBefore, "utf8");
170
+ }
171
+ /**
172
+ * Bind `readline` to the live stdin/stdout. Reporter-only — workers must
173
+ * never reach this code path (they have no TTY and would deadlock).
174
+ *
175
+ * Hardening:
176
+ * • If stdin is not a TTY (CI, non-interactive shell, Playwright reporter
177
+ * context that detached stdio), default to Apply ('y') without blocking.
178
+ * • Explicitly `resume()` stdin — Node pauses it by default once any other
179
+ * consumer (readline-from-an-earlier-iteration, raw-mode toggles) lets go.
180
+ * • Write the prompt to `stderr` so Playwright's stdout summary buffer does
181
+ * not overwrite or strip the question line.
182
+ */
183
+ function defaultPrompt(question) {
184
+ const stdinIsTty = process.stdin.isTTY === true;
185
+ if (!stdinIsTty) {
186
+ process.stderr.write(question + "y (no TTY — auto-apply)\n");
187
+ return Promise.resolve("y");
188
+ }
189
+ return new Promise((resolve) => {
190
+ try {
191
+ process.stdin.resume();
192
+ }
193
+ catch {
194
+ // Already resumed / not resumable — readline will still try.
195
+ }
196
+ // `terminal: false` reads stdin line-buffered without raw-mode tricks.
197
+ // This is reliable for both real TTYs AND piped harnesses (CI smoke
198
+ // tests, child-process drivers). `terminal: true` only adds history /
199
+ // arrow-key support, which we don't need for a single Y/N prompt and
200
+ // which mis-parses piped input on Windows.
201
+ process.stderr.write(question);
202
+ const rl = readline.createInterface({
203
+ input: process.stdin,
204
+ output: process.stderr,
205
+ terminal: false,
206
+ });
207
+ let settled = false;
208
+ const finalise = (answer) => {
209
+ if (settled)
210
+ return;
211
+ settled = true;
212
+ rl.close();
213
+ resolve(answer);
214
+ };
215
+ rl.once("line", (line) => finalise(line));
216
+ rl.once("close", () => finalise("y"));
217
+ });
218
+ }
@@ -1,3 +1,4 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
1
2
  import { FixRequest } from "../types";
2
3
  export interface SelectorSegment {
3
4
  type: "frame" | "element";
@@ -76,14 +77,34 @@ export interface FixResponse {
76
77
  */
77
78
  chainSegments?: SmartChainSegment[];
78
79
  originalChainHint?: SmartChainSegment[];
79
- confidence: number;
80
+ /**
81
+ * Confidence score 0-100. Undefined when the model omitted it or returned a
82
+ * non-finite value — downstream code must treat undefined as "unknown" and
83
+ * render "n/a" rather than synthesising a 0.
84
+ */
85
+ confidence?: number;
80
86
  explanation: string;
81
87
  new_selector?: string | null;
82
88
  contentChange?: ContentChange;
83
89
  }
90
+ /**
91
+ * Constructor options for LLMService.
92
+ *
93
+ * - `anthropic`: pre-built Anthropic client. Tests use this to inject a
94
+ * mock client without touching env vars or network.
95
+ * - `apiKey`: override the env-based ANTHROPIC_API_KEY when constructing
96
+ * the default client.
97
+ * - `model`: override the Sonnet model id (lets tests pin the SDK call).
98
+ */
99
+ export interface LLMServiceOptions {
100
+ anthropic?: Anthropic;
101
+ apiKey?: string;
102
+ model?: string;
103
+ }
84
104
  export declare class LLMService {
85
105
  private anthropic;
86
- constructor();
106
+ private readonly model;
107
+ constructor(opts?: LLMServiceOptions);
87
108
  private cleanJson;
88
109
  /**
89
110
  * Attempts to infer contentChange when the AI did not return the field.
@@ -1 +1 @@
1
- {"version":3,"file":"LLMService.d.ts","sourceRoot":"","sources":["../../src/services/LLMService.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"LLMService.d.ts","sourceRoot":"","sources":["../../src/services/LLMService.ts"],"names":[],"mappings":"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"}
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.LLMService = void 0;
40
40
  const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
41
41
  const dotenv = __importStar(require("dotenv"));
42
+ const SelaError_1 = require("../errors/SelaError");
42
43
  dotenv.config();
43
44
  // ─────────────────────────────────────────────────────────────────
44
45
  // SYSTEM PROMPT
@@ -103,21 +104,34 @@ The healing system depends on the attribute value being present in your response
103
104
  If the element's text changed, you MUST include:
104
105
  "contentChange": { "oldText": "<previous>", "newText": "<current>" }
105
106
 
107
+ ### CONFIDENCE SCORE (MANDATORY)
108
+ You MUST include a numeric "confidence" field — an INTEGER in the range 0-100 —
109
+ representing how certain you are that the returned chain will resolve the
110
+ intended element on the next test run. Use this rubric:
111
+ - 90-100: stable id, data-testid, or unique role+name — zero ambiguity in the DOM.
112
+ - 70-89: strong semantic match, but some structural drift or competing candidates.
113
+ - 50-69: best-guess match — multiple plausible candidates required disambiguation.
114
+ - 1-49: speculative match — return this only when no better candidate exists.
115
+ NEVER omit this field. NEVER return it as a string, boolean, or null.
116
+ Reports rendered downstream will show "n/a" when this field is missing — which
117
+ is a clear failure signal to the developer reviewing the heal.
118
+
106
119
  ### MANDATORY OUTPUT FORMAT
107
120
  Return ONLY valid JSON.
108
121
 
109
122
  Example output — Identifying a specific button in a list row:
110
123
  {
111
124
  "status": "FIXED",
125
+ "confidence": 92,
112
126
  "chainSegments": [
113
127
  { "type": "locator", "selector": ".user-item" },
114
128
  { "type": "filter", "hasText": "Bob Smith" },
115
129
  { "type": "getByRole", "role": "button", "name": "Modify Bob" }
116
130
  ],
117
131
  "segments": [
118
- {
119
- "type": "element",
120
- "selector": ".user-item >> internal:has-text=\"Bob Smith\"i >> internal:role=button[name=\"Modify Bob\"i]"
132
+ {
133
+ "type": "element",
134
+ "selector": ".user-item >> internal:has-text=\"Bob Smith\"i >> internal:role=button[name=\"Modify Bob\"i]"
121
135
  }
122
136
  ],
123
137
  "explanation": "Updated container to .user-item and targeted the button by its new semantic name 'Modify Bob' to ensure a unique match."
@@ -186,15 +200,24 @@ function inferOriginalChainHint(selector) {
186
200
  }
187
201
  return segments;
188
202
  }
189
- // ─────────────────────────────────────────────────────────────────
190
- // LLM SERVICE
191
- // ─────────────────────────────────────────────────────────────────
203
+ const DEFAULT_LLM_MODEL = "claude-sonnet-4-20250514";
192
204
  class LLMService {
193
205
  anthropic;
194
- constructor() {
195
- const apiKey = process.env.ANTHROPIC_API_KEY || "";
196
- if (!apiKey)
197
- throw new Error("ANTHROPIC_API_KEY not found in .env");
206
+ model;
207
+ constructor(opts = {}) {
208
+ this.model = opts.model ?? DEFAULT_LLM_MODEL;
209
+ if (opts.anthropic) {
210
+ this.anthropic = opts.anthropic;
211
+ return;
212
+ }
213
+ const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
214
+ if (!apiKey) {
215
+ throw new SelaError_1.SelaError({
216
+ subsystem: "LLMService",
217
+ code: SelaError_1.LLM_SERVICE_CODES.NO_API_KEY,
218
+ reason: "ANTHROPIC_API_KEY not found — pass an `anthropic` client or `apiKey` to LLMService(), or set the env var",
219
+ });
220
+ }
198
221
  this.anthropic = new sdk_1.default({ apiKey });
199
222
  }
200
223
  cleanJson(text) {
@@ -331,7 +354,7 @@ ${request.currentDom}
331
354
  Return ONLY a raw JSON object — no markdown, no preamble.`;
332
355
  try {
333
356
  const msg = await this.anthropic.messages.create({
334
- model: "claude-sonnet-4-20250514",
357
+ model: this.model,
335
358
  max_tokens: 1024,
336
359
  system: SYSTEM_PROMPT_TEXT +
337
360
  "\nIMPORTANT: Respond ONLY with a raw JSON object.",
@@ -343,6 +366,19 @@ Return ONLY a raw JSON object — no markdown, no preamble.`;
343
366
  if (!jsonString)
344
367
  throw new Error("AI did not return a valid JSON object");
345
368
  const parsed = JSON.parse(jsonString);
369
+ // ── Normalise confidence: keep only finite numbers in [0,100] ─────
370
+ // Anything else (undefined, NaN, null, string, out-of-range) collapses
371
+ // to `undefined` so reports render "n/a" instead of synthesising 0.
372
+ const rawConf = parsed.confidence;
373
+ if (typeof rawConf === "number" && Number.isFinite(rawConf)) {
374
+ parsed.confidence = Math.max(0, Math.min(100, Math.round(rawConf)));
375
+ }
376
+ else {
377
+ if (rawConf !== undefined) {
378
+ console.warn(`[LLMService] ⚠️ Discarded non-numeric confidence: ${JSON.stringify(rawConf)}`);
379
+ }
380
+ parsed.confidence = undefined;
381
+ }
346
382
  // ── Normalize chainSegments → segments (legacy) ───────────────────
347
383
  if (parsed.chainSegments && Array.isArray(parsed.chainSegments)) {
348
384
  // Reconstruct a flat new_selector from chain segments for legacy paths
@@ -430,7 +466,7 @@ Return ONLY a raw JSON object — no markdown, no preamble.`;
430
466
  status: "NOT_FOUND",
431
467
  segments: [],
432
468
  new_selector: null,
433
- confidence: 0,
469
+ confidence: undefined,
434
470
  explanation: `AI Error: ${error.message}`,
435
471
  };
436
472
  }
@@ -1,5 +1,6 @@
1
1
  import type { ResolvedPRAutomation, PRStrategy, BugAction } from "../config/SelaConfig";
2
2
  import type { HealedEvent, ProtectedEvent } from "./HealReportService";
3
+ import { type WorkspaceSnapshotService } from "./WorkspaceSnapshotService";
3
4
  export interface BranchInfo {
4
5
  branch: string | null;
5
6
  isPR: boolean;
@@ -10,8 +11,14 @@ export interface StrategyDecision {
10
11
  effective: PRStrategy;
11
12
  configured: PRStrategy;
12
13
  downgradeReason: "PROTECTED_BRANCH" | "LOW_CONFIDENCE" | "LOW_AUDITOR" | null;
13
- minAiConfidence: number;
14
- minAuditorConfidence: number;
14
+ /**
15
+ * Lowest AI confidence observed across this session's heals, or `undefined`
16
+ * when no heal carried a numeric confidence (LLM omitted the field). Render
17
+ * via `fmtPct` so the absence reads as "n/a" instead of 0%.
18
+ */
19
+ minAiConfidence: number | undefined;
20
+ /** Same semantics as `minAiConfidence`, for the auditor's adjusted score. */
21
+ minAuditorConfidence: number | undefined;
15
22
  }
16
23
  export interface ExecuteContext {
17
24
  cwd: string;
@@ -19,12 +26,25 @@ export interface ExecuteContext {
19
26
  }
20
27
  export declare function detectBranch(cwd?: string): BranchInfo;
21
28
  export declare function decideEffectiveStrategy(cfg: ResolvedPRAutomation, branch: string | null, heals: HealedEvent[]): StrategyDecision;
29
+ export declare function fmtPct(v: unknown): string;
22
30
  export interface ExecutionResult {
23
31
  effective: PRStrategy;
24
32
  prUrl: string | null;
25
33
  branchPushed: string | null;
26
34
  errors: string[];
27
35
  }
28
- export declare function execute(cfg: ResolvedPRAutomation, decision: StrategyDecision, heals: HealedEvent[], branchInfo: BranchInfo, ctx: ExecuteContext): Promise<ExecutionResult>;
36
+ /**
37
+ * Clean-Room execution lifecycle.
38
+ *
39
+ * The legacy flow committed the developer's whole dirty working tree onto
40
+ * the PR branch — polluting reviewers with `test.only`, commented debug
41
+ * blocks, etc. This refactor isolates Sela's specific AST changes onto an
42
+ * ephemeral branch, leaves the developer's local workspace byte-identical
43
+ * to its pre-Sela state, and is wrapped in a try/finally that restores
44
+ * the workspace even on mid-flight crashes.
45
+ */
46
+ export declare function execute(cfg: ResolvedPRAutomation, decision: StrategyDecision, heals: HealedEvent[], branchInfo: BranchInfo, ctx: ExecuteContext,
47
+ /** Injectable for tests — defaults to the module singleton. */
48
+ workspace?: WorkspaceSnapshotService): Promise<ExecutionResult>;
29
49
  export declare function handleBugDetected(action: BugAction, events: ProtectedEvent[], branchInfo: BranchInfo, ctx: ExecuteContext): Promise<void>;
30
50
  //# sourceMappingURL=PRAutomationService.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PRAutomationService.d.ts","sourceRoot":"","sources":["../../src/services/PRAutomationService.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,SAAS,EACV,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EACV,WAAW,EACX,cAAc,EACf,MAAM,qBAAqB,CAAC;AAM7B,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,UAAU,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,kBAAkB,GAAG,gBAAgB,GAAG,aAAa,GAAG,IAAI,CAAC;IAC9E,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AA2ED,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,UAAU,CA8EpE;AAMD,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,oBAAoB,EACzB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,WAAW,EAAE,GACnB,gBAAgB,CAuDlB;AA6DD,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,UAAU,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wBAAsB,OAAO,CAC3B,GAAG,EAAE,oBAAoB,EACzB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,WAAW,EAAE,EACpB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,eAAe,CAAC,CA4J1B;AAMD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,cAAc,EAAE,EACxB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,IAAI,CAAC,CA0Ff"}
1
+ {"version":3,"file":"PRAutomationService.d.ts","sourceRoot":"","sources":["../../src/services/PRAutomationService.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,SAAS,EACV,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EACV,WAAW,EACX,cAAc,EACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,4BAA4B,CAAC;AAUpC,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,UAAU,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,kBAAkB,GAAG,gBAAgB,GAAG,aAAa,GAAG,IAAI,CAAC;IAC9E;;;;OAIG;IACH,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,6EAA6E;IAC7E,oBAAoB,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AA2ED,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,UAAU,CA8EpE;AAMD,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,oBAAoB,EACzB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,WAAW,EAAE,GACnB,gBAAgB,CAgElB;AAMD,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAEzC;AAuFD,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,UAAU,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAsB,OAAO,CAC3B,GAAG,EAAE,oBAAoB,EACzB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,WAAW,EAAE,EACpB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc;AACnB,+DAA+D;AAC/D,SAAS,GAAE,wBAAkD,GAC5D,OAAO,CAAC,eAAe,CAAC,CAkG1B;AA4SD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,cAAc,EAAE,EACxB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,IAAI,CAAC,CA0Ff"}