sela-core 1.0.4 → 1.0.6

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,31 @@ 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
+ * Capture the native `git diff --unified=1 HEAD -- <file>` for the file the
19
+ * AST mutation just wrote to. Returns an empty string when git is not
20
+ * available, the file is not tracked, or any other failure — callers must
21
+ * treat empty output as "no diff captured" and fall back to a direct disk
22
+ * read of the mutated line.
23
+ *
24
+ * Path safety: the absolute path is always wrapped in double quotes after
25
+ * escaping any embedded double quotes, which makes the call safe on Windows
26
+ * paths containing spaces or backslashes as well as POSIX paths.
27
+ */
28
+ private _captureGitDiff;
29
+ /**
30
+ * Walk a unified diff and pull out the first removed line and the first
31
+ * added line so the legacy report fields (`oldCodeLine`, `newCodeLine`)
32
+ * stay populated for callers that haven't migrated to the full
33
+ * `gitUnifiedDiff` payload yet.
34
+ *
35
+ * Carefully skips the diff preamble (--- a/file, +++ b/file, "diff ",
36
+ * "index ", rename headers) — those lines would otherwise be mistaken for
37
+ * a -/+ change pair and produce nonsense like the original "line replaced
38
+ * by itself" output. Real change lines only count once the parser has seen
39
+ * a "@@" hunk header.
40
+ */
41
+ private _extractFirstChangeFromDiff;
17
42
  private _auditorBlockFromDecision;
18
43
  private _buildAlternatives;
19
44
  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;AAOxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,OAAO,EAAe,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAyChE,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;IAyclB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,aAAa;IAarB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,eAAe;IAwBvB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,2BAA2B;IA4BnC,OAAO,CAAC,yBAAyB;IAwBjC,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;YA6FtB,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"}
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.SelaEngine = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
+ const child_process_1 = require("child_process");
39
40
  const LLMService_1 = require("../services/LLMService");
40
41
  const SnapshotService_1 = require("../services/SnapshotService");
41
42
  const DOMUtils_1 = require("../utils/DOMUtils");
@@ -44,6 +45,14 @@ const SafetyGuard_1 = require("../services/SafetyGuard");
44
45
  const ConfigLoader_1 = require("../config/ConfigLoader");
45
46
  const HealReportService_1 = require("../services/HealReportService");
46
47
  const PRAutomationService_1 = require("../services/PRAutomationService");
48
+ /**
49
+ * Graceful percentage formatter for engine-side log lines. Mirrors the helper
50
+ * in PRAutomationService / HealReportService so a missing AI/auditor
51
+ * confidence reads as "n/a" instead of "undefined%" or "0%".
52
+ */
53
+ function fmtPctEngine(v) {
54
+ return typeof v === "number" && Number.isFinite(v) ? `${v}%` : "n/a";
55
+ }
47
56
  class SelaEngine {
48
57
  llmService;
49
58
  snapshotService;
@@ -281,17 +290,42 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
281
290
  parts.push(`${breakdown.penalty} ancestry drift`);
282
291
  if (breakdown.bonus !== 0)
283
292
  parts.push(`+${breakdown.bonus} anchor match`);
284
- console.log(`[Sela] 📊 Score — AI: ${aiFix.confidence}% | ` +
285
- `Auditor: ${breakdown.adjustedConfidence}% ` +
286
- `(raw ${breakdown.rawConfidence}%, ${parts.join(", ")})`);
293
+ console.log(`[Sela] 📊 Score — AI: ${fmtPctEngine(aiFix.confidence)} | ` +
294
+ `Auditor: ${fmtPctEngine(breakdown.adjustedConfidence)} ` +
295
+ `(raw ${fmtPctEngine(breakdown.rawConfidence)}, ${parts.join(", ")})`);
287
296
  }
288
297
  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);
291
298
  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);
299
+ // Resolve the line that was actually mutated. Order of precedence:
300
+ // 1. updateResult.lineUpdated (the strategy's literal write location)
301
+ // 2. updateResult.definitionSite.line (semantic JumpToDef target,
302
+ // used when the mutation crossed into a const/variable declaration)
303
+ // 3. line (the original failure line — last-resort fallback)
304
+ // For cross-file definitionSite.file, we still read the new line from
305
+ // the SAME file that was mutated.
306
+ const defSite = updateResult.definitionSite;
307
+ const sameFileDef = defSite && defSite.file
308
+ ? path.resolve(defSite.file) ===
309
+ (SourceUpdater_1.SourceUpdater.resolveFilePath(filePath) ?? "")
310
+ : true;
311
+ const writtenLine = updateResult.lineUpdated ??
312
+ (sameFileDef ? defSite?.line : undefined) ??
313
+ line;
314
+ const mutatedFilePath = !sameFileDef && defSite ? defSite.file : filePath;
315
+ // Capture the native unified diff straight from git. This runs AFTER
316
+ // SourceUpdater flushed the file to disk and BEFORE any `git add` —
317
+ // so `git diff HEAD` reports the exact pending change at the real
318
+ // mutated line. No in-process line-index math, no off-by-one when
319
+ // JumpToDef lands above the failure site, and cross-file healing
320
+ // falls out for free.
321
+ const gitUnifiedDiff = this._captureGitDiff(mutatedFilePath);
322
+ const firstChange = this._extractFirstChangeFromDiff(gitUnifiedDiff);
323
+ // Backward-compat: keep oldCodeLine/newCodeLine populated from the
324
+ // first -/+ pair inside the diff. When git produced no diff (file
325
+ // untracked, git unavailable, …) fall back to a direct disk read of
326
+ // the mutated line — strictly inferior but preserves old reports.
327
+ const oldCodeLine = firstChange.oldLine ?? this._safeReadLine(mutatedFilePath, writtenLine);
328
+ const newCodeLine = firstChange.newLine ?? this._safeReadLine(mutatedFilePath, writtenLine);
295
329
  const canonicalSelector = updateResult.healedLocatorString ?? newFullSelector;
296
330
  console.log(`[Sela] ✅ Canonical selector (disk == runtime): "${canonicalSelector}"`);
297
331
  // Buffer CLI-ready metadata — merged into DNA at commitUpdates() time.
@@ -339,6 +373,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
339
373
  oldCodeLine,
340
374
  newCodeLine,
341
375
  newLineNumber: writtenLine,
376
+ gitUnifiedDiff: gitUnifiedDiff || undefined,
342
377
  aiConfidence: aiFix.confidence,
343
378
  aiExplanation: aiFix.explanation ?? "",
344
379
  aiAlternatives: this._buildAlternatives(aiFix.chainSegments, aiFix.segments),
@@ -435,12 +470,95 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
435
470
  return "";
436
471
  }
437
472
  }
473
+ /**
474
+ * Capture the native `git diff --unified=1 HEAD -- <file>` for the file the
475
+ * AST mutation just wrote to. Returns an empty string when git is not
476
+ * available, the file is not tracked, or any other failure — callers must
477
+ * treat empty output as "no diff captured" and fall back to a direct disk
478
+ * read of the mutated line.
479
+ *
480
+ * Path safety: the absolute path is always wrapped in double quotes after
481
+ * escaping any embedded double quotes, which makes the call safe on Windows
482
+ * paths containing spaces or backslashes as well as POSIX paths.
483
+ */
484
+ _captureGitDiff(filePath) {
485
+ try {
486
+ const cleaned = this._toReportFile(filePath);
487
+ const abs = path.isAbsolute(cleaned)
488
+ ? cleaned
489
+ : path.resolve(process.cwd(), cleaned);
490
+ if (!abs || !fs.existsSync(abs))
491
+ return "";
492
+ const quoted = `"${abs.replace(/"/g, '\\"')}"`;
493
+ const out = (0, child_process_1.execSync)(`git diff --unified=1 HEAD -- ${quoted}`, {
494
+ cwd: process.cwd(),
495
+ encoding: "utf-8",
496
+ stdio: ["pipe", "pipe", "pipe"],
497
+ windowsHide: true,
498
+ });
499
+ return out.toString();
500
+ }
501
+ catch (err) {
502
+ // git missing / not a repo / file untracked vs HEAD → log & fall back.
503
+ console.debug?.(`[Sela] git diff capture skipped: ${err?.message ?? err}`);
504
+ return "";
505
+ }
506
+ }
507
+ /**
508
+ * Walk a unified diff and pull out the first removed line and the first
509
+ * added line so the legacy report fields (`oldCodeLine`, `newCodeLine`)
510
+ * stay populated for callers that haven't migrated to the full
511
+ * `gitUnifiedDiff` payload yet.
512
+ *
513
+ * Carefully skips the diff preamble (--- a/file, +++ b/file, "diff ",
514
+ * "index ", rename headers) — those lines would otherwise be mistaken for
515
+ * a -/+ change pair and produce nonsense like the original "line replaced
516
+ * by itself" output. Real change lines only count once the parser has seen
517
+ * a "@@" hunk header.
518
+ */
519
+ _extractFirstChangeFromDiff(diff) {
520
+ if (!diff)
521
+ return {};
522
+ let inHunk = false;
523
+ let oldLine;
524
+ let newLine;
525
+ for (const row of diff.split(/\r?\n/)) {
526
+ if (!inHunk) {
527
+ if (row.startsWith("@@"))
528
+ inHunk = true;
529
+ continue;
530
+ }
531
+ // Defensive: a fresh @@ header starts a new hunk but does not reset
532
+ // the values we've already captured.
533
+ if (row.startsWith("@@"))
534
+ continue;
535
+ if (row.startsWith("---") || row.startsWith("+++"))
536
+ continue;
537
+ if (row.startsWith("\\"))
538
+ continue;
539
+ if (row.startsWith("-") && oldLine === undefined) {
540
+ oldLine = row.slice(1);
541
+ }
542
+ else if (row.startsWith("+") && newLine === undefined) {
543
+ newLine = row.slice(1);
544
+ }
545
+ if (oldLine !== undefined && newLine !== undefined)
546
+ break;
547
+ }
548
+ return { oldLine, newLine };
549
+ }
438
550
  _auditorBlockFromDecision(meta) {
439
551
  if (!meta || !meta.auditorVerdict)
440
552
  return null;
553
+ // Keep auditor confidence as `undefined` when SafetyGuard did not produce
554
+ // one — the renderer fmtPct helper turns it into "n/a" rather than 0%.
555
+ const conf = typeof meta.auditorConfidence === "number" &&
556
+ Number.isFinite(meta.auditorConfidence)
557
+ ? meta.auditorConfidence
558
+ : undefined;
441
559
  return {
442
560
  verdict: meta.auditorVerdict,
443
- confidence: meta.auditorConfidence ?? 0,
561
+ confidence: conf,
444
562
  reason: meta.auditScoreBreakdown
445
563
  ? `Adjusted ${meta.auditScoreBreakdown.rawConfidence}% → ${meta.auditScoreBreakdown.adjustedConfidence}% (penalty ${meta.auditScoreBreakdown.penalty}, bonus +${meta.auditScoreBreakdown.bonus}).`
446
564
  : "Auditor verdict recorded by SafetyGuard.",
@@ -518,7 +636,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
518
636
  if (ctx.aiFix.explanation) {
519
637
  steps.push(`AI reasoning: ${ctx.aiFix.explanation}`);
520
638
  }
521
- steps.push(`Confidence: ${ctx.aiFix.confidence}%.`);
639
+ steps.push(`Confidence: ${fmtPctEngine(ctx.aiFix.confidence)}.`);
522
640
  if (ctx.aiFix.contentChange) {
523
641
  steps.push(`Detected content drift: "${ctx.aiFix.contentChange.oldText}" → "${ctx.aiFix.contentChange.newText}".`);
524
642
  }
@@ -571,7 +689,8 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
571
689
  if (decision.downgradeReason) {
572
690
  console.warn(`[Sela PR] ⚠️ Strategy downgraded: '${decision.configured}' → '${decision.effective}' ` +
573
691
  `(reason: ${decision.downgradeReason}, ` +
574
- `minAI: ${decision.minAiConfidence}%, minAuditor: ${decision.minAuditorConfidence}%)`);
692
+ `minAI: ${fmtPctEngine(decision.minAiConfidence)}, ` +
693
+ `minAuditor: ${fmtPctEngine(decision.minAuditorConfidence)})`);
575
694
  }
576
695
  else {
577
696
  console.log(`[Sela PR] 🎯 Effective strategy: '${decision.effective}' on branch '${branchInfo.branch ?? "unknown"}'`);
@@ -17,7 +17,11 @@ export interface DnaSummary {
17
17
  }
18
18
  export interface AuditorBlock {
19
19
  verdict: AuditVerdictValue;
20
- confidence: number;
20
+ /**
21
+ * Adjusted confidence 0-100. `undefined` when the auditor produced no score
22
+ * (e.g. SafetyGuard bypassed it) — reports MUST render this as "n/a".
23
+ */
24
+ confidence?: number;
21
25
  reason: string;
22
26
  inversionType: AuditVerdict["inversionType"];
23
27
  rawConfidence?: number;
@@ -39,10 +43,22 @@ export interface HealedEvent extends HealEventBase {
39
43
  kind: "HEALED";
40
44
  oldSelector: string;
41
45
  newSelector: string;
46
+ /** First removed line from the native git diff (backward-compat fallback). */
42
47
  oldCodeLine: string;
48
+ /** First added line from the native git diff (backward-compat fallback). */
43
49
  newCodeLine: string;
44
50
  newLineNumber: number;
45
- aiConfidence: number;
51
+ /**
52
+ * Raw `git diff --unified=1` output captured immediately after the AST
53
+ * mutation hit disk and BEFORE any `git add`. Includes headers (---/+++)
54
+ * and one or more @@ hunks. Renderers strip headers; PR body emits the
55
+ * hunks verbatim inside a ```diff fence so cross-file healing, multi-line
56
+ * edits, and template-literal changes are all displayed natively without
57
+ * any in-process line-index math.
58
+ */
59
+ gitUnifiedDiff?: string;
60
+ /** AI-reported confidence 0-100. `undefined` when the model omitted it. */
61
+ aiConfidence?: number;
46
62
  aiExplanation: string;
47
63
  aiAlternatives: string[];
48
64
  reasoningSteps: string[];
@@ -75,7 +91,8 @@ export interface ProtectedEvent extends HealEventBase {
75
91
  reason: string;
76
92
  safetyLevel: string;
77
93
  auditor: AuditorBlock | null;
78
- aiConfidence: number;
94
+ /** AI-reported confidence 0-100. `undefined` when the model omitted it. */
95
+ aiConfidence?: number;
79
96
  aiExplanation: string;
80
97
  dnaBefore: DnaSummary | null;
81
98
  dnaAfter: DnaSummary | null;
@@ -1 +1 @@
1
- {"version":3,"file":"HealReportService.d.ts","sourceRoot":"","sources":["../../src/services/HealReportService.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"HealReportService.d.ts","sourceRoot":"","sources":["../../src/services/HealReportService.ts"],"names":[],"mappings":"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"}
@@ -752,6 +752,13 @@ const REPORT_JS = String.raw `
752
752
  try { return new Date(iso).toLocaleString(); } catch { return iso; }
753
753
  }
754
754
  function pluralize(n, one, many) { return n === 1 ? one : (many || one + "s"); }
755
+ // Graceful percentage formatter — mirrors the server-side helper in
756
+ // PRAutomationService. Anything that is not a finite number renders "n/a"
757
+ // instead of being coerced to 0%. Used in every spot that previously
758
+ // hard-coded a plain "x + %" concatenation.
759
+ function fmtPct(v) {
760
+ return (typeof v === "number" && isFinite(v)) ? (v + "%") : "n/a";
761
+ }
755
762
 
756
763
  // ── Hero meta + KPIs ──────────────────────────────────────────
757
764
  $("#meta-generated").textContent = "Generated " + fmtTime(DATA.generatedAt);
@@ -905,15 +912,16 @@ const REPORT_JS = String.raw `
905
912
  function renderAuditor(a) {
906
913
  if (!a) return '<div class="muted-block">The Intent Auditor was not consulted for this event (no AI fix attempted, or auditor unavailable).</div>';
907
914
  const verdictCls = a.verdict.toLowerCase();
908
- const pct = Math.max(0, Math.min(100, a.confidence));
915
+ const hasPct = typeof a.confidence === "number" && isFinite(a.confidence);
916
+ const pct = hasPct ? Math.max(0, Math.min(100, a.confidence)) : 0;
909
917
  const radius = 56;
910
918
  const circ = 2 * Math.PI * radius;
911
- const dash = (pct / 100) * circ;
919
+ const dash = hasPct ? (pct / 100) * circ : 0;
912
920
  const adjustments = [];
913
921
  if (a.penalty != null && a.penalty !== 0) adjustments.push("ancestry drift " + a.penalty);
914
922
  if (a.bonus != null && a.bonus !== 0) adjustments.push("anchor +" + a.bonus);
915
923
  const adjLine = adjustments.length
916
- ? '<div class="audit-row"><span class="label">Score adjustments</span><span class="value">raw ' + a.rawConfidence + '% → ' + pct + '% (' + adjustments.join(", ") + ')</span></div>'
924
+ ? '<div class="audit-row"><span class="label">Score adjustments</span><span class="value">raw ' + fmtPct(a.rawConfidence) + ' → ' + fmtPct(hasPct ? pct : undefined) + ' (' + adjustments.join(", ") + ')</span></div>'
917
925
  : "";
918
926
  return (
919
927
  '<div class="auditor-grid">' +
@@ -924,7 +932,7 @@ const REPORT_JS = String.raw `
924
932
  'stroke-dasharray="' + dash.toFixed(2) + ' ' + circ.toFixed(2) + '"/>' +
925
933
  '</svg>' +
926
934
  '<div class="center">' +
927
- '<div class="pct">' + pct + '%</div>' +
935
+ '<div class="pct">' + (hasPct ? pct + "%" : "n/a") + '</div>' +
928
936
  '<div class="pct-label">Consistency</div>' +
929
937
  '</div>' +
930
938
  '</div>' +
@@ -938,6 +946,58 @@ const REPORT_JS = String.raw `
938
946
  );
939
947
  }
940
948
 
949
+ // Parse "git diff --unified=N" output into structured hunks. Skips file
950
+ // headers ("diff ", "index ", "--- ", "+++ ", rename/similarity lines) and
951
+ // walks each "@@ -a,b +c,d @@" block, tracking real old/new line numbers
952
+ // so the rendered diff shows correct line refs even when the AST mutation
953
+ // landed several lines above the failure site (JumpToDef scenario).
954
+ function parseUnifiedDiff(raw) {
955
+ const out = [];
956
+ if (!raw) return out;
957
+ const rows = raw.split(/\r?\n/);
958
+ let cur = null;
959
+ let oldLn = 0, newLn = 0;
960
+ for (const r of rows) {
961
+ if (
962
+ r.startsWith("diff ") ||
963
+ r.startsWith("index ") ||
964
+ r.startsWith("--- ") ||
965
+ r.startsWith("+++ ") ||
966
+ r.startsWith("new file mode") ||
967
+ r.startsWith("deleted file mode") ||
968
+ r.startsWith("similarity ") ||
969
+ r.startsWith("rename ") ||
970
+ r.startsWith("Binary files")
971
+ ) continue;
972
+ const hunk = r.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
973
+ if (hunk) {
974
+ if (cur) out.push(cur);
975
+ cur = { header: r, lines: [] };
976
+ oldLn = parseInt(hunk[1], 10);
977
+ newLn = parseInt(hunk[3], 10);
978
+ continue;
979
+ }
980
+ if (!cur) continue;
981
+ // "" + truly empty trailing lines: ignore
982
+ if (r.startsWith("\\")) continue;
983
+ if (r.length === 0) continue;
984
+ const head = r.charAt(0);
985
+ const text = r.slice(1);
986
+ if (head === "+") {
987
+ cur.lines.push({ type: "add", text, ln: newLn });
988
+ newLn++;
989
+ } else if (head === "-") {
990
+ cur.lines.push({ type: "del", text, ln: oldLn });
991
+ oldLn++;
992
+ } else if (head === " ") {
993
+ cur.lines.push({ type: "ctx", text, ln: newLn });
994
+ oldLn++; newLn++;
995
+ }
996
+ }
997
+ if (cur) out.push(cur);
998
+ return out;
999
+ }
1000
+
941
1001
  function renderDiff(ev) {
942
1002
  if (ev.kind !== "HEALED") {
943
1003
  if (ev.kind === "PROTECTED") {
@@ -947,8 +1007,45 @@ const REPORT_JS = String.raw `
947
1007
  }
948
1008
  return '<div class="muted-block">No source code was modified — the AI could not propose a confident fix for this selector.</div>';
949
1009
  }
950
- const oldLine = ev.oldCodeLine || "";
951
- const newLine = ev.newCodeLine || "";
1010
+
1011
+ const raw = ev.gitUnifiedDiff || "";
1012
+ const hunks = parseUnifiedDiff(raw);
1013
+
1014
+ let bodyHtml;
1015
+ let sourceTag;
1016
+ if (hunks.length > 0) {
1017
+ sourceTag = "native git diff";
1018
+ bodyHtml = hunks.map(h => {
1019
+ const hdr =
1020
+ '<div class="diff-line ctx">' +
1021
+ '<span class="gutter">@</span><span class="ln"></span><span class="code">' + esc(h.header) + '</span>' +
1022
+ '</div>';
1023
+ const body = h.lines.map(L => {
1024
+ const gutter = L.type === "add" ? "+" : L.type === "del" ? "-" : " ";
1025
+ return '<div class="diff-line ' + L.type + '">' +
1026
+ '<span class="gutter">' + gutter + '</span>' +
1027
+ '<span class="ln">' + L.ln + '</span>' +
1028
+ '<span class="code">' + esc(L.text) + '</span>' +
1029
+ '</div>';
1030
+ }).join("");
1031
+ return hdr + body;
1032
+ }).join("");
1033
+ } else {
1034
+ // Fallback when git diff capture was unavailable (no git, untracked
1035
+ // file, etc.). Render the single before/after pair the engine carried
1036
+ // over from the legacy code path.
1037
+ sourceTag = "fallback (no git diff captured)";
1038
+ const oldLine = ev.oldCodeLine || "";
1039
+ const newLine = ev.newCodeLine || "";
1040
+ bodyHtml =
1041
+ '<div class="diff-line del">' +
1042
+ '<span class="gutter">-</span><span class="ln">' + (ev.newLineNumber || "") + '</span><span class="code">' + esc(oldLine) + '</span>' +
1043
+ '</div>' +
1044
+ '<div class="diff-line add">' +
1045
+ '<span class="gutter">+</span><span class="ln">' + (ev.newLineNumber || "") + '</span><span class="code">' + esc(newLine) + '</span>' +
1046
+ '</div>';
1047
+ }
1048
+
952
1049
  return (
953
1050
  '<div class="diff">' +
954
1051
  '<div class="diff-head">' +
@@ -957,15 +1054,9 @@ const REPORT_JS = String.raw `
957
1054
  '<span>line ' + ev.newLineNumber + '</span>' +
958
1055
  (ev.diffStrategy ? '<span class="muted">·</span><span>strategy: ' + esc(ev.diffStrategy) + '</span>' : "") +
959
1056
  (ev.blastRadius != null ? '<span class="muted">·</span><span>blast radius: ' + ev.blastRadius + '</span>' : "") +
1057
+ '<span class="muted">·</span><span>' + sourceTag + '</span>' +
960
1058
  '</div>' +
961
- '<div class="diff-body">' +
962
- '<div class="diff-line del">' +
963
- '<span class="gutter">-</span><span class="ln">' + ev.newLineNumber + '</span><span class="code">' + esc(oldLine) + '</span>' +
964
- '</div>' +
965
- '<div class="diff-line add">' +
966
- '<span class="gutter">+</span><span class="ln">' + ev.newLineNumber + '</span><span class="code">' + esc(newLine) + '</span>' +
967
- '</div>' +
968
- '</div>' +
1059
+ '<div class="diff-body">' + bodyHtml + '</div>' +
969
1060
  '</div>'
970
1061
  );
971
1062
  }
@@ -76,7 +76,12 @@ export interface FixResponse {
76
76
  */
77
77
  chainSegments?: SmartChainSegment[];
78
78
  originalChainHint?: SmartChainSegment[];
79
- confidence: number;
79
+ /**
80
+ * Confidence score 0-100. Undefined when the model omitted it or returned a
81
+ * non-finite value — downstream code must treat undefined as "unknown" and
82
+ * render "n/a" rather than synthesising a 0.
83
+ */
84
+ confidence?: number;
80
85
  explanation: string;
81
86
  new_selector?: string | null;
82
87
  contentChange?: ContentChange;
@@ -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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"}
@@ -103,21 +103,34 @@ The healing system depends on the attribute value being present in your response
103
103
  If the element's text changed, you MUST include:
104
104
  "contentChange": { "oldText": "<previous>", "newText": "<current>" }
105
105
 
106
+ ### CONFIDENCE SCORE (MANDATORY)
107
+ You MUST include a numeric "confidence" field — an INTEGER in the range 0-100 —
108
+ representing how certain you are that the returned chain will resolve the
109
+ intended element on the next test run. Use this rubric:
110
+ - 90-100: stable id, data-testid, or unique role+name — zero ambiguity in the DOM.
111
+ - 70-89: strong semantic match, but some structural drift or competing candidates.
112
+ - 50-69: best-guess match — multiple plausible candidates required disambiguation.
113
+ - 1-49: speculative match — return this only when no better candidate exists.
114
+ NEVER omit this field. NEVER return it as a string, boolean, or null.
115
+ Reports rendered downstream will show "n/a" when this field is missing — which
116
+ is a clear failure signal to the developer reviewing the heal.
117
+
106
118
  ### MANDATORY OUTPUT FORMAT
107
119
  Return ONLY valid JSON.
108
120
 
109
121
  Example output — Identifying a specific button in a list row:
110
122
  {
111
123
  "status": "FIXED",
124
+ "confidence": 92,
112
125
  "chainSegments": [
113
126
  { "type": "locator", "selector": ".user-item" },
114
127
  { "type": "filter", "hasText": "Bob Smith" },
115
128
  { "type": "getByRole", "role": "button", "name": "Modify Bob" }
116
129
  ],
117
130
  "segments": [
118
- {
119
- "type": "element",
120
- "selector": ".user-item >> internal:has-text=\"Bob Smith\"i >> internal:role=button[name=\"Modify Bob\"i]"
131
+ {
132
+ "type": "element",
133
+ "selector": ".user-item >> internal:has-text=\"Bob Smith\"i >> internal:role=button[name=\"Modify Bob\"i]"
121
134
  }
122
135
  ],
123
136
  "explanation": "Updated container to .user-item and targeted the button by its new semantic name 'Modify Bob' to ensure a unique match."
@@ -343,6 +356,19 @@ Return ONLY a raw JSON object — no markdown, no preamble.`;
343
356
  if (!jsonString)
344
357
  throw new Error("AI did not return a valid JSON object");
345
358
  const parsed = JSON.parse(jsonString);
359
+ // ── Normalise confidence: keep only finite numbers in [0,100] ─────
360
+ // Anything else (undefined, NaN, null, string, out-of-range) collapses
361
+ // to `undefined` so reports render "n/a" instead of synthesising 0.
362
+ const rawConf = parsed.confidence;
363
+ if (typeof rawConf === "number" && Number.isFinite(rawConf)) {
364
+ parsed.confidence = Math.max(0, Math.min(100, Math.round(rawConf)));
365
+ }
366
+ else {
367
+ if (rawConf !== undefined) {
368
+ console.warn(`[LLMService] ⚠️ Discarded non-numeric confidence: ${JSON.stringify(rawConf)}`);
369
+ }
370
+ parsed.confidence = undefined;
371
+ }
346
372
  // ── Normalize chainSegments → segments (legacy) ───────────────────
347
373
  if (parsed.chainSegments && Array.isArray(parsed.chainSegments)) {
348
374
  // Reconstruct a flat new_selector from chain segments for legacy paths
@@ -430,7 +456,7 @@ Return ONLY a raw JSON object — no markdown, no preamble.`;
430
456
  status: "NOT_FOUND",
431
457
  segments: [],
432
458
  new_selector: null,
433
- confidence: 0,
459
+ confidence: undefined,
434
460
  explanation: `AI Error: ${error.message}`,
435
461
  };
436
462
  }
@@ -10,8 +10,14 @@ export interface StrategyDecision {
10
10
  effective: PRStrategy;
11
11
  configured: PRStrategy;
12
12
  downgradeReason: "PROTECTED_BRANCH" | "LOW_CONFIDENCE" | "LOW_AUDITOR" | null;
13
- minAiConfidence: number;
14
- minAuditorConfidence: number;
13
+ /**
14
+ * Lowest AI confidence observed across this session's heals, or `undefined`
15
+ * when no heal carried a numeric confidence (LLM omitted the field). Render
16
+ * via `fmtPct` so the absence reads as "n/a" instead of 0%.
17
+ */
18
+ minAiConfidence: number | undefined;
19
+ /** Same semantics as `minAiConfidence`, for the auditor's adjusted score. */
20
+ minAuditorConfidence: number | undefined;
15
21
  }
16
22
  export interface ExecuteContext {
17
23
  cwd: string;
@@ -19,6 +25,7 @@ export interface ExecuteContext {
19
25
  }
20
26
  export declare function detectBranch(cwd?: string): BranchInfo;
21
27
  export declare function decideEffectiveStrategy(cfg: ResolvedPRAutomation, branch: string | null, heals: HealedEvent[]): StrategyDecision;
28
+ export declare function fmtPct(v: unknown): string;
22
29
  export interface ExecutionResult {
23
30
  effective: PRStrategy;
24
31
  prUrl: string | 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;;;;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,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"}
@@ -48,6 +48,7 @@ var __importStar = (this && this.__importStar) || (function () {
48
48
  Object.defineProperty(exports, "__esModule", { value: true });
49
49
  exports.detectBranch = detectBranch;
50
50
  exports.decideEffectiveStrategy = decideEffectiveStrategy;
51
+ exports.fmtPct = fmtPct;
51
52
  exports.execute = execute;
52
53
  exports.handleBugDetected = handleBugDetected;
53
54
  const fs = __importStar(require("fs"));
@@ -82,6 +83,30 @@ function gitAvailable(cwd) {
82
83
  function isDetachedHead(cwd) {
83
84
  return !sh("git symbolic-ref HEAD", cwd).ok;
84
85
  }
86
+ const SELA_BRANCH_PREFIX = "sela/heal-";
87
+ function isSelaHealBranch(branch) {
88
+ return !!branch && branch.startsWith(SELA_BRANCH_PREFIX);
89
+ }
90
+ /**
91
+ * Resolve the repo's default integration branch.
92
+ * 1. origin/HEAD symbolic-ref (set by `git clone`).
93
+ * 2. First existing local ref among: main, master, develop, dev.
94
+ * Returns null if none found — caller must handle that.
95
+ */
96
+ function findDefaultBranch(cwd) {
97
+ const headRef = sh("git symbolic-ref refs/remotes/origin/HEAD", cwd);
98
+ if (headRef.ok) {
99
+ const m = headRef.stdout.trim().match(/refs\/remotes\/origin\/(.+)$/);
100
+ if (m)
101
+ return m[1];
102
+ }
103
+ for (const candidate of ["main", "master", "develop", "dev"]) {
104
+ if (sh(`git rev-parse --verify --quiet ${candidate}`, cwd).ok) {
105
+ return candidate;
106
+ }
107
+ }
108
+ return null;
109
+ }
85
110
  // ═══════════════════════════════════════════════════════════════════
86
111
  // TEMPLATING
87
112
  // ═══════════════════════════════════════════════════════════════════
@@ -98,14 +123,17 @@ function detectBranch(cwd = process.cwd()) {
98
123
  const ghEventName = process.env.GITHUB_EVENT_NAME;
99
124
  const ghRef = process.env.GITHUB_REF; // "refs/pull/123/merge"
100
125
  // Prefer HEAD_REF on PRs — REF_NAME is the synthetic merge ref.
101
- if (ghHeadRef && ghHeadRef.length > 0) {
126
+ if (ghHeadRef && ghHeadRef.length > 0 && !isSelaHealBranch(ghHeadRef)) {
102
127
  const prNumber = (() => {
103
128
  const m = ghRef?.match(/refs\/pull\/(\d+)\//);
104
129
  return m ? Number(m[1]) : null;
105
130
  })();
106
131
  return { branch: ghHeadRef, isPR: true, prNumber, source: "env" };
107
132
  }
108
- if (ghRefName && ghRefName.length > 0 && !ghRefName.endsWith("/merge")) {
133
+ if (ghRefName &&
134
+ ghRefName.length > 0 &&
135
+ !ghRefName.endsWith("/merge") &&
136
+ !isSelaHealBranch(ghRefName)) {
109
137
  return {
110
138
  branch: ghRefName,
111
139
  isPR: ghEventName === "pull_request",
@@ -115,18 +143,16 @@ function detectBranch(cwd = process.cwd()) {
115
143
  }
116
144
  // GitLab CI fallback
117
145
  const gitlabRef = process.env.CI_COMMIT_REF_NAME;
118
- if (gitlabRef && gitlabRef.length > 0) {
146
+ if (gitlabRef && gitlabRef.length > 0 && !isSelaHealBranch(gitlabRef)) {
119
147
  return { branch: gitlabRef, isPR: false, prNumber: null, source: "env" };
120
148
  }
121
149
  // Generic Jenkins / local fallback
122
150
  const jenkinsBranch = process.env.GIT_BRANCH;
123
151
  if (jenkinsBranch && jenkinsBranch.length > 0) {
124
- return {
125
- branch: jenkinsBranch.replace(/^origin\//, ""),
126
- isPR: false,
127
- prNumber: null,
128
- source: "env",
129
- };
152
+ const cleaned = jenkinsBranch.replace(/^origin\//, "");
153
+ if (!isSelaHealBranch(cleaned)) {
154
+ return { branch: cleaned, isPR: false, prNumber: null, source: "env" };
155
+ }
130
156
  }
131
157
  // Local: git CLI
132
158
  if (!gitAvailable(cwd)) {
@@ -135,22 +161,45 @@ function detectBranch(cwd = process.cwd()) {
135
161
  const r = sh("git rev-parse --abbrev-ref HEAD", cwd);
136
162
  if (!r.ok)
137
163
  return { branch: null, isPR: false, prNumber: null, source: "none" };
138
- const branch = r.stdout.trim();
139
- if (!branch || branch === "HEAD") {
164
+ const localBranch = r.stdout.trim();
165
+ if (!localBranch || localBranch === "HEAD") {
140
166
  return { branch: null, isPR: false, prNumber: null, source: "none" };
141
167
  }
142
- return { branch, isPR: false, prNumber: null, source: "git" };
168
+ // ── Sela self-branch guard ─────────────────────────────────────────
169
+ // If we're sitting on a previously-created `sela/heal-*` branch (typical
170
+ // local-rerun scenario), do NOT use it as the PR base. Fall back to the
171
+ // repo's default integration branch (main / master / develop) so the new
172
+ // PR targets a real branch, not another sela patch branch.
173
+ if (isSelaHealBranch(localBranch)) {
174
+ const fallback = findDefaultBranch(cwd);
175
+ if (fallback) {
176
+ console.warn(`[Sela PR] ⚠️ HEAD is on '${localBranch}' (sela patch branch) — ` +
177
+ `using '${fallback}' as PR base instead.`);
178
+ return { branch: fallback, isPR: false, prNumber: null, source: "git" };
179
+ }
180
+ console.warn(`[Sela PR] ⚠️ HEAD is on '${localBranch}' and no default branch found — ` +
181
+ `skipping PR automation to avoid self-targeting.`);
182
+ return { branch: null, isPR: false, prNumber: null, source: "none" };
183
+ }
184
+ return { branch: localBranch, isPR: false, prNumber: null, source: "git" };
143
185
  }
144
186
  // ═══════════════════════════════════════════════════════════════════
145
187
  // STRATEGY DECISION
146
188
  // ═══════════════════════════════════════════════════════════════════
147
189
  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));
190
+ // Compute MIN across only the heals that reported a finite numeric
191
+ // confidence. When *no* heal carried one, the result is `undefined` —
192
+ // surfaced as "n/a" in reports rather than synthesised as 100% (which
193
+ // would silently mask the absence of evidence) or 0% (which would
194
+ // incorrectly force a downgrade for every clean run on Sonnet).
195
+ const aiVals = heals
196
+ .map((h) => h.aiConfidence)
197
+ .filter((v) => typeof v === "number" && Number.isFinite(v));
198
+ const audVals = heals
199
+ .map((h) => h.auditor?.confidence)
200
+ .filter((v) => typeof v === "number" && Number.isFinite(v));
201
+ const minAi = aiVals.length > 0 ? Math.min(...aiVals) : undefined;
202
+ const minAud = audVals.length > 0 ? Math.min(...audVals) : undefined;
154
203
  const baseDecision = {
155
204
  effective: cfg.strategy,
156
205
  configured: cfg.strategy,
@@ -168,8 +217,11 @@ function decideEffectiveStrategy(cfg, branch, heals) {
168
217
  downgradeReason: "PROTECTED_BRANCH",
169
218
  };
170
219
  }
171
- // Rule 2: low AI confidence
220
+ // Rule 2: low AI confidence — only triggers when the measured min is
221
+ // actually below the threshold. Missing confidence is "unknown", not
222
+ // "zero", so we do NOT downgrade purely on absence.
172
223
  if (cfg.reviewThresholds.minConfidenceForDirectCommit > 0 &&
224
+ typeof minAi === "number" &&
173
225
  minAi < cfg.reviewThresholds.minConfidenceForDirectCommit) {
174
226
  return {
175
227
  ...baseDecision,
@@ -177,8 +229,9 @@ function decideEffectiveStrategy(cfg, branch, heals) {
177
229
  downgradeReason: "LOW_CONFIDENCE",
178
230
  };
179
231
  }
180
- // Rule 3: low auditor score
232
+ // Rule 3: low auditor score — same "absence ≠ zero" rule as Rule 2.
181
233
  if (cfg.reviewThresholds.minAuditorScoreForDirectCommit > 0 &&
234
+ typeof minAud === "number" &&
182
235
  minAud < cfg.reviewThresholds.minAuditorScoreForDirectCommit) {
183
236
  return {
184
237
  ...baseDecision,
@@ -191,6 +244,43 @@ function decideEffectiveStrategy(cfg, branch, heals) {
191
244
  // ═══════════════════════════════════════════════════════════════════
192
245
  // PR BODY RENDERING
193
246
  // ═══════════════════════════════════════════════════════════════════
247
+ function fmtPct(v) {
248
+ return typeof v === "number" && Number.isFinite(v) ? `${v}%` : "n/a";
249
+ }
250
+ /**
251
+ * Strip the file-level preamble from a unified diff so the GitHub Markdown
252
+ * renderer doesn't show `--- a/file` / `+++ b/file` as red/green lines (which
253
+ * read as fake deletions/additions in a ```diff fence). Keeps the `@@` hunk
254
+ * header and every -/+/space content row intact, so the rendered block is
255
+ * still syntactically a valid unified diff that GitHub highlights correctly.
256
+ */
257
+ function stripDiffPreamble(raw) {
258
+ return raw
259
+ .split(/\r?\n/)
260
+ .filter((ln) => {
261
+ if (ln.startsWith("diff "))
262
+ return false;
263
+ if (ln.startsWith("index "))
264
+ return false;
265
+ if (ln.startsWith("--- "))
266
+ return false;
267
+ if (ln.startsWith("+++ "))
268
+ return false;
269
+ if (ln.startsWith("new file mode"))
270
+ return false;
271
+ if (ln.startsWith("deleted file mode"))
272
+ return false;
273
+ if (ln.startsWith("similarity "))
274
+ return false;
275
+ if (ln.startsWith("rename "))
276
+ return false;
277
+ if (ln.startsWith("Binary files"))
278
+ return false;
279
+ return true;
280
+ })
281
+ .join("\n")
282
+ .replace(/\n+$/, "");
283
+ }
194
284
  function buildDefaultBody(heals, branchInfo, reportLink, decision) {
195
285
  const lines = [];
196
286
  lines.push(`# 🤖 Sela Insights — Automation Suite Healed!`);
@@ -199,8 +289,8 @@ function buildDefaultBody(heals, branchInfo, reportLink, decision) {
199
289
  lines.push("");
200
290
  lines.push(`## 📊 Executive Summary`);
201
291
  lines.push(`* **Heals:** ${heals.length}`);
202
- lines.push(`* **Min AI Confidence:** ${decision.minAiConfidence}%`);
203
- lines.push(`* **Min Auditor Confidence:** ${decision.minAuditorConfidence}%`);
292
+ lines.push(`* **Min AI Confidence:** ${fmtPct(decision.minAiConfidence)}`);
293
+ lines.push(`* **Min Auditor Confidence:** ${fmtPct(decision.minAuditorConfidence)}`);
204
294
  lines.push(`* **Branch:** \`${branchInfo.branch ?? "unknown"}\``);
205
295
  if (decision.downgradeReason) {
206
296
  lines.push(`* **⚠️ Strategy Downgrade:** \`${decision.configured}\` → \`${decision.effective}\` (${decision.downgradeReason})`);
@@ -208,16 +298,26 @@ function buildDefaultBody(heals, branchInfo, reportLink, decision) {
208
298
  lines.push("");
209
299
  lines.push(`## 💻 Per-Heal Diff`);
210
300
  for (const h of heals) {
211
- lines.push(`### \`${h.sourceFile}:${h.sourceLine}\` ${h.testTitle ?? "(no test title)"}`);
301
+ const lineRef = h.newLineNumber ?? h.sourceLine;
302
+ lines.push(`### \`${h.sourceFile}:${lineRef}\` — ${h.testTitle ?? "(no test title)"}`);
212
303
  lines.push("");
213
304
  lines.push("```diff");
214
- lines.push(`- ${h.oldCodeLine.trim()}`);
215
- lines.push(`+ ${h.newCodeLine.trim()}`);
305
+ if (h.gitUnifiedDiff && h.gitUnifiedDiff.trim().length > 0) {
306
+ // Emit the real git diff verbatim. This natively handles cross-file
307
+ // healing, multi-line edits, and template-literal changes — none of
308
+ // which the legacy single-line -/+ pair could represent.
309
+ lines.push(stripDiffPreamble(h.gitUnifiedDiff));
310
+ }
311
+ else {
312
+ // Fallback when git was unavailable / the file was untracked.
313
+ lines.push(`- ${(h.oldCodeLine ?? "").trim()}`);
314
+ lines.push(`+ ${(h.newCodeLine ?? "").trim()}`);
315
+ }
216
316
  lines.push("```");
217
317
  if (h.aiExplanation) {
218
318
  lines.push(`> 🧠 ${h.aiExplanation}`);
219
319
  }
220
- lines.push(`> AI Confidence: **${h.aiConfidence}%** · Auditor: **${h.auditor?.confidence ?? "n/a"}%**`);
320
+ lines.push(`> AI Confidence: **${fmtPct(h.aiConfidence)}** · Auditor: **${fmtPct(h.auditor?.confidence)}**`);
221
321
  lines.push("");
222
322
  }
223
323
  lines.push(`## 🔍 Visual Evidence`);
@@ -8,7 +8,13 @@ export interface HealContext {
8
8
  branch?: string;
9
9
  }
10
10
  export interface AIFixSummary {
11
- confidence: number;
11
+ /**
12
+ * Raw AI confidence 0-100. `undefined` when the LLM omitted the field —
13
+ * SafetyGuard's hard-stop / review-threshold logic treats absence as the
14
+ * conservative worst case (0) so unverified heals are NOT silently let
15
+ * through, while downstream report renderers display "n/a".
16
+ */
17
+ confidence?: number;
12
18
  contentChange?: {
13
19
  oldText: string;
14
20
  newText: string;
@@ -1 +1 @@
1
- {"version":3,"file":"SafetyGuard.d.ts","sourceRoot":"","sources":["../../src/services/SafetyGuard.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAMpE,MAAM,MAAM,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,uEAAuE;IACvE,YAAY,CAAC,EAAE,YAAY,EAAE,CAAC;IAC9B,6DAA6D;IAC7D,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,YAAY,EAAE,CAAC;IAC7B,+CAA+C;IAC/C,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,iBAAiB,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;IACpB,wEAAwE;IACxE,IAAI,CAAC,EAAE;QACL,cAAc,CAAC,EAAE,YAAY,GAAG,YAAY,GAAG,cAAc,CAAC;QAC9D,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,qFAAqF;QACrF,mBAAmB,CAAC,EAAE;YACpB,aAAa,EAAE,MAAM,CAAC;YACtB,OAAO,EAAE,MAAM,CAAC;YAChB,KAAK,EAAE,MAAM,CAAC;YACd,kBAAkB,EAAE,MAAM,CAAC;SAC5B,CAAC;KACH,CAAC;CACH;AA6DD,KAAK,cAAc,GACf,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,MAAM,GACN,iBAAiB,GACjB,iBAAiB,GACjB,SAAS,CAAC;AA2Id,qBAAa,WAAW;IACtB,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,UAAU,CAAqB;gBAE3B,UAAU,CAAC,EAAE,kBAAkB;IASrC,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC;IAkTlF,OAAO,CAAC,kBAAkB;IAY1B,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc;IAsBpD,yBAAyB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM;IAiBvD,OAAO,CAAC,mBAAmB;IAyB3B,OAAO,CAAC,iBAAiB;CAY1B"}
1
+ {"version":3,"file":"SafetyGuard.d.ts","sourceRoot":"","sources":["../../src/services/SafetyGuard.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAMpE,MAAM,MAAM,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,uEAAuE;IACvE,YAAY,CAAC,EAAE,YAAY,EAAE,CAAC;IAC9B,6DAA6D;IAC7D,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,YAAY,EAAE,CAAC;IAC7B,+CAA+C;IAC/C,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,iBAAiB,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;IACpB,wEAAwE;IACxE,IAAI,CAAC,EAAE;QACL,cAAc,CAAC,EAAE,YAAY,GAAG,YAAY,GAAG,cAAc,CAAC;QAC9D,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,qFAAqF;QACrF,mBAAmB,CAAC,EAAE;YACpB,aAAa,EAAE,MAAM,CAAC;YACtB,OAAO,EAAE,MAAM,CAAC;YAChB,KAAK,EAAE,MAAM,CAAC;YACd,kBAAkB,EAAE,MAAM,CAAC;SAC5B,CAAC;KACH,CAAC;CACH;AA6DD,KAAK,cAAc,GACf,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,MAAM,GACN,iBAAiB,GACjB,iBAAiB,GACjB,SAAS,CAAC;AA2Id,qBAAa,WAAW;IACtB,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,UAAU,CAAqB;gBAE3B,UAAU,CAAC,EAAE,kBAAkB;IASrC,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC;IAyTlF,OAAO,CAAC,kBAAkB;IAY1B,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc;IAsBpD,yBAAyB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM;IAiBvD,OAAO,CAAC,mBAAmB;IAyB3B,OAAO,CAAC,iBAAiB;CAY1B"}
@@ -199,7 +199,13 @@ class SafetyGuard {
199
199
  // PUBLIC ENTRY POINT
200
200
  // ─────────────────────────────────────────────────────────────
201
201
  async evaluate(context, aiFix) {
202
- const { confidence, liveText, dnaBaselineText } = aiFix;
202
+ const { liveText, dnaBaselineText } = aiFix;
203
+ // Conservative fallback for missing confidence: treat as 0 so the
204
+ // hardStop / reviewThreshold rules still gate the heal. Reporting layers
205
+ // separately render the absence as "n/a" via fmtPct.
206
+ const confidence = typeof aiFix.confidence === "number" && Number.isFinite(aiFix.confidence)
207
+ ? aiFix.confidence
208
+ : 0;
203
209
  const { mode } = context;
204
210
  // ── 0. Visibility Gate ───────────────────────────────────────
205
211
  // Runs before any confidence or content checks. A ghost element
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sela-core",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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",