@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,109 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
3
|
+
import { DataError } from '../util/errors.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `verax inspect` command
|
|
7
|
+
* Read an existing run folder and display summary
|
|
8
|
+
*/
|
|
9
|
+
export async function inspectCommand(runPath, options = {}) {
|
|
10
|
+
const { json = false } = options;
|
|
11
|
+
|
|
12
|
+
const fullPath = resolve(runPath);
|
|
13
|
+
|
|
14
|
+
// Validate run directory exists
|
|
15
|
+
if (!existsSync(fullPath)) {
|
|
16
|
+
throw new DataError(`Run directory not found: ${fullPath}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check for required files
|
|
20
|
+
const requiredFiles = ['summary.json', 'findings.json'];
|
|
21
|
+
const missingFiles = [];
|
|
22
|
+
|
|
23
|
+
for (const file of requiredFiles) {
|
|
24
|
+
const filePath = `${fullPath}/${file}`;
|
|
25
|
+
if (!existsSync(filePath)) {
|
|
26
|
+
missingFiles.push(file);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (missingFiles.length > 0) {
|
|
31
|
+
throw new DataError(
|
|
32
|
+
`Invalid run directory. Missing files: ${missingFiles.join(', ')}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Read summary and findings
|
|
37
|
+
let summary, findings;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
summary = JSON.parse(readFileSync(`${fullPath}/summary.json`, 'utf8'));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new DataError(`Failed to parse summary.json: ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
findings = JSON.parse(readFileSync(`${fullPath}/findings.json`, 'utf8'));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new DataError(`Failed to parse findings.json: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check for evidence directory
|
|
52
|
+
const evidenceDir = `${fullPath}/evidence`;
|
|
53
|
+
const hasEvidence = existsSync(evidenceDir);
|
|
54
|
+
let evidenceCount = 0;
|
|
55
|
+
|
|
56
|
+
if (hasEvidence) {
|
|
57
|
+
try {
|
|
58
|
+
evidenceCount = readdirSync(evidenceDir).length;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
evidenceCount = 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Build output
|
|
65
|
+
const output = {
|
|
66
|
+
runId: summary.runId || 'unknown',
|
|
67
|
+
status: summary.status || 'unknown',
|
|
68
|
+
startedAt: summary.startedAt || null,
|
|
69
|
+
completedAt: summary.completedAt || null,
|
|
70
|
+
url: summary.url || null,
|
|
71
|
+
findingsCount: Array.isArray(findings) ? findings.length : 0,
|
|
72
|
+
evidenceDir: hasEvidence ? evidenceDir : null,
|
|
73
|
+
evidenceFileCount: evidenceCount,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (json) {
|
|
77
|
+
// Output as single JSON object
|
|
78
|
+
console.log(JSON.stringify(output, null, 2));
|
|
79
|
+
} else {
|
|
80
|
+
// Output as human-readable summary
|
|
81
|
+
console.log('\n=== Run Summary ===\n');
|
|
82
|
+
console.log(`Run ID: ${output.runId}`);
|
|
83
|
+
console.log(`Status: ${output.status}`);
|
|
84
|
+
|
|
85
|
+
if (output.startedAt) {
|
|
86
|
+
console.log(`Started: ${output.startedAt}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (output.completedAt) {
|
|
90
|
+
console.log(`Completed: ${output.completedAt}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (output.url) {
|
|
94
|
+
console.log(`URL: ${output.url}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`\nFindings: ${output.findingsCount}`);
|
|
98
|
+
|
|
99
|
+
if (output.evidenceDir) {
|
|
100
|
+
console.log(`Evidence: ${output.evidenceDir} (${output.evidenceFileCount} files)`);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(`Evidence: not found`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log('');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return output;
|
|
109
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { resolve, join } from 'path';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname } from 'path';
|
|
5
|
+
import { UsageError, DataError, CrashError } from '../util/errors.js';
|
|
6
|
+
import { generateRunId } from '../util/run-id.js';
|
|
7
|
+
import { getRunPaths, ensureRunDirectories } from '../util/paths.js';
|
|
8
|
+
import { atomicWriteJson, atomicWriteText } from '../util/atomic-write.js';
|
|
9
|
+
import { RunEventEmitter } from '../util/events.js';
|
|
10
|
+
import { discoverProject } from '../util/project-discovery.js';
|
|
11
|
+
import { writeProjectJson } from '../util/project-writer.js';
|
|
12
|
+
import { extractExpectations } from '../util/expectation-extractor.js';
|
|
13
|
+
import { writeLearnJson } from '../util/learn-writer.js';
|
|
14
|
+
import { observeExpectations } from '../util/observation-engine.js';
|
|
15
|
+
import { writeObserveJson } from '../util/observe-writer.js';
|
|
16
|
+
import { detectFindings } from '../util/detection-engine.js';
|
|
17
|
+
import { writeFindingsJson } from '../util/findings-writer.js';
|
|
18
|
+
import { writeSummaryJson } from '../util/summary-writer.js';
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = dirname(__filename);
|
|
22
|
+
|
|
23
|
+
function getVersion() {
|
|
24
|
+
try {
|
|
25
|
+
const pkgPath = resolve(__dirname, '../../../package.json');
|
|
26
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
27
|
+
return pkg.version;
|
|
28
|
+
} catch {
|
|
29
|
+
return '0.2.0';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `verax run` command
|
|
35
|
+
* Strict, non-interactive CLI mode with explicit flags
|
|
36
|
+
*/
|
|
37
|
+
export async function runCommand(options) {
|
|
38
|
+
const {
|
|
39
|
+
url,
|
|
40
|
+
src = '.',
|
|
41
|
+
out = '.verax',
|
|
42
|
+
json = false,
|
|
43
|
+
verbose = false,
|
|
44
|
+
} = options;
|
|
45
|
+
|
|
46
|
+
// Validate required arguments
|
|
47
|
+
if (!url) {
|
|
48
|
+
throw new UsageError('Missing required argument: --url <url>');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const projectRoot = resolve(process.cwd());
|
|
52
|
+
const srcPath = resolve(projectRoot, src);
|
|
53
|
+
|
|
54
|
+
// Validate src directory exists
|
|
55
|
+
if (!existsSync(srcPath)) {
|
|
56
|
+
throw new DataError(`Source directory not found: ${srcPath}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Create event emitter
|
|
60
|
+
const events = new RunEventEmitter();
|
|
61
|
+
|
|
62
|
+
// Setup event handlers
|
|
63
|
+
if (json) {
|
|
64
|
+
events.on('*', (event) => {
|
|
65
|
+
console.log(JSON.stringify(event));
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
events.on('*', (event) => {
|
|
69
|
+
if (verbose) {
|
|
70
|
+
console.log(`[${event.type}] ${event.message || ''}`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let runId = null;
|
|
76
|
+
let paths = null;
|
|
77
|
+
let startedAt = null;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Generate run ID
|
|
81
|
+
runId = generateRunId();
|
|
82
|
+
if (verbose && !json) console.log(`Run ID: ${runId}`);
|
|
83
|
+
|
|
84
|
+
paths = getRunPaths(projectRoot, out, runId);
|
|
85
|
+
ensureRunDirectories(paths);
|
|
86
|
+
|
|
87
|
+
// Discover project configuration
|
|
88
|
+
let projectProfile;
|
|
89
|
+
try {
|
|
90
|
+
projectProfile = await discoverProject(srcPath);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
projectProfile = {
|
|
93
|
+
framework: 'unknown',
|
|
94
|
+
router: null,
|
|
95
|
+
sourceRoot: srcPath,
|
|
96
|
+
packageManager: 'unknown',
|
|
97
|
+
scripts: { dev: null, build: null, start: null },
|
|
98
|
+
detectedAt: new Date().toISOString(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Emit project detection events
|
|
103
|
+
events.emit('project:detected', {
|
|
104
|
+
framework: projectProfile.framework,
|
|
105
|
+
router: projectProfile.router,
|
|
106
|
+
sourceRoot: projectProfile.sourceRoot,
|
|
107
|
+
packageManager: projectProfile.packageManager,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Emit phase events
|
|
111
|
+
events.emit('phase:started', {
|
|
112
|
+
phase: 'Detect Project',
|
|
113
|
+
message: 'Detecting project structure...',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
events.emit('phase:started', {
|
|
117
|
+
phase: 'Resolve URL',
|
|
118
|
+
message: `Using URL: ${url}`,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
events.emit('phase:started', {
|
|
122
|
+
phase: 'Initialize Run',
|
|
123
|
+
message: 'Initializing run artifacts...',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Write initial status
|
|
127
|
+
const now = new Date();
|
|
128
|
+
startedAt = now.toISOString();
|
|
129
|
+
|
|
130
|
+
atomicWriteJson(paths.runStatusJson, {
|
|
131
|
+
status: 'RUNNING',
|
|
132
|
+
runId,
|
|
133
|
+
startedAt,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Write metadata
|
|
137
|
+
atomicWriteJson(paths.runMetaJson, {
|
|
138
|
+
veraxVersion: getVersion(),
|
|
139
|
+
nodeVersion: process.version,
|
|
140
|
+
platform: process.platform,
|
|
141
|
+
cwd: projectRoot,
|
|
142
|
+
command: 'run',
|
|
143
|
+
args: { url, src, out },
|
|
144
|
+
url,
|
|
145
|
+
src: srcPath,
|
|
146
|
+
startedAt,
|
|
147
|
+
completedAt: null,
|
|
148
|
+
error: null,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Simulate learning phase (placeholder)
|
|
152
|
+
events.emit('phase:started', {
|
|
153
|
+
phase: 'Learn',
|
|
154
|
+
message: 'Analyzing project structure...',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Extract expectations
|
|
158
|
+
const { expectations, skipped } = await extractExpectations(projectProfile, projectProfile.sourceRoot);
|
|
159
|
+
|
|
160
|
+
// For now, emit a placeholder trace event
|
|
161
|
+
events.emit('phase:completed', {
|
|
162
|
+
phase: 'Learn',
|
|
163
|
+
message: 'Project analysis complete',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Observe phase
|
|
167
|
+
events.emit('phase:started', {
|
|
168
|
+
phase: 'Observe',
|
|
169
|
+
message: 'Launching browser and observing expectations...',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
let observeData = null;
|
|
173
|
+
if (expectations.length > 0) {
|
|
174
|
+
try {
|
|
175
|
+
observeData = await observeExpectations(
|
|
176
|
+
expectations,
|
|
177
|
+
url,
|
|
178
|
+
paths.evidenceDir,
|
|
179
|
+
(progress) => {
|
|
180
|
+
events.emit(progress.event, progress);
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
events.emit('observe:error', {
|
|
185
|
+
message: error.message,
|
|
186
|
+
});
|
|
187
|
+
observeData = {
|
|
188
|
+
observations: [],
|
|
189
|
+
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
190
|
+
observedAt: new Date().toISOString(),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
observeData = {
|
|
195
|
+
observations: [],
|
|
196
|
+
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
197
|
+
observedAt: new Date().toISOString(),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
events.emit('phase:completed', {
|
|
202
|
+
phase: 'Observe',
|
|
203
|
+
message: 'Browser observation complete',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Detect phase
|
|
207
|
+
events.emit('phase:started', {
|
|
208
|
+
phase: 'Detect',
|
|
209
|
+
message: 'Analyzing findings and detecting silent failures...',
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
let detectData = null;
|
|
213
|
+
try {
|
|
214
|
+
// Use already-extracted expectations
|
|
215
|
+
const learnData = {
|
|
216
|
+
expectations,
|
|
217
|
+
skipped,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
detectData = await detectFindings(learnData, observeData, projectRoot, (progress) => {
|
|
221
|
+
events.emit(progress.event, progress);
|
|
222
|
+
});
|
|
223
|
+
} catch (error) {
|
|
224
|
+
events.emit('detect:error', {
|
|
225
|
+
message: error.message,
|
|
226
|
+
});
|
|
227
|
+
detectData = {
|
|
228
|
+
findings: [],
|
|
229
|
+
stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
|
|
230
|
+
detectedAt: new Date().toISOString(),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
events.emit('phase:completed', {
|
|
235
|
+
phase: 'Detect',
|
|
236
|
+
message: 'Silent failure detection complete',
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Emit finalize phase
|
|
240
|
+
events.emit('phase:started', {
|
|
241
|
+
phase: 'Finalize Artifacts',
|
|
242
|
+
message: 'Writing run results...',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const completedAt = new Date().toISOString();
|
|
246
|
+
|
|
247
|
+
// Write completed status
|
|
248
|
+
atomicWriteJson(paths.runStatusJson, {
|
|
249
|
+
status: 'COMPLETE',
|
|
250
|
+
runId,
|
|
251
|
+
startedAt,
|
|
252
|
+
completedAt,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Update metadata with completion time
|
|
256
|
+
atomicWriteJson(paths.runMetaJson, {
|
|
257
|
+
veraxVersion: getVersion(),
|
|
258
|
+
nodeVersion: process.version,
|
|
259
|
+
platform: process.platform,
|
|
260
|
+
cwd: projectRoot,
|
|
261
|
+
command: 'run',
|
|
262
|
+
args: { url, src, out },
|
|
263
|
+
url,
|
|
264
|
+
src: srcPath,
|
|
265
|
+
startedAt,
|
|
266
|
+
completedAt,
|
|
267
|
+
error: null,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Write summary with stable digest
|
|
271
|
+
writeSummaryJson(paths.summaryJson, {
|
|
272
|
+
runId,
|
|
273
|
+
status: 'COMPLETE',
|
|
274
|
+
startedAt,
|
|
275
|
+
completedAt,
|
|
276
|
+
command: 'run',
|
|
277
|
+
url,
|
|
278
|
+
notes: 'Run completed successfully',
|
|
279
|
+
}, {
|
|
280
|
+
expectationsTotal: expectations.length,
|
|
281
|
+
attempted: observeData.stats?.attempted || 0,
|
|
282
|
+
observed: observeData.stats?.observed || 0,
|
|
283
|
+
silentFailures: detectData.stats?.silentFailures || 0,
|
|
284
|
+
coverageGaps: detectData.stats?.coverageGaps || 0,
|
|
285
|
+
unproven: detectData.stats?.unproven || 0,
|
|
286
|
+
informational: detectData.stats?.informational || 0,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Write detect results (or empty if detection failed)
|
|
290
|
+
writeFindingsJson(paths.baseDir, detectData);
|
|
291
|
+
|
|
292
|
+
// Write traces (at least phase events)
|
|
293
|
+
const traces = [
|
|
294
|
+
{
|
|
295
|
+
type: 'phase:started',
|
|
296
|
+
timestamp: startedAt,
|
|
297
|
+
phase: 'Detect Project',
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
type: 'phase:completed',
|
|
301
|
+
timestamp: new Date().toISOString(),
|
|
302
|
+
phase: 'Detect Project',
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
type: 'phase:started',
|
|
306
|
+
timestamp: new Date().toISOString(),
|
|
307
|
+
phase: 'Learn',
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
type: 'phase:completed',
|
|
311
|
+
timestamp: completedAt,
|
|
312
|
+
phase: 'Learn',
|
|
313
|
+
},
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
const tracesContent = traces.map(t => JSON.stringify(t)).join('\n') + '\n';
|
|
317
|
+
atomicWriteText(paths.tracesJsonl, tracesContent);
|
|
318
|
+
|
|
319
|
+
// Write project profile
|
|
320
|
+
writeProjectJson(paths, projectProfile);
|
|
321
|
+
|
|
322
|
+
// Write learn results
|
|
323
|
+
writeLearnJson(paths, expectations, skipped);
|
|
324
|
+
|
|
325
|
+
// Write observe results
|
|
326
|
+
writeObserveJson(paths.baseDir, observeData);
|
|
327
|
+
|
|
328
|
+
events.emit('phase:completed', {
|
|
329
|
+
phase: 'Finalize Artifacts',
|
|
330
|
+
message: 'Run artifacts written',
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Print summary if not JSON mode
|
|
334
|
+
if (!json) {
|
|
335
|
+
console.log('\nRun complete.');
|
|
336
|
+
console.log(`Run ID: ${runId}`);
|
|
337
|
+
console.log(`Artifacts: ${paths.baseDir}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { runId, paths, success: true };
|
|
341
|
+
} catch (error) {
|
|
342
|
+
// Mark run as FAILED if we have paths
|
|
343
|
+
if (paths && runId && startedAt) {
|
|
344
|
+
try {
|
|
345
|
+
const failedAt = new Date().toISOString();
|
|
346
|
+
atomicWriteJson(paths.runStatusJson, {
|
|
347
|
+
status: 'FAILED',
|
|
348
|
+
runId,
|
|
349
|
+
startedAt,
|
|
350
|
+
failedAt,
|
|
351
|
+
error: error.message,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Update metadata
|
|
355
|
+
atomicWriteJson(paths.runMetaJson, {
|
|
356
|
+
veraxVersion: getVersion(),
|
|
357
|
+
nodeVersion: process.version,
|
|
358
|
+
platform: process.platform,
|
|
359
|
+
cwd: projectRoot,
|
|
360
|
+
command: 'run',
|
|
361
|
+
args: { url, src, out },
|
|
362
|
+
url,
|
|
363
|
+
src: srcPath,
|
|
364
|
+
startedAt,
|
|
365
|
+
completedAt: failedAt,
|
|
366
|
+
error: error.message,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Write summary with digest even on failure
|
|
370
|
+
try {
|
|
371
|
+
writeSummaryJson(paths.summaryJson, {
|
|
372
|
+
runId,
|
|
373
|
+
status: 'FAILED',
|
|
374
|
+
startedAt,
|
|
375
|
+
completedAt: failedAt,
|
|
376
|
+
command: 'run',
|
|
377
|
+
url,
|
|
378
|
+
notes: `Run failed: ${error.message}`,
|
|
379
|
+
}, {
|
|
380
|
+
expectationsTotal: 0,
|
|
381
|
+
attempted: 0,
|
|
382
|
+
observed: 0,
|
|
383
|
+
silentFailures: 0,
|
|
384
|
+
coverageGaps: 0,
|
|
385
|
+
unproven: 0,
|
|
386
|
+
informational: 0,
|
|
387
|
+
});
|
|
388
|
+
} catch (summaryError) {
|
|
389
|
+
// Ignore summary write errors during failure handling
|
|
390
|
+
}
|
|
391
|
+
} catch (statusError) {
|
|
392
|
+
// Ignore errors when writing failure status
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
events.emit('error', {
|
|
397
|
+
message: error.message,
|
|
398
|
+
stack: error.stack,
|
|
399
|
+
});
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
}
|
package/src/cli/entry.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VERAX CLI Entry Point
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* - verax (smart default with URL detection/prompting)
|
|
8
|
+
* - verax run --url <url> (strict, non-interactive)
|
|
9
|
+
* - verax inspect <runPath> (read and display run summary)
|
|
10
|
+
*
|
|
11
|
+
* Exit codes:
|
|
12
|
+
* - 0: success
|
|
13
|
+
* - 2: internal crash
|
|
14
|
+
* - 64: invalid CLI usage
|
|
15
|
+
* - 65: invalid input data
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import { dirname, resolve } from 'path';
|
|
20
|
+
import { readFileSync } from 'fs';
|
|
21
|
+
import { defaultCommand } from './commands/default.js';
|
|
22
|
+
import { runCommand } from './commands/run.js';
|
|
23
|
+
import { inspectCommand } from './commands/inspect.js';
|
|
24
|
+
import { doctorCommand } from './commands/doctor.js';
|
|
25
|
+
import { getExitCode, UsageError, DataError, CrashError } from './util/errors.js';
|
|
26
|
+
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname = dirname(__filename);
|
|
29
|
+
|
|
30
|
+
// Read package.json for version
|
|
31
|
+
function getVersion() {
|
|
32
|
+
try {
|
|
33
|
+
const pkgPath = resolve(__dirname, '../../package.json');
|
|
34
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
35
|
+
return pkg.version;
|
|
36
|
+
} catch {
|
|
37
|
+
return 'unknown';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
const args = process.argv.slice(2);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Handle --version
|
|
46
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
47
|
+
console.log(`verax ${getVersion()}`);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle explicit --help
|
|
52
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
53
|
+
showHelp();
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// If no args, run default command
|
|
58
|
+
if (args.length === 0) {
|
|
59
|
+
await defaultCommand({});
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const command = args[0];
|
|
64
|
+
|
|
65
|
+
// Handle 'run' command
|
|
66
|
+
if (command === 'run') {
|
|
67
|
+
const url = parseArg(args, '--url');
|
|
68
|
+
const src = parseArg(args, '--src') || '.';
|
|
69
|
+
const out = parseArg(args, '--out') || '.verax';
|
|
70
|
+
const json = args.includes('--json');
|
|
71
|
+
const verbose = args.includes('--verbose');
|
|
72
|
+
|
|
73
|
+
if (!url) {
|
|
74
|
+
throw new UsageError('run command requires --url <url> argument');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = await runCommand({ url, src, out, json, verbose });
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Handle 'inspect' command
|
|
82
|
+
if (command === 'inspect') {
|
|
83
|
+
if (args.length < 2) {
|
|
84
|
+
throw new UsageError('inspect command requires a run path argument');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const runPath = args[1];
|
|
88
|
+
const json = args.includes('--json');
|
|
89
|
+
|
|
90
|
+
const result = await inspectCommand(runPath, { json });
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle 'doctor' command
|
|
95
|
+
if (command === 'doctor') {
|
|
96
|
+
const allowedFlags = new Set(['--json']);
|
|
97
|
+
const extraFlags = args.slice(1).filter((a) => a.startsWith('-') && !allowedFlags.has(a));
|
|
98
|
+
const json = args.includes('--json');
|
|
99
|
+
const result = await doctorCommand({ json, extraFlags });
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle 'help' command
|
|
104
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
105
|
+
showHelp();
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Default command: smart scanning mode
|
|
110
|
+
// Options can be passed as flags before/after the default command position
|
|
111
|
+
const url = parseArg(args, '--url');
|
|
112
|
+
const src = parseArg(args, '--src') || '.';
|
|
113
|
+
const out = parseArg(args, '--out') || '.verax';
|
|
114
|
+
const json = args.includes('--json');
|
|
115
|
+
const verbose = args.includes('--verbose');
|
|
116
|
+
|
|
117
|
+
const result = await defaultCommand({ url, src, out, json, verbose });
|
|
118
|
+
process.exit(0);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// Print error message
|
|
121
|
+
if (error.message) {
|
|
122
|
+
console.error(`Error: ${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Get exit code
|
|
126
|
+
const exitCode = getExitCode(error);
|
|
127
|
+
process.exit(exitCode);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function showHelp() {
|
|
132
|
+
const version = getVersion();
|
|
133
|
+
console.log(`
|
|
134
|
+
verax ${version}
|
|
135
|
+
VERAX — Silent failure detection for websites
|
|
136
|
+
|
|
137
|
+
USAGE:
|
|
138
|
+
verax [options] Smart mode (detects/prompts for URL)
|
|
139
|
+
verax run --url <url> [options] Strict mode (non-interactive, CI-friendly)
|
|
140
|
+
verax inspect <runPath> [--json] Inspect an existing run
|
|
141
|
+
verax doctor [--json] Diagnose local environment
|
|
142
|
+
verax --version Show version
|
|
143
|
+
verax --help Show this help
|
|
144
|
+
|
|
145
|
+
OPTIONS:
|
|
146
|
+
--url <url> Target URL to scan
|
|
147
|
+
--src <path> Source directory (default: .)
|
|
148
|
+
--out <path> Output directory for artifacts (default: .verax)
|
|
149
|
+
--json Output as JSON lines (progress events)
|
|
150
|
+
--verbose Verbose output
|
|
151
|
+
--help Show this help
|
|
152
|
+
--version Show version
|
|
153
|
+
|
|
154
|
+
EXAMPLES:
|
|
155
|
+
# Smart mode (interactive if needed)
|
|
156
|
+
verax
|
|
157
|
+
|
|
158
|
+
# Smart mode with explicit URL
|
|
159
|
+
verax --url https://example.com
|
|
160
|
+
|
|
161
|
+
# Strict mode (CI-friendly, non-interactive)
|
|
162
|
+
verax run --url https://example.com --src . --out .verax
|
|
163
|
+
|
|
164
|
+
# Inspect previous run
|
|
165
|
+
verax inspect .verax/runs/2026-01-11T00-59-12Z_4f2a9c
|
|
166
|
+
|
|
167
|
+
EXIT CODES:
|
|
168
|
+
0 Success (tool executed)
|
|
169
|
+
2 Internal crash
|
|
170
|
+
64 Invalid CLI usage (missing args, invalid flags)
|
|
171
|
+
65 Invalid input data (bad JSON, unreadable folder, etc.)
|
|
172
|
+
|
|
173
|
+
ARTIFACTS:
|
|
174
|
+
Artifacts are written to: <out>/runs/<runId>/
|
|
175
|
+
Required files:
|
|
176
|
+
- run.status.json Run status lifecycle
|
|
177
|
+
- run.meta.json Metadata about the run
|
|
178
|
+
- summary.json Summary of results
|
|
179
|
+
- findings.json Array of findings
|
|
180
|
+
- traces.jsonl JSONL traces of execution
|
|
181
|
+
- evidence/ Directory for evidence files
|
|
182
|
+
`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseArg(args, name) {
|
|
186
|
+
const index = args.indexOf(name);
|
|
187
|
+
if (index !== -1 && index + 1 < args.length) {
|
|
188
|
+
return args[index + 1];
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main().catch((error) => {
|
|
194
|
+
console.error(`Fatal error: ${error.message}`);
|
|
195
|
+
process.exit(2);
|
|
196
|
+
});
|