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.
- package/README.md +1 -1
- package/dist/config/ConfigLoader.d.ts +1 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -1
- package/dist/config/ConfigLoader.js +10 -0
- package/dist/config/DryRunGuard.d.ts +14 -0
- package/dist/config/DryRunGuard.d.ts.map +1 -0
- package/dist/config/DryRunGuard.js +79 -0
- package/dist/config/SelaConfig.d.ts +15 -1
- package/dist/config/SelaConfig.d.ts.map +1 -1
- package/dist/config/SelaConfig.js +27 -1
- package/dist/engine/SelaEngine.d.ts +26 -6
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +329 -77
- package/dist/errors/SelaError.d.ts +133 -0
- package/dist/errors/SelaError.d.ts.map +1 -0
- package/dist/errors/SelaError.js +155 -0
- package/dist/fixtures/expectProxy.d.ts.map +1 -1
- package/dist/fixtures/expectProxy.js +7 -0
- package/dist/fixtures/index.d.ts +7 -1
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +15 -0
- package/dist/fixtures/proxyTag.d.ts +12 -0
- package/dist/fixtures/proxyTag.d.ts.map +1 -0
- package/dist/fixtures/proxyTag.js +45 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/reporter/SelaReporter.d.ts +110 -0
- package/dist/reporter/SelaReporter.d.ts.map +1 -0
- package/dist/reporter/SelaReporter.js +328 -0
- package/dist/services/ASTSourceUpdater.d.ts +19 -0
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
- package/dist/services/ASTSourceUpdater.js +173 -29
- package/dist/services/HealReportService.d.ts +62 -3
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +184 -14
- package/dist/services/HealingCacheService.d.ts +135 -0
- package/dist/services/HealingCacheService.d.ts.map +1 -0
- package/dist/services/HealingCacheService.js +460 -0
- package/dist/services/InitializerUpdater.d.ts.map +1 -1
- package/dist/services/InitializerUpdater.js +20 -1
- package/dist/services/IntentAuditor.d.ts +18 -1
- package/dist/services/IntentAuditor.d.ts.map +1 -1
- package/dist/services/IntentAuditor.js +19 -6
- package/dist/services/InteractiveReview.d.ts +24 -0
- package/dist/services/InteractiveReview.d.ts.map +1 -0
- package/dist/services/InteractiveReview.js +218 -0
- package/dist/services/LLMService.d.ts +23 -2
- package/dist/services/LLMService.d.ts.map +1 -1
- package/dist/services/LLMService.js +48 -12
- package/dist/services/PRAutomationService.d.ts +23 -3
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +325 -79
- package/dist/services/PendingPromptLedger.d.ts +44 -0
- package/dist/services/PendingPromptLedger.d.ts.map +1 -0
- package/dist/services/PendingPromptLedger.js +180 -0
- package/dist/services/ReportGenerator.d.ts +70 -0
- package/dist/services/ReportGenerator.d.ts.map +1 -0
- package/dist/services/ReportGenerator.js +191 -0
- package/dist/services/SafetyGuard.d.ts +34 -2
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SafetyGuard.js +65 -13
- package/dist/services/SourceUpdater.d.ts.map +1 -1
- package/dist/services/SourceUpdater.js +44 -13
- package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
- package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
- package/dist/services/WorkspaceSnapshotService.js +202 -0
- package/dist/utils/IsolatedDiff.d.ts +45 -0
- package/dist/utils/IsolatedDiff.d.ts.map +1 -0
- package/dist/utils/IsolatedDiff.js +379 -0
- 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
|
-
|
|
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
|
-
|
|
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":"
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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;
|
|
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"}
|