@odavl/guardian 0.1.0-rc1 → 1.0.0

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 (101) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +155 -97
  3. package/bin/guardian.js +1544 -55
  4. package/config/README.md +59 -0
  5. package/config/profiles/landing-demo.yaml +16 -0
  6. package/package.json +26 -11
  7. package/policies/landing-demo.json +22 -0
  8. package/src/enterprise/audit-logger.js +166 -0
  9. package/src/enterprise/pdf-exporter.js +267 -0
  10. package/src/enterprise/rbac-gate.js +142 -0
  11. package/src/enterprise/rbac.js +239 -0
  12. package/src/enterprise/site-manager.js +180 -0
  13. package/src/founder/feedback-system.js +156 -0
  14. package/src/founder/founder-tracker.js +213 -0
  15. package/src/founder/usage-signals.js +141 -0
  16. package/src/guardian/alert-ledger.js +121 -0
  17. package/src/guardian/attempt-engine.js +587 -12
  18. package/src/guardian/attempt-registry.js +42 -1
  19. package/src/guardian/attempt-relevance.js +106 -0
  20. package/src/guardian/attempt.js +85 -39
  21. package/src/guardian/attempts-filter.js +63 -0
  22. package/src/guardian/baseline.js +50 -8
  23. package/src/guardian/breakage-intelligence.js +1 -0
  24. package/src/guardian/browser-pool.js +131 -0
  25. package/src/guardian/browser.js +28 -1
  26. package/src/guardian/ci-cli.js +121 -0
  27. package/src/guardian/ci-mode.js +15 -0
  28. package/src/guardian/ci-output.js +38 -0
  29. package/src/guardian/cli-summary.js +167 -67
  30. package/src/guardian/config-loader.js +162 -0
  31. package/src/guardian/data-guardian-detector.js +189 -0
  32. package/src/guardian/detection-layers.js +271 -0
  33. package/src/guardian/drift-detector.js +100 -0
  34. package/src/guardian/enhanced-html-reporter.js +221 -4
  35. package/src/guardian/env-guard.js +127 -0
  36. package/src/guardian/failure-intelligence.js +173 -0
  37. package/src/guardian/first-run-profile.js +89 -0
  38. package/src/guardian/first-run.js +54 -0
  39. package/src/guardian/flag-validator.js +111 -0
  40. package/src/guardian/flow-executor.js +309 -44
  41. package/src/guardian/html-reporter.js +2 -0
  42. package/src/guardian/human-reporter.js +431 -0
  43. package/src/guardian/index.js +22 -19
  44. package/src/guardian/init-command.js +9 -5
  45. package/src/guardian/intent-detector.js +146 -0
  46. package/src/guardian/journey-definitions.js +132 -0
  47. package/src/guardian/journey-scan-cli.js +145 -0
  48. package/src/guardian/journey-scanner.js +583 -0
  49. package/src/guardian/junit-reporter.js +18 -1
  50. package/src/guardian/language-detection.js +99 -0
  51. package/src/guardian/live-cli.js +95 -0
  52. package/src/guardian/live-scheduler-runner.js +137 -0
  53. package/src/guardian/live-scheduler.js +146 -0
  54. package/src/guardian/market-reporter.js +357 -82
  55. package/src/guardian/parallel-executor.js +116 -0
  56. package/src/guardian/pattern-analyzer.js +348 -0
  57. package/src/guardian/policy.js +80 -3
  58. package/src/guardian/prerequisite-checker.js +101 -0
  59. package/src/guardian/preset-loader.js +27 -18
  60. package/src/guardian/profile-loader.js +96 -0
  61. package/src/guardian/reality.js +1612 -115
  62. package/src/guardian/reporter.js +27 -41
  63. package/src/guardian/run-artifacts.js +212 -0
  64. package/src/guardian/run-cleanup.js +207 -0
  65. package/src/guardian/run-latest.js +90 -0
  66. package/src/guardian/run-list.js +211 -0
  67. package/src/guardian/run-summary.js +20 -0
  68. package/src/guardian/scan-presets.js +100 -11
  69. package/src/guardian/selector-fallbacks.js +394 -0
  70. package/src/guardian/semantic-contact-detection.js +255 -0
  71. package/src/guardian/semantic-contact-finder.js +201 -0
  72. package/src/guardian/semantic-targets.js +234 -0
  73. package/src/guardian/site-introspection.js +257 -0
  74. package/src/guardian/smoke.js +258 -0
  75. package/src/guardian/snapshot-schema.js +25 -1
  76. package/src/guardian/snapshot.js +69 -3
  77. package/src/guardian/stability-scorer.js +169 -0
  78. package/src/guardian/success-evaluator.js +214 -0
  79. package/src/guardian/template-command.js +184 -0
  80. package/src/guardian/text-formatters.js +426 -0
  81. package/src/guardian/timeout-profiles.js +57 -0
  82. package/src/guardian/verdict.js +320 -0
  83. package/src/guardian/verdicts.js +74 -0
  84. package/src/guardian/wait-for-outcome.js +120 -0
  85. package/src/guardian/watch-runner.js +181 -0
  86. package/src/payments/stripe-checkout.js +169 -0
  87. package/src/plans/plan-definitions.js +148 -0
  88. package/src/plans/plan-manager.js +211 -0
  89. package/src/plans/usage-tracker.js +210 -0
  90. package/src/recipes/recipe-engine.js +188 -0
  91. package/src/recipes/recipe-failure-analysis.js +159 -0
  92. package/src/recipes/recipe-registry.js +134 -0
  93. package/src/recipes/recipe-runtime.js +507 -0
  94. package/src/recipes/recipe-store.js +410 -0
  95. package/guardian-contract-v1.md +0 -149
  96. /package/{guardian.config.json → config/guardian.config.json} +0 -0
  97. /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
  98. /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
  99. /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
  100. /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
  101. /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
@@ -0,0 +1,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
+ 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 };
@@ -176,35 +176,38 @@ async function runGuardian(config) {
176
176
  }
177
177
  }
178
178
 
179
- // Display verdict
179
+ // Display verdict with evidence-first messaging
180
180
  console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
181
-
182
- const { decision } = report.finalJudgment;
181
+
182
+ const { decision, reasons } = report.finalJudgment;
183
183
  const coverageStr = `${report.summary.coverage}%`;
184
-
185
- if (decision === 'READY') {
186
- console.log(`\n🟢 READY Safe to launch`);
187
- } else if (decision === 'DO_NOT_LAUNCH') {
188
- console.log(`\n🔴 DO_NOT_LAUNCH Issues found`);
189
- } else {
190
- console.log(`\n🟡 INSUFFICIENT_CONFIDENCE Needs more data`);
191
- }
192
-
193
- console.log(`\n📈 Coverage: ${coverageStr}`);
184
+
185
+ // Map internal to canonical verdict labels for CLI output
186
+ const { toCanonicalVerdict } = require('./verdicts');
187
+ const canonical = toCanonicalVerdict(decision);
188
+ const verdictLabel = canonical === 'READY'
189
+ ? 'READY — flows seen end-to-end'
190
+ : canonical === 'FRICTION'
191
+ ? 'FRICTION — some flows failed or could not be confirmed'
192
+ : 'DO_NOT_LAUNCH — only limited observations or critical failures'
193
+
194
+ console.log(`\n🔎 Verdict: ${verdictLabel}`);
195
+ console.log(`\n📈 Coverage (link discovery only): ${coverageStr}`);
194
196
  console.log(`📄 Pages visited: ${report.summary.visitedPages}`);
195
- console.log(`❌ Failed pages: ${report.summary.failedPages}`);
197
+ console.log(`❌ Failed pages (server/nav errors): ${report.summary.failedPages}`);
196
198
  console.log(`💬 Confidence: ${report.confidence.level}`);
197
-
198
- console.log(`\n📋 Reasons:`);
199
- report.finalJudgment.reasons.forEach(reason => {
199
+
200
+ console.log(`\nEvidence and limitations:`);
201
+ reasons.forEach(reason => {
200
202
  console.log(` • ${reason}`);
201
203
  });
202
204
 
203
205
  console.log(`\n💾 Full report: ${savedReport.reportPath}`);
204
206
  console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
205
207
 
206
- // Exit with appropriate code
207
- const exitCode = (decision === 'READY') ? 0 : 1;
208
+ // Exit with deterministic codes: OBSERVED=0, PARTIAL=1, INSUFFICIENT_DATA=2
209
+ const { mapExitCodeFromCanonical } = require('./verdicts');
210
+ const exitCode = mapExitCodeFromCanonical(canonical);
208
211
  process.exit(exitCode);
209
212
 
210
213
  } catch (err) {
@@ -27,10 +27,14 @@ function initGuardian(options = {}) {
27
27
 
28
28
  // 1. Create policy file
29
29
  try {
30
- const policyPath = path.join(cwd, 'guardian.policy.json');
30
+ const configDir = path.join(cwd, 'config');
31
+ if (!fs.existsSync(configDir)) {
32
+ fs.mkdirSync(configDir, { recursive: true });
33
+ }
34
+ const policyPath = path.join(configDir, 'guardian.policy.json');
31
35
 
32
36
  if (fs.existsSync(policyPath)) {
33
- console.log('⚠️ guardian.policy.json already exists. Skipping.');
37
+ console.log('⚠️ config/guardian.policy.json already exists. Skipping.');
34
38
  } else {
35
39
  // Load preset
36
40
  const presetsDir = path.join(__dirname, '../../policies');
@@ -56,8 +60,8 @@ function initGuardian(options = {}) {
56
60
  }
57
61
 
58
62
  fs.writeFileSync(policyPath, policyContent, 'utf-8');
59
- result.created.push('guardian.policy.json');
60
- console.log('✅ Created guardian.policy.json');
63
+ result.created.push('config/guardian.policy.json');
64
+ console.log('✅ Created config/guardian.policy.json');
61
65
  }
62
66
  } catch (error) {
63
67
  result.errors.push(`Failed to create policy: ${error.message}`);
@@ -105,7 +109,7 @@ function initGuardian(options = {}) {
105
109
  console.log(' 3. Review the generated report:');
106
110
  console.log(' Check artifacts/ directory\n');
107
111
  console.log(' 4. Customize policy:');
108
- console.log(' Edit guardian.policy.json\n');
112
+ console.log(' Edit config/guardian.policy.json\n');
109
113
  console.log(' 5. Integrate with CI/CD:');
110
114
  console.log(' Use .github/workflows/guardian.yml as template\n');
111
115
  console.log('━'.repeat(60) + '\n');
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Intent Detection Engine
3
+ * Inspects the homepage and classifies site intent.
4
+ * Returns { intent: 'saas'|'shop'|'landing'|'unknown', confidence: 0-100, signals: string[] }
5
+ */
6
+
7
+ const { GuardianBrowser } = require('./browser');
8
+
9
+ async function detectIntent(url, options = {}) {
10
+ const browser = new GuardianBrowser();
11
+ const timeout = options.timeout || 20000;
12
+ const headless = options.headless !== false;
13
+ const result = { intent: 'unknown', confidence: 0, signals: [] };
14
+
15
+ try {
16
+ await browser.launch(timeout, { headless });
17
+ await browser.page.goto(url, { waitUntil: 'load', timeout });
18
+ // Ensure content is rendered before signal extraction
19
+ await browser.page.waitForLoadState('domcontentloaded');
20
+ await browser.page.waitForTimeout(100);
21
+ const signals = await _extractSignals(browser.page);
22
+ const { intent, confidence } = _classifyFromSignals(signals);
23
+ result.intent = intent;
24
+ result.confidence = confidence;
25
+ result.signals = signals;
26
+ return result;
27
+ } catch (err) {
28
+ return { intent: 'unknown', confidence: 0, signals: [`error:${err.message}`] };
29
+ } finally {
30
+ try {
31
+ if (browser?.context) await browser.context.close();
32
+ if (browser?.browser) await browser.browser.close();
33
+ } catch {}
34
+ }
35
+ }
36
+
37
+ async function _extractSignals(page) {
38
+ try {
39
+ return await page.evaluate(() => {
40
+ const signals = [];
41
+
42
+ function textIncludes(el, kws) {
43
+ const t = (el.innerText || '').toLowerCase();
44
+ return kws.some(k => t.includes(k));
45
+ }
46
+
47
+ const anchors = Array.from(document.querySelectorAll('a'));
48
+ const buttons = Array.from(document.querySelectorAll('button'));
49
+ const forms = Array.from(document.querySelectorAll('form'));
50
+
51
+ // URL patterns in anchors
52
+ const urlKWs = ['/pricing', '/signup', '/account/signup', '/shop', '/cart', '/checkout', '/contact'];
53
+ for (const a of anchors) {
54
+ const href = (a.getAttribute('href') || '').toLowerCase();
55
+ urlKWs.forEach(k => { if (href.includes(k)) signals.push(`url:${k}`); });
56
+ }
57
+
58
+ // Keywords: signup/subscribe/pricing
59
+ const saasKWs = ['sign up', 'signup', 'subscribe', 'get started', 'pricing', 'plan'];
60
+ [...anchors, ...buttons].forEach(el => {
61
+ if (textIncludes(el, saasKWs)) signals.push('kw:saas');
62
+ });
63
+
64
+ // Shop keywords: buy/cart/checkout/add to cart/order/purchase
65
+ const shopKWs = ['buy', 'cart', 'checkout', 'add to cart', 'order', 'purchase'];
66
+ [...anchors, ...buttons].forEach(el => {
67
+ if (textIncludes(el, shopKWs)) signals.push('kw:shop');
68
+ });
69
+
70
+ // Landing/contact keywords
71
+ const landingKWs = ['contact', 'get in touch'];
72
+ [...anchors, ...buttons].forEach(el => {
73
+ if (textIncludes(el, landingKWs)) signals.push('kw:landing');
74
+ });
75
+
76
+ // Forms vs payment buttons
77
+ const hasEmailInput = !!document.querySelector('input[type="email"], input[name*="email" i]');
78
+ const hasContactForm = forms.some(f => {
79
+ const txts = f.querySelectorAll('textarea');
80
+ const names = f.querySelectorAll('input[name*="name" i]');
81
+ const emails = f.querySelectorAll('input[type="email"], input[name*="email" i]');
82
+ return emails.length > 0 || (names.length > 0 && txts.length > 0);
83
+ });
84
+ if (hasEmailInput) signals.push('form:email');
85
+ if (hasContactForm) signals.push('form:contact');
86
+
87
+ const hasPaymentBtn = [...buttons, ...anchors].some(el => textIncludes(el, ['buy', 'checkout', 'add to cart', 'purchase']));
88
+ if (hasPaymentBtn) signals.push('btn:payment');
89
+
90
+ // Price indications (numbers with per month/year)
91
+ const bodyText = (document.body.innerText || '').toLowerCase();
92
+ if (/\$\s*\d+/.test(bodyText) || /€\s*\d+/.test(bodyText)) {
93
+ if (bodyText.includes('per month') || bodyText.includes('/month') || bodyText.includes('monthly')) {
94
+ signals.push('price:monthly');
95
+ }
96
+ if (bodyText.includes('per year') || bodyText.includes('/year') || bodyText.includes('annually')) {
97
+ signals.push('price:annual');
98
+ }
99
+ }
100
+
101
+ // Fallback: scan body text for keywords
102
+ if (saasKWs.some(k => bodyText.includes(k))) signals.push('kw:saas');
103
+ if (shopKWs.some(k => bodyText.includes(k))) signals.push('kw:shop');
104
+ if (landingKWs.some(k => bodyText.includes(k))) signals.push('kw:landing');
105
+
106
+ return signals;
107
+ });
108
+ } catch (e) {
109
+ return [`error:${e.message}`];
110
+ }
111
+ }
112
+
113
+ function _classifyFromSignals(signals) {
114
+ let saas = 0, shop = 0, landing = 0;
115
+
116
+ for (const s of signals) {
117
+ if (s.startsWith('kw:saas')) saas += 2;
118
+ if (s.startsWith('url:/pricing')) saas += 3;
119
+ if (s.startsWith('url:/signup')) saas += 3;
120
+ if (s.startsWith('form:email')) saas += 2;
121
+ if (s.startsWith('price:')) saas += 1;
122
+
123
+ if (s.startsWith('kw:shop')) shop += 2;
124
+ if (s.startsWith('btn:payment')) shop += 3;
125
+ if (s.startsWith('url:/shop')) shop += 2;
126
+ if (s.startsWith('url:/cart')) shop += 3;
127
+ if (s.startsWith('url:/checkout')) shop += 3;
128
+
129
+ if (s.startsWith('kw:landing')) landing += 2;
130
+ if (s.startsWith('form:contact')) landing += 3;
131
+ if (s.startsWith('url:/contact')) landing += 2;
132
+ }
133
+
134
+ const scores = { saas, shop, landing };
135
+ const top = Object.entries(scores).sort((a, b) => b[1] - a[1])[0];
136
+ const maxScore = top ? top[1] : 0;
137
+ const intent = maxScore >= 2 ? top[0] : 'unknown';
138
+ const confidence = Math.min(100, Math.round((maxScore / 10) * 100));
139
+ return { intent, confidence };
140
+ }
141
+
142
+ module.exports = {
143
+ detectIntent,
144
+ _extractSignals,
145
+ _classifyFromSignals
146
+ };