@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,226 @@
1
+ /**
2
+ * DOM Diff Engine
3
+ * Computes structured differences between HTML snapshots
4
+ * Distinguishes meaningful changes from noise
5
+ */
6
+
7
+ /**
8
+ * Compute a diff between two HTML documents
9
+ * Returns a summary of changes without full tree comparison (for performance)
10
+ * Includes isMeaningful flag to distinguish signal from noise
11
+ */
12
+ export function computeDOMDiff(htmlBefore, htmlAfter) {
13
+ const _before = parseHTML(htmlBefore);
14
+ const _after = parseHTML(htmlAfter);
15
+
16
+ const diff = {
17
+ htmlLengthBefore: htmlBefore.length,
18
+ htmlLengthAfter: htmlAfter.length,
19
+ changed: htmlBefore !== htmlAfter,
20
+ isMeaningful: false,
21
+ elementsRemoved: [],
22
+ elementsAdded: [],
23
+ attributesChanged: [],
24
+ contentChanged: [],
25
+ };
26
+
27
+ if (!diff.changed) {
28
+ return diff;
29
+ }
30
+
31
+ // Check if this is only noise (timestamps, random ids, tracking)
32
+ if (isNoisyChangeOnly(htmlBefore, htmlAfter)) {
33
+ diff.changed = true;
34
+ diff.isMeaningful = false;
35
+ return diff;
36
+ }
37
+
38
+ // Quick heuristics for specific changes
39
+ // Check for new elements with specific roles/classes that indicate feedback
40
+ const feedbackPatterns = [
41
+ 'role="alert"',
42
+ 'role="status"',
43
+ 'aria-live',
44
+ 'class="toast"',
45
+ 'class="error"',
46
+ 'class="success"',
47
+ 'class="modal"',
48
+ 'class="dialog"',
49
+ '[data-error]',
50
+ '[data-success]',
51
+ ];
52
+
53
+ for (const pattern of feedbackPatterns) {
54
+ if (!htmlBefore.includes(pattern) && htmlAfter.includes(pattern)) {
55
+ diff.elementsAdded.push(pattern);
56
+ diff.isMeaningful = true;
57
+ }
58
+ if (htmlBefore.includes(pattern) && !htmlAfter.includes(pattern)) {
59
+ diff.elementsRemoved.push(pattern);
60
+ diff.isMeaningful = true;
61
+ }
62
+ }
63
+
64
+ // Check for attribute changes (disabled, aria-invalid, etc.)
65
+ const attrPatterns = [
66
+ 'disabled',
67
+ 'aria-invalid',
68
+ 'aria-disabled',
69
+ 'data-loading',
70
+ ];
71
+
72
+ for (const attr of attrPatterns) {
73
+ const beforeCount = countOccurrences(htmlBefore, attr);
74
+ const afterCount = countOccurrences(htmlAfter, attr);
75
+
76
+ if (beforeCount !== afterCount) {
77
+ diff.attributesChanged.push({
78
+ attribute: attr,
79
+ before: beforeCount,
80
+ after: afterCount,
81
+ });
82
+ diff.isMeaningful = true;
83
+ }
84
+ }
85
+
86
+ // Check for form state changes (values, structure)
87
+ if (checkFormStateChange(htmlBefore, htmlAfter)) {
88
+ diff.isMeaningful = true;
89
+ }
90
+
91
+ return diff;
92
+ }
93
+
94
+ /**
95
+ * Parse HTML and return basic structure info
96
+ */
97
+ function parseHTML(html) {
98
+ return {
99
+ bodyLength: html.length,
100
+ hasHead: html.includes('<head'),
101
+ hasBody: html.includes('<body'),
102
+ formCount: countOccurrences(html, '<form'),
103
+ inputCount: countOccurrences(html, '<input'),
104
+ buttonCount: countOccurrences(html, '<button'),
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Count occurrences of a substring
110
+ */
111
+ function countOccurrences(str, substr) {
112
+ let count = 0;
113
+ let pos = 0;
114
+ while ((pos = str.indexOf(substr, pos)) !== -1) {
115
+ count++;
116
+ pos += substr.length;
117
+ }
118
+ return count;
119
+ }
120
+
121
+ /**
122
+ * Detect if DOM appears to have feedback elements
123
+ */
124
+ export function hasFeedbackElements(html) {
125
+ const feedbackIndicators = [
126
+ 'role="alert"',
127
+ 'role="status"',
128
+ 'aria-live="polite"',
129
+ 'aria-live="assertive"',
130
+ 'toast',
131
+ 'error',
132
+ 'success',
133
+ 'validation',
134
+ ];
135
+
136
+ return feedbackIndicators.some(indicator =>
137
+ html.includes(indicator)
138
+ );
139
+ }
140
+
141
+ /**
142
+ * Detect if DOM appears to have validation errors
143
+ */
144
+ export function hasValidationErrors(html) {
145
+ const errorPatterns = [
146
+ 'aria-invalid="true"',
147
+ 'aria-invalid=\'true\'',
148
+ 'invalid',
149
+ 'error',
150
+ 'required',
151
+ ];
152
+
153
+ return errorPatterns.some(pattern => html.includes(pattern));
154
+ }
155
+
156
+ /**
157
+ * Check if the HTML change is only noise (timestamps, random IDs, tracking pixels)
158
+ * Returns true if ONLY noise detected, false if meaningful changes exist
159
+ */
160
+ function isNoisyChangeOnly(htmlBefore, htmlAfter) {
161
+ // Make a copy and remove known noise patterns
162
+ let before = htmlBefore;
163
+ let after = htmlAfter;
164
+
165
+ // Remove timestamps (ISO, Unix, etc.)
166
+ const timestampPattern = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^"']*/g;
167
+ before = before.replace(timestampPattern, '[TIMESTAMP]');
168
+ after = after.replace(timestampPattern, '[TIMESTAMP]');
169
+
170
+ // Remove UUID-like patterns
171
+ const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
172
+ before = before.replace(uuidPattern, '[UUID]');
173
+ after = after.replace(uuidPattern, '[UUID]');
174
+
175
+ // Remove random hash-like values
176
+ const hashPattern = /[a-f0-9]{32,}/g;
177
+ before = before.replace(hashPattern, '[HASH]');
178
+ after = after.replace(hashPattern, '[HASH]');
179
+
180
+ // Remove tracking params (ga, fbclid, etc.)
181
+ const trackingPattern = /[?&](ga[a-z_]*|fbclid|utm_[a-z]*|gclid|msclkid)=[^&"']*/g;
182
+ before = before.replace(trackingPattern, '[TRACKING]');
183
+ after = after.replace(trackingPattern, '[TRACKING]');
184
+
185
+ // Remove data-testid and similar noise attrs
186
+ const testIdPattern = /data-testid="[^"]*"/g;
187
+ before = before.replace(testIdPattern, '');
188
+ after = after.replace(testIdPattern, '');
189
+
190
+ // If they're now equal, it was only noise
191
+ return before === after;
192
+ }
193
+
194
+ /**
195
+ * Check for meaningful form state changes
196
+ */
197
+ function checkFormStateChange(htmlBefore, htmlAfter) {
198
+ // Check for form input value changes (meaningful state change)
199
+ const beforeInputs = extractInputValues(htmlBefore);
200
+ const afterInputs = extractInputValues(htmlAfter);
201
+
202
+ if (beforeInputs.size !== afterInputs.size) {
203
+ return true;
204
+ }
205
+
206
+ for (const [key, value] of beforeInputs) {
207
+ if (afterInputs.get(key) !== value) {
208
+ return true;
209
+ }
210
+ }
211
+
212
+ return false;
213
+ }
214
+
215
+ /**
216
+ * Extract input name-value pairs from HTML
217
+ */
218
+ function extractInputValues(html) {
219
+ const values = new Map();
220
+ const inputPattern = /<input[^>]*name="([^"]*)"[^>]*value="([^"]*)"/g;
221
+ let match;
222
+ while ((match = inputPattern.exec(html)) !== null) {
223
+ values.set(match[1], match[2]);
224
+ }
225
+ return values;
226
+ }
@@ -1,11 +1,7 @@
1
- import { assertExecutionBootstrapAllowed } from './bootstrap-guard.js';
2
-
3
1
  /**
4
2
  * Attempt to infer URL from environment variables and common dev configs
5
3
  */
6
4
  export function tryResolveUrlFromEnv() {
7
- // PHASE 21.6.1: Runtime guard - crash if called during inspection
8
- assertExecutionBootstrapAllowed('tryResolveUrlFromEnv');
9
5
  // Check common environment variables
10
6
  const candidates = [
11
7
  process.env.VERCEL_URL,
@@ -0,0 +1,287 @@
1
+ import { writeFileSync, mkdirSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import { computeDOMDiff, hasFeedbackElements as _hasFeedbackElements, hasValidationErrors as _hasValidationErrors } from './dom-diff.js';
4
+
5
+ /**
6
+ * Evidence Engine
7
+ * Captures and bundles evidence for each interaction attempt
8
+ */
9
+
10
+ export class EvidenceBundle {
11
+ constructor(promiseId, expNum, evidencePath) {
12
+ this.promiseId = promiseId;
13
+ this.expNum = expNum;
14
+ this.evidencePath = evidencePath;
15
+ this.beforeScreenshot = null;
16
+ this.afterScreenshot = null;
17
+ this.beforeHTML = null;
18
+ this.afterHTML = null;
19
+ this.domDiff = null;
20
+ this.networkEvents = [];
21
+ this.networkEventsByTime = new Map(); // For correlation
22
+ this.consoleErrors = [];
23
+ this.actionStartTime = null; // When action was executed
24
+ this.timing = {
25
+ startedAt: null,
26
+ endedAt: null,
27
+ };
28
+ this.signals = {
29
+ navigationChanged: false,
30
+ domChanged: false,
31
+ feedbackSeen: false,
32
+ networkActivity: false,
33
+ consoleErrors: false,
34
+ meaningfulDomChange: false, // NEW: tracks only meaningful changes
35
+ correlatedNetworkActivity: false, // NEW: tracks network within action window
36
+ };
37
+ this.files = [];
38
+ this.correlatedRequests = []; // Network requests correlated to this action
39
+ }
40
+
41
+ /**
42
+ * Capture before state
43
+ */
44
+ async captureBeforeState(page, suffix = '') {
45
+ try {
46
+ this.beforeScreenshot = `exp_${this.expNum}_before${suffix}.png`;
47
+ const screenshotPath = resolve(this.evidencePath, this.beforeScreenshot);
48
+ await page.screenshot({ path: screenshotPath });
49
+ this.files.push(this.beforeScreenshot);
50
+ } catch (e) {
51
+ this.beforeScreenshot = null;
52
+ }
53
+
54
+ try {
55
+ this.beforeHTML = await page.content();
56
+ } catch (e) {
57
+ this.beforeHTML = null;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Capture after state
63
+ */
64
+ async captureAfterState(page, suffix = '') {
65
+ try {
66
+ this.afterScreenshot = `exp_${this.expNum}_after${suffix}.png`;
67
+ const screenshotPath = resolve(this.evidencePath, this.afterScreenshot);
68
+ await page.screenshot({ path: screenshotPath });
69
+ this.files.push(this.afterScreenshot);
70
+ } catch (e) {
71
+ this.afterScreenshot = null;
72
+ }
73
+
74
+ try {
75
+ this.afterHTML = await page.content();
76
+ } catch (e) {
77
+ this.afterHTML = null;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Analyze changes between before and after
83
+ */
84
+ analyzeChanges(urlBefore, urlAfter) {
85
+ // Navigation change
86
+ if (urlBefore && urlAfter && urlBefore !== urlAfter) {
87
+ this.signals.navigationChanged = true;
88
+ }
89
+
90
+ // DOM change
91
+ if (this.beforeHTML && this.afterHTML) {
92
+ this.domDiff = computeDOMDiff(this.beforeHTML, this.afterHTML);
93
+ if (this.domDiff.changed) {
94
+ this.signals.domChanged = true;
95
+ }
96
+
97
+ // Track meaningful changes separately
98
+ if (this.domDiff.isMeaningful) {
99
+ this.signals.meaningfulDomChange = true;
100
+ }
101
+
102
+ // Check for feedback elements visibility change
103
+ if (detectNewFeedback(this.beforeHTML, this.afterHTML)) {
104
+ this.signals.feedbackSeen = true;
105
+ }
106
+
107
+ // Check for validation error appearance
108
+ if (detectNewValidationError(this.beforeHTML, this.afterHTML)) {
109
+ this.signals.feedbackSeen = true;
110
+ }
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Record network events that occurred during this action
116
+ */
117
+ recordNetworkEvent(networkLog) {
118
+ this.networkEvents.push(networkLog);
119
+
120
+ // Track event time for correlation
121
+ if (networkLog.startTime) {
122
+ this.networkEventsByTime.set(networkLog.startTime, networkLog);
123
+ }
124
+
125
+ if (this.networkEvents.length > 0) {
126
+ this.signals.networkActivity = true;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Correlate network events to this action
132
+ * Captures requests that started within correlation window after action
133
+ * Window: 0-2500ms after action starts
134
+ */
135
+ correlateNetworkRequests() {
136
+ if (!this.actionStartTime || this.networkEvents.length === 0) {
137
+ return;
138
+ }
139
+
140
+ const actionTime = new Date(this.actionStartTime).getTime();
141
+ const correlationWindow = 2500; // milliseconds
142
+
143
+ for (const event of this.networkEvents) {
144
+ if (event.startTime) {
145
+ const eventTime = new Date(event.startTime).getTime();
146
+ if (eventTime >= actionTime && eventTime <= actionTime + correlationWindow) {
147
+ this.correlatedRequests.push(event);
148
+ this.signals.correlatedNetworkActivity = true;
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Record console errors
156
+ */
157
+ recordConsoleError(error) {
158
+ this.consoleErrors.push(error);
159
+ if (this.consoleErrors.length > 0) {
160
+ this.signals.consoleErrors = true;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Finalize evidence bundle
166
+ */
167
+ finalize(_page) {
168
+ this.timing.endedAt = new Date().toISOString();
169
+
170
+ // Persist evidence files
171
+ try {
172
+ mkdirSync(this.evidencePath, { recursive: true });
173
+
174
+ // Save DOM diff if available
175
+ if (this.domDiff) {
176
+ const diffFile = `exp_${this.expNum}_dom_diff.json`;
177
+ writeFileSync(
178
+ resolve(this.evidencePath, diffFile),
179
+ JSON.stringify(this.domDiff, null, 2),
180
+ 'utf-8'
181
+ );
182
+ this.files.push(diffFile);
183
+ }
184
+
185
+ // Save network events if any
186
+ if (this.networkEvents.length > 0) {
187
+ const netFile = `exp_${this.expNum}_network.json`;
188
+ writeFileSync(
189
+ resolve(this.evidencePath, netFile),
190
+ JSON.stringify(this.networkEvents, null, 2),
191
+ 'utf-8'
192
+ );
193
+ this.files.push(netFile);
194
+ }
195
+
196
+ // Save console errors if any
197
+ if (this.consoleErrors.length > 0) {
198
+ const errFile = `exp_${this.expNum}_console_errors.json`;
199
+ writeFileSync(
200
+ resolve(this.evidencePath, errFile),
201
+ JSON.stringify(this.consoleErrors, null, 2),
202
+ 'utf-8'
203
+ );
204
+ this.files.push(errFile);
205
+ }
206
+ } catch (e) {
207
+ // Best effort
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Check if we have meaningful evidence
213
+ */
214
+ hasEvidence() {
215
+ return (
216
+ this.beforeScreenshot ||
217
+ this.afterScreenshot ||
218
+ this.domDiff?.changed ||
219
+ this.networkEvents.length > 0 ||
220
+ this.consoleErrors.length > 0 ||
221
+ Object.values(this.signals).some(v => v === true)
222
+ );
223
+ }
224
+
225
+ /**
226
+ * Summarize signals for observation
227
+ */
228
+ getSummary() {
229
+ return {
230
+ files: this.files,
231
+ timing: this.timing,
232
+ signals: this.signals,
233
+ hasEvidence: this.hasEvidence(),
234
+ };
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Create a bundle for a promise
240
+ */
241
+ export function createEvidenceBundle(promise, expNum, evidencePath) {
242
+ return new EvidenceBundle(promise.id, expNum, evidencePath);
243
+ }
244
+
245
+ /**
246
+ * Detect if new feedback elements appeared
247
+ */
248
+ function detectNewFeedback(htmlBefore, htmlAfter) {
249
+ const feedbackPatterns = [
250
+ 'role="alert"',
251
+ 'role="status"',
252
+ 'aria-live="polite"',
253
+ 'aria-live="assertive"',
254
+ 'class="toast"',
255
+ 'class="modal"',
256
+ 'class="dialog"',
257
+ ];
258
+
259
+ for (const pattern of feedbackPatterns) {
260
+ if (!htmlBefore.includes(pattern) && htmlAfter.includes(pattern)) {
261
+ return true;
262
+ }
263
+ }
264
+
265
+ return false;
266
+ }
267
+
268
+ /**
269
+ * Detect if new validation errors appeared
270
+ */
271
+ function detectNewValidationError(htmlBefore, htmlAfter) {
272
+ const errorPatterns = [
273
+ 'aria-invalid="true"',
274
+ 'aria-invalid=\'true\'',
275
+ 'class="error"',
276
+ 'class="invalid"',
277
+ '[data-error]',
278
+ ];
279
+
280
+ for (const pattern of errorPatterns) {
281
+ if (!htmlBefore.includes(pattern) && htmlAfter.includes(pattern)) {
282
+ return true;
283
+ }
284
+ }
285
+
286
+ return false;
287
+ }