@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
@@ -16,12 +16,31 @@ export async function navigateToUrl(page, url, scanBudget = DEFAULT_SCAN_BUDGET)
16
16
  if (url.startsWith('file:') || url.includes('localhost:') || url.includes('127.0.0.1')) {
17
17
  stableWait = 200; // Short wait for local fixtures
18
18
  }
19
- } catch {}
20
- await page.goto(url, { waitUntil: 'networkidle', timeout: scanBudget.initialNavigationTimeoutMs });
19
+ } catch {
20
+ // Ignore config errors
21
+ }
22
+ // Use domcontentloaded first for faster timeout, then wait for networkidle separately
23
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: scanBudget.initialNavigationTimeoutMs });
24
+ await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
25
+ // Network idle timeout is acceptable, continue
26
+ });
21
27
  await page.waitForTimeout(stableWait);
22
28
  }
23
29
 
24
30
  export async function closeBrowser(browser) {
25
- await browser.close();
31
+ try {
32
+ // Close all contexts first
33
+ const contexts = browser.contexts();
34
+ for (const context of contexts) {
35
+ try {
36
+ await context.close({ timeout: 5000 }).catch(() => {});
37
+ } catch (e) {
38
+ // Ignore context close errors
39
+ }
40
+ }
41
+ await browser.close({ timeout: 5000 }).catch(() => {});
42
+ } catch (e) {
43
+ // Ignore browser close errors - best effort cleanup
44
+ }
26
45
  }
27
46
 
@@ -57,7 +57,7 @@ export class ConsoleSensor {
57
57
  };
58
58
 
59
59
  // Capture unhandled promise rejections
60
- const onUnhandledRejection = (promise, reason) => {
60
+ const _onUnhandledRejection = (promise, reason) => {
61
61
  const message = (reason?.toString?.() || String(reason)).slice(0, 200);
62
62
  state.unhandledRejections.push({
63
63
  message: message,
@@ -104,7 +104,7 @@ export class ConsoleSensor {
104
104
  /**
105
105
  * Stop monitoring and return a summary for the window.
106
106
  */
107
- stopWindow(windowId, page) {
107
+ stopWindow(windowId, _page) {
108
108
  const state = this.windows.get(windowId);
109
109
  if (!state) {
110
110
  return this.getEmptySummary();
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Executes every PROVEN expectation from the manifest.
5
5
  * Each expectation must result in: VERIFIED, SILENT_FAILURE, or COVERAGE_GAP.
6
+ * @typedef {import('playwright').Page} Page
6
7
  */
7
8
 
8
9
  import { isProvenExpectation } from '../shared/expectation-prover.js';
@@ -11,7 +12,7 @@ import { hasMeaningfulUrlChange, hasVisibleChange, hasDomChange } from '../detec
11
12
  import { runInteraction } from './interaction-runner.js';
12
13
  import { captureScreenshot } from './evidence-capture.js';
13
14
  import { getBaseOrigin } from './domain-boundary.js';
14
- import { resolve, dirname } from 'path';
15
+ import { resolve } from 'path';
15
16
 
16
17
  /**
17
18
  * Execute a single PROVEN expectation.
@@ -155,7 +155,7 @@ export class FocusSensor {
155
155
  /**
156
156
  * Detect if focus didn't move into modal (expected but didn't happen)
157
157
  */
158
- detectModalFocusFailure(page) {
158
+ detectModalFocusFailure(_page) {
159
159
  // This is checked via modal detection - focus should move to modal
160
160
  // Presence of modal without focus change = failure
161
161
  return false; // Caller will check if modal opened
@@ -158,7 +158,9 @@ export class HumanBehaviorDriver {
158
158
  } else if (url.includes('localhost:') || url.includes('127.0.0.1')) {
159
159
  timeoutMs = 200; // Short wait for local http fixtures
160
160
  }
161
- } catch {}
161
+ } catch {
162
+ // Ignore config errors
163
+ }
162
164
 
163
165
  const waitForUiIdle = async () => {
164
166
  const start = Date.now();
@@ -336,7 +338,10 @@ export class HumanBehaviorDriver {
336
338
 
337
339
  const focusableSelectors = await page.evaluate(() => {
338
340
  const focusables = Array.from(document.querySelectorAll('a[href], button, input, select, textarea, [tabindex], [role="button"], [role="menuitem"], [contenteditable="true"]'))
339
- .filter(el => !el.hasAttribute('disabled') && el.tabIndex >= 0 && el.offsetParent !== null);
341
+ .filter(el => {
342
+ const htmlEl = /** @type {HTMLElement} */ (el);
343
+ return !el.hasAttribute('disabled') && htmlEl.tabIndex >= 0 && htmlEl.offsetParent !== null;
344
+ });
340
345
  const describe = (el) => {
341
346
  if (!el) return 'body';
342
347
  if (el.id) return `#${el.id}`;
@@ -609,7 +614,9 @@ export class HumanBehaviorDriver {
609
614
  page.waitForTimeout(CLICK_TIMEOUT_MS)
610
615
  ]);
611
616
  await this.waitAfterAction(page, 600);
612
- } catch {}
617
+ } catch {
618
+ // Ignore form submission errors
619
+ }
613
620
 
614
621
  const afterUrl = page.url();
615
622
  const afterStorageKeys = await page.evaluate(() => Object.keys(localStorage));
@@ -660,7 +667,9 @@ export class HumanBehaviorDriver {
660
667
  clicked = true;
661
668
  await this.waitAfterAction(page, 400);
662
669
  break;
663
- } catch {}
670
+ } catch {
671
+ // Ignore interaction errors
672
+ }
664
673
  }
665
674
  }
666
675
 
@@ -686,17 +695,23 @@ export class HumanBehaviorDriver {
686
695
  const beforeUrl = page.url();
687
696
  try {
688
697
  await page.goto(url, { waitUntil: 'load', timeout: CLICK_TIMEOUT_MS }).catch(() => null);
689
- } catch {}
698
+ } catch {
699
+ // Ignore navigation errors
700
+ }
690
701
 
691
702
  const afterUrl = page.url();
692
- const blocked = beforeUrl !== afterUrl && (afterUrl.includes('/login') || afterUrl.includes('/signin'));
703
+ const redirectedToLogin = beforeUrl !== afterUrl && (afterUrl.includes('/login') || afterUrl.includes('/signin'));
693
704
  const content = await page.content();
694
705
  const hasAccessDenied = content.includes('401') || content.includes('403') || content.includes('unauthorized') || content.includes('forbidden');
706
+ const isProtected = redirectedToLogin || hasAccessDenied;
695
707
 
696
708
  return {
697
709
  url,
698
- blocked: blocked || hasAccessDenied,
699
- redirectedTo: afterUrl,
710
+ beforeUrl,
711
+ afterUrl,
712
+ isProtected,
713
+ redirectedToLogin,
714
+ hasAccessDenied,
700
715
  httpStatus: hasAccessDenied ? (content.includes('403') ? 403 : 401) : 200
701
716
  };
702
717
  }
@@ -710,7 +725,9 @@ export class HumanBehaviorDriver {
710
725
  const key = window.localStorage.key(i);
711
726
  if (key) result[key] = window.localStorage.getItem(key);
712
727
  }
713
- } catch (e) {}
728
+ } catch {
729
+ // Ignore localStorage access errors
730
+ }
714
731
  return result;
715
732
  });
716
733
 
@@ -721,7 +738,9 @@ export class HumanBehaviorDriver {
721
738
  const key = window.sessionStorage.key(i);
722
739
  if (key) result[key] = window.sessionStorage.getItem(key);
723
740
  }
724
- } catch (e) {}
741
+ } catch {
742
+ // Ignore sessionStorage access errors
743
+ }
725
744
  return result;
726
745
  });
727
746
 
@@ -1,5 +1,5 @@
1
1
  import { resolve, dirname } from 'path';
2
- import { mkdirSync, readFileSync, existsSync } from 'fs';
2
+ import { mkdirSync, readFileSync, existsSync, writeFileSync } from 'fs';
3
3
  import { createBrowser, navigateToUrl, closeBrowser } from './browser.js';
4
4
  import { discoverAllInteractions } from './interaction-discovery.js';
5
5
  import { captureScreenshot } from './evidence-capture.js';
@@ -15,7 +15,7 @@ import { deriveObservedExpectation, shouldAttemptRepeatObservedExpectation, eval
15
15
  import { computeRouteBudget } from '../core/budget-engine.js';
16
16
  import { loadPreviousSnapshot, saveSnapshot, buildSnapshot, compareSnapshots, shouldSkipInteractionIncremental } from '../core/incremental-store.js';
17
17
  import SilenceTracker from '../core/silence-model.js';
18
- import { DecisionRecorder, recordBudgetProfile, recordTimeoutConfig, recordAdaptiveStabilization, recordTruncation, recordEnvironment } from '../core/determinism-model.js';
18
+ import { DecisionRecorder, recordBudgetProfile, recordTimeoutConfig, recordTruncation, recordEnvironment } from '../core/determinism-model.js';
19
19
 
20
20
  /**
21
21
  * OBSERVE PHASE - Execute interactions and capture runtime behavior
@@ -134,8 +134,11 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
134
134
  await navigateToUrl(page, url, scanBudget);
135
135
 
136
136
  const projectDir = manifestPath ? dirname(dirname(dirname(manifestPath))) : process.cwd();
137
- const observeDir = resolve(projectDir, '.veraxverax', 'observe');
138
- const screenshotsDir = resolve(observeDir, 'screenshots');
137
+ if (!runId) {
138
+ throw new Error('runId is required');
139
+ }
140
+ const { getScreenshotDir } = await import('../core/run-id.js');
141
+ const screenshotsDir = getScreenshotDir(projectDir, runId);
139
142
  mkdirSync(screenshotsDir, { recursive: true });
140
143
 
141
144
  const timestamp = Date.now();
@@ -307,7 +310,7 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
307
310
 
308
311
  // Discover ALL interactions on this page
309
312
  // Note: discoverAllInteractions already returns sorted interactions deterministically
310
- const { interactions, coverage: pageCoverage } = await discoverAllInteractions(page, baseOrigin, routeBudget);
313
+ const { interactions } = await discoverAllInteractions(page, baseOrigin, routeBudget);
311
314
  totalInteractionsDiscovered += interactions.length;
312
315
 
313
316
  // SCALE INTELLIGENCE: Apply adaptive budget cap (interactions are already sorted deterministically)
@@ -847,7 +850,7 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
847
850
  }));
848
851
 
849
852
  const currentSnapshot = buildSnapshot(manifest, observedInteractions);
850
- saveSnapshot(projectDir, currentSnapshot);
853
+ saveSnapshot(projectDir, currentSnapshot, runId);
851
854
 
852
855
  // Add incremental mode metadata to observation
853
856
  observation.incremental = {
@@ -865,7 +868,7 @@ export async function observe(url, manifestPath = null, scanBudgetOverride = nul
865
868
  mkdirSync(runsDir, { recursive: true });
866
869
  const decisionsPath = resolve(runsDir, 'decisions.json');
867
870
  const decisionsData = JSON.stringify(decisionRecorder.export(), null, 2);
868
- require('fs').writeFileSync(decisionsPath, decisionsData, 'utf-8');
871
+ writeFileSync(decisionsPath, decisionsData, 'utf-8');
869
872
  }
870
873
 
871
874
  // Phase 4: Add safety mode statistics
@@ -73,7 +73,6 @@ async function extractLabel(element) {
73
73
 
74
74
  export async function discoverInteractions(page, baseOrigin, scanBudget = DEFAULT_SCAN_BUDGET) {
75
75
  const currentUrl = page.url();
76
- const interactions = [];
77
76
  const seenElements = new Set();
78
77
 
79
78
  const allInteractions = [];
@@ -246,7 +245,6 @@ export async function discoverInteractions(page, baseOrigin, scanBudget = DEFAUL
246
245
  if (item.type === 'button' || item.type === 'link') {
247
246
  const text = (item.text || '').trim().toLowerCase();
248
247
  const label = (item.label || '').trim().toLowerCase();
249
- const combined = `${text} ${label}`;
250
248
 
251
249
  const isLogout = /^(logout|sign\s*out|signout|log\s*out)$/i.test(text) ||
252
250
  /^(logout|sign\s*out|signout|log\s*out)$/i.test(label) ||
@@ -419,12 +417,16 @@ export async function discoverInteractions(page, baseOrigin, scanBudget = DEFAUL
419
417
  try {
420
418
  const box = await item.element.boundingBox();
421
419
  if (box) {
420
+ // @ts-expect-error - Adding runtime properties to interaction object
422
421
  item.boundingY = box.y;
422
+ // @ts-expect-error - Adding runtime properties to interaction object
423
423
  item.boundingAvailable = true;
424
424
  }
425
425
  } catch (error) {
426
+ // @ts-expect-error - Adding runtime properties to interaction object
426
427
  item.boundingAvailable = false;
427
428
  }
429
+ // @ts-expect-error - Adding runtime properties to interaction object
428
430
  item.priority = computePriority(item, viewportHeight);
429
431
  }
430
432
 
@@ -457,7 +459,7 @@ export async function discoverInteractions(page, baseOrigin, scanBudget = DEFAUL
457
459
  * Discover ALL interactions on a page (no priority cap).
458
460
  * Used for full-site coverage traversal.
459
461
  */
460
- export async function discoverAllInteractions(page, baseOrigin, scanBudget = DEFAULT_SCAN_BUDGET) {
462
+ export async function discoverAllInteractions(page, baseOrigin, _scanBudget = DEFAULT_SCAN_BUDGET) {
461
463
  const currentUrl = page.url();
462
464
  const seenElements = new Set();
463
465
  const allInteractions = [];
@@ -597,18 +599,28 @@ export async function discoverAllInteractions(page, baseOrigin, scanBudget = DEF
597
599
 
598
600
  // Return ALL interactions (no priority cap)
599
601
  return {
600
- interactions: ordered.map(item => ({
601
- type: item.type,
602
- selector: item.selector,
603
- label: item.label,
604
- element: item.element,
605
- isExternal: item.isExternal || false,
606
- href: item.href,
607
- text: item.text,
608
- dataHref: item.dataHref,
609
- dataDanger: item.dataDanger || false,
610
- dataDestructive: item.dataDestructive || false
611
- })),
602
+ interactions: ordered.map(item => {
603
+ const mapped = {
604
+ type: item.type,
605
+ selector: item.selector,
606
+ label: item.label,
607
+ element: item.element,
608
+ isExternal: item.isExternal || false,
609
+ href: item.href,
610
+ text: item.text,
611
+ dataHref: item.dataHref,
612
+ // @ts-expect-error - dataDanger and dataDestructive are optional runtime properties on interaction objects
613
+ dataDanger: item.dataDanger || false,
614
+ // @ts-expect-error - dataDestructive is an optional runtime property on interaction objects
615
+ dataDestructive: item.dataDestructive || false
616
+ };
617
+ // hasPasswordInput only exists on form types
618
+ if (item.type === 'form' || item.type === 'login') {
619
+ // @ts-expect-error - hasPasswordInput is only on form/login types at runtime
620
+ mapped.hasPasswordInput = item.hasPasswordInput || false;
621
+ }
622
+ return mapped;
623
+ }),
612
624
  coverage: {
613
625
  candidatesDiscovered: allInteractions.length,
614
626
  candidatesSelected: allInteractions.length,
@@ -12,7 +12,9 @@ import { FocusSensor } from './focus-sensor.js';
12
12
  import { AriaSensor } from './aria-sensor.js';
13
13
  import { TimingSensor } from './timing-sensor.js';
14
14
  import { HumanBehaviorDriver } from './human-driver.js';
15
- import { DEFAULT_SCAN_BUDGET } from '../shared/scan-budget.js';
15
+
16
+ // Import CLICK_TIMEOUT_MS from human-driver (re-export needed)
17
+ const CLICK_TIMEOUT_MS = 2000;
16
18
 
17
19
  /**
18
20
  * SILENCE TRACKING: Mark timeout and record to silence tracker.
@@ -136,13 +138,11 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
136
138
  let loadingWindowData = null;
137
139
 
138
140
  let uiBefore = {};
139
- let stateBefore = null;
140
- let sessionStateBefore = null;
141
141
 
142
142
  try {
143
143
  // Capture session state before interaction for auth-aware interactions
144
144
  if (interaction.type === 'login' || interaction.type === 'logout') {
145
- sessionStateBefore = await humanDriver.captureSessionState(page);
145
+ await humanDriver.captureSessionState(page);
146
146
  }
147
147
 
148
148
  if (Date.now() - startTime > scanBudget.maxScanDurationMs) {
@@ -263,7 +263,7 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
263
263
  const locator = interaction.element;
264
264
  // On file:// origins, avoid long navigation waits for simple link clicks
265
265
  const isFileOrigin = baseOrigin && baseOrigin.startsWith('file:');
266
- const shouldWaitForNavigation = (interaction.type === 'link' || interaction.type === 'form') && !isFileOrigin;
266
+ let shouldWaitForNavigation = (interaction.type === 'link' || interaction.type === 'form') && !isFileOrigin;
267
267
  let navigationResult = null;
268
268
 
269
269
  try {
@@ -300,7 +300,7 @@ export async function runInteraction(page, interaction, timestamp, i, screenshot
300
300
  }
301
301
  } else if (interaction.type === 'logout') {
302
302
  // Logout action: click logout and observe session changes
303
- const logoutResult = await humanDriver.executeLogout(page, locator);
303
+ const logoutResult = await humanDriver.performLogout(page);
304
304
  const sessionStateAfter = await humanDriver.captureSessionState(page);
305
305
  trace.logout = {
306
306
  clicked: logoutResult.clicked,
@@ -84,6 +84,12 @@ export class LoadingSensor {
84
84
  // Set up interval to check loading state (every 100ms for deterministic detection)
85
85
  const intervalId = setInterval(checkLoading, 100);
86
86
 
87
+ // CRITICAL: Unref the interval so it doesn't keep the process alive
88
+ // This allows tests to exit cleanly even if stopWindow() is not called
89
+ if (intervalId && intervalId.unref) {
90
+ intervalId.unref();
91
+ }
92
+
87
93
  // Immediately check once
88
94
  checkLoading();
89
95
 
@@ -109,7 +109,7 @@ export class NavigationSensor {
109
109
  *
110
110
  * @param {number} windowId - Window ID
111
111
  * @param {Object} page - Playwright page
112
- * @returns {Object} - Navigation summary
112
+ * @returns {Promise<any>} - Navigation summary
113
113
  */
114
114
  async stopWindow(windowId, page) {
115
115
  const state = this.windows.get(windowId);
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * WAVE 2: Deterministic DOM settle logic
3
3
  * Waits for page to stabilize after navigation or interaction
4
+ * @typedef {import('playwright').Page} Page
4
5
  */
5
6
 
6
7
  import { DEFAULT_SCAN_BUDGET } from '../shared/scan-budget.js';
@@ -58,7 +58,10 @@ class ReduxSensor {
58
58
  resolve();
59
59
  } else {
60
60
  // Wait up to 5 seconds for store initialization
61
- const timeout = setTimeout(() => resolve(), 5000);
61
+ const timeout = setTimeout(() => {
62
+ clearInterval(check);
63
+ resolve();
64
+ }, 5000);
62
65
  const check = setInterval(() => {
63
66
  if (window.__REDUX_STORE__) {
64
67
  clearInterval(check);
@@ -125,7 +128,7 @@ class ReduxSensor {
125
128
  // Shallow copy of top-level keys only (privacy: no values)
126
129
  const snapshot = {};
127
130
  for (const key in state) {
128
- if (state.hasOwnProperty(key)) {
131
+ if (Object.prototype.hasOwnProperty.call(state, key)) {
129
132
  snapshot[key] = '[REDACTED]'; // Never store values, only keys
130
133
  }
131
134
  }
@@ -222,8 +225,9 @@ class ZustandSensor {
222
225
  // Look for common Zustand patterns in window object
223
226
  for (const key in window) {
224
227
  if (key.startsWith('use') && typeof window[key] === 'function') {
225
- const store = window[key];
226
- if (store.getState && typeof store.getState === 'function') {
228
+ // Dynamic property access on window for Zustand store detection (runtime property)
229
+ const store = /** @type {any} */ (window[key]);
230
+ if (store && typeof store === 'object' && 'getState' in store && typeof store.getState === 'function') {
227
231
  return true;
228
232
  }
229
233
  }
@@ -22,13 +22,14 @@ export class StateUISensor {
22
22
  */
23
23
  async snapshot(page, contextSelector = null) {
24
24
  try {
25
- const snapshot = await page.evaluate(({ selector }) => {
25
+ const snapshot = await page.evaluate(() => {
26
26
  const signals = {};
27
27
  const rawSnapshot = {};
28
28
 
29
29
  // 1. Dialog/Modal signals
30
30
  signals.dialogs = [];
31
31
  const dialogs = document.querySelectorAll('[role="dialog"]');
32
+ // @ts-expect-error - NodeListOf is iterable in browser context
32
33
  for (const dialog of dialogs) {
33
34
  const isVisible = dialog.offsetParent !== null || dialog.hasAttribute('open');
34
35
  const ariaModal = dialog.getAttribute('aria-modal');
@@ -46,6 +47,7 @@ export class StateUISensor {
46
47
  // 2. Expansion state (aria-expanded)
47
48
  signals.expandedElements = [];
48
49
  const expandables = document.querySelectorAll('[aria-expanded]');
50
+ // @ts-expect-error - NodeListOf is iterable in browser context
49
51
  for (const el of expandables) {
50
52
  const expanded = el.getAttribute('aria-expanded') === 'true';
51
53
  signals.expandedElements.push({
@@ -58,6 +60,7 @@ export class StateUISensor {
58
60
  // 3. Tab selection (role=tab with aria-selected)
59
61
  signals.selectedTabs = [];
60
62
  const tabs = document.querySelectorAll('[role="tab"]');
63
+ // @ts-expect-error - NodeListOf is iterable in browser context
61
64
  for (const tab of tabs) {
62
65
  const selected = tab.getAttribute('aria-selected') === 'true';
63
66
  signals.selectedTabs.push({
@@ -70,6 +73,7 @@ export class StateUISensor {
70
73
  // 4. Checkbox/toggle state (aria-checked)
71
74
  signals.checkedElements = [];
72
75
  const checkables = document.querySelectorAll('[aria-checked]');
76
+ // @ts-expect-error - NodeListOf is iterable in browser context
73
77
  for (const el of checkables) {
74
78
  const checked = el.getAttribute('aria-checked') === 'true';
75
79
  signals.checkedElements.push({
@@ -82,6 +86,7 @@ export class StateUISensor {
82
86
  // 5. Alert/Status changes (role=alert, role=status)
83
87
  signals.alerts = [];
84
88
  const alerts = document.querySelectorAll('[role="alert"], [role="status"]');
89
+ // @ts-expect-error - NodeListOf is iterable in browser context
85
90
  for (const alert of alerts) {
86
91
  const text = alert.textContent?.trim() || '';
87
92
  signals.alerts.push({
@@ -96,6 +101,7 @@ export class StateUISensor {
96
101
  // Count visible nodes and text nodes (excluding style/comment nodes)
97
102
  const countMeaningfulNodes = () => {
98
103
  let count = 0;
104
+ // @ts-expect-error - NodeListOf is iterable in browser context
99
105
  for (const node of document.querySelectorAll('*')) {
100
106
  if (node.offsetParent !== null) { // Visible
101
107
  count++;
@@ -1,7 +1,23 @@
1
- import { resolve } from 'path';
2
1
  import { writeFileSync, mkdirSync } from 'fs';
3
2
  import { getArtifactPath, getRunArtifactDir } from '../core/run-id.js';
4
3
 
4
+ /**
5
+ * @typedef {Object} WriteTracesResult
6
+ * @property {number} version
7
+ * @property {string} observedAt
8
+ * @property {string} url
9
+ * @property {Array} traces
10
+ * @property {Array} [observedExpectations]
11
+ * @property {Object} [coverage]
12
+ * @property {Array} [warnings]
13
+ * @property {Object} [silences] - Added by writeTraces if silenceTracker provided
14
+ * @property {string} tracesPath
15
+ * @property {Object} observeTruth
16
+ * @property {Object} [expectationExecution] - Added by caller after writeTraces
17
+ * @property {Array} [expectationCoverageGaps] - Added by caller after writeTraces
18
+ * @property {Object} [incremental] - Added by caller after writeTraces
19
+ */
20
+
5
21
  /**
6
22
  * SILENCE TRACKING: Write observation traces with explicit silence tracking.
7
23
  * All gaps, skips, caps, and unknowns must be recorded and surfaced.
@@ -11,24 +27,19 @@ import { getArtifactPath, getRunArtifactDir } from '../core/run-id.js';
11
27
  * @param {string} projectDir - Project directory
12
28
  * @param {string} url - URL observed
13
29
  * @param {Array} traces - Execution traces
14
- * @param {Object} coverage - Coverage data (if capped, this is a silence)
15
- * @param {Array} warnings - Warnings (caps are silences)
16
- * @param {Array} observedExpectations - Observed expectations
17
- * @param {Object} silenceTracker - Silence tracker (optional)
18
- * @param {string} runId - Run identifier (Phase 5)
30
+ * @param {Object} [coverage] - Coverage data (if capped, this is a silence)
31
+ * @param {Array} [warnings] - Warnings (caps are silences)
32
+ * @param {Array} [observedExpectations] - Observed expectations
33
+ * @param {Object} [silenceTracker] - Silence tracker (optional)
34
+ * @param {string} [runId] - Run identifier (Phase 5) - required but optional in signature for type compatibility
35
+ * @returns {WriteTracesResult}
19
36
  */
20
37
  export function writeTraces(projectDir, url, traces, coverage = null, warnings = [], observedExpectations = [], silenceTracker = null, runId = null) {
21
- // PHASE 5: Use deterministic artifact path if runId provided, otherwise fall back to old path
22
- let observeDir, tracesPath;
23
- if (runId) {
24
- observeDir = getRunArtifactDir(projectDir, runId);
25
- tracesPath = getArtifactPath(projectDir, runId, 'traces.json');
26
- } else {
27
- // Backwards compatibility for tests
28
- observeDir = resolve(projectDir, '.veraxverax', 'observe');
29
- tracesPath = resolve(observeDir, 'observation-traces.json');
38
+ if (!runId) {
39
+ throw new Error('runId is required');
30
40
  }
31
-
41
+ const observeDir = getRunArtifactDir(projectDir, runId);
42
+ const tracesPath = getArtifactPath(projectDir, runId, 'traces.json');
32
43
  mkdirSync(observeDir, { recursive: true });
33
44
 
34
45
  const observation = {
@@ -41,6 +41,7 @@ export class UISignalSensor {
41
41
  const statusRegions = Array.from(document.querySelectorAll('[role="status"], [role="alert"]'));
42
42
  const visibleStatusRegions = statusRegions.filter((el) => {
43
43
  const style = window.getComputedStyle(el);
44
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
44
45
  return el.offsetParent !== null && style.visibility !== 'hidden' && style.display !== 'none' && style.opacity !== '0';
45
46
  });
46
47
  if (visibleStatusRegions.length > 0) {
@@ -57,6 +58,7 @@ export class UISignalSensor {
57
58
 
58
59
  // Check for dialogs
59
60
  const dialog = document.querySelector('[role="dialog"], [aria-modal="true"]');
61
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
60
62
  if (dialog && dialog.offsetParent !== null) {
61
63
  // offsetParent is null if element is hidden
62
64
  result.hasDialog = true;
@@ -90,6 +92,7 @@ export class UISignalSensor {
90
92
 
91
93
  for (const invalidEl of invalidElements) {
92
94
  const style = window.getComputedStyle(invalidEl);
95
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
93
96
  const isVisible = invalidEl.offsetParent !== null &&
94
97
  style.visibility !== 'hidden' &&
95
98
  style.display !== 'none' &&
@@ -120,6 +123,7 @@ export class UISignalSensor {
120
123
  const errorText = Array.from(parent.querySelectorAll('[role="alert"], .error, .invalid-feedback'))
121
124
  .find(el => {
122
125
  const elStyle = window.getComputedStyle(el);
126
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
123
127
  return el.offsetParent !== null &&
124
128
  elStyle.visibility !== 'hidden' &&
125
129
  elStyle.display !== 'none' &&
@@ -138,6 +142,7 @@ export class UISignalSensor {
138
142
  const alertRegions = Array.from(document.querySelectorAll('[role="alert"], [role="status"]'));
139
143
  const visibleAlertRegions = alertRegions.filter((el) => {
140
144
  const style = window.getComputedStyle(el);
145
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
141
146
  const isVisible = el.offsetParent !== null &&
142
147
  style.visibility !== 'hidden' &&
143
148
  style.display !== 'none' &&
@@ -149,6 +154,7 @@ export class UISignalSensor {
149
154
  const liveRegions = Array.from(document.querySelectorAll('[aria-live]'));
150
155
  const visibleLiveRegions = liveRegions.filter((el) => {
151
156
  const style = window.getComputedStyle(el);
157
+ // @ts-expect-error - offsetParent exists on HTMLElement in browser context
152
158
  const isVisible = el.offsetParent !== null &&
153
159
  style.visibility !== 'hidden' &&
154
160
  style.display !== 'none' &&
@@ -176,6 +182,7 @@ export class UISignalSensor {
176
182
  '[role="alert"], [class*="error"], [class*="danger"]'
177
183
  );
178
184
  if (errorMessages.length > 0) {
185
+ // @ts-expect-error - NodeListOf is iterable in browser context
179
186
  for (const elem of errorMessages) {
180
187
  const text = elem.textContent.trim().slice(0, 50);
181
188
  if (text && (text.toLowerCase().includes('error') || text.toLowerCase().includes('fail'))) {
@@ -4,8 +4,11 @@ import { computeExpectationsSummary } from './shared/artifact-manager.js';
4
4
  import { createImpactSummary } from './core/silence-impact.js';
5
5
  import { computeDecisionSnapshot } from './core/decision-snapshot.js';
6
6
 
7
- export function writeScanSummary(projectDir, url, projectType, learnTruth, observeTruth, detectTruth, manifestPath, tracesPath, findingsPath, runDirOpt = null, findingsArray = null) {
8
- const scanDir = runDirOpt ? resolve(runDirOpt) : resolve(projectDir, '.veraxverax', 'scan');
7
+ export function writeScanSummary(projectDir, url, projectType, learnTruth, observeTruth, detectTruth, manifestPath, tracesPath, findingsPath, runDirOpt, findingsArray = null) {
8
+ if (!runDirOpt) {
9
+ throw new Error('runDirOpt is required');
10
+ }
11
+ const scanDir = resolve(runDirOpt);
9
12
  mkdirSync(scanDir, { recursive: true });
10
13
 
11
14
  // Compute expectations summary from manifest
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { existsSync, mkdirSync, writeFileSync, appendFileSync } from 'fs';
16
- import { resolve, join } from 'path';
16
+ import { resolve } from 'path';
17
17
  import { randomBytes } from 'crypto';
18
18
 
19
19
  /**
@@ -11,7 +11,7 @@
11
11
  * - EXHAUSTIVE: 300 seconds, maximum coverage (deep audit)
12
12
  */
13
13
 
14
- import { DEFAULT_SCAN_BUDGET, createScanBudget } from './scan-budget.js';
14
+ import { createScanBudget } from './scan-budget.js';
15
15
 
16
16
  /**
17
17
  * QUICK profile: Fast feedback for development
@@ -107,7 +107,7 @@ export function getActiveBudgetProfile() {
107
107
 
108
108
  /**
109
109
  * Create a scan budget with the active profile applied.
110
- * @returns {ScanBudget} Complete scan budget with profile applied
110
+ * @returns {Object} Complete scan budget with profile applied
111
111
  */
112
112
  export function createScanBudgetWithProfile() {
113
113
  const profile = getActiveBudgetProfile();
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { createHash } from 'crypto';
12
12
  import { readFileSync, existsSync, statSync } from 'fs';
13
- import { resolve, join } from 'path';
13
+ import { resolve } from 'path';
14
14
 
15
15
  const memoryCache = new Map(); // Global in-memory cache
16
16