@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.
Files changed (126) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +14 -36
  4. package/src/cli/commands/default.js +523 -0
  5. package/src/cli/commands/doctor.js +165 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +402 -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 +296 -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 +34 -0
  14. package/src/cli/util/expectation-extractor.js +378 -0
  15. package/src/cli/util/findings-writer.js +31 -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 +366 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +29 -0
  21. package/src/cli/util/project-discovery.js +277 -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/summary-writer.js +32 -0
  26. package/src/verax/cli/ci-summary.js +35 -0
  27. package/src/verax/cli/context-explanation.js +89 -0
  28. package/src/verax/cli/doctor.js +277 -0
  29. package/src/verax/cli/error-normalizer.js +154 -0
  30. package/src/verax/cli/explain-output.js +105 -0
  31. package/src/verax/cli/finding-explainer.js +130 -0
  32. package/src/verax/cli/init.js +237 -0
  33. package/src/verax/cli/run-overview.js +163 -0
  34. package/src/verax/cli/url-safety.js +101 -0
  35. package/src/verax/cli/wizard.js +98 -0
  36. package/src/verax/cli/zero-findings-explainer.js +57 -0
  37. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  38. package/src/verax/core/action-classifier.js +86 -0
  39. package/src/verax/core/budget-engine.js +218 -0
  40. package/src/verax/core/canonical-outcomes.js +157 -0
  41. package/src/verax/core/decision-snapshot.js +335 -0
  42. package/src/verax/core/determinism-model.js +403 -0
  43. package/src/verax/core/incremental-store.js +237 -0
  44. package/src/verax/core/invariants.js +356 -0
  45. package/src/verax/core/promise-model.js +230 -0
  46. package/src/verax/core/replay-validator.js +350 -0
  47. package/src/verax/core/replay.js +222 -0
  48. package/src/verax/core/run-id.js +175 -0
  49. package/src/verax/core/run-manifest.js +99 -0
  50. package/src/verax/core/silence-impact.js +369 -0
  51. package/src/verax/core/silence-model.js +521 -0
  52. package/src/verax/detect/comparison.js +2 -34
  53. package/src/verax/detect/confidence-engine.js +764 -329
  54. package/src/verax/detect/detection-engine.js +293 -0
  55. package/src/verax/detect/evidence-index.js +177 -0
  56. package/src/verax/detect/expectation-model.js +194 -172
  57. package/src/verax/detect/explanation-helpers.js +187 -0
  58. package/src/verax/detect/finding-detector.js +450 -0
  59. package/src/verax/detect/findings-writer.js +44 -8
  60. package/src/verax/detect/flow-detector.js +366 -0
  61. package/src/verax/detect/index.js +172 -286
  62. package/src/verax/detect/interactive-findings.js +613 -0
  63. package/src/verax/detect/signal-mapper.js +308 -0
  64. package/src/verax/detect/verdict-engine.js +563 -0
  65. package/src/verax/evidence-index-writer.js +61 -0
  66. package/src/verax/index.js +90 -14
  67. package/src/verax/intel/effect-detector.js +368 -0
  68. package/src/verax/intel/handler-mapper.js +249 -0
  69. package/src/verax/intel/index.js +281 -0
  70. package/src/verax/intel/route-extractor.js +280 -0
  71. package/src/verax/intel/ts-program.js +256 -0
  72. package/src/verax/intel/vue-navigation-extractor.js +579 -0
  73. package/src/verax/intel/vue-router-extractor.js +323 -0
  74. package/src/verax/learn/action-contract-extractor.js +335 -101
  75. package/src/verax/learn/ast-contract-extractor.js +95 -5
  76. package/src/verax/learn/flow-extractor.js +172 -0
  77. package/src/verax/learn/manifest-writer.js +97 -47
  78. package/src/verax/learn/project-detector.js +40 -0
  79. package/src/verax/learn/route-extractor.js +27 -96
  80. package/src/verax/learn/state-extractor.js +212 -0
  81. package/src/verax/learn/static-extractor-navigation.js +114 -0
  82. package/src/verax/learn/static-extractor-validation.js +88 -0
  83. package/src/verax/learn/static-extractor.js +112 -4
  84. package/src/verax/learn/truth-assessor.js +24 -21
  85. package/src/verax/observe/aria-sensor.js +211 -0
  86. package/src/verax/observe/browser.js +10 -5
  87. package/src/verax/observe/console-sensor.js +1 -17
  88. package/src/verax/observe/domain-boundary.js +10 -1
  89. package/src/verax/observe/expectation-executor.js +512 -0
  90. package/src/verax/observe/flow-matcher.js +143 -0
  91. package/src/verax/observe/focus-sensor.js +196 -0
  92. package/src/verax/observe/human-driver.js +643 -275
  93. package/src/verax/observe/index.js +908 -27
  94. package/src/verax/observe/index.js.backup +1 -0
  95. package/src/verax/observe/interaction-discovery.js +365 -14
  96. package/src/verax/observe/interaction-runner.js +563 -198
  97. package/src/verax/observe/loading-sensor.js +139 -0
  98. package/src/verax/observe/navigation-sensor.js +255 -0
  99. package/src/verax/observe/network-sensor.js +55 -7
  100. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  101. package/src/verax/observe/observed-expectation.js +305 -0
  102. package/src/verax/observe/page-frontier.js +234 -0
  103. package/src/verax/observe/settle.js +37 -17
  104. package/src/verax/observe/state-sensor.js +389 -0
  105. package/src/verax/observe/timing-sensor.js +228 -0
  106. package/src/verax/observe/traces-writer.js +61 -20
  107. package/src/verax/observe/ui-signal-sensor.js +136 -17
  108. package/src/verax/scan-summary-writer.js +77 -15
  109. package/src/verax/shared/artifact-manager.js +110 -8
  110. package/src/verax/shared/budget-profiles.js +136 -0
  111. package/src/verax/shared/ci-detection.js +39 -0
  112. package/src/verax/shared/config-loader.js +170 -0
  113. package/src/verax/shared/dynamic-route-utils.js +218 -0
  114. package/src/verax/shared/expectation-coverage.js +44 -0
  115. package/src/verax/shared/expectation-prover.js +81 -0
  116. package/src/verax/shared/expectation-tracker.js +201 -0
  117. package/src/verax/shared/expectations-writer.js +60 -0
  118. package/src/verax/shared/first-run.js +44 -0
  119. package/src/verax/shared/progress-reporter.js +171 -0
  120. package/src/verax/shared/retry-policy.js +14 -1
  121. package/src/verax/shared/root-artifacts.js +49 -0
  122. package/src/verax/shared/scan-budget.js +86 -0
  123. package/src/verax/shared/url-normalizer.js +162 -0
  124. package/src/verax/shared/zip-artifacts.js +65 -0
  125. package/src/verax/validate/context-validator.js +244 -0
  126. package/src/verax/validate/context-validator.js.bak +0 -0
@@ -0,0 +1,37 @@
1
+ import { writeFileSync, renameSync, mkdirSync } from 'fs';
2
+ import { dirname } from 'path';
3
+
4
+ /**
5
+ * Atomic write for JSON files
6
+ * Writes to a temp file and renames to prevent partial writes
7
+ */
8
+ export function atomicWriteJson(filePath, data) {
9
+ const tempPath = `${filePath}.tmp`;
10
+ const dirPath = dirname(filePath);
11
+
12
+ // Ensure directory exists
13
+ mkdirSync(dirPath, { recursive: true });
14
+
15
+ // Write to temp file
16
+ writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf8');
17
+
18
+ // Atomic rename
19
+ renameSync(tempPath, filePath);
20
+ }
21
+
22
+ /**
23
+ * Atomic write for text files (JSONL, logs, etc.)
24
+ */
25
+ export function atomicWriteText(filePath, content) {
26
+ const tempPath = `${filePath}.tmp`;
27
+ const dirPath = dirname(filePath);
28
+
29
+ // Ensure directory exists
30
+ mkdirSync(dirPath, { recursive: true });
31
+
32
+ // Write to temp file
33
+ writeFileSync(tempPath, content, 'utf8');
34
+
35
+ // Atomic rename
36
+ renameSync(tempPath, filePath);
37
+ }
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Detection Engine
3
+ * Compares learned expectations against observations to detect silent failures.
4
+ * Produces exactly one finding per expectation with deterministic confidence and impact.
5
+ */
6
+
7
+ export async function detectFindings(learnData, observeData, projectPath, onProgress) {
8
+ const findings = [];
9
+ const stats = {
10
+ total: 0,
11
+ silentFailures: 0,
12
+ observed: 0,
13
+ coverageGaps: 0,
14
+ unproven: 0,
15
+ informational: 0,
16
+ };
17
+
18
+ const observationMap = indexObservations(observeData);
19
+ const expectations = learnData?.expectations || [];
20
+
21
+ const narration = [];
22
+
23
+ for (let i = 0; i < expectations.length; i++) {
24
+ const expectation = expectations[i];
25
+ const index = i + 1;
26
+
27
+ if (onProgress) {
28
+ onProgress({
29
+ event: 'detect:attempt',
30
+ index,
31
+ total: expectations.length,
32
+ expectationId: expectation.id,
33
+ type: expectation.type,
34
+ value: expectation?.promise?.value,
35
+ });
36
+ }
37
+
38
+ const observation = getObservationForExpectation(expectation, observationMap);
39
+ const finding = classifyExpectation(expectation, observation);
40
+
41
+ findings.push(finding);
42
+ stats.total++;
43
+ stats[findingStatKey(finding.classification)]++;
44
+
45
+ const icon = classificationIcon(finding.classification);
46
+ narration.push(`${icon} ${finding.classification.toUpperCase()} ${finding.promise?.value || ''}`.trim());
47
+
48
+ if (onProgress) {
49
+ onProgress({
50
+ event: 'detect:classified',
51
+ index,
52
+ classification: finding.classification,
53
+ impact: finding.impact,
54
+ confidence: finding.confidence,
55
+ expectationId: expectation.id,
56
+ });
57
+ }
58
+ }
59
+
60
+ // Terminal narration (one line per finding + summary)
61
+ narration.forEach((line) => console.log(line));
62
+ console.log(`SUMMARY findings=${findings.length} observed=${stats.observed} silent-failure=${stats.silentFailures} coverage-gap=${stats.coverageGaps} unproven=${stats.unproven}`);
63
+
64
+ // Emit completion event
65
+ if (onProgress) {
66
+ onProgress({
67
+ event: 'detect:completed',
68
+ total: findings.length,
69
+ stats,
70
+ });
71
+ }
72
+
73
+ return {
74
+ findings,
75
+ stats,
76
+ detectedAt: new Date().toISOString(),
77
+ };
78
+ }
79
+
80
+ function indexObservations(observeData) {
81
+ const map = new Map();
82
+ if (!observeData || !Array.isArray(observeData.observations)) return map;
83
+
84
+ observeData.observations.forEach((obs) => {
85
+ if (obs == null) return;
86
+ const keys = [];
87
+ if (obs.id) keys.push(obs.id);
88
+ if (obs.expectationId) keys.push(obs.expectationId);
89
+ keys.forEach((key) => {
90
+ if (!map.has(key)) map.set(key, obs);
91
+ });
92
+ });
93
+ return map;
94
+ }
95
+
96
+ function getObservationForExpectation(expectation, observationMap) {
97
+ if (!expectation || !expectation.id) return null;
98
+ return observationMap.get(expectation.id) || null;
99
+ }
100
+
101
+ function classificationIcon(classification) {
102
+ switch (classification) {
103
+ case 'observed':
104
+ return '✓';
105
+ case 'silent-failure':
106
+ return '✗';
107
+ case 'coverage-gap':
108
+ return '⚠';
109
+ case 'unproven':
110
+ return '⚠';
111
+ default:
112
+ return '•';
113
+ }
114
+ }
115
+
116
+ function findingStatKey(classification) {
117
+ switch (classification) {
118
+ case 'silent-failure':
119
+ return 'silentFailures';
120
+ case 'observed':
121
+ return 'observed';
122
+ case 'coverage-gap':
123
+ return 'coverageGaps';
124
+ case 'unproven':
125
+ return 'unproven';
126
+ default:
127
+ return 'informational';
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Classify a single expectation according to deterministic rules.
133
+ */
134
+ function classifyExpectation(expectation, observation) {
135
+ const finding = {
136
+ id: expectation.id,
137
+ type: expectation.type,
138
+ promise: expectation.promise,
139
+ source: expectation.source,
140
+ classification: 'informational',
141
+ impact: 'LOW',
142
+ confidence: 0,
143
+ evidence: [],
144
+ reason: null,
145
+ };
146
+
147
+ const attempted = Boolean(observation?.attempted);
148
+ const observed = observation?.observed === true;
149
+ const reason = observation?.reason || null;
150
+
151
+ const evidence = normalizeEvidence(observation?.evidenceFiles || []);
152
+ finding.evidence = evidence;
153
+
154
+ const evidenceSignals = analyzeEvidenceSignals(observation, evidence);
155
+
156
+ // 1) observed
157
+ if (observed) {
158
+ finding.classification = 'observed';
159
+ finding.reason = 'Expectation observed at runtime';
160
+ finding.impact = getImpact(expectation);
161
+ finding.confidence = 1.0;
162
+ return finding;
163
+ }
164
+
165
+ // 2) coverage-gap (not attempted or explicitly blocked)
166
+ if (!attempted || isSafetySkip(reason)) {
167
+ finding.classification = 'coverage-gap';
168
+ finding.reason = reason || 'No observation attempt recorded';
169
+ finding.impact = 'LOW';
170
+ finding.confidence = 0;
171
+ return finding;
172
+ }
173
+
174
+ // 3) silent-failure (attempted, observed === false, no safety skip)
175
+ if (attempted && observation?.observed === false && !isSafetySkip(reason)) {
176
+ finding.classification = 'silent-failure';
177
+ finding.reason = reason || 'Expected behavior not observed';
178
+ finding.impact = getImpact(expectation);
179
+ finding.confidence = calculateConfidence(expectation, evidenceSignals, 'silent-failure');
180
+ return finding;
181
+ }
182
+
183
+ // 4) unproven (attempted, ambiguous evidence)
184
+ if (attempted && !observed) {
185
+ finding.classification = 'unproven';
186
+ finding.reason = reason || 'Attempted but evidence insufficient';
187
+ finding.impact = 'MEDIUM';
188
+ finding.confidence = calculateConfidence(expectation, evidenceSignals, 'unproven');
189
+ return finding;
190
+ }
191
+
192
+ // 5) informational fallback
193
+ finding.classification = 'informational';
194
+ finding.reason = reason || 'No classification rule matched';
195
+ finding.impact = 'LOW';
196
+ finding.confidence = calculateConfidence(expectation, evidenceSignals, 'informational');
197
+ return finding;
198
+ }
199
+
200
+ function isSafetySkip(reason) {
201
+ if (!reason) return false;
202
+ const lower = reason.toLowerCase();
203
+ const safetyIndicators = ['blocked', 'timeout', 'not found', 'safety', 'permission', 'denied', 'captcha', 'forbidden'];
204
+ return safetyIndicators.some((indicator) => lower.includes(indicator));
205
+ }
206
+
207
+ /**
208
+ * Deterministic confidence calculation.
209
+ * Screenshots + network + DOM change => >=0.8
210
+ * Screenshots only => ~0.6
211
+ * Weak signals => <0.5
212
+ */
213
+ function calculateConfidence(expectation, evidenceSignals, classification) {
214
+ if (classification === 'observed') return 1.0;
215
+ if (classification === 'coverage-gap') return 0;
216
+
217
+ const { hasScreenshots, hasNetworkLogs, hasDomChange } = evidenceSignals;
218
+
219
+ if (classification === 'silent-failure') {
220
+ if (hasScreenshots && hasNetworkLogs && hasDomChange) return 0.85;
221
+ if (hasScreenshots && hasNetworkLogs) return 0.75;
222
+ if (hasScreenshots && hasDomChange) return 0.7;
223
+ if (hasScreenshots) return 0.6;
224
+ if (hasNetworkLogs || hasDomChange) return 0.5;
225
+ return 0.4; // weak signals, attempted but no evidence
226
+ }
227
+
228
+ if (classification === 'unproven') {
229
+ if (hasScreenshots || hasNetworkLogs || hasDomChange) return 0.45;
230
+ return 0.3;
231
+ }
232
+
233
+ return 0.3;
234
+ }
235
+
236
+ function analyzeEvidenceSignals(observation, evidence) {
237
+ const hasScreenshots = evidence.some((e) => e.type === 'screenshot');
238
+ const hasNetworkLogs = evidence.some((e) => e.type === 'network-log');
239
+ const hasDomChange = Boolean(
240
+ observation?.domChanged ||
241
+ observation?.domChange === true ||
242
+ observation?.sensors?.uiSignals?.diff?.changed
243
+ );
244
+
245
+ return { hasScreenshots, hasNetworkLogs, hasDomChange };
246
+ }
247
+
248
+ function normalizeEvidence(evidenceFiles) {
249
+ if (!Array.isArray(evidenceFiles)) return [];
250
+ return evidenceFiles
251
+ .filter(Boolean)
252
+ .map((file) => ({
253
+ type: getEvidenceType(file),
254
+ path: file,
255
+ available: true,
256
+ }));
257
+ }
258
+
259
+ /**
260
+ * Impact classification per product spec.
261
+ */
262
+ function getImpact(expectation) {
263
+ const type = expectation?.type;
264
+ const value = expectation?.promise?.value || '';
265
+ const valueStr = String(value).toLowerCase();
266
+
267
+ if (type === 'navigation') {
268
+ const primaryRoutes = ['/', '/home', '/about', '/contact', '/products', '/pricing', '/features', '/login', '/signup'];
269
+ if (primaryRoutes.includes(value)) return 'HIGH';
270
+ if (valueStr.includes('admin') || valueStr.includes('dashboard') || valueStr.includes('settings')) return 'MEDIUM';
271
+ if (valueStr.includes('privacy') || valueStr.includes('terms') || valueStr.includes('footer')) return 'LOW';
272
+ return 'MEDIUM';
273
+ }
274
+
275
+ if (type === 'network') {
276
+ if (valueStr.includes('/api/auth') || valueStr.includes('/api/payment') || valueStr.includes('/api/user')) return 'HIGH';
277
+ if (valueStr.includes('api.')) return 'MEDIUM';
278
+ if (valueStr.includes('/api/') && !valueStr.includes('/api/analytics')) return 'MEDIUM';
279
+ return 'LOW';
280
+ }
281
+
282
+ if (type === 'state') {
283
+ return 'MEDIUM';
284
+ }
285
+
286
+ return 'LOW';
287
+ }
288
+
289
+ function getEvidenceType(filename) {
290
+ if (!filename) return 'unknown';
291
+ const lower = filename.toLowerCase();
292
+ if (lower.endsWith('.png') || lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'screenshot';
293
+ if (lower.endsWith('.json')) return 'network-log';
294
+ if (lower.endsWith('.log') || lower.endsWith('.txt')) return 'console-log';
295
+ return 'artifact';
296
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Attempt to infer URL from environment variables and common dev configs
3
+ */
4
+ export function tryResolveUrlFromEnv() {
5
+ // Check common environment variables
6
+ const candidates = [
7
+ process.env.VERCEL_URL,
8
+ process.env.NEXT_PUBLIC_SITE_URL,
9
+ process.env.SITE_URL,
10
+ process.env.PUBLIC_URL,
11
+ ];
12
+
13
+ for (const candidate of candidates) {
14
+ if (candidate) {
15
+ // Ensure it's a valid URL
16
+ if (candidate.startsWith('http://') || candidate.startsWith('https://')) {
17
+ return candidate;
18
+ }
19
+ // If it's a bare domain, assume https
20
+ if (candidate.includes('.') && !candidate.includes(' ')) {
21
+ return `https://${candidate}`;
22
+ }
23
+ }
24
+ }
25
+
26
+ // Check for localhost/PORT
27
+ if (process.env.PORT) {
28
+ const port = process.env.PORT;
29
+ return `http://localhost:${port}`;
30
+ }
31
+
32
+ return null;
33
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * CLI Error System
3
+ * Maps errors to exit codes:
4
+ * - 0: success (tool executed)
5
+ * - 2: internal crash
6
+ * - 64: invalid CLI usage
7
+ * - 65: invalid input data
8
+ */
9
+
10
+ export class CLIError extends Error {
11
+ constructor(message, exitCode = 2) {
12
+ super(message);
13
+ this.name = 'CLIError';
14
+ this.exitCode = exitCode;
15
+ }
16
+ }
17
+
18
+ export class UsageError extends CLIError {
19
+ constructor(message) {
20
+ super(message, 64);
21
+ this.name = 'UsageError';
22
+ }
23
+ }
24
+
25
+ export class DataError extends CLIError {
26
+ constructor(message) {
27
+ super(message, 65);
28
+ this.name = 'DataError';
29
+ }
30
+ }
31
+
32
+ export class CrashError extends CLIError {
33
+ constructor(message) {
34
+ super(message, 2);
35
+ this.name = 'CrashError';
36
+ }
37
+ }
38
+
39
+ export function getExitCode(error) {
40
+ if (error instanceof CLIError) {
41
+ return error.exitCode;
42
+ }
43
+ return 2; // default to crash
44
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Event emitter for run progress
3
+ */
4
+ export class RunEventEmitter {
5
+ constructor() {
6
+ this.events = [];
7
+ this.listeners = [];
8
+ }
9
+
10
+ on(event, handler) {
11
+ this.listeners.push({ event, handler });
12
+ }
13
+
14
+ emit(type, data = {}) {
15
+ const event = {
16
+ type,
17
+ timestamp: new Date().toISOString(),
18
+ ...data,
19
+ };
20
+
21
+ this.events.push(event);
22
+
23
+ // Call registered listeners
24
+ this.listeners.forEach(({ event: listenEvent, handler }) => {
25
+ if (listenEvent === type || listenEvent === '*') {
26
+ handler(event);
27
+ }
28
+ });
29
+ }
30
+
31
+ getEvents() {
32
+ return this.events;
33
+ }
34
+ }