@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.
Files changed (135) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +24 -36
  4. package/src/cli/commands/default.js +681 -0
  5. package/src/cli/commands/doctor.js +197 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +586 -0
  8. package/src/cli/entry.js +196 -0
  9. package/src/cli/util/atomic-write.js +37 -0
  10. package/src/cli/util/detection-engine.js +297 -0
  11. package/src/cli/util/env-url.js +33 -0
  12. package/src/cli/util/errors.js +44 -0
  13. package/src/cli/util/events.js +110 -0
  14. package/src/cli/util/expectation-extractor.js +388 -0
  15. package/src/cli/util/findings-writer.js +32 -0
  16. package/src/cli/util/idgen.js +87 -0
  17. package/src/cli/util/learn-writer.js +39 -0
  18. package/src/cli/util/observation-engine.js +412 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +30 -0
  21. package/src/cli/util/project-discovery.js +297 -0
  22. package/src/cli/util/project-writer.js +26 -0
  23. package/src/cli/util/redact.js +128 -0
  24. package/src/cli/util/run-id.js +30 -0
  25. package/src/cli/util/runtime-budget.js +147 -0
  26. package/src/cli/util/summary-writer.js +43 -0
  27. package/src/types/global.d.ts +28 -0
  28. package/src/types/ts-ast.d.ts +24 -0
  29. package/src/verax/cli/ci-summary.js +35 -0
  30. package/src/verax/cli/context-explanation.js +89 -0
  31. package/src/verax/cli/doctor.js +277 -0
  32. package/src/verax/cli/error-normalizer.js +154 -0
  33. package/src/verax/cli/explain-output.js +105 -0
  34. package/src/verax/cli/finding-explainer.js +130 -0
  35. package/src/verax/cli/init.js +237 -0
  36. package/src/verax/cli/run-overview.js +163 -0
  37. package/src/verax/cli/url-safety.js +111 -0
  38. package/src/verax/cli/wizard.js +109 -0
  39. package/src/verax/cli/zero-findings-explainer.js +57 -0
  40. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  41. package/src/verax/core/action-classifier.js +86 -0
  42. package/src/verax/core/budget-engine.js +218 -0
  43. package/src/verax/core/canonical-outcomes.js +157 -0
  44. package/src/verax/core/decision-snapshot.js +335 -0
  45. package/src/verax/core/determinism-model.js +432 -0
  46. package/src/verax/core/incremental-store.js +245 -0
  47. package/src/verax/core/invariants.js +356 -0
  48. package/src/verax/core/promise-model.js +230 -0
  49. package/src/verax/core/replay-validator.js +350 -0
  50. package/src/verax/core/replay.js +222 -0
  51. package/src/verax/core/run-id.js +175 -0
  52. package/src/verax/core/run-manifest.js +99 -0
  53. package/src/verax/core/silence-impact.js +369 -0
  54. package/src/verax/core/silence-model.js +523 -0
  55. package/src/verax/detect/comparison.js +7 -34
  56. package/src/verax/detect/confidence-engine.js +764 -329
  57. package/src/verax/detect/detection-engine.js +293 -0
  58. package/src/verax/detect/evidence-index.js +127 -0
  59. package/src/verax/detect/expectation-model.js +241 -168
  60. package/src/verax/detect/explanation-helpers.js +187 -0
  61. package/src/verax/detect/finding-detector.js +450 -0
  62. package/src/verax/detect/findings-writer.js +41 -12
  63. package/src/verax/detect/flow-detector.js +366 -0
  64. package/src/verax/detect/index.js +200 -288
  65. package/src/verax/detect/interactive-findings.js +612 -0
  66. package/src/verax/detect/signal-mapper.js +308 -0
  67. package/src/verax/detect/skip-classifier.js +4 -4
  68. package/src/verax/detect/verdict-engine.js +561 -0
  69. package/src/verax/evidence-index-writer.js +61 -0
  70. package/src/verax/flow/flow-engine.js +3 -2
  71. package/src/verax/flow/flow-spec.js +1 -2
  72. package/src/verax/index.js +103 -15
  73. package/src/verax/intel/effect-detector.js +368 -0
  74. package/src/verax/intel/handler-mapper.js +249 -0
  75. package/src/verax/intel/index.js +281 -0
  76. package/src/verax/intel/route-extractor.js +280 -0
  77. package/src/verax/intel/ts-program.js +256 -0
  78. package/src/verax/intel/vue-navigation-extractor.js +642 -0
  79. package/src/verax/intel/vue-router-extractor.js +325 -0
  80. package/src/verax/learn/action-contract-extractor.js +338 -104
  81. package/src/verax/learn/ast-contract-extractor.js +148 -6
  82. package/src/verax/learn/flow-extractor.js +172 -0
  83. package/src/verax/learn/index.js +36 -2
  84. package/src/verax/learn/manifest-writer.js +122 -58
  85. package/src/verax/learn/project-detector.js +40 -0
  86. package/src/verax/learn/route-extractor.js +28 -97
  87. package/src/verax/learn/route-validator.js +8 -7
  88. package/src/verax/learn/state-extractor.js +212 -0
  89. package/src/verax/learn/static-extractor-navigation.js +114 -0
  90. package/src/verax/learn/static-extractor-validation.js +88 -0
  91. package/src/verax/learn/static-extractor.js +119 -10
  92. package/src/verax/learn/truth-assessor.js +24 -21
  93. package/src/verax/learn/ts-contract-resolver.js +14 -12
  94. package/src/verax/observe/aria-sensor.js +211 -0
  95. package/src/verax/observe/browser.js +30 -6
  96. package/src/verax/observe/console-sensor.js +2 -18
  97. package/src/verax/observe/domain-boundary.js +10 -1
  98. package/src/verax/observe/expectation-executor.js +513 -0
  99. package/src/verax/observe/flow-matcher.js +143 -0
  100. package/src/verax/observe/focus-sensor.js +196 -0
  101. package/src/verax/observe/human-driver.js +660 -273
  102. package/src/verax/observe/index.js +910 -26
  103. package/src/verax/observe/interaction-discovery.js +378 -15
  104. package/src/verax/observe/interaction-runner.js +562 -197
  105. package/src/verax/observe/loading-sensor.js +145 -0
  106. package/src/verax/observe/navigation-sensor.js +255 -0
  107. package/src/verax/observe/network-sensor.js +55 -7
  108. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  109. package/src/verax/observe/observed-expectation.js +305 -0
  110. package/src/verax/observe/page-frontier.js +234 -0
  111. package/src/verax/observe/settle.js +38 -17
  112. package/src/verax/observe/state-sensor.js +393 -0
  113. package/src/verax/observe/state-ui-sensor.js +7 -1
  114. package/src/verax/observe/timing-sensor.js +228 -0
  115. package/src/verax/observe/traces-writer.js +73 -21
  116. package/src/verax/observe/ui-signal-sensor.js +143 -17
  117. package/src/verax/scan-summary-writer.js +80 -15
  118. package/src/verax/shared/artifact-manager.js +111 -9
  119. package/src/verax/shared/budget-profiles.js +136 -0
  120. package/src/verax/shared/caching.js +1 -1
  121. package/src/verax/shared/ci-detection.js +39 -0
  122. package/src/verax/shared/config-loader.js +169 -0
  123. package/src/verax/shared/dynamic-route-utils.js +224 -0
  124. package/src/verax/shared/expectation-coverage.js +44 -0
  125. package/src/verax/shared/expectation-prover.js +81 -0
  126. package/src/verax/shared/expectation-tracker.js +201 -0
  127. package/src/verax/shared/expectations-writer.js +60 -0
  128. package/src/verax/shared/first-run.js +44 -0
  129. package/src/verax/shared/progress-reporter.js +171 -0
  130. package/src/verax/shared/retry-policy.js +9 -1
  131. package/src/verax/shared/root-artifacts.js +49 -0
  132. package/src/verax/shared/scan-budget.js +86 -0
  133. package/src/verax/shared/url-normalizer.js +162 -0
  134. package/src/verax/shared/zip-artifacts.js +66 -0
  135. 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
+ }