@veraxhq/verax 0.1.0 → 0.2.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/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +24 -36
- package/src/cli/commands/default.js +681 -0
- package/src/cli/commands/doctor.js +197 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +586 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +297 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +110 -0
- package/src/cli/util/expectation-extractor.js +388 -0
- package/src/cli/util/findings-writer.js +32 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +412 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +30 -0
- package/src/cli/util/project-discovery.js +297 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +43 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +111 -0
- package/src/verax/cli/wizard.js +109 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +432 -0
- package/src/verax/core/incremental-store.js +245 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +523 -0
- package/src/verax/detect/comparison.js +7 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +127 -0
- package/src/verax/detect/expectation-model.js +241 -168
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +41 -12
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +200 -288
- package/src/verax/detect/interactive-findings.js +612 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +561 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +103 -15
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +642 -0
- package/src/verax/intel/vue-router-extractor.js +325 -0
- package/src/verax/learn/action-contract-extractor.js +338 -104
- package/src/verax/learn/ast-contract-extractor.js +148 -6
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +122 -58
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +28 -97
- package/src/verax/learn/route-validator.js +8 -7
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +119 -10
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +30 -6
- package/src/verax/observe/console-sensor.js +2 -18
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +513 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +660 -273
- package/src/verax/observe/index.js +910 -26
- package/src/verax/observe/interaction-discovery.js +378 -15
- package/src/verax/observe/interaction-runner.js +562 -197
- package/src/verax/observe/loading-sensor.js +145 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +38 -17
- package/src/verax/observe/state-sensor.js +393 -0
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +73 -21
- package/src/verax/observe/ui-signal-sensor.js +143 -17
- package/src/verax/scan-summary-writer.js +80 -15
- package/src/verax/shared/artifact-manager.js +111 -9
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +169 -0
- package/src/verax/shared/dynamic-route-utils.js +224 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +9 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +66 -0
- package/src/verax/validate/context-validator.js +244 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CANONICAL OUTCOME TAXONOMY
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for all outcome classifications in VERAX.
|
|
5
|
+
* Every interaction outcome, silence, gap, and finding must map to exactly ONE of these types.
|
|
6
|
+
*
|
|
7
|
+
* NO free-form or implicit outcome types allowed.
|
|
8
|
+
* NO semantic drift between outputs.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Canonical Outcome Types - Mutually Exclusive Categories
|
|
13
|
+
*/
|
|
14
|
+
export const CANONICAL_OUTCOMES = {
|
|
15
|
+
// Code/user interaction broke (promise unmet, expectation failed)
|
|
16
|
+
SILENT_FAILURE: 'SILENT_FAILURE',
|
|
17
|
+
|
|
18
|
+
// Evaluation was not completed due to time/interaction/data limits (not a failure, just incomplete coverage)
|
|
19
|
+
COVERAGE_GAP: 'COVERAGE_GAP',
|
|
20
|
+
|
|
21
|
+
// Interaction executed but outcome cannot be asserted (ambiguity: SPA fallback, no DOM change signal, missing sensor)
|
|
22
|
+
UNPROVEN_INTERACTION: 'UNPROVEN_INTERACTION',
|
|
23
|
+
|
|
24
|
+
// Intentionally prevented (destructive action, external navigation, safety policy)
|
|
25
|
+
SAFETY_BLOCK: 'SAFETY_BLOCK',
|
|
26
|
+
|
|
27
|
+
// No problem detected; for informational artifacts (e.g., expectations without matching interactions)
|
|
28
|
+
INFORMATIONAL: 'INFORMATIONAL'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Outcome Definitions - For documentation and validation
|
|
33
|
+
*/
|
|
34
|
+
export const OUTCOME_DEFINITIONS = {
|
|
35
|
+
SILENT_FAILURE: {
|
|
36
|
+
title: 'Silent Failure',
|
|
37
|
+
description: 'User action executed, code ran, but expected observable change did not occur and no error was presented to user',
|
|
38
|
+
example: 'User clicks "Save" → page loads without spinning wheel → data is not saved but user receives no error message',
|
|
39
|
+
implication: 'Gap between user expectation and actual system behavior'
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
COVERAGE_GAP: {
|
|
43
|
+
title: 'Coverage Gap',
|
|
44
|
+
description: 'Interaction or expectation discovered but not evaluated due to scan budget limit (time, page count, interaction count)',
|
|
45
|
+
example: '500 pages discovered but scan budget allows only 100 pages → 400 pages not evaluated',
|
|
46
|
+
implication: 'Incomplete scan; behaviors on skipped interactions are unknown'
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
UNPROVEN_INTERACTION: {
|
|
50
|
+
title: 'Unproven Interaction',
|
|
51
|
+
description: 'Interaction executed but outcome ambiguous: no expectation matched, or observable change is ambiguous, or expectation-driven path failed',
|
|
52
|
+
example: 'User clicks SPA link → no observable change (SPA routing ambiguous) → cannot assert if routing worked or if feature is broken',
|
|
53
|
+
implication: 'Behavior is unknown; cannot determine if code matches reality'
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
SAFETY_BLOCK: {
|
|
57
|
+
title: 'Safety Block',
|
|
58
|
+
description: 'Interaction prevented intentionally: destructive text, external navigation, or safety policy blocks execution',
|
|
59
|
+
example: 'User clicks "Delete Account" → blocked by safety policy → behavior untested',
|
|
60
|
+
implication: 'Intentional limitation; not a failure, just a boundary'
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
INFORMATIONAL: {
|
|
64
|
+
title: 'Informational',
|
|
65
|
+
description: 'Metadata or artifact without direct outcome status (e.g., expectation defined but no matching interaction found)',
|
|
66
|
+
example: 'Manifest defines logout expectation → but no logout interaction discovered on site',
|
|
67
|
+
implication: 'Context for interpretation but not a test failure or success'
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Map silence reason codes to canonical outcomes
|
|
73
|
+
*
|
|
74
|
+
* Used by SilenceTracker and verdict-engine to classify silence entries
|
|
75
|
+
* with explicit outcome types.
|
|
76
|
+
*/
|
|
77
|
+
export function mapSilenceReasonToOutcome(reason) {
|
|
78
|
+
// Timeouts, failures → SILENT_FAILURE (promise unmet)
|
|
79
|
+
if (reason.includes('timeout') || reason.includes('failed') || reason.includes('error')) {
|
|
80
|
+
return CANONICAL_OUTCOMES.SILENT_FAILURE;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Budget/limit exceeded → COVERAGE_GAP (incomplete scan)
|
|
84
|
+
if (reason.includes('budget') || reason.includes('limit') || reason.includes('exceeded')) {
|
|
85
|
+
return CANONICAL_OUTCOMES.COVERAGE_GAP;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Safety/destructive/external → SAFETY_BLOCK (intentional)
|
|
89
|
+
if (reason.includes('destructive') || reason.includes('external') || reason.includes('unsafe') || reason.includes('safety')) {
|
|
90
|
+
return CANONICAL_OUTCOMES.SAFETY_BLOCK;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Incremental reuse → COVERAGE_GAP (unchanged, so gap in new scan)
|
|
94
|
+
if (reason.includes('incremental')) {
|
|
95
|
+
return CANONICAL_OUTCOMES.COVERAGE_GAP;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// No expectation found → INFORMATIONAL (metadata, not a test result)
|
|
99
|
+
if (reason.includes('no_expectation')) {
|
|
100
|
+
return CANONICAL_OUTCOMES.INFORMATIONAL;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Discovery error (selector mismatch, link not found) → COVERAGE_GAP (could not evaluate)
|
|
104
|
+
if (reason.includes('discovery') || reason.includes('no_matching_selector')) {
|
|
105
|
+
return CANONICAL_OUTCOMES.COVERAGE_GAP;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Sensor unavailable → COVERAGE_GAP (incomplete data)
|
|
109
|
+
if (reason.includes('sensor')) {
|
|
110
|
+
return CANONICAL_OUTCOMES.COVERAGE_GAP;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Default: treat as coverage gap if unknown
|
|
114
|
+
return CANONICAL_OUTCOMES.COVERAGE_GAP;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Map finding type to canonical outcome
|
|
119
|
+
*
|
|
120
|
+
* Used by finding-detector.js to classify findings explicitly
|
|
121
|
+
*/
|
|
122
|
+
export function mapFindingTypeToOutcome(findingType) {
|
|
123
|
+
// All forms of silent failure → SILENT_FAILURE
|
|
124
|
+
if (findingType.includes('silent_failure') ||
|
|
125
|
+
findingType.includes('unobserved') ||
|
|
126
|
+
findingType.includes('observed_break')) {
|
|
127
|
+
return CANONICAL_OUTCOMES.SILENT_FAILURE;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Default to SILENT_FAILURE for findings (they represent observed problems)
|
|
131
|
+
return CANONICAL_OUTCOMES.SILENT_FAILURE;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Validate that an outcome is canonical
|
|
136
|
+
*/
|
|
137
|
+
export function isValidOutcome(outcome) {
|
|
138
|
+
return Object.values(CANONICAL_OUTCOMES).includes(outcome);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Format outcome for human display
|
|
143
|
+
*/
|
|
144
|
+
export function formatOutcomeForDisplay(outcome) {
|
|
145
|
+
if (!isValidOutcome(outcome)) {
|
|
146
|
+
throw new Error(`Invalid outcome type: ${outcome}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const definition = OUTCOME_DEFINITIONS[outcome];
|
|
150
|
+
return {
|
|
151
|
+
type: outcome,
|
|
152
|
+
title: definition.title,
|
|
153
|
+
description: definition.description
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default CANONICAL_OUTCOMES;
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 7 — DECISION SNAPSHOT
|
|
3
|
+
*
|
|
4
|
+
* Computes a top-level decision snapshot that answers 6 mandatory questions:
|
|
5
|
+
*
|
|
6
|
+
* 1. Do we have confirmed SILENT FAILURES?
|
|
7
|
+
* 2. Where exactly are they?
|
|
8
|
+
* 3. How severe are they (user-impact-wise)?
|
|
9
|
+
* 4. What did VERAX NOT verify?
|
|
10
|
+
* 5. Why was it not verified?
|
|
11
|
+
* 6. How much confidence do we have in the findings?
|
|
12
|
+
*
|
|
13
|
+
* Rules:
|
|
14
|
+
* - Derived ONLY from existing data (findings, silences, coverage)
|
|
15
|
+
* - No new detection logic
|
|
16
|
+
* - No subjective language
|
|
17
|
+
* - No recommendations or advice
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Severity levels (user-impact-based, not technical)
|
|
22
|
+
*/
|
|
23
|
+
export const SEVERITY = {
|
|
24
|
+
CRITICAL_USER_BLOCKER: 'critical_user_blocker', // Prevents user from completing core task
|
|
25
|
+
FLOW_BREAKING: 'flow_breaking', // Breaks expected navigation/state flow
|
|
26
|
+
DEGRADING: 'degrading', // Reduces functionality but doesn't block
|
|
27
|
+
INFORMATIONAL: 'informational' // Observable but no clear user impact
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Classify finding severity based on promise type and outcome
|
|
32
|
+
* @param {Object} finding - Finding object
|
|
33
|
+
* @returns {string} - Severity level from SEVERITY enum
|
|
34
|
+
*/
|
|
35
|
+
function classifyFindingSeverity(finding) {
|
|
36
|
+
const promiseType = finding.promiseType || finding.type;
|
|
37
|
+
const outcome = finding.outcome;
|
|
38
|
+
|
|
39
|
+
// Navigation promises that fail are flow-breaking
|
|
40
|
+
if (promiseType === 'navigation' && outcome === 'broken') {
|
|
41
|
+
return SEVERITY.FLOW_BREAKING;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Network actions that fail are critical (forms, submissions)
|
|
45
|
+
if (promiseType === 'networkAction' && outcome === 'broken') {
|
|
46
|
+
return SEVERITY.CRITICAL_USER_BLOCKER;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// State actions that fail are degrading
|
|
50
|
+
if (promiseType === 'stateAction' && outcome === 'broken') {
|
|
51
|
+
return SEVERITY.DEGRADING;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Auth failures are critical user blockers
|
|
55
|
+
if (promiseType === 'authentication' && outcome === 'broken') {
|
|
56
|
+
return SEVERITY.CRITICAL_USER_BLOCKER;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Timeout/unknown outcomes are degrading (cannot confirm impact)
|
|
60
|
+
if (outcome === 'timeout' || outcome === 'unknown') {
|
|
61
|
+
return SEVERITY.DEGRADING;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Default to informational if unclear
|
|
65
|
+
return SEVERITY.INFORMATIONAL;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compute confidence score (0.0 - 1.0) based on silences and coverage
|
|
70
|
+
* @param {Object} detectTruth - Detect phase truth
|
|
71
|
+
* @param {Object} observeTruth - Observe phase truth
|
|
72
|
+
* @returns {Object} - Confidence assessment
|
|
73
|
+
*/
|
|
74
|
+
function computeConfidence(detectTruth, observeTruth) {
|
|
75
|
+
const totalInteractions = observeTruth?.interactionsObserved || 0;
|
|
76
|
+
const analyzed = detectTruth?.interactionsAnalyzed || 0;
|
|
77
|
+
const skipped = detectTruth?.skips?.total || 0;
|
|
78
|
+
const timeouts = observeTruth?.timeoutsCount || 0;
|
|
79
|
+
const coverageGaps = detectTruth?.coverageGapsCount || 0;
|
|
80
|
+
|
|
81
|
+
// Coverage ratio: how much was actually verified
|
|
82
|
+
const coverageRatio = totalInteractions > 0 ? analyzed / totalInteractions : 0;
|
|
83
|
+
|
|
84
|
+
// Silence penalty: reduce confidence for unknown outcomes
|
|
85
|
+
const silencePenalty = (skipped + timeouts + coverageGaps) / Math.max(totalInteractions, 1);
|
|
86
|
+
|
|
87
|
+
// Base confidence from coverage
|
|
88
|
+
let confidenceScore = coverageRatio;
|
|
89
|
+
|
|
90
|
+
// Apply silence penalty (max 50% reduction)
|
|
91
|
+
confidenceScore = confidenceScore * (1 - Math.min(silencePenalty * 0.5, 0.5));
|
|
92
|
+
|
|
93
|
+
// Clamp to 0-1 range
|
|
94
|
+
confidenceScore = Math.max(0, Math.min(1, confidenceScore));
|
|
95
|
+
|
|
96
|
+
// Classify confidence level
|
|
97
|
+
let confidenceLevel = 'very_low';
|
|
98
|
+
if (confidenceScore >= 0.9) {
|
|
99
|
+
confidenceLevel = 'high';
|
|
100
|
+
} else if (confidenceScore >= 0.7) {
|
|
101
|
+
confidenceLevel = 'medium';
|
|
102
|
+
} else if (confidenceScore >= 0.5) {
|
|
103
|
+
confidenceLevel = 'low';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
score: confidenceScore,
|
|
108
|
+
level: confidenceLevel,
|
|
109
|
+
coverageRatio,
|
|
110
|
+
silencePenalty,
|
|
111
|
+
factors: {
|
|
112
|
+
totalInteractions,
|
|
113
|
+
analyzed,
|
|
114
|
+
skipped,
|
|
115
|
+
timeouts,
|
|
116
|
+
coverageGaps
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Extract unverified items with reasons
|
|
123
|
+
* @param {Object} detectTruth - Detect phase truth
|
|
124
|
+
* @param {Object} observeTruth - Observe phase truth
|
|
125
|
+
* @returns {Array} - List of unverified items with reasons
|
|
126
|
+
*/
|
|
127
|
+
function extractUnverified(detectTruth, observeTruth) {
|
|
128
|
+
const unverified = [];
|
|
129
|
+
|
|
130
|
+
// Coverage gaps (expectations not evaluated)
|
|
131
|
+
const coverageGaps = detectTruth?.coverageGapsCount || 0;
|
|
132
|
+
if (coverageGaps > 0) {
|
|
133
|
+
unverified.push({
|
|
134
|
+
category: 'expectations',
|
|
135
|
+
count: coverageGaps,
|
|
136
|
+
reason: 'budget_exceeded'
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Skipped interactions by reason
|
|
141
|
+
const skips = detectTruth?.skips?.reasons || [];
|
|
142
|
+
for (const skipReason of skips) {
|
|
143
|
+
unverified.push({
|
|
144
|
+
category: 'interactions',
|
|
145
|
+
count: skipReason.count,
|
|
146
|
+
reason: skipReason.code
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Timeouts
|
|
151
|
+
const timeouts = observeTruth?.timeoutsCount || 0;
|
|
152
|
+
if (timeouts > 0) {
|
|
153
|
+
unverified.push({
|
|
154
|
+
category: 'interactions',
|
|
155
|
+
count: timeouts,
|
|
156
|
+
reason: 'timeout'
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// External navigations blocked
|
|
161
|
+
const externalBlocked = observeTruth?.externalNavigationBlockedCount || 0;
|
|
162
|
+
if (externalBlocked > 0) {
|
|
163
|
+
unverified.push({
|
|
164
|
+
category: 'navigations',
|
|
165
|
+
count: externalBlocked,
|
|
166
|
+
reason: 'external_blocked'
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return unverified;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Compute decision snapshot from scan results
|
|
175
|
+
* @param {Array} findings - Array of findings
|
|
176
|
+
* @param {Object} detectTruth - Detect phase truth
|
|
177
|
+
* @param {Object} observeTruth - Observe phase truth
|
|
178
|
+
* @param {Object} _silences - Silence data (unused parameter, kept for API compatibility)
|
|
179
|
+
* @returns {Object} - Decision snapshot answering 6 mandatory questions
|
|
180
|
+
*/
|
|
181
|
+
export function computeDecisionSnapshot(findings, detectTruth, observeTruth, _silences) {
|
|
182
|
+
// Question 1: Do we have confirmed SILENT FAILURES?
|
|
183
|
+
const confirmedFailures = findings.filter(f =>
|
|
184
|
+
f.outcome === 'broken' || f.type === 'silent_failure'
|
|
185
|
+
);
|
|
186
|
+
const hasConfirmedFailures = confirmedFailures.length > 0;
|
|
187
|
+
|
|
188
|
+
// Question 2: Where exactly are they?
|
|
189
|
+
const failureLocations = confirmedFailures.map(f => ({
|
|
190
|
+
type: f.promiseType || f.type,
|
|
191
|
+
fromPath: f.fromPath,
|
|
192
|
+
toPath: f.toPath,
|
|
193
|
+
selector: f.interaction?.selector,
|
|
194
|
+
description: f.description || f.reason
|
|
195
|
+
}));
|
|
196
|
+
|
|
197
|
+
// Question 3: How severe are they (user-impact-wise)?
|
|
198
|
+
const severityCounts = {
|
|
199
|
+
[SEVERITY.CRITICAL_USER_BLOCKER]: 0,
|
|
200
|
+
[SEVERITY.FLOW_BREAKING]: 0,
|
|
201
|
+
[SEVERITY.DEGRADING]: 0,
|
|
202
|
+
[SEVERITY.INFORMATIONAL]: 0
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const failuresBySeverity = confirmedFailures.map(f => {
|
|
206
|
+
const severity = classifyFindingSeverity(f);
|
|
207
|
+
severityCounts[severity]++;
|
|
208
|
+
return {
|
|
209
|
+
severity,
|
|
210
|
+
finding: f
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Question 4: What did VERAX NOT verify?
|
|
215
|
+
const unverified = extractUnverified(detectTruth, observeTruth);
|
|
216
|
+
const totalUnverified = unverified.reduce((sum, u) => sum + u.count, 0);
|
|
217
|
+
|
|
218
|
+
// Question 5: Why was it not verified?
|
|
219
|
+
const unverifiedReasons = {};
|
|
220
|
+
for (const item of unverified) {
|
|
221
|
+
if (!unverifiedReasons[item.reason]) {
|
|
222
|
+
unverifiedReasons[item.reason] = 0;
|
|
223
|
+
}
|
|
224
|
+
unverifiedReasons[item.reason] += item.count;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Question 6: How much confidence do we have in the findings?
|
|
228
|
+
const confidence = computeConfidence(detectTruth, observeTruth);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
// Question 1
|
|
232
|
+
hasConfirmedFailures,
|
|
233
|
+
confirmedFailureCount: confirmedFailures.length,
|
|
234
|
+
|
|
235
|
+
// Question 2
|
|
236
|
+
failureLocations,
|
|
237
|
+
|
|
238
|
+
// Question 3
|
|
239
|
+
severityCounts,
|
|
240
|
+
failuresBySeverity: failuresBySeverity.map(f => ({
|
|
241
|
+
severity: f.severity,
|
|
242
|
+
type: f.finding.promiseType || f.finding.type,
|
|
243
|
+
description: f.finding.description || f.finding.reason,
|
|
244
|
+
fromPath: f.finding.fromPath
|
|
245
|
+
})),
|
|
246
|
+
|
|
247
|
+
// Question 4
|
|
248
|
+
totalUnverified,
|
|
249
|
+
unverifiedByCategory: unverified.reduce((acc, u) => {
|
|
250
|
+
if (!acc[u.category]) {
|
|
251
|
+
acc[u.category] = 0;
|
|
252
|
+
}
|
|
253
|
+
acc[u.category] += u.count;
|
|
254
|
+
return acc;
|
|
255
|
+
}, {}),
|
|
256
|
+
|
|
257
|
+
// Question 5
|
|
258
|
+
unverifiedReasons,
|
|
259
|
+
unverifiedDetails: unverified,
|
|
260
|
+
|
|
261
|
+
// Question 6
|
|
262
|
+
confidence: {
|
|
263
|
+
level: confidence.level,
|
|
264
|
+
score: confidence.score,
|
|
265
|
+
coverageRatio: confidence.coverageRatio,
|
|
266
|
+
factors: confidence.factors
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Format decision snapshot for human reading
|
|
273
|
+
* @param {Object} snapshot - Decision snapshot
|
|
274
|
+
* @returns {string} - Formatted text
|
|
275
|
+
*/
|
|
276
|
+
export function formatDecisionSnapshot(snapshot) {
|
|
277
|
+
const lines = [];
|
|
278
|
+
|
|
279
|
+
lines.push('=== DECISION SNAPSHOT ===');
|
|
280
|
+
lines.push('');
|
|
281
|
+
|
|
282
|
+
// Question 1: Confirmed failures?
|
|
283
|
+
lines.push(`1. CONFIRMED SILENT FAILURES: ${snapshot.hasConfirmedFailures ? 'YES' : 'NO'}`);
|
|
284
|
+
lines.push(` Count: ${snapshot.confirmedFailureCount}`);
|
|
285
|
+
lines.push('');
|
|
286
|
+
|
|
287
|
+
// Question 2: Where?
|
|
288
|
+
if (snapshot.failureLocations.length > 0) {
|
|
289
|
+
lines.push('2. FAILURE LOCATIONS:');
|
|
290
|
+
for (const loc of snapshot.failureLocations.slice(0, 5)) {
|
|
291
|
+
lines.push(` - ${loc.type}: ${loc.fromPath} → ${loc.toPath || 'unknown'}`);
|
|
292
|
+
if (loc.selector) {
|
|
293
|
+
lines.push(` Selector: ${loc.selector}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (snapshot.failureLocations.length > 5) {
|
|
297
|
+
lines.push(` ... and ${snapshot.failureLocations.length - 5} more`);
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
lines.push('2. FAILURE LOCATIONS: None');
|
|
301
|
+
}
|
|
302
|
+
lines.push('');
|
|
303
|
+
|
|
304
|
+
// Question 3: How severe?
|
|
305
|
+
lines.push('3. SEVERITY BREAKDOWN:');
|
|
306
|
+
lines.push(` Critical user blockers: ${snapshot.severityCounts.critical_user_blocker}`);
|
|
307
|
+
lines.push(` Flow-breaking issues: ${snapshot.severityCounts.flow_breaking}`);
|
|
308
|
+
lines.push(` Degrading issues: ${snapshot.severityCounts.degrading}`);
|
|
309
|
+
lines.push(` Informational: ${snapshot.severityCounts.informational}`);
|
|
310
|
+
lines.push('');
|
|
311
|
+
|
|
312
|
+
// Question 4: What NOT verified?
|
|
313
|
+
lines.push('4. NOT VERIFIED:');
|
|
314
|
+
lines.push(` Total: ${snapshot.totalUnverified}`);
|
|
315
|
+
for (const [category, count] of Object.entries(snapshot.unverifiedByCategory)) {
|
|
316
|
+
lines.push(` - ${category}: ${count}`);
|
|
317
|
+
}
|
|
318
|
+
lines.push('');
|
|
319
|
+
|
|
320
|
+
// Question 5: Why not verified?
|
|
321
|
+
lines.push('5. REASONS NOT VERIFIED:');
|
|
322
|
+
for (const [reason, count] of Object.entries(snapshot.unverifiedReasons)) {
|
|
323
|
+
lines.push(` - ${reason}: ${count}`);
|
|
324
|
+
}
|
|
325
|
+
lines.push('');
|
|
326
|
+
|
|
327
|
+
// Question 6: Confidence?
|
|
328
|
+
lines.push('6. CONFIDENCE IN FINDINGS:');
|
|
329
|
+
lines.push(` Level: ${snapshot.confidence.level.toUpperCase()}`);
|
|
330
|
+
lines.push(` Score: ${(snapshot.confidence.score * 100).toFixed(1)}%`);
|
|
331
|
+
lines.push(` Coverage: ${(snapshot.confidence.coverageRatio * 100).toFixed(1)}% of interactions verified`);
|
|
332
|
+
lines.push('');
|
|
333
|
+
|
|
334
|
+
return lines.join('\n');
|
|
335
|
+
}
|