@veraxhq/verax 0.2.0 → 0.3.0
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 +14 -18
- package/bin/verax.js +7 -0
- package/package.json +15 -5
- package/src/cli/commands/baseline.js +104 -0
- package/src/cli/commands/default.js +323 -111
- package/src/cli/commands/doctor.js +36 -4
- package/src/cli/commands/ga.js +243 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +131 -2
- package/src/cli/commands/release-check.js +213 -0
- package/src/cli/commands/run.js +498 -103
- package/src/cli/commands/security-check.js +211 -0
- package/src/cli/commands/truth.js +114 -0
- package/src/cli/entry.js +305 -68
- package/src/cli/util/angular-component-extractor.js +179 -0
- package/src/cli/util/angular-navigation-detector.js +141 -0
- package/src/cli/util/angular-network-detector.js +161 -0
- package/src/cli/util/angular-state-detector.js +162 -0
- package/src/cli/util/ast-interactive-detector.js +546 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/bootstrap-guard.js +86 -0
- package/src/cli/util/detection-engine.js +4 -3
- package/src/cli/util/determinism-runner.js +123 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/env-url.js +4 -0
- package/src/cli/util/events.js +76 -0
- package/src/cli/util/expectation-extractor.js +380 -74
- package/src/cli/util/findings-writer.js +126 -15
- package/src/cli/util/learn-writer.js +3 -1
- package/src/cli/util/observation-engine.js +69 -23
- package/src/cli/util/observe-writer.js +3 -1
- package/src/cli/util/paths.js +6 -14
- package/src/cli/util/project-discovery.js +23 -0
- package/src/cli/util/project-writer.js +3 -1
- package/src/cli/util/redact.js +2 -2
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +13 -1
- package/src/cli/util/svelte-navigation-detector.js +163 -0
- package/src/cli/util/svelte-network-detector.js +80 -0
- package/src/cli/util/svelte-sfc-extractor.js +147 -0
- package/src/cli/util/svelte-state-detector.js +243 -0
- package/src/cli/util/vue-navigation-detector.js +177 -0
- package/src/cli/util/vue-sfc-extractor.js +162 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/doctor.js +2 -2
- package/src/verax/cli/finding-explainer.js +56 -3
- package/src/verax/cli/init.js +1 -1
- package/src/verax/cli/url-safety.js +12 -2
- package/src/verax/cli/wizard.js +13 -2
- package/src/verax/core/artifacts/registry.js +154 -0
- package/src/verax/core/artifacts/verifier.js +980 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +231 -0
- package/src/verax/core/budget-engine.js +1 -1
- package/src/verax/core/capabilities/gates.js +499 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +137 -0
- package/src/verax/core/confidence/confidence-invariants.js +234 -0
- package/src/verax/core/confidence/confidence-report-writer.js +112 -0
- package/src/verax/core/confidence/confidence-weights.js +44 -0
- package/src/verax/core/confidence/confidence.defaults.js +65 -0
- package/src/verax/core/confidence/confidence.loader.js +79 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +484 -0
- package/src/verax/core/confidence-engine.js +486 -0
- package/src/verax/core/confidence-engine.js.backup +471 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +185 -0
- package/src/verax/core/contracts/validators.js +381 -0
- package/src/verax/core/decision-snapshot.js +31 -4
- package/src/verax/core/decisions/decision.trace.js +276 -0
- package/src/verax/core/determinism/contract-writer.js +89 -0
- package/src/verax/core/determinism/contract.js +139 -0
- package/src/verax/core/determinism/diff.js +364 -0
- package/src/verax/core/determinism/engine.js +221 -0
- package/src/verax/core/determinism/finding-identity.js +148 -0
- package/src/verax/core/determinism/normalize.js +438 -0
- package/src/verax/core/determinism/report-writer.js +92 -0
- package/src/verax/core/determinism/run-fingerprint.js +118 -0
- package/src/verax/core/determinism-model.js +35 -6
- package/src/verax/core/dynamic-route-intelligence.js +528 -0
- package/src/verax/core/evidence/evidence-capture-service.js +307 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
- package/src/verax/core/evidence-builder.js +487 -0
- package/src/verax/core/execution-mode-context.js +77 -0
- package/src/verax/core/execution-mode-detector.js +190 -0
- package/src/verax/core/failures/exit-codes.js +86 -0
- package/src/verax/core/failures/failure-summary.js +76 -0
- package/src/verax/core/failures/failure.factory.js +225 -0
- package/src/verax/core/failures/failure.ledger.js +132 -0
- package/src/verax/core/failures/failure.types.js +196 -0
- package/src/verax/core/failures/index.js +10 -0
- package/src/verax/core/ga/ga-report-writer.js +43 -0
- package/src/verax/core/ga/ga.artifact.js +49 -0
- package/src/verax/core/ga/ga.contract.js +434 -0
- package/src/verax/core/ga/ga.enforcer.js +86 -0
- package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
- package/src/verax/core/guardrails/policy.defaults.js +210 -0
- package/src/verax/core/guardrails/policy.loader.js +83 -0
- package/src/verax/core/guardrails/policy.schema.js +110 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
- package/src/verax/core/guardrails-engine.js +505 -0
- package/src/verax/core/incremental-store.js +15 -7
- package/src/verax/core/observe/run-timeline.js +316 -0
- package/src/verax/core/perf/perf.contract.js +186 -0
- package/src/verax/core/perf/perf.display.js +65 -0
- package/src/verax/core/perf/perf.enforcer.js +91 -0
- package/src/verax/core/perf/perf.monitor.js +209 -0
- package/src/verax/core/perf/perf.report.js +198 -0
- package/src/verax/core/pipeline-tracker.js +238 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +271 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +159 -0
- package/src/verax/core/release/reproducibility.check.js +221 -0
- package/src/verax/core/release/sbom.builder.js +283 -0
- package/src/verax/core/replay-validator.js +4 -4
- package/src/verax/core/replay.js +1 -1
- package/src/verax/core/report/cross-index.js +192 -0
- package/src/verax/core/report/human-summary.js +222 -0
- package/src/verax/core/route-intelligence.js +419 -0
- package/src/verax/core/security/secrets.scan.js +326 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +124 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +326 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/silence-impact.js +1 -1
- package/src/verax/core/silence-model.js +9 -7
- package/src/verax/core/truth/truth.certificate.js +250 -0
- package/src/verax/core/ui-feedback-intelligence.js +515 -0
- package/src/verax/detect/comparison.js +8 -3
- package/src/verax/detect/confidence-engine.js +645 -57
- package/src/verax/detect/confidence-helper.js +33 -0
- package/src/verax/detect/detection-engine.js +19 -2
- package/src/verax/detect/dynamic-route-findings.js +335 -0
- package/src/verax/detect/evidence-index.js +15 -65
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +56 -3
- package/src/verax/detect/explanation-helpers.js +1 -1
- package/src/verax/detect/finding-detector.js +2 -2
- package/src/verax/detect/findings-writer.js +149 -20
- package/src/verax/detect/flow-detector.js +4 -4
- package/src/verax/detect/index.js +265 -15
- package/src/verax/detect/interactive-findings.js +3 -4
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/route-findings.js +218 -0
- package/src/verax/detect/signal-mapper.js +2 -2
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/verdict-engine.js +61 -9
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +413 -33
- package/src/verax/intel/effect-detector.js +1 -1
- package/src/verax/intel/index.js +2 -2
- package/src/verax/intel/route-extractor.js +3 -3
- package/src/verax/intel/vue-navigation-extractor.js +81 -18
- package/src/verax/intel/vue-router-extractor.js +4 -2
- package/src/verax/learn/action-contract-extractor.js +684 -66
- package/src/verax/learn/ast-contract-extractor.js +53 -1
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +28 -14
- package/src/verax/learn/route-extractor.js +1 -1
- package/src/verax/learn/route-validator.js +12 -8
- package/src/verax/learn/state-extractor.js +1 -1
- package/src/verax/learn/static-extractor-navigation.js +1 -1
- package/src/verax/learn/static-extractor-validation.js +2 -2
- package/src/verax/learn/static-extractor.js +8 -7
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/browser.js +22 -3
- package/src/verax/observe/console-sensor.js +2 -2
- package/src/verax/observe/expectation-executor.js +2 -1
- package/src/verax/observe/focus-sensor.js +1 -1
- package/src/verax/observe/human-driver.js +29 -10
- package/src/verax/observe/index.js +92 -844
- package/src/verax/observe/interaction-discovery.js +27 -15
- package/src/verax/observe/interaction-runner.js +31 -14
- package/src/verax/observe/loading-sensor.js +6 -0
- package/src/verax/observe/navigation-sensor.js +1 -1
- package/src/verax/observe/observe-context.js +205 -0
- package/src/verax/observe/observe-helpers.js +191 -0
- package/src/verax/observe/observe-runner.js +226 -0
- package/src/verax/observe/observers/budget-observer.js +185 -0
- package/src/verax/observe/observers/console-observer.js +102 -0
- package/src/verax/observe/observers/coverage-observer.js +107 -0
- package/src/verax/observe/observers/interaction-observer.js +471 -0
- package/src/verax/observe/observers/navigation-observer.js +132 -0
- package/src/verax/observe/observers/network-observer.js +87 -0
- package/src/verax/observe/observers/safety-observer.js +82 -0
- package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
- package/src/verax/observe/settle.js +1 -0
- package/src/verax/observe/state-sensor.js +8 -4
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/traces-writer.js +27 -16
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/observe/ui-signal-sensor.js +155 -2
- package/src/verax/scan-summary-writer.js +46 -9
- package/src/verax/shared/artifact-manager.js +9 -6
- package/src/verax/shared/budget-profiles.js +2 -2
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/config-loader.js +1 -2
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/dynamic-route-utils.js +12 -6
- package/src/verax/shared/retry-policy.js +1 -6
- package/src/verax/shared/root-artifacts.js +1 -1
- package/src/verax/shared/view-switch-rules.js +208 -0
- package/src/verax/shared/zip-artifacts.js +1 -0
- package/src/verax/validate/context-validator.js +1 -1
- package/src/verax/observe/index.js.backup +0 -1
- package/src/verax/validate/context-validator.js.bak +0 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXPECTATION CONTINUITY & USER STALL DETECTION
|
|
3
|
+
*
|
|
4
|
+
* Detects silent failures that occur across multiple interactions where each step
|
|
5
|
+
* individually works, but the user journey stalls.
|
|
6
|
+
*
|
|
7
|
+
* Requirements:
|
|
8
|
+
* 1) Track interaction sequence context (previous → next)
|
|
9
|
+
* 2) After successful interaction, infer expected next signals:
|
|
10
|
+
* - navigation
|
|
11
|
+
* - new actionable elements (CTA)
|
|
12
|
+
* - content progression
|
|
13
|
+
* 3) Detect stall conditions:
|
|
14
|
+
* - no navigation
|
|
15
|
+
* - no new actionable UI
|
|
16
|
+
* - no meaningful DOM progression within timeout
|
|
17
|
+
* 4) Emit finding type: "journey-stall-silent-failure"
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Journey Stall Detector
|
|
22
|
+
*
|
|
23
|
+
* Analyzes sequences of traces to detect when interactions work individually
|
|
24
|
+
* but the overall user journey stalls.
|
|
25
|
+
*/
|
|
26
|
+
export class JourneyStallDetector {
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.stallThresholdMs = options.stallThresholdMs || 3000; // DOM should change within 3s
|
|
29
|
+
this.minSequenceLength = options.minSequenceLength || 2; // At least 2 interactions for a journey
|
|
30
|
+
this.maxSequenceLength = options.maxSequenceLength || 20; // But limit to 20 for performance
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Analyze traces for journey stalls
|
|
35
|
+
* @param {Array} traces - Interaction traces from observe phase
|
|
36
|
+
* @returns {Array} Journey stall findings
|
|
37
|
+
*/
|
|
38
|
+
detectStalls(traces) {
|
|
39
|
+
if (!Array.isArray(traces) || traces.length < this.minSequenceLength) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const findings = [];
|
|
44
|
+
const sequences = this._extractSequences(traces);
|
|
45
|
+
|
|
46
|
+
for (const sequence of sequences) {
|
|
47
|
+
const stall = this._analyzeSequence(sequence);
|
|
48
|
+
if (stall) {
|
|
49
|
+
findings.push(stall);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return findings;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract meaningful interaction sequences from traces
|
|
58
|
+
* @private
|
|
59
|
+
*/
|
|
60
|
+
_extractSequences(traces) {
|
|
61
|
+
const sequences = [];
|
|
62
|
+
let currentSequence = [];
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < traces.length; i++) {
|
|
65
|
+
const trace = traces[i];
|
|
66
|
+
|
|
67
|
+
// Skip traces without proper interaction data
|
|
68
|
+
if (!trace || !trace.interaction || !trace.sensors) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
currentSequence.push({
|
|
73
|
+
index: i,
|
|
74
|
+
trace,
|
|
75
|
+
beforeUrl: trace.before?.url || '',
|
|
76
|
+
afterUrl: trace.after?.url || '',
|
|
77
|
+
navigation: trace.sensors.navigation || {},
|
|
78
|
+
uiSignals: trace.sensors.uiSignals || {},
|
|
79
|
+
dom: trace.dom || {},
|
|
80
|
+
timing: trace.sensors.timing || {},
|
|
81
|
+
uiFeedback: trace.sensors.uiFeedback || {}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Sequence ends if:
|
|
85
|
+
// 1. We reached max length
|
|
86
|
+
// 2. Next trace shows successful navigation (end of journey segment)
|
|
87
|
+
// 3. We're at the end of traces
|
|
88
|
+
const isLastTrace = i === traces.length - 1;
|
|
89
|
+
const nextTrace = !isLastTrace ? traces[i + 1] : null;
|
|
90
|
+
const navigationOccurred = trace.sensors?.navigation?.urlChanged === true;
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
currentSequence.length >= this.maxSequenceLength ||
|
|
94
|
+
navigationOccurred ||
|
|
95
|
+
isLastTrace
|
|
96
|
+
) {
|
|
97
|
+
if (currentSequence.length >= this.minSequenceLength) {
|
|
98
|
+
sequences.push(currentSequence);
|
|
99
|
+
}
|
|
100
|
+
currentSequence = [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return sequences;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Analyze a single sequence for stalls
|
|
109
|
+
* @private
|
|
110
|
+
*/
|
|
111
|
+
_analyzeSequence(sequence) {
|
|
112
|
+
if (sequence.length < this.minSequenceLength) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// For each step (except the last), check if it progresses the journey
|
|
117
|
+
const stallPoints = [];
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < sequence.length - 1; i++) {
|
|
120
|
+
const current = sequence[i];
|
|
121
|
+
const next = sequence[i + 1];
|
|
122
|
+
|
|
123
|
+
const stall = this._detectStallBetweenSteps(current, next, i, sequence);
|
|
124
|
+
if (stall) {
|
|
125
|
+
stallPoints.push(stall);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (stallPoints.length === 0) {
|
|
130
|
+
return null; // No stalls detected
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Generate finding for this journey stall sequence
|
|
134
|
+
return this._generateStallFinding(sequence, stallPoints);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Detect if there's a stall between two consecutive steps
|
|
139
|
+
* @private
|
|
140
|
+
*/
|
|
141
|
+
_detectStallBetweenSteps(current, next, stepIndex, sequence) {
|
|
142
|
+
const reasons = [];
|
|
143
|
+
const evidence = [];
|
|
144
|
+
|
|
145
|
+
// Check 1: Was the current interaction successful?
|
|
146
|
+
const currentSuccessful = this._isInteractionSuccessful(current);
|
|
147
|
+
if (!currentSuccessful) {
|
|
148
|
+
return null; // Current step failed, not a stall
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check 2: Navigation expectation
|
|
152
|
+
const expectedNavigation = this._shouldExpectNavigation(current);
|
|
153
|
+
const actualNavigation = next.navigation?.urlChanged === true;
|
|
154
|
+
|
|
155
|
+
if (expectedNavigation && !actualNavigation) {
|
|
156
|
+
reasons.push('no_navigation');
|
|
157
|
+
evidence.push({
|
|
158
|
+
type: 'navigation_expectation_unmet',
|
|
159
|
+
currentInteraction: current.trace.interaction.type,
|
|
160
|
+
currentSelector: current.trace.interaction.selector,
|
|
161
|
+
expectedNavigation: expectedNavigation,
|
|
162
|
+
actualNavigation: actualNavigation,
|
|
163
|
+
beforeUrl: current.beforeUrl,
|
|
164
|
+
afterUrl: current.afterUrl,
|
|
165
|
+
nextUrl: next.beforeUrl
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check 3: New actionable UI expectation
|
|
170
|
+
const expectedNewActionableUI = this._shouldExpectNewActionableUI(current);
|
|
171
|
+
const foundNewActionableUI = this._hasNewActionableUI(current, next);
|
|
172
|
+
|
|
173
|
+
if (expectedNewActionableUI && !foundNewActionableUI) {
|
|
174
|
+
reasons.push('no_new_actionable_ui');
|
|
175
|
+
evidence.push({
|
|
176
|
+
type: 'actionable_ui_expectation_unmet',
|
|
177
|
+
currentInteraction: current.trace.interaction.type,
|
|
178
|
+
currentSelector: current.trace.interaction.selector,
|
|
179
|
+
expectedNewActions: expectedNewActionableUI,
|
|
180
|
+
foundNew: foundNewActionableUI,
|
|
181
|
+
currentUIActions: this._countActionableElements(current),
|
|
182
|
+
nextUIActions: this._countActionableElements(next)
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check 4: DOM progression expectation
|
|
187
|
+
const expectedDomProgression = this._shouldExpectDomProgression(current);
|
|
188
|
+
const actualDomProgression = this._hasMeaningfulDomProgression(current, next);
|
|
189
|
+
|
|
190
|
+
if (expectedDomProgression && !actualDomProgression) {
|
|
191
|
+
reasons.push('no_dom_progression');
|
|
192
|
+
evidence.push({
|
|
193
|
+
type: 'dom_progression_expectation_unmet',
|
|
194
|
+
currentInteraction: current.trace.interaction.type,
|
|
195
|
+
currentSelector: current.trace.interaction.selector,
|
|
196
|
+
expectedDomChange: expectedDomProgression,
|
|
197
|
+
domChanged: current.uiSignals?.diff?.changed === true,
|
|
198
|
+
domHash: {
|
|
199
|
+
before: current.dom?.beforeHash,
|
|
200
|
+
after: current.dom?.afterHash,
|
|
201
|
+
domChangedDuringSettle: current.dom?.settle?.domChangedDuringSettle
|
|
202
|
+
},
|
|
203
|
+
nextDomChanged: next.uiSignals?.diff?.changed === true
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Only return stall if we have reasons
|
|
208
|
+
if (reasons.length === 0) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
stepIndex,
|
|
214
|
+
currentInteractionIndex: current.index,
|
|
215
|
+
nextInteractionIndex: next.index,
|
|
216
|
+
reasons,
|
|
217
|
+
evidence,
|
|
218
|
+
severity: this._calculateSeverity(reasons, current, next)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Check if an interaction was successful
|
|
224
|
+
* @private
|
|
225
|
+
*/
|
|
226
|
+
_isInteractionSuccessful(step) {
|
|
227
|
+
const trace = step.trace;
|
|
228
|
+
|
|
229
|
+
// Check for policy violations
|
|
230
|
+
if (trace.policy) {
|
|
231
|
+
if (trace.policy.timeout) return false;
|
|
232
|
+
if (trace.policy.executionError) return false;
|
|
233
|
+
if (trace.policy.blocked) return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Should have sensors captured
|
|
237
|
+
return !!trace.sensors;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Determine if interaction should trigger navigation
|
|
242
|
+
* @private
|
|
243
|
+
*/
|
|
244
|
+
_shouldExpectNavigation(step) {
|
|
245
|
+
const trace = step.trace;
|
|
246
|
+
const interactionType = trace.interaction?.type;
|
|
247
|
+
const href = trace.interaction?.href;
|
|
248
|
+
const dataHref = trace.interaction?.dataHref;
|
|
249
|
+
const formAction = trace.interaction?.formAction;
|
|
250
|
+
|
|
251
|
+
// Links with href should navigate
|
|
252
|
+
if (interactionType === 'link' && (href || dataHref)) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Forms with action should navigate (unless AJAX)
|
|
257
|
+
if (interactionType === 'form' && formAction) {
|
|
258
|
+
// Check if AJAX detected
|
|
259
|
+
const networkSummary = step.trace.sensors?.network;
|
|
260
|
+
if (networkSummary && networkSummary.totalRequests > 0) {
|
|
261
|
+
// Could be AJAX, but still might navigate
|
|
262
|
+
// Conservative: expect navigation if form has action
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Buttons might trigger navigation if they have href (OR if href attribute indicates navigation)
|
|
268
|
+
// Any button might be a form submit or navigation trigger
|
|
269
|
+
if (interactionType === 'button') {
|
|
270
|
+
if (href || dataHref) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
// Check if button label suggests navigation (Next, Continue, Submit, etc.)
|
|
274
|
+
const label = trace.interaction?.label || '';
|
|
275
|
+
if (/next|continue|submit|go|proceed|forward/i.test(label)) {
|
|
276
|
+
// Likely expects navigation
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Determine if interaction should produce new actionable UI
|
|
286
|
+
* @private
|
|
287
|
+
*/
|
|
288
|
+
_shouldExpectNewActionableUI(step) {
|
|
289
|
+
const trace = step.trace;
|
|
290
|
+
const interactionType = trace.interaction?.type;
|
|
291
|
+
|
|
292
|
+
// Click/tap interactions often reveal new UI
|
|
293
|
+
if (interactionType === 'click' || interactionType === 'tap') {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Form submissions reveal new content
|
|
298
|
+
if (interactionType === 'form') {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Hover might reveal new UI (dropdowns, tooltips)
|
|
303
|
+
if (interactionType === 'hover') {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Check if new actionable UI appeared between steps
|
|
312
|
+
* @private
|
|
313
|
+
*/
|
|
314
|
+
_hasNewActionableUI(current, next) {
|
|
315
|
+
const currentActions = this._countActionableElements(current);
|
|
316
|
+
const nextActions = this._countActionableElements(next);
|
|
317
|
+
|
|
318
|
+
// New actionable UI if we have more clickable elements
|
|
319
|
+
if (nextActions > currentActions) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Or if UI feedback detected new interactive elements
|
|
324
|
+
const nextFeedback = next.uiFeedback;
|
|
325
|
+
if (nextFeedback?.signals?.notification?.detected === true) {
|
|
326
|
+
return true; // New notification appeared
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (nextFeedback?.signals?.buttonStateTransition?.detected === true) {
|
|
330
|
+
return true; // Button state changed (became enabled, etc.)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Count actionable elements from UI signals
|
|
338
|
+
* @private
|
|
339
|
+
*/
|
|
340
|
+
_countActionableElements(step) {
|
|
341
|
+
const uiSignals = step.uiSignals || {};
|
|
342
|
+
const after = uiSignals.after || {};
|
|
343
|
+
|
|
344
|
+
let count = 0;
|
|
345
|
+
|
|
346
|
+
// Count detected interactive elements
|
|
347
|
+
if (after.clickableCount) count += after.clickableCount;
|
|
348
|
+
if (after.formCount) count += after.formCount;
|
|
349
|
+
if (after.buttonCount) count += after.buttonCount;
|
|
350
|
+
if (after.linkCount) count += after.linkCount;
|
|
351
|
+
|
|
352
|
+
// If no explicit counts, estimate from diff
|
|
353
|
+
if (count === 0 && uiSignals.diff?.changed) {
|
|
354
|
+
count = 1; // At least something changed
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return count;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Determine if DOM progression is expected
|
|
362
|
+
* @private
|
|
363
|
+
*/
|
|
364
|
+
_shouldExpectDomProgression(step) {
|
|
365
|
+
const trace = step.trace;
|
|
366
|
+
const interactionType = trace.interaction?.type;
|
|
367
|
+
const uiFeedback = step.uiFeedback;
|
|
368
|
+
|
|
369
|
+
// Any interaction that changes visible state should progress DOM
|
|
370
|
+
if (
|
|
371
|
+
interactionType === 'click' ||
|
|
372
|
+
interactionType === 'tap' ||
|
|
373
|
+
interactionType === 'form'
|
|
374
|
+
) {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// If we detected loading indicators, expect DOM change after loading
|
|
379
|
+
if (uiFeedback?.signals?.loading?.detected === true) {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Check if meaningful DOM progression occurred
|
|
388
|
+
* @private
|
|
389
|
+
*/
|
|
390
|
+
_hasMeaningfulDomProgression(current, next) {
|
|
391
|
+
// Check if DOM hash changed (content changed)
|
|
392
|
+
const currentHash = current.dom?.afterHash;
|
|
393
|
+
const nextHash = next.dom?.beforeHash;
|
|
394
|
+
|
|
395
|
+
if (currentHash && nextHash && currentHash !== nextHash) {
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Check if UI signals indicate change
|
|
400
|
+
if (next.uiSignals?.diff?.changed === true) {
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Check if DOM settle detected changes
|
|
405
|
+
if (next.dom?.settle?.domChangedDuringSettle === true) {
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Check if DOM elements increased (more content)
|
|
410
|
+
const currentElements = current.uiSignals?.after?.domNodeCount || 0;
|
|
411
|
+
const nextElements = next.uiSignals?.before?.domNodeCount || 0;
|
|
412
|
+
|
|
413
|
+
if (nextElements > currentElements && nextElements > 100) {
|
|
414
|
+
// Significant increase in DOM elements
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// More lenient: if hashes exist and are different, DOM changed
|
|
419
|
+
if (current.dom?.beforeHash && current.dom?.afterHash && current.dom.beforeHash !== current.dom.afterHash) {
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Calculate severity level for stall
|
|
428
|
+
* @private
|
|
429
|
+
*/
|
|
430
|
+
_calculateSeverity(reasons, current, next) {
|
|
431
|
+
let score = 0;
|
|
432
|
+
|
|
433
|
+
// Multiple reasons = higher severity
|
|
434
|
+
score += reasons.length * 0.3;
|
|
435
|
+
|
|
436
|
+
// Navigation expected but missing is very serious
|
|
437
|
+
if (reasons.includes('no_navigation')) {
|
|
438
|
+
score += 0.4;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// No new UI is concerning
|
|
442
|
+
if (reasons.includes('no_new_actionable_ui')) {
|
|
443
|
+
score += 0.3;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// DOM stagnation is concerning
|
|
447
|
+
if (reasons.includes('no_dom_progression')) {
|
|
448
|
+
score += 0.3;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (score >= 0.7) return 'CRITICAL';
|
|
452
|
+
if (score >= 0.5) return 'HIGH';
|
|
453
|
+
if (score >= 0.3) return 'MEDIUM';
|
|
454
|
+
return 'LOW';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Generate a finding for this journey stall
|
|
459
|
+
* @private
|
|
460
|
+
*/
|
|
461
|
+
_generateStallFinding(sequence, stallPoints) {
|
|
462
|
+
const firstTrace = sequence[0].trace;
|
|
463
|
+
const lastTrace = sequence[sequence.length - 1].trace;
|
|
464
|
+
|
|
465
|
+
const id = `journey-stall-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
id,
|
|
469
|
+
type: 'journey-stall-silent-failure',
|
|
470
|
+
severity: this._getHighestSeverity(stallPoints),
|
|
471
|
+
sequenceLength: sequence.length,
|
|
472
|
+
stallPoints,
|
|
473
|
+
sequence: {
|
|
474
|
+
startedAt: sequence[0].beforeUrl,
|
|
475
|
+
endedAt: lastTrace.after?.url || sequence[sequence.length - 1].beforeUrl,
|
|
476
|
+
interactions: sequence.map((s, idx) => ({
|
|
477
|
+
index: idx,
|
|
478
|
+
type: s.trace.interaction.type,
|
|
479
|
+
selector: s.trace.interaction.selector,
|
|
480
|
+
label: s.trace.interaction.label
|
|
481
|
+
}))
|
|
482
|
+
},
|
|
483
|
+
summary: this._generateSummary(stallPoints, sequence),
|
|
484
|
+
evidence: {
|
|
485
|
+
stallPoints: stallPoints.map(sp => ({
|
|
486
|
+
stepIndex: sp.stepIndex,
|
|
487
|
+
reasons: sp.reasons,
|
|
488
|
+
severity: sp.severity,
|
|
489
|
+
details: sp.evidence
|
|
490
|
+
})),
|
|
491
|
+
journeyContext: {
|
|
492
|
+
totalInteractions: sequence.length,
|
|
493
|
+
startUrl: sequence[0].beforeUrl,
|
|
494
|
+
finalUrl: lastTrace.after?.url || sequence[sequence.length - 1].beforeUrl,
|
|
495
|
+
urlProgression: this._extractUrlProgression(sequence)
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
expectedOutcome: 'user_should_progress_through_journey',
|
|
499
|
+
actualOutcome: 'journey_stalls_despite_successful_individual_steps',
|
|
500
|
+
confidence: null, // Will be calculated by confidence engine
|
|
501
|
+
impact: 'HIGH' // Journey stalls have high user impact
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Get highest severity from stall points
|
|
507
|
+
* @private
|
|
508
|
+
*/
|
|
509
|
+
_getHighestSeverity(stallPoints) {
|
|
510
|
+
const levels = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
|
|
511
|
+
const severities = stallPoints.map(sp => levels[sp.severity] || 0);
|
|
512
|
+
const max = Math.max(...severities);
|
|
513
|
+
|
|
514
|
+
for (const [level, value] of Object.entries(levels)) {
|
|
515
|
+
if (value === max) return level;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return 'MEDIUM';
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Generate human-readable summary
|
|
523
|
+
* @private
|
|
524
|
+
*/
|
|
525
|
+
_generateSummary(stallPoints, sequence) {
|
|
526
|
+
const steps = sequence.length;
|
|
527
|
+
const failureTypes = new Set();
|
|
528
|
+
|
|
529
|
+
stallPoints.forEach(sp => {
|
|
530
|
+
sp.reasons.forEach(reason => failureTypes.add(reason));
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const typeList = Array.from(failureTypes).join(', ');
|
|
534
|
+
return `User journey stalled after ${steps} successful interactions: ${typeList}`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Extract URL progression from sequence
|
|
539
|
+
* @private
|
|
540
|
+
*/
|
|
541
|
+
_extractUrlProgression(sequence) {
|
|
542
|
+
const progression = [];
|
|
543
|
+
let lastUrl = null;
|
|
544
|
+
|
|
545
|
+
for (const step of sequence) {
|
|
546
|
+
const url = step.afterUrl || step.beforeUrl;
|
|
547
|
+
|
|
548
|
+
if (url && url !== lastUrl) {
|
|
549
|
+
progression.push(url);
|
|
550
|
+
lastUrl = url;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return progression;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export default JourneyStallDetector;
|