@veraxhq/verax 0.1.0 → 0.2.1
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.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +24 -36
- package/src/cli/commands/default.js +681 -0
- package/src/cli/commands/doctor.js +197 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +586 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +297 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +110 -0
- package/src/cli/util/expectation-extractor.js +388 -0
- package/src/cli/util/findings-writer.js +32 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +412 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +30 -0
- package/src/cli/util/project-discovery.js +297 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +43 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +111 -0
- package/src/verax/cli/wizard.js +109 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +432 -0
- package/src/verax/core/incremental-store.js +245 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +523 -0
- package/src/verax/detect/comparison.js +7 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +127 -0
- package/src/verax/detect/expectation-model.js +241 -168
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +41 -12
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +200 -288
- package/src/verax/detect/interactive-findings.js +612 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +561 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +103 -15
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +642 -0
- package/src/verax/intel/vue-router-extractor.js +325 -0
- package/src/verax/learn/action-contract-extractor.js +338 -104
- package/src/verax/learn/ast-contract-extractor.js +148 -6
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +122 -58
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +28 -97
- package/src/verax/learn/route-validator.js +8 -7
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +119 -10
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +30 -6
- package/src/verax/observe/console-sensor.js +2 -18
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +513 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +660 -273
- package/src/verax/observe/index.js +910 -26
- package/src/verax/observe/interaction-discovery.js +378 -15
- package/src/verax/observe/interaction-runner.js +562 -197
- package/src/verax/observe/loading-sensor.js +145 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +38 -17
- package/src/verax/observe/state-sensor.js +393 -0
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +73 -21
- package/src/verax/observe/ui-signal-sensor.js +143 -17
- package/src/verax/scan-summary-writer.js +80 -15
- package/src/verax/shared/artifact-manager.js +111 -9
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +169 -0
- package/src/verax/shared/dynamic-route-utils.js +224 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +9 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +66 -0
- package/src/verax/validate/context-validator.js +244 -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 } 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 (unused parameter, kept for API compatibility)
|
|
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 {Promise<any>} 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
|
+
}
|