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.
@@ -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
- // It would be a no-op even if set to 'complete': FastAPI decorator routes set
1032
- // no handlerName (generic worker path) and Django sets methodName: null, so no
1033
- // Python file ever resolves a handlerSymbolId and none would be parse-skipped.
1034
- // Declaring 'complete' now is only a latent trap for the moment a follow-up
1035
- // gives FastAPI routes a handlerName. `hasConsumerSignals` is kept (and is a
1036
- // true superset of scan()'s consumer shapes) so the precondition already holds
1037
- // when Python is later flipped to 'complete'.
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
- try {
676
- const syms = await db(CONTAINING_QUERY, { filePath });
677
- const hit = syms.find((s) => String(s.uid ?? s[0]) === handlerSymbolId);
678
- if (hit) {
679
- symbolName = String(hit.name ?? hit[1]) || symbolName;
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
- const handlerPathFor = (routeKey, entry) => {
269
- if (entry.source !== DATA_ROUTE_TABLE_SOURCE)
270
- return entry.filePath;
271
- const handlerSymbolId = routeHandlerSymbols.get(routeKey);
272
- const resolvedPath = handlerSymbolId
273
- ? ctx.graph.getNode(handlerSymbolId)?.properties.filePath
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
- return typeof resolvedPath === 'string' ? resolvedPath : entry.filePath;
276
- };
277
- const handlerPaths = [...routeRegistry].map(([key, entry]) => handlerPathFor(key, entry));
278
- handlerContents = await readFileContents(ctx.repoPath, handlerPaths);
279
- for (const [routeKey, entry] of routeRegistry) {
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
- // A pre-seeded route can never legitimately appear in
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, handlerSymbolId ? ctx.graph.getNode(handlerSymbolId) : undefined)
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
@@ -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 ARE LABEL-GATED TODAY, and the cross product over-declares for
358
- * them ON PURPOSE (~47 of the 182 attachment pairs are unreachable right now):
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 `generateId('File', handlerPath)`, a
363
- * literal `File`, so every non-`File` anchor is headroom.
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 ~47 are a part) at 0.93–1.05×, i.e. inside
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. Widening either predicate is then a no-op here.
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
- const SCHEMA_BUMP = 89;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.11-rc.30",
3
+ "version": "1.6.11-rc.31",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",