@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,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
|
+
|