@veraxhq/verax 0.1.0 → 0.2.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 +123 -88
- package/bin/verax.js +11 -452
- package/package.json +14 -36
- package/src/cli/commands/default.js +523 -0
- package/src/cli/commands/doctor.js +165 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +402 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +296 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +34 -0
- package/src/cli/util/expectation-extractor.js +378 -0
- package/src/cli/util/findings-writer.js +31 -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 +366 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +29 -0
- package/src/cli/util/project-discovery.js +277 -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/summary-writer.js +32 -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 +101 -0
- package/src/verax/cli/wizard.js +98 -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 +403 -0
- package/src/verax/core/incremental-store.js +237 -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 +521 -0
- package/src/verax/detect/comparison.js +2 -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 +177 -0
- package/src/verax/detect/expectation-model.js +194 -172
- 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 +44 -8
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +172 -286
- package/src/verax/detect/interactive-findings.js +613 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/verdict-engine.js +563 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/index.js +90 -14
- 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 +579 -0
- package/src/verax/intel/vue-router-extractor.js +323 -0
- package/src/verax/learn/action-contract-extractor.js +335 -101
- package/src/verax/learn/ast-contract-extractor.js +95 -5
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/manifest-writer.js +97 -47
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +27 -96
- 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 +112 -4
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +10 -5
- package/src/verax/observe/console-sensor.js +1 -17
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +512 -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 +643 -275
- package/src/verax/observe/index.js +908 -27
- package/src/verax/observe/index.js.backup +1 -0
- package/src/verax/observe/interaction-discovery.js +365 -14
- package/src/verax/observe/interaction-runner.js +563 -198
- package/src/verax/observe/loading-sensor.js +139 -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 +37 -17
- package/src/verax/observe/state-sensor.js +389 -0
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +61 -20
- package/src/verax/observe/ui-signal-sensor.js +136 -17
- package/src/verax/scan-summary-writer.js +77 -15
- package/src/verax/shared/artifact-manager.js +110 -8
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +170 -0
- package/src/verax/shared/dynamic-route-utils.js +218 -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 +14 -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 +65 -0
- package/src/verax/validate/context-validator.js +244 -0
- package/src/verax/validate/context-validator.js.bak +0 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding Detector - Pure finding creation from trace analysis
|
|
3
|
+
*
|
|
4
|
+
* Contains logic to create finding objects from:
|
|
5
|
+
* - Expectation-driven execution outcomes
|
|
6
|
+
* - Observed expectation breaks
|
|
7
|
+
* - Flow failures
|
|
8
|
+
*
|
|
9
|
+
* PHASE 2: All findings include explicit outcome classification and purely factual wording.
|
|
10
|
+
* PHASE 3: All findings include Promise awareness - which promise was evaluated/unmet.
|
|
11
|
+
*
|
|
12
|
+
* All functions are pure (no file I/O, no side effects).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { hasMeaningfulUrlChange, hasVisibleChange, hasDomChange } from './comparison.js';
|
|
16
|
+
import { computeConfidence } from './confidence-engine.js';
|
|
17
|
+
import { generateHumanSummary, generateActionHint, deriveConfidenceExplanation } from './explanation-helpers.js';
|
|
18
|
+
import { mapFindingTypeToOutcome } from '../core/canonical-outcomes.js';
|
|
19
|
+
import { inferPromiseFromInteraction, PROMISE_TYPES } from '../core/promise-model.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Map finding type to confidence engine type.
|
|
23
|
+
*/
|
|
24
|
+
export function mapFindingTypeToConfidenceType(findingType) {
|
|
25
|
+
if (findingType === 'silent_failure') {
|
|
26
|
+
return 'no_effect_silent_failure';
|
|
27
|
+
}
|
|
28
|
+
if (findingType === 'flow_silent_failure') {
|
|
29
|
+
return 'no_effect_silent_failure';
|
|
30
|
+
}
|
|
31
|
+
if (findingType === 'observed_break') {
|
|
32
|
+
return 'no_effect_silent_failure';
|
|
33
|
+
}
|
|
34
|
+
return findingType;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compute confidence for a finding.
|
|
39
|
+
*/
|
|
40
|
+
export function computeFindingConfidence(finding, matchedExpectation, trace, comparisons) {
|
|
41
|
+
const sensors = trace.sensors || {};
|
|
42
|
+
const findingType = mapFindingTypeToConfidenceType(finding.type);
|
|
43
|
+
|
|
44
|
+
const confidence = computeConfidence({
|
|
45
|
+
findingType,
|
|
46
|
+
expectation: matchedExpectation || {},
|
|
47
|
+
sensors: {
|
|
48
|
+
network: sensors.network || {},
|
|
49
|
+
console: sensors.console || {},
|
|
50
|
+
uiSignals: sensors.uiSignals || {}
|
|
51
|
+
},
|
|
52
|
+
comparisons: {
|
|
53
|
+
hasUrlChange: comparisons.hasUrlChange || false,
|
|
54
|
+
hasDomChange: comparisons.hasDomChange || false,
|
|
55
|
+
hasVisibleChange: comparisons.hasVisibleChange || false
|
|
56
|
+
},
|
|
57
|
+
attemptMeta: {}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return confidence;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Enrich finding with Phase 9 explanation fields: humanSummary, actionHint.
|
|
65
|
+
*/
|
|
66
|
+
export function enrichFindingWithExplanations(finding, trace) {
|
|
67
|
+
// Add human summary
|
|
68
|
+
finding.humanSummary = generateHumanSummary(finding, trace);
|
|
69
|
+
|
|
70
|
+
// Add action hint
|
|
71
|
+
finding.actionHint = generateActionHint(finding, finding.confidence);
|
|
72
|
+
|
|
73
|
+
// Add confidence explanation (Phase 9)
|
|
74
|
+
finding.confidenceExplanation = deriveConfidenceExplanation(
|
|
75
|
+
finding.confidence || {},
|
|
76
|
+
finding.type
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return finding;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* PHASE 3: Bind Promise to finding
|
|
84
|
+
*
|
|
85
|
+
* Infers the promise from the interaction context and attaches it to the finding.
|
|
86
|
+
* All findings must have a promise descriptor.
|
|
87
|
+
*/
|
|
88
|
+
export function bindPromiseToFinding(finding, trace) {
|
|
89
|
+
const interaction = trace.interaction || {};
|
|
90
|
+
|
|
91
|
+
// Infer promise from interaction type/label
|
|
92
|
+
const promise = inferPromiseFromInteraction(interaction);
|
|
93
|
+
|
|
94
|
+
if (promise) {
|
|
95
|
+
finding.promise = promise;
|
|
96
|
+
} else {
|
|
97
|
+
// No promise could be inferred - mark as unproven
|
|
98
|
+
finding.promise = {
|
|
99
|
+
type: 'UNPROVEN_INTERACTION',
|
|
100
|
+
source: 'unknown',
|
|
101
|
+
expected_signal: 'Unknown',
|
|
102
|
+
reason: 'Could not infer promise from interaction type'
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return finding;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create finding from expectation-driven execution outcome.
|
|
111
|
+
* Called when expectation execution resulted in SILENT_FAILURE.
|
|
112
|
+
*/
|
|
113
|
+
export function createFindingFromExpectationOutcome(expectation, trace, beforeUrl, afterUrl, beforeScreenshot, afterScreenshot, projectDir, manifest) {
|
|
114
|
+
const interaction = trace.interaction;
|
|
115
|
+
const sensors = trace.sensors || {};
|
|
116
|
+
|
|
117
|
+
if (expectation.type === 'navigation' || expectation.type === 'spa_navigation') {
|
|
118
|
+
const finding = {
|
|
119
|
+
outcome: mapFindingTypeToOutcome('navigation_silent_failure'),
|
|
120
|
+
type: 'navigation_silent_failure',
|
|
121
|
+
interaction: {
|
|
122
|
+
type: interaction.type,
|
|
123
|
+
selector: interaction.selector,
|
|
124
|
+
label: interaction.label
|
|
125
|
+
},
|
|
126
|
+
what_happened: 'User action executed',
|
|
127
|
+
what_was_expected: `Navigation to ${expectation.targetPath || expectation.expectedTarget || 'target URL'}`,
|
|
128
|
+
what_was_observed: 'No URL change, no user-visible feedback provided',
|
|
129
|
+
why_it_matters: 'User took action expecting navigation but received no confirmation of success or failure',
|
|
130
|
+
evidence: {
|
|
131
|
+
before: beforeScreenshot,
|
|
132
|
+
after: afterScreenshot,
|
|
133
|
+
beforeUrl: beforeUrl,
|
|
134
|
+
afterUrl: afterUrl,
|
|
135
|
+
expectedTarget: expectation.targetPath || expectation.expectedTarget || '',
|
|
136
|
+
targetReached: false,
|
|
137
|
+
urlChanged: sensors.navigation?.urlChanged === true,
|
|
138
|
+
historyLengthDelta: sensors.navigation?.historyLengthDelta || 0,
|
|
139
|
+
uiFeedback: sensors.uiSignals?.diff?.changed === true
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const hasUrlChange = sensors.navigation?.urlChanged === true || hasMeaningfulUrlChange(beforeUrl, afterUrl);
|
|
144
|
+
finding.confidence = computeFindingConfidence(
|
|
145
|
+
finding,
|
|
146
|
+
expectation,
|
|
147
|
+
trace,
|
|
148
|
+
{ hasUrlChange, hasDomChange: false, hasVisibleChange: false }
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
bindPromiseToFinding(finding, trace);
|
|
152
|
+
enrichFindingWithExplanations(finding, trace);
|
|
153
|
+
return finding;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (expectation.type === 'network_action') {
|
|
157
|
+
const networkData = sensors.network || {};
|
|
158
|
+
const hasRequest = networkData.totalRequests > 0;
|
|
159
|
+
const hasFailed = networkData.failedRequests > 0;
|
|
160
|
+
|
|
161
|
+
if (!hasRequest) {
|
|
162
|
+
const finding = {
|
|
163
|
+
outcome: mapFindingTypeToOutcome('missing_network_action'),
|
|
164
|
+
type: 'missing_network_action',
|
|
165
|
+
interaction: {
|
|
166
|
+
type: interaction.type,
|
|
167
|
+
selector: interaction.selector,
|
|
168
|
+
label: interaction.label
|
|
169
|
+
},
|
|
170
|
+
what_happened: 'User action executed',
|
|
171
|
+
what_was_expected: `Network request to ${expectation.expectedTarget || expectation.urlPath || 'endpoint'} (${expectation.method || 'GET'})`,
|
|
172
|
+
what_was_observed: 'No network request was made',
|
|
173
|
+
why_it_matters: 'User took action expecting server communication but request was never sent',
|
|
174
|
+
evidence: {
|
|
175
|
+
before: beforeScreenshot,
|
|
176
|
+
after: afterScreenshot,
|
|
177
|
+
beforeUrl: beforeUrl,
|
|
178
|
+
afterUrl: afterUrl,
|
|
179
|
+
expectedEndpoint: expectation.expectedTarget || expectation.urlPath || '',
|
|
180
|
+
expectedMethod: expectation.method || 'GET',
|
|
181
|
+
totalRequests: networkData.totalRequests
|
|
182
|
+
},
|
|
183
|
+
confidence: computeFindingConfidence(
|
|
184
|
+
{ type: 'missing_network_action' },
|
|
185
|
+
expectation,
|
|
186
|
+
trace,
|
|
187
|
+
{ hasUrlChange: false, hasDomChange: false, hasVisibleChange: false }
|
|
188
|
+
)
|
|
189
|
+
};
|
|
190
|
+
bindPromiseToFinding(finding, trace);
|
|
191
|
+
enrichFindingWithExplanations(finding, trace);
|
|
192
|
+
return finding;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (hasFailed && !sensors.uiSignals?.diff?.changed) {
|
|
196
|
+
const finding = {
|
|
197
|
+
outcome: mapFindingTypeToOutcome('network_silent_failure'),
|
|
198
|
+
type: 'network_silent_failure',
|
|
199
|
+
interaction: {
|
|
200
|
+
type: interaction.type,
|
|
201
|
+
selector: interaction.selector,
|
|
202
|
+
label: interaction.label
|
|
203
|
+
},
|
|
204
|
+
what_happened: 'User action executed; network request was sent but failed',
|
|
205
|
+
what_was_expected: `Request to ${expectation.expectedTarget || expectation.urlPath || 'endpoint'} succeeds with user feedback`,
|
|
206
|
+
what_was_observed: 'Request failed but no user-visible feedback provided',
|
|
207
|
+
why_it_matters: 'User took action expecting server communication; request failed but they received no error notification',
|
|
208
|
+
evidence: {
|
|
209
|
+
before: beforeScreenshot,
|
|
210
|
+
after: afterScreenshot,
|
|
211
|
+
beforeUrl: beforeUrl,
|
|
212
|
+
afterUrl: afterUrl,
|
|
213
|
+
expectedEndpoint: expectation.expectedTarget || expectation.urlPath || '',
|
|
214
|
+
expectedMethod: expectation.method || 'GET',
|
|
215
|
+
failedRequests: networkData.failedRequests,
|
|
216
|
+
uiFeedback: false
|
|
217
|
+
},
|
|
218
|
+
confidence: computeFindingConfidence(
|
|
219
|
+
{ type: 'network_silent_failure' },
|
|
220
|
+
expectation,
|
|
221
|
+
trace,
|
|
222
|
+
{ hasUrlChange: false, hasDomChange: false, hasVisibleChange: false }
|
|
223
|
+
)
|
|
224
|
+
};
|
|
225
|
+
bindPromiseToFinding(finding, trace);
|
|
226
|
+
enrichFindingWithExplanations(finding, trace);
|
|
227
|
+
return finding;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (expectation.type === 'validation_block') {
|
|
232
|
+
const finding = {
|
|
233
|
+
outcome: mapFindingTypeToOutcome('validation_silent_failure'),
|
|
234
|
+
type: 'validation_silent_failure',
|
|
235
|
+
interaction: {
|
|
236
|
+
type: interaction.type,
|
|
237
|
+
selector: interaction.selector,
|
|
238
|
+
label: interaction.label
|
|
239
|
+
},
|
|
240
|
+
what_happened: 'User submitted form',
|
|
241
|
+
what_was_expected: 'Form validation fails with user-visible error message',
|
|
242
|
+
what_was_observed: 'Form submission was blocked but no user-visible feedback provided',
|
|
243
|
+
why_it_matters: 'User submitted form but received no indication why the submission was rejected',
|
|
244
|
+
evidence: {
|
|
245
|
+
before: beforeScreenshot,
|
|
246
|
+
after: afterScreenshot,
|
|
247
|
+
beforeUrl: beforeUrl,
|
|
248
|
+
afterUrl: afterUrl,
|
|
249
|
+
urlChanged: false,
|
|
250
|
+
networkRequests: sensors.network?.totalRequests || 0,
|
|
251
|
+
validationFeedbackDetected: sensors.uiSignals?.after?.validationFeedbackDetected === true,
|
|
252
|
+
sourceRef: expectation.sourceRef,
|
|
253
|
+
handlerRef: expectation.handlerRef
|
|
254
|
+
},
|
|
255
|
+
confidence: computeFindingConfidence(
|
|
256
|
+
{ type: 'validation_silent_failure' },
|
|
257
|
+
expectation,
|
|
258
|
+
trace,
|
|
259
|
+
{ hasUrlChange: false, hasDomChange: false, hasVisibleChange: false }
|
|
260
|
+
)
|
|
261
|
+
};
|
|
262
|
+
bindPromiseToFinding(finding, trace);
|
|
263
|
+
enrichFindingWithExplanations(finding, trace);
|
|
264
|
+
return finding;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (expectation.type === 'state_action') {
|
|
268
|
+
const finding = {
|
|
269
|
+
outcome: mapFindingTypeToOutcome('missing_state_action'),
|
|
270
|
+
type: 'missing_state_action',
|
|
271
|
+
interaction: {
|
|
272
|
+
type: interaction.type,
|
|
273
|
+
selector: interaction.selector,
|
|
274
|
+
label: interaction.label
|
|
275
|
+
},
|
|
276
|
+
what_happened: 'User action executed',
|
|
277
|
+
what_was_expected: `Application state changes to: ${expectation.expectedTarget || ''} with user-visible feedback`,
|
|
278
|
+
what_was_observed: 'State did not change, or changed without user-visible feedback',
|
|
279
|
+
why_it_matters: 'User took action expecting app state to change but saw no confirmation',
|
|
280
|
+
evidence: {
|
|
281
|
+
before: beforeScreenshot,
|
|
282
|
+
after: afterScreenshot,
|
|
283
|
+
beforeUrl: beforeUrl,
|
|
284
|
+
afterUrl: afterUrl,
|
|
285
|
+
expectedStateKey: expectation.expectedTarget || '',
|
|
286
|
+
stateChanged: sensors.state?.changed?.length > 0,
|
|
287
|
+
stateKeysChanged: sensors.state?.changed || [],
|
|
288
|
+
uiFeedback: sensors.uiSignals?.diff?.changed === true
|
|
289
|
+
},
|
|
290
|
+
confidence: computeFindingConfidence(
|
|
291
|
+
{ type: 'missing_state_action' },
|
|
292
|
+
expectation,
|
|
293
|
+
trace,
|
|
294
|
+
{ hasUrlChange: false, hasDomChange: false, hasVisibleChange: false }
|
|
295
|
+
)
|
|
296
|
+
};
|
|
297
|
+
bindPromiseToFinding(finding, trace);
|
|
298
|
+
enrichFindingWithExplanations(finding, trace);
|
|
299
|
+
return finding;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Generic silent failure
|
|
303
|
+
const finding = {
|
|
304
|
+
outcome: mapFindingTypeToOutcome('silent_failure'),
|
|
305
|
+
type: 'silent_failure',
|
|
306
|
+
interaction: {
|
|
307
|
+
type: interaction.type,
|
|
308
|
+
selector: interaction.selector,
|
|
309
|
+
label: interaction.label
|
|
310
|
+
},
|
|
311
|
+
what_happened: 'User action executed',
|
|
312
|
+
what_was_expected: 'User-visible change (URL, DOM, visual state)',
|
|
313
|
+
what_was_observed: 'No observable change occurred',
|
|
314
|
+
why_it_matters: 'User took action but system provided no visible confirmation of success or failure',
|
|
315
|
+
evidence: {
|
|
316
|
+
before: beforeScreenshot,
|
|
317
|
+
after: afterScreenshot,
|
|
318
|
+
beforeUrl: beforeUrl,
|
|
319
|
+
afterUrl: afterUrl
|
|
320
|
+
},
|
|
321
|
+
confidence: computeFindingConfidence(
|
|
322
|
+
{ type: 'silent_failure' },
|
|
323
|
+
expectation,
|
|
324
|
+
trace,
|
|
325
|
+
{
|
|
326
|
+
hasUrlChange: hasMeaningfulUrlChange(beforeUrl, afterUrl),
|
|
327
|
+
hasDomChange: hasDomChange(trace),
|
|
328
|
+
hasVisibleChange: hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir)
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
};
|
|
332
|
+
bindPromiseToFinding(finding, trace);
|
|
333
|
+
enrichFindingWithExplanations(finding, trace);
|
|
334
|
+
return finding;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Create finding from observed expectation that didn't repeat.
|
|
339
|
+
*/
|
|
340
|
+
export function createObservedBreakFinding(trace, projectDir) {
|
|
341
|
+
const obs = trace.observedExpectation;
|
|
342
|
+
const interaction = trace.interaction || {};
|
|
343
|
+
const beforeUrl = trace.before?.url;
|
|
344
|
+
const afterUrl = trace.after?.url;
|
|
345
|
+
const beforeScreenshot = trace.before?.screenshot;
|
|
346
|
+
const afterScreenshot = trace.after?.screenshot;
|
|
347
|
+
const sensors = trace.sensors || {};
|
|
348
|
+
|
|
349
|
+
const comparisons = {
|
|
350
|
+
hasUrlChange: hasMeaningfulUrlChange(beforeUrl, afterUrl),
|
|
351
|
+
hasDomChange: hasDomChange(trace),
|
|
352
|
+
hasVisibleChange: hasVisibleChange(beforeScreenshot, afterScreenshot, projectDir)
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const finding = {
|
|
356
|
+
outcome: mapFindingTypeToOutcome('observed_break'),
|
|
357
|
+
type: 'observed_break',
|
|
358
|
+
interaction: {
|
|
359
|
+
type: interaction.type,
|
|
360
|
+
selector: interaction.selector,
|
|
361
|
+
label: interaction.label
|
|
362
|
+
},
|
|
363
|
+
what_happened: 'User interaction was executed',
|
|
364
|
+
what_was_expected: `Previously observed outcome repeats: ${obs?.reason || 'observable change'}`,
|
|
365
|
+
what_was_observed: 'Same user interaction this time produced a different outcome',
|
|
366
|
+
why_it_matters: 'Interaction behavior changed between executions: system is non-deterministic',
|
|
367
|
+
evidence: {
|
|
368
|
+
before: beforeScreenshot,
|
|
369
|
+
after: afterScreenshot,
|
|
370
|
+
beforeUrl,
|
|
371
|
+
afterUrl,
|
|
372
|
+
observedExpectation: obs
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
finding.confidence = computeConfidence({
|
|
377
|
+
findingType: mapFindingTypeToConfidenceType('observed_break'),
|
|
378
|
+
expectation: obs || { expectationStrength: 'OBSERVED' },
|
|
379
|
+
sensors: {
|
|
380
|
+
network: sensors.network || {},
|
|
381
|
+
console: sensors.console || {},
|
|
382
|
+
uiSignals: sensors.uiSignals || {}
|
|
383
|
+
},
|
|
384
|
+
comparisons,
|
|
385
|
+
attemptMeta: { repeated: obs?.repeated === true }
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
bindPromiseToFinding(finding, trace);
|
|
389
|
+
enrichFindingWithExplanations(finding, trace);
|
|
390
|
+
return finding;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Compute confidence for OBSERVED expectations with strict caps.
|
|
395
|
+
* OBSERVED can be at most MEDIUM unless repeated twice.
|
|
396
|
+
*/
|
|
397
|
+
export function computeObservedConfidence(finding, observedExp, trace, comparisons, repeated) {
|
|
398
|
+
// Base score for OBSERVED expectations (lower than PROVEN)
|
|
399
|
+
let baseScore = 50;
|
|
400
|
+
|
|
401
|
+
// Boost if repeated (confirms consistency)
|
|
402
|
+
if (repeated) {
|
|
403
|
+
baseScore += 15; // Boost for repetition
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Apply sensor presence penalties/boosts
|
|
407
|
+
const sensors = trace.sensors || {};
|
|
408
|
+
const sensorsPresent = {
|
|
409
|
+
network: Object.keys(sensors.network || {}).length > 0,
|
|
410
|
+
console: Object.keys(sensors.console || {}).length > 0,
|
|
411
|
+
ui: Object.keys(sensors.uiSignals || {}).length > 0
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const allSensorsPresent = sensorsPresent.network && sensorsPresent.console && sensorsPresent.ui;
|
|
415
|
+
|
|
416
|
+
if (!allSensorsPresent) {
|
|
417
|
+
baseScore -= 10; // Penalty for missing sensors
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Clamp score
|
|
421
|
+
let score = Math.max(0, Math.min(100, baseScore));
|
|
422
|
+
|
|
423
|
+
// Determine level with strict caps
|
|
424
|
+
let level = 'LOW';
|
|
425
|
+
|
|
426
|
+
if (score >= 55 && repeated) {
|
|
427
|
+
// MEDIUM only if repeated
|
|
428
|
+
level = 'MEDIUM';
|
|
429
|
+
score = Math.min(score, 70); // Cap at 70 for OBSERVED
|
|
430
|
+
} else if (score >= 55) {
|
|
431
|
+
// Without repetition, cap at LOW
|
|
432
|
+
level = 'LOW';
|
|
433
|
+
score = Math.min(score, 54);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
score: Math.round(score),
|
|
438
|
+
level,
|
|
439
|
+
explain: [
|
|
440
|
+
`Expectation strength: OBSERVED (runtime-derived)`,
|
|
441
|
+
repeated ? 'Confirmed by repetition' : 'Single observation (not repeated)',
|
|
442
|
+
allSensorsPresent ? 'All sensors present' : 'Some sensors missing'
|
|
443
|
+
],
|
|
444
|
+
factors: {
|
|
445
|
+
expectationStrength: 'OBSERVED',
|
|
446
|
+
repeated: repeated,
|
|
447
|
+
sensorsPresent: sensorsPresent
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
@@ -1,28 +1,64 @@
|
|
|
1
|
-
import { resolve } from 'path';
|
|
1
|
+
import { resolve, dirname } from 'path';
|
|
2
2
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
3
|
+
import { getArtifactPath } from '../core/run-id.js';
|
|
4
|
+
import { CANONICAL_OUTCOMES } from '../core/canonical-outcomes.js';
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Write findings to canonical artifact root. If a runDir can be inferred,
|
|
8
|
+
* write to .verax/runs/<runId>/findings.json; otherwise fall back to legacy path.
|
|
9
|
+
*
|
|
10
|
+
* PHASE 2: Includes outcome classification summary.
|
|
11
|
+
* PHASE 3: Includes promise type summary.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} projectDir
|
|
14
|
+
* @param {string} url
|
|
15
|
+
* @param {Array} findings
|
|
16
|
+
* @param {Array} coverageGaps
|
|
17
|
+
* @param {string|null} runDirOpt - Optional absolute run directory path
|
|
18
|
+
*/
|
|
19
|
+
export function writeFindings(projectDir, url, findings, coverageGaps = [], runDirOpt = null) {
|
|
5
20
|
let findingsPath;
|
|
6
|
-
if (
|
|
7
|
-
//
|
|
8
|
-
|
|
21
|
+
if (runDirOpt) {
|
|
22
|
+
// Canonical location
|
|
23
|
+
mkdirSync(runDirOpt, { recursive: true });
|
|
24
|
+
findingsPath = resolve(runDirOpt, 'findings.json');
|
|
9
25
|
} else {
|
|
10
|
-
// Legacy
|
|
26
|
+
// Legacy location for backward compatibility (tests without runId)
|
|
11
27
|
const detectDir = resolve(projectDir, '.veraxverax', 'detect');
|
|
12
28
|
mkdirSync(detectDir, { recursive: true });
|
|
13
29
|
findingsPath = resolve(detectDir, 'findings.json');
|
|
14
30
|
}
|
|
31
|
+
|
|
32
|
+
// PHASE 2: Compute outcome summary
|
|
33
|
+
const outcomeSummary = {};
|
|
34
|
+
Object.values(CANONICAL_OUTCOMES).forEach(outcome => {
|
|
35
|
+
outcomeSummary[outcome] = 0;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// PHASE 3: Compute promise summary
|
|
39
|
+
const promiseSummary = {};
|
|
15
40
|
|
|
41
|
+
for (const finding of (findings || [])) {
|
|
42
|
+
const outcome = finding.outcome || CANONICAL_OUTCOMES.SILENT_FAILURE;
|
|
43
|
+
outcomeSummary[outcome] = (outcomeSummary[outcome] || 0) + 1;
|
|
44
|
+
|
|
45
|
+
const promiseType = finding.promise?.type || 'UNKNOWN_PROMISE';
|
|
46
|
+
promiseSummary[promiseType] = (promiseSummary[promiseType] || 0) + 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
16
49
|
const findingsReport = {
|
|
17
50
|
version: 1,
|
|
18
51
|
detectedAt: new Date().toISOString(),
|
|
19
52
|
url: url,
|
|
53
|
+
outcomeSummary: outcomeSummary, // PHASE 2
|
|
54
|
+
promiseSummary: promiseSummary, // PHASE 3
|
|
20
55
|
findings: findings,
|
|
56
|
+
coverageGaps: coverageGaps,
|
|
21
57
|
notes: []
|
|
22
58
|
};
|
|
23
|
-
|
|
59
|
+
|
|
24
60
|
writeFileSync(findingsPath, JSON.stringify(findingsReport, null, 2) + '\n');
|
|
25
|
-
|
|
61
|
+
|
|
26
62
|
return {
|
|
27
63
|
...findingsReport,
|
|
28
64
|
findingsPath: findingsPath
|