gitnexus 1.6.0 → 1.6.1

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 (136) hide show
  1. package/dist/cli/analyze.js +28 -3
  2. package/dist/core/group/extractors/fs-utils.d.ts +10 -0
  3. package/dist/core/group/extractors/fs-utils.js +24 -0
  4. package/dist/core/group/extractors/grpc-extractor.d.ts +17 -8
  5. package/dist/core/group/extractors/grpc-extractor.js +313 -191
  6. package/dist/core/group/extractors/grpc-patterns/go.d.ts +2 -0
  7. package/dist/core/group/extractors/grpc-patterns/go.js +97 -0
  8. package/dist/core/group/extractors/grpc-patterns/index.d.ts +19 -0
  9. package/dist/core/group/extractors/grpc-patterns/index.js +46 -0
  10. package/dist/core/group/extractors/grpc-patterns/java.d.ts +2 -0
  11. package/dist/core/group/extractors/grpc-patterns/java.js +173 -0
  12. package/dist/core/group/extractors/grpc-patterns/node.d.ts +4 -0
  13. package/dist/core/group/extractors/grpc-patterns/node.js +290 -0
  14. package/dist/core/group/extractors/grpc-patterns/proto.d.ts +9 -0
  15. package/dist/core/group/extractors/grpc-patterns/proto.js +134 -0
  16. package/dist/core/group/extractors/grpc-patterns/python.d.ts +2 -0
  17. package/dist/core/group/extractors/grpc-patterns/python.js +67 -0
  18. package/dist/core/group/extractors/grpc-patterns/types.d.ts +50 -0
  19. package/dist/core/group/extractors/grpc-patterns/types.js +1 -0
  20. package/dist/core/group/extractors/http-patterns/go.d.ts +2 -0
  21. package/dist/core/group/extractors/http-patterns/go.js +215 -0
  22. package/dist/core/group/extractors/http-patterns/index.d.ts +17 -0
  23. package/dist/core/group/extractors/http-patterns/index.js +44 -0
  24. package/dist/core/group/extractors/http-patterns/java.d.ts +2 -0
  25. package/dist/core/group/extractors/http-patterns/java.js +253 -0
  26. package/dist/core/group/extractors/http-patterns/node.d.ts +4 -0
  27. package/dist/core/group/extractors/http-patterns/node.js +354 -0
  28. package/dist/core/group/extractors/http-patterns/php.d.ts +2 -0
  29. package/dist/core/group/extractors/http-patterns/php.js +70 -0
  30. package/dist/core/group/extractors/http-patterns/python.d.ts +2 -0
  31. package/dist/core/group/extractors/http-patterns/python.js +133 -0
  32. package/dist/core/group/extractors/http-patterns/types.d.ts +61 -0
  33. package/dist/core/group/extractors/http-patterns/types.js +1 -0
  34. package/dist/core/group/extractors/http-route-extractor.d.ts +10 -13
  35. package/dist/core/group/extractors/http-route-extractor.js +201 -238
  36. package/dist/core/group/extractors/manifest-extractor.d.ts +54 -0
  37. package/dist/core/group/extractors/manifest-extractor.js +235 -0
  38. package/dist/core/group/extractors/topic-extractor.d.ts +0 -1
  39. package/dist/core/group/extractors/topic-extractor.js +55 -192
  40. package/dist/core/group/extractors/topic-patterns/go.d.ts +2 -0
  41. package/dist/core/group/extractors/topic-patterns/go.js +120 -0
  42. package/dist/core/group/extractors/topic-patterns/index.d.ts +14 -0
  43. package/dist/core/group/extractors/topic-patterns/index.js +38 -0
  44. package/dist/core/group/extractors/topic-patterns/java.d.ts +2 -0
  45. package/dist/core/group/extractors/topic-patterns/java.js +80 -0
  46. package/dist/core/group/extractors/topic-patterns/node.d.ts +4 -0
  47. package/dist/core/group/extractors/topic-patterns/node.js +155 -0
  48. package/dist/core/group/extractors/topic-patterns/python.d.ts +2 -0
  49. package/dist/core/group/extractors/topic-patterns/python.js +116 -0
  50. package/dist/core/group/extractors/topic-patterns/types.d.ts +25 -0
  51. package/dist/core/group/extractors/topic-patterns/types.js +10 -0
  52. package/dist/core/group/extractors/tree-sitter-scanner.d.ts +113 -0
  53. package/dist/core/group/extractors/tree-sitter-scanner.js +94 -0
  54. package/dist/core/ingestion/binding-accumulator.d.ts +22 -17
  55. package/dist/core/ingestion/binding-accumulator.js +29 -25
  56. package/dist/core/ingestion/cobol-processor.d.ts +1 -1
  57. package/dist/core/ingestion/import-processor.js +1 -1
  58. package/dist/core/ingestion/language-config.js +1 -1
  59. package/dist/core/ingestion/language-provider.d.ts +8 -0
  60. package/dist/core/ingestion/languages/ruby.js +15 -0
  61. package/dist/core/ingestion/markdown-processor.d.ts +1 -1
  62. package/dist/core/ingestion/method-extractors/configs/jvm.js +1 -0
  63. package/dist/core/ingestion/method-extractors/configs/ruby.js +1 -0
  64. package/dist/core/ingestion/method-extractors/generic.d.ts +6 -0
  65. package/dist/core/ingestion/method-extractors/generic.js +48 -4
  66. package/dist/core/ingestion/method-types.d.ts +4 -0
  67. package/dist/core/ingestion/model/resolve.js +103 -48
  68. package/dist/core/ingestion/model/semantic-model.d.ts +1 -1
  69. package/dist/core/ingestion/model/semantic-model.js +1 -1
  70. package/dist/core/ingestion/model/symbol-table.d.ts +7 -7
  71. package/dist/core/ingestion/model/symbol-table.js +7 -7
  72. package/dist/core/ingestion/mro-processor.d.ts +1 -1
  73. package/dist/core/ingestion/mro-processor.js +1 -1
  74. package/dist/core/ingestion/parsing-processor.js +54 -42
  75. package/dist/core/ingestion/pipeline-phases/cobol.d.ts +16 -0
  76. package/dist/core/ingestion/pipeline-phases/cobol.js +45 -0
  77. package/dist/core/ingestion/pipeline-phases/communities.d.ts +16 -0
  78. package/dist/core/ingestion/pipeline-phases/communities.js +62 -0
  79. package/dist/core/ingestion/pipeline-phases/cross-file-impl.d.ts +17 -0
  80. package/dist/core/ingestion/pipeline-phases/cross-file-impl.js +156 -0
  81. package/dist/core/ingestion/pipeline-phases/cross-file.d.ts +37 -0
  82. package/dist/core/ingestion/pipeline-phases/cross-file.js +63 -0
  83. package/dist/core/ingestion/pipeline-phases/index.d.ts +21 -0
  84. package/dist/core/ingestion/pipeline-phases/index.js +22 -0
  85. package/dist/core/ingestion/pipeline-phases/markdown.d.ts +17 -0
  86. package/dist/core/ingestion/pipeline-phases/markdown.js +33 -0
  87. package/dist/core/ingestion/pipeline-phases/mro.d.ts +18 -0
  88. package/dist/core/ingestion/pipeline-phases/mro.js +36 -0
  89. package/dist/core/ingestion/pipeline-phases/orm-extraction.d.ts +22 -0
  90. package/dist/core/ingestion/pipeline-phases/orm-extraction.js +92 -0
  91. package/dist/core/ingestion/pipeline-phases/orm.d.ts +15 -0
  92. package/dist/core/ingestion/pipeline-phases/orm.js +74 -0
  93. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +47 -0
  94. package/dist/core/ingestion/pipeline-phases/parse-impl.js +437 -0
  95. package/dist/core/ingestion/pipeline-phases/parse.d.ts +49 -0
  96. package/dist/core/ingestion/pipeline-phases/parse.js +33 -0
  97. package/dist/core/ingestion/pipeline-phases/processes.d.ts +16 -0
  98. package/dist/core/ingestion/pipeline-phases/processes.js +143 -0
  99. package/dist/core/ingestion/pipeline-phases/routes.d.ts +21 -0
  100. package/dist/core/ingestion/pipeline-phases/routes.js +243 -0
  101. package/dist/core/ingestion/pipeline-phases/runner.d.ts +22 -0
  102. package/dist/core/ingestion/pipeline-phases/runner.js +203 -0
  103. package/dist/core/ingestion/pipeline-phases/scan.d.ts +21 -0
  104. package/dist/core/ingestion/pipeline-phases/scan.js +46 -0
  105. package/dist/core/ingestion/pipeline-phases/structure.d.ts +27 -0
  106. package/dist/core/ingestion/pipeline-phases/structure.js +35 -0
  107. package/dist/core/ingestion/pipeline-phases/tools.d.ts +20 -0
  108. package/dist/core/ingestion/pipeline-phases/tools.js +79 -0
  109. package/dist/core/ingestion/pipeline-phases/types.d.ts +79 -0
  110. package/dist/core/ingestion/pipeline-phases/types.js +37 -0
  111. package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.d.ts +35 -0
  112. package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.js +174 -0
  113. package/dist/core/ingestion/pipeline.d.ts +16 -10
  114. package/dist/core/ingestion/pipeline.js +66 -1534
  115. package/dist/core/ingestion/process-processor.js +1 -1
  116. package/dist/core/ingestion/tree-sitter-queries.d.ts +2 -2
  117. package/dist/core/ingestion/tree-sitter-queries.js +69 -0
  118. package/dist/core/ingestion/utils/ast-helpers.d.ts +1 -3
  119. package/dist/core/ingestion/utils/ast-helpers.js +48 -21
  120. package/dist/core/ingestion/utils/env.d.ts +10 -0
  121. package/dist/core/ingestion/utils/env.js +10 -0
  122. package/dist/core/ingestion/utils/graph-sort.d.ts +58 -0
  123. package/dist/core/ingestion/utils/graph-sort.js +100 -0
  124. package/dist/core/ingestion/workers/parse-worker.js +12 -8
  125. package/dist/core/lbug/lbug-adapter.js +66 -24
  126. package/package.json +3 -3
  127. package/vendor/tree-sitter-proto/binding.gyp +30 -0
  128. package/vendor/tree-sitter-proto/bindings/node/binding.cc +20 -0
  129. package/vendor/tree-sitter-proto/bindings/node/index.d.ts +28 -0
  130. package/vendor/tree-sitter-proto/bindings/node/index.js +7 -0
  131. package/vendor/tree-sitter-proto/package.json +18 -0
  132. package/vendor/tree-sitter-proto/src/node-types.json +1145 -0
  133. package/vendor/tree-sitter-proto/src/parser.c +10149 -0
  134. package/vendor/tree-sitter-proto/src/tree_sitter/alloc.h +54 -0
  135. package/vendor/tree-sitter-proto/src/tree_sitter/array.h +291 -0
  136. package/vendor/tree-sitter-proto/src/tree_sitter/parser.h +266 -0
@@ -1,6 +1,30 @@
1
- import * as fs from 'node:fs';
2
1
  import * as path from 'node:path';
3
2
  import { glob } from 'glob';
3
+ import Parser from 'tree-sitter';
4
+ import { readSafe } from './fs-utils.js';
5
+ import { getPluginForFile, HTTP_SCAN_GLOB } from './http-patterns/index.js';
6
+ /**
7
+ * Language-agnostic orchestrator for HTTP route (provider + consumer)
8
+ * contract extraction. Two strategies, in order of preference per role:
9
+ *
10
+ * 1. **Graph-assisted (Strategy A)** — if a per-repo LadybugDB executor
11
+ * is available, read `HANDLES_ROUTE` / `FETCHES` Cypher edges that
12
+ * the ingestion pipeline already produced via tree-sitter. This is
13
+ * the preferred path because the graph has richer symbol metadata
14
+ * (real uids, class/method structure, etc.).
15
+ *
16
+ * 2. **Source-scan fallback (Strategy B)** — parse files directly with
17
+ * the per-language plugin registry in `./http-patterns/`. Used when
18
+ * the graph has no routes/fetches for this repo (e.g. a repo that
19
+ * hasn't been indexed yet, or whose indexer doesn't know the
20
+ * framework). Each plugin owns its tree-sitter grammar and query
21
+ * sources — this orchestrator imports NO grammars or query strings.
22
+ *
23
+ * Adding a new language for Strategy B is a one-file edit in
24
+ * `http-patterns/index.ts`: register a new `HttpLanguagePlugin` and
25
+ * widen `HTTP_SCAN_GLOB` if needed.
26
+ */
27
+ // ─── Graph-assisted queries ──────────────────────────────────────────
4
28
  const HANDLES_ROUTE_QUERY = `
5
29
  MATCH (handlerFile:File)-[r:CodeRelation {type: 'HANDLES_ROUTE'}]->(route:Route)
6
30
  RETURN handlerFile.id AS fileId, handlerFile.filePath AS filePath,
@@ -17,13 +41,52 @@ MATCH (file:File {id: $fileId})<-[:CodeRelation {type: 'CONTAINS'}]-(sym)
17
41
  WHERE sym.startLine IS NOT NULL
18
42
  RETURN sym.id AS uid, sym.name AS name, sym.filePath AS filePath, labels(sym) AS labels
19
43
  ORDER BY sym.startLine`;
44
+ // ─── Path normalization (shared between provider / consumer paths) ──
45
+ /**
46
+ * Canonicalize a provider-side HTTP path for contract-id generation:
47
+ * - strip query string
48
+ * - lower-case
49
+ * - drop trailing slash
50
+ * - collapse `:id`, `{id}`, `[id]` path params into a single `{param}`
51
+ */
20
52
  export function normalizeHttpPath(p) {
21
53
  let s = p.trim().split('?')[0].toLowerCase().replace(/\/+$/, '');
22
54
  s = s.replace(/:\w+/g, '{param}');
23
55
  s = s.replace(/\{[^}]+\}/g, '{param}');
24
56
  s = s.replace(/\[[^\]]+\]/g, '{param}');
25
- return s;
57
+ // Preserve root: after stripping trailing slashes, the root "/"
58
+ // collapses to "" which would produce malformed contract ids like
59
+ // `http::GET::`. Restore a single slash for the root case.
60
+ return s === '' ? '/' : s;
26
61
  }
62
+ /**
63
+ * Consumer-side normalization is more aggressive:
64
+ * - template literals (`${x}`) → `{param}`
65
+ * - strip protocol + host if the URL is absolute
66
+ * - numeric segments → `{param}` (so `/api/orders/42` → `/api/orders/{param}`)
67
+ */
68
+ function normalizeConsumerPath(url) {
69
+ const templated = url.replace(/\$\{[^}]+\}/g, '{param}').trim();
70
+ let pathOnly = templated;
71
+ if (/^https?:\/\//i.test(templated)) {
72
+ try {
73
+ pathOnly = new URL(templated).pathname;
74
+ }
75
+ catch {
76
+ pathOnly = templated.replace(/^https?:\/\/[^/]+/i, '');
77
+ }
78
+ }
79
+ const normalized = normalizeHttpPath(pathOnly || '/');
80
+ const segments = normalized
81
+ .split('/')
82
+ .filter(Boolean)
83
+ .map((segment) => (/^\d+$/.test(segment) ? '{param}' : segment));
84
+ return `/${segments.join('/')}`.replace(/\/+$/, '') || '/';
85
+ }
86
+ function contractIdFor(method, pathNorm) {
87
+ return `http::${method.toUpperCase()}::${pathNorm}`;
88
+ }
89
+ // ─── Graph row helpers ───────────────────────────────────────────────
27
90
  function methodFromRouteReason(reason) {
28
91
  const r = reason || '';
29
92
  if (/GetMapping|decorator-Get/i.test(r))
@@ -38,47 +101,6 @@ function methodFromRouteReason(reason) {
38
101
  return 'PATCH';
39
102
  return null;
40
103
  }
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
104
  function pickSymbolUid(rows, preferredName) {
83
105
  const norm = (x) => String(x ?? '');
84
106
  const labeled = rows.filter((r) => {
@@ -104,19 +126,71 @@ function pickSymbolUid(rows, preferredName) {
104
126
  filePath: norm(first?.filePath ?? first?.[2]),
105
127
  };
106
128
  }
129
+ // ─── Orchestrator ────────────────────────────────────────────────────
107
130
  export class HttpRouteExtractor {
108
131
  type = 'http';
109
132
  async canExtract(_repo) {
110
133
  return true;
111
134
  }
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);
135
+ async extract(dbExecutor, repoPath, _repo) {
136
+ // Parse each file at most once and reuse the plugin results across
137
+ // both graph-assisted enrichment and source-scan emission.
138
+ const parser = new Parser();
139
+ const cachedDetections = new Map();
140
+ const getDetections = (rel) => {
141
+ const cached = cachedDetections.get(rel);
142
+ if (cached)
143
+ return cached;
144
+ const plugin = getPluginForFile(rel);
145
+ if (!plugin) {
146
+ cachedDetections.set(rel, []);
147
+ return [];
148
+ }
149
+ const content = readSafe(repoPath, rel);
150
+ if (!content) {
151
+ cachedDetections.set(rel, []);
152
+ return [];
153
+ }
154
+ try {
155
+ parser.setLanguage(plugin.language);
156
+ const tree = parser.parse(content);
157
+ const detections = plugin.scan(tree);
158
+ cachedDetections.set(rel, detections);
159
+ return detections;
160
+ }
161
+ catch {
162
+ cachedDetections.set(rel, []);
163
+ return [];
164
+ }
165
+ };
166
+ // Glob the source-scan file list at most once per extract() —
167
+ // both provider and consumer fallback paths share the same list.
168
+ let scannedFiles = null;
169
+ const getScannedFiles = async () => {
170
+ if (scannedFiles)
171
+ return scannedFiles;
172
+ scannedFiles = await this.scanFiles(repoPath);
173
+ return scannedFiles;
174
+ };
175
+ const graphProviders = dbExecutor != null ? await this.extractProvidersGraph(dbExecutor, getDetections) : [];
176
+ const providers = graphProviders.length > 0
177
+ ? graphProviders
178
+ : this.extractProvidersSourceScan(await getScannedFiles(), getDetections);
179
+ const graphConsumers = dbExecutor != null ? await this.extractConsumersGraph(dbExecutor, getDetections) : [];
180
+ const consumers = graphConsumers.length > 0
181
+ ? graphConsumers
182
+ : this.extractConsumersSourceScan(await getScannedFiles(), getDetections);
117
183
  return [...providers, ...consumers];
118
184
  }
119
- async extractProvidersGraph(db, repoPath) {
185
+ async scanFiles(repoPath) {
186
+ return glob(HTTP_SCAN_GLOB, {
187
+ cwd: repoPath,
188
+ ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**', '**/vendor/**'],
189
+ nodir: true,
190
+ });
191
+ }
192
+ // ─── Graph-assisted providers ──────────────────────────────────────
193
+ async extractProvidersGraph(db, getDetections) {
120
194
  const out = [];
121
195
  let rows;
122
196
  try {
@@ -130,15 +204,26 @@ export class HttpRouteExtractor {
130
204
  const routePath = String(row.routePath ?? '');
131
205
  const routeSource = String(row.routeSource ?? row.routeReason ?? '');
132
206
  let method = methodFromRouteReason(routeSource);
133
- const content = readSafe(repoPath, filePath);
134
- if (!method && content) {
135
- method = this.inferMethodFromFileScan(content, routePath, 'provider');
207
+ // Look up handler name (and backfill method if missing) from the
208
+ // plugin's scan of the handler file. This replaces the old
209
+ // regex-based `inferMethodFromFileScan` and `pickJavaHandlerName`
210
+ // helpers — tree-sitter gives both pieces of information
211
+ // structurally. Always run the lookup: even when method is set by
212
+ // `methodFromRouteReason`, we still need the handler name.
213
+ const detections = filePath ? getDetections(filePath) : [];
214
+ const providerDetections = detections.filter((d) => d.role === 'provider');
215
+ let handlerName = null;
216
+ const normalizedRoute = normalizeHttpPath(routePath);
217
+ const match = providerDetections.find((d) => normalizeHttpPath(d.path) === normalizedRoute);
218
+ if (match) {
219
+ if (!method)
220
+ method = match.method;
221
+ handlerName = match.name;
136
222
  }
137
223
  if (!method)
138
224
  method = 'GET';
139
225
  const pathNorm = normalizeHttpPath(routePath);
140
226
  const cid = contractIdFor(method, pathNorm);
141
- const handlerName = content && routePath ? pickJavaHandlerName(content, routePath, method) : null;
142
227
  let symbolUid = '';
143
228
  let symbolName = path.basename(filePath) || 'handler';
144
229
  let symPath = filePath;
@@ -176,134 +261,37 @@ export class HttpRouteExtractor {
176
261
  }
177
262
  return out;
178
263
  }
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
- });
264
+ // ─── Source-scan providers ─────────────────────────────────────────
265
+ extractProvidersSourceScan(files, getDetections) {
202
266
  const out = [];
203
267
  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));
268
+ const detections = getDetections(rel);
269
+ for (const d of detections) {
270
+ if (d.role !== 'provider')
271
+ continue;
272
+ const pathNorm = normalizeHttpPath(d.path);
273
+ out.push({
274
+ contractId: contractIdFor(d.method, pathNorm),
275
+ type: 'http',
276
+ role: 'provider',
277
+ symbolUid: '',
278
+ symbolRef: { filePath: rel, name: d.name ?? 'handler' },
279
+ symbolName: d.name ?? 'handler',
280
+ confidence: d.confidence,
281
+ meta: {
282
+ method: d.method,
283
+ path: pathNorm,
284
+ pathSegments: pathNorm.split('/').filter(Boolean),
285
+ extractionStrategy: 'source_scan',
286
+ framework: d.framework,
287
+ },
288
+ });
289
+ }
211
290
  }
212
291
  return this.dedupeContracts(out);
213
292
  }
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) {
293
+ // ─── Graph-assisted consumers ──────────────────────────────────────
294
+ async extractConsumersGraph(db, getDetections) {
307
295
  const out = [];
308
296
  let rows;
309
297
  try {
@@ -317,12 +305,12 @@ export class HttpRouteExtractor {
317
305
  const routePath = String(row.routePath ?? '');
318
306
  const pathNorm = normalizeHttpPath(routePath);
319
307
  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
- }
308
+ // Prefer the plugin's detected method if we can find a matching
309
+ // fetch/axios call in the same file.
310
+ const detections = filePath ? getDetections(filePath) : [];
311
+ const inferred = detections.find((d) => d.role === 'consumer' && normalizeConsumerPath(d.path) === pathNorm);
312
+ if (inferred)
313
+ method = inferred.method;
326
314
  const cid = contractIdFor(method, pathNorm);
327
315
  let symbolUid = '';
328
316
  let symbolName = 'fetch';
@@ -360,69 +348,44 @@ export class HttpRouteExtractor {
360
348
  }
361
349
  return out;
362
350
  }
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
- });
351
+ // ─── Source-scan consumers ─────────────────────────────────────────
352
+ extractConsumersSourceScan(files, getDetections) {
377
353
  const out = [];
378
354
  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));
355
+ const detections = getDetections(rel);
356
+ for (const d of detections) {
357
+ if (d.role !== 'consumer')
358
+ continue;
359
+ const pathNorm = normalizeConsumerPath(d.path);
360
+ out.push({
361
+ contractId: contractIdFor(d.method, pathNorm),
362
+ type: 'http',
363
+ role: 'consumer',
364
+ symbolUid: '',
365
+ symbolRef: { filePath: rel, name: 'fetch' },
366
+ symbolName: 'fetch',
367
+ confidence: d.confidence,
368
+ meta: {
369
+ method: d.method,
370
+ path: pathNorm,
371
+ extractionStrategy: 'source_scan',
372
+ framework: d.framework,
373
+ },
374
+ });
375
+ }
384
376
  }
385
377
  return this.dedupeContracts(out);
386
378
  }
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) {
379
+ dedupeContracts(items) {
380
+ const seen = new Set();
402
381
  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));
382
+ for (const c of items) {
383
+ const k = `${c.contractId}|${c.symbolRef.filePath}|${c.symbolRef.name}`;
384
+ if (seen.has(k))
385
+ continue;
386
+ seen.add(k);
387
+ out.push(c);
409
388
  }
410
389
  return out;
411
390
  }
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
391
  }
@@ -0,0 +1,54 @@
1
+ import type { CrossLink, GroupManifestLink, StoredContract } from '../types.js';
2
+ import type { CypherExecutor } from '../contract-extractor.js';
3
+ export interface ManifestExtractResult {
4
+ contracts: StoredContract[];
5
+ crossLinks: CrossLink[];
6
+ }
7
+ /**
8
+ * Stable synthetic symbolUid for a manifest-declared contract whose target
9
+ * symbol could not be resolved against the per-repo graph (resolveSymbol
10
+ * returned null). Two reasons we don't leave the uid empty:
11
+ *
12
+ * 1. The bridge stores Contract nodes keyed in part by symbolUid; an empty
13
+ * uid means downstream Cypher queries that anchor on `provider.symbolUid`
14
+ * can't tell two different unresolved manifest contracts apart.
15
+ * 2. The cross-impact bridge query in cross-impact.ts joins local impact
16
+ * results to bridge contracts via `WHERE provider.symbolUid IN $localUids`.
17
+ * If the local impact engine produces a deterministic identifier for the
18
+ * unresolved target, it must agree with the value the bridge stored. A
19
+ * synthetic uid keyed off (repo, contractId) is the only thing both sides
20
+ * can derive without knowing about each other.
21
+ *
22
+ * Format: `manifest::<repo>::<contractId>`. Stable across syncs, scoped to a
23
+ * single repo within a group, and never collides with real indexer uids
24
+ * (which never start with `manifest::`).
25
+ */
26
+ export declare function manifestSymbolUid(repo: string, contractId: string): string;
27
+ export declare class ManifestExtractor {
28
+ extractFromManifest(links: GroupManifestLink[], dbExecutors?: Map<string, CypherExecutor>): Promise<ManifestExtractResult>;
29
+ private resolveSymbol;
30
+ /**
31
+ * Build a canonical contract id for a manifest link.
32
+ *
33
+ * HTTP is the only type with two valid forms:
34
+ * - Explicit method: `"GET::/api/orders"` → `"http::GET::/api/orders"`
35
+ * (matches exactly against `HttpRouteExtractor` provider/consumer
36
+ * contracts, which are also keyed by `http::<METHOD>::<path>`).
37
+ * - Method-agnostic: `"/api/orders"` → `"http::*::/api/orders"`
38
+ * — the `*` is a wildcard and is intended to match any concrete
39
+ * HTTP method on that path. Wildcard-aware matching is the
40
+ * responsibility of the sync / cross-impact layer (see #793);
41
+ * downstream code should treat `http::*::<path>` as matching
42
+ * every `http::<METHOD>::<path>` for the same path.
43
+ *
44
+ * Recommend the explicit-method form in group.yaml whenever the
45
+ * manifest author knows the method — it round-trips through exact
46
+ * equality matching without requiring wildcard logic downstream.
47
+ *
48
+ * NOTE on exhaustiveness: the switch covers every current
49
+ * `ContractType` variant and falls through to a `never` assertion so
50
+ * TypeScript fails the build if a new variant is added without a
51
+ * corresponding case.
52
+ */
53
+ private buildContractId;
54
+ }