@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.
Files changed (172) hide show
  1. package/CHANGELOG.md +210 -210
  2. package/LICENSE +21 -21
  3. package/README.md +297 -184
  4. package/bin/guardian.js +2242 -2221
  5. package/config/README.md +59 -59
  6. package/config/guardian.config.json +54 -54
  7. package/config/guardian.policy.json +12 -12
  8. package/config/profiles/docs.yaml +18 -18
  9. package/config/profiles/ecommerce.yaml +17 -17
  10. package/config/profiles/landing-demo.yaml +16 -16
  11. package/config/profiles/marketing.yaml +18 -18
  12. package/config/profiles/saas.yaml +21 -21
  13. package/flows/example-login-flow.json +36 -36
  14. package/flows/example-signup-flow.json +44 -44
  15. package/package.json +124 -116
  16. package/policies/enterprise.json +12 -12
  17. package/policies/landing-demo.json +22 -22
  18. package/policies/saas.json +12 -12
  19. package/policies/startup.json +12 -12
  20. package/src/enterprise/audit-logger.js +166 -166
  21. package/src/enterprise/pdf-exporter.js +267 -267
  22. package/src/enterprise/rbac-gate.js +142 -142
  23. package/src/enterprise/rbac.js +239 -239
  24. package/src/enterprise/site-manager.js +180 -180
  25. package/src/founder/feedback-system.js +156 -156
  26. package/src/founder/founder-tracker.js +213 -213
  27. package/src/founder/usage-signals.js +141 -141
  28. package/src/guardian/action-hints.js +439 -439
  29. package/src/guardian/alert-ledger.js +121 -121
  30. package/src/guardian/artifact-sanitizer.js +56 -56
  31. package/src/guardian/attempt-engine.js +1069 -1029
  32. package/src/guardian/attempt-registry.js +267 -267
  33. package/src/guardian/attempt-relevance.js +106 -106
  34. package/src/guardian/attempt-reporter.js +513 -507
  35. package/src/guardian/attempt.js +274 -273
  36. package/src/guardian/attempts-filter.js +63 -63
  37. package/src/guardian/auto-attempt-builder.js +283 -283
  38. package/src/guardian/baseline-registry.js +177 -177
  39. package/src/guardian/baseline-reporter.js +143 -143
  40. package/src/guardian/baseline-storage.js +285 -285
  41. package/src/guardian/baseline.js +535 -534
  42. package/src/guardian/behavioral-signals.js +261 -261
  43. package/src/guardian/breakage-intelligence.js +224 -224
  44. package/src/guardian/browser-pool.js +131 -131
  45. package/src/guardian/browser.js +119 -119
  46. package/src/guardian/canonical-truth.js +308 -308
  47. package/src/guardian/ci-cli.js +121 -121
  48. package/src/guardian/ci-gate.js +96 -96
  49. package/src/guardian/ci-mode.js +15 -15
  50. package/src/guardian/ci-output.js +55 -38
  51. package/src/guardian/cli-summary.js +102 -102
  52. package/src/guardian/confidence-signals.js +251 -251
  53. package/src/guardian/config-loader.js +161 -161
  54. package/src/guardian/config-validator.js +285 -283
  55. package/src/guardian/coverage-model.js +239 -239
  56. package/src/guardian/coverage-packs.js +58 -58
  57. package/src/guardian/crawler.js +142 -142
  58. package/src/guardian/data-guardian-detector.js +189 -189
  59. package/src/guardian/decision-authority.js +746 -725
  60. package/src/guardian/detection-layers.js +271 -271
  61. package/src/guardian/determinism.js +146 -146
  62. package/src/guardian/discovery-engine.js +661 -661
  63. package/src/guardian/drift-detector.js +100 -100
  64. package/src/guardian/enhanced-html-reporter.js +522 -522
  65. package/src/guardian/env-guard.js +128 -127
  66. package/src/guardian/error-clarity.js +399 -399
  67. package/src/guardian/export-contract.js +196 -196
  68. package/src/guardian/fail-safe.js +212 -212
  69. package/src/guardian/failure-intelligence.js +173 -173
  70. package/src/guardian/failure-taxonomy.js +169 -169
  71. package/src/guardian/final-outcome.js +206 -206
  72. package/src/guardian/first-run-profile.js +89 -89
  73. package/src/guardian/first-run.js +65 -67
  74. package/src/guardian/flag-validator.js +111 -111
  75. package/src/guardian/flow-executor.js +641 -639
  76. package/src/guardian/flow-registry.js +67 -67
  77. package/src/guardian/honesty.js +394 -394
  78. package/src/guardian/html-reporter.js +416 -416
  79. package/src/guardian/human-intent-resolver.js +296 -296
  80. package/src/guardian/human-interaction-model.js +351 -351
  81. package/src/guardian/human-journey-context.js +184 -184
  82. package/src/guardian/human-navigator.js +544 -544
  83. package/src/guardian/human-reporter.js +435 -431
  84. package/src/guardian/index.js +226 -221
  85. package/src/guardian/init-command.js +143 -143
  86. package/src/guardian/intent-detector.js +148 -146
  87. package/src/guardian/journey-definitions.js +132 -132
  88. package/src/guardian/journey-scan-cli.js +142 -145
  89. package/src/guardian/journey-scanner.js +583 -583
  90. package/src/guardian/junit-reporter.js +281 -281
  91. package/src/guardian/language-detection.js +99 -99
  92. package/src/guardian/live-alert.js +56 -56
  93. package/src/guardian/live-baseline-compare.js +146 -146
  94. package/src/guardian/live-cli.js +95 -95
  95. package/src/guardian/live-guardian.js +210 -210
  96. package/src/guardian/live-scheduler-runner.js +137 -137
  97. package/src/guardian/live-scheduler-state.js +167 -168
  98. package/src/guardian/live-scheduler.js +146 -146
  99. package/src/guardian/live-state.js +110 -110
  100. package/src/guardian/market-criticality.js +335 -335
  101. package/src/guardian/market-reporter.js +577 -577
  102. package/src/guardian/network-trace.js +178 -178
  103. package/src/guardian/obs-logger.js +110 -110
  104. package/src/guardian/observed-capabilities.js +427 -427
  105. package/src/guardian/output-contract.js +154 -0
  106. package/src/guardian/output-readability.js +264 -264
  107. package/src/guardian/parallel-executor.js +116 -116
  108. package/src/guardian/path-safety.js +56 -56
  109. package/src/guardian/pattern-analyzer.js +348 -348
  110. package/src/guardian/policy.js +432 -434
  111. package/src/guardian/prelaunch-gate.js +193 -193
  112. package/src/guardian/prerequisite-checker.js +101 -101
  113. package/src/guardian/preset-loader.js +152 -157
  114. package/src/guardian/profile-loader.js +96 -96
  115. package/src/guardian/reality.js +3025 -2826
  116. package/src/guardian/realworld-scenarios.js +94 -94
  117. package/src/guardian/reporter.js +167 -167
  118. package/src/guardian/retry-policy.js +123 -123
  119. package/src/guardian/root-cause-analysis.js +171 -171
  120. package/src/guardian/rules-engine.js +558 -558
  121. package/src/guardian/run-artifacts.js +212 -212
  122. package/src/guardian/run-cleanup.js +207 -207
  123. package/src/guardian/run-export.js +522 -522
  124. package/src/guardian/run-latest.js +90 -90
  125. package/src/guardian/run-list.js +211 -211
  126. package/src/guardian/run-summary.js +20 -20
  127. package/src/guardian/runtime-root.js +246 -246
  128. package/src/guardian/safety.js +248 -248
  129. package/src/guardian/scan-presets.js +133 -149
  130. package/src/guardian/screenshot.js +152 -152
  131. package/src/guardian/secret-hygiene.js +44 -44
  132. package/src/guardian/selector-fallbacks.js +394 -394
  133. package/src/guardian/semantic-contact-detection.js +255 -255
  134. package/src/guardian/semantic-contact-finder.js +201 -201
  135. package/src/guardian/semantic-targets.js +234 -234
  136. package/src/guardian/site-intelligence.js +588 -588
  137. package/src/guardian/site-introspection.js +257 -257
  138. package/src/guardian/sitemap.js +225 -225
  139. package/src/guardian/smoke.js +283 -258
  140. package/src/guardian/snapshot-schema.js +177 -290
  141. package/src/guardian/snapshot.js +430 -397
  142. package/src/guardian/stability-scorer.js +169 -169
  143. package/src/guardian/success-evaluator.js +214 -214
  144. package/src/guardian/template-command.js +184 -184
  145. package/src/guardian/text-formatters.js +426 -426
  146. package/src/guardian/timeout-profiles.js +57 -57
  147. package/src/guardian/truth/attempt.contract.js +158 -0
  148. package/src/guardian/truth/decision.contract.js +275 -0
  149. package/src/guardian/truth/snapshot.contract.js +363 -0
  150. package/src/guardian/validators.js +323 -323
  151. package/src/guardian/verdict-card.js +474 -474
  152. package/src/guardian/verdict-clarity.js +298 -298
  153. package/src/guardian/verdict-policy.js +363 -363
  154. package/src/guardian/verdict.js +333 -333
  155. package/src/guardian/verdicts.js +79 -74
  156. package/src/guardian/visual-diff.js +247 -247
  157. package/src/guardian/wait-for-outcome.js +119 -119
  158. package/src/guardian/watch-runner.js +181 -181
  159. package/src/guardian/watchdog-diff.js +167 -167
  160. package/src/guardian/webhook.js +206 -206
  161. package/src/payments/stripe-checkout.js +169 -169
  162. package/src/plans/plan-definitions.js +148 -148
  163. package/src/plans/plan-manager.js +211 -211
  164. package/src/plans/usage-tracker.js +210 -210
  165. package/src/recipes/recipe-engine.js +188 -188
  166. package/src/recipes/recipe-failure-analysis.js +159 -159
  167. package/src/recipes/recipe-registry.js +134 -134
  168. package/src/recipes/recipe-runtime.js +507 -507
  169. package/src/recipes/recipe-store.js +410 -410
  170. package/SECURITY.md +0 -77
  171. package/VERSIONING.md +0 -100
  172. package/guardian-contract-v1.md +0 -502
@@ -1,399 +1,399 @@
1
- /**
2
- * Error & Failure Messaging — Human-Readable Error Output
3
- *
4
- * Classifies execution failures into canonical categories and provides
5
- * plain-language explanations with actionable next steps.
6
- * Production-grade DX improvement for CLI output.
7
- */
8
-
9
- // Canonical error taxonomy (internal-only classification)
10
- const ERROR_CATEGORIES = {
11
- TIMEOUT: 'TIMEOUT',
12
- ELEMENT_NOT_FOUND: 'ELEMENT_NOT_FOUND',
13
- NAVIGATION_FAILED: 'NAVIGATION_FAILED',
14
- AUTH_BLOCKED: 'AUTH_BLOCKED',
15
- NOT_APPLICABLE: 'NOT_APPLICABLE',
16
- DISABLED_BY_PRESET: 'DISABLED_BY_PRESET',
17
- USER_FILTERED: 'USER_FILTERED',
18
- MISSING_DEPENDENCY: 'MISSING_DEPENDENCY',
19
- INFRA_ERROR: 'INFRA_ERROR',
20
- UNKNOWN: 'UNKNOWN'
21
- };
22
-
23
- // Error message templates (centralized, not duplicated)
24
- const ERROR_MESSAGES = {
25
- TIMEOUT: {
26
- title: 'Timeout waiting for interaction',
27
- explanation: 'This step took longer than expected to complete. The page may have performance issues or the timeout setting may be too strict.',
28
- action: 'Increase timeout setting or verify page performance.'
29
- },
30
- ELEMENT_NOT_FOUND: {
31
- title: 'Expected element not found',
32
- explanation: 'A critical UI element (button, form, link) was not present on the page. The page structure may have changed or the element is dynamically loaded.',
33
- action: 'Verify the element selector is correct or wait for dynamic content to load.'
34
- },
35
- NAVIGATION_FAILED: {
36
- title: 'Navigation failed',
37
- explanation: 'Attempting to navigate to a URL resulted in an error. The page may be unavailable, blocked, or returned an error status.',
38
- action: 'Check if the URL is accessible and returns a valid response (HTTP 200).'
39
- },
40
- AUTH_BLOCKED: {
41
- title: 'Authentication blocked',
42
- explanation: 'The test was blocked by authentication or access control. The credentials may be incorrect or the account may lack permissions.',
43
- action: 'Verify credentials and ensure the test account has required permissions.'
44
- },
45
- NOT_APPLICABLE: {
46
- title: 'Skipped (not applicable)',
47
- explanation: 'This step is not applicable to this site based on its capabilities or configuration.',
48
- action: 'This is expected behavior. No action required.'
49
- },
50
- DISABLED_BY_PRESET: {
51
- title: 'Skipped (disabled by preset)',
52
- explanation: 'This step is disabled in the selected testing preset. It can be re-enabled by choosing a different preset.',
53
- action: 'Choose a different preset if you want to test this flow, or create a custom configuration.'
54
- },
55
- USER_FILTERED: {
56
- title: 'Skipped (user filtered)',
57
- explanation: 'This step was explicitly filtered out in your configuration.',
58
- action: 'Update your configuration if you want to include this step in testing.'
59
- },
60
- MISSING_DEPENDENCY: {
61
- title: 'Skipped (missing dependency)',
62
- explanation: 'This step requires a previous step to pass, but that step did not execute or failed.',
63
- action: 'Fix the blocking issue in the previous step, or run tests without dependencies.'
64
- },
65
- INFRA_ERROR: {
66
- title: 'Infrastructure error',
67
- explanation: 'A system-level issue occurred (browser launch failed, permissions error, etc.). This is not related to your site.',
68
- action: 'Check system resources, permissions, and Guardian logs. Retry the test.'
69
- },
70
- UNKNOWN: {
71
- title: 'Unexpected error',
72
- explanation: 'An error occurred that does not fit common categories. See detailed logs for more information.',
73
- action: 'Check Guardian logs with GUARDIAN_DEBUG=1 for full error details.'
74
- }
75
- };
76
-
77
- // Skip messages (NOT errors, labeled clearly)
78
- const SKIP_MESSAGES = {
79
- DISABLED_BY_PRESET: 'Skipped by preset configuration',
80
- NOT_APPLICABLE: 'Not applicable to this site',
81
- USER_FILTERED: 'User-filtered from testing',
82
- MISSING_DEPENDENCY: 'Skipped (blocking flow failed)'
83
- };
84
-
85
- /**
86
- * Classify a raw error/failure into canonical category
87
- *
88
- * @param {Object} failure - Failure object with outcome, error, reason, etc.
89
- * @returns {string} Canonical error category
90
- */
91
- function classifyError(failure = {}) {
92
- const {
93
- outcome,
94
- reason,
95
- failureReason,
96
- message,
97
- code
98
- } = failure;
99
-
100
- // Skip outcomes (NOT errors)
101
- if (outcome === 'NOT_APPLICABLE') return ERROR_CATEGORIES.NOT_APPLICABLE;
102
- if (outcome === 'SKIPPED') {
103
- if (reason === 'DISABLED_BY_PRESET') return ERROR_CATEGORIES.DISABLED_BY_PRESET;
104
- if (reason === 'USER_FILTERED') return ERROR_CATEGORIES.USER_FILTERED;
105
- if (reason === 'MISSING_DEPENDENCY') return ERROR_CATEGORIES.MISSING_DEPENDENCY;
106
- return ERROR_CATEGORIES.NOT_APPLICABLE;
107
- }
108
-
109
- // Timeout errors
110
- if (failureReason === 'TIMEOUT' || reason === 'TIMEOUT') {
111
- return ERROR_CATEGORIES.TIMEOUT;
112
- }
113
- if (message && message.toLowerCase().includes('timeout')) {
114
- return ERROR_CATEGORIES.TIMEOUT;
115
- }
116
- if (code === 'TIMEOUT') {
117
- return ERROR_CATEGORIES.TIMEOUT;
118
- }
119
-
120
- // Element not found
121
- if (failureReason === 'ELEMENT_NOT_FOUND' || reason === 'ELEMENT_NOT_FOUND') {
122
- return ERROR_CATEGORIES.ELEMENT_NOT_FOUND;
123
- }
124
- if (message && (message.toLowerCase().includes('not found') || message.toLowerCase().includes('selector'))) {
125
- return ERROR_CATEGORIES.ELEMENT_NOT_FOUND;
126
- }
127
- if (code === 'ELEMENT_NOT_FOUND') {
128
- return ERROR_CATEGORIES.ELEMENT_NOT_FOUND;
129
- }
130
-
131
- // Navigation failures
132
- if (failureReason === 'NAVIGATION_FAILED' || reason === 'NAVIGATION_FAILED') {
133
- return ERROR_CATEGORIES.NAVIGATION_FAILED;
134
- }
135
- if (message && (message.toLowerCase().includes('navigation') || message.toLowerCase().includes('net::err'))) {
136
- return ERROR_CATEGORIES.NAVIGATION_FAILED;
137
- }
138
- if (code === 'NAVIGATION_FAILED') {
139
- return ERROR_CATEGORIES.NAVIGATION_FAILED;
140
- }
141
-
142
- // Auth blocked
143
- if (failureReason === 'AUTH_BLOCKED' || reason === 'AUTH_BLOCKED') {
144
- return ERROR_CATEGORIES.AUTH_BLOCKED;
145
- }
146
- if (message && (message.toLowerCase().includes('unauthorized') || message.toLowerCase().includes('forbidden') || message.toLowerCase().includes('403') || message.toLowerCase().includes('401'))) {
147
- return ERROR_CATEGORIES.AUTH_BLOCKED;
148
- }
149
- if (code === 'AUTH_BLOCKED') {
150
- return ERROR_CATEGORIES.AUTH_BLOCKED;
151
- }
152
-
153
- // Infrastructure errors
154
- if (failureReason === 'INFRA_ERROR' || reason === 'INFRA_ERROR') {
155
- return ERROR_CATEGORIES.INFRA_ERROR;
156
- }
157
- if (message && (message.toLowerCase().includes('browser') || message.toLowerCase().includes('permission') || message.toLowerCase().includes('system'))) {
158
- return ERROR_CATEGORIES.INFRA_ERROR;
159
- }
160
- if (code === 'BROWSER_LAUNCH_FAILED' || code === 'PERMISSION_DENIED') {
161
- return ERROR_CATEGORIES.INFRA_ERROR;
162
- }
163
-
164
- // Missing dependency
165
- if (failureReason === 'MISSING_DEPENDENCY' || reason === 'MISSING_DEPENDENCY') {
166
- return ERROR_CATEGORIES.MISSING_DEPENDENCY;
167
- }
168
-
169
- // Default to unknown
170
- return ERROR_CATEGORIES.UNKNOWN;
171
- }
172
-
173
- /**
174
- * Extract human-friendly error info from failure
175
- *
176
- * @param {Object} failure - Failure with outcome, error details, etc.
177
- * @returns {Object} { category, title, explanation, action }
178
- */
179
- function getErrorInfo(failure = {}) {
180
- const category = classifyError(failure);
181
- const template = ERROR_MESSAGES[category] || ERROR_MESSAGES[ERROR_CATEGORIES.UNKNOWN];
182
-
183
- return {
184
- category,
185
- title: template.title,
186
- explanation: template.explanation,
187
- action: template.action
188
- };
189
- }
190
-
191
- /**
192
- * Check if output should be shown
193
- * Skip in quiet, CI, or non-TTY environments
194
- *
195
- * @param {Object} config - Guardian config
196
- * @param {Array} args - CLI arguments
197
- * @returns {boolean} true if should show error clarity
198
- */
199
- function shouldShowErrorClarity(config = {}, args = []) {
200
- // Skip if --quiet or -q flag
201
- if (args.includes('--quiet') || args.includes('-q')) {
202
- return false;
203
- }
204
-
205
- // Skip if non-TTY (CI/automation without explicit output)
206
- if (!process.stdout.isTTY) {
207
- return false;
208
- }
209
-
210
- return true;
211
- }
212
-
213
- /**
214
- * Group failures by category
215
- *
216
- * @param {Array} failures - Array of failed attempts/flows
217
- * @returns {Object} Map of category -> [failures]
218
- */
219
- function groupFailuresByCategory(failures = []) {
220
- const groups = {};
221
-
222
- (failures || []).forEach(failure => {
223
- const category = classifyError(failure);
224
- if (!groups[category]) {
225
- groups[category] = [];
226
- }
227
- groups[category].push(failure);
228
- });
229
-
230
- return groups;
231
- }
232
-
233
- /**
234
- * Deduplicate similar errors within a category
235
- *
236
- * @param {Array} failures - Array of failures in same category
237
- * @returns {Array} Deduplicated failures (max 3)
238
- */
239
- function deduplicateErrors(failures = []) {
240
- const seen = new Set();
241
- const deduplicated = [];
242
-
243
- (failures || []).forEach(failure => {
244
- const key = `${failure.attemptId || failure.name}`;
245
- if (!seen.has(key) && deduplicated.length < 3) {
246
- seen.add(key);
247
- deduplicated.push(failure);
248
- }
249
- });
250
-
251
- return deduplicated;
252
- }
253
-
254
- /**
255
- * Check if this is a skip (not an error)
256
- *
257
- * @param {string} category - Error category
258
- * @returns {boolean} true if this is a skip
259
- */
260
- function isSkip(category) {
261
- return [
262
- ERROR_CATEGORIES.NOT_APPLICABLE,
263
- ERROR_CATEGORIES.DISABLED_BY_PRESET,
264
- ERROR_CATEGORIES.USER_FILTERED,
265
- ERROR_CATEGORIES.MISSING_DEPENDENCY
266
- ].includes(category);
267
- }
268
-
269
- /**
270
- * Format error clarity block for CLI output
271
- *
272
- * @param {Array} failures - Failed attempts/flows
273
- * @param {Object} config - Guardian config
274
- * @param {Array} args - CLI arguments
275
- * @returns {string} Formatted error clarity block
276
- */
277
- function formatErrorClarity(failures = [], config = {}, args = []) {
278
- if (!shouldShowErrorClarity(config, args)) {
279
- return '';
280
- }
281
-
282
- if (!failures || failures.length === 0) {
283
- return '';
284
- }
285
-
286
- const lines = [];
287
- const groups = groupFailuresByCategory(failures);
288
-
289
- // Separate actual errors from skips
290
- const errors = {};
291
- const skips = {};
292
-
293
- Object.entries(groups).forEach(([category, categoryFailures]) => {
294
- if (isSkip(category)) {
295
- skips[category] = categoryFailures;
296
- } else {
297
- errors[category] = categoryFailures;
298
- }
299
- });
300
-
301
- // Print errors section
302
- const errorCategories = Object.keys(errors);
303
- if (errorCategories.length > 0) {
304
- lines.push('');
305
- lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
306
- lines.push('FAILURES & ERRORS');
307
- lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
308
- lines.push('');
309
-
310
- errorCategories.forEach(category => {
311
- const categoryFailures = errors[category];
312
- const errorInfo = getErrorInfo(categoryFailures[0]);
313
-
314
- lines.push(`${errorInfo.title}`);
315
- lines.push('────────────────────────────────────────────────────────────');
316
- lines.push(`${errorInfo.explanation}`);
317
- lines.push(`Action: ${errorInfo.action}`);
318
-
319
- // List affected flows/attempts (max 3)
320
- const deduped = deduplicateErrors(categoryFailures);
321
- const names = deduped
322
- .map(f => f.attemptName || f.name || f.attemptId || 'unknown');
323
-
324
- if (names.length > 0) {
325
- lines.push(`Affected: ${names.join(', ')}`);
326
- }
327
-
328
- if (categoryFailures.length > 3) {
329
- lines.push(`(+${categoryFailures.length - 3} more)`);
330
- }
331
-
332
- lines.push('');
333
- });
334
- }
335
-
336
- // Print skips section
337
- const skipCategories = Object.keys(skips);
338
- if (skipCategories.length > 0) {
339
- lines.push('');
340
- lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
341
- lines.push('SKIPPED ATTEMPTS');
342
- lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
343
- lines.push('');
344
-
345
- skipCategories.forEach(category => {
346
- const categorySkips = skips[category];
347
- const skipReason = SKIP_MESSAGES[category] || 'Skipped';
348
-
349
- // Count skips in this category
350
- const names = deduplicateErrors(categorySkips)
351
- .map(s => s.attemptName || s.name || s.attemptId || 'unknown')
352
- .slice(0, 3);
353
-
354
- lines.push(`${skipReason} (${categorySkips.length})`);
355
- lines.push('────────────────────────────────────────────────────────────');
356
- if (names.length > 0) {
357
- lines.push(`${names.join(', ')}`);
358
- }
359
- if (categorySkips.length > 3) {
360
- lines.push(`+${categorySkips.length - 3} more`);
361
- }
362
- lines.push('');
363
- });
364
- }
365
-
366
- if (lines.length > 1) {
367
- lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
368
- }
369
-
370
- return lines.join('\n');
371
- }
372
-
373
- /**
374
- * Print error clarity block to stdout
375
- *
376
- * @param {Array} failures - Failed attempts/flows
377
- * @param {Object} config - Guardian config
378
- * @param {Array} args - CLI arguments
379
- */
380
- function printErrorClarity(failures = [], config = {}, args = []) {
381
- const output = formatErrorClarity(failures, config, args);
382
- if (output && output.trim().length > 0) {
383
- console.log(output);
384
- }
385
- }
386
-
387
- module.exports = {
388
- ERROR_CATEGORIES,
389
- ERROR_MESSAGES,
390
- SKIP_MESSAGES,
391
- classifyError,
392
- getErrorInfo,
393
- shouldShowErrorClarity,
394
- groupFailuresByCategory,
395
- deduplicateErrors,
396
- isSkip,
397
- formatErrorClarity,
398
- printErrorClarity
399
- };
1
+ /**
2
+ * Error & Failure Messaging — Human-Readable Error Output
3
+ *
4
+ * Classifies execution failures into canonical categories and provides
5
+ * plain-language explanations with actionable next steps.
6
+ * Production-grade DX improvement for CLI output.
7
+ */
8
+
9
+ // Canonical error taxonomy (internal-only classification)
10
+ const ERROR_CATEGORIES = {
11
+ TIMEOUT: 'TIMEOUT',
12
+ ELEMENT_NOT_FOUND: 'ELEMENT_NOT_FOUND',
13
+ NAVIGATION_FAILED: 'NAVIGATION_FAILED',
14
+ AUTH_BLOCKED: 'AUTH_BLOCKED',
15
+ NOT_APPLICABLE: 'NOT_APPLICABLE',
16
+ DISABLED_BY_PRESET: 'DISABLED_BY_PRESET',
17
+ USER_FILTERED: 'USER_FILTERED',
18
+ MISSING_DEPENDENCY: 'MISSING_DEPENDENCY',
19
+ INFRA_ERROR: 'INFRA_ERROR',
20
+ UNKNOWN: 'UNKNOWN'
21
+ };
22
+
23
+ // Error message templates (centralized, not duplicated)
24
+ const ERROR_MESSAGES = {
25
+ TIMEOUT: {
26
+ title: 'Timeout waiting for interaction',
27
+ explanation: 'This step took longer than expected to complete. The page may have performance issues or the timeout setting may be too strict.',
28
+ action: 'Increase timeout setting or verify page performance.'
29
+ },
30
+ ELEMENT_NOT_FOUND: {
31
+ title: 'Expected element not found',
32
+ explanation: 'A critical UI element (button, form, link) was not present on the page. The page structure may have changed or the element is dynamically loaded.',
33
+ action: 'Verify the element selector is correct or wait for dynamic content to load.'
34
+ },
35
+ NAVIGATION_FAILED: {
36
+ title: 'Navigation failed',
37
+ explanation: 'Attempting to navigate to a URL resulted in an error. The page may be unavailable, blocked, or returned an error status.',
38
+ action: 'Check if the URL is accessible and returns a valid response (HTTP 200).'
39
+ },
40
+ AUTH_BLOCKED: {
41
+ title: 'Authentication blocked',
42
+ explanation: 'The test was blocked by authentication or access control. The credentials may be incorrect or the account may lack permissions.',
43
+ action: 'Verify credentials and ensure the test account has required permissions.'
44
+ },
45
+ NOT_APPLICABLE: {
46
+ title: 'Skipped (not applicable)',
47
+ explanation: 'This step is not applicable to this site based on its capabilities or configuration.',
48
+ action: 'This is expected behavior. No action required.'
49
+ },
50
+ DISABLED_BY_PRESET: {
51
+ title: 'Skipped (disabled by preset)',
52
+ explanation: 'This step is disabled in the selected testing preset. It can be re-enabled by choosing a different preset.',
53
+ action: 'Choose a different preset if you want to test this flow, or create a custom configuration.'
54
+ },
55
+ USER_FILTERED: {
56
+ title: 'Skipped (user filtered)',
57
+ explanation: 'This step was explicitly filtered out in your configuration.',
58
+ action: 'Update your configuration if you want to include this step in testing.'
59
+ },
60
+ MISSING_DEPENDENCY: {
61
+ title: 'Skipped (missing dependency)',
62
+ explanation: 'This step requires a previous step to pass, but that step did not execute or failed.',
63
+ action: 'Fix the blocking issue in the previous step, or run tests without dependencies.'
64
+ },
65
+ INFRA_ERROR: {
66
+ title: 'Infrastructure error',
67
+ explanation: 'A system-level issue occurred (browser launch failed, permissions error, etc.). This is not related to your site.',
68
+ action: 'Check system resources, permissions, and Guardian logs. Retry the test.'
69
+ },
70
+ UNKNOWN: {
71
+ title: 'Unexpected error',
72
+ explanation: 'An error occurred that does not fit common categories. See detailed logs for more information.',
73
+ action: 'Check Guardian logs with GUARDIAN_DEBUG=1 for full error details.'
74
+ }
75
+ };
76
+
77
+ // Skip messages (NOT errors, labeled clearly)
78
+ const SKIP_MESSAGES = {
79
+ DISABLED_BY_PRESET: 'Skipped by preset configuration',
80
+ NOT_APPLICABLE: 'Not applicable to this site',
81
+ USER_FILTERED: 'User-filtered from testing',
82
+ MISSING_DEPENDENCY: 'Skipped (blocking flow failed)'
83
+ };
84
+
85
+ /**
86
+ * Classify a raw error/failure into canonical category
87
+ *
88
+ * @param {Object} failure - Failure object with outcome, error, reason, etc.
89
+ * @returns {string} Canonical error category
90
+ */
91
+ function classifyError(failure = {}) {
92
+ const {
93
+ outcome,
94
+ reason,
95
+ failureReason,
96
+ message,
97
+ code
98
+ } = failure;
99
+
100
+ // Skip outcomes (NOT errors)
101
+ if (outcome === 'NOT_APPLICABLE') return ERROR_CATEGORIES.NOT_APPLICABLE;
102
+ if (outcome === 'SKIPPED') {
103
+ if (reason === 'DISABLED_BY_PRESET') return ERROR_CATEGORIES.DISABLED_BY_PRESET;
104
+ if (reason === 'USER_FILTERED') return ERROR_CATEGORIES.USER_FILTERED;
105
+ if (reason === 'MISSING_DEPENDENCY') return ERROR_CATEGORIES.MISSING_DEPENDENCY;
106
+ return ERROR_CATEGORIES.NOT_APPLICABLE;
107
+ }
108
+
109
+ // Timeout errors
110
+ if (failureReason === 'TIMEOUT' || reason === 'TIMEOUT') {
111
+ return ERROR_CATEGORIES.TIMEOUT;
112
+ }
113
+ if (message && message.toLowerCase().includes('timeout')) {
114
+ return ERROR_CATEGORIES.TIMEOUT;
115
+ }
116
+ if (code === 'TIMEOUT') {
117
+ return ERROR_CATEGORIES.TIMEOUT;
118
+ }
119
+
120
+ // Element not found
121
+ if (failureReason === 'ELEMENT_NOT_FOUND' || reason === 'ELEMENT_NOT_FOUND') {
122
+ return ERROR_CATEGORIES.ELEMENT_NOT_FOUND;
123
+ }
124
+ if (message && (message.toLowerCase().includes('not found') || message.toLowerCase().includes('selector'))) {
125
+ return ERROR_CATEGORIES.ELEMENT_NOT_FOUND;
126
+ }
127
+ if (code === 'ELEMENT_NOT_FOUND') {
128
+ return ERROR_CATEGORIES.ELEMENT_NOT_FOUND;
129
+ }
130
+
131
+ // Navigation failures
132
+ if (failureReason === 'NAVIGATION_FAILED' || reason === 'NAVIGATION_FAILED') {
133
+ return ERROR_CATEGORIES.NAVIGATION_FAILED;
134
+ }
135
+ if (message && (message.toLowerCase().includes('navigation') || message.toLowerCase().includes('net::err'))) {
136
+ return ERROR_CATEGORIES.NAVIGATION_FAILED;
137
+ }
138
+ if (code === 'NAVIGATION_FAILED') {
139
+ return ERROR_CATEGORIES.NAVIGATION_FAILED;
140
+ }
141
+
142
+ // Auth blocked
143
+ if (failureReason === 'AUTH_BLOCKED' || reason === 'AUTH_BLOCKED') {
144
+ return ERROR_CATEGORIES.AUTH_BLOCKED;
145
+ }
146
+ if (message && (message.toLowerCase().includes('unauthorized') || message.toLowerCase().includes('forbidden') || message.toLowerCase().includes('403') || message.toLowerCase().includes('401'))) {
147
+ return ERROR_CATEGORIES.AUTH_BLOCKED;
148
+ }
149
+ if (code === 'AUTH_BLOCKED') {
150
+ return ERROR_CATEGORIES.AUTH_BLOCKED;
151
+ }
152
+
153
+ // Infrastructure errors
154
+ if (failureReason === 'INFRA_ERROR' || reason === 'INFRA_ERROR') {
155
+ return ERROR_CATEGORIES.INFRA_ERROR;
156
+ }
157
+ if (message && (message.toLowerCase().includes('browser') || message.toLowerCase().includes('permission') || message.toLowerCase().includes('system'))) {
158
+ return ERROR_CATEGORIES.INFRA_ERROR;
159
+ }
160
+ if (code === 'BROWSER_LAUNCH_FAILED' || code === 'PERMISSION_DENIED') {
161
+ return ERROR_CATEGORIES.INFRA_ERROR;
162
+ }
163
+
164
+ // Missing dependency
165
+ if (failureReason === 'MISSING_DEPENDENCY' || reason === 'MISSING_DEPENDENCY') {
166
+ return ERROR_CATEGORIES.MISSING_DEPENDENCY;
167
+ }
168
+
169
+ // Default to unknown
170
+ return ERROR_CATEGORIES.UNKNOWN;
171
+ }
172
+
173
+ /**
174
+ * Extract human-friendly error info from failure
175
+ *
176
+ * @param {Object} failure - Failure with outcome, error details, etc.
177
+ * @returns {Object} { category, title, explanation, action }
178
+ */
179
+ function getErrorInfo(failure = {}) {
180
+ const category = classifyError(failure);
181
+ const template = ERROR_MESSAGES[category] || ERROR_MESSAGES[ERROR_CATEGORIES.UNKNOWN];
182
+
183
+ return {
184
+ category,
185
+ title: template.title,
186
+ explanation: template.explanation,
187
+ action: template.action
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Check if output should be shown
193
+ * Skip in quiet, CI, or non-TTY environments
194
+ *
195
+ * @param {Object} config - Guardian config
196
+ * @param {Array} args - CLI arguments
197
+ * @returns {boolean} true if should show error clarity
198
+ */
199
+ function shouldShowErrorClarity(config = {}, args = []) {
200
+ // Skip if --quiet or -q flag
201
+ if (args.includes('--quiet') || args.includes('-q')) {
202
+ return false;
203
+ }
204
+
205
+ // Skip if non-TTY (CI/automation without explicit output)
206
+ if (!process.stdout.isTTY) {
207
+ return false;
208
+ }
209
+
210
+ return true;
211
+ }
212
+
213
+ /**
214
+ * Group failures by category
215
+ *
216
+ * @param {Array} failures - Array of failed attempts/flows
217
+ * @returns {Object} Map of category -> [failures]
218
+ */
219
+ function groupFailuresByCategory(failures = []) {
220
+ const groups = {};
221
+
222
+ (failures || []).forEach(failure => {
223
+ const category = classifyError(failure);
224
+ if (!groups[category]) {
225
+ groups[category] = [];
226
+ }
227
+ groups[category].push(failure);
228
+ });
229
+
230
+ return groups;
231
+ }
232
+
233
+ /**
234
+ * Deduplicate similar errors within a category
235
+ *
236
+ * @param {Array} failures - Array of failures in same category
237
+ * @returns {Array} Deduplicated failures (max 3)
238
+ */
239
+ function deduplicateErrors(failures = []) {
240
+ const seen = new Set();
241
+ const deduplicated = [];
242
+
243
+ (failures || []).forEach(failure => {
244
+ const key = `${failure.attemptId || failure.name}`;
245
+ if (!seen.has(key) && deduplicated.length < 3) {
246
+ seen.add(key);
247
+ deduplicated.push(failure);
248
+ }
249
+ });
250
+
251
+ return deduplicated;
252
+ }
253
+
254
+ /**
255
+ * Check if this is a skip (not an error)
256
+ *
257
+ * @param {string} category - Error category
258
+ * @returns {boolean} true if this is a skip
259
+ */
260
+ function isSkip(category) {
261
+ return [
262
+ ERROR_CATEGORIES.NOT_APPLICABLE,
263
+ ERROR_CATEGORIES.DISABLED_BY_PRESET,
264
+ ERROR_CATEGORIES.USER_FILTERED,
265
+ ERROR_CATEGORIES.MISSING_DEPENDENCY
266
+ ].includes(category);
267
+ }
268
+
269
+ /**
270
+ * Format error clarity block for CLI output
271
+ *
272
+ * @param {Array} failures - Failed attempts/flows
273
+ * @param {Object} config - Guardian config
274
+ * @param {Array} args - CLI arguments
275
+ * @returns {string} Formatted error clarity block
276
+ */
277
+ function formatErrorClarity(failures = [], config = {}, args = []) {
278
+ if (!shouldShowErrorClarity(config, args)) {
279
+ return '';
280
+ }
281
+
282
+ if (!failures || failures.length === 0) {
283
+ return '';
284
+ }
285
+
286
+ const lines = [];
287
+ const groups = groupFailuresByCategory(failures);
288
+
289
+ // Separate actual errors from skips
290
+ const errors = {};
291
+ const skips = {};
292
+
293
+ Object.entries(groups).forEach(([category, categoryFailures]) => {
294
+ if (isSkip(category)) {
295
+ skips[category] = categoryFailures;
296
+ } else {
297
+ errors[category] = categoryFailures;
298
+ }
299
+ });
300
+
301
+ // Print errors section
302
+ const errorCategories = Object.keys(errors);
303
+ if (errorCategories.length > 0) {
304
+ lines.push('');
305
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
306
+ lines.push('FAILURES & ERRORS');
307
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
308
+ lines.push('');
309
+
310
+ errorCategories.forEach(category => {
311
+ const categoryFailures = errors[category];
312
+ const errorInfo = getErrorInfo(categoryFailures[0]);
313
+
314
+ lines.push(`${errorInfo.title}`);
315
+ lines.push('────────────────────────────────────────────────────────────');
316
+ lines.push(`${errorInfo.explanation}`);
317
+ lines.push(`Action: ${errorInfo.action}`);
318
+
319
+ // List affected flows/attempts (max 3)
320
+ const deduped = deduplicateErrors(categoryFailures);
321
+ const names = deduped
322
+ .map(f => f.attemptName || f.name || f.attemptId || 'unknown');
323
+
324
+ if (names.length > 0) {
325
+ lines.push(`Affected: ${names.join(', ')}`);
326
+ }
327
+
328
+ if (categoryFailures.length > 3) {
329
+ lines.push(`(+${categoryFailures.length - 3} more)`);
330
+ }
331
+
332
+ lines.push('');
333
+ });
334
+ }
335
+
336
+ // Print skips section
337
+ const skipCategories = Object.keys(skips);
338
+ if (skipCategories.length > 0) {
339
+ lines.push('');
340
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
341
+ lines.push('SKIPPED ATTEMPTS');
342
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
343
+ lines.push('');
344
+
345
+ skipCategories.forEach(category => {
346
+ const categorySkips = skips[category];
347
+ const skipReason = SKIP_MESSAGES[category] || 'Skipped';
348
+
349
+ // Count skips in this category
350
+ const names = deduplicateErrors(categorySkips)
351
+ .map(s => s.attemptName || s.name || s.attemptId || 'unknown')
352
+ .slice(0, 3);
353
+
354
+ lines.push(`${skipReason} (${categorySkips.length})`);
355
+ lines.push('────────────────────────────────────────────────────────────');
356
+ if (names.length > 0) {
357
+ lines.push(`${names.join(', ')}`);
358
+ }
359
+ if (categorySkips.length > 3) {
360
+ lines.push(`+${categorySkips.length - 3} more`);
361
+ }
362
+ lines.push('');
363
+ });
364
+ }
365
+
366
+ if (lines.length > 1) {
367
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
368
+ }
369
+
370
+ return lines.join('\n');
371
+ }
372
+
373
+ /**
374
+ * Print error clarity block to stdout
375
+ *
376
+ * @param {Array} failures - Failed attempts/flows
377
+ * @param {Object} config - Guardian config
378
+ * @param {Array} args - CLI arguments
379
+ */
380
+ function printErrorClarity(failures = [], config = {}, args = []) {
381
+ const output = formatErrorClarity(failures, config, args);
382
+ if (output && output.trim().length > 0) {
383
+ console.log(output);
384
+ }
385
+ }
386
+
387
+ module.exports = {
388
+ ERROR_CATEGORIES,
389
+ ERROR_MESSAGES,
390
+ SKIP_MESSAGES,
391
+ classifyError,
392
+ getErrorInfo,
393
+ shouldShowErrorClarity,
394
+ groupFailuresByCategory,
395
+ deduplicateErrors,
396
+ isSkip,
397
+ formatErrorClarity,
398
+ printErrorClarity
399
+ };