@veraxhq/verax 0.1.0 → 0.2.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 (126) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +14 -36
  4. package/src/cli/commands/default.js +523 -0
  5. package/src/cli/commands/doctor.js +165 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +402 -0
  8. package/src/cli/entry.js +196 -0
  9. package/src/cli/util/atomic-write.js +37 -0
  10. package/src/cli/util/detection-engine.js +296 -0
  11. package/src/cli/util/env-url.js +33 -0
  12. package/src/cli/util/errors.js +44 -0
  13. package/src/cli/util/events.js +34 -0
  14. package/src/cli/util/expectation-extractor.js +378 -0
  15. package/src/cli/util/findings-writer.js +31 -0
  16. package/src/cli/util/idgen.js +87 -0
  17. package/src/cli/util/learn-writer.js +39 -0
  18. package/src/cli/util/observation-engine.js +366 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +29 -0
  21. package/src/cli/util/project-discovery.js +277 -0
  22. package/src/cli/util/project-writer.js +26 -0
  23. package/src/cli/util/redact.js +128 -0
  24. package/src/cli/util/run-id.js +30 -0
  25. package/src/cli/util/summary-writer.js +32 -0
  26. package/src/verax/cli/ci-summary.js +35 -0
  27. package/src/verax/cli/context-explanation.js +89 -0
  28. package/src/verax/cli/doctor.js +277 -0
  29. package/src/verax/cli/error-normalizer.js +154 -0
  30. package/src/verax/cli/explain-output.js +105 -0
  31. package/src/verax/cli/finding-explainer.js +130 -0
  32. package/src/verax/cli/init.js +237 -0
  33. package/src/verax/cli/run-overview.js +163 -0
  34. package/src/verax/cli/url-safety.js +101 -0
  35. package/src/verax/cli/wizard.js +98 -0
  36. package/src/verax/cli/zero-findings-explainer.js +57 -0
  37. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  38. package/src/verax/core/action-classifier.js +86 -0
  39. package/src/verax/core/budget-engine.js +218 -0
  40. package/src/verax/core/canonical-outcomes.js +157 -0
  41. package/src/verax/core/decision-snapshot.js +335 -0
  42. package/src/verax/core/determinism-model.js +403 -0
  43. package/src/verax/core/incremental-store.js +237 -0
  44. package/src/verax/core/invariants.js +356 -0
  45. package/src/verax/core/promise-model.js +230 -0
  46. package/src/verax/core/replay-validator.js +350 -0
  47. package/src/verax/core/replay.js +222 -0
  48. package/src/verax/core/run-id.js +175 -0
  49. package/src/verax/core/run-manifest.js +99 -0
  50. package/src/verax/core/silence-impact.js +369 -0
  51. package/src/verax/core/silence-model.js +521 -0
  52. package/src/verax/detect/comparison.js +2 -34
  53. package/src/verax/detect/confidence-engine.js +764 -329
  54. package/src/verax/detect/detection-engine.js +293 -0
  55. package/src/verax/detect/evidence-index.js +177 -0
  56. package/src/verax/detect/expectation-model.js +194 -172
  57. package/src/verax/detect/explanation-helpers.js +187 -0
  58. package/src/verax/detect/finding-detector.js +450 -0
  59. package/src/verax/detect/findings-writer.js +44 -8
  60. package/src/verax/detect/flow-detector.js +366 -0
  61. package/src/verax/detect/index.js +172 -286
  62. package/src/verax/detect/interactive-findings.js +613 -0
  63. package/src/verax/detect/signal-mapper.js +308 -0
  64. package/src/verax/detect/verdict-engine.js +563 -0
  65. package/src/verax/evidence-index-writer.js +61 -0
  66. package/src/verax/index.js +90 -14
  67. package/src/verax/intel/effect-detector.js +368 -0
  68. package/src/verax/intel/handler-mapper.js +249 -0
  69. package/src/verax/intel/index.js +281 -0
  70. package/src/verax/intel/route-extractor.js +280 -0
  71. package/src/verax/intel/ts-program.js +256 -0
  72. package/src/verax/intel/vue-navigation-extractor.js +579 -0
  73. package/src/verax/intel/vue-router-extractor.js +323 -0
  74. package/src/verax/learn/action-contract-extractor.js +335 -101
  75. package/src/verax/learn/ast-contract-extractor.js +95 -5
  76. package/src/verax/learn/flow-extractor.js +172 -0
  77. package/src/verax/learn/manifest-writer.js +97 -47
  78. package/src/verax/learn/project-detector.js +40 -0
  79. package/src/verax/learn/route-extractor.js +27 -96
  80. package/src/verax/learn/state-extractor.js +212 -0
  81. package/src/verax/learn/static-extractor-navigation.js +114 -0
  82. package/src/verax/learn/static-extractor-validation.js +88 -0
  83. package/src/verax/learn/static-extractor.js +112 -4
  84. package/src/verax/learn/truth-assessor.js +24 -21
  85. package/src/verax/observe/aria-sensor.js +211 -0
  86. package/src/verax/observe/browser.js +10 -5
  87. package/src/verax/observe/console-sensor.js +1 -17
  88. package/src/verax/observe/domain-boundary.js +10 -1
  89. package/src/verax/observe/expectation-executor.js +512 -0
  90. package/src/verax/observe/flow-matcher.js +143 -0
  91. package/src/verax/observe/focus-sensor.js +196 -0
  92. package/src/verax/observe/human-driver.js +643 -275
  93. package/src/verax/observe/index.js +908 -27
  94. package/src/verax/observe/index.js.backup +1 -0
  95. package/src/verax/observe/interaction-discovery.js +365 -14
  96. package/src/verax/observe/interaction-runner.js +563 -198
  97. package/src/verax/observe/loading-sensor.js +139 -0
  98. package/src/verax/observe/navigation-sensor.js +255 -0
  99. package/src/verax/observe/network-sensor.js +55 -7
  100. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  101. package/src/verax/observe/observed-expectation.js +305 -0
  102. package/src/verax/observe/page-frontier.js +234 -0
  103. package/src/verax/observe/settle.js +37 -17
  104. package/src/verax/observe/state-sensor.js +389 -0
  105. package/src/verax/observe/timing-sensor.js +228 -0
  106. package/src/verax/observe/traces-writer.js +61 -20
  107. package/src/verax/observe/ui-signal-sensor.js +136 -17
  108. package/src/verax/scan-summary-writer.js +77 -15
  109. package/src/verax/shared/artifact-manager.js +110 -8
  110. package/src/verax/shared/budget-profiles.js +136 -0
  111. package/src/verax/shared/ci-detection.js +39 -0
  112. package/src/verax/shared/config-loader.js +170 -0
  113. package/src/verax/shared/dynamic-route-utils.js +218 -0
  114. package/src/verax/shared/expectation-coverage.js +44 -0
  115. package/src/verax/shared/expectation-prover.js +81 -0
  116. package/src/verax/shared/expectation-tracker.js +201 -0
  117. package/src/verax/shared/expectations-writer.js +60 -0
  118. package/src/verax/shared/first-run.js +44 -0
  119. package/src/verax/shared/progress-reporter.js +171 -0
  120. package/src/verax/shared/retry-policy.js +14 -1
  121. package/src/verax/shared/root-artifacts.js +49 -0
  122. package/src/verax/shared/scan-budget.js +86 -0
  123. package/src/verax/shared/url-normalizer.js +162 -0
  124. package/src/verax/shared/zip-artifacts.js +65 -0
  125. package/src/verax/validate/context-validator.js +244 -0
  126. package/src/verax/validate/context-validator.js.bak +0 -0
@@ -0,0 +1,366 @@
1
+ /**
2
+ * FLOW DETECTION MODULE
3
+ *
4
+ * Detects multi-step flow failures - when a sequence of interactions
5
+ * in a flow should work together but one step fails silently.
6
+ *
7
+ * FLOW INTELLIGENCE v1:
8
+ * - Groups traces by flowId
9
+ * - Validates each flow has 2+ proven expectations
10
+ * - Detects silent failures in flow steps
11
+ * - Checks for recovery in subsequent steps
12
+ * - Emits flow_silent_failure findings when appropriate
13
+ * - Suppresses per-step findings when flow finding is emitted
14
+ */
15
+
16
+ import { getUrlPath } from './evidence-validator.js';
17
+ import {
18
+ hasMeaningfulUrlChange,
19
+ hasVisibleChange,
20
+ hasDomChange
21
+ } from './comparison.js';
22
+ import {
23
+ expectsNavigation,
24
+ matchExpectation
25
+ } from './expectation-model.js';
26
+ import { isProvenExpectation } from '../shared/expectation-prover.js';
27
+ import { computeConfidence } from './confidence-engine.js';
28
+ import { enrichFindingWithExplanations } from './finding-detector.js';
29
+
30
+ /**
31
+ * Detect flow-level silent failures in multi-step flows.
32
+ *
33
+ * @param {Array} traces - Observation traces from observe phase
34
+ * @param {Object} manifest - Expectation manifest with staticExpectations
35
+ * @param {Array} findings - Array of findings (will be mutated to suppress per-step findings)
36
+ * @param {Array} coverageGaps - Coverage gaps data
37
+ * @param {Object} helpers - Helper functions { enrichFindingWithDecisionSignals, computeConfidence }
38
+ * @returns {Array} Array of flow_silent_failure findings
39
+ */
40
+ export function detectFlowSilentFailures(traces, manifest, findings, coverageGaps, helpers = {}) {
41
+ const flowFindings = [];
42
+ const projectDir = manifest.projectDir;
43
+ const { enrichFindingWithDecisionSignals } = helpers;
44
+
45
+ /**
46
+ * Compute confidence for a flow finding.
47
+ */
48
+ function computeFlowFindingConfidence(flowFinding, matchedExpectation, trace, comparisons) {
49
+ const sensors = trace.sensors || {};
50
+ const findingType = 'flow_silent_failure';
51
+
52
+ const confidence = computeConfidence({
53
+ findingType,
54
+ expectation: matchedExpectation || {},
55
+ sensors: {
56
+ network: sensors.network || {},
57
+ console: sensors.console || {},
58
+ uiSignals: sensors.uiSignals || {}
59
+ },
60
+ comparisons: {
61
+ hasUrlChange: comparisons.hasUrlChange || false,
62
+ hasDomChange: comparisons.hasDomChange || false,
63
+ hasVisibleChange: comparisons.hasVisibleChange || false
64
+ },
65
+ attemptMeta: {}
66
+ });
67
+
68
+ return confidence;
69
+ }
70
+
71
+ // Group traces by flowId
72
+ const flowsByFlowId = {};
73
+ let tracesWithFlowId = 0;
74
+ for (const trace of traces) {
75
+ const flowId = trace.flow?.flowId;
76
+ if (!flowId) continue; // Flow tracking not available for this trace
77
+ tracesWithFlowId++;
78
+
79
+ if (!flowsByFlowId[flowId]) {
80
+ flowsByFlowId[flowId] = [];
81
+ }
82
+ flowsByFlowId[flowId].push(trace);
83
+ }
84
+
85
+ // FLOW INTELLIGENCE v1: Only process if we have traces with flowIds
86
+ if (tracesWithFlowId === 0) {
87
+ return flowFindings; // No flow tracking available
88
+ }
89
+
90
+ // Detect flow silent failures
91
+ for (const flowId in flowsByFlowId) {
92
+ const flowTraces = flowsByFlowId[flowId];
93
+
94
+ // Flow must have at least 2 steps
95
+ if (flowTraces.length < 2) {
96
+ continue;
97
+ }
98
+
99
+ // FLOW INTELLIGENCE v1: Check if flow has PROVEN expectations (at least 2 steps with PROVEN expectations)
100
+ let stepCountWithProvenExpectation = 0;
101
+ for (const trace of flowTraces) {
102
+ const interaction = trace.interaction;
103
+ const beforeUrl = trace.before.url;
104
+
105
+ // Check for any PROVEN expectation (navigation, network, state)
106
+ let hasProvenExpectation = false;
107
+
108
+ // Check navigation
109
+ if (expectsNavigation(manifest, interaction, beforeUrl)) {
110
+ hasProvenExpectation = true;
111
+ }
112
+
113
+ // Check network
114
+ if (!hasProvenExpectation && manifest.staticExpectations) {
115
+ const networkExp = manifest.staticExpectations.find(e =>
116
+ e.type === 'network_action' &&
117
+ isProvenExpectation(e) &&
118
+ matchExpectation(e, interaction, beforeUrl)
119
+ );
120
+ if (networkExp) {
121
+ hasProvenExpectation = true;
122
+ }
123
+ }
124
+
125
+ if (hasProvenExpectation) {
126
+ stepCountWithProvenExpectation++;
127
+ }
128
+ }
129
+
130
+ // Only process flows with 2+ PROVEN steps
131
+ if (stepCountWithProvenExpectation < 2) {
132
+ continue;
133
+ }
134
+
135
+ // Look for silent failures followed by lack of recovery
136
+ let hasSilentFailure = false;
137
+ let failedStepIndex = -1;
138
+ let failedExpectation = null;
139
+
140
+ for (let i = 0; i < flowTraces.length; i++) {
141
+ const trace = flowTraces[i];
142
+ const interaction = trace.interaction;
143
+ const beforeUrl = trace.before.url;
144
+ const afterUrl = trace.after.url;
145
+ const beforeScreenshot = trace.before.screenshot;
146
+ const afterScreenshot = trace.after.screenshot;
147
+
148
+ // Check for PROVEN expectations at this step
149
+ let matchedExpectation = null;
150
+ let expectationType = null;
151
+ let isSilentFailure = false;
152
+
153
+ // Check navigation expectation
154
+ if (expectsNavigation(manifest, interaction, beforeUrl)) {
155
+ const navExp = manifest.staticExpectations?.find(e =>
156
+ e.type === 'navigation' &&
157
+ isProvenExpectation(e) &&
158
+ e.fromPath && getUrlPath(beforeUrl) &&
159
+ e.fromPath.replace(/\/$/, '') === getUrlPath(beforeUrl).replace(/\/$/, '') &&
160
+ matchExpectation(e, interaction, beforeUrl)
161
+ );
162
+
163
+ if (navExp) {
164
+ matchedExpectation = navExp;
165
+ expectationType = 'navigation';
166
+
167
+ const afterUrlPath = getUrlPath(afterUrl) || '/';
168
+ // Normalize paths: remove .html extension and trailing slashes for comparison
169
+ const normalizePath = (path) => {
170
+ if (!path) return '/';
171
+ let normalized = path.toLowerCase().trim().replace(/\/$/, '') || '/';
172
+ // Remove .html extension for comparison
173
+ normalized = normalized.replace(/\.html$/, '');
174
+ return normalized;
175
+ };
176
+ const normalizedAfter = normalizePath(afterUrlPath);
177
+ const normalizedTarget = normalizePath(navExp.targetPath || '');
178
+
179
+ const urlMatchesTarget = normalizedAfter === normalizedTarget;
180
+ const hasVisibleChangeResult = beforeScreenshot && afterScreenshot ?
181
+ hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir) : false;
182
+ const hasDomChangeResult = hasDomChange(trace);
183
+
184
+ const hasEffect = urlMatchesTarget || hasVisibleChangeResult || hasDomChangeResult;
185
+ const uiSignals = trace.sensors?.uiSignals || {};
186
+ const uiAfter = uiSignals.after || {};
187
+ const hasUIFeedback = uiAfter.uiFeedbackDetected === true || uiAfter.changed === true;
188
+
189
+ // Navigation silent failure: expected navigation didn't occur and no UI feedback
190
+ // BUT: in flow context, if navigation fails but we have subsequent steps, don't mark as silent failure yet
191
+ // Let flow detection handle it
192
+ if (!hasEffect && !hasUIFeedback) {
193
+ isSilentFailure = true;
194
+ } else {
195
+ // Navigation succeeded - clear any silent failure flag
196
+ isSilentFailure = false;
197
+ }
198
+ }
199
+ }
200
+
201
+ // Check network expectation (check even if navigation was found, as different steps may have different types)
202
+ if (!matchedExpectation && manifest.staticExpectations) {
203
+ const networkExp = manifest.staticExpectations.find(e =>
204
+ e.type === 'network_action' &&
205
+ isProvenExpectation(e) &&
206
+ matchExpectation(e, interaction, beforeUrl)
207
+ );
208
+
209
+ if (networkExp) {
210
+ matchedExpectation = networkExp;
211
+ expectationType = 'network_action';
212
+
213
+ const networkData = trace.sensors?.network || { totalRequests: 0, failedRequests: 0 };
214
+ const uiSignals = trace.sensors?.uiSignals || {};
215
+ const uiAfter = uiSignals.after || {};
216
+ const hasUIFeedback = uiAfter.uiFeedbackDetected === true || uiAfter.changed === true;
217
+
218
+ // Silent failure: network request failed OR missing with no UI feedback
219
+ if ((networkData.failedRequests > 0 || networkData.totalRequests === 0) && !hasUIFeedback) {
220
+ isSilentFailure = true;
221
+ }
222
+ }
223
+ }
224
+
225
+ // Also check network expectation even if navigation was matched (for step 2 of flow)
226
+ if (matchedExpectation && matchedExpectation.type === 'navigation' && manifest.staticExpectations) {
227
+ const networkExp = manifest.staticExpectations.find(e =>
228
+ e.type === 'network_action' &&
229
+ isProvenExpectation(e) &&
230
+ matchExpectation(e, interaction, beforeUrl)
231
+ );
232
+
233
+ if (networkExp) {
234
+ // This step has both navigation and network expectations - check network failure
235
+ const networkData = trace.sensors?.network || { totalRequests: 0, failedRequests: 0 };
236
+ const uiSignals = trace.sensors?.uiSignals || {};
237
+ const uiAfter = uiSignals.after || {};
238
+ const hasUIFeedback = uiAfter.uiFeedbackDetected === true || uiAfter.changed === true;
239
+
240
+ // If network fails silently, this is the failure we want to detect
241
+ if ((networkData.failedRequests > 0 || networkData.totalRequests === 0) && !hasUIFeedback) {
242
+ matchedExpectation = networkExp;
243
+ expectationType = 'network_action';
244
+ isSilentFailure = true;
245
+ }
246
+ }
247
+ }
248
+
249
+ if (isSilentFailure && matchedExpectation) {
250
+ // Silent failure detected at this step
251
+ hasSilentFailure = true;
252
+ failedStepIndex = i;
253
+ failedExpectation = matchedExpectation;
254
+
255
+ // Check if subsequent steps show UI recovery
256
+ let hasSubsequentRecovery = false;
257
+ if (i + 1 < flowTraces.length) {
258
+ const nextTrace = flowTraces[i + 1];
259
+ const nextBeforeScreenshot = nextTrace.before.screenshot;
260
+ const nextAfterScreenshot = nextTrace.after.screenshot;
261
+ const nextUISignals = nextTrace.sensors?.uiSignals || {};
262
+ const nextUIAfter = nextUISignals.after || {};
263
+
264
+ if (nextBeforeScreenshot && nextAfterScreenshot) {
265
+ hasSubsequentRecovery = hasVisibleChange(nextBeforeScreenshot, nextAfterScreenshot, projectDir) ||
266
+ nextUIAfter.uiFeedbackDetected === true;
267
+ }
268
+ }
269
+
270
+ // Emit flow_silent_failure only if no recovery in next step
271
+ if (!hasSubsequentRecovery) {
272
+ const hasUrlChange = hasMeaningfulUrlChange(beforeUrl, afterUrl);
273
+ const hasVisibleChangeResult = beforeScreenshot && afterScreenshot ?
274
+ hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir) : false;
275
+ const hasDomChangeResult = hasDomChange(trace);
276
+
277
+ // Build prior steps summary
278
+ const priorSteps = [];
279
+ for (let j = 0; j < i; j++) {
280
+ priorSteps.push({
281
+ stepIndex: j,
282
+ interaction: flowTraces[j].interaction,
283
+ url: flowTraces[j].after.url
284
+ });
285
+ }
286
+
287
+ const flowFinding = {
288
+ type: 'flow_silent_failure',
289
+ flowId: flowId,
290
+ failedStepIndex: failedStepIndex,
291
+ priorStepsCount: failedStepIndex,
292
+ priorSteps: priorSteps,
293
+ interaction: {
294
+ type: interaction.type,
295
+ selector: interaction.selector,
296
+ label: interaction.label
297
+ },
298
+ reason: 'Silent failure in multi-step flow with no recovery in subsequent step',
299
+ evidence: {
300
+ before: beforeScreenshot,
301
+ after: afterScreenshot,
302
+ beforeUrl: beforeUrl,
303
+ afterUrl: afterUrl,
304
+ failedExpectation: {
305
+ type: expectationType,
306
+ sourceRef: matchedExpectation.sourceRef,
307
+ handlerRef: matchedExpectation.handlerRef
308
+ }
309
+ }
310
+ };
311
+
312
+ flowFinding.confidence = computeFlowFindingConfidence(
313
+ flowFinding,
314
+ matchedExpectation,
315
+ trace,
316
+ { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult }
317
+ );
318
+
319
+ enrichFindingWithExplanations(flowFinding, trace);
320
+ if (enrichFindingWithDecisionSignals) {
321
+ enrichFindingWithDecisionSignals(flowFinding, trace);
322
+ }
323
+ flowFindings.push(flowFinding);
324
+
325
+ // Suppress per-step findings when flow finding is emitted
326
+ // Remove any findings for this step AND any previous steps in the flow from the main findings array
327
+ // Match by interaction details and also by finding type (network_silent_failure, navigation_silent_failure, etc.)
328
+ const findingTypesToSuppress = ['network_silent_failure', 'navigation_silent_failure', 'missing_network_action', 'no_effect_silent_failure', 'silent_failure'];
329
+
330
+ // Suppress findings for ALL steps in this flow (not just the failed step)
331
+ for (let stepIdx = 0; stepIdx <= i; stepIdx++) {
332
+ const stepTrace = flowTraces[stepIdx];
333
+ const stepInteraction = stepTrace.interaction;
334
+ const stepBeforeUrl = stepTrace.before.url;
335
+ const stepAfterUrl = stepTrace.after.url;
336
+
337
+ for (let k = findings.length - 1; k >= 0; k--) {
338
+ const finding = findings[k];
339
+ if (finding.interaction &&
340
+ findingTypesToSuppress.includes(finding.type)) {
341
+ // Match by selector, label, or by URL context for this step
342
+ const matchesSelector = finding.interaction.selector === stepInteraction.selector;
343
+ const matchesLabel = finding.interaction.label === stepInteraction.label;
344
+ const matchesUrl = (finding.evidence?.beforeUrl === stepBeforeUrl) ||
345
+ (finding.evidence?.afterUrl === stepAfterUrl) ||
346
+ (finding.evidence?.beforeUrl === stepTrace.before.url) ||
347
+ (finding.evidence?.afterUrl === stepTrace.after.url);
348
+
349
+ if (matchesSelector || matchesLabel || matchesUrl) {
350
+ findings.splice(k, 1);
351
+ // Note: findingsFromProven is in outer scope, but we can't access it here
352
+ // The caller will handle the adjustment
353
+ }
354
+ }
355
+ }
356
+ }
357
+ }
358
+
359
+ // Only report first silent failure in flow - break after emitting finding
360
+ break;
361
+ }
362
+ }
363
+ }
364
+
365
+ return flowFindings;
366
+ }