@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
@@ -75,6 +75,14 @@ export async function detectFindings(learnData, observeData, projectPath, onProg
75
75
  findings,
76
76
  stats,
77
77
  detectedAt: new Date().toISOString(),
78
+ enforcement: {
79
+ evidenceLawEnforced: true,
80
+ contractVersion: 1,
81
+ timestamp: new Date().toISOString(),
82
+ droppedCount: 0,
83
+ downgradedCount: 0,
84
+ downgrades: []
85
+ }
78
86
  };
79
87
  }
80
88
 
@@ -100,7 +108,9 @@ function getObservationForExpectation(expectation, observationMap) {
100
108
  }
101
109
 
102
110
  function classificationIcon(classification) {
103
- switch (classification) {
111
+ // Handle both old format and new taxonomy format
112
+ const baseClassification = classification.split(':')[0];
113
+ switch (baseClassification) {
104
114
  case 'observed':
105
115
  return '✓';
106
116
  case 'silent-failure':
@@ -115,7 +125,9 @@ function classificationIcon(classification) {
115
125
  }
116
126
 
117
127
  function findingStatKey(classification) {
118
- switch (classification) {
128
+ // Handle both old format and new taxonomy format
129
+ const baseClassification = classification.split(':')[0];
130
+ switch (baseClassification) {
119
131
  case 'silent-failure':
120
132
  return 'silentFailures';
121
133
  case 'observed':
@@ -131,6 +143,8 @@ function findingStatKey(classification) {
131
143
 
132
144
  /**
133
145
  * Classify a single expectation according to deterministic rules.
146
+ * EVIDENCE GATE: silent-failure REQUIRES evidence.
147
+ * OUTCOME BINDING: Use attempt.cause to provide precise taxonomy.
134
148
  */
135
149
  function classifyExpectation(expectation, observation) {
136
150
  const finding = {
@@ -148,13 +162,15 @@ function classifyExpectation(expectation, observation) {
148
162
  const attempted = Boolean(observation?.attempted);
149
163
  const observed = observation?.observed === true;
150
164
  const reason = observation?.reason || null;
165
+ const cause = observation?.cause || null; // NEW: precise cause from planner
151
166
 
152
167
  const evidence = normalizeEvidence(observation?.evidenceFiles || []);
153
168
  finding.evidence = evidence;
154
169
 
155
170
  const evidenceSignals = analyzeEvidenceSignals(observation, evidence);
171
+ const hasAnyEvidence = evidence.length > 0 || evidenceSignals.hasDomChange;
156
172
 
157
- // 1) observed
173
+ // 1) observed (success)
158
174
  if (observed) {
159
175
  finding.classification = 'observed';
160
176
  finding.reason = 'Expectation observed at runtime';
@@ -163,8 +179,8 @@ function classifyExpectation(expectation, observation) {
163
179
  return finding;
164
180
  }
165
181
 
166
- // 2) coverage-gap (not attempted or explicitly blocked)
167
- if (!attempted || isSafetySkip(reason)) {
182
+ // 2) coverage-gap (not attempted or safety skip)
183
+ if (!attempted) {
168
184
  finding.classification = 'coverage-gap';
169
185
  finding.reason = reason || 'No observation attempt recorded';
170
186
  finding.impact = 'LOW';
@@ -172,29 +188,48 @@ function classifyExpectation(expectation, observation) {
172
188
  return finding;
173
189
  }
174
190
 
175
- // 3) silent-failure (attempted, observed === false, no safety skip)
176
- if (attempted && observation?.observed === false && !isSafetySkip(reason)) {
177
- finding.classification = 'silent-failure';
178
- finding.reason = reason || 'Expected behavior not observed';
179
- finding.impact = getImpact(expectation);
180
- finding.confidence = calculateConfidence(expectation, evidenceSignals, 'silent-failure');
181
- return finding;
182
- }
183
-
184
- // 4) unproven (attempted, ambiguous evidence)
191
+ // 3) Attempted but not observed - apply EVIDENCE GATE + OUTCOME BINDING
185
192
  if (attempted && !observed) {
186
- finding.classification = 'unproven';
187
- finding.reason = reason || 'Attempted but evidence insufficient';
188
- finding.impact = 'MEDIUM';
189
- finding.confidence = calculateConfidence(expectation, evidenceSignals, 'unproven');
193
+ // CRITICAL: Evidence Gate - silent-failure REQUIRES evidence
194
+ if (!hasAnyEvidence) {
195
+ // NO EVIDENCE → cannot prove silence → coverage-gap or unproven
196
+ if (isSafetySkip(reason)) {
197
+ finding.classification = 'coverage-gap';
198
+ finding.reason = reason || 'Blocked or skipped for safety';
199
+ finding.impact = 'LOW';
200
+ finding.confidence = 0;
201
+ } else {
202
+ finding.classification = 'unproven';
203
+ finding.reason = reason || 'Attempted but no evidence captured';
204
+ finding.impact = 'MEDIUM';
205
+ finding.confidence = 0;
206
+ }
207
+ return finding;
208
+ }
209
+
210
+ // HAS EVIDENCE → can classify as silent-failure with PRECISE taxonomy
211
+ let taxonomy = 'no-change'; // default
212
+
213
+ if (cause) {
214
+ // Use the cause from interaction planner (most precise)
215
+ taxonomy = cause; // 'not-found' | 'blocked' | 'prevented-submit' | 'timeout' | 'no-change' | 'error'
216
+ } else {
217
+ // Fallback to signal-based detection (legacy)
218
+ taxonomy = determineSilenceTaxonomy(reason, evidenceSignals);
219
+ }
220
+
221
+ finding.classification = `silent-failure:${taxonomy}`;
222
+ finding.reason = reason || `Expected behavior not observed (${taxonomy})`;
223
+ finding.impact = getImpact(expectation);
224
+ finding.confidence = calculateConfidenceFromEvidence(evidenceSignals);
190
225
  return finding;
191
226
  }
192
227
 
193
- // 5) informational fallback
228
+ // 4) Fallback
194
229
  finding.classification = 'informational';
195
230
  finding.reason = reason || 'No classification rule matched';
196
231
  finding.impact = 'LOW';
197
- finding.confidence = calculateConfidence(expectation, evidenceSignals, 'informational');
232
+ finding.confidence = 0;
198
233
  return finding;
199
234
  }
200
235
 
@@ -206,32 +241,61 @@ function isSafetySkip(reason) {
206
241
  }
207
242
 
208
243
  /**
209
- * Deterministic confidence calculation.
210
- * Screenshots + network + DOM change => >=0.8
211
- * Screenshots only => ~0.6
212
- * Weak signals => <0.5
244
+ * Determine silence taxonomy based on reason and evidence signals.
245
+ * Returns: no-change | blocked | not-found | timeout | prevented-submit
213
246
  */
214
- function calculateConfidence(expectation, evidenceSignals, classification) {
215
- if (classification === 'observed') return 1.0;
216
- if (classification === 'coverage-gap') return 0;
247
+ function determineSilenceTaxonomy(reason, evidenceSignals) {
248
+ if (!reason) {
249
+ // No explicit reason - check evidence
250
+ if (evidenceSignals.hasScreenshots || evidenceSignals.hasDomChange) {
251
+ return 'no-change'; // Evidence exists but no observed change
252
+ }
253
+ return 'no-change';
254
+ }
217
255
 
218
- const { hasScreenshots, hasNetworkLogs, hasDomChange } = evidenceSignals;
256
+ const lower = reason.toLowerCase();
219
257
 
220
- if (classification === 'silent-failure') {
221
- if (hasScreenshots && hasNetworkLogs && hasDomChange) return 0.85;
222
- if (hasScreenshots && hasNetworkLogs) return 0.75;
223
- if (hasScreenshots && hasDomChange) return 0.7;
224
- if (hasScreenshots) return 0.6;
225
- if (hasNetworkLogs || hasDomChange) return 0.5;
226
- return 0.4; // weak signals, attempted but no evidence
258
+ // Check for specific conditions
259
+ if (lower.includes('timeout') || lower.includes('timed out')) {
260
+ return 'timeout';
227
261
  }
228
-
229
- if (classification === 'unproven') {
230
- if (hasScreenshots || hasNetworkLogs || hasDomChange) return 0.45;
231
- return 0.3;
262
+ if (lower.includes('not found') || lower.includes('element not found') || lower.includes('selector not found') || lower.includes('not-found')) {
263
+ return 'not-found';
264
+ }
265
+ if (lower.includes('blocked') || lower.includes('not-interactable') || lower.includes('interactable')) {
266
+ return 'blocked';
232
267
  }
268
+ if (lower.includes('prevented') || lower.includes('prevented-submit') || lower.includes('submit-prevented')) {
269
+ return 'prevented-submit';
270
+ }
271
+ if (lower.includes('no matching event') || lower.includes('no change') || lower.includes('no-change')) {
272
+ return 'no-change';
273
+ }
274
+
275
+ // Default to no-change if evidence exists
276
+ return 'no-change';
277
+ }
278
+
279
+ /**
280
+ * Calculate confidence from evidence ONLY.
281
+ * No evidence = 0 confidence.
282
+ */
283
+ function calculateConfidenceFromEvidence(evidenceSignals) {
284
+ const { hasScreenshots, hasNetworkLogs, hasDomChange } = evidenceSignals;
233
285
 
234
- return 0.3;
286
+ // Multiple strong signals
287
+ if (hasScreenshots && hasNetworkLogs && hasDomChange) return 0.85;
288
+ if (hasScreenshots && hasNetworkLogs) return 0.75;
289
+ if (hasScreenshots && hasDomChange) return 0.7;
290
+
291
+ // Single strong signal
292
+ if (hasScreenshots) return 0.6;
293
+
294
+ // Weak signals
295
+ if (hasNetworkLogs || hasDomChange) return 0.5;
296
+
297
+ // No evidence
298
+ return 0;
235
299
  }
236
300
 
237
301
  function analyzeEvidenceSignals(observation, evidence) {
@@ -19,7 +19,7 @@ import { writeDeterminismReport } from './determinism-writer.js';
19
19
  * @param {string} options.out - Output directory
20
20
  * @returns {Promise<Object>} Determinism check results
21
21
  */
22
- export async function runWithDeterminism(scanFn, options = {}) {
22
+ export async function runWithDeterminism(scanFn, options = { runs: 2, out: '.verax' }) {
23
23
  const { runs = 2, out = '.verax' } = options;
24
24
 
25
25
  // Wrap scan function to return artifact paths
@@ -61,6 +61,7 @@ export async function runWithDeterminism(scanFn, options = {}) {
61
61
  if (existsSync(metaPath)) {
62
62
  try {
63
63
  const metaContent = readFileSync(metaPath, 'utf-8');
64
+ // @ts-expect-error - readFileSync with encoding returns string
64
65
  const meta = JSON.parse(metaContent);
65
66
  return meta.runFingerprint || null;
66
67
  } catch {
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { resolve } from 'path';
8
8
  import { mkdirSync, writeFileSync } from 'fs';
9
- import { ARTIFACT_REGISTRY, getArtifactVersions } from '../../verax/core/artifacts/registry.js';
9
+ import { ARTIFACT_REGISTRY, getArtifactVersions } from '../../verax/core/artifacts/registry.js'; // eslint-disable-line no-unused-vars
10
10
 
11
11
  /**
12
12
  * PHASE 18: Write determinism report
@@ -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
+ }