@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,431 +1,435 @@
1
- /**
2
- * Human Report Generator
3
- * Transforms journey results into human-readable summaries
4
- */
5
-
6
- const fs = require('fs');
7
- const path = require('path');
8
- const { analyzeFailure, recordSignature, getSignatureCount } = require('./failure-intelligence');
9
-
10
- class HumanReporter {
11
- constructor(options = {}) {
12
- this.options = options;
13
- }
14
-
15
- /**
16
- * Generate a complete human summary and save to file
17
- */
18
- generateSummary(journeyResult, outputDir) {
19
- const summary = this._buildSummary(journeyResult);
20
-
21
- // Save as both .txt and .md
22
- const txtPath = path.join(outputDir, 'summary.txt');
23
- const mdPath = path.join(outputDir, 'summary.md');
24
-
25
- fs.writeFileSync(txtPath, summary.text, 'utf8');
26
- fs.writeFileSync(mdPath, summary.markdown, 'utf8');
27
-
28
- return {
29
- text: txtPath,
30
- markdown: mdPath,
31
- content: summary.text
32
- };
33
- }
34
-
35
- /**
36
- * Generate JSON report for programmatic access
37
- */
38
- generateJSON(journeyResult, outputDir) {
39
- const { toCanonicalJourneyVerdict } = require('./verdicts');
40
- const decisionCanonical = toCanonicalJourneyVerdict(journeyResult.finalDecision);
41
- const report = {
42
- metadata: {
43
- timestamp: new Date().toISOString(),
44
- version: '1.0',
45
- journey: journeyResult.journey
46
- },
47
- target: {
48
- url: journeyResult.url,
49
- reachable: journeyResult.executedSteps.length > 0
50
- },
51
- intentDetection: journeyResult.intentDetection || { intent: 'unknown', confidence: 0, signals: [] },
52
- goal: journeyResult.goal || { goalReached: false, goalDescription: '' },
53
- baseline: journeyResult.baseline || null,
54
- drift: journeyResult.drift || { driftDetected: false, driftReasons: [] },
55
- execution: {
56
- totalSteps: journeyResult.executedSteps.length + journeyResult.failedSteps.length,
57
- succeededSteps: journeyResult.executedSteps.length,
58
- failedSteps: journeyResult.failedSteps.length,
59
- successRate: journeyResult.executedSteps.length > 0
60
- ? Math.round((journeyResult.executedSteps.length /
61
- (journeyResult.executedSteps.length + journeyResult.failedSteps.length)) * 100)
62
- : 0
63
- },
64
- classification: journeyResult.errorClassification || { type: 'UNKNOWN' },
65
- decision: journeyResult.finalDecision,
66
- decisionCanonical,
67
- reasoning: this._buildReasoning(journeyResult),
68
- impact: this._assessUserImpact(journeyResult),
69
- timing: {
70
- started: journeyResult.startedAt,
71
- ended: journeyResult.endedAt
72
- },
73
- details: {
74
- steps: journeyResult.executedSteps,
75
- failures: journeyResult.failedSteps,
76
- evidence: journeyResult.evidence
77
- }
78
- };
79
-
80
- // Failure intelligence section
81
- const info = analyzeFailure(journeyResult);
82
- const rec = recordSignature(journeyResult.url, info);
83
- report.failureInsights = {
84
- failureStage: info.failureStage,
85
- failureStepId: info.failureStepId,
86
- cause: info.cause,
87
- hint: info.hint,
88
- signature: rec.signature,
89
- occurrences: rec.count,
90
- };
91
-
92
- const reportPath = path.join(outputDir, 'report.json');
93
- fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8');
94
-
95
- return report;
96
- }
97
-
98
- /**
99
- * Build human text summary
100
- */
101
- _buildSummary(journeyResult) {
102
- const decision = journeyResult.finalDecision || 'UNKNOWN';
103
- const classification = journeyResult.errorClassification || {};
104
-
105
- const text = this._formatText(journeyResult);
106
- const markdown = this._formatMarkdown(journeyResult);
107
-
108
- return { text, markdown };
109
- }
110
-
111
- _formatText(journeyResult) {
112
- const { toCanonicalJourneyVerdict } = require('./verdicts');
113
- const canonical = toCanonicalJourneyVerdict(journeyResult.finalDecision);
114
- // Baseline compare
115
- if (journeyResult.baseline) {
116
- lines.push('BASELINE');
117
- lines.push('─────────────────────────────────────────────────────────────────');
118
- const b = journeyResult.baseline;
119
- lines.push(`Saved decision: ${b.decision}`);
120
- lines.push(`Saved intent: ${String(b.intent || 'unknown').toUpperCase()}`);
121
- lines.push(`Saved goal: ${b.goalReached ? 'Reached' : 'Not reached'}\n`);
122
-
123
- lines.push('CURRENT VS BASELINE');
124
- lines.push('─────────────────────────────────────────────────────────────────');
125
- const d = journeyResult.drift || { driftDetected: false, driftReasons: [] };
126
- if (d.driftDetected) {
127
- lines.push('Regression detected:');
128
- for (const r of d.driftReasons) lines.push(`– ${r}`);
129
- } else {
130
- lines.push('No regression detected');
131
- }
132
- lines.push();
133
- }
134
- const lines = [];
135
- const decision = canonical || 'DO_NOT_LAUNCH';
136
-
137
- lines.push('═══════════════════════════════════════════════════════════════');
138
- lines.push(' ODAVL GUARDIAN — JOURNEY REPORT');
139
- lines.push('═══════════════════════════════════════════════════════════════\n');
140
-
141
- lines.push(`DECISION: ${this._decisionEmoji(decision)} ${decision}`);
142
- lines.push(`Journey: ${journeyResult.journey || 'Unknown'}`);
143
- lines.push(`Target: ${journeyResult.url}\n`);
144
-
145
- // Intent detection
146
- if (journeyResult.intentDetection) {
147
- const id = journeyResult.intentDetection;
148
- lines.push('SITE TYPE');
149
- lines.push('─────────────────────────────────────────────────────────────────');
150
- lines.push(`Detected: ${String(id.intent || 'unknown').toUpperCase()} (confidence ${id.confidence || 0}%)`);
151
- lines.push(`Visitor Goal: ${this._intentToHumanGoal(id.intent)}`);
152
- const goal = journeyResult.goal || { goalReached: false, goalDescription: '' };
153
- lines.push(`Goal Reached: ${goal.goalReached ? 'Yes' : 'No'} — ${goal.goalDescription}\n`);
154
- }
155
-
156
- lines.push('EXECUTION SUMMARY');
157
- lines.push('─────────────────────────────────────────────────────────────────');
158
- lines.push(`Steps Executed: ${journeyResult.executedSteps.length}`);
159
- lines.push(`Steps Failed: ${journeyResult.failedSteps.length}`);
160
- lines.push(`Total Steps: ${journeyResult.executedSteps.length + journeyResult.failedSteps.length}`);
161
-
162
- if (journeyResult.executedSteps.length > 0) {
163
- const rate = Math.round((journeyResult.executedSteps.length /
164
- (journeyResult.executedSteps.length + journeyResult.failedSteps.length)) * 100);
165
- lines.push(`Success Rate: ${rate}%\n`);
166
- }
167
-
168
- lines.push('WHAT GUARDIAN TESTED');
169
- lines.push('─────────────────────────────────────────────────────────────────');
170
- const steps = journeyResult.executedSteps || [];
171
- for (let i = 0; i < steps.length; i++) {
172
- lines.push(`${i + 1}. ${steps[i].name || `Step ${i + 1}`}`);
173
- }
174
- lines.push();
175
-
176
- if (journeyResult.failedSteps && journeyResult.failedSteps.length > 0) {
177
- lines.push('WHAT FAILED');
178
- lines.push('─────────────────────────────────────────────────────────────────');
179
- for (const failure of journeyResult.failedSteps) {
180
- const step = journeyResult.executedSteps.find(s => s.id === failure);
181
- if (step) {
182
- lines.push(`✗ ${step.name || failure}: ${step.error || 'Unknown failure'}`);
183
- }
184
- }
185
- lines.push();
186
- }
187
-
188
- if (journeyResult.errorClassification) {
189
- lines.push('ERROR CLASSIFICATION');
190
- lines.push('─────────────────────────────────────────────────────────────────');
191
- lines.push(`Type: ${journeyResult.errorClassification.type || 'UNKNOWN'}`);
192
- lines.push(`Reason: ${journeyResult.errorClassification.reason || 'N/A'}\n`);
193
- }
194
-
195
- lines.push('DECISION REASONING');
196
- lines.push('─────────────────────────────────────────────────────────────────');
197
- lines.push(this._buildReasoning(journeyResult));
198
- lines.push();
199
-
200
- // Failure intelligence
201
- const fi = analyzeFailure(journeyResult);
202
- const occurrences = getSignatureCount(journeyResult.url, fi);
203
- lines.push('WHERE USERS STOP');
204
- lines.push('─────────────────────────────────────────────────────────────────');
205
- lines.push(`Stage: ${fi.failureStage}`);
206
- lines.push(`Step: ${fi.failureStepId ?? 'unknown'}`);
207
- lines.push();
208
-
209
- lines.push('WHY IT LIKELY HAPPENS');
210
- lines.push('─────────────────────────────────────────────────────────────────');
211
- lines.push(fi.cause);
212
- lines.push();
213
-
214
- lines.push('FIRST FIX TO TRY');
215
- lines.push('─────────────────────────────────────────────────────────────────');
216
- lines.push(fi.hint);
217
- lines.push(`\nThis failure pattern occurred ${occurrences} time(s).`);
218
- lines.push();
219
-
220
- lines.push('USER IMPACT');
221
- lines.push('─────────────────────────────────────────────────────────────────');
222
- lines.push(this._assessUserImpact(journeyResult));
223
- lines.push();
224
-
225
- lines.push('═══════════════════════════════════════════════════════════════');
226
- lines.push('RECOMMENDATION');
227
- lines.push('═══════════════════════════════════════════════════════════════');
228
- lines.push(this._getRecommendation(journeyResult));
229
- lines.push();
230
-
231
- return lines.join('\n');
232
- }
233
-
234
- _formatMarkdown(journeyResult) {
235
- const { toCanonicalJourneyVerdict } = require('./verdicts');
236
- const canonical = toCanonicalJourneyVerdict(journeyResult.finalDecision);
237
- if (journeyResult.baseline) {
238
- const b = journeyResult.baseline;
239
- const d = journeyResult.drift || { driftDetected: false, driftReasons: [] };
240
- lines.push(`## Baseline`);
241
- lines.push(`- Decision: ${b.decision}`);
242
- lines.push(`- Intent: ${String(b.intent || 'unknown').toUpperCase()}`);
243
- lines.push(`- Goal: ${b.goalReached ? 'Reached' : 'Not reached'}`);
244
- lines.push();
245
- lines.push(`## Current vs Baseline`);
246
- if (d.driftDetected) {
247
- lines.push('Regression detected:');
248
- for (const r of d.driftReasons) lines.push(`- ${r}`);
249
- } else {
250
- lines.push('No regression detected');
251
- }
252
- lines.push();
253
- }
254
- const lines = [];
255
- const decision = canonical || 'DO_NOT_LAUNCH';
256
-
257
- lines.push(`# ODAVL Guardian — Journey Report\n`);
258
-
259
- lines.push(`## Decision: ${this._decisionEmoji(decision)} **${decision}**\n`);
260
- lines.push(`- **Journey:** ${journeyResult.journey || 'Unknown'}`);
261
- lines.push(`- **Target:** ${journeyResult.url}`);
262
- lines.push(`- **Time:** ${journeyResult.startedAt}\n`);
263
-
264
- if (journeyResult.intentDetection) {
265
- const id = journeyResult.intentDetection;
266
- lines.push(`## Site Type`);
267
- lines.push(`- **Detected:** ${String(id.intent || 'unknown').toUpperCase()} (confidence ${id.confidence || 0}%)`);
268
- lines.push(`- **Visitor Goal:** ${this._intentToHumanGoal(id.intent)}`);
269
- const goal = journeyResult.goal || { goalReached: false, goalDescription: '' };
270
- lines.push(`- **Goal Reached:** ${goal.goalReached ? 'Yes' : 'No'} — ${goal.goalDescription}\n`);
271
- }
272
-
273
- lines.push(`## Execution Summary\n`);
274
- lines.push(`| Metric | Value |`);
275
- lines.push(`|--------|-------|`);
276
- lines.push(`| Steps Executed | ${journeyResult.executedSteps.length} |`);
277
- lines.push(`| Steps Failed | ${journeyResult.failedSteps.length} |`);
278
- const rate = journeyResult.executedSteps.length > 0
279
- ? Math.round((journeyResult.executedSteps.length /
280
- (journeyResult.executedSteps.length + journeyResult.failedSteps.length)) * 100)
281
- : 0;
282
- lines.push(`| Success Rate | ${rate}% |\n`);
283
-
284
- lines.push(`## What Guardian Tested\n`);
285
- const steps = journeyResult.executedSteps || [];
286
- for (let i = 0; i < steps.length; i++) {
287
- lines.push(`${i + 1}. ${steps[i].name || `Step ${i + 1}`}`);
288
- }
289
- lines.push();
290
-
291
- if (journeyResult.failedSteps?.length > 0) {
292
- lines.push(`## Failures\n`);
293
- for (const failure of journeyResult.failedSteps) {
294
- const step = journeyResult.executedSteps.find(s => s.id === failure);
295
- if (step) {
296
- lines.push(`- ✗ **${step.name || failure}**: ${step.error || 'Unknown failure'}`);
297
- }
298
- }
299
- lines.push();
300
- }
301
-
302
- if (journeyResult.errorClassification) {
303
- lines.push(`## Error Classification\n`);
304
- lines.push(`- **Type:** ${journeyResult.errorClassification.type || 'UNKNOWN'}`);
305
- lines.push(`- **Reason:** ${journeyResult.errorClassification.reason || 'N/A'}\n`);
306
- }
307
-
308
- lines.push(`## Reasoning\n`);
309
- lines.push(this._buildReasoning(journeyResult));
310
- lines.push();
311
-
312
- lines.push(`## User Impact\n`);
313
- lines.push(this._assessUserImpact(journeyResult));
314
- lines.push();
315
-
316
- const fi = analyzeFailure(journeyResult);
317
- const occurrences = getSignatureCount(journeyResult.url, fi);
318
- lines.push(`## Where Users Stop`);
319
- lines.push(`- **Stage:** ${fi.failureStage}`);
320
- lines.push(`- **Step:** ${fi.failureStepId ?? 'unknown'}`);
321
- lines.push();
322
- lines.push(`## Why It Likely Happens`);
323
- lines.push(`- ${fi.cause}`);
324
- lines.push();
325
- lines.push(`## First Fix To Try`);
326
- lines.push(`- ${fi.hint}`);
327
- lines.push();
328
- lines.push(`> This failure pattern occurred ${occurrences} time(s).`);
329
- lines.push();
330
-
331
- lines.push(`## Recommendation\n`);
332
- lines.push(this._getRecommendation(journeyResult));
333
- lines.push();
334
-
335
- return lines.join('\n');
336
- }
337
-
338
- _decisionEmoji(decision) {
339
- const map = {
340
- 'READY': '✅',
341
- 'FRICTION': '⚠️ ',
342
- 'DO_NOT_LAUNCH': '🚫'
343
- };
344
- return map[decision] || '';
345
- }
346
-
347
- _buildReasoning(journeyResult) {
348
- const decision = journeyResult.finalDecision;
349
- const executed = journeyResult.executedSteps?.length || 0;
350
- const failed = journeyResult.failedSteps?.length || 0;
351
- const goalReached = journeyResult.goal?.goalReached === true;
352
-
353
- if (decision === 'SAFE') {
354
- return `All ${executed} steps completed, and the visitor goal was reached. Journey is fully functional.`;
355
- }
356
-
357
- if (decision === 'RISK') {
358
- if (failed === 0 && !goalReached) {
359
- return `Journey steps succeeded, but the visitor goal was not reached. Conversion risk exists.`;
360
- }
361
- return `${executed} of ${executed + failed} steps succeeded (${Math.round((executed / (executed + failed)) * 100)}%). Some parts of the critical journey work, but risks exist.`;
362
- }
363
-
364
- if (decision === 'DO_NOT_LAUNCH') {
365
- if (journeyResult.fatalError) {
366
- return `Site is unreachable or blocked. Cannot complete user journey at all. Error: ${journeyResult.fatalError}`;
367
- }
368
- return `Journey failed completely (0/${executed + failed} steps succeeded). Site or critical elements are broken. Do not launch.`;
369
- }
370
-
371
- return 'Unable to determine outcome from results.';
372
- }
373
-
374
- _assessUserImpact(journeyResult) {
375
- const classification = journeyResult.errorClassification?.type;
376
-
377
- if (journeyResult.finalDecision === 'SAFE') {
378
- return 'Visitors will successfully complete the critical user journey. No blockers detected.';
379
- }
380
-
381
- if (classification === 'CTA_NOT_FOUND') {
382
- return 'Visitors cannot find the key conversion element. Sign-up/checkout CTA is missing or inaccessible.';
383
- }
384
-
385
- if (classification === 'NAVIGATION_BLOCKED') {
386
- return 'Visitors cannot navigate to critical pages. Internal navigation is broken.';
387
- }
388
-
389
- if (classification === 'SITE_UNREACHABLE') {
390
- return 'Site is entirely unreachable. Visitors cannot access the website at all.';
391
- }
392
-
393
- if (journeyResult.finalDecision === 'RISK') {
394
- return 'Some steps work but the journey is incomplete. Visitors may struggle to convert.';
395
- }
396
-
397
- return 'Site has critical failures that impact user conversion.';
398
- }
399
-
400
- _getRecommendation(journeyResult) {
401
- const decision = journeyResult.finalDecision;
402
-
403
- if (decision === 'SAFE') {
404
- return '✅ **READY TO LAUNCH** — Critical journey is fully functional. Monitor for regressions.';
405
- }
406
-
407
- if (decision === 'RISK') {
408
- if (journeyResult.goal?.goalReached === false && journeyResult.failedSteps?.length === 0) {
409
- return '⚠️ **LAUNCH WITH CAUTION** — Help visitors reach the goal (signup, checkout, contact) before launch.';
410
- }
411
- return '⚠️ **LAUNCH WITH CAUTION** — Fix identified failures before launch. Test thoroughly.';
412
- }
413
-
414
- if (decision === 'DO_NOT_LAUNCH') {
415
- return '🚫 **DO NOT LAUNCH** — Critical failures detected. Fix issues before attempting deployment.';
416
- }
417
-
418
- return 'Unable to make recommendation.';
419
- }
420
-
421
- _intentToHumanGoal(intent) {
422
- switch (intent) {
423
- case 'saas': return 'Sign up or view pricing';
424
- case 'shop': return 'Add to cart or begin checkout';
425
- case 'landing': return 'Send a message or reach contact section';
426
- default: return 'Find a clear action and proceed';
427
- }
428
- }
429
- }
430
-
431
- module.exports = { HumanReporter };
1
+ /**
2
+ * Human Report Generator
3
+ * Transforms journey results into human-readable summaries
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { analyzeFailure, recordSignature, getSignatureCount } = require('./failure-intelligence');
9
+
10
+ class HumanReporter {
11
+ constructor(options = {}) {
12
+ this.options = options;
13
+ }
14
+
15
+ /**
16
+ * Generate a complete human summary and save to file
17
+ */
18
+ generateSummary(journeyResult, outputDir) {
19
+ const summary = this._buildSummary(journeyResult);
20
+
21
+ // Save as both .txt and .md
22
+ const txtPath = path.join(outputDir, 'summary.txt');
23
+ const mdPath = path.join(outputDir, 'summary.md');
24
+
25
+ fs.writeFileSync(txtPath, summary.text, 'utf8');
26
+ fs.writeFileSync(mdPath, summary.markdown, 'utf8');
27
+
28
+ return {
29
+ text: txtPath,
30
+ markdown: mdPath,
31
+ content: summary.text
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Generate JSON report for programmatic access
37
+ */
38
+ generateJSON(journeyResult, outputDir) {
39
+ const { toCanonicalJourneyVerdict } = require('./verdicts');
40
+ const decisionCanonical = toCanonicalJourneyVerdict(journeyResult.finalDecision);
41
+ const report = {
42
+ metadata: {
43
+ timestamp: new Date().toISOString(),
44
+ version: '1.0',
45
+ journey: journeyResult.journey
46
+ },
47
+ target: {
48
+ url: journeyResult.url,
49
+ reachable: journeyResult.executedSteps.length > 0
50
+ },
51
+ intentDetection: journeyResult.intentDetection || { intent: 'unknown', confidence: 0, signals: [] },
52
+ goal: journeyResult.goal || { goalReached: false, goalDescription: '' },
53
+ baseline: journeyResult.baseline || null,
54
+ drift: journeyResult.drift || { driftDetected: false, driftReasons: [] },
55
+ execution: {
56
+ totalSteps: journeyResult.executedSteps.length + journeyResult.failedSteps.length,
57
+ succeededSteps: journeyResult.executedSteps.length,
58
+ failedSteps: journeyResult.failedSteps.length,
59
+ successRate: journeyResult.executedSteps.length > 0
60
+ ? Math.round((journeyResult.executedSteps.length /
61
+ (journeyResult.executedSteps.length + journeyResult.failedSteps.length)) * 100)
62
+ : 0
63
+ },
64
+ classification: journeyResult.errorClassification || { type: 'UNKNOWN' },
65
+ decision: journeyResult.finalDecision,
66
+ decisionCanonical,
67
+ reasoning: this._buildReasoning(journeyResult),
68
+ impact: this._assessUserImpact(journeyResult),
69
+ timing: {
70
+ started: journeyResult.startedAt,
71
+ ended: journeyResult.endedAt
72
+ },
73
+ details: {
74
+ steps: journeyResult.executedSteps,
75
+ failures: journeyResult.failedSteps,
76
+ evidence: journeyResult.evidence
77
+ }
78
+ };
79
+
80
+ // Failure intelligence section
81
+ const info = analyzeFailure(journeyResult);
82
+ const rec = recordSignature(journeyResult.url, info);
83
+ report.failureInsights = {
84
+ failureStage: info.failureStage,
85
+ failureStepId: info.failureStepId,
86
+ cause: info.cause,
87
+ hint: info.hint,
88
+ signature: rec.signature,
89
+ occurrences: rec.count,
90
+ };
91
+
92
+ const reportPath = path.join(outputDir, 'report.json');
93
+ fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8');
94
+
95
+ return report;
96
+ }
97
+
98
+ /**
99
+ * Build human text summary
100
+ */
101
+ _buildSummary(journeyResult) {
102
+ // Legacy code - decision and classification were used in earlier versions
103
+ // Keeping structure for potential future use
104
+ // eslint-disable-next-line no-unused-vars
105
+ const decision = journeyResult.finalDecision || 'UNKNOWN';
106
+ // eslint-disable-next-line no-unused-vars
107
+ const classification = journeyResult.errorClassification || {};
108
+
109
+ const text = this._formatText(journeyResult);
110
+ const markdown = this._formatMarkdown(journeyResult);
111
+
112
+ return { text, markdown };
113
+ }
114
+
115
+ _formatText(journeyResult) {
116
+ const { toCanonicalJourneyVerdict } = require('./verdicts');
117
+ const canonical = toCanonicalJourneyVerdict(journeyResult.finalDecision);
118
+ // Baseline compare
119
+ if (journeyResult.baseline) {
120
+ lines.push('BASELINE');
121
+ lines.push('─────────────────────────────────────────────────────────────────');
122
+ const b = journeyResult.baseline;
123
+ lines.push(`Saved decision: ${b.decision}`);
124
+ lines.push(`Saved intent: ${String(b.intent || 'unknown').toUpperCase()}`);
125
+ lines.push(`Saved goal: ${b.goalReached ? 'Reached' : 'Not reached'}\n`);
126
+
127
+ lines.push('CURRENT VS BASELINE');
128
+ lines.push('─────────────────────────────────────────────────────────────────');
129
+ const d = journeyResult.drift || { driftDetected: false, driftReasons: [] };
130
+ if (d.driftDetected) {
131
+ lines.push('Regression detected:');
132
+ for (const r of d.driftReasons) lines.push(`– ${r}`);
133
+ } else {
134
+ lines.push('No regression detected');
135
+ }
136
+ lines.push();
137
+ }
138
+ const lines = [];
139
+ const decision = canonical || 'DO_NOT_LAUNCH';
140
+
141
+ lines.push('═══════════════════════════════════════════════════════════════');
142
+ lines.push(' ODAVL GUARDIAN JOURNEY REPORT');
143
+ lines.push('═══════════════════════════════════════════════════════════════\n');
144
+
145
+ lines.push(`DECISION: ${this._decisionEmoji(decision)} ${decision}`);
146
+ lines.push(`Journey: ${journeyResult.journey || 'Unknown'}`);
147
+ lines.push(`Target: ${journeyResult.url}\n`);
148
+
149
+ // Intent detection
150
+ if (journeyResult.intentDetection) {
151
+ const id = journeyResult.intentDetection;
152
+ lines.push('SITE TYPE');
153
+ lines.push('─────────────────────────────────────────────────────────────────');
154
+ lines.push(`Detected: ${String(id.intent || 'unknown').toUpperCase()} (confidence ${id.confidence || 0}%)`);
155
+ lines.push(`Visitor Goal: ${this._intentToHumanGoal(id.intent)}`);
156
+ const goal = journeyResult.goal || { goalReached: false, goalDescription: '' };
157
+ lines.push(`Goal Reached: ${goal.goalReached ? 'Yes' : 'No'} — ${goal.goalDescription}\n`);
158
+ }
159
+
160
+ lines.push('EXECUTION SUMMARY');
161
+ lines.push('─────────────────────────────────────────────────────────────────');
162
+ lines.push(`Steps Executed: ${journeyResult.executedSteps.length}`);
163
+ lines.push(`Steps Failed: ${journeyResult.failedSteps.length}`);
164
+ lines.push(`Total Steps: ${journeyResult.executedSteps.length + journeyResult.failedSteps.length}`);
165
+
166
+ if (journeyResult.executedSteps.length > 0) {
167
+ const rate = Math.round((journeyResult.executedSteps.length /
168
+ (journeyResult.executedSteps.length + journeyResult.failedSteps.length)) * 100);
169
+ lines.push(`Success Rate: ${rate}%\n`);
170
+ }
171
+
172
+ lines.push('WHAT GUARDIAN TESTED');
173
+ lines.push('─────────────────────────────────────────────────────────────────');
174
+ const steps = journeyResult.executedSteps || [];
175
+ for (let i = 0; i < steps.length; i++) {
176
+ lines.push(`${i + 1}. ${steps[i].name || `Step ${i + 1}`}`);
177
+ }
178
+ lines.push();
179
+
180
+ if (journeyResult.failedSteps && journeyResult.failedSteps.length > 0) {
181
+ lines.push('WHAT FAILED');
182
+ lines.push('─────────────────────────────────────────────────────────────────');
183
+ for (const failure of journeyResult.failedSteps) {
184
+ const step = journeyResult.executedSteps.find(s => s.id === failure);
185
+ if (step) {
186
+ lines.push(`✗ ${step.name || failure}: ${step.error || 'Unknown failure'}`);
187
+ }
188
+ }
189
+ lines.push();
190
+ }
191
+
192
+ if (journeyResult.errorClassification) {
193
+ lines.push('ERROR CLASSIFICATION');
194
+ lines.push('─────────────────────────────────────────────────────────────────');
195
+ lines.push(`Type: ${journeyResult.errorClassification.type || 'UNKNOWN'}`);
196
+ lines.push(`Reason: ${journeyResult.errorClassification.reason || 'N/A'}\n`);
197
+ }
198
+
199
+ lines.push('DECISION REASONING');
200
+ lines.push('─────────────────────────────────────────────────────────────────');
201
+ lines.push(this._buildReasoning(journeyResult));
202
+ lines.push();
203
+
204
+ // Failure intelligence
205
+ const fi = analyzeFailure(journeyResult);
206
+ const occurrences = getSignatureCount(journeyResult.url, fi);
207
+ lines.push('WHERE USERS STOP');
208
+ lines.push('─────────────────────────────────────────────────────────────────');
209
+ lines.push(`Stage: ${fi.failureStage}`);
210
+ lines.push(`Step: ${fi.failureStepId ?? 'unknown'}`);
211
+ lines.push();
212
+
213
+ lines.push('WHY IT LIKELY HAPPENS');
214
+ lines.push('─────────────────────────────────────────────────────────────────');
215
+ lines.push(fi.cause);
216
+ lines.push();
217
+
218
+ lines.push('FIRST FIX TO TRY');
219
+ lines.push('─────────────────────────────────────────────────────────────────');
220
+ lines.push(fi.hint);
221
+ lines.push(`\nThis failure pattern occurred ${occurrences} time(s).`);
222
+ lines.push();
223
+
224
+ lines.push('USER IMPACT');
225
+ lines.push('─────────────────────────────────────────────────────────────────');
226
+ lines.push(this._assessUserImpact(journeyResult));
227
+ lines.push();
228
+
229
+ lines.push('═══════════════════════════════════════════════════════════════');
230
+ lines.push('RECOMMENDATION');
231
+ lines.push('═══════════════════════════════════════════════════════════════');
232
+ lines.push(this._getRecommendation(journeyResult));
233
+ lines.push();
234
+
235
+ return lines.join('\n');
236
+ }
237
+
238
+ _formatMarkdown(journeyResult) {
239
+ const { toCanonicalJourneyVerdict } = require('./verdicts');
240
+ const canonical = toCanonicalJourneyVerdict(journeyResult.finalDecision);
241
+ if (journeyResult.baseline) {
242
+ const b = journeyResult.baseline;
243
+ const d = journeyResult.drift || { driftDetected: false, driftReasons: [] };
244
+ lines.push(`## Baseline`);
245
+ lines.push(`- Decision: ${b.decision}`);
246
+ lines.push(`- Intent: ${String(b.intent || 'unknown').toUpperCase()}`);
247
+ lines.push(`- Goal: ${b.goalReached ? 'Reached' : 'Not reached'}`);
248
+ lines.push();
249
+ lines.push(`## Current vs Baseline`);
250
+ if (d.driftDetected) {
251
+ lines.push('Regression detected:');
252
+ for (const r of d.driftReasons) lines.push(`- ${r}`);
253
+ } else {
254
+ lines.push('No regression detected');
255
+ }
256
+ lines.push();
257
+ }
258
+ const lines = [];
259
+ const decision = canonical || 'DO_NOT_LAUNCH';
260
+
261
+ lines.push(`# ODAVL Guardian — Journey Report\n`);
262
+
263
+ lines.push(`## Decision: ${this._decisionEmoji(decision)} **${decision}**\n`);
264
+ lines.push(`- **Journey:** ${journeyResult.journey || 'Unknown'}`);
265
+ lines.push(`- **Target:** ${journeyResult.url}`);
266
+ lines.push(`- **Time:** ${journeyResult.startedAt}\n`);
267
+
268
+ if (journeyResult.intentDetection) {
269
+ const id = journeyResult.intentDetection;
270
+ lines.push(`## Site Type`);
271
+ lines.push(`- **Detected:** ${String(id.intent || 'unknown').toUpperCase()} (confidence ${id.confidence || 0}%)`);
272
+ lines.push(`- **Visitor Goal:** ${this._intentToHumanGoal(id.intent)}`);
273
+ const goal = journeyResult.goal || { goalReached: false, goalDescription: '' };
274
+ lines.push(`- **Goal Reached:** ${goal.goalReached ? 'Yes' : 'No'} — ${goal.goalDescription}\n`);
275
+ }
276
+
277
+ lines.push(`## Execution Summary\n`);
278
+ lines.push(`| Metric | Value |`);
279
+ lines.push(`|--------|-------|`);
280
+ lines.push(`| Steps Executed | ${journeyResult.executedSteps.length} |`);
281
+ lines.push(`| Steps Failed | ${journeyResult.failedSteps.length} |`);
282
+ const rate = journeyResult.executedSteps.length > 0
283
+ ? Math.round((journeyResult.executedSteps.length /
284
+ (journeyResult.executedSteps.length + journeyResult.failedSteps.length)) * 100)
285
+ : 0;
286
+ lines.push(`| Success Rate | ${rate}% |\n`);
287
+
288
+ lines.push(`## What Guardian Tested\n`);
289
+ const steps = journeyResult.executedSteps || [];
290
+ for (let i = 0; i < steps.length; i++) {
291
+ lines.push(`${i + 1}. ${steps[i].name || `Step ${i + 1}`}`);
292
+ }
293
+ lines.push();
294
+
295
+ if (journeyResult.failedSteps?.length > 0) {
296
+ lines.push(`## Failures\n`);
297
+ for (const failure of journeyResult.failedSteps) {
298
+ const step = journeyResult.executedSteps.find(s => s.id === failure);
299
+ if (step) {
300
+ lines.push(`- ✗ **${step.name || failure}**: ${step.error || 'Unknown failure'}`);
301
+ }
302
+ }
303
+ lines.push();
304
+ }
305
+
306
+ if (journeyResult.errorClassification) {
307
+ lines.push(`## Error Classification\n`);
308
+ lines.push(`- **Type:** ${journeyResult.errorClassification.type || 'UNKNOWN'}`);
309
+ lines.push(`- **Reason:** ${journeyResult.errorClassification.reason || 'N/A'}\n`);
310
+ }
311
+
312
+ lines.push(`## Reasoning\n`);
313
+ lines.push(this._buildReasoning(journeyResult));
314
+ lines.push();
315
+
316
+ lines.push(`## User Impact\n`);
317
+ lines.push(this._assessUserImpact(journeyResult));
318
+ lines.push();
319
+
320
+ const fi = analyzeFailure(journeyResult);
321
+ const occurrences = getSignatureCount(journeyResult.url, fi);
322
+ lines.push(`## Where Users Stop`);
323
+ lines.push(`- **Stage:** ${fi.failureStage}`);
324
+ lines.push(`- **Step:** ${fi.failureStepId ?? 'unknown'}`);
325
+ lines.push();
326
+ lines.push(`## Why It Likely Happens`);
327
+ lines.push(`- ${fi.cause}`);
328
+ lines.push();
329
+ lines.push(`## First Fix To Try`);
330
+ lines.push(`- ${fi.hint}`);
331
+ lines.push();
332
+ lines.push(`> This failure pattern occurred ${occurrences} time(s).`);
333
+ lines.push();
334
+
335
+ lines.push(`## Recommendation\n`);
336
+ lines.push(this._getRecommendation(journeyResult));
337
+ lines.push();
338
+
339
+ return lines.join('\n');
340
+ }
341
+
342
+ _decisionEmoji(decision) {
343
+ const map = {
344
+ 'READY': '',
345
+ 'FRICTION': '⚠️ ',
346
+ 'DO_NOT_LAUNCH': '🚫'
347
+ };
348
+ return map[decision] || '❓';
349
+ }
350
+
351
+ _buildReasoning(journeyResult) {
352
+ const decision = journeyResult.finalDecision;
353
+ const executed = journeyResult.executedSteps?.length || 0;
354
+ const failed = journeyResult.failedSteps?.length || 0;
355
+ const goalReached = journeyResult.goal?.goalReached === true;
356
+
357
+ if (decision === 'SAFE') {
358
+ return `All ${executed} steps completed, and the visitor goal was reached. Journey is fully functional.`;
359
+ }
360
+
361
+ if (decision === 'RISK') {
362
+ if (failed === 0 && !goalReached) {
363
+ return `Journey steps succeeded, but the visitor goal was not reached. Conversion risk exists.`;
364
+ }
365
+ return `${executed} of ${executed + failed} steps succeeded (${Math.round((executed / (executed + failed)) * 100)}%). Some parts of the critical journey work, but risks exist.`;
366
+ }
367
+
368
+ if (decision === 'DO_NOT_LAUNCH') {
369
+ if (journeyResult.fatalError) {
370
+ return `Site is unreachable or blocked. Cannot complete user journey at all. Error: ${journeyResult.fatalError}`;
371
+ }
372
+ return `Journey failed completely (0/${executed + failed} steps succeeded). Site or critical elements are broken. Do not launch.`;
373
+ }
374
+
375
+ return 'Unable to determine outcome from results.';
376
+ }
377
+
378
+ _assessUserImpact(journeyResult) {
379
+ const classification = journeyResult.errorClassification?.type;
380
+
381
+ if (journeyResult.finalDecision === 'SAFE') {
382
+ return 'Visitors will successfully complete the critical user journey. No blockers detected.';
383
+ }
384
+
385
+ if (classification === 'CTA_NOT_FOUND') {
386
+ return 'Visitors cannot find the key conversion element. Sign-up/checkout CTA is missing or inaccessible.';
387
+ }
388
+
389
+ if (classification === 'NAVIGATION_BLOCKED') {
390
+ return 'Visitors cannot navigate to critical pages. Internal navigation is broken.';
391
+ }
392
+
393
+ if (classification === 'SITE_UNREACHABLE') {
394
+ return 'Site is entirely unreachable. Visitors cannot access the website at all.';
395
+ }
396
+
397
+ if (journeyResult.finalDecision === 'RISK') {
398
+ return 'Some steps work but the journey is incomplete. Visitors may struggle to convert.';
399
+ }
400
+
401
+ return 'Site has critical failures that impact user conversion.';
402
+ }
403
+
404
+ _getRecommendation(journeyResult) {
405
+ const decision = journeyResult.finalDecision;
406
+
407
+ if (decision === 'SAFE') {
408
+ return '✅ **READY TO LAUNCH** — Critical journey is fully functional. Monitor for regressions.';
409
+ }
410
+
411
+ if (decision === 'RISK') {
412
+ if (journeyResult.goal?.goalReached === false && journeyResult.failedSteps?.length === 0) {
413
+ return '⚠️ **LAUNCH WITH CAUTION** — Help visitors reach the goal (signup, checkout, contact) before launch.';
414
+ }
415
+ return '⚠️ **LAUNCH WITH CAUTION** — Fix identified failures before launch. Test thoroughly.';
416
+ }
417
+
418
+ if (decision === 'DO_NOT_LAUNCH') {
419
+ return '🚫 **DO NOT LAUNCH** — Critical failures detected. Fix issues before attempting deployment.';
420
+ }
421
+
422
+ return 'Unable to make recommendation.';
423
+ }
424
+
425
+ _intentToHumanGoal(intent) {
426
+ switch (intent) {
427
+ case 'saas': return 'Sign up or view pricing';
428
+ case 'shop': return 'Add to cart or begin checkout';
429
+ case 'landing': return 'Send a message or reach contact section';
430
+ default: return 'Find a clear action and proceed';
431
+ }
432
+ }
433
+ }
434
+
435
+ module.exports = { HumanReporter };