@odavl/guardian 2.0.0 → 2.0.1

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 (172) hide show
  1. package/CHANGELOG.md +210 -210
  2. package/LICENSE +21 -21
  3. package/README.md +297 -184
  4. package/bin/guardian.js +2242 -2221
  5. package/config/README.md +59 -59
  6. package/config/guardian.config.json +54 -54
  7. package/config/guardian.policy.json +12 -12
  8. package/config/profiles/docs.yaml +18 -18
  9. package/config/profiles/ecommerce.yaml +17 -17
  10. package/config/profiles/landing-demo.yaml +16 -16
  11. package/config/profiles/marketing.yaml +18 -18
  12. package/config/profiles/saas.yaml +21 -21
  13. package/flows/example-login-flow.json +36 -36
  14. package/flows/example-signup-flow.json +44 -44
  15. package/package.json +124 -116
  16. package/policies/enterprise.json +12 -12
  17. package/policies/landing-demo.json +22 -22
  18. package/policies/saas.json +12 -12
  19. package/policies/startup.json +12 -12
  20. package/src/enterprise/audit-logger.js +166 -166
  21. package/src/enterprise/pdf-exporter.js +267 -267
  22. package/src/enterprise/rbac-gate.js +142 -142
  23. package/src/enterprise/rbac.js +239 -239
  24. package/src/enterprise/site-manager.js +180 -180
  25. package/src/founder/feedback-system.js +156 -156
  26. package/src/founder/founder-tracker.js +213 -213
  27. package/src/founder/usage-signals.js +141 -141
  28. package/src/guardian/action-hints.js +439 -439
  29. package/src/guardian/alert-ledger.js +121 -121
  30. package/src/guardian/artifact-sanitizer.js +56 -56
  31. package/src/guardian/attempt-engine.js +1069 -1029
  32. package/src/guardian/attempt-registry.js +267 -267
  33. package/src/guardian/attempt-relevance.js +106 -106
  34. package/src/guardian/attempt-reporter.js +513 -507
  35. package/src/guardian/attempt.js +274 -273
  36. package/src/guardian/attempts-filter.js +63 -63
  37. package/src/guardian/auto-attempt-builder.js +283 -283
  38. package/src/guardian/baseline-registry.js +177 -177
  39. package/src/guardian/baseline-reporter.js +143 -143
  40. package/src/guardian/baseline-storage.js +285 -285
  41. package/src/guardian/baseline.js +535 -534
  42. package/src/guardian/behavioral-signals.js +261 -261
  43. package/src/guardian/breakage-intelligence.js +224 -224
  44. package/src/guardian/browser-pool.js +131 -131
  45. package/src/guardian/browser.js +119 -119
  46. package/src/guardian/canonical-truth.js +308 -308
  47. package/src/guardian/ci-cli.js +121 -121
  48. package/src/guardian/ci-gate.js +96 -96
  49. package/src/guardian/ci-mode.js +15 -15
  50. package/src/guardian/ci-output.js +55 -38
  51. package/src/guardian/cli-summary.js +102 -102
  52. package/src/guardian/confidence-signals.js +251 -251
  53. package/src/guardian/config-loader.js +161 -161
  54. package/src/guardian/config-validator.js +285 -283
  55. package/src/guardian/coverage-model.js +239 -239
  56. package/src/guardian/coverage-packs.js +58 -58
  57. package/src/guardian/crawler.js +142 -142
  58. package/src/guardian/data-guardian-detector.js +189 -189
  59. package/src/guardian/decision-authority.js +746 -725
  60. package/src/guardian/detection-layers.js +271 -271
  61. package/src/guardian/determinism.js +146 -146
  62. package/src/guardian/discovery-engine.js +661 -661
  63. package/src/guardian/drift-detector.js +100 -100
  64. package/src/guardian/enhanced-html-reporter.js +522 -522
  65. package/src/guardian/env-guard.js +128 -127
  66. package/src/guardian/error-clarity.js +399 -399
  67. package/src/guardian/export-contract.js +196 -196
  68. package/src/guardian/fail-safe.js +212 -212
  69. package/src/guardian/failure-intelligence.js +173 -173
  70. package/src/guardian/failure-taxonomy.js +169 -169
  71. package/src/guardian/final-outcome.js +206 -206
  72. package/src/guardian/first-run-profile.js +89 -89
  73. package/src/guardian/first-run.js +65 -67
  74. package/src/guardian/flag-validator.js +111 -111
  75. package/src/guardian/flow-executor.js +641 -639
  76. package/src/guardian/flow-registry.js +67 -67
  77. package/src/guardian/honesty.js +394 -394
  78. package/src/guardian/html-reporter.js +416 -416
  79. package/src/guardian/human-intent-resolver.js +296 -296
  80. package/src/guardian/human-interaction-model.js +351 -351
  81. package/src/guardian/human-journey-context.js +184 -184
  82. package/src/guardian/human-navigator.js +544 -544
  83. package/src/guardian/human-reporter.js +435 -431
  84. package/src/guardian/index.js +226 -221
  85. package/src/guardian/init-command.js +143 -143
  86. package/src/guardian/intent-detector.js +148 -146
  87. package/src/guardian/journey-definitions.js +132 -132
  88. package/src/guardian/journey-scan-cli.js +142 -145
  89. package/src/guardian/journey-scanner.js +583 -583
  90. package/src/guardian/junit-reporter.js +281 -281
  91. package/src/guardian/language-detection.js +99 -99
  92. package/src/guardian/live-alert.js +56 -56
  93. package/src/guardian/live-baseline-compare.js +146 -146
  94. package/src/guardian/live-cli.js +95 -95
  95. package/src/guardian/live-guardian.js +210 -210
  96. package/src/guardian/live-scheduler-runner.js +137 -137
  97. package/src/guardian/live-scheduler-state.js +167 -168
  98. package/src/guardian/live-scheduler.js +146 -146
  99. package/src/guardian/live-state.js +110 -110
  100. package/src/guardian/market-criticality.js +335 -335
  101. package/src/guardian/market-reporter.js +577 -577
  102. package/src/guardian/network-trace.js +178 -178
  103. package/src/guardian/obs-logger.js +110 -110
  104. package/src/guardian/observed-capabilities.js +427 -427
  105. package/src/guardian/output-contract.js +154 -0
  106. package/src/guardian/output-readability.js +264 -264
  107. package/src/guardian/parallel-executor.js +116 -116
  108. package/src/guardian/path-safety.js +56 -56
  109. package/src/guardian/pattern-analyzer.js +348 -348
  110. package/src/guardian/policy.js +432 -434
  111. package/src/guardian/prelaunch-gate.js +193 -193
  112. package/src/guardian/prerequisite-checker.js +101 -101
  113. package/src/guardian/preset-loader.js +152 -157
  114. package/src/guardian/profile-loader.js +96 -96
  115. package/src/guardian/reality.js +3025 -2826
  116. package/src/guardian/realworld-scenarios.js +94 -94
  117. package/src/guardian/reporter.js +167 -167
  118. package/src/guardian/retry-policy.js +123 -123
  119. package/src/guardian/root-cause-analysis.js +171 -171
  120. package/src/guardian/rules-engine.js +558 -558
  121. package/src/guardian/run-artifacts.js +212 -212
  122. package/src/guardian/run-cleanup.js +207 -207
  123. package/src/guardian/run-export.js +522 -522
  124. package/src/guardian/run-latest.js +90 -90
  125. package/src/guardian/run-list.js +211 -211
  126. package/src/guardian/run-summary.js +20 -20
  127. package/src/guardian/runtime-root.js +246 -246
  128. package/src/guardian/safety.js +248 -248
  129. package/src/guardian/scan-presets.js +133 -149
  130. package/src/guardian/screenshot.js +152 -152
  131. package/src/guardian/secret-hygiene.js +44 -44
  132. package/src/guardian/selector-fallbacks.js +394 -394
  133. package/src/guardian/semantic-contact-detection.js +255 -255
  134. package/src/guardian/semantic-contact-finder.js +201 -201
  135. package/src/guardian/semantic-targets.js +234 -234
  136. package/src/guardian/site-intelligence.js +588 -588
  137. package/src/guardian/site-introspection.js +257 -257
  138. package/src/guardian/sitemap.js +225 -225
  139. package/src/guardian/smoke.js +283 -258
  140. package/src/guardian/snapshot-schema.js +177 -290
  141. package/src/guardian/snapshot.js +430 -397
  142. package/src/guardian/stability-scorer.js +169 -169
  143. package/src/guardian/success-evaluator.js +214 -214
  144. package/src/guardian/template-command.js +184 -184
  145. package/src/guardian/text-formatters.js +426 -426
  146. package/src/guardian/timeout-profiles.js +57 -57
  147. package/src/guardian/truth/attempt.contract.js +158 -0
  148. package/src/guardian/truth/decision.contract.js +275 -0
  149. package/src/guardian/truth/snapshot.contract.js +363 -0
  150. package/src/guardian/validators.js +323 -323
  151. package/src/guardian/verdict-card.js +474 -474
  152. package/src/guardian/verdict-clarity.js +298 -298
  153. package/src/guardian/verdict-policy.js +363 -363
  154. package/src/guardian/verdict.js +333 -333
  155. package/src/guardian/verdicts.js +79 -74
  156. package/src/guardian/visual-diff.js +247 -247
  157. package/src/guardian/wait-for-outcome.js +119 -119
  158. package/src/guardian/watch-runner.js +181 -181
  159. package/src/guardian/watchdog-diff.js +167 -167
  160. package/src/guardian/webhook.js +206 -206
  161. package/src/payments/stripe-checkout.js +169 -169
  162. package/src/plans/plan-definitions.js +148 -148
  163. package/src/plans/plan-manager.js +211 -211
  164. package/src/plans/usage-tracker.js +210 -210
  165. package/src/recipes/recipe-engine.js +188 -188
  166. package/src/recipes/recipe-failure-analysis.js +159 -159
  167. package/src/recipes/recipe-registry.js +134 -134
  168. package/src/recipes/recipe-runtime.js +507 -507
  169. package/src/recipes/recipe-store.js +410 -410
  170. package/SECURITY.md +0 -77
  171. package/VERSIONING.md +0 -100
  172. package/guardian-contract-v1.md +0 -502
@@ -1,434 +1,432 @@
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
- // Coverage and evidence expectations
56
- coverage: {
57
- failOnGap: true,
58
- warnOnGap: false
59
- },
60
- evidence: {
61
- minCompleteness: 1.0,
62
- minIntegrity: 0.9,
63
- requireScreenshots: false,
64
- requireTraces: false
65
- }
66
- };
67
-
68
- if (!policyPath) {
69
- // Try to find guardian.policy.json in current directory, config/, or .odavl-guardian/
70
- const candidates = [
71
- 'config/guardian.policy.json',
72
- 'guardian.policy.json',
73
- '.odavl-guardian/policy.json',
74
- '.odavl-guardian/guardian.policy.json'
75
- ];
76
-
77
- for (const candidate of candidates) {
78
- if (fs.existsSync(candidate)) {
79
- policyPath = candidate;
80
- break;
81
- }
82
- }
83
- }
84
-
85
- // If no policy file found, use defaults
86
- if (!policyPath || !fs.existsSync(policyPath)) {
87
- return defaultPolicy;
88
- }
89
-
90
- try {
91
- const json = fs.readFileSync(policyPath, 'utf8');
92
- const loaded = JSON.parse(json);
93
- return { ...defaultPolicy, ...loaded };
94
- } catch (e) {
95
- console.warn(`⚠️ Failed to load policy from ${policyPath}: ${e.message}`);
96
- console.warn(' Using default policy');
97
- return defaultPolicy;
98
- }
99
- }
100
-
101
- /**
102
- * Evaluate snapshot against policy
103
- * Returns { passed: boolean, exitCode: 0|1|2, reasons: string[], summary: string }
104
- */
105
- function evaluatePolicy(snapshot, policy, signals = {}) {
106
- const effectivePolicy = policy || loadPolicy();
107
- const reasons = [];
108
- let exitCode = 0;
109
-
110
- // Check for INSUFFICIENT_EVIDENCE verdict - always exit 2 (WARN)
111
- const verdict = snapshot.verdict || {};
112
- if (verdict.verdict === 'INSUFFICIENT_EVIDENCE') {
113
- reasons.push('No meaningful attempts executed; element discovery failed on uninstrumented site or all journeys not applicable');
114
- exitCode = 2; // WARN - cannot make a confident decision
115
- return {
116
- passed: false,
117
- exitCode,
118
- reasons,
119
- summary: '⚠️ Policy evaluation INSUFFICIENT_EVIDENCE (exit code 2)',
120
- counts: {
121
- critical: 0,
122
- warning: 0,
123
- info: 0,
124
- softFailures: 0,
125
- totalRisk: 0
126
- }
127
- };
128
- }
129
-
130
- // Extract market impact summary (Phase 3)
131
- const marketImpact = snapshot.marketImpactSummary || {};
132
- const criticalCount = marketImpact.countsBySeverity?.CRITICAL || 0;
133
- const warningCount = marketImpact.countsBySeverity?.WARNING || 0;
134
- const infoCount = marketImpact.countsBySeverity?.INFO || 0;
135
- const totalRisk = marketImpact.totalRiskCount || 0;
136
-
137
- // Coverage and evidence signals
138
- const coverage = signals.coverage || { gaps: 0, total: 0, executed: 0 };
139
- const evidenceMetrics = signals.evidence?.metrics || { completeness: 0, integrity: 0 };
140
- const missingScreenshots = signals.evidence?.missingScreenshots || false;
141
- const missingTraces = signals.evidence?.missingTraces || false;
142
- const runtimeSignals = Array.isArray(signals.runtimeSignals) ? signals.runtimeSignals : [];
143
-
144
- // Extract soft failures (Phase 2)
145
- const softFailureCount = snapshot.attempts?.reduce((sum, attempt) => {
146
- return sum + (attempt.softFailureCount || 0);
147
- }, 0) || 0;
148
-
149
- // Phase 4: Check domain gates if intelligence available
150
- if (!exitCode && effectivePolicy.domainGates && snapshot.intelligence) {
151
- const intelligence = snapshot.intelligence;
152
- const domainFailures = intelligence.byDomain || {};
153
-
154
- for (const [domain, gates] of Object.entries(effectivePolicy.domainGates)) {
155
- const domainFailure = domainFailures[domain] || { failures: [] };
156
-
157
- // Check CRITICAL gate
158
- if (gates.CRITICAL !== undefined) {
159
- const criticalInDomain = domainFailure.failures?.filter(f => f.severity === 'CRITICAL').length || 0;
160
- if (criticalInDomain > gates.CRITICAL) {
161
- reasons.push(`Domain ${domain}: ${criticalInDomain} CRITICAL failure(s) exceed gate limit of ${gates.CRITICAL}`);
162
- exitCode = 1;
163
- }
164
- }
165
-
166
- // Check WARNING gate
167
- if (!exitCode && gates.WARNING !== undefined) {
168
- const warningInDomain = domainFailure.failures?.filter(f => f.severity === 'WARNING').length || 0;
169
- if (warningInDomain > gates.WARNING) {
170
- reasons.push(`Domain ${domain}: ${warningInDomain} WARNING failure(s) exceed gate limit of ${gates.WARNING}`);
171
- exitCode = 2;
172
- }
173
- }
174
- }
175
- }
176
-
177
- // Phase 5: Check visual regression gates if configured
178
- if (!exitCode && effectivePolicy.visualGates && snapshot.intelligence) {
179
- const intelligence = snapshot.intelligence;
180
- const visualFailures = intelligence.failures?.filter(f => f.breakType === 'VISUAL') || [];
181
- const visualCritical = visualFailures.filter(f => f.severity === 'CRITICAL').length || 0;
182
- const visualWarning = visualFailures.filter(f => f.severity === 'WARNING').length || 0;
183
- const maxDiffPercent = Math.max(...visualFailures.map(f => f.visualDiff?.percentChange || 0));
184
-
185
- // Check CRITICAL visual diffs
186
- if (effectivePolicy.visualGates.CRITICAL !== undefined) {
187
- if (visualCritical > effectivePolicy.visualGates.CRITICAL) {
188
- reasons.push(`Visual regression: ${visualCritical} CRITICAL diff(s) exceed gate limit of ${effectivePolicy.visualGates.CRITICAL}`);
189
- exitCode = 1;
190
- }
191
- }
192
-
193
- // Check WARNING visual diffs
194
- if (!exitCode && effectivePolicy.visualGates.WARNING !== undefined) {
195
- if (visualWarning > effectivePolicy.visualGates.WARNING) {
196
- reasons.push(`Visual regression: ${visualWarning} WARNING diff(s) exceed gate limit of ${effectivePolicy.visualGates.WARNING}`);
197
- exitCode = 2;
198
- }
199
- }
200
-
201
- // Check max diff percent
202
- if (!exitCode && effectivePolicy.visualGates.maxDiffPercent !== undefined) {
203
- if (maxDiffPercent > effectivePolicy.visualGates.maxDiffPercent) {
204
- reasons.push(`Visual regression: ${maxDiffPercent.toFixed(1)}% diff exceeds max threshold of ${effectivePolicy.visualGates.maxDiffPercent}%`);
205
- exitCode = 1;
206
- }
207
- }
208
- }
209
-
210
- // Evaluate CRITICAL severity (always exit 1 if present)
211
- if (effectivePolicy.failOnSeverity === 'CRITICAL' && criticalCount > 0) {
212
- reasons.push(`${criticalCount} CRITICAL risk(s) detected (policy: failOnSeverity=CRITICAL)`);
213
- exitCode = 1;
214
- }
215
-
216
- // Evaluate WARNING severity
217
- if (effectivePolicy.failOnSeverity === 'WARNING' && warningCount > 0) {
218
- reasons.push(`${warningCount} WARNING risk(s) detected (policy: failOnSeverity=WARNING)`);
219
- exitCode = 1;
220
- }
221
-
222
- // Evaluate max warnings
223
- if (!exitCode && warningCount > effectivePolicy.maxWarnings) {
224
- reasons.push(`${warningCount} WARNING(s) exceed limit of ${effectivePolicy.maxWarnings}`);
225
- exitCode = 2;
226
- }
227
-
228
- // Evaluate max info
229
- if (!exitCode && infoCount > effectivePolicy.maxInfo) {
230
- reasons.push(`${infoCount} INFO(s) exceed limit of ${effectivePolicy.maxInfo}`);
231
- exitCode = 2;
232
- }
233
-
234
- // Evaluate total risk
235
- if (!exitCode && totalRisk > effectivePolicy.maxTotalRisk) {
236
- reasons.push(`${totalRisk} total risk(s) exceed limit of ${effectivePolicy.maxTotalRisk}`);
237
- exitCode = 1;
238
- }
239
-
240
- // Evaluate baseline regression
241
- if (!exitCode && effectivePolicy.failOnNewRegression) {
242
- const baseline = snapshot.baseline || {};
243
- const diff = baseline.diff || {};
244
-
245
- if (diff.regressions && Object.keys(diff.regressions).length > 0) {
246
- const regCount = Object.keys(diff.regressions).length;
247
- reasons.push(`${regCount} baseline regression(s) detected (policy: failOnNewRegression=true)`);
248
- exitCode = 1;
249
- }
250
- }
251
-
252
- // Evaluate soft failures
253
- if (!exitCode && effectivePolicy.failOnSoftFailures && softFailureCount > 0) {
254
- reasons.push(`${softFailureCount} soft failure(s) detected (policy: failOnSoftFailures=true)`);
255
- exitCode = 1;
256
- }
257
-
258
- // Evaluate soft failure threshold
259
- if (!exitCode && softFailureCount > effectivePolicy.softFailureThreshold) {
260
- reasons.push(`${softFailureCount} soft failure(s) exceed threshold of ${effectivePolicy.softFailureThreshold}`);
261
- exitCode = 2;
262
- }
263
-
264
- // Coverage gaps (attempts skipped/not applicable)
265
- if (!exitCode && effectivePolicy.coverage) {
266
- if (effectivePolicy.coverage.failOnGap && coverage.gaps > 0) {
267
- reasons.push(`Coverage gaps detected: ${coverage.gaps} of ${coverage.total || 'n/a'} attempts not executed`);
268
- exitCode = 1;
269
- } else if (effectivePolicy.coverage.warnOnGap && coverage.gaps > 0) {
270
- reasons.push(`Coverage gaps detected: ${coverage.gaps} of ${coverage.total || 'n/a'} attempts not executed`);
271
- exitCode = 2;
272
- }
273
- }
274
-
275
- // Evidence completeness/integrity
276
- if (!exitCode && effectivePolicy.evidence) {
277
- if (evidenceMetrics.completeness < (effectivePolicy.evidence.minCompleteness ?? 1)) {
278
- reasons.push(`Evidence completeness ${evidenceMetrics.completeness.toFixed(2)} below policy minimum ${(effectivePolicy.evidence.minCompleteness ?? 1)}`);
279
- exitCode = exitCode || (effectivePolicy.evidence.minCompleteness >= 1 ? 1 : 2);
280
- }
281
- if (evidenceMetrics.integrity < (effectivePolicy.evidence.minIntegrity ?? 0)) {
282
- reasons.push(`Evidence integrity ${evidenceMetrics.integrity.toFixed(2)} below policy minimum ${(effectivePolicy.evidence.minIntegrity ?? 0)}`);
283
- exitCode = exitCode || 2;
284
- }
285
- if (effectivePolicy.evidence.requireScreenshots && missingScreenshots) {
286
- reasons.push('Screenshots disabled but required by policy');
287
- exitCode = 1;
288
- }
289
- if (effectivePolicy.evidence.requireTraces && missingTraces) {
290
- reasons.push('Network traces disabled but required by policy');
291
- exitCode = 1;
292
- }
293
- }
294
-
295
- // Runtime signals (crawl/discovery/system)
296
- if (!exitCode && runtimeSignals.length > 0) {
297
- const desc = runtimeSignals.map(s => s.description).slice(0, 3).join('; ');
298
- reasons.push(`Runtime issues detected: ${desc}`);
299
- exitCode = 2;
300
- }
301
-
302
- // Evaluate baseline requirement
303
- if (!exitCode && effectivePolicy.requireBaseline) {
304
- const baseline = snapshot.baseline || {};
305
- if (!baseline.baselineFound && !baseline.baselineCreatedThisRun) {
306
- reasons.push('Baseline required but not found (policy: requireBaseline=true)');
307
- exitCode = 1;
308
- }
309
- }
310
-
311
- // Build summary
312
- const summary =
313
- exitCode === 0
314
- ? ' Policy evaluation PASSED'
315
- : exitCode === 1
316
- ? '❌ Policy evaluation FAILED (exit code 1)'
317
- : '⚠️ Policy evaluation WARNING (exit code 2)';
318
-
319
- return {
320
- passed: exitCode === 0,
321
- exitCode,
322
- reasons,
323
- summary,
324
- counts: {
325
- critical: criticalCount,
326
- warning: warningCount,
327
- info: infoCount,
328
- softFailures: softFailureCount,
329
- totalRisk
330
- }
331
- };
332
- }
333
-
334
- /**
335
- * Format policy evaluation results for CLI output
336
- */
337
- function formatPolicyOutput(evaluation) {
338
- let output = '\n' + '━'.repeat(60) + '\n';
339
- output += '🛡️ Policy Evaluation\n';
340
- output += '━'.repeat(60) + '\n\n';
341
-
342
- output += `${evaluation.summary}\n`;
343
-
344
- if (evaluation.reasons.length > 0) {
345
- output += '\nFailure reasons:\n';
346
- evaluation.reasons.forEach(r => {
347
- output += ` ❌ ${r}\n`;
348
- });
349
- }
350
-
351
- output += `\nRisk counts:\n`;
352
- output += ` 🔴 CRITICAL: ${evaluation.counts.critical}\n`;
353
- output += ` 🟡 WARNING: ${evaluation.counts.warning}\n`;
354
- output += ` 🔵 INFO: ${evaluation.counts.info}\n`;
355
- output += ` 🐛 Soft Failures: ${evaluation.counts.softFailures}\n`;
356
- output += ` 📊 Total Risks: ${evaluation.counts.totalRisk}\n`;
357
-
358
- output += `\nExit Code: ${evaluation.exitCode}\n`;
359
- output += '━'.repeat(60) + '\n';
360
-
361
- return output;
362
- }
363
-
364
- /**
365
- * Create a default policy file
366
- */
367
- function createDefaultPolicyFile(outputPath = 'config/guardian.policy.json') {
368
- const defaultPolicy = {
369
- failOnSeverity: 'CRITICAL',
370
- maxWarnings: 0,
371
- maxInfo: 999,
372
- maxTotalRisk: 999,
373
- failOnNewRegression: true,
374
- failOnSoftFailures: false,
375
- softFailureThreshold: 5,
376
- requireBaseline: false
377
- };
378
-
379
- fs.writeFileSync(
380
- outputPath,
381
- JSON.stringify(defaultPolicy, null, 2),
382
- 'utf8'
383
- );
384
-
385
- return outputPath;
386
- }
387
-
388
- /**
389
- * Validate policy object structure
390
- */
391
- function validatePolicy(policy) {
392
- const errors = [];
393
-
394
- if (!policy || typeof policy !== 'object') {
395
- return {
396
- valid: false,
397
- errors: ['Policy must be an object']
398
- };
399
- }
400
-
401
- const severityValues = ['CRITICAL', 'WARNING', 'INFO'];
402
- if (policy.failOnSeverity && !severityValues.includes(policy.failOnSeverity)) {
403
- errors.push(`failOnSeverity must be one of: ${severityValues.join(', ')}`);
404
- }
405
-
406
- if (typeof policy.maxWarnings !== 'number' || policy.maxWarnings < 0) {
407
- errors.push('maxWarnings must be a non-negative number');
408
- }
409
-
410
- if (typeof policy.maxInfo !== 'number' || policy.maxInfo < 0) {
411
- errors.push('maxInfo must be a non-negative number');
412
- }
413
-
414
- if (typeof policy.maxTotalRisk !== 'number' || policy.maxTotalRisk < 0) {
415
- errors.push('maxTotalRisk must be a non-negative number');
416
- }
417
-
418
- if (typeof policy.failOnNewRegression !== 'boolean') {
419
- errors.push('failOnNewRegression must be a boolean');
420
- }
421
-
422
- return {
423
- valid: errors.length === 0,
424
- errors
425
- };
426
- }
427
-
428
- module.exports = {
429
- loadPolicy,
430
- evaluatePolicy,
431
- formatPolicyOutput,
432
- createDefaultPolicyFile,
433
- validatePolicy
434
- };
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
+
15
+ /**
16
+ * @typedef {Object} GuardianPolicy
17
+ * @property {string} [failOnSeverity='CRITICAL'] - Severity level that triggers exit 1 (CRITICAL|WARNING|INFO)
18
+ * @property {number} [maxWarnings=0] - Max WARNING count before fail
19
+ * @property {number} [maxInfo=999] - Max INFO count before fail
20
+ * @property {number} [maxTotalRisk=999] - Max total risks before fail
21
+ * @property {boolean} [failOnNewRegression=true] - Fail if baseline regression detected
22
+ * @property {boolean} [failOnSoftFailures=false] - Fail if any soft failures detected
23
+ * @property {number} [softFailureThreshold=5] - Max soft failures before fail
24
+ * @property {boolean} [requireBaseline=false] - Require baseline to exist
25
+ * @property {Object} [domainGates] - Domain-aware gates (Phase 4). Ex: { REVENUE: { CRITICAL: 0, WARNING: 3 }, TRUST: { CRITICAL: 0 } }
26
+ * @property {Object} [visualGates] - Phase 5: Visual regression gates. Ex: { CRITICAL: 0, WARNING: 999, maxDiffPercent: 25 }
27
+ */
28
+
29
+ /**
30
+ * Load policy from file or return defaults
31
+ */
32
+ function loadPolicy(policyPath = null) {
33
+ const defaultPolicy = {
34
+ failOnSeverity: 'CRITICAL',
35
+ maxWarnings: 0,
36
+ maxInfo: 999,
37
+ maxTotalRisk: 999,
38
+ failOnNewRegression: true,
39
+ failOnSoftFailures: false,
40
+ softFailureThreshold: 5,
41
+ requireBaseline: false,
42
+ domainGates: {
43
+ // Phase 4: Fail on any CRITICAL in REVENUE or TRUST domains
44
+ REVENUE: { CRITICAL: 0, WARNING: 999 },
45
+ TRUST: { CRITICAL: 0, WARNING: 999 }
46
+ },
47
+ // Phase 5: Visual regression gates
48
+ visualGates: {
49
+ CRITICAL: 0, // Fail if any CRITICAL visual diffs
50
+ WARNING: 999, // Warn if more than 999 WARNING visual diffs
51
+ maxDiffPercent: 25 // Fail if visual change > 25% of page
52
+ },
53
+ // Coverage and evidence expectations
54
+ coverage: {
55
+ failOnGap: true,
56
+ warnOnGap: false
57
+ },
58
+ evidence: {
59
+ minCompleteness: 1.0,
60
+ minIntegrity: 0.9,
61
+ requireScreenshots: false,
62
+ requireTraces: false
63
+ }
64
+ };
65
+
66
+ if (!policyPath) {
67
+ // Try to find guardian.policy.json in current directory, config/, or .odavl-guardian/
68
+ const candidates = [
69
+ 'config/guardian.policy.json',
70
+ 'guardian.policy.json',
71
+ '.odavl-guardian/policy.json',
72
+ '.odavl-guardian/guardian.policy.json'
73
+ ];
74
+
75
+ for (const candidate of candidates) {
76
+ if (fs.existsSync(candidate)) {
77
+ policyPath = candidate;
78
+ break;
79
+ }
80
+ }
81
+ }
82
+
83
+ // If no policy file found, use defaults
84
+ if (!policyPath || !fs.existsSync(policyPath)) {
85
+ return defaultPolicy;
86
+ }
87
+
88
+ try {
89
+ const json = fs.readFileSync(policyPath, 'utf8');
90
+ const loaded = JSON.parse(json);
91
+ return { ...defaultPolicy, ...loaded };
92
+ } catch (e) {
93
+ console.warn(`⚠️ Failed to load policy from ${policyPath}: ${e.message}`);
94
+ console.warn(' Using default policy');
95
+ return defaultPolicy;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Evaluate snapshot against policy
101
+ * Returns { passed: boolean, exitCode: 0|1|2, reasons: string[], summary: string }
102
+ */
103
+ function evaluatePolicy(snapshot, policy, signals = {}) {
104
+ const effectivePolicy = policy || loadPolicy();
105
+ const reasons = [];
106
+ let exitCode = 0;
107
+
108
+ // Check for INSUFFICIENT_EVIDENCE verdict - always exit 2 (WARN)
109
+ const verdict = snapshot.verdict || {};
110
+ if (verdict.verdict === 'INSUFFICIENT_EVIDENCE') {
111
+ reasons.push('No meaningful attempts executed; element discovery failed on uninstrumented site or all journeys not applicable');
112
+ exitCode = 2; // WARN - cannot make a confident decision
113
+ return {
114
+ passed: false,
115
+ exitCode,
116
+ reasons,
117
+ summary: '⚠️ Policy evaluation INSUFFICIENT_EVIDENCE (exit code 2)',
118
+ counts: {
119
+ critical: 0,
120
+ warning: 0,
121
+ info: 0,
122
+ softFailures: 0,
123
+ totalRisk: 0
124
+ }
125
+ };
126
+ }
127
+
128
+ // Extract market impact summary (Phase 3)
129
+ const marketImpact = snapshot.marketImpactSummary || {};
130
+ const criticalCount = marketImpact.countsBySeverity?.CRITICAL || 0;
131
+ const warningCount = marketImpact.countsBySeverity?.WARNING || 0;
132
+ const infoCount = marketImpact.countsBySeverity?.INFO || 0;
133
+ const totalRisk = marketImpact.totalRiskCount || 0;
134
+
135
+ // Coverage and evidence signals
136
+ const coverage = signals.coverage || { gaps: 0, total: 0, executed: 0 };
137
+ const evidenceMetrics = signals.evidence?.metrics || { completeness: 0, integrity: 0 };
138
+ const missingScreenshots = signals.evidence?.missingScreenshots || false;
139
+ const missingTraces = signals.evidence?.missingTraces || false;
140
+ const runtimeSignals = Array.isArray(signals.runtimeSignals) ? signals.runtimeSignals : [];
141
+
142
+ // Extract soft failures (Phase 2)
143
+ const softFailureCount = snapshot.attempts?.reduce((sum, attempt) => {
144
+ return sum + (attempt.softFailureCount || 0);
145
+ }, 0) || 0;
146
+
147
+ // Phase 4: Check domain gates if intelligence available
148
+ if (!exitCode && effectivePolicy.domainGates && snapshot.intelligence) {
149
+ const intelligence = snapshot.intelligence;
150
+ const domainFailures = intelligence.byDomain || {};
151
+
152
+ for (const [domain, gates] of Object.entries(effectivePolicy.domainGates)) {
153
+ const domainFailure = domainFailures[domain] || { failures: [] };
154
+
155
+ // Check CRITICAL gate
156
+ if (gates.CRITICAL !== undefined) {
157
+ const criticalInDomain = domainFailure.failures?.filter(f => f.severity === 'CRITICAL').length || 0;
158
+ if (criticalInDomain > gates.CRITICAL) {
159
+ reasons.push(`Domain ${domain}: ${criticalInDomain} CRITICAL failure(s) exceed gate limit of ${gates.CRITICAL}`);
160
+ exitCode = 1;
161
+ }
162
+ }
163
+
164
+ // Check WARNING gate
165
+ if (!exitCode && gates.WARNING !== undefined) {
166
+ const warningInDomain = domainFailure.failures?.filter(f => f.severity === 'WARNING').length || 0;
167
+ if (warningInDomain > gates.WARNING) {
168
+ reasons.push(`Domain ${domain}: ${warningInDomain} WARNING failure(s) exceed gate limit of ${gates.WARNING}`);
169
+ exitCode = 2;
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ // Phase 5: Check visual regression gates if configured
176
+ if (!exitCode && effectivePolicy.visualGates && snapshot.intelligence) {
177
+ const intelligence = snapshot.intelligence;
178
+ const visualFailures = intelligence.failures?.filter(f => f.breakType === 'VISUAL') || [];
179
+ const visualCritical = visualFailures.filter(f => f.severity === 'CRITICAL').length || 0;
180
+ const visualWarning = visualFailures.filter(f => f.severity === 'WARNING').length || 0;
181
+ const maxDiffPercent = Math.max(...visualFailures.map(f => f.visualDiff?.percentChange || 0));
182
+
183
+ // Check CRITICAL visual diffs
184
+ if (effectivePolicy.visualGates.CRITICAL !== undefined) {
185
+ if (visualCritical > effectivePolicy.visualGates.CRITICAL) {
186
+ reasons.push(`Visual regression: ${visualCritical} CRITICAL diff(s) exceed gate limit of ${effectivePolicy.visualGates.CRITICAL}`);
187
+ exitCode = 1;
188
+ }
189
+ }
190
+
191
+ // Check WARNING visual diffs
192
+ if (!exitCode && effectivePolicy.visualGates.WARNING !== undefined) {
193
+ if (visualWarning > effectivePolicy.visualGates.WARNING) {
194
+ reasons.push(`Visual regression: ${visualWarning} WARNING diff(s) exceed gate limit of ${effectivePolicy.visualGates.WARNING}`);
195
+ exitCode = 2;
196
+ }
197
+ }
198
+
199
+ // Check max diff percent
200
+ if (!exitCode && effectivePolicy.visualGates.maxDiffPercent !== undefined) {
201
+ if (maxDiffPercent > effectivePolicy.visualGates.maxDiffPercent) {
202
+ reasons.push(`Visual regression: ${maxDiffPercent.toFixed(1)}% diff exceeds max threshold of ${effectivePolicy.visualGates.maxDiffPercent}%`);
203
+ exitCode = 1;
204
+ }
205
+ }
206
+ }
207
+
208
+ // Evaluate CRITICAL severity (always exit 1 if present)
209
+ if (effectivePolicy.failOnSeverity === 'CRITICAL' && criticalCount > 0) {
210
+ reasons.push(`${criticalCount} CRITICAL risk(s) detected (policy: failOnSeverity=CRITICAL)`);
211
+ exitCode = 1;
212
+ }
213
+
214
+ // Evaluate WARNING severity
215
+ if (effectivePolicy.failOnSeverity === 'WARNING' && warningCount > 0) {
216
+ reasons.push(`${warningCount} WARNING risk(s) detected (policy: failOnSeverity=WARNING)`);
217
+ exitCode = 1;
218
+ }
219
+
220
+ // Evaluate max warnings
221
+ if (!exitCode && warningCount > effectivePolicy.maxWarnings) {
222
+ reasons.push(`${warningCount} WARNING(s) exceed limit of ${effectivePolicy.maxWarnings}`);
223
+ exitCode = 2;
224
+ }
225
+
226
+ // Evaluate max info
227
+ if (!exitCode && infoCount > effectivePolicy.maxInfo) {
228
+ reasons.push(`${infoCount} INFO(s) exceed limit of ${effectivePolicy.maxInfo}`);
229
+ exitCode = 2;
230
+ }
231
+
232
+ // Evaluate total risk
233
+ if (!exitCode && totalRisk > effectivePolicy.maxTotalRisk) {
234
+ reasons.push(`${totalRisk} total risk(s) exceed limit of ${effectivePolicy.maxTotalRisk}`);
235
+ exitCode = 1;
236
+ }
237
+
238
+ // Evaluate baseline regression
239
+ if (!exitCode && effectivePolicy.failOnNewRegression) {
240
+ const baseline = snapshot.baseline || {};
241
+ const diff = baseline.diff || {};
242
+
243
+ if (diff.regressions && Object.keys(diff.regressions).length > 0) {
244
+ const regCount = Object.keys(diff.regressions).length;
245
+ reasons.push(`${regCount} baseline regression(s) detected (policy: failOnNewRegression=true)`);
246
+ exitCode = 1;
247
+ }
248
+ }
249
+
250
+ // Evaluate soft failures
251
+ if (!exitCode && effectivePolicy.failOnSoftFailures && softFailureCount > 0) {
252
+ reasons.push(`${softFailureCount} soft failure(s) detected (policy: failOnSoftFailures=true)`);
253
+ exitCode = 1;
254
+ }
255
+
256
+ // Evaluate soft failure threshold
257
+ if (!exitCode && softFailureCount > effectivePolicy.softFailureThreshold) {
258
+ reasons.push(`${softFailureCount} soft failure(s) exceed threshold of ${effectivePolicy.softFailureThreshold}`);
259
+ exitCode = 2;
260
+ }
261
+
262
+ // Coverage gaps (attempts skipped/not applicable)
263
+ if (!exitCode && effectivePolicy.coverage) {
264
+ if (effectivePolicy.coverage.failOnGap && coverage.gaps > 0) {
265
+ reasons.push(`Coverage gaps detected: ${coverage.gaps} of ${coverage.total || 'n/a'} attempts not executed`);
266
+ exitCode = 1;
267
+ } else if (effectivePolicy.coverage.warnOnGap && coverage.gaps > 0) {
268
+ reasons.push(`Coverage gaps detected: ${coverage.gaps} of ${coverage.total || 'n/a'} attempts not executed`);
269
+ exitCode = 2;
270
+ }
271
+ }
272
+
273
+ // Evidence completeness/integrity
274
+ if (!exitCode && effectivePolicy.evidence) {
275
+ if (evidenceMetrics.completeness < (effectivePolicy.evidence.minCompleteness ?? 1)) {
276
+ reasons.push(`Evidence completeness ${evidenceMetrics.completeness.toFixed(2)} below policy minimum ${(effectivePolicy.evidence.minCompleteness ?? 1)}`);
277
+ exitCode = exitCode || (effectivePolicy.evidence.minCompleteness >= 1 ? 1 : 2);
278
+ }
279
+ if (evidenceMetrics.integrity < (effectivePolicy.evidence.minIntegrity ?? 0)) {
280
+ reasons.push(`Evidence integrity ${evidenceMetrics.integrity.toFixed(2)} below policy minimum ${(effectivePolicy.evidence.minIntegrity ?? 0)}`);
281
+ exitCode = exitCode || 2;
282
+ }
283
+ if (effectivePolicy.evidence.requireScreenshots && missingScreenshots) {
284
+ reasons.push('Screenshots disabled but required by policy');
285
+ exitCode = 1;
286
+ }
287
+ if (effectivePolicy.evidence.requireTraces && missingTraces) {
288
+ reasons.push('Network traces disabled but required by policy');
289
+ exitCode = 1;
290
+ }
291
+ }
292
+
293
+ // Runtime signals (crawl/discovery/system)
294
+ if (!exitCode && runtimeSignals.length > 0) {
295
+ const desc = runtimeSignals.map(s => s.description).slice(0, 3).join('; ');
296
+ reasons.push(`Runtime issues detected: ${desc}`);
297
+ exitCode = 2;
298
+ }
299
+
300
+ // Evaluate baseline requirement
301
+ if (!exitCode && effectivePolicy.requireBaseline) {
302
+ const baseline = snapshot.baseline || {};
303
+ if (!baseline.baselineFound && !baseline.baselineCreatedThisRun) {
304
+ reasons.push('Baseline required but not found (policy: requireBaseline=true)');
305
+ exitCode = 1;
306
+ }
307
+ }
308
+
309
+ // Build summary
310
+ const summary =
311
+ exitCode === 0
312
+ ? '✅ Policy evaluation PASSED'
313
+ : exitCode === 1
314
+ ? ' Policy evaluation FAILED (exit code 1)'
315
+ : '⚠️ Policy evaluation WARNING (exit code 2)';
316
+
317
+ return {
318
+ passed: exitCode === 0,
319
+ exitCode,
320
+ reasons,
321
+ summary,
322
+ counts: {
323
+ critical: criticalCount,
324
+ warning: warningCount,
325
+ info: infoCount,
326
+ softFailures: softFailureCount,
327
+ totalRisk
328
+ }
329
+ };
330
+ }
331
+
332
+ /**
333
+ * Format policy evaluation results for CLI output
334
+ */
335
+ function formatPolicyOutput(evaluation) {
336
+ let output = '\n' + '━'.repeat(60) + '\n';
337
+ output += '🛡️ Policy Evaluation\n';
338
+ output += '━'.repeat(60) + '\n\n';
339
+
340
+ output += `${evaluation.summary}\n`;
341
+
342
+ if (evaluation.reasons.length > 0) {
343
+ output += '\nFailure reasons:\n';
344
+ evaluation.reasons.forEach(r => {
345
+ output += ` ❌ ${r}\n`;
346
+ });
347
+ }
348
+
349
+ output += `\nRisk counts:\n`;
350
+ output += ` 🔴 CRITICAL: ${evaluation.counts.critical}\n`;
351
+ output += ` 🟡 WARNING: ${evaluation.counts.warning}\n`;
352
+ output += ` 🔵 INFO: ${evaluation.counts.info}\n`;
353
+ output += ` 🐛 Soft Failures: ${evaluation.counts.softFailures}\n`;
354
+ output += ` 📊 Total Risks: ${evaluation.counts.totalRisk}\n`;
355
+
356
+ output += `\nExit Code: ${evaluation.exitCode}\n`;
357
+ output += '━'.repeat(60) + '\n';
358
+
359
+ return output;
360
+ }
361
+
362
+ /**
363
+ * Create a default policy file
364
+ */
365
+ function createDefaultPolicyFile(outputPath = 'config/guardian.policy.json') {
366
+ const defaultPolicy = {
367
+ failOnSeverity: 'CRITICAL',
368
+ maxWarnings: 0,
369
+ maxInfo: 999,
370
+ maxTotalRisk: 999,
371
+ failOnNewRegression: true,
372
+ failOnSoftFailures: false,
373
+ softFailureThreshold: 5,
374
+ requireBaseline: false
375
+ };
376
+
377
+ fs.writeFileSync(
378
+ outputPath,
379
+ JSON.stringify(defaultPolicy, null, 2),
380
+ 'utf8'
381
+ );
382
+
383
+ return outputPath;
384
+ }
385
+
386
+ /**
387
+ * Validate policy object structure
388
+ */
389
+ function validatePolicy(policy) {
390
+ const errors = [];
391
+
392
+ if (!policy || typeof policy !== 'object') {
393
+ return {
394
+ valid: false,
395
+ errors: ['Policy must be an object']
396
+ };
397
+ }
398
+
399
+ const severityValues = ['CRITICAL', 'WARNING', 'INFO'];
400
+ if (policy.failOnSeverity && !severityValues.includes(policy.failOnSeverity)) {
401
+ errors.push(`failOnSeverity must be one of: ${severityValues.join(', ')}`);
402
+ }
403
+
404
+ if (typeof policy.maxWarnings !== 'number' || policy.maxWarnings < 0) {
405
+ errors.push('maxWarnings must be a non-negative number');
406
+ }
407
+
408
+ if (typeof policy.maxInfo !== 'number' || policy.maxInfo < 0) {
409
+ errors.push('maxInfo must be a non-negative number');
410
+ }
411
+
412
+ if (typeof policy.maxTotalRisk !== 'number' || policy.maxTotalRisk < 0) {
413
+ errors.push('maxTotalRisk must be a non-negative number');
414
+ }
415
+
416
+ if (typeof policy.failOnNewRegression !== 'boolean') {
417
+ errors.push('failOnNewRegression must be a boolean');
418
+ }
419
+
420
+ return {
421
+ valid: errors.length === 0,
422
+ errors
423
+ };
424
+ }
425
+
426
+ module.exports = {
427
+ loadPolicy,
428
+ evaluatePolicy,
429
+ formatPolicyOutput,
430
+ createDefaultPolicyFile,
431
+ validatePolicy
432
+ };