@veraxhq/verax 0.1.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/LICENSE +21 -0
- package/README.md +237 -0
- package/bin/verax.js +452 -0
- package/package.json +57 -0
- package/src/verax/detect/comparison.js +69 -0
- package/src/verax/detect/confidence-engine.js +498 -0
- package/src/verax/detect/evidence-validator.js +33 -0
- package/src/verax/detect/expectation-model.js +204 -0
- package/src/verax/detect/findings-writer.js +31 -0
- package/src/verax/detect/index.js +397 -0
- package/src/verax/detect/skip-classifier.js +202 -0
- package/src/verax/flow/flow-engine.js +265 -0
- package/src/verax/flow/flow-spec.js +145 -0
- package/src/verax/flow/redaction.js +74 -0
- package/src/verax/index.js +97 -0
- package/src/verax/learn/action-contract-extractor.js +281 -0
- package/src/verax/learn/ast-contract-extractor.js +255 -0
- package/src/verax/learn/index.js +18 -0
- package/src/verax/learn/manifest-writer.js +97 -0
- package/src/verax/learn/project-detector.js +87 -0
- package/src/verax/learn/react-router-extractor.js +73 -0
- package/src/verax/learn/route-extractor.js +122 -0
- package/src/verax/learn/route-validator.js +215 -0
- package/src/verax/learn/source-instrumenter.js +214 -0
- package/src/verax/learn/static-extractor.js +222 -0
- package/src/verax/learn/truth-assessor.js +96 -0
- package/src/verax/learn/ts-contract-resolver.js +395 -0
- package/src/verax/observe/browser.js +22 -0
- package/src/verax/observe/console-sensor.js +166 -0
- package/src/verax/observe/dom-signature.js +23 -0
- package/src/verax/observe/domain-boundary.js +38 -0
- package/src/verax/observe/evidence-capture.js +5 -0
- package/src/verax/observe/human-driver.js +376 -0
- package/src/verax/observe/index.js +67 -0
- package/src/verax/observe/interaction-discovery.js +269 -0
- package/src/verax/observe/interaction-runner.js +410 -0
- package/src/verax/observe/network-sensor.js +173 -0
- package/src/verax/observe/selector-generator.js +74 -0
- package/src/verax/observe/settle.js +155 -0
- package/src/verax/observe/state-ui-sensor.js +200 -0
- package/src/verax/observe/traces-writer.js +82 -0
- package/src/verax/observe/ui-signal-sensor.js +197 -0
- package/src/verax/resolve-workspace-root.js +173 -0
- package/src/verax/scan-summary-writer.js +41 -0
- package/src/verax/shared/artifact-manager.js +139 -0
- package/src/verax/shared/caching.js +104 -0
- package/src/verax/shared/expectation-proof.js +4 -0
- package/src/verax/shared/redaction.js +227 -0
- package/src/verax/shared/retry-policy.js +89 -0
- package/src/verax/shared/timing-metrics.js +44 -0
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@veraxhq/verax",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "VERAX - Silent failure detection for websites",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/verax/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"verax": "bin/verax.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./src/verax/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./learn": {
|
|
20
|
+
"import": "./src/verax/learn/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./observe": {
|
|
23
|
+
"import": "./src/verax/observe/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./detect": {
|
|
26
|
+
"import": "./src/verax/detect/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "node --test test/learn.test.js test/observe.test.js test/detect.test.js test/static-learn.test.js test/static-detect.test.js test/observe-boundary.test.js test/static-buttons-detect.test.js test/spa-support.test.js test/scan-summary.test.js test/route-validation.test.js test/skip-reasons.test.js test/observe-stabilization.test.js test/observe-coverage.test.js test/ast-extraction.test.js test/spa-ast-integration.test.js test/wave2-human-driver.test.js test/wave4-confidence.test.js test/wave5-action-contracts.test.js test/wave6-action-contracts.test.js test/wave7-auth-flows.test.js test/wave8-state-contracts.test.js test/wave9-hardening.test.js test/wave10-ci-summary.test.js test/cli-scan.test.js test/final-gate.test.js"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@babel/generator": "^7.28.5",
|
|
37
|
+
"@babel/parser": "^7.28.5",
|
|
38
|
+
"@babel/traverse": "^7.28.5",
|
|
39
|
+
"@babel/types": "^7.28.5",
|
|
40
|
+
"glob": "^10.3.10",
|
|
41
|
+
"inquirer": "^9.2.15",
|
|
42
|
+
"node-html-parser": "^7.0.1",
|
|
43
|
+
"playwright": "^1.40.0",
|
|
44
|
+
"typescript": "^5.4.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"testing",
|
|
51
|
+
"qa",
|
|
52
|
+
"website",
|
|
53
|
+
"silent-failures",
|
|
54
|
+
"verification"
|
|
55
|
+
],
|
|
56
|
+
"license": "MIT"
|
|
57
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
3
|
+
import { getUrlPath, getScreenshotHash } from './evidence-validator.js';
|
|
4
|
+
|
|
5
|
+
export function hasMeaningfulUrlChange(beforeUrl, afterUrl) {
|
|
6
|
+
const beforePath = getUrlPath(beforeUrl);
|
|
7
|
+
const afterPath = getUrlPath(afterUrl);
|
|
8
|
+
|
|
9
|
+
if (!beforePath || !afterPath) return false;
|
|
10
|
+
|
|
11
|
+
if (beforePath === afterPath) return false;
|
|
12
|
+
|
|
13
|
+
const beforeNormalized = beforePath.replace(/\/$/, '') || '/';
|
|
14
|
+
const afterNormalized = afterPath.replace(/\/$/, '') || '/';
|
|
15
|
+
|
|
16
|
+
return beforeNormalized !== afterNormalized;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir) {
|
|
20
|
+
// Screenshots are stored as relative paths like "screenshots/before-..."
|
|
21
|
+
// Try new structure first (.verax/runs/<runId>/evidence/), then legacy
|
|
22
|
+
let beforePath, afterPath;
|
|
23
|
+
const newEvidencePath = resolve(projectDir, '.verax', 'runs');
|
|
24
|
+
const legacyObservePath = resolve(projectDir, '.veraxverax', 'observe');
|
|
25
|
+
|
|
26
|
+
// Check if new structure exists and find latest run
|
|
27
|
+
if (existsSync(newEvidencePath)) {
|
|
28
|
+
try {
|
|
29
|
+
const runs = readdirSync(newEvidencePath)
|
|
30
|
+
.map(name => ({ name, time: statSync(resolve(newEvidencePath, name)).mtimeMs }))
|
|
31
|
+
.sort((a, b) => b.time - a.time);
|
|
32
|
+
if (runs.length > 0) {
|
|
33
|
+
const runEvidencePath = resolve(newEvidencePath, runs[0].name, 'evidence', beforeScreenshot);
|
|
34
|
+
if (existsSync(runEvidencePath)) {
|
|
35
|
+
beforePath = resolve(newEvidencePath, runs[0].name, 'evidence', beforeScreenshot);
|
|
36
|
+
afterPath = resolve(newEvidencePath, runs[0].name, 'evidence', afterScreenshot);
|
|
37
|
+
} else {
|
|
38
|
+
beforePath = resolve(legacyObservePath, beforeScreenshot);
|
|
39
|
+
afterPath = resolve(legacyObservePath, afterScreenshot);
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
beforePath = resolve(legacyObservePath, beforeScreenshot);
|
|
43
|
+
afterPath = resolve(legacyObservePath, afterScreenshot);
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
beforePath = resolve(legacyObservePath, beforeScreenshot);
|
|
47
|
+
afterPath = resolve(legacyObservePath, afterScreenshot);
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
beforePath = resolve(legacyObservePath, beforeScreenshot);
|
|
51
|
+
afterPath = resolve(legacyObservePath, afterScreenshot);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const beforeHash = getScreenshotHash(beforePath);
|
|
55
|
+
const afterHash = getScreenshotHash(afterPath);
|
|
56
|
+
|
|
57
|
+
if (!beforeHash || !afterHash) return false;
|
|
58
|
+
|
|
59
|
+
return beforeHash !== afterHash;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function hasDomChange(trace) {
|
|
63
|
+
if (!trace.dom || !trace.dom.beforeHash || !trace.dom.afterHash) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return trace.dom.beforeHash !== trace.dom.afterHash;
|
|
68
|
+
}
|
|
69
|
+
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAVE 4: CONFIDENCE ENGINE
|
|
3
|
+
*
|
|
4
|
+
* Evidence-based scoring for findings (0-100). No heuristics, no AI.
|
|
5
|
+
* Confidence computed strictly from:
|
|
6
|
+
* - Expectation proof strength (PROVEN_EXPECTATION required)
|
|
7
|
+
* - Runtime sensor evidence (network/console/ui-signals)
|
|
8
|
+
* - Deterministic observation signals (url/dom/screenshot changes)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const BASE_SCORES = {
|
|
12
|
+
network_silent_failure: 70,
|
|
13
|
+
validation_silent_failure: 60,
|
|
14
|
+
missing_feedback_failure: 55,
|
|
15
|
+
no_effect_silent_failure: 50,
|
|
16
|
+
missing_network_action: 65,
|
|
17
|
+
missing_state_action: 60
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const CONFIDENCE_LEVELS = {
|
|
21
|
+
HIGH: 80,
|
|
22
|
+
MEDIUM: 60
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const LONG_ACTION_MS = 2000; // Threshold for slow requests
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Compute confidence score for a finding.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object} params
|
|
31
|
+
* @param {string} params.findingType - Type of finding
|
|
32
|
+
* @param {Object} params.expectation - Expectation with proof status
|
|
33
|
+
* @param {Object} params.sensors - Sensor data (network, console, uiSignals)
|
|
34
|
+
* @param {Object} params.comparisons - Comparison results (hasUrlChange, hasDomChange, hasVisibleChange)
|
|
35
|
+
* @param {Object} params.attemptMeta - Metadata about the interaction attempt
|
|
36
|
+
* @returns {Object} { score, level, reasons, breakdown }
|
|
37
|
+
*/
|
|
38
|
+
export function computeConfidence({ findingType, expectation, sensors = {}, comparisons = {}, attemptMeta = {} }) {
|
|
39
|
+
const baseScore = BASE_SCORES[findingType] || 50;
|
|
40
|
+
const points = { plus: {}, minus: {} };
|
|
41
|
+
const reasons = [];
|
|
42
|
+
|
|
43
|
+
const networkSummary = sensors.network || {};
|
|
44
|
+
const consoleSummary = sensors.console || {};
|
|
45
|
+
const uiSignals = sensors.uiSignals || {};
|
|
46
|
+
|
|
47
|
+
// Extract signals
|
|
48
|
+
const hasErrorFeedback = detectErrorFeedback(uiSignals);
|
|
49
|
+
const hasLoadingFeedback = detectLoadingFeedback(uiSignals);
|
|
50
|
+
const hasAnyFeedback = hasErrorFeedback || hasLoadingFeedback || detectStatusFeedback(uiSignals);
|
|
51
|
+
|
|
52
|
+
let totalPlus = 0;
|
|
53
|
+
let totalMinus = 0;
|
|
54
|
+
|
|
55
|
+
// Type-specific scoring
|
|
56
|
+
switch (findingType) {
|
|
57
|
+
case 'network_silent_failure':
|
|
58
|
+
totalPlus += scoreNetworkSilentFailure({
|
|
59
|
+
networkSummary,
|
|
60
|
+
consoleSummary,
|
|
61
|
+
hasErrorFeedback,
|
|
62
|
+
hasAnyFeedback,
|
|
63
|
+
points,
|
|
64
|
+
reasons
|
|
65
|
+
});
|
|
66
|
+
totalMinus += penalizeNetworkSilentFailure({
|
|
67
|
+
hasAnyFeedback,
|
|
68
|
+
points,
|
|
69
|
+
reasons
|
|
70
|
+
});
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
case 'validation_silent_failure':
|
|
74
|
+
totalPlus += scoreValidationSilentFailure({
|
|
75
|
+
networkSummary,
|
|
76
|
+
consoleSummary,
|
|
77
|
+
hasErrorFeedback,
|
|
78
|
+
attemptMeta,
|
|
79
|
+
points,
|
|
80
|
+
reasons
|
|
81
|
+
});
|
|
82
|
+
totalMinus += penalizeValidationSilentFailure({
|
|
83
|
+
hasErrorFeedback,
|
|
84
|
+
points,
|
|
85
|
+
reasons
|
|
86
|
+
});
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case 'missing_feedback_failure':
|
|
90
|
+
totalPlus += scoreMissingFeedbackFailure({
|
|
91
|
+
networkSummary,
|
|
92
|
+
hasLoadingFeedback,
|
|
93
|
+
points,
|
|
94
|
+
reasons
|
|
95
|
+
});
|
|
96
|
+
totalMinus += penalizeMissingFeedbackFailure({
|
|
97
|
+
hasLoadingFeedback,
|
|
98
|
+
points,
|
|
99
|
+
reasons
|
|
100
|
+
});
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case 'no_effect_silent_failure':
|
|
104
|
+
totalPlus += scoreNoEffectSilentFailure({
|
|
105
|
+
expectation,
|
|
106
|
+
comparisons,
|
|
107
|
+
networkSummary,
|
|
108
|
+
hasAnyFeedback,
|
|
109
|
+
points,
|
|
110
|
+
reasons
|
|
111
|
+
});
|
|
112
|
+
totalMinus += penalizeNoEffectSilentFailure({
|
|
113
|
+
networkSummary,
|
|
114
|
+
hasAnyFeedback,
|
|
115
|
+
points,
|
|
116
|
+
reasons
|
|
117
|
+
});
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case 'missing_network_action':
|
|
121
|
+
totalPlus += scoreMissingNetworkAction({
|
|
122
|
+
expectation,
|
|
123
|
+
attemptMeta,
|
|
124
|
+
consoleSummary,
|
|
125
|
+
networkSummary,
|
|
126
|
+
points,
|
|
127
|
+
reasons
|
|
128
|
+
});
|
|
129
|
+
totalMinus += penalizeMissingNetworkAction({
|
|
130
|
+
networkSummary,
|
|
131
|
+
points,
|
|
132
|
+
reasons
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
case 'missing_state_action':
|
|
137
|
+
totalPlus += scoreMissingStateAction({
|
|
138
|
+
expectation,
|
|
139
|
+
attemptMeta,
|
|
140
|
+
sensors,
|
|
141
|
+
comparisons,
|
|
142
|
+
points,
|
|
143
|
+
reasons
|
|
144
|
+
});
|
|
145
|
+
totalMinus += penalizeMissingStateAction({
|
|
146
|
+
sensors,
|
|
147
|
+
networkSummary,
|
|
148
|
+
points,
|
|
149
|
+
reasons
|
|
150
|
+
});
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Compute final score
|
|
155
|
+
let score = baseScore + totalPlus - totalMinus;
|
|
156
|
+
score = Math.max(0, Math.min(100, score)); // Cap to [0, 100]
|
|
157
|
+
|
|
158
|
+
// Determine level
|
|
159
|
+
let level = 'LOW';
|
|
160
|
+
if (score >= CONFIDENCE_LEVELS.HIGH) {
|
|
161
|
+
level = 'HIGH';
|
|
162
|
+
} else if (score >= CONFIDENCE_LEVELS.MEDIUM) {
|
|
163
|
+
level = 'MEDIUM';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Limit reasons to 6 most important
|
|
167
|
+
const limitedReasons = reasons.slice(0, 6);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
score: Math.round(score),
|
|
171
|
+
level,
|
|
172
|
+
reasons: limitedReasons,
|
|
173
|
+
breakdown: {
|
|
174
|
+
base: baseScore,
|
|
175
|
+
plus: points.plus,
|
|
176
|
+
minus: points.minus
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// === NETWORK SILENT FAILURE SCORING ===
|
|
182
|
+
|
|
183
|
+
function scoreNetworkSilentFailure({ networkSummary, consoleSummary, hasErrorFeedback, hasAnyFeedback, points, reasons }) {
|
|
184
|
+
let total = 0;
|
|
185
|
+
|
|
186
|
+
// +15 if server error (5xx)
|
|
187
|
+
if (networkSummary.failedRequests >= 1) {
|
|
188
|
+
const has5xxError = Object.keys(networkSummary.failedByStatus || {}).some(status => parseInt(status) >= 500);
|
|
189
|
+
if (has5xxError) {
|
|
190
|
+
points.plus.serverError = 15;
|
|
191
|
+
total += 15;
|
|
192
|
+
reasons.push('Server error (5xx) detected');
|
|
193
|
+
} else {
|
|
194
|
+
// Client error (4xx)
|
|
195
|
+
points.plus.networkFailure = 10;
|
|
196
|
+
total += 10;
|
|
197
|
+
reasons.push('Network request failed');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// +10 if request explicitly failed (requestfailed event)
|
|
202
|
+
if (networkSummary.failedRequests > 0) {
|
|
203
|
+
const hasExplicitFailure = networkSummary.topFailedUrls?.length > 0;
|
|
204
|
+
if (hasExplicitFailure) {
|
|
205
|
+
points.plus.explicitFailure = 10;
|
|
206
|
+
total += 10;
|
|
207
|
+
reasons.push('Request failure event captured');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// +10 if console shows page error or unhandled rejection
|
|
212
|
+
if ((consoleSummary.pageErrorCount || 0) > 0 || (consoleSummary.unhandledRejectionCount || 0) > 0) {
|
|
213
|
+
points.plus.jsError = 10;
|
|
214
|
+
total += 10;
|
|
215
|
+
reasons.push('JavaScript error or unhandled rejection logged');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// +10 if NO user feedback at all
|
|
219
|
+
if (!hasAnyFeedback) {
|
|
220
|
+
points.plus.noFeedback = 10;
|
|
221
|
+
total += 10;
|
|
222
|
+
reasons.push('No user-visible error feedback');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return total;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function penalizeNetworkSilentFailure({ hasAnyFeedback, points, reasons }) {
|
|
229
|
+
let total = 0;
|
|
230
|
+
|
|
231
|
+
// -20 if any feedback exists (shouldn't be classified as silent failure)
|
|
232
|
+
if (hasAnyFeedback) {
|
|
233
|
+
points.minus.hasFeedback = 20;
|
|
234
|
+
total += 20;
|
|
235
|
+
reasons.push('User feedback detected (reduces confidence)');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return total;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// === VALIDATION SILENT FAILURE SCORING ===
|
|
242
|
+
|
|
243
|
+
function scoreValidationSilentFailure({ networkSummary, consoleSummary, hasErrorFeedback, attemptMeta, points, reasons }) {
|
|
244
|
+
let total = 0;
|
|
245
|
+
|
|
246
|
+
// +15 if invalid fields detected
|
|
247
|
+
const invalidFieldsCount = attemptMeta.invalidFieldsCount || 0;
|
|
248
|
+
if (invalidFieldsCount >= 1) {
|
|
249
|
+
points.plus.invalidFields = 15;
|
|
250
|
+
total += 15;
|
|
251
|
+
reasons.push(`${invalidFieldsCount} invalid form field(s) detected`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// +10 if console errors logged
|
|
255
|
+
if ((consoleSummary.consoleErrorCount || 0) >= 1) {
|
|
256
|
+
points.plus.consoleError = 10;
|
|
257
|
+
total += 10;
|
|
258
|
+
reasons.push('Validation errors logged to console');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// +10 if no validation feedback visible
|
|
262
|
+
if (!hasErrorFeedback) {
|
|
263
|
+
points.plus.noValidationFeedback = 10;
|
|
264
|
+
total += 10;
|
|
265
|
+
reasons.push('No visible validation error message');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return total;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function penalizeValidationSilentFailure({ hasErrorFeedback, points, reasons }) {
|
|
272
|
+
let total = 0;
|
|
273
|
+
|
|
274
|
+
// -15 if error feedback exists
|
|
275
|
+
if (hasErrorFeedback) {
|
|
276
|
+
points.minus.hasErrorFeedback = 15;
|
|
277
|
+
total += 15;
|
|
278
|
+
reasons.push('Error feedback visible (reduces confidence)');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return total;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// === MISSING FEEDBACK FAILURE SCORING ===
|
|
285
|
+
|
|
286
|
+
function scoreMissingFeedbackFailure({ networkSummary, hasLoadingFeedback, points, reasons }) {
|
|
287
|
+
let total = 0;
|
|
288
|
+
|
|
289
|
+
// +15 if slow requests detected
|
|
290
|
+
if ((networkSummary.slowRequestsCount || 0) >= 1) {
|
|
291
|
+
const slowestDuration = networkSummary.slowRequests?.[0]?.duration || 0;
|
|
292
|
+
points.plus.slowRequest = 15;
|
|
293
|
+
total += 15;
|
|
294
|
+
reasons.push(`Slow request detected (${slowestDuration}ms)`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// +10 if significant network activity
|
|
298
|
+
if ((networkSummary.totalRequests || 0) >= 1 && (networkSummary.durationMs || 0) >= LONG_ACTION_MS) {
|
|
299
|
+
points.plus.longAction = 10;
|
|
300
|
+
total += 10;
|
|
301
|
+
reasons.push('Long-running network activity');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// +10 if no loading feedback
|
|
305
|
+
if (!hasLoadingFeedback) {
|
|
306
|
+
points.plus.noLoadingFeedback = 10;
|
|
307
|
+
total += 10;
|
|
308
|
+
reasons.push('No loading indicator shown');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return total;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function penalizeMissingFeedbackFailure({ hasLoadingFeedback, points, reasons }) {
|
|
315
|
+
let total = 0;
|
|
316
|
+
|
|
317
|
+
// -10 if loading indicator detected
|
|
318
|
+
if (hasLoadingFeedback) {
|
|
319
|
+
points.minus.hasLoadingFeedback = 10;
|
|
320
|
+
total += 10;
|
|
321
|
+
reasons.push('Loading indicator detected (reduces confidence)');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return total;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// === NO EFFECT SILENT FAILURE SCORING ===
|
|
328
|
+
|
|
329
|
+
function scoreNoEffectSilentFailure({ expectation, comparisons, networkSummary, hasAnyFeedback, points, reasons }) {
|
|
330
|
+
let total = 0;
|
|
331
|
+
|
|
332
|
+
// +15 if expected navigation but no URL change
|
|
333
|
+
const expectsNavigation = expectation?.expectationType === 'navigation' ||
|
|
334
|
+
expectation?.expectationType === 'spa_navigation' ||
|
|
335
|
+
expectation?.expectationType === 'form_submission';
|
|
336
|
+
if (expectsNavigation && !comparisons.hasUrlChange) {
|
|
337
|
+
points.plus.expectedNavNoUrl = 15;
|
|
338
|
+
total += 15;
|
|
339
|
+
reasons.push('Expected navigation did not occur');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// +10 if no DOM change at all
|
|
343
|
+
if (!comparisons.hasDomChange) {
|
|
344
|
+
points.plus.noDomChange = 10;
|
|
345
|
+
total += 10;
|
|
346
|
+
reasons.push('No DOM changes detected');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// +10 if screenshot unchanged (if available)
|
|
350
|
+
if (comparisons.hasVisibleChange === false) {
|
|
351
|
+
points.plus.noVisibleChange = 10;
|
|
352
|
+
total += 10;
|
|
353
|
+
reasons.push('No visible changes in screenshot');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return total;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function penalizeNoEffectSilentFailure({ networkSummary, hasAnyFeedback, points, reasons }) {
|
|
360
|
+
let total = 0;
|
|
361
|
+
|
|
362
|
+
// -10 if network activity occurred (might be effect without visible change)
|
|
363
|
+
if ((networkSummary.totalRequests || 0) > 0) {
|
|
364
|
+
points.minus.hasNetworkActivity = 10;
|
|
365
|
+
total += 10;
|
|
366
|
+
reasons.push('Network activity detected (potential hidden effect)');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// -10 if UI signal changed
|
|
370
|
+
if (hasAnyFeedback) {
|
|
371
|
+
points.minus.uiSignalChanged = 10;
|
|
372
|
+
total += 10;
|
|
373
|
+
reasons.push('UI signal changed (potential effect)');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return total;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// === HELPER FUNCTIONS ===
|
|
380
|
+
|
|
381
|
+
function detectErrorFeedback(uiSignals) {
|
|
382
|
+
const before = uiSignals.before || {};
|
|
383
|
+
const after = uiSignals.after || {};
|
|
384
|
+
const changes = uiSignals.changes || {};
|
|
385
|
+
|
|
386
|
+
// Check if error signal appeared after interaction
|
|
387
|
+
return (after.hasErrorSignal && !before.hasErrorSignal) ||
|
|
388
|
+
(changes.changed && after.hasErrorSignal);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function detectLoadingFeedback(uiSignals) {
|
|
392
|
+
const before = uiSignals.before || {};
|
|
393
|
+
const after = uiSignals.after || {};
|
|
394
|
+
|
|
395
|
+
// Check if loading indicator appeared or changed
|
|
396
|
+
return after.hasLoadingIndicator ||
|
|
397
|
+
(before.hasLoadingIndicator !== after.hasLoadingIndicator);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function detectStatusFeedback(uiSignals) {
|
|
401
|
+
const after = uiSignals.after || {};
|
|
402
|
+
|
|
403
|
+
// Check for any status/live region updates
|
|
404
|
+
return after.hasStatusSignal || after.hasLiveRegion || after.hasDialog;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// === MISSING NETWORK ACTION SCORING ===
|
|
408
|
+
|
|
409
|
+
function scoreMissingNetworkAction({ expectation, attemptMeta, consoleSummary, networkSummary, points, reasons }) {
|
|
410
|
+
let total = 0;
|
|
411
|
+
|
|
412
|
+
// +15 if PROVEN expectation with source attribution (high certainty of broken promise)
|
|
413
|
+
if (expectation?.proof === 'PROVEN_EXPECTATION' && attemptMeta.sourceRef) {
|
|
414
|
+
points.plus.provenContract = 15;
|
|
415
|
+
total += 15;
|
|
416
|
+
reasons.push('Code contract proven via AST analysis');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// +10 if console has errors (might explain why request didn't fire)
|
|
420
|
+
if ((consoleSummary.consoleErrorCount || 0) > 0 || (consoleSummary.pageErrorCount || 0) > 0) {
|
|
421
|
+
points.plus.consoleError = 10;
|
|
422
|
+
total += 10;
|
|
423
|
+
reasons.push('JavaScript errors may have prevented request');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// +10 if absolutely zero network activity (not even unrelated requests)
|
|
427
|
+
if ((networkSummary.totalRequests || 0) === 0) {
|
|
428
|
+
points.plus.zeroNetworkActivity = 10;
|
|
429
|
+
total += 10;
|
|
430
|
+
reasons.push('Zero network activity despite code promise');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return total;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function penalizeMissingNetworkAction({ networkSummary, points, reasons }) {
|
|
437
|
+
let total = 0;
|
|
438
|
+
|
|
439
|
+
// -15 if there WAS network activity (maybe promise fulfilled differently)
|
|
440
|
+
if ((networkSummary.totalRequests || 0) > 0) {
|
|
441
|
+
points.minus.hadNetworkActivity = 15;
|
|
442
|
+
total += 15;
|
|
443
|
+
reasons.push('Other network requests occurred (may be fulfilling contract)');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return total;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// === MISSING STATE ACTION SCORING ===
|
|
450
|
+
|
|
451
|
+
function scoreMissingStateAction({ expectation, attemptMeta, sensors, comparisons, points, reasons }) {
|
|
452
|
+
let total = 0;
|
|
453
|
+
|
|
454
|
+
// +15 if PROVEN expectation with handlerRef (TS cross-file proof)
|
|
455
|
+
if (expectation?.proof === 'PROVEN_EXPECTATION' && attemptMeta.handlerRef) {
|
|
456
|
+
points.plus.provenHandlerRef = 15;
|
|
457
|
+
total += 15;
|
|
458
|
+
reasons.push('State mutation proven via TS cross-file analysis');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// +10 if state UI did not change (explicit signal)
|
|
462
|
+
const stateUI = sensors.stateUI || {};
|
|
463
|
+
if (stateUI.changed === false) {
|
|
464
|
+
points.plus.noStateChange = 10;
|
|
465
|
+
total += 10;
|
|
466
|
+
reasons.push('State UI signals show no change');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// +5 if DOM did not change (supports missing state mutation theory)
|
|
470
|
+
if (comparisons.hasDomChange === false) {
|
|
471
|
+
points.plus.noDomChange = 5;
|
|
472
|
+
total += 5;
|
|
473
|
+
reasons.push('DOM unchanged despite promised state mutation');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return total;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function penalizeMissingStateAction({ sensors, networkSummary, points, reasons }) {
|
|
480
|
+
let total = 0;
|
|
481
|
+
|
|
482
|
+
// -10 if there IS network activity (may be causing state change asynchronously)
|
|
483
|
+
if ((networkSummary.totalRequests || 0) > 0) {
|
|
484
|
+
points.minus.hadNetworkActivity = 10;
|
|
485
|
+
total += 10;
|
|
486
|
+
reasons.push('Network activity may be causing deferred state update');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// -10 if UI feedback changed (may indicate state managed differently)
|
|
490
|
+
const uiSignals = sensors.uiSignals || {};
|
|
491
|
+
if (uiSignals.changes?.changed === true) {
|
|
492
|
+
points.minus.hadUIFeedback = 10;
|
|
493
|
+
total += 10;
|
|
494
|
+
reasons.push('UI feedback changed (state may be managed via feedback rather than direct mutation)');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return total;
|
|
498
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
|
|
4
|
+
export function normalizeUrl(url) {
|
|
5
|
+
try {
|
|
6
|
+
const urlObj = new URL(url);
|
|
7
|
+
return {
|
|
8
|
+
origin: urlObj.origin,
|
|
9
|
+
pathname: urlObj.pathname,
|
|
10
|
+
search: urlObj.search,
|
|
11
|
+
hash: urlObj.hash
|
|
12
|
+
};
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getUrlPath(url) {
|
|
19
|
+
const normalized = normalizeUrl(url);
|
|
20
|
+
if (!normalized) return null;
|
|
21
|
+
return normalized.pathname + normalized.hash;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getScreenshotHash(screenshotPath) {
|
|
25
|
+
try {
|
|
26
|
+
if (!existsSync(screenshotPath)) return null;
|
|
27
|
+
const imageData = readFileSync(screenshotPath);
|
|
28
|
+
return createHash('md5').update(imageData).digest('hex');
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|