@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,359 @@
1
+ /**
2
+ * Digest Engine
3
+ * Produces cryptographic hashes of run artifacts for determinism proof
4
+ * Ensures two identical runs produce identical digests
5
+ */
6
+
7
+ import { createHash } from 'crypto';
8
+ import { readFileSync, writeFileSync } from 'fs';
9
+ import { resolve } from 'path';
10
+
11
+ /**
12
+ * Normalize JSON for hashing (canonicalize)
13
+ * Removes volatile fields, sorts keys consistently
14
+ */
15
+ function normalizeJSON(obj, volatileFields = []) {
16
+ if (typeof obj !== 'object' || obj === null) {
17
+ return obj;
18
+ }
19
+
20
+ if (Array.isArray(obj)) {
21
+ return obj.map(item => normalizeJSON(item, volatileFields));
22
+ }
23
+
24
+ const normalized = {};
25
+ const keys = Object.keys(obj).sort();
26
+
27
+ for (const key of keys) {
28
+ // Skip volatile fields
29
+ if (volatileFields.some(field => key.includes(field))) {
30
+ continue;
31
+ }
32
+
33
+ const value = obj[key];
34
+ if (typeof value === 'object' && value !== null) {
35
+ normalized[key] = normalizeJSON(value, volatileFields);
36
+ } else {
37
+ normalized[key] = value;
38
+ }
39
+ }
40
+
41
+ return normalized;
42
+ }
43
+
44
+ /**
45
+ * Strip timestamps and volatile data from string content
46
+ */
47
+ function stripVolatile(content) {
48
+ if (typeof content !== 'string') {
49
+ return content;
50
+ }
51
+
52
+ let cleaned = content;
53
+
54
+ // Strip ISO timestamps
55
+ cleaned = cleaned.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[.Z0-9]*/g, '[TIMESTAMP]');
56
+
57
+ // Strip UUIDs
58
+ cleaned = cleaned.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '[UUID]');
59
+
60
+ // Strip run IDs (format: YYYY-MM-DDTHH-MM-SSZ_XXXXXX)
61
+ cleaned = cleaned.replace(/\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z_[a-z0-9]+/gi, '[RUN_ID]');
62
+
63
+ // Strip execution times (durations)
64
+ cleaned = cleaned.replace(/"endedAt":\s*"[^"]*"/g, '"endedAt":"[TIMESTAMP]"');
65
+ cleaned = cleaned.replace(/"startedAt":\s*"[^"]*"/g, '"startedAt":"[TIMESTAMP]"');
66
+ cleaned = cleaned.replace(/"observedAt":\s*"[^"]*"/g, '"observedAt":"[TIMESTAMP]"');
67
+
68
+ // Strip finding IDs
69
+ cleaned = cleaned.replace(/"findingId":\s*"[^"]*"/g, '"findingId":"[FINDING_ID]"');
70
+
71
+ return cleaned;
72
+ }
73
+
74
+ /**
75
+ * Compute SHA256 hash of content
76
+ */
77
+ function hashContent(content) {
78
+ const hash = createHash('sha256');
79
+ hash.update(content, 'utf-8');
80
+ return hash.digest('hex');
81
+ }
82
+
83
+ /**
84
+ * Normalize and hash a JSON file
85
+ */
86
+ function hashJSONFile(filePath, volatileFields = []) {
87
+ try {
88
+ const content = readFileSync(filePath, 'utf-8');
89
+ // @ts-expect-error - readFileSync with encoding returns string
90
+ const parsed = JSON.parse(content);
91
+ const normalized = normalizeJSON(parsed, volatileFields);
92
+ const serialized = JSON.stringify(normalized);
93
+ const stripped = stripVolatile(serialized);
94
+ return hashContent(stripped);
95
+ } catch (e) {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Hash a raw file (e.g., screenshot PNG)
102
+ */
103
+ function _hashFile(filePath) {
104
+ try {
105
+ const content = readFileSync(filePath);
106
+ return hashContent(content.toString('utf-8'));
107
+ } catch (e) {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * H5: Compute deterministic digest for observations
114
+ * Ensures identical inputs produce identical digests
115
+ * Used for reproducibility proof
116
+ */
117
+ export function computeDigest(expectations, observations, metadata = {}) {
118
+ const digest = {
119
+ version: '1.0',
120
+ deterministicSeed: 'verax-h5-determinism-proof',
121
+ contentHashes: {},
122
+ normalized: {},
123
+ };
124
+
125
+ // Normalize expectations
126
+ const normalizedExpectations = (expectations || []).map(exp => ({
127
+ id: exp.id,
128
+ type: exp.type,
129
+ category: exp.category,
130
+ promise: exp.promise,
131
+ }));
132
+
133
+ const expString = JSON.stringify(normalizedExpectations);
134
+ digest.contentHashes.expectations = hashContent(expString);
135
+ digest.normalized.expectations = normalizedExpectations;
136
+
137
+ // Normalize observations (remove timing)
138
+ const normalizedObservations = (observations || []).map(obs => ({
139
+ id: obs.id,
140
+ category: obs.category,
141
+ observed: obs.observed,
142
+ reason: obs.reason,
143
+ }));
144
+
145
+ const obsString = JSON.stringify(normalizedObservations);
146
+ digest.contentHashes.observations = hashContent(obsString);
147
+ digest.normalized.observations = normalizedObservations;
148
+
149
+ // Normalize metadata
150
+ const normalizedMetadata = {
151
+ framework: metadata.framework,
152
+ url: metadata.url,
153
+ version: metadata.version,
154
+ };
155
+
156
+ const metaString = JSON.stringify(normalizedMetadata);
157
+ digest.contentHashes.metadata = hashContent(metaString);
158
+ digest.normalized.metadata = normalizedMetadata;
159
+
160
+ // Compute final digest
161
+ const digestInput = [
162
+ digest.contentHashes.expectations,
163
+ digest.contentHashes.observations,
164
+ digest.contentHashes.metadata,
165
+ digest.deterministicSeed,
166
+ ].join(':');
167
+
168
+ digest.deterministicDigest = hashContent(digestInput);
169
+
170
+ return digest;
171
+ }
172
+
173
+ /**
174
+ * H5: Validate determinism across multiple runs
175
+ */
176
+ export function validateDeterminism(digests) {
177
+ if (!digests || digests.length === 0) {
178
+ return {
179
+ isDeterministic: true,
180
+ reason: 'No runs to compare',
181
+ };
182
+ }
183
+
184
+ const firstDigest = digests[0].deterministicDigest;
185
+ const allMatch = digests.every(d => d.deterministicDigest === firstDigest);
186
+
187
+ return {
188
+ isDeterministic: allMatch,
189
+ firstDigest,
190
+ mismatchedRuns: !allMatch ? digests.map((d, i) => ({
191
+ runIndex: i,
192
+ digest: d.deterministicDigest,
193
+ })).filter(d => d.digest !== firstDigest) : [],
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Produce a complete run digest
199
+ */
200
+ export async function produceRunDigest(runPath, runData) {
201
+ const digest = {
202
+ format: 'run-digest-v1',
203
+ timestamp: new Date().toISOString(),
204
+ digests: {
205
+ learn: null,
206
+ observe: null,
207
+ findings: null,
208
+ evidence: {},
209
+ },
210
+ metadata: {
211
+ runPath,
212
+ isReproducible: false,
213
+ },
214
+ };
215
+
216
+ // Hash learn.json (should be deterministic)
217
+ const learnPath = resolve(runPath, 'learn.json');
218
+ const learnHash = hashJSONFile(learnPath, ['extractedAt', 'duration']);
219
+ if (learnHash) {
220
+ digest.digests.learn = learnHash;
221
+ }
222
+
223
+ // Hash observe.json (normalized)
224
+ const observePath = resolve(runPath, 'observe.json');
225
+ const observeHash = hashJSONFile(observePath, [
226
+ 'observedAt',
227
+ 'startedAt',
228
+ 'endedAt',
229
+ 'timing',
230
+ 'duration',
231
+ 'findingId',
232
+ ]);
233
+ if (observeHash) {
234
+ digest.digests.observe = observeHash;
235
+ }
236
+
237
+ // Hash findings.json (normalized)
238
+ const findingsPath = resolve(runPath, 'findings.json');
239
+ const findingsHash = hashJSONFile(findingsPath, ['findingId', 'confidence']);
240
+ if (findingsHash) {
241
+ digest.digests.findings = findingsHash;
242
+ }
243
+
244
+ // Hash evidence directory
245
+ if (runData?.evidence && Array.isArray(runData.evidence)) {
246
+ const evidenceDigests = {};
247
+ for (const evidenceFile of runData.evidence) {
248
+ if (evidenceFile.endsWith('.png') || evidenceFile.endsWith('.jpg')) {
249
+ // Skip images (they may have subtle compression differences)
250
+ evidenceDigests[evidenceFile] = '[IMAGE_SKIPPED]';
251
+ } else if (evidenceFile.endsWith('.json')) {
252
+ const filePath = resolve(runPath, 'evidence', evidenceFile);
253
+ const hash = hashJSONFile(filePath, ['timestamp', 'duration']);
254
+ if (hash) {
255
+ evidenceDigests[evidenceFile] = hash;
256
+ }
257
+ }
258
+ }
259
+ digest.digests.evidence = evidenceDigests;
260
+ }
261
+
262
+ // Determine if reproducible (all core files match if run again)
263
+ const hasAllHashes =
264
+ digest.digests.learn &&
265
+ digest.digests.observe &&
266
+ digest.digests.findings &&
267
+ Object.keys(digest.digests.evidence).length > 0;
268
+
269
+ digest.metadata.isReproducible = hasAllHashes;
270
+
271
+ return digest;
272
+ }
273
+
274
+ /**
275
+ * Compare two digests for determinism
276
+ */
277
+ export function compareDigests(digest1, digest2) {
278
+ const comparison = {
279
+ match: false,
280
+ diffs: [],
281
+ };
282
+
283
+ // Learn must match (deterministic)
284
+ if (digest1.digests.learn !== digest2.digests.learn) {
285
+ comparison.diffs.push('learn.json hash differs');
286
+ }
287
+
288
+ // Observe must match (deterministic execution)
289
+ if (digest1.digests.observe !== digest2.digests.observe) {
290
+ comparison.diffs.push('observe.json hash differs');
291
+ }
292
+
293
+ // Findings must match (deterministic classification)
294
+ if (digest1.digests.findings !== digest2.digests.findings) {
295
+ comparison.diffs.push('findings.json hash differs');
296
+ }
297
+
298
+ // Evidence files should match
299
+ const evidenceKeys1 = Object.keys(digest1.digests.evidence || {});
300
+ const evidenceKeys2 = Object.keys(digest2.digests.evidence || {});
301
+
302
+ if (evidenceKeys1.length !== evidenceKeys2.length) {
303
+ comparison.diffs.push(
304
+ `Evidence count differs: ${evidenceKeys1.length} vs ${evidenceKeys2.length}`
305
+ );
306
+ }
307
+
308
+ for (const key of evidenceKeys1) {
309
+ if (
310
+ digest1.digests.evidence[key] &&
311
+ digest2.digests.evidence[key] &&
312
+ digest1.digests.evidence[key] !== '[IMAGE_SKIPPED]' &&
313
+ digest1.digests.evidence[key] !== digest2.digests.evidence[key]
314
+ ) {
315
+ comparison.diffs.push(`Evidence file '${key}' hash differs`);
316
+ }
317
+ }
318
+
319
+ comparison.match = comparison.diffs.length === 0;
320
+
321
+ return comparison;
322
+ }
323
+
324
+ /**
325
+ * Save digest to file
326
+ */
327
+ export function saveDigest(digestPath, digest) {
328
+ try {
329
+ writeFileSync(digestPath, JSON.stringify(digest, null, 2), 'utf-8');
330
+ return true;
331
+ } catch (e) {
332
+ return false;
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Load digest from file
338
+ */
339
+ export function loadDigest(digestPath) {
340
+ try {
341
+ const content = readFileSync(digestPath, 'utf-8');
342
+ // @ts-expect-error - readFileSync with encoding returns string
343
+ return JSON.parse(content);
344
+ } catch (e) {
345
+ return null;
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Check if a run is deterministically reproducible
351
+ */
352
+ export function isRunDeterministic(digest) {
353
+ if (!digest) return false;
354
+ return (
355
+ Boolean(digest.digests.learn) &&
356
+ Boolean(digest.digests.observe) &&
357
+ Boolean(digest.digests.findings)
358
+ );
359
+ }
@@ -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
+ }