sela-core 1.0.6 → 1.0.8
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/README.md +1 -1
- package/dist/cli/ErrorHandler.d.ts +1 -1
- package/dist/cli/ErrorHandler.js +14 -14
- package/dist/cli/commands/bulk.d.ts +1 -1
- package/dist/cli/commands/bulk.d.ts.map +1 -1
- package/dist/cli/commands/bulk.js +51 -33
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +26 -18
- package/dist/cli/commands/showReport.d.ts +1 -1
- package/dist/cli/commands/showReport.d.ts.map +1 -1
- package/dist/cli/commands/showReport.js +12 -10
- package/dist/cli/commands/status.d.ts +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +8 -8
- package/dist/cli/commands/sync.d.ts +1 -1
- package/dist/cli/commands/sync.d.ts.map +1 -1
- package/dist/cli/commands/sync.js +38 -30
- package/dist/cli/index.js +9 -9
- package/dist/cli/ui/DnaTable.d.ts +1 -1
- package/dist/cli/ui/DnaTable.d.ts.map +1 -1
- package/dist/cli/ui/DnaTable.js +23 -16
- package/dist/cli/ui/RefactorWizard.d.ts +3 -3
- package/dist/cli/ui/RefactorWizard.d.ts.map +1 -1
- package/dist/cli/ui/RefactorWizard.js +108 -82
- package/dist/config/ConfigLoader.d.ts +3 -2
- package/dist/config/ConfigLoader.d.ts.map +1 -1
- package/dist/config/ConfigLoader.js +23 -13
- package/dist/config/DryRunGuard.d.ts +14 -0
- package/dist/config/DryRunGuard.d.ts.map +1 -0
- package/dist/config/DryRunGuard.js +79 -0
- package/dist/config/SelaConfig.d.ts +26 -16
- package/dist/config/SelaConfig.d.ts.map +1 -1
- package/dist/config/SelaConfig.js +54 -22
- package/dist/engine/HealingRegistry.d.ts +1 -1
- package/dist/engine/HealingRegistry.js +3 -3
- package/dist/engine/SelaEngine.d.ts +13 -10
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +292 -101
- package/dist/errors/SelaError.d.ts +132 -0
- package/dist/errors/SelaError.d.ts.map +1 -0
- package/dist/errors/SelaError.js +154 -0
- package/dist/fixtures/expectProxy.d.ts +1 -1
- package/dist/fixtures/expectProxy.d.ts.map +1 -1
- package/dist/fixtures/expectProxy.js +17 -10
- package/dist/fixtures/index.d.ts +7 -1
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +26 -11
- package/dist/fixtures/proxyTag.d.ts +12 -0
- package/dist/fixtures/proxyTag.d.ts.map +1 -0
- package/dist/fixtures/proxyTag.js +45 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/reporter/SelaReporter.d.ts +143 -0
- package/dist/reporter/SelaReporter.d.ts.map +1 -0
- package/dist/reporter/SelaReporter.js +428 -0
- package/dist/services/ASTSourceUpdater.d.ts +20 -1
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
- package/dist/services/ASTSourceUpdater.js +257 -89
- package/dist/services/ArgumentTypeAnalyzer.js +7 -7
- package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -1
- package/dist/services/BlastRadiusAnalyzer.js +5 -2
- package/dist/services/ChainValidator.js +22 -22
- package/dist/services/CrossFileHealer.js +7 -7
- package/dist/services/DefinitionTracer.d.ts +1 -1
- package/dist/services/DefinitionTracer.d.ts.map +1 -1
- package/dist/services/DefinitionTracer.js +28 -13
- package/dist/services/DnaEditorService.d.ts +2 -2
- package/dist/services/DnaEditorService.d.ts.map +1 -1
- package/dist/services/DnaEditorService.js +81 -62
- package/dist/services/DnaIndexService.d.ts +1 -1
- package/dist/services/DnaIndexService.d.ts.map +1 -1
- package/dist/services/DnaIndexService.js +14 -14
- package/dist/services/HealReportService.d.ts +43 -1
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +108 -25
- package/dist/services/HealingAdvisory.js +1 -1
- package/dist/services/HealingCacheService.d.ts +135 -0
- package/dist/services/HealingCacheService.d.ts.map +1 -0
- package/dist/services/HealingCacheService.js +460 -0
- package/dist/services/HealthReportService.d.ts +1 -1
- package/dist/services/HealthReportService.d.ts.map +1 -1
- package/dist/services/HealthReportService.js +25 -25
- package/dist/services/InitializerUpdater.d.ts.map +1 -1
- package/dist/services/InitializerUpdater.js +20 -1
- package/dist/services/IntentAuditor.d.ts +18 -1
- package/dist/services/IntentAuditor.d.ts.map +1 -1
- package/dist/services/IntentAuditor.js +50 -26
- package/dist/services/InteractiveReview.d.ts +15 -0
- package/dist/services/InteractiveReview.d.ts.map +1 -0
- package/dist/services/InteractiveReview.js +125 -0
- package/dist/services/LLMService.d.ts +18 -2
- package/dist/services/LLMService.d.ts.map +1 -1
- package/dist/services/LLMService.js +33 -23
- package/dist/services/PRAutomationService.d.ts +14 -1
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +287 -92
- package/dist/services/PendingPromptLedger.d.ts +44 -0
- package/dist/services/PendingPromptLedger.d.ts.map +1 -0
- package/dist/services/PendingPromptLedger.js +180 -0
- package/dist/services/ReportGenerator.d.ts +70 -0
- package/dist/services/ReportGenerator.d.ts.map +1 -0
- package/dist/services/ReportGenerator.js +191 -0
- package/dist/services/SafetyGuard.d.ts +30 -7
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SafetyGuard.js +85 -116
- package/dist/services/SnapshotService.js +5 -5
- package/dist/services/SourceLinkService.d.ts +2 -2
- package/dist/services/SourceLinkService.d.ts.map +1 -1
- package/dist/services/SourceLinkService.js +31 -22
- package/dist/services/SourceUpdater.d.ts +1 -1
- package/dist/services/SourceUpdater.d.ts.map +1 -1
- package/dist/services/SourceUpdater.js +66 -34
- package/dist/services/TemplateDiffService.d.ts.map +1 -1
- package/dist/services/TemplateDiffService.js +18 -15
- package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
- package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
- package/dist/services/WorkspaceSnapshotService.js +200 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/utils/IsolatedDiff.d.ts +45 -0
- package/dist/utils/IsolatedDiff.d.ts.map +1 -0
- package/dist/utils/IsolatedDiff.js +382 -0
- package/package.json +71 -67
|
@@ -36,15 +36,21 @@ 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");
|
|
40
39
|
const LLMService_1 = require("../services/LLMService");
|
|
40
|
+
const WorkspaceSnapshotService_1 = require("../services/WorkspaceSnapshotService");
|
|
41
|
+
const IsolatedDiff_1 = require("../utils/IsolatedDiff");
|
|
41
42
|
const SnapshotService_1 = require("../services/SnapshotService");
|
|
42
43
|
const DOMUtils_1 = require("../utils/DOMUtils");
|
|
43
44
|
const SourceUpdater_1 = require("../services/SourceUpdater");
|
|
45
|
+
const ASTSourceUpdater_1 = require("../services/ASTSourceUpdater");
|
|
44
46
|
const SafetyGuard_1 = require("../services/SafetyGuard");
|
|
47
|
+
const HealingCacheService_1 = require("../services/HealingCacheService");
|
|
45
48
|
const ConfigLoader_1 = require("../config/ConfigLoader");
|
|
49
|
+
const DryRunGuard_1 = require("../config/DryRunGuard");
|
|
46
50
|
const HealReportService_1 = require("../services/HealReportService");
|
|
47
51
|
const PRAutomationService_1 = require("../services/PRAutomationService");
|
|
52
|
+
const PendingPromptLedger_1 = require("../services/PendingPromptLedger");
|
|
53
|
+
const SelaError_1 = require("../errors/SelaError");
|
|
48
54
|
/**
|
|
49
55
|
* Graceful percentage formatter for engine-side log lines. Mirrors the helper
|
|
50
56
|
* in PRAutomationService / HealReportService so a missing AI/auditor
|
|
@@ -53,10 +59,25 @@ const PRAutomationService_1 = require("../services/PRAutomationService");
|
|
|
53
59
|
function fmtPctEngine(v) {
|
|
54
60
|
return typeof v === "number" && Number.isFinite(v) ? `${v}%` : "n/a";
|
|
55
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Truthy when the current process is a CI runner. We accept the standard
|
|
64
|
+
* `CI=1`/`CI=true` convention used by GitHub Actions, GitLab, CircleCI,
|
|
65
|
+
* Buildkite, etc. Anything else (empty string, undefined, `0`, `false`)
|
|
66
|
+
* is treated as Local DX mode.
|
|
67
|
+
*/
|
|
68
|
+
function isCiEnvironment() {
|
|
69
|
+
const raw = process.env.CI;
|
|
70
|
+
if (raw === undefined || raw === null)
|
|
71
|
+
return false;
|
|
72
|
+
const v = String(raw).trim().toLowerCase();
|
|
73
|
+
return v !== "" && v !== "0" && v !== "false";
|
|
74
|
+
}
|
|
56
75
|
class SelaEngine {
|
|
57
76
|
llmService;
|
|
58
77
|
snapshotService;
|
|
59
78
|
safetyGuard;
|
|
79
|
+
astDirectiveScanner;
|
|
80
|
+
healingCache;
|
|
60
81
|
config;
|
|
61
82
|
dnaBuffer = new Map();
|
|
62
83
|
healMetaBuffer = new Map();
|
|
@@ -66,9 +87,17 @@ class SelaEngine {
|
|
|
66
87
|
this.llmService = new LLMService_1.LLMService();
|
|
67
88
|
this.snapshotService = new SnapshotService_1.SnapshotService();
|
|
68
89
|
this.safetyGuard = new SafetyGuard_1.SafetyGuard(this.config.thresholds);
|
|
90
|
+
// Dedicated lightweight ts-morph project used only for the leading-comment
|
|
91
|
+
// pre-scan. Keeping it separate from the SourceUpdater's project avoids
|
|
92
|
+
// polluting that cache with files we may never actually mutate.
|
|
93
|
+
this.astDirectiveScanner = new ASTSourceUpdater_1.ASTSourceUpdater();
|
|
94
|
+
// Local healing cache - short-circuits the LLM call when an identical
|
|
95
|
+
// (filePath, oldSelector) pair was healed safely in a prior session.
|
|
96
|
+
// Disabled when SELA_NO_CACHE=1; reads/writes skipped during dry runs.
|
|
97
|
+
this.healingCache = HealingCacheService_1.sharedHealingCache;
|
|
69
98
|
}
|
|
70
99
|
// ─────────────────────────────────────────────────────────────
|
|
71
|
-
// healArgument
|
|
100
|
+
// healArgument - repairs a wrong action argument (e.g. selectOption)
|
|
72
101
|
// ─────────────────────────────────────────────────────────────
|
|
73
102
|
async healArgument(page, selector, action, oldArgument, stableId, filePath, line) {
|
|
74
103
|
console.log(`[Sela] 🔧 HEALING ARGUMENT for action '${action}': "${oldArgument}"`);
|
|
@@ -76,10 +105,10 @@ class SelaEngine {
|
|
|
76
105
|
const { dom } = await DOMUtils_1.DOMUtils.getNeighborhoodDom(page, selector);
|
|
77
106
|
const neighborhoodDom = dom;
|
|
78
107
|
const targetIntent = action === "selectOption"
|
|
79
|
-
? `ARGUMENT_HEALING: The selectOption argument "${oldArgument}" does not exist.
|
|
80
|
-
Look at the <select> element in the DOM and find all <option> elements inside it.
|
|
81
|
-
Return in 'new_selector' ONLY the TEXT CONTENT of the closest matching <option>.
|
|
82
|
-
For example: if the options are "Male" and "Female" and the old argument was "Man", return "Male".
|
|
108
|
+
? `ARGUMENT_HEALING: The selectOption argument "${oldArgument}" does not exist.
|
|
109
|
+
Look at the <select> element in the DOM and find all <option> elements inside it.
|
|
110
|
+
Return in 'new_selector' ONLY the TEXT CONTENT of the closest matching <option>.
|
|
111
|
+
For example: if the options are "Male" and "Female" and the old argument was "Man", return "Male".
|
|
83
112
|
CRITICAL: Do NOT return a CSS selector like "#my-select". Return plain text only, exactly as it appears in the option.`
|
|
84
113
|
: `Fix the broken argument "${oldArgument}" for action "${action}". Return the correct value.`;
|
|
85
114
|
const aiFix = await this.llmService.getFix({
|
|
@@ -101,13 +130,44 @@ CRITICAL: Do NOT return a CSS selector like "#my-select". Return plain text only
|
|
|
101
130
|
return null;
|
|
102
131
|
}
|
|
103
132
|
// ─────────────────────────────────────────────────────────────
|
|
104
|
-
// heal
|
|
133
|
+
// heal - repairs a broken selector
|
|
105
134
|
// ─────────────────────────────────────────────────────────────
|
|
106
135
|
async heal(page, fullSelector, elementSelectorOnly, stableId, filePath, line, healMode = "action") {
|
|
107
136
|
console.log(`\n[Sela] 🛠️ HEALING START for: ${stableId}`);
|
|
108
137
|
// Resolve file path for report purposes (project-relative when possible).
|
|
109
138
|
const reportFile = this._toReportFile(filePath);
|
|
110
139
|
const testTitle = this.testTitleBuffer.get(stableId);
|
|
140
|
+
// ── Developer fail-fast directive pre-gate ──────────────────────
|
|
141
|
+
// Scan the failing statement's leading comments for `// sela-fail-fast`
|
|
142
|
+
// BEFORE the expensive LLM round-trip. When present, record a
|
|
143
|
+
// "Skipped by Developer" FailedEvent and propagate the canonical
|
|
144
|
+
// SelaError(AST_HEAL_DISABLED_BY_DIRECTIVE) so retries are skipped too.
|
|
145
|
+
try {
|
|
146
|
+
this.astDirectiveScanner.enforceFailFastDirective({ filePath, line });
|
|
147
|
+
}
|
|
148
|
+
catch (directiveErr) {
|
|
149
|
+
if ((0, SelaError_1.isSelaErrorFrom)(directiveErr, "ASTUpdater") &&
|
|
150
|
+
directiveErr.code === SelaError_1.AST_UPDATER_CODES.HEAL_DISABLED_BY_DIRECTIVE) {
|
|
151
|
+
console.warn(`[Sela] 🛑 Skipped by Developer - ${directiveErr.reason}`);
|
|
152
|
+
const skipEvent = {
|
|
153
|
+
kind: "FAILED",
|
|
154
|
+
stableId,
|
|
155
|
+
sourceFile: reportFile,
|
|
156
|
+
sourceLine: line,
|
|
157
|
+
testTitle,
|
|
158
|
+
timestamp: new Date().toISOString(),
|
|
159
|
+
framePath: [],
|
|
160
|
+
inIframe: false,
|
|
161
|
+
inShadowDom: false,
|
|
162
|
+
oldSelector: elementSelectorOnly,
|
|
163
|
+
reason: "Skipped by Developer (sela-fail-fast directive)",
|
|
164
|
+
dnaBefore: null,
|
|
165
|
+
};
|
|
166
|
+
HealReportService_1.sharedHealReport.record(skipEvent);
|
|
167
|
+
throw directiveErr;
|
|
168
|
+
}
|
|
169
|
+
throw directiveErr;
|
|
170
|
+
}
|
|
111
171
|
// Snapshot for report event context (loaded once below, used at all exit paths).
|
|
112
172
|
let lastKnownStateForReport = null;
|
|
113
173
|
let aiFixForReport = null;
|
|
@@ -134,19 +194,19 @@ CRITICAL: Do NOT return a CSS selector like "#my-select". Return plain text only
|
|
|
134
194
|
const intentDescription = lastKnownState
|
|
135
195
|
? `The element that was a ${lastKnownState.tagName} with text "${lastKnownState.text}"`
|
|
136
196
|
: `Element identified by ${stableId}`;
|
|
137
|
-
const contentChangeInstructions = `
|
|
138
|
-
|
|
139
|
-
[CONTENT CHANGE DETECTION]:
|
|
140
|
-
If the element's VISIBLE TEXT has changed compared to its previous value${oldText ? ` ("${oldText}")` : ""}, you MUST include a "contentChange" field.
|
|
141
|
-
NOTE: This change will NOT be applied automatically to the source code to prevent masking potential bugs.
|
|
142
|
-
It will only be presented as a suggested fix in the terminal.
|
|
143
|
-
{
|
|
144
|
-
"contentChange": {
|
|
145
|
-
"oldText": "<the previous visible text>",
|
|
146
|
-
"newText": "<the new visible text as it appears in the current DOM>"
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
This is required so that any expect(...).toHaveText("${oldText ?? "..."}") assertions in the test file can be automatically updated to match the new text.
|
|
197
|
+
const contentChangeInstructions = `
|
|
198
|
+
|
|
199
|
+
[CONTENT CHANGE DETECTION]:
|
|
200
|
+
If the element's VISIBLE TEXT has changed compared to its previous value${oldText ? ` ("${oldText}")` : ""}, you MUST include a "contentChange" field.
|
|
201
|
+
NOTE: This change will NOT be applied automatically to the source code to prevent masking potential bugs.
|
|
202
|
+
It will only be presented as a suggested fix in the terminal.
|
|
203
|
+
{
|
|
204
|
+
"contentChange": {
|
|
205
|
+
"oldText": "<the previous visible text>",
|
|
206
|
+
"newText": "<the new visible text as it appears in the current DOM>"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
This is required so that any expect(...).toHaveText("${oldText ?? "..."}") assertions in the test file can be automatically updated to match the new text.
|
|
150
210
|
If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
151
211
|
// ── Semantic anchor context hint for the LLM ─────────────────────
|
|
152
212
|
// When DNA v2 anchor data exists, tell the AI which label and row
|
|
@@ -158,10 +218,10 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
158
218
|
return "";
|
|
159
219
|
const lines = [];
|
|
160
220
|
if (anchors.closestLabel) {
|
|
161
|
-
lines.push(`- Label: "${anchors.closestLabel}"
|
|
221
|
+
lines.push(`- Label: "${anchors.closestLabel}" - the UI label or heading associated with this element.`);
|
|
162
222
|
}
|
|
163
223
|
if (anchors.rowAnchor) {
|
|
164
|
-
lines.push(`- Row key: "${anchors.rowAnchor}"
|
|
224
|
+
lines.push(`- Row key: "${anchors.rowAnchor}" - unique cell text identifying the table row this element belonged to.`);
|
|
165
225
|
}
|
|
166
226
|
if (lines.length === 0)
|
|
167
227
|
return "";
|
|
@@ -171,16 +231,57 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
171
231
|
"\n" +
|
|
172
232
|
"Prioritize selectors that preserve these relationships (e.g. filter by row key, then target by label).");
|
|
173
233
|
})();
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
234
|
+
// ── Cache pre-check ────────────────────────────────────────────
|
|
235
|
+
// When a prior session healed this exact (filePath, oldSelector)
|
|
236
|
+
// pair AND SafetyGuard cleared it, replay the cached fix instead
|
|
237
|
+
// of paying the LLM round-trip. Disabled when SELA_NO_CACHE=1.
|
|
238
|
+
// The synthetic aiFix is fed through SafetyGuard exactly like a
|
|
239
|
+
// fresh LLM response - cache is a latency optimisation, never a
|
|
240
|
+
// safety bypass.
|
|
241
|
+
let aiFix = null;
|
|
242
|
+
let cacheHit = false;
|
|
243
|
+
// Fingerprint inputs the cache key depends on. Recomputed every
|
|
244
|
+
// heal so a DNA rewrite - which changes `dnaContentHash` -
|
|
245
|
+
// automatically invalidates every prior entry without an explicit
|
|
246
|
+
// purge.
|
|
247
|
+
const dnaContentHash = (0, HealingCacheService_1.hashDnaContent)(lastKnownState ?? null);
|
|
248
|
+
const cacheKeyInput = {
|
|
249
|
+
filePath,
|
|
250
|
+
line,
|
|
251
|
+
dnaContentHash,
|
|
252
|
+
brokenSelector: fullSelector,
|
|
253
|
+
mode: healMode,
|
|
254
|
+
};
|
|
255
|
+
if (!this.config.noCache) {
|
|
256
|
+
const cached = this.healingCache.get(cacheKeyInput);
|
|
257
|
+
if (cached) {
|
|
258
|
+
console.log(`[Sela] 🧊 Cache hit (0ms LLM) - replaying healed selector "${cached.healedSelector}"`);
|
|
259
|
+
aiFix = {
|
|
260
|
+
status: "FIXED",
|
|
261
|
+
new_selector: cached.healedSelector,
|
|
262
|
+
chainSegments: cached.chainSegments,
|
|
263
|
+
segments: cached.segments ?? [
|
|
264
|
+
{ type: "element", selector: cached.healedSelector },
|
|
265
|
+
],
|
|
266
|
+
confidence: cached.confidence,
|
|
267
|
+
contentChange: cached.contentChange,
|
|
268
|
+
explanation: cached.explanation ?? "Replayed from local healing cache",
|
|
269
|
+
};
|
|
270
|
+
cacheHit = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (!cacheHit) {
|
|
274
|
+
console.log(`[Sela] 🔍 Step 3: Requesting AI fix from LLM...`);
|
|
275
|
+
aiFix = await this.llmService.getFix({
|
|
276
|
+
targetIntent: intentDescription,
|
|
277
|
+
failedSelector: fullSelector,
|
|
278
|
+
previousState: lastKnownState || {},
|
|
279
|
+
currentDom: neighborhoodDom +
|
|
280
|
+
contextHint +
|
|
281
|
+
anchorContextHint +
|
|
282
|
+
contentChangeInstructions,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
184
285
|
aiFixForReport = aiFix;
|
|
185
286
|
if (aiFix && aiFix.status === "FIXED") {
|
|
186
287
|
console.log(`[Sela] 🔍 Step 3: AI responded ✅`);
|
|
@@ -208,7 +309,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
208
309
|
console.log(`[Sela] 🔍 Step 3.5: Live text fetch skipped (element not yet rendered or non-text)`);
|
|
209
310
|
}
|
|
210
311
|
// ── Step 3.6: Atomic v2 capture of the candidate element ──
|
|
211
|
-
// Single page.evaluate() call
|
|
312
|
+
// Single page.evaluate() call - collects visibility, ancestry,
|
|
212
313
|
// and anchors of the healed element for SafetyGuard use.
|
|
213
314
|
// Returns null on any failure; never blocks the heal.
|
|
214
315
|
const liveSnapshot = await this.snapshotService.captureElement(page, elementOnlySelector, successfulPath);
|
|
@@ -221,7 +322,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
221
322
|
: "";
|
|
222
323
|
const occFlag = liveVisibility?.isOccluded ? " 🫥 OCCLUDED" : "";
|
|
223
324
|
console.log(`[Sela] 🔍 Step 3.6: Live v2 capture complete` +
|
|
224
|
-
`${ghostFlag}${occFlag}
|
|
325
|
+
`${ghostFlag}${occFlag} - ancestry depth: ${liveAncestry?.length ?? 0}`);
|
|
225
326
|
}
|
|
226
327
|
else {
|
|
227
328
|
console.log(`[Sela] 🔍 Step 3.6: Live v2 capture skipped`);
|
|
@@ -255,7 +356,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
255
356
|
}
|
|
256
357
|
// ── Report: PROTECTED (or BLOCKED non-INCONSISTENT) ─────────
|
|
257
358
|
// The auto-fix was blocked. If the auditor verdict is INCONSISTENT
|
|
258
|
-
// this is a "Security Win"
|
|
359
|
+
// this is a "Security Win" - a masked regression was caught.
|
|
259
360
|
const protectedEvent = {
|
|
260
361
|
kind: "PROTECTED",
|
|
261
362
|
stableId,
|
|
@@ -278,7 +379,24 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
278
379
|
contentChange: aiFix.contentChange,
|
|
279
380
|
};
|
|
280
381
|
HealReportService_1.sharedHealReport.record(protectedEvent);
|
|
281
|
-
throw new
|
|
382
|
+
throw new SelaError_1.SelaError({
|
|
383
|
+
subsystem: "SafetyGuard",
|
|
384
|
+
code: safetyDecision.code ?? SelaError_1.SAFETY_GUARD_CODES.GENERIC_REJECTION,
|
|
385
|
+
reason: safetyDecision.reason,
|
|
386
|
+
context: {
|
|
387
|
+
filePath: reportFile,
|
|
388
|
+
line,
|
|
389
|
+
selector: elementSelectorOnly,
|
|
390
|
+
healedSelector: newFullSelector,
|
|
391
|
+
stableId,
|
|
392
|
+
testTitle,
|
|
393
|
+
branch,
|
|
394
|
+
safetyLevel: safetyDecision.level,
|
|
395
|
+
auditorVerdict: safetyDecision.meta?.auditorVerdict,
|
|
396
|
+
auditorConfidence: safetyDecision.meta?.auditorConfidence,
|
|
397
|
+
aiConfidence: aiFix.confidence,
|
|
398
|
+
},
|
|
399
|
+
});
|
|
282
400
|
}
|
|
283
401
|
// ── Heal-completion score summary ─────────────────────────────────
|
|
284
402
|
// Emits a single consolidated line showing AI confidence plus the
|
|
@@ -290,7 +408,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
290
408
|
parts.push(`${breakdown.penalty} ancestry drift`);
|
|
291
409
|
if (breakdown.bonus !== 0)
|
|
292
410
|
parts.push(`+${breakdown.bonus} anchor match`);
|
|
293
|
-
console.log(`[Sela] 📊 Score
|
|
411
|
+
console.log(`[Sela] 📊 Score - AI: ${fmtPctEngine(aiFix.confidence)} | ` +
|
|
294
412
|
`Auditor: ${fmtPctEngine(breakdown.adjustedConfidence)} ` +
|
|
295
413
|
`(raw ${fmtPctEngine(breakdown.rawConfidence)}, ${parts.join(", ")})`);
|
|
296
414
|
}
|
|
@@ -300,7 +418,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
300
418
|
// 1. updateResult.lineUpdated (the strategy's literal write location)
|
|
301
419
|
// 2. updateResult.definitionSite.line (semantic JumpToDef target,
|
|
302
420
|
// used when the mutation crossed into a const/variable declaration)
|
|
303
|
-
// 3. line (the original failure line
|
|
421
|
+
// 3. line (the original failure line - last-resort fallback)
|
|
304
422
|
// For cross-file definitionSite.file, we still read the new line from
|
|
305
423
|
// the SAME file that was mutated.
|
|
306
424
|
const defSite = updateResult.definitionSite;
|
|
@@ -312,23 +430,26 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
312
430
|
(sameFileDef ? defSite?.line : undefined) ??
|
|
313
431
|
line;
|
|
314
432
|
const mutatedFilePath = !sameFileDef && defSite ? defSite.file : filePath;
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
|
|
433
|
+
// Clean-Room isolated diff. We diff the in-memory snapshot recorded
|
|
434
|
+
// by WorkspaceSnapshotService (contentBefore - disk state right
|
|
435
|
+
// before Sela's first write to this file) against the post-write
|
|
436
|
+
// disk state. The result contains EXCLUSIVELY Sela's selector swap;
|
|
437
|
+
// any developer dirty edits sit identically in both buffers and
|
|
438
|
+
// therefore cancel out. Falls back to "" when the snapshot is
|
|
439
|
+
// missing (cross-file healing into an untracked path, etc.).
|
|
440
|
+
const gitUnifiedDiff = this._captureIsolatedDiff(mutatedFilePath);
|
|
322
441
|
const firstChange = this._extractFirstChangeFromDiff(gitUnifiedDiff);
|
|
323
442
|
// Backward-compat: keep oldCodeLine/newCodeLine populated from the
|
|
324
443
|
// first -/+ pair inside the diff. When git produced no diff (file
|
|
325
444
|
// untracked, git unavailable, …) fall back to a direct disk read of
|
|
326
|
-
// the mutated line
|
|
327
|
-
const oldCodeLine = firstChange.oldLine ??
|
|
328
|
-
|
|
445
|
+
// the mutated line - strictly inferior but preserves old reports.
|
|
446
|
+
const oldCodeLine = firstChange.oldLine ??
|
|
447
|
+
this._safeReadLine(mutatedFilePath, writtenLine);
|
|
448
|
+
const newCodeLine = firstChange.newLine ??
|
|
449
|
+
this._safeReadLine(mutatedFilePath, writtenLine);
|
|
329
450
|
const canonicalSelector = updateResult.healedLocatorString ?? newFullSelector;
|
|
330
451
|
console.log(`[Sela] ✅ Canonical selector (disk == runtime): "${canonicalSelector}"`);
|
|
331
|
-
// Buffer CLI-ready metadata
|
|
452
|
+
// Buffer CLI-ready metadata - merged into DNA at commitUpdates() time.
|
|
332
453
|
// Sanitize filePath: strip any residual "at " stack-trace prefix + whitespace,
|
|
333
454
|
// then resolve to an absolute path before converting to project-relative form.
|
|
334
455
|
const match = filePath.match(/at\s+(.*)/i);
|
|
@@ -368,6 +489,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
368
489
|
framePath: successfulPath,
|
|
369
490
|
inIframe: successfulPath.length > 0,
|
|
370
491
|
inShadowDom: liveSnapshot?.isInShadowDom ?? false,
|
|
492
|
+
dryRun: DryRunGuard_1.DryRunGuard.active() ? true : undefined,
|
|
371
493
|
oldSelector: elementSelectorOnly,
|
|
372
494
|
newSelector: canonicalSelector,
|
|
373
495
|
oldCodeLine,
|
|
@@ -396,9 +518,28 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
396
518
|
contentChange: aiFix.contentChange,
|
|
397
519
|
};
|
|
398
520
|
HealReportService_1.sharedHealReport.record(healedEvent);
|
|
521
|
+
// ── Cache write - persist this safe heal for future sessions ──
|
|
522
|
+
// Best-effort: failures are logged inside the service and never
|
|
523
|
+
// block the heal return. Skipped automatically during dry runs
|
|
524
|
+
// and when SELA_NO_CACHE=1.
|
|
525
|
+
if (!this.config.noCache && !cacheHit) {
|
|
526
|
+
this.healingCache
|
|
527
|
+
.set({
|
|
528
|
+
...cacheKeyInput,
|
|
529
|
+
healedSelector: canonicalSelector,
|
|
530
|
+
chainSegments: aiFix.chainSegments,
|
|
531
|
+
segments: aiFix.segments,
|
|
532
|
+
confidence: aiFix.confidence,
|
|
533
|
+
contentChange: aiFix.contentChange,
|
|
534
|
+
explanation: aiFix.explanation,
|
|
535
|
+
alternatives: this._buildAlternatives(aiFix.chainSegments, aiFix.segments),
|
|
536
|
+
safe: true,
|
|
537
|
+
})
|
|
538
|
+
.catch((err) => console.warn(`[HealingCache] ⚠️ set() failed: ${err?.message ?? err}`));
|
|
539
|
+
}
|
|
399
540
|
return canonicalSelector;
|
|
400
541
|
}
|
|
401
|
-
// AI did not return FIXED
|
|
542
|
+
// AI did not return FIXED - record as FAILED.
|
|
402
543
|
const failedEvent = {
|
|
403
544
|
kind: "FAILED",
|
|
404
545
|
stableId,
|
|
@@ -421,8 +562,15 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
421
562
|
catch (error) {
|
|
422
563
|
console.error(`[Sela] ❌ Healing Failure:`, error.message);
|
|
423
564
|
// Avoid double-recording when a SafetyGuard PROTECTED event was already pushed.
|
|
424
|
-
|
|
425
|
-
|
|
565
|
+
// Prefer the structured instanceof check; fall back to message-substring
|
|
566
|
+
// detection for any legacy throw sites that have not yet been migrated.
|
|
567
|
+
const isSafetyBlock = (0, SelaError_1.isSelaErrorFrom)(error, "SafetyGuard") ||
|
|
568
|
+
(typeof error?.message === "string" &&
|
|
569
|
+
error.message.includes("[Sela-SafetyGuard]")) ||
|
|
570
|
+
(typeof error?.message === "string" &&
|
|
571
|
+
error.message.includes("[SafetyGuard]"));
|
|
572
|
+
const isNotFoundAlreadyRecorded = typeof error?.message === "string" &&
|
|
573
|
+
error.message === "AI could not provide a fix";
|
|
426
574
|
if (!isSafetyBlock && !isNotFoundAlreadyRecorded) {
|
|
427
575
|
const failedEvent = {
|
|
428
576
|
kind: "FAILED",
|
|
@@ -452,14 +600,20 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
452
600
|
if (!filePath)
|
|
453
601
|
return "";
|
|
454
602
|
const match = filePath.match(/at\s+(.*)/i);
|
|
455
|
-
const cleaned = (match ? match[1] : filePath)
|
|
456
|
-
|
|
603
|
+
const cleaned = (match ? match[1] : filePath)
|
|
604
|
+
.replace(/^\(|\)$/g, "")
|
|
605
|
+
.trim();
|
|
606
|
+
const abs = path.isAbsolute(cleaned)
|
|
607
|
+
? cleaned
|
|
608
|
+
: path.resolve(process.cwd(), cleaned);
|
|
457
609
|
return path.relative(process.cwd(), abs) || abs;
|
|
458
610
|
}
|
|
459
611
|
_safeReadLine(filePath, lineNumber) {
|
|
460
612
|
try {
|
|
461
613
|
const cleaned = this._toReportFile(filePath);
|
|
462
|
-
const abs = path.isAbsolute(cleaned)
|
|
614
|
+
const abs = path.isAbsolute(cleaned)
|
|
615
|
+
? cleaned
|
|
616
|
+
: path.resolve(process.cwd(), cleaned);
|
|
463
617
|
if (!fs.existsSync(abs))
|
|
464
618
|
return "";
|
|
465
619
|
const lines = fs.readFileSync(abs, "utf8").split(/\r?\n/);
|
|
@@ -471,36 +625,37 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
471
625
|
}
|
|
472
626
|
}
|
|
473
627
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
628
|
+
* Clean-Room isolated diff: render a unified diff between the file's
|
|
629
|
+
* pre-Sela disk snapshot (`contentBefore`) and the post-write disk state
|
|
630
|
+
* (`contentAfter`) captured by `WorkspaceSnapshotService`. Returns "" when
|
|
631
|
+
* the file was never tracked (no snapshot) or the AST mutation produced
|
|
632
|
+
* no net change.
|
|
479
633
|
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
634
|
+
* This is what makes the HTML report immune to developer dirty edits: any
|
|
635
|
+
* pre-existing uncommitted changes live in BOTH buffers identically and
|
|
636
|
+
* are therefore filtered out by the diff. The output contains only Sela's
|
|
637
|
+
* selector swap.
|
|
483
638
|
*/
|
|
484
|
-
|
|
639
|
+
_captureIsolatedDiff(filePath) {
|
|
485
640
|
try {
|
|
486
641
|
const cleaned = this._toReportFile(filePath);
|
|
487
642
|
const abs = path.isAbsolute(cleaned)
|
|
488
643
|
? cleaned
|
|
489
644
|
: path.resolve(process.cwd(), cleaned);
|
|
490
|
-
if (!abs
|
|
645
|
+
if (!abs)
|
|
491
646
|
return "";
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
647
|
+
const snap = WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.getSnapshot(abs);
|
|
648
|
+
if (!snap || snap.contentAfter === null)
|
|
649
|
+
return "";
|
|
650
|
+
if (snap.contentBefore === snap.contentAfter)
|
|
651
|
+
return "";
|
|
652
|
+
return (0, IsolatedDiff_1.generateUnifiedDiff)(snap.contentBefore, snap.contentAfter, {
|
|
653
|
+
filePath: snap.relativePath,
|
|
654
|
+
context: 1,
|
|
498
655
|
});
|
|
499
|
-
return out.toString();
|
|
500
656
|
}
|
|
501
657
|
catch (err) {
|
|
502
|
-
|
|
503
|
-
console.debug?.(`[Sela] git diff capture skipped: ${err?.message ?? err}`);
|
|
658
|
+
console.debug?.(`[Sela] isolated diff capture skipped: ${err?.message ?? err}`);
|
|
504
659
|
return "";
|
|
505
660
|
}
|
|
506
661
|
}
|
|
@@ -511,7 +666,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
511
666
|
* `gitUnifiedDiff` payload yet.
|
|
512
667
|
*
|
|
513
668
|
* Carefully skips the diff preamble (--- a/file, +++ b/file, "diff ",
|
|
514
|
-
* "index ", rename headers)
|
|
669
|
+
* "index ", rename headers) - those lines would otherwise be mistaken for
|
|
515
670
|
* a -/+ change pair and produce nonsense like the original "line replaced
|
|
516
671
|
* by itself" output. Real change lines only count once the parser has seen
|
|
517
672
|
* a "@@" hunk header.
|
|
@@ -551,7 +706,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
551
706
|
if (!meta || !meta.auditorVerdict)
|
|
552
707
|
return null;
|
|
553
708
|
// Keep auditor confidence as `undefined` when SafetyGuard did not produce
|
|
554
|
-
// one
|
|
709
|
+
// one - the renderer fmtPct helper turns it into "n/a" rather than 0%.
|
|
555
710
|
const conf = typeof meta.auditorConfidence === "number" &&
|
|
556
711
|
Number.isFinite(meta.auditorConfidence)
|
|
557
712
|
? meta.auditorConfidence
|
|
@@ -623,15 +778,16 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
623
778
|
_buildReasoningSteps(ctx) {
|
|
624
779
|
const steps = [];
|
|
625
780
|
if (ctx.dnaTag || ctx.dnaText) {
|
|
626
|
-
steps.push(`Recovered DNA baseline
|
|
627
|
-
(ctx.dnaText ? ` with text "${ctx.dnaText}"` : "") +
|
|
781
|
+
steps.push(`Recovered DNA baseline - previously a <${ctx.dnaTag ?? "?"}>` +
|
|
782
|
+
(ctx.dnaText ? ` with text "${ctx.dnaText}"` : "") +
|
|
783
|
+
".");
|
|
628
784
|
}
|
|
629
785
|
if (ctx.successfulPath.length > 0) {
|
|
630
786
|
steps.push(`Resolved DOM context inside ${ctx.successfulPath.length} nested frame(s): ${ctx.successfulPath.join(" >> ")}.`);
|
|
631
787
|
}
|
|
632
788
|
const firstSeg = ctx.aiFix.chainSegments?.[0];
|
|
633
789
|
if (firstSeg) {
|
|
634
|
-
steps.push(`AI preserved the ${firstSeg.type} pattern
|
|
790
|
+
steps.push(`AI preserved the ${firstSeg.type} pattern - semantic locator over raw CSS path.`);
|
|
635
791
|
}
|
|
636
792
|
if (ctx.aiFix.explanation) {
|
|
637
793
|
steps.push(`AI reasoning: ${ctx.aiFix.explanation}`);
|
|
@@ -640,7 +796,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
640
796
|
if (ctx.aiFix.contentChange) {
|
|
641
797
|
steps.push(`Detected content drift: "${ctx.aiFix.contentChange.oldText}" → "${ctx.aiFix.contentChange.newText}".`);
|
|
642
798
|
}
|
|
643
|
-
steps.push(`SafetyGuard verdict: ${ctx.safetyLevel}
|
|
799
|
+
steps.push(`SafetyGuard verdict: ${ctx.safetyLevel} - change cleared for write.`);
|
|
644
800
|
steps.push(`Source updated via ${ctx.strategy} at line ${ctx.writtenLine}.`);
|
|
645
801
|
if (ctx.blastRadius != null && ctx.blastRadius > 0) {
|
|
646
802
|
steps.push(`Blast radius: ${ctx.blastRadius} dependent test reference(s) inherited the fix.`);
|
|
@@ -665,7 +821,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
665
821
|
this.testTitleBuffer.clear();
|
|
666
822
|
SourceUpdater_1.SourceUpdater.flushAdvisories();
|
|
667
823
|
}
|
|
668
|
-
// ── Sela Insights
|
|
824
|
+
// ── Sela Insights - render the transparent healing report ───────
|
|
669
825
|
let reportHtmlPath = null;
|
|
670
826
|
const healedEvents = HealReportService_1.sharedHealReport.getHealedEvents();
|
|
671
827
|
const protectedEvents = HealReportService_1.sharedHealReport.getProtectedEvents();
|
|
@@ -681,26 +837,55 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
681
837
|
console.warn(`[Sela] ⚠️ Failed to write insights report: ${err.message}`);
|
|
682
838
|
}
|
|
683
839
|
}
|
|
684
|
-
// ──
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
840
|
+
// ── Dual-Mode post-run lifecycle ─────────────────────────────────
|
|
841
|
+
//
|
|
842
|
+
// CI (process.env.CI truthy) → Clean-Room PR automation (existing flow).
|
|
843
|
+
// Local DX (CI falsy) → Write-and-notify: AST mutations are
|
|
844
|
+
// already on disk; the reporter prints a
|
|
845
|
+
// diff + vscode:// deeplink + report link
|
|
846
|
+
// via printHealReview() (no Y/N prompt).
|
|
847
|
+
//
|
|
848
|
+
// The local branch deliberately bypasses PR automation because:
|
|
849
|
+
// • AST fixes are ALREADY persisted to disk (the test passed on them).
|
|
850
|
+
// • Local devs want to inspect + cherry-pick, not auto-commit.
|
|
851
|
+
if (isCiEnvironment()) {
|
|
852
|
+
if (this.config.prAutomation.enabled) {
|
|
853
|
+
try {
|
|
854
|
+
const branchInfo = (0, PRAutomationService_1.detectBranch)(process.cwd());
|
|
855
|
+
const decision = (0, PRAutomationService_1.decideEffectiveStrategy)(this.config.prAutomation, branchInfo.branch, healedEvents);
|
|
856
|
+
if (decision.downgradeReason) {
|
|
857
|
+
console.warn(`[Sela PR] ⚠️ Strategy downgraded: '${decision.configured}' → '${decision.effective}' ` +
|
|
858
|
+
`(reason: ${decision.downgradeReason}, ` +
|
|
859
|
+
`minAI: ${fmtPctEngine(decision.minAiConfidence)}, ` +
|
|
860
|
+
`minAuditor: ${fmtPctEngine(decision.minAuditorConfidence)})`);
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
console.log(`[Sela PR] 🎯 Effective strategy: '${decision.effective}' on branch '${branchInfo.branch ?? "unknown"}'`);
|
|
864
|
+
}
|
|
865
|
+
const ctx = { cwd: process.cwd(), reportHtmlPath };
|
|
866
|
+
await (0, PRAutomationService_1.execute)(this.config.prAutomation, decision, healedEvents, branchInfo, ctx);
|
|
867
|
+
await (0, PRAutomationService_1.handleBugDetected)(this.config.prAutomation.onBugDetected, protectedEvents, branchInfo, ctx);
|
|
694
868
|
}
|
|
695
|
-
|
|
696
|
-
console.
|
|
869
|
+
catch (err) {
|
|
870
|
+
console.warn(`[Sela PR] ⚠️ PR automation step failed: ${err.message}`);
|
|
697
871
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
else if (healedEvents.length > 0) {
|
|
875
|
+
// Local DX: serialise the mutated snapshots to a cross-process
|
|
876
|
+
// ledger. The SelaReporter drains it from `onEnd()` in the main
|
|
877
|
+
// process and prints a non-blocking summary (diff + vscode://
|
|
878
|
+
// deeplink). Mutations are already on disk - the ledger only
|
|
879
|
+
// exists so the reporter knows what to narrate.
|
|
880
|
+
try {
|
|
881
|
+
(0, PendingPromptLedger_1.writePendingPromptsLedger)({
|
|
882
|
+
cwd: process.cwd(),
|
|
883
|
+
workspace: WorkspaceSnapshotService_1.sharedWorkspaceSnapshot,
|
|
884
|
+
healedEvents,
|
|
885
|
+
});
|
|
701
886
|
}
|
|
702
887
|
catch (err) {
|
|
703
|
-
console.warn(`[Sela
|
|
888
|
+
console.warn(`[Sela] ⚠️ Pending-prompts ledger write failed: ${err.message}`);
|
|
704
889
|
}
|
|
705
890
|
}
|
|
706
891
|
// Clear the report buffer last so any downstream consumers (e.g. test
|
|
@@ -708,14 +893,20 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
708
893
|
if (HealReportService_1.sharedHealReport.hasContent()) {
|
|
709
894
|
HealReportService_1.sharedHealReport.clear();
|
|
710
895
|
}
|
|
896
|
+
// Drop the Clean-Room workspace snapshot ledger so the next session
|
|
897
|
+
// starts with an empty `contentBefore` map. Without this, a long-lived
|
|
898
|
+
// Node process (`vitest --watch`, IDE test runner) would carry stale
|
|
899
|
+
// pre-Sela snapshots from one session into the next, causing the
|
|
900
|
+
// isolated-diff layer to compare against the wrong baseline.
|
|
901
|
+
WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.clear();
|
|
711
902
|
}
|
|
712
903
|
// ─────────────────────────────────────────────────────────────
|
|
713
|
-
// fetchLiveText
|
|
904
|
+
// fetchLiveText - frame-aware innerText fetch (zero-trust probe)
|
|
714
905
|
//
|
|
715
906
|
// Navigates the frame chain encoded in successfulPath, then calls
|
|
716
907
|
// .first().innerText() on the element-only selector. Uses a short
|
|
717
908
|
// 1.5 s timeout so it never adds meaningful latency. Returns null
|
|
718
|
-
// on ANY failure
|
|
909
|
+
// on ANY failure - the caller must treat null as "no data" and skip
|
|
719
910
|
// live verification rather than blocking the heal.
|
|
720
911
|
// ─────────────────────────────────────────────────────────────
|
|
721
912
|
async fetchLiveText(page, elementOnlySelector, framePath) {
|
|
@@ -746,7 +937,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
746
937
|
p === "frame" ||
|
|
747
938
|
p.includes("frame-"));
|
|
748
939
|
if (aiHasFrameSegments) {
|
|
749
|
-
console.log(`[Sela] 🔗 AI returned full frame path
|
|
940
|
+
console.log(`[Sela] 🔗 AI returned full frame path - using directly: ${aiNewSelector}`);
|
|
750
941
|
return aiNewSelector;
|
|
751
942
|
}
|
|
752
943
|
return [...frameParts, ...aiParts].join(" >> ");
|
|
@@ -774,7 +965,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
774
965
|
return arrayOfFiles;
|
|
775
966
|
}
|
|
776
967
|
// ─────────────────────────────────────────────────────────────
|
|
777
|
-
// captureSuccessfulElement
|
|
968
|
+
// captureSuccessfulElement - capture and buffer a v2 DNA snapshot.
|
|
778
969
|
//
|
|
779
970
|
// Replaces the previous DOMUtils.getElementDNA() path with a single
|
|
780
971
|
// captureElement() call that collects all v2 metrics atomically.
|