From source to graph, without guessing.
Lattice reads each Python file once, works out what every call points at, measures every symbol, and stores the result. Questions are answered from the stored graph, so they are fast and the source is never re-parsed.
Four stages, one direction.
Parse
The standard library's
astparser reads each file on its own and emits symbols (modules, classes, functions, methods, constants) and call stubs. No file is written, and no marker is left in the repository.Resolve
A symbol table over the whole repository turns each call stub into an edge, cheapest rule first, and tags it with how it was resolved and how confident that is.
Measure
Topological depth from the entry points, PageRank, and in and out degree are computed once and stamped on every symbol.
Serve
MCP tools, the REST API, the command line, and the visualizer read the stored graph in PostgreSQL. A repository whose files have not changed is skipped on the next index.
Types come from your annotations.
Most calls look like x.foo(), and you must know what x is to know what foo is. Many tools infer types and get it wrong. Lattice reads x's declared type from the source instead, which is why fully annotated code resolves best. Every edge records which rule produced it.
| Tag | What it means | Confidence |
|---|---|---|
scope | A bare call resolved in the enclosing lexical scope. | 1.0 |
import | Resolved through an explicit import. | 1.0 |
self | self.foo() found on the enclosing class or one of its base classes. | 1.0 |
annotation | The receiver's type was written down: a parameter, a class attribute, or an explicit annotation. | 1.0 |
inferred | A local assigned from a constructor call or a return-annotated call that itself resolved exactly. Tagged apart so a derived type is never mistaken for a written one. | 1.0 |
heuristic | Nothing else resolved the call, and the method name is unique in the repository. | 0.5 |
external | A standard library or third-party call. No node exists for it, and that is a final answer, not a failure. | n/a |
unresolved | Not resolvable with confidence. The raw call text is kept, and no edge is invented. | none |
The governing rule: never fabricate an edge. A confidently wrong call graph is worse than an incomplete one, because an agent acts on it.
Depth, centrality, and blast radius.
- Depth. A breadth-first walk from every entry point. Entry points are 0; foundations are deepest. Unreachable code gets no depth, which marks it as dormant.
- PageRank. Power iteration over the call graph in plain Python. It sizes the dots and ranks search results.
- Fan-in and fan-out. Counted at index time, so coupling is a lookup.
- Blast radius. A recursive query walks callers backward and groups them by hop.


What it does not do.
- It is not complete. Dynamic calls, untyped receivers, and code built from conditionals and loops can stay unresolved. Resolution quality follows annotation coverage.
- It does not read bodies. It stores structure, not source text, so answers cite a location instead of quoting code.
- It does not change anything. Indexing is read-only, and the visualizer and tools only read.
- It is not a linter, a test runner, or a reviewer. It answers what a symbol is, what calls it, what it calls, and what breaks if it changes.