Skip to content

find_references

Who calls or references a symbol. Type-checked, not name-matched: grep cannot separate homonyms, and an empty answer means nobody calls it. A bare name suffices: an ambiguous one returns its candidates, so no lookup call first. view: "files" answers which files without a line each.

ArgumentTypeDefaultMeaning
confidencestringnoneKeeps only edges carrying exactly this confidence. Accepted: EXACT_TYPECHECKED, EXACT_DECLARATION_MAPPED, EXACT_PACKAGE_MAPPED, STRUCTURAL_CERTAIN, CANDIDATE, UNRESOLVED. One value, not a list. Anything else is rejected with INVALID_ARGUMENT.
cursorstringnoneOpaque token taken from next_cursor. Resumes the same query at the next offset.
directionstringincomingincoming returns the symbols that reference this one. outgoing returns the ones it reaches. Any other value is rejected with INVALID_ARGUMENT.
edge_kindsarray of stringnone, meaning every reference kind except EXPORTS and REEXPORTSRestricts rows to these relations. The default leaves out the forwarding bindings: an export { x } or export { x } from "./y" names a path to the declaration rather than a use of it, and the response says so under edge_kinds_default_excluded. Nothing becomes unreachable — the checker resolves an import through however many barrels stand in the way, so every consumer behind one carries its own IMPORTS_SYMBOL edge and is listed without them. A list replaces that default; ["*"] reports every reference kind, and the wildcard stands alone since mixing it with names asks for a filter and for no filter at once. Accepted: IMPORTS_SYMBOL, EXPORTS, REEXPORTS, REFERENCES, CALLS_DIRECT, PASSES_AS_CALLBACK, ASSIGNS_FUNCTION, RETURNS_FUNCTION, TYPE_USES, IMPLEMENTS, EXTENDS, EMBEDS, OVERRIDES. Containment and package kinds are rejected. Duplicates collapse; an empty or space-padded entry is rejected.
include_derivedbooleanfalseIncludes rows from providers Kivgraph derives from the machine, which take the rust: namespace, such as a Rust toolchain’s standard library. Naming one of them in repo has the same effect.
languagestringnoneKeeps rows whose symbol carries this language: go, typescript or rust. Compared exactly; surrounding whitespace is rejected.
limitinteger50Rows in this page. Must be between 1 and 500.
namestringnoneThe unqualified name of the declaration to ask about, so the common question costs one call instead of two. One declaration carrying that name is answered directly; several are rejected with AMBIGUOUS_SYMBOL naming the candidates as repository:path:line. Cannot be combined with stable_key or qualified_name; repository, or repository plus path, narrows it. A name that is only imported or re-exported and never declared is SYMBOL_NOT_FOUND, because the declaration is what a reference question is about.
pathstringnoneRepository-relative path narrowing qualified_name. Requires repository. Rejected together with stable_key.
qualified_namestringnoneNames the symbol to ask about. One of qualified_name, stable_key or name is required, never two of them. A name matching more than one symbol is rejected with AMBIGUOUS_SYMBOL instead of being resolved silently.
repostringnoneKeeps only rows belonging to this repository, by name, compared exactly. It filters the answer; repository selects the subject.
repositorystringnoneRepository of the symbol being asked about. Narrows qualified_name.
response_formatstringconciseconcise or detailed. Detailed adds the derived identifiers — stable_key, file_key, repository_key and evidence_kind — to the rows of the full view. The compact and files views carry none of them under either format: a row is addressed by the header’s repository plus its own path and qualified name. Anything else is rejected with INVALID_ARGUMENT.
stable_keystringnoneNames the symbol directly. Cannot be combined with qualified_name, name, repository or path.
viewstringcompactThe granularity of the answer, never a different answer. compact lifts into a header what every row shares and groups the rows by file. full is the field-per-row shape. files answers only which files hold references and how many each holds, and raises the default limit to 500 so a file list is never cut by a page. Any other value is rejected with INVALID_ARGUMENT.

Who calls or references one symbol, and what that symbol reaches directly. It is a single hop, not a walk: for a chain use trace_dependencies or get_blast_radius. The response states the subject once, echoes the direction it answered in, and returns one page of the matching edges. Each row names a symbol with its repository, repository-relative path, qualified name and line range, so the next call is built from the answer just received.

By default the answer arrives in the compact view: what every row shares is stated once in the header and the rows are grouped by file. It is the same edges, with the same confidence and the same provenance, as the full view — confidence and provenance alone were 1.200 of the 4.236 tokens of one fifty-row page over the private benchmark corpus, the same pair on all fifty rows. Measured over that corpus, the page went from 4.236 to 874 tokens.

That page-wide hoist is unanimous or nothing: on a real 66-row page, 65 rows shared kind and edge_kind and the 66th, a re-export, was enough to push both columns back down to every row. results.groups is the second tier that catches it, grouping the rows by whatever exact tuple of the remaining columns they still share instead of files repeating it row by row. Measured over the same two questions of that corpus, 1.205 and 1.143 tokens fell to 788 and 779. See when a page groups for the mechanism shared by six tools, and reading a grouped page below for a captured page.

Incoming references, three at a time. Nothing asks for a view, so this is the compact answer a caller gets by default:

{
"repository": "kivgraph",
"path": "internal/facts/facts.go",
"qualified_name": "MergeAll",
"direction": "incoming",
"limit": 3
}
{
"snapshot_id": 30,
"total": 3,
"returned": 3,
"coverage": {
"exact": 3
},
"results": {
"subject": "kivgraph:internal/facts/facts.go:516",
"qn": "MergeAll",
"direction": "incoming",
"repository": "kivgraph",
"edge_kind": "CALLS_DIRECT",
"confidence": "EXACT_TYPECHECKED",
"provenance": "GO_AST_CALL",
"files": [
{
"file": "internal/indexer/full.go",
"at": [
["mergeSets@681-712", "func"],
["closeCrossRepositoryEdges@735-783", "func"]
]
},
{
"file": "internal/facts/facts.go",
"at": [["Set.Merge@505-507", "method"]]
}
]
}
}

All three rows are calls a type checker resolved, so edge_kind, confidence and provenance are in the header rather than on each row. kind is not: two of the callers are functions and one is a method, so each row carries its own.

The same question, the same three edges, in the full view — the field-per-row shape, which is what a client written against the previous output should ask for explicitly:

{
"repository": "kivgraph",
"path": "internal/facts/facts.go",
"qualified_name": "MergeAll",
"direction": "incoming",
"limit": 3,
"view": "full"
}
{
"snapshot_id": 30,
"snapshot_age_ms": 9020,
"total": 3,
"returned": 3,
"truncated": false,
"next_cursor": null,
"coverage": {
"exact": 3,
"candidate": 0,
"unresolved_related": 0,
"package_level": 0
},
"results": {
"subject": {
"stable_key": "KHXAWFM5ED2YEIFIEMB5NALA7L7YXNHSUJEBLU7SDAMLGKX24UAA",
"name": "MergeAll",
"qualified_name": "MergeAll",
"kind": "func",
"repository": "kivgraph",
"file_path": "internal/facts/facts.go",
"start_line": 516
},
"direction": "incoming",
"references": [
{
"name": "mergeSets",
"qualified_name": "mergeSets",
"kind": "func",
"repository": "kivgraph",
"file_path": "internal/indexer/full.go",
"start_line": 681,
"end_line": 712,
"language": "go",
"edge_kind": "CALLS_DIRECT",
"confidence": "EXACT_TYPECHECKED",
"provenance": "GO_AST_CALL"
},
{
"name": "closeCrossRepositoryEdges",
"qualified_name": "closeCrossRepositoryEdges",
"kind": "func",
"repository": "kivgraph",
"file_path": "internal/indexer/full.go",
"start_line": 735,
"end_line": 783,
"language": "go",
"edge_kind": "CALLS_DIRECT",
"confidence": "EXACT_TYPECHECKED",
"provenance": "GO_AST_CALL"
},
{
"name": "Merge",
"qualified_name": "Set.Merge",
"kind": "method",
"repository": "kivgraph",
"file_path": "internal/facts/facts.go",
"start_line": 505,
"end_line": 507,
"language": "go",
"edge_kind": "CALLS_DIRECT",
"confidence": "EXACT_TYPECHECKED",
"provenance": "GO_AST_CALL"
}
]
}
}

The same symbol, the other direction, with a page smaller than the answer:

{
"repository": "kivgraph",
"path": "internal/facts/facts.go",
"qualified_name": "MergeAll",
"direction": "outgoing",
"limit": 2
}
{
"snapshot_id": 30,
"total": 66,
"returned": 2,
"truncated": true,
"next_cursor": "Ah4CAHjbgtIy3kG-GzdYiDefgr2UKDg",
"coverage": {
"exact": 66
},
"guidance": "showing 2 of 66; narrow with edge_kinds, confidence, repo or language, or pass the cursor for the next page",
"results": {
"subject": "kivgraph:internal/facts/facts.go:516",
"qn": "MergeAll",
"direction": "outgoing",
"repository": "kivgraph",
"kind": "field",
"edge_kind": "REFERENCES",
"confidence": "EXACT_TYPECHECKED",
"files": [
{
"file": "internal/facts/facts.go",
"at": [
["Set.Symbols@251", "GO_TYPES_USE"],
["Set.Symbols@251", "GO_TYPES_SELECTION"]
]
}
]
}
}

Here provenance stayed on the rows. The two entries are the same symbol on the same line under the same relation, observed twice by two different mechanisms, so no single value covered the page and each row carries its own as the tail of an array. truncated and next_cursor are present because rows remain; on the complete page above, both were absent rather than false and null.

The same page in the full view:

{
"repository": "kivgraph",
"path": "internal/facts/facts.go",
"qualified_name": "MergeAll",
"direction": "outgoing",
"limit": 2,
"view": "full"
}
{
"snapshot_id": 30,
"snapshot_age_ms": 21769,
"total": 66,
"returned": 2,
"truncated": true,
"next_cursor": "Ah4CAHjbgtIy3kG-GzdYiDefgr2UKDg",
"coverage": {
"exact": 66,
"candidate": 0,
"unresolved_related": 0,
"package_level": 0
},
"guidance": "showing 2 of 66; narrow with edge_kinds, confidence, repo or language, or pass the cursor for the next page",
"results": {
"subject": {
"stable_key": "KHXAWFM5ED2YEIFIEMB5NALA7L7YXNHSUJEBLU7SDAMLGKX24UAA",
"name": "MergeAll",
"qualified_name": "MergeAll",
"kind": "func",
"repository": "kivgraph",
"file_path": "internal/facts/facts.go",
"start_line": 516
},
"direction": "outgoing",
"references": [
{
"name": "Symbols",
"qualified_name": "Set.Symbols",
"kind": "field",
"repository": "kivgraph",
"file_path": "internal/facts/facts.go",
"start_line": 251,
"end_line": 251,
"language": "go",
"edge_kind": "REFERENCES",
"confidence": "EXACT_TYPECHECKED",
"provenance": "GO_TYPES_USE"
},
{
"name": "Symbols",
"qualified_name": "Set.Symbols",
"kind": "field",
"repository": "kivgraph",
"file_path": "internal/facts/facts.go",
"start_line": 251,
"end_line": 251,
"language": "go",
"edge_kind": "REFERENCES",
"confidence": "EXACT_TYPECHECKED",
"provenance": "GO_TYPES_SELECTION"
}
]
}
}

When the question is which files to open, the rows are noise: seven calls in one file are one file. view: "files" answers with the file list and a count each, and it pages at 500 by default so the list is whole:

{
"repository": "kivgraph",
"path": "internal/facts/facts.go",
"qualified_name": "MergeAll",
"direction": "incoming",
"view": "files"
}
{
"snapshot_id": 30,
"total": 3,
"returned": 3,
"coverage": {
"exact": 3
},
"results": {
"subject": "kivgraph:internal/facts/facts.go:516",
"qn": "MergeAll",
"direction": "incoming",
"files": [
{ "file": "kivgraph/internal/indexer/full.go", "count": 2 },
{ "file": "kivgraph/internal/facts/facts.go", "count": 1 }
]
}
}

A file here is prefixed with its repository, because a file list spanning two repositories has no header to hoist the repository into. Over the four reference questions of benchmarks/codebase-memory-comparison, this view answered in 963 tokens against the 2.883 of the compact rows and the 13.594 of the previous shape.

name resolves the declaration itself, so a reference question does not have to be preceded by a find_symbol call:

{
"name": "MergeAll",
"direction": "incoming"
}

That answers exactly the page above, because one symbol in the graph declares MergeAll. When several do, the tool refuses to pick and says which ones it refused between:

AMBIGUOUS_SYMBOL: name "withBackoff" declares 7 symbols; repeat with the repository and path of the one you mean: workspace:apps/api/src/retry.ts:14, workspace:apps/web/src/lib/retry.ts:22, …

The message lists all seven; the two above are the shape, the rest are cut here with the . Those candidates are the same repository:path:line triple every tool accepts, so narrowing is a matter of copying one of them into repository and path. The refusal costs 49 to 144 tokens over the benchmark corpus, against the 2.293 of listing every find_symbol row for the name, imports and re-exports included. A name the graph knows only as an import or a re-export is SYMBOL_NOT_FOUND with the same instruction: name the repository and path that declares it.

All the answers above come from snapshot 30 of two repositories, kivgraph and mole. The refusal is not one of them: it comes from the private benchmark corpus, so its repository, path and symbol names are substituted. The line numbers, the count of seven declarations and the token figures are the measured ones, and the 49 to 144 range was measured against the real identifiers, whose length the message carries.

subject is the symbol the query resolved to, stated once rather than on every row, and it is worth checking: it is the proof that the selector picked the symbol you meant. The compact view spells it as the repository:path:line triple in one string, with the resolved qualified name beside it in qn; the full view spells the same facts as an object of fields. direction echoes which question was answered, so a cached response cannot be misread later.

dispatch_through is present when the subject is the one implementation of an interface method and the answer therefore also holds the references to it: a call through the interface can reach nothing else, so leaving them out answered that nothing called the implementation. Every row that arrived that way repeats the interface method in via, which hoists and groups like the other columns, so a bridged row is never read as a direct call. With two implementations nothing is bridged — a call reaches one of them, and naming both would trade a false absence for a false presence — and get_blast_radius crosses IMPLEMENTS in either direction regardless.

edge_kinds_default_excluded is present only when a filter you did not ask for ran. Its value, ["EXPORTS", "REEXPORTS"], is what the answer left out: the export bindings that forward the name rather than use it. It is on the page and not on a row because it describes the query, and total counts what the answer holds, so a filtered page never reports a larger number than it can show. Pass edge_kinds: ["*"] to turn the filter off, or name REEXPORTS to ask only for the barrels — which is the question a rename has, since a rename must edit them.

The header holds what every row of the page shares: any of repository, kind, edge_kind, confidence and provenance. A field missing from the header is not a field nobody knows — it means the rows disagreed and each carries its own. confidence and provenance are therefore always readable in one of the two places, never in neither.

files groups the rows by the file holding them. Inside a group, an entry of at is one of two shapes:

  • qualified_name@start, or qualified_name@start-end when the declaration spans more than one line. Nothing was left over: the header answers the rest.
  • An array whose first element is that same label, when the row had to carry a column the header could not hoist. The remaining elements are appended in a fixed order — edge_kind, kind, confidence, provenance — skipping every column the header already states. That is why ["mergeSets@681-712", "func"] above is a kind and ["Set.Symbols@251", "GO_TYPES_USE"] is a provenance: the header says which columns are still on the rows, and the order says which is which.

A group carries a repo of its own only when its file is not in the header’s repository, which on a single-repository answer never happens.

The triple for the next call is assembled from both levels: repository from the header (or the group’s repo when it has one), path from the group’s file, and the qualified name and line from the label. That is the same repository + path + qualified_name every other tool accepts, so no row needs a stable key to be followed.

Two things a compact row never carries. language is gone, because the file extension beside it says the same thing. name is gone whenever the qualified name already ends with it, which for a Go or TypeScript declaration it does.

Each row carries three facts describing the relationship and not the symbol — in the header when the whole page agrees on them, on the row when it does not:

  • edge_kind is the relation. CALLS_DIRECT above is a call site; REFERENCES is a use that is not a call. The full accepted vocabulary is the edge_kinds list in the table above.
  • confidence is how well the relation is proven. EXACT_TYPECHECKED means a type checker resolved it. CANDIDATE is plausible and not proven, and UNRESOLVED carries no target identity at all; they are distinct results from an exact edge and are never promoted into one.
  • provenance is the mechanism that produced it. GO_AST_CALL is a call expression over a go/types resolution; GO_TYPES_USE and GO_TYPES_SELECTION are two different observations of a use. Two rows for the same symbol pair with different provenance, as in the outgoing example above, are two observed facts, not a duplicate.

The line range — start_line and end_line in the full view, the @start-end of a compact label — bounds the declaration of the symbol holding the reference, never the position of the token. The snapshot records which symbol contains a reference and not where inside it, and publishing a line nobody observed would be inventing evidence. The range is there so the row can be opened with get_source without a second lookup. A label with a single line, Set.Symbols@251, is a declaration that starts and ends there, not a range somebody dropped.

coverage classifies every matching edge, including the ones this page did not return: exact counts edges whose confidence is exact, candidate the plausible ones, unresolved_related the related references that carry no target identity. package_level counts facts about a package rather than about the symbol asked for; this tool produces none. In the full view it stays 0; in the compact view a category that counted nothing is absent, and coverage as a whole is absent when all four are, because four zeros only say that the tool has four counters. Only find_cross_repo_consumers fills it, because a package dependency proves a dependency on the provider and never a use of the symbol.

completeness states how far the answer reaches, and it rides on every response, in either view: a verdict of COMPLETE means nothing the index recorded could add to this page, LOWER_BOUND that the page is a floor. What can bound a reference answer is what the index could not read that asks for this name — the references it recorded and could not resolve for the subject’s name, in blind_spots, and the scopes of the subject’s repository it could not read at all, in invisible_scopes — with fallback carrying the pattern and the paths that close the gap. The distinction is load-bearing on an empty page: COMPLETE is the absence, LOWER_BOUND is a minimum, guidance says which of the two you got, and a reader who takes “nobody references this” from a LOWER_BOUND page can delete live code. See the completeness verdict for the semantics the six checking tools share.

An empty answer — an empty files list in the compact view, an empty references list in the full one — with total at 0 and a COMPLETE verdict is a proven absence, not a miss. The edges came from go/types, the TypeScript checker and rust-analyzer, not from matching names, so nothing referencing the symbol was left out by a spelling. Grep cannot make that claim. The response says so itself, in guidance.

A homonym with declarations of two kinds in two repositories, so no single kind covers the page:

{
"name": "NewRegistry",
"repository": "kivgraph",
"path": "internal/workspace/registry.go",
"direction": "incoming",
"limit": 100
}
{
"snapshot_id": 1,
"total": 6,
"returned": 6,
"coverage": { "exact": 6 },
"results": {
"subject": "kivgraph:internal/workspace/registry.go:68",
"qn": "NewRegistry",
"direction": "incoming",
"repository": "kivgraph",
"edge_kind": "CALLS_DIRECT",
"confidence": "EXACT_TYPECHECKED",
"provenance": "GO_AST_CALL",
"groups": [
{
"kind": "method",
"files": [
{ "file": "internal/indexing/service.go", "at": ["Service.IndexProjects@140-232", "Service.Reindex@241-277"] }
]
},
{
"kind": "func",
"files": [
{ "file": "cmd/kivgraph/main.go", "at": ["runDoctor@1164-1289", "resyncOnBranchChange@432-490", "runUpgrade@973-1050", "runIndexFull@786-903"] }
]
}
]
}
}

All six calls are the same relation with the same confidence from the same mechanism, so edge_kind, confidence and provenance still hoist to the page header exactly as on a flat answer. kind is what breaks the unanimous vote — two methods against four functions — so results.groups replaces results.files, and each group states its own kind once instead of repeating it on six rows. Inside a group, files reads exactly like the flat view: a file and a label per declaration, becoming an array only if a row still disagrees with its own group. Neither total, returned nor coverage moved for grouping; six rows are six rows either way.

truncated is true when rows remain after this page, and next_cursor then carries the token to continue. When the page holds everything the full view says truncated: false and next_cursor: null, and the compact view omits both fields: a false flag and a cursor that does not exist are not facts worth a line. This tool walks no graph, so it has no traversal_truncated field; that one belongs to trace_dependencies and get_blast_radius, where the bound is on the walk rather than on the page.

The cursor is an opaque base64url token over a binary body: the format version, the snapshot id, the offset, a digest of the query identity, a digest of the sorting contract references-v1 and a checksum over all of it. It is about 31 characters — Ah4CAHjbgtIy3kG-GzdYiDefgr2UKDg above — where the previous format spent 314 characters of base64-wrapped JSON, 221 tokens on every truncated page. Pass it back unchanged. It fails closed:

What changedWhat you get
A new generation was publishedCURSOR_SNAPSHOT_EXPIRED; restart pagination
The selector, direction, repo, language, edge_kinds or confidence differ from the call that produced itCURSOR_INVALID
The token was edited, re-encoded, truncated, or decodes with trailing bytes after the checksumCURSOR_INVALID
The token was minted by a server of the previous cursor versionCURSOR_INVALID; the body is version 2 and version 1 is not reinterpreted under the new layout

limit, view, response_format and include_derived are not part of the cursor identity, so changing one of them mid-pagination is accepted: the same cursor can continue a query in a different view, and changing limit shifts what the remaining pages contain. A page taken through name is bound to the qualified name the name resolved to, not to the name, so it keeps working while the snapshot does.

guidance is present only when the count alone would mislead: when nothing was found, and when the page is truncated. It stays absent on a complete non-empty answer. The zero-row sentence differs by direction, and both are worth reading literally: incoming says the absence is type-checked and points at find_cross_repo_consumers and graph_status; outgoing says the symbol reaches nothing and suggests asking the other direction.

include_derived is false by default and that default is load-bearing. With a Rust toolchain in the graph, references to a name like Clone reach most of the corpus, and a page of the standard library is not what a question about your own code wants. Withholding those rows is a decision about the page and never a claim about the graph: the edge stays published with its exact confidence, and include_derived: true, or naming the provider in repo, brings it back.

A rare name in one small repository is cheaper to grep, and this tool costs a symbol resolution before it answers. It is a single hop, so it cannot tell you what happens two calls away. It also depends on the snapshot being current: if the tree moved since it was indexed, the answer describes the code that was indexed, not the code on disk. graph_status reports that.