@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,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARIA Announcement Sensor
|
|
3
|
+
* Tracks ARIA live regions, status/alert roles, and announcements
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class AriaSensor {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.ariaStateBefore = null;
|
|
9
|
+
this.ariaStateAfter = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Capture ARIA state before interaction
|
|
14
|
+
*/
|
|
15
|
+
async captureBefore(page) {
|
|
16
|
+
const ariaData = await page.evaluate(() => {
|
|
17
|
+
const result = {
|
|
18
|
+
liveRegions: [],
|
|
19
|
+
statusRoles: [],
|
|
20
|
+
alerts: [],
|
|
21
|
+
ariaBusyElements: [],
|
|
22
|
+
ariaLive: [],
|
|
23
|
+
announcements: []
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Find all live regions
|
|
27
|
+
const liveRegions = document.querySelectorAll('[aria-live]');
|
|
28
|
+
liveRegions.forEach(el => {
|
|
29
|
+
result.liveRegions.push({
|
|
30
|
+
selector: generateSelector(el),
|
|
31
|
+
ariaLive: el.getAttribute('aria-live'),
|
|
32
|
+
text: el.textContent?.slice(0, 100) || '',
|
|
33
|
+
ariaAtomic: el.getAttribute('aria-atomic'),
|
|
34
|
+
ariaRelevant: el.getAttribute('aria-relevant')
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Find status and alert roles
|
|
39
|
+
const statusAlerts = document.querySelectorAll('[role="status"], [role="alert"]');
|
|
40
|
+
statusAlerts.forEach(el => {
|
|
41
|
+
result.statusRoles.push({
|
|
42
|
+
selector: generateSelector(el),
|
|
43
|
+
role: el.getAttribute('role'),
|
|
44
|
+
text: el.textContent?.slice(0, 100) || '',
|
|
45
|
+
ariaLive: el.getAttribute('aria-live')
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Find aria-busy elements
|
|
50
|
+
const busyElements = document.querySelectorAll('[aria-busy="true"]');
|
|
51
|
+
busyElements.forEach(el => {
|
|
52
|
+
result.ariaBusyElements.push({
|
|
53
|
+
selector: generateSelector(el),
|
|
54
|
+
ariaBusy: el.getAttribute('aria-busy')
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
|
|
60
|
+
function generateSelector(el) {
|
|
61
|
+
if (el.id) return `#${el.id}`;
|
|
62
|
+
if (el.className) {
|
|
63
|
+
const classes = Array.from(el.classList || []).slice(0, 2).join('.');
|
|
64
|
+
return el.tagName.toLowerCase() + (classes ? `.${classes}` : '');
|
|
65
|
+
}
|
|
66
|
+
return el.tagName.toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.ariaStateBefore = ariaData;
|
|
71
|
+
return ariaData;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Capture ARIA state after interaction
|
|
76
|
+
*/
|
|
77
|
+
async captureAfter(page) {
|
|
78
|
+
const ariaData = await page.evaluate(() => {
|
|
79
|
+
const result = {
|
|
80
|
+
liveRegions: [],
|
|
81
|
+
statusRoles: [],
|
|
82
|
+
alerts: [],
|
|
83
|
+
ariaBusyElements: [],
|
|
84
|
+
ariaLive: [],
|
|
85
|
+
announcements: []
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Find all live regions
|
|
89
|
+
const liveRegions = document.querySelectorAll('[aria-live]');
|
|
90
|
+
liveRegions.forEach(el => {
|
|
91
|
+
result.liveRegions.push({
|
|
92
|
+
selector: generateSelector(el),
|
|
93
|
+
ariaLive: el.getAttribute('aria-live'),
|
|
94
|
+
text: el.textContent?.slice(0, 100) || '',
|
|
95
|
+
ariaAtomic: el.getAttribute('aria-atomic'),
|
|
96
|
+
ariaRelevant: el.getAttribute('aria-relevant')
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Find status and alert roles
|
|
101
|
+
const statusAlerts = document.querySelectorAll('[role="status"], [role="alert"]');
|
|
102
|
+
statusAlerts.forEach(el => {
|
|
103
|
+
result.statusRoles.push({
|
|
104
|
+
selector: generateSelector(el),
|
|
105
|
+
role: el.getAttribute('role'),
|
|
106
|
+
text: el.textContent?.slice(0, 100) || '',
|
|
107
|
+
ariaLive: el.getAttribute('aria-live')
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Find aria-busy elements
|
|
112
|
+
const busyElements = document.querySelectorAll('[aria-busy="true"]');
|
|
113
|
+
busyElements.forEach(el => {
|
|
114
|
+
result.ariaBusyElements.push({
|
|
115
|
+
selector: generateSelector(el),
|
|
116
|
+
ariaBusy: el.getAttribute('aria-busy')
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
|
|
122
|
+
function generateSelector(el) {
|
|
123
|
+
if (el.id) return `#${el.id}`;
|
|
124
|
+
if (el.className) {
|
|
125
|
+
const classes = Array.from(el.classList || []).slice(0, 2).join('.');
|
|
126
|
+
return el.tagName.toLowerCase() + (classes ? `.${classes}` : '');
|
|
127
|
+
}
|
|
128
|
+
return el.tagName.toLowerCase();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
this.ariaStateAfter = ariaData;
|
|
133
|
+
return ariaData;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Detect if ARIA state changed (live region updates, role changes, etc)
|
|
138
|
+
*/
|
|
139
|
+
detectAriaChange() {
|
|
140
|
+
if (!this.ariaStateBefore || !this.ariaStateAfter) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check if live region text changed
|
|
145
|
+
const beforeLiveText = this.ariaStateBefore.liveRegions.map(r => r.text).join('|');
|
|
146
|
+
const afterLiveText = this.ariaStateAfter.liveRegions.map(r => r.text).join('|');
|
|
147
|
+
|
|
148
|
+
if (beforeLiveText !== afterLiveText) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check if status/alert role text changed
|
|
153
|
+
const beforeStatusText = this.ariaStateBefore.statusRoles.map(r => r.text).join('|');
|
|
154
|
+
const afterStatusText = this.ariaStateAfter.statusRoles.map(r => r.text).join('|');
|
|
155
|
+
|
|
156
|
+
if (beforeStatusText !== afterStatusText) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check if aria-busy state changed
|
|
161
|
+
const beforeBusy = this.ariaStateBefore.ariaBusyElements.length;
|
|
162
|
+
const afterBusy = this.ariaStateAfter.ariaBusyElements.length;
|
|
163
|
+
|
|
164
|
+
if (beforeBusy !== afterBusy) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Check if ARIA announcement should have occurred but didn't
|
|
173
|
+
*/
|
|
174
|
+
detectMissingAnnouncement(eventType) {
|
|
175
|
+
// eventType: 'submit', 'network_success', 'network_error', 'validation_error', etc
|
|
176
|
+
// These meaningful events should typically trigger ARIA announcements
|
|
177
|
+
|
|
178
|
+
if (!this.ariaStateBefore || !this.ariaStateAfter) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check if any live region exists (at least one should for accessibility)
|
|
183
|
+
const hasLiveRegion = this.ariaStateAfter.liveRegions.length > 0;
|
|
184
|
+
const hasStatus = this.ariaStateAfter.statusRoles.length > 0;
|
|
185
|
+
|
|
186
|
+
if (!hasLiveRegion && !hasStatus) {
|
|
187
|
+
return true; // No ARIA announcement mechanism present
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check if announcement actually changed
|
|
191
|
+
const ariaChanged = this.detectAriaChange();
|
|
192
|
+
|
|
193
|
+
// For meaningful events, ARIA should have changed
|
|
194
|
+
if (!ariaChanged && (eventType === 'submit' || eventType === 'network_success' || eventType === 'network_error')) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get ARIA diff for evidence
|
|
203
|
+
*/
|
|
204
|
+
getAriaDiff() {
|
|
205
|
+
return {
|
|
206
|
+
before: this.ariaStateBefore,
|
|
207
|
+
after: this.ariaStateAfter,
|
|
208
|
+
changed: this.detectAriaChange()
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { chromium } from 'playwright';
|
|
2
|
-
|
|
3
|
-
const STABLE_WAIT_MS = 2000;
|
|
2
|
+
import { DEFAULT_SCAN_BUDGET } from '../shared/scan-budget.js';
|
|
4
3
|
|
|
5
4
|
export async function createBrowser() {
|
|
6
5
|
const browser = await chromium.launch({ headless: true });
|
|
@@ -11,9 +10,15 @@ export async function createBrowser() {
|
|
|
11
10
|
return { browser, page };
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
export async function navigateToUrl(page, url) {
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
export async function navigateToUrl(page, url, scanBudget = DEFAULT_SCAN_BUDGET) {
|
|
14
|
+
let stableWait = scanBudget.navigationStableWaitMs;
|
|
15
|
+
try {
|
|
16
|
+
if (url.startsWith('file:') || url.includes('localhost:') || url.includes('127.0.0.1')) {
|
|
17
|
+
stableWait = 200; // Short wait for local fixtures
|
|
18
|
+
}
|
|
19
|
+
} catch {}
|
|
20
|
+
await page.goto(url, { waitUntil: 'networkidle', timeout: scanBudget.initialNavigationTimeoutMs });
|
|
21
|
+
await page.waitForTimeout(stableWait);
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
export async function closeBrowser(browser) {
|
|
@@ -104,7 +104,7 @@ export class ConsoleSensor {
|
|
|
104
104
|
/**
|
|
105
105
|
* Stop monitoring and return a summary for the window.
|
|
106
106
|
*/
|
|
107
|
-
|
|
107
|
+
stopWindow(windowId, page) {
|
|
108
108
|
const state = this.windows.get(windowId);
|
|
109
109
|
if (!state) {
|
|
110
110
|
return this.getEmptySummary();
|
|
@@ -112,22 +112,6 @@ export class ConsoleSensor {
|
|
|
112
112
|
|
|
113
113
|
state.cleanup();
|
|
114
114
|
|
|
115
|
-
// Collect any unhandled rejections that were captured
|
|
116
|
-
let capturedRejections = [];
|
|
117
|
-
try {
|
|
118
|
-
capturedRejections = await page.evaluate(() => {
|
|
119
|
-
return window.__unhandledRejections || [];
|
|
120
|
-
});
|
|
121
|
-
} catch {
|
|
122
|
-
// Page may not have this
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Merge captured rejections with ones we heard about
|
|
126
|
-
state.unhandledRejections = [
|
|
127
|
-
...state.unhandledRejections,
|
|
128
|
-
...capturedRejections
|
|
129
|
-
].slice(0, this.maxErrorsToKeep);
|
|
130
|
-
|
|
131
115
|
const summary = {
|
|
132
116
|
windowId,
|
|
133
117
|
errorCount: state.consoleErrors.length + state.pageErrors.length,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export function getBaseOrigin(url) {
|
|
2
2
|
try {
|
|
3
3
|
const urlObj = new URL(url);
|
|
4
|
-
|
|
4
|
+
if (urlObj.protocol === 'file:') {
|
|
5
|
+
return 'file://';
|
|
6
|
+
}
|
|
7
|
+
return urlObj.origin;
|
|
5
8
|
} catch (error) {
|
|
6
9
|
return null;
|
|
7
10
|
}
|
|
@@ -12,6 +15,12 @@ export function isExternalUrl(url, baseOrigin) {
|
|
|
12
15
|
|
|
13
16
|
try {
|
|
14
17
|
const urlObj = new URL(url);
|
|
18
|
+
// Special-case file protocol: treat all file:// URLs as same-origin
|
|
19
|
+
const isFileProtocol = urlObj.protocol === 'file:';
|
|
20
|
+
const baseIsFile = baseOrigin.startsWith('file:');
|
|
21
|
+
if (isFileProtocol && baseIsFile) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
15
24
|
const urlOrigin = urlObj.origin;
|
|
16
25
|
return urlOrigin !== baseOrigin;
|
|
17
26
|
} catch (error) {
|