gitnexus 1.6.11-rc.30 → 1.6.11-rc.32

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
  }
@@ -29,8 +29,12 @@ export interface EntryPointScoreResult {
29
29
  */
30
30
  export declare function calculateEntryPointScore(name: string, language: SupportedLanguages, isExported: boolean, callerCount: number, calleeCount: number, filePath?: string): EntryPointScoreResult;
31
31
  /**
32
- * Check if a file path is a test file (should be excluded from entry points)
33
- * Covers common test file patterns across all supported languages
32
+ * Check if a file path is a test file (should be excluded from entry points).
33
+ *
34
+ * Delegates to the shared predicate in `utils/test-file-path.ts`. This used to be
35
+ * a second, hand-maintained copy that had drifted from the one backing the MCP
36
+ * `includeTests` flag — see that module's header. Re-exported under this name so
37
+ * existing importers are unaffected.
34
38
  */
35
39
  export declare function isTestFile(filePath: string): boolean;
36
40
  /**
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { detectFrameworkFromPath } from './framework-detection.js';
13
13
  import { providers } from './languages/index.js';
14
+ import { isTestFilePath } from './utils/test-file-path.js';
14
15
  // ============================================================================
15
16
  // NAME PATTERNS
16
17
  // ============================================================================
@@ -127,52 +128,15 @@ export function calculateEntryPointScore(name, language, isExported, callerCount
127
128
  // HELPER FUNCTIONS
128
129
  // ============================================================================
129
130
  /**
130
- * Check if a file path is a test file (should be excluded from entry points)
131
- * Covers common test file patterns across all supported languages
131
+ * Check if a file path is a test file (should be excluded from entry points).
132
+ *
133
+ * Delegates to the shared predicate in `utils/test-file-path.ts`. This used to be
134
+ * a second, hand-maintained copy that had drifted from the one backing the MCP
135
+ * `includeTests` flag — see that module's header. Re-exported under this name so
136
+ * existing importers are unaffected.
132
137
  */
133
138
  export function isTestFile(filePath) {
134
- const p = filePath.toLowerCase().replace(/\\/g, '/');
135
- return (
136
- // JavaScript/TypeScript test patterns
137
- p.includes('.test.') ||
138
- p.includes('.spec.') ||
139
- p.includes('__tests__/') ||
140
- p.includes('__mocks__/') ||
141
- // Generic test folders
142
- p.includes('/test/') ||
143
- p.includes('/tests/') ||
144
- p.includes('/testing/') ||
145
- // Python test patterns
146
- p.endsWith('_test.py') ||
147
- p.includes('/test_') ||
148
- // Go test patterns
149
- p.endsWith('_test.go') ||
150
- // Java test patterns
151
- p.includes('/src/test/') ||
152
- // Rust test patterns (inline tests are different, but test files)
153
- p.includes('/tests/') ||
154
- // Swift/iOS test patterns
155
- p.endsWith('tests.swift') ||
156
- p.endsWith('test.swift') ||
157
- p.includes('uitests/') ||
158
- // C# test patterns
159
- p.endsWith('tests.cs') ||
160
- p.endsWith('test.cs') ||
161
- p.includes('.tests/') ||
162
- p.includes('.test/') ||
163
- p.includes('.integrationtests/') ||
164
- p.includes('.unittests/') ||
165
- p.includes('/testproject/') ||
166
- // PHP/Laravel test patterns
167
- p.endsWith('test.php') ||
168
- p.endsWith('spec.php') ||
169
- p.includes('/tests/feature/') ||
170
- p.includes('/tests/unit/') ||
171
- // Ruby test patterns
172
- p.endsWith('_spec.rb') ||
173
- p.endsWith('_test.rb') ||
174
- p.includes('/spec/') ||
175
- p.includes('/test/fixtures/'));
139
+ return isTestFilePath(filePath);
176
140
  }
177
141
  /**
178
142
  * Check if a file path is likely a utility/helper file
@@ -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
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Test-file path classification — the single source of truth.
3
+ *
4
+ * WHY THIS MODULE EXISTS
5
+ *
6
+ * Two independent copies of this predicate existed and had drifted apart:
7
+ *
8
+ * - `core/ingestion/entry-point-scoring.ts` `isTestFile` — excludes test
9
+ * files from process entry-point detection.
10
+ * - `mcp/local/local-backend.ts` `isTestFilePath` — backs the
11
+ * `includeTests` flag on `impact` / `trace` / `context`.
12
+ *
13
+ * They answered "is this a test file?" differently, so the same path could be a
14
+ * test in one code path and not the other. The MCP copy recognized no C#, Java,
15
+ * or Swift test convention at all, meaning `includeTests: false` silently failed
16
+ * to filter them; the scoring copy missed `/conftest.` (it already matched
17
+ * `/test/`, so `/test/fixtures/` was never a scoring gap).
18
+ *
19
+ * The duplication was not gratuitous: `entry-point-scoring.ts` imports the
20
+ * language-provider registry, and #2802 deliberately cut that closure out of MCP
21
+ * server startup. Importing it back into `local-backend.ts` would reintroduce
22
+ * that cost. So the shared predicate lives here instead, with NO imports — pure
23
+ * string matching — and both callers delegate to it.
24
+ *
25
+ * Keep it dependency-free. Anything imported here lands in MCP startup.
26
+ */
27
+ /** Absent / empty paths are not test paths. */
28
+ export declare function isTestFilePath(filePath: string | null | undefined): boolean;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Test-file path classification — the single source of truth.
3
+ *
4
+ * WHY THIS MODULE EXISTS
5
+ *
6
+ * Two independent copies of this predicate existed and had drifted apart:
7
+ *
8
+ * - `core/ingestion/entry-point-scoring.ts` `isTestFile` — excludes test
9
+ * files from process entry-point detection.
10
+ * - `mcp/local/local-backend.ts` `isTestFilePath` — backs the
11
+ * `includeTests` flag on `impact` / `trace` / `context`.
12
+ *
13
+ * They answered "is this a test file?" differently, so the same path could be a
14
+ * test in one code path and not the other. The MCP copy recognized no C#, Java,
15
+ * or Swift test convention at all, meaning `includeTests: false` silently failed
16
+ * to filter them; the scoring copy missed `/conftest.` (it already matched
17
+ * `/test/`, so `/test/fixtures/` was never a scoring gap).
18
+ *
19
+ * The duplication was not gratuitous: `entry-point-scoring.ts` imports the
20
+ * language-provider registry, and #2802 deliberately cut that closure out of MCP
21
+ * server startup. Importing it back into `local-backend.ts` would reintroduce
22
+ * that cost. So the shared predicate lives here instead, with NO imports — pure
23
+ * string matching — and both callers delegate to it.
24
+ *
25
+ * Keep it dependency-free. Anything imported here lands in MCP startup.
26
+ */
27
+ /**
28
+ * Lowercase forward-slash path substrings. Directory needles include a leading
29
+ * slash so they match path components after the caller slash-prefixes relative
30
+ * paths. `/test/` already covers Maven `src/test` and `/test/fixtures/`;
31
+ * `/tests/` covers Laravel `tests/Feature` and `/tests/fixtures/`.
32
+ */
33
+ const TEST_PATH_SUBSTRINGS = [
34
+ '.test.',
35
+ '.spec.',
36
+ '__tests__/',
37
+ '__mocks__/',
38
+ '/test/',
39
+ '/tests/',
40
+ '/testing/',
41
+ '/spec/',
42
+ '/test_',
43
+ '/conftest.',
44
+ '/uitests/',
45
+ '.tests/',
46
+ '.test/',
47
+ '.integrationtests/',
48
+ '.unittests/',
49
+ '/testproject/',
50
+ ];
51
+ /** Case-insensitive suffixes that already include a delimiter (`_test.py`, not `test.py`). */
52
+ const TEST_PATH_DELIMITED_SUFFIXES = [
53
+ '_test.py',
54
+ '_test.go',
55
+ '_spec.rb',
56
+ '_test.rb',
57
+ ];
58
+ /**
59
+ * Case-sensitive `Test`/`Tests`/`Spec` suffixes. Lowercasing first would also
60
+ * match production names such as `Contest.swift` and `Latest.php`.
61
+ */
62
+ const TEST_PATH_CASED_SUFFIXES = [
63
+ 'Tests.swift',
64
+ 'Test.swift',
65
+ 'Tests.cs',
66
+ 'Test.cs',
67
+ 'Test.php',
68
+ 'Spec.php',
69
+ ];
70
+ /** Absent / empty paths are not test paths. */
71
+ export function isTestFilePath(filePath) {
72
+ if (!filePath)
73
+ return false;
74
+ const slashed = filePath.replace(/\\/g, '/');
75
+ const prefixed = slashed.startsWith('/') ? slashed : `/${slashed}`;
76
+ const lower = prefixed.toLowerCase();
77
+ if (TEST_PATH_SUBSTRINGS.some((needle) => lower.includes(needle)))
78
+ return true;
79
+ if (TEST_PATH_DELIMITED_SUFFIXES.some((suffix) => lower.endsWith(suffix)))
80
+ return true;
81
+ // Xcode `{Product}UITests` targets. Slash-anchored `/uitests/` does not match
82
+ // `MyAppUITests`; an unanchored `uitests/` substring also matches `fruitests`.
83
+ if (prefixed.split('/').some((seg) => seg.endsWith('UITests')))
84
+ return true;
85
+ const basename = prefixed.slice(prefixed.lastIndexOf('/') + 1);
86
+ return TEST_PATH_CASED_SUFFIXES.some((suffix) => basename.endsWith(suffix));
87
+ }
@@ -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
@@ -5,14 +5,12 @@
5
5
  * Supports multiple indexed repositories via a global registry.
6
6
  * LadybugDB connections are opened lazily per repo on first query.
7
7
  */
8
+ import { isTestFilePath } from '../../core/ingestion/utils/test-file-path.js';
8
9
  import { type RegistryEntry, type BranchSummary } from '../../storage/repo-manager.js';
9
10
  import { GroupService } from '../../core/group/service.js';
10
11
  import { type StalenessInfo } from '../../core/git-staleness.js';
11
- /**
12
- * Quick test-file detection for filtering impact results.
13
- * Matches common test file patterns across all supported languages.
14
- */
15
- export declare function isTestFilePath(filePath: string | null | undefined): boolean;
12
+ /** Shared predicate; re-exported so MCP importers keep the old public name. */
13
+ export { isTestFilePath };
16
14
  /** Valid LadybugDB node labels for safe Cypher query construction */
17
15
  export declare const VALID_NODE_LABELS: Set<string>;
18
16
  /** Valid relation types for impact analysis filtering */
@@ -859,4 +857,3 @@ export declare class LocalBackend {
859
857
  queryProcessDetail(name: string, repoName?: string): Promise<any>;
860
858
  disconnect(): Promise<void>;
861
859
  }
862
- export {};
@@ -19,6 +19,7 @@ import { LBUG_ID_PROBE_BATCH_SIZE, LBUG_QUERY_BATCH_SIZE } from '../../core/lbug
19
19
  import { chunk, mapConcurrent } from '../../lib/utils.js';
20
20
  import { pathSuffixOf } from './path-predicate.js';
21
21
  import { toOneBasedLine } from '../../core/ingestion/utils/line-base.js';
22
+ import { isTestFilePath } from '../../core/ingestion/utils/test-file-path.js';
22
23
  import { isWalCorruptionError, WAL_RECOVERY_SUGGESTION } from '../../core/lbug/lbug-config.js';
23
24
  // Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
24
25
  // at MCP server startup — crashes on unsupported Node ABI versions (#89)
@@ -190,30 +191,8 @@ function normalizeToolParams(method, params) {
190
191
  }
191
192
  // AI context generation is CLI-only (gitnexus analyze)
192
193
  // import { generateAIContextFiles } from '../../cli/ai-context.js';
193
- /**
194
- * Quick test-file detection for filtering impact results.
195
- * Matches common test file patterns across all supported languages.
196
- */
197
- export function isTestFilePath(filePath) {
198
- if (!filePath)
199
- return false;
200
- const p = filePath.toLowerCase().replace(/\\/g, '/');
201
- return (p.includes('.test.') ||
202
- p.includes('.spec.') ||
203
- p.includes('__tests__/') ||
204
- p.includes('__mocks__/') ||
205
- p.includes('/test/') ||
206
- p.includes('/tests/') ||
207
- p.includes('/testing/') ||
208
- p.includes('/fixtures/') ||
209
- p.endsWith('_test.go') ||
210
- p.endsWith('_test.py') ||
211
- p.endsWith('_spec.rb') ||
212
- p.endsWith('_test.rb') ||
213
- p.includes('/spec/') ||
214
- p.includes('/test_') ||
215
- p.includes('/conftest.'));
216
- }
194
+ /** Shared predicate; re-exported so MCP importers keep the old public name. */
195
+ export { isTestFilePath };
217
196
  /** Valid LadybugDB node labels for safe Cypher query construction */
218
197
  export const VALID_NODE_LABELS = new Set([
219
198
  'File',
@@ -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.32",
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",