@veraxhq/verax 0.2.1 → 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 (213) hide show
  1. package/README.md +10 -6
  2. package/bin/verax.js +11 -11
  3. package/package.json +29 -8
  4. package/src/cli/commands/baseline.js +103 -0
  5. package/src/cli/commands/default.js +51 -6
  6. package/src/cli/commands/doctor.js +29 -0
  7. package/src/cli/commands/ga.js +246 -0
  8. package/src/cli/commands/gates.js +95 -0
  9. package/src/cli/commands/inspect.js +4 -2
  10. package/src/cli/commands/release-check.js +215 -0
  11. package/src/cli/commands/run.js +45 -6
  12. package/src/cli/commands/security-check.js +212 -0
  13. package/src/cli/commands/truth.js +113 -0
  14. package/src/cli/entry.js +30 -20
  15. package/src/cli/util/angular-component-extractor.js +179 -0
  16. package/src/cli/util/angular-navigation-detector.js +141 -0
  17. package/src/cli/util/angular-network-detector.js +161 -0
  18. package/src/cli/util/angular-state-detector.js +162 -0
  19. package/src/cli/util/ast-interactive-detector.js +544 -0
  20. package/src/cli/util/ast-network-detector.js +603 -0
  21. package/src/cli/util/ast-promise-extractor.js +581 -0
  22. package/src/cli/util/ast-usestate-detector.js +602 -0
  23. package/src/cli/util/atomic-write.js +12 -1
  24. package/src/cli/util/bootstrap-guard.js +86 -0
  25. package/src/cli/util/console-reporter.js +72 -0
  26. package/src/cli/util/detection-engine.js +105 -41
  27. package/src/cli/util/determinism-runner.js +124 -0
  28. package/src/cli/util/determinism-writer.js +129 -0
  29. package/src/cli/util/digest-engine.js +359 -0
  30. package/src/cli/util/dom-diff.js +226 -0
  31. package/src/cli/util/evidence-engine.js +287 -0
  32. package/src/cli/util/expectation-extractor.js +151 -5
  33. package/src/cli/util/findings-writer.js +3 -0
  34. package/src/cli/util/framework-detector.js +572 -0
  35. package/src/cli/util/idgen.js +1 -1
  36. package/src/cli/util/interaction-planner.js +529 -0
  37. package/src/cli/util/learn-writer.js +2 -0
  38. package/src/cli/util/ledger-writer.js +110 -0
  39. package/src/cli/util/monorepo-resolver.js +162 -0
  40. package/src/cli/util/observation-engine.js +127 -278
  41. package/src/cli/util/observe-writer.js +2 -0
  42. package/src/cli/util/project-discovery.js +284 -0
  43. package/src/cli/util/project-writer.js +2 -0
  44. package/src/cli/util/run-id.js +23 -27
  45. package/src/cli/util/run-resolver.js +64 -0
  46. package/src/cli/util/run-result.js +778 -0
  47. package/src/cli/util/selector-resolver.js +235 -0
  48. package/src/cli/util/source-requirement.js +55 -0
  49. package/src/cli/util/summary-writer.js +2 -0
  50. package/src/cli/util/svelte-navigation-detector.js +163 -0
  51. package/src/cli/util/svelte-network-detector.js +80 -0
  52. package/src/cli/util/svelte-sfc-extractor.js +146 -0
  53. package/src/cli/util/svelte-state-detector.js +242 -0
  54. package/src/cli/util/trust-activation-integration.js +496 -0
  55. package/src/cli/util/trust-activation-wrapper.js +85 -0
  56. package/src/cli/util/trust-integration-hooks.js +164 -0
  57. package/src/cli/util/types.js +153 -0
  58. package/src/cli/util/url-validation.js +40 -0
  59. package/src/cli/util/vue-navigation-detector.js +178 -0
  60. package/src/cli/util/vue-sfc-extractor.js +161 -0
  61. package/src/cli/util/vue-state-detector.js +215 -0
  62. package/src/types/fs-augment.d.ts +23 -0
  63. package/src/types/global.d.ts +137 -0
  64. package/src/types/internal-types.d.ts +35 -0
  65. package/src/verax/cli/init.js +4 -18
  66. package/src/verax/core/action-classifier.js +4 -3
  67. package/src/verax/core/artifacts/registry.js +139 -0
  68. package/src/verax/core/artifacts/verifier.js +990 -0
  69. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  70. package/src/verax/core/baseline/baseline.snapshot.js +233 -0
  71. package/src/verax/core/capabilities/gates.js +505 -0
  72. package/src/verax/core/capabilities/registry.js +475 -0
  73. package/src/verax/core/confidence/confidence-compute.js +144 -0
  74. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  75. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  76. package/src/verax/core/confidence/confidence-weights.js +44 -0
  77. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  78. package/src/verax/core/confidence/confidence.loader.js +80 -0
  79. package/src/verax/core/confidence/confidence.schema.js +94 -0
  80. package/src/verax/core/confidence-engine-refactor.js +489 -0
  81. package/src/verax/core/confidence-engine.js +625 -0
  82. package/src/verax/core/contracts/index.js +29 -0
  83. package/src/verax/core/contracts/types.js +186 -0
  84. package/src/verax/core/contracts/validators.js +456 -0
  85. package/src/verax/core/decisions/decision.trace.js +278 -0
  86. package/src/verax/core/determinism/contract-writer.js +89 -0
  87. package/src/verax/core/determinism/contract.js +139 -0
  88. package/src/verax/core/determinism/diff.js +405 -0
  89. package/src/verax/core/determinism/engine.js +222 -0
  90. package/src/verax/core/determinism/finding-identity.js +149 -0
  91. package/src/verax/core/determinism/normalize.js +466 -0
  92. package/src/verax/core/determinism/report-writer.js +93 -0
  93. package/src/verax/core/determinism/run-fingerprint.js +123 -0
  94. package/src/verax/core/dynamic-route-intelligence.js +529 -0
  95. package/src/verax/core/evidence/evidence-capture-service.js +308 -0
  96. package/src/verax/core/evidence/evidence-intent-ledger.js +166 -0
  97. package/src/verax/core/evidence-builder.js +487 -0
  98. package/src/verax/core/execution-mode-context.js +77 -0
  99. package/src/verax/core/execution-mode-detector.js +192 -0
  100. package/src/verax/core/failures/exit-codes.js +88 -0
  101. package/src/verax/core/failures/failure-summary.js +76 -0
  102. package/src/verax/core/failures/failure.factory.js +225 -0
  103. package/src/verax/core/failures/failure.ledger.js +133 -0
  104. package/src/verax/core/failures/failure.types.js +196 -0
  105. package/src/verax/core/failures/index.js +10 -0
  106. package/src/verax/core/ga/ga-report-writer.js +43 -0
  107. package/src/verax/core/ga/ga.artifact.js +49 -0
  108. package/src/verax/core/ga/ga.contract.js +435 -0
  109. package/src/verax/core/ga/ga.enforcer.js +87 -0
  110. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  111. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  112. package/src/verax/core/guardrails/policy.loader.js +84 -0
  113. package/src/verax/core/guardrails/policy.schema.js +110 -0
  114. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  115. package/src/verax/core/guardrails-engine.js +505 -0
  116. package/src/verax/core/incremental-store.js +1 -0
  117. package/src/verax/core/integrity/budget.js +138 -0
  118. package/src/verax/core/integrity/determinism.js +342 -0
  119. package/src/verax/core/integrity/integrity.js +208 -0
  120. package/src/verax/core/integrity/poisoning.js +108 -0
  121. package/src/verax/core/integrity/transaction.js +140 -0
  122. package/src/verax/core/observe/run-timeline.js +318 -0
  123. package/src/verax/core/perf/perf.contract.js +186 -0
  124. package/src/verax/core/perf/perf.display.js +65 -0
  125. package/src/verax/core/perf/perf.enforcer.js +91 -0
  126. package/src/verax/core/perf/perf.monitor.js +209 -0
  127. package/src/verax/core/perf/perf.report.js +200 -0
  128. package/src/verax/core/pipeline-tracker.js +243 -0
  129. package/src/verax/core/product-definition.js +127 -0
  130. package/src/verax/core/release/provenance.builder.js +130 -0
  131. package/src/verax/core/release/release-report-writer.js +40 -0
  132. package/src/verax/core/release/release.enforcer.js +164 -0
  133. package/src/verax/core/release/reproducibility.check.js +222 -0
  134. package/src/verax/core/release/sbom.builder.js +292 -0
  135. package/src/verax/core/replay-validator.js +2 -0
  136. package/src/verax/core/replay.js +4 -0
  137. package/src/verax/core/report/cross-index.js +195 -0
  138. package/src/verax/core/report/human-summary.js +362 -0
  139. package/src/verax/core/route-intelligence.js +420 -0
  140. package/src/verax/core/run-id.js +6 -3
  141. package/src/verax/core/run-manifest.js +4 -3
  142. package/src/verax/core/security/secrets.scan.js +329 -0
  143. package/src/verax/core/security/security-report.js +50 -0
  144. package/src/verax/core/security/security.enforcer.js +128 -0
  145. package/src/verax/core/security/supplychain.defaults.json +38 -0
  146. package/src/verax/core/security/supplychain.policy.js +334 -0
  147. package/src/verax/core/security/vuln.scan.js +265 -0
  148. package/src/verax/core/truth/truth.certificate.js +252 -0
  149. package/src/verax/core/ui-feedback-intelligence.js +481 -0
  150. package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
  151. package/src/verax/detect/confidence-engine.js +62 -34
  152. package/src/verax/detect/confidence-helper.js +34 -0
  153. package/src/verax/detect/dynamic-route-findings.js +338 -0
  154. package/src/verax/detect/expectation-chain-detector.js +417 -0
  155. package/src/verax/detect/expectation-model.js +2 -2
  156. package/src/verax/detect/failure-cause-inference.js +293 -0
  157. package/src/verax/detect/findings-writer.js +131 -35
  158. package/src/verax/detect/flow-detector.js +2 -2
  159. package/src/verax/detect/form-silent-failure.js +98 -0
  160. package/src/verax/detect/index.js +46 -5
  161. package/src/verax/detect/invariants-enforcer.js +147 -0
  162. package/src/verax/detect/journey-stall-detector.js +558 -0
  163. package/src/verax/detect/navigation-silent-failure.js +82 -0
  164. package/src/verax/detect/problem-aggregator.js +361 -0
  165. package/src/verax/detect/route-findings.js +219 -0
  166. package/src/verax/detect/summary-writer.js +477 -0
  167. package/src/verax/detect/test-failure-cause-inference.js +314 -0
  168. package/src/verax/detect/ui-feedback-findings.js +207 -0
  169. package/src/verax/detect/view-switch-correlator.js +242 -0
  170. package/src/verax/flow/flow-engine.js +2 -1
  171. package/src/verax/flow/flow-spec.js +0 -6
  172. package/src/verax/index.js +4 -0
  173. package/src/verax/intel/ts-program.js +1 -0
  174. package/src/verax/intel/vue-navigation-extractor.js +3 -0
  175. package/src/verax/learn/action-contract-extractor.js +3 -0
  176. package/src/verax/learn/ast-contract-extractor.js +1 -1
  177. package/src/verax/learn/flow-extractor.js +1 -0
  178. package/src/verax/learn/project-detector.js +5 -0
  179. package/src/verax/learn/react-router-extractor.js +2 -0
  180. package/src/verax/learn/source-instrumenter.js +1 -0
  181. package/src/verax/learn/state-extractor.js +2 -1
  182. package/src/verax/learn/static-extractor.js +1 -0
  183. package/src/verax/observe/coverage-gaps.js +132 -0
  184. package/src/verax/observe/expectation-handler.js +126 -0
  185. package/src/verax/observe/incremental-skip.js +46 -0
  186. package/src/verax/observe/index.js +51 -155
  187. package/src/verax/observe/interaction-executor.js +192 -0
  188. package/src/verax/observe/interaction-runner.js +782 -513
  189. package/src/verax/observe/network-firewall.js +86 -0
  190. package/src/verax/observe/observation-builder.js +169 -0
  191. package/src/verax/observe/observe-context.js +205 -0
  192. package/src/verax/observe/observe-helpers.js +192 -0
  193. package/src/verax/observe/observe-runner.js +230 -0
  194. package/src/verax/observe/observers/budget-observer.js +185 -0
  195. package/src/verax/observe/observers/console-observer.js +102 -0
  196. package/src/verax/observe/observers/coverage-observer.js +107 -0
  197. package/src/verax/observe/observers/interaction-observer.js +471 -0
  198. package/src/verax/observe/observers/navigation-observer.js +132 -0
  199. package/src/verax/observe/observers/network-observer.js +87 -0
  200. package/src/verax/observe/observers/safety-observer.js +82 -0
  201. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  202. package/src/verax/observe/page-traversal.js +138 -0
  203. package/src/verax/observe/snapshot-ops.js +94 -0
  204. package/src/verax/observe/ui-feedback-detector.js +742 -0
  205. package/src/verax/scan-summary-writer.js +2 -0
  206. package/src/verax/shared/artifact-manager.js +25 -5
  207. package/src/verax/shared/caching.js +1 -0
  208. package/src/verax/shared/css-spinner-rules.js +204 -0
  209. package/src/verax/shared/expectation-tracker.js +1 -0
  210. package/src/verax/shared/view-switch-rules.js +208 -0
  211. package/src/verax/shared/zip-artifacts.js +6 -0
  212. package/src/verax/shared/config-loader.js +0 -169
  213. /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
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * PHASE 21.3 — Observe Context Contract
3
+ *
4
+ * HARD CONTRACT: Defines the interface between observe-runner and observers
5
+ *
6
+ * RULES:
7
+ * - Observers MUST only access fields defined in this contract
8
+ * - Observers MUST NOT import from outside observe/*
9
+ * - Observers MUST NOT read files
10
+ * - Observers MUST NOT write artifacts directly
11
+ * - Observers MUST NOT mutate global state
12
+ * - Observers MUST propagate all errors (no silent catches)
13
+ *
14
+ * Runtime invariant checks enforce these rules.
15
+ */
16
+
17
+ /**
18
+ * ObserveContext — The context passed to all observers
19
+ *
20
+ * @typedef {Object} ObserveContext
21
+ * @property {import('playwright').Page} page - Playwright page instance
22
+ * @property {string} baseOrigin - Base origin for same-origin checks
23
+ * @property {Object} scanBudget - Scan budget configuration
24
+ * @property {number} startTime - Start time of the scan (timestamp)
25
+ * @property {Object} frontier - PageFrontier instance
26
+ * @property {Object|null} manifest - Manifest object (if available)
27
+ * @property {Object|null} expectationResults - Expectation execution results
28
+ * @property {boolean} incrementalMode - Whether incremental mode is enabled
29
+ * @property {Object|null} oldSnapshot - Previous snapshot (if available)
30
+ * @property {Object|null} snapshotDiff - Snapshot diff (if available)
31
+ * @property {string} currentUrl - Current page URL
32
+ * @property {string} screenshotsDir - Directory for screenshots
33
+ * @property {number} timestamp - Timestamp for this observation
34
+ * @property {Object} decisionRecorder - DecisionRecorder instance
35
+ * @property {Object} silenceTracker - SilenceTracker instance
36
+ * @property {Object} safetyFlags - Safety flags { allowWrites, allowRiskyActions, allowCrossOrigin }
37
+ * @property {Object} routeBudget - Route-specific budget (computed)
38
+ */
39
+
40
+ /**
41
+ * RunState — Mutable state passed between observers
42
+ *
43
+ * @typedef {Object} RunState
44
+ * @property {Array} traces - Array of interaction traces
45
+ * @property {Array} skippedInteractions - Array of skipped interactions
46
+ * @property {Array} observedExpectations - Array of observed expectations
47
+ * @property {number} totalInteractionsDiscovered - Total interactions discovered
48
+ * @property {number} totalInteractionsExecuted - Total interactions executed
49
+ * @property {Array} remainingInteractionsGaps - Remaining interaction gaps
50
+ * @property {boolean} navigatedToNewPage - Whether navigation occurred
51
+ * @property {string|null} navigatedPageUrl - URL of navigated page (if any)
52
+ */
53
+
54
+ /**
55
+ * Observation — Result returned by an observer
56
+ *
57
+ * @typedef {Object} Observation
58
+ * @property {string} type - Type of observation (e.g., 'network_idle', 'console_error', 'ui_feedback')
59
+ * @property {string} scope - Scope of observation (e.g., 'page', 'interaction', 'navigation')
60
+ * @property {Object} data - Observation data
61
+ * @property {number} timestamp - Timestamp of observation
62
+ * @property {string} [url] - URL where observation occurred
63
+ */
64
+
65
+ /**
66
+ * Forbidden imports that observers MUST NOT use
67
+ */
68
+ const _FORBIDDEN_IMPORTS = [
69
+ 'fs',
70
+ 'path',
71
+ '../core/determinism/report-writer',
72
+ '../core/scan-summary-writer',
73
+ './traces-writer',
74
+ './expectation-executor'
75
+ ];
76
+
77
+ /**
78
+ * Forbidden context fields that observers MUST NOT access
79
+ */
80
+ const FORBIDDEN_CONTEXT_FIELDS = [
81
+ 'projectDir',
82
+ 'runId',
83
+ 'writeFileSync',
84
+ 'readFileSync',
85
+ 'mkdirSync'
86
+ ];
87
+
88
+ /**
89
+ * Validate that an observer result is a valid Observation
90
+ *
91
+ * @param {Object} observation - Observation to validate
92
+ * @param {string} observerName - Name of observer for error messages
93
+ * @throws {Error} If observation is invalid
94
+ */
95
+ export function validateObservation(observation, observerName) {
96
+ if (!observation) {
97
+ throw new Error(`${observerName}: Observer returned null/undefined observation`);
98
+ }
99
+
100
+ if (typeof observation !== 'object') {
101
+ throw new Error(`${observerName}: Observer returned non-object observation: ${typeof observation}`);
102
+ }
103
+
104
+ if (!observation.type || typeof observation.type !== 'string') {
105
+ throw new Error(`${observerName}: Observation missing or invalid 'type' field`);
106
+ }
107
+
108
+ if (!observation.scope || typeof observation.scope !== 'string') {
109
+ throw new Error(`${observerName}: Observation missing or invalid 'scope' field`);
110
+ }
111
+
112
+ if (!observation.data || typeof observation.data !== 'object') {
113
+ throw new Error(`${observerName}: Observation missing or invalid 'data' field`);
114
+ }
115
+
116
+ if (typeof observation.timestamp !== 'number') {
117
+ throw new Error(`${observerName}: Observation missing or invalid 'timestamp' field`);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Validate that context contains only allowed fields
123
+ *
124
+ * @param {Object} context - Context to validate
125
+ * @throws {Error} If context contains forbidden fields
126
+ */
127
+ export function validateContext(context) {
128
+ for (const field of FORBIDDEN_CONTEXT_FIELDS) {
129
+ if (field in context) {
130
+ throw new Error(`ObserveContext contains forbidden field: ${field}. Observers must not access file I/O or project directories.`);
131
+ }
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Create a safe context for observers (removes forbidden fields)
137
+ *
138
+ * @param {Object} rawContext - Raw context from observe-runner
139
+ * @returns {ObserveContext} Safe context for observers
140
+ */
141
+ export function createObserveContext(rawContext) {
142
+ const {
143
+ page,
144
+ baseOrigin,
145
+ scanBudget,
146
+ startTime,
147
+ frontier,
148
+ manifest,
149
+ expectationResults,
150
+ incrementalMode,
151
+ oldSnapshot,
152
+ snapshotDiff,
153
+ currentUrl,
154
+ screenshotsDir,
155
+ timestamp,
156
+ decisionRecorder,
157
+ silenceTracker,
158
+ safetyFlags,
159
+ routeBudget
160
+ } = rawContext;
161
+
162
+ return {
163
+ page,
164
+ baseOrigin,
165
+ scanBudget,
166
+ startTime,
167
+ frontier,
168
+ manifest,
169
+ expectationResults,
170
+ incrementalMode,
171
+ oldSnapshot,
172
+ snapshotDiff,
173
+ currentUrl,
174
+ screenshotsDir,
175
+ timestamp,
176
+ decisionRecorder,
177
+ silenceTracker,
178
+ safetyFlags: safetyFlags || { allowWrites: false, allowRiskyActions: false, allowCrossOrigin: false },
179
+ routeBudget
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Observer execution order (FIXED - must not change)
185
+ *
186
+ * This order is critical for determinism and correctness.
187
+ */
188
+ export const OBSERVER_ORDER = [
189
+ 'navigation-observer', // 1. Navigation decisions first
190
+ 'budget-observer', // 2. Budget checks before interactions
191
+ 'interaction-observer', // 3. Interaction discovery and execution
192
+ 'network-observer', // 4. Network state observation
193
+ 'ui-feedback-observer', // 5. UI state observation
194
+ 'console-observer', // 6. Console error observation
195
+ 'coverage-observer' // 7. Coverage gap tracking
196
+ ];
197
+
198
+ /**
199
+ * Get observer execution order
200
+ *
201
+ * @returns {Array<string>} Ordered list of observer names
202
+ */
203
+ export function getObserverOrder() {
204
+ return [...OBSERVER_ORDER];
205
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * PHASE 21.3 — Observe Helpers
3
+ *
4
+ * Helper functions extracted from observe/index.js to keep it slim
5
+ */
6
+
7
+ import { resolve } from 'path';
8
+ import { mkdirSync, writeFileSync } from 'fs';
9
+ import { writeTraces } from './traces-writer.js';
10
+
11
+ /**
12
+ * Setup manifest and expectations
13
+ */
14
+ export async function setupManifestAndExpectations(manifestPath, projectDir, page, url, screenshotsDir, scanBudget, startTime, silenceTracker) {
15
+ const { readFileSync, existsSync } = await import('fs');
16
+ const { loadPreviousSnapshot, saveSnapshot: _saveSnapshot, buildSnapshot, compareSnapshots } = await import('../core/incremental-store.js');
17
+ const { executeProvenExpectations } = await import('./expectation-executor.js');
18
+ const { isProvenExpectation } = await import('../shared/expectation-prover.js');
19
+
20
+ let manifest = null;
21
+ let expectationResults = null;
22
+ let expectationCoverageGaps = [];
23
+ let incrementalMode = false;
24
+ let snapshotDiff = null;
25
+ let oldSnapshot = null;
26
+
27
+ if (manifestPath && existsSync(manifestPath)) {
28
+ try {
29
+ const manifestContent = readFileSync(manifestPath, 'utf-8');
30
+ // @ts-expect-error - readFileSync with encoding returns string
31
+ manifest = JSON.parse(manifestContent);
32
+
33
+ oldSnapshot = loadPreviousSnapshot(projectDir);
34
+ if (oldSnapshot) {
35
+ const currentSnapshot = buildSnapshot(manifest, []);
36
+ snapshotDiff = compareSnapshots(oldSnapshot, currentSnapshot);
37
+ incrementalMode = !snapshotDiff.hasChanges;
38
+ }
39
+
40
+ const provenCount = (manifest.staticExpectations || []).filter(exp => isProvenExpectation(exp)).length;
41
+ if (provenCount > 0) {
42
+ expectationResults = await executeProvenExpectations(page, manifest, url, screenshotsDir, scanBudget, startTime, projectDir);
43
+ expectationCoverageGaps = expectationResults.coverageGaps || [];
44
+ }
45
+ } catch (err) {
46
+ silenceTracker.record({
47
+ scope: 'discovery',
48
+ reason: 'discovery_error',
49
+ description: 'Manifest load or expectation execution failed',
50
+ context: { error: err?.message },
51
+ impact: 'incomplete_check'
52
+ });
53
+ }
54
+ }
55
+
56
+ return { manifest, expectationResults, expectationCoverageGaps, incrementalMode, snapshotDiff, oldSnapshot };
57
+ }
58
+
59
+ /**
60
+ * Process traversal results and build observation
61
+ */
62
+ export async function processTraversalResults(traversalResult, expectationResults, expectationCoverageGaps, remainingInteractionsGaps, frontier, scanBudget, page, url, finalTraces, finalSkippedInteractions, finalObservedExpectations, silenceTracker, manifest, incrementalMode, snapshotDiff, projectDir, runId) {
63
+ // Combine all coverage gaps
64
+ const allCoverageGaps = [...expectationCoverageGaps];
65
+ if (remainingInteractionsGaps.length > 0) {
66
+ allCoverageGaps.push(...remainingInteractionsGaps.map(gap => ({
67
+ expectationId: null,
68
+ type: gap.interaction.type,
69
+ reason: gap.reason,
70
+ fromPath: gap.url,
71
+ source: null,
72
+ evidence: { interaction: gap.interaction }
73
+ })));
74
+ }
75
+
76
+ if (frontier.frontierCapped) {
77
+ allCoverageGaps.push({
78
+ expectationId: null,
79
+ type: 'navigation',
80
+ reason: 'frontier_capped',
81
+ fromPath: page.url(),
82
+ source: null,
83
+ evidence: { message: `Frontier capped at ${scanBudget.maxUniqueUrls || 'unlimited'} unique URLs` }
84
+ });
85
+ }
86
+
87
+ // Build coverage object
88
+ const coverage = {
89
+ candidatesDiscovered: traversalResult.totalInteractionsDiscovered,
90
+ candidatesSelected: traversalResult.totalInteractionsExecuted,
91
+ cap: scanBudget.maxTotalInteractions,
92
+ capped: traversalResult.totalInteractionsExecuted >= scanBudget.maxTotalInteractions || remainingInteractionsGaps.length > 0,
93
+ pagesVisited: frontier.pagesVisited,
94
+ pagesDiscovered: frontier.pagesDiscovered,
95
+ skippedInteractions: finalSkippedInteractions.length,
96
+ interactionsDiscovered: traversalResult.totalInteractionsDiscovered,
97
+ interactionsExecuted: traversalResult.totalInteractionsExecuted
98
+ };
99
+
100
+ // Build warnings
101
+ const observeWarnings = [];
102
+ if (coverage.capped) {
103
+ observeWarnings.push({
104
+ code: 'INTERACTIONS_CAPPED',
105
+ message: `Interaction execution capped. Visited ${coverage.pagesVisited} pages, discovered ${coverage.pagesDiscovered}, executed ${coverage.candidatesSelected} of ${coverage.candidatesDiscovered} interactions. Coverage incomplete.`
106
+ });
107
+ }
108
+ if (finalSkippedInteractions.length > 0) {
109
+ observeWarnings.push({
110
+ code: 'INTERACTIONS_SKIPPED',
111
+ message: `Skipped ${finalSkippedInteractions.length} dangerous interactions`,
112
+ details: finalSkippedInteractions
113
+ });
114
+ }
115
+
116
+ // Append expectation traces
117
+ if (expectationResults?.results) {
118
+ for (const result of expectationResults.results) {
119
+ if (result.trace) {
120
+ result.trace.expectationDriven = true;
121
+ result.trace.expectationId = result.expectationId;
122
+ result.trace.expectationOutcome = result.outcome;
123
+ finalTraces.push(result.trace);
124
+ }
125
+ }
126
+ }
127
+
128
+ // Write traces
129
+ const observation = writeTraces(projectDir, url, finalTraces, coverage, observeWarnings, finalObservedExpectations, silenceTracker, runId);
130
+ observation.silences = silenceTracker.getDetailedSummary();
131
+
132
+ // Add expectation execution results
133
+ if (expectationResults) {
134
+ observation.expectationExecution = {
135
+ totalProvenExpectations: expectationResults.totalProvenExpectations,
136
+ executedCount: expectationResults.executedCount,
137
+ coverageGapsCount: allCoverageGaps.length,
138
+ results: expectationResults.results.map(r => ({
139
+ expectationId: r.expectationId,
140
+ type: r.type,
141
+ fromPath: r.fromPath,
142
+ outcome: r.outcome,
143
+ reason: r.reason
144
+ }))
145
+ };
146
+ observation.expectationCoverageGaps = allCoverageGaps;
147
+ }
148
+
149
+ // Add incremental mode metadata
150
+ if (manifest) {
151
+ const { buildSnapshot, saveSnapshot } = await import('../core/incremental-store.js');
152
+ const observedInteractions = finalTraces
153
+ .filter(t => t.interaction && !t.incremental)
154
+ .map(t => ({
155
+ type: t.interaction?.type,
156
+ selector: t.interaction?.selector,
157
+ url: t.before?.url || url
158
+ }));
159
+
160
+ const currentSnapshot = buildSnapshot(manifest, observedInteractions);
161
+ saveSnapshot(projectDir, currentSnapshot, runId);
162
+
163
+ observation.incremental = {
164
+ enabled: incrementalMode,
165
+ snapshotDiff: snapshotDiff,
166
+ skippedInteractionsCount: finalSkippedInteractions.filter(s => s.reason === 'incremental_unchanged').length
167
+ };
168
+ }
169
+
170
+ return observation;
171
+ }
172
+
173
+ /**
174
+ * Write determinism artifacts
175
+ */
176
+ export async function writeDeterminismArtifacts(projectDir, runId, decisionRecorder) {
177
+ // PHASE 25: Write determinism contract
178
+ const { writeDeterminismContract } = await import('../core/determinism/contract-writer.js');
179
+ const { getRunArtifactDir } = await import('../core/run-id.js');
180
+ const runDir = getRunArtifactDir(projectDir, runId);
181
+ writeDeterminismContract(runDir, decisionRecorder);
182
+ if (!runId || !projectDir) return;
183
+
184
+ const runsDir = resolve(projectDir, '.verax', 'runs', runId);
185
+ mkdirSync(runsDir, { recursive: true });
186
+ const decisionsPath = resolve(runsDir, 'decisions.json');
187
+ writeFileSync(decisionsPath, JSON.stringify(decisionRecorder.export(), null, 2), 'utf-8');
188
+
189
+ const { writeDeterminismReport } = await import('../core/determinism/report-writer.js');
190
+ writeDeterminismReport(runsDir, decisionRecorder);
191
+ }
192
+