@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.
- package/README.md +28 -20
- package/bin/verax.js +11 -18
- package/package.json +28 -7
- package/src/cli/commands/baseline.js +1 -2
- package/src/cli/commands/default.js +72 -81
- package/src/cli/commands/doctor.js +29 -0
- package/src/cli/commands/ga.js +3 -0
- package/src/cli/commands/gates.js +1 -1
- package/src/cli/commands/inspect.js +6 -133
- package/src/cli/commands/release-check.js +2 -0
- package/src/cli/commands/run.js +74 -246
- package/src/cli/commands/security-check.js +2 -1
- package/src/cli/commands/truth.js +0 -1
- package/src/cli/entry.js +82 -309
- package/src/cli/util/angular-component-extractor.js +2 -2
- package/src/cli/util/angular-navigation-detector.js +2 -2
- package/src/cli/util/ast-interactive-detector.js +4 -6
- package/src/cli/util/ast-network-detector.js +3 -3
- package/src/cli/util/ast-promise-extractor.js +581 -0
- package/src/cli/util/ast-usestate-detector.js +3 -3
- package/src/cli/util/atomic-write.js +12 -1
- package/src/cli/util/console-reporter.js +72 -0
- package/src/cli/util/detection-engine.js +105 -41
- package/src/cli/util/determinism-runner.js +2 -1
- package/src/cli/util/determinism-writer.js +1 -1
- package/src/cli/util/digest-engine.js +359 -0
- package/src/cli/util/dom-diff.js +226 -0
- package/src/cli/util/env-url.js +0 -4
- package/src/cli/util/evidence-engine.js +287 -0
- package/src/cli/util/expectation-extractor.js +217 -367
- package/src/cli/util/findings-writer.js +19 -126
- package/src/cli/util/framework-detector.js +572 -0
- package/src/cli/util/idgen.js +1 -1
- package/src/cli/util/interaction-planner.js +529 -0
- package/src/cli/util/learn-writer.js +2 -2
- package/src/cli/util/ledger-writer.js +110 -0
- package/src/cli/util/monorepo-resolver.js +162 -0
- package/src/cli/util/observation-engine.js +127 -278
- package/src/cli/util/observe-writer.js +2 -2
- package/src/cli/util/paths.js +12 -3
- package/src/cli/util/project-discovery.js +284 -3
- package/src/cli/util/project-writer.js +2 -2
- package/src/cli/util/run-id.js +23 -27
- package/src/cli/util/run-result.js +778 -0
- package/src/cli/util/selector-resolver.js +235 -0
- package/src/cli/util/summary-writer.js +2 -1
- package/src/cli/util/svelte-navigation-detector.js +3 -3
- package/src/cli/util/svelte-sfc-extractor.js +0 -1
- package/src/cli/util/svelte-state-detector.js +1 -2
- package/src/cli/util/trust-activation-integration.js +496 -0
- package/src/cli/util/trust-activation-wrapper.js +85 -0
- package/src/cli/util/trust-integration-hooks.js +164 -0
- package/src/cli/util/types.js +153 -0
- package/src/cli/util/url-validation.js +40 -0
- package/src/cli/util/vue-navigation-detector.js +4 -3
- package/src/cli/util/vue-sfc-extractor.js +1 -2
- package/src/cli/util/vue-state-detector.js +1 -1
- package/src/types/fs-augment.d.ts +23 -0
- package/src/types/global.d.ts +137 -0
- package/src/types/internal-types.d.ts +35 -0
- package/src/verax/cli/finding-explainer.js +3 -56
- package/src/verax/cli/init.js +4 -18
- package/src/verax/core/action-classifier.js +4 -3
- package/src/verax/core/artifacts/registry.js +0 -15
- package/src/verax/core/artifacts/verifier.js +18 -8
- package/src/verax/core/baseline/baseline.snapshot.js +2 -0
- package/src/verax/core/capabilities/gates.js +7 -1
- package/src/verax/core/confidence/confidence-compute.js +14 -7
- package/src/verax/core/confidence/confidence.loader.js +1 -0
- package/src/verax/core/confidence-engine-refactor.js +8 -3
- package/src/verax/core/confidence-engine.js +162 -23
- package/src/verax/core/contracts/types.js +1 -0
- package/src/verax/core/contracts/validators.js +79 -4
- package/src/verax/core/decision-snapshot.js +3 -30
- package/src/verax/core/decisions/decision.trace.js +2 -0
- package/src/verax/core/determinism/contract-writer.js +2 -2
- package/src/verax/core/determinism/contract.js +1 -1
- package/src/verax/core/determinism/diff.js +42 -1
- package/src/verax/core/determinism/engine.js +7 -6
- package/src/verax/core/determinism/finding-identity.js +3 -2
- package/src/verax/core/determinism/normalize.js +32 -4
- package/src/verax/core/determinism/report-writer.js +1 -0
- package/src/verax/core/determinism/run-fingerprint.js +7 -2
- package/src/verax/core/dynamic-route-intelligence.js +8 -7
- package/src/verax/core/evidence/evidence-capture-service.js +1 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +2 -1
- package/src/verax/core/evidence-builder.js +2 -2
- package/src/verax/core/execution-mode-context.js +1 -1
- package/src/verax/core/execution-mode-detector.js +5 -3
- package/src/verax/core/failures/exit-codes.js +39 -37
- package/src/verax/core/failures/failure-summary.js +1 -1
- package/src/verax/core/failures/failure.factory.js +3 -3
- package/src/verax/core/failures/failure.ledger.js +3 -2
- package/src/verax/core/ga/ga.artifact.js +1 -1
- package/src/verax/core/ga/ga.contract.js +3 -2
- package/src/verax/core/ga/ga.enforcer.js +1 -0
- package/src/verax/core/guardrails/policy.loader.js +1 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +1 -1
- package/src/verax/core/guardrails-engine.js +2 -2
- package/src/verax/core/incremental-store.js +1 -0
- package/src/verax/core/integrity/budget.js +138 -0
- package/src/verax/core/integrity/determinism.js +342 -0
- package/src/verax/core/integrity/integrity.js +208 -0
- package/src/verax/core/integrity/poisoning.js +108 -0
- package/src/verax/core/integrity/transaction.js +140 -0
- package/src/verax/core/observe/run-timeline.js +2 -0
- package/src/verax/core/perf/perf.report.js +2 -0
- package/src/verax/core/pipeline-tracker.js +5 -0
- package/src/verax/core/release/provenance.builder.js +73 -214
- package/src/verax/core/release/release.enforcer.js +14 -9
- package/src/verax/core/release/reproducibility.check.js +1 -0
- package/src/verax/core/release/sbom.builder.js +32 -23
- package/src/verax/core/replay-validator.js +2 -0
- package/src/verax/core/replay.js +4 -0
- package/src/verax/core/report/cross-index.js +6 -3
- package/src/verax/core/report/human-summary.js +141 -1
- package/src/verax/core/route-intelligence.js +4 -3
- package/src/verax/core/run-id.js +6 -3
- package/src/verax/core/run-manifest.js +4 -3
- package/src/verax/core/security/secrets.scan.js +10 -7
- package/src/verax/core/security/security.enforcer.js +4 -0
- package/src/verax/core/security/supplychain.policy.js +9 -1
- package/src/verax/core/security/vuln.scan.js +2 -2
- package/src/verax/core/truth/truth.certificate.js +3 -1
- package/src/verax/core/ui-feedback-intelligence.js +12 -46
- package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
- package/src/verax/detect/confidence-engine.js +100 -660
- package/src/verax/detect/confidence-helper.js +1 -0
- package/src/verax/detect/detection-engine.js +1 -18
- package/src/verax/detect/dynamic-route-findings.js +17 -14
- package/src/verax/detect/expectation-chain-detector.js +1 -1
- package/src/verax/detect/expectation-model.js +3 -5
- package/src/verax/detect/failure-cause-inference.js +293 -0
- package/src/verax/detect/findings-writer.js +126 -166
- package/src/verax/detect/flow-detector.js +2 -2
- package/src/verax/detect/form-silent-failure.js +98 -0
- package/src/verax/detect/index.js +51 -234
- package/src/verax/detect/invariants-enforcer.js +147 -0
- package/src/verax/detect/journey-stall-detector.js +4 -4
- package/src/verax/detect/navigation-silent-failure.js +82 -0
- package/src/verax/detect/problem-aggregator.js +361 -0
- package/src/verax/detect/route-findings.js +7 -6
- package/src/verax/detect/summary-writer.js +477 -0
- package/src/verax/detect/test-failure-cause-inference.js +314 -0
- package/src/verax/detect/ui-feedback-findings.js +18 -18
- package/src/verax/detect/verdict-engine.js +3 -57
- package/src/verax/detect/view-switch-correlator.js +2 -2
- package/src/verax/flow/flow-engine.js +2 -1
- package/src/verax/flow/flow-spec.js +0 -6
- package/src/verax/index.js +48 -412
- package/src/verax/intel/ts-program.js +1 -0
- package/src/verax/intel/vue-navigation-extractor.js +3 -0
- package/src/verax/learn/action-contract-extractor.js +67 -682
- package/src/verax/learn/ast-contract-extractor.js +1 -1
- package/src/verax/learn/flow-extractor.js +1 -0
- package/src/verax/learn/project-detector.js +5 -0
- package/src/verax/learn/react-router-extractor.js +2 -0
- package/src/verax/learn/route-validator.js +1 -4
- package/src/verax/learn/source-instrumenter.js +1 -0
- package/src/verax/learn/state-extractor.js +2 -1
- package/src/verax/learn/static-extractor.js +1 -0
- package/src/verax/observe/coverage-gaps.js +132 -0
- package/src/verax/observe/expectation-handler.js +126 -0
- package/src/verax/observe/incremental-skip.js +46 -0
- package/src/verax/observe/index.js +735 -84
- package/src/verax/observe/interaction-executor.js +192 -0
- package/src/verax/observe/interaction-runner.js +782 -530
- package/src/verax/observe/network-firewall.js +86 -0
- package/src/verax/observe/observation-builder.js +169 -0
- package/src/verax/observe/observe-context.js +1 -1
- package/src/verax/observe/observe-helpers.js +2 -1
- package/src/verax/observe/observe-runner.js +28 -24
- package/src/verax/observe/observers/budget-observer.js +3 -3
- package/src/verax/observe/observers/console-observer.js +4 -4
- package/src/verax/observe/observers/coverage-observer.js +4 -4
- package/src/verax/observe/observers/interaction-observer.js +3 -3
- package/src/verax/observe/observers/navigation-observer.js +4 -4
- package/src/verax/observe/observers/network-observer.js +4 -4
- package/src/verax/observe/observers/safety-observer.js +1 -1
- package/src/verax/observe/observers/ui-feedback-observer.js +4 -4
- package/src/verax/observe/page-traversal.js +138 -0
- package/src/verax/observe/snapshot-ops.js +94 -0
- package/src/verax/observe/ui-signal-sensor.js +2 -148
- package/src/verax/scan-summary-writer.js +10 -42
- package/src/verax/shared/artifact-manager.js +30 -13
- package/src/verax/shared/caching.js +1 -0
- package/src/verax/shared/expectation-tracker.js +1 -0
- package/src/verax/shared/zip-artifacts.js +6 -0
- package/src/verax/core/confidence-engine.js.backup +0 -471
- package/src/verax/shared/config-loader.js +0 -169
- /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
|
-
|
|
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
|
-
|
|
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
|
|
167
|
-
if (!attempted
|
|
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)
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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
|
-
*
|
|
210
|
-
*
|
|
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
|
|
215
|
-
if (
|
|
216
|
-
|
|
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
|
|
256
|
+
const lower = reason.toLowerCase();
|
|
219
257
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
+
}
|