@veraxhq/verax 0.1.0 → 0.2.1
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 +123 -88
- package/bin/verax.js +11 -452
- package/package.json +24 -36
- package/src/cli/commands/default.js +681 -0
- package/src/cli/commands/doctor.js +197 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +586 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +297 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +110 -0
- package/src/cli/util/expectation-extractor.js +388 -0
- package/src/cli/util/findings-writer.js +32 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +412 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +30 -0
- package/src/cli/util/project-discovery.js +297 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +43 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +111 -0
- package/src/verax/cli/wizard.js +109 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +432 -0
- package/src/verax/core/incremental-store.js +245 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +523 -0
- package/src/verax/detect/comparison.js +7 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +127 -0
- package/src/verax/detect/expectation-model.js +241 -168
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +41 -12
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +200 -288
- package/src/verax/detect/interactive-findings.js +612 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +561 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +103 -15
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +642 -0
- package/src/verax/intel/vue-router-extractor.js +325 -0
- package/src/verax/learn/action-contract-extractor.js +338 -104
- package/src/verax/learn/ast-contract-extractor.js +148 -6
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +122 -58
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +28 -97
- package/src/verax/learn/route-validator.js +8 -7
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +119 -10
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +30 -6
- package/src/verax/observe/console-sensor.js +2 -18
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +513 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +660 -273
- package/src/verax/observe/index.js +910 -26
- package/src/verax/observe/interaction-discovery.js +378 -15
- package/src/verax/observe/interaction-runner.js +562 -197
- package/src/verax/observe/loading-sensor.js +145 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +38 -17
- package/src/verax/observe/state-sensor.js +393 -0
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +73 -21
- package/src/verax/observe/ui-signal-sensor.js +143 -17
- package/src/verax/scan-summary-writer.js +80 -15
- package/src/verax/shared/artifact-manager.js +111 -9
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +169 -0
- package/src/verax/shared/dynamic-route-utils.js +224 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +9 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +66 -0
- package/src/verax/validate/context-validator.js +244 -0
|
@@ -1,204 +1,277 @@
|
|
|
1
1
|
import { getUrlPath } from './evidence-validator.js';
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
function routeMatchesUrl(routePath, url) {
|
|
4
|
+
const urlPath = getUrlPath(url);
|
|
5
|
+
if (!urlPath) return false;
|
|
6
|
+
|
|
7
|
+
const normalizedRoute = routePath.replace(/\/$/, '') || '/';
|
|
8
|
+
const normalizedUrl = urlPath.replace(/\/$/, '') || '/';
|
|
9
|
+
|
|
10
|
+
if (normalizedRoute === normalizedUrl) return true;
|
|
11
|
+
|
|
12
|
+
if (normalizedRoute.includes(':')) {
|
|
13
|
+
const routeParts = normalizedRoute.split('/');
|
|
14
|
+
const urlParts = normalizedUrl.split('/');
|
|
15
|
+
|
|
16
|
+
if (routeParts.length !== urlParts.length) return false;
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < routeParts.length; i++) {
|
|
19
|
+
if (routeParts[i].startsWith(':')) continue;
|
|
20
|
+
if (routeParts[i] !== urlParts[i]) return false;
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeExpectationType(expectationType) {
|
|
29
|
+
if (expectationType === 'spa_navigation') {
|
|
30
|
+
return 'navigation';
|
|
31
|
+
}
|
|
32
|
+
return expectationType;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Normalize selector for comparison.
|
|
37
|
+
* Removes brackets, parentheses, and normalizes whitespace.
|
|
38
|
+
*/
|
|
39
|
+
function normalizeSelector(selector) {
|
|
40
|
+
if (!selector) return '';
|
|
41
|
+
return selector.replace(/[[\]()]/g, '').trim();
|
|
42
|
+
}
|
|
3
43
|
|
|
4
44
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
45
|
+
* Check if two selectors match.
|
|
46
|
+
* Returns true if:
|
|
47
|
+
* - Exact match, OR
|
|
48
|
+
* - One contains the other (after normalization)
|
|
7
49
|
*/
|
|
8
|
-
function
|
|
9
|
-
if (!
|
|
50
|
+
function selectorsMatch(selector1, selector2) {
|
|
51
|
+
if (!selector1 || !selector2) return false;
|
|
10
52
|
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
if (hrefMatch) {
|
|
14
|
-
return hrefMatch[1];
|
|
15
|
-
}
|
|
53
|
+
const norm1 = normalizeSelector(selector1);
|
|
54
|
+
const norm2 = normalizeSelector(selector2);
|
|
16
55
|
|
|
17
|
-
return
|
|
56
|
+
if (selector1 === selector2) return true;
|
|
57
|
+
if (norm1 === norm2) return true;
|
|
58
|
+
if (selector1.includes(selector2)) return true;
|
|
59
|
+
if (selector2.includes(selector1)) return true;
|
|
60
|
+
if (norm1.includes(norm2)) return true;
|
|
61
|
+
if (norm2.includes(norm1)) return true;
|
|
62
|
+
|
|
63
|
+
return false;
|
|
18
64
|
}
|
|
19
65
|
|
|
20
66
|
/**
|
|
21
|
-
*
|
|
22
|
-
* Wave 0: PROVEN expectations from staticExpectations (HTML-derived)
|
|
23
|
-
* Wave 1: PROVEN expectations from spaExpectations (AST-derived JSX contracts)
|
|
24
|
-
* Wave 5: PROVEN expectations from actionContracts (AST-derived network actions)
|
|
25
|
-
* Wave 8: PROVEN expectations from actionContracts (AST-derived state actions)
|
|
26
|
-
*
|
|
27
|
-
* @returns {{ hasExpectation: boolean, proof: string, expectedTargetPath?: string, expectationType?: string, method?: string, urlPath?: string, stateKind?: string }}
|
|
67
|
+
* Check if interaction type is compatible with expectation type.
|
|
28
68
|
*/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
function typesCompatible(expectationType, interactionType) {
|
|
70
|
+
const normalizedType = normalizeExpectationType(expectationType);
|
|
71
|
+
if (normalizedType === 'navigation') {
|
|
72
|
+
return interactionType === 'link' || interactionType === 'button';
|
|
73
|
+
}
|
|
74
|
+
if (normalizedType === 'form_submission') {
|
|
75
|
+
return interactionType === 'form';
|
|
76
|
+
}
|
|
77
|
+
// VALIDATION INTELLIGENCE v1: Validation blocks are triggered by forms
|
|
78
|
+
if (normalizedType === 'validation_block') {
|
|
79
|
+
return interactionType === 'form';
|
|
80
|
+
}
|
|
81
|
+
if (normalizedType === 'network_action') {
|
|
82
|
+
return interactionType === 'button' || interactionType === 'form';
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Match expectation to interaction.
|
|
89
|
+
* Returns the matched expectation or null.
|
|
90
|
+
*/
|
|
91
|
+
export function matchExpectation(expectation, interaction, beforeUrl) {
|
|
92
|
+
const beforePath = getUrlPath(beforeUrl);
|
|
93
|
+
if (!beforePath) return null;
|
|
94
|
+
|
|
95
|
+
const normalizedBefore = beforePath.replace(/\/$/, '') || '/';
|
|
96
|
+
const normalizedFrom = expectation.fromPath.replace(/\/$/, '') || '/';
|
|
97
|
+
const normalizedType = normalizeExpectationType(expectation.type);
|
|
98
|
+
|
|
99
|
+
if (normalizedFrom !== normalizedBefore) return null;
|
|
100
|
+
|
|
101
|
+
if (!typesCompatible(normalizedType, interaction.type)) return null;
|
|
102
|
+
|
|
103
|
+
const selectorHint = expectation.evidence?.selectorHint || '';
|
|
104
|
+
const interactionSelector = interaction.selector || '';
|
|
105
|
+
|
|
106
|
+
if (selectorHint && interactionSelector) {
|
|
107
|
+
if (selectorsMatch(selectorHint, interactionSelector)) {
|
|
108
|
+
return { ...expectation, type: normalizedType };
|
|
55
109
|
}
|
|
110
|
+
} else if (!selectorHint && !interactionSelector) {
|
|
111
|
+
return { ...expectation, type: normalizedType };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
56
116
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
117
|
+
function _matchesStaticExpectation(expectation, interaction, afterUrl) {
|
|
118
|
+
if (interaction.type !== 'link') return false;
|
|
119
|
+
|
|
120
|
+
const afterPath = getUrlPath(afterUrl);
|
|
121
|
+
if (!afterPath) return false;
|
|
122
|
+
|
|
123
|
+
const normalizedTarget = expectation.targetPath.replace(/\/$/, '') || '/';
|
|
124
|
+
const normalizedAfter = afterPath.replace(/\/$/, '') || '/';
|
|
125
|
+
|
|
126
|
+
if (normalizedAfter === normalizedTarget) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const selectorHint = expectation.evidence.selectorHint || '';
|
|
131
|
+
const interactionSelector = interaction.selector || '';
|
|
132
|
+
const _interactionLabel = (interaction.label || '').toLowerCase().trim();
|
|
133
|
+
|
|
134
|
+
if (selectorHint && interactionSelector) {
|
|
135
|
+
if (selectorHint.includes(interactionSelector) || interactionSelector.includes(selectorHint.replace(/[\]()]/g, ''))) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function expectsNavigation(manifest, interaction, beforeUrl) {
|
|
144
|
+
if (manifest.staticExpectations && manifest.staticExpectations.length > 0) {
|
|
145
|
+
for (const expectation of manifest.staticExpectations) {
|
|
146
|
+
const matched = matchExpectation(expectation, interaction, beforeUrl);
|
|
147
|
+
if (matched) {
|
|
148
|
+
return true;
|
|
78
149
|
}
|
|
79
150
|
}
|
|
80
151
|
}
|
|
81
152
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
for (const expectation of manifest.spaExpectations) {
|
|
93
|
-
if (expectation.proof !== ExpectationProof.PROVEN_EXPECTATION) {
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const normalizedTarget = expectation.targetPath.replace(/\/$/, '') || '/';
|
|
98
|
-
|
|
99
|
-
// Match by href attribute value
|
|
100
|
-
if (expectation.matchAttribute === 'href' && normalizedHref === normalizedTarget) {
|
|
101
|
-
return {
|
|
102
|
-
hasExpectation: true,
|
|
103
|
-
proof: ExpectationProof.PROVEN_EXPECTATION,
|
|
104
|
-
expectedTargetPath: expectation.targetPath,
|
|
105
|
-
expectationType: 'spa_navigation'
|
|
106
|
-
};
|
|
107
|
-
}
|
|
153
|
+
if (manifest.projectType === 'react_spa' && interaction.type === 'link') {
|
|
154
|
+
const beforePath = getUrlPath(beforeUrl);
|
|
155
|
+
if (beforePath) {
|
|
156
|
+
const href = interaction.selector ? interaction.selector.match(/href=["']([^"']+)["']/) : null;
|
|
157
|
+
if (!href) {
|
|
158
|
+
for (const route of (manifest.routes || [])) {
|
|
159
|
+
if (!route.public) continue;
|
|
160
|
+
const routePath = route.path.toLowerCase();
|
|
161
|
+
const routeName = routePath.split('/').pop() || 'home';
|
|
162
|
+
const interactionLabel = (interaction.label || '').toLowerCase().trim();
|
|
108
163
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
hasExpectation: true,
|
|
113
|
-
proof: ExpectationProof.PROVEN_EXPECTATION,
|
|
114
|
-
expectedTargetPath: expectation.targetPath,
|
|
115
|
-
expectationType: 'spa_navigation'
|
|
116
|
-
};
|
|
164
|
+
if (interactionLabel.includes(routeName) || routeName.includes(interactionLabel)) {
|
|
165
|
+
return true;
|
|
117
166
|
}
|
|
118
167
|
}
|
|
119
168
|
}
|
|
120
169
|
}
|
|
121
170
|
}
|
|
122
171
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
172
|
+
if (interaction.type === 'link') {
|
|
173
|
+
const label = (interaction.label || '').toLowerCase().trim();
|
|
174
|
+
|
|
175
|
+
for (const route of (manifest.routes || [])) {
|
|
176
|
+
if (!route.public) continue;
|
|
177
|
+
|
|
178
|
+
const routePath = route.path.toLowerCase();
|
|
179
|
+
const routeName = routePath.split('/').pop() || 'home';
|
|
128
180
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
181
|
+
if (label.includes(routeName) || routeName.includes(label)) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (routeMatchesUrl(route.path, beforeUrl + route.path)) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (interaction.type === 'button' || interaction.type === 'form') {
|
|
192
|
+
const label = (interaction.label || '').toLowerCase().trim();
|
|
193
|
+
|
|
194
|
+
const navigationKeywords = ['go', 'navigate', 'next', 'continue', 'submit', 'save'];
|
|
195
|
+
const hasNavKeyword = navigationKeywords.some(keyword => label.includes(keyword));
|
|
196
|
+
|
|
197
|
+
if (hasNavKeyword) {
|
|
198
|
+
for (const route of (manifest.routes || [])) {
|
|
199
|
+
if (!route.public) continue;
|
|
200
|
+
if (route.path === '/' && label.includes('home')) {
|
|
201
|
+
return true;
|
|
132
202
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const normalizedFrom = expectation.fromPath.replace(/\/$/, '') || '/';
|
|
136
|
-
|
|
137
|
-
if (normalizedFrom === normalizedBefore) {
|
|
138
|
-
if (interaction.type === 'link' || interaction.type === 'button') {
|
|
139
|
-
const selectorHint = expectation.evidence.selectorHint || '';
|
|
140
|
-
const interactionSelector = interaction.selector || '';
|
|
141
|
-
|
|
142
|
-
if (selectorHint && interactionSelector) {
|
|
143
|
-
const normalizedSelectorHint = selectorHint.replace(/[\[\]()]/g, '');
|
|
144
|
-
const normalizedInteractionSelector = interactionSelector.replace(/[\[\]()]/g, '');
|
|
145
|
-
|
|
146
|
-
if (selectorHint === interactionSelector ||
|
|
147
|
-
selectorHint.includes(interactionSelector) ||
|
|
148
|
-
interactionSelector.includes(normalizedSelectorHint) ||
|
|
149
|
-
normalizedSelectorHint === normalizedInteractionSelector) {
|
|
150
|
-
return {
|
|
151
|
-
hasExpectation: true,
|
|
152
|
-
proof: ExpectationProof.PROVEN_EXPECTATION,
|
|
153
|
-
expectedTargetPath: expectation.targetPath,
|
|
154
|
-
expectationType: 'navigation'
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
} else if (expectation.type === 'form_submission') {
|
|
161
|
-
const normalizedFrom = expectation.fromPath.replace(/\/$/, '') || '/';
|
|
162
|
-
|
|
163
|
-
if (normalizedFrom === normalizedBefore && interaction.type === 'form') {
|
|
164
|
-
const selectorHint = expectation.evidence.selectorHint || '';
|
|
165
|
-
const interactionSelector = interaction.selector || '';
|
|
166
|
-
|
|
167
|
-
if (selectorHint && interactionSelector) {
|
|
168
|
-
const normalizedSelectorHint = selectorHint.replace(/[\[\]()]/g, '');
|
|
169
|
-
const normalizedInteractionSelector = interactionSelector.replace(/[\[\]()]/g, '');
|
|
170
|
-
|
|
171
|
-
if (selectorHint === interactionSelector ||
|
|
172
|
-
selectorHint.includes(interactionSelector) ||
|
|
173
|
-
interactionSelector.includes(normalizedSelectorHint) ||
|
|
174
|
-
normalizedSelectorHint === normalizedInteractionSelector) {
|
|
175
|
-
return {
|
|
176
|
-
hasExpectation: true,
|
|
177
|
-
proof: ExpectationProof.PROVEN_EXPECTATION,
|
|
178
|
-
expectedTargetPath: expectation.targetPath,
|
|
179
|
-
expectationType: 'form_submission'
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
203
|
+
if (route.path !== '/') {
|
|
204
|
+
return true;
|
|
184
205
|
}
|
|
185
206
|
}
|
|
186
207
|
}
|
|
208
|
+
|
|
209
|
+
for (const route of (manifest.routes || [])) {
|
|
210
|
+
if (!route.public) continue;
|
|
211
|
+
const routePath = route.path.toLowerCase();
|
|
212
|
+
const routeName = routePath.split('/').pop() || 'home';
|
|
213
|
+
|
|
214
|
+
if (routeName === 'home' && (label.includes('home') || label.includes('main') || label.includes('index'))) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (label.includes(routeName) || routeName.includes(label)) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
187
222
|
}
|
|
188
223
|
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
hasExpectation: false,
|
|
192
|
-
proof: ExpectationProof.UNKNOWN_EXPECTATION
|
|
193
|
-
};
|
|
224
|
+
return false;
|
|
194
225
|
}
|
|
195
226
|
|
|
196
227
|
/**
|
|
197
|
-
*
|
|
198
|
-
*
|
|
228
|
+
* Get expectation for interaction from manifest (unified API for static and action contracts)
|
|
229
|
+
* @param {Object} manifest - Project manifest
|
|
230
|
+
* @param {Object} interaction - Interaction object
|
|
231
|
+
* @param {string} beforeUrl - URL before interaction
|
|
232
|
+
* @param {Object} [attemptMeta] - Optional metadata about the interaction attempt
|
|
233
|
+
* @returns {Object} { hasExpectation: boolean, proof: string, ...expectationData }
|
|
199
234
|
*/
|
|
200
|
-
export function
|
|
201
|
-
|
|
202
|
-
|
|
235
|
+
export function getExpectation(manifest, interaction, beforeUrl, attemptMeta = {}) {
|
|
236
|
+
// Check static expectations first (for static sites)
|
|
237
|
+
if (manifest.staticExpectations && manifest.staticExpectations.length > 0) {
|
|
238
|
+
for (const expectation of manifest.staticExpectations) {
|
|
239
|
+
const matched = matchExpectation(expectation, interaction, beforeUrl);
|
|
240
|
+
if (matched) {
|
|
241
|
+
return {
|
|
242
|
+
hasExpectation: true,
|
|
243
|
+
proof: expectation.proof || 'PROVEN_EXPECTATION',
|
|
244
|
+
expectationType: expectation.type,
|
|
245
|
+
expectedTargetPath: expectation.targetPath,
|
|
246
|
+
...expectation
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Check action contracts (for apps with source-level contracts)
|
|
253
|
+
if (manifest.actionContracts && manifest.actionContracts.length > 0) {
|
|
254
|
+
const sourceRef = attemptMeta?.sourceRef;
|
|
255
|
+
if (sourceRef) {
|
|
256
|
+
for (const contract of manifest.actionContracts) {
|
|
257
|
+
if (contract.source === sourceRef) {
|
|
258
|
+
const expectationType = contract.kind === 'NETWORK_ACTION' ? 'network_action' : 'action';
|
|
259
|
+
return {
|
|
260
|
+
hasExpectation: true,
|
|
261
|
+
proof: 'PROVEN_EXPECTATION',
|
|
262
|
+
expectationType,
|
|
263
|
+
method: contract.method,
|
|
264
|
+
urlPath: contract.urlPath,
|
|
265
|
+
...contract
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
hasExpectation: false,
|
|
274
|
+
proof: 'UNKNOWN_EXPECTATION'
|
|
275
|
+
};
|
|
203
276
|
}
|
|
204
277
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 9: Reality Confidence & Explanation Layer
|
|
3
|
+
*
|
|
4
|
+
* Helper functions to generate human-readable explanations for findings.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate a human summary - one short sentence describing what the USER experienced.
|
|
9
|
+
* NO technical jargon.
|
|
10
|
+
*/
|
|
11
|
+
export function generateHumanSummary(finding, _trace) {
|
|
12
|
+
const findingType = finding.type || '';
|
|
13
|
+
const interaction = finding.interaction || {};
|
|
14
|
+
const label = interaction.label || interaction.text || 'the button';
|
|
15
|
+
const evidence = finding.evidence || {};
|
|
16
|
+
|
|
17
|
+
switch (findingType) {
|
|
18
|
+
case 'navigation_silent_failure':
|
|
19
|
+
if (evidence.urlChanged) {
|
|
20
|
+
return `The ${label} changed the page URL, but the visible content did not change.`;
|
|
21
|
+
}
|
|
22
|
+
return `Clicking ${label} should have navigated to a different page, but nothing happened.`;
|
|
23
|
+
|
|
24
|
+
case 'network_silent_failure':
|
|
25
|
+
return `The ${label} was clicked, but the expected network request was not sent.`;
|
|
26
|
+
|
|
27
|
+
case 'validation_silent_failure':
|
|
28
|
+
return `Submitting the form did not show any validation feedback, even though validation should have occurred.`;
|
|
29
|
+
|
|
30
|
+
case 'state_silent_failure':
|
|
31
|
+
return `The ${label} was clicked, but the expected state change did not happen.`;
|
|
32
|
+
|
|
33
|
+
case 'reality_dead_interaction':
|
|
34
|
+
return `The ${label} was clickable, but nothing happened after clicking it.`;
|
|
35
|
+
|
|
36
|
+
case 'partial_navigation_failure':
|
|
37
|
+
return `The ${label} started navigating, but did not reach the expected destination.`;
|
|
38
|
+
|
|
39
|
+
case 'silent_failure':
|
|
40
|
+
case 'flow_silent_failure':
|
|
41
|
+
return `The ${label} was clicked, but the expected result did not occur.`;
|
|
42
|
+
|
|
43
|
+
case 'observed_break':
|
|
44
|
+
return `The ${label} behaved differently than expected based on previous observations.`;
|
|
45
|
+
|
|
46
|
+
default:
|
|
47
|
+
return `An unexpected issue occurred with ${label}.`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate action hint for a finding.
|
|
53
|
+
* Returns: { recommendedAction: 'FIX' | 'REVIEW' | 'IGNORE', reason, suggestedNextStep }
|
|
54
|
+
*/
|
|
55
|
+
export function generateActionHint(finding, confidence) {
|
|
56
|
+
const findingType = finding.type || '';
|
|
57
|
+
// Handle both full confidence object and simple confidence object
|
|
58
|
+
const confidenceLevel = confidence?.level || finding.confidence?.level || 'UNKNOWN';
|
|
59
|
+
const expectationStrength = confidence?.factors?.expectationStrength ||
|
|
60
|
+
finding.confidence?.factors?.expectationStrength ||
|
|
61
|
+
'UNKNOWN';
|
|
62
|
+
|
|
63
|
+
// Rules:
|
|
64
|
+
// - PROVEN silent_failure (MEDIUM/HIGH) → FIX
|
|
65
|
+
// - reality_dead_interaction → REVIEW
|
|
66
|
+
// - coverage_gap → IGNORE
|
|
67
|
+
// - LOW confidence findings → REVIEW
|
|
68
|
+
// - HIGH confidence findings → FIX
|
|
69
|
+
|
|
70
|
+
if (findingType === 'reality_dead_interaction') {
|
|
71
|
+
return {
|
|
72
|
+
recommendedAction: 'REVIEW',
|
|
73
|
+
reason: 'This is a dead interaction that may be intentional or a design issue',
|
|
74
|
+
suggestedNextStep: 'Check if this button is supposed to do something. If yes, add a handler. If no, remove it or disable it.'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Coverage gaps are informational only
|
|
79
|
+
if (findingType.includes('coverage') || findingType === 'coverage_gap') {
|
|
80
|
+
return {
|
|
81
|
+
recommendedAction: 'IGNORE',
|
|
82
|
+
reason: 'This is a coverage gap, not a user-facing failure',
|
|
83
|
+
suggestedNextStep: 'No action needed - this indicates incomplete test coverage, not a broken feature'
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// PROVEN expectations with MEDIUM/HIGH confidence → FIX
|
|
88
|
+
if (expectationStrength === 'PROVEN' && (confidenceLevel === 'HIGH' || confidenceLevel === 'MEDIUM')) {
|
|
89
|
+
return {
|
|
90
|
+
recommendedAction: 'FIX',
|
|
91
|
+
reason: 'Your code explicitly promises this behavior, and evidence shows it failed',
|
|
92
|
+
suggestedNextStep: 'Fix the implementation to match what your code promises, or update the code if the promise changed'
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// HIGH confidence (even if not PROVEN) → FIX
|
|
97
|
+
if (confidenceLevel === 'HIGH') {
|
|
98
|
+
return {
|
|
99
|
+
recommendedAction: 'FIX',
|
|
100
|
+
reason: 'Strong evidence indicates this is a real failure',
|
|
101
|
+
suggestedNextStep: 'Investigate and fix the issue'
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// MEDIUM confidence → REVIEW
|
|
106
|
+
if (confidenceLevel === 'MEDIUM') {
|
|
107
|
+
return {
|
|
108
|
+
recommendedAction: 'REVIEW',
|
|
109
|
+
reason: 'Evidence suggests a failure, but some uncertainty remains',
|
|
110
|
+
suggestedNextStep: 'Review the finding and evidence to determine if this is a real issue or a false positive'
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// LOW confidence → REVIEW
|
|
115
|
+
if (confidenceLevel === 'LOW') {
|
|
116
|
+
return {
|
|
117
|
+
recommendedAction: 'REVIEW',
|
|
118
|
+
reason: 'Limited evidence - this may be a false positive',
|
|
119
|
+
suggestedNextStep: 'Review manually to confirm if this is a real issue'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Default for unknown
|
|
124
|
+
return {
|
|
125
|
+
recommendedAction: 'REVIEW',
|
|
126
|
+
reason: 'Insufficient information to determine action',
|
|
127
|
+
suggestedNextStep: 'Review the finding and evidence manually'
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Derive a confidence explanation structure for findings.
|
|
133
|
+
* Ensures required fields exist even if upstream confidence missed them.
|
|
134
|
+
*/
|
|
135
|
+
export function deriveConfidenceExplanation(confidence = {}, findingType = 'unknown') {
|
|
136
|
+
const base = confidence.confidenceExplanation || {};
|
|
137
|
+
const factors = confidence.factors || {};
|
|
138
|
+
const expectationStrength = factors.expectationStrength || 'UNKNOWN';
|
|
139
|
+
const sensorsPresent = factors.sensorsPresent || {};
|
|
140
|
+
|
|
141
|
+
const why = Array.isArray(base.whyThisConfidence) && base.whyThisConfidence.length > 0
|
|
142
|
+
? base.whyThisConfidence
|
|
143
|
+
: [
|
|
144
|
+
expectationStrength === 'PROVEN'
|
|
145
|
+
? 'Expectation is proven and failed in practice'
|
|
146
|
+
: `Expectation strength is ${expectationStrength}, adding uncertainty`,
|
|
147
|
+
sensorsPresent.network || sensorsPresent.console || sensorsPresent.ui
|
|
148
|
+
? 'Some evidence was captured from available sensors'
|
|
149
|
+
: 'Limited evidence captured from sensors'
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const whatIncrease = Array.isArray(base.whatWouldIncreaseConfidence) && base.whatWouldIncreaseConfidence.length > 0
|
|
153
|
+
? base.whatWouldIncreaseConfidence
|
|
154
|
+
: [
|
|
155
|
+
expectationStrength === 'PROVEN'
|
|
156
|
+
? 'Capture more sensor evidence (network/console/UI) for this interaction'
|
|
157
|
+
: 'Make this expectation PROVEN or capture more evidence to remove uncertainty'
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
const whatReduce = Array.isArray(base.whatWouldReduceConfidence) && base.whatWouldReduceConfidence.length > 0
|
|
161
|
+
? base.whatWouldReduceConfidence
|
|
162
|
+
: ['Confidence would drop if sensors were missing or expectation weakened'];
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
whyThisConfidence: dedupeStrings(why),
|
|
166
|
+
whatWouldIncreaseConfidence: dedupeStrings(whatIncrease),
|
|
167
|
+
whatWouldReduceConfidence: dedupeStrings(whatReduce),
|
|
168
|
+
expectationStrength,
|
|
169
|
+
sensorsPresent,
|
|
170
|
+
findingType
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function dedupeStrings(list = []) {
|
|
175
|
+
const seen = new Set();
|
|
176
|
+
const output = [];
|
|
177
|
+
for (const item of list) {
|
|
178
|
+
const val = String(item || '').trim();
|
|
179
|
+
if (!val) continue;
|
|
180
|
+
if (!seen.has(val)) {
|
|
181
|
+
seen.add(val);
|
|
182
|
+
output.push(val);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return output;
|
|
186
|
+
}
|
|
187
|
+
|