Store.Sync and the method name is only the start. The agent also needs the HTTP handler that calls it, the Swift client that consumes its response, the tests that describe its behavior, and the database state behind the cursor.
A broad repository scan wastes context and time. Working from a narrow guess creates blind spots. An index gives the agent a better starting point for each task.
A code index gives the agent a map. It turns definitions, references, calls, imports, tests, and file ownership into context the agent can query before it reads or changes code.

Why a coding agent needs an index
The model does not carry your current repository in its weights. Each task starts with a context-selection problem. The index decides which parts of the codebase the agent sees and which relationships it can follow.Select the smallest useful context
Sending the whole repository to a model is expensive and noisy. Sending one file is often too little. A useful index sits between those extremes. For a change toStore.Sync, the right slice includes the definition, its production caller, the client contract, and the tests. The agent can read four relevant files instead of searching hundreds and still understand the path it is about to change.

Preserve relationships across files and languages
Repositories are collections of files. Software behavior comes from the relationships between them. Checkmate’s synchronization path starts in a SwiftSyncEngine, crosses an HTTP handler, calls the Go store, and is verified by API tests. A text search can find the word sync. An index should tell the agent which occurrences form the same execution path.

Keep the map current
An index becomes dangerous when it is stale. If a developer adds a caller after the graph was built, the old answer can still look exact while omitting the new dependency. Freshness is a correctness requirement. The graph needs to reflect the code the agent is about to change. Automatic refresh helps. An explicit refresh command can also work when the agent workflow enforces it.
Estimate the scope of a change
Agents that modify code need more than navigation. They need to identify callers, contracts, likely tests, and the parts of the repository that deserve human review. An index cannot prove that a change is safe. It can narrow the inspection plan before the edit and help check the resulting diff afterward. The test suite still decides whether the behavior holds.
The Checkmate test case
Checkmate is a self-hosted personal task manager that I am developing for my own needs. It packages a React and TanStack frontend, a Go API, OAuth, and an MCP endpoint into one deployable binary backed by SQLite. A separate Swift application talks to the HTTP API and keeps a local cache for synchronization. That structure makes it a useful test. The repository is small enough to inspect manually, but real behavior crosses language and application boundaries. The relevant stack includes- Go for the API and core business logic
- TypeScript and TSX for the web application
- Swift for the iOS application
- SQL for migrations and persistence structure
- YAML, JSON, Markdown, and supporting project files

SyncEngine and LocalStore in Swift, an HTTP handler and Store.Sync in Go, API tests, and SQL-backed state. An index that stops at the Go server or the web client cannot describe that whole path.
I ran the benchmark against disposable copies of the repository and did not modify my working project. I used Graft 0.8.2 and roam-code 13.10.0, the current releases on August 4, 2026. Both projects are young and moving quickly, so the version numbers are part of the result.
How I tested the indexes
The test had four parts. I checked coverage by building each index from a fresh copy of the repository, inspecting its language and file counts, and asking for the structure of the SwiftLocalStore.swift file.
I checked retrieval with exact navigation around Store.Sync, a real server method with a production caller. I also asked where CHECKMATE_BASE_URL is configured and used. The cross-stack query asked “How do web and iOS clients synchronize changes with the Go server?”
I checked freshness with a no-change rebuild and an incremental update. For the update, I added the same harmless comment to server/internal/recurrence/recurrence.go in each disposable copy. I recorded the times to catch an operational problem, not to score small differences measured in seconds.
I checked change analysis with Roam’s preflight and affected-test commands. The goal was to see whether its deeper graph produced guidance I could rely on before an edit.
All timings below include CLI process startup. An MCP integration can have a different latency profile. I did not run graft build --deep. That optional step sends source through a configured model provider and adds semantic summaries, which would make this a test of model configuration as well as indexing.
I also excluded both vendors’ published benchmark claims from the scorecard. Neither project publishes a direct Graft-versus-Roam benchmark, and their existing tests use different tasks, models, and evaluation methods.
Timing was a non-factor
Every index operation completed in less than five seconds. Graft’s cold build took 2.78 seconds and Roam’s took 4.85. Roam’s no-change refresh was faster, while their one-file updates were separated by 0.11 seconds.
Based on these results, I would give indexing speed zero weight in the decision. Indexing happens before the much longer work of reading, changing, testing, and reviewing code. I would revisit timing if either tool took minutes, blocked every query, or could not refresh incrementally. Neither did.
The incremental behavior still tells us how the tools maintain their graphs. Graft parsed the one changed file and replayed the other 125. Roam marked one file as modified, then re-extracted 13 neighboring files to update related graph information. Both finished quickly enough that coverage and answer quality determined the decision.
Coverage changed the recommendation
The graph totals are not directly comparable. Graft reports nodes by structural type, while Roam reports symbols. The counts describe what each tool built. They do not make a larger graph better by default. Roam’s cold build reported 212 files. Its first repeat created and indexed
.roamignore, bringing the settled count to 213. The coverage graphics use that settled state.
Graft currently has first-class extractors for JavaScript and TypeScript variants, Python, and Go. That matches its own 0.8.2 language configuration. On Checkmate, those extractors covered the server and web application well.
They did not cover Swift or SQL.
When I asked Graft for a skeleton of the iOS LocalStore.swift file, it reported no indexed definitions. Roam returned 47 symbols from that file, including properties and methods. Its index also included 38 Swift files and 11 SQL files, plus documentation and configuration formats.
If all of your important code is written in languages Graft supports, this gap may not be a factor for you. Even then, relying on one index for the whole repository is easier to standardize than routing agents between overlapping tools. For Checkmate, the gap disqualifies Graft as the primary index. The iOS app is not an attachment to the system. It is one of the system’s clients.

Graft returned more direct focused results
Within the languages it supports, Graft returned more direct results on the focused queries. Its caller query forStore.Sync correctly found Server.handleSync. Its free-form query about CHECKMATE_BASE_URL returned the validation function, the default value, and the configuration loader. There was some unrelated web code in the result, but the important paths were near the top.
Roam’s exact symbol lookup also found Store.Sync. The response included its definition, one caller, eight callees, and useful body context. A separate uses query found the production consumer.
Two plausible Roam query forms exposed a rough edge. roam uses Store.Sync and a file-qualified variation failed even though Roam’s search had already found the symbol. The command-specific path worked.
The broad architectural question exposed a different problem. Graft returned partial server-side matches but could not include the iOS implementation because it had never indexed Swift. Roam had indexed both clients and the server, yet its free-form ask command returned no confident recipe match.
Graft’s answer was incomplete because Swift was outside its index. Roam had the source material but its free-form router did not assemble it into an answer.
Test the commands an agent will actually call. Language count and graph size do not predict retrieval quality. A successful demo query does not predict it either.
Roam added change analysis, with caveats
Roam’s broader feature set becomes useful before and after edits. It can report symbol context, impact, affected tests, git-aware changes, and pull request risk. This is closer to an engineering safety layer than a search index. On the recurrence module,roam preflight reported a 56-symbol blast radius and identified 29 direct plus 13 transitive tests. That is the kind of context I want before modifying shared scheduling logic.
The suggested test command was pytest, however. Checkmate’s recurrence code is Go. The graph relationships were valuable; the execution advice was wrong.
On Store.Sync, affected-test analysis returned no tests even though the repository has HTTP synchronization tests. Static analysis is the likely reason. Dynamic route registration does not always create an edge that a code graph can follow. Roam documents similar limitations around dynamic dispatch, reflection, heuristic imports, cross-language FFI, and generic language support.
Treat the analysis as a lead. A reported test is a strong reason to run that test. An empty test list is not proof that no test matters.
The same rule applies to pull request risk. A score can focus review, but it cannot replace reading the diff and running the relevant test suite. I now require agents to include the final roam pr-risk value and material findings in every pull request they create. If Roam cannot calculate a value, the pull request must say so.
Freshness and data sharing
Graft’s normal structural build is local. Since version 0.8.1, its integrations can refresh structural data automatically when the graph is stale. Deep summaries do not refresh automatically; they require another deep build. Its graph is now a local, gitignored cache, despite older documentation that described committing graph data. Roam also keeps its main analysis local in a SQLite graph. Index freshness is explicit. The agent needs to run or refreshroam index. That is easy to encode in a project workflow, but it is still a behavior to enforce. Roam can download grammar packs on first use, and optional AI summary features can send structured reports and code snippets to a configured model. Its documentation on data sent over the network is worth reading before enabling those features.
For regulated or private codebases, document which commands stay local, which send data over the network, and exactly what they send.
Indexing is doing two jobs
The benchmark stopped feeling contradictory once I separated the products into two layers. One layer retrieves context. It should be focused, easy for an agent to call, and good at returning the smallest useful slice of code. Graft is strong here. Its default set of MCP tools is small, and its free-form retrieval worked well on Checkmate’s Go and TypeScript code. The other layer supports change analysis. It should cover the repository, represent relationships, use history, estimate blast radius, point to tests, and summarize pull request risk. Roam is aiming at this broader job. One number cannot express that tradeoff. A focused retrieval layer can fit a service written in its supported languages. A broader engineering graph can fit a multi-language product. Some teams may want both, provided agents know which tool answers which question.
Which one should you choose?
Choose Graft first when:- Your important code is JavaScript, TypeScript, Python, or Go.
- Your main goal is compact context retrieval for coding agents.
- You want a small set of MCP tools with useful free-form search.
- Automatic structural freshness matters more than deep change analysis.
- The repository spans languages outside Graft’s current set.
- You need exact symbol navigation across a larger graph.
- You want preflight, impact, affected-test, diff, health, or pull request risk analysis.
- You are prepared to standardize the command workflow and verify imperfect recommendations.
- Graft’s supported languages contain most day-to-day implementation work.
- Roam’s broader graph is still needed for repository coverage and change safety.
- The benefit of combining them outweighs the added setup and maintenance.
- You can give agents explicit routing rules instead of letting them guess between two overlapping tools.
My decision for Checkmate
I am keeping Roam as Checkmate’s primary index. The decision rests on the 38 Swift files and 11 SQL files that Graft could not represent. I do not want an agent answering architectural questions from a graph that silently ends at the server and web client. I may still use Graft as a focused companion for Go and TypeScript retrieval. Its direct queries were often pleasant to use. I would not treat it as the system of record for this repository today. An index determines what your coding agent can see, what it can connect, and what it can miss while sounding confident. That makes the index part of the agent’s context-selection and review workflow. Start by opening the graph and asking which parts of your product are absent. On Checkmate, that check decided the outcome.Sources and reproducibility notes
- NanoNets/Graft repository and documentation
- Graft 0.8.2 source
- Graft 0.8.2 language extractors
- Cranot/roam-code repository and documentation
- roam-code 13.10.0 release
- roam-code 13.10.0 data sharing and network behavior