@odavl/guardian 2.0.0 → 2.0.1
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/CHANGELOG.md +210 -210
- package/LICENSE +21 -21
- package/README.md +297 -184
- package/bin/guardian.js +2242 -2221
- package/config/README.md +59 -59
- package/config/guardian.config.json +54 -54
- package/config/guardian.policy.json +12 -12
- package/config/profiles/docs.yaml +18 -18
- package/config/profiles/ecommerce.yaml +17 -17
- package/config/profiles/landing-demo.yaml +16 -16
- package/config/profiles/marketing.yaml +18 -18
- package/config/profiles/saas.yaml +21 -21
- package/flows/example-login-flow.json +36 -36
- package/flows/example-signup-flow.json +44 -44
- package/package.json +124 -116
- package/policies/enterprise.json +12 -12
- package/policies/landing-demo.json +22 -22
- package/policies/saas.json +12 -12
- package/policies/startup.json +12 -12
- package/src/enterprise/audit-logger.js +166 -166
- package/src/enterprise/pdf-exporter.js +267 -267
- package/src/enterprise/rbac-gate.js +142 -142
- package/src/enterprise/rbac.js +239 -239
- package/src/enterprise/site-manager.js +180 -180
- package/src/founder/feedback-system.js +156 -156
- package/src/founder/founder-tracker.js +213 -213
- package/src/founder/usage-signals.js +141 -141
- package/src/guardian/action-hints.js +439 -439
- package/src/guardian/alert-ledger.js +121 -121
- package/src/guardian/artifact-sanitizer.js +56 -56
- package/src/guardian/attempt-engine.js +1069 -1029
- package/src/guardian/attempt-registry.js +267 -267
- package/src/guardian/attempt-relevance.js +106 -106
- package/src/guardian/attempt-reporter.js +513 -507
- package/src/guardian/attempt.js +274 -273
- package/src/guardian/attempts-filter.js +63 -63
- package/src/guardian/auto-attempt-builder.js +283 -283
- package/src/guardian/baseline-registry.js +177 -177
- package/src/guardian/baseline-reporter.js +143 -143
- package/src/guardian/baseline-storage.js +285 -285
- package/src/guardian/baseline.js +535 -534
- package/src/guardian/behavioral-signals.js +261 -261
- package/src/guardian/breakage-intelligence.js +224 -224
- package/src/guardian/browser-pool.js +131 -131
- package/src/guardian/browser.js +119 -119
- package/src/guardian/canonical-truth.js +308 -308
- package/src/guardian/ci-cli.js +121 -121
- package/src/guardian/ci-gate.js +96 -96
- package/src/guardian/ci-mode.js +15 -15
- package/src/guardian/ci-output.js +55 -38
- package/src/guardian/cli-summary.js +102 -102
- package/src/guardian/confidence-signals.js +251 -251
- package/src/guardian/config-loader.js +161 -161
- package/src/guardian/config-validator.js +285 -283
- package/src/guardian/coverage-model.js +239 -239
- package/src/guardian/coverage-packs.js +58 -58
- package/src/guardian/crawler.js +142 -142
- package/src/guardian/data-guardian-detector.js +189 -189
- package/src/guardian/decision-authority.js +746 -725
- package/src/guardian/detection-layers.js +271 -271
- package/src/guardian/determinism.js +146 -146
- package/src/guardian/discovery-engine.js +661 -661
- package/src/guardian/drift-detector.js +100 -100
- package/src/guardian/enhanced-html-reporter.js +522 -522
- package/src/guardian/env-guard.js +128 -127
- package/src/guardian/error-clarity.js +399 -399
- package/src/guardian/export-contract.js +196 -196
- package/src/guardian/fail-safe.js +212 -212
- package/src/guardian/failure-intelligence.js +173 -173
- package/src/guardian/failure-taxonomy.js +169 -169
- package/src/guardian/final-outcome.js +206 -206
- package/src/guardian/first-run-profile.js +89 -89
- package/src/guardian/first-run.js +65 -67
- package/src/guardian/flag-validator.js +111 -111
- package/src/guardian/flow-executor.js +641 -639
- package/src/guardian/flow-registry.js +67 -67
- package/src/guardian/honesty.js +394 -394
- package/src/guardian/html-reporter.js +416 -416
- package/src/guardian/human-intent-resolver.js +296 -296
- package/src/guardian/human-interaction-model.js +351 -351
- package/src/guardian/human-journey-context.js +184 -184
- package/src/guardian/human-navigator.js +544 -544
- package/src/guardian/human-reporter.js +435 -431
- package/src/guardian/index.js +226 -221
- package/src/guardian/init-command.js +143 -143
- package/src/guardian/intent-detector.js +148 -146
- package/src/guardian/journey-definitions.js +132 -132
- package/src/guardian/journey-scan-cli.js +142 -145
- package/src/guardian/journey-scanner.js +583 -583
- package/src/guardian/junit-reporter.js +281 -281
- package/src/guardian/language-detection.js +99 -99
- package/src/guardian/live-alert.js +56 -56
- package/src/guardian/live-baseline-compare.js +146 -146
- package/src/guardian/live-cli.js +95 -95
- package/src/guardian/live-guardian.js +210 -210
- package/src/guardian/live-scheduler-runner.js +137 -137
- package/src/guardian/live-scheduler-state.js +167 -168
- package/src/guardian/live-scheduler.js +146 -146
- package/src/guardian/live-state.js +110 -110
- package/src/guardian/market-criticality.js +335 -335
- package/src/guardian/market-reporter.js +577 -577
- package/src/guardian/network-trace.js +178 -178
- package/src/guardian/obs-logger.js +110 -110
- package/src/guardian/observed-capabilities.js +427 -427
- package/src/guardian/output-contract.js +154 -0
- package/src/guardian/output-readability.js +264 -264
- package/src/guardian/parallel-executor.js +116 -116
- package/src/guardian/path-safety.js +56 -56
- package/src/guardian/pattern-analyzer.js +348 -348
- package/src/guardian/policy.js +432 -434
- package/src/guardian/prelaunch-gate.js +193 -193
- package/src/guardian/prerequisite-checker.js +101 -101
- package/src/guardian/preset-loader.js +152 -157
- package/src/guardian/profile-loader.js +96 -96
- package/src/guardian/reality.js +3025 -2826
- package/src/guardian/realworld-scenarios.js +94 -94
- package/src/guardian/reporter.js +167 -167
- package/src/guardian/retry-policy.js +123 -123
- package/src/guardian/root-cause-analysis.js +171 -171
- package/src/guardian/rules-engine.js +558 -558
- package/src/guardian/run-artifacts.js +212 -212
- package/src/guardian/run-cleanup.js +207 -207
- package/src/guardian/run-export.js +522 -522
- package/src/guardian/run-latest.js +90 -90
- package/src/guardian/run-list.js +211 -211
- package/src/guardian/run-summary.js +20 -20
- package/src/guardian/runtime-root.js +246 -246
- package/src/guardian/safety.js +248 -248
- package/src/guardian/scan-presets.js +133 -149
- package/src/guardian/screenshot.js +152 -152
- package/src/guardian/secret-hygiene.js +44 -44
- package/src/guardian/selector-fallbacks.js +394 -394
- package/src/guardian/semantic-contact-detection.js +255 -255
- package/src/guardian/semantic-contact-finder.js +201 -201
- package/src/guardian/semantic-targets.js +234 -234
- package/src/guardian/site-intelligence.js +588 -588
- package/src/guardian/site-introspection.js +257 -257
- package/src/guardian/sitemap.js +225 -225
- package/src/guardian/smoke.js +283 -258
- package/src/guardian/snapshot-schema.js +177 -290
- package/src/guardian/snapshot.js +430 -397
- package/src/guardian/stability-scorer.js +169 -169
- package/src/guardian/success-evaluator.js +214 -214
- package/src/guardian/template-command.js +184 -184
- package/src/guardian/text-formatters.js +426 -426
- package/src/guardian/timeout-profiles.js +57 -57
- package/src/guardian/truth/attempt.contract.js +158 -0
- package/src/guardian/truth/decision.contract.js +275 -0
- package/src/guardian/truth/snapshot.contract.js +363 -0
- package/src/guardian/validators.js +323 -323
- package/src/guardian/verdict-card.js +474 -474
- package/src/guardian/verdict-clarity.js +298 -298
- package/src/guardian/verdict-policy.js +363 -363
- package/src/guardian/verdict.js +333 -333
- package/src/guardian/verdicts.js +79 -74
- package/src/guardian/visual-diff.js +247 -247
- package/src/guardian/wait-for-outcome.js +119 -119
- package/src/guardian/watch-runner.js +181 -181
- package/src/guardian/watchdog-diff.js +167 -167
- package/src/guardian/webhook.js +206 -206
- package/src/payments/stripe-checkout.js +169 -169
- package/src/plans/plan-definitions.js +148 -148
- package/src/plans/plan-manager.js +211 -211
- package/src/plans/usage-tracker.js +210 -210
- package/src/recipes/recipe-engine.js +188 -188
- package/src/recipes/recipe-failure-analysis.js +159 -159
- package/src/recipes/recipe-registry.js +134 -134
- package/src/recipes/recipe-runtime.js +507 -507
- package/src/recipes/recipe-store.js +410 -410
- package/SECURITY.md +0 -77
- package/VERSIONING.md +0 -100
- package/guardian-contract-v1.md +0 -502
|
@@ -1,1029 +1,1069 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Guardian Attempt Engine - PHASE 1 + PHASE 2
|
|
3
|
-
* Executes a single user attempt and tracks outcome (SUCCESS, FAILURE, FRICTION, NOT_APPLICABLE, DISCOVERY_FAILED)
|
|
4
|
-
* Phase 2: Soft failure detection via validators
|
|
5
|
-
* Phase 3: Robust selector discovery with fallbacks
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
break;
|
|
480
|
-
} catch
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
})
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Guardian Attempt Engine - PHASE 1 + PHASE 2
|
|
3
|
+
* Executes a single user attempt and tracks outcome (SUCCESS, FAILURE, FRICTION, NOT_APPLICABLE, DISCOVERY_FAILED)
|
|
4
|
+
* Phase 2: Soft failure detection via validators
|
|
5
|
+
* Phase 3: Robust selector discovery with fallbacks
|
|
6
|
+
*
|
|
7
|
+
* @typedef {import('./truth/attempt.contract.js').AttemptResult} AttemptResult
|
|
8
|
+
* @typedef {import('./truth/attempt.contract.js').AttemptStep} AttemptStep
|
|
9
|
+
* @typedef {import('./truth/attempt.contract.js').FrictionAnalysis} FrictionAnalysis
|
|
10
|
+
* @typedef {import('./truth/attempt.contract.js').AttemptOutcome} AttemptOutcome
|
|
11
|
+
* @typedef {import('./truth/attempt.contract.js').StepStatus} StepStatus
|
|
12
|
+
* @typedef {import('./truth/attempt.contract.js').FrictionSeverity} FrictionSeverity
|
|
13
|
+
* @typedef {import('./truth/attempt.contract.js').FrictionMetrics} FrictionMetrics
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { getAttemptDefinition } = require('./attempt-registry');
|
|
19
|
+
const { runValidators, analyzeSoftFailures } = require('./validators');
|
|
20
|
+
const { buildSelectorChain, findElement, detectFeature } = require('./selector-fallbacks');
|
|
21
|
+
|
|
22
|
+
class AttemptEngine {
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.attemptId = options.attemptId || 'default';
|
|
25
|
+
this.timeout = options.timeout || 30000;
|
|
26
|
+
this.frictionThresholds = options.frictionThresholds || {
|
|
27
|
+
totalDurationMs: 2500, // Total attempt > 2.5s
|
|
28
|
+
stepDurationMs: 1500, // Any single step > 1.5s
|
|
29
|
+
retryCount: 1 // More than 1 retry = friction
|
|
30
|
+
};
|
|
31
|
+
this.maxStepRetries = typeof options.maxStepRetries === 'number'
|
|
32
|
+
? Math.max(1, options.maxStepRetries)
|
|
33
|
+
: 2;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load attempt definition by ID (Phase 3 registry)
|
|
38
|
+
*/
|
|
39
|
+
loadAttemptDefinition(attemptId) {
|
|
40
|
+
return getAttemptDefinition(attemptId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Execute a single attempt
|
|
45
|
+
* @param {Object} page - Playwright page object
|
|
46
|
+
* @param {string} attemptId - Attempt identifier
|
|
47
|
+
* @param {string} baseUrl - Base URL for attempt
|
|
48
|
+
* @param {string|null} artifactsDir - Artifacts directory path
|
|
49
|
+
* @param {Array|null} validatorSpecs - Validator specifications
|
|
50
|
+
* @returns {Promise<AttemptResult>} Attempt execution result
|
|
51
|
+
*/
|
|
52
|
+
async executeAttempt(page, attemptId, baseUrl, artifactsDir = null, validatorSpecs = null) {
|
|
53
|
+
const attemptDef = this.loadAttemptDefinition(attemptId);
|
|
54
|
+
if (!attemptDef) {
|
|
55
|
+
throw new Error(`Attempt ${attemptId} not found`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const startedAt = new Date();
|
|
59
|
+
const steps = [];
|
|
60
|
+
const frictionSignals = [];
|
|
61
|
+
const consoleMessages = []; // Capture console messages for validators
|
|
62
|
+
const consoleErrors = [];
|
|
63
|
+
const pageErrors = [];
|
|
64
|
+
let currentStep = null;
|
|
65
|
+
let lastError = null;
|
|
66
|
+
const frictionReasons = [];
|
|
67
|
+
let frictionMetrics = {};
|
|
68
|
+
|
|
69
|
+
// Capture console messages for soft failure detection
|
|
70
|
+
const consoleHandler = (msg) => {
|
|
71
|
+
consoleMessages.push({
|
|
72
|
+
type: msg.type(), // 'log', 'error', 'warning', etc.
|
|
73
|
+
text: msg.text(),
|
|
74
|
+
location: msg.location()
|
|
75
|
+
});
|
|
76
|
+
if (msg.type() === 'error') {
|
|
77
|
+
consoleErrors.push(msg.text());
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
page.on('console', consoleHandler);
|
|
82
|
+
const pageErrorHandler = (err) => {
|
|
83
|
+
pageErrors.push(err.message || 'page error');
|
|
84
|
+
};
|
|
85
|
+
page.on('pageerror', pageErrorHandler);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Custom universal attempts bypass base step execution and implement purpose-built logic
|
|
89
|
+
if (attemptId === 'site_smoke') {
|
|
90
|
+
return await this._runSiteSmokeAttempt(page, baseUrl, artifactsDir, consoleErrors, pageErrors);
|
|
91
|
+
}
|
|
92
|
+
if (attemptId === 'primary_ctas') {
|
|
93
|
+
return await this._runPrimaryCtasAttempt(page, baseUrl, artifactsDir, consoleErrors, pageErrors);
|
|
94
|
+
}
|
|
95
|
+
if (attemptId === 'contact_discovery_v2') {
|
|
96
|
+
return await this._runContactDiscoveryAttempt(page, baseUrl, artifactsDir, consoleErrors, pageErrors);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Replace $BASEURL placeholder in all steps
|
|
100
|
+
const processedSteps = attemptDef.baseSteps.map(step => {
|
|
101
|
+
if (step.target && step.target === '$BASEURL') {
|
|
102
|
+
return { ...step, target: baseUrl };
|
|
103
|
+
}
|
|
104
|
+
return step;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Execute each step
|
|
108
|
+
for (const stepDef of processedSteps) {
|
|
109
|
+
currentStep = {
|
|
110
|
+
id: stepDef.id,
|
|
111
|
+
type: stepDef.type,
|
|
112
|
+
target: stepDef.target,
|
|
113
|
+
description: stepDef.description,
|
|
114
|
+
startedAt: new Date().toISOString(),
|
|
115
|
+
retries: 0,
|
|
116
|
+
status: /** @type {StepStatus} */ ('pending'),
|
|
117
|
+
error: null,
|
|
118
|
+
screenshots: []
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const stepStartTime = Date.now();
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
// Execute with retry logic (up to 2 attempts)
|
|
125
|
+
const success = false;
|
|
126
|
+
for (let attempt = 0; attempt < this.maxStepRetries; attempt++) {
|
|
127
|
+
try {
|
|
128
|
+
if (attempt > 0) {
|
|
129
|
+
currentStep.retries++;
|
|
130
|
+
// Small backoff before retry
|
|
131
|
+
await page.waitForTimeout(200);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await this._executeStep(page, stepDef);
|
|
135
|
+
break;
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (attempt === this.maxStepRetries - 1) {
|
|
138
|
+
throw err; // Final attempt failed
|
|
139
|
+
}
|
|
140
|
+
// Retry on first failure
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const stepEndTime = Date.now();
|
|
145
|
+
const stepDurationMs = stepEndTime - stepStartTime;
|
|
146
|
+
|
|
147
|
+
currentStep.endedAt = new Date().toISOString();
|
|
148
|
+
currentStep.durationMs = stepDurationMs;
|
|
149
|
+
currentStep.status = /** @type {StepStatus} */ ('success');
|
|
150
|
+
|
|
151
|
+
// Check for friction signals in step timing
|
|
152
|
+
if (stepDurationMs > this.frictionThresholds.stepDurationMs) {
|
|
153
|
+
frictionSignals.push({
|
|
154
|
+
id: 'slow_step_execution',
|
|
155
|
+
description: `Step took longer than threshold`,
|
|
156
|
+
metric: 'stepDurationMs',
|
|
157
|
+
threshold: this.frictionThresholds.stepDurationMs,
|
|
158
|
+
observedValue: stepDurationMs,
|
|
159
|
+
affectedStepId: stepDef.id,
|
|
160
|
+
severity: /** @type {FrictionSeverity} */ ('medium')
|
|
161
|
+
});
|
|
162
|
+
frictionReasons.push(`Step "${stepDef.id}" took ${stepDurationMs}ms (threshold: ${this.frictionThresholds.stepDurationMs}ms)`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (currentStep.retries > this.frictionThresholds.retryCount) {
|
|
166
|
+
frictionSignals.push({
|
|
167
|
+
id: 'multiple_retries_required',
|
|
168
|
+
description: `Step required multiple retry attempts`,
|
|
169
|
+
metric: 'retryCount',
|
|
170
|
+
threshold: this.frictionThresholds.retryCount,
|
|
171
|
+
observedValue: currentStep.retries,
|
|
172
|
+
affectedStepId: stepDef.id,
|
|
173
|
+
severity: /** @type {FrictionSeverity} */ ('high')
|
|
174
|
+
});
|
|
175
|
+
frictionReasons.push(`Step "${stepDef.id}" required ${currentStep.retries} retries`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Capture screenshot on success if artifacts dir provided
|
|
179
|
+
if (artifactsDir) {
|
|
180
|
+
const screenshotPath = await this._captureScreenshot(
|
|
181
|
+
page,
|
|
182
|
+
artifactsDir,
|
|
183
|
+
stepDef.id
|
|
184
|
+
);
|
|
185
|
+
if (screenshotPath) {
|
|
186
|
+
currentStep.screenshots.push(screenshotPath);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
} catch (err) {
|
|
191
|
+
currentStep.endedAt = new Date().toISOString();
|
|
192
|
+
currentStep.durationMs = Date.now() - stepStartTime;
|
|
193
|
+
currentStep.status = /** @type {StepStatus} */ (stepDef.optional ? 'optional_failed' : 'failed');
|
|
194
|
+
currentStep.error = err.message;
|
|
195
|
+
|
|
196
|
+
if (stepDef.optional) {
|
|
197
|
+
// Optional steps should not fail the attempt; record soft failure
|
|
198
|
+
frictionSignals.push({
|
|
199
|
+
id: 'optional_step_failed',
|
|
200
|
+
description: `Optional step failed: ${stepDef.id}`,
|
|
201
|
+
metric: 'optionalStep',
|
|
202
|
+
threshold: 0,
|
|
203
|
+
observedValue: 1,
|
|
204
|
+
affectedStepId: stepDef.id,
|
|
205
|
+
severity: /** @type {FrictionSeverity} */ ('low')
|
|
206
|
+
});
|
|
207
|
+
frictionReasons.push(`Optional step failed and was skipped: ${stepDef.id}`);
|
|
208
|
+
if (artifactsDir) {
|
|
209
|
+
const screenshotPath = await this._captureScreenshot(
|
|
210
|
+
page,
|
|
211
|
+
artifactsDir,
|
|
212
|
+
`${stepDef.id}_optional_failure`
|
|
213
|
+
);
|
|
214
|
+
if (screenshotPath) {
|
|
215
|
+
currentStep.screenshots.push(screenshotPath);
|
|
216
|
+
}
|
|
217
|
+
const domPath = await this._savePageContent(page, artifactsDir, `${stepDef.id}_optional_failure`);
|
|
218
|
+
if (domPath) {
|
|
219
|
+
currentStep.domPath = domPath;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
steps.push(currentStep);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// eslint-disable-next-line no-unused-vars
|
|
227
|
+
lastError = err;
|
|
228
|
+
|
|
229
|
+
// Capture screenshot and DOM on failure
|
|
230
|
+
if (artifactsDir) {
|
|
231
|
+
const screenshotPath = await this._captureScreenshot(
|
|
232
|
+
page,
|
|
233
|
+
artifactsDir,
|
|
234
|
+
`${stepDef.id}_failure`
|
|
235
|
+
);
|
|
236
|
+
if (screenshotPath) {
|
|
237
|
+
currentStep.screenshots.push(screenshotPath);
|
|
238
|
+
}
|
|
239
|
+
const domPath = await this._savePageContent(page, artifactsDir, `${stepDef.id}_failure`);
|
|
240
|
+
if (domPath) {
|
|
241
|
+
currentStep.domPath = domPath;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
throw err; // Stop attempt on step failure
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
steps.push(currentStep);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// All steps successful, now check success conditions
|
|
252
|
+
const endedAt = new Date();
|
|
253
|
+
const totalDurationMs = endedAt.getTime() - startedAt.getTime();
|
|
254
|
+
|
|
255
|
+
// Check success conditions
|
|
256
|
+
let successMet = false;
|
|
257
|
+
let successReason = null;
|
|
258
|
+
|
|
259
|
+
for (const condition of attemptDef.successConditions) {
|
|
260
|
+
try {
|
|
261
|
+
if (condition.type === 'url') {
|
|
262
|
+
const currentUrl = page.url();
|
|
263
|
+
if (condition.pattern.test(currentUrl)) {
|
|
264
|
+
successMet = true;
|
|
265
|
+
successReason = `URL matched: ${currentUrl}`;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
} else if (condition.type === 'selector') {
|
|
269
|
+
// Wait briefly for selector to become visible
|
|
270
|
+
try {
|
|
271
|
+
await page.waitForSelector(condition.target, { timeout: 3000, state: 'visible' });
|
|
272
|
+
successMet = true;
|
|
273
|
+
successReason = `Success element visible: ${condition.target}`;
|
|
274
|
+
break;
|
|
275
|
+
} catch {
|
|
276
|
+
// Continue to next condition
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
// Continue to next condition
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!successMet) {
|
|
285
|
+
page.removeListener('console', consoleHandler);
|
|
286
|
+
page.removeListener('pageerror', pageErrorHandler);
|
|
287
|
+
return {
|
|
288
|
+
outcome: /** @type {AttemptOutcome} */ ('FAILURE'),
|
|
289
|
+
steps,
|
|
290
|
+
startedAt: startedAt.toISOString(),
|
|
291
|
+
endedAt: endedAt.toISOString(),
|
|
292
|
+
totalDurationMs,
|
|
293
|
+
friction: {
|
|
294
|
+
isFriction: false,
|
|
295
|
+
signals: [],
|
|
296
|
+
summary: null,
|
|
297
|
+
reasons: [],
|
|
298
|
+
thresholds: this.frictionThresholds,
|
|
299
|
+
metrics: {
|
|
300
|
+
totalDurationMs,
|
|
301
|
+
stepCount: steps.length,
|
|
302
|
+
totalRetries: steps.reduce((sum, s) => sum + s.retries, 0),
|
|
303
|
+
maxStepDurationMs: Math.max(...steps.map(s => s.durationMs || 0), 0)
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
error: 'Success conditions not met after all steps completed',
|
|
307
|
+
successReason: null,
|
|
308
|
+
validators: [],
|
|
309
|
+
softFailures: { hasSoftFailure: false, failureCount: 0, warnCount: 0 },
|
|
310
|
+
discoverySignals: {
|
|
311
|
+
consoleErrorCount: consoleErrors.length,
|
|
312
|
+
pageErrorCount: pageErrors.length
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Run validators for soft failure detection (Phase 2)
|
|
318
|
+
let validatorResults = [];
|
|
319
|
+
let softFailureAnalysis = { hasSoftFailure: false, failureCount: 0, warnCount: 0 };
|
|
320
|
+
|
|
321
|
+
if (validatorSpecs && validatorSpecs.length > 0) {
|
|
322
|
+
const validatorContext = {
|
|
323
|
+
page,
|
|
324
|
+
consoleMessages,
|
|
325
|
+
url: page.url()
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
validatorResults = await runValidators(validatorSpecs, validatorContext);
|
|
329
|
+
softFailureAnalysis = analyzeSoftFailures(validatorResults);
|
|
330
|
+
|
|
331
|
+
// If validators detected soft failures, upgrade outcome
|
|
332
|
+
if (softFailureAnalysis.hasSoftFailure) {
|
|
333
|
+
// Soft failure still counts as FAILURE (outcome), not FRICTION
|
|
334
|
+
// Soft failures are recorded separately for analysis
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check for friction signals in total duration
|
|
339
|
+
if (totalDurationMs > this.frictionThresholds.totalDurationMs) {
|
|
340
|
+
frictionSignals.push({
|
|
341
|
+
id: 'slow_total_duration',
|
|
342
|
+
description: `Total attempt duration exceeded threshold`,
|
|
343
|
+
metric: 'totalDurationMs',
|
|
344
|
+
threshold: this.frictionThresholds.totalDurationMs,
|
|
345
|
+
observedValue: totalDurationMs,
|
|
346
|
+
affectedStepId: null,
|
|
347
|
+
severity: /** @type {FrictionSeverity} */ ('low')
|
|
348
|
+
});
|
|
349
|
+
frictionReasons.push(`Attempt took ${totalDurationMs}ms total (threshold: ${this.frictionThresholds.totalDurationMs}ms)`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
frictionMetrics = {
|
|
353
|
+
totalDurationMs,
|
|
354
|
+
stepCount: steps.length,
|
|
355
|
+
totalRetries: steps.reduce((sum, s) => sum + s.retries, 0),
|
|
356
|
+
maxStepDurationMs: Math.max(...steps.map(s => s.durationMs || 0))
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Determine outcome based on friction signals
|
|
360
|
+
const isFriction = frictionSignals.length > 0;
|
|
361
|
+
const outcome = /** @type {AttemptOutcome} */ (isFriction ? 'FRICTION' : 'SUCCESS');
|
|
362
|
+
|
|
363
|
+
// Generate friction summary
|
|
364
|
+
const frictionSummary = isFriction
|
|
365
|
+
? `User succeeded, but encountered ${frictionSignals.length} friction ${frictionSignals.length === 1 ? 'signal' : 'signals'}`
|
|
366
|
+
: null;
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
outcome,
|
|
370
|
+
steps,
|
|
371
|
+
startedAt: startedAt.toISOString(),
|
|
372
|
+
endedAt: endedAt.toISOString(),
|
|
373
|
+
totalDurationMs,
|
|
374
|
+
friction: {
|
|
375
|
+
isFriction,
|
|
376
|
+
signals: frictionSignals,
|
|
377
|
+
summary: frictionSummary,
|
|
378
|
+
reasons: frictionReasons, // Keep for backward compatibility
|
|
379
|
+
thresholds: this.frictionThresholds,
|
|
380
|
+
metrics: frictionMetrics
|
|
381
|
+
},
|
|
382
|
+
error: null,
|
|
383
|
+
successReason,
|
|
384
|
+
validators: validatorResults,
|
|
385
|
+
softFailures: softFailureAnalysis,
|
|
386
|
+
discoverySignals: {
|
|
387
|
+
consoleErrorCount: consoleErrors.length,
|
|
388
|
+
pageErrorCount: pageErrors.length
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
} catch (err) {
|
|
393
|
+
const endedAt = new Date();
|
|
394
|
+
page.removeListener('console', consoleHandler);
|
|
395
|
+
page.removeListener('pageerror', pageErrorHandler);
|
|
396
|
+
const failureDurationMs = endedAt.getTime() - startedAt.getTime();
|
|
397
|
+
return {
|
|
398
|
+
outcome: /** @type {AttemptOutcome} */ ('FAILURE'),
|
|
399
|
+
steps,
|
|
400
|
+
startedAt: startedAt.toISOString(),
|
|
401
|
+
endedAt: endedAt.toISOString(),
|
|
402
|
+
totalDurationMs: failureDurationMs,
|
|
403
|
+
friction: {
|
|
404
|
+
isFriction: false,
|
|
405
|
+
signals: [],
|
|
406
|
+
summary: null,
|
|
407
|
+
reasons: [],
|
|
408
|
+
thresholds: this.frictionThresholds,
|
|
409
|
+
metrics: {
|
|
410
|
+
totalDurationMs: failureDurationMs,
|
|
411
|
+
stepCount: steps.length,
|
|
412
|
+
totalRetries: steps.reduce((sum, s) => sum + s.retries, 0),
|
|
413
|
+
maxStepDurationMs: Math.max(...steps.map(s => s.durationMs || 0), 0)
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
error: `Step "${currentStep?.id}" failed: ${err.message}`,
|
|
417
|
+
successReason: null,
|
|
418
|
+
validators: [],
|
|
419
|
+
softFailures: { hasSoftFailure: false, failureCount: 0, warnCount: 0 },
|
|
420
|
+
discoverySignals: {
|
|
421
|
+
consoleErrorCount: consoleErrors.length,
|
|
422
|
+
pageErrorCount: pageErrors.length
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
} finally {
|
|
426
|
+
page.removeListener('console', consoleHandler);
|
|
427
|
+
page.removeListener('pageerror', pageErrorHandler);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Execute a single step
|
|
433
|
+
*/
|
|
434
|
+
async _executeStep(page, stepDef) {
|
|
435
|
+
const timeout = stepDef.timeout || this.timeout;
|
|
436
|
+
|
|
437
|
+
switch (stepDef.type) {
|
|
438
|
+
case 'navigate':
|
|
439
|
+
await page.goto(stepDef.target, {
|
|
440
|
+
waitUntil: 'domcontentloaded',
|
|
441
|
+
timeout
|
|
442
|
+
});
|
|
443
|
+
break;
|
|
444
|
+
|
|
445
|
+
case 'click':
|
|
446
|
+
// Try each selector in the target (semicolon-separated)
|
|
447
|
+
const selectors = stepDef.target.split(',').map(s => s.trim());
|
|
448
|
+
let clicked = false;
|
|
449
|
+
|
|
450
|
+
for (const selector of selectors) {
|
|
451
|
+
try {
|
|
452
|
+
await page.click(selector, { timeout: 5000 });
|
|
453
|
+
clicked = true;
|
|
454
|
+
break;
|
|
455
|
+
} catch {
|
|
456
|
+
// Try next selector
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (!clicked) {
|
|
461
|
+
throw new Error(`Could not click element: ${stepDef.target}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Wait for navigation if expected
|
|
465
|
+
if (stepDef.waitForNavigation) {
|
|
466
|
+
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
|
467
|
+
}
|
|
468
|
+
break;
|
|
469
|
+
|
|
470
|
+
case 'type':
|
|
471
|
+
// Try each selector
|
|
472
|
+
const typeSelectors = stepDef.target.split(',').map(s => s.trim());
|
|
473
|
+
let typed = false;
|
|
474
|
+
|
|
475
|
+
for (const selector of typeSelectors) {
|
|
476
|
+
try {
|
|
477
|
+
await page.fill(selector, stepDef.value, { timeout: 5000 });
|
|
478
|
+
typed = true;
|
|
479
|
+
break;
|
|
480
|
+
} catch {
|
|
481
|
+
// Try next selector
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!typed) {
|
|
486
|
+
throw new Error(`Could not type into element: ${stepDef.target}`);
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
|
|
490
|
+
case 'waitFor':
|
|
491
|
+
const waitSelectors = stepDef.target.split(',').map(s => s.trim());
|
|
492
|
+
let found = false;
|
|
493
|
+
let earlyExitReason = null;
|
|
494
|
+
|
|
495
|
+
for (const selector of waitSelectors) {
|
|
496
|
+
try {
|
|
497
|
+
// Phase 7.4: Adaptive timeout
|
|
498
|
+
const adaptiveTimeout = stepDef.timeout || 5000;
|
|
499
|
+
|
|
500
|
+
await page.waitForSelector(selector, {
|
|
501
|
+
timeout: adaptiveTimeout,
|
|
502
|
+
state: stepDef.state || 'visible'
|
|
503
|
+
});
|
|
504
|
+
found = true;
|
|
505
|
+
break;
|
|
506
|
+
} catch (err) {
|
|
507
|
+
// Phase 7.4: Detect early exit signals
|
|
508
|
+
if (err.message && err.message.includes('Timeout')) {
|
|
509
|
+
earlyExitReason = 'Target never appeared (DOM settled)';
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (!found) {
|
|
515
|
+
// Phase 7.4: Include early exit reason
|
|
516
|
+
const errorMsg = earlyExitReason
|
|
517
|
+
? `${earlyExitReason}: ${stepDef.target}`
|
|
518
|
+
: `Element not found: ${stepDef.target}`;
|
|
519
|
+
throw new Error(errorMsg);
|
|
520
|
+
}
|
|
521
|
+
break;
|
|
522
|
+
|
|
523
|
+
case 'wait':
|
|
524
|
+
await page.waitForTimeout(stepDef.duration || 1000);
|
|
525
|
+
break;
|
|
526
|
+
|
|
527
|
+
default:
|
|
528
|
+
throw new Error(`Unknown step type: ${stepDef.type}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Capture screenshot
|
|
534
|
+
*/
|
|
535
|
+
async _captureScreenshot(page, artifactsDir, stepId) {
|
|
536
|
+
try {
|
|
537
|
+
const screenshotsDir = path.join(artifactsDir, 'attempt-screenshots');
|
|
538
|
+
if (!fs.existsSync(screenshotsDir)) {
|
|
539
|
+
fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const filename = `${stepId}.jpeg`;
|
|
543
|
+
const fullPath = path.join(screenshotsDir, filename);
|
|
544
|
+
|
|
545
|
+
await page.screenshot({
|
|
546
|
+
path: fullPath,
|
|
547
|
+
type: 'jpeg',
|
|
548
|
+
quality: 80,
|
|
549
|
+
fullPage: true
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
return filename;
|
|
553
|
+
} catch {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async _savePageContent(page, artifactsDir, stepId) {
|
|
559
|
+
try {
|
|
560
|
+
const domDir = path.join(artifactsDir, 'attempt-dom');
|
|
561
|
+
if (!fs.existsSync(domDir)) {
|
|
562
|
+
fs.mkdirSync(domDir, { recursive: true });
|
|
563
|
+
}
|
|
564
|
+
const filename = `${stepId}.html`;
|
|
565
|
+
const fullPath = path.join(domDir, filename);
|
|
566
|
+
const content = await page.content();
|
|
567
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
568
|
+
return path.relative(artifactsDir, fullPath);
|
|
569
|
+
} catch {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Check if an attempt is applicable to this site
|
|
576
|
+
* Returns: { applicable: boolean, confidence: number, reason: string, discoverySignals: {} }
|
|
577
|
+
*/
|
|
578
|
+
async checkAttemptApplicability(page, attemptId) {
|
|
579
|
+
const attemptDef = this.loadAttemptDefinition(attemptId);
|
|
580
|
+
if (!attemptDef) {
|
|
581
|
+
return {
|
|
582
|
+
applicable: false,
|
|
583
|
+
confidence: 0,
|
|
584
|
+
reason: 'Attempt not found in registry',
|
|
585
|
+
discoverySignals: {}
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Map attempt IDs to feature types
|
|
590
|
+
const featureTypeMap = {
|
|
591
|
+
'login': 'login',
|
|
592
|
+
'signup': 'signup',
|
|
593
|
+
'checkout': 'checkout',
|
|
594
|
+
'contact_form': 'contact_form',
|
|
595
|
+
'newsletter_signup': 'newsletter',
|
|
596
|
+
'language_switch': 'language_switch'
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const featureType = featureTypeMap[attemptId] || null;
|
|
600
|
+
|
|
601
|
+
if (!featureType) {
|
|
602
|
+
// Attempt with no feature detection (e.g., custom attempts) - always applicable
|
|
603
|
+
return {
|
|
604
|
+
applicable: true,
|
|
605
|
+
confidence: 0.5,
|
|
606
|
+
reason: 'Custom attempt, assuming applicable',
|
|
607
|
+
discoverySignals: {}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
const detection = await detectFeature(page, featureType);
|
|
613
|
+
return {
|
|
614
|
+
applicable: detection.present,
|
|
615
|
+
confidence: detection.confidence,
|
|
616
|
+
reason: detection.present
|
|
617
|
+
? `Feature detected with signals: ${detection.evidence.join(', ')}`
|
|
618
|
+
: `Feature not detected; no signals found`,
|
|
619
|
+
discoverySignals: {
|
|
620
|
+
featureType,
|
|
621
|
+
detectionSignals: detection.evidence,
|
|
622
|
+
detected: detection.present,
|
|
623
|
+
confidence: detection.confidence
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
} catch (err) {
|
|
627
|
+
return {
|
|
628
|
+
applicable: false,
|
|
629
|
+
confidence: 0,
|
|
630
|
+
reason: `Detection error: ${err.message}`,
|
|
631
|
+
discoverySignals: { error: err.message }
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Attempt to find an element using fallback selectors
|
|
638
|
+
* Used by _executeStep when element not found with primary selector
|
|
639
|
+
* Returns: { element, discoverySignals }
|
|
640
|
+
*/
|
|
641
|
+
async findElementWithFallbacks(page, goalType) {
|
|
642
|
+
try {
|
|
643
|
+
const selectorChain = buildSelectorChain(goalType);
|
|
644
|
+
if (!selectorChain || selectorChain.length === 0) {
|
|
645
|
+
return {
|
|
646
|
+
element: null,
|
|
647
|
+
discoverySignals: { error: `No selector chain for goal: ${goalType}` }
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const result = await findElement(page, selectorChain, { timeout: 5000, requireVisible: true });
|
|
652
|
+
return {
|
|
653
|
+
element: result.element,
|
|
654
|
+
discoverySignals: {
|
|
655
|
+
goalType,
|
|
656
|
+
selectorChainLength: selectorChain.length,
|
|
657
|
+
strategy: result.strategy,
|
|
658
|
+
confidence: result.confidence,
|
|
659
|
+
found: result.element ? true : false,
|
|
660
|
+
...result.discoverySignals
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
} catch (err) {
|
|
664
|
+
return {
|
|
665
|
+
element: null,
|
|
666
|
+
discoverySignals: { error: err.message }
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async _runSiteSmokeAttempt(page, baseUrl, artifactsDir, consoleErrors, pageErrors) {
|
|
672
|
+
const startedAt = new Date();
|
|
673
|
+
const steps = [];
|
|
674
|
+
const discoverySignals = {
|
|
675
|
+
discoveredLinks: [],
|
|
676
|
+
chosenTargets: [],
|
|
677
|
+
navigationResults: [],
|
|
678
|
+
consoleErrorCount: consoleErrors.length,
|
|
679
|
+
pageErrorCount: pageErrors.length
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const recordStep = (step) => {
|
|
683
|
+
steps.push(step);
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// Step: navigate home
|
|
687
|
+
let homepageStatus = null;
|
|
688
|
+
try {
|
|
689
|
+
const resp = await page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: this.timeout });
|
|
690
|
+
homepageStatus = resp ? resp.status() : null;
|
|
691
|
+
recordStep({
|
|
692
|
+
id: 'navigate_home',
|
|
693
|
+
type: 'navigate',
|
|
694
|
+
target: baseUrl,
|
|
695
|
+
status: /** @type {StepStatus} */ ('success'),
|
|
696
|
+
startedAt: startedAt.toISOString(),
|
|
697
|
+
endedAt: new Date().toISOString(),
|
|
698
|
+
durationMs: null,
|
|
699
|
+
retries: 0,
|
|
700
|
+
screenshots: []
|
|
701
|
+
});
|
|
702
|
+
if (artifactsDir) {
|
|
703
|
+
await this._captureScreenshot(page, artifactsDir, 'site_smoke_home');
|
|
704
|
+
}
|
|
705
|
+
} catch (err) {
|
|
706
|
+
recordStep({
|
|
707
|
+
id: 'navigate_home',
|
|
708
|
+
type: 'navigate',
|
|
709
|
+
target: baseUrl,
|
|
710
|
+
status: /** @type {StepStatus} */ ('failed'),
|
|
711
|
+
error: err.message,
|
|
712
|
+
startedAt: startedAt.toISOString(),
|
|
713
|
+
endedAt: new Date().toISOString(),
|
|
714
|
+
durationMs: null,
|
|
715
|
+
retries: 0,
|
|
716
|
+
screenshots: []
|
|
717
|
+
});
|
|
718
|
+
const failureEndedAt = new Date();
|
|
719
|
+
const failureDurationMs = failureEndedAt.getTime() - startedAt.getTime();
|
|
720
|
+
return {
|
|
721
|
+
outcome: /** @type {AttemptOutcome} */ ('FAILURE'),
|
|
722
|
+
steps,
|
|
723
|
+
startedAt: startedAt.toISOString(),
|
|
724
|
+
endedAt: failureEndedAt.toISOString(),
|
|
725
|
+
totalDurationMs: failureDurationMs,
|
|
726
|
+
friction: {
|
|
727
|
+
isFriction: false,
|
|
728
|
+
signals: [],
|
|
729
|
+
summary: null,
|
|
730
|
+
reasons: [],
|
|
731
|
+
thresholds: this.frictionThresholds,
|
|
732
|
+
metrics: {
|
|
733
|
+
totalDurationMs: failureDurationMs,
|
|
734
|
+
stepCount: steps.length,
|
|
735
|
+
totalRetries: steps.reduce((sum, s) => sum + (s.retries || 0), 0),
|
|
736
|
+
maxStepDurationMs: Math.max(...steps.map(s => s.durationMs || 0), 0)
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
error: `Failed to load homepage: ${err.message}`,
|
|
740
|
+
successReason: null,
|
|
741
|
+
validators: [],
|
|
742
|
+
softFailures: { hasSoftFailure: false, failureCount: 0, warnCount: 0 },
|
|
743
|
+
discoverySignals
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Discover internal links from header/nav/footer
|
|
748
|
+
const prioritized = ['docs', 'pricing', 'features', 'about', 'contact', 'login', 'signup', 'privacy', 'terms'];
|
|
749
|
+
const baseOrigin = new URL(baseUrl).origin;
|
|
750
|
+
const { discoveredLinks, chosenLinks } = await page.evaluate(({ origin, prioritizedList }) => {
|
|
751
|
+
const anchors = Array.from(document.querySelectorAll('header a[href], nav a[href], footer a[href], a[href]'));
|
|
752
|
+
const cleaned = anchors
|
|
753
|
+
.map(a => ({ href: a.getAttribute('href') || '', text: (a.textContent || '').trim() }))
|
|
754
|
+
.filter(a => a.href && !a.href.startsWith('mailto:') && !a.href.startsWith('tel:') && !a.href.startsWith('javascript:'))
|
|
755
|
+
.map(a => {
|
|
756
|
+
let abs = a.href;
|
|
757
|
+
try {
|
|
758
|
+
abs = new URL(a.href, origin).href;
|
|
759
|
+
} catch (_) {}
|
|
760
|
+
return { ...a, abs };
|
|
761
|
+
})
|
|
762
|
+
.filter(a => a.abs.startsWith(origin));
|
|
763
|
+
|
|
764
|
+
const seen = new Set();
|
|
765
|
+
const unique = [];
|
|
766
|
+
for (const link of cleaned) {
|
|
767
|
+
if (seen.has(link.abs)) continue;
|
|
768
|
+
seen.add(link.abs);
|
|
769
|
+
unique.push(link);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const prioritizedMatches = [];
|
|
773
|
+
for (const link of unique) {
|
|
774
|
+
const lower = (link.abs + ' ' + link.text).toLowerCase();
|
|
775
|
+
const match = prioritizedList.find(p => lower.includes(`/${p}`) || lower.includes(p));
|
|
776
|
+
if (match) {
|
|
777
|
+
prioritizedMatches.push({ ...link, priority: prioritizedList.indexOf(match) });
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
prioritizedMatches.sort((a, b) => a.priority - b.priority);
|
|
782
|
+
const topPrioritized = prioritizedMatches.slice(0, 3);
|
|
783
|
+
const fallback = unique.filter(l => !topPrioritized.find(t => t.abs === l.abs)).slice(0, 3 - topPrioritized.length);
|
|
784
|
+
const chosen = [...topPrioritized, ...fallback];
|
|
785
|
+
|
|
786
|
+
return {
|
|
787
|
+
discoveredLinks: unique,
|
|
788
|
+
chosenLinks: chosen
|
|
789
|
+
};
|
|
790
|
+
}, { origin: baseOrigin, prioritizedList: prioritized });
|
|
791
|
+
|
|
792
|
+
discoverySignals.discoveredLinks = discoveredLinks;
|
|
793
|
+
discoverySignals.chosenTargets = chosenLinks;
|
|
794
|
+
|
|
795
|
+
// Attempt navigation to chosen links (up to 3)
|
|
796
|
+
for (const link of chosenLinks) {
|
|
797
|
+
const start = Date.now();
|
|
798
|
+
const navResult = { target: link.abs, text: link.text, ok: false, status: null, finalUrl: null };
|
|
799
|
+
try {
|
|
800
|
+
const resp = await page.goto(link.abs, { waitUntil: 'domcontentloaded', timeout: this.timeout });
|
|
801
|
+
navResult.status = resp ? resp.status() : null;
|
|
802
|
+
navResult.finalUrl = page.url();
|
|
803
|
+
navResult.ok = (navResult.status && navResult.status < 400) || navResult.finalUrl.startsWith(link.abs);
|
|
804
|
+
} catch (err) {
|
|
805
|
+
navResult.error = err.message;
|
|
806
|
+
}
|
|
807
|
+
navResult.durationMs = Date.now() - start;
|
|
808
|
+
discoverySignals.navigationResults.push(navResult);
|
|
809
|
+
recordStep({
|
|
810
|
+
id: `nav_${link.text || link.abs}`,
|
|
811
|
+
type: 'navigate',
|
|
812
|
+
target: link.abs,
|
|
813
|
+
status: navResult.ok ? 'success' : 'failed',
|
|
814
|
+
error: navResult.ok ? null : navResult.error || 'Navigation failed',
|
|
815
|
+
startedAt: new Date(start).toISOString(),
|
|
816
|
+
endedAt: new Date().toISOString(),
|
|
817
|
+
durationMs: navResult.durationMs,
|
|
818
|
+
retries: 0,
|
|
819
|
+
screenshots: []
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const executedOk = discoverySignals.navigationResults.some(r => r.ok) || homepageStatus !== null;
|
|
824
|
+
const endedAt = new Date();
|
|
825
|
+
const totalDurationMs = endedAt - startedAt;
|
|
826
|
+
const outcome = executedOk ? 'SUCCESS' : 'FAILURE';
|
|
827
|
+
|
|
828
|
+
return {
|
|
829
|
+
outcome,
|
|
830
|
+
steps,
|
|
831
|
+
startedAt: startedAt.toISOString(),
|
|
832
|
+
endedAt: endedAt.toISOString(),
|
|
833
|
+
totalDurationMs,
|
|
834
|
+
friction: { isFriction: false, signals: [], summary: null, reasons: [], thresholds: this.frictionThresholds, metrics: {} },
|
|
835
|
+
error: executedOk ? null : 'No internal navigation succeeded',
|
|
836
|
+
successReason: executedOk ? 'At least one navigation completed' : null,
|
|
837
|
+
validators: [],
|
|
838
|
+
softFailures: { hasSoftFailure: false, failureCount: 0, warnCount: 0 },
|
|
839
|
+
discoverySignals: {
|
|
840
|
+
...discoverySignals,
|
|
841
|
+
consoleErrorCount: consoleErrors.length,
|
|
842
|
+
pageErrorCount: pageErrors.length,
|
|
843
|
+
homepageStatus
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async _runPrimaryCtasAttempt(page, baseUrl, artifactsDir, consoleErrors, pageErrors) {
|
|
849
|
+
const startedAt = new Date();
|
|
850
|
+
const steps = [];
|
|
851
|
+
const selectorChainTried = ['text:Docs', 'text:Pricing', 'text:GitHub', 'text:Contact', 'text:Sign in', 'text:Sign up', 'text:Get started', 'text:Try', 'text:Demo'];
|
|
852
|
+
const discoverySignals = {
|
|
853
|
+
ctaCandidates: [],
|
|
854
|
+
navigationResults: [],
|
|
855
|
+
githubValidated: false,
|
|
856
|
+
selectorChainTried,
|
|
857
|
+
consoleErrorCount: consoleErrors.length,
|
|
858
|
+
pageErrorCount: pageErrors.length
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
await page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: this.timeout });
|
|
862
|
+
steps.push({ id: 'navigate_home', type: 'navigate', target: baseUrl, status: 'success', startedAt: startedAt.toISOString(), endedAt: new Date().toISOString(), retries: 0, screenshots: [] });
|
|
863
|
+
if (artifactsDir) {
|
|
864
|
+
await this._captureScreenshot(page, artifactsDir, 'primary_ctas_home');
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const baseOrigin = new URL(baseUrl).origin;
|
|
868
|
+
const ctaCandidates = await page.evaluate(({ origin }) => {
|
|
869
|
+
const keywords = ['docs','pricing','github','contact','sign in','sign up','get started','try','demo','start'];
|
|
870
|
+
const elements = Array.from(document.querySelectorAll('a[href], button'));
|
|
871
|
+
const candidates = [];
|
|
872
|
+
for (const el of elements) {
|
|
873
|
+
const text = (el.textContent || '').trim();
|
|
874
|
+
if (!text) continue;
|
|
875
|
+
const lower = text.toLowerCase();
|
|
876
|
+
if (!keywords.some(k => lower.includes(k))) continue;
|
|
877
|
+
const href = el.getAttribute('href') || '';
|
|
878
|
+
let abs = href;
|
|
879
|
+
if (href) {
|
|
880
|
+
try {
|
|
881
|
+
abs = new URL(href, origin).href;
|
|
882
|
+
} catch (_) {}
|
|
883
|
+
}
|
|
884
|
+
candidates.push({ text, href, abs, tag: el.tagName, target: el.getAttribute('target') || null });
|
|
885
|
+
}
|
|
886
|
+
const seen = new Set();
|
|
887
|
+
const unique = [];
|
|
888
|
+
for (const c of candidates) {
|
|
889
|
+
const key = c.abs || c.text;
|
|
890
|
+
if (seen.has(key)) continue;
|
|
891
|
+
seen.add(key);
|
|
892
|
+
unique.push(c);
|
|
893
|
+
}
|
|
894
|
+
return unique;
|
|
895
|
+
}, { origin: baseOrigin });
|
|
896
|
+
|
|
897
|
+
discoverySignals.ctaCandidates = ctaCandidates;
|
|
898
|
+
|
|
899
|
+
if (ctaCandidates.length === 0) {
|
|
900
|
+
return {
|
|
901
|
+
outcome: /** @type {AttemptOutcome} */ ('NOT_APPLICABLE'),
|
|
902
|
+
skipReason: 'No CTA elements detected',
|
|
903
|
+
steps,
|
|
904
|
+
startedAt: startedAt.toISOString(),
|
|
905
|
+
endedAt: new Date().toISOString(),
|
|
906
|
+
totalDurationMs: Date.now() - startedAt.getTime(),
|
|
907
|
+
friction: { isFriction: false, signals: [], summary: null, reasons: [], thresholds: this.frictionThresholds, metrics: {} },
|
|
908
|
+
error: null,
|
|
909
|
+
successReason: null,
|
|
910
|
+
validators: [],
|
|
911
|
+
softFailures: { hasSoftFailure: false, failureCount: 0, warnCount: 0 },
|
|
912
|
+
discoverySignals
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const targets = ctaCandidates.slice(0, 2);
|
|
917
|
+
for (const target of targets) {
|
|
918
|
+
const start = Date.now();
|
|
919
|
+
const navResult = { target: target.abs || target.href, text: target.text, ok: false, status: null, finalUrl: null };
|
|
920
|
+
try {
|
|
921
|
+
const resp = await page.goto(target.abs || target.href || baseUrl, { waitUntil: 'domcontentloaded', timeout: this.timeout });
|
|
922
|
+
navResult.status = resp ? resp.status() : null;
|
|
923
|
+
navResult.finalUrl = page.url();
|
|
924
|
+
navResult.ok = (navResult.status && navResult.status < 400) || (navResult.finalUrl && navResult.finalUrl !== baseUrl);
|
|
925
|
+
if ((target.abs || '').includes('github.com') && navResult.ok) {
|
|
926
|
+
discoverySignals.githubValidated = true;
|
|
927
|
+
}
|
|
928
|
+
} catch (err) {
|
|
929
|
+
navResult.error = err.message;
|
|
930
|
+
}
|
|
931
|
+
navResult.durationMs = Date.now() - start;
|
|
932
|
+
discoverySignals.navigationResults.push(navResult);
|
|
933
|
+
steps.push({
|
|
934
|
+
id: `cta_${target.text.toLowerCase().replace(/\s+/g, '_')}`,
|
|
935
|
+
type: 'navigate',
|
|
936
|
+
target: target.abs || target.href,
|
|
937
|
+
status: navResult.ok ? 'success' : 'failed',
|
|
938
|
+
error: navResult.ok ? null : navResult.error || 'Navigation failed',
|
|
939
|
+
startedAt: new Date(start).toISOString(),
|
|
940
|
+
endedAt: new Date().toISOString(),
|
|
941
|
+
retries: 0,
|
|
942
|
+
screenshots: []
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const executedOk = discoverySignals.navigationResults.some(r => r.ok);
|
|
947
|
+
const endedAt = new Date();
|
|
948
|
+
const totalDurationMs = endedAt - startedAt;
|
|
949
|
+
return {
|
|
950
|
+
outcome: executedOk ? 'SUCCESS' : 'FAILURE',
|
|
951
|
+
steps,
|
|
952
|
+
startedAt: startedAt.toISOString(),
|
|
953
|
+
endedAt: endedAt.toISOString(),
|
|
954
|
+
totalDurationMs,
|
|
955
|
+
friction: { isFriction: false, signals: [], summary: null, reasons: [], thresholds: this.frictionThresholds, metrics: {} },
|
|
956
|
+
error: executedOk ? null : 'CTA navigation did not succeed',
|
|
957
|
+
successReason: executedOk ? 'CTA navigation completed' : null,
|
|
958
|
+
validators: [],
|
|
959
|
+
softFailures: { hasSoftFailure: false, failureCount: 0, warnCount: 0 },
|
|
960
|
+
discoverySignals
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async _runContactDiscoveryAttempt(page, baseUrl, artifactsDir, consoleErrors, pageErrors) {
|
|
965
|
+
const startedAt = new Date();
|
|
966
|
+
const steps = [];
|
|
967
|
+
const discoverySignals = {
|
|
968
|
+
mailto: null,
|
|
969
|
+
contactLinks: [],
|
|
970
|
+
navigationResults: [],
|
|
971
|
+
consoleErrorCount: consoleErrors.length,
|
|
972
|
+
pageErrorCount: pageErrors.length
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
await page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: this.timeout });
|
|
976
|
+
steps.push({ id: 'navigate_home', type: 'navigate', target: baseUrl, status: 'success', startedAt: startedAt.toISOString(), endedAt: new Date().toISOString(), retries: 0, screenshots: [] });
|
|
977
|
+
|
|
978
|
+
const baseOrigin = new URL(baseUrl).origin;
|
|
979
|
+
const contactInfo = await page.evaluate(({ origin }) => {
|
|
980
|
+
const anchors = Array.from(document.querySelectorAll('a[href]'));
|
|
981
|
+
const mailto = anchors.find(a => (a.getAttribute('href') || '').startsWith('mailto:'));
|
|
982
|
+
if (mailto) {
|
|
983
|
+
return { mailto: mailto.getAttribute('href'), contactLinks: [] };
|
|
984
|
+
}
|
|
985
|
+
const contactLinks = anchors
|
|
986
|
+
.filter(a => {
|
|
987
|
+
const href = a.getAttribute('href') || '';
|
|
988
|
+
const text = (a.textContent || '').toLowerCase();
|
|
989
|
+
return href.toLowerCase().includes('contact') || text.includes('contact');
|
|
990
|
+
})
|
|
991
|
+
.map(a => {
|
|
992
|
+
const href = a.getAttribute('href') || '';
|
|
993
|
+
let abs = href;
|
|
994
|
+
try { abs = new URL(href, origin).href; } catch (_) {}
|
|
995
|
+
return { href, abs, text: (a.textContent || '').trim() };
|
|
996
|
+
});
|
|
997
|
+
return { mailto: null, contactLinks };
|
|
998
|
+
}, { origin: baseOrigin });
|
|
999
|
+
|
|
1000
|
+
discoverySignals.mailto = contactInfo.mailto;
|
|
1001
|
+
discoverySignals.contactLinks = contactInfo.contactLinks;
|
|
1002
|
+
|
|
1003
|
+
if (contactInfo.mailto) {
|
|
1004
|
+
return {
|
|
1005
|
+
outcome: /** @type {AttemptOutcome} */ ('SUCCESS'),
|
|
1006
|
+
steps,
|
|
1007
|
+
startedAt: startedAt.toISOString(),
|
|
1008
|
+
endedAt: new Date().toISOString(),
|
|
1009
|
+
totalDurationMs: Date.now() - startedAt.getTime(),
|
|
1010
|
+
friction: { isFriction: false, signals: [], summary: null, reasons: [], thresholds: this.frictionThresholds, metrics: {} },
|
|
1011
|
+
error: null,
|
|
1012
|
+
successReason: `Found mailto: ${contactInfo.mailto}`,
|
|
1013
|
+
validators: [],
|
|
1014
|
+
softFailures: { hasSoftFailure: false, failureCount: 0, warnCount: 0 },
|
|
1015
|
+
discoverySignals
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (contactInfo.contactLinks.length === 0) {
|
|
1020
|
+
return {
|
|
1021
|
+
outcome: /** @type {AttemptOutcome} */ ('NOT_APPLICABLE'),
|
|
1022
|
+
skipReason: 'No contact link or mailto detected',
|
|
1023
|
+
steps,
|
|
1024
|
+
startedAt: startedAt.toISOString(),
|
|
1025
|
+
endedAt: new Date().toISOString(),
|
|
1026
|
+
totalDurationMs: Date.now() - startedAt.getTime(),
|
|
1027
|
+
friction: { isFriction: false, signals: [], summary: null, reasons: [], thresholds: this.frictionThresholds, metrics: {} },
|
|
1028
|
+
error: null,
|
|
1029
|
+
successReason: null,
|
|
1030
|
+
validators: [],
|
|
1031
|
+
softFailures: { hasSoftFailure: false, failureCount: 0, warnCount: 0 },
|
|
1032
|
+
discoverySignals
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const target = contactInfo.contactLinks[0];
|
|
1037
|
+
const startNav = Date.now();
|
|
1038
|
+
const navResult = { target: target.abs || target.href, text: target.text, ok: false, status: null, finalUrl: null };
|
|
1039
|
+
try {
|
|
1040
|
+
const resp = await page.goto(target.abs || target.href, { waitUntil: 'domcontentloaded', timeout: this.timeout });
|
|
1041
|
+
navResult.status = resp ? resp.status() : null;
|
|
1042
|
+
navResult.finalUrl = page.url();
|
|
1043
|
+
navResult.ok = (navResult.status && navResult.status < 400) || (navResult.finalUrl && navResult.finalUrl.includes('contact'));
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
navResult.error = err.message;
|
|
1046
|
+
}
|
|
1047
|
+
navResult.durationMs = Date.now() - startNav;
|
|
1048
|
+
discoverySignals.navigationResults.push(navResult);
|
|
1049
|
+
steps.push({ id: 'visit_contact', type: 'navigate', target: target.abs || target.href, status: navResult.ok ? 'success' : 'failed', error: navResult.error || null, startedAt: new Date(startNav).toISOString(), endedAt: new Date().toISOString(), retries: 0, screenshots: [] });
|
|
1050
|
+
|
|
1051
|
+
const endedAt = new Date();
|
|
1052
|
+
const totalDurationMs = endedAt - startedAt;
|
|
1053
|
+
return {
|
|
1054
|
+
outcome: navResult.ok ? 'SUCCESS' : 'FAILURE',
|
|
1055
|
+
steps,
|
|
1056
|
+
startedAt: startedAt.toISOString(),
|
|
1057
|
+
endedAt: endedAt.toISOString(),
|
|
1058
|
+
totalDurationMs,
|
|
1059
|
+
friction: { isFriction: false, signals: [], summary: null, reasons: [], thresholds: this.frictionThresholds, metrics: {} },
|
|
1060
|
+
error: navResult.ok ? null : 'Contact link navigation failed',
|
|
1061
|
+
successReason: navResult.ok ? 'Contact link reachable' : null,
|
|
1062
|
+
validators: [],
|
|
1063
|
+
softFailures: { hasSoftFailure: false, failureCount: 0, warnCount: 0 },
|
|
1064
|
+
discoverySignals
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
module.exports = { AttemptEngine };
|