@veraxhq/verax 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +28 -20
  2. package/bin/verax.js +11 -18
  3. package/package.json +28 -7
  4. package/src/cli/commands/baseline.js +1 -2
  5. package/src/cli/commands/default.js +72 -81
  6. package/src/cli/commands/doctor.js +29 -0
  7. package/src/cli/commands/ga.js +3 -0
  8. package/src/cli/commands/gates.js +1 -1
  9. package/src/cli/commands/inspect.js +6 -133
  10. package/src/cli/commands/release-check.js +2 -0
  11. package/src/cli/commands/run.js +74 -246
  12. package/src/cli/commands/security-check.js +2 -1
  13. package/src/cli/commands/truth.js +0 -1
  14. package/src/cli/entry.js +82 -309
  15. package/src/cli/util/angular-component-extractor.js +2 -2
  16. package/src/cli/util/angular-navigation-detector.js +2 -2
  17. package/src/cli/util/ast-interactive-detector.js +4 -6
  18. package/src/cli/util/ast-network-detector.js +3 -3
  19. package/src/cli/util/ast-promise-extractor.js +581 -0
  20. package/src/cli/util/ast-usestate-detector.js +3 -3
  21. package/src/cli/util/atomic-write.js +12 -1
  22. package/src/cli/util/console-reporter.js +72 -0
  23. package/src/cli/util/detection-engine.js +105 -41
  24. package/src/cli/util/determinism-runner.js +2 -1
  25. package/src/cli/util/determinism-writer.js +1 -1
  26. package/src/cli/util/digest-engine.js +359 -0
  27. package/src/cli/util/dom-diff.js +226 -0
  28. package/src/cli/util/env-url.js +0 -4
  29. package/src/cli/util/evidence-engine.js +287 -0
  30. package/src/cli/util/expectation-extractor.js +217 -367
  31. package/src/cli/util/findings-writer.js +19 -126
  32. package/src/cli/util/framework-detector.js +572 -0
  33. package/src/cli/util/idgen.js +1 -1
  34. package/src/cli/util/interaction-planner.js +529 -0
  35. package/src/cli/util/learn-writer.js +2 -2
  36. package/src/cli/util/ledger-writer.js +110 -0
  37. package/src/cli/util/monorepo-resolver.js +162 -0
  38. package/src/cli/util/observation-engine.js +127 -278
  39. package/src/cli/util/observe-writer.js +2 -2
  40. package/src/cli/util/paths.js +12 -3
  41. package/src/cli/util/project-discovery.js +284 -3
  42. package/src/cli/util/project-writer.js +2 -2
  43. package/src/cli/util/run-id.js +23 -27
  44. package/src/cli/util/run-result.js +778 -0
  45. package/src/cli/util/selector-resolver.js +235 -0
  46. package/src/cli/util/summary-writer.js +2 -1
  47. package/src/cli/util/svelte-navigation-detector.js +3 -3
  48. package/src/cli/util/svelte-sfc-extractor.js +0 -1
  49. package/src/cli/util/svelte-state-detector.js +1 -2
  50. package/src/cli/util/trust-activation-integration.js +496 -0
  51. package/src/cli/util/trust-activation-wrapper.js +85 -0
  52. package/src/cli/util/trust-integration-hooks.js +164 -0
  53. package/src/cli/util/types.js +153 -0
  54. package/src/cli/util/url-validation.js +40 -0
  55. package/src/cli/util/vue-navigation-detector.js +4 -3
  56. package/src/cli/util/vue-sfc-extractor.js +1 -2
  57. package/src/cli/util/vue-state-detector.js +1 -1
  58. package/src/types/fs-augment.d.ts +23 -0
  59. package/src/types/global.d.ts +137 -0
  60. package/src/types/internal-types.d.ts +35 -0
  61. package/src/verax/cli/finding-explainer.js +3 -56
  62. package/src/verax/cli/init.js +4 -18
  63. package/src/verax/core/action-classifier.js +4 -3
  64. package/src/verax/core/artifacts/registry.js +0 -15
  65. package/src/verax/core/artifacts/verifier.js +18 -8
  66. package/src/verax/core/baseline/baseline.snapshot.js +2 -0
  67. package/src/verax/core/capabilities/gates.js +7 -1
  68. package/src/verax/core/confidence/confidence-compute.js +14 -7
  69. package/src/verax/core/confidence/confidence.loader.js +1 -0
  70. package/src/verax/core/confidence-engine-refactor.js +8 -3
  71. package/src/verax/core/confidence-engine.js +162 -23
  72. package/src/verax/core/contracts/types.js +1 -0
  73. package/src/verax/core/contracts/validators.js +79 -4
  74. package/src/verax/core/decision-snapshot.js +3 -30
  75. package/src/verax/core/decisions/decision.trace.js +2 -0
  76. package/src/verax/core/determinism/contract-writer.js +2 -2
  77. package/src/verax/core/determinism/contract.js +1 -1
  78. package/src/verax/core/determinism/diff.js +42 -1
  79. package/src/verax/core/determinism/engine.js +7 -6
  80. package/src/verax/core/determinism/finding-identity.js +3 -2
  81. package/src/verax/core/determinism/normalize.js +32 -4
  82. package/src/verax/core/determinism/report-writer.js +1 -0
  83. package/src/verax/core/determinism/run-fingerprint.js +7 -2
  84. package/src/verax/core/dynamic-route-intelligence.js +8 -7
  85. package/src/verax/core/evidence/evidence-capture-service.js +1 -0
  86. package/src/verax/core/evidence/evidence-intent-ledger.js +2 -1
  87. package/src/verax/core/evidence-builder.js +2 -2
  88. package/src/verax/core/execution-mode-context.js +1 -1
  89. package/src/verax/core/execution-mode-detector.js +5 -3
  90. package/src/verax/core/failures/exit-codes.js +39 -37
  91. package/src/verax/core/failures/failure-summary.js +1 -1
  92. package/src/verax/core/failures/failure.factory.js +3 -3
  93. package/src/verax/core/failures/failure.ledger.js +3 -2
  94. package/src/verax/core/ga/ga.artifact.js +1 -1
  95. package/src/verax/core/ga/ga.contract.js +3 -2
  96. package/src/verax/core/ga/ga.enforcer.js +1 -0
  97. package/src/verax/core/guardrails/policy.loader.js +1 -0
  98. package/src/verax/core/guardrails/truth-reconciliation.js +1 -1
  99. package/src/verax/core/guardrails-engine.js +2 -2
  100. package/src/verax/core/incremental-store.js +1 -0
  101. package/src/verax/core/integrity/budget.js +138 -0
  102. package/src/verax/core/integrity/determinism.js +342 -0
  103. package/src/verax/core/integrity/integrity.js +208 -0
  104. package/src/verax/core/integrity/poisoning.js +108 -0
  105. package/src/verax/core/integrity/transaction.js +140 -0
  106. package/src/verax/core/observe/run-timeline.js +2 -0
  107. package/src/verax/core/perf/perf.report.js +2 -0
  108. package/src/verax/core/pipeline-tracker.js +5 -0
  109. package/src/verax/core/release/provenance.builder.js +73 -214
  110. package/src/verax/core/release/release.enforcer.js +14 -9
  111. package/src/verax/core/release/reproducibility.check.js +1 -0
  112. package/src/verax/core/release/sbom.builder.js +32 -23
  113. package/src/verax/core/replay-validator.js +2 -0
  114. package/src/verax/core/replay.js +4 -0
  115. package/src/verax/core/report/cross-index.js +6 -3
  116. package/src/verax/core/report/human-summary.js +141 -1
  117. package/src/verax/core/route-intelligence.js +4 -3
  118. package/src/verax/core/run-id.js +6 -3
  119. package/src/verax/core/run-manifest.js +4 -3
  120. package/src/verax/core/security/secrets.scan.js +10 -7
  121. package/src/verax/core/security/security.enforcer.js +4 -0
  122. package/src/verax/core/security/supplychain.policy.js +9 -1
  123. package/src/verax/core/security/vuln.scan.js +2 -2
  124. package/src/verax/core/truth/truth.certificate.js +3 -1
  125. package/src/verax/core/ui-feedback-intelligence.js +12 -46
  126. package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
  127. package/src/verax/detect/confidence-engine.js +100 -660
  128. package/src/verax/detect/confidence-helper.js +1 -0
  129. package/src/verax/detect/detection-engine.js +1 -18
  130. package/src/verax/detect/dynamic-route-findings.js +17 -14
  131. package/src/verax/detect/expectation-chain-detector.js +1 -1
  132. package/src/verax/detect/expectation-model.js +3 -5
  133. package/src/verax/detect/failure-cause-inference.js +293 -0
  134. package/src/verax/detect/findings-writer.js +126 -166
  135. package/src/verax/detect/flow-detector.js +2 -2
  136. package/src/verax/detect/form-silent-failure.js +98 -0
  137. package/src/verax/detect/index.js +51 -234
  138. package/src/verax/detect/invariants-enforcer.js +147 -0
  139. package/src/verax/detect/journey-stall-detector.js +4 -4
  140. package/src/verax/detect/navigation-silent-failure.js +82 -0
  141. package/src/verax/detect/problem-aggregator.js +361 -0
  142. package/src/verax/detect/route-findings.js +7 -6
  143. package/src/verax/detect/summary-writer.js +477 -0
  144. package/src/verax/detect/test-failure-cause-inference.js +314 -0
  145. package/src/verax/detect/ui-feedback-findings.js +18 -18
  146. package/src/verax/detect/verdict-engine.js +3 -57
  147. package/src/verax/detect/view-switch-correlator.js +2 -2
  148. package/src/verax/flow/flow-engine.js +2 -1
  149. package/src/verax/flow/flow-spec.js +0 -6
  150. package/src/verax/index.js +48 -412
  151. package/src/verax/intel/ts-program.js +1 -0
  152. package/src/verax/intel/vue-navigation-extractor.js +3 -0
  153. package/src/verax/learn/action-contract-extractor.js +67 -682
  154. package/src/verax/learn/ast-contract-extractor.js +1 -1
  155. package/src/verax/learn/flow-extractor.js +1 -0
  156. package/src/verax/learn/project-detector.js +5 -0
  157. package/src/verax/learn/react-router-extractor.js +2 -0
  158. package/src/verax/learn/route-validator.js +1 -4
  159. package/src/verax/learn/source-instrumenter.js +1 -0
  160. package/src/verax/learn/state-extractor.js +2 -1
  161. package/src/verax/learn/static-extractor.js +1 -0
  162. package/src/verax/observe/coverage-gaps.js +132 -0
  163. package/src/verax/observe/expectation-handler.js +126 -0
  164. package/src/verax/observe/incremental-skip.js +46 -0
  165. package/src/verax/observe/index.js +735 -84
  166. package/src/verax/observe/interaction-executor.js +192 -0
  167. package/src/verax/observe/interaction-runner.js +782 -530
  168. package/src/verax/observe/network-firewall.js +86 -0
  169. package/src/verax/observe/observation-builder.js +169 -0
  170. package/src/verax/observe/observe-context.js +1 -1
  171. package/src/verax/observe/observe-helpers.js +2 -1
  172. package/src/verax/observe/observe-runner.js +28 -24
  173. package/src/verax/observe/observers/budget-observer.js +3 -3
  174. package/src/verax/observe/observers/console-observer.js +4 -4
  175. package/src/verax/observe/observers/coverage-observer.js +4 -4
  176. package/src/verax/observe/observers/interaction-observer.js +3 -3
  177. package/src/verax/observe/observers/navigation-observer.js +4 -4
  178. package/src/verax/observe/observers/network-observer.js +4 -4
  179. package/src/verax/observe/observers/safety-observer.js +1 -1
  180. package/src/verax/observe/observers/ui-feedback-observer.js +4 -4
  181. package/src/verax/observe/page-traversal.js +138 -0
  182. package/src/verax/observe/snapshot-ops.js +94 -0
  183. package/src/verax/observe/ui-signal-sensor.js +2 -148
  184. package/src/verax/scan-summary-writer.js +10 -42
  185. package/src/verax/shared/artifact-manager.js +30 -13
  186. package/src/verax/shared/caching.js +1 -0
  187. package/src/verax/shared/expectation-tracker.js +1 -0
  188. package/src/verax/shared/zip-artifacts.js +6 -0
  189. package/src/verax/core/confidence-engine.js.backup +0 -471
  190. package/src/verax/shared/config-loader.js +0 -169
  191. /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
@@ -0,0 +1,86 @@
1
+ /**
2
+ * NETWORK SAFETY FIREWALL
3
+ *
4
+ * Handles network request interception and blocking:
5
+ * - Cross-origin blocking (unless --allow-cross-origin)
6
+ * - Read-only mode (blocks POST/PUT/PATCH/DELETE unconditionally)
7
+ * - Safety tracking via SilenceTracker
8
+ */
9
+
10
+ /**
11
+ * Setup network interception firewall for safety mode
12
+ *
13
+ * @param {Object} page - Playwright page object
14
+ * @param {string} baseOrigin - Base origin URL for cross-origin checks
15
+ * @param {boolean} allowCrossOrigin - Whether to allow cross-origin requests
16
+ * @param {Object} silenceTracker - Silence tracker instance
17
+ * @returns {Promise<{blockedNetworkWrites: Array, blockedCrossOrigin: Array}>}
18
+ */
19
+ export async function setupNetworkFirewall(page, baseOrigin, allowCrossOrigin, silenceTracker) {
20
+ const blockedNetworkWrites = [];
21
+ const blockedCrossOrigin = [];
22
+
23
+ await page.route('**/*', (route) => {
24
+ const request = route.request();
25
+ const method = request.method();
26
+ const requestUrl = request.url();
27
+ const resourceType = request.resourceType();
28
+
29
+ // Check cross-origin blocking (skip for file:// URLs)
30
+ if (!allowCrossOrigin && !requestUrl.startsWith('file://')) {
31
+ try {
32
+ const reqOrigin = new URL(requestUrl).origin;
33
+ if (reqOrigin !== baseOrigin) {
34
+ blockedCrossOrigin.push({
35
+ url: requestUrl,
36
+ origin: reqOrigin,
37
+ method,
38
+ resourceType,
39
+ timestamp: Date.now()
40
+ });
41
+
42
+ silenceTracker.record({
43
+ scope: 'safety',
44
+ reason: 'cross_origin_blocked',
45
+ description: `Cross-origin request blocked: ${method} ${requestUrl}`,
46
+ context: { url: requestUrl, origin: reqOrigin, method, baseOrigin },
47
+ impact: 'request_blocked'
48
+ });
49
+
50
+ return route.abort('blockedbyclient');
51
+ }
52
+ } catch (e) {
53
+ // Invalid URL, allow and let browser handle
54
+ }
55
+ }
56
+
57
+ // CONSTITUTIONAL: Block all write methods (read-only mode enforced)
58
+ if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
59
+ // Check if it's a GraphQL mutation (best-effort)
60
+ const isGraphQLMutation = requestUrl.includes('/graphql') && method === 'POST';
61
+
62
+ blockedNetworkWrites.push({
63
+ url: requestUrl,
64
+ method,
65
+ resourceType,
66
+ isGraphQLMutation,
67
+ timestamp: Date.now()
68
+ });
69
+
70
+ silenceTracker.record({
71
+ scope: 'safety',
72
+ reason: 'blocked_network_write',
73
+ description: `Network write blocked: ${method} ${requestUrl}${isGraphQLMutation ? ' (GraphQL mutation)' : ''}`,
74
+ context: { url: requestUrl, method, resourceType, isGraphQLMutation },
75
+ impact: 'write_blocked'
76
+ });
77
+
78
+ return route.abort('blockedbyclient');
79
+ }
80
+
81
+ // Allow request
82
+ route.continue();
83
+ });
84
+
85
+ return { blockedNetworkWrites, blockedCrossOrigin };
86
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * OBSERVATION BUILDER
3
+ *
4
+ * Constructs observation object from traces, calculates coverage, and generates warnings.
5
+ */
6
+
7
+ /**
8
+ * Build observation object from collected traces
9
+ *
10
+ * @param {Array} traces - Array of interaction traces
11
+ * @param {Array} coverage - Coverage information
12
+ * @param {Array} warnings - Warning messages
13
+ * @param {Object} safetyStats - Safety mode statistics
14
+ * @returns {Object}
15
+ */
16
+ export function buildObservation(traces, coverage, warnings, safetyStats) {
17
+ const observation = {
18
+ timestamp: new Date().toISOString(),
19
+ traces: traces,
20
+ coverage: coverage || [],
21
+ warnings: warnings || [],
22
+ safetyStats: safetyStats || {}
23
+ };
24
+
25
+ return observation;
26
+ }
27
+
28
+ /**
29
+ * Calculate coverage gaps from traces
30
+ *
31
+ * @param {Array} traces - Array of interaction traces
32
+ * @param {Array} discoveredInteractions - All interactions discovered during scan
33
+ * @returns {Array}
34
+ */
35
+ export function calculateCoverageGaps(traces, discoveredInteractions) {
36
+ if (!discoveredInteractions || discoveredInteractions.length === 0) {
37
+ return [];
38
+ }
39
+
40
+ const executedSelectors = new Set(
41
+ traces
42
+ .filter(t => t.interaction && t.interaction.selector)
43
+ .map(t => t.interaction.selector)
44
+ );
45
+
46
+ const gaps = discoveredInteractions
47
+ .filter(interaction => !executedSelectors.has(interaction.selector))
48
+ .map(interaction => ({
49
+ selector: interaction.selector,
50
+ type: interaction.type,
51
+ reason: 'budget_exhausted_or_skipped'
52
+ }));
53
+
54
+ return gaps;
55
+ }
56
+
57
+ /**
58
+ * Generate warnings from observation data
59
+ *
60
+ * @param {Object} frontier - Page frontier
61
+ * @param {Array} coverage - Coverage data
62
+ * @param {Object} safetyStats - Safety statistics
63
+ * @returns {Array}
64
+ */
65
+ export function generateWarnings(frontier, coverage, safetyStats) {
66
+ const warnings = [];
67
+
68
+ // Warn if frontier has unexplored pages
69
+ if (frontier && frontier.queue && frontier.queue.length > 0) {
70
+ warnings.push({
71
+ type: 'unexplored_pages',
72
+ count: frontier.queue.length,
73
+ message: `${frontier.queue.length} pages in queue were not explored due to budget constraints`
74
+ });
75
+ }
76
+
77
+ // Warn if safety mode blocked actions
78
+ if (safetyStats && safetyStats.blockedNetworkWrites > 0) {
79
+ warnings.push({
80
+ type: 'safety_mode_active',
81
+ blockedWrites: safetyStats.blockedNetworkWrites,
82
+ message: `Safety mode prevented ${safetyStats.blockedNetworkWrites} write operations`
83
+ });
84
+ }
85
+
86
+ if (safetyStats && safetyStats.blockedCrossOrigin > 0) {
87
+ warnings.push({
88
+ type: 'cross_origin_blocked',
89
+ count: safetyStats.blockedCrossOrigin,
90
+ message: `${safetyStats.blockedCrossOrigin} cross-origin requests were blocked`
91
+ });
92
+ }
93
+
94
+ return warnings;
95
+ }
96
+
97
+ /**
98
+ * Build safety statistics object
99
+ *
100
+ * @param {number} blockedNetworkWrites - Number of blocked write operations
101
+ * @param {number} blockedCrossOrigin - Number of blocked cross-origin requests
102
+ * @param {Array} skippedInteractions - Interactions that were skipped
103
+ * @returns {Object}
104
+ */
105
+ export function buildSafetyStatistics(blockedNetworkWrites, blockedCrossOrigin, skippedInteractions) {
106
+ return {
107
+ safetyModeEnabled: true,
108
+ blockedNetworkWrites: blockedNetworkWrites || 0,
109
+ blockedCrossOrigin: blockedCrossOrigin || 0,
110
+ skippedInteractions: skippedInteractions ? skippedInteractions.length : 0,
111
+ skippedReasons: skippedInteractions ? countSkipReasons(skippedInteractions) : {}
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Count reasons for skipped interactions
117
+ *
118
+ * @param {Array} skippedInteractions - Skipped interactions
119
+ * @returns {Object}
120
+ */
121
+ function countSkipReasons(skippedInteractions) {
122
+ const reasons = {};
123
+ for (const skip of skippedInteractions) {
124
+ const reason = skip.reason || 'unknown';
125
+ reasons[reason] = (reasons[reason] || 0) + 1;
126
+ }
127
+ return reasons;
128
+ }
129
+
130
+ /**
131
+ * Extract observedExpectations from traces
132
+ *
133
+ * @param {Array} traces - Array of interaction traces
134
+ * @returns {Array}
135
+ */
136
+ export function extractObservedExpectations(traces) {
137
+ const expectations = [];
138
+
139
+ for (const trace of traces) {
140
+ if (trace.observedExpectation) {
141
+ expectations.push({
142
+ ...trace.observedExpectation,
143
+ traceId: trace.id,
144
+ executionTimestamp: trace.timestamp
145
+ });
146
+ }
147
+ }
148
+
149
+ return expectations;
150
+ }
151
+
152
+ /**
153
+ * Build skipped interactions summary
154
+ *
155
+ * @param {Array} interactions - Skipped interactions
156
+ * @returns {Array}
157
+ */
158
+ export function buildSkippedInteractionsSummary(interactions) {
159
+ if (!interactions || interactions.length === 0) {
160
+ return [];
161
+ }
162
+
163
+ return interactions.map(skip => ({
164
+ selector: skip.selector,
165
+ type: skip.type,
166
+ reason: skip.reason,
167
+ element: skip.element
168
+ }));
169
+ }
@@ -65,7 +65,7 @@
65
65
  /**
66
66
  * Forbidden imports that observers MUST NOT use
67
67
  */
68
- const FORBIDDEN_IMPORTS = [
68
+ const _FORBIDDEN_IMPORTS = [
69
69
  'fs',
70
70
  'path',
71
71
  '../core/determinism/report-writer',
@@ -13,7 +13,7 @@ import { writeTraces } from './traces-writer.js';
13
13
  */
14
14
  export async function setupManifestAndExpectations(manifestPath, projectDir, page, url, screenshotsDir, scanBudget, startTime, silenceTracker) {
15
15
  const { readFileSync, existsSync } = await import('fs');
16
- const { loadPreviousSnapshot, saveSnapshot, buildSnapshot, compareSnapshots } = await import('../core/incremental-store.js');
16
+ const { loadPreviousSnapshot, saveSnapshot: _saveSnapshot, buildSnapshot, compareSnapshots } = await import('../core/incremental-store.js');
17
17
  const { executeProvenExpectations } = await import('./expectation-executor.js');
18
18
  const { isProvenExpectation } = await import('../shared/expectation-prover.js');
19
19
 
@@ -27,6 +27,7 @@ export async function setupManifestAndExpectations(manifestPath, projectDir, pag
27
27
  if (manifestPath && existsSync(manifestPath)) {
28
28
  try {
29
29
  const manifestContent = readFileSync(manifestPath, 'utf-8');
30
+ // @ts-expect-error - readFileSync with encoding returns string
30
31
  manifest = JSON.parse(manifestContent);
31
32
 
32
33
  oldSnapshot = loadPreviousSnapshot(projectDir);
@@ -15,31 +15,31 @@ import { createObserveContext } from './observe-context.js';
15
15
  /**
16
16
  * Run main traversal/interaction loop
17
17
  *
18
- * @param {Object} params
19
- * @param {import('playwright').Page} params.page
20
- * @param {string} params.url
21
- * @param {string} params.baseOrigin
22
- * @param {Object} params.scanBudget
23
- * @param {number} params.startTime
24
- * @param {PageFrontier} params.frontier
25
- * @param {Object|null} params.manifest
26
- * @param {Object|null} params.expectationResults
27
- * @param {boolean} params.incrementalMode
28
- * @param {Object|null} params.oldSnapshot
29
- * @param {Object|null} params.snapshotDiff
30
- * @param {string} params.currentUrl
31
- * @param {string} params.screenshotsDir
32
- * @param {number} params.timestamp
18
+ * @param {Object} params - Parameters object
19
+ * @param {import('playwright').Page} params.page - Playwright page instance
20
+ * @param {string} params.url - Page URL
21
+ * @param {string} params.baseOrigin - Base origin for relative URLs
22
+ * @param {Object} params.scanBudget - Scan budget configuration
23
+ * @param {number} params.startTime - Scan start time
24
+ * @param {Object} params.frontier - Page frontier (URL queue)
25
+ * @param {Object|null} params.manifest - Manifest file data
26
+ * @param {Object|null} params.expectationResults - Expectation results
27
+ * @param {boolean} params.incrementalMode - Incremental mode flag
28
+ * @param {Object|null} params.oldSnapshot - Old snapshot data
29
+ * @param {Object|null} params.snapshotDiff - Snapshot diff
30
+ * @param {string} params.currentUrl - Current page URL
31
+ * @param {string} params.screenshotsDir - Screenshots directory
32
+ * @param {number} params.timestamp - Current timestamp
33
33
  * @param {Object} params.decisionRecorder - DecisionRecorder instance
34
34
  * @param {Object} params.silenceTracker - SilenceTracker instance
35
- * @param {Array} params.traces
36
- * @param {Array} params.skippedInteractions
37
- * @param {Array} params.observedExpectations
38
- * @param {number} params.totalInteractionsDiscovered
39
- * @param {number} params.totalInteractionsExecuted
40
- * @param {Array} params.remainingInteractionsGaps
41
- * @param {boolean} [params.allowWrites]
42
- * @param {boolean} [params.allowRiskyActions]
35
+ * @param {Array} params.traces - Interaction traces array
36
+ * @param {Array} params.skippedInteractions - Skipped interactions array
37
+ * @param {Array} params.observedExpectations - Observed expectations array
38
+ * @param {number} params.totalInteractionsDiscovered - Total discovered interactions
39
+ * @param {number} params.totalInteractionsExecuted - Total executed interactions
40
+ * @param {Array} params.remainingInteractionsGaps - Remaining interaction gaps
41
+ * @param {boolean} [params.allowWrites=false] - Allow file writes
42
+ * @param {boolean} [params.allowRiskyActions=false] - Allow risky actions
43
43
  * @returns {Promise<Object>} { traces, skippedInteractions, observedExpectations, totalInteractionsDiscovered, totalInteractionsExecuted, remainingInteractionsGaps }
44
44
  */
45
45
  export async function runTraversalLoop(params) {
@@ -110,7 +110,11 @@ export async function runTraversalLoop(params) {
110
110
  const context = createObserveContext({ ...baseContext, currentUrl, routeBudget: baseContext.routeBudget });
111
111
 
112
112
  // PHASE 21.3: Check page limit using budget-observer
113
- const pageLimitCheck = checkBudget(context, runState, { limitType: 'pages' });
113
+ const pageLimitCheck = checkBudget(context, runState, {
114
+ remainingInteractions: 0,
115
+ currentTotalExecuted: 0,
116
+ limitType: 'pages'
117
+ });
114
118
  if (pageLimitCheck.exceeded) break;
115
119
 
116
120
  // Check if we're already on the target page (from navigation via link click)
@@ -15,13 +15,13 @@ import { recordTruncation } from '../../core/determinism-model.js';
15
15
  /**
16
16
  * Check if budget limits are exceeded
17
17
  *
18
- * @param {ObserveContext} context - Observe context
19
- * @param {RunState} runState - Current run state
18
+ * @param {Object} context - Observe context
19
+ * @param {Object} runState - Current run state
20
20
  * @param {Object} options - Additional options
21
21
  * @param {number} options.remainingInteractions - Remaining interactions count
22
22
  * @param {number} options.currentTotalExecuted - Current total executed
23
23
  * @param {string} options.limitType - Type of limit to check ('time', 'per_page', 'total', 'pages')
24
- * @returns {Object} { exceeded: boolean, reason?: string, observation?: Observation }
24
+ * @returns {Object} { exceeded: boolean, reason?: string, observation?: Object }
25
25
  */
26
26
  export function checkBudget(context, runState, options) {
27
27
  const { scanBudget, startTime, routeBudget, decisionRecorder, silenceTracker, frontier, page } = context;
@@ -14,11 +14,11 @@ import { ConsoleSensor } from '../console-sensor.js';
14
14
  /**
15
15
  * Observe console errors and warnings on current page
16
16
  *
17
- * @param {ObserveContext} context - Observe context
18
- * @param {RunState} runState - Current run state
19
- * @returns {Promise<Array<Observation>>} Array of console observations
17
+ * @param {Object} context - Observe context
18
+ * @param {Object} _runState - Current run state
19
+ * @returns {Promise<Array<Object>>} Array of console observations
20
20
  */
21
- export async function observe(context, runState) {
21
+ export async function observe(context, _runState) {
22
22
  const { page, currentUrl, timestamp, silenceTracker } = context;
23
23
  const observations = [];
24
24
 
@@ -9,12 +9,12 @@
9
9
  * - NO side effects outside its scope
10
10
  */
11
11
 
12
- import { recordTruncation } from '../../core/determinism-model.js';
12
+ import { recordTruncation as _recordTruncation } from '../../core/determinism-model.js';
13
13
 
14
14
  /**
15
15
  * Create coverage gap for remaining interactions
16
16
  *
17
- * @param {ObserveContext} context - Observe context
17
+ * @param {Object} context - Observe context
18
18
  * @param {Array} remainingInteractions - Remaining interactions
19
19
  * @param {number} startIndex - Start index of remaining interactions
20
20
  * @param {string} reason - Reason for gap
@@ -42,7 +42,7 @@ export function createCoverageGaps(context, remainingInteractions, startIndex, r
42
42
  /**
43
43
  * Build coverage summary
44
44
  *
45
- * @param {ObserveContext} context - Observe context
45
+ * @param {Object} context - Observe context
46
46
  * @param {number} totalDiscovered - Total interactions discovered
47
47
  * @param {number} totalExecuted - Total interactions executed
48
48
  * @param {number} skippedCount - Number of skipped interactions
@@ -68,7 +68,7 @@ export function buildCoverageSummary(context, totalDiscovered, totalExecuted, sk
68
68
  /**
69
69
  * Create coverage gap for frontier capping
70
70
  *
71
- * @param {ObserveContext} context - Observe context
71
+ * @param {Object} context - Observe context
72
72
  * @returns {Object} Coverage gap
73
73
  */
74
74
  export function createFrontierCappedGap(context) {
@@ -56,8 +56,8 @@ export async function discoverInteractions(context) {
56
56
  * @param {string} currentUrl
57
57
  * @param {Array} traces - Mutable array to push skipped traces
58
58
  * @param {Array} skippedInteractions - Mutable array to push skipped interactions
59
- * @param {SilenceTracker} silenceTracker
60
- * @param {PageFrontier} frontier
59
+ * @param {Object} silenceTracker - Silence tracker instance
60
+ * @param {Object} frontier - Page frontier instance
61
61
  * @param {boolean} incrementalMode
62
62
  * @param {Object|null} manifest
63
63
  * @param {Object|null} oldSnapshot
@@ -255,7 +255,7 @@ export async function checkAndSkipInteraction(
255
255
  * @param {Array} traces - Mutable array to push traces
256
256
  * @param {Array} observedExpectations - Mutable array to push observed expectations
257
257
  * @param {Array} remainingInteractionsGaps - Mutable array to push gaps
258
- * @param {PageFrontier} frontier
258
+ * @param {Object} frontier - Page frontier instance
259
259
  * @param {string} baseOrigin
260
260
  * @param {boolean} incrementalMode
261
261
  * @param {Object|null} expectationResults
@@ -15,7 +15,7 @@ import { isExternalUrl } from '../domain-boundary.js';
15
15
  /**
16
16
  * Navigate to a URL
17
17
  *
18
- * @param {ObserveContext} context - Observe context
18
+ * @param {Object} context - Observe context
19
19
  * @param {string} targetUrl - URL to navigate to
20
20
  * @returns {Promise<boolean>} True if navigation succeeded, false if failed
21
21
  */
@@ -48,7 +48,7 @@ export async function navigateToPage(context, targetUrl) {
48
48
  /**
49
49
  * Discover links on current page and add to frontier
50
50
  *
51
- * @param {ObserveContext} context - Observe context
51
+ * @param {Object} context - Observe context
52
52
  * @returns {Promise<void>}
53
53
  */
54
54
  export async function discoverPageLinks(context) {
@@ -91,7 +91,7 @@ export async function discoverPageLinks(context) {
91
91
  /**
92
92
  * Check if we're already on the target page
93
93
  *
94
- * @param {ObserveContext} context - Observe context
94
+ * @param {Object} context - Observe context
95
95
  * @param {string} targetUrl - Target URL
96
96
  * @returns {boolean} True if already on page
97
97
  */
@@ -106,7 +106,7 @@ export function isAlreadyOnPage(context, targetUrl) {
106
106
  /**
107
107
  * Mark page as visited in frontier
108
108
  *
109
- * @param {ObserveContext} context - Observe context
109
+ * @param {Object} context - Observe context
110
110
  * @param {string} targetUrl - Target URL
111
111
  * @param {boolean} alreadyOnPage - Whether we're already on the page
112
112
  * @returns {void}
@@ -16,11 +16,11 @@ import { NetworkSensor } from '../network-sensor.js';
16
16
  /**
17
17
  * Observe network state on current page
18
18
  *
19
- * @param {ObserveContext} context - Observe context
20
- * @param {RunState} runState - Current run state
21
- * @returns {Promise<Array<Observation>>} Array of network observations
19
+ * @param {Object} context - Observe context
20
+ * @param {Object} _runState - Current run state
21
+ * @returns {Promise<Array<Object>>} Array of network observations
22
22
  */
23
- export async function observe(context, runState) {
23
+ export async function observe(context, _runState) {
24
24
  const { page, currentUrl, timestamp } = context;
25
25
  const observations = [];
26
26
 
@@ -10,7 +10,7 @@
10
10
  /**
11
11
  * Setup network interception firewall
12
12
  *
13
- * @param {ObserveContext} context - Observe context
13
+ * @param {Object} context - Observe context
14
14
  * @returns {Promise<void>}
15
15
  */
16
16
  export async function setupNetworkInterception(context) {
@@ -16,11 +16,11 @@ import { captureDomSignature } from '../dom-signature.js';
16
16
  /**
17
17
  * Observe UI feedback and DOM state on current page
18
18
  *
19
- * @param {ObserveContext} context - Observe context
20
- * @param {RunState} runState - Current run state
21
- * @returns {Promise<Array<Observation>>} Array of UI feedback observations
19
+ * @param {Object} context - Observe context
20
+ * @param {Object} _runState - Current run state
21
+ * @returns {Promise<Array<Object>>} Array of UI feedback observations
22
22
  */
23
- export async function observe(context, runState) {
23
+ export async function observe(context, _runState) {
24
24
  const { page, currentUrl, timestamp } = context;
25
25
  const observations = [];
26
26
 
@@ -0,0 +1,138 @@
1
+ /**
2
+ * PAGE TRAVERSAL ENGINE
3
+ *
4
+ * Manages page frontier, link discovery, and page-to-page navigation.
5
+ */
6
+
7
+ import { isExternalUrl } from './domain-boundary.js';
8
+
9
+ /**
10
+ * Discover links on current page
11
+ *
12
+ * @param {Object} page - Playwright page
13
+ * @param {string} baseOrigin - Base origin URL
14
+ * @param {Object} silenceTracker - Silence tracker
15
+ * @returns {Promise<Array>}
16
+ */
17
+ export async function discoverPageLinks(page, baseOrigin, silenceTracker) {
18
+ try {
19
+ // Discover all links on the page
20
+ const links = await page.locator('a[href]').all();
21
+ const linkData = [];
22
+
23
+ for (const link of links) {
24
+ const href = await link.getAttribute('href');
25
+ if (href) {
26
+ linkData.push({ href });
27
+ }
28
+ }
29
+
30
+ // Filter to same-origin links
31
+ const sameOriginLinks = linkData.filter(link => {
32
+ if (!link.href) return false;
33
+ return !isExternalUrl(link.href, baseOrigin);
34
+ });
35
+
36
+ return sameOriginLinks;
37
+ } catch (error) {
38
+ silenceTracker.record('link_discovery_error');
39
+ return [];
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Get next page URL from frontier
45
+ *
46
+ * @param {Object} frontier - Page frontier object
47
+ * @returns {string|null}
48
+ */
49
+ export function getNextPageUrl(frontier) {
50
+ if (!frontier || !frontier.queue || frontier.queue.length === 0) {
51
+ return null;
52
+ }
53
+
54
+ const nextUrl = frontier.queue[0];
55
+ return nextUrl;
56
+ }
57
+
58
+ /**
59
+ * Mark page as visited in frontier
60
+ *
61
+ * @param {Object} frontier - Page frontier object
62
+ * @param {string} url - URL that was visited
63
+ */
64
+ export function markPageVisited(frontier, url) {
65
+ if (!frontier) return;
66
+
67
+ // Remove from queue
68
+ if (frontier.queue && frontier.queue.length > 0) {
69
+ frontier.queue = frontier.queue.filter(u => u !== url);
70
+ }
71
+
72
+ // Add to visited
73
+ if (!frontier.visited) {
74
+ frontier.visited = [];
75
+ }
76
+ if (!frontier.visited.includes(url)) {
77
+ frontier.visited.push(url);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Add discovered links to frontier queue
83
+ *
84
+ * @param {Object} frontier - Page frontier object
85
+ * @param {Array} links - Links to add
86
+ */
87
+ export function addLinksToFrontier(frontier, links) {
88
+ if (!frontier || !links || links.length === 0) return;
89
+
90
+ if (!frontier.queue) {
91
+ frontier.queue = [];
92
+ }
93
+
94
+ for (const link of links) {
95
+ if (link.href && !frontier.queue.includes(link.href) &&
96
+ (!frontier.visited || !frontier.visited.includes(link.href))) {
97
+ frontier.queue.push(link.href);
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Check if page limit has been reached
104
+ *
105
+ * @param {number} pagesVisited - Number of pages visited
106
+ * @param {number} pageLimit - Maximum pages to visit
107
+ * @returns {boolean}
108
+ */
109
+ export function isPageLimitReached(pagesVisited, pageLimit) {
110
+ return pagesVisited >= pageLimit;
111
+ }
112
+
113
+ /**
114
+ * Cap frontier to maximum size
115
+ *
116
+ * @param {Object} frontier - Page frontier object
117
+ * @param {number} maxFrontierSize - Maximum frontier queue size
118
+ */
119
+ export function capFrontier(frontier, maxFrontierSize) {
120
+ if (!frontier || !frontier.queue) return;
121
+
122
+ if (frontier.queue.length > maxFrontierSize) {
123
+ frontier.queue = frontier.queue.slice(0, maxFrontierSize);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Initialize frontier for traversal
129
+ *
130
+ * @param {string} baseUrl - Starting URL
131
+ * @returns {Object}
132
+ */
133
+ export function initializeFrontier(baseUrl) {
134
+ return {
135
+ queue: [baseUrl],
136
+ visited: []
137
+ };
138
+ }