@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
|
@@ -1,725 +1,746 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* UNIFIED DECISION AUTHORITY
|
|
3
|
-
*
|
|
4
|
-
* The SINGLE source of truth for final verdict determination.
|
|
5
|
-
* All verdict signals (rules, flows, attempts, journey, policy, baseline)
|
|
6
|
-
* flow through this function only.
|
|
7
|
-
*
|
|
8
|
-
* This module is PURE: no IO, no side effects, no hidden state.
|
|
9
|
-
* All dependencies are passed in explicitly.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* @param {Object} signals
|
|
87
|
-
* @param {
|
|
88
|
-
* @param {
|
|
89
|
-
* @param {Object} signals.
|
|
90
|
-
* @param {
|
|
91
|
-
* @param {
|
|
92
|
-
*
|
|
93
|
-
* @
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
timestamp
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
// Classify
|
|
250
|
-
const
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1
|
+
/**
|
|
2
|
+
* UNIFIED DECISION AUTHORITY
|
|
3
|
+
*
|
|
4
|
+
* The SINGLE source of truth for final verdict determination.
|
|
5
|
+
* All verdict signals (rules, flows, attempts, journey, policy, baseline)
|
|
6
|
+
* flow through this function only.
|
|
7
|
+
*
|
|
8
|
+
* This module is PURE: no IO, no side effects, no hidden state.
|
|
9
|
+
* All dependencies are passed in explicitly.
|
|
10
|
+
*
|
|
11
|
+
* @typedef {import('./truth/decision.contract.js').FinalDecision} FinalDecision
|
|
12
|
+
* @typedef {import('./truth/decision.contract.js').FinalVerdict} FinalVerdict
|
|
13
|
+
* @typedef {import('./truth/decision.contract.js').VerdictSource} VerdictSource
|
|
14
|
+
* @typedef {import('./truth/decision.contract.js').DecisionReason} DecisionReason
|
|
15
|
+
* @typedef {import('./truth/decision.contract.js').VerdictHistoryEntry} VerdictHistoryEntry
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
toCanonicalVerdict,
|
|
20
|
+
mapExitCodeFromCanonical,
|
|
21
|
+
normalizeCanonicalVerdict
|
|
22
|
+
} = require('./verdicts');
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
computeCoverageSummary,
|
|
26
|
+
computeSelectorConfidence,
|
|
27
|
+
SELECTOR_CONFIDENCE,
|
|
28
|
+
COVERAGE_THRESHOLD
|
|
29
|
+
} = require('./coverage-model');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* RUNTIME GUARD: One-call-per-run enforcement
|
|
33
|
+
* Prevents accidental double calls within the same process execution.
|
|
34
|
+
* Per-run state is maintained via runId passed in options.
|
|
35
|
+
*/
|
|
36
|
+
const callTracker = new Map(); // runId -> { called: boolean, timestamp }
|
|
37
|
+
|
|
38
|
+
function validateSingleCall(runId) {
|
|
39
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
40
|
+
|
|
41
|
+
// Track by runId. If no runId provided, use a default key for tests.
|
|
42
|
+
const trackKey = runId || '__default_run__';
|
|
43
|
+
|
|
44
|
+
if (callTracker.has(trackKey)) {
|
|
45
|
+
const entry = callTracker.get(trackKey);
|
|
46
|
+
const message = `computeDecisionAuthority called twice in same run (${trackKey}). First call: ${entry.timestamp}`;
|
|
47
|
+
|
|
48
|
+
if (!isProduction) {
|
|
49
|
+
throw new Error(message);
|
|
50
|
+
}
|
|
51
|
+
// In production, log warning but allow (graceful degradation)
|
|
52
|
+
console.warn(`⚠️ ${message}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
callTracker.set(trackKey, { called: true, timestamp: new Date().toISOString() });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resetCallTracker(runId) {
|
|
59
|
+
const trackKey = runId || '__default_run__';
|
|
60
|
+
callTracker.delete(trackKey);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* VERDICT SOURCE CONSTANTS
|
|
65
|
+
*/
|
|
66
|
+
const VERDICT_SOURCE = {
|
|
67
|
+
RULES_ENGINE: 'rules_engine',
|
|
68
|
+
RULES_ENGINE_FALLBACK: 'rules_engine_fallback',
|
|
69
|
+
FLOWS_FAILURE: 'flows_failure',
|
|
70
|
+
FLOWS_FRICTION: 'flows_friction',
|
|
71
|
+
ATTEMPTS_FAILURE: 'attempts_failure',
|
|
72
|
+
ATTEMPTS_FRICTION: 'attempts_friction',
|
|
73
|
+
JOURNEY_DOWNGRADE: 'journey_downgrade',
|
|
74
|
+
INSUFFICIENT_DATA: 'insufficient_data',
|
|
75
|
+
OBSERVED: 'observed_success',
|
|
76
|
+
POLICY_HARD_FAIL: 'policy_hard_failure',
|
|
77
|
+
BASELINE_REGRESSION: 'baseline_regression',
|
|
78
|
+
ERROR: 'error_handler'
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* PRIMARY DECISION AUTHORITY FUNCTION
|
|
83
|
+
*
|
|
84
|
+
* Accepts all signals and produces a single, deterministic final verdict.
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} signals - All verdict input signals
|
|
87
|
+
* @param {Array} signals.flows - Flow execution results
|
|
88
|
+
* @param {Array} signals.attempts - Attempt execution results
|
|
89
|
+
* @param {Object} signals.rulesEngineOutput - Rules engine result (if successful)
|
|
90
|
+
* @param {string} signals.journeyVerdict - Journey verdict (if human journey executed)
|
|
91
|
+
* @param {Object} signals.policyEval - Policy evaluation result
|
|
92
|
+
* @param {Object} signals.baseline - Baseline comparison result
|
|
93
|
+
* @param {Object} signals.marketImpact - Market impact assessment
|
|
94
|
+
* @param {Object} signals.coverage - Coverage statistics
|
|
95
|
+
* @param {Object} signals.siteIntelligence - Site intelligence data
|
|
96
|
+
* @param {Object} options - Configuration options
|
|
97
|
+
* @param {boolean} options.ciMode - CI mode (affects logging)
|
|
98
|
+
*
|
|
99
|
+
* @returns {Object} - Final decision object
|
|
100
|
+
* - finalVerdict: string (READY|FRICTION|DO_NOT_LAUNCH|INSUFFICIENT_DATA|ERROR)
|
|
101
|
+
* - verdictSource: string (which component determined verdict)
|
|
102
|
+
* - verdictHistory: Array of {phase, source, suggestedVerdict, reasonCode, timestamp}
|
|
103
|
+
* - exitCode: number (0|1|2, derived deterministically from finalVerdict)
|
|
104
|
+
* - reasons: Array of {code, message}
|
|
105
|
+
* - confidence: number (0-1, how confident is the verdict)
|
|
106
|
+
*/
|
|
107
|
+
function computeDecisionAuthority(signals, options = {}) {
|
|
108
|
+
const runId = options.runId; // Per-run identifier
|
|
109
|
+
const timestamp = Date.now();
|
|
110
|
+
|
|
111
|
+
// RUNTIME GUARD: Enforce single call per run
|
|
112
|
+
validateSingleCall(runId);
|
|
113
|
+
|
|
114
|
+
// Initialize tracking
|
|
115
|
+
/** @type {VerdictHistoryEntry[]} */
|
|
116
|
+
const verdictHistory = [];
|
|
117
|
+
/** @type {DecisionReason[]} */
|
|
118
|
+
const reasons = [];
|
|
119
|
+
/** @type {FinalVerdict|null} */
|
|
120
|
+
let currentVerdict = null;
|
|
121
|
+
/** @type {VerdictSource|null} */
|
|
122
|
+
let verdictSource = null;
|
|
123
|
+
let finalConfidence = 1.0;
|
|
124
|
+
|
|
125
|
+
// Extract signals with safe defaults
|
|
126
|
+
const flows = signals.flows || [];
|
|
127
|
+
const attempts = signals.attempts || [];
|
|
128
|
+
const rulesEngineOutput = signals.rulesEngineOutput || null;
|
|
129
|
+
const journeyVerdict = signals.journeyVerdict || null;
|
|
130
|
+
const policyEval = signals.policyEval || null;
|
|
131
|
+
const baseline = signals.baseline || {};
|
|
132
|
+
const audit = signals.audit || {};
|
|
133
|
+
const humanPath = signals.humanPath || null; // Stage 3
|
|
134
|
+
const networkSafety = signals.networkSafety || {};
|
|
135
|
+
const secretFindings = signals.secretFindings || [];
|
|
136
|
+
|
|
137
|
+
// ========================================================================
|
|
138
|
+
// COVERAGE & SELECTOR CONFIDENCE PRE-CHECK
|
|
139
|
+
// ========================================================================
|
|
140
|
+
|
|
141
|
+
const coverageSummary = computeCoverageSummary(attempts, flows, audit);
|
|
142
|
+
const selectorConfidenceSummary = computeSelectorConfidence(attempts);
|
|
143
|
+
|
|
144
|
+
// Store for artifact inclusion
|
|
145
|
+
const coverageInfo = {
|
|
146
|
+
...coverageSummary,
|
|
147
|
+
selectorConfidence: selectorConfidenceSummary
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// ========================================================================
|
|
151
|
+
// PHASE 1: RULES ENGINE AUTHORITY (Highest Priority)
|
|
152
|
+
// ========================================================================
|
|
153
|
+
|
|
154
|
+
if (rulesEngineOutput) {
|
|
155
|
+
// Rules engine succeeded and produced a verdict
|
|
156
|
+
const rulesVerdict = toCanonicalVerdict(rulesEngineOutput.finalVerdict);
|
|
157
|
+
|
|
158
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
159
|
+
phase: 1,
|
|
160
|
+
source: VERDICT_SOURCE.RULES_ENGINE,
|
|
161
|
+
suggestedVerdict: rulesVerdict,
|
|
162
|
+
reasonCode: 'RULES_ENGINE_TRIGGERED',
|
|
163
|
+
triggeredRuleIds: rulesEngineOutput.triggeredRuleIds || [],
|
|
164
|
+
timestamp
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
// Add final normalization entry to ensure >= 2 history entries
|
|
168
|
+
const timestamp2 = new Date().toISOString();
|
|
169
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
170
|
+
phase: 'final',
|
|
171
|
+
source: 'normalization',
|
|
172
|
+
suggestedVerdict: rulesVerdict,
|
|
173
|
+
reasonCode: 'VERDICT_NORMALIZED',
|
|
174
|
+
timestamp: timestamp2
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
currentVerdict = /** @type {FinalVerdict} */ (rulesVerdict);
|
|
178
|
+
verdictSource = /** @type {VerdictSource} */ (VERDICT_SOURCE.RULES_ENGINE);
|
|
179
|
+
|
|
180
|
+
// Add rules reasons to main reasons array
|
|
181
|
+
if (rulesEngineOutput.reasons) {
|
|
182
|
+
reasons.push(...rulesEngineOutput.reasons);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ====================================================================
|
|
186
|
+
// COVERAGE ENFORCEMENT: READY requires sufficient coverage
|
|
187
|
+
// ====================================================================
|
|
188
|
+
if (rulesVerdict === 'READY' && coverageSummary.coverageStatus === 'INSUFFICIENT') {
|
|
189
|
+
// READY cannot proceed without sufficient coverage
|
|
190
|
+
reasons.push({
|
|
191
|
+
code: 'COVERAGE_INSUFFICIENT',
|
|
192
|
+
message: `Coverage ${(coverageSummary.coverageRatio * 100).toFixed(1)}% below 70% threshold`,
|
|
193
|
+
severity: 'blocker'
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Downgrade to FRICTION
|
|
197
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
198
|
+
phase: 'enforcement',
|
|
199
|
+
source: 'coverage_check',
|
|
200
|
+
suggestedVerdict: 'FRICTION',
|
|
201
|
+
reasonCode: 'COVERAGE_INSUFFICIENT',
|
|
202
|
+
timestamp: new Date().toISOString()
|
|
203
|
+
}));
|
|
204
|
+
|
|
205
|
+
currentVerdict = /** @type {FinalVerdict} */ ('FRICTION');
|
|
206
|
+
verdictSource = 'coverage_downgrade';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// SELECTOR CONFIDENCE ENFORCEMENT: LOW confidence on critical path downgrades verdict
|
|
210
|
+
if (rulesVerdict === 'READY' &&
|
|
211
|
+
selectorConfidenceSummary.selectorConfidenceMin === SELECTOR_CONFIDENCE.LOW) {
|
|
212
|
+
reasons.push({
|
|
213
|
+
code: 'LOW_SELECTOR_CONFIDENCE',
|
|
214
|
+
message: 'Critical interaction steps used LOW-confidence selectors (classes, nth-child)',
|
|
215
|
+
severity: 'warning'
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
219
|
+
phase: 'enforcement',
|
|
220
|
+
source: 'selector_confidence_check',
|
|
221
|
+
suggestedVerdict: 'FRICTION',
|
|
222
|
+
reasonCode: 'LOW_SELECTOR_CONFIDENCE',
|
|
223
|
+
timestamp: new Date().toISOString()
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
currentVerdict = /** @type {FinalVerdict} */ ('FRICTION');
|
|
227
|
+
verdictSource = /** @type {VerdictSource} */ ('selector_downgrade');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Rules engine verdict takes absolute precedence - return immediately
|
|
231
|
+
return buildFinalDecision({
|
|
232
|
+
finalVerdict: /** @type {FinalVerdict} */ (currentVerdict || rulesVerdict),
|
|
233
|
+
verdictSource: /** @type {VerdictSource} */ (verdictSource || VERDICT_SOURCE.RULES_ENGINE),
|
|
234
|
+
verdictHistory: /** @type {VerdictHistoryEntry[]} */ (verdictHistory),
|
|
235
|
+
reasons,
|
|
236
|
+
confidence: rulesEngineOutput.confidence || 0.95,
|
|
237
|
+
exitCode: mapExitCodeFromCanonical(currentVerdict || rulesVerdict),
|
|
238
|
+
coverageInfo,
|
|
239
|
+
humanPath,
|
|
240
|
+
networkSafety,
|
|
241
|
+
secretFindings
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ========================================================================
|
|
246
|
+
// PHASE 2: LEGACY VERDICT COMPUTATION (Flows → Attempts → Journey)
|
|
247
|
+
// ========================================================================
|
|
248
|
+
|
|
249
|
+
// Classify flows
|
|
250
|
+
const failedFlows = flows.filter(f =>
|
|
251
|
+
f.outcome === 'FAILURE' || f.success === false
|
|
252
|
+
);
|
|
253
|
+
const frictionFlows = flows.filter(f =>
|
|
254
|
+
f.outcome === 'FRICTION'
|
|
255
|
+
);
|
|
256
|
+
const notApplicableFlows = flows.filter(f => f.outcome === 'NOT_APPLICABLE');
|
|
257
|
+
|
|
258
|
+
// Classify attempts
|
|
259
|
+
const executedAttempts = attempts.filter(a => a.executed);
|
|
260
|
+
const failedAttempts = executedAttempts.filter(a =>
|
|
261
|
+
a.outcome === 'FAILURE'
|
|
262
|
+
);
|
|
263
|
+
const frictionAttempts = executedAttempts.filter(a =>
|
|
264
|
+
a.outcome === 'FRICTION'
|
|
265
|
+
);
|
|
266
|
+
const notApplicableAttempts = attempts.filter(a => a.outcome === 'NOT_APPLICABLE');
|
|
267
|
+
const successfulAttempts = executedAttempts.filter(a => a.outcome === 'SUCCESS');
|
|
268
|
+
|
|
269
|
+
// Count applicable signals
|
|
270
|
+
const applicableAttempts = attempts.filter(a => a.outcome !== 'NOT_APPLICABLE');
|
|
271
|
+
const applicableFlows = flows.filter(f => f.outcome !== 'NOT_APPLICABLE');
|
|
272
|
+
|
|
273
|
+
// STEP 2a: Check for CRITICAL FAILURES (Flows)
|
|
274
|
+
if (failedFlows.length > 0) {
|
|
275
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
276
|
+
phase: 2,
|
|
277
|
+
step: 'a',
|
|
278
|
+
source: VERDICT_SOURCE.FLOWS_FAILURE,
|
|
279
|
+
suggestedVerdict: 'DO_NOT_LAUNCH',
|
|
280
|
+
reasonCode: 'FLOWS_HAVE_FAILURES',
|
|
281
|
+
count: failedFlows.length,
|
|
282
|
+
timestamp
|
|
283
|
+
}));
|
|
284
|
+
|
|
285
|
+
currentVerdict = /** @type {FinalVerdict} */ ('DO_NOT_LAUNCH');
|
|
286
|
+
verdictSource = /** @type {VerdictSource} */ (VERDICT_SOURCE.FLOWS_FAILURE);
|
|
287
|
+
reasons.push({
|
|
288
|
+
code: 'FLOW_FAILURES',
|
|
289
|
+
message: `Critical flow failures detected (${failedFlows.length}): ${failedFlows.map(f => f.flowId || f.flowName).join(', ')}`
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Don't check attempts if flows already failed
|
|
293
|
+
return buildFinalDecision({
|
|
294
|
+
finalVerdict: /** @type {FinalVerdict} */ (currentVerdict),
|
|
295
|
+
verdictSource: /** @type {VerdictSource} */ (verdictSource),
|
|
296
|
+
verdictHistory: /** @type {VerdictHistoryEntry[]} */ (verdictHistory),
|
|
297
|
+
reasons: enrichReasons(reasons, {failedFlows, failedAttempts, notApplicableFlows, notApplicableAttempts}),
|
|
298
|
+
confidence: 0.99,
|
|
299
|
+
exitCode: mapExitCodeFromCanonical(currentVerdict),
|
|
300
|
+
coverageInfo,
|
|
301
|
+
humanPath,
|
|
302
|
+
networkSafety,
|
|
303
|
+
secretFindings
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// STEP 2b: Check for FLOW FRICTION
|
|
308
|
+
if (frictionFlows.length > 0 && failedFlows.length === 0) {
|
|
309
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
310
|
+
phase: 2,
|
|
311
|
+
step: 'b',
|
|
312
|
+
source: VERDICT_SOURCE.FLOWS_FRICTION,
|
|
313
|
+
suggestedVerdict: 'FRICTION',
|
|
314
|
+
reasonCode: 'FLOWS_HAVE_FRICTION',
|
|
315
|
+
count: frictionFlows.length,
|
|
316
|
+
timestamp
|
|
317
|
+
}));
|
|
318
|
+
|
|
319
|
+
currentVerdict = 'FRICTION';
|
|
320
|
+
verdictSource = VERDICT_SOURCE.FLOWS_FRICTION;
|
|
321
|
+
reasons.push({
|
|
322
|
+
code: 'FLOW_FRICTION',
|
|
323
|
+
message: `Flow friction detected (${frictionFlows.length}): ${frictionFlows.map(f => f.flowId || f.flowName).join(', ')}`
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// STEP 2c: Check for ATTEMPT FAILURES (only if current verdict is not already FRICTION)
|
|
328
|
+
if (currentVerdict !== 'FRICTION' && failedAttempts.length > 0) {
|
|
329
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
330
|
+
phase: 2,
|
|
331
|
+
step: 'c',
|
|
332
|
+
source: VERDICT_SOURCE.ATTEMPTS_FAILURE,
|
|
333
|
+
suggestedVerdict: 'DO_NOT_LAUNCH',
|
|
334
|
+
reasonCode: 'ATTEMPTS_HAVE_FAILURES',
|
|
335
|
+
count: failedAttempts.length,
|
|
336
|
+
timestamp
|
|
337
|
+
}));
|
|
338
|
+
|
|
339
|
+
currentVerdict = 'DO_NOT_LAUNCH';
|
|
340
|
+
verdictSource = VERDICT_SOURCE.ATTEMPTS_FAILURE;
|
|
341
|
+
reasons.push({
|
|
342
|
+
code: 'ATTEMPT_FAILURES',
|
|
343
|
+
message: `Critical attempt failures detected (${failedAttempts.length}): ${failedAttempts.map(a => a.attemptId).join(', ')}`
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return buildFinalDecision({
|
|
347
|
+
finalVerdict: /** @type {FinalVerdict} */ (currentVerdict),
|
|
348
|
+
verdictSource: /** @type {VerdictSource} */ (verdictSource),
|
|
349
|
+
verdictHistory: /** @type {VerdictHistoryEntry[]} */ (verdictHistory),
|
|
350
|
+
reasons: enrichReasons(reasons, {failedAttempts, notApplicableAttempts}),
|
|
351
|
+
confidence: 0.99,
|
|
352
|
+
exitCode: mapExitCodeFromCanonical(currentVerdict),
|
|
353
|
+
coverageInfo,
|
|
354
|
+
humanPath,
|
|
355
|
+
networkSafety,
|
|
356
|
+
secretFindings
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// STEP 2d: Check for ATTEMPT FRICTION (only if not already FRICTION from flows)
|
|
361
|
+
if (currentVerdict !== 'FRICTION' && frictionAttempts.length > 0 && failedAttempts.length === 0) {
|
|
362
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
363
|
+
phase: 2,
|
|
364
|
+
step: 'd',
|
|
365
|
+
source: VERDICT_SOURCE.ATTEMPTS_FRICTION,
|
|
366
|
+
suggestedVerdict: 'FRICTION',
|
|
367
|
+
reasonCode: 'ATTEMPTS_HAVE_FRICTION',
|
|
368
|
+
count: frictionAttempts.length,
|
|
369
|
+
timestamp
|
|
370
|
+
}));
|
|
371
|
+
|
|
372
|
+
currentVerdict = 'FRICTION';
|
|
373
|
+
verdictSource = VERDICT_SOURCE.ATTEMPTS_FRICTION;
|
|
374
|
+
reasons.push({
|
|
375
|
+
code: 'ATTEMPT_FRICTION',
|
|
376
|
+
message: `Attempt friction detected (${frictionAttempts.length}): ${frictionAttempts.map(a => a.attemptId).join(', ')}`
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// STEP 2e: Check for POLICY HARD FAILURE
|
|
381
|
+
if (policyEval && !policyEval.passed && policyEval.exitCode === 1) {
|
|
382
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
383
|
+
phase: 2,
|
|
384
|
+
step: 'e',
|
|
385
|
+
source: VERDICT_SOURCE.POLICY_HARD_FAIL,
|
|
386
|
+
suggestedVerdict: 'DO_NOT_LAUNCH',
|
|
387
|
+
reasonCode: 'POLICY_HARD_FAILURE',
|
|
388
|
+
timestamp
|
|
389
|
+
}));
|
|
390
|
+
|
|
391
|
+
currentVerdict = /** @type {FinalVerdict} */ ('DO_NOT_LAUNCH');
|
|
392
|
+
verdictSource = /** @type {VerdictSource} */ (VERDICT_SOURCE.POLICY_HARD_FAIL);
|
|
393
|
+
reasons.push({
|
|
394
|
+
code: 'POLICY_HARD_FAILURE',
|
|
395
|
+
message: policyEval.summary || 'Policy hard failure detected'
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
return buildFinalDecision({
|
|
399
|
+
finalVerdict: /** @type {FinalVerdict} */ (currentVerdict),
|
|
400
|
+
verdictSource: /** @type {VerdictSource} */ (verdictSource),
|
|
401
|
+
verdictHistory: /** @type {VerdictHistoryEntry[]} */ (verdictHistory),
|
|
402
|
+
reasons: enrichReasons(reasons, {policyEval}),
|
|
403
|
+
confidence: 0.99,
|
|
404
|
+
exitCode: mapExitCodeFromCanonical(currentVerdict),
|
|
405
|
+
coverageInfo,
|
|
406
|
+
humanPath,
|
|
407
|
+
networkSafety,
|
|
408
|
+
secretFindings
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ========================================================================
|
|
413
|
+
// PHASE 3: DETERMINE DEFAULT VERDICT (No failures/friction found)
|
|
414
|
+
// ========================================================================
|
|
415
|
+
|
|
416
|
+
// Check if we have any applicable signals
|
|
417
|
+
if (applicableAttempts.length === 0 && applicableFlows.length === 0) {
|
|
418
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
419
|
+
phase: 3,
|
|
420
|
+
source: VERDICT_SOURCE.INSUFFICIENT_DATA,
|
|
421
|
+
suggestedVerdict: 'INSUFFICIENT_DATA',
|
|
422
|
+
reasonCode: 'NO_APPLICABLE_SIGNALS',
|
|
423
|
+
timestamp
|
|
424
|
+
}));
|
|
425
|
+
|
|
426
|
+
currentVerdict = 'INSUFFICIENT_DATA';
|
|
427
|
+
verdictSource = VERDICT_SOURCE.INSUFFICIENT_DATA;
|
|
428
|
+
reasons.push({
|
|
429
|
+
code: 'NO_APPLICABLE_SIGNALS',
|
|
430
|
+
message: 'No applicable flows or attempts found to execute'
|
|
431
|
+
});
|
|
432
|
+
finalConfidence = 0.3;
|
|
433
|
+
} else {
|
|
434
|
+
// We have signals and no critical failures/friction
|
|
435
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
436
|
+
phase: 3,
|
|
437
|
+
source: VERDICT_SOURCE.OBSERVED,
|
|
438
|
+
suggestedVerdict: 'OBSERVED',
|
|
439
|
+
reasonCode: 'NO_CRITICAL_FAILURES',
|
|
440
|
+
timestamp
|
|
441
|
+
}));
|
|
442
|
+
|
|
443
|
+
currentVerdict = /** @type {FinalVerdict} */ ('READY');
|
|
444
|
+
verdictSource = VERDICT_SOURCE.OBSERVED;
|
|
445
|
+
reasons.push({
|
|
446
|
+
code: 'OBSERVED_SUCCESS',
|
|
447
|
+
message: `Executed ${executedAttempts.length} attempt(s): ${successfulAttempts.length} successful, ${failedAttempts.length} failed, ${frictionAttempts.length} friction`
|
|
448
|
+
});
|
|
449
|
+
finalConfidence = 0.95;
|
|
450
|
+
|
|
451
|
+
// *** COVERAGE ENFORCEMENT IN PHASE 3 ***
|
|
452
|
+
// Even if no failures/friction, coverage must still be sufficient for READY
|
|
453
|
+
if (coverageInfo.coverageStatus === 'INSUFFICIENT') {
|
|
454
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
455
|
+
phase: 3,
|
|
456
|
+
source: 'coverage_enforcement',
|
|
457
|
+
suggestedVerdict: 'FRICTION',
|
|
458
|
+
reasonCode: 'COVERAGE_INSUFFICIENT',
|
|
459
|
+
details: {
|
|
460
|
+
coverage: coverageInfo.coverageRatio,
|
|
461
|
+
threshold: COVERAGE_THRESHOLD,
|
|
462
|
+
message: `Coverage is ${(coverageInfo.coverageRatio * 100).toFixed(1)}%, below ${(COVERAGE_THRESHOLD * 100).toFixed(0)}% threshold`
|
|
463
|
+
},
|
|
464
|
+
timestamp
|
|
465
|
+
}));
|
|
466
|
+
|
|
467
|
+
currentVerdict = /** @type {FinalVerdict} */ ('FRICTION');
|
|
468
|
+
verdictSource = 'coverage_downgrade';
|
|
469
|
+
finalConfidence = 0.75;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// *** SELECTOR CONFIDENCE ENFORCEMENT IN PHASE 3 ***
|
|
473
|
+
// Even if coverage OK, LOW selector confidence blocks READY
|
|
474
|
+
if (currentVerdict !== 'FRICTION' &&
|
|
475
|
+
coverageInfo.selectorConfidence &&
|
|
476
|
+
coverageInfo.selectorConfidence.selectorConfidenceMin === SELECTOR_CONFIDENCE.LOW) {
|
|
477
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
478
|
+
phase: 3,
|
|
479
|
+
source: 'selector_confidence_enforcement',
|
|
480
|
+
suggestedVerdict: 'FRICTION',
|
|
481
|
+
reasonCode: 'LOW_SELECTOR_CONFIDENCE',
|
|
482
|
+
details: {
|
|
483
|
+
minConfidence: coverageInfo.selectorConfidence.selectorConfidenceMin,
|
|
484
|
+
message: 'Critical interaction steps used LOW-confidence selectors (classes, nth-child, text)'
|
|
485
|
+
},
|
|
486
|
+
timestamp
|
|
487
|
+
}));
|
|
488
|
+
|
|
489
|
+
currentVerdict = /** @type {FinalVerdict} */ ('FRICTION');
|
|
490
|
+
verdictSource = 'selector_downgrade';
|
|
491
|
+
finalConfidence = 0.75;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ========================================================================
|
|
496
|
+
// PHASE 4: JOURNEY VERDICT MERGE (Can downgrade but not upgrade)
|
|
497
|
+
// ========================================================================
|
|
498
|
+
|
|
499
|
+
if (journeyVerdict) {
|
|
500
|
+
const canonicalJourney = toCanonicalVerdict(journeyVerdict);
|
|
501
|
+
const canonicalCurrent = toCanonicalVerdict(currentVerdict);
|
|
502
|
+
|
|
503
|
+
const rank = { READY: 0, FRICTION: 1, DO_NOT_LAUNCH: 2 };
|
|
504
|
+
|
|
505
|
+
// Journey can only downgrade (move to higher rank number)
|
|
506
|
+
if (rank[canonicalJourney] > rank[canonicalCurrent]) {
|
|
507
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
508
|
+
phase: 4,
|
|
509
|
+
source: VERDICT_SOURCE.JOURNEY_DOWNGRADE,
|
|
510
|
+
previousVerdict: currentVerdict,
|
|
511
|
+
suggestedVerdict: canonicalJourney,
|
|
512
|
+
reasonCode: 'JOURNEY_DOWNGRADE',
|
|
513
|
+
timestamp
|
|
514
|
+
}));
|
|
515
|
+
|
|
516
|
+
currentVerdict = /** @type {FinalVerdict} */ (canonicalJourney);
|
|
517
|
+
verdictSource = VERDICT_SOURCE.JOURNEY_DOWNGRADE;
|
|
518
|
+
reasons.push({
|
|
519
|
+
code: 'JOURNEY_DOWNGRADE',
|
|
520
|
+
message: `Journey verdict downgraded from ${canonicalCurrent} to ${canonicalJourney}`
|
|
521
|
+
});
|
|
522
|
+
finalConfidence = Math.min(finalConfidence, 0.85);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ========================================================================
|
|
527
|
+
// PHASE 4a: SECURITY & NETWORK SAFETY ENFORCEMENT
|
|
528
|
+
// ========================================================================
|
|
529
|
+
|
|
530
|
+
const httpWarnings = Array.isArray(networkSafety?.httpWarnings) ? networkSafety.httpWarnings : [];
|
|
531
|
+
const excessiveThirdParty = Boolean(networkSafety?.excessiveThirdParty);
|
|
532
|
+
const thirdPartyCount = networkSafety?.thirdPartyCount || 0;
|
|
533
|
+
const thirdPartyDomains = Array.isArray(networkSafety?.thirdPartyDomains) ? networkSafety.thirdPartyDomains : [];
|
|
534
|
+
|
|
535
|
+
if (httpWarnings.length > 0) {
|
|
536
|
+
reasons.push({
|
|
537
|
+
code: 'INSECURE_TRANSPORT',
|
|
538
|
+
message: `HTTP detected on ${httpWarnings.length} request(s): ${httpWarnings.slice(0, 3).join(', ')}`
|
|
539
|
+
});
|
|
540
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
541
|
+
phase: 4,
|
|
542
|
+
source: 'network_safety',
|
|
543
|
+
suggestedVerdict: currentVerdict === 'DO_NOT_LAUNCH' ? currentVerdict : 'FRICTION',
|
|
544
|
+
reasonCode: 'HTTP_WARNING',
|
|
545
|
+
timestamp
|
|
546
|
+
}));
|
|
547
|
+
if (currentVerdict === 'READY') {
|
|
548
|
+
currentVerdict = 'FRICTION';
|
|
549
|
+
verdictSource = 'network_safety';
|
|
550
|
+
finalConfidence = Math.min(finalConfidence, 0.7);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (excessiveThirdParty) {
|
|
555
|
+
reasons.push({
|
|
556
|
+
code: 'EXCESSIVE_THIRD_PARTY',
|
|
557
|
+
message: `Excessive third-party requests detected (${thirdPartyCount}). Domains: ${thirdPartyDomains.slice(0, 5).join(', ')}`
|
|
558
|
+
});
|
|
559
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
560
|
+
phase: 4,
|
|
561
|
+
source: 'network_safety',
|
|
562
|
+
suggestedVerdict: currentVerdict === 'DO_NOT_LAUNCH' ? currentVerdict : 'FRICTION',
|
|
563
|
+
reasonCode: 'EXCESSIVE_THIRD_PARTY',
|
|
564
|
+
timestamp
|
|
565
|
+
}));
|
|
566
|
+
if (currentVerdict === 'READY') {
|
|
567
|
+
currentVerdict = 'FRICTION';
|
|
568
|
+
verdictSource = 'network_safety';
|
|
569
|
+
finalConfidence = Math.min(finalConfidence, 0.7);
|
|
570
|
+
}
|
|
571
|
+
} else if (thirdPartyCount > 0) {
|
|
572
|
+
reasons.push({
|
|
573
|
+
code: 'THIRD_PARTY_REQUESTS',
|
|
574
|
+
message: `Observed ${thirdPartyCount} third-party request(s)`
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (Array.isArray(secretFindings) && secretFindings.length > 0) {
|
|
579
|
+
reasons.push({
|
|
580
|
+
code: 'MISSING_SECRETS',
|
|
581
|
+
message: `Required secrets missing: ${secretFindings.map(s => s.key).join(', ')}`
|
|
582
|
+
});
|
|
583
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
584
|
+
phase: 4,
|
|
585
|
+
source: 'secret_hygiene',
|
|
586
|
+
suggestedVerdict: currentVerdict === 'DO_NOT_LAUNCH' ? currentVerdict : 'FRICTION',
|
|
587
|
+
reasonCode: 'MISSING_SECRETS',
|
|
588
|
+
timestamp
|
|
589
|
+
}));
|
|
590
|
+
if (currentVerdict === 'READY') {
|
|
591
|
+
currentVerdict = 'FRICTION';
|
|
592
|
+
verdictSource = 'secret_hygiene';
|
|
593
|
+
finalConfidence = Math.min(finalConfidence, 0.7);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ========================================================================
|
|
598
|
+
// PHASE 5: BASELINE REGRESSION CHECK (Informational only, not verdict-changing)
|
|
599
|
+
// ========================================================================
|
|
600
|
+
|
|
601
|
+
const diff = baseline.diffResult || baseline.diff;
|
|
602
|
+
if (diff && diff.regressions && Object.keys(diff.regressions).length > 0) {
|
|
603
|
+
verdictHistory.push(/** @type {VerdictHistoryEntry} */ ({
|
|
604
|
+
phase: 5,
|
|
605
|
+
source: 'baseline',
|
|
606
|
+
regressionCount: Object.keys(diff.regressions).length,
|
|
607
|
+
reasonCode: 'BASELINE_REGRESSIONS_DETECTED',
|
|
608
|
+
timestamp
|
|
609
|
+
}));
|
|
610
|
+
|
|
611
|
+
reasons.push({
|
|
612
|
+
code: 'BASELINE_REGRESSIONS',
|
|
613
|
+
message: `Baseline regressions detected: ${Object.keys(diff.regressions).join(', ')}`
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ========================================================================
|
|
618
|
+
// FINALIZE
|
|
619
|
+
// ========================================================================
|
|
620
|
+
|
|
621
|
+
// Add NOT_APPLICABLE as informational (not verdict-affecting)
|
|
622
|
+
if (notApplicableFlows.length > 0) {
|
|
623
|
+
reasons.push({
|
|
624
|
+
code: 'NOT_APPLICABLE_FLOWS',
|
|
625
|
+
message: `${notApplicableFlows.length} flow(s) not applicable to this site`
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (notApplicableAttempts.length > 0) {
|
|
630
|
+
reasons.push({
|
|
631
|
+
code: 'NOT_APPLICABLE_ATTEMPTS',
|
|
632
|
+
message: `${notApplicableAttempts.length} attempt(s) not applicable to this site`
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Policy warnings (exitCode 2) are informational, not verdict-changing
|
|
637
|
+
if (policyEval && !policyEval.passed && policyEval.exitCode === 2) {
|
|
638
|
+
reasons.push({
|
|
639
|
+
code: 'POLICY_WARNING',
|
|
640
|
+
message: policyEval.summary || 'Policy warnings detected'
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return buildFinalDecision({
|
|
645
|
+
finalVerdict: currentVerdict,
|
|
646
|
+
verdictSource,
|
|
647
|
+
verdictHistory,
|
|
648
|
+
reasons,
|
|
649
|
+
confidence: finalConfidence,
|
|
650
|
+
exitCode: mapExitCodeFromCanonical(currentVerdict),
|
|
651
|
+
coverageInfo,
|
|
652
|
+
humanPath,
|
|
653
|
+
networkSafety,
|
|
654
|
+
secretFindings
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Build the final decision object with all required fields
|
|
660
|
+
* @param {Object} params - Decision parameters
|
|
661
|
+
* @param {FinalVerdict} params.finalVerdict - Final canonical verdict
|
|
662
|
+
* @param {VerdictSource} params.verdictSource - Verdict source
|
|
663
|
+
* @param {VerdictHistoryEntry[]} params.verdictHistory - Verdict history
|
|
664
|
+
* @param {DecisionReason[]} params.reasons - Decision reasons
|
|
665
|
+
* @param {number} params.confidence - Confidence score (0-1)
|
|
666
|
+
* @param {number} [params.exitCode] - Exit code (if not provided, derived from verdict)
|
|
667
|
+
* @param {Object} [params.coverageInfo] - Coverage information
|
|
668
|
+
* @param {Object|null} [params.humanPath] - Human navigation path
|
|
669
|
+
* @param {Object} [params.networkSafety] - Network safety signals
|
|
670
|
+
* @param {Object[]} [params.secretFindings] - Secret findings
|
|
671
|
+
* @returns {FinalDecision}
|
|
672
|
+
*/
|
|
673
|
+
function buildFinalDecision({
|
|
674
|
+
finalVerdict,
|
|
675
|
+
verdictSource,
|
|
676
|
+
verdictHistory,
|
|
677
|
+
reasons,
|
|
678
|
+
confidence,
|
|
679
|
+
exitCode,
|
|
680
|
+
coverageInfo,
|
|
681
|
+
humanPath,
|
|
682
|
+
networkSafety,
|
|
683
|
+
secretFindings
|
|
684
|
+
}) {
|
|
685
|
+
// Ensure deterministic reason ordering
|
|
686
|
+
const sortedReasons = (reasons || [])
|
|
687
|
+
.filter(r => r && r.code && r.message)
|
|
688
|
+
.sort((a, b) => a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
|
|
689
|
+
|
|
690
|
+
// Normalize verdict first
|
|
691
|
+
const normalizedVerdict = normalizeCanonicalVerdict(finalVerdict) || 'UNKNOWN';
|
|
692
|
+
|
|
693
|
+
// Calculate exit code from normalized verdict if not provided
|
|
694
|
+
const finalExitCode = exitCode !== undefined ? exitCode : mapExitCodeFromCanonical(normalizedVerdict);
|
|
695
|
+
|
|
696
|
+
return {
|
|
697
|
+
finalVerdict: normalizedVerdict,
|
|
698
|
+
verdictSource,
|
|
699
|
+
verdictHistory,
|
|
700
|
+
reasons: sortedReasons,
|
|
701
|
+
confidence: Math.max(0, Math.min(1, confidence || 0.5)),
|
|
702
|
+
exitCode: finalExitCode,
|
|
703
|
+
|
|
704
|
+
// For backwards compatibility
|
|
705
|
+
finalExitCode: finalExitCode,
|
|
706
|
+
|
|
707
|
+
// Coverage information for decision artifact
|
|
708
|
+
coverageInfo: coverageInfo || {},
|
|
709
|
+
|
|
710
|
+
// Human navigation path (Stage 3)
|
|
711
|
+
humanPath: humanPath || null,
|
|
712
|
+
|
|
713
|
+
// Security and hygiene signals
|
|
714
|
+
networkSafety: networkSafety || {},
|
|
715
|
+
secretFindings: secretFindings || []
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Enrich reasons array with additional context
|
|
721
|
+
*/
|
|
722
|
+
function enrichReasons(reasons, context) {
|
|
723
|
+
const { notApplicableFlows, notApplicableAttempts } = context;
|
|
724
|
+
|
|
725
|
+
if (notApplicableFlows && notApplicableFlows.length > 0) {
|
|
726
|
+
reasons.push({
|
|
727
|
+
code: 'NOT_APPLICABLE_FLOWS',
|
|
728
|
+
message: `${notApplicableFlows.length} flow(s) not applicable to this site`
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (notApplicableAttempts && notApplicableAttempts.length > 0) {
|
|
733
|
+
reasons.push({
|
|
734
|
+
code: 'NOT_APPLICABLE_ATTEMPTS',
|
|
735
|
+
message: `${notApplicableAttempts.length} attempt(s) not applicable to this site`
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return reasons;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
module.exports = {
|
|
743
|
+
computeDecisionAuthority,
|
|
744
|
+
VERDICT_SOURCE,
|
|
745
|
+
resetCallTracker // For testing: reset the call tracker
|
|
746
|
+
};
|