* Flow-from-named-symbols: an agent's codegraph_explore query is a bag of * symbol names that usually spans the flow it's investigating (e.g. * "PmsProductController getList PmsProductService list PmsProductServiceImpl"). * Surface the longest call chain AMONG those named symbols — scoped t
(cg: CodeGraph, query: string)
| 1845 | * dropping unrelated `OmsOrderService::list`. |
| 1846 | */ |
| 1847 | private buildFlowFromNamedSymbols(cg: CodeGraph, query: string): { text: string; pathNodeIds: Set<string>; namedNodeIds: Set<string>; uniqueNamedNodeIds: Set<string>; spineCallSites: Map<string, number> } { |
| 1848 | // spineCallSites: for each spine node, the line where it CALLS the next hop — |
| 1849 | // lets the source assembler window an oversize spine method (e.g. n8n's 962-line |
| 1850 | // processRunExecutionData) to the call site instead of dumping the whole body. |
| 1851 | const EMPTY = { text: '', pathNodeIds: new Set<string>(), namedNodeIds: new Set<string>(), uniqueNamedNodeIds: new Set<string>(), spineCallSites: new Map<string, number>() }; |
| 1852 | try { |
| 1853 | const CALLABLE = new Set(['method', 'function', 'component', 'constructor']); |
| 1854 | // Strip only a REAL file extension (Create.cs → Create); KEEP qualified |
| 1855 | // names (Class.method / Class::method) — the agent's most precise input, |
| 1856 | // resolved exactly by findAllSymbols. (The old strip mangled Class.method |
| 1857 | // into Class, throwing the method away.) |
| 1858 | const FILE_EXT = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte|astro)$/i; |
| 1859 | const tokens = [...new Set( |
| 1860 | query.split(/[\s,()[\]]+/) |
| 1861 | .map((t) => t.replace(FILE_EXT, '').trim()) |
| 1862 | .filter((t) => t.length >= 3 && /^[A-Za-z_$][\w$]*(?:(?:::|\.)[\w$]+)*$/.test(t)) |
| 1863 | )].slice(0, 16); |
| 1864 | if (tokens.length < 2) return EMPTY; |
| 1865 | // Pool of name SEGMENTS (Class + method from every token) used to |
| 1866 | // disambiguate an ambiguous SIMPLE name: keep a candidate only if its |
| 1867 | // CONTAINER class is itself named in the query. |
| 1868 | const segPool = new Set<string>(); |
| 1869 | for (const t of tokens) for (const s of t.toLowerCase().split(/::|\./)) if (s) segPool.add(s); |
| 1870 | const named = new Map<string, Node>(); |
| 1871 | // Nodes whose token is SPECIFIC — a (near-)unique callable name (<=3 defs in |
| 1872 | // the whole graph). These are safe to SPARE a file on: the agent named THIS |
| 1873 | // method (`getResponseWithInterceptorChain`, 1 def). A hyper-polymorphic name |
| 1874 | // (`as_sql`, 110 defs across every Expression/Compiler subclass) is NOT here, |
| 1875 | // so naming it doesn't keep every backend variant full and flood the budget. |
| 1876 | const uniqueNamedNodeIds = new Set<string>(); |
| 1877 | // token → resolved node ids: drives the token-coverage check that gates |
| 1878 | // the dynamic-boundary scan (a token is covered when ANY of its nodes |
| 1879 | // lands on the main chain — overloads off the chain don't count against). |
| 1880 | const tokenNodes = new Map<string, string[]>(); |
| 1881 | // token → its full same-name callable family (before the container filter). |
| 1882 | // A LARGE family that fails to connect on the chain is a polymorphic |
| 1883 | // interface/registry dispatch — surfaced by buildPolymorphicBoundaries below. |
| 1884 | const tokenFamily = new Map<string, Node[]>(); |
| 1885 | // Non-callable endpoints (CONSTANT/VARIABLE/FIELD) connected by a SYNTHESIZED |
| 1886 | // edge. RTK thunks are `const X = createAsyncThunk(...)`, so a thunk→thunk hop |
| 1887 | // is constant→constant — the CALLABLE-only `named` set can't hold it, and |
| 1888 | // without this the hop is invisible to the Flow path at every tier (the |
| 1889 | // Relationships section catches it only on repos ≥500 files). Kept SEPARATE |
| 1890 | // from `named` (which drives the call-chain + source sizing, callable-only); |
| 1891 | // fed only to the dynamic-dispatch-links scan below. |
| 1892 | const dynNamed = new Map<string, Node>(); |
| 1893 | const DYN_KINDS = new Set(['constant', 'variable', 'field', 'property']); |
| 1894 | const hasHeuristicEdge = (id: string): boolean => |
| 1895 | [...cg.getCallers(id), ...cg.getCallees(id)].some(({ edge }) => edge.provenance === 'heuristic'); |
| 1896 | for (const t of tokens) { |
| 1897 | const hits = this.findAllSymbols(cg, t).nodes; |
| 1898 | const cands = hits.filter((n) => CALLABLE.has(n.kind)); |
| 1899 | tokenFamily.set(t, cands); |
| 1900 | // A qualified or otherwise-specific name (<=3 hits) keeps all; an |
| 1901 | // ambiguous simple name keeps only candidates whose container is named. |
| 1902 | const specific = cands.length <= 3; |
| 1903 | const pick = specific |
| 1904 | ? cands |
no test coverage detected