GoGraph Four Months Later: From Repository Map to Evidence Layer for AI Coding Agents
A few months ago, I wrote about a problem I kept seeing with AI coding agents: they could write Go perfectly well, but before doing anything useful they spent a surprising amount of their context window trying to understand the repository.
They would grep for symbols, guess filenames, read large files, follow imports manually, and slowly reconstruct relationships that the Go compiler already knows.
That was the motivation behind GoGraph.
The first version I wrote about in May was relatively simple in concept: parse a Go repository, turn its structure into a graph, and expose that graph to coding agents through CLI commands and MCP.
The basic idea has not changed.
But GoGraph itself has changed quite a lot.
Today, I would describe it less as a static repository map and more as a local evidence layer between a Go codebase and an AI coding agent.
The Original Model: Give the Agent a Map
The first version concentrated heavily on AST-based structural analysis.
GoGraph parsed source code and extracted things such as:
- packages
- functions and methods
- imports
- calls
- interfaces and implementations
- HTTP routes
- tests
- SQL-related structures
- error paths
Commands such as plan, review, and errorflow were intended to answer practical questions before and after an agent edited code.
Instead of:
grep
rg
awk
sed
read file
grep again
read another file
guess
the agent could ask GoGraph for a bounded structural answer.
That is still useful, and the AST layer still exists.
The limitation is that Go is not completely understandable from syntax alone.
Consider this:
type Store interface {
Save(User) error
}
func Create(s Store, u User) error {
return s.Save(u)
}
From the AST, we know that Create calls Store.Save.
But that does not necessarily tell us which concrete Save implementation executes.
That becomes increasingly important once an agent starts reasoning about impact rather than simply locating code.
GoGraph Now Has a Precision Layer
The largest architectural change is that GoGraph no longer relies only on syntactic relationships.
Precise builds now use Go’s type information together with go/packages, go/types, SSA, and Class Hierarchy Analysis.
The result is effectively a two-layer model.
The AST layer remains fast, resilient, and useful even when a repository cannot be fully type-checked.
The precision layer adds compiler-aware relationships when the repository can be successfully loaded.
This matters particularly for interfaces.
An AST-level analysis might know that:
service -> Repository.Save
Type-aware analysis can identify actual implementations of that interface.
SSA analysis can sometimes go further and prove that a particular interface value contains one concrete implementation at a call site.
GoGraph deliberately distinguishes those cases.
A call that is proven receives different treatment from a call that is merely a possible dynamic dispatch target.
That distinction turned out to be important for agents.
An AI should not see:
A calls B
when what the analysis actually knows is:
A may call B
Those are different facts.
Exact and Possible Relationships
This led to another change in the graph model.
Relationships now carry more information about how strongly GoGraph knows them.
For example, paths can contain relationships that are:
Commands can request exact-only results when an agent needs a conservative answer.
When several possible paths exist, GoGraph also ranks them deterministically rather than returning whichever traversal happens to find first.
The preference is roughly toward:
- stronger evidence
- shorter paths
- production code rather than tests
- typed resolution rather than heuristics
- fewer cross-repository transitions
This sounds like an implementation detail, but it changes agent behavior substantially.
Determinism is useful when an LLM repeatedly asks the same structural question during planning, editing, and review. The answer should not change merely because map iteration order changed.
A First-Call Tool for Agents
One problem became obvious after using GoGraph with actual coding agents.
Even with dozens of specialized commands, the agent first has to know which command to use.
So GoGraph now has explore.
For example:
gograph explore "authentication middleware"
or through MCP, gograph_explore.
It performs bounded lexical discovery and, when a symbol can be selected unambiguously, combines several kinds of context into one response:
- matching symbols
- source location
- direct callers
- direct callees
- tests
- upstream impact
- package context
- deeper call relationships when requested
There are compact and deep modes.
This is deliberately not semantic RAG.
GoGraph does not pretend that it “understands” a natural-language question. It tokenizes the query deterministically, finds structural candidates, and reports how the selection was made.
If a symbol is ambiguous, it exposes the ambiguity instead of silently picking one.
That is an important design rule throughout the newer versions of GoGraph:
Uncertainty should be data, not something hidden by the tool.
From Repository Analysis to Change Analysis
The original plan idea answered:
What could be affected if I modify this symbol?
That is useful before editing.
But after an agent has changed the repository, the more interesting question is:
What did I actually change, and what does that change affect?
GoGraph now performs declaration-level change analysis against Git references and the working tree.
It distinguishes things such as:
- edited declarations
- added declarations
- removed declarations
- excluded declarations
- changes that cannot be evaluated reliably
Untracked Go files are included when looking at working-tree changes.
This also avoids a surprisingly dangerous failure mode.
Imagine the agent deletes a function.
Looking only at the current graph cannot tell you who called the deleted function, because the function no longer exists.
Change analysis therefore needs historical evidence from the baseline rather than pretending the current graph contains enough information.
When GoGraph does not have enough evidence to calculate an impact safely, newer versions prefer to report that the evaluation is incomplete rather than returning an empty impact set.
For AI tooling, “I cannot prove this” is much safer than “nothing is affected.”
Graph Freshness Became a First-Class Concept
Another thing I underestimated in the original version was graph freshness.
A perfectly accurate graph of yesterday’s checkout is still the wrong answer.
GoGraph now tracks graph state explicitly.
Results can distinguish:
- persisted vs. in-memory graph
- current vs. stale graph
- complete vs. partial parsing
- AST vs. precise vs. precision-fallback analysis
MCP can refresh stale graphs automatically.
If precise enrichment fails but AST analysis succeeds, GoGraph can return the fresh AST result while explicitly saying that precise analysis failed.
If refreshing itself fails, it can retain the last trusted graph while exposing that it is stale.
The important part is that these states are not collapsed into one vague “success.”
For an AI agent, provenance matters.
There is a substantial difference between:
No callers found.
and:
No callers found.
Graph: current.
Analysis: precise.
Parsing: complete.
There is an even larger difference between that and:
No callers found.
Graph: stale.
Analysis: AST fallback.
The model should be allowed to reason differently about those answers.
Large Results Are Now Explicitly Bounded
Token efficiency was the original reason I created GoGraph, so there is an obvious irony if a structural query returns a 200 KB JSON response.
Newer GoGraph query APIs therefore put considerably more effort into bounded output.
Large result sets use deterministic pagination and expose information such as:
total
returned
truncated
next_cursor
The cursor is tied to the graph snapshot and query selection, so it cannot silently continue over a different graph after the repository changes.
MCP output also has an explicit response-size budget.
This is not merely an API optimization.
A tool built for LLMs needs to consider context consumption as part of its interface design.
Traditional developer tooling often assumes:
Returning more information is better.
Agent tooling often has the opposite requirement:
Return the smallest amount of information that preserves the evidence required for the decision.
Better Test Reasoning
Test discovery has also moved beyond simply asking whether a symbol name appears in a _test.go file.
GoGraph can now trace tests transitively through call relationships and distinguish exact test coverage paths from possible ones.
This becomes useful with architectures where a test reaches the target through several layers:
TestCreateUser
-> handler
-> service
-> repository
-> Save
Router callbacks and other intermediary functions can participate in that attribution.
Interface-backed test doubles are also important.
GoGraph’s typed test analysis can recognize cases where a test assigns a concrete fake or mock to an interface and prove the resulting call target when the type information allows it.
That makes commands such as tests and untested much more useful as structural evidence for coding agents.
It still does not mean “this code is correctly tested.”
That requires running the tests.
It means something narrower and more defensible:
These tests have a statically observable relationship to this declaration.
SQL Became Structured Evidence
SQL started as another useful static extraction target.
It has since become a more structured query system.
GoGraph can classify statically resolvable PostgreSQL statements and expose information such as:
- operation
- read/write access
- referenced tables
- access per table
- source location
- module
- test/production origin
It understands more than literal strings.
Statically provable local constants, variables, assignments, and bounded concatenations can also participate in SQL resolution.
Dynamic SQL is intentionally not presented as if it were fully known.
The result is useful for questions such as:
Which code writes the oauth_clients table?
or:
Which production paths execute DELETE statements?
Again, the goal is not to replace a SQL parser, database audit system, or runtime tracing.
It is to give an agent a useful static census of evidence that exists in the repository.
HTTP Relationships Also Became More Precise
Route extraction existed in the original version, but HTTP reasoning has expanded.
GoGraph now distinguishes constructing an HTTP request from actually proving that the request is dispatched.
It can preserve statically known URL bases and suffixes.
More importantly, this becomes useful in the workspace model.
A repository can declare mappings between outbound HTTP client authorities and another repository in the same workspace.
That allows GoGraph to represent a relationship such as:
frontend-api
-> HTTP request
-> identity-service
-> POST /users
-> CreateUser handler
without inventing relationships from runtime environment variables or guessed hostnames.
GoGraph Can Now Analyze a Workspace
This is probably the biggest conceptual expansion since the original article.
Modern systems are often not one Go repository.
You may have:
identity-service/
agent-service/
gateway/
shared-lib/
Each repository has its own graph.
GoGraph can now build a workspace overlay across them.
The individual repository graphs remain independent. The workspace adds explicitly resolved relationships between them.
These can include cross-repository Go relationships and configured HTTP relationships.
The workspace can then answer questions such as:
What path connects this gateway handler
to this identity-service function?
or:
What repositories may be affected by this change?
There is also a separate read-only workspace MCP server exposing:
gograph_workspace_status
gograph_workspace_query
gograph_workspace_path
gograph_workspace_impact
This matters because the “whole codebase” visible to an AI agent is increasingly not the same thing as a Git repository.
The useful unit is often the engineering workspace.
Go Build Configuration Is Part of the Graph
Another source of false confidence is pretending that a Go repository has one universal structure.
It often does not.
Build tags, GOOS, GOARCH, GOFLAGS, cgo, go.work, generated files, and module selection can all change which source files constitute the program.
GoGraph now records the effective build selection used to construct the graph.
Explicit build tags are supported.
If the environment used to query a persisted graph no longer matches the environment used to build it, the graph can be considered stale rather than silently mixing incompatible views of the program.
That is another example of something which is easy to ignore in a developer convenience tool but important in an evidence tool.
Local Does Not Automatically Mean Safe
The original article emphasized that GoGraph runs locally.
That remains true.
But over time I became much more conservative about what “local static analysis” should be allowed to touch.
The current code performs extensive path and source confinement checks.
Among other things, it treats Go build inputs, module/workspace metadata, graph artifacts, and generated files carefully around symbolic links and repository boundaries.
The goal is simple:
Asking an AI agent to inspect an untrusted repository should not casually turn a static-analysis request into arbitrary traversal outside the intended source tree.
Go tooling itself can legitimately access module caches, proxies, and toolchains, so the boundary cannot simply be “never touch anything outside the repository.”
Instead, GoGraph tries to distinguish repository-controlled source authority from legitimate Go dependency resolution.
This security work is considerably less visible than a new query command, but I now consider it part of the core design.
MCP Is No Longer Just a Wrapper Around CLI Output
MCP was already present in the original version.
Its role has expanded.
The project MCP server now exposes most of GoGraph’s repository analysis capabilities directly, with CLI and MCP generally sharing the same underlying result contracts.
That prevents an unfortunate architecture where:
CLI implementation
+
separate MCP implementation
slowly develops different semantics.
The MCP server can also expose its own version and analysis capabilities, which matters because restarting an MCP process is necessary after installing a newer GoGraph binary. An already-running process does not magically inherit the new tool schema.
The server still runs locally over stdio.
There is no hosted GoGraph service receiving the repository.
What Has Not Changed
Despite all of these additions, there are several things I deliberately do not want GoGraph to become.
It is not an AI model.
It does not decide what code should be written.
It does not replace go test.
It does not replace the compiler.
It does not replace runtime tracing.
It does not replace semantic code search.
And it should not pretend static analysis can prove runtime behavior that static analysis cannot see.
I increasingly think the useful division of labor looks like this:
LLM
reasoning
interpretation
implementation
Semantic search / grep
discovery
text
configuration
documentation
GoGraph
structural evidence
relationships
impact
provenance
uncertainty
Go compiler / tests
executable verification
These tools complement each other.
The Larger Lesson
When I wrote the first article, I described the problem mostly as a context-window problem.
I still think that is true, but I would phrase it slightly differently now.
The scarce resource is not simply tokens.
It is reliable context.
Giving an agent 50,000 lines of source code is context.
Giving it this:
CreateUser
exact caller:
POST /api/v1/users -> UsersHandler.Create
exact dependency:
UserRepository.Insert
database:
INSERT -> users
attributed tests:
TestCreateUser
TestCreateUserDuplicate
graph:
current
precise
complete
is also context.
But the second form has already converted a large amount of syntax into a much smaller set of claims.
That is where I now see GoGraph fitting.
It is not trying to make the LLM smarter.
It is trying to reduce how much of the LLM’s intelligence has to be spent rediscovering facts that static analysis can provide deterministically.
The original goal was to give AI coding agents a map.
The current goal is slightly more demanding:
Give them evidence, identify where that evidence came from, distinguish certainty from possibility, and say when the evidence is incomplete.
For coding agents operating on large Go systems, I think that is a much more useful foundation.
GoGraph on GitHub: https://github.com/ozgurcd/gograph