@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,95 @@
1
+ /**
2
+ * Live Guardian CLI
3
+ * Periodically run journey scans and compare against baseline.
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { runJourneyScanCLI } = require('./journey-scan-cli');
9
+ const { getJourneyDefinition } = require('./journey-definitions');
10
+ const { detectIntent } = require('./intent-detector');
11
+ const { JourneyScanner } = require('./journey-scanner');
12
+ const { buildBaselineFromJourneyResult, compareAgainstBaseline, classifySeverity } = require('./drift-detector');
13
+ const { shouldEmitAlert, recordAlert } = require('./alert-ledger');
14
+
15
+ async function runLiveCLI(config) {
16
+ const { baseUrl, artifactsDir = './.odavlguardian', intervalMinutes = null, headless = true, timeout = 20000, preset, presetProvided = false, cooldownMinutes = 60 } = config;
17
+
18
+ // Ensure output directories
19
+ if (!fs.existsSync(artifactsDir)) fs.mkdirSync(artifactsDir, { recursive: true });
20
+ const baselinePath = path.join(artifactsDir, 'baseline.json');
21
+
22
+ async function singleRunEvaluate() {
23
+ // Auto intent + journey selection if preset not provided
24
+ let finalPreset = preset || 'saas';
25
+ let intentDetection = null;
26
+ if (!presetProvided) {
27
+ intentDetection = await detectIntent(baseUrl, { timeout, headless });
28
+ if (intentDetection.intent === 'saas') finalPreset = 'saas';
29
+ else if (intentDetection.intent === 'shop') finalPreset = 'shop';
30
+ else if (intentDetection.intent === 'landing') finalPreset = 'landing';
31
+ else finalPreset = 'landing';
32
+ }
33
+
34
+ const journey = getJourneyDefinition(finalPreset);
35
+ const scanner = new JourneyScanner({ timeout, headless, screenshotDir: path.join(artifactsDir, 'screenshots') });
36
+ const result = await scanner.scan(baseUrl, journey);
37
+ if (intentDetection) result.intentDetection = intentDetection;
38
+
39
+ // Baseline capture on first run
40
+ if (!fs.existsSync(baselinePath)) {
41
+ const baseline = buildBaselineFromJourneyResult(result);
42
+ fs.writeFileSync(baselinePath, JSON.stringify(baseline, null, 2), 'utf8');
43
+ console.log('📌 Baseline captured.');
44
+ return { result, baseline, drift: { hasDrift: false, reasons: [] }, exitCode: 0 };
45
+ }
46
+
47
+ // Load baseline and compare
48
+ const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
49
+ const drift = compareAgainstBaseline(baseline, result);
50
+ const severity = classifySeverity(drift, result);
51
+
52
+ // Write report and baseline comparison summary
53
+ const { HumanReporter } = require('./human-reporter');
54
+ result.baseline = baseline;
55
+ result.drift = drift;
56
+ result.severity = severity;
57
+ const reporter = new HumanReporter();
58
+ reporter.generateSummary(result, artifactsDir);
59
+ reporter.generateJSON(result, artifactsDir);
60
+
61
+ if (drift.hasDrift) {
62
+ const alertDecision = shouldEmitAlert(drift.reasons, severity, artifactsDir, cooldownMinutes);
63
+
64
+ if (alertDecision.emit) {
65
+ console.log(`🚨 Guardian detected a behavioral regression (${severity}):`);
66
+ for (const r of drift.reasons) console.log(`– ${r}`);
67
+ recordAlert(alertDecision.signature, severity, artifactsDir);
68
+ return { result, baseline, drift, severity, exitCode: 3 };
69
+ } else {
70
+ console.log(`✅ Drift detected but alert suppressed (${alertDecision.reason})`);
71
+ return { result, baseline, drift, severity, exitCode: 0 };
72
+ }
73
+ }
74
+
75
+ console.log('✅ No regression detected.');
76
+ return { result, baseline, drift, severity, exitCode: 0 };
77
+ }
78
+
79
+ if (intervalMinutes && intervalMinutes > 0) {
80
+ let running = true;
81
+ const handleSigint = () => { running = false; console.log('\n🛑 Live Guardian stopped.'); process.exit(0); };
82
+ process.on('SIGINT', handleSigint);
83
+ console.log(`⏱️ Live mode: every ${intervalMinutes} minute(s)`);
84
+ while (running) {
85
+ const { exitCode } = await singleRunEvaluate();
86
+ if (exitCode === 3) process.exit(3);
87
+ await new Promise(r => setTimeout(r, intervalMinutes * 60 * 1000));
88
+ }
89
+ } else {
90
+ const { exitCode } = await singleRunEvaluate();
91
+ process.exit(exitCode);
92
+ }
93
+ }
94
+
95
+ module.exports = { runLiveCLI };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Live Scheduler Runner
3
+ * Detached background process that executes schedules on time.
4
+ *
5
+ * Each tick invokes the existing CLI path: "guardian live <url> [--preset ...]"
6
+ * to reuse baseline/drift/alerts and plan/RBAC enforcement without refactoring.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+ const { spawn } = require('child_process');
13
+
14
+ const SCHED_DIR = path.join(os.homedir(), '.odavl-guardian', 'scheduler');
15
+ const STATE_FILE = path.join(SCHED_DIR, 'schedules.json');
16
+
17
+ // Active timers per schedule id
18
+ const timers = new Map();
19
+
20
+ function loadState() {
21
+ try {
22
+ const data = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
23
+ if (!data.schedules) data.schedules = [];
24
+ return data;
25
+ } catch {
26
+ return { schedules: [] };
27
+ }
28
+ }
29
+
30
+ function saveState(state) {
31
+ const data = {
32
+ schedules: state.schedules || [],
33
+ runner: state.runner || null,
34
+ updatedAt: new Date().toISOString(),
35
+ };
36
+ fs.writeFileSync(STATE_FILE, JSON.stringify(data, null, 2), 'utf-8');
37
+ }
38
+
39
+ function scheduleRun(entry) {
40
+ // Clear existing timer
41
+ const existing = timers.get(entry.id);
42
+ if (existing) {
43
+ clearTimeout(existing);
44
+ timers.delete(entry.id);
45
+ }
46
+
47
+ if (entry.status !== 'running') {
48
+ return;
49
+ }
50
+
51
+ const now = Date.now();
52
+ const nextTs = entry.nextRunAt ? Date.parse(entry.nextRunAt) : (now + entry.intervalMinutes * 60 * 1000);
53
+ const delay = Math.max(0, nextTs - now);
54
+
55
+ const t = setTimeout(() => {
56
+ executeLive(entry).then(() => {
57
+ // Update times and reschedule
58
+ const state = loadState();
59
+ const s = state.schedules.find(x => x.id === entry.id);
60
+ if (s) {
61
+ const finished = Date.now();
62
+ s.lastRunAt = new Date(finished).toISOString();
63
+ s.nextRunAt = new Date(finished + s.intervalMinutes * 60 * 1000).toISOString();
64
+ saveState(state);
65
+ scheduleRun(s);
66
+ }
67
+ }).catch(() => {
68
+ // On failure, still advance nextRunAt
69
+ const state = loadState();
70
+ const s = state.schedules.find(x => x.id === entry.id);
71
+ if (s) {
72
+ const finished = Date.now();
73
+ s.lastRunAt = new Date(finished).toISOString();
74
+ s.nextRunAt = new Date(finished + s.intervalMinutes * 60 * 1000).toISOString();
75
+ saveState(state);
76
+ scheduleRun(s);
77
+ }
78
+ });
79
+ }, delay);
80
+
81
+ timers.set(entry.id, t);
82
+ }
83
+
84
+ function executeLive(entry) {
85
+ return new Promise((resolve) => {
86
+ // Spawn the existing CLI path in a child process.
87
+ const nodeExec = process.execPath;
88
+ const binPath = path.join(__dirname, '..', '..', 'bin', 'guardian.js');
89
+ const args = ['live', '--url', entry.url];
90
+ if (entry.preset) {
91
+ args.push('--preset', entry.preset);
92
+ }
93
+
94
+ const child = spawn(nodeExec, [binPath, ...args], {
95
+ stdio: 'ignore',
96
+ windowsHide: true,
97
+ });
98
+
99
+ child.on('exit', () => resolve());
100
+ child.on('error', () => resolve());
101
+ });
102
+ }
103
+
104
+ function reconcile() {
105
+ const state = loadState();
106
+ // For every running schedule, ensure a timer exists
107
+ for (const s of state.schedules) {
108
+ if (s.status === 'running') {
109
+ scheduleRun(s);
110
+ } else {
111
+ const t = timers.get(s.id);
112
+ if (t) {
113
+ clearTimeout(t);
114
+ timers.delete(s.id);
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ function startWatch() {
121
+ try {
122
+ fs.watch(STATE_FILE, { persistent: true }, () => {
123
+ reconcile();
124
+ });
125
+ } catch {
126
+ // Fallback: periodic reconcile
127
+ setInterval(reconcile, 5000);
128
+ }
129
+ }
130
+
131
+ function main() {
132
+ // Initial reconcile and watch for changes
133
+ reconcile();
134
+ startWatch();
135
+ }
136
+
137
+ main();
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Live Scheduler (Phase A)
3
+ * Local-first, time-based scheduler with persisted state.
4
+ *
5
+ * Schedules are stored under ~/.odavl-guardian/scheduler/schedules.json
6
+ * Background runner: live-scheduler-runner.js (detached child process).
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+ const crypto = require('crypto');
13
+ const { spawn } = require('child_process');
14
+
15
+ const SCHED_DIR = path.join(os.homedir(), '.odavl-guardian', 'scheduler');
16
+ const STATE_FILE = path.join(SCHED_DIR, 'schedules.json');
17
+ const RUNNER_FILE = path.join(__dirname, 'live-scheduler-runner.js');
18
+
19
+ function ensureSchedDir() {
20
+ if (!fs.existsSync(SCHED_DIR)) {
21
+ fs.mkdirSync(SCHED_DIR, { recursive: true });
22
+ }
23
+ }
24
+
25
+ function loadState() {
26
+ ensureSchedDir();
27
+ if (!fs.existsSync(STATE_FILE)) {
28
+ return { schedules: [], runner: null };
29
+ }
30
+ try {
31
+ const data = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
32
+ if (!data.schedules) data.schedules = [];
33
+ return data;
34
+ } catch (err) {
35
+ return { schedules: [], runner: null };
36
+ }
37
+ }
38
+
39
+ function saveState(state) {
40
+ ensureSchedDir();
41
+ const data = {
42
+ schedules: state.schedules || [],
43
+ runner: state.runner || null,
44
+ updatedAt: new Date().toISOString(),
45
+ };
46
+ fs.writeFileSync(STATE_FILE, JSON.stringify(data, null, 2), 'utf-8');
47
+ return data;
48
+ }
49
+
50
+ function genId() {
51
+ return crypto.randomBytes(8).toString('hex');
52
+ }
53
+
54
+ /**
55
+ * Create and register a new schedule
56
+ */
57
+ function createSchedule({ url, preset = 'saas', intervalMinutes }) {
58
+ if (!url || typeof url !== 'string') throw new Error('url is required');
59
+ if (!intervalMinutes || intervalMinutes <= 0) throw new Error('intervalMinutes must be > 0');
60
+
61
+ const state = loadState();
62
+ const id = genId();
63
+ const now = Date.now();
64
+ const nextRunAt = new Date(now + intervalMinutes * 60 * 1000).toISOString();
65
+
66
+ const entry = {
67
+ id,
68
+ url,
69
+ preset,
70
+ intervalMinutes,
71
+ status: 'running',
72
+ lastRunAt: null,
73
+ nextRunAt,
74
+ createdAt: new Date().toISOString(),
75
+ };
76
+
77
+ state.schedules.push(entry);
78
+ saveState(state);
79
+ return entry;
80
+ }
81
+
82
+ /**
83
+ * Stop a schedule by id
84
+ */
85
+ function stopSchedule(id) {
86
+ const state = loadState();
87
+ const s = state.schedules.find(x => x.id === id);
88
+ if (!s) throw new Error(`Schedule not found: ${id}`);
89
+ s.status = 'stopped';
90
+ saveState(state);
91
+ return s;
92
+ }
93
+
94
+ /**
95
+ * List schedules
96
+ */
97
+ function listSchedules() {
98
+ const state = loadState();
99
+ return state.schedules;
100
+ }
101
+
102
+ /**
103
+ * Check if runner pid is active
104
+ */
105
+ function isRunnerActive(pid) {
106
+ if (!pid) return false;
107
+ try { process.kill(pid, 0); return true; } catch { return false; }
108
+ }
109
+
110
+ /**
111
+ * Start background runner (detached)
112
+ */
113
+ function startBackgroundRunner() {
114
+ ensureSchedDir();
115
+ const state = loadState();
116
+
117
+ // Avoid multiple runners
118
+ if (state.runner && isRunnerActive(state.runner.pid)) {
119
+ return state.runner;
120
+ }
121
+
122
+ const nodeExec = process.execPath;
123
+ const child = spawn(nodeExec, [RUNNER_FILE], {
124
+ detached: true,
125
+ stdio: 'ignore',
126
+ windowsHide: true,
127
+ });
128
+ const runnerInfo = {
129
+ pid: child.pid,
130
+ startedAt: new Date().toISOString(),
131
+ };
132
+ child.unref();
133
+
134
+ state.runner = runnerInfo;
135
+ saveState(state);
136
+ return runnerInfo;
137
+ }
138
+
139
+ module.exports = {
140
+ createSchedule,
141
+ stopSchedule,
142
+ listSchedules,
143
+ startBackgroundRunner,
144
+ loadState,
145
+ saveState,
146
+ };