# Developer Guide This document covers the internals of **SocratiCode** — architecture, data flow, configuration, and how to build, extend, and debug. ## Table of Contents - [Architecture Overview](#architecture-overview) - [Prerequisites for Development](#prerequisites-for-development) - [Building and Running](#building-and-running) - [Project Structure](#project-structure) - [Configuration & Constants](#configuration--constants) - [Data Flow: Indexing](#data-flow-indexing) - [Data Flow: Search](#data-flow-search) - [Data Flow: Incremental Update](#data-flow-incremental-update) - [Data Flow: Code Graph](#data-flow-code-graph) - [Testing](#testing) - [Services Reference](#services-reference) - [MCP Tools Reference](#mcp-tools-reference) - [Data Structures](#data-structures) - [Docker & Infrastructure](#docker--infrastructure) - [Extending the Indexer](#extending-the-indexer) - [Troubleshooting](#troubleshooting) --- ## Architecture Overview ``` ┌─────────────────────────────────────────────────────┐ │ MCP Host │ │ (VS Code, Claude Desktop, etc.) │ └──────────────────────┬──────────────────────────────┘ │ stdio (JSON-RPC) ┌──────────────────────▼──────────────────────────────┐ │ MCP Server (src/index.ts) │ │ │ │ ┌──────────┐ ┌───────────┐ ┌───────┐ ┌──────────┐ │ │ │ Index │ │ Query │ │ Graph │ │ Manage │ │ │ │ Tools │ │ Tools │ │ Tools │ │ Tools │ │ │ └────┬─────┘ └─────┬─────┘ └───┬───┘ └────┬─────┘ │ │ │ │ │ │ │ │ ┌────▼─────────────▼───────────▼───────────▼─────┐ │ │ │ Services │ │ │ │ ┌─────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ │ │ Indexer │ │ Qdrant │ │ Ollama │ │ Docker │ │ │ │ │ │ │ │ Client │ │ Client │ │ Mgmt │ │ │ │ │ └────┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ │ │ │ │ │ │ │ │ │ │ │ │ ┌────▼────┐ ┌───▼────┐ ┌──▼──────┐ │ │ │ │ │ Ignore │ │Embedder│ │ Watcher │ │ │ │ │ │ Filter │ │ │ │(@parcel) │ │ │ │ │ └─────────┘ └────────┘ └─────────┘ │ │ │ └─────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────┘ │ │ ▼ ▼ ┌──────────────────┐ ┌─────────────────────┐ │ Qdrant (Docker) │ │ Ollama (Docker) │ │ localhost:16333 │ │ localhost:11435 │ │ │ │ │ │ Vector storage │ │ nomic-embed-text │ │ 768-dim cosine │ │ 768-dim embeddings │ └──────────────────┘ └─────────────────────┘ ``` --- ## Prerequisites for Development | Tool | Version | Purpose | |------|---------|---------| | Node.js | 18+ | Runtime | | npm | 9+ | Package manager | | TypeScript | 5.7+ | Installed as devDependency | | Docker | Any recent | Runs Qdrant | | Ollama | Any recent | Runs embedding model | --- ## Building and Running ### Install dependencies ```bash npm install ``` ### Build ```bash npm run build ``` This compiles TypeScript from `src/` to `dist/` with source maps and declarations. ### Run directly (development) ```bash npm run dev ``` Uses `tsx` to run TypeScript directly without a build step. ### Run built version ```bash npm start # or node dist/index.js ``` The server communicates over **stdio** using JSON-RPC (MCP protocol). It's designed to be launched by an MCP host, not run standalone in a terminal. For testing, you can use the MCP Inspector or pipe JSON-RPC messages. ### TypeScript Configuration - **Target**: ES2022 - **Module**: Node16 (ESM) - **Strict mode**: Enabled - **Output**: `dist/` with source maps and `.d.ts` declarations ### Linting SocratiCode uses [Biome](https://biomejs.dev/) for linting. Biome is fast, zero-config, and catches unused imports, style issues, and potential bugs. ```bash # Check for lint issues npm run lint # Auto-fix safe issues npm run lint:fix ``` For VS Code, install the [Biome extension](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) for real-time lint feedback and auto-fix on save. ### Versioning & Releases SocratiCode uses [Conventional Commits](https://www.conventionalcommits.org/) and [release-it](https://github.com/release-it/release-it) for automated versioning and changelog generation. **Commit message format:** ``` feat: add fuzzy search support → Features fix: resolve race condition → Bug Fixes perf: optimise embedding batching → Performance refactor: simplify provider factory → Refactors docs: update quickstart guide → Documentation test: add watcher edge-case tests → Tests chore: update deps → hidden from changelog ``` **Before committing:** ```bash npm run lint && npx tsc --noEmit && npm run test:unit ``` **Creating a release** (maintainers only): ```bash # Interactive — prompts for patch/minor/major npm run release # Dry run — preview what will happen without making changes npm run release:dry ``` This will automatically: 1. Determine the version bump from your commits 2. Update `CHANGELOG.md` with all `feat:`, `fix:`, etc. entries 3. Bump the version in `package.json` 4. Create a git commit and tag (`v1.1.0`) 5. Push to GitHub and create a GitHub Release --- ## Project Structure ``` src/ ├── index.ts # MCP server entry point — registers all 21 tools ├── config.ts # Project ID generation (SHA-256), collection naming ├── constants.ts # All constants: ports, container names, models, chunk sizes, extensions ├── types.ts # TypeScript interfaces and types │ ├── services/ │ ├── docker.ts # Docker CLI wrapper — manage Qdrant & Ollama containers │ ├── ollama.ts # Ollama client — model availability, embedding calls │ ├── embeddings.ts # Embedding generation with batching and task prefixes │ ├── qdrant.ts # Qdrant client — collections, upsert, search, metadata │ ├── indexer.ts # Core indexing — file discovery, chunking, full/incremental │ ├── watcher.ts # File system watcher via @parcel/watcher with debouncing │ ├── lock.ts # Cross-process file-based locking via proper-lockfile │ ├── ignore.ts # Ignore filter (.gitignore + .socraticodeignore + defaults) │ ├── logger.ts # Structured JSON logging — stderr (startup/no MCP transport) or MCP notifications/message (when hosted) │ ├── code-graph.ts # AST-based code graph building via ast-grep │ ├── graph-analysis.ts # Graph queries: dependencies, stats, cycles, Mermaid diagrams │ ├── graph-imports.ts # Import/require/use extraction for 18+ languages via AST │ ├── graph-resolution.ts # Module specifier → file path resolution │ ├── startup.ts # Startup lifecycle: auto-resume, graceful shutdown coordination │ └── context-artifacts.ts # Context artifact loading, chunking, indexing, search │ ├── tools/ │ ├── index-tools.ts # Handlers: codebase_index, codebase_update, codebase_remove, codebase_stop, codebase_watch │ ├── query-tools.ts # Handlers: codebase_search, codebase_status │ ├── graph-tools.ts # Handlers: codebase_graph_build/query/stats/circular/visualize │ ├── context-tools.ts # Handlers: codebase_context, codebase_context_search/index/remove │ └── manage-tools.ts # Handlers: codebase_health, codebase_list_projects, codebase_about tests/ ├── helpers/ │ ├── fixtures.ts # Test fixture utilities (temp projects, Docker checks) │ └── setup.ts # Integration test infrastructure (Qdrant client, cleanup) ├── unit/ # 460 tests — no Docker required ├── integration/ # 137 tests — requires Docker └── e2e/ # 20 tests — full lifecycle docker-compose.yml # Alternative way to run infrastructure vitest.config.ts # Test framework configuration ``` --- ## Configuration & Constants All constants are defined in `src/constants.ts`: | Constant | Value | Description | |----------|-------|-------------| | `SEARCH_DEFAULT_LIMIT` | `10` | Default search results per query (env-configurable, 1-50) | | `SEARCH_MIN_SCORE` | `0.10` | Minimum RRF score threshold (env-configurable, 0-1) | | `CHUNK_SIZE` | `100` | Lines per chunk | | `CHUNK_OVERLAP` | `10` | Overlapping lines between adjacent chunks | | `MAX_FILE_BYTES` | `5 MB` | Max file size before skipping (env-configurable via `MAX_FILE_SIZE_MB`) | | `MAX_AVG_LINE_LENGTH` | `500` | Avg line length above which character-based chunking is used (minified files) | | `MAX_CHUNK_CHARS` | `2000` | Hard character limit per chunk (provider-level safety net) | | `QDRANT_PORT` | `16333` | Qdrant HTTP API port (host-side) | | `QDRANT_GRPC_PORT` | `16334` | Qdrant gRPC port (host-side) | | `QDRANT_CONTAINER_NAME` | `socraticode-qdrant` | Docker container name | | `QDRANT_IMAGE` | `qdrant/qdrant:v1.17.0` | Docker image (pinned version) | | `OLLAMA_PORT` | `11435` | Ollama API port (host-side) | | `OLLAMA_CONTAINER_NAME` | `socraticode-ollama` | Docker container name | | `OLLAMA_IMAGE` | `ollama/ollama:latest` | Docker image | > **Note**: `EMBEDDING_MODEL`, `EMBEDDING_DIMENSIONS`, and `EMBEDDING_CONTEXT_LENGTH` are defined in `src/services/embedding-config.ts`, not in `src/constants.ts`. Defaults are `nomic-embed-text` / `768` for Ollama, `text-embedding-3-small` / `1536` for OpenAI, and `gemini-embedding-001` / `3072` for Google. ### Embedding batch size Defined in `src/services/embeddings.ts`: texts are sent to Ollama in batches of **32**. ### File watcher debounce Defined in `src/services/watcher.ts`: file changes are debounced for **2000ms** before triggering an index update. ### Maximum file size Defined in `src/constants.ts` as `MAX_FILE_BYTES`: files larger than **5 MB** are skipped (configurable via `MAX_FILE_SIZE_MB` env var). ### Qdrant health check Defined in `src/services/docker.ts`: after starting the container, the server polls `/healthz` up to **30 times** with **1000ms** between retries. ### Project ID & Collection Naming Defined in `src/config.ts`: - **Project ID**: First 12 characters of SHA-256 hash of the absolute project path. - **Code collection**: `codebase_{projectId}` - **Graph collection**: `codegraph_{projectId}` - **Context artifacts collection**: `context_{projectId}` This means the same folder path always maps to the same collection, even across restarts. ### Supported File Extensions (54) | Category | Extensions | |----------|-----------| | JavaScript/TypeScript | `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs` | | Python | `.py`, `.pyw`, `.pyi` | | Java/Kotlin/Scala | `.java`, `.kt`, `.kts`, `.scala` | | C/C++ | `.c`, `.h`, `.cpp`, `.hpp`, `.cc`, `.hh`, `.cxx` | | C# | `.cs` | | Go | `.go` | | Rust | `.rs` | | Ruby | `.rb` | | PHP | `.php` | | Swift | `.swift` | | Shell | `.sh`, `.bash`, `.zsh` | | Web | `.html`, `.htm`, `.css`, `.scss`, `.sass`, `.less`, `.vue`, `.svelte` | | Config | `.json`, `.yaml`, `.yml`, `.toml`, `.xml`, `.ini`, `.cfg` | | Documentation | `.md`, `.mdx`, `.rst`, `.txt` | | SQL | `.sql` | | Dart | `.dart` | | Lua | `.lua` | | R | `.r`, `.R` | | Docker | `.dockerfile` | Special files always indexed: `Dockerfile`, `Makefile`, `Rakefile`, `Gemfile`, `Procfile`, `.env.example`, `.gitignore`, `.dockerignore`. ### Built-in Ignore Patterns (45) The full list is in `src/services/ignore.ts`. Key entries: `node_modules`, `.git`, `dist`, `build`, `.next`, `__pycache__`, `.venv`, `target`, `.idea`, `.vscode`, `*.min.js`, `*.lock`, `package-lock.json`, `yarn.lock`, `coverage`, `vendor`, `.DS_Store`, `Thumbs.db`. --- ## Data Flow: Indexing When `codebase_index` is called: ``` 1. INFRASTRUCTURE CHECK handleIndexTool() → ensureQdrantReady() + ensureOllamaReady() ├── Check Docker CLI: docker info ├── Check Qdrant image: docker images (qdrant/qdrant:v1.17.0) ├── Pull image if missing: docker pull qdrant/qdrant:v1.17.0 ├── Check container: docker ps --filter name=socraticode-qdrant ├── Create/start container with volume mount ├── Wait for /healthz (up to 30s, 30 × 1s retries) ├── Check Ollama container: docker ps --filter name=socraticode-ollama ├── Start Ollama container if needed ├── Check for nomic-embed-text model └── Pull model if missing 2. FILE DISCOVERY getIndexableFiles(projectPath, extraExts?) ├── glob("**/*") to enumerate all files ├── Build ignore filter: defaults + .gitignore + .socraticodeignore ├── Filter by supported extension, special filename, or extra extensions └── Filter out ignored paths 3. COLLECTION SETUP ensureCollection(collectionName) ├── Check if collection exists ├── Create with: provider-dependent dimensions (768/1536/3072), cosine distance, on-disk payload └── Create payload indexes: filePath, relativePath, language, contentHash 4. FILE SCANNING & CHUNKING (parallel batches of 50 files) ├── Read file content (skip if > 5 MB or unreadable) ├── Hash content: SHA-256 → 16-char prefix ├── Skip if hash matches existing (re-index mode) ├── Chunk using three-tier strategy: │ ├── Minified/bundled (avg line length > 500): character-based chunking │ │ └── Splits at safe boundaries (newline, space, semicolon, comma) │ ├── AST-aware (supported languages): chunk at function/class boundaries │ │ ├── ast-grep parses top-level declarations │ │ ├── Small declarations merged, large ones sub-chunked │ │ └── Preamble (imports) and epilogue handled separately │ └── Line-based fallback: 100-line segments with 10-line overlap ├── Hard character cap (2000 chars) applied to all chunks ├── Generate chunk ID: SHA-256 of "filePath:startLine" formatted as UUID └── Detect language from file extension 5. BATCHED EMBEDDING + UPSERT (50 files per batch) For each batch of files: ├── Prepare text: "search_document: {relativePath}\n{content}" ├── Generate embeddings via configured provider (further batched internally) ├── Upsert to Qdrant with dense vector + BM25 text + payload ├── Update in-memory file hashes ├── Checkpoint: persist hashes to Qdrant (progress survives crashes) └── Check for cancellation request before next batch 6. POST-INDEX ├── Save final metadata (status: "completed") ├── Auto-build code dependency graph (non-fatal on failure) └── Auto-index context artifacts if config exists (non-fatal on failure) ``` --- ## Data Flow: Search When `codebase_search` is called: ``` 1. Generate query embedding ├── Prepare text: "search_query: {query}" └── Send to configured embedding provider → provider-dependent vector (768 / 1536 / 3072 dims) 2. HYBRID SEARCH (dense + BM25, RRF-fused) ├── Build two parallel prefetch sub-queries: │ ├── Dense: query vector → semantic cosine similarity (client-side) │ └── BM25: query text → server-side BM25 inference (Qdrant v1.15.2+) ├── Apply optional filters (filePath, language) as payload conditions on both sub-queries ├── Qdrant Query API runs both sub-queries then fuses results via Reciprocal Rank Fusion (RRF) └── Return top N results with RRF-combined scores and payloads 3. Format results └── Each result: file path, line range, language, RRF score, code content ``` ### nomic-embed-text Task Prefixes The `nomic-embed-text` model uses task-specific prefixes for asymmetric retrieval: - **Documents** are prefixed with `search_document: ` — this tells the model to encode the text as a passage to be retrieved. - **Queries** are prefixed with `search_query: ` — this tells the model to encode as a search query. This asymmetric encoding significantly improves retrieval quality. --- ## Data Flow: Incremental Update When `codebase_update` is called: ``` 1. Check if collection exists and has points └── If empty/missing → fall back to full indexProject() 2. Enumerate current files on disk └── Same filtering as full index (extensions, ignore rules) 3. Compare against in-memory hash map ├── File hash matches → skip (unchanged) ├── File hash differs → delete old chunks, re-chunk, re-embed, upsert ├── File not in hash map → new file, chunk, embed, upsert └── Hash map entry not on disk → deleted file, remove chunks 4. Return delta: { added, updated, removed, chunksCreated, cancelled } ``` > **Note**: File content hashes are persisted in Qdrant after each batch. On server restart, hashes are loaded from Qdrant on first use, so incremental updates remain truly incremental across restarts. --- ## Data Flow: Code Graph When `codebase_graph_build` is called: ``` 0. CONCURRENCY GUARD ├── If a build is already in progress for this project, return the │ existing in-flight promise (deduplication — callers share the result) └── Otherwise, start a new tracked build 1. BACKGROUND EXECUTION (fire-and-forget) ├── Tool returns immediately with "build started" message ├── Actual build runs asynchronously on the event loop ├── Progress tracked via GraphBuildProgress { filesTotal, filesProcessed, phase } └── Client polls codebase_graph_status for progress % 2. FILE DISCOVERY (phase: "scanning files") ├── Get graphable files from project (same ignore filters as indexing) └── Include files with AST-grep grammar + files with extra extensions 3. PARSE IMPORTS (phase: "analyzing imports", per file, via ast-grep) ├── Determine AST-grep language from file extension │ ├── Built-in: TypeScript, JavaScript, Python, Java, Kotlin, etc. │ └── Dynamic: C, C++, C#, Go, Rust, Ruby, PHP, Swift, Bash, Scala ├── Files with extra extensions (no AST grammar) → leaf nodes only ├── Parse file with ast-grep ├── Extract import/require/use/include statements using AST patterns │ ├── JavaScript/TypeScript: import ... from, require(), dynamic import() │ ├── Python: import, from ... import │ ├── Java/Kotlin/Scala: import statements │ ├── Go: import declarations │ ├── Rust: use, mod │ ├── Ruby: require, require_relative │ ├── PHP: use, require, include │ ├── C/C++: #include │ ├── Swift: import │ ├── Bash: source, . (dot) │ ├── Dart/Lua: regex-based extraction │ └── Svelte/Vue: HTML parse →