@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,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
|
+
}
|
package/src/cli/util/env-url.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import { assertExecutionBootstrapAllowed } from './bootstrap-guard.js';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Attempt to infer URL from environment variables and common dev configs
|
|
5
3
|
*/
|
|
6
4
|
export function tryResolveUrlFromEnv() {
|
|
7
|
-
// PHASE 21.6.1: Runtime guard - crash if called during inspection
|
|
8
|
-
assertExecutionBootstrapAllowed('tryResolveUrlFromEnv');
|
|
9
5
|
// Check common environment variables
|
|
10
6
|
const candidates = [
|
|
11
7
|
process.env.VERCEL_URL,
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { computeDOMDiff, hasFeedbackElements as _hasFeedbackElements, hasValidationErrors as _hasValidationErrors } from './dom-diff.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Evidence Engine
|
|
7
|
+
* Captures and bundles evidence for each interaction attempt
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class EvidenceBundle {
|
|
11
|
+
constructor(promiseId, expNum, evidencePath) {
|
|
12
|
+
this.promiseId = promiseId;
|
|
13
|
+
this.expNum = expNum;
|
|
14
|
+
this.evidencePath = evidencePath;
|
|
15
|
+
this.beforeScreenshot = null;
|
|
16
|
+
this.afterScreenshot = null;
|
|
17
|
+
this.beforeHTML = null;
|
|
18
|
+
this.afterHTML = null;
|
|
19
|
+
this.domDiff = null;
|
|
20
|
+
this.networkEvents = [];
|
|
21
|
+
this.networkEventsByTime = new Map(); // For correlation
|
|
22
|
+
this.consoleErrors = [];
|
|
23
|
+
this.actionStartTime = null; // When action was executed
|
|
24
|
+
this.timing = {
|
|
25
|
+
startedAt: null,
|
|
26
|
+
endedAt: null,
|
|
27
|
+
};
|
|
28
|
+
this.signals = {
|
|
29
|
+
navigationChanged: false,
|
|
30
|
+
domChanged: false,
|
|
31
|
+
feedbackSeen: false,
|
|
32
|
+
networkActivity: false,
|
|
33
|
+
consoleErrors: false,
|
|
34
|
+
meaningfulDomChange: false, // NEW: tracks only meaningful changes
|
|
35
|
+
correlatedNetworkActivity: false, // NEW: tracks network within action window
|
|
36
|
+
};
|
|
37
|
+
this.files = [];
|
|
38
|
+
this.correlatedRequests = []; // Network requests correlated to this action
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Capture before state
|
|
43
|
+
*/
|
|
44
|
+
async captureBeforeState(page, suffix = '') {
|
|
45
|
+
try {
|
|
46
|
+
this.beforeScreenshot = `exp_${this.expNum}_before${suffix}.png`;
|
|
47
|
+
const screenshotPath = resolve(this.evidencePath, this.beforeScreenshot);
|
|
48
|
+
await page.screenshot({ path: screenshotPath });
|
|
49
|
+
this.files.push(this.beforeScreenshot);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
this.beforeScreenshot = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
this.beforeHTML = await page.content();
|
|
56
|
+
} catch (e) {
|
|
57
|
+
this.beforeHTML = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Capture after state
|
|
63
|
+
*/
|
|
64
|
+
async captureAfterState(page, suffix = '') {
|
|
65
|
+
try {
|
|
66
|
+
this.afterScreenshot = `exp_${this.expNum}_after${suffix}.png`;
|
|
67
|
+
const screenshotPath = resolve(this.evidencePath, this.afterScreenshot);
|
|
68
|
+
await page.screenshot({ path: screenshotPath });
|
|
69
|
+
this.files.push(this.afterScreenshot);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
this.afterScreenshot = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
this.afterHTML = await page.content();
|
|
76
|
+
} catch (e) {
|
|
77
|
+
this.afterHTML = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Analyze changes between before and after
|
|
83
|
+
*/
|
|
84
|
+
analyzeChanges(urlBefore, urlAfter) {
|
|
85
|
+
// Navigation change
|
|
86
|
+
if (urlBefore && urlAfter && urlBefore !== urlAfter) {
|
|
87
|
+
this.signals.navigationChanged = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// DOM change
|
|
91
|
+
if (this.beforeHTML && this.afterHTML) {
|
|
92
|
+
this.domDiff = computeDOMDiff(this.beforeHTML, this.afterHTML);
|
|
93
|
+
if (this.domDiff.changed) {
|
|
94
|
+
this.signals.domChanged = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Track meaningful changes separately
|
|
98
|
+
if (this.domDiff.isMeaningful) {
|
|
99
|
+
this.signals.meaningfulDomChange = true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check for feedback elements visibility change
|
|
103
|
+
if (detectNewFeedback(this.beforeHTML, this.afterHTML)) {
|
|
104
|
+
this.signals.feedbackSeen = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check for validation error appearance
|
|
108
|
+
if (detectNewValidationError(this.beforeHTML, this.afterHTML)) {
|
|
109
|
+
this.signals.feedbackSeen = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Record network events that occurred during this action
|
|
116
|
+
*/
|
|
117
|
+
recordNetworkEvent(networkLog) {
|
|
118
|
+
this.networkEvents.push(networkLog);
|
|
119
|
+
|
|
120
|
+
// Track event time for correlation
|
|
121
|
+
if (networkLog.startTime) {
|
|
122
|
+
this.networkEventsByTime.set(networkLog.startTime, networkLog);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (this.networkEvents.length > 0) {
|
|
126
|
+
this.signals.networkActivity = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Correlate network events to this action
|
|
132
|
+
* Captures requests that started within correlation window after action
|
|
133
|
+
* Window: 0-2500ms after action starts
|
|
134
|
+
*/
|
|
135
|
+
correlateNetworkRequests() {
|
|
136
|
+
if (!this.actionStartTime || this.networkEvents.length === 0) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const actionTime = new Date(this.actionStartTime).getTime();
|
|
141
|
+
const correlationWindow = 2500; // milliseconds
|
|
142
|
+
|
|
143
|
+
for (const event of this.networkEvents) {
|
|
144
|
+
if (event.startTime) {
|
|
145
|
+
const eventTime = new Date(event.startTime).getTime();
|
|
146
|
+
if (eventTime >= actionTime && eventTime <= actionTime + correlationWindow) {
|
|
147
|
+
this.correlatedRequests.push(event);
|
|
148
|
+
this.signals.correlatedNetworkActivity = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Record console errors
|
|
156
|
+
*/
|
|
157
|
+
recordConsoleError(error) {
|
|
158
|
+
this.consoleErrors.push(error);
|
|
159
|
+
if (this.consoleErrors.length > 0) {
|
|
160
|
+
this.signals.consoleErrors = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Finalize evidence bundle
|
|
166
|
+
*/
|
|
167
|
+
finalize(_page) {
|
|
168
|
+
this.timing.endedAt = new Date().toISOString();
|
|
169
|
+
|
|
170
|
+
// Persist evidence files
|
|
171
|
+
try {
|
|
172
|
+
mkdirSync(this.evidencePath, { recursive: true });
|
|
173
|
+
|
|
174
|
+
// Save DOM diff if available
|
|
175
|
+
if (this.domDiff) {
|
|
176
|
+
const diffFile = `exp_${this.expNum}_dom_diff.json`;
|
|
177
|
+
writeFileSync(
|
|
178
|
+
resolve(this.evidencePath, diffFile),
|
|
179
|
+
JSON.stringify(this.domDiff, null, 2),
|
|
180
|
+
'utf-8'
|
|
181
|
+
);
|
|
182
|
+
this.files.push(diffFile);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Save network events if any
|
|
186
|
+
if (this.networkEvents.length > 0) {
|
|
187
|
+
const netFile = `exp_${this.expNum}_network.json`;
|
|
188
|
+
writeFileSync(
|
|
189
|
+
resolve(this.evidencePath, netFile),
|
|
190
|
+
JSON.stringify(this.networkEvents, null, 2),
|
|
191
|
+
'utf-8'
|
|
192
|
+
);
|
|
193
|
+
this.files.push(netFile);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Save console errors if any
|
|
197
|
+
if (this.consoleErrors.length > 0) {
|
|
198
|
+
const errFile = `exp_${this.expNum}_console_errors.json`;
|
|
199
|
+
writeFileSync(
|
|
200
|
+
resolve(this.evidencePath, errFile),
|
|
201
|
+
JSON.stringify(this.consoleErrors, null, 2),
|
|
202
|
+
'utf-8'
|
|
203
|
+
);
|
|
204
|
+
this.files.push(errFile);
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
// Best effort
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check if we have meaningful evidence
|
|
213
|
+
*/
|
|
214
|
+
hasEvidence() {
|
|
215
|
+
return (
|
|
216
|
+
this.beforeScreenshot ||
|
|
217
|
+
this.afterScreenshot ||
|
|
218
|
+
this.domDiff?.changed ||
|
|
219
|
+
this.networkEvents.length > 0 ||
|
|
220
|
+
this.consoleErrors.length > 0 ||
|
|
221
|
+
Object.values(this.signals).some(v => v === true)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Summarize signals for observation
|
|
227
|
+
*/
|
|
228
|
+
getSummary() {
|
|
229
|
+
return {
|
|
230
|
+
files: this.files,
|
|
231
|
+
timing: this.timing,
|
|
232
|
+
signals: this.signals,
|
|
233
|
+
hasEvidence: this.hasEvidence(),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Create a bundle for a promise
|
|
240
|
+
*/
|
|
241
|
+
export function createEvidenceBundle(promise, expNum, evidencePath) {
|
|
242
|
+
return new EvidenceBundle(promise.id, expNum, evidencePath);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Detect if new feedback elements appeared
|
|
247
|
+
*/
|
|
248
|
+
function detectNewFeedback(htmlBefore, htmlAfter) {
|
|
249
|
+
const feedbackPatterns = [
|
|
250
|
+
'role="alert"',
|
|
251
|
+
'role="status"',
|
|
252
|
+
'aria-live="polite"',
|
|
253
|
+
'aria-live="assertive"',
|
|
254
|
+
'class="toast"',
|
|
255
|
+
'class="modal"',
|
|
256
|
+
'class="dialog"',
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
for (const pattern of feedbackPatterns) {
|
|
260
|
+
if (!htmlBefore.includes(pattern) && htmlAfter.includes(pattern)) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Detect if new validation errors appeared
|
|
270
|
+
*/
|
|
271
|
+
function detectNewValidationError(htmlBefore, htmlAfter) {
|
|
272
|
+
const errorPatterns = [
|
|
273
|
+
'aria-invalid="true"',
|
|
274
|
+
'aria-invalid=\'true\'',
|
|
275
|
+
'class="error"',
|
|
276
|
+
'class="invalid"',
|
|
277
|
+
'[data-error]',
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
for (const pattern of errorPatterns) {
|
|
281
|
+
if (!htmlBefore.includes(pattern) && htmlAfter.includes(pattern)) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return false;
|
|
287
|
+
}
|