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.
- package/dist/engine/SelaEngine.d.ts +25 -0
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +130 -11
- package/dist/services/HealReportService.d.ts +20 -3
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +105 -14
- package/dist/services/LLMService.d.ts +6 -1
- package/dist/services/LLMService.d.ts.map +1 -1
- package/dist/services/LLMService.js +30 -4
- package/dist/services/PRAutomationService.d.ts +9 -2
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +126 -26
- package/dist/services/SafetyGuard.d.ts +7 -1
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SafetyGuard.js +7 -1
- package/package.json +1 -1
|
@@ -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;
|
|
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}
|
|
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
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
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:
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAMlD,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE9D,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;
|
|
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
|
|
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 + '
|
|
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 + '
|
|
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
|
-
|
|
951
|
-
const
|
|
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
|
-
|
|
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":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAQtC,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,iBAAiB,GACzB;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IAEjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACD;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC1C;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,GACD;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAEpD;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACvD;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACrD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACD;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnC,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,GAAG,WAAW,CAAC;IAC9B;;;OAGG;IACH,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B;;OAEG;IACH,aAAa,CAAC,EAAE,iBAAiB,EAAE,CAAC;IACpC,iBAAiB,CAAC,EAAE,iBAAiB,EAAE,CAAC;IACxC,UAAU,EAAE,MAAM,CAAC;
|
|
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:
|
|
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
|
-
|
|
14
|
-
|
|
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;
|
|
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 &&
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
isPR: false,
|
|
127
|
-
|
|
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
|
|
139
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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}
|
|
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
|
-
|
|
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;
|
|
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 {
|
|
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
|