@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,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
- await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
16
- await page.waitForTimeout(STABLE_WAIT_MS);
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
- async stopWindow(windowId, page) {
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
- return `${urlObj.protocol}//${urlObj.host}${urlObj.port ? `:${urlObj.port}` : ''}`;
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) {