@veraxhq/verax 0.2.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 (89) hide show
  1. package/package.json +14 -4
  2. package/src/cli/commands/default.js +244 -86
  3. package/src/cli/commands/doctor.js +36 -4
  4. package/src/cli/commands/run.js +253 -69
  5. package/src/cli/entry.js +5 -5
  6. package/src/cli/util/detection-engine.js +4 -3
  7. package/src/cli/util/events.js +76 -0
  8. package/src/cli/util/expectation-extractor.js +11 -1
  9. package/src/cli/util/findings-writer.js +1 -0
  10. package/src/cli/util/observation-engine.js +69 -23
  11. package/src/cli/util/paths.js +3 -2
  12. package/src/cli/util/project-discovery.js +20 -0
  13. package/src/cli/util/redact.js +2 -2
  14. package/src/cli/util/runtime-budget.js +147 -0
  15. package/src/cli/util/summary-writer.js +12 -1
  16. package/src/types/global.d.ts +28 -0
  17. package/src/types/ts-ast.d.ts +24 -0
  18. package/src/verax/cli/doctor.js +2 -2
  19. package/src/verax/cli/init.js +1 -1
  20. package/src/verax/cli/url-safety.js +12 -2
  21. package/src/verax/cli/wizard.js +13 -2
  22. package/src/verax/core/budget-engine.js +1 -1
  23. package/src/verax/core/decision-snapshot.js +2 -2
  24. package/src/verax/core/determinism-model.js +35 -6
  25. package/src/verax/core/incremental-store.js +15 -7
  26. package/src/verax/core/replay-validator.js +4 -4
  27. package/src/verax/core/replay.js +1 -1
  28. package/src/verax/core/silence-impact.js +1 -1
  29. package/src/verax/core/silence-model.js +9 -7
  30. package/src/verax/detect/comparison.js +8 -3
  31. package/src/verax/detect/confidence-engine.js +17 -17
  32. package/src/verax/detect/detection-engine.js +1 -1
  33. package/src/verax/detect/evidence-index.js +15 -65
  34. package/src/verax/detect/expectation-model.js +54 -3
  35. package/src/verax/detect/explanation-helpers.js +1 -1
  36. package/src/verax/detect/finding-detector.js +2 -2
  37. package/src/verax/detect/findings-writer.js +9 -16
  38. package/src/verax/detect/flow-detector.js +4 -4
  39. package/src/verax/detect/index.js +37 -11
  40. package/src/verax/detect/interactive-findings.js +3 -4
  41. package/src/verax/detect/signal-mapper.js +2 -2
  42. package/src/verax/detect/skip-classifier.js +4 -4
  43. package/src/verax/detect/verdict-engine.js +4 -6
  44. package/src/verax/flow/flow-engine.js +3 -2
  45. package/src/verax/flow/flow-spec.js +1 -2
  46. package/src/verax/index.js +15 -3
  47. package/src/verax/intel/effect-detector.js +1 -1
  48. package/src/verax/intel/index.js +2 -2
  49. package/src/verax/intel/route-extractor.js +3 -3
  50. package/src/verax/intel/vue-navigation-extractor.js +81 -18
  51. package/src/verax/intel/vue-router-extractor.js +4 -2
  52. package/src/verax/learn/action-contract-extractor.js +3 -3
  53. package/src/verax/learn/ast-contract-extractor.js +53 -1
  54. package/src/verax/learn/index.js +36 -2
  55. package/src/verax/learn/manifest-writer.js +28 -14
  56. package/src/verax/learn/route-extractor.js +1 -1
  57. package/src/verax/learn/route-validator.js +8 -7
  58. package/src/verax/learn/state-extractor.js +1 -1
  59. package/src/verax/learn/static-extractor-navigation.js +1 -1
  60. package/src/verax/learn/static-extractor-validation.js +2 -2
  61. package/src/verax/learn/static-extractor.js +8 -7
  62. package/src/verax/learn/ts-contract-resolver.js +14 -12
  63. package/src/verax/observe/browser.js +22 -3
  64. package/src/verax/observe/console-sensor.js +2 -2
  65. package/src/verax/observe/expectation-executor.js +2 -1
  66. package/src/verax/observe/focus-sensor.js +1 -1
  67. package/src/verax/observe/human-driver.js +29 -10
  68. package/src/verax/observe/index.js +10 -7
  69. package/src/verax/observe/interaction-discovery.js +27 -15
  70. package/src/verax/observe/interaction-runner.js +6 -6
  71. package/src/verax/observe/loading-sensor.js +6 -0
  72. package/src/verax/observe/navigation-sensor.js +1 -1
  73. package/src/verax/observe/settle.js +1 -0
  74. package/src/verax/observe/state-sensor.js +8 -4
  75. package/src/verax/observe/state-ui-sensor.js +7 -1
  76. package/src/verax/observe/traces-writer.js +27 -16
  77. package/src/verax/observe/ui-signal-sensor.js +7 -0
  78. package/src/verax/scan-summary-writer.js +5 -2
  79. package/src/verax/shared/artifact-manager.js +1 -1
  80. package/src/verax/shared/budget-profiles.js +2 -2
  81. package/src/verax/shared/caching.js +1 -1
  82. package/src/verax/shared/config-loader.js +1 -2
  83. package/src/verax/shared/dynamic-route-utils.js +12 -6
  84. package/src/verax/shared/retry-policy.js +1 -6
  85. package/src/verax/shared/root-artifacts.js +1 -1
  86. package/src/verax/shared/zip-artifacts.js +1 -0
  87. package/src/verax/validate/context-validator.js +1 -1
  88. package/src/verax/observe/index.js.backup +0 -1
  89. package/src/verax/validate/context-validator.js.bak +0 -0
@@ -1,6 +1,6 @@
1
1
  import { chromium } from 'playwright';
2
2
  import { writeFileSync, mkdirSync } from 'fs';
3
- import { resolve, join } from 'path';
3
+ import { resolve } from 'path';
4
4
  import { redactHeaders, redactUrl, redactBody, redactConsole, getRedactionCounters } from './redact.js';
5
5
 
6
6
  /**
@@ -60,9 +60,16 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
60
60
  });
61
61
  });
62
62
 
63
- // Navigate to base URL first
63
+ // Navigate to base URL first with explicit timeout
64
64
  try {
65
- await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
65
+ await page.goto(url, {
66
+ waitUntil: 'domcontentloaded', // Use domcontentloaded instead of networkidle for faster timeout
67
+ timeout: 30000
68
+ });
69
+ // Wait for network idle with separate timeout
70
+ await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
71
+ // Network idle timeout is acceptable, continue
72
+ });
66
73
  } catch (error) {
67
74
  // Continue even if initial load fails
68
75
  if (onProgress) {
@@ -96,6 +103,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
96
103
  type: exp.type,
97
104
  promise: exp.promise,
98
105
  source: exp.source,
106
+ attempted: false,
99
107
  observed: false,
100
108
  observedAt: null,
101
109
  evidenceFiles: [],
@@ -107,6 +115,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
107
115
  let evidence = null;
108
116
 
109
117
  if (exp.type === 'navigation') {
118
+ observation.attempted = true; // Mark as attempted
110
119
  result = await observeNavigation(
111
120
  page,
112
121
  exp,
@@ -117,6 +126,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
117
126
  );
118
127
  evidence = result ? `nav_${expNum}_after.png` : null;
119
128
  } else if (exp.type === 'network') {
129
+ observation.attempted = true; // Mark as attempted
120
130
  result = await observeNetwork(page, exp, networkLogs, 5000);
121
131
  if (result) {
122
132
  const evidenceFile = `network_${expNum}.json`;
@@ -135,6 +145,7 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
135
145
  evidence = null;
136
146
  }
137
147
  } else if (exp.type === 'state') {
148
+ observation.attempted = true; // Mark as attempted
138
149
  result = await observeState(page, exp, evidencePath, expNum);
139
150
  evidence = result ? `state_${expNum}_after.png` : null;
140
151
  }
@@ -187,19 +198,47 @@ export async function observeExpectations(expectations, url, evidencePath, onPro
187
198
  observedAt: new Date().toISOString(),
188
199
  };
189
200
  } finally {
190
- // Clean up
201
+ // Robust cleanup: ensure browser/context/page are closed
202
+ // Remove all event listeners to prevent leaks
191
203
  if (page) {
192
204
  try {
193
- await page.close();
205
+ // Remove all listeners
206
+ page.removeAllListeners();
207
+ // @ts-expect-error - Playwright page.close() doesn't accept timeout option, but we use it for safety
208
+ await page.close({ timeout: 5000 }).catch(() => {});
194
209
  } catch (e) {
195
- // Ignore close errors
210
+ // Ignore close errors but emit warning if onProgress available
211
+ if (onProgress) {
212
+ onProgress({
213
+ event: 'observe:warning',
214
+ message: `Page cleanup warning: ${e.message}`,
215
+ });
216
+ }
196
217
  }
197
218
  }
219
+
220
+ // Close browser context if it exists
198
221
  if (browser) {
199
222
  try {
200
- await browser.close();
223
+ const contexts = browser.contexts();
224
+ for (const context of contexts) {
225
+ try {
226
+ // @ts-expect-error - Playwright context.close() doesn't accept timeout option, but we use it for safety
227
+ await context.close({ timeout: 5000 }).catch(() => {});
228
+ } catch (e) {
229
+ // Ignore context close errors
230
+ }
231
+ }
232
+ // @ts-expect-error - Playwright browser.close() doesn't accept timeout option, but we use it for safety
233
+ await browser.close({ timeout: 5000 }).catch(() => {});
201
234
  } catch (e) {
202
- // Ignore close errors
235
+ // Ignore browser close errors but emit warning if onProgress available
236
+ if (onProgress) {
237
+ onProgress({
238
+ event: 'observe:warning',
239
+ message: `Browser cleanup warning: ${e.message}`,
240
+ });
241
+ }
203
242
  }
204
243
  }
205
244
  }
@@ -242,6 +281,7 @@ async function observeNavigation(page, expectation, baseUrl, visitedUrls, eviden
242
281
  await page.click(`a[href="${element.href}"]`);
243
282
  } catch (e2) {
244
283
  // Try clicking by text content
284
+ // eslint-disable-next-line no-undef
245
285
  const text = await page.evaluate((href) => {
246
286
  const anchors = Array.from(document.querySelectorAll('a'));
247
287
  const found = anchors.find(a => a.getAttribute('href') === href);
@@ -254,14 +294,23 @@ async function observeNavigation(page, expectation, baseUrl, visitedUrls, eviden
254
294
  }
255
295
  }
256
296
 
257
- // Wait for navigation or SPA update
297
+ // Wait for navigation or SPA update with explicit timeout
258
298
  try {
259
- await page.waitForNavigation({ waitUntil: 'networkidle', timeout: 2000 }).catch(() => {});
299
+ await page.waitForNavigation({
300
+ waitUntil: 'domcontentloaded',
301
+ timeout: 5000
302
+ }).catch(() => {
303
+ // Navigation timeout is acceptable for SPAs
304
+ });
305
+ // Wait for network idle with separate timeout
306
+ await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
307
+ // Network idle timeout is acceptable
308
+ });
260
309
  } catch (e) {
261
- // Navigation might not happen
310
+ // Navigation might not happen, continue
262
311
  }
263
312
 
264
- // Wait for potential SPA updates
313
+ // Wait for potential SPA updates (bounded)
265
314
  await page.waitForTimeout(300);
266
315
 
267
316
  // Screenshot after interaction
@@ -304,13 +353,21 @@ async function observeNetwork(page, expectation, networkLogs, timeoutMs) {
304
353
  if (found) {
305
354
  clearInterval(checkTimer);
306
355
  resolve(true);
356
+ return;
307
357
  }
308
358
 
309
359
  if (Date.now() - startTime > timeoutMs) {
310
360
  clearInterval(checkTimer);
311
361
  resolve(false);
362
+ return;
312
363
  }
313
364
  }, 100);
365
+
366
+ // CRITICAL: Unref the interval so it doesn't keep the process alive
367
+ // This allows tests to exit cleanly even if interval is not cleared
368
+ if (checkTimer && checkTimer.unref) {
369
+ checkTimer.unref();
370
+ }
314
371
  });
315
372
  }
316
373
 
@@ -353,14 +410,3 @@ async function observeState(page, expectation, evidencePath, expNum) {
353
410
  }
354
411
  }
355
412
 
356
- /**
357
- * Check if page content changed (for SPA detection)
358
- */
359
- async function checkPageContentChanged(page) {
360
- try {
361
- const bodyText = await page.locator('body').textContent();
362
- return bodyText && bodyText.length > 0;
363
- } catch (error) {
364
- return false;
365
- }
366
- }
@@ -1,11 +1,12 @@
1
- import { join } from 'path';
1
+ import { join, isAbsolute } from 'path';
2
2
  import { mkdirSync } from 'fs';
3
3
 
4
4
  /**
5
5
  * Build run artifact paths
6
6
  */
7
7
  export function getRunPaths(projectRoot, outDir, runId) {
8
- const baseDir = join(projectRoot, outDir, 'runs', runId);
8
+ const outBase = isAbsolute(outDir) ? outDir : join(projectRoot, outDir);
9
+ const baseDir = join(outBase, 'runs', runId);
9
10
 
10
11
  return {
11
12
  baseDir,
@@ -4,8 +4,22 @@ import { resolve, dirname } from 'path';
4
4
  /**
5
5
  * Project Discovery Module
6
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
7
17
  */
8
18
 
19
+ /**
20
+ * @param {string} srcPath
21
+ * @returns {Promise<ProjectProfile>}
22
+ */
9
23
  export async function discoverProject(srcPath) {
10
24
  const projectRoot = resolve(srcPath);
11
25
 
@@ -62,6 +76,12 @@ function findPackageJson(startPath) {
62
76
  return immediatePackage;
63
77
  }
64
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
+
65
85
  // Then walk up (limit to 5 levels for monorepos, not 10)
66
86
  for (let i = 0; i < 5; i++) {
67
87
  const parentPath = dirname(currentPath);
@@ -105,14 +105,14 @@ export function redactTokensInText(text, counters = { headersRedacted: 0, tokens
105
105
  });
106
106
 
107
107
  // Bearer tokens
108
- output = output.replace(/Bearer\s+([A-Za-z0-9._-]+)/gi, (match, token) => {
108
+ output = output.replace(/Bearer\s+([A-Za-z0-9._-]+)/gi, (_match, _token) => {
109
109
  c.tokensRedacted += 1;
110
110
  return `Bearer ${REDACTED}`;
111
111
  });
112
112
 
113
113
  // JWT-like strings (three base64url-ish segments)
114
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) => {
115
+ output = output.replace(/[A-Z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, (_match) => {
116
116
  c.tokensRedacted += 1;
117
117
  return REDACTED;
118
118
  });
@@ -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
+
@@ -1,5 +1,4 @@
1
1
  import { atomicWriteJson } from './atomic-write.js';
2
- import { resolve } from 'path';
3
2
 
4
3
  /**
5
4
  * Write summary.json with deterministic digest
@@ -15,6 +14,18 @@ export function writeSummaryJson(summaryPath, summaryData, stats = {}) {
15
14
  command: summaryData.command,
16
15
  url: summaryData.url,
17
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
+ },
18
29
 
19
30
  // Stable digest that should be identical across repeated runs on same input
20
31
  digest: {
@@ -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
+
@@ -0,0 +1,24 @@
1
+ /**
2
+ * TypeScript AST Node type extensions
3
+ * These extend the base Node type to include properties used by VERAX
4
+ */
5
+
6
+ import type * as ts from 'typescript';
7
+
8
+ declare module 'typescript' {
9
+ interface Node {
10
+ attributes?: ts.NodeArray<ts.JSDocAttribute>;
11
+ tagName?: ts.Identifier;
12
+ body?: ts.Node;
13
+ arguments?: ts.NodeArray<ts.Expression>;
14
+ initializer?: ts.Expression;
15
+ expression?: ts.Expression;
16
+ children?: ts.NodeArray<ts.Node>;
17
+ }
18
+
19
+ namespace ts {
20
+ // Add isFalseKeyword if it doesn't exist (it might be in a different version)
21
+ function isFalseKeyword(node: ts.Node): node is ts.FalseKeyword;
22
+ }
23
+ }
24
+
@@ -4,7 +4,7 @@
4
4
  * Checks environment, dependencies, and project setup.
5
5
  */
6
6
 
7
- import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
7
+ import { mkdirSync, writeFileSync, unlinkSync } from 'fs';
8
8
  import { resolve } from 'path';
9
9
  import { chromium } from 'playwright';
10
10
  import { get } from 'http';
@@ -182,7 +182,7 @@ async function checkUrlReachability(url) {
182
182
  * @returns {Promise<Object>} Doctor results
183
183
  */
184
184
  export async function runDoctor(options = {}) {
185
- const { projectRoot = process.cwd(), url = null, json = false } = options;
185
+ const { projectRoot = process.cwd(), url = null, json: _json = false } = options;
186
186
 
187
187
  const checks = [];
188
188
  let overallStatus = 'ok';
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { existsSync, writeFileSync, mkdirSync } from 'fs';
8
- import { resolve, dirname } from 'path';
8
+ import { resolve } from 'path';
9
9
  import { getDefaultConfig } from '../shared/config-loader.js';
10
10
 
11
11
  /**
@@ -68,11 +68,21 @@ export function checkUrlSafety(url) {
68
68
  }
69
69
  }
70
70
 
71
+ /**
72
+ * @typedef {Object} ReadlineInterface
73
+ * @property {function(string): Promise<string>} question - Prompt user with question
74
+ * @property {function(): void} close - Close the readline interface
75
+ */
76
+
77
+ /**
78
+ * @typedef {Object} ConfirmExternalUrlOptions
79
+ * @property {ReadlineInterface} [readlineInterface] - Readline interface (injectable)
80
+ */
81
+
71
82
  /**
72
83
  * Prompt for external URL confirmation
73
84
  * @param {string} hostname - Hostname to confirm
74
- * @param {Object} options - Options
75
- * @param {Function} options.readlineInterface - Readline interface (injectable)
85
+ * @param {ConfirmExternalUrlOptions} [options={}] - Options
76
86
  * @returns {Promise<boolean>} True if confirmed
77
87
  */
78
88
  export async function confirmExternalUrl(hostname, options = {}) {
@@ -4,8 +4,15 @@
4
4
  * Guides users through VERAX configuration with friendly prompts.
5
5
  */
6
6
 
7
+ /**
8
+ * @typedef {Object} ReadlineInterface
9
+ * @property {function(string): Promise<string>} question - Prompt user with question
10
+ * @property {function(): void} close - Close the readline interface
11
+ */
12
+
7
13
  /**
8
14
  * Create a readline interface (can be injected for testing)
15
+ * @returns {Promise<ReadlineInterface>}
9
16
  */
10
17
  async function createReadlineInterface(input = process.stdin, output = process.stdout) {
11
18
  const readline = await import('readline/promises');
@@ -16,10 +23,14 @@ async function createReadlineInterface(input = process.stdin, output = process.s
16
23
  });
17
24
  }
18
25
 
26
+ /**
27
+ * @typedef {Object} WizardOptions
28
+ * @property {ReadlineInterface} [readlineInterface] - Readline interface (injectable for testing)
29
+ */
30
+
19
31
  /**
20
32
  * Run interactive wizard
21
- * @param {Object} options - Options for wizard
22
- * @param {Function} options.readlineInterface - Readline interface (injectable for testing)
33
+ * @param {WizardOptions} [options={}] - Options for wizard
23
34
  * @returns {Promise<Object>} Wizard results
24
35
  */
25
36
  export async function runWizard(options = {}) {
@@ -131,7 +131,7 @@ export function computeRouteBudget(manifest, currentUrl, baseBudget) {
131
131
  const routes = manifest.routes || [];
132
132
  const expectations = manifest.staticExpectations || [];
133
133
  const totalRoutes = routes.length;
134
- const totalExpectations = expectations.length;
134
+ // const totalExpectations = expectations.length; // Reserved for future use
135
135
 
136
136
  // Count expectations per route
137
137
  const routeExpectationCount = new Map();
@@ -175,10 +175,10 @@ function extractUnverified(detectTruth, observeTruth) {
175
175
  * @param {Array} findings - Array of findings
176
176
  * @param {Object} detectTruth - Detect phase truth
177
177
  * @param {Object} observeTruth - Observe phase truth
178
- * @param {Object} silences - Silence data
178
+ * @param {Object} _silences - Silence data (unused parameter, kept for API compatibility)
179
179
  * @returns {Object} - Decision snapshot answering 6 mandatory questions
180
180
  */
181
- export function computeDecisionSnapshot(findings, detectTruth, observeTruth, silences) {
181
+ export function computeDecisionSnapshot(findings, detectTruth, observeTruth, _silences) {
182
182
  // Question 1: Do we have confirmed SILENT FAILURES?
183
183
  const confirmedFailures = findings.filter(f =>
184
184
  f.outcome === 'broken' || f.type === 'silent_failure'