gitnexus 1.6.13-rc.25 → 1.6.13-rc.26
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/README.md +2 -2
- package/dist/core/group/service.d.ts +2 -0
- package/dist/core/group/service.js +68 -6
- package/dist/core/ingestion/call-processor.d.ts +2 -0
- package/dist/core/ingestion/call-processor.js +70 -4
- package/dist/core/ingestion/entry-point-scoring.js +20 -8
- package/dist/core/ingestion/framework-detection.js +8 -0
- package/dist/core/ingestion/language-provider.d.ts +11 -0
- package/dist/core/ingestion/languages/javascript/captures.js +8 -1
- package/dist/core/ingestion/languages/javascript/query.js +194 -0
- package/dist/core/ingestion/languages/typescript/captures.js +8 -1
- package/dist/core/ingestion/languages/typescript/query.js +211 -0
- package/dist/core/ingestion/languages/typescript.js +43 -15
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +4 -0
- package/dist/core/ingestion/route-extractors/trpc.d.ts +4 -0
- package/dist/core/ingestion/route-extractors/trpc.js +664 -0
- package/dist/core/ingestion/tree-sitter-queries.js +363 -4
- package/dist/core/ingestion/ts-js-hoc-utils.d.ts +3 -1
- package/dist/core/ingestion/ts-js-hoc-utils.js +53 -1
- package/dist/core/ingestion/workers/parse-worker.js +17 -1
- package/dist/core/search/fts-schema.js +14 -6
- package/dist/mcp/local/local-backend.d.ts +27 -0
- package/dist/mcp/local/local-backend.js +440 -9
- package/dist/mcp/local/pdg-impact.js +2 -2
- package/dist/mcp/tools.d.ts +7 -0
- package/dist/mcp/tools.js +31 -9
- package/dist/storage/parse-cache.js +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -187,8 +187,8 @@ Your AI agent gets **17 tools** (15 per-repo + 2 group) automatically:
|
|
|
187
187
|
| Tool | What It Does |
|
|
188
188
|
| ---------------- | ---------------------------------------------------------------------- |
|
|
189
189
|
| `list_repos` | Discover all indexed repositories (paginated — `limit`/`offset`) |
|
|
190
|
-
| `query` | Process-grouped hybrid search (BM25 + semantic + RRF)
|
|
191
|
-
| `context` | 360-degree symbol view — categorized refs, process participation
|
|
190
|
+
| `query` | Process-grouped hybrid search (BM25 + semantic + RRF); optional `chain_depth` expands each result's call chain |
|
|
191
|
+
| `context` | 360-degree symbol view — categorized refs, process participation, HTTP routes, `is_entry_point` flag; optional `chain_depth` call-chain expansion |
|
|
192
192
|
| `impact` | Blast radius analysis with depth grouping and confidence |
|
|
193
193
|
| `trace` | Shortest directed path between two symbols (call + class-member edges) |
|
|
194
194
|
| `detect_changes` | Git-diff impact — maps changed lines to affected processes |
|
|
@@ -40,6 +40,7 @@ export interface GroupToolPort {
|
|
|
40
40
|
limit?: number;
|
|
41
41
|
max_symbols?: number;
|
|
42
42
|
include_content?: boolean;
|
|
43
|
+
chain_depth?: number;
|
|
43
44
|
}): Promise<unknown>;
|
|
44
45
|
impactByUid(repoId: string, uid: string, direction: string, opts: {
|
|
45
46
|
maxDepth: number;
|
|
@@ -53,6 +54,7 @@ export interface GroupToolPort {
|
|
|
53
54
|
uid?: string;
|
|
54
55
|
file_path?: string;
|
|
55
56
|
include_content?: boolean;
|
|
57
|
+
chain_depth?: number;
|
|
56
58
|
}): Promise<unknown>;
|
|
57
59
|
trace?(repo: GroupRepoHandle, params: {
|
|
58
60
|
from?: string;
|
|
@@ -6,6 +6,7 @@ import fsp from 'node:fs/promises';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { checkStaleness } from '../git-staleness.js';
|
|
8
8
|
import { stalenessStatus } from '../staleness-status.js';
|
|
9
|
+
import { mapConcurrent } from '../../lib/utils.js';
|
|
9
10
|
import { canonicalizePath, loadMeta, readRegistryStrict, registryPathEquals, } from '../../storage/repo-manager.js';
|
|
10
11
|
import { crossRepoCompleteness } from './completeness.js';
|
|
11
12
|
import { recordedMatchStages, recordedRepoList } from './completeness.js';
|
|
@@ -246,6 +247,39 @@ function rejectRetiredSyncParams(params) {
|
|
|
246
247
|
}
|
|
247
248
|
return null;
|
|
248
249
|
}
|
|
250
|
+
/** Advertised MCP query page bounds (tools.ts schema). Mirrored so core stays MCP-free. */
|
|
251
|
+
const GROUP_QUERY_DEFAULT_LIMIT = 10;
|
|
252
|
+
const GROUP_QUERY_MAX_LIMIT = 100;
|
|
253
|
+
const GROUP_QUERY_DEFAULT_MAX_SYMBOLS = 25;
|
|
254
|
+
const GROUP_QUERY_MAX_SYMBOLS = 200;
|
|
255
|
+
const GROUP_CHAIN_MAX_DEPTH = 3;
|
|
256
|
+
/** Cap per-member fan-out for group query/context. Complements LocalBackend's per-query BFS cap. */
|
|
257
|
+
const GROUP_MEMBER_CONCURRENCY = 4;
|
|
258
|
+
function parseGroupQueryBound(value, field, fallback, max) {
|
|
259
|
+
if (value === undefined)
|
|
260
|
+
return { ok: true, value: fallback };
|
|
261
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > max) {
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
error: `Invalid "${field}": expected an integer in [1, ${max}], got ${describeValue(value)}.`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return { ok: true, value };
|
|
268
|
+
}
|
|
269
|
+
function parseGroupChainDepth(value) {
|
|
270
|
+
if (value === undefined)
|
|
271
|
+
return { ok: true, value: undefined };
|
|
272
|
+
if (typeof value !== 'number' ||
|
|
273
|
+
!Number.isInteger(value) ||
|
|
274
|
+
value < 0 ||
|
|
275
|
+
value > GROUP_CHAIN_MAX_DEPTH) {
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
error: `Invalid "chain_depth": expected an integer in [0, ${GROUP_CHAIN_MAX_DEPTH}], got ${describeValue(value)}.`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return { ok: true, value };
|
|
282
|
+
}
|
|
249
283
|
export class GroupService {
|
|
250
284
|
port;
|
|
251
285
|
constructor(port) {
|
|
@@ -453,6 +487,11 @@ export class GroupService {
|
|
|
453
487
|
if (!uid && !target) {
|
|
454
488
|
return { group: name, error: 'target or uid is required', results: [] };
|
|
455
489
|
}
|
|
490
|
+
const parsedChainDepth = parseGroupChainDepth(params.chain_depth);
|
|
491
|
+
if (parsedChainDepth.ok === false) {
|
|
492
|
+
return { group: name, error: parsedChainDepth.error, results: [] };
|
|
493
|
+
}
|
|
494
|
+
const chain_depth = parsedChainDepth.value;
|
|
456
495
|
const groupDir = getGroupDir(getDefaultGitnexusDir(), name);
|
|
457
496
|
let config;
|
|
458
497
|
try {
|
|
@@ -476,7 +515,7 @@ export class GroupService {
|
|
|
476
515
|
};
|
|
477
516
|
}
|
|
478
517
|
const memberEntries = Object.entries(config.repos).filter(([repoPath]) => repoInSubgroup(repoPath, subgroup, subgroupExact));
|
|
479
|
-
const results = await
|
|
518
|
+
const results = (await mapConcurrent(memberEntries, async ([repoPath, registryName]) => {
|
|
480
519
|
try {
|
|
481
520
|
const repoObj = await this.port.resolveRepo(registryName);
|
|
482
521
|
const payload = await this.port.context(repoObj, {
|
|
@@ -484,6 +523,7 @@ export class GroupService {
|
|
|
484
523
|
uid,
|
|
485
524
|
file_path,
|
|
486
525
|
include_content,
|
|
526
|
+
chain_depth,
|
|
487
527
|
});
|
|
488
528
|
if (servicePrefix) {
|
|
489
529
|
const st = payload?.status;
|
|
@@ -501,7 +541,12 @@ export class GroupService {
|
|
|
501
541
|
payload: { error: e instanceof Error ? e.message : String(e) },
|
|
502
542
|
};
|
|
503
543
|
}
|
|
504
|
-
}))
|
|
544
|
+
}, { concurrency: GROUP_MEMBER_CONCURRENCY })).map((result, index) => {
|
|
545
|
+
if (result)
|
|
546
|
+
return result;
|
|
547
|
+
const [repoPath, registryName] = memberEntries[index];
|
|
548
|
+
return { repoPath, registryName, payload: {} };
|
|
549
|
+
});
|
|
505
550
|
return {
|
|
506
551
|
group: name,
|
|
507
552
|
target: target || uid,
|
|
@@ -520,7 +565,18 @@ export class GroupService {
|
|
|
520
565
|
return { error: 'service must not be an empty string' };
|
|
521
566
|
}
|
|
522
567
|
const servicePrefix = normalizeServicePrefix(params.service);
|
|
523
|
-
const
|
|
568
|
+
const parsedLimit = parseGroupQueryBound(params.limit, 'limit', GROUP_QUERY_DEFAULT_LIMIT, GROUP_QUERY_MAX_LIMIT);
|
|
569
|
+
if (parsedLimit.ok === false)
|
|
570
|
+
return { error: parsedLimit.error };
|
|
571
|
+
const parsedMaxSymbols = parseGroupQueryBound(params.max_symbols, 'max_symbols', GROUP_QUERY_DEFAULT_MAX_SYMBOLS, GROUP_QUERY_MAX_SYMBOLS);
|
|
572
|
+
if (parsedMaxSymbols.ok === false)
|
|
573
|
+
return { error: parsedMaxSymbols.error };
|
|
574
|
+
const parsedChainDepth = parseGroupChainDepth(params.chain_depth);
|
|
575
|
+
if (parsedChainDepth.ok === false)
|
|
576
|
+
return { error: parsedChainDepth.error };
|
|
577
|
+
const limit = parsedLimit.value;
|
|
578
|
+
const max_symbols = parsedMaxSymbols.value;
|
|
579
|
+
const chain_depth = parsedChainDepth.value;
|
|
524
580
|
const subgroup = typeof params.subgroup === 'string' ? params.subgroup : undefined;
|
|
525
581
|
const subgroupExact = params.subgroupExact === true;
|
|
526
582
|
const groupDir = getGroupDir(getDefaultGitnexusDir(), name);
|
|
@@ -534,14 +590,15 @@ export class GroupService {
|
|
|
534
590
|
throw err;
|
|
535
591
|
}
|
|
536
592
|
const memberEntries = Object.entries(config.repos).filter(([repoPath]) => repoInSubgroup(repoPath, subgroup, subgroupExact));
|
|
537
|
-
const perRepo = await
|
|
593
|
+
const perRepo = (await mapConcurrent(memberEntries, async ([repoPath, registryName]) => {
|
|
538
594
|
try {
|
|
539
595
|
const repoObj = await this.port.resolveRepo(registryName);
|
|
540
596
|
const queryResult = (await this.port.query(repoObj, {
|
|
541
597
|
query: queryText,
|
|
542
598
|
limit,
|
|
543
|
-
max_symbols
|
|
599
|
+
max_symbols,
|
|
544
600
|
include_content: false,
|
|
601
|
+
chain_depth,
|
|
545
602
|
}));
|
|
546
603
|
const processes = servicePrefix
|
|
547
604
|
? filterQueryByServicePrefix(queryResult, servicePrefix).processes
|
|
@@ -556,7 +613,12 @@ export class GroupService {
|
|
|
556
613
|
catch {
|
|
557
614
|
return { repo: repoPath, score: 0, processes: [] };
|
|
558
615
|
}
|
|
559
|
-
}))
|
|
616
|
+
}, { concurrency: GROUP_MEMBER_CONCURRENCY })).map((result, index) => {
|
|
617
|
+
if (result)
|
|
618
|
+
return result;
|
|
619
|
+
const [repoPath] = memberEntries[index];
|
|
620
|
+
return { repo: repoPath, score: 0, processes: [] };
|
|
621
|
+
});
|
|
560
622
|
const allProcesses = perRepo.flatMap((r) => r.processes);
|
|
561
623
|
allProcesses.sort((a, b) => b._rrf_score - a._rrf_score);
|
|
562
624
|
const topN = allProcesses.slice(0, limit);
|
|
@@ -31,6 +31,8 @@ interface RouteHandlerResolutionContext {
|
|
|
31
31
|
readonly files: readonly RouteResolutionFile[];
|
|
32
32
|
readonly resolveImportTarget: (parsedImport: ParsedImport, fromFile: string) => string | null;
|
|
33
33
|
readonly isExportedSymbol: (nodeId: string) => boolean;
|
|
34
|
+
/** 0-based graph-node startLine for same-name tRPC handler disambiguation. */
|
|
35
|
+
readonly nodeStartLine?: (nodeId: string) => number | undefined;
|
|
34
36
|
}
|
|
35
37
|
/** Record one exported graph node into the incremental ExportedTypeMap. */
|
|
36
38
|
export declare const accumulateExportedTypesFromParsedNode: (result: ExportedTypeMap, node: {
|
|
@@ -20,8 +20,46 @@ import { normalizeFetchURL, routeMatches } from './route-extractors/nextjs.js';
|
|
|
20
20
|
import { normalizeExtractedRoutePath, normalizeRouteMethod, routeNodeKey, } from './route-extractors/route-path.js';
|
|
21
21
|
import { extractReturnTypeName } from './type-extractors/shared.js';
|
|
22
22
|
import { DATA_ROUTE_TABLE_SOURCE } from './route-extractors/data-route-table.js';
|
|
23
|
+
import { toZeroBasedLine } from './utils/line-base.js';
|
|
23
24
|
const MAX_EXPORTS_PER_FILE = 500;
|
|
24
25
|
const MAX_TYPE_NAME_LENGTH = 256;
|
|
26
|
+
/** Same Function/Method gate as data-route resolution (`routeCallables`). */
|
|
27
|
+
function routeCallableDefs(defs) {
|
|
28
|
+
return defs.filter((def) => def.type === 'Function' || def.type === 'Method');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Pick a same-file route handler from `lookupExactAll` hits.
|
|
32
|
+
*
|
|
33
|
+
* Graph nodes store 0-based `startLine`; `ExtractedRoute.lineNumber` is 1-based
|
|
34
|
+
* (`i + 1` in the tRPC scanner). File-index lookup is not callable-only, so
|
|
35
|
+
* drop Property/Variable/Const (and other non-callables) first. When several
|
|
36
|
+
* callables share a name, prefer the unique def whose node startLine equals
|
|
37
|
+
* `toZeroBasedLine(route.lineNumber)`. If that exact match is missing (common
|
|
38
|
+
* when the arrow starts on the line after `.mutation(`), take the unique def
|
|
39
|
+
* whose startLine is the nearest `>=` target. Zero or 2+ winners (or no line
|
|
40
|
+
* reader) → `undefined` (fail-open).
|
|
41
|
+
*/
|
|
42
|
+
function pickSameFileHandler(defs, route, getStartLine) {
|
|
43
|
+
defs = routeCallableDefs(defs);
|
|
44
|
+
if (defs.length === 1)
|
|
45
|
+
return defs[0];
|
|
46
|
+
if (defs.length > 1 && getStartLine !== undefined) {
|
|
47
|
+
const targetLine = toZeroBasedLine(route.lineNumber);
|
|
48
|
+
const withLines = defs
|
|
49
|
+
.map((def) => ({ def, startLine: Number(getStartLine(def.nodeId)) }))
|
|
50
|
+
.filter((entry) => Number.isFinite(entry.startLine));
|
|
51
|
+
const exact = withLines.filter((entry) => entry.startLine === targetLine);
|
|
52
|
+
if (exact.length === 1)
|
|
53
|
+
return exact[0].def;
|
|
54
|
+
const atOrAfter = withLines.filter((entry) => entry.startLine >= targetLine);
|
|
55
|
+
if (atOrAfter.length === 0)
|
|
56
|
+
return undefined;
|
|
57
|
+
const nearestLine = Math.min(...atOrAfter.map((entry) => entry.startLine));
|
|
58
|
+
const nearest = atOrAfter.filter((entry) => entry.startLine === nearestLine);
|
|
59
|
+
return nearest.length === 1 ? nearest[0].def : undefined;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
25
63
|
/** Record one exported graph node into the incremental ExportedTypeMap. */
|
|
26
64
|
export const accumulateExportedTypesFromParsedNode = (result, node, symbolTable) => {
|
|
27
65
|
if (!node.properties?.isExported)
|
|
@@ -165,8 +203,32 @@ export const processRoutesFromExtracted = async (graph, extractedRoutes, model,
|
|
|
165
203
|
onProgress?.(i, extractedRoutes.length);
|
|
166
204
|
await yieldToEventLoop();
|
|
167
205
|
}
|
|
168
|
-
if (!route.
|
|
206
|
+
if (!route.methodName)
|
|
207
|
+
continue;
|
|
208
|
+
// tRPC routes carry NO controller: a router is an object binding, not a
|
|
209
|
+
// class, so the extractor leaves controllerName unset and names the
|
|
210
|
+
// handler from the callback identifier when present, otherwise from its
|
|
211
|
+
// object-literal key. Bind the same-file symbol directly.
|
|
212
|
+
// No unique match → skip, fail-open. Laravel routes always set
|
|
213
|
+
// controllerName and Django routes leave methodName null, so this
|
|
214
|
+
// branch is tRPC-only by construction — the laravel guessed-method
|
|
215
|
+
// fallback below never sees a controller-less route.
|
|
216
|
+
if (!route.controllerName) {
|
|
217
|
+
const handler = pickSameFileHandler(model.symbols.lookupExactAll(route.filePath, route.methodName), route, (id) => graph.getNode(id)?.properties.startLine);
|
|
218
|
+
if (!handler)
|
|
219
|
+
continue;
|
|
220
|
+
const sourceId = generateId('File', route.filePath);
|
|
221
|
+
const relId = generateId('CALLS', sourceId + ':route->' + handler.nodeId);
|
|
222
|
+
graph.addRelationship({
|
|
223
|
+
id: relId,
|
|
224
|
+
sourceId,
|
|
225
|
+
targetId: handler.nodeId,
|
|
226
|
+
type: 'CALLS',
|
|
227
|
+
confidence: ROUTE_EDGE_CONFIDENCE,
|
|
228
|
+
reason: 'trpc-route',
|
|
229
|
+
});
|
|
169
230
|
continue;
|
|
231
|
+
}
|
|
170
232
|
// Resolve the controller class. Qualified-first: when the routes file
|
|
171
233
|
// disambiguated the controller (a `use` import or inline `::class` FQN, both
|
|
172
234
|
// normalized to the registry's dot-joined key shape by the extractor), look
|
|
@@ -249,8 +311,9 @@ export function resolveRouteHandlerSymbols(model, extractedRoutes, decoratorRout
|
|
|
249
311
|
// Mirrors the routes phase `addRoute` first-writer-wins so the handler we
|
|
250
312
|
// stamp always belongs to the route that actually won the Route node.
|
|
251
313
|
const claimed = new Set();
|
|
252
|
-
// Resolve a single same-file symbol by name
|
|
253
|
-
//
|
|
314
|
+
// Resolve a single same-file symbol by name. Exactly one match → its nodeId.
|
|
315
|
+
// Zero or many matches → undefined (fail-open). tRPC same-name handlers
|
|
316
|
+
// use pickSameFileHandler at the controller-less call site instead.
|
|
254
317
|
const uniqueSymbolId = (filePath, name) => {
|
|
255
318
|
const defs = model.symbols.lookupExactAll(filePath, name);
|
|
256
319
|
return defs.length === 1 ? defs[0]?.nodeId : undefined;
|
|
@@ -259,7 +322,7 @@ export function resolveRouteHandlerSymbols(model, extractedRoutes, decoratorRout
|
|
|
259
322
|
const byId = new Map(defs.map((def) => [def.nodeId, def]));
|
|
260
323
|
return byId.size === 1 ? byId.values().next().value : undefined;
|
|
261
324
|
};
|
|
262
|
-
const routeCallables = (defs) => defs
|
|
325
|
+
const routeCallables = (defs) => routeCallableDefs(defs);
|
|
263
326
|
const exportedRouteCallables = (defs) => routeContext === undefined
|
|
264
327
|
? []
|
|
265
328
|
: routeCallables(defs).filter((def) => routeContext.isExportedSymbol(def.nodeId));
|
|
@@ -352,6 +415,9 @@ export function resolveRouteHandlerSymbols(model, extractedRoutes, decoratorRout
|
|
|
352
415
|
if (controllerDef)
|
|
353
416
|
methodId = uniqueSymbolId(controllerDef.filePath, route.methodName);
|
|
354
417
|
}
|
|
418
|
+
else if (!route.controllerName && route.methodName) {
|
|
419
|
+
methodId = pickSameFileHandler(model.symbols.lookupExactAll(route.filePath, route.methodName), route, routeContext?.nodeStartLine)?.nodeId;
|
|
420
|
+
}
|
|
355
421
|
claim(route.routePath, route.prefix ?? null, route.httpMethod, methodId);
|
|
356
422
|
}
|
|
357
423
|
const dataHandlerByRoute = new Map();
|
|
@@ -95,8 +95,24 @@ export function calculateEntryPointScore(name, language, isExported, callerCount
|
|
|
95
95
|
}
|
|
96
96
|
// Name pattern scoring
|
|
97
97
|
let nameMultiplier = 1.0;
|
|
98
|
+
// tRPC procedures often use accessor-style names
|
|
99
|
+
// (`getX`, `setX`, `isX`) that legitimately ARE entry points. Without this
|
|
100
|
+
// exemption, the 0.3× utility penalty cancels the 3.0× tRPC framework boost
|
|
101
|
+
// (net 0.9, below baseline), preventing procedures like `setSettings`
|
|
102
|
+
// from becoming Process entry points and hiding them from `query` results.
|
|
103
|
+
// Only accessor/predicate names skip the penalty — helpers such as
|
|
104
|
+
// `formatDate`, `_internal`, or `parseInput` in the same router file still
|
|
105
|
+
// match UTILITY_PATTERNS. The 3.0× tRPC framework boost stays path-based.
|
|
106
|
+
// The exemption follows detectFrameworkFromPath so T3 (`/api/routers/`) and
|
|
107
|
+
// `/app/trpc/routers/` layouts get the same treatment as `/trpc/routers/`.
|
|
108
|
+
// Optional chaining still guards paths that fail the tRPC predicates
|
|
109
|
+
// (e.g. generic `/routers/*.js` without `/trpc/` or `/api/routers/`).
|
|
110
|
+
// JS/JSX routers that match those predicates get `framework: 'trpc'`
|
|
111
|
+
// the same way TS/TSX routers do.
|
|
112
|
+
const frameworkHint = filePath ? detectFrameworkFromPath(filePath) : null;
|
|
113
|
+
const trpcAccessorExemption = frameworkHint?.framework === 'trpc' && /^(get|set|is|has|can|should|will|did)[A-Z]/.test(name);
|
|
98
114
|
// Check negative patterns first (utilities get penalized)
|
|
99
|
-
if (UTILITY_PATTERNS.some((p) => p.test(name))) {
|
|
115
|
+
if (!trpcAccessorExemption && UTILITY_PATTERNS.some((p) => p.test(name))) {
|
|
100
116
|
nameMultiplier = 0.3; // Significant penalty
|
|
101
117
|
reasons.push('utility-pattern');
|
|
102
118
|
}
|
|
@@ -108,14 +124,10 @@ export function calculateEntryPointScore(name, language, isExported, callerCount
|
|
|
108
124
|
reasons.push('entry-pattern');
|
|
109
125
|
}
|
|
110
126
|
}
|
|
111
|
-
// Framework detection bonus (Phase 2)
|
|
112
127
|
let frameworkMultiplier = 1.0;
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
frameworkMultiplier = frameworkHint.entryPointMultiplier;
|
|
117
|
-
reasons.push(`framework:${frameworkHint.reason}`);
|
|
118
|
-
}
|
|
128
|
+
if (frameworkHint) {
|
|
129
|
+
frameworkMultiplier = frameworkHint.entryPointMultiplier;
|
|
130
|
+
reasons.push(`framework:${frameworkHint.reason}`);
|
|
119
131
|
}
|
|
120
132
|
// Calculate final score
|
|
121
133
|
const finalScore = baseScore * exportMultiplier * nameMultiplier * frameworkMultiplier;
|
|
@@ -55,6 +55,14 @@ export function detectFrameworkFromPath(filePath) {
|
|
|
55
55
|
(p.endsWith('layout.tsx') || p.endsWith('layout.ts'))) {
|
|
56
56
|
return { framework: 'nextjs-app', entryPointMultiplier: 2.0, reason: 'nextjs-layout' };
|
|
57
57
|
}
|
|
58
|
+
// tRPC router files — before the expo-router /app/ catch-all so
|
|
59
|
+
// `src/app/trpc/routers/*.ts` is tRPC, not an Expo screen. `/routers/`
|
|
60
|
+
// alone also matches Vue Router / Next.js-style folders and must not
|
|
61
|
+
// take the 3.0 entry-point multiplier.
|
|
62
|
+
if ((p.endsWith('.ts') || p.endsWith('.tsx') || p.endsWith('.js') || p.endsWith('.jsx')) &&
|
|
63
|
+
((p.includes('/trpc/') && p.includes('/routers/')) || p.includes('/api/routers/'))) {
|
|
64
|
+
return { framework: 'trpc', entryPointMultiplier: 3.0, reason: 'trpc-router' };
|
|
65
|
+
}
|
|
58
66
|
// Expo Router - screen/layout/api files in app/ directory
|
|
59
67
|
if (p.includes('/app/') &&
|
|
60
68
|
(p.endsWith('.tsx') || p.endsWith('.ts') || p.endsWith('.jsx') || p.endsWith('.js'))) {
|
|
@@ -406,6 +406,17 @@ interface LanguageProviderConfig {
|
|
|
406
406
|
* language's grammar, available for re-parsing included files.
|
|
407
407
|
* Default: undefined (no route extraction). */
|
|
408
408
|
readonly extractRoutes?: (tree: Parser.Tree, filePath: string, reader: (relativePath: string) => string | null, parser?: Parser | null) => ExtractedRoute[];
|
|
409
|
+
/**
|
|
410
|
+
* Extract routes from file text without an AST.
|
|
411
|
+
*
|
|
412
|
+
* Content-based (regex / line scan), not tree-sitter. tRPC uses this hook:
|
|
413
|
+
* procedure routers are recognized from source text (`publicProcedure.query`)
|
|
414
|
+
* rather than grammar captures. The parse worker calls this when the hook is
|
|
415
|
+
* defined. Providers that need a path-gate apply it inside the hook.
|
|
416
|
+
*
|
|
417
|
+
* Default: undefined (no text-route extraction).
|
|
418
|
+
*/
|
|
419
|
+
readonly extractTextRoutes?: (filePath: string, content: string) => ExtractedRoute[];
|
|
409
420
|
/**
|
|
410
421
|
* Extract routes that a parsed file declares in its own AST.
|
|
411
422
|
*
|
|
@@ -49,7 +49,7 @@ import { getTreeSitterBufferSize } from '../../constants.js';
|
|
|
49
49
|
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
|
50
50
|
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
|
51
51
|
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
|
52
|
-
import { deriveDefaultExportHocName, isBlockedDefaultExportHoc, isDefaultExportHocFunctionNode, } from '../../ts-js-hoc-utils.js';
|
|
52
|
+
import { deriveDefaultExportHocName, isBlockedDefaultExportHoc, isBlockedPairCallbackRegistration, isDefaultExportHocFunctionNode, } from '../../ts-js-hoc-utils.js';
|
|
53
53
|
/** JS function-like node types that may carry a synthesized `this` binding.
|
|
54
54
|
* Kept in sync with the `@scope.function` patterns in `query.ts`. */
|
|
55
55
|
export const FUNCTION_NODE_TYPES = [
|
|
@@ -1019,6 +1019,13 @@ export function emitJsScopeCaptures(sourceText, filePath, cachedTree) {
|
|
|
1019
1019
|
if (arrowNode !== null && isBlockedDefaultExportHoc(arrowNode)) {
|
|
1020
1020
|
continue;
|
|
1021
1021
|
}
|
|
1022
|
+
// Pair-value built-in registrations (`{ timer: setTimeout(() => …) }`,
|
|
1023
|
+
// `{ later: promise.then(() => …) }`) bind handles/values, not
|
|
1024
|
+
// callables. See `isBlockedPairCallbackRegistration` for why this
|
|
1025
|
+
// gate lives emit-side rather than in the query predicates.
|
|
1026
|
+
if (arrowNode !== null && isBlockedPairCallbackRegistration(arrowNode)) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1022
1029
|
// #2723 — see the matching filter in `typescript/captures.ts`.
|
|
1023
1030
|
if (arrowNode !== null && isShadowedCjsExportAssignment(arrowNode, tree.rootNode)) {
|
|
1024
1031
|
continue;
|
|
@@ -234,6 +234,200 @@ export const JAVASCRIPT_SCOPE_QUERY = `
|
|
|
234
234
|
key: (string (string_fragment) @declaration.name)
|
|
235
235
|
value: (function_expression) @declaration.function)
|
|
236
236
|
|
|
237
|
+
;; HOC-wrapped pair values: create: procedure.mutation(async ({ input }) => { ... }).
|
|
238
|
+
;; tRPC, Express route definitions, and similar frameworks use this pattern where
|
|
239
|
+
;; an object property's value is a call_expression (like .mutation()/.query()) that
|
|
240
|
+
;; wraps an arrow_function or function_expression as a callback argument.
|
|
241
|
+
;; Without these patterns, tRPC procedures are invisible — the arrow registers as
|
|
242
|
+
;; anonymous and all calls inside fall back to file-level attribution.
|
|
243
|
+
;;
|
|
244
|
+
;; AST shape:
|
|
245
|
+
;; pair
|
|
246
|
+
;; key: property_identifier "create"
|
|
247
|
+
;; value: call_expression
|
|
248
|
+
;; function: identifier | member_expression
|
|
249
|
+
;; arguments: arguments
|
|
250
|
+
;; arrow_function | function_expression
|
|
251
|
+
;;
|
|
252
|
+
;; Anchor discipline: same as direct pairs — on the inner arrow/function, not
|
|
253
|
+
;; the outer call_expression. The arrow's range matches its @scope.function range,
|
|
254
|
+
;; so pass2AttachDeclarations auto-hoists the binding to the parent scope.
|
|
255
|
+
((pair
|
|
256
|
+
key: (property_identifier) @declaration.name
|
|
257
|
+
value: (call_expression
|
|
258
|
+
function: (identifier) @hoc
|
|
259
|
+
arguments: (arguments
|
|
260
|
+
(arrow_function) @declaration.function)))
|
|
261
|
+
${DEFAULT_EXPORT_IDENTIFIER_NOT_ANY_OF_PREDICATE})
|
|
262
|
+
|
|
263
|
+
((pair
|
|
264
|
+
key: (property_identifier) @declaration.name
|
|
265
|
+
value: (call_expression
|
|
266
|
+
function: (identifier) @hoc
|
|
267
|
+
arguments: (arguments
|
|
268
|
+
(function_expression) @declaration.function)))
|
|
269
|
+
${DEFAULT_EXPORT_IDENTIFIER_NOT_ANY_OF_PREDICATE})
|
|
270
|
+
|
|
271
|
+
; Member-expression variants exclude callback-taking array methods —
|
|
272
|
+
; '{ visible: items.filter(item => item.active) }' is a value holding an
|
|
273
|
+
; array, not a Function — same exclusion as the HOC variable rules below.
|
|
274
|
+
((pair
|
|
275
|
+
key: (property_identifier) @declaration.name
|
|
276
|
+
value: (call_expression
|
|
277
|
+
function: (member_expression
|
|
278
|
+
property: (property_identifier) @callee)
|
|
279
|
+
arguments: (arguments
|
|
280
|
+
(arrow_function) @declaration.function)))
|
|
281
|
+
${ARRAY_METHOD_NOT_ANY_OF_PREDICATE})
|
|
282
|
+
|
|
283
|
+
((pair
|
|
284
|
+
key: (property_identifier) @declaration.name
|
|
285
|
+
value: (call_expression
|
|
286
|
+
function: (member_expression
|
|
287
|
+
property: (property_identifier) @callee)
|
|
288
|
+
arguments: (arguments
|
|
289
|
+
(function_expression) @declaration.function)))
|
|
290
|
+
${ARRAY_METHOD_NOT_ANY_OF_PREDICATE})
|
|
291
|
+
|
|
292
|
+
;; String-key pair variants: { 'create': procedure.mutation(async () => ...) }.
|
|
293
|
+
;; Quoted object keys are identical shapes to the identifier-key pairs above —
|
|
294
|
+
;; a tRPC router that quotes its keys (lint-enforced or JSON-ish style) would
|
|
295
|
+
;; otherwise leave its procedures anonymous and file-level attributed.
|
|
296
|
+
;; Quoted-key identifier callees ({ 'handler': wrap(() => {}) }) match these
|
|
297
|
+
;; string-key rules, not the identifier-key block above; member-expression
|
|
298
|
+
;; callees keep the array-method exclusion.
|
|
299
|
+
((pair
|
|
300
|
+
key: (string (string_fragment) @declaration.name)
|
|
301
|
+
value: (call_expression
|
|
302
|
+
function: (identifier) @hoc
|
|
303
|
+
arguments: (arguments
|
|
304
|
+
(arrow_function) @declaration.function)))
|
|
305
|
+
${DEFAULT_EXPORT_IDENTIFIER_NOT_ANY_OF_PREDICATE})
|
|
306
|
+
|
|
307
|
+
((pair
|
|
308
|
+
key: (string (string_fragment) @declaration.name)
|
|
309
|
+
value: (call_expression
|
|
310
|
+
function: (identifier) @hoc
|
|
311
|
+
arguments: (arguments
|
|
312
|
+
(function_expression) @declaration.function)))
|
|
313
|
+
${DEFAULT_EXPORT_IDENTIFIER_NOT_ANY_OF_PREDICATE})
|
|
314
|
+
|
|
315
|
+
((pair
|
|
316
|
+
key: (string (string_fragment) @declaration.name)
|
|
317
|
+
value: (call_expression
|
|
318
|
+
function: (member_expression
|
|
319
|
+
property: (property_identifier) @callee)
|
|
320
|
+
arguments: (arguments
|
|
321
|
+
(arrow_function) @declaration.function)))
|
|
322
|
+
${ARRAY_METHOD_NOT_ANY_OF_PREDICATE})
|
|
323
|
+
|
|
324
|
+
((pair
|
|
325
|
+
key: (string (string_fragment) @declaration.name)
|
|
326
|
+
value: (call_expression
|
|
327
|
+
function: (member_expression
|
|
328
|
+
property: (property_identifier) @callee)
|
|
329
|
+
arguments: (arguments
|
|
330
|
+
(function_expression) @declaration.function)))
|
|
331
|
+
${ARRAY_METHOD_NOT_ANY_OF_PREDICATE})
|
|
332
|
+
|
|
333
|
+
;; Curried pair HOC: create: publicProcedure.mutation(withAuth(async () => {})).
|
|
334
|
+
;; Existing pair-HOC rules require the arrow to be a DIRECT argument of the
|
|
335
|
+
;; pair's call_expression. They miss mutation's argument being another
|
|
336
|
+
;; call_expression (withAuth(...)). Object-pair only — do NOT add a
|
|
337
|
+
;; variable-level nested-HOC rule (\`const X = memo(forwardRef(...))\` must
|
|
338
|
+
;; stay a Variable; see typescript-hoc-wrapped.test.ts).
|
|
339
|
+
((pair
|
|
340
|
+
key: (property_identifier) @declaration.name
|
|
341
|
+
value: (call_expression
|
|
342
|
+
function: (identifier) @hoc
|
|
343
|
+
arguments: (arguments
|
|
344
|
+
(call_expression
|
|
345
|
+
function: (identifier)
|
|
346
|
+
arguments: (arguments
|
|
347
|
+
(arrow_function) @declaration.function)))))
|
|
348
|
+
${DEFAULT_EXPORT_IDENTIFIER_NOT_ANY_OF_PREDICATE})
|
|
349
|
+
|
|
350
|
+
((pair
|
|
351
|
+
key: (property_identifier) @declaration.name
|
|
352
|
+
value: (call_expression
|
|
353
|
+
function: (identifier) @hoc
|
|
354
|
+
arguments: (arguments
|
|
355
|
+
(call_expression
|
|
356
|
+
function: (identifier)
|
|
357
|
+
arguments: (arguments
|
|
358
|
+
(function_expression) @declaration.function)))))
|
|
359
|
+
${DEFAULT_EXPORT_IDENTIFIER_NOT_ANY_OF_PREDICATE})
|
|
360
|
+
|
|
361
|
+
((pair
|
|
362
|
+
key: (property_identifier) @declaration.name
|
|
363
|
+
value: (call_expression
|
|
364
|
+
function: (member_expression
|
|
365
|
+
property: (property_identifier) @callee)
|
|
366
|
+
arguments: (arguments
|
|
367
|
+
(call_expression
|
|
368
|
+
function: (identifier)
|
|
369
|
+
arguments: (arguments
|
|
370
|
+
(arrow_function) @declaration.function)))))
|
|
371
|
+
${ARRAY_METHOD_NOT_ANY_OF_PREDICATE})
|
|
372
|
+
|
|
373
|
+
((pair
|
|
374
|
+
key: (property_identifier) @declaration.name
|
|
375
|
+
value: (call_expression
|
|
376
|
+
function: (member_expression
|
|
377
|
+
property: (property_identifier) @callee)
|
|
378
|
+
arguments: (arguments
|
|
379
|
+
(call_expression
|
|
380
|
+
function: (identifier)
|
|
381
|
+
arguments: (arguments
|
|
382
|
+
(function_expression) @declaration.function)))))
|
|
383
|
+
${ARRAY_METHOD_NOT_ANY_OF_PREDICATE})
|
|
384
|
+
|
|
385
|
+
((pair
|
|
386
|
+
key: (string (string_fragment) @declaration.name)
|
|
387
|
+
value: (call_expression
|
|
388
|
+
function: (identifier) @hoc
|
|
389
|
+
arguments: (arguments
|
|
390
|
+
(call_expression
|
|
391
|
+
function: (identifier)
|
|
392
|
+
arguments: (arguments
|
|
393
|
+
(arrow_function) @declaration.function)))))
|
|
394
|
+
${DEFAULT_EXPORT_IDENTIFIER_NOT_ANY_OF_PREDICATE})
|
|
395
|
+
|
|
396
|
+
((pair
|
|
397
|
+
key: (string (string_fragment) @declaration.name)
|
|
398
|
+
value: (call_expression
|
|
399
|
+
function: (identifier) @hoc
|
|
400
|
+
arguments: (arguments
|
|
401
|
+
(call_expression
|
|
402
|
+
function: (identifier)
|
|
403
|
+
arguments: (arguments
|
|
404
|
+
(function_expression) @declaration.function)))))
|
|
405
|
+
${DEFAULT_EXPORT_IDENTIFIER_NOT_ANY_OF_PREDICATE})
|
|
406
|
+
|
|
407
|
+
((pair
|
|
408
|
+
key: (string (string_fragment) @declaration.name)
|
|
409
|
+
value: (call_expression
|
|
410
|
+
function: (member_expression
|
|
411
|
+
property: (property_identifier) @callee)
|
|
412
|
+
arguments: (arguments
|
|
413
|
+
(call_expression
|
|
414
|
+
function: (identifier)
|
|
415
|
+
arguments: (arguments
|
|
416
|
+
(arrow_function) @declaration.function)))))
|
|
417
|
+
${ARRAY_METHOD_NOT_ANY_OF_PREDICATE})
|
|
418
|
+
|
|
419
|
+
((pair
|
|
420
|
+
key: (string (string_fragment) @declaration.name)
|
|
421
|
+
value: (call_expression
|
|
422
|
+
function: (member_expression
|
|
423
|
+
property: (property_identifier) @callee)
|
|
424
|
+
arguments: (arguments
|
|
425
|
+
(call_expression
|
|
426
|
+
function: (identifier)
|
|
427
|
+
arguments: (arguments
|
|
428
|
+
(function_expression) @declaration.function)))))
|
|
429
|
+
${ARRAY_METHOD_NOT_ANY_OF_PREDICATE})
|
|
430
|
+
|
|
237
431
|
;; HOC-wrapped variable declarations: const X = HOC((args) => { ... }).
|
|
238
432
|
;; Covers React.forwardRef, memo, useCallback, useMemo, observer,
|
|
239
433
|
;; debounce, and any user-defined HOC factory.
|
|
@@ -39,7 +39,7 @@ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
|
|
39
39
|
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
|
40
40
|
import { synthesizeCjsModuleExports } from './cjs-module-exports.js';
|
|
41
41
|
import { synthesizeReceiverChainCapture } from '../../utils/receiver-chain-captures.js';
|
|
42
|
-
import { deriveDefaultExportHocName, isBlockedDefaultExportHoc, isDefaultExportHocFunctionNode, } from '../../ts-js-hoc-utils.js';
|
|
42
|
+
import { deriveDefaultExportHocName, isBlockedDefaultExportHoc, isBlockedPairCallbackRegistration, isDefaultExportHocFunctionNode, } from '../../ts-js-hoc-utils.js';
|
|
43
43
|
/** tree-sitter-typescript node types for function-like scopes that may
|
|
44
44
|
* carry a synthesized `this` binding. Kept in sync with the
|
|
45
45
|
* `@scope.function` patterns in `query.ts`. */
|
|
@@ -477,6 +477,13 @@ export function emitTsScopeCaptures(sourceText, filePath, cachedTree) {
|
|
|
477
477
|
if (arrowNode !== null && isBlockedDefaultExportHoc(arrowNode)) {
|
|
478
478
|
continue;
|
|
479
479
|
}
|
|
480
|
+
// Pair-value built-in registrations (`{ timer: setTimeout(() => …) }`,
|
|
481
|
+
// `{ later: promise.then(() => …) }`) bind handles/values, not
|
|
482
|
+
// callables. See `isBlockedPairCallbackRegistration` for why this
|
|
483
|
+
// gate lives emit-side rather than in the query predicates.
|
|
484
|
+
if (arrowNode !== null && isBlockedPairCallbackRegistration(arrowNode)) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
480
487
|
// #2723: a CJS export assignment must not register a SECOND module-scope
|
|
481
488
|
// declaration for a name the file already declares lexically — the name
|
|
482
489
|
// would become ambiguous and the resolver would drop the intra-module
|