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.
- package/README.md +93 -0
- package/bin/sela.js +3 -0
- package/dist/cli/ErrorHandler.d.ts +10 -0
- package/dist/cli/ErrorHandler.d.ts.map +1 -0
- package/dist/cli/ErrorHandler.js +70 -0
- package/dist/cli/commands/bulk.d.ts +3 -0
- package/dist/cli/commands/bulk.d.ts.map +1 -0
- package/dist/cli/commands/bulk.js +140 -0
- package/dist/cli/commands/find.d.ts +3 -0
- package/dist/cli/commands/find.d.ts.map +1 -0
- package/dist/cli/commands/find.js +51 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +133 -0
- package/dist/cli/commands/list.d.ts +3 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +56 -0
- package/dist/cli/commands/refactor.d.ts +3 -0
- package/dist/cli/commands/refactor.d.ts.map +1 -0
- package/dist/cli/commands/refactor.js +30 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +51 -0
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +123 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +42 -0
- package/dist/cli/ui/DnaTable.d.ts +3 -0
- package/dist/cli/ui/DnaTable.d.ts.map +1 -0
- package/dist/cli/ui/DnaTable.js +70 -0
- package/dist/cli/ui/LocatorPicker.d.ts +8 -0
- package/dist/cli/ui/LocatorPicker.d.ts.map +1 -0
- package/dist/cli/ui/LocatorPicker.js +33 -0
- package/dist/cli/ui/ProgressReporter.d.ts +11 -0
- package/dist/cli/ui/ProgressReporter.d.ts.map +1 -0
- package/dist/cli/ui/ProgressReporter.js +33 -0
- package/dist/cli/ui/RefactorWizard.d.ts +26 -0
- package/dist/cli/ui/RefactorWizard.d.ts.map +1 -0
- package/dist/cli/ui/RefactorWizard.js +269 -0
- package/dist/config/ConfigLoader.d.ts +15 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +174 -0
- package/dist/config/SelaConfig.d.ts +67 -0
- package/dist/config/SelaConfig.d.ts.map +1 -0
- package/dist/config/SelaConfig.js +57 -0
- package/dist/config/defineConfig.d.ts +3 -0
- package/dist/config/defineConfig.d.ts.map +1 -0
- package/dist/config/defineConfig.js +9 -0
- package/dist/engine/FixwrightEngine.d.ts +24 -0
- package/dist/engine/FixwrightEngine.d.ts.map +1 -0
- package/dist/engine/FixwrightEngine.js +403 -0
- package/dist/engine/HealingRegistry.d.ts +40 -0
- package/dist/engine/HealingRegistry.d.ts.map +1 -0
- package/dist/engine/HealingRegistry.js +98 -0
- package/dist/engine/singleton.d.ts +3 -0
- package/dist/engine/singleton.d.ts.map +1 -0
- package/dist/engine/singleton.js +5 -0
- package/dist/fixtures/expectProxy.d.ts +12 -0
- package/dist/fixtures/expectProxy.d.ts.map +1 -0
- package/dist/fixtures/expectProxy.js +228 -0
- package/dist/fixtures/index.d.ts +6 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/index.js +688 -0
- package/dist/fixtures/moduleExpect.d.ts +2 -0
- package/dist/fixtures/moduleExpect.d.ts.map +1 -0
- package/dist/fixtures/moduleExpect.js +46 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/services/ASTSourceUpdater.d.ts +79 -0
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -0
- package/dist/services/ASTSourceUpdater.js +3177 -0
- package/dist/services/ArgumentTypeAnalyzer.d.ts +26 -0
- package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -0
- package/dist/services/ArgumentTypeAnalyzer.js +92 -0
- package/dist/services/BlastRadiusAnalyzer.d.ts +15 -0
- package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -0
- package/dist/services/BlastRadiusAnalyzer.js +103 -0
- package/dist/services/ChainValidator.d.ts +76 -0
- package/dist/services/ChainValidator.d.ts.map +1 -0
- package/dist/services/ChainValidator.js +569 -0
- package/dist/services/CrossFileHealer.d.ts +6 -0
- package/dist/services/CrossFileHealer.d.ts.map +1 -0
- package/dist/services/CrossFileHealer.js +134 -0
- package/dist/services/DefinitionTracer.d.ts +41 -0
- package/dist/services/DefinitionTracer.d.ts.map +1 -0
- package/dist/services/DefinitionTracer.js +350 -0
- package/dist/services/DnaEditorService.d.ts +31 -0
- package/dist/services/DnaEditorService.d.ts.map +1 -0
- package/dist/services/DnaEditorService.js +198 -0
- package/dist/services/DnaIndexService.d.ts +24 -0
- package/dist/services/DnaIndexService.d.ts.map +1 -0
- package/dist/services/DnaIndexService.js +131 -0
- package/dist/services/HealingAdvisory.d.ts +22 -0
- package/dist/services/HealingAdvisory.d.ts.map +1 -0
- package/dist/services/HealingAdvisory.js +42 -0
- package/dist/services/HealthReportService.d.ts +10 -0
- package/dist/services/HealthReportService.d.ts.map +1 -0
- package/dist/services/HealthReportService.js +84 -0
- package/dist/services/InitializerUpdater.d.ts +16 -0
- package/dist/services/InitializerUpdater.d.ts.map +1 -0
- package/dist/services/InitializerUpdater.js +37 -0
- package/dist/services/IntentAuditor.d.ts +39 -0
- package/dist/services/IntentAuditor.d.ts.map +1 -0
- package/dist/services/IntentAuditor.js +302 -0
- package/dist/services/LLMService.d.ts +100 -0
- package/dist/services/LLMService.d.ts.map +1 -0
- package/dist/services/LLMService.js +439 -0
- package/dist/services/SafetyGuard.d.ts +65 -0
- package/dist/services/SafetyGuard.d.ts.map +1 -0
- package/dist/services/SafetyGuard.js +524 -0
- package/dist/services/SnapshotService.d.ts +11 -0
- package/dist/services/SnapshotService.d.ts.map +1 -0
- package/dist/services/SnapshotService.js +349 -0
- package/dist/services/SourceLinkService.d.ts +26 -0
- package/dist/services/SourceLinkService.d.ts.map +1 -0
- package/dist/services/SourceLinkService.js +156 -0
- package/dist/services/SourceUpdater.d.ts +45 -0
- package/dist/services/SourceUpdater.d.ts.map +1 -0
- package/dist/services/SourceUpdater.js +1021 -0
- package/dist/services/TemplateDiffService.d.ts +25 -0
- package/dist/services/TemplateDiffService.d.ts.map +1 -0
- package/dist/services/TemplateDiffService.js +331 -0
- package/dist/services/types.d.ts +59 -0
- package/dist/services/types.d.ts.map +1 -0
- package/dist/services/types.js +2 -0
- package/dist/storage/SnapshotManager.d.ts +5 -0
- package/dist/storage/SnapshotManager.d.ts.map +1 -0
- package/dist/storage/SnapshotManager.js +31 -0
- package/dist/types/index.d.ts +95 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/utils/DOMUtils.d.ts +33 -0
- package/dist/utils/DOMUtils.d.ts.map +1 -0
- package/dist/utils/DOMUtils.js +179 -0
- package/dist/utils/StackUtils.d.ts +11 -0
- package/dist/utils/StackUtils.d.ts.map +1 -0
- package/dist/utils/StackUtils.js +120 -0
- package/dist/vendor/enquirer.d.ts +33 -0
- package/dist/vendor/enquirer.d.ts.map +1 -0
- package/dist/vendor/enquirer.js +11 -0
- 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"}
|