@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,212 @@
1
+ import { parse } from '@babel/parser';
2
+ import traverse from '@babel/traverse';
3
+ import { readFileSync } from 'fs';
4
+ import { glob } from 'glob';
5
+ import { resolve } from 'path';
6
+ import { ExpectationProof } from '../shared/expectation-proof.js';
7
+
8
+ const MAX_FILES_TO_SCAN = 200;
9
+
10
+ /**
11
+ * Extracts static string value from call expression arguments.
12
+ * Returns null if value is dynamic.
13
+ */
14
+ function extractStaticActionName(node) {
15
+ if (!node) return null;
16
+
17
+ // String literal: dispatch('increment')
18
+ if (node.type === 'StringLiteral') {
19
+ return node.value;
20
+ }
21
+
22
+ // Template literal without interpolation: `increment`
23
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
24
+ if (node.quasis.length === 1) {
25
+ return node.quasis[0].value.cooked;
26
+ }
27
+ }
28
+
29
+ return null;
30
+ }
31
+
32
+ /**
33
+ * Extracts PROVEN state action expectations from Redux and Zustand patterns.
34
+ *
35
+ * Supported patterns (all require static string literals):
36
+ * - Redux Toolkit: dispatch(increment()), dispatch(decrement())
37
+ * - Redux Toolkit: dispatch(slice.actions.increment())
38
+ * - Zustand: set((state) => ({ ... })) with key names
39
+ *
40
+ * Returns array of state expectations with:
41
+ * - type: 'state_action'
42
+ * - expectedTarget: action name or store key
43
+ * - storeType: 'redux' | 'zustand'
44
+ * - sourceFile: string
45
+ * - proof: PROVEN_EXPECTATION
46
+ * - line: number
47
+ */
48
+ function extractStateExpectations(filePath, fileContent) {
49
+ const expectations = [];
50
+
51
+ try {
52
+ const ast = parse(fileContent, {
53
+ sourceType: 'module',
54
+ plugins: ['jsx', 'typescript']
55
+ });
56
+
57
+ traverse.default(ast, {
58
+ // Redux dispatch(action())
59
+ CallExpression(path) {
60
+ const callee = path.node.callee;
61
+
62
+ // dispatch(someAction())
63
+ if (callee.type === 'Identifier' && callee.name === 'dispatch') {
64
+ const firstArg = path.node.arguments[0];
65
+
66
+ // dispatch(increment()) - action creator call
67
+ if (firstArg && firstArg.type === 'CallExpression') {
68
+ let actionName = null;
69
+
70
+ // Simple action creator: increment()
71
+ if (firstArg.callee.type === 'Identifier') {
72
+ actionName = firstArg.callee.name;
73
+ }
74
+
75
+ // Slice action: counterSlice.actions.increment()
76
+ else if (firstArg.callee.type === 'MemberExpression') {
77
+ const property = firstArg.callee.property;
78
+ if (property.type === 'Identifier') {
79
+ actionName = property.name;
80
+ }
81
+ }
82
+
83
+ if (actionName) {
84
+ expectations.push({
85
+ type: 'state_action',
86
+ expectedTarget: actionName,
87
+ storeType: 'redux',
88
+ sourceFile: filePath,
89
+ proof: ExpectationProof.PROVEN_EXPECTATION,
90
+ line: path.node.loc?.start.line || null
91
+ });
92
+ }
93
+ }
94
+ }
95
+
96
+ // Zustand set((state) => ({ key: value }))
97
+ if (callee.type === 'Identifier' && callee.name === 'set') {
98
+ const firstArg = path.node.arguments[0];
99
+
100
+ // set((state) => ({ ... })) - arrow function returning object
101
+ if (firstArg && firstArg.type === 'ArrowFunctionExpression') {
102
+ const body = firstArg.body;
103
+
104
+ // Arrow function body is object expression
105
+ if (body.type === 'ObjectExpression') {
106
+ for (const prop of body.properties) {
107
+ if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') {
108
+ const keyName = prop.key.name;
109
+ expectations.push({
110
+ type: 'state_action',
111
+ expectedTarget: keyName,
112
+ storeType: 'zustand',
113
+ sourceFile: filePath,
114
+ proof: ExpectationProof.PROVEN_EXPECTATION,
115
+ line: path.node.loc?.start.line || null
116
+ });
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+ });
124
+ } catch (error) {
125
+ // Parse error - skip this file
126
+ return [];
127
+ }
128
+
129
+ return expectations;
130
+ }
131
+
132
+ /**
133
+ * Detects if project uses Redux or Zustand by scanning package.json and imports.
134
+ */
135
+ function detectStateStores(projectDir) {
136
+ const stores = {
137
+ redux: false,
138
+ zustand: false
139
+ };
140
+
141
+ try {
142
+ const pkgPath = resolve(projectDir, 'package.json');
143
+ const pkgContent = readFileSync(pkgPath, 'utf-8');
144
+ const pkg = JSON.parse(pkgContent);
145
+
146
+ const allDeps = {
147
+ ...(pkg.dependencies || {}),
148
+ ...(pkg.devDependencies || {})
149
+ };
150
+
151
+ if (allDeps['@reduxjs/toolkit'] || allDeps['redux']) {
152
+ stores.redux = true;
153
+ }
154
+ if (allDeps['zustand']) {
155
+ stores.zustand = true;
156
+ }
157
+ } catch (error) {
158
+ // No package.json or parse error
159
+ }
160
+
161
+ return stores;
162
+ }
163
+
164
+ /**
165
+ * Scans project for state action expectations.
166
+ * Returns { expectations: [], storesDetected: { redux: bool, zustand: bool } }
167
+ */
168
+ export async function extractStateExpectationsFromAST(projectDir) {
169
+ const storesDetected = detectStateStores(projectDir);
170
+ const expectations = [];
171
+
172
+ // Only scan if supported stores are detected
173
+ if (!storesDetected.redux && !storesDetected.zustand) {
174
+ return { expectations: [], storesDetected };
175
+ }
176
+
177
+ try {
178
+ const files = await glob('**/*.{js,jsx,ts,tsx}', {
179
+ cwd: projectDir,
180
+ absolute: false,
181
+ ignore: ['node_modules/**', 'dist/**', 'build/**', '.next/**', 'out/**']
182
+ });
183
+
184
+ const filesToScan = files.slice(0, MAX_FILES_TO_SCAN);
185
+
186
+ for (const file of filesToScan) {
187
+ try {
188
+ const filePath = resolve(projectDir, file);
189
+ const content = readFileSync(filePath, 'utf-8');
190
+ const fileExpectations = extractStateExpectations(file, content);
191
+ expectations.push(...fileExpectations);
192
+ } catch (error) {
193
+ continue;
194
+ }
195
+ }
196
+ } catch (error) {
197
+ return { expectations: [], storesDetected };
198
+ }
199
+
200
+ // Deduplicate by target name
201
+ const seen = new Set();
202
+ const unique = [];
203
+ for (const exp of expectations) {
204
+ const key = `${exp.storeType}:${exp.expectedTarget}`;
205
+ if (!seen.has(key)) {
206
+ seen.add(key);
207
+ unique.push(exp);
208
+ }
209
+ }
210
+
211
+ return { expectations: unique, storesDetected };
212
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Extract navigation expectations from static HTML files.
3
+ * Detects router.push/replace/navigate calls in inline scripts.
4
+ */
5
+
6
+ function extractNavigationExpectations(root, fromPath, file, projectDir) {
7
+ const expectations = [];
8
+
9
+ // Extract from inline scripts
10
+ const scripts = root.querySelectorAll('script');
11
+ for (const script of scripts) {
12
+ const scriptContent = script.textContent || '';
13
+ if (!scriptContent) continue;
14
+
15
+ // Find router.push/replace/navigate calls with string literals
16
+ const routerPushMatches = scriptContent.matchAll(/router\.push\s*\(\s*['"]([^'"]+)['"]/g);
17
+ const routerReplaceMatches = scriptContent.matchAll(/router\.replace\s*\(\s*['"]([^'"]+)['"]/g);
18
+ const navigateMatches = scriptContent.matchAll(/navigate\s*\(\s*['"]([^'"]+)['"]/g);
19
+
20
+ for (const match of [...routerPushMatches, ...routerReplaceMatches, ...navigateMatches]) {
21
+ const target = match[1];
22
+ if (!target) continue;
23
+
24
+ // Only extract if it's a relative path (starts with /)
25
+ if (!target.startsWith('/')) continue;
26
+
27
+ // Find the button/function that triggers this
28
+ const buttonId = scriptContent.match(/getElementById\s*\(\s*['"]([^'"]+)['"]/)?.[1];
29
+ const functionName = scriptContent.match(/function\s+(\w+)\s*\(/)?.[1];
30
+
31
+ let selectorHint = null;
32
+ if (buttonId) {
33
+ selectorHint = `#${buttonId}`;
34
+ } else if (functionName) {
35
+ // Try to find button with onclick that calls this function
36
+ const buttons = root.querySelectorAll(`button[onclick*="${functionName}"]`);
37
+ if (buttons.length > 0) {
38
+ const btn = buttons[0];
39
+ selectorHint = btn.id ? `#${btn.id}` : `button[onclick*="${functionName}"]`;
40
+ }
41
+ }
42
+
43
+ if (selectorHint) {
44
+ // Determine method from match
45
+ let method = 'push';
46
+ if (match[0].includes('replace')) {
47
+ method = 'replace';
48
+ } else if (match[0].includes('navigate')) {
49
+ method = 'navigate';
50
+ }
51
+
52
+ expectations.push({
53
+ fromPath: fromPath,
54
+ type: 'spa_navigation',
55
+ targetPath: target,
56
+ expectedTarget: target,
57
+ navigationMethod: method,
58
+ proof: 'PROVEN_EXPECTATION',
59
+ sourceRef: `${file}:${scriptContent.indexOf(match[0])}`,
60
+ selectorHint: selectorHint,
61
+ evidence: {
62
+ source: file,
63
+ selectorHint: selectorHint
64
+ }
65
+ });
66
+ }
67
+ }
68
+ }
69
+
70
+ // Also check onclick attributes directly
71
+ const buttons = root.querySelectorAll('button[onclick]');
72
+ for (const button of buttons) {
73
+ const onclick = button.getAttribute('onclick') || '';
74
+ const routerPushMatch = onclick.match(/router\.push\s*\(\s*['"]([^'"]+)['"]/);
75
+ const routerReplaceMatch = onclick.match(/router\.replace\s*\(\s*['"]([^'"]+)['"]/);
76
+ const navigateMatch = onclick.match(/navigate\s*\(\s*['"]([^'"]+)['"]/);
77
+
78
+ const match = routerPushMatch || routerReplaceMatch || navigateMatch;
79
+ if (match) {
80
+ const target = match[1];
81
+ if (target.startsWith('/')) {
82
+ const buttonId = button.getAttribute('id');
83
+ const selectorHint = buttonId ? `#${buttonId}` : `button[onclick*="${target}"]`;
84
+
85
+ let method = 'push';
86
+ if (routerReplaceMatch) {
87
+ method = 'replace';
88
+ } else if (navigateMatch) {
89
+ method = 'navigate';
90
+ }
91
+
92
+ expectations.push({
93
+ fromPath: fromPath,
94
+ type: 'spa_navigation',
95
+ targetPath: target,
96
+ expectedTarget: target,
97
+ navigationMethod: method,
98
+ proof: 'PROVEN_EXPECTATION',
99
+ sourceRef: `${file}:onclick`,
100
+ selectorHint: selectorHint,
101
+ evidence: {
102
+ source: file,
103
+ selectorHint: selectorHint
104
+ }
105
+ });
106
+ }
107
+ }
108
+ }
109
+
110
+ return expectations;
111
+ }
112
+
113
+ export { extractNavigationExpectations };
114
+
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Extract validation_block expectations from static HTML files.
3
+ * Detects preventDefault() calls in onSubmit handlers.
4
+ */
5
+
6
+ function extractValidationExpectations(root, fromPath, file, projectDir) {
7
+ const expectations = [];
8
+
9
+ // Extract from inline scripts
10
+ const scripts = root.querySelectorAll('script');
11
+ for (const script of scripts) {
12
+ const scriptContent = script.textContent || '';
13
+ if (!scriptContent) continue;
14
+
15
+ // Find preventDefault() calls in functions that might be onSubmit handlers
16
+ const preventDefaultMatches = scriptContent.matchAll(/preventDefault\s*\(\s*\)/g);
17
+
18
+ for (const match of preventDefaultMatches) {
19
+ // Check if this is in an onSubmit context
20
+ // Look for function definitions that might be onSubmit handlers
21
+ const beforeMatch = scriptContent.substring(0, match.index);
22
+ const afterMatch = scriptContent.substring(match.index);
23
+
24
+ // Check if there's a function definition before this preventDefault
25
+ const functionMatch = beforeMatch.match(/function\s+(\w+)\s*\([^)]*event[^)]*\)/);
26
+ const onSubmitMatch = beforeMatch.match(/onsubmit\s*=\s*["'](\w+)["']/);
27
+
28
+ let functionName = null;
29
+ if (functionMatch) {
30
+ functionName = functionMatch[1];
31
+ } else if (onSubmitMatch) {
32
+ functionName = onSubmitMatch[1];
33
+ }
34
+
35
+ if (functionName) {
36
+ // Find form with onsubmit that calls this function
37
+ const forms = root.querySelectorAll(`form[onsubmit*="${functionName}"]`);
38
+ for (const form of forms) {
39
+ const formId = form.getAttribute('id');
40
+ const selectorHint = formId ? `#${formId}` : `form[onsubmit*="${functionName}"]`;
41
+
42
+ expectations.push({
43
+ fromPath: fromPath,
44
+ type: 'validation_block',
45
+ proof: 'PROVEN_EXPECTATION',
46
+ sourceRef: `${file}:${scriptContent.substring(0, match.index).split('\n').length}`,
47
+ selectorHint: selectorHint,
48
+ handlerRef: `${file}:${functionName}`,
49
+ evidence: {
50
+ source: file,
51
+ selectorHint: selectorHint
52
+ }
53
+ });
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ // Also check inline onsubmit attributes directly
60
+ const forms = root.querySelectorAll('form[onsubmit]');
61
+ for (const form of forms) {
62
+ const onsubmit = form.getAttribute('onsubmit') || '';
63
+ const preventDefaultMatch = onsubmit.match(/preventDefault\s*\(\s*\)/);
64
+
65
+ if (preventDefaultMatch) {
66
+ const formId = form.getAttribute('id');
67
+ const selectorHint = formId ? `#${formId}` : `form[onsubmit*="preventDefault"]`;
68
+
69
+ expectations.push({
70
+ fromPath: fromPath,
71
+ type: 'validation_block',
72
+ proof: 'PROVEN_EXPECTATION',
73
+ sourceRef: `${file}:onsubmit`,
74
+ selectorHint: selectorHint,
75
+ handlerRef: `${file}:onsubmit`,
76
+ evidence: {
77
+ source: file,
78
+ selectorHint: selectorHint
79
+ }
80
+ });
81
+ }
82
+ }
83
+
84
+ return expectations;
85
+ }
86
+
87
+ export { extractValidationExpectations };
88
+
@@ -2,7 +2,6 @@ import { glob } from 'glob';
2
2
  import { resolve, dirname, join, relative } from 'path';
3
3
  import { readFileSync, existsSync } from 'fs';
4
4
  import { parse } from 'node-html-parser';
5
- import { ExpectationProof } from '../shared/expectation-proof.js';
6
5
 
7
6
  const MAX_HTML_FILES = 200;
8
7
 
@@ -121,7 +120,6 @@ function extractButtonNavigationExpectations(root, fromPath, file, routeMap, pro
121
120
  fromPath: fromPath,
122
121
  type: 'navigation',
123
122
  targetPath: targetPath,
124
- proof: ExpectationProof.PROVEN_EXPECTATION,
125
123
  evidence: {
126
124
  source: file,
127
125
  selectorHint: selectorHint
@@ -151,7 +149,6 @@ function extractFormSubmissionExpectations(root, fromPath, file, routeMap, proje
151
149
  fromPath: fromPath,
152
150
  type: 'form_submission',
153
151
  targetPath: targetPath,
154
- proof: ExpectationProof.PROVEN_EXPECTATION,
155
152
  evidence: {
156
153
  source: file,
157
154
  selectorHint: selectorHint
@@ -162,6 +159,105 @@ function extractFormSubmissionExpectations(root, fromPath, file, routeMap, proje
162
159
  return expectations;
163
160
  }
164
161
 
162
+ function extractNetworkExpectations(root, fromPath, file, projectDir) {
163
+ const expectations = [];
164
+
165
+ // Extract from inline scripts
166
+ const scripts = root.querySelectorAll('script');
167
+ for (const script of scripts) {
168
+ const scriptContent = script.textContent || '';
169
+ if (!scriptContent) continue;
170
+
171
+ // Find fetch() calls with string literals
172
+ const fetchMatches = scriptContent.matchAll(/fetch\s*\(\s*['"]([^'"]+)['"]/g);
173
+ for (const match of fetchMatches) {
174
+ const endpoint = match[1];
175
+ if (!endpoint) continue;
176
+
177
+ // Only extract if it's an API endpoint (starts with /api/)
178
+ if (!endpoint.startsWith('/api/')) continue;
179
+
180
+ // Find the button/function that triggers this
181
+ // Look for onclick handlers or function definitions
182
+ const buttonId = scriptContent.match(/getElementById\s*\(\s*['"]([^'"]+)['"]/)?.[1];
183
+ const functionName = scriptContent.match(/function\s+(\w+)\s*\(/)?.[1];
184
+
185
+ let selectorHint = null;
186
+ if (buttonId) {
187
+ selectorHint = `#${buttonId}`;
188
+ } else if (functionName) {
189
+ // Try to find button with onclick that calls this function
190
+ const buttons = root.querySelectorAll(`button[onclick*="${functionName}"]`);
191
+ if (buttons.length > 0) {
192
+ const btn = buttons[0];
193
+ selectorHint = btn.id ? `#${btn.id}` : `button[onclick*="${functionName}"]`;
194
+ }
195
+ }
196
+
197
+ if (selectorHint) {
198
+ // Determine method from fetch options if present
199
+ let method = 'GET';
200
+ const methodMatch = scriptContent.match(/method\s*:\s*['"]([^'"]+)['"]/i);
201
+ if (methodMatch) {
202
+ method = methodMatch[1].toUpperCase();
203
+ }
204
+
205
+ expectations.push({
206
+ fromPath: fromPath,
207
+ type: 'network_action',
208
+ expectedTarget: endpoint,
209
+ urlPath: endpoint,
210
+ method: method,
211
+ proof: 'PROVEN_EXPECTATION',
212
+ sourceRef: `${file}:${scriptContent.indexOf(match[0])}`,
213
+ selectorHint: selectorHint,
214
+ evidence: {
215
+ source: file,
216
+ selectorHint: selectorHint
217
+ }
218
+ });
219
+ }
220
+ }
221
+ }
222
+
223
+ // Also check onclick attributes directly
224
+ const buttons = root.querySelectorAll('button[onclick]');
225
+ for (const button of buttons) {
226
+ const onclick = button.getAttribute('onclick') || '';
227
+ const fetchMatch = onclick.match(/fetch\s*\(\s*['"]([^'"]+)['"]/);
228
+ if (fetchMatch) {
229
+ const endpoint = fetchMatch[1];
230
+ if (endpoint.startsWith('/api/')) {
231
+ const buttonId = button.getAttribute('id');
232
+ const selectorHint = buttonId ? `#${buttonId}` : `button[onclick*="${endpoint}"]`;
233
+
234
+ let method = 'GET';
235
+ const methodMatch = onclick.match(/method\s*:\s*['"]([^'"]+)['"]/i);
236
+ if (methodMatch) {
237
+ method = methodMatch[1].toUpperCase();
238
+ }
239
+
240
+ expectations.push({
241
+ fromPath: fromPath,
242
+ type: 'network_action',
243
+ expectedTarget: endpoint,
244
+ urlPath: endpoint,
245
+ method: method,
246
+ proof: 'PROVEN_EXPECTATION',
247
+ sourceRef: `${file}:onclick`,
248
+ selectorHint: selectorHint,
249
+ evidence: {
250
+ source: file,
251
+ selectorHint: selectorHint
252
+ }
253
+ });
254
+ }
255
+ }
256
+ }
257
+
258
+ return expectations;
259
+ }
260
+
165
261
  export async function extractStaticExpectations(projectDir, routes) {
166
262
  const expectations = [];
167
263
  const routeMap = new Map(routes.map(r => [r.path, r]));
@@ -199,7 +295,6 @@ export async function extractStaticExpectations(projectDir, routes) {
199
295
  fromPath: fromPath,
200
296
  type: 'navigation',
201
297
  targetPath: targetPath,
202
- proof: ExpectationProof.PROVEN_EXPECTATION,
203
298
  evidence: {
204
299
  source: file,
205
300
  selectorHint: selectorHint
@@ -212,6 +307,19 @@ export async function extractStaticExpectations(projectDir, routes) {
212
307
 
213
308
  const formExpectations = extractFormSubmissionExpectations(root, fromPath, file, routeMap, projectDir);
214
309
  expectations.push(...formExpectations);
310
+
311
+ const networkExpectations = extractNetworkExpectations(root, fromPath, file, projectDir);
312
+ expectations.push(...networkExpectations);
313
+
314
+ // NAVIGATION INTELLIGENCE v2: Extract navigation expectations from inline scripts
315
+ const { extractNavigationExpectations } = await import('./static-extractor-navigation.js');
316
+ const navigationExpectations = extractNavigationExpectations(root, fromPath, file, projectDir);
317
+ expectations.push(...navigationExpectations);
318
+
319
+ // VALIDATION INTELLIGENCE v1: Extract validation_block expectations from inline scripts
320
+ const { extractValidationExpectations } = await import('./static-extractor-validation.js');
321
+ const validationExpectations = extractValidationExpectations(root, fromPath, file, projectDir);
322
+ expectations.push(...validationExpectations);
215
323
  } catch (error) {
216
324
  continue;
217
325
  }
@@ -3,7 +3,7 @@ import { hasReactRouterDom } from './project-detector.js';
3
3
 
4
4
  const MAX_HTML_FILES = 200;
5
5
 
6
- export async function assessLearnTruth(projectDir, projectType, routes, staticExpectations, spaExpectations = null) {
6
+ export async function assessLearnTruth(projectDir, projectType, routes, staticExpectations) {
7
7
  const truth = {
8
8
  routesDiscovered: routes.length,
9
9
  routesSource: 'none',
@@ -18,14 +18,24 @@ export async function assessLearnTruth(projectDir, projectType, routes, staticEx
18
18
  if (projectType === 'nextjs_app_router' || projectType === 'nextjs_pages_router') {
19
19
  truth.routesSource = 'nextjs_fs';
20
20
  truth.routesConfidence = 'HIGH';
21
+ } else if (projectType === 'vue_router') {
22
+ truth.routesSource = 'vue_router_ast';
23
+ truth.routesConfidence = 'HIGH';
21
24
 
22
- // Wave 1 - CODE TRUTH ENGINE: Count AST-derived expectations
23
- if (spaExpectations && spaExpectations.length > 0) {
24
- truth.expectationsDiscovered = spaExpectations.length;
25
- truth.expectationsStrong = spaExpectations.length;
25
+ if (staticExpectations && staticExpectations.length > 0) {
26
+ truth.expectationsDiscovered = staticExpectations.length;
27
+ truth.expectationsStrong = staticExpectations.filter(e =>
28
+ e.type === 'spa_navigation'
29
+ ).length;
26
30
  truth.expectationsWeak = 0;
27
- truth.expectationsSource = 'ast_contracts';
28
31
  }
32
+ } else if (projectType === 'vue_spa') {
33
+ truth.routesSource = 'vue_no_router';
34
+ truth.routesConfidence = 'LOW';
35
+ truth.limitations.push({
36
+ code: 'VUE_ROUTER_NOT_INSTALLED',
37
+ message: 'Vue detected but vue-router not installed. Routes cannot be extracted from router configuration.'
38
+ });
29
39
  } else if (projectType === 'static') {
30
40
  truth.routesSource = 'static_html';
31
41
 
@@ -67,21 +77,14 @@ export async function assessLearnTruth(projectDir, projectType, routes, staticEx
67
77
  });
68
78
  }
69
79
 
70
- // Wave 1 - CODE TRUTH ENGINE: Report AST-based expectations instead of routes
71
- if (spaExpectations && spaExpectations.length > 0) {
72
- truth.expectationsDiscovered = spaExpectations.length;
73
- truth.expectationsStrong = spaExpectations.length;
74
- truth.expectationsWeak = 0;
75
- truth.expectationsSource = 'ast_contracts';
76
- } else {
77
- truth.expectationsDiscovered = 0;
78
- truth.expectationsStrong = 0;
79
- truth.expectationsWeak = 0;
80
- truth.warnings.push({
81
- code: 'NO_AST_CONTRACTS_FOUND',
82
- message: 'No JSX Link/NavLink elements with static href/to found. No PROVEN expectations available.'
83
- });
84
- }
80
+ truth.warnings.push({
81
+ code: 'REACT_ROUTE_EXTRACTION_FRAGILE',
82
+ message: 'Route extraction uses regex parsing of source files. Dynamic routes, nested routers, and code-split route definitions may be missed.'
83
+ });
84
+
85
+ truth.expectationsDiscovered = routes.length;
86
+ truth.expectationsStrong = routes.length;
87
+ truth.expectationsWeak = 0;
85
88
  } else if (projectType === 'unknown') {
86
89
  truth.routesSource = 'none';
87
90
  truth.routesConfidence = 'LOW';