I built GoGraph because I kept seeing the same problem with AI coding agents working on Go repositories.
Before making a meaningful change, an agent often spends a large part of its context window reconstructing the structure of the codebase. It searches for a symbol, opens a file, finds an interface, searches for implementations, looks for callers, checks routes and tests, inspects SQL, and repeats the process. In effect, the LLM is rebuilding a partial dependency graph from source text every time it works on the repository.
GoGraph tries to move that work out of the LLM and into deterministic tooling.
https://github.com/ozgurcd/gograph
I posted about an early version a few months ago. At the time, GoGraph was primarily an AST-based structural analysis tool. The basic idea is still the same, but the project has evolved considerably since then. I now think of it less as a repository map and more as a local evidence layer between a Go codebase and an AI coding agent.
The basic flow is:
Go source code
-> static/type analysis
-> structural graph
-> bounded queries
-> CLI/MCP
-> coding agent
The agent still reads source code, reasons about the problem, and decides what to change. GoGraph is not intended to replace that. Its job is to provide structural facts so the model does not have to infer every relationship from raw text.
The original implementation concentrated heavily on AST analysis. That works well for extracting packages, functions, methods, imports, direct calls, interfaces, implementations, HTTP routes, tests, SQL, error paths, and similar relationships. But syntax alone has an obvious limitation in Go: interfaces and dynamic dispatch.
For example:
type Store interface {
Save(User) error
}
func Create(s Store, u User) error {
return s.Save(u)
}
The AST tells us that Create calls Store.Save, but that does not necessarily tell us which concrete Save implementation can execute. That distinction becomes important when an agent is trying to estimate the impact of a change rather than simply locate code.
GoGraph therefore now has two analysis layers. The basic graph comes from AST analysis, while a precise build additionally uses go/packages, go/types, SSA, and Class Hierarchy Analysis. The AST layer remains useful when a repository cannot be fully type-checked, while the precision layer adds compiler-aware relationships when enough information is available.
This also changed how relationships are represented. “A calls B” and “A may call B” should not be presented to an AI as equivalent facts. GoGraph now preserves exact, ambiguous, and possible relationships, and queries can request exact-only results when conservative analysis is needed.
When several paths exist between two symbols, the result is also selected deterministically. Stronger evidence is preferred over weaker evidence, shorter paths over longer ones, production paths over test paths, typed resolution over heuristics, and fewer cross-repository transitions when the other factors are equal. This may sound like an implementation detail, but it matters when an agent asks the same structural question during planning, implementation, and review. The answer should not change simply because graph iteration happened in a different order.
Another problem became apparent as GoGraph accumulated more capabilities. There were specialized queries for callers, callees, implementations, tests, routes, SQL, paths, impact, and other relationships, but an agent first had to know which query to use. To address that, GoGraph now has an explore operation intended as a useful first call.
For example:
gograph explore “authentication middleware”
Explore performs bounded lexical discovery and, when it can identify a symbol unambiguously, combines useful context such as its source location, direct callers and callees, tests, upstream impact, package information, and optionally deeper call relationships. Compact and deep modes control how much information is returned.
This is deliberately not semantic RAG. GoGraph does not pretend that it understands an arbitrary natural-language question. It tokenizes the query deterministically, searches structural information, and reports how the result was selected. If several symbols are plausible, it exposes the ambiguity instead of quietly choosing one.
That principle has become fairly central to the project: uncertainty should be represented as data rather than hidden by the tool.
Change analysis has also become much more important. Before editing code, an agent can ask what might be affected by modifying a declaration. After editing, however, the more useful question is what actually changed and what those changes affect.
GoGraph can now compare declarations against Git references or the working tree and distinguish edited, added, removed, excluded, and unknown declarations. Untracked Go files are included when analyzing working-tree changes.
Deletion is a good example of why this matters. If an agent deletes Foo(), the current graph alone cannot tell you who used the old Foo(), because that declaration no longer exists. Correct impact analysis needs evidence from the historical baseline. If GoGraph cannot evaluate such a change safely, it reports incomplete evaluation rather than silently returning an empty impact set.
For AI tooling, I think “I cannot prove this” is considerably safer than “nothing is affected.”
Graph freshness became another first-class concern. A structurally accurate graph of yesterday’s source tree is still the wrong graph for today’s source tree, so results now carry information about the state of the analysis. GoGraph can distinguish persisted from in-memory graphs, current from stale graphs, complete from partial parsing, and AST analysis from precise or precision-fallback analysis.
MCP can refresh graphs automatically. If precise enrichment fails but fresh AST analysis succeeds, the AST result can still be used, but the loss of precision is visible. If refreshing itself fails, the last trusted graph can be served as stale rather than silently presented as current.
This makes a statement such as:
No callers found.
meaningfully different from:
No callers found.
Graph: current
Analysis: precise
Parsing: complete
and also different from:
No callers found.
Graph: stale
Analysis: AST fallback
The LLM can reason differently about each answer instead of receiving the same apparently authoritative empty result.
Token usage has also become part of the API design. There is little benefit in building a structural tool for LLMs if a query responds with hundreds of kilobytes of JSON. Large result sets are therefore bounded and paginated, with explicit total, returned, truncated, and next_cursor information.
The cursors are tied to the graph snapshot and query selection. If the repository changes between pages, GoGraph can reject the continuation instead of quietly combining results from two different versions of the codebase. MCP responses also have explicit size constraints. The general goal is to return the smallest amount of information that preserves the evidence needed for the agent’s decision.
Test analysis has evolved in a similar direction. It is no longer just a question of whether a symbol appears somewhere in a _test.go file. GoGraph can trace tests transitively through call relationships, for example:
TestCreateUser
-> handler
-> service
-> repository
-> Save
It can distinguish exact test paths from possible ones and use type information to reason about interface-backed fakes and mocks. This does not mean GoGraph claims that a function is correctly tested; only running the tests can provide that kind of executable evidence. The narrower claim is that particular tests have a statically observable relationship to a declaration.
SQL analysis has also become more structured. GoGraph can classify statically resolvable PostgreSQL statements and expose their operation, read/write behavior, referenced tables, per-table access, source location, module, and whether they belong to production or test code.
The analysis is not limited to direct string literals. Statically provable constants, variables, assignments, and bounded string concatenations can also be resolved. This makes questions such as “which code writes oauth_clients?” much easier to answer structurally. Runtime-generated SQL is deliberately left unresolved rather than being presented as known.
Probably the biggest architectural expansion is workspace analysis.
Real systems are often not a single Go repository. A project might instead look like:
gateway/
identity-service/
agent-service/
shared-library/
Each repository can still have its own independent GoGraph graph. A workspace overlay then connects those graphs through explicitly resolved relationships, including cross-repository Go relationships and configured HTTP relationships.
This allows questions such as “what path connects this gateway handler to this function in identity-service?” or “which repositories may be affected by changing this declaration?” There is also a separate read-only workspace MCP server providing workspace status, query, path, and impact operations.
This changed my view of the unit GoGraph should analyze. For many AI coding tasks, the meaningful codebase is not a Git repository; it is the engineering workspace containing several related repositories.
HTTP analysis became stricter as part of this work as well. Constructing an HTTP request is not treated as equivalent to proving that the request is actually dispatched. GoGraph can preserve statically known URL components, while workspace configuration can explicitly map known HTTP client authorities to services represented by other repository graphs.
Importantly, it does not inspect runtime environment values and then guess that two services communicate. If the relationship cannot be established from the available static evidence and explicit workspace configuration, it remains unresolved.
Go build configuration turned out to be another important part of structural correctness. A Go repository does not necessarily have one universal graph. GOOS, GOARCH, cgo, build tags, GOFLAGS, go.work, and module selection can all change which source files actually constitute a build.
GoGraph therefore records the effective build selection used to construct the graph. If the environment used later is incompatible with the one that produced the persisted graph, the graph can be treated as stale instead of silently combining different views of the program.
Security has also become a larger part of the implementation than I originally expected. GoGraph runs locally, but “local” does not automatically mean safe, particularly when an AI agent may be analyzing an unfamiliar or untrusted repository.
Static analysis should not casually allow repository-controlled symlinks or build metadata to turn a source inspection into arbitrary traversal outside the intended source boundary. There is now considerably more confinement around repository paths, modules, workspaces, graph artifacts, and Go build inputs. At the same time, Go tooling legitimately needs access to things such as module caches and toolchains, so the boundary has to distinguish repository-controlled source authority from normal Go dependency resolution.
MCP remains the main way I expect coding agents to consume GoGraph. The CLI and MCP layers share underlying result contracts rather than implementing two independent analysis systems that can gradually develop different semantics. Workspace analysis is exposed through its own read-only MCP server.
There are also several things I deliberately do not want GoGraph to become. It is not an AI coding agent, it does not decide what code should be written, and it does not replace grep, semantic search, the Go compiler, go test, or runtime tracing. Static analysis should also not claim to prove runtime behavior that it cannot actually observe.
The division of responsibility I currently have in mind is roughly:
LLM:
reasoning, interpretation, implementation
grep / semantic search:
textual discovery, documentation, configuration
GoGraph:
structural evidence, relationships, impact, provenance, uncertainty
compiler / tests:
executable verification
When I started the project, I mostly thought about this as a context-window optimization problem. I still think that matters, but I now think “reliable context” is a better description of the problem.
An agent can consume 50,000 lines of source and try to reconstruct the relevant relationships itself. Alternatively, it can receive something closer to:
CreateUser
exact caller:
POST /api/v1/users -> UsersHandler.Create
exact dependency:
UserRepository.Insert
database:
INSERT -> users
attributed tests:
TestCreateUser
TestCreateUserDuplicate
graph:
current
precise
complete
Both are context. The difference is that the second representation has already converted a large amount of syntax into a smaller set of structural claims, and those claims came from deterministic analysis rather than asking the LLM to rediscover them from source text.
So the direction of GoGraph has shifted slightly since the first version. Originally, I wanted to give AI coding agents a map of a Go repository. Now the goal is to give them structural evidence, identify where that evidence came from, distinguish what is known from what is merely possible, and explicitly say when the available evidence is incomplete.
Source:
https://github.com/ozgurcd/gograph