@veraxhq/verax 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +24 -36
- package/src/cli/commands/default.js +681 -0
- package/src/cli/commands/doctor.js +197 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +586 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +297 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +110 -0
- package/src/cli/util/expectation-extractor.js +388 -0
- package/src/cli/util/findings-writer.js +32 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +412 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +30 -0
- package/src/cli/util/project-discovery.js +297 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +43 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +111 -0
- package/src/verax/cli/wizard.js +109 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +432 -0
- package/src/verax/core/incremental-store.js +245 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +523 -0
- package/src/verax/detect/comparison.js +7 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +127 -0
- package/src/verax/detect/expectation-model.js +241 -168
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +41 -12
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +200 -288
- package/src/verax/detect/interactive-findings.js +612 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +561 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +103 -15
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +642 -0
- package/src/verax/intel/vue-router-extractor.js +325 -0
- package/src/verax/learn/action-contract-extractor.js +338 -104
- package/src/verax/learn/ast-contract-extractor.js +148 -6
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +122 -58
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +28 -97
- package/src/verax/learn/route-validator.js +8 -7
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +119 -10
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +30 -6
- package/src/verax/observe/console-sensor.js +2 -18
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +513 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +660 -273
- package/src/verax/observe/index.js +910 -26
- package/src/verax/observe/interaction-discovery.js +378 -15
- package/src/verax/observe/interaction-runner.js +562 -197
- package/src/verax/observe/loading-sensor.js +145 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +38 -17
- package/src/verax/observe/state-sensor.js +393 -0
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +73 -21
- package/src/verax/observe/ui-signal-sensor.js +143 -17
- package/src/verax/scan-summary-writer.js +80 -15
- package/src/verax/shared/artifact-manager.js +111 -9
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +169 -0
- package/src/verax/shared/dynamic-route-utils.js +224 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +9 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +66 -0
- package/src/verax/validate/context-validator.js +244 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BUDGET PROFILES
|
|
3
|
+
*
|
|
4
|
+
* Predefined scan budgets for different use cases.
|
|
5
|
+
* Select using VERAX_BUDGET_PROFILE environment variable.
|
|
6
|
+
*
|
|
7
|
+
* Available profiles:
|
|
8
|
+
* - QUICK: 20 seconds, minimal coverage (developer rapid feedback)
|
|
9
|
+
* - STANDARD: 60 seconds, balanced (default CI behavior)
|
|
10
|
+
* - THOROUGH: 120 seconds, deeper coverage (comprehensive validation)
|
|
11
|
+
* - EXHAUSTIVE: 300 seconds, maximum coverage (deep audit)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createScanBudget } from './scan-budget.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* QUICK profile: Fast feedback for development
|
|
18
|
+
* - maxScanDurationMs: 20 seconds (vs 60)
|
|
19
|
+
* - maxInteractionsPerPage: 20 (vs 30)
|
|
20
|
+
* - maxPages: 2 (vs 50)
|
|
21
|
+
* - maxFlows: 1 (vs 3)
|
|
22
|
+
* Recommended for: Local development, PR checks, pre-commit
|
|
23
|
+
*/
|
|
24
|
+
const QUICK_PROFILE = {
|
|
25
|
+
maxScanDurationMs: 20000,
|
|
26
|
+
maxInteractionsPerPage: 20,
|
|
27
|
+
maxPages: 2,
|
|
28
|
+
maxFlows: 1,
|
|
29
|
+
maxFlowSteps: 3
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* STANDARD profile: Balanced coverage (DEFAULT)
|
|
34
|
+
* - maxScanDurationMs: 60 seconds (default)
|
|
35
|
+
* - maxInteractionsPerPage: 30 (default)
|
|
36
|
+
* - maxPages: 50 (default)
|
|
37
|
+
* - maxFlows: 3 (default)
|
|
38
|
+
* Recommended for: CI/CD pipelines, standard regression testing
|
|
39
|
+
*/
|
|
40
|
+
const STANDARD_PROFILE = {};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* THOROUGH profile: Deeper coverage for comprehensive validation
|
|
44
|
+
* - maxScanDurationMs: 120 seconds (2x default)
|
|
45
|
+
* - maxInteractionsPerPage: 50 (1.67x default)
|
|
46
|
+
* - maxPages: 50 (unchanged)
|
|
47
|
+
* - maxFlows: 5 (1.67x default)
|
|
48
|
+
* - stabilizationWindowMs: 6000 (2x default for flakiness reduction)
|
|
49
|
+
* - adaptiveStabilization: true (extend settle if DOM/network still changing)
|
|
50
|
+
* Recommended for: Pre-release testing, critical applications
|
|
51
|
+
*/
|
|
52
|
+
const THOROUGH_PROFILE = {
|
|
53
|
+
maxScanDurationMs: 120000,
|
|
54
|
+
maxInteractionsPerPage: 50,
|
|
55
|
+
maxFlows: 5,
|
|
56
|
+
maxFlowSteps: 7,
|
|
57
|
+
stabilizationWindowMs: 6000,
|
|
58
|
+
adaptiveStabilization: true
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* EXHAUSTIVE profile: Maximum coverage for deep audit
|
|
63
|
+
* - maxScanDurationMs: 300 seconds (5x default)
|
|
64
|
+
* - maxInteractionsPerPage: 100 (3.3x default)
|
|
65
|
+
* - maxPages: 50 (unchanged, depth not breadth)
|
|
66
|
+
* - maxFlows: 10 (3.3x default)
|
|
67
|
+
* - stabilizationWindowMs: 8000 (2.67x default for comprehensive stability)
|
|
68
|
+
* - adaptiveStabilization: true (extend settle generously for difficult applications)
|
|
69
|
+
* Recommended for: Security audits, full app validation, non-time-critical analysis
|
|
70
|
+
*/
|
|
71
|
+
const EXHAUSTIVE_PROFILE = {
|
|
72
|
+
maxScanDurationMs: 300000,
|
|
73
|
+
maxInteractionsPerPage: 100,
|
|
74
|
+
maxFlows: 10,
|
|
75
|
+
maxFlowSteps: 10,
|
|
76
|
+
stabilizationWindowMs: 8000,
|
|
77
|
+
adaptiveStabilization: true
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const PROFILES = {
|
|
81
|
+
QUICK: QUICK_PROFILE,
|
|
82
|
+
STANDARD: STANDARD_PROFILE,
|
|
83
|
+
THOROUGH: THOROUGH_PROFILE,
|
|
84
|
+
EXHAUSTIVE: EXHAUSTIVE_PROFILE
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the active budget profile based on VERAX_BUDGET_PROFILE env var.
|
|
89
|
+
* Defaults to STANDARD if not set.
|
|
90
|
+
* @returns {Object} Profile overrides
|
|
91
|
+
*/
|
|
92
|
+
export function getActiveBudgetProfile() {
|
|
93
|
+
const profileName = process.env.VERAX_BUDGET_PROFILE || 'STANDARD';
|
|
94
|
+
const profile = PROFILES[profileName.toUpperCase()];
|
|
95
|
+
|
|
96
|
+
if (!profile) {
|
|
97
|
+
console.warn(
|
|
98
|
+
`Warning: Unknown budget profile '${profileName}'. ` +
|
|
99
|
+
`Available profiles: ${Object.keys(PROFILES).join(', ')}. ` +
|
|
100
|
+
`Defaulting to STANDARD.`
|
|
101
|
+
);
|
|
102
|
+
return STANDARD_PROFILE;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return profile;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create a scan budget with the active profile applied.
|
|
110
|
+
* @returns {Object} Complete scan budget with profile applied
|
|
111
|
+
*/
|
|
112
|
+
export function createScanBudgetWithProfile() {
|
|
113
|
+
const profile = getActiveBudgetProfile();
|
|
114
|
+
return createScanBudget(profile);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get profile metadata for logging/debugging.
|
|
119
|
+
* @returns {Object} Profile name and configuration
|
|
120
|
+
*/
|
|
121
|
+
export function getProfileMetadata() {
|
|
122
|
+
const profileName = process.env.VERAX_BUDGET_PROFILE || 'STANDARD';
|
|
123
|
+
const profile = PROFILES[profileName.toUpperCase()] || STANDARD_PROFILE;
|
|
124
|
+
const budget = createScanBudget(profile);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
name: profileName.toUpperCase(),
|
|
128
|
+
maxScanDurationMs: budget.maxScanDurationMs,
|
|
129
|
+
maxInteractionsPerPage: budget.maxInteractionsPerPage,
|
|
130
|
+
maxPages: budget.maxPages,
|
|
131
|
+
maxFlows: budget.maxFlows,
|
|
132
|
+
maxFlowSteps: budget.maxFlowSteps
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { PROFILES };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 5 — CI Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects CI environment and handles CI-specific behavior.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Detect if running in CI environment
|
|
9
|
+
* @param {Object} options - Options with explicit ci flag
|
|
10
|
+
* @returns {boolean} True if in CI mode
|
|
11
|
+
*/
|
|
12
|
+
export function isCI(options = {}) {
|
|
13
|
+
// Explicit --ci flag takes precedence
|
|
14
|
+
if (options.ci === true || options.ci === 'true') {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check common CI environment variables
|
|
19
|
+
const ciEnvVars = [
|
|
20
|
+
'CI',
|
|
21
|
+
'CONTINUOUS_INTEGRATION',
|
|
22
|
+
'GITHUB_ACTIONS',
|
|
23
|
+
'GITLAB_CI',
|
|
24
|
+
'JENKINS_URL',
|
|
25
|
+
'TRAVIS',
|
|
26
|
+
'CIRCLECI',
|
|
27
|
+
'BITBUCKET_COMMIT',
|
|
28
|
+
'TEAMCITY_VERSION'
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
for (const envVar of ciEnvVars) {
|
|
32
|
+
if (process.env[envVar]) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 7 — Config File Support
|
|
3
|
+
*
|
|
4
|
+
* Loads and validates .verax/config.json configuration file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, existsSync } from 'fs';
|
|
8
|
+
import { resolve } from 'path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default config values
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_CONFIG = {
|
|
14
|
+
defaultUrl: 'http://localhost:3000',
|
|
15
|
+
projectRoot: '.',
|
|
16
|
+
outDir: '.verax/runs',
|
|
17
|
+
ciDefaults: {
|
|
18
|
+
json: true,
|
|
19
|
+
zip: true,
|
|
20
|
+
explain: false
|
|
21
|
+
},
|
|
22
|
+
safety: {
|
|
23
|
+
allowlistDomains: [],
|
|
24
|
+
denyKeywords: ['delete', 'remove', 'billing', 'payment']
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validate config structure
|
|
30
|
+
* @param {Object} config - Config object to validate
|
|
31
|
+
* @returns {Object} { valid: boolean, errors: string[] }
|
|
32
|
+
*/
|
|
33
|
+
export function validateConfig(config) {
|
|
34
|
+
const errors = [];
|
|
35
|
+
|
|
36
|
+
if (typeof config !== 'object' || config === null) {
|
|
37
|
+
errors.push('Config must be an object');
|
|
38
|
+
return { valid: false, errors };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Validate defaultUrl
|
|
42
|
+
if (config.defaultUrl !== undefined) {
|
|
43
|
+
if (typeof config.defaultUrl !== 'string' || config.defaultUrl.trim() === '') {
|
|
44
|
+
errors.push('defaultUrl must be a non-empty string');
|
|
45
|
+
} else {
|
|
46
|
+
try {
|
|
47
|
+
new URL(config.defaultUrl);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
errors.push(`defaultUrl is not a valid URL: ${config.defaultUrl}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate projectRoot
|
|
55
|
+
if (config.projectRoot !== undefined && typeof config.projectRoot !== 'string') {
|
|
56
|
+
errors.push('projectRoot must be a string');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Validate outDir
|
|
60
|
+
if (config.outDir !== undefined && typeof config.outDir !== 'string') {
|
|
61
|
+
errors.push('outDir must be a string');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate ciDefaults
|
|
65
|
+
if (config.ciDefaults !== undefined) {
|
|
66
|
+
if (typeof config.ciDefaults !== 'object' || config.ciDefaults === null) {
|
|
67
|
+
errors.push('ciDefaults must be an object');
|
|
68
|
+
} else {
|
|
69
|
+
if (config.ciDefaults.json !== undefined && typeof config.ciDefaults.json !== 'boolean') {
|
|
70
|
+
errors.push('ciDefaults.json must be a boolean');
|
|
71
|
+
}
|
|
72
|
+
if (config.ciDefaults.zip !== undefined && typeof config.ciDefaults.zip !== 'boolean') {
|
|
73
|
+
errors.push('ciDefaults.zip must be a boolean');
|
|
74
|
+
}
|
|
75
|
+
if (config.ciDefaults.explain !== undefined && typeof config.ciDefaults.explain !== 'boolean') {
|
|
76
|
+
errors.push('ciDefaults.explain must be a boolean');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Validate safety
|
|
82
|
+
if (config.safety !== undefined) {
|
|
83
|
+
if (typeof config.safety !== 'object' || config.safety === null) {
|
|
84
|
+
errors.push('safety must be an object');
|
|
85
|
+
} else {
|
|
86
|
+
if (config.safety.allowlistDomains !== undefined) {
|
|
87
|
+
if (!Array.isArray(config.safety.allowlistDomains)) {
|
|
88
|
+
errors.push('safety.allowlistDomains must be an array');
|
|
89
|
+
} else {
|
|
90
|
+
for (const domain of config.safety.allowlistDomains) {
|
|
91
|
+
if (typeof domain !== 'string') {
|
|
92
|
+
errors.push('safety.allowlistDomains must contain only strings');
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (config.safety.denyKeywords !== undefined) {
|
|
99
|
+
if (!Array.isArray(config.safety.denyKeywords)) {
|
|
100
|
+
errors.push('safety.denyKeywords must be an array');
|
|
101
|
+
} else {
|
|
102
|
+
for (const keyword of config.safety.denyKeywords) {
|
|
103
|
+
if (typeof keyword !== 'string') {
|
|
104
|
+
errors.push('safety.denyKeywords must contain only strings');
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
valid: errors.length === 0,
|
|
115
|
+
errors
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Load config file from project directory
|
|
121
|
+
* @param {string} projectRoot - Project root directory
|
|
122
|
+
* @returns {Object|null} Config object or null if not found
|
|
123
|
+
* @throws {Error} If config file exists but is invalid
|
|
124
|
+
*/
|
|
125
|
+
export function loadConfig(projectRoot) {
|
|
126
|
+
const configPath = resolve(projectRoot, '.verax', 'config.json');
|
|
127
|
+
|
|
128
|
+
if (!existsSync(configPath)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
134
|
+
const config = JSON.parse(content);
|
|
135
|
+
|
|
136
|
+
const validation = validateConfig(config);
|
|
137
|
+
if (!validation.valid) {
|
|
138
|
+
throw new Error(`Invalid config file: ${validation.errors.join(', ')}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Merge with defaults
|
|
142
|
+
return {
|
|
143
|
+
...DEFAULT_CONFIG,
|
|
144
|
+
...config,
|
|
145
|
+
ciDefaults: {
|
|
146
|
+
...DEFAULT_CONFIG.ciDefaults,
|
|
147
|
+
...(config.ciDefaults || {})
|
|
148
|
+
},
|
|
149
|
+
safety: {
|
|
150
|
+
...DEFAULT_CONFIG.safety,
|
|
151
|
+
...(config.safety || {})
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof SyntaxError) {
|
|
156
|
+
throw new Error(`Config file is not valid JSON: ${error.message}`);
|
|
157
|
+
}
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get default config (for init)
|
|
164
|
+
* @returns {Object} Default config object
|
|
165
|
+
*/
|
|
166
|
+
export function getDefaultConfig() {
|
|
167
|
+
return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
168
|
+
}
|
|
169
|
+
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DYNAMIC ROUTE UTILITIES
|
|
3
|
+
*
|
|
4
|
+
* Safely converts dynamic route patterns into executable example paths.
|
|
5
|
+
* Patterns are deterministic - always produce the same example paths.
|
|
6
|
+
*
|
|
7
|
+
* Supported patterns:
|
|
8
|
+
* - React Router: /users/:id
|
|
9
|
+
* - Next.js: /users/[id], /blog/[slug]
|
|
10
|
+
* - Vue Router: /users/:id
|
|
11
|
+
* - Template literals: /blog/${slug}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Detect if a path contains dynamic parameters.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} path - Route path
|
|
18
|
+
* @returns {boolean} - True if path contains dynamic parameters
|
|
19
|
+
*/
|
|
20
|
+
export function isDynamicPath(path) {
|
|
21
|
+
if (!path || typeof path !== 'string') return false;
|
|
22
|
+
// React/Vue Router :param
|
|
23
|
+
if (/:(\w+)/.test(path)) return true;
|
|
24
|
+
// Next.js [param]
|
|
25
|
+
if (/\[\w+\]/.test(path)) return true;
|
|
26
|
+
// Template literal ${param}
|
|
27
|
+
if (/\$\{\w+\}/.test(path)) return true;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Alternative name for isDynamicPath for compatibility.
|
|
33
|
+
*/
|
|
34
|
+
export function isDynamicRoute(path) {
|
|
35
|
+
return isDynamicPath(path);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generate a deterministic example value for a parameter.
|
|
40
|
+
* Always produces the same value for the same parameter name.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} paramName - Parameter name
|
|
43
|
+
* @returns {string} - Example value
|
|
44
|
+
*/
|
|
45
|
+
function getExampleValue(paramName) {
|
|
46
|
+
// Parameter name heuristics (deterministic)
|
|
47
|
+
const lowerName = paramName.toLowerCase();
|
|
48
|
+
|
|
49
|
+
// IDs → numeric
|
|
50
|
+
if (lowerName.includes('id') ||
|
|
51
|
+
lowerName.includes('num') ||
|
|
52
|
+
lowerName.includes('index')) {
|
|
53
|
+
return '1';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Slugs, names, titles, etc → 'example'
|
|
57
|
+
if (lowerName.includes('slug') ||
|
|
58
|
+
lowerName.includes('name') ||
|
|
59
|
+
lowerName.includes('title') ||
|
|
60
|
+
lowerName.includes('username') ||
|
|
61
|
+
lowerName.includes('email') ||
|
|
62
|
+
lowerName.includes('author')) {
|
|
63
|
+
return 'example';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// UUIDs → 'uuid-example'
|
|
67
|
+
if (lowerName.includes('uuid') || lowerName.includes('guid')) {
|
|
68
|
+
return 'uuid-example';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Default: use 'example'
|
|
72
|
+
return 'example';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convert a dynamic route pattern into an executable example path.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} originalPath - Route with dynamic parameters
|
|
79
|
+
* @returns {Object|null} - { examplePath, originalPattern, isDynamic } or null
|
|
80
|
+
*/
|
|
81
|
+
export function createExamplePath(originalPath) {
|
|
82
|
+
if (!originalPath || typeof originalPath !== 'string') {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!isDynamicPath(originalPath)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let examplePath = originalPath;
|
|
91
|
+
let _isDynamic = false;
|
|
92
|
+
const parameters = new Set();
|
|
93
|
+
|
|
94
|
+
// Replace React/Vue :param
|
|
95
|
+
examplePath = examplePath.replace(/:(\w+)/g, (match, paramName) => {
|
|
96
|
+
_isDynamic = true;
|
|
97
|
+
parameters.add(paramName);
|
|
98
|
+
return getExampleValue(paramName);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Replace Next.js [param]
|
|
102
|
+
examplePath = examplePath.replace(/\[(\w+)\]/g, (match, paramName) => {
|
|
103
|
+
_isDynamic = true;
|
|
104
|
+
parameters.add(paramName);
|
|
105
|
+
return getExampleValue(paramName);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Replace template ${param}
|
|
109
|
+
examplePath = examplePath.replace(/\$\{(\w+)\}/g, (match, paramName) => {
|
|
110
|
+
_isDynamic = true;
|
|
111
|
+
parameters.add(paramName);
|
|
112
|
+
return getExampleValue(paramName);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
examplePath,
|
|
117
|
+
originalPattern: originalPath,
|
|
118
|
+
isDynamic: true,
|
|
119
|
+
exampleExecution: true,
|
|
120
|
+
parameters: Array.from(parameters)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Normalize a dynamic route pattern (alias for createExamplePath).
|
|
126
|
+
*
|
|
127
|
+
* @param {string} pattern - Original route pattern
|
|
128
|
+
* @returns {Object|null} - Normalized result
|
|
129
|
+
*/
|
|
130
|
+
export function normalizeDynamicRoute(pattern) {
|
|
131
|
+
return createExamplePath(pattern);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if a target can be safely normalized (is string literal or simple pattern).
|
|
136
|
+
*
|
|
137
|
+
* @param {string} target - Navigation target
|
|
138
|
+
* @returns {boolean} - True if can be normalized safely
|
|
139
|
+
*/
|
|
140
|
+
export function canNormalizeTarget(target) {
|
|
141
|
+
if (!target || typeof target !== 'string') return false;
|
|
142
|
+
|
|
143
|
+
// Pure variable reference like ${path} - can't normalize
|
|
144
|
+
if (target === '${path}' || target === '${id}') {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Normalize a navigation target with potential template literals.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} originalTarget - Original target string
|
|
155
|
+
* @returns {Object} - { exampleTarget, originalTarget, isDynamic, parameters }
|
|
156
|
+
*/
|
|
157
|
+
export function normalizeNavigationTarget(originalTarget) {
|
|
158
|
+
if (!canNormalizeTarget(originalTarget)) {
|
|
159
|
+
return {
|
|
160
|
+
exampleTarget: null,
|
|
161
|
+
originalTarget: null,
|
|
162
|
+
isDynamic: false,
|
|
163
|
+
parameters: []
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check if it has template literals
|
|
168
|
+
const templateRegex = /\$\{(\w+)\}/g;
|
|
169
|
+
const matches = [...originalTarget.matchAll(templateRegex)];
|
|
170
|
+
|
|
171
|
+
if (matches.length === 0) {
|
|
172
|
+
// No template literals - static path
|
|
173
|
+
return {
|
|
174
|
+
exampleTarget: originalTarget,
|
|
175
|
+
originalTarget: null,
|
|
176
|
+
isDynamic: false,
|
|
177
|
+
parameters: []
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Extract parameter names
|
|
182
|
+
const parameters = matches.map(m => m[1]);
|
|
183
|
+
|
|
184
|
+
// Replace ${param} with example values
|
|
185
|
+
let exampleTarget = originalTarget;
|
|
186
|
+
for (const match of matches) {
|
|
187
|
+
const paramName = match[1];
|
|
188
|
+
const exampleValue = getExampleValue(paramName);
|
|
189
|
+
exampleTarget = exampleTarget.replace(match[0], exampleValue);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
exampleTarget,
|
|
194
|
+
originalTarget,
|
|
195
|
+
isDynamic: true,
|
|
196
|
+
parameters
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Normalize a template literal string (for backwards compatibility).
|
|
202
|
+
*
|
|
203
|
+
* @param {string} template - Template literal or string
|
|
204
|
+
* @returns {Object|null} - Result or null
|
|
205
|
+
*/
|
|
206
|
+
export function normalizeTemplateLiteral(template) {
|
|
207
|
+
if (!template || typeof template !== 'string') {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const result = normalizeNavigationTarget(template);
|
|
212
|
+
|
|
213
|
+
if (result.isDynamic) {
|
|
214
|
+
return {
|
|
215
|
+
originalPattern: result.originalTarget,
|
|
216
|
+
examplePath: result.exampleTarget,
|
|
217
|
+
isDynamic: true,
|
|
218
|
+
exampleExecution: true,
|
|
219
|
+
parameters: result.parameters
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 9 — Expectation Coverage Signal
|
|
3
|
+
*
|
|
4
|
+
* Computes and explains expectation coverage.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Calculate expectation coverage
|
|
9
|
+
* @param {Object} context - Coverage context
|
|
10
|
+
* @returns {Object} { discovered: number, exercised: number, coveragePercent: number, explanation: string }
|
|
11
|
+
*/
|
|
12
|
+
export function calculateCoverage(context = {}) {
|
|
13
|
+
const {
|
|
14
|
+
expectationsTotal = 0,
|
|
15
|
+
expectationsUsed = 0
|
|
16
|
+
} = context;
|
|
17
|
+
|
|
18
|
+
const discovered = expectationsTotal;
|
|
19
|
+
const exercised = expectationsUsed;
|
|
20
|
+
|
|
21
|
+
let coveragePercent = 0;
|
|
22
|
+
if (discovered > 0) {
|
|
23
|
+
coveragePercent = Math.round((exercised / discovered) * 100);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let explanation = '';
|
|
27
|
+
if (discovered === 0) {
|
|
28
|
+
explanation = 'No expectations found in code';
|
|
29
|
+
} else if (coveragePercent === 100) {
|
|
30
|
+
explanation = 'All expectations were exercised';
|
|
31
|
+
} else if (coveragePercent >= 50) {
|
|
32
|
+
explanation = `${discovered - exercised} expectation(s) not exercised during scan`;
|
|
33
|
+
} else {
|
|
34
|
+
explanation = `Most expectations (${discovered - exercised}) were not exercised`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
discovered,
|
|
39
|
+
exercised,
|
|
40
|
+
coveragePercent,
|
|
41
|
+
explanation
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NO-GUESSING GUARANTEE
|
|
3
|
+
*
|
|
4
|
+
* VERAX only generates findings from PROVEN expectations.
|
|
5
|
+
* Unproven expectations become coverage gaps, not failures.
|
|
6
|
+
*
|
|
7
|
+
* This module provides the single source of truth for "what is proven?"
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if an expectation is PROVEN.
|
|
12
|
+
*
|
|
13
|
+
* ABSOLUTE RULES:
|
|
14
|
+
* - Returns true ONLY if the expectation has definitive code evidence
|
|
15
|
+
* - Returns false for heuristic/label-matched/guessed expectations
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} expectation - Expectation object to check
|
|
18
|
+
* @returns {boolean} - true if PROVEN, false otherwise
|
|
19
|
+
*/
|
|
20
|
+
export function isProvenExpectation(expectation) {
|
|
21
|
+
if (!expectation) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Rule 1: Explicit proof marker
|
|
26
|
+
if (expectation.proof === 'PROVEN_EXPECTATION') {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Rule 2: Has sourceRef with file + line (code location)
|
|
31
|
+
if (expectation.sourceRef) {
|
|
32
|
+
const ref = expectation.sourceRef;
|
|
33
|
+
// Must include file path AND line number (format: "file.js:123")
|
|
34
|
+
if (typeof ref === 'string' && ref.includes(':')) {
|
|
35
|
+
const parts = ref.split(':');
|
|
36
|
+
if (parts.length >= 2 && parts[1].match(/^\d+$/)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Rule 3: Static HTML expectations with evidence.source (file path)
|
|
43
|
+
if (expectation.evidence && expectation.evidence.source) {
|
|
44
|
+
// Static HTML parsing provides file path as evidence
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Rule 4: Explicit proven flag
|
|
49
|
+
if (expectation.proven === true) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Everything else is unproven
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a coverage gap entry for an unproven expectation.
|
|
59
|
+
*
|
|
60
|
+
* @param {Object} expectation - The unproven expectation
|
|
61
|
+
* @param {Object} interaction - The interaction that triggered this gap
|
|
62
|
+
* @returns {Object} - Coverage gap entry
|
|
63
|
+
*/
|
|
64
|
+
export function createCoverageGap(expectation, interaction) {
|
|
65
|
+
return {
|
|
66
|
+
expectationId: expectation.id || null,
|
|
67
|
+
type: expectation.type || 'unknown',
|
|
68
|
+
reason: 'UNPROVEN_EXPECTATION',
|
|
69
|
+
source: expectation.sourceRef || expectation.source?.file || expectation.evidence?.source || null,
|
|
70
|
+
interaction: {
|
|
71
|
+
type: interaction.type,
|
|
72
|
+
selector: interaction.selector,
|
|
73
|
+
label: interaction.label || null
|
|
74
|
+
},
|
|
75
|
+
metadata: {
|
|
76
|
+
proof: expectation.proof || 'none',
|
|
77
|
+
expectationType: expectation.type,
|
|
78
|
+
targetPath: expectation.targetPath || null
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|