@vibecheckai/cli 3.2.2 → 3.2.4
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/bin/.generated +25 -25
- package/bin/dev/run-v2-torture.js +30 -30
- package/bin/runners/ENHANCEMENT_GUIDE.md +121 -121
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
- package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +117 -28
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +23 -14
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +72 -1
- package/bin/runners/lib/agent-firewall/interceptor/base.js +2 -2
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +6 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +34 -3
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +29 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +12 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +21 -0
- package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
- package/bin/runners/lib/analyzers.js +606 -325
- package/bin/runners/lib/auth-truth.js +193 -193
- package/bin/runners/lib/backup.js +62 -62
- package/bin/runners/lib/billing.js +107 -107
- package/bin/runners/lib/claims.js +118 -118
- package/bin/runners/lib/cli-ui.js +540 -540
- package/bin/runners/lib/contracts/auth-contract.js +202 -202
- package/bin/runners/lib/contracts/env-contract.js +181 -181
- package/bin/runners/lib/contracts/external-contract.js +206 -206
- package/bin/runners/lib/contracts/guard.js +168 -168
- package/bin/runners/lib/contracts/index.js +89 -89
- package/bin/runners/lib/contracts/plan-validator.js +311 -311
- package/bin/runners/lib/contracts/route-contract.js +199 -199
- package/bin/runners/lib/contracts.js +804 -804
- package/bin/runners/lib/detect.js +89 -89
- package/bin/runners/lib/doctor/autofix.js +254 -254
- package/bin/runners/lib/doctor/index.js +37 -37
- package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
- package/bin/runners/lib/doctor/modules/index.js +46 -46
- package/bin/runners/lib/doctor/modules/network.js +250 -250
- package/bin/runners/lib/doctor/modules/project.js +312 -312
- package/bin/runners/lib/doctor/modules/runtime.js +224 -224
- package/bin/runners/lib/doctor/modules/security.js +348 -348
- package/bin/runners/lib/doctor/modules/system.js +213 -213
- package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
- package/bin/runners/lib/doctor/reporter.js +262 -262
- package/bin/runners/lib/doctor/service.js +262 -262
- package/bin/runners/lib/doctor/types.js +113 -113
- package/bin/runners/lib/doctor/ui.js +263 -263
- package/bin/runners/lib/doctor-v2.js +608 -608
- package/bin/runners/lib/drift.js +425 -425
- package/bin/runners/lib/enforcement.js +72 -72
- package/bin/runners/lib/engines/accessibility-engine.js +190 -0
- package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
- package/bin/runners/lib/engines/ast-cache.js +99 -0
- package/bin/runners/lib/engines/code-quality-engine.js +255 -0
- package/bin/runners/lib/engines/console-logs-engine.js +115 -0
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
- package/bin/runners/lib/engines/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
- package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
- package/bin/runners/lib/engines/file-filter.js +131 -0
- package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
- package/bin/runners/lib/engines/mock-data-engine.js +272 -0
- package/bin/runners/lib/engines/parallel-processor.js +71 -0
- package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
- package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
- package/bin/runners/lib/engines/type-aware-engine.js +152 -0
- package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
- package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
- package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
- package/bin/runners/lib/enterprise-detect.js +603 -603
- package/bin/runners/lib/enterprise-init.js +942 -942
- package/bin/runners/lib/env-resolver.js +417 -417
- package/bin/runners/lib/env-template.js +66 -66
- package/bin/runners/lib/env.js +189 -189
- package/bin/runners/lib/extractors/client-calls.js +990 -990
- package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
- package/bin/runners/lib/extractors/fastify-routes.js +426 -426
- package/bin/runners/lib/extractors/index.js +363 -363
- package/bin/runners/lib/extractors/next-routes.js +524 -524
- package/bin/runners/lib/extractors/proof-graph.js +431 -431
- package/bin/runners/lib/extractors/route-matcher.js +451 -451
- package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
- package/bin/runners/lib/extractors/ui-bindings.js +547 -547
- package/bin/runners/lib/findings-schema.js +281 -281
- package/bin/runners/lib/firewall-prompt.js +50 -50
- package/bin/runners/lib/global-flags.js +213 -213
- package/bin/runners/lib/graph/graph-builder.js +265 -265
- package/bin/runners/lib/graph/html-renderer.js +413 -413
- package/bin/runners/lib/graph/index.js +32 -32
- package/bin/runners/lib/graph/runtime-collector.js +215 -215
- package/bin/runners/lib/graph/static-extractor.js +518 -518
- package/bin/runners/lib/html-report.js +650 -650
- package/bin/runners/lib/interactive-menu.js +1496 -1496
- package/bin/runners/lib/llm.js +75 -75
- package/bin/runners/lib/meter.js +61 -61
- package/bin/runners/lib/missions/evidence.js +126 -126
- package/bin/runners/lib/patch.js +40 -40
- package/bin/runners/lib/permissions/auth-model.js +213 -213
- package/bin/runners/lib/permissions/idor-prover.js +205 -205
- package/bin/runners/lib/permissions/index.js +45 -45
- package/bin/runners/lib/permissions/matrix-builder.js +198 -198
- package/bin/runners/lib/pkgjson.js +28 -28
- package/bin/runners/lib/policy.js +295 -295
- package/bin/runners/lib/preflight.js +142 -142
- package/bin/runners/lib/reality/correlation-detectors.js +359 -359
- package/bin/runners/lib/reality/index.js +318 -318
- package/bin/runners/lib/reality/request-hashing.js +416 -416
- package/bin/runners/lib/reality/request-mapper.js +453 -453
- package/bin/runners/lib/reality/safety-rails.js +463 -463
- package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
- package/bin/runners/lib/reality/toast-detector.js +393 -393
- package/bin/runners/lib/reality-findings.js +84 -84
- package/bin/runners/lib/receipts.js +179 -179
- package/bin/runners/lib/redact.js +29 -29
- package/bin/runners/lib/replay/capsule-manager.js +154 -154
- package/bin/runners/lib/replay/index.js +263 -263
- package/bin/runners/lib/replay/player.js +348 -348
- package/bin/runners/lib/replay/recorder.js +331 -331
- package/bin/runners/lib/report-output.js +187 -187
- package/bin/runners/lib/report.js +135 -135
- package/bin/runners/lib/route-detection.js +1140 -1140
- package/bin/runners/lib/sandbox/index.js +59 -59
- package/bin/runners/lib/sandbox/proof-chain.js +399 -399
- package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
- package/bin/runners/lib/sandbox/worktree.js +174 -174
- package/bin/runners/lib/scan-output.js +525 -190
- package/bin/runners/lib/schema-validator.js +350 -350
- package/bin/runners/lib/schemas/contracts.schema.json +160 -160
- package/bin/runners/lib/schemas/finding.schema.json +100 -100
- package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
- package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
- package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
- package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
- package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
- package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
- package/bin/runners/lib/schemas/validator.js +438 -438
- package/bin/runners/lib/score-history.js +282 -282
- package/bin/runners/lib/share-pack.js +239 -239
- package/bin/runners/lib/snippets.js +67 -67
- package/bin/runners/lib/status-output.js +253 -253
- package/bin/runners/lib/terminal-ui.js +351 -271
- package/bin/runners/lib/upsell.js +510 -510
- package/bin/runners/lib/usage.js +153 -153
- package/bin/runners/lib/validate-patch.js +156 -156
- package/bin/runners/lib/verdict-engine.js +628 -628
- package/bin/runners/reality/engine.js +917 -917
- package/bin/runners/reality/flows.js +122 -122
- package/bin/runners/reality/report.js +378 -378
- package/bin/runners/reality/session.js +193 -193
- package/bin/runners/runGuard.js +168 -168
- package/bin/runners/runProof.zip +0 -0
- package/bin/runners/runProve.js +8 -0
- package/bin/runners/runReality.js +14 -0
- package/bin/runners/runScan.js +17 -1
- package/bin/runners/runTruth.js +15 -3
- package/mcp-server/tier-auth.js +4 -4
- package/mcp-server/tools/index.js +72 -72
- package/package.json +1 -1
|
@@ -1,463 +1,463 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Safety Rails v2
|
|
3
|
-
*
|
|
4
|
-
* False-positive prevention for UI change detection.
|
|
5
|
-
* Handles hydration suppression, action stabilization, duplicate filtering.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
"use strict";
|
|
9
|
-
|
|
10
|
-
// =============================================================================
|
|
11
|
-
// CONFIGURATION
|
|
12
|
-
// =============================================================================
|
|
13
|
-
|
|
14
|
-
const DEFAULT_CONFIG = {
|
|
15
|
-
// Hydration suppression
|
|
16
|
-
hydration: {
|
|
17
|
-
suppressionWindowMs: 2000, // Ignore changes in first 2s after navigation
|
|
18
|
-
ignoreInitialRender: true,
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
// Action stabilization
|
|
22
|
-
stabilization: {
|
|
23
|
-
quietMs: 300, // Wait for no DOM mutations
|
|
24
|
-
settleMs: 250, // Wait after action before snapshot
|
|
25
|
-
actionWindowMs: 8000, // Max wait for action effects
|
|
26
|
-
networkIdleMs: 500, // Wait for network idle
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
// Duplicate filtering
|
|
30
|
-
duplicates: {
|
|
31
|
-
debounceMs: 100, // Ignore repeated clicks within 100ms
|
|
32
|
-
sameElementDebounceMs: 500, // Same element clicks
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
// Toast filtering
|
|
36
|
-
toast: {
|
|
37
|
-
maxLifetimeMs: 15000, // Ignore persistent banners
|
|
38
|
-
minVisibleMs: 100, // Ignore flash toasts
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
// Ignore selectors (user configurable)
|
|
42
|
-
ignoreSelectors: [
|
|
43
|
-
"[data-hydration]",
|
|
44
|
-
"[data-nextjs-scroll-focus-boundary]",
|
|
45
|
-
"nextjs-portal",
|
|
46
|
-
"#__next-build-watcher",
|
|
47
|
-
"#__next-prerender-indicator",
|
|
48
|
-
".dev-overlay",
|
|
49
|
-
"[data-hot-reload]",
|
|
50
|
-
],
|
|
51
|
-
|
|
52
|
-
// Text noise patterns (timestamps, counters)
|
|
53
|
-
textNoisePatterns: [
|
|
54
|
-
/^\d{1,2}:\d{2}(:\d{2})?\s*(AM|PM)?$/i, // 12:34, 12:34:56, 12:34 PM
|
|
55
|
-
/^\d+(\.\d+)?\s*(s|ms|sec|seconds?)$/i, // 3s, 100ms
|
|
56
|
-
/^(just now|now|ago|\d+\s*(min|hour|day)s?\s*ago)$/i,
|
|
57
|
-
/^\d+$/, // Pure numbers
|
|
58
|
-
],
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// =============================================================================
|
|
62
|
-
// HYDRATION SUPPRESSION
|
|
63
|
-
// =============================================================================
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Create hydration tracker for a page
|
|
67
|
-
*/
|
|
68
|
-
function createHydrationTracker(options = {}) {
|
|
69
|
-
const config = { ...DEFAULT_CONFIG.hydration, ...options };
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
navigationTime: null,
|
|
73
|
-
isHydrating: false,
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Mark navigation start
|
|
77
|
-
*/
|
|
78
|
-
startNavigation() {
|
|
79
|
-
this.navigationTime = Date.now();
|
|
80
|
-
this.isHydrating = true;
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Check if still in hydration window
|
|
85
|
-
*/
|
|
86
|
-
inHydrationWindow() {
|
|
87
|
-
if (!this.navigationTime || !config.ignoreInitialRender) {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
const elapsed = Date.now() - this.navigationTime;
|
|
91
|
-
return elapsed < config.suppressionWindowMs;
|
|
92
|
-
},
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Mark hydration complete (e.g., after first meaningful paint)
|
|
96
|
-
*/
|
|
97
|
-
markHydrationComplete() {
|
|
98
|
-
this.isHydrating = false;
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Should suppress this change?
|
|
103
|
-
*/
|
|
104
|
-
shouldSuppress(change) {
|
|
105
|
-
if (!this.inHydrationWindow()) return false;
|
|
106
|
-
|
|
107
|
-
// During hydration, suppress semantic-only changes
|
|
108
|
-
// Still allow route changes and toasts
|
|
109
|
-
if (change.routeChanged) return false;
|
|
110
|
-
if (change.reasons?.includes("toast_success") || change.reasons?.includes("toast_error")) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return true;
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// =============================================================================
|
|
120
|
-
// ACTION STABILIZATION
|
|
121
|
-
// =============================================================================
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Create action stabilizer for DOM mutations
|
|
125
|
-
*/
|
|
126
|
-
function createActionStabilizer(page, options = {}) {
|
|
127
|
-
const config = { ...DEFAULT_CONFIG.stabilization, ...options };
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
mutationCount: 0,
|
|
131
|
-
lastMutationTime: 0,
|
|
132
|
-
pendingRequests: new Set(),
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Wait for DOM to be quiet before action
|
|
136
|
-
*/
|
|
137
|
-
async waitForQuiet() {
|
|
138
|
-
const startTime = Date.now();
|
|
139
|
-
|
|
140
|
-
while (Date.now() - startTime < config.quietMs * 3) {
|
|
141
|
-
// Check if DOM has been quiet
|
|
142
|
-
const timeSinceLastMutation = Date.now() - this.lastMutationTime;
|
|
143
|
-
const hasPendingRequests = this.pendingRequests.size > 0;
|
|
144
|
-
|
|
145
|
-
if (timeSinceLastMutation >= config.quietMs && !hasPendingRequests) {
|
|
146
|
-
return { stable: true, waitedMs: Date.now() - startTime };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
await sleep(50);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return { stable: false, waitedMs: Date.now() - startTime };
|
|
153
|
-
},
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Wait for effects after action
|
|
157
|
-
*/
|
|
158
|
-
async waitForSettle() {
|
|
159
|
-
await sleep(config.settleMs);
|
|
160
|
-
|
|
161
|
-
// Additional wait if network is active
|
|
162
|
-
if (this.pendingRequests.size > 0) {
|
|
163
|
-
const networkStart = Date.now();
|
|
164
|
-
while (this.pendingRequests.size > 0 && Date.now() - networkStart < config.networkIdleMs) {
|
|
165
|
-
await sleep(50);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Record mutation
|
|
172
|
-
*/
|
|
173
|
-
recordMutation() {
|
|
174
|
-
this.mutationCount++;
|
|
175
|
-
this.lastMutationTime = Date.now();
|
|
176
|
-
},
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Track request start
|
|
180
|
-
*/
|
|
181
|
-
trackRequestStart(requestId) {
|
|
182
|
-
this.pendingRequests.add(requestId);
|
|
183
|
-
},
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Track request end
|
|
187
|
-
*/
|
|
188
|
-
trackRequestEnd(requestId) {
|
|
189
|
-
this.pendingRequests.delete(requestId);
|
|
190
|
-
},
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Reset for new action
|
|
194
|
-
*/
|
|
195
|
-
reset() {
|
|
196
|
-
this.mutationCount = 0;
|
|
197
|
-
this.lastMutationTime = Date.now();
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// =============================================================================
|
|
203
|
-
// DUPLICATE ACTION FILTERING
|
|
204
|
-
// =============================================================================
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Create duplicate action filter
|
|
208
|
-
*/
|
|
209
|
-
function createDuplicateFilter(options = {}) {
|
|
210
|
-
const config = { ...DEFAULT_CONFIG.duplicates, ...options };
|
|
211
|
-
const recentActions = [];
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
/**
|
|
215
|
-
* Check if action is duplicate
|
|
216
|
-
*/
|
|
217
|
-
isDuplicate(action) {
|
|
218
|
-
const now = Date.now();
|
|
219
|
-
|
|
220
|
-
// Clean old entries
|
|
221
|
-
while (recentActions.length > 0 && now - recentActions[0].time > config.sameElementDebounceMs) {
|
|
222
|
-
recentActions.shift();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Check for duplicate
|
|
226
|
-
const isDupe = recentActions.some(recent => {
|
|
227
|
-
const timeDiff = now - recent.time;
|
|
228
|
-
|
|
229
|
-
// Same element within debounce window
|
|
230
|
-
if (recent.selector === action.selector && timeDiff < config.sameElementDebounceMs) {
|
|
231
|
-
return true;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Any click within short debounce
|
|
235
|
-
if (timeDiff < config.debounceMs) {
|
|
236
|
-
return true;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return false;
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// Record this action
|
|
243
|
-
recentActions.push({
|
|
244
|
-
selector: action.selector,
|
|
245
|
-
type: action.type,
|
|
246
|
-
time: now,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
return isDupe;
|
|
250
|
-
},
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Clear history
|
|
254
|
-
*/
|
|
255
|
-
clear() {
|
|
256
|
-
recentActions.length = 0;
|
|
257
|
-
},
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// =============================================================================
|
|
262
|
-
// TOAST FALSE-POSITIVE FILTERING
|
|
263
|
-
// =============================================================================
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Filter toast false positives
|
|
267
|
-
*/
|
|
268
|
-
function filterToastFalsePositives(toasts, options = {}) {
|
|
269
|
-
const config = { ...DEFAULT_CONFIG.toast, ...options };
|
|
270
|
-
|
|
271
|
-
return toasts.filter(toast => {
|
|
272
|
-
// Ignore if visible too long (persistent banner)
|
|
273
|
-
if (toast.visibleDuration && toast.visibleDuration > config.maxLifetimeMs) {
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Ignore if flash (too short to be intentional)
|
|
278
|
-
if (toast.visibleDuration && toast.visibleDuration < config.minVisibleMs) {
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Ignore common false positive patterns
|
|
283
|
-
const text = (toast.message || "").toLowerCase();
|
|
284
|
-
const falsePositivePatterns = [
|
|
285
|
-
/cookie.*consent/i,
|
|
286
|
-
/accept.*cookies/i,
|
|
287
|
-
/privacy.*policy/i,
|
|
288
|
-
/subscribe.*newsletter/i,
|
|
289
|
-
/sign.*up.*newsletter/i,
|
|
290
|
-
/promotional/i,
|
|
291
|
-
];
|
|
292
|
-
|
|
293
|
-
if (falsePositivePatterns.some(p => p.test(text))) {
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return true;
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// =============================================================================
|
|
302
|
-
// TEXT NOISE FILTERING
|
|
303
|
-
// =============================================================================
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Check if text is noise (timestamp, counter, etc.)
|
|
307
|
-
*/
|
|
308
|
-
function isTextNoise(text, options = {}) {
|
|
309
|
-
const patterns = options.textNoisePatterns || DEFAULT_CONFIG.textNoisePatterns;
|
|
310
|
-
|
|
311
|
-
const trimmed = (text || "").trim();
|
|
312
|
-
if (!trimmed) return true;
|
|
313
|
-
|
|
314
|
-
return patterns.some(pattern => pattern.test(trimmed));
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Filter noise from semantic diff
|
|
319
|
-
*/
|
|
320
|
-
function filterSemanticDiffNoise(diff, options = {}) {
|
|
321
|
-
const filtered = {
|
|
322
|
-
added: diff.added?.filter(item => !isTextNoise(extractTextFromDiffItem(item), options)) || [],
|
|
323
|
-
removed: diff.removed?.filter(item => !isTextNoise(extractTextFromDiffItem(item), options)) || [],
|
|
324
|
-
changed: diff.changed?.filter(item => !isTextNoise(extractTextFromDiffItem(item), options)) || [],
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
// Preserve original flags
|
|
328
|
-
filtered.routeChanged = diff.routeChanged;
|
|
329
|
-
filtered.dialogOpened = diff.dialogOpened;
|
|
330
|
-
filtered.dialogClosed = diff.dialogClosed;
|
|
331
|
-
filtered.errorsChanged = diff.errorsChanged;
|
|
332
|
-
filtered.errorDelta = diff.errorDelta;
|
|
333
|
-
|
|
334
|
-
return filtered;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function extractTextFromDiffItem(item) {
|
|
338
|
-
// "button: Save" -> "Save"
|
|
339
|
-
const match = item.match(/:\s*(.+)$/);
|
|
340
|
-
return match ? match[1] : item;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// =============================================================================
|
|
344
|
-
// SELECTOR IGNORE LIST
|
|
345
|
-
// =============================================================================
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Check if element should be ignored
|
|
349
|
-
*/
|
|
350
|
-
function shouldIgnoreElement(selector, options = {}) {
|
|
351
|
-
const ignoreSelectors = options.ignoreSelectors || DEFAULT_CONFIG.ignoreSelectors;
|
|
352
|
-
|
|
353
|
-
if (!selector) return false;
|
|
354
|
-
|
|
355
|
-
return ignoreSelectors.some(pattern => {
|
|
356
|
-
if (pattern.startsWith("[") || pattern.startsWith("#") || pattern.startsWith(".")) {
|
|
357
|
-
return selector.includes(pattern);
|
|
358
|
-
}
|
|
359
|
-
return selector === pattern;
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Build ignore selector for Playwright
|
|
365
|
-
*/
|
|
366
|
-
function buildIgnoreSelector(options = {}) {
|
|
367
|
-
const ignoreSelectors = options.ignoreSelectors || DEFAULT_CONFIG.ignoreSelectors;
|
|
368
|
-
return ignoreSelectors.map(s => `:not(${s})`).join("");
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// =============================================================================
|
|
372
|
-
// MEANINGFUL CHANGE VALIDATION
|
|
373
|
-
// =============================================================================
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Validate that a UI change is truly meaningful (with safety rails applied)
|
|
377
|
-
*/
|
|
378
|
-
function validateMeaningfulChange(uiChange, options = {}) {
|
|
379
|
-
const {
|
|
380
|
-
hydrationTracker = null,
|
|
381
|
-
meaningfulScoreThreshold = 0.6,
|
|
382
|
-
} = options;
|
|
383
|
-
|
|
384
|
-
// Check hydration suppression
|
|
385
|
-
if (hydrationTracker?.shouldSuppress(uiChange)) {
|
|
386
|
-
return {
|
|
387
|
-
valid: false,
|
|
388
|
-
suppressed: true,
|
|
389
|
-
reason: "hydration_window",
|
|
390
|
-
original: uiChange,
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Filter noise from diff
|
|
395
|
-
if (uiChange.semanticDiffSummary) {
|
|
396
|
-
uiChange.semanticDiffSummary = filterSemanticDiffNoise(uiChange.semanticDiffSummary, options);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Recalculate score after filtering
|
|
400
|
-
const filteredDiffSize = (uiChange.semanticDiffSummary?.added?.length || 0) +
|
|
401
|
-
(uiChange.semanticDiffSummary?.removed?.length || 0) +
|
|
402
|
-
(uiChange.semanticDiffSummary?.changed?.length || 0);
|
|
403
|
-
|
|
404
|
-
// If only minor changes remain after filtering, downgrade
|
|
405
|
-
if (!uiChange.routeChanged &&
|
|
406
|
-
!uiChange.dialogOpened &&
|
|
407
|
-
!uiChange.dialogClosed &&
|
|
408
|
-
!uiChange.reasons?.some(r => r.startsWith("toast_")) &&
|
|
409
|
-
filteredDiffSize === 0) {
|
|
410
|
-
return {
|
|
411
|
-
valid: false,
|
|
412
|
-
suppressed: true,
|
|
413
|
-
reason: "noise_only",
|
|
414
|
-
original: uiChange,
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
return {
|
|
419
|
-
valid: uiChange.meaningful || uiChange.score >= meaningfulScoreThreshold,
|
|
420
|
-
suppressed: false,
|
|
421
|
-
uiChange,
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// =============================================================================
|
|
426
|
-
// HELPERS
|
|
427
|
-
// =============================================================================
|
|
428
|
-
|
|
429
|
-
function sleep(ms) {
|
|
430
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// =============================================================================
|
|
434
|
-
// EXPORTS
|
|
435
|
-
// =============================================================================
|
|
436
|
-
|
|
437
|
-
module.exports = {
|
|
438
|
-
// Configuration
|
|
439
|
-
DEFAULT_CONFIG,
|
|
440
|
-
|
|
441
|
-
// Hydration
|
|
442
|
-
createHydrationTracker,
|
|
443
|
-
|
|
444
|
-
// Stabilization
|
|
445
|
-
createActionStabilizer,
|
|
446
|
-
|
|
447
|
-
// Duplicates
|
|
448
|
-
createDuplicateFilter,
|
|
449
|
-
|
|
450
|
-
// Toast filtering
|
|
451
|
-
filterToastFalsePositives,
|
|
452
|
-
|
|
453
|
-
// Text noise
|
|
454
|
-
isTextNoise,
|
|
455
|
-
filterSemanticDiffNoise,
|
|
456
|
-
|
|
457
|
-
// Selector ignore
|
|
458
|
-
shouldIgnoreElement,
|
|
459
|
-
buildIgnoreSelector,
|
|
460
|
-
|
|
461
|
-
// Validation
|
|
462
|
-
validateMeaningfulChange,
|
|
463
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Safety Rails v2
|
|
3
|
+
*
|
|
4
|
+
* False-positive prevention for UI change detection.
|
|
5
|
+
* Handles hydration suppression, action stabilization, duplicate filtering.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// CONFIGURATION
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
const DEFAULT_CONFIG = {
|
|
15
|
+
// Hydration suppression
|
|
16
|
+
hydration: {
|
|
17
|
+
suppressionWindowMs: 2000, // Ignore changes in first 2s after navigation
|
|
18
|
+
ignoreInitialRender: true,
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// Action stabilization
|
|
22
|
+
stabilization: {
|
|
23
|
+
quietMs: 300, // Wait for no DOM mutations
|
|
24
|
+
settleMs: 250, // Wait after action before snapshot
|
|
25
|
+
actionWindowMs: 8000, // Max wait for action effects
|
|
26
|
+
networkIdleMs: 500, // Wait for network idle
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// Duplicate filtering
|
|
30
|
+
duplicates: {
|
|
31
|
+
debounceMs: 100, // Ignore repeated clicks within 100ms
|
|
32
|
+
sameElementDebounceMs: 500, // Same element clicks
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Toast filtering
|
|
36
|
+
toast: {
|
|
37
|
+
maxLifetimeMs: 15000, // Ignore persistent banners
|
|
38
|
+
minVisibleMs: 100, // Ignore flash toasts
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Ignore selectors (user configurable)
|
|
42
|
+
ignoreSelectors: [
|
|
43
|
+
"[data-hydration]",
|
|
44
|
+
"[data-nextjs-scroll-focus-boundary]",
|
|
45
|
+
"nextjs-portal",
|
|
46
|
+
"#__next-build-watcher",
|
|
47
|
+
"#__next-prerender-indicator",
|
|
48
|
+
".dev-overlay",
|
|
49
|
+
"[data-hot-reload]",
|
|
50
|
+
],
|
|
51
|
+
|
|
52
|
+
// Text noise patterns (timestamps, counters)
|
|
53
|
+
textNoisePatterns: [
|
|
54
|
+
/^\d{1,2}:\d{2}(:\d{2})?\s*(AM|PM)?$/i, // 12:34, 12:34:56, 12:34 PM
|
|
55
|
+
/^\d+(\.\d+)?\s*(s|ms|sec|seconds?)$/i, // 3s, 100ms
|
|
56
|
+
/^(just now|now|ago|\d+\s*(min|hour|day)s?\s*ago)$/i,
|
|
57
|
+
/^\d+$/, // Pure numbers
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// HYDRATION SUPPRESSION
|
|
63
|
+
// =============================================================================
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create hydration tracker for a page
|
|
67
|
+
*/
|
|
68
|
+
function createHydrationTracker(options = {}) {
|
|
69
|
+
const config = { ...DEFAULT_CONFIG.hydration, ...options };
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
navigationTime: null,
|
|
73
|
+
isHydrating: false,
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Mark navigation start
|
|
77
|
+
*/
|
|
78
|
+
startNavigation() {
|
|
79
|
+
this.navigationTime = Date.now();
|
|
80
|
+
this.isHydrating = true;
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if still in hydration window
|
|
85
|
+
*/
|
|
86
|
+
inHydrationWindow() {
|
|
87
|
+
if (!this.navigationTime || !config.ignoreInitialRender) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const elapsed = Date.now() - this.navigationTime;
|
|
91
|
+
return elapsed < config.suppressionWindowMs;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Mark hydration complete (e.g., after first meaningful paint)
|
|
96
|
+
*/
|
|
97
|
+
markHydrationComplete() {
|
|
98
|
+
this.isHydrating = false;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Should suppress this change?
|
|
103
|
+
*/
|
|
104
|
+
shouldSuppress(change) {
|
|
105
|
+
if (!this.inHydrationWindow()) return false;
|
|
106
|
+
|
|
107
|
+
// During hydration, suppress semantic-only changes
|
|
108
|
+
// Still allow route changes and toasts
|
|
109
|
+
if (change.routeChanged) return false;
|
|
110
|
+
if (change.reasons?.includes("toast_success") || change.reasons?.includes("toast_error")) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return true;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// ACTION STABILIZATION
|
|
121
|
+
// =============================================================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create action stabilizer for DOM mutations
|
|
125
|
+
*/
|
|
126
|
+
function createActionStabilizer(page, options = {}) {
|
|
127
|
+
const config = { ...DEFAULT_CONFIG.stabilization, ...options };
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
mutationCount: 0,
|
|
131
|
+
lastMutationTime: 0,
|
|
132
|
+
pendingRequests: new Set(),
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Wait for DOM to be quiet before action
|
|
136
|
+
*/
|
|
137
|
+
async waitForQuiet() {
|
|
138
|
+
const startTime = Date.now();
|
|
139
|
+
|
|
140
|
+
while (Date.now() - startTime < config.quietMs * 3) {
|
|
141
|
+
// Check if DOM has been quiet
|
|
142
|
+
const timeSinceLastMutation = Date.now() - this.lastMutationTime;
|
|
143
|
+
const hasPendingRequests = this.pendingRequests.size > 0;
|
|
144
|
+
|
|
145
|
+
if (timeSinceLastMutation >= config.quietMs && !hasPendingRequests) {
|
|
146
|
+
return { stable: true, waitedMs: Date.now() - startTime };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await sleep(50);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { stable: false, waitedMs: Date.now() - startTime };
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Wait for effects after action
|
|
157
|
+
*/
|
|
158
|
+
async waitForSettle() {
|
|
159
|
+
await sleep(config.settleMs);
|
|
160
|
+
|
|
161
|
+
// Additional wait if network is active
|
|
162
|
+
if (this.pendingRequests.size > 0) {
|
|
163
|
+
const networkStart = Date.now();
|
|
164
|
+
while (this.pendingRequests.size > 0 && Date.now() - networkStart < config.networkIdleMs) {
|
|
165
|
+
await sleep(50);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Record mutation
|
|
172
|
+
*/
|
|
173
|
+
recordMutation() {
|
|
174
|
+
this.mutationCount++;
|
|
175
|
+
this.lastMutationTime = Date.now();
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Track request start
|
|
180
|
+
*/
|
|
181
|
+
trackRequestStart(requestId) {
|
|
182
|
+
this.pendingRequests.add(requestId);
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Track request end
|
|
187
|
+
*/
|
|
188
|
+
trackRequestEnd(requestId) {
|
|
189
|
+
this.pendingRequests.delete(requestId);
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reset for new action
|
|
194
|
+
*/
|
|
195
|
+
reset() {
|
|
196
|
+
this.mutationCount = 0;
|
|
197
|
+
this.lastMutationTime = Date.now();
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// =============================================================================
|
|
203
|
+
// DUPLICATE ACTION FILTERING
|
|
204
|
+
// =============================================================================
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Create duplicate action filter
|
|
208
|
+
*/
|
|
209
|
+
function createDuplicateFilter(options = {}) {
|
|
210
|
+
const config = { ...DEFAULT_CONFIG.duplicates, ...options };
|
|
211
|
+
const recentActions = [];
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
/**
|
|
215
|
+
* Check if action is duplicate
|
|
216
|
+
*/
|
|
217
|
+
isDuplicate(action) {
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
|
|
220
|
+
// Clean old entries
|
|
221
|
+
while (recentActions.length > 0 && now - recentActions[0].time > config.sameElementDebounceMs) {
|
|
222
|
+
recentActions.shift();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check for duplicate
|
|
226
|
+
const isDupe = recentActions.some(recent => {
|
|
227
|
+
const timeDiff = now - recent.time;
|
|
228
|
+
|
|
229
|
+
// Same element within debounce window
|
|
230
|
+
if (recent.selector === action.selector && timeDiff < config.sameElementDebounceMs) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Any click within short debounce
|
|
235
|
+
if (timeDiff < config.debounceMs) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return false;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Record this action
|
|
243
|
+
recentActions.push({
|
|
244
|
+
selector: action.selector,
|
|
245
|
+
type: action.type,
|
|
246
|
+
time: now,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return isDupe;
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Clear history
|
|
254
|
+
*/
|
|
255
|
+
clear() {
|
|
256
|
+
recentActions.length = 0;
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// =============================================================================
|
|
262
|
+
// TOAST FALSE-POSITIVE FILTERING
|
|
263
|
+
// =============================================================================
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Filter toast false positives
|
|
267
|
+
*/
|
|
268
|
+
function filterToastFalsePositives(toasts, options = {}) {
|
|
269
|
+
const config = { ...DEFAULT_CONFIG.toast, ...options };
|
|
270
|
+
|
|
271
|
+
return toasts.filter(toast => {
|
|
272
|
+
// Ignore if visible too long (persistent banner)
|
|
273
|
+
if (toast.visibleDuration && toast.visibleDuration > config.maxLifetimeMs) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Ignore if flash (too short to be intentional)
|
|
278
|
+
if (toast.visibleDuration && toast.visibleDuration < config.minVisibleMs) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Ignore common false positive patterns
|
|
283
|
+
const text = (toast.message || "").toLowerCase();
|
|
284
|
+
const falsePositivePatterns = [
|
|
285
|
+
/cookie.*consent/i,
|
|
286
|
+
/accept.*cookies/i,
|
|
287
|
+
/privacy.*policy/i,
|
|
288
|
+
/subscribe.*newsletter/i,
|
|
289
|
+
/sign.*up.*newsletter/i,
|
|
290
|
+
/promotional/i,
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
if (falsePositivePatterns.some(p => p.test(text))) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return true;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// =============================================================================
|
|
302
|
+
// TEXT NOISE FILTERING
|
|
303
|
+
// =============================================================================
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Check if text is noise (timestamp, counter, etc.)
|
|
307
|
+
*/
|
|
308
|
+
function isTextNoise(text, options = {}) {
|
|
309
|
+
const patterns = options.textNoisePatterns || DEFAULT_CONFIG.textNoisePatterns;
|
|
310
|
+
|
|
311
|
+
const trimmed = (text || "").trim();
|
|
312
|
+
if (!trimmed) return true;
|
|
313
|
+
|
|
314
|
+
return patterns.some(pattern => pattern.test(trimmed));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Filter noise from semantic diff
|
|
319
|
+
*/
|
|
320
|
+
function filterSemanticDiffNoise(diff, options = {}) {
|
|
321
|
+
const filtered = {
|
|
322
|
+
added: diff.added?.filter(item => !isTextNoise(extractTextFromDiffItem(item), options)) || [],
|
|
323
|
+
removed: diff.removed?.filter(item => !isTextNoise(extractTextFromDiffItem(item), options)) || [],
|
|
324
|
+
changed: diff.changed?.filter(item => !isTextNoise(extractTextFromDiffItem(item), options)) || [],
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Preserve original flags
|
|
328
|
+
filtered.routeChanged = diff.routeChanged;
|
|
329
|
+
filtered.dialogOpened = diff.dialogOpened;
|
|
330
|
+
filtered.dialogClosed = diff.dialogClosed;
|
|
331
|
+
filtered.errorsChanged = diff.errorsChanged;
|
|
332
|
+
filtered.errorDelta = diff.errorDelta;
|
|
333
|
+
|
|
334
|
+
return filtered;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function extractTextFromDiffItem(item) {
|
|
338
|
+
// "button: Save" -> "Save"
|
|
339
|
+
const match = item.match(/:\s*(.+)$/);
|
|
340
|
+
return match ? match[1] : item;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// =============================================================================
|
|
344
|
+
// SELECTOR IGNORE LIST
|
|
345
|
+
// =============================================================================
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Check if element should be ignored
|
|
349
|
+
*/
|
|
350
|
+
function shouldIgnoreElement(selector, options = {}) {
|
|
351
|
+
const ignoreSelectors = options.ignoreSelectors || DEFAULT_CONFIG.ignoreSelectors;
|
|
352
|
+
|
|
353
|
+
if (!selector) return false;
|
|
354
|
+
|
|
355
|
+
return ignoreSelectors.some(pattern => {
|
|
356
|
+
if (pattern.startsWith("[") || pattern.startsWith("#") || pattern.startsWith(".")) {
|
|
357
|
+
return selector.includes(pattern);
|
|
358
|
+
}
|
|
359
|
+
return selector === pattern;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Build ignore selector for Playwright
|
|
365
|
+
*/
|
|
366
|
+
function buildIgnoreSelector(options = {}) {
|
|
367
|
+
const ignoreSelectors = options.ignoreSelectors || DEFAULT_CONFIG.ignoreSelectors;
|
|
368
|
+
return ignoreSelectors.map(s => `:not(${s})`).join("");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// =============================================================================
|
|
372
|
+
// MEANINGFUL CHANGE VALIDATION
|
|
373
|
+
// =============================================================================
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Validate that a UI change is truly meaningful (with safety rails applied)
|
|
377
|
+
*/
|
|
378
|
+
function validateMeaningfulChange(uiChange, options = {}) {
|
|
379
|
+
const {
|
|
380
|
+
hydrationTracker = null,
|
|
381
|
+
meaningfulScoreThreshold = 0.6,
|
|
382
|
+
} = options;
|
|
383
|
+
|
|
384
|
+
// Check hydration suppression
|
|
385
|
+
if (hydrationTracker?.shouldSuppress(uiChange)) {
|
|
386
|
+
return {
|
|
387
|
+
valid: false,
|
|
388
|
+
suppressed: true,
|
|
389
|
+
reason: "hydration_window",
|
|
390
|
+
original: uiChange,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Filter noise from diff
|
|
395
|
+
if (uiChange.semanticDiffSummary) {
|
|
396
|
+
uiChange.semanticDiffSummary = filterSemanticDiffNoise(uiChange.semanticDiffSummary, options);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Recalculate score after filtering
|
|
400
|
+
const filteredDiffSize = (uiChange.semanticDiffSummary?.added?.length || 0) +
|
|
401
|
+
(uiChange.semanticDiffSummary?.removed?.length || 0) +
|
|
402
|
+
(uiChange.semanticDiffSummary?.changed?.length || 0);
|
|
403
|
+
|
|
404
|
+
// If only minor changes remain after filtering, downgrade
|
|
405
|
+
if (!uiChange.routeChanged &&
|
|
406
|
+
!uiChange.dialogOpened &&
|
|
407
|
+
!uiChange.dialogClosed &&
|
|
408
|
+
!uiChange.reasons?.some(r => r.startsWith("toast_")) &&
|
|
409
|
+
filteredDiffSize === 0) {
|
|
410
|
+
return {
|
|
411
|
+
valid: false,
|
|
412
|
+
suppressed: true,
|
|
413
|
+
reason: "noise_only",
|
|
414
|
+
original: uiChange,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
valid: uiChange.meaningful || uiChange.score >= meaningfulScoreThreshold,
|
|
420
|
+
suppressed: false,
|
|
421
|
+
uiChange,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// =============================================================================
|
|
426
|
+
// HELPERS
|
|
427
|
+
// =============================================================================
|
|
428
|
+
|
|
429
|
+
function sleep(ms) {
|
|
430
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// =============================================================================
|
|
434
|
+
// EXPORTS
|
|
435
|
+
// =============================================================================
|
|
436
|
+
|
|
437
|
+
module.exports = {
|
|
438
|
+
// Configuration
|
|
439
|
+
DEFAULT_CONFIG,
|
|
440
|
+
|
|
441
|
+
// Hydration
|
|
442
|
+
createHydrationTracker,
|
|
443
|
+
|
|
444
|
+
// Stabilization
|
|
445
|
+
createActionStabilizer,
|
|
446
|
+
|
|
447
|
+
// Duplicates
|
|
448
|
+
createDuplicateFilter,
|
|
449
|
+
|
|
450
|
+
// Toast filtering
|
|
451
|
+
filterToastFalsePositives,
|
|
452
|
+
|
|
453
|
+
// Text noise
|
|
454
|
+
isTextNoise,
|
|
455
|
+
filterSemanticDiffNoise,
|
|
456
|
+
|
|
457
|
+
// Selector ignore
|
|
458
|
+
shouldIgnoreElement,
|
|
459
|
+
buildIgnoreSelector,
|
|
460
|
+
|
|
461
|
+
// Validation
|
|
462
|
+
validateMeaningfulChange,
|
|
463
|
+
};
|