sela-core 1.0.0

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 (144) hide show
  1. package/README.md +93 -0
  2. package/bin/sela.js +3 -0
  3. package/dist/cli/ErrorHandler.d.ts +10 -0
  4. package/dist/cli/ErrorHandler.d.ts.map +1 -0
  5. package/dist/cli/ErrorHandler.js +70 -0
  6. package/dist/cli/commands/bulk.d.ts +3 -0
  7. package/dist/cli/commands/bulk.d.ts.map +1 -0
  8. package/dist/cli/commands/bulk.js +140 -0
  9. package/dist/cli/commands/find.d.ts +3 -0
  10. package/dist/cli/commands/find.d.ts.map +1 -0
  11. package/dist/cli/commands/find.js +51 -0
  12. package/dist/cli/commands/init.d.ts +3 -0
  13. package/dist/cli/commands/init.d.ts.map +1 -0
  14. package/dist/cli/commands/init.js +133 -0
  15. package/dist/cli/commands/list.d.ts +3 -0
  16. package/dist/cli/commands/list.d.ts.map +1 -0
  17. package/dist/cli/commands/list.js +56 -0
  18. package/dist/cli/commands/refactor.d.ts +3 -0
  19. package/dist/cli/commands/refactor.d.ts.map +1 -0
  20. package/dist/cli/commands/refactor.js +30 -0
  21. package/dist/cli/commands/status.d.ts +3 -0
  22. package/dist/cli/commands/status.d.ts.map +1 -0
  23. package/dist/cli/commands/status.js +51 -0
  24. package/dist/cli/commands/sync.d.ts +3 -0
  25. package/dist/cli/commands/sync.d.ts.map +1 -0
  26. package/dist/cli/commands/sync.js +123 -0
  27. package/dist/cli/index.d.ts +2 -0
  28. package/dist/cli/index.d.ts.map +1 -0
  29. package/dist/cli/index.js +42 -0
  30. package/dist/cli/ui/DnaTable.d.ts +3 -0
  31. package/dist/cli/ui/DnaTable.d.ts.map +1 -0
  32. package/dist/cli/ui/DnaTable.js +70 -0
  33. package/dist/cli/ui/LocatorPicker.d.ts +8 -0
  34. package/dist/cli/ui/LocatorPicker.d.ts.map +1 -0
  35. package/dist/cli/ui/LocatorPicker.js +33 -0
  36. package/dist/cli/ui/ProgressReporter.d.ts +11 -0
  37. package/dist/cli/ui/ProgressReporter.d.ts.map +1 -0
  38. package/dist/cli/ui/ProgressReporter.js +33 -0
  39. package/dist/cli/ui/RefactorWizard.d.ts +26 -0
  40. package/dist/cli/ui/RefactorWizard.d.ts.map +1 -0
  41. package/dist/cli/ui/RefactorWizard.js +269 -0
  42. package/dist/config/ConfigLoader.d.ts +15 -0
  43. package/dist/config/ConfigLoader.d.ts.map +1 -0
  44. package/dist/config/ConfigLoader.js +174 -0
  45. package/dist/config/SelaConfig.d.ts +67 -0
  46. package/dist/config/SelaConfig.d.ts.map +1 -0
  47. package/dist/config/SelaConfig.js +57 -0
  48. package/dist/config/defineConfig.d.ts +3 -0
  49. package/dist/config/defineConfig.d.ts.map +1 -0
  50. package/dist/config/defineConfig.js +9 -0
  51. package/dist/engine/FixwrightEngine.d.ts +24 -0
  52. package/dist/engine/FixwrightEngine.d.ts.map +1 -0
  53. package/dist/engine/FixwrightEngine.js +403 -0
  54. package/dist/engine/HealingRegistry.d.ts +40 -0
  55. package/dist/engine/HealingRegistry.d.ts.map +1 -0
  56. package/dist/engine/HealingRegistry.js +98 -0
  57. package/dist/engine/singleton.d.ts +3 -0
  58. package/dist/engine/singleton.d.ts.map +1 -0
  59. package/dist/engine/singleton.js +5 -0
  60. package/dist/fixtures/expectProxy.d.ts +12 -0
  61. package/dist/fixtures/expectProxy.d.ts.map +1 -0
  62. package/dist/fixtures/expectProxy.js +228 -0
  63. package/dist/fixtures/index.d.ts +6 -0
  64. package/dist/fixtures/index.d.ts.map +1 -0
  65. package/dist/fixtures/index.js +688 -0
  66. package/dist/fixtures/moduleExpect.d.ts +2 -0
  67. package/dist/fixtures/moduleExpect.d.ts.map +1 -0
  68. package/dist/fixtures/moduleExpect.js +46 -0
  69. package/dist/index.d.ts +7 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +28 -0
  72. package/dist/services/ASTSourceUpdater.d.ts +79 -0
  73. package/dist/services/ASTSourceUpdater.d.ts.map +1 -0
  74. package/dist/services/ASTSourceUpdater.js +3177 -0
  75. package/dist/services/ArgumentTypeAnalyzer.d.ts +26 -0
  76. package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -0
  77. package/dist/services/ArgumentTypeAnalyzer.js +92 -0
  78. package/dist/services/BlastRadiusAnalyzer.d.ts +15 -0
  79. package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -0
  80. package/dist/services/BlastRadiusAnalyzer.js +103 -0
  81. package/dist/services/ChainValidator.d.ts +76 -0
  82. package/dist/services/ChainValidator.d.ts.map +1 -0
  83. package/dist/services/ChainValidator.js +569 -0
  84. package/dist/services/CrossFileHealer.d.ts +6 -0
  85. package/dist/services/CrossFileHealer.d.ts.map +1 -0
  86. package/dist/services/CrossFileHealer.js +134 -0
  87. package/dist/services/DefinitionTracer.d.ts +41 -0
  88. package/dist/services/DefinitionTracer.d.ts.map +1 -0
  89. package/dist/services/DefinitionTracer.js +350 -0
  90. package/dist/services/DnaEditorService.d.ts +31 -0
  91. package/dist/services/DnaEditorService.d.ts.map +1 -0
  92. package/dist/services/DnaEditorService.js +198 -0
  93. package/dist/services/DnaIndexService.d.ts +24 -0
  94. package/dist/services/DnaIndexService.d.ts.map +1 -0
  95. package/dist/services/DnaIndexService.js +131 -0
  96. package/dist/services/HealingAdvisory.d.ts +22 -0
  97. package/dist/services/HealingAdvisory.d.ts.map +1 -0
  98. package/dist/services/HealingAdvisory.js +42 -0
  99. package/dist/services/HealthReportService.d.ts +10 -0
  100. package/dist/services/HealthReportService.d.ts.map +1 -0
  101. package/dist/services/HealthReportService.js +84 -0
  102. package/dist/services/InitializerUpdater.d.ts +16 -0
  103. package/dist/services/InitializerUpdater.d.ts.map +1 -0
  104. package/dist/services/InitializerUpdater.js +37 -0
  105. package/dist/services/IntentAuditor.d.ts +39 -0
  106. package/dist/services/IntentAuditor.d.ts.map +1 -0
  107. package/dist/services/IntentAuditor.js +302 -0
  108. package/dist/services/LLMService.d.ts +100 -0
  109. package/dist/services/LLMService.d.ts.map +1 -0
  110. package/dist/services/LLMService.js +439 -0
  111. package/dist/services/SafetyGuard.d.ts +65 -0
  112. package/dist/services/SafetyGuard.d.ts.map +1 -0
  113. package/dist/services/SafetyGuard.js +524 -0
  114. package/dist/services/SnapshotService.d.ts +11 -0
  115. package/dist/services/SnapshotService.d.ts.map +1 -0
  116. package/dist/services/SnapshotService.js +349 -0
  117. package/dist/services/SourceLinkService.d.ts +26 -0
  118. package/dist/services/SourceLinkService.d.ts.map +1 -0
  119. package/dist/services/SourceLinkService.js +156 -0
  120. package/dist/services/SourceUpdater.d.ts +45 -0
  121. package/dist/services/SourceUpdater.d.ts.map +1 -0
  122. package/dist/services/SourceUpdater.js +1021 -0
  123. package/dist/services/TemplateDiffService.d.ts +25 -0
  124. package/dist/services/TemplateDiffService.d.ts.map +1 -0
  125. package/dist/services/TemplateDiffService.js +331 -0
  126. package/dist/services/types.d.ts +59 -0
  127. package/dist/services/types.d.ts.map +1 -0
  128. package/dist/services/types.js +2 -0
  129. package/dist/storage/SnapshotManager.d.ts +5 -0
  130. package/dist/storage/SnapshotManager.d.ts.map +1 -0
  131. package/dist/storage/SnapshotManager.js +31 -0
  132. package/dist/types/index.d.ts +95 -0
  133. package/dist/types/index.d.ts.map +1 -0
  134. package/dist/types/index.js +7 -0
  135. package/dist/utils/DOMUtils.d.ts +33 -0
  136. package/dist/utils/DOMUtils.d.ts.map +1 -0
  137. package/dist/utils/DOMUtils.js +179 -0
  138. package/dist/utils/StackUtils.d.ts +11 -0
  139. package/dist/utils/StackUtils.d.ts.map +1 -0
  140. package/dist/utils/StackUtils.js +120 -0
  141. package/dist/vendor/enquirer.d.ts +33 -0
  142. package/dist/vendor/enquirer.d.ts.map +1 -0
  143. package/dist/vendor/enquirer.js +11 -0
  144. package/package.json +67 -0
@@ -0,0 +1,524 @@
1
+ "use strict";
2
+ // src/services/SafetyGuard.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.SafetyGuard = void 0;
5
+ const IntentAuditor_1 = require("./IntentAuditor");
6
+ // ═══════════════════════════════════════════════════════════════════
7
+ // DEFAULT THRESHOLDS (match pre-config hardcoded values exactly)
8
+ // ═══════════════════════════════════════════════════════════════════
9
+ const DEFAULT_THRESHOLDS = {
10
+ confidenceHardStop: 85,
11
+ confidenceReviewThreshold: 95,
12
+ assertionSimilarityFloor: 0.7,
13
+ auditorConfidenceMin: 75,
14
+ allowSuspicious: false,
15
+ auditorStrictness: "balanced",
16
+ };
17
+ // Binary semantic opposites — always trigger FAIL_HARD regardless of mode,
18
+ // because they indicate a state inversion (pass→fail, success→error, etc.)
19
+ // that almost certainly masks a real logical regression.
20
+ const SEMANTIC_OPPOSITE_PAIRS = new Set([
21
+ "denied|welcome",
22
+ "welcome|denied",
23
+ "login|logout",
24
+ "logout|login",
25
+ "success|failure",
26
+ "failure|success",
27
+ "success|error",
28
+ "error|success",
29
+ "pass|fail",
30
+ "fail|pass",
31
+ "approved|rejected",
32
+ "rejected|approved",
33
+ "active|inactive",
34
+ "inactive|active",
35
+ "enabled|disabled",
36
+ "disabled|enabled",
37
+ "open|closed",
38
+ "closed|open",
39
+ "visible|hidden",
40
+ "hidden|visible",
41
+ "on|off",
42
+ "off|on",
43
+ "true|false",
44
+ "false|true",
45
+ "allowed|denied",
46
+ "denied|allowed",
47
+ "granted|denied",
48
+ "denied|granted",
49
+ "locked|unlocked",
50
+ "unlocked|locked",
51
+ "connected|disconnected",
52
+ "disconnected|connected",
53
+ "running|stopped",
54
+ "stopped|running",
55
+ "valid|invalid",
56
+ "invalid|valid",
57
+ ]);
58
+ const ROLE_KEYWORDS = new Map([
59
+ [
60
+ "SUBMIT",
61
+ new Set([
62
+ "submit",
63
+ "send",
64
+ "confirm",
65
+ "save",
66
+ "ok",
67
+ "done",
68
+ "apply",
69
+ "proceed",
70
+ "continue",
71
+ "execute",
72
+ "finish",
73
+ "complete",
74
+ "publish",
75
+ "post",
76
+ ]),
77
+ ],
78
+ [
79
+ "CANCEL",
80
+ new Set([
81
+ "cancel",
82
+ "abort",
83
+ "dismiss",
84
+ "close",
85
+ "back",
86
+ "exit",
87
+ "skip",
88
+ "no",
89
+ "discard",
90
+ "reject",
91
+ "decline",
92
+ ]),
93
+ ],
94
+ [
95
+ "DELETE",
96
+ new Set([
97
+ "delete",
98
+ "remove",
99
+ "clear",
100
+ "erase",
101
+ "destroy",
102
+ "trash",
103
+ "purge",
104
+ "drop",
105
+ "wipe",
106
+ ]),
107
+ ],
108
+ [
109
+ "NAVIGATE",
110
+ new Set([
111
+ "next",
112
+ "previous",
113
+ "prev",
114
+ "forward",
115
+ "backward",
116
+ "go",
117
+ "navigate",
118
+ "open",
119
+ "view",
120
+ "home",
121
+ "return",
122
+ "redirect",
123
+ ]),
124
+ ],
125
+ [
126
+ "AUTH",
127
+ new Set([
128
+ "login",
129
+ "logout",
130
+ "sign in",
131
+ "sign out",
132
+ "register",
133
+ "sign up",
134
+ "log in",
135
+ "log out",
136
+ "authenticate",
137
+ "authorize",
138
+ ]),
139
+ ],
140
+ [
141
+ "STATUS_POSITIVE",
142
+ new Set([
143
+ "success",
144
+ "approved",
145
+ "active",
146
+ "enabled",
147
+ "visible",
148
+ "on",
149
+ "open",
150
+ "allowed",
151
+ "welcome",
152
+ "pass",
153
+ "passed",
154
+ "valid",
155
+ "granted",
156
+ "connected",
157
+ "running",
158
+ "unlocked",
159
+ "online",
160
+ "available",
161
+ ]),
162
+ ],
163
+ [
164
+ "STATUS_NEGATIVE",
165
+ new Set([
166
+ "error",
167
+ "failure",
168
+ "failed",
169
+ "denied",
170
+ "inactive",
171
+ "disabled",
172
+ "hidden",
173
+ "off",
174
+ "closed",
175
+ "blocked",
176
+ "rejected",
177
+ "invalid",
178
+ "locked",
179
+ "disconnected",
180
+ "stopped",
181
+ "offline",
182
+ "unavailable",
183
+ "forbidden",
184
+ "unauthorized",
185
+ ]),
186
+ ],
187
+ ]);
188
+ // ═══════════════════════════════════════════════════════════════════
189
+ // SafetyGuard
190
+ // ═══════════════════════════════════════════════════════════════════
191
+ class SafetyGuard {
192
+ intentAuditor;
193
+ thresholds;
194
+ constructor(thresholds) {
195
+ this.thresholds = thresholds ?? DEFAULT_THRESHOLDS;
196
+ this.intentAuditor = new IntentAuditor_1.IntentAuditor();
197
+ }
198
+ // ─────────────────────────────────────────────────────────────
199
+ // PUBLIC ENTRY POINT
200
+ // ─────────────────────────────────────────────────────────────
201
+ async evaluate(context, aiFix) {
202
+ const { confidence, liveText, dnaBaselineText } = aiFix;
203
+ const { mode } = context;
204
+ // ── 0. Visibility Gate ───────────────────────────────────────
205
+ // Runs before any confidence or content checks. A ghost element
206
+ // can never be safely acted upon regardless of AI confidence.
207
+ // Occlusion policy is branch-dependent (spec open question Q2
208
+ // decision: hard-fail on main, REQUIRES_REVIEW elsewhere).
209
+ if (aiFix.liveVisibility) {
210
+ const vis = aiFix.liveVisibility;
211
+ if (vis.isGhost) {
212
+ return {
213
+ level: "FAIL_HARD",
214
+ reason: `Visibility Gate: candidate element is a Ghost (${vis.ghostReason}) — ` +
215
+ `functionally absent from the user's perspective`,
216
+ canProceed: false,
217
+ };
218
+ }
219
+ if (vis.isOccluded) {
220
+ const isMainBranch = context.branch === "main" || context.branch === "master";
221
+ if (isMainBranch) {
222
+ return {
223
+ level: "FAIL_HARD",
224
+ reason: `Visibility Gate: candidate element is occluded by an overlay on branch "${context.branch}" — ` +
225
+ `hard-fail policy active on main`,
226
+ canProceed: false,
227
+ };
228
+ }
229
+ console.warn(`[SafetyGuard] ⚠️ Occlusion detected on branch "${context.branch ?? "unknown"}" — ` +
230
+ `escalating to REQUIRES_REVIEW (not hard-failing on non-main branch)`);
231
+ return {
232
+ level: "REQUIRES_REVIEW",
233
+ reason: `Visibility Gate: candidate element is occluded by an overlay ` +
234
+ `(branch: ${context.branch ?? "unknown"})`,
235
+ canProceed: true,
236
+ };
237
+ }
238
+ }
239
+ // ── 1. Hard confidence stop ──────────────────────────────────
240
+ if (confidence < this.thresholds.confidenceHardStop) {
241
+ return {
242
+ level: "BLOCKED",
243
+ reason: `Confidence too low (${confidence}/100 < ${this.thresholds.confidenceHardStop}) — refusing to heal`,
244
+ canProceed: false,
245
+ };
246
+ }
247
+ // ── 2. Zero-Trust Verification ───────────────────────────────
248
+ // The AI's contentChange field is self-reported and may be absent even
249
+ // when the element's text actually changed (e.g. AI found #error-label-v2
250
+ // but stayed silent about it now showing "Error" instead of "Success").
251
+ //
252
+ // When liveText + dnaBaselineText are both available and differ, we
253
+ // synthesise a contentChange pair and apply the full inversion-check
254
+ // pipeline on it — regardless of what the AI reported.
255
+ //
256
+ // If the AI DID report contentChange we use that (more precise than raw
257
+ // innerText, which may include child element noise).
258
+ let effectiveContentChange = aiFix.contentChange;
259
+ if (!effectiveContentChange && liveText !== undefined && dnaBaselineText !== undefined) {
260
+ const normLive = liveText.trim();
261
+ const normDNA = dnaBaselineText.trim();
262
+ if (normLive !== normDNA && normLive.length > 0 && normDNA.length > 0) {
263
+ console.warn(`[SafetyGuard] 🚨 Zero-Trust override — AI omitted contentChange but live DOM differs:\n` +
264
+ ` DNA baseline : "${normDNA}"\n` +
265
+ ` Live text : "${normLive}"`);
266
+ effectiveContentChange = { oldText: normDNA, newText: normLive };
267
+ }
268
+ }
269
+ // ── 3. Structural-only heal (no text change detected) ────────
270
+ if (!effectiveContentChange) {
271
+ const level = confidence < this.thresholds.confidenceReviewThreshold ? "REQUIRES_REVIEW" : "SAFE";
272
+ return {
273
+ level,
274
+ reason: `Structural selector change only (confidence=${confidence})`,
275
+ canProceed: true,
276
+ };
277
+ }
278
+ const { oldText, newText } = effectiveContentChange;
279
+ const source = aiFix.contentChange ? "AI-reported" : "zero-trust";
280
+ console.log(`[SafetyGuard] 🔬 Content change (${source}): "${oldText}" → "${newText}"`);
281
+ // ── 4. Binary semantic-opposite guard ────────────────────────
282
+ // Applies in ALL modes: "Enabled"→"Disabled", "Success"→"Error", etc.
283
+ // These always indicate a state inversion — never safe to auto-heal.
284
+ if (this.isSemanticOpposite(oldText, newText)) {
285
+ return {
286
+ level: "FAIL_HARD",
287
+ reason: `Semantic inversion detected: "${oldText}" ↔ "${newText}" — ` +
288
+ `state inversions mask logical regressions`,
289
+ canProceed: false,
290
+ };
291
+ }
292
+ // ── 5. Functional role analysis ──────────────────────────────
293
+ const oldRole = this.classifyFunctionalRole(oldText);
294
+ const newRole = this.classifyFunctionalRole(newText);
295
+ console.log(`[SafetyGuard] 🔍 Role classification: "${oldText}"→${oldRole}, "${newText}"→${newRole}`);
296
+ // STATUS_POSITIVE ↔ STATUS_NEGATIVE is always a logic inversion
297
+ const isStatusInversion = (oldRole === "STATUS_POSITIVE" && newRole === "STATUS_NEGATIVE") ||
298
+ (oldRole === "STATUS_NEGATIVE" && newRole === "STATUS_POSITIVE");
299
+ if (isStatusInversion) {
300
+ return {
301
+ level: "FAIL_HARD",
302
+ reason: `Status inversion: "${oldText}" (${oldRole}) → "${newText}" (${newRole}) — ` +
303
+ `masking a logic failure is not permitted`,
304
+ canProceed: false,
305
+ };
306
+ }
307
+ // Cross-role swap (e.g. DELETE → CANCEL, SUBMIT → NAVIGATE)
308
+ if (oldRole !== "UNKNOWN" &&
309
+ newRole !== "UNKNOWN" &&
310
+ oldRole !== newRole) {
311
+ if (mode === "assertion") {
312
+ // Assertion explicitly checked the old functional text; a different
313
+ // function being found is almost certainly a wrong element fix.
314
+ return {
315
+ level: "BLOCKED",
316
+ reason: `Functional role swap in assertion: "${oldText}" (${oldRole}) → "${newText}" (${newRole}) — ` +
317
+ `the assertion was verifying a specific functional state`,
318
+ canProceed: false,
319
+ };
320
+ }
321
+ // Action mode: allow with warning — a UI refactor may rename a control
322
+ // (e.g. "Delete" button renamed "Remove" or merged into a menu)
323
+ console.warn(`[SafetyGuard] ⚠️ Functional role change in action: ` +
324
+ `"${oldText}" (${oldRole}) → "${newText}" (${newRole}) — review recommended`);
325
+ return {
326
+ level: "REQUIRES_REVIEW",
327
+ reason: `Functional role change in action: "${oldText}" (${oldRole}) → "${newText}" (${newRole})`,
328
+ canProceed: true,
329
+ };
330
+ }
331
+ // ── 6. Intent Auditor gate ───────────────────────────────────
332
+ // Call the secondary LLM auditor when:
333
+ // a) zero-trust synthesized the contentChange (AI was silent → extra scrutiny), OR
334
+ // b) we are in assertion mode (the test explicitly checked the text), OR
335
+ // c) both roles are UNKNOWN (no functional category to fall back on).
336
+ // Captures the auditor's score breakdown when it runs so the engine can
337
+ // emit a consolidated heal-summary log after all checks pass.
338
+ let localAuditMeta;
339
+ const shouldAudit = source === "zero-trust" ||
340
+ mode === "assertion" ||
341
+ (oldRole === "UNKNOWN" && newRole === "UNKNOWN");
342
+ if (shouldAudit) {
343
+ const auditVerdict = await this.intentAuditor.auditIntent({
344
+ dnaText: oldText,
345
+ dnaRole: aiFix.dnaRole,
346
+ liveText: newText,
347
+ healMode: mode,
348
+ // v2 structural context forwarded for scoring
349
+ dnaAncestry: aiFix.dnaAncestry,
350
+ liveAncestry: aiFix.liveAncestry,
351
+ dnaAnchors: aiFix.dnaAnchors,
352
+ liveAnchors: aiFix.liveAnchors,
353
+ });
354
+ // ── 7. Verdict mapping ──────────────────────────────────────
355
+ if (auditVerdict.verdict === "INCONSISTENT") {
356
+ return {
357
+ level: "FAIL_HARD",
358
+ reason: `Intent Auditor — INCONSISTENT: ${auditVerdict.reason}` +
359
+ (auditVerdict.inversionType
360
+ ? ` [inversion: ${auditVerdict.inversionType}]`
361
+ : ""),
362
+ canProceed: false,
363
+ };
364
+ }
365
+ if (auditVerdict.verdict === "SUSPICIOUS") {
366
+ const meta = {
367
+ auditorVerdict: "SUSPICIOUS",
368
+ auditorConfidence: auditVerdict.confidence,
369
+ };
370
+ // Block by default in assertion mode; allowSuspicious (loose policy) downgrades to REQUIRES_REVIEW
371
+ if (!this.thresholds.allowSuspicious && mode === "assertion") {
372
+ return {
373
+ level: "BLOCKED",
374
+ reason: `Intent Auditor — SUSPICIOUS in assertion mode: ${auditVerdict.reason}`,
375
+ canProceed: false,
376
+ meta,
377
+ };
378
+ }
379
+ if (this.thresholds.allowSuspicious && mode === "assertion") {
380
+ console.log(`[SafetyGuard] ℹ️ allowSuspicious=true — SUSPICIOUS in assertion mode escalated to REQUIRES_REVIEW`);
381
+ }
382
+ return {
383
+ level: "REQUIRES_REVIEW",
384
+ reason: `Intent Auditor — SUSPICIOUS in ${mode} mode: ${auditVerdict.reason}`,
385
+ canProceed: true,
386
+ meta,
387
+ };
388
+ }
389
+ // CONSISTENT — fall through to similarity gate
390
+ console.log(`[SafetyGuard] ✅ Intent Auditor — CONSISTENT (${auditVerdict.confidence}%): ${auditVerdict.reason}`);
391
+ if (auditVerdict.scoreBreakdown) {
392
+ const { rawConfidence, penalty, bonus } = auditVerdict.scoreBreakdown;
393
+ if (penalty !== 0 || bonus !== 0) {
394
+ localAuditMeta = {
395
+ auditScoreBreakdown: {
396
+ rawConfidence,
397
+ penalty,
398
+ bonus,
399
+ adjustedConfidence: auditVerdict.confidence,
400
+ },
401
+ };
402
+ }
403
+ }
404
+ }
405
+ // ── 8. Semantic similarity gate (same-role or both UNKNOWN) ──
406
+ // For same-role pairs (e.g. SUBMIT→SUBMIT: "Submit"→"Send") we skip
407
+ // the similarity gate entirely — the role match is sufficient proof
408
+ // of functional equivalence.
409
+ //
410
+ // The gate is only applied when BOTH roles are UNKNOWN in assertion
411
+ // mode, where we have no functional category to fall back on.
412
+ if (mode === "assertion" && oldRole === "UNKNOWN" && newRole === "UNKNOWN") {
413
+ const similarity = this.computeSemanticSimilarity(oldText, newText);
414
+ console.log(`[SafetyGuard] 📊 Similarity: "${oldText}" ↔ "${newText}" = ${(similarity * 100).toFixed(1)}%`);
415
+ if (similarity < this.thresholds.assertionSimilarityFloor) {
416
+ return {
417
+ level: "BLOCKED",
418
+ reason: `Text divergence too high in assertion: "${oldText}" → "${newText}" ` +
419
+ `(similarity=${(similarity * 100).toFixed(1)}% < ${(this.thresholds.assertionSimilarityFloor * 100).toFixed(0)}%) — ` +
420
+ `possible content regression`,
421
+ canProceed: false,
422
+ };
423
+ }
424
+ }
425
+ // ── 9. Confidence tiers ──────────────────────────────────────
426
+ if (confidence < this.thresholds.confidenceReviewThreshold) {
427
+ return {
428
+ level: "REQUIRES_REVIEW",
429
+ reason: `Low-confidence fix (${confidence}/100) — tagging for manual review`,
430
+ canProceed: true,
431
+ meta: localAuditMeta,
432
+ };
433
+ }
434
+ return {
435
+ level: "SAFE",
436
+ reason: `All safety checks passed (confidence=${confidence}, source=${source}, ` +
437
+ `oldRole=${oldRole}, newRole=${newRole})`,
438
+ canProceed: true,
439
+ meta: localAuditMeta,
440
+ };
441
+ }
442
+ // ─────────────────────────────────────────────────────────────
443
+ // SEMANTIC OPPOSITE CHECK
444
+ // ─────────────────────────────────────────────────────────────
445
+ isSemanticOpposite(oldText, newText) {
446
+ const key = `${oldText.toLowerCase().trim()}|${newText.toLowerCase().trim()}`;
447
+ return SEMANTIC_OPPOSITE_PAIRS.has(key);
448
+ }
449
+ // ─────────────────────────────────────────────────────────────
450
+ // FUNCTIONAL ROLE CLASSIFICATION
451
+ //
452
+ // Returns the first role whose keyword set contains the text
453
+ // (exact match or substring). Returns UNKNOWN when no role matches.
454
+ // ─────────────────────────────────────────────────────────────
455
+ classifyFunctionalRole(text) {
456
+ const normalized = text.toLowerCase().trim();
457
+ for (const [role, keywords] of ROLE_KEYWORDS) {
458
+ for (const kw of keywords) {
459
+ if (normalized === kw || normalized.includes(kw)) {
460
+ return role;
461
+ }
462
+ }
463
+ }
464
+ return "UNKNOWN";
465
+ }
466
+ // ─────────────────────────────────────────────────────────────
467
+ // SEMANTIC SIMILARITY (no external dependencies)
468
+ //
469
+ // Combined score: 0.5 × levenshtein_similarity + 0.5 × jaccard_similarity
470
+ //
471
+ // Levenshtein captures character-level similarity (typos, abbreviations).
472
+ // Jaccard captures word-level overlap (shared vocabulary, different order).
473
+ // The 50/50 blend is robust against short labels where one metric misleads.
474
+ // ─────────────────────────────────────────────────────────────
475
+ computeSemanticSimilarity(a, b) {
476
+ const na = a.toLowerCase().trim();
477
+ const nb = b.toLowerCase().trim();
478
+ if (na === nb)
479
+ return 1;
480
+ const maxLen = Math.max(na.length, nb.length);
481
+ const levSim = maxLen === 0 ? 1 : 1 - this.levenshteinDistance(na, nb) / maxLen;
482
+ const jaccard = this.jaccardSimilarity(na, nb);
483
+ return 0.5 * levSim + 0.5 * jaccard;
484
+ }
485
+ // ─────────────────────────────────────────────────────────────
486
+ // PRIVATE: Levenshtein distance (space-optimised two-row DP)
487
+ // ─────────────────────────────────────────────────────────────
488
+ levenshteinDistance(a, b) {
489
+ const m = a.length;
490
+ const n = b.length;
491
+ if (m === 0)
492
+ return n;
493
+ if (n === 0)
494
+ return m;
495
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
496
+ let curr = new Array(n + 1);
497
+ for (let i = 1; i <= m; i++) {
498
+ curr[0] = i;
499
+ for (let j = 1; j <= n; j++) {
500
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
501
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
502
+ }
503
+ [prev, curr] = [curr, prev];
504
+ }
505
+ return prev[n];
506
+ }
507
+ // ─────────────────────────────────────────────────────────────
508
+ // PRIVATE: Jaccard word-set similarity
509
+ // ─────────────────────────────────────────────────────────────
510
+ jaccardSimilarity(a, b) {
511
+ const setA = new Set(a.split(/\s+/).filter(Boolean));
512
+ const setB = new Set(b.split(/\s+/).filter(Boolean));
513
+ if (setA.size === 0 && setB.size === 0)
514
+ return 1;
515
+ let intersection = 0;
516
+ for (const w of setA) {
517
+ if (setB.has(w))
518
+ intersection++;
519
+ }
520
+ const union = setA.size + setB.size - intersection;
521
+ return union === 0 ? 1 : intersection / union;
522
+ }
523
+ }
524
+ exports.SafetyGuard = SafetyGuard;
@@ -0,0 +1,11 @@
1
+ import { Page } from "@playwright/test";
2
+ import { ElementSnapshot, ElementSnapshotV2 } from "../types";
3
+ export declare class SnapshotService {
4
+ private baseDir;
5
+ constructor();
6
+ save(key: string, data: ElementSnapshot): Promise<void>;
7
+ load(key: string): Promise<ElementSnapshotV2 | null>;
8
+ captureElement(page: Page, selector: string, framePath?: string[]): Promise<ElementSnapshotV2 | null>;
9
+ private migrateToV2;
10
+ }
11
+ //# sourceMappingURL=SnapshotService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SnapshotService.d.ts","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EACL,eAAe,EACf,iBAAiB,EAElB,MAAM,UAAU,CAAC;AAElB,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAAS;;IASlB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAYvD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAwBpD,cAAc,CAClB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAM,EAAO,GACvB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAoRpC,OAAO,CAAC,WAAW;CAsBpB"}