@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,280 @@
1
+ /**
2
+ * CODE INTELLIGENCE v1 — Route Extraction (AST-based)
3
+ *
4
+ * Extracts routes from Next.js and React Router using AST analysis.
5
+ * Includes dynamic routes with example paths.
6
+ *
7
+ * Supported:
8
+ * - Next.js pages router (file-system)
9
+ * - Next.js app router (file-system)
10
+ * - React Router <Route path="...">
11
+ * - Dynamic routes: /users/[id] → /users/1
12
+ */
13
+
14
+ import ts from 'typescript';
15
+ import { resolve, relative, sep, basename, dirname, extname } from 'path';
16
+ import { existsSync, readdirSync, statSync } from 'fs';
17
+ import { parseFile, findNodes, getStringLiteral, getNodeLocation } from './ts-program.js';
18
+ import { extractVueRoutes } from './vue-router-extractor.js';
19
+ import { normalizeDynamicRoute } from '../shared/dynamic-route-utils.js';
20
+
21
+ const INTERNAL_PATH_PATTERNS = [
22
+ /^\/admin/,
23
+ /^\/dashboard/,
24
+ /^\/account/,
25
+ /^\/settings/,
26
+ /\/internal/,
27
+ /\/private/
28
+ ];
29
+
30
+ function isInternalRoute(path) {
31
+ return INTERNAL_PATH_PATTERNS.some(pattern => pattern.test(path));
32
+ }
33
+
34
+ /**
35
+ * Extract routes from project.
36
+ *
37
+ * @param {string} projectRoot - Project root
38
+ * @param {Object} program - TypeScript program from createTSProgram
39
+ * @returns {Array} - Array of route objects
40
+ */
41
+ export function extractRoutes(projectRoot, program) {
42
+ const routes = [];
43
+
44
+ // Detect Next.js
45
+ const hasNextConfig = existsSync(resolve(projectRoot, 'next.config.js')) ||
46
+ existsSync(resolve(projectRoot, 'next.config.mjs'));
47
+ const hasPagesDir = existsSync(resolve(projectRoot, 'pages'));
48
+ const hasAppDir = existsSync(resolve(projectRoot, 'app'));
49
+
50
+ if (hasNextConfig || hasPagesDir || hasAppDir) {
51
+ // Next.js detected
52
+ if (hasPagesDir) {
53
+ routes.push(...extractNextPagesRoutes(projectRoot));
54
+ }
55
+ if (hasAppDir) {
56
+ routes.push(...extractNextAppRoutes(projectRoot));
57
+ }
58
+ }
59
+
60
+ // React Router detection
61
+ if (program && program.program) {
62
+ routes.push(...extractReactRouterRoutes(projectRoot, program));
63
+ }
64
+
65
+ // Vue Router detection
66
+ if (program && program.program) {
67
+ routes.push(...extractVueRoutes(projectRoot, program));
68
+ }
69
+
70
+ return routes;
71
+ }
72
+
73
+ /**
74
+ * Extract Next.js pages router routes (file-system based).
75
+ *
76
+ * @param {string} projectRoot - Project root
77
+ * @returns {Array} - Routes with sourceRef
78
+ */
79
+ function extractNextPagesRoutes(projectRoot) {
80
+ const routes = [];
81
+ const pagesDir = resolve(projectRoot, 'pages');
82
+
83
+ if (!existsSync(pagesDir)) return routes;
84
+
85
+ function walk(dir, urlPath = '') {
86
+ const entries = readdirSync(dir);
87
+
88
+ for (const entry of entries) {
89
+ const fullPath = resolve(dir, entry);
90
+ const stat = statSync(fullPath);
91
+
92
+ if (stat.isDirectory()) {
93
+ // Nested route
94
+ walk(fullPath, `${urlPath}/${entry}`);
95
+ } else if (stat.isFile()) {
96
+ const ext = extname(entry);
97
+ if (!['.js', '.jsx', '.ts', '.tsx'].includes(ext)) continue;
98
+
99
+ const baseName = basename(entry, ext);
100
+
101
+ // Skip special files
102
+ if (baseName.startsWith('_')) continue;
103
+ if (baseName === 'index') {
104
+ // index.tsx -> /path or /
105
+ const route = urlPath || '/';
106
+ const relativePath = relative(projectRoot, fullPath);
107
+ routes.push({
108
+ path: route,
109
+ sourceRef: `${relativePath.replace(/\\/g, '/')}:1`,
110
+ file: relativePath.replace(/\\/g, '/'),
111
+ framework: 'next-pages'
112
+ });
113
+ } else {
114
+ // file.tsx -> /path/file
115
+ const route = `${urlPath}/${baseName}`;
116
+ const relativePath = relative(projectRoot, fullPath);
117
+ const routeObj = {
118
+ path: route,
119
+ sourceRef: `${relativePath.replace(/\\/g, '/')}:1`,
120
+ file: relativePath.replace(/\\/g, '/'),
121
+ framework: 'next-pages'
122
+ };
123
+
124
+ // Normalize dynamic routes to example paths
125
+ const normalized = normalizeDynamicRoute(route);
126
+ if (normalized) {
127
+ routeObj.path = normalized.examplePath;
128
+ routeObj.originalPattern = normalized.originalPattern;
129
+ routeObj.isDynamic = true;
130
+ routeObj.exampleExecution = true;
131
+ }
132
+
133
+ routes.push(routeObj);
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ walk(pagesDir);
140
+ return routes;
141
+ }
142
+
143
+ /**
144
+ * Extract Next.js app router routes (file-system based).
145
+ *
146
+ * @param {string} projectRoot - Project root
147
+ * @returns {Array} - Routes with sourceRef
148
+ */
149
+ function extractNextAppRoutes(projectRoot) {
150
+ const routes = [];
151
+ const appDir = resolve(projectRoot, 'app');
152
+
153
+ if (!existsSync(appDir)) return routes;
154
+
155
+ function walk(dir, urlPath = '') {
156
+ const entries = readdirSync(dir);
157
+
158
+ for (const entry of entries) {
159
+ const fullPath = resolve(dir, entry);
160
+ const stat = statSync(fullPath);
161
+
162
+ if (stat.isDirectory()) {
163
+ // Nested route segment
164
+ walk(fullPath, `${urlPath}/${entry}`);
165
+ } else if (stat.isFile()) {
166
+ const ext = extname(entry);
167
+ const baseName = basename(entry, ext);
168
+
169
+ // App router: page.tsx defines the route
170
+ if (baseName === 'page' && ['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
171
+ const route = urlPath || '/';
172
+ const relativePath = relative(projectRoot, fullPath);
173
+ const routeObj = {
174
+ path: route,
175
+ sourceRef: `${relativePath.replace(/\\/g, '/')}:1`,
176
+ file: relativePath.replace(/\\/g, '/'),
177
+ framework: 'next-app'
178
+ };
179
+
180
+ // Normalize dynamic routes to example paths
181
+ const normalized = normalizeDynamicRoute(route);
182
+ if (normalized) {
183
+ routeObj.path = normalized.examplePath;
184
+ routeObj.originalPattern = normalized.originalPattern;
185
+ routeObj.isDynamic = true;
186
+ routeObj.exampleExecution = true;
187
+ }
188
+
189
+ routes.push(routeObj);
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ walk(appDir);
196
+ return routes;
197
+ }
198
+
199
+ /**
200
+ * Extract React Router routes from JSX.
201
+ *
202
+ * @param {string} projectRoot - Project root
203
+ * @param {Object} program - TypeScript program
204
+ * @returns {Array} - Routes with sourceRef
205
+ */
206
+ function extractReactRouterRoutes(projectRoot, program) {
207
+ const routes = [];
208
+
209
+ for (const sourceFile of program.sourceFiles) {
210
+ const ast = parseFile(sourceFile, true);
211
+ if (!ast) continue;
212
+
213
+ // Find <Route path="..."> elements
214
+ const routeElements = findNodes(ast, node => {
215
+ return ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node);
216
+ });
217
+
218
+ for (const element of routeElements) {
219
+ const tagName = element.tagName;
220
+ if (!ts.isIdentifier(tagName)) continue;
221
+ if (tagName.text !== 'Route') continue;
222
+
223
+ // Find path attribute
224
+ const attributes = element.attributes;
225
+ if (!attributes || !attributes.properties) continue;
226
+
227
+ for (const attr of attributes.properties) {
228
+ if (!ts.isJsxAttribute(attr)) continue;
229
+
230
+ const name = attr.name;
231
+ if (!ts.isIdentifier(name)) continue;
232
+ if (name.text !== 'path') continue;
233
+
234
+ const initializer = attr.initializer;
235
+ if (!initializer) continue;
236
+
237
+ let pathValue = null;
238
+
239
+ // StringLiteral: path="..."
240
+ if (ts.isStringLiteral(initializer)) {
241
+ pathValue = initializer.text;
242
+ }
243
+ // JsxExpression: path={"..."}
244
+ else if (ts.isJsxExpression(initializer)) {
245
+ const expr = initializer.expression;
246
+ if (expr && (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr))) {
247
+ pathValue = expr.text;
248
+ }
249
+ }
250
+
251
+ if (pathValue) {
252
+ const location = getNodeLocation(ast, element, projectRoot);
253
+
254
+ // Normalize dynamic routes to example paths
255
+ const normalized = normalizeDynamicRoute(pathValue);
256
+ const routeObj = normalized ? {
257
+ path: normalized.examplePath,
258
+ originalPattern: normalized.originalPattern,
259
+ isDynamic: true,
260
+ exampleExecution: true,
261
+ sourceRef: location.sourceRef,
262
+ file: location.file,
263
+ line: location.line,
264
+ framework: 'react-router'
265
+ } : {
266
+ path: pathValue,
267
+ sourceRef: location.sourceRef,
268
+ file: location.file,
269
+ line: location.line,
270
+ framework: 'react-router'
271
+ };
272
+
273
+ routes.push(routeObj);
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ return routes;
280
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * CODE INTELLIGENCE v1 — TypeScript Program Foundation
3
+ *
4
+ * Provides AST-based code understanding:
5
+ * - Creates TypeScript Program over project sources
6
+ * - Walks AST nodes
7
+ * - Resolves symbols
8
+ * - Tracks source locations (file:line)
9
+ *
10
+ * NO REGEX. NO GUESSING. AST ONLY.
11
+ */
12
+
13
+ import ts from 'typescript';
14
+ import { resolve, relative, extname } from 'path';
15
+ import { readdirSync, statSync, readFileSync, existsSync } from 'fs';
16
+
17
+ /**
18
+ * Create a TypeScript Program for AST analysis.
19
+ *
20
+ * @param {string} projectRoot - Project root directory
21
+ * @param {Object} options - Options { includeJs: boolean }
22
+ * @returns {Object} - { program, typeChecker, sourceFiles }
23
+ */
24
+ export function createTSProgram(projectRoot, options = {}) {
25
+ const { includeJs = true } = options;
26
+
27
+ // Collect source files
28
+ const sourceFiles = collectSourceFiles(projectRoot, includeJs);
29
+
30
+ if (sourceFiles.length === 0) {
31
+ return {
32
+ program: null,
33
+ typeChecker: null,
34
+ sourceFiles: [],
35
+ error: 'No source files found'
36
+ };
37
+ }
38
+
39
+ // Create compiler options
40
+ const compilerOptions = {
41
+ target: ts.ScriptTarget.ES2020,
42
+ module: ts.ModuleKind.ESNext,
43
+ jsx: ts.JsxEmit.React,
44
+ allowJs: includeJs,
45
+ checkJs: false,
46
+ noEmit: true,
47
+ skipLibCheck: true,
48
+ skipDefaultLibCheck: true,
49
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
50
+ esModuleInterop: true,
51
+ resolveJsonModule: true
52
+ };
53
+
54
+ // Create program
55
+ const program = ts.createProgram(sourceFiles, compilerOptions);
56
+ const typeChecker = program.getTypeChecker();
57
+
58
+ return {
59
+ program,
60
+ typeChecker,
61
+ sourceFiles,
62
+ error: null
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Collect source files (.ts, .tsx, .js, .jsx).
68
+ *
69
+ * @param {string} projectRoot - Project root
70
+ * @param {boolean} includeJs - Include .js/.jsx files
71
+ * @returns {string[]} - Array of absolute file paths
72
+ */
73
+ function collectSourceFiles(projectRoot, includeJs) {
74
+ const files = [];
75
+ const extensions = includeJs
76
+ ? ['.ts', '.tsx', '.js', '.jsx']
77
+ : ['.ts', '.tsx'];
78
+
79
+ const ignoreDirs = ['node_modules', '.verax', 'dist', 'build', '.next', 'out', '.git'];
80
+
81
+ function walk(dir) {
82
+ try {
83
+ const entries = readdirSync(dir);
84
+
85
+ for (const entry of entries) {
86
+ const fullPath = resolve(dir, entry);
87
+ const stat = statSync(fullPath);
88
+
89
+ if (stat.isDirectory()) {
90
+ // Skip ignored directories
91
+ if (ignoreDirs.includes(entry)) continue;
92
+ walk(fullPath);
93
+ } else if (stat.isFile()) {
94
+ const ext = extname(entry);
95
+ if (extensions.includes(ext)) {
96
+ files.push(fullPath);
97
+ }
98
+ }
99
+ }
100
+ } catch (err) {
101
+ // Skip directories we can't read
102
+ }
103
+ }
104
+
105
+ walk(projectRoot);
106
+ return files;
107
+ }
108
+
109
+ /**
110
+ * Get source location for AST node.
111
+ *
112
+ * @param {ts.SourceFile} sourceFile - TypeScript source file
113
+ * @param {ts.Node} node - AST node
114
+ * @param {string} projectRoot - Project root for relative path
115
+ * @returns {Object} - { file, line, column, sourceRef }
116
+ */
117
+ export function getNodeLocation(sourceFile, node, projectRoot) {
118
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
119
+ const relativePath = relative(projectRoot, sourceFile.fileName);
120
+
121
+ return {
122
+ file: relativePath.replace(/\\/g, '/'),
123
+ line: line + 1, // 1-indexed
124
+ column: character + 1,
125
+ sourceRef: `${relativePath.replace(/\\/g, '/')}:${line + 1}`
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Walk AST and invoke callback for each node.
131
+ *
132
+ * @param {ts.Node} node - Root node
133
+ * @param {Function} callback - Callback(node) => void
134
+ */
135
+ export function walkAST(node, callback) {
136
+ callback(node);
137
+ ts.forEachChild(node, child => walkAST(child, callback));
138
+ }
139
+
140
+ /**
141
+ * Find nodes matching a predicate.
142
+ *
143
+ * @param {ts.Node} root - Root node
144
+ * @param {Function} predicate - Predicate(node) => boolean
145
+ * @returns {ts.Node[]} - Matching nodes
146
+ */
147
+ export function findNodes(root, predicate) {
148
+ const results = [];
149
+
150
+ walkAST(root, node => {
151
+ if (predicate(node)) {
152
+ results.push(node);
153
+ }
154
+ });
155
+
156
+ return results;
157
+ }
158
+
159
+ /**
160
+ * Get string literal value from node.
161
+ *
162
+ * @param {ts.Node} node - AST node
163
+ * @returns {string|null} - String value or null
164
+ */
165
+ export function getStringLiteral(node) {
166
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
167
+ return node.text;
168
+ }
169
+ return null;
170
+ }
171
+
172
+ /**
173
+ * Resolve identifier to its declaration.
174
+ *
175
+ * @param {ts.TypeChecker} typeChecker - Type checker
176
+ * @param {ts.Identifier} identifier - Identifier node
177
+ * @returns {ts.Declaration|null} - Declaration node or null
178
+ */
179
+ export function resolveIdentifier(typeChecker, identifier) {
180
+ try {
181
+ const symbol = typeChecker.getSymbolAtLocation(identifier);
182
+ if (!symbol || !symbol.declarations || symbol.declarations.length === 0) {
183
+ return null;
184
+ }
185
+ return symbol.declarations[0];
186
+ } catch (err) {
187
+ return null;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Check if node is a function declaration or arrow function.
193
+ *
194
+ * @param {ts.Node} node - AST node
195
+ * @returns {boolean}
196
+ */
197
+ export function isFunctionNode(node) {
198
+ return ts.isFunctionDeclaration(node) ||
199
+ ts.isFunctionExpression(node) ||
200
+ ts.isArrowFunction(node) ||
201
+ ts.isMethodDeclaration(node);
202
+ }
203
+
204
+ /**
205
+ * Get function body statements.
206
+ *
207
+ * @param {ts.Node} funcNode - Function node
208
+ * @returns {ts.Statement[]|null} - Statements or null
209
+ */
210
+ export function getFunctionBody(funcNode) {
211
+ if (!isFunctionNode(funcNode)) return null;
212
+
213
+ const body = funcNode.body;
214
+ if (!body) return null;
215
+
216
+ // Arrow function with expression body
217
+ if (ts.isExpression(body)) {
218
+ return []; // No statements, just expression
219
+ }
220
+
221
+ // Block body
222
+ if (ts.isBlock(body)) {
223
+ return Array.from(body.statements);
224
+ }
225
+
226
+ return null;
227
+ }
228
+
229
+ /**
230
+ * Parse a single file into AST.
231
+ *
232
+ * @param {string} filePath - File path
233
+ * @param {boolean} isJsx - Is JSX/TSX
234
+ * @returns {ts.SourceFile|null} - Parsed source file
235
+ */
236
+ export function parseFile(filePath, isJsx = false) {
237
+ if (!existsSync(filePath)) return null;
238
+
239
+ try {
240
+ const content = readFileSync(filePath, 'utf-8');
241
+ const ext = extname(filePath);
242
+ const scriptKind = isJsx || ext === '.tsx' || ext === '.jsx'
243
+ ? ts.ScriptKind.TSX
244
+ : ts.ScriptKind.TS;
245
+
246
+ return ts.createSourceFile(
247
+ filePath,
248
+ content,
249
+ ts.ScriptTarget.ES2020,
250
+ true,
251
+ scriptKind
252
+ );
253
+ } catch (err) {
254
+ return null;
255
+ }
256
+ }