@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
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 6A: Semantic Determinism Comparison
|
|
3
|
+
*
|
|
4
|
+
* Provides deep semantic comparison of runs with field normalization.
|
|
5
|
+
* Ignores non-deterministic fields (timestamps, IDs, paths).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { join, normalize } from 'path';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Normalize non-deterministic fields for comparison
|
|
14
|
+
*
|
|
15
|
+
* @param {any} obj - Object to normalize
|
|
16
|
+
* @param {string} basePath - Base path for path normalization
|
|
17
|
+
* @returns {any} Normalized object
|
|
18
|
+
*/
|
|
19
|
+
function normalizeObject(obj, basePath = '') {
|
|
20
|
+
if (obj === null || obj === undefined) {
|
|
21
|
+
return obj;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (Array.isArray(obj)) {
|
|
25
|
+
return obj.map(item => normalizeObject(item, basePath));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof obj !== 'object') {
|
|
29
|
+
return obj;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const normalized = {};
|
|
33
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
34
|
+
|
|
35
|
+
for (const key of sortedKeys) {
|
|
36
|
+
const value = obj[key];
|
|
37
|
+
|
|
38
|
+
// Skip non-deterministic fields
|
|
39
|
+
if (isNonDeterministicField(key)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Normalize paths
|
|
44
|
+
if (isPathField(key) && typeof value === 'string') {
|
|
45
|
+
normalized[key] = normalizePath(value, basePath);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Recursively normalize
|
|
50
|
+
normalized[key] = normalizeObject(value, basePath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if field name indicates non-deterministic data
|
|
58
|
+
*
|
|
59
|
+
* @param {string} fieldName - Field name
|
|
60
|
+
* @returns {boolean} True if non-deterministic
|
|
61
|
+
*/
|
|
62
|
+
function isNonDeterministicField(fieldName) {
|
|
63
|
+
const nonDeterministicFields = [
|
|
64
|
+
'timestamp',
|
|
65
|
+
'createdAt',
|
|
66
|
+
'updatedAt',
|
|
67
|
+
'startedAt',
|
|
68
|
+
'completedAt',
|
|
69
|
+
'observedAt',
|
|
70
|
+
'detectedAt',
|
|
71
|
+
'generatedAt',
|
|
72
|
+
'verifiedAt',
|
|
73
|
+
'writtenAt',
|
|
74
|
+
'learnedAt',
|
|
75
|
+
'failedAt',
|
|
76
|
+
'runId',
|
|
77
|
+
'pid',
|
|
78
|
+
'duration',
|
|
79
|
+
'durationMs',
|
|
80
|
+
'totalMs',
|
|
81
|
+
'learnMs',
|
|
82
|
+
'observeMs',
|
|
83
|
+
'detectMs',
|
|
84
|
+
'relativeTime',
|
|
85
|
+
'sequence',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
return nonDeterministicFields.includes(fieldName) ||
|
|
89
|
+
fieldName.endsWith('At') ||
|
|
90
|
+
fieldName.endsWith('Time') ||
|
|
91
|
+
fieldName.endsWith('Ms') ||
|
|
92
|
+
fieldName.includes('timestamp') ||
|
|
93
|
+
fieldName.includes('Timestamp');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if field name indicates path data
|
|
98
|
+
*
|
|
99
|
+
* @param {string} fieldName - Field name
|
|
100
|
+
* @returns {boolean} True if path field
|
|
101
|
+
*/
|
|
102
|
+
function isPathField(fieldName) {
|
|
103
|
+
return fieldName.endsWith('Path') ||
|
|
104
|
+
fieldName.endsWith('Dir') ||
|
|
105
|
+
fieldName === 'cwd' ||
|
|
106
|
+
fieldName === 'src';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Normalize path for comparison (make relative to base)
|
|
111
|
+
*
|
|
112
|
+
* @param {string} path - Path to normalize
|
|
113
|
+
* @param {string} basePath - Base path
|
|
114
|
+
* @returns {string} Normalized path
|
|
115
|
+
*/
|
|
116
|
+
function normalizePath(path, basePath) {
|
|
117
|
+
if (!path || !basePath) {
|
|
118
|
+
return path;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Normalize separators
|
|
122
|
+
const normalizedPath = normalize(path).replace(/\\/g, '/');
|
|
123
|
+
const normalizedBase = normalize(basePath).replace(/\\/g, '/');
|
|
124
|
+
|
|
125
|
+
// Make relative if starts with base
|
|
126
|
+
if (normalizedPath.startsWith(normalizedBase)) {
|
|
127
|
+
return normalizedPath.substring(normalizedBase.length).replace(/^\//, '');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return normalizedPath;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Compute semantic hash of normalized object
|
|
135
|
+
*
|
|
136
|
+
* @param {Object} obj - Object to hash
|
|
137
|
+
* @returns {string} Semantic hash
|
|
138
|
+
*/
|
|
139
|
+
function computeSemanticHash(obj) {
|
|
140
|
+
// Use JSON.stringify with replacer that sorts all object keys recursively
|
|
141
|
+
const sortedReplacer = (key, value) => {
|
|
142
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
143
|
+
return Object.keys(value).sort().reduce((sorted, key) => {
|
|
144
|
+
sorted[key] = value[key];
|
|
145
|
+
return sorted;
|
|
146
|
+
}, {});
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const normalized = JSON.stringify(obj, sortedReplacer);
|
|
152
|
+
// @ts-expect-error - digest returns string
|
|
153
|
+
return createHash('sha256').update(normalized).digest('hex');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Compare two run summaries semantically
|
|
158
|
+
*
|
|
159
|
+
* @param {Object} summary1 - First run summary
|
|
160
|
+
* @param {Object} summary2 - Second run summary
|
|
161
|
+
* @param {string} basePath - Base path for normalization
|
|
162
|
+
* @returns {{ identical: boolean, differences: Object[] }} Comparison result
|
|
163
|
+
*/
|
|
164
|
+
export function compareRunsSemantically(summary1, summary2, basePath = '') {
|
|
165
|
+
const norm1 = normalizeObject(summary1, basePath);
|
|
166
|
+
const norm2 = normalizeObject(summary2, basePath);
|
|
167
|
+
|
|
168
|
+
const hash1 = computeSemanticHash(norm1);
|
|
169
|
+
const hash2 = computeSemanticHash(norm2);
|
|
170
|
+
|
|
171
|
+
if (hash1 === hash2) {
|
|
172
|
+
return { identical: true, differences: [] };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Find differences
|
|
176
|
+
const differences = findDifferences(norm1, norm2, '');
|
|
177
|
+
|
|
178
|
+
return { identical: false, differences };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Find differences between two normalized objects
|
|
183
|
+
*
|
|
184
|
+
* @param {any} obj1 - First object
|
|
185
|
+
* @param {any} obj2 - Second object
|
|
186
|
+
* @param {string} path - Current path in object tree
|
|
187
|
+
* @returns {Object[]} List of differences
|
|
188
|
+
*/
|
|
189
|
+
function findDifferences(obj1, obj2, path) {
|
|
190
|
+
const differences = [];
|
|
191
|
+
|
|
192
|
+
if (typeof obj1 !== typeof obj2) {
|
|
193
|
+
differences.push({
|
|
194
|
+
path,
|
|
195
|
+
type: 'type-mismatch',
|
|
196
|
+
value1: typeof obj1,
|
|
197
|
+
value2: typeof obj2,
|
|
198
|
+
});
|
|
199
|
+
return differences;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (obj1 === null || obj2 === null) {
|
|
203
|
+
if (obj1 !== obj2) {
|
|
204
|
+
differences.push({
|
|
205
|
+
path,
|
|
206
|
+
type: 'null-mismatch',
|
|
207
|
+
value1: obj1,
|
|
208
|
+
value2: obj2,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return differences;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (typeof obj1 !== 'object') {
|
|
215
|
+
if (obj1 !== obj2) {
|
|
216
|
+
differences.push({
|
|
217
|
+
path,
|
|
218
|
+
type: 'value-mismatch',
|
|
219
|
+
value1: obj1,
|
|
220
|
+
value2: obj2,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return differences;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (Array.isArray(obj1) !== Array.isArray(obj2)) {
|
|
227
|
+
differences.push({
|
|
228
|
+
path,
|
|
229
|
+
type: 'array-mismatch',
|
|
230
|
+
value1: Array.isArray(obj1),
|
|
231
|
+
value2: Array.isArray(obj2),
|
|
232
|
+
});
|
|
233
|
+
return differences;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (Array.isArray(obj1)) {
|
|
237
|
+
if (obj1.length !== obj2.length) {
|
|
238
|
+
differences.push({
|
|
239
|
+
path,
|
|
240
|
+
type: 'length-mismatch',
|
|
241
|
+
value1: obj1.length,
|
|
242
|
+
value2: obj2.length,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const maxLen = Math.max(obj1.length, obj2.length);
|
|
247
|
+
for (let i = 0; i < maxLen; i++) {
|
|
248
|
+
differences.push(...findDifferences(
|
|
249
|
+
obj1[i],
|
|
250
|
+
obj2[i],
|
|
251
|
+
`${path}[${i}]`
|
|
252
|
+
));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return differences;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Object comparison
|
|
259
|
+
const keys1 = Object.keys(obj1).sort();
|
|
260
|
+
const keys2 = Object.keys(obj2).sort();
|
|
261
|
+
|
|
262
|
+
const allKeys = new Set([...keys1, ...keys2]);
|
|
263
|
+
|
|
264
|
+
for (const key of allKeys) {
|
|
265
|
+
const subPath = path ? `${path}.${key}` : key;
|
|
266
|
+
|
|
267
|
+
if (!(key in obj1)) {
|
|
268
|
+
differences.push({
|
|
269
|
+
path: subPath,
|
|
270
|
+
type: 'missing-in-first',
|
|
271
|
+
value2: obj2[key],
|
|
272
|
+
});
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!(key in obj2)) {
|
|
277
|
+
differences.push({
|
|
278
|
+
path: subPath,
|
|
279
|
+
type: 'missing-in-second',
|
|
280
|
+
value1: obj1[key],
|
|
281
|
+
});
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
differences.push(...findDifferences(obj1[key], obj2[key], subPath));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return differences;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Load and compare two runs
|
|
293
|
+
*
|
|
294
|
+
* @param {string} runDir1 - First run directory
|
|
295
|
+
* @param {string} runDir2 - Second run directory
|
|
296
|
+
* @param {string} basePath - Base path for normalization
|
|
297
|
+
* @returns {{ ok: boolean, identical?: boolean, differences?: Object[], error?: string }} Result
|
|
298
|
+
*/
|
|
299
|
+
export function loadAndCompareRuns(runDir1, runDir2, basePath) {
|
|
300
|
+
try {
|
|
301
|
+
const summary1Path = join(runDir1, 'summary.json');
|
|
302
|
+
const summary2Path = join(runDir2, 'summary.json');
|
|
303
|
+
|
|
304
|
+
const content1 = readFileSync(summary1Path, 'utf8');
|
|
305
|
+
const content2 = readFileSync(summary2Path, 'utf8');
|
|
306
|
+
|
|
307
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
308
|
+
const summary1 = JSON.parse(content1);
|
|
309
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
310
|
+
const summary2 = JSON.parse(content2);
|
|
311
|
+
|
|
312
|
+
const comparison = compareRunsSemantically(summary1, summary2, basePath);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
ok: true,
|
|
316
|
+
identical: comparison.identical,
|
|
317
|
+
differences: comparison.differences,
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
return {
|
|
321
|
+
ok: false,
|
|
322
|
+
error: error.message,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Normalize findings for semantic comparison
|
|
329
|
+
*
|
|
330
|
+
* @param {Object[]} findings - Findings array
|
|
331
|
+
* @returns {Object[]} Normalized findings
|
|
332
|
+
*/
|
|
333
|
+
export function normalizeFindingsForComparison(findings) {
|
|
334
|
+
return findings
|
|
335
|
+
.map(f => normalizeObject(f, ''))
|
|
336
|
+
.sort((a, b) => {
|
|
337
|
+
// Sort by expectationId for stable comparison
|
|
338
|
+
const idA = a.expectationId || '';
|
|
339
|
+
const idB = b.expectationId || '';
|
|
340
|
+
return idA.localeCompare(idB);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 6A: Cryptographic Integrity System
|
|
3
|
+
*
|
|
4
|
+
* Provides SHA256-based integrity verification for all run artifacts.
|
|
5
|
+
* Ensures tamper detection and corruption protection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
import { readFileSync, statSync as _statSync, readdirSync } from 'fs';
|
|
10
|
+
import { join, basename as _basename } from 'path';
|
|
11
|
+
import { atomicWriteJson } from '../../../cli/util/atomic-write.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compute SHA256 hash of file contents
|
|
15
|
+
*
|
|
16
|
+
* @param {string} filePath - Absolute path to file
|
|
17
|
+
* @returns {{ hash: string, size: number, error?: string }} Hash result
|
|
18
|
+
*/
|
|
19
|
+
export function computeFileIntegrity(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
const content = readFileSync(filePath);
|
|
22
|
+
const hash = createHash('sha256').update(content).digest('hex');
|
|
23
|
+
const size = content.length;
|
|
24
|
+
// @ts-expect-error - digest returns string
|
|
25
|
+
return { hash, size };
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return { hash: null, size: 0, error: error.message };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate integrity manifest for all artifacts in run directory
|
|
33
|
+
*
|
|
34
|
+
* @param {string} runDir - Run directory path
|
|
35
|
+
* @param {string[]} artifactNames - List of artifact filenames to include
|
|
36
|
+
* @returns {{ manifest: Object, errors: string[] }} Manifest and any errors
|
|
37
|
+
*/
|
|
38
|
+
export function generateIntegrityManifest(runDir, artifactNames) {
|
|
39
|
+
const manifest = {
|
|
40
|
+
version: 1,
|
|
41
|
+
generatedAt: new Date().toISOString(),
|
|
42
|
+
runDir,
|
|
43
|
+
artifacts: {},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const errors = [];
|
|
47
|
+
|
|
48
|
+
for (const name of artifactNames) {
|
|
49
|
+
const filePath = join(runDir, name);
|
|
50
|
+
const integrity = computeFileIntegrity(filePath);
|
|
51
|
+
|
|
52
|
+
if (integrity.error) {
|
|
53
|
+
errors.push(`Failed to hash ${name}: ${integrity.error}`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
manifest.artifacts[name] = {
|
|
58
|
+
sha256: integrity.hash,
|
|
59
|
+
size: integrity.size,
|
|
60
|
+
verifiedAt: null, // Set when verified
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { manifest, errors };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Write integrity manifest to run directory
|
|
69
|
+
*
|
|
70
|
+
* @param {string} runDir - Run directory path
|
|
71
|
+
* @param {Object} manifest - Integrity manifest object
|
|
72
|
+
* @returns {{ ok: boolean, path?: string, error?: Error }} Write result
|
|
73
|
+
*/
|
|
74
|
+
export function writeIntegrityManifest(runDir, manifest) {
|
|
75
|
+
const manifestPath = join(runDir, 'integrity.manifest.json');
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
atomicWriteJson(manifestPath, manifest);
|
|
79
|
+
return { ok: true, path: manifestPath };
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return { ok: false, error };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Verify artifact integrity against manifest
|
|
87
|
+
*
|
|
88
|
+
* @param {string} runDir - Run directory path
|
|
89
|
+
* @param {string} artifactName - Artifact filename
|
|
90
|
+
* @param {Object} manifest - Integrity manifest
|
|
91
|
+
* @returns {{ ok: boolean, error?: string, expectedHash?: string, actualHash?: string }} Verification result
|
|
92
|
+
*/
|
|
93
|
+
export function verifyArtifactIntegrity(runDir, artifactName, manifest) {
|
|
94
|
+
const artifactPath = join(runDir, artifactName);
|
|
95
|
+
|
|
96
|
+
// Check if artifact exists in manifest
|
|
97
|
+
if (!manifest.artifacts[artifactName]) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
error: `Artifact ${artifactName} not found in integrity manifest`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const expected = manifest.artifacts[artifactName];
|
|
105
|
+
const integrity = computeFileIntegrity(artifactPath);
|
|
106
|
+
|
|
107
|
+
if (integrity.error) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
error: `Failed to read artifact ${artifactName}: ${integrity.error}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (integrity.hash !== expected.sha256) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: `Integrity violation: ${artifactName} hash mismatch`,
|
|
118
|
+
expectedHash: expected.sha256,
|
|
119
|
+
actualHash: integrity.hash,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (integrity.size !== expected.size) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
error: `Integrity violation: ${artifactName} size mismatch (expected ${expected.size}, got ${integrity.size})`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { ok: true };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Load and verify integrity manifest
|
|
135
|
+
*
|
|
136
|
+
* @param {string} runDir - Run directory path
|
|
137
|
+
* @returns {{ ok: boolean, manifest?: Object, error?: string }} Load result
|
|
138
|
+
*/
|
|
139
|
+
export function loadIntegrityManifest(runDir) {
|
|
140
|
+
try {
|
|
141
|
+
const manifestPath = join(runDir, 'integrity.manifest.json');
|
|
142
|
+
const content = readFileSync(manifestPath, 'utf8');
|
|
143
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
144
|
+
const manifest = JSON.parse(content);
|
|
145
|
+
|
|
146
|
+
if (!manifest.version || !manifest.artifacts) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
error: 'Invalid integrity manifest format',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { ok: true, manifest };
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
error: `Failed to load integrity manifest: ${error.message}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Verify all artifacts in manifest
|
|
164
|
+
*
|
|
165
|
+
* @param {string} runDir - Run directory path
|
|
166
|
+
* @param {Object} manifest - Integrity manifest
|
|
167
|
+
* @returns {{ ok: boolean, verified: string[], failed: Array<{name: string, error: string}> }} Verification results
|
|
168
|
+
*/
|
|
169
|
+
export function verifyAllArtifacts(runDir, manifest) {
|
|
170
|
+
const verified = [];
|
|
171
|
+
const failed = [];
|
|
172
|
+
|
|
173
|
+
for (const artifactName of Object.keys(manifest.artifacts)) {
|
|
174
|
+
const result = verifyArtifactIntegrity(runDir, artifactName, manifest);
|
|
175
|
+
|
|
176
|
+
if (result.ok) {
|
|
177
|
+
verified.push(artifactName);
|
|
178
|
+
} else {
|
|
179
|
+
failed.push({
|
|
180
|
+
name: artifactName,
|
|
181
|
+
error: result.error,
|
|
182
|
+
expectedHash: result.expectedHash,
|
|
183
|
+
actualHash: result.actualHash,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
ok: failed.length === 0,
|
|
190
|
+
verified,
|
|
191
|
+
failed,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Discover all JSON artifacts in run directory
|
|
197
|
+
*
|
|
198
|
+
* @param {string} runDir - Run directory path
|
|
199
|
+
* @returns {string[]} List of artifact filenames
|
|
200
|
+
*/
|
|
201
|
+
export function discoverArtifacts(runDir) {
|
|
202
|
+
try {
|
|
203
|
+
const files = readdirSync(runDir);
|
|
204
|
+
return files.filter(f => f.endsWith('.json') && f !== 'integrity.manifest.json');
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 6A: Run Poisoning System
|
|
3
|
+
*
|
|
4
|
+
* Prevents consumption of incomplete or failed runs.
|
|
5
|
+
* Implements .INCOMPLETE marker for run safety.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create poisoning marker for run
|
|
13
|
+
*
|
|
14
|
+
* @param {string} runDir - Run directory path
|
|
15
|
+
* @param {string} runId - Run identifier
|
|
16
|
+
* @returns {{ ok: boolean, path?: string, error?: Error }} Result
|
|
17
|
+
*/
|
|
18
|
+
export function createPoisonMarker(runDir, runId) {
|
|
19
|
+
try {
|
|
20
|
+
const markerPath = join(runDir, '.INCOMPLETE');
|
|
21
|
+
const marker = {
|
|
22
|
+
runId,
|
|
23
|
+
createdAt: new Date().toISOString(),
|
|
24
|
+
pid: process.pid,
|
|
25
|
+
reason: 'Run in progress',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
writeFileSync(markerPath, JSON.stringify(marker, null, 2), 'utf8');
|
|
29
|
+
|
|
30
|
+
return { ok: true, path: markerPath };
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return { ok: false, error };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Remove poisoning marker (only on successful completion)
|
|
38
|
+
*
|
|
39
|
+
* @param {string} runDir - Run directory path
|
|
40
|
+
* @returns {{ ok: boolean, error?: Error }} Result
|
|
41
|
+
*/
|
|
42
|
+
export function removePoisonMarker(runDir) {
|
|
43
|
+
try {
|
|
44
|
+
const markerPath = join(runDir, '.INCOMPLETE');
|
|
45
|
+
|
|
46
|
+
if (existsSync(markerPath)) {
|
|
47
|
+
unlinkSync(markerPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { ok: true };
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return { ok: false, error };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if run is poisoned (incomplete)
|
|
58
|
+
*
|
|
59
|
+
* @param {string} runDir - Run directory path
|
|
60
|
+
* @returns {{ poisoned: boolean, marker?: Object, reason?: string }} Poisoning status
|
|
61
|
+
*/
|
|
62
|
+
export function checkPoisonMarker(runDir) {
|
|
63
|
+
try {
|
|
64
|
+
const markerPath = join(runDir, '.INCOMPLETE');
|
|
65
|
+
|
|
66
|
+
if (!existsSync(markerPath)) {
|
|
67
|
+
return { poisoned: false };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const content = readFileSync(markerPath, 'utf8');
|
|
71
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
72
|
+
const marker = JSON.parse(content);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
poisoned: true,
|
|
76
|
+
marker,
|
|
77
|
+
reason: marker.reason || 'Run incomplete',
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
// If marker exists but is unreadable, consider it poisoned
|
|
81
|
+
const markerPath = join(runDir, '.INCOMPLETE');
|
|
82
|
+
if (existsSync(markerPath)) {
|
|
83
|
+
return {
|
|
84
|
+
poisoned: true,
|
|
85
|
+
reason: 'Marker unreadable or corrupted',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { poisoned: false };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Enforce poisoning check - throw if run is poisoned
|
|
95
|
+
*
|
|
96
|
+
* @param {string} runDir - Run directory path
|
|
97
|
+
* @throws {Error} If run is poisoned
|
|
98
|
+
*/
|
|
99
|
+
export function enforcePoisonCheck(runDir) {
|
|
100
|
+
const status = checkPoisonMarker(runDir);
|
|
101
|
+
|
|
102
|
+
if (status.poisoned) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Cannot read run: RUN_POISONED (${status.reason || 'incomplete'}). ` +
|
|
105
|
+
`Run directory: ${runDir}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|