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
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.SafetyGuard = void 0;
|
|
5
5
|
const IntentAuditor_1 = require("./IntentAuditor");
|
|
6
|
+
const SelaError_1 = require("../errors/SelaError");
|
|
6
7
|
// ═══════════════════════════════════════════════════════════════════
|
|
7
8
|
// DEFAULT THRESHOLDS (match pre-config hardcoded values exactly)
|
|
8
9
|
// ═══════════════════════════════════════════════════════════════════
|
|
9
10
|
const DEFAULT_THRESHOLDS = {
|
|
10
11
|
confidenceHardStop: 85,
|
|
11
12
|
confidenceReviewThreshold: 95,
|
|
12
|
-
assertionSimilarityFloor: 0.7,
|
|
13
13
|
auditorConfidenceMin: 75,
|
|
14
14
|
allowSuspicious: false,
|
|
15
15
|
auditorStrictness: "balanced",
|
|
16
16
|
};
|
|
17
|
-
// Binary semantic opposites
|
|
17
|
+
// Binary semantic opposites - always trigger FAIL_HARD regardless of mode,
|
|
18
18
|
// because they indicate a state inversion (pass→fail, success→error, etc.)
|
|
19
19
|
// that almost certainly masks a real logical regression.
|
|
20
20
|
const SEMANTIC_OPPOSITE_PAIRS = new Set([
|
|
@@ -55,6 +55,9 @@ const SEMANTIC_OPPOSITE_PAIRS = new Set([
|
|
|
55
55
|
"valid|invalid",
|
|
56
56
|
"invalid|valid",
|
|
57
57
|
]);
|
|
58
|
+
function escapeRegExp(s) {
|
|
59
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
60
|
+
}
|
|
58
61
|
const ROLE_KEYWORDS = new Map([
|
|
59
62
|
[
|
|
60
63
|
"SUBMIT",
|
|
@@ -185,15 +188,35 @@ const ROLE_KEYWORDS = new Map([
|
|
|
185
188
|
]),
|
|
186
189
|
],
|
|
187
190
|
]);
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
+
// Pre-compiled word-boundary regexes for the role classifier. Built
|
|
192
|
+
// once at module load - classifyFunctionalRole() is on the hot heal
|
|
193
|
+
// path, so we don't pay for regex construction per call.
|
|
194
|
+
const ROLE_KEYWORD_REGEXES = Array.from(ROLE_KEYWORDS, ([role, keywords]) => ({
|
|
195
|
+
role,
|
|
196
|
+
regexes: Array.from(keywords, (kw) => new RegExp(`\\b${escapeRegExp(kw)}\\b`, "i")),
|
|
197
|
+
}));
|
|
191
198
|
class SafetyGuard {
|
|
192
199
|
intentAuditor;
|
|
193
200
|
thresholds;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Accepts either the legacy positional `thresholds` argument or an
|
|
203
|
+
* options bag (DI-friendly). Existing call sites keep working
|
|
204
|
+
* unchanged.
|
|
205
|
+
*/
|
|
206
|
+
constructor(arg) {
|
|
207
|
+
const opts = SafetyGuard.normalizeArgs(arg);
|
|
208
|
+
this.thresholds = opts.thresholds ?? DEFAULT_THRESHOLDS;
|
|
209
|
+
this.intentAuditor = opts.intentAuditor ?? new IntentAuditor_1.IntentAuditor();
|
|
210
|
+
}
|
|
211
|
+
static normalizeArgs(arg) {
|
|
212
|
+
if (!arg)
|
|
213
|
+
return {};
|
|
214
|
+
// Heuristic: an options bag has either `intentAuditor` or a nested
|
|
215
|
+
// `thresholds` field; otherwise the value itself is a ResolvedThresholds.
|
|
216
|
+
if ("intentAuditor" in arg || "thresholds" in arg) {
|
|
217
|
+
return arg;
|
|
218
|
+
}
|
|
219
|
+
return { thresholds: arg };
|
|
197
220
|
}
|
|
198
221
|
// ─────────────────────────────────────────────────────────────
|
|
199
222
|
// PUBLIC ENTRY POINT
|
|
@@ -217,7 +240,8 @@ class SafetyGuard {
|
|
|
217
240
|
if (vis.isGhost) {
|
|
218
241
|
return {
|
|
219
242
|
level: "FAIL_HARD",
|
|
220
|
-
|
|
243
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_VISIBILITY_GHOST,
|
|
244
|
+
reason: `Visibility Gate: candidate element is a Ghost (${vis.ghostReason}) - ` +
|
|
221
245
|
`functionally absent from the user's perspective`,
|
|
222
246
|
canProceed: false,
|
|
223
247
|
};
|
|
@@ -227,12 +251,13 @@ class SafetyGuard {
|
|
|
227
251
|
if (isMainBranch) {
|
|
228
252
|
return {
|
|
229
253
|
level: "FAIL_HARD",
|
|
230
|
-
|
|
254
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_VISIBILITY_OCCLUDED,
|
|
255
|
+
reason: `Visibility Gate: candidate element is occluded by an overlay on branch "${context.branch}" - ` +
|
|
231
256
|
`hard-fail policy active on main`,
|
|
232
257
|
canProceed: false,
|
|
233
258
|
};
|
|
234
259
|
}
|
|
235
|
-
console.warn(`[SafetyGuard] ⚠️ Occlusion detected on branch "${context.branch ?? "unknown"}"
|
|
260
|
+
console.warn(`[SafetyGuard] ⚠️ Occlusion detected on branch "${context.branch ?? "unknown"}" - ` +
|
|
236
261
|
`escalating to REQUIRES_REVIEW (not hard-failing on non-main branch)`);
|
|
237
262
|
return {
|
|
238
263
|
level: "REQUIRES_REVIEW",
|
|
@@ -246,7 +271,8 @@ class SafetyGuard {
|
|
|
246
271
|
if (confidence < this.thresholds.confidenceHardStop) {
|
|
247
272
|
return {
|
|
248
273
|
level: "BLOCKED",
|
|
249
|
-
|
|
274
|
+
code: SelaError_1.SAFETY_GUARD_CODES.BLOCKED_LOW_CONFIDENCE,
|
|
275
|
+
reason: `Confidence too low (${confidence}/100 < ${this.thresholds.confidenceHardStop}) - refusing to heal`,
|
|
250
276
|
canProceed: false,
|
|
251
277
|
};
|
|
252
278
|
}
|
|
@@ -257,16 +283,18 @@ class SafetyGuard {
|
|
|
257
283
|
//
|
|
258
284
|
// When liveText + dnaBaselineText are both available and differ, we
|
|
259
285
|
// synthesise a contentChange pair and apply the full inversion-check
|
|
260
|
-
// pipeline on it
|
|
286
|
+
// pipeline on it - regardless of what the AI reported.
|
|
261
287
|
//
|
|
262
288
|
// If the AI DID report contentChange we use that (more precise than raw
|
|
263
289
|
// innerText, which may include child element noise).
|
|
264
290
|
let effectiveContentChange = aiFix.contentChange;
|
|
265
|
-
if (!effectiveContentChange &&
|
|
291
|
+
if (!effectiveContentChange &&
|
|
292
|
+
liveText !== undefined &&
|
|
293
|
+
dnaBaselineText !== undefined) {
|
|
266
294
|
const normLive = liveText.trim();
|
|
267
295
|
const normDNA = dnaBaselineText.trim();
|
|
268
296
|
if (normLive !== normDNA && normLive.length > 0 && normDNA.length > 0) {
|
|
269
|
-
console.warn(`[SafetyGuard] 🚨 Zero-Trust override
|
|
297
|
+
console.warn(`[SafetyGuard] 🚨 Zero-Trust override - AI omitted contentChange but live DOM differs:\n` +
|
|
270
298
|
` DNA baseline : "${normDNA}"\n` +
|
|
271
299
|
` Live text : "${normLive}"`);
|
|
272
300
|
effectiveContentChange = { oldText: normDNA, newText: normLive };
|
|
@@ -274,7 +302,9 @@ class SafetyGuard {
|
|
|
274
302
|
}
|
|
275
303
|
// ── 3. Structural-only heal (no text change detected) ────────
|
|
276
304
|
if (!effectiveContentChange) {
|
|
277
|
-
const level = confidence < this.thresholds.confidenceReviewThreshold
|
|
305
|
+
const level = confidence < this.thresholds.confidenceReviewThreshold
|
|
306
|
+
? "REQUIRES_REVIEW"
|
|
307
|
+
: "SAFE";
|
|
278
308
|
return {
|
|
279
309
|
level,
|
|
280
310
|
reason: `Structural selector change only (confidence=${confidence})`,
|
|
@@ -286,11 +316,12 @@ class SafetyGuard {
|
|
|
286
316
|
console.log(`[SafetyGuard] 🔬 Content change (${source}): "${oldText}" → "${newText}"`);
|
|
287
317
|
// ── 4. Binary semantic-opposite guard ────────────────────────
|
|
288
318
|
// Applies in ALL modes: "Enabled"→"Disabled", "Success"→"Error", etc.
|
|
289
|
-
// These always indicate a state inversion
|
|
319
|
+
// These always indicate a state inversion - never safe to auto-heal.
|
|
290
320
|
if (this.isSemanticOpposite(oldText, newText)) {
|
|
291
321
|
return {
|
|
292
322
|
level: "FAIL_HARD",
|
|
293
|
-
|
|
323
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_SEMANTIC_INVERSION,
|
|
324
|
+
reason: `Semantic inversion detected: "${oldText}" ↔ "${newText}" - ` +
|
|
294
325
|
`state inversions mask logical regressions`,
|
|
295
326
|
canProceed: false,
|
|
296
327
|
};
|
|
@@ -305,29 +336,29 @@ class SafetyGuard {
|
|
|
305
336
|
if (isStatusInversion) {
|
|
306
337
|
return {
|
|
307
338
|
level: "FAIL_HARD",
|
|
308
|
-
|
|
339
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_STATUS_INVERSION,
|
|
340
|
+
reason: `Status inversion: "${oldText}" (${oldRole}) → "${newText}" (${newRole}) - ` +
|
|
309
341
|
`masking a logic failure is not permitted`,
|
|
310
342
|
canProceed: false,
|
|
311
343
|
};
|
|
312
344
|
}
|
|
313
345
|
// Cross-role swap (e.g. DELETE → CANCEL, SUBMIT → NAVIGATE)
|
|
314
|
-
if (oldRole !== "UNKNOWN" &&
|
|
315
|
-
newRole !== "UNKNOWN" &&
|
|
316
|
-
oldRole !== newRole) {
|
|
346
|
+
if (oldRole !== "UNKNOWN" && newRole !== "UNKNOWN" && oldRole !== newRole) {
|
|
317
347
|
if (mode === "assertion") {
|
|
318
348
|
// Assertion explicitly checked the old functional text; a different
|
|
319
349
|
// function being found is almost certainly a wrong element fix.
|
|
320
350
|
return {
|
|
321
351
|
level: "BLOCKED",
|
|
322
|
-
|
|
352
|
+
code: SelaError_1.SAFETY_GUARD_CODES.BLOCKED_ROLE_SWAP_ASSERTION,
|
|
353
|
+
reason: `Functional role swap in assertion: "${oldText}" (${oldRole}) → "${newText}" (${newRole}) - ` +
|
|
323
354
|
`the assertion was verifying a specific functional state`,
|
|
324
355
|
canProceed: false,
|
|
325
356
|
};
|
|
326
357
|
}
|
|
327
|
-
// Action mode: allow with warning
|
|
358
|
+
// Action mode: allow with warning - a UI refactor may rename a control
|
|
328
359
|
// (e.g. "Delete" button renamed "Remove" or merged into a menu)
|
|
329
360
|
console.warn(`[SafetyGuard] ⚠️ Functional role change in action: ` +
|
|
330
|
-
`"${oldText}" (${oldRole}) → "${newText}" (${newRole})
|
|
361
|
+
`"${oldText}" (${oldRole}) → "${newText}" (${newRole}) - review recommended`);
|
|
331
362
|
return {
|
|
332
363
|
level: "REQUIRES_REVIEW",
|
|
333
364
|
reason: `Functional role change in action: "${oldText}" (${oldRole}) → "${newText}" (${newRole})`,
|
|
@@ -361,7 +392,8 @@ class SafetyGuard {
|
|
|
361
392
|
if (auditVerdict.verdict === "INCONSISTENT") {
|
|
362
393
|
return {
|
|
363
394
|
level: "FAIL_HARD",
|
|
364
|
-
|
|
395
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_AUDITOR_INCONSISTENT,
|
|
396
|
+
reason: `Intent Auditor - INCONSISTENT: ${auditVerdict.reason}` +
|
|
365
397
|
(auditVerdict.inversionType
|
|
366
398
|
? ` [inversion: ${auditVerdict.inversionType}]`
|
|
367
399
|
: ""),
|
|
@@ -377,23 +409,24 @@ class SafetyGuard {
|
|
|
377
409
|
if (!this.thresholds.allowSuspicious && mode === "assertion") {
|
|
378
410
|
return {
|
|
379
411
|
level: "BLOCKED",
|
|
380
|
-
|
|
412
|
+
code: SelaError_1.SAFETY_GUARD_CODES.BLOCKED_AUDITOR_SUSPICIOUS,
|
|
413
|
+
reason: `Intent Auditor - SUSPICIOUS in assertion mode: ${auditVerdict.reason}`,
|
|
381
414
|
canProceed: false,
|
|
382
415
|
meta,
|
|
383
416
|
};
|
|
384
417
|
}
|
|
385
418
|
if (this.thresholds.allowSuspicious && mode === "assertion") {
|
|
386
|
-
console.log(`[SafetyGuard] ℹ️ allowSuspicious=true
|
|
419
|
+
console.log(`[SafetyGuard] ℹ️ allowSuspicious=true - SUSPICIOUS in assertion mode escalated to REQUIRES_REVIEW`);
|
|
387
420
|
}
|
|
388
421
|
return {
|
|
389
422
|
level: "REQUIRES_REVIEW",
|
|
390
|
-
reason: `Intent Auditor
|
|
423
|
+
reason: `Intent Auditor - SUSPICIOUS in ${mode} mode: ${auditVerdict.reason}`,
|
|
391
424
|
canProceed: true,
|
|
392
425
|
meta,
|
|
393
426
|
};
|
|
394
427
|
}
|
|
395
|
-
// CONSISTENT
|
|
396
|
-
console.log(`[SafetyGuard] ✅ Intent Auditor
|
|
428
|
+
// CONSISTENT - fall through to confidence tiers
|
|
429
|
+
console.log(`[SafetyGuard] ✅ Intent Auditor - CONSISTENT (${auditVerdict.confidence}%): ${auditVerdict.reason}`);
|
|
397
430
|
if (auditVerdict.scoreBreakdown) {
|
|
398
431
|
const { rawConfidence, penalty, bonus } = auditVerdict.scoreBreakdown;
|
|
399
432
|
if (penalty !== 0 || bonus !== 0) {
|
|
@@ -408,31 +441,11 @@ class SafetyGuard {
|
|
|
408
441
|
}
|
|
409
442
|
}
|
|
410
443
|
}
|
|
411
|
-
// ── 8.
|
|
412
|
-
// For same-role pairs (e.g. SUBMIT→SUBMIT: "Submit"→"Send") we skip
|
|
413
|
-
// the similarity gate entirely — the role match is sufficient proof
|
|
414
|
-
// of functional equivalence.
|
|
415
|
-
//
|
|
416
|
-
// The gate is only applied when BOTH roles are UNKNOWN in assertion
|
|
417
|
-
// mode, where we have no functional category to fall back on.
|
|
418
|
-
if (mode === "assertion" && oldRole === "UNKNOWN" && newRole === "UNKNOWN") {
|
|
419
|
-
const similarity = this.computeSemanticSimilarity(oldText, newText);
|
|
420
|
-
console.log(`[SafetyGuard] 📊 Similarity: "${oldText}" ↔ "${newText}" = ${(similarity * 100).toFixed(1)}%`);
|
|
421
|
-
if (similarity < this.thresholds.assertionSimilarityFloor) {
|
|
422
|
-
return {
|
|
423
|
-
level: "BLOCKED",
|
|
424
|
-
reason: `Text divergence too high in assertion: "${oldText}" → "${newText}" ` +
|
|
425
|
-
`(similarity=${(similarity * 100).toFixed(1)}% < ${(this.thresholds.assertionSimilarityFloor * 100).toFixed(0)}%) — ` +
|
|
426
|
-
`possible content regression`,
|
|
427
|
-
canProceed: false,
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
// ── 9. Confidence tiers ──────────────────────────────────────
|
|
444
|
+
// ── 8. Confidence tiers ──────────────────────────────────────
|
|
432
445
|
if (confidence < this.thresholds.confidenceReviewThreshold) {
|
|
433
446
|
return {
|
|
434
447
|
level: "REQUIRES_REVIEW",
|
|
435
|
-
reason: `Low-confidence fix (${confidence}/100)
|
|
448
|
+
reason: `Low-confidence fix (${confidence}/100) - tagging for manual review`,
|
|
436
449
|
canProceed: true,
|
|
437
450
|
meta: localAuditMeta,
|
|
438
451
|
};
|
|
@@ -455,76 +468,32 @@ class SafetyGuard {
|
|
|
455
468
|
// ─────────────────────────────────────────────────────────────
|
|
456
469
|
// FUNCTIONAL ROLE CLASSIFICATION
|
|
457
470
|
//
|
|
458
|
-
// Returns the first role whose keyword
|
|
459
|
-
// (
|
|
471
|
+
// Returns the first role whose keyword matches the text as a whole
|
|
472
|
+
// word (delimited by \b boundaries). Returns UNKNOWN when no role
|
|
473
|
+
// matches.
|
|
474
|
+
//
|
|
475
|
+
// Why \b instead of `.includes(kw)`:
|
|
476
|
+
// - "unauthorized".includes("authorize") → true ← bug
|
|
477
|
+
// - "offline".includes("on") → true ← bug
|
|
478
|
+
// - "disconnected".includes("connected") → true ← bug
|
|
479
|
+
// - "inactive".includes("active") → true ← bug
|
|
480
|
+
// The substring approach mis-classified negations into the
|
|
481
|
+
// positive role of their root word, silently bypassing
|
|
482
|
+
// FAIL_HARD_STATUS_INVERSION. Word-boundary regex eliminates
|
|
483
|
+
// these collisions while keeping multi-word keywords ("sign in")
|
|
484
|
+
// and punctuation-flanked matches ("Submit!") working.
|
|
460
485
|
// ─────────────────────────────────────────────────────────────
|
|
461
486
|
classifyFunctionalRole(text) {
|
|
462
487
|
const normalized = text.toLowerCase().trim();
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
488
|
+
if (normalized.length === 0)
|
|
489
|
+
return "UNKNOWN";
|
|
490
|
+
for (const { role, regexes } of ROLE_KEYWORD_REGEXES) {
|
|
491
|
+
for (const re of regexes) {
|
|
492
|
+
if (re.test(normalized))
|
|
466
493
|
return role;
|
|
467
|
-
}
|
|
468
494
|
}
|
|
469
495
|
}
|
|
470
496
|
return "UNKNOWN";
|
|
471
497
|
}
|
|
472
|
-
// ─────────────────────────────────────────────────────────────
|
|
473
|
-
// SEMANTIC SIMILARITY (no external dependencies)
|
|
474
|
-
//
|
|
475
|
-
// Combined score: 0.5 × levenshtein_similarity + 0.5 × jaccard_similarity
|
|
476
|
-
//
|
|
477
|
-
// Levenshtein captures character-level similarity (typos, abbreviations).
|
|
478
|
-
// Jaccard captures word-level overlap (shared vocabulary, different order).
|
|
479
|
-
// The 50/50 blend is robust against short labels where one metric misleads.
|
|
480
|
-
// ─────────────────────────────────────────────────────────────
|
|
481
|
-
computeSemanticSimilarity(a, b) {
|
|
482
|
-
const na = a.toLowerCase().trim();
|
|
483
|
-
const nb = b.toLowerCase().trim();
|
|
484
|
-
if (na === nb)
|
|
485
|
-
return 1;
|
|
486
|
-
const maxLen = Math.max(na.length, nb.length);
|
|
487
|
-
const levSim = maxLen === 0 ? 1 : 1 - this.levenshteinDistance(na, nb) / maxLen;
|
|
488
|
-
const jaccard = this.jaccardSimilarity(na, nb);
|
|
489
|
-
return 0.5 * levSim + 0.5 * jaccard;
|
|
490
|
-
}
|
|
491
|
-
// ─────────────────────────────────────────────────────────────
|
|
492
|
-
// PRIVATE: Levenshtein distance (space-optimised two-row DP)
|
|
493
|
-
// ─────────────────────────────────────────────────────────────
|
|
494
|
-
levenshteinDistance(a, b) {
|
|
495
|
-
const m = a.length;
|
|
496
|
-
const n = b.length;
|
|
497
|
-
if (m === 0)
|
|
498
|
-
return n;
|
|
499
|
-
if (n === 0)
|
|
500
|
-
return m;
|
|
501
|
-
let prev = Array.from({ length: n + 1 }, (_, i) => i);
|
|
502
|
-
let curr = new Array(n + 1);
|
|
503
|
-
for (let i = 1; i <= m; i++) {
|
|
504
|
-
curr[0] = i;
|
|
505
|
-
for (let j = 1; j <= n; j++) {
|
|
506
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
507
|
-
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
508
|
-
}
|
|
509
|
-
[prev, curr] = [curr, prev];
|
|
510
|
-
}
|
|
511
|
-
return prev[n];
|
|
512
|
-
}
|
|
513
|
-
// ─────────────────────────────────────────────────────────────
|
|
514
|
-
// PRIVATE: Jaccard word-set similarity
|
|
515
|
-
// ─────────────────────────────────────────────────────────────
|
|
516
|
-
jaccardSimilarity(a, b) {
|
|
517
|
-
const setA = new Set(a.split(/\s+/).filter(Boolean));
|
|
518
|
-
const setB = new Set(b.split(/\s+/).filter(Boolean));
|
|
519
|
-
if (setA.size === 0 && setB.size === 0)
|
|
520
|
-
return 1;
|
|
521
|
-
let intersection = 0;
|
|
522
|
-
for (const w of setA) {
|
|
523
|
-
if (setB.has(w))
|
|
524
|
-
intersection++;
|
|
525
|
-
}
|
|
526
|
-
const union = setA.size + setB.size - intersection;
|
|
527
|
-
return union === 0 ? 1 : intersection / union;
|
|
528
|
-
}
|
|
529
498
|
}
|
|
530
499
|
exports.SafetyGuard = SafetyGuard;
|
|
@@ -67,12 +67,12 @@ class SnapshotService {
|
|
|
67
67
|
return raw;
|
|
68
68
|
}
|
|
69
69
|
// ─────────────────────────────────────────────────────────────
|
|
70
|
-
// captureElement
|
|
70
|
+
// captureElement - single-pass atomic extraction of all v2 metrics.
|
|
71
71
|
//
|
|
72
72
|
// All data collection (core props, ancestry, visibility, anchors,
|
|
73
73
|
// shadow-DOM flag) runs inside ONE page.evaluate() call so the
|
|
74
74
|
// browser serialises a single IPC payload. Returns null on any
|
|
75
|
-
// failure
|
|
75
|
+
// failure - callers must treat null as "capture skipped" and not
|
|
76
76
|
// block the heal.
|
|
77
77
|
// ─────────────────────────────────────────────────────────────
|
|
78
78
|
async captureElement(page, selector, framePath = []) {
|
|
@@ -219,7 +219,7 @@ class SnapshotService {
|
|
|
219
219
|
closestLabel = t;
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
-
// P4: implicit <label> wrapper
|
|
222
|
+
// P4: implicit <label> wrapper - clone and strip the form element
|
|
223
223
|
if (!closestLabel) {
|
|
224
224
|
const wrapper = el.closest("label");
|
|
225
225
|
if (wrapper) {
|
|
@@ -318,10 +318,10 @@ class SnapshotService {
|
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
// ─────────────────────────────────────────────────────────────
|
|
321
|
-
// migrateToV2
|
|
321
|
+
// migrateToV2 - backfills default v2 fields onto a v1 snapshot.
|
|
322
322
|
//
|
|
323
323
|
// ancestry, visibility, and anchors are empty / safe defaults.
|
|
324
|
-
// The migrated snapshot is NOT re-saved to disk automatically
|
|
324
|
+
// The migrated snapshot is NOT re-saved to disk automatically -
|
|
325
325
|
// the engine decides when to flush.
|
|
326
326
|
// ─────────────────────────────────────────────────────────────
|
|
327
327
|
migrateToV2(v1) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type ASTUpdateResult } from
|
|
2
|
-
import type { DnaRecord, DnaEditPatch } from
|
|
1
|
+
import { type ASTUpdateResult } from "./ASTSourceUpdater.js";
|
|
2
|
+
import type { DnaRecord, DnaEditPatch } from "./types.js";
|
|
3
3
|
interface CallerContext {
|
|
4
4
|
filePath: string;
|
|
5
5
|
line: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SourceLinkService.d.ts","sourceRoot":"","sources":["../../src/services/SourceLinkService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SourceLinkService.d.ts","sourceRoot":"","sources":["../../src/services/SourceLinkService.ts"],"names":[],"mappings":"AAQA,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG1D,UAAU,aAAa;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAoDD,MAAM,WAAW,kBAAkB;IACjC,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,aAAa,CAAC;IACvD,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAAC;IAC9E,iBAAiB,CACf,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,eAAe,CAAC,CAAC;CAC7B;AAED,qBAAa,iBAAkB,YAAW,kBAAkB;IAC1D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAGrB;IAEH,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,aAAa;IAItD,uBAAuB,CACrB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,kBAAkB,EAAE;IAyDjB,iBAAiB,CACrB,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,eAAe,CAAC;CAuE5B"}
|
|
@@ -10,15 +10,15 @@ const ts_morph_1 = require("ts-morph");
|
|
|
10
10
|
const ASTSourceUpdater_js_1 = require("./ASTSourceUpdater.js");
|
|
11
11
|
// Mirror of ASTSourceUpdater's module-scoped ALL_SELECTOR_METHODS.
|
|
12
12
|
const ALL_SELECTOR_METHODS = new Set([
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
"locator",
|
|
14
|
+
"frameLocator",
|
|
15
|
+
"getByRole",
|
|
16
|
+
"getByLabel",
|
|
17
|
+
"getByText",
|
|
18
|
+
"getByTestId",
|
|
19
|
+
"getByPlaceholder",
|
|
20
|
+
"getByAltText",
|
|
21
|
+
"getByTitle",
|
|
22
22
|
]);
|
|
23
23
|
function dnaError(message, code) {
|
|
24
24
|
const err = new Error(message);
|
|
@@ -33,11 +33,12 @@ function extractSelector(call) {
|
|
|
33
33
|
return null;
|
|
34
34
|
const first = args[0];
|
|
35
35
|
const kind = first.getKind();
|
|
36
|
-
if (kind === ts_morph_1.SyntaxKind.StringLiteral ||
|
|
36
|
+
if (kind === ts_morph_1.SyntaxKind.StringLiteral ||
|
|
37
|
+
kind === ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral) {
|
|
37
38
|
const raw = first.getText();
|
|
38
39
|
return raw.slice(1, -1); // strip surrounding quotes/backticks
|
|
39
40
|
}
|
|
40
|
-
// For template literals with expressions, identifiers, etc.
|
|
41
|
+
// For template literals with expressions, identifiers, etc. - return raw text.
|
|
41
42
|
return first.getText();
|
|
42
43
|
}
|
|
43
44
|
// Get the method name from a CallExpression's callee.
|
|
@@ -68,7 +69,7 @@ class SourceLinkService {
|
|
|
68
69
|
return [];
|
|
69
70
|
let sf;
|
|
70
71
|
try {
|
|
71
|
-
// addSourceFileAtPath throws if already added
|
|
72
|
+
// addSourceFileAtPath throws if already added - use getOrAdd pattern.
|
|
72
73
|
sf =
|
|
73
74
|
this.project.getSourceFile(absPath) ??
|
|
74
75
|
this.project.addSourceFileAtPath(absPath);
|
|
@@ -91,7 +92,7 @@ class SourceLinkService {
|
|
|
91
92
|
if (selector === null)
|
|
92
93
|
return;
|
|
93
94
|
const raw = call.getText();
|
|
94
|
-
const preview = raw.length > 80 ? raw.slice(0, 79) +
|
|
95
|
+
const preview = raw.length > 80 ? raw.slice(0, 79) + "…" : raw;
|
|
95
96
|
candidates.push({
|
|
96
97
|
col: call.getStart(),
|
|
97
98
|
preview,
|
|
@@ -110,16 +111,24 @@ class SourceLinkService {
|
|
|
110
111
|
}
|
|
111
112
|
async applySourceUpdate(record, patch) {
|
|
112
113
|
if (!record.sourceFile) {
|
|
113
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
reason: "DNA record has no sourceFile",
|
|
117
|
+
strategy: "none",
|
|
118
|
+
};
|
|
114
119
|
}
|
|
115
120
|
if (!(0, fs_1.existsSync)(path_1.default.resolve(record.sourceFile))) {
|
|
116
|
-
throw dnaError(`Source file not found: ${record.sourceFile}`,
|
|
121
|
+
throw dnaError(`Source file not found: ${record.sourceFile}`, "SOURCE_FILE_MISSING");
|
|
117
122
|
}
|
|
118
123
|
if (record.sourceLine <= 0) {
|
|
119
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
success: false,
|
|
126
|
+
reason: "DNA record has no valid sourceLine",
|
|
127
|
+
strategy: "none",
|
|
128
|
+
};
|
|
120
129
|
}
|
|
121
130
|
const caller = this.resolveCallerContext(record);
|
|
122
|
-
if (patch.field ===
|
|
131
|
+
if (patch.field === "selector") {
|
|
123
132
|
const newSelector = patch.newValue;
|
|
124
133
|
// Resolve oldSelector: prefer patch.oldValue, fall back to the locator
|
|
125
134
|
// at patch.locatorIndex on the recorded line.
|
|
@@ -131,25 +140,25 @@ class SourceLinkService {
|
|
|
131
140
|
return {
|
|
132
141
|
success: false,
|
|
133
142
|
reason: `No locator found at index ${patch.locatorIndex} on line ${record.sourceLine}`,
|
|
134
|
-
strategy:
|
|
143
|
+
strategy: "none",
|
|
135
144
|
};
|
|
136
145
|
}
|
|
137
146
|
oldSelector = target.selector;
|
|
138
147
|
}
|
|
139
148
|
return this.updater.update(caller, oldSelector, newSelector);
|
|
140
149
|
}
|
|
141
|
-
if (patch.field ===
|
|
150
|
+
if (patch.field === "text") {
|
|
142
151
|
// Use ASTSourceUpdater's contentChange path for text-content edits.
|
|
143
152
|
const contentChange = {
|
|
144
|
-
oldText: String(patch.oldValue ??
|
|
145
|
-
newText: String(patch.newValue ??
|
|
153
|
+
oldText: String(patch.oldValue ?? ""),
|
|
154
|
+
newText: String(patch.newValue ?? ""),
|
|
146
155
|
};
|
|
147
156
|
return this.updater.update(caller, record.selector, record.selector, undefined, undefined, contentChange);
|
|
148
157
|
}
|
|
149
158
|
return {
|
|
150
159
|
success: false,
|
|
151
160
|
reason: `Source update not supported for field: ${patch.field}`,
|
|
152
|
-
strategy:
|
|
161
|
+
strategy: "none",
|
|
153
162
|
};
|
|
154
163
|
}
|
|
155
164
|
}
|
|
@@ -4,7 +4,7 @@ interface UpdateResult {
|
|
|
4
4
|
success: boolean;
|
|
5
5
|
reason: string;
|
|
6
6
|
lineUpdated?: number;
|
|
7
|
-
/** Canonical locator string written to disk
|
|
7
|
+
/** Canonical locator string written to disk - use this for runtime execution. */
|
|
8
8
|
healedLocatorString?: string;
|
|
9
9
|
/** Populated by semantic dispatch: where the definition was mutated. */
|
|
10
10
|
definitionSite?: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SourceUpdater.d.ts","sourceRoot":"","sources":["../../src/services/SourceUpdater.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"SourceUpdater.d.ts","sourceRoot":"","sources":["../../src/services/SourceUpdater.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AA4BtD,UAAU,YAAY;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,cAAc,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,aAAa;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAqZD,qBAAa,aAAa;IAMxB,MAAM,CAAC,MAAM,CACX,MAAM,EAAE,aAAa,EACrB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,MAAM,EACpB,mBAAmB,CAAC,EAAE,MAAM,EAC5B,UAAU,CAAC,EAAE,eAAe,EAAE,EAC9B,aAAa,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EACpD,kBAAkB,CAAC,EAAE,iBAAiB,EAAE,EACxC,iBAAiB,CAAC,EAAE,iBAAiB,EAAE,EACvC,cAAc,CAAC,EAAE,SAAS,GAAG,aAAa,GAAG,QAAQ,EACrD,UAAU,CAAC,EAAE,OAAO,GACnB,YAAY;IAoDf,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAwC/B,OAAO,CAAC,MAAM,CAAC,WAAW;IAkT1B,OAAO,CAAC,MAAM,CAAC,2BAA2B;IA8E1C,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA8EvC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAoErC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAsBjC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IA6DpC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAuCpC,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA6CvC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAqCrC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAqGrC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IA0BjC,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAatC,MAAM,CAAC,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAgBlD,MAAM,CAAC,cAAc,CACnB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,YAAY;IA0Cf,0FAA0F;IAC1F,MAAM,CAAC,eAAe,IAAI,IAAI;CAG/B"}
|