@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,213 @@
1
+ /**
2
+ * Phase 10: Founder Tracker
3
+ * Detects and tracks first 100 users (Founding Users)
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+ const crypto = require('crypto');
10
+
11
+ const FOUNDER_DIR = path.join(os.homedir(), '.odavl-guardian', 'founder');
12
+ const FOUNDER_FILE = path.join(FOUNDER_DIR, 'status.json');
13
+ const GLOBAL_COUNTER_FILE = path.join(FOUNDER_DIR, 'global-counter.json');
14
+ const FOUNDER_LIMIT = 100;
15
+
16
+ /**
17
+ * Ensure founder directory exists
18
+ */
19
+ function ensureFounderDir() {
20
+ if (!fs.existsSync(FOUNDER_DIR)) {
21
+ fs.mkdirSync(FOUNDER_DIR, { recursive: true });
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Generate unique machine ID (stable across runs)
27
+ */
28
+ function getMachineId() {
29
+ const machineFile = path.join(FOUNDER_DIR, 'machine-id.txt');
30
+ ensureFounderDir();
31
+
32
+ if (fs.existsSync(machineFile)) {
33
+ return fs.readFileSync(machineFile, 'utf-8').trim();
34
+ }
35
+
36
+ // Generate new machine ID
37
+ const id = crypto.randomBytes(16).toString('hex');
38
+ fs.writeFileSync(machineFile, id, 'utf-8');
39
+ return id;
40
+ }
41
+
42
+ /**
43
+ * Get global founder counter
44
+ */
45
+ function getGlobalCounter() {
46
+ ensureFounderDir();
47
+
48
+ if (!fs.existsSync(GLOBAL_COUNTER_FILE)) {
49
+ return { count: 0, users: [] };
50
+ }
51
+
52
+ try {
53
+ return JSON.parse(fs.readFileSync(GLOBAL_COUNTER_FILE, 'utf-8'));
54
+ } catch (error) {
55
+ return { count: 0, users: [] };
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Save global founder counter
61
+ */
62
+ function saveGlobalCounter(counter) {
63
+ ensureFounderDir();
64
+ fs.writeFileSync(GLOBAL_COUNTER_FILE, JSON.stringify(counter, null, 2), 'utf-8');
65
+ }
66
+
67
+ /**
68
+ * Get founder status for current user
69
+ */
70
+ function getFounderStatus() {
71
+ ensureFounderDir();
72
+
73
+ if (!fs.existsSync(FOUNDER_FILE)) {
74
+ return null;
75
+ }
76
+
77
+ try {
78
+ return JSON.parse(fs.readFileSync(FOUNDER_FILE, 'utf-8'));
79
+ } catch (error) {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Save founder status for current user
86
+ */
87
+ function saveFounderStatus(status) {
88
+ ensureFounderDir();
89
+ fs.writeFileSync(FOUNDER_FILE, JSON.stringify(status, null, 2), 'utf-8');
90
+ }
91
+
92
+ /**
93
+ * Check if user is a founding user
94
+ */
95
+ function isFoundingUser() {
96
+ const status = getFounderStatus();
97
+ return status && status.isFounder === true;
98
+ }
99
+
100
+ /**
101
+ * Register current user (call on first scan)
102
+ */
103
+ function registerUser() {
104
+ // Check if already registered
105
+ const existingStatus = getFounderStatus();
106
+ if (existingStatus) {
107
+ return existingStatus;
108
+ }
109
+
110
+ const machineId = getMachineId();
111
+ const counter = getGlobalCounter();
112
+
113
+ // Check if machine ID already registered
114
+ const alreadyRegistered = counter.users.includes(machineId);
115
+
116
+ if (alreadyRegistered) {
117
+ // Find their position
118
+ const position = counter.users.indexOf(machineId) + 1;
119
+ const status = {
120
+ isFounder: position <= FOUNDER_LIMIT,
121
+ registeredAt: new Date().toISOString(),
122
+ founderNumber: position <= FOUNDER_LIMIT ? position : null,
123
+ machineId,
124
+ };
125
+ saveFounderStatus(status);
126
+ return status;
127
+ }
128
+
129
+ // New user - add to global counter
130
+ counter.count += 1;
131
+ counter.users.push(machineId);
132
+ saveGlobalCounter(counter);
133
+
134
+ const isFounder = counter.count <= FOUNDER_LIMIT;
135
+ const status = {
136
+ isFounder,
137
+ registeredAt: new Date().toISOString(),
138
+ founderNumber: isFounder ? counter.count : null,
139
+ machineId,
140
+ };
141
+
142
+ saveFounderStatus(status);
143
+ return status;
144
+ }
145
+
146
+ /**
147
+ * Get founder message for display
148
+ */
149
+ function getFounderMessage() {
150
+ const status = getFounderStatus();
151
+
152
+ if (!status) {
153
+ return null;
154
+ }
155
+
156
+ if (status.isFounder) {
157
+ return `🌟 You're Founding User #${status.founderNumber} — thank you for helping shape Guardian.`;
158
+ }
159
+
160
+ return null;
161
+ }
162
+
163
+ /**
164
+ * Get founder badge for reports (HTML)
165
+ */
166
+ function getFounderBadgeHTML() {
167
+ const status = getFounderStatus();
168
+
169
+ if (!status || !status.isFounder) {
170
+ return '';
171
+ }
172
+
173
+ return `
174
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px 20px; border-radius: 8px; margin: 16px 0; text-align: center; font-weight: 600; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);">
175
+ 🌟 Founding User #${status.founderNumber}
176
+ <div style="font-size: 0.85em; opacity: 0.9; margin-top: 4px;">Thank you for being an early supporter</div>
177
+ </div>
178
+ `;
179
+ }
180
+
181
+ /**
182
+ * Get total registered users count
183
+ */
184
+ function getTotalUsers() {
185
+ const counter = getGlobalCounter();
186
+ return counter.count;
187
+ }
188
+
189
+ /**
190
+ * Reset founder data (for testing only)
191
+ */
192
+ function resetFounderData() {
193
+ if (fs.existsSync(FOUNDER_FILE)) {
194
+ fs.unlinkSync(FOUNDER_FILE);
195
+ }
196
+ if (fs.existsSync(GLOBAL_COUNTER_FILE)) {
197
+ fs.unlinkSync(GLOBAL_COUNTER_FILE);
198
+ }
199
+ const machineFile = path.join(FOUNDER_DIR, 'machine-id.txt');
200
+ if (fs.existsSync(machineFile)) {
201
+ fs.unlinkSync(machineFile);
202
+ }
203
+ }
204
+
205
+ module.exports = {
206
+ registerUser,
207
+ isFoundingUser,
208
+ getFounderStatus,
209
+ getFounderMessage,
210
+ getFounderBadgeHTML,
211
+ getTotalUsers,
212
+ resetFounderData,
213
+ };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Phase 10: Usage Signals Tracker
3
+ * Minimal local tracking of key milestones
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const SIGNALS_DIR = path.join(os.homedir(), '.odavl-guardian');
11
+ const SIGNALS_FILE = path.join(SIGNALS_DIR, 'signals.json');
12
+
13
+ /**
14
+ * Ensure signals directory exists
15
+ */
16
+ function ensureSignalsDir() {
17
+ if (!fs.existsSync(SIGNALS_DIR)) {
18
+ fs.mkdirSync(SIGNALS_DIR, { recursive: true });
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Get current signals
24
+ */
25
+ function getSignals() {
26
+ ensureSignalsDir();
27
+
28
+ if (!fs.existsSync(SIGNALS_FILE)) {
29
+ return {
30
+ firstScanAt: null,
31
+ firstLiveAt: null,
32
+ firstUpgradeAt: null,
33
+ totalScans: 0,
34
+ totalLiveSessions: 0,
35
+ };
36
+ }
37
+
38
+ try {
39
+ return JSON.parse(fs.readFileSync(SIGNALS_FILE, 'utf-8'));
40
+ } catch (error) {
41
+ return {
42
+ firstScanAt: null,
43
+ firstLiveAt: null,
44
+ firstUpgradeAt: null,
45
+ totalScans: 0,
46
+ totalLiveSessions: 0,
47
+ };
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Save signals
53
+ */
54
+ function saveSignals(signals) {
55
+ ensureSignalsDir();
56
+ fs.writeFileSync(SIGNALS_FILE, JSON.stringify(signals, null, 2), 'utf-8');
57
+ }
58
+
59
+ /**
60
+ * Record first scan
61
+ */
62
+ function recordFirstScan() {
63
+ const signals = getSignals();
64
+
65
+ if (!signals.firstScanAt) {
66
+ signals.firstScanAt = new Date().toISOString();
67
+ }
68
+
69
+ signals.totalScans += 1;
70
+ saveSignals(signals);
71
+
72
+ return signals;
73
+ }
74
+
75
+ /**
76
+ * Record first live guardian session
77
+ */
78
+ function recordFirstLive() {
79
+ const signals = getSignals();
80
+
81
+ if (!signals.firstLiveAt) {
82
+ signals.firstLiveAt = new Date().toISOString();
83
+ }
84
+
85
+ signals.totalLiveSessions += 1;
86
+ saveSignals(signals);
87
+
88
+ return signals;
89
+ }
90
+
91
+ /**
92
+ * Record first upgrade
93
+ */
94
+ function recordFirstUpgrade(planId) {
95
+ const signals = getSignals();
96
+
97
+ if (!signals.firstUpgradeAt) {
98
+ signals.firstUpgradeAt = new Date().toISOString();
99
+ signals.firstUpgradePlan = planId;
100
+ }
101
+
102
+ saveSignals(signals);
103
+
104
+ return signals;
105
+ }
106
+
107
+ /**
108
+ * Get usage summary
109
+ */
110
+ function getUsageSummary() {
111
+ const signals = getSignals();
112
+
113
+ return {
114
+ hasScanned: signals.firstScanAt !== null,
115
+ hasUsedLive: signals.firstLiveAt !== null,
116
+ hasUpgraded: signals.firstUpgradeAt !== null,
117
+ totalScans: signals.totalScans,
118
+ totalLiveSessions: signals.totalLiveSessions,
119
+ daysSinceFirstScan: signals.firstScanAt
120
+ ? Math.floor((Date.now() - new Date(signals.firstScanAt).getTime()) / (1000 * 60 * 60 * 24))
121
+ : 0,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Reset signals (for testing)
127
+ */
128
+ function resetSignals() {
129
+ if (fs.existsSync(SIGNALS_FILE)) {
130
+ fs.unlinkSync(SIGNALS_FILE);
131
+ }
132
+ }
133
+
134
+ module.exports = {
135
+ recordFirstScan,
136
+ recordFirstLive,
137
+ recordFirstUpgrade,
138
+ getSignals,
139
+ getUsageSummary,
140
+ resetSignals,
141
+ };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Alert Ledger - Deduplication and Cooldown
3
+ * Tracks emitted alerts to prevent spam
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const crypto = require('crypto');
9
+
10
+ /**
11
+ * Compute deterministic signature from drift reasons
12
+ * @param {string[]} reasons - Array of drift reasons
13
+ * @returns {string} - SHA256 hash (hex)
14
+ */
15
+ function computeAlertSignature(reasons) {
16
+ if (!Array.isArray(reasons) || reasons.length === 0) return null;
17
+
18
+ // Normalize: sort and stringify
19
+ const normalized = reasons.slice().sort();
20
+ const str = JSON.stringify(normalized);
21
+ return crypto.createHash('sha256').update(str).digest('hex');
22
+ }
23
+
24
+ function getLedgerPath(outDir) {
25
+ const alertDir = path.join(outDir, 'alerts');
26
+ if (!fs.existsSync(alertDir)) fs.mkdirSync(alertDir, { recursive: true });
27
+ return path.join(alertDir, 'ledger.json');
28
+ }
29
+
30
+ function loadLedger(outDir) {
31
+ const ledgerPath = getLedgerPath(outDir);
32
+ if (!fs.existsSync(ledgerPath)) return { alerts: [] };
33
+ try {
34
+ return JSON.parse(fs.readFileSync(ledgerPath, 'utf8'));
35
+ } catch {
36
+ return { alerts: [] };
37
+ }
38
+ }
39
+
40
+ function saveLedger(outDir, ledger) {
41
+ const ledgerPath = getLedgerPath(outDir);
42
+ fs.writeFileSync(ledgerPath, JSON.stringify(ledger, null, 2), 'utf8');
43
+ }
44
+
45
+ /**
46
+ * Determine if alert should be emitted based on deduplication and cooldown
47
+ * @param {string[]} reasons - Drift reasons
48
+ * @param {string} severity - 'CRITICAL' or 'WARN'
49
+ * @param {string} outDir - Artifacts directory
50
+ * @param {number} cooldownMinutes - Cooldown period (default 60)
51
+ * @returns {object} - {emit: boolean, reason: string, signature: string, severity: string}
52
+ */
53
+ function shouldEmitAlert(reasons, severity, outDir, cooldownMinutes = 60) {
54
+ const signature = computeAlertSignature(reasons);
55
+ if (!signature) return { emit: false, reason: 'no drift reasons', signature: null, severity };
56
+
57
+ const ledger = loadLedger(outDir);
58
+
59
+ // First alert ever
60
+ if (ledger.alerts.length === 0) {
61
+ return { emit: true, reason: 'first alert (no ledger)', signature, severity };
62
+ }
63
+
64
+ // Find existing alert with same signature
65
+ const existingAlert = ledger.alerts.find(a => a.signature === signature);
66
+
67
+ if (!existingAlert) {
68
+ // New drift pattern
69
+ return { emit: true, reason: 'new drift pattern', signature, severity };
70
+ }
71
+
72
+ // Duplicate signature - check cooldown
73
+ const lastTime = new Date(existingAlert.timestamp).getTime();
74
+ const now = Date.now();
75
+ const elapsedMinutes = (now - lastTime) / (60 * 1000);
76
+
77
+ if (elapsedMinutes < cooldownMinutes) {
78
+ // Check severity escalation
79
+ if (existingAlert.severity === 'WARN' && severity === 'CRITICAL') {
80
+ return { emit: true, reason: 'severity escalated from WARN to CRITICAL', signature, severity };
81
+ }
82
+ // Suppressed by cooldown
83
+ return { emit: false, reason: `duplicate alert (cooldown active: ${Math.round(elapsedMinutes)}/${cooldownMinutes}min)`, signature, severity };
84
+ }
85
+
86
+ // Cooldown expired
87
+ return { emit: true, reason: `cooldown expired (${Math.round(elapsedMinutes)}min)`, signature, severity };
88
+ }
89
+
90
+ /**
91
+ * Record emitted alert to ledger
92
+ * @param {string} signature - Alert signature
93
+ * @param {string} severity - 'CRITICAL' or 'WARN'
94
+ * @param {string} outDir - Artifacts directory
95
+ */
96
+ function recordAlert(signature, severity, outDir) {
97
+ const ledger = loadLedger(outDir);
98
+
99
+ const alert = {
100
+ signature,
101
+ severity,
102
+ timestamp: new Date().toISOString()
103
+ };
104
+
105
+ // Update or add alert
106
+ const existingIndex = ledger.alerts.findIndex(a => a.signature === signature);
107
+ if (existingIndex >= 0) {
108
+ ledger.alerts[existingIndex] = alert;
109
+ } else {
110
+ ledger.alerts.push(alert);
111
+ }
112
+
113
+ saveLedger(outDir, ledger);
114
+ }
115
+
116
+ module.exports = {
117
+ computeAlertSignature,
118
+ shouldEmitAlert,
119
+ recordAlert,
120
+ loadLedger
121
+ };