* Build a URL → handler manifest from the index. Each route node's * `references` edge points at the function/method that handles the * request. We join them in one pass; the agent gets the canonical * routing answer ("POST /users/login → AuthController#login") without * having to parse
(limit: number = 40)
| 976 | * mapping AND the handler implementations. |
| 977 | */ |
| 978 | getRoutingManifest(limit: number = 40): { |
| 979 | entries: Array<{ url: string; handler: string; handlerFile: string; handlerLine: number; handlerKind: string }>; |
| 980 | topHandlerFile: string | null; |
| 981 | topHandlerFileCount: number; |
| 982 | totalRoutes: number; |
| 983 | } | null { |
| 984 | if (!this.stmts.getRoutingManifest) { |
| 985 | // Edge kind varies across framework resolvers: Spring/Rails/ |
| 986 | // Laravel/Drupal emit `references`, Express emits `calls`. Accept |
| 987 | // both — the semantic is the same (route → its handler). |
| 988 | this.stmts.getRoutingManifest = this.db.prepare(` |
| 989 | SELECT |
| 990 | r.name AS url, |
| 991 | h.name AS handler, |
| 992 | h.file_path AS handler_file, |
| 993 | h.start_line AS handler_line, |
| 994 | h.kind AS handler_kind |
| 995 | FROM nodes r |
| 996 | JOIN edges e ON e.source = r.id |
| 997 | JOIN nodes h ON e.target = h.id |
| 998 | WHERE r.kind = 'route' |
| 999 | AND e.kind IN ('references', 'calls') |
| 1000 | AND h.kind IN ('function', 'method', 'class') |
| 1001 | ORDER BY r.file_path, r.start_line |
| 1002 | LIMIT ? |
| 1003 | `); |
| 1004 | } |
| 1005 | const rows = this.stmts.getRoutingManifest.all(limit) as Array<{ |
| 1006 | url: string; handler: string; handler_file: string; handler_line: number; handler_kind: string; |
| 1007 | }>; |
| 1008 | // Drop test/generated handlers — same hygiene as elsewhere. |
| 1009 | const filtered = rows.filter(r => !isLowValueFile(r.handler_file)); |
| 1010 | if (filtered.length < 3) return null; |
| 1011 | // Identify the file holding the most handlers (the "primary handler file"). |
| 1012 | const fileCounts = new Map<string, number>(); |
| 1013 | for (const r of filtered) { |
| 1014 | fileCounts.set(r.handler_file, (fileCounts.get(r.handler_file) ?? 0) + 1); |
| 1015 | } |
| 1016 | let topHandlerFile: string | null = null; |
| 1017 | let topHandlerFileCount = 0; |
| 1018 | for (const [file, count] of fileCounts) { |
| 1019 | if (count > topHandlerFileCount) { |
| 1020 | topHandlerFile = file; |
| 1021 | topHandlerFileCount = count; |
| 1022 | } |
| 1023 | } |
| 1024 | return { |
| 1025 | entries: filtered.map(r => ({ |
| 1026 | url: r.url, |
| 1027 | handler: r.handler, |
| 1028 | handlerFile: r.handler_file, |
| 1029 | handlerLine: r.handler_line, |
| 1030 | handlerKind: r.handler_kind, |
| 1031 | })), |
| 1032 | topHandlerFile, |
| 1033 | topHandlerFileCount, |
| 1034 | totalRoutes: filtered.length, |
| 1035 | }; |
nothing calls this directly
no test coverage detected