Documentation
Everything sciogen does, and why.
sciogen builds a queryable knowledge graph over a codebase so a coding agent reads structure instead of raw files. These docs cover the whole surface — from pip install to the design decisions inside the pipeline.
Introduction #
sciogen is a static analysis and indexing tool. It parses a codebase once, resolves every reference to the exact definition it points to, and materializes the result into an embedded triple store. After that, questions like "who calls AuthService.login?" or "what breaks if I change find_by_email?" are sub-second index lookups that return typed records with exact file:line locations.
sciogen is to codebases what Elasticsearch is to documents, or a database index is to rows: precomputed, queryable structure. It never calls an LLM, never counts tokens, and has no concept of a context window. The token savings for agents are a consequence of what it returns — structure instead of source.
Why files are the wrong granularity
A coding agent understands code by reading files — and to learn one function's callers it pulls thousands of lines into its context window, burning tokens on noise. sciogen precomputes the call graph, the dependency graph, the type hierarchy, and semantic embeddings, so the same answer costs a handful of typed records.
You run sciogen index . once. The graph is written to a .sciogen/ directory inside the repo. From then on, an agent working in that repo queries the graph — over MCP or the CLI — and opens only the files the graph points it to.
Two phases, no ML routing
Indexing turns source into a typed, confidence-scored graph plus multi-granularity embeddings. Querying answers questions deterministically — which store answers is fixed by the method called, never by a model: structural queries hit KuzuDB, semantic queries hit ChromaDB, hybrid uses vector hits as seeds for graph expansion.
Getting started #
Install
$ pip install sciogenThat's the full indexer, query engine, CLI, MCP server, and explore GUI. Semantic search works out of the box on a deterministic hashing embedder (keyword-level quality, zero downloads). For code-optimized semantic search, add the embeddings extra — one larger local model download, nothing leaves your machine:
$ pip install "sciogen[embeddings]"pip puts the sciogen command in your Python installation's scripts folder, and on some setups that folder isn't on PATH — most commonly Windows with the Microsoft Store Python, or pip install --user on Linux/macOS. pip prints a warning about this during install, but it's easy to miss. Any one of these three fixes works.
1Run it through Python — no setup needed, works everywhere:
$ python -m sciogen index .2Add the scripts folder to PATH — one-time. On Windows, in PowerShell:
# 1. Print the folder pip installed the command into: > python -c "import sysconfig, os; print(sysconfig.get_path('scripts', os.name + '_user'))" # 2. Add it to your user PATH (paste the folder from step 1 in place of <folder>): > [Environment]::SetEnvironmentVariable('Path', "$([Environment]::GetEnvironmentVariable('Path','User'));<folder>", 'User') # 3. Open a NEW terminal — existing windows keep the old PATH.
On Linux/macOS, user installs land in ~/.local/bin:
$ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc
3Install with pipx instead — pipx exists precisely for CLI tools: it isolates the install and manages PATH for you.
$ pipx install sciogenIndex a project
$ sciogen index . ✓ Scanning project structure... ✓ Reading 143 files... ✓ Parsing Python, TypeScript... ✓ Building knowledge graph... ✓ Embedding 1,204 symbols... ✓ Linking dependencies... ✓ Done! 1,204 nodes · 3,456 edges · indexed in 4.2s Ready. Your codebase is now queryable.
The index lives in .sciogen/ at the project root — add it to your .gitignore, it's machine-local and regenerable. Re-run any time: it's incremental. Unchanged files are skipped via a stat + SHA256 differ, so a no-op re-index is effectively instant and a single-file change takes under a second.
Explore it visually
$ sciogen exploreOpens the knowledge graph in your browser from a single self-contained HTML file — no server, no port. The view starts collapsed to file nodes; click a file to reveal its classes and functions, click a function to reveal its call edges. Node color encodes type, node size encodes in-degree (hotspots), edge dash style encodes confidence. It's a snapshot — re-run sciogen index then sciogen explore after changes.
Query from the terminal
$ sciogen search "password hashing" # semantic search $ sciogen callers AuthService.login # who calls this? $ sciogen impact UserModel.find_by_email # what breaks if this changes? $ sciogen deps src/auth/service.py # imports + transitive deps
Using with coding agents #
sciogen never calls an LLM. The LLM calls sciogen. There is no code path anywhere in sciogen that sends a prompt to a model — it's a hard design rule. "Connecting to an LLM" means an agent reaches in to sciogen and reads the graph. sciogen is a tool the model uses, not a model itself.
Three parties, not two
The piece people miss is the host in the middle. The host (Claude Code, Cursor, Cline, Claude Desktop…) is what actually talks to the LLM — it holds the API key and routes the model's tool-call requests. sciogen is only a tool server: it answers graph queries and returns JSON. It has no idea which model is on the other side, which makes it LLM-agnostic by construction.
What happens in one turn
You ask your agent: "is it safe to change hash_password?"
1. Host sends the model your message + sciogen's tool list 2. Model decides to call analyze_impact("hash_password") 3. Host routes the request to the sciogen mcp subprocess 4. sciogen queries .sciogen/ → JSON: impacted symbols, hops, confidence 5. Host feeds that JSON back to the model as the tool result 6. Model answers — having read ~200 tokens of structure, not 3 files
Steps 3–4 are the only ones sciogen takes part in. Everything about the model is the host's job.
Path 1 — MCP (structured)
Register the server once; the host launches it and calls typed tools that return JSON. For Claude Code, one command per project:
$ claude mcp add sciogen -- sciogen mcp .Or a .mcp.json in the repo root — shareable with your team; Claude Code, Cursor, and Windsurf all read this shape:
{
"mcpServers": {
"sciogen": {
"command": "sciogen",
"args": ["mcp", "."]
}
}
}Path 2 — CLI (zero-config)
If the agent can run shell commands (Claude Code can, out of the box), it needs no setup at all — it runs a command and reads the table from stdout:
$ sciogen callers AuthService.login --depth 2 hops conf via symbol location 1 0.90 CALLS AuthService.check_pass… src/auth/service.py:22 2 0.40 CALLS handle_login src/app.py:4
MCP vs CLI: MCP is structured — typed JSON, formal tool schemas, best when the host supports it. The CLI is universal — any agent with a shell. They expose the same query engine; pick whichever your agent can reach.
Autonomous use — one command
An agent reaches for a tool on its own when three things are true: the tool says when to use itself, the project's agent memory says to prefer it, and the data never goes stale. setup-agent wires all three:
$ sciogen setup-agent . --hooks ✓ .mcp.json — sciogen MCP server registered ✓ CLAUDE.md — autonomous-usage rules added ✓ .claude/settings.json — re-index hook installed
.mcp.jsonregisters the server — Claude Code, Cursor, and Windsurf read it automatically; commit it to share with your team.CLAUDE.mdgets a marker-guarded rules block the agent reads every session: query the graph before modifying a symbol, usesearchinstead of grep, open only thefile:linelocations results point to. Existing content is preserved.--hooksinstalls a Claude Code hook that runssciogen index . --quietafter every file edit — the graph refreshes itself, sub-second, so the agent never queries stale structure.
The tool descriptions themselves are trigger-oriented too — analyze_impact announces "use BEFORE any rename or refactor", get_context_for_task says "START HERE for any broad task". The result: you give the agent a task, it finds the right files through the graph, checks impact before touching anything, edits, and the hook re-indexes behind it. No human in the loop for any of it.
MCP tools #
Eight tools, each a thin wrapper over the query engine — the MCP layer contains no logic of its own. Every tool returns JSON where node references carry exact file / line_start / line_end, so the agent reads source only when it actually needs to.
| Tool | What it answers |
|---|---|
index_codebase(path) | Build or incrementally update the graph |
get_symbol(name, file?) | Typed node info for a named symbol + its immediate relations |
get_callers(symbol, depth?, min_confidence?) | Who calls this, up to N hops, with path confidence |
get_dependencies(file) | Direct, transitive, and external imports of a file |
analyze_impact(symbol, max_depth?, min_confidence?) | Full blast radius: callers, subclasses, tests, mutators |
search(query, mode?, granularity?) | Semantic or hybrid search over local embeddings |
get_diff_impact(unified_diff) | Union impact graph of every symbol a diff touches |
get_context_for_task(description) | Broad intent → curated subgraph of relevant files + symbols |
Architecture #
Indexing runs five stages per file. Only the normalizer knows languages; everything downstream — resolver, graph builder, query engine, MCP — is language-agnostic. And only changed files are ever re-processed.
The full system, interactive
Everything on one map — the indexing pipeline, the triple store, and the query side. Click any component to see what it does, or play a flow to watch how data moves.
The buttons above animate how data moves: indexing writes the three stores; each query mode reads them differently.
1 — Discovery + two-tier differ
One walk over the project tree, pruning ignored directories. Change detection is two-tier because it's the hot path of re-indexing: first a stat fast path — if a file's (size, mtime) signature matches what SQLite recorded, the file is never even opened. Then SHA256 truth — only stat-changed files are read and hashed; if the hash still matches (a touch, a branch switch that restored content), the file is not re-parsed. mtime is never trusted to declare a file changed, only to prove it unchanged — which is what makes this correct in CI/CD.
2 — AST normalizer
Converts language-specific tree-sitter ASTs into a universal IR: the same ClassNode, FunctionNode, call sites, imports, and mutations regardless of source language. This is what makes multi-language support composable — adding a language is one normalizer class; unsupported languages fall back to a generic normalizer that still yields a searchable file node.
3 — Symbol resolver
Tree-sitter gives syntax, not semantics — without this step a CALLS edge is a string, not a pointer. The resolver builds a scope table from each file's imports, then traces every reference to the exact definition: a call to login() resolves to src/auth/service.py:AuthService.login, not the string "login". References that can't be statically proven (dynamic dispatch, injected dependencies) still produce edges — with a low confidence score rather than being silently dropped.
4 — Graph builder
Materializes resolved nodes and typed edges into KuzuDB. Confidence is never filtered at write time: the graph stores everything, and callers set their own threshold at query time.
5 — Embeddings
Alongside the graph, ChromaDB holds embeddings at four granularities — file, class, function, chunk — because no single granularity answers all queries well: broad "how is auth structured?" needs file vectors; precise "the block that rate-limits login" needs chunk vectors. What gets embedded is always a normalized summary (name + file + params + docstring + call list), never raw source — raw code embeds noise.
Technology choices #
Every component, what it does, and why it was chosen over the alternatives. The through-line: everything is embedded — no servers, no ports, no long-running processes anywhere.
| Component | What it does here | Why this one |
|---|---|---|
| tree-sitter | Parses source into ASTs — stage 1 of the pipeline, used on every indexed file. | One API over 100+ language grammars, incremental and error-tolerant. The alternative — a parser per language — makes multi-language support a rewrite each time. |
| KuzuDB | Stores graph topology: nodes, typed edges, confidence scores. Answers all structural queries (callers, impact, deps) via Cypher. | Embedded graph database — no server to run, full Cypher support. A Neo4j-style server would be the only stateful process in an otherwise serverless design. |
| ChromaDB | Stores vector embeddings at four granularities; answers semantic and hybrid search by cosine similarity. | Embedded vector store with persistent collections. Same rationale: no service to operate, works offline. |
| SQLite | Operational metadata: file hashes for the differ, the symbol table, per-file reference records, schema version + migrations, parse errors. | The canonical embedded store for relational bookkeeping. The reference records it keeps are what allow re-resolving a dependent file without re-parsing it. |
| nomic-embed-code | The default embedder when sciogen[embeddings] is installed — turns symbol summaries into vectors, fully locally. | Local, free, code-optimized. Nothing leaves the machine. Without it, a deterministic hashing embedder keeps everything working with zero downloads. |
| MCP | The protocol agents use to call sciogen's eight query tools. | The emerging standard for agent↔tool connections — Claude Code, Cursor, Cline all speak it. The MCP layer is a thin adapter; all logic lives in the query engine. |
| Typer + Rich | CLI command routing and all terminal output — banner, spinner progress, result tables. | Typer gives typed commands with almost no boilerplate; Rich renders in-place progress and tables an agent can also read as plain text. |
| Cytoscape.js | Renders the explore GUI inside one generated, self-contained HTML file. | Built-in force-directed and hierarchical layouts, and native styling by data attributes — node type and edge confidence map straight onto visuals with no custom code. No server needed. |
| watchdog | Optional dev-mode file watching. | Explicitly not the staleness mechanism — the SHA256 differ is, because it's the one that works correctly in CI/CD. The watcher is opt-in convenience only. |
Performance design #
The contract: first index fast, re-index near-instant, queries sub-second. Measured on sciogen's own source: first index 25.4s, no-op re-index 0.2s.
- Parallel parsing. Parsing + normalization is CPU-bound and per-file independent, so large batches fan out across cores. Small batches stay serial — pool startup costs more than parsing a handful of files.
- Two-tier differ. Stat signature short-circuit before any file is opened; SHA256 as the source of truth.
- Batched transactional writes. Prepared statements inside one explicit transaction per stage — not one auto-commit round trip per row.
- Embedding deduplication. Embedding is the slowest stage by an order of magnitude. Each node's summary is content-hashed; only changed summaries are re-embedded. A whitespace refactor doesn't re-embed the file.
- Incremental re-resolution. When file A changes, files importing A are re-resolved from reference records persisted in SQLite — without re-parsing them. Renaming a function in A correctly rewires call edges out of an untouched B.
- Query cache keyed by index version. Every index run bumps a counter; one bump invalidates every cached result, and repeated agent queries on an unchanged index are dictionary lookups.
- Lazy imports. The heavy stores load only when first touched, so CLI startup stays instant.
Graph schema & confidence #
Node kinds
| Kind | Key properties | Example id |
|---|---|---|
FileNode | path, language, size, last_modified | src/auth/service.py |
ClassNode | name, file, extends, implements, docstring | src/auth/service.py:AuthService |
FunctionNode | name, params, return_type, line_range, complexity | src/auth/service.py:AuthService.login |
TypeNode | name, file, fields | src/models/user.py:UserPayload |
ChunkNode | parent function, line_range, embedding_text | …:AuthService.login#2 |
ModuleNode | name, package | module:hashlib |
ExternalNode | name (unresolved target) | external:requests.post |
Edge kinds
| Edge | Meaning | Typical confidence |
|---|---|---|
IMPORTS | file → file/module it imports | 1.0 |
DEFINES | file/class → symbol it defines | 1.0 |
CALLS | function → function it calls | 1.0 direct · 0.8–0.9 via import · 0.2–0.5 dynamic |
EXTENDS / IMPLEMENTS | class → base / interface | 1.0 local · 0.9 imported |
MUTATES | function → state it mutates | 0.7–0.9 |
TEST_FOR | test → symbol under test | 1.0 if naming matches · 0.6 otherwise |
PART_OF | chunk → parent function | 1.0 |
The confidence model
Confidence exists because static analysis is incomplete, not wrong. Dropping unprovable edges would fabricate a misleadingly clean graph — so sciogen keeps them and scores them, and you choose the cutoff at query time.
On multi-hop traversals sciogen reports path confidence — the minimum edge confidence along the best path, because a chain is only as trustworthy as its weakest link: min(0.9, 0.4, 1.0) → 0.4.
CLI reference #
| Command | Does | Key flags |
|---|---|---|
sciogen / sciogen index [path] | Build or incrementally update the graph (banner + progress + summary) | --rebuild |
sciogen search <query> | Semantic or hybrid search | --mode · --granularity · --top |
sciogen callers <symbol> | Who calls a symbol, up to N hops | --depth · --min-confidence |
sciogen impact <symbol> | Blast radius: callers, subclasses, tests, mutators | --depth · --min-confidence |
sciogen deps <file> | Direct, transitive, external imports | |
sciogen symbol <name> | Typed node info + relations | --file |
sciogen explore [path] | Interactive graph GUI in the browser | |
sciogen mcp [path] | MCP server over stdio for agents |
Only index shows the banner and progress flow; query commands print their result and nothing else. Files that fail to parse are skipped with a single ⚠ line and logged — indexing never stops for one bad file.
Configuration #
Everything has working defaults; environment variables override:
| Variable | Effect |
|---|---|
SCIOGEN_EMBEDDER | nomic (default) or hash — force the deterministic embedder |
SCIOGEN_WORKERS | Parse worker processes; unset = auto per CPU, 1 = serial |
SCIOGEN_DATA_DIR | Override the index location (default <project>/.sciogen) |
Contributing #
sciogen is open source and contributions are welcome — bug reports, language-normalizer improvements, docs, and features all move it forward. The complete guide lives in CONTRIBUTING.md; the essentials are below.
Set up a dev environment
sciogen targets Python 3.10+ and builds with hatchling. Install it editable (-e) so source edits — including the version and CLI banner — take effect immediately, with no reinstall:
$ git clone https://github.com/ayanbag/sciogen $ cd sciogen $ pip install -e ".[dev,embeddings]"
Then confirm your environment is live — both must print the same version; if they disagree you're running a stale install:
$ python -c "import sciogen; print(sciogen.__version__)" $ python -m sciogen --version
Run the tests
Every change should keep the suite green. New behavior needs a test; a bug fix should add a regression test that fails before and passes after.
$ pytest # full suite $ pytest -p no:randomly # deterministic order
sciogen is a static index, not a generative tool. A few constraints are non-negotiable, and a PR that breaks one will be asked to change: no LLM calls anywhere; no token counting or context-window logic; query methods return typed nodes, never raw source; the pipeline writes every edge and confidence is filtered at query time, not at write time. Language-specific logic lives in exactly one place — the AST normalizer — so everything downstream stays language-agnostic.
Versioning
The version lives in a single place — __version__ in sciogen/__init__.py. pyproject.toml reads it via hatchling and the CLI banner imports the same constant, so bump that one line and everything stays in lockstep. Never add a second version string.