@veraxhq/verax 0.1.0 → 0.2.1

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 (135) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +24 -36
  4. package/src/cli/commands/default.js +681 -0
  5. package/src/cli/commands/doctor.js +197 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +586 -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 +297 -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 +110 -0
  14. package/src/cli/util/expectation-extractor.js +388 -0
  15. package/src/cli/util/findings-writer.js +32 -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 +412 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +30 -0
  21. package/src/cli/util/project-discovery.js +297 -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/runtime-budget.js +147 -0
  26. package/src/cli/util/summary-writer.js +43 -0
  27. package/src/types/global.d.ts +28 -0
  28. package/src/types/ts-ast.d.ts +24 -0
  29. package/src/verax/cli/ci-summary.js +35 -0
  30. package/src/verax/cli/context-explanation.js +89 -0
  31. package/src/verax/cli/doctor.js +277 -0
  32. package/src/verax/cli/error-normalizer.js +154 -0
  33. package/src/verax/cli/explain-output.js +105 -0
  34. package/src/verax/cli/finding-explainer.js +130 -0
  35. package/src/verax/cli/init.js +237 -0
  36. package/src/verax/cli/run-overview.js +163 -0
  37. package/src/verax/cli/url-safety.js +111 -0
  38. package/src/verax/cli/wizard.js +109 -0
  39. package/src/verax/cli/zero-findings-explainer.js +57 -0
  40. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  41. package/src/verax/core/action-classifier.js +86 -0
  42. package/src/verax/core/budget-engine.js +218 -0
  43. package/src/verax/core/canonical-outcomes.js +157 -0
  44. package/src/verax/core/decision-snapshot.js +335 -0
  45. package/src/verax/core/determinism-model.js +432 -0
  46. package/src/verax/core/incremental-store.js +245 -0
  47. package/src/verax/core/invariants.js +356 -0
  48. package/src/verax/core/promise-model.js +230 -0
  49. package/src/verax/core/replay-validator.js +350 -0
  50. package/src/verax/core/replay.js +222 -0
  51. package/src/verax/core/run-id.js +175 -0
  52. package/src/verax/core/run-manifest.js +99 -0
  53. package/src/verax/core/silence-impact.js +369 -0
  54. package/src/verax/core/silence-model.js +523 -0
  55. package/src/verax/detect/comparison.js +7 -34
  56. package/src/verax/detect/confidence-engine.js +764 -329
  57. package/src/verax/detect/detection-engine.js +293 -0
  58. package/src/verax/detect/evidence-index.js +127 -0
  59. package/src/verax/detect/expectation-model.js +241 -168
  60. package/src/verax/detect/explanation-helpers.js +187 -0
  61. package/src/verax/detect/finding-detector.js +450 -0
  62. package/src/verax/detect/findings-writer.js +41 -12
  63. package/src/verax/detect/flow-detector.js +366 -0
  64. package/src/verax/detect/index.js +200 -288
  65. package/src/verax/detect/interactive-findings.js +612 -0
  66. package/src/verax/detect/signal-mapper.js +308 -0
  67. package/src/verax/detect/skip-classifier.js +4 -4
  68. package/src/verax/detect/verdict-engine.js +561 -0
  69. package/src/verax/evidence-index-writer.js +61 -0
  70. package/src/verax/flow/flow-engine.js +3 -2
  71. package/src/verax/flow/flow-spec.js +1 -2
  72. package/src/verax/index.js +103 -15
  73. package/src/verax/intel/effect-detector.js +368 -0
  74. package/src/verax/intel/handler-mapper.js +249 -0
  75. package/src/verax/intel/index.js +281 -0
  76. package/src/verax/intel/route-extractor.js +280 -0
  77. package/src/verax/intel/ts-program.js +256 -0
  78. package/src/verax/intel/vue-navigation-extractor.js +642 -0
  79. package/src/verax/intel/vue-router-extractor.js +325 -0
  80. package/src/verax/learn/action-contract-extractor.js +338 -104
  81. package/src/verax/learn/ast-contract-extractor.js +148 -6
  82. package/src/verax/learn/flow-extractor.js +172 -0
  83. package/src/verax/learn/index.js +36 -2
  84. package/src/verax/learn/manifest-writer.js +122 -58
  85. package/src/verax/learn/project-detector.js +40 -0
  86. package/src/verax/learn/route-extractor.js +28 -97
  87. package/src/verax/learn/route-validator.js +8 -7
  88. package/src/verax/learn/state-extractor.js +212 -0
  89. package/src/verax/learn/static-extractor-navigation.js +114 -0
  90. package/src/verax/learn/static-extractor-validation.js +88 -0
  91. package/src/verax/learn/static-extractor.js +119 -10
  92. package/src/verax/learn/truth-assessor.js +24 -21
  93. package/src/verax/learn/ts-contract-resolver.js +14 -12
  94. package/src/verax/observe/aria-sensor.js +211 -0
  95. package/src/verax/observe/browser.js +30 -6
  96. package/src/verax/observe/console-sensor.js +2 -18
  97. package/src/verax/observe/domain-boundary.js +10 -1
  98. package/src/verax/observe/expectation-executor.js +513 -0
  99. package/src/verax/observe/flow-matcher.js +143 -0
  100. package/src/verax/observe/focus-sensor.js +196 -0
  101. package/src/verax/observe/human-driver.js +660 -273
  102. package/src/verax/observe/index.js +910 -26
  103. package/src/verax/observe/interaction-discovery.js +378 -15
  104. package/src/verax/observe/interaction-runner.js +562 -197
  105. package/src/verax/observe/loading-sensor.js +145 -0
  106. package/src/verax/observe/navigation-sensor.js +255 -0
  107. package/src/verax/observe/network-sensor.js +55 -7
  108. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  109. package/src/verax/observe/observed-expectation.js +305 -0
  110. package/src/verax/observe/page-frontier.js +234 -0
  111. package/src/verax/observe/settle.js +38 -17
  112. package/src/verax/observe/state-sensor.js +393 -0
  113. package/src/verax/observe/state-ui-sensor.js +7 -1
  114. package/src/verax/observe/timing-sensor.js +228 -0
  115. package/src/verax/observe/traces-writer.js +73 -21
  116. package/src/verax/observe/ui-signal-sensor.js +143 -17
  117. package/src/verax/scan-summary-writer.js +80 -15
  118. package/src/verax/shared/artifact-manager.js +111 -9
  119. package/src/verax/shared/budget-profiles.js +136 -0
  120. package/src/verax/shared/caching.js +1 -1
  121. package/src/verax/shared/ci-detection.js +39 -0
  122. package/src/verax/shared/config-loader.js +169 -0
  123. package/src/verax/shared/dynamic-route-utils.js +224 -0
  124. package/src/verax/shared/expectation-coverage.js +44 -0
  125. package/src/verax/shared/expectation-prover.js +81 -0
  126. package/src/verax/shared/expectation-tracker.js +201 -0
  127. package/src/verax/shared/expectations-writer.js +60 -0
  128. package/src/verax/shared/first-run.js +44 -0
  129. package/src/verax/shared/progress-reporter.js +171 -0
  130. package/src/verax/shared/retry-policy.js +9 -1
  131. package/src/verax/shared/root-artifacts.js +49 -0
  132. package/src/verax/shared/scan-budget.js +86 -0
  133. package/src/verax/shared/url-normalizer.js +162 -0
  134. package/src/verax/shared/zip-artifacts.js +66 -0
  135. package/src/verax/validate/context-validator.js +244 -0
@@ -0,0 +1,297 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'fs';
2
+ import { resolve, dirname } from 'path';
3
+
4
+ /**
5
+ * Project Discovery Module
6
+ * Detects framework, router, source root, and dev server configuration
7
+ *
8
+ * @typedef {Object} ProjectProfile
9
+ * @property {string} framework
10
+ * @property {string|null} router
11
+ * @property {string} sourceRoot
12
+ * @property {string} packageManager
13
+ * @property {{dev: string|null, build: string|null, start: string|null}} scripts
14
+ * @property {string} detectedAt
15
+ * @property {string|null} packageJsonPath
16
+ * @property {number} [fileCount] - Optional file count for budget calculation
17
+ */
18
+
19
+ /**
20
+ * @param {string} srcPath
21
+ * @returns {Promise<ProjectProfile>}
22
+ */
23
+ export async function discoverProject(srcPath) {
24
+ const projectRoot = resolve(srcPath);
25
+
26
+ // Find the nearest package.json
27
+ const packageJsonPath = findPackageJson(projectRoot);
28
+
29
+ // If there's a package.json, use its directory
30
+ // Otherwise, use the srcPath (even if it's a static HTML project)
31
+ const projectDir = packageJsonPath ? dirname(packageJsonPath) : projectRoot;
32
+
33
+ let packageJson = null;
34
+ if (packageJsonPath && existsSync(packageJsonPath)) {
35
+ try {
36
+ packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
37
+ } catch (error) {
38
+ packageJson = null;
39
+ }
40
+ }
41
+
42
+ // Detect framework
43
+ const framework = detectFramework(projectDir, packageJson);
44
+ const router = detectRouter(framework, projectDir);
45
+
46
+ // Determine package manager
47
+ const packageManager = detectPackageManager(projectDir);
48
+
49
+ // Extract scripts
50
+ const scripts = {
51
+ dev: packageJson?.scripts?.dev || null,
52
+ build: packageJson?.scripts?.build || null,
53
+ start: packageJson?.scripts?.start || null,
54
+ };
55
+
56
+ return {
57
+ framework,
58
+ router,
59
+ sourceRoot: projectDir,
60
+ packageManager,
61
+ scripts,
62
+ detectedAt: new Date().toISOString(),
63
+ packageJsonPath,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Find the nearest package.json by walking up directories
69
+ */
70
+ function findPackageJson(startPath) {
71
+ let currentPath = resolve(startPath);
72
+
73
+ // First check if package.json exists in startPath itself
74
+ const immediatePackage = resolve(currentPath, 'package.json');
75
+ if (existsSync(immediatePackage)) {
76
+ return immediatePackage;
77
+ }
78
+
79
+ // For static HTML projects, don't walk up - use the startPath as project root
80
+ // This prevents finding parent package.json files that aren't relevant
81
+ if (hasStaticHtml(currentPath)) {
82
+ return null;
83
+ }
84
+
85
+ // Then walk up (limit to 5 levels for monorepos, not 10)
86
+ for (let i = 0; i < 5; i++) {
87
+ const parentPath = dirname(currentPath);
88
+ if (parentPath === currentPath) {
89
+ // Reached filesystem root
90
+ break;
91
+ }
92
+
93
+ currentPath = parentPath;
94
+ const packageJsonPath = resolve(currentPath, 'package.json');
95
+ if (existsSync(packageJsonPath)) {
96
+ return packageJsonPath;
97
+ }
98
+ }
99
+
100
+ return null;
101
+ }
102
+
103
+ /**
104
+ * Detect the framework type
105
+ */
106
+ function detectFramework(projectDir, packageJson) {
107
+ // Check for Next.js
108
+ if (hasNextJs(projectDir, packageJson)) {
109
+ return 'nextjs';
110
+ }
111
+
112
+ // Check for Vite + React
113
+ if (hasViteReact(projectDir, packageJson)) {
114
+ return 'react-vite';
115
+ }
116
+
117
+ // Check for Create React App
118
+ if (hasCreateReactApp(packageJson)) {
119
+ return 'react-cra';
120
+ }
121
+
122
+ // Check for static HTML
123
+ if (hasStaticHtml(projectDir)) {
124
+ return 'static-html';
125
+ }
126
+
127
+ // Unknown framework
128
+ return 'unknown';
129
+ }
130
+
131
+ /**
132
+ * Detect Next.js
133
+ */
134
+ function hasNextJs(projectDir, packageJson) {
135
+ // Check for next.config.js or next.config.mjs
136
+ const hasNextConfig = existsSync(resolve(projectDir, 'next.config.js')) ||
137
+ existsSync(resolve(projectDir, 'next.config.mjs')) ||
138
+ existsSync(resolve(projectDir, 'next.config.ts'));
139
+
140
+ if (hasNextConfig) {
141
+ return true;
142
+ }
143
+
144
+ // Check for 'next' dependency
145
+ if (packageJson?.dependencies?.next || packageJson?.devDependencies?.next) {
146
+ return true;
147
+ }
148
+
149
+ return false;
150
+ }
151
+
152
+ /**
153
+ * Detect router type for Next.js
154
+ */
155
+ function detectRouter(framework, projectDir) {
156
+ if (framework !== 'nextjs') {
157
+ return null;
158
+ }
159
+
160
+ // Check for /app directory (app router) - must contain actual files
161
+ const appPath = resolve(projectDir, 'app');
162
+ if (existsSync(appPath) && hasRouteFiles(appPath)) {
163
+ return 'app';
164
+ }
165
+
166
+ // Check for /pages directory (pages router)
167
+ if (existsSync(resolve(projectDir, 'pages'))) {
168
+ return 'pages';
169
+ }
170
+
171
+ return null;
172
+ }
173
+
174
+ /**
175
+ * Check if a directory contains route files (not just an empty scaffold)
176
+ */
177
+ function hasRouteFiles(dirPath) {
178
+ try {
179
+ const entries = readdirSync(dirPath);
180
+ return entries.some(entry => {
181
+ // Look for .js, .ts, .jsx, .tsx files (not just directories)
182
+ return /\.(js|ts|jsx|tsx)$/.test(entry);
183
+ });
184
+ } catch (error) {
185
+ return false;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Detect Vite + React
191
+ */
192
+ function hasViteReact(projectDir, packageJson) {
193
+ const hasViteConfig = existsSync(resolve(projectDir, 'vite.config.js')) ||
194
+ existsSync(resolve(projectDir, 'vite.config.ts')) ||
195
+ existsSync(resolve(projectDir, 'vite.config.mjs'));
196
+
197
+ if (!hasViteConfig) {
198
+ return false;
199
+ }
200
+
201
+ // Check for react dependency
202
+ if (packageJson?.dependencies?.react || packageJson?.devDependencies?.react) {
203
+ return true;
204
+ }
205
+
206
+ return false;
207
+ }
208
+
209
+ /**
210
+ * Detect Create React App
211
+ */
212
+ function hasCreateReactApp(packageJson) {
213
+ return !!(packageJson?.dependencies?.['react-scripts'] ||
214
+ packageJson?.devDependencies?.['react-scripts']);
215
+ }
216
+
217
+ /**
218
+ * Detect static HTML (no framework)
219
+ */
220
+ function hasStaticHtml(projectDir) {
221
+ return existsSync(resolve(projectDir, 'index.html'));
222
+ }
223
+
224
+ /**
225
+ * Detect package manager
226
+ */
227
+ function detectPackageManager(projectDir) {
228
+ // Check for pnpm-lock.yaml
229
+ if (existsSync(resolve(projectDir, 'pnpm-lock.yaml'))) {
230
+ return 'pnpm';
231
+ }
232
+
233
+ // Check for yarn.lock
234
+ if (existsSync(resolve(projectDir, 'yarn.lock'))) {
235
+ return 'yarn';
236
+ }
237
+
238
+ // Check for package-lock.json (npm)
239
+ if (existsSync(resolve(projectDir, 'package-lock.json'))) {
240
+ return 'npm';
241
+ }
242
+
243
+ // Default to npm if none found but package.json exists
244
+ if (existsSync(resolve(projectDir, 'package.json'))) {
245
+ return 'npm';
246
+ }
247
+
248
+ return 'unknown';
249
+ }
250
+
251
+ /**
252
+ * Get human-readable framework name
253
+ */
254
+ export function getFrameworkDisplayName(framework, router) {
255
+ if (framework === 'nextjs') {
256
+ const routerType = router === 'app' ? 'app router' : router === 'pages' ? 'pages router' : 'unknown router';
257
+ return `Next.js (${routerType})`;
258
+ }
259
+
260
+ if (framework === 'react-vite') {
261
+ return 'Vite + React';
262
+ }
263
+
264
+ if (framework === 'react-cra') {
265
+ return 'Create React App';
266
+ }
267
+
268
+ if (framework === 'static-html') {
269
+ return 'Static HTML';
270
+ }
271
+
272
+ return 'Unknown';
273
+ }
274
+
275
+ /**
276
+ * Extract probable port from dev script
277
+ */
278
+ export function extractPortFromScript(script) {
279
+ if (!script) return null;
280
+
281
+ // Common patterns:
282
+ // - --port 3000
283
+ // - -p 3000
284
+ // - PORT=3000
285
+
286
+ const portMatch = script.match(/(?:--port|-p)\s+(\d+)/);
287
+ if (portMatch) {
288
+ return parseInt(portMatch[1], 10);
289
+ }
290
+
291
+ const portEnvMatch = script.match(/PORT=(\d+)/);
292
+ if (portEnvMatch) {
293
+ return parseInt(portEnvMatch[1], 10);
294
+ }
295
+
296
+ return null;
297
+ }
@@ -0,0 +1,26 @@
1
+ import { atomicWriteJson } from './atomic-write.js';
2
+ import { resolve } from 'path';
3
+
4
+ /**
5
+ * Write project profile artifact
6
+ */
7
+ export function writeProjectJson(runPaths, projectProfile) {
8
+ const projectJsonPath = resolve(runPaths.baseDir, 'project.json');
9
+
10
+ const projectJson = {
11
+ framework: projectProfile.framework,
12
+ router: projectProfile.router,
13
+ sourceRoot: projectProfile.sourceRoot,
14
+ packageManager: projectProfile.packageManager,
15
+ scripts: {
16
+ dev: projectProfile.scripts.dev,
17
+ build: projectProfile.scripts.build,
18
+ start: projectProfile.scripts.start,
19
+ },
20
+ detectedAt: projectProfile.detectedAt,
21
+ };
22
+
23
+ atomicWriteJson(projectJsonPath, projectJson);
24
+
25
+ return projectJsonPath;
26
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Redaction utilities (Phase 8.2)
3
+ * Deterministic redaction for headers, URLs, bodies, console messages.
4
+ */
5
+
6
+ const REDACTED = '***REDACTED***';
7
+ const SENSITIVE_HEADERS = [
8
+ 'authorization',
9
+ 'cookie',
10
+ 'set-cookie',
11
+ 'x-api-key',
12
+ 'proxy-authorization',
13
+ ];
14
+
15
+ function ensureCounters(counters) {
16
+ if (!counters) return { headersRedacted: 0, tokensRedacted: 0 };
17
+ if (typeof counters.headersRedacted !== 'number') counters.headersRedacted = 0;
18
+ if (typeof counters.tokensRedacted !== 'number') counters.tokensRedacted = 0;
19
+ return counters;
20
+ }
21
+
22
+ export function redactHeaders(headers = {}, counters = { headersRedacted: 0, tokensRedacted: 0 }) {
23
+ const c = ensureCounters(counters);
24
+ const sanitized = {};
25
+ Object.entries(headers || {}).forEach(([key, value]) => {
26
+ const lower = key.toLowerCase();
27
+ if (SENSITIVE_HEADERS.includes(lower)) {
28
+ sanitized[key] = REDACTED;
29
+ c.headersRedacted += 1;
30
+ } else {
31
+ sanitized[key] = value;
32
+ }
33
+ });
34
+ return sanitized;
35
+ }
36
+
37
+ export function redactUrl(url, counters = { headersRedacted: 0, tokensRedacted: 0 }) {
38
+ if (url == null) return '';
39
+ return redactTokensInText(url, counters);
40
+ }
41
+
42
+ export function redactBody(body, counters = { headersRedacted: 0, tokensRedacted: 0 }) {
43
+ if (body == null) return body;
44
+
45
+ const c = ensureCounters(counters);
46
+
47
+ if (typeof body === 'string') {
48
+ return redactTokensInText(body, counters);
49
+ }
50
+
51
+ if (typeof body === 'object' && !Array.isArray(body)) {
52
+ // Handle plain objects by redacting sensitive property names
53
+ const redacted = {};
54
+ Object.entries(body).forEach(([key, value]) => {
55
+ const keyLower = key.toLowerCase();
56
+ if (keyLower === 'token' || keyLower === 'api_key' || keyLower === 'access_token' ||
57
+ keyLower === 'password' || keyLower === 'secret') {
58
+ // Redact sensitive property values
59
+ c.tokensRedacted += 1;
60
+ redacted[key] = REDACTED;
61
+ } else if (typeof value === 'string') {
62
+ // Redact token patterns within string values
63
+ redacted[key] = redactTokensInText(value, counters);
64
+ } else if (typeof value === 'object' && value !== null) {
65
+ // Recursively redact nested objects/arrays
66
+ redacted[key] = redactBody(value, counters);
67
+ } else {
68
+ redacted[key] = value;
69
+ }
70
+ });
71
+ return redacted;
72
+ }
73
+
74
+ if (Array.isArray(body)) {
75
+ return body.map(item => redactBody(item, counters));
76
+ }
77
+
78
+ try {
79
+ const str = JSON.stringify(body);
80
+ const redacted = redactTokensInText(str, counters);
81
+ return JSON.parse(redacted);
82
+ } catch {
83
+ return REDACTED;
84
+ }
85
+ }
86
+
87
+ export function redactConsole(text = '', counters = { headersRedacted: 0, tokensRedacted: 0 }) {
88
+ return redactTokensInText(text, counters);
89
+ }
90
+
91
+ export function redactTokensInText(text, counters = { headersRedacted: 0, tokensRedacted: 0 }) {
92
+ const c = ensureCounters(counters);
93
+ if (text == null) return '';
94
+ if (typeof text !== 'string') return String(text);
95
+
96
+ let output = text;
97
+
98
+ // Query/string parameters FIRST (most specific - only api_key, access_token)
99
+ // Use word boundary \b to avoid matching within words like "token" inside "example.com"
100
+ output = output.replace(/\b(api_key|access_token)=([^&\s]+)/gi, (match, key, value) => {
101
+ // Skip if value is itself just REDACTED (avoid double-redacting)
102
+ if (value === REDACTED) return match;
103
+ c.tokensRedacted += 1;
104
+ return `${key}=${REDACTED}`;
105
+ });
106
+
107
+ // Bearer tokens
108
+ output = output.replace(/Bearer\s+([A-Za-z0-9._-]+)/gi, (_match, _token) => {
109
+ c.tokensRedacted += 1;
110
+ return `Bearer ${REDACTED}`;
111
+ });
112
+
113
+ // JWT-like strings (three base64url-ish segments)
114
+ // More specific: require uppercase or numbers, not just domain patterns like "api.example.com"
115
+ output = output.replace(/[A-Z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, (_match) => {
116
+ c.tokensRedacted += 1;
117
+ return REDACTED;
118
+ });
119
+
120
+ return output;
121
+ }
122
+
123
+ export function getRedactionCounters(counters) {
124
+ const c = ensureCounters(counters);
125
+ return { headersRedacted: c.headersRedacted, tokensRedacted: c.tokensRedacted };
126
+ }
127
+
128
+ export const REDACTION_PLACEHOLDER = REDACTED;
@@ -0,0 +1,30 @@
1
+ import crypto from 'crypto';
2
+
3
+ /**
4
+ * Generate a run ID in format: <ISO_TIMESTAMP_UTC>_<6-8 char short hash>
5
+ * Example: 2026-01-11T00-59-12Z_4f2a9c
6
+ */
7
+ export function generateRunId() {
8
+ // Create ISO timestamp with colons replaced by dashes for filesystem compatibility
9
+ const now = new Date();
10
+ const isoString = now.toISOString();
11
+ // Format: 2026-01-11T00:59:12.123Z -> 2026-01-11T00-59-12Z
12
+ const timestamp = isoString.replace(/:/g, '-').replace(/\.\d+Z/, 'Z');
13
+
14
+ // Generate a short hash (6-8 chars)
15
+ const hash = crypto
16
+ .randomBytes(4)
17
+ .toString('hex')
18
+ .substring(0, 6);
19
+
20
+ return `${timestamp}_${hash}`;
21
+ }
22
+
23
+ /**
24
+ * Validate a run ID format
25
+ */
26
+ export function isValidRunId(runId) {
27
+ // Pattern: YYYY-MM-DDTHH-MM-SSZ_hexchars
28
+ const pattern = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z_[a-f0-9]{6,8}$/;
29
+ return pattern.test(runId);
30
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Runtime Budget Model
3
+ * Computes timeouts based on project size, execution mode, and framework
4
+ * Ensures deterministic, bounded execution times
5
+ */
6
+
7
+ /**
8
+ * @typedef {Object} RuntimeBudgetOptions
9
+ * @property {number} [expectationsCount=0] - Number of expectations to process
10
+ * @property {string} [mode='default'] - Execution mode: 'default', 'run', 'ci'
11
+ * @property {string} [framework='unknown'] - Detected framework (optional)
12
+ * @property {number|null} [fileCount=null] - Number of files scanned (optional, fallback to expectationsCount)
13
+ */
14
+
15
+ /**
16
+ * Compute runtime budgets for a VERAX run
17
+ * @param {RuntimeBudgetOptions} [options={}] - Budget computation options
18
+ * @returns {Object} Budget object with phase timeouts
19
+ */
20
+ export function computeRuntimeBudget(options = {}) {
21
+ const {
22
+ expectationsCount = 0,
23
+ mode = 'default',
24
+ framework = 'unknown',
25
+ fileCount = null,
26
+ } = options;
27
+
28
+ // TEST MODE OVERRIDE: Fixed deterministic budgets for integration tests
29
+ if (process.env.VERAX_TEST_MODE === '1') {
30
+ return {
31
+ totalMaxMs: 30000, // Hard cap per run
32
+ learnMaxMs: 5000, // Keep learn bounded
33
+ observeMaxMs: 20000, // Deterministic observe budget
34
+ detectMaxMs: 5000, // Bounded detect
35
+ perExpectationMaxMs: 5000, // Deterministic per-expectation guard
36
+ mode: 'test',
37
+ framework,
38
+ expectationsCount,
39
+ projectSize: fileCount !== null ? fileCount : expectationsCount,
40
+ frameworkMultiplier: 1.0,
41
+ };
42
+ }
43
+
44
+ // Use file count if available, otherwise use expectations count as proxy
45
+ const projectSize = fileCount !== null ? fileCount : expectationsCount;
46
+
47
+ // Base timeouts (milliseconds)
48
+ // Small project: < 10 expectations/files
49
+ // Medium project: 10-50 expectations/files
50
+ // Large project: > 50 expectations/files
51
+
52
+ // Learn phase: file scanning and AST parsing
53
+ const learnBaseMs = mode === 'ci' ? 30000 : 60000; // CI: 30s, default: 60s
54
+ const learnPerFileMs = 50; // 50ms per file
55
+ const learnMaxMs = mode === 'ci' ? 120000 : 300000; // CI: 2min, default: 5min
56
+
57
+ // Observe phase: browser automation
58
+ const observeBaseMs = mode === 'ci' ? 60000 : 120000; // CI: 1min, default: 2min
59
+ const observePerExpectationMs = mode === 'ci' ? 2000 : 5000; // CI: 2s, default: 5s per expectation
60
+ const observeMaxMs = mode === 'ci' ? 600000 : 1800000; // CI: 10min, default: 30min
61
+
62
+ // Detect phase: analysis and comparison
63
+ const detectBaseMs = mode === 'ci' ? 15000 : 30000; // CI: 15s, default: 30s
64
+ const detectPerExpectationMs = 100; // 100ms per expectation
65
+ const detectMaxMs = mode === 'ci' ? 120000 : 300000; // CI: 2min, default: 5min
66
+
67
+ // Per-expectation timeout during observe phase
68
+ const perExpectationBaseMs = mode === 'ci' ? 10000 : 30000; // CI: 10s, default: 30s
69
+ const perExpectationMaxMs = 120000; // 2min max per expectation
70
+
71
+ // Framework weighting (some frameworks may need more time)
72
+ let frameworkMultiplier = 1.0;
73
+ if (framework === 'nextjs' || framework === 'remix') {
74
+ frameworkMultiplier = 1.2; // SSR frameworks may need slightly more time
75
+ } else if (framework === 'react' || framework === 'vue') {
76
+ frameworkMultiplier = 1.1; // SPA frameworks
77
+ }
78
+
79
+ // Compute phase budgets
80
+ const computedLearnMaxMs = Math.min(
81
+ learnBaseMs + (projectSize * learnPerFileMs * frameworkMultiplier),
82
+ learnMaxMs
83
+ );
84
+
85
+ const computedObserveMaxMs = Math.min(
86
+ observeBaseMs + (expectationsCount * observePerExpectationMs * frameworkMultiplier),
87
+ observeMaxMs
88
+ );
89
+
90
+ const computedDetectMaxMs = Math.min(
91
+ detectBaseMs + (expectationsCount * detectPerExpectationMs * frameworkMultiplier),
92
+ detectMaxMs
93
+ );
94
+
95
+ const computedPerExpectationMaxMs = Math.min(
96
+ perExpectationBaseMs * frameworkMultiplier,
97
+ perExpectationMaxMs
98
+ );
99
+
100
+ // Global watchdog timeout (must be >= sum of all phases + buffer)
101
+ // Add 30s buffer for finalization
102
+ const totalMaxMs = Math.max(
103
+ computedLearnMaxMs + computedObserveMaxMs + computedDetectMaxMs + 30000,
104
+ mode === 'ci' ? 900000 : 2400000 // CI: 15min minimum, default: 40min minimum
105
+ );
106
+
107
+ // Cap global timeout
108
+ const totalMaxMsCap = mode === 'ci' ? 1800000 : 3600000; // CI: 30min, default: 60min
109
+ const finalTotalMaxMs = Math.min(totalMaxMs, totalMaxMsCap);
110
+
111
+ // Ensure minimums are met
112
+ const finalLearnMaxMs = Math.max(computedLearnMaxMs, 10000); // At least 10s
113
+ const finalObserveMaxMs = Math.max(computedObserveMaxMs, 30000); // At least 30s
114
+ const finalDetectMaxMs = Math.max(computedDetectMaxMs, 5000); // At least 5s
115
+ const finalPerExpectationMaxMs = Math.max(computedPerExpectationMaxMs, 5000); // At least 5s
116
+
117
+ return {
118
+ totalMaxMs: finalTotalMaxMs,
119
+ learnMaxMs: finalLearnMaxMs,
120
+ observeMaxMs: finalObserveMaxMs,
121
+ detectMaxMs: finalDetectMaxMs,
122
+ perExpectationMaxMs: finalPerExpectationMaxMs,
123
+ mode,
124
+ framework,
125
+ expectationsCount,
126
+ projectSize,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Create a timeout wrapper that rejects after specified milliseconds
132
+ * @param {number} timeoutMs - Timeout in milliseconds
133
+ * @param {Promise} promise - Promise to wrap
134
+ * @param {string} phase - Phase name for error messages
135
+ * @returns {Promise} Promise that rejects on timeout
136
+ */
137
+ export function withTimeout(timeoutMs, promise, phase = 'unknown') {
138
+ return Promise.race([
139
+ promise,
140
+ new Promise((_, reject) => {
141
+ setTimeout(() => {
142
+ reject(new Error(`Phase timeout: ${phase} exceeded ${timeoutMs}ms`));
143
+ }, timeoutMs);
144
+ }),
145
+ ]);
146
+ }
147
+
@@ -0,0 +1,43 @@
1
+ import { atomicWriteJson } from './atomic-write.js';
2
+
3
+ /**
4
+ * Write summary.json with deterministic digest
5
+ * The digest provides stable counts that should be identical across runs
6
+ * on the same input (assuming site behavior is stable)
7
+ */
8
+ export function writeSummaryJson(summaryPath, summaryData, stats = {}) {
9
+ const payload = {
10
+ runId: summaryData.runId,
11
+ status: summaryData.status,
12
+ startedAt: summaryData.startedAt,
13
+ completedAt: summaryData.completedAt,
14
+ command: summaryData.command,
15
+ url: summaryData.url,
16
+ notes: summaryData.notes,
17
+ metrics: summaryData.metrics || {
18
+ learnMs: stats.learnMs || 0,
19
+ observeMs: stats.observeMs || 0,
20
+ detectMs: stats.detectMs || 0,
21
+ totalMs: stats.totalMs || 0,
22
+ },
23
+ findingsCounts: summaryData.findingsCounts || {
24
+ HIGH: stats.HIGH || 0,
25
+ MEDIUM: stats.MEDIUM || 0,
26
+ LOW: stats.LOW || 0,
27
+ UNKNOWN: stats.UNKNOWN || 0,
28
+ },
29
+
30
+ // Stable digest that should be identical across repeated runs on same input
31
+ digest: {
32
+ expectationsTotal: stats.expectationsTotal || 0,
33
+ attempted: stats.attempted || 0,
34
+ observed: stats.observed || 0,
35
+ silentFailures: stats.silentFailures || 0,
36
+ coverageGaps: stats.coverageGaps || 0,
37
+ unproven: stats.unproven || 0,
38
+ informational: stats.informational || 0,
39
+ },
40
+ };
41
+
42
+ atomicWriteJson(summaryPath, payload);
43
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Global type declarations for VERAX
3
+ * These extend built-in types to support runtime-injected properties
4
+ */
5
+
6
+ // Extend Window interface for browser-injected properties
7
+ declare global {
8
+ interface Window {
9
+ __veraxNavTracking?: any;
10
+ next?: any;
11
+ __REDUX_STORE__?: any;
12
+ store?: any;
13
+ __REDUX_DEVTOOLS_EXTENSION__?: any;
14
+ __REACT_DEVTOOLS_GLOBAL_HOOK__?: any;
15
+ __VERAX_STATE_SENSOR__?: any;
16
+ __unhandledRejections?: any[];
17
+ __ZUSTAND_STORE__?: any;
18
+ }
19
+ }
20
+
21
+ // Playwright Page type (imported from playwright)
22
+ import type { Page as PlaywrightPage } from 'playwright';
23
+
24
+ // Re-export for use in JS files
25
+ export type Page = PlaywrightPage;
26
+
27
+ export {};
28
+