@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.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +14 -36
- package/src/cli/commands/default.js +523 -0
- package/src/cli/commands/doctor.js +165 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +402 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +296 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +34 -0
- package/src/cli/util/expectation-extractor.js +378 -0
- package/src/cli/util/findings-writer.js +31 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +366 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +29 -0
- package/src/cli/util/project-discovery.js +277 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/summary-writer.js +32 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +101 -0
- package/src/verax/cli/wizard.js +98 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +403 -0
- package/src/verax/core/incremental-store.js +237 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +521 -0
- package/src/verax/detect/comparison.js +2 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +177 -0
- package/src/verax/detect/expectation-model.js +194 -172
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +44 -8
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +172 -286
- package/src/verax/detect/interactive-findings.js +613 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/verdict-engine.js +563 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/index.js +90 -14
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +579 -0
- package/src/verax/intel/vue-router-extractor.js +323 -0
- package/src/verax/learn/action-contract-extractor.js +335 -101
- package/src/verax/learn/ast-contract-extractor.js +95 -5
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/manifest-writer.js +97 -47
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +27 -96
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +112 -4
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +10 -5
- package/src/verax/observe/console-sensor.js +1 -17
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +512 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +643 -275
- package/src/verax/observe/index.js +908 -27
- package/src/verax/observe/index.js.backup +1 -0
- package/src/verax/observe/interaction-discovery.js +365 -14
- package/src/verax/observe/interaction-runner.js +563 -198
- package/src/verax/observe/loading-sensor.js +139 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +37 -17
- package/src/verax/observe/state-sensor.js +389 -0
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +61 -20
- package/src/verax/observe/ui-signal-sensor.js +136 -17
- package/src/verax/scan-summary-writer.js +77 -15
- package/src/verax/shared/artifact-manager.js +110 -8
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +170 -0
- package/src/verax/shared/dynamic-route-utils.js +218 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +14 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +65 -0
- package/src/verax/validate/context-validator.js +244 -0
- 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
|
+
}
|