@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.
Files changed (135) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +24 -36
  4. package/src/cli/commands/default.js +681 -0
  5. package/src/cli/commands/doctor.js +197 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +586 -0
  8. package/src/cli/entry.js +196 -0
  9. package/src/cli/util/atomic-write.js +37 -0
  10. package/src/cli/util/detection-engine.js +297 -0
  11. package/src/cli/util/env-url.js +33 -0
  12. package/src/cli/util/errors.js +44 -0
  13. package/src/cli/util/events.js +110 -0
  14. package/src/cli/util/expectation-extractor.js +388 -0
  15. package/src/cli/util/findings-writer.js +32 -0
  16. package/src/cli/util/idgen.js +87 -0
  17. package/src/cli/util/learn-writer.js +39 -0
  18. package/src/cli/util/observation-engine.js +412 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +30 -0
  21. package/src/cli/util/project-discovery.js +297 -0
  22. package/src/cli/util/project-writer.js +26 -0
  23. package/src/cli/util/redact.js +128 -0
  24. package/src/cli/util/run-id.js +30 -0
  25. package/src/cli/util/runtime-budget.js +147 -0
  26. package/src/cli/util/summary-writer.js +43 -0
  27. package/src/types/global.d.ts +28 -0
  28. package/src/types/ts-ast.d.ts +24 -0
  29. package/src/verax/cli/ci-summary.js +35 -0
  30. package/src/verax/cli/context-explanation.js +89 -0
  31. package/src/verax/cli/doctor.js +277 -0
  32. package/src/verax/cli/error-normalizer.js +154 -0
  33. package/src/verax/cli/explain-output.js +105 -0
  34. package/src/verax/cli/finding-explainer.js +130 -0
  35. package/src/verax/cli/init.js +237 -0
  36. package/src/verax/cli/run-overview.js +163 -0
  37. package/src/verax/cli/url-safety.js +111 -0
  38. package/src/verax/cli/wizard.js +109 -0
  39. package/src/verax/cli/zero-findings-explainer.js +57 -0
  40. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  41. package/src/verax/core/action-classifier.js +86 -0
  42. package/src/verax/core/budget-engine.js +218 -0
  43. package/src/verax/core/canonical-outcomes.js +157 -0
  44. package/src/verax/core/decision-snapshot.js +335 -0
  45. package/src/verax/core/determinism-model.js +432 -0
  46. package/src/verax/core/incremental-store.js +245 -0
  47. package/src/verax/core/invariants.js +356 -0
  48. package/src/verax/core/promise-model.js +230 -0
  49. package/src/verax/core/replay-validator.js +350 -0
  50. package/src/verax/core/replay.js +222 -0
  51. package/src/verax/core/run-id.js +175 -0
  52. package/src/verax/core/run-manifest.js +99 -0
  53. package/src/verax/core/silence-impact.js +369 -0
  54. package/src/verax/core/silence-model.js +523 -0
  55. package/src/verax/detect/comparison.js +7 -34
  56. package/src/verax/detect/confidence-engine.js +764 -329
  57. package/src/verax/detect/detection-engine.js +293 -0
  58. package/src/verax/detect/evidence-index.js +127 -0
  59. package/src/verax/detect/expectation-model.js +241 -168
  60. package/src/verax/detect/explanation-helpers.js +187 -0
  61. package/src/verax/detect/finding-detector.js +450 -0
  62. package/src/verax/detect/findings-writer.js +41 -12
  63. package/src/verax/detect/flow-detector.js +366 -0
  64. package/src/verax/detect/index.js +200 -288
  65. package/src/verax/detect/interactive-findings.js +612 -0
  66. package/src/verax/detect/signal-mapper.js +308 -0
  67. package/src/verax/detect/skip-classifier.js +4 -4
  68. package/src/verax/detect/verdict-engine.js +561 -0
  69. package/src/verax/evidence-index-writer.js +61 -0
  70. package/src/verax/flow/flow-engine.js +3 -2
  71. package/src/verax/flow/flow-spec.js +1 -2
  72. package/src/verax/index.js +103 -15
  73. package/src/verax/intel/effect-detector.js +368 -0
  74. package/src/verax/intel/handler-mapper.js +249 -0
  75. package/src/verax/intel/index.js +281 -0
  76. package/src/verax/intel/route-extractor.js +280 -0
  77. package/src/verax/intel/ts-program.js +256 -0
  78. package/src/verax/intel/vue-navigation-extractor.js +642 -0
  79. package/src/verax/intel/vue-router-extractor.js +325 -0
  80. package/src/verax/learn/action-contract-extractor.js +338 -104
  81. package/src/verax/learn/ast-contract-extractor.js +148 -6
  82. package/src/verax/learn/flow-extractor.js +172 -0
  83. package/src/verax/learn/index.js +36 -2
  84. package/src/verax/learn/manifest-writer.js +122 -58
  85. package/src/verax/learn/project-detector.js +40 -0
  86. package/src/verax/learn/route-extractor.js +28 -97
  87. package/src/verax/learn/route-validator.js +8 -7
  88. package/src/verax/learn/state-extractor.js +212 -0
  89. package/src/verax/learn/static-extractor-navigation.js +114 -0
  90. package/src/verax/learn/static-extractor-validation.js +88 -0
  91. package/src/verax/learn/static-extractor.js +119 -10
  92. package/src/verax/learn/truth-assessor.js +24 -21
  93. package/src/verax/learn/ts-contract-resolver.js +14 -12
  94. package/src/verax/observe/aria-sensor.js +211 -0
  95. package/src/verax/observe/browser.js +30 -6
  96. package/src/verax/observe/console-sensor.js +2 -18
  97. package/src/verax/observe/domain-boundary.js +10 -1
  98. package/src/verax/observe/expectation-executor.js +513 -0
  99. package/src/verax/observe/flow-matcher.js +143 -0
  100. package/src/verax/observe/focus-sensor.js +196 -0
  101. package/src/verax/observe/human-driver.js +660 -273
  102. package/src/verax/observe/index.js +910 -26
  103. package/src/verax/observe/interaction-discovery.js +378 -15
  104. package/src/verax/observe/interaction-runner.js +562 -197
  105. package/src/verax/observe/loading-sensor.js +145 -0
  106. package/src/verax/observe/navigation-sensor.js +255 -0
  107. package/src/verax/observe/network-sensor.js +55 -7
  108. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  109. package/src/verax/observe/observed-expectation.js +305 -0
  110. package/src/verax/observe/page-frontier.js +234 -0
  111. package/src/verax/observe/settle.js +38 -17
  112. package/src/verax/observe/state-sensor.js +393 -0
  113. package/src/verax/observe/state-ui-sensor.js +7 -1
  114. package/src/verax/observe/timing-sensor.js +228 -0
  115. package/src/verax/observe/traces-writer.js +73 -21
  116. package/src/verax/observe/ui-signal-sensor.js +143 -17
  117. package/src/verax/scan-summary-writer.js +80 -15
  118. package/src/verax/shared/artifact-manager.js +111 -9
  119. package/src/verax/shared/budget-profiles.js +136 -0
  120. package/src/verax/shared/caching.js +1 -1
  121. package/src/verax/shared/ci-detection.js +39 -0
  122. package/src/verax/shared/config-loader.js +169 -0
  123. package/src/verax/shared/dynamic-route-utils.js +224 -0
  124. package/src/verax/shared/expectation-coverage.js +44 -0
  125. package/src/verax/shared/expectation-prover.js +81 -0
  126. package/src/verax/shared/expectation-tracker.js +201 -0
  127. package/src/verax/shared/expectations-writer.js +60 -0
  128. package/src/verax/shared/first-run.js +44 -0
  129. package/src/verax/shared/progress-reporter.js +171 -0
  130. package/src/verax/shared/retry-policy.js +9 -1
  131. package/src/verax/shared/root-artifacts.js +49 -0
  132. package/src/verax/shared/scan-budget.js +86 -0
  133. package/src/verax/shared/url-normalizer.js +162 -0
  134. package/src/verax/shared/zip-artifacts.js +66 -0
  135. 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
+ }