Metadata-Version: 2.4
Name: rootset
Version: 0.2.0
Summary: Add your description here
Author-email: Jeremy Tregunna <jeremy@tregunna.ca>
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: aiosqlite>=0.22.1
Requires-Dist: jedi>=0.19.2
Requires-Dist: pydantic-settings>=2.13.1
Requires-Dist: pydantic>=2.12.5
Requires-Dist: rustworkx>=0.17.1
Requires-Dist: sentence-transformers>=5.2.3
Requires-Dist: sqlite-vec>=0.1.6
Requires-Dist: tree-sitter-language-pack>=0.13.0
Requires-Dist: tree-sitter>=0.25.2
Provides-Extra: openai
Requires-Dist: openai>=2.21.0; extra == 'openai'
Provides-Extra: voyage
Requires-Dist: voyageai>=0.3.7; extra == 'voyage'
Description-Content-Type: text/markdown

# rootset

In-process code intelligence database for AI coding tools.

Parses repository structure with tree-sitter, resolves symbols via LSP, builds a call graph with rustworkx, and indexes everything into a single SQLite file. Exposes multiple retrieval strategies — full-text BM25, vector ANN, structural S-expression queries, graph traversal, and RRF hybrid fusion — through a single async API.

Supported languages: Python, TypeScript, JavaScript, Rust, Go.

---

## Install

```
pip install rootset
```

Optional extras:

```
pip install rootset[voyage]   # Voyage AI embeddings (voyage-code-3)
pip install rootset[openai]   # OpenAI embeddings (text-embedding-3-small)
```

The default embedding provider is `nomic-ai/nomic-embed-code` via `sentence-transformers`, which runs locally with no API key.

---

## Configuration

`Settings` is a plain dataclass — constructing it explicitly never reads env vars:

```python
from rootset import Settings

settings = Settings(db_path="/path/to/index.db")
```

To load from `ROOTSET_*` environment variables or a `.env` file, use `get_settings()`:

```python
from rootset import get_settings

settings = get_settings()  # reads env, result is cached
```

Key settings:

| Field | Default | Description |
|---|---|---|
| `db_path` | `rootset.db` | Path to the SQLite index file |
| `embedding_provider` | `local` | `local`, `voyage`, or `openai` |
| `voyage_api_key` | `None` | Activates Voyage provider if set |
| `openai_api_key` | `None` | Activates OpenAI provider if set |
| `reindex_debounce_seconds` | `1.5` | Debounce delay for `notify_file_changed` |

---

## Usage

```python
import asyncio
from pathlib import Path
from rootset import Repository, SearchMode, Settings

async def main():
    repo = Repository(Settings(db_path="myindex.db"))

    async with repo:
        # Index a repository
        stats = await repo.index("/path/to/project")
        print(stats)  # IndexStats(files_indexed=..., symbols_indexed=..., ...)

        # Search
        results = await repo.search("function that builds call graph")
        for r in results:
            print(r.symbol.qualified_name, r.score)

        # Explicit search mode
        results = await repo.search("build", mode=SearchMode.TEXT)
        results = await repo.search("build", mode=SearchMode.SEMANTIC)
        results = await repo.search("build", mode=SearchMode.HYBRID)

        # Symbol lookup
        sym = await repo.get_symbol("mymodule.MyClass.my_method")

        # Call graph traversal
        callers = await repo.find_callers("GraphIndexer.build", depth=2)
        callees = await repo.find_callees("GraphIndexer.build", depth=1)

        # Rich context for a symbol
        ctx = await repo.get_context("Repository.index")
        print(ctx.callers, ctx.callees, ctx.import_chain)

        # S-expression structural query
        symbols = await repo.structural_query(
            "(function_definition name: (identifier) @name)", language="python"
        )

        # LLM reranking over hybrid results
        results = await repo.reasoning_search("parse and index source files")

asyncio.run(main())
```

### Incremental reindex

After writing a file, notify the repository to schedule a debounced re-index:

```python
# synchronous — safe to call from a write_file tool handler
repo.notify_file_changed("/path/to/project/src/foo.py")
```

Rapid successive calls for the same path cancel the previous pending reindex; only one fires after `reindex_debounce_seconds`.

---

## Public API

### `Repository`

| Method | Returns | Description |
|---|---|---|
| `index(path, *, incremental=True)` | `IndexStats` | Index a repository. Skips unchanged files when `incremental=True`. |
| `search(query, *, top_k, mode)` | `list[SearchResult]` | Search indexed symbols. Default mode is `HYBRID`. |
| `reasoning_search(query, *, top_k)` | `list[SearchResult]` | Hybrid search followed by LLM reranking. |
| `get_symbol(qualified_name)` | `Symbol \| None` | Exact qualified-name lookup. |
| `find_callers(symbol_name, depth)` | `list[Symbol]` | BFS inbound from call graph. |
| `find_callees(symbol_name, depth)` | `list[Symbol]` | BFS outbound from call graph. |
| `get_context(symbol_name)` | `CodeContext` | Callers, callees, related symbols, import chain. |
| `structural_query(pattern, language)` | `list[Symbol]` | Tree-sitter S-expression query across all indexed files. |
| `notify_file_changed(path)` | `None` | Schedule debounced single-file reindex (synchronous). |

### `SearchMode`

`TEXT` · `SEMANTIC` · `STRUCTURAL` · `GRAPH` · `HYBRID` · `REASONING`

### Return types

- **`Symbol`** — `id`, `qualified_name`, `kind` (`SymbolKind`), `line_start`, `line_end`, `signature`, `docstring`, `content`
- **`SearchResult`** — `symbol`, `score`, `search_type`, `explanation`
- **`CodeContext`** — `symbol`, `definition_file`, `callers`, `callees`, `related_symbols`, `import_chain`
- **`IndexStats`** — `files_indexed`, `symbols_indexed`, `call_edges_indexed`, `import_edges_indexed`, `files_skipped`

---

## License

MIT
