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.
- 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/entry-point-scoring.d.ts +6 -2
- package/dist/core/ingestion/entry-point-scoring.js +8 -44
- 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/utils/test-file-path.d.ts +28 -0
- package/dist/core/ingestion/utils/test-file-path.js +87 -0
- package/dist/core/ingestion/workers/parse-worker.js +2 -0
- package/dist/core/lbug/schema.js +17 -7
- package/dist/mcp/local/local-backend.d.ts +3 -6
- package/dist/mcp/local/local-backend.js +3 -24
- 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
|
}
|
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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