@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.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +14 -36
- package/src/cli/commands/default.js +523 -0
- package/src/cli/commands/doctor.js +165 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +402 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +296 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +34 -0
- package/src/cli/util/expectation-extractor.js +378 -0
- package/src/cli/util/findings-writer.js +31 -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 +366 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +29 -0
- package/src/cli/util/project-discovery.js +277 -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/summary-writer.js +32 -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 +101 -0
- package/src/verax/cli/wizard.js +98 -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 +403 -0
- package/src/verax/core/incremental-store.js +237 -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 +521 -0
- package/src/verax/detect/comparison.js +2 -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 +177 -0
- package/src/verax/detect/expectation-model.js +194 -172
- 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 +44 -8
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +172 -286
- package/src/verax/detect/interactive-findings.js +613 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/verdict-engine.js +563 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/index.js +90 -14
- 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 +579 -0
- package/src/verax/intel/vue-router-extractor.js +323 -0
- package/src/verax/learn/action-contract-extractor.js +335 -101
- package/src/verax/learn/ast-contract-extractor.js +95 -5
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/manifest-writer.js +97 -47
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +27 -96
- 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 +112 -4
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +10 -5
- package/src/verax/observe/console-sensor.js +1 -17
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +512 -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 +643 -275
- package/src/verax/observe/index.js +908 -27
- package/src/verax/observe/index.js.backup +1 -0
- package/src/verax/observe/interaction-discovery.js +365 -14
- package/src/verax/observe/interaction-runner.js +563 -198
- package/src/verax/observe/loading-sensor.js +139 -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 +37 -17
- package/src/verax/observe/state-sensor.js +389 -0
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +61 -20
- package/src/verax/observe/ui-signal-sensor.js +136 -17
- package/src/verax/scan-summary-writer.js +77 -15
- package/src/verax/shared/artifact-manager.js +110 -8
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +170 -0
- package/src/verax/shared/dynamic-route-utils.js +218 -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 +14 -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 +65 -0
- package/src/verax/validate/context-validator.js +244 -0
- 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
|
+
|