@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,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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
lines.push(
|
|
121
|
-
lines.push(
|
|
122
|
-
|
|
123
|
-
lines.push(
|
|
124
|
-
lines.push('
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
lines
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
lines.push(
|
|
142
|
-
lines.push(
|
|
143
|
-
lines.push(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
lines.push(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
lines.push(
|
|
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
|
-
lines.push(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
lines.push();
|
|
208
|
-
|
|
209
|
-
lines.push(
|
|
210
|
-
lines.push('
|
|
211
|
-
lines.push(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
lines.push('
|
|
215
|
-
lines.push(
|
|
216
|
-
lines.push(
|
|
217
|
-
|
|
218
|
-
lines.push();
|
|
219
|
-
|
|
220
|
-
lines.push(
|
|
221
|
-
lines.push(
|
|
222
|
-
lines.push(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
lines.push('
|
|
226
|
-
lines.push(
|
|
227
|
-
lines.push(
|
|
228
|
-
|
|
229
|
-
lines.push();
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
lines.push();
|
|
245
|
-
lines.push(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
lines.push(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
lines.push(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
lines.push(
|
|
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
|
-
lines.push(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
lines.push(`##
|
|
313
|
-
lines.push(this.
|
|
314
|
-
lines.push();
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
lines.push(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
lines.push(`##
|
|
323
|
-
lines.push(`- ${fi.
|
|
324
|
-
lines.push();
|
|
325
|
-
lines.push(
|
|
326
|
-
lines.push(
|
|
327
|
-
lines.push();
|
|
328
|
-
lines.push(
|
|
329
|
-
lines.push();
|
|
330
|
-
|
|
331
|
-
lines.push(
|
|
332
|
-
lines.push(
|
|
333
|
-
lines.push();
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (decision === '
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (
|
|
382
|
-
return 'Visitors
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (classification === '
|
|
386
|
-
return 'Visitors cannot
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (classification === '
|
|
390
|
-
return '
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (
|
|
394
|
-
return '
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (decision === '
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
return '
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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 };
|