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.
- package/dist/cli/analyze.js +28 -3
- package/dist/core/group/extractors/fs-utils.d.ts +10 -0
- package/dist/core/group/extractors/fs-utils.js +24 -0
- package/dist/core/group/extractors/grpc-extractor.d.ts +17 -8
- package/dist/core/group/extractors/grpc-extractor.js +313 -191
- package/dist/core/group/extractors/grpc-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/go.js +97 -0
- package/dist/core/group/extractors/grpc-patterns/index.d.ts +19 -0
- package/dist/core/group/extractors/grpc-patterns/index.js +46 -0
- package/dist/core/group/extractors/grpc-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/java.js +173 -0
- package/dist/core/group/extractors/grpc-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/grpc-patterns/node.js +290 -0
- package/dist/core/group/extractors/grpc-patterns/proto.d.ts +9 -0
- package/dist/core/group/extractors/grpc-patterns/proto.js +134 -0
- package/dist/core/group/extractors/grpc-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/grpc-patterns/python.js +67 -0
- package/dist/core/group/extractors/grpc-patterns/types.d.ts +50 -0
- package/dist/core/group/extractors/grpc-patterns/types.js +1 -0
- package/dist/core/group/extractors/http-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/go.js +215 -0
- package/dist/core/group/extractors/http-patterns/index.d.ts +17 -0
- package/dist/core/group/extractors/http-patterns/index.js +44 -0
- package/dist/core/group/extractors/http-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/java.js +253 -0
- package/dist/core/group/extractors/http-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/http-patterns/node.js +354 -0
- package/dist/core/group/extractors/http-patterns/php.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/php.js +70 -0
- package/dist/core/group/extractors/http-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/http-patterns/python.js +133 -0
- package/dist/core/group/extractors/http-patterns/types.d.ts +61 -0
- package/dist/core/group/extractors/http-patterns/types.js +1 -0
- package/dist/core/group/extractors/http-route-extractor.d.ts +10 -13
- package/dist/core/group/extractors/http-route-extractor.js +201 -238
- package/dist/core/group/extractors/manifest-extractor.d.ts +54 -0
- package/dist/core/group/extractors/manifest-extractor.js +235 -0
- package/dist/core/group/extractors/topic-extractor.d.ts +0 -1
- package/dist/core/group/extractors/topic-extractor.js +55 -192
- package/dist/core/group/extractors/topic-patterns/go.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/go.js +120 -0
- package/dist/core/group/extractors/topic-patterns/index.d.ts +14 -0
- package/dist/core/group/extractors/topic-patterns/index.js +38 -0
- package/dist/core/group/extractors/topic-patterns/java.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/java.js +80 -0
- package/dist/core/group/extractors/topic-patterns/node.d.ts +4 -0
- package/dist/core/group/extractors/topic-patterns/node.js +155 -0
- package/dist/core/group/extractors/topic-patterns/python.d.ts +2 -0
- package/dist/core/group/extractors/topic-patterns/python.js +116 -0
- package/dist/core/group/extractors/topic-patterns/types.d.ts +25 -0
- package/dist/core/group/extractors/topic-patterns/types.js +10 -0
- package/dist/core/group/extractors/tree-sitter-scanner.d.ts +113 -0
- package/dist/core/group/extractors/tree-sitter-scanner.js +94 -0
- package/dist/core/ingestion/binding-accumulator.d.ts +22 -17
- package/dist/core/ingestion/binding-accumulator.js +29 -25
- package/dist/core/ingestion/cobol-processor.d.ts +1 -1
- package/dist/core/ingestion/import-processor.js +1 -1
- package/dist/core/ingestion/language-config.js +1 -1
- package/dist/core/ingestion/language-provider.d.ts +8 -0
- package/dist/core/ingestion/languages/ruby.js +15 -0
- package/dist/core/ingestion/markdown-processor.d.ts +1 -1
- package/dist/core/ingestion/method-extractors/configs/jvm.js +1 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.js +1 -0
- package/dist/core/ingestion/method-extractors/generic.d.ts +6 -0
- package/dist/core/ingestion/method-extractors/generic.js +48 -4
- package/dist/core/ingestion/method-types.d.ts +4 -0
- package/dist/core/ingestion/model/resolve.js +103 -48
- package/dist/core/ingestion/model/semantic-model.d.ts +1 -1
- package/dist/core/ingestion/model/semantic-model.js +1 -1
- package/dist/core/ingestion/model/symbol-table.d.ts +7 -7
- package/dist/core/ingestion/model/symbol-table.js +7 -7
- package/dist/core/ingestion/mro-processor.d.ts +1 -1
- package/dist/core/ingestion/mro-processor.js +1 -1
- package/dist/core/ingestion/parsing-processor.js +54 -42
- package/dist/core/ingestion/pipeline-phases/cobol.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/cobol.js +45 -0
- package/dist/core/ingestion/pipeline-phases/communities.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/communities.js +62 -0
- package/dist/core/ingestion/pipeline-phases/cross-file-impl.d.ts +17 -0
- package/dist/core/ingestion/pipeline-phases/cross-file-impl.js +156 -0
- package/dist/core/ingestion/pipeline-phases/cross-file.d.ts +37 -0
- package/dist/core/ingestion/pipeline-phases/cross-file.js +63 -0
- package/dist/core/ingestion/pipeline-phases/index.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/index.js +22 -0
- package/dist/core/ingestion/pipeline-phases/markdown.d.ts +17 -0
- package/dist/core/ingestion/pipeline-phases/markdown.js +33 -0
- package/dist/core/ingestion/pipeline-phases/mro.d.ts +18 -0
- package/dist/core/ingestion/pipeline-phases/mro.js +36 -0
- package/dist/core/ingestion/pipeline-phases/orm-extraction.d.ts +22 -0
- package/dist/core/ingestion/pipeline-phases/orm-extraction.js +92 -0
- package/dist/core/ingestion/pipeline-phases/orm.d.ts +15 -0
- package/dist/core/ingestion/pipeline-phases/orm.js +74 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +47 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +437 -0
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +49 -0
- package/dist/core/ingestion/pipeline-phases/parse.js +33 -0
- package/dist/core/ingestion/pipeline-phases/processes.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/processes.js +143 -0
- package/dist/core/ingestion/pipeline-phases/routes.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/routes.js +243 -0
- package/dist/core/ingestion/pipeline-phases/runner.d.ts +22 -0
- package/dist/core/ingestion/pipeline-phases/runner.js +203 -0
- package/dist/core/ingestion/pipeline-phases/scan.d.ts +21 -0
- package/dist/core/ingestion/pipeline-phases/scan.js +46 -0
- package/dist/core/ingestion/pipeline-phases/structure.d.ts +27 -0
- package/dist/core/ingestion/pipeline-phases/structure.js +35 -0
- package/dist/core/ingestion/pipeline-phases/tools.d.ts +20 -0
- package/dist/core/ingestion/pipeline-phases/tools.js +79 -0
- package/dist/core/ingestion/pipeline-phases/types.d.ts +79 -0
- package/dist/core/ingestion/pipeline-phases/types.js +37 -0
- package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.d.ts +35 -0
- package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.js +174 -0
- package/dist/core/ingestion/pipeline.d.ts +16 -10
- package/dist/core/ingestion/pipeline.js +66 -1534
- package/dist/core/ingestion/process-processor.js +1 -1
- package/dist/core/ingestion/tree-sitter-queries.d.ts +2 -2
- package/dist/core/ingestion/tree-sitter-queries.js +69 -0
- package/dist/core/ingestion/utils/ast-helpers.d.ts +1 -3
- package/dist/core/ingestion/utils/ast-helpers.js +48 -21
- package/dist/core/ingestion/utils/env.d.ts +10 -0
- package/dist/core/ingestion/utils/env.js +10 -0
- package/dist/core/ingestion/utils/graph-sort.d.ts +58 -0
- package/dist/core/ingestion/utils/graph-sort.js +100 -0
- package/dist/core/ingestion/workers/parse-worker.js +12 -8
- package/dist/core/lbug/lbug-adapter.js +66 -24
- package/package.json +3 -3
- package/vendor/tree-sitter-proto/binding.gyp +30 -0
- package/vendor/tree-sitter-proto/bindings/node/binding.cc +20 -0
- package/vendor/tree-sitter-proto/bindings/node/index.d.ts +28 -0
- package/vendor/tree-sitter-proto/bindings/node/index.js +7 -0
- package/vendor/tree-sitter-proto/package.json +18 -0
- package/vendor/tree-sitter-proto/src/node-types.json +1145 -0
- package/vendor/tree-sitter-proto/src/parser.c +10149 -0
- package/vendor/tree-sitter-proto/src/tree_sitter/alloc.h +54 -0
- package/vendor/tree-sitter-proto/src/tree_sitter/array.h +291 -0
- 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
|
-
|
|
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,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
const
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
364
|
-
|
|
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
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
388
|
-
const
|
|
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
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
out.push(
|
|
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
|
+
}
|