sela-core 0.1.0-alpha.39 → 0.1.0-alpha.41
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/boot/bootImpl.d.ts +14 -0
- package/dist/boot/bootImpl.d.ts.map +1 -0
- package/dist/boot/bootImpl.js +68 -0
- package/dist/boot/register.cjs +45 -0
- package/dist/boot/register.d.cts +2 -0
- package/dist/boot/register.d.cts.map +1 -0
- package/dist/boot/register.d.mts +2 -0
- package/dist/boot/register.d.mts.map +1 -0
- package/dist/boot/register.mjs +10 -0
- package/dist/cli/commands/init.js +48 -16
- package/dist/cli/commands/merge.d.ts.map +1 -1
- package/dist/cli/commands/merge.js +5 -0
- package/dist/cli/commands/shadowRegression.js +1 -1
- package/dist/config/ConfigLoader.d.ts.map +1 -1
- package/dist/config/ConfigLoader.js +33 -12
- package/dist/config/SelaConfig.d.ts +59 -0
- package/dist/config/SelaConfig.d.ts.map +1 -1
- package/dist/config/SelaConfig.js +36 -0
- package/dist/engine/SelaEngine.d.ts +3 -1
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +193 -3
- package/dist/errors/SelaError.d.ts +7 -0
- package/dist/errors/SelaError.d.ts.map +1 -1
- package/dist/errors/SelaError.js +7 -1
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +5 -4
- package/dist/providers/AnthropicProvider.d.ts +9 -0
- package/dist/providers/AnthropicProvider.d.ts.map +1 -0
- package/dist/providers/AnthropicProvider.js +33 -0
- package/dist/providers/LLMProvider.d.ts +13 -0
- package/dist/providers/LLMProvider.d.ts.map +1 -0
- package/dist/providers/LLMProvider.js +7 -0
- package/dist/providers/OpenAICompatibleProvider.d.ts +8 -0
- package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -0
- package/dist/providers/OpenAICompatibleProvider.js +48 -0
- package/dist/providers/ProviderFactory.d.ts +4 -0
- package/dist/providers/ProviderFactory.d.ts.map +1 -0
- package/dist/providers/ProviderFactory.js +50 -0
- package/dist/providers/SanitizingProvider.d.ts +8 -0
- package/dist/providers/SanitizingProvider.d.ts.map +1 -0
- package/dist/providers/SanitizingProvider.js +27 -0
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
- package/dist/services/ASTSourceUpdater.js +39 -5
- package/dist/services/CrossFileHealer.d.ts +7 -1
- package/dist/services/CrossFileHealer.d.ts.map +1 -1
- package/dist/services/CrossFileHealer.js +243 -9
- package/dist/services/DefinitionTracer.d.ts +1 -0
- package/dist/services/DefinitionTracer.d.ts.map +1 -1
- package/dist/services/DefinitionTracer.js +59 -36
- package/dist/services/HealReportService.d.ts +32 -0
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +111 -18
- package/dist/services/IntentAuditor.d.ts +9 -7
- package/dist/services/IntentAuditor.d.ts.map +1 -1
- package/dist/services/IntentAuditor.js +21 -16
- package/dist/services/LLMService.d.ts +10 -7
- package/dist/services/LLMService.d.ts.map +1 -1
- package/dist/services/LLMService.js +76 -10
- package/dist/services/PRAutomationService.d.ts +7 -2
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +144 -133
- package/dist/services/RegressionManifestService.d.ts +8 -0
- package/dist/services/RegressionManifestService.d.ts.map +1 -1
- package/dist/services/RegressionManifestService.js +8 -0
- package/dist/services/RegressionReclassifier.d.ts +25 -0
- package/dist/services/RegressionReclassifier.d.ts.map +1 -0
- package/dist/services/RegressionReclassifier.js +195 -0
- package/dist/services/ReportMergeService.d.ts +9 -0
- package/dist/services/ReportMergeService.d.ts.map +1 -1
- package/dist/services/ReportMergeService.js +28 -0
- package/dist/services/SmartDOMPruner.d.ts +154 -0
- package/dist/services/SmartDOMPruner.d.ts.map +1 -0
- package/dist/services/SmartDOMPruner.js +678 -0
- package/dist/services/TraceBackEngine.d.ts +23 -6
- package/dist/services/TraceBackEngine.d.ts.map +1 -1
- package/dist/services/TraceBackEngine.js +51 -13
- package/dist/services/VisualSnapshotService.d.ts +54 -0
- package/dist/services/VisualSnapshotService.d.ts.map +1 -0
- package/dist/services/VisualSnapshotService.js +279 -0
- package/dist/services/vcs/GitHubProvider.d.ts +17 -0
- package/dist/services/vcs/GitHubProvider.d.ts.map +1 -0
- package/dist/services/vcs/GitHubProvider.js +162 -0
- package/dist/services/vcs/GitLabProvider.d.ts +17 -0
- package/dist/services/vcs/GitLabProvider.d.ts.map +1 -0
- package/dist/services/vcs/GitLabProvider.js +175 -0
- package/dist/services/vcs/VCSProvider.d.ts +100 -0
- package/dist/services/vcs/VCSProvider.d.ts.map +1 -0
- package/dist/services/vcs/VCSProvider.js +7 -0
- package/dist/services/vcs/VCSProviderFactory.d.ts +7 -0
- package/dist/services/vcs/VCSProviderFactory.d.ts.map +1 -0
- package/dist/services/vcs/VCSProviderFactory.js +42 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/shell.d.ts +7 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +32 -0
- package/package.json +7 -1
|
@@ -65,6 +65,7 @@ const crypto_1 = require("crypto");
|
|
|
65
65
|
const atomicWrite_1 = require("../utils/atomicWrite");
|
|
66
66
|
const logger_1 = require("../utils/logger");
|
|
67
67
|
const ReportGenerator_1 = require("./ReportGenerator");
|
|
68
|
+
const RegressionReclassifier_1 = require("./RegressionReclassifier");
|
|
68
69
|
/**
|
|
69
70
|
* Compute a stable, run-independent key for an event.
|
|
70
71
|
* Based solely on immutable facts: normalized file path, line number, and original (broken) selector.
|
|
@@ -314,6 +315,13 @@ exports.REPORT_JSON_FILE = "sela-report.json";
|
|
|
314
315
|
* Merge a regression verdict into the report at `targetDir` and rewrite
|
|
315
316
|
* `sela-report.html` + `sela-report.json`. Returns the HTML path.
|
|
316
317
|
* Throws when no base report exists (Job 2 must run after Job 1).
|
|
318
|
+
*
|
|
319
|
+
* When `regression.records` (the manifest) is supplied, each BYPASSED event is
|
|
320
|
+
* reclassified against its OWN blast-radius verdict (see
|
|
321
|
+
* {@link reclassifyBypassEvents}): a fix whose downstream tests all passed is
|
|
322
|
+
* promoted to HEALED, one whose tests broke is demoted to FAILED. Stats + ROI
|
|
323
|
+
* are recomputed from the reclassified stream so the donut, KPIs and filters
|
|
324
|
+
* reflect the post-regression truth — history is left untouched (Job 1 owns it).
|
|
317
325
|
*/
|
|
318
326
|
function mergeRegressionIntoReport(targetDir, regression, opts = {}) {
|
|
319
327
|
const jsonPath = opts.reportJsonPath ?? path.join(targetDir, exports.REPORT_JSON_FILE);
|
|
@@ -322,8 +330,20 @@ function mergeRegressionIntoReport(targetDir, regression, opts = {}) {
|
|
|
322
330
|
`Run the primary suite (Job 1) before shadow-regression (Job 2).`);
|
|
323
331
|
}
|
|
324
332
|
const base = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
|
333
|
+
let events = base.events;
|
|
334
|
+
let stats = base.stats;
|
|
335
|
+
let roi = base.roi;
|
|
336
|
+
if (regression.records && regression.records.length > 0) {
|
|
337
|
+
const { events: reclassified } = (0, RegressionReclassifier_1.reclassifyBypassEvents)(base.events, regression.records, regression.regressionTests);
|
|
338
|
+
events = reclassified;
|
|
339
|
+
stats = ReportGenerator_1.ReportGenerator.computeStats(events);
|
|
340
|
+
roi = ReportGenerator_1.ReportGenerator.computeRoiMetrics(events, base.historical_runs ?? [], stats);
|
|
341
|
+
}
|
|
325
342
|
const merged = {
|
|
326
343
|
...base,
|
|
344
|
+
events,
|
|
345
|
+
stats,
|
|
346
|
+
roi,
|
|
327
347
|
regressionState: regression.regressionState,
|
|
328
348
|
regressionTests: regression.regressionTests,
|
|
329
349
|
};
|
|
@@ -719,6 +739,12 @@ const REPORT_CSS = [
|
|
|
719
739
|
".fail-img-wrap{background:var(--bg1);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}",
|
|
720
740
|
".fail-img-wrap img{display:block;max-width:100%;height:auto}",
|
|
721
741
|
".fail-img-meta{padding:6px 11px;font-size:11px;color:var(--text3);font-family:var(--mono)}",
|
|
742
|
+
".vd-pair{display:grid;grid-template-columns:1fr 1fr;gap:12px}",
|
|
743
|
+
".vd-col{background:var(--bg1);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}",
|
|
744
|
+
".vd-col img{display:block;width:100%;height:auto}",
|
|
745
|
+
".vd-label{padding:6px 10px;font-size:11px;font-weight:600;color:var(--text2)}",
|
|
746
|
+
".vd-ts{padding:0 10px 7px;font-size:10px;color:var(--text3);font-family:var(--mono)}",
|
|
747
|
+
".vd-placeholder{padding:28px 16px;text-align:center;font-size:12px;color:var(--text3)}",
|
|
722
748
|
".prot-notice{padding:12px 14px;background:rgba(162,89,255,.07);border:1px solid rgba(162,89,255,.2);border-radius:var(--radius);font-size:12.5px;color:var(--text2);line-height:1.55}",
|
|
723
749
|
".prot-notice strong{color:var(--purple)}",
|
|
724
750
|
".bypass-banner{padding:13px 15px;background:rgba(255,184,77,.08);border:1px solid rgba(255,184,77,.32);border-radius:var(--radius);margin-bottom:14px}",
|
|
@@ -733,6 +759,13 @@ const REPORT_CSS = [
|
|
|
733
759
|
".row2{display:grid;grid-template-columns:1fr 1fr;gap:14px}",
|
|
734
760
|
"@media(max-width:700px){.row2{grid-template-columns:1fr}}",
|
|
735
761
|
".muted-block{padding:12px 14px;background:var(--bg1);border:1px dashed var(--line);border-radius:var(--radius);font-size:12px;color:var(--text3)}",
|
|
762
|
+
".brain-details{margin-top:14px;border:1px solid var(--line2);border-radius:var(--radius);overflow:hidden}",
|
|
763
|
+
".brain-summary{display:flex;align-items:center;gap:8px;padding:10px 14px;cursor:pointer;font-size:13px;font-weight:600;color:var(--text2);background:var(--bg2);user-select:none;list-style:none}",
|
|
764
|
+
".brain-summary::-webkit-details-marker{display:none}",
|
|
765
|
+
".brain-summary .brain-arrow{font-size:10px;color:var(--text3);transition:transform .2s ease;display:inline-block}",
|
|
766
|
+
"details.brain-details[open] .brain-summary .brain-arrow{transform:rotate(90deg)}",
|
|
767
|
+
".brain-body{padding:14px;display:flex;flex-direction:column;gap:16px;border-top:1px solid var(--line)}",
|
|
768
|
+
".brain-section-title{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--text3);margin-bottom:6px}",
|
|
736
769
|
".hist-row{display:grid;grid-template-columns:1fr repeat(5,80px);gap:0;font-size:12px}",
|
|
737
770
|
".hist-row .hc{padding:6px 10px;border-bottom:1px solid var(--line)}",
|
|
738
771
|
".hist-row.hdr .hc{color:var(--text3);font-size:10.5px;letter-spacing:.6px;text-transform:uppercase;background:var(--bg3)}",
|
|
@@ -816,6 +849,12 @@ const REPORT_CSS = [
|
|
|
816
849
|
".reg-trace{display:none;padding:10px 18px 14px;background:var(--bg0)}",
|
|
817
850
|
".reg-entry.reg-row-open .reg-trace{display:block}",
|
|
818
851
|
".reg-trace pre{font-family:var(--mono);font-size:11px;line-height:1.5;color:var(--text2);white-space:pre-wrap;word-break:break-word;margin:0}",
|
|
852
|
+
// ── Per-fix (scoped) blast-radius regression panel inside an event card
|
|
853
|
+
".fixreg-count{margin-left:6px;font-family:var(--mono);font-size:11px;color:var(--text3);font-weight:400}",
|
|
854
|
+
".fixreg-note{font-size:12.5px;line-height:1.55;padding:10px 13px;border-radius:var(--radius);margin-bottom:10px}",
|
|
855
|
+
".fixreg-note.fixreg-ok{background:var(--green-dim);border:1px solid rgba(0,229,160,.32);color:var(--green)}",
|
|
856
|
+
".fixreg-note.fixreg-bad{background:var(--red-dim);border:1px solid rgba(255,77,106,.32);color:var(--red)}",
|
|
857
|
+
".fixreg-list{border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}",
|
|
819
858
|
].join("\n");
|
|
820
859
|
/* eslint-enable prettier/prettier */
|
|
821
860
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1007,7 +1046,8 @@ const REPORT_JS = [
|
|
|
1007
1046
|
"function confBadge(pct){",
|
|
1008
1047
|
" if(typeof pct!=='number'||!isFinite(pct))return'';",
|
|
1009
1048
|
" var cls=pct>=80?'conf-high':pct>=60?'conf-mid':'conf-low';",
|
|
1010
|
-
"
|
|
1049
|
+
" var lbl=pct>=80?'High':pct>=60?'Medium':'Low';",
|
|
1050
|
+
" return'<span class=\"conf-badge '+cls+'\">'+lbl+' \xb7 '+pct+'%</span>';",
|
|
1011
1051
|
"}",
|
|
1012
1052
|
// PROTECTED events were BLOCKED - showing the AI's high confidence would be
|
|
1013
1053
|
// a lie. Surface a strict Blocked pill instead; the honest consistency score
|
|
@@ -1025,8 +1065,10 @@ const REPORT_JS = [
|
|
|
1025
1065
|
" :'confidence too low ('+ev.aiConfidence+'/100)';",
|
|
1026
1066
|
" return'<div class=\"prot-notice\"><strong>Source code untouched.</strong> Sela blocked this auto-fix \u2014 '+blockMsg+'. This is a real regression surfacing in CI. Investigate <a href=\"'+vscodeLink(ev.sourceFile,ev.sourceLine)+'\">'+esc(ev.sourceFile)+':'+ev.sourceLine+'</a> manually.</div>';",
|
|
1027
1067
|
" }",
|
|
1028
|
-
" if(ev.kind==='FAILED')",
|
|
1068
|
+
" if(ev.kind==='FAILED'){",
|
|
1069
|
+
" if(ev.wasBypassed)return'<div class=\"muted-block\">This shared-locator heal was applied in memory for the regression run only \u2014 never written to disk. It is listed as Failed because its blast-radius regression did not pass (see the breakdown above).</div>';",
|
|
1029
1070
|
" return'<div class=\"muted-block\">No source code was modified. The AI could not propose a confident fix \u2014 all candidates resolved to 0 elements. Open the file directly to investigate.</div>';",
|
|
1071
|
+
" }",
|
|
1030
1072
|
" var isBypass=ev.kind==='BYPASSED';",
|
|
1031
1073
|
" var raw=isBypass?(ev.ghostDiff||''):(ev.gitUnifiedDiff||'');",
|
|
1032
1074
|
" var skipPfx=['diff ','index ','--- ','+++','new file','deleted','similarity','rename','Binary'];",
|
|
@@ -1179,8 +1221,52 @@ const REPORT_JS = [
|
|
|
1179
1221
|
' +\'<div class="fail-img-wrap"><img src="data:image/png;base64,\'+ev.failureScreenshotPng+\'" alt="Failure screenshot" loading="lazy"/>\'',
|
|
1180
1222
|
" +'<div class=\"fail-img-meta\">captured at test failure \xb7 base64-embedded</div></div></div>';",
|
|
1181
1223
|
"}",
|
|
1224
|
+
// ── Visual element diff (DNA baseline vs live capture)
|
|
1225
|
+
"function renderVisualDiff(ev){",
|
|
1226
|
+
" if(ev.kind!=='HEALED'||!ev.visualDiff)return'';",
|
|
1227
|
+
" var vd=ev.visualDiff;",
|
|
1228
|
+
" function vdCol(label,ts,imgPath,placeholder){",
|
|
1229
|
+
" var meta=ts?'<div class=\"vd-ts\">'+esc(ts)+'</div>':'';",
|
|
1230
|
+
" var body=imgPath",
|
|
1231
|
+
" ?'<img src=\"'+esc(imgPath)+'\" alt=\"'+esc(label)+'\" loading=\"lazy\"/>'",
|
|
1232
|
+
" :'<div class=\"vd-placeholder\">'+esc(placeholder)+'</div>';",
|
|
1233
|
+
" return'<div class=\"vd-col\"><div class=\"vd-label\">'+esc(label)+'</div>'+body+meta+'</div>';",
|
|
1234
|
+
" }",
|
|
1235
|
+
" var leftImg=vd.dnaSnapshotPath||null;",
|
|
1236
|
+
" var leftLabel='DNA Baseline';",
|
|
1237
|
+
" var leftPlaceholder=leftImg===null?'First heal — no baseline yet':'';",
|
|
1238
|
+
" var rightImg=vd.elementFound&&vd.liveSnapshotPath?vd.liveSnapshotPath:null;",
|
|
1239
|
+
" var rightPlaceholder=vd.elementFound?'':'Element not found';",
|
|
1240
|
+
" var pair='<div class=\"vd-pair\">'",
|
|
1241
|
+
" +vdCol(leftLabel,vd.dnaSnapshotCapturedAt,leftImg,leftPlaceholder)",
|
|
1242
|
+
" +vdCol('Current (this fix)',vd.liveSnapshotCapturedAt,rightImg,rightPlaceholder)",
|
|
1243
|
+
" +'</div>';",
|
|
1244
|
+
" return'<div class=\"body-section\"><div class=\"bs-title\"><span class=\"bst-dot\"></span>Visual Element Comparison</div>'+pair+'</div>';",
|
|
1245
|
+
"}",
|
|
1246
|
+
// ── Per-fix scoped blast-radius regression (shadow-regression provenance)
|
|
1247
|
+
// Rendered only on events reclassified by `sela shadow-regression`: a HEALED
|
|
1248
|
+
// event promoted from a verified bypass, or a FAILED event demoted from a
|
|
1249
|
+
// rejected one. This is a personal mini-Overview — exactly the tests in THIS
|
|
1250
|
+
// fix's blast radius, which passed and which broke.
|
|
1251
|
+
"function renderFixRegression(ev){",
|
|
1252
|
+
" if(!ev.regressionTests||!ev.regressionTests.length)return'';",
|
|
1253
|
+
" var st=ev.regressionState||'';",
|
|
1254
|
+
" var passN=ev.regressionTests.filter(function(t){return t.status==='PASSED';}).length;",
|
|
1255
|
+
" var failN=ev.regressionTests.filter(function(t){return t.status==='FAILED';}).length;",
|
|
1256
|
+
" var noteCls=st==='SUCCESS'?'fixreg-ok':'fixreg-bad';",
|
|
1257
|
+
" var note=ev.regressionNote?'<div class=\"fixreg-note '+noteCls+'\">'+esc(ev.regressionNote)+'</div>':'';",
|
|
1258
|
+
" var rows=ev.regressionTests.map(function(t){",
|
|
1259
|
+
" var file=esc(t.filePath||'');var name=esc(t.testName||'');",
|
|
1260
|
+
" if(t.status==='PASSED')return'<div class=\"reg-entry reg-pass\"><div class=\"reg-row\"><span class=\"reg-row-icon\">✓</span><span class=\"reg-row-name\">'+name+'</span><span class=\"reg-row-file\">'+file+'</span></div></div>';",
|
|
1261
|
+
" var err=t.error||{};var trace=(err.message||'')+(err.stack?'\\n\\n'+err.stack:'');",
|
|
1262
|
+
" return'<div class=\"reg-entry reg-fail\"><div class=\"reg-row\" onclick=\"this.parentElement.classList.toggle(\\'reg-row-open\\')\"><span class=\"reg-row-icon\">✕</span><span class=\"reg-row-name\">'+name+'</span><span class=\"reg-row-file\">'+file+'</span><svg class=\"reg-row-chev\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"5 3 11 8 5 13\"/></svg></div><div class=\"reg-trace\"><pre>'+esc(trace)+'</pre></div></div>';",
|
|
1263
|
+
" }).join('');",
|
|
1264
|
+
" var title=st==='SUCCESS'?'Blast-Radius Regression — Passed':'Blast-Radius Regression — Failed';",
|
|
1265
|
+
" return'<div class=\"body-section\"><div class=\"bs-title\"><span class=\"bst-dot\"></span>'+esc(title)+'<span class=\"fixreg-count\">'+passN+' passed \xb7 '+failN+' failed</span></div>'+note+'<div class=\"fixreg-list\">'+rows+'</div></div>';",
|
|
1266
|
+
"}",
|
|
1182
1267
|
// ── Shared-locator runtime bypass (blast-radius protection)
|
|
1183
1268
|
"var WARN_TRIANGLE='<svg class=\"warn-tri\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 1.5L15 14H1L8 1.5z\"/><line x1=\"8\" y1=\"6\" x2=\"8\" y2=\"9.5\"/><circle cx=\"8\" cy=\"11.6\" r=\"0.7\" fill=\"currentColor\" stroke=\"none\"/></svg>';",
|
|
1269
|
+
"var LIGHTBULB='<svg class=\"lightbulb-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 1a3.5 3.5 0 0 0-3.5 3.5c0 1.5.5 2.5 1 3.5v1.5h5v-1.5c.5-1 1-2 1-3.5A3.5 3.5 0 0 0 8 1z\"/><line x1=\"5\" y1=\"10.5\" x2=\"11\" y2=\"10.5\"/><path d=\"M6 12.5h4\"/><path d=\"M6.5 14h3\"/></svg>';",
|
|
1184
1270
|
"function renderBypassBody(ev){",
|
|
1185
1271
|
" var link=vscodeLink(ev.sourceFile,ev.sourceLine);",
|
|
1186
1272
|
" var files=(ev.affectedFiles&&ev.affectedFiles.length)?'<div class=\"bb-files\">Affected: '+ev.affectedFiles.map(esc).join(', ')+'</div>':'';",
|
|
@@ -1197,15 +1283,15 @@ const REPORT_JS = [
|
|
|
1197
1283
|
" steps.push('Sela healed this run in memory but did NOT write to disk — the locator is shared by '+((ev.testFunctionCount||0)+' test(s).')+'');",
|
|
1198
1284
|
" steps.push('Review whether the shared declaration at '+esc(ev.variablePath||'')+' needs a structural refactor.');",
|
|
1199
1285
|
" }",
|
|
1200
|
-
" var
|
|
1201
|
-
" +'<div><div class=\"
|
|
1202
|
-
" +'<div><div class=\"
|
|
1203
|
-
"
|
|
1286
|
+
" var bypassBrainInner=''",
|
|
1287
|
+
" +'<div><div class=\"brain-section-title\">Chain of Thought</div>'+renderCoT(steps)+'</div>'",
|
|
1288
|
+
" +'<div><div class=\"brain-section-title\">Intent Audit</div>'+renderAuditor(ev.auditor||null)+'</div>';",
|
|
1289
|
+
" var bypassBrain='<details class=\"brain-details\"><summary class=\"brain-summary\"><span class=\"brain-arrow\">▶</span>'+LIGHTBULB+' AI Reasoning \xb7 Audit</summary><div class=\"brain-body\">'+bypassBrainInner+'</div></details>';",
|
|
1204
1290
|
" var actions='<div class=\"actions\">'",
|
|
1205
1291
|
" +'<a class=\"btn btn-primary\" href=\"'+link+'\">→ Open shared declaration in VS Code</a>'",
|
|
1206
1292
|
" +'</div>';",
|
|
1207
1293
|
" return banner",
|
|
1208
|
-
" +
|
|
1294
|
+
" +bypassBrain",
|
|
1209
1295
|
" +renderVectors(ev)",
|
|
1210
1296
|
" +'<div class=\"body-section\"><div class=\"bs-title\"><span class=\"bst-dot\"></span>Speculative Heal (not written to disk)</div>'+renderDiff(ev)+'</div>'",
|
|
1211
1297
|
" +'<div class=\"body-section\"><div class=\"bs-title\"><span class=\"bst-dot\"></span>Visual DNA Evidence</div>'+renderDna(ev.dnaBefore||null,ev.dnaAfter||null)+'</div>'",
|
|
@@ -1230,7 +1316,7 @@ const REPORT_JS = [
|
|
|
1230
1316
|
" }",
|
|
1231
1317
|
" var link=vscodeLink(ev.sourceFile,ev.sourceLine);",
|
|
1232
1318
|
" var actions='';",
|
|
1233
|
-
" if(ev.kind==='HEALED'){",
|
|
1319
|
+
" if(ev.kind==='HEALED'&&!ev.wasBypassed){",
|
|
1234
1320
|
" actions='<div class=\"actions\">'",
|
|
1235
1321
|
" +'<button class=\"btn btn-primary\" data-action=\"keep\" data-id=\"'+ev.stableId+'\" onclick=\"handleAction(this,\\'keep\\',\\''+ev.stableId+'\\')\">\u2713 Keep changes</button>'",
|
|
1236
1322
|
" +'<button class=\"btn btn-danger\" onclick=\"handleAction(this,\\'rollback\\',\\''+ev.stableId+'\\')\">↶ Rollback</button>'",
|
|
@@ -1246,20 +1332,27 @@ const REPORT_JS = [
|
|
|
1246
1332
|
" +'<a class=\"btn btn-primary\" href=\"'+link+'\">\u2192 Open in VS Code</a>'",
|
|
1247
1333
|
" +'</div>';",
|
|
1248
1334
|
" }",
|
|
1249
|
-
" var
|
|
1250
|
-
|
|
1335
|
+
" var brainInner=''",
|
|
1336
|
+
" +'<div><div class=\"brain-section-title\">Chain of Thought</div>'+renderCoT(steps)+'</div>'",
|
|
1337
|
+
" +'<div><div class=\"brain-section-title\">Intent Audit</div>'+renderAuditor(ev.auditor||null)+'</div>';",
|
|
1338
|
+
" if(ev.kind==='HEALED'&&ev.aiAlternatives&&ev.aiAlternatives.length){",
|
|
1339
|
+
" brainInner+='<div><div class=\"brain-section-title\">Alternatives Considered</div><div class=\"alts\">'",
|
|
1251
1340
|
" +ev.aiAlternatives.map(function(a){return'<div class=\"alt-row\"><span>'+esc(a)+'</span><button class=\"alt-copy\" onclick=\"copyText(\\''+esc(a)+'\\',this)\">Copy</button></div>';}).join('')",
|
|
1252
|
-
" +'</div></div>'",
|
|
1253
|
-
"
|
|
1254
|
-
|
|
1255
|
-
"
|
|
1256
|
-
" +'
|
|
1257
|
-
" +'</div>'",
|
|
1341
|
+
" +'</div></div>';",
|
|
1342
|
+
" }",
|
|
1343
|
+
" if(ev.dnaBefore){",
|
|
1344
|
+
" var _d=ev.dnaBefore;",
|
|
1345
|
+
" var _snip=(_d.tagName||'element')+(_d.role?' role=\"'+_d.role+'\"':'')+(_d.text?' \\''+String(_d.text).slice(0,60)+'\\'':'');",
|
|
1346
|
+
" brainInner+='<div><div class=\"brain-section-title\">Target DOM Snapshot</div><div class=\"muted-block\" style=\"font-family:var(--mono);font-size:12px\">'+esc(_snip)+'</div></div>';",
|
|
1347
|
+
" }",
|
|
1348
|
+
" var brain='<details class=\"brain-details\"><summary class=\"brain-summary\"><span class=\"brain-arrow\">▶</span>'+LIGHTBULB+' AI Reasoning & Audit</summary><div class=\"brain-body\">'+brainInner+'</div></details>';",
|
|
1349
|
+
' return\'<div class="body-section"><div class="bs-title"><span class="bst-dot"></span>Source Code Diff</div>\'+renderDiff(ev)+\'</div>\'',
|
|
1350
|
+
" +brain",
|
|
1351
|
+
" +renderFixRegression(ev)",
|
|
1258
1352
|
" +renderVectors(ev)",
|
|
1259
|
-
|
|
1353
|
+
" +renderVisualDiff(ev)",
|
|
1260
1354
|
' +\'<div class="body-section"><div class="bs-title"><span class="bst-dot"></span>Visual DNA Evidence</div>\'+renderDna(ev.dnaBefore||null,ev.dnaAfter||null)+\'</div>\'',
|
|
1261
1355
|
" +renderScreenshot(ev)",
|
|
1262
|
-
" +alts",
|
|
1263
1356
|
" +actions;",
|
|
1264
1357
|
"}",
|
|
1265
1358
|
// ── Event card
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
2
|
import { AncestorNode, AnchorInfo } from "../types";
|
|
3
|
+
import type { LLMProvider } from "../providers/LLMProvider";
|
|
3
4
|
export interface AuditRequest {
|
|
4
5
|
dnaText: string;
|
|
5
6
|
dnaRole?: string;
|
|
@@ -31,20 +32,21 @@ export interface AuditVerdict {
|
|
|
31
32
|
/**
|
|
32
33
|
* Constructor options for IntentAuditor.
|
|
33
34
|
*
|
|
34
|
-
* - `
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* Pass `
|
|
38
|
-
* - `apiKey`: override the
|
|
39
|
-
* - `model`: override the Haiku model id
|
|
35
|
+
* - `provider`: pre-built LLMProvider (new path, used by SelaEngine via ProviderFactory).
|
|
36
|
+
* Pass `null` explicitly to force disabled mode.
|
|
37
|
+
* - `anthropic`: pre-built Anthropic client (legacy path for existing tests).
|
|
38
|
+
* Pass `null` explicitly to force disabled mode.
|
|
39
|
+
* - `apiKey`: override ANTHROPIC_API_KEY for the default env-based client.
|
|
40
|
+
* - `model`: override the Haiku model id.
|
|
40
41
|
*/
|
|
41
42
|
export interface IntentAuditorOptions {
|
|
43
|
+
provider?: LLMProvider | null;
|
|
42
44
|
anthropic?: Anthropic | null;
|
|
43
45
|
apiKey?: string;
|
|
44
46
|
model?: string;
|
|
45
47
|
}
|
|
46
48
|
export declare class IntentAuditor {
|
|
47
|
-
private
|
|
49
|
+
private readonly provider;
|
|
48
50
|
private readonly disabled;
|
|
49
51
|
private readonly model;
|
|
50
52
|
constructor(opts?: IntentAuditorOptions);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IntentAuditor.d.ts","sourceRoot":"","sources":["../../src/services/IntentAuditor.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"IntentAuditor.d.ts","sourceRoot":"","sources":["../../src/services/IntentAuditor.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAGpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAY5D,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,WAAW,GAAG,QAAQ,CAAC;IAEjC,8EAA8E;IAC9E,WAAW,CAAC,EAAE,YAAY,EAAE,CAAC;IAC7B,+EAA+E;IAC/E,YAAY,CAAC,EAAE,YAAY,EAAE,CAAC;IAC9B,+CAA+C;IAC/C,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,qEAAqE;IACrE,WAAW,CAAC,EAAE,UAAU,CAAC;CAC1B;AAED,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,cAAc,GAAG,YAAY,CAAC;AAE7E,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,UAAU,GAAG,cAAc,GAAG,OAAO,GAAG,UAAU,GAAG,IAAI,CAAC;IACzE,yEAAyE;IACzE,cAAc,CAAC,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5E;AAqDD;;;;;;;;;GASG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC9B,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAID,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAEnB,IAAI,GAAE,oBAAyB;IAwCrC,WAAW,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IAqH3D,sBAAsB,CACpB,OAAO,EAAE,YAAY,EAAE,EACvB,QAAQ,EAAE,YAAY,EAAE,GACvB,MAAM;IAkBT,kBAAkB,CAChB,UAAU,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,EACzC,WAAW,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACzC,MAAM;IAiBT,OAAO,CAAC,YAAY;CA8CrB"}
|
|
@@ -37,6 +37,7 @@ exports.IntentAuditor = void 0;
|
|
|
37
37
|
const dotenv = __importStar(require("dotenv"));
|
|
38
38
|
const SanitizingAnthropic_1 = require("./SanitizingAnthropic");
|
|
39
39
|
const logger_1 = require("../utils/logger");
|
|
40
|
+
const AnthropicProvider_1 = require("../providers/AnthropicProvider");
|
|
40
41
|
const StructuralScoringUtils_1 = require("./confidence/StructuralScoringUtils");
|
|
41
42
|
dotenv.config();
|
|
42
43
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -84,39 +85,45 @@ RESPONSE FORMAT (strict JSON, no other text):
|
|
|
84
85
|
}`;
|
|
85
86
|
const DEFAULT_AUDITOR_MODEL = "claude-haiku-4-5-20251001";
|
|
86
87
|
class IntentAuditor {
|
|
87
|
-
|
|
88
|
+
provider;
|
|
88
89
|
disabled;
|
|
89
90
|
model;
|
|
90
91
|
constructor(opts = {}) {
|
|
91
92
|
this.model = opts.model ?? DEFAULT_AUDITOR_MODEL;
|
|
92
|
-
//
|
|
93
|
+
// New provider injection path (opts.provider is explicitly set).
|
|
94
|
+
if (opts.provider !== undefined) {
|
|
95
|
+
this.provider = opts.provider;
|
|
96
|
+
this.disabled = opts.provider === null;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Legacy: explicit `null` forces disabled mode (used by tests).
|
|
93
100
|
if (opts.anthropic === null) {
|
|
94
|
-
this.
|
|
101
|
+
this.provider = null;
|
|
95
102
|
this.disabled = true;
|
|
96
103
|
return;
|
|
97
104
|
}
|
|
98
|
-
//
|
|
105
|
+
// Legacy: caller supplied a ready-to-use Anthropic client.
|
|
99
106
|
if (opts.anthropic !== undefined) {
|
|
100
|
-
this.
|
|
107
|
+
this.provider = new AnthropicProvider_1.AnthropicProvider(this.model, opts.anthropic);
|
|
101
108
|
this.disabled = false;
|
|
102
109
|
return;
|
|
103
110
|
}
|
|
104
|
-
//
|
|
111
|
+
// Default: env-based construction (backward compat).
|
|
105
112
|
const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
106
113
|
if (!apiKey) {
|
|
107
114
|
logger_1.logger.warn("ANTHROPIC_API_KEY not set - auditor disabled, will return SUSPICIOUS for all audited heals");
|
|
108
|
-
this.
|
|
115
|
+
this.provider = null;
|
|
109
116
|
this.disabled = true;
|
|
110
117
|
}
|
|
111
118
|
else {
|
|
112
|
-
// Route through the PII/secrets sanitizer (T3)
|
|
113
|
-
|
|
114
|
-
this.
|
|
119
|
+
// Route through the PII/secrets sanitizer (T3).
|
|
120
|
+
const sanitizedClient = (0, SanitizingAnthropic_1.createSanitizingAnthropic)({ apiKey });
|
|
121
|
+
this.provider = new AnthropicProvider_1.AnthropicProvider(this.model, sanitizedClient);
|
|
115
122
|
this.disabled = false;
|
|
116
123
|
}
|
|
117
124
|
}
|
|
118
125
|
async auditIntent(req) {
|
|
119
|
-
if (this.disabled || !this.
|
|
126
|
+
if (this.disabled || !this.provider) {
|
|
120
127
|
return { ...FALLBACK_VERDICT };
|
|
121
128
|
}
|
|
122
129
|
const userMessage = [
|
|
@@ -129,14 +136,12 @@ class IntentAuditor {
|
|
|
129
136
|
.filter(Boolean)
|
|
130
137
|
.join("\n");
|
|
131
138
|
try {
|
|
132
|
-
const
|
|
133
|
-
model: this.model,
|
|
134
|
-
max_tokens: 256,
|
|
135
|
-
temperature: 0.1,
|
|
139
|
+
const raw = await this.provider.complete({
|
|
136
140
|
system: SYSTEM_PROMPT,
|
|
137
141
|
messages: [{ role: "user", content: userMessage }],
|
|
142
|
+
maxTokens: 256,
|
|
143
|
+
temperature: 0.1,
|
|
138
144
|
});
|
|
139
|
-
const raw = response.content[0].type === "text" ? response.content[0].text : "";
|
|
140
145
|
// parseVerdict returns the raw LLM verdict without the low-confidence
|
|
141
146
|
// downgrade so that scoring adjustments below are applied first.
|
|
142
147
|
const rawVerdict = this.parseVerdict(raw);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
2
|
import { FixRequest } from "../types";
|
|
3
|
+
import type { LLMProvider } from "../providers/LLMProvider";
|
|
3
4
|
export interface SelectorSegment {
|
|
4
5
|
type: "frame" | "element";
|
|
5
6
|
selector: string;
|
|
@@ -66,7 +67,7 @@ export type SmartChainSegment = {
|
|
|
66
67
|
index: number;
|
|
67
68
|
};
|
|
68
69
|
export interface FixResponse {
|
|
69
|
-
status: "FIXED" | "NOT_FOUND";
|
|
70
|
+
status: "FIXED" | "NOT_FOUND" | "NO_HEAL";
|
|
70
71
|
/**
|
|
71
72
|
* LEGACY/PROXY: This selector is used for IMMEDIATE RETRY in the proxy.
|
|
72
73
|
* CRITICAL: Must be as specific as the chainSegments to avoid Strict Mode errors.
|
|
@@ -86,23 +87,25 @@ export interface FixResponse {
|
|
|
86
87
|
explanation: string;
|
|
87
88
|
new_selector?: string | null;
|
|
88
89
|
contentChange?: ContentChange;
|
|
90
|
+
/** Populated when status === "NO_HEAL": the LLM's reason for refusing to heal. */
|
|
91
|
+
reason?: string;
|
|
89
92
|
}
|
|
90
93
|
/**
|
|
91
94
|
* Constructor options for LLMService.
|
|
92
95
|
*
|
|
93
|
-
* - `
|
|
94
|
-
*
|
|
95
|
-
* - `apiKey`: override the env-based
|
|
96
|
-
*
|
|
97
|
-
* - `model`: override the Sonnet model id (lets tests pin the SDK call).
|
|
96
|
+
* - `provider`: pre-built LLMProvider (new path, used by SelaEngine via ProviderFactory).
|
|
97
|
+
* - `anthropic`: pre-built Anthropic client (legacy path for existing tests).
|
|
98
|
+
* - `apiKey`: override ANTHROPIC_API_KEY for the default env-based client.
|
|
99
|
+
* - `model`: override the model id.
|
|
98
100
|
*/
|
|
99
101
|
export interface LLMServiceOptions {
|
|
102
|
+
provider?: LLMProvider;
|
|
100
103
|
anthropic?: Anthropic;
|
|
101
104
|
apiKey?: string;
|
|
102
105
|
model?: string;
|
|
103
106
|
}
|
|
104
107
|
export declare class LLMService {
|
|
105
|
-
private
|
|
108
|
+
private readonly provider;
|
|
106
109
|
private readonly model;
|
|
107
110
|
constructor(opts?: LLMServiceOptions);
|
|
108
111
|
private cleanJson;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LLMService.d.ts","sourceRoot":"","sources":["../../src/services/LLMService.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"LLMService.d.ts","sourceRoot":"","sources":["../../src/services/LLMService.ts"],"names":[],"mappings":"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"}
|
|
@@ -39,6 +39,7 @@ const SelaError_1 = require("../errors/SelaError");
|
|
|
39
39
|
const SanitizingAnthropic_1 = require("./SanitizingAnthropic");
|
|
40
40
|
const logger_1 = require("../utils/logger");
|
|
41
41
|
const ConfigLoader_1 = require("../config/ConfigLoader");
|
|
42
|
+
const AnthropicProvider_1 = require("../providers/AnthropicProvider");
|
|
42
43
|
const Conventions_1 = require("../config/Conventions");
|
|
43
44
|
dotenv.config();
|
|
44
45
|
/**
|
|
@@ -178,6 +179,32 @@ NEVER omit this field. NEVER return it as a string, boolean, or null.
|
|
|
178
179
|
Reports rendered downstream will show "n/a" when this field is missing - which
|
|
179
180
|
is a clear failure signal to the developer reviewing the heal.
|
|
180
181
|
|
|
182
|
+
### SCOPE BOUNDARY CHECK (evaluate BEFORE proposing any selector)
|
|
183
|
+
Before returning a FIXED response, determine whether the failure is:
|
|
184
|
+
(A) SELECTOR DRIFT — the element exists in the DOM but its selector changed structurally
|
|
185
|
+
(class renamed, id updated, attribute value changed). → Proceed with FIXED.
|
|
186
|
+
(B) APP LOGIC / STATE MISMATCH — the element is ENTIRELY ABSENT because of a
|
|
187
|
+
functional bug, wrong routing, empty state, error page, or dynamic runtime variable.
|
|
188
|
+
→ Return NO_HEAL immediately.
|
|
189
|
+
|
|
190
|
+
Patterns that MUST trigger NO_HEAL:
|
|
191
|
+
1. The failing selector ends with a numeric suffix (e.g., "status-201", "item-3", "hi-200")
|
|
192
|
+
AND the DOM contains a similar selector with a different numeric suffix. The number
|
|
193
|
+
difference is likely a runtime state value (HTTP status code, counter, index), NOT a
|
|
194
|
+
CSS rename. Healing this would mask a backend regression.
|
|
195
|
+
2. The target element is completely absent AND the nearest DOM candidate has a fundamentally
|
|
196
|
+
different semantic role/action (e.g., original was a "Delete" action, candidate is "Save").
|
|
197
|
+
3. An error message, empty state placeholder, or loading spinner is the only visible content
|
|
198
|
+
— the application is in an unexpected state, not a selector-drift state.
|
|
199
|
+
4. The failing selector contains programmatic patterns (UUIDs, timestamps, sequential IDs)
|
|
200
|
+
that suggest the value is generated at runtime.
|
|
201
|
+
|
|
202
|
+
NO_HEAL response format:
|
|
203
|
+
{
|
|
204
|
+
"status": "NO_HEAL",
|
|
205
|
+
"reason": "<one sentence: why this is app logic, not selector drift>"
|
|
206
|
+
}
|
|
207
|
+
|
|
181
208
|
### MANDATORY OUTPUT FORMAT
|
|
182
209
|
Return ONLY valid JSON.
|
|
183
210
|
|
|
@@ -238,6 +265,12 @@ Example output D - Multiple typo corrections, each unambiguous (confidence 85-92
|
|
|
238
265
|
{ "type": "element", "selector": ".user-table >> .active-row >> internal:role=button[name=\"Submit\"i]" }
|
|
239
266
|
],
|
|
240
267
|
"explanation": "Two typos corrected: '.user-tble' → '.user-table' (confirmed by DNA ancestry class) and button name 'Sumbit' → 'Submit' (confirmed by DNA text). Each fix is an unambiguous spelling correction. DNA text 'Submit' matches live DOM exactly. Semantic identity is certain."
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
Example output E - Application state mismatch, not selector drift (NO_HEAL):
|
|
271
|
+
{
|
|
272
|
+
"status": "NO_HEAL",
|
|
273
|
+
"reason": "Selector 'hi-201' uses a numeric HTTP status suffix. DOM contains 'hi-200' with a different status number — this is a backend state bug, not selector drift."
|
|
241
274
|
}`;
|
|
242
275
|
// ─────────────────────────────────────────────────────────────────
|
|
243
276
|
// HELPERS
|
|
@@ -324,12 +357,17 @@ function inferOriginalChainHint(selector) {
|
|
|
324
357
|
}
|
|
325
358
|
const DEFAULT_LLM_MODEL = "claude-sonnet-4-6";
|
|
326
359
|
class LLMService {
|
|
327
|
-
|
|
360
|
+
provider;
|
|
328
361
|
model;
|
|
329
362
|
constructor(opts = {}) {
|
|
330
363
|
this.model = opts.model ?? process.env.SELA_LLM_MODEL ?? DEFAULT_LLM_MODEL;
|
|
364
|
+
if (opts.provider) {
|
|
365
|
+
this.provider = opts.provider;
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
331
368
|
if (opts.anthropic) {
|
|
332
|
-
|
|
369
|
+
// Legacy path: wrap injected Anthropic client (used by tests).
|
|
370
|
+
this.provider = new AnthropicProvider_1.AnthropicProvider(this.model, opts.anthropic);
|
|
333
371
|
return;
|
|
334
372
|
}
|
|
335
373
|
const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY ?? "";
|
|
@@ -337,12 +375,12 @@ class LLMService {
|
|
|
337
375
|
throw new SelaError_1.SelaError({
|
|
338
376
|
subsystem: "LLMService",
|
|
339
377
|
code: SelaError_1.LLM_SERVICE_CODES.NO_API_KEY,
|
|
340
|
-
reason: "ANTHROPIC_API_KEY not found - pass
|
|
378
|
+
reason: "ANTHROPIC_API_KEY not found - pass a `provider` or `anthropic` client to LLMService(), or set the env var",
|
|
341
379
|
});
|
|
342
380
|
}
|
|
343
|
-
// Route the default client through the PII/secrets sanitizer (T3)
|
|
344
|
-
|
|
345
|
-
this.
|
|
381
|
+
// Route the default client through the PII/secrets sanitizer (T3).
|
|
382
|
+
const sanitizedClient = (0, SanitizingAnthropic_1.createSanitizingAnthropic)({ apiKey });
|
|
383
|
+
this.provider = new AnthropicProvider_1.AnthropicProvider(this.model, sanitizedClient);
|
|
346
384
|
}
|
|
347
385
|
cleanJson(text) {
|
|
348
386
|
const cleaned = text.replace(/```json/g, "").replace(/```/g, "").trim();
|
|
@@ -520,20 +558,30 @@ ${task2}
|
|
|
520
558
|
|
|
521
559
|
Return ONLY a raw JSON object - no markdown, no preamble.`;
|
|
522
560
|
try {
|
|
523
|
-
const
|
|
524
|
-
model: this.model,
|
|
525
|
-
max_tokens: 1024,
|
|
561
|
+
const rawText = await this.provider.complete({
|
|
526
562
|
system: SYSTEM_PROMPT_TEXT +
|
|
527
563
|
buildConventionsBlock() +
|
|
528
564
|
"\nIMPORTANT: Respond ONLY with a raw JSON object.",
|
|
529
565
|
messages: [{ role: "user", content: userMessage }],
|
|
566
|
+
maxTokens: 1024,
|
|
530
567
|
});
|
|
531
|
-
const rawText = msg.content[0].type === "text" ? msg.content[0].text : "";
|
|
532
568
|
logger_1.logger.debug(`AI Raw Response: ${rawText}`);
|
|
533
569
|
const jsonString = this.cleanJson(rawText);
|
|
534
570
|
if (!jsonString)
|
|
535
571
|
throw new Error("AI did not return a valid JSON object");
|
|
536
572
|
const parsed = JSON.parse(jsonString);
|
|
573
|
+
// ── NO_HEAL: LLM detected app logic failure — skip all processing ──
|
|
574
|
+
// Return immediately so SelaEngine can record a FailedEvent and surface
|
|
575
|
+
// the native Playwright error without any AST mutation attempt.
|
|
576
|
+
if (parsed.status === "NO_HEAL") {
|
|
577
|
+
logger_1.logger.warn(`[Sela] LLM NO_HEAL: ${parsed.reason ?? "application state mismatch"}`);
|
|
578
|
+
return {
|
|
579
|
+
status: "NO_HEAL",
|
|
580
|
+
reason: parsed.reason ?? "Application logic/state mismatch detected by LLM",
|
|
581
|
+
segments: [],
|
|
582
|
+
explanation: parsed.reason ?? "Application logic/state mismatch detected by LLM",
|
|
583
|
+
};
|
|
584
|
+
}
|
|
537
585
|
// ── Normalise confidence: keep only finite numbers in [0,100] ─────
|
|
538
586
|
// Anything else (undefined, NaN, null, string, out-of-range) collapses
|
|
539
587
|
// to `undefined` so reports render "n/a" instead of synthesising 0.
|
|
@@ -611,6 +659,24 @@ Return ONLY a raw JSON object - no markdown, no preamble.`;
|
|
|
611
659
|
if (!parsed.segments || !Array.isArray(parsed.segments)) {
|
|
612
660
|
throw new Error("AI response missing 'segments' array");
|
|
613
661
|
}
|
|
662
|
+
// ── Static output contract ────────────────────────────────────────
|
|
663
|
+
// Sela only writes static selector strings to disk. If LLM returned
|
|
664
|
+
// a dynamic expression (template literal interpolation, assertion,
|
|
665
|
+
// arrow function, or JS string concatenation), treat as NOT_FOUND —
|
|
666
|
+
// never write executable code to test source.
|
|
667
|
+
//
|
|
668
|
+
// Patterns blocked:
|
|
669
|
+
// ${...} — template literal interpolation
|
|
670
|
+
// expect( — assertion leak
|
|
671
|
+
// => { — arrow function body
|
|
672
|
+
// "str" + ident — string concat with variable (e.g. "id-" + status)
|
|
673
|
+
// ident + "str" — string concat with variable (e.g. status + "-id")
|
|
674
|
+
const DYNAMIC_EXPR_RE = /\$\{[^}]+\}|expect\s*\(|=>\s*\{|['"`][^'"`]*['"`]\s*\+\s*[a-zA-Z_$]|\b[a-zA-Z_$]\w*\s*\+\s*['"`]/;
|
|
675
|
+
const hasDynamicOutput = parsed.segments.some((s) => typeof s.selector === "string" && DYNAMIC_EXPR_RE.test(s.selector));
|
|
676
|
+
if (hasDynamicOutput) {
|
|
677
|
+
logger_1.logger.warn("[Sela] Static output violation — LLM returned dynamic expression in selector. Blocking.");
|
|
678
|
+
throw new Error("ENG_DYNAMIC_OUTPUT_BLOCKED: selector contains dynamic expression");
|
|
679
|
+
}
|
|
614
680
|
parsed.new_selector =
|
|
615
681
|
parsed.new_selector ??
|
|
616
682
|
parsed.segments.map((s) => s.selector).join(" >> ");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ResolvedPRAutomation, PRStrategy, BugAction } from "../config/SelaConfig";
|
|
2
2
|
import type { HealedEvent, ProtectedEvent } from "./HealReportService";
|
|
3
3
|
import { type WorkspaceSnapshotService } from "./WorkspaceSnapshotService";
|
|
4
|
+
import type { IVCSProvider } from "./vcs/VCSProvider";
|
|
4
5
|
export interface BranchInfo {
|
|
5
6
|
branch: string | null;
|
|
6
7
|
isPR: boolean;
|
|
@@ -45,8 +46,12 @@ export interface ExecutionResult {
|
|
|
45
46
|
*/
|
|
46
47
|
export declare function execute(cfg: ResolvedPRAutomation, decision: StrategyDecision, heals: HealedEvent[], branchInfo: BranchInfo, ctx: ExecuteContext,
|
|
47
48
|
/** Injectable for tests - defaults to the module singleton. */
|
|
48
|
-
workspace?: WorkspaceSnapshotService
|
|
49
|
-
|
|
49
|
+
workspace?: WorkspaceSnapshotService,
|
|
50
|
+
/** Injectable for tests - defaults to auto-detected platform provider. */
|
|
51
|
+
provider?: IVCSProvider): Promise<ExecutionResult>;
|
|
52
|
+
export declare function handleBugDetected(action: BugAction, events: ProtectedEvent[], branchInfo: BranchInfo, ctx: ExecuteContext,
|
|
53
|
+
/** Injectable for tests - defaults to auto-detected platform provider. */
|
|
54
|
+
provider?: IVCSProvider): Promise<void>;
|
|
50
55
|
export interface CentralizedGitFlowOpts {
|
|
51
56
|
cwd: string;
|
|
52
57
|
healedEvents: HealedEvent[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PRAutomationService.d.ts","sourceRoot":"","sources":["../../src/services/PRAutomationService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PRAutomationService.d.ts","sourceRoot":"","sources":["../../src/services/PRAutomationService.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,SAAS,EACV,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAQvE,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,4BAA4B,CAAC;AAKpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAMtD,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;AAyDD,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,UAAU,CAyFpE;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;AA6FD,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;;;;;;;;;GASG;AACH,wBAAsB,OAAO,CAC3B,GAAG,EAAE,oBAAoB,EACzB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,WAAW,EAAE,EACpB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc;AACnB,+DAA+D;AAC/D,SAAS,GAAE,wBAAkD;AAC7D,0EAA0E;AAC1E,QAAQ,CAAC,EAAE,YAAY,GACtB,OAAO,CAAC,eAAe,CAAC,CAwG1B;AAgWD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,cAAc,EAAE,EACxB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc;AACnB,0EAA0E;AAC1E,QAAQ,CAAC,EAAE,YAAY,GACtB,OAAO,CAAC,IAAI,CAAC,CAwFf;AAMD,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,eAAe,EAAE,cAAc,EAAE,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAgDf"}
|