@odavl/guardian 0.1.0-rc1

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 (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -0
  4. package/bin/guardian.js +690 -0
  5. package/flows/example-login-flow.json +36 -0
  6. package/flows/example-signup-flow.json +44 -0
  7. package/guardian-contract-v1.md +149 -0
  8. package/guardian.config.json +54 -0
  9. package/guardian.policy.json +12 -0
  10. package/guardian.profile.docs.yaml +18 -0
  11. package/guardian.profile.ecommerce.yaml +17 -0
  12. package/guardian.profile.marketing.yaml +18 -0
  13. package/guardian.profile.saas.yaml +21 -0
  14. package/package.json +69 -0
  15. package/policies/enterprise.json +12 -0
  16. package/policies/saas.json +12 -0
  17. package/policies/startup.json +12 -0
  18. package/src/guardian/attempt-engine.js +454 -0
  19. package/src/guardian/attempt-registry.js +227 -0
  20. package/src/guardian/attempt-reporter.js +507 -0
  21. package/src/guardian/attempt.js +227 -0
  22. package/src/guardian/auto-attempt-builder.js +283 -0
  23. package/src/guardian/baseline-reporter.js +143 -0
  24. package/src/guardian/baseline-storage.js +285 -0
  25. package/src/guardian/baseline.js +492 -0
  26. package/src/guardian/behavioral-signals.js +261 -0
  27. package/src/guardian/breakage-intelligence.js +223 -0
  28. package/src/guardian/browser.js +92 -0
  29. package/src/guardian/cli-summary.js +141 -0
  30. package/src/guardian/crawler.js +142 -0
  31. package/src/guardian/discovery-engine.js +661 -0
  32. package/src/guardian/enhanced-html-reporter.js +305 -0
  33. package/src/guardian/failure-taxonomy.js +169 -0
  34. package/src/guardian/flow-executor.js +374 -0
  35. package/src/guardian/flow-registry.js +67 -0
  36. package/src/guardian/html-reporter.js +414 -0
  37. package/src/guardian/index.js +218 -0
  38. package/src/guardian/init-command.js +139 -0
  39. package/src/guardian/junit-reporter.js +264 -0
  40. package/src/guardian/market-criticality.js +335 -0
  41. package/src/guardian/market-reporter.js +305 -0
  42. package/src/guardian/network-trace.js +178 -0
  43. package/src/guardian/policy.js +357 -0
  44. package/src/guardian/preset-loader.js +148 -0
  45. package/src/guardian/reality.js +547 -0
  46. package/src/guardian/reporter.js +181 -0
  47. package/src/guardian/root-cause-analysis.js +171 -0
  48. package/src/guardian/safety.js +248 -0
  49. package/src/guardian/scan-presets.js +60 -0
  50. package/src/guardian/screenshot.js +152 -0
  51. package/src/guardian/sitemap.js +225 -0
  52. package/src/guardian/snapshot-schema.js +266 -0
  53. package/src/guardian/snapshot.js +327 -0
  54. package/src/guardian/validators.js +323 -0
  55. package/src/guardian/visual-diff.js +247 -0
  56. package/src/guardian/webhook.js +206 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Guardian Network Trace Module
3
+ * Captures network activity (HAR files) and browser traces
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ class GuardianNetworkTrace {
10
+ constructor(options = {}) {
11
+ this.enableHAR = options.enableHAR !== false; // Enable HAR by default
12
+ this.enableTrace = options.enableTrace !== false; // Enable trace by default
13
+ }
14
+
15
+ /**
16
+ * Start HAR recording for a browser context
17
+ * @param {BrowserContext} context - Playwright browser context
18
+ * @param {string} artifactsDir - Directory to save HAR
19
+ * @returns {Promise<string|null>} Path to HAR file
20
+ */
21
+ async startHAR(context, artifactsDir) {
22
+ if (!this.enableHAR) {
23
+ return null;
24
+ }
25
+
26
+ try {
27
+ const harPath = path.join(artifactsDir, 'network.har');
28
+
29
+ // Note: HAR recording must be started when creating context
30
+ // This method returns the path where HAR will be saved
31
+ return harPath;
32
+ } catch (error) {
33
+ console.error(`❌ Failed to prepare HAR recording: ${error.message}`);
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Stop HAR recording (Playwright handles this automatically on context.close())
40
+ * @param {BrowserContext} context - Playwright browser context
41
+ * @returns {Promise<boolean>} Success status
42
+ */
43
+ async stopHAR(context) {
44
+ // HAR is saved automatically when context closes
45
+ return true;
46
+ }
47
+
48
+ /**
49
+ * Start browser trace recording
50
+ * @param {BrowserContext} context - Playwright browser context
51
+ * @param {string} artifactsDir - Directory to save trace
52
+ * @returns {Promise<string|null>} Path where trace will be saved
53
+ */
54
+ async startTrace(context, artifactsDir) {
55
+ if (!this.enableTrace) {
56
+ return null;
57
+ }
58
+
59
+ try {
60
+ const tracePath = path.join(artifactsDir, 'trace.zip');
61
+
62
+ await context.tracing.start({
63
+ screenshots: true,
64
+ snapshots: true,
65
+ sources: false, // Don't include source code
66
+ });
67
+
68
+ return tracePath;
69
+ } catch (error) {
70
+ console.error(`❌ Failed to start trace: ${error.message}`);
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Stop browser trace recording and save
77
+ * @param {BrowserContext} context - Playwright browser context
78
+ * @param {string} tracePath - Where to save trace file
79
+ * @returns {Promise<boolean>} Success status
80
+ */
81
+ async stopTrace(context, tracePath) {
82
+ if (!this.enableTrace || !tracePath) {
83
+ return false;
84
+ }
85
+
86
+ try {
87
+ await context.tracing.stop({ path: tracePath });
88
+ return true;
89
+ } catch (error) {
90
+ console.error(`❌ Failed to stop trace: ${error.message}`);
91
+ return false;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Validate HAR file exists and is valid JSON
97
+ * @param {string} harPath - Path to HAR file
98
+ * @returns {boolean} True if valid
99
+ */
100
+ validateHAR(harPath) {
101
+ try {
102
+ if (!fs.existsSync(harPath)) {
103
+ return false;
104
+ }
105
+
106
+ const content = fs.readFileSync(harPath, 'utf8');
107
+ const har = JSON.parse(content);
108
+
109
+ // Basic HAR structure validation
110
+ return har.log && har.log.entries && Array.isArray(har.log.entries);
111
+ } catch (error) {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Validate trace file exists and has reasonable size
118
+ * @param {string} tracePath - Path to trace file
119
+ * @returns {boolean} True if valid
120
+ */
121
+ validateTrace(tracePath) {
122
+ try {
123
+ if (!fs.existsSync(tracePath)) {
124
+ return false;
125
+ }
126
+
127
+ const stats = fs.statSync(tracePath);
128
+ // Trace should be at least 10KB
129
+ return stats.size > 10240;
130
+ } catch (error) {
131
+ return false;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Get HAR statistics (request count, failed requests, etc.)
137
+ * @param {string} harPath - Path to HAR file
138
+ * @returns {object|null} HAR statistics
139
+ */
140
+ getHARStats(harPath) {
141
+ try {
142
+ if (!this.validateHAR(harPath)) {
143
+ return null;
144
+ }
145
+
146
+ const content = fs.readFileSync(harPath, 'utf8');
147
+ const har = JSON.parse(content);
148
+ const entries = har.log.entries;
149
+
150
+ const stats = {
151
+ totalRequests: entries.length,
152
+ failedRequests: entries.filter(e => e.response && e.response.status >= 400).length,
153
+ requestsByType: {},
154
+ totalSize: 0,
155
+ };
156
+
157
+ // Count by content type
158
+ entries.forEach(entry => {
159
+ const mimeType = entry.response?.content?.mimeType || 'unknown';
160
+ const baseType = mimeType.split(';')[0].split('/')[0]; // e.g., 'text', 'image'
161
+
162
+ stats.requestsByType[baseType] = (stats.requestsByType[baseType] || 0) + 1;
163
+
164
+ // Sum up sizes
165
+ if (entry.response?.content?.size) {
166
+ stats.totalSize += entry.response.content.size;
167
+ }
168
+ });
169
+
170
+ return stats;
171
+ } catch (error) {
172
+ console.error(`❌ Failed to parse HAR stats: ${error.message}`);
173
+ return null;
174
+ }
175
+ }
176
+ }
177
+
178
+ module.exports = GuardianNetworkTrace;
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Guardian Policy Evaluation
3
+ *
4
+ * Deterministic threshold-based gating for CI/CD pipelines.
5
+ * - Evaluate snapshot against policy thresholds
6
+ * - Determine exit code (success/warn/fail)
7
+ * - Support baseline regression detection
8
+ * - Domain-aware gates for Phase 4 (REVENUE/TRUST critical failures)
9
+ *
10
+ * NO AI. Pure deterministic logic.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { aggregateIntelligence } = require('./breakage-intelligence');
16
+
17
+ /**
18
+ * @typedef {Object} GuardianPolicy
19
+ * @property {string} [failOnSeverity='CRITICAL'] - Severity level that triggers exit 1 (CRITICAL|WARNING|INFO)
20
+ * @property {number} [maxWarnings=0] - Max WARNING count before fail
21
+ * @property {number} [maxInfo=999] - Max INFO count before fail
22
+ * @property {number} [maxTotalRisk=999] - Max total risks before fail
23
+ * @property {boolean} [failOnNewRegression=true] - Fail if baseline regression detected
24
+ * @property {boolean} [failOnSoftFailures=false] - Fail if any soft failures detected
25
+ * @property {number} [softFailureThreshold=5] - Max soft failures before fail
26
+ * @property {boolean} [requireBaseline=false] - Require baseline to exist
27
+ * @property {Object} [domainGates] - Domain-aware gates (Phase 4). Ex: { REVENUE: { CRITICAL: 0, WARNING: 3 }, TRUST: { CRITICAL: 0 } }
28
+ * @property {Object} [visualGates] - Phase 5: Visual regression gates. Ex: { CRITICAL: 0, WARNING: 999, maxDiffPercent: 25 }
29
+ */
30
+
31
+ /**
32
+ * Load policy from file or return defaults
33
+ */
34
+ function loadPolicy(policyPath = null) {
35
+ const defaultPolicy = {
36
+ failOnSeverity: 'CRITICAL',
37
+ maxWarnings: 0,
38
+ maxInfo: 999,
39
+ maxTotalRisk: 999,
40
+ failOnNewRegression: true,
41
+ failOnSoftFailures: false,
42
+ softFailureThreshold: 5,
43
+ requireBaseline: false,
44
+ domainGates: {
45
+ // Phase 4: Fail on any CRITICAL in REVENUE or TRUST domains
46
+ REVENUE: { CRITICAL: 0, WARNING: 999 },
47
+ TRUST: { CRITICAL: 0, WARNING: 999 }
48
+ },
49
+ // Phase 5: Visual regression gates
50
+ visualGates: {
51
+ CRITICAL: 0, // Fail if any CRITICAL visual diffs
52
+ WARNING: 999, // Warn if more than 999 WARNING visual diffs
53
+ maxDiffPercent: 25 // Fail if visual change > 25% of page
54
+ }
55
+ };
56
+
57
+ if (!policyPath) {
58
+ // Try to find guardian.policy.json in current directory or .odavl-guardian/
59
+ const candidates = [
60
+ 'guardian.policy.json',
61
+ '.odavl-guardian/policy.json',
62
+ '.odavl-guardian/guardian.policy.json'
63
+ ];
64
+
65
+ for (const candidate of candidates) {
66
+ if (fs.existsSync(candidate)) {
67
+ policyPath = candidate;
68
+ break;
69
+ }
70
+ }
71
+ }
72
+
73
+ // If no policy file found, use defaults
74
+ if (!policyPath || !fs.existsSync(policyPath)) {
75
+ return defaultPolicy;
76
+ }
77
+
78
+ try {
79
+ const json = fs.readFileSync(policyPath, 'utf8');
80
+ const loaded = JSON.parse(json);
81
+ return { ...defaultPolicy, ...loaded };
82
+ } catch (e) {
83
+ console.warn(`⚠️ Failed to load policy from ${policyPath}: ${e.message}`);
84
+ console.warn(' Using default policy');
85
+ return defaultPolicy;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Evaluate snapshot against policy
91
+ * Returns { passed: boolean, exitCode: 0|1|2, reasons: string[], summary: string }
92
+ */
93
+ function evaluatePolicy(snapshot, policy) {
94
+ const effectivePolicy = policy || loadPolicy();
95
+ const reasons = [];
96
+ let exitCode = 0;
97
+
98
+ // Extract market impact summary (Phase 3)
99
+ const marketImpact = snapshot.marketImpactSummary || {};
100
+ const criticalCount = marketImpact.countsBySeverity?.CRITICAL || 0;
101
+ const warningCount = marketImpact.countsBySeverity?.WARNING || 0;
102
+ const infoCount = marketImpact.countsBySeverity?.INFO || 0;
103
+ const totalRisk = marketImpact.totalRiskCount || 0;
104
+
105
+ // Extract soft failures (Phase 2)
106
+ const softFailureCount = snapshot.attempts?.reduce((sum, attempt) => {
107
+ return sum + (attempt.softFailureCount || 0);
108
+ }, 0) || 0;
109
+
110
+ // Phase 4: Check domain gates if intelligence available
111
+ if (!exitCode && effectivePolicy.domainGates && snapshot.intelligence) {
112
+ const intelligence = snapshot.intelligence;
113
+ const domainFailures = intelligence.byDomain || {};
114
+
115
+ for (const [domain, gates] of Object.entries(effectivePolicy.domainGates)) {
116
+ const domainFailure = domainFailures[domain] || { failures: [] };
117
+
118
+ // Check CRITICAL gate
119
+ if (gates.CRITICAL !== undefined) {
120
+ const criticalInDomain = domainFailure.failures?.filter(f => f.severity === 'CRITICAL').length || 0;
121
+ if (criticalInDomain > gates.CRITICAL) {
122
+ reasons.push(`Domain ${domain}: ${criticalInDomain} CRITICAL failure(s) exceed gate limit of ${gates.CRITICAL}`);
123
+ exitCode = 1;
124
+ }
125
+ }
126
+
127
+ // Check WARNING gate
128
+ if (!exitCode && gates.WARNING !== undefined) {
129
+ const warningInDomain = domainFailure.failures?.filter(f => f.severity === 'WARNING').length || 0;
130
+ if (warningInDomain > gates.WARNING) {
131
+ reasons.push(`Domain ${domain}: ${warningInDomain} WARNING failure(s) exceed gate limit of ${gates.WARNING}`);
132
+ exitCode = 2;
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ // Phase 5: Check visual regression gates if configured
139
+ if (!exitCode && effectivePolicy.visualGates && snapshot.intelligence) {
140
+ const intelligence = snapshot.intelligence;
141
+ const visualFailures = intelligence.failures?.filter(f => f.breakType === 'VISUAL') || [];
142
+ const visualCritical = visualFailures.filter(f => f.severity === 'CRITICAL').length || 0;
143
+ const visualWarning = visualFailures.filter(f => f.severity === 'WARNING').length || 0;
144
+ const maxDiffPercent = Math.max(...visualFailures.map(f => f.visualDiff?.percentChange || 0));
145
+
146
+ // Check CRITICAL visual diffs
147
+ if (effectivePolicy.visualGates.CRITICAL !== undefined) {
148
+ if (visualCritical > effectivePolicy.visualGates.CRITICAL) {
149
+ reasons.push(`Visual regression: ${visualCritical} CRITICAL diff(s) exceed gate limit of ${effectivePolicy.visualGates.CRITICAL}`);
150
+ exitCode = 1;
151
+ }
152
+ }
153
+
154
+ // Check WARNING visual diffs
155
+ if (!exitCode && effectivePolicy.visualGates.WARNING !== undefined) {
156
+ if (visualWarning > effectivePolicy.visualGates.WARNING) {
157
+ reasons.push(`Visual regression: ${visualWarning} WARNING diff(s) exceed gate limit of ${effectivePolicy.visualGates.WARNING}`);
158
+ exitCode = 2;
159
+ }
160
+ }
161
+
162
+ // Check max diff percent
163
+ if (!exitCode && effectivePolicy.visualGates.maxDiffPercent !== undefined) {
164
+ if (maxDiffPercent > effectivePolicy.visualGates.maxDiffPercent) {
165
+ reasons.push(`Visual regression: ${maxDiffPercent.toFixed(1)}% diff exceeds max threshold of ${effectivePolicy.visualGates.maxDiffPercent}%`);
166
+ exitCode = 1;
167
+ }
168
+ }
169
+ }
170
+
171
+ // Evaluate CRITICAL severity (always exit 1 if present)
172
+ if (effectivePolicy.failOnSeverity === 'CRITICAL' && criticalCount > 0) {
173
+ reasons.push(`${criticalCount} CRITICAL risk(s) detected (policy: failOnSeverity=CRITICAL)`);
174
+ exitCode = 1;
175
+ }
176
+
177
+ // Evaluate WARNING severity
178
+ if (effectivePolicy.failOnSeverity === 'WARNING' && warningCount > 0) {
179
+ reasons.push(`${warningCount} WARNING risk(s) detected (policy: failOnSeverity=WARNING)`);
180
+ exitCode = 1;
181
+ }
182
+
183
+ // Evaluate max warnings
184
+ if (!exitCode && warningCount > effectivePolicy.maxWarnings) {
185
+ reasons.push(`${warningCount} WARNING(s) exceed limit of ${effectivePolicy.maxWarnings}`);
186
+ exitCode = 2;
187
+ }
188
+
189
+ // Evaluate max info
190
+ if (!exitCode && infoCount > effectivePolicy.maxInfo) {
191
+ reasons.push(`${infoCount} INFO(s) exceed limit of ${effectivePolicy.maxInfo}`);
192
+ exitCode = 2;
193
+ }
194
+
195
+ // Evaluate total risk
196
+ if (!exitCode && totalRisk > effectivePolicy.maxTotalRisk) {
197
+ reasons.push(`${totalRisk} total risk(s) exceed limit of ${effectivePolicy.maxTotalRisk}`);
198
+ exitCode = 1;
199
+ }
200
+
201
+ // Evaluate baseline regression
202
+ if (!exitCode && effectivePolicy.failOnNewRegression) {
203
+ const baseline = snapshot.baseline || {};
204
+ const diff = baseline.diff || {};
205
+
206
+ if (diff.regressions && Object.keys(diff.regressions).length > 0) {
207
+ const regCount = Object.keys(diff.regressions).length;
208
+ reasons.push(`${regCount} baseline regression(s) detected (policy: failOnNewRegression=true)`);
209
+ exitCode = 1;
210
+ }
211
+ }
212
+
213
+ // Evaluate soft failures
214
+ if (!exitCode && effectivePolicy.failOnSoftFailures && softFailureCount > 0) {
215
+ reasons.push(`${softFailureCount} soft failure(s) detected (policy: failOnSoftFailures=true)`);
216
+ exitCode = 1;
217
+ }
218
+
219
+ // Evaluate soft failure threshold
220
+ if (!exitCode && softFailureCount > effectivePolicy.softFailureThreshold) {
221
+ reasons.push(`${softFailureCount} soft failure(s) exceed threshold of ${effectivePolicy.softFailureThreshold}`);
222
+ exitCode = 2;
223
+ }
224
+
225
+ // Evaluate baseline requirement
226
+ if (!exitCode && effectivePolicy.requireBaseline) {
227
+ const baseline = snapshot.baseline || {};
228
+ if (!baseline.baselineFound && !baseline.baselineCreatedThisRun) {
229
+ reasons.push('Baseline required but not found (policy: requireBaseline=true)');
230
+ exitCode = 1;
231
+ }
232
+ }
233
+
234
+ // Build summary
235
+ const summary =
236
+ exitCode === 0
237
+ ? '✅ Policy evaluation PASSED'
238
+ : exitCode === 1
239
+ ? '❌ Policy evaluation FAILED (exit code 1)'
240
+ : '⚠️ Policy evaluation WARNING (exit code 2)';
241
+
242
+ return {
243
+ passed: exitCode === 0,
244
+ exitCode,
245
+ reasons,
246
+ summary,
247
+ counts: {
248
+ critical: criticalCount,
249
+ warning: warningCount,
250
+ info: infoCount,
251
+ softFailures: softFailureCount,
252
+ totalRisk
253
+ }
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Format policy evaluation results for CLI output
259
+ */
260
+ function formatPolicyOutput(evaluation) {
261
+ let output = '\n' + '━'.repeat(60) + '\n';
262
+ output += '🛡️ Policy Evaluation\n';
263
+ output += '━'.repeat(60) + '\n\n';
264
+
265
+ output += `${evaluation.summary}\n`;
266
+
267
+ if (evaluation.reasons.length > 0) {
268
+ output += '\nFailure reasons:\n';
269
+ evaluation.reasons.forEach(r => {
270
+ output += ` ❌ ${r}\n`;
271
+ });
272
+ }
273
+
274
+ output += `\nRisk counts:\n`;
275
+ output += ` 🔴 CRITICAL: ${evaluation.counts.critical}\n`;
276
+ output += ` 🟡 WARNING: ${evaluation.counts.warning}\n`;
277
+ output += ` 🔵 INFO: ${evaluation.counts.info}\n`;
278
+ output += ` 🐛 Soft Failures: ${evaluation.counts.softFailures}\n`;
279
+ output += ` 📊 Total Risks: ${evaluation.counts.totalRisk}\n`;
280
+
281
+ output += `\nExit Code: ${evaluation.exitCode}\n`;
282
+ output += '━'.repeat(60) + '\n';
283
+
284
+ return output;
285
+ }
286
+
287
+ /**
288
+ * Create a default policy file
289
+ */
290
+ function createDefaultPolicyFile(outputPath = 'guardian.policy.json') {
291
+ const defaultPolicy = {
292
+ failOnSeverity: 'CRITICAL',
293
+ maxWarnings: 0,
294
+ maxInfo: 999,
295
+ maxTotalRisk: 999,
296
+ failOnNewRegression: true,
297
+ failOnSoftFailures: false,
298
+ softFailureThreshold: 5,
299
+ requireBaseline: false
300
+ };
301
+
302
+ fs.writeFileSync(
303
+ outputPath,
304
+ JSON.stringify(defaultPolicy, null, 2),
305
+ 'utf8'
306
+ );
307
+
308
+ return outputPath;
309
+ }
310
+
311
+ /**
312
+ * Validate policy object structure
313
+ */
314
+ function validatePolicy(policy) {
315
+ const errors = [];
316
+
317
+ if (!policy || typeof policy !== 'object') {
318
+ return {
319
+ valid: false,
320
+ errors: ['Policy must be an object']
321
+ };
322
+ }
323
+
324
+ const severityValues = ['CRITICAL', 'WARNING', 'INFO'];
325
+ if (policy.failOnSeverity && !severityValues.includes(policy.failOnSeverity)) {
326
+ errors.push(`failOnSeverity must be one of: ${severityValues.join(', ')}`);
327
+ }
328
+
329
+ if (typeof policy.maxWarnings !== 'number' || policy.maxWarnings < 0) {
330
+ errors.push('maxWarnings must be a non-negative number');
331
+ }
332
+
333
+ if (typeof policy.maxInfo !== 'number' || policy.maxInfo < 0) {
334
+ errors.push('maxInfo must be a non-negative number');
335
+ }
336
+
337
+ if (typeof policy.maxTotalRisk !== 'number' || policy.maxTotalRisk < 0) {
338
+ errors.push('maxTotalRisk must be a non-negative number');
339
+ }
340
+
341
+ if (typeof policy.failOnNewRegression !== 'boolean') {
342
+ errors.push('failOnNewRegression must be a boolean');
343
+ }
344
+
345
+ return {
346
+ valid: errors.length === 0,
347
+ errors
348
+ };
349
+ }
350
+
351
+ module.exports = {
352
+ loadPolicy,
353
+ evaluatePolicy,
354
+ formatPolicyOutput,
355
+ createDefaultPolicyFile,
356
+ validatePolicy
357
+ };
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Preset Policy Loader
3
+ *
4
+ * Load preset policies (startup, saas, enterprise) for easy adoption.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ /**
11
+ * Load preset policy by name
12
+ * @param {string} presetName - 'startup', 'saas', or 'enterprise'
13
+ * @returns {object|null} Policy object or null if not found
14
+ */
15
+ function loadPreset(presetName) {
16
+ const validPresets = ['startup', 'saas', 'enterprise'];
17
+
18
+ if (!presetName || !validPresets.includes(presetName.toLowerCase())) {
19
+ console.warn(`⚠️ Invalid preset: ${presetName}. Valid presets: ${validPresets.join(', ')}`);
20
+ return null;
21
+ }
22
+
23
+ const presetPath = path.join(__dirname, '../../policies', `${presetName.toLowerCase()}.json`);
24
+
25
+ if (!fs.existsSync(presetPath)) {
26
+ console.error(`⚠️ Preset file not found: ${presetPath}`);
27
+ return null;
28
+ }
29
+
30
+ try {
31
+ const content = fs.readFileSync(presetPath, 'utf-8');
32
+ const policy = JSON.parse(content);
33
+
34
+ console.log(`✅ Loaded preset: ${policy.name || presetName}`);
35
+ if (policy.description) {
36
+ console.log(` ${policy.description}`);
37
+ }
38
+
39
+ return policy;
40
+ } catch (error) {
41
+ console.error(`⚠️ Failed to load preset ${presetName}: ${error.message}`);
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Parse policy option (file path or preset:name)
48
+ * @param {string} policyOption - File path or 'preset:name'
49
+ * @returns {object|null} Policy object or null
50
+ */
51
+ function parsePolicyOption(policyOption) {
52
+ // Guard null/undefined
53
+ if (policyOption == null) {
54
+ return null;
55
+ }
56
+
57
+ // If a policy object was passed directly, keep legacy behavior (no implicit acceptance)
58
+ // to avoid changing defaults. Only handle string-like inputs safely.
59
+ const optionStr = typeof policyOption === 'string'
60
+ ? policyOption.trim()
61
+ : String(policyOption).trim();
62
+
63
+ if (optionStr.length === 0) {
64
+ return null;
65
+ }
66
+
67
+ // Check if it's a preset
68
+ if (optionStr.startsWith('preset:')) {
69
+ const presetName = optionStr.substring(7); // Remove 'preset:' prefix
70
+ return loadPreset(presetName);
71
+ }
72
+
73
+ // Otherwise, treat as file path
74
+ if (fs.existsSync(optionStr)) {
75
+ try {
76
+ const content = fs.readFileSync(optionStr, 'utf-8');
77
+ const policy = JSON.parse(content);
78
+ console.log(`✅ Loaded policy from: ${optionStr}`);
79
+ return policy;
80
+ } catch (error) {
81
+ console.error(`⚠️ Failed to load policy from ${optionStr}: ${error.message}`);
82
+ return null;
83
+ }
84
+ }
85
+
86
+ console.warn(`⚠️ Policy file not found: ${optionStr}`);
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * List available presets
92
+ * @returns {array} Array of preset info objects
93
+ */
94
+ function listPresets() {
95
+ const presetsDir = path.join(__dirname, '../../policies');
96
+
97
+ if (!fs.existsSync(presetsDir)) {
98
+ return [];
99
+ }
100
+
101
+ const presets = [];
102
+ const files = fs.readdirSync(presetsDir);
103
+
104
+ files.forEach(file => {
105
+ if (file.endsWith('.json')) {
106
+ try {
107
+ const content = fs.readFileSync(path.join(presetsDir, file), 'utf-8');
108
+ const policy = JSON.parse(content);
109
+ presets.push({
110
+ name: file.replace('.json', ''),
111
+ displayName: policy.name || file,
112
+ description: policy.description || 'No description',
113
+ policy
114
+ });
115
+ } catch (error) {
116
+ // Skip invalid files
117
+ }
118
+ }
119
+ });
120
+
121
+ return presets;
122
+ }
123
+
124
+ /**
125
+ * Print preset list to console
126
+ */
127
+ function printPresets() {
128
+ const presets = listPresets();
129
+
130
+ if (presets.length === 0) {
131
+ console.log('No presets found.');
132
+ return;
133
+ }
134
+
135
+ console.log('\n📋 Available Policy Presets:\n');
136
+ presets.forEach(preset => {
137
+ console.log(` • ${preset.name}`);
138
+ console.log(` ${preset.description}`);
139
+ console.log(` Usage: --policy preset:${preset.name}\n`);
140
+ });
141
+ }
142
+
143
+ module.exports = {
144
+ loadPreset,
145
+ parsePolicyOption,
146
+ listPresets,
147
+ printPresets
148
+ };