sela-core 1.0.4 โ†’ 1.0.5

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.
@@ -14,6 +14,14 @@ export declare class SelaEngine {
14
14
  heal(page: Page, fullSelector: string, elementSelectorOnly: string, stableId: string, filePath: string, line: number, healMode?: HealMode): Promise<string>;
15
15
  private _toReportFile;
16
16
  private _safeReadLine;
17
+ /**
18
+ * Snapshot the entire file as a string[] of lines. Returns an empty array
19
+ * if the file is missing or unreadable. Used to preserve the PRE-mutation
20
+ * source so the diff in HealedEvent reflects the line that was actually
21
+ * mutated (which may differ from the failure line when JumpToDef lands on
22
+ * a const declaration above).
23
+ */
24
+ private _safeReadAllLines;
17
25
  private _auditorBlockFromDecision;
18
26
  private _buildAlternatives;
19
27
  private _buildReasoningSteps;
@@ -1 +1 @@
1
- {"version":3,"file":"SelaEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SelaEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAMxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,OAAO,EAAe,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAgChE,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,eAAe,CAAkC;;IAanD,YAAY,CAChB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAiDnB,IAAI,CACR,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,MAAM,EACpB,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,QAAmB,GAC5B,OAAO,CAAC,MAAM,CAAC;IAyalB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,yBAAyB;IAiBjC,OAAO,CAAC,kBAAkB;IA8B1B,OAAO,CAAC,oBAAoB;IAuC5B,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAIrD,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;YA4FtB,aAAa;IAsB3B,OAAO,CAAC,0BAA0B;IAiClC,OAAO,CAAC,WAAW;IA6Bb,wBAAwB,CAC5B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM;CAqBnB;AAED,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"SelaEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SelaEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAMxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,OAAO,EAAe,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAgChE,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,eAAe,CAAkC;;IAanD,YAAY,CAChB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAiDnB,IAAI,CACR,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,MAAM,EACpB,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,QAAmB,GAC5B,OAAO,CAAC,MAAM,CAAC;IAwclB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,aAAa;IAarB;;;;;;OAMG;IACH,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,yBAAyB;IAiBjC,OAAO,CAAC,kBAAkB;IA8B1B,OAAO,CAAC,oBAAoB;IAuC5B,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAIrD,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;YA4FtB,aAAa;IAsB3B,OAAO,CAAC,0BAA0B;IAiClC,OAAO,CAAC,WAAW;IA6Bb,wBAAwB,CAC5B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM;CAqBnB;AAED,OAAO,EAAE,CAAC"}
@@ -262,7 +262,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
262
262
  reason: safetyDecision.reason,
263
263
  safetyLevel: safetyDecision.level,
264
264
  auditor: this._auditorBlockFromDecision(safetyDecision.meta),
265
- aiConfidence: aiFix.confidence,
265
+ aiConfidence: typeof aiFix.confidence === "number" ? aiFix.confidence : 0,
266
266
  aiExplanation: aiFix.explanation ?? "",
267
267
  dnaBefore: (0, HealReportService_1.summariseSnapshot)(lastKnownState),
268
268
  dnaAfter: (0, HealReportService_1.summariseSnapshot)(liveSnapshot ?? null),
@@ -286,12 +286,37 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
286
286
  `(raw ${breakdown.rawConfidence}%, ${parts.join(", ")})`);
287
287
  }
288
288
  console.log(`[Sela] ๐Ÿš€ Rebuilt Full Runtime Selector (Clean): ${newFullSelector}`);
289
- // Capture old source line BEFORE the AST mutation โ€” needed for the diff.
290
- const oldCodeLine = this._safeReadLine(filePath, line);
289
+ // Snapshot the ENTIRE source file BEFORE the AST mutation.
290
+ // We don't know which line will actually be mutated until update()
291
+ // returns (JumpToDef may land on the const-decl line, not the failure
292
+ // line) โ€” so we keep the pre-image around and slice into it after.
293
+ const preMutationLines = this._safeReadAllLines(filePath);
291
294
  const updateResult = SourceUpdater_1.SourceUpdater.update({ filePath, line }, elementSelectorOnly, newFullSelector, neighborhoodDom, undefined, fullSelector, aiFix.segments, aiFix.contentChange, aiFix.chainSegments, aiFix.originalChainHint, this.config.updateStrategy, this.config.autoCommit);
292
- // Capture new source line AFTER mutation, at the line the updater landed on.
293
- const writtenLine = updateResult.lineUpdated ?? line;
294
- const newCodeLine = this._safeReadLine(filePath, writtenLine);
295
+ // Resolve the line that was actually mutated. Order of precedence:
296
+ // 1. updateResult.lineUpdated (the strategy's literal write location)
297
+ // 2. updateResult.definitionSite.line (semantic JumpToDef target,
298
+ // used when the mutation crossed into a const/variable declaration)
299
+ // 3. line (the original failure line โ€” last-resort fallback)
300
+ // For cross-file definitionSite.file, we still read the new line from
301
+ // the SAME file that was mutated.
302
+ const defSite = updateResult.definitionSite;
303
+ const sameFileDef = defSite && defSite.file
304
+ ? path.resolve(defSite.file) ===
305
+ (SourceUpdater_1.SourceUpdater.resolveFilePath(filePath) ?? "")
306
+ : true;
307
+ const writtenLine = updateResult.lineUpdated ??
308
+ (sameFileDef ? defSite?.line : undefined) ??
309
+ line;
310
+ const mutatedFilePath = !sameFileDef && defSite ? defSite.file : filePath;
311
+ // Old line: PRE-mutation content at the line that ended up being mutated.
312
+ // If the mutation was cross-file, read the pre-image of THAT file by
313
+ // recovering it from disk minus our own change (we don't track that โ€”
314
+ // fall back to current pre-image if same file, else empty string).
315
+ const oldCodeLine = sameFileDef && preMutationLines.length > 0
316
+ ? (preMutationLines[writtenLine - 1] ?? "")
317
+ : this._safeReadLine(mutatedFilePath, writtenLine);
318
+ // New line: POST-mutation content at the same line in the mutated file.
319
+ const newCodeLine = this._safeReadLine(mutatedFilePath, writtenLine);
295
320
  const canonicalSelector = updateResult.healedLocatorString ?? newFullSelector;
296
321
  console.log(`[Sela] โœ… Canonical selector (disk == runtime): "${canonicalSelector}"`);
297
322
  // Buffer CLI-ready metadata โ€” merged into DNA at commitUpdates() time.
@@ -339,7 +364,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
339
364
  oldCodeLine,
340
365
  newCodeLine,
341
366
  newLineNumber: writtenLine,
342
- aiConfidence: aiFix.confidence,
367
+ aiConfidence: typeof aiFix.confidence === "number" ? aiFix.confidence : 0,
343
368
  aiExplanation: aiFix.explanation ?? "",
344
369
  aiAlternatives: this._buildAlternatives(aiFix.chainSegments, aiFix.segments),
345
370
  reasoningSteps: this._buildReasoningSteps({
@@ -435,6 +460,25 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
435
460
  return "";
436
461
  }
437
462
  }
463
+ /**
464
+ * Snapshot the entire file as a string[] of lines. Returns an empty array
465
+ * if the file is missing or unreadable. Used to preserve the PRE-mutation
466
+ * source so the diff in HealedEvent reflects the line that was actually
467
+ * mutated (which may differ from the failure line when JumpToDef lands on
468
+ * a const declaration above).
469
+ */
470
+ _safeReadAllLines(filePath) {
471
+ try {
472
+ const cleaned = this._toReportFile(filePath);
473
+ const abs = path.isAbsolute(cleaned) ? cleaned : path.resolve(process.cwd(), cleaned);
474
+ if (!fs.existsSync(abs))
475
+ return [];
476
+ return fs.readFileSync(abs, "utf8").split(/\r?\n/);
477
+ }
478
+ catch {
479
+ return [];
480
+ }
481
+ }
438
482
  _auditorBlockFromDecision(meta) {
439
483
  if (!meta || !meta.auditorVerdict)
440
484
  return null;
@@ -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;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"}
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"}
@@ -82,6 +82,30 @@ function gitAvailable(cwd) {
82
82
  function isDetachedHead(cwd) {
83
83
  return !sh("git symbolic-ref HEAD", cwd).ok;
84
84
  }
85
+ const SELA_BRANCH_PREFIX = "sela/heal-";
86
+ function isSelaHealBranch(branch) {
87
+ return !!branch && branch.startsWith(SELA_BRANCH_PREFIX);
88
+ }
89
+ /**
90
+ * Resolve the repo's default integration branch.
91
+ * 1. origin/HEAD symbolic-ref (set by `git clone`).
92
+ * 2. First existing local ref among: main, master, develop, dev.
93
+ * Returns null if none found โ€” caller must handle that.
94
+ */
95
+ function findDefaultBranch(cwd) {
96
+ const headRef = sh("git symbolic-ref refs/remotes/origin/HEAD", cwd);
97
+ if (headRef.ok) {
98
+ const m = headRef.stdout.trim().match(/refs\/remotes\/origin\/(.+)$/);
99
+ if (m)
100
+ return m[1];
101
+ }
102
+ for (const candidate of ["main", "master", "develop", "dev"]) {
103
+ if (sh(`git rev-parse --verify --quiet ${candidate}`, cwd).ok) {
104
+ return candidate;
105
+ }
106
+ }
107
+ return null;
108
+ }
85
109
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
86
110
  // TEMPLATING
87
111
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -98,14 +122,17 @@ function detectBranch(cwd = process.cwd()) {
98
122
  const ghEventName = process.env.GITHUB_EVENT_NAME;
99
123
  const ghRef = process.env.GITHUB_REF; // "refs/pull/123/merge"
100
124
  // Prefer HEAD_REF on PRs โ€” REF_NAME is the synthetic merge ref.
101
- if (ghHeadRef && ghHeadRef.length > 0) {
125
+ if (ghHeadRef && ghHeadRef.length > 0 && !isSelaHealBranch(ghHeadRef)) {
102
126
  const prNumber = (() => {
103
127
  const m = ghRef?.match(/refs\/pull\/(\d+)\//);
104
128
  return m ? Number(m[1]) : null;
105
129
  })();
106
130
  return { branch: ghHeadRef, isPR: true, prNumber, source: "env" };
107
131
  }
108
- if (ghRefName && ghRefName.length > 0 && !ghRefName.endsWith("/merge")) {
132
+ if (ghRefName &&
133
+ ghRefName.length > 0 &&
134
+ !ghRefName.endsWith("/merge") &&
135
+ !isSelaHealBranch(ghRefName)) {
109
136
  return {
110
137
  branch: ghRefName,
111
138
  isPR: ghEventName === "pull_request",
@@ -115,18 +142,16 @@ function detectBranch(cwd = process.cwd()) {
115
142
  }
116
143
  // GitLab CI fallback
117
144
  const gitlabRef = process.env.CI_COMMIT_REF_NAME;
118
- if (gitlabRef && gitlabRef.length > 0) {
145
+ if (gitlabRef && gitlabRef.length > 0 && !isSelaHealBranch(gitlabRef)) {
119
146
  return { branch: gitlabRef, isPR: false, prNumber: null, source: "env" };
120
147
  }
121
148
  // Generic Jenkins / local fallback
122
149
  const jenkinsBranch = process.env.GIT_BRANCH;
123
150
  if (jenkinsBranch && jenkinsBranch.length > 0) {
124
- return {
125
- branch: jenkinsBranch.replace(/^origin\//, ""),
126
- isPR: false,
127
- prNumber: null,
128
- source: "env",
129
- };
151
+ const cleaned = jenkinsBranch.replace(/^origin\//, "");
152
+ if (!isSelaHealBranch(cleaned)) {
153
+ return { branch: cleaned, isPR: false, prNumber: null, source: "env" };
154
+ }
130
155
  }
131
156
  // Local: git CLI
132
157
  if (!gitAvailable(cwd)) {
@@ -135,11 +160,27 @@ function detectBranch(cwd = process.cwd()) {
135
160
  const r = sh("git rev-parse --abbrev-ref HEAD", cwd);
136
161
  if (!r.ok)
137
162
  return { branch: null, isPR: false, prNumber: null, source: "none" };
138
- const branch = r.stdout.trim();
139
- if (!branch || branch === "HEAD") {
163
+ const localBranch = r.stdout.trim();
164
+ if (!localBranch || localBranch === "HEAD") {
140
165
  return { branch: null, isPR: false, prNumber: null, source: "none" };
141
166
  }
142
- return { branch, isPR: false, prNumber: null, source: "git" };
167
+ // โ”€โ”€ Sela self-branch guard โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
168
+ // If we're sitting on a previously-created `sela/heal-*` branch (typical
169
+ // local-rerun scenario), do NOT use it as the PR base. Fall back to the
170
+ // repo's default integration branch (main / master / develop) so the new
171
+ // PR targets a real branch, not another sela patch branch.
172
+ if (isSelaHealBranch(localBranch)) {
173
+ const fallback = findDefaultBranch(cwd);
174
+ if (fallback) {
175
+ console.warn(`[Sela PR] โš ๏ธ HEAD is on '${localBranch}' (sela patch branch) โ€” ` +
176
+ `using '${fallback}' as PR base instead.`);
177
+ return { branch: fallback, isPR: false, prNumber: null, source: "git" };
178
+ }
179
+ console.warn(`[Sela PR] โš ๏ธ HEAD is on '${localBranch}' and no default branch found โ€” ` +
180
+ `skipping PR automation to avoid self-targeting.`);
181
+ return { branch: null, isPR: false, prNumber: null, source: "none" };
182
+ }
183
+ return { branch: localBranch, isPR: false, prNumber: null, source: "git" };
143
184
  }
144
185
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
145
186
  // STRATEGY DECISION
@@ -191,6 +232,9 @@ function decideEffectiveStrategy(cfg, branch, heals) {
191
232
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
192
233
  // PR BODY RENDERING
193
234
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
235
+ function fmtPct(v) {
236
+ return typeof v === "number" && Number.isFinite(v) ? `${v}%` : "n/a";
237
+ }
194
238
  function buildDefaultBody(heals, branchInfo, reportLink, decision) {
195
239
  const lines = [];
196
240
  lines.push(`# ๐Ÿค– Sela Insights โ€” Automation Suite Healed!`);
@@ -199,8 +243,8 @@ function buildDefaultBody(heals, branchInfo, reportLink, decision) {
199
243
  lines.push("");
200
244
  lines.push(`## ๐Ÿ“Š Executive Summary`);
201
245
  lines.push(`* **Heals:** ${heals.length}`);
202
- lines.push(`* **Min AI Confidence:** ${decision.minAiConfidence}%`);
203
- lines.push(`* **Min Auditor Confidence:** ${decision.minAuditorConfidence}%`);
246
+ lines.push(`* **Min AI Confidence:** ${fmtPct(decision.minAiConfidence)}`);
247
+ lines.push(`* **Min Auditor Confidence:** ${fmtPct(decision.minAuditorConfidence)}`);
204
248
  lines.push(`* **Branch:** \`${branchInfo.branch ?? "unknown"}\``);
205
249
  if (decision.downgradeReason) {
206
250
  lines.push(`* **โš ๏ธ Strategy Downgrade:** \`${decision.configured}\` โ†’ \`${decision.effective}\` (${decision.downgradeReason})`);
@@ -208,16 +252,17 @@ function buildDefaultBody(heals, branchInfo, reportLink, decision) {
208
252
  lines.push("");
209
253
  lines.push(`## ๐Ÿ’ป Per-Heal Diff`);
210
254
  for (const h of heals) {
211
- lines.push(`### \`${h.sourceFile}:${h.sourceLine}\` โ€” ${h.testTitle ?? "(no test title)"}`);
255
+ const lineRef = h.newLineNumber ?? h.sourceLine;
256
+ lines.push(`### \`${h.sourceFile}:${lineRef}\` โ€” ${h.testTitle ?? "(no test title)"}`);
212
257
  lines.push("");
213
258
  lines.push("```diff");
214
- lines.push(`- ${h.oldCodeLine.trim()}`);
215
- lines.push(`+ ${h.newCodeLine.trim()}`);
259
+ lines.push(`- ${(h.oldCodeLine ?? "").trim()}`);
260
+ lines.push(`+ ${(h.newCodeLine ?? "").trim()}`);
216
261
  lines.push("```");
217
262
  if (h.aiExplanation) {
218
263
  lines.push(`> ๐Ÿง  ${h.aiExplanation}`);
219
264
  }
220
- lines.push(`> AI Confidence: **${h.aiConfidence}%** ยท Auditor: **${h.auditor?.confidence ?? "n/a"}%**`);
265
+ lines.push(`> AI Confidence: **${fmtPct(h.aiConfidence)}** ยท Auditor: **${fmtPct(h.auditor?.confidence)}**`);
221
266
  lines.push("");
222
267
  }
223
268
  lines.push(`## ๐Ÿ” Visual Evidence`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sela-core",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
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",