@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.
Files changed (126) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +14 -36
  4. package/src/cli/commands/default.js +523 -0
  5. package/src/cli/commands/doctor.js +165 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +402 -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 +296 -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 +34 -0
  14. package/src/cli/util/expectation-extractor.js +378 -0
  15. package/src/cli/util/findings-writer.js +31 -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 +366 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +29 -0
  21. package/src/cli/util/project-discovery.js +277 -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/summary-writer.js +32 -0
  26. package/src/verax/cli/ci-summary.js +35 -0
  27. package/src/verax/cli/context-explanation.js +89 -0
  28. package/src/verax/cli/doctor.js +277 -0
  29. package/src/verax/cli/error-normalizer.js +154 -0
  30. package/src/verax/cli/explain-output.js +105 -0
  31. package/src/verax/cli/finding-explainer.js +130 -0
  32. package/src/verax/cli/init.js +237 -0
  33. package/src/verax/cli/run-overview.js +163 -0
  34. package/src/verax/cli/url-safety.js +101 -0
  35. package/src/verax/cli/wizard.js +98 -0
  36. package/src/verax/cli/zero-findings-explainer.js +57 -0
  37. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  38. package/src/verax/core/action-classifier.js +86 -0
  39. package/src/verax/core/budget-engine.js +218 -0
  40. package/src/verax/core/canonical-outcomes.js +157 -0
  41. package/src/verax/core/decision-snapshot.js +335 -0
  42. package/src/verax/core/determinism-model.js +403 -0
  43. package/src/verax/core/incremental-store.js +237 -0
  44. package/src/verax/core/invariants.js +356 -0
  45. package/src/verax/core/promise-model.js +230 -0
  46. package/src/verax/core/replay-validator.js +350 -0
  47. package/src/verax/core/replay.js +222 -0
  48. package/src/verax/core/run-id.js +175 -0
  49. package/src/verax/core/run-manifest.js +99 -0
  50. package/src/verax/core/silence-impact.js +369 -0
  51. package/src/verax/core/silence-model.js +521 -0
  52. package/src/verax/detect/comparison.js +2 -34
  53. package/src/verax/detect/confidence-engine.js +764 -329
  54. package/src/verax/detect/detection-engine.js +293 -0
  55. package/src/verax/detect/evidence-index.js +177 -0
  56. package/src/verax/detect/expectation-model.js +194 -172
  57. package/src/verax/detect/explanation-helpers.js +187 -0
  58. package/src/verax/detect/finding-detector.js +450 -0
  59. package/src/verax/detect/findings-writer.js +44 -8
  60. package/src/verax/detect/flow-detector.js +366 -0
  61. package/src/verax/detect/index.js +172 -286
  62. package/src/verax/detect/interactive-findings.js +613 -0
  63. package/src/verax/detect/signal-mapper.js +308 -0
  64. package/src/verax/detect/verdict-engine.js +563 -0
  65. package/src/verax/evidence-index-writer.js +61 -0
  66. package/src/verax/index.js +90 -14
  67. package/src/verax/intel/effect-detector.js +368 -0
  68. package/src/verax/intel/handler-mapper.js +249 -0
  69. package/src/verax/intel/index.js +281 -0
  70. package/src/verax/intel/route-extractor.js +280 -0
  71. package/src/verax/intel/ts-program.js +256 -0
  72. package/src/verax/intel/vue-navigation-extractor.js +579 -0
  73. package/src/verax/intel/vue-router-extractor.js +323 -0
  74. package/src/verax/learn/action-contract-extractor.js +335 -101
  75. package/src/verax/learn/ast-contract-extractor.js +95 -5
  76. package/src/verax/learn/flow-extractor.js +172 -0
  77. package/src/verax/learn/manifest-writer.js +97 -47
  78. package/src/verax/learn/project-detector.js +40 -0
  79. package/src/verax/learn/route-extractor.js +27 -96
  80. package/src/verax/learn/state-extractor.js +212 -0
  81. package/src/verax/learn/static-extractor-navigation.js +114 -0
  82. package/src/verax/learn/static-extractor-validation.js +88 -0
  83. package/src/verax/learn/static-extractor.js +112 -4
  84. package/src/verax/learn/truth-assessor.js +24 -21
  85. package/src/verax/observe/aria-sensor.js +211 -0
  86. package/src/verax/observe/browser.js +10 -5
  87. package/src/verax/observe/console-sensor.js +1 -17
  88. package/src/verax/observe/domain-boundary.js +10 -1
  89. package/src/verax/observe/expectation-executor.js +512 -0
  90. package/src/verax/observe/flow-matcher.js +143 -0
  91. package/src/verax/observe/focus-sensor.js +196 -0
  92. package/src/verax/observe/human-driver.js +643 -275
  93. package/src/verax/observe/index.js +908 -27
  94. package/src/verax/observe/index.js.backup +1 -0
  95. package/src/verax/observe/interaction-discovery.js +365 -14
  96. package/src/verax/observe/interaction-runner.js +563 -198
  97. package/src/verax/observe/loading-sensor.js +139 -0
  98. package/src/verax/observe/navigation-sensor.js +255 -0
  99. package/src/verax/observe/network-sensor.js +55 -7
  100. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  101. package/src/verax/observe/observed-expectation.js +305 -0
  102. package/src/verax/observe/page-frontier.js +234 -0
  103. package/src/verax/observe/settle.js +37 -17
  104. package/src/verax/observe/state-sensor.js +389 -0
  105. package/src/verax/observe/timing-sensor.js +228 -0
  106. package/src/verax/observe/traces-writer.js +61 -20
  107. package/src/verax/observe/ui-signal-sensor.js +136 -17
  108. package/src/verax/scan-summary-writer.js +77 -15
  109. package/src/verax/shared/artifact-manager.js +110 -8
  110. package/src/verax/shared/budget-profiles.js +136 -0
  111. package/src/verax/shared/ci-detection.js +39 -0
  112. package/src/verax/shared/config-loader.js +170 -0
  113. package/src/verax/shared/dynamic-route-utils.js +218 -0
  114. package/src/verax/shared/expectation-coverage.js +44 -0
  115. package/src/verax/shared/expectation-prover.js +81 -0
  116. package/src/verax/shared/expectation-tracker.js +201 -0
  117. package/src/verax/shared/expectations-writer.js +60 -0
  118. package/src/verax/shared/first-run.js +44 -0
  119. package/src/verax/shared/progress-reporter.js +171 -0
  120. package/src/verax/shared/retry-policy.js +14 -1
  121. package/src/verax/shared/root-artifacts.js +49 -0
  122. package/src/verax/shared/scan-budget.js +86 -0
  123. package/src/verax/shared/url-normalizer.js +162 -0
  124. package/src/verax/shared/zip-artifacts.js +65 -0
  125. package/src/verax/validate/context-validator.js +244 -0
  126. package/src/verax/validate/context-validator.js.bak +0 -0
@@ -0,0 +1,523 @@
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 { UsageError, DataError, CrashError } 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
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = dirname(__filename);
24
+
25
+ function getVersion() {
26
+ try {
27
+ const pkgPath = resolve(__dirname, '../../../package.json');
28
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
29
+ return pkg.version;
30
+ } catch {
31
+ return '0.2.0';
32
+ }
33
+ }
34
+
35
+ /**
36
+ * `verax` smart default command
37
+ * Interactive mode with intelligent URL detection
38
+ */
39
+ export async function defaultCommand(options = {}) {
40
+ const {
41
+ src = '.',
42
+ out = '.verax',
43
+ url = null,
44
+ json = false,
45
+ verbose = false,
46
+ } = options;
47
+
48
+ const projectRoot = resolve(process.cwd());
49
+ const srcPath = resolve(projectRoot, src);
50
+
51
+ // Validate src directory exists
52
+ if (!existsSync(srcPath)) {
53
+ throw new DataError(`Source directory not found: ${srcPath}`);
54
+ }
55
+
56
+ // Create event emitter
57
+ const events = new RunEventEmitter();
58
+
59
+ // Setup event handlers
60
+ if (json) {
61
+ events.on('*', (event) => {
62
+ console.log(JSON.stringify(event));
63
+ });
64
+ }
65
+
66
+ // Show progress if not JSON
67
+ if (!json && !verbose) {
68
+ events.on('phase:started', (event) => {
69
+ if (!json) {
70
+ console.log(`${event.phase}...`);
71
+ }
72
+ });
73
+ }
74
+
75
+ try {
76
+ events.emit('phase:started', {
77
+ phase: 'Detect Project',
78
+ message: 'Detecting project type...',
79
+ });
80
+
81
+ // Discover project configuration
82
+ let projectProfile;
83
+ try {
84
+ projectProfile = await discoverProject(srcPath);
85
+ } catch (error) {
86
+ // If discovery fails, create a minimal profile
87
+ projectProfile = {
88
+ framework: 'unknown',
89
+ router: null,
90
+ sourceRoot: srcPath,
91
+ packageManager: 'unknown',
92
+ scripts: { dev: null, build: null, start: null },
93
+ detectedAt: new Date().toISOString(),
94
+ };
95
+ }
96
+
97
+ const frameworkName = getFrameworkDisplayName(projectProfile.framework, projectProfile.router);
98
+
99
+ if (!json) {
100
+ console.log(`Detected framework: ${frameworkName}`);
101
+ console.log(`Resolved source root: ${projectProfile.sourceRoot}`);
102
+ }
103
+
104
+ events.emit('project:detected', {
105
+ framework: projectProfile.framework,
106
+ router: projectProfile.router,
107
+ frameworkName,
108
+ sourceRoot: projectProfile.sourceRoot,
109
+ });
110
+
111
+ events.emit('phase:completed', {
112
+ phase: 'Detect Project',
113
+ message: `Detected framework: ${frameworkName}`,
114
+ });
115
+
116
+ // Phase: Resolve URL
117
+ let resolvedUrl = url;
118
+
119
+ if (!resolvedUrl) {
120
+ events.emit('phase:started', {
121
+ phase: 'Resolve URL',
122
+ message: 'Attempting to resolve URL from environment...',
123
+ });
124
+
125
+ resolvedUrl = tryResolveUrlFromEnv();
126
+
127
+ // Try to extract URL from dev script if available
128
+ if (!resolvedUrl && projectProfile.scripts.dev) {
129
+ const port = extractPortFromScript(projectProfile.scripts.dev);
130
+ if (port) {
131
+ resolvedUrl = `http://localhost:${port}`;
132
+ if (!json) {
133
+ console.log(`Detected dev script: ${projectProfile.scripts.dev}`);
134
+ }
135
+ events.emit('dev:script:detected', {
136
+ script: projectProfile.scripts.dev,
137
+ port,
138
+ });
139
+ }
140
+ }
141
+
142
+ if (resolvedUrl && !json) {
143
+ console.log(`Detected URL: ${resolvedUrl}`);
144
+ }
145
+ }
146
+
147
+ // If still no URL, prompt interactively
148
+ if (!resolvedUrl) {
149
+ events.emit('phase:started', {
150
+ phase: 'Resolve URL',
151
+ message: 'Prompting for URL...',
152
+ });
153
+
154
+ if (!json) {
155
+ console.log(''); // blank line
156
+ }
157
+
158
+ const answer = await inquirer.prompt([
159
+ {
160
+ type: 'input',
161
+ name: 'url',
162
+ message: 'Enter the URL to scan',
163
+ validate: (input) => {
164
+ if (!input.trim()) {
165
+ return 'URL is required';
166
+ }
167
+ if (!input.startsWith('http://') && !input.startsWith('https://')) {
168
+ return 'URL must start with http:// or https://';
169
+ }
170
+ return true;
171
+ },
172
+ },
173
+ ]);
174
+
175
+ resolvedUrl = answer.url;
176
+ }
177
+
178
+ if (!json) {
179
+ console.log(`Using URL: ${resolvedUrl}`);
180
+ }
181
+
182
+ events.emit('phase:completed', {
183
+ phase: 'Resolve URL',
184
+ message: `URL resolved: ${resolvedUrl}`,
185
+ });
186
+
187
+ // Generate run ID
188
+ let runId = generateRunId();
189
+ if (verbose && !json) console.log(`Run ID: ${runId}`);
190
+
191
+ let paths = getRunPaths(projectRoot, out, runId);
192
+ ensureRunDirectories(paths);
193
+
194
+ // Initialize Run
195
+ events.emit('phase:started', {
196
+ phase: 'Initialize Run',
197
+ message: 'Initializing run artifacts...',
198
+ });
199
+
200
+ const now = new Date();
201
+ let startedAt = now.toISOString();
202
+
203
+ atomicWriteJson(paths.runStatusJson, {
204
+ status: 'RUNNING',
205
+ runId,
206
+ startedAt,
207
+ });
208
+
209
+ atomicWriteJson(paths.runMetaJson, {
210
+ veraxVersion: getVersion(),
211
+ nodeVersion: process.version,
212
+ platform: process.platform,
213
+ cwd: projectRoot,
214
+ command: 'default',
215
+ args: { url: resolvedUrl, src },
216
+ url: resolvedUrl,
217
+ src: srcPath,
218
+ startedAt,
219
+ completedAt: null,
220
+ error: null,
221
+ });
222
+
223
+ events.emit('phase:completed', {
224
+ phase: 'Initialize Run',
225
+ message: 'Run initialized',
226
+ });
227
+
228
+ // Learning phase (placeholder)
229
+ events.emit('phase:started', {
230
+ phase: 'Learn',
231
+ message: 'Analyzing project structure...',
232
+ });
233
+
234
+ // Extract expectations
235
+ const { expectations, skipped } = await extractExpectations(projectProfile, projectProfile.sourceRoot);
236
+
237
+ if (!json) {
238
+ console.log(`Found ${expectations.length} expectations`);
239
+ if (Object.values(skipped).reduce((a, b) => a + b, 0) > 0) {
240
+ console.log(`Skipped: ${Object.values(skipped).reduce((a, b) => a + b, 0)} (dynamic/computed)`);
241
+ }
242
+ }
243
+
244
+ // Emit expectations found events
245
+ expectations.slice(0, 5).forEach(exp => {
246
+ events.emit('expectation:found', {
247
+ type: exp.type,
248
+ promise: exp.promise,
249
+ file: exp.source.file,
250
+ });
251
+ });
252
+
253
+ if (expectations.length > 5) {
254
+ events.emit('expectation:found', {
255
+ message: `... and ${expectations.length - 5} more expectations`,
256
+ });
257
+ }
258
+
259
+ events.emit('phase:completed', {
260
+ phase: 'Learn',
261
+ message: 'Project analysis complete',
262
+ });
263
+
264
+ // Observe phase
265
+ events.emit('phase:started', {
266
+ phase: 'Observe',
267
+ message: 'Launching browser and observing expectations...',
268
+ });
269
+
270
+ let observeData = null;
271
+ if (expectations.length > 0) {
272
+ try {
273
+ observeData = await observeExpectations(
274
+ expectations,
275
+ resolvedUrl,
276
+ paths.evidenceDir,
277
+ (progress) => {
278
+ events.emit(progress.event, progress);
279
+ if (!json && progress.event === 'observe:result') {
280
+ const status = progress.observed ? '✓' : '✗';
281
+ console.log(` ${status} ${progress.index}/${expectations.length}`);
282
+ }
283
+ }
284
+ );
285
+
286
+ if (!json) {
287
+ console.log(`Observed: ${observeData.stats.observed}/${expectations.length}`);
288
+ }
289
+ } catch (error) {
290
+ if (!json) {
291
+ console.error(`Observe error: ${error.message}`);
292
+ }
293
+ events.emit('observe:error', {
294
+ message: error.message,
295
+ });
296
+ observeData = {
297
+ observations: [],
298
+ stats: { attempted: 0, observed: 0, notObserved: 0 },
299
+ observedAt: new Date().toISOString(),
300
+ };
301
+ }
302
+ } else {
303
+ observeData = {
304
+ observations: [],
305
+ stats: { attempted: 0, observed: 0, notObserved: 0 },
306
+ observedAt: new Date().toISOString(),
307
+ };
308
+ }
309
+
310
+ events.emit('phase:completed', {
311
+ phase: 'Observe',
312
+ message: 'Browser observation complete',
313
+ });
314
+
315
+ // Detect phase
316
+ events.emit('phase:started', {
317
+ phase: 'Detect',
318
+ message: 'Analyzing findings and detecting silent failures...',
319
+ });
320
+
321
+ // Load learn and observe data for detection
322
+ let learnData = { expectations: [] };
323
+ let detectData = null;
324
+
325
+ try {
326
+ learnData = {
327
+ expectations,
328
+ skipped,
329
+ };
330
+
331
+ detectData = await detectFindings(learnData, observeData, projectRoot, (progress) => {
332
+ events.emit(progress.event, progress);
333
+ if (!json && progress.event === 'detect:classified') {
334
+ const symbol = progress.classification === 'silent-failure' ? '✗' :
335
+ progress.classification === 'observed' ? '✓' :
336
+ progress.classification === 'coverage-gap' ? '⊘' : '⚠';
337
+ console.log(` ${symbol} ${progress.index}/${learnData.expectations.length}`);
338
+ }
339
+ });
340
+
341
+ if (!json && detectData.stats.silentFailures > 0) {
342
+ console.log(`Silent failures detected: ${detectData.stats.silentFailures}`);
343
+ }
344
+ } catch (error) {
345
+ if (!json) {
346
+ console.error(`Detect error: ${error.message}`);
347
+ }
348
+ events.emit('detect:error', {
349
+ message: error.message,
350
+ });
351
+ detectData = {
352
+ findings: [],
353
+ stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
354
+ detectedAt: new Date().toISOString(),
355
+ };
356
+ }
357
+
358
+ events.emit('phase:completed', {
359
+ phase: 'Detect',
360
+ message: 'Silent failure detection complete',
361
+ });
362
+
363
+ // Finalize Artifacts
364
+ events.emit('phase:started', {
365
+ phase: 'Finalize Artifacts',
366
+ message: 'Writing run results...',
367
+ });
368
+
369
+ const completedAt = new Date().toISOString();
370
+
371
+ atomicWriteJson(paths.runStatusJson, {
372
+ status: 'COMPLETE',
373
+ runId,
374
+ startedAt,
375
+ completedAt,
376
+ });
377
+
378
+ atomicWriteJson(paths.runMetaJson, {
379
+ veraxVersion: getVersion(),
380
+ nodeVersion: process.version,
381
+ platform: process.platform,
382
+ cwd: projectRoot,
383
+ command: 'default',
384
+ args: { url: resolvedUrl, src },
385
+ url: resolvedUrl,
386
+ src: srcPath,
387
+ startedAt,
388
+ completedAt,
389
+ error: null,
390
+ });
391
+
392
+ // Write summary with stable digest
393
+ writeSummaryJson(paths.summaryJson, {
394
+ runId,
395
+ status: 'COMPLETE',
396
+ startedAt,
397
+ completedAt,
398
+ command: 'default',
399
+ url: resolvedUrl,
400
+ notes: 'Run completed successfully',
401
+ }, {
402
+ expectationsTotal: expectations.length,
403
+ attempted: observeData.stats?.attempted || 0,
404
+ observed: observeData.stats?.observed || 0,
405
+ silentFailures: detectData.stats?.silentFailures || 0,
406
+ coverageGaps: detectData.stats?.coverageGaps || 0,
407
+ unproven: detectData.stats?.unproven || 0,
408
+ informational: detectData.stats?.informational || 0,
409
+ });
410
+
411
+ // Write detect results (or empty if detection failed)
412
+ writeFindingsJson(paths.baseDir, detectData);
413
+
414
+ const traces = [
415
+ {
416
+ type: 'phase:started',
417
+ timestamp: startedAt,
418
+ phase: 'Detect Project',
419
+ },
420
+ {
421
+ type: 'phase:completed',
422
+ timestamp: new Date().toISOString(),
423
+ phase: 'Detect Project',
424
+ },
425
+ {
426
+ type: 'phase:started',
427
+ timestamp: new Date().toISOString(),
428
+ phase: 'Learn',
429
+ },
430
+ {
431
+ type: 'phase:completed',
432
+ timestamp: completedAt,
433
+ phase: 'Learn',
434
+ },
435
+ ];
436
+
437
+ const tracesContent = traces.map(t => JSON.stringify(t)).join('\n') + '\n';
438
+ atomicWriteText(paths.tracesJsonl, tracesContent);
439
+
440
+ // Write project profile
441
+ writeProjectJson(paths, projectProfile);
442
+
443
+ // Write learn results
444
+ writeLearnJson(paths, expectations, skipped);
445
+
446
+ // Write observe results
447
+ writeObserveJson(paths.baseDir, observeData);
448
+
449
+ events.emit('phase:completed', {
450
+ phase: 'Finalize Artifacts',
451
+ message: 'Run artifacts written',
452
+ });
453
+
454
+ // Print summary if not JSON mode
455
+ if (!json) {
456
+ console.log('\nRun complete.');
457
+ console.log(`Run ID: ${runId}`);
458
+ console.log(`Artifacts: ${paths.baseDir}`);
459
+ }
460
+
461
+ return { runId, paths, url: resolvedUrl, success: true };
462
+ } catch (error) {
463
+ // Mark run as FAILED if we have paths
464
+ if (paths && runId && startedAt) {
465
+ try {
466
+ const failedAt = new Date().toISOString();
467
+ atomicWriteJson(paths.runStatusJson, {
468
+ status: 'FAILED',
469
+ runId,
470
+ startedAt,
471
+ failedAt,
472
+ error: error.message,
473
+ });
474
+
475
+ // Update metadata
476
+ atomicWriteJson(paths.runMetaJson, {
477
+ veraxVersion: getVersion(),
478
+ nodeVersion: process.version,
479
+ platform: process.platform,
480
+ cwd: projectRoot,
481
+ command: 'default',
482
+ args: { url: url || null, src },
483
+ url: url || null,
484
+ src: srcPath,
485
+ startedAt,
486
+ completedAt: failedAt,
487
+ error: error.message,
488
+ });
489
+
490
+ // Write summary with digest even on failure
491
+ try {
492
+ writeSummaryJson(paths.summaryJson, {
493
+ runId,
494
+ status: 'FAILED',
495
+ startedAt,
496
+ completedAt: failedAt,
497
+ command: 'default',
498
+ url: url || null,
499
+ notes: `Run failed: ${error.message}`,
500
+ }, {
501
+ expectationsTotal: 0,
502
+ attempted: 0,
503
+ observed: 0,
504
+ silentFailures: 0,
505
+ coverageGaps: 0,
506
+ unproven: 0,
507
+ informational: 0,
508
+ });
509
+ } catch (summaryError) {
510
+ // Ignore summary write errors during failure handling
511
+ }
512
+ } catch (statusError) {
513
+ // Ignore errors when writing failure status
514
+ }
515
+ }
516
+
517
+ events.emit('error', {
518
+ message: error.message,
519
+ stack: error.stack,
520
+ });
521
+ throw error;
522
+ }
523
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * VERAX Doctor (Phase 8.1)
3
+ * Environment diagnostics for enterprise hardening.
4
+ * Never throws on check failures; only invalid usage should bubble out.
5
+ */
6
+
7
+ import { existsSync } from 'fs';
8
+ import { platform } from 'os';
9
+ import { createRequire } from 'module';
10
+ import { UsageError } from '../util/errors.js';
11
+
12
+ export async function doctorCommand(options = {}) {
13
+ const { json = false, extraFlags = [] } = options;
14
+
15
+ if (extraFlags.length > 0) {
16
+ throw new UsageError(`Unknown flag(s): ${extraFlags.join(', ')}`);
17
+ }
18
+
19
+ const checks = [];
20
+ const recommendations = [];
21
+ const platformName = platform();
22
+ const nodeVersion = process.versions.node;
23
+ let playwrightVersion = null;
24
+ let playwright = null;
25
+ let chromiumPath = null;
26
+ const require = createRequire(import.meta.url);
27
+
28
+ const addCheck = (name, status, details, recommendation) => {
29
+ checks.push({ name, status, details });
30
+ if (recommendation) {
31
+ recommendations.push(recommendation);
32
+ }
33
+ };
34
+
35
+ // 1) Node.js version
36
+ const nodeMajor = parseInt(nodeVersion.split('.')[0], 10);
37
+ if (Number.isFinite(nodeMajor) && nodeMajor >= 18) {
38
+ addCheck('Node.js version', 'pass', `Detected v${nodeVersion} (>=18 required)`);
39
+ } else {
40
+ addCheck('Node.js version', 'fail', `Detected v${nodeVersion} (<18)`, 'Upgrade Node.js to v18+');
41
+ }
42
+
43
+ // 2) Playwright package presence + version
44
+ try {
45
+ playwright = await import('playwright');
46
+ try {
47
+ const pkg = require('playwright/package.json');
48
+ playwrightVersion = pkg?.version || null;
49
+ } catch {
50
+ playwrightVersion = null;
51
+ }
52
+ addCheck('Playwright package', 'pass', `Installed${playwrightVersion ? ` v${playwrightVersion}` : ''}`);
53
+ } catch (error) {
54
+ addCheck(
55
+ 'Playwright package',
56
+ 'fail',
57
+ 'Not installed or not resolvable',
58
+ 'npm install -D playwright'
59
+ );
60
+ }
61
+
62
+ // 3) Playwright Chromium binaries
63
+ if (playwright && playwright.chromium) {
64
+ try {
65
+ chromiumPath = playwright.chromium.executablePath();
66
+ if (chromiumPath && existsSync(chromiumPath)) {
67
+ addCheck('Playwright Chromium', 'pass', `Executable found at ${chromiumPath}`);
68
+ } else {
69
+ addCheck(
70
+ 'Playwright Chromium',
71
+ 'fail',
72
+ 'Chromium binary not found',
73
+ 'npx playwright install --with-deps chromium'
74
+ );
75
+ }
76
+ } catch (error) {
77
+ addCheck(
78
+ 'Playwright Chromium',
79
+ 'fail',
80
+ `Unable to resolve Chromium executable (${error.message})`,
81
+ 'npx playwright install --with-deps chromium'
82
+ );
83
+ }
84
+ } else {
85
+ addCheck(
86
+ 'Playwright Chromium',
87
+ 'fail',
88
+ 'Skipped because Playwright is missing',
89
+ 'npm install -D playwright && npx playwright install --with-deps chromium'
90
+ );
91
+ }
92
+
93
+ // 4) Headless launch smoke test
94
+ if (playwright && playwright.chromium && chromiumPath && existsSync(chromiumPath)) {
95
+ try {
96
+ const browser = await playwright.chromium.launch({ headless: true });
97
+ const page = await browser.newPage();
98
+ await page.goto('about:blank');
99
+ await browser.close();
100
+ addCheck('Headless smoke test', 'pass', 'Chromium launched headless and closed successfully');
101
+ } catch (error) {
102
+ addCheck(
103
+ 'Headless smoke test',
104
+ 'fail',
105
+ `Headless launch failed: ${error.message}`,
106
+ platformName === 'linux'
107
+ ? 'Try: npx playwright install --with-deps chromium && launch with --no-sandbox in constrained environments'
108
+ : 'Reinstall playwright: npx playwright install --with-deps chromium'
109
+ );
110
+ }
111
+ } else {
112
+ addCheck(
113
+ 'Headless smoke test',
114
+ 'fail',
115
+ 'Skipped because Chromium executable is unavailable',
116
+ 'npx playwright install --with-deps chromium'
117
+ );
118
+ }
119
+
120
+ // 5) Linux sandbox guidance
121
+ if (platformName === 'linux') {
122
+ const ciHints = detectCIHints();
123
+ const detailParts = ['Sandbox guidance: if launch fails, use --no-sandbox or ensure libnss3/libatk are installed'];
124
+ if (ciHints) detailParts.push(`Detected CI: ${ciHints}`);
125
+ addCheck('Linux sandbox guidance', 'pass', detailParts.join(' | '));
126
+ }
127
+
128
+ const ok = checks.every((c) => c.status === 'pass');
129
+
130
+ if (json) {
131
+ const report = {
132
+ ok,
133
+ platform: platformName,
134
+ nodeVersion,
135
+ playwrightVersion,
136
+ checks,
137
+ recommendations,
138
+ };
139
+ console.log(JSON.stringify(report, null, 2));
140
+ return report;
141
+ }
142
+
143
+ // Human-readable output
144
+ console.log('VERAX Doctor — Environment Diagnostics');
145
+ checks.forEach((c) => {
146
+ const label = c.status === 'pass' ? 'PASS' : 'FAIL';
147
+ console.log(`[${label}] ${c.name}: ${c.details}`);
148
+ });
149
+ console.log(`
150
+ Overall: ${ok ? 'OK' : 'Issues found'} (${checks.filter(c => c.status === 'fail').length} failing checks)`);
151
+ if (recommendations.length > 0) {
152
+ console.log('\nRecommended actions:');
153
+ recommendations.forEach((r) => console.log(`- ${r}`));
154
+ }
155
+
156
+ return { ok, checks, recommendations };
157
+ }
158
+
159
+ function detectCIHints() {
160
+ if (process.env.GITHUB_ACTIONS === 'true') return 'GITHUB_ACTIONS';
161
+ if (process.env.CI === 'true') return 'CI';
162
+ if (process.env.BITBUCKET_BUILD_NUMBER) return 'BITBUCKET';
163
+ if (process.env.GITLAB_CI) return 'GITLAB_CI';
164
+ return '';
165
+ }