tessera-codegraph 0.8.0

A local, deterministic semantic code graph and MCP server for AI coding agents. 11 languages, personalized PageRank impact, hallucination validator, call-path tracing, graph export, incremental indexing.
Documentation
use std::fmt::{self, Display};
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::Result;
use rusqlite::Connection;
use serde::{Deserialize, Serialize};

use crate::db;
use crate::indexer;
use crate::types::Language;

#[derive(Debug, Clone)]
pub struct DoctorOptions {
    pub root: PathBuf,
    pub db_path: PathBuf,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DoctorResult {
    pub ok: bool,
    pub root: String,
    pub db_path: String,
    pub checks: Vec<DoctorCheck>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DoctorCheck {
    pub name: String,
    pub status: DoctorStatus,
    pub message: String,
    pub hint: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DoctorStatus {
    Ok,
    Warn,
    Error,
}

impl Display for DoctorResult {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "Tessera doctor for {} (db={})", self.root, self.db_path)?;
        for check in &self.checks {
            let marker = match check.status {
                DoctorStatus::Ok => "ok",
                DoctorStatus::Warn => "warn",
                DoctorStatus::Error => "error",
            };
            writeln!(f, "  [{marker}] {}: {}", check.name, check.message)?;
            if let Some(hint) = &check.hint {
                writeln!(f, "        hint: {hint}")?;
            }
        }
        if self.ok {
            writeln!(f, "Doctor passed.")
        } else {
            writeln!(f, "Doctor found issues.")
        }
    }
}

pub fn run(options: DoctorOptions) -> Result<DoctorResult> {
    let root = options
        .root
        .canonicalize()
        .unwrap_or_else(|_| options.root.clone());
    let db_path = options.db_path;
    let mut checks = Vec::new();

    check_root(&root, &mut checks);
    check_db(&db_path, &mut checks)?;
    check_snapshot(&db_path, &mut checks);
    check_parsers(&mut checks);
    check_ignored_paths(&mut checks);
    check_mcp_config(&db_path, &mut checks);

    let ok = !checks
        .iter()
        .any(|check| matches!(check.status, DoctorStatus::Error));

    Ok(DoctorResult {
        ok,
        root: root.display().to_string(),
        db_path: db_path.display().to_string(),
        checks,
    })
}

fn check_root(root: &Path, checks: &mut Vec<DoctorCheck>) {
    if root.exists() && root.is_dir() {
        push_ok(checks, "root", format!("{} exists", root.display()), None);
    } else {
        push_error(
            checks,
            "root",
            format!("{} is not a directory", root.display()),
            Some("Run from a repository root or pass `--root <path>`.".to_string()),
        );
    }
}

fn check_db(db_path: &Path, checks: &mut Vec<DoctorCheck>) -> Result<()> {
    if !db_path.exists() {
        push_error(
            checks,
            "database",
            format!("{} does not exist", db_path.display()),
            Some(format!(
                "Run `tessera index . --db {}` first.",
                db_path.display()
            )),
        );
        return Ok(());
    }

    let conn = Connection::open(db_path)?;
    let schema = db::get_meta(&conn, "schema_version")?;
    match schema {
        Some(version) if version == db::SCHEMA_VERSION.to_string() => {
            push_ok(checks, "schema", format!("schema version {version}"), None)
        }
        Some(version) => push_error(
            checks,
            "schema",
            format!("schema version {version}, expected {}", db::SCHEMA_VERSION),
            Some("Run `tessera index . --full` to rebuild with the current schema.".to_string()),
        ),
        None => push_error(
            checks,
            "schema",
            "schema version metadata is missing".to_string(),
            Some("Run `tessera index . --full` to rebuild the database.".to_string()),
        ),
    }

    let files = count_table(&conn, "files")?;
    let symbols = count_table(&conn, "symbols")?;
    push_ok(
        checks,
        "index",
        format!("{files} files, {symbols} symbols"),
        if symbols == 0 {
            Some("The DB is valid but empty; run `tessera index . --full`.".to_string())
        } else {
            None
        },
    );
    Ok(())
}

fn check_snapshot(db_path: &Path, checks: &mut Vec<DoctorCheck>) {
    let Some(parent) = db_path.parent() else {
        push_warn(
            checks,
            "snapshot",
            "database path has no parent directory".to_string(),
            None,
        );
        return;
    };
    let snapshot_path = parent.join("snapshot.bin");
    if !snapshot_path.exists() {
        push_warn(
            checks,
            "snapshot",
            format!("{} is missing", snapshot_path.display()),
            Some(format!(
                "Run `tessera snapshot --db {}`.",
                db_path.display()
            )),
        );
        return;
    }

    let db_modified = fs::metadata(db_path).and_then(|m| m.modified());
    let snapshot_modified = fs::metadata(&snapshot_path).and_then(|m| m.modified());
    match (db_modified, snapshot_modified) {
        (Ok(db_time), Ok(snapshot_time)) if snapshot_time >= db_time => push_ok(
            checks,
            "snapshot",
            format!("{} is fresh", snapshot_path.display()),
            None,
        ),
        (Ok(_), Ok(_)) => push_warn(
            checks,
            "snapshot",
            format!("{} is older than the database", snapshot_path.display()),
            Some(format!(
                "Run `tessera snapshot --db {}`.",
                db_path.display()
            )),
        ),
        _ => push_warn(
            checks,
            "snapshot",
            "could not compare snapshot and database timestamps".to_string(),
            None,
        ),
    }
}

fn check_parsers(checks: &mut Vec<DoctorCheck>) {
    let snippets = [
        (Language::JavaScript, "function f() { return 1; }\n"),
        (
            Language::TypeScript,
            "export function f(x: string): string { return x; }\n",
        ),
        (Language::Tsx, "export function C() { return <div />; }\n"),
        (Language::Python, "def f():\n    return 1\n"),
        (Language::Go, "package sample\nfunc F() int { return 1 }\n"),
        (Language::Rust, "pub fn f() -> i32 { 1 }\n"),
        (Language::Java, "class A { int f() { return 1; } }\n"),
        (Language::C, "int f() { return 1; }\n"),
        (
            Language::Cpp,
            "class A { public: int f() { return 1; } };\n",
        ),
        (Language::CSharp, "class A { int F() { return 1; } }\n"),
        (Language::Ruby, "def f\n  1\nend\n"),
        (Language::Php, "<?php function f() { return 1; }\n"),
    ];

    let mut failed = Vec::new();
    for (language, snippet) in snippets {
        if indexer::parse_file(language, snippet).is_err() {
            failed.push(language.to_string());
        }
    }

    if failed.is_empty() {
        push_ok(
            checks,
            "parsers",
            "all bundled parsers load".to_string(),
            None,
        );
    } else {
        push_error(
            checks,
            "parsers",
            format!("failed parser smoke tests: {}", failed.join(", ")),
            Some("Reinstall Tessera or rebuild from a clean checkout.".to_string()),
        );
    }
}

fn check_ignored_paths(checks: &mut Vec<DoctorCheck>) {
    push_ok(
        checks,
        "ignored-paths",
        "skips .git, node_modules, target, dist, build, .next, virtualenvs, __pycache__, and .tessera".to_string(),
        None,
    );
}

fn check_mcp_config(db_path: &Path, checks: &mut Vec<DoctorCheck>) {
    push_ok(
        checks,
        "mcp",
        format!("stdio command: tessera mcp --db {}", db_path.display()),
        Some("Add this command to your agent MCP config after indexing.".to_string()),
    );
}

fn count_table(conn: &Connection, table: &str) -> Result<usize> {
    let sql = format!("SELECT COUNT(*) FROM {table}");
    let count: i64 = conn.query_row(&sql, [], |row| row.get(0))?;
    Ok(count as usize)
}

fn push_ok(checks: &mut Vec<DoctorCheck>, name: &str, message: String, hint: Option<String>) {
    checks.push(DoctorCheck {
        name: name.to_string(),
        status: DoctorStatus::Ok,
        message,
        hint,
    });
}

fn push_warn(checks: &mut Vec<DoctorCheck>, name: &str, message: String, hint: Option<String>) {
    checks.push(DoctorCheck {
        name: name.to_string(),
        status: DoctorStatus::Warn,
        message,
        hint,
    });
}

fn push_error(checks: &mut Vec<DoctorCheck>, name: &str, message: String, hint: Option<String>) {
    checks.push(DoctorCheck {
        name: name.to_string(),
        status: DoctorStatus::Error,
        message,
        hint,
    });
}