AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
duckgrep는 코드베이스를 실시간으로 질의해 구조적 질문을 한 번에 답합니다
Grep finds strings. duckgrep answers questions. Give your coding agent a live, queryable model of your codebase, so structural questions take one query instead of a search loop.
개발 도구Python · HTMLGitHub @ccf온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Grep finds strings. duckgrep answers questions.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Grep finds strings. duckgrep answers questions.
Give your coding agent a live, queryable model of your codebase (definitions, callers, imports, history), so structural questions take one query instead of a search loop.
We measured duckgrep with Claude Code (Sonnet 5.5) on 494 structural questions from 25 open-source Python and Rust repos: who calls this, what reaches it through one more caller, what imports this module. In every setup the model and tools were the same; the only difference was the MCP server. Each agent also had the same one-line instruction to use its tool.README 전문 보기 ↗Grep finds strings. duckgrep answers questions.
Give your coding agent a live, queryable model of your codebase (definitions, callers, imports, history), so structural questions take one query instead of a search loop.
We measured duckgrep with Claude Code (Sonnet 5.5) on 494 structural questions from 25 open-source Python and Rust repos: who calls this, what reaches it through one more caller, what imports this module. In every setup the model and tools were the same; the only difference was the MCP server. Each agent also had the same one-line instruction to use its tool.
| Per question | Claude Code alone | + Serena | + duckgrep |
|---|---|---|---|
| Python (237 questions) | |||
| Tool calls | 3.0 | 2.8 (−5%)† | 2.3 (−18%) |
| Round trips to the model | 3.6 | 2.9 (−16%) | 2.8 (−19%) |
| Tokens | 27.4k | 33.6k (+23%) | 22.5k (−18%) |
| Cost | $0.0267 | $0.0255 (−4%)† | $0.0203 (−24%) |
| Correct answers | 94% | 95% | 96% |
| Rust (257 questions) | |||
| Tool calls | 3.3 | 3.9 (+14%) | 2.6 (−16%) |
| Round trips to the model | 3.7 | 3.8 (+2%)† | 3.1 (−12%) |
| Tokens | 29.4k | 45.9k (+56%) | 26.0k (−12%) |
| Cost | $0.0299 | $0.0336 (+12%) | $0.0226 (−25%) |
| Correct answers | 96% | 96% | 96% |
12–19% less work and a quarter less cost, with answers just as accurate.
Where it doesn't help: finding where to fix a bug from an issue report (230 SWE-bench tasks). The issue usually names something one grep finds, so there's nothing to save. Carrying the tool's description added 11–13% tokens on Python; they're cached, so cost stayed flat.
3,620 runs in all, made on 2026-10-02 with Claude Code 2.1.287. Changes compare each setup with Claude Code alone, task by task, with tool calls and round trips compared as 1 + n. † marks a difference that doesn't hold after correcting for all 160 comparisons. Every duckgrep change in calls, round trips, tokens and cost holds; the correct-answer rates differ by no more than chance in any setup. Full tables, the method and the harness are in bench/RESULTS.md and bench/eval.
Coding agents find their way around a codebase with chains of grep → read file → grep again. That works for "where is this string." But the questions that matter when changing code are structural: who calls this, what does it call, what breaks if I change it, what's dead. Each one turns into a loop of searches, file reads and guesses, paid for in turns and tokens.
get_or_create and the function it sits in took 22 calls (one grep, then 21 file reads to find the enclosing functions) and 58 KB of output. duckgrep answered it in one query and 9 KB.pricing/ with no callers outside their module" isn't a feature anyone built; the agent just writes the query.duckgrep parses the repo with tree-sitter into a single DuckDB file and exposes it as one read-only query(sql) MCP tool. The tool's description carries the whole schema, so the agent can write a correct query on its first call.
SELECT * FROM callers('Session.request') WHERE resolution <> 'name';
-- everything that reaches a function within 3 hops
WITH RECURSIVE up(p, q, d) AS (
SELECT path, qualname, 0 FROM symbols WHERE qualname = 'SQLCompiler.execute_sql'
UNION SELECT e.src_path, e.src_scope, d + 1 FROM edges e JOIN up ON e.dst_path = up.p AND e.dst_qualname = up.q
WHERE d < 3 AND e.resolution NOT IN ('name', 'ambiguous', 'unresolved') AND e.src_scope <> '')
SELECT p, q, min(d) FROM up GROUP BY p, q;
-- hottest files by churn that define classes
SELECT * FROM file_churn WHERE path IN (SELECT path FROM symbols WHERE kind = 'class')
ORDER BY n_commits DESC LIMIT 10;
Languages: Python, TypeScript/TSX, JavaScript, Go, Rust. Every other text file is still indexed for grep(). Built on DuckDB and tree-sitter.
uv tool install duckgrep
claude mcp add duckgrep -- duckgrep mcp
The server answers at once and builds the index in the background; the first call waits for it. After that, every call runs an incremental refresh, so results reflect the agent's latest edits with no hooks.
Agents reach for grep out of habit. Add this to your CLAUDE.md (or your agent's instructions). It's the line the numbers above were measured with:
The repository in the current directory is indexed by duckgrep. To find code, call its query tool before Grep, Glob or Read: SELECT * FROM defs('name') finds where a symbol is defined, callers('name') who calls it, and grep('regex') searches the text and names each match's enclosing function. Read a file once you know where to look.
-C DIR sets the repo root, used as given.
duckgrep index # build / update (.duckgrep/index.duckdb, self-gitignored)
duckgrep def Session # where defined
duckgrep callers Session.request # who references it, with the calling function
duckgrep callees Session.request # what it references
duckgrep outline sessions.py # symbols in a file
duckgrep grep '(?i)retry' # regex + enclosing symbol
duckgrep source Session.request # the code
duckgrep q "SELECT ..." # anything
duckgrep schema # what the agent sees
| table | what |
|---|---|
files | path, lang, size, sha, n_lines, skipped, parse_errors |
symbols | name, qualname (Class.method, GoType.Method), kind, parent, lines, signature, first doc line, exported |
refs | every identifier use: kind (call/attr/write/type/inherit/decorator/import/...), receiver text, enclosing scope |
imports / imports_resolved | as written, plus the repo file each resolves to |
edges | reference → definition (the call graph), with a resolution tier |
lines | full text, for grep() |
commits, file_changes, file_churn | git history |
Table macros: defs, callers, callees, outline, grep, source. duckgrep schema prints exactly what the agent sees.
Tree-sitter gives syntax, not types, so every edge says how it was resolved:
Confident: self, local, package (Go), import, module and qualified. These come from scope and import analysis:
go.mod prefixes and Cargo crate names;__init__.py, export … from, pub use), and up to three named or star (from .x import *) hops in Python.qualified (Class.method, Type::method) requires the class to be defined or imported in the calling file.
typed (confident, Python for now): obj.method() where the receiver's class can be read from syntax. That covers:
x = Foo() and Foo(...).m();p: Foo, or -> Foo one call away), or an unannotated function whose every return gives the same class;self.attr set or annotated in the class, one more attribute hop (bot.exchange.m(), User.objects.m(), mod.hook.m()), and class-body aliases (form_class = Foo, then self.form_class());x = h.m() on a typed h;self, super() and super(C, self) through base classes in other files.A name bound more than once, or to something untypable, gets no inference. The edge goes to the method on that class or its nearest in-repo base; a subclass that overrides it isn't followed, so callers('Sub.m') misses calls typed as the base. A receiver whose class is outside the repo (io.StringIO(), a literal) is unresolved, not a name guess.
name: obj.method() on a receiver of unknown type, matched by method name only. Kept as one row per candidate when there are ≤10 candidates.
ambiguous: more than 10 candidates, or a method that builtin types also have (get, append, push, …). The target is left NULL rather than guessed. ends with a row counting these calls.
A bare name (helper()) resolves only through its file's scope and imports. It never matches another file's definition by name alone; Rust macros and .d.ts declarations are the exceptions.
Measured against jedi's goto-definition on sampled calls in Python repos (bench/RESULTS.md):
| repo | calls with an in-repo target | resolved confidently | precision |
|---|---|---|---|
| django | 1,554 | 95.3% | 100% |
| freqtrade | 1,598 | 95.4% | 100% |
| requests | 137 | 95.6% | 100% |
The rest fall to name, ambiguous or unresolved, which say so rather than guess. What remains are receivers syntax can't type (unannotated parameters, loop variables, longer chains, and runtime-built APIs such as Django's managers); see the roadmap.
Yes. freshen() runs before every query: one git ls-files, a stat per file, then a content hash only for files whose size or mtime changed. Changed files have their rows swapped in one transaction.
edges/callers/callees.go.mod/Cargo.toml renames.The root is -C DIR, else $DUCKGREP_ROOT (for the MCP server), else the nearest directory above the working directory with .git or .duckgrep. Outside a repository, duckgrep refuses to start; it never indexes a directory by guess. The index is one DuckDB file, <root>/.duckgrep/index.duckdb, which ignores itself in git.
On django (7k files):
On vscode (19.5k files, 6.5M refs), typical queries take 80–370 ms, and the full initial index takes about 2.5 minutes on 2 cores.
duckgrep pays off when the question is about how code connects.
The harness, the suites and every number are in the repo: bench/eval and bench/RESULTS.md.
Apache License 2.0. See LICENSE.
clonecallers('Class.method')unresolved: no in-repo target found. That covers stdlib, builtins, third-party code, calls on a receiver bound to an external import (json.dumps), and imports duckgrep can't follow.