<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Oper18</title>
    <description>The latest articles on DEV Community by Oper18 (@oper18).</description>
    <link>https://dev.to/oper18</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1377096%2F22e1afdd-c793-47e8-91d1-c60bbe8ae482.png</url>
      <title>DEV Community: Oper18</title>
      <link>https://dev.to/oper18</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oper18"/>
    <language>en</language>
    <item>
      <title>Giving Coding Agents a Map: Symbol-Level RAG With tree-sitter, Qdrant and SQLite</title>
      <dc:creator>Oper18</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:04:29 +0000</pubDate>
      <link>https://dev.to/oper18/giving-coding-agents-a-map-symbol-level-rag-with-tree-sitter-qdrant-and-sqlite-3ae5</link>
      <guid>https://dev.to/oper18/giving-coding-agents-a-map-symbol-level-rag-with-tree-sitter-qdrant-and-sqlite-3ae5</guid>
      <description>&lt;h2&gt;
  
  
  Your coding agent doesn’t know your codebase. That’s the actual bug.
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7hofwuelxww4c9qo2hwv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7hofwuelxww4c9qo2hwv.png" alt=" " width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  An agent without context doesn’t write bad code. It writes someone else’s code.
&lt;/h3&gt;

&lt;p&gt;You ask for a new endpoint and get a fresh error-handling pattern, a second HTTP client, validation inlined in the handler when the whole repo does it in a service, a raw query in a project that’s been behind a repository interface for two years. It compiles. It passes review only if nobody’s paying attention. And every one of those diffs makes the next generation slightly worse, because now there are two patterns in the repo instead of one.&lt;br&gt;
Fewer tokens aren’t the problem I care about. The problem is that the agent has no idea how this project does things.&lt;br&gt;
So I built nav — a Go CLI (~10k LOC, GPL-3.0) that indexes a repository into semantic code units and feeds the relevant ones to the agent before it writes a line of code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Symbols, not chunks
&lt;/h3&gt;

&lt;p&gt;Go, Python, TypeScript, and JavaScript. tree-sitter parses each file and extracts real definitions using per-language queries: in Go, that’s functions, methods with their receiver, structs, interfaces, and consts; in Python, functions and classes; in TS/JS, functions, methods, classes, and interfaces. Symbols below a minimum line count are dropped.&lt;br&gt;
No sliding windows. A retrieved unit is always a complete definition the agent can imitate, not the back half of one function glued to the front of the next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgaxdd7f448k47oa2yuzj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgaxdd7f448k47oa2yuzj.png" alt=" " width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Enrichment, grounded in the project
&lt;/h3&gt;

&lt;p&gt;Each symbol goes to an LLM (qwen3-coder by default, with fallbacks on rate limit) for four things: a dense summary, 3–6 tags, a one-sentence business context—the why, not the implementation—and 1–4 responsibilities.&lt;br&gt;
Here’s the detail that matters: before summarising anything, a full index generates a project-level README from the source, then passes it as shared context into every per-symbol prompt.&lt;br&gt;
So the summary for create_user is written by a model that already knows what the product is. You get “registers a tenant admin during onboarding” instead of “creates a user.”&lt;br&gt;
Everything structural stays deterministic. Calls and imports come from the AST. called_by is a post-index pass. layer (controller / service / repository / model / middleware / util) and framework come from path and import heuristics. The LLM writes the prose; it never touches the graph.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Felzvx943y4k4vcsdemlc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Felzvx943y4k4vcsdemlc.png" alt=" " width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What actually gets embedded
&lt;/h3&gt;

&lt;p&gt;Not the raw source, but a rendered block: symbol, type, module, signature, layer, framework, purpose, business context, responsibilities, imports, calls, called_by, and then the code.&lt;br&gt;
That’s what makes “where do we hash passwords” return UserService.create_user rather than every function with password in it.&lt;br&gt;
It’s also why the retrieved context carries the architecture with it: the agent sees the layer, the callers, and the dependencies in the same block as the code.&lt;br&gt;
Vectors and the payload live in Qdrant, one collection per project, with filtered search—nav search "auth" --type method --layer service --threshold 0.70.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQLite does the bookkeeping
&lt;/h3&gt;

&lt;p&gt;Alongside Qdrant, there’s a per-branch SQLite database at .nav/nav-.db holding three things: a chunk manifest (content_hash vs embedded_hash), the knowledge graph as nodes and edges, and metadata.&lt;br&gt;
The manifest means re-embedding only happens when a symbol’s content actually changes. Reformat a file, pay nothing.&lt;br&gt;
The graph holds pkg:, file:, and sym: nodes with defines, calls, and imports edges, plus Go-specific embeds and implements heuristics. It answers nav graph callers, nav graph deps, and top fan-in queries without touching the vector store.&lt;br&gt;
Per-branch is deliberate—the set of symbols genuinely differs across branches. A new branch records its parent and inherits the ancestor’s embeddings through a resolved branch chain, so creating a feature branch doesn’t trigger a reindex.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two injection points
&lt;/h3&gt;

&lt;p&gt;SessionStart hands the agent a digest built directly from the graph: every package, every file, symbol counts, capped at ~8k tokens. It stops the session from opening with find, ls, and tree.&lt;br&gt;
UserPromptSubmit runs a lazy sync in-process, embeds the prompt, retrieves the top five results above a 0.72 cosine threshold, and injects a  block capped at 4k tokens.&lt;br&gt;
Sync runs behind a file lock with a four-second wait. If another sync already holds the lock, the hook answers against a slightly stale index instead of making you wait.&lt;br&gt;
The same core runs for Claude Code, Qwen Code, Cursor, and OpenCode. A Git pre-commit hook keeps the index warm and always exits with 0, so it can never block a commit.&lt;br&gt;
&lt;strong&gt;A stale index means yesterday’s conventions.&lt;/strong&gt; That’s exactly why sync happens on the prompt path rather than on a cron schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  No numbers yet
&lt;/h2&gt;

&lt;p&gt;Gen 1 &lt;a href="https://github.com/Oper18/nav-cli" rel="noopener noreferrer"&gt;github&lt;/a&gt;.&lt;br&gt;
I’m not going to invent a “40% fewer tokens” figure — that isn’t even the metric.&lt;br&gt;
What I’m building an evaluation for is conformance: does the generated code use the abstractions that already exist in the repo, or reinvent them?&lt;br&gt;
Part 2 is where I’ll share those results, including the cases where nav didn’t help.&lt;br&gt;
If you’re running agents against a mature codebase, what are you doing to keep them aligned with your project’s conventions? CLAUDE.md only scales so far.&lt;/p&gt;

</description>
      <category>go</category>
      <category>llm</category>
      <category>rag</category>
      <category>cli</category>
    </item>
  </channel>
</rss>
