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.
Files changed (124) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/ErrorHandler.d.ts +1 -1
  3. package/dist/cli/ErrorHandler.js +14 -14
  4. package/dist/cli/commands/bulk.d.ts +1 -1
  5. package/dist/cli/commands/bulk.d.ts.map +1 -1
  6. package/dist/cli/commands/bulk.js +51 -33
  7. package/dist/cli/commands/init.d.ts +1 -1
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +26 -18
  10. package/dist/cli/commands/showReport.d.ts +1 -1
  11. package/dist/cli/commands/showReport.d.ts.map +1 -1
  12. package/dist/cli/commands/showReport.js +12 -10
  13. package/dist/cli/commands/status.d.ts +1 -1
  14. package/dist/cli/commands/status.d.ts.map +1 -1
  15. package/dist/cli/commands/status.js +8 -8
  16. package/dist/cli/commands/sync.d.ts +1 -1
  17. package/dist/cli/commands/sync.d.ts.map +1 -1
  18. package/dist/cli/commands/sync.js +38 -30
  19. package/dist/cli/index.js +9 -9
  20. package/dist/cli/ui/DnaTable.d.ts +1 -1
  21. package/dist/cli/ui/DnaTable.d.ts.map +1 -1
  22. package/dist/cli/ui/DnaTable.js +23 -16
  23. package/dist/cli/ui/RefactorWizard.d.ts +3 -3
  24. package/dist/cli/ui/RefactorWizard.d.ts.map +1 -1
  25. package/dist/cli/ui/RefactorWizard.js +108 -82
  26. package/dist/config/ConfigLoader.d.ts +3 -2
  27. package/dist/config/ConfigLoader.d.ts.map +1 -1
  28. package/dist/config/ConfigLoader.js +23 -13
  29. package/dist/config/DryRunGuard.d.ts +14 -0
  30. package/dist/config/DryRunGuard.d.ts.map +1 -0
  31. package/dist/config/DryRunGuard.js +79 -0
  32. package/dist/config/SelaConfig.d.ts +26 -16
  33. package/dist/config/SelaConfig.d.ts.map +1 -1
  34. package/dist/config/SelaConfig.js +54 -22
  35. package/dist/engine/HealingRegistry.d.ts +1 -1
  36. package/dist/engine/HealingRegistry.js +3 -3
  37. package/dist/engine/SelaEngine.d.ts +13 -10
  38. package/dist/engine/SelaEngine.d.ts.map +1 -1
  39. package/dist/engine/SelaEngine.js +292 -101
  40. package/dist/errors/SelaError.d.ts +132 -0
  41. package/dist/errors/SelaError.d.ts.map +1 -0
  42. package/dist/errors/SelaError.js +154 -0
  43. package/dist/fixtures/expectProxy.d.ts +1 -1
  44. package/dist/fixtures/expectProxy.d.ts.map +1 -1
  45. package/dist/fixtures/expectProxy.js +17 -10
  46. package/dist/fixtures/index.d.ts +7 -1
  47. package/dist/fixtures/index.d.ts.map +1 -1
  48. package/dist/fixtures/index.js +26 -11
  49. package/dist/fixtures/proxyTag.d.ts +12 -0
  50. package/dist/fixtures/proxyTag.d.ts.map +1 -0
  51. package/dist/fixtures/proxyTag.js +45 -0
  52. package/dist/index.d.ts +8 -6
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +9 -4
  55. package/dist/reporter/SelaReporter.d.ts +143 -0
  56. package/dist/reporter/SelaReporter.d.ts.map +1 -0
  57. package/dist/reporter/SelaReporter.js +428 -0
  58. package/dist/services/ASTSourceUpdater.d.ts +20 -1
  59. package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
  60. package/dist/services/ASTSourceUpdater.js +257 -89
  61. package/dist/services/ArgumentTypeAnalyzer.js +7 -7
  62. package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -1
  63. package/dist/services/BlastRadiusAnalyzer.js +5 -2
  64. package/dist/services/ChainValidator.js +22 -22
  65. package/dist/services/CrossFileHealer.js +7 -7
  66. package/dist/services/DefinitionTracer.d.ts +1 -1
  67. package/dist/services/DefinitionTracer.d.ts.map +1 -1
  68. package/dist/services/DefinitionTracer.js +28 -13
  69. package/dist/services/DnaEditorService.d.ts +2 -2
  70. package/dist/services/DnaEditorService.d.ts.map +1 -1
  71. package/dist/services/DnaEditorService.js +81 -62
  72. package/dist/services/DnaIndexService.d.ts +1 -1
  73. package/dist/services/DnaIndexService.d.ts.map +1 -1
  74. package/dist/services/DnaIndexService.js +14 -14
  75. package/dist/services/HealReportService.d.ts +43 -1
  76. package/dist/services/HealReportService.d.ts.map +1 -1
  77. package/dist/services/HealReportService.js +108 -25
  78. package/dist/services/HealingAdvisory.js +1 -1
  79. package/dist/services/HealingCacheService.d.ts +135 -0
  80. package/dist/services/HealingCacheService.d.ts.map +1 -0
  81. package/dist/services/HealingCacheService.js +460 -0
  82. package/dist/services/HealthReportService.d.ts +1 -1
  83. package/dist/services/HealthReportService.d.ts.map +1 -1
  84. package/dist/services/HealthReportService.js +25 -25
  85. package/dist/services/InitializerUpdater.d.ts.map +1 -1
  86. package/dist/services/InitializerUpdater.js +20 -1
  87. package/dist/services/IntentAuditor.d.ts +18 -1
  88. package/dist/services/IntentAuditor.d.ts.map +1 -1
  89. package/dist/services/IntentAuditor.js +50 -26
  90. package/dist/services/InteractiveReview.d.ts +15 -0
  91. package/dist/services/InteractiveReview.d.ts.map +1 -0
  92. package/dist/services/InteractiveReview.js +125 -0
  93. package/dist/services/LLMService.d.ts +18 -2
  94. package/dist/services/LLMService.d.ts.map +1 -1
  95. package/dist/services/LLMService.js +33 -23
  96. package/dist/services/PRAutomationService.d.ts +14 -1
  97. package/dist/services/PRAutomationService.d.ts.map +1 -1
  98. package/dist/services/PRAutomationService.js +287 -92
  99. package/dist/services/PendingPromptLedger.d.ts +44 -0
  100. package/dist/services/PendingPromptLedger.d.ts.map +1 -0
  101. package/dist/services/PendingPromptLedger.js +180 -0
  102. package/dist/services/ReportGenerator.d.ts +70 -0
  103. package/dist/services/ReportGenerator.d.ts.map +1 -0
  104. package/dist/services/ReportGenerator.js +191 -0
  105. package/dist/services/SafetyGuard.d.ts +30 -7
  106. package/dist/services/SafetyGuard.d.ts.map +1 -1
  107. package/dist/services/SafetyGuard.js +85 -116
  108. package/dist/services/SnapshotService.js +5 -5
  109. package/dist/services/SourceLinkService.d.ts +2 -2
  110. package/dist/services/SourceLinkService.d.ts.map +1 -1
  111. package/dist/services/SourceLinkService.js +31 -22
  112. package/dist/services/SourceUpdater.d.ts +1 -1
  113. package/dist/services/SourceUpdater.d.ts.map +1 -1
  114. package/dist/services/SourceUpdater.js +66 -34
  115. package/dist/services/TemplateDiffService.d.ts.map +1 -1
  116. package/dist/services/TemplateDiffService.js +18 -15
  117. package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
  118. package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
  119. package/dist/services/WorkspaceSnapshotService.js +200 -0
  120. package/dist/types/index.d.ts +1 -1
  121. package/dist/utils/IsolatedDiff.d.ts +45 -0
  122. package/dist/utils/IsolatedDiff.d.ts.map +1 -0
  123. package/dist/utils/IsolatedDiff.js +382 -0
  124. 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 always trigger FAIL_HARD regardless of mode,
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
- // SafetyGuard
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
- constructor(thresholds) {
195
- this.thresholds = thresholds ?? DEFAULT_THRESHOLDS;
196
- this.intentAuditor = new IntentAuditor_1.IntentAuditor();
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
- reason: `Visibility Gate: candidate element is a Ghost (${vis.ghostReason}) — ` +
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
- reason: `Visibility Gate: candidate element is occluded by an overlay on branch "${context.branch}" — ` +
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
- reason: `Confidence too low (${confidence}/100 < ${this.thresholds.confidenceHardStop}) — refusing to heal`,
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 regardless of what the AI reported.
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 && liveText !== undefined && dnaBaselineText !== undefined) {
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 AI omitted contentChange but live DOM differs:\n` +
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 ? "REQUIRES_REVIEW" : "SAFE";
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 never safe to auto-heal.
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
- reason: `Semantic inversion detected: "${oldText}" ↔ "${newText}" — ` +
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
- reason: `Status inversion: "${oldText}" (${oldRole}) → "${newText}" (${newRole}) — ` +
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
- reason: `Functional role swap in assertion: "${oldText}" (${oldRole}) → "${newText}" (${newRole}) — ` +
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 a UI refactor may rename a control
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}) review recommended`);
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
- reason: `Intent Auditor — INCONSISTENT: ${auditVerdict.reason}` +
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
- reason: `Intent Auditor — SUSPICIOUS in assertion mode: ${auditVerdict.reason}`,
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 SUSPICIOUS in assertion mode escalated to REQUIRES_REVIEW`);
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 SUSPICIOUS in ${mode} mode: ${auditVerdict.reason}`,
423
+ reason: `Intent Auditor - SUSPICIOUS in ${mode} mode: ${auditVerdict.reason}`,
391
424
  canProceed: true,
392
425
  meta,
393
426
  };
394
427
  }
395
- // CONSISTENT fall through to similarity gate
396
- console.log(`[SafetyGuard] ✅ Intent Auditor CONSISTENT (${auditVerdict.confidence}%): ${auditVerdict.reason}`);
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. Semantic similarity gate (same-role or both UNKNOWN) ──
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) tagging for manual review`,
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 set contains the text
459
- // (exact match or substring). Returns UNKNOWN when no role matches.
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
- for (const [role, keywords] of ROLE_KEYWORDS) {
464
- for (const kw of keywords) {
465
- if (normalized === kw || normalized.includes(kw)) {
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 single-pass atomic extraction of all v2 metrics.
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 callers must treat null as "capture skipped" and not
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 clone and strip the form element
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 backfills default v2 fields onto a v1 snapshot.
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 './ASTSourceUpdater.js';
2
- import type { DnaRecord, DnaEditPatch } from './types.js';
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":"AAGA,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;AAiDD,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,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CACrF;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,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAyDvE,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC;CAyD1F"}
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
- 'locator',
14
- 'frameLocator',
15
- 'getByRole',
16
- 'getByLabel',
17
- 'getByText',
18
- 'getByTestId',
19
- 'getByPlaceholder',
20
- 'getByAltText',
21
- 'getByTitle',
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 || kind === ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral) {
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. return raw text.
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 use getOrAdd pattern.
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) + '' : raw;
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 { success: false, reason: 'DNA record has no sourceFile', strategy: 'none' };
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}`, 'SOURCE_FILE_MISSING');
121
+ throw dnaError(`Source file not found: ${record.sourceFile}`, "SOURCE_FILE_MISSING");
117
122
  }
118
123
  if (record.sourceLine <= 0) {
119
- return { success: false, reason: 'DNA record has no valid sourceLine', strategy: 'none' };
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 === 'selector') {
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: 'none',
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 === 'text') {
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: 'none',
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 use this for runtime execution. */
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;AAMtD,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;AA0YD,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;IAwS1B,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"}
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"}