Koragraph

Declaration extraction

Declaration extraction is finding every function, class, method and type a codebase defines, which is the floor every later query stands on.

Declaration extraction is the job of finding every named thing a codebase defines: every function, class, method, interface, type, and constant. It sounds mechanical, and in a way it is, but it is the floor that every later question stands on. If a name is not on the list of things the code defines, nothing built on top can ever find it, and the failure is silent.

What a declaration actually is

A declaration is the place in the code where a name is introduced and given a meaning. It is the moment the program says, in effect, from here on the word parseDate refers to this specific piece of behavior. There is exactly one place where that introduction happens, and there can be many places later that use the name. Extraction is about the introductions, not the uses.

The kinds of things a program can declare are more varied than people first assume. A function is a named block of behavior you can invoke. A class is a named blueprint that bundles data and behavior together. A method is a function that belongs to a class, so its full identity is really the pair of the class name and the method name rather than the method name alone. An interface or a type is a named shape that says what fields and operations some value is expected to have, without necessarily saying how they work. A constant is a name bound to a fixed value. Different languages add their own: enums, traits, structs, modules, macros, properties, type aliases. Each is a name being introduced, and each belongs on the list.

Think of it like the index at the back of a reference book. The index does not contain the content of the book. It contains every term the book defines and the page where each one is introduced. If a term is missing from the index, the book has not lost that content, but you can no longer look it up. Declaration extraction is the work of building that index for a codebase, automatically, for every file, in every language the project uses.

How you find them: walking the tree

You do not find declarations by searching the text for words like function or class, because those words appear in comments, in strings, inside longer identifiers, and in code that is commented out. You find them by first turning the source into a structured form and then walking that structure.

Source code is text, but it is text with a strict grammar, and a parser can turn it into a tree that reflects that grammar. This tree is called an abstract syntax tree. In it, a whole file is the trunk, and every construct in the file is a branch: a function definition is a node, its name is a child of that node, its parameter list is another child, its body is another. The tree throws away the noise that does not affect meaning, things like spacing and comments, and keeps the shape. Because the shape is explicit, you no longer have to guess what a piece of text is. The tree already knows that this node is a function definition and that node is a variable and that other node is a string of text that merely happens to contain the word class.

Extraction, then, is a walk over that tree. You visit each node, and whenever you land on a node whose kind is one of the declaration kinds, you record it: what kind it is, what its name is, where it lives in the file, and what it is nested inside. A method declaration found three levels down inside a class node gets recorded as belonging to that class. A function declared at the top level of the file gets recorded as belonging to the file. The walk is patient and complete: it does not skip a branch because the branch looks uninteresting, because a class defined inside a function inside another class is still a class, and a reader will one day need to find it.

A tiny worked example

Consider a small piece of Python. There is one class, and inside it two methods, and above it one plain function.

def load_config(path):
    ...

class Invoice:
    def total(self):
        ...
    def mark_paid(self):
        ...

A correct extraction returns four declarations, not one and not two. It returns the function load_config, the class Invoice, and the two methods, recorded with their owner so that they are understood as Invoice.total and Invoice.mark_paid rather than as two loose names called total and mark_paid that could collide with methods of the same name on some other class. That ownership detail is not a nicety. It is the difference between a list of names and a list of things, and everything downstream depends on it being right.

Why this is the foundation of everything

Every richer question you might ask about a codebase reduces, at its base, to declarations. Who calls this function? That question presumes you have the function on record as a thing that can be called. What would break if I change this class? That question presumes the class is a known node that other nodes can point at. Where is the type that this value is supposed to match? Same again. The relationships between pieces of code, the calls, the imports, the inheritance links, are edges, and an edge needs two endpoints. A declaration is an endpoint. Miss the declaration and every edge that should have touched it simply never forms.

This is why extraction comes first and why it is unglamorous. It is the survey that has to be done before any map can be drawn. Koragraph MCP parses repositories with tree-sitter across twelve languages and records the symbols it finds so that later tools can reason over them. The tool file_symbols exists precisely to hand back the declarations found in a file, and tools like neighbours and blast_radius only have anything to say because the declarations they connect were captured in the first place. Nothing above the floor works if the floor has holes.

Every relationship in a code graph is an edge, and an edge needs two endpoints. A declaration is an endpoint. Miss it and the edge never forms.

The quiet danger: a missed declaration is a silent failure

Most tools fail loudly. They throw an error, they turn red, they print a message. Declaration extraction fails in the opposite way, and that is what makes it dangerous. When a declaration is missed, nothing goes wrong at the moment it is missed. There is no error. The name is simply absent from the record, and the record looks complete because you cannot see the thing that is not there.

The cost arrives later, and somewhere else. A developer, or an AI agent, asks who calls a particular function. If that function was captured, the answer is a real list. If it was missed, the answer is an empty list, and an empty list does not announce itself as wrong. It looks exactly like a function that genuinely has no callers. The person reads it as good news, decides the function is unused, and deletes it, or refactors it freely, only to find in production that forty callers depended on it after all. The tool did not lie. It answered the question it could answer against the record it had. The record was incomplete, and it never said so.

This is the trap to keep in mind for the whole subject. A lookup against a missing declaration is a lookup that can never succeed and is never reported. It is a false floor. You put weight on it because it looks solid, and there was never anything underneath.

Why eighty-two percent is much worse than it sounds

People hear a coverage number like eighty-two percent and file it under mostly working. For this task the intuition is wrong, and the reason is the silence described above. If a tool extracts eighty-two out of every hundred declarations, then just under one lookup in every five is quietly failing. Not returning an error. Not asking you to check. Returning a confident, wrong, empty or partial answer.

Compare that to ninety-nine percent. On paper it is seventeen points better. In practice it is a different kind of tool. At ninety-nine percent, roughly one lookup in a hundred is incomplete, and a person can hold a healthy suspicion about a rare miss the way they hold suspicion about any rare event. At eighty-two percent, misses are common enough that they are woven into normal use, but rare enough that you never learn to expect the next one. That is the worst place for a tool to sit: unreliable often enough to hurt you, reliable often enough that you trust it. The gap between those two numbers is not a matter of polish. It is the difference between a record you can build on and a record that will occasionally drop you without warning.

None of this means perfection is achievable, and honesty matters here. Some constructs are genuinely hard to recover, and the right response is to be clear about the limits rather than to pretend the last fraction of a percent does not exist. But the shape of the payoff is real: because the failure is silent, every point of coverage you claw back removes a class of quiet wrong answers, and the last few points matter more than the first few, not less.

Why consistency across languages is the hard part

Extracting declarations from one language is a solved kind of problem. Extracting the same idea faithfully across many languages, so that a function in Go and a function in Ruby and a method in Swift all land in the record as comparable things, is where the real difficulty lives. The trouble is that the languages do not agree on what a declaration even is.

A few of the ways they diverge:

  • In some languages a function must be written with an explicit keyword and name. In others a function can be an anonymous value assigned to a variable, so the name you would index it by is not on the function at all but on the assignment beside it.
  • A method belongs to a class in most object oriented languages, but in Go a method is attached to a type through a receiver written in the signature, and in Rust methods live in separate implementation blocks away from the type they extend. The same concept, three different tree shapes.
  • Types can be declared with a dedicated keyword, aliased to other types, inferred rather than written, or expressed as interfaces that no single file fully owns.
  • A constant in one language is a first class declaration, and in another it is just a variable that convention says nobody should reassign, with nothing in the syntax to mark it.
  • Nesting rules differ. Some languages let you define a class inside a function; others do not. Some allow multiple declarations to share a name and be told apart by their arguments; others forbid it.

Each of these is a separate small decision about where the name lives in the tree and how to record it so it means the same thing as its cousins in other languages. Get one of them wrong for one language and you have introduced a silent gap that only shows up when someone works in that language and asks a question that quietly returns less than the truth. This is why breadth is not just twelve times the work of depth. It is twelve grammars that each hide their declarations in their own places, all of which must be reconciled into one honest record.

Where this sits in the larger picture

Declaration extraction is the first real step from raw text toward a usable map. The parse turns a file into an abstract syntax tree, and this walk turns that tree into a list of the things the file defines. That is the raw material. It is not yet a map, because a list of nouns is not a set of relationships, but no relationship can be drawn until the nouns exist.

From here the story moves in two directions. One is toward the machinery that makes the parse fast and forgiving across every language at once, which is the work of tree-sitter. The other is toward the harder question of what the extracted names actually refer to, since the same name can mean different things in different places, and untangling that is the subject of symbols, scope and binding. Both of those lead, in the end, to the same destination: the code knowledge graph, the explicit map of how the pieces connect, which cannot begin to be built until the pieces themselves have been found.

Connected concepts

The abstract syntax treeAn abstract syntax tree is the nested, typed representation of a program that a parser produces, where every construct is a node and containment is the shape.Tree-sitterTree-sitter is a parser generator fast and forgiving enough to parse every file in a repository, including ones that do not currently compile.Symbols, scope and bindingA symbol is a name the program gives to something, and scope is the set of rules that decides which definition a given use of that name actually refers to.The code knowledge graphA code knowledge graph is a structured map of a codebase where every declaration is a node and every real relationship, calls, imports, inheritance, cross-service links, is an edge.The ingest pipelineIngest is the pass that turns a pile of repositories into one graph: parse with tree-sitter, extract declarations, resolve every edge, mine the git history, and write it all to the local store.

Where this sits

Back to the full graphThe short glossary