@odavl/guardian 0.1.0-rc1

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 (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -0
  4. package/bin/guardian.js +690 -0
  5. package/flows/example-login-flow.json +36 -0
  6. package/flows/example-signup-flow.json +44 -0
  7. package/guardian-contract-v1.md +149 -0
  8. package/guardian.config.json +54 -0
  9. package/guardian.policy.json +12 -0
  10. package/guardian.profile.docs.yaml +18 -0
  11. package/guardian.profile.ecommerce.yaml +17 -0
  12. package/guardian.profile.marketing.yaml +18 -0
  13. package/guardian.profile.saas.yaml +21 -0
  14. package/package.json +69 -0
  15. package/policies/enterprise.json +12 -0
  16. package/policies/saas.json +12 -0
  17. package/policies/startup.json +12 -0
  18. package/src/guardian/attempt-engine.js +454 -0
  19. package/src/guardian/attempt-registry.js +227 -0
  20. package/src/guardian/attempt-reporter.js +507 -0
  21. package/src/guardian/attempt.js +227 -0
  22. package/src/guardian/auto-attempt-builder.js +283 -0
  23. package/src/guardian/baseline-reporter.js +143 -0
  24. package/src/guardian/baseline-storage.js +285 -0
  25. package/src/guardian/baseline.js +492 -0
  26. package/src/guardian/behavioral-signals.js +261 -0
  27. package/src/guardian/breakage-intelligence.js +223 -0
  28. package/src/guardian/browser.js +92 -0
  29. package/src/guardian/cli-summary.js +141 -0
  30. package/src/guardian/crawler.js +142 -0
  31. package/src/guardian/discovery-engine.js +661 -0
  32. package/src/guardian/enhanced-html-reporter.js +305 -0
  33. package/src/guardian/failure-taxonomy.js +169 -0
  34. package/src/guardian/flow-executor.js +374 -0
  35. package/src/guardian/flow-registry.js +67 -0
  36. package/src/guardian/html-reporter.js +414 -0
  37. package/src/guardian/index.js +218 -0
  38. package/src/guardian/init-command.js +139 -0
  39. package/src/guardian/junit-reporter.js +264 -0
  40. package/src/guardian/market-criticality.js +335 -0
  41. package/src/guardian/market-reporter.js +305 -0
  42. package/src/guardian/network-trace.js +178 -0
  43. package/src/guardian/policy.js +357 -0
  44. package/src/guardian/preset-loader.js +148 -0
  45. package/src/guardian/reality.js +547 -0
  46. package/src/guardian/reporter.js +181 -0
  47. package/src/guardian/root-cause-analysis.js +171 -0
  48. package/src/guardian/safety.js +248 -0
  49. package/src/guardian/scan-presets.js +60 -0
  50. package/src/guardian/screenshot.js +152 -0
  51. package/src/guardian/sitemap.js +225 -0
  52. package/src/guardian/snapshot-schema.js +266 -0
  53. package/src/guardian/snapshot.js +327 -0
  54. package/src/guardian/validators.js +323 -0
  55. package/src/guardian/visual-diff.js +247 -0
  56. package/src/guardian/webhook.js +206 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * CLI Summary Module
3
+ *
4
+ * Generate human-friendly CLI summaries at the end of Guardian runs.
5
+ * Shows critical info, top risks, and actionable next steps.
6
+ */
7
+
8
+ /**
9
+ * Generate final CLI summary
10
+ * @param {object} snapshot - Guardian snapshot
11
+ * @param {object} policyEval - Policy evaluation result
12
+ * @returns {string} Formatted CLI summary
13
+ */
14
+ function generateCliSummary(snapshot, policyEval) {
15
+ if (!snapshot) {
16
+ return 'No snapshot data available.';
17
+ }
18
+
19
+ const meta = snapshot.meta || {};
20
+ const marketImpact = snapshot.marketImpactSummary || {};
21
+ const counts = marketImpact.countsBySeverity || { CRITICAL: 0, WARNING: 0, INFO: 0 };
22
+ const topRisks = marketImpact.topRisks || [];
23
+ const attempts = snapshot.attempts || [];
24
+ const discovery = snapshot.discovery || {};
25
+
26
+ let output = '\n';
27
+ output += '━'.repeat(70) + '\n';
28
+ output += 'đŸ›Ąī¸ Guardian Reality Summary\n';
29
+ output += '━'.repeat(70) + '\n\n';
30
+
31
+ // Target URL
32
+ output += `Target: ${meta.url || 'unknown'}\n`;
33
+ output += `Run ID: ${meta.runId || 'unknown'}\n\n`;
34
+
35
+ // Risk Counts
36
+ output += '📊 Risk Summary:\n';
37
+ output += ` 🚨 CRITICAL: ${counts.CRITICAL}`;
38
+ if (counts.CRITICAL > 0) output += ' (Revenue impact)';
39
+ output += '\n';
40
+ output += ` âš ī¸ WARNING: ${counts.WARNING}`;
41
+ if (counts.WARNING > 0) output += ' (User experience)';
42
+ output += '\n';
43
+ output += ` â„šī¸ INFO: ${counts.INFO}`;
44
+ if (counts.INFO > 0) output += ' (Minor issues)';
45
+ output += '\n\n';
46
+
47
+ // Top Risks (up to 3)
48
+ if (topRisks.length > 0) {
49
+ // Preserve legacy label for backward compatibility
50
+ output += 'đŸ”Ĩ Top Risk:\n';
51
+ // New: compact list of top 3 issues
52
+ output += ' (Top Issues)\n';
53
+ topRisks.slice(0, 3).forEach((risk, idx) => {
54
+ output += ` ${idx + 1}. ${risk.humanReadableReason || 'Unknown issue'}\n`;
55
+ output += ` Impact: ${risk.impactScore || 0} (${risk.category || 'UNKNOWN'}) | Severity: ${risk.severity || 'INFO'}\n`;
56
+ });
57
+
58
+ // Link evidence if available for the top-most
59
+ const topRisk = topRisks[0];
60
+ const relatedAttempt = attempts.find(a =>
61
+ a.attemptId === topRisk.attemptId ||
62
+ (topRisk.humanReadableReason || '').toLowerCase().includes(a.attemptName?.toLowerCase() || '')
63
+ );
64
+ if (relatedAttempt && relatedAttempt.evidence) {
65
+ output += ` Evidence: ${relatedAttempt.evidence.screenshotPath || 'See report'}\n`;
66
+ }
67
+ output += '\n';
68
+ }
69
+
70
+ // Attempt Summary
71
+ const successfulAttempts = attempts.filter(a => a.outcome === 'SUCCESS').length;
72
+ const totalAttempts = attempts.length;
73
+ if (totalAttempts > 0) {
74
+ output += 'đŸŽ¯ Attempts:\n';
75
+ output += ` ${successfulAttempts}/${totalAttempts} successful`;
76
+ if (successfulAttempts < totalAttempts) {
77
+ output += ` (${totalAttempts - successfulAttempts} failed)`;
78
+ }
79
+ output += '\n\n';
80
+ }
81
+
82
+ // Discovery Summary
83
+ if (discovery.pagesVisitedCount > 0) {
84
+ output += '🔍 Discovery:\n';
85
+ output += ` Pages visited: ${discovery.pagesVisitedCount || 0}\n`;
86
+ output += ` Interactions discovered: ${discovery.interactionsDiscovered || 0}\n`;
87
+ output += ` Interactions executed: ${discovery.interactionsExecuted || 0}\n\n`;
88
+ }
89
+
90
+ // Policy Evaluation
91
+ if (policyEval) {
92
+ output += 'đŸ›Ąī¸ Policy:\n';
93
+ if (policyEval.passed) {
94
+ output += ' ✅ PASSED - All checks satisfied\n';
95
+ } else {
96
+ output += ' ❌ FAILED - Policy violations detected\n';
97
+ if (policyEval.reasons && policyEval.reasons.length > 0) {
98
+ output += ' Reasons:\n';
99
+ policyEval.reasons.slice(0, 3).forEach(reason => {
100
+ output += ` â€ĸ ${reason}\n`;
101
+ });
102
+ }
103
+ }
104
+ output += ` Exit code: ${policyEval.exitCode || 0}\n\n`;
105
+ }
106
+
107
+ // Next Action
108
+ output += '👉 Next Action:\n';
109
+ if (counts.CRITICAL > 0) {
110
+ output += ' âš ī¸ Fix the CRITICAL issue(s) before deploying.\n';
111
+ output += ' Review the top risk above and check evidence screenshots.\n';
112
+ } else if (counts.WARNING > 0) {
113
+ output += ' âš ī¸ Review WARNING issues - they may impact user experience.\n';
114
+ output += ' Consider fixing before next release.\n';
115
+ } else if (policyEval && !policyEval.passed) {
116
+ output += ' âš ī¸ Policy check failed. Review policy violations above.\n';
117
+ } else {
118
+ output += ' ✅ All checks passed! Site is ready for deployment.\n';
119
+ }
120
+
121
+ output += '\n';
122
+ output += '📁 Full report: ' + (meta.runId ? `artifacts/${meta.runId}/` : 'See artifacts/\n');
123
+ output += '\n';
124
+
125
+ output += '━'.repeat(70) + '\n';
126
+
127
+ return output;
128
+ }
129
+
130
+ /**
131
+ * Print summary to console
132
+ */
133
+ function printCliSummary(snapshot, policyEval) {
134
+ const summary = generateCliSummary(snapshot, policyEval);
135
+ console.log(summary);
136
+ }
137
+
138
+ module.exports = {
139
+ generateCliSummary,
140
+ printCliSummary
141
+ };
@@ -0,0 +1,142 @@
1
+ class GuardianCrawler {
2
+ constructor(baseUrl, maxPages = 25, maxDepth = 3) {
3
+ this.baseUrl = new URL(baseUrl);
4
+ this.maxPages = maxPages;
5
+ this.maxDepth = maxDepth;
6
+
7
+ this.visited = new Set();
8
+ this.discovered = new Set();
9
+ this.toVisit = ['/'];
10
+ this.pages = [];
11
+
12
+ // Phase 2 features
13
+ this.screenshot = null; // Will be set by main engine
14
+ this.safety = null; // Will be set by main engine
15
+ this.artifactsDir = null; // Will be set during crawl
16
+ this.safetyStats = { urlsBlocked: 0 };
17
+ }
18
+
19
+ isSameOrigin(url) {
20
+ try {
21
+ const parsed = new URL(url, this.baseUrl);
22
+ return parsed.origin === this.baseUrl.origin;
23
+ } catch (e) {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ getPathname(url) {
29
+ try {
30
+ const parsed = new URL(url, this.baseUrl);
31
+ return parsed.pathname;
32
+ } catch (e) {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ async crawl(browser, artifactsDir = null) {
38
+ this.artifactsDir = artifactsDir;
39
+ let depth = 0;
40
+ let currentDepthUrls = ['/'];
41
+
42
+ while (currentDepthUrls.length > 0 && depth < this.maxDepth && this.visited.size < this.maxPages) {
43
+ const nextDepthUrls = [];
44
+
45
+ for (const pathname of currentDepthUrls) {
46
+ if (this.visited.size >= this.maxPages) break;
47
+ if (this.visited.has(pathname)) continue;
48
+
49
+ // Safety check
50
+ const fullUrl = new URL(pathname, this.baseUrl).toString();
51
+ if (this.safety) {
52
+ const safetyCheck = this.safety.isUrlSafe(fullUrl);
53
+ if (!safetyCheck.safe) {
54
+ console.log(`đŸ›Ąī¸ Blocked: ${pathname} (${safetyCheck.reason})`);
55
+ this.safetyStats.urlsBlocked++;
56
+ continue;
57
+ }
58
+ }
59
+
60
+ this.visited.add(pathname);
61
+ this.discovered.add(pathname);
62
+
63
+ const result = await browser.navigate(fullUrl);
64
+
65
+ if (result.success) {
66
+ const links = await browser.getLinks();
67
+
68
+ // Capture screenshot if enabled
69
+ let screenshotFile = null;
70
+ if (this.screenshot && this.artifactsDir) {
71
+ screenshotFile = await this.screenshot.captureForCrawl(
72
+ browser.page,
73
+ fullUrl,
74
+ this.pages.length + 1,
75
+ this.artifactsDir
76
+ );
77
+ }
78
+
79
+ const pageRecord = {
80
+ index: this.pages.length + 1,
81
+ url: fullUrl,
82
+ pathname: pathname,
83
+ status: result.status,
84
+ links: links.length,
85
+ linkCount: links.length, // Deprecated, use 'links'
86
+ depth: depth,
87
+ screenshot: screenshotFile,
88
+ timestamp: new Date().toISOString()
89
+ };
90
+
91
+ this.pages.push(pageRecord);
92
+
93
+ // Extract unique new links
94
+ for (const link of links) {
95
+ if (!this.isSameOrigin(link.href)) continue;
96
+
97
+ const newPathname = this.getPathname(link.href);
98
+ if (!newPathname || this.discovered.has(newPathname)) continue;
99
+
100
+ this.discovered.add(newPathname);
101
+ if (!this.visited.has(newPathname)) {
102
+ nextDepthUrls.push(newPathname);
103
+ }
104
+ }
105
+ } else {
106
+ // Still record failed visits
107
+ this.pages.push({
108
+ index: this.pages.length + 1,
109
+ url: fullUrl,
110
+ pathname: pathname,
111
+ status: null,
112
+ links: 0,
113
+ linkCount: 0,
114
+ depth: depth,
115
+ error: result.error,
116
+ screenshot: null,
117
+ timestamp: new Date().toISOString()
118
+ });
119
+ }
120
+ }
121
+
122
+ currentDepthUrls = nextDepthUrls;
123
+ depth++;
124
+ }
125
+
126
+ // Add discovered but not visited pages
127
+ for (const pathname of this.discovered) {
128
+ if (!this.visited.has(pathname)) {
129
+ // Do nothing, we'll handle in report
130
+ }
131
+ }
132
+
133
+ return {
134
+ visited: this.pages,
135
+ totalDiscovered: this.discovered.size,
136
+ totalVisited: this.visited.size,
137
+ safetyStats: this.safetyStats
138
+ };
139
+ }
140
+ }
141
+
142
+ module.exports = { GuardianCrawler };