@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.
- package/CHANGELOG.md +210 -210
- package/LICENSE +21 -21
- package/README.md +297 -184
- package/bin/guardian.js +2242 -2221
- package/config/README.md +59 -59
- package/config/guardian.config.json +54 -54
- package/config/guardian.policy.json +12 -12
- package/config/profiles/docs.yaml +18 -18
- package/config/profiles/ecommerce.yaml +17 -17
- package/config/profiles/landing-demo.yaml +16 -16
- package/config/profiles/marketing.yaml +18 -18
- package/config/profiles/saas.yaml +21 -21
- package/flows/example-login-flow.json +36 -36
- package/flows/example-signup-flow.json +44 -44
- package/package.json +124 -116
- package/policies/enterprise.json +12 -12
- package/policies/landing-demo.json +22 -22
- package/policies/saas.json +12 -12
- package/policies/startup.json +12 -12
- package/src/enterprise/audit-logger.js +166 -166
- package/src/enterprise/pdf-exporter.js +267 -267
- package/src/enterprise/rbac-gate.js +142 -142
- package/src/enterprise/rbac.js +239 -239
- package/src/enterprise/site-manager.js +180 -180
- package/src/founder/feedback-system.js +156 -156
- package/src/founder/founder-tracker.js +213 -213
- package/src/founder/usage-signals.js +141 -141
- package/src/guardian/action-hints.js +439 -439
- package/src/guardian/alert-ledger.js +121 -121
- package/src/guardian/artifact-sanitizer.js +56 -56
- package/src/guardian/attempt-engine.js +1069 -1029
- package/src/guardian/attempt-registry.js +267 -267
- package/src/guardian/attempt-relevance.js +106 -106
- package/src/guardian/attempt-reporter.js +513 -507
- package/src/guardian/attempt.js +274 -273
- package/src/guardian/attempts-filter.js +63 -63
- package/src/guardian/auto-attempt-builder.js +283 -283
- package/src/guardian/baseline-registry.js +177 -177
- package/src/guardian/baseline-reporter.js +143 -143
- package/src/guardian/baseline-storage.js +285 -285
- package/src/guardian/baseline.js +535 -534
- package/src/guardian/behavioral-signals.js +261 -261
- package/src/guardian/breakage-intelligence.js +224 -224
- package/src/guardian/browser-pool.js +131 -131
- package/src/guardian/browser.js +119 -119
- package/src/guardian/canonical-truth.js +308 -308
- package/src/guardian/ci-cli.js +121 -121
- package/src/guardian/ci-gate.js +96 -96
- package/src/guardian/ci-mode.js +15 -15
- package/src/guardian/ci-output.js +55 -38
- package/src/guardian/cli-summary.js +102 -102
- package/src/guardian/confidence-signals.js +251 -251
- package/src/guardian/config-loader.js +161 -161
- package/src/guardian/config-validator.js +285 -283
- package/src/guardian/coverage-model.js +239 -239
- package/src/guardian/coverage-packs.js +58 -58
- package/src/guardian/crawler.js +142 -142
- package/src/guardian/data-guardian-detector.js +189 -189
- package/src/guardian/decision-authority.js +746 -725
- package/src/guardian/detection-layers.js +271 -271
- package/src/guardian/determinism.js +146 -146
- package/src/guardian/discovery-engine.js +661 -661
- package/src/guardian/drift-detector.js +100 -100
- package/src/guardian/enhanced-html-reporter.js +522 -522
- package/src/guardian/env-guard.js +128 -127
- package/src/guardian/error-clarity.js +399 -399
- package/src/guardian/export-contract.js +196 -196
- package/src/guardian/fail-safe.js +212 -212
- package/src/guardian/failure-intelligence.js +173 -173
- package/src/guardian/failure-taxonomy.js +169 -169
- package/src/guardian/final-outcome.js +206 -206
- package/src/guardian/first-run-profile.js +89 -89
- package/src/guardian/first-run.js +65 -67
- package/src/guardian/flag-validator.js +111 -111
- package/src/guardian/flow-executor.js +641 -639
- package/src/guardian/flow-registry.js +67 -67
- package/src/guardian/honesty.js +394 -394
- package/src/guardian/html-reporter.js +416 -416
- package/src/guardian/human-intent-resolver.js +296 -296
- package/src/guardian/human-interaction-model.js +351 -351
- package/src/guardian/human-journey-context.js +184 -184
- package/src/guardian/human-navigator.js +544 -544
- package/src/guardian/human-reporter.js +435 -431
- package/src/guardian/index.js +226 -221
- package/src/guardian/init-command.js +143 -143
- package/src/guardian/intent-detector.js +148 -146
- package/src/guardian/journey-definitions.js +132 -132
- package/src/guardian/journey-scan-cli.js +142 -145
- package/src/guardian/journey-scanner.js +583 -583
- package/src/guardian/junit-reporter.js +281 -281
- package/src/guardian/language-detection.js +99 -99
- package/src/guardian/live-alert.js +56 -56
- package/src/guardian/live-baseline-compare.js +146 -146
- package/src/guardian/live-cli.js +95 -95
- package/src/guardian/live-guardian.js +210 -210
- package/src/guardian/live-scheduler-runner.js +137 -137
- package/src/guardian/live-scheduler-state.js +167 -168
- package/src/guardian/live-scheduler.js +146 -146
- package/src/guardian/live-state.js +110 -110
- package/src/guardian/market-criticality.js +335 -335
- package/src/guardian/market-reporter.js +577 -577
- package/src/guardian/network-trace.js +178 -178
- package/src/guardian/obs-logger.js +110 -110
- package/src/guardian/observed-capabilities.js +427 -427
- package/src/guardian/output-contract.js +154 -0
- package/src/guardian/output-readability.js +264 -264
- package/src/guardian/parallel-executor.js +116 -116
- package/src/guardian/path-safety.js +56 -56
- package/src/guardian/pattern-analyzer.js +348 -348
- package/src/guardian/policy.js +432 -434
- package/src/guardian/prelaunch-gate.js +193 -193
- package/src/guardian/prerequisite-checker.js +101 -101
- package/src/guardian/preset-loader.js +152 -157
- package/src/guardian/profile-loader.js +96 -96
- package/src/guardian/reality.js +3025 -2826
- package/src/guardian/realworld-scenarios.js +94 -94
- package/src/guardian/reporter.js +167 -167
- package/src/guardian/retry-policy.js +123 -123
- package/src/guardian/root-cause-analysis.js +171 -171
- package/src/guardian/rules-engine.js +558 -558
- package/src/guardian/run-artifacts.js +212 -212
- package/src/guardian/run-cleanup.js +207 -207
- package/src/guardian/run-export.js +522 -522
- package/src/guardian/run-latest.js +90 -90
- package/src/guardian/run-list.js +211 -211
- package/src/guardian/run-summary.js +20 -20
- package/src/guardian/runtime-root.js +246 -246
- package/src/guardian/safety.js +248 -248
- package/src/guardian/scan-presets.js +133 -149
- package/src/guardian/screenshot.js +152 -152
- package/src/guardian/secret-hygiene.js +44 -44
- package/src/guardian/selector-fallbacks.js +394 -394
- package/src/guardian/semantic-contact-detection.js +255 -255
- package/src/guardian/semantic-contact-finder.js +201 -201
- package/src/guardian/semantic-targets.js +234 -234
- package/src/guardian/site-intelligence.js +588 -588
- package/src/guardian/site-introspection.js +257 -257
- package/src/guardian/sitemap.js +225 -225
- package/src/guardian/smoke.js +283 -258
- package/src/guardian/snapshot-schema.js +177 -290
- package/src/guardian/snapshot.js +430 -397
- package/src/guardian/stability-scorer.js +169 -169
- package/src/guardian/success-evaluator.js +214 -214
- package/src/guardian/template-command.js +184 -184
- package/src/guardian/text-formatters.js +426 -426
- package/src/guardian/timeout-profiles.js +57 -57
- package/src/guardian/truth/attempt.contract.js +158 -0
- package/src/guardian/truth/decision.contract.js +275 -0
- package/src/guardian/truth/snapshot.contract.js +363 -0
- package/src/guardian/validators.js +323 -323
- package/src/guardian/verdict-card.js +474 -474
- package/src/guardian/verdict-clarity.js +298 -298
- package/src/guardian/verdict-policy.js +363 -363
- package/src/guardian/verdict.js +333 -333
- package/src/guardian/verdicts.js +79 -74
- package/src/guardian/visual-diff.js +247 -247
- package/src/guardian/wait-for-outcome.js +119 -119
- package/src/guardian/watch-runner.js +181 -181
- package/src/guardian/watchdog-diff.js +167 -167
- package/src/guardian/webhook.js +206 -206
- package/src/payments/stripe-checkout.js +169 -169
- package/src/plans/plan-definitions.js +148 -148
- package/src/plans/plan-manager.js +211 -211
- package/src/plans/usage-tracker.js +210 -210
- package/src/recipes/recipe-engine.js +188 -188
- package/src/recipes/recipe-failure-analysis.js +159 -159
- package/src/recipes/recipe-registry.js +134 -134
- package/src/recipes/recipe-runtime.js +507 -507
- package/src/recipes/recipe-store.js +410 -410
- package/SECURITY.md +0 -77
- package/VERSIONING.md +0 -100
- package/guardian-contract-v1.md +0 -502
package/src/guardian/policy.js
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* @
|
|
19
|
-
* @property {
|
|
20
|
-
* @property {number} [
|
|
21
|
-
* @property {
|
|
22
|
-
* @property {
|
|
23
|
-
* @property {
|
|
24
|
-
* @property {boolean} [
|
|
25
|
-
* @property {
|
|
26
|
-
* @property {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
'
|
|
72
|
-
'guardian.policy.json'
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
? '
|
|
315
|
-
:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
output +=
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
output +=
|
|
352
|
-
output += `
|
|
353
|
-
output += `
|
|
354
|
-
output += `
|
|
355
|
-
|
|
356
|
-
output +=
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
output
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
+
};
|