@esankhan3/code-search-mcp 0.0.2 → 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/README.md +203 -263
- package/dist/resources/resources.js +3 -3
- package/dist/server.js +2 -2
- package/dist/tools/graph.js +1 -1
- package/dist/tools/index-tools.js +1 -1
- package/dist/tools/profile.js +2 -2
- package/dist/tools/search.js +1 -1
- package/package.json +16 -9
- package/dist/core/ast-graph-builder.js +0 -800
- package/dist/core/chunker.js +0 -368
- package/dist/core/claude-runner.js +0 -173
- package/dist/core/config.js +0 -59
- package/dist/core/cross-repo-detector.js +0 -887
- package/dist/core/embedder.js +0 -334
- package/dist/core/file-walker.js +0 -186
- package/dist/core/git-diff.js +0 -160
- package/dist/core/graph-metrics.js +0 -233
- package/dist/core/indexer.js +0 -552
- package/dist/core/project-graph-builder.js +0 -202
- package/dist/core/query-classifier.js +0 -144
- package/dist/core/query-router.js +0 -210
- package/dist/core/rag-evaluator.js +0 -213
- package/dist/core/repo-profiler.js +0 -383
- package/dist/core/reranker.js +0 -218
- package/dist/core/retriever.js +0 -242
- package/dist/core/semantic-edge-detector.js +0 -147
- package/dist/core/service-mesh-inferrer.js +0 -241
- package/dist/core/structural-hasher.js +0 -316
- package/dist/core/tree-sitter-parser.js +0 -539
- package/dist/core/types.js +0 -0
- package/dist/core/vector-store.js +0 -259
- package/dist/core/workspace-detector.js +0 -481
|
@@ -1,887 +0,0 @@
|
|
|
1
|
-
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { join, extname } from "node:path";
|
|
3
|
-
import { detectWorkspace } from "./workspace-detector.js";
|
|
4
|
-
function walkFiles(dir, match, maxFiles = 5e3) {
|
|
5
|
-
const results = [];
|
|
6
|
-
const skipDirs = /* @__PURE__ */ new Set(["node_modules", "dist", ".git", ".next", "build", "coverage", "__pycache__", ".venv", "vendor"]);
|
|
7
|
-
function walk(currentDir) {
|
|
8
|
-
if (results.length >= maxFiles) return;
|
|
9
|
-
let entries;
|
|
10
|
-
try {
|
|
11
|
-
entries = readdirSync(currentDir);
|
|
12
|
-
} catch {
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
for (const entry of entries) {
|
|
16
|
-
if (results.length >= maxFiles) return;
|
|
17
|
-
if (skipDirs.has(entry)) continue;
|
|
18
|
-
const fullPath = join(currentDir, entry);
|
|
19
|
-
try {
|
|
20
|
-
const stat = statSync(fullPath);
|
|
21
|
-
if (stat.isDirectory()) {
|
|
22
|
-
walk(fullPath);
|
|
23
|
-
} else if (stat.isFile() && match(fullPath)) {
|
|
24
|
-
results.push(fullPath);
|
|
25
|
-
}
|
|
26
|
-
} catch {
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
walk(dir);
|
|
31
|
-
return results;
|
|
32
|
-
}
|
|
33
|
-
function safeReadFile(filePath) {
|
|
34
|
-
try {
|
|
35
|
-
return readFileSync(filePath, "utf-8");
|
|
36
|
-
} catch {
|
|
37
|
-
return "";
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function detectSharedNpmDeps(repos) {
|
|
41
|
-
const edges = [];
|
|
42
|
-
const repoDeps = /* @__PURE__ */ new Map();
|
|
43
|
-
for (const repo of repos) {
|
|
44
|
-
const pkgPath = join(repo.path, "package.json");
|
|
45
|
-
const content = safeReadFile(pkgPath);
|
|
46
|
-
if (!content) continue;
|
|
47
|
-
try {
|
|
48
|
-
const pkg = JSON.parse(content);
|
|
49
|
-
const allDeps = /* @__PURE__ */ new Set([
|
|
50
|
-
...Object.keys(pkg.dependencies ?? {}),
|
|
51
|
-
...Object.keys(pkg.devDependencies ?? {}),
|
|
52
|
-
...Object.keys(pkg.peerDependencies ?? {})
|
|
53
|
-
]);
|
|
54
|
-
const orgDeps = new Set(
|
|
55
|
-
[...allDeps].filter((d) => d.startsWith("@"))
|
|
56
|
-
);
|
|
57
|
-
repoDeps.set(repo.name, orgDeps);
|
|
58
|
-
} catch {
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
const repoNames = [...repoDeps.keys()];
|
|
62
|
-
for (let i = 0; i < repoNames.length; i++) {
|
|
63
|
-
for (let j = i + 1; j < repoNames.length; j++) {
|
|
64
|
-
const nameA = repoNames[i];
|
|
65
|
-
const nameB = repoNames[j];
|
|
66
|
-
const depsA = repoDeps.get(nameA);
|
|
67
|
-
const depsB = repoDeps.get(nameB);
|
|
68
|
-
for (const dep of depsA) {
|
|
69
|
-
if (depsB.has(dep)) {
|
|
70
|
-
edges.push({
|
|
71
|
-
sourceRepo: nameA,
|
|
72
|
-
sourceNode: `package.json::${dep}`,
|
|
73
|
-
targetRepo: nameB,
|
|
74
|
-
targetNode: `package.json::${dep}`,
|
|
75
|
-
edgeType: "npm-dep",
|
|
76
|
-
evidence: `Shared scoped dependency: ${dep}`,
|
|
77
|
-
confidence: 0.7
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return edges;
|
|
84
|
-
}
|
|
85
|
-
function detectSharedTypes(repos) {
|
|
86
|
-
const edges = [];
|
|
87
|
-
const typeExportPattern = /export\s+(?:interface|type)\s+(\w+)/g;
|
|
88
|
-
const repoTypes = /* @__PURE__ */ new Map();
|
|
89
|
-
for (const repo of repos) {
|
|
90
|
-
const tsFiles = walkFiles(repo.path, (f) => {
|
|
91
|
-
const ext = extname(f);
|
|
92
|
-
return ext === ".ts" || ext === ".d.ts";
|
|
93
|
-
}, 2e3);
|
|
94
|
-
const typeMap = /* @__PURE__ */ new Map();
|
|
95
|
-
for (const filePath of tsFiles) {
|
|
96
|
-
const content = safeReadFile(filePath);
|
|
97
|
-
let match;
|
|
98
|
-
typeExportPattern.lastIndex = 0;
|
|
99
|
-
while ((match = typeExportPattern.exec(content)) !== null) {
|
|
100
|
-
const typeName = match[1];
|
|
101
|
-
if (!typeMap.has(typeName)) {
|
|
102
|
-
typeMap.set(typeName, filePath);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
repoTypes.set(repo.name, typeMap);
|
|
107
|
-
}
|
|
108
|
-
const repoNames = [...repoTypes.keys()];
|
|
109
|
-
for (let i = 0; i < repoNames.length; i++) {
|
|
110
|
-
for (let j = i + 1; j < repoNames.length; j++) {
|
|
111
|
-
const nameA = repoNames[i];
|
|
112
|
-
const nameB = repoNames[j];
|
|
113
|
-
const typesA = repoTypes.get(nameA);
|
|
114
|
-
const typesB = repoTypes.get(nameB);
|
|
115
|
-
for (const [typeName, fileA] of typesA) {
|
|
116
|
-
const fileB = typesB.get(typeName);
|
|
117
|
-
if (fileB) {
|
|
118
|
-
edges.push({
|
|
119
|
-
sourceRepo: nameA,
|
|
120
|
-
sourceNode: `${fileA}::${typeName}`,
|
|
121
|
-
targetRepo: nameB,
|
|
122
|
-
targetNode: `${fileB}::${typeName}`,
|
|
123
|
-
edgeType: "shared-type",
|
|
124
|
-
evidence: `Shared type: ${typeName}`,
|
|
125
|
-
confidence: 0.9
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return edges;
|
|
132
|
-
}
|
|
133
|
-
function detectEnvVarCorrelation(repos) {
|
|
134
|
-
const edges = [];
|
|
135
|
-
const envFileNames = [".env.example", ".env.template", ".env.sample"];
|
|
136
|
-
const envVarPattern = /^([A-Z][A-Z0-9_]{2,})=/gm;
|
|
137
|
-
const repoEnvVars = /* @__PURE__ */ new Map();
|
|
138
|
-
for (const repo of repos) {
|
|
139
|
-
const vars = /* @__PURE__ */ new Set();
|
|
140
|
-
for (const envFile of envFileNames) {
|
|
141
|
-
const content = safeReadFile(join(repo.path, envFile));
|
|
142
|
-
if (!content) continue;
|
|
143
|
-
let match;
|
|
144
|
-
envVarPattern.lastIndex = 0;
|
|
145
|
-
while ((match = envVarPattern.exec(content)) !== null) {
|
|
146
|
-
vars.add(match[1]);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (vars.size > 0) {
|
|
150
|
-
repoEnvVars.set(repo.name, vars);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
const repoNames = [...repoEnvVars.keys()];
|
|
154
|
-
for (let i = 0; i < repoNames.length; i++) {
|
|
155
|
-
for (let j = i + 1; j < repoNames.length; j++) {
|
|
156
|
-
const nameA = repoNames[i];
|
|
157
|
-
const nameB = repoNames[j];
|
|
158
|
-
const varsA = repoEnvVars.get(nameA);
|
|
159
|
-
const varsB = repoEnvVars.get(nameB);
|
|
160
|
-
for (const v of varsA) {
|
|
161
|
-
if (varsB.has(v)) {
|
|
162
|
-
edges.push({
|
|
163
|
-
sourceRepo: nameA,
|
|
164
|
-
sourceNode: `env::${v}`,
|
|
165
|
-
targetRepo: nameB,
|
|
166
|
-
targetNode: `env::${v}`,
|
|
167
|
-
edgeType: "env-var",
|
|
168
|
-
evidence: `Shared env var: ${v}`,
|
|
169
|
-
confidence: 0.6
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return edges;
|
|
176
|
-
}
|
|
177
|
-
function detectEventSchemas(repos) {
|
|
178
|
-
const edges = [];
|
|
179
|
-
const eventPatterns = [
|
|
180
|
-
/(?:topic|TOPIC|channel|CHANNEL|event|EVENT)\s*[:=]\s*['"`]([a-zA-Z0-9._-]+)['"`]/g,
|
|
181
|
-
/(?:subscribe|publish|emit|on)\s*\(\s*['"`]([a-zA-Z0-9._-]+)['"`]/g
|
|
182
|
-
];
|
|
183
|
-
const repoEvents = /* @__PURE__ */ new Map();
|
|
184
|
-
for (const repo of repos) {
|
|
185
|
-
const sourceFiles = walkFiles(repo.path, (f) => {
|
|
186
|
-
const ext = extname(f);
|
|
187
|
-
return [".ts", ".js", ".py", ".go", ".java"].includes(ext);
|
|
188
|
-
}, 3e3);
|
|
189
|
-
const eventMap = /* @__PURE__ */ new Map();
|
|
190
|
-
for (const filePath of sourceFiles) {
|
|
191
|
-
const content = safeReadFile(filePath);
|
|
192
|
-
for (const pattern of eventPatterns) {
|
|
193
|
-
pattern.lastIndex = 0;
|
|
194
|
-
let match;
|
|
195
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
196
|
-
const eventName = match[1];
|
|
197
|
-
if (eventName.length > 3 && eventName.includes(".")) {
|
|
198
|
-
if (!eventMap.has(eventName)) {
|
|
199
|
-
eventMap.set(eventName, filePath);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
repoEvents.set(repo.name, eventMap);
|
|
206
|
-
}
|
|
207
|
-
const repoNames = [...repoEvents.keys()];
|
|
208
|
-
for (let i = 0; i < repoNames.length; i++) {
|
|
209
|
-
for (let j = i + 1; j < repoNames.length; j++) {
|
|
210
|
-
const nameA = repoNames[i];
|
|
211
|
-
const nameB = repoNames[j];
|
|
212
|
-
const eventsA = repoEvents.get(nameA);
|
|
213
|
-
const eventsB = repoEvents.get(nameB);
|
|
214
|
-
for (const [eventName, fileA] of eventsA) {
|
|
215
|
-
const fileB = eventsB.get(eventName);
|
|
216
|
-
if (fileB) {
|
|
217
|
-
edges.push({
|
|
218
|
-
sourceRepo: nameA,
|
|
219
|
-
sourceNode: `${fileA}::${eventName}`,
|
|
220
|
-
targetRepo: nameB,
|
|
221
|
-
targetNode: `${fileB}::${eventName}`,
|
|
222
|
-
edgeType: "event-schema",
|
|
223
|
-
evidence: `Shared event/topic: ${eventName}`,
|
|
224
|
-
confidence: 0.85
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return edges;
|
|
231
|
-
}
|
|
232
|
-
function detectApiEndpoints(repos) {
|
|
233
|
-
const edges = [];
|
|
234
|
-
const routeDefPatterns = [
|
|
235
|
-
/(?:app|router)\.(get|post|put|patch|delete)\s*\(\s*['"`](\/[a-zA-Z0-9/:._-]+)['"`]/g,
|
|
236
|
-
/@(Get|Post|Put|Patch|Delete)\s*\(\s*['"`](\/[a-zA-Z0-9/:._-]+)['"`]/g
|
|
237
|
-
];
|
|
238
|
-
const httpCallPatterns = [
|
|
239
|
-
/(?:axios|fetch|http|client)\.(get|post|put|patch|delete)\s*\(\s*[`'"](?:https?:\/\/[^/]*?)?(\/[a-zA-Z0-9/:._-]+)['"`]/g,
|
|
240
|
-
/fetch\s*\(\s*[`'"](?:https?:\/\/[^/]*?)?(\/[a-zA-Z0-9/:._-]+)['"`]/g
|
|
241
|
-
];
|
|
242
|
-
const repoRoutes = /* @__PURE__ */ new Map();
|
|
243
|
-
const repoClients = /* @__PURE__ */ new Map();
|
|
244
|
-
for (const repo of repos) {
|
|
245
|
-
const sourceFiles = walkFiles(repo.path, (f) => {
|
|
246
|
-
const ext = extname(f);
|
|
247
|
-
return [".ts", ".js"].includes(ext);
|
|
248
|
-
}, 3e3);
|
|
249
|
-
const routes = /* @__PURE__ */ new Map();
|
|
250
|
-
const clients = /* @__PURE__ */ new Map();
|
|
251
|
-
for (const filePath of sourceFiles) {
|
|
252
|
-
const content = safeReadFile(filePath);
|
|
253
|
-
for (const pattern of routeDefPatterns) {
|
|
254
|
-
pattern.lastIndex = 0;
|
|
255
|
-
let match;
|
|
256
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
257
|
-
const routePath = match[2].replace(/:[a-zA-Z]+/g, ":param");
|
|
258
|
-
if (!routes.has(routePath)) {
|
|
259
|
-
routes.set(routePath, filePath);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
for (const pattern of httpCallPatterns) {
|
|
264
|
-
pattern.lastIndex = 0;
|
|
265
|
-
let match;
|
|
266
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
267
|
-
const clientPath = match[2].replace(/:[a-zA-Z]+/g, ":param");
|
|
268
|
-
if (!clients.has(clientPath)) {
|
|
269
|
-
clients.set(clientPath, filePath);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
repoRoutes.set(repo.name, routes);
|
|
275
|
-
repoClients.set(repo.name, clients);
|
|
276
|
-
}
|
|
277
|
-
for (const [routeRepo, routes] of repoRoutes) {
|
|
278
|
-
for (const [clientRepo, clients] of repoClients) {
|
|
279
|
-
if (routeRepo === clientRepo) continue;
|
|
280
|
-
for (const [routePath, routeFile] of routes) {
|
|
281
|
-
const clientFile = clients.get(routePath);
|
|
282
|
-
if (clientFile) {
|
|
283
|
-
edges.push({
|
|
284
|
-
sourceRepo: clientRepo,
|
|
285
|
-
sourceNode: `${clientFile}::${routePath}`,
|
|
286
|
-
targetRepo: routeRepo,
|
|
287
|
-
targetNode: `${routeFile}::${routePath}`,
|
|
288
|
-
edgeType: "http",
|
|
289
|
-
evidence: `API endpoint: ${routePath}`,
|
|
290
|
-
confidence: 0.8
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return edges;
|
|
297
|
-
}
|
|
298
|
-
function detectWorkspaceDeps(repos, workspaceMaps) {
|
|
299
|
-
const edges = [];
|
|
300
|
-
for (const repo of repos) {
|
|
301
|
-
const wsMap = workspaceMaps?.get(repo.name) ?? detectWorkspace(repo.path);
|
|
302
|
-
if (wsMap.packages.length < 2) continue;
|
|
303
|
-
for (const pkg of wsMap.packages) {
|
|
304
|
-
for (const depName of pkg.dependencies) {
|
|
305
|
-
const target = wsMap.nameToPackage.get(depName);
|
|
306
|
-
if (target && target.name !== pkg.name) {
|
|
307
|
-
edges.push({
|
|
308
|
-
sourceRepo: repo.name,
|
|
309
|
-
sourceNode: `pkg::${pkg.name}`,
|
|
310
|
-
targetRepo: repo.name,
|
|
311
|
-
targetNode: `pkg::${target.name}`,
|
|
312
|
-
edgeType: "workspace-dep",
|
|
313
|
-
evidence: `${pkg.name} depends on ${target.name} (${pkg.manifestFile})`,
|
|
314
|
-
confidence: 1
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
return edges;
|
|
321
|
-
}
|
|
322
|
-
function detectGrpcServices(repos) {
|
|
323
|
-
const edges = [];
|
|
324
|
-
const serviceDefPattern = /service\s+(\w+)\s*\{[^}]*rpc\s+(\w+)/gs;
|
|
325
|
-
const tsClientPattern = /import\s*\{[^}]*(\w+)Client[^}]*\}\s*from\s*['"][^'"]*_grpc_pb['"]/g;
|
|
326
|
-
const goClientPattern = /pb\.New(\w+)Client\s*\(/g;
|
|
327
|
-
const repoServiceDefs = /* @__PURE__ */ new Map();
|
|
328
|
-
const repoServiceClients = /* @__PURE__ */ new Map();
|
|
329
|
-
for (const repo of repos) {
|
|
330
|
-
const protoFiles = walkFiles(repo.path, (f) => extname(f) === ".proto", 500);
|
|
331
|
-
const serviceDefs = /* @__PURE__ */ new Map();
|
|
332
|
-
for (const filePath of protoFiles) {
|
|
333
|
-
const content = safeReadFile(filePath);
|
|
334
|
-
serviceDefPattern.lastIndex = 0;
|
|
335
|
-
let match;
|
|
336
|
-
while ((match = serviceDefPattern.exec(content)) !== null) {
|
|
337
|
-
serviceDefs.set(match[1], filePath);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
repoServiceDefs.set(repo.name, serviceDefs);
|
|
341
|
-
const sourceFiles = walkFiles(repo.path, (f) => {
|
|
342
|
-
const ext = extname(f);
|
|
343
|
-
return [".ts", ".js", ".go"].includes(ext);
|
|
344
|
-
}, 3e3);
|
|
345
|
-
const serviceClients = /* @__PURE__ */ new Map();
|
|
346
|
-
for (const filePath of sourceFiles) {
|
|
347
|
-
const content = safeReadFile(filePath);
|
|
348
|
-
tsClientPattern.lastIndex = 0;
|
|
349
|
-
let match;
|
|
350
|
-
while ((match = tsClientPattern.exec(content)) !== null) {
|
|
351
|
-
serviceClients.set(match[1], filePath);
|
|
352
|
-
}
|
|
353
|
-
goClientPattern.lastIndex = 0;
|
|
354
|
-
while ((match = goClientPattern.exec(content)) !== null) {
|
|
355
|
-
serviceClients.set(match[1], filePath);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
repoServiceClients.set(repo.name, serviceClients);
|
|
359
|
-
}
|
|
360
|
-
for (const [defRepo, defs] of repoServiceDefs) {
|
|
361
|
-
for (const [clientRepo, clients] of repoServiceClients) {
|
|
362
|
-
if (defRepo === clientRepo) continue;
|
|
363
|
-
for (const [serviceName, defFile] of defs) {
|
|
364
|
-
const clientFile = clients.get(serviceName);
|
|
365
|
-
if (clientFile) {
|
|
366
|
-
edges.push({
|
|
367
|
-
sourceRepo: clientRepo,
|
|
368
|
-
sourceNode: `${clientFile}::${serviceName}Client`,
|
|
369
|
-
targetRepo: defRepo,
|
|
370
|
-
targetNode: `${defFile}::${serviceName}`,
|
|
371
|
-
edgeType: "grpc",
|
|
372
|
-
evidence: `gRPC service: ${serviceName}`,
|
|
373
|
-
confidence: 0.9
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
return edges;
|
|
380
|
-
}
|
|
381
|
-
function detectDatabaseSchemas(repos) {
|
|
382
|
-
const edges = [];
|
|
383
|
-
const sqlCreatePattern = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)["`]?/gi;
|
|
384
|
-
const sqlQueryPattern = /(?:FROM|INTO|JOIN|UPDATE)\s+["`]?(\w+)["`]?/gi;
|
|
385
|
-
const typeormPattern = /@Entity\s*\(\s*['"`](\w+)['"`]\s*\)/g;
|
|
386
|
-
const gormPattern = /TableName\s*\(\s*\)\s*string\s*\{\s*return\s*["'](\w+)["']/g;
|
|
387
|
-
const sequelizePattern = /sequelize\.define\s*\(\s*['"`](\w+)['"`]/g;
|
|
388
|
-
const repoTables = /* @__PURE__ */ new Map();
|
|
389
|
-
for (const repo of repos) {
|
|
390
|
-
const tableMap = /* @__PURE__ */ new Map();
|
|
391
|
-
const sqlFiles = walkFiles(repo.path, (f) => {
|
|
392
|
-
const lower = f.toLowerCase();
|
|
393
|
-
return lower.endsWith(".sql") || lower.includes("migration");
|
|
394
|
-
}, 1e3);
|
|
395
|
-
const sourceFiles = walkFiles(repo.path, (f) => {
|
|
396
|
-
const ext = extname(f);
|
|
397
|
-
return [".ts", ".js", ".go", ".py", ".java"].includes(ext);
|
|
398
|
-
}, 3e3);
|
|
399
|
-
const allFiles = [...sqlFiles, ...sourceFiles];
|
|
400
|
-
const patterns = [sqlCreatePattern, sqlQueryPattern, typeormPattern, gormPattern, sequelizePattern];
|
|
401
|
-
const skipTables = /* @__PURE__ */ new Set(["information_schema", "pg_catalog", "dual", "sqlite_master", "schema_migrations"]);
|
|
402
|
-
for (const filePath of allFiles) {
|
|
403
|
-
const content = safeReadFile(filePath);
|
|
404
|
-
for (const pattern of patterns) {
|
|
405
|
-
pattern.lastIndex = 0;
|
|
406
|
-
let match;
|
|
407
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
408
|
-
const tableName = match[1].toLowerCase();
|
|
409
|
-
if (tableName.length > 2 && !skipTables.has(tableName)) {
|
|
410
|
-
if (!tableMap.has(tableName)) {
|
|
411
|
-
tableMap.set(tableName, filePath);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
repoTables.set(repo.name, tableMap);
|
|
418
|
-
}
|
|
419
|
-
const repoNames = [...repoTables.keys()];
|
|
420
|
-
for (let i = 0; i < repoNames.length; i++) {
|
|
421
|
-
for (let j = i + 1; j < repoNames.length; j++) {
|
|
422
|
-
const nameA = repoNames[i];
|
|
423
|
-
const nameB = repoNames[j];
|
|
424
|
-
const tablesA = repoTables.get(nameA);
|
|
425
|
-
const tablesB = repoTables.get(nameB);
|
|
426
|
-
for (const [tableName, fileA] of tablesA) {
|
|
427
|
-
const fileB = tablesB.get(tableName);
|
|
428
|
-
if (fileB) {
|
|
429
|
-
edges.push({
|
|
430
|
-
sourceRepo: nameA,
|
|
431
|
-
sourceNode: `${fileA}::${tableName}`,
|
|
432
|
-
targetRepo: nameB,
|
|
433
|
-
targetNode: `${fileB}::${tableName}`,
|
|
434
|
-
edgeType: "database",
|
|
435
|
-
evidence: `Shared table: ${tableName}`,
|
|
436
|
-
confidence: 0.85
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return edges;
|
|
443
|
-
}
|
|
444
|
-
function detectRedisKeyPatterns(repos) {
|
|
445
|
-
const edges = [];
|
|
446
|
-
const redisPatterns = [
|
|
447
|
-
/(?:redis|cache|rdb)\.(get|set|del|hget|hset|sadd|srem)\s*\(\s*(?:ctx,\s*)?['"`]([a-zA-Z0-9_:-]+)/g,
|
|
448
|
-
/(?:redis|cache|rdb)\.(?:Get|Set|Del|HGet|HSet)\s*\(\s*ctx\s*,\s*["']([a-zA-Z0-9_:-]+)/g
|
|
449
|
-
];
|
|
450
|
-
const repoKeyPrefixes = /* @__PURE__ */ new Map();
|
|
451
|
-
for (const repo of repos) {
|
|
452
|
-
const sourceFiles = walkFiles(repo.path, (f) => {
|
|
453
|
-
const ext = extname(f);
|
|
454
|
-
return [".ts", ".js", ".go", ".py", ".java"].includes(ext);
|
|
455
|
-
}, 3e3);
|
|
456
|
-
const prefixMap = /* @__PURE__ */ new Map();
|
|
457
|
-
for (const filePath of sourceFiles) {
|
|
458
|
-
const content = safeReadFile(filePath);
|
|
459
|
-
for (const pattern of redisPatterns) {
|
|
460
|
-
pattern.lastIndex = 0;
|
|
461
|
-
let match;
|
|
462
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
463
|
-
const key = match[2] ?? match[1];
|
|
464
|
-
const colonParts = key.split(":");
|
|
465
|
-
const prefix = colonParts.length >= 2 ? `${colonParts[0]}:${colonParts[1]}` : key;
|
|
466
|
-
if (prefix.length > 3 && !prefixMap.has(prefix)) {
|
|
467
|
-
prefixMap.set(prefix, filePath);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
repoKeyPrefixes.set(repo.name, prefixMap);
|
|
473
|
-
}
|
|
474
|
-
const repoNames = [...repoKeyPrefixes.keys()];
|
|
475
|
-
for (let i = 0; i < repoNames.length; i++) {
|
|
476
|
-
for (let j = i + 1; j < repoNames.length; j++) {
|
|
477
|
-
const nameA = repoNames[i];
|
|
478
|
-
const nameB = repoNames[j];
|
|
479
|
-
const prefixesA = repoKeyPrefixes.get(nameA);
|
|
480
|
-
const prefixesB = repoKeyPrefixes.get(nameB);
|
|
481
|
-
for (const [prefix, fileA] of prefixesA) {
|
|
482
|
-
const fileB = prefixesB.get(prefix);
|
|
483
|
-
if (fileB) {
|
|
484
|
-
edges.push({
|
|
485
|
-
sourceRepo: nameA,
|
|
486
|
-
sourceNode: `${fileA}::redis:${prefix}`,
|
|
487
|
-
targetRepo: nameB,
|
|
488
|
-
targetNode: `${fileB}::redis:${prefix}`,
|
|
489
|
-
edgeType: "redis",
|
|
490
|
-
evidence: `Shared Redis key prefix: ${prefix}`,
|
|
491
|
-
confidence: 0.75
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
return edges;
|
|
498
|
-
}
|
|
499
|
-
function detectS3Buckets(repos) {
|
|
500
|
-
const edges = [];
|
|
501
|
-
const s3Patterns = [
|
|
502
|
-
/s3\.(?:putObject|getObject|deleteObject|upload|headObject)\s*\(\s*\{[^}]*Bucket\s*:\s*['"`]([a-zA-Z0-9._-]+)['"`]/gs,
|
|
503
|
-
/new\s+(?:S3|AWS\.S3)\s*\([^)]*\)[^;]*\.(?:putObject|getObject)\s*\(\s*\{[^}]*Bucket\s*:\s*['"`]([a-zA-Z0-9._-]+)['"`]/gs,
|
|
504
|
-
/storage\.bucket\s*\(\s*['"`]([a-zA-Z0-9._-]+)['"`]\s*\)/g
|
|
505
|
-
];
|
|
506
|
-
const envBucketPattern = /(?:S3_BUCKET|GCS_BUCKET|BUCKET_NAME|AWS_BUCKET)\s*=\s*['"`]?([a-zA-Z0-9._-]+)['"`]?/g;
|
|
507
|
-
const repoBuckets = /* @__PURE__ */ new Map();
|
|
508
|
-
for (const repo of repos) {
|
|
509
|
-
const bucketMap = /* @__PURE__ */ new Map();
|
|
510
|
-
const sourceFiles = walkFiles(repo.path, (f) => {
|
|
511
|
-
const ext = extname(f);
|
|
512
|
-
return [".ts", ".js", ".go", ".py", ".java", ".env", ".yaml", ".yml"].includes(ext) || f.endsWith(".env.example") || f.endsWith(".env.template");
|
|
513
|
-
}, 3e3);
|
|
514
|
-
for (const filePath of sourceFiles) {
|
|
515
|
-
const content = safeReadFile(filePath);
|
|
516
|
-
for (const pattern of s3Patterns) {
|
|
517
|
-
pattern.lastIndex = 0;
|
|
518
|
-
let match2;
|
|
519
|
-
while ((match2 = pattern.exec(content)) !== null) {
|
|
520
|
-
const bucket = match2[1];
|
|
521
|
-
if (bucket && !bucketMap.has(bucket)) {
|
|
522
|
-
bucketMap.set(bucket, filePath);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
envBucketPattern.lastIndex = 0;
|
|
527
|
-
let match;
|
|
528
|
-
while ((match = envBucketPattern.exec(content)) !== null) {
|
|
529
|
-
const bucket = match[1];
|
|
530
|
-
if (bucket && bucket.length > 3 && !bucketMap.has(bucket)) {
|
|
531
|
-
bucketMap.set(bucket, filePath);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
repoBuckets.set(repo.name, bucketMap);
|
|
536
|
-
}
|
|
537
|
-
const repoNames = [...repoBuckets.keys()];
|
|
538
|
-
for (let i = 0; i < repoNames.length; i++) {
|
|
539
|
-
for (let j = i + 1; j < repoNames.length; j++) {
|
|
540
|
-
const nameA = repoNames[i];
|
|
541
|
-
const nameB = repoNames[j];
|
|
542
|
-
const bucketsA = repoBuckets.get(nameA);
|
|
543
|
-
const bucketsB = repoBuckets.get(nameB);
|
|
544
|
-
for (const [bucket, fileA] of bucketsA) {
|
|
545
|
-
const fileB = bucketsB.get(bucket);
|
|
546
|
-
if (fileB) {
|
|
547
|
-
edges.push({
|
|
548
|
-
sourceRepo: nameA,
|
|
549
|
-
sourceNode: `${fileA}::s3:${bucket}`,
|
|
550
|
-
targetRepo: nameB,
|
|
551
|
-
targetNode: `${fileB}::s3:${bucket}`,
|
|
552
|
-
edgeType: "s3",
|
|
553
|
-
evidence: `Shared bucket: ${bucket}`,
|
|
554
|
-
confidence: 0.8
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
return edges;
|
|
561
|
-
}
|
|
562
|
-
function detectOpenApiSchemas(repos) {
|
|
563
|
-
const edges = [];
|
|
564
|
-
const openApiFileNames = ["openapi.yaml", "openapi.yml", "openapi.json", "swagger.yaml", "swagger.yml", "swagger.json"];
|
|
565
|
-
const pathPattern = /^\s*(?:\/[a-zA-Z0-9/:._{}*-]+)\s*:/gm;
|
|
566
|
-
const jsonPathPattern = /"(\/[a-zA-Z0-9/:._{}*-]+)"\s*:/g;
|
|
567
|
-
const repoApiPaths = /* @__PURE__ */ new Map();
|
|
568
|
-
const httpCallPatterns = [
|
|
569
|
-
/(?:axios|fetch|http|client|httpGet|httpPost|httpPut|httpDelete)\s*\(\s*[^,]*?,?\s*['"`](\/[a-zA-Z0-9/:._-]+)['"`]/g,
|
|
570
|
-
/fetch\s*\(\s*[`'"](?:https?:\/\/[^/]*?)?(\/[a-zA-Z0-9/:._-]+)['"`]/g
|
|
571
|
-
];
|
|
572
|
-
const repoHttpCalls = /* @__PURE__ */ new Map();
|
|
573
|
-
for (const repo of repos) {
|
|
574
|
-
const apiPaths = /* @__PURE__ */ new Map();
|
|
575
|
-
const specFiles = walkFiles(repo.path, (f) => {
|
|
576
|
-
const basename = f.split("/").pop() ?? "";
|
|
577
|
-
return openApiFileNames.includes(basename);
|
|
578
|
-
}, 200);
|
|
579
|
-
for (const filePath of specFiles) {
|
|
580
|
-
const content = safeReadFile(filePath);
|
|
581
|
-
if (filePath.endsWith(".json")) {
|
|
582
|
-
jsonPathPattern.lastIndex = 0;
|
|
583
|
-
let match;
|
|
584
|
-
while ((match = jsonPathPattern.exec(content)) !== null) {
|
|
585
|
-
const path = match[1].replace(/\{[^}]+\}/g, ":param");
|
|
586
|
-
if (!apiPaths.has(path)) {
|
|
587
|
-
apiPaths.set(path, filePath);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
} else {
|
|
591
|
-
pathPattern.lastIndex = 0;
|
|
592
|
-
let match;
|
|
593
|
-
while ((match = pathPattern.exec(content)) !== null) {
|
|
594
|
-
const rawPath = match[0].trim().replace(/:$/, "").trim();
|
|
595
|
-
if (rawPath.startsWith("/")) {
|
|
596
|
-
const normalized = rawPath.replace(/\{[^}]+\}/g, ":param");
|
|
597
|
-
if (!apiPaths.has(normalized)) {
|
|
598
|
-
apiPaths.set(normalized, filePath);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
repoApiPaths.set(repo.name, apiPaths);
|
|
605
|
-
const sourceFiles = walkFiles(repo.path, (f) => {
|
|
606
|
-
const ext = extname(f);
|
|
607
|
-
return [".ts", ".js"].includes(ext);
|
|
608
|
-
}, 3e3);
|
|
609
|
-
const httpCalls = /* @__PURE__ */ new Map();
|
|
610
|
-
for (const filePath of sourceFiles) {
|
|
611
|
-
const content = safeReadFile(filePath);
|
|
612
|
-
for (const pattern of httpCallPatterns) {
|
|
613
|
-
pattern.lastIndex = 0;
|
|
614
|
-
let match;
|
|
615
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
616
|
-
const callPath = (match[2] ?? match[1]).replace(/:[a-zA-Z]+/g, ":param");
|
|
617
|
-
if (!httpCalls.has(callPath)) {
|
|
618
|
-
httpCalls.set(callPath, filePath);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
repoHttpCalls.set(repo.name, httpCalls);
|
|
624
|
-
}
|
|
625
|
-
for (const [specRepo, apiPaths] of repoApiPaths) {
|
|
626
|
-
for (const [clientRepo, httpCalls] of repoHttpCalls) {
|
|
627
|
-
if (specRepo === clientRepo) continue;
|
|
628
|
-
for (const [apiPath, specFile] of apiPaths) {
|
|
629
|
-
const clientFile = httpCalls.get(apiPath);
|
|
630
|
-
if (clientFile) {
|
|
631
|
-
edges.push({
|
|
632
|
-
sourceRepo: clientRepo,
|
|
633
|
-
sourceNode: `${clientFile}::${apiPath}`,
|
|
634
|
-
targetRepo: specRepo,
|
|
635
|
-
targetNode: `${specFile}::${apiPath}`,
|
|
636
|
-
edgeType: "api-contract",
|
|
637
|
-
evidence: `OpenAPI endpoint: ${apiPath}`,
|
|
638
|
-
confidence: 0.9
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
return edges;
|
|
645
|
-
}
|
|
646
|
-
function detectDockerComposeLinks(repos) {
|
|
647
|
-
const edges = [];
|
|
648
|
-
const composeFiles = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
|
|
649
|
-
const serviceBlockPattern = /^\s{2}(\w[\w.-]*)\s*:/gm;
|
|
650
|
-
const dependsOnPattern = /depends_on\s*:\s*\n((?:\s+-\s+\w[\w.-]*\n?)+)/gm;
|
|
651
|
-
const dependsOnItemPattern = /^\s+-\s+(\w[\w.-]*)/gm;
|
|
652
|
-
const linksPattern = /links\s*:\s*\n((?:\s+-\s+\w[\w.-]*(?::\w[\w.-]*)?\n?)+)/gm;
|
|
653
|
-
const linksItemPattern = /^\s+-\s+(\w[\w.-]*)/gm;
|
|
654
|
-
function normalizeServiceName(name) {
|
|
655
|
-
return name.toLowerCase().replace(/[-_.]/g, "-");
|
|
656
|
-
}
|
|
657
|
-
const repoNameMap = /* @__PURE__ */ new Map();
|
|
658
|
-
for (const repo of repos) {
|
|
659
|
-
repoNameMap.set(normalizeServiceName(repo.name), repo.name);
|
|
660
|
-
const parts = repo.name.split(/[-_]/);
|
|
661
|
-
if (parts.length > 1) {
|
|
662
|
-
for (let k = 1; k < parts.length; k++) {
|
|
663
|
-
const suffix = parts.slice(k).join("-");
|
|
664
|
-
if (suffix.length > 3) {
|
|
665
|
-
repoNameMap.set(normalizeServiceName(suffix), repo.name);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
for (const repo of repos) {
|
|
671
|
-
const foundFiles = walkFiles(repo.path, (f) => {
|
|
672
|
-
const basename = f.split("/").pop() ?? "";
|
|
673
|
-
return composeFiles.includes(basename);
|
|
674
|
-
}, 50);
|
|
675
|
-
for (const filePath of foundFiles) {
|
|
676
|
-
const content = safeReadFile(filePath);
|
|
677
|
-
serviceBlockPattern.lastIndex = 0;
|
|
678
|
-
const services = /* @__PURE__ */ new Set();
|
|
679
|
-
let match;
|
|
680
|
-
while ((match = serviceBlockPattern.exec(content)) !== null) {
|
|
681
|
-
services.add(match[1]);
|
|
682
|
-
}
|
|
683
|
-
dependsOnPattern.lastIndex = 0;
|
|
684
|
-
while ((match = dependsOnPattern.exec(content)) !== null) {
|
|
685
|
-
const block = match[1];
|
|
686
|
-
dependsOnItemPattern.lastIndex = 0;
|
|
687
|
-
let dep;
|
|
688
|
-
while ((dep = dependsOnItemPattern.exec(block)) !== null) {
|
|
689
|
-
const depService = dep[1];
|
|
690
|
-
const normalizedDep = normalizeServiceName(depService);
|
|
691
|
-
const targetRepo = repoNameMap.get(normalizedDep);
|
|
692
|
-
if (targetRepo && targetRepo !== repo.name) {
|
|
693
|
-
edges.push({
|
|
694
|
-
sourceRepo: repo.name,
|
|
695
|
-
sourceNode: `${filePath}::compose`,
|
|
696
|
-
targetRepo,
|
|
697
|
-
targetNode: `service::${depService}`,
|
|
698
|
-
edgeType: "http",
|
|
699
|
-
evidence: `Docker Compose depends_on: ${depService}`,
|
|
700
|
-
confidence: 0.85
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
linksPattern.lastIndex = 0;
|
|
706
|
-
while ((match = linksPattern.exec(content)) !== null) {
|
|
707
|
-
const block = match[1];
|
|
708
|
-
linksItemPattern.lastIndex = 0;
|
|
709
|
-
let link;
|
|
710
|
-
while ((link = linksItemPattern.exec(block)) !== null) {
|
|
711
|
-
const linkService = link[1];
|
|
712
|
-
const normalizedLink = normalizeServiceName(linkService);
|
|
713
|
-
const targetRepo = repoNameMap.get(normalizedLink);
|
|
714
|
-
if (targetRepo && targetRepo !== repo.name) {
|
|
715
|
-
edges.push({
|
|
716
|
-
sourceRepo: repo.name,
|
|
717
|
-
sourceNode: `${filePath}::compose`,
|
|
718
|
-
targetRepo,
|
|
719
|
-
targetNode: `service::${linkService}`,
|
|
720
|
-
edgeType: "http",
|
|
721
|
-
evidence: `Docker Compose link: ${linkService}`,
|
|
722
|
-
confidence: 0.85
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
return edges;
|
|
730
|
-
}
|
|
731
|
-
function detectK8sServiceRefs(repos) {
|
|
732
|
-
const edges = [];
|
|
733
|
-
const k8sServicePattern = /kind:\s*Service\s[\s\S]*?name:\s*["']?(\w[\w.-]*)["']?/gm;
|
|
734
|
-
const k8sDeployPattern = /kind:\s*Deployment\s[\s\S]*?name:\s*["']?(\w[\w.-]*)["']?/gm;
|
|
735
|
-
const svcRefPattern = /["'](\w[\w.-]*)\.(\w[\w.-]*)\.svc(?:\.cluster\.local)?["']/g;
|
|
736
|
-
const envServiceUrlPattern = /(?:SERVICE_HOST|SERVICE_URL|_HOST|_URL)\s*:\s*["']?(\w[\w.-]*)(?:\.[\w.-]+)?\.svc/g;
|
|
737
|
-
function normalizeK8sName(name) {
|
|
738
|
-
return name.toLowerCase().replace(/[-_.]/g, "-");
|
|
739
|
-
}
|
|
740
|
-
const repoNameMap = /* @__PURE__ */ new Map();
|
|
741
|
-
for (const repo of repos) {
|
|
742
|
-
repoNameMap.set(normalizeK8sName(repo.name), repo.name);
|
|
743
|
-
const parts = repo.name.split(/[-_]/);
|
|
744
|
-
if (parts.length > 1) {
|
|
745
|
-
for (let k = 1; k < parts.length; k++) {
|
|
746
|
-
const suffix = parts.slice(k).join("-");
|
|
747
|
-
if (suffix.length > 3) {
|
|
748
|
-
repoNameMap.set(normalizeK8sName(suffix), repo.name);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
for (const repo of repos) {
|
|
754
|
-
const k8sFiles = walkFiles(repo.path, (f) => {
|
|
755
|
-
const ext = extname(f);
|
|
756
|
-
if (ext !== ".yaml" && ext !== ".yml") return false;
|
|
757
|
-
const lower = f.toLowerCase();
|
|
758
|
-
return lower.includes("k8s") || lower.includes("deploy") || lower.includes("manifests") || lower.includes("kustomiz") || lower.includes("kubernetes") || lower.includes("helm");
|
|
759
|
-
}, 500);
|
|
760
|
-
for (const filePath of k8sFiles) {
|
|
761
|
-
const content = safeReadFile(filePath);
|
|
762
|
-
svcRefPattern.lastIndex = 0;
|
|
763
|
-
let match;
|
|
764
|
-
while ((match = svcRefPattern.exec(content)) !== null) {
|
|
765
|
-
const svcName = match[1];
|
|
766
|
-
const normalized = normalizeK8sName(svcName);
|
|
767
|
-
const targetRepo = repoNameMap.get(normalized);
|
|
768
|
-
if (targetRepo && targetRepo !== repo.name) {
|
|
769
|
-
edges.push({
|
|
770
|
-
sourceRepo: repo.name,
|
|
771
|
-
sourceNode: `${filePath}::k8s-ref`,
|
|
772
|
-
targetRepo,
|
|
773
|
-
targetNode: `k8s-service::${svcName}`,
|
|
774
|
-
edgeType: "http",
|
|
775
|
-
evidence: `K8s service DNS: ${svcName}.${match[2]}.svc`,
|
|
776
|
-
confidence: 0.8
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
envServiceUrlPattern.lastIndex = 0;
|
|
781
|
-
while ((match = envServiceUrlPattern.exec(content)) !== null) {
|
|
782
|
-
const svcName = match[1];
|
|
783
|
-
const normalized = normalizeK8sName(svcName);
|
|
784
|
-
const targetRepo = repoNameMap.get(normalized);
|
|
785
|
-
if (targetRepo && targetRepo !== repo.name) {
|
|
786
|
-
edges.push({
|
|
787
|
-
sourceRepo: repo.name,
|
|
788
|
-
sourceNode: `${filePath}::k8s-env`,
|
|
789
|
-
targetRepo,
|
|
790
|
-
targetNode: `k8s-service::${svcName}`,
|
|
791
|
-
edgeType: "http",
|
|
792
|
-
evidence: `K8s env service ref: ${svcName}`,
|
|
793
|
-
confidence: 0.8
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
return edges;
|
|
800
|
-
}
|
|
801
|
-
function detectSharedConstants(repos) {
|
|
802
|
-
const edges = [];
|
|
803
|
-
const tsConstPattern = /export\s+const\s+(\w+)\s*=\s*['"`]([^'"`]+)['"`]/g;
|
|
804
|
-
const goConstPattern = /(?:const|var)\s+(\w+)\s*=\s*["']([^"']+)["']/g;
|
|
805
|
-
function isInterestingConstant(value) {
|
|
806
|
-
if (value.length < 8) return false;
|
|
807
|
-
return /[./:]+/.test(value);
|
|
808
|
-
}
|
|
809
|
-
const repoConstants = /* @__PURE__ */ new Map();
|
|
810
|
-
for (const repo of repos) {
|
|
811
|
-
const sourceFiles = walkFiles(repo.path, (f) => {
|
|
812
|
-
const ext = extname(f);
|
|
813
|
-
return [".ts", ".js", ".go"].includes(ext);
|
|
814
|
-
}, 3e3);
|
|
815
|
-
const constMap = /* @__PURE__ */ new Map();
|
|
816
|
-
for (const filePath of sourceFiles) {
|
|
817
|
-
const content = safeReadFile(filePath);
|
|
818
|
-
const patterns = [tsConstPattern, goConstPattern];
|
|
819
|
-
for (const pattern of patterns) {
|
|
820
|
-
pattern.lastIndex = 0;
|
|
821
|
-
let match;
|
|
822
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
823
|
-
const constName = match[1];
|
|
824
|
-
const constValue = match[2];
|
|
825
|
-
if (isInterestingConstant(constValue) && !constMap.has(constValue)) {
|
|
826
|
-
constMap.set(constValue, { name: constName, filePath });
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
repoConstants.set(repo.name, constMap);
|
|
832
|
-
}
|
|
833
|
-
const repoNames = [...repoConstants.keys()];
|
|
834
|
-
for (let i = 0; i < repoNames.length; i++) {
|
|
835
|
-
for (let j = i + 1; j < repoNames.length; j++) {
|
|
836
|
-
const nameA = repoNames[i];
|
|
837
|
-
const nameB = repoNames[j];
|
|
838
|
-
const constsA = repoConstants.get(nameA);
|
|
839
|
-
const constsB = repoConstants.get(nameB);
|
|
840
|
-
for (const [value, infoA] of constsA) {
|
|
841
|
-
const infoB = constsB.get(value);
|
|
842
|
-
if (infoB) {
|
|
843
|
-
edges.push({
|
|
844
|
-
sourceRepo: nameA,
|
|
845
|
-
sourceNode: `${infoA.filePath}::${infoA.name}`,
|
|
846
|
-
targetRepo: nameB,
|
|
847
|
-
targetNode: `${infoB.filePath}::${infoB.name}`,
|
|
848
|
-
edgeType: "shared-constant",
|
|
849
|
-
evidence: `Shared constant value: "${value}" (${infoA.name} / ${infoB.name})`,
|
|
850
|
-
confidence: 0.7
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
return edges;
|
|
857
|
-
}
|
|
858
|
-
async function detectCrossRepoEdges(repos, workspaceMaps) {
|
|
859
|
-
const allEdges = [];
|
|
860
|
-
const strategies = [
|
|
861
|
-
{ name: "shared-npm-deps", fn: () => detectSharedNpmDeps(repos) },
|
|
862
|
-
{ name: "shared-types", fn: () => detectSharedTypes(repos) },
|
|
863
|
-
{ name: "env-var-correlation", fn: () => detectEnvVarCorrelation(repos) },
|
|
864
|
-
{ name: "event-schemas", fn: () => detectEventSchemas(repos) },
|
|
865
|
-
{ name: "api-endpoints", fn: () => detectApiEndpoints(repos) },
|
|
866
|
-
{ name: "workspace-deps", fn: () => detectWorkspaceDeps(repos, workspaceMaps) },
|
|
867
|
-
{ name: "grpc-services", fn: () => detectGrpcServices(repos) },
|
|
868
|
-
{ name: "database-schemas", fn: () => detectDatabaseSchemas(repos) },
|
|
869
|
-
{ name: "redis-key-patterns", fn: () => detectRedisKeyPatterns(repos) },
|
|
870
|
-
{ name: "s3-buckets", fn: () => detectS3Buckets(repos) },
|
|
871
|
-
{ name: "openapi-schemas", fn: () => detectOpenApiSchemas(repos) },
|
|
872
|
-
{ name: "docker-compose-links", fn: () => detectDockerComposeLinks(repos) },
|
|
873
|
-
{ name: "k8s-service-refs", fn: () => detectK8sServiceRefs(repos) },
|
|
874
|
-
{ name: "shared-constants", fn: () => detectSharedConstants(repos) }
|
|
875
|
-
];
|
|
876
|
-
for (const strategy of strategies) {
|
|
877
|
-
try {
|
|
878
|
-
const edges = strategy.fn();
|
|
879
|
-
allEdges.push(...edges);
|
|
880
|
-
} catch {
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
return allEdges;
|
|
884
|
-
}
|
|
885
|
-
export {
|
|
886
|
-
detectCrossRepoEdges
|
|
887
|
-
};
|