# Symvanta: The Code Knowledge Graph for AI Agents > Symvanta gives your AI coding agent your codebase's real call graph over MCP, so it stops guessing and knows what breaks before it edits. Index your GitHub repositories into a live code knowledge graph and give any MCP-compatible agent exact symbol resolution, call graphs, blast-radius analysis, and semantic search. ## What it is AI coding agents guess about code they cannot see. Symvanta removes the guessing: it parses each repository into a graph of code symbols (functions, classes, endpoints) and the relationships between them (calls, imports, implements), stores vector embeddings for semantic search, and serves all of it over the Model Context Protocol (MCP). Any MCP client gets graph-precise answers instead of grepping blindly. ## Capabilities - **Symbol resolution**: resolve any function, class, interface, or HTTP route by name to its exact location and signature. - **Call graphs**: trace direct callers and dependencies across the whole codebase, not just one file. - **Blast radius**: see the transitive impact of changing a symbol before you touch it, including cross-repository edges. - **Semantic search**: find code by meaning, not just by keyword. - **HTTP route lookup**: resolve a route by path and method to its handler. - **Behavior questions**: ask how something works and get a synthesized answer with citations. - **Branch-aware indexing**: index feature/RFC branches and make uncommitted working-tree edits queryable. ## Supported languages TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby. ## Connect via MCP MCP server endpoint: `https://mcp.symvanta.com/mcp` Authentication: OAuth 2.0 (PKCE). Discovery: `https://mcp.symvanta.com/.well-known/oauth-authorization-server` Symvanta runs alongside any other MCP servers you already use. Add the endpoint to your client: ```json { "mcpServers": { "symvanta": { "url": "https://mcp.symvanta.com/mcp" } } } ``` That JSON works for Cursor (`.cursor/mcp.json`) and other URL-based MCP clients. For Claude Code, add the same URL as an MCP server. On first use the client runs the OAuth flow in your browser. ## Pricing - **Starter**: $19/month. - **Pro**: $29/seat/month, with a free trial that needs no card. - **Enterprise**: $99/seat/month, 15-seat minimum on an annual contract with SSO and SCIM. See for current details. ## Data handling - Your source is parsed into a graph in memory and then discarded by default; stored source is an optional paid add-on. - We never train, fine-tune, or improve any AI model on your code, and never share it across tenants. - Data is encrypted in transit and isolated per tenant. Private repositories work identically to public ones via GitHub webhooks and encrypted per-tenant credentials. ## Frequently asked questions ### Does it work with private GitHub repos? Yes. Symvanta uses GitHub webhooks and encrypted per-tenant credentials. Private repos work identically to public ones. ### What data do you store? We store a graph of code symbols and relationships (nodes, edges, file hashes) plus vector embeddings for semantic search, isolated per tenant, encrypted in transit, and encrypted at rest where a row carries source text (on Pro and Enterprise, optionally under a key you control). By default your source is never kept: it is parsed in memory to build the graph and the working copy is then discarded. Stored source is an optional, paid add-on that stays off unless you explicitly turn it on; when enabled it powers instant file reads and in-repo search for your CI and headless agents, and you can turn it off again at any time. Uncommitted edits made queryable with index_working_tree are indexed into a short-lived revision (reclaimed after a few hours) scoped to your tenant. Our Security page at symvanta.com/security walks through the full parse, graph, and discard pipeline. ### How long does indexing take? A typical 100k-line repo indexes in a few minutes. Incremental re-indexing on subsequent pushes is faster: we skip unchanged files using content hashing. ### Does it work across multiple repositories? Yes. Repositories in a project share one graph: imports are resolved past the package name back to the source repository, and HTTP calls between services become cross-repo edges. Ask for the callers or blast radius of a shared symbol and consumers in other repositories are part of the answer. Cross-repo intelligence is included on every plan. ### Can my agent see my feature branch or unmerged work? Yes. Open a same-repo pull request and Symvanta auto-tracks and indexes that branch (you can also add a branch from the dashboard). Your agent pins a session to it with ref(op: "use"), so every query answers from the branch instead of the default. Uncommitted working-tree edits can be made queryable too via index_working_tree. The number of tracked branches depends on your plan. ### Why not just use a free or local code-graph tool, or my editor's built-in indexing? A local index lives on one machine and covers one clone in one editor, so every developer rebuilds and maintains their own copy and it only knows the repository currently open. Symvanta is one graph shared by the whole team, kept current automatically: you push, a webhook re-indexes the changed files, and nobody rebuilds anything by hand. It resolves relationships across repositories, so the callers or blast radius of a shared symbol include consumers in your other services, beyond the repo in front of you. Because it answers over an authenticated endpoint, headless CI agents and code-review bots read the same graph an IDE session does. And it can pin a session to a specific branch and index uncommitted working-tree edits, so answers stay accurate when you switch branches or work before committing. For one developer on one repository a built-in index is fine; the value here is a graph that is shared, cross-repo, always current, and reachable from outside any single editor. ### Does it work with my existing Cursor or Claude Code setup? Yes. Add the Symvanta URL to your .cursor/mcp.json or Claude Code MCP config. It runs alongside any other MCP servers you already use. ### Which languages are supported? TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby. ### Can I cancel anytime? Yes. Pro is month-to-month: cancel anytime with no annual contract. Enterprise runs on a custom term. Contact us at info@symvanta.com to discuss your needs. ### Do you train AI models on my code? No. We never train, fine-tune, or improve an AI model on your code, and we never share it across tenants. Your source is parsed into a graph in memory and then discarded unless you turn on source storage, so every answer comes from the graph, not a model that learned your code. ### Which AI models process my code? Open models on GPU infrastructure Symvanta operates in the EU: Gemma generates symbol summaries and graph answers, and Nomic Embed Code powers semantic search. Your code is never used to train or fine-tune any model. ### Can we bring our own encryption key? Yes, on Pro and Enterprise. Your workspace gets its own data key, wrapped by a key you control in AWS KMS, GCP Cloud KMS, Azure Key Vault, or an HTTPS key endpoint you run. Symvanta unwraps it for ten minutes at a time, every unwrap shows in your own audit log, and revoking access makes your stored source unreadable to us within ten minutes. Set it up under Workspace settings > Customer-managed encryption key; the Security page lists exactly what the key covers. ### Can I self-host Symvanta? Yes, on Enterprise. The on-prem engine and its own admin console run entirely inside your network: source code, the parsed graph, users, and teams never leave it, including SSO and SCIM. Only a license heartbeat and public-OSS catalog lookups cross the boundary. Contact us at info@symvanta.com to set it up. ## Blog ### Onboarding onto a New Codebase with AI https://symvanta.com/blog/onboard-new-codebase-with-ai The first week on an unfamiliar codebase goes on questions the code can already answer: where a request comes in, which module owns billing, what sits between a button and the row it writes. You open files, follow imports by hand, and build a mental model out of fragments. An AI coding agent starts from the same blank page with one extra handicap: it cannot skim. It searches for a string, reads whatever comes back, and forms an opinion from the sample that search happened to return. The way through, for you and the agent both, is to get the shape of the system before chasing any single behavior, and to check impact before anything gets edited. ## Orientation is a mapping problem Ask a new joiner what they did on day one and the answer is usually "read the folder tree". The tree records filing decisions, some of them years old, and says nothing about which module calls which when the program runs. Two files in the same directory can share no code path at all, and a single import can bind directories at opposite ends of the tree. The map worth having is computed from the calls. Parse the repository into symbols and edges, cluster those edges by how densely groups of symbols call each other, and the modules that exist in behavior fall out, each with the symbol most of the traffic routes through. That is what [dependency mapping](/use-cases/dependency-mapping) produces: a few dozen named clusters with weighted arrows between them, derived from call behavior and frequently disagreeing with the directory layout. Read the disagreements closely: a cluster spanning four top-level folders marks a real seam the directory layout hides. Pair the map with the entry points: HTTP routes, CLI commands, queue consumers, scheduled jobs. Execution begins at one of them, and so does every trace you will run this week. ## What to ask in the first hour With the graph served to your agent over MCP, the opening questions have short answers that fit in a working context. Four asks cover most of day one. - The module map for the repository, with the hub symbol inside each module. This is the shape of the system in a page. - The symbols carrying the most inbound calls. High fan-in marks the code you will meet again, so read it early and touch it carefully. - The handler behind a route you care about, by method and path. `find_http_route` goes straight there with no guessing about the project's naming conventions. - A behavior question in plain words: how this project does authentication, where retries live, what happens after checkout. `ask_codebase` answers with file citations you can open. Each answer arrives as names, file paths, and line bounds. A whole-file read to settle a one-line question spends budget you need later in the session, and those tokens sit in the window for every turn that follows. [Context engineering for coding agents](/blog/context-engineering-for-coding-agents) works through the arithmetic. ## Trace the behavior you were handed Onboarding usually ends with a ticket, and a ticket names a behavior. "Bookings sometimes send two confirmation emails." Nothing in that sentence is a symbol, so the first job is turning it into one. Start at the entry point: resolve the route or the job that begins the flow, then walk the call chain out of its handler. A graph traversal follows the calls the parser resolved, including the hop where a call site names an interface method and the concrete implementation carries a different name. Text search cannot make that hop, and it is the hop most likely to hide the code you actually want. Read source last, and only the spans the traversal named. By then you know which file to open and which twenty lines inside it carry the behavior. That is a different exercise from opening a file to find out whether it matters at all, and the difference compounds over a week of them. ## Where a grep-only agent loses the thread An agent onboarding without a graph searches for `sendConfirmation`, gets nine matches, and opens all nine. Two are comments. One is a changelog entry. One is a method of the same name on an unrelated class. The real call site, routed through a `Notifier` interface, never appears in the results. The agent then explains the flow with total confidence, built from the five files it happened to read. The gap widens with the repository. On a small project the sample is most of the code, so guessing works well enough to feel like understanding. On a monorepo, or across the four repositories a production system actually spans, the sample is a rounding error, and the failure modes get specific: [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases) walks through each one. Onboarding is where you feel it first, because everything is unfamiliar and you carry no prior knowledge to catch a wrong answer with. ## The first change is an impact question A new joiner's first pull request is small on purpose, and a small diff can still reach a long way. `getHttpError`, a nineteen-line cal.com helper measured at commit `176037d` for [an earlier post here](/blog/repo-wiki-vs-code-graph), has 2 callers, both sitting in the file with it. Read the callers alone and you ship the edit in a minute. Its blast radius, walked outward from those two, is 77 symbols across 66 files, and the named set includes both booking endpoints, the tRPC handler factory, and a payment refund. Same nineteen lines, two very different reviews. So the order before an edit is callers first, then the downstream set, then whichever tests already stand over the symbol. Those calls cost less than one file read and they change what the pull request looks like. This is also the step where a new joiner is weakest, because knowing which parts of a system are load-bearing is exactly the instinct you have not built yet. A number substitutes for the missing instinct: fourteen callers across three repositories means the change wants splitting, and zero callers usually means dead code.
Three passes over an unfamiliar codebase: orient with a module map and entry points, trace a route to its handler and the call chain through it, then size the edit with callers, downstream symbols, and covering tests, with source opened last
Three passes before the first edit. Names and file paths travel through the session; source gets opened last, only where the earlier passes pointed. Link to this diagram Open full size
## A loop for the first week The version that holds up, run with the graph in the agent's tool list: 1. Pull the module map and the entry-point list. Learn names before files. 2. Pick the entry point closest to your first ticket and trace its chain to the leaf. 3. Ask the behavior question in words and open the citations that come back. 4. Before any edit, pull callers, blast radius, and covering tests for every symbol you plan to touch. 5. Open source last, only the spans the earlier steps located. Steps 1 through 4 return identifiers, so the window stays mostly free until step 5, the first moment real source enters it. The loop is the one an experienced engineer runs from memory; the graph supplies the memory you have not built yet. Wiring it takes one command in [Claude Code](/integrations/claude-code), and Cursor, Windsurf, and the other MCP clients call the same tools. ## Known limits A parsed graph sees what the source says. Dependency injection keyed by string, a handler wired from a config file, a plugin loaded by scanning a directory: those edges come back weak or missing, and in a codebase leaning on them the blast radius understates the true reach. Treat a suspiciously low number in that kind of area as a prompt to go look. The graph also cannot tell you why. It shows that a module exists and where its traffic goes. The reason it was split from its neighbor two years ago lives in a pull request description or in someone's head, and asking that person is still the fastest route to it. An index trailing the branch you are working on describes a codebase you are not editing, and it describes it confidently. Per-branch indexing, plus a way to overlay the edits you have not pushed, is what keeps the map pointed at the code in front of you. Onboarding is the stretch where confident wrong answers cost the most, because you have nothing to check them against. Symvanta gives your AI coding agent your codebase's real call graph over MCP, so it stops guessing and knows what breaks before it edits. The module map is there on day one, the caller list before the first pull request, and the answers hold whether the code is one repository or four. Point it at the repository you just inherited and see what the first hour turns up. --- ### Remote vs Local MCP Code Indexes https://symvanta.com/blog/remote-vs-local-mcp-code-index Wiring a code-context MCP server into a coding agent takes two decisions, and most write-ups cover only the first. You pick the tool. Then you pick where its index lives: a process on your laptop reading your working copy, or an endpoint holding a parsed copy of your repositories. The protocol is identical either way. Zed's settings file shows both shapes on one page: a `command` with `args` for a server it spawns locally, a `url` with optional `headers` for one it reaches over HTTP ([Zed docs, MCP](https://zed.dev/docs/ai/mcp)). The choice reads like a deployment detail. It sets index freshness, parsing cost, how far past one repository the agent can see, and how access is controlled. ## Two shapes of the same server A local index server is a process you start. Serena is the clearest example: MIT licensed, installed on your machine, doing semantic retrieval and editing at the symbol level through language servers that implement LSP, with "support for over 40 programming languages" on the free backend ([oraios/serena](https://github.com/oraios/serena)). No account, no upload, and nothing to pay. You point it at a project directory and it answers from what the language server resolves. A hosted index server is a URL. DeepWiki publishes one at `https://mcp.deepwiki.com/mcp`, described by Cognition's docs as "a free, remote, no-authentication-required service that provides access to public repositories" ([Devin docs, DeepWiki MCP](https://docs.devin.ai/work-with-devin/deepwiki-mcp)); the [DeepWiki alternative](/compare/deepwiki-alternative) page covers what its three wiki tools return. Sourcegraph runs one against your own instance at `https://your-sourcegraph-instance.com/.api/mcp`, "Supported on Enterprise plans" ([Sourcegraph MCP server docs](https://sourcegraph.com/docs/api/mcp)), where Enterprise is listed as "Starting at $16K" with credits that scale by team size ([Sourcegraph pricing](https://sourcegraph.com/pricing)); the fuller picture is on the [Sourcegraph Cody alternative](/compare/sourcegraph-cody-alternative) page. Symvanta is a hosted endpoint too. Same protocol, same tool call, different machine doing the parsing.
A local index where one agent talks over stdio to an MCP index server reading a single working copy, beside a hosted index where an agent, a teammate's agent, and a CI job all reach one index over HTTPS with a scoped OAuth token across three linked repositories reindexed by a push webhook
The same MCP tool call, two places for the index to live. The local shape ends at the checkout; the hosted shape spans repositories and callers. Link to this diagram Open full size
## What the laptop pays Building an index costs compute, and the bill lands somewhere. A language-server backend is the cheap end: the servers already exist, they update incrementally, and on a mid-size repository you will not notice them. On a large monorepo, cold-starting them competes with your build for the same cores and memory. Semantic search is the expensive end, and it is where "local" quietly stops being local. Zilliz's claude-context is an MCP plugin that "adds semantic code search to Claude Code and other AI coding agents", and its setup asks you for two things it will not run itself: an embedding provider (an OpenAI key, among others) and a vector database, either Zilliz Cloud or a Milvus deployment you stand up ([zilliztech/claude-context](https://github.com/zilliztech/claude-context)). The plugin is on your machine. The embedding compute and the vector store are somewhere else, because the models are large: nomic-embed-code lists 7B total parameters ([nomic-ai/nomic-embed-code](https://huggingface.co/nomic-ai/nomic-embed-code)). So the local line is rarely clean. The useful question about any "local" indexer is which half of the work runs on your hardware: the tool call, or the parsing and embedding behind it. ## Freshness and who runs the reindex Local wins this one outright, and it is the strongest argument in its favor. Augment puts the claim on their own product page: "Our indexer runs locally on your machine. When you make local changes, your next context query immediately reflects those changes" ([Augment, Context Engine MCP](https://www.augmentcode.com/product/context-engine-mcp)). Their docs describe Local mode as indexing "in real-time as you edit, no manual sync required" ([Augment docs, Context Engine MCP](https://docs.augmentcode.com/context-services/mcp/overview)). An index sitting on the same disk as the file you just saved has no staleness window to close. A hosted index starts from behind. Its natural trigger is a push, so everything between your last save and your last push is invisible to it unless the product does something about that. Symvanta reindexes on a GitHub push webhook, indexes tracked feature branches so an agent can pin a session to one, and takes uncommitted working-tree edits through a `ref` call that overlays them on a synthetic revision. That shrinks the window to near zero. It is still engineering someone had to do, where a local file watcher gets it free. If your agent spends its day on code you have not committed, weigh this heavily. ## The edge a single-repo index cannot see A local index is bounded by what sits on disk, and that bound stays invisible until a question crosses it. A service calling another over HTTP has a real dependency on it, and parsing repo A never reveals which handler in repo B answers that path. Same for a queue producer and the consumer draining its channel, or two services writing one table. Answering those locally means both repositories checked out, both indexed, and something joining them: the hosted architecture rebuilt by hand on a laptop. The vendor shipping the local indexer says this out loud. Augment's docs recommend Remote mode for "Cross-repo context" and "CI/server environments" ([Augment docs, Context Engine MCP](https://docs.augmentcode.com/context-services/mcp/overview)). Their local indexer is good at the machine it runs on; their answer for the code on the other side of the call is the hosted one. Symvanta links repositories inside a project and matches HTTP call sites to the routes they hit, queue producers to their consumers, and SQL access to the ORM model that owns the table. The tool-by-tool version of that sits on the [Augment Code alternative](/compare/augment-code-alternative) page. ## One index, every seat Eight developers running a local indexer build eight indexes of one repository on eight machines, each paying its own parse, and none can answer a question for anyone else. Give each developer a second agent and you have sixteen. A hosted index is parsed once and read by everyone holding a token. That matters most for the callers with no laptop attached: a CI job checking the blast radius of a diff before it merges, or a scheduled agent opening a pull request overnight. Augment's docs point CI and server environments at Remote mode for exactly this. A stdio server in CI means checking out and indexing the repository on every run, so a two-second query rides behind a cold parse each time. One shared index also means one shared answer. When your agent and a colleague's agent disagree about who calls a function, that is a bug in one of two local indexes, and nobody will ever find it. ## Auth is the part people skip The MCP authorization spec draws this line for you: "Implementations using an HTTP-based transport SHOULD conform to this specification. Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment" ([MCP specification, Authorization](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)). Read that as an access-control statement. A local server's permission model is your user account. It reads whatever you can read, and its only credential is whatever sits in its environment. On your own laptop that is usually right. It is also nothing an administrator can scope, audit, or revoke. The HTTP side is built on OAuth 2.1. Clients must implement PKCE, tokens must carry a `resource` parameter naming the server they were issued for (RFC 8707), servers must reject tokens issued for anyone else, and 403 is reserved for "Invalid scopes or insufficient permissions" ([MCP specification, Authorization](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)). Sourcegraph's server supports OAuth 2.0 through Dynamic Client Registration or pre-registered clients, and organizations requiring administrator approval can disable dynamic registration ([Sourcegraph MCP server docs](https://sourcegraph.com/docs/api/mcp)). Symvanta runs OAuth 2.0 with PKCE, which is why the [Zed setup](/integrations/zed) carries no `Authorization` header to paste: the browser flow issues a scoped session, and the account that owns it can revoke it. ## When local is the right call Some code cannot leave the building. Air-gapped networks and classified work, or a contract that forbids egress in writing. That is policy, and no freshness or cross-repo argument outranks it. A local LSP-backed server is the entire answer there, and Serena's free language-server backend costs nothing and asks for no account. One developer on one repository of moderate size gets little from a hosted index, because the questions needing cross-repo edges never come up. A weekend project does not want an account or a billing relationship. An agent working offline, on a plane or behind a captive portal, needs the index on the same disk as the code. In each of these, the hosted version is overhead. The two also compose. A local symbol server for the file you are editing plus a hosted graph for questions reaching past your checkout is a reasonable setup, and MCP clients hold several servers at once by design. ## Where the index belongs Local's advantages are properties of one machine: unsaved-code freshness, zero egress, no account. Hosted's are properties of a team: cross-repo edges, one parse for eight seats, an agent running in CI, a token an administrator can revoke. No amount of tuning a laptop index produces the second list. That is the case for defaulting to hosted. A code index has the same shape as CI: expensive to build, cheap to read, wasteful to duplicate per laptop, and useful to more callers than the person who triggered it. Nobody runs their test suite only on the machine that wrote the code. The freshness gap is the one genuine cost, and it yields to engineering: push webhooks, branch-aware indexing, and a working-tree overlay for edits that have not landed. The cross-repo gap on the local side does not yield the same way, because the missing data is a repository you never had. Symvanta is that bet: your repositories parsed into a graph of symbols and edges, reached over an authenticated MCP endpoint by whichever agent asks, current on every push and pinnable to a branch. The claim is cheap to test, and it should be tested on code you maintain. Connect one repository, point your agent at the endpoint, and ask what breaks if you change a function two services share. Seven days is plenty of time to find out whether that beats what your laptop gives you. --- ### Where Claude Code Sessions Spend Tokens https://symvanta.com/blog/where-claude-code-sessions-spend-tokens Anthropic published [Maximizing the value of your Claude Code sessions](https://claude.com/blog/maximizing-the-value-of-your-claude-code-sessions) this week. It explains what a session bills: prefill and decode, cache reads and writes, and which habits decide how many tokens each turn drags along. Worth reading in full. What struck us is how much of the bill it describes is exploration, the agent hunting for code before it can change anything. That part of the bill has a fix outside the session: resolve the question in a database and send back the answer instead of the search. ## What a session actually bills Input tokens are cheap because the model reads them in parallel. Output tokens generate one at a time, which is why they cost about five times as much. Caching discounts the input side: a cache read costs a tenth of the normal input price, a write up to double, and the cache expires after an hour on a subscription. Model and effort level multiply all of it, so the post says to set both once at the start of a session; switching midway breaks the cache and re-bills the whole prefix. The line worth keeping is about session shape: "One long session costs more than the same work spread over a few short ones, and by more than you'd think, because turn 40 is also re-reading the 39 turns before it." A file read on turn 3 is still there at turn 40, discounted while the cache holds, full price when it breaks. ## The advice converges on one habit Each tip protects the window from a different direction. @-mention files so they land in the first request without a Read call. Put quiet flags on chatty commands, and write them into CLAUDE.md so the agent picks them up on its own. `/clear` between unrelated tasks, `/compact` before a long break, `/rewind` to drop turns that led nowhere. Push log-reading into a subagent so the noise never touches your main window. Check `/context` in a fresh session and turn off MCP servers you don't need. All of it is the same move: keep things out of the window, because whatever gets in stays in, and bills again on every turn. ## The reference you cannot type The @-mention tip has a precondition: you already know which file matters. On code you wrote last month, sure. The expensive sessions are the ones where the agent has to find the code first, and finding is where the window fills up. Watch a transcript of that. A text search comes back with forty matches, pasted in full. Figuring out which ones are live call sites means opening files, and every opened file sits in the conversation for the rest of the session. "Who calls this function" has a three-line answer; the search route to it can cost tens of thousands of tokens, and per the arithmetic above, those tokens land on every later turn's bill too. We covered the attention cost of this loop in [context engineering for coding agents](/blog/context-engineering-for-coding-agents). This post adds the price tag. A graph lookup skips the loop. Symvanta parses the repository into symbols and edges, so "who calls this" is a traversal, and the answer is a short list of file paths, line bounds, and signatures. We measured this on cal.com at commit `176037d` for [an earlier post](/blog/repo-wiki-vs-code-graph). "Is this 19-line helper safe to edit" came back as 2 direct callers and a blast radius of 77 symbols across 66 files, named, in a few hundred tokens. That's the precise reference the @-mention tip wants, produced by a tool when you don't have a path to type. Source enters the window once, at the spans the graph pointed to.
Two sessions of four turns drawn as bars. In the search-and-read session each turn's bar grows taller, because every turn resends the search output and files earlier turns pulled in. In the graph session the bars stay small: lookups return short caller lists and one located span, so each turn resends almost nothing.
Each turn resends what earlier turns let in, so the bill is the area under the bars. Search output and opened files stack; graph answers barely move it. Link to this diagram Open full size
## The subagent tip, taken further A subagent gets its own window, does the messy work there, and only its conclusion comes back. The post suggests it for reading logs, and it's fair about the trade: a subagent sometimes re-reads things the main session already had. Exploration fits the same shape. Search output and opened files exist to answer a question, and none of it needs to outlive the answer. A graph server goes one step further. The traversal behind a caller list runs in the database, so the intermediate work never enters any window at all, yours or a subagent's. Behavior questions ("how does retry work here") run the same way: retrieval and synthesis happen server-side, and the session gets back an answer with citations. The subagent tip moves exploration somewhere cheaper. Retrieval on the server removes it from the bill, and the subagent slot stays free for jobs that need a model actually reading in bulk. ## Tool schemas count too The `/mcp` advice cuts toward tool vendors, ours included. A connected server loads its tool definitions and instructions into the session before you type anything, and if it never earns that back, turning it off is the right call. We ran that audit on our own server in July: cut the instruction block from about 3,000 characters to under 2,000, halved the tool descriptions, and shrank result rows for the text-search tool by about 40 percent. The math a server has to survive is simple. Its schema overhead has to come in under the exploration it replaces, and one avoided whole-file read covers a lot of tool definitions. Run `/context` with your own stack loaded and see what each server costs you at turn zero. ## What the tokens were for The post ends well: "Being efficient with tokens doesn't mean using fewer of them overall. It means making sure the ones you do use go towards the thing you actually asked for." In a coding session, what you asked for is the change. Exploration is overhead on the way there, and some of it is irreducible, since the agent has to know what it's changing. But that knowledge can arrive as forty pasted matches or as a caller list with line bounds, and only one of those is still billing you at turn 40. Open one of your own transcripts and count the file reads that were never referenced again. That number, times every turn that followed, is the part of the bill this post is about. Symvanta serves the caller-list kind of answer over MCP: callers, dependencies, blast radius, and test coverage from a live graph of your repositories, per branch, sized for the window. Setup for Claude Code and the other MCP clients is in [our integrations guides](/integrations/claude-code). [Start a free trial](/signup) and watch a session of yours run without the search loop: 7 days, no credit card. --- ### Cleaning Up a Vibe-Coded Codebase https://symvanta.com/blog/cleaning-up-a-vibe-coded-codebase Andrej Karpathy coined "vibe coding" in February 2025, describing a way of building where you "fully give in to the vibes, embrace exponentials, and forget that the code even exists." By November, [Collins Dictionary had named it word of the year](https://www.cnn.com/2025/11/06/tech/vibe-coding-collins-word-year-scli-intl). Somewhere between those two dates, a very large amount of production software got written that no human has ever read. What comes after has already arrived: one of the breakout searches around the term this year is "vibe coding cleanup specialist" ([Google Trends](https://trends.google.com/trends/explore?date=today%2012-m&q=vibe%20coding)). ## Cleanup became a job title 404 Media profiled [the engineers paid to fix vibe-coded messes](https://www.404media.co/the-software-engineers-paid-to-fix-vibe-coded-messes/): freelancers and small firms whose entire pipeline is AI-generated apps that outgrew their authors' understanding. Indeed now [explains the role to job seekers](https://www.indeed.com/career-advice/news/vibe-code-cleanup-specialist). Marketplaces match fixers with founders whose prototype reached paying customers before it reached review. Even people who enjoy the workflow draw the same line. Linus Torvalds vibe-coded the audio visualizer in his AudioNoise side project over the holidays ([Phoronix has the story](https://www.phoronix.com/news/Linus-Torvalds-Vide-Coding)), and [told The Register](https://www.theregister.com/2025/11/18/linus_torvalds_vibe_coding/) he is fine with the practice "as long as it's not used for anything that matters". Plenty of it was used for things that matter. The bill lands on whoever inherits the repository. ## Why inherited AI code is its own problem Engineers have always inherited messy codebases. Two things make the vibe-coded variety different. There is no author to ask. The person who "wrote" the code watched it stream past; the reasoning lives in a chat log that scrolled away months ago. The standard onboarding move, find the person who knows and buy them a coffee, has no target. The code is the only witness to its own design. And the mess has an unusual texture. Session-by-session generation means each conversation solved its problems from scratch: three date-parsing helpers, two retry wrappers, a hand-rolled auth check beside a half-wired auth library. Quality varies file by file in a way human tech debt rarely does, a clean, well-shaped module next to one that silently swallows errors, because each file reflects the context one session happened to have. This is the same visibility failure we describe in [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases), compounded over every session that ever touched the repo. There is also simply more of it. An agent produces code at a pace review practices were never sized for, and the inheritance moment arrives sooner: the cleanup market grew up within a year of the term being coined, which is faster than most codebases used to need a rescue. Both properties defeat reading as a strategy. Reading a repository end to end was already impractical. Reading one where structure may or may not exist, with nobody to confirm intent, is how cleanup engagements blow their estimates. ## Survey before you touch anything The first deliverable of a cleanup is a map nobody ever drew. A code graph computes one directly from the dependency structure: parse the repository into symbols and edges, run community detection over the result, and the modules that actually exist fall out, with the load-bearing functions ranked by how much of the graph routes through them and the places where two "modules" are secretly one tangle exposed as coupling. We generate this view for [the repositories in our architecture library](/architecture); on a vibe-coded repo it answers the first client question, "what did we actually buy", in an afternoon. The map also finds the duplication. Semantic search over the indexed code surfaces the three retry implementations as neighbors even when their names share nothing, which turns "I suspect there's duplication" into a list with file paths. The survey is also the deliverable that survives contact with stakeholders. "We found three hundred issues" starts an argument. A module map with named hubs, a ranked duplication list, and a blast radius per proposed edit reads as a plan, and it prices the engagement in units the person paying for it can check.
A four-step cleanup loop: map the modules with community detection, rank targets by fan-in and duplication, size each edit with callers and blast radius, then verify with tests and connect the agent to the graph so new code stops accruing the same way
The cleanup loop: survey the structure, rank the targets, size every edit before making it, verify, and wire the graph into the agent so the loop does not restart. Link to this diagram Open full size
## Size every edit by blast radius The dangerous cleanup edit is the one that looks local. Consolidating those three retry helpers into one reads as pure hygiene, until the callers of each turn out to sit on different assumptions about timeouts. Before every consolidation, rename, or deletion, ask [what breaks when you change this function](/blog/what-breaks-if-i-change-this-function): callers first, then the downstream set, then decide whether the edit is an afternoon or a project. The same numbers rank the backlog. High fan-in plus duplicated logic marks the profitable targets, the ones where one fix removes risk from many paths. Zero callers marks the safe deletions, and vibe-coded repos carry a lot of those, because sessions abandoned approaches without removing them. Tests deserve their own line item: these projects often ship with few or none, which removes the usual refactoring safety net. Until a test base exists, [blast radius](/blog/blast-radius-code-change-impact) is the available substitute: it tells you which paths could change behavior, so you know where to verify by hand and where the first tests you write will earn the most. ## Keep the next agent from re-vibing A cleanup that ends with clean code and an unchanged workflow has scheduled the next cleanup. The root cause was visibility: the agent writing file forty could never see the first thirty-nine, so it duplicated, diverged, and accreted, one plausible file at a time. The fix is the same graph, pointed forward. An agent connected to it over MCP resolves "add retry logic" to the retry helper that now exists, checks callers before changing a signature, and sizes its plan before executing it. That turns a one-time rescue into a workflow change, and it is the point where the cleanup stops being a cost center: the graph that paid for the audit keeps paying every session after it. ## Known limits A graph reads structure, and intent stays out of reach: it can tell you two functions are near-duplicates, and it cannot tell you which of the two behaviors was the requirement, so cleanup still involves a human deciding what the software is supposed to do. Community detection proposes modules; the proposal needs review before it becomes the target architecture. And in repositories that wire behavior through configuration or reflection, parsed edges understate reach, so treat a low blast-radius number in those areas with suspicion. If you have just inherited one of these repositories, start with the survey, because it is the part machines genuinely do faster: point Symvanta at the repo and the module map, the duplication list, and per-edit blast radius come back before the first billable week ends. The [free trial](/signup) runs 7 days, no credit card: long enough to survey the repo before you quote the work. --- ### Context Engineering for Coding Agents https://symvanta.com/blog/context-engineering-for-coding-agents Search interest in "context engineering" has roughly doubled over the past year ([Google Trends](https://trends.google.com/trends/explore?date=today%2012-m&q=context%20engineering)), and the questions driving it are definitional: what it is, how it differs from prompt engineering. The definition that stuck comes from Anthropic's engineering team, in [Effective context engineering for AI agents](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents): "the set of strategies for curating and maintaining the optimal set of tokens (information) during LLM inference." Prompt engineering asked how to phrase the instruction. Context engineering asks what deserves to be in the window at all. For coding agents the second question is the harder one, because the repository is the data source that always exceeds the window. ## The window is a budget Two findings anchor the discipline. Models degrade as context grows: Anthropic calls it context rot, and states it plainly: "as the number of tokens in the context window increases, the model's ability to accurately recall information from that context decreases." The degradation has an architectural cause. Attention computes pairwise relationships across tokens, so each added token stretches the model's attention budget thinner across everything already there. The practical stance that follows: treat tokens as spend. The goal, in the post's words, is "the smallest possible set of high-signal tokens that maximize the likelihood of some desired outcome". A token spent on a file the model needed nothing from is worse than wasted, because it also dilutes attention over the tokens that mattered. The budget is shared, which makes it tighter than it looks. System prompt, tool definitions, conversation history, and every tool result the session has produced draw from the same pool the task needs. For scale: a mid-sized production codebase runs to millions of tokens of source, so a 200,000-token window holds a low single-digit percentage of it, minus everything above. Whatever survives the cut is the agent's entire view of your system. ## How coding agents spend it today Watch a session transcript and the spend pattern repeats: whole-file reads to answer a one-line question, search output with forty matches pasted in full, directory listings three levels deep, and the residue of abandoned attempts nobody cleared. The agent pages the repository through its window hoping the relevant part sticks. On a small project this works, which is why the habit survives. On a large one it produces the failure mode we walked through in [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases): the model did not change, the signal density of its context did. Sub-questions are where the budget actually drains. "Who calls this function" answered by text search costs one grep plus opening every match to decide whether it is a live call site, a string coincidence, or a same-named method on an unrelated class. Every check pulls another file into the window. The question had a three-line answer; the route to it can cost tens of thousands of tokens, and those tokens stay in context for the rest of the session. ## Just-in-time context The direction Anthropic describes is a shift away from pre-computing everything relevant up front, toward "just in time" strategies: the agent keeps lightweight identifiers and retrieves data at runtime through tools. They note the approach mirrors human cognition, and any working engineer will recognize it. Nobody reads a repository before starting a task. You keep an index in your head (paths, names, a sense of what owns what) and look things up the moment a question becomes concrete. For code, the lookup layer does not need to be improvised, because the recurring questions have a known shape. Who calls this. What does it depend on. What sits downstream if it changes. Which tests stand over it. Each is a traversal over a graph of symbols and edges, and a parsed code graph returns each answer as a short list of identifiers with file paths and line bounds. This is retrieval by relationship, and it behaves differently from retrieval by similarity; the comparison has its own post in [code embeddings vs the code graph](/blog/code-embeddings-vs-code-graph). The economics compound over a session. An agent that answers its first sub-question cheaply arrives at the second with a cleaner window, so the second answer lands sharper too. A session that opens with three whole-file reads pays interest on them in every turn that follows, because those tokens keep occupying attention long after their moment passed. The longer the task, the more the retrieval layer matters. ## What a high-signal answer looks like We measured the shape on cal.com at commit `176037d` for [an earlier post](/blog/repo-wiki-vs-code-graph). The question was whether a 19-line error-building helper was safe to edit. The graph answered with 2 direct callers and a blast radius of 77 symbols across 66 files, named, including the API responders and both booking endpoints. The whole answer fits in a few hundred tokens, and it changed the plan. Reaching the same confidence by reading means opening every file a text search surfaced and tracing call paths by hand, with each opened file drawing down budget and thinning attention over the few lines that mattered.
Two context windows for the same task: a pre-loaded window nearly filled by whole files, search output, and a directory tree with a thin slice left for the task, and a just-in-time window holding four small graph answers with most of the budget still free
One window, two ways to spend it. Pre-loading fills the budget with files that might matter; just-in-time queries keep source out of the window until the graph has located the spans that do. Link to this diagram Open full size
## A loop that holds the budget The version of this we run in practice, with the graph served over MCP: 1. Orient with a computed module map, so the agent learns the shape of the system without pasting directory trees. 2. Resolve the task to symbols with one targeted lookup: a name, a route, a literal string. 3. Before any edit, pull callers and blast radius for those symbols. This step replaces the grep-and-open spiral. 4. Read source last, and only the spans the graph located. 5. Close by listing the tests that cover what changed. Steps 1 through 3 return compact, structured answers, so the window stays mostly free until step 4, the first moment real source enters it. That ordering is the entire trick: identifiers travel through the session, and source appears once, briefly, where the map points. The loop also degrades gracefully: when a step returns something surprising (more callers than expected, a test that should not exist), the agent spends a read early, on purpose. Curation does not mean starving the model. It means every file that enters the window is there because something located it first. Claude Code and the other MCP clients wire into this directly; setup for each lives in [our integrations guides](/integrations/claude-code). ## Known limits Curation cannot rescue a wrong plan; a perfectly trimmed window still executes the task it was given. Some questions are semantic ("where is retry handled, conceptually") and want meaning-level search before graph traversal takes over. And the graph must track the code: an index that trails the branch you are editing curates the wrong tokens with full confidence, so the graph has to be branch-aware and follow every push. Symvanta is the just-in-time layer in this loop: it parses your repositories into a live graph and serves callers, dependencies, blast radius, and test coverage to your agent over MCP, per branch, in answers sized for the window. [Start a free trial](/signup) and watch your own codebase come back as caller lists: 7 days, no credit card. --- ### React App Architecture and Project Structure https://symvanta.com/blog/react-app-architecture Every React project answers the same question in its first week and then lives with the answer for years: what goes in which folder. The answer used to matter for exactly one audience, the people on the team, and it degraded slowly enough that nobody had to defend it. A second audience arrived recently. Your coding agent opens the repository with no memory of last week and no sense of which directory is load-bearing. It reconstructs both from file paths and text matches, every session, from scratch. That changes what a good structure is worth. A layout a new hire figures out in a day is a layout an agent re-derives at the start of every task. The layouts below are the two that most React codebases end up with, what each one costs, and the dependency structure neither of them can express. ## Layer folders and feature folders The first shape sorts files by what kind of thing they are. `src/components`, `src/hooks`, `src/services`, `src/types`, `src/utils`. It is the shape every tutorial starts with and it is genuinely fine at twenty files, because at twenty files you can hold the whole thing in your head and the folder is just a filing cabinet. It stops being fine at the point where one change stops being one directory. Adding a field to the booking flow now means editing `components/BookingForm.tsx`, `hooks/useBooking.ts`, `services/bookings.ts`, `types/booking.ts`, and a test three levels away. The change is coherent. The tree scatters it. Every reviewer, and every agent, has to reassemble the feature in their head from five directories that share nothing but a word in the filename. The second shape sorts by domain. [bulletproof-react](https://github.com/alan2207/bulletproof-react/tree/9506629ed003a561c6627735480cce4994244bb4/apps/react-vite/src) is the reference layout a lot of React teams borrow from, and at commit `9506629` its Vite app splits `src` into `app`, `components`, `config`, `features`, `hooks`, `lib`, `testing`, `types`, and `utils`. The interesting directory is `features`, which holds `auth`, `comments`, `discussions`, `teams`, and `users`. Each domain keeps its own `api` and `components` directories, created only where it needs them: `teams` carries just `api`, `auth` just `components`. The booking-flow change lands in one place, and deleting a feature deletes its code. ## Colocation is what makes the feature folder pay The directory name does none of the work. What does the work is colocation: the rule that a piece of code lives as close as possible to the only place that uses it. A component used by one feature belongs inside that feature. A hook that encodes a domain rule belongs beside the domain. The shared directories hold what at least two features really need, and the bar for promoting something into them should feel slightly high, because every promotion widens the surface anyone can reach. The failure mode is a `src/components` folder that has quietly become the codebase. Forty components, half of them used exactly once, each one a candidate answer when someone searches for the thing that renders a discussion. Colocation keeps that folder small enough that its contents are actually shared, which makes the shared directory informative: if something sits there, more than one feature depends on it, and you should be careful with it. ## Folders name things; they do not enforce direction The rule that keeps a feature-sliced app from collapsing is about direction, and no directory tree can state it. Imports flow one way: `app` may reach into `features`, `features` may reach into the shared modules, and nothing flows back up. Feature-to-feature imports are the edge that quietly turns five modules into one. bulletproof-react writes that rule down as lint. Its [ESLint config at the same commit](https://github.com/alan2207/bulletproof-react/blob/9506629ed003a561c6627735480cce4994244bb4/apps/react-vite/.eslintrc.cjs) uses `import/no-restricted-paths` with one zone per feature blocking every sibling feature, then two more zones under the comment `enforce unidirectional codebase`: one stopping `features` from importing `app`, one stopping `components`, `hooks`, `lib`, `types`, and `utils` from importing either. `import/no-cycle` sits directly underneath, set to `error`. Those two rules are the entire architecture. Everything else is filing. A convention that lives in a style document is a convention that erodes on the Friday somebody needs one function from the neighboring feature and takes it. A convention that fails CI holds.
A dependency-direction diagram for a feature-sliced React app: the app layer imports from five feature folders, the feature folders import from the shared components, hooks, lib, types and utils modules, and two blocked edges show a feature importing a sibling feature and a shared module importing a feature
Allowed import direction in a feature-sliced React app, with the two edges bulletproof-react's import/no-restricted-paths zones block. Layout follows the src tree at commit 9506629; bulletproof-react is MIT licensed. Link to this diagram Open full size
## What the folder tree cannot tell you Discipline about direction gets you a long way and then stops, because the tree describes where files sit while the risk lives in which files reach which. React's own repository is the clearest illustration available, because nobody would call its layout careless. We indexed [facebook/react at commit `eafeac0`](/architecture/react) and Louvain clustering over its call edges groups the codebase into 381 functional modules at a modularity of Q=0.85, which is a clean separation for a monorepo shipping five products out of one source tree. It also contains 15 dependency cycles. The largest spans 116 files in the React Compiler's lowering passes, where the HIR, SSA, and reactive-scope stages reach back into each other's types on the way from a component to its memoized form. The next two are React DevTools' view layer (115 files), where panel components reference the views they render and the views reference the panels that host them, and the [Fiber reconciler](/architecture/react/fiber-reconciler) itself (91 files). None of those three is a defect. They are the expected shape of a multi-pass compiler, a UI layer, and a tree reconciler. The point is that you cannot see any of them by reading the directory listing, and no amount of tidying the directory listing would surface them. A cycle is a property of the import edges. The tree stores names. Your own app has the smaller version of this. Two features that never import each other still both call a shared hook that wraps a shared client, and a change to that client reaches both. The folder boundary held perfectly. The blast radius crossed it anyway. ## Structure for a reader with no memory Now put the agent back in the picture, because this is where the two audiences diverge. A person accumulates a model of the codebase over months. An agent starts every session at zero and rebuilds one from `grep`. That makes the feature folder worth real money: a task scoped to discussions has an obvious first directory, the files it needs sit next to each other, and the agent reads one subtree rather than sampling five. Distinctive names help for the same reason, and we went through what that is worth and what it costs in [should you rename your code for AI agents](/blog/rename-your-code-for-ai-agents). The ceiling shows up on the second question. Structure and naming make each round of search cheaper without changing how many rounds there are, and the number of rounds is set by how deep the call chain goes. An agent asked to change the shared client has to find the hook that wraps it, then the features that call the hook, then whatever those features expose. Every hop is another search, another set of files opened and mostly discarded. The [hooks dispatcher](/architecture/react/hooks-dispatcher) is a nice miniature of the problem: `useState` resolves through a dispatcher that is swapped at render time, so the function you end up executing is not the one whose name you searched for. ## When a dependency graph beats a folder convention A folder convention answers where should this live. A dependency graph answers what happens if I change this, and those are different questions that teams often try to solve with the same tool. The second answer comes from [dependency mapping](/use-cases/dependency-mapping): the call and import edges read back as a map of what reaches what. Keep the convention. Feature folders, colocation, lint-enforced direction: all of it is cheap at authoring time and it pays every day, for humans and agents alike. Add the graph for the moment before an edit, when the useful output is a list of the symbols and files downstream of the thing you are about to touch, resolved through imports and calls, cutting across whatever the directory tree suggests. That question is the one we take apart in [what breaks when you change a function](/blog/what-breaks-if-i-change-this-function). Symvanta gives your AI coding agent your codebase's real call graph over MCP, so it stops guessing and knows what breaks before it edits. Exact callers, dependencies, and blast radius for any symbol in your React app, served to Claude Code, Cursor, or [any other MCP client](/integrations), computed per commit so the answer matches the code on the branch you are working on. The quickest way to find out where your folder tree and your dependencies disagree is to point the graph at your own repository and read the two side by side: [start a free trial](/signup), 7 days, no credit card. --- ### Spec-Driven Development in a Real Codebase https://symvanta.com/blog/spec-driven-development-real-codebase Twelve months ago "spec-driven development" needed an introduction. Today GitHub's [Spec Kit](https://github.com/github/spec-kit) sits above 120,000 stars, [OpenSpec](https://github.com/Fission-AI/OpenSpec) above 60,000, AWS built a whole product around the workflow with [Kiro](https://kiro.dev/), and search interest has roughly quadrupled year over year, with "what is spec driven development" among the fastest-rising questions ([Google Trends](https://trends.google.com/trends/explore?date=today%2012-m&q=spec%20driven%20development)). The definition is quick. The hard case is a codebase that already exists, where the plan has to describe the present as accurately as the future. ## What spec-driven development is Spec-driven development puts the specification first and makes it the artifact the code is generated from. You write down what to build and why; an agent turns that into a technical plan; the plan becomes an ordered task list; implementation comes last, executed task by task with the spec as the reference for every decision. The tools agree on this pipeline almost exactly. Spec Kit walks it as slash commands: `/speckit.specify` for requirements and user stories, `/speckit.plan` for the technical approach, `/speckit.tasks` to cut the plan into actionable pieces, `/speckit.implement` to execute them. OpenSpec runs the same loop as change folders in plain markdown: propose a change, apply it, archive it when merged. Kiro phases it as requirements, then design, then sequenced tasks. Three independent teams, one conclusion: agents produce better software when the thinking is written down before the code, in a form a human can correct. Part of the surge is a correction. The industry spent 2025 [vibe coding](/blog/cleaning-up-a-vibe-coded-codebase), and spec-driven development is the swing back toward writing intent down before generating anything from it. ## Greenfield is the demo, brownfield is the job Every spec-driven demo starts from an empty directory, for a good reason: on greenfield the spec is the complete truth about the system. Nothing contradicts it, because nothing else exists. Most engineering happens somewhere else. The repository is five years old and enforces its opinions through ten thousand existing symbols. A spec describes the future; in a brownfield repo the plan step also has to describe the present, and describe it accurately: which symbols own the behavior being changed, what conventions the surrounding code follows, which tests already stand over the area. The pipeline has a slot for this work (Spec Kit's plan phase, OpenSpec's proposal, Kiro's design doc). What it lacks is a reliable way to fill the slot: the planning agent surveys the codebase the way any agent does, by searching and reading until the window fills up, and the survey's blind spots become the plan's blind spots. ## How ungrounded plans fail The failures are specific and they repeat. A plan schedules "add a retry helper" because the survey never surfaced the one in `lib/net`, and the codebase ends the week with two. A task says "change the signature of `resolveTenant`" and no task mentions its callers, because nobody enumerated them; execution discovers each one as a compile error, and the task list's tidy ordering dissolves mid-run. Two tasks look the same size in the document, yet one touches a symbol with three callers and the other a symbol with sixty and a downstream set that crosses a service boundary; the estimate, the ordering, and the review plan would all be different if that number had been on the page. We have written about the underlying question before: [what breaks when you change a function](/blog/what-breaks-if-i-change-this-function) is the question every task in a plan silently contains. What makes these failures expensive is where they surface. A wrong guess during planning costs a sentence to fix. The same wrong guess during implementation costs a rewrite of every task built on top of it, and agents are much better at following a plan than at noticing the plan itself is broken. ## The plan step is a survey For each task, a grounded plan needs four facts: the symbols the task touches, who calls them, what sits downstream of a change, and which tests cover the area. Each fact is a query with an exact answer. `find_node` resolves a symbol to its definition and signature. `relate` with kind `callers` enumerates the call sites; with kind `blast_radius` it walks the downstream set, the number that sizes a task before anyone commits to it (the case for that number is [its own post](/blog/blast-radius-code-change-impact)). `list_tests_for` returns the tests standing over a symbol, which tells the plan whether a task inherits a safety net or must build one. When the planning agent can run these over MCP, the plan phase stops being a reading exercise. `/speckit.plan` or an OpenSpec proposal comes out with file paths, line bounds, caller counts, and a named impact set attached to every task, and reviewing the plan means checking claims against a graph instead of trusting a survey nobody can inspect. The loop closes at the other end. After implementation, `diff_impact` takes the finished diff and reports which symbols, tests, and routes it actually reaches: the mechanical check that the change stayed inside the blast radius the plan declared.
The spec-driven pipeline from spec to plan to tasks to implement, with a live code graph feeding the plan step callers, blast radius, signatures, and test coverage, and diff_impact checking the implemented diff against the plan
The spec pipeline with the plan step grounded in the graph. Queries fill the survey slot during planning; diff_impact verifies the result against the declared impact set after implementation. Link to this diagram Open full size
## What this looks like in practice A concrete pass through Spec Kit's flow with a graph attached. The spec says: users should be able to cancel a booking within 24 hours without a fee. During `/speckit.plan`, the agent resolves the booking cancellation path, pulls callers on the fee-calculation function it intends to change, and finds a second consumer in the invoicing module that the spec never mentions. That discovery happens in the plan document, where it costs one clarifying question to the spec's author. The tasks that come out name both call sites, cite the three tests that cover the fee path, and flag the invoicing task as the risky one because its downstream set is four times larger. None of that required the agent to be smarter. It required the plan step to have access to the same facts a senior engineer would check before signing off on the estimate. ## Known limits The graph contributes no requirements. What to build and why stays a human document, and no traversal makes a bad product decision good. Archived specs drift the moment the code moves again, so treat them as history and the graph as the present tense. And parsed edges have blind spots (dispatch through string keys, reflection, handlers wired by configuration), so in codebases that lean on those patterns the impact set understates reality and the plan should say so out loud. Symvanta serves the survey half of this workflow: a live code graph over MCP that a planning agent queries mid-spec, branch-aware and cross-repo. If you run Spec Kit, OpenSpec, or Kiro against a codebase that existed before this year, the plan step is where the graph pays for itself. [Start a free trial](/signup) and run your next spec with the graph attached: 7 days, no credit card. --- ### Repo Wikis vs a Live Code Graph https://symvanta.com/blog/repo-wiki-vs-code-graph Hand an agent a repository it has never seen and the cheapest useful thing you can give it is a map. Generated repo wikis are good at making one. DeepWiki turns any public GitHub repository into a browsable wiki by swapping the domain in the URL, and the homepage promises "AI documentation you can talk to, for every repo" ([deepwiki.com](https://deepwiki.com/)). No login for public code, three MCP tools so an agent can read the wiki without a human in the loop, and a price of zero. For orientation that is a good deal. The gap opens at the first edit, when the agent needs callers and downstream effects, which a wiki was never built to store. ## What a generated wiki gets right A wiki is a compression of a codebase into topics a person can hold in their head. Someone new to a project wants to know what the major pieces are called, which one owns bookings, where the API surface lives. Prose answers that well because the question is about meaning, and meaning is what prose carries. DeepWiki's MCP tool set describes its own shape precisely. It exposes `read_wiki_structure` ("Get a list of documentation topics for a GitHub repository"), `read_wiki_contents` ("View documentation about a GitHub repository"), and `ask_question` ("Ask any question about a GitHub repository and get an AI-powered, context-grounded response") at `https://mcp.deepwiki.com/mcp`, with no authentication for public repositories ([Devin docs, DeepWiki MCP](https://docs.devin.ai/work-with-devin/deepwiki-mcp)). Three documentation verbs: list the topics, read the docs, ask about them. An agent that calls those arrives at a task knowing roughly where things live, which beats arriving blind. ## The question that falls through Then the agent picks up a ticket, opens a file, and the question changes from "what is this" to "if I change this line, what else has to change with it." That second question has an exact answer, and the answer is a set of relationships: the call sites that reach this symbol, the symbols downstream of those call sites, the tests standing over them. A wiki can describe a module accurately and still leave that set entirely unstated, because the set was never what the document was written to hold. Ask a wiki who calls a function and you get an answer synthesized from prose about the area the function lives in. Sometimes that is right. You cannot tell from the answer whether it is. The set the wiki cannot hold is exactly what [dependency mapping](/use-cases/dependency-mapping) records: every call site, resolved from the parse itself. ## Two answers to the same question in cal.com Cal.com, a repository anyone can open, turns thrown errors into HTTP responses in `packages/lib/server/getServerErrorFromUnknown.ts`, and near the bottom of that file sits a [nineteen-line helper](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/lib/server/getServerErrorFromUnknown.ts#L182-L200): ```ts function getHttpError({ statusCode, cause, traceId, tracedData, }: { statusCode: number; cause: T; traceId?: string; tracedData?: Record; }) { const redacted = redactError(cause); return new HttpError({ statusCode, message: redacted.message, cause: redacted, data: traceId ? { ...tracedData, traceId } : undefined, }); } ``` Suppose you want a trace id in `data` on every error, including the ones the caller never stamped. Small function, clear intent, one line of edit. The wiki page for that directory will tell you it normalizes unknown throws into `HttpError` instances with a status code and a redacted message. That is correct, and it is the same paragraph before and after you ask your question. Now the graph answer, measured against cal.com at commit `176037d`. `getHttpError` has 2 callers, both sitting in the file with it: the exported `getServerErrorFromUnknown` above it and `getServerErrorFromPrismaError` below. If callers were the whole story you would ship the edit in a minute. Its blast radius, walked outward from those two, is 77 symbols across 66 files, and the graph marks the set as crossing into the `api` layer. The names in it are the ones you would want on the review: `defaultResponder` and `defaultResponderForAppDir`, the tRPC handler factory `createNextApiHandler`, `RegularBookingService.createBooking` and `rescheduleBooking`, `StripePaymentService.refund`, and both booking endpoints under `apps/web/pages/api/book`. `getServerErrorFromUnknown`, the exported wrapper that calls it, carries 25 callers of its own.
One question about cal.com's server error module splits into two answers: a wiki page describing what the module does, and a blast-radius query returning 2 callers and 77 downstream symbols across 66 files for getHttpError
The same question, answered by a generated document and by a graph traversal. Counts measured on calcom/cal.com at commit 176037d via relate (kinds callers and blast_radius); cal.com is MIT licensed. Link to this diagram Open full size
The gap between those two numbers is the whole argument. Two callers in one file reads as a safe local edit. A downstream set that includes booking creation, reschedule, and a payment refund reads as a change that wants a second pair of eyes. Both answers describe the same nineteen lines. Only one of them changes what you do next. ## Generation time and question time Timing separates the two as much as content does, and it matters more over a long project. A wiki is written at generation time. Your question arrives later. Everything in between, every merge, every rename, every new caller someone added on Tuesday, sits in that gap. Wikis handle this with refresh: repositories that add a DeepWiki badge get regenerated when the code changes ([CognitionAI/deepwiki](https://github.com/CognitionAI/deepwiki)). Refresh shortens the gap and cannot close it, because a document is a thing produced once and read many times. A graph query runs at question time. `relate` walks edges that were parsed from the code as it stands on the revision you are pointed at, which is why the same call against a feature branch returns that branch's callers. The answer is computed for the question rather than retrieved from a document that anticipated it. This is the same distinction we drew for retrieval in [code embeddings vs code graph](/blog/code-embeddings-vs-code-graph): similarity finds text that reads like your query, traversal finds code that is connected to your symbol. ## Reading work and editing work The practical way to hold this: sort the work by whether the agent is about to write. Reading work is orientation, dependency triage, understanding a library you did not author, writing a design doc, answering "how does this project do auth." Prose is the right output shape, a public wiki costs nothing, and DeepWiki is very good at it. Point an agent there for open-source dependencies and it will save you real time. Editing work is renames, signature changes, deletions, migrations, anything where being wrong means a broken build or a silent behavior change in production. Here the useful output is a list of edges with file paths and line bounds the agent can act on: [what breaks when you change a function](/blog/what-breaks-if-i-change-this-function) is the shape of that question, and a graph is the thing built to answer it. Most agent sessions contain both, and the division that holds up is simple: the wiki for the dependency you did not write, the graph for the code you own. ## Known limits A parsed graph sees what the parser can resolve. Dynamic dispatch through a string-keyed registry, a handler wired up by configuration, a call made through reflection: those edges are weak or absent, and blast radius understates the true reach in codebases that lean on them heavily. Fan-in is also not proof of breakage. A blast radius of 77 means 77 symbols sit on a path through the thing you are changing, and many of them will pass through your edit untouched. The number sizes the review; it does not do the review. The wiki has the opposite blind spot and the opposite strength. It can tell you a module exists because of a migration that is half finished, which no edge in any graph encodes. It also indexes public repositories that you have not connected to anything, which is exactly why it is the fast answer for a dependency. Symvanta indexes your own code and hands your agent the graph over MCP: callers, dependencies, blast radius, cross-repo edges, branch-aware. If you want the tool-by-tool version against DeepWiki specifically, that is on the [DeepWiki alternative](/compare/deepwiki-alternative) page. If you want to see the numbers above computed against a repository you actually maintain, [start a free trial](/signup): 7 days, no credit card. --- ### Sonnet Plus a Code Graph vs Opus Alone https://symvanta.com/blog/sonnet-plus-code-graph-vs-opus Every AI coding bill carries the same quiet assumption: harder problem, bigger model. When an agent flails on a large codebase, the fix on offer is an upgrade to the flagship, and the flagship charges accordingly. The claim this post makes runs the other way: for most day-to-day engineering work, Claude Sonnet with a code graph behind it does better work than Claude Opus without one, at well under half the session cost. The pricing, the mechanism, and the arithmetic are below, and every number is checkable against your own workload. ## What the price sheet says Anthropic's published [model catalog](https://platform.claude.com/docs/en/about-claude/models/overview) puts Claude Opus 5 at $5 per million input tokens and $25 per million output tokens. Claude Sonnet 5 lists at $3 and $15, with introductory pricing of $2 and $10 through August 31, 2026 ([pricing page](https://platform.claude.com/docs/en/about-claude/pricing)). At list, Sonnet is 40% cheaper on both sides of the ledger. Under the introductory rate it is 60% cheaper. | Model | Input per MTok | Output per MTok | |---|---|---| | Claude Opus 5 | $5.00 | $25.00 | | Claude Sonnet 5 | $3.00 ($2.00 through Aug 31, 2026) | $15.00 ($10.00 through Aug 31, 2026) | Prompt caching scales off the same base rates on both models, with cache reads billed at roughly a tenth of the input price, so caching lowers both bills while preserving the ratio between them. Whatever your cache hit rate, an Opus session costs about 1.7x the identical Sonnet session. The sessions stop being identical once a graph removes the exploration turns. ## Agent sessions bill like meetings An agent conversation is a loop: the model calls a tool, the harness appends the result, and the whole transcript goes back over the wire for the next turn. Every file the agent opened in turn three is still being paid for in turn thirty. Input tokens dominate the bill for exactly this reason; on a long session, the output is a rounding error next to the accumulated context being re-sent. That structure decides where the money goes. A session's cost is roughly the average transcript size, times the number of turns, times the input rate. The model choice moves the rate. Everything else, the transcript size and the turn count, is decided by how the agent acquires context. An agent that greps, opens a 400-line file, discovers it was the wrong one, and opens two more has permanently widened its own transcript, and it keeps paying for that detour on every turn that follows. ## Most agent failures are context failures The case for the bigger model is that it reasons better, and that is true. Reasoning quality is rarely what sinks an agent on a production codebase, though. What sinks it is editing a function without knowing about the fourth caller, confidently importing an API that does not exist, or burning fifteen turns locating code a maintainer would have found in one. We walked through these failure modes in [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases); the short version is that the ceiling is set by what the model can see, and no amount of model capability recovers information that never entered the context window. There is public data on the size of that lever. A Google Research study on enterprise code migration held the task constant and swapped only the retrieval layer, standard vector RAG against graph-aware retrieval. API hallucination dropped from 56.4% to 16.2%, and dependency-resolution quality nearly doubled, from 34.8% to 65.9% ([Beyond Vector Similarity](https://research.google/pubs/beyond-vector-similarity-hierarchical-context-aware-graph-rag-vs-standard-rag-in-enterprise-code-migration/)). Three and a half times fewer hallucinated APIs, and the swing came entirely from context, with the model held constant. That is the dial the upgrade path ignores. ## What a graph hands a smaller model A code graph stores the structure a text search cannot see: nodes for symbols, edges for calls, imports, and implementations. Exposed over MCP, it turns the expensive gathering phase into single tool calls. Where is this defined: one `find_node` call returns the file, the line bounds, and the signature. Who consumes it: one callers query returns the resolved call sites, with comments and lookalike strings excluded. What breaks if the signature changes: one blast-radius query walks the edges transitively and returns the full impact set, the question we unpacked in [what breaks when you change a function](/blog/what-breaks-if-i-change-this-function). Each answer arrives as a compact envelope instead of a stack of file dumps. The transcript stays narrow, the turn count stays low, and the ambiguity a flagship model is supposed to power through never enters the session. That last part is the capability argument, separate from the cost one. Opus outruns Sonnet on long chains of uncertain reasoning. Feed the agent resolved facts and the chains get short: the task collapses from guessing a way through an unfamiliar codebase to applying a precise edit at known coordinates. Anthropic markets Sonnet 5 as [the best combination of speed and intelligence](https://platform.claude.com/docs/en/about-claude/models/overview), and short-horizon precision work is exactly that profile. The speed half compounds, too: the faster model with the shorter loop hands you a finished edit while the flagship session is still reading files. ## The session math
Two session timelines compared: an Opus-alone session with six exploration turns above a growing transcript wedge and a long cost bar, and a Sonnet plus code graph session with five precise tool calls above a flat transcript bar and a short cost bar
The exploration loop re-bills its own detours every turn, while graph lookups keep the transcript narrow: the meter runs slower and the session ends sooner. Link to this diagram Open full size
Here is the arithmetic on a concrete shape of task: rename a service method and update every caller, on a repository big enough that nobody holds it in their head. The session numbers are stated assumptions, so swap in your own; the prices are Anthropic's list prices from the table above. Assume the exploration-driven session takes 30 turns at an average of 60,000 transcript tokens per turn, which is 1.8M billed input tokens, plus 60,000 output tokens across edits and explanations. Assume the graph-backed session resolves the callers up front and finishes in 18 turns averaging 35,000 tokens, 630,000 input tokens total, with 45,000 output. | Setup | Billed input | Output | Session cost | |---|---|---|---| | Opus alone | 1.8M | 60k | $10.50 | | Sonnet alone | 1.8M | 60k | $6.30 | | Sonnet + code graph | 630k | 45k | $2.57 | The middle row is the boring part: same session, cheaper meter, 40% saved. The bottom row is the argument of this post. The graph changes the quantity while the model changes the rate: fewer turns, a narrower transcript, and every remaining token billed at the Sonnet price. Against the Opus-alone session that is roughly a 75% reduction, and under Sonnet's introductory pricing the same session lands near $1.70. With prompt caching on, every row shrinks by a similar factor and the ratios hold. Multiply by a team. Twenty engineers running five sessions like this per day is a difference of about $790 every day between the first row and the third, on assumptions you should absolutely re-derive from your own usage dashboard. ## Where Opus still earns the premium None of this retires the flagship. Genuinely ambiguous work still benefits from the strongest reasoning available: a multi-day refactor with shifting requirements, an architectural decision with sparse precedent, a long autonomous run where the agent holds a plan across hours. When the stakes carry that weight, the premium is cheap. The argument is about the default. The everyday loop of find, trace, edit, verify makes up most of what an engineering team asks an agent to do, and that loop is exactly the shape the graph compresses. Route the everyday loop to Sonnet with the graph behind it, keep Opus for the work that is hard because the thinking is hard, and the blended bill drops without the output quality following it down. The cheapest token is the one the agent never has to send, and the strongest model is the one holding the right context. Symvanta serves the code graph over MCP to whatever client your team already runs, [Claude Code](/integrations/claude-code), Cursor, and the rest of the [integrations list](/integrations), so dropping the default model a tier is a config change and the graph follows the agent everywhere. Run the arithmetic against your own repository: [start a free trial](/signup), 7 days, no credit card. --- ### Should You Rename Your Code for AI Agents https://symvanta.com/blog/rename-your-code-for-ai-agents There is a good argument going around that if you want AI coding agents to work well, you should write code they can find. Agents navigate a repository by running ripgrep, so a function called `create()` costs an agent more than one called `scheduleUpdateOnFiber()`. The first returns hundreds of matches the agent has to open and discard. The second lands on the definition and its call sites. From there the conclusion follows cleanly: naming, typing, and file layout are now performance characteristics of your codebase, and you should treat them that way. The mechanism is right. Agents really do retrieve by text search, and generic identifiers really do burn context. What deserves more attention is the bill, and who is in a position to pay it. Every number below comes from [facebook/react](https://github.com/facebook/react) at commit `eafeac0`, so you can clone that commit and check any of them. ## Text search really is the retrieval layer Give the argument its due first. Search React for the string `update` and the matches run past two hundred before you have looked at a single one, spread across the reconciler, the DOM bindings, the devtools backend, the release scripts, and the Flow type definitions. At roughly ten tokens a line before the agent has read a single surrounding function, that is a real slice of a context window spent confirming that most of the hits are irrelevant. React is a carefully maintained codebase with strong conventions, and the generic word still behaves this way. Now search for `scheduleUpdateOnFiber`, the function React calls when something has told a fiber it needs to re-render. Thirty-two matching lines, six files, all of them inside `packages/react-reconciler/src`, and every one of them relevant. Three words, unambiguous, no synonyms competing for the same idea. This is the discoverable-code argument working exactly as advertised, on a name that was chosen well years before anyone was optimising for agents. So the diagnosis holds. Where the argument gets interesting is the step from "generic names cost tokens" to "therefore rename your code." ## What the retrieval papers actually measured Two recent papers get cited in support of text search over structural retrieval, and both are worth reading closely because neither says quite what the summary says. [GrepRAG](https://arxiv.org/abs/2601.23254) (January 2026) studies repository-level code completion on CrossCodeEval and RepoEval. The task is assembling cross-file context for a single cursor position. Naive grep matched sophisticated graph-based baselines, which is the finding people quote. The version that beat them, by 7.04 to 15.58 percent relative exact match, is the optimised one, and what it adds on top of lexical retrieval is identifier-weighted re-ranking and structure-aware deduplication. Structure earned the win. The paper's own stated failure modes are noisy matches from high-frequency ambiguous keywords and context fragmentation from rigid truncation boundaries. [Is Grep All You Need?](https://arxiv.org/html/2605.15184v1) tests lexical against dense retrieval on LongMemEval, a long-memory conversational QA benchmark. Inline grep beat vector search for every harness and model pair tested, 83.6 to 93.1 percent against 62.9 to 83.6 percent. Then the authors changed how results were delivered to the model, from inline to file-based, and vector won five of ten pairs. Their conclusion is that retrieval in practice means retrieval plus orchestration, and that swapping the agent harness moves accuracy about as much as swapping the retriever does. Neither paper measured an agent trying to answer "who calls this" or "what breaks if I change this signature." Both measured find-the-relevant-span, which is the question text search is built for. The published discoverable-code experiments have the same shape: where is the retry backoff computed, where is the signature attached to the payload. Good questions, and single-hop ones. ## The rename has a bill, and not everyone can pay it Naming things well is free when the code does not exist yet. It costs almost nothing to call it `scheduleUpdateOnFiber` the first time, and React did. This is why the argument lands hardest with teams whose repositories are young, whose conventions are still soft, and in the strongest published case, whose code was largely machine-written from the start. That is the best case for the prescription, and it is a real one. The bill arrives when the code already exists. Renaming a shared symbol touches every call site, needs review from people who did not ask for the change, and rewrites the file's git blame in a way that makes the next incident harder to investigate. If the symbol crosses a package boundary you own, you break consumers you do not control. And a rename only reaches code you are allowed to change: vendored dependencies, generated API clients, the service that arrived with an acquisition, and the subsystem whose author left in 2021 all keep their vocabulary regardless of what your style guide now says. Partial coverage is where this shows up in practice. Refactoring a monolithic file into concept-named modules produces a measurable improvement on that file, and then the agent follows an import into the untouched helper module next door and is back where it started. The published experiments in this area report exactly that pattern, and it is the shape any incremental cleanup takes: the program works on the part you finished, and your agent keeps meeting the part you did not. A codebase-wide rename is one of those projects that is always eighty percent done. ## A good name still does not tell you what breaks Go back to `scheduleUpdateOnFiber`, the name that behaved perfectly a few paragraphs ago. Six files, thirty-two lines, no noise. Suppose an agent has been asked to change its signature and wants to know what it is about to break. Notice something about those six files: every one of them lives in `packages/react-reconciler/src`. Resolved call edges give five files reaching it directly, which lines up closely with the text search. Naming did its job again, and inside the reconciler the two methods agree. The disagreement starts one package over. Two functions in `react-dom-bindings` reach `scheduleUpdateOnFiber` through an intermediate hop, and neither file contains the string anywhere: ``` dispatchEvent ReactDOMEventListener.js:157 -> attemptSynchronousHydration ReactFiberReconciler.js:486 -> scheduleUpdateOnFiber ReactFiberWorkLoop.js:987 accumulateOrCreateContinuousQueuedReplayableEvent ReactDOMEventReplaying.js:184 -> attemptContinuousHydration ReactFiberReconciler.js:534 -> scheduleUpdateOnFiber ReactFiberWorkLoop.js:987 ``` Both are resolved `calls` edges at high confidence, and both cross a package boundary. Change how `scheduleUpdateOnFiber` takes its arguments and browser event dispatch is in the affected set, but no search for the function's name will tell you that, because the connection runs through `attemptSynchronousHydration` and `attemptContinuousHydration`. The name is perfect. It is simply not written in the files that depend on it.
A diagram showing that a text search for scheduleUpdateOnFiber returns six files all inside the react-reconciler package, while two resolved call chains starting in the react-dom-bindings package reach the same function through an intermediate hydration function, in files that never contain the string
Text search for `scheduleUpdateOnFiber` stops at the package boundary. Two resolved call chains cross it, starting in files that never spell the name. Measured on facebook/react at commit eafeac0. Link to this diagram Open full size
An agent working from text search alone sees the six and stops. To reach the DOM event layer it has to open the reconciler files, notice `attemptSynchronousHydration`, search for that name, find its callers, and only then arrive at `dispatchEvent`. That is the search-read-search loop the naming argument is trying to shorten. Better names make each round of the loop cheaper without reducing the number of rounds, because the number of rounds is set by how deep the call structure goes, and no naming convention flattens a two-hop path into a one-hop one. No naming convention closes that gap, because the gap is not a vocabulary problem. Connectivity lives in the edges between symbols, and a text index does not store edges. We went through the mechanics of that walk in [blast radius analysis](/blog/blast-radius-code-change-impact), and the same limit applies to embeddings for a different reason, covered in [code embeddings vs the code graph](/blog/code-embeddings-vs-code-graph). ## The read-side version of the same fix Both approaches are aimed at one root cause. An agent starts every session with no model of your codebase and rebuilds one from string matches, and everything about your repository that makes those matches noisy makes the rebuild more expensive. That framing is correct and it is the useful contribution of the discoverable-code argument. The write-side fix improves the strings. The read-side fix gives the agent something that was never a string: a prebuilt index of symbols and the resolved edges between them, so "who calls this" is a lookup that costs one tool call and returns a list, with no search-read-search loop and no dependence on what anyone named anything. It applies to the acquired service and the vendored client on the same terms as your newest module, because it reads structure out of the parse. The usual objection to structural retrieval is operational, and it is a fair one: a language server per language, on every machine, for every checkout, is real setup cost for uncertain gain. That objection argues against running the analysis locally. It says nothing about whether the structure is useful, and a hosted index that computes edges once per commit and serves them over [MCP](/integrations) removes the setup entirely. Symvanta is built that way for exactly this reason. ## Do both, and know which one you are paying for Write new code so it can be found. Distinctive names, precise types, modules named after concepts: all of it is free at authoring time and it genuinely lowers what an agent spends. The typing point in particular deserves more credit than it usually gets, because a type is checked and a comment is not, and a compiler error is feedback an agent can act on inside one turn. What is worth resisting is treating a rename program as the path to agent performance on a codebase you inherited. That work is expensive, it lands unevenly, and even where it lands it leaves the connectivity questions unanswered. Those questions are the ones that turn a twenty-minute task into a two-hour one, and they are answerable today without touching a single identifier. We wrote up the broader set of failure modes in [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases). The fastest way to see which of your agent's questions are naming problems and which are structure problems is to point it at your own repository and watch where it starts guessing: [start a free trial](/signup), 7 days, no credit card. --- ### What Breaks When You Change a Function https://symvanta.com/blog/what-breaks-if-i-change-this-function Every change to a function starts with the same question: what else moves if I move this? Say `getUser` needs a required `tenantId` argument. The edit itself takes ten seconds. The risk lives entirely in the code you did not open: the caller in another module that still passes the old argument list, the service in another repository that depends on the shape it returns, the test that pins the current behavior. Answer that question well and the change is routine. Answer it by guessing and you ship a green build that breaks something three hops away. ## Three questions hiding inside one "What breaks if I change this function" sounds like one question. It is three, and they have different answers. The first is who calls it: the direct call sites, the functions that name `getUser` and hand it arguments. The second is what depends on its behavior: not just the direct callers but whatever sits behind them and relies on the value flowing through, a dependency that never mentions your function by name. The third is what covers it: the tests that would catch the regression if you got it wrong, and whether they actually exercise the path you are about to touch. A rename usually needs only the first. A signature change like adding `tenantId`, or a change to what a function returns, needs all three. Most tools answer the first, approximately, and leave the other two to the person or the agent making the edit. All three read off the same [dependency map](/use-cases/dependency-mapping), which is why one index answers them together. ## Why find-references stops short Every IDE has a find-references command, and it is genuinely better than grep: it resolves symbols instead of matching strings, so it skips the comment that mentions the name and finds the call written across two lines. For a human refactoring inside one project with the whole thing loaded in the editor, it is often enough. It stops short in three places. It answers who calls `getUser` but not what depends on the behavior two hops out, so a transitive break stays invisible until it fails. It sees the project the editor has open, so a caller living in a separate repository that consumes this code as a dependency is off the map. And an autonomous agent cannot invoke it at all: the IDE index lives behind the editor UI, not behind a callable interface the agent can hit mid-run. So the agent falls back to grep and pattern-matching, which is where the misses start, and it is the same class of gap we walk through in [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases). ## The agent changed the stakes Impact analysis is not new. Static-analysis tools have walked dependency graphs for years and printed reports a human architect reads before a big refactor. That loop assumes a person in the middle who studies the report, weighs it, and decides. An agent removes the person from the middle. It reads the question, makes the edit, and moves to the next one, dozens of times in a session. There is no pause where someone reviews an impact report. So the answer to "what breaks" has to arrive as a callable primitive the agent hits before it writes the edit, in the time of one more tool call, or it never gets consulted. A report nobody opens is worse than useless here: it is latency with a false sense of safety attached.
A getUser() node on the left connects by three labeled edges to three cards: who calls it, resolving 14 sites including one routed through an interface; what depends on its behavior, the transitive and cross-repo blast radius; and what tests cover it, six tests from list_tests_for
The three questions hiding inside "what breaks if I change getUser": its callers, the transitive and cross-repo dependents of its behavior, and the tests that actually cover it. Counts in the diagram illustrate the worked example rather than measuring a specific repository. Link to this diagram Open full size
## Answering it in one call [Symvanta](https://symvanta.com/) turns each of the three questions into a call an agent makes over MCP against a graph built by parsing your code. For who calls it, `relate` with `kind: "callers"` returns the resolved call sites for `getUser`, including the one routed through an interface that grep never finds, with file paths and line bounds the agent hands straight to its editor. For what depends on the behavior, `kind: "blast_radius"` walks past the direct callers into the transitive set and, where repositories are linked in the same graph, across the repo boundary into every consumer. For what covers it, `list_tests_for` returns the tests that exercise the symbol, so the agent knows whether a safety net exists before it changes the thing under the net. The mechanics of that transitive and cross-repo walk are their own subject, and we cover them in [blast radius analysis](/blog/blast-radius-code-change-impact). The short version: a call becomes an edge because the graph is built from parsed calls and implementations, so the callers you get back are the real set, resolved through interfaces, with no comments and no false positives. It reads the same whether the agent driving it is Claude Code, Cursor, or [any other MCP client](/integrations). ## Where a parsed graph stops A parsed graph sees what the source says. It does not see everything the program does at runtime. A method resolved by string key from a dependency-injection container, a handler invoked by reflection from a config value, a plugin loaded by scanning a directory: these are real edges in the running system that a static graph can miss or show only in part. A tool worth trusting gets the ordinary cases right, plain calls and interfaces with no false negatives, and states plainly which dynamic cases it cannot fully resolve. Merging runtime signal, traces and logs, back into the static graph is how that last gap closes. One related trap is worth naming: reaching for semantic search to answer this. Similarity ranks how much two snippets resemble each other, a different signal from which symbol calls which, and it will return code that looks related but sits nowhere in the call path. We draw that line in [code embeddings vs code graph](/blog/code-embeddings-vs-code-graph). "What breaks if I change this function" is the question your agent should ask before every non-trivial edit, and the one it currently guesses at. [Start a free trial](/signup) and watch it answered against your own callers, your own interfaces, and your own repositories: 7 days, no credit card. --- ### AI Autofix: From Error Webhook to Draft PR https://symvanta.com/blog/ai-agent-fixes-production-errors An error tracker is a backlog of small, fully described bugs that nobody gets to. The stack trace names the file, the fix is usually a few lines, and it still loses the prioritization fight to whatever the roadmap says this quarter. We got tired of watching that queue grow, so we built autofix: a service that turns a production error webhook into a draft pull request, written by an agent that has never cloned the repository. The agent reads our code exclusively through the [Symvanta](https://symvanta.com/) MCP server, the same endpoint any [MCP-compatible agent](/integrations) connects to. It has been running against our own production since early June, and it opened its first correct draft PR the day it went live. ## From webhook to draft PR Three sources feed the pipeline. [Bugsink](https://www.bugsink.com/), the error tracker in front of our services, fires a webhook on every new issue. Grafana alerting fires one when a metric crosses a threshold. And a small poller watches the error log of our own MCP server, because a platform that indexes code for AI agents should be filing fixes for its own bugs with it. Every event is normalized into one shape: title, message, stack frames, and the repository it maps to. A fingerprint over that normalized error, with request ids and other volatile tokens stripped out, collapses duplicates, so a crash loop becomes one fix attempt instead of hundreds. Fresh fingerprints land in a queue, and a worker claims one attempt at a time. Guardrails run the whole way through: a cooldown per issue, a cap on open autofix PRs, and a daily token budget as the runaway backstop.
Autofix pipeline diagram: Bugsink webhooks, Grafana alerts, and the MCP error log feed a normalize-and-fingerprint step, then an attempt queue, then a headless agent run that reads code only through the Symvanta MCP; the agent's structured verdict either logs a canFix false analysis or passes validated edits to a draft PR
The autofix pipeline, from error webhook to draft PR. Link to this diagram Open full size
## The agent never sees a checkout Each attempt spawns a headless [Claude Code](/integrations/claude-code) run in print mode with exactly one tool surface: the Symvanta MCP. Everything local is stripped away: ```bash claude -p "$PROMPT" \ --mcp-config '{"mcpServers":{"symvanta":{"type":"http","url":"https://mcp.symvanta.com/mcp","headers":{"Authorization":"Bearer "}}}}' \ --strict-mcp-config \ --allowedTools "mcp__symvanta" \ --disallowedTools "Read,Write,Edit,MultiEdit,NotebookEdit,Bash,Glob,Grep,LS,WebFetch,WebSearch,Task,TodoWrite,ToolSearch" \ --output-format stream-json \ --max-turns 30 ``` None of that lockdown costs the agent anything: every read it would have made against a checkout (open a file, search a string, walk the callers) already exists as a graph call, so local file access is redundant the moment the repository is indexed. An early version of the prompt skipped over where the code lived, and the agent burned its turns hunting for a checkout before reporting that the working directory was empty. Disabling the filesystem tools was half the cure. The other half was saying out loud what does exist: the prompt now opens by stating that the repository lives only behind the MCP, that the working directory is intentionally empty, and that an empty directory is expected, never a finding. From there the workflow is what you would ask of a careful engineer. Resolve the symbols in the stack trace to exact definitions with `find_node`. Walk `relate` with `kind: callers` to understand what the surrounding code expects. Then pull the current source of any file worth changing through the `source` tool and build the edit against that exact text. The run ends with a structured verdict: ```json { "canFix": true, "rootCause": "one or two sentences on the underlying cause", "summary": "what the fix does, in plain language", "edits": [ { "path": "src/foo.ts", "oldString": "exact current text", "newString": "replacement", "why": "reason" } ] } ``` The service applies the edits, and it applies them defensively: every `oldString` must match the file at the branch head byte for byte, and a mismatch fails the attempt instead of committing a guess. Clean applies become a commit through the GitHub API and a draft PR whose body carries the root cause, the summary, and a citation trail of every graph call the agent made, so the reviewer can audit the reasoning alongside the diff. ## What it actually shipped The first live attempt set the tone. The day autofix went live, Bugsink reported a 500 in our own MCP text search: an empty query string was hitting a Zod validator that required at least one character, and the validator threw where it should have returned an empty match list. Seventeen turns later the agent had resolved the failing schema, traced the caller, and opened a draft PR dropping the `.min(1)` constraint and returning an empty match list early. It was the diff we would have written by hand. The second came from the MCP error log poller: a file-read tool handed a directory path crashed with a raw EISDIR instead of answering cleanly. The agent split the error handling so a directory path produces a clear envelope for the calling agent. Correct again, cited again. The miss matters as much. An internal 500 arrived whose error envelope carried no stack trace. The agent spent its full 30-turn budget searching, then returned `canFix: false` with its best root-cause analysis. That is the designed outcome for an under-evidenced error: a confident draft PR built on a guess costs a reviewer more than it saves. Evidence in, fix out. A typical attempt costs about a dollar in model tokens and runs a few minutes end to end. ## Why this needs the graph Strip the Symvanta MCP out of that pipeline and every step degrades. A stack frame names a function; `find_node` resolves it to the exact definition and signature, where a text search returns every string that happens to match. Understanding a bug means knowing who calls the broken code, and `relate` answers that from edges resolved at index time; we wrote up why that distinction decides correctness in [blast radius analysis](/blog/blast-radius-code-change-impact). The index updates on every push, so the `oldString` the agent copies out of `source` matches the branch head the service commits against. And because the graph spans linked repositories, an error whose cause sits one repo over is still reachable, a failure mode we covered in [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases). The security surface shrinks with it. The only credentials that touch your code are a Symvanta API key that can read the graph and a GitHub token scoped to opening draft PRs. No clone of your source ever lands on the fix box: no working copy on disk, no deploy key that can pull repositories, nothing for an attacker to tar up. Compromising the box yields two revokable tokens, and the incident response is two rotations. Compare that with any pipeline that starts by cloning: the moment a repo lands on an ops server, that server joins your source's attack surface, with all the patching, access control, and audit duty that comes with it. ## Build your own on Symvanta Autofix is a few hundred lines of service code around two APIs, and both are available to you today. The recipe: 1. **Index your repositories.** Create an account at [symvanta.com](https://symvanta.com), connect GitHub, and pick the repos. Webhooks keep the graph current on every push. 2. **Mint an API key.** Interactive editors sign in over OAuth, but a headless service wants a key: create one in workspace settings and send it as a Bearer token to `https://mcp.symvanta.com/mcp`. 3. **Wire an error source.** Anything that can POST a webhook works: Sentry, Bugsink, GlitchTip, or your alerting stack. Normalize to one shape and fingerprint before you queue, so repeats collapse. 4. **Spawn a headless agent per attempt.** Claude Code in print mode with the flags above works out of the box; any [MCP-compatible runner](/integrations) does. Give it the stack trace, the repository and project to scope to, and a hard output contract. 5. **Apply server-side, open a draft PR.** The agent proposes edits as data. Your service validates them against the branch head and opens the PR through your VCS API. The merge button stays with a human. And the rules that took us weeks of tuning, free of charge: - Disable every local tool and say why in the prompt, or the agent will spend its budget looking for a checkout that does not exist. - Demand the structured verdict by a deadline. "Emit the JSON block by your second-to-last turn" survived every prompt revision we made. - Fingerprint and dedupe before you spawn. Agent runs cost real money; a crash loop should cost you one attempt. - Fail closed on stale edits. If `oldString` no longer matches the head of the branch, the attempt dies with nothing half-applied. - Cap turns per attempt and tokens per day. The turn cap also forces convergence: an agent that knows its budget stops exploring and commits to an answer. - Dry-run into a chat channel first. We piped proposed fixes to Telegram and read every diff before letting the service open its first PR. ## Known limits Autofix works the long tail: validator bugs, missing guards, error-envelope handling, the fixes whose whole story is in the stack trace. Bugs that need a design decision, a schema migration, or product judgment end in `canFix: false`, and that is the correct ending; the root-cause analysis still lands in the attempt log, which speeds up triage even when no PR appears. Errors that arrive without a stack trace usually exhaust the budget the same way. And every PR it opens is a draft: review stays mandatory, because the agent's job is the first response, and merging is a human decision. If you want to see the graph that carries all of this against your own codebase, [start a free trial](/signup): connect a repository and run the same lookups the agent makes, on your own code. 7 days, no credit card. --- ### Why AI Coding Agents Fail on Large Codebases https://symvanta.com/blog/why-ai-coding-agents-fail-large-codebases An AI coding agent that nails a 200-line script will confidently break a 200,000-line monorepo, and it will sound just as certain doing it. The context handed to that agent stays flat text as the codebase around it turns into a graph of calls, imports, and dependencies the agent never gets to see. That gap breaks [Claude Code](/integrations/claude-code), Cursor, and Copilot in the same five ways: a context ceiling, grep-and-guess retrieval, stale embeddings, a missing call graph, and blindness past the repository boundary. ## The context ceiling Every agent runs against a token budget, and a large repo blows past it before the real work starts. Point an agent at a 50-file feature and it can read every file involved and hold the whole picture in its working context. Point the same agent at a 200,000-line monorepo and reading "everything relevant" stops being an option, because relevant is exactly what it doesn't know yet. Anthropic's own guide to Claude Code in large codebases admits this directly: ask for every instance of a vague pattern across a billion-line codebase and the agent runs out of context window before the work begins ([Claude Code in large codebases](https://claude.com/blog/how-claude-code-works-in-large-codebases-best-practices-and-where-to-start)). The same guide notes that its hierarchical CLAUDE.md convention, the standard workaround for feeding an agent project context, breaks down on codebases with hundreds of thousands of folders or legacy systems on non-git version control. A bigger window buys headroom. It doesn't decide what the agent reads first, and that decision is where accuracy gets made or lost. ## Grep-and-guess retrieval Lacking a map, an agent falls back on the tool every terminal has: text search. Grep finds the string `getUserById`. It doesn't know that `getUserById` is called through an interface, that three of its matches are comments, or that a fourth is a similarly-named method on an unrelated class. The same Anthropic guide concedes the failure mode without a language server in the loop: "Claude pattern-matches on text and can land on the wrong symbol." Pattern-matching on text is a reasonable first move for a human skimming a file. As the retrieval strategy behind an autonomous edit, it's a coin flip dressed up as an answer, and the agent hands you the result with full confidence either way. ## Embedding staleness Some agents pair grep with an embedding index: vectors over the codebase, ranked by similarity to the query. That helps with "find code like this" and does nothing for "is this index still true." Every commit changes what's true; most embedding pipelines reindex on a schedule rather than on every commit, so an agent working against yesterday's snapshot can recommend a caller that was deleted an hour ago. Similarity search also answers a different question than the one an agent usually needs answered: "what looks like X" isn't "what depends on X." We go deeper on that distinction in [code embeddings vs code graph](/blog/code-embeddings-vs-code-graph); the short version is that embeddings rank resemblance, and resemblance is a different signal than connectivity. ## No call graph The question an agent actually needs answered before it edits a shared function is who calls this, and what happens to them. Grep gives you a list of lines that contain the function's name. It does not give you the call graph: which of those lines are real invocations, which are dead code, which route through an interface with a different name at the call site. Without that graph, an agent renaming or changing a shared helper is working blind, and it ships the diff anyway. We cover the mechanics of this failure, and what a real answer looks like, in [blast radius analysis](/blog/blast-radius-code-change-impact). ## No cross-repo view Production systems rarely live in one repository. A backend service, a shared internal library, and two consumers of that library are four repos with one dependency graph running between them, and an agent operating repo-by-repo can't see that graph at all. It changes a function signature in the library repo, runs the library's own tests, and has no way to know a consumer three repos away now fails to build. This failure is structural, and it is where the current crop of code-context tools stops. A pre-computed index of a single repository, however fast or local, is bounded by that repository: the moment a dependency crosses a git boundary, the edge is missing from the index by construction. The boundary is exactly where the expensive failures live, because the seam between repositories is the one place no single team's tests cover end to end. The mechanism that closes the seam is the one that already answers the question inside a repository, pointed across a git boundary. Ask what breaks if you change a symbol and the answer arrives as a traversal with file paths attached. On [cal.com](/architecture/cal-com) at commit `176037d`, the private helper `getHttpError` has 2 callers, both in its own file. Its blast radius is 77 symbols across 66 files, reaching the API responders, the tRPC handler factory, and both booking endpoints. Index the consuming repository as well and its call sites join that same answer, where they would otherwise surface in next week's incident review. Grep cannot follow that traversal inside one repository, and a single-repo index cannot follow it out of one, however much it precomputes inside its own walls. ## Why bigger context windows don't fix it The industry's default answer to all of the above has been: wait for a bigger window. It helps at the margins. A ten-million-token window stuffed with unstructured file contents still leaves the agent without a way to know which of those files call which other files; you can widen the pipe without changing what flows through it. What closes the gap is structure: a representation of the codebase where "who calls this" and "what depends on this" are direct lookups. The bigger-model reflex has the same shape and a steeper bill; we run the session arithmetic in [Sonnet plus a code graph vs Opus alone](/blog/sonnet-plus-code-graph-vs-opus). ## What structure fixes This is the case for a code graph over a document pile: index the codebase into nodes (functions, classes, endpoints) and edges (calls, imports, implements, instantiates), and connectivity questions become graph traversals instead of educated guesses. The effect is measured. A Google Research study on enterprise code migration compared standard vector RAG against a hierarchical, graph-aware retrieval approach on real dependency-heavy migration tasks and found API hallucination dropped from 56.4% to 16.2%, and dependency-resolution quality rose from 34.8% to 65.9%, once the graph was in the retrieval loop ([Beyond Vector Similarity: Hierarchical Context-Aware Graph RAG vs Standard RAG in Enterprise Code Migration](https://research.google/pubs/beyond-vector-similarity-hierarchical-context-aware-graph-rag-vs-standard-rag-in-enterprise-code-migration/)). That's the gap between an agent that invents a plausible-looking API call and one that cites the real one. The same paper reports the trade: graph-aware retrieval scored worse on cyclomatic complexity consistency, 46.7% against standard RAG's 71.6%, and slightly worse on docstring preservation, with CodeBLEU identical at 91% for both. Structure bought correctness on dependencies and cost some tidiness elsewhere. [Symvanta](https://symvanta.com/) builds this graph directly: it indexes a GitHub repository into nodes and edges, layers semantic search on top with Qdrant embeddings for cases where meaning matters more than exact structure, and serves the whole thing over MCP so [any compatible agent](/integrations), Claude Code, Cursor, or otherwise, gets graph-precise answers. ## How this looks over MCP In practice an agent calls `find_node` to resolve a symbol to its exact file and signature, `relate` with `kind: callers` or `kind: dependencies` to walk the graph in either direction, `find_http_route` to jump straight from a path and method to its handler, and `ask_codebase` when the question is closer to "how does X work" than "where is X." Cross-repo edges mean a `relate` call against a shared library returns consumers across every indexed repository, current and otherwise. The agent asks the graph a precise question and gets a precise answer in one call.
A two-panel diagram contrasts text search, where the query getUserById fans out to four ambiguous matches (a real call, a comment, a similarly named method, and a deleted caller), with a code graph, where find_node resolves the same query to one exact definition and relate kind:callers returns real callers including one in another repository
Text search returns four ambiguous guesses for getUserById, while the code graph resolves one exact definition and its real callers, including one in another repository. Link to this diagram Open full size
## Known limits A graph is only as current as the last index, so freshness matters as much as coverage: an agent working on a stale index inherits the same wrong-answer problem it started with, just with more confidence behind it. A graph removes the guessing at the retrieval step; the judgment required to act on what it finds still belongs to the model. Any codebase with heavy runtime-only wiring, plugins loaded by string name, reflection-driven dispatch, has edges a static graph can miss or represent only partially. Pairing structure with a plain account of what it does and doesn't cover is the posture we take through the rest of this series. We publish this exact analysis for well-known open-source codebases: the [architecture pages](/architecture) walk symfony, etcd, prisma, and others through their real module maps. If you want to see this against a real repository instead of a slide, [start a free trial](/signup) and point it at your own codebase: 7 days, no credit card. --- ### Code Embeddings vs Code Graph for AI Agents https://symvanta.com/blog/code-embeddings-vs-code-graph Ask five developers whether AI coding agents should use embeddings or grep and you'll get five confident, contradictory answers, and almost none of them will mention the graph. Embeddings find code that looks like your query. Grep finds code that contains your exact string. Neither one can tell you who calls a function or what breaks if you change it, and that gap is exactly where agents keep failing on real codebases. ## Grep: fast, precise, blind to meaning Grep is the oldest tool in this fight and it's underrated for a reason: it's exact. Search for `resetPassword` and you get every line where that string appears, instantly, with zero infrastructure and zero staleness, because it reads the files as they exist right now. The limit is baked into what it does: grep sees characters and nothing else. It can't find the function that does the same job under a different name, and it can't distinguish a real call site from a comment that mentions the function in passing. A text match only confirms a line contains a string; whether that line is a real invocation is a separate question grep never answers. ## Embeddings: the similarity signal Embeddings solve a narrower version of the meaning problem. A vector index over a codebase can find `resetPassword` when you search for "how do I let a user recover their account," because the two are semantically close even with no shared vocabulary. That's a real capability grep doesn't have. It's also where the argument for embeddings in coding agents tends to stop, and where it gets overstated, including in editors like [Cursor](/integrations/cursor) whose codebase indexing is embeddings-based by default. Jason Liu's widely-read post on this made the opposite case: that RAG, in practice embeddings, was a mistake for coding agents, and that agentic grep paired with a capable model beats a filtered, chunked embedding pipeline for finding code ([Why I Stopped Using RAG for Coding Agents](https://jxnl.co/writing/2025/09/11/why-i-stopped-using-rag-for-coding-agents-and-you-should-too/)). The core complaint holds up: embeddings chunk code into fragments, rank them by resemblance, and hand the model disconnected pieces that lose the surrounding structure a senior engineer would read as a whole. For "find the function that handles X," a capable model exploring the repo directly, reading imports, following directory structure, often beats a filtered vector search that hands back the top five most-similar snippets and stops there. What that framing leaves out is the graph. The comparison is grep versus embeddings, both of which answer "what code is relevant to this query" from two different angles, exact match or similarity. Neither answers "what is this code connected to." That's a different question, and it needs a different structure to answer.
A two-panel diagram where the left panel shows code vectors clustered by similarity around a semantic query with resetPassword as the nearest match, and the right panel shows the same resetPassword symbol linked to its callers through resolved call edges
Embeddings rank code by resemblance to a query, while the code graph links the same symbol to its callers through resolved edges: the two compose rather than compete. Link to this diagram Open full size
## The questions embeddings cannot answer "Who calls `UserService.delete`" is a connectivity question, and cosine similarity was never built to answer it. The callers of a function don't necessarily read as similar to the function itself; a controller invoking `delete` might share almost no vocabulary with the delete method's own implementation. Rank by similarity and you surface functions that talk about deleting users conceptually, which may or may not overlap with the actual call sites wired into the running system. "What breaks if I change this function's signature" needs the transitive closure of a call graph: the callers, their callers, and so on until the walk terminates. A ranked list of nearby vectors stops at the first hop, if it finds the right hop at all. We walk through exactly what that answer looks like, and why grep and embeddings both miss it, in [blast radius analysis](/blog/blast-radius-code-change-impact). Connectivity questions need edges, and content stores, whether grep's file index or an embedding's vector store, don't keep edges around. Keeping those edges and reading them back is what [dependency mapping](/use-cases/dependency-mapping) does, from a single symbol's callers up to the module map. | Question | Grep | Embeddings | Graph | |---|---|---|---| | Find an exact string or config key | Yes, instantly | Weak, ranks by meaning not exact match | Yes, via symbol or text lookup | | Find code by meaning, unfamiliar vocabulary | No | Yes | Yes, via semantic search | | Who calls this function | Partial, string matches only | No | Yes, resolved edges | | What breaks if this changes | No | No | Yes, transitive closure | | Freshness after a commit | Always current, reads live files | Stale until reindex | Current if edges recompute incrementally | ## Embedding staleness vs a live graph There's a second, more operational gap. An embedding index is a snapshot: it's built by encoding files and stored until the next reindex job runs. Most pipelines reindex on a schedule or on push, which means there's always some window where the index describes code that no longer exists. A graph carries the same staleness risk in principle, but the fix is more tractable: edges are cheap to recompute incrementally on a commit, because they come from parsing what changed rather than re-embedding everything that might be affected. A repository with branch-aware indexing compounds this: an agent working a feature branch needs the graph and the embeddings both current to that branch; a default-branch index three days behind reintroduces the staleness problem. We cover why this matters at the retrieval-strategy level in [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases): stale context is one of the concrete failure modes agents hit at scale, alongside the context ceiling and the missing call graph. ## What the data says The gap between similarity and connectivity shows up in benchmark numbers. A Google Research study on enterprise code migration ran standard vector RAG against a hierarchical, graph-aware retrieval approach on dependency-heavy migration tasks and found the API hallucination rate dropped from 56.4% to 16.2%, and dependency-resolution quality rose from 34.8% to 65.9%, once the graph was in the retrieval loop ([Beyond Vector Similarity: Hierarchical Context-Aware Graph RAG vs Standard RAG in Enterprise Code Migration](https://research.google/pubs/beyond-vector-similarity-hierarchical-context-aware-graph-rag-vs-standard-rag-in-enterprise-code-migration/)). Standard RAG in that study is doing what embeddings do best, ranking by resemblance, and it still hallucinates APIs at more than three times the rate of the graph-aware approach on tasks that are fundamentally about dependencies. That's the connectivity gap showing up as a number. The same paper is worth reading for what it does not claim. Graph-aware retrieval scored worse on cyclomatic complexity consistency, 46.7% against standard RAG's 71.6%, and a little worse on docstring preservation, while CodeBLEU came out identical at 91% for both. The graph fixed the dependency problem it was aimed at and left the rest roughly where it found it. ## The conclusion: they compose None of this makes grep or embeddings obsolete. Grep is still the fastest way to find an exact string, a config key, an error message. Embeddings are still the right tool for "find code like this" when the vocabulary doesn't match and a human, or agent, can't guess the exact name. What neither one does, individually or combined, is answer a connectivity question, because neither stores the edges between symbols. That's the graph's job: it adds the connectivity layer that grep and embeddings each lack on their own. [Symvanta](https://symvanta.com/) is built on that premise directly: it runs a code graph, nodes for symbols, edges for calls, imports, implements, and instantiates, semantic search over Qdrant embeddings, and text search side by side, and routes an agent's question to whichever one actually answers it. "Where is `resetPassword` defined" is a lookup. "What handles account recovery" is semantic. "What breaks if I change this" is graph. Serving all three over one MCP endpoint means an agent isn't stuck picking one strategy at the start of a session and living with its blind spots for the rest of it, whatever [MCP client](/integrations) it's running in. If you're weighing this against a context-engine tool like [Sourcegraph Cody](/compare/sourcegraph-cody-alternative), the question worth asking is whether the tool answers connectivity questions at all, before you get to which retrieval strategy it defaults to. For a live example of what graph structure surfaces that similarity search cannot, the [Meilisearch architecture page](/architecture/meilisearch) maps a search engine's own codebase, ranking pipeline and all. See it answer a real one on your own repository: [start a free trial](/signup), 7 days, no credit card. --- ### Blast Radius Analysis: What Breaks First https://symvanta.com/blog/blast-radius-code-change-impact An agent renames a shared helper, runs the tests it can see, and ships a diff that breaks three services it never looked at. Picture the shape of it: a helper with fourteen callers, of which grep finds nine, misses one routed through an interface, and has no way to know about the four living in a different repository. Blast radius, done right, is a call the agent makes mid-session, before it writes the edit, answered in the time it takes for one more tool call. ## What blast radius means for an agent Static-analysis desktop tools built for human architects have scanned dependency graphs overnight for years and produced impact reports for review before a big refactor. That workflow suits a human with a quarter to plan. An agent making dozens of small edits in a single session, the kind of long autonomous run [Windsurf Cascade](/integrations/windsurf) is built for, needs the same answer in milliseconds, as a callable primitive: give it a symbol, get back the direct and transitive dependents, in the time it takes to make one more tool call. ## The worked example: renaming a shared helper Say a shared helper, `formatCurrency`, needs a signature change: add a required `locale` parameter. An agent working from grep searches for `formatCurrency` and gets back every line that contains the string. Some of those are the real call sites. Some are a comment referencing the function by name. One is a call routed through an interface where the concrete implementation is named `formatCurrency` but the call site only ever references the interface method `format`, so grep never finds it at all. The agent, working from an incomplete and noisy list, either misses the interface-routed caller and ships a broken build, or plays it safe and touches every string match including the comment, which is its own kind of wrong. A graph-backed lookup resolves the same question differently. `formatCurrency` is a node. Every real call to it, including the one routed through the interface, is an edge, because the graph is built by parsing calls and implementations. Ask for the callers and you get the real set: the direct call sites, resolved through the interface where one exists, with no comments and no false positives. Ask for the blast radius and you get one layer further out: what breaks in the callers of those callers if the signature changes underneath them. The table below is the shape of the difference on this worked example, not a measurement of any particular repository: | Question | Grep on `formatCurrency` | Graph `relate(kind: callers)` | |---|---|---| | Direct string matches | Every line containing the name, comments included | n/a, resolves symbols not strings | | Interface-routed caller | Missed entirely | Found, resolved through `format` | | Comment mentioning the name | Included as a false positive | Excluded | | Transitive callers of callers | Not answered | Returned as the blast radius set | | Cross-repo callers | Not answered | Returned when repos are linked | This is the same shape of gap that shows up on every rename, signature change, or delete once a codebase has more than a handful of files and at least one interface in the mix. ## Direct vs transitive dependents Two different questions get asked here, and agents, along with the tools built for them, frequently collapse them into one. "Who calls this function" is a direct-dependents question: one hop out on the call graph. "What breaks if I change this function's behavior" is transitive: it has to walk beyond the immediate callers into whatever depends on those callers in turn, because a function two hops away can depend on behavior that never appears in its own source. A direct-callers answer is cheap and often sufficient for a simple rename. A real blast radius answer walks the full transitive closure and returns it as a set an agent can reason over. Both answers come off the same edge set, which is what [dependency mapping](/use-cases/dependency-mapping) builds and keeps current across the whole repository. ## Cross-repo blast radius The hardest version of this question crosses a repository boundary. A shared library ships from its own repo; two backend services and a CLI tool consume it from three other repos. Change a public method's behavior in the library and the blast radius extends into every consumer repo that calls that method, whether or not anyone remembered to check those other repos before merging. An agent working repo-by-repo, which is how most agents work by default, has no visibility into this. It needs the library repo and every consumer repo indexed into the same graph, with edges that cross the repository boundary the way real dependencies do. That's a structural requirement: no instruction in a prompt gives an agent visibility into repos it was never shown.
A blast radius diagram: a teal-highlighted formatCurrency node in repo A radiates blue edges to a direct-caller service, a transitive dependent test, and a direct-caller handler, a teal edge to an interface-routed caller resolved via format(), and teal cross-repo edges across a dashed boundary to a backend service and a CLI tool in a linked repo B
A signature change to formatCurrency and its impact surface: direct callers, a transitive dependent, an interface-routed caller resolved through format(), and cross-repo consumers reached across a linked-repo boundary. Link to this diagram Open full size
## How agents guess today Absent a real blast radius answer, agents fall back on a mix of grep and pattern recognition: search for the symbol name, read a few of the matching files, infer the rest from naming conventions and folder structure. It works often enough on small, well-organized codebases to build false confidence, and it fails quietly on the codebases where it matters most: large, inconsistently named, with interfaces and dependency injection in the mix. The agent ships the edit and reports it as done, confident by default because nothing in its process flagged uncertainty. ## Blast radius as an MCP call This is what a graph makes callable. [Symvanta](https://symvanta.com/) exposes a `relate` tool over MCP with `kind: "blast_radius"`: pass a selector for the symbol you're about to change and get back the callers, the transitive dependents, and, where the repositories are linked, the cross-repo callers too, resolved from parsed calls and implementations. The same `relate` tool answers narrower questions as well: direct `callers`, `dependencies`, interface `implementers`, so an agent can start with a cheap direct-callers check and only pay for the full blast radius when the change actually warrants it. Selectors accept a `nodeId` or a plain `symbol` name, and the response returns file paths and line bounds an agent can hand straight to its editor, whether that's Claude Code, Cursor, or [any other MCP client](/integrations). This is one instance of the broader pattern we cover in [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases): a graph turns a connectivity question into a lookup. Embeddings alone can't answer this kind of question, because similarity search ranks how much two snippets resemble each other, a different signal than which symbols call which; we go into that distinction directly in [code embeddings vs code graph](/blog/code-embeddings-vs-code-graph). If you're evaluating this space more broadly, see how this compares to [Augment Code](/compare/augment-code-alternative), another tool agents reach for when navigating unfamiliar code. ## Known limits A parsed call graph sees what's written in the source. It does not reliably see everything that happens at runtime. Dynamic dispatch through a container that resolves an implementation by string key at boot time, reflection that invokes a method by name computed from a config value, plugins loaded from a directory scan, these are real edges in the running system that a static graph can miss or represent only partially. The real fix for that class of problem is runtime signal, traces and logs, merged with the static graph. A blast radius tool worth trusting gets the static case right, ordinary calls and interfaces with no false negatives, and states plainly which dynamic cases it can't fully resolve. Blast radius is easiest to grasp on a codebase you can see: the [Cal.com architecture page](/architecture/cal-com) shows the kind of shared hub (its `HttpError` type) where a single change fans out across dozens of call sites. Want to see blast radius run against your own repository, with your own callers and your own interfaces? [Start a free trial](/signup): 7 days, no credit card. ## Comparisons ### Context7 Alternative: MCP Code Graph https://symvanta.com/compare/context7-alternative This page is for teams who wired Context7 into their agent to stop it hallucinating library APIs, and are now wondering whether the same MCP setup can answer questions about their own code. It cannot, and that is not a knock on Context7: the two tools solve different problems. Here is where the line sits. ## What Context7 does Context7 is an Upstash project that serves "up-to-date documentation for LLMs and AI code editors" ([context7.com](https://context7.com)). The problem it targets is stated plainly in its README: "LLMs rely on outdated or generic information about the libraries you use," which produces code examples based on year-old training data, hallucinated APIs that do not exist, and generic answers for old package versions ([upstash/context7](https://github.com/upstash/context7)). Its fix is to pull version-specific documentation and code examples straight from the source and place them into the prompt. The MCP server is open source under the MIT license, though the API backend, parsing engine, and crawling engine are private ([upstash/context7](https://github.com/upstash/context7)). It exposes two tools: `resolve-library-id`, which turns a library name into a Context7 id, and `query-docs`, which returns documentation for that id matched to your question ([upstash/context7-mcp](https://github.com/upstash/context7-mcp)). The agent names a library, Context7 hands back current docs and snippets for it. ## What Symvanta does [Symvanta](https://symvanta.com/) indexes your own repository into a live graph: nodes are symbols, edges are calls, imports, implements, and instantiates. It exposes that graph as MCP tools the agent queries directly: `relate` with `callers`, `dependencies`, `blast_radius`, `implementers`, `heritage`, and `chain` modes, plus `find_node` for symbol resolution and `find_http_route` for handlers. When an agent is about to change a function, it asks "who calls this" or "what breaks if I change this" and gets a graph traversal back, across repositories, on the branch it is working on. Embeddings and text search sit beside the graph for semantic and literal matching. That is a different question than the one Context7 answers. Context7 tells the agent how a library's public API is meant to be used. Symvanta tells the agent how your code is actually wired together. As [code embeddings vs. code graph](/blog/code-embeddings-vs-code-graph) puts it: docs and search tell you what looks related, a graph tells you what is connected. ## Private code Context7 covers more than public libraries now. The Pro plan adds private GitHub, GitLab, and Bitbucket repos, parsed into the same documentation-and-snippets index as public ones ([Context7 plans](https://context7.com/plans)). What that parses your repo into matters: the output is still documentation and code examples. Pointed at your own code, Context7 answers "how is this library used" and returns snippets; it does not return every caller of a method or a blast radius across services. Private libraries also do not refresh automatically, so a team triggers a rebuild whenever internal packages change. Symvanta indexes private code as its default job, and cross-repo is a first-class edge: it matches HTTP call sites to the routes they hit, SQL access to the ORM model that owns the table, and queue producers to their consumers, so a change in one service surfaces the callers it breaks in another. Indexing is branch-aware, down to uncommitted working-tree edits. ## Pricing, as of July 2026 Context7 Free is $0: public repos, OAuth 2.0, 1,000 API calls a month. Pro is $10 per seat/month with private repos, team collaboration, and 5,000 calls per seat (further calls at $10 per 1,000, private-repo parsing billed at $25 per 1M tokens). Enterprise is custom, starting around $30/user/month and scaling down toward $2.50/user for large teamspaces, with SOC-2, SSO, and self-hosted deployment ([Context7 plans](https://context7.com/plans)). Symvanta is per seat: Starter $19/month, Pro $29 per seat/month with a 7-day trial, Enterprise $99 per seat/month with a 15-seat minimum. The unit of value differs, so the numbers do not line up directly: Context7 meters documentation API calls, Symvanta charges for a seat that can query the graph as much as it wants. ## MCP support Both are MCP-native and both work with the clients you already run. Context7 lists Cursor, Claude Code, and VS Code, with "manual MCP installation for 30+ clients" ([upstash/context7](https://github.com/upstash/context7)). Symvanta is a hosted MCP endpoint with OAuth2 PKCE, URL-based, working with [Claude Code](/integrations/claude-code), Cursor, Windsurf, VS Code, Claude Desktop, and [any MCP client](/integrations). ## Run both There is no conflict here. A well-set-up agent holds both at once: Context7 for "how do I call this framework correctly," Symvanta for "what in our code depends on this and what breaks if I change it." One keeps the agent current on the libraries you import, the other keeps it grounded in the code you own. Teams that run both point Context7 at the public and internal libraries they consume, and Symvanta at the services they build. ## Side by side | | Context7 | Symvanta | |---|---|---| | What it provides | Up-to-date documentation and code examples for libraries | Live call graph of your codebase (callers, dependencies, blast radius) | | Primary job | Keep the agent current on library and framework APIs | Tell the agent what breaks before it edits your code | | MCP tools | `resolve-library-id`, `query-docs` (returns docs) | `relate` (callers, dependencies, blast_radius, implementers, heritage, chain), `find_node`, `find_http_route` | | Your private code | Pro and Enterprise parse private repos into docs and snippets; manual refresh | Indexed into a graph by default, branch-aware, cross-repo edges | | Cross-repo impact | Per-library documentation, not a graph | HTTP call to route, SQL access to ORM model, queue producer to consumer | | Pricing (as of July 2026) | Free $0; Pro $10/seat/mo; Enterprise custom | Starter $19/mo; Pro $29/seat/mo (7-day trial); Enterprise $99/seat/mo (15-seat min) | | MCP support | Cursor, Claude Code, VS Code, 30+ clients | Hosted endpoint, OAuth2 PKCE, any MCP client | | Open source | MCP server MIT; backend and parser private | Hosted, on-prem bundle for Enterprise | ## Who should pick Context7 If your main pain is an agent that hallucinates library APIs or writes against a version you upgraded off months ago, Context7 fixes exactly that, and the free tier lets you wire it in this afternoon. It is the right tool for keeping an agent current on the frameworks and packages you pull in. ## Who should pick Symvanta If the failure you keep hitting is an agent that edits your code confidently and misses a caller in another file or another service, that is a graph problem, and documentation retrieval does not touch it. That failure mode is the subject of [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases). Symvanta gives the agent your call graph over MCP so it knows what connects to what before it changes anything. Most teams comparing these end up wanting both. The one you cannot fake with better documentation is the graph of your own code. ## Frequently asked questions
What is the difference between Symvanta and Context7? Context7 serves up-to-date documentation and code examples for the libraries an agent imports. Symvanta indexes your own codebase into a call graph: callers, dependencies, and blast radius for the code you wrote, not the packages you depend on. Most teams that compare them end up running both.
Can I use Symvanta and Context7 together? Yes, there is no conflict between them. Context7 keeps an agent current on library APIs; Symvanta keeps it grounded in how your own code is wired together. A well-set-up agent holds both MCP servers at once.
Does Context7 index my own private code the way Symvanta does? Not quite. Context7's Pro plan parses private repos into documentation and code snippets, and private libraries need a manual rebuild when they change. Symvanta indexes private code by default, refreshed automatically on every push, and returns exact callers and cross-repo blast radius rather than documentation.
[Book a 15-minute demo](/demo) and ask it a real question about a real change. --- ### DeepWiki Alternative: MCP Code Graph https://symvanta.com/compare/deepwiki-alternative This page is for teams who used DeepWiki to get oriented in an open-source dependency (replace github.com with deepwiki.com, read the generated wiki, ask it a question) and are now asking whether the same tool can sit inside an agent working on their own private code. The two overlap on the surface: both index repositories, both answer questions over MCP. They diverge on the thing that matters when an agent is about to make an edit: the shape of what comes back. ## What DeepWiki actually is DeepWiki is Cognition's tool for turning a GitHub repository into a browsable wiki. Cognition is the company behind Devin, and DeepWiki is described as "the free public version of Devin Wiki and Devin Search" ([Cognition, DeepWiki](https://cognition.com/blog/deepwiki)). You reach it by replacing github.com with deepwiki.com in any repo URL, and the homepage frames the product as "up-to-date documentation you can talk to, for every repo in the world" ([deepwiki.com](https://deepwiki.com/)). Cognition says it has "already indexed over 50,000 of the top public GitHub repos" ([Cognition, DeepWiki](https://cognition.com/blog/deepwiki)). What it produces is documentation for a person to read. DeepWiki analyzes the codebase and generates a set of documentation topics and pages you navigate, plus a chat interface for asking questions about the code. Its own MCP tool names describe the shape precisely: `read_wiki_structure` "lists documentation topics for a GitHub repository," `read_wiki_contents` "views documentation about a GitHub repository," and `ask_question` queries the repo for an AI-generated answer ([Devin docs, DeepWiki MCP](https://docs.devin.ai/work-with-devin/deepwiki-mcp)). Repositories that add a DeepWiki badge to their README get the wiki refreshed automatically when the code changes ([CognitionAI/deepwiki](https://github.com/CognitionAI/deepwiki)). ## How each one gives an agent context [Symvanta](https://symvanta.com/) is not a wiki. It parses each repository into a live graph (nodes are symbols, edges are calls, imports, implements, and instantiates) and exposes that graph as MCP tools the agent calls while it works. An agent runs `relate` with kind `callers` and gets the exact set of call sites; it runs kind `blast_radius` and gets what would break, walked outward from the symbol across files and repositories; `find_node` returns a symbol's definition and signature by name. The answer is a set of graph edges the agent can act on, not prose it has to read and interpret. DeepWiki's `ask_question` is genuinely good at "explain how this repo works," and it is free, which makes it a fast way for a human or an agent to get oriented. What it returns is a synthesized explanation grounded in the generated wiki. When the agent's next step is an edit, the question changes from "explain this" to "who calls `updateInvoice`, and what breaks if I change its signature." That second question wants a precise, current list of edges, and that is what a graph is built to return. We draw the line in [code embeddings vs. code graph](/blog/code-embeddings-vs-code-graph): a generated explanation tells you what the code is about, a graph traversal tells you what is actually connected to it. ## The DeepWiki MCP server Both are real MCP servers, so this is where the comparison gets concrete. DeepWiki's is free, remote, and takes no authentication for public repositories. The streamable HTTP endpoint is `https://mcp.deepwiki.com/mcp`, and an older SSE endpoint at `https://mcp.deepwiki.com/sse` is deprecated ([Devin docs, DeepWiki MCP](https://docs.devin.ai/work-with-devin/deepwiki-mcp)). Cognition announced it in May 2025 as "completely free with no login or auth required" ([Cognition, DeepWiki MCP server](https://cognition.com/blog/deepwiki-mcp-server)). It exposes three tools, and the names give away the shape: `read_wiki_structure` lists the documentation topics for a GitHub repository, `read_wiki_contents` reads those pages, and `ask_question` returns an AI-generated answer about the repo ([Devin docs, DeepWiki MCP](https://docs.devin.ai/work-with-devin/deepwiki-mcp)). All three take a repository and give back prose. There is no named callers, dependencies, or blast-radius traversal in the public tool set. Private code goes through a second server. Cognition points private-repo users at the Devin MCP server at `https://mcp.devin.ai/mcp`, which needs a Devin API key (the `cog_` prefix; legacy `apk_` keys are rejected) and covers public and private repositories. It carries the same three wiki tools plus `list_available_repos`, alongside Devin platform tools for sessions, playbooks, knowledge, and schedules ([Devin docs, Devin MCP](https://docs.devin.ai/work-with-devin/devin-mcp)). The free endpoint reads public repos; private repos ride on a Devin subscription. Symvanta is MCP-first by design: the product is an authenticated MCP endpoint (OAuth2 PKCE, URL-based) plus a dashboard for managing what is indexed. It works with [Claude Code](/integrations/claude-code), Cursor, Codex, Windsurf, VS Code, Claude Desktop, or [any MCP client](/integrations). The tool set is graph-shaped: `find_node` for a symbol's definition and signature, `relate` for callers, dependencies, blast radius, implementers, heritage, and call chains, `find_http_route` for the handler behind a path and method, with semantic and text search alongside for the cases where matching is the right move. An agent that calls `ask_question` gets a paragraph it has to trust. An agent that calls `relate` gets a list of call sites it can open. ## Private repos and cross-repo DeepWiki covers private repositories through Devin. For public code the wiki and MCP server are free with no login; for a private repo you create a Devin account, connect your GitHub, and use the authenticated path ([Cognition, DeepWiki](https://cognition.com/blog/deepwiki), [Devin docs, DeepWiki MCP](https://docs.devin.ai/work-with-devin/deepwiki-mcp)). Devin is Cognition's paid agent product, so private-repo DeepWiki rides on that subscription rather than a standalone DeepWiki price. Symvanta indexes private code as the default case: connect a repository, get an MCP endpoint, point your agent at it. Cross-repo context comes from explicitly linking repositories in a project, including cross-repo HTTP-call edges where one service calls another's route, and indexing is branch-aware, so an agent can pin a session to a feature branch or query uncommitted working-tree edits. DeepWiki's public docs describe a per-repo generated wiki; they do not describe cross-repo transport edges or branch-level graph queries as features. ## Pricing, as of July 2026 DeepWiki's public wiki and its public MCP server are free, no login required ([Cognition, DeepWiki MCP server](https://cognition.com/blog/deepwiki-mcp-server)). Private repositories require a Devin account, which is Cognition's paid product; DeepWiki itself does not publish a separate private-repo price. Symvanta is per seat: Starter at $19/month, Pro at $29 per seat/month with a 7-day trial, Enterprise at $99 per seat/month with a 15-seat minimum. The two are not really priced against each other, because they sell different things: DeepWiki gives away a reading surface for open source and takes private repos through Devin, while Symvanta charges per seat for an agent-facing graph over your own code. ## Side by side | | DeepWiki | Symvanta | |---|---|---| | What it produces | Generated browsable wiki plus chat/Q&A for a repo | Live queryable code graph (symbols, calls, imports, implements) | | Built for | A human or agent reading up on a codebase | An agent making an edit and checking impact | | MCP tools | `read_wiki_structure`, `read_wiki_contents`, `ask_question` | `find_node`, `relate` (callers, dependencies, blast_radius, implementers, heritage, chain), `find_http_route` | | Named callers / blast-radius traversal | Not in the public tool set | `relate` (kind: callers, blast_radius) | | Public repos | Free, no login, 50,000+ indexed | Index your own repos | | Private repos | Via a Devin account (paid) | Default case, private by design | | Cross-repo / branch-aware | Per-repo wiki; not described in public docs | Linked repos with HTTP-call edges, branch-aware, uncommitted working-tree edits | | Pricing (July 2026) | Public free, private via Devin | Starter $19/mo, Pro $29/seat/mo (7-day trial), Enterprise $99/seat/mo (15-seat min) | ## Who should pick DeepWiki If you want to understand an open-source project fast, or give your team a readable, auto-refreshing wiki over a public dependency, DeepWiki is excellent and free, and the URL trick makes it frictionless. As an MCP tool it is a strong "explain this repo" source that any agent can call at no cost. For onboarding and comprehension over public code, it is hard to beat. ## Who should pick Symvanta If the agent's job is to change your private code and you want it to know who calls a function and what breaks before it edits, that is a different tool. Symvanta returns graph edges (callers, dependencies, blast radius) over your own repositories, cross-repo and branch-aware, through an authenticated MCP endpoint. Read [blast radius as a code-change impact check](/blog/blast-radius-code-change-impact) for the shape of the answer. The two can sit side by side: point an agent at DeepWiki to read up on a library, and at Symvanta to reason about your own code before it writes. ## Frequently asked questions
Is Symvanta a good DeepWiki alternative? For understanding an open-source repo, DeepWiki's free wiki and chat are hard to beat. For an agent about to edit your own private code, Symvanta is the better fit: it returns exact graph edges, callers, dependencies, blast radius, instead of a generated explanation. Many teams use both: DeepWiki for reading up on a dependency, Symvanta for reasoning about their own code.
What's the difference between Symvanta and DeepWiki? DeepWiki generates a browsable wiki and a chat Q&A tool for a repository, public or private through Devin. Symvanta is a live, queryable code graph: an agent calls `relate` and gets a precise list of callers or a blast radius, not prose it has to interpret.
Does DeepWiki cover private repositories? Yes, through a Devin account, which is Cognition's paid agent product. DeepWiki's free public wiki and MCP server need no login for public repos. Symvanta indexes private code as the default case: connect a repository and get an MCP endpoint, no separate paid agent product required.
The fastest way to see the second half is on a repository you actually maintain. [Book a 15-minute demo](/demo) --- ### Greptile Alternative: MCP Code Graph https://symvanta.com/compare/greptile-alternative This page is for teams that use Greptile for AI code review, or evaluated it, and now want an AI coding agent to have the code graph while it writes, ahead of the review that runs once the pull request exists. Greptile and Symvanta both attach to your codebase and both speak MCP, so it is easy to assume they compete head to head. They sit at different points in the same workflow. ## What Greptile does Greptile is an AI code reviewer. It watches pull requests, reads the diff against full codebase context, and posts review comments: multi-file logic bugs, security risks, style violations, and repo-specific rules written in plain English ([Greptile](https://www.greptile.com/)). Two pieces sit on top of that: TREX, which writes and runs tests for a PR in a sandbox to validate behavior at runtime, and the Greptile Agent, a set of parallel agents that assess a change's impact beyond the diff ([Greptile](https://www.greptile.com/)). Greptile calls itself the central validation layer for code changes, and that is an accurate description of what it does: it grades work an agent or a human already produced. ## The layer each one works at The role difference is the whole comparison. Greptile runs on the PR, after the code is written, and its value is catching what slipped through before the change merges. [Symvanta](https://symvanta.com/) runs while the agent is still writing. It parses each repository into a live graph (nodes are symbols, edges are calls, imports, implements, and instantiates) and exposes that graph as MCP tools the agent calls mid-task. Before an agent edits a shared function, it can ask Symvanta for the blast radius and get back every caller and dependent across files and repositories, so it knows what breaks before it touches anything. One product reduces bad diffs by reviewing them after the fact; the other reduces them by handing the author the map first. That distinction is the subject of [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases): the agent finds a plausible match and guesses what it is connected to. A reviewer catches some of those guesses once they are on the page; a graph keeps the agent from guessing to begin with. The tradeoff between search and graph is [code embeddings vs. code graph](/blog/code-embeddings-vs-code-graph): search tells you what looks related, a graph tells you what is actually connected. ## What each exposes over MCP Both ship an MCP server, and this is where the difference gets concrete. Greptile's MCP exposes its review product: eleven tools across custom context (coding patterns and standards), pull request data, review lifecycle, and comment search, so an agent in your IDE can pull unaddressed Greptile comments and apply the fixes without leaving the editor ([Greptile MCP docs](https://www.greptile.com/docs/mcp/overview)). It brings review feedback to the agent. Symvanta's MCP exposes the graph itself. `relate` answers callers, dependencies, blast_radius, implementers, heritage, and chain; `find_node` resolves a symbol to its definition and signature; `find_http_route` maps a method and path to its handler. Text search and Qdrant embeddings sit beside the graph for literal and semantic lookups. The agent is not reading someone's review of the code, it is querying the structure of the code directly. ## Pricing, as of July 2026 Greptile is self-serve with a free tier. Starter is free for a single active developer: unlimited repositories, 50 credits a month, where one credit is one standard review and three credits is one TREX review. Pro is $30 per seat per month with 50 credits included per seat and $1 per credit after that, unlimited users, custom rules, and unlimited connected apps. Enterprise is custom with a self-hosting option, SSO/SAML, and GitHub Enterprise support. Both paid paths carry a 14-day trial ([Greptile pricing](https://www.greptile.com/pricing)). Symvanta is also self-serve and per seat: Starter at $19 a month, Pro at $29 per seat a month with a 7-day trial, Enterprise at $99 per seat a month with a 15-seat minimum. Greptile's Pro price is per seat with review credits on top, so the bill tracks how many reviews you run as well as headcount. Symvanta's per-seat price tracks how many developers point an agent at the graph, independent of how many changes they ship. ## Cross-repo and deployment Greptile reviews a PR with context from that repository, works with GitHub and GitLab, and offers self-hosting on Enterprise ([Greptile](https://www.greptile.com/)). Symvanta links repositories into one project and matches the couplings a single-repo view misses: HTTP call sites to the routes they hit across services, SQL table access to the ORM model that owns the table, queue producers to consumers on the same channel. Indexing is branch-aware, so an agent can pin a session to a feature branch or query uncommitted working-tree edits. It runs as a hosted MCP endpoint by default, with an on-prem bundle for Enterprise, and works with [Claude Code](/integrations/claude-code), Cursor, Windsurf, and [any MCP client](/integrations). ## Side by side | | Greptile | Symvanta | |---|---|---| | Primary role | AI code review on pull requests | Code graph an agent queries while it writes | | Pricing (as of July 2026) | Starter free (1 dev, 50 credits/mo), Pro $30/seat/mo (50 credits/seat, $1/extra), 14-day trial | Starter $19/mo, Pro $29/seat/mo (7-day trial) | | Enterprise | Custom, self-host option, SSO/SAML | $99/seat/mo (15-seat min), on-prem bundle, customer-managed encryption keys | | What its MCP exposes | Review comments, PR data, custom rules (11 tools) | Graph traversal plus find_node, find_http_route | | Graph primitives (callers / blast radius) | Not exposed as tools; impact assessed inside the review | `relate`: callers, dependencies, blast_radius, implementers, heritage, chain | | When it runs | After the diff exists, on the PR | During the agent's own work | | Cross-repo | PR context per repository, GitHub/GitLab | Linked repos, HTTP/SQL/queue transport edges | | Runtime test validation | Yes (TREX, sandboxed) | No | | Self-serve signup | Yes | Yes | ## Who should pick Greptile If your main gap is code review, catching bugs and standards violations on every PR before it merges, and you want runtime validation that actually runs tests, Greptile is a focused, mature product built for exactly that. It fits a team that wants a consistent reviewer on every change and is happy to pull that review feedback into the editor over MCP. ## Who should pick Symvanta If you want the agent itself to know the code before it edits, to ask who calls this and what breaks if I change it and get a graph traversal back, and you want that across linked repositories and pinned to a branch, that is the gap Symvanta fills. Symvanta is the map the agent reads while writing the diff, so fewer bad ones reach a reviewer at all. The two are not mutually exclusive. A team can give its agents the graph through Symvanta and still run Greptile on the resulting PRs. The question this page answers is which one closes your current gap. ## Frequently asked questions
Is Symvanta a good Greptile alternative? Only if your gap is different from what Greptile solves. Greptile reviews pull requests after the code is written. Symvanta gives an agent the code graph, callers, dependencies, blast radius, to query while it is still writing. Many teams run both: Symvanta for the agent's context, Greptile for the review that follows.
What's the difference between Symvanta and Greptile? Greptile is an AI code reviewer that comments on pull requests. Symvanta is a code graph an agent queries over MCP before and during an edit. Greptile grades a diff after it exists; Symvanta hands the agent the map before it writes one.
Does Symvanta do cross-repo blast radius? Yes. Symvanta links repositories into one project and matches HTTP call sites to the routes they hit, SQL access to the ORM model that owns the table, and queue producers to their consumers, so a blast-radius query surfaces callers in another service, not just the current repo.
The fastest way to tell is on a codebase you maintain. [Book a 15-minute demo](/demo) and bring a real question about real code. --- ### Augment Code Alternative: MCP Graph Context https://symvanta.com/compare/augment-code-alternative This page is for teams already sold on the idea that an agent needs real code context beyond a bigger prompt, and who are now comparing Augment Code's Context Engine against a more graph-forward approach. Augment is the closest architectural neighbor Symvanta has, so the comparison is narrower and more useful than most. ## Where the two products actually differ Augment's Context Engine parses a dependency graph, indexes commit history, and generates embeddings, then retrieves against that index when an agent asks a question: fetch the relevant slice, hand it to the model ([Augment Code, Context Engine](https://www.augmentcode.com/context-engine)). The graph is real, but based on their public documentation, it functions as an input to retrieval rather than something the agent queries directly. Their docs describe semantic indexing and mapping of relationships across a codebase; they do not document direct graph-traversal tools such as "find every caller of this function" or "compute blast radius" as callable operations. [Symvanta](https://symvanta.com/) builds the same kind of graph (nodes are symbols, edges are calls, imports, implements, and instantiates) and then exposes it as first-class MCP tools: `relate` with `callers`, `dependencies`, `blast_radius`, `implementers`, `heritage`, and `chain` modes, plus `find_node` for symbol resolution and `find_http_route` for HTTP handlers. An agent does not get a pre-fetched context bundle assembled on Symvanta's side; it walks the graph itself, one traversal at a time, and can ask a second, more specific question if the first answer is not enough. Embeddings and text search sit alongside the graph for the cases where semantic or literal matching is the right tool. That difference matters most exactly where [code embeddings vs. code graph](/blog/code-embeddings-vs-code-graph) draws the line: embeddings are excellent at "find code that looks like this," and weaker at "tell me everything that would break if I change this," because relatedness in vector space is not the same thing as an edge in a call graph. It is also the pattern behind [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases): retrieval that returns a plausible chunk still leaves the agent guessing about what is actually connected to it. ## Pricing, as of July 2026 Augment's Business plan is $100 per month flat, no per-seat charge, covering up to 50 seats with a pooled $100 usage credit across LLM inference, the Context Engine, and compute, plus pay-as-you-go top-ups when the pool runs out. LLM inference is billed at the underlying provider's API price plus a 40% service fee on top; compute carries no fee. Enterprise is custom, unlimited seats, volume discounts ([Augment Code pricing](https://www.augmentcode.com/pricing)). If you have seen a $20/month individual tier mentioned in older roundups, that is stale: it is not on Augment's current pricing page. Symvanta pricing is per seat: Starter at $19/month, Pro at $29 per seat/month with a 7-day trial, Enterprise at $99 per seat/month with a 15-seat minimum. The practical difference shows up at the margins: Augment's flat $100 pool is attractive for a mid-size team with predictable usage, and gets more expensive per person as usage grows past the pool given the 40% fee on inference. A per-seat model is easier to forecast for a team that just wants to know what next month's bill looks like. ## Deployment and MCP Augment runs as cloud SaaS with two modes: a Local mode that indexes the active repository on the developer's machine, and a hosted Remote mode MCP for cross-repo and org-wide context, indexing entire organizations across GitHub, GitLab, and Bitbucket, plus docs and wikis, with auto-sync via CI/CD hooks ([Augment Code, Context Engine MCP](https://www.augmentcode.com/product/context-engine-mcp)). MCP is core to the product: Context Engine MCP is generally available for Cursor, Claude Code, Zed, and other MCP clients. Augment also publishes its own benchmark: on 300 Elasticsearch pull requests, they report a "+80%" improvement running Claude Code with Opus 4.6 through the Context Engine MCP versus without it. That is Augment's own published number, not an independent eval, and it is worth reading as such: a vendor benchmark on a vendor-chosen task. Symvanta is a hosted MCP endpoint by default, working with [Claude Code](/integrations/claude-code), Cursor, Windsurf, and [other MCP clients](/integrations), with an on-prem bundle available for Enterprise customers. Cross-repo context comes from explicitly linking repositories, including cross-repo HTTP-call edges between services, and indexing is branch-aware, so an agent can pin a session to a feature branch or even query uncommitted working-tree edits. On-prem specifically: Augment claims VPC and air-gapped deployment for Enterprise customers and holds SOC 2 Type II, but does not publish a detailed self-hosted deployment architecture the way some competitors do. Treat that as a sales-conversation item; their docs alone don't verify it. ## The 40% fee, worked through The fee structure is worth sitting with for a second, because it changes how a team should think about the $100 flat price. The $100/month covers the pool; once a team burns through it, top-ups are pay-as-you-go, and every dollar of LLM inference in that top-up carries the 40% service fee on top of the provider's own API price. A team running heavy agentic workloads (long sessions, big context windows, frequent tool calls) will burn the pool faster than a team doing light autocomplete-style usage, and the fee compounds with usage rather than flattening out. None of that makes the pricing bad; it makes it usage-shaped, which is a different thing to budget for than a flat per-seat number. The other lever on an inference-shaped bill is what the agent re-sends every turn; we work that side of the equation in [Sonnet plus a code graph vs Opus alone](/blog/sonnet-plus-code-graph-vs-opus). ## Side by side | | Augment Code | Symvanta | |---|---|---| | Pricing (as of July 2026) | Business $100/mo flat (pooled $100 usage, up to 50 seats, LLM billed at provider price + 40% fee) | Starter $19/mo, Pro $29/seat/mo (7-day trial) | | Enterprise | Custom, unlimited seats, volume discounts | $99/seat/mo (15-seat min) | | Context method | Dependency graph plus commit history plus embeddings, fetch-then-generate retrieval | Live code graph plus Qdrant embeddings plus text search, agent-driven traversal | | Direct graph-traversal tools | Not documented publicly | `relate`: callers, dependencies, blast_radius, implementers, heritage, chain | | MCP support | Context Engine MCP, GA for Cursor, Claude Code, Zed, others | MCP-native, hosted endpoint, any MCP client | | Cross-repo / org-wide | Remote mode, GitHub/GitLab/Bitbucket, docs and wikis | Linked repositories, cross-repo edges including HTTP calls | | Published benchmarks | Vendor benchmark: +80% on 300 Elasticsearch PRs with Claude Code + Opus 4.6 | None published; verify on your own repository | | On-prem | VPC/air-gapped claimed for Enterprise, SOC 2 Type II, architecture not publicly detailed | On-prem bundle for Enterprise; customer-managed encryption keys on Pro and Enterprise | ## Who should pick Augment If your team wants a single flat monthly number that covers a shared pool of usage across up to 50 people, and you are comfortable with retrieval that happens behind the scenes (the agent never calls it as a tool), Augment's Context Engine is a solid, mature product with real org-wide indexing and a genuinely useful Remote mode for multi-repo work. ## Who should pick Symvanta If you want the agent to be able to ask "who calls this" and get an actual graph traversal back (a retrieved chunk still needs interpreting), and you would rather pay per seat than manage a shared usage pool with a service fee on top, that is the fit. The difference in one line: the graph is the interface itself, the thing the agent queries directly, and search sits beside it as one more tool. ## Frequently asked questions
Is Symvanta a good Augment Code alternative? Augment is the closest architectural neighbor Symvanta has: both build a real dependency graph. The difference is who queries it. Augment's Context Engine retrieves a pre-fetched context bundle for the agent; Symvanta exposes the graph itself as MCP tools the agent calls directly and can follow up with a more specific question.
What's the difference between Symvanta and Augment's MCP context? Augment's Context Engine parses a graph and embeddings, then assembles a context bundle for the agent behind the scenes. Symvanta's graph is queryable directly: the agent runs `relate` with `kind: blast_radius` or `callers` and gets back the actual edges, one traversal at a time.
Is Symvanta cheaper than Augment Code? It depends on usage. Augment's Business plan is $100 a month flat for up to 50 seats with a pooled usage credit, plus a 40% fee on LLM inference past the pool. Symvanta is per seat, $19 to $99 a month, with no usage fee on top. A team with predictable, moderate usage often finds Symvanta's per-seat number easier to forecast.
Both of these are claims worth testing on a codebase you actually maintain. [Book a 15-minute demo](/demo) on your own repository and see what the graph returns. --- ### Sourcegraph Cody Alternative: MCP Code Graph https://symvanta.com/compare/sourcegraph-cody-alternative This page is for engineering teams who evaluated Cody before it went enterprise-only, or who are now staring at a six-figure Sourcegraph quote and wondering what else gives an AI coding agent real code context. If you want a second opinion after a Sourcegraph sales call, read on. ## What happened to Cody Cody Free and Cody Pro are gone. Sourcegraph stopped new signups on June 25, 2025, and cut off access entirely on July 23, 2025, folding what remained into Cody Enterprise ([Sourcegraph, changes to Cody Free, Pro, and Enterprise Starter plans](https://sourcegraph.com/blog/changes-to-cody-free-pro-and-enterprise-starter-plans)). Enterprise Starter workspaces created after that date no longer include Cody at all, only Code Search. Then on December 2, 2025, Sourcegraph and Amp split into two independent companies. Amp, the agentic coding product built by Sourcegraph co-founders Quinn Slack and Beyang Liu, is now its own company. Cody stayed with Sourcegraph as an Enterprise product. Cody was not rebranded to Amp: they are two separate products from two separate companies now, sharing a common origin and little else going forward. If you evaluated Cody a year ago and lost track of it, that is the whole story: a consumer and pro tier that no longer exists, an enterprise tier that remains, and a spun-out agent product that is not the same thing. ## What Sourcegraph costs today Sourcegraph publishes one number: Enterprise pricing "starting at $16K," scaling with AI feature credits and team size, quote-only past that ([Sourcegraph pricing](https://sourcegraph.com/pricing)). There is no self-serve tier and no public per-seat rate, as of July 2026. If you have seen a $59/user/month figure floating around older blog posts, that was the discontinued Cody Pro tier: it does not exist anymore and Sourcegraph does not publish anything like it today. That is a deliberate positioning choice, not a mistake on their part. Sourcegraph sells to organizations that already run procurement processes for infrastructure this size. It is a mismatch for a five-person team that wants to wire an MCP server into Claude Code this afternoon. ## How each one gives an agent context Sourcegraph's engine is code search plus code graph: keyword, regex, and structural search over the codebase, layered with SCIP-based code intelligence for precise go-to-definition and find-references ([Sourcegraph, code graph context](https://sourcegraph.com/docs/cody/explanations/code_graph_context)). Cody Enterprise's own documentation describes this search-and-graph combination without describing an embeddings-based retrieval path, which lines up with Sourcegraph's public messaging that Search replaced embeddings as the default context mechanism for Cody. [Symvanta](https://symvanta.com/) takes a different bet. It parses each repository into a live graph (nodes are symbols, edges are calls, imports, implements, and instantiates, each with a confidence score), stores embeddings in Qdrant for semantic search, and keeps literal text search as a third path. An agent asking "who calls this function" gets a graph traversal it can act on without re-deriving anything. That distinction is the subject of [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases): the failure mode is rarely a missing search index, it is an agent that finds a plausible-looking match and guesses the rest. We go deeper on the tradeoff itself in [code embeddings vs. code graph](/blog/code-embeddings-vs-code-graph): search and embeddings tell you what looks related, a graph tells you what is actually connected. One place Sourcegraph's documentation is quiet: neither the Cody nor the Search docs describe a named blast-radius or transitive-impact primitive, the kind of "what breaks if I change this" answer that comes from walking edges outward from a symbol. That does not mean Sourcegraph cannot answer that question through search and code intel combined; it means their public docs do not describe it as a first-class tool the way callers, dependencies, and blast radius are first-class `relate` operations in Symvanta. ## Sourcegraph's MCP server Sourcegraph ships its own MCP server, and it moved quickly through 2026. It went generally available on February 25, 2026 with OAuth Dynamic Client Registration on by default and a dedicated `mcp` OAuth scope, in Sourcegraph Cloud and self-hosted 7.0 ([Sourcegraph changelog, MCP server GA](https://sourcegraph.com/changelog/mcp-ga)). You point a client at your own instance, at `https://your-sourcegraph-instance.com/.api/mcp`. Sourcegraph access tokens work alongside OAuth, and the docs name Claude Code, Codex, Cursor, Copilot, OpenCode, and Amp among the clients it supports ([Sourcegraph MCP server docs](https://sourcegraph.com/docs/api/mcp)). Sourcegraph pitches it as "code context for any AI agent" ([Sourcegraph MCP](https://sourcegraph.com/mcp)). The tool set is search-and-navigation shaped. Since March 30, 2026 the default endpoint serves a curated eight: `read_file`, `list_files`, `keyword_search`, `nls_search`, `list_repos`, `commit_search`, `diff_search`, and `deepsearch_read`. The full suite, including `go_to_definition` and `find_references`, moved to `/.api/mcp/all`, to keep the default tool list from eating context window ([Sourcegraph changelog, curated default MCP tools](https://sourcegraph.com/changelog/mcp-curated-default-tools)). Those primitives map onto what Sourcegraph has always been strong at: find the text, open the file, jump to the definition, read the history. Cody sits on the other side of the protocol as a consumer, so a Cody Enterprise seat can call external MCP servers of its own ([Sourcegraph supports the Model Context Protocol](https://sourcegraph.com/blog/cody-supports-anthropic-model-context-protocol)). Amp, its own company since the December 2025 split, appears on Sourcegraph's supported-client list. The gating is the part to plan around. Sourcegraph's docs say the MCP server is "Supported on Enterprise plans" ([Sourcegraph MCP server docs](https://sourcegraph.com/docs/api/mcp)), which puts it behind the same starting-at-$16K floor as the rest of the platform, and it answers against an instance someone has already stood up and indexed. There is no self-serve path to try it on one repository this afternoon. Symvanta is MCP-first by design: the whole product is an authenticated MCP endpoint (OAuth 2.0 with PKCE, URL-based) plus a dashboard for managing what is indexed, self-serve from $19/month. It works with [Claude Code](/integrations/claude-code), Cursor, Claude Desktop, or [any MCP client](/integrations), same as Sourcegraph's server does. The tool shapes diverge where an edit is at stake. `find_references` answers "where is this symbol mentioned"; `relate` with kind `blast_radius` answers "what breaks if I change it", walked outward through calls, imports, and implementations across every repository linked in the project. `nls_search` and `deepsearch_read` hand the agent passages to read; `find_node`, `relate`, and `find_http_route` hand it edges to act on. ## Cross-repo and on-prem Multi-repo context is core to Sourcegraph, and the mechanism behind it is worth understanding, because the same design that makes it work also bounds it. Sourcegraph's precise cross-repo navigation runs on SCIP, where a symbol is a string. The grammar is explicit: a symbol is a scheme, then a package written as `manager package-name version`, then a descriptor path that fully qualifies the symbol inside that package ([SCIP symbol grammar, scip.proto](https://github.com/sourcegraph/scip)). Cross-repo navigation treats that package name plus version plus qualified symbol as a unique id, so a reference indexed in one repo resolves to a definition indexed in another whenever the two ids match ([scip-clang cross-repo docs](https://github.com/sourcegraph/scip-clang/blob/main/docs/CrossRepo.md)). That design means each repository is indexed independently, and cross-repo is a lookup that joins a reference to a definition on package identity at query time ([Sourcegraph, cross-repository code navigation](https://sourcegraph.com/blog/cross-repository-code-navigation)). The join lands only when both sides are indexed at matching package versions. When the versions differ or an index is missing, precise navigation is unavailable and Sourcegraph automatically falls back to search-based (fuzzy) results, as of July 2026 ([Sourcegraph precise code navigation docs](https://sourcegraph.com/docs/code_intelligence/explanations/precise_code_intelligence)). The fallback is quiet: you still get an answer, it just comes from text search instead of the resolved symbol. The identity has to come from somewhere, and where there is no package manager there is nothing to derive it from. For C and C++, users hand-write a `package-map.json` to supply the package names and versions themselves, and a version must never be reused over time or the ids collide ([scip-clang cross-repo docs](https://github.com/sourcegraph/scip-clang/blob/main/docs/CrossRepo.md)). Cost also climbs with dependency depth: Sourcegraph's own C++ documentation describes the indexing work as scaling quadratically with the depth of the dependency graph, since a shared dependency is reindexed once for every dependent that pulls it in ([scip-clang cross-repo docs](https://github.com/sourcegraph/scip-clang/blob/main/docs/CrossRepo.md)). The structural point is simple: anything a package manager names is reachable by the precise tier, and anything it does not name falls to text search. A service calling another over HTTP, a queue producer and the consumer that drains its channel, two services reading and writing the same database table: a package manager assigns none of those relationships a `manager package-name version` id. Symvanta links repositories in a project by that coupling directly. It matches HTTP call sites to the route definitions they hit across repos, SQL table access to the ORM model that owns the table, and queue producers to consumers on the same channel, alongside project-wide symbol resolution and library import and call edges. And as of July 2026 Symvanta runs the package join too: when one repository in a project defines an internal package and a sibling consumes it, the reference resolves to the real definition by package identity, matched name-first so a version bump never silently demotes you to text search. The version gap that match reveals (a consumer pinned at 1.1.0 while the sibling ships 1.2.0) is reported as drift in the index health check instead of being swallowed. Sourcegraph draws the boundary of connected at the package; Symvanta draws it at the package and the transport. On-prem is Sourcegraph's strongest ground. Self-hosted deployment covers Docker Compose and Kubernetes/Helm, with air-gapped, bring-your-own-key deployments supported for regulated environments ([Sourcegraph self-hosted docs](https://sourcegraph.com/docs/self-hosted)). This is a mature, years-old deployment story. Symvanta has a hosted MCP endpoint plus an on-prem bundle for enterprise customers, and hosted Pro and Enterprise workspaces can bring their own encryption key (AWS KMS, GCP Cloud KMS, Azure Key Vault, or a key endpoint you run), but Sourcegraph has more field experience running fully air-gapped. ## What evaluating each one actually looks like With Sourcegraph, evaluation starts with a sales conversation, because there is no public per-seat price and no self-serve signup to try against your own repository first. That is normal for infrastructure sold at this scale, but it means the first real signal you get is a demo someone else drives; pointing the tool at your own codebase on your own schedule comes later, if at all. With Symvanta, the path is the reverse: connect a repository, get an MCP endpoint, point your agent at it, and ask it a real question about your own code before anyone from Symvanta talks to you. That is a smaller test than a full Sourcegraph rollout, and it is meant to be: the goal is to see whether the graph answers correctly on the codebase you actually maintain. ## Side by side | | Sourcegraph Cody / Enterprise | Symvanta | |---|---|---| | Pricing (as of July 2026) | Enterprise only, starting at $16K, custom credit-based quote | Starter $19/mo, Pro $29/seat/mo (7-day trial), Enterprise $99/seat/mo (15-seat min) | | Self-serve signup | No | Yes | | Context method | Code search (keyword/regex/structural) plus SCIP code graph | Live code graph plus Qdrant embeddings plus text search | | Named blast-radius tool | Not described in public docs | `relate` (kind: blast_radius) | | MCP support | Own MCP server at `/.api/mcp`, GA February 2026, Enterprise plans; Cody also consumes external MCP servers | MCP-native, hosted endpoint, self-serve, any MCP client | | Cross-repo context | Precise tier joins on the SCIP package name + version in the symbol id; unversioned or unindexed code falls back to text search | Transport edges (HTTP call to route, SQL access to ORM model, queue producer to consumer) plus a name-first package join for sibling repos, with version drift surfaced | | On-prem / self-hosted | Docker Compose, Kubernetes/Helm, air-gapped BYOK | Hosted by default with customer-managed encryption keys on Pro and Enterprise, on-prem bundle for Enterprise | | Branch-aware indexing | Not the focus of public docs | Yes, including uncommitted working-tree edits | ## Who should pick Sourcegraph If you are a large organization that already needs air-gapped deployment, a mature multi-repo search platform, and a procurement process that can absorb a five- or six-figure annual contract, Sourcegraph Enterprise is a legitimate, battle-tested choice. Their code intelligence and search infrastructure predate the current wave of AI coding agents by years, and that maturity shows in the self-hosted story. ## Who should pick Symvanta If you want an MCP endpoint your agents can call this week, at a per-seat price a small team can expense without a sales call, and you want the agent asking "what calls this" or "what breaks if I change this" to get a graph answer instead of a search result it has to interpret, that is the gap Symvanta fills. Graph not grep, without the six-figure floor. ## Frequently asked questions
Is Symvanta a good Sourcegraph Cody alternative? For a team that wants a self-serve MCP endpoint without an Enterprise contract, yes. Cody Free and Pro are gone, and Sourcegraph now publishes only Enterprise pricing starting at $16K with no self-serve signup. Symvanta is self-serve from $19 a month, with a named blast-radius tool Sourcegraph's public docs do not describe as a first-class primitive.
Does Symvanta require a six-figure contract like Sourcegraph Enterprise? No. Symvanta is per seat and self-serve: Starter at $19 a month, Pro at $29 a seat with a 7-day trial, Enterprise at $99 a seat with a 15-seat minimum. Sourcegraph publishes only an Enterprise tier starting at $16K, quote-only past that.
Does Symvanta have a named blast-radius tool like Sourcegraph's code graph? Yes. `relate` with `kind: blast_radius` walks outward from a symbol through calls, imports, and implementations across every repository linked in the project. Sourcegraph's public docs describe code search plus SCIP-based navigation, but do not name a blast-radius primitive the way Symvanta's `relate` tool does.
The fastest way to see the difference is on your own repository. [Book a 15-minute demo](/demo) and bring a real question about a real codebase. ## Architecture ### n8n Architecture: How It Actually Works https://symvanta.com/architecture/n8n n8n is a workflow automation server. A Vue editor draws the workflow, a REST and webhook server stores it, an execution engine walks it node by node, and a large library of integration nodes makes the outbound calls. Symvanta's graph of the repo at `af27b0d` on `master` detects 778 functional modules (modularity Q=0.93), and serves the 500 largest, which hold 75,637 symbols. The mass of the codebase is integrations. 197 of those 500 modules live in `packages/nodes-base` or `packages/@n8n/nodes-langchain`, roughly one per vendor transport, and five of the twelve repo-wide load-bearing functions are a vendor's own HTTP helper: TheHive's, Google's, OpenAI's, Microsoft Excel's and Pipedrive's. That is what an integration catalog looks like in a call graph, and this one holds 308 vendor directories and 406 credential definition files at this commit. Those 197 modules are 17.9% of the mapped symbols, behind `packages/cli` at 24.8% and the four AI assistant packages at 19.3%, ahead of the editor frontend at 10.8%. The execution engine is genuinely small and deliberately so: the clusters whose members sit in `packages/core/src/execution-engine`, `packages/workflow` and `packages/cli/src/scaling` are 6.3% of the map between them. The engine walks the graph and hands each node an execution context, and the work happens inside the node. Module names on this page are read by hand from the packages the members live in and from each hub symbol's file. The generated cluster summaries are used for nothing here, because on this repo several of them are wrong: the cluster that holds the execution engine was summarized as logging, and the cluster that holds the REST controllers was summarized as licensing. Every cluster is cited by its id, the member-set hash that survives a reindex. Symbol counts, hubs, ids and edge weights are what the graph computed. ## Module map The diagram shows the 10 largest of the 778 detected modules, with edges weighted by how many calls cross between them. Two of the ten are the repo's own evaluation harnesses, drawn because they really are that big: the `@n8n/instance-ai` harness (2,650 symbols) and the AI workflow builder harness (2,082). Two more take their hub symbol from an ambient TypeScript declaration file, which inflates both their size and the weight of the arrows into them: the global interface augmentation in `packages/frontend/editor-ui/src/shims-global.d.ts` is referenced from 82 files, and the `*.vue` module shim in `packages/@n8n/mcp-apps/src/apps/workflow-preview/shims-vue.d.ts` from 96, in packages as unrelated as the vendored ORM and the push service. Read those two boxes as where the editor's own code sits.
n8n-io/n8n module map: the 10 largest of 778 detected modules with call-weighted edges, generated by Symvanta
Module map of n8n-io/n8n, generated by Symvanta. Link to this diagram Open full size
## Where to start reading These are the load-bearing functions: the PageRank ranking over the call graph, most depended-upon first. Four of the names are defined independently in several packages (`apiRequest` alone in six vendor transports), so each link points at the definition that hubs the largest cluster carrying that name. Five of the twelve are one vendor's HTTP transport, which is the shape of this repo showing up in the ranking. - [`isRecord`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/@n8n/utils/src/is-record.ts#L1) - [`Logger.scoped`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/@n8n/backend-common/src/logging/logger.ts#L76) - [`makeRestApiRequest`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/frontend/@n8n/rest-api-client/src/utils.ts#L196) - [`Telemetry.track`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/cli/src/telemetry/index.ts#L580) - [`MessageEventBus.sendAuditEvent`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/cli/src/eventbus/message-event-bus/message-event-bus.ts#L251) - [`theHiveApiRequest`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/nodes-base/nodes/TheHiveProject/transport/requestApi.ts#L10) - [`LicenseState.isLicensed`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/@n8n/backend-common/src/license-state.ts#L33) - [`JsonColumn`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/@n8n/db/src/entities/abstract-entity.ts#L30) - [`getGoogleAccessToken`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/nodes-base/nodes/Google/GenericFunctions.ts#L75) - [`apiRequest`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/transport/index.ts#L15) - [`microsoftApiRequest`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/nodes-base/nodes/Microsoft/Excel/v2/transport/index.ts#L114) - [`pipedriveApiRequest`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/nodes-base/nodes/Pipedrive/v2/transport/pipedrive.api.ts#L30) ## Key subsystems ### Instance AI assistant runtime Module id `1122f926faac11ce`, 3,706 symbols, the largest on the map. Its members live in `packages/@n8n/instance-ai/src` and its hub is `InstanceAiContext` from `src/types.ts`, the context object every assistant tool receives. `InstanceAiToolRegistry`, `OrchestrationContext`, `WorkflowBuildOutcome` and `ModelConfig` sit beside it. Its heaviest outgoing edge is 148 calls into the editor's agents feature, which is the other half of the same product surface. ### Node errors and credential helpers Module id `532185b8a99ea6b4`, 3,337 symbols. The hub is `NodeApiError` from `packages/workflow/src/errors/node-api.error.ts`, and the members are `NodeOperationError` plus a wide slice of the credential and transport helpers under `packages/nodes-base` (`beeminderApiRequest`, `getActiveCredentialType`, `getHost`, `DatabricksCredentials`). Integration nodes raise these two error types when a request fails, so the error classes cluster with the transports that throw them instead of with the rest of the workflow package. ### Agents SDK types and JSON values Module id `4e0cee763e689c87`, 3,183 symbols, hubbed on `JSONValue` from `packages/@n8n/agents/src/types/utils/json.ts`. This is the type surface of the agents SDK: `JSONObject`, `JSONArray`, `AgentMessage`, `AgentDbMessage`, `BuiltTool`. A one-line recursive type alias ranks as the hub here because every message and tool definition in the SDK is built out of it. ### Editor UI composables and log view Module id `f7c4c628d73c66c6`, 2,887 symbols, members under `packages/frontend/editor-ui/src/app/composables`: `useWorkflowDocumentStore`, `useTelemetry`, and the `LogEntry` and `NodeLogEntry` shapes the execution log view renders. This is one of the two clusters whose hub is an ambient declaration, so the name comes from where the members live. ### REST controllers and response errors Module id `ad43c0ce9514e986`, 2,871 symbols, hubbed on `ResponseError` from `packages/cli/src/errors/response-errors/abstract/response.error.ts` with `BadRequestError` and `ForbiddenError` under it. Around them sit the chat hub module (`ChatHubSession`, `ChatHubTool`) and the license accessors (`License.manager`, `License.isLicensed`). Controllers throw response errors and check the license on the same request path, so the graph puts them together. Its heaviest edges are 116 calls into the server agents module and 102 into the database entities. ### Server agents and the editor agents feature The server side is module id `1fbd1c236a830671`, 2,185 symbols under `packages/cli/src/modules/agents`, hubbed on the `Agent` entity in `entities/agent.entity.ts`, with `AgentHistory`, `AgentExecution`, `AgentExecutionThread` and `AgentRepository`. The editor side is module id `43d35099e877cd82`, 2,541 symbols under `packages/frontend/editor-ui/src/features/agents/composables`. The server module takes 116 calls from the REST controller cluster and sends 46 back. ### Execution engine and queue runner The engine is two modules. Id `83094e44dd365222` (1,255 symbols) has its members in `packages/core/src/execution-engine` while its hub is `Logger.scoped` from `packages/@n8n/backend-common`, which is why its generated summary described logging. Id `8d9cdb44e96f26ba` (540 symbols) is the partial-execution machinery: `DirectedGraph`, `GraphConnection`, `NodeExecutionContext`, `Workflow.getNode`. The queue runner is another two: id `c4a90fa06ac89e11` (616 symbols) under `packages/cli/src/scaling`, holding `ExecutionRef`, `ActiveExecutions` and the execution lifecycle hook handlers, and id `e4d215ec66e79a25` (399 symbols) for the pub/sub commands workers exchange. Together with `packages/workflow`, that is 6.3% of the mapped symbols. ### Integration transports The 197 integration modules are the long tail of the map, each one a vendor's transport file plus the actions that call through it, 13,575 symbols in total. Past the error cluster above, the largest are id `a1594a6836dd063d` (787 symbols) hubbed on `googleApiRequest` from the Gmail helpers, id `be2b32f6e5a912ef` (660) on `pipedriveApiRequest`, id `f6c860f8c23c0180` (630) on `getGoogleAccessToken`, and id `4419e5cfc5c6b81d` (440) on the shared `verifySignature` webhook helper. The pattern repeats verbatim down the tail: one `*ApiRequest` function per vendor, a credentials type, and a directory of actions that call it. Symvanta's clustering gives each of those its own module, which is why 197 of the 500 served modules are integrations while they are only 17.9% of the symbols. ### Workflow error hierarchy Module id `91ba1b8bb399e612`, 1,891 symbols in `packages/workflow/src`: `BaseError`, `NodeError`, `ExecutionBaseError`, the `JsonObject` and `JsonValue` types, and helpers like `removeCircularRefs`. This is the package every other package imports its error classes from. ### Evaluation harnesses Two of the ten drawn clusters are test harnesses for the AI features: id `ca1dbdc7a8b683df` (2,650 symbols, `packages/@n8n/instance-ai/evaluations`, hubbed on the harness `N8nClient`) and id `20557a094fd53329` (2,082 symbols, `packages/@n8n/ai-workflow-builder.ee/evaluations`). With the rest of `packages/testing` they are 9.8% of the mapped symbols. They rank this high because these harnesses drive a real n8n instance, so they carry client, workflow and validation code of their own. ## Canonical request flow The sequence worth reading first is one workflow execution: the path from the call that starts a run to the node's own `execute` method and back. Every step below is a call edge out of the step above it, in the order the work happens. 1. `WorkflowRunner.run` ([`packages/cli/src/workflow-runner.ts:242`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/cli/src/workflow-runner.ts#L242)) is the entry point 13 files in the server call: retries, waiting executions resumed by the wait tracker, chat runs, the MCP execute and test tools, and the evaluation runner. 2. `CredentialsPermissionChecker.check` ([`packages/cli/src/executions/pre-execution-checks/credentials-permission-checker.ts:102`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/cli/src/executions/pre-execution-checks/credentials-permission-checker.ts#L102)) maps every credential id the workflow's nodes use to the projects the workflow belongs to, and refuses the run when one of them is not shared with any of those projects. 3. `ActiveExecutions.add` ([`packages/cli/src/active-executions.ts:62`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/cli/src/active-executions.ts#L62)) reserves a concurrency slot, creates the execution row at status `new`, and builds the in-memory record everything later attaches to: the cancellation controller, the response promise, the post-execute promise. 4. `WorkflowRunner.enqueueExecution` ([`packages/cli/src/workflow-runner.ts:515`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/cli/src/workflow-runner.ts#L515)) is the queue-mode branch: it hands the job to [`ScalingService.addJob`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/cli/src/scaling/scaling.service.ts#L225) and waits for a worker to report the result. The worker picks it up in [`JobProcessor.processJob`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/cli/src/scaling/job-processor.ts#L84) and runs step 9 onwards in its own process. 5. `WorkflowRunner.runMainProcess` ([`packages/cli/src/workflow-runner.ts:343`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/cli/src/workflow-runner.ts#L343)) is the other branch, running the workflow in the server process. It registers the lifecycle hooks, arms the execution timeout, and owns the failure paths. 6. `ExecutionRepository.setRunning` ([`packages/@n8n/db/src/repositories/execution.repository.ts:380`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/@n8n/db/src/repositories/execution.repository.ts#L380)) flips the row to `running` inside a transaction and keeps the original `startedAt` when this is a resumed execution. 7. `ManualExecutionService.runManually` ([`packages/cli/src/manual-execution.service.ts:49`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/cli/src/manual-execution.service.ts#L49)) decides what a manual run actually executes: the whole workflow from its trigger, or a partial re-run seeded with the pinned data and start nodes the editor sent. 8. `DirectedGraph.fromWorkflow` ([`packages/core/src/execution-engine/partial-execution-utils/directed-graph.ts:466`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/core/src/execution-engine/partial-execution-utils/directed-graph.ts#L466)) turns the stored workflow into the graph object the partial-execution logic walks to find which nodes a re-run has to touch. 9. `WorkflowExecute.processRunExecutionData` ([`packages/core/src/execution-engine/workflow-execute.ts:1586`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/core/src/execution-engine/workflow-execute.ts#L1586)) is the loop, roughly 970 lines of it. It shifts the next node off the execution stack, checks that its input data is ready, runs it, pushes the nodes its output feeds, and repeats until the stack is empty. Per-node retries and the wait state live inside the same loop. 10. `WorkflowExecute.runNode` ([`packages/core/src/execution-engine/workflow-execute.ts:1311`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/core/src/execution-engine/workflow-execute.ts#L1311)) handles one node: disabled nodes, execute-once, trigger and poll nodes, and the error-output branch that lets a workflow continue past a failure. 11. `WorkflowExecute.executeNode` ([`packages/core/src/execution-engine/workflow-execute.ts:1050`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/core/src/execution-engine/workflow-execute.ts#L1050)) builds an [`ExecuteContext`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/core/src/execution-engine/node-execution-context/execute-context.ts#L43) and calls [`INodeType.execute`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/workflow/src/interfaces.ts#L2433). This one edge is the boundary between the engine and the integration library: above it is n8n's code, below it is the node's, and the 197 transport modules hang off the far side. 12. `ExecutionLifecycleHooks.runHook` ([`packages/core/src/execution-engine/execution-lifecycle-hooks.ts:118`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/core/src/execution-engine/execution-lifecycle-hooks.ts#L118)) runs the handlers registered for one of eight events (`workflowExecuteBefore`, `nodeExecuteBefore`, `nodeExecuteAfter`, `nodeFetchedData`, `sendResponse`, `sendChunk`, `workflowExecuteResume`, `workflowExecuteAfter`). Saving progress, pushing status to the editor and recording statistics all hang here. 13. `ActiveExecutions.finalizeExecution` ([`packages/cli/src/active-executions.ts:246`](https://github.com/n8n-io/n8n/blob/af27b0d3d6b3603e5a41529d687b1b15aab570de/packages/cli/src/active-executions.ts#L246)) closes a streaming response if the run had one, resolves the post-execute promise, and drops the in-memory record. ## Health signals Symvanta detected 255 dependency cycles across 778 modules (modularity Q=0.93). The largest cycle spans 269 files in the `services` area. 85 sets of mutually recursive symbols were also detected, the largest being `workflow` (53 symbols). Read the cycle count with its composition in hand: 194 of the 255 sit entirely inside `packages/@n8n/typeorm`, the TypeORM fork the repo vendors, which the clustering already excludes (it holds zero of the 778 modules) while the cycle listing still covers it. The 61 that remain are n8n's own. The largest is the one worth acting on: every file the map names for it sits under `packages/cli/src`, including `services/role.service.ts`, `active-executions.ts`, the MCP tool files and the agents services, all reaching each other through imports. The second largest spans 91 files in `packages/workflow`. The recursion is milder and mostly deliberate: the 53-symbol `workflow` group is the node interface surface in `packages/workflow/src/interfaces.ts` (`IExecuteFunctions`, `ILoadOptionsFunctions`, `INodeParameters` and their neighbours), which is what a self-referential parameter type looks like in a graph. --- ### VS Code Architecture: How It Actually Works https://symvanta.com/architecture/vscode Visual Studio Code is an Electron application whose window is a workbench shell: a title bar, an activity bar, a side bar, an editor area, a panel and a status bar, assembled from services that a dependency injector hands out, with third-party extensions running in their own process. Symvanta's graph of the repo at `584b2da` detects 152 functional modules (modularity Q=0.67), and the ten largest hold 70.6% of the 202,577 clustered symbols. Nine of those ten take their hub from `src/vs/base`, `src/vs/platform`, or the editor core. That is a platform layer sitting across the top of the map. Lifecycle, events, URIs, observables, cancellation, collections, the identifiers the service injector is keyed on and the action registry are called from everywhere in the tree, so clustering gathers the files that use them around the primitive itself, and every one of the ten biggest modules carries a primitive as its hub. The product layer sits underneath. Below the top ten there are 36 modules of 200 symbols or more, and 24 of them take their hub from a file outside `src/vs/base`, the editor core and the shared platform services: notebooks (5,731 symbols), editor inputs and groups (4,941), the search result tree (2,722), chat prompt and AI customization files (1,711), source control (1,620), user data profiles (1,579), remote tunnels (645) and MCP server management (617). The map is a clustering of the call graph, so it is not a subsystem inventory, and two of the subsystems people look for first have no box of their own at this resolution. The dominant member directory of the disposables module is the debug UI at `src/vs/workbench/contrib/debug/browser`, and the dominant member directory of the collections module is the terminal UI at `src/vs/workbench/contrib/terminal/browser`. Each was absorbed into the primitive its files reach for most. ## Module map The diagram shows the 10 largest of the 152 detected modules, with edges weighted by how many calls cross between them. The heaviest pair runs between service identifiers and disposables: 8,767 calls one way and 6,903 back. The first of those clusters holds the interfaces the injector resolves against (`IInstantiationService`, `IConfigurationService`, `IFileService`, `IStorageService`), the second holds `IDisposable`, `DisposableStore` and `Emitter`. Requesting a service and disposing what it hands back is the pattern most files in the workbench repeat. Module names on this page are grounded by hand in each module's hub file and in the directories its members live in; the symbol counts, hubs and edge weights are what the graph computed.
microsoft/vscode module map: the 10 largest of 152 detected modules with call-weighted edges, generated by Symvanta
Module map of microsoft/vscode, generated by Symvanta. Link to this diagram Open full size
## Where to start reading These are the load-bearing symbols by PageRank over the call graph, most depended-upon first. Ten of the eleven live under `src/vs/base` or `src/vs/platform`, which is what a tree this size does to a ranking: `Disposable.dispose` has callers in 82 files of the index, `DisposableStore.dispose` in 68, `onUnexpectedError` in 54 and `IContextKey.set` in 50. The eleventh, `es5ClassCompat`, is the deprecated shim that wraps extension API classes so extension code written before classes can still call them as functions; it has callers in 13 files. One entry is left out below: the ranking also lists the global `setTimeout`, whose only definitions in this tree are ambient type declarations, so it points at no code to read. - [`trackDisposable`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/base/common/lifecycle.ts#L271) - [`URI.toString`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/base/common/uri.ts#L386) - [`Disposable.dispose`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/base/common/lifecycle.ts#L542) - [`DisposableStore.dispose`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/base/common/lifecycle.ts#L432) - [`ContextKeyExpr.and`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/platform/contextkey/common/contextkey.ts#L608) - [`IObservableWithChange.get`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/base/common/observableInternal/base.ts#L33) - [`onUnexpectedError`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/base/common/errors.ts#L107) - [`IChannel.call`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/base/parts/ipc/common/ipc.ts#L26) - [`IContextKey.set`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/platform/contextkey/common/contextkey.ts#L2042) - [`ITelemetryService.publicLog2`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/platform/telemetry/common/telemetry.ts#L44) - [`es5ClassCompat`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/workbench/api/common/extHostTypes/es5ClassCompat.ts#L12) ## Key subsystems ### Disposables and events 22,695 symbols, the largest module in the repo, hubbed on `IDisposable` in `src/vs/base/common/lifecycle.ts`. It holds the disposal primitives (`Disposable`, `DisposableStore`, `trackDisposable`) together with the event layer around `Emitter.fire`. Anything that subscribes to an event owns a disposable, so the two files travel together, and the files that use them are pulled in behind: the module's dominant member directory is the debug UI. ### Editor core model 22,619 symbols, hubbed on `Range` in `src/vs/editor/common/core/range.ts`, with `IRange`, `Position`, `Selection`, `ITextModel` and `ICodeEditor` beside it. This is the Monaco editor core: coordinates in a document, the model that stores the text, and the editor interface built over it. It is the only module in the top ten that comes from `src/vs/editor`. ### Resource URIs 18,789 symbols, hubbed on `URI` in `src/vs/base/common/uri.ts`, with `UriComponents`, `URI.toString`, `URI.scheme`, `URI.path` and `VSBuffer`. Every file, editor, extension and setting is addressed by a URI, including the ones served by remote and virtual file systems, so the type and its accessors are reached from every layer. ### Extension API declarations 16,230 symbols, hubbed on `Uri` in `src/vscode-dts/vscode.d.ts`, the file that declares the public extension API, and its dominant member directory is `src/vs/workbench/api/common`, the extension host side of that same API. The cluster is the API surface plus the code that implements it: `Uri`, `Position`, `Range`, `Event` and `Disposable` as an extension author sees them, next to `IExtensionDescription`. ### Service identifiers 13,563 symbols, hubbed on `ServiceIdentifier` in `src/vs/platform/instantiation/common/instantiation.ts`. A service here is an interface with a branded identifier, and the injector resolves a constructor parameter by that identifier, so the file is imported by nearly everything that consumes a service. The cluster holds the mechanism plus the services asked for most: `IInstantiationService`, `ILogService`, `IConfigurationService`, `IContextKeyService`, `IFileService`, `IStorageService`. ### Actions menus and context keys 12,683 symbols, hubbed on `Action2` in `src/vs/platform/actions/common/actions.ts`, clustered with `MenuId`, `ContextKeyExpression`, `ContextKeyExpr.and`, `ServicesAccessor` and `EditorAction`. A command, the menus it appears in and the `when` clause that decides whether it appears are one mechanism in this codebase, and the graph puts them in one module. ### Extension identity and manifests 8,161 symbols, hubbed on `ExtensionIdentifier` in `src/vs/platform/extensions/common/extensions.ts`, with `IExtensionManifest`, `ILocalExtension`, `TargetPlatform` and `Severity`. Installing, enabling, updating and reporting on an extension all key off that identifier. ### Other shared primitives Three more of the ten are primitives the rest of the tree calls into. Cancellation and markdown strings (10,124 symbols) holds `CancellationToken` from `src/vs/base/common/cancellation.ts` alongside `IMarkdownString` and `MarkdownString`. Observables (9,642 symbols) is `IObservable`, `IObserver`, `IReader` and `ISettable` from `src/vs/base/common/observableInternal`, the reactive layer the editor and the chat UI are built on. Collections and platform checks (8,517 symbols) is `IStringDictionary` from `src/vs/base/common/collections.ts` with the platform predicates (`isWindows`, `OperatingSystem`), `IWorkspaceFolder` and `IChannel.call`; its dominant member directory is the terminal UI. ### The product layer The feature code sits below the top ten and it is not small. The notebook editor model (5,731 symbols, hub `ICellViewModel`) and editor inputs and groups (4,941 symbols, hub `EditorInput`) are the two biggest. After them come the search result tree (2,722, `ISearchTreeFileMatch`), chat prompt and AI customization files (1,711, `PromptsType`), source control (1,620, `ISCMRepository`), the Electron-main browser view host (1,593), user data profiles and sync (1,579, `IUserDataProfile`), GitHub API types (1,120, `GitHubAccountHandle`), chat request variables (981, `IChatRequestVariableEntry`), remote tunnels (645, `RemoteTunnel`), MCP server management (617, `ILocalMcpServer`) and speech to text (445). Each of these takes its hub from its own feature directory, which is what separates a subsystem from a crowd of callers gathered around one primitive. ## Canonical request flow The sequence worth reading first is window startup: what runs between the main window loading the workbench bundle and the workbench being usable. Create the services, wire the layout, start the two registries that instantiate contributions, render the parts into the DOM, lay them out, then restore the previous session. Every step below is a call edge out of `Workbench.startup`, in the order the work happens. 1. `Workbench.startup` ([`src/vs/workbench/browser/workbench.ts:131`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/workbench/browser/workbench.ts#L131)) is the entry point. It raises the emitter leak-warning threshold, calls `initServices` to get an instantiation service, then runs every step below inside one `invokeFunction` block so they all share the same service accessor. 2. `Workbench.initServices` ([`src/vs/workbench/browser/workbench.ts:192`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/workbench/browser/workbench.ts#L192)) registers the workbench layout service and creates the instantiation service the window is built from. Individual services come from `registerSingleton` calls in `workbench.common.main.ts`, which a capitalized comment block in this function points at. 3. `Layout.initLayout` ([`src/vs/workbench/browser/layout.ts:325`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/workbench/browser/layout.ts#L325)) takes the services the layout needs off that accessor: editor service, editor groups, pane composites, view descriptors, title, notifications and status bar. Nothing is drawn yet. 4. `IWorkbenchContributionsRegistry.start` ([`src/vs/workbench/common/contributions.ts:121`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/workbench/common/contributions.ts#L121)) starts the workbench contributions registry, which instantiates each registered contribution at its declared lifecycle phase. This is how a feature attaches itself to the window without the window knowing about it. 5. `IEditorFactoryRegistry.start` ([`src/vs/workbench/common/editor.ts:457`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/workbench/common/editor.ts#L457)) does the same for editor serializers, the registry that lets an editor input be written to storage and rebuilt in the next session. 6. `Workbench.registerListeners` ([`src/vs/workbench/browser/workbench.ts:231`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/workbench/browser/workbench.ts#L231)) subscribes to configuration changes, storage save points, window focus changes and both shutdown events. The shutdown listener is what disposes the workbench. 7. `Workbench.renderWorkbench` ([`src/vs/workbench/browser/workbench.ts:320`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/workbench/browser/workbench.ts#L320)) sets the ARIA container and the platform CSS classes, restores cached font information, creates the eight parts (title bar, banner, activity bar, side bar, editor, panel, auxiliary bar, status bar) with a performance mark around each one, and appends the container to the DOM. 8. `Layout.createWorkbenchLayout` ([`src/vs/workbench/browser/layout.ts:1644`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/workbench/browser/layout.ts#L1644)) collects those eight parts as views and deserializes them into a `SerializableGrid` from a descriptor built out of the stored side bar, auxiliary bar and panel sizes. That descriptor is how a window remembers its splits. 9. `Layout.layout` ([`src/vs/workbench/browser/layout.ts:1753`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/workbench/browser/layout.ts#L1753)) measures the client area, sizes the container, and hands the grid its width and height. The layout is marked initialized here. 10. `Workbench.restore` ([`src/vs/workbench/browser/workbench.ts:412`](https://github.com/microsoft/vscode/blob/584b2dacffdba8b29df943d8d6f99154c9f967e4/src/vs/workbench/browser/workbench.ts#L412)) asks every part to restore its own state, then moves the lifecycle to the `Restored` phase once layout restoration settles or two seconds pass, whichever comes first, so a slow editor cannot hold contributions back. The `Eventually` phase follows on an idle callback a few seconds later. ## Health signals Symvanta detected 159 dependency cycles across 152 modules (modularity Q=0.67). The map lists 86 of them, and the biggest one listed pulls 212 files of the chat UI under `src/vs/workbench/contrib/chat/browser` into a single cycle. 657 sets of mutually recursive symbols were also detected, the biggest listed being 51 methods of `AbstractTaskService` calling one another. A modularity score of 0.67 is low, and the reason is at the top of this page: when a handful of primitives are called from every directory in the tree, the clustering has less to separate them by. --- ### Caddy Architecture: How It Actually Works https://symvanta.com/architecture/caddy Caddy is a web server built as a module system: a small core that parses config, loads modules, and swaps configurations at runtime, with the HTTP server, reverse proxy, and TLS automation all plugged in as modules. The graph at `45ba327` shows that split directly. Symvanta detects 39 functional modules (modularity Q=0.81), and the biggest clusters are the config machinery itself: the root `caddy` package around the `Log` accessor (467 symbols), the Caddyfile token dispenser (428), and the module loader that provisions everything a config names (417). That is the structural signal: in most servers the request path dominates the map, and in Caddy the config path does. `Next`, `LoadModule`, and `RegisterModule` all rank in the PageRank top 10 because every plugin in the repo goes through them. The mutually recursive symbols tell the same story: the largest group is the config-swap cycle in `caddy.go` (`changeConfig`, `run`, `provisionContext`, `finishSettingUp`). ## Module map The diagram shows the 10 largest of the 39 detected modules, with edges weighted by how many calls cross between them. The biggest holds 467 symbols and its hub is `Log`, the global logger accessor in `logging.go` that every other cluster reaches for. The caddytest harness (204 symbols, hub `NewTester` in `caddytest/caddytest.go`) ranks eighth by size and is left out as scaffolding. Module names are checked by hand against the files their members live in; the symbol counts, hubs, and edge weights are what the graph computed.
caddyserver/caddy module map: the 10 largest of 39 detected modules with call-weighted edges, generated by Symvanta
Module map of caddyserver/caddy, generated by Symvanta. Link to this diagram Open full size
## Where to start reading These are the load-bearing entry points: the PageRank ranking over the call graph, blended with the hub symbol of each of the largest clusters. Five of those entries are left out below. `Name` is a struct field the graph groups across a dozen types (`httpcaddyfile.App`, `caddypki.CA`, `cmd.Command`), `key` is a private map helper in `internal/filesystems/map.go`, `resetDynamicHosts` is defined in `modules/caddyhttp/reverseproxy/dynamic_upstreams_test.go` and only the reverse proxy suite calls it, and `filtered` and `Filter` are internals of the log filter modules in `modules/logging`. They rank high without pointing anywhere useful. - [`Log`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/logging.go#L779) - [`Next`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/caddyconfig/caddyfile/dispenser.go#L60) - [`LoadModule`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/context.go#L188) - [`replace`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/replacer.go#L175) - [`provisionHeaderAliasAllowlist`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/modules/caddyhttp/server.go#L385) - [`ModuleInfo`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/modules.go#L63) - [`NewContext`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/context.go#L65) - [`RegisterModule`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/modules.go#L138) ## Key subsystems ### Core logging and network addresses The root `caddy` package surface: the `Log` global logger accessor in `logging.go`, network address parsing and joining in `listeners.go` (`JoinNetworkAddress`, `IsUnixNetwork`), and the config adapter registry in `caddyconfig/configadapters.go`. 467 symbols, the largest cluster in the repo, and its heaviest edge runs into the module loader (24 calls). ### Caddyfile dispenser The token reader in `caddyconfig/caddyfile/dispenser.go`. `Next`, `Val`, `NextArg`, and `ArgErr` are how every Caddyfile directive walks its own arguments. 428 symbols, and its edges fan out to the httpcaddyfile adapter (24 calls), the core, and the directive registry. ### Module loading and TLS provisioning `Context.LoadModule`, `LoadModuleByID`, and `loadModuleInline` in `context.go`: the reflection walk that turns raw JSON config into provisioned module instances. The graph clusters it with the `caddytls` automation code it provisions, 417 symbols in all, and its heaviest edge runs into the core cluster (44 calls). ### Placeholder replacer `Replacer.replace` and `NewReplacer` in `replacer.go`, the `{placeholder}` substitution engine every other layer reaches for. 348 symbols. Placeholders resolve inside request handling, so the cluster's heaviest edge points at the HTTP server internals (28 calls). ### HTTP server internals `modules/caddyhttp/server.go`: request serving, trusted proxy resolution (`determineTrustedProxy`), and the header alias provisioning a server runs at startup. 297 symbols, hub `provisionHeaderAliasAllowlist`. ### Module descriptors and plugin types The `ModuleInfo` descriptor in `modules.go` plus the plugin config structs that declare one, from `Argon2idHash` in caddyauth to `LeafFileLoader` in caddytls. 222 symbols. This is the shape of a plugin catalog: types that exist to be registered and unmarshaled into. ### httpcaddyfile adapter `caddyconfig/httpcaddyfile`: the adapter that turns a parsed Caddyfile into JSON config, including the automation policy consolidation in `tlsapp.go` (`subjectQualifiesForPublicCert`, `automationPolicyIsSubset`). 220 symbols, and it reads its input through the dispenser (29 calls). ### Context and metrics `NewContext` in `context.go` with the metrics registry and the instrumented route wrappers in `modules/caddyhttp/metrics.go`. 178 symbols. ### Shared infrastructure Two smaller clusters round out the diagram: the reverse proxy upstreams (167 symbols, whose hub `resetDynamicHosts` is defined in `dynamic_upstreams_test.go`, which is what a hub looks like when the test suite drives every dynamic-upstream path) and response encoding (162 symbols, the Accept-Encoding negotiation and compressing response writer in `modules/caddyhttp/encode`). ## Canonical request flow The sequence worth reading first is a config apply: what happens when a new configuration reaches the admin API. Take the config lock, check the If-Match hash, decode the JSON, provision a fresh Context, load every module the config references, start the apps, then finish setting up the admin endpoint and config loaders. Every step below is a call edge in the config-swap cycle, in the order the work happens. 1. `changeConfig` ([`caddy.go:158`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/caddy.go#L158)) takes `rawCfgMu`, verifies the If-Match hash against the current config, applies the mutation, and indexes any `@id` fields before reloading. 2. `unsyncedDecodeAndRun` ([`caddy.go:337`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/caddy.go#L337)) strips the meta fields, decodes strictly into a `Config`, refuses a recursive config load, then swaps the running context and stops the old one. 3. `run` ([`caddy.go:419`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/caddy.go#L419)) provisions the context, starts each app in turn, and rolls back the apps it already started when one fails. 4. `provisionContext` ([`caddy.go:484`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/caddy.go#L484)) opens the loggers, resolves the storage module, replaces the local admin server, and loads every app in `AppsRaw`. 5. `Context.LoadModule` ([`context.go:188`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/context.go#L188)) reflects over the struct field, reads the `caddy:` tag for its namespace and `inline_key`, and dispatches on the field's kind. 6. `loadModuleInline` ([`context.go:478`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/context.go#L478)) pulls the module name out of the raw JSON object and joins it to the namespace to form a module ID. 7. `LoadModuleByID` ([`context.go:364`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/context.go#L364)) looks that ID up in the registry `RegisterModule` filled at init, calls the module's `New`, unmarshals the JSON into it, and provisions it. 8. `finishSettingUp` ([`caddy.go:594`](https://github.com/caddyserver/caddy/blob/45ba3278b5822f6fa6107a44ca41669b6e722482/caddy.go#L594)) establishes the server identity, replaces the remote admin endpoint, and starts the dynamic config loader. ## Health signals Symvanta detected 0 dependency cycles across 39 modules (modularity Q=0.81). 6 sets of mutually recursive symbols were also detected, the largest being `caddy` (9 symbols). The recursion that exists sits in the config path: `finishSettingUp` loads a config through `LoadConfig` and hands it back to `changeConfig`, which closes the loop through `run` and `provisionContext`. --- ### Firecracker Architecture: How It Actually Works https://symvanta.com/architecture/firecracker Firecracker is the virtual machine monitor behind AWS Lambda and Fargate: one Rust binary that boots stripped-down microVMs on KVM, driven by a REST API over a unix socket, with its own virtio devices, seccomp filters, and a jailer. Symvanta's graph at `81b38b9` detects 46 functional modules (modularity Q=0.79), and the three biggest clusters are guest memory and the device trait built on it (886 symbols, hub `GuestMemoryMmap`), the virtio descriptor ring (739, hub `Queue`), and the VMM core the API drives (714, hub `Net`). The map is clean for a systems repo. The hubs are real domain types (`GuestMemoryMmap`, `Queue`, `ParsedRequest`, `VirtioBlock`), the graph carries zero dependency cycles and just 2 pairs of mutually recursive symbols, and the heaviest edge runs from the VMM core into guest memory (210 calls). The devices sit in a ring around guest memory, which is what a VMM looks like when every device's job is moving bytes in and out of the guest. Two virtio transports coexist in that ring: the MMIO transport in `devices/virtio/transport/mmio.rs`, and a PCI stack of 460 symbols under `vmm/src/pci` whose `VirtioPciDevice` puts the same devices on a PCI bus. ## Module map The diagram shows the 10 largest of the 46 detected modules, with edges weighted by how many calls cross between them. The biggest holds 886 symbols and its hub is `GuestMemoryMmap`, the guest-physical-memory handle every virtio device keeps a clone of. Module names are checked by hand against the files their members live in and renamed where the generated label misread the cluster; the symbol counts, hubs, and edge weights are what the graph computed.
firecracker-microvm/firecracker module map: the 10 largest of 46 detected modules with call-weighted edges, generated by Symvanta
Module map of firecracker-microvm/firecracker, generated by Symvanta. Link to this diagram Open full size
## Where to start reading The raw PageRank ranking says little on this repo. Half of its top twelve are scaffolding: `http_request` is a helper inside the parsed-request test module (`api_server/parsed_request.rs:423`), `single_region_mem` comes from `vmm/src/test_utils`, `default_virtio_mem`, `default_mem` and `default_vmm` are per-suite defaults, and `is_activated` is a `DummyDevice` stub in the MMIO device manager. The other half are converters and bare method names: `u64_to_usize`, `write_be_u16`, `host_page_size`, plus `new`, `add` and `setup`. A hypervisor's most-called functions are tiny helpers. The list below is built from the boot path and the hub of each cluster in the diagram. - [`build_microvm_for_boot`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/builder.rs#L143) - [`ParsedRequest`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/firecracker/src/api_server/parsed_request.rs#L62) - [`VmmAction`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/rpc_interface.rs#L53) - [`GuestMemoryMmap`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/vstate/memory.rs#L38) - [`VirtioDevice`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/devices/virtio/device.rs#L83) - [`Queue`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/devices/virtio/queue.rs#L200) - [`DeviceState`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/devices/virtio/device.rs#L35) - [`Net`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/devices/virtio/net/device.rs#L251) - [`VirtioBlock`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/devices/virtio/block/virtio/device.rs#L243) - [`VirtioPciDevice`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/devices/virtio/transport/pci/device.rs#L261) ## Key subsystems ### Guest memory and the device trait `GuestMemoryMmap` is a collection of `GuestRegionMmapExt` regions (`vstate/memory.rs:38` and `:399`), and the graph clusters it with the `VirtioDevice` trait every device implements and the `KvmVm` handle they are wired against. 886 symbols, the largest module in the repo, and its heaviest outbound edge is the 136 calls into the VMM core. ### Virtio queues and rate limiting The descriptor ring in `devices/virtio/queue.rs`: available and used ring parsing, descriptor chain walking, and the `RateLimiter` token buckets that throttle block and net traffic. 739 symbols. The cluster also holds the `VirtQueue` fixture from `devices/virtio/test_utils.rs` that the device suites build rings with. ### VMM core and machine configuration The Louvain label for this cluster is the `Net` device, its hub, but the membership is the VMM top level: `VmmError` and `FcExitCode` in `vmm/src/lib.rs`, the `EventManager` loop, and the `vmm_config` structs (`BootSourceConfig`, `RateLimiterConfig`) that describe a machine before it boots. 714 symbols, and it carries the heaviest edge on the map: 210 calls into guest memory. ### API server and VmmAction The control plane. `ParsedRequest` (`firecracker/src/api_server/parsed_request.rs:62`) turns each request on the API socket into a `VmmAction` from `rpc_interface.rs`, and `VmResources` accumulates the machine config before boot. 646 symbols, and 196 of its calls land in the VMM core. ### Device activation and vsock `DeviceState` (`devices/virtio/device.rs:35`) is the enum every virtio device carries to say whether it is inactive or active, and the graph clusters it with the virtio-vsock packets, connections, and unix-socket muxer under `devices/virtio/vsock`. 642 symbols. ### Dumbo MMDS network stack `InnerBytes`, `MacAddr`, and `Connection` from `vmm/src/dumbo`: a hand-written TCP stack that serves the metadata service (MMDS) to guests directly from the VMM process, with the `Mmds` data store behind it. 555 symbols, and only 26 calls leave it, 14 of them into the API server that exposes MMDS. ### PCI and the virtio-pci transport Configuration space and BAR programming in `vmm/src/pci`, MSI-X interrupt tables in `pci/msix.rs`, and the `VirtioPciDevice` transport that puts a virtio device on the PCI bus. 460 symbols, hub `PciSBDF`, the segment, bus, device and function address a config access is decoded from. ### io_uring engine and bindings The async block-IO engine: the submission and completion rings in `vmm/src/io_uring`, `IoUringError`, and the bindgen kernel ABI types (`io_uring_sqe`, `Cqe`, the `__u32` family) the ring shares with Linux. 459 symbols, and nearly a leaf: 8 outbound calls, 5 of them into the block device that drives it. ### Block devices The virtio-block device in `devices/virtio/block/virtio`, its vhost-user variant, and the cache-type and file-engine selection a block device boots with. 459 symbols, and its heaviest edge is the 133 calls into the queue cluster, which is what a block device does for a living. ### Shared infrastructure The tenth cluster in the diagram is CPU templates and CPUID (396 symbols, hub `CustomCpuTemplate`): the register and CPUID leaf modifiers under `vmm/src/cpu_config`. Just outside it sit metrics and logging (356, hub `SharedIncMetric`), the jailer binary (333, hub `JailerError`), and machine configuration around the vm-memory crate's `GuestRegionMmap` (282), the one hub in the top fifteen that is not defined in this repo at all. ## Canonical request flow The sequence worth reading first is the microVM build: everything between an InstanceStart action and vcpu threads running. Allocate guest memory, open KVM, create the VM and its vcpus, register the memory regions, build the device manager, load the kernel, attach the block and net devices, configure the system for boot, then start the vcpus under their seccomp filters. Every step below is a call out of `build_microvm_for_boot`, in the order the work happens. Three of them are compiled per architecture: the links point at the x86_64 copy, and aarch64 carries a twin of each. 1. `build_microvm_for_boot` ([`builder.rs:143`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/builder.rs#L143)) takes the boot source, the accumulated `VmResources`, the event manager, and the seccomp filter map, and returns a paused `Vmm`. 2. `VmResources.allocate_guest_memory` ([`resources.rs:519`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/resources.rs#L519)) turns the machine config's memory size and huge-page setting into a vector of guest regions. 3. `Kvm.new` ([`vstate/kvm.rs:29`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/vstate/kvm.rs#L29)) opens `/dev/kvm` and applies the capability modifiers the CPU template asked for. 4. `KvmVm.new` ([`arch/x86_64/vm.rs:74`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/arch/x86_64/vm.rs#L74)) creates the VM file descriptor and the arch-specific interrupt state. The struct is per-arch; its shared methods come from one `impl KvmVm` block in `vstate/vm.rs:141`. 5. `KvmVm.create_vcpus` ([`vstate/vm.rs:200`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/vstate/vm.rs#L200)) creates one `Vcpu` per configured vcpu, each with its own exit event fd. 6. `KvmVm.register_dram_memory_regions` ([`vstate/vm.rs:453`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/vstate/vm.rs#L453)) hands the allocated regions to KVM as guest memory slots. 7. `DeviceManager.new` ([`device_manager/mod.rs:233`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/device_manager/mod.rs#L233)) builds the MMIO bus, the PCI bus when PCI is enabled, the serial console, and the resource allocator every later attach draws addresses from. 8. `load_kernel` ([`arch/x86_64/mod.rs:500`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/arch/x86_64/mod.rs#L500)) reads the kernel image into guest memory and returns its entry point, then `InitrdConfig::from_config` places the initrd behind it. 9. `attach_block_devices` ([`builder.rs:659`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/builder.rs#L659)) registers each configured block device on the bus and appends its root-device arguments to the kernel command line. 10. `attach_net_devices` ([`builder.rs:691`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/builder.rs#L691)) does the same for the net devices, and `attach_pmem_devices` follows for persistent memory. 11. `configure_system_for_boot` ([`arch/x86_64/mod.rs:237`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/arch/x86_64/mod.rs#L237)) applies the CPU template to every vcpu, writes the boot parameters and the device information the guest reads at startup, and seals the command line. 12. `KvmVm.start_vcpus` ([`vstate/vm.rs:243`](https://github.com/firecracker-microvm/firecracker/blob/81b38b9dad6056d7a48073e95ac5a9aed51cb2ab/src/vmm/src/vstate/vm.rs#L243)) moves each vcpu onto its own thread, installs the `vcpu` seccomp filter there, and leaves the state machine paused until the API says resume. ## Health signals Symvanta detected 0 dependency cycles across 46 modules (modularity Q=0.79). 2 sets of mutually recursive symbols were also detected, the largest being `transport` (2 symbols). Both pairs are small and local: `MmioTransport.set_device_status` (`transport/mmio.rs:165`) loops back through the transport's own `write`, and the second pair sits in the virtio queue fixtures. --- ### vLLM Architecture: How It Actually Works https://symvanta.com/architecture/vllm vLLM is an inference engine for large language models: a continuous-batching scheduler feeds a paged KV cache and tensor-parallel model execution, behind an OpenAI-compatible HTTP server. Symvanta's graph at `fe76112` detects 423 functional modules (modularity Q=0.85), and the largest by a wide margin is the plumbing every model file imports: the tensor-parallel state accessors and the `Platform` interface, 3832 symbols in one cluster. That shape is the structural signal. Hundreds of model implementations and kernels reach for the same distributed-state, platform, and logging helpers, so the utility clusters grow huge while the engine itself stays compact: the V1 scheduler cluster holds 1571 symbols and the engine core client 787. Three of the ten largest clusters take their hub from `vllm/logger.py`. The graph found no module-level dependency cycles across the 423 modules. ## Module map The diagram shows the 10 largest of the 423 detected modules, with edges weighted by how many calls cross between them. The biggest holds 3832 symbols and its hub is `get_tp_group`, the tensor-parallel group accessor in `vllm/distributed/parallel_state.py`. The heaviest edge pair on the map runs between that cluster and the log-once helpers: 338 calls one way, 247 back. The `LLM.generate` cluster ranks eighth by size (1153 symbols) and is left out as scaffolding, because most of its members are the asset and output-comparison helpers under `tests/`. Module names are checked by hand against the files their members live in; the symbol counts, hubs, and edge weights are what the graph computed.
vllm-project/vllm module map: the 10 largest of 423 detected modules with call-weighted edges, generated by Symvanta
Module map of vllm-project/vllm, generated by Symvanta. Link to this diagram Open full size
## Where to start reading These are the load-bearing entry points: the PageRank ranking over the call graph, blended with the hub symbol of each of the largest clusters, followed by the OpenAI-compatible server's HTTP surface. Seven entries are left out below. `_print_warning_once`, `_VllmLogger.warning_once`, and `init_logger` are the logging wrappers in `vllm/logger.py` that every file in the repo calls, `_nvmlGetFunctionPointer` lives in the vendored NVML bindings at `vllm/third_party/pynvml.py`, `RemoteVLLMServer.url_for` is the harness server wrapper in `tests/utils.py`, `hf_api` is a cached Hugging Face client accessor in `vllm/transformers_utils/repo_utils.py`, and `random_uuid` is a bare request-id utility. They rank high without pointing anywhere useful. `LLM.generate` is listed even though the blend drops it with its scaffolding cluster: it is the public offline API and it ranks seventh in the raw PageRank order. - [`get_tp_group`](https://github.com/vllm-project/vllm/blob/fe76112ff2981e9373765d76687d53f94880f38e/vllm/distributed/parallel_state.py#L1389) - [`LLM.generate`](https://github.com/vllm-project/vllm/blob/fe76112ff2981e9373765d76687d53f94880f38e/vllm/entrypoints/llm.py#L418) - [`ModelConfig.is_encoder_decoder`](https://github.com/vllm-project/vllm/blob/fe76112ff2981e9373765d76687d53f94880f38e/vllm/config/model.py#L1806) - [`get_kv_cache_torch_dtype`](https://github.com/vllm-project/vllm/blob/fe76112ff2981e9373765d76687d53f94880f38e/vllm/utils/torch_utils.py#L404) - [`AsyncMPClient.call_utility_async`](https://github.com/vllm-project/vllm/blob/fe76112ff2981e9373765d76687d53f94880f38e/vllm/v1/engine/core_client.py#L1129) - [`_parse_gemma4_args`](https://github.com/vllm-project/vllm/blob/fe76112ff2981e9373765d76687d53f94880f38e/vllm/parser/gemma4.py#L68) - `GET /health` - `GET /version` - `POST /tokenize` - `POST /detokenize` - `GET /v1/models` - `POST /v1/chat/completions` - `POST /v1/completions` - `POST /v1/embeddings` - `POST /pooling` - `POST /score` ## Key subsystems ### Tensor parallel state and platform interface `get_tp_group` and the accessors around it in `vllm/distributed/parallel_state.py` cluster with the `Platform` interface from `vllm/platforms/interface.py` (`is_rocm`, `is_cuda`, `fp8_dtype`, `get_device_capability`). 3832 symbols, the largest module in the repo. Every model implementation asks which distributed group it belongs to and what hardware it runs on, and those two habits pull the whole model zoo into one cluster. ### V1 scheduler and KV cache specs The engine's brain: `Scheduler.schedule` and `Scheduler.add_request` from `vllm/v1/core/sched/scheduler.py`, `Request.num_tokens`, and `register_all_kvcache_specs` from `vllm/v1/core/single_type_kv_cache_manager.py`. 1571 symbols. Its hub is `_print_warning_once` from `vllm/logger.py`, a log wrapper that ranks first here because the scheduler path calls it more than anything else in the cluster. The canonical flow below starts in this module. ### Model config and GPU input batch `ModelConfig.is_encoder_decoder` from `vllm/config/model.py` clusters with the V1 GPU worker state it shapes: `InputBatch.req_ids` (`vllm/v1/worker/gpu_input_batch.py`), `BlockTable.map_to_kernel_blocks` (`vllm/v1/worker/block_table.py`), and the sampling kernel `apply_top_k_top_p_triton` (`vllm/v1/sample/ops/topk_topp_triton.py`). 1392 symbols. Model shape decides batch layout, so the config accessors and the batch structures land together. ### KV cache dtype and attention metadata `get_kv_cache_torch_dtype` from `vllm/utils/torch_utils.py` sits with the shape and dtype questions an attention backend asks before it builds metadata: `ModelConfig.use_mla` and `ModelConfig.get_head_size` (`vllm/config/model.py`), `split_decodes_and_prefills` (`vllm/v1/attention/backends/utils.py`), and `MambaStateDtypeCalculator._mamba_state_dtype`. 929 symbols. ### SamplingParams and offline entrypoint utils Handles per-request sampling settings and the offline batch entrypoint: `SamplingParams.from_optional` (`vllm/sampling_params.py`), `ModelConfig.get_diff_sampling_param`, and the `OfflineInferenceMixin` run helpers in `vllm/entrypoints/offline_utils.py`. 1572 symbols. Its hub is `random_uuid` from `vllm/utils/__init__.py`, the request-id helper the serving path calls everywhere. ### EngineArgs and CLI parsing Provides functions for turning command-line flags into a running engine: `FlexibleArgumentParser.add_argument` and `parse_args` from `vllm/utils/argparse_utils.py`, then `EngineArgs.add_cli_args` and `EngineArgs.create_engine_config` from `vllm/engine/arg_utils.py`. The dataclass-reflection helper pair `is_init_field` and `get_field` sits here too. 1398 symbols. ### Engine core client `AsyncMPClient` from `vllm/v1/engine/core_client.py`, the msgpack codec in `vllm/v1/serial_utils.py`, and `AsyncLLM.generate` (`vllm/v1/engine/async_llm.py`): the async client side of the engine core process boundary. 787 symbols. This is the cluster that carries a request across the process split between the API server and the engine. ### Tool call and reasoning parsers `_parse_gemma4_args` from `vllm/parser/gemma4.py` and `get_json_schema_from_tools` from `vllm/tool_parsers/utils.py`: the code that reads a model's tool-call and reasoning output back into structured arguments. 755 symbols, the one cluster in the top 10 that lives in the serving layer. ### Shared infrastructure Two logging clusters round out the diagram, and between them they are why the map looks the way it does. Log-once helpers (1381 symbols, hub `_VllmLogger.warning_once`) holds the `warning_once` / `info_once` / `debug_once` family and the `_should_log_with_scope` rank gate. Logger factory and shared utils (1380 symbols, hub `init_logger`) holds logger construction plus small shared functions like `cdiv` and `resolve_obj_by_qualname`. Both are called from everywhere, which is what pins them near the top by size. ## Canonical request flow The sequence worth reading first is one engine step: the loop that turns queued requests into sampled tokens. Pick a batch, allocate KV cache blocks for it inside the schedule pass, dispatch the model without blocking, build the grammar bitmask for structured output, sample, then fold the model output back into scheduler state. Every step below is a call edge out of `EngineCore.step` and its `Scheduler.schedule` pass, in the order the work happens. 1. `EngineCore.step` ([`vllm/v1/engine/core.py:583`](https://github.com/vllm-project/vllm/blob/fe76112ff2981e9373765d76687d53f94880f38e/vllm/v1/engine/core.py#L583)) returns an empty result when the scheduler holds no requests, then drives one schedule, execute, sample, and update cycle. 2. `Scheduler.schedule` ([`vllm/v1/core/sched/scheduler.py:484`](https://github.com/vllm-project/vllm/blob/fe76112ff2981e9373765d76687d53f94880f38e/vllm/v1/core/sched/scheduler.py#L484)) spends a token budget across running and waiting requests. Each request simply gets tokens assigned until its computed count catches up with its token count, which is the one mechanism that covers chunked prefill, prefix caching, and speculative decoding. 3. `KVCacheManager.allocate_slots` ([`vllm/v1/core/kv_cache_manager.py:347`](https://github.com/vllm-project/vllm/blob/fe76112ff2981e9373765d76687d53f94880f38e/vllm/v1/core/kv_cache_manager.py#L347)) allocates the blocks a scheduled request needs and returns `None` when the pool is short, which is the signal that makes the scheduler preempt the lowest-priority request and try again. 4. `Executor.execute_model` ([`vllm/v1/executor/abstract.py:212`](https://github.com/vllm-project/vllm/blob/fe76112ff2981e9373765d76687d53f94880f38e/vllm/v1/executor/abstract.py#L212)) broadcasts the scheduler output to the workers over `collective_rpc` with `non_block=True`, so the engine can do the next step while the forward pass runs. 5. `Scheduler.get_grammar_bitmask` ([`vllm/v1/core/sched/scheduler.py:1720`](https://github.com/vllm-project/vllm/blob/fe76112ff2981e9373765d76687d53f94880f38e/vllm/v1/core/sched/scheduler.py#L1720)) collects the scheduled requests that use structured output and asks the structured-output manager for their bitmask rows, returning `None` when the batch has none. 6. `Executor.sample_tokens` ([`vllm/v1/executor/abstract.py:232`](https://github.com/vllm-project/vllm/blob/fe76112ff2981e9373765d76687d53f94880f38e/vllm/v1/executor/abstract.py#L232)) runs when the executor deferred sampling: `execute_model` hands back `None` in that mode, and this call ships the grammar bitmask to the workers and samples the batch. 7. `Scheduler.update_from_output` ([`vllm/v1/core/sched/scheduler.py:1744`](https://github.com/vllm-project/vllm/blob/fe76112ff2981e9373765d76687d53f94880f38e/vllm/v1/core/sched/scheduler.py#L1744)) reads sampled tokens, logprobs, and KV-connector results back, returns deferred-free blocks to the pool, and builds the `EngineCoreOutputs` the client receives. ## Health signals Symvanta detected 0 dependency cycles across 423 modules (modularity Q=0.85). 14 sets of mutually recursive symbols were also detected, the largest being `tool_parsers` (5 symbols). That group is the streaming XML tool-call parser's state machine (`StreamingXMLToolCallParser.setup_parser`, `_start_element`, `_end_element`, and the two reset helpers), which is what mutual recursion looks like when it is deliberate. --- ### Airflow Architecture: How It Actually Works https://symvanta.com/architecture/airflow Apache Airflow is a workflow orchestrator: a scheduler turns DAG definitions into task runs, executors run them, and provider packages connect those tasks to outside services. [Symvanta](https://symvanta.com) indexed the monorepo (`airflow-core`, `task-sdk`, `providers`, `chart`, and the `dev/breeze` tooling) and grouped its symbols into 500 functional modules (modularity Q=0.92), flagging a display cap at that number, so a repo this size carries more clusters than the map prints. The largest, 2,436 symbols anchored on `provide_session`, is the core's SQLAlchemy session plumbing, the decorator that hands a database session to any function whose caller did not supply one; the Google provider base hook follows at 2,001 symbols, and a cluster joining `LoggingMixin` to the Amazon base hook at 1,747. Four of the ten largest clusters hub inside `task-sdk`, which is where Airflow 3 moved the DAG-authoring API that operators, hooks, and task groups are written against. ## Module map The diagram below shows the 10 largest of the 500 detected modules, sized by symbol count, with arrows weighted by how many calls cross between them, the [kind of connectivity data a code graph captures that similarity search alone can't](/blog/code-embeddings-vs-code-graph). The two heaviest arrows both run into shared plumbing: `Core config and CLI bootstrap` into `Airflow core session plumbing` (193 calls), and `Google provider base hook` into `LoggingMixin and AWS base hook` (186). One drawn cluster stands apart: `Breeze developer CLI console` (1,195 symbols) is the repo's own developer tooling under `dev/breeze`, and its single outbound edge lands in a Breeze parameter cluster too small to draw. The repo's fifth-largest cluster is left off the diagram on purpose: 1,385 symbols hubbed on `render_chart` in `chart/tests/chart_utils/helm_template_generator.py`, which renders the Helm chart so the chart tests can assert against the Kubernetes objects it produces.
apache/airflow module map: the 10 largest of 500 detected modules with call-weighted edges, generated by Symvanta
Module map of apache/airflow, generated by Symvanta. Link to this diagram Open full size
## Where to start reading These are 12 of the codebase's most depended-upon symbols, blended from the global PageRank ranking and the hub of each of the ten largest modules, followed by a sample of the HTTP surface the graph indexed. Two raw PageRank entries are dropped. `render_chart` tops the raw ranking as the entry point the Helm chart suite runs every assertion through, and `validate_template_fields` ranks because the Amazon provider's operator tests call it from 33 files; both live under a `tests/` directory and neither is code an application runs. Start here to see how the pieces connect. - [`provide_session`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/utils/session.py#L85) - [`GoogleBaseHook.get_credentials_and_project_id`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/providers/google/src/airflow/providers/google/common/hooks/base_google.py#L330) - [`create_session`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/utils/session.py#L32) - [`LoggingMixin.log`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/utils/log/logging_mixin.py#L126) - [`get_console`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/dev/breeze/src/airflow_breeze/utils/console.py#L98) - [`BaseHook.get_connection`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/task-sdk/src/airflow/sdk/bases/hook.py#L52) - [`BaseSetupTeardownContext.update_context_map`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/task-sdk/src/airflow/sdk/definitions/_internal/setup_teardown.py#L131) - [`BaseOperator.__init__`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/task-sdk/src/airflow/sdk/bases/operator.py#L1029) - [`CommsDecoder.send`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/task-sdk/src/airflow/sdk/execution_time/comms.py#L250) - [`TaskGroup.child_id`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/task-sdk/src/airflow/sdk/definitions/taskgroup.py#L471) - [`mask_secret`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/task-sdk/src/airflow/sdk/log.py#L250) - [`aws_template_fields`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/providers/amazon/src/airflow/providers/amazon/aws/utils/mixins.py#L153) - `POST /auth/token` - `GET /auth/me` - `GET /assets` - `POST /assets/events` - `GET /assets/{asset_id}` - `GET /config` - `GET /dag_stats` - `GET /dagWarnings` - `POST /clearTaskInstances` - `POST /clearDagRuns` Three of these are worth calling out. `provide_session` and `create_session` are the same file's two halves: `create_session` opens a SQLAlchemy session and commits or rolls it back, and `provide_session` is the decorator that hands one to any function whose caller did not pass a `session` keyword. The graph records callers of `provide_session` in 35 files, reaching it from `airflow-core` models, dependency checks, and the Amazon, Google, Edge, and Standard providers. `BaseOperator.__init__` is 219 lines of argument validation that every operator in every provider package inherits, which is why a constructor ranks alongside the session helpers. `mask_secret` registers a value with the secrets masker in the task process and forwards it to the supervisor over the same comms channel, so a connection extra or a Variable is redacted in both processes' log output. ## Key subsystems ### Airflow core session plumbing The core's database access layer, 2,436 symbols around [`provide_session`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/utils/session.py#L85) in `airflow-core/src/airflow/utils/session.py`, plus the `LoggingMixin` accessors and the ORM model properties (`DagRun.state`, `DagVersion.get_latest_version`) that ride on the same sessions. It is the largest cluster on the map and the busiest destination: `Core config and CLI bootstrap` sends 193 calls into it, `Task SDK DAG authoring context` 63, and `Log context and scheduler queueing` 42. ### Google provider base hook `GoogleBaseHook` in `providers/google` is the base class every Google Cloud hook inherits: credential resolution, client options, quota project checks, and the `fallback_to_default_project_id` decorator that fills in a project id when a caller omits one. At 2,001 symbols it is the second largest cluster, and 186 of its calls land in the logging and AWS hook cluster, the heaviest single edge out of any provider. ### Core config and CLI bootstrap Configuration loading, CLI action wrappers, and process setup: `create_session`, `providers_configuration_loaded`, `action_cli`, `get_hostname`, and `Variable.get`. 1,841 symbols. This is the code that runs before anything schedules, and its 193 calls into the session cluster are the heaviest edge on the whole map. ### LoggingMixin and AWS base hook Louvain puts [`LoggingMixin.log`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/utils/log/logging_mixin.py#L126) and the Amazon provider's [`AwsGenericHook.conn`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/providers/amazon/src/airflow/providers/amazon/aws/hooks/base_aws.py#L778) in one 1,747-symbol cluster, because every boto3 session, connection config, and region lookup in `providers/amazon` logs through the mixin on its way. It receives the map's second heaviest edge (186 calls from the Google base hook) and sends 69 back. ### Log context and scheduler queueing A second 1,201-symbol logging cluster, holding `LoggingMixin.__init__` and `_set_context` (the logger a task instance writes through), the Google credential provider, and `SchedulerJobRunner._executable_task_instances_to_queued`. The split from the cluster above is real: one gathers around reading the logger, this one around configuring it per task instance. ### Breeze developer CLI console Breeze is Airflow's own development environment, and its console layer under `dev/breeze/src/airflow_breeze/utils/` is 1,195 symbols: [`get_console`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/dev/breeze/src/airflow_breeze/utils/console.py#L98), `console_print`, `run_command`, theming, and the cache-file helpers. It is tooling for working on Airflow, and it barely touches the rest of the graph: one outbound edge, into a Breeze build-parameter cluster the diagram does not draw. ### Task SDK BaseHook and Connection `BaseHook.get_connection` in `task-sdk/src/airflow/sdk/bases/hook.py` is the single call every provider hook makes to resolve a connection id, and `Connection.get`, `Connection.extra_dejson`, and `get_field` unpack what comes back. 1,067 symbols. It sends 78 calls into the logging and AWS cluster and 11 into the Google base hook, which is the shape of provider hooks resolving credentials. ### Task SDK DAG authoring context The DAG-authoring context machinery: `DAG.task`, `DAG.get_task`, `get_current_context`, and the setup/teardown context map hubbed on [`BaseSetupTeardownContext.update_context_map`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/task-sdk/src/airflow/sdk/definitions/_internal/setup_teardown.py#L131). 916 symbols, and 63 of its calls reach the core session cluster. ### Task SDK BaseOperator constructors [`BaseOperator.__init__`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/task-sdk/src/airflow/sdk/bases/operator.py#L1029) plus the argument validators it calls (`validate_key`, `validate_instance_args`) and the provider base-operator constructors that chain up to it, such as `DataplexCatalogBaseOperator.__init__` and `ManagedKafkaBaseOperator.__init__`. 907 symbols, almost all of it inheritance converging on one initializer. ### Task SDK supervisor comms How a running task talks to its supervisor process: [`CommsDecoder.send`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/task-sdk/src/airflow/sdk/execution_time/comms.py#L250) frames a request as length-prefixed msgpack, writes it to the supervisor over stdin, and blocks for the reply, with `_FrameMixin.as_bytes` and `CommsDecoder._from_frame` on either side of the wire. 881 symbols. User task code never speaks to the Task Execution API server itself, which the module's docstring gives two reasons for: it halves the concurrent HTTP connections on that server, and it keeps the per-try identity token out of user code. ## Canonical request flow The sequence worth reading first is one pass of the scheduler loop, the code that turns DAG definitions into queued task instances. Guard the critical section against stray commits, create the DAG runs that are due, start the queued ones, fetch the running runs to examine, schedule their task instances, resolve each run's serialized DAG for its callbacks, check how much executor capacity is free, enqueue task instances inside the critical section, then heartbeat the executors. Every step below is a call edge out of `_run_scheduler_loop` or its `_do_scheduling` pass, listed in the order the calls appear in the source. 1. [`SchedulerJobRunner._run_scheduler_loop`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/jobs/scheduler_job_runner.py#L1785) is the loop itself: an unbounded `itertools.count` that times every pass and breaks once the configured `num_runs` is reached. 2. [`SchedulerJobRunner._do_scheduling`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/jobs/scheduler_job_runner.py#L1985) makes the pass's decisions and returns how many task instances it queued. 3. [`prohibit_commit`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/utils/sqlalchemy.py#L661) wraps the work in a guard that raises if anything commits without going through the guard. 4. [`SchedulerJobRunner._create_dagruns_for_dags`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/jobs/scheduler_job_runner.py#L2460) creates the runs whose `next_dagrun_create_after` has passed. 5. [`SchedulerJobRunner._start_queued_dagruns`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/jobs/scheduler_job_runner.py#L2811) moves queued runs to running, within the per-DAG concurrency limits. 6. [`DagRun.get_running_dag_runs_to_examine`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/models/dagrun.py#L747) bulk-fetches the active runs in one query. 7. [`SchedulerJobRunner._schedule_all_dag_runs`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/jobs/scheduler_job_runner.py#L2907) walks those runs and collects the callbacks they produce. 8. [`DBDagBag.get_dag_for_run`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/models/dagbag.py#L206) resolves each run's serialized DAG, behind an `lru_cache` so repeated runs of one DAG cost one lookup. 9. [`BaseExecutor.slots_available`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/executors/base_executor.py#L617) is summed across executors; a zero total skips the critical section entirely. 10. [`SchedulerJobRunner._critical_section_enqueue_task_instances`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/jobs/scheduler_job_runner.py#L1201) takes the lock and moves task instances from scheduled to queued. 11. [`BaseExecutor.heartbeat`](https://github.com/apache/airflow/blob/4e4d0608c42f405e248df502184b4d6df99bf774/airflow-core/src/airflow/executors/base_executor.py#L349) then runs on every executor, whether or not it received work this pass. ## Health signals Symvanta detected 7 dependency cycles across 500 modules (modularity Q=0.92). The largest cycle spans 19 files in the `FlexibleForm` area. 28 sets of mutually recursive symbols were also detected, the largest being `serialization` (21 symbols). All 7 cycles sit in the repo's TypeScript: six in the React web UI under `airflow-core/src/airflow/ui/` (its form, graph, connections, and pagination components), one in the simple auth manager's login UI. The Python packages under `airflow-core/`, `task-sdk/`, and `providers/` carry none. --- ### Ollama Architecture: How It Actually Works https://symvanta.com/architecture/ollama Ollama is a local model runtime with an HTTP server in front of it: it resolves a model reference, loads the weights, schedules a runner, and streams tokens back. [Symvanta](https://symvanta.com)'s graph of the repo at `8f91241` groups it into 105 functional modules (modularity Q=0.90), and the four largest are the Go server with the naming layer under it, the shared API types with the per-model output parsers, the terminal chat UI, and the launcher that decides which installed model an editor starts with. Two things about the shape stand out. The chat path concentrates in a single handler: `ChatHandler` runs from line 2440 to line 2941 of `server/routes.go`, and five of the eleven endpoints listed below end there, the OpenAI-compatible `/v1/chat/completions` and the Anthropic-compatible `/v1/messages` among them. And the Apple silicon runtime under `x/` has grown into a second inference path in its own right: MLX model layers and quantization (678 symbols), the MLX KV cache (522), and the MLX runner loop itself (427) together outweigh the Go server cluster (1,261). ## Module map The diagram shows the 10 largest of the 105 detected modules, with edges weighted by how many calls cross between them. Every module name here was re-derived from the packages its members live in and checked symbol by symbol against the graph, because the clustering labels each module after its highest-PageRank symbol: three of those are helpers the test suite defines, and a fourth is a function from the Go standard library. The symbol counts, hubs, and edge weights are what the graph computed. The heaviest arrow runs from the launcher config cluster into the launcher itself (94 calls), and the launcher sends 33 back.
ollama/ollama module map: the 10 largest of 105 detected modules with call-weighted edges, generated by Symvanta
Module map of ollama/ollama, generated by Symvanta. Link to this diagram Open full size
## Where to start reading These are the most depended-upon symbols by PageRank over the call graph, blended with the hub of each module on the diagram, followed by the HTTP surface registered in `server/routes.go`. Six entries are left out below. `New` resolves to Go's own `crypto/md5.New`, and `Models` is a bare Go name that many packages define independently: nearly every editor adapter in `cmd/launch` has one, and so do `envconfig`, `cmd/config`, `api`, and `app/store`. `Arrays` is three different methods in `x/mlxrunner` that the graph groups by name. `setTestHome`, `setLaunchTestHome`, and `Setup` are test-harness helpers that rank high because every test in their package calls them. - `clamp` - `ParseRef` - `keyValue` - `NewKVCache` - `Var` - `Trace` - `POST /api/chat` - `POST /api/generate` - `GET /api/version` - `POST /v1/chat/completions` - `POST /v1/completions` - `POST /v1/embeddings` - `GET /v1/models` - `GET /v1/models/:model` - `POST /v1/responses` - `POST /v1/audio/transcriptions` - `POST /v1/messages` The endpoint list is worth a second look. Alongside its own `/api/*` surface, Ollama serves an OpenAI-compatible `/v1/chat/completions` and an Anthropic-compatible `/v1/messages`, and both land in the same `ChatHandler` as `/api/chat`. The compatibility work happens in gin middleware (`middleware.ChatMiddleware`, `middleware.AnthropicMessagesMiddleware`) that rewrites the request before the handler sees it, which is why one handler can serve three wire formats. ## Key subsystems ### HTTP server and model resolution The gin server and the naming layer it resolves against: `types/model` validates a reference, `manifest.BlobsPath` turns a digest into a path on disk, and `server` caches what it read. At 1,261 symbols it is the largest cluster on the map. Its two heaviest edges weigh 44 calls each, one into the API types cluster and one into the model reference cluster, and 41 come back from the latter. ### API types and model parsers The request and response types every surface shares, in `api/types.go` (`ThinkValue`, `ToolCallFunction`, `NewToolPropertiesMap`), together with the per-model output parsers that `model/parsers.ParserForName` dispatches to. At 1,173 symbols it is second on the map, and its PageRank hub is an external symbol: Go's own `crypto/md5.New`. ### Terminal chat UI The chat pane `ollama run` draws: `cmd/tui/chat` lays out the transcript, wraps text to the pane width, and repaints as tokens stream in. 1,155 symbols, third on the map, and its heaviest edge is 59 calls into the agent cluster that runs tools on the user's behalf. ### Launcher and model inventory `cmd/launch` decides which installed model an editor or coding agent starts with, through `fallbackLaunchModel`, `launchModelsFromNames`, and the cloud-limit rules in `model_inventory.go`. It calls the launcher config cluster 33 times and takes 94 calls back, the heaviest single edge on the diagram. ### MLX model layers and quantization The Apple silicon model stack: `x/models/nn` builds the linear and attention layers, and `x/mlxrunner/model/quant.go` resolves group size, bit width, and mode per quantized tensor. 678 symbols, with 31 calls into the MLX KV cache and 12 coming back. ### MLX KV cache and attention `x/mlxrunner/cache` holds the key-value cache the MLX path decodes against, along with the batching and causal-mask helpers around it. 522 symbols, ninth on the map, taking 31 calls from the layer stack and 9 from the runner loop. ## Canonical request flow A chat request enters at the gin route and stays inside one handler for most of its life. Every step below is a call out of `ChatHandler`, in the order its call sites appear. 1. `r.POST./api/chat` ([`server/routes.go:1909`](https://github.com/ollama/ollama/blob/8f912415e867d86a511ac51afbd6b79e0d1bbc35/server/routes.go#L1909)) registers the route, wrapped in `withInferenceRequestLogging`. 2. `server.Server.ChatHandler` ([`server/routes.go:2440`](https://github.com/ollama/ollama/blob/8f912415e867d86a511ac51afbd6b79e0d1bbc35/server/routes.go#L2440)) validates the request body and keeps the request through to line 2941. 3. `server.parseAndValidateModelRef` ([`server/model_resolver.go:36`](https://github.com/ollama/ollama/blob/8f912415e867d86a511ac51afbd6b79e0d1bbc35/server/model_resolver.go#L36)) splits the model string, and a cloud source sends the request straight to the proxy path. 4. `server.getExistingName` ([`server/routes.go:1222`](https://github.com/ollama/ollama/blob/8f912415e867d86a511ac51afbd6b79e0d1bbc35/server/routes.go#L1222)) searches the models directory for the longest prefix match and fills in the parts of the name that already exist there. 5. `server.Server.getModel` ([`server/model_inference_cache.go:116`](https://github.com/ollama/ollama/blob/8f912415e867d86a511ac51afbd6b79e0d1bbc35/server/model_inference_cache.go#L116)) serves the model out of the inference cache, falling back to `GetModel` in `server/images.go`; a miss on disk becomes the handler's 404. 6. `server.Model.Capabilities` ([`server/images.go:112`](https://github.com/ollama/ollama/blob/8f912415e867d86a511ac51afbd6b79e0d1bbc35/server/images.go#L112)) reports what the model supports, which is where a thinking request on a non-thinking model gets rejected. 7. `server.Server.scheduleRunner` ([`server/routes.go:202`](https://github.com/ollama/ollama/blob/8f912415e867d86a511ac51afbd6b79e0d1bbc35/server/routes.go#L202)) checks the model against those capabilities, then blocks on `s.sched.getRunner` until a loaded runner comes back. 8. `server.filterThinkTags` ([`server/routes.go:3122`](https://github.com/ollama/ollama/blob/8f912415e867d86a511ac51afbd6b79e0d1bbc35/server/routes.go#L3122)) strips `` blocks out of the assistant turns before the last user message, for qwen3 and deepseek-r1 only. 9. `server.chatModeForModel` ([`server/routes.go:2357`](https://github.com/ollama/ollama/blob/8f912415e867d86a511ac51afbd6b79e0d1bbc35/server/routes.go#L2357)) picks the execution mode, and the native path leaves this handler entirely. 10. `server.chatPrompt` ([`server/prompt.go:23`](https://github.com/ollama/ollama/blob/8f912415e867d86a511ac51afbd6b79e0d1bbc35/server/prompt.go#L23)) renders the messages through the model template, dropping them from the front until the result fits the context window. ## Health signals Symvanta detected 3 dependency cycles across 105 modules (modularity Q=0.90). The largest cycle spans 2 files in the `components` area. 12 sets of mutually recursive symbols were also detected, the largest being `convert` (3 symbols). All three cycles sit in the desktop UI, under `app/ui/app/src`: two between React components and one between the `useChats` and `useSelectedModel` hooks. The Go server has none. Seven of the twelve recursion groups are recursive-descent walks over a nested value: two in the model output parsers (`parseArray` / `parseObject` / `parseValue`, and the Olmo3 equivalents) and five in `model/renderers`, the schema and tool-argument formatters. --- ### Prometheus Architecture: How It Actually Works https://symvanta.com/architecture/prometheus Prometheus is a single Go binary that scrapes targets, writes samples into a local time series database, and answers PromQL queries against it. Symvanta's graph of the repo at `d8eeedd` detects 83 functional modules (modularity Q=0.79), and the weight sits in storage: the largest cluster holds 1,363 symbols around `labels.FromStrings`, and it sends 520 calls into the head block and chunk pool cluster beside it and takes 744 back. The scrape loop, service discovery, remote write, and the HTTP API all arrange themselves around that pair. The clearest structural signal is where the cycles are. All 8 dependency cycles live in the web UI (`web/ui/react-app`, `web/ui/mantine-ui`, and the codemirror-promql module). The Go packages that make up the server have none. ## Module map The diagram shows the 10 largest of the 83 detected modules, with edges weighted by how many calls cross between them. The biggest holds 1,363 symbols and its hub is `labels.FromStrings`, which puts label construction at the center of the codebase: nothing reaches a block or a chunk without building a label set first. Module names are checked by hand against the files their members live in; the symbol counts, hubs, and edge weights are what the graph computed.
prometheus/prometheus module map: the 10 largest of 83 detected modules with call-weighted edges, generated by Symvanta
Module map of prometheus/prometheus, generated by Symvanta. Link to this diagram Open full size
## Where to start reading These are the most depended-upon symbols by PageRank over the call graph, followed by the HTTP surface registered in `web/web.go`. Three of them are one-word Go names that several types define independently, so each link points at the one definition the ranking scored: `Labels.Get` in `model/labels`, `Head.MinTime` in `tsdb`, and the `bstream` accessor in `tsdb/chunkenc`. - [`labels.FromStrings`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/model/labels/labels_stringlabels.go#L321) - [`labels.Labels.Get`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/model/labels/labels_stringlabels.go#L192) - [`tsdb.Head.MinTime`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/head.go#L1998) - [`labels.EmptyLabels`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/model/labels/labels_stringlabels.go#L302) - [`histogram.FloatHistogram.UsesCustomBuckets`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/model/histogram/float_histogram.go#L65) - [`labels.decodeSize`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/model/labels/labels_stringlabels.go#L38) - [`strutil.SanitizeLabelName`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/util/strutil/strconv.go#L45) - [`histogram.IsCustomBucketsSchema`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/model/histogram/generic.go#L68) - [`labels.NewMatcher`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/model/labels/matcher.go#L56) - [`v1.createYAMLNode`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/web/api/v1/openapi_helpers.go#L311) - [`chunkenc.bstream.bytes`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/chunkenc/bstream.go#L61) - [`labels.NewFastRegexMatcher`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/model/labels/regexp.go#L56) - `GET /graph` - `GET /federate` - `GET /consoles/*filepath` - `GET /user/*filepath` - `GET /-/healthy` - `HEAD /-/healthy` - `GET /-/ready` - `HEAD /-/ready` - `GET /-/quit` - `GET /-/reload` ## Key subsystems ### Label sets and TSDB database The largest cluster in the repo. `labels.FromStrings` and the string-packed `Labels` representation in `model/labels/labels_stringlabels.go` sit with the database lifecycle in `tsdb/db.go` (`DefaultOptions`) and the compactor beside it (`DefaultPostingsDecoderFactory` in `tsdb/compact.go`). Every read and every write builds a label set before it opens a block, so label construction and database setup land in one cluster. ### Scrape loop and appendables The scrape loop and the appendable interfaces every scrape writes through, with staleness handling from `model/value.IsStaleNaN`. 1,018 symbols, and the ingest path shows in its edge weights: 129 calls into the label and database cluster, 89 into label string encoding, 62 into label access and config parsing. ### Head block and chunk pool The in-memory head block (`DefaultHeadOptions` in `tsdb/head.go`), the chunk pool that hands out and recycles chunk objects (`chunkenc.NewPool`), and label hashing (`Labels.Hash`). Its pair of edges with the label and database cluster is the heaviest coupling on the map: 744 calls out and 520 back. ### Label access and config parsing `labels.EmptyLabels` and the label accessors in `model/labels`, clustered with the loaders that read them: `ScrapeConfig.Validate` in `config/config.go`, `rulefmt.ParseFile`, and the PromQL parser constructor. Config and rule files are label-shaped, so the parsers land next to the accessors. ### PromQL functions and native histograms The PromQL function table in `promql/functions.go` (`simpleFloatFunc`) clustered with the native histogram arithmetic in `model/histogram` (`FloatHistogram.UsesCustomBuckets`, `compactBuckets`, `floatBucketIterator`). Histogram functions are where the query engine and the histogram model meet, 868 symbols in all. ### Write-ahead log segments `tsdb/wlog`: segment naming and listing (`SegmentName`, `listSegments`) alongside the label symbol table in `model/labels`. 691 symbols, and it calls into the head and chunk pool cluster 250 times, the third heaviest edge on the map. ### Service discovery core The `discovery` registry and metrics plumbing every service discovery provider registers through (`NewMetricRegisterer` in `discovery/util.go`, `NewRefreshMetrics`), plus `strutil.SanitizeLabelName`, which turns provider metadata into valid label names. 629 symbols, and its heaviest edges leave for the providers (6 into the AWS discovery cluster, 3 each into configuration management and the scrape loop). It reaches the label and database cluster once, which is how isolated this subsystem is from storage. ## Canonical request flow The sequence worth reading first is the database opening. Repair index files an old release left behind, clear temporary directories and checkpoints, build the chunk pool, take the directory lock, construct the compactor, open the write-ahead log, then create the head block and replay the log into it. Every step below is a call edge out of `tsdb.open`, in the order the work happens. 1. `tsdb.Open` ([`tsdb/db.go:902`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/db.go#L902)) is the public entry point: it validates the options, derives the block ranges from them, and hands the rest to the unexported `open`. 2. `tsdb.open` ([`tsdb/db.go:986`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/db.go#L986)) runs every step below in one function and returns a `*DB` that is ready to serve. 3. `tsdb.repairBadIndexVersion` ([`tsdb/repair.go:30`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/repair.go#L30)) rewrites the index and `meta.json` versions on blocks written by Prometheus 2.1, before anything tries to read them. 4. `tsdbutil.RemoveTmpDirs` ([`tsdb/tsdbutil/remove_tmp_dirs.go:27`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/tsdbutil/remove_tmp_dirs.go#L27)) deletes leftover temporary directories, once under the WAL directory and once under the data directory. 5. `wlog.DeleteTempCheckpoints` ([`tsdb/wlog/checkpoint.go:89`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/wlog/checkpoint.go#L89)) removes checkpoint directories from a truncation the process died in the middle of. 6. `chunkenc.NewPool` ([`tsdb/chunkenc/chunk.go:356`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/chunkenc/chunk.go#L356)) builds the pool that hands out and recycles chunk objects, and the `DB` struct takes it at construction. 7. `tsdbutil.NewDirLocker` ([`tsdb/tsdbutil/dir_locker.go:44`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/tsdbutil/dir_locker.go#L44)) takes the lock file in the data directory so a second process cannot open the same database. 8. `tsdb.NewLeveledCompactorWithOptions` ([`tsdb/compact.go:208`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/compact.go#L208)) constructs the compactor over those block ranges and the chunk pool. 9. `wlog.NewSize` ([`tsdb/wlog/wlog.go:300`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/wlog/wlog.go#L300)) opens the write-ahead log at the configured segment size, and opens a second log for out-of-order samples when that window is set or a WBL is already on disk. 10. `tsdb.NewHead` ([`tsdb/head.go:284`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/head.go#L284)) creates the in-memory head block over both logs. 11. `tsdb.Head.Init` ([`tsdb/head.go:729`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/head.go#L729)) replays the write-ahead log into the head. This is where a restart spends its time. 12. `wlog.WL.Repair` ([`tsdb/wlog/wlog.go:400`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/wlog/wlog.go#L400)) runs only when that replay returns a read error: the log is truncated at the last good record, and the WBL takes the same path through its own error type. 13. `tsdb.DB.run` ([`tsdb/db.go:1260`](https://github.com/prometheus/prometheus/blob/d8eeedd6734b7cc2bf16a2806128fd00bc3c3b19/tsdb/db.go#L1260)) starts the background loop that reloads blocks, signals compaction, and mmaps head chunks for the life of the process. ## Health signals Symvanta detected 8 dependency cycles across 83 modules (modularity Q=0.79). The largest cycle spans 16 files in the `graph` area. 17 sets of mutually recursive symbols were also detected, the largest being `parser` (16 symbols). Every one of those cycles sits in the browser code: the React app under `web/ui/react-app`, the Mantine app under `web/ui/mantine-ui`, and the codemirror-promql editor module. The Go server packages carry none, and the recursion they do carry is the PromQL lexer and evaluator calling back into themselves, which is what a hand-written parser looks like in a graph. --- ### Prisma Architecture: How It Actually Works https://symvanta.com/architecture/prisma Prisma Next is a TypeScript rewrite of Prisma ORM, in Early Access on the default branch of prisma/prisma while Prisma ORM 7 continues from the repository's [`v7` branch](https://github.com/prisma/prisma/tree/v7). It moves the schema off the codegen path and onto a contract-first model: a `.prisma` file compiles to a versioned JSON contract plus TypeScript types, and queries are written against a composable DSL that compiles to SQL at runtime, described in the repo's own [ARCHITECTURE.md](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/ARCHITECTURE.md). [Symvanta](https://symvanta.com) indexed the monorepo and grouped its symbols into 500 functional clusters (modularity Q=0.96); the map flags a display cap at that number, so a monorepo laid out in ten numbered package groups carries more clusters than the map prints. The largest, 1,217 symbols anchored on `AnyExpression`, is the SQL expression AST every query plan is built from, and the ORM client that builds those plans calls into it 67 times while it calls back 62 times, the heaviest pair of edges on the map. ## Module map The diagram shows the 10 largest of 500 detected modules, sized by symbol count, with arrows weighted by how many calls cross between them, the [kind of connectivity data a code graph captures that similarity search alone can't](/blog/code-embeddings-vs-code-graph). Two pairs dominate it. `SQL Relational AST` and `SQL ORM Client Collections` trade 67 and 62 calls, the seam where a user's `db.orm.User.all()` becomes an expression tree. `SQL Storage Contract IR` and `SQL Schema IR` trade 13 and 7, the two representations of a database's structure that the contract compiler and the migration planner each work from. Integration-test clusters are kept off the diagram: `Postgres Port Test Harness` (347 symbols, hub `withPostgresPort`), `Mongo Port Test Harness` (333, hub `withMongoPort`), and `Engine Command Test Harness` (398, hub `JourneyContext`) all live under `test/integration/`, and they group by shared fixture scaffolding. Two production clusters just missed the top ten and are worth knowing about: `Symbol Table Management` (384 symbols, hub `interpretPslDocumentToSqlContract`), which interprets a parsed PSL document into a SQL contract, and `Postgres Migration Tools` (244), whose 14 calls into `SQL Storage Contract IR` are the heaviest arrow not drawn.
prisma/prisma module map: the 10 largest of 500 detected modules with call-weighted edges, generated by Symvanta
Module map of prisma/prisma, generated by Symvanta. Link to this diagram Open full size
## Where to start reading These are 12 of the codebase's most depended-upon symbols, blended from the global PageRank ranking and the hub of each of the ten largest production modules. Four raw candidates are swapped out. `withPostgresPort` and `withMongoPort` rank first and sixth, and both are integration-test port harnesses under [`test/integration/test/ports/_harness/`](https://github.com/prisma/prisma/tree/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/test/integration/test/ports/_harness): every port test in the repo opens its database through one of them. `defineConfig` resolves to a build-config stub from the external `tsdown` package rather than to prisma source. `createOrmClient` lives in `examples/paradedb-demo`, a sample app rather than a shipped package. Start here to understand how the pieces connect. - [`AnyExpression`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/2-sql/4-lanes/relational-core/src/ast/types.ts#L2237-L2259) - [`SqlStorage`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/2-sql/1-core/contract/src/ir/sql-storage.ts#L148-L166) - [`CliStructuredError.is`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/1-framework/1-core/errors/src/control.ts#L117-L127) - [`SyntaxNode.children`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/1-framework/2-authoring/psl-parser/src/syntax/red.ts#L213-L221) - [`postgresError`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/3-extensions/postgres/src/errors.ts#L12-L18) - [`MongoAggExpr`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/2-mongo-family/4-query/query-ast/src/aggregation-expressions.ts#L472-L483) - [`defineNonEnumerable`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/2-sql/1-core/schema-ir/src/ir/sql-schema-ir-node.ts#L76-L88) - [`Collection`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/3-extensions/sql-orm-client/src/collection.ts#L2595-L2601) - [`MigrationToolsError`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/1-framework/3-tooling/migration/src/errors.ts#L39-L68) - [`toneSpans`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/1-framework/3-tooling/cli/src/utils/formatters/tone-markup.ts#L88-L110) - [`ColumnRef.of`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/2-sql/4-lanes/relational-core/src/ast/types.ts#L509-L511) - [`serializeValue`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts#L20-L48) A few of these are worth calling out individually. `AnyExpression` is the union every SQL expression node satisfies, and `ColumnRef.of` is the constructor that turns a table and column pair into the leaf those expressions are built from; both live in one 2,328-line [`ast/types.ts`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/2-sql/4-lanes/relational-core/src/ast/types.ts) that the whole SQL family compiles against. `SqlStorage` is the contract's picture of a database: namespaces, tables, and the type entries `normaliseTypeEntry` canonicalises. `Collection` is the type application code holds behind `db.orm.User`, `CollectionImpl` intersected with the aggregate reducers that model's contract declares. `CliStructuredError.is` is the type guard every CLI command runs on a caught error before deciding what to print, and `MigrationToolsError` is the migration package's own error class carrying a category. `SyntaxNode.children` walks the PSL parser's red tree. `postgresError` and its sibling `sqliteError` build the target-specific error envelopes each database extension throws. `serializeValue` renders a contract value back into emitted TypeScript source, and `MigrationCLI.run` ([`packages/1-framework/3-tooling/cli/src/migration-cli.ts`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/1-framework/3-tooling/cli/src/migration-cli.ts#L189-L227)) is the static entry point a migration module runs itself through, returning the process exit code. ## Key subsystems ### SQL Relational AST The largest cluster at 1,217 symbols: the query AST the SQL family shares, under [`packages/2-sql/4-lanes/relational-core/src/ast/`](https://github.com/prisma/prisma/tree/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/2-sql/4-lanes/relational-core/src/ast). `AnyExpression`, `Expression`, `AstNode`, and `ColumnRef` define the node types; `AstNode.freeze` and `frozenArrayCopy` make every node immutable once built, which is what lets one plan be inspected, rewritten by middleware, and rendered without copying. Its 62 calls into the ORM client are the return leg of the pair described above, and it reaches into a smaller `Expression Handling` cluster 29 more times. ### SQL ORM Client Collections 472 symbols in [`packages/3-extensions/sql-orm-client/`](https://github.com/prisma/prisma/tree/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/3-extensions/sql-orm-client), the layer application code touches. `CollectionImpl` carries the chainable query API (`.where()`, `.include()`, `.all()`) and `Collection` is the public type over it, `resolveModelTableName` and `domainModelTableInNamespace` map a contract model onto its physical table, and `JunctionThrough` carries many-to-many relations. `ormError`, `OrmCode`, and `OrmSubcode` give every failure here a structured code. This package also holds the repo's largest dependency cycle, 20 files (see Health signals below). ### SQL Storage Contract IR 735 symbols in [`packages/2-sql/1-core/contract/src/ir/`](https://github.com/prisma/prisma/tree/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/2-sql/1-core/contract/src/ir): the contract's own model of storage. `SqlStorage`, `SqlNamespace`, `SqlNamespaceEntries`, and `StorageTable` describe what exists in the database, and `PostgresSchema`, `PostgresTableSchemaNode`, and `PostgresDatabaseSchemaNode` specialise that for Postgres. Everything downstream reads the contract through this cluster, which is why `Postgres Migration Tools` calls into it 14 times and `SQL Schema IR` 7 more. ### SQL Schema IR 509 symbols in [`packages/2-sql/1-core/schema-ir/`](https://github.com/prisma/prisma/tree/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/2-sql/1-core/schema-ir), a second representation aimed at migrations: `SqlTableIR`, `SqlColumnIR`, `SqlSchemaIR`, plus the constraint nodes `SqlCheckConstraintIR`, `SqlForeignKeyIR`, `SqlUniqueIR`, and `SqlIndexIR`. The hub is `defineNonEnumerable`, a twelve-line helper the IR nodes use to attach a derivation-time field that stays out of `JSON.stringify`, out of structural test assertions, and out of spreads, while the one consumer that needs it at plan time still reads it as `node.field`. The contract IR and the schema IR call each other 13 and 7 times, the second-heaviest pair on the map. ### PSL Syntax Tree 615 symbols in [`packages/1-framework/2-authoring/psl-parser/src/syntax/`](https://github.com/prisma/prisma/tree/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/1-framework/2-authoring/psl-parser/src/syntax), a red/green syntax tree of the sort rust-analyzer and Roslyn use: immutable `GreenNode` and `GreenElement` values hold the shape and text, and the red layer (`SyntaxNode`, `SyntaxToken`, `SyntaxElement`) wraps them with absolute offsets and parent links computed on demand. `SyntaxNode.children`, `findChildToken`, and `SourceFile.positionAt` are how the CLI and the language server navigate a `.prisma` file. The cluster has almost no outbound weight (three calls total) because a syntax tree is read by everything and calls almost nothing. ### Mongo Aggregation Expressions 522 symbols in [`packages/2-mongo-family/4-query/query-ast/`](https://github.com/prisma/prisma/tree/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/2-mongo-family/4-query/query-ast). `MongoAggExpr`, `MongoAggExprNode`, `MongoFilterExpr`, `MongoFieldFilter`, and `MongoAggOperator.of` are the MongoDB half of the same idea the SQL AST implements: a typed expression tree that a target lowers into a driver command. It calls into the pipeline-stage cluster (hub `MongoStageNode`) 11 times, and that cluster calls back 15 times, since a `$match` stage holds a filter expression and a filter expression is assembled inside a stage. The whole `2-mongo-family` group repeats the `2-sql` layering, with its own foundation, authoring, tooling, query, transport, and runtime tiers. ### Postgres Target and DDL 545 symbols spanning the Postgres target, the postgres extension, and the SQL family's control adapter. `PostgresDdlNode`, `PostgresDdlVisitor`, `AlterTableAction`, and `quoteIdentifier` are the DDL side, the nodes a migration plan renders into `CREATE TABLE` and `ALTER TABLE`; `ExecuteRequestLowerer.lowerToExecuteRequest` lowers a statement into the shape the driver executes; and `postgresError` with `PostgresTargetErrorCode` shapes what surfaces when Postgres rejects it. The same three-way split (target, adapter, driver) repeats under [`packages/3-targets/`](https://github.com/prisma/prisma/tree/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/3-targets) for SQLite, and `Database Query Utilities` (315 symbols, hub `sqliteError`) is its SQLite twin, calling into this cluster 12 times. ### CLI Errors and Command Actions 649 symbols across [`packages/1-framework/1-core/errors/`](https://github.com/prisma/prisma/tree/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/1-framework/1-core/errors) and the CLI that consumes it. `CliStructuredError.is` and `CliStructuredError.code` classify a caught error, `normalizeError` puts anything thrown into that shape, and `ActionableCliError` with `ActionableCliError.nextActions` carries the remediation steps `chooseAction` and `runCommandAction` print. Its heaviest edge, 12 calls into `Migration Tools`, is the CLI invoking the migration engine; `Migration Tools` calls back 5 times to raise its own errors through the same reporting path. ### Also on the map Two more of the ten drawn modules are CLI-side. `Migration Tools` (401 symbols, hub `MigrationToolsError`) lives in [`packages/1-framework/3-tooling/migration/`](https://github.com/prisma/prisma/tree/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/1-framework/3-tooling/migration) and assembles contract spaces: `createAggregateContractSpace`, `makeAggregateContractSpace`, and `createContractSpaceAggregate` combine the per-package contracts of a workspace into the single view a migration plans against. `CLI Migration Output Rendering` (398, hub `toneSpans`) is everything the terminal shows while that runs: `toneSpans` and `toneDrawing` parse the CLI's inline tone markup, `ClassifiedEdge` and the renderers under [`src/utils/formatters/`](https://github.com/prisma/prisma/tree/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/1-framework/3-tooling/cli/src/utils/formatters) draw the migration graph, and `shortDisplayHash` abbreviates a contract hash for display. ## Canonical request flow Prisma Next exposes no HTTP surface of its own (the endpoint scan over prisma/prisma returns nothing), so the flow traced here is the read path every query takes: what happens between `db.orm.User.all()` and the SQL text that reaches the driver. 1. `CollectionImpl.all` ([`packages/3-extensions/sql-orm-client/src/collection.ts#L1025`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/3-extensions/sql-orm-client/src/collection.ts#L1025-L1027)) is the read terminal. It takes an optional `configure` callback for typed annotations, folds them into the collection's state, and calls the private dispatch. 2. `CollectionImpl.#dispatch` ([`packages/3-extensions/sql-orm-client/src/collection.ts#L2492`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/3-extensions/sql-orm-client/src/collection.ts#L2492-L2501)) packs the accumulated builder state into one options object: the execution context, the runtime, the collection state, and the table, model, and namespace names. 3. `dispatchCollectionRows` ([`packages/3-extensions/sql-orm-client/src/collection-dispatch.ts#L75`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/3-extensions/sql-orm-client/src/collection-dispatch.ts#L75-L109)) branches on whether the query has includes. With none, it compiles and runs one select; with includes, `dispatchWithIncludes` lowers every include descriptor into correlated subqueries so the read path still issues a single query. 4. `compileSelect` ([`packages/3-extensions/sql-orm-client/src/query-plan-select.ts#L1451`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/3-extensions/sql-orm-client/src/query-plan-select.ts#L1451-L1506)) turns that state into a plan. It resolves polymorphism against the contract, builds the projection and any table-inheritance joins, assembles a `SelectAst`, and derives the parameter list from the `ParamRef` nodes inside it. 5. `queryPlanRows` ([`packages/3-extensions/sql-orm-client/src/query-plan-rows.ts#L5`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/3-extensions/sql-orm-client/src/query-plan-rows.ts#L5-L10)) is a six-line seam: it hands the plan to `scope.query(plan)`, where `scope` is a `RuntimeScope`, the two-method interface `sql-relational-core` owns so the ORM client and the runtime share one contract without a layering inversion. 6. `SqlRuntimeBase.query` ([`packages/2-sql/5-runtime/src/sql-runtime.ts#L309`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/2-sql/5-runtime/src/sql-runtime.ts#L309-L314)) is the implementation behind that interface. It forwards to `queryAgainstQueryable`, which opens an async generator, prepares the plan, and streams decoded rows back. 7. `SqlRuntimeBase.lowerToDraft` ([`packages/2-sql/5-runtime/src/sql-runtime.ts#L254`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/2-sql/5-runtime/src/sql-runtime.ts#L254-L256)) runs inside that preparation. It produces a draft with SQL rendered and params filled from the user-domain values the lowering collected from `ParamRef` nodes. No codec encoding has happened yet, which is the window where a middleware can still mutate those params through the `SqlParamRefMutator`. 8. `lowerSqlPlan` ([`packages/2-sql/5-runtime/src/lower-sql-plan.ts#L16`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/2-sql/5-runtime/src/lower-sql-plan.ts#L16-L41)) calls `adapter.lower(ast, { contract, params })`, unwraps the returned literal slots into a bare value array, and freezes the result. A bind-site slot arriving here means the caller sent a prepared-statement AST down the ad-hoc path, and it raises `RUNTIME.PREPARE_BIND_ON_ADHOC`. 9. `PostgresAdapterImpl.lower` ([`packages/3-targets/6-adapters/postgres/src/core/adapter.ts#L85`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/3-targets/6-adapters/postgres/src/core/adapter.ts#L85-L97)) is the concrete adapter behind that interface call. It refuses DDL, which belongs to the control adapter, and delegates the rest with its codec registry attached. 10. `renderLoweredSql` ([`packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts#L151`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts#L151-L155)) walks the AST and emits Postgres-flavoured `{ sql, params }`. It collects the ordered `ParamRef` nodes first, assigns each a `$n` index, then renders. The runtime and control entry points share this one function so an emitted migration and a live query produce byte-identical SQL for the same AST. Two of those steps go through an interface rather than a direct call, and both are the extension points the rewrite is built around. Step 5 into step 6 crosses `RuntimeScope`, which is how the same ORM client drives a plain connection, a pooled one, or a test double. Step 8 into step 9 crosses `Adapter.lower`, which is how one AST reaches Postgres, SQLite, or any adapter a third party ships: `SqliteAdapterImpl` implements the identical pair of `lower` and `renderLoweredSql` in [`packages/3-targets/6-adapters/sqlite/src/core/adapter.ts`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/packages/3-targets/6-adapters/sqlite/src/core/adapter.ts). ## Health signals Symvanta detected 27 dependency cycles across 500 modules (modularity Q=0.96). The largest cycle spans 20 files in the `sql-orm-client` area: `collection-column-mapping.ts`, `aggregate-builder.ts`, `grouped-collection.ts`, `query-plan.ts`, `filters.ts`, and their siblings all reference each other while building one query plan, which is what a builder API with chainable, mutually-referencing stages produces. The suggested break edge runs from `collection-column-mapping.ts` into `collection-contract.ts`. The remaining 26 cycles are small: the next largest are 7 files each in `mongo-schema-ir` and in the SQL runtime's `prepared/` directory, then 6 in the Mongo contract IR. A Q of 0.96 across 500 clusters is the shape a monorepo with hard layering rules produces, and this repo writes those rules down: a [`dependency-cruiser.config.mjs`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/dependency-cruiser.config.mjs) and an [`architecture.config.json`](https://github.com/prisma/prisma/blob/dd6c12bfab432998a75e4cdcf8df45d65648e3d5/architecture.config.json) sit at its root, next to the ARCHITECTURE.md that states the dependency direction. 54 sets of mutually recursive symbols were also detected, the largest being `ast` (37 symbols): the expression node types in `relational-core` that reference each other by construction, since a `BinaryExpr` holds two `Expression` values and an `Expression` may be a `BinaryExpr`. The next two are the per-adapter SQL renderers, 26 symbols and 23 symbols, where `renderWindowFuncExpr`, `renderJoinOn`, `renderCastExpr`, and two dozen siblings call back into the shared `renderParts` and `renderProjection` as they descend an expression tree. Two more `ast` groups follow the same tree shape through the visitor methods each node type implements: 21 symbols across `.rewrite()` and 15 across `.fold()`. `query-ast` repeats all of it on the MongoDB side (14 symbols of node types, 10 of `.rewrite()`), and a 12-symbol `syntax` group covers the PSL red tree, where `SyntaxNode.children`, `childAt`, `climbingNext`, and `climbingPrev` re-enter each other while walking. Every one of these is a tree walked by functions that call themselves on child nodes, concentrated in the four places this codebase models trees. --- ### Symfony Architecture: How It Actually Works https://symvanta.com/architecture/symfony Symfony is a set of decoupled, independently versioned PHP components (DependencyInjection, HttpKernel, Validator, Form, Console, Serializer, and dozens more) that also compose into a full-stack web framework: the same components ship standalone for any PHP project to pull in one at a time. [Symvanta](https://symvanta.com) indexed the symfony/symfony monorepo and grouped its symbols into Louvain-detected clusters at modularity Q=0.95, the clean separation you would expect from a design rule that lets one component depend only on another component's public interface. The map lists 500 of those clusters and flags a display cap at that number, so a monorepo this size carries more of them than the map prints. The largest, 1,868 symbols anchored on `MockHttpClient`, gathers the Notifier component's 81 transport bridges and the Translation component's four remote providers: each talks to a vendor API through an injected HttpClient, and the same mock client stands in for that HttpClient throughout the repo. Dependency injection follows at 1,768 symbols around `ContainerBuilder`, then the Validator's constraint classes, VarExporter's lazy-object and Redis proxies, and the HttpFoundation request layer that HttpKernel's events and the Security component's tokens both sit on. ## Module map The diagram below shows the 10 largest of the 500 detected modules, sized by symbol count, with arrows weighted by how many calls cross between them, the [kind of connectivity data a code graph captures that similarity search alone can't](/blog/code-embeddings-vs-code-graph). `Notifier and Translation Transports` (1,868 symbols) leads because 81 notification bridges and four translation providers share one HTTP client and one message-sending shape. The heaviest arrow on the map runs from `Form Building and Rendering` into `Event Dispatching` (28 calls), the Form component firing a `FormEvent` at every stage of building, submitting, and validating a form; `Request and Security Context` into `Serialization and Property Metadata` (27 calls) is close behind.
symfony/symfony module map: the 10 largest of 500 detected modules with call-weighted edges, generated by Symvanta
Module map of symfony/symfony, generated by Symvanta. Link to this diagram Open full size
## Where to start reading These are 12 of the codebase's most depended-upon symbols, blended from the global PageRank ranking and the hub of each of the ten largest modules. Three raw candidates are swapped out below. `validate()` is a bare-name entry: the ranking lists symbols by name, and every constraint validator in the Validator component declares a method under that one name, so the interface that declares it, `ConstraintValidatorInterface`, takes its place. `assertWidgetMatchesXpath()` and `createContainerFromFile()` are test-only helpers that rank high because Symfony's own suite calls them from thousands of files; each gives way to the production class its cluster exists to exercise, the Twig bridge's `FormExtension` and FrameworkBundle's `FrameworkExtension`. Start here to understand how the pieces connect. - [`MockHttpClient`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/HttpClient/MockHttpClient.php#L27) - [`ContainerBuilder`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/DependencyInjection/ContainerBuilder.php#L59) - [`ConstraintValidatorInterface`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Validator/ConstraintValidatorInterface.php#L19) - [`initializeLazyObject()`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/VarExporter/LazyObjectInterface.php#L26) - [`RequestStack`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/HttpFoundation/RequestStack.php#L22) - [`ObjectNormalizer`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php#L32) - [`CommandTester`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Console/Tester/CommandTester.php#L23) - [`TransformationFailedException`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Form/Exception/TransformationFailedException.php#L19) - [`RouteCollection`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Routing/RouteCollection.php#L30) - [`EventDispatcher`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/EventDispatcher/EventDispatcher.php#L32) - [`FormExtension`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Bridge/Twig/Extension/FormExtension.php#L35) - [`FrameworkExtension`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L207) A few of these are worth calling out individually. `ContainerBuilder` ([`src/Symfony/Component/DependencyInjection/ContainerBuilder.php`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/DependencyInjection/ContainerBuilder.php)) is the class every bundle's `Extension::load()` calls to register a `Definition` or `ChildDefinition`, add a method call, or set an alias; compiler passes then walk the same object before `PhpDumper` writes it out as a compiled PHP class. `MockHttpClient` is the HttpClient stand-in every Notifier transport and every Translation provider in the repo is exercised against, which is why the largest cluster on the map forms around a class most applications meet only in their own suites. `initializeLazyObject()` is the `LazyObjectInterface` method every generated lazy ghost, lazy proxy, and Redis proxy implements, and the one each proxied method calls before it forwards, which is the inbound weight that puts it first in the PageRank ranking. `ConstraintValidatorInterface` is what every Validator constraint's validation logic implements; `ContainerConstraintValidatorFactory` resolves the right implementation for a given `Constraint` object via `$constraint->validatedBy()`. `RequestStack` is where a listener, a session handler, or a security token resolver reads the current request from, and `HttpKernel::handle()` pushes and pops it around every request. `TransformationFailedException` anchors the Form cluster because every data transformer in the component throws it when a submitted value cannot be converted back to a model value. `CommandTester` ([`src/Symfony/Component/Console/Tester/CommandTester.php`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Console/Tester/CommandTester.php)) is the Console component's in-process harness for running a command and reading back its output and exit code, shipped as public API of the component. `FormExtension` is the Twig bridge's form-rendering extension, the class that registers `form_row`, `form_widget`, `form_label`, and their siblings as Twig functions, and `FrameworkExtension` is the FrameworkBundle extension that reads a project's `framework.*` configuration and registers the services it implies. ## Key subsystems ### Notifier and Translation Transports The Notifier component's 81 transport bridges under [`src/Symfony/Component/Notifier/Bridge/`](https://github.com/symfony/symfony/tree/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Notifier/Bridge) (Slack, Twilio, Telegram, and 78 more), together with the Translation component: message catalogues, file loaders such as `XliffFileLoader`, and the four remote providers Crowdin, Loco, Lokalise, and Phrase. Every class here reaches a vendor API through an injected HttpClient, and `MockHttpClient` is the stand-in each one runs against inside the repo, which is why the cluster gathers on it. ### Container Definition Management Symfony's dependency injection container: the code that turns service definitions (from YAML, XML, PHP config, or `#[Autoconfigure]` attributes) into a compiled autowired service graph. `ContainerBuilder` is the hub every bundle's `Extension::load()` calls to register a `Definition` or `ChildDefinition`, compiler passes such as `CheckTypeDeclarationsPass` then walk the same object, and `PhpDumper` writes the result out as a compiled PHP class. At 1,768 symbols it is the second largest cluster on the map, and its two heaviest outbound edges (7 calls each) run into the routing cluster and into a smaller FrameworkBundle container-building cluster not drawn above. ### Validation and Constraints The Validator component: `Constraint` subclasses (`Length`, `Range`, `NotBlank`, `Valid`, and the rest of the 150 files under [`src/Symfony/Component/Validator/Constraints/`](https://github.com/symfony/symfony/tree/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Validator/Constraints)) declare a rule, and a matching class implementing `ConstraintValidatorInterface` runs it against a value. The cluster's hub is the bare `validate()` name, because every one of those validators declares a method with exactly that name. ### Lazy Object and Redis Proxies VarExporter's lazy-object machinery (`LazyGhostTrait`, `LazyProxyTrait`, and the `initializeLazyObject()` contract every generated proxy implements) plus the Cache component's Redis client proxies under `src/Symfony/Component/Cache/Traits/`. The Redis proxies are what make this the fourth largest cluster: [`Redis6Proxy`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/Cache/Traits/Redis6Proxy.php#L25) alone runs from line 25 to line 1266, re-declaring the phpredis surface (`get`, `set`, `eval`, `pipeline`, `getex`, `waitaof`, and hundreds of siblings) method by method, and every one of those methods calls `initializeLazyObject()` before it forwards. ### Request and Security Context HttpFoundation's request objects and session handlers, the HttpKernel events that carry them (`RequestEvent`, `ResponseEvent`), and the Security component's tokens and users (`UsernamePasswordToken`, `InMemoryUser`). `RequestStack` anchors the cluster because it is where every listener, session handler, and token resolver reads the current request from. Its heaviest outbound edge is 27 calls into the serialization cluster, which is also where `CsrfToken` and the controller-argument metadata classes sit. ### Serialization and Property Metadata The Serializer component's normalizers and encoders (`ObjectNormalizer`, `JsonEncoder`, `ClassMetadataFactory`, `AttributeMetadata`) and the PropertyInfo extractors they lean on, `ReflectionExtractor` above all. This is the layer that turns an object into an array and back, reading attributes and reflection to decide which properties travel and under what names. ### Also on the map Four more of the ten drawn modules carry the framework's plumbing. Console Command Execution (hub `CommandTester`) holds the Console component's `Command` class, the `Dotenv` loading around it, and the in-process harnesses `CommandTester`, `ApplicationTester`, and `CommandCompletionTester`, all three shipped as public API. Form Building and Rendering (hub `TransformationFailedException`) holds the Form component's `FormError`, the `DataMapper` that moves values between a form and its underlying object, the `getBuilder()` entry points, and the `renderRow()` and `renderHelp()` calls that drive Twig form themes. Route Collection and Matching (hub `RouteCollection`) adds `RequestContext`, which carries the scheme, host, and base URL a match runs against, and `UrlGenerator`, which walks the same collection in reverse. Event Dispatching (hub `EventDispatcher`) holds the dispatcher itself plus the `FormBuilder`, `FormConfigBuilder`, `Stopwatch`, and `ConstraintViolation` objects listeners pass around. ## Canonical request flow Symfony has no endpoints of its own to trace (the endpoint scan over symfony/symfony returns nothing but the Routing component's attribute fixtures under `src/Symfony/Component/Routing/Tests/Fixtures/`: HttpKernel is a library other applications embed), so the representative flow traced here is the request lifecycle every Symfony application runs through instead. 1. `handle()` ([`src/Symfony/Component/HttpKernel/HttpKernel.php`](https://github.com/symfony/symfony/blob/66f06e5e066c95109dd3251cf93d00adbbb82309/src/Symfony/Component/HttpKernel/HttpKernel.php)) is the entry point every Symfony front controller calls with the incoming `Request`. It pushes that request onto the `RequestStack` and delegates to `handleRaw()`, catching any `Throwable` and routing it to `handleThrowable()` before it can propagate (a caller passing `catch: false` gets the exception instead). 2. `handleRaw()` does the actual work: it dispatches a `RequestEvent` (`KernelEvents::REQUEST`), giving listeners like the router a chance to resolve a controller and short-circuit with a `Response` early (redirects, cached responses). If none did, it resolves the controller and its arguments, dispatching `ControllerEvent` and `ControllerArgumentsEvent` so listeners can swap the controller or its arguments before it runs. 3. `ControllerEvent`'s `getControllerReflector()` is what `handleRaw()` hands to `argumentResolver->getArguments()`: the reflection of the resolved controller, whose parameter list drives how each argument is resolved before the controller is invoked. 4. The controller runs and returns a value. If that value is anything other than a `Response`, `handleRaw()` dispatches a `ViewEvent` (`KernelEvents::VIEW`) so a listener can turn it into one; if nothing does, `handleRaw()` throws `ControllerDoesNotReturnResponseException`. 5. Whichever path produced the `Response`, `handleRaw()` returns it through `filterResponse()`, which dispatches `KernelEvents::RESPONSE` so listeners can modify headers, add cookies, or wrap the response, then calls `finishRequest()` to dispatch `KernelEvents::FINISH_REQUEST` for cleanup. Popping the request back off the `RequestStack` is separate: that happens in `handle()`'s `finally` block, so the previous request context is restored whether the request returned a response or threw. 6. If `handleRaw()` threw instead, `handleThrowable()` catches it and dispatches `KernelEvents::EXCEPTION` so an exception listener can substitute a proper error `Response`. With one supplied, the same `filterResponse()` and `finishRequest()` steps as the success path run, so an error response goes through identical header and cleanup handling; with no listener supplying one, `handleThrowable()` calls `finishRequest()` and rethrows. This event chain, `RequestEvent` then `ControllerEvent` then `ControllerArgumentsEvent` then `ViewEvent` then the response and finish-request events, with `ExceptionEvent` as the alternate branch on failure, is how nearly every framework feature, routing, security, the profiler, hooks into a request without `HttpKernel` itself knowing anything about them: the whole component is built around this one event pipeline. ## Health signals Symvanta detected 0 dependency cycles across the 500 modules (modularity Q=0.95), consistent with the module map above: Symfony's components are built to stand alone and touch only each other's public interfaces. 144 sets of mutually recursive symbols were also detected, the largest being `DependencyInjection` (33 symbols), where `FrameworkExtension::load()` and the per-feature registrars it calls (`registerNotifierConfiguration()`, `registerPropertyAccessConfiguration()`, and their siblings) reach back into each other while walking one bundle's configuration tree. Second is a 9-symbol group in the same component, the `ContainerBuilder` service-instantiation path where `resolveServices()`, `createService()`, and `getEnv()` re-enter each other while building one service's dependencies. Joint third is an 8-symbol ExpressionLanguage group tied with an 8-symbol HttpCache group: the first is the recursive-descent `Parser`, where `parseExpression()`, `parsePrimaryExpression()`, `parseArrayExpression()`, and their siblings call back into each other as the grammar nests; the second is `HttpCache` itself, where `lookup()`, `validate()`, `fetch()`, and `forward()` re-enter `handle()` to serve a stale entry or revalidate one. --- ### Cal.com Architecture: How It Actually Works https://symvanta.com/architecture/cal-com Cal.com is an open-source scheduling platform built on Next.js, tRPC, and a NestJS public API (`apps/api/v2`): a monorepo covering the booking web app, a versioned REST API for platform integrations, the embeddable Booker other sites drop onto their own pages, and the app-store adapters that connect a user's calendars, video providers, and payment processors. Symvanta's Louvain community detection organized the indexed symbols into 350 functional clusters (modularity Q=0.96), which for a monorepo this size means almost every call stays inside the cluster it starts in. The largest cluster is the web app's server-side request path around `buildLegacyRequest` (837 symbols): the bridge that rebuilds a legacy request out of App Router headers and cookies, the session resolver behind it, and the repositories a page loads through. Behind it sit the tRPC server layer around `TrpcSessionUser` (783 symbols) and the app-store integration layer around `getAppKeysFromSlug` (744 symbols), then the design system (`classNames`, 724 symbols), the webhook pipeline (`BaseEventDTO`, 643 symbols), and the booking audit trail (557 symbols). Louvain labels a cluster after the directory its members share whenever no symbol dominates it, and names a few after a fraction of what they hold, so two of the ten module names below were re-derived from the packages and symbols the cluster actually holds, checked one by one with `find_node` against the graph. Cal.com is released under the MIT License, with no separate commercial directory in the current codebase. ## Module map The diagram below shows the 10 largest of the 350 detected modules, sized by symbol count, with arrows weighted by how many calls cross between them. Two clusters sit at the center: `buildLegacyRequest`, the server request path every page renders through, and `safeStringify`, the log-safe serialization layer. The two point at each other, 14 calls one way and 7 back, and every other drawn module except the design system has an edge into one of them. Those 14 calls are the heaviest edge on the map, followed by a pair of 11s out of the app-store layer, one into the request path and one into the serializer, which is what a codebase looks like when every third-party integration logs its credential exchange through one PII-stripping serializer. The design system is the one box with no line attached: at this clustering its only outgoing edge lands in the avatar helpers, a cluster too small to draw.
calcom/cal.com module map: the 10 largest of 350 detected modules with call-weighted edges, generated by Symvanta
Module map of calcom/cal.com, generated by Symvanta. Link to this diagram Open full size
## Where to start reading These are the 12 most depended-upon symbols in the codebase, blended from the global PageRank ranking and the hub of each of the 10 diagrammed modules, so a product-shaped hub like `BookingOutput` is guaranteed a seat next to the infrastructure symbols PageRank favors: start here to understand how the pieces connect. - [`buildLegacyRequest`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/web/lib/buildLegacyCtx.ts#L47) - [`TrpcSessionUser`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/trpc/server/types.ts#L3) - [`getAppKeysFromSlug`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/_utils/getAppKeysFromSlug.ts#L4) - [`classNames`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/ui/classNames.ts#L3) - [`BaseEventDTO`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/webhooks/lib/dto/types.ts#L6) - [`DataRequirements`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/booking-audit/lib/service/EnrichmentDataStore.ts#L33) - [`safeStringify`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/lib/safeStringify.ts#L4) - [`BookingOutput`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/web/modules/bookings/types.ts#L17) - [`BaseEmail`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/emails/templates/_base-email.ts#L14) - [`useAtomsContext`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/platform/atoms/hooks/useAtomsContext.ts#L49) - [`renderEmail`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/emails/src/renderEmail.ts#L3) - [`createNextApiHandler`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/trpc/server/createNextApiHandler.ts#L10) Five raw PageRank entries lose their seat to a module hub, and one of them is a warning about the ranking itself. `hasPermission` ranks tenth globally and is not one function: [`packages/platform/enums`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/platform/enums/permissions.ts#L16) and [`packages/platform/utils`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/platform/utils/permissions.ts#L16) each define a permission-bitmask helper of that name, and the graph records a separate `PermissionCheckService.hasPermission` node in every file that calls that service, so the rank counts a name rather than a definition. The other four are single definitions that a hub outranked: [`useAppContextWithSchema`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/EventTypeAppContext.tsx#L34), the [`ErrorWithCode`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/lib/errors.ts#L3) constructor, [`getPlaceholderAvatar`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/lib/defaultAvatarImage.ts#L12), and [`useDataTable`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/web/modules/data-table/hooks/useDataTable.ts#L5). At this clustering no test helper reaches the raw ranking at all: all twelve entries are product code. A few of the twelve are worth calling out individually. `buildLegacyRequest` rebuilds a legacy request object out of App Router headers and cookies, and [`buildLegacyCtx`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/web/lib/buildLegacyCtx.ts#L51) beside it does the same for a whole `getServerSideProps` context, which is how pages that have not migrated keep working. `getAppKeysFromSlug` reads the stored credentials for one installed app by slug, the first thing every calendar, video, CRM, and payment integration does. `safeStringify` is the serializer the whole monorepo logs through, and [`getPiiFreeCredential`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/lib/piiFreeData.ts#L60) beside it is why: a credential reaches a log line with its tokens removed. `TrpcSessionUser` is the type every signed-in tRPC procedure receives, resolved by [`createContext`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/trpc/server/createContext.ts#L90) and enforced by [`authedProcedure`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/trpc/server/procedures/authedProcedure.ts#L27). `DataRequirements` is the hub of the booking audit trail, the interface that declares which related records the enrichment store has to load before a stored audit row can be rendered as text. `BookingOutput` types one row of the web app's bookings list. `renderEmail` and `BaseEmail` are the two halves of notification delivery, one turning a React template into HTML and one holding the send logic every template class inherits. ## Key subsystems ### The web app's server request path `buildLegacyRequest`, 837 symbols, is the largest cluster on the map and the only one of the ten that spans several packages, which is why Louvain named it after its hub. It covers what happens before an `apps/web` page renders: [`buildLegacyRequest`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/web/lib/buildLegacyCtx.ts#L47) and `buildLegacyCtx` reconstruct a legacy request and context from App Router headers, cookies, params, and search params; [`getServerSession`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/auth/lib/getServerSession.ts#L39) resolves who the request runs as; [`UserRepository`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/users/repositories/UserRepository.ts#L124) and [`FeaturesRepository`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/flags/features.repository.ts#L20) are the two repositories most pages load through; [`getTranslate`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/web/app/_utils.tsx#L15) loads the request's translations; and [`HttpError`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/lib/http-error.ts#L1) with [`getServerErrorFromUnknown`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/lib/server/getServerErrorFromUnknown.ts#L53) shape what a failed server call returns. Its heaviest outgoing edge is 14 calls into `safeStringify`, and it calls the app-store layer 8 times. ### The tRPC server layer `TrpcSessionUser`, 783 symbols, is `packages/trpc/server`: the request context and the procedures built on it. `createContext` resolves the session user a request runs as, `authedProcedure` is the base every signed-in procedure extends, [`createNextApiHandler`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/trpc/server/createNextApiHandler.ts#L10) mounts the router onto Next.js, and [`onErrorHandler`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/trpc/server/onErrorHandler.ts#L16) normalizes what a failed procedure returns to the client. Most of the rest of the cluster is the Zod input schema that each router declares for each of its procedures. Its heaviest edge on the map points into the request path 4 times, the same request resolving its session and locale on the way out. ### The app-store integration layer `getAppKeysFromSlug`, 744 symbols, is `packages/app-store`, where every calendar, video, CRM, and payment app is registered and configured. It holds per-app credential lookup ([`getAppKeysFromSlug`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/_utils/getAppKeysFromSlug.ts#L4), [`getParsedAppKeysFromSlug`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/_utils/getParsedAppKeysFromSlug.ts#L6)), the install path each app returns to ([`getInstalledAppPath`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/_utils/getInstalledAppPath.ts#L7)), and the OAuth callback state that survives the round trip to a provider ([`IntegrationOAuthCallbackState`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/types.d.ts#L8), [`encodeOAuthState`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/_utils/oauth/encodeOAuthState.ts#L6), [`decodeOAuthState`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/_utils/oauth/decodeOAuthState.ts#L8)). Its two heaviest edges tie at 11, one into the request path and one into `safeStringify`, and the named integrations under them are HubSpot 7, Salesforce 3, and Stripe 2. ### The shared UI surface The design system in `packages/ui`, 724 symbols, holds the [`classNames`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/ui/classNames.ts#L3) merge function alongside [`Tooltip`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/ui/components/tooltip/Tooltip.tsx#L8), [`Avatar`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/ui/components/avatar/Avatar.tsx#L70), [`buttonClasses`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/ui/components/button/Button.tsx#L42), and the [`IconName`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/ui/components/icon/icon-names.ts#L3) union that types every icon in the product. It is the most self-contained module drawn: one outgoing edge of 4 calls, into the avatar helpers, and nothing on the map calls back into it. Most of what a component here calls is another component in the same cluster. ### Webhook delivery `BaseEventDTO`, 643 symbols, is `packages/features/webhooks`: the pipeline that tells a customer's endpoint a booking changed. It holds the DTOs an event is serialized into ([`BaseEventDTO`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/webhooks/lib/dto/types.ts#L6), [`WebhookPayload`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/webhooks/lib/factory/types.ts#L76)), the queued task shape and its schema ([`WebhookTaskPayload`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/webhooks/lib/types/webhookTask.ts#L122), [`webhookTaskPayloadSchema`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/webhooks/lib/types/webhookTask.ts#L97)), and the subscriber lookup that decides who receives it ([`WebhookSubscriber`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/webhooks/lib/dto/types.ts#L416)). Its outgoing edges are all small, 2 into the request path and 1 into `safeStringify`, the shape of a subsystem that is handed a payload instead of going looking for data. ### The booking audit trail `booking-audit`, 557 symbols, is `packages/features/booking-audit`, the record of who changed a booking and what changed. [`BookingAuditContextSchema`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/booking-audit/lib/dto/types.ts#L65) and [`BaseStoredAuditData`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/booking-audit/lib/actions/IAuditActionService.ts#L26) define what gets written, [`AuditActorType`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/booking-audit/lib/repository/IAuditActorRepository.ts#L1) records who wrote it, and `DataRequirements` drives the enrichment store that turns a stored row back into readable text through [`TranslationWithParams`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/booking-audit/lib/actions/IAuditActionService.ts#L15). Its one edge on the map is 5 calls into `safeStringify`. Louvain labeled this cluster "Data Auditing Components"; the name here comes from the feature package its members all live in. ### Log-safe serialization and the video adapters `safeStringify`, 545 symbols, is the cluster the rest of the map leans on hardest: six of the other nine drawn modules have an edge into it. Alongside the serializer itself sit `getPiiFreeCredential`, which strips personal data out of a credential before it reaches a log line, [`getUid`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/lib/CalEventParser.ts#L215) from the calendar-event parser, [`findValidApiKey`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/_utils/findValidApiKey.ts#L9), and [`getVideoAdapters`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/getVideoAdapters.ts#L11), the lookup that turns a stored credential into a working video-provider client. It calls back into the request path 7 times and into the tRPC layer once. ### The bookings list `BookingOutput`, 512 symbols, is the bookings screen in `apps/web`. The hub, [`BookingOutput`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/web/modules/bookings/types.ts#L17), is one booking as the `viewer.bookings.get` tRPC router returns it, [`BookingRowData`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/web/modules/bookings/types.ts#L28) wraps that booking in the row state the table needs, [`BookingListingStatus`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/web/components/booking/types.ts#L3) is the status filter the listing runs under, taken straight off the router's input type, and [`BookingActionContext`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/web/components/booking/actions/bookingActions.ts#L7) carries the booking plus the flags every row action reads (`isUpcoming`, `isCancelled`, `isPending`, `isTabRecurring`). The platform atoms call into it 4 times and it calls back twice, the heaviest traffic between any two UI clusters on the map. ### Notification delivery `BaseEmail`, 499 symbols, is `packages/emails`: [`BaseEmail`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/emails/templates/_base-email.ts#L14) is the class every template extends, [`renderEmail`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/emails/src/renderEmail.ts#L3) turns a React template into the HTML that ships, and [`SMSManager`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/sms/sms-manager.ts#L23) covers the text-message half of the same job. Its heaviest edge, 13 calls, goes to a second and smaller emails cluster around [`BaseScheduledEmail`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/emails/src/templates/BaseScheduledEmail.tsx#L18) (165 symbols) that holds the layout primitives every template renders inside. Louvain named this cluster after the `templates` directory its members share; the name here is its hub. On the map it calls the request path twice and `safeStringify` twice. ### The platform atoms and the Booker `useAtomsContext`, 425 symbols, is `packages/platform/atoms`, the React components Cal.com ships to platform customers, plus the Booker state they drive. [`useAtomsContext`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/platform/atoms/hooks/useAtomsContext.ts#L49) and [`useIsPlatform`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/platform/atoms/hooks/useIsPlatform.ts#L3) tell a component whether it is rendering inside a customer's embed or inside Cal.com's own web app, and [`useBookerStoreContext`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/bookings/Booker/BookerStoreProvider.tsx#L23) exposes the Booker's in-progress selections to both. Its heaviest edge is 4 calls into the bookings list, and it calls `safeStringify` twice. ## Canonical request flow Cal.com's public API (`apps/api/v2`) exposes a versioned booking-creation endpoint at `POST /v2/bookings` (selected with the `cal-api-version: 2024-08-13` request header); tracing `relate(kind:chain)` downstream from its handler gives the following flow for creating a booking. 1. `BookingsController_2024_08_13.createBooking` ([`apps/api/v2/src/platform/bookings/2024-08-13/controllers/bookings.controller.ts:149`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/controllers/bookings.controller.ts#L149)) is the entry point, guarded by `OptionalApiAuthGuard` since booking creation does not require the caller to be signed in. It hands the parsed body, the raw request, and the optional user straight to the service layer. 2. `BookingsService_2024_08_13.createBooking` ([`services/bookings.service.ts:112`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/services/bookings.service.ts#L112)) runs the validation every booking has to pass regardless of type: [`getBookedEventType`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/services/bookings.service.ts#L188) resolves the event type from an id, a username and slug, or a team slug and slug; [`EventTypeAccessService.userIsEventTypeAdminOrOwner`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/event-types/services/event-type-access.service.ts#L19) decides whether the caller counts as an owner or host; [`checkBookingRequiresAuthenticationSetting`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/services/bookings.service.ts#L169) enforces the event type's own authentication rule with that answer; a managed parent event type is rejected outright; collective and round-robin event types go through [`checkEventTypeHasHosts`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/services/bookings.service.ts#L160); and [`hasRequiredBookingFieldsResponses`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/services/bookings.service.ts#L217) confirms the submitted form answers cover every required field. 3. Two flags off the resolved event type, `recurringEvent` and `seatsPerTimeSlot`, then pick exactly one of four sibling methods: [`createRegularBooking`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/services/bookings.service.ts#L459) for a plain single booking, [`createSeatedBooking`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/services/bookings.service.ts#L499) for a shared-seat event, and [`createRecurringBooking`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/services/bookings.service.ts#L399) / [`createRecurringSeatedBooking`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/services/bookings.service.ts#L428) for their recurring counterparts. 4. All four branches translate the API request into the internal booking shape through `InputBookingsService_2024_08_13` ([`createBookingRequest`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/services/input.service.ts#L97) for the two single-booking branches, [`createRecurringBookingRequest`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/services/input.service.ts#L242) for the recurring pair), then hand that shape to the shared booking engine in `packages/features`: [`RegularBookingService.createBooking`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/bookings/lib/service/RegularBookingService.ts#L2653) for the single branches, [`RecurringBookingService.createBooking`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/bookings/lib/service/RecurringBookingService.ts#L144) for the recurring ones. This is the same engine the web app books through, so the REST API adds a translation layer on top of it rather than a second implementation. 5. Each branch shapes its response through a matching `OutputBookingsService_2024_08_13` method, and the two non-recurring branches re-read the row they just wrote through `BookingsRepository_2024_08_13` first ([`getByUidWithAttendeesAndUserAndEvent`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/repositories/bookings.repository.ts#L149) for a regular booking) because the engine returns less than the API promises. If anything along the way throws, [`ErrorsBookingsService_2024_08_13.handleBookingError`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/services/errors.service.ts#L34), or [`handleEventTypeToBeBookedNotFound`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/bookings/2024-08-13/services/errors.service.ts#L10) for the specific case of a missing event type, converts the failure into the API's standard error shape before it reaches the caller. The four creation branches are siblings: a single request runs exactly one of them, and all four converge on the same input and output services, which is why `InputBookingsService_2024_08_13` and `OutputBookingsService_2024_08_13` receive calls from all four while `BookingsRepository_2024_08_13` receives calls from only the two non-recurring ones. ## Health signals Symvanta detected 25 dependency cycles across 350 modules (modularity Q=0.96). The largest spans 227 files and ties the notification code to the code that triggers it: the webhook notifier ([`WebhookNotifier.ts`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/features/webhooks/lib/service/WebhookNotifier.ts)), the webhook output mapper, the email templates ([`OrganizerRequestEmail.tsx`](https://github.com/calcom/cal.com/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/emails/src/templates/OrganizerRequestEmail.tsx) is one of several in the loop), the booking handlers that send them, and the app-store credential helpers those handlers call all sit in one import loop. The second largest spans 68 files of React components: the app-store apps' own settings components, the event-type and calendar screens in `apps/web`, and the platform atoms' wrappers around them. The other 23 are small and land in five places. Five are in the platform API and the types it shares: the calendars controller with its Outlook service, the conferencing controller with its Zoom, Office 365, and shared conferencing services, the throttler decorator with its guard, an event-type input with the validator it declares, and the bookings service with its own input service, which sits directly on the request path traced above. Eight are in `apps/web`, among the app shell, the settings, event-type, form-builder, navigation, OAuth-client, and app-installation screens, the largest of them the six-file installation wizard. Five are in `packages/features`: availability, feature opt-in, no-show handling, the form builder, and the embed tab UI. Three are in shared packages: the 16-file `embed-core` bundle, the calendar and video-adapter type declarations, and a text field that imports its own types file. The last two are test scaffolding, the Playwright fixtures and the booking-scenario mock. 15 sets of mutually recursive symbols were also detected, the largest being `availability` (12 symbols): `UserAvailabilityService` and the input and result types around it (`GetAvailabilityUser`, `CurrentSeats`, `GetUserAvailabilityInitialData`) reference each other while resolving when a user is free. A modularity Q of 0.96 says the 350 modules are cleanly separated: nearly all call traffic stays inside the module it starts in, and only a thin layer of edges crosses between them. --- ### etcd Architecture: How It Actually Works https://symvanta.com/architecture/etcd etcd is a distributed, reliable key-value store built on the Raft consensus protocol: the coordination store that Kubernetes and a long list of other distributed systems rely on to hold cluster state consistently across machines. Clients talk to it over gRPC (`Range`, `Put`, `Delete`, `Txn`, `Watch`, `LeaseGrant`, and friends), and every write that changes the store's state has to be proposed to and committed by Raft before it lands in the underlying storage engine. [Symvanta](https://symvanta.com)'s Louvain community detection organized the codebase's indexed symbols into 148 functional modules (modularity Q=0.92, a clean separation of concerns for a codebase this size, with zero circular dependencies between modules). The largest module that belongs to the server rather than to its harnesses is `etcdserver` (962 symbols), and its hub, [`raftRequest`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/v3_server.go#L1012), is the funnel every mutating client request passes through on its way to a Raft proposal. The modules around it exist to make that funnel safe: `mvcc` holds the versioned data, `wal` makes an entry durable before Raft is allowed to call it committed, `schema` gives the bolt database its bucket layout, and the two `client/v3` modules carry the request vocabulary and the connection every caller reaches the cluster through. ## Module map The diagram below shows the 10 largest of the 148 detected modules once test clusters are excluded, sized by symbol count, with arrows weighted by how many calls cross between them. Five of the biggest Louvain communities in the repository are excluded on that rule: the `tests/framework/integration` cluster at 1,103 symbols (the largest community in the codebase), the 699-symbol linearizability model under `tests/robustness/model`, the two clusters of the `e2e` framework (696 and 557 symbols), and the `tests/common` suite they share (269). Each of them exercises the server without being part of it.
etcd-io/etcd module map: the 10 largest of 148 detected modules with call-weighted edges, generated by Symvanta
Module map of etcd-io/etcd, generated by Symvanta. Link to this diagram Open full size
`etcdserver` carries the heaviest traffic in both directions. It makes 39 calls into `mvcc` and 36 into `schema`, and it receives 20 from `embed`, 17 from `rafthttp`, and 8 from `wal`. The storage packages form a tighter triangle underneath: `mvcc` into `schema` (7 calls), `schema` back into `mvcc` (16), and `wal` into `schema` (13). On the client side the two `client/v3` modules trade 37 calls one way and 13 the other, the request builders and the connection layer leaning on each other. ## Where to start reading In a 148-module codebase, finding the handful of symbols everything else depends on is exactly the kind of task that [trips up AI coding agents working from a file tree and grep instead of a call graph](/blog/why-ai-coding-agents-fail-large-codebases). These 10 are blended from the global PageRank ranking and the hub of each module in the diagram above, so load-bearing symbols from smaller modules keep their seats alongside the generic utilities that get called more often: start here to orient in the call graph. - [`raftRequest`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/v3_server.go#L1012) - [`ensureCompare`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/client/v3/compare.go#L119) - [`ContextError`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/client/v3/client.go#L606) - [`String`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/api/rafthttp/stream.go#L85) - [`tail`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L1067) - [`NewConfig`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/embed/config.go#L492) - [`Revision`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/auth/store.go#L1009) - [`enqueueResponse`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/cache/watcher.go#L42) - [`mustClientFromCmd`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/etcdctl/ctlv3/command/global.go#L154) - [`togRPCError`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/api/v3rpc/util.go#L98) Two entries from the raw ranking are dropped here, `cleanup` and `NewTmpBackendFromCfg`, throwaway-backend fixtures in [`server/storage/mvcc/kv_test.go`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/mvcc/kv_test.go#L881) and [`server/storage/backend/testing/betesting.go`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/backend/testing/betesting.go#L29) that etcd's storage suite calls from nearly every package. Four of the survivors (`String`, `tail`, `Revision`, `NewConfig`) are bare Go names the graph groups across receivers and packages, so every link above points at one exact definition. A few are worth calling out individually. `raftRequest` is `EtcdServer.raftRequest` ([`server/etcdserver/v3_server.go`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/v3_server.go)), the funnel every mutating v3 RPC reaches: it delegates to `processInternalRaftRequestOnce` on its first line, then reworks the `traceutil.Trace` that comes back so a slow proposal is logged against the request's own start time. `ensureCompare` is `clientv3.Cmp.ensureCompare` ([`client/v3/compare.go`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/client/v3/compare.go)), the step that normalizes the comparison a `Txn` guards on before the transaction goes out over the wire, and it anchors the whole `Op` and `OpOption` builder surface around it. `ContextError` sits at the other end of the client: it turns a cancelled or expired context into the error a caller actually sees, so a dead connection reads as a client-side timeout rather than a server fault. `String` is `rafthttp.streamType.String`, the label the two peer-stream flavors carry through logs and metrics. `tail` is `WAL.tail`, the accessor for the log segment currently being appended to: `Save` reaches through it to find the write offset, and `sync` reaches through it to pick the file descriptor it fsyncs. `Revision` is `authStore.Revision` ([`server/auth/store.go`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/auth/store.go)), the auth revision stamped into every request header so the server can tell a proposal built against a stale permission set from a current one. `togRPCError` ([`server/etcdserver/api/v3rpc/util.go`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/api/v3rpc/util.go)) sits on the boundary every client-facing RPC crosses, converting etcd's internal error values into the gRPC status codes clients see. `mustClientFromCmd` is `etcdctl`'s client constructor, which is why the CLI packages rank as high as they do. ## Key subsystems ### etcdserver The server core. `raftRequest` and `processInternalRaftRequestOnce` turn a client mutation into a Raft proposal and block until it has been applied, the apply loop that executes committed entries in index order lives beside them, and the membership accessors around both (`MemberID`, `AuthStore`, `ToMemberDir`) answer who this node is and who else is in the cluster. ### mvcc The multi-version key-value store. `NewStore` opens the bolt-backed store, `newTreeIndex` builds the in-memory index that maps a key to the revisions it was written at, and the tombstone and revision helpers around them decide what a read at a given revision returns. Its highest-PageRank member is a teardown fixture from the package's own suite (`cleanup`), which is what happens when a package is exercised from as many directions as etcd exercises this one; the thematic center is the store, the tree index, and the compaction path. ### client/v3 The client library's request vocabulary. `NewOp` and the `OpOption` builders assemble every Get, Put, Delete, and Txn a client sends, and `Cmp.ensureCompare` normalizes the comparison a transaction guards on before it goes out over the wire. At 621 symbols this is the largest module outside the server, and it is the one piece of etcd that ships into every consumer of the store. ### client/v3 connection The client library's connection layer. `newClient` builds the gRPC connection a `Config` describes, `translateEndpoint` turns an endpoint string into a dial target and its credential requirement, the retry interceptor decides which failures are safe to replay, and `ContextError` maps a dead context onto the error the caller sees. Louvain splits it from the request vocabulary because the two halves call each other far more than either calls anything else: 37 calls one way, 13 back. ### schema The bucket layout of the bolt database. `UnsafeReadConsistentIndex` reads the applied Raft index etcd stamps into the database on every commit, and the alarm, auth, lease, and membership buckets alongside it carry the server state that must survive a restart. `Validate` and `UnsafeMigrate` are what let one binary open a database another version wrote. ### rafthttp Peer-to-peer transport. `Transport.Send` hands outbound Raft messages to the right peer, each peer holds a `streamWriter` for the long-lived connection and a `pipeline` for one-shot posts, and `urlPicker` rotates through a peer's advertised URLs when one stops answering. It is the only module on the map whose whole job is talking to other machines. ### wal The write-ahead log. `Create` and `parseWALName` own the segment files, `encode` frames each record with a chained CRC, and `sync` is the fsync barrier a Raft entry clears before the cluster is allowed to call it committed. The [write-ahead log walkthrough](/architecture/etcd/write-ahead-log) follows one entry through this module to disk and back out again on restart. ### embed Server configuration. `NewConfig` builds the `Config` an embedded or standalone etcd starts from, and `Validate`, `InitialClusterFromName`, and the advertise-URL accessors decide whether that configuration describes a cluster this node can actually join. Its 20 calls into `etcdserver` are the boot sequence handing a validated config to the server it is about to start. ### auth The authentication store: users, roles, and the range-permission cache that answers whether a key is in scope for the caller. `Revision` is the auth revision every request header carries, so a proposal built against a stale permission set is rejected instead of applied. ### cache A watch-backed read cache that sits in front of a cluster. `Cache.Watch` fans one upstream watch out to many local watchers, `demux` keeps the local snapshot current from that stream, and `newRingBuffer` holds a bounded window of recent events so a lagging watcher resyncs from history instead of opening a second upstream watch. Its hub, `enqueueResponse`, is the non-blocking delivery step that returns false when a watcher buffer is full, which is how a slow consumer gets marked lagging instead of stalling the fan-out. ## Canonical request flow etcd has no plain HTTP surface of its own (the client API is gRPC), so the flow traced here is a client `Put` reaching consensus, as far as the graph's static call edges follow it: 1. `v3rpc.kvServer.Put` ([`server/etcdserver/api/v3rpc/key.go:90`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/api/v3rpc/key.go#L90)) is the gRPC entry point. It validates the request with `checkPutRequest`, calls the server's `Put`, routes any failure through `togRPCError` so the client gets a gRPC status code rather than an internal error value, and stamps the cluster and revision header on the way out. 2. `etcdserver.EtcdServer.Put` ([`server/etcdserver/v3_server.go:295`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/v3_server.go#L295)) is 14 lines and writes nothing. It wraps the request in an `InternalRaftRequest` and hands it to `raftRequest`. 3. `etcdserver.EtcdServer.raftRequest` ([`server/etcdserver/v3_server.go:1012`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/v3_server.go#L1012)) calls `processInternalRaftRequestOnce` on its first line, then reworks the `traceutil.Trace` that comes back on the result (`GetStartTime`, `SetStartTime`, `InsertStep`, `LogIfLong`) so a slow proposal gets logged against the request's own start time. 4. `etcdserver.EtcdServer.processInternalRaftRequestOnce` ([`server/etcdserver/v3_server.go:1058-1133`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/v3_server.go#L1058-L1133)) checks `exceedsRequestLimit` first (line 1062), which compares the server's applied and committed Raft indexes so a client cannot propose faster than the server can keep up. It then stamps the request with a unique ID (line 1067), resolves the caller's identity through `AuthInfoFromCtx` (line 1072), classifies the request with `getRequestType` (line 1086), marshals it (line 1093), registers a wait keyed on the request ID (line 1106), and proposes the bytes to the Raft node (line 1113). A proposal that times out is translated by `parseProposeCtxErr` (line 1129) before the error reaches the caller. The traced chain stops there. Submitting the proposal to Raft and the eventual write into etcd's mvcc storage cross a channel boundary: the bytes go into the Raft library, and the apply loop reads the committed entry back off a `Ready` channel later, on a different goroutine. The static call graph has no edge to follow across that gap. The apply side exists in the graph as its own connected run, `EtcdServer.run` to `applyEntries` to [`EtcdServer.apply`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/server.go#L1877) to [`applyEntryNormal`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/server.go#L1930), with no edge joining it to the propose side. One more seam sits at the end of that run: [`apply.applierV3backend.Put`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/apply/backend.go#L50), the function that finally writes the key, has no inbound call edge from `applyEntryNormal` at all, because the apply loop dispatches through the `applierV3` interface instead of naming it. The [Raft consensus spoke](/architecture/etcd/raft-consensus) follows the whole path, both sides of the channel included. ## Health signals Symvanta detected 0 dependency cycles across 148 modules (modularity Q=0.92): every module's call traffic resolves without a circular dependency between modules. Symvanta also detected 11 sets of mutually recursive symbols, ten of them two-symbol pairs. Six of the eleven are the same shape, and it is the shape etcd's data model dictates: a `Txn` can nest sub-transactions, so anything that walks a transaction has to recurse into its own children. Permission checking does it (`apply`: `checkTxnPermission` and `checkTxnReqsPermission`), so does request validation (`v3rpc`: `checkRequestOp` and `checkTxnRequest`), quota accounting (`server/storage/quota.go`: `costTxn` and `costTxnReq`), proxy translation (`grpcproxy`: `requestOpToOp` and `TxnRequestToOp`), key-prefix namespacing (`namespace`: `prefixOps` and `prefixOp`), and the client's own conversion to the wire format (`clientv3`: `toTxnRequest` and `toRequestOp`). The largest set is the four-symbol group in `etcdctl`'s watch command (`NewWatchCommand`, `parseWatchArgs`, `watchInteractiveFunc`, `watchCommandFunc`), where interactive mode re-enters the command parser it was launched from. --- ### Meilisearch Architecture: How It Actually Works https://symvanta.com/architecture/meilisearch Meilisearch is an open-source, Rust-based search engine: a self-hosted alternative to hosted search APIs, built around a fast full-text and vector-ranking core called `milli`. Symvanta's Louvain community detection organized the codebase into 61 functional modules (modularity Q=0.78, generated by [Symvanta](https://symvanta.com)'s code graph). The largest is the `meilisearch` server crate itself at 1728 symbols: the actix route handlers, the request and response types they deserialize, and the `IndexScheduler` handle every handler holds. The product-shaped structures sit inside these clusters as members. `IndexScheduler` owns the durable, async task queue that every write (document ingestion, settings change, index swap) is enqueued onto and processed from; a `Search`/`SearchContext` pair builds and walks a query graph over the index; `bucket_sort` ranks candidates through an ordered chain of ranking rules; and `Embedder` wraps one of several pluggable providers (HuggingFace, OpenAI, Ollama, a generic REST backend, user-supplied vectors, or a composite of an index-time and a search-time embedder) for semantic search. The clustering at this commit labels most modules with the generic Rust types that everything routes through (`Result`, `FieldId`, `Error`, `IndexUid`), so every module name on this page was re-derived from the crates and symbols the cluster actually holds, checked one by one against the graph. The repository also vendors a full typed client for the OpenAI API (`external-crates/async-openai`) and carries a large integration-test harness (`crates/meilisearch/tests/common`); both are real graph clusters but neither is Meilisearch's own architecture. Two subsystems are traced end to end on their own pages: the [indexing pipeline](/architecture/meilisearch/indexing-pipeline), how a document becomes searchable, and the [ranking rules](/architecture/meilisearch/ranking-rules), how results get ordered. ## Module map The diagram below shows the 10 largest of the 61 detected modules, sized by symbol count, with arrows weighted by how many calls cross between them. The largest is the Meilisearch HTTP API cluster (1728 symbols), the server crate that owns every route. One cluster that would otherwise place fifth by size is left off: the integration-test harness in `crates/meilisearch/tests/common` (1263 symbols, hub `Server`), a real graph cluster that is test scaffolding rather than runtime architecture, large only because the tests exercise the whole API surface. The vendored async-openai client stays on the map at 735 symbols, and a dozen smaller vendored fragments (audit-log, fine-tuning, and vector-store types) all trace back to that same bundled crate.
meilisearch/meilisearch module map: the 10 largest of 61 detected modules with call-weighted edges, generated by Symvanta
Module map of meilisearch/meilisearch, generated by Symvanta. Link to this diagram Open full size
## Where to start reading These are the load-bearing entry points into the codebase: the product-shaped structures at the center of the largest clusters, plus the handful of HTTP routes most requests actually go through. Start here to see how the pieces connect. - [`IndexScheduler`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/index-scheduler/src/lib.rs#L176) - [`Task`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/meilisearch-types/src/tasks/mod.rs#L32) - [`Index`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/index.rs#L129) - [`Search`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/mod.rs#L132) - [`SearchContext`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/mod.rs#L77) - [`Interned`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/interner.rs#L10) - [`Embedder`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/vector/embedder/mod.rs#L23) - [`Settings`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/meilisearch-types/src/settings.rs#L214) - [`Opt`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/meilisearch/src/option.rs#L214) - [`perform_federated_search`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/meilisearch/src/search/federated/perform.rs#L57) - [`insert_object`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/flatten-serde-json/src/lib.rs#L15) - `POST /indexes/{index_uid}/search` - `POST /indexes/{index_uid}/documents` - `PATCH /indexes/{index_uid}/settings` - `POST /multi-search` - `GET /tasks` The raw PageRank list at this commit is dominated by names the graph groups by bare identifier (`new`, `repr`, `parse_u32`, `variant_size`) and by helpers the integration-test harness calls from every test (`new_shared`, `default_settings`, `dynamic_search_rules_server`); those are dropped here in favor of the product-shaped structures at the center of each cluster, each one resolved to its defining crate before it went on the list. A few of these are worth calling out. `IndexScheduler` is the owner of every asynchronous write; it is declared inside the HTTP API cluster because that is where the handlers hold it, while its own methods (`register`, `tick`, `create_next_batch`, `process_batch`) cluster with milli's query execution. `Task` is the enqueued unit of work and the model the `/tasks` route serves. `Index` is milli's LMDB environment, the one structure the write path and the read path share. `Search` and `SearchContext` carry a query from the HTTP request into ranking, and `Interned` is the term-id type the ranking rules compare instead of strings. `Embedder` is the runtime core of the vector-search cluster, one variant per configured provider. `Settings` is the top-level, per-field-optional configuration object every `PATCH /indexes/{index_uid}/settings` call updates, and `Opt` is the process-level counterpart: every CLI flag and environment variable the binary accepts. `perform_federated_search` is the request-level orchestrator traced in the canonical flow below, and `insert_object` comes from the `flatten-serde-json` crate that turns nested JSON documents into the flat field-value pairs the index stores. The five HTTP routes above are Meilisearch's primary surface: search, document writes, settings, multi-index search, and task status. ## Key subsystems ### Meilisearch HTTP API Hub `IndexUid`, 1728 symbols, the largest cluster on the map. The `meilisearch` server crate: the actix route handlers, the request and response types they deserialize (`Param`, `SearchQuery`, `IndexUid`), `ResponseError`, and the `AuthController` that gates every call. `IndexScheduler` is declared here too, as the shared state the handlers hold. It makes 245 calls into Scheduler Runtime and Query Execution and 244 into Task and Error Types, because nearly every handler ends by enqueuing a task or reading one back. ### Scheduler Runtime and Query Execution Hub `Result`, 1413 symbols. Louvain grouped the scheduler loop and milli's query execution into one cluster, and its hub is the crate-local `Result` alias both sides return. It holds `tick`, `register`, `create_next_batch`, `process_batch`, `Queue`, and `IndexMapper` from the index-scheduler crate, next to milli's `Search`, `SearchContext`, `QueryGraph`, `execute_search`, `bucket_sort`, and every `RankingRule` implementation. It calls into Index Storage and Write Pipeline 253 times and into Query Term Interning and Bitmaps 183 times, and takes 447 calls back from the write pipeline. The [indexing pipeline trace](/architecture/meilisearch/indexing-pipeline) follows the scheduler half of this cluster from an HTTP write to a committed index; the [ranking rules trace](/architecture/meilisearch/ranking-rules) follows the query half. ### Task and Error Types Hub `Result`, 1392 symbols. The `meilisearch-types` task model: `Task`, `KindWithContent`, `Kind`, `Status`, `TaskId`, `Network`, and the error types the other crates return. This is the shape of a single enqueued job (document addition, settings update, index swap, snapshot) moving from `enqueued` through `processing` to `succeeded` or `failed`. It makes 191 calls into the HTTP API cluster and 129 into the settings model, since a settings change is itself dispatched as an asynchronous task. ### Index Storage and Write Pipeline Hub `Index`, 1369 symbols. milli's LMDB `Index` and the write path that fills it: the `index` entry point, `extract_all`, `write_to_db`, the `ExtractorBbqueueSender` channel between them, and `FilterableAttributesRule`. Its 447 calls into Scheduler Runtime and Query Execution are the heaviest edge on the whole map, and its 301 calls into On-disk Codecs and Field Ids are the second heaviest, because extraction is a loop that resolves document fields to field ids and writes their postings. ### On-disk Codecs and Field Ids Hub `FieldId` (a `u16` field identifier), 837 symbols. milli's heed codecs and key types: `DocumentId`, `DelAdd`, `FacetGroupKey`, `FacetGroupKeyCodec`, `BytesRefCodec`, `OrderedF64Codec`, and the LMDB key-size bound. It calls into Index Storage and Write Pipeline 230 times and back into Scheduler Runtime and Query Execution 198 times: reads and writes both flow through field identifiers and the on-disk databases they key. ### Embedders and Vector Search Hub `EmbedError`, 706 symbols. milli's `vector` module: `Embedder` with one variant per configured provider (HuggingFace, OpenAI, Ollama, a generic REST backend, user-provided vectors, and a composite of an index-time and a search-time embedder), plus `RuntimeEmbedder`, `RuntimeFragment`, `Embedding`, `DistributionShift`, and the `EmbedError` family. It calls into Index Storage and Write Pipeline 58 times and into the settings model 40 times, because an embedder is configured per index and rebuilt when those settings change. ### Vendored code and configuration Four more clusters round out the top ten. Dump Import and Export (hub `Error`, 977 symbols) is the `dump` crate: `IndexMetadata`, `Version`, `UpdateState`, and the per-version readers that import and export a Meilisearch dump. Vendored async-openai Client (hub `OpenAIError`, 735 symbols) is the bundled `external-crates/async-openai` typed client. CLI Options and Runtime Features (hub `default_settings`, 707 symbols) holds `Opt`, `RuntimeTogglableFeatures`, and the API-key bootstrap. Index Settings Model (hub `Setting`, 624 symbols) holds `Settings` and the `Setting` three-state wrapper that lets a configurable field be explicitly set, reset, or left alone rather than defaulting silently. ## Canonical request flow The representative flow traced here is a document search: what happens when a client calls `POST /indexes/{index_uid}/search`. 1. [`search_with_post`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/meilisearch/src/routes/indexes/search.rs#L869) (and its `GET` counterpart `search_with_url_query`) is the actix handler. It takes a search permit from the `SearchQueue`, builds a `DocumentSearch` around the parsed query, and awaits its `execute`. When the `legacy_search` experimental feature is on it falls back to `legacy_search_with_post` instead, the older path that calls `perform_search` directly. 2. [`DocumentSearch`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/meilisearch/src/documents_retrieval/mod.rs#L32) is the request bundle the handler hands off. Its `execute` method (`documents_retrieval/mod.rs:42`) checks that the API key authorizes each index, applies any tenant-token search rules to the filter, preprocesses the filters, and then runs each query through the federated search path. A single-index search runs as a one-query federation, so there is one code path for both. 3. [`perform_federated_search`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/meilisearch/src/search/federated/perform.rs#L57) partitions the queries into local indexes and remote network shards, runs the local ones through `SearchByIndex::execute` (`federated/perform.rs:1335`), and merges the results by weighted score. `SearchByIndex::execute` opens the index read transaction and resolves the `SearchKind` for the query: keyword only, semantic only, or hybrid. 4. [`prepare_search`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/meilisearch/src/search/mod.rs#L1639) builds the `milli::Search` from that query. It applies the deadline, the ranking-score threshold, the filter, the sort criteria, the distinct field, and any dynamic search rules configured on the index. 5. [`search_from_kind`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/meilisearch/src/search/mod.rs#L2142) runs it. Keyword and semantic searches both go through `Search::execute`; a hybrid search calls `execute_hybrid` with its semantic ratio, which runs both orderings and blends them. 6. [`execute_search`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/mod.rs#L812) is what `Search::execute` calls. It resolves the ranking-rule chain for the query, via `get_ranking_rules_for_query_graph_search` for a text query or `get_ranking_rules_for_placeholder_search` for a filter-only browse, checks sort criteria and geo parameters, then calls `bucket_sort`. 7. [`bucket_sort`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/bucket_sort.rs#L23) walks the ordered chain of `BoxRankingRule` implementations (words, typo tolerance, proximity, attribute rank, sort, word position, and exactness in the default chain) over the candidate document set, applies `apply_distinct_rule` to drop duplicates by the configured distinct field, and returns a `BucketSortOutput` carrying `docids`, `scores`, `all_candidates`, and a `degraded` flag for searches that hit the time budget. `execute_search` copies those into the `documents_ids` and `document_scores` of the `PartialSearchResult` it returns. 8. [`compute_facet_distribution_stats`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/meilisearch/src/search/mod.rs#L2057), back in the federated path, builds `ComputedFacets`, and the ranked documents are formatted into `SearchHit`s for the JSON response. The [ranking rules trace](/architecture/meilisearch/ranking-rules) walks steps 6 and 7 in detail. ## Health signals Symvanta detected 0 dependency cycles across 61 modules (modularity Q=0.78). 7 sets of mutually recursive symbols were also detected, the largest being `filter-parser` (7 symbols). All seven recursive groups sit exactly where a function walks a tree or an expression grammar and calls itself on the pieces it contains. The biggest is the `filter-parser` crate's recursive-descent parser (`parse_expression`, `parse_and`, `parse_or`, `parse_not`, `parse_primary`, `parse_foreign`, `parse_foreign_operator`), which is the shape a parser for a boolean filter grammar with nested `AND`/`OR`/`NOT` takes. The rest are JSON and field-map walkers: `flatten-serde-json` and `permissive-json-pointer` (two groups) recurse through nested documents, one flattening them for indexing and the other mapping leaf values back out, `json_template` parses nested template values, `fields_ids_map` inserts and looks up field ids by name, and milli's `extract` seeks leaf values through arrays and objects. Zero cycles at a modularity Q of 0.78 means the module boundaries hold: call traffic mostly stays inside a module, and no cluster depends on another in a loop. For teams weighing a dedicated search engine against embedding search results straight into an existing store, [code embeddings vs. code graph](/blog/code-embeddings-vs-code-graph) covers the same call-graph-vs-vector-similarity tradeoff from the code-search side. --- ### React Architecture: How It Actually Works https://symvanta.com/architecture/react React is Meta's UI library and the renderer, compiler, and devtools ecosystem that ships alongside it in one monorepo: the core `react` package that exposes hooks and `createElement`, the Fiber reconciler that schedules and commits updates, per-target renderers (`react-dom`, `react-native-renderer`, `react-test-renderer`, `react-art`), the Flight protocol for React Server Components (`react-server`, `react-client`), the React Compiler that auto-memoizes components at build time, and React DevTools for inspecting all of it at runtime. [Symvanta](https://symvanta.com)'s Louvain community detection organized the codebase's indexed symbols into 381 functional clusters (modularity Q=0.85, a clean separation of concerns for a codebase that ships five different products out of one source tree). The single largest cluster, at 3,086 symbols, is the shared Jest surface (hub `Component`): the `expect`, `describe`, `it`, and fixture components every package's suite reaches for, excluded from the diagram below the same way etcd's end-to-end harness was excluded from its own map. Underneath that sits the real product shape, and two clusters tie for the top of it at 1,980 symbols each. One is the reconciler plus the shared React types (hub `Fiber`), holding `Fiber`, `FiberRoot`, `Lanes`, `ReactContext`, and `ReactSharedInternals` together. The other is the React Compiler's HIR machinery (hub `GeneratedSource`: source-location and identifier tracking for lowered code). React DevTools contributes two of the ten largest, the frontend store and inspection types (1,167 symbols, hub `ReactCallSite`, which also absorbs the profiler timeline's `Rect` geometry) and the backend bridge's element-kind types (872 symbols, hub `ElementType`), and the Flight protocol contributes a third (1,113 symbols, hub `ReactComponentInfo`). This page is about the library's own internals. If you came here for [how to structure a React project](/blog/react-app-architecture) of your own (feature folders, module boundaries, dependency direction), that is a different question, and the companion post answers it. ## Module map The diagram below shows the 10 largest of 381 detected modules, sized by symbol count, with arrows weighted by how many calls cross between them. Every module is labelled after the package its hub symbol actually lives in, checked one hub at a time against the graph, because the names Symvanta's summarizer assigned this repo describe the shape of the code without placing it: the reconciler arrived as `React Fiber Implementation`, the compiler's HIR as `Code Location Tracking`, the DevTools frontend as `React State Management`, the DevTools backend as `Element Interaction Tools`, the Flight protocol as `React Debugging Utilities`, the react-dom host config as `DOM Element Management`, the server bindings as `Rendering State Management`, the public API and Fizz core as `Request Resolution Logic`, the compiler's fixture corpus as `Object Manipulation Utilities`, and the monorepo's own build scripts as `Execution Engine`. The Jest cluster (3,086 symbols, hub `Component`) is excluded, the same way etcd's end-to-end test harness was excluded from its map: it exercises every package's suite rather than forming part of any one of them.
facebook/react module map: the 10 largest of 381 detected modules with call-weighted edges, generated by Symvanta
Module map of facebook/react, generated by Symvanta. Link to this diagram Open full size
## Where to start reading These 16 symbols blend the hub of each product-shaped cluster above with the load-bearing entries from Symvanta's PageRank ranking, filtered to drop the fixture and tooling noise the same PageRank pass surfaced: `identity` (a pass-through helper in the compiler's `snap` fixture runtime), `expect` (Jest's assertion function), `cloneAst` (a Babel AST-cloning helper in the compiler's `snap` minimizer), `Button` (a component in the Server Components demo app under `fixtures/flight`), `ForkContext.head` (an internal accessor from `eslint-plugin-react-hooks`, the lint package React ships beside the library), `createError` (a helper inside a DevTools end-to-end script), `createMouseEvent` (an event factory from `dom-event-testing-library`), and `abort` (a bare name that resolves to the DOM `AbortController` the Fizz, Flight, and DOM abort paths all reach for). Start here to understand how React's pieces connect: - [`Fiber`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactInternalTypes.js#L89) - [`ReactContext`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/shared/ReactTypes.js#L58) - [`GeneratedSource`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts#L40) - [`ReactCallSite`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/shared/ReactTypes.js#L208) - [`ReactComponentInfo`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/shared/ReactTypes.js#L227) - [`ElementType`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-devtools-shared/src/frontend/types.js#L69) - [`DOMEventName`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-dom-bindings/src/events/DOMEventNames.js#L12) - [`Instance`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L224) - [`ReactNodeList`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/shared/ReactTypes.js#L28) - [`createElement`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react/src/jsx/ReactJSXElement.js#L610) - [`useState`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react/src/ReactHooks.js#L66) - [`useEffect`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react/src/ReactHooks.js#L87) - [`resolveDispatcher`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react/src/ReactHooks.js#L24) - [`runWithFiberInDEV`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactCurrentFiber.js#L46) - [`CompilerError.invariant`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts#L307) - [`reportGlobalError`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-client/src/ReactFlightClient.js#L1209) A few of these are worth calling out individually. `Fiber` and `FiberRoot` (same file) are the reconciler's core data structures: one unit of work and the root that owns a tree of them. At this commit the reconciler cluster also absorbs the shared React types, so `ReactContext`, `ReactSharedInternals`, and `Lanes` all live inside `react-reconciler` alongside `Fiber`, which is the cluster's hub. `useState` and `useEffect` are the public hooks; both call `resolveDispatcher` first to find the dispatcher the reconciler installed, covered in depth in the [hooks dispatcher spoke](/architecture/react/hooks-dispatcher). `react-dom-bindings` defines a second, unrelated `resolveDispatcher` for form actions; the one linked above is the hooks entry point in `packages/react`. `runWithFiberInDEV` is how the reconciler attaches the current fiber to a callback so a development warning can name the component it came from. `GeneratedSource` is the React Compiler's HIR source-tracking type, threaded through every instruction the compiler lowers so it can point errors back at the original source. `ReactCallSite`, `ReactComponentInfo`, `ElementType`, `DOMEventName`, `Instance`, and `ReactNodeList` are Flow interfaces: the shared vocabulary the reconciler, DevTools, Flight, the DOM host config, and the server renderer pass values through. `CompilerError.invariant` is the assertion helper the React Compiler calls when it hits a state its own passes should never produce. `reportGlobalError` is where a Server Components client gives up on a Flight stream and rejects every chunk still pending. ## Key subsystems ### The Fiber reconciler and the shared types `react-reconciler`, 1,980 symbols, hub `Fiber`. This is the scheduler and commit engine every renderer shares, clustered together with the shared React type vocabulary: it walks a tree of `Fiber` nodes rooted at a `FiberRoot`, assigns each unit of work a priority `Lane`, and holds `ReactContext`, `ReactSharedInternals`, and `Wakeable` in the same community. It sits at both ends of the busiest traffic on the map. Outbound it runs into `react-api-and-fizz` 430 times, the single heaviest edge in the codebase, and into the react-dom host layer 217 times. Inbound, the host layer calls it 229 times, the React API and Fizz cluster 133, DevTools' bridge 79, and Flight 55. The [Fiber reconciler spoke](/architecture/react/fiber-reconciler) traces a `setState` call from scheduling through commit. ### The React API and the Fizz render core `react-api-and-fizz`, 717 symbols, hub `__DEV__`. Two things share this community because both reference the development-mode flag from nearly every file, and the flag itself is the hub: its graph node is the ambient declaration in `scripts/jest/typescript/jest.d.ts`. The first is the public `react` package: `useState`, `useEffect`, `createElement`, `createContext`, `lazy`, `startTransition`, `memo`, `forwardRef`, and `resolveDispatcher`. The second is the Fizz server's render core in `packages/react-server/src/ReactFizzServer.js`: `renderNode`, `renderElement`, `performWork`, `flushCompletedQueues`, and the `SuspenseBoundary` and `ResumeSlots` types they operate on. It takes 430 calls from the reconciler, 169 from Flight, 111 from the server bindings, and 106 from the host layer, and sends 133 back into the reconciler. The dispatcher-resolution logic every hook goes through is dissected in the [hooks dispatcher spoke](/architecture/react/hooks-dispatcher). ### React Compiler: HIR and source tracking `compiler-hir`, 1,980 symbols, hub `GeneratedSource`, tied with the reconciler for the largest cluster on the map and the largest single-package one. The compiler's high-level intermediate representation: `SourceLocation`, `Place`, `Identifier`, `BlockId`, and `InstructionId` are the pieces every lowering pass threads through. Its heaviest edge runs into the `Flood` type model (22) in `compiler/packages/babel-plugin-react-compiler/src/Flood`, as inference resolves the types an instruction operates on. Nothing in the runtime half of the monorepo calls it: its inbound traffic comes from the compiler's HIR debug printer (48), the `Flood` type model again (21), and the Rust rewrite's type configuration under `compiler/crates` (12). ### React DevTools Two clusters belong squarely to DevTools. `devtools-frontend` (1,167 symbols, hub `ReactCallSite`) is the frontend's inspection surface, holding `SerializedElement`, `Store`, `StoreContext`, `SuspenseNode`, and the profiler timeline's `Rect` geometry that used to cluster on its own. `devtools-backend` (872 symbols, hub `ElementType`) is the backend and bridge: element kinds, `Agent`, `HostInstance`, `DevToolsInstance`, and the component filters. The two call into each other more than into anything else (`devtools-frontend` into `devtools-backend` 125 times, `devtools-backend` back 88), with Flight next (100 and 71) and the reconciler after that (12 and 79), the inspector and the timeline reading live fiber state. ### Flight and Server Components `flight-protocol`, 1,113 symbols, hub `ReactComponentInfo`, carries the Flight protocol's component and request metadata: `ReactDebugInfo`, `Thenable`, `ReactClientValue`, `ReactStackTrace`, `Request`. It calls into `react-api-and-fizz` 169 times, the reconciler 55, and the DevTools frontend 51, the point where a resolved server payload becomes fiber updates on the client and something DevTools can display. The client half sits in its own smaller cluster of 221 symbols hubbed on `Response`, where `reportGlobalError` rejects every chunk still pending when a stream breaks; it sends 77 calls back into this one and 75 into `react-api-and-fizz`. ### react-dom host layer `react-dom-bindings`, 897 symbols, hub `DOMEventName`. The react-dom host config: `DOMEventName`, `Instance`, `Container`, `TextInstance`, `SuspenseInstance`, and `ReactDOMSharedInternals`, the types and writes the reconciler drives to mutate the actual DOM. Its 229 calls into `react-reconciler` are the second-heaviest edge on the map, and the reconciler answers with 217. ### The server bindings `react-dom-server`, 607 symbols, hub `ReactNodeList`. React's server-rendering entry points and DOM configuration: `renderToPipeableStream` in `packages/react-dom/src/server`, plus `RenderState`, `StylesheetResource`, `ResumableState`, and the `createRequest` setup path. It calls into `react-api-and-fizz` 111 times, where the Fizz render loop it configures actually lives, then Flight 20 and the host layer 11. ### React Compiler fixture corpus `compiler-fixtures`, 733 symbols, hub `identity`, is the React Compiler's own fixture mass: small sample React components (`makeObject_Primitives`, `useIdentity`, `makeArray`, `useHook`) that the compiler's snapshot suite compiles and re-compiles to check the transform's output, plus the `snap` shared runtime in `compiler/packages/snap/src/sprout/shared-runtime.ts` they import. Its hub `identity` is a pass-through helper from that runtime, which is why the load-bearing list above trims it. Outbound traffic is almost nil (5 calls into the Jest surface, then 1 each into a hooks fixture cluster, DevTools, and the build tooling), exactly what a corpus of standalone fixtures should look like. ### Repository build tooling `repo-tooling`, 583 symbols, hub `cloneAst`, is the monorepo's own scripts and the Node surface they type against: `asyncCopyTo` in `scripts/bench/build.js`, `cloneAst` in the compiler's `snap` minimizer, `main` and `exec` entry points from the build scripts, and the `child_process` and `fs` declarations in `flow-typed/environments/node.js`. It ships in no React package. It reaches the top ten on size alone, which is what a monorepo that carries its own benchmark harness, release scripts, and Flow libdefs looks like from the graph's side. ## Canonical request flow React has no HTTP surface of its own, so the flow traced here is a load-bearing library call instead: what happens when a function component calls `useEffect`. 1. [`useEffect`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react/src/ReactHooks.js#L87) is the public hook. In development it warns if the effect callback is missing, then calls `resolveDispatcher()` and delegates to `dispatcher.useEffect(create, deps)`. 2. [`resolveDispatcher`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react/src/ReactHooks.js#L24) reads `ReactSharedInternals.H`. Every other hook in the file (`useState`, `useContext`, `useRef`, and the rest) calls the same function first. If `H` is `null`, development mode logs the "Invalid hook call" warning readers of the React docs will recognize, then returns the dispatcher anyway (the source comment explains that it avoids throwing its own error to keep this hot path inlinable). 3. [`ReactSharedInternals`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/shared/ReactSharedInternals.js#L12) is the module-level singleton whose `H` field points at whichever hooks dispatcher is currently active. 4. That field is set by [`renderWithHooks`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L505) in the Fiber reconciler, immediately before it calls the function component being rendered. It installs one of two concrete implementations of the [`Dispatcher`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactInternalTypes.js#L397) interface, a mount dispatcher on a component's first render and an update dispatcher on every render after, which is why the same `useEffect` call allocates a new hook on mount but reuses its slot on update. When the render finishes, [`finishRenderingHooks`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L636) swaps in `ContextOnlyDispatcher` instead of clearing the slot. Outside render `H` therefore holds `ContextOnlyDispatcher`, whose hook members all point at [`throwInvalidHookError`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L445), so a hook called from a click handler or a module body throws the "Invalid hook call" error from there. The `null` check in `resolveDispatcher` fires only before anything has rendered at all, while `H` still holds its initial `null`. The [hooks dispatcher spoke](/architecture/react/hooks-dispatcher) walks the full mount-vs-update path. ## Health signals Symvanta detected 15 dependency cycles across 381 modules (modularity Q=0.85). The largest spans 116 files in the `ReactiveScopes` area, entirely inside the React Compiler's `babel-plugin-react-compiler` package: the HIR, SSA, and reactive-scope lowering passes reference each other's types and visitor interfaces while walking a component down to its memoized form. The second, `Components` (115 files), is React DevTools' view layer (`DevTools.js`, `InspectedElementStateTree.js`, `WhatChanged.js`, `ErrorBoundary/TimeoutView.js`, and roughly 110 more), where panel components reference the views they render and the views reference the panels that host them. The third, `react-reconciler` (91 files), is the fiber walkers and commit-phase functions calling back and forth across the render and commit modules. The remaining twelve are small: the react-dom event plugins (21 files) and the DevTools element overlay (14 files) are the next two down, and ten of the fifteen span fewer than ten files each. None of the three large ones is a defect so much as the expected shape of a multi-pass compiler, a UI layer, and a tree reconciler. Symvanta also detected 95 sets of mutually recursive symbols, the largest being `react-reconciler` (142 symbols: `completeRoot`, `commitPassiveUnmountOnFiber`, `commitShowHideSuspenseBoundary`, `flushSyncWorkOnAllRoots`, and the rest of the walkers that recurse into a fiber's children and back up). `Flood` (68 symbols) and `HIR` (51 symbols) follow, the React Compiler's structurally recursive Flow type model and its instruction-lowering functions, then `react-server` (40 symbols), the Flight and Fizz task-rendering functions. All four recurse for the same reason: they walk a tree whose nodes contain more of the same kind of node. A modularity Q of 0.85 across 381 modules indicates most of React's call traffic stays inside its own cluster despite the monorepo housing five separate products, and the cycles are concentrated in the three places a compiler, a UI layer, and a reconciler are expected to have them. --- ### etcd Raft Consensus: Put to Committed Write https://symvanta.com/architecture/etcd/raft-consensus etcd's promise is that a successful write is durable and linearizable: once a client's `Put` returns, every replica in the cluster agrees the key changed, and no later read is allowed to miss it. That guarantee comes from the path a write takes through Raft before it is ever permitted to touch storage. [`EtcdServer.Put`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/v3_server.go#L295) writes nothing. It wraps the client's `PutRequest` in an `InternalRaftRequest`, proposes it to the local Raft node, and blocks until the cluster has committed that entry and the apply loop has executed it. The key only changes inside [`applierV3backend.Put`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/apply/backend.go#L50), which runs later, on a different goroutine, after consensus is already reached. This spoke traces that whole path, from the gRPC handler to the applied mutation, as far as the static call graph follows it. The analysis is generated from [Symvanta](https://symvanta.com)'s code graph of etcd pinned at commit c34dc7e, and one artifact of the graph is worth stating up front: the two halves of the write path show up as two disconnected runs. The propose side reaches from the gRPC handler down to the Raft proposal, the apply side reaches from the server's run loop down to the applier, and the hop between them is a Go channel handoff, so no call edge joins them. The consensus algorithm itself lives in an external module, `go.etcd.io/raft/v3` (pinned at v3.7.0), which etcd's server drives through a ready loop. ## The moving parts - [`EtcdServer.Put`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/v3_server.go#L295) - [`EtcdServer.processInternalRaftRequestOnce`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/v3_server.go#L1058) - [`exceedsRequestLimit`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/util.go#L60) - [`raftNode.start`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/raft.go#L174) - [`EtcdServer.apply`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/server.go#L1877) - [`EtcdServer.applyEntryNormal`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/server.go#L1930) - [`applierV3backend.Put`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/apply/backend.go#L50) - [`storeTxnWrite.Put`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/mvcc/kvstore_txn.go#L204) - [`Read.LinearizableReadLoop`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/read/read.go#L96) The two ends of the write path are the pair to hold in your head. [`EtcdServer.Put`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/v3_server.go#L295) is a thin 14-line handler: build the internal request, propose, wait, return. [`applierV3backend.Put`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/apply/backend.go#L50) is the function that finally writes the key, and it is three lines long: one call straight into the mvcc transaction package. The graph records no inbound call edge into it from the apply loop, which is what a handler dispatched through an interface off a committed log entry looks like. Between them, [`processInternalRaftRequestOnce`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/v3_server.go#L1058) is the gatekeeper that admits or rejects each proposal, assigns it an ID, and parks the calling goroutine on a wait channel, while [`raftNode.start`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/raft.go#L174) is the long-running loop that talks to the Raft library and turns its output into work for the rest of the server. [`Read.LinearizableReadLoop`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/read/read.go#L96) is the read-side mirror of all this: it exists so that reads can be linearizable without going through the log themselves, and the graph records exactly one caller for it, `EtcdServer.Start`, which launches it as a goroutine once at boot. ## How it works Here is the canonical path of a client `Put`, from the gRPC handler to the applied mutation and back.
Flowchart of an etcd Put moving from EtcdServer.Put through processInternalRaftRequestOnce, a raftNode.start quorum commit across a Raft channel boundary, EtcdServer.apply and applyEntryNormal, into the highlighted applierV3backend.Put and the mvcc storeTxnWrite.Put, with Read.LinearizableReadLoop mirroring it on the read side
The write path of a client Put: a thin propose front end crosses the Raft channel boundary at quorum commit, then the apply loop dispatches to applierV3backend.Put, the first point the request becomes an actual write against the mvcc store. Link to this diagram Open full size
1. [`EtcdServer.Put`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/v3_server.go#L295) receives the decoded `PutRequest` from the gRPC layer (`v3rpc.kvServer.Put` validates it first), wraps it in an `InternalRaftRequest` (the union type every mutating operation shares), and calls `raftRequest`, which delegates to `processInternalRaftRequestOnce` on its first line and afterwards reworks the `traceutil.Trace` carried back on the result so a slow write gets logged. 2. [`EtcdServer.processInternalRaftRequestOnce`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/v3_server.go#L1058) runs the admission check [`exceedsRequestLimit`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/util.go#L60) first, which compares the server's applied and committed Raft indexes so a client cannot propose faster than the node can apply, and only then stamps the request with a unique ID from `idutil.Generator.Next`, resolves the caller through `AuthInfoFromCtx`, and classifies it with `getRequestType`. It marshals the request, registers a wait keyed on the request ID, and proposes the bytes to the Raft node. 3. [`raftNode.start`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/raft.go#L174) is where the proposal leaves etcd's own code. The Raft library replicates the entry to peers over the transport, and once a quorum has acknowledged it, the library returns it as a committed entry on its `Ready` channel. This loop reads each `Ready`, packages its committed entries as a `toApply` batch and sends it down a channel to the server, sends outbound messages to peers, and persists the unstable entries and hard state to the write-ahead log through the server's `Storage` wrapper; on the leader path the `toApply` handoff and the peer send both go out before the disk write, so persisting overlaps with replication to the followers. Because this hop is a channel handoff rather than a call, the static graph shows no edge from the propose side to the apply side. 4. [`EtcdServer.apply`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/server.go#L1877) drains that batch inside the server's own run loop (the graph reaches it through `EtcdServer.run` and `applyEntries`). It walks the committed entries in index order, which is the point where Raft's total ordering becomes etcd's execution order. 5. [`EtcdServer.applyEntryNormal`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/server.go#L1930) handles each ordinary entry: it unmarshals the bytes back into an `InternalRaftRequest`, checks whether the entry has already been applied (so replay after a crash is idempotent), and dispatches it through the `applierV3` interface rather than a direct call. 6. [`applierV3backend.Put`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/apply/backend.go#L50) is the applier method the dispatch lands on for a `Put`. This is the first moment in the entire path that the request is actually a write against storage rather than a proposal about a write. 7. [`storeTxnWrite.Put`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/mvcc/kvstore_txn.go#L204) does the mutation. The applier delegates to [`txn.Put`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/txn/put.go#L30), which checks the lease and opens a write transaction with [`store.Write`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/mvcc/kvstore_txn.go#L174), and `storeTxnWrite.Put` assigns the key its new main revision, writes the versioned value into the bolt-backed store, and updates the in-memory `treeIndex`. When the transaction ends and the apply index advances, the wait registered back in step 2 fires and the original `Put` call unblocks with its response. 8. [`Read.LinearizableReadLoop`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/read/read.go#L96) is how reads stay consistent with all of the above without paying for a log entry. A linearizable read requests a confirmed read index from the leader, records the current `AppliedIndex`, and if the applied index is behind, waits on `ApplyWait` until the store has caught up to that index before serving from mvcc. It batches concurrent reads so one round trip to the leader can release many waiters at once. ## Where it connects This path is the spine the hub page's modules hang off. Steps 1 through 5 all live inside `etcdserver`, the largest module on the [etcd module map](/architecture/etcd) at 962 symbols, and the hub of that module is `raftRequest`, the call step 1 makes. The request and option vocabulary a client uses to build the `Put` in the first place lives in `client/v3`, the largest module outside the server; the durability step in the middle belongs to `wal`; and the applied mutation in step 7 lands in `mvcc`. The apply loop is the only writer into that storage, which is why neither `applierV3backend.Put` nor `storeTxnWrite.Put` carries a call edge back to the code that drives it: each is reached through an interface (`applierV3` for the applier, `mvcc.TxnWrite` for the store transaction), and an interface dispatch is a runtime decision the static graph cannot resolve to a single target. The half of the story this spoke skips over is step 3's phrase "persists the entries to the write-ahead log." Before Raft is allowed to call an entry committed and before the apply loop is allowed to execute it, that entry has to be durable on disk, so a power loss cannot lose an acknowledged write. That durability contract, the encoding, the fsync discipline, the segment rotation, and the replay that rebuilds this exact in-memory state after a restart, is the subject of the sibling spoke on the [write-ahead log](/architecture/etcd/write-ahead-log). ## By the numbers The propose side of the flow is compact: `processInternalRaftRequestOnce` spans 76 lines and, per the graph, pulls in 54 symbols, among them `exceedsRequestLimit`, `getAppliedIndex`, `getCommittedIndex`, `AuthInfoFromCtx`, `getRequestType`, `parseProposeCtxErr`, and the three sentinel errors it can return without ever reaching Raft (`ErrTooManyRequests`, `ErrRequestTooLarge`, `ErrStopped`). The apply side is where the length lives: `applyEntryNormal` runs 59 lines and `raftNode.start`, the ready loop, is a single 169-line function (raft.go lines 174 to 342) that pulls in 59 symbols of its own: the external `go.etcd.io/raft/v3` v3.7.0 API (`IsEmptySnap`, `CommittedEntries`, `SoftState`, `StateLeader`), etcd's own storage wrapper (`Save`, `SaveSnap`, `Release`, `Sync`), and the peer transport's `Send`. The clean split between a thin proposal front end and a fat apply back end is the structural signature of a consensus system: almost nothing happens where the request arrives, and everything happens where the log is replayed. --- ### etcd Write-Ahead Log: Entry to Disk and Replay https://symvanta.com/architecture/etcd/write-ahead-log Raft can only call an entry committed once it is durable on the disks of a quorum, and etcd's durability primitive is the write-ahead log. The WAL is an append-only sequence of length-prefixed, CRC-protected records on disk: before any raft entry is treated as committed and before the apply loop is allowed to execute it, that entry is encoded into a record, appended, and (when the entry demands it) flushed to stable storage with an fsync. The same file format is what lets a node that lost power come back up, replay its log, and reconstruct the exact raft state it had before the crash. [`WAL`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L73) is the struct that owns this: an open file directory, an [`encoder`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/encoder.go#L38) for the tail segment, a running CRC, and the index of the last entry it has seen. This spoke follows a single raft entry from the moment the ready loop hands it over to the moment it is safely fsynced, then follows the reverse path a restarting server takes to replay it. The analysis is generated from [Symvanta](https://symvanta.com)'s code graph of etcd pinned at commit c34dc7e. One thing to keep in mind while reading the call counts: the server never calls this package directly. The ready loop calls a small `Storage` wrapper in `server/storage/storage.go`, and that wrapper is what calls `WAL.Save`, `WAL.SaveSnapshot`, and `WAL.ReleaseLockTo`, so the WAL's own suite accounts for most of the recorded callers of every method below. ## The moving parts - [`WAL`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L73) - [`WAL.Save`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L995) - [`encoder.encode`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/encoder.go#L66) - [`WAL.sync`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L869) - [`WAL.cut`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L785) - [`NewDecoderAdvanced`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/decoder.go#L60) - [`decoder.decodeRecord`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/decoder.go#L88) - [`Record.Validate`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/walpb/record.go#L26) - [`WAL.ReadAll`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L472) The write side and the read side are near mirror images. On the way to disk, [`WAL.Save`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L995) is the entry point, [`encoder.encode`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/encoder.go#L66) turns a `walpb.Record` into framed, CRC-tagged bytes, and [`WAL.sync`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L869) is the fsync that makes those bytes survive a crash. On the way back, [`NewDecoderAdvanced`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/decoder.go#L60) constructs the reader over the on-disk segments, [`decoder.decodeRecord`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/decoder.go#L88) pulls one record at a time, and [`Record.Validate`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/walpb/record.go#L26) checks each record's stored CRC against the running checksum so a torn or corrupted tail is caught rather than silently replayed. [`WAL.cut`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L785) is the housekeeping that keeps segments bounded, and [`NewDecoder`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/decoder.go#L72) is the strict wrapper over `NewDecoderAdvanced` that refuses to continue past a CRC mismatch, the entry point both a replaying server and the offline `etcdutl` inspection tools decode through. ## How it works Here is what happens to a raft entry between the ready loop calling `Save` and the same entry being replayed after a restart.
Flow diagram of an etcd WAL entry moving through WAL.Save, WAL.saveEntry, encoder.encode, and the highlighted WAL.sync fsync barrier down to on-disk WAL segments bounded by WAL.cut and WAL.SaveSnapshot, then replayed on restart back up through WAL.ReadAll, decoder.decodeRecord, and Record.Validate into the apply loop
One raft entry down the write path to the WAL.sync fsync barrier, then back up the replay path after a crash. Link to this diagram Open full size
1. [`WAL.Save`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L995) receives the raft `HardState` and the slice of entries the ready loop just pulled off the Raft library. It short-circuits when there is nothing to write, then computes `mustSync` from `raft.MustSync`, which decides whether this batch requires a durable flush or can ride along in the OS page cache. 2. [`WAL.saveEntry`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L974) runs once per entry. It marshals the raft entry into a `walpb.Record` tagged as an entry record and hands it to the encoder; `Save` then writes the hard state as a state record after the entries. 3. [`encoder.encode`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/encoder.go#L66) frames the record. It folds the record's payload into the running CRC (seeded when the encoder is built by [`newEncoder`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/encoder.go#L47)), stamps that checksum into the record, writes a length prefix, and pads each record to an 8-byte boundary so the decoder can detect a partial trailing write. The CRC chaining is what ties the integrity of each record to every record before it. 4. [`WAL.sync`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L869) is the durability barrier. When `mustSync` is set and the current segment still has room, `Save` calls `sync`, which flushes the encoder's page writer and then invokes [`fileutil.Fdatasync`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/client/pkg/fileutil/sync.go#L27) on the tail segment to force the appended bytes to stable storage, timing the flush and logging a warning when it runs long. Only after this call returns is the entry genuinely durable, which is the precondition Raft needs before it treats the entry as committed. 5. [`WAL.cut`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L785) rotates the log. When `Save` sees the tail segment has grown past `SegmentSizeBytes`, it closes the current file, opens a preallocated next segment, and starts it with a fresh CRC record via [`WAL.saveCrc`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L1063) so the chain continues cleanly into the new file. Bounded segments are what make compaction and lock release cheap later. 6. [`Snapshotter.SaveSnap`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/etcdserver/api/snap/snapshotter.go#L70) is the compaction half. Periodically the server writes a full snapshot of applied state through the snap package, and [`WAL.SaveSnapshot`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L1039) records a small snapshot marker in the log itself; [`WAL.ReleaseLockTo`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L904) then unlocks and lets old segments below that snapshot be reclaimed, so the log does not grow without bound. 7. [`WAL.ReadAll`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L472) is the restart path. A recovering server opens the WAL at its most recent snapshot with [`Open`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/wal.go#L346), and `ReadAll` walks the segments from that point forward, dispatching on each record's type to rebuild the metadata, the last `HardState`, and the slice of entries the node had persisted. 8. [`decoder.decodeRecord`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/decoder.go#L88) does the low-level reading under `ReadAll`. It reads a length, reads the framed record, and calls [`Record.Validate`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/server/storage/wal/walpb/record.go#L26) to verify the stored CRC against the running checksum built with [`crc.New`](https://github.com/etcd-io/etcd/blob/c34dc7ee0048fd2bcc44d50beff002e5e8069b69/pkg/crc/crc.go#L35). A mismatch at the very end of the log is the expected signature of a crash mid-append and is tolerated as a truncated tail; a mismatch anywhere else is real corruption and stops recovery. Once replay finishes, the entries feed straight back into the same apply loop that produced them, and the node is exactly where it left off. ## Where it connects The WAL sits directly under the ready loop that the sibling spoke on [Raft consensus](/architecture/etcd/raft-consensus) describes. `raftNode.start` calls `Save` on every `Ready` batch of entries before those entries are treated as committed, and it does so through the `Storage` wrapper in `server/storage/storage.go`. That wrapper is one of only two production callers among the 37 the graph records for `WAL.Save`: the other is `AppendAndCommitEntries` in the server's bootstrap path, and the remaining 35 belong to the package's own suite and its fixtures. The durability that `Save` provides is the precondition that makes the whole "committed means safe" contract in that spoke true. In the other direction, the server consumes the WAL once per process lifetime, at startup, through `openWALFromSnapshot`, which calls `ReadAll` before the server begins serving clients; the offline `etcd-dump-logs` tool reads the log through the same method. On the [etcd module map](/architecture/etcd), this package is its own module: `wal`, 311 symbols, with `WAL.tail` (the accessor for the segment currently being appended to) as its highest-PageRank member, which is what you get when every append, fsync, and rotation in the package routes through one file handle. Its outbound traffic goes almost entirely to `schema` (13 calls) and `etcdserver` (8), the two places a log record's meaning is decided. The snapshot machinery that compaction depends on clusters separately as `snap`, and the offline `etcdutl` tools that decode a WAL without a running server form another module again, so the same decoder serves a recovering server and a command-line reader with no code in common between them. ## By the numbers The write path is dominated by one method: `WAL.Save` is 43 lines (wal.go 995 to 1037) and everything else on the hot path (`saveEntry`, `encode`, `sync`) is small and called from it. The read path is lopsided the other way: `ReadAll` is a 124-line function (wal.go 472 to 595) because it has to handle every record type, snapshot matching, and the torn-tail case, while `decodeRecord` beneath it is 68 lines. The two decoder constructors make the integrity policy explicit in the type system: `NewDecoderAdvanced` takes a `continueOnCrcError` flag, and `NewDecoder` is the one-line wrapper that hard-codes it to false, so the default everywhere in the running server is to stop on corruption rather than guess. --- ### Meilisearch Indexing Pipeline, Write to Index https://symvanta.com/architecture/meilisearch/indexing-pipeline Every write in Meilisearch (adding documents, updating settings, swapping indexes) is asynchronous. A `POST /indexes/{index_uid}/documents` call does not touch the search index inline: it streams the request body to a file on disk, appends one durable task to a queue, and returns a task id. A background scheduler picks the task up later, groups it with compatible neighbors, and runs the `milli` update pipeline that actually rewrites the on-disk databases. That split is what lets Meilisearch absorb bursts of writes without blocking search, and `IndexScheduler` is the structure both halves meet at: the HTTP layer holds it as shared state, and the scheduler loop drives it. This page traces one document from the HTTP handler to a committed index, using the module graph generated by [Symvanta](https://symvanta.com). The pipeline has two halves that never share a thread. The front half is cheap and synchronous with the request: validate, persist the payload, enqueue. The back half runs inside the scheduler's own loop: it opens a write transaction against LMDB, extracts field and word data from the changed documents, and commits roaring-bitmap postings. Everything the search side later reads (the [ranking rules trace](/architecture/meilisearch/ranking-rules) covers that side) is written here. ## The moving parts - [`document_addition`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/meilisearch/src/routes/indexes/documents.rs#L1496) - [`register`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/index-scheduler/src/lib.rs#L795) - [`tick`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/index-scheduler/src/scheduler/mod.rs#L168) - [`create_next_batch`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/index-scheduler/src/scheduler/create_batch.rs#L524) - [`autobatch`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/index-scheduler/src/scheduler/autobatcher.rs#L570) - [`apply_index_operation`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/index-scheduler/src/scheduler/process_index_operation.rs#L45) - [`index`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/update/new/indexer/mod.rs#L68) - [`extract_all`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/update/new/indexer/extract.rs#L34) - [`write_to_db`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/update/new/indexer/write.rs#L22) - [`CboRoaringBitmapCodec`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/heed_codec/roaring_bitmap/cbo_roaring_bitmap_codec.rs#L19) `document_addition` is the actix handler behind both `POST` (replace) and `PUT` (update) on the documents route: it validates the content type, streams the body to an update file, and hands off. `register` is the queue's front door: it turns the request into a `Task` (a `KindWithContent` describing the operation) and persists it in `enqueued` state. `tick` is the scheduler loop, `create_next_batch` and `autobatch` decide how many enqueued tasks to fold into one transaction, and `apply_index_operation` is the bridge from a scheduler batch into `milli`. The `index` function is milli's update entry point; it fans out into `extract_all` (which turns documents into word and facet deltas) and `write_to_db` (which drains those deltas into LMDB). The postings themselves are encoded by `CboRoaringBitmapCodec`, the compressed-bitmap codec that stores which document ids match each term or facet value. `Index` (the LMDB environment) and `FieldsIdsMap` (the string-to-`u16` field map) are the two on-disk structures both halves of the pipeline share. ## How it works Here is the canonical path for a document write, from the HTTP handler through the enqueued task to a committed index.
A vertical flow diagram tracing a Meilisearch document write from the document_addition HTTP handler through register, tick, create_next_batch and the highlighted autobatch stage inside the IndexScheduler, into milli's index which fans out over a bbqueue channel to extract_all and write_to_db, committing roaring-bitmap postings via CboRoaringBitmapCodec to the LMDB index
The write path: an async HTTP handoff, autobatched into one scheduler transaction, then extracted and committed as roaring-bitmap postings in LMDB. Link to this diagram Open full size
1. [`document_addition`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/meilisearch/src/routes/indexes/documents.rs#L1496) is the shared handler for `POST /indexes/{index_uid}/documents` (replace) and its `PUT` sibling (update). It checks the content type, then streams the raw request body to an on-disk update file via `copy_body_to_file` and `create_update_file` rather than parsing every document inline, so the request thread does almost no work. 2. The [`register`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/index-scheduler/src/lib.rs#L795) family appends the write to the queue. The handler builds a `KindWithContent::DocumentAdditionOrUpdate` (index uid, primary key, method, and the update-file uuid) and passes it to `register_with_custom_metadata` (`documents.rs:1633`), the sibling that also carries the request's custom metadata; both it and `register` are thin wrappers over `register_with_custom_metadata_and_network`, which persists the `Task` in `enqueued` state and returns its id immediately. The HTTP response is sent here, long before any indexing happens. 3. [`tick`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/index-scheduler/src/scheduler/mod.rs#L168) is the scheduler loop, running on its own thread. On each turn it opens a read transaction, asks `create_next_batch` for the next unit of work, processes it, and writes the resulting task states back to the queue. 4. [`create_next_batch`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/index-scheduler/src/scheduler/create_batch.rs#L524) picks the next unit of work, and for ordinary document writes it delegates to `create_next_batch_unprioritized`, which is what runs `autobatch` over the enqueued tasks. Consecutive document operations on the same index coalesce into a single `IndexOperation`, so a burst of `POST` calls is applied in one write transaction instead of one per request. 5. [`apply_index_operation`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/index-scheduler/src/scheduler/process_index_operation.rs#L45) is reached from `process_batch` (call site line 183). For a document operation it opens the index write transaction, reads back the current `FieldsIdsMap`, builds a milli `IndexOperations` from the batched update files, and turns it into the `DocumentChanges` the indexer consumes. 6. [`index`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/update/new/indexer/mod.rs#L68) is milli's update entry point. It takes those `DocumentChanges`, spins up the extractor thread pool and a bounded bbqueue channel, and runs extraction and writing in parallel: producers extract, a consumer writes, so a large payload never has to fit in memory at once. 7. [`extract_all`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/update/new/indexer/extract.rs#L34) runs the extractors over the changed documents, producing the field distribution, a `WordDelta` (words added, modified, and deleted), and facet-field deltas, and streams the resulting key-value writes into the channel. 8. [`write_to_db`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/update/new/indexer/write.rs#L22) drains that channel on a dedicated writer thread and puts each buffer straight into its LMDB database. The `CboRoaringBitmapCodec` encoding already happened on the producer side, in the extractor caches and the merger that filled the channel. `post_process` then rebuilds the prefix and word-FST databases from the deltas, and the write transaction commits: the document is now searchable. ## Where it connects The scheduler and the query engine share one cluster on the [Meilisearch module map](/architecture/meilisearch): Scheduler Runtime and Query Execution (hub `Result`, 1413 symbols), which holds `tick`, `register`, `create_next_batch`, and `process_batch` next to milli's `execute_search` and `bucket_sort`. It calls into the Index Storage and Write Pipeline cluster 253 times and into On-disk Codecs and Field Ids 128 times, because every task it runs ends by reading and writing index state through field identifiers. The indexer half lives in Index Storage and Write Pipeline (hub `Index`, 1369 symbols), where milli's `index`, `extract_all`, and `write_to_db` sit beside the LMDB `Index` they commit into. Its 447 calls back into the scheduler cluster are the heaviest edge on the whole map, and its 301 calls into On-disk Codecs and Field Ids are the second heaviest: extraction is a loop that resolves document fields to field ids and writes their postings. `CboRoaringBitmapCodec` itself sits one cluster over, in Query Term Interning and Bitmaps (hub `Interned`, 591 symbols), alongside the interner and bitmap types the query path reads. The output of this pipeline is exactly the input to the query side. `extract_all` writes the word and facet postings; the [ranking rules trace](/architecture/meilisearch/ranking-rules) shows `bucket_sort` walking those same postings to order results. The one structure both halves share is `Index`: the write path commits into it under a write transaction, and every search opens a read transaction against it, which is why the scheduler exposes its own `read_txn` helper next to the index-level one. ## By the numbers The Scheduler Runtime and Query Execution cluster is 1413 symbols, the second largest of Meilisearch's 61 detected modules, behind the Meilisearch HTTP API cluster at 1728. The milli indexer subtree (`crates/milli/src/update/new/indexer`) is 15 files and 327 symbols. The two functions where a batched write becomes committed on-disk state are large: `apply_index_operation` spans lines 45 through 581, and milli's `index` spans lines 68 through 246. The whole pipeline sits in a graph with zero dependency cycles (modularity Q=0.78), so the write path and the read path stay cleanly separated even though they meet at the same LMDB `Index`. --- ### Meilisearch Ranking Rules, Query to Results https://symvanta.com/architecture/meilisearch/ranking-rules Once a Meilisearch query has narrowed the index to a set of candidate documents, something has to decide their order. That is the job of the ranking-rule chain: an ordered list of rules (words, typo, proximity, attribute rank, sort, word position, and exactness by default) applied one after another, each breaking the ties the previous rule left behind. The chain is not a hardcoded `switch`: it is built as a `Vec` of boxed trait objects and walked by a single function, `bucket_sort`, which is what makes the ordering configurable per index and per query. This page traces a query from `execute_search` through rule-chain construction into `bucket_sort` and out as ranked hits, using the graph generated by [Symvanta](https://symvanta.com). The rules operate over a query graph, not raw strings. Meilisearch interns query terms (`Interned`, `SearchContext`) into a `QueryGraph` of alternative interpretations (typo variants, word splits, synonyms), and most ranking rules are `GraphBasedRankingRule`s that differ only in which graph they walk. A hybrid search additionally blends this keyword ordering with vector similarity through a `SemanticRatio`. The postings these rules read are produced by the write side (the [indexing pipeline trace](/architecture/meilisearch/indexing-pipeline) covers that half). ## The moving parts - [`execute_search`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/mod.rs#L812) - [`SearchContext`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/mod.rs#L77) - [`Interned`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/interner.rs#L10) - [`QueryGraph`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/query_graph.rs#L84) - [`get_ranking_rules_for_query_graph_search`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/mod.rs#L510) - [`RankingRule`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/ranking_rules.rs#L26) - [`BoxRankingRule`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/ranking_rules.rs#L19) - [`GraphBasedRankingRule`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/graph_based_ranking_rule.rs#L97) - [`bucket_sort`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/bucket_sort.rs#L23) - [`SemanticRatio`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/meilisearch/src/search/mod.rs#L751) `execute_search` is the entry point for the whole ranking step, and `SearchContext` is the object it threads through everything: it holds the read transaction, the term `Interned` pool, and the `QueryGraph`. `get_ranking_rules_for_query_graph_search` is the builder that reads the index's configured criteria and produces the ordered rule chain. `RankingRule` is the trait every rule implements, and `BoxRankingRule` is the boxed-trait-object alias the chain is actually a `Vec` of. Most default rules are `GraphBasedRankingRule` instances parameterized by their graph (words, typo, proximity, field id, position, and exactness, which is a type alias for `GraphBasedRankingRule`); `ExactAttribute` and `Sort` are standalone types, and `VectorSort` and `GeoSort` cover semantic and geo ordering. `bucket_sort` is the walker that turns the chain into a ranked list, and `SemanticRatio` is the single knob that blends keyword and vector results in a hybrid search. ## How it works Here is the canonical path a text query takes through ranking, from `execute_search` into `bucket_sort` and back out.
A flow diagram tracing a Meilisearch text query from execute_search through SearchContext and its QueryGraph into the get_ranking_rules_for_query_graph_search builder, which produces the words, typo, proximity, attribute, sort, exactness rule chain that bucket_sort walks into a BucketSortOutput of ranked hits, with a hybrid path that blends by SemanticRatio
A text query becomes ordered results: execute_search builds the words, typo, proximity, attribute, sort, exactness rule chain, and bucket_sort walks it into ranked hits. Link to this diagram Open full size
1. [`execute_search`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/mod.rs#L812) is the ranking entry point. It receives a `SearchContext`, an optional `(QueryGraph, located terms)` pair, the sort criteria, and a `universe` bitmap of the candidate documents, and returns a `PartialSearchResult` of ordered document ids and scores. 2. [`SearchContext`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/mod.rs#L77) carries the in-flight query. Terms are interned via `Interned` so the ranking rules compare small integer ids instead of strings, and the query is expanded into a `QueryGraph` of alternative interpretations (typo variants, word splits, synonyms) that the graph-based rules walk. 3. [`get_ranking_rules_for_query_graph_search`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/mod.rs#L510) builds the chain by iterating the index's configured criteria (`ctx.index.criteria`) and pushing one or more rules per criterion. The default list (`default_criteria`) is `Words`, `Typo`, `Proximity`, `AttributeRank`, `Sort`, `WordPosition`, `Exactness`; attribute-rank pushes an `Fid` rule, word-position pushes a `Position` rule, and exactness pushes an `ExactAttribute` rule followed by an `Exactness` rule, so the short criteria list becomes a longer `Vec`. `get_ranking_rules_for_placeholder_search` builds the shorter chain for a filter-only browse with no query terms. 4. [`RankingRule`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/ranking_rules.rs#L26) is the trait every rule in that `Vec` implements. Its interface is bucket-at-a-time (`start_iteration`, `next_bucket`, `end_iteration`): a rule yields its next bucket of equally-ranked documents on demand rather than sorting the whole set up front. `Words`, `Typo`, `Proximity`, `Fid`, `Position`, and `Exactness` are all aliases for `GraphBasedRankingRule` that differ only in which ranking-rule graph they walk. 5. [`bucket_sort`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/bucket_sort.rs#L23) walks the chain. It asks the first rule for its next bucket of tied documents, and for each bucket recurses into the next rule to break the tie, down the chain, until it has collected enough documents to fill the requested window. `apply_distinct_rule` drops duplicates by the configured distinct field as buckets are emitted. 6. [`bucket_sort`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/bucket_sort.rs#L23) returns a `BucketSortOutput` of `docids`, `scores` (a `ScoreDetails` per rule, so a client can see why a document ranked where it did), `all_candidates`, and a `degraded` flag set when the search hit its time budget before the chain was exhausted. `execute_search` moves those into the `documents_ids` and `document_scores` fields of the `PartialSearchResult` its caller receives. 7. [`execute_hybrid`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/hybrid.rs#L274) is the alternate top of the flow when a query sets a semantic ratio. It runs the keyword search above and a vector search, then blends the two orderings by `SemanticRatio`, a float in the range 0 to 1 where 0 is pure keyword and 1 is pure semantic. ## Where it connects The ranking engine lives in the Scheduler Runtime and Query Execution cluster (hub `Result`, 1413 symbols), which holds `Search`, `SearchContext`, `QueryGraph`, `ScoreDetails`, and every type that implements `RankingRule`. On the [Meilisearch module map](/architecture/meilisearch) it is the connective layer between a parsed query and the index: it calls into Index Storage and Write Pipeline 253 times and into On-disk Codecs and Field Ids 128 times, because every rule ultimately reads postings keyed by field id. The interned terms those rules compare come from Query Term Interning and Bitmaps (hub `Interned`, 591 symbols), whose 162 calls back into the ranking cluster are its heaviest outbound edge. The candidate documents that ranking orders, and the word and facet postings each rule reads, are produced by the write side: the [indexing pipeline trace](/architecture/meilisearch/indexing-pipeline) shows how `extract_all` and `write_to_db` build exactly the `CboRoaringBitmapCodec` postings that `bucket_sort` walks here. Both halves meet at the same LMDB `Index`, which is why the design invariant is simple: indexing writes postings under a write transaction, and ranking reads them under a read transaction, with no cycle between the two (the graph reports zero dependency cycles at modularity Q=0.78). ## By the numbers The ranking chain is assembled from seven default criteria that expand into a longer list of concrete rules, and `bucket_sort` (lines 23 through 343 of `bucket_sort.rs`) is the one function that walks all of them. Five distinct types implement the `RankingRule` trait directly (`GraphBasedRankingRule`, `Sort`, `ExactAttribute`, `VectorSort`, `GeoSort`), with the word, typo, proximity, field-id, and position rules all sharing the single `GraphBasedRankingRule` implementation. Each `GraphBasedRankingRule` is parameterized by a [`RankingRuleGraphTrait`](https://github.com/meilisearch/meilisearch/blob/577f7af28942b71782eab1e59f44ad8296ce0a92/crates/milli/src/search/new/ranking_rule_graph/mod.rs#L96), one implementation per graph the chain walks: `WordsGraph`, `TypoGraph`, `ProximityGraph`, `FidGraph`, `PositionGraph`, and `ExactnessGraph`. Symvanta reports 7 sets of mutually recursive symbols across the codebase at this commit, and none of them sit in the ranking-rule graph module: the recursion that is left lives in the filter grammar, the JSON walkers, and the field-id map. The cluster this ranking chain belongs to is one of 61 Meilisearch modules with zero dependency cycles between them. --- ### React Fiber Reconciler: Schedule to Commit https://symvanta.com/architecture/react/fiber-reconciler The Fiber reconciler is the part of React that decides what changed and when to apply it. Symvanta's Louvain community detection groups it into a single cluster, `react-reconciler`, at 1,980 symbols the largest product cluster in the repository, tied exactly with the React Compiler's HIR. Its hub is `Fiber` itself. The cluster also absorbs React's shared type files at this commit, so `ReactContext`, `ReactSharedInternals`, `Lanes`, and `Wakeable` sit in the same community as the fiber walkers that read them. Every renderer React ships (`react-dom`, `react-native-renderer`, `react-test-renderer`, `react-art`) drives the same reconciler code; only the host config that turns fiber effects into real mutations differs. This is why a single package owns both the scheduler that prioritizes work and the commit engine that writes it out. The reconciler's job splits cleanly in two. The render phase builds a work-in-progress tree of `Fiber` nodes and can be paused, aborted, or restarted at a higher priority. The commit phase takes a completed tree and applies it to the host in one synchronous, uninterruptible pass. The data structures below are what make that interruptibility possible: React keeps two copies of every fiber and never mutates the visible tree until the whole render is finished. This analysis is generated from [Symvanta](https://symvanta.com)'s code graph over `facebook/react` at commit `eafeac0`. It describes React's internals; for [React app architecture](/blog/react-app-architecture) in the sense of laying out your own codebase, read the companion post instead. ## The moving parts - [`Fiber`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactInternalTypes.js#L89) - [`FiberRoot`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactInternalTypes.js#L386) - [`Lane`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberLane.js#L18) - [`scheduleUpdateOnFiber`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberWorkLoop.js#L987) - [`performWorkOnRoot`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberWorkLoop.js#L1137) - [`beginWork`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberBeginWork.js#L4226) - [`completeWork`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberCompleteWork.js#L1082) - [`commitRoot`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberWorkLoop.js#L3725) - [`renderWithHooks`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L505) A `Fiber` is one unit of work: a single object with roughly thirty fields spanning lines 89 to 210, carrying the component `type`, the `stateNode` (the host instance or class instance), the `return`, `child`, and `sibling` pointers that form the tree, a `memoizedState` field (the head of the hook linked list for function components), a `lanes` bitmask of pending work, `flags` and `subtreeFlags` effect bitfields, and an `alternate` pointer to its paired copy. That `alternate` is the whole trick: every fiber that updates gets a work-in-progress twin, so React can build the next tree without touching the one currently on screen. The `FiberRoot` sits above the tree; its `current` pointer names whichever of the two trees is live, and the swap between them is a single assignment at commit time. A `Lane` is a single bit in a 31-bit priority set; `Lanes` is a set of them, and the reconciler uses lane bitmasks everywhere to decide which pending updates a given render should include and which to defer. `renderWithHooks` is the seam every renderer shares: it installs the hooks dispatcher and calls the component function, the bridge between the reconciler and the [hooks dispatcher](/architecture/react/hooks-dispatcher). ## How it works Here is the canonical path a state update travels, from a component calling `setState` (a bound `dispatchSetState`) through the pixels changing on screen.
A vertical pipeline showing a setState traveling through dispatchSetState, scheduleUpdateOnFiber, ensureRootIsScheduled, performWorkOnRoot, the beginWork and completeWork render walk, then the highlighted commitRoot commit phase that writes DOM changes, and finally flushPassiveEffects after paint
The path a setState travels: schedule, the beginWork/completeWork render walk, then the uninterruptible commitRoot commit phase, with passive effects deferred after paint. Link to this diagram Open full size
1. [`dispatchSetState`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L3626) is the function `useState` returns, bound to the fiber and its update queue. It asks `requestUpdateLane(fiber)` for a priority lane, then hands off to `dispatchSetStateInternal`, which enqueues an update record. When the queue is empty it eagerly computes the next state first and bails out entirely if the value is unchanged, so an unchanged `setState` never schedules a render. 2. [`scheduleUpdateOnFiber`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberWorkLoop.js#L987) is where a real update enters the work loop. The `return`-pointer walk that propagates `childLanes` up the tree has already happened by then, in [`markUpdateLaneFromFiberToRoot`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberConcurrentUpdates.js#L189), which is also how the `FiberRoot` passed in here was found. `scheduleUpdateOnFiber` calls `markRootUpdated` to merge the new lane into the root's pending set, then `ensureRootIsScheduled`. 3. [`ensureRootIsScheduled`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberRootScheduler.js#L116) registers the root with the root scheduler so a task is queued (via the Scheduler package for concurrent work, or a microtask for sync work). Multiple updates in the same tick collapse into one scheduled render here. 4. [`performWorkOnRoot`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberWorkLoop.js#L1137) is the entry point the scheduled task calls. It chooses `renderRootSync` or `renderRootConcurrent` based on the lanes and whether the work can be time-sliced, then drives the render phase to completion before finishing with a commit. 5. [`beginWork`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberBeginWork.js#L4226) runs on the way down. The work loop (`workLoopSync` or `workLoopConcurrent`) calls it once per fiber, top-down; it reconciles children against the previous tree, and for a function component it calls `renderWithHooks` to run the component body and diff its output into child fibers. 6. [`completeWork`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberCompleteWork.js#L1082) runs on the way back up, once a fiber has no more children to descend into. It creates or updates the host instance (a DOM node, for react-dom), bubbles each fiber's `flags` into its parent's `subtreeFlags`, and returns to the sibling or parent, so the loop knows in one bitmask read whether a subtree has any commit work at all. 7. [`commitRoot`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberWorkLoop.js#L3725) takes over once the work-in-progress tree is complete. It runs the before-mutation snapshot, then `commitMutationEffects` (which inserts, updates, and deletes host nodes), then flips `root.current` to the finished tree, then `commitLayoutEffects` (which fires `useLayoutEffect` and attaches refs against the now-current tree). This whole pass is synchronous and cannot be interrupted. 8. [`flushPassiveEffects`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberWorkLoop.js#L4691) is the deferred tail. Passive effects (`useEffect` cleanups and callbacks) are scheduled during commit but run later, off the critical path, which is what makes `useEffect` fire after paint while `useLayoutEffect` fires before it. ## Where it connects The reconciler cluster is the hub the rest of React reaches through. On [the module map](/architecture/react), `react-reconciler` sits at both ends of the heaviest traffic in the codebase. Its own busiest edge runs into `react-api-and-fizz` 430 times, the single heaviest cross-module edge on the map: that cluster holds the public `react` package and the shared type vocabulary the reconciler reads on every path. Next comes the react-dom host layer (`react-dom-bindings`, hub `DOMEventName`), which the reconciler calls 217 times and which calls back 229, the seam where fiber effects become real DOM mutations. Inbound after that: the React API and Fizz cluster reaches in 133 times, React DevTools' element-inspection cluster (`ElementType`) 79, Flight (`ReactComponentInfo`) 55, the react-native renderer's host types and legacy event plumbing (`Component Structure`, hub `Instance`) 29, and the alternate renderers 15 from `react-test-renderer` and 12 from `react-noop-renderer`. The tightest coupling is with hooks. `renderWithHooks` lives inside this cluster and is the exact point where the reconciler installs a dispatcher and calls a component; the sibling [hooks dispatcher spoke](/architecture/react/hooks-dispatcher) picks the story up there, following a single `useState` call through mount and re-render. Every hook's state, the `memoizedState` linked list described above, hangs off the very `Fiber` objects the render phase walks. ## By the numbers Symvanta counts 1,980 symbols in the `react-reconciler` cluster. The `Fiber` type alone spans 122 lines (89 to 210) as one flat object, deliberately merged into a single allocation and never split across intersected types. `commitRoot` runs 198 lines (3,725 to 3,922) of synchronous commit orchestration. The reconciler also forms one of the repository's 15 dependency cycles, a 91-file cycle across the render and commit modules that call back and forth, the third-largest cycle in the codebase behind the React Compiler's reactive-scope passes (116 files) and DevTools' view layer (115). It carries the largest set of mutually recursive symbols too, 142 of them, the walkers that descend into a fiber's children and return through the same functions. --- ### React Hooks Dispatcher: How useState Resolves https://symvanta.com/architecture/react/hooks-dispatcher When you call `useState` in a component, the function in the `react` package does almost nothing: it reads a mutable slot called the dispatcher and forwards the call to whatever the reconciler installed there a moment ago. That indirection is the entire hooks mechanism. The `react` package ships a hooks API with no implementation, and the reconciler swaps in one of two concrete implementations right before it runs your component. Symvanta's graph shows the two halves living in different clusters: the public hook functions sit in the cluster the [module map](/architecture/react) calls `react-api-and-fizz` (717 symbols), while the mount and update implementations, plus the machinery that installs them, live in the `react-reconciler` cluster (1,980 symbols). The single field that connects them is `ReactSharedInternals.H`. This indirection is why the same `useState` line behaves differently on a component's first render than on every render after, why hook state can live on the fiber instead of in a closure, and why the "Invalid hook call" warning exists at all. This analysis is generated from [Symvanta](https://symvanta.com)'s code graph over `facebook/react` at commit `eafeac0`. It covers how React implements hooks; if you are looking for [frontend architecture for React](/blog/react-app-architecture) applications you build, the companion post covers that. ## The moving parts - [`ReactSharedInternals`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/shared/ReactSharedInternals.js#L12) - [`resolveDispatcher`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react/src/ReactHooks.js#L24) - [`Dispatcher`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactInternalTypes.js#L397) - [`renderWithHooks`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L505) - [`HooksDispatcherOnMount`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L3926) - [`HooksDispatcherOnUpdate`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L3954) - [`mountState`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L1948) - [`mountWorkInProgressHook`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L983) - [`updateWorkInProgressHook`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L1004) `ReactSharedInternals` is a module-level singleton, re-exported from React's internal client bundle, whose `H` field holds the currently active dispatcher (or `null`). `resolveDispatcher` is the function every public hook calls first; it returns `ReactSharedInternals.H`, and in production that is all two statements of it (the definition spans lines 24 to 42 because of the development-only null check). The `Dispatcher` interface is the contract both implementations satisfy: `useState`, `useReducer`, `useEffect`, `useContext`, `useRef`, and the rest, roughly two dozen slots. `HooksDispatcherOnMount` and `HooksDispatcherOnUpdate` are the two concrete objects: on mount, `useState` maps to `mountState`; on update, it maps to `updateState` (which delegates to `updateReducer`). `mountWorkInProgressHook` and `updateWorkInProgressHook` are the two functions that manage where hook state lives: a singly linked list of `Hook` objects hanging off the fiber's `memoizedState` field, one node per hook call, in call order. ## How it works Here is the canonical path a `useState` call travels, showing where the first render and every render after diverge.
Flowchart of a useState call resolving through resolveDispatcher and the ReactSharedInternals.H slot, which renderWithHooks sets to either HooksDispatcherOnMount or HooksDispatcherOnUpdate, each writing the fiber's Hook linked list
How a useState call resolves: resolveDispatcher reads ReactSharedInternals.H, the seam renderWithHooks fills with the mount or update dispatcher before every render. Link to this diagram Open full size
1. [`useState`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react/src/ReactHooks.js#L66) in the `react` package spans six lines, two of them the body: `const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState);`. It holds no state and knows nothing about fibers. Every other hook (`useEffect`, `useReducer`, `useContext`) has the same shape. 2. [`resolveDispatcher`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react/src/ReactHooks.js#L24) returns `ReactSharedInternals.H`. In development it first checks whether `H` is `null` and, if so, logs the "Invalid hook call. Hooks can only be called inside of the body of a function component" message. The comment in the source notes it deliberately does not throw its own error (a null access will throw naturally, and skipping the check keeps this hot path inlinable). 3. [`renderWithHooks`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L505) is the reconciler function that made `H` non-null in the first place. Before it calls your component, it resets the work-in-progress fiber's `memoizedState`, `updateQueue`, and `lanes`, then assigns `ReactSharedInternals.H` to either the mount or the update dispatcher. The choice is one condition: `current !== null && current.memoizedState !== null`, meaning the fiber already rendered once and used at least one stateful hook, selects the update dispatcher; otherwise the mount dispatcher. 4. [`HooksDispatcherOnMount`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L3926) is installed on first render. Its `useState` slot is `mountState`, which calls `mountWorkInProgressHook` to allocate a fresh `Hook`, stores the initial state, and binds `dispatchSetState` to the fiber and the hook's queue to produce the setter you get back. 5. [`mountWorkInProgressHook`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L983) is where hook state attaches to the fiber. The first hook call sets `currentlyRenderingFiber.memoizedState` to the new node; each later call appends to the previous node's `next`. The result is a linked list whose order is exactly the order your hooks were called, with no keys or names. 6. [`HooksDispatcherOnUpdate`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L3954) is installed on every render after the first. Its `useState` slot is `updateState`, which forwards to `updateReducer` with a basic state reducer, since a state update is just a reducer update in disguise. 7. [`updateWorkInProgressHook`](https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberHooks.js#L1004) walks that list by position on re-render. It reads the next node from the alternate fiber's `memoizedState` (the previous render's list) and clones it forward. If it runs off the end of the list, it throws "Rendered more hooks than during the previous render." That single positional walk is why the [Rules of Hooks](/architecture/react) forbid calling hooks conditionally: the list has no names, so position is the only identity a hook has. ## Where it connects The dispatcher is a deliberately thin seam between two clusters that [the module map](/architecture/react) keeps separate. The public hook functions and the element helpers around them (`memo`, `forwardRef`, `createElement`, `getComponentNameFromType`) cluster together in `react-api-and-fizz`, while the dispatchers, the `Hook` list machinery, and `renderWithHooks` live in `react-reconciler`. Neither imports the other's internals; they meet only through `ReactSharedInternals.H`, which is also how a `react-dom` build and a `react-native` build can install different renderers behind the identical `react` package the app imports. The graph puts 430 calls on the reconciler's edge into that cluster, the heaviest on the whole map, and 133 coming back. That seam is the same one the [Fiber reconciler spoke](/architecture/react/fiber-reconciler) describes from the render side: `renderWithHooks` is called from `beginWork` as the reconciler descends the tree, and the `memoizedState` linked list every hook reads and writes hangs off the very `Fiber` objects the render phase walks and the commit phase applies. When `dispatchSetState` (the setter `mountState` bound in step 4) fires later, it schedules a new render, `renderWithHooks` runs again, and this time the update dispatcher walks the existing list instead of building it. ## By the numbers The `Dispatcher` interface is satisfied by four objects in `ReactFiberHooks.js` at this commit: `ContextOnlyDispatcher` (line 3898), `HooksDispatcherOnMount` (3926), `HooksDispatcherOnUpdate` (3954), and `HooksDispatcherOnRerender` (3982), each mapping roughly two dozen hook names to distinct functions, plus a parallel set of development-mode dispatchers that add hook-order validation. `ContextOnlyDispatcher` is the one installed outside render: every hook slot on it is `throwInvalidHookError`, which is what actually raises the "Invalid hook call" error a hook called from an event handler or a module body hits. `renderWithHooks` spans 130 lines (505 to 634), most of it the branching that selects which of those dispatchers to install. After the first render ever performed, `ReactSharedInternals.H` stays non-null for the life of the page; what changes is which object sits there, and "inside the body of a function component" is the window where that object is a real mount or update dispatcher. ## Integrations ### Cline MCP Server: Codebase Graph Setup https://symvanta.com/integrations/cline Cline builds its context by reading files as it works. You point it at a file or let it search, it opens what it needs, and it plans the edit from what it just read. That keeps every step visible and keeps you in control of what the agent looks at. It also means that on a large repository Cline only knows the structure it has already opened. The function it is about to change might have callers in files it never read, and reading enough of the tree to be certain costs tokens and turns. Symvanta gives Cline that structure over MCP: a live code graph where nodes are symbols and edges are calls, imports, and implementations. Cline queries it directly for the questions that file-reading answers slowly: who calls this function, what it depends on, what breaks if the signature changes, which consumer in another repo relies on it. Cline keeps reading and editing the way it already does. The graph is a second source it can call when the question is structural. ## Setting up the Symvanta MCP server in Cline 1. **Create a free Symvanta account** at [symvanta.com](https://symvanta.com/) and connect GitHub. 2. **Index the repos Cline works in.** A webhook reindexes on every push, so the graph stays current without a manual trigger. Plans start at $19/month, and Pro (per-seat) ships with a 7-day trial. 3. **Add Symvanta as a remote MCP server.** Click the MCP Servers icon (the stacked server icon in the Cline toolbar), open the **Remote Servers** tab, and fill in a **Server Name** (`symvanta`) and a **Server URL** (`https://mcp.symvanta.com/mcp`). Set **Transport Type** to **Streamable HTTP**, then click **Add Server**. 4. **Or edit the config directly.** In the MCP Servers panel, open the **Configure** tab and click **Configure MCP Servers** to open `cline_mcp_settings.json`. A remote entry needs `type` set to `streamableHttp` and a `url`: ```json { "mcpServers": { "symvanta": { "type": "streamableHttp", "url": "https://mcp.symvanta.com/mcp" } } } ``` The `type` value is `streamableHttp` in camelCase with no hyphen. Leaving it out or writing `streamable-http` makes Cline fall back to the legacy SSE transport and the connection fails. 5. **Call any Symvanta tool.** The first call opens a browser window for OAuth 2.0 (PKCE) login and hands Cline a scoped token. There is no API key to generate and no static `headers` block to paste into the config. Cline runs inside VS Code, so this sits next to whatever else you already use there. If you want the same graph available to the editor's own agent, the [VS Code MCP integration](/integrations/vs-code) uses the same endpoint. ## What Cline gets Once the server connects, Cline has 25 tools available over MCP. The ones it reaches for most during an edit are these: | Tool | What it gives Cline | |---|---| | `find_node` | Exact symbol definition, file, and signature | | `relate` (callers / dependencies / blast_radius / implementers) | Real callers, what a symbol depends on, what breaks if it changes, what implements an interface | | `ask_codebase` | Behavior questions ("how does X work") answered with cited files | | `locate` | Text, semantic, or config-key search across the repo | | `find_http_route` | The handler behind a route path and method | | `list_file_symbols` | Every symbol defined in a file, before Cline opens it | The graph covers TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby, so a polyglot repo keeps its coverage at the language boundary. ## Blast radius before a multi-file edit Say Cline is asked to change the signature of a shared validation helper used by three services. Working from its own reading, it opens the files it can find, edits the call sites it sees, and moves on, unaware of a fourth caller behind an interface or a consumer repo it never opened. The diff looks complete right up until the build fails somewhere Cline never looked. With Symvanta wired in, Cline calls `relate` with `kind: blast_radius` on the helper before touching anything. The result comes back as real edges: which functions call it directly, which route through an interface, and which repos outside the current workspace depend on it. That is the distinction we cover in [blast radius analysis](/blog/blast-radius-code-change-impact): a file read finds the call sites in one file, a blast radius returns the full graph of what depends on what. Cline plans the edit against that list, and the multi-file diff it proposes covers every real caller on the first pass. Why a graph answers this and similarity search does not is the subject of [code embeddings vs. code graph](/blog/code-embeddings-vs-code-graph), and you can see the same graph applied to real open-source repos on the [architecture pages](/architecture). ## Frequently asked questions
Cline says the server failed to connect. What is wrong? Check the `type` field. It must be `streamableHttp`, camelCase with no hyphen. If it is missing or spelled `streamable-http`, Cline falls back to SSE and the connection to a Streamable HTTP endpoint fails. Set `"type": "streamableHttp"` and reconnect from the MCP Servers panel.
Does this replace the files Cline reads on its own? No. Cline keeps reading files, running commands, and planning the way it does today. Symvanta adds a second source it can call over MCP for the structural questions file-reading answers slowly: exact callers, dependencies, blast radius, and cross-repo edges. The two compose.
Does it work in both Plan mode and Act mode? Yes. Cline can call MCP tools in either mode. In Plan mode it queries the graph to shape a plan before any edit lands, so the plan already reflects real callers and dependencies. In Act mode it runs the same tools while making the change.
Is my code used to train anything? No. Symvanta parses your repository into a graph of symbols and relationships. Source code is discarded by default once parsing completes and is never used to train models. An Enterprise source-storage add-on exists for teams that want raw source persisted and queryable.
[Book a 15-minute demo](/demo) --- ### Gemini CLI MCP Server for Codebase Context https://symvanta.com/integrations/gemini-cli Gemini CLI is Google's open-source terminal agent: a reason-and-act loop with a large context window and built-in file tools (grep, read, glob, shell). On a repo small enough to fit, that context window carries the run; Gemini reads what it needs and reasons over it. On a large one, the built-in tools do what every text-search agent does, walk the tree, grep for a name, open files to reconstruct structure that was never handed to it. That burns context on every turn and still misses the files that never got opened. A shared helper renamed in one place, three callers in files Gemini never read, and the run finishes clean with a broken build behind it. Symvanta closes that gap with an MCP server that hands Gemini CLI the actual code graph instead of a directory to search: exact symbol definitions and signatures, real caller and dependency edges, blast radius for a change before it ships, and cross-repo edges when the callers live in a separate repository entirely. ## Setting up Gemini CLI with Symvanta 1. **Create a free Symvanta account** at [symvanta.com](https://symvanta.com/) and connect GitHub. No card required to start. 2. **Index your repos.** Pick which ones Symvanta should parse into a graph. A webhook reindexes automatically on every push, so the graph Gemini reads from stays current. Plans start at $19/month, with a 7-day trial on the Pro tier. 3. **Add the Symvanta MCP server.** Gemini CLI reads MCP config from `settings.json`: `~/.gemini/settings.json` applies across every project on the machine, or `.gemini/settings.json` in a project root scopes it to that one repo. A remote HTTP server goes under `mcpServers`, keyed by `httpUrl`: ```json { "mcpServers": { "symvanta": { "httpUrl": "https://mcp.symvanta.com/mcp" } } } ``` The `httpUrl` property is what selects streamable HTTP transport; a plain `url` key would select SSE instead, and `command` would launch a local stdio process. For a remote endpoint like Symvanta's, `httpUrl` is the one you want. 4. **Or add it from the command line** instead of editing the file: ```bash gemini mcp add --transport http symvanta https://mcp.symvanta.com/mcp ``` Append `-s user` to write the entry to the global `~/.gemini/settings.json`, or `-s project` for the project-scoped file. 5. **Authenticate.** Symvanta's endpoint speaks OAuth 2.0 (PKCE), and Gemini CLI discovers that from the server on first contact. Trigger the browser login from inside a session with: ``` /mcp auth symvanta ``` Approve the flow once and Gemini holds the credentials for later sessions. No API key to paste, no token to babysit. 6. **Confirm the server is live.** Run the `/mcp` command inside a session to list connected servers and the tools each one exposes. Once `symvanta` shows up there, Gemini can call it in any session. ## What Gemini gets | Tool | What it does | |---|---| | `find_node` | Exact symbol definition and signature, no guessing which of several matches is real | | `relate` | Callers, dependencies, blast radius, implementers, and call chains, walked as graph edges | | `ask_codebase` | Behavior questions ("how does X work") answered with cited file references | | `locate` | Text, semantic, and config search across the indexed repo | | `find_http_route` | Route path and method resolved straight to its handler | | `list_file_symbols` | Every symbol defined in a file, in declaration order | | `map` | Architecture skeleton of a repo or a specific module | | `diff_impact` | What a branch or diff breaks before it merges | | `estimate_scope` | Rough sizing of a change across files and layers before Gemini commits to a plan | | `ref` | Pin a session to a feature branch, or overlay uncommitted edits so the graph reflects work in progress | The graph covers TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby, so a polyglot repo gets the same precision on every language in it. See the [architecture pages](/architecture) for real module maps built this way on well-known open-source repos. ## The graph shows what a rename breaks The failure worth designing around is the quiet one: a change that passes every check the agent can run and still breaks the build. Take a rename on a shared authentication helper called from a dozen sites across three services. Grep finds the string, Gemini edits the definition, and the run reports success, because from a text-search view success just means the pattern matched and changed. It has no signal that two of those call sites reach the helper through an interface with a different name, or that a fourth service in a separate repository imports it and was never opened during the task. Before that edit lands, a `relate` call with `kind: blast_radius` on the helper returns every real caller, across every indexed repository, ranked by confidence. The graph doesn't stop Gemini from making the change, it stops Gemini from being surprised by what the change touches. We go deeper on why this failure mode is so common in agent-driven edits in [blast radius analysis](/blog/blast-radius-code-change-impact). ## Frequently asked questions
Does this conflict with Gemini CLI's built-in file tools? No, they compose. Gemini's grep, read, and glob tools plus its large context window still do what they are good at: reading code and reasoning over what fits. Symvanta adds the structural edges (callers, dependencies, blast radius) that no context window reconstructs reliably from text. Why a call graph is a different signal from similarity search, we cover in [code embeddings vs. code graph](/blog/code-embeddings-vs-code-graph).
Is my code used to train anything? No. Symvanta parses your repository into a graph of nodes and edges, and that structure is what gets stored and queried. Source code itself is discarded by default after parsing and is never used for training. An Enterprise source-storage add-on exists for teams that specifically want raw source persisted and queryable.
Can I use the same Symvanta server in Claude Code, Cursor, or Codex too? Yes. The MCP endpoint and the underlying index are shared across clients: one repository, indexed once, queried by Gemini CLI, Claude Code, Cursor, Codex, or any other MCP-compatible agent you run against the same codebase. Nothing about the setup is Gemini-specific beyond the `settings.json` entry.
What does it cost? Plans start at $19/month for the Starter tier. Pro is $29/seat/month with a 7-day trial, and Enterprise is $99/seat/month with a 15-seat minimum. The MCP server itself doesn't charge per call or per token; you're paying for indexed repos and seats, not per-question usage.
[Book a 15-minute demo](/demo) --- ### JetBrains MCP Server: Codebase Graph Setup https://symvanta.com/integrations/jetbrains JetBrains IDEs ship two AI surfaces that read your code: AI Assistant for chat and inline help, and Junie for autonomous multi-step edits. Both lean on the IDE's own project index, the PSI model IntelliJ builds for whatever project is open. That index is precise inside a single project and answers "where is this defined" well. It gets thinner at the question that decides whether an edit is safe: who calls this function across every module, what breaks if I change its signature, and which repo two projects over depends on it. Symvanta adds that signal over MCP: a live code graph (nodes are symbols, edges are calls, imports, implements, instantiates) that AI Assistant or Junie can query directly, next to the IDE's own index. The two run side by side. Keep the JetBrains index for local structure and add Symvanta for the cross-project, branch-aware layer: exact symbol resolution, real callers, and blast radius before an edit lands. ## Add MCP servers in JetBrains IDEs AI Assistant keeps its MCP list at **Settings | Tools | AI Assistant | Model Context Protocol (MCP)**. Typing `/` in the chat and choosing **Add Command** lands in the same place. Click **Add** to open the **New MCP Server** dialog and pick the transport: **STDIO** for a server the IDE launches as a subprocess, **Streamable HTTP** for one it reaches over HTTP, or **SSE** for legacy servers still on that transport. If you already run servers in Claude Desktop, **Import from Claude** copies them across. Whichever transport you pick, the dialog takes JSON in the `mcpServers` format every MCP client uses. A stdio server names a command: ```json { "mcpServers": { "yourServerName": { "command": "node", "args": ["path/to/server.js"] } } } ``` An HTTP server names a URL: ```json { "mcpServers": { "yourServerName": { "url": "https://example.com/mcp" } } } ``` **Working directory** sets where relative paths resolve for a stdio command, and **Server level** decides whether the server is available in every project or only this one. Click **OK**, then **Apply**, and watch the **Status** column. Clicking the status icon shows the **List of available tools** the server reported, which is the fastest way to tell a live connection from a config typo. Symvanta is a remote server behind OAuth, and AI Assistant does not run that browser login yet, so its entry uses the STDIO shape with a bridge command. Here is the whole thing. ## Setup 1. Create a free Symvanta account at [symvanta.com](https://symvanta.com/). Connect GitHub and pick the repositories to index. A webhook reindexes on every push, so the graph tracks the branch you are on. Plans start at $19/month, with a 7-day trial on Pro. 2. Make sure Node.js 20 or newer is on your PATH. AI Assistant (2026.2) can read a remote MCP server from a plain `url` over the Streamable HTTP transport, but it does not yet run the OAuth browser login a server like Symvanta needs (tracked as JetBrains issue LLM-25012). The `mcp-remote` bridge closes that gap: it runs as a local stdio command, performs the OAuth 2.0 (PKCE) login in your browser, and forwards every call to the remote endpoint. 3. Open **Settings > Tools > AI Assistant > Model Context Protocol (MCP)**, click Add, switch to the JSON view, and paste: ```json { "mcpServers": { "symvanta": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.symvanta.com/mcp"] } } } ``` 4. The first tool call opens a browser window for the OAuth login. Approve it once; `mcp-remote` caches the token locally (under `~/.mcp-auth`) and reuses it on later calls. Restart the IDE window if AI Assistant does not pick up the new server right away. For **Junie**, open **Settings > Tools > Junie > MCP Settings**, click Add, and put the same `mcpServers` block in the `mcp.json` that opens. Junie reads the identical format, so one config covers both surfaces. Reload the IDE window for it to take effect. When native remote-OAuth support ships in AI Assistant, you can drop the bridge and point a `url` entry straight at `https://mcp.symvanta.com/mcp`. Until then, the `npx mcp-remote` command is the path that runs the login end to end, with no API key to paste and no static header to manage. ## What the agent gets | Tool | What the agent gets | |---|---| | `find_node` | Exact symbol definition, file, and signature, no pattern-matching | | `relate` (callers / dependencies / blast_radius / implementers) | Real callers, what a symbol depends on, what breaks if it changes, what implements an interface | | `ask_codebase` | Behavior questions ("how does X work") answered with cited files | | `locate` | Text, semantic, or config-key search across the repo | | `find_http_route` | The handler behind a route path and HTTP method | | `list_file_symbols` | Every symbol declared in a given file | | `map` | Architecture skeleton of a repo or module before diving in | | `estimate_scope` | Rough sizing of a change before the agent commits to a plan | The graph covers TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby, so an IntelliJ Java project, a PyCharm service, and a WebStorm frontend in the same workspace all keep coverage across the language boundary. These are 8 of the 25 tools Symvanta exposes over MCP; the rest cover history, freshness checks, and library-level lookups. ## Blast radius before you touch a shared function Say Junie is asked to change the signature of a shared `validateInput` helper used by both the payment flow and the auth flow, three modules apart, in a project no one has fully memorized. The IDE's own search finds every line containing `validateInput`, including a comment and an unrelated method with the same name on a different class. It does not tell the agent which calls are real. With Symvanta wired in, the agent calls `relate` with `kind: blast_radius` on the function first. That returns the actual call sites across both flows, including any that route through an interface with a different name at the call site, before a single line changes. The same holds for routing: if two controllers define near-duplicate handlers for `/api/users/:id`, `find_http_route` resolves the exact one that method and path hit. We cover why this distinction exists in [code embeddings vs. code graph](/blog/code-embeddings-vs-code-graph) and [blast radius analysis](/blog/blast-radius-code-change-impact), and you can see the same graph applied to real open-source repos on the [architecture pages](/architecture). ## Frequently asked questions
Does AI Assistant handle the OAuth login for a remote MCP server on its own? Not yet. Native OAuth 2.0 for remote MCP connections is an open JetBrains request (LLM-25012), so AI Assistant today accepts a direct `url` only for servers that sit behind a static token. Symvanta uses OAuth 2.0 (PKCE), and the `mcp-remote` bridge runs that browser login for you and caches the token, which is why the config above uses an `npx` command instead of a bare `url`. When native support ships you can switch to the `url` entry.
Does this work across all the JetBrains IDEs, or just IntelliJ IDEA? Yes. MCP support lives in the shared IntelliJ platform, so the AI Assistant and Junie MCP settings appear the same way in PyCharm, WebStorm, GoLand, PhpStorm, RubyMine, Rider, and CLion on 2026.2 and newer. The same `mcpServers` block works in every one.
Is my code used to train anything? No. Symvanta parses your repository into a graph of symbols and relationships. Source code is discarded by default after parsing, not retained or used for training. An Enterprise source-storage add-on exists for teams that specifically want raw source persisted and queryable.
What does this cost? Starter is $19/month. Pro is $29 per seat per month with a 7-day trial. Enterprise is $99 per seat per month with a 15-seat minimum and adds an on-prem bundle. MCP access is included at every tier, with no separate charge for the JetBrains connection.
[Book a 15-minute demo](/demo) --- ### Roo Code MCP Server: Codebase Graph Setup https://symvanta.com/integrations/roo-code Roo Code (the open-source VS Code agent, formerly Roo Cline) works by reading files directly, running commands you approve, and, if you enable it, a codebase index that embeds your repo for semantic search. That combination is good at pulling relevant files into context and pattern-matching across them. It stays weaker at the structural question that matters right before an edit: who calls this symbol, and what breaks if I change it. Reading a file tells the agent what that file contains. It doesn't tell it which other files depend on the function inside. Symvanta adds that structural layer over MCP: a real code graph (nodes are symbols, edges are calls, imports, implements, instantiates) that Roo Code queries directly, next to whatever its own reading and indexing already give it. The two run side by side. Keep Roo Code's file reading and optional index for finding and pattern search, and add Symvanta for structure: exact symbol resolution, real callers, and blast radius before an edit lands. ## Setting up the Roo Code MCP server 1. **Create a free Symvanta account** at [symvanta.com](https://symvanta.com/) and connect GitHub. 2. **Index the repos Roo Code works in.** A webhook reindexes on every push, so the graph stays current without a manual trigger. Plans start at $19/month, and Pro (per-seat) ships with a 7-day trial. 3. **Add Symvanta as an MCP server.** Open the MCP icon at the top of the Roo Code panel, scroll to the bottom of the MCP settings view, and click **Edit Global MCP** (the `mcp_settings.json` file that applies across every workspace) or **Edit Project MCP** (a `.roo/mcp.json` in the current project root). A remote server uses `type: streamable-http` with a `url`: ```json { "mcpServers": { "symvanta": { "type": "streamable-http", "url": "https://mcp.symvanta.com/mcp" } } } ``` Set `type` explicitly. Roo Code does not infer the transport from a URL alone, so a URL entry with no `"type": "streamable-http"` fails to connect. 4. **Call any Symvanta tool.** The first call opens a browser window for OAuth 2.0 (PKCE) login and hands Roo Code a scoped token. There is no API key to generate and no static header to paste into the config. If a server name exists in both files, the project-level `.roo/mcp.json` wins. That suits a team that wants the MCP connection checked into version control next to the code it indexes. The global `mcp_settings.json` suits a solo developer working across several repos on one machine. ## What Roo Code's agent gets Once the server is connected, Roo Code has 25 tools available over MCP. The ones it reaches for most during an edit-heavy session: | Tool | What it gives Roo Code | |---|---| | `find_node` | Exact symbol definition, file, and signature | | `relate` (callers / dependencies / blast_radius / implementers) | Real callers, what a symbol depends on, what breaks if it changes, what implements an interface | | `ask_codebase` | Behavior questions ("how does X work") answered with cited files | | `locate` | Text, semantic, or config-key search across the repo | | `find_http_route` | The handler behind a route path and method | | `list_file_symbols` | Every symbol defined in a file, without opening it | | `list_tests_for` | Existing tests that already cover a symbol | | `estimate_scope` | Rough sizing of a change before Roo Code commits to a plan | The graph covers TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby, so a polyglot repo keeps coverage at the language boundary. ## Blast radius before a shared edit Roo Code's Architect mode drafts a plan, then Code mode carries it out. Both are only as good as the context behind them. Say Roo Code is asked to change the signature of a validation helper used by three services. Reading the file it lives in, plus a grep for the name, finds a handful of call sites. It edits those and moves on, unaware of a fourth caller behind an interface or a consumer in a repo it never opened. With Symvanta wired in, Roo Code calls `relate` with `kind: blast_radius` on the helper first. The result comes back as real edges: which functions call it directly, which route through an interface, and which repos outside the open one depend on it. That is the distinction we cover in [blast radius analysis](/blog/blast-radius-code-change-impact): a grep result is a list of matching lines, a blast radius is the graph of what actually depends on what. Architect mode plans against that list, and the diff Code mode writes covers every real caller on the first pass. For an unfamiliar module, the pattern runs in reverse: call `map` for the module skeleton, see which files own which responsibilities, then plan, the way we walk real open-source repos through their module maps on the [architecture pages](/architecture). Either direction, the graph call lands while the fix is still cheap. Why a graph answers this question and embeddings can only approximate it is the subject of [code embeddings vs. code graph](/blog/code-embeddings-vs-code-graph). ## Frequently asked questions
Does this replace Roo Code's codebase indexing? No. Roo Code's file reading and its optional embeddings-based codebase index stay exactly as they are. Symvanta adds a second source Roo Code can call over MCP for the questions retrieval answers poorly: exact callers, dependencies, blast radius, and cross-repo edges. The two compose.
Do I need an API key or a header for the connection? No. The config entry is just `type` and `url`. Authentication happens on the first tool call through an OAuth 2.0 (PKCE) browser login, so there is no key to generate or header to store in `mcp_settings.json`. Do set `type` to `streamable-http`, since Roo Code cannot infer the transport from the URL.
Is my code used to train anything? No. Symvanta parses your repository into a graph of symbols and relationships. Source code is discarded by default once parsing completes and is never used to train models. An Enterprise source-storage add-on exists for teams that want raw source persisted and queryable.
What does Symvanta cost? Starter is $19/month. Pro is $29 per seat per month with a 7-day trial. Enterprise is $99 per seat per month with a 15-seat minimum, for teams that need on-prem deployment or SSO. MCP access is included at every tier, so connecting Roo Code adds no separate charge.
[Book a 15-minute demo](/demo) --- ### Zed MCP Server: Codebase Graph Setup https://symvanta.com/integrations/zed Zed's agent panel is fast because Zed is fast: a Rust editor with the model wired straight into the open worktree. When the agent needs context, it reads the files you have open and greps the tree. That holds up until a question depends on structure the text doesn't show: which functions call this one, what a signature change breaks, which module in another repo imports it. Grep returns matching lines. It doesn't return edges. Symvanta supplies those edges over MCP: a live code graph where nodes are symbols and edges are calls, imports, implements, and instantiates, queried by Zed's agent like any other tool. Zed keeps reading and editing the way it already does. The graph answers the questions grep can't: exact symbol resolution, real callers, and blast radius before an edit lands. ## Set up MCP servers in Zed Zed calls MCP servers **context servers**, and there are two ways to get one running. The first is the extension route: open **Settings > AI > MCP Servers**, click **Add Server** in the page header, and pick **Install from Extensions**. The same catalog is in the command palette under `zed: extensions` and on the extensions directory at zed.dev filtered to context servers. GitHub, Puppeteer, Brave Search, Prisma, and Figma all ship as extensions, and most open a setup modal on install asking for whatever credential they need (the GitHub one wants a personal access token). The second is a custom server, for anything without an extension. **Add Server** offers **Add Local Server** and **Add Remote Server**, and both write into the `context_servers` object in `settings.json`, which you can also edit by hand with the `zed: open settings file` action. A local server is a command Zed spawns over stdio. A remote server is a URL Zed talks to over HTTP: ```json { "context_servers": { "local-mcp-server": { "command": "some-command", "args": ["arg-1", "arg-2"], "env": {} }, "remote-mcp-server": { "url": "https://example.com/mcp", "headers": { "Authorization": "Bearer " } } } } ``` Leave `headers` out of a remote entry and Zed runs the standard MCP OAuth flow in your browser on the first call, which is the path any server with real accounts behind it takes. Zed supports the tools and prompts halves of MCP and reloads a server's tool list on its own when the server changes what it offers. Approval is governed by `agent.tool_permissions.default` (`confirm`, `allow`, or `deny`, on v0.224.0 and newer), and a single tool can be named as `mcp::` when you want one exception to the default. Symvanta is the remote shape with no `headers` block, because the login runs over OAuth. Here is the whole setup. ## Setting up the Symvanta MCP server in Zed 1. **Create a free Symvanta account** at [symvanta.com](https://symvanta.com/) and connect GitHub. 2. **Index the repos your agent works in.** A webhook reindexes on every push, so the graph stays current without a manual trigger. Plans start at $19/month, and Pro (per-seat) ships with a 7-day trial. 3. **Add Symvanta as a remote MCP server.** Zed keys MCP servers under `context_servers` in `settings.json` (open it with the `zed: open settings file` action). A remote server uses a `url` field: ```json { "context_servers": { "symvanta": { "url": "https://mcp.symvanta.com/mcp" } } } ``` Prefer the UI: open **Settings > AI > MCP Servers** (also reachable through the `agent: open settings` action), click **Add Server** in the page header, choose **Add Remote Server**, and paste the same URL. 4. **Call any Symvanta tool.** Because the config carries no `Authorization` header, Zed runs the standard MCP OAuth flow (2.0 with PKCE) in your browser on the first tool call. Approve the connection once and Zed holds the scoped session for later calls. There's no API key to generate and no static header to paste. Native remote MCP over URL landed in Zed in late 2025. On an older build that only speaks stdio, bridge the same endpoint with `mcp-remote`: set `"command": "npx"` and `"args": ["-y", "mcp-remote", "https://mcp.symvanta.com/mcp"]` instead of the `url` field, and let the bridge handle the HTTP transport and OAuth. ## What Zed's agent gets Once the server is connected, Zed's agent has 25 tools available over MCP. The ones it reaches for most during an edit-heavy session are these: | Tool | What it does | |---|---| | `find_node` | Exact symbol definition, file, and signature, no pattern-matching | | `relate` (callers / dependencies / blast_radius / implementers) | Real callers, what a symbol depends on, what breaks if it changes, what implements an interface | | `ask_codebase` | Behavior questions ("how does X work") answered with cited files | | `locate` | Text, semantic, or config-key search across the repo | | `find_http_route` | The exact handler behind a route path and method | | `list_file_symbols` | Every symbol declared in a file, in order | The graph covers TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby, so a Rust service that calls into a TypeScript package keeps its edges across the language boundary. The tools above are a subset; the rest cover architecture maps, diff impact, test lookup, scope estimates, and branch-aware reads. ## Blast radius before you touch a shared function Say the agent is asked to change the signature of a `validateInput` helper used by both the payment flow and the auth flow, three files apart, in a repo it has only partly read. Reading and grepping finds every line containing `validateInput`, including a comment and an unrelated method with the same name on a different class. It doesn't say which calls are real. With Symvanta wired in, the agent calls `relate` with `kind: blast_radius` on the function first. That returns the actual call sites across both flows, including any that route through an interface with a different name at the call site, before a single line changes. The distinction is the one we cover in [blast radius analysis](/blog/blast-radius-code-change-impact): a grep result is a list of matching lines, a blast radius is the graph of what depends on what. The agent plans the edit against that list, and the multi-file diff it proposes covers every real caller on the first pass. The same pattern applies to routing: if two controllers define near-duplicate handlers for `/api/users/:id`, `find_http_route` resolves the exact one that method and path hit. For why an embeddings index and a call graph answer different questions, see [code embeddings vs. code graph](/blog/code-embeddings-vs-code-graph), and the same graph runs against real open-source repos on the [architecture pages](/architecture). ## Frequently asked questions
Does Symvanta replace what Zed's agent already reads? No. Zed's agent keeps reading files and grepping the worktree exactly as it does now. Symvanta adds a second source it can call over MCP for the questions text search answers poorly: exact callers, dependencies, blast radius, and cross-repo edges. The two compose.
Does Zed support remote MCP servers by URL? Yes. Zed added native HTTP transport for MCP servers in late 2025, so a `context_servers` entry with a `url` field connects directly and runs the MCP OAuth flow when no `Authorization` header is set. On an older stdio-only build, bridge the same URL with `mcp-remote` over `npx`.
Is my code used to train anything? No. Symvanta parses your repository into a graph of symbols and relationships. Source code is discarded by default after parsing rather than retained or used for training. An Enterprise source-storage add-on exists for teams that specifically want raw source persisted and queryable.
What does Symvanta cost? Starter is $19/month. Pro is $29 per seat per month with a 7-day trial. Enterprise is $99 per seat per month with a 15-seat minimum, for teams that need on-prem deployment or SSO. MCP access is included at every tier, with no separate charge for the Zed connection.
[Book a 15-minute demo](/demo) --- ### Claude Code MCP Server: Give It Your Codebase https://symvanta.com/integrations/claude-code Claude Code is good at reading and grepping. Point it at a small repo and it can hold the whole thing in working context, follow an import by hand, and get the shape of a change right. Point it at a large monorepo and the same agent is still grepping, just against ten times as many false matches, burning context on files that turn out irrelevant, and guessing at the rest with the same confidence it had on the small repo. Symvanta gives Claude Code the thing grep can't: a code graph. Instead of a string match on `getUserById`, it gets the exact symbol, its real signature, and every caller resolved through the graph rather than pattern-matched by name. Claude Code is the client we build this for first: this page covers both ways to wire the two together. ## Connect Claude Code to Symvanta 1. Create a free account at [symvanta.com](https://symvanta.com), connect GitHub, and pick the repositories to index. Indexing runs automatically from there: GitHub webhooks reindex the graph on every push, so you don't trigger it by hand. 2. Install the plugin (recommended). It's the fastest path and it ships more than the MCP connection: tool-routing skills and hooks that steer Claude Code onto the graph instead of falling back to its own grep. ```bash /plugin marketplace add Symvanta/claude-plugin /plugin install symvanta@symvanta ``` Run `/reload-plugins` to activate it, then `/mcp` to complete sign-in. 3. Or skip the plugin and add the plain MCP server. This gets you the tools without the skills and hooks: ```bash claude mcp add --transport http symvanta https://mcp.symvanta.com/mcp ``` First use opens a browser for an OAuth 2.0 (PKCE) login: no API key to generate or paste. Both paths point at the same hosted endpoint, so nothing here requires you to run infrastructure. Paid plans start at $19/month, with a 7-day trial on the Pro tier; the free tier is enough to try this against a real repository before you decide anything. ## What Claude Code gets
Diagram of Claude Code connecting to the Symvanta MCP endpoint over HTTPS with OAuth sign-in; the MCP serves the code graph of nodes and edges, and GitHub webhooks reindex the graph on every push
How Claude Code reaches the graph: MCP over HTTPS to mcp.symvanta.com/mcp, with the graph reindexed on every push. Link to this diagram Open full size
Symvanta exposes 25 tools over MCP. The ones that change how an agent navigates a large codebase: | Tool | What it does | |---|---| | `find_node` | Resolves a symbol to its exact file, line range, and signature | | `relate` | Callers, dependencies, blast radius, implementers, call chains, all as graph traversals | | `ask_codebase` | Answers behavior questions ("how does X work") with cited file references | | `locate` | Text, semantic, or config-key search across the repo | | `map` | A repo or module's architecture skeleton | | `find_http_route` | Jumps from a path and HTTP method straight to its handler | | `diff_impact` | What a branch or diff breaks, before you merge it | | `list_tests_for` | The tests that already cover a given symbol | | `estimate_scope` | Sizes a change across files and layers before you start it | | `ref` | Pins a feature branch, or overlays uncommitted edits on the graph | Coverage runs across TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby. The [architecture pages](/architecture) show what this output looks like on real open-source repos, walking their module maps the same way it would walk yours. ## What changes in practice Take a shared helper function used across a dozen files. Ask Claude Code to change its signature without any of this wired in, and it greps the function name, reads the files that match, and edits based on what it read. It has no way to know whether a match is a real call, a comment, or an unrelated method with the same name, and it has no way to see a caller it didn't happen to grep for. With Symvanta connected, the same task starts with a `relate` call, `kind: blast_radius`, against the helper's node. That returns every real caller, resolved through the graph rather than guessed from a string match, across every file and every linked repository. The agent sees the actual impact surface before it writes a single edit, then makes the change knowing what it touches instead of finding out from a broken build later. Same task, same agent, the difference is whether the first step is a graph traversal or a coin flip. We go deeper on why that gap exists in [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases), and on what the shorter loop does to the model bill in [Sonnet plus a code graph vs Opus alone](/blog/sonnet-plus-code-graph-vs-opus). This doesn't replace Claude Code's own file tools. It complements them: the plugin routes code-navigation questions to the graph and leaves everything else, reading a file, running a command, writing the edit, to Claude Code as usual. ## Frequently asked questions
Is my code used to train anything? No, never. Symvanta parses your repository into a graph of nodes and edges: symbols and the relationships between them. The parsed structure is what powers the MCP tools. Source code itself is not retained by default and is never used for training.
How fresh is the index? GitHub webhooks reindex a repository on every push to its tracked branch, so the graph stays current without you triggering anything manually. For a feature branch or uncommitted local edits, the `ref` tool pins the session to that branch or overlays the edits on a synthetic revision, so the agent can query work that hasn't landed on the default branch yet.
Does this replace Claude Code's built-in file tools? No. Claude Code still reads files, runs commands, and writes edits the way it always has. Symvanta adds a second option for the specific question of "where is this" or "what does this touch": the plugin's skills steer those questions to the graph instead of a grep pass, but nothing about Claude Code's own tools changes.
What does it cost? The account is free to start: connect GitHub, index your repos, and use the MCP server against them. Paid plans start at $19/month, with Pro seats at $29/month and a 7-day trial. Enterprise is $99 per seat per month with a 15-seat minimum, for teams that need on-prem deployment or SSO.
--- ### Claude Desktop MCP Connector: Codebase Context https://symvanta.com/integrations/claude-desktop Claude Desktop and claude.ai can already reason about a codebase without an IDE open: ask an architecture question in plain chat, walk a new hire through a module, or scope an impact analysis from your phone before a standup. What Claude doesn't have by default is access to that codebase. It has a chat window and whatever you paste into it. Symvanta's remote MCP server closes that gap. Connect it once and Claude can call `find_node`, `relate`, `ask_codebase`, and the rest of the graph directly, in the same conversation, with no file uploads and no copy-pasting a directory tree into the prompt. ## Setup Start with a Symvanta account: create a free account at [symvanta.com](https://symvanta.com), connect GitHub, and pick the repositories to index. Webhooks reindex on every push after that, so the graph stays current without a manual step. Plans start at $19/month, with a 7-day Pro trial. From there, two paths connect Claude to the graph. Pick the one that matches how you use Claude. ### Path 1: Custom connector (claude.ai web and Claude Desktop) Per Anthropic's help center, custom connectors using remote MCP are available on Claude, Cowork, and Claude Desktop for Free, Pro, Max, Team, and Enterprise plans (Free accounts are limited to one connector). The steps differ slightly by plan. **Pro and Max:** 1. Go to Settings > Connectors. 2. Click the "+" button, then "Add custom connector." 3. Enter `https://mcp.symvanta.com/mcp` as the server URL. 4. Click "Add." 5. On first use, Claude opens a browser window for an OAuth 2.0 (PKCE) login against your Symvanta account. Approve it once; no API key to generate or paste. **Team and Enterprise:** 1. An organization Owner goes to Organization settings > Connectors, clicks "Add," then "Custom," then "Web," and enters the same URL. 2. Members then go to Settings > Connectors, find the connector (labeled "Custom"), and click "Connect" to run their own OAuth login. Because this is a remote connector, Claude calls it from Anthropic's cloud infrastructure rather than from your machine, which is also why it works identically on claude.ai in a browser and inside the Claude Desktop app: same URL, same login, same tools either way. ### Path 2: Signed desktop connector bundle (.mcpb) If you live in Claude Desktop and want a one-click install, download the signed Symvanta bundle from [symvanta.com/connector/download](https://symvanta.com/connector/download) while logged in to your Symvanta account. That page issues a `.mcpb` file, Anthropic's packaged desktop-extension format, pre-configured for your tenant. Install it any of the three ways Claude Desktop supports for `.mcpb` files: double-click it, drag it into the Claude Desktop window, or go to Settings > Extensions > Advanced settings > Install Extension and pick the file. Claude shows an install screen with the extension's permissions before it activates. Either path lands you in the same place: an active Symvanta connection inside Claude, and no API keys living in a config file. ## What Claude gets
Diagram of Claude Desktop connecting to the Symvanta MCP endpoint over HTTPS with OAuth sign-in; the MCP serves the code graph of nodes and edges, and GitHub webhooks reindex the graph on every push
How Claude Desktop reaches the graph: MCP over HTTPS to mcp.symvanta.com/mcp, with the graph reindexed on every push. Link to this diagram Open full size
Once connected, Claude can call the same graph tools that power Symvanta's IDE and CLI integrations. Headline tools: | Tool | What it does | |---|---| | `find_node` | Resolves a symbol to its exact file, line range, and signature | | `relate` (callers) | Lists every caller of a function or method | | `relate` (dependencies) | Lists what a symbol depends on | | `relate` (blast_radius) | Shows what breaks if a symbol changes, across files and repos | | `ask_codebase` | Answers "how does X work" questions with cited file references | | `locate` | Text, semantic, or config search across the indexed repos | | `map` | Renders a repo or module architecture skeleton | | `find_http_route` | Jumps from an HTTP method and path straight to its handler | | `list_tests_for` | Finds the tests that already cover a given symbol | | `estimate_scope` | Sizes a proposed change before you commit to it | That's 10 of the 25 tools the server exposes. Supported languages across the graph: TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby. The tools work the same whether Claude is running in a terminal agent or in a chat window, because the graph doesn't care which client is asking. For how that graph gets built in the first place, see [the architecture pages](/architecture). ## Non-IDE use cases This connector matters most in the moments where you're not sitting in an editor. **Onboarding.** A new engineer can chat through the module map, the entry points, and the ownership boundaries before writing a single line, asking follow-up questions the way they'd ask a senior teammate. **"What breaks if we change X."** A PM or staff engineer can ask that question directly in claude.ai without pulling in an IDE or a developer's time, and get a blast-radius answer backed by the actual call graph. **Code review from a phone or tablet.** Open claude.ai, paste a PR link or describe the change, and ask Claude to trace the affected callers before you're back at a desk. The MCP connection means the answer comes from the live graph; Claude's memory of what the code probably does never enters it. ## Why this beats a Claude Project upload Uploading files to a Claude Project gives Claude a snapshot: accurate the day you uploaded it, stale the next time someone merges a PR. It also gives Claude text, not structure, so "who calls this function" still has to be re-derived by reading through files by hand. A live MCP connection reindexes on every push and answers connectivity questions as graph traversals: `relate` (callers) is a single lookup. We go deeper on that distinction in [code embeddings vs. code graph](/blog/code-embeddings-vs-code-graph). ## Frequently asked questions
Is my code used to train Symvanta or Claude? No, never. Symvanta parses your repository into a graph (nodes and edges representing symbols and their relationships) that lives in memory for querying. Source code itself is discarded by default after parsing and is not used for training.
Do I need Claude Desktop installed, or does claude.ai work? Both work. Anthropic's documentation states that custom connectors using remote MCP are available on claude.ai, Claude Desktop, and Cowork, and that they function identically across platforms because the connection runs from Anthropic's cloud rather than from your local device. The signed `.mcpb` bundle is the one Desktop-only option, useful if you prefer a one-click install over pasting a URL.
How is this different from uploading files to a Claude Project? A Project upload is a snapshot: it's accurate the moment you upload it and drifts out of date with every commit after. Symvanta's MCP connection reindexes on push, so Claude is always querying the current graph. It's also structural: a Project upload gives Claude raw file text to re-read for every question, while the MCP server gives Claude exact call graphs, so "who calls this" is a direct traversal.
What does this cost? The MCP connection itself is included in every Symvanta plan. Plans start at $19/month, with a 7-day trial on the Pro tier. There's no separate charge for connecting Claude Desktop or claude.ai once your repositories are indexed.
--- ### Codex CLI MCP Server for Codebase Context https://symvanta.com/integrations/codex-cli Codex CLI is built to run long, mostly unattended tasks in a terminal: a migration, a refactor, a multi-file bug fix, with minimal check-ins along the way. On a small repo that works because Codex can read every file that matters. On a large one, its default context strategy is reading and grepping through source files to reconstruct structure it was never given, and that costs tokens on every turn while still missing the parts of the codebase that never got opened. A shared helper renamed in one file, three callers in files Codex never read, and the task finishes green with a broken build waiting on the other side. Symvanta closes that gap with an MCP server that hands Codex the actual code graph instead of a pile of text to search: exact symbol definitions and signatures, real caller and dependency edges, blast radius for a change before it ships, and cross-repo edges when the callers live in a different repository entirely. ## Add MCP servers to Codex CLI Codex keeps its MCP servers in `~/.codex/config.toml`, and reads a project-scoped `.codex/config.toml` too when a server should only exist inside one repo. Each server is a TOML table named `[mcp_servers.]`, and the fields you set depend on the transport. A local stdio server is a command Codex launches as a subprocess: ```toml [mcp_servers.context7] command = "npx" args = ["-y", "@upstash/context7-mcp"] env_vars = ["LOCAL_TOKEN"] [mcp_servers.context7.env] MY_ENV_VAR = "MY_ENV_VALUE" ``` `command` is the only required field. `args`, `cwd`, the `env` sub-table, and `env_vars` (names forwarded from your shell) are optional. A remote streamable HTTP server is a URL: ```toml [mcp_servers.figma] url = "https://mcp.figma.com/mcp" bearer_token_env_var = "FIGMA_OAUTH_TOKEN" ``` `url` is required here. `bearer_token_env_var` names an environment variable holding a static token, and `auth` picks the login method (`oauth` or `chatgpt`) for servers that run a real sign-in. The `codex mcp` subcommands write the same file, so the editor is optional. `codex mcp add -- ` registers a stdio server, `codex mcp add --url ` registers a streamable HTTP one, and `--bearer-token-env-var` attaches the token variable to either. `codex mcp list` prints what is configured, `codex mcp login ` runs the browser login for a server that needs one, and `codex mcp --help` covers the rest. Codex stores the variable name and never the token value, which keeps `config.toml` safe to check in. Symvanta uses the remote shape and an OAuth login, so no token variable is involved. ## Setting up Codex CLI with Symvanta 1. **Create a free Symvanta account** and connect GitHub. No card required to start. 2. **Index your repos.** Pick which ones Symvanta should parse into a graph. A webhook reindexes automatically on every push, so the graph Codex reads from stays current. Plans start at $19/month, with a 7-day trial on the Pro tier. 3. **Add the Symvanta MCP server to `~/.codex/config.toml`.** Codex CLI supports remote streamable HTTP MCP servers natively, configured under an `[mcp_servers.]` table: ```toml [mcp_servers.symvanta] url = "https://mcp.symvanta.com/mcp" auth = "oauth" ``` `auth = "oauth"` is the default for streamable HTTP servers, so the line above is there for clarity more than necessity. If you would rather not open an editor, `codex mcp add symvanta --url https://mcp.symvanta.com/mcp` writes the same table from the terminal. 4. **Authenticate.** Run: ```bash codex mcp login symvanta ``` This opens a browser for the standard OAuth PKCE flow and stores the resulting credentials for Codex to use on every session afterward. No API key to paste, no token to babysit. 5. **Confirm the server is live:** ```bash codex mcp list ``` Once `symvanta` shows up there, Codex can call it in any session, including the long, autonomous ones this tool is built for. ## What Codex gets
Diagram of Codex CLI connecting to the Symvanta MCP endpoint over HTTPS with OAuth sign-in; the MCP serves the code graph of nodes and edges, and GitHub webhooks reindex the graph on every push
How Codex CLI reaches the graph: MCP over HTTPS to mcp.symvanta.com/mcp, with the graph reindexed on every push. Link to this diagram Open full size
| Tool | What it does | |---|---| | `find_node` | Exact symbol definition and signature, no guessing which of five matches is real | | `relate` | Callers, dependencies, blast radius, implementers, and call chains, walked as graph edges | | `ask_codebase` | Behavior questions ("how does X work") answered with cited file references | | `locate` | Text, semantic, and config search across the indexed repo | | `map` | Architecture skeleton of a repo or a specific module | | `find_http_route` | Route path and method resolved straight to its handler | | `diff_impact` | What a branch or diff breaks before it merges | | `list_tests_for` | The tests that already cover a given symbol | | `estimate_scope` | Rough sizing of a change across files and layers before Codex commits to a plan | | `ref` | Pin a session to a feature branch, or overlay uncommitted edits so the graph reflects work in progress | The graph covers TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby, so a polyglot repo gets the same precision on every language in it. See the [architecture pages](/architecture) for real module maps built this way on well-known open-source repos. ## Long tasks surface what they break The place this matters most is exactly the workload Codex CLI is built for: a task you kick off and check back on later, unsupervised in between. Take a migration that touches a shared authentication helper across a dozen call sites in three services. A grep-based approach finds the string, edits the definition, and reports success, because success from a text search perspective just means the pattern was found and changed. It has no way to know that two of those call sites route through an interface with a different name, or that a fourth service in a different repository imports the same helper and was never opened during the task. Before Codex commits to that kind of change, a `relate` call with `kind: blast_radius` on the helper returns every real caller, across every indexed repository, ranked by confidence. That's the difference between a task that finishes and a task that finishes correctly: the graph doesn't stop Codex from making the edit, it stops Codex from being surprised by what the edit touches. We go deeper on why this specific failure mode is so common in agent-driven changes in [blast radius analysis](/blog/blast-radius-code-change-impact). ## Frequently asked questions
Does this work with Codex's sandbox and approval modes? Yes, with one thing to be aware of. Codex CLI's sandbox and approval settings (read-only, workspace-write, full access) govern filesystem and network access for the commands Codex executes, and MCP servers are configured separately in `config.toml`. If you run Codex in a mode with network access restricted or disabled, a remote MCP server like Symvanta's needs that access to respond, so workspace-write or full-access mode (or whatever profile in your setup permits outbound network calls) is what you want when Symvanta is wired in.
Is my code used to train anything? No. Symvanta parses your repository into a graph of nodes and edges, that structure is what gets stored and queried. Source code itself is discarded by default after parsing and is never used for training.
Can I use the same Symvanta server in Claude Code or Cursor too? Yes. The MCP endpoint and the underlying index are shared across clients: one repository, indexed once, queried by Codex CLI, Claude Code, Cursor, or any other MCP-compatible agent you run against the same codebase. Nothing about the setup is Codex-specific beyond the `config.toml` entry.
What does it cost? Plans start at $19/month for the Starter tier. Pro is $29/seat/month with a 7-day trial, and Enterprise is $99/seat/month with a 15-seat minimum. The MCP server itself doesn't charge per call or per token; you're paying for indexed repos and seats, not per-question usage.
--- ### Cursor MCP Server: Codebase Context and Setup https://symvanta.com/integrations/cursor Cursor's own codebase indexing is embeddings-based retrieval: it chunks your repo, embeds it, and ranks results by similarity to whatever the agent is asking. That's genuinely good at "find code like this." It's weaker at the question that actually matters before an edit: who calls this function, and what breaks if I change it. Similarity in vector space isn't the same signal as an edge in a call graph, and an agent that only has the former is guessing at the latter. Symvanta adds that second signal over MCP: a real code graph (nodes are symbols, edges are calls, imports, implements, instantiates) that Cursor's agent can query directly, alongside whatever Cursor's own index already gives you. The two run side by side. Keep Cursor's indexing for similarity search and add Symvanta for structure: exact symbol resolution, a real call graph, and blast radius before an edit lands. ## Setup 1. Create a free Symvanta account at [symvanta.com](https://symvanta.com/). 2. Connect GitHub and pick the repositories to index. A webhook reindexes on every push, so the graph stays current without manual re-syncing. Plans start at $19/month, with a 7-day trial on Pro. 3. Add the Symvanta MCP server to Cursor. Cursor reads MCP config from `.cursor/mcp.json` in your project root (scoped to that project) or `~/.cursor/mcp.json` in your home directory (available across all projects). For the project-level file: ```json { "mcpServers": { "symvanta": { "url": "https://mcp.symvanta.com/mcp" } } } ``` 4. The first time Cursor's agent calls a Symvanta tool, it triggers an OAuth 2.0 (PKCE) browser login. Approve the connection once and Cursor holds the session for subsequent calls. 5. Cursor also lists connected MCP servers under Customize in the sidebar, where you can toggle individual tools on or off if you want to trim what the agent sees. That's the whole setup: no separate daemon, no local index to keep warm. The graph lives on Symvanta's side and Cursor's agent queries it like any other MCP tool, and every push to a linked repository triggers a reindex through the webhook, so the graph the agent queries during a session matches the branch you're actually working on, never a stale snapshot from last week. If you'd rather keep the config out of the repo entirely, add the same block to `~/.cursor/mcp.json` instead of the project file. The global file applies across every project Cursor opens on that machine, which is the better fit for a solo developer working across several repos; the project-level file is the better fit for a team that wants the MCP connection checked into version control alongside the code it's indexing. ## What Cursor's agent gets
Diagram of Cursor connecting to the Symvanta MCP endpoint over HTTPS with OAuth sign-in; the MCP serves the code graph of nodes and edges, and GitHub webhooks reindex the graph on every push
How Cursor reaches the graph: MCP over HTTPS to mcp.symvanta.com/mcp, with the graph reindexed on every push. Link to this diagram Open full size
| Tool | What it does | |---|---| | `find_node` | Exact symbol definition and signature, no pattern-matching | | `relate` | Callers, dependencies, blast radius, implementers, call chains | | `ask_codebase` | Behavior questions answered with cited file references | | `locate` | Text, semantic, or config-key search across the repo | | `map` | Architecture skeleton for a repo or a module | | `find_http_route` | Exact handler for a given path and HTTP method | | `diff_impact` | What a branch or pending diff actually breaks | | `list_tests_for` | Existing tests that already cover a symbol | | `estimate_scope` | Rough sizing for a proposed change before you commit to it | | `ref` | Pin a session to a feature branch, or overlay uncommitted edits | Supported languages: TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby. These are 10 of 25 tools Symvanta exposes; the rest cover history, freshness checks, and library-level lookups. ## Blast radius before you touch a shared function Say Cursor's agent is asked to change the signature of a `validateInput` helper used by both the payment flow and the auth flow, three files apart, in a repo neither of you has fully memorized. Grep-and-guess retrieval finds every line containing `validateInput`, including a comment and an unrelated method with the same name on a different class. It doesn't tell you which calls are real. With Symvanta wired in, the agent calls `relate` with `kind: blast_radius` on the function first. That returns the actual call sites across both flows, including any that route through an interface with a different name at the call site, before a single line changes. The same pattern applies to routing: if two controllers define near-duplicate handlers for `/api/users/:id`, `find_http_route` resolves the exact one that method and path actually hit; no plausible-looking guess required. We go deeper on why this distinction exists at all in [code embeddings vs. code graph](/blog/code-embeddings-vs-code-graph), and you can see the same graph applied to real open-source repos on the [architecture pages](/architecture). ## Frequently asked questions
Does this conflict with Cursor's built-in codebase indexing? No, they compose. Cursor's index is embeddings-based and answers "what looks like this." Symvanta's graph answers "what calls this, what depends on it, what breaks if it changes." Leave Cursor's indexing on and add Symvanta as a second, structural source of context.
Is my code used to train anything? No. Symvanta parses your repository into a graph of symbols and relationships. Source code itself is discarded by default after parsing rather than retained or used for training; an Enterprise source-storage add-on exists for teams that specifically want raw source persisted and queryable.
Does this work in every Cursor mode? Cursor's own docs state that it automatically uses MCP tools listed under Available Tools when relevant, including Plan Mode, and that MCP tools follow the same run-mode rules as terminal commands. In practice, if a Symvanta tool is enabled in Customize, the agent can call it whenever the current mode allows tool use.
What does this cost? Symvanta plans start at Starter, $19/month, with Pro at $29/seat/month including a 7-day free trial. Enterprise is $99/seat/month with a 15-seat minimum and adds an on-prem bundle. The Cursor MCP connection itself is not a separate charge: it's included in whatever plan you're already on.
--- ### VS Code Copilot MCP Server for Codebase Context https://symvanta.com/integrations/vs-code Copilot's agent mode in VS Code can run a genuinely multi-step task: read a few files, edit several more, run a command, check the result. What it reasons over while doing that is retrieval plus whatever you have open; a map of the codebase never enters the picture. Ask it who calls a shared method, or what a schema change breaks three services downstream, and it is pattern-matching on text and open tabs, then presenting a guess with full confidence. Symvanta's MCP server gives agent mode the actual code graph, callers, dependencies, blast radius, each resolved in one tool call. It sits alongside Copilot's own retrieval: use Copilot for the fast, file-local stuff, and hand agent mode a Symvanta tool the moment the question is structural. ## Setting up Symvanta in VS Code 1. **Create a free Symvanta account** at [symvanta.com](https://symvanta.com/). 2. **Connect GitHub.** Install the Symvanta GitHub App and grant it access to the repositories you want indexed. 3. **Index your repos.** Symvanta builds the graph on first index, then a webhook reindexes automatically on every push, so agent mode is never working off a stale snapshot. Plans start at $19/month, with a 7-day trial on the Pro tier if you want branch-aware indexing and higher repo limits before you commit. 4. **Add the MCP server in VS Code.** Per the [official VS Code MCP docs](https://code.visualstudio.com/docs/copilot/customization/mcp-servers), the fastest path is a workspace config file at `.vscode/mcp.json`: ```json { "servers": { "symvanta": { "type": "http", "url": "https://mcp.symvanta.com/mcp" } } } ``` Commit this file if you want the whole team wired up the same way; it takes precedence over any user-level entry with the same name for that workspace. For a guided flow with no JSON editing, open the Command Palette and run **MCP: Add Server**, pick **HTTP** as the server type, paste `https://mcp.symvanta.com/mcp` as the URL, and choose **Workspace** to scope it to this repo or **Global** to make it available across every project you open. Global setup goes through **MCP: Open User Configuration** instead of `.vscode/mcp.json`. 5. **Authenticate once.** The first time agent mode calls a Symvanta tool, VS Code opens a browser window for an OAuth 2.0 login with PKCE. There is no API key to generate or paste into the config; approve the login once and VS Code holds the session for you. 6. **Confirm the tools are live.** Open Copilot Chat, switch to agent mode, and click **Configure Tools** in the chat input. Symvanta's tools show up grouped under the server name, each with an on/off toggle if you want to trim the set the model can reach for a given session. If your organization manages Copilot centrally, VS Code's `chat.mcp.access` setting can restrict which MCP servers are allowed at all; if Symvanta doesn't show up as an option, that's an admin-side allowlist, not a Symvanta-side block. ## What Copilot gets
Diagram of VS Code Copilot connecting to the Symvanta MCP endpoint over HTTPS with OAuth sign-in; the MCP serves the code graph of nodes and edges, and GitHub webhooks reindex the graph on every push
How Copilot reaches the graph: MCP over HTTPS to mcp.symvanta.com/mcp, with the graph reindexed on every push. Link to this diagram Open full size
Once the server is connected, agent mode can reach for any of these: | Tool | What it returns | |---|---| | `find_node` | A symbol's exact file, line range, and signature | | `relate` | Callers, dependencies, blast radius, implementers, or call chains for a symbol | | `ask_codebase` | A synthesized answer to a behavior question, with file citations | | `locate` | Text, semantic, or config-key search across every indexed repo | | `map` | An architecture skeleton for a repo or a specific module | | `find_http_route` | The handler behind an HTTP method and path | | `diff_impact` | What a branch or diff actually breaks downstream | | `list_tests_for` | The existing tests that already cover a symbol | | `estimate_scope` | A rough size estimate for a proposed change before anyone commits to it | | `ref` | Pin a feature branch, or overlay uncommitted working-tree edits, so the reads above reflect what you're actually looking at | That's 10 of the 25 tools the server exposes; `relate` alone covers five traversal modes (callers, dependencies, blast_radius, implementers, chain). Coverage spans TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby, so a polyglot repo gets one consistent graph across every language in it. ## Using it in agent mode The pattern that matters most is asking agent mode to check the graph before it edits. Take "rename this service method safely": tell agent mode to rename `processPayment` on `PaymentService` and update every call site. A prompt-only agent will grep for the name and hope it caught every call site, including the ones that go through an interface or a differently-named override. With Symvanta connected, a good agent-mode instruction is closer to: "Before renaming, call `relate` with `kind: callers` on `PaymentService.processPayment`, call `list_tests_for` on the same symbol, then make the rename and update every caller and test you found." The graph traversal happens in one tool call each, and the edit that follows is grounded in an actual call list instead of a text match. This is the same failure mode we walk through in more detail in [why AI coding agents fail on large codebases](/blog/why-ai-coding-agents-fail-large-codebases): a bigger context window doesn't tell the agent which files call which other files, and grep on a shared symbol name returns a mix of real call sites, comments, and unrelated matches with the same name. A graph turns "who calls this" into a direct lookup. For a broader look at how the graph itself is structured, module boundaries, dependency direction, the shape of a codebase before you start asking it questions, see the [architecture pages](/architecture), which walk several well-known open-source repos through their real module maps. ## Frequently asked questions
Does this replace Copilot's own codebase search? No, the two compose. Copilot's built-in retrieval and open-file context are still useful for fast, file-local edits. Point agent mode at a Symvanta tool specifically when the question is structural: who calls this, what breaks if it changes, what does this route actually handle. Both sit in the same tool picker, and the model chooses between them per turn.
Is my code used to train anything? No. Symvanta never uses customer code to train models. The graph (symbols and relationships) is what powers the MCP tools; source snippets are fetched on demand and discarded by default, never retained. If your plan includes the source storage add-on, that's an explicit opt-in, not the default behavior.
Should I use workspace config or user config? Use `.vscode/mcp.json` (workspace) when you want the setup checked into the repo so every teammate who opens the project gets the same server automatically. Use the user-level configuration, added via **MCP: Open User Configuration**, when you want Symvanta available across every workspace you personally open, regardless of which repo it is. Workspace config takes precedence for that project if both are defined.
What does this cost? The VS Code side is free: adding an MCP server costs nothing beyond a Copilot subscription you likely already have. Symvanta itself runs from $19/month on the Starter plan, with a 7-day trial on Pro if you want branch-aware indexing, cross-repo edges, and higher seat and repo limits before deciding.
--- ### Windsurf MCP Server Setup for Codebase Graphs https://symvanta.com/integrations/windsurf Cascade plans multi-step edits, and it plans them well when the context it retrieves is right. That context comes from Windsurf's local indexing and retrieval: a fast, useful default that still leaves Cascade guessing at structure once a codebase gets large or spans more than one repository. Grep and embeddings tell Cascade what looks related. They don't tell it what actually calls what, what a change breaks, or which consumer three repos away depends on the function it's about to edit. Symvanta adds the missing layer over MCP: a live code graph with exact callers, dependencies, blast radius, and cross-repo edges, alongside semantic and text search. Cascade keeps its own retrieval and planning; it just stops guessing at the parts a graph answers directly. Nothing here replaces Cascade's index or its planning loop; it gives that loop a second, more precise source to call when the question is "who calls this" rather than "what looks similar to this." ## Setting up the Symvanta MCP server in Windsurf 1. **Create a free Symvanta account** at [symvanta.com](https://symvanta.com/) and connect GitHub. 2. **Index the repos Cascade works in.** A webhook reindexes on every push, so the graph stays current without a manual trigger. Plans start at $19/month, and Pro (per-seat) ships with a 7-day trial. 3. **Add Symvanta as an MCP server.** Windsurf's remote-server config lives in `~/.codeium/windsurf/mcp_config.json`, and a remote HTTP entry uses a `serverUrl` field rather than the `command`/`args` pair used for local stdio servers: ```json { "mcpServers": { "symvanta": { "serverUrl": "https://mcp.symvanta.com/mcp" } } } ``` Prefer the UI: open the MCPs icon in the top right of the Cascade panel for the MCP Marketplace, or go to Settings > Cascade > MCP Servers and add a remote server with the same URL. 4. **Call any Symvanta tool.** The first call opens a browser window for OAuth login and hands Cascade a scoped token. There is no API key to generate or paste into the config, and no static header to manage. ## What Cascade gets
Diagram of Windsurf Cascade connecting to the Symvanta MCP endpoint over HTTPS with OAuth sign-in; the MCP serves the code graph of nodes and edges, and GitHub webhooks reindex the graph on every push
How Cascade reaches the graph: MCP over HTTPS to mcp.symvanta.com/mcp, with the graph reindexed on every push. Link to this diagram Open full size
Once the server is connected, Cascade has 25 tools available over MCP; the ones it reaches for most during an edit-heavy session are these: | Tool | What it gives Cascade | |---|---| | `find_node` | Exact symbol definition, file, and signature | | `relate` (callers / dependencies / blast_radius) | Real callers, what a symbol depends on, what breaks if it changes | | `ask_codebase` | Behavior questions ("how does X work") answered with cited files | | `locate` | Text, semantic, or config-key search across the repo | | `map` | Architecture skeleton of a repo or module before diving in | | `find_http_route` | The handler behind a route path and method | | `list_tests_for` | Existing tests that already cover a symbol | | `estimate_scope` | Rough sizing of a change before Cascade commits to a plan | | `ref` | Pin a feature branch, or overlay uncommitted edits so the graph sees WIP | The graph covers TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby, so a polyglot repo doesn't lose coverage at the language boundary. ## Blast radius before a multi-file edit Say Cascade is asked to change the signature of a shared validation helper used by three services. Left to its own retrieval, it greps the function name, finds a handful of call sites, edits them, and moves on, unaware of a fourth caller behind an interface or a consumer repo it never opened. With Symvanta wired in, Cascade calls `relate` with `kind: blast_radius` on the helper before touching anything. The result comes back as real edges: which functions call it directly, which route through an interface, and which repos outside the one Cascade has open depend on it. That's the same distinction we cover in [blast radius analysis](/blog/blast-radius-code-change-impact): a grep result is a list of matching lines, a blast radius is the actual graph of what depends on what. Cascade plans the edit against that list instead of the grep result, and the multi-file diff it proposes covers every real caller on the first pass. For an unfamiliar module, the same pattern runs in reverse: Cascade calls `map` first to get the module's skeleton, sees which files own which responsibilities, and only then drafts the plan it shows you, the way we walk real open-source repos through their own module maps on the [architecture pages](/architecture). Either direction, the graph call lands before the edit, while the fix is still cheap. ## Frequently asked questions
Does this replace Windsurf's local index? No. Windsurf's local indexing and retrieval stay exactly as they are. Symvanta adds a second source Cascade can call over MCP for the questions local retrieval can't answer well: exact callers, dependencies, blast radius, and cross-repo edges. The two compose rather than compete.
Is my code used to train anything? No. Symvanta parses your repository into a graph in memory and stores that graph, not a training corpus. Source code is discarded by default once parsing completes, and it is never used to train models.
Does Symvanta handle multi-repo codebases? Yes. A Symvanta project links multiple repositories together, and the graph includes cross-repo edges, so a `relate` call on a shared library returns consumers in every linked repo, including ones Cascade has never opened.
What does Symvanta cost? Starter is $19/month. Pro is $29 per seat per month with a 7-day trial. Enterprise is $99 per seat per month with a 15-seat minimum, for teams that need on-prem deployment or SSO. There's no separate charge for MCP access; it's included at every tier.
## Use cases ### Code Dependency Mapping and Architecture Maps https://symvanta.com/use-cases/dependency-mapping Every codebase has a shape nobody wrote down. Someone on the team knows that billing reaches into the user module through two helpers, and that one of those helpers is called from a webhook handler nobody has opened in a year. When that person is on holiday, everyone else greps. Dependency mapping is the work of making that shape explicit: which code calls which, how far a change travels, and where the seams actually sit. ## What dependency mapping across a codebase means The phrase covers two different jobs that get mixed up constantly. The first is package dependency mapping: your lockfile, the third-party libraries you pull in, the vulnerability report. Plenty of tools do that, and all of them stop at the package boundary. The second is dependency mapping inside code you wrote: this function calls that one, this class implements that interface, this HTTP route ends up in this handler, this service imports a type that lives in a different repository. The second job is the one that decides whether a change is safe to make. It is also the one that goes stale fastest, because it changes with every merge. A diagram drawn in a workshop last quarter describes a system that no longer exists. A map worth having gets rebuilt from the code on every index, so what you read is what compiles. ## Why folder structure and grep undersell it A folder tree tells you where files were filed. Directory layout follows team habits, historical refactors, and whoever set up the repo, and it says nothing about which module calls which at runtime. Two files sitting in the same directory can share no code path at all, while a single import can bind two directories at opposite ends of the tree. Grep gets closer, because it reads the code, then fails in both directions at once. It matches strings, so a search for `formatCurrency` returns comments, docs, a changelog entry, and a local variable that happens to share the name. And it misses real calls: when the call site goes through an interface method named `format` and the concrete implementation is `formatCurrency`, no string search will ever connect them. You get noise to read and calls you cannot see, from the same query. In a second repository, the search does not run at all. ## What the graph stores Symvanta parses each repository into nodes and edges. A node is a symbol: a function, class, interface, struct, enum, HTTP endpoint, or test case. An edge is a relationship the parser saw, such as `calls`, `imports`, `contains`, `references`, plus type-hierarchy edges that keep each language's own vocabulary (`extends` in TypeScript, `implements` in Java, `conforms_to` in Swift, `impl_trait` in Rust, `embeds_interface` in Go, `uses_trait` in PHP, `include` in Ruby). Eleven languages parse today: TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby. Edges carry a confidence tier, and the tier ships with the answer. High means a compiler-grade index resolved the symbol. Medium is a framework or heuristic match. Low is a string heuristic. Correlational means the two symbols keep changing together in git history, which is a lead worth checking and nothing stronger. A map that hides its own uncertainty invites you to trust the weakest line on it. Once the edges exist, the questions get cheap. The callers of a symbol are the edges pointing at it. Its dependencies are the edges leaving it. Blast radius is a recursive walk up the caller edges, capped so the answer comes back inside one tool call, which turns "what breaks if I change this" into a list of symbols and files. That walk crosses repository boundaries when a project holds more than one repo, which is exactly where a hand-drawn map gives up. We wrote the long version of that argument in [what breaks when you change a function](/blog/what-breaks-if-i-change-this-function) and [blast radius analysis](/blog/blast-radius-code-change-impact). ## From symbols to an architecture map Symbol-level edges answer local questions well and drown you at repository scale. A million-edge graph printed as a graph is a hairball. So the same edges get clustered: Louvain community detection groups symbols into modules by how densely they call each other, and PageRank picks out the hub symbol inside each module. What comes back is a map at the altitude people actually reason about, a few dozen functional modules with call-weighted arrows between them, derived from call behavior and frequently disagreeing with the directory layout. You can read the output before indexing anything. The [architecture library](/architecture) runs this over public repositories and publishes the result: prisma/prisma resolves into 66 modules at modularity Q=0.84, with a module map diagram, the load-bearing symbols to read first, and the commit each snapshot came from. Same pipeline, same clustering, on repositories you can go check by hand. ## The map is built for an agent to query Codebase architecture visualization usually means a picture for a human. A picture is useful once a quarter. The reader that needs this map every few minutes is the coding agent in your editor, and it cannot read a PNG. Symvanta gives your AI coding agent your codebase's real call graph over MCP, so it stops guessing and knows what breaks before it edits. Every part of the map is a tool call: resolve a symbol to its file and signature, list its callers or dependencies, ask what implements an interface, trace the path between two symbols, look up an HTTP route by method and path, pull the module map, or ask a behavior question and get an answer with citations. Claude Code, Cursor, Windsurf, Codex, Zed, and any other MCP client call the same tools. That changes the ordering of a session. The agent asks for the blast radius before it writes the edit, so a rename that reaches fourteen call sites across three repositories shows up as a list at planning time instead of a red build twenty minutes later. Reads follow tracked feature branches, and uncommitted working-tree edits can be overlaid on the graph, so the map matches the code in front of you. ## Where a dependency map earns its keep Onboarding, where a new engineer gets the module map and the hub symbols in each module instead of a two-week tour. Refactors, where the argument about scope gets settled by a caller list. Incidents, where the question is which callers reach the failing function. Reviews of agent-written diffs, where the reviewer needs to know what the agent did not look at. Each one is the same query at a different altitude. Point Symvanta at a repository and the first index gives you the symbol graph, the callers, the blast radius, and the module map, over MCP and in the dashboard. Book a walkthrough and we will run it against your codebase, on a call, with your own repositories. ## Links - Home: - Blog: - Architecture: - Architecture data: every architecture page ships a machine-readable JSON companion at `https://symvanta.com/architecture//data.json` - Integrations: - Pricing: - Book a demo: - Security: - Team: - Concise index: - Terms of Service: - Privacy Policy: ## Sign up Create a free account at to index your first repository.