@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.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +14 -36
- package/src/cli/commands/default.js +523 -0
- package/src/cli/commands/doctor.js +165 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +402 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +296 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +34 -0
- package/src/cli/util/expectation-extractor.js +378 -0
- package/src/cli/util/findings-writer.js +31 -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 +366 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +29 -0
- package/src/cli/util/project-discovery.js +277 -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/summary-writer.js +32 -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 +101 -0
- package/src/verax/cli/wizard.js +98 -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 +403 -0
- package/src/verax/core/incremental-store.js +237 -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 +521 -0
- package/src/verax/detect/comparison.js +2 -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 +177 -0
- package/src/verax/detect/expectation-model.js +194 -172
- 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 +44 -8
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +172 -286
- package/src/verax/detect/interactive-findings.js +613 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/verdict-engine.js +563 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/index.js +90 -14
- 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 +579 -0
- package/src/verax/intel/vue-router-extractor.js +323 -0
- package/src/verax/learn/action-contract-extractor.js +335 -101
- package/src/verax/learn/ast-contract-extractor.js +95 -5
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/manifest-writer.js +97 -47
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +27 -96
- 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 +112 -4
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +10 -5
- package/src/verax/observe/console-sensor.js +1 -17
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +512 -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 +643 -275
- package/src/verax/observe/index.js +908 -27
- package/src/verax/observe/index.js.backup +1 -0
- package/src/verax/observe/interaction-discovery.js +365 -14
- package/src/verax/observe/interaction-runner.js +563 -198
- package/src/verax/observe/loading-sensor.js +139 -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 +37 -17
- package/src/verax/observe/state-sensor.js +389 -0
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +61 -20
- package/src/verax/observe/ui-signal-sensor.js +136 -17
- package/src/verax/scan-summary-writer.js +77 -15
- package/src/verax/shared/artifact-manager.js +110 -8
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +170 -0
- package/src/verax/shared/dynamic-route-utils.js +218 -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 +14 -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 +65 -0
- package/src/verax/validate/context-validator.js +244 -0
- package/src/verax/validate/context-validator.js.bak +0 -0
|
@@ -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
|
+
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 6 — Finding Explanation
|
|
3
|
+
*
|
|
4
|
+
* Formats findings in a clear, human-readable way showing the chain:
|
|
5
|
+
* Expectation → Observation → Mismatch → Why this is a silent failure
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Format a single finding for console output
|
|
10
|
+
* @param {Object} finding - Finding object
|
|
11
|
+
* @param {Object} expectation - Related expectation (if available)
|
|
12
|
+
* @returns {string} Formatted finding text
|
|
13
|
+
*/
|
|
14
|
+
export function formatFinding(finding, expectation = null) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
|
|
17
|
+
// Finding type and confidence
|
|
18
|
+
const confidenceLevel = finding.confidence?.level || 'UNKNOWN';
|
|
19
|
+
const confidenceScore = finding.confidence?.score || 0;
|
|
20
|
+
lines.push(` [${confidenceLevel} (${confidenceScore}%)] ${finding.type || 'unknown'}`);
|
|
21
|
+
|
|
22
|
+
// Expectation (what was promised)
|
|
23
|
+
if (expectation) {
|
|
24
|
+
let expectationDesc = '';
|
|
25
|
+
if (expectation.type === 'navigation') {
|
|
26
|
+
const target = expectation.raw?.targetPath || expectation.targetPath || 'route';
|
|
27
|
+
expectationDesc = `Expected navigation to ${target}`;
|
|
28
|
+
} else if (expectation.type === 'network_action') {
|
|
29
|
+
const method = expectation.raw?.method || 'request';
|
|
30
|
+
const url = expectation.raw?.urlPath || expectation.urlPath || 'endpoint';
|
|
31
|
+
expectationDesc = `Expected ${method} request to ${url}`;
|
|
32
|
+
} else if (expectation.type === 'state_action') {
|
|
33
|
+
expectationDesc = `Expected state mutation`;
|
|
34
|
+
} else {
|
|
35
|
+
expectationDesc = `Expected ${expectation.type}`;
|
|
36
|
+
}
|
|
37
|
+
lines.push(` └─ Expectation: ${expectationDesc}`);
|
|
38
|
+
} else if (finding.expectationId) {
|
|
39
|
+
lines.push(` └─ Expectation: Referenced expectation ${finding.expectationId}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Observation (what actually happened)
|
|
43
|
+
const interaction = finding.interaction || {};
|
|
44
|
+
if (interaction.type) {
|
|
45
|
+
let interactionDesc = `${interaction.type}`;
|
|
46
|
+
if (interaction.selector) {
|
|
47
|
+
interactionDesc += ` on "${interaction.label || interaction.selector}"`;
|
|
48
|
+
}
|
|
49
|
+
lines.push(` └─ Observation: User interacted (${interactionDesc})`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Mismatch (what went wrong)
|
|
53
|
+
const evidence = finding.evidence || {};
|
|
54
|
+
if (finding.type === 'silent_failure' || finding.type === 'navigation_silent_failure') {
|
|
55
|
+
if (!evidence.hasUrlChange && !evidence.hasVisibleChange) {
|
|
56
|
+
lines.push(` └─ Mismatch: No navigation occurred, no visible change`);
|
|
57
|
+
} else if (evidence.hasUrlChange && !evidence.hasVisibleChange) {
|
|
58
|
+
lines.push(` └─ Mismatch: URL changed but no visible feedback`);
|
|
59
|
+
} else {
|
|
60
|
+
lines.push(` └─ Mismatch: Expected navigation did not occur`);
|
|
61
|
+
}
|
|
62
|
+
} else if (finding.type === 'network_silent_failure') {
|
|
63
|
+
if (evidence.slowRequests && evidence.slowRequests > 0) {
|
|
64
|
+
lines.push(` └─ Mismatch: Request was slow (${evidence.slowRequests} slow request(s))`);
|
|
65
|
+
} else {
|
|
66
|
+
lines.push(` └─ Mismatch: Request failed or returned error with no user feedback`);
|
|
67
|
+
}
|
|
68
|
+
} else if (finding.type === 'missing_network_action') {
|
|
69
|
+
lines.push(` └─ Mismatch: Expected network request never fired`);
|
|
70
|
+
} else if (finding.type === 'missing_state_action') {
|
|
71
|
+
lines.push(` └─ Mismatch: Expected state change did not occur`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Why this is a silent failure
|
|
75
|
+
if (finding.reason) {
|
|
76
|
+
lines.push(` └─ Silent Failure: ${finding.reason}`);
|
|
77
|
+
} else {
|
|
78
|
+
lines.push(` └─ Silent Failure: User receives no feedback when expected behavior fails`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Source location if available
|
|
82
|
+
if (expectation?.source?.file) {
|
|
83
|
+
const sourceLine = expectation.source.line ? `:${expectation.source.line}` : '';
|
|
84
|
+
lines.push(` └─ Source: ${expectation.source.file}${sourceLine}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Print top findings with explanations
|
|
92
|
+
* @param {Array} findings - Array of finding objects
|
|
93
|
+
* @param {Array} expectations - Array of expectation objects (for lookup)
|
|
94
|
+
* @param {number} limit - Maximum number of findings to print
|
|
95
|
+
*/
|
|
96
|
+
export function printFindings(findings, expectations = [], limit = 5) {
|
|
97
|
+
if (!findings || findings.length === 0) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.error('\n' + '─'.repeat(60));
|
|
102
|
+
console.error(`Top Findings (${Math.min(findings.length, limit)} of ${findings.length})`);
|
|
103
|
+
console.error('─'.repeat(60));
|
|
104
|
+
|
|
105
|
+
const topFindings = findings.slice(0, limit);
|
|
106
|
+
|
|
107
|
+
// Create expectation lookup map
|
|
108
|
+
const expectationMap = new Map();
|
|
109
|
+
for (const exp of expectations) {
|
|
110
|
+
if (exp.id) {
|
|
111
|
+
expectationMap.set(exp.id, exp);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
topFindings.forEach((finding, index) => {
|
|
116
|
+
const expectation = finding.expectationId
|
|
117
|
+
? expectationMap.get(finding.expectationId)
|
|
118
|
+
: null;
|
|
119
|
+
|
|
120
|
+
console.error(`\n${index + 1}.`);
|
|
121
|
+
console.error(formatFinding(finding, expectation));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (findings.length > limit) {
|
|
125
|
+
console.error(`\n... and ${findings.length - limit} more (see findings.json)`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.error('─'.repeat(60) + '\n');
|
|
129
|
+
}
|
|
130
|
+
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 7 — VERAX Init
|
|
3
|
+
*
|
|
4
|
+
* Initializes VERAX configuration and templates.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { resolve, dirname } from 'path';
|
|
9
|
+
import { getDefaultConfig } from '../shared/config-loader.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialize VERAX configuration
|
|
13
|
+
* @param {Object} options - { projectRoot, yes, ciTemplate, flowTemplate }
|
|
14
|
+
* @returns {Promise<Object>} { created: string[], skipped: string[] }
|
|
15
|
+
*/
|
|
16
|
+
export async function runInit(options = {}) {
|
|
17
|
+
const {
|
|
18
|
+
projectRoot = process.cwd(),
|
|
19
|
+
yes = false,
|
|
20
|
+
ciTemplate = null,
|
|
21
|
+
flowTemplate = null
|
|
22
|
+
} = options;
|
|
23
|
+
|
|
24
|
+
const created = [];
|
|
25
|
+
const skipped = [];
|
|
26
|
+
|
|
27
|
+
// Create .verax directory if needed
|
|
28
|
+
const veraxDir = resolve(projectRoot, '.verax');
|
|
29
|
+
mkdirSync(veraxDir, { recursive: true });
|
|
30
|
+
|
|
31
|
+
// Create config.json
|
|
32
|
+
const configPath = resolve(veraxDir, 'config.json');
|
|
33
|
+
if (existsSync(configPath) && !yes) {
|
|
34
|
+
skipped.push('config.json');
|
|
35
|
+
} else {
|
|
36
|
+
const defaultConfig = getDefaultConfig();
|
|
37
|
+
writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n');
|
|
38
|
+
created.push('config.json');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Create CI template if requested
|
|
42
|
+
if (ciTemplate === 'github') {
|
|
43
|
+
const workflowsDir = resolve(projectRoot, '.github', 'workflows');
|
|
44
|
+
mkdirSync(workflowsDir, { recursive: true });
|
|
45
|
+
|
|
46
|
+
const workflowFile = resolve(workflowsDir, 'verax-ci.yml');
|
|
47
|
+
if (existsSync(workflowFile) && !yes) {
|
|
48
|
+
skipped.push('.github/workflows/verax-ci.yml');
|
|
49
|
+
} else {
|
|
50
|
+
const workflowContent = `name: VERAX CI
|
|
51
|
+
|
|
52
|
+
on:
|
|
53
|
+
workflow_dispatch:
|
|
54
|
+
pull_request:
|
|
55
|
+
|
|
56
|
+
jobs:
|
|
57
|
+
verax-scan:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- name: Checkout
|
|
61
|
+
uses: actions/checkout@v4
|
|
62
|
+
|
|
63
|
+
- name: Setup Node.js
|
|
64
|
+
uses: actions/setup-node@v4
|
|
65
|
+
with:
|
|
66
|
+
node-version: '20'
|
|
67
|
+
cache: 'npm'
|
|
68
|
+
|
|
69
|
+
- name: Install dependencies
|
|
70
|
+
run: npm ci
|
|
71
|
+
|
|
72
|
+
- name: Install Playwright browsers
|
|
73
|
+
run: npx playwright install --with-deps chromium
|
|
74
|
+
|
|
75
|
+
- name: Start fixture server
|
|
76
|
+
id: fixture-server
|
|
77
|
+
run: |
|
|
78
|
+
node test/helpers/fixture-server.js &
|
|
79
|
+
SERVER_PID=$!
|
|
80
|
+
echo "SERVER_PID=$SERVER_PID" >> $GITHUB_ENV
|
|
81
|
+
sleep 3
|
|
82
|
+
echo "url=http://127.0.0.1:8888" >> $GITHUB_OUTPUT
|
|
83
|
+
working-directory: \${{ github.workspace }}
|
|
84
|
+
|
|
85
|
+
- name: Run VERAX CI scan
|
|
86
|
+
id: verax
|
|
87
|
+
run: |
|
|
88
|
+
npx @veraxhq/verax ci --url \${{ steps.fixture-server.outputs.url }} --projectRoot .
|
|
89
|
+
continue-on-error: true
|
|
90
|
+
|
|
91
|
+
- name: Stop fixture server
|
|
92
|
+
if: always()
|
|
93
|
+
run: |
|
|
94
|
+
if [ ! -z "$SERVER_PID" ]; then
|
|
95
|
+
kill $SERVER_PID 2>/dev/null || true
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
- name: Upload VERAX artifacts
|
|
99
|
+
if: always()
|
|
100
|
+
uses: actions/upload-artifact@v4
|
|
101
|
+
with:
|
|
102
|
+
name: verax-artifacts
|
|
103
|
+
path: |
|
|
104
|
+
.verax/runs/**/*
|
|
105
|
+
.verax/verax-run-*.zip
|
|
106
|
+
retention-days: 7
|
|
107
|
+
if-no-files-found: ignore
|
|
108
|
+
|
|
109
|
+
- name: Check VERAX exit code and fail gate
|
|
110
|
+
if: always()
|
|
111
|
+
run: |
|
|
112
|
+
EXIT_CODE=\${{ steps.verax.exitcode }}
|
|
113
|
+
if [ "$EXIT_CODE" == "" ]; then
|
|
114
|
+
EXIT_CODE=\${{ steps.verax.outcome == 'success' && 0 || 1 }}
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
if [ "$EXIT_CODE" == "0" ]; then
|
|
118
|
+
echo "✅ VERAX: VERIFIED - Scan passed"
|
|
119
|
+
elif [ "$EXIT_CODE" == "1" ]; then
|
|
120
|
+
echo "⚠️ VERAX: NO_EXPECTATIONS_FOUND or MEDIUM/LOW findings"
|
|
121
|
+
echo "Gate passes (non-blocking)"
|
|
122
|
+
elif [ "$EXIT_CODE" == "2" ]; then
|
|
123
|
+
echo "❌ VERAX: HIGH severity findings detected"
|
|
124
|
+
echo "Gate fails (blocking)"
|
|
125
|
+
exit 2
|
|
126
|
+
elif [ "$EXIT_CODE" == "4" ]; then
|
|
127
|
+
echo "❌ VERAX: INVALID_CONTEXT - URL does not match project"
|
|
128
|
+
echo "Gate fails (blocking)"
|
|
129
|
+
exit 4
|
|
130
|
+
elif [ "$EXIT_CODE" == "3" ]; then
|
|
131
|
+
echo "❌ VERAX: FATAL error"
|
|
132
|
+
echo "Gate fails (blocking)"
|
|
133
|
+
exit 3
|
|
134
|
+
else
|
|
135
|
+
echo "❌ VERAX: Unexpected exit code $EXIT_CODE"
|
|
136
|
+
exit $EXIT_CODE
|
|
137
|
+
fi
|
|
138
|
+
`;
|
|
139
|
+
writeFileSync(workflowFile, workflowContent);
|
|
140
|
+
created.push('.github/workflows/verax-ci.yml');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Create flow template if requested
|
|
145
|
+
if (flowTemplate === 'login') {
|
|
146
|
+
const flowsDir = resolve(projectRoot, 'flows');
|
|
147
|
+
mkdirSync(flowsDir, { recursive: true });
|
|
148
|
+
|
|
149
|
+
const flowFile = resolve(flowsDir, 'login.json');
|
|
150
|
+
if (existsSync(flowFile) && !yes) {
|
|
151
|
+
skipped.push('flows/login.json');
|
|
152
|
+
} else {
|
|
153
|
+
const flowContent = {
|
|
154
|
+
name: 'login',
|
|
155
|
+
description: 'User login flow',
|
|
156
|
+
steps: [
|
|
157
|
+
{
|
|
158
|
+
type: 'goto',
|
|
159
|
+
url: '${VERAX_BASE_URL}/login'
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
type: 'fill',
|
|
163
|
+
selector: 'input[name="email"]',
|
|
164
|
+
value: '${VERAX_TEST_EMAIL}'
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'fill',
|
|
168
|
+
selector: 'input[name="password"]',
|
|
169
|
+
value: '${VERAX_TEST_PASSWORD}',
|
|
170
|
+
isSecret: true
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: 'click',
|
|
174
|
+
selector: 'button[type="submit"]'
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
type: 'wait',
|
|
178
|
+
selector: '[data-testid="dashboard"]',
|
|
179
|
+
timeout: 5000
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
};
|
|
183
|
+
writeFileSync(flowFile, JSON.stringify(flowContent, null, 2) + '\n');
|
|
184
|
+
created.push('flows/login.json');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { created, skipped };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Print init results
|
|
193
|
+
* @param {Object} results - Init results
|
|
194
|
+
*/
|
|
195
|
+
export function printInitResults(results) {
|
|
196
|
+
console.error('\n' + '═'.repeat(60));
|
|
197
|
+
console.error('VERAX Init');
|
|
198
|
+
console.error('═'.repeat(60));
|
|
199
|
+
|
|
200
|
+
if (results.created.length > 0) {
|
|
201
|
+
console.error('\n✅ Created:');
|
|
202
|
+
results.created.forEach(file => {
|
|
203
|
+
console.error(` • ${file}`);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (results.skipped.length > 0) {
|
|
208
|
+
console.error('\n⏭️ Skipped (already exist):');
|
|
209
|
+
results.skipped.forEach(file => {
|
|
210
|
+
console.error(` • ${file}`);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (results.created.includes('config.json')) {
|
|
215
|
+
console.error('\n📝 Next Steps:');
|
|
216
|
+
console.error(' 1. Review .verax/config.json and adjust settings');
|
|
217
|
+
console.error(' 2. Run: verax doctor (to verify setup)');
|
|
218
|
+
console.error(' 3. Run: verax run --url <your-url>');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (results.created.includes('.github/workflows/verax-ci.yml')) {
|
|
222
|
+
console.error('\n🔧 CI Setup:');
|
|
223
|
+
console.error(' • Review .github/workflows/verax-ci.yml');
|
|
224
|
+
console.error(' • Update URL placeholder with your deployment URL');
|
|
225
|
+
console.error(' • Commit and push to enable VERAX in CI');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (results.created.includes('flows/login.json')) {
|
|
229
|
+
console.error('\n🔐 Flow Template:');
|
|
230
|
+
console.error(' • Review flows/login.json');
|
|
231
|
+
console.error(' • Set VERAX_TEST_EMAIL and VERAX_TEST_PASSWORD environment variables');
|
|
232
|
+
console.error(' • Note: Flow commands are not available in this version');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.error('═'.repeat(60) + '\n');
|
|
236
|
+
}
|
|
237
|
+
|