@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.
Files changed (217) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +15 -5
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +323 -111
  6. package/src/cli/commands/doctor.js +36 -4
  7. package/src/cli/commands/ga.js +243 -0
  8. package/src/cli/commands/gates.js +95 -0
  9. package/src/cli/commands/inspect.js +131 -2
  10. package/src/cli/commands/release-check.js +213 -0
  11. package/src/cli/commands/run.js +498 -103
  12. package/src/cli/commands/security-check.js +211 -0
  13. package/src/cli/commands/truth.js +114 -0
  14. package/src/cli/entry.js +305 -68
  15. package/src/cli/util/angular-component-extractor.js +179 -0
  16. package/src/cli/util/angular-navigation-detector.js +141 -0
  17. package/src/cli/util/angular-network-detector.js +161 -0
  18. package/src/cli/util/angular-state-detector.js +162 -0
  19. package/src/cli/util/ast-interactive-detector.js +546 -0
  20. package/src/cli/util/ast-network-detector.js +603 -0
  21. package/src/cli/util/ast-usestate-detector.js +602 -0
  22. package/src/cli/util/bootstrap-guard.js +86 -0
  23. package/src/cli/util/detection-engine.js +4 -3
  24. package/src/cli/util/determinism-runner.js +123 -0
  25. package/src/cli/util/determinism-writer.js +129 -0
  26. package/src/cli/util/env-url.js +4 -0
  27. package/src/cli/util/events.js +76 -0
  28. package/src/cli/util/expectation-extractor.js +380 -74
  29. package/src/cli/util/findings-writer.js +126 -15
  30. package/src/cli/util/learn-writer.js +3 -1
  31. package/src/cli/util/observation-engine.js +69 -23
  32. package/src/cli/util/observe-writer.js +3 -1
  33. package/src/cli/util/paths.js +6 -14
  34. package/src/cli/util/project-discovery.js +23 -0
  35. package/src/cli/util/project-writer.js +3 -1
  36. package/src/cli/util/redact.js +2 -2
  37. package/src/cli/util/run-resolver.js +64 -0
  38. package/src/cli/util/runtime-budget.js +147 -0
  39. package/src/cli/util/source-requirement.js +55 -0
  40. package/src/cli/util/summary-writer.js +13 -1
  41. package/src/cli/util/svelte-navigation-detector.js +163 -0
  42. package/src/cli/util/svelte-network-detector.js +80 -0
  43. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  44. package/src/cli/util/svelte-state-detector.js +243 -0
  45. package/src/cli/util/vue-navigation-detector.js +177 -0
  46. package/src/cli/util/vue-sfc-extractor.js +162 -0
  47. package/src/cli/util/vue-state-detector.js +215 -0
  48. package/src/types/global.d.ts +28 -0
  49. package/src/types/ts-ast.d.ts +24 -0
  50. package/src/verax/cli/doctor.js +2 -2
  51. package/src/verax/cli/finding-explainer.js +56 -3
  52. package/src/verax/cli/init.js +1 -1
  53. package/src/verax/cli/url-safety.js +12 -2
  54. package/src/verax/cli/wizard.js +13 -2
  55. package/src/verax/core/artifacts/registry.js +154 -0
  56. package/src/verax/core/artifacts/verifier.js +980 -0
  57. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  58. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  59. package/src/verax/core/budget-engine.js +1 -1
  60. package/src/verax/core/capabilities/gates.js +499 -0
  61. package/src/verax/core/capabilities/registry.js +475 -0
  62. package/src/verax/core/confidence/confidence-compute.js +137 -0
  63. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  64. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  65. package/src/verax/core/confidence/confidence-weights.js +44 -0
  66. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  67. package/src/verax/core/confidence/confidence.loader.js +79 -0
  68. package/src/verax/core/confidence/confidence.schema.js +94 -0
  69. package/src/verax/core/confidence-engine-refactor.js +484 -0
  70. package/src/verax/core/confidence-engine.js +486 -0
  71. package/src/verax/core/confidence-engine.js.backup +471 -0
  72. package/src/verax/core/contracts/index.js +29 -0
  73. package/src/verax/core/contracts/types.js +185 -0
  74. package/src/verax/core/contracts/validators.js +381 -0
  75. package/src/verax/core/decision-snapshot.js +31 -4
  76. package/src/verax/core/decisions/decision.trace.js +276 -0
  77. package/src/verax/core/determinism/contract-writer.js +89 -0
  78. package/src/verax/core/determinism/contract.js +139 -0
  79. package/src/verax/core/determinism/diff.js +364 -0
  80. package/src/verax/core/determinism/engine.js +221 -0
  81. package/src/verax/core/determinism/finding-identity.js +148 -0
  82. package/src/verax/core/determinism/normalize.js +438 -0
  83. package/src/verax/core/determinism/report-writer.js +92 -0
  84. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  85. package/src/verax/core/determinism-model.js +35 -6
  86. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  87. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  88. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  89. package/src/verax/core/evidence-builder.js +487 -0
  90. package/src/verax/core/execution-mode-context.js +77 -0
  91. package/src/verax/core/execution-mode-detector.js +190 -0
  92. package/src/verax/core/failures/exit-codes.js +86 -0
  93. package/src/verax/core/failures/failure-summary.js +76 -0
  94. package/src/verax/core/failures/failure.factory.js +225 -0
  95. package/src/verax/core/failures/failure.ledger.js +132 -0
  96. package/src/verax/core/failures/failure.types.js +196 -0
  97. package/src/verax/core/failures/index.js +10 -0
  98. package/src/verax/core/ga/ga-report-writer.js +43 -0
  99. package/src/verax/core/ga/ga.artifact.js +49 -0
  100. package/src/verax/core/ga/ga.contract.js +434 -0
  101. package/src/verax/core/ga/ga.enforcer.js +86 -0
  102. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  103. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  104. package/src/verax/core/guardrails/policy.loader.js +83 -0
  105. package/src/verax/core/guardrails/policy.schema.js +110 -0
  106. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  107. package/src/verax/core/guardrails-engine.js +505 -0
  108. package/src/verax/core/incremental-store.js +15 -7
  109. package/src/verax/core/observe/run-timeline.js +316 -0
  110. package/src/verax/core/perf/perf.contract.js +186 -0
  111. package/src/verax/core/perf/perf.display.js +65 -0
  112. package/src/verax/core/perf/perf.enforcer.js +91 -0
  113. package/src/verax/core/perf/perf.monitor.js +209 -0
  114. package/src/verax/core/perf/perf.report.js +198 -0
  115. package/src/verax/core/pipeline-tracker.js +238 -0
  116. package/src/verax/core/product-definition.js +127 -0
  117. package/src/verax/core/release/provenance.builder.js +271 -0
  118. package/src/verax/core/release/release-report-writer.js +40 -0
  119. package/src/verax/core/release/release.enforcer.js +159 -0
  120. package/src/verax/core/release/reproducibility.check.js +221 -0
  121. package/src/verax/core/release/sbom.builder.js +283 -0
  122. package/src/verax/core/replay-validator.js +4 -4
  123. package/src/verax/core/replay.js +1 -1
  124. package/src/verax/core/report/cross-index.js +192 -0
  125. package/src/verax/core/report/human-summary.js +222 -0
  126. package/src/verax/core/route-intelligence.js +419 -0
  127. package/src/verax/core/security/secrets.scan.js +326 -0
  128. package/src/verax/core/security/security-report.js +50 -0
  129. package/src/verax/core/security/security.enforcer.js +124 -0
  130. package/src/verax/core/security/supplychain.defaults.json +38 -0
  131. package/src/verax/core/security/supplychain.policy.js +326 -0
  132. package/src/verax/core/security/vuln.scan.js +265 -0
  133. package/src/verax/core/silence-impact.js +1 -1
  134. package/src/verax/core/silence-model.js +9 -7
  135. package/src/verax/core/truth/truth.certificate.js +250 -0
  136. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  137. package/src/verax/detect/comparison.js +8 -3
  138. package/src/verax/detect/confidence-engine.js +645 -57
  139. package/src/verax/detect/confidence-helper.js +33 -0
  140. package/src/verax/detect/detection-engine.js +19 -2
  141. package/src/verax/detect/dynamic-route-findings.js +335 -0
  142. package/src/verax/detect/evidence-index.js +15 -65
  143. package/src/verax/detect/expectation-chain-detector.js +417 -0
  144. package/src/verax/detect/expectation-model.js +56 -3
  145. package/src/verax/detect/explanation-helpers.js +1 -1
  146. package/src/verax/detect/finding-detector.js +2 -2
  147. package/src/verax/detect/findings-writer.js +149 -20
  148. package/src/verax/detect/flow-detector.js +4 -4
  149. package/src/verax/detect/index.js +265 -15
  150. package/src/verax/detect/interactive-findings.js +3 -4
  151. package/src/verax/detect/journey-stall-detector.js +558 -0
  152. package/src/verax/detect/route-findings.js +218 -0
  153. package/src/verax/detect/signal-mapper.js +2 -2
  154. package/src/verax/detect/skip-classifier.js +4 -4
  155. package/src/verax/detect/ui-feedback-findings.js +207 -0
  156. package/src/verax/detect/verdict-engine.js +61 -9
  157. package/src/verax/detect/view-switch-correlator.js +242 -0
  158. package/src/verax/flow/flow-engine.js +3 -2
  159. package/src/verax/flow/flow-spec.js +1 -2
  160. package/src/verax/index.js +413 -33
  161. package/src/verax/intel/effect-detector.js +1 -1
  162. package/src/verax/intel/index.js +2 -2
  163. package/src/verax/intel/route-extractor.js +3 -3
  164. package/src/verax/intel/vue-navigation-extractor.js +81 -18
  165. package/src/verax/intel/vue-router-extractor.js +4 -2
  166. package/src/verax/learn/action-contract-extractor.js +684 -66
  167. package/src/verax/learn/ast-contract-extractor.js +53 -1
  168. package/src/verax/learn/index.js +36 -2
  169. package/src/verax/learn/manifest-writer.js +28 -14
  170. package/src/verax/learn/route-extractor.js +1 -1
  171. package/src/verax/learn/route-validator.js +12 -8
  172. package/src/verax/learn/state-extractor.js +1 -1
  173. package/src/verax/learn/static-extractor-navigation.js +1 -1
  174. package/src/verax/learn/static-extractor-validation.js +2 -2
  175. package/src/verax/learn/static-extractor.js +8 -7
  176. package/src/verax/learn/ts-contract-resolver.js +14 -12
  177. package/src/verax/observe/browser.js +22 -3
  178. package/src/verax/observe/console-sensor.js +2 -2
  179. package/src/verax/observe/expectation-executor.js +2 -1
  180. package/src/verax/observe/focus-sensor.js +1 -1
  181. package/src/verax/observe/human-driver.js +29 -10
  182. package/src/verax/observe/index.js +92 -844
  183. package/src/verax/observe/interaction-discovery.js +27 -15
  184. package/src/verax/observe/interaction-runner.js +31 -14
  185. package/src/verax/observe/loading-sensor.js +6 -0
  186. package/src/verax/observe/navigation-sensor.js +1 -1
  187. package/src/verax/observe/observe-context.js +205 -0
  188. package/src/verax/observe/observe-helpers.js +191 -0
  189. package/src/verax/observe/observe-runner.js +226 -0
  190. package/src/verax/observe/observers/budget-observer.js +185 -0
  191. package/src/verax/observe/observers/console-observer.js +102 -0
  192. package/src/verax/observe/observers/coverage-observer.js +107 -0
  193. package/src/verax/observe/observers/interaction-observer.js +471 -0
  194. package/src/verax/observe/observers/navigation-observer.js +132 -0
  195. package/src/verax/observe/observers/network-observer.js +87 -0
  196. package/src/verax/observe/observers/safety-observer.js +82 -0
  197. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  198. package/src/verax/observe/settle.js +1 -0
  199. package/src/verax/observe/state-sensor.js +8 -4
  200. package/src/verax/observe/state-ui-sensor.js +7 -1
  201. package/src/verax/observe/traces-writer.js +27 -16
  202. package/src/verax/observe/ui-feedback-detector.js +742 -0
  203. package/src/verax/observe/ui-signal-sensor.js +155 -2
  204. package/src/verax/scan-summary-writer.js +46 -9
  205. package/src/verax/shared/artifact-manager.js +9 -6
  206. package/src/verax/shared/budget-profiles.js +2 -2
  207. package/src/verax/shared/caching.js +1 -1
  208. package/src/verax/shared/config-loader.js +1 -2
  209. package/src/verax/shared/css-spinner-rules.js +204 -0
  210. package/src/verax/shared/dynamic-route-utils.js +12 -6
  211. package/src/verax/shared/retry-policy.js +1 -6
  212. package/src/verax/shared/root-artifacts.js +1 -1
  213. package/src/verax/shared/view-switch-rules.js +208 -0
  214. package/src/verax/shared/zip-artifacts.js +1 -0
  215. package/src/verax/validate/context-validator.js +1 -1
  216. package/src/verax/observe/index.js.backup +0 -1
  217. 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;