@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
@@ -1,29 +1,13 @@
1
1
  import { readFileSync, existsSync } from 'fs';
2
- import { getExpectation } from './expectation-model.js';
2
+ import { dirname } 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
+ export async function detect(manifestPath, tracesPath, validation = null) {
27
11
  if (!existsSync(manifestPath)) {
28
12
  throw new Error(`Manifest not found: ${manifestPath}`);
29
13
  }
@@ -52,334 +36,236 @@ export async function detect(manifestPath, tracesPath, validation = null, artifa
52
36
  const beforeScreenshot = trace.before.screenshot;
53
37
  const afterScreenshot = trace.after.screenshot;
54
38
 
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);
39
+ const expectsNav = expectsNavigation(manifest, interaction, beforeUrl);
58
40
 
59
- // Only analyze interactions with PROVEN expectations
60
- if (!expectationInfo.hasExpectation || expectationInfo.proof !== ExpectationProof.PROVEN_EXPECTATION) {
41
+ if (!expectsNav) {
61
42
  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);
43
+ const skipReason = classifySkipReason(manifest, interaction, beforeUrl, validation);
44
+ if (skipReason) {
45
+ skips.push({
46
+ code: skipReason.code,
47
+ message: skipReason.message,
48
+ interaction: {
49
+ type: interaction.type,
50
+ selector: interaction.selector,
51
+ label: interaction.label
52
+ }
53
+ });
54
+ }
73
55
  continue;
74
56
  }
75
57
 
76
- const expectedTargetPath = expectationInfo.expectedTargetPath;
77
- const expectationType = expectationInfo.expectationType;
78
-
79
- // Expectation is PROVEN, proceed with analysis
80
- interactionsAnalyzed++;
58
+ let expectedTargetPath = null;
59
+ let expectationType = null;
60
+ let selectorMismatch = false;
61
+ let multipleMatches = false;
81
62
 
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
63
+ if (manifest.staticExpectations && manifest.staticExpectations.length > 0) {
64
+ const beforePath = getUrlPath(beforeUrl);
65
+ if (beforePath) {
66
+ const normalizedBefore = beforePath.replace(/\/$/, '') || '/';
67
+ const matchingExpectations = [];
68
+
69
+ for (const expectation of manifest.staticExpectations) {
70
+ const normalizedFrom = expectation.fromPath.replace(/\/$/, '') || '/';
71
+ if (normalizedFrom === normalizedBefore) {
72
+ const selectorHint = expectation.selectorHint || '';
73
+ const interactionSelector = interaction.selector || '';
74
+
75
+ if (selectorHint && interactionSelector) {
76
+ const normalizedSelectorHint = selectorHint.replace(/[\[\]()]/g, '');
77
+ const normalizedInteractionSelector = interactionSelector.replace(/[\[\]()]/g, '');
78
+
79
+ if (selectorHint === interactionSelector ||
80
+ selectorHint.includes(interactionSelector) ||
81
+ interactionSelector.includes(normalizedSelectorHint) ||
82
+ normalizedSelectorHint === normalizedInteractionSelector) {
83
+ if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
84
+ matchingExpectations.push(expectation);
85
+ } else if (expectation.type === 'form_submission' && interaction.type === 'form') {
86
+ matchingExpectations.push(expectation);
87
+ }
88
+ } else {
89
+ if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
90
+ selectorMismatch = true;
91
+ } else if (expectation.type === 'form_submission' && interaction.type === 'form') {
92
+ selectorMismatch = true;
93
+ }
94
+ }
95
+ } else if (!selectorHint && !interactionSelector) {
96
+ if (expectation.type === 'navigation' && (interaction.type === 'link' || interaction.type === 'button')) {
97
+ matchingExpectations.push(expectation);
98
+ } else if (expectation.type === 'form_submission' && interaction.type === 'form') {
99
+ matchingExpectations.push(expectation);
100
+ }
118
101
  }
119
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
120
- findings.push(finding);
102
+ }
121
103
  }
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',
104
+
105
+ if (matchingExpectations.length > 1) {
106
+ multipleMatches = true;
107
+ } else if (matchingExpectations.length === 1) {
108
+ expectedTargetPath = matchingExpectations[0].targetPath;
109
+ expectationType = matchingExpectations[0].type;
110
+ } else if (selectorMismatch) {
111
+ skips.push({
112
+ code: 'SELECTOR_MISMATCH',
113
+ message: 'Expectations exist but selector mismatch and no safe fallback match',
126
114
  interaction: {
127
115
  type: interaction.type,
128
116
  selector: interaction.selector,
129
117
  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
118
  }
141
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
142
- findings.push(finding);
119
+ });
120
+ interactionsSkippedNoExpectation++;
121
+ continue;
143
122
  }
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
123
+ }
124
+ }
125
+
126
+ if (!expectedTargetPath && manifest.projectType === 'react_spa' && interaction.type === 'link') {
127
+ const beforePath = getUrlPath(beforeUrl);
128
+ if (beforePath) {
129
+ const matchingRoutes = [];
130
+ const unreachableRoutes = new Set();
131
+
132
+ if (validation && validation.details) {
133
+ for (const detail of validation.details) {
134
+ if (detail.status === 'UNREACHABLE') {
135
+ unreachableRoutes.add(detail.path);
162
136
  }
163
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
164
- findings.push(finding);
137
+ }
138
+ }
139
+
140
+ for (const route of manifest.routes) {
141
+ if (!route.public) continue;
142
+ if (unreachableRoutes.has(route.path)) {
143
+ continue;
144
+ }
145
+ const routePath = route.path.toLowerCase();
146
+ const routeName = routePath.split('/').pop() || 'home';
147
+ const interactionLabel = (interaction.label || '').toLowerCase().trim();
148
+
149
+ if (interactionLabel.includes(routeName) || routeName.includes(interactionLabel)) {
150
+ matchingRoutes.push(route.path);
151
+ }
165
152
  }
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',
153
+
154
+ if (matchingRoutes.length > 1) {
155
+ skips.push({
156
+ code: 'AMBIGUOUS_MATCH',
157
+ message: 'Multiple expectations could match; conservative approach requires single clear match',
170
158
  interaction: {
171
159
  type: interaction.type,
172
160
  selector: interaction.selector,
173
161
  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
162
  }
183
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
184
- findings.push(finding);
163
+ });
164
+ interactionsSkippedNoExpectation++;
165
+ continue;
166
+ } else if (matchingRoutes.length === 1) {
167
+ expectedTargetPath = matchingRoutes[0];
168
+ expectationType = 'navigation';
185
169
  }
186
170
  }
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;
171
+ }
172
+
173
+ if (multipleMatches) {
174
+ skips.push({
175
+ code: 'AMBIGUOUS_MATCH',
176
+ message: 'Multiple expectations could match; conservative approach requires single clear match',
177
+ interaction: {
178
+ type: interaction.type,
179
+ selector: interaction.selector,
180
+ label: interaction.label
181
+ }
182
+ });
183
+ interactionsSkippedNoExpectation++;
184
+ continue;
185
+ }
186
+
187
+ interactionsAnalyzed++;
188
+
189
+ const hasUrlChange = hasMeaningfulUrlChange(beforeUrl, afterUrl);
190
+ const hasVisibleChangeResult = hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir);
191
+ const hasDomChangeResult = hasDomChange(trace);
192
+
193
+ if (expectedTargetPath) {
194
+ const afterPath = getUrlPath(afterUrl);
195
+ const normalizedTarget = expectedTargetPath.replace(/\/$/, '') || '/';
196
+ const normalizedAfter = afterPath ? afterPath.replace(/\/$/, '') || '/' : '';
191
197
 
192
- if (!hasEffect) {
193
- // Check what prevented the navigation
194
- if (networkSummary.failedRequests > 0) {
195
- const finding = createFindingWithConfidence({
196
- type: 'network_silent_failure',
198
+ if (expectationType === 'form_submission') {
199
+ if (normalizedAfter !== normalizedTarget && !hasUrlChange && !hasDomChangeResult) {
200
+ findings.push({
201
+ type: 'silent_failure',
197
202
  interaction: {
198
203
  type: interaction.type,
199
204
  selector: interaction.selector,
200
205
  label: interaction.label
201
206
  },
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,
207
+ reason: 'Expected form submission did not occur',
244
208
  evidence: {
245
209
  before: beforeScreenshot,
246
210
  after: afterScreenshot,
247
211
  beforeUrl: beforeUrl,
248
- afterUrl: afterUrl,
249
- slowRequests: networkSummary.slowRequestsCount,
250
- slowRequestDurations: networkSummary.slowRequests?.map(r => r.duration)
212
+ afterUrl: afterUrl
251
213
  }
252
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
253
- findings.push(finding);
254
- } else {
255
- const finding = createFindingWithConfidence({
256
- type: 'no_effect_silent_failure',
214
+ });
215
+ }
216
+ } else if (expectationType === 'navigation') {
217
+ const urlMatchesTarget = normalizedAfter === normalizedTarget;
218
+ const hasEffect = urlMatchesTarget || hasVisibleChangeResult || hasDomChangeResult;
219
+
220
+ if (!hasEffect) {
221
+ findings.push({
222
+ type: 'silent_failure',
257
223
  interaction: {
258
224
  type: interaction.type,
259
225
  selector: interaction.selector,
260
226
  label: interaction.label
261
227
  },
262
228
  reason: 'Expected user-visible outcome did not occur',
263
- expectationProof: ExpectationProof.PROVEN_EXPECTATION,
264
229
  evidence: {
265
230
  before: beforeScreenshot,
266
231
  after: afterScreenshot,
267
232
  beforeUrl: beforeUrl,
268
233
  afterUrl: afterUrl
269
234
  }
270
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
271
- findings.push(finding);
235
+ });
272
236
  }
273
237
  }
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',
238
+ } else {
239
+ if (!hasUrlChange && !hasVisibleChangeResult && !hasDomChangeResult) {
240
+ findings.push({
241
+ type: 'silent_failure',
359
242
  interaction: {
360
243
  type: interaction.type,
361
244
  selector: interaction.selector,
362
245
  label: interaction.label
363
246
  },
364
- reason: `Code contract promises state mutation (${expectationInfo.stateKind}) but state UI did not change`,
365
- expectationProof: ExpectationProof.PROVEN_EXPECTATION,
247
+ reason: 'Expected user-visible outcome did not occur',
366
248
  evidence: {
367
249
  before: beforeScreenshot,
368
250
  after: afterScreenshot,
369
251
  beforeUrl: beforeUrl,
370
- afterUrl: afterUrl,
371
- expectedStateKind: expectationInfo.stateKind,
372
- sourceRef: attemptMeta.sourceRef,
373
- handlerRef: attemptMeta.handlerRef,
374
- stateUIReasons: stateUIData.reasons || []
252
+ afterUrl: afterUrl
375
253
  }
376
- }, expectationInfo, sensorData, { hasUrlChange, hasDomChange: hasDomChangeResult, hasVisibleChange: hasVisibleChangeResult });
377
- findings.push(finding);
254
+ });
378
255
  }
379
256
  }
380
257
  }
381
258
 
382
- const findingsResult = writeFindings(projectDir, observation.url, findings, artifactPaths);
259
+ // Interactive and accessibility intelligence
260
+ detectInteractiveFindings(observation.traces, manifest, findings);
261
+
262
+ // Infer canonical run directory from tracesPath when available
263
+ let runDir = null;
264
+ try {
265
+ runDir = dirname(tracesPath);
266
+ } catch {}
267
+
268
+ const findingsResult = writeFindings(projectDir, observation.url, findings, [], runDir);
383
269
 
384
270
  const skipSummary = collectSkipReasons(skips);
385
271