gitnexus 1.5.2 → 1.6.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 +10 -0
- package/dist/_shared/graph/types.d.ts +1 -1
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +1 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/language-detection.d.ts.map +1 -1
- package/dist/_shared/language-detection.js +2 -0
- package/dist/_shared/language-detection.js.map +1 -1
- package/dist/_shared/languages.d.ts +1 -0
- package/dist/_shared/languages.d.ts.map +1 -1
- package/dist/_shared/languages.js +1 -0
- package/dist/_shared/languages.js.map +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
- package/dist/_shared/lbug/schema-constants.js +3 -1
- package/dist/_shared/lbug/schema-constants.js.map +1 -1
- package/dist/_shared/mro-strategy.d.ts +19 -0
- package/dist/_shared/mro-strategy.d.ts.map +1 -0
- package/dist/_shared/mro-strategy.js +2 -0
- package/dist/_shared/mro-strategy.js.map +1 -0
- package/dist/cli/ai-context.d.ts +1 -0
- package/dist/cli/ai-context.js +28 -4
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +2 -1
- package/dist/cli/group.d.ts +2 -0
- package/dist/cli/group.js +233 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/serve.js +4 -1
- package/dist/cli/setup.js +34 -3
- package/dist/cli/wiki.js +15 -44
- package/dist/config/ignore-service.js +8 -3
- package/dist/core/augmentation/engine.js +1 -1
- package/dist/core/git-staleness.d.ts +13 -0
- package/dist/core/git-staleness.js +29 -0
- package/dist/core/group/bridge-db.d.ts +82 -0
- package/dist/core/group/bridge-db.js +460 -0
- package/dist/core/group/bridge-schema.d.ts +27 -0
- package/dist/core/group/bridge-schema.js +55 -0
- package/dist/core/group/config-parser.d.ts +3 -0
- package/dist/core/group/config-parser.js +83 -0
- package/dist/core/group/contract-extractor.d.ts +7 -0
- package/dist/core/group/contract-extractor.js +1 -0
- package/dist/core/group/extractors/grpc-extractor.d.ts +16 -0
- package/dist/core/group/extractors/grpc-extractor.js +264 -0
- package/dist/core/group/extractors/http-route-extractor.d.ts +24 -0
- package/dist/core/group/extractors/http-route-extractor.js +428 -0
- package/dist/core/group/extractors/topic-extractor.d.ts +9 -0
- package/dist/core/group/extractors/topic-extractor.js +234 -0
- package/dist/core/group/matching.d.ts +13 -0
- package/dist/core/group/matching.js +198 -0
- package/dist/core/group/normalization.d.ts +3 -0
- package/dist/core/group/normalization.js +115 -0
- package/dist/core/group/service-boundary-detector.d.ts +8 -0
- package/dist/core/group/service-boundary-detector.js +155 -0
- package/dist/core/group/service.d.ts +46 -0
- package/dist/core/group/service.js +160 -0
- package/dist/core/group/storage.d.ts +9 -0
- package/dist/core/group/storage.js +91 -0
- package/dist/core/group/sync.d.ts +21 -0
- package/dist/core/group/sync.js +148 -0
- package/dist/core/group/types.d.ts +130 -0
- package/dist/core/group/types.js +1 -0
- package/dist/core/ingestion/binding-accumulator.d.ts +207 -0
- package/dist/core/ingestion/binding-accumulator.js +332 -0
- package/dist/core/ingestion/call-processor.d.ts +155 -24
- package/dist/core/ingestion/call-processor.js +1129 -247
- package/dist/core/ingestion/class-extractors/generic.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/generic.js +135 -0
- package/dist/core/ingestion/class-types.d.ts +34 -0
- package/dist/core/ingestion/class-types.js +1 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +1 -0
- package/dist/core/ingestion/entry-point-scoring.js +1 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +5 -1
- package/dist/core/ingestion/field-extractors/configs/helpers.js +13 -3
- package/dist/core/ingestion/field-types.d.ts +2 -2
- package/dist/core/ingestion/filesystem-walker.js +8 -0
- package/dist/core/ingestion/framework-detection.d.ts +1 -0
- package/dist/core/ingestion/framework-detection.js +1 -0
- package/dist/core/ingestion/heritage-processor.d.ts +8 -15
- package/dist/core/ingestion/heritage-processor.js +15 -28
- package/dist/core/ingestion/import-processor.d.ts +1 -11
- package/dist/core/ingestion/import-processor.js +0 -12
- package/dist/core/ingestion/import-resolvers/utils.js +1 -0
- package/dist/core/ingestion/import-resolvers/vue.d.ts +8 -0
- package/dist/core/ingestion/import-resolvers/vue.js +9 -0
- package/dist/core/ingestion/language-provider.d.ts +6 -3
- package/dist/core/ingestion/languages/c-cpp.js +168 -1
- package/dist/core/ingestion/languages/csharp.js +20 -0
- package/dist/core/ingestion/languages/dart.js +26 -4
- package/dist/core/ingestion/languages/go.js +22 -0
- package/dist/core/ingestion/languages/index.d.ts +1 -0
- package/dist/core/ingestion/languages/index.js +2 -0
- package/dist/core/ingestion/languages/java.js +17 -0
- package/dist/core/ingestion/languages/kotlin.js +24 -1
- package/dist/core/ingestion/languages/php.js +23 -11
- package/dist/core/ingestion/languages/python.js +9 -0
- package/dist/core/ingestion/languages/ruby.js +28 -0
- package/dist/core/ingestion/languages/rust.js +38 -0
- package/dist/core/ingestion/languages/swift.js +31 -0
- package/dist/core/ingestion/languages/typescript.d.ts +1 -0
- package/dist/core/ingestion/languages/typescript.js +54 -1
- package/dist/core/ingestion/languages/vue.d.ts +13 -0
- package/dist/core/ingestion/languages/vue.js +81 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +387 -0
- package/dist/core/ingestion/method-extractors/configs/csharp.js +5 -1
- package/dist/core/ingestion/method-extractors/configs/dart.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/dart.js +376 -0
- package/dist/core/ingestion/method-extractors/configs/go.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/go.js +176 -0
- package/dist/core/ingestion/method-extractors/configs/jvm.js +13 -4
- package/dist/core/ingestion/method-extractors/configs/php.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/php.js +304 -0
- package/dist/core/ingestion/method-extractors/configs/python.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/python.js +309 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.js +285 -0
- package/dist/core/ingestion/method-extractors/configs/rust.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/rust.js +195 -0
- package/dist/core/ingestion/method-extractors/configs/swift.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/swift.js +277 -0
- package/dist/core/ingestion/method-extractors/configs/typescript-javascript.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/typescript-javascript.js +338 -0
- package/dist/core/ingestion/method-extractors/generic.js +38 -15
- package/dist/core/ingestion/method-types.d.ts +25 -0
- package/dist/core/ingestion/model/field-registry.d.ts +18 -0
- package/dist/core/ingestion/model/field-registry.js +22 -0
- package/dist/core/ingestion/model/heritage-map.d.ts +70 -0
- package/dist/core/ingestion/model/heritage-map.js +159 -0
- package/dist/core/ingestion/model/index.d.ts +20 -0
- package/dist/core/ingestion/model/index.js +41 -0
- package/dist/core/ingestion/model/method-registry.d.ts +62 -0
- package/dist/core/ingestion/model/method-registry.js +130 -0
- package/dist/core/ingestion/model/registration-table.d.ts +139 -0
- package/dist/core/ingestion/model/registration-table.js +224 -0
- package/dist/core/ingestion/model/resolution-context.d.ts +93 -0
- package/dist/core/ingestion/model/resolution-context.js +337 -0
- package/dist/core/ingestion/model/resolve.d.ts +56 -0
- package/dist/core/ingestion/model/resolve.js +242 -0
- package/dist/core/ingestion/model/semantic-model.d.ts +86 -0
- package/dist/core/ingestion/model/semantic-model.js +120 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +222 -0
- package/dist/core/ingestion/model/symbol-table.js +206 -0
- package/dist/core/ingestion/model/type-registry.d.ts +39 -0
- package/dist/core/ingestion/model/type-registry.js +62 -0
- package/dist/core/ingestion/mro-processor.d.ts +4 -3
- package/dist/core/ingestion/mro-processor.js +310 -106
- package/dist/core/ingestion/parsing-processor.d.ts +5 -4
- package/dist/core/ingestion/parsing-processor.js +210 -85
- package/dist/core/ingestion/pipeline.d.ts +2 -0
- package/dist/core/ingestion/pipeline.js +192 -68
- package/dist/core/ingestion/tree-sitter-queries.d.ts +6 -6
- package/dist/core/ingestion/tree-sitter-queries.js +37 -0
- package/dist/core/ingestion/type-env.d.ts +15 -2
- package/dist/core/ingestion/type-env.js +163 -102
- package/dist/core/ingestion/type-extractors/csharp.js +17 -0
- package/dist/core/ingestion/type-extractors/jvm.js +11 -0
- package/dist/core/ingestion/type-extractors/php.js +0 -55
- package/dist/core/ingestion/type-extractors/ruby.js +0 -32
- package/dist/core/ingestion/type-extractors/swift.js +13 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +8 -8
- package/dist/core/ingestion/type-extractors/typescript.js +66 -69
- package/dist/core/ingestion/utils/ast-helpers.d.ts +33 -43
- package/dist/core/ingestion/utils/ast-helpers.js +129 -565
- package/dist/core/ingestion/utils/method-props.d.ts +32 -0
- package/dist/core/ingestion/utils/method-props.js +147 -0
- package/dist/core/ingestion/vue-sfc-extractor.d.ts +44 -0
- package/dist/core/ingestion/vue-sfc-extractor.js +94 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +31 -19
- package/dist/core/ingestion/workers/parse-worker.js +463 -198
- package/dist/core/lbug/lbug-adapter.d.ts +6 -0
- package/dist/core/lbug/lbug-adapter.js +68 -3
- package/dist/core/lbug/pool-adapter.d.ts +76 -0
- package/dist/core/lbug/pool-adapter.js +522 -0
- package/dist/core/run-analyze.d.ts +2 -0
- package/dist/core/run-analyze.js +1 -1
- package/dist/core/search/bm25-index.js +1 -1
- package/dist/core/tree-sitter/parser-loader.js +1 -0
- package/dist/core/wiki/graph-queries.js +1 -1
- package/dist/core/wiki/html-viewer.js +6 -4
- package/dist/core/wiki/llm-client.js +4 -6
- package/dist/mcp/core/embedder.js +6 -5
- package/dist/mcp/core/lbug-adapter.d.ts +3 -63
- package/dist/mcp/core/lbug-adapter.js +3 -484
- package/dist/mcp/local/local-backend.d.ts +31 -2
- package/dist/mcp/local/local-backend.js +255 -46
- package/dist/mcp/resources.js +5 -4
- package/dist/mcp/staleness.d.ts +3 -13
- package/dist/mcp/staleness.js +2 -31
- package/dist/mcp/tools.js +80 -4
- package/dist/server/analyze-job.d.ts +2 -0
- package/dist/server/analyze-job.js +4 -0
- package/dist/server/api.d.ts +20 -1
- package/dist/server/api.js +306 -71
- package/dist/server/git-clone.d.ts +2 -1
- package/dist/server/git-clone.js +98 -5
- package/dist/storage/git.d.ts +13 -0
- package/dist/storage/git.js +25 -0
- package/dist/storage/repo-manager.js +1 -1
- package/package.json +8 -2
- package/scripts/patch-tree-sitter-swift.cjs +78 -0
- package/dist/core/ingestion/named-binding-processor.d.ts +0 -18
- package/dist/core/ingestion/named-binding-processor.js +0 -42
- package/dist/core/ingestion/resolution-context.d.ts +0 -58
- package/dist/core/ingestion/resolution-context.js +0 -135
- package/dist/core/ingestion/symbol-table.d.ts +0 -79
- package/dist/core/ingestion/symbol-table.js +0 -115
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
const HANDLES_ROUTE_QUERY = `
|
|
5
|
+
MATCH (handlerFile:File)-[r:CodeRelation {type: 'HANDLES_ROUTE'}]->(route:Route)
|
|
6
|
+
RETURN handlerFile.id AS fileId, handlerFile.filePath AS filePath,
|
|
7
|
+
route.name AS routePath, route.id AS routeId,
|
|
8
|
+
route.responseKeys AS responseKeys,
|
|
9
|
+
r.reason AS routeSource`;
|
|
10
|
+
const FETCHES_QUERY = `
|
|
11
|
+
MATCH (callerFile:File)-[r:CodeRelation {type: 'FETCHES'}]->(route:Route)
|
|
12
|
+
RETURN callerFile.id AS fileId, callerFile.filePath AS filePath,
|
|
13
|
+
route.name AS routePath, route.id AS routeId,
|
|
14
|
+
r.reason AS fetchReason`;
|
|
15
|
+
const CONTAINS_QUERY = `
|
|
16
|
+
MATCH (file:File {id: $fileId})<-[:CodeRelation {type: 'CONTAINS'}]-(sym)
|
|
17
|
+
WHERE sym.startLine IS NOT NULL
|
|
18
|
+
RETURN sym.id AS uid, sym.name AS name, sym.filePath AS filePath, labels(sym) AS labels
|
|
19
|
+
ORDER BY sym.startLine`;
|
|
20
|
+
export function normalizeHttpPath(p) {
|
|
21
|
+
let s = p.trim().split('?')[0].toLowerCase().replace(/\/+$/, '');
|
|
22
|
+
s = s.replace(/:\w+/g, '{param}');
|
|
23
|
+
s = s.replace(/\{[^}]+\}/g, '{param}');
|
|
24
|
+
s = s.replace(/\[[^\]]+\]/g, '{param}');
|
|
25
|
+
return s;
|
|
26
|
+
}
|
|
27
|
+
function methodFromRouteReason(reason) {
|
|
28
|
+
const r = reason || '';
|
|
29
|
+
if (/GetMapping|decorator-Get/i.test(r))
|
|
30
|
+
return 'GET';
|
|
31
|
+
if (/PostMapping|decorator-Post/i.test(r))
|
|
32
|
+
return 'POST';
|
|
33
|
+
if (/PutMapping|decorator-Put/i.test(r))
|
|
34
|
+
return 'PUT';
|
|
35
|
+
if (/DeleteMapping|decorator-Delete/i.test(r))
|
|
36
|
+
return 'DELETE';
|
|
37
|
+
if (/PatchMapping|decorator-Patch/i.test(r))
|
|
38
|
+
return 'PATCH';
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function contractIdFor(method, pathNorm) {
|
|
42
|
+
return `http::${method.toUpperCase()}::${pathNorm}`;
|
|
43
|
+
}
|
|
44
|
+
function readSafe(repoPath, rel) {
|
|
45
|
+
const abs = path.resolve(repoPath, rel);
|
|
46
|
+
const base = path.resolve(repoPath);
|
|
47
|
+
const relToBase = path.relative(base, abs);
|
|
48
|
+
if (relToBase.startsWith('..') || path.isAbsolute(relToBase))
|
|
49
|
+
return null;
|
|
50
|
+
try {
|
|
51
|
+
return fs.readFileSync(abs, 'utf-8');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function pickJavaHandlerName(content, routePath, httpMethod) {
|
|
58
|
+
const tail = routePath.split('/').filter(Boolean).pop() || '';
|
|
59
|
+
const mapNames = {
|
|
60
|
+
GET: 'GetMapping',
|
|
61
|
+
POST: 'PostMapping',
|
|
62
|
+
PUT: 'PutMapping',
|
|
63
|
+
DELETE: 'DeleteMapping',
|
|
64
|
+
PATCH: 'PatchMapping',
|
|
65
|
+
};
|
|
66
|
+
const ann = mapNames[httpMethod] || 'GetMapping';
|
|
67
|
+
const lines = content.split(/\r?\n/);
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
const line = lines[i];
|
|
70
|
+
if (!line.includes(`@${ann}`))
|
|
71
|
+
continue;
|
|
72
|
+
if (!line.includes(`"${tail}"`) && !line.includes(`'${tail}'`) && tail && !line.includes(tail))
|
|
73
|
+
continue;
|
|
74
|
+
for (let j = i + 1; j < Math.min(i + 8, lines.length); j++) {
|
|
75
|
+
const m = lines[j].match(/(?:public|protected|private)\s+[\w<>,\s\[\]]+\s+(\w+)\s*\(/);
|
|
76
|
+
if (m)
|
|
77
|
+
return m[1];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
function pickSymbolUid(rows, preferredName) {
|
|
83
|
+
const norm = (x) => String(x ?? '');
|
|
84
|
+
const labeled = rows.filter((r) => {
|
|
85
|
+
const labels = r.labels ?? r[3];
|
|
86
|
+
const s = JSON.stringify(labels);
|
|
87
|
+
return s.includes('Method') || s.includes('Function');
|
|
88
|
+
});
|
|
89
|
+
const pool = labeled.length > 0 ? labeled : rows;
|
|
90
|
+
if (preferredName) {
|
|
91
|
+
const hit = pool.find((r) => norm(r.name ?? r[1]) === preferredName);
|
|
92
|
+
if (hit) {
|
|
93
|
+
return {
|
|
94
|
+
uid: norm(hit.uid ?? hit[0]),
|
|
95
|
+
name: norm(hit.name ?? hit[1]),
|
|
96
|
+
filePath: norm(hit.filePath ?? hit[2]),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const first = pool[0] || rows[0];
|
|
101
|
+
return {
|
|
102
|
+
uid: norm(first?.uid ?? first?.[0]),
|
|
103
|
+
name: norm(first?.name ?? first?.[1]),
|
|
104
|
+
filePath: norm(first?.filePath ?? first?.[2]),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export class HttpRouteExtractor {
|
|
108
|
+
type = 'http';
|
|
109
|
+
async canExtract(_repo) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
async extract(dbExecutor, repoPath, repo) {
|
|
113
|
+
const graphP = dbExecutor != null ? await this.extractProvidersGraph(dbExecutor, repoPath) : [];
|
|
114
|
+
const providers = graphP.length > 0 ? graphP : await this.extractProvidersSourceScan(repoPath);
|
|
115
|
+
const graphC = dbExecutor != null ? await this.extractConsumersGraph(dbExecutor, repoPath) : [];
|
|
116
|
+
const consumers = graphC.length > 0 ? graphC : await this.extractConsumersSourceScan(repoPath);
|
|
117
|
+
return [...providers, ...consumers];
|
|
118
|
+
}
|
|
119
|
+
async extractProvidersGraph(db, repoPath) {
|
|
120
|
+
const out = [];
|
|
121
|
+
let rows;
|
|
122
|
+
try {
|
|
123
|
+
rows = await db(HANDLES_ROUTE_QUERY);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
for (const row of rows) {
|
|
129
|
+
const filePath = String(row.filePath ?? '');
|
|
130
|
+
const routePath = String(row.routePath ?? '');
|
|
131
|
+
const routeSource = String(row.routeSource ?? row.routeReason ?? '');
|
|
132
|
+
let method = methodFromRouteReason(routeSource);
|
|
133
|
+
const content = readSafe(repoPath, filePath);
|
|
134
|
+
if (!method && content) {
|
|
135
|
+
method = this.inferMethodFromFileScan(content, routePath, 'provider');
|
|
136
|
+
}
|
|
137
|
+
if (!method)
|
|
138
|
+
method = 'GET';
|
|
139
|
+
const pathNorm = normalizeHttpPath(routePath);
|
|
140
|
+
const cid = contractIdFor(method, pathNorm);
|
|
141
|
+
const handlerName = content && routePath ? pickJavaHandlerName(content, routePath, method) : null;
|
|
142
|
+
let symbolUid = '';
|
|
143
|
+
let symbolName = path.basename(filePath) || 'handler';
|
|
144
|
+
let symPath = filePath;
|
|
145
|
+
const fileId = row.fileId ?? row[0];
|
|
146
|
+
if (fileId) {
|
|
147
|
+
try {
|
|
148
|
+
const syms = await db(CONTAINS_QUERY, { fileId });
|
|
149
|
+
if (syms.length > 0) {
|
|
150
|
+
const picked = pickSymbolUid(syms, handlerName);
|
|
151
|
+
symbolUid = picked.uid;
|
|
152
|
+
symbolName = picked.name;
|
|
153
|
+
symPath = picked.filePath || filePath;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* ignore */
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
out.push({
|
|
161
|
+
contractId: cid,
|
|
162
|
+
type: 'http',
|
|
163
|
+
role: 'provider',
|
|
164
|
+
symbolUid,
|
|
165
|
+
symbolRef: { filePath: symPath, name: symbolName },
|
|
166
|
+
symbolName,
|
|
167
|
+
confidence: 0.9,
|
|
168
|
+
meta: {
|
|
169
|
+
method,
|
|
170
|
+
path: pathNorm,
|
|
171
|
+
pathSegments: pathNorm.split('/').filter(Boolean),
|
|
172
|
+
extractionStrategy: 'graph_assisted',
|
|
173
|
+
routeSource,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
inferMethodFromFileScan(content, routePath, _role) {
|
|
180
|
+
const tail = routePath.split('/').filter(Boolean).pop() || '';
|
|
181
|
+
for (const m of ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) {
|
|
182
|
+
const mapNames = {
|
|
183
|
+
GET: 'GetMapping',
|
|
184
|
+
POST: 'PostMapping',
|
|
185
|
+
PUT: 'PutMapping',
|
|
186
|
+
DELETE: 'DeleteMapping',
|
|
187
|
+
PATCH: 'PatchMapping',
|
|
188
|
+
};
|
|
189
|
+
if (content.includes(`@${mapNames[m]}`) &&
|
|
190
|
+
(content.includes(tail) || routePath.includes(tail))) {
|
|
191
|
+
return m;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
async extractProvidersSourceScan(repoPath) {
|
|
197
|
+
const files = await glob('**/*.{ts,tsx,js,jsx,java,vue,svelte,php,py}', {
|
|
198
|
+
cwd: repoPath,
|
|
199
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
|
|
200
|
+
nodir: true,
|
|
201
|
+
});
|
|
202
|
+
const out = [];
|
|
203
|
+
for (const rel of files) {
|
|
204
|
+
const content = readSafe(repoPath, rel);
|
|
205
|
+
if (!content)
|
|
206
|
+
continue;
|
|
207
|
+
out.push(...this.scanSpringProviders(content, rel));
|
|
208
|
+
out.push(...this.scanExpressProviders(content, rel));
|
|
209
|
+
out.push(...this.scanLaravelProviders(content, rel));
|
|
210
|
+
out.push(...this.scanFastApiProviders(content, rel));
|
|
211
|
+
}
|
|
212
|
+
return this.dedupeContracts(out);
|
|
213
|
+
}
|
|
214
|
+
dedupeContracts(items) {
|
|
215
|
+
const seen = new Set();
|
|
216
|
+
const out = [];
|
|
217
|
+
for (const c of items) {
|
|
218
|
+
const k = `${c.contractId}|${c.symbolRef.filePath}|${c.symbolRef.name}`;
|
|
219
|
+
if (seen.has(k))
|
|
220
|
+
continue;
|
|
221
|
+
seen.add(k);
|
|
222
|
+
out.push(c);
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
scanSpringProviders(content, filePath) {
|
|
227
|
+
const out = [];
|
|
228
|
+
// Skip Feign/client interfaces — annotated methods in interfaces are
|
|
229
|
+
// consumers (Feign, JAX-RS proxies), not provider endpoints.
|
|
230
|
+
// Anchored to line start (with optional access modifier) so we do not
|
|
231
|
+
// match "interface" inside comments or string literals.
|
|
232
|
+
if (/^\s*(?:public\s+)?interface\s+\w+/m.test(content) &&
|
|
233
|
+
!/@(?:Rest)?Controller\b/.test(content)) {
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
let classPrefix = '';
|
|
237
|
+
const classRm = content.match(/@RequestMapping\s*\(\s*"([^"]+)"/);
|
|
238
|
+
if (classRm)
|
|
239
|
+
classPrefix = classRm[1].replace(/\/+$/, '');
|
|
240
|
+
const re = /@(Get|Post|Put|Delete|Patch)Mapping\s*\(\s*"([^"]+)"/gi;
|
|
241
|
+
let m;
|
|
242
|
+
while ((m = re.exec(content)) !== null) {
|
|
243
|
+
const method = m[1].toUpperCase();
|
|
244
|
+
let p = m[2];
|
|
245
|
+
if (classPrefix)
|
|
246
|
+
p = `${classPrefix}/${p.replace(/^\//, '')}`;
|
|
247
|
+
const pathNorm = normalizeHttpPath(p);
|
|
248
|
+
const sub = content.slice(m.index);
|
|
249
|
+
const nameM = sub.match(/(?:public|protected|private)\s+[\w<>,\s\[\]]+\s+(\w+)\s*\(/);
|
|
250
|
+
const name = nameM ? nameM[1] : m[0];
|
|
251
|
+
out.push(this.makeProvider(filePath, method, pathNorm, name, 0.8));
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
scanExpressProviders(content, filePath) {
|
|
256
|
+
const out = [];
|
|
257
|
+
const re = /(?:router|app)\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
258
|
+
let m;
|
|
259
|
+
while ((m = re.exec(content)) !== null) {
|
|
260
|
+
const method = m[1].toUpperCase();
|
|
261
|
+
const pathNorm = normalizeHttpPath(m[2]);
|
|
262
|
+
out.push(this.makeProvider(filePath, method, pathNorm, 'handler', 0.8));
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
scanLaravelProviders(content, filePath) {
|
|
267
|
+
const out = [];
|
|
268
|
+
const re = /Route::(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
269
|
+
let m;
|
|
270
|
+
while ((m = re.exec(content)) !== null) {
|
|
271
|
+
const method = m[1].toUpperCase();
|
|
272
|
+
const pathNorm = normalizeHttpPath(m[2]);
|
|
273
|
+
out.push(this.makeProvider(filePath, method, pathNorm, 'route', 0.8));
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
scanFastApiProviders(content, filePath) {
|
|
278
|
+
const out = [];
|
|
279
|
+
const re = /@app\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
280
|
+
let m;
|
|
281
|
+
while ((m = re.exec(content)) !== null) {
|
|
282
|
+
const method = m[1].toUpperCase();
|
|
283
|
+
const pathNorm = normalizeHttpPath(m[2]);
|
|
284
|
+
out.push(this.makeProvider(filePath, method, pathNorm, 'handler', 0.8));
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
makeProvider(filePath, method, pathNorm, name, confidence) {
|
|
289
|
+
const cid = contractIdFor(method, pathNorm);
|
|
290
|
+
return {
|
|
291
|
+
contractId: cid,
|
|
292
|
+
type: 'http',
|
|
293
|
+
role: 'provider',
|
|
294
|
+
symbolUid: '',
|
|
295
|
+
symbolRef: { filePath, name },
|
|
296
|
+
symbolName: name,
|
|
297
|
+
confidence,
|
|
298
|
+
meta: {
|
|
299
|
+
method,
|
|
300
|
+
path: pathNorm,
|
|
301
|
+
pathSegments: pathNorm.split('/').filter(Boolean),
|
|
302
|
+
extractionStrategy: 'source_scan',
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
async extractConsumersGraph(db, repoPath) {
|
|
307
|
+
const out = [];
|
|
308
|
+
let rows;
|
|
309
|
+
try {
|
|
310
|
+
rows = await db(FETCHES_QUERY);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
for (const row of rows) {
|
|
316
|
+
const filePath = String(row.filePath ?? '');
|
|
317
|
+
const routePath = String(row.routePath ?? '');
|
|
318
|
+
const pathNorm = normalizeHttpPath(routePath);
|
|
319
|
+
let method = 'GET';
|
|
320
|
+
const content = readSafe(repoPath, filePath);
|
|
321
|
+
if (content) {
|
|
322
|
+
const inferred = this.inferFetchMethod(content, pathNorm);
|
|
323
|
+
if (inferred)
|
|
324
|
+
method = inferred;
|
|
325
|
+
}
|
|
326
|
+
const cid = contractIdFor(method, pathNorm);
|
|
327
|
+
let symbolUid = '';
|
|
328
|
+
let symbolName = 'fetch';
|
|
329
|
+
let symPath = filePath;
|
|
330
|
+
const fileId = row.fileId ?? row[0];
|
|
331
|
+
if (fileId) {
|
|
332
|
+
try {
|
|
333
|
+
const syms = await db(CONTAINS_QUERY, { fileId });
|
|
334
|
+
if (syms.length > 0) {
|
|
335
|
+
const picked = pickSymbolUid(syms, null);
|
|
336
|
+
symbolUid = picked.uid;
|
|
337
|
+
symbolName = picked.name;
|
|
338
|
+
symPath = picked.filePath || filePath;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
/* ignore */
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
out.push({
|
|
346
|
+
contractId: cid,
|
|
347
|
+
type: 'http',
|
|
348
|
+
role: 'consumer',
|
|
349
|
+
symbolUid,
|
|
350
|
+
symbolRef: { filePath: symPath, name: symbolName },
|
|
351
|
+
symbolName,
|
|
352
|
+
confidence: 0.9,
|
|
353
|
+
meta: {
|
|
354
|
+
method,
|
|
355
|
+
path: pathNorm,
|
|
356
|
+
extractionStrategy: 'graph_assisted',
|
|
357
|
+
fetchReason: String(row.fetchReason ?? ''),
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
inferFetchMethod(content, pathNorm) {
|
|
364
|
+
const esc = pathNorm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
365
|
+
const fetchRe = new RegExp(`fetch\\s*\\(\\s*['"\`]([^'"\`]*${esc}[^'"\`]*)['"\`]\\s*,\\s*\\{[^}]*method:\\s*['"](\\w+)['"]`, 'i');
|
|
366
|
+
const m = content.match(fetchRe);
|
|
367
|
+
if (m)
|
|
368
|
+
return m[2].toUpperCase();
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
async extractConsumersSourceScan(repoPath) {
|
|
372
|
+
const files = await glob('**/*.{ts,tsx,js,jsx,vue,svelte}', {
|
|
373
|
+
cwd: repoPath,
|
|
374
|
+
ignore: ['**/node_modules/**', '**/.git/**'],
|
|
375
|
+
nodir: true,
|
|
376
|
+
});
|
|
377
|
+
const out = [];
|
|
378
|
+
for (const rel of files) {
|
|
379
|
+
const content = readSafe(repoPath, rel);
|
|
380
|
+
if (!content)
|
|
381
|
+
continue;
|
|
382
|
+
out.push(...this.scanFetchConsumers(content, rel));
|
|
383
|
+
out.push(...this.scanAxiosConsumers(content, rel));
|
|
384
|
+
}
|
|
385
|
+
return this.dedupeContracts(out);
|
|
386
|
+
}
|
|
387
|
+
scanFetchConsumers(content, filePath) {
|
|
388
|
+
const out = [];
|
|
389
|
+
const re = /fetch\s*\(\s*['"`]([^'"`]+)['"`](?:\s*,\s*\{[^}]*method:\s*['"](\w+)['"][^}]*\})?\s*\)/gi;
|
|
390
|
+
let m;
|
|
391
|
+
while ((m = re.exec(content)) !== null) {
|
|
392
|
+
const pathNorm = normalizeHttpPath(this.templateToPattern(m[1]));
|
|
393
|
+
const method = (m[2] || 'GET').toUpperCase();
|
|
394
|
+
out.push(this.makeConsumer(filePath, method, pathNorm, 0.7));
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
templateToPattern(url) {
|
|
399
|
+
return url.replace(/\$\{[^}]+\}/g, '{param}');
|
|
400
|
+
}
|
|
401
|
+
scanAxiosConsumers(content, filePath) {
|
|
402
|
+
const out = [];
|
|
403
|
+
const re = /axios\.(get|post|put|delete|patch)\s*\(\s*[`'"]([^`'"]+)[`'"]/gi;
|
|
404
|
+
let m;
|
|
405
|
+
while ((m = re.exec(content)) !== null) {
|
|
406
|
+
const method = m[1].toUpperCase();
|
|
407
|
+
const pathNorm = normalizeHttpPath(this.templateToPattern(m[2]));
|
|
408
|
+
out.push(this.makeConsumer(filePath, method, pathNorm, 0.7));
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
makeConsumer(filePath, method, pathNorm, confidence) {
|
|
413
|
+
return {
|
|
414
|
+
contractId: contractIdFor(method, pathNorm),
|
|
415
|
+
type: 'http',
|
|
416
|
+
role: 'consumer',
|
|
417
|
+
symbolUid: '',
|
|
418
|
+
symbolRef: { filePath, name: 'fetch' },
|
|
419
|
+
symbolName: 'fetch',
|
|
420
|
+
confidence,
|
|
421
|
+
meta: {
|
|
422
|
+
method,
|
|
423
|
+
path: pathNorm,
|
|
424
|
+
extractionStrategy: 'source_scan',
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ContractExtractor, CypherExecutor } from '../contract-extractor.js';
|
|
2
|
+
import type { ExtractedContract, RepoHandle } from '../types.js';
|
|
3
|
+
export declare class TopicExtractor implements ContractExtractor {
|
|
4
|
+
type: "topic";
|
|
5
|
+
canExtract(_repo: RepoHandle): Promise<boolean>;
|
|
6
|
+
extract(_dbExecutor: CypherExecutor | null, repoPath: string, _repo: RepoHandle): Promise<ExtractedContract[]>;
|
|
7
|
+
private scanFile;
|
|
8
|
+
private dedupe;
|
|
9
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
function readSafe(repoPath, rel) {
|
|
5
|
+
const abs = path.resolve(repoPath, rel);
|
|
6
|
+
const base = path.resolve(repoPath);
|
|
7
|
+
const relToBase = path.relative(base, abs);
|
|
8
|
+
if (relToBase.startsWith('..') || path.isAbsolute(relToBase))
|
|
9
|
+
return null;
|
|
10
|
+
try {
|
|
11
|
+
return fs.readFileSync(abs, 'utf-8');
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function makeContract(topicName, role, filePath, symbolName, confidence, broker) {
|
|
18
|
+
return {
|
|
19
|
+
contractId: `topic::${topicName}`,
|
|
20
|
+
type: 'topic',
|
|
21
|
+
role,
|
|
22
|
+
symbolUid: '',
|
|
23
|
+
symbolRef: { filePath: filePath.replace(/\\/g, '/'), name: symbolName },
|
|
24
|
+
symbolName,
|
|
25
|
+
confidence,
|
|
26
|
+
meta: {
|
|
27
|
+
broker,
|
|
28
|
+
topicName,
|
|
29
|
+
extractionStrategy: 'source_scan',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// --- Kafka patterns ---
|
|
34
|
+
const KAFKA_PATTERNS = [
|
|
35
|
+
// Java: @KafkaListener(topics = "xxx")
|
|
36
|
+
{
|
|
37
|
+
regex: /@KafkaListener\s*\(\s*topics\s*=\s*"([^"]+)"/g,
|
|
38
|
+
role: 'consumer',
|
|
39
|
+
broker: 'kafka',
|
|
40
|
+
confidence: 0.8,
|
|
41
|
+
topicGroup: 1,
|
|
42
|
+
symbolName: 'kafkaListener',
|
|
43
|
+
},
|
|
44
|
+
// Java: kafkaTemplate.send("xxx"
|
|
45
|
+
{
|
|
46
|
+
regex: /kafkaTemplate\.send\s*\(\s*"([^"]+)"/gi,
|
|
47
|
+
role: 'provider',
|
|
48
|
+
broker: 'kafka',
|
|
49
|
+
confidence: 0.8,
|
|
50
|
+
topicGroup: 1,
|
|
51
|
+
symbolName: 'kafkaTemplate.send',
|
|
52
|
+
},
|
|
53
|
+
// Node: producer.send({ topic: 'xxx'
|
|
54
|
+
{
|
|
55
|
+
regex: /producer\.send\s*\(\s*\{\s*topic:\s*['"]([^'"]+)['"]/g,
|
|
56
|
+
role: 'provider',
|
|
57
|
+
broker: 'kafka',
|
|
58
|
+
confidence: 0.8,
|
|
59
|
+
topicGroup: 1,
|
|
60
|
+
symbolName: 'producer.send',
|
|
61
|
+
},
|
|
62
|
+
// Node: consumer.subscribe({ topic: 'xxx'
|
|
63
|
+
{
|
|
64
|
+
regex: /consumer\.subscribe\s*\(\s*\{\s*topic:\s*['"]([^'"]+)['"]/g,
|
|
65
|
+
role: 'consumer',
|
|
66
|
+
broker: 'kafka',
|
|
67
|
+
confidence: 0.8,
|
|
68
|
+
topicGroup: 1,
|
|
69
|
+
symbolName: 'consumer.subscribe',
|
|
70
|
+
},
|
|
71
|
+
// Go: consumer.ConsumePartition("xxx"
|
|
72
|
+
{
|
|
73
|
+
regex: /\.ConsumePartition\s*\(\s*"([^"]+)"/g,
|
|
74
|
+
role: 'consumer',
|
|
75
|
+
broker: 'kafka',
|
|
76
|
+
confidence: 0.7,
|
|
77
|
+
topicGroup: 1,
|
|
78
|
+
symbolName: 'ConsumePartition',
|
|
79
|
+
},
|
|
80
|
+
// Python: KafkaConsumer('xxx'
|
|
81
|
+
{
|
|
82
|
+
regex: /KafkaConsumer\s*\(\s*['"]([^'"]+)['"]/g,
|
|
83
|
+
role: 'consumer',
|
|
84
|
+
broker: 'kafka',
|
|
85
|
+
confidence: 0.7,
|
|
86
|
+
topicGroup: 1,
|
|
87
|
+
symbolName: 'KafkaConsumer',
|
|
88
|
+
},
|
|
89
|
+
// Python: producer.send('xxx' or producer.produce('xxx'
|
|
90
|
+
{
|
|
91
|
+
regex: /producer\.(?:send|produce)\s*\(\s*['"]([^'"]+)['"]/g,
|
|
92
|
+
role: 'provider',
|
|
93
|
+
broker: 'kafka',
|
|
94
|
+
confidence: 0.7,
|
|
95
|
+
topicGroup: 1,
|
|
96
|
+
symbolName: 'producer.send',
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
// --- RabbitMQ patterns ---
|
|
100
|
+
const RABBITMQ_PATTERNS = [
|
|
101
|
+
// Java: @RabbitListener(queues = "xxx")
|
|
102
|
+
{
|
|
103
|
+
regex: /@RabbitListener\s*\(\s*queues\s*=\s*"([^"]+)"/g,
|
|
104
|
+
role: 'consumer',
|
|
105
|
+
broker: 'rabbitmq',
|
|
106
|
+
confidence: 0.8,
|
|
107
|
+
topicGroup: 1,
|
|
108
|
+
symbolName: 'rabbitListener',
|
|
109
|
+
},
|
|
110
|
+
// Java: rabbitTemplate.convertAndSend("xxx"
|
|
111
|
+
{
|
|
112
|
+
regex: /rabbitTemplate\.convertAndSend\s*\(\s*"([^"]+)"/gi,
|
|
113
|
+
role: 'provider',
|
|
114
|
+
broker: 'rabbitmq',
|
|
115
|
+
confidence: 0.8,
|
|
116
|
+
topicGroup: 1,
|
|
117
|
+
symbolName: 'rabbitTemplate.convertAndSend',
|
|
118
|
+
},
|
|
119
|
+
// Node: channel.consume("xxx"
|
|
120
|
+
{
|
|
121
|
+
regex: /channel\.consume\s*\(\s*"([^"]+)"/g,
|
|
122
|
+
role: 'consumer',
|
|
123
|
+
broker: 'rabbitmq',
|
|
124
|
+
confidence: 0.8,
|
|
125
|
+
topicGroup: 1,
|
|
126
|
+
symbolName: 'channel.consume',
|
|
127
|
+
},
|
|
128
|
+
// Node: channel.publish("xxx"
|
|
129
|
+
{
|
|
130
|
+
regex: /channel\.publish\s*\(\s*"([^"]+)"/g,
|
|
131
|
+
role: 'provider',
|
|
132
|
+
broker: 'rabbitmq',
|
|
133
|
+
confidence: 0.8,
|
|
134
|
+
topicGroup: 1,
|
|
135
|
+
symbolName: 'channel.publish',
|
|
136
|
+
},
|
|
137
|
+
// Node: channel.sendToQueue("xxx"
|
|
138
|
+
{
|
|
139
|
+
regex: /channel\.sendToQueue\s*\(\s*"([^"]+)"/g,
|
|
140
|
+
role: 'provider',
|
|
141
|
+
broker: 'rabbitmq',
|
|
142
|
+
confidence: 0.8,
|
|
143
|
+
topicGroup: 1,
|
|
144
|
+
symbolName: 'channel.sendToQueue',
|
|
145
|
+
},
|
|
146
|
+
// Python: channel.basic_consume(queue='xxx'
|
|
147
|
+
{
|
|
148
|
+
regex: /channel\.basic_consume\s*\(\s*queue\s*=\s*['"]([^'"]+)['"]/g,
|
|
149
|
+
role: 'consumer',
|
|
150
|
+
broker: 'rabbitmq',
|
|
151
|
+
confidence: 0.7,
|
|
152
|
+
topicGroup: 1,
|
|
153
|
+
symbolName: 'basic_consume',
|
|
154
|
+
},
|
|
155
|
+
// Python: channel.basic_publish(exchange='xxx'
|
|
156
|
+
{
|
|
157
|
+
regex: /channel\.basic_publish\s*\([^)]*exchange\s*=\s*['"]([^'"]+)['"]/g,
|
|
158
|
+
role: 'provider',
|
|
159
|
+
broker: 'rabbitmq',
|
|
160
|
+
confidence: 0.7,
|
|
161
|
+
topicGroup: 1,
|
|
162
|
+
symbolName: 'basic_publish',
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
// --- NATS patterns ---
|
|
166
|
+
const NATS_PATTERNS = [
|
|
167
|
+
// Go/Node: nc.Subscribe("xxx" or nc.subscribe("xxx"
|
|
168
|
+
{
|
|
169
|
+
regex: /nc\.(?:S|s)ubscribe\s*\(\s*"([^"]+)"/g,
|
|
170
|
+
role: 'consumer',
|
|
171
|
+
broker: 'nats',
|
|
172
|
+
confidence: 0.8,
|
|
173
|
+
topicGroup: 1,
|
|
174
|
+
symbolName: 'nc.Subscribe',
|
|
175
|
+
},
|
|
176
|
+
// Go/Node: nc.Publish("xxx" or nc.publish("xxx"
|
|
177
|
+
{
|
|
178
|
+
regex: /nc\.(?:P|p)ublish\s*\(\s*"([^"]+)"/g,
|
|
179
|
+
role: 'provider',
|
|
180
|
+
broker: 'nats',
|
|
181
|
+
confidence: 0.8,
|
|
182
|
+
topicGroup: 1,
|
|
183
|
+
symbolName: 'nc.Publish',
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
const ALL_PATTERNS = [...KAFKA_PATTERNS, ...RABBITMQ_PATTERNS, ...NATS_PATTERNS];
|
|
187
|
+
export class TopicExtractor {
|
|
188
|
+
type = 'topic';
|
|
189
|
+
async canExtract(_repo) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
async extract(_dbExecutor, repoPath, _repo) {
|
|
193
|
+
const files = await glob('**/*.{ts,tsx,js,jsx,java,go,py}', {
|
|
194
|
+
cwd: repoPath,
|
|
195
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/vendor/**', '**/dist/**', '**/build/**'],
|
|
196
|
+
nodir: true,
|
|
197
|
+
});
|
|
198
|
+
const out = [];
|
|
199
|
+
for (const rel of files) {
|
|
200
|
+
const content = readSafe(repoPath, rel);
|
|
201
|
+
if (!content)
|
|
202
|
+
continue;
|
|
203
|
+
out.push(...this.scanFile(content, rel));
|
|
204
|
+
}
|
|
205
|
+
return this.dedupe(out);
|
|
206
|
+
}
|
|
207
|
+
scanFile(content, filePath) {
|
|
208
|
+
const out = [];
|
|
209
|
+
for (const pattern of ALL_PATTERNS) {
|
|
210
|
+
// Reset regex state for each file
|
|
211
|
+
const re = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
212
|
+
let m;
|
|
213
|
+
while ((m = re.exec(content)) !== null) {
|
|
214
|
+
const topicName = m[pattern.topicGroup];
|
|
215
|
+
if (!topicName)
|
|
216
|
+
continue;
|
|
217
|
+
out.push(makeContract(topicName, pattern.role, filePath, pattern.symbolName, pattern.confidence, pattern.broker));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
dedupe(items) {
|
|
223
|
+
const seen = new Set();
|
|
224
|
+
const out = [];
|
|
225
|
+
for (const c of items) {
|
|
226
|
+
const k = `${c.contractId}|${c.role}|${c.symbolRef.filePath}`;
|
|
227
|
+
if (seen.has(k))
|
|
228
|
+
continue;
|
|
229
|
+
seen.add(k);
|
|
230
|
+
out.push(c);
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StoredContract, CrossLink } from './types.js';
|
|
2
|
+
export interface MatchResult {
|
|
3
|
+
matched: CrossLink[];
|
|
4
|
+
unmatched: StoredContract[];
|
|
5
|
+
}
|
|
6
|
+
export interface WildcardMatchResult {
|
|
7
|
+
matched: CrossLink[];
|
|
8
|
+
remaining: StoredContract[];
|
|
9
|
+
}
|
|
10
|
+
export declare function normalizeContractId(id: string): string;
|
|
11
|
+
export declare function buildProviderIndex(contracts: StoredContract[]): Map<string, StoredContract[]>;
|
|
12
|
+
export declare function runExactMatch(contracts: StoredContract[], providerIndex?: Map<string, StoredContract[]>): MatchResult;
|
|
13
|
+
export declare function runWildcardMatch(unmatched: StoredContract[], providerIndex: Map<string, StoredContract[]>): WildcardMatchResult;
|