@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.
Files changed (135) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +24 -36
  4. package/src/cli/commands/default.js +681 -0
  5. package/src/cli/commands/doctor.js +197 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +586 -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 +297 -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 +110 -0
  14. package/src/cli/util/expectation-extractor.js +388 -0
  15. package/src/cli/util/findings-writer.js +32 -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 +412 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +30 -0
  21. package/src/cli/util/project-discovery.js +297 -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/runtime-budget.js +147 -0
  26. package/src/cli/util/summary-writer.js +43 -0
  27. package/src/types/global.d.ts +28 -0
  28. package/src/types/ts-ast.d.ts +24 -0
  29. package/src/verax/cli/ci-summary.js +35 -0
  30. package/src/verax/cli/context-explanation.js +89 -0
  31. package/src/verax/cli/doctor.js +277 -0
  32. package/src/verax/cli/error-normalizer.js +154 -0
  33. package/src/verax/cli/explain-output.js +105 -0
  34. package/src/verax/cli/finding-explainer.js +130 -0
  35. package/src/verax/cli/init.js +237 -0
  36. package/src/verax/cli/run-overview.js +163 -0
  37. package/src/verax/cli/url-safety.js +111 -0
  38. package/src/verax/cli/wizard.js +109 -0
  39. package/src/verax/cli/zero-findings-explainer.js +57 -0
  40. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  41. package/src/verax/core/action-classifier.js +86 -0
  42. package/src/verax/core/budget-engine.js +218 -0
  43. package/src/verax/core/canonical-outcomes.js +157 -0
  44. package/src/verax/core/decision-snapshot.js +335 -0
  45. package/src/verax/core/determinism-model.js +432 -0
  46. package/src/verax/core/incremental-store.js +245 -0
  47. package/src/verax/core/invariants.js +356 -0
  48. package/src/verax/core/promise-model.js +230 -0
  49. package/src/verax/core/replay-validator.js +350 -0
  50. package/src/verax/core/replay.js +222 -0
  51. package/src/verax/core/run-id.js +175 -0
  52. package/src/verax/core/run-manifest.js +99 -0
  53. package/src/verax/core/silence-impact.js +369 -0
  54. package/src/verax/core/silence-model.js +523 -0
  55. package/src/verax/detect/comparison.js +7 -34
  56. package/src/verax/detect/confidence-engine.js +764 -329
  57. package/src/verax/detect/detection-engine.js +293 -0
  58. package/src/verax/detect/evidence-index.js +127 -0
  59. package/src/verax/detect/expectation-model.js +241 -168
  60. package/src/verax/detect/explanation-helpers.js +187 -0
  61. package/src/verax/detect/finding-detector.js +450 -0
  62. package/src/verax/detect/findings-writer.js +41 -12
  63. package/src/verax/detect/flow-detector.js +366 -0
  64. package/src/verax/detect/index.js +200 -288
  65. package/src/verax/detect/interactive-findings.js +612 -0
  66. package/src/verax/detect/signal-mapper.js +308 -0
  67. package/src/verax/detect/skip-classifier.js +4 -4
  68. package/src/verax/detect/verdict-engine.js +561 -0
  69. package/src/verax/evidence-index-writer.js +61 -0
  70. package/src/verax/flow/flow-engine.js +3 -2
  71. package/src/verax/flow/flow-spec.js +1 -2
  72. package/src/verax/index.js +103 -15
  73. package/src/verax/intel/effect-detector.js +368 -0
  74. package/src/verax/intel/handler-mapper.js +249 -0
  75. package/src/verax/intel/index.js +281 -0
  76. package/src/verax/intel/route-extractor.js +280 -0
  77. package/src/verax/intel/ts-program.js +256 -0
  78. package/src/verax/intel/vue-navigation-extractor.js +642 -0
  79. package/src/verax/intel/vue-router-extractor.js +325 -0
  80. package/src/verax/learn/action-contract-extractor.js +338 -104
  81. package/src/verax/learn/ast-contract-extractor.js +148 -6
  82. package/src/verax/learn/flow-extractor.js +172 -0
  83. package/src/verax/learn/index.js +36 -2
  84. package/src/verax/learn/manifest-writer.js +122 -58
  85. package/src/verax/learn/project-detector.js +40 -0
  86. package/src/verax/learn/route-extractor.js +28 -97
  87. package/src/verax/learn/route-validator.js +8 -7
  88. package/src/verax/learn/state-extractor.js +212 -0
  89. package/src/verax/learn/static-extractor-navigation.js +114 -0
  90. package/src/verax/learn/static-extractor-validation.js +88 -0
  91. package/src/verax/learn/static-extractor.js +119 -10
  92. package/src/verax/learn/truth-assessor.js +24 -21
  93. package/src/verax/learn/ts-contract-resolver.js +14 -12
  94. package/src/verax/observe/aria-sensor.js +211 -0
  95. package/src/verax/observe/browser.js +30 -6
  96. package/src/verax/observe/console-sensor.js +2 -18
  97. package/src/verax/observe/domain-boundary.js +10 -1
  98. package/src/verax/observe/expectation-executor.js +513 -0
  99. package/src/verax/observe/flow-matcher.js +143 -0
  100. package/src/verax/observe/focus-sensor.js +196 -0
  101. package/src/verax/observe/human-driver.js +660 -273
  102. package/src/verax/observe/index.js +910 -26
  103. package/src/verax/observe/interaction-discovery.js +378 -15
  104. package/src/verax/observe/interaction-runner.js +562 -197
  105. package/src/verax/observe/loading-sensor.js +145 -0
  106. package/src/verax/observe/navigation-sensor.js +255 -0
  107. package/src/verax/observe/network-sensor.js +55 -7
  108. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  109. package/src/verax/observe/observed-expectation.js +305 -0
  110. package/src/verax/observe/page-frontier.js +234 -0
  111. package/src/verax/observe/settle.js +38 -17
  112. package/src/verax/observe/state-sensor.js +393 -0
  113. package/src/verax/observe/state-ui-sensor.js +7 -1
  114. package/src/verax/observe/timing-sensor.js +228 -0
  115. package/src/verax/observe/traces-writer.js +73 -21
  116. package/src/verax/observe/ui-signal-sensor.js +143 -17
  117. package/src/verax/scan-summary-writer.js +80 -15
  118. package/src/verax/shared/artifact-manager.js +111 -9
  119. package/src/verax/shared/budget-profiles.js +136 -0
  120. package/src/verax/shared/caching.js +1 -1
  121. package/src/verax/shared/ci-detection.js +39 -0
  122. package/src/verax/shared/config-loader.js +169 -0
  123. package/src/verax/shared/dynamic-route-utils.js +224 -0
  124. package/src/verax/shared/expectation-coverage.js +44 -0
  125. package/src/verax/shared/expectation-prover.js +81 -0
  126. package/src/verax/shared/expectation-tracker.js +201 -0
  127. package/src/verax/shared/expectations-writer.js +60 -0
  128. package/src/verax/shared/first-run.js +44 -0
  129. package/src/verax/shared/progress-reporter.js +171 -0
  130. package/src/verax/shared/retry-policy.js +9 -1
  131. package/src/verax/shared/root-artifacts.js +49 -0
  132. package/src/verax/shared/scan-budget.js +86 -0
  133. package/src/verax/shared/url-normalizer.js +162 -0
  134. package/src/verax/shared/zip-artifacts.js +66 -0
  135. package/src/verax/validate/context-validator.js +244 -0
@@ -1,29 +1,19 @@
1
1
  import { readFileSync, existsSync } from 'fs';
2
- import { getExpectation } from './expectation-model.js';
2
+ import { dirname, basename } from 'path';
3
+ import { expectsNavigation } from './expectation-model.js';
3
4
  import { hasMeaningfulUrlChange, hasVisibleChange, hasDomChange } from './comparison.js';
4
5
  import { writeFindings } from './findings-writer.js';
5
6
  import { getUrlPath } from './evidence-validator.js';
6
7
  import { classifySkipReason, collectSkipReasons } from './skip-classifier.js';
7
- import { ExpectationProof } from '../shared/expectation-proof.js';
8
- import { computeConfidence } from './confidence-engine.js';
8
+ import { detectInteractiveFindings } from './interactive-findings.js';
9
9
 
10
- // Helper to create finding with confidence score
11
- function createFindingWithConfidence(baseFinding, expectationInfo, sensors, comparisons) {
12
- const confidence = computeConfidence({
13
- findingType: baseFinding.type,
14
- expectation: expectationInfo,
15
- sensors: sensors,
16
- comparisons: comparisons,
17
- attemptMeta: {}
18
- });
19
-
20
- return {
21
- ...baseFinding,
22
- confidence
23
- };
24
- }
25
-
26
- export async function detect(manifestPath, tracesPath, validation = null, artifactPaths = null) {
10
+ /**
11
+ * @param {string} manifestPath
12
+ * @param {string} tracesPath
13
+ * @param {Object} [validation]
14
+ * @returns {Promise<any>}
15
+ */
16
+ export async function detect(manifestPath, tracesPath, validation = null, _expectationCoverageGaps = null, _silenceTracker = null) {
27
17
  if (!existsSync(manifestPath)) {
28
18
  throw new Error(`Manifest not found: ${manifestPath}`);
29
19
  }
@@ -41,8 +31,21 @@ export async function detect(manifestPath, tracesPath, validation = null, artifa
41
31
  const projectDir = manifest.projectDir;
42
32
  const findings = [];
43
33
 
34
+ // Extract runId from tracesPath: .verax/runs/<runId>/observation-traces.json
35
+ let runId = null;
36
+ try {
37
+ const runDir = dirname(tracesPath);
38
+ const runDirBasename = basename(runDir);
39
+ // Check if runDir is in .verax/runs/<runId> structure
40
+ const parentDir = dirname(runDir);
41
+ if (basename(parentDir) === 'runs' && basename(dirname(parentDir)) === '.verax') {
42
+ runId = runDirBasename;
43
+ }
44
+ } catch {
45
+ // Ignore path parsing errors
46
+ }
47
+
44
48
  let interactionsAnalyzed = 0;
45
- let interactionsSkippedNoExpectation = 0;
46
49
  const skips = [];
47
50
 
48
51
  for (const trace of observation.traces) {
@@ -52,334 +55,243 @@ export async function detect(manifestPath, tracesPath, validation = null, artifa
52
55
  const beforeScreenshot = trace.before.screenshot;
53
56
  const afterScreenshot = trace.after.screenshot;
54
57
 
55
- // Get expectation with proof status (Wave 0 - TRUTH LOCK, Wave 5 - ACTION CONTRACTS)
56
- const attemptMeta = trace.meta || {};
57
- const expectationInfo = getExpectation(manifest, interaction, beforeUrl, attemptMeta);
58
+ const expectsNav = expectsNavigation(manifest, interaction, beforeUrl);
58
59
 
59
- // Only analyze interactions with PROVEN expectations
60
- if (!expectationInfo.hasExpectation || expectationInfo.proof !== ExpectationProof.PROVEN_EXPECTATION) {
61
- interactionsSkippedNoExpectation++;
62
- const skipReason = {
63
- code: 'UNPROVEN_EXPECTATION',
64
- message: 'No proven code-derived expectation for this interaction',
65
- interaction: {
66
- type: interaction.type,
67
- selector: interaction.selector,
68
- label: interaction.label
69
- },
70
- expectationProof: expectationInfo.proof
71
- };
72
- skips.push(skipReason);
60
+ if (!expectsNav) {
61
+ const skipReason = classifySkipReason(manifest, interaction, beforeUrl, validation);
62
+ if (skipReason) {
63
+ skips.push({
64
+ code: skipReason.code,
65
+ message: skipReason.message,
66
+ interaction: {
67
+ type: interaction.type,
68
+ selector: interaction.selector,
69
+ label: interaction.label
70
+ }
71
+ });
72
+ }
73
73
  continue;
74
74
  }
75
75
 
76
- const expectedTargetPath = expectationInfo.expectedTargetPath;
77
- const expectationType = expectationInfo.expectationType;
76
+ let expectedTargetPath = null;
77
+ let expectationType = null;
78
+ let selectorMismatch = false;
79
+ let multipleMatches = false;
78
80
 
79
- // Expectation is PROVEN, proceed with analysis
80
- interactionsAnalyzed++;
81
-
82
- const hasUrlChange = hasMeaningfulUrlChange(beforeUrl, afterUrl);
83
- const hasVisibleChangeResult = hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir);
84
- const hasDomChangeResult = hasDomChange(trace);
85
-
86
- // Wave 3: Analyze sensor data for silent failures
87
- const sensorData = trace.sensors || {};
88
- const networkSummary = sensorData.network || {};
89
- const consoleSummary = sensorData.console || {};
90
- const uiSignalChanges = sensorData.uiSignals?.changes || {};
91
-
92
- // All expectations reaching here are PROVEN with explicit target paths
93
- const afterPath = getUrlPath(afterUrl);
94
- const normalizedTarget = expectedTargetPath.replace(/\/$/, '') || '/';
95
- const normalizedAfter = afterPath ? afterPath.replace(/\/$/, '') || '/' : '';
96
-
97
- if (expectationType === 'form_submission') {
98
- if (normalizedAfter !== normalizedTarget && !hasUrlChange && !hasDomChangeResult) {
99
- // Check for network errors during form submission
100
- if (networkSummary.failedRequests > 0) {
101
- const finding = createFindingWithConfidence({
102
- type: 'network_silent_failure',
103
- interaction: {
104
- type: interaction.type,
105
- selector: interaction.selector,
106
- label: interaction.label
107
- },
108
- reason: 'Form submission triggered network errors with no UI feedback',
109
- expectationProof: ExpectationProof.PROVEN_EXPECTATION,
110
- evidence: {
111
- before: beforeScreenshot,
112
- after: afterScreenshot,
113
- beforeUrl: beforeUrl,
114
- afterUrl: afterUrl,
115
- networkErrors: networkSummary.failedRequests,
116
- failedByStatus: networkSummary.failedByStatus,
117
- topFailedUrls: networkSummary.topFailedUrls
81
+ if (manifest.staticExpectations && manifest.staticExpectations.length > 0) {
82
+ const beforePath = getUrlPath(beforeUrl);
83
+ if (beforePath) {
84
+ const normalizedBefore = beforePath.replace(/\/$/, '') || '/';
85
+ const matchingExpectations = [];
86
+
87
+ for (const expectation of manifest.staticExpectations) {
88
+ const normalizedFrom = expectation.fromPath.replace(/\/$/, '') || '/';
89
+ if (normalizedFrom === normalizedBefore) {
90
+ const selectorHint = expectation.selectorHint || '';
91
+ const interactionSelector = interaction.selector || '';
92
+
93
+ if (selectorHint && interactionSelector) {
94
+ const normalizedSelectorHint = selectorHint.replace(/[[\]()]/g, '');
95
+ const normalizedInteractionSelector = interactionSelector.replace(/[[\]()]/g, '');
96
+
97
+ if (selectorHint === interactionSelector ||
98
+ selectorHint.includes(interactionSelector) ||
99
+ interactionSelector.includes(normalizedSelectorHint) ||
100
+ normalizedSelectorHint === normalizedInteractionSelector) {
101
+ if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
102
+ matchingExpectations.push(expectation);
103
+ } else if (expectation.type === 'form_submission' && interaction.type === 'form') {
104
+ matchingExpectations.push(expectation);
105
+ }
106
+ } else {
107
+ if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
108
+ selectorMismatch = true;
109
+ } else if (expectation.type === 'form_submission' && interaction.type === 'form') {
110
+ selectorMismatch = true;
111
+ }
112
+ }
113
+ } else if (!selectorHint && !interactionSelector) {
114
+ if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
115
+ matchingExpectations.push(expectation);
116
+ } else if (expectation.type === 'form_submission' && interaction.type === 'form') {
117
+ matchingExpectations.push(expectation);
118
+ }
118
119
  }
119
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
120
- findings.push(finding);
120
+ }
121
121
  }
122
- // Check for validation errors (console errors with no feedback)
123
- else if (consoleSummary.hasErrors && !uiSignalChanges.changed) {
124
- const finding = createFindingWithConfidence({
125
- type: 'validation_silent_failure',
122
+
123
+ if (matchingExpectations.length > 1) {
124
+ multipleMatches = true;
125
+ } else if (matchingExpectations.length === 1) {
126
+ expectedTargetPath = matchingExpectations[0].targetPath;
127
+ expectationType = matchingExpectations[0].type;
128
+ } else if (selectorMismatch) {
129
+ skips.push({
130
+ code: 'SELECTOR_MISMATCH',
131
+ message: 'Expectations exist but selector mismatch and no safe fallback match',
126
132
  interaction: {
127
133
  type: interaction.type,
128
134
  selector: interaction.selector,
129
135
  label: interaction.label
130
- },
131
- reason: 'Form validation errors logged to console with no visible feedback',
132
- expectationProof: ExpectationProof.PROVEN_EXPECTATION,
133
- evidence: {
134
- before: beforeScreenshot,
135
- after: afterScreenshot,
136
- beforeUrl: beforeUrl,
137
- afterUrl: afterUrl,
138
- consoleErrors: consoleSummary.consoleErrorCount,
139
- errorMessages: consoleSummary.lastErrors?.slice(0, 3)
140
136
  }
141
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
142
- findings.push(finding);
137
+ });
138
+ continue;
143
139
  }
144
- // Check for missing loading/error feedback
145
- else if (!uiSignalChanges.changed) {
146
- const finding = createFindingWithConfidence({
147
- type: 'missing_feedback_failure',
148
- interaction: {
149
- type: interaction.type,
150
- selector: interaction.selector,
151
- label: interaction.label
152
- },
153
- reason: 'Form submission occurred with no loading or error feedback',
154
- expectationProof: ExpectationProof.PROVEN_EXPECTATION,
155
- evidence: {
156
- before: beforeScreenshot,
157
- after: afterScreenshot,
158
- beforeUrl: beforeUrl,
159
- afterUrl: afterUrl,
160
- networkActivity: networkSummary.totalRequests || 0,
161
- hadFeedback: uiSignalChanges.changed || false
140
+ }
141
+ }
142
+
143
+ if (!expectedTargetPath && manifest.projectType === 'react_spa' && interaction.type === 'link') {
144
+ const beforePath = getUrlPath(beforeUrl);
145
+ if (beforePath) {
146
+ const matchingRoutes = [];
147
+ const unreachableRoutes = new Set();
148
+
149
+ if (validation && validation.details) {
150
+ for (const detail of validation.details) {
151
+ if (detail.status === 'UNREACHABLE') {
152
+ unreachableRoutes.add(detail.path);
162
153
  }
163
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
164
- findings.push(finding);
154
+ }
165
155
  }
166
- // Generic no-effect failure (no network, no console errors, no visible changes)
167
- else {
168
- const finding = createFindingWithConfidence({
169
- type: 'no_effect_silent_failure',
156
+
157
+ for (const route of manifest.routes) {
158
+ if (!route.public) continue;
159
+ if (unreachableRoutes.has(route.path)) {
160
+ continue;
161
+ }
162
+ const routePath = route.path.toLowerCase();
163
+ const routeName = routePath.split('/').pop() || 'home';
164
+ const interactionLabel = (interaction.label || '').toLowerCase().trim();
165
+
166
+ if (interactionLabel.includes(routeName) || routeName.includes(interactionLabel)) {
167
+ matchingRoutes.push(route.path);
168
+ }
169
+ }
170
+
171
+ if (matchingRoutes.length > 1) {
172
+ skips.push({
173
+ code: 'AMBIGUOUS_MATCH',
174
+ message: 'Multiple expectations could match; conservative approach requires single clear match',
170
175
  interaction: {
171
176
  type: interaction.type,
172
177
  selector: interaction.selector,
173
178
  label: interaction.label
174
- },
175
- reason: 'Expected form submission did not occur',
176
- expectationProof: ExpectationProof.PROVEN_EXPECTATION,
177
- evidence: {
178
- before: beforeScreenshot,
179
- after: afterScreenshot,
180
- beforeUrl: beforeUrl,
181
- afterUrl: afterUrl
182
179
  }
183
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
184
- findings.push(finding);
180
+ });
181
+ continue;
182
+ } else if (matchingRoutes.length === 1) {
183
+ expectedTargetPath = matchingRoutes[0];
184
+ expectationType = 'navigation';
185
185
  }
186
186
  }
187
- } else if (expectationType === 'navigation' || expectationType === 'spa_navigation') {
188
- // Wave 1: spa_navigation expectations from AST contracts
189
- const urlMatchesTarget = normalizedAfter === normalizedTarget;
190
- const hasEffect = urlMatchesTarget || hasVisibleChangeResult || hasDomChangeResult;
187
+ }
188
+
189
+ if (multipleMatches) {
190
+ skips.push({
191
+ code: 'AMBIGUOUS_MATCH',
192
+ message: 'Multiple expectations could match; conservative approach requires single clear match',
193
+ interaction: {
194
+ type: interaction.type,
195
+ selector: interaction.selector,
196
+ label: interaction.label
197
+ }
198
+ });
199
+ continue;
200
+ }
201
+
202
+ interactionsAnalyzed++;
203
+
204
+ const hasUrlChange = hasMeaningfulUrlChange(beforeUrl, afterUrl);
205
+ // hasVisibleChange requires runId, skip comparison if runId unavailable
206
+ let hasVisibleChangeResult = false;
207
+ if (runId) {
208
+ try {
209
+ hasVisibleChangeResult = hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir, runId);
210
+ } catch (e) {
211
+ // If screenshot comparison fails, treat as no visible change
212
+ hasVisibleChangeResult = false;
213
+ }
214
+ }
215
+ const hasDomChangeResult = hasDomChange(trace);
216
+
217
+ if (expectedTargetPath) {
218
+ const afterPath = getUrlPath(afterUrl);
219
+ const normalizedTarget = expectedTargetPath.replace(/\/$/, '') || '/';
220
+ const normalizedAfter = afterPath ? afterPath.replace(/\/$/, '') || '/' : '';
191
221
 
192
- if (!hasEffect) {
193
- // Check what prevented the navigation
194
- if (networkSummary.failedRequests > 0) {
195
- const finding = createFindingWithConfidence({
196
- type: 'network_silent_failure',
222
+ if (expectationType === 'form_submission') {
223
+ if (normalizedAfter !== normalizedTarget && !hasUrlChange && !hasDomChangeResult) {
224
+ findings.push({
225
+ type: 'silent_failure',
197
226
  interaction: {
198
227
  type: interaction.type,
199
228
  selector: interaction.selector,
200
229
  label: interaction.label
201
230
  },
202
- reason: 'Navigation attempt resulted in network errors with no UI feedback',
203
- expectationProof: ExpectationProof.PROVEN_EXPECTATION,
204
- evidence: {
205
- before: beforeScreenshot,
206
- after: afterScreenshot,
207
- beforeUrl: beforeUrl,
208
- afterUrl: afterUrl,
209
- networkErrors: networkSummary.failedRequests,
210
- topFailedUrls: networkSummary.topFailedUrls
211
- }
212
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
213
- findings.push(finding);
214
- } else if (consoleSummary.hasErrors) {
215
- const finding = createFindingWithConfidence({
216
- type: 'validation_silent_failure',
217
- interaction: {
218
- type: interaction.type,
219
- selector: interaction.selector,
220
- label: interaction.label
221
- },
222
- reason: 'Navigation blocked by client-side validation errors',
223
- expectationProof: ExpectationProof.PROVEN_EXPECTATION,
224
- evidence: {
225
- before: beforeScreenshot,
226
- after: afterScreenshot,
227
- beforeUrl: beforeUrl,
228
- afterUrl: afterUrl,
229
- consoleErrors: consoleSummary.consoleErrorCount,
230
- errorMessages: consoleSummary.lastErrors?.slice(0, 3)
231
- }
232
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
233
- findings.push(finding);
234
- } else if (networkSummary.slowRequestsCount > 0 && !uiSignalChanges.changed) {
235
- const finding = createFindingWithConfidence({
236
- type: 'missing_feedback_failure',
237
- interaction: {
238
- type: interaction.type,
239
- selector: interaction.selector,
240
- label: interaction.label
241
- },
242
- reason: 'Slow navigation request with no loading indicator',
243
- expectationProof: ExpectationProof.PROVEN_EXPECTATION,
231
+ reason: 'Expected form submission did not occur',
244
232
  evidence: {
245
233
  before: beforeScreenshot,
246
234
  after: afterScreenshot,
247
235
  beforeUrl: beforeUrl,
248
- afterUrl: afterUrl,
249
- slowRequests: networkSummary.slowRequestsCount,
250
- slowRequestDurations: networkSummary.slowRequests?.map(r => r.duration)
236
+ afterUrl: afterUrl
251
237
  }
252
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
253
- findings.push(finding);
254
- } else {
255
- const finding = createFindingWithConfidence({
256
- type: 'no_effect_silent_failure',
238
+ });
239
+ }
240
+ } else if (expectationType === 'navigation') {
241
+ const urlMatchesTarget = normalizedAfter === normalizedTarget;
242
+ const hasEffect = urlMatchesTarget || hasVisibleChangeResult || hasDomChangeResult;
243
+
244
+ if (!hasEffect) {
245
+ findings.push({
246
+ type: 'silent_failure',
257
247
  interaction: {
258
248
  type: interaction.type,
259
249
  selector: interaction.selector,
260
250
  label: interaction.label
261
251
  },
262
252
  reason: 'Expected user-visible outcome did not occur',
263
- expectationProof: ExpectationProof.PROVEN_EXPECTATION,
264
253
  evidence: {
265
254
  before: beforeScreenshot,
266
255
  after: afterScreenshot,
267
256
  beforeUrl: beforeUrl,
268
257
  afterUrl: afterUrl
269
258
  }
270
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
271
- findings.push(finding);
259
+ });
272
260
  }
273
261
  }
274
- } else if (expectationType === 'network_action') {
275
- // Wave 5: Network action expectations from AST action contracts
276
- // Check if promised network request actually occurred
277
- if (networkSummary.totalRequests === 0) {
278
- // Code promised a fetch/axios call but no network activity detected
279
- const finding = createFindingWithConfidence({
280
- type: 'missing_network_action',
281
- interaction: {
282
- type: interaction.type,
283
- selector: interaction.selector,
284
- label: interaction.label
285
- },
286
- reason: 'Code contract promises network request but none was made',
287
- expectationProof: ExpectationProof.PROVEN_EXPECTATION,
288
- evidence: {
289
- before: beforeScreenshot,
290
- after: afterScreenshot,
291
- beforeUrl: beforeUrl,
292
- afterUrl: afterUrl,
293
- expectedMethod: expectationInfo.method,
294
- expectedUrl: expectationInfo.urlPath,
295
- sourceRef: attemptMeta.sourceRef
296
- }
297
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
298
- findings.push(finding);
299
- }
300
- // Check for network errors without feedback
301
- else if (networkSummary.failedRequests > 0 && !uiSignalChanges.changed) {
302
- const finding = createFindingWithConfidence({
303
- type: 'network_silent_failure',
304
- interaction: {
305
- type: interaction.type,
306
- selector: interaction.selector,
307
- label: interaction.label
308
- },
309
- reason: 'Network action failed with no UI feedback',
310
- expectationProof: ExpectationProof.PROVEN_EXPECTATION,
311
- evidence: {
312
- before: beforeScreenshot,
313
- after: afterScreenshot,
314
- beforeUrl: beforeUrl,
315
- afterUrl: afterUrl,
316
- networkErrors: networkSummary.failedRequests,
317
- failedByStatus: networkSummary.failedByStatus,
318
- topFailedUrls: networkSummary.topFailedUrls,
319
- expectedMethod: expectationInfo.method,
320
- expectedUrl: expectationInfo.urlPath,
321
- sourceRef: attemptMeta.sourceRef
322
- }
323
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
324
- findings.push(finding);
325
- }
326
- // Check for missing feedback on slow requests
327
- else if (networkSummary.slowRequestsCount > 0 && !uiSignalChanges.changed) {
328
- const finding = createFindingWithConfidence({
329
- type: 'missing_feedback_failure',
330
- interaction: {
331
- type: interaction.type,
332
- selector: interaction.selector,
333
- label: interaction.label
334
- },
335
- reason: 'Slow network action with no loading feedback',
336
- expectationProof: ExpectationProof.PROVEN_EXPECTATION,
337
- evidence: {
338
- before: beforeScreenshot,
339
- after: afterScreenshot,
340
- beforeUrl: beforeUrl,
341
- afterUrl: afterUrl,
342
- slowRequests: networkSummary.slowRequestsCount,
343
- expectedMethod: expectationInfo.method,
344
- expectedUrl: expectationInfo.urlPath,
345
- sourceRef: attemptMeta.sourceRef
346
- }
347
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
348
- findings.push(finding);
349
- }
350
- } else if (expectationType === 'state_action') {
351
- // Wave 8: State action expectations from AST state contracts
352
- const stateUIData = sensorData.stateUI || {};
353
- const stateChanged = stateUIData.changed === true;
354
-
355
- // Check if promised state mutation actually occurred
356
- if (!stateChanged && !hasUrlChange && !hasDomChangeResult && networkSummary.totalRequests === 0) {
357
- const finding = createFindingWithConfidence({
358
- type: 'missing_state_action',
262
+ } else {
263
+ if (!hasUrlChange && !hasVisibleChangeResult && !hasDomChangeResult) {
264
+ findings.push({
265
+ type: 'silent_failure',
359
266
  interaction: {
360
267
  type: interaction.type,
361
268
  selector: interaction.selector,
362
269
  label: interaction.label
363
270
  },
364
- reason: `Code contract promises state mutation (${expectationInfo.stateKind}) but state UI did not change`,
365
- expectationProof: ExpectationProof.PROVEN_EXPECTATION,
271
+ reason: 'Expected user-visible outcome did not occur',
366
272
  evidence: {
367
273
  before: beforeScreenshot,
368
274
  after: afterScreenshot,
369
275
  beforeUrl: beforeUrl,
370
- afterUrl: afterUrl,
371
- expectedStateKind: expectationInfo.stateKind,
372
- sourceRef: attemptMeta.sourceRef,
373
- handlerRef: attemptMeta.handlerRef,
374
- stateUIReasons: stateUIData.reasons || []
276
+ afterUrl: afterUrl
375
277
  }
376
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
377
- findings.push(finding);
278
+ });
378
279
  }
379
280
  }
380
281
  }
381
282
 
382
- const findingsResult = writeFindings(projectDir, observation.url, findings, artifactPaths);
283
+ // Interactive and accessibility intelligence
284
+ detectInteractiveFindings(observation.traces, manifest, findings);
285
+
286
+ // Infer canonical run directory from tracesPath when available
287
+ let runDir = null;
288
+ try {
289
+ runDir = dirname(tracesPath);
290
+ } catch {
291
+ // Ignore path parsing errors
292
+ }
293
+
294
+ const findingsResult = writeFindings(projectDir, observation.url, findings, [], runDir);
383
295
 
384
296
  const skipSummary = collectSkipReasons(skips);
385
297