@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,681 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname } from 'path';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import { DataError } from '../util/errors.js';
|
|
7
|
+
import { generateRunId } from '../util/run-id.js';
|
|
8
|
+
import { getRunPaths, ensureRunDirectories } from '../util/paths.js';
|
|
9
|
+
import { atomicWriteJson, atomicWriteText } from '../util/atomic-write.js';
|
|
10
|
+
import { RunEventEmitter } from '../util/events.js';
|
|
11
|
+
import { tryResolveUrlFromEnv } from '../util/env-url.js';
|
|
12
|
+
import { discoverProject, getFrameworkDisplayName, extractPortFromScript } from '../util/project-discovery.js';
|
|
13
|
+
import { writeProjectJson } from '../util/project-writer.js';
|
|
14
|
+
import { extractExpectations } from '../util/expectation-extractor.js';
|
|
15
|
+
import { writeLearnJson } from '../util/learn-writer.js';
|
|
16
|
+
import { observeExpectations } from '../util/observation-engine.js';
|
|
17
|
+
import { writeObserveJson } from '../util/observe-writer.js';
|
|
18
|
+
import { detectFindings } from '../util/detection-engine.js';
|
|
19
|
+
import { writeFindingsJson } from '../util/findings-writer.js';
|
|
20
|
+
import { writeSummaryJson } from '../util/summary-writer.js';
|
|
21
|
+
import { computeRuntimeBudget, withTimeout } from '../util/runtime-budget.js';
|
|
22
|
+
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = dirname(__filename);
|
|
25
|
+
|
|
26
|
+
function getVersion() {
|
|
27
|
+
try {
|
|
28
|
+
const pkgPath = resolve(__dirname, '../../../package.json');
|
|
29
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
30
|
+
return pkg.version;
|
|
31
|
+
} catch {
|
|
32
|
+
return '0.2.0';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `verax` smart default command
|
|
38
|
+
* Interactive mode with intelligent URL detection
|
|
39
|
+
*/
|
|
40
|
+
export async function defaultCommand(options = {}) {
|
|
41
|
+
const {
|
|
42
|
+
src = '.',
|
|
43
|
+
out = '.verax',
|
|
44
|
+
url = null,
|
|
45
|
+
json = false,
|
|
46
|
+
verbose = false,
|
|
47
|
+
} = options;
|
|
48
|
+
|
|
49
|
+
const projectRoot = resolve(process.cwd());
|
|
50
|
+
const srcPath = resolve(projectRoot, src);
|
|
51
|
+
|
|
52
|
+
// Validate src directory exists
|
|
53
|
+
if (!existsSync(srcPath)) {
|
|
54
|
+
throw new DataError(`Source directory not found: ${srcPath}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Create event emitter
|
|
58
|
+
const events = new RunEventEmitter();
|
|
59
|
+
|
|
60
|
+
// Setup event handlers
|
|
61
|
+
if (json) {
|
|
62
|
+
events.on('*', (event) => {
|
|
63
|
+
console.log(JSON.stringify(event));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Show progress if not JSON
|
|
68
|
+
if (!json && !verbose) {
|
|
69
|
+
events.on('phase:started', (event) => {
|
|
70
|
+
if (!json) {
|
|
71
|
+
console.log(`${event.phase}...`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let runId = null;
|
|
77
|
+
/** @type {ReturnType<typeof getRunPaths> | null} */
|
|
78
|
+
let paths = null;
|
|
79
|
+
let startedAt = null;
|
|
80
|
+
let watchdogTimer = null;
|
|
81
|
+
let budget = null;
|
|
82
|
+
let timedOut = false;
|
|
83
|
+
|
|
84
|
+
// Graceful finalization function
|
|
85
|
+
const finalizeOnTimeout = async (reason) => {
|
|
86
|
+
if (timedOut) return; // Prevent double finalization
|
|
87
|
+
timedOut = true;
|
|
88
|
+
|
|
89
|
+
events.stopHeartbeat();
|
|
90
|
+
|
|
91
|
+
// TypeScript narrowing: paths is guaranteed to be non-null here due to control flow
|
|
92
|
+
if (paths && runId && startedAt) {
|
|
93
|
+
try {
|
|
94
|
+
const failedAt = new Date().toISOString();
|
|
95
|
+
atomicWriteJson(paths.runStatusJson, {
|
|
96
|
+
status: 'FAILED',
|
|
97
|
+
runId,
|
|
98
|
+
startedAt,
|
|
99
|
+
failedAt,
|
|
100
|
+
error: reason,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
atomicWriteJson(paths.runMetaJson, {
|
|
104
|
+
veraxVersion: getVersion(),
|
|
105
|
+
nodeVersion: process.version,
|
|
106
|
+
platform: process.platform,
|
|
107
|
+
cwd: projectRoot,
|
|
108
|
+
command: 'default',
|
|
109
|
+
args: { url: url || null, src },
|
|
110
|
+
url: url || null,
|
|
111
|
+
src: srcPath,
|
|
112
|
+
startedAt,
|
|
113
|
+
completedAt: failedAt,
|
|
114
|
+
error: reason,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
writeSummaryJson(paths.summaryJson, {
|
|
119
|
+
runId,
|
|
120
|
+
status: 'FAILED',
|
|
121
|
+
startedAt,
|
|
122
|
+
completedAt: failedAt,
|
|
123
|
+
command: 'default',
|
|
124
|
+
url: url || null,
|
|
125
|
+
notes: `Run timed out: ${reason}`,
|
|
126
|
+
}, {
|
|
127
|
+
expectationsTotal: 0,
|
|
128
|
+
attempted: 0,
|
|
129
|
+
observed: 0,
|
|
130
|
+
silentFailures: 0,
|
|
131
|
+
coverageGaps: 0,
|
|
132
|
+
unproven: 0,
|
|
133
|
+
informational: 0,
|
|
134
|
+
});
|
|
135
|
+
} catch (summaryError) {
|
|
136
|
+
// Ignore summary write errors during timeout handling
|
|
137
|
+
}
|
|
138
|
+
} catch (statusError) {
|
|
139
|
+
// Ignore errors when writing failure status
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
events.emit('error', {
|
|
144
|
+
message: reason,
|
|
145
|
+
type: 'timeout',
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
events.emit('phase:started', {
|
|
151
|
+
phase: 'Detect Project',
|
|
152
|
+
message: 'Detecting project type...',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Discover project configuration
|
|
156
|
+
let projectProfile;
|
|
157
|
+
try {
|
|
158
|
+
projectProfile = await discoverProject(srcPath);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
// If discovery fails, create a minimal profile
|
|
161
|
+
projectProfile = {
|
|
162
|
+
framework: 'unknown',
|
|
163
|
+
router: null,
|
|
164
|
+
sourceRoot: srcPath,
|
|
165
|
+
packageManager: 'unknown',
|
|
166
|
+
scripts: { dev: null, build: null, start: null },
|
|
167
|
+
detectedAt: new Date().toISOString(),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const frameworkName = getFrameworkDisplayName(projectProfile.framework, projectProfile.router);
|
|
172
|
+
|
|
173
|
+
if (!json) {
|
|
174
|
+
console.log(`Detected framework: ${frameworkName}`);
|
|
175
|
+
console.log(`Resolved source root: ${projectProfile.sourceRoot}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
events.emit('project:detected', {
|
|
179
|
+
framework: projectProfile.framework,
|
|
180
|
+
router: projectProfile.router,
|
|
181
|
+
frameworkName,
|
|
182
|
+
sourceRoot: projectProfile.sourceRoot,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
events.emit('phase:completed', {
|
|
186
|
+
phase: 'Detect Project',
|
|
187
|
+
message: `Detected framework: ${frameworkName}`,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Phase: Resolve URL
|
|
191
|
+
let resolvedUrl = url;
|
|
192
|
+
|
|
193
|
+
if (!resolvedUrl) {
|
|
194
|
+
events.emit('phase:started', {
|
|
195
|
+
phase: 'Resolve URL',
|
|
196
|
+
message: 'Attempting to resolve URL from environment...',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
resolvedUrl = tryResolveUrlFromEnv();
|
|
200
|
+
|
|
201
|
+
// Try to extract URL from dev script if available
|
|
202
|
+
if (!resolvedUrl && projectProfile.scripts.dev) {
|
|
203
|
+
const port = extractPortFromScript(projectProfile.scripts.dev);
|
|
204
|
+
if (port) {
|
|
205
|
+
resolvedUrl = `http://localhost:${port}`;
|
|
206
|
+
if (!json) {
|
|
207
|
+
console.log(`Detected dev script: ${projectProfile.scripts.dev}`);
|
|
208
|
+
}
|
|
209
|
+
events.emit('dev:script:detected', {
|
|
210
|
+
script: projectProfile.scripts.dev,
|
|
211
|
+
port,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (resolvedUrl && !json) {
|
|
217
|
+
console.log(`Detected URL: ${resolvedUrl}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// If still no URL, prompt interactively
|
|
222
|
+
if (!resolvedUrl) {
|
|
223
|
+
events.emit('phase:started', {
|
|
224
|
+
phase: 'Resolve URL',
|
|
225
|
+
message: 'Prompting for URL...',
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (!json) {
|
|
229
|
+
console.log(''); // blank line
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const answer = await inquirer.prompt([
|
|
233
|
+
{
|
|
234
|
+
type: 'input',
|
|
235
|
+
name: 'url',
|
|
236
|
+
message: 'Enter the URL to scan',
|
|
237
|
+
validate: (input) => {
|
|
238
|
+
if (!input.trim()) {
|
|
239
|
+
return 'URL is required';
|
|
240
|
+
}
|
|
241
|
+
if (!input.startsWith('http://') && !input.startsWith('https://')) {
|
|
242
|
+
return 'URL must start with http:// or https://';
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
resolvedUrl = answer.url;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!json) {
|
|
253
|
+
console.log(`Using URL: ${resolvedUrl}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
events.emit('phase:completed', {
|
|
257
|
+
phase: 'Resolve URL',
|
|
258
|
+
message: `URL resolved: ${resolvedUrl}`,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Generate run ID
|
|
262
|
+
let runId = generateRunId();
|
|
263
|
+
if (verbose && !json) console.log(`Run ID: ${runId}`);
|
|
264
|
+
|
|
265
|
+
let paths = getRunPaths(projectRoot, out, runId);
|
|
266
|
+
ensureRunDirectories(paths);
|
|
267
|
+
|
|
268
|
+
// Initialize Run
|
|
269
|
+
events.emit('phase:started', {
|
|
270
|
+
phase: 'Initialize Run',
|
|
271
|
+
message: 'Initializing run artifacts...',
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const now = new Date();
|
|
275
|
+
let startedAt = now.toISOString();
|
|
276
|
+
|
|
277
|
+
atomicWriteJson(paths.runStatusJson, {
|
|
278
|
+
status: 'RUNNING',
|
|
279
|
+
runId,
|
|
280
|
+
startedAt,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
atomicWriteJson(paths.runMetaJson, {
|
|
284
|
+
veraxVersion: getVersion(),
|
|
285
|
+
nodeVersion: process.version,
|
|
286
|
+
platform: process.platform,
|
|
287
|
+
cwd: projectRoot,
|
|
288
|
+
command: 'default',
|
|
289
|
+
args: { url: resolvedUrl, src },
|
|
290
|
+
url: resolvedUrl,
|
|
291
|
+
src: srcPath,
|
|
292
|
+
startedAt,
|
|
293
|
+
completedAt: null,
|
|
294
|
+
error: null,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
events.emit('phase:completed', {
|
|
298
|
+
phase: 'Initialize Run',
|
|
299
|
+
message: 'Run initialized',
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Learning phase (placeholder)
|
|
303
|
+
events.emit('phase:started', {
|
|
304
|
+
phase: 'Learn',
|
|
305
|
+
message: 'Analyzing project structure...',
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
events.startHeartbeat('Learn', json);
|
|
309
|
+
|
|
310
|
+
let expectations, skipped;
|
|
311
|
+
try {
|
|
312
|
+
// Extract expectations
|
|
313
|
+
const result = await extractExpectations(projectProfile, projectProfile.sourceRoot);
|
|
314
|
+
expectations = result.expectations;
|
|
315
|
+
skipped = result.skipped;
|
|
316
|
+
} finally {
|
|
317
|
+
events.stopHeartbeat();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!json) {
|
|
321
|
+
console.log(`Found ${expectations.length} expectations`);
|
|
322
|
+
if (Object.values(skipped).reduce((a, b) => a + b, 0) > 0) {
|
|
323
|
+
console.log(`Skipped: ${Object.values(skipped).reduce((a, b) => a + b, 0)} (dynamic/computed)`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Emit expectations found events
|
|
328
|
+
expectations.slice(0, 5).forEach(exp => {
|
|
329
|
+
events.emit('expectation:found', {
|
|
330
|
+
type: exp.type,
|
|
331
|
+
promise: exp.promise,
|
|
332
|
+
file: exp.source.file,
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
if (expectations.length > 5) {
|
|
337
|
+
events.emit('expectation:found', {
|
|
338
|
+
message: `... and ${expectations.length - 5} more expectations`,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Compute runtime budget based on expectations count
|
|
343
|
+
budget = computeRuntimeBudget({
|
|
344
|
+
expectationsCount: expectations.length,
|
|
345
|
+
mode: 'default',
|
|
346
|
+
framework: projectProfile.framework,
|
|
347
|
+
fileCount: projectProfile.fileCount || expectations.length,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Set up global watchdog timer
|
|
351
|
+
watchdogTimer = setTimeout(async () => {
|
|
352
|
+
await finalizeOnTimeout(`Global timeout exceeded: ${budget.totalMaxMs}ms`);
|
|
353
|
+
// Exit with code 0 (tool executed, just timed out)
|
|
354
|
+
process.exit(0);
|
|
355
|
+
}, budget.totalMaxMs);
|
|
356
|
+
|
|
357
|
+
// Wrap Learn phase with timeout
|
|
358
|
+
try {
|
|
359
|
+
await withTimeout(
|
|
360
|
+
budget.learnMaxMs,
|
|
361
|
+
Promise.resolve(), // Learn phase already completed
|
|
362
|
+
'Learn'
|
|
363
|
+
);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (error.message.includes('timeout')) {
|
|
366
|
+
await finalizeOnTimeout(`Learn phase timeout: ${budget.learnMaxMs}ms`);
|
|
367
|
+
process.exit(0);
|
|
368
|
+
}
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
events.emit('phase:completed', {
|
|
373
|
+
phase: 'Learn',
|
|
374
|
+
message: 'Project analysis complete',
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Observe phase with timeout
|
|
378
|
+
events.emit('phase:started', {
|
|
379
|
+
phase: 'Observe',
|
|
380
|
+
message: 'Launching browser and observing expectations...',
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
events.startHeartbeat('Observe', json);
|
|
384
|
+
|
|
385
|
+
let observeData = null;
|
|
386
|
+
try {
|
|
387
|
+
if (expectations.length > 0) {
|
|
388
|
+
try {
|
|
389
|
+
observeData = await withTimeout(
|
|
390
|
+
budget.observeMaxMs,
|
|
391
|
+
observeExpectations(
|
|
392
|
+
expectations,
|
|
393
|
+
resolvedUrl,
|
|
394
|
+
paths.evidenceDir,
|
|
395
|
+
(progress) => {
|
|
396
|
+
events.emit(progress.event, progress);
|
|
397
|
+
if (!json && progress.event === 'observe:result') {
|
|
398
|
+
const status = progress.observed ? '✓' : '✗';
|
|
399
|
+
console.log(` ${status} ${progress.index}/${expectations.length}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
),
|
|
403
|
+
'Observe'
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
if (!json) {
|
|
407
|
+
console.log(`Observed: ${observeData.stats.observed}/${expectations.length}`);
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (error.message.includes('timeout')) {
|
|
411
|
+
if (!json) {
|
|
412
|
+
console.error(`Observe error: timeout after ${budget.observeMaxMs}ms`);
|
|
413
|
+
}
|
|
414
|
+
events.emit('observe:error', {
|
|
415
|
+
message: `Observe phase timeout: ${budget.observeMaxMs}ms`,
|
|
416
|
+
});
|
|
417
|
+
observeData = {
|
|
418
|
+
observations: [],
|
|
419
|
+
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
420
|
+
observedAt: new Date().toISOString(),
|
|
421
|
+
};
|
|
422
|
+
} else {
|
|
423
|
+
if (!json) {
|
|
424
|
+
console.error(`Observe error: ${error.message}`);
|
|
425
|
+
}
|
|
426
|
+
events.emit('observe:error', {
|
|
427
|
+
message: error.message,
|
|
428
|
+
});
|
|
429
|
+
observeData = {
|
|
430
|
+
observations: [],
|
|
431
|
+
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
432
|
+
observedAt: new Date().toISOString(),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
observeData = {
|
|
438
|
+
observations: [],
|
|
439
|
+
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
440
|
+
observedAt: new Date().toISOString(),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
} finally {
|
|
444
|
+
events.stopHeartbeat();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
events.emit('phase:completed', {
|
|
448
|
+
phase: 'Observe',
|
|
449
|
+
message: 'Browser observation complete',
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Detect phase with timeout
|
|
453
|
+
events.emit('phase:started', {
|
|
454
|
+
phase: 'Detect',
|
|
455
|
+
message: 'Analyzing findings and detecting silent failures...',
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
events.startHeartbeat('Detect', json);
|
|
459
|
+
|
|
460
|
+
// Load learn and observe data for detection
|
|
461
|
+
let learnData = { expectations: [] };
|
|
462
|
+
let detectData = null;
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
try {
|
|
466
|
+
learnData = {
|
|
467
|
+
expectations,
|
|
468
|
+
skipped,
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
detectData = await withTimeout(
|
|
472
|
+
budget.detectMaxMs,
|
|
473
|
+
detectFindings(learnData, observeData, projectRoot, (progress) => {
|
|
474
|
+
events.emit(progress.event, progress);
|
|
475
|
+
if (!json && progress.event === 'detect:classified') {
|
|
476
|
+
const symbol = progress.classification === 'silent-failure' ? '✗' :
|
|
477
|
+
progress.classification === 'observed' ? '✓' :
|
|
478
|
+
progress.classification === 'coverage-gap' ? '⊘' : '⚠';
|
|
479
|
+
console.log(` ${symbol} ${progress.index}/${learnData.expectations.length}`);
|
|
480
|
+
}
|
|
481
|
+
}),
|
|
482
|
+
'Detect'
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
if (!json && detectData.stats.silentFailures > 0) {
|
|
486
|
+
console.log(`Silent failures detected: ${detectData.stats.silentFailures}`);
|
|
487
|
+
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (error.message.includes('timeout')) {
|
|
490
|
+
if (!json) {
|
|
491
|
+
console.error(`Detect error: timeout after ${budget.detectMaxMs}ms`);
|
|
492
|
+
}
|
|
493
|
+
events.emit('detect:error', {
|
|
494
|
+
message: `Detect phase timeout: ${budget.detectMaxMs}ms`,
|
|
495
|
+
});
|
|
496
|
+
detectData = {
|
|
497
|
+
findings: [],
|
|
498
|
+
stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
|
|
499
|
+
detectedAt: new Date().toISOString(),
|
|
500
|
+
};
|
|
501
|
+
} else {
|
|
502
|
+
if (!json) {
|
|
503
|
+
console.error(`Detect error: ${error.message}`);
|
|
504
|
+
}
|
|
505
|
+
events.emit('detect:error', {
|
|
506
|
+
message: error.message,
|
|
507
|
+
});
|
|
508
|
+
detectData = {
|
|
509
|
+
findings: [],
|
|
510
|
+
stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
|
|
511
|
+
detectedAt: new Date().toISOString(),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
} finally {
|
|
516
|
+
events.stopHeartbeat();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
events.emit('phase:completed', {
|
|
520
|
+
phase: 'Detect',
|
|
521
|
+
message: 'Silent failure detection complete',
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Clear watchdog timer on successful completion
|
|
525
|
+
if (watchdogTimer) {
|
|
526
|
+
clearTimeout(watchdogTimer);
|
|
527
|
+
watchdogTimer = null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Finalize Artifacts
|
|
531
|
+
events.emit('phase:started', {
|
|
532
|
+
phase: 'Finalize Artifacts',
|
|
533
|
+
message: 'Writing run results...',
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
events.stopHeartbeat();
|
|
537
|
+
|
|
538
|
+
const completedAt = new Date().toISOString();
|
|
539
|
+
|
|
540
|
+
atomicWriteJson(paths.runStatusJson, {
|
|
541
|
+
status: 'COMPLETE',
|
|
542
|
+
runId,
|
|
543
|
+
startedAt,
|
|
544
|
+
completedAt,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
atomicWriteJson(paths.runMetaJson, {
|
|
548
|
+
veraxVersion: getVersion(),
|
|
549
|
+
nodeVersion: process.version,
|
|
550
|
+
platform: process.platform,
|
|
551
|
+
cwd: projectRoot,
|
|
552
|
+
command: 'default',
|
|
553
|
+
args: { url: resolvedUrl, src },
|
|
554
|
+
url: resolvedUrl,
|
|
555
|
+
src: srcPath,
|
|
556
|
+
startedAt,
|
|
557
|
+
completedAt,
|
|
558
|
+
error: null,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Write summary with stable digest
|
|
562
|
+
writeSummaryJson(paths.summaryJson, {
|
|
563
|
+
runId,
|
|
564
|
+
status: 'COMPLETE',
|
|
565
|
+
startedAt,
|
|
566
|
+
completedAt,
|
|
567
|
+
command: 'default',
|
|
568
|
+
url: resolvedUrl,
|
|
569
|
+
notes: 'Run completed successfully',
|
|
570
|
+
}, {
|
|
571
|
+
expectationsTotal: expectations.length,
|
|
572
|
+
attempted: observeData.stats?.attempted || 0,
|
|
573
|
+
observed: observeData.stats?.observed || 0,
|
|
574
|
+
silentFailures: detectData.stats?.silentFailures || 0,
|
|
575
|
+
coverageGaps: detectData.stats?.coverageGaps || 0,
|
|
576
|
+
unproven: detectData.stats?.unproven || 0,
|
|
577
|
+
informational: detectData.stats?.informational || 0,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// Write detect results (or empty if detection failed)
|
|
581
|
+
writeFindingsJson(paths.baseDir, detectData);
|
|
582
|
+
|
|
583
|
+
// Write traces (include all events including heartbeats)
|
|
584
|
+
const allEvents = events.getEvents();
|
|
585
|
+
const tracesContent = allEvents
|
|
586
|
+
.map(e => JSON.stringify(e))
|
|
587
|
+
.join('\n') + '\n';
|
|
588
|
+
atomicWriteText(paths.tracesJsonl, tracesContent);
|
|
589
|
+
|
|
590
|
+
// Write project profile
|
|
591
|
+
writeProjectJson(paths, projectProfile);
|
|
592
|
+
|
|
593
|
+
// Write learn results
|
|
594
|
+
writeLearnJson(paths, expectations, skipped);
|
|
595
|
+
|
|
596
|
+
// Write observe results
|
|
597
|
+
writeObserveJson(paths.baseDir, observeData);
|
|
598
|
+
|
|
599
|
+
events.emit('phase:completed', {
|
|
600
|
+
phase: 'Finalize Artifacts',
|
|
601
|
+
message: 'Run artifacts written',
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Print summary if not JSON mode
|
|
605
|
+
if (!json) {
|
|
606
|
+
console.log('\nRun complete.');
|
|
607
|
+
console.log(`Run ID: ${runId}`);
|
|
608
|
+
console.log(`Artifacts: ${paths.baseDir}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return { runId, paths, url: resolvedUrl, success: true };
|
|
612
|
+
} catch (error) {
|
|
613
|
+
// Clear watchdog timer on error
|
|
614
|
+
if (watchdogTimer) {
|
|
615
|
+
clearTimeout(watchdogTimer);
|
|
616
|
+
watchdogTimer = null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
events.stopHeartbeat();
|
|
620
|
+
|
|
621
|
+
// Mark run as FAILED if we have paths
|
|
622
|
+
if (paths && runId && startedAt && typeof paths === 'object') {
|
|
623
|
+
try {
|
|
624
|
+
const failedAt = new Date().toISOString();
|
|
625
|
+
atomicWriteJson(paths.runStatusJson, {
|
|
626
|
+
status: 'FAILED',
|
|
627
|
+
runId,
|
|
628
|
+
startedAt,
|
|
629
|
+
failedAt,
|
|
630
|
+
error: error.message,
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// Update metadata
|
|
634
|
+
atomicWriteJson(paths.runMetaJson, {
|
|
635
|
+
veraxVersion: getVersion(),
|
|
636
|
+
nodeVersion: process.version,
|
|
637
|
+
platform: process.platform,
|
|
638
|
+
cwd: projectRoot,
|
|
639
|
+
command: 'default',
|
|
640
|
+
args: { url: url || null, src },
|
|
641
|
+
url: url || null,
|
|
642
|
+
src: srcPath,
|
|
643
|
+
startedAt,
|
|
644
|
+
completedAt: failedAt,
|
|
645
|
+
error: error.message,
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// Write summary with digest even on failure
|
|
649
|
+
try {
|
|
650
|
+
writeSummaryJson(paths.summaryJson, {
|
|
651
|
+
runId,
|
|
652
|
+
status: 'FAILED',
|
|
653
|
+
startedAt,
|
|
654
|
+
completedAt: failedAt,
|
|
655
|
+
command: 'default',
|
|
656
|
+
url: url || null,
|
|
657
|
+
notes: `Run failed: ${error.message}`,
|
|
658
|
+
}, {
|
|
659
|
+
expectationsTotal: 0,
|
|
660
|
+
attempted: 0,
|
|
661
|
+
observed: 0,
|
|
662
|
+
silentFailures: 0,
|
|
663
|
+
coverageGaps: 0,
|
|
664
|
+
unproven: 0,
|
|
665
|
+
informational: 0,
|
|
666
|
+
});
|
|
667
|
+
} catch (summaryError) {
|
|
668
|
+
// Ignore summary write errors during failure handling
|
|
669
|
+
}
|
|
670
|
+
} catch (statusError) {
|
|
671
|
+
// Ignore errors when writing failure status
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
events.emit('error', {
|
|
676
|
+
message: error.message,
|
|
677
|
+
stack: error.stack,
|
|
678
|
+
});
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
681
|
+
}
|