@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,211 @@
1
+ /**
2
+ * Run Listing CLI Command
3
+ * Lists completed Guardian runs with their metadata
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { readMetaJson, formatDuration } = require('./run-artifacts');
9
+
10
+ /**
11
+ * Find all run directories in artifacts folder
12
+ *
13
+ * @param {string} artifactsDir - Path to artifacts directory
14
+ * @returns {string[]} array of run directory paths
15
+ */
16
+ function findRunDirs(artifactsDir) {
17
+ if (!fs.existsSync(artifactsDir)) {
18
+ return [];
19
+ }
20
+
21
+ try {
22
+ const entries = fs.readdirSync(artifactsDir, { withFileTypes: true });
23
+ return entries
24
+ .filter(e => e.isDirectory())
25
+ .map(e => path.join(artifactsDir, e.name));
26
+ } catch (e) {
27
+ return [];
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Load run metadata with fallback to directory name parsing
33
+ *
34
+ * @param {string} runDir - Run directory path
35
+ * @returns {Object|null} run info or null if cannot parse
36
+ */
37
+ function loadRunInfo(runDir) {
38
+ const dirName = path.basename(runDir);
39
+
40
+ // Try META.json first
41
+ const meta = readMetaJson(runDir);
42
+ if (meta) {
43
+ return {
44
+ path: runDir,
45
+ dirName,
46
+ timestamp: meta.timestamp,
47
+ url: meta.url,
48
+ siteSlug: meta.siteSlug,
49
+ policy: meta.policy,
50
+ result: meta.result,
51
+ durationMs: meta.durationMs,
52
+ attempts: meta.attempts
53
+ };
54
+ }
55
+
56
+ // Silently skip if no META.json
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * Format and sort runs for display
62
+ *
63
+ * @param {Array} runs - Array of run info objects
64
+ * @returns {Array} sorted by timestamp (newest first)
65
+ */
66
+ function sortRuns(runs) {
67
+ return runs
68
+ .filter(r => r !== null)
69
+ .sort((a, b) => {
70
+ const timeA = new Date(a.timestamp).getTime();
71
+ const timeB = new Date(b.timestamp).getTime();
72
+ return timeB - timeA; // newest first
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Format time string from ISO timestamp
78
+ *
79
+ * @param {string} isoString - ISO-8601 timestamp
80
+ * @returns {string} formatted local time (YYYY-MM-DD HH:MM:SS)
81
+ */
82
+ function formatTime(isoString) {
83
+ const date = new Date(isoString);
84
+ const year = date.getFullYear();
85
+ const month = String(date.getMonth() + 1).padStart(2, '0');
86
+ const day = String(date.getDate()).padStart(2, '0');
87
+ const hours = String(date.getHours()).padStart(2, '0');
88
+ const minutes = String(date.getMinutes()).padStart(2, '0');
89
+ const seconds = String(date.getSeconds()).padStart(2, '0');
90
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
91
+ }
92
+
93
+ /**
94
+ * Calculate column widths for table display
95
+ *
96
+ * @param {Array} runs - Run records
97
+ * @returns {Object} widths for each column
98
+ */
99
+ function calculateColumnWidths(runs) {
100
+ // Use fixed reasonable widths for 100-char display
101
+ return {
102
+ time: 19, // "YYYY-MM-DD HH:MM:SS"
103
+ site: 14,
104
+ policy: 10,
105
+ result: 7,
106
+ duration: 10,
107
+ path: 30
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Format a single row for table output
113
+ *
114
+ * @param {Object} run - Run info
115
+ * @param {Object} widths - Column widths
116
+ * @returns {string} formatted row
117
+ */
118
+ function formatRow(run, widths) {
119
+ const time = formatTime(run.timestamp);
120
+ const site = (run.siteSlug || 'unknown').substring(0, widths.site);
121
+ const policy = (run.policy || 'default').substring(0, widths.policy);
122
+ const result = (run.result || 'UNKNOWN').substring(0, widths.result);
123
+ const duration = formatDuration(run.durationMs).substring(0, widths.duration);
124
+ const path = run.dirName.substring(0, widths.path);
125
+
126
+ return `${time} ${site.padEnd(widths.site)} ${policy.padEnd(widths.policy)} ${result.padEnd(widths.result)} ${duration.padEnd(widths.duration)} ${path}`;
127
+ }
128
+
129
+ /**
130
+ * Main list command with filtering
131
+ *
132
+ * @param {string} artifactsDir - Path to artifacts directory
133
+ * @param {Object} filters - Filter options
134
+ * @param {boolean} filters.failed - Show only FAILED runs
135
+ * @param {string} filters.site - Show only runs for specific site slug
136
+ * @param {number} filters.limit - Show only newest N runs
137
+ */
138
+ function listRuns(artifactsDir = './artifacts', filters = {}) {
139
+ const runDirs = findRunDirs(artifactsDir);
140
+
141
+ if (runDirs.length === 0) {
142
+ console.log('No runs found.');
143
+ return 0;
144
+ }
145
+
146
+ // Load metadata for all runs
147
+ let runs = runDirs
148
+ .map(dir => loadRunInfo(dir))
149
+ .filter(r => r !== null);
150
+
151
+ if (runs.length === 0) {
152
+ console.log('No completed runs with META.json found.');
153
+ return 0;
154
+ }
155
+
156
+ // Apply filters
157
+ if (filters.failed) {
158
+ runs = runs.filter(r => r.result === 'FAILED');
159
+ }
160
+
161
+ if (filters.site) {
162
+ runs = runs.filter(r => r.siteSlug === filters.site);
163
+ }
164
+
165
+ if (runs.length === 0) {
166
+ console.log('No runs matching the filters.');
167
+ return 0;
168
+ }
169
+
170
+ // Sort by timestamp (newest first)
171
+ const sorted = sortRuns(runs);
172
+
173
+ // Apply limit filter
174
+ let displayed = sorted;
175
+ if (filters.limit && filters.limit > 0) {
176
+ displayed = sorted.slice(0, filters.limit);
177
+ }
178
+
179
+ // Calculate column widths
180
+ const widths = calculateColumnWidths(displayed);
181
+
182
+ // Print header
183
+ console.log('');
184
+ console.log('Guardian Runs');
185
+ console.log('='.repeat(100));
186
+ const header = 'Time'.padEnd(widths.time + 2) +
187
+ 'Site'.padEnd(widths.site + 2) +
188
+ 'Policy'.padEnd(widths.policy + 2) +
189
+ 'Result'.padEnd(widths.result + 2) +
190
+ 'Duration'.padEnd(widths.duration + 2) +
191
+ 'Path';
192
+ console.log(header);
193
+ console.log('-'.repeat(100));
194
+
195
+ // Print rows
196
+ for (const run of displayed) {
197
+ console.log(formatRow(run, widths));
198
+ }
199
+
200
+ console.log('='.repeat(100));
201
+ console.log(`Total: ${runs.length} run(s)${displayed.length < runs.length ? ` (showing ${displayed.length})` : ''}\n`);
202
+
203
+ return 0;
204
+ }
205
+
206
+ module.exports = {
207
+ listRuns,
208
+ findRunDirs,
209
+ loadRunInfo,
210
+ sortRuns
211
+ };
@@ -0,0 +1,20 @@
1
+ function deriveBaselineVerdict({ baselineCreated, diffResult }) {
2
+ if (baselineCreated) return 'BASELINE_CREATED';
3
+ if (!diffResult) return 'NO_BASELINE';
4
+ const hasRegressions = diffResult.regressions && Object.keys(diffResult.regressions).length > 0;
5
+ const hasImprovements = diffResult.improvements && Object.keys(diffResult.improvements).length > 0;
6
+ if (hasRegressions) return 'REGRESSION_DETECTED';
7
+ if (hasImprovements) return 'IMPROVEMENT_DETECTED';
8
+ return 'NO_REGRESSION';
9
+ }
10
+
11
+ function formatRunSummary({ flowResults = [], diffResult = null, baselineCreated = false, exitCode = 0 }, options = {}) {
12
+ const success = flowResults.filter(f => f.outcome === 'SUCCESS').length;
13
+ const friction = flowResults.filter(f => f.outcome === 'FRICTION').length;
14
+ const failure = flowResults.filter(f => f.outcome === 'FAILURE').length;
15
+ const baseline = deriveBaselineVerdict({ baselineCreated, diffResult });
16
+ const label = options.label || 'Summary';
17
+ return `${label}: flows=${flowResults.length} success=${success} friction=${friction} failure=${failure} | baseline=${baseline} | exit=${exitCode}`;
18
+ }
19
+
20
+ module.exports = { formatRunSummary, deriveBaselineVerdict };
@@ -2,6 +2,8 @@
2
2
  * Scan Presets (Phase 6)
3
3
  * Opinionated defaults for one-command scans
4
4
  * Deterministic mappings: attempts, flows, policy thresholds.
5
+ * Each preset must fully define coverage (enabled/disabled attempts), flows,
6
+ * policy strictness, and operational defaults (fail-fast, evidence expectations).
5
7
  */
6
8
 
7
9
  const { getDefaultAttemptIds } = require('./attempt-registry');
@@ -10,50 +12,137 @@ const { getDefaultFlowIds } = require('./flow-registry');
10
12
  function resolveScanPreset(name = 'landing') {
11
13
  const preset = (name || '').toLowerCase();
12
14
 
15
+ if (!preset) {
16
+ throw new Error('Preset name is required');
17
+ }
18
+
13
19
  // Defaults: curated attempts + curated flows
14
20
  const defaults = {
21
+ id: 'default',
15
22
  attempts: getDefaultAttemptIds(),
23
+ disabledAttempts: [],
16
24
  flows: getDefaultFlowIds(),
17
- policy: null
25
+ policy: null,
26
+ failFast: true,
27
+ evidence: {
28
+ requireScreenshots: true,
29
+ requireTraces: true,
30
+ minCompleteness: 1.0,
31
+ minIntegrity: 0.9
32
+ }
18
33
  };
19
34
 
20
35
  switch (preset) {
21
36
  case 'landing':
22
37
  return {
23
- attempts: ['contact_form', 'language_switch', 'newsletter_signup'],
38
+ id: 'landing',
39
+ attempts: ['site_smoke', 'primary_ctas', 'contact_discovery_v2', 'contact_form', 'language_switch', 'newsletter_signup'],
40
+ disabledAttempts: ['signup', 'login', 'checkout'],
24
41
  flows: [], // focus on landing conversion, flows optional
25
42
  policy: {
26
43
  // lenient warnings, strict criticals
27
44
  failOnSeverity: 'CRITICAL',
28
- maxWarnings: 999,
29
- visualGates: { CRITICAL: 0, WARNING: 999, maxDiffPercent: 25 }
30
- }
45
+ maxWarnings: 5,
46
+ visualGates: { CRITICAL: 0, WARNING: 5, maxDiffPercent: 25 },
47
+ coverage: { failOnGap: false, warnOnGap: true },
48
+ evidence: { minCompleteness: 0.6, minIntegrity: 0.7, requireScreenshots: true, requireTraces: false }
49
+ },
50
+ failFast: true,
51
+ evidence: { requireScreenshots: true, requireTraces: false, minCompleteness: 0.6, minIntegrity: 0.7 }
52
+ };
53
+ case 'landing-demo':
54
+ return {
55
+ id: 'landing-demo',
56
+ attempts: ['site_smoke', 'primary_ctas', 'contact_discovery_v2', 'contact_form', 'language_switch'],
57
+ disabledAttempts: ['signup', 'login', 'checkout', 'newsletter_signup'],
58
+ flows: [],
59
+ policy: {
60
+ // Strict on broken navigation and CTA, lenient on revenue-related issues
61
+ failOnSeverity: 'CRITICAL',
62
+ maxWarnings: 5,
63
+ failOnNewRegression: false,
64
+ visualGates: { CRITICAL: 0, WARNING: 999, maxDiffPercent: 30 },
65
+ coverage: { failOnGap: true, warnOnGap: false },
66
+ evidence: { minCompleteness: 0.7, minIntegrity: 0.8, requireScreenshots: true, requireTraces: false }
67
+ },
68
+ failFast: true,
69
+ evidence: { requireScreenshots: true, requireTraces: false, minCompleteness: 0.7, minIntegrity: 0.8 }
70
+ };
71
+ case 'startup':
72
+ return {
73
+ id: 'startup',
74
+ attempts: getDefaultAttemptIds(),
75
+ disabledAttempts: [],
76
+ flows: getDefaultFlowIds(),
77
+ policy: {
78
+ failOnSeverity: 'CRITICAL',
79
+ maxWarnings: 3,
80
+ maxInfo: 999,
81
+ failOnNewRegression: false,
82
+ visualGates: { CRITICAL: 0, WARNING: 5, maxDiffPercent: 30 },
83
+ coverage: { failOnGap: true, warnOnGap: false },
84
+ evidence: { minCompleteness: 0.7, minIntegrity: 0.8, requireScreenshots: true, requireTraces: false }
85
+ },
86
+ failFast: true,
87
+ evidence: { requireScreenshots: true, requireTraces: false, minCompleteness: 0.7, minIntegrity: 0.8 }
31
88
  };
32
89
  case 'saas':
33
90
  return {
34
- attempts: ['language_switch', 'contact_form', 'newsletter_signup'],
91
+ id: 'saas',
92
+ attempts: ['site_smoke', 'primary_ctas', 'contact_discovery_v2', 'language_switch', 'contact_form', 'newsletter_signup'],
35
93
  flows: ['signup_flow', 'login_flow'],
36
94
  policy: {
37
95
  failOnSeverity: 'CRITICAL',
38
96
  maxWarnings: 1,
39
97
  failOnNewRegression: true,
40
- visualGates: { CRITICAL: 0, WARNING: 5, maxDiffPercent: 20 }
41
- }
98
+ visualGates: { CRITICAL: 0, WARNING: 5, maxDiffPercent: 20 },
99
+ coverage: { failOnGap: true, warnOnGap: false },
100
+ evidence: { minCompleteness: 0.8, minIntegrity: 0.9, requireScreenshots: true, requireTraces: true }
101
+ },
102
+ failFast: true,
103
+ evidence: { requireScreenshots: true, requireTraces: true, minCompleteness: 0.8, minIntegrity: 0.9 }
42
104
  };
43
105
  case 'shop':
44
106
  case 'ecommerce':
45
107
  return {
108
+ id: 'shop',
46
109
  attempts: ['language_switch', 'contact_form', 'newsletter_signup'],
47
110
  flows: ['checkout_flow'],
48
111
  policy: {
49
112
  failOnSeverity: 'CRITICAL',
50
113
  maxWarnings: 0,
51
114
  failOnNewRegression: true,
52
- visualGates: { CRITICAL: 0, WARNING: 0, maxDiffPercent: 15 }
53
- }
115
+ visualGates: { CRITICAL: 0, WARNING: 0, maxDiffPercent: 15 },
116
+ coverage: { failOnGap: true, warnOnGap: false },
117
+ evidence: { minCompleteness: 0.9, minIntegrity: 0.95, requireScreenshots: true, requireTraces: true }
118
+ },
119
+ failFast: true,
120
+ evidence: { requireScreenshots: true, requireTraces: true, minCompleteness: 0.9, minIntegrity: 0.95 }
121
+ };
122
+ case 'enterprise':
123
+ return {
124
+ id: 'enterprise',
125
+ attempts: getDefaultAttemptIds(),
126
+ disabledAttempts: [],
127
+ flows: getDefaultFlowIds(),
128
+ policy: {
129
+ failOnSeverity: 'WARNING',
130
+ maxWarnings: 0,
131
+ maxInfo: 0,
132
+ maxTotalRisk: 0,
133
+ failOnNewRegression: true,
134
+ failOnSoftFailures: true,
135
+ softFailureThreshold: 0,
136
+ requireBaseline: true,
137
+ visualGates: { CRITICAL: 0, WARNING: 0, maxDiffPercent: 10 },
138
+ coverage: { failOnGap: true, warnOnGap: false },
139
+ evidence: { minCompleteness: 1.0, minIntegrity: 0.98, requireScreenshots: true, requireTraces: true }
140
+ },
141
+ failFast: true,
142
+ evidence: { requireScreenshots: true, requireTraces: true, minCompleteness: 1.0, minIntegrity: 0.98 }
54
143
  };
55
144
  default:
56
- return defaults;
145
+ throw new Error(`Unknown preset: ${name}`);
57
146
  }
58
147
  }
59
148