@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,24 @@
1
+ /**
2
+ * TypeScript AST Node type extensions
3
+ * These extend the base Node type to include properties used by VERAX
4
+ */
5
+
6
+ import type * as ts from 'typescript';
7
+
8
+ declare module 'typescript' {
9
+ interface Node {
10
+ attributes?: ts.NodeArray<ts.JSDocAttribute>;
11
+ tagName?: ts.Identifier;
12
+ body?: ts.Node;
13
+ arguments?: ts.NodeArray<ts.Expression>;
14
+ initializer?: ts.Expression;
15
+ expression?: ts.Expression;
16
+ children?: ts.NodeArray<ts.Node>;
17
+ }
18
+
19
+ namespace ts {
20
+ // Add isFalseKeyword if it doesn't exist (it might be in a different version)
21
+ function isFalseKeyword(node: ts.Node): node is ts.FalseKeyword;
22
+ }
23
+ }
24
+
@@ -0,0 +1,35 @@
1
+ /**
2
+ * CI One-Line Summary
3
+ *
4
+ * Prints a deterministic one-line summary for CI logs.
5
+ * OBSERVATIONAL ONLY - no verdicts, no trust badges, no judgments.
6
+ */
7
+
8
+ /**
9
+ * Generate CI one-line summary
10
+ * @param {Object} context - Summary context
11
+ * @returns {string} One-line summary
12
+ */
13
+ export function generateCISummary(context = {}) {
14
+ const {
15
+ expectations = 0,
16
+ interactions = 0,
17
+ findings = 0,
18
+ gaps = 0,
19
+ silences = 0,
20
+ runId = 'unknown'
21
+ } = context;
22
+
23
+ // Format: VERAX | expectations=... | interactions=... | findings=... | gaps=... | silences=... | run=...
24
+ return `VERAX | expectations=${expectations} | interactions=${interactions} | findings=${findings} | gaps=${gaps} | silences=${silences} | run=${runId}`;
25
+ }
26
+
27
+ /**
28
+ * Print CI one-line summary
29
+ * @param {Object} context - Summary context
30
+ */
31
+ export function printCISummary(context) {
32
+ const summary = generateCISummary(context);
33
+ console.error(summary);
34
+ }
35
+
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Wave 4.1 — Context Validation Explanation
3
+ *
4
+ * Provides detailed explanations when context validation fails or matches.
5
+ */
6
+
7
+ /**
8
+ * Generate context validation explanation
9
+ * @param {Object} contextCheck - Context check result
10
+ * @param {Array} projectRoutes - Routes extracted from project
11
+ * @returns {Array<string>} Explanation lines
12
+ */
13
+ export function explainContextValidation(contextCheck, projectRoutes = []) {
14
+ const lines = [];
15
+
16
+ if (!contextCheck.ran) {
17
+ if (contextCheck.reason === 'no_routes_extracted') {
18
+ lines.push('Context validation skipped: No routes extracted from project.');
19
+ } else if (contextCheck.reason === 'invalid_url') {
20
+ lines.push('Context validation skipped: Invalid URL format.');
21
+ } else {
22
+ lines.push('Context validation skipped.');
23
+ }
24
+ return lines;
25
+ }
26
+
27
+ if (contextCheck.verdict === 'VALID_CONTEXT') {
28
+ lines.push(`✓ Context validated: URL matches project`);
29
+ lines.push(` ${contextCheck.matchedRoutesCount} of ${contextCheck.totalRoutesChecked || projectRoutes.length} routes matched`);
30
+ if (contextCheck.sampleMatched && contextCheck.sampleMatched.length > 0) {
31
+ lines.push(` Sample matched routes: ${contextCheck.sampleMatched.slice(0, 3).join(', ')}`);
32
+ }
33
+ return lines;
34
+ }
35
+
36
+ // INVALID_CONTEXT or INVALID_CONTEXT_FORCED
37
+ lines.push(`⚠ Context mismatch: URL does not match project`);
38
+ lines.push(` Project routes found: ${projectRoutes.length}`);
39
+ lines.push(` Routes matched: ${contextCheck.matchedRoutesCount} of ${contextCheck.totalRoutesChecked || projectRoutes.length}`);
40
+
41
+ if (projectRoutes.length > 0 && contextCheck.matchedRoutesCount === 0) {
42
+ lines.push('');
43
+ lines.push(' Project routes (sample):');
44
+ const sampleRoutes = projectRoutes.slice(0, 5);
45
+ for (const route of sampleRoutes) {
46
+ lines.push(` - ${route}`);
47
+ }
48
+
49
+ if (contextCheck.internalLinksFound !== undefined) {
50
+ lines.push(` Live site internal links found: ${contextCheck.internalLinksFound}`);
51
+ }
52
+
53
+ lines.push('');
54
+ lines.push(' Possible reasons:');
55
+ lines.push(' • Route paths don\'t match (e.g., /about vs /about.html)');
56
+ lines.push(' • Project routes not linked on homepage');
57
+ lines.push(' • SPA routes not accessible at expected paths');
58
+
59
+ if (contextCheck.verdict === 'INVALID_CONTEXT') {
60
+ lines.push('');
61
+ lines.push(' Next steps:');
62
+ lines.push(' • Use --force to scan anyway');
63
+ lines.push(' • Verify URL matches project deployment');
64
+ lines.push(' • Check that routes exist on the live site');
65
+ } else {
66
+ lines.push('');
67
+ lines.push(' Scan continued with --force flag despite mismatch.');
68
+ }
69
+ }
70
+
71
+ return lines;
72
+ }
73
+
74
+ /**
75
+ * Print context validation explanation
76
+ * @param {Object} contextCheck - Context check result
77
+ * @param {Array} projectRoutes - Routes extracted from project
78
+ */
79
+ export function printContextExplanation(contextCheck, projectRoutes = []) {
80
+ const lines = explainContextValidation(contextCheck, projectRoutes);
81
+ if (lines.length > 0) {
82
+ console.error('');
83
+ console.error('Context Validation:');
84
+ lines.forEach(line => {
85
+ console.error(line);
86
+ });
87
+ }
88
+ }
89
+
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Wave 7 — VERAX Doctor
3
+ *
4
+ * Checks environment, dependencies, and project setup.
5
+ */
6
+
7
+ import { mkdirSync, writeFileSync, unlinkSync } from 'fs';
8
+ import { resolve } from 'path';
9
+ import { chromium } from 'playwright';
10
+ import { get } from 'http';
11
+ import { get as httpsGet } from 'https';
12
+ import { learn } from '../index.js';
13
+
14
+ /**
15
+ * Check Node version
16
+ * @returns {Object} { status: 'ok'|'warn'|'fail', message: string }
17
+ */
18
+ function checkNodeVersion() {
19
+ const requiredMajor = 18;
20
+ const nodeVersion = process.version;
21
+ const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0], 10);
22
+
23
+ if (majorVersion >= requiredMajor) {
24
+ return { status: 'ok', message: `Node.js ${nodeVersion} (required: >=${requiredMajor}.0.0)` };
25
+ } else {
26
+ return {
27
+ status: 'fail',
28
+ message: `Node.js ${nodeVersion} is too old (required: >=${requiredMajor}.0.0)`,
29
+ fix: `Upgrade Node.js: nvm install ${requiredMajor} or visit nodejs.org`
30
+ };
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Check write permissions to output directory
36
+ * @param {string} projectRoot - Project root
37
+ * @returns {Object} { status: 'ok'|'fail', message: string }
38
+ */
39
+ function checkWritePermissions(projectRoot) {
40
+ try {
41
+ const veraxDir = resolve(projectRoot, '.verax');
42
+ mkdirSync(veraxDir, { recursive: true });
43
+
44
+ const testFile = resolve(veraxDir, '.write-test');
45
+ writeFileSync(testFile, 'test');
46
+ unlinkSync(testFile);
47
+
48
+ return { status: 'ok', message: 'Can write to .verax directory' };
49
+ } catch (error) {
50
+ return {
51
+ status: 'fail',
52
+ message: `Cannot write to .verax directory: ${error.message}`,
53
+ fix: 'Check file permissions or run with appropriate access'
54
+ };
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Check Playwright availability
60
+ * @returns {Promise<Object>} { status: 'ok'|'fail', message: string }
61
+ */
62
+ async function checkPlaywright() {
63
+ try {
64
+ const browser = await chromium.launch({ headless: true });
65
+ await browser.close();
66
+ return { status: 'ok', message: 'Playwright browser is available' };
67
+ } catch (error) {
68
+ // Detect common error messages and provide specific fixes
69
+ const errorMsg = error.message.toLowerCase();
70
+ let fix = 'Run: npx playwright install';
71
+
72
+ if (errorMsg.includes('chromium') || errorMsg.includes('executable')) {
73
+ fix = 'Run: npx playwright install chromium';
74
+ } else if (errorMsg.includes('missing') || errorMsg.includes('not found')) {
75
+ fix = 'Run: npx playwright install --with-deps chromium';
76
+ }
77
+
78
+ return {
79
+ status: 'fail',
80
+ message: `Playwright browser not available: ${error.message}`,
81
+ fix: fix
82
+ };
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Check project detection and expectations
88
+ * @param {string} projectRoot - Project root
89
+ * @returns {Promise<Object>} { status: 'ok'|'warn', message: string, details: Object }
90
+ */
91
+ async function checkProjectExpectations(projectRoot) {
92
+ try {
93
+ const manifest = await learn(projectRoot);
94
+ const projectType = manifest.projectType || 'unknown';
95
+ const expectationsCount = manifest.learnTruth?.expectationsDiscovered || 0;
96
+
97
+ if (expectationsCount > 0) {
98
+ return {
99
+ status: 'ok',
100
+ message: `Project type: ${projectType}, ${expectationsCount} expectations found`,
101
+ details: {
102
+ projectType,
103
+ expectationsCount,
104
+ routesCount: manifest.publicRoutes?.length || 0
105
+ }
106
+ };
107
+ } else {
108
+ return {
109
+ status: 'warn',
110
+ message: `Project type: ${projectType}, but 0 expectations found`,
111
+ details: {
112
+ projectType,
113
+ expectationsCount: 0
114
+ },
115
+ fix: 'Add static patterns (HTML links, static fetch calls, or state mutations)'
116
+ };
117
+ }
118
+ } catch (error) {
119
+ return {
120
+ status: 'fail',
121
+ message: `Failed to analyze project: ${error.message}`,
122
+ fix: 'Check that projectRoot is correct and project is readable'
123
+ };
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Check URL reachability
129
+ * @param {string} url - URL to check
130
+ * @returns {Promise<Object>} { status: 'ok'|'fail', message: string }
131
+ */
132
+ async function checkUrlReachability(url) {
133
+ return new Promise((resolve) => {
134
+ try {
135
+ const urlObj = new URL(url);
136
+ const clientGet = urlObj.protocol === 'https:' ? httpsGet : get;
137
+
138
+ const request = clientGet(url, { timeout: 5000 }, (response) => {
139
+ request.destroy();
140
+ if (response.statusCode >= 200 && response.statusCode < 400) {
141
+ resolve({ status: 'ok', message: `URL ${url} is reachable (${response.statusCode})` });
142
+ } else {
143
+ resolve({
144
+ status: 'warn',
145
+ message: `URL ${url} returned ${response.statusCode}`,
146
+ fix: 'Verify URL is correct and server is running'
147
+ });
148
+ }
149
+ });
150
+
151
+ request.on('error', (error) => {
152
+ resolve({
153
+ status: 'fail',
154
+ message: `Cannot reach ${url}: ${error.message}`,
155
+ fix: 'Ensure server is running and URL is correct'
156
+ });
157
+ });
158
+
159
+ request.on('timeout', () => {
160
+ request.destroy();
161
+ resolve({
162
+ status: 'warn',
163
+ message: `URL ${url} did not respond within 5 seconds`,
164
+ fix: 'Check if server is running and accessible'
165
+ });
166
+ });
167
+
168
+ request.setTimeout(5000);
169
+ } catch (error) {
170
+ resolve({
171
+ status: 'fail',
172
+ message: `Invalid URL: ${error.message}`,
173
+ fix: 'Provide a valid URL (e.g., http://localhost:3000)'
174
+ });
175
+ }
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Run doctor checks
181
+ * @param {Object} options - { projectRoot, url, json }
182
+ * @returns {Promise<Object>} Doctor results
183
+ */
184
+ export async function runDoctor(options = {}) {
185
+ const { projectRoot = process.cwd(), url = null, json: _json = false } = options;
186
+
187
+ const checks = [];
188
+ let overallStatus = 'ok';
189
+
190
+ // Check 1: Node version
191
+ const nodeCheck = checkNodeVersion();
192
+ checks.push({ name: 'Node.js Version', ...nodeCheck });
193
+ if (nodeCheck.status === 'fail') overallStatus = 'fail';
194
+
195
+ // Check 2: Write permissions
196
+ const writeCheck = checkWritePermissions(projectRoot);
197
+ checks.push({ name: 'Write Permissions', ...writeCheck });
198
+ if (writeCheck.status === 'fail') overallStatus = 'fail';
199
+
200
+ // Check 3: Playwright
201
+ const playwrightCheck = await checkPlaywright();
202
+ checks.push({ name: 'Playwright Browser', ...playwrightCheck });
203
+ if (playwrightCheck.status === 'fail') overallStatus = 'fail';
204
+
205
+ // Check 4: Project expectations
206
+ const projectCheck = await checkProjectExpectations(projectRoot);
207
+ checks.push({ name: 'Project Analysis', ...projectCheck });
208
+ if (projectCheck.status === 'fail') overallStatus = 'fail';
209
+ else if (projectCheck.status === 'warn' && overallStatus === 'ok') overallStatus = 'warn';
210
+
211
+ // Check 5: URL reachability (if provided)
212
+ if (url) {
213
+ const urlCheck = await checkUrlReachability(url);
214
+ checks.push({ name: 'URL Reachability', ...urlCheck });
215
+ if (urlCheck.status === 'fail') overallStatus = 'fail';
216
+ else if (urlCheck.status === 'warn' && overallStatus === 'ok') overallStatus = 'warn';
217
+ }
218
+
219
+ // Collect fixes
220
+ const fixes = checks.filter(c => c.fix).map(c => c.fix);
221
+
222
+ return {
223
+ status: overallStatus,
224
+ checks,
225
+ fixes
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Print doctor results
231
+ * @param {Object} results - Doctor results
232
+ * @param {boolean} json - Whether to output JSON
233
+ */
234
+ export function printDoctorResults(results, json = false) {
235
+ if (json) {
236
+ console.log(JSON.stringify(results, null, 2));
237
+ return;
238
+ }
239
+
240
+ // Human-readable output
241
+ console.error('\n' + '═'.repeat(60));
242
+ console.error('VERAX Doctor');
243
+ console.error('═'.repeat(60));
244
+
245
+ const statusEmoji = {
246
+ 'ok': '✅',
247
+ 'warn': '⚠️',
248
+ 'fail': '❌'
249
+ };
250
+
251
+ for (const check of results.checks) {
252
+ const emoji = statusEmoji[check.status] || '❓';
253
+ console.error(`\n${emoji} ${check.name}`);
254
+ console.error(` ${check.message}`);
255
+ if (check.details) {
256
+ for (const [key, value] of Object.entries(check.details)) {
257
+ console.error(` ${key}: ${value}`);
258
+ }
259
+ }
260
+ if (check.fix) {
261
+ console.error(` Fix: ${check.fix}`);
262
+ }
263
+ }
264
+
265
+ console.error('\n' + '─'.repeat(60));
266
+ console.error(`Overall Status: ${results.status.toUpperCase()}`);
267
+
268
+ if (results.fixes.length > 0) {
269
+ console.error('\nRecommended Fixes:');
270
+ results.fixes.forEach((fix, index) => {
271
+ console.error(` ${index + 1}. ${fix}`);
272
+ });
273
+ }
274
+
275
+ console.error('═'.repeat(60) + '\n');
276
+ }
277
+
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Wave 3 — Error Normalizer
3
+ *
4
+ * Converts technical errors into human-friendly messages with next steps.
5
+ */
6
+
7
+ /**
8
+ * Normalize error into user-friendly message
9
+ * @param {Error} error - Error object
10
+ * @param {Object} context - Context information
11
+ * @param {boolean} debug - Whether to show full stack
12
+ * @returns {Object} { message: string, nextSteps: string[], stack: string }
13
+ */
14
+ export function normalizeError(error, context = {}, debug = false) {
15
+ const errorMessage = error.message || String(error);
16
+ const stack = error.stack || '';
17
+
18
+ // Missing URL
19
+ if (errorMessage.includes('--url is required') || errorMessage.includes('URL is required')) {
20
+ return {
21
+ message: 'URL is required to run a scan.',
22
+ nextSteps: [
23
+ 'Try: verax run --url http://localhost:3000',
24
+ 'Or run: verax (interactive wizard)'
25
+ ],
26
+ stack: debug ? stack : null
27
+ };
28
+ }
29
+
30
+ // Project root not found
31
+ if (errorMessage.includes('does not exist') && context.projectRoot) {
32
+ return {
33
+ message: `Project directory not found: ${context.projectRoot}`,
34
+ nextSteps: [
35
+ 'Check that the path is correct',
36
+ 'Or specify a different path with --projectRoot',
37
+ 'Or run from the project directory: cd /path/to/project && verax run --url <url>'
38
+ ],
39
+ stack: debug ? stack : null
40
+ };
41
+ }
42
+
43
+ // INVALID_CONTEXT
44
+ if (errorMessage.includes('INVALID_CONTEXT') || context.verdict === 'INVALID_CONTEXT') {
45
+ return {
46
+ message: 'The URL you\'re scanning doesn\'t match the project being analyzed.',
47
+ nextSteps: [
48
+ 'Ensure the URL matches your project (e.g., localhost:3000 for local dev server)',
49
+ 'Or use --force to scan anyway (not recommended)',
50
+ 'Or specify the correct --projectRoot that matches the URL'
51
+ ],
52
+ stack: debug ? stack : null
53
+ };
54
+ }
55
+
56
+ // NO_EXPECTATIONS_FOUND
57
+ if (errorMessage.includes('NO_EXPECTATIONS_FOUND') || context.verdict === 'NO_EXPECTATIONS_FOUND') {
58
+ return {
59
+ message: 'No code-derived expectations found in your project.',
60
+ nextSteps: [
61
+ 'VERAX needs static, proven patterns in your code:',
62
+ ' • HTML links: <a href="/about">',
63
+ ' • Static fetch/axios: fetch("/api/users") (no template literals)',
64
+ ' • State mutations: useState, Redux dispatch, Zustand set',
65
+ 'Dynamic routes and URLs are intentionally skipped.',
66
+ 'See README for supported patterns.'
67
+ ],
68
+ stack: debug ? stack : null
69
+ };
70
+ }
71
+
72
+ // Playwright launch failure
73
+ const lowerError = errorMessage.toLowerCase();
74
+ const lowerStack = stack.toLowerCase();
75
+ if (lowerError.includes('executable') ||
76
+ lowerError.includes('browsertype') ||
77
+ lowerError.includes('chromium') ||
78
+ lowerError.includes('playwright') ||
79
+ lowerStack.includes('playwright')) {
80
+ return {
81
+ message: 'Browser automation failed. Playwright browsers are not installed.',
82
+ nextSteps: [
83
+ 'Install Playwright browsers: npx playwright install chromium',
84
+ 'Or with system dependencies: npx playwright install --with-deps chromium',
85
+ 'See: https://playwright.dev/docs/installation'
86
+ ],
87
+ stack: debug ? stack : null
88
+ };
89
+ }
90
+
91
+ // Network/navigation errors
92
+ if (errorMessage.includes('net::ERR') || errorMessage.includes('Navigation timeout') || errorMessage.includes('Protocol error')) {
93
+ return {
94
+ message: `Cannot connect to ${context.url || 'the URL'}.`,
95
+ nextSteps: [
96
+ 'Ensure the URL is correct and the server is running',
97
+ 'Check network connectivity',
98
+ 'For localhost: ensure your dev server is started'
99
+ ],
100
+ stack: debug ? stack : null
101
+ };
102
+ }
103
+
104
+ // Flow file not found
105
+ if (errorMessage.includes('Flow file not found')) {
106
+ return {
107
+ message: `Flow file not found: ${context.flowPath || 'specified path'}`,
108
+ nextSteps: [
109
+ 'Check that the file path is correct',
110
+ 'Ensure the flow file exists',
111
+ 'See README for flow file format'
112
+ ],
113
+ stack: debug ? stack : null
114
+ };
115
+ }
116
+
117
+ // Generic error
118
+ return {
119
+ message: errorMessage,
120
+ nextSteps: [
121
+ 'Check the error message above',
122
+ 'Run with --debug for detailed error information',
123
+ 'See README or run: verax --help'
124
+ ],
125
+ stack: debug ? stack : null
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Print normalized error to console
131
+ * @param {Error} error - Error object
132
+ * @param {Object} context - Context
133
+ * @param {boolean} debug - Debug mode
134
+ */
135
+ export function printNormalizedError(error, context = {}, debug = false) {
136
+ const normalized = normalizeError(error, context, debug);
137
+
138
+ console.error(`\nError: ${normalized.message}\n`);
139
+
140
+ if (normalized.nextSteps && normalized.nextSteps.length > 0) {
141
+ console.error('Next steps:');
142
+ normalized.nextSteps.forEach(step => {
143
+ console.error(` ${step}`);
144
+ });
145
+ console.error('');
146
+ }
147
+
148
+ if (normalized.stack && debug) {
149
+ console.error('Stack trace:');
150
+ console.error(normalized.stack);
151
+ console.error('');
152
+ }
153
+ }
154
+
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Wave 4 — Explain Output
3
+ *
4
+ * Human-readable explanation of expectations and their usage.
5
+ */
6
+
7
+ /**
8
+ * Format expectation for --explain output
9
+ * @param {Object} expectation - Expectation object from expectations.json
10
+ * @returns {string} Formatted string
11
+ */
12
+ function formatExpectation(expectation) {
13
+ const lines = [];
14
+
15
+ lines.push(` ID: ${expectation.id}`);
16
+ lines.push(` Type: ${expectation.type}`);
17
+ lines.push(` Proof: ${expectation.proof}`);
18
+ lines.push(` Reason: ${expectation.reason}`);
19
+
20
+ if (expectation.source) {
21
+ const sourceStr = expectation.source.file
22
+ ? `${expectation.source.file}${expectation.source.line ? `:${expectation.source.line}` : ''}`
23
+ : 'unknown source';
24
+ lines.push(` Source: ${sourceStr}`);
25
+ }
26
+
27
+ lines.push(` Used: ${expectation.used ? 'YES' : 'NO'}`);
28
+
29
+ if (expectation.usedReason) {
30
+ lines.push(` Used Reason: ${expectation.usedReason}`);
31
+ } else if (!expectation.used) {
32
+ lines.push(` Not Used: ${expectation.usedReason || 'no matching interaction'}`);
33
+ }
34
+
35
+ return lines.join('\n');
36
+ }
37
+
38
+ /**
39
+ * Print explain output
40
+ * @param {Array} expectations - Array of expectation objects
41
+ * @param {Object} summary - Expectations summary
42
+ */
43
+ export function printExplainOutput(expectations, summary) {
44
+ console.error('\n' + '═'.repeat(60));
45
+ console.error('VERAX Expectations Explanation');
46
+ console.error('═'.repeat(60));
47
+ console.error('');
48
+
49
+ console.error(`Summary:`);
50
+ console.error(` Total expectations: ${summary.total}`);
51
+ console.error(` By type: navigation=${summary.byType.navigation}, network_action=${summary.byType.network_action}, state_action=${summary.byType.state_action}`);
52
+ console.error(` Used: ${summary.total - summary.skipped}`);
53
+ console.error(` Unused: ${summary.skipped}`);
54
+ console.error('');
55
+
56
+ if (expectations.length === 0) {
57
+ console.error('No expectations found in your project.');
58
+ console.error('VERAX needs static code patterns to create expectations.');
59
+ console.error('See README for supported patterns.');
60
+ console.error('');
61
+ return;
62
+ }
63
+
64
+ // Group by type
65
+ const byType = {
66
+ navigation: expectations.filter(e => e.type === 'navigation'),
67
+ network_action: expectations.filter(e => e.type === 'network_action'),
68
+ state_action: expectations.filter(e => e.type === 'state_action')
69
+ };
70
+
71
+ // Print by type
72
+ for (const [type, exps] of Object.entries(byType)) {
73
+ if (exps.length === 0) continue;
74
+
75
+ console.error(`${type.toUpperCase().replace('_', ' ')} (${exps.length}):`);
76
+ console.error('─'.repeat(60));
77
+
78
+ for (const exp of exps) {
79
+ console.error(formatExpectation(exp));
80
+ console.error('');
81
+ }
82
+ }
83
+
84
+ // Summary of unused expectations
85
+ const unused = expectations.filter(e => !e.used);
86
+ if (unused.length > 0) {
87
+ console.error('Unused Expectations Summary:');
88
+ console.error('─'.repeat(60));
89
+
90
+ const unusedByReason = {};
91
+ for (const exp of unused) {
92
+ const reason = exp.usedReason || 'no matching interaction';
93
+ unusedByReason[reason] = (unusedByReason[reason] || 0) + 1;
94
+ }
95
+
96
+ for (const [reason, count] of Object.entries(unusedByReason)) {
97
+ console.error(` ${reason}: ${count}`);
98
+ }
99
+ console.error('');
100
+ }
101
+
102
+ console.error('═'.repeat(60));
103
+ console.error('');
104
+ }
105
+