@veraxhq/verax 0.2.1 → 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 +10 -6
- package/bin/verax.js +11 -11
- package/package.json +29 -8
- package/src/cli/commands/baseline.js +103 -0
- package/src/cli/commands/default.js +51 -6
- package/src/cli/commands/doctor.js +29 -0
- package/src/cli/commands/ga.js +246 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +4 -2
- package/src/cli/commands/release-check.js +215 -0
- package/src/cli/commands/run.js +45 -6
- package/src/cli/commands/security-check.js +212 -0
- package/src/cli/commands/truth.js +113 -0
- package/src/cli/entry.js +30 -20
- package/src/cli/util/angular-component-extractor.js +179 -0
- package/src/cli/util/angular-navigation-detector.js +141 -0
- package/src/cli/util/angular-network-detector.js +161 -0
- package/src/cli/util/angular-state-detector.js +162 -0
- package/src/cli/util/ast-interactive-detector.js +544 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-promise-extractor.js +581 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/atomic-write.js +12 -1
- package/src/cli/util/bootstrap-guard.js +86 -0
- 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 +124 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/digest-engine.js +359 -0
- package/src/cli/util/dom-diff.js +226 -0
- package/src/cli/util/evidence-engine.js +287 -0
- package/src/cli/util/expectation-extractor.js +151 -5
- package/src/cli/util/findings-writer.js +3 -0
- 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 -0
- 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 -0
- package/src/cli/util/project-discovery.js +284 -0
- package/src/cli/util/project-writer.js +2 -0
- package/src/cli/util/run-id.js +23 -27
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/run-result.js +778 -0
- package/src/cli/util/selector-resolver.js +235 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +2 -0
- package/src/cli/util/svelte-navigation-detector.js +163 -0
- package/src/cli/util/svelte-network-detector.js +80 -0
- package/src/cli/util/svelte-sfc-extractor.js +146 -0
- package/src/cli/util/svelte-state-detector.js +242 -0
- 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 +178 -0
- package/src/cli/util/vue-sfc-extractor.js +161 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- 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/init.js +4 -18
- package/src/verax/core/action-classifier.js +4 -3
- package/src/verax/core/artifacts/registry.js +139 -0
- package/src/verax/core/artifacts/verifier.js +990 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +233 -0
- package/src/verax/core/capabilities/gates.js +505 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +144 -0
- package/src/verax/core/confidence/confidence-invariants.js +234 -0
- package/src/verax/core/confidence/confidence-report-writer.js +112 -0
- package/src/verax/core/confidence/confidence-weights.js +44 -0
- package/src/verax/core/confidence/confidence.defaults.js +65 -0
- package/src/verax/core/confidence/confidence.loader.js +80 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +489 -0
- package/src/verax/core/confidence-engine.js +625 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +186 -0
- package/src/verax/core/contracts/validators.js +456 -0
- package/src/verax/core/decisions/decision.trace.js +278 -0
- package/src/verax/core/determinism/contract-writer.js +89 -0
- package/src/verax/core/determinism/contract.js +139 -0
- package/src/verax/core/determinism/diff.js +405 -0
- package/src/verax/core/determinism/engine.js +222 -0
- package/src/verax/core/determinism/finding-identity.js +149 -0
- package/src/verax/core/determinism/normalize.js +466 -0
- package/src/verax/core/determinism/report-writer.js +93 -0
- package/src/verax/core/determinism/run-fingerprint.js +123 -0
- package/src/verax/core/dynamic-route-intelligence.js +529 -0
- package/src/verax/core/evidence/evidence-capture-service.js +308 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +166 -0
- package/src/verax/core/evidence-builder.js +487 -0
- package/src/verax/core/execution-mode-context.js +77 -0
- package/src/verax/core/execution-mode-detector.js +192 -0
- package/src/verax/core/failures/exit-codes.js +88 -0
- package/src/verax/core/failures/failure-summary.js +76 -0
- package/src/verax/core/failures/failure.factory.js +225 -0
- package/src/verax/core/failures/failure.ledger.js +133 -0
- package/src/verax/core/failures/failure.types.js +196 -0
- package/src/verax/core/failures/index.js +10 -0
- package/src/verax/core/ga/ga-report-writer.js +43 -0
- package/src/verax/core/ga/ga.artifact.js +49 -0
- package/src/verax/core/ga/ga.contract.js +435 -0
- package/src/verax/core/ga/ga.enforcer.js +87 -0
- package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
- package/src/verax/core/guardrails/policy.defaults.js +210 -0
- package/src/verax/core/guardrails/policy.loader.js +84 -0
- package/src/verax/core/guardrails/policy.schema.js +110 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
- package/src/verax/core/guardrails-engine.js +505 -0
- 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 +318 -0
- package/src/verax/core/perf/perf.contract.js +186 -0
- package/src/verax/core/perf/perf.display.js +65 -0
- package/src/verax/core/perf/perf.enforcer.js +91 -0
- package/src/verax/core/perf/perf.monitor.js +209 -0
- package/src/verax/core/perf/perf.report.js +200 -0
- package/src/verax/core/pipeline-tracker.js +243 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +130 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +164 -0
- package/src/verax/core/release/reproducibility.check.js +222 -0
- package/src/verax/core/release/sbom.builder.js +292 -0
- 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 +195 -0
- package/src/verax/core/report/human-summary.js +362 -0
- package/src/verax/core/route-intelligence.js +420 -0
- 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 +329 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +128 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +334 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/truth/truth.certificate.js +252 -0
- package/src/verax/core/ui-feedback-intelligence.js +481 -0
- package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
- package/src/verax/detect/confidence-engine.js +62 -34
- package/src/verax/detect/confidence-helper.js +34 -0
- package/src/verax/detect/dynamic-route-findings.js +338 -0
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +2 -2
- package/src/verax/detect/failure-cause-inference.js +293 -0
- package/src/verax/detect/findings-writer.js +131 -35
- 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 +46 -5
- package/src/verax/detect/invariants-enforcer.js +147 -0
- package/src/verax/detect/journey-stall-detector.js +558 -0
- 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 +219 -0
- 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 +207 -0
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/flow/flow-engine.js +2 -1
- package/src/verax/flow/flow-spec.js +0 -6
- package/src/verax/index.js +4 -0
- 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 +3 -0
- 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/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 +51 -155
- package/src/verax/observe/interaction-executor.js +192 -0
- package/src/verax/observe/interaction-runner.js +782 -513
- 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 +205 -0
- package/src/verax/observe/observe-helpers.js +192 -0
- package/src/verax/observe/observe-runner.js +230 -0
- package/src/verax/observe/observers/budget-observer.js +185 -0
- package/src/verax/observe/observers/console-observer.js +102 -0
- package/src/verax/observe/observers/coverage-observer.js +107 -0
- package/src/verax/observe/observers/interaction-observer.js +471 -0
- package/src/verax/observe/observers/navigation-observer.js +132 -0
- package/src/verax/observe/observers/network-observer.js +87 -0
- package/src/verax/observe/observers/safety-observer.js +82 -0
- package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
- package/src/verax/observe/page-traversal.js +138 -0
- package/src/verax/observe/snapshot-ops.js +94 -0
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/scan-summary-writer.js +2 -0
- package/src/verax/shared/artifact-manager.js +25 -5
- package/src/verax/shared/caching.js +1 -0
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/expectation-tracker.js +1 -0
- package/src/verax/shared/view-switch-rules.js +208 -0
- package/src/verax/shared/zip-artifacts.js +6 -0
- package/src/verax/shared/config-loader.js +0 -169
- /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM Diff Engine
|
|
3
|
+
* Computes structured differences between HTML snapshots
|
|
4
|
+
* Distinguishes meaningful changes from noise
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compute a diff between two HTML documents
|
|
9
|
+
* Returns a summary of changes without full tree comparison (for performance)
|
|
10
|
+
* Includes isMeaningful flag to distinguish signal from noise
|
|
11
|
+
*/
|
|
12
|
+
export function computeDOMDiff(htmlBefore, htmlAfter) {
|
|
13
|
+
const _before = parseHTML(htmlBefore);
|
|
14
|
+
const _after = parseHTML(htmlAfter);
|
|
15
|
+
|
|
16
|
+
const diff = {
|
|
17
|
+
htmlLengthBefore: htmlBefore.length,
|
|
18
|
+
htmlLengthAfter: htmlAfter.length,
|
|
19
|
+
changed: htmlBefore !== htmlAfter,
|
|
20
|
+
isMeaningful: false,
|
|
21
|
+
elementsRemoved: [],
|
|
22
|
+
elementsAdded: [],
|
|
23
|
+
attributesChanged: [],
|
|
24
|
+
contentChanged: [],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (!diff.changed) {
|
|
28
|
+
return diff;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check if this is only noise (timestamps, random ids, tracking)
|
|
32
|
+
if (isNoisyChangeOnly(htmlBefore, htmlAfter)) {
|
|
33
|
+
diff.changed = true;
|
|
34
|
+
diff.isMeaningful = false;
|
|
35
|
+
return diff;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Quick heuristics for specific changes
|
|
39
|
+
// Check for new elements with specific roles/classes that indicate feedback
|
|
40
|
+
const feedbackPatterns = [
|
|
41
|
+
'role="alert"',
|
|
42
|
+
'role="status"',
|
|
43
|
+
'aria-live',
|
|
44
|
+
'class="toast"',
|
|
45
|
+
'class="error"',
|
|
46
|
+
'class="success"',
|
|
47
|
+
'class="modal"',
|
|
48
|
+
'class="dialog"',
|
|
49
|
+
'[data-error]',
|
|
50
|
+
'[data-success]',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
for (const pattern of feedbackPatterns) {
|
|
54
|
+
if (!htmlBefore.includes(pattern) && htmlAfter.includes(pattern)) {
|
|
55
|
+
diff.elementsAdded.push(pattern);
|
|
56
|
+
diff.isMeaningful = true;
|
|
57
|
+
}
|
|
58
|
+
if (htmlBefore.includes(pattern) && !htmlAfter.includes(pattern)) {
|
|
59
|
+
diff.elementsRemoved.push(pattern);
|
|
60
|
+
diff.isMeaningful = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check for attribute changes (disabled, aria-invalid, etc.)
|
|
65
|
+
const attrPatterns = [
|
|
66
|
+
'disabled',
|
|
67
|
+
'aria-invalid',
|
|
68
|
+
'aria-disabled',
|
|
69
|
+
'data-loading',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
for (const attr of attrPatterns) {
|
|
73
|
+
const beforeCount = countOccurrences(htmlBefore, attr);
|
|
74
|
+
const afterCount = countOccurrences(htmlAfter, attr);
|
|
75
|
+
|
|
76
|
+
if (beforeCount !== afterCount) {
|
|
77
|
+
diff.attributesChanged.push({
|
|
78
|
+
attribute: attr,
|
|
79
|
+
before: beforeCount,
|
|
80
|
+
after: afterCount,
|
|
81
|
+
});
|
|
82
|
+
diff.isMeaningful = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check for form state changes (values, structure)
|
|
87
|
+
if (checkFormStateChange(htmlBefore, htmlAfter)) {
|
|
88
|
+
diff.isMeaningful = true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return diff;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parse HTML and return basic structure info
|
|
96
|
+
*/
|
|
97
|
+
function parseHTML(html) {
|
|
98
|
+
return {
|
|
99
|
+
bodyLength: html.length,
|
|
100
|
+
hasHead: html.includes('<head'),
|
|
101
|
+
hasBody: html.includes('<body'),
|
|
102
|
+
formCount: countOccurrences(html, '<form'),
|
|
103
|
+
inputCount: countOccurrences(html, '<input'),
|
|
104
|
+
buttonCount: countOccurrences(html, '<button'),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Count occurrences of a substring
|
|
110
|
+
*/
|
|
111
|
+
function countOccurrences(str, substr) {
|
|
112
|
+
let count = 0;
|
|
113
|
+
let pos = 0;
|
|
114
|
+
while ((pos = str.indexOf(substr, pos)) !== -1) {
|
|
115
|
+
count++;
|
|
116
|
+
pos += substr.length;
|
|
117
|
+
}
|
|
118
|
+
return count;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Detect if DOM appears to have feedback elements
|
|
123
|
+
*/
|
|
124
|
+
export function hasFeedbackElements(html) {
|
|
125
|
+
const feedbackIndicators = [
|
|
126
|
+
'role="alert"',
|
|
127
|
+
'role="status"',
|
|
128
|
+
'aria-live="polite"',
|
|
129
|
+
'aria-live="assertive"',
|
|
130
|
+
'toast',
|
|
131
|
+
'error',
|
|
132
|
+
'success',
|
|
133
|
+
'validation',
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
return feedbackIndicators.some(indicator =>
|
|
137
|
+
html.includes(indicator)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Detect if DOM appears to have validation errors
|
|
143
|
+
*/
|
|
144
|
+
export function hasValidationErrors(html) {
|
|
145
|
+
const errorPatterns = [
|
|
146
|
+
'aria-invalid="true"',
|
|
147
|
+
'aria-invalid=\'true\'',
|
|
148
|
+
'invalid',
|
|
149
|
+
'error',
|
|
150
|
+
'required',
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
return errorPatterns.some(pattern => html.includes(pattern));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if the HTML change is only noise (timestamps, random IDs, tracking pixels)
|
|
158
|
+
* Returns true if ONLY noise detected, false if meaningful changes exist
|
|
159
|
+
*/
|
|
160
|
+
function isNoisyChangeOnly(htmlBefore, htmlAfter) {
|
|
161
|
+
// Make a copy and remove known noise patterns
|
|
162
|
+
let before = htmlBefore;
|
|
163
|
+
let after = htmlAfter;
|
|
164
|
+
|
|
165
|
+
// Remove timestamps (ISO, Unix, etc.)
|
|
166
|
+
const timestampPattern = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^"']*/g;
|
|
167
|
+
before = before.replace(timestampPattern, '[TIMESTAMP]');
|
|
168
|
+
after = after.replace(timestampPattern, '[TIMESTAMP]');
|
|
169
|
+
|
|
170
|
+
// Remove UUID-like patterns
|
|
171
|
+
const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
|
172
|
+
before = before.replace(uuidPattern, '[UUID]');
|
|
173
|
+
after = after.replace(uuidPattern, '[UUID]');
|
|
174
|
+
|
|
175
|
+
// Remove random hash-like values
|
|
176
|
+
const hashPattern = /[a-f0-9]{32,}/g;
|
|
177
|
+
before = before.replace(hashPattern, '[HASH]');
|
|
178
|
+
after = after.replace(hashPattern, '[HASH]');
|
|
179
|
+
|
|
180
|
+
// Remove tracking params (ga, fbclid, etc.)
|
|
181
|
+
const trackingPattern = /[?&](ga[a-z_]*|fbclid|utm_[a-z]*|gclid|msclkid)=[^&"']*/g;
|
|
182
|
+
before = before.replace(trackingPattern, '[TRACKING]');
|
|
183
|
+
after = after.replace(trackingPattern, '[TRACKING]');
|
|
184
|
+
|
|
185
|
+
// Remove data-testid and similar noise attrs
|
|
186
|
+
const testIdPattern = /data-testid="[^"]*"/g;
|
|
187
|
+
before = before.replace(testIdPattern, '');
|
|
188
|
+
after = after.replace(testIdPattern, '');
|
|
189
|
+
|
|
190
|
+
// If they're now equal, it was only noise
|
|
191
|
+
return before === after;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Check for meaningful form state changes
|
|
196
|
+
*/
|
|
197
|
+
function checkFormStateChange(htmlBefore, htmlAfter) {
|
|
198
|
+
// Check for form input value changes (meaningful state change)
|
|
199
|
+
const beforeInputs = extractInputValues(htmlBefore);
|
|
200
|
+
const afterInputs = extractInputValues(htmlAfter);
|
|
201
|
+
|
|
202
|
+
if (beforeInputs.size !== afterInputs.size) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const [key, value] of beforeInputs) {
|
|
207
|
+
if (afterInputs.get(key) !== value) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Extract input name-value pairs from HTML
|
|
217
|
+
*/
|
|
218
|
+
function extractInputValues(html) {
|
|
219
|
+
const values = new Map();
|
|
220
|
+
const inputPattern = /<input[^>]*name="([^"]*)"[^>]*value="([^"]*)"/g;
|
|
221
|
+
let match;
|
|
222
|
+
while ((match = inputPattern.exec(html)) !== null) {
|
|
223
|
+
values.set(match[1], match[2]);
|
|
224
|
+
}
|
|
225
|
+
return values;
|
|
226
|
+
}
|