@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.
Files changed (135) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +24 -36
  4. package/src/cli/commands/default.js +681 -0
  5. package/src/cli/commands/doctor.js +197 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +586 -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 +297 -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 +110 -0
  14. package/src/cli/util/expectation-extractor.js +388 -0
  15. package/src/cli/util/findings-writer.js +32 -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 +412 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +30 -0
  21. package/src/cli/util/project-discovery.js +297 -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/runtime-budget.js +147 -0
  26. package/src/cli/util/summary-writer.js +43 -0
  27. package/src/types/global.d.ts +28 -0
  28. package/src/types/ts-ast.d.ts +24 -0
  29. package/src/verax/cli/ci-summary.js +35 -0
  30. package/src/verax/cli/context-explanation.js +89 -0
  31. package/src/verax/cli/doctor.js +277 -0
  32. package/src/verax/cli/error-normalizer.js +154 -0
  33. package/src/verax/cli/explain-output.js +105 -0
  34. package/src/verax/cli/finding-explainer.js +130 -0
  35. package/src/verax/cli/init.js +237 -0
  36. package/src/verax/cli/run-overview.js +163 -0
  37. package/src/verax/cli/url-safety.js +111 -0
  38. package/src/verax/cli/wizard.js +109 -0
  39. package/src/verax/cli/zero-findings-explainer.js +57 -0
  40. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  41. package/src/verax/core/action-classifier.js +86 -0
  42. package/src/verax/core/budget-engine.js +218 -0
  43. package/src/verax/core/canonical-outcomes.js +157 -0
  44. package/src/verax/core/decision-snapshot.js +335 -0
  45. package/src/verax/core/determinism-model.js +432 -0
  46. package/src/verax/core/incremental-store.js +245 -0
  47. package/src/verax/core/invariants.js +356 -0
  48. package/src/verax/core/promise-model.js +230 -0
  49. package/src/verax/core/replay-validator.js +350 -0
  50. package/src/verax/core/replay.js +222 -0
  51. package/src/verax/core/run-id.js +175 -0
  52. package/src/verax/core/run-manifest.js +99 -0
  53. package/src/verax/core/silence-impact.js +369 -0
  54. package/src/verax/core/silence-model.js +523 -0
  55. package/src/verax/detect/comparison.js +7 -34
  56. package/src/verax/detect/confidence-engine.js +764 -329
  57. package/src/verax/detect/detection-engine.js +293 -0
  58. package/src/verax/detect/evidence-index.js +127 -0
  59. package/src/verax/detect/expectation-model.js +241 -168
  60. package/src/verax/detect/explanation-helpers.js +187 -0
  61. package/src/verax/detect/finding-detector.js +450 -0
  62. package/src/verax/detect/findings-writer.js +41 -12
  63. package/src/verax/detect/flow-detector.js +366 -0
  64. package/src/verax/detect/index.js +200 -288
  65. package/src/verax/detect/interactive-findings.js +612 -0
  66. package/src/verax/detect/signal-mapper.js +308 -0
  67. package/src/verax/detect/skip-classifier.js +4 -4
  68. package/src/verax/detect/verdict-engine.js +561 -0
  69. package/src/verax/evidence-index-writer.js +61 -0
  70. package/src/verax/flow/flow-engine.js +3 -2
  71. package/src/verax/flow/flow-spec.js +1 -2
  72. package/src/verax/index.js +103 -15
  73. package/src/verax/intel/effect-detector.js +368 -0
  74. package/src/verax/intel/handler-mapper.js +249 -0
  75. package/src/verax/intel/index.js +281 -0
  76. package/src/verax/intel/route-extractor.js +280 -0
  77. package/src/verax/intel/ts-program.js +256 -0
  78. package/src/verax/intel/vue-navigation-extractor.js +642 -0
  79. package/src/verax/intel/vue-router-extractor.js +325 -0
  80. package/src/verax/learn/action-contract-extractor.js +338 -104
  81. package/src/verax/learn/ast-contract-extractor.js +148 -6
  82. package/src/verax/learn/flow-extractor.js +172 -0
  83. package/src/verax/learn/index.js +36 -2
  84. package/src/verax/learn/manifest-writer.js +122 -58
  85. package/src/verax/learn/project-detector.js +40 -0
  86. package/src/verax/learn/route-extractor.js +28 -97
  87. package/src/verax/learn/route-validator.js +8 -7
  88. package/src/verax/learn/state-extractor.js +212 -0
  89. package/src/verax/learn/static-extractor-navigation.js +114 -0
  90. package/src/verax/learn/static-extractor-validation.js +88 -0
  91. package/src/verax/learn/static-extractor.js +119 -10
  92. package/src/verax/learn/truth-assessor.js +24 -21
  93. package/src/verax/learn/ts-contract-resolver.js +14 -12
  94. package/src/verax/observe/aria-sensor.js +211 -0
  95. package/src/verax/observe/browser.js +30 -6
  96. package/src/verax/observe/console-sensor.js +2 -18
  97. package/src/verax/observe/domain-boundary.js +10 -1
  98. package/src/verax/observe/expectation-executor.js +513 -0
  99. package/src/verax/observe/flow-matcher.js +143 -0
  100. package/src/verax/observe/focus-sensor.js +196 -0
  101. package/src/verax/observe/human-driver.js +660 -273
  102. package/src/verax/observe/index.js +910 -26
  103. package/src/verax/observe/interaction-discovery.js +378 -15
  104. package/src/verax/observe/interaction-runner.js +562 -197
  105. package/src/verax/observe/loading-sensor.js +145 -0
  106. package/src/verax/observe/navigation-sensor.js +255 -0
  107. package/src/verax/observe/network-sensor.js +55 -7
  108. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  109. package/src/verax/observe/observed-expectation.js +305 -0
  110. package/src/verax/observe/page-frontier.js +234 -0
  111. package/src/verax/observe/settle.js +38 -17
  112. package/src/verax/observe/state-sensor.js +393 -0
  113. package/src/verax/observe/state-ui-sensor.js +7 -1
  114. package/src/verax/observe/timing-sensor.js +228 -0
  115. package/src/verax/observe/traces-writer.js +73 -21
  116. package/src/verax/observe/ui-signal-sensor.js +143 -17
  117. package/src/verax/scan-summary-writer.js +80 -15
  118. package/src/verax/shared/artifact-manager.js +111 -9
  119. package/src/verax/shared/budget-profiles.js +136 -0
  120. package/src/verax/shared/caching.js +1 -1
  121. package/src/verax/shared/ci-detection.js +39 -0
  122. package/src/verax/shared/config-loader.js +169 -0
  123. package/src/verax/shared/dynamic-route-utils.js +224 -0
  124. package/src/verax/shared/expectation-coverage.js +44 -0
  125. package/src/verax/shared/expectation-prover.js +81 -0
  126. package/src/verax/shared/expectation-tracker.js +201 -0
  127. package/src/verax/shared/expectations-writer.js +60 -0
  128. package/src/verax/shared/first-run.js +44 -0
  129. package/src/verax/shared/progress-reporter.js +171 -0
  130. package/src/verax/shared/retry-policy.js +9 -1
  131. package/src/verax/shared/root-artifacts.js +49 -0
  132. package/src/verax/shared/scan-budget.js +86 -0
  133. package/src/verax/shared/url-normalizer.js +162 -0
  134. package/src/verax/shared/zip-artifacts.js +66 -0
  135. package/src/verax/validate/context-validator.js +244 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Wave 6 — Finding Explanation
3
+ *
4
+ * Formats findings in a clear, human-readable way showing the chain:
5
+ * Expectation → Observation → Mismatch → Why this is a silent failure
6
+ */
7
+
8
+ /**
9
+ * Format a single finding for console output
10
+ * @param {Object} finding - Finding object
11
+ * @param {Object} expectation - Related expectation (if available)
12
+ * @returns {string} Formatted finding text
13
+ */
14
+ export function formatFinding(finding, expectation = null) {
15
+ const lines = [];
16
+
17
+ // Finding type and confidence
18
+ const confidenceLevel = finding.confidence?.level || 'UNKNOWN';
19
+ const confidenceScore = finding.confidence?.score || 0;
20
+ lines.push(` [${confidenceLevel} (${confidenceScore}%)] ${finding.type || 'unknown'}`);
21
+
22
+ // Expectation (what was promised)
23
+ if (expectation) {
24
+ let expectationDesc = '';
25
+ if (expectation.type === 'navigation') {
26
+ const target = expectation.raw?.targetPath || expectation.targetPath || 'route';
27
+ expectationDesc = `Expected navigation to ${target}`;
28
+ } else if (expectation.type === 'network_action') {
29
+ const method = expectation.raw?.method || 'request';
30
+ const url = expectation.raw?.urlPath || expectation.urlPath || 'endpoint';
31
+ expectationDesc = `Expected ${method} request to ${url}`;
32
+ } else if (expectation.type === 'state_action') {
33
+ expectationDesc = `Expected state mutation`;
34
+ } else {
35
+ expectationDesc = `Expected ${expectation.type}`;
36
+ }
37
+ lines.push(` └─ Expectation: ${expectationDesc}`);
38
+ } else if (finding.expectationId) {
39
+ lines.push(` └─ Expectation: Referenced expectation ${finding.expectationId}`);
40
+ }
41
+
42
+ // Observation (what actually happened)
43
+ const interaction = finding.interaction || {};
44
+ if (interaction.type) {
45
+ let interactionDesc = `${interaction.type}`;
46
+ if (interaction.selector) {
47
+ interactionDesc += ` on "${interaction.label || interaction.selector}"`;
48
+ }
49
+ lines.push(` └─ Observation: User interacted (${interactionDesc})`);
50
+ }
51
+
52
+ // Mismatch (what went wrong)
53
+ const evidence = finding.evidence || {};
54
+ if (finding.type === 'silent_failure' || finding.type === 'navigation_silent_failure') {
55
+ if (!evidence.hasUrlChange && !evidence.hasVisibleChange) {
56
+ lines.push(` └─ Mismatch: No navigation occurred, no visible change`);
57
+ } else if (evidence.hasUrlChange && !evidence.hasVisibleChange) {
58
+ lines.push(` └─ Mismatch: URL changed but no visible feedback`);
59
+ } else {
60
+ lines.push(` └─ Mismatch: Expected navigation did not occur`);
61
+ }
62
+ } else if (finding.type === 'network_silent_failure') {
63
+ if (evidence.slowRequests && evidence.slowRequests > 0) {
64
+ lines.push(` └─ Mismatch: Request was slow (${evidence.slowRequests} slow request(s))`);
65
+ } else {
66
+ lines.push(` └─ Mismatch: Request failed or returned error with no user feedback`);
67
+ }
68
+ } else if (finding.type === 'missing_network_action') {
69
+ lines.push(` └─ Mismatch: Expected network request never fired`);
70
+ } else if (finding.type === 'missing_state_action') {
71
+ lines.push(` └─ Mismatch: Expected state change did not occur`);
72
+ }
73
+
74
+ // Why this is a silent failure
75
+ if (finding.reason) {
76
+ lines.push(` └─ Silent Failure: ${finding.reason}`);
77
+ } else {
78
+ lines.push(` └─ Silent Failure: User receives no feedback when expected behavior fails`);
79
+ }
80
+
81
+ // Source location if available
82
+ if (expectation?.source?.file) {
83
+ const sourceLine = expectation.source.line ? `:${expectation.source.line}` : '';
84
+ lines.push(` └─ Source: ${expectation.source.file}${sourceLine}`);
85
+ }
86
+
87
+ return lines.join('\n');
88
+ }
89
+
90
+ /**
91
+ * Print top findings with explanations
92
+ * @param {Array} findings - Array of finding objects
93
+ * @param {Array} expectations - Array of expectation objects (for lookup)
94
+ * @param {number} limit - Maximum number of findings to print
95
+ */
96
+ export function printFindings(findings, expectations = [], limit = 5) {
97
+ if (!findings || findings.length === 0) {
98
+ return;
99
+ }
100
+
101
+ console.error('\n' + '─'.repeat(60));
102
+ console.error(`Top Findings (${Math.min(findings.length, limit)} of ${findings.length})`);
103
+ console.error('─'.repeat(60));
104
+
105
+ const topFindings = findings.slice(0, limit);
106
+
107
+ // Create expectation lookup map
108
+ const expectationMap = new Map();
109
+ for (const exp of expectations) {
110
+ if (exp.id) {
111
+ expectationMap.set(exp.id, exp);
112
+ }
113
+ }
114
+
115
+ topFindings.forEach((finding, index) => {
116
+ const expectation = finding.expectationId
117
+ ? expectationMap.get(finding.expectationId)
118
+ : null;
119
+
120
+ console.error(`\n${index + 1}.`);
121
+ console.error(formatFinding(finding, expectation));
122
+ });
123
+
124
+ if (findings.length > limit) {
125
+ console.error(`\n... and ${findings.length - limit} more (see findings.json)`);
126
+ }
127
+
128
+ console.error('─'.repeat(60) + '\n');
129
+ }
130
+
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Wave 7 — VERAX Init
3
+ *
4
+ * Initializes VERAX configuration and templates.
5
+ */
6
+
7
+ import { existsSync, writeFileSync, mkdirSync } from 'fs';
8
+ import { resolve } from 'path';
9
+ import { getDefaultConfig } from '../shared/config-loader.js';
10
+
11
+ /**
12
+ * Initialize VERAX configuration
13
+ * @param {Object} options - { projectRoot, yes, ciTemplate, flowTemplate }
14
+ * @returns {Promise<Object>} { created: string[], skipped: string[] }
15
+ */
16
+ export async function runInit(options = {}) {
17
+ const {
18
+ projectRoot = process.cwd(),
19
+ yes = false,
20
+ ciTemplate = null,
21
+ flowTemplate = null
22
+ } = options;
23
+
24
+ const created = [];
25
+ const skipped = [];
26
+
27
+ // Create .verax directory if needed
28
+ const veraxDir = resolve(projectRoot, '.verax');
29
+ mkdirSync(veraxDir, { recursive: true });
30
+
31
+ // Create config.json
32
+ const configPath = resolve(veraxDir, 'config.json');
33
+ if (existsSync(configPath) && !yes) {
34
+ skipped.push('config.json');
35
+ } else {
36
+ const defaultConfig = getDefaultConfig();
37
+ writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n');
38
+ created.push('config.json');
39
+ }
40
+
41
+ // Create CI template if requested
42
+ if (ciTemplate === 'github') {
43
+ const workflowsDir = resolve(projectRoot, '.github', 'workflows');
44
+ mkdirSync(workflowsDir, { recursive: true });
45
+
46
+ const workflowFile = resolve(workflowsDir, 'verax-ci.yml');
47
+ if (existsSync(workflowFile) && !yes) {
48
+ skipped.push('.github/workflows/verax-ci.yml');
49
+ } else {
50
+ const workflowContent = `name: VERAX CI
51
+
52
+ on:
53
+ workflow_dispatch:
54
+ pull_request:
55
+
56
+ jobs:
57
+ verax-scan:
58
+ runs-on: ubuntu-latest
59
+ steps:
60
+ - name: Checkout
61
+ uses: actions/checkout@v4
62
+
63
+ - name: Setup Node.js
64
+ uses: actions/setup-node@v4
65
+ with:
66
+ node-version: '20'
67
+ cache: 'npm'
68
+
69
+ - name: Install dependencies
70
+ run: npm ci
71
+
72
+ - name: Install Playwright browsers
73
+ run: npx playwright install --with-deps chromium
74
+
75
+ - name: Start fixture server
76
+ id: fixture-server
77
+ run: |
78
+ node test/helpers/fixture-server.js &
79
+ SERVER_PID=$!
80
+ echo "SERVER_PID=$SERVER_PID" >> $GITHUB_ENV
81
+ sleep 3
82
+ echo "url=http://127.0.0.1:8888" >> $GITHUB_OUTPUT
83
+ working-directory: \${{ github.workspace }}
84
+
85
+ - name: Run VERAX CI scan
86
+ id: verax
87
+ run: |
88
+ npx @veraxhq/verax ci --url \${{ steps.fixture-server.outputs.url }} --projectRoot .
89
+ continue-on-error: true
90
+
91
+ - name: Stop fixture server
92
+ if: always()
93
+ run: |
94
+ if [ ! -z "$SERVER_PID" ]; then
95
+ kill $SERVER_PID 2>/dev/null || true
96
+ fi
97
+
98
+ - name: Upload VERAX artifacts
99
+ if: always()
100
+ uses: actions/upload-artifact@v4
101
+ with:
102
+ name: verax-artifacts
103
+ path: |
104
+ .verax/runs/**/*
105
+ .verax/verax-run-*.zip
106
+ retention-days: 7
107
+ if-no-files-found: ignore
108
+
109
+ - name: Check VERAX exit code and fail gate
110
+ if: always()
111
+ run: |
112
+ EXIT_CODE=\${{ steps.verax.exitcode }}
113
+ if [ "$EXIT_CODE" == "" ]; then
114
+ EXIT_CODE=\${{ steps.verax.outcome == 'success' && 0 || 1 }}
115
+ fi
116
+
117
+ if [ "$EXIT_CODE" == "0" ]; then
118
+ echo "✅ VERAX: VERIFIED - Scan passed"
119
+ elif [ "$EXIT_CODE" == "1" ]; then
120
+ echo "⚠️ VERAX: NO_EXPECTATIONS_FOUND or MEDIUM/LOW findings"
121
+ echo "Gate passes (non-blocking)"
122
+ elif [ "$EXIT_CODE" == "2" ]; then
123
+ echo "❌ VERAX: HIGH severity findings detected"
124
+ echo "Gate fails (blocking)"
125
+ exit 2
126
+ elif [ "$EXIT_CODE" == "4" ]; then
127
+ echo "❌ VERAX: INVALID_CONTEXT - URL does not match project"
128
+ echo "Gate fails (blocking)"
129
+ exit 4
130
+ elif [ "$EXIT_CODE" == "3" ]; then
131
+ echo "❌ VERAX: FATAL error"
132
+ echo "Gate fails (blocking)"
133
+ exit 3
134
+ else
135
+ echo "❌ VERAX: Unexpected exit code $EXIT_CODE"
136
+ exit $EXIT_CODE
137
+ fi
138
+ `;
139
+ writeFileSync(workflowFile, workflowContent);
140
+ created.push('.github/workflows/verax-ci.yml');
141
+ }
142
+ }
143
+
144
+ // Create flow template if requested
145
+ if (flowTemplate === 'login') {
146
+ const flowsDir = resolve(projectRoot, 'flows');
147
+ mkdirSync(flowsDir, { recursive: true });
148
+
149
+ const flowFile = resolve(flowsDir, 'login.json');
150
+ if (existsSync(flowFile) && !yes) {
151
+ skipped.push('flows/login.json');
152
+ } else {
153
+ const flowContent = {
154
+ name: 'login',
155
+ description: 'User login flow',
156
+ steps: [
157
+ {
158
+ type: 'goto',
159
+ url: '${VERAX_BASE_URL}/login'
160
+ },
161
+ {
162
+ type: 'fill',
163
+ selector: 'input[name="email"]',
164
+ value: '${VERAX_TEST_EMAIL}'
165
+ },
166
+ {
167
+ type: 'fill',
168
+ selector: 'input[name="password"]',
169
+ value: '${VERAX_TEST_PASSWORD}',
170
+ isSecret: true
171
+ },
172
+ {
173
+ type: 'click',
174
+ selector: 'button[type="submit"]'
175
+ },
176
+ {
177
+ type: 'wait',
178
+ selector: '[data-testid="dashboard"]',
179
+ timeout: 5000
180
+ }
181
+ ]
182
+ };
183
+ writeFileSync(flowFile, JSON.stringify(flowContent, null, 2) + '\n');
184
+ created.push('flows/login.json');
185
+ }
186
+ }
187
+
188
+ return { created, skipped };
189
+ }
190
+
191
+ /**
192
+ * Print init results
193
+ * @param {Object} results - Init results
194
+ */
195
+ export function printInitResults(results) {
196
+ console.error('\n' + '═'.repeat(60));
197
+ console.error('VERAX Init');
198
+ console.error('═'.repeat(60));
199
+
200
+ if (results.created.length > 0) {
201
+ console.error('\n✅ Created:');
202
+ results.created.forEach(file => {
203
+ console.error(` • ${file}`);
204
+ });
205
+ }
206
+
207
+ if (results.skipped.length > 0) {
208
+ console.error('\n⏭️ Skipped (already exist):');
209
+ results.skipped.forEach(file => {
210
+ console.error(` • ${file}`);
211
+ });
212
+ }
213
+
214
+ if (results.created.includes('config.json')) {
215
+ console.error('\n📝 Next Steps:');
216
+ console.error(' 1. Review .verax/config.json and adjust settings');
217
+ console.error(' 2. Run: verax doctor (to verify setup)');
218
+ console.error(' 3. Run: verax run --url <your-url>');
219
+ }
220
+
221
+ if (results.created.includes('.github/workflows/verax-ci.yml')) {
222
+ console.error('\n🔧 CI Setup:');
223
+ console.error(' • Review .github/workflows/verax-ci.yml');
224
+ console.error(' • Update URL placeholder with your deployment URL');
225
+ console.error(' • Commit and push to enable VERAX in CI');
226
+ }
227
+
228
+ if (results.created.includes('flows/login.json')) {
229
+ console.error('\n🔐 Flow Template:');
230
+ console.error(' • Review flows/login.json');
231
+ console.error(' • Set VERAX_TEST_EMAIL and VERAX_TEST_PASSWORD environment variables');
232
+ console.error(' • Note: Flow commands are not available in this version');
233
+ }
234
+
235
+ console.error('═'.repeat(60) + '\n');
236
+ }
237
+
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Wave 6 — Run Overview
3
+ *
4
+ * Generates a clear, human-readable overview of the scan run.
5
+ */
6
+
7
+ /**
8
+ * Generate run overview data structure
9
+ * @param {Object} context - Scan context
10
+ * @returns {Object} Overview data
11
+ */
12
+ export function generateRunOverview(context = {}) {
13
+ const {
14
+ manifest = null,
15
+ expectationsSummary = { total: 0, navigation: 0, networkActions: 0, stateActions: 0 },
16
+ interactionsObserved = 0,
17
+ contextCheck = null,
18
+ verdict = 'UNKNOWN',
19
+ findingsCount = 0
20
+ } = context;
21
+
22
+ const projectType = manifest?.projectType || 'unknown';
23
+ const expectationsTotal = expectationsSummary.total || 0;
24
+
25
+ // Determine validation status
26
+ let validationStatus = 'skipped';
27
+ let validationReason = 'No routes to validate';
28
+
29
+ if (contextCheck && contextCheck.ran) {
30
+ if (contextCheck.verdict === 'VALID_CONTEXT') {
31
+ validationStatus = 'validated';
32
+ validationReason = `${contextCheck.matchedRoutesCount} routes matched`;
33
+ } else if (contextCheck.verdict === 'INVALID_CONTEXT') {
34
+ validationStatus = 'mismatch';
35
+ validationReason = 'URL does not match project';
36
+ } else if (contextCheck.verdict === 'INVALID_CONTEXT_FORCED') {
37
+ validationStatus = 'forced';
38
+ validationReason = 'Scan continued with --force despite mismatch';
39
+ }
40
+ }
41
+
42
+ // Generate trust statement
43
+ let trustLevel = 'low';
44
+ const trustReasons = [];
45
+
46
+ if (verdict === 'VERIFIED') {
47
+ trustLevel = 'high';
48
+ trustReasons.push(`All ${expectationsTotal} expectations validated`);
49
+ trustReasons.push(`${interactionsObserved} interactions tested`);
50
+ if (validationStatus === 'validated') {
51
+ trustReasons.push('Context validated');
52
+ }
53
+ } else if (verdict === 'ISSUES_FOUND') {
54
+ trustLevel = 'high';
55
+ trustReasons.push(`${expectationsTotal} expectations analyzed`);
56
+ trustReasons.push(`${interactionsObserved} interactions tested`);
57
+ trustReasons.push(`${findingsCount} issue(s) detected`);
58
+ } else if (verdict === 'NO_EXPECTATIONS_FOUND') {
59
+ trustLevel = 'low';
60
+ trustReasons.push('No code-derived expectations found');
61
+ trustReasons.push('Cannot validate without expectations');
62
+ } else if (verdict === 'INVALID_CONTEXT' || verdict === 'INVALID_CONTEXT_FORCED') {
63
+ trustLevel = 'partial';
64
+ trustReasons.push(`${expectationsTotal} expectations found`);
65
+ if (validationStatus === 'forced') {
66
+ trustReasons.push('Context mismatch (forced scan)');
67
+ } else {
68
+ trustReasons.push('Context mismatch - scan stopped early');
69
+ }
70
+ if (interactionsObserved === 0) {
71
+ trustReasons.push('No interactions executed');
72
+ }
73
+ }
74
+
75
+ // Adjust trust level based on interactions
76
+ if (expectationsTotal > 0 && interactionsObserved === 0 && verdict !== 'INVALID_CONTEXT') {
77
+ trustLevel = 'partial';
78
+ if (!trustReasons.some(r => r.includes('No interactions'))) {
79
+ trustReasons.push('No interactions executed - validation incomplete');
80
+ }
81
+ }
82
+
83
+ // Explicitly mention skipped expectations
84
+ if (context?.expectationsUnused !== undefined && context.expectationsUnused > 0) {
85
+ if (trustLevel === 'high') {
86
+ trustLevel = 'partial';
87
+ }
88
+ trustReasons.push(`${context.expectationsUnused} expectation(s) unused - partial validation`);
89
+ }
90
+
91
+ return {
92
+ projectType: projectType,
93
+ expectationsFound: expectationsTotal,
94
+ expectationsByType: {
95
+ navigation: expectationsSummary.navigation || 0,
96
+ networkActions: expectationsSummary.networkActions || 0,
97
+ stateActions: expectationsSummary.stateActions || 0
98
+ },
99
+ interactionsExecuted: interactionsObserved,
100
+ validationStatus: validationStatus,
101
+ validationReason: validationReason,
102
+ trustLevel: trustLevel,
103
+ trustReasons: trustReasons
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Print run overview console block
109
+ * @param {Object} overview - Overview data from generateRunOverview
110
+ * @param {boolean} isCI - Whether in CI mode
111
+ */
112
+ export function printRunOverview(overview, isCI = false) {
113
+ if (isCI) {
114
+ // Compact CI format
115
+ console.error(`VERAX Run: ${overview.projectType} | ${overview.expectationsFound} expectations | ${overview.interactionsExecuted} interactions | ${overview.validationStatus}`);
116
+ return;
117
+ }
118
+
119
+ // Full format
120
+ console.error('\n' + '═'.repeat(60));
121
+ console.error('Run Overview');
122
+ console.error('═'.repeat(60));
123
+
124
+ console.error(`Project Type: ${overview.projectType}`);
125
+
126
+ console.error(`\nExpectations Found: ${overview.expectationsFound}`);
127
+ if (overview.expectationsFound > 0) {
128
+ const types = [];
129
+ if (overview.expectationsByType.navigation > 0) {
130
+ types.push(`navigation (${overview.expectationsByType.navigation})`);
131
+ }
132
+ if (overview.expectationsByType.networkActions > 0) {
133
+ types.push(`network actions (${overview.expectationsByType.networkActions})`);
134
+ }
135
+ if (overview.expectationsByType.stateActions > 0) {
136
+ types.push(`state actions (${overview.expectationsByType.stateActions})`);
137
+ }
138
+ if (types.length > 0) {
139
+ console.error(` Types: ${types.join(', ')}`);
140
+ }
141
+ }
142
+
143
+ console.error(`\nInteractions Executed: ${overview.interactionsExecuted}`);
144
+
145
+ console.error(`\nValidation Status: ${overview.validationStatus}`);
146
+ console.error(` ${overview.validationReason}`);
147
+
148
+ // Trust statement
149
+ console.error(`\nTrust Assessment: ${overview.trustLevel.toUpperCase()}`);
150
+ const trustPrefix = overview.trustLevel === 'high'
151
+ ? 'This result is trustworthy because'
152
+ : overview.trustLevel === 'partial'
153
+ ? 'This result is limited because'
154
+ : 'This result cannot be trusted because';
155
+
156
+ console.error(` ${trustPrefix}:`);
157
+ overview.trustReasons.forEach(reason => {
158
+ console.error(` • ${reason}`);
159
+ });
160
+
161
+ console.error('═'.repeat(60) + '\n');
162
+ }
163
+
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Wave 3 — URL Safety Checks
3
+ *
4
+ * Detects external/public URLs and requires confirmation.
5
+ */
6
+
7
+ /**
8
+ * Check if an IP address is private/local
9
+ * @param {string} ip - IP address
10
+ * @returns {boolean} True if private/local
11
+ */
12
+ function isPrivateIP(ip) {
13
+ // localhost
14
+ if (ip === '127.0.0.1' || ip === '::1' || ip === 'localhost') {
15
+ return true;
16
+ }
17
+
18
+ // Private IP ranges
19
+ const privateRanges = [
20
+ /^10\./, // 10.0.0.0/8
21
+ /^172\.(1[6-9]|2[0-9]|3[01])\./, // 172.16.0.0/12
22
+ /^192\.168\./, // 192.168.0.0/16
23
+ /^169\.254\./, // Link-local
24
+ /^fc00:/, // IPv6 private
25
+ /^fe80:/ // IPv6 link-local
26
+ ];
27
+
28
+ return privateRanges.some(range => range.test(ip));
29
+ }
30
+
31
+ /**
32
+ * Check if URL is external/public (requires confirmation)
33
+ * @param {string} url - URL to check
34
+ * @returns {Object} { isExternal: boolean, hostname: string }
35
+ */
36
+ export function checkUrlSafety(url) {
37
+ try {
38
+ const urlObj = new URL(url);
39
+ const hostname = urlObj.hostname;
40
+
41
+ // Check if localhost
42
+ if (hostname === 'localhost' || hostname === '127.0.0.1') {
43
+ return {
44
+ isExternal: false,
45
+ hostname: hostname
46
+ };
47
+ }
48
+
49
+ // Check if private IP
50
+ if (isPrivateIP(hostname)) {
51
+ return {
52
+ isExternal: false,
53
+ hostname: hostname
54
+ };
55
+ }
56
+
57
+ // All other hosts are considered external
58
+ return {
59
+ isExternal: true,
60
+ hostname: hostname
61
+ };
62
+ } catch (error) {
63
+ // Invalid URL - let other validation catch it
64
+ return {
65
+ isExternal: false,
66
+ hostname: null
67
+ };
68
+ }
69
+ }
70
+
71
+ /**
72
+ * @typedef {Object} ReadlineInterface
73
+ * @property {function(string): Promise<string>} question - Prompt user with question
74
+ * @property {function(): void} close - Close the readline interface
75
+ */
76
+
77
+ /**
78
+ * @typedef {Object} ConfirmExternalUrlOptions
79
+ * @property {ReadlineInterface} [readlineInterface] - Readline interface (injectable)
80
+ */
81
+
82
+ /**
83
+ * Prompt for external URL confirmation
84
+ * @param {string} hostname - Hostname to confirm
85
+ * @param {ConfirmExternalUrlOptions} [options={}] - Options
86
+ * @returns {Promise<boolean>} True if confirmed
87
+ */
88
+ export async function confirmExternalUrl(hostname, options = {}) {
89
+ const rl = options.readlineInterface || null;
90
+
91
+ if (!rl) {
92
+ // Use readline if not injected
93
+ const readline = await import('readline/promises');
94
+ const rlInterface = readline.default.createInterface({
95
+ input: process.stdin,
96
+ output: process.stdout,
97
+ terminal: true
98
+ });
99
+
100
+ try {
101
+ const answer = await rlInterface.question(`This will scan a public site (${hostname}). Continue? (y/N): `);
102
+ return (answer.trim().toLowerCase() || 'n').startsWith('y');
103
+ } finally {
104
+ rlInterface.close();
105
+ }
106
+ } else {
107
+ const answer = await rl.question(`This will scan a public site (${hostname}). Continue? (y/N): `);
108
+ return (answer.trim().toLowerCase() || 'n').startsWith('y');
109
+ }
110
+ }
111
+