@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,186 @@
1
+ /**
2
+ * OBSERVED EXPECTATION DERIVER
3
+ *
4
+ * Generates STRICT, EVIDENCE-BASED "Observed Expectations" from DOM attributes
5
+ * and runtime signals. Only creates expectations when evidence is provable.
6
+ *
7
+ * Rules:
8
+ * - Navigation: only if element has href/data-href OR navigation event happened
9
+ * - Network: only if network request occurred with concrete URL (no template variables)
10
+ * - Validation: only if validation message is detected (not inferred)
11
+ * - State: only if state sensor shows named store mutation (supported stores only)
12
+ */
13
+
14
+ import { getUrlPath } from '../detect/evidence-validator.js';
15
+
16
+ /**
17
+ * Derive an OBSERVED expectation from an interaction trace.
18
+ * Returns null if no strict evidence is available.
19
+ *
20
+ * @param {Object} trace - Interaction trace with sensors and evidence
21
+ * @param {Object} interaction - Interaction object (type, selector, label, etc.)
22
+ * @param {string} beforeUrl - URL before interaction
23
+ * @param {string} afterUrl - URL after interaction
24
+ * @returns {Object|null} Observed expectation or null
25
+ */
26
+ export function deriveObservedExpectation(trace, interaction, beforeUrl, afterUrl) {
27
+ if (!trace || !interaction) {
28
+ return null;
29
+ }
30
+
31
+ const sensors = trace.sensors || {};
32
+ const networkSummary = sensors.network || {};
33
+ const navigationSummary = sensors.navigation || {};
34
+ const uiSignals = sensors.uiSignals || {};
35
+ const stateDiff = sensors.state || {};
36
+
37
+ // === NAVIGATION EXPECTATION ===
38
+ if (interaction.type === 'link' || interaction.type === 'button') {
39
+ // Check for href/data-href attribute
40
+ const href = interaction.href || interaction.dataHref || '';
41
+ const hasHrefAttribute = href && !href.startsWith('#') && !href.startsWith('javascript:');
42
+
43
+ // Check for navigation event
44
+ const urlChanged = navigationSummary.urlChanged === true;
45
+ const historyChanged = navigationSummary.historyLengthDelta !== null && navigationSummary.historyLengthDelta !== 0;
46
+ const hasNavigationEvent = urlChanged || historyChanged;
47
+
48
+ // Check for stable path change
49
+ const beforePath = getUrlPath(beforeUrl);
50
+ const afterPath = getUrlPath(afterUrl);
51
+ const pathChanged = beforePath && afterPath && beforePath !== afterPath;
52
+ const pathStable = afterPath && afterPath !== ''; // Path exists and is not empty
53
+
54
+ // Navigation expectation: requires href/data-href OR (navigation event AND stable path)
55
+ if (hasHrefAttribute || (hasNavigationEvent && pathChanged && pathStable)) {
56
+ const targetPath = hasHrefAttribute ? (href.startsWith('http') ? getUrlPath(href) : href) : afterPath;
57
+
58
+ // Only create if target path is concrete (no template variables like {id})
59
+ if (targetPath && !targetPath.includes('{') && !targetPath.includes('${')) {
60
+ return {
61
+ type: 'navigation',
62
+ expectationStrength: 'OBSERVED',
63
+ fromPath: beforePath || '/',
64
+ targetPath: targetPath,
65
+ evidence: {
66
+ selectorHint: interaction.selector,
67
+ source: 'runtime_observation',
68
+ attributeSource: hasHrefAttribute ? (interaction.href ? 'href' : 'data-href') : 'navigation_event',
69
+ observedUrl: afterUrl,
70
+ sourcePage: beforeUrl
71
+ },
72
+ proof: 'OBSERVED_EXPECTATION'
73
+ };
74
+ }
75
+ }
76
+ }
77
+
78
+ // === NETWORK EXPECTATION ===
79
+ if (interaction.type === 'button' || interaction.type === 'form') {
80
+ const totalRequests = networkSummary.totalRequests || 0;
81
+ const hasNetworkRequest = totalRequests > 0;
82
+
83
+ // Check for concrete request URLs from network sensor
84
+ // Network sensor provides slowRequests and topFailedUrls arrays with url property
85
+ const slowRequests = networkSummary.slowRequests || [];
86
+ const topFailedUrls = networkSummary.topFailedUrls || [];
87
+ const allRequestUrls = [
88
+ ...slowRequests.map(r => r.url),
89
+ ...topFailedUrls.map(r => r.url)
90
+ ];
91
+
92
+ // Find first concrete URL (no template variables, no query params for now)
93
+ const concreteRequestUrl = allRequestUrls.find(url =>
94
+ url &&
95
+ typeof url === 'string' &&
96
+ !url.includes('{') &&
97
+ !url.includes('${') &&
98
+ !url.includes('?') // Avoid query params for now (could be dynamic)
99
+ );
100
+
101
+ // Network expectation: requires network request with concrete URL
102
+ if (hasNetworkRequest && concreteRequestUrl) {
103
+ return {
104
+ type: 'network_action',
105
+ expectationStrength: 'OBSERVED',
106
+ fromPath: getUrlPath(beforeUrl) || '/',
107
+ expectedTarget: concreteRequestUrl,
108
+ evidence: {
109
+ selectorHint: interaction.selector,
110
+ source: 'runtime_observation',
111
+ observedRequestUrl: concreteRequestUrl,
112
+ requestCount: totalRequests,
113
+ sourcePage: beforeUrl
114
+ },
115
+ proof: 'OBSERVED_EXPECTATION'
116
+ };
117
+ }
118
+ }
119
+
120
+ // === VALIDATION EXPECTATION ===
121
+ if (interaction.type === 'form') {
122
+ // Check for explicit validation feedback (detected by UI signal sensor)
123
+ const validationFeedback = uiSignals.after?.validationFeedbackDetected === true;
124
+
125
+ // Validation expectation: requires explicit validation feedback detection
126
+ // UI signal sensor detects: invalid elements, visible alert regions, aria-live regions
127
+ if (validationFeedback) {
128
+ return {
129
+ type: 'validation_block',
130
+ expectationStrength: 'OBSERVED',
131
+ fromPath: getUrlPath(beforeUrl) || '/',
132
+ evidence: {
133
+ selectorHint: interaction.selector,
134
+ source: 'runtime_observation',
135
+ validationFeedbackDetected: true,
136
+ sourcePage: beforeUrl
137
+ },
138
+ proof: 'OBSERVED_EXPECTATION'
139
+ };
140
+ }
141
+ }
142
+
143
+ // === STATE EXPECTATION ===
144
+ if (interaction.type === 'button') {
145
+ // Check for state mutation
146
+ const stateChanged = stateDiff.available === true && stateDiff.changed && stateDiff.changed.length > 0;
147
+ const storeType = stateDiff.storeType || null;
148
+ const supportedStores = ['redux', 'zustand', 'mobx', 'recoil']; // Only supported stores
149
+
150
+ // State expectation: requires state mutation in supported store
151
+ if (stateChanged && storeType && supportedStores.includes(storeType.toLowerCase())) {
152
+ const changedKeys = stateDiff.changed || [];
153
+ const firstChangedKey = changedKeys[0] || null;
154
+
155
+ if (firstChangedKey) {
156
+ return {
157
+ type: 'state_action',
158
+ expectationStrength: 'OBSERVED',
159
+ fromPath: getUrlPath(beforeUrl) || '/',
160
+ expectedTarget: firstChangedKey,
161
+ evidence: {
162
+ selectorHint: interaction.selector,
163
+ source: 'runtime_observation',
164
+ storeType: storeType,
165
+ changedKeys: changedKeys,
166
+ sourcePage: beforeUrl
167
+ },
168
+ proof: 'OBSERVED_EXPECTATION'
169
+ };
170
+ }
171
+ }
172
+ }
173
+
174
+ // No strict evidence available
175
+ return null;
176
+ }
177
+
178
+ /**
179
+ * Check if an expectation is OBSERVED (not PROVEN).
180
+ */
181
+ export function isObservedExpectation(expectation) {
182
+ if (!expectation) return false;
183
+ return expectation.expectationStrength === 'OBSERVED' ||
184
+ expectation.proof === 'OBSERVED_EXPECTATION';
185
+ }
186
+
@@ -0,0 +1,305 @@
1
+ /**
2
+ * OBSERVED EXPECTATIONS
3
+ *
4
+ * Derives strict, evidence-backed expectations from runtime signals
5
+ * (DOM attributes, navigation events, network requests, validation feedback,
6
+ * and supported state mutations) to reduce UNPROVEN_RESULT volume.
7
+ *
8
+ * Guarantees:
9
+ * - Only uses concrete, observable evidence (no heuristics)
10
+ * - Never writes to source manifests; data lives in runtime traces
11
+ */
12
+
13
+ import { getUrlPath } from '../detect/evidence-validator.js';
14
+ import { isExternalUrl } from './domain-boundary.js';
15
+
16
+ function normalizePath(path) {
17
+ if (!path) return '';
18
+ if (path === '/') return '/';
19
+ return path.replace(/\/$/, '') || '/';
20
+ }
21
+
22
+ function safeResolvePath(target, baseUrl, baseOrigin) {
23
+ if (!target) return null;
24
+ try {
25
+ const resolved = new URL(target, baseUrl || baseOrigin || undefined);
26
+ if (baseOrigin && isExternalUrl(resolved.href, baseOrigin)) {
27
+ return null;
28
+ }
29
+ return resolved.pathname || '/';
30
+ } catch (err) {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function hasTemplateToken(url) {
36
+ if (!url) return false;
37
+ try {
38
+ const parsed = new URL(url);
39
+ const path = parsed.pathname || '';
40
+ return /[{}`*]/.test(path) || /:\w+/.test(path);
41
+ } catch (err) {
42
+ return /[{}`*]/.test(url);
43
+ }
44
+ }
45
+
46
+ function buildNavigationExpectation(interaction, trace, baseOrigin) {
47
+ const beforeUrl = trace.before?.url || '';
48
+ const navSensor = trace.sensors?.navigation || {};
49
+ const attributeSource = interaction.dataHref
50
+ ? 'data-href'
51
+ : interaction.href
52
+ ? 'href'
53
+ : interaction.formAction
54
+ ? 'action'
55
+ : null;
56
+
57
+ let targetPath = null;
58
+ let source = attributeSource;
59
+
60
+ if (attributeSource === 'data-href') {
61
+ targetPath = safeResolvePath(interaction.dataHref, beforeUrl, baseOrigin);
62
+ } else if (attributeSource === 'href') {
63
+ targetPath = safeResolvePath(interaction.href, beforeUrl, baseOrigin);
64
+ } else if (attributeSource === 'action') {
65
+ targetPath = safeResolvePath(interaction.formAction, beforeUrl, baseOrigin);
66
+ }
67
+
68
+ if (!targetPath && navSensor.urlChanged && navSensor.afterUrl) {
69
+ const observedPath = safeResolvePath(navSensor.afterUrl, beforeUrl, baseOrigin);
70
+ if (observedPath) {
71
+ targetPath = observedPath;
72
+ source = 'navigation_event';
73
+ }
74
+ }
75
+
76
+ if (!targetPath) {
77
+ return null;
78
+ }
79
+
80
+ return {
81
+ id: `obs-nav-${Date.now()}-${(interaction.selector || 'nav').replace(/[^a-zA-Z0-9]/g, '').slice(-8)}`,
82
+ type: 'navigation',
83
+ expectationStrength: 'OBSERVED',
84
+ expectedTargetPath: targetPath,
85
+ evidence: {
86
+ selector: interaction.selector,
87
+ attributeSource: source,
88
+ observedUrl: targetPath,
89
+ sourcePage: beforeUrl
90
+ },
91
+ sourcePage: beforeUrl,
92
+ outcome: null,
93
+ reason: null,
94
+ repeatAttempted: false,
95
+ repeated: false,
96
+ confidenceLevel: 'LOW'
97
+ };
98
+ }
99
+
100
+ function buildNetworkExpectation(interaction, trace) {
101
+ const beforeUrl = trace.before?.url || '';
102
+ const network = trace.sensors?.network || {};
103
+
104
+ // Network sensor provides firstRequestUrl and observedRequestUrls
105
+ const observedUrl = network.firstRequestUrl || network.observedRequestUrls?.[0] || null;
106
+
107
+ if (!network.totalRequests || network.totalRequests <= 0) return null;
108
+ if (!observedUrl || hasTemplateToken(observedUrl)) return null;
109
+
110
+ return {
111
+ id: `obs-net-${Date.now()}-${(interaction.selector || 'net').replace(/[^a-zA-Z0-9]/g, '').slice(-8)}`,
112
+ type: 'network_action',
113
+ expectationStrength: 'OBSERVED',
114
+ expectedRequestUrl: observedUrl,
115
+ evidence: {
116
+ selector: interaction.selector,
117
+ attributeSource: 'network_request',
118
+ observedRequestUrl: observedUrl,
119
+ sourcePage: beforeUrl
120
+ },
121
+ sourcePage: beforeUrl,
122
+ outcome: null,
123
+ reason: null,
124
+ repeatAttempted: false,
125
+ repeated: false,
126
+ confidenceLevel: 'LOW'
127
+ };
128
+ }
129
+
130
+ function buildValidationExpectation(interaction, trace) {
131
+ const beforeUrl = trace.before?.url || '';
132
+ const uiSignals = trace.sensors?.uiSignals || {};
133
+ const validationDetected = uiSignals.after?.validationFeedbackDetected === true;
134
+
135
+ if (!validationDetected) return null;
136
+
137
+ return {
138
+ id: `obs-val-${Date.now()}-${(interaction.selector || 'val').replace(/[^a-zA-Z0-9]/g, '').slice(-8)}`,
139
+ type: 'validation_block',
140
+ expectationStrength: 'OBSERVED',
141
+ evidence: {
142
+ selector: interaction.selector,
143
+ attributeSource: 'validation_feedback',
144
+ validationDetected: true,
145
+ sourcePage: beforeUrl
146
+ },
147
+ sourcePage: beforeUrl,
148
+ outcome: null,
149
+ reason: null,
150
+ repeatAttempted: false,
151
+ repeated: false,
152
+ confidenceLevel: 'LOW'
153
+ };
154
+ }
155
+
156
+ function buildStateExpectation(interaction, trace) {
157
+ const beforeUrl = trace.before?.url || '';
158
+ const state = trace.sensors?.state || {};
159
+ const hasChange = state.available && Array.isArray(state.changed) && state.changed.length > 0;
160
+
161
+ if (!hasChange) return null;
162
+
163
+ return {
164
+ id: `obs-state-${Date.now()}-${(interaction.selector || 'state').replace(/[^a-zA-Z0-9]/g, '').slice(-8)}`,
165
+ type: 'state_action',
166
+ expectationStrength: 'OBSERVED',
167
+ expectedStateKey: state.changed[0],
168
+ evidence: {
169
+ selector: interaction.selector,
170
+ attributeSource: 'state_change',
171
+ stateKeysChanged: state.changed,
172
+ sourcePage: beforeUrl,
173
+ storeType: state.storeType || null
174
+ },
175
+ sourcePage: beforeUrl,
176
+ outcome: null,
177
+ reason: null,
178
+ repeatAttempted: false,
179
+ repeated: false,
180
+ confidenceLevel: 'LOW'
181
+ };
182
+ }
183
+
184
+ export function deriveObservedExpectation(interaction, trace, baseOrigin) {
185
+ if (!trace || !interaction) return null;
186
+
187
+ const enrichedInteraction = {
188
+ selector: interaction.selector,
189
+ href: interaction.href || trace.interaction?.href,
190
+ dataHref: interaction.dataHref || trace.interaction?.dataHref,
191
+ formAction: interaction.formAction || trace.interaction?.formAction
192
+ };
193
+
194
+ const builders = [
195
+ () => buildNavigationExpectation(enrichedInteraction, trace, baseOrigin),
196
+ () => buildNetworkExpectation(interaction, trace),
197
+ () => buildValidationExpectation(interaction, trace),
198
+ () => buildStateExpectation(interaction, trace)
199
+ ];
200
+
201
+ for (const build of builders) {
202
+ const expectation = build();
203
+ if (expectation) {
204
+ const evaluation = evaluateObservedExpectation(expectation, trace);
205
+ expectation.outcome = evaluation.outcome;
206
+ expectation.reason = evaluation.reason;
207
+ expectation.confidenceLevel = 'LOW';
208
+ return expectation;
209
+ }
210
+ }
211
+
212
+ return null;
213
+ }
214
+
215
+ export function evaluateObservedExpectation(expectation, trace) {
216
+ if (!expectation || !trace) {
217
+ return { outcome: 'OBSERVED_BREAK', reason: 'missing_expectation' };
218
+ }
219
+
220
+ if (expectation.type === 'navigation') {
221
+ const afterPath = normalizePath(getUrlPath(trace.after?.url || ''));
222
+ const targetPath = normalizePath(expectation.expectedTargetPath || '');
223
+ const navSensor = trace.sensors?.navigation || {};
224
+ const urlChanged = navSensor.urlChanged === true || (trace.before?.url && trace.after?.url && trace.before.url !== trace.after.url);
225
+
226
+ if (targetPath && afterPath === targetPath) {
227
+ return { outcome: 'VERIFIED', reason: null };
228
+ }
229
+
230
+ if (urlChanged) {
231
+ return { outcome: 'OBSERVED_BREAK', reason: 'navigation_target_mismatch' };
232
+ }
233
+
234
+ return { outcome: 'OBSERVED_BREAK', reason: 'navigation_not_observed' };
235
+ }
236
+
237
+ if (expectation.type === 'network_action') {
238
+ const network = trace.sensors?.network || {};
239
+ const expectedUrl = expectation.expectedRequestUrl || '';
240
+
241
+ if (network.totalRequests && network.totalRequests > 0) {
242
+ // Check if expected URL was requested
243
+ const requestUrls = network.observedRequestUrls || [];
244
+ const firstRequestUrl = network.firstRequestUrl || '';
245
+ const allRequestUrls = [...requestUrls, firstRequestUrl].filter(Boolean);
246
+
247
+ const expectedUrlFound = expectedUrl && allRequestUrls.some(url =>
248
+ url && url.includes(expectedUrl)
249
+ );
250
+
251
+ if (expectedUrlFound) {
252
+ return { outcome: 'VERIFIED', reason: null };
253
+ }
254
+
255
+ // Request occurred but not the expected one
256
+ return { outcome: 'OBSERVED_BREAK', reason: 'network_request_url_mismatch' };
257
+ }
258
+
259
+ return { outcome: 'OBSERVED_BREAK', reason: 'network_request_missing' };
260
+ }
261
+
262
+ if (expectation.type === 'validation_block') {
263
+ const uiSignals = trace.sensors?.uiSignals || {};
264
+ const network = trace.sensors?.network || {};
265
+ const validationDetected = uiSignals.after?.validationFeedbackDetected === true;
266
+ const beforePath = normalizePath(getUrlPath(trace.before?.url || ''));
267
+ const afterPath = normalizePath(getUrlPath(trace.after?.url || ''));
268
+
269
+ if (validationDetected && beforePath === afterPath && (network.totalRequests || 0) === 0) {
270
+ return { outcome: 'VERIFIED', reason: null };
271
+ }
272
+
273
+ if (!validationDetected) {
274
+ return { outcome: 'OBSERVED_BREAK', reason: 'validation_feedback_missing' };
275
+ }
276
+
277
+ return { outcome: 'OBSERVED_BREAK', reason: 'validation_not_blocked' };
278
+ }
279
+
280
+ if (expectation.type === 'state_action') {
281
+ const state = trace.sensors?.state || {};
282
+ if (state.available && Array.isArray(state.changed) && state.changed.includes(expectation.expectedStateKey)) {
283
+ return { outcome: 'VERIFIED', reason: null };
284
+ }
285
+ return { outcome: 'OBSERVED_BREAK', reason: 'state_not_changed' };
286
+ }
287
+
288
+ return { outcome: 'OBSERVED_BREAK', reason: 'unknown_expectation' };
289
+ }
290
+
291
+ export function shouldAttemptRepeatObservedExpectation(expectation, trace) {
292
+ if (!expectation) return false;
293
+ if (expectation.outcome !== 'VERIFIED') return false;
294
+ if (expectation.repeatAttempted === true) return false; // Already attempted
295
+
296
+ // Only repeat when we stayed on the same page to avoid altering traversal
297
+ const beforeUrl = trace.before?.url || '';
298
+ const afterUrl = trace.after?.url || '';
299
+ const stayedOnPage = normalizePath(getUrlPath(beforeUrl)) === normalizePath(getUrlPath(afterUrl));
300
+
301
+ if (!stayedOnPage) return false;
302
+
303
+ // Only repeat non-navigation expectations (navigation would change page state)
304
+ return expectation.type === 'network_action' || expectation.type === 'validation_block' || expectation.type === 'state_action';
305
+ }