@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,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 { DEFAULT_SCAN_BUDGET, 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 {ScanBudget} 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,170 @@
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, dirname, join } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ /**
12
+ * Default config values
13
+ */
14
+ const DEFAULT_CONFIG = {
15
+ defaultUrl: 'http://localhost:3000',
16
+ projectRoot: '.',
17
+ outDir: '.verax/runs',
18
+ ciDefaults: {
19
+ json: true,
20
+ zip: true,
21
+ explain: false
22
+ },
23
+ safety: {
24
+ allowlistDomains: [],
25
+ denyKeywords: ['delete', 'remove', 'billing', 'payment']
26
+ }
27
+ };
28
+
29
+ /**
30
+ * Validate config structure
31
+ * @param {Object} config - Config object to validate
32
+ * @returns {Object} { valid: boolean, errors: string[] }
33
+ */
34
+ export function validateConfig(config) {
35
+ const errors = [];
36
+
37
+ if (typeof config !== 'object' || config === null) {
38
+ errors.push('Config must be an object');
39
+ return { valid: false, errors };
40
+ }
41
+
42
+ // Validate defaultUrl
43
+ if (config.defaultUrl !== undefined) {
44
+ if (typeof config.defaultUrl !== 'string' || config.defaultUrl.trim() === '') {
45
+ errors.push('defaultUrl must be a non-empty string');
46
+ } else {
47
+ try {
48
+ new URL(config.defaultUrl);
49
+ } catch (e) {
50
+ errors.push(`defaultUrl is not a valid URL: ${config.defaultUrl}`);
51
+ }
52
+ }
53
+ }
54
+
55
+ // Validate projectRoot
56
+ if (config.projectRoot !== undefined && typeof config.projectRoot !== 'string') {
57
+ errors.push('projectRoot must be a string');
58
+ }
59
+
60
+ // Validate outDir
61
+ if (config.outDir !== undefined && typeof config.outDir !== 'string') {
62
+ errors.push('outDir must be a string');
63
+ }
64
+
65
+ // Validate ciDefaults
66
+ if (config.ciDefaults !== undefined) {
67
+ if (typeof config.ciDefaults !== 'object' || config.ciDefaults === null) {
68
+ errors.push('ciDefaults must be an object');
69
+ } else {
70
+ if (config.ciDefaults.json !== undefined && typeof config.ciDefaults.json !== 'boolean') {
71
+ errors.push('ciDefaults.json must be a boolean');
72
+ }
73
+ if (config.ciDefaults.zip !== undefined && typeof config.ciDefaults.zip !== 'boolean') {
74
+ errors.push('ciDefaults.zip must be a boolean');
75
+ }
76
+ if (config.ciDefaults.explain !== undefined && typeof config.ciDefaults.explain !== 'boolean') {
77
+ errors.push('ciDefaults.explain must be a boolean');
78
+ }
79
+ }
80
+ }
81
+
82
+ // Validate safety
83
+ if (config.safety !== undefined) {
84
+ if (typeof config.safety !== 'object' || config.safety === null) {
85
+ errors.push('safety must be an object');
86
+ } else {
87
+ if (config.safety.allowlistDomains !== undefined) {
88
+ if (!Array.isArray(config.safety.allowlistDomains)) {
89
+ errors.push('safety.allowlistDomains must be an array');
90
+ } else {
91
+ for (const domain of config.safety.allowlistDomains) {
92
+ if (typeof domain !== 'string') {
93
+ errors.push('safety.allowlistDomains must contain only strings');
94
+ break;
95
+ }
96
+ }
97
+ }
98
+ }
99
+ if (config.safety.denyKeywords !== undefined) {
100
+ if (!Array.isArray(config.safety.denyKeywords)) {
101
+ errors.push('safety.denyKeywords must be an array');
102
+ } else {
103
+ for (const keyword of config.safety.denyKeywords) {
104
+ if (typeof keyword !== 'string') {
105
+ errors.push('safety.denyKeywords must contain only strings');
106
+ break;
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ return {
115
+ valid: errors.length === 0,
116
+ errors
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Load config file from project directory
122
+ * @param {string} projectRoot - Project root directory
123
+ * @returns {Object|null} Config object or null if not found
124
+ * @throws {Error} If config file exists but is invalid
125
+ */
126
+ export function loadConfig(projectRoot) {
127
+ const configPath = resolve(projectRoot, '.verax', 'config.json');
128
+
129
+ if (!existsSync(configPath)) {
130
+ return null;
131
+ }
132
+
133
+ try {
134
+ const content = readFileSync(configPath, 'utf-8');
135
+ const config = JSON.parse(content);
136
+
137
+ const validation = validateConfig(config);
138
+ if (!validation.valid) {
139
+ throw new Error(`Invalid config file: ${validation.errors.join(', ')}`);
140
+ }
141
+
142
+ // Merge with defaults
143
+ return {
144
+ ...DEFAULT_CONFIG,
145
+ ...config,
146
+ ciDefaults: {
147
+ ...DEFAULT_CONFIG.ciDefaults,
148
+ ...(config.ciDefaults || {})
149
+ },
150
+ safety: {
151
+ ...DEFAULT_CONFIG.safety,
152
+ ...(config.safety || {})
153
+ }
154
+ };
155
+ } catch (error) {
156
+ if (error instanceof SyntaxError) {
157
+ throw new Error(`Config file is not valid JSON: ${error.message}`);
158
+ }
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Get default config (for init)
165
+ * @returns {Object} Default config object
166
+ */
167
+ export function getDefaultConfig() {
168
+ return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
169
+ }
170
+
@@ -0,0 +1,218 @@
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
+
93
+ // Replace React/Vue :param
94
+ examplePath = examplePath.replace(/:(\w+)/g, (match, paramName) => {
95
+ isDynamic = true;
96
+ return getExampleValue(paramName);
97
+ });
98
+
99
+ // Replace Next.js [param]
100
+ examplePath = examplePath.replace(/\[(\w+)\]/g, (match, paramName) => {
101
+ isDynamic = true;
102
+ return getExampleValue(paramName);
103
+ });
104
+
105
+ // Replace template ${param}
106
+ examplePath = examplePath.replace(/\$\{(\w+)\}/g, (match, paramName) => {
107
+ isDynamic = true;
108
+ return getExampleValue(paramName);
109
+ });
110
+
111
+ return {
112
+ examplePath,
113
+ originalPattern: originalPath,
114
+ isDynamic: true,
115
+ exampleExecution: true
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Normalize a dynamic route pattern (alias for createExamplePath).
121
+ *
122
+ * @param {string} pattern - Original route pattern
123
+ * @returns {Object|null} - Normalized result
124
+ */
125
+ export function normalizeDynamicRoute(pattern) {
126
+ return createExamplePath(pattern);
127
+ }
128
+
129
+ /**
130
+ * Check if a target can be safely normalized (is string literal or simple pattern).
131
+ *
132
+ * @param {string} target - Navigation target
133
+ * @returns {boolean} - True if can be normalized safely
134
+ */
135
+ export function canNormalizeTarget(target) {
136
+ if (!target || typeof target !== 'string') return false;
137
+
138
+ // Pure variable reference like ${path} - can't normalize
139
+ if (target === '${path}' || target === '${id}') {
140
+ return false;
141
+ }
142
+
143
+ return true;
144
+ }
145
+
146
+ /**
147
+ * Normalize a navigation target with potential template literals.
148
+ *
149
+ * @param {string} originalTarget - Original target string
150
+ * @returns {Object} - { exampleTarget, originalTarget, isDynamic, parameters }
151
+ */
152
+ export function normalizeNavigationTarget(originalTarget) {
153
+ if (!canNormalizeTarget(originalTarget)) {
154
+ return {
155
+ exampleTarget: null,
156
+ originalTarget: null,
157
+ isDynamic: false,
158
+ parameters: []
159
+ };
160
+ }
161
+
162
+ // Check if it has template literals
163
+ const templateRegex = /\$\{(\w+)\}/g;
164
+ const matches = [...originalTarget.matchAll(templateRegex)];
165
+
166
+ if (matches.length === 0) {
167
+ // No template literals - static path
168
+ return {
169
+ exampleTarget: originalTarget,
170
+ originalTarget: null,
171
+ isDynamic: false,
172
+ parameters: []
173
+ };
174
+ }
175
+
176
+ // Extract parameter names
177
+ const parameters = matches.map(m => m[1]);
178
+
179
+ // Replace ${param} with example values
180
+ let exampleTarget = originalTarget;
181
+ for (const match of matches) {
182
+ const paramName = match[1];
183
+ const exampleValue = getExampleValue(paramName);
184
+ exampleTarget = exampleTarget.replace(match[0], exampleValue);
185
+ }
186
+
187
+ return {
188
+ exampleTarget,
189
+ originalTarget,
190
+ isDynamic: true,
191
+ parameters
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Normalize a template literal string (for backwards compatibility).
197
+ *
198
+ * @param {string} template - Template literal or string
199
+ * @returns {Object|null} - Result or null
200
+ */
201
+ export function normalizeTemplateLiteral(template) {
202
+ if (!template || typeof template !== 'string') {
203
+ return null;
204
+ }
205
+
206
+ const result = normalizeNavigationTarget(template);
207
+
208
+ if (result.isDynamic) {
209
+ return {
210
+ originalPattern: result.originalTarget,
211
+ examplePath: result.exampleTarget,
212
+ isDynamic: true,
213
+ exampleExecution: true
214
+ };
215
+ }
216
+
217
+ return null;
218
+ }
@@ -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
+ }