sela-core 1.0.2 → 1.0.4

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 (34) hide show
  1. package/dist/cli/commands/showReport.d.ts +3 -0
  2. package/dist/cli/commands/showReport.d.ts.map +1 -0
  3. package/dist/cli/commands/showReport.js +80 -0
  4. package/dist/cli/index.js +4 -1
  5. package/dist/config/SelaConfig.d.ts +57 -11
  6. package/dist/config/SelaConfig.d.ts.map +1 -1
  7. package/dist/config/SelaConfig.js +37 -7
  8. package/dist/engine/SelaEngine.d.ts +29 -0
  9. package/dist/engine/SelaEngine.d.ts.map +1 -0
  10. package/dist/engine/SelaEngine.js +682 -0
  11. package/dist/engine/singleton.d.ts +2 -2
  12. package/dist/engine/singleton.d.ts.map +1 -1
  13. package/dist/engine/singleton.js +2 -2
  14. package/dist/fixtures/expectProxy.d.ts +2 -2
  15. package/dist/fixtures/expectProxy.d.ts.map +1 -1
  16. package/dist/fixtures/expectProxy.js +2 -2
  17. package/dist/fixtures/index.d.ts.map +1 -1
  18. package/dist/fixtures/index.js +21 -15
  19. package/dist/fixtures/moduleExpect.d.ts.map +1 -1
  20. package/dist/fixtures/moduleExpect.js +13 -6
  21. package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
  22. package/dist/services/ASTSourceUpdater.js +10 -1
  23. package/dist/services/HealReportService.d.ts +110 -0
  24. package/dist/services/HealReportService.d.ts.map +1 -0
  25. package/dist/services/HealReportService.js +1195 -0
  26. package/dist/services/PRAutomationService.d.ts +30 -0
  27. package/dist/services/PRAutomationService.d.ts.map +1 -0
  28. package/dist/services/PRAutomationService.js +464 -0
  29. package/dist/services/SnapshotService.d.ts.map +1 -1
  30. package/dist/services/SnapshotService.js +7 -8
  31. package/dist/services/SourceUpdater.d.ts.map +1 -1
  32. package/dist/services/SourceUpdater.js +13 -2
  33. package/dist/storage/SnapshotManager.js +1 -1
  34. package/package.json +2 -2
@@ -0,0 +1,30 @@
1
+ import type { ResolvedPRAutomation, PRStrategy, BugAction } from "../config/SelaConfig";
2
+ import type { HealedEvent, ProtectedEvent } from "./HealReportService";
3
+ export interface BranchInfo {
4
+ branch: string | null;
5
+ isPR: boolean;
6
+ prNumber: number | null;
7
+ source: "env" | "git" | "none";
8
+ }
9
+ export interface StrategyDecision {
10
+ effective: PRStrategy;
11
+ configured: PRStrategy;
12
+ downgradeReason: "PROTECTED_BRANCH" | "LOW_CONFIDENCE" | "LOW_AUDITOR" | null;
13
+ minAiConfidence: number;
14
+ minAuditorConfidence: number;
15
+ }
16
+ export interface ExecuteContext {
17
+ cwd: string;
18
+ reportHtmlPath: string | null;
19
+ }
20
+ export declare function detectBranch(cwd?: string): BranchInfo;
21
+ export declare function decideEffectiveStrategy(cfg: ResolvedPRAutomation, branch: string | null, heals: HealedEvent[]): StrategyDecision;
22
+ export interface ExecutionResult {
23
+ effective: PRStrategy;
24
+ prUrl: string | null;
25
+ branchPushed: string | null;
26
+ errors: string[];
27
+ }
28
+ export declare function execute(cfg: ResolvedPRAutomation, decision: StrategyDecision, heals: HealedEvent[], branchInfo: BranchInfo, ctx: ExecuteContext): Promise<ExecutionResult>;
29
+ export declare function handleBugDetected(action: BugAction, events: ProtectedEvent[], branchInfo: BranchInfo, ctx: ExecuteContext): Promise<void>;
30
+ //# sourceMappingURL=PRAutomationService.d.ts.map
@@ -0,0 +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;AAiDD,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,UAAU,CAqDpE;AAMD,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,oBAAoB,EACzB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,WAAW,EAAE,GACnB,gBAAgB,CAuDlB;AAsDD,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"}
@@ -0,0 +1,464 @@
1
+ "use strict";
2
+ // src/services/PRAutomationService.ts
3
+ //
4
+ // Git-flow automation layer. Runs ONCE per session at SelaEngine.commitUpdates().
5
+ // Responsibilities:
6
+ // 1. Detect the active branch (CI env vars first, git CLI fallback).
7
+ // 2. Resolve the EFFECTIVE strategy via trust-aware downgrade.
8
+ // 3. Execute the strategy: directCommit | pr | draftPR | readOnly.
9
+ // 4. Handle PROTECTED events per onBugDetected policy.
10
+ //
11
+ // Hard requirements:
12
+ // - GitHub-only in v1 (uses `gh` CLI).
13
+ // - Never throws into the caller. Failures degrade to warnings + readOnly.
14
+ // - Detached HEAD or missing `gh` → skip git ops, emit clear log.
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.detectBranch = detectBranch;
50
+ exports.decideEffectiveStrategy = decideEffectiveStrategy;
51
+ exports.execute = execute;
52
+ exports.handleBugDetected = handleBugDetected;
53
+ const fs = __importStar(require("fs"));
54
+ const path = __importStar(require("path"));
55
+ const child_process_1 = require("child_process");
56
+ // ═══════════════════════════════════════════════════════════════════
57
+ // SHELL HELPERS
58
+ // ═══════════════════════════════════════════════════════════════════
59
+ function sh(cmd, cwd) {
60
+ try {
61
+ const stdout = (0, child_process_1.execSync)(cmd, {
62
+ cwd,
63
+ encoding: "utf-8",
64
+ stdio: ["pipe", "pipe", "pipe"],
65
+ });
66
+ return { ok: true, stdout: stdout.toString(), stderr: "" };
67
+ }
68
+ catch (err) {
69
+ return {
70
+ ok: false,
71
+ stdout: err?.stdout?.toString() ?? "",
72
+ stderr: err?.stderr?.toString() ?? err?.message ?? "unknown error",
73
+ };
74
+ }
75
+ }
76
+ function ghAvailable(cwd) {
77
+ return sh("gh --version", cwd).ok;
78
+ }
79
+ function gitAvailable(cwd) {
80
+ return sh("git --version", cwd).ok;
81
+ }
82
+ function isDetachedHead(cwd) {
83
+ return !sh("git symbolic-ref HEAD", cwd).ok;
84
+ }
85
+ // ═══════════════════════════════════════════════════════════════════
86
+ // TEMPLATING
87
+ // ═══════════════════════════════════════════════════════════════════
88
+ function render(template, vars) {
89
+ return template.replace(/\$\{(\w+)\}/g, (_m, k) => k in vars ? String(vars[k]) : `\${${k}}`);
90
+ }
91
+ // ═══════════════════════════════════════════════════════════════════
92
+ // BRANCH DETECTION
93
+ // ═══════════════════════════════════════════════════════════════════
94
+ function detectBranch(cwd = process.cwd()) {
95
+ // GitHub Actions exposes both REF_NAME (push) and HEAD_REF (PR).
96
+ const ghHeadRef = process.env.GITHUB_HEAD_REF; // PR source branch
97
+ const ghRefName = process.env.GITHUB_REF_NAME; // current branch on push, or "<n>/merge" on PR
98
+ const ghEventName = process.env.GITHUB_EVENT_NAME;
99
+ const ghRef = process.env.GITHUB_REF; // "refs/pull/123/merge"
100
+ // Prefer HEAD_REF on PRs — REF_NAME is the synthetic merge ref.
101
+ if (ghHeadRef && ghHeadRef.length > 0) {
102
+ const prNumber = (() => {
103
+ const m = ghRef?.match(/refs\/pull\/(\d+)\//);
104
+ return m ? Number(m[1]) : null;
105
+ })();
106
+ return { branch: ghHeadRef, isPR: true, prNumber, source: "env" };
107
+ }
108
+ if (ghRefName && ghRefName.length > 0 && !ghRefName.endsWith("/merge")) {
109
+ return {
110
+ branch: ghRefName,
111
+ isPR: ghEventName === "pull_request",
112
+ prNumber: null,
113
+ source: "env",
114
+ };
115
+ }
116
+ // GitLab CI fallback
117
+ const gitlabRef = process.env.CI_COMMIT_REF_NAME;
118
+ if (gitlabRef && gitlabRef.length > 0) {
119
+ return { branch: gitlabRef, isPR: false, prNumber: null, source: "env" };
120
+ }
121
+ // Generic Jenkins / local fallback
122
+ const jenkinsBranch = process.env.GIT_BRANCH;
123
+ if (jenkinsBranch && jenkinsBranch.length > 0) {
124
+ return {
125
+ branch: jenkinsBranch.replace(/^origin\//, ""),
126
+ isPR: false,
127
+ prNumber: null,
128
+ source: "env",
129
+ };
130
+ }
131
+ // Local: git CLI
132
+ if (!gitAvailable(cwd)) {
133
+ return { branch: null, isPR: false, prNumber: null, source: "none" };
134
+ }
135
+ const r = sh("git rev-parse --abbrev-ref HEAD", cwd);
136
+ if (!r.ok)
137
+ return { branch: null, isPR: false, prNumber: null, source: "none" };
138
+ const branch = r.stdout.trim();
139
+ if (!branch || branch === "HEAD") {
140
+ return { branch: null, isPR: false, prNumber: null, source: "none" };
141
+ }
142
+ return { branch, isPR: false, prNumber: null, source: "git" };
143
+ }
144
+ // ═══════════════════════════════════════════════════════════════════
145
+ // STRATEGY DECISION
146
+ // ═══════════════════════════════════════════════════════════════════
147
+ function decideEffectiveStrategy(cfg, branch, heals) {
148
+ const minAi = heals.length === 0
149
+ ? 100
150
+ : Math.min(...heals.map((h) => h.aiConfidence ?? 100));
151
+ const minAud = heals.length === 0
152
+ ? 100
153
+ : Math.min(...heals.map((h) => h.auditor?.confidence ?? 100));
154
+ const baseDecision = {
155
+ effective: cfg.strategy,
156
+ configured: cfg.strategy,
157
+ downgradeReason: null,
158
+ minAiConfidence: minAi,
159
+ minAuditorConfidence: minAud,
160
+ };
161
+ if (cfg.strategy !== "directCommit")
162
+ return baseDecision;
163
+ // Rule 1: protected branch → forced downgrade
164
+ if (branch && cfg.protectedBranches.includes(branch)) {
165
+ return {
166
+ ...baseDecision,
167
+ effective: cfg.reviewThresholds.downgradeTo,
168
+ downgradeReason: "PROTECTED_BRANCH",
169
+ };
170
+ }
171
+ // Rule 2: low AI confidence
172
+ if (cfg.reviewThresholds.minConfidenceForDirectCommit > 0 &&
173
+ minAi < cfg.reviewThresholds.minConfidenceForDirectCommit) {
174
+ return {
175
+ ...baseDecision,
176
+ effective: cfg.reviewThresholds.downgradeTo,
177
+ downgradeReason: "LOW_CONFIDENCE",
178
+ };
179
+ }
180
+ // Rule 3: low auditor score
181
+ if (cfg.reviewThresholds.minAuditorScoreForDirectCommit > 0 &&
182
+ minAud < cfg.reviewThresholds.minAuditorScoreForDirectCommit) {
183
+ return {
184
+ ...baseDecision,
185
+ effective: cfg.reviewThresholds.downgradeTo,
186
+ downgradeReason: "LOW_AUDITOR",
187
+ };
188
+ }
189
+ return baseDecision;
190
+ }
191
+ // ═══════════════════════════════════════════════════════════════════
192
+ // PR BODY RENDERING
193
+ // ═══════════════════════════════════════════════════════════════════
194
+ function buildDefaultBody(heals, branchInfo, reportLink, decision) {
195
+ const lines = [];
196
+ lines.push(`# 🤖 Sela Insights — Automation Suite Healed!`);
197
+ lines.push("");
198
+ lines.push(`Sela kept your pipeline running by healing ${heals.length} broken locator(s).`);
199
+ lines.push("");
200
+ lines.push(`## 📊 Executive Summary`);
201
+ lines.push(`* **Heals:** ${heals.length}`);
202
+ lines.push(`* **Min AI Confidence:** ${decision.minAiConfidence}%`);
203
+ lines.push(`* **Min Auditor Confidence:** ${decision.minAuditorConfidence}%`);
204
+ lines.push(`* **Branch:** \`${branchInfo.branch ?? "unknown"}\``);
205
+ if (decision.downgradeReason) {
206
+ lines.push(`* **⚠️ Strategy Downgrade:** \`${decision.configured}\` → \`${decision.effective}\` (${decision.downgradeReason})`);
207
+ }
208
+ lines.push("");
209
+ lines.push(`## 💻 Per-Heal Diff`);
210
+ for (const h of heals) {
211
+ lines.push(`### \`${h.sourceFile}:${h.sourceLine}\` — ${h.testTitle ?? "(no test title)"}`);
212
+ lines.push("");
213
+ lines.push("```diff");
214
+ lines.push(`- ${h.oldCodeLine.trim()}`);
215
+ lines.push(`+ ${h.newCodeLine.trim()}`);
216
+ lines.push("```");
217
+ if (h.aiExplanation) {
218
+ lines.push(`> 🧠 ${h.aiExplanation}`);
219
+ }
220
+ lines.push(`> AI Confidence: **${h.aiConfidence}%** · Auditor: **${h.auditor?.confidence ?? "n/a"}%**`);
221
+ lines.push("");
222
+ }
223
+ lines.push(`## 🔍 Visual Evidence`);
224
+ lines.push(`[📄 View Full Interactive Healing Report](${reportLink})`);
225
+ lines.push("");
226
+ lines.push(`---`);
227
+ lines.push(`Generated automatically by \`sela-core\`.`);
228
+ return lines.join("\n");
229
+ }
230
+ async function execute(cfg, decision, heals, branchInfo, ctx) {
231
+ const result = {
232
+ effective: decision.effective,
233
+ prUrl: null,
234
+ branchPushed: null,
235
+ errors: [],
236
+ };
237
+ if (decision.effective === "readOnly") {
238
+ console.log(`[Sela PR] 🛡️ readOnly — no git operations performed.`);
239
+ if (ctx.reportHtmlPath) {
240
+ console.log(`[Sela PR] Report artifact: ${ctx.reportHtmlPath}`);
241
+ }
242
+ return result;
243
+ }
244
+ if (heals.length === 0) {
245
+ console.log(`[Sela PR] No HEALED events — skipping git operations.`);
246
+ return result;
247
+ }
248
+ if (!gitAvailable(ctx.cwd)) {
249
+ result.errors.push("git CLI not available");
250
+ console.warn(`[Sela PR] ⚠️ git CLI not available — skipping.`);
251
+ return result;
252
+ }
253
+ if (isDetachedHead(ctx.cwd)) {
254
+ result.errors.push("detached HEAD");
255
+ console.warn(`[Sela PR] ⚠️ Detached HEAD — skipping git operations.`);
256
+ return result;
257
+ }
258
+ // ── Stage all healed files (relative paths from event records) ────
259
+ const filesToStage = Array.from(new Set(heals.map((h) => h.sourceFile).filter((f) => f && f.length > 0)));
260
+ if (filesToStage.length === 0) {
261
+ result.errors.push("no files to stage");
262
+ console.warn(`[Sela PR] ⚠️ No file paths in HEALED events — skipping.`);
263
+ return result;
264
+ }
265
+ // Render title — use first heal as representative
266
+ const first = heals[0];
267
+ const titleVars = {
268
+ testName: first.testTitle ?? "automation",
269
+ fileName: path.basename(first.sourceFile),
270
+ count: heals.length,
271
+ };
272
+ const title = render(cfg.titleTemplate, titleVars);
273
+ const reportLink = ctx.reportHtmlPath
274
+ ? path.relative(ctx.cwd, ctx.reportHtmlPath)
275
+ : cfg.reportArtifactPath;
276
+ const body = cfg.bodyTemplate
277
+ ? render(cfg.bodyTemplate, {
278
+ ...titleVars,
279
+ summary: `${heals.length} heal(s)`,
280
+ reportLink,
281
+ reasoning: heals.map((h) => h.aiExplanation).join("\n\n"),
282
+ })
283
+ : buildDefaultBody(heals, branchInfo, reportLink, decision);
284
+ // ── directCommit ────────────────────────────────────────────────
285
+ if (decision.effective === "directCommit") {
286
+ for (const f of filesToStage) {
287
+ const add = sh(`git add "${f}"`, ctx.cwd);
288
+ if (!add.ok)
289
+ result.errors.push(`git add ${f}: ${add.stderr}`);
290
+ }
291
+ const commit = sh(`git commit -m ${shellQuote(title)}`, ctx.cwd);
292
+ if (!commit.ok) {
293
+ result.errors.push(`git commit: ${commit.stderr}`);
294
+ console.warn(`[Sela PR] ⚠️ git commit failed: ${commit.stderr}`);
295
+ return result;
296
+ }
297
+ console.log(`[Sela PR] ✅ Direct commit on '${branchInfo.branch}': ${title}`);
298
+ const push = sh(`git push`, ctx.cwd);
299
+ if (!push.ok) {
300
+ result.errors.push(`git push: ${push.stderr}`);
301
+ console.warn(`[Sela PR] ⚠️ git push failed: ${push.stderr}`);
302
+ return result;
303
+ }
304
+ console.log(`[Sela PR] 🚀 Pushed to '${branchInfo.branch}'`);
305
+ return result;
306
+ }
307
+ // ── pr / draftPR ────────────────────────────────────────────────
308
+ if (decision.effective === "pr" || decision.effective === "draftPR") {
309
+ if (!ghAvailable(ctx.cwd)) {
310
+ result.errors.push("gh CLI not available");
311
+ console.warn(`[Sela PR] ⚠️ gh CLI not installed — cannot open PR. Run \`gh auth login\` after install.`);
312
+ return result;
313
+ }
314
+ const id = String(Date.now());
315
+ const branchName = render(cfg.branchNameTemplate, { id, timestamp: id });
316
+ const checkout = sh(`git checkout -b ${branchName}`, ctx.cwd);
317
+ if (!checkout.ok) {
318
+ result.errors.push(`git checkout: ${checkout.stderr}`);
319
+ console.warn(`[Sela PR] ⚠️ Could not create branch '${branchName}': ${checkout.stderr}`);
320
+ return result;
321
+ }
322
+ for (const f of filesToStage) {
323
+ const add = sh(`git add "${f}"`, ctx.cwd);
324
+ if (!add.ok)
325
+ result.errors.push(`git add ${f}: ${add.stderr}`);
326
+ }
327
+ const commit = sh(`git commit -m ${shellQuote(title)}`, ctx.cwd);
328
+ if (!commit.ok) {
329
+ result.errors.push(`git commit: ${commit.stderr}`);
330
+ console.warn(`[Sela PR] ⚠️ git commit failed: ${commit.stderr}`);
331
+ return result;
332
+ }
333
+ const push = sh(`git push -u origin ${branchName}`, ctx.cwd);
334
+ if (!push.ok) {
335
+ result.errors.push(`git push: ${push.stderr}`);
336
+ console.warn(`[Sela PR] ⚠️ git push failed: ${push.stderr}`);
337
+ return result;
338
+ }
339
+ result.branchPushed = branchName;
340
+ // Write body to a temp file to avoid shell-escaping headaches
341
+ const bodyFile = path.join(ctx.cwd, `.sela-pr-body-${id}.md`);
342
+ fs.writeFileSync(bodyFile, body, "utf8");
343
+ const labelFlag = cfg.labels.length > 0
344
+ ? cfg.labels.map((l) => `--label ${shellQuote(l)}`).join(" ")
345
+ : "";
346
+ const draftFlag = decision.effective === "draftPR" ? "--draft" : "";
347
+ const baseFlag = branchInfo.branch ? `--base ${shellQuote(branchInfo.branch)}` : "";
348
+ const prCmd = `gh pr create ${draftFlag} ${baseFlag} ` +
349
+ `--title ${shellQuote(title)} ` +
350
+ `--body-file ${shellQuote(bodyFile)} ` +
351
+ `${labelFlag}`;
352
+ const pr = sh(prCmd, ctx.cwd);
353
+ try {
354
+ fs.unlinkSync(bodyFile);
355
+ }
356
+ catch { /* ignore */ }
357
+ if (!pr.ok) {
358
+ result.errors.push(`gh pr create: ${pr.stderr}`);
359
+ console.warn(`[Sela PR] ⚠️ gh pr create failed: ${pr.stderr}`);
360
+ return result;
361
+ }
362
+ result.prUrl = pr.stdout.trim();
363
+ console.log(`[Sela PR] ✅ ${decision.effective === "draftPR" ? "Draft PR" : "PR"} opened: ${result.prUrl}`);
364
+ return result;
365
+ }
366
+ return result;
367
+ }
368
+ // ═══════════════════════════════════════════════════════════════════
369
+ // BUG-DETECTED HANDLING
370
+ // ═══════════════════════════════════════════════════════════════════
371
+ async function handleBugDetected(action, events, branchInfo, ctx) {
372
+ if (events.length === 0)
373
+ return;
374
+ console.warn(`\n[Sela Protection] ⚠️ ${events.length} potential regression(s) detected — auto-fix blocked.`);
375
+ for (const e of events) {
376
+ console.warn(` • ${e.sourceFile}:${e.sourceLine} — ${e.reason} (auditor: ${e.auditor?.verdict ?? "n/a"})`);
377
+ }
378
+ if (action === "failCI") {
379
+ console.warn(`[Sela Protection] 🔴 onBugDetected='failCI' — setting process.exitCode = 1`);
380
+ process.exitCode = 1;
381
+ return;
382
+ }
383
+ if (action === "createIssue") {
384
+ if (!ghAvailable(ctx.cwd)) {
385
+ console.warn(`[Sela Protection] ⚠️ gh CLI unavailable — cannot create issue.`);
386
+ return;
387
+ }
388
+ for (const e of events) {
389
+ const id = String(Date.now());
390
+ const title = `[Sela Alert] Potential Bug Detected: ${e.testTitle ?? path.basename(e.sourceFile)}`;
391
+ const body = [
392
+ `## 🛑 Sela detected a potential regression`,
393
+ ``,
394
+ `* **File:** \`${e.sourceFile}:${e.sourceLine}\``,
395
+ `* **Test:** ${e.testTitle ?? "(unknown)"}`,
396
+ `* **Reason:** ${e.reason}`,
397
+ `* **Safety Level:** ${e.safetyLevel}`,
398
+ `* **Auditor:** ${e.auditor?.verdict ?? "n/a"} (${e.auditor?.confidence ?? "n/a"}%)`,
399
+ ``,
400
+ `### Old selector`,
401
+ `\`${e.oldSelector}\``,
402
+ ``,
403
+ `### AI candidate (BLOCKED)`,
404
+ `\`${e.candidateNewSelector ?? "(none)"}\``,
405
+ ``,
406
+ `### AI explanation`,
407
+ `> ${e.aiExplanation || "(none)"}`,
408
+ ].join("\n");
409
+ const bodyFile = path.join(ctx.cwd, `.sela-issue-body-${id}.md`);
410
+ fs.writeFileSync(bodyFile, body, "utf8");
411
+ const cmd = `gh issue create --title ${shellQuote(title)} ` +
412
+ `--body-file ${shellQuote(bodyFile)} ` +
413
+ `--label sela-bug-detected`;
414
+ const r = sh(cmd, ctx.cwd);
415
+ try {
416
+ fs.unlinkSync(bodyFile);
417
+ }
418
+ catch { /* ignore */ }
419
+ if (!r.ok) {
420
+ console.warn(`[Sela Protection] ⚠️ gh issue create failed: ${r.stderr}`);
421
+ }
422
+ else {
423
+ console.log(`[Sela Protection] 📝 Issue created: ${r.stdout.trim()}`);
424
+ }
425
+ }
426
+ return;
427
+ }
428
+ if (action === "labelOnly") {
429
+ if (!ghAvailable(ctx.cwd)) {
430
+ console.warn(`[Sela Protection] ⚠️ gh CLI unavailable — cannot label PR.`);
431
+ return;
432
+ }
433
+ if (!branchInfo.isPR || !branchInfo.prNumber) {
434
+ console.warn(`[Sela Protection] ⚠️ Not running on a PR — labelOnly skipped.`);
435
+ return;
436
+ }
437
+ const labelCmd = `gh pr edit ${branchInfo.prNumber} --add-label sela-bug-detected`;
438
+ const labelRes = sh(labelCmd, ctx.cwd);
439
+ if (!labelRes.ok) {
440
+ console.warn(`[Sela Protection] ⚠️ gh pr edit failed: ${labelRes.stderr}`);
441
+ return;
442
+ }
443
+ const comment = `⚠️ Sela detected ${events.length} potential regression(s). Review before merging.`;
444
+ const commentRes = sh(`gh pr comment ${branchInfo.prNumber} --body ${shellQuote(comment)}`, ctx.cwd);
445
+ if (!commentRes.ok) {
446
+ console.warn(`[Sela Protection] ⚠️ gh pr comment failed: ${commentRes.stderr}`);
447
+ }
448
+ else {
449
+ console.log(`[Sela Protection] 🏷️ PR #${branchInfo.prNumber} labeled + commented.`);
450
+ }
451
+ }
452
+ }
453
+ // ═══════════════════════════════════════════════════════════════════
454
+ // SHELL QUOTING — cross-platform (POSIX single-quote escape;
455
+ // on Windows execSync uses cmd.exe which handles double-quotes natively).
456
+ // ═══════════════════════════════════════════════════════════════════
457
+ function shellQuote(s) {
458
+ if (process.platform === "win32") {
459
+ // cmd.exe: escape inner double-quotes by doubling them.
460
+ return `"${s.replace(/"/g, '""')}"`;
461
+ }
462
+ // POSIX: wrap in single quotes; close-and-reopen for embedded single quotes.
463
+ return `'${s.replace(/'/g, "'\\''")}'`;
464
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"SnapshotService.d.ts","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EACL,eAAe,EACf,iBAAiB,EAElB,MAAM,UAAU,CAAC;AAElB,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAAS;;IASlB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAYvD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAwBpD,cAAc,CAClB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAM,EAAO,GACvB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAoRpC,OAAO,CAAC,WAAW;CAsBpB"}
1
+ {"version":3,"file":"SnapshotService.d.ts","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAkB,MAAM,UAAU,CAAC;AAE9E,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAAS;;IASlB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAYvD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAsBpD,cAAc,CAClB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAM,EAAO,GACvB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAoRpC,OAAO,CAAC,WAAW;CAsBpB"}
@@ -50,10 +50,10 @@ class SnapshotService {
50
50
  const filePath = path.join(this.baseDir, `${key}.json`);
51
51
  try {
52
52
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
53
- console.log(`[Fixwright] 📸 Snapshot saved: ${key}.json`);
53
+ console.log(`[Sela] 📸 Snapshot saved: ${key}.json`);
54
54
  }
55
55
  catch (err) {
56
- console.error(`[Fixwright] ❌ Failed to save snapshot:`, err);
56
+ console.error(`[Sela] ❌ Failed to save snapshot:`, err);
57
57
  }
58
58
  }
59
59
  async load(key) {
@@ -163,8 +163,8 @@ class SnapshotService {
163
163
  }
164
164
  else if (cs.pointerEvents === "none") {
165
165
  // Child exception: ghost only if no immediate child re-enables pointer events
166
- const hasInteractableChild = Array.from(el.children).some((child) => window.getComputedStyle(child).pointerEvents !==
167
- "none");
166
+ const hasInteractableChild = Array.from(el.children).some((child) => window.getComputedStyle(child)
167
+ .pointerEvents !== "none");
168
168
  if (!hasInteractableChild) {
169
169
  isGhost = true;
170
170
  ghostReason = "G4_POINTER_EVENTS_NONE";
@@ -237,8 +237,7 @@ class SnapshotService {
237
237
  let sib = el.previousElementSibling;
238
238
  while (sib) {
239
239
  const sibCs = window.getComputedStyle(sib);
240
- if (sibCs.display !== "none" &&
241
- sibCs.visibility !== "hidden") {
240
+ if (sibCs.display !== "none" && sibCs.visibility !== "hidden") {
242
241
  const t = (sib.textContent || "").trim();
243
242
  if (t && t.length < 80) {
244
243
  closestLabel = t;
@@ -314,7 +313,7 @@ class SnapshotService {
314
313
  };
315
314
  }
316
315
  catch (err) {
317
- console.debug(`[Fixwright] 🧬 captureElement skipped for "${selector}": ${err.message}`);
316
+ console.debug(`[Sela] 🧬 captureElement skipped for "${selector}": ${err.message}`);
318
317
  return null;
319
318
  }
320
319
  }
@@ -326,7 +325,7 @@ class SnapshotService {
326
325
  // the engine decides when to flush.
327
326
  // ─────────────────────────────────────────────────────────────
328
327
  migrateToV2(v1) {
329
- console.log(`[Fixwright] 🔄 Migrating v1 snapshot to v2 schema (key: "${v1.tagName}/${v1.id ?? "no-id"}")`);
328
+ console.log(`[Sela] 🔄 Migrating v1 snapshot to v2 schema (key: "${v1.tagName}/${v1.id ?? "no-id"}")`);
330
329
  return {
331
330
  ...v1,
332
331
  schemaVersion: 2,
@@ -1 +1 @@
1
- {"version":3,"file":"SourceUpdater.d.ts","sourceRoot":"","sources":["../../src/services/SourceUpdater.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAMtD,UAAU,YAAY;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,cAAc,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,aAAa;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AA0YD,qBAAa,aAAa;IAMxB,MAAM,CAAC,MAAM,CACX,MAAM,EAAE,aAAa,EACrB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,MAAM,EACpB,mBAAmB,CAAC,EAAE,MAAM,EAC5B,UAAU,CAAC,EAAE,eAAe,EAAE,EAC9B,aAAa,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EACpD,kBAAkB,CAAC,EAAE,iBAAiB,EAAE,EACxC,iBAAiB,CAAC,EAAE,iBAAiB,EAAE,EACvC,cAAc,CAAC,EAAE,SAAS,GAAG,aAAa,GAAG,QAAQ,EACrD,UAAU,CAAC,EAAE,OAAO,GACnB,YAAY;IAoDf,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAwC/B,OAAO,CAAC,MAAM,CAAC,WAAW;IAwS1B,OAAO,CAAC,MAAM,CAAC,2BAA2B;IA8E1C,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA8EvC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAoErC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAsBjC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IA6DpC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAuCpC,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA6CvC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAqCrC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAqGrC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IA0BjC,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAatC,MAAM,CAAC,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IASlD,MAAM,CAAC,cAAc,CACnB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,YAAY;IA0Cf,0FAA0F;IAC1F,MAAM,CAAC,eAAe,IAAI,IAAI;CAG/B"}
1
+ {"version":3,"file":"SourceUpdater.d.ts","sourceRoot":"","sources":["../../src/services/SourceUpdater.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAMtD,UAAU,YAAY;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,cAAc,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,aAAa;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AA0YD,qBAAa,aAAa;IAMxB,MAAM,CAAC,MAAM,CACX,MAAM,EAAE,aAAa,EACrB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,MAAM,EACpB,mBAAmB,CAAC,EAAE,MAAM,EAC5B,UAAU,CAAC,EAAE,eAAe,EAAE,EAC9B,aAAa,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EACpD,kBAAkB,CAAC,EAAE,iBAAiB,EAAE,EACxC,iBAAiB,CAAC,EAAE,iBAAiB,EAAE,EACvC,cAAc,CAAC,EAAE,SAAS,GAAG,aAAa,GAAG,QAAQ,EACrD,UAAU,CAAC,EAAE,OAAO,GACnB,YAAY;IAoDf,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAwC/B,OAAO,CAAC,MAAM,CAAC,WAAW;IAwS1B,OAAO,CAAC,MAAM,CAAC,2BAA2B;IA8E1C,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA8EvC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAoErC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAsBjC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IA6DpC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAuCpC,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA6CvC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAqCrC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAqGrC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IA0BjC,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAatC,MAAM,CAAC,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAgBlD,MAAM,CAAC,cAAc,CACnB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,YAAY;IA0Cf,0FAA0F;IAC1F,MAAM,CAAC,eAAe,IAAI,IAAI;CAG/B"}
@@ -471,7 +471,7 @@ class SourceUpdater {
471
471
  if (!contentChange)
472
472
  return;
473
473
  console.log(`[SourceUpdater] 🔍 Checking for assertion mismatches...`);
474
- console.warn(`\n[Fixwright-Advice] 💡 To fix the assertion at line ${caller.line}, ` +
474
+ console.warn(`\n[Sela] 💡 To fix the assertion at line ${caller.line}, ` +
475
475
  `change to: .toHaveText("${contentChange.newText}")\n`);
476
476
  };
477
477
  // ── Strategy 0: Layered Frame Healing ──────────────────────
@@ -971,13 +971,24 @@ class SourceUpdater {
971
971
  return domValueMatch?.[1] ?? null;
972
972
  }
973
973
  static resolveFilePath(raw) {
974
+ if (!raw || typeof raw !== "string")
975
+ return null;
974
976
  let clean = raw
975
977
  .replace(/^.*?at\s+/, "")
976
978
  .replace(/:\d+:\d+.*$/, "")
977
979
  .trim();
980
+ if (!clean)
981
+ return null;
978
982
  if (!path.isAbsolute(clean))
979
983
  clean = path.resolve(process.cwd(), clean);
980
- return fs.existsSync(clean) ? clean : null;
984
+ if (!fs.existsSync(clean))
985
+ return null;
986
+ // Reject directories: fs.readFileSync on a dir throws EISDIR. This guards
987
+ // against ctx.filePath being empty → path.resolve(cwd, '') → cwd.
988
+ const stat = fs.statSync(clean);
989
+ if (!stat.isFile())
990
+ return null;
991
+ return clean;
981
992
  }
982
993
  static updateArgument(caller, action, oldArgument, newArgument) {
983
994
  const filePath = SourceUpdater.resolveFilePath(caller.filePath);
@@ -15,7 +15,7 @@ class SnapshotManager {
15
15
  await promises_1.default.writeFile(filePath, JSON.stringify(data, null, 2));
16
16
  }
17
17
  catch (error) {
18
- console.error(`[Fixwright] Failed to save snapshot: ${error}`);
18
+ console.error(`[Sela] Failed to save snapshot: ${error}`);
19
19
  }
20
20
  }
21
21
  static async load(filePath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sela-core",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "AI self-healing Playwright wrapper — drop-in replacement for @playwright/test",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -55,7 +55,7 @@
55
55
  "ts-morph": "^27.0.2"
56
56
  },
57
57
  "devDependencies": {
58
- "@playwright/test": "^1.58.2",
58
+ "@playwright/test": "^1.60.0",
59
59
  "@types/jest": "^30.0.0",
60
60
  "@types/node": "^25.6.2",
61
61
  "jest": "^30.3.0",