Every Pylon job starts the same way: before the agent writes a single line of code, it builds a representation of the repository's structure. We call this the context graph. The quality of everything Pylon produces, from branch naming to diff scope to test selection, depends on how accurately the context graph reflects what the codebase actually is.
This post is about the data structure itself: what it stores, how it gets built, and the specific decisions we made that turned out to matter for producing diffs that reviewers actually merge.
The four layers of the graph
The context graph is a directed property graph where nodes are files and edges represent relationships between them. But "file A imports file B" is only one kind of relationship. The graph stores four distinct edge types, and the choice of which edge types to include was the most consequential design decision we made in the early build.
Import edges: static import and require declarations. The most straightforward layer. For Python this is every import and from X import Y statement. For JavaScript and TypeScript, every import and require() call. These are parsed from the AST, not inferred, so they are accurate for static imports. Dynamic imports (strings computed at runtime, reflection-based module loading) are flagged separately as uncertain edges.
Call edges: which functions invoke which other functions, across file boundaries. This layer is more expensive to build and more error-prone, because call resolution requires understanding the type system well enough to know which concrete implementation gets invoked at a given call site. We use a combination of static type information (where available via TypeScript or Python type annotations) and heuristic resolution (matching call signatures against known function signatures in the import closure). Call edges are weighted by our confidence in the resolution.
Test coverage edges: which test files exercise which source files, and specifically which code paths within those files. We derive this from coverage data if the repository has it (Jest coverage JSON, pytest-cov XML, etc.) or from static analysis of the test file's import structure if it does not. Coverage edges are directional from test file to source file and are annotated with branch coverage percentages when available.
Change co-occurrence edges: which files have historically changed together in the same commit. This is derived from the git log and is one of the most practically useful edge types. Two files that always change together are probably logically coupled even if there is no explicit import relationship between them. Change co-occurrence catches configuration files that must be updated alongside source files, migration scripts coupled to schema files, and documentation paired with implementation.
Node properties: what each file knows about itself
Each node in the graph stores a set of properties beyond just the file path. These properties are what lets the agent make scope decisions without reading every file in full.
The most important node properties are: file language and AST summary (the function signatures, class names, and module-level variables, without full function bodies); change frequency over a rolling 90-day window (commits touching this file per week); test coverage percentage (if derivable); module role classification (is this a utility library, a service entry point, a type definition file, a configuration file, a test file?); and a list of the files that import this file, which we call reverse import edges.
Module role classification is heuristic and wrong sometimes. We use a combination of file path patterns (utils/, lib/, types/), naming conventions, and graph structure (a file with many reverse import edges and few of its own imports is probably a utility) to classify. When Pylon makes a scoping decision based on module role and the classification is wrong, it tends to produce PRs that touch more files than necessary. This is one of the areas where we are still iterating.
How scope decisions use the graph
When a job arrives, the agent uses the context graph to answer a specific question before it reads any file content: which files does this job need to touch, and which files does it need to read-but-not-touch to understand the context?
For a Pylon-Patch job patching a CVE in a specific dependency, the answer is usually: touch the dependency manifest and any files that call the vulnerable API; read (but do not modify) the files that import the fixed dependency to verify the patched API is backward compatible. The touch set is small. The read set is larger.
For a Pylon-Refactor job eliminating dead code, the graph traversal goes in reverse. Start from the candidate dead function, walk all reverse import edges, resolve all call paths that could reach this function. If the traversal finds no live call path from any service entry point or test entry point to the candidate function, the candidate is provably unreachable in static analysis. If it finds a call path, dead code deletion is unsafe and the job reports the reason.
This traversal is why Pylon can distinguish between a function that is never called (safe to delete) and a function that appears to be never called but is actually invoked through a reflection-based plugin system (not safe to delete, and the reason it appears unreachable is a signal that the plugin system exists). The uncertain edges we flag for dynamic imports show up here as potential call paths that cannot be resolved statically.
Incremental updates: keeping the graph current
For a repository that is actively developed, the graph needs to stay current between jobs. We do not rebuild the full graph from scratch on every job. Instead, Pylon listens to the GitHub or GitLab webhooks for push events and processes each push incrementally: re-parse only the files that changed, update import edges for those files, update change co-occurrence data for the files in the commit, and invalidate coverage data for any source files that were modified (coverage data is stale the moment the source changes).
Full rebuilds happen on repository connect (the initial index) and after any push that changes more than a configurable threshold of files, currently 15 percent. The 15 percent threshold is a heuristic: when that much of the codebase changes at once, the incremental update logic is likely to miss inter-file consistency issues that a full rebuild would catch. Large-scale refactors in the host repository can trigger full rebuilds, which take longer but produce a more accurate graph for subsequent jobs.
What the graph does not capture
Runtime behavior is not in the graph. The graph is a static analysis artifact. It does not know which code paths execute at high frequency in production, which database queries are slow, or which API endpoints are called by external clients. That kind of information would make the graph significantly more useful for prioritizing which technical debt to address first, and it is something we want to incorporate eventually, but it requires instrumentation that most teams do not have installed in a form we can easily ingest.
We are also explicitly not trying to reason about business logic. Whether a function is correct, whether it handles edge cases properly, whether it implements the right algorithm: these are questions for the engineer reviewing the PR. The context graph tells Pylon where a change is safe to make and how to scope it. It does not tell Pylon whether the change is semantically meaningful or correct. That remains human judgment, which is exactly why every Pylon job ends with a PR rather than a direct merge.