@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,586 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname } from 'path';
|
|
5
|
+
import { UsageError, DataError } 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
|
+
import { computeRuntimeBudget, withTimeout } from '../util/runtime-budget.js';
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
|
|
24
|
+
function getVersion() {
|
|
25
|
+
try {
|
|
26
|
+
const pkgPath = resolve(__dirname, '../../../package.json');
|
|
27
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
28
|
+
return pkg.version;
|
|
29
|
+
} catch {
|
|
30
|
+
return '0.2.0';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `verax run` command
|
|
36
|
+
* Strict, non-interactive CLI mode with explicit flags
|
|
37
|
+
*/
|
|
38
|
+
export async function runCommand(options) {
|
|
39
|
+
const {
|
|
40
|
+
url,
|
|
41
|
+
src = '.',
|
|
42
|
+
out = '.verax',
|
|
43
|
+
json = false,
|
|
44
|
+
verbose = false,
|
|
45
|
+
} = options;
|
|
46
|
+
|
|
47
|
+
// Validate required arguments
|
|
48
|
+
if (!url) {
|
|
49
|
+
throw new UsageError('Missing required argument: --url <url>');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const projectRoot = resolve(process.cwd());
|
|
53
|
+
const srcPath = resolve(projectRoot, src);
|
|
54
|
+
|
|
55
|
+
// Validate src directory exists
|
|
56
|
+
if (!existsSync(srcPath)) {
|
|
57
|
+
throw new DataError(`Source directory not found: ${srcPath}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create event emitter
|
|
61
|
+
const events = new RunEventEmitter();
|
|
62
|
+
|
|
63
|
+
// Setup event handlers
|
|
64
|
+
if (json) {
|
|
65
|
+
// In JSON mode, emit events as JSONL (one JSON object per line)
|
|
66
|
+
events.on('*', (event) => {
|
|
67
|
+
console.log(JSON.stringify(event));
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
events.on('*', (event) => {
|
|
71
|
+
if (verbose) {
|
|
72
|
+
console.log(`[${event.type}] ${event.message || ''}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let runId = 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
|
+
if (paths && runId && startedAt) {
|
|
92
|
+
try {
|
|
93
|
+
const failedAt = new Date().toISOString();
|
|
94
|
+
atomicWriteJson(paths.runStatusJson, {
|
|
95
|
+
status: 'FAILED',
|
|
96
|
+
runId,
|
|
97
|
+
startedAt,
|
|
98
|
+
failedAt,
|
|
99
|
+
error: reason,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
atomicWriteJson(paths.runMetaJson, {
|
|
103
|
+
veraxVersion: getVersion(),
|
|
104
|
+
nodeVersion: process.version,
|
|
105
|
+
platform: process.platform,
|
|
106
|
+
cwd: projectRoot,
|
|
107
|
+
command: 'run',
|
|
108
|
+
args: { url, src, out },
|
|
109
|
+
url,
|
|
110
|
+
src: srcPath,
|
|
111
|
+
startedAt,
|
|
112
|
+
completedAt: failedAt,
|
|
113
|
+
error: reason,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
writeSummaryJson(paths.summaryJson, {
|
|
118
|
+
runId,
|
|
119
|
+
status: 'FAILED',
|
|
120
|
+
startedAt,
|
|
121
|
+
completedAt: failedAt,
|
|
122
|
+
command: 'run',
|
|
123
|
+
url,
|
|
124
|
+
notes: `Run timed out: ${reason}`,
|
|
125
|
+
}, {
|
|
126
|
+
expectationsTotal: 0,
|
|
127
|
+
attempted: 0,
|
|
128
|
+
observed: 0,
|
|
129
|
+
silentFailures: 0,
|
|
130
|
+
coverageGaps: 0,
|
|
131
|
+
unproven: 0,
|
|
132
|
+
informational: 0,
|
|
133
|
+
});
|
|
134
|
+
} catch (summaryError) {
|
|
135
|
+
// Ignore summary write errors during timeout handling
|
|
136
|
+
}
|
|
137
|
+
} catch (statusError) {
|
|
138
|
+
// Ignore errors when writing failure status
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
events.emit('error', {
|
|
143
|
+
message: reason,
|
|
144
|
+
type: 'timeout',
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
// Generate run ID
|
|
150
|
+
runId = generateRunId();
|
|
151
|
+
if (verbose && !json) console.log(`Run ID: ${runId}`);
|
|
152
|
+
|
|
153
|
+
paths = getRunPaths(projectRoot, out, runId);
|
|
154
|
+
ensureRunDirectories(paths);
|
|
155
|
+
|
|
156
|
+
// Discover project configuration
|
|
157
|
+
let projectProfile;
|
|
158
|
+
try {
|
|
159
|
+
projectProfile = await discoverProject(srcPath);
|
|
160
|
+
} catch (error) {
|
|
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
|
+
// Emit project detection events
|
|
172
|
+
events.emit('project:detected', {
|
|
173
|
+
framework: projectProfile.framework,
|
|
174
|
+
router: projectProfile.router,
|
|
175
|
+
sourceRoot: projectProfile.sourceRoot,
|
|
176
|
+
packageManager: projectProfile.packageManager,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Emit phase events
|
|
180
|
+
events.emit('phase:started', {
|
|
181
|
+
phase: 'Detect Project',
|
|
182
|
+
message: 'Detecting project structure...',
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
events.emit('phase:started', {
|
|
186
|
+
phase: 'Resolve URL',
|
|
187
|
+
message: `Using URL: ${url}`,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
events.emit('phase:started', {
|
|
191
|
+
phase: 'Initialize Run',
|
|
192
|
+
message: 'Initializing run artifacts...',
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Write initial status
|
|
196
|
+
const now = new Date();
|
|
197
|
+
startedAt = now.toISOString();
|
|
198
|
+
|
|
199
|
+
atomicWriteJson(paths.runStatusJson, {
|
|
200
|
+
status: 'RUNNING',
|
|
201
|
+
runId,
|
|
202
|
+
startedAt,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Write metadata
|
|
206
|
+
atomicWriteJson(paths.runMetaJson, {
|
|
207
|
+
veraxVersion: getVersion(),
|
|
208
|
+
nodeVersion: process.version,
|
|
209
|
+
platform: process.platform,
|
|
210
|
+
cwd: projectRoot,
|
|
211
|
+
command: 'run',
|
|
212
|
+
args: { url, src, out },
|
|
213
|
+
url,
|
|
214
|
+
src: srcPath,
|
|
215
|
+
startedAt,
|
|
216
|
+
completedAt: null,
|
|
217
|
+
error: null,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Extract expectations first to compute budget
|
|
221
|
+
events.emit('phase:started', {
|
|
222
|
+
phase: 'Learn',
|
|
223
|
+
message: 'Analyzing project structure...',
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
events.startHeartbeat('Learn', json);
|
|
227
|
+
|
|
228
|
+
let expectations, skipped;
|
|
229
|
+
try {
|
|
230
|
+
// Extract expectations (quick operation, no timeout needed here)
|
|
231
|
+
const result = await extractExpectations(projectProfile, projectProfile.sourceRoot);
|
|
232
|
+
expectations = result.expectations;
|
|
233
|
+
skipped = result.skipped;
|
|
234
|
+
} finally {
|
|
235
|
+
events.stopHeartbeat();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Compute runtime budget based on expectations count
|
|
239
|
+
budget = computeRuntimeBudget({
|
|
240
|
+
expectationsCount: expectations.length,
|
|
241
|
+
mode: 'run',
|
|
242
|
+
framework: projectProfile.framework,
|
|
243
|
+
fileCount: projectProfile.fileCount || expectations.length,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Set up global watchdog timer
|
|
247
|
+
watchdogTimer = setTimeout(async () => {
|
|
248
|
+
await finalizeOnTimeout(`Global timeout exceeded: ${budget.totalMaxMs}ms`);
|
|
249
|
+
// Exit with code 0 (tool executed, just timed out)
|
|
250
|
+
process.exit(0);
|
|
251
|
+
}, budget.totalMaxMs);
|
|
252
|
+
|
|
253
|
+
// Wrap Learn phase with timeout
|
|
254
|
+
try {
|
|
255
|
+
await withTimeout(
|
|
256
|
+
budget.learnMaxMs,
|
|
257
|
+
Promise.resolve(), // Learn phase already completed
|
|
258
|
+
'Learn'
|
|
259
|
+
);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (error.message.includes('timeout')) {
|
|
262
|
+
await finalizeOnTimeout(`Learn phase timeout: ${budget.learnMaxMs}ms`);
|
|
263
|
+
process.exit(0);
|
|
264
|
+
}
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// For now, emit a placeholder trace event
|
|
269
|
+
events.emit('phase:completed', {
|
|
270
|
+
phase: 'Learn',
|
|
271
|
+
message: 'Project analysis complete',
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Observe phase with timeout
|
|
275
|
+
events.emit('phase:started', {
|
|
276
|
+
phase: 'Observe',
|
|
277
|
+
message: 'Launching browser and observing expectations...',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
events.startHeartbeat('Observe', json);
|
|
281
|
+
|
|
282
|
+
let observeData = null;
|
|
283
|
+
try {
|
|
284
|
+
if (expectations.length > 0) {
|
|
285
|
+
try {
|
|
286
|
+
observeData = await withTimeout(
|
|
287
|
+
budget.observeMaxMs,
|
|
288
|
+
observeExpectations(
|
|
289
|
+
expectations,
|
|
290
|
+
url,
|
|
291
|
+
paths.evidenceDir,
|
|
292
|
+
(progress) => {
|
|
293
|
+
events.emit(progress.event, progress);
|
|
294
|
+
}
|
|
295
|
+
),
|
|
296
|
+
'Observe'
|
|
297
|
+
);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (error.message.includes('timeout')) {
|
|
300
|
+
events.emit('observe:error', {
|
|
301
|
+
message: `Observe phase timeout: ${budget.observeMaxMs}ms`,
|
|
302
|
+
});
|
|
303
|
+
observeData = {
|
|
304
|
+
observations: [],
|
|
305
|
+
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
306
|
+
observedAt: new Date().toISOString(),
|
|
307
|
+
};
|
|
308
|
+
} else {
|
|
309
|
+
events.emit('observe:error', {
|
|
310
|
+
message: error.message,
|
|
311
|
+
});
|
|
312
|
+
observeData = {
|
|
313
|
+
observations: [],
|
|
314
|
+
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
315
|
+
observedAt: new Date().toISOString(),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
observeData = {
|
|
321
|
+
observations: [],
|
|
322
|
+
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
323
|
+
observedAt: new Date().toISOString(),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
} finally {
|
|
327
|
+
events.stopHeartbeat();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
events.emit('phase:completed', {
|
|
331
|
+
phase: 'Observe',
|
|
332
|
+
message: 'Browser observation complete',
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Detect phase with timeout
|
|
336
|
+
events.emit('phase:started', {
|
|
337
|
+
phase: 'Detect',
|
|
338
|
+
message: 'Analyzing findings and detecting silent failures...',
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
events.startHeartbeat('Detect', json);
|
|
342
|
+
|
|
343
|
+
let detectData = null;
|
|
344
|
+
try {
|
|
345
|
+
try {
|
|
346
|
+
// Use already-extracted expectations
|
|
347
|
+
const learnData = {
|
|
348
|
+
expectations,
|
|
349
|
+
skipped,
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
detectData = await withTimeout(
|
|
353
|
+
budget.detectMaxMs,
|
|
354
|
+
detectFindings(learnData, observeData, projectRoot, (progress) => {
|
|
355
|
+
events.emit(progress.event, progress);
|
|
356
|
+
}),
|
|
357
|
+
'Detect'
|
|
358
|
+
);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (error.message.includes('timeout')) {
|
|
361
|
+
events.emit('detect:error', {
|
|
362
|
+
message: `Detect phase timeout: ${budget.detectMaxMs}ms`,
|
|
363
|
+
});
|
|
364
|
+
detectData = {
|
|
365
|
+
findings: [],
|
|
366
|
+
stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
|
|
367
|
+
detectedAt: new Date().toISOString(),
|
|
368
|
+
};
|
|
369
|
+
} else {
|
|
370
|
+
events.emit('detect:error', {
|
|
371
|
+
message: error.message,
|
|
372
|
+
});
|
|
373
|
+
detectData = {
|
|
374
|
+
findings: [],
|
|
375
|
+
stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
|
|
376
|
+
detectedAt: new Date().toISOString(),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
} finally {
|
|
381
|
+
events.stopHeartbeat();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
events.emit('phase:completed', {
|
|
385
|
+
phase: 'Detect',
|
|
386
|
+
message: 'Silent failure detection complete',
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Clear watchdog timer on successful completion
|
|
390
|
+
if (watchdogTimer) {
|
|
391
|
+
clearTimeout(watchdogTimer);
|
|
392
|
+
watchdogTimer = null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Emit finalize phase
|
|
396
|
+
events.emit('phase:started', {
|
|
397
|
+
phase: 'Finalize Artifacts',
|
|
398
|
+
message: 'Writing run results...',
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
events.stopHeartbeat();
|
|
402
|
+
|
|
403
|
+
const completedAt = new Date().toISOString();
|
|
404
|
+
|
|
405
|
+
// Write completed status
|
|
406
|
+
atomicWriteJson(paths.runStatusJson, {
|
|
407
|
+
status: 'COMPLETE',
|
|
408
|
+
runId,
|
|
409
|
+
startedAt,
|
|
410
|
+
completedAt,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Update metadata with completion time
|
|
414
|
+
atomicWriteJson(paths.runMetaJson, {
|
|
415
|
+
veraxVersion: getVersion(),
|
|
416
|
+
nodeVersion: process.version,
|
|
417
|
+
platform: process.platform,
|
|
418
|
+
cwd: projectRoot,
|
|
419
|
+
command: 'run',
|
|
420
|
+
args: { url, src, out },
|
|
421
|
+
url,
|
|
422
|
+
src: srcPath,
|
|
423
|
+
startedAt,
|
|
424
|
+
completedAt,
|
|
425
|
+
error: null,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const runDurationMs = completedAt && startedAt ? (Date.parse(completedAt) - Date.parse(startedAt)) : 0;
|
|
429
|
+
const metrics = {
|
|
430
|
+
learnMs: observeData?.timings?.learnMs || 0,
|
|
431
|
+
observeMs: observeData?.timings?.observeMs || observeData?.timings?.totalMs || 0,
|
|
432
|
+
detectMs: detectData?.timings?.detectMs || detectData?.timings?.totalMs || 0,
|
|
433
|
+
totalMs: runDurationMs > 0 ? runDurationMs : (budget?.ms || 0)
|
|
434
|
+
};
|
|
435
|
+
const findingsCounts = detectData?.findingsCounts || {
|
|
436
|
+
HIGH: 0,
|
|
437
|
+
MEDIUM: 0,
|
|
438
|
+
LOW: 0,
|
|
439
|
+
UNKNOWN: 0,
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// Write summary with stable digest
|
|
443
|
+
writeSummaryJson(paths.summaryJson, {
|
|
444
|
+
runId,
|
|
445
|
+
status: 'COMPLETE',
|
|
446
|
+
startedAt,
|
|
447
|
+
completedAt,
|
|
448
|
+
command: 'run',
|
|
449
|
+
url,
|
|
450
|
+
notes: 'Run completed successfully',
|
|
451
|
+
metrics,
|
|
452
|
+
findingsCounts,
|
|
453
|
+
}, {
|
|
454
|
+
expectationsTotal: expectations.length,
|
|
455
|
+
attempted: observeData.stats?.attempted || 0,
|
|
456
|
+
observed: observeData.stats?.observed || 0,
|
|
457
|
+
silentFailures: detectData.stats?.silentFailures || 0,
|
|
458
|
+
coverageGaps: detectData.stats?.coverageGaps || 0,
|
|
459
|
+
unproven: detectData.stats?.unproven || 0,
|
|
460
|
+
informational: detectData.stats?.informational || 0,
|
|
461
|
+
...metrics,
|
|
462
|
+
...findingsCounts,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Write detect results (or empty if detection failed)
|
|
466
|
+
writeFindingsJson(paths.baseDir, detectData);
|
|
467
|
+
|
|
468
|
+
// Write traces (include all events including heartbeats)
|
|
469
|
+
const allEvents = events.getEvents();
|
|
470
|
+
const tracesContent = allEvents
|
|
471
|
+
.map(e => JSON.stringify(e))
|
|
472
|
+
.join('\n') + '\n';
|
|
473
|
+
atomicWriteText(paths.tracesJsonl, tracesContent);
|
|
474
|
+
|
|
475
|
+
// Write project profile
|
|
476
|
+
writeProjectJson(paths, projectProfile);
|
|
477
|
+
|
|
478
|
+
// Write learn results
|
|
479
|
+
writeLearnJson(paths, expectations, skipped);
|
|
480
|
+
|
|
481
|
+
// Write observe results
|
|
482
|
+
writeObserveJson(paths.baseDir, observeData);
|
|
483
|
+
|
|
484
|
+
events.emit('phase:completed', {
|
|
485
|
+
phase: 'Finalize Artifacts',
|
|
486
|
+
message: 'Run artifacts written',
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Emit final summary event
|
|
490
|
+
if (json) {
|
|
491
|
+
events.emit('run:complete', {
|
|
492
|
+
runId,
|
|
493
|
+
url,
|
|
494
|
+
command: 'run',
|
|
495
|
+
findingsCounts,
|
|
496
|
+
metrics,
|
|
497
|
+
digest: {
|
|
498
|
+
expectationsTotal: expectations.length,
|
|
499
|
+
attempted: observeData.stats?.attempted || 0,
|
|
500
|
+
observed: observeData.stats?.observed || 0,
|
|
501
|
+
silentFailures: detectData.stats?.silentFailures || 0,
|
|
502
|
+
coverageGaps: detectData.stats?.coverageGaps || 0,
|
|
503
|
+
unproven: detectData.stats?.unproven || 0,
|
|
504
|
+
informational: detectData.stats?.informational || 0,
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Print summary if not JSON mode
|
|
510
|
+
if (!json) {
|
|
511
|
+
console.log('\nRun complete.');
|
|
512
|
+
console.log(`Run ID: ${runId}`);
|
|
513
|
+
console.log(`Artifacts: ${paths.baseDir}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return { runId, paths, success: true };
|
|
517
|
+
} catch (error) {
|
|
518
|
+
// Clear watchdog timer on error
|
|
519
|
+
if (watchdogTimer) {
|
|
520
|
+
clearTimeout(watchdogTimer);
|
|
521
|
+
watchdogTimer = null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
events.stopHeartbeat();
|
|
525
|
+
|
|
526
|
+
// Mark run as FAILED if we have paths
|
|
527
|
+
if (paths && runId && startedAt) {
|
|
528
|
+
try {
|
|
529
|
+
const failedAt = new Date().toISOString();
|
|
530
|
+
atomicWriteJson(paths.runStatusJson, {
|
|
531
|
+
status: 'FAILED',
|
|
532
|
+
runId,
|
|
533
|
+
startedAt,
|
|
534
|
+
failedAt,
|
|
535
|
+
error: error.message,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Update metadata
|
|
539
|
+
atomicWriteJson(paths.runMetaJson, {
|
|
540
|
+
veraxVersion: getVersion(),
|
|
541
|
+
nodeVersion: process.version,
|
|
542
|
+
platform: process.platform,
|
|
543
|
+
cwd: projectRoot,
|
|
544
|
+
command: 'run',
|
|
545
|
+
args: { url, src, out },
|
|
546
|
+
url,
|
|
547
|
+
src: srcPath,
|
|
548
|
+
startedAt,
|
|
549
|
+
completedAt: failedAt,
|
|
550
|
+
error: error.message,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Write summary with digest even on failure
|
|
554
|
+
try {
|
|
555
|
+
writeSummaryJson(paths.summaryJson, {
|
|
556
|
+
runId,
|
|
557
|
+
status: 'FAILED',
|
|
558
|
+
startedAt,
|
|
559
|
+
completedAt: failedAt,
|
|
560
|
+
command: 'run',
|
|
561
|
+
url,
|
|
562
|
+
notes: `Run failed: ${error.message}`,
|
|
563
|
+
}, {
|
|
564
|
+
expectationsTotal: 0,
|
|
565
|
+
attempted: 0,
|
|
566
|
+
observed: 0,
|
|
567
|
+
silentFailures: 0,
|
|
568
|
+
coverageGaps: 0,
|
|
569
|
+
unproven: 0,
|
|
570
|
+
informational: 0,
|
|
571
|
+
});
|
|
572
|
+
} catch (summaryError) {
|
|
573
|
+
// Ignore summary write errors during failure handling
|
|
574
|
+
}
|
|
575
|
+
} catch (statusError) {
|
|
576
|
+
// Ignore errors when writing failure status
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
events.emit('error', {
|
|
581
|
+
message: error.message,
|
|
582
|
+
stack: error.stack,
|
|
583
|
+
});
|
|
584
|
+
throw error;
|
|
585
|
+
}
|
|
586
|
+
}
|