gitnexus 1.6.9-rc.7 → 1.6.9-rc.8
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/core/group/extractors/http-patterns/java.js +18 -0
- package/dist/core/group/extractors/http-patterns/php.js +11 -0
- package/dist/core/group/extractors/http-patterns/python.js +14 -0
- package/dist/core/group/extractors/http-patterns/types.d.ts +37 -0
- package/dist/core/group/extractors/http-route-extractor.d.ts +1 -1
- package/dist/core/group/extractors/http-route-extractor.js +131 -59
- package/dist/core/ingestion/call-processor.d.ts +26 -0
- package/dist/core/ingestion/call-processor.js +73 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +4 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +5 -1
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +3 -0
- package/dist/core/ingestion/pipeline-phases/routes.d.ts +2 -1
- package/dist/core/ingestion/pipeline-phases/routes.js +6 -7
- package/dist/core/ingestion/route-extractors/route-path.d.ts +15 -0
- package/dist/core/ingestion/route-extractors/route-path.js +20 -0
- package/dist/core/ingestion/route-extractors/spring.js +4 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +10 -0
- package/dist/core/lbug/csv-generator.js +2 -1
- package/dist/core/lbug/lbug-adapter.js +1 -1
- package/dist/core/lbug/schema.d.ts +1 -1
- package/dist/core/lbug/schema.js +1 -0
- package/dist/storage/parse-cache.js +1 -1
- package/package.json +1 -1
|
@@ -574,6 +574,24 @@ function scanSpringProject(files) {
|
|
|
574
574
|
export const JAVA_HTTP_PLUGIN = {
|
|
575
575
|
name: 'java-http',
|
|
576
576
|
language: Java,
|
|
577
|
+
// routeCoverage intentionally LEFT at the default 'partial' (#2138 Part 2).
|
|
578
|
+
// The graph provider set is a strict *subset* of this scan()'s provider set —
|
|
579
|
+
// ingestion does NOT emit a Route node for (1) array-form `@GetMapping({...})`,
|
|
580
|
+
// (2) interface-inherited Spring routes, or (3) the 2nd verb of a same-URL
|
|
581
|
+
// GET+POST pair (Route nodes are URL-keyed). Declaring 'complete' here would
|
|
582
|
+
// let the parse-skip drop those group-only providers. Java flips to 'complete'
|
|
583
|
+
// only once ingestion provider extraction matches this scan (a follow-up:
|
|
584
|
+
// array-form query branch + interface-inheritance emission + per-verb Route
|
|
585
|
+
// identity). `hasConsumerSignals` below is kept ready for that flip.
|
|
586
|
+
// Consumer signals this plugin's scan() can detect: RestTemplate / WebClient /
|
|
587
|
+
// OkHttp / Java-HttpClient / Apache-HttpClient call sites, OpenFeign
|
|
588
|
+
// (`@FeignClient` + `@RequestLine`) interfaces, and Spring 6 HTTP Interface
|
|
589
|
+
// `@(Get|...)Exchange` / `@HttpExchange`. A provider-covered file containing
|
|
590
|
+
// any of these must still be parsed so its consumer contracts are not dropped
|
|
591
|
+
// (ingestion emits no FETCHES for Java). Conservative by design.
|
|
592
|
+
hasConsumerSignals(content) {
|
|
593
|
+
return /\brestTemplate\b|\bwebClient\b|Request\.Builder|HttpRequest|HttpMethod\.|new\s+Http(Get|Post|Put|Delete|Patch)\b|@RequestLine|@FeignClient|Exchange/.test(content);
|
|
594
|
+
},
|
|
577
595
|
scan(tree) {
|
|
578
596
|
const out = [];
|
|
579
597
|
// ─── Spring providers + OpenFeign consumers (one query pass) ────
|
|
@@ -104,6 +104,17 @@ function isHttpUrlLiteral(path) {
|
|
|
104
104
|
export const PHP_HTTP_PLUGIN = {
|
|
105
105
|
name: 'php-http',
|
|
106
106
|
language: PHP.php_only,
|
|
107
|
+
// Laravel `Route::<verb>(...)` definitions are emitted as Route nodes by
|
|
108
|
+
// ingestion, so the graph is authoritative for PHP providers (#2138 Part 2).
|
|
109
|
+
routeCoverage: 'complete',
|
|
110
|
+
// Consumer signals scan() can detect: Laravel `Http::<verb>`, Guzzle client
|
|
111
|
+
// `->get/post/.../request(...)`, and `file_get_contents` of an HTTP URL. A
|
|
112
|
+
// provider-covered file with any of these must still be parsed (ingestion
|
|
113
|
+
// emits no FETCHES for PHP). Conservative — the `->verb(` shape over-matches
|
|
114
|
+
// ordinary method calls, which only costs a parse, never data.
|
|
115
|
+
hasConsumerSignals(content) {
|
|
116
|
+
return /Http::|file_get_contents|->\s*(get|post|put|delete|patch|request)\s*\(/i.test(content);
|
|
117
|
+
},
|
|
107
118
|
scan(tree) {
|
|
108
119
|
const out = [];
|
|
109
120
|
for (const match of runCompiledPatterns(PHP_PATTERNS.laravelRoute, tree)) {
|
|
@@ -825,6 +825,20 @@ function joinPrefix(prefix, route) {
|
|
|
825
825
|
export const PYTHON_HTTP_PLUGIN = {
|
|
826
826
|
name: 'python-http',
|
|
827
827
|
language: Python,
|
|
828
|
+
// routeCoverage intentionally LEFT at the default 'partial' (#2138 Part 2).
|
|
829
|
+
// It would be a no-op even if set to 'complete': FastAPI decorator routes set
|
|
830
|
+
// no handlerName (generic worker path) and Django sets methodName: null, so no
|
|
831
|
+
// Python file ever resolves a handlerSymbolId and none would be parse-skipped.
|
|
832
|
+
// Declaring 'complete' now is only a latent trap for the moment a follow-up
|
|
833
|
+
// gives FastAPI routes a handlerName. `hasConsumerSignals` is kept (and is a
|
|
834
|
+
// true superset of scan()'s consumer shapes) so the precondition already holds
|
|
835
|
+
// when Python is later flipped to 'complete'.
|
|
836
|
+
// Consumer signals scan() can detect: `requests.<verb>`/`requests.request`,
|
|
837
|
+
// `httpx` (sync/async client), the `uri=`/`url=` keyword/variable wrapper
|
|
838
|
+
// calls, plus aiohttp/urllib. Conservative — over-matching only costs a parse.
|
|
839
|
+
hasConsumerSignals(content) {
|
|
840
|
+
return /\brequests\s*\.|\bhttpx\b|\baiohttp\b|\burllib\b|\burlopen\b|\buri\s*=|\burl\s*=/.test(content);
|
|
841
|
+
},
|
|
828
842
|
prepareRepo({ files, parser, readFile, parseSource }) {
|
|
829
843
|
return buildPythonRepoContext(files, parser, readFile, parseSource);
|
|
830
844
|
},
|
|
@@ -71,6 +71,43 @@ export interface HttpLanguagePlugin {
|
|
|
71
71
|
name: string;
|
|
72
72
|
/** tree-sitter grammar object (passed to the shared parser). */
|
|
73
73
|
language: unknown;
|
|
74
|
+
/**
|
|
75
|
+
* Whether ingestion is known to emit a `Route` graph node for EVERY
|
|
76
|
+
* provider route in this language (Spring/FastAPI/Laravel annotations are
|
|
77
|
+
* extracted into Route nodes during parse). When `'complete'`, the
|
|
78
|
+
* orchestrator may skip the source-scan + tree-sitter parse for a file whose
|
|
79
|
+
* graph provider routes all resolved a handler symbol (#2138 Part 2) — the
|
|
80
|
+
* graph is authoritative, the scan would only re-discover the same routes.
|
|
81
|
+
*
|
|
82
|
+
* Defaults to `'partial'` (the safe assumption): the source scan always runs,
|
|
83
|
+
* so a language whose ingestion coverage is incomplete never loses routes.
|
|
84
|
+
* This is a deliberate, per-language trust assertion — set it only for
|
|
85
|
+
* languages whose route ingestion is provably complete.
|
|
86
|
+
*/
|
|
87
|
+
routeCoverage?: 'complete' | 'partial';
|
|
88
|
+
/**
|
|
89
|
+
* Cheap, parse-free pre-check used by the parse-skip optimization (#2138
|
|
90
|
+
* Part 2). Given a file's raw source text, return `false` ONLY when the file
|
|
91
|
+
* provably contains no outbound-HTTP (consumer) call that this plugin's
|
|
92
|
+
* `scan()` would detect; return `true` on any doubt.
|
|
93
|
+
*
|
|
94
|
+
* Why it exists: `routeCoverage: 'complete'` asserts *provider* Route-node
|
|
95
|
+
* completeness only. A provider-covered file may ALSO be a consumer (e.g. a
|
|
96
|
+
* Spring `@RestController` that calls `restTemplate`/`webClient`, a Laravel
|
|
97
|
+
* controller using Guzzle, a FastAPI handler calling `requests`/`httpx`).
|
|
98
|
+
* Ingestion's `FETCHES` edges are JS/TS-only, so the graph cannot back up
|
|
99
|
+
* those server-side consumers — they come solely from the source scan. The
|
|
100
|
+
* orchestrator may therefore skip a provider-covered file's parse only when
|
|
101
|
+
* this returns `false`; otherwise the file is still scanned so its consumer
|
|
102
|
+
* contracts are not dropped.
|
|
103
|
+
*
|
|
104
|
+
* MUST be implemented by any plugin whose `scan()` can emit `'consumer'`
|
|
105
|
+
* detections AND that declares `routeCoverage: 'complete'`; otherwise that
|
|
106
|
+
* language's provider-covered files are never parse-skipped (safe, no win).
|
|
107
|
+
* The check is intentionally conservative — over-matching only costs a parse
|
|
108
|
+
* that could have been skipped; it never drops data.
|
|
109
|
+
*/
|
|
110
|
+
hasConsumerSignals?(content: string): boolean;
|
|
74
111
|
/**
|
|
75
112
|
* Optional pre-pass: walk the relevant files in the repo and produce
|
|
76
113
|
* an opaque context that `scan` can use to resolve cross-file facts.
|
|
@@ -23,7 +23,7 @@ import type { ExtractedContract, RepoHandle } from '../types.js';
|
|
|
23
23
|
* `http-patterns/index.ts`: register a new `HttpLanguagePlugin` and
|
|
24
24
|
* widen `HTTP_SCAN_GLOB` if needed.
|
|
25
25
|
*/
|
|
26
|
-
export declare const HANDLES_ROUTE_QUERY = "\nMATCH (handlerFile:File)-[r:CodeRelation {type: 'HANDLES_ROUTE'}]->(route:Route)\nRETURN handlerFile.id AS fileId, handlerFile.filePath AS filePath,\n route.name AS routePath, route.id AS routeId,\n route.method AS routeMethod,\n route.responseKeys AS responseKeys,\n r.reason AS routeSource";
|
|
26
|
+
export declare const HANDLES_ROUTE_QUERY = "\nMATCH (handlerFile:File)-[r:CodeRelation {type: 'HANDLES_ROUTE'}]->(route:Route)\nRETURN handlerFile.id AS fileId, handlerFile.filePath AS filePath,\n route.name AS routePath, route.id AS routeId,\n route.method AS routeMethod,\n route.handlerSymbolId AS handlerSymbolId,\n route.responseKeys AS responseKeys,\n r.reason AS routeSource";
|
|
27
27
|
/**
|
|
28
28
|
* Canonicalize a provider-side HTTP path for contract-id generation:
|
|
29
29
|
* - strip query string
|
|
@@ -38,6 +38,7 @@ MATCH (handlerFile:File)-[r:CodeRelation {type: 'HANDLES_ROUTE'}]->(route:Route)
|
|
|
38
38
|
RETURN handlerFile.id AS fileId, handlerFile.filePath AS filePath,
|
|
39
39
|
route.name AS routePath, route.id AS routeId,
|
|
40
40
|
route.method AS routeMethod,
|
|
41
|
+
route.handlerSymbolId AS handlerSymbolId,
|
|
41
42
|
route.responseKeys AS responseKeys,
|
|
42
43
|
r.reason AS routeSource`;
|
|
43
44
|
const FETCHES_QUERY = `
|
|
@@ -248,13 +249,44 @@ export class HttpRouteExtractor {
|
|
|
248
249
|
cachedDetections.clear();
|
|
249
250
|
};
|
|
250
251
|
const files = await getScannedFiles();
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
|
|
252
|
+
// Run the graph provider pass FIRST. After #2138 Part 2 it reads handler
|
|
253
|
+
// symbols from the graph (no source parse for resolved routes), so it can
|
|
254
|
+
// report which files are fully graph-covered BEFORE we decide what to
|
|
255
|
+
// parse. Files fully covered by a `routeCoverage: 'complete'` language are
|
|
256
|
+
// candidates to skip the source scan + tree-sitter parse — but only their
|
|
257
|
+
// *providers* are graph-authoritative; the consumer-safety gate below
|
|
258
|
+
// removes any candidate that still needs scanning for outbound calls.
|
|
259
|
+
const coveredFiles = new Set();
|
|
260
|
+
const graphProviders = dbExecutor != null
|
|
261
|
+
? await this.extractProvidersGraph(dbExecutor, getDetections, coveredFiles)
|
|
262
|
+
: [];
|
|
263
|
+
// Consumer-safety gate (#2138 Part 2): `extractProvidersGraph` marks a file
|
|
264
|
+
// covered on *provider* grounds (all HANDLES_ROUTE rows resolved + a
|
|
265
|
+
// `routeCoverage: 'complete'` language). But a provider-covered file may also
|
|
266
|
+
// be a *consumer* (a controller that calls RestTemplate/WebClient/Guzzle/
|
|
267
|
+
// requests/...), and ingestion emits no FETCHES edges for those server-side
|
|
268
|
+
// languages — the graph can't back them up. So a covered file is only truly
|
|
269
|
+
// safe to skip (parse) when its plugin can PROVE, from a cheap parse-free
|
|
270
|
+
// text scan, that it holds no such consumer call. Anything else (a positive
|
|
271
|
+
// signal, no `hasConsumerSignals` hook, or an unreadable file) stays in the
|
|
272
|
+
// scan set so its consumer contracts are preserved.
|
|
273
|
+
for (const f of [...coveredFiles]) {
|
|
274
|
+
const plugin = getPluginForFile(f);
|
|
275
|
+
const content = readSafe(repoPath, f);
|
|
276
|
+
const provenNoConsumer = content != null && typeof plugin?.hasConsumerSignals === 'function'
|
|
277
|
+
? plugin.hasConsumerSignals(content) === false
|
|
278
|
+
: false;
|
|
279
|
+
if (!provenNoConsumer)
|
|
280
|
+
coveredFiles.delete(f);
|
|
281
|
+
}
|
|
282
|
+
// Everything the graph did not fully cover still gets a full source scan
|
|
283
|
+
// (fail-open: partial-coverage languages, unresolved routes, and graph-less
|
|
284
|
+
// runs all land here).
|
|
285
|
+
const scanFiles = files.filter((f) => !coveredFiles.has(f));
|
|
286
|
+
await collectProjectDetections(scanFiles);
|
|
287
|
+
const providers = this.mergeGraphAndSourceContracts(graphProviders, await this.extractProvidersSourceScan(scanFiles, getDetections));
|
|
256
288
|
const graphConsumers = dbExecutor != null ? await this.extractConsumersGraph(dbExecutor, getDetections) : [];
|
|
257
|
-
const consumers = this.mergeGraphAndSourceContracts(graphConsumers, await this.extractConsumersSourceScan(
|
|
289
|
+
const consumers = this.mergeGraphAndSourceContracts(graphConsumers, await this.extractConsumersSourceScan(scanFiles, getDetections));
|
|
258
290
|
return [...providers, ...consumers];
|
|
259
291
|
}
|
|
260
292
|
async scanFiles(repoPath) {
|
|
@@ -272,8 +304,13 @@ export class HttpRouteExtractor {
|
|
|
272
304
|
});
|
|
273
305
|
}
|
|
274
306
|
// ─── Graph-assisted providers ──────────────────────────────────────
|
|
275
|
-
async extractProvidersGraph(db, getDetections) {
|
|
307
|
+
async extractProvidersGraph(db, getDetections, coveredFiles) {
|
|
276
308
|
const out = [];
|
|
309
|
+
// Per-file coverage tracking (#2138 Part 2): a file is "fully graph-covered"
|
|
310
|
+
// when every one of its HANDLES_ROUTE rows resolved a handlerSymbolId AND its
|
|
311
|
+
// language plugin declares `routeCoverage: 'complete'`. Such files can skip
|
|
312
|
+
// the source scan + parse entirely — the graph is authoritative for them.
|
|
313
|
+
const fileAllResolved = new Map();
|
|
277
314
|
let rows;
|
|
278
315
|
try {
|
|
279
316
|
rows = await db(HANDLES_ROUTE_QUERY);
|
|
@@ -298,66 +335,90 @@ export class HttpRouteExtractor {
|
|
|
298
335
|
.trim()
|
|
299
336
|
.toUpperCase();
|
|
300
337
|
let method = (graphMethod || null) ?? methodFromRouteReason(routeSource);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const providerDetections = detections.filter((d) => d.role === 'provider');
|
|
309
|
-
let handlerName = null;
|
|
310
|
-
const normalizedRoute = normalizeHttpPath(routePath);
|
|
311
|
-
// Candidates share the same normalized path. When multiple
|
|
312
|
-
// detections at the same path exist (e.g. GET + POST /api/orders
|
|
313
|
-
// in one router), a blind `.find()` silently returned the first
|
|
314
|
-
// verb — attaching the wrong handler and, when method was not
|
|
315
|
-
// already pinned by the route reason, the wrong method too.
|
|
316
|
-
// Disambiguate by method when we know it; refuse to guess when
|
|
317
|
-
// we don't.
|
|
318
|
-
const candidates = providerDetections.filter((d) => normalizeHttpPath(d.path) === normalizedRoute);
|
|
319
|
-
let match;
|
|
320
|
-
const ambiguousCandidates = !method && candidates.length > 1;
|
|
321
|
-
if (method) {
|
|
322
|
-
match = candidates.find((d) => d.method === method);
|
|
323
|
-
}
|
|
324
|
-
else if (candidates.length === 1) {
|
|
325
|
-
match = candidates[0];
|
|
326
|
-
}
|
|
327
|
-
// else: multiple candidates + unknown method → leave match
|
|
328
|
-
// undefined so handlerName stays null and skip symbol
|
|
329
|
-
// enrichment below, keeping the file-basename fallback instead
|
|
330
|
-
// of letting pickSymbolUid silently pick the first Function /
|
|
331
|
-
// Method in the file (which reintroduces the mis-attribution
|
|
332
|
-
// we were trying to avoid). Method stays at the conservative
|
|
333
|
-
// 'GET' default set below.
|
|
334
|
-
if (match) {
|
|
335
|
-
if (!method)
|
|
336
|
-
method = match.method;
|
|
337
|
-
handlerName = match.name;
|
|
338
|
+
const handlerSymbolId = String(row.handlerSymbolId ?? '').trim();
|
|
339
|
+
const fileId = row.fileId ?? row[0];
|
|
340
|
+
// Track per-file resolution for the parse-skip coverage set: a file stays
|
|
341
|
+
// "all resolved" only while every one of its rows carries a handlerSymbolId.
|
|
342
|
+
if (filePath) {
|
|
343
|
+
const prev = fileAllResolved.get(filePath);
|
|
344
|
+
fileAllResolved.set(filePath, (prev ?? true) && handlerSymbolId.length > 0);
|
|
338
345
|
}
|
|
339
|
-
|
|
340
|
-
method = 'GET';
|
|
341
|
-
const pathNorm = normalizeHttpPath(routePath);
|
|
342
|
-
const cid = contractIdFor(method, pathNorm);
|
|
346
|
+
const pathNormEarly = normalizeHttpPath(routePath);
|
|
343
347
|
let symbolUid = '';
|
|
344
348
|
let symbolName = path.basename(filePath) || 'handler';
|
|
345
349
|
let symPath = filePath;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
350
|
+
if (handlerSymbolId) {
|
|
351
|
+
// Fast path (Part 2, #2138): the handler symbol was resolved during
|
|
352
|
+
// ingestion and persisted on the Route node, so we read it straight
|
|
353
|
+
// from the graph and SKIP the `getDetections()` source-scan/parse the
|
|
354
|
+
// legacy path needed just to recover the handler name. CONTAINS is a
|
|
355
|
+
// cheap graph query (no tree-sitter parse) used only to surface the
|
|
356
|
+
// handler's display name/path; the uid is authoritative regardless.
|
|
357
|
+
if (!method)
|
|
358
|
+
method = 'GET';
|
|
359
|
+
symbolUid = handlerSymbolId;
|
|
360
|
+
if (fileId) {
|
|
361
|
+
try {
|
|
362
|
+
const syms = await db(CONTAINS_QUERY, { fileId });
|
|
363
|
+
const hit = syms.find((s) => String(s.uid ?? s[0]) === handlerSymbolId);
|
|
364
|
+
if (hit) {
|
|
365
|
+
symbolName = String(hit.name ?? hit[1]) || symbolName;
|
|
366
|
+
symPath = String(hit.filePath ?? hit[2]) || filePath;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
/* keep the authoritative uid + basename fallback */
|
|
355
371
|
}
|
|
356
372
|
}
|
|
357
|
-
|
|
358
|
-
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
// Legacy fallback (old index / unresolved handler): recover the handler
|
|
376
|
+
// name from the plugin's scan of the handler file (this parses source).
|
|
377
|
+
// Always run the lookup: even when method is set, we still need the name.
|
|
378
|
+
const detections = filePath ? await getDetections(filePath) : [];
|
|
379
|
+
const providerDetections = detections.filter((d) => d.role === 'provider');
|
|
380
|
+
let handlerName = null;
|
|
381
|
+
// Candidates share the same normalized path. When multiple detections at
|
|
382
|
+
// the same path exist (GET + POST /api/orders in one router), a blind
|
|
383
|
+
// `.find()` silently returned the first verb — attaching the wrong
|
|
384
|
+
// handler/method. Disambiguate by method when known; refuse to guess.
|
|
385
|
+
const candidates = providerDetections.filter((d) => normalizeHttpPath(d.path) === pathNormEarly);
|
|
386
|
+
let match;
|
|
387
|
+
const ambiguousCandidates = !method && candidates.length > 1;
|
|
388
|
+
if (method) {
|
|
389
|
+
match = candidates.find((d) => d.method === method);
|
|
390
|
+
}
|
|
391
|
+
else if (candidates.length === 1) {
|
|
392
|
+
match = candidates[0];
|
|
393
|
+
}
|
|
394
|
+
// else: multiple candidates + unknown method → leave match undefined so
|
|
395
|
+
// handlerName stays null and we skip symbol enrichment, keeping the
|
|
396
|
+
// file-basename fallback rather than letting pickSymbolUid pick the
|
|
397
|
+
// first Function/Method (which reintroduces mis-attribution).
|
|
398
|
+
if (match) {
|
|
399
|
+
if (!method)
|
|
400
|
+
method = match.method;
|
|
401
|
+
handlerName = match.name;
|
|
402
|
+
}
|
|
403
|
+
if (!method)
|
|
404
|
+
method = 'GET';
|
|
405
|
+
if (fileId && !ambiguousCandidates) {
|
|
406
|
+
try {
|
|
407
|
+
const syms = await db(CONTAINS_QUERY, { fileId });
|
|
408
|
+
if (syms.length > 0) {
|
|
409
|
+
const picked = pickSymbolUid(syms, handlerName);
|
|
410
|
+
symbolUid = picked.uid;
|
|
411
|
+
symbolName = picked.name;
|
|
412
|
+
symPath = picked.filePath || filePath;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
/* ignore */
|
|
417
|
+
}
|
|
359
418
|
}
|
|
360
419
|
}
|
|
420
|
+
const pathNorm = pathNormEarly;
|
|
421
|
+
const cid = contractIdFor(method, pathNorm);
|
|
361
422
|
out.push({
|
|
362
423
|
contractId: cid,
|
|
363
424
|
type: 'http',
|
|
@@ -375,6 +436,17 @@ export class HttpRouteExtractor {
|
|
|
375
436
|
},
|
|
376
437
|
});
|
|
377
438
|
}
|
|
439
|
+
// Populate the parse-skip coverage set: files whose every provider route
|
|
440
|
+
// resolved a handler symbol AND whose language declares complete ingestion
|
|
441
|
+
// route coverage. Fail-open — any unresolved row or a 'partial' language
|
|
442
|
+
// leaves the file out, so it still gets a full source scan.
|
|
443
|
+
if (coveredFiles) {
|
|
444
|
+
for (const [fp, allResolved] of fileAllResolved) {
|
|
445
|
+
if (allResolved && getPluginForFile(fp)?.routeCoverage === 'complete') {
|
|
446
|
+
coveredFiles.add(fp);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
378
450
|
return out;
|
|
379
451
|
}
|
|
380
452
|
// ─── Source-scan providers ─────────────────────────────────────────
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { KnowledgeGraph } from '../graph/types.js';
|
|
18
18
|
import type { SemanticModel, SymbolTableReader } from './model/index.js';
|
|
19
19
|
import type { ExtractedRoute, ExtractedFetchCall } from './workers/parse-worker.js';
|
|
20
|
+
import type { ExtractedDecoratorRoute } from './workers/parse-worker.js';
|
|
20
21
|
/** Per-file resolved type bindings for exported symbols.
|
|
21
22
|
* Consumed by the cross-file re-resolution / enrichment pass. */
|
|
22
23
|
export type ExportedTypeMap = Map<string, Map<string, string>>;
|
|
@@ -57,6 +58,31 @@ export declare function buildExportedTypeMapFromGraph(graph: KnowledgeGraph, sym
|
|
|
57
58
|
* passes — acceptable for an edge whose target method could not be resolved.
|
|
58
59
|
*/
|
|
59
60
|
export declare const processRoutesFromExtracted: (graph: KnowledgeGraph, extractedRoutes: ExtractedRoute[], model: SemanticModel, onProgress?: (current: number, total: number) => void) => Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Resolve each route's handler to a real symbol UID, keyed by the normalized
|
|
63
|
+
* route URL (the same key the routes phase uses for the `Route` node). This is
|
|
64
|
+
* the Part 2 (#2138) groundwork that lets `HttpRouteExtractor.extractProvidersGraph`
|
|
65
|
+
* read the handler symbol from the graph instead of re-parsing source via
|
|
66
|
+
* `getDetections()`.
|
|
67
|
+
*
|
|
68
|
+
* Two route shapes, one resolution target — `(filePath, name) → nodeId`:
|
|
69
|
+
* - Laravel framework routes (`ExtractedRoute`) carry `controllerName` +
|
|
70
|
+
* `methodName`; resolve the controller (qualified-first) then the method in
|
|
71
|
+
* the controller's own file (mirrors `processRoutesFromExtracted`).
|
|
72
|
+
* - Decorator routes (`ExtractedDecoratorRoute`, e.g. Spring/FastAPI) carry
|
|
73
|
+
* `handlerName` (the decorated method, captured at extraction); resolve it
|
|
74
|
+
* directly in the route's own file.
|
|
75
|
+
*
|
|
76
|
+
* First-writer-wins per URL, matching the routes phase's dedup (it keeps the
|
|
77
|
+
* first route registered for a URL and counts the rest as duplicates). The first
|
|
78
|
+
* route to claim a URL reserves it **even when its handler is unresolvable**, so
|
|
79
|
+
* a later same-URL route can never stamp its handler onto the first route's Route
|
|
80
|
+
* node (the routes phase made that first route the node-winner). Routes whose
|
|
81
|
+
* handler cannot be *uniquely* resolved (no name, zero matches, or an ambiguous
|
|
82
|
+
* same-name match) carry no `handlerSymbolId`; the extractor then falls back to
|
|
83
|
+
* source scan for that route (fail-open, no regression, never a wrong handler).
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveRouteHandlerSymbols(model: SemanticModel, extractedRoutes: readonly ExtractedRoute[], decoratorRoutes: readonly ExtractedDecoratorRoute[]): Map<string, string>;
|
|
60
86
|
/**
|
|
61
87
|
* Extract property access keys from a consumer file's source code near fetch calls.
|
|
62
88
|
*
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { generateId } from '../../lib/utils.js';
|
|
18
18
|
import { yieldToEventLoop } from './utils/event-loop.js';
|
|
19
19
|
import { normalizeFetchURL, routeMatches } from './route-extractors/nextjs.js';
|
|
20
|
+
import { normalizeExtractedRoutePath } from './route-extractors/route-path.js';
|
|
20
21
|
import { extractReturnTypeName } from './type-extractors/shared.js';
|
|
21
22
|
const MAX_EXPORTS_PER_FILE = 500;
|
|
22
23
|
const MAX_TYPE_NAME_LENGTH = 256;
|
|
@@ -213,6 +214,78 @@ export const processRoutesFromExtracted = async (graph, extractedRoutes, model,
|
|
|
213
214
|
}
|
|
214
215
|
onProgress?.(extractedRoutes.length, extractedRoutes.length);
|
|
215
216
|
};
|
|
217
|
+
/**
|
|
218
|
+
* Resolve each route's handler to a real symbol UID, keyed by the normalized
|
|
219
|
+
* route URL (the same key the routes phase uses for the `Route` node). This is
|
|
220
|
+
* the Part 2 (#2138) groundwork that lets `HttpRouteExtractor.extractProvidersGraph`
|
|
221
|
+
* read the handler symbol from the graph instead of re-parsing source via
|
|
222
|
+
* `getDetections()`.
|
|
223
|
+
*
|
|
224
|
+
* Two route shapes, one resolution target — `(filePath, name) → nodeId`:
|
|
225
|
+
* - Laravel framework routes (`ExtractedRoute`) carry `controllerName` +
|
|
226
|
+
* `methodName`; resolve the controller (qualified-first) then the method in
|
|
227
|
+
* the controller's own file (mirrors `processRoutesFromExtracted`).
|
|
228
|
+
* - Decorator routes (`ExtractedDecoratorRoute`, e.g. Spring/FastAPI) carry
|
|
229
|
+
* `handlerName` (the decorated method, captured at extraction); resolve it
|
|
230
|
+
* directly in the route's own file.
|
|
231
|
+
*
|
|
232
|
+
* First-writer-wins per URL, matching the routes phase's dedup (it keeps the
|
|
233
|
+
* first route registered for a URL and counts the rest as duplicates). The first
|
|
234
|
+
* route to claim a URL reserves it **even when its handler is unresolvable**, so
|
|
235
|
+
* a later same-URL route can never stamp its handler onto the first route's Route
|
|
236
|
+
* node (the routes phase made that first route the node-winner). Routes whose
|
|
237
|
+
* handler cannot be *uniquely* resolved (no name, zero matches, or an ambiguous
|
|
238
|
+
* same-name match) carry no `handlerSymbolId`; the extractor then falls back to
|
|
239
|
+
* source scan for that route (fail-open, no regression, never a wrong handler).
|
|
240
|
+
*/
|
|
241
|
+
export function resolveRouteHandlerSymbols(model, extractedRoutes, decoratorRoutes) {
|
|
242
|
+
const out = new Map();
|
|
243
|
+
// URLs already claimed by an earlier route (resolved or not). Mirrors the
|
|
244
|
+
// routes phase `addRoute` first-writer-wins so the handler we stamp always
|
|
245
|
+
// belongs to the route that actually won the Route node.
|
|
246
|
+
const claimed = new Set();
|
|
247
|
+
// Resolve a single same-file symbol by name, refusing to guess on ambiguity:
|
|
248
|
+
// exactly one match → its nodeId; zero or many → undefined (fail-open).
|
|
249
|
+
const uniqueSymbolId = (filePath, name) => {
|
|
250
|
+
const defs = model.symbols.lookupExactAll(filePath, name);
|
|
251
|
+
return defs.length === 1 ? defs[0]?.nodeId : undefined;
|
|
252
|
+
};
|
|
253
|
+
const claim = (routePath, prefix, symbolId) => {
|
|
254
|
+
if (!routePath)
|
|
255
|
+
return;
|
|
256
|
+
const url = normalizeExtractedRoutePath(routePath, prefix);
|
|
257
|
+
if (claimed.has(url))
|
|
258
|
+
return; // first-writer-wins: later same-URL routes can't override
|
|
259
|
+
claimed.add(url);
|
|
260
|
+
if (symbolId)
|
|
261
|
+
out.set(url, symbolId);
|
|
262
|
+
};
|
|
263
|
+
// Laravel framework routes — controller class + method name.
|
|
264
|
+
for (const route of extractedRoutes) {
|
|
265
|
+
let methodId;
|
|
266
|
+
if (route.controllerName && route.methodName) {
|
|
267
|
+
let controllerDef;
|
|
268
|
+
if (route.controllerQualifiedName) {
|
|
269
|
+
controllerDef = resolveControllerByQualifiedName(model, route.controllerQualifiedName);
|
|
270
|
+
}
|
|
271
|
+
if (!controllerDef) {
|
|
272
|
+
const controllerDefs = model.types.lookupClassByName(route.controllerName);
|
|
273
|
+
if (controllerDefs.length === 1)
|
|
274
|
+
controllerDef = controllerDefs[0];
|
|
275
|
+
}
|
|
276
|
+
if (controllerDef)
|
|
277
|
+
methodId = uniqueSymbolId(controllerDef.filePath, route.methodName);
|
|
278
|
+
}
|
|
279
|
+
claim(route.routePath, route.prefix ?? null, methodId);
|
|
280
|
+
}
|
|
281
|
+
// Decorator routes (Spring / FastAPI / generic) — the decorated handler in
|
|
282
|
+
// the route's own file.
|
|
283
|
+
for (const dr of decoratorRoutes) {
|
|
284
|
+
const handlerId = dr.handlerName ? uniqueSymbolId(dr.filePath, dr.handlerName) : undefined;
|
|
285
|
+
claim(dr.routePath, dr.prefix ?? null, handlerId);
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
216
289
|
/** Common method names on response/data objects that are NOT property accesses */
|
|
217
290
|
// Properties/methods to ignore when extracting consumer accessed keys from `data.X` patterns.
|
|
218
291
|
// Avoids false positives from Fetch API, Array, Object, Promise, and DOM access on variables
|
|
@@ -66,6 +66,10 @@ export declare function runChunkedParseAndResolve(graph: KnowledgeGraph, scanned
|
|
|
66
66
|
allToolDefs: ExtractedToolDef[];
|
|
67
67
|
allORMQueries: ExtractedORMQuery[];
|
|
68
68
|
bindingAccumulator: BindingAccumulator;
|
|
69
|
+
/** Route URL → resolved handler symbol UID (Part 2, #2138). Lets the routes
|
|
70
|
+
* phase stamp `handlerSymbolId` on Route nodes so contract extraction can
|
|
71
|
+
* read the handler from the graph instead of re-parsing source. */
|
|
72
|
+
routeHandlerSymbols: ReadonlyMap<string, string>;
|
|
69
73
|
/** SemanticModel populated during parse — scope-resolution reads its
|
|
70
74
|
* TypeRegistry / MethodRegistry / SymbolTable indexes. */
|
|
71
75
|
model: MutableSemanticModel;
|
|
@@ -19,7 +19,7 @@ import { mergeChunkResults, dispatchChunkParse } from '../parsing-processor.js';
|
|
|
19
19
|
import { fileContentHash, computeChunkHash, loadParseCacheChunk, persistParseCacheChunk, PARSE_CACHE_VERSION, } from '../../../storage/parse-cache.js';
|
|
20
20
|
import { clearParsedFileStore, persistParsedFileChunk, getDurableParsedFileDir, loadDurableParsedFileIndex, restoreDurableParsedFileShard, } from '../../../storage/parsedfile-store.js';
|
|
21
21
|
import { DEFAULT_PDG_MAX_FUNCTION_LINES } from '../cfg/collect.js';
|
|
22
|
-
import { processRoutesFromExtracted, buildExportedTypeMapFromGraph, } from '../call-processor.js';
|
|
22
|
+
import { processRoutesFromExtracted, resolveRouteHandlerSymbols, buildExportedTypeMapFromGraph, } from '../call-processor.js';
|
|
23
23
|
import { createSemanticModel } from '../model/index.js';
|
|
24
24
|
import { getLanguageFromFilename, } from '../../../_shared/index.js';
|
|
25
25
|
import { readFileContents } from '../filesystem-walker.js';
|
|
@@ -1048,6 +1048,9 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
1048
1048
|
}
|
|
1049
1049
|
}
|
|
1050
1050
|
logHeapProbe('parse-impl-return', `exportedTypeMap=${exportedTypeMap.size} parsedFiles=${allParsedFiles.length} nodes=${graph.nodeCount}`);
|
|
1051
|
+
// Part 2 (#2138): resolve each route's handler to a real symbol UID now that
|
|
1052
|
+
// the model is fully populated and decorator-route prefixes are finalized.
|
|
1053
|
+
const routeHandlerSymbols = resolveRouteHandlerSymbols(model, allExtractedRoutes, allDecoratorRoutes);
|
|
1051
1054
|
return {
|
|
1052
1055
|
exportedTypeMap,
|
|
1053
1056
|
allFetchCalls,
|
|
@@ -1057,6 +1060,7 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
1057
1060
|
allToolDefs,
|
|
1058
1061
|
allORMQueries,
|
|
1059
1062
|
bindingAccumulator,
|
|
1063
|
+
routeHandlerSymbols,
|
|
1060
1064
|
model,
|
|
1061
1065
|
// Whether a worker pool was actually constructed for this run. False means
|
|
1062
1066
|
// no pool was needed: a warm all-cache-hit run replays cached worker output
|
|
@@ -39,6 +39,9 @@ export interface ParseOutput {
|
|
|
39
39
|
readonly allDecoratorRoutes: readonly ExtractedDecoratorRoute[];
|
|
40
40
|
readonly allToolDefs: readonly ExtractedToolDef[];
|
|
41
41
|
readonly allORMQueries: readonly ExtractedORMQuery[];
|
|
42
|
+
/** Route URL → resolved handler symbol UID (Part 2, #2138). Consumed by the
|
|
43
|
+
* routes phase to stamp `handlerSymbolId` on Route nodes. */
|
|
44
|
+
readonly routeHandlerSymbols: ReadonlyMap<string, string>;
|
|
42
45
|
bindingAccumulator: BindingAccumulator;
|
|
43
46
|
/** SemanticModel populated during parse — scope-resolution reads its
|
|
44
47
|
* TypeRegistry / MethodRegistry / SymbolTable indexes. */
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* @output routeRegistry, handlerContents
|
|
12
12
|
*/
|
|
13
13
|
import type { PipelinePhase } from './types.js';
|
|
14
|
+
import { normalizeExtractedRoutePath } from '../route-extractors/route-path.js';
|
|
14
15
|
export interface RouteEntry {
|
|
15
16
|
filePath: string;
|
|
16
17
|
source: string;
|
|
@@ -34,6 +35,6 @@ export interface TemplateFetchCall {
|
|
|
34
35
|
}
|
|
35
36
|
export declare const isTemplateRouteCandidate: (filePath: string) => boolean;
|
|
36
37
|
export declare function extractTemplateStaticFetchCalls(filePath: string, content: string, namedRouteUrls?: ReadonlyMap<string, string>): TemplateFetchCall[];
|
|
37
|
-
export
|
|
38
|
+
export { normalizeExtractedRoutePath };
|
|
38
39
|
export declare function normalizeRouteMethod(raw: string | null | undefined): string | undefined;
|
|
39
40
|
export declare const routesPhase: PipelinePhase<RoutesOutput>;
|
|
@@ -18,6 +18,7 @@ import { phpFileToRouteURL } from '../route-extractors/php.js';
|
|
|
18
18
|
import { extractResponseShapes, extractPHPResponseShapes, } from '../route-extractors/response-shapes.js';
|
|
19
19
|
import { extractMiddlewareChain, extractNextjsMiddlewareConfig, compileMatcher, compiledMatcherMatchesRoute, } from '../route-extractors/middleware.js';
|
|
20
20
|
import { processNextjsFetchRoutes } from '../call-processor.js';
|
|
21
|
+
import { normalizeExtractedRoutePath } from '../route-extractors/route-path.js';
|
|
21
22
|
import { generateId } from '../../../lib/utils.js';
|
|
22
23
|
import { readFileContents } from '../filesystem-walker.js';
|
|
23
24
|
import { isDev } from '../utils/env.js';
|
|
@@ -88,15 +89,11 @@ export function extractTemplateStaticFetchCalls(filePath, content, namedRouteUrl
|
|
|
88
89
|
}
|
|
89
90
|
return calls;
|
|
90
91
|
}
|
|
91
|
-
export function normalizeExtractedRoutePath(routePath, prefix) {
|
|
92
|
-
const pathPart = routePath.trim().replace(/^\/+/, '').replace(/\/+$/g, '');
|
|
93
|
-
const prefixPart = prefix?.trim().replace(/^\/+/, '').replace(/\/+$/g, '');
|
|
94
|
-
const joined = prefixPart ? `/${prefixPart}${pathPart ? `/${pathPart}` : ''}` : `/${pathPart}`;
|
|
95
|
-
return joined.replace(/\/+/g, '/') || '/';
|
|
96
|
-
}
|
|
97
92
|
function escapeRegex(s) {
|
|
98
93
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
99
94
|
}
|
|
95
|
+
// Re-exported for existing consumers/tests that import it from the routes phase.
|
|
96
|
+
export { normalizeExtractedRoutePath };
|
|
100
97
|
/**
|
|
101
98
|
* Canonicalize a route's HTTP verb for persistence on the Route node.
|
|
102
99
|
* Returns an upper-cased standard method, or `undefined` when the value
|
|
@@ -132,7 +129,7 @@ export const routesPhase = {
|
|
|
132
129
|
name: 'routes',
|
|
133
130
|
deps: ['parse'],
|
|
134
131
|
async execute(ctx, deps) {
|
|
135
|
-
const { allPaths, allFetchCalls: parseFetchCalls, allFetchWrapperDefs, allExtractedRoutes, allDecoratorRoutes, } = getPhaseOutput(deps, 'parse');
|
|
132
|
+
const { allPaths, allFetchCalls: parseFetchCalls, allFetchWrapperDefs, allExtractedRoutes, allDecoratorRoutes, routeHandlerSymbols, } = getPhaseOutput(deps, 'parse');
|
|
136
133
|
// Local copy — routes phase must not mutate upstream ParseOutput
|
|
137
134
|
const allFetchCalls = [...parseFetchCalls];
|
|
138
135
|
const routeRegistry = new Map();
|
|
@@ -226,6 +223,7 @@ export const routesPhase = {
|
|
|
226
223
|
const mwResult = content ? extractMiddlewareChain(content) : undefined;
|
|
227
224
|
const middleware = mwResult?.chain;
|
|
228
225
|
const routeNodeId = generateId('Route', routeURL);
|
|
226
|
+
const handlerSymbolId = routeHandlerSymbols.get(routeURL);
|
|
229
227
|
ctx.graph.addNode({
|
|
230
228
|
id: routeNodeId,
|
|
231
229
|
label: 'Route',
|
|
@@ -233,6 +231,7 @@ export const routesPhase = {
|
|
|
233
231
|
name: routeURL,
|
|
234
232
|
filePath: handlerPath,
|
|
235
233
|
...(routeMethod ? { method: routeMethod } : {}),
|
|
234
|
+
...(handlerSymbolId ? { handlerSymbolId } : {}),
|
|
236
235
|
...(responseKeys ? { responseKeys } : {}),
|
|
237
236
|
...(errorKeys ? { errorKeys } : {}),
|
|
238
237
|
...(middleware && middleware.length > 0 ? { middleware } : {}),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared route-path normalization.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the routes phase so both the routes phase (which creates the
|
|
5
|
+
* `Route` graph node, keyed by the normalized URL) and the parse phase (which
|
|
6
|
+
* resolves each route's handler symbol and needs the SAME key to associate the
|
|
7
|
+
* resolved id back to the route) can compute an identical route URL without a
|
|
8
|
+
* phase-to-phase import cycle. Pure string logic, no dependencies.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Join a route's path with its (optional) prefix into a normalized,
|
|
12
|
+
* leading-slash URL used as the Route node identity. Collapses duplicate
|
|
13
|
+
* slashes and strips trailing ones; an empty result degrades to `/`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function normalizeExtractedRoutePath(routePath: string, prefix: string | null): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared route-path normalization.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the routes phase so both the routes phase (which creates the
|
|
5
|
+
* `Route` graph node, keyed by the normalized URL) and the parse phase (which
|
|
6
|
+
* resolves each route's handler symbol and needs the SAME key to associate the
|
|
7
|
+
* resolved id back to the route) can compute an identical route URL without a
|
|
8
|
+
* phase-to-phase import cycle. Pure string logic, no dependencies.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Join a route's path with its (optional) prefix into a normalized,
|
|
12
|
+
* leading-slash URL used as the Route node identity. Collapses duplicate
|
|
13
|
+
* slashes and strips trailing ones; an empty result degrades to `/`.
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeExtractedRoutePath(routePath, prefix) {
|
|
16
|
+
const pathPart = routePath.trim().replace(/^\/+/, '').replace(/\/+$/g, '');
|
|
17
|
+
const prefixPart = prefix?.trim().replace(/^\/+/, '').replace(/\/+$/g, '');
|
|
18
|
+
const joined = prefixPart ? `/${prefixPart}${pathPart ? `/${pathPart}` : ''}` : `/${pathPart}`;
|
|
19
|
+
return joined.replace(/\/+/g, '/') || '/';
|
|
20
|
+
}
|
|
@@ -123,6 +123,9 @@ export function extractSpringRoutes(tree, filePath, lineOffset = 0) {
|
|
|
123
123
|
continue;
|
|
124
124
|
const enclosingClass = findEnclosingClass(node);
|
|
125
125
|
const classPrefix = enclosingClass ? (prefixByClassId.get(enclosingClass.id) ?? '') : '';
|
|
126
|
+
// `node` is the annotated `method_declaration`; its name field is the
|
|
127
|
+
// handler method name (resolved to a symbol UID later by the routes phase).
|
|
128
|
+
const handlerName = node.childForFieldName('name')?.text;
|
|
126
129
|
routes.push({
|
|
127
130
|
filePath,
|
|
128
131
|
routePath,
|
|
@@ -130,6 +133,7 @@ export function extractSpringRoutes(tree, filePath, lineOffset = 0) {
|
|
|
130
133
|
decoratorName: ann,
|
|
131
134
|
lineNumber: annNode.startPosition.row + lineOffset,
|
|
132
135
|
...(classPrefix ? { prefix: classPrefix } : {}),
|
|
136
|
+
...(handlerName ? { handlerName } : {}),
|
|
133
137
|
});
|
|
134
138
|
}
|
|
135
139
|
return routes;
|
|
@@ -119,6 +119,16 @@ export interface ExtractedDecoratorRoute {
|
|
|
119
119
|
* absent ⇒ no prefix applies.
|
|
120
120
|
*/
|
|
121
121
|
prefix?: string | null;
|
|
122
|
+
/**
|
|
123
|
+
* Name of the handler the route decorator sits on (the decorated
|
|
124
|
+
* method/function — e.g. `create` for `@PostMapping("/orders") Order create()`).
|
|
125
|
+
* Captured at extraction where the decorated definition node is in hand, so
|
|
126
|
+
* the routes phase can resolve it to a real handler symbol UID via the
|
|
127
|
+
* SemanticModel (same `(filePath, name) → nodeId` lookup Laravel routes use).
|
|
128
|
+
* Absent when the extractor could not identify the decorated definition;
|
|
129
|
+
* resolution then falls back (the Route node simply carries no handlerSymbolId).
|
|
130
|
+
*/
|
|
131
|
+
handlerName?: string;
|
|
122
132
|
}
|
|
123
133
|
export interface ExtractedToolDef {
|
|
124
134
|
filePath: string;
|
|
@@ -304,7 +304,7 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir, onNodePhaseCo
|
|
|
304
304
|
// Section nodes have an extra 'level' column
|
|
305
305
|
const sectionWriter = new BufferedCSVWriter(path.join(csvDir, 'section.csv'), 'id,name,filePath,startLine,endLine,level,content,description');
|
|
306
306
|
// Route nodes for API endpoint mapping
|
|
307
|
-
const routeWriter = new BufferedCSVWriter(path.join(csvDir, 'route.csv'), 'id,name,filePath,responseKeys,errorKeys,middleware,method');
|
|
307
|
+
const routeWriter = new BufferedCSVWriter(path.join(csvDir, 'route.csv'), 'id,name,filePath,responseKeys,errorKeys,middleware,method,handlerSymbolId');
|
|
308
308
|
// Tool nodes for MCP tool definitions
|
|
309
309
|
const toolWriter = new BufferedCSVWriter(path.join(csvDir, 'tool.csv'), 'id,name,filePath,description');
|
|
310
310
|
// BasicBlock nodes — taint/PDG substrate (issue #2080). No `name` column;
|
|
@@ -452,6 +452,7 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir, onNodePhaseCo
|
|
|
452
452
|
escapeCSVField(errorKeysStr),
|
|
453
453
|
escapeCSVField(middlewareStr),
|
|
454
454
|
escapeCSVField(String(node.properties.method ?? '')),
|
|
455
|
+
escapeCSVField(String(node.properties.handlerSymbolId ?? '')),
|
|
455
456
|
].join(','));
|
|
456
457
|
break;
|
|
457
458
|
}
|
|
@@ -1149,7 +1149,7 @@ export const getCopyQuery = (table, filePath) => {
|
|
|
1149
1149
|
return `COPY ${t}(id, name, filePath, startLine, endLine, level, content, description) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
|
1150
1150
|
}
|
|
1151
1151
|
if (table === 'Route') {
|
|
1152
|
-
return `COPY ${t}(id, name, filePath, responseKeys, errorKeys, middleware, method) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
|
1152
|
+
return `COPY ${t}(id, name, filePath, responseKeys, errorKeys, middleware, method, handlerSymbolId) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
|
1153
1153
|
}
|
|
1154
1154
|
if (table === 'Tool') {
|
|
1155
1155
|
return `COPY ${t}(id, name, filePath, description) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
|
@@ -39,7 +39,7 @@ export declare const ANNOTATION_SCHEMA: string;
|
|
|
39
39
|
export declare const CONSTRUCTOR_SCHEMA: string;
|
|
40
40
|
export declare const TEMPLATE_SCHEMA: string;
|
|
41
41
|
export declare const MODULE_SCHEMA: string;
|
|
42
|
-
export declare const ROUTE_SCHEMA = "\nCREATE NODE TABLE Route (\n id STRING,\n name STRING,\n filePath STRING,\n responseKeys STRING[],\n errorKeys STRING[],\n middleware STRING[],\n method STRING,\n PRIMARY KEY (id)\n)";
|
|
42
|
+
export declare const ROUTE_SCHEMA = "\nCREATE NODE TABLE Route (\n id STRING,\n name STRING,\n filePath STRING,\n responseKeys STRING[],\n errorKeys STRING[],\n middleware STRING[],\n method STRING,\n handlerSymbolId STRING,\n PRIMARY KEY (id)\n)";
|
|
43
43
|
export declare const TOOL_SCHEMA = "\nCREATE NODE TABLE Tool (\n id STRING,\n name STRING,\n filePath STRING,\n description STRING,\n PRIMARY KEY (id)\n)";
|
|
44
44
|
export declare const SECTION_SCHEMA = "\nCREATE NODE TABLE Section (\n id STRING,\n name STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n level INT64,\n content STRING,\n description STRING,\n PRIMARY KEY (id)\n)";
|
|
45
45
|
export declare const BASICBLOCK_SCHEMA = "\nCREATE NODE TABLE BasicBlock (\n id STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n text STRING,\n callees STRING,\n calleeIds STRING,\n PRIMARY KEY (id)\n)";
|
package/dist/core/lbug/schema.js
CHANGED
|
@@ -52,7 +52,7 @@ import { fileURLToPath } from 'url';
|
|
|
52
52
|
// the main thread (the #1983 OOM). Because the two stores share this version,
|
|
53
53
|
// any future change to the `ParsedFile` serialization shape MUST bump
|
|
54
54
|
// SCHEMA_BUMP so both invalidate in lockstep.
|
|
55
|
-
const SCHEMA_BUMP =
|
|
55
|
+
const SCHEMA_BUMP = 7; // #2138 Part 2: ExtractedDecoratorRoute gained `handlerName` (route handler symbol resolution)
|
|
56
56
|
const GITNEXUS_PKG_VERSION = (() => {
|
|
57
57
|
try {
|
|
58
58
|
// package.json sits at gitnexus/package.json — two levels up from
|
package/package.json
CHANGED