@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,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,101 @@
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
+ * Prompt for external URL confirmation
73
+ * @param {string} hostname - Hostname to confirm
74
+ * @param {Object} options - Options
75
+ * @param {Function} options.readlineInterface - Readline interface (injectable)
76
+ * @returns {Promise<boolean>} True if confirmed
77
+ */
78
+ export async function confirmExternalUrl(hostname, options = {}) {
79
+ const rl = options.readlineInterface || null;
80
+
81
+ if (!rl) {
82
+ // Use readline if not injected
83
+ const readline = await import('readline/promises');
84
+ const rlInterface = readline.default.createInterface({
85
+ input: process.stdin,
86
+ output: process.stdout,
87
+ terminal: true
88
+ });
89
+
90
+ try {
91
+ const answer = await rlInterface.question(`This will scan a public site (${hostname}). Continue? (y/N): `);
92
+ return (answer.trim().toLowerCase() || 'n').startsWith('y');
93
+ } finally {
94
+ rlInterface.close();
95
+ }
96
+ } else {
97
+ const answer = await rl.question(`This will scan a public site (${hostname}). Continue? (y/N): `);
98
+ return (answer.trim().toLowerCase() || 'n').startsWith('y');
99
+ }
100
+ }
101
+
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Wave 3 — Interactive Wizard
3
+ *
4
+ * Guides users through VERAX configuration with friendly prompts.
5
+ */
6
+
7
+ /**
8
+ * Create a readline interface (can be injected for testing)
9
+ */
10
+ async function createReadlineInterface(input = process.stdin, output = process.stdout) {
11
+ const readline = await import('readline/promises');
12
+ return readline.default.createInterface({
13
+ input,
14
+ output,
15
+ terminal: true
16
+ });
17
+ }
18
+
19
+ /**
20
+ * Run interactive wizard
21
+ * @param {Object} options - Options for wizard
22
+ * @param {Function} options.readlineInterface - Readline interface (injectable for testing)
23
+ * @returns {Promise<Object>} Wizard results
24
+ */
25
+ export async function runWizard(options = {}) {
26
+ const rl = options.readlineInterface || await createReadlineInterface();
27
+
28
+ try {
29
+ console.log('VERAX — Silent Failure Detection\n');
30
+ console.log('This wizard will guide you through setting up a VERAX scan.\n');
31
+
32
+ // 1. Mode selection (Wave 7: add init and doctor)
33
+ const modeAnswer = await rl.question('Mode: (s)can, (f)low, (i)nit, or (d)octor? [s]: ');
34
+ const modeInput = (modeAnswer.trim().toLowerCase() || 's');
35
+ let mode;
36
+ if (modeInput.startsWith('i')) {
37
+ mode = 'init';
38
+ } else if (modeInput.startsWith('d')) {
39
+ mode = 'doctor';
40
+ } else if (modeInput.startsWith('f')) {
41
+ mode = 'flow';
42
+ } else {
43
+ mode = 'scan';
44
+ }
45
+
46
+ // For init and doctor modes, return early
47
+ if (mode === 'init' || mode === 'doctor') {
48
+ return { mode };
49
+ }
50
+
51
+ // 2. URL
52
+ const urlAnswer = await rl.question('URL to scan [http://localhost:3000]: ');
53
+ let url = urlAnswer.trim();
54
+ if (!url) {
55
+ url = 'http://localhost:3000';
56
+ }
57
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
58
+ url = 'http://' + url;
59
+ }
60
+
61
+ // 3. Project root (for scan) or flow file (for flow)
62
+ let projectRoot = process.cwd();
63
+ let flowPath = null;
64
+
65
+ if (mode === 'scan') {
66
+ const projectRootAnswer = await rl.question(`Project root [${projectRoot}]: `);
67
+ if (projectRootAnswer.trim()) {
68
+ projectRoot = projectRootAnswer.trim();
69
+ }
70
+ } else {
71
+ const flowPathAnswer = await rl.question('Flow file path: ');
72
+ flowPath = flowPathAnswer.trim();
73
+ if (!flowPath) {
74
+ throw new Error('Flow file path is required');
75
+ }
76
+ }
77
+
78
+ // 4. JSON output
79
+ const jsonAnswer = await rl.question('JSON output? (y/N) [N]: ');
80
+ const jsonOutput = (jsonAnswer.trim().toLowerCase() || 'n').startsWith('y');
81
+
82
+ // 5. Output directory
83
+ const outDirAnswer = await rl.question(`Output directory [.verax/runs]: `);
84
+ const outDir = outDirAnswer.trim() || '.verax/runs';
85
+
86
+ return {
87
+ mode,
88
+ url,
89
+ projectRoot,
90
+ flowPath,
91
+ json: jsonOutput,
92
+ out: outDir
93
+ };
94
+ } finally {
95
+ rl.close();
96
+ }
97
+ }
98
+
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Zero Findings Explanation
3
+ *
4
+ * Observational explanation of zero findings (not a judgment).
5
+ */
6
+
7
+ /**
8
+ * Generate explanation for zero findings
9
+ * @param {Object} context - Context information
10
+ * @returns {Array<string>} Explanation lines
11
+ */
12
+ export function explainZeroFindings(context = {}) {
13
+ const {
14
+ expectationsCount = 0,
15
+ interactionsObserved = 0,
16
+ discrepanciesObserved = 0
17
+ } = context;
18
+
19
+ const lines = [
20
+ '────────────────────────────────────────────────────────────',
21
+ 'OBSERVATION: No Discrepancies Detected',
22
+ '────────────────────────────────────────────────────────────',
23
+ '',
24
+ `VERAX analyzed ${expectationsCount} code-derived expectations`,
25
+ `Observed ${interactionsObserved} user interactions`,
26
+ `Discrepancies observed: ${discrepanciesObserved}`,
27
+ '',
28
+ 'What this means:',
29
+ ' • During this scan, no discrepancies were observed between code promises and runtime behavior',
30
+ ' • This does NOT guarantee safety, correctness, or completeness',
31
+ ' • Some expectations may not have been evaluated (see gaps below)',
32
+ '',
33
+ 'What was not observed:',
34
+ ];
35
+
36
+ if (expectationsCount === 0) {
37
+ lines.push(' • No expectations found in code - no evaluation was possible');
38
+ lines.push(' • VERAX requires PROVEN expectations (static patterns) to observe behavior');
39
+ } else {
40
+ lines.push(' • Check coverage gaps to see what was not evaluated');
41
+ }
42
+
43
+ return lines;
44
+ }
45
+
46
+ /**
47
+ * Print zero findings explanation
48
+ * @param {Object} context - Context
49
+ */
50
+ export function printZeroFindingsExplanation(context = {}) {
51
+ const lines = explainZeroFindings(context);
52
+ lines.forEach(line => {
53
+ console.error(line);
54
+ });
55
+ console.error('');
56
+ }
57
+
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Wave 6 — Zero Interaction Explanation
3
+ *
4
+ * Explains why no interactions were executed and what could/could not be validated.
5
+ */
6
+
7
+ /**
8
+ * Explain why no interactions were executed
9
+ * @param {Object} context - Scan context
10
+ * @returns {Array<string>} Explanation lines
11
+ */
12
+ export function explainZeroInteractions(context = {}) {
13
+ const {
14
+ verdict = 'UNKNOWN',
15
+ contextCheck = null,
16
+ interactionsObserved = 0,
17
+ expectationsTotal = 0,
18
+ observation = null
19
+ } = context;
20
+
21
+ // If interactions were actually observed, return empty
22
+ if (interactionsObserved > 0) {
23
+ return [];
24
+ }
25
+
26
+ // If no expectations, different explanation
27
+ if (expectationsTotal === 0) {
28
+ return [
29
+ 'No interactions executed because no expectations were found.',
30
+ 'VERAX needs code-derived expectations to know what to test.'
31
+ ];
32
+ }
33
+
34
+ const reasons = [];
35
+
36
+ // Context validation stopped early
37
+ if (verdict === 'INVALID_CONTEXT' && contextCheck && !contextCheck.forced) {
38
+ reasons.push([
39
+ '⚠️ No interactions executed because context validation failed.',
40
+ '',
41
+ 'VERAX stopped the scan early to prevent analyzing the wrong site.',
42
+ '',
43
+ 'What VERAX could validate:',
44
+ ` ✓ Discovered ${expectationsTotal} expectations from your code`,
45
+ ' ✗ Could not validate interactions (scan stopped)',
46
+ '',
47
+ 'What to do:',
48
+ ' • Use --force to scan anyway, or',
49
+ ' • Ensure your URL matches the project being analyzed'
50
+ ]);
51
+ } else if (verdict === 'INVALID_CONTEXT_FORCED') {
52
+ reasons.push([
53
+ '⚠️ Limited interactions executed due to context mismatch.',
54
+ '',
55
+ 'VERAX continued with --force but may not have found expected routes.',
56
+ '',
57
+ 'What VERAX validated:',
58
+ ` ✓ ${expectationsTotal} expectations found`,
59
+ ' ⚠ Interactions may not match expectations (context mismatch)',
60
+ '',
61
+ 'What to do:',
62
+ ' • Verify URL matches project deployment',
63
+ ' • Check that routes exist on the live site'
64
+ ]);
65
+ } else {
66
+ // No interactions discovered or executed
67
+ const observationDetails = observation?.observeTruth || {};
68
+
69
+ if (observationDetails.interactionsObserved === 0) {
70
+ reasons.push([
71
+ '⚠️ No interactions executed.',
72
+ '',
73
+ 'VERAX could not discover any interactive elements on the page.',
74
+ '',
75
+ 'What VERAX could validate:',
76
+ ` ✓ Discovered ${expectationsTotal} expectations from your code`,
77
+ ' ✗ Could not test interactions (no discoverable elements)',
78
+ '',
79
+ 'Possible reasons:',
80
+ ' • Page has no clickable links, buttons, or forms',
81
+ ' • Elements are hidden or not rendered',
82
+ ' • JavaScript errors prevented interaction discovery',
83
+ '',
84
+ 'What to do:',
85
+ ' • Verify the page loads correctly',
86
+ ' • Check browser console for errors',
87
+ ' • Ensure interactive elements are visible and accessible'
88
+ ]);
89
+ } else {
90
+ // Interactions discovered but none executed
91
+ reasons.push([
92
+ '⚠️ No interactions executed despite discoveries.',
93
+ '',
94
+ 'VERAX discovered interactions but could not execute them.',
95
+ '',
96
+ 'What VERAX could validate:',
97
+ ` ✓ Discovered ${expectationsTotal} expectations from your code`,
98
+ ' ✗ Could not test interactions (execution failed)',
99
+ '',
100
+ 'What to do:',
101
+ ' • Check network connectivity',
102
+ ' • Verify page is accessible',
103
+ ' • Review error logs for details'
104
+ ]);
105
+ }
106
+ }
107
+
108
+ return reasons.flat();
109
+ }
110
+
111
+ /**
112
+ * Print zero interaction explanation
113
+ * @param {Object} context - Scan context
114
+ */
115
+ export function printZeroInteractionExplanation(context) {
116
+ const explanation = explainZeroInteractions(context);
117
+ if (explanation.length > 0) {
118
+ console.error('\n' + '─'.repeat(60));
119
+ console.error('Interaction Status');
120
+ console.error('─'.repeat(60));
121
+ explanation.forEach(line => {
122
+ console.error(line);
123
+ });
124
+ console.error('─'.repeat(60) + '\n');
125
+ }
126
+ }
127
+
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ACTION CLASSIFIER
3
+ *
4
+ * Classifies user interactions by safety level for misuse resistance.
5
+ * NO JUDGMENT SEMANTICS - only risk classification for protection.
6
+ *
7
+ * Classifications:
8
+ * - SAFE_READONLY: Navigation, focus, hover, non-submitting clicks
9
+ * - RISKY: Destructive actions (delete/remove/clear data), logout, admin
10
+ * - WRITE_INTENT: State-changing actions (submit, file upload, checkout/pay)
11
+ *
12
+ * Phase 4: Default safe mode blocks RISKY and WRITE_INTENT unless explicitly allowed.
13
+ */
14
+
15
+ /**
16
+ * Classify an interaction's safety level
17
+ * @param {Object} interaction - Interaction object with type, text, label, selector
18
+ * @returns {Object} { classification: string, reason: string }
19
+ */
20
+ export function classifyAction(interaction) {
21
+ const type = (interaction.type || '').toLowerCase();
22
+ const text = (interaction.text || '').toLowerCase().trim();
23
+ const label = (interaction.label || '').toLowerCase().trim();
24
+ const ariaLabel = (interaction.ariaLabel || '').toLowerCase().trim();
25
+ const selector = (interaction.selector || '').toLowerCase();
26
+ const combined = `${text} ${label} ${ariaLabel}`.trim();
27
+
28
+ // RISKY patterns - destructive/dangerous actions
29
+ const riskyKeywords = /\b(delete|remove|erase|wipe|destroy|drop|clear\s+(data|all|history|cache|account|database|storage)|reset\s+(account|data|database)|deactivate|terminate|uninstall|unsubscribe)\b/i;
30
+ const adminKeywords = /\b(admin|administrator|sudo|root|settings|config|preferences)\b/i;
31
+
32
+ if (riskyKeywords.test(combined)) {
33
+ return { classification: 'RISKY', reason: 'destructive_keyword' };
34
+ }
35
+
36
+ // Logout/signout treated as risky (state loss)
37
+ if (/\b(logout|sign\s*out|log\s*out)\b/i.test(combined)) {
38
+ return { classification: 'RISKY', reason: 'auth_logout' };
39
+ }
40
+
41
+ // Admin/config actions risky
42
+ if (adminKeywords.test(combined) && !/view|read|show/.test(combined)) {
43
+ return { classification: 'RISKY', reason: 'admin_action' };
44
+ }
45
+
46
+ // WRITE_INTENT patterns - state-changing actions
47
+ const writeKeywords = /\b(submit|send|post|save|update|create|add|checkout|purchase|pay|buy|order|upload|attach|edit|modify)\b/i;
48
+
49
+ if (writeKeywords.test(combined)) {
50
+ return { classification: 'WRITE_INTENT', reason: 'write_keyword' };
51
+ }
52
+
53
+ // Form submissions are write intent
54
+ if (type === 'submit' || selector.includes('type="submit"') || selector.includes('[type=submit]')) {
55
+ return { classification: 'WRITE_INTENT', reason: 'form_submit' };
56
+ }
57
+
58
+ // File inputs are write intent
59
+ if (type === 'file' || selector.includes('type="file"') || selector.includes('input[type=file]')) {
60
+ return { classification: 'WRITE_INTENT', reason: 'file_input' };
61
+ }
62
+
63
+ // Default: safe readonly (navigation, clicks, focus, hover)
64
+ return { classification: 'SAFE_READONLY', reason: 'default_safe' };
65
+ }
66
+
67
+ /**
68
+ * Check if action should be blocked based on safety mode and flags
69
+ * @param {Object} interaction - Interaction to check
70
+ * @param {Object} flags - Safety flags { allowWrites: boolean, allowRiskyActions: boolean }
71
+ * @returns {Object} { shouldBlock: boolean, classification: string, reason: string }
72
+ */
73
+ export function shouldBlockAction(interaction, flags = {}) {
74
+ const { allowWrites = false, allowRiskyActions = false } = flags;
75
+ const { classification, reason } = classifyAction(interaction);
76
+
77
+ if (classification === 'RISKY' && !allowRiskyActions) {
78
+ return { shouldBlock: true, classification, reason };
79
+ }
80
+
81
+ if (classification === 'WRITE_INTENT' && !allowWrites) {
82
+ return { shouldBlock: true, classification, reason };
83
+ }
84
+
85
+ return { shouldBlock: false, classification, reason };
86
+ }