@odavl/guardian 0.2.0 → 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 (84) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/README.md +155 -97
  3. package/bin/guardian.js +1345 -60
  4. package/config/README.md +59 -0
  5. package/config/profiles/landing-demo.yaml +16 -0
  6. package/package.json +21 -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 +568 -7
  18. package/src/guardian/attempt-registry.js +42 -1
  19. package/src/guardian/attempt-relevance.js +106 -0
  20. package/src/guardian/attempt.js +24 -0
  21. package/src/guardian/baseline.js +12 -4
  22. package/src/guardian/breakage-intelligence.js +1 -0
  23. package/src/guardian/ci-cli.js +121 -0
  24. package/src/guardian/ci-output.js +4 -3
  25. package/src/guardian/cli-summary.js +79 -92
  26. package/src/guardian/config-loader.js +162 -0
  27. package/src/guardian/drift-detector.js +100 -0
  28. package/src/guardian/enhanced-html-reporter.js +221 -4
  29. package/src/guardian/env-guard.js +127 -0
  30. package/src/guardian/failure-intelligence.js +173 -0
  31. package/src/guardian/first-run-profile.js +89 -0
  32. package/src/guardian/first-run.js +6 -1
  33. package/src/guardian/flag-validator.js +17 -3
  34. package/src/guardian/html-reporter.js +2 -0
  35. package/src/guardian/human-reporter.js +431 -0
  36. package/src/guardian/index.js +22 -19
  37. package/src/guardian/init-command.js +9 -5
  38. package/src/guardian/intent-detector.js +146 -0
  39. package/src/guardian/journey-definitions.js +132 -0
  40. package/src/guardian/journey-scan-cli.js +145 -0
  41. package/src/guardian/journey-scanner.js +583 -0
  42. package/src/guardian/junit-reporter.js +18 -1
  43. package/src/guardian/live-cli.js +95 -0
  44. package/src/guardian/live-scheduler-runner.js +137 -0
  45. package/src/guardian/live-scheduler.js +146 -0
  46. package/src/guardian/market-reporter.js +341 -81
  47. package/src/guardian/pattern-analyzer.js +348 -0
  48. package/src/guardian/policy.js +80 -3
  49. package/src/guardian/preset-loader.js +9 -6
  50. package/src/guardian/reality.js +1278 -117
  51. package/src/guardian/reporter.js +27 -41
  52. package/src/guardian/run-artifacts.js +212 -0
  53. package/src/guardian/run-cleanup.js +207 -0
  54. package/src/guardian/run-latest.js +90 -0
  55. package/src/guardian/run-list.js +211 -0
  56. package/src/guardian/scan-presets.js +100 -11
  57. package/src/guardian/selector-fallbacks.js +394 -0
  58. package/src/guardian/semantic-contact-finder.js +2 -1
  59. package/src/guardian/site-introspection.js +257 -0
  60. package/src/guardian/smoke.js +2 -2
  61. package/src/guardian/snapshot-schema.js +25 -1
  62. package/src/guardian/snapshot.js +46 -2
  63. package/src/guardian/stability-scorer.js +169 -0
  64. package/src/guardian/template-command.js +184 -0
  65. package/src/guardian/text-formatters.js +426 -0
  66. package/src/guardian/verdict.js +320 -0
  67. package/src/guardian/verdicts.js +74 -0
  68. package/src/guardian/watch-runner.js +3 -7
  69. package/src/payments/stripe-checkout.js +169 -0
  70. package/src/plans/plan-definitions.js +148 -0
  71. package/src/plans/plan-manager.js +211 -0
  72. package/src/plans/usage-tracker.js +210 -0
  73. package/src/recipes/recipe-engine.js +188 -0
  74. package/src/recipes/recipe-failure-analysis.js +159 -0
  75. package/src/recipes/recipe-registry.js +134 -0
  76. package/src/recipes/recipe-runtime.js +507 -0
  77. package/src/recipes/recipe-store.js +410 -0
  78. package/guardian-contract-v1.md +0 -149
  79. /package/{guardian.config.json → config/guardian.config.json} +0 -0
  80. /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
  81. /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
  82. /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
  83. /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
  84. /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Phase 12.3: Human Failure Intelligence
3
+ * Deterministic heuristics to explain failures like a human.
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const STORE_DIR = path.join(os.homedir(), '.odavl-guardian', 'failures');
11
+ const SIGNATURE_FILE = path.join(STORE_DIR, 'signatures.json');
12
+
13
+ function ensureStore() {
14
+ if (!fs.existsSync(STORE_DIR)) {
15
+ fs.mkdirSync(STORE_DIR, { recursive: true });
16
+ }
17
+ }
18
+
19
+ function loadSignatures() {
20
+ ensureStore();
21
+ if (!fs.existsSync(SIGNATURE_FILE)) {
22
+ return { sites: {} };
23
+ }
24
+ try {
25
+ return JSON.parse(fs.readFileSync(SIGNATURE_FILE, 'utf-8'));
26
+ } catch {
27
+ return { sites: {} };
28
+ }
29
+ }
30
+
31
+ function saveSignatures(data) {
32
+ ensureStore();
33
+ fs.writeFileSync(SIGNATURE_FILE, JSON.stringify(data, null, 2), 'utf-8');
34
+ return data;
35
+ }
36
+
37
+ function getDomain(url) {
38
+ try {
39
+ const u = new URL(url);
40
+ return u.hostname;
41
+ } catch {
42
+ return url;
43
+ }
44
+ }
45
+
46
+ function classifyFailureStage(stepIndex, totalSteps, goalIndex, success) {
47
+ if (success) return 'AFTER_GOAL';
48
+ if (typeof goalIndex !== 'number') goalIndex = totalSteps - 1;
49
+ if (stepIndex < goalIndex) return 'BEFORE_GOAL';
50
+ if (stepIndex === goalIndex) return 'AT_GOAL';
51
+ return 'AFTER_GOAL';
52
+ }
53
+
54
+ function determineCause(step) {
55
+ // Deterministic priority
56
+ // 1) CTA_NOT_FOUND
57
+ // 2) ELEMENT_NOT_FOUND on submit
58
+ // 3) TIMEOUT near navigation
59
+ // 4) INTENT_DRIFT
60
+ const code = step?.errorCode || step?.result?.errorCode || step?.status;
61
+ const tags = step?.tags || step?.result?.tags || [];
62
+ const action = (step?.action || step?.name || '').toLowerCase();
63
+
64
+ if (code === 'CTA_NOT_FOUND' || tags.includes('cta')) {
65
+ return { cause: 'Primary action not visible', hint: 'Make the main signup button visible without scrolling.' };
66
+ }
67
+
68
+ if ((code === 'ELEMENT_NOT_FOUND' || code === 'MISSING_ELEMENT') && (tags.includes('submit') || action.includes('submit'))) {
69
+ return { cause: 'Form submission blocked', hint: 'Ensure the submit button exists and is enabled.' };
70
+ }
71
+
72
+ if (code === 'TIMEOUT' || (tags.includes('nav') && (code === 'SLOW' || code === 'BLOCKED'))) {
73
+ return { cause: 'Slow or blocked navigation', hint: 'Speed up routing or ensure target page loads reliably.' };
74
+ }
75
+
76
+ if (code === 'INTENT_DRIFT' || tags.includes('drift')) {
77
+ return { cause: 'Page no longer matches visitor intent', hint: 'Restore intent-aligned content and CTA on the target page.' };
78
+ }
79
+
80
+ return { cause: 'Unknown failure', hint: 'Investigate logs and UI for missing elements or errors.' };
81
+ }
82
+
83
+ function analyzeFailure(journeyResult) {
84
+ const totalSteps = (journeyResult.executedSteps?.length || 0) + (journeyResult.failedSteps?.length || 0);
85
+ const steps = journeyResult.executedSteps || [];
86
+ const failed = journeyResult.failedSteps || [];
87
+
88
+ let failureStepId = null;
89
+ let failureStepIdx = -1;
90
+ let failureStep = null;
91
+
92
+ if (failed.length > 0) {
93
+ // failedSteps may be an array of IDs; locate the first failing step
94
+ const firstFailId = typeof failed[0] === 'string' ? failed[0] : failed[0]?.id || failed[0];
95
+ failureStepId = firstFailId;
96
+ failureStepIdx = steps.findIndex(s => s.id === firstFailId);
97
+ if (failureStepIdx === -1 && typeof firstFailId === 'number') {
98
+ failureStepIdx = firstFailId;
99
+ }
100
+ } else {
101
+ // Otherwise, look for error status in executed steps
102
+ failureStepIdx = steps.findIndex(s => s.status === 'error' || s.status === 'timeout' || s.result?.status === 'error');
103
+ if (failureStepIdx !== -1) failureStepId = steps[failureStepIdx].id;
104
+ }
105
+
106
+ if (failureStepIdx === -1) {
107
+ // No explicit failure found; classify after goal if success, else before goal as default
108
+ const stage = classifyFailureStage(totalSteps - 1, totalSteps, totalSteps - 1, journeyResult.goal?.goalReached === true || journeyResult.success === true);
109
+ const causeInfo = { cause: 'Unknown failure', hint: 'Investigate logs and UI for missing elements or errors.' };
110
+ return {
111
+ failureStepId: failureStepId,
112
+ failureStage: stage,
113
+ cause: causeInfo.cause,
114
+ hint: causeInfo.hint,
115
+ };
116
+ }
117
+
118
+ failureStep = steps[failureStepIdx] || null;
119
+ const goalReached = journeyResult.goal?.goalReached === true || journeyResult.success === true;
120
+ let stage = 'AFTER_GOAL';
121
+ if (!goalReached) {
122
+ // Default to BEFORE_GOAL when goal not reached unless explicitly marked as goal step
123
+ const goalStepId = journeyResult.goal?.goalStepId;
124
+ if (goalStepId && (failureStep?.id === goalStepId || failureStepIdx === goalStepId)) {
125
+ stage = 'AT_GOAL';
126
+ } else {
127
+ stage = 'BEFORE_GOAL';
128
+ }
129
+ }
130
+ const causeInfo = determineCause(failureStep);
131
+
132
+ return {
133
+ failureStepId: failureStep?.id || failureStepIdx,
134
+ failureStage: stage,
135
+ cause: causeInfo.cause,
136
+ hint: causeInfo.hint,
137
+ };
138
+ }
139
+
140
+ function buildSignature(info) {
141
+ return `${info.failureStage}|${info.cause}|${info.failureStepId ?? 'unknown'}`;
142
+ }
143
+
144
+ function recordSignature(siteUrl, info) {
145
+ const data = loadSignatures();
146
+ const domain = getDomain(siteUrl);
147
+ if (!data.sites[domain]) data.sites[domain] = { signatures: {} };
148
+ const sig = buildSignature(info);
149
+ const entry = data.sites[domain].signatures[sig] || { count: 0, lastSeen: null };
150
+ entry.count += 1;
151
+ entry.lastSeen = new Date().toISOString();
152
+ data.sites[domain].signatures[sig] = entry;
153
+ saveSignatures(data);
154
+ return { signature: sig, count: entry.count };
155
+ }
156
+
157
+ function getSignatureCount(siteUrl, info) {
158
+ const data = loadSignatures();
159
+ const domain = getDomain(siteUrl);
160
+ const sig = buildSignature(info);
161
+ return data.sites[domain]?.signatures?.[sig]?.count || 0;
162
+ }
163
+
164
+ module.exports = {
165
+ analyzeFailure,
166
+ determineCause,
167
+ classifyFailureStage,
168
+ recordSignature,
169
+ getSignatureCount,
170
+ buildSignature,
171
+ loadSignatures,
172
+ saveSignatures,
173
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * First-Run State Tracking
3
+ *
4
+ * Detects if this is the user's first invocation of Guardian
5
+ * Applies conservative "golden path" profile on first run
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ const FIRST_RUN_STATE_DIR = path.join(os.homedir(), '.odavl-guardian');
13
+ const FIRST_RUN_MARKER = path.join(FIRST_RUN_STATE_DIR, 'first-run-complete.json');
14
+
15
+ /**
16
+ * Check if this is the user's first run
17
+ */
18
+ function isFirstRun() {
19
+ return !fs.existsSync(FIRST_RUN_MARKER);
20
+ }
21
+
22
+ /**
23
+ * Mark first run as complete
24
+ */
25
+ function markFirstRunComplete() {
26
+ try {
27
+ if (!fs.existsSync(FIRST_RUN_STATE_DIR)) {
28
+ fs.mkdirSync(FIRST_RUN_STATE_DIR, { recursive: true });
29
+ }
30
+ fs.writeFileSync(FIRST_RUN_MARKER, JSON.stringify({
31
+ completedAt: new Date().toISOString(),
32
+ version: require('../../package.json').version
33
+ }), 'utf8');
34
+ } catch (e) {
35
+ // Silent fail if we can't write state
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Get first-run execution profile
41
+ * Conservative settings for initial scan
42
+ */
43
+ function getFirstRunProfile() {
44
+ return {
45
+ // Timeouts: more generous for first run
46
+ timeout: 25000, // 25s (vs default 20s)
47
+ // Disable resource-intensive options
48
+ parallel: 1, // Single-threaded
49
+ failFast: false,
50
+ fast: false, // Don't skip for speed
51
+ // Minimal discovery
52
+ enableDiscovery: false,
53
+ enableCrawl: true, // Light crawl only
54
+ maxPages: 10, // Fewer pages
55
+ maxDepth: 2, // Shallower
56
+ // Evidence capture
57
+ enableScreenshots: true,
58
+ enableTrace: false, // Traces can slow things down
59
+ headful: false, // Headless is faster
60
+ // Safety
61
+ includeUniversal: false,
62
+ // CI mode off for first run (more readable output)
63
+ ciMode: false
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Apply first-run profile to config
69
+ * Merges conservative defaults without overwriting user intent on --url
70
+ */
71
+ function applyFirstRunProfile(userConfig) {
72
+ if (!isFirstRun()) {
73
+ return userConfig; // Not first run; use as-is
74
+ }
75
+
76
+ const profile = getFirstRunProfile();
77
+ return {
78
+ ...profile,
79
+ ...userConfig, // User overrides profile
80
+ baseUrl: userConfig.baseUrl // Preserve required --url
81
+ };
82
+ }
83
+
84
+ module.exports = {
85
+ isFirstRun,
86
+ markFirstRunComplete,
87
+ getFirstRunProfile,
88
+ applyFirstRunProfile
89
+ };
@@ -41,9 +41,14 @@ function printWelcome(label = 'ODAVL Guardian') {
41
41
  console.log(lines.join('\n'));
42
42
  }
43
43
 
44
+ function printFirstRunHint() {
45
+ console.log("\nTip: Try 'guardian smoke <url>' for a fast CI-ready check.\n");
46
+ }
47
+
44
48
  module.exports = {
45
49
  isFirstRun,
46
50
  hasRunBefore,
47
51
  markAsRun,
48
- printWelcome
52
+ printWelcome,
53
+ printFirstRunHint
49
54
  };
@@ -5,7 +5,9 @@
5
5
 
6
6
  const VALID_SUBCOMMANDS = [
7
7
  'init', 'protect', 'reality', 'attempt', 'baseline',
8
- 'presets', 'evaluate', 'version', 'flow', 'scan', 'smoke'
8
+ 'presets', 'template', 'list', 'cleanup', 'evaluate', 'version', 'flow', 'scan', 'smoke', 'check',
9
+ 'journey-scan', 'journey', 'live', 'plan', 'upgrade', 'ci', 'feedback',
10
+ 'sites', 'users', 'audit', 'export', 'recipe'
9
11
  ];
10
12
 
11
13
  const VALID_GLOBAL_FLAGS = [
@@ -14,13 +16,25 @@ const VALID_GLOBAL_FLAGS = [
14
16
 
15
17
  const VALID_SUBCOMMAND_FLAGS = {
16
18
  'scan': ['--url', '--preset', '--artifacts', '--policy', '--headful', '--no-trace', '--no-screenshots', '--watch', '-w', '--fast', '--fail-fast', '--timeout-profile', '--attempts', '--parallel', '--help', '-h'],
19
+ 'journey-scan': ['--url', '--preset', '--out', '--artifacts', '--timeout', '--headful', '--help', '-h'],
20
+ 'journey': ['--url', '--preset', '--out', '--artifacts', '--timeout', '--headful', '--help', '-h'],
21
+ 'live': ['--url', '--preset', '--out', '--artifacts', '--timeout', '--interval', '--cooldown', '--headful', '--help', '-h'],
17
22
  'protect': ['--url', '--policy', '--webhook', '--watch', '-w', '--fast', '--fail-fast', '--timeout-profile', '--attempts', '--parallel', '--help', '-h'],
18
- 'reality': ['--url', '--attempts', '--artifacts', '--policy', '--discover', '--universal', '--webhook', '--headful', '--watch', '-w', '--no-trace', '--no-screenshots', '--fast', '--fail-fast', '--timeout-profile', '--parallel', '--help', '-h'],
23
+ 'reality': ['--url', '--attempts', '--artifacts', '--policy', '--preset', '--discover', '--universal', '--webhook', '--headful', '--watch', '-w', '--no-trace', '--no-screenshots', '--fast', '--fail-fast', '--timeout-profile', '--parallel', '--help', '-h', '--max-pages', '--max-depth', '--timeout'],
19
24
  'attempt': ['--url', '--attempt', '--artifacts', '--headful', '--no-trace', '--no-screenshots', '--help', '-h'],
20
25
  'smoke': ['--url', '--headful', '--budget-ms', '--help', '-h'],
26
+ 'check': ['--url', '--headful', '--budget-ms', '--help', '-h'],
21
27
  'baseline': [],
22
28
  'init': ['--preset', '--help', '-h'],
23
- 'presets': ['--help', '-h']
29
+ 'list': ['--artifacts', '--failed', '--site', '--limit', '--help', '-h'],
30
+ 'cleanup': ['--artifacts', '--older-than', '--keep-latest', '--failed-only', '--help', '-h'],
31
+ 'presets': ['--help', '-h'],
32
+ 'template': ['--output', '--help', '-h'],
33
+ 'sites': ['--project', '--help', '-h'],
34
+ 'users': ['--help', '-h'],
35
+ 'audit': ['--limit', '--action', '--user', '--help', '-h'],
36
+ 'export': ['--format', '--output', '--help', '-h'],
37
+ 'recipe': ['--url', '--file', '--out', '--force', '--help', '-h']
24
38
  };
25
39
 
26
40
  function validateFlags(argv) {
@@ -5,6 +5,7 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
+ const { getFounderBadgeHTML } = require('../founder/founder-tracker');
8
9
 
9
10
  class GuardianHTMLReporter {
10
11
  /**
@@ -207,6 +208,7 @@ class GuardianHTMLReporter {
207
208
  <div class="subtitle">Market Reality Testing Report</div>
208
209
  </div>
209
210
 
211
+ ${getFounderBadgeHTML()}
210
212
  ${this.generateVerdictSection(jsonReport)}
211
213
  ${this.generateMetricsSection(jsonReport)}
212
214
  ${this.generateReasonsSection(jsonReport)}