@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.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +14 -36
- package/src/cli/commands/default.js +523 -0
- package/src/cli/commands/doctor.js +165 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +402 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +296 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +34 -0
- package/src/cli/util/expectation-extractor.js +378 -0
- package/src/cli/util/findings-writer.js +31 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +366 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +29 -0
- package/src/cli/util/project-discovery.js +277 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/summary-writer.js +32 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +101 -0
- package/src/verax/cli/wizard.js +98 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +403 -0
- package/src/verax/core/incremental-store.js +237 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +521 -0
- package/src/verax/detect/comparison.js +2 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +177 -0
- package/src/verax/detect/expectation-model.js +194 -172
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +44 -8
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +172 -286
- package/src/verax/detect/interactive-findings.js +613 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/verdict-engine.js +563 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/index.js +90 -14
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +579 -0
- package/src/verax/intel/vue-router-extractor.js +323 -0
- package/src/verax/learn/action-contract-extractor.js +335 -101
- package/src/verax/learn/ast-contract-extractor.js +95 -5
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/manifest-writer.js +97 -47
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +27 -96
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +112 -4
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +10 -5
- package/src/verax/observe/console-sensor.js +1 -17
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +512 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +643 -275
- package/src/verax/observe/index.js +908 -27
- package/src/verax/observe/index.js.backup +1 -0
- package/src/verax/observe/interaction-discovery.js +365 -14
- package/src/verax/observe/interaction-runner.js +563 -198
- package/src/verax/observe/loading-sensor.js +139 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +37 -17
- package/src/verax/observe/state-sensor.js +389 -0
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +61 -20
- package/src/verax/observe/ui-signal-sensor.js +136 -17
- package/src/verax/scan-summary-writer.js +77 -15
- package/src/verax/shared/artifact-manager.js +110 -8
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +170 -0
- package/src/verax/shared/dynamic-route-utils.js +218 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +14 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +65 -0
- package/src/verax/validate/context-validator.js +244 -0
- 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
|
+
}
|