ragcode-context-engine 0.1.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/LICENSE +21 -0
- package/README.md +366 -0
- package/README.zh-CN.md +363 -0
- package/dist/src/cli/configure/app.d.ts +6 -0
- package/dist/src/cli/configure/app.js +81 -0
- package/dist/src/cli/configure/run.d.ts +5 -0
- package/dist/src/cli/configure/run.js +85 -0
- package/dist/src/cli/configure/state.d.ts +42 -0
- package/dist/src/cli/configure/state.js +174 -0
- package/dist/src/cli/configure.d.ts +31 -0
- package/dist/src/cli/configure.js +101 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +503 -0
- package/dist/src/cli/tui/index-progress.d.ts +12 -0
- package/dist/src/cli/tui/index-progress.js +49 -0
- package/dist/src/cli/tui/watch-status.d.ts +10 -0
- package/dist/src/cli/tui/watch-status.js +27 -0
- package/dist/src/cli/update.d.ts +18 -0
- package/dist/src/cli/update.js +111 -0
- package/dist/src/config/dotenv.d.ts +1 -0
- package/dist/src/config/dotenv.js +14 -0
- package/dist/src/config/graph-runtime.d.ts +13 -0
- package/dist/src/config/graph-runtime.js +29 -0
- package/dist/src/config/runtime-config.d.ts +87 -0
- package/dist/src/config/runtime-config.js +215 -0
- package/dist/src/config/semantic-runtime.d.ts +24 -0
- package/dist/src/config/semantic-runtime.js +89 -0
- package/dist/src/context/context-builder.d.ts +20 -0
- package/dist/src/context/context-builder.js +277 -0
- package/dist/src/context/expansion-policy.d.ts +6 -0
- package/dist/src/context/expansion-policy.js +49 -0
- package/dist/src/context/skeletonizer.d.ts +2 -0
- package/dist/src/context/skeletonizer.js +79 -0
- package/dist/src/context/snippet-renderer.d.ts +2 -0
- package/dist/src/context/snippet-renderer.js +67 -0
- package/dist/src/core/contracts.d.ts +74 -0
- package/dist/src/core/contracts.js +1 -0
- package/dist/src/core/engine.d.ts +64 -0
- package/dist/src/core/engine.js +442 -0
- package/dist/src/core/types.d.ts +490 -0
- package/dist/src/core/types.js +1 -0
- package/dist/src/diagnostics/doctor.d.ts +66 -0
- package/dist/src/diagnostics/doctor.js +193 -0
- package/dist/src/diagnostics/embedding-test.d.ts +24 -0
- package/dist/src/diagnostics/embedding-test.js +83 -0
- package/dist/src/graph/diff-files.d.ts +1 -0
- package/dist/src/graph/diff-files.js +14 -0
- package/dist/src/graph/impact-report.d.ts +10 -0
- package/dist/src/graph/impact-report.js +173 -0
- package/dist/src/graph/in-memory-graph-store.d.ts +36 -0
- package/dist/src/graph/in-memory-graph-store.js +395 -0
- package/dist/src/graph/owner-ranking.d.ts +2 -0
- package/dist/src/graph/owner-ranking.js +41 -0
- package/dist/src/graph/sqlite-graph-store.d.ts +51 -0
- package/dist/src/graph/sqlite-graph-store.js +724 -0
- package/dist/src/graph/sqlite-statements.d.ts +36 -0
- package/dist/src/graph/sqlite-statements.js +105 -0
- package/dist/src/graph/target-matcher.d.ts +13 -0
- package/dist/src/graph/target-matcher.js +64 -0
- package/dist/src/index.d.ts +32 -0
- package/dist/src/index.js +32 -0
- package/dist/src/indexing/analyzers/fallback-analyzer.d.ts +6 -0
- package/dist/src/indexing/analyzers/fallback-analyzer.js +45 -0
- package/dist/src/indexing/analyzers/go-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/go-treesitter-analyzer.js +87 -0
- package/dist/src/indexing/analyzers/java-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/java-treesitter-analyzer.js +88 -0
- package/dist/src/indexing/analyzers/python-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/python-treesitter-analyzer.js +96 -0
- package/dist/src/indexing/analyzers/registry.d.ts +5 -0
- package/dist/src/indexing/analyzers/registry.js +23 -0
- package/dist/src/indexing/analyzers/rust-treesitter-analyzer.d.ts +2 -0
- package/dist/src/indexing/analyzers/rust-treesitter-analyzer.js +96 -0
- package/dist/src/indexing/analyzers/tree-sitter-base.d.ts +30 -0
- package/dist/src/indexing/analyzers/tree-sitter-base.js +163 -0
- package/dist/src/indexing/analyzers/types.d.ts +17 -0
- package/dist/src/indexing/analyzers/types.js +1 -0
- package/dist/src/indexing/analyzers/typescript-analyzer.d.ts +5 -0
- package/dist/src/indexing/analyzers/typescript-analyzer.js +199 -0
- package/dist/src/indexing/ast-analyzer.d.ts +11 -0
- package/dist/src/indexing/ast-analyzer.js +11 -0
- package/dist/src/indexing/chunker.d.ts +11 -0
- package/dist/src/indexing/chunker.js +157 -0
- package/dist/src/indexing/ignore-policy.d.ts +6 -0
- package/dist/src/indexing/ignore-policy.js +40 -0
- package/dist/src/indexing/indexer.d.ts +13 -0
- package/dist/src/indexing/indexer.js +189 -0
- package/dist/src/indexing/language.d.ts +3 -0
- package/dist/src/indexing/language.js +24 -0
- package/dist/src/indexing/scanner.d.ts +13 -0
- package/dist/src/indexing/scanner.js +87 -0
- package/dist/src/lsp/definition-resolver.d.ts +6 -0
- package/dist/src/lsp/definition-resolver.js +60 -0
- package/dist/src/lsp/typescript-language-service.d.ts +21 -0
- package/dist/src/lsp/typescript-language-service.js +82 -0
- package/dist/src/mcp/server.d.ts +11 -0
- package/dist/src/mcp/server.js +64 -0
- package/dist/src/mcp/tools.d.ts +266 -0
- package/dist/src/mcp/tools.js +309 -0
- package/dist/src/project/project-identity.d.ts +2 -0
- package/dist/src/project/project-identity.js +24 -0
- package/dist/src/project/project-registry.d.ts +12 -0
- package/dist/src/project/project-registry.js +49 -0
- package/dist/src/project/workspace-resolver.d.ts +20 -0
- package/dist/src/project/workspace-resolver.js +62 -0
- package/dist/src/retrieval/graph-reranker.d.ts +11 -0
- package/dist/src/retrieval/graph-reranker.js +0 -0
- package/dist/src/retrieval/hybrid-retriever.d.ts +31 -0
- package/dist/src/retrieval/hybrid-retriever.js +111 -0
- package/dist/src/retrieval/path-classification.d.ts +6 -0
- package/dist/src/retrieval/path-classification.js +22 -0
- package/dist/src/retrieval/query-matching.d.ts +22 -0
- package/dist/src/retrieval/query-matching.js +166 -0
- package/dist/src/retrieval/query-planner.d.ts +5 -0
- package/dist/src/retrieval/query-planner.js +77 -0
- package/dist/src/retrieval/ranking-signals.d.ts +19 -0
- package/dist/src/retrieval/ranking-signals.js +97 -0
- package/dist/src/retrieval/topology-distance.d.ts +21 -0
- package/dist/src/retrieval/topology-distance.js +116 -0
- package/dist/src/reuse/reuse-detector.d.ts +12 -0
- package/dist/src/reuse/reuse-detector.js +564 -0
- package/dist/src/semantic/deterministic-embedding.d.ts +7 -0
- package/dist/src/semantic/deterministic-embedding.js +31 -0
- package/dist/src/semantic/in-memory-semantic-store.d.ts +11 -0
- package/dist/src/semantic/in-memory-semantic-store.js +65 -0
- package/dist/src/semantic/lance-semantic-store.d.ts +131 -0
- package/dist/src/semantic/lance-semantic-store.js +623 -0
- package/dist/src/semantic/openai-compatible-embedding.d.ts +19 -0
- package/dist/src/semantic/openai-compatible-embedding.js +75 -0
- package/dist/src/service/service-identity.d.ts +13 -0
- package/dist/src/service/service-identity.js +48 -0
- package/dist/src/service/service-manager.d.ts +29 -0
- package/dist/src/service/service-manager.js +231 -0
- package/dist/src/service/service-templates.d.ts +22 -0
- package/dist/src/service/service-templates.js +101 -0
- package/dist/src/subgraph/impact-explainer.d.ts +2 -0
- package/dist/src/subgraph/impact-explainer.js +54 -0
- package/dist/src/subgraph/node-expander.d.ts +13 -0
- package/dist/src/subgraph/node-expander.js +139 -0
- package/dist/src/subgraph/output-preset.d.ts +3 -0
- package/dist/src/subgraph/output-preset.js +102 -0
- package/dist/src/subgraph/subgraph-builder.d.ts +17 -0
- package/dist/src/subgraph/subgraph-builder.js +688 -0
- package/dist/src/topology/export-index.d.ts +7 -0
- package/dist/src/topology/export-index.js +14 -0
- package/dist/src/topology/framework-topology.d.ts +3 -0
- package/dist/src/topology/framework-topology.js +460 -0
- package/dist/src/topology/import-resolver.d.ts +2 -0
- package/dist/src/topology/import-resolver.js +29 -0
- package/dist/src/topology/orm-topology.d.ts +3 -0
- package/dist/src/topology/orm-topology.js +200 -0
- package/dist/src/topology/runtime-topology.d.ts +3 -0
- package/dist/src/topology/runtime-topology.js +204 -0
- package/dist/src/topology/symbol-resolver.d.ts +6 -0
- package/dist/src/topology/symbol-resolver.js +74 -0
- package/dist/src/topology/test-topology.d.ts +2 -0
- package/dist/src/topology/test-topology.js +82 -0
- package/dist/src/utils/hash.d.ts +2 -0
- package/dist/src/utils/hash.js +7 -0
- package/dist/src/utils/path.d.ts +2 -0
- package/dist/src/utils/path.js +7 -0
- package/dist/src/watch/event-journal.d.ts +17 -0
- package/dist/src/watch/event-journal.js +81 -0
- package/dist/src/watch/file-event-coalescer.d.ts +9 -0
- package/dist/src/watch/file-event-coalescer.js +39 -0
- package/dist/src/watch/index-scheduler.d.ts +52 -0
- package/dist/src/watch/index-scheduler.js +190 -0
- package/dist/src/watch/watch-daemon.d.ts +73 -0
- package/dist/src/watch/watch-daemon.js +368 -0
- package/dist/src/watch/watcher-liveness.d.ts +47 -0
- package/dist/src/watch/watcher-liveness.js +168 -0
- package/dist/src/web/server.d.ts +1 -0
- package/dist/src/web/server.js +375 -0
- package/package.json +94 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function createExportIndex(symbols) {
|
|
2
|
+
const fileSymbols = new Map();
|
|
3
|
+
const exportedSymbols = new Map();
|
|
4
|
+
for (const symbol of symbols) {
|
|
5
|
+
if (symbol.kind === "file")
|
|
6
|
+
fileSymbols.set(symbol.filePath, symbol);
|
|
7
|
+
if (symbol.exported)
|
|
8
|
+
exportedSymbols.set(exportKey(symbol.filePath, symbol.name), symbol);
|
|
9
|
+
}
|
|
10
|
+
return { fileSymbols, exportedSymbols };
|
|
11
|
+
}
|
|
12
|
+
export function exportKey(filePath, name) {
|
|
13
|
+
return `${filePath}::${name}`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CodeFile, GraphEdge, SymbolNode } from "../core/types.js";
|
|
2
|
+
import type { TypeScriptSourceFile } from "../lsp/typescript-language-service.js";
|
|
3
|
+
export declare function buildFrameworkTopologyEdges(files: CodeFile[], sources: TypeScriptSourceFile[], symbols: SymbolNode[], edges: GraphEdge[], priorEdges?: GraphEdge[]): GraphEdge[];
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
export function buildFrameworkTopologyEdges(files, sources, symbols, edges, priorEdges = []) {
|
|
4
|
+
const context = { files, sources, symbols, edges };
|
|
5
|
+
const routes = dedupeRoutes([
|
|
6
|
+
...routesFromEdges(priorEdges),
|
|
7
|
+
...frameworkResolvers.flatMap((resolver) => resolver.routes(context))
|
|
8
|
+
]);
|
|
9
|
+
const frameworkEdges = [];
|
|
10
|
+
frameworkEdges.push(...clientApiEdges(sources, symbols, routes));
|
|
11
|
+
frameworkEdges.push(...routeServiceEdges(edges, symbols, routes));
|
|
12
|
+
frameworkEdges.push(...webhookEdges(routes));
|
|
13
|
+
return dedupeEdges(frameworkEdges);
|
|
14
|
+
}
|
|
15
|
+
function routesFromEdges(edges) {
|
|
16
|
+
const routes = [];
|
|
17
|
+
for (const edge of edges) {
|
|
18
|
+
if (edge.kind !== "calls_api" && edge.kind !== "routes_to" && edge.kind !== "handles_webhook")
|
|
19
|
+
continue;
|
|
20
|
+
const routePath = stringMetadata(edge, "route") ?? stringMetadata(edge, "requestPath");
|
|
21
|
+
const routeFile = stringMetadata(edge, "routeFile") ?? stringMetadata(edge, "targetFile") ?? stringMetadata(edge, "sourceFile");
|
|
22
|
+
const filePath = stringMetadata(edge, "targetFile") ?? stringMetadata(edge, "sourceFile");
|
|
23
|
+
const targetName = stringMetadata(edge, "targetName") ?? edge.targetId;
|
|
24
|
+
if (!routePath || !routeFile || !filePath)
|
|
25
|
+
continue;
|
|
26
|
+
routes.push({
|
|
27
|
+
framework: stringMetadata(edge, "framework") ?? "unknown",
|
|
28
|
+
routePath,
|
|
29
|
+
routeFile,
|
|
30
|
+
filePath,
|
|
31
|
+
symbol: {
|
|
32
|
+
id: edge.targetId,
|
|
33
|
+
projectId: edge.projectId,
|
|
34
|
+
filePath,
|
|
35
|
+
name: targetName,
|
|
36
|
+
kind: "unknown",
|
|
37
|
+
language: "unknown",
|
|
38
|
+
startLine: 0,
|
|
39
|
+
endLine: 0,
|
|
40
|
+
exported: false
|
|
41
|
+
},
|
|
42
|
+
isWebhook: /webhook/i.test(routePath) || /webhook/i.test(path.posix.basename(routeFile))
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return routes;
|
|
46
|
+
}
|
|
47
|
+
function dedupeRoutes(routes) {
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
const deduped = [];
|
|
50
|
+
for (const route of routes) {
|
|
51
|
+
const key = [route.framework, route.routePath, route.routeFile, route.filePath, route.symbol.id].join("::");
|
|
52
|
+
if (seen.has(key))
|
|
53
|
+
continue;
|
|
54
|
+
seen.add(key);
|
|
55
|
+
deduped.push(route);
|
|
56
|
+
}
|
|
57
|
+
return deduped;
|
|
58
|
+
}
|
|
59
|
+
function stringMetadata(edge, key) {
|
|
60
|
+
const value = edge.metadata?.[key];
|
|
61
|
+
return typeof value === "string" ? value : undefined;
|
|
62
|
+
}
|
|
63
|
+
const nextJsResolver = {
|
|
64
|
+
name: "nextjs",
|
|
65
|
+
routes: ({ files, symbols }) => {
|
|
66
|
+
const routes = [];
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
const routePath = nextRoutePath(file.path);
|
|
69
|
+
if (!routePath)
|
|
70
|
+
continue;
|
|
71
|
+
const routeSymbol = routeHandlerSymbol(symbols, file.path) ?? fileSymbol(symbols, file.path);
|
|
72
|
+
if (!routeSymbol)
|
|
73
|
+
continue;
|
|
74
|
+
routes.push({
|
|
75
|
+
framework: "nextjs",
|
|
76
|
+
routePath,
|
|
77
|
+
routeFile: file.path,
|
|
78
|
+
filePath: file.path,
|
|
79
|
+
symbol: routeSymbol,
|
|
80
|
+
isWebhook: isWebhookRoute(routePath, file.path)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return routes;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const expressResolver = {
|
|
87
|
+
name: "express",
|
|
88
|
+
routes: ({ sources, symbols }) => sources
|
|
89
|
+
.filter((source) => importsPackage(source.content, "express"))
|
|
90
|
+
.flatMap((source) => routeDefinitions(source, symbols, "express"))
|
|
91
|
+
};
|
|
92
|
+
const fastifyResolver = {
|
|
93
|
+
name: "fastify",
|
|
94
|
+
routes: ({ sources, symbols }) => sources
|
|
95
|
+
.filter((source) => importsPackage(source.content, "fastify"))
|
|
96
|
+
.flatMap((source) => routeDefinitions(source, symbols, "fastify"))
|
|
97
|
+
};
|
|
98
|
+
const frameworkResolvers = [
|
|
99
|
+
nextJsResolver,
|
|
100
|
+
expressResolver,
|
|
101
|
+
fastifyResolver
|
|
102
|
+
];
|
|
103
|
+
function clientApiEdges(sources, symbols, routes) {
|
|
104
|
+
const edges = [];
|
|
105
|
+
for (const source of sources) {
|
|
106
|
+
if (!isClientSource(source))
|
|
107
|
+
continue;
|
|
108
|
+
for (const call of apiCalls(source)) {
|
|
109
|
+
if (call.resolution === "framework_template")
|
|
110
|
+
continue;
|
|
111
|
+
const route = findRoute(routes, call.url);
|
|
112
|
+
if (!route)
|
|
113
|
+
continue;
|
|
114
|
+
const sourceSymbol = containingSymbol(symbols, source.filePath, call.line) ?? fileSymbol(symbols, source.filePath);
|
|
115
|
+
if (!sourceSymbol)
|
|
116
|
+
continue;
|
|
117
|
+
edges.push({
|
|
118
|
+
projectId: sourceSymbol.projectId,
|
|
119
|
+
sourceId: sourceSymbol.id,
|
|
120
|
+
targetId: route.symbol.id,
|
|
121
|
+
kind: "calls_api",
|
|
122
|
+
metadata: {
|
|
123
|
+
framework: route.framework,
|
|
124
|
+
sourceFile: source.filePath,
|
|
125
|
+
targetFile: route.filePath,
|
|
126
|
+
routeFile: route.routeFile,
|
|
127
|
+
route: route.routePath,
|
|
128
|
+
requestPath: call.url,
|
|
129
|
+
targetName: route.symbol.name,
|
|
130
|
+
line: call.line,
|
|
131
|
+
resolution: call.resolution
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return edges;
|
|
137
|
+
}
|
|
138
|
+
function routeServiceEdges(edges, symbols, routes) {
|
|
139
|
+
const routeFiles = new Map(routes.map((route) => [route.routeFile, route]));
|
|
140
|
+
return edges.flatMap((edge) => {
|
|
141
|
+
const sourceFile = typeof edge.metadata?.sourceFile === "string" ? edge.metadata.sourceFile : undefined;
|
|
142
|
+
if (edge.kind !== "calls" || !isResolvedCall(edge) || !sourceFile || !routeFiles.has(sourceFile))
|
|
143
|
+
return [];
|
|
144
|
+
const source = symbols.find((symbol) => symbol.id === edge.sourceId);
|
|
145
|
+
const target = symbols.find((symbol) => symbol.id === edge.targetId);
|
|
146
|
+
const route = routeFiles.get(sourceFile);
|
|
147
|
+
return [{
|
|
148
|
+
projectId: edge.projectId,
|
|
149
|
+
sourceId: edge.sourceId,
|
|
150
|
+
targetId: edge.targetId,
|
|
151
|
+
kind: "routes_to",
|
|
152
|
+
metadata: {
|
|
153
|
+
framework: route?.framework,
|
|
154
|
+
sourceFile: source?.filePath ?? edge.metadata?.sourceFile,
|
|
155
|
+
targetFile: target?.filePath ?? edge.metadata?.targetFile,
|
|
156
|
+
routeFile: route?.routeFile,
|
|
157
|
+
route: route?.routePath,
|
|
158
|
+
targetName: target?.name ?? edge.metadata?.targetName,
|
|
159
|
+
line: edge.metadata?.line,
|
|
160
|
+
resolution: "framework_call_graph"
|
|
161
|
+
}
|
|
162
|
+
}];
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function webhookEdges(routes) {
|
|
166
|
+
return routes
|
|
167
|
+
.filter((route) => route.isWebhook)
|
|
168
|
+
.map((route) => ({
|
|
169
|
+
projectId: route.symbol.projectId,
|
|
170
|
+
sourceId: route.symbol.id,
|
|
171
|
+
targetId: route.symbol.id,
|
|
172
|
+
kind: "handles_webhook",
|
|
173
|
+
metadata: {
|
|
174
|
+
framework: route.framework,
|
|
175
|
+
sourceFile: route.filePath,
|
|
176
|
+
targetFile: route.filePath,
|
|
177
|
+
routeFile: route.routeFile,
|
|
178
|
+
route: route.routePath,
|
|
179
|
+
targetName: route.symbol.name,
|
|
180
|
+
resolution: "framework_static"
|
|
181
|
+
}
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
function routeDefinitions(source, symbols, framework) {
|
|
185
|
+
const sourceFile = parseSourceFile(source);
|
|
186
|
+
const routes = [];
|
|
187
|
+
function visit(node) {
|
|
188
|
+
if (ts.isCallExpression(node)) {
|
|
189
|
+
const definition = routeDefinitionFromCall(node, sourceFile, source, symbols, framework);
|
|
190
|
+
if (definition)
|
|
191
|
+
routes.push(definition);
|
|
192
|
+
}
|
|
193
|
+
ts.forEachChild(node, visit);
|
|
194
|
+
}
|
|
195
|
+
ts.forEachChild(sourceFile, visit);
|
|
196
|
+
return routes;
|
|
197
|
+
}
|
|
198
|
+
function routeDefinitionFromCall(node, sourceFile, source, symbols, framework) {
|
|
199
|
+
if (!ts.isPropertyAccessExpression(node.expression))
|
|
200
|
+
return undefined;
|
|
201
|
+
const method = node.expression.name.text;
|
|
202
|
+
if (!httpMethodNames.has(method))
|
|
203
|
+
return undefined;
|
|
204
|
+
const routePath = stringLiteralValue(node.arguments[0]);
|
|
205
|
+
const handler = node.arguments[1];
|
|
206
|
+
if (!routePath?.startsWith("/") || !handler || !looksLikeRouteHandler(handler))
|
|
207
|
+
return undefined;
|
|
208
|
+
const line = lineRange(sourceFile, node).startLine;
|
|
209
|
+
const symbol = routeHandlerFromArgument(handler, symbols, source.filePath, line) ?? containingSymbol(symbols, source.filePath, line) ?? fileSymbol(symbols, source.filePath);
|
|
210
|
+
if (!symbol)
|
|
211
|
+
return undefined;
|
|
212
|
+
return {
|
|
213
|
+
framework,
|
|
214
|
+
routePath,
|
|
215
|
+
routeFile: source.filePath,
|
|
216
|
+
filePath: symbol.filePath,
|
|
217
|
+
symbol,
|
|
218
|
+
isWebhook: isWebhookRoute(routePath, symbol.filePath)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function routeHandlerFromArgument(node, symbols, filePath, line) {
|
|
222
|
+
if (ts.isIdentifier(node)) {
|
|
223
|
+
return symbols.find((symbol) => symbol.name === node.text && symbol.kind !== "file" && symbol.exported)
|
|
224
|
+
?? symbols.find((symbol) => symbol.name === node.text && symbol.kind !== "file" && symbol.filePath === filePath)
|
|
225
|
+
?? containingSymbol(symbols, filePath, line);
|
|
226
|
+
}
|
|
227
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
228
|
+
return symbols.find((symbol) => symbol.name === node.name.text && symbol.kind !== "file" && symbol.exported)
|
|
229
|
+
?? symbols.find((symbol) => symbol.name === node.name.text && symbol.kind !== "file" && symbol.filePath === filePath)
|
|
230
|
+
?? containingSymbol(symbols, filePath, line);
|
|
231
|
+
}
|
|
232
|
+
return containingSymbol(symbols, filePath, line);
|
|
233
|
+
}
|
|
234
|
+
function looksLikeRouteHandler(node) {
|
|
235
|
+
return ts.isIdentifier(node)
|
|
236
|
+
|| ts.isPropertyAccessExpression(node)
|
|
237
|
+
|| ts.isFunctionExpression(node)
|
|
238
|
+
|| ts.isArrowFunction(node);
|
|
239
|
+
}
|
|
240
|
+
function importsPackage(content, packageName) {
|
|
241
|
+
const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
242
|
+
return new RegExp(`from ['\"]${escaped}['\"]|require\\(['\"]${escaped}['\"]\\)`).test(content);
|
|
243
|
+
}
|
|
244
|
+
function isResolvedCall(edge) {
|
|
245
|
+
return edge.metadata?.resolution === "resolved" || edge.metadata?.resolution === "resolved_lsp";
|
|
246
|
+
}
|
|
247
|
+
function nextRoutePath(filePath) {
|
|
248
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
249
|
+
const match = /(?:^|\/)(?:app|pages)\/api\/(.+)\/route\.[jt]sx?$/.exec(normalized);
|
|
250
|
+
if (match?.[1])
|
|
251
|
+
return `/api/${trimRouteSegments(match[1])}`;
|
|
252
|
+
const pagesMatch = /(?:^|\/)pages\/api\/(.+)\.[jt]sx?$/.exec(normalized);
|
|
253
|
+
if (pagesMatch?.[1])
|
|
254
|
+
return `/api/${trimRouteSegments(pagesMatch[1])}`;
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
function trimRouteSegments(route) {
|
|
258
|
+
return route
|
|
259
|
+
.split("/")
|
|
260
|
+
.filter((segment) => segment && !segment.startsWith("("))
|
|
261
|
+
.map((segment) => segment.replace(/^\[(.+)\]$/, ":$1"))
|
|
262
|
+
.join("/");
|
|
263
|
+
}
|
|
264
|
+
function routeHandlerSymbol(symbols, filePath) {
|
|
265
|
+
return symbols.find((symbol) => symbol.filePath === filePath && ["GET", "POST", "PUT", "PATCH", "DELETE"].includes(symbol.name))
|
|
266
|
+
?? symbols.find((symbol) => symbol.filePath === filePath && symbol.exported && symbol.kind !== "file");
|
|
267
|
+
}
|
|
268
|
+
function fileSymbol(symbols, filePath) {
|
|
269
|
+
return symbols.find((symbol) => symbol.filePath === filePath && symbol.kind === "file");
|
|
270
|
+
}
|
|
271
|
+
function containingSymbol(symbols, filePath, line) {
|
|
272
|
+
return symbols
|
|
273
|
+
.filter((symbol) => symbol.filePath === filePath && symbol.kind !== "file" && symbol.startLine <= line && symbol.endLine >= line)
|
|
274
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
|
275
|
+
}
|
|
276
|
+
function isClientSource(source) {
|
|
277
|
+
return /\.(tsx|jsx)$/.test(source.filePath) || /^["']use client["'];?/.test(source.content.trimStart()) || /from ['"]react['"]/.test(source.content);
|
|
278
|
+
}
|
|
279
|
+
function apiCalls(source) {
|
|
280
|
+
const sourceFile = parseSourceFile(source);
|
|
281
|
+
const stringConstants = collectStringConstants(sourceFile);
|
|
282
|
+
const calls = [];
|
|
283
|
+
function visit(node) {
|
|
284
|
+
if (ts.isCallExpression(node)) {
|
|
285
|
+
const url = apiUrlForCall(node, stringConstants);
|
|
286
|
+
if (url?.url.startsWith("/api/")) {
|
|
287
|
+
calls.push({
|
|
288
|
+
...url,
|
|
289
|
+
line: lineRange(sourceFile, node).startLine
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
ts.forEachChild(node, visit);
|
|
294
|
+
}
|
|
295
|
+
ts.forEachChild(sourceFile, visit);
|
|
296
|
+
return calls;
|
|
297
|
+
}
|
|
298
|
+
function apiUrlForCall(node, stringConstants) {
|
|
299
|
+
const directUrl = node.arguments[0] ? urlFromExpression(node.arguments[0], stringConstants) : undefined;
|
|
300
|
+
const expression = node.expression;
|
|
301
|
+
if (ts.isIdentifier(expression) && expression.text === "fetch" && directUrl)
|
|
302
|
+
return directUrl;
|
|
303
|
+
const chain = propertyChain(expression);
|
|
304
|
+
if (chain.length >= 2 && chain[0] === "axios" && httpMethodNames.has(chain[chain.length - 1] ?? "") && directUrl) {
|
|
305
|
+
// Preserve the URL's own resolution (a literal stays framework_static, a const stays
|
|
306
|
+
// framework_dataflow). Flattening static literals to framework_wrapper understated their
|
|
307
|
+
// certainty and dropped them to heuristic confidence downstream.
|
|
308
|
+
return directUrl;
|
|
309
|
+
}
|
|
310
|
+
const clientUrl = urlFromClientCall(chain);
|
|
311
|
+
if (clientUrl)
|
|
312
|
+
return clientUrl;
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
function urlFromExpression(expression, stringConstants) {
|
|
316
|
+
if (ts.isStringLiteralLike(expression))
|
|
317
|
+
return { url: expression.text, resolution: "framework_static" };
|
|
318
|
+
if (ts.isIdentifier(expression)) {
|
|
319
|
+
const value = stringConstants.get(expression.text);
|
|
320
|
+
return value ? { url: value, resolution: "framework_dataflow" } : undefined;
|
|
321
|
+
}
|
|
322
|
+
if (ts.isTemplateExpression(expression)) {
|
|
323
|
+
const result = templateStringValue(expression, stringConstants);
|
|
324
|
+
return {
|
|
325
|
+
url: result.value,
|
|
326
|
+
resolution: result.resolved ? "framework_dataflow" : "framework_template"
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
function collectStringConstants(sourceFile) {
|
|
332
|
+
const constants = new Map();
|
|
333
|
+
const conflicting = new Set();
|
|
334
|
+
function visit(node) {
|
|
335
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
336
|
+
const name = node.name.text;
|
|
337
|
+
const value = stringValueFromExpression(node.initializer, constants);
|
|
338
|
+
if (value !== undefined && !conflicting.has(name)) {
|
|
339
|
+
const existing = constants.get(name);
|
|
340
|
+
if (existing !== undefined && existing !== value) {
|
|
341
|
+
// Same identifier bound to different string values across scopes in one file.
|
|
342
|
+
// Bounded dataflow cannot tell which binding a reference resolves to, so drop it
|
|
343
|
+
// and let callers degrade to framework_template / unresolved instead of emitting
|
|
344
|
+
// a confident-but-wrong route link.
|
|
345
|
+
conflicting.add(name);
|
|
346
|
+
constants.delete(name);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
constants.set(name, value);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
ts.forEachChild(node, visit);
|
|
354
|
+
}
|
|
355
|
+
ts.forEachChild(sourceFile, visit);
|
|
356
|
+
return constants;
|
|
357
|
+
}
|
|
358
|
+
function stringValueFromExpression(expression, constants) {
|
|
359
|
+
if (ts.isStringLiteralLike(expression))
|
|
360
|
+
return expression.text;
|
|
361
|
+
if (ts.isIdentifier(expression))
|
|
362
|
+
return constants.get(expression.text);
|
|
363
|
+
if (ts.isTemplateExpression(expression)) {
|
|
364
|
+
const result = templateStringValue(expression, constants);
|
|
365
|
+
return result.resolved ? result.value : undefined;
|
|
366
|
+
}
|
|
367
|
+
if (ts.isNoSubstitutionTemplateLiteral(expression))
|
|
368
|
+
return expression.text;
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
function templateStringValue(expression, constants) {
|
|
372
|
+
let resolved = true;
|
|
373
|
+
let value = expression.head.text;
|
|
374
|
+
for (const span of expression.templateSpans) {
|
|
375
|
+
const part = stringValueFromExpression(span.expression, constants);
|
|
376
|
+
if (part === undefined) {
|
|
377
|
+
resolved = false;
|
|
378
|
+
value += "*";
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
value += part;
|
|
382
|
+
}
|
|
383
|
+
value += span.literal.text;
|
|
384
|
+
}
|
|
385
|
+
return { value, resolved };
|
|
386
|
+
}
|
|
387
|
+
function urlFromClientCall(chain) {
|
|
388
|
+
if (chain.length >= 3 && ["api", "apiClient"].includes(chain[0] ?? "")) {
|
|
389
|
+
const resource = chain[1];
|
|
390
|
+
if (resource)
|
|
391
|
+
return { url: `/api/${resource}`, resolution: "framework_wrapper" };
|
|
392
|
+
}
|
|
393
|
+
const root = chain[0];
|
|
394
|
+
if (chain.length >= 2 && root?.endsWith("Api")) {
|
|
395
|
+
const resource = root.slice(0, -"Api".length);
|
|
396
|
+
if (resource)
|
|
397
|
+
return { url: `/api/${resource}`, resolution: "framework_wrapper" };
|
|
398
|
+
}
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
function propertyChain(expression) {
|
|
402
|
+
if (ts.isIdentifier(expression))
|
|
403
|
+
return [expression.text];
|
|
404
|
+
if (ts.isPropertyAccessExpression(expression))
|
|
405
|
+
return [...propertyChain(expression.expression), expression.name.text];
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
function findRoute(routes, requestPath) {
|
|
409
|
+
const exact = routes.find((route) => route.routePath === requestPath);
|
|
410
|
+
if (exact)
|
|
411
|
+
return exact;
|
|
412
|
+
return routes.find((route) => routePathMatches(route.routePath, requestPath));
|
|
413
|
+
}
|
|
414
|
+
function routePathMatches(routePath, requestPath) {
|
|
415
|
+
const routeSegments = routePath.split("/");
|
|
416
|
+
const requestSegments = requestPath.split("/");
|
|
417
|
+
if (routeSegments.length !== requestSegments.length)
|
|
418
|
+
return false;
|
|
419
|
+
return routeSegments.every((segment, index) => segment === requestSegments[index] || segment.startsWith(":"));
|
|
420
|
+
}
|
|
421
|
+
function isWebhookRoute(routePath, filePath) {
|
|
422
|
+
return /webhook/i.test(routePath) || /webhook/i.test(path.posix.basename(filePath));
|
|
423
|
+
}
|
|
424
|
+
function stringLiteralValue(node) {
|
|
425
|
+
if (!node)
|
|
426
|
+
return undefined;
|
|
427
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
428
|
+
return node.text;
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
function parseSourceFile(source) {
|
|
432
|
+
return ts.createSourceFile(source.filePath, source.content, ts.ScriptTarget.Latest, true, scriptKindForPath(source.filePath));
|
|
433
|
+
}
|
|
434
|
+
function lineRange(sourceFile, node) {
|
|
435
|
+
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
436
|
+
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
437
|
+
return { startLine: start.line + 1, endLine: end.line + 1 };
|
|
438
|
+
}
|
|
439
|
+
function scriptKindForPath(filePath) {
|
|
440
|
+
if (filePath.endsWith(".tsx"))
|
|
441
|
+
return ts.ScriptKind.TSX;
|
|
442
|
+
if (filePath.endsWith(".jsx"))
|
|
443
|
+
return ts.ScriptKind.JSX;
|
|
444
|
+
if (filePath.endsWith(".js") || filePath.endsWith(".mjs") || filePath.endsWith(".cjs"))
|
|
445
|
+
return ts.ScriptKind.JS;
|
|
446
|
+
return ts.ScriptKind.TS;
|
|
447
|
+
}
|
|
448
|
+
const httpMethodNames = new Set(["get", "post", "put", "patch", "delete"]);
|
|
449
|
+
function dedupeEdges(edges) {
|
|
450
|
+
const seen = new Set();
|
|
451
|
+
const deduped = [];
|
|
452
|
+
for (const edge of edges) {
|
|
453
|
+
const key = [edge.kind, edge.sourceId, edge.targetId, edge.metadata?.framework, edge.metadata?.route, edge.metadata?.requestPath, edge.metadata?.sourceFile, edge.metadata?.line].join("::");
|
|
454
|
+
if (seen.has(key))
|
|
455
|
+
continue;
|
|
456
|
+
seen.add(key);
|
|
457
|
+
deduped.push(edge);
|
|
458
|
+
}
|
|
459
|
+
return deduped;
|
|
460
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
const RESOLVABLE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"];
|
|
3
|
+
export function resolveImportPath(sourceFilePath, importSource, files) {
|
|
4
|
+
if (!importSource.startsWith(".") && !importSource.startsWith("/"))
|
|
5
|
+
return undefined;
|
|
6
|
+
const filePaths = new Set(files.map((file) => file.path));
|
|
7
|
+
const sourceDir = path.posix.dirname(sourceFilePath);
|
|
8
|
+
const base = normalizeImportCandidate(importSource.startsWith("/")
|
|
9
|
+
? importSource.slice(1)
|
|
10
|
+
: path.posix.normalize(path.posix.join(sourceDir, importSource)));
|
|
11
|
+
for (const candidate of candidatePaths(base)) {
|
|
12
|
+
if (filePaths.has(candidate))
|
|
13
|
+
return candidate;
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
function candidatePaths(base) {
|
|
18
|
+
const candidates = [base];
|
|
19
|
+
if (!RESOLVABLE_EXTENSIONS.some((extension) => base.endsWith(extension))) {
|
|
20
|
+
for (const extension of RESOLVABLE_EXTENSIONS)
|
|
21
|
+
candidates.push(`${base}${extension}`);
|
|
22
|
+
}
|
|
23
|
+
for (const extension of RESOLVABLE_EXTENSIONS)
|
|
24
|
+
candidates.push(path.posix.join(base, `index${extension}`));
|
|
25
|
+
return [...new Set(candidates.map(normalizeImportCandidate))];
|
|
26
|
+
}
|
|
27
|
+
function normalizeImportCandidate(candidate) {
|
|
28
|
+
return candidate.replaceAll("\\", "/").replace(/^\/+/, "");
|
|
29
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CodeFile, GraphEdge, SymbolNode } from "../core/types.js";
|
|
2
|
+
import type { TypeScriptSourceFile } from "../lsp/typescript-language-service.js";
|
|
3
|
+
export declare function buildOrmTopologyEdges(repoRoot: string, _files: CodeFile[], sources: TypeScriptSourceFile[], symbols: SymbolNode[]): GraphEdge[];
|