gitnexus 1.5.3 → 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.
Files changed (201) hide show
  1. package/README.md +10 -0
  2. package/dist/_shared/graph/types.d.ts +1 -1
  3. package/dist/_shared/graph/types.d.ts.map +1 -1
  4. package/dist/_shared/index.d.ts +1 -0
  5. package/dist/_shared/index.d.ts.map +1 -1
  6. package/dist/_shared/language-detection.d.ts.map +1 -1
  7. package/dist/_shared/language-detection.js +2 -0
  8. package/dist/_shared/language-detection.js.map +1 -1
  9. package/dist/_shared/languages.d.ts +1 -0
  10. package/dist/_shared/languages.d.ts.map +1 -1
  11. package/dist/_shared/languages.js +1 -0
  12. package/dist/_shared/languages.js.map +1 -1
  13. package/dist/_shared/lbug/schema-constants.d.ts +1 -1
  14. package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
  15. package/dist/_shared/lbug/schema-constants.js +3 -1
  16. package/dist/_shared/lbug/schema-constants.js.map +1 -1
  17. package/dist/_shared/mro-strategy.d.ts +19 -0
  18. package/dist/_shared/mro-strategy.d.ts.map +1 -0
  19. package/dist/_shared/mro-strategy.js +2 -0
  20. package/dist/_shared/mro-strategy.js.map +1 -0
  21. package/dist/cli/ai-context.d.ts +1 -0
  22. package/dist/cli/ai-context.js +28 -4
  23. package/dist/cli/analyze.d.ts +2 -0
  24. package/dist/cli/analyze.js +2 -1
  25. package/dist/cli/group.d.ts +2 -0
  26. package/dist/cli/group.js +233 -0
  27. package/dist/cli/index.js +3 -0
  28. package/dist/cli/serve.js +4 -1
  29. package/dist/cli/setup.js +34 -3
  30. package/dist/config/ignore-service.js +8 -3
  31. package/dist/core/augmentation/engine.js +1 -1
  32. package/dist/core/git-staleness.d.ts +13 -0
  33. package/dist/core/git-staleness.js +29 -0
  34. package/dist/core/group/bridge-db.d.ts +82 -0
  35. package/dist/core/group/bridge-db.js +460 -0
  36. package/dist/core/group/bridge-schema.d.ts +27 -0
  37. package/dist/core/group/bridge-schema.js +55 -0
  38. package/dist/core/group/config-parser.d.ts +3 -0
  39. package/dist/core/group/config-parser.js +83 -0
  40. package/dist/core/group/contract-extractor.d.ts +7 -0
  41. package/dist/core/group/contract-extractor.js +1 -0
  42. package/dist/core/group/extractors/grpc-extractor.d.ts +16 -0
  43. package/dist/core/group/extractors/grpc-extractor.js +264 -0
  44. package/dist/core/group/extractors/http-route-extractor.d.ts +24 -0
  45. package/dist/core/group/extractors/http-route-extractor.js +428 -0
  46. package/dist/core/group/extractors/topic-extractor.d.ts +9 -0
  47. package/dist/core/group/extractors/topic-extractor.js +234 -0
  48. package/dist/core/group/matching.d.ts +13 -0
  49. package/dist/core/group/matching.js +198 -0
  50. package/dist/core/group/normalization.d.ts +3 -0
  51. package/dist/core/group/normalization.js +115 -0
  52. package/dist/core/group/service-boundary-detector.d.ts +8 -0
  53. package/dist/core/group/service-boundary-detector.js +155 -0
  54. package/dist/core/group/service.d.ts +46 -0
  55. package/dist/core/group/service.js +160 -0
  56. package/dist/core/group/storage.d.ts +9 -0
  57. package/dist/core/group/storage.js +91 -0
  58. package/dist/core/group/sync.d.ts +21 -0
  59. package/dist/core/group/sync.js +148 -0
  60. package/dist/core/group/types.d.ts +130 -0
  61. package/dist/core/group/types.js +1 -0
  62. package/dist/core/ingestion/binding-accumulator.d.ts +207 -0
  63. package/dist/core/ingestion/binding-accumulator.js +332 -0
  64. package/dist/core/ingestion/call-processor.d.ts +155 -24
  65. package/dist/core/ingestion/call-processor.js +1129 -247
  66. package/dist/core/ingestion/class-extractors/generic.d.ts +2 -0
  67. package/dist/core/ingestion/class-extractors/generic.js +135 -0
  68. package/dist/core/ingestion/class-types.d.ts +34 -0
  69. package/dist/core/ingestion/class-types.js +1 -0
  70. package/dist/core/ingestion/entry-point-scoring.d.ts +1 -0
  71. package/dist/core/ingestion/entry-point-scoring.js +1 -0
  72. package/dist/core/ingestion/field-types.d.ts +2 -2
  73. package/dist/core/ingestion/filesystem-walker.js +8 -0
  74. package/dist/core/ingestion/framework-detection.d.ts +1 -0
  75. package/dist/core/ingestion/framework-detection.js +1 -0
  76. package/dist/core/ingestion/heritage-processor.d.ts +8 -15
  77. package/dist/core/ingestion/heritage-processor.js +15 -28
  78. package/dist/core/ingestion/import-processor.d.ts +1 -11
  79. package/dist/core/ingestion/import-processor.js +0 -12
  80. package/dist/core/ingestion/import-resolvers/utils.js +1 -0
  81. package/dist/core/ingestion/import-resolvers/vue.d.ts +8 -0
  82. package/dist/core/ingestion/import-resolvers/vue.js +9 -0
  83. package/dist/core/ingestion/language-provider.d.ts +6 -3
  84. package/dist/core/ingestion/languages/c-cpp.js +168 -1
  85. package/dist/core/ingestion/languages/csharp.js +20 -0
  86. package/dist/core/ingestion/languages/dart.js +26 -4
  87. package/dist/core/ingestion/languages/go.js +22 -0
  88. package/dist/core/ingestion/languages/index.d.ts +1 -0
  89. package/dist/core/ingestion/languages/index.js +2 -0
  90. package/dist/core/ingestion/languages/java.js +17 -0
  91. package/dist/core/ingestion/languages/kotlin.js +24 -1
  92. package/dist/core/ingestion/languages/php.js +23 -11
  93. package/dist/core/ingestion/languages/python.js +9 -0
  94. package/dist/core/ingestion/languages/ruby.js +28 -0
  95. package/dist/core/ingestion/languages/rust.js +38 -0
  96. package/dist/core/ingestion/languages/swift.js +31 -0
  97. package/dist/core/ingestion/languages/typescript.d.ts +1 -0
  98. package/dist/core/ingestion/languages/typescript.js +52 -3
  99. package/dist/core/ingestion/languages/vue.d.ts +13 -0
  100. package/dist/core/ingestion/languages/vue.js +81 -0
  101. package/dist/core/ingestion/method-extractors/configs/c-cpp.d.ts +3 -0
  102. package/dist/core/ingestion/method-extractors/configs/c-cpp.js +387 -0
  103. package/dist/core/ingestion/method-extractors/configs/csharp.js +5 -1
  104. package/dist/core/ingestion/method-extractors/configs/dart.d.ts +2 -0
  105. package/dist/core/ingestion/method-extractors/configs/dart.js +376 -0
  106. package/dist/core/ingestion/method-extractors/configs/go.d.ts +2 -0
  107. package/dist/core/ingestion/method-extractors/configs/go.js +176 -0
  108. package/dist/core/ingestion/method-extractors/configs/jvm.js +13 -4
  109. package/dist/core/ingestion/method-extractors/configs/php.d.ts +2 -0
  110. package/dist/core/ingestion/method-extractors/configs/php.js +304 -0
  111. package/dist/core/ingestion/method-extractors/configs/python.d.ts +2 -0
  112. package/dist/core/ingestion/method-extractors/configs/python.js +309 -0
  113. package/dist/core/ingestion/method-extractors/configs/ruby.d.ts +2 -0
  114. package/dist/core/ingestion/method-extractors/configs/ruby.js +285 -0
  115. package/dist/core/ingestion/method-extractors/configs/rust.d.ts +2 -0
  116. package/dist/core/ingestion/method-extractors/configs/rust.js +195 -0
  117. package/dist/core/ingestion/method-extractors/configs/swift.d.ts +2 -0
  118. package/dist/core/ingestion/method-extractors/configs/swift.js +277 -0
  119. package/dist/core/ingestion/method-extractors/configs/typescript-javascript.js +85 -8
  120. package/dist/core/ingestion/method-extractors/generic.js +38 -15
  121. package/dist/core/ingestion/method-types.d.ts +25 -0
  122. package/dist/core/ingestion/model/field-registry.d.ts +18 -0
  123. package/dist/core/ingestion/model/field-registry.js +22 -0
  124. package/dist/core/ingestion/model/heritage-map.d.ts +70 -0
  125. package/dist/core/ingestion/model/heritage-map.js +159 -0
  126. package/dist/core/ingestion/model/index.d.ts +20 -0
  127. package/dist/core/ingestion/model/index.js +41 -0
  128. package/dist/core/ingestion/model/method-registry.d.ts +62 -0
  129. package/dist/core/ingestion/model/method-registry.js +130 -0
  130. package/dist/core/ingestion/model/registration-table.d.ts +139 -0
  131. package/dist/core/ingestion/model/registration-table.js +224 -0
  132. package/dist/core/ingestion/model/resolution-context.d.ts +93 -0
  133. package/dist/core/ingestion/model/resolution-context.js +337 -0
  134. package/dist/core/ingestion/model/resolve.d.ts +56 -0
  135. package/dist/core/ingestion/model/resolve.js +242 -0
  136. package/dist/core/ingestion/model/semantic-model.d.ts +86 -0
  137. package/dist/core/ingestion/model/semantic-model.js +120 -0
  138. package/dist/core/ingestion/model/symbol-table.d.ts +222 -0
  139. package/dist/core/ingestion/model/symbol-table.js +206 -0
  140. package/dist/core/ingestion/model/type-registry.d.ts +39 -0
  141. package/dist/core/ingestion/model/type-registry.js +62 -0
  142. package/dist/core/ingestion/mro-processor.d.ts +4 -3
  143. package/dist/core/ingestion/mro-processor.js +310 -106
  144. package/dist/core/ingestion/parsing-processor.d.ts +5 -4
  145. package/dist/core/ingestion/parsing-processor.js +210 -85
  146. package/dist/core/ingestion/pipeline.d.ts +2 -0
  147. package/dist/core/ingestion/pipeline.js +192 -68
  148. package/dist/core/ingestion/tree-sitter-queries.d.ts +5 -5
  149. package/dist/core/ingestion/tree-sitter-queries.js +21 -0
  150. package/dist/core/ingestion/type-env.d.ts +15 -2
  151. package/dist/core/ingestion/type-env.js +163 -102
  152. package/dist/core/ingestion/type-extractors/csharp.js +17 -0
  153. package/dist/core/ingestion/type-extractors/jvm.js +11 -0
  154. package/dist/core/ingestion/type-extractors/php.js +0 -55
  155. package/dist/core/ingestion/type-extractors/ruby.js +0 -32
  156. package/dist/core/ingestion/type-extractors/swift.js +13 -0
  157. package/dist/core/ingestion/type-extractors/types.d.ts +8 -8
  158. package/dist/core/ingestion/type-extractors/typescript.js +66 -69
  159. package/dist/core/ingestion/utils/ast-helpers.d.ts +33 -43
  160. package/dist/core/ingestion/utils/ast-helpers.js +129 -572
  161. package/dist/core/ingestion/utils/method-props.d.ts +32 -0
  162. package/dist/core/ingestion/utils/method-props.js +147 -0
  163. package/dist/core/ingestion/vue-sfc-extractor.d.ts +44 -0
  164. package/dist/core/ingestion/vue-sfc-extractor.js +94 -0
  165. package/dist/core/ingestion/workers/parse-worker.d.ts +31 -19
  166. package/dist/core/ingestion/workers/parse-worker.js +463 -198
  167. package/dist/core/lbug/lbug-adapter.d.ts +6 -0
  168. package/dist/core/lbug/lbug-adapter.js +68 -3
  169. package/dist/core/lbug/pool-adapter.d.ts +76 -0
  170. package/dist/core/lbug/pool-adapter.js +522 -0
  171. package/dist/core/run-analyze.d.ts +2 -0
  172. package/dist/core/run-analyze.js +1 -1
  173. package/dist/core/search/bm25-index.js +1 -1
  174. package/dist/core/tree-sitter/parser-loader.js +1 -0
  175. package/dist/core/wiki/graph-queries.js +1 -1
  176. package/dist/mcp/core/embedder.js +6 -5
  177. package/dist/mcp/core/lbug-adapter.d.ts +3 -63
  178. package/dist/mcp/core/lbug-adapter.js +3 -484
  179. package/dist/mcp/local/local-backend.d.ts +31 -2
  180. package/dist/mcp/local/local-backend.js +255 -46
  181. package/dist/mcp/resources.js +5 -4
  182. package/dist/mcp/staleness.d.ts +3 -13
  183. package/dist/mcp/staleness.js +2 -31
  184. package/dist/mcp/tools.js +80 -4
  185. package/dist/server/analyze-job.d.ts +2 -0
  186. package/dist/server/analyze-job.js +4 -0
  187. package/dist/server/api.d.ts +20 -1
  188. package/dist/server/api.js +306 -71
  189. package/dist/server/git-clone.d.ts +2 -1
  190. package/dist/server/git-clone.js +98 -5
  191. package/dist/storage/git.d.ts +13 -0
  192. package/dist/storage/git.js +25 -0
  193. package/dist/storage/repo-manager.js +1 -1
  194. package/package.json +8 -2
  195. package/scripts/patch-tree-sitter-swift.cjs +78 -0
  196. package/dist/core/ingestion/named-binding-processor.d.ts +0 -18
  197. package/dist/core/ingestion/named-binding-processor.js +0 -42
  198. package/dist/core/ingestion/resolution-context.d.ts +0 -58
  199. package/dist/core/ingestion/resolution-context.js +0 -135
  200. package/dist/core/ingestion/symbol-table.d.ts +0 -79
  201. 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;