gitnexus 1.6.11-rc.30 → 1.6.11-rc.31
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/python.js +11 -7
- package/dist/core/group/extractors/http-route-extractor.js +7 -12
- package/dist/core/ingestion/language-provider.d.ts +21 -0
- package/dist/core/ingestion/languages/python.js +2 -0
- package/dist/core/ingestion/pipeline-phases/routes.js +27 -18
- package/dist/core/ingestion/route-extractors/python-decorator-handler.d.ts +6 -0
- package/dist/core/ingestion/route-extractors/python-decorator-handler.js +16 -0
- package/dist/core/ingestion/workers/parse-worker.js +2 -0
- package/dist/core/lbug/schema.js +17 -7
- package/dist/storage/parse-cache.js +12 -1
- package/package.json +1 -1
|
@@ -1028,13 +1028,17 @@ export const PYTHON_HTTP_PLUGIN = {
|
|
|
1028
1028
|
name: 'python-http',
|
|
1029
1029
|
language: Python,
|
|
1030
1030
|
// routeCoverage intentionally LEFT at the default 'partial' (#2138 Part 2).
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
//
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1031
|
+
// 'complete' is now an active data-loss risk rather than a no-op: FastAPI and
|
|
1032
|
+
// Flask decorator routes do carry a handlerName (Python's
|
|
1033
|
+
// `decoratorRouteHandlerName` hook reads the `decorated_definition`), so their
|
|
1034
|
+
// files can resolve every handlerSymbolId and become parse-skip candidates.
|
|
1035
|
+
// The flag asserts more than that — it asserts ingestion emits a Route node
|
|
1036
|
+
// for EVERY provider route this scan() finds, and it does not: Flask's
|
|
1037
|
+
// imperative `add_url_rule('/p', view_func=handler)` registration below has no
|
|
1038
|
+
// ingestion counterpart, so skipping a file that mixes it with resolved
|
|
1039
|
+
// decorator routes would drop those providers. `hasConsumerSignals` is kept
|
|
1040
|
+
// (and is a true superset of scan()'s consumer shapes) so the consumer half of
|
|
1041
|
+
// the precondition already holds once provider parity is closed.
|
|
1038
1042
|
// Consumer signals scan() can detect: `requests.<verb>`/`requests.request`,
|
|
1039
1043
|
// `httpx` (sync/async client), the `uri=`/`url=` keyword/variable wrapper
|
|
1040
1044
|
// calls, plus aiohttp/urllib. Conservative — over-matching only costs a parse.
|
|
@@ -571,7 +571,7 @@ export class HttpRouteExtractor {
|
|
|
571
571
|
// removes any candidate that still needs scanning for outbound calls.
|
|
572
572
|
const coveredFiles = new Set();
|
|
573
573
|
const graphProviders = dbExecutor != null
|
|
574
|
-
? await this.extractProvidersGraph(dbExecutor, getDetections, resolveDetectionSymbol, coveredFiles)
|
|
574
|
+
? await this.extractProvidersGraph(dbExecutor, getDetections, resolveDetectionSymbol, loadFileSymbols, coveredFiles)
|
|
575
575
|
: [];
|
|
576
576
|
// Consumer-safety gate (#2138 Part 2): `extractProvidersGraph` marks a file
|
|
577
577
|
// covered on *provider* grounds (all HANDLES_ROUTE rows resolved + a
|
|
@@ -619,7 +619,7 @@ export class HttpRouteExtractor {
|
|
|
619
619
|
});
|
|
620
620
|
}
|
|
621
621
|
// ─── Graph-assisted providers ──────────────────────────────────────
|
|
622
|
-
async extractProvidersGraph(db, getDetections, resolveSymbol, coveredFiles) {
|
|
622
|
+
async extractProvidersGraph(db, getDetections, resolveSymbol, loadFileSymbols, coveredFiles) {
|
|
623
623
|
const out = [];
|
|
624
624
|
// Per-file coverage tracking (#2138 Part 2): a file is "fully graph-covered"
|
|
625
625
|
// when every one of its HANDLES_ROUTE rows resolved a handlerSymbolId AND its
|
|
@@ -672,16 +672,11 @@ export class HttpRouteExtractor {
|
|
|
672
672
|
method = 'GET';
|
|
673
673
|
symbolUid = handlerSymbolId;
|
|
674
674
|
if (filePath) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
symPath = String(hit.filePath ?? hit[2]) || filePath;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
catch {
|
|
684
|
-
/* keep the authoritative uid + basename fallback */
|
|
675
|
+
const syms = await loadFileSymbols(filePath);
|
|
676
|
+
const hit = syms.find((s) => String(s.uid ?? s[0]) === handlerSymbolId);
|
|
677
|
+
if (hit) {
|
|
678
|
+
symbolName = String(hit.name ?? hit[1]) || symbolName;
|
|
679
|
+
symPath = String(hit.filePath ?? hit[2]) || filePath;
|
|
685
680
|
}
|
|
686
681
|
}
|
|
687
682
|
}
|
|
@@ -306,6 +306,27 @@ interface LanguageProviderConfig {
|
|
|
306
306
|
* Default: undefined (no language-specific route extraction).
|
|
307
307
|
*/
|
|
308
308
|
readonly extractDecoratorRoutes?: (tree: Parser.Tree, filePath: string, lineOffset: number) => ExtractedDecoratorRoute[];
|
|
309
|
+
/**
|
|
310
|
+
* Name of the function a route decorator captured by the worker's generic
|
|
311
|
+
* `@decorator` query applies to, given the decorator's own AST node.
|
|
312
|
+
*
|
|
313
|
+
* The worker knows a decorator is a route decorator but not how this
|
|
314
|
+
* language's grammar attaches it to a definition, so it hands the node over
|
|
315
|
+
* unchanged and takes whatever the language returns. Only languages that
|
|
316
|
+
* declare route handlers through the generic decorator captures need this;
|
|
317
|
+
* languages with a dedicated {@link extractDecoratorRoutes} extractor
|
|
318
|
+
* (JS/TS via `nest.ts`, Java via `spring.ts`) already set
|
|
319
|
+
* `ExtractedDecoratorRoute.handlerName` there and should leave this undefined.
|
|
320
|
+
*
|
|
321
|
+
* Implementations must read their own decorated-definition shape directly and
|
|
322
|
+
* return undefined for anything else — never climb ancestors to find a name,
|
|
323
|
+
* since a decorator that is not attached to a function has no handler and a
|
|
324
|
+
* borrowed enclosing name resolves `handlerSymbolId` to the wrong symbol. The
|
|
325
|
+
* routes phase treats undefined as "fall back to the file-level edge".
|
|
326
|
+
*
|
|
327
|
+
* Default: undefined (no handler name from generic decorator captures).
|
|
328
|
+
*/
|
|
329
|
+
readonly decoratorRouteHandlerName?: (decoratorNode: SyntaxNode) => string | undefined;
|
|
309
330
|
/**
|
|
310
331
|
* Collect a project-wide, language-agnostic view of route-defining
|
|
311
332
|
* class/interface declarations (`SharedSpringType`) from a parsed file.
|
|
@@ -29,6 +29,7 @@ import { emitPythonScopeCaptures, pythonFunctionDefinitionLabel, interpretPython
|
|
|
29
29
|
import { extractDjangoRoutes } from '../route-extractors/django.js';
|
|
30
30
|
import { discoverDjangoRootUrls } from '../route-extractors/django-root-discovery.js';
|
|
31
31
|
import { extractPythonModuleConstants } from '../route-extractors/python-const-resolver.js';
|
|
32
|
+
import { pythonDecoratorRouteHandlerName } from '../route-extractors/python-decorator-handler.js';
|
|
32
33
|
const BUILT_INS = new Set([
|
|
33
34
|
'print',
|
|
34
35
|
'len',
|
|
@@ -120,6 +121,7 @@ export const pythonProvider = defineLanguage({
|
|
|
120
121
|
// and extraction resolve any repo-relative file regardless of parse chunking.
|
|
121
122
|
discoverRootRouteFiles: (files, contentMap, reader) => discoverDjangoRootUrls(files, contentMap, reader),
|
|
122
123
|
extractRoutes: (tree, filePath, reader, parser) => parser ? extractDjangoRoutes(tree, filePath, parser, reader) : [],
|
|
124
|
+
decoratorRouteHandlerName: pythonDecoratorRouteHandlerName,
|
|
123
125
|
labelOverride: pythonFunctionDefinitionLabel,
|
|
124
126
|
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
|
|
125
127
|
// Python is the first migration. See ./python/index.ts for the
|
|
@@ -265,28 +265,25 @@ export const routesPhase = {
|
|
|
265
265
|
}
|
|
266
266
|
let handlerContents;
|
|
267
267
|
if (routeRegistry.size > 0) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
268
|
+
// Resolve once so content attribution, the route stamp, and the edge use
|
|
269
|
+
// the same live graph node. Pre-seeded routes never own handler symbols.
|
|
270
|
+
const routes = [...routeRegistry].map(([routeKey, entry]) => {
|
|
271
|
+
const id = preSeededKeys.has(routeKey) ? undefined : routeHandlerSymbols.get(routeKey);
|
|
272
|
+
const node = id ? ctx.graph.getNode(id) : undefined;
|
|
273
|
+
const handlerSymbol = id && node ? { id, node } : undefined;
|
|
274
|
+
const resolvedPath = entry.source === DATA_ROUTE_TABLE_SOURCE
|
|
275
|
+
? handlerSymbol?.node.properties.filePath
|
|
274
276
|
: undefined;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
handlerContents = await readFileContents(ctx.repoPath,
|
|
279
|
-
for (const
|
|
277
|
+
const handlerPath = typeof resolvedPath === 'string' ? resolvedPath : entry.filePath;
|
|
278
|
+
return { routeKey, entry, handlerSymbol, handlerPath };
|
|
279
|
+
});
|
|
280
|
+
handlerContents = await readFileContents(ctx.repoPath, routes.map(({ handlerPath }) => handlerPath));
|
|
281
|
+
for (const { routeKey, entry, handlerSymbol, handlerPath } of routes) {
|
|
280
282
|
const { source: routeSource, method: routeMethod, url } = entry;
|
|
281
|
-
const handlerPath = handlerPathFor(routeKey, entry);
|
|
282
283
|
const content = handlerContents.get(handlerPath);
|
|
283
|
-
|
|
284
|
-
// `routeHandlerSymbols`, so a key that does is a route that LOST (#3049).
|
|
285
|
-
const handlerSymbolId = preSeededKeys.has(routeKey)
|
|
286
|
-
? undefined
|
|
287
|
-
: routeHandlerSymbols.get(routeKey);
|
|
284
|
+
const handlerSymbolId = handlerSymbol?.id;
|
|
288
285
|
const analysisContent = entry.source === DATA_ROUTE_TABLE_SOURCE && content
|
|
289
|
-
? handlerSymbolContent(content,
|
|
286
|
+
? handlerSymbolContent(content, handlerSymbol?.node)
|
|
290
287
|
: content;
|
|
291
288
|
const { responseKeys, errorKeys } = analysisContent
|
|
292
289
|
? handlerPath.endsWith('.php')
|
|
@@ -318,6 +315,18 @@ export const routesPhase = {
|
|
|
318
315
|
confidence: 1.0,
|
|
319
316
|
reason: routeSource,
|
|
320
317
|
});
|
|
318
|
+
// Keep the file edge for existing extractor queries; add the live
|
|
319
|
+
// definition edge for explicit handler-level traversal.
|
|
320
|
+
if (handlerSymbolId) {
|
|
321
|
+
ctx.graph.addRelationship({
|
|
322
|
+
id: generateId('HANDLES_ROUTE', `${handlerSymbolId}->${routeNodeId}`),
|
|
323
|
+
sourceId: handlerSymbolId,
|
|
324
|
+
targetId: routeNodeId,
|
|
325
|
+
type: 'HANDLES_ROUTE',
|
|
326
|
+
confidence: 1.0,
|
|
327
|
+
reason: routeSource,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
321
330
|
}
|
|
322
331
|
if (isDev) {
|
|
323
332
|
logger.info(`🗺️ Route registry: ${routeRegistry.size} routes${duplicateRoutes > 0 ? ` (${duplicateRoutes} duplicate URLs skipped)` : ''}`);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the function name attached to a Python decorator's immediate
|
|
3
|
+
* `decorated_definition`; reject every other shape rather than climbing.
|
|
4
|
+
*/
|
|
5
|
+
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
|
6
|
+
export declare function pythonDecoratorRouteHandlerName(decoratorNode: SyntaxNode): string | undefined;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the function name attached to a Python decorator's immediate
|
|
3
|
+
* `decorated_definition`; reject every other shape rather than climbing.
|
|
4
|
+
*/
|
|
5
|
+
export function pythonDecoratorRouteHandlerName(decoratorNode) {
|
|
6
|
+
const decorated = decoratorNode.parent;
|
|
7
|
+
if (decorated === null || decorated.type !== 'decorated_definition')
|
|
8
|
+
return undefined;
|
|
9
|
+
// `async def` is still a `function_definition` in tree-sitter-python (the
|
|
10
|
+
// `async` keyword is an anonymous child), so async handlers need no branch.
|
|
11
|
+
const definition = decorated.childForFieldName('definition');
|
|
12
|
+
if (!definition || definition.type !== 'function_definition')
|
|
13
|
+
return undefined;
|
|
14
|
+
const name = definition.childForFieldName('name')?.text;
|
|
15
|
+
return name !== undefined && name.length > 0 ? name : undefined;
|
|
16
|
+
}
|
|
@@ -1214,12 +1214,14 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1214
1214
|
const httpMethod = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(method)
|
|
1215
1215
|
? method
|
|
1216
1216
|
: 'GET';
|
|
1217
|
+
const handlerName = provider.decoratorRouteHandlerName?.(decoratorNode);
|
|
1217
1218
|
const base = {
|
|
1218
1219
|
filePath: file.path,
|
|
1219
1220
|
httpMethod,
|
|
1220
1221
|
decoratorName,
|
|
1221
1222
|
lineNumber: decoratorNode.startPosition.row + lineOffset,
|
|
1222
1223
|
...(decoratorReceiver ? { decoratorReceiver } : {}),
|
|
1224
|
+
...(handlerName ? { handlerName } : {}),
|
|
1223
1225
|
};
|
|
1224
1226
|
if (decoratorArgStr) {
|
|
1225
1227
|
// String-literal path (the fast path, unchanged). Empty-string
|
package/dist/core/lbug/schema.js
CHANGED
|
@@ -342,7 +342,7 @@ const DEFINITION_ANCHOR_LABELS = NODE_TABLES.filter((label) => !NON_DEFINITION_L
|
|
|
342
342
|
* | `Annotation` | `frameworks/spring/conditionals.ts` CONDITIONAL_ON | `resolveDefGraphId` / `resolveCallerGraphId` |
|
|
343
343
|
* | `Community` | `pipeline-phases/communities.ts` MEMBER_OF | Leiden membership, `isCommunitySymbol`-gated |
|
|
344
344
|
* | `Process` | `pipeline-phases/processes.ts` STEP_IN_PROCESS | trace step node |
|
|
345
|
-
* | `Route` | `pipeline-phases/routes.ts` HANDLES_ROUTE | `generateId('File', handlerPath)` — a literal |
|
|
345
|
+
* | `Route` | `pipeline-phases/routes.ts` HANDLES_ROUTE | `generateId('File', handlerPath)` — a literal — plus, when the route's handler resolves, that definition |
|
|
346
346
|
* | `Tool` | `pipeline-phases/tools.ts` HANDLES_TOOL | `handlerNodeId` — whatever definition the decorator sat on |
|
|
347
347
|
* | `File` | `languages/vue/scope-resolver.ts` BINDS_EVENT_HANDLER | handler node |
|
|
348
348
|
* | `Record` | `cobol-processor.ts` × 8 external-resource sites | `scopedCallerLookup` |
|
|
@@ -354,25 +354,35 @@ const DEFINITION_ANCHOR_LABELS = NODE_TABLES.filter((label) => !NON_DEFINITION_L
|
|
|
354
354
|
* {@link DEFINITION_ANCHOR_LABELS} × this set covers all four plus every
|
|
355
355
|
* sibling the same emitters can reach.
|
|
356
356
|
*
|
|
357
|
-
* TWO TARGETS
|
|
358
|
-
* them ON PURPOSE (~
|
|
357
|
+
* TWO TARGETS USE ONLY PART OF THEIR CROSS PRODUCT TODAY, and it over-declares
|
|
358
|
+
* for them ON PURPOSE (~45 of the 182 attachment pairs have no emitter that can
|
|
359
|
+
* reach them right now):
|
|
359
360
|
* - `Community` — `isCommunitySymbol` (`community-processor.ts`) admits only
|
|
360
361
|
* `Function` / `Class` / `Method` / `Interface` as members, so the other 22
|
|
361
362
|
* anchors cannot source a MEMBER_OF edge until that predicate widens.
|
|
362
|
-
* - `Route` — HANDLES_ROUTE sources
|
|
363
|
-
*
|
|
363
|
+
* - `Route` — HANDLES_ROUTE always sources the literal
|
|
364
|
+
* `generateId('File', handlerPath)`, and additionally the route's HANDLER
|
|
365
|
+
* DEFINITION whenever `routeHandlerSymbols` produced an id that
|
|
366
|
+
* `ctx.graph.getNode` confirms is in the graph. That second anchor is a
|
|
367
|
+
* lookup result the emitter never label-checks; handler resolution returns
|
|
368
|
+
* `Function` / `Method` definitions today, so `Route` is no longer a
|
|
369
|
+
* `File`-only target and the remaining anchors are headroom.
|
|
364
370
|
*
|
|
365
371
|
* Those pairs stay declared because the two sides of the error are not
|
|
366
372
|
* symmetric: an UNDECLARED pair makes LadybugDB reject the edge and aborts
|
|
367
373
|
* `analyze` outright on a user's repo, while an unused DECLARED pair costs
|
|
368
374
|
* almost nothing — `bench/schema-pairs` measured the pre-#2801 332→450 growth
|
|
369
|
-
* (118 pairs, of which these ~
|
|
375
|
+
* (118 pairs, of which these ~45 are a part) at 0.93–1.05×, i.e. inside
|
|
370
376
|
* run-to-run noise. #2801's 11 generated `Record` pairs bring the total to 461.
|
|
371
377
|
* Its Windows measurements and noise caveats live in the benchmark README; the
|
|
372
378
|
* operational production ceiling is the checked 1.5× budget.
|
|
373
379
|
* Every one of the four aborts above came from re-narrowing a set to what one
|
|
374
380
|
* predicate looked like it allowed — so a reading of `isCommunitySymbol` is not
|
|
375
|
-
* grounds to shrink this.
|
|
381
|
+
* grounds to shrink this. `Route` is the worked example in the other direction:
|
|
382
|
+
* the definition-level HANDLES_ROUTE edge started emitting `Function|Route` /
|
|
383
|
+
* `Method|Route` with no DDL change, because those pairs were already declared.
|
|
384
|
+
* Widening `isCommunitySymbol`, or handler resolution returning a further
|
|
385
|
+
* label, is likewise a no-op here.
|
|
376
386
|
*
|
|
377
387
|
* `Route` / `Tool` being excluded as ANCHORS (see {@link NON_DEFINITION_LABELS})
|
|
378
388
|
* is likewise a SIZE choice, not something derived from a rule: they do source
|
|
@@ -700,7 +700,18 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
|
|
|
700
700
|
// pre-change cache would have served as zero. origin/main at allocation is 88.
|
|
701
701
|
// RE-CHECK AGAINST origin/main AND OPEN PRs IMMEDIATELY BEFORE MERGING — this
|
|
702
702
|
// entry was allocated 83 first, and five bumps landed upstream before it merged.
|
|
703
|
-
|
|
703
|
+
// 89 -> 90 (#2865): route decorator captures now carry `handlerName` in
|
|
704
|
+
// `decoratorRoutes`, which is what lets `resolveRouteHandlerSymbols` stamp
|
|
705
|
+
// `handlerSymbolId` and the routes phase emit the definition-level
|
|
706
|
+
// HANDLES_ROUTE edge. The field is minted in the parse WORKER and persisted
|
|
707
|
+
// verbatim, so a warm v89 cache replays handler-less decorator routes: every
|
|
708
|
+
// route loses its definition-level association on incremental analyze while
|
|
709
|
+
// every cold-run test passes — the inert-feature trap the entries above record.
|
|
710
|
+
// 89 is now taken by merged #3128. 90 is the next free value above origin/main
|
|
711
|
+
// (89) and above every in-flight claim found by scanning open PRs'
|
|
712
|
+
// parse-cache.ts at their exact head SHAs (highest other open claim was still
|
|
713
|
+
// ≤88). RE-CHECK AGAINST origin/main AND OPEN PRs IMMEDIATELY BEFORE MERGING.
|
|
714
|
+
const SCHEMA_BUMP = 90;
|
|
704
715
|
const GITNEXUS_PKG_VERSION = (() => {
|
|
705
716
|
try {
|
|
706
717
|
// package.json sits at gitnexus/package.json — two levels up from
|
package/package.json
CHANGED