@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,26 @@
1
+ import { atomicWriteJson } from './atomic-write.js';
2
+ import { resolve } from 'path';
3
+
4
+ /**
5
+ * Write project profile artifact
6
+ */
7
+ export function writeProjectJson(runPaths, projectProfile) {
8
+ const projectJsonPath = resolve(runPaths.baseDir, 'project.json');
9
+
10
+ const projectJson = {
11
+ framework: projectProfile.framework,
12
+ router: projectProfile.router,
13
+ sourceRoot: projectProfile.sourceRoot,
14
+ packageManager: projectProfile.packageManager,
15
+ scripts: {
16
+ dev: projectProfile.scripts.dev,
17
+ build: projectProfile.scripts.build,
18
+ start: projectProfile.scripts.start,
19
+ },
20
+ detectedAt: projectProfile.detectedAt,
21
+ };
22
+
23
+ atomicWriteJson(projectJsonPath, projectJson);
24
+
25
+ return projectJsonPath;
26
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Redaction utilities (Phase 8.2)
3
+ * Deterministic redaction for headers, URLs, bodies, console messages.
4
+ */
5
+
6
+ const REDACTED = '***REDACTED***';
7
+ const SENSITIVE_HEADERS = [
8
+ 'authorization',
9
+ 'cookie',
10
+ 'set-cookie',
11
+ 'x-api-key',
12
+ 'proxy-authorization',
13
+ ];
14
+
15
+ function ensureCounters(counters) {
16
+ if (!counters) return { headersRedacted: 0, tokensRedacted: 0 };
17
+ if (typeof counters.headersRedacted !== 'number') counters.headersRedacted = 0;
18
+ if (typeof counters.tokensRedacted !== 'number') counters.tokensRedacted = 0;
19
+ return counters;
20
+ }
21
+
22
+ export function redactHeaders(headers = {}, counters = { headersRedacted: 0, tokensRedacted: 0 }) {
23
+ const c = ensureCounters(counters);
24
+ const sanitized = {};
25
+ Object.entries(headers || {}).forEach(([key, value]) => {
26
+ const lower = key.toLowerCase();
27
+ if (SENSITIVE_HEADERS.includes(lower)) {
28
+ sanitized[key] = REDACTED;
29
+ c.headersRedacted += 1;
30
+ } else {
31
+ sanitized[key] = value;
32
+ }
33
+ });
34
+ return sanitized;
35
+ }
36
+
37
+ export function redactUrl(url, counters = { headersRedacted: 0, tokensRedacted: 0 }) {
38
+ if (url == null) return '';
39
+ return redactTokensInText(url, counters);
40
+ }
41
+
42
+ export function redactBody(body, counters = { headersRedacted: 0, tokensRedacted: 0 }) {
43
+ if (body == null) return body;
44
+
45
+ const c = ensureCounters(counters);
46
+
47
+ if (typeof body === 'string') {
48
+ return redactTokensInText(body, counters);
49
+ }
50
+
51
+ if (typeof body === 'object' && !Array.isArray(body)) {
52
+ // Handle plain objects by redacting sensitive property names
53
+ const redacted = {};
54
+ Object.entries(body).forEach(([key, value]) => {
55
+ const keyLower = key.toLowerCase();
56
+ if (keyLower === 'token' || keyLower === 'api_key' || keyLower === 'access_token' ||
57
+ keyLower === 'password' || keyLower === 'secret') {
58
+ // Redact sensitive property values
59
+ c.tokensRedacted += 1;
60
+ redacted[key] = REDACTED;
61
+ } else if (typeof value === 'string') {
62
+ // Redact token patterns within string values
63
+ redacted[key] = redactTokensInText(value, counters);
64
+ } else if (typeof value === 'object' && value !== null) {
65
+ // Recursively redact nested objects/arrays
66
+ redacted[key] = redactBody(value, counters);
67
+ } else {
68
+ redacted[key] = value;
69
+ }
70
+ });
71
+ return redacted;
72
+ }
73
+
74
+ if (Array.isArray(body)) {
75
+ return body.map(item => redactBody(item, counters));
76
+ }
77
+
78
+ try {
79
+ const str = JSON.stringify(body);
80
+ const redacted = redactTokensInText(str, counters);
81
+ return JSON.parse(redacted);
82
+ } catch {
83
+ return REDACTED;
84
+ }
85
+ }
86
+
87
+ export function redactConsole(text = '', counters = { headersRedacted: 0, tokensRedacted: 0 }) {
88
+ return redactTokensInText(text, counters);
89
+ }
90
+
91
+ export function redactTokensInText(text, counters = { headersRedacted: 0, tokensRedacted: 0 }) {
92
+ const c = ensureCounters(counters);
93
+ if (text == null) return '';
94
+ if (typeof text !== 'string') return String(text);
95
+
96
+ let output = text;
97
+
98
+ // Query/string parameters FIRST (most specific - only api_key, access_token)
99
+ // Use word boundary \b to avoid matching within words like "token" inside "example.com"
100
+ output = output.replace(/\b(api_key|access_token)=([^&\s]+)/gi, (match, key, value) => {
101
+ // Skip if value is itself just REDACTED (avoid double-redacting)
102
+ if (value === REDACTED) return match;
103
+ c.tokensRedacted += 1;
104
+ return `${key}=${REDACTED}`;
105
+ });
106
+
107
+ // Bearer tokens
108
+ output = output.replace(/Bearer\s+([A-Za-z0-9._-]+)/gi, (match, token) => {
109
+ c.tokensRedacted += 1;
110
+ return `Bearer ${REDACTED}`;
111
+ });
112
+
113
+ // JWT-like strings (three base64url-ish segments)
114
+ // More specific: require uppercase or numbers, not just domain patterns like "api.example.com"
115
+ output = output.replace(/[A-Z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, (match) => {
116
+ c.tokensRedacted += 1;
117
+ return REDACTED;
118
+ });
119
+
120
+ return output;
121
+ }
122
+
123
+ export function getRedactionCounters(counters) {
124
+ const c = ensureCounters(counters);
125
+ return { headersRedacted: c.headersRedacted, tokensRedacted: c.tokensRedacted };
126
+ }
127
+
128
+ export const REDACTION_PLACEHOLDER = REDACTED;
@@ -0,0 +1,30 @@
1
+ import crypto from 'crypto';
2
+
3
+ /**
4
+ * Generate a run ID in format: <ISO_TIMESTAMP_UTC>_<6-8 char short hash>
5
+ * Example: 2026-01-11T00-59-12Z_4f2a9c
6
+ */
7
+ export function generateRunId() {
8
+ // Create ISO timestamp with colons replaced by dashes for filesystem compatibility
9
+ const now = new Date();
10
+ const isoString = now.toISOString();
11
+ // Format: 2026-01-11T00:59:12.123Z -> 2026-01-11T00-59-12Z
12
+ const timestamp = isoString.replace(/:/g, '-').replace(/\.\d+Z/, 'Z');
13
+
14
+ // Generate a short hash (6-8 chars)
15
+ const hash = crypto
16
+ .randomBytes(4)
17
+ .toString('hex')
18
+ .substring(0, 6);
19
+
20
+ return `${timestamp}_${hash}`;
21
+ }
22
+
23
+ /**
24
+ * Validate a run ID format
25
+ */
26
+ export function isValidRunId(runId) {
27
+ // Pattern: YYYY-MM-DDTHH-MM-SSZ_hexchars
28
+ const pattern = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z_[a-f0-9]{6,8}$/;
29
+ return pattern.test(runId);
30
+ }
@@ -0,0 +1,32 @@
1
+ import { atomicWriteJson } from './atomic-write.js';
2
+ import { resolve } from 'path';
3
+
4
+ /**
5
+ * Write summary.json with deterministic digest
6
+ * The digest provides stable counts that should be identical across runs
7
+ * on the same input (assuming site behavior is stable)
8
+ */
9
+ export function writeSummaryJson(summaryPath, summaryData, stats = {}) {
10
+ const payload = {
11
+ runId: summaryData.runId,
12
+ status: summaryData.status,
13
+ startedAt: summaryData.startedAt,
14
+ completedAt: summaryData.completedAt,
15
+ command: summaryData.command,
16
+ url: summaryData.url,
17
+ notes: summaryData.notes,
18
+
19
+ // Stable digest that should be identical across repeated runs on same input
20
+ digest: {
21
+ expectationsTotal: stats.expectationsTotal || 0,
22
+ attempted: stats.attempted || 0,
23
+ observed: stats.observed || 0,
24
+ silentFailures: stats.silentFailures || 0,
25
+ coverageGaps: stats.coverageGaps || 0,
26
+ unproven: stats.unproven || 0,
27
+ informational: stats.informational || 0,
28
+ },
29
+ };
30
+
31
+ atomicWriteJson(summaryPath, payload);
32
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * CI One-Line Summary
3
+ *
4
+ * Prints a deterministic one-line summary for CI logs.
5
+ * OBSERVATIONAL ONLY - no verdicts, no trust badges, no judgments.
6
+ */
7
+
8
+ /**
9
+ * Generate CI one-line summary
10
+ * @param {Object} context - Summary context
11
+ * @returns {string} One-line summary
12
+ */
13
+ export function generateCISummary(context = {}) {
14
+ const {
15
+ expectations = 0,
16
+ interactions = 0,
17
+ findings = 0,
18
+ gaps = 0,
19
+ silences = 0,
20
+ runId = 'unknown'
21
+ } = context;
22
+
23
+ // Format: VERAX | expectations=... | interactions=... | findings=... | gaps=... | silences=... | run=...
24
+ return `VERAX | expectations=${expectations} | interactions=${interactions} | findings=${findings} | gaps=${gaps} | silences=${silences} | run=${runId}`;
25
+ }
26
+
27
+ /**
28
+ * Print CI one-line summary
29
+ * @param {Object} context - Summary context
30
+ */
31
+ export function printCISummary(context) {
32
+ const summary = generateCISummary(context);
33
+ console.error(summary);
34
+ }
35
+
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Wave 4.1 — Context Validation Explanation
3
+ *
4
+ * Provides detailed explanations when context validation fails or matches.
5
+ */
6
+
7
+ /**
8
+ * Generate context validation explanation
9
+ * @param {Object} contextCheck - Context check result
10
+ * @param {Array} projectRoutes - Routes extracted from project
11
+ * @returns {Array<string>} Explanation lines
12
+ */
13
+ export function explainContextValidation(contextCheck, projectRoutes = []) {
14
+ const lines = [];
15
+
16
+ if (!contextCheck.ran) {
17
+ if (contextCheck.reason === 'no_routes_extracted') {
18
+ lines.push('Context validation skipped: No routes extracted from project.');
19
+ } else if (contextCheck.reason === 'invalid_url') {
20
+ lines.push('Context validation skipped: Invalid URL format.');
21
+ } else {
22
+ lines.push('Context validation skipped.');
23
+ }
24
+ return lines;
25
+ }
26
+
27
+ if (contextCheck.verdict === 'VALID_CONTEXT') {
28
+ lines.push(`✓ Context validated: URL matches project`);
29
+ lines.push(` ${contextCheck.matchedRoutesCount} of ${contextCheck.totalRoutesChecked || projectRoutes.length} routes matched`);
30
+ if (contextCheck.sampleMatched && contextCheck.sampleMatched.length > 0) {
31
+ lines.push(` Sample matched routes: ${contextCheck.sampleMatched.slice(0, 3).join(', ')}`);
32
+ }
33
+ return lines;
34
+ }
35
+
36
+ // INVALID_CONTEXT or INVALID_CONTEXT_FORCED
37
+ lines.push(`⚠ Context mismatch: URL does not match project`);
38
+ lines.push(` Project routes found: ${projectRoutes.length}`);
39
+ lines.push(` Routes matched: ${contextCheck.matchedRoutesCount} of ${contextCheck.totalRoutesChecked || projectRoutes.length}`);
40
+
41
+ if (projectRoutes.length > 0 && contextCheck.matchedRoutesCount === 0) {
42
+ lines.push('');
43
+ lines.push(' Project routes (sample):');
44
+ const sampleRoutes = projectRoutes.slice(0, 5);
45
+ for (const route of sampleRoutes) {
46
+ lines.push(` - ${route}`);
47
+ }
48
+
49
+ if (contextCheck.internalLinksFound !== undefined) {
50
+ lines.push(` Live site internal links found: ${contextCheck.internalLinksFound}`);
51
+ }
52
+
53
+ lines.push('');
54
+ lines.push(' Possible reasons:');
55
+ lines.push(' • Route paths don\'t match (e.g., /about vs /about.html)');
56
+ lines.push(' • Project routes not linked on homepage');
57
+ lines.push(' • SPA routes not accessible at expected paths');
58
+
59
+ if (contextCheck.verdict === 'INVALID_CONTEXT') {
60
+ lines.push('');
61
+ lines.push(' Next steps:');
62
+ lines.push(' • Use --force to scan anyway');
63
+ lines.push(' • Verify URL matches project deployment');
64
+ lines.push(' • Check that routes exist on the live site');
65
+ } else {
66
+ lines.push('');
67
+ lines.push(' Scan continued with --force flag despite mismatch.');
68
+ }
69
+ }
70
+
71
+ return lines;
72
+ }
73
+
74
+ /**
75
+ * Print context validation explanation
76
+ * @param {Object} contextCheck - Context check result
77
+ * @param {Array} projectRoutes - Routes extracted from project
78
+ */
79
+ export function printContextExplanation(contextCheck, projectRoutes = []) {
80
+ const lines = explainContextValidation(contextCheck, projectRoutes);
81
+ if (lines.length > 0) {
82
+ console.error('');
83
+ console.error('Context Validation:');
84
+ lines.forEach(line => {
85
+ console.error(line);
86
+ });
87
+ }
88
+ }
89
+
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Wave 7 — VERAX Doctor
3
+ *
4
+ * Checks environment, dependencies, and project setup.
5
+ */
6
+
7
+ import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
8
+ import { resolve } from 'path';
9
+ import { chromium } from 'playwright';
10
+ import { get } from 'http';
11
+ import { get as httpsGet } from 'https';
12
+ import { learn } from '../index.js';
13
+
14
+ /**
15
+ * Check Node version
16
+ * @returns {Object} { status: 'ok'|'warn'|'fail', message: string }
17
+ */
18
+ function checkNodeVersion() {
19
+ const requiredMajor = 18;
20
+ const nodeVersion = process.version;
21
+ const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0], 10);
22
+
23
+ if (majorVersion >= requiredMajor) {
24
+ return { status: 'ok', message: `Node.js ${nodeVersion} (required: >=${requiredMajor}.0.0)` };
25
+ } else {
26
+ return {
27
+ status: 'fail',
28
+ message: `Node.js ${nodeVersion} is too old (required: >=${requiredMajor}.0.0)`,
29
+ fix: `Upgrade Node.js: nvm install ${requiredMajor} or visit nodejs.org`
30
+ };
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Check write permissions to output directory
36
+ * @param {string} projectRoot - Project root
37
+ * @returns {Object} { status: 'ok'|'fail', message: string }
38
+ */
39
+ function checkWritePermissions(projectRoot) {
40
+ try {
41
+ const veraxDir = resolve(projectRoot, '.verax');
42
+ mkdirSync(veraxDir, { recursive: true });
43
+
44
+ const testFile = resolve(veraxDir, '.write-test');
45
+ writeFileSync(testFile, 'test');
46
+ unlinkSync(testFile);
47
+
48
+ return { status: 'ok', message: 'Can write to .verax directory' };
49
+ } catch (error) {
50
+ return {
51
+ status: 'fail',
52
+ message: `Cannot write to .verax directory: ${error.message}`,
53
+ fix: 'Check file permissions or run with appropriate access'
54
+ };
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Check Playwright availability
60
+ * @returns {Promise<Object>} { status: 'ok'|'fail', message: string }
61
+ */
62
+ async function checkPlaywright() {
63
+ try {
64
+ const browser = await chromium.launch({ headless: true });
65
+ await browser.close();
66
+ return { status: 'ok', message: 'Playwright browser is available' };
67
+ } catch (error) {
68
+ // Detect common error messages and provide specific fixes
69
+ const errorMsg = error.message.toLowerCase();
70
+ let fix = 'Run: npx playwright install';
71
+
72
+ if (errorMsg.includes('chromium') || errorMsg.includes('executable')) {
73
+ fix = 'Run: npx playwright install chromium';
74
+ } else if (errorMsg.includes('missing') || errorMsg.includes('not found')) {
75
+ fix = 'Run: npx playwright install --with-deps chromium';
76
+ }
77
+
78
+ return {
79
+ status: 'fail',
80
+ message: `Playwright browser not available: ${error.message}`,
81
+ fix: fix
82
+ };
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Check project detection and expectations
88
+ * @param {string} projectRoot - Project root
89
+ * @returns {Promise<Object>} { status: 'ok'|'warn', message: string, details: Object }
90
+ */
91
+ async function checkProjectExpectations(projectRoot) {
92
+ try {
93
+ const manifest = await learn(projectRoot);
94
+ const projectType = manifest.projectType || 'unknown';
95
+ const expectationsCount = manifest.learnTruth?.expectationsDiscovered || 0;
96
+
97
+ if (expectationsCount > 0) {
98
+ return {
99
+ status: 'ok',
100
+ message: `Project type: ${projectType}, ${expectationsCount} expectations found`,
101
+ details: {
102
+ projectType,
103
+ expectationsCount,
104
+ routesCount: manifest.publicRoutes?.length || 0
105
+ }
106
+ };
107
+ } else {
108
+ return {
109
+ status: 'warn',
110
+ message: `Project type: ${projectType}, but 0 expectations found`,
111
+ details: {
112
+ projectType,
113
+ expectationsCount: 0
114
+ },
115
+ fix: 'Add static patterns (HTML links, static fetch calls, or state mutations)'
116
+ };
117
+ }
118
+ } catch (error) {
119
+ return {
120
+ status: 'fail',
121
+ message: `Failed to analyze project: ${error.message}`,
122
+ fix: 'Check that projectRoot is correct and project is readable'
123
+ };
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Check URL reachability
129
+ * @param {string} url - URL to check
130
+ * @returns {Promise<Object>} { status: 'ok'|'fail', message: string }
131
+ */
132
+ async function checkUrlReachability(url) {
133
+ return new Promise((resolve) => {
134
+ try {
135
+ const urlObj = new URL(url);
136
+ const clientGet = urlObj.protocol === 'https:' ? httpsGet : get;
137
+
138
+ const request = clientGet(url, { timeout: 5000 }, (response) => {
139
+ request.destroy();
140
+ if (response.statusCode >= 200 && response.statusCode < 400) {
141
+ resolve({ status: 'ok', message: `URL ${url} is reachable (${response.statusCode})` });
142
+ } else {
143
+ resolve({
144
+ status: 'warn',
145
+ message: `URL ${url} returned ${response.statusCode}`,
146
+ fix: 'Verify URL is correct and server is running'
147
+ });
148
+ }
149
+ });
150
+
151
+ request.on('error', (error) => {
152
+ resolve({
153
+ status: 'fail',
154
+ message: `Cannot reach ${url}: ${error.message}`,
155
+ fix: 'Ensure server is running and URL is correct'
156
+ });
157
+ });
158
+
159
+ request.on('timeout', () => {
160
+ request.destroy();
161
+ resolve({
162
+ status: 'warn',
163
+ message: `URL ${url} did not respond within 5 seconds`,
164
+ fix: 'Check if server is running and accessible'
165
+ });
166
+ });
167
+
168
+ request.setTimeout(5000);
169
+ } catch (error) {
170
+ resolve({
171
+ status: 'fail',
172
+ message: `Invalid URL: ${error.message}`,
173
+ fix: 'Provide a valid URL (e.g., http://localhost:3000)'
174
+ });
175
+ }
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Run doctor checks
181
+ * @param {Object} options - { projectRoot, url, json }
182
+ * @returns {Promise<Object>} Doctor results
183
+ */
184
+ export async function runDoctor(options = {}) {
185
+ const { projectRoot = process.cwd(), url = null, json = false } = options;
186
+
187
+ const checks = [];
188
+ let overallStatus = 'ok';
189
+
190
+ // Check 1: Node version
191
+ const nodeCheck = checkNodeVersion();
192
+ checks.push({ name: 'Node.js Version', ...nodeCheck });
193
+ if (nodeCheck.status === 'fail') overallStatus = 'fail';
194
+
195
+ // Check 2: Write permissions
196
+ const writeCheck = checkWritePermissions(projectRoot);
197
+ checks.push({ name: 'Write Permissions', ...writeCheck });
198
+ if (writeCheck.status === 'fail') overallStatus = 'fail';
199
+
200
+ // Check 3: Playwright
201
+ const playwrightCheck = await checkPlaywright();
202
+ checks.push({ name: 'Playwright Browser', ...playwrightCheck });
203
+ if (playwrightCheck.status === 'fail') overallStatus = 'fail';
204
+
205
+ // Check 4: Project expectations
206
+ const projectCheck = await checkProjectExpectations(projectRoot);
207
+ checks.push({ name: 'Project Analysis', ...projectCheck });
208
+ if (projectCheck.status === 'fail') overallStatus = 'fail';
209
+ else if (projectCheck.status === 'warn' && overallStatus === 'ok') overallStatus = 'warn';
210
+
211
+ // Check 5: URL reachability (if provided)
212
+ if (url) {
213
+ const urlCheck = await checkUrlReachability(url);
214
+ checks.push({ name: 'URL Reachability', ...urlCheck });
215
+ if (urlCheck.status === 'fail') overallStatus = 'fail';
216
+ else if (urlCheck.status === 'warn' && overallStatus === 'ok') overallStatus = 'warn';
217
+ }
218
+
219
+ // Collect fixes
220
+ const fixes = checks.filter(c => c.fix).map(c => c.fix);
221
+
222
+ return {
223
+ status: overallStatus,
224
+ checks,
225
+ fixes
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Print doctor results
231
+ * @param {Object} results - Doctor results
232
+ * @param {boolean} json - Whether to output JSON
233
+ */
234
+ export function printDoctorResults(results, json = false) {
235
+ if (json) {
236
+ console.log(JSON.stringify(results, null, 2));
237
+ return;
238
+ }
239
+
240
+ // Human-readable output
241
+ console.error('\n' + '═'.repeat(60));
242
+ console.error('VERAX Doctor');
243
+ console.error('═'.repeat(60));
244
+
245
+ const statusEmoji = {
246
+ 'ok': '✅',
247
+ 'warn': '⚠️',
248
+ 'fail': '❌'
249
+ };
250
+
251
+ for (const check of results.checks) {
252
+ const emoji = statusEmoji[check.status] || '❓';
253
+ console.error(`\n${emoji} ${check.name}`);
254
+ console.error(` ${check.message}`);
255
+ if (check.details) {
256
+ for (const [key, value] of Object.entries(check.details)) {
257
+ console.error(` ${key}: ${value}`);
258
+ }
259
+ }
260
+ if (check.fix) {
261
+ console.error(` Fix: ${check.fix}`);
262
+ }
263
+ }
264
+
265
+ console.error('\n' + '─'.repeat(60));
266
+ console.error(`Overall Status: ${results.status.toUpperCase()}`);
267
+
268
+ if (results.fixes.length > 0) {
269
+ console.error('\nRecommended Fixes:');
270
+ results.fixes.forEach((fix, index) => {
271
+ console.error(` ${index + 1}. ${fix}`);
272
+ });
273
+ }
274
+
275
+ console.error('═'.repeat(60) + '\n');
276
+ }
277
+