@veraxhq/verax 0.1.0 → 0.2.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 (126) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +14 -36
  4. package/src/cli/commands/default.js +523 -0
  5. package/src/cli/commands/doctor.js +165 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +402 -0
  8. package/src/cli/entry.js +196 -0
  9. package/src/cli/util/atomic-write.js +37 -0
  10. package/src/cli/util/detection-engine.js +296 -0
  11. package/src/cli/util/env-url.js +33 -0
  12. package/src/cli/util/errors.js +44 -0
  13. package/src/cli/util/events.js +34 -0
  14. package/src/cli/util/expectation-extractor.js +378 -0
  15. package/src/cli/util/findings-writer.js +31 -0
  16. package/src/cli/util/idgen.js +87 -0
  17. package/src/cli/util/learn-writer.js +39 -0
  18. package/src/cli/util/observation-engine.js +366 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +29 -0
  21. package/src/cli/util/project-discovery.js +277 -0
  22. package/src/cli/util/project-writer.js +26 -0
  23. package/src/cli/util/redact.js +128 -0
  24. package/src/cli/util/run-id.js +30 -0
  25. package/src/cli/util/summary-writer.js +32 -0
  26. package/src/verax/cli/ci-summary.js +35 -0
  27. package/src/verax/cli/context-explanation.js +89 -0
  28. package/src/verax/cli/doctor.js +277 -0
  29. package/src/verax/cli/error-normalizer.js +154 -0
  30. package/src/verax/cli/explain-output.js +105 -0
  31. package/src/verax/cli/finding-explainer.js +130 -0
  32. package/src/verax/cli/init.js +237 -0
  33. package/src/verax/cli/run-overview.js +163 -0
  34. package/src/verax/cli/url-safety.js +101 -0
  35. package/src/verax/cli/wizard.js +98 -0
  36. package/src/verax/cli/zero-findings-explainer.js +57 -0
  37. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  38. package/src/verax/core/action-classifier.js +86 -0
  39. package/src/verax/core/budget-engine.js +218 -0
  40. package/src/verax/core/canonical-outcomes.js +157 -0
  41. package/src/verax/core/decision-snapshot.js +335 -0
  42. package/src/verax/core/determinism-model.js +403 -0
  43. package/src/verax/core/incremental-store.js +237 -0
  44. package/src/verax/core/invariants.js +356 -0
  45. package/src/verax/core/promise-model.js +230 -0
  46. package/src/verax/core/replay-validator.js +350 -0
  47. package/src/verax/core/replay.js +222 -0
  48. package/src/verax/core/run-id.js +175 -0
  49. package/src/verax/core/run-manifest.js +99 -0
  50. package/src/verax/core/silence-impact.js +369 -0
  51. package/src/verax/core/silence-model.js +521 -0
  52. package/src/verax/detect/comparison.js +2 -34
  53. package/src/verax/detect/confidence-engine.js +764 -329
  54. package/src/verax/detect/detection-engine.js +293 -0
  55. package/src/verax/detect/evidence-index.js +177 -0
  56. package/src/verax/detect/expectation-model.js +194 -172
  57. package/src/verax/detect/explanation-helpers.js +187 -0
  58. package/src/verax/detect/finding-detector.js +450 -0
  59. package/src/verax/detect/findings-writer.js +44 -8
  60. package/src/verax/detect/flow-detector.js +366 -0
  61. package/src/verax/detect/index.js +172 -286
  62. package/src/verax/detect/interactive-findings.js +613 -0
  63. package/src/verax/detect/signal-mapper.js +308 -0
  64. package/src/verax/detect/verdict-engine.js +563 -0
  65. package/src/verax/evidence-index-writer.js +61 -0
  66. package/src/verax/index.js +90 -14
  67. package/src/verax/intel/effect-detector.js +368 -0
  68. package/src/verax/intel/handler-mapper.js +249 -0
  69. package/src/verax/intel/index.js +281 -0
  70. package/src/verax/intel/route-extractor.js +280 -0
  71. package/src/verax/intel/ts-program.js +256 -0
  72. package/src/verax/intel/vue-navigation-extractor.js +579 -0
  73. package/src/verax/intel/vue-router-extractor.js +323 -0
  74. package/src/verax/learn/action-contract-extractor.js +335 -101
  75. package/src/verax/learn/ast-contract-extractor.js +95 -5
  76. package/src/verax/learn/flow-extractor.js +172 -0
  77. package/src/verax/learn/manifest-writer.js +97 -47
  78. package/src/verax/learn/project-detector.js +40 -0
  79. package/src/verax/learn/route-extractor.js +27 -96
  80. package/src/verax/learn/state-extractor.js +212 -0
  81. package/src/verax/learn/static-extractor-navigation.js +114 -0
  82. package/src/verax/learn/static-extractor-validation.js +88 -0
  83. package/src/verax/learn/static-extractor.js +112 -4
  84. package/src/verax/learn/truth-assessor.js +24 -21
  85. package/src/verax/observe/aria-sensor.js +211 -0
  86. package/src/verax/observe/browser.js +10 -5
  87. package/src/verax/observe/console-sensor.js +1 -17
  88. package/src/verax/observe/domain-boundary.js +10 -1
  89. package/src/verax/observe/expectation-executor.js +512 -0
  90. package/src/verax/observe/flow-matcher.js +143 -0
  91. package/src/verax/observe/focus-sensor.js +196 -0
  92. package/src/verax/observe/human-driver.js +643 -275
  93. package/src/verax/observe/index.js +908 -27
  94. package/src/verax/observe/index.js.backup +1 -0
  95. package/src/verax/observe/interaction-discovery.js +365 -14
  96. package/src/verax/observe/interaction-runner.js +563 -198
  97. package/src/verax/observe/loading-sensor.js +139 -0
  98. package/src/verax/observe/navigation-sensor.js +255 -0
  99. package/src/verax/observe/network-sensor.js +55 -7
  100. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  101. package/src/verax/observe/observed-expectation.js +305 -0
  102. package/src/verax/observe/page-frontier.js +234 -0
  103. package/src/verax/observe/settle.js +37 -17
  104. package/src/verax/observe/state-sensor.js +389 -0
  105. package/src/verax/observe/timing-sensor.js +228 -0
  106. package/src/verax/observe/traces-writer.js +61 -20
  107. package/src/verax/observe/ui-signal-sensor.js +136 -17
  108. package/src/verax/scan-summary-writer.js +77 -15
  109. package/src/verax/shared/artifact-manager.js +110 -8
  110. package/src/verax/shared/budget-profiles.js +136 -0
  111. package/src/verax/shared/ci-detection.js +39 -0
  112. package/src/verax/shared/config-loader.js +170 -0
  113. package/src/verax/shared/dynamic-route-utils.js +218 -0
  114. package/src/verax/shared/expectation-coverage.js +44 -0
  115. package/src/verax/shared/expectation-prover.js +81 -0
  116. package/src/verax/shared/expectation-tracker.js +201 -0
  117. package/src/verax/shared/expectations-writer.js +60 -0
  118. package/src/verax/shared/first-run.js +44 -0
  119. package/src/verax/shared/progress-reporter.js +171 -0
  120. package/src/verax/shared/retry-policy.js +14 -1
  121. package/src/verax/shared/root-artifacts.js +49 -0
  122. package/src/verax/shared/scan-budget.js +86 -0
  123. package/src/verax/shared/url-normalizer.js +162 -0
  124. package/src/verax/shared/zip-artifacts.js +65 -0
  125. package/src/verax/validate/context-validator.js +244 -0
  126. package/src/verax/validate/context-validator.js.bak +0 -0
@@ -0,0 +1,350 @@
1
+ /**
2
+ * PHASE 6 — REPLAY TRUST VALIDATION
3
+ *
4
+ * Compares decisions from previous run with current run to identify deviations.
5
+ * DOES NOT fail on deviations - only reports them with explicit reasons.
6
+ *
7
+ * Answers: "Why was result different?" or "Why is result identical?"
8
+ *
9
+ * Rules:
10
+ * - Load decisions.json from baseline run
11
+ * - Compare with current run decisions
12
+ * - Warn (not fail) on deviations
13
+ * - Categorize deviations: truncation_difference, timeout_difference, environment_difference
14
+ */
15
+
16
+ import { DecisionRecorder, DECISION_IDS } from './determinism-model.js';
17
+
18
+ /**
19
+ * Compare two decision values and determine if they're equivalent
20
+ * @param {*} value1 - First value
21
+ * @param {*} value2 - Second value
22
+ * @returns {boolean} - True if values are considered equivalent
23
+ */
24
+ function areValuesEquivalent(value1, value2) {
25
+ // Handle null/undefined
26
+ if (value1 == null && value2 == null) return true;
27
+ if (value1 == null || value2 == null) return false;
28
+
29
+ // Handle primitives
30
+ if (typeof value1 !== 'object' || typeof value2 !== 'object') {
31
+ return value1 === value2;
32
+ }
33
+
34
+ // Handle objects/arrays (shallow comparison for now)
35
+ const keys1 = Object.keys(value1);
36
+ const keys2 = Object.keys(value2);
37
+
38
+ if (keys1.length !== keys2.length) return false;
39
+
40
+ for (const key of keys1) {
41
+ if (value1[key] !== value2[key]) return false;
42
+ }
43
+
44
+ return true;
45
+ }
46
+
47
+ /**
48
+ * Categorize a decision deviation
49
+ * @param {Object} baseline - Baseline decision
50
+ * @param {Object} current - Current decision
51
+ * @returns {string} - Deviation category
52
+ */
53
+ function categorizeDeviation(baseline, current) {
54
+ const category = baseline.category;
55
+
56
+ switch (category) {
57
+ case 'BUDGET':
58
+ return 'budget_difference';
59
+ case 'TIMEOUT':
60
+ return 'timeout_difference';
61
+ case 'TRUNCATION':
62
+ return 'truncation_difference';
63
+ case 'RETRY':
64
+ return 'retry_difference';
65
+ case 'ADAPTIVE_STABILIZATION':
66
+ return 'stabilization_difference';
67
+ case 'ENVIRONMENT':
68
+ return 'environment_difference';
69
+ default:
70
+ return 'unknown_difference';
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Explain why a deviation occurred
76
+ * @param {Object} baseline - Baseline decision
77
+ * @param {Object} current - Current decision
78
+ * @returns {string} - Human-readable explanation
79
+ */
80
+ function explainDeviation(baseline, current) {
81
+ const category = baseline.category;
82
+ const devCategory = categorizeDeviation(baseline, current);
83
+
84
+ // Build explanation based on decision type
85
+ switch (devCategory) {
86
+ case 'budget_difference':
87
+ return `Budget configuration changed: baseline used ${JSON.stringify(baseline.chosen_value)}, current uses ${JSON.stringify(current.chosen_value)}`;
88
+
89
+ case 'timeout_difference':
90
+ return `Timeout configuration changed: baseline used ${baseline.chosen_value}ms, current uses ${current.chosen_value}ms`;
91
+
92
+ case 'truncation_difference':
93
+ if (baseline.decision_id === current.decision_id) {
94
+ return `Truncation point changed: baseline reached limit at ${JSON.stringify(baseline.inputs)}, current reached at ${JSON.stringify(current.inputs)}`;
95
+ } else {
96
+ return `Different truncation occurred: baseline had ${baseline.decision_id}, current has ${current.decision_id}`;
97
+ }
98
+
99
+ case 'retry_difference':
100
+ return `Retry behavior changed: baseline had ${baseline.chosen_value} retries, current has ${current.chosen_value} retries`;
101
+
102
+ case 'stabilization_difference':
103
+ return `Adaptive stabilization changed: baseline ${baseline.chosen_value ? 'extended' : 'did not extend'} stabilization, current ${current.chosen_value ? 'extends' : 'does not extend'}`;
104
+
105
+ case 'environment_difference':
106
+ return `Environment changed: baseline detected ${JSON.stringify(baseline.chosen_value)}, current detects ${JSON.stringify(current.chosen_value)}`;
107
+
108
+ default:
109
+ return `Decision changed: baseline value ${JSON.stringify(baseline.chosen_value)}, current value ${JSON.stringify(current.chosen_value)}`;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Decision comparison result
115
+ * @typedef {Object} DecisionComparison
116
+ * @property {string} decision_id - Decision identifier
117
+ * @property {boolean} matches - Whether baseline and current match
118
+ * @property {string} deviation_category - Category of deviation (if any)
119
+ * @property {string} explanation - Human-readable explanation
120
+ * @property {Object} baseline_value - Baseline chosen value
121
+ * @property {Object} current_value - Current chosen value
122
+ */
123
+
124
+ /**
125
+ * Replay validation result
126
+ * @typedef {Object} ReplayValidation
127
+ * @property {boolean} isDeterministic - Whether runs are identical
128
+ * @property {number} totalDecisions - Total decisions compared
129
+ * @property {number} matchingDecisions - Count of matching decisions
130
+ * @property {number} deviations - Count of deviations
131
+ * @property {Object} deviationsByCategory - Deviations grouped by category
132
+ * @property {DecisionComparison[]} comparisonDetails - Detailed comparison for each decision
133
+ * @property {string[]} baselineOnlyDecisions - Decision IDs present only in baseline
134
+ * @property {string[]} currentOnlyDecisions - Decision IDs present only in current
135
+ * @property {string} verdict - Overall verdict ('identical', 'minor_deviations', 'major_deviations')
136
+ */
137
+
138
+ /**
139
+ * Compare decisions from baseline and current run
140
+ * @param {Object} baselineExport - Exported decisions from baseline run
141
+ * @param {Object} currentExport - Exported decisions from current run
142
+ * @returns {ReplayValidation} - Comparison result
143
+ */
144
+ export function compareReplayDecisions(baselineExport, currentExport) {
145
+ const baselineRecorder = DecisionRecorder.fromExport(baselineExport);
146
+ const currentRecorder = DecisionRecorder.fromExport(currentExport);
147
+
148
+ const baselineDecisions = baselineRecorder.getAll();
149
+ const currentDecisions = currentRecorder.getAll();
150
+
151
+ // Index current decisions by decision_id for fast lookup
152
+ const currentByDecisionId = new Map();
153
+ for (const decision of currentDecisions) {
154
+ // Multiple decisions with same ID possible (e.g., multiple truncations)
155
+ if (!currentByDecisionId.has(decision.decision_id)) {
156
+ currentByDecisionId.set(decision.decision_id, []);
157
+ }
158
+ currentByDecisionId.get(decision.decision_id).push(decision);
159
+ }
160
+
161
+ // Track comparison details
162
+ const comparisonDetails = [];
163
+ let matchingDecisions = 0;
164
+ const deviationsByCategory = {};
165
+ const baselineOnlyDecisions = [];
166
+ const currentOnlyDecisions = [];
167
+
168
+ // Compare baseline decisions with current
169
+ for (const baselineDecision of baselineDecisions) {
170
+ const decisionId = baselineDecision.decision_id;
171
+
172
+ if (!currentByDecisionId.has(decisionId)) {
173
+ // Decision present in baseline but not in current
174
+ baselineOnlyDecisions.push(decisionId);
175
+ const deviation = categorizeDeviation(baselineDecision, null);
176
+ deviationsByCategory[deviation] = (deviationsByCategory[deviation] || 0) + 1;
177
+
178
+ comparisonDetails.push({
179
+ decision_id: decisionId,
180
+ matches: false,
181
+ deviation_category: deviation,
182
+ explanation: `Decision present in baseline but missing in current run: ${baselineDecision.reason}`,
183
+ baseline_value: baselineDecision.chosen_value,
184
+ current_value: null
185
+ });
186
+ continue;
187
+ }
188
+
189
+ // Find matching decision in current run (compare by timestamp proximity or sequence)
190
+ const currentCandidates = currentByDecisionId.get(decisionId);
191
+ let bestMatch = null;
192
+ let bestMatchDistance = Infinity;
193
+
194
+ for (const currentCandidate of currentCandidates) {
195
+ // Use timestamp proximity to match decisions (within 1 second)
196
+ const timeDiff = Math.abs(currentCandidate.timestamp - baselineDecision.timestamp);
197
+ if (timeDiff < bestMatchDistance) {
198
+ bestMatchDistance = timeDiff;
199
+ bestMatch = currentCandidate;
200
+ }
201
+ }
202
+
203
+ // Compare values
204
+ const valuesMatch = areValuesEquivalent(baselineDecision.chosen_value, bestMatch.chosen_value);
205
+ const inputsMatch = areValuesEquivalent(baselineDecision.inputs, bestMatch.inputs);
206
+
207
+ if (valuesMatch && inputsMatch) {
208
+ matchingDecisions++;
209
+ comparisonDetails.push({
210
+ decision_id: decisionId,
211
+ matches: true,
212
+ deviation_category: null,
213
+ explanation: 'Decision identical between runs',
214
+ baseline_value: baselineDecision.chosen_value,
215
+ current_value: bestMatch.chosen_value
216
+ });
217
+ } else {
218
+ const deviation = categorizeDeviation(baselineDecision, bestMatch);
219
+ deviationsByCategory[deviation] = (deviationsByCategory[deviation] || 0) + 1;
220
+
221
+ comparisonDetails.push({
222
+ decision_id: decisionId,
223
+ matches: false,
224
+ deviation_category: deviation,
225
+ explanation: explainDeviation(baselineDecision, bestMatch),
226
+ baseline_value: baselineDecision.chosen_value,
227
+ current_value: bestMatch.chosen_value
228
+ });
229
+ }
230
+ }
231
+
232
+ // Check for decisions only in current run
233
+ const baselineDecisionIds = new Set(baselineDecisions.map(d => d.decision_id));
234
+ for (const currentDecision of currentDecisions) {
235
+ if (!baselineDecisionIds.has(currentDecision.decision_id)) {
236
+ currentOnlyDecisions.push(currentDecision.decision_id);
237
+ const deviation = categorizeDeviation(currentDecision, null);
238
+ deviationsByCategory[deviation] = (deviationsByCategory[deviation] || 0) + 1;
239
+
240
+ comparisonDetails.push({
241
+ decision_id: currentDecision.decision_id,
242
+ matches: false,
243
+ deviation_category: deviation,
244
+ explanation: `Decision present in current run but missing in baseline: ${currentDecision.reason}`,
245
+ baseline_value: null,
246
+ current_value: currentDecision.chosen_value
247
+ });
248
+ }
249
+ }
250
+
251
+ // Compute overall verdict
252
+ const totalDecisions = Math.max(baselineDecisions.length, currentDecisions.length);
253
+ const deviations = totalDecisions - matchingDecisions;
254
+ const isDeterministic = deviations === 0;
255
+
256
+ let verdict = 'identical';
257
+ if (deviations > 0) {
258
+ // Minor deviations: only environment or timeout differences
259
+ const hasMajorDeviations = Object.keys(deviationsByCategory).some(cat =>
260
+ !['environment_difference', 'timeout_difference'].includes(cat)
261
+ );
262
+ verdict = hasMajorDeviations ? 'major_deviations' : 'minor_deviations';
263
+ }
264
+
265
+ return {
266
+ isDeterministic,
267
+ totalDecisions,
268
+ matchingDecisions,
269
+ deviations,
270
+ deviationsByCategory,
271
+ comparisonDetails,
272
+ baselineOnlyDecisions: [...new Set(baselineOnlyDecisions)],
273
+ currentOnlyDecisions: [...new Set(currentOnlyDecisions)],
274
+ verdict
275
+ };
276
+ }
277
+
278
+ /**
279
+ * Load and compare decisions from file paths
280
+ * @param {string} baselinePath - Path to baseline decisions.json
281
+ * @param {string} currentPath - Path to current decisions.json
282
+ * @returns {ReplayValidation} - Comparison result
283
+ */
284
+ export function validateReplay(baselinePath, currentPath) {
285
+ const fs = require('fs');
286
+
287
+ if (!fs.existsSync(baselinePath)) {
288
+ throw new Error(`Baseline decisions not found: ${baselinePath}`);
289
+ }
290
+
291
+ if (!fs.existsSync(currentPath)) {
292
+ throw new Error(`Current decisions not found: ${currentPath}`);
293
+ }
294
+
295
+ const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
296
+ const current = JSON.parse(fs.readFileSync(currentPath, 'utf-8'));
297
+
298
+ return compareReplayDecisions(baseline, current);
299
+ }
300
+
301
+ /**
302
+ * Format replay validation result for display
303
+ * @param {ReplayValidation} validation - Validation result
304
+ * @returns {string} - Formatted text output
305
+ */
306
+ export function formatReplayValidation(validation) {
307
+ const lines = [];
308
+
309
+ lines.push('=== REPLAY VALIDATION RESULT ===');
310
+ lines.push('');
311
+ lines.push(`Verdict: ${validation.verdict.toUpperCase()}`);
312
+ lines.push(`Deterministic: ${validation.isDeterministic ? 'YES' : 'NO'}`);
313
+ lines.push(`Total Decisions: ${validation.totalDecisions}`);
314
+ lines.push(`Matching: ${validation.matchingDecisions}`);
315
+ lines.push(`Deviations: ${validation.deviations}`);
316
+ lines.push('');
317
+
318
+ if (validation.deviations > 0) {
319
+ lines.push('--- Deviations by Category ---');
320
+ for (const [category, count] of Object.entries(validation.deviationsByCategory)) {
321
+ lines.push(` ${category}: ${count}`);
322
+ }
323
+ lines.push('');
324
+
325
+ lines.push('--- Deviation Details ---');
326
+ for (const detail of validation.comparisonDetails) {
327
+ if (!detail.matches) {
328
+ lines.push(`[${detail.decision_id}]`);
329
+ lines.push(` ${detail.explanation}`);
330
+ lines.push('');
331
+ }
332
+ }
333
+ } else {
334
+ lines.push('✓ All decisions identical between baseline and current run');
335
+ }
336
+
337
+ if (validation.baselineOnlyDecisions.length > 0) {
338
+ lines.push('--- Baseline-Only Decisions ---');
339
+ lines.push(validation.baselineOnlyDecisions.join(', '));
340
+ lines.push('');
341
+ }
342
+
343
+ if (validation.currentOnlyDecisions.length > 0) {
344
+ lines.push('--- Current-Only Decisions ---');
345
+ lines.push(validation.currentOnlyDecisions.join(', '));
346
+ lines.push('');
347
+ }
348
+
349
+ return lines.join('\n');
350
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * PHASE 5: REPLAY MODE
3
+ *
4
+ * Load artifacts from previous run and recompute summaries without browser/network.
5
+ * Output must be identical to original run.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { computeFileHash } from './run-id.js';
11
+ import SilenceTracker from './silence-model.js';
12
+
13
+ /**
14
+ * Load artifacts from a run directory
15
+ *
16
+ * @param {string} runDir - Path to run directory (.verax/runs/<runId>)
17
+ * @returns {Object} Loaded artifacts with integrity status
18
+ */
19
+ export function loadRunArtifacts(runDir) {
20
+ const artifacts = {
21
+ runManifest: null,
22
+ traces: null,
23
+ findings: null,
24
+ manifest: null,
25
+ integrityViolations: []
26
+ };
27
+
28
+ // Load run manifest first
29
+ const runManifestPath = join(runDir, 'run-manifest.json');
30
+ if (!existsSync(runManifestPath)) {
31
+ throw new Error(`Run manifest not found: ${runManifestPath}`);
32
+ }
33
+
34
+ try {
35
+ artifacts.runManifest = JSON.parse(readFileSync(runManifestPath, 'utf-8'));
36
+ } catch (error) {
37
+ throw new Error(`Failed to parse run manifest: ${error.message}`);
38
+ }
39
+
40
+ // Load and verify each artifact
41
+ const expectedHashes = artifacts.runManifest.artifactHashes || {};
42
+
43
+ // Load traces
44
+ const tracesPath = join(runDir, 'traces.json');
45
+ if (existsSync(tracesPath)) {
46
+ try {
47
+ artifacts.traces = JSON.parse(readFileSync(tracesPath, 'utf-8'));
48
+
49
+ // Verify hash
50
+ if (expectedHashes.traces) {
51
+ const actualHash = computeFileHash(tracesPath);
52
+ if (actualHash !== expectedHashes.traces) {
53
+ artifacts.integrityViolations.push({
54
+ artifact: 'traces.json',
55
+ path: tracesPath,
56
+ expectedHash: expectedHashes.traces,
57
+ actualHash,
58
+ reason: 'hash_mismatch'
59
+ });
60
+ }
61
+ }
62
+ } catch (error) {
63
+ artifacts.integrityViolations.push({
64
+ artifact: 'traces.json',
65
+ path: tracesPath,
66
+ reason: 'parse_error',
67
+ error: error.message
68
+ });
69
+ }
70
+ } else if (expectedHashes.traces) {
71
+ artifacts.integrityViolations.push({
72
+ artifact: 'traces.json',
73
+ path: tracesPath,
74
+ expectedHash: expectedHashes.traces,
75
+ reason: 'file_missing'
76
+ });
77
+ }
78
+
79
+ // Load findings
80
+ const findingsPath = join(runDir, 'findings.json');
81
+ if (existsSync(findingsPath)) {
82
+ try {
83
+ artifacts.findings = JSON.parse(readFileSync(findingsPath, 'utf-8'));
84
+
85
+ // Verify hash
86
+ if (expectedHashes.findings) {
87
+ const actualHash = computeFileHash(findingsPath);
88
+ if (actualHash !== expectedHashes.findings) {
89
+ artifacts.integrityViolations.push({
90
+ artifact: 'findings.json',
91
+ path: findingsPath,
92
+ expectedHash: expectedHashes.findings,
93
+ actualHash,
94
+ reason: 'hash_mismatch'
95
+ });
96
+ }
97
+ }
98
+ } catch (error) {
99
+ artifacts.integrityViolations.push({
100
+ artifact: 'findings.json',
101
+ path: findingsPath,
102
+ reason: 'parse_error',
103
+ error: error.message
104
+ });
105
+ }
106
+ }
107
+
108
+ // Load manifest
109
+ const manifestPath = join(runDir, 'manifest.json');
110
+ if (existsSync(manifestPath)) {
111
+ try {
112
+ artifacts.manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
113
+
114
+ // Verify hash
115
+ if (expectedHashes.manifest) {
116
+ const actualHash = computeFileHash(manifestPath);
117
+ if (actualHash !== expectedHashes.manifest) {
118
+ artifacts.integrityViolations.push({
119
+ artifact: 'manifest.json',
120
+ path: manifestPath,
121
+ expectedHash: expectedHashes.manifest,
122
+ actualHash,
123
+ reason: 'hash_mismatch'
124
+ });
125
+ }
126
+ }
127
+ } catch (error) {
128
+ artifacts.integrityViolations.push({
129
+ artifact: 'manifest.json',
130
+ path: manifestPath,
131
+ reason: 'parse_error',
132
+ error: error.message
133
+ });
134
+ }
135
+ }
136
+
137
+ return artifacts;
138
+ }
139
+
140
+ /**
141
+ * Replay a previous run from artifacts
142
+ *
143
+ * @param {string} runDir - Path to run directory
144
+ * @returns {Object} Replay result with observation summary
145
+ */
146
+ export async function replayRun(runDir) {
147
+ // Load artifacts
148
+ const artifacts = loadRunArtifacts(runDir);
149
+
150
+ // Check for critical integrity violations
151
+ if (artifacts.integrityViolations.length > 0) {
152
+ // Record integrity violations as silences
153
+ const silenceTracker = new SilenceTracker();
154
+ for (const violation of artifacts.integrityViolations) {
155
+ silenceTracker.record({
156
+ scope: 'integrity',
157
+ reason: violation.reason,
158
+ description: `Artifact integrity violation: ${violation.artifact}`,
159
+ context: {
160
+ artifact: violation.artifact,
161
+ path: violation.path,
162
+ expectedHash: violation.expectedHash,
163
+ actualHash: violation.actualHash,
164
+ error: violation.error
165
+ },
166
+ impact: 'replay_affected'
167
+ });
168
+ }
169
+
170
+ // Return early with integrity violations
171
+ return {
172
+ runManifest: artifacts.runManifest,
173
+ integrityViolations: artifacts.integrityViolations,
174
+ silences: silenceTracker.export(),
175
+ replaySuccessful: false
176
+ };
177
+ }
178
+
179
+ // Artifacts are valid - recompute summaries
180
+ if (!artifacts.traces) {
181
+ throw new Error('No traces found - cannot replay run');
182
+ }
183
+
184
+ // Extract data from artifacts
185
+ const traces = artifacts.traces.traces || [];
186
+ const findings = artifacts.findings?.findings || [];
187
+ const observeTruth = artifacts.traces.observeTruth || {
188
+ interactionsObserved: traces.length
189
+ };
190
+
191
+ // Import verdict engine to recompute summary
192
+ const { computeObservationSummary } = await import('../detect/verdict-engine.js');
193
+
194
+ // Create silence tracker from loaded silences
195
+ const silenceTracker = new SilenceTracker();
196
+ if (artifacts.traces.silences && artifacts.traces.silences.entries) {
197
+ silenceTracker.recordBatch(artifacts.traces.silences.entries);
198
+ }
199
+
200
+ // Recompute observation summary (no browser, just from loaded data)
201
+ const observationSummary = computeObservationSummary(
202
+ findings,
203
+ { ...observeTruth, traces },
204
+ artifacts.manifest?.learnTruth || {},
205
+ [],
206
+ observeTruth.budgetExceeded,
207
+ null,
208
+ null,
209
+ silenceTracker
210
+ );
211
+
212
+ return {
213
+ runManifest: artifacts.runManifest,
214
+ observationSummary,
215
+ traces,
216
+ findings,
217
+ manifest: artifacts.manifest,
218
+ integrityViolations: [],
219
+ silences: silenceTracker.export(),
220
+ replaySuccessful: true
221
+ };
222
+ }