@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
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection Engine: Core of VERAX
|
|
3
|
+
* Compares learn.json and observe.json to produce evidence-backed findings
|
|
4
|
+
* with deterministic classification and confidence calculation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class DetectionEngine {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detect findings by comparing expectations with observations
|
|
14
|
+
* @param {Object} learnData - The learn.json output
|
|
15
|
+
* @param {Object} observeData - The observe.json output
|
|
16
|
+
* @returns {Object} Findings with classifications
|
|
17
|
+
*/
|
|
18
|
+
detect(learnData, observeData) {
|
|
19
|
+
if (!learnData || !observeData) {
|
|
20
|
+
throw new Error('Both learnData and observeData are required');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const expectations = learnData.expectations || [];
|
|
24
|
+
const observations = observeData.observations || [];
|
|
25
|
+
|
|
26
|
+
// Index observations for fast lookup
|
|
27
|
+
const observationMap = this._indexObservations(observations);
|
|
28
|
+
|
|
29
|
+
// Generate one finding per expectation
|
|
30
|
+
const findings = expectations.map((expectation) => {
|
|
31
|
+
return this._classifyExpectation(expectation, observationMap, observations);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Calculate stats
|
|
35
|
+
const stats = this._calculateStats(findings);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
findings,
|
|
39
|
+
stats,
|
|
40
|
+
detectedAt: new Date().toISOString(),
|
|
41
|
+
version: '1.0.0'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Index observations by expectation ID and promise for fast lookup
|
|
47
|
+
* @private
|
|
48
|
+
*/
|
|
49
|
+
_indexObservations(observations) {
|
|
50
|
+
const map = {
|
|
51
|
+
byId: {},
|
|
52
|
+
byPromise: {}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
observations.forEach((obs) => {
|
|
56
|
+
// Index by expectation ID if available
|
|
57
|
+
if (obs.expectationId) {
|
|
58
|
+
if (!map.byId[obs.expectationId]) {
|
|
59
|
+
map.byId[obs.expectationId] = [];
|
|
60
|
+
}
|
|
61
|
+
map.byId[obs.expectationId].push(obs);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Index by promise kind for coverage gap detection
|
|
65
|
+
if (obs.promise && obs.promise.kind) {
|
|
66
|
+
if (!map.byPromise[obs.promise.kind]) {
|
|
67
|
+
map.byPromise[obs.promise.kind] = [];
|
|
68
|
+
}
|
|
69
|
+
map.byPromise[obs.promise.kind].push(obs);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return map;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Classify a single expectation against observations
|
|
78
|
+
* @private
|
|
79
|
+
*/
|
|
80
|
+
_classifyExpectation(expectation, observationMap, _allObservations) {
|
|
81
|
+
const expectationId = expectation.id;
|
|
82
|
+
const promise = expectation.promise || {};
|
|
83
|
+
|
|
84
|
+
// Find direct observations for this expectation
|
|
85
|
+
const directObservations = observationMap.byId[expectationId] || [];
|
|
86
|
+
|
|
87
|
+
// Determine classification
|
|
88
|
+
let classification = 'informational'; // default
|
|
89
|
+
let matchedObservation = null;
|
|
90
|
+
let evidence = [];
|
|
91
|
+
|
|
92
|
+
if (directObservations.length > 0) {
|
|
93
|
+
// We have observations for this expectation
|
|
94
|
+
const obs = directObservations[0];
|
|
95
|
+
matchedObservation = obs;
|
|
96
|
+
|
|
97
|
+
if (obs.observed === true) {
|
|
98
|
+
classification = 'observed';
|
|
99
|
+
evidence = obs.evidence || [];
|
|
100
|
+
} else if (obs.attempted === true) {
|
|
101
|
+
classification = 'silent-failure';
|
|
102
|
+
evidence = obs.evidence || [];
|
|
103
|
+
} else {
|
|
104
|
+
classification = 'unproven';
|
|
105
|
+
evidence = obs.evidence || [];
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
// No direct observations - check if we attempted this type of observation
|
|
109
|
+
const relevantObservations =
|
|
110
|
+
observationMap.byPromise[promise.kind] || [];
|
|
111
|
+
const wasAttempted = relevantObservations.some((obs) => obs.attempted);
|
|
112
|
+
|
|
113
|
+
if (wasAttempted) {
|
|
114
|
+
// We attempted this type but didn't get this specific promise
|
|
115
|
+
classification = 'silent-failure';
|
|
116
|
+
} else {
|
|
117
|
+
// We never attempted to observe this type of promise
|
|
118
|
+
classification = 'coverage-gap';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Calculate deterministic confidence
|
|
123
|
+
const confidence = this._calculateConfidence(
|
|
124
|
+
expectation,
|
|
125
|
+
matchedObservation,
|
|
126
|
+
classification
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Determine impact
|
|
130
|
+
const impact = this._determineImpact(expectation, classification);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
id: expectation.id,
|
|
134
|
+
expectation: expectation,
|
|
135
|
+
classification,
|
|
136
|
+
confidence,
|
|
137
|
+
impact,
|
|
138
|
+
evidence,
|
|
139
|
+
matched: matchedObservation || null,
|
|
140
|
+
summary: this._generateSummary(expectation, classification),
|
|
141
|
+
details: {
|
|
142
|
+
expectationType: expectation.type,
|
|
143
|
+
promiseKind: promise.kind,
|
|
144
|
+
promiseValue: promise.value,
|
|
145
|
+
attemptedObservations: observationMap.byPromise[promise.kind]
|
|
146
|
+
? observationMap.byPromise[promise.kind].length
|
|
147
|
+
: 0
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Calculate deterministic confidence score (0-1)
|
|
154
|
+
* Based on: expectation confidence, observation evidence, classification
|
|
155
|
+
* @private
|
|
156
|
+
*/
|
|
157
|
+
_calculateConfidence(expectation, observation, classification) {
|
|
158
|
+
const baseConfidence = expectation.confidence || 0.5;
|
|
159
|
+
|
|
160
|
+
let multiplier = 1.0;
|
|
161
|
+
switch (classification) {
|
|
162
|
+
case 'observed':
|
|
163
|
+
// High confidence - we saw it
|
|
164
|
+
multiplier = 1.0;
|
|
165
|
+
break;
|
|
166
|
+
case 'silent-failure':
|
|
167
|
+
// Medium-high - we looked for it but didn't see it
|
|
168
|
+
multiplier = 0.75;
|
|
169
|
+
break;
|
|
170
|
+
case 'coverage-gap':
|
|
171
|
+
// Lower - we didn't even try to look
|
|
172
|
+
multiplier = 0.5;
|
|
173
|
+
break;
|
|
174
|
+
case 'unproven':
|
|
175
|
+
// Low - we tried but evidence insufficient
|
|
176
|
+
multiplier = 0.25;
|
|
177
|
+
break;
|
|
178
|
+
default:
|
|
179
|
+
multiplier = 0.5;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// If we have evidence, boost confidence slightly
|
|
183
|
+
if (observation && observation.evidence && observation.evidence.length > 0) {
|
|
184
|
+
multiplier = Math.min(1.0, multiplier + 0.1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const confidence = baseConfidence * multiplier;
|
|
188
|
+
return Math.round(confidence * 100) / 100; // Round to 2 decimals
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Determine impact level based on expectation criticality
|
|
193
|
+
* @private
|
|
194
|
+
*/
|
|
195
|
+
_determineImpact(expectation, classification) {
|
|
196
|
+
// If explicitly marked as critical
|
|
197
|
+
if (expectation.critical === true) {
|
|
198
|
+
return 'HIGH';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// If silent-failure on a navigation or network call, it's high impact
|
|
202
|
+
if (
|
|
203
|
+
classification === 'silent-failure' &&
|
|
204
|
+
(expectation.type === 'navigation' || expectation.type === 'network')
|
|
205
|
+
) {
|
|
206
|
+
return 'HIGH';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// State mutations are typically medium impact
|
|
210
|
+
if (expectation.type === 'state') {
|
|
211
|
+
return 'MEDIUM';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Coverage gaps are lower impact (we didn't even try)
|
|
215
|
+
if (classification === 'coverage-gap') {
|
|
216
|
+
return 'LOW';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return 'MEDIUM';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Generate human-readable summary
|
|
224
|
+
* @private
|
|
225
|
+
*/
|
|
226
|
+
_generateSummary(expectation, classification) {
|
|
227
|
+
const type = expectation.type || 'unknown';
|
|
228
|
+
const promise = expectation.promise || {};
|
|
229
|
+
const value = promise.value || 'unknown';
|
|
230
|
+
|
|
231
|
+
const summaries = {
|
|
232
|
+
'silent-failure': `Silent failure: ${type} "${value}" was learned but never observed during testing`,
|
|
233
|
+
'observed': `Success: ${type} "${value}" was verified during testing`,
|
|
234
|
+
'coverage-gap': `Coverage gap: ${type} "${value}" was never attempted during testing`,
|
|
235
|
+
'unproven': `Unproven: ${type} "${value}" was attempted but insufficient evidence`,
|
|
236
|
+
'informational': `Info: ${type} "${value}" was analyzed`
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
return summaries[classification] || summaries.informational;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Calculate statistics across all findings
|
|
244
|
+
* @private
|
|
245
|
+
*/
|
|
246
|
+
_calculateStats(findings) {
|
|
247
|
+
const stats = {
|
|
248
|
+
total: findings.length,
|
|
249
|
+
byClassification: {},
|
|
250
|
+
byImpact: {},
|
|
251
|
+
averageConfidence: 0,
|
|
252
|
+
highConfidence: 0,
|
|
253
|
+
mediumConfidence: 0,
|
|
254
|
+
lowConfidence: 0
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Initialize counters
|
|
258
|
+
['silent-failure', 'observed', 'coverage-gap', 'unproven', 'informational'].forEach(
|
|
259
|
+
(c) => {
|
|
260
|
+
stats.byClassification[c] = 0;
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
['HIGH', 'MEDIUM', 'LOW'].forEach((i) => {
|
|
264
|
+
stats.byImpact[i] = 0;
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Count and aggregate
|
|
268
|
+
let totalConfidence = 0;
|
|
269
|
+
findings.forEach((finding) => {
|
|
270
|
+
stats.byClassification[finding.classification]++;
|
|
271
|
+
stats.byImpact[finding.impact]++;
|
|
272
|
+
|
|
273
|
+
totalConfidence += finding.confidence;
|
|
274
|
+
|
|
275
|
+
if (finding.confidence >= 0.8) {
|
|
276
|
+
stats.highConfidence++;
|
|
277
|
+
} else if (finding.confidence >= 0.5) {
|
|
278
|
+
stats.mediumConfidence++;
|
|
279
|
+
} else {
|
|
280
|
+
stats.lowConfidence++;
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
stats.averageConfidence =
|
|
285
|
+
findings.length > 0
|
|
286
|
+
? Math.round((totalConfidence / findings.length) * 100) / 100
|
|
287
|
+
: 0;
|
|
288
|
+
|
|
289
|
+
return stats;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = DetectionEngine;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EVIDENCE INDEX MODULE
|
|
3
|
+
*
|
|
4
|
+
* Builds and validates evidence index from observation traces.
|
|
5
|
+
* Handles screenshot validation and missing evidence tracking via silence system.
|
|
6
|
+
*
|
|
7
|
+
* SILENCE INTEGRATION: Missing screenshot files are tracked as silence entries
|
|
8
|
+
* with scope='evidence', reason='evidence_missing', preserving full context.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// fs and path imports removed - currently unused
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build evidence index from observation traces with validation.
|
|
15
|
+
*
|
|
16
|
+
* @param {Array} traces - Observation traces from observe phase
|
|
17
|
+
* @returns {Object} { evidenceIndex, expectationEvidenceMap, findingEvidenceMap }
|
|
18
|
+
*/
|
|
19
|
+
export function buildEvidenceIndex(traces, _projectDir = null, _silenceTracker = null) {
|
|
20
|
+
const evidenceIndex = [];
|
|
21
|
+
const expectationEvidenceMap = new Map();
|
|
22
|
+
const findingEvidenceMap = new Map();
|
|
23
|
+
let id = 1;
|
|
24
|
+
|
|
25
|
+
if (!Array.isArray(traces)) {
|
|
26
|
+
return { evidenceIndex, expectationEvidenceMap, findingEvidenceMap };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Use fs/path for evidence validation if projectDir provided (currently unused)
|
|
30
|
+
// let existsSync = null;
|
|
31
|
+
// let resolvePath = null;
|
|
32
|
+
// if (projectDir && fs && path) {
|
|
33
|
+
// existsSync = fs.existsSync;
|
|
34
|
+
// resolvePath = path.resolve;
|
|
35
|
+
// }
|
|
36
|
+
|
|
37
|
+
for (const trace of traces) {
|
|
38
|
+
// Prefer modern trace schema: trace.before/trace.after
|
|
39
|
+
const beforeUrl = trace.before?.url ?? trace.evidence?.beforeUrl ?? null;
|
|
40
|
+
const afterUrl = trace.after?.url ?? trace.evidence?.afterUrl ?? null;
|
|
41
|
+
let beforeScreenshot = trace.before?.screenshot ?? trace.evidence?.beforeScreenshot ?? null;
|
|
42
|
+
let afterScreenshot = trace.after?.screenshot ?? trace.evidence?.afterScreenshot ?? null;
|
|
43
|
+
|
|
44
|
+
// PHASE 3: Evidence file validation removed - screenshots stored in .verax/runs/<runId>/evidence/
|
|
45
|
+
|
|
46
|
+
const entry = {
|
|
47
|
+
id: `ev-${id}`,
|
|
48
|
+
expectationId: trace.expectationId || null,
|
|
49
|
+
interaction: trace.interaction ? { ...trace.interaction } : null,
|
|
50
|
+
resultType: trace.resultType || 'UNKNOWN',
|
|
51
|
+
evidence: {
|
|
52
|
+
beforeUrl,
|
|
53
|
+
afterUrl,
|
|
54
|
+
beforeScreenshot,
|
|
55
|
+
afterScreenshot
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
evidenceIndex.push(entry);
|
|
60
|
+
|
|
61
|
+
if (trace.expectationId) {
|
|
62
|
+
expectationEvidenceMap.set(trace.expectationId, entry.id);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const selector = trace.interaction?.selector;
|
|
66
|
+
if (selector) {
|
|
67
|
+
findingEvidenceMap.set(selector, entry.id);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
id++;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { evidenceIndex, expectationEvidenceMap, findingEvidenceMap };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Write evidence index to artifacts directory.
|
|
78
|
+
* Maps findingId/expectationId to evidence paths (screenshots, traces, network logs).
|
|
79
|
+
*
|
|
80
|
+
* @param {string} projectDir - Project root directory
|
|
81
|
+
* @param {Array} evidenceIndex - Evidence index from buildEvidenceIndex
|
|
82
|
+
* @param {string} tracesPath - Path to observation-traces.json
|
|
83
|
+
* @param {string} findingsPath - Path to findings.json
|
|
84
|
+
* @returns {Promise<string>} Path to written evidence-index.json
|
|
85
|
+
*/
|
|
86
|
+
export async function writeEvidenceIndex(projectDir, evidenceIndex, tracesPath, findingsPath, runDirOpt) {
|
|
87
|
+
const { resolve } = await import('path');
|
|
88
|
+
const { mkdirSync, writeFileSync } = await import('fs');
|
|
89
|
+
|
|
90
|
+
if (!runDirOpt) {
|
|
91
|
+
throw new Error('runDirOpt is required');
|
|
92
|
+
}
|
|
93
|
+
const artifactsDir = resolve(runDirOpt, 'evidence');
|
|
94
|
+
mkdirSync(artifactsDir, { recursive: true });
|
|
95
|
+
|
|
96
|
+
const evidenceIndexPath = resolve(artifactsDir, 'evidence-index.json');
|
|
97
|
+
|
|
98
|
+
// Build evidence index with full paths
|
|
99
|
+
const index = {
|
|
100
|
+
version: 1,
|
|
101
|
+
generatedAt: new Date().toISOString(),
|
|
102
|
+
tracesPath: tracesPath,
|
|
103
|
+
findingsPath: findingsPath,
|
|
104
|
+
evidence: evidenceIndex.map(entry => ({
|
|
105
|
+
id: entry.id,
|
|
106
|
+
expectationId: entry.expectationId,
|
|
107
|
+
interaction: entry.interaction ? {
|
|
108
|
+
type: entry.interaction.type,
|
|
109
|
+
selector: entry.interaction.selector,
|
|
110
|
+
label: entry.interaction.label
|
|
111
|
+
} : null,
|
|
112
|
+
resultType: entry.resultType,
|
|
113
|
+
expectationOutcome: entry.expectationOutcome,
|
|
114
|
+
evidence: {
|
|
115
|
+
beforeUrl: entry.evidence.beforeUrl,
|
|
116
|
+
afterUrl: entry.evidence.afterUrl,
|
|
117
|
+
beforeScreenshot: entry.evidence.beforeScreenshot,
|
|
118
|
+
afterScreenshot: entry.evidence.afterScreenshot,
|
|
119
|
+
traceFile: tracesPath // All traces are in the same file
|
|
120
|
+
}
|
|
121
|
+
}))
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
writeFileSync(evidenceIndexPath, JSON.stringify(index, null, 2) + '\n');
|
|
125
|
+
|
|
126
|
+
return evidenceIndexPath;
|
|
127
|
+
}
|