@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,99 @@
1
+ /**
2
+ * Language Detection
3
+ *
4
+ * Deterministic language detection from HTML attributes.
5
+ * No guessing, no AI. Only reads explicit language declarations.
6
+ */
7
+
8
+ /**
9
+ * Detect page language from HTML
10
+ *
11
+ * Detection order:
12
+ * 1. <html lang="..."> attribute
13
+ * 2. <meta http-equiv="content-language" ...> attribute
14
+ * 3. fallback: "unknown"
15
+ *
16
+ * @param {Page} page - Playwright page object
17
+ * @returns {Promise<string>} BCP-47 language code or "unknown"
18
+ */
19
+ async function detectLanguage(page) {
20
+ try {
21
+ // Try <html lang="...">
22
+ const htmlLang = await page.evaluate(() => {
23
+ const htmlElement = document.documentElement;
24
+ return htmlElement.getAttribute('lang');
25
+ });
26
+
27
+ if (htmlLang && htmlLang.trim()) {
28
+ // Return the language code (e.g., "de", "de-DE", "en", "en-US")
29
+ return htmlLang.trim().toLowerCase();
30
+ }
31
+
32
+ // Try <meta http-equiv="content-language" ...>
33
+ const metaLang = await page.evaluate(() => {
34
+ const meta = document.querySelector('meta[http-equiv="content-language"]');
35
+ return meta ? meta.getAttribute('content') : null;
36
+ });
37
+
38
+ if (metaLang && metaLang.trim()) {
39
+ return metaLang.trim().toLowerCase();
40
+ }
41
+
42
+ // Fallback
43
+ return 'unknown';
44
+ } catch (error) {
45
+ // If evaluation fails, return unknown
46
+ return 'unknown';
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Parse BCP-47 language code to get primary language
52
+ * e.g., "de-DE" -> "de", "en-US" -> "en"
53
+ */
54
+ function getPrimaryLanguage(languageCode) {
55
+ if (!languageCode || languageCode === 'unknown') {
56
+ return 'unknown';
57
+ }
58
+
59
+ // Extract primary language (before first hyphen)
60
+ const primary = languageCode.split('-')[0].toLowerCase();
61
+ return primary || 'unknown';
62
+ }
63
+
64
+ /**
65
+ * Get human-readable language name from code
66
+ */
67
+ const LANGUAGE_NAMES = {
68
+ 'de': 'German',
69
+ 'en': 'English',
70
+ 'es': 'Spanish',
71
+ 'fr': 'French',
72
+ 'pt': 'Portuguese',
73
+ 'it': 'Italian',
74
+ 'nl': 'Dutch',
75
+ 'sv': 'Swedish',
76
+ 'ar': 'Arabic',
77
+ 'zh': 'Chinese',
78
+ 'ja': 'Japanese',
79
+ 'ko': 'Korean',
80
+ 'ru': 'Russian',
81
+ 'pl': 'Polish',
82
+ 'unknown': 'Unknown'
83
+ };
84
+
85
+ function getLanguageName(languageCode) {
86
+ if (!languageCode || languageCode === 'unknown') {
87
+ return 'Unknown';
88
+ }
89
+
90
+ const primary = getPrimaryLanguage(languageCode);
91
+ return LANGUAGE_NAMES[primary] || `Unknown (${primary})`;
92
+ }
93
+
94
+ module.exports = {
95
+ detectLanguage,
96
+ getPrimaryLanguage,
97
+ getLanguageName,
98
+ LANGUAGE_NAMES
99
+ };
@@ -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
+ };