sela-core 1.0.5 → 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.
@@ -15,13 +15,30 @@ export declare class SelaEngine {
15
15
  private _toReportFile;
16
16
  private _safeReadLine;
17
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).
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.
23
27
  */
24
- private _safeReadAllLines;
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;
25
42
  private _auditorBlockFromDecision;
26
43
  private _buildAlternatives;
27
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;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"}
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;
@@ -262,7 +271,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
262
271
  reason: safetyDecision.reason,
263
272
  safetyLevel: safetyDecision.level,
264
273
  auditor: this._auditorBlockFromDecision(safetyDecision.meta),
265
- aiConfidence: typeof aiFix.confidence === "number" ? aiFix.confidence : 0,
274
+ aiConfidence: aiFix.confidence,
266
275
  aiExplanation: aiFix.explanation ?? "",
267
276
  dnaBefore: (0, HealReportService_1.summariseSnapshot)(lastKnownState),
268
277
  dnaAfter: (0, HealReportService_1.summariseSnapshot)(liveSnapshot ?? null),
@@ -281,16 +290,11 @@ 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
- // 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);
294
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);
295
299
  // Resolve the line that was actually mutated. Order of precedence:
296
300
  // 1. updateResult.lineUpdated (the strategy's literal write location)
@@ -308,15 +312,20 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
308
312
  (sameFileDef ? defSite?.line : undefined) ??
309
313
  line;
310
314
  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);
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);
320
329
  const canonicalSelector = updateResult.healedLocatorString ?? newFullSelector;
321
330
  console.log(`[Sela] ✅ Canonical selector (disk == runtime): "${canonicalSelector}"`);
322
331
  // Buffer CLI-ready metadata — merged into DNA at commitUpdates() time.
@@ -364,7 +373,8 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
364
373
  oldCodeLine,
365
374
  newCodeLine,
366
375
  newLineNumber: writtenLine,
367
- aiConfidence: typeof aiFix.confidence === "number" ? aiFix.confidence : 0,
376
+ gitUnifiedDiff: gitUnifiedDiff || undefined,
377
+ aiConfidence: aiFix.confidence,
368
378
  aiExplanation: aiFix.explanation ?? "",
369
379
  aiAlternatives: this._buildAlternatives(aiFix.chainSegments, aiFix.segments),
370
380
  reasoningSteps: this._buildReasoningSteps({
@@ -461,30 +471,94 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
461
471
  }
462
472
  }
463
473
  /**
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).
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.
469
483
  */
470
- _safeReadAllLines(filePath) {
484
+ _captureGitDiff(filePath) {
471
485
  try {
472
486
  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/);
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();
477
500
  }
478
- catch {
479
- return [];
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;
480
547
  }
548
+ return { oldLine, newLine };
481
549
  }
482
550
  _auditorBlockFromDecision(meta) {
483
551
  if (!meta || !meta.auditorVerdict)
484
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;
485
559
  return {
486
560
  verdict: meta.auditorVerdict,
487
- confidence: meta.auditorConfidence ?? 0,
561
+ confidence: conf,
488
562
  reason: meta.auditScoreBreakdown
489
563
  ? `Adjusted ${meta.auditScoreBreakdown.rawConfidence}% → ${meta.auditScoreBreakdown.adjustedConfidence}% (penalty ${meta.auditScoreBreakdown.penalty}, bonus +${meta.auditScoreBreakdown.bonus}).`
490
564
  : "Auditor verdict recorded by SafetyGuard.",
@@ -562,7 +636,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
562
636
  if (ctx.aiFix.explanation) {
563
637
  steps.push(`AI reasoning: ${ctx.aiFix.explanation}`);
564
638
  }
565
- steps.push(`Confidence: ${ctx.aiFix.confidence}%.`);
639
+ steps.push(`Confidence: ${fmtPctEngine(ctx.aiFix.confidence)}.`);
566
640
  if (ctx.aiFix.contentChange) {
567
641
  steps.push(`Detected content drift: "${ctx.aiFix.contentChange.oldText}" → "${ctx.aiFix.contentChange.newText}".`);
568
642
  }
@@ -615,7 +689,8 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
615
689
  if (decision.downgradeReason) {
616
690
  console.warn(`[Sela PR] ⚠️ Strategy downgraded: '${decision.configured}' → '${decision.effective}' ` +
617
691
  `(reason: ${decision.downgradeReason}, ` +
618
- `minAI: ${decision.minAiConfidence}%, minAuditor: ${decision.minAuditorConfidence}%)`);
692
+ `minAI: ${fmtPctEngine(decision.minAiConfidence)}, ` +
693
+ `minAuditor: ${fmtPctEngine(decision.minAuditorConfidence)})`);
619
694
  }
620
695
  else {
621
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;AA2ED,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,UAAU,CA8EpE;AAMD,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,oBAAoB,EACzB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,WAAW,EAAE,GACnB,gBAAgB,CAuDlB;AA6DD,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,UAAU,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wBAAsB,OAAO,CAC3B,GAAG,EAAE,oBAAoB,EACzB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,WAAW,EAAE,EACpB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,eAAe,CAAC,CA4J1B;AAMD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,cAAc,EAAE,EACxB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,IAAI,CAAC,CA0Ff"}
1
+ {"version":3,"file":"PRAutomationService.d.ts","sourceRoot":"","sources":["../../src/services/PRAutomationService.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,SAAS,EACV,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EACV,WAAW,EACX,cAAc,EACf,MAAM,qBAAqB,CAAC;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"));
@@ -186,12 +187,19 @@ function detectBranch(cwd = process.cwd()) {
186
187
  // STRATEGY DECISION
187
188
  // ═══════════════════════════════════════════════════════════════════
188
189
  function decideEffectiveStrategy(cfg, branch, heals) {
189
- const minAi = heals.length === 0
190
- ? 100
191
- : Math.min(...heals.map((h) => h.aiConfidence ?? 100));
192
- const minAud = heals.length === 0
193
- ? 100
194
- : 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;
195
203
  const baseDecision = {
196
204
  effective: cfg.strategy,
197
205
  configured: cfg.strategy,
@@ -209,8 +217,11 @@ function decideEffectiveStrategy(cfg, branch, heals) {
209
217
  downgradeReason: "PROTECTED_BRANCH",
210
218
  };
211
219
  }
212
- // 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.
213
223
  if (cfg.reviewThresholds.minConfidenceForDirectCommit > 0 &&
224
+ typeof minAi === "number" &&
214
225
  minAi < cfg.reviewThresholds.minConfidenceForDirectCommit) {
215
226
  return {
216
227
  ...baseDecision,
@@ -218,8 +229,9 @@ function decideEffectiveStrategy(cfg, branch, heals) {
218
229
  downgradeReason: "LOW_CONFIDENCE",
219
230
  };
220
231
  }
221
- // Rule 3: low auditor score
232
+ // Rule 3: low auditor score — same "absence ≠ zero" rule as Rule 2.
222
233
  if (cfg.reviewThresholds.minAuditorScoreForDirectCommit > 0 &&
234
+ typeof minAud === "number" &&
223
235
  minAud < cfg.reviewThresholds.minAuditorScoreForDirectCommit) {
224
236
  return {
225
237
  ...baseDecision,
@@ -235,6 +247,40 @@ function decideEffectiveStrategy(cfg, branch, heals) {
235
247
  function fmtPct(v) {
236
248
  return typeof v === "number" && Number.isFinite(v) ? `${v}%` : "n/a";
237
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
+ }
238
284
  function buildDefaultBody(heals, branchInfo, reportLink, decision) {
239
285
  const lines = [];
240
286
  lines.push(`# 🤖 Sela Insights — Automation Suite Healed!`);
@@ -256,8 +302,17 @@ function buildDefaultBody(heals, branchInfo, reportLink, decision) {
256
302
  lines.push(`### \`${h.sourceFile}:${lineRef}\` — ${h.testTitle ?? "(no test title)"}`);
257
303
  lines.push("");
258
304
  lines.push("```diff");
259
- lines.push(`- ${(h.oldCodeLine ?? "").trim()}`);
260
- 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
+ }
261
316
  lines.push("```");
262
317
  if (h.aiExplanation) {
263
318
  lines.push(`> 🧠 ${h.aiExplanation}`);
@@ -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.5",
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",