sifs 0.3.0

SIFS Is Fast Search: instant local code search for agents
Documentation
use crate::agent_artifacts::{
    AgentArtifact, AgentTarget, MANAGED_BLOCK_BEGIN_PREFIX, render_artifact,
};
use serde::Serialize;
use std::fs;
use std::path::PathBuf;
use std::process::Command;

#[derive(Clone, Debug, Serialize)]
pub struct AgentDoctorOutput {
    pub schema_version: u8,
    pub targets: Vec<AgentDoctorTarget>,
}

#[derive(Clone, Debug, Serialize)]
pub struct AgentDoctorTarget {
    pub target: AgentTarget,
    pub status: String,
    pub checks: Vec<AgentDoctorCheck>,
    pub next_actions: Vec<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct AgentDoctorCheck {
    pub name: String,
    pub state: String,
    pub evidence: String,
}

pub fn doctor(target: AgentTarget, artifact: AgentArtifact) -> AgentDoctorOutput {
    let mut targets = Vec::new();
    for concrete_target in target.concrete_targets() {
        let artifacts = artifact.concrete_artifacts(concrete_target);
        targets.push(doctor_target(concrete_target, artifacts));
    }
    AgentDoctorOutput {
        schema_version: 1,
        targets,
    }
}

fn doctor_target(target: AgentTarget, artifacts: Vec<AgentArtifact>) -> AgentDoctorTarget {
    let mut checks = Vec::new();
    let binary = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("sifs"));
    let binary_ok = Command::new(&binary).arg("--version").output();
    match binary_ok {
        Ok(output) if output.status.success() => checks.push(check(
            "binary_on_path",
            "pass",
            format!(
                "{} ({})",
                binary.display(),
                String::from_utf8_lossy(&output.stdout).trim()
            ),
        )),
        Ok(output) => checks.push(check(
            "binary_on_path",
            "fail",
            String::from_utf8_lossy(&output.stderr).trim().to_owned(),
        )),
        Err(err) => checks.push(check("binary_on_path", "fail", err.to_string())),
    }

    for artifact in artifacts {
        match artifact {
            AgentArtifact::Skill => checks.push(skill_check(target)),
            AgentArtifact::Snippet => checks.push(snippet_check(target)),
            AgentArtifact::Mcp => checks.extend(mcp_checks(target)),
            AgentArtifact::All => {}
        }
    }
    checks.push(check(
        "visible_to_current_session",
        "unknown",
        "Current-session agent tool visibility cannot be proven from this process.",
    ));
    checks.push(check(
        "cli_fallback_ready",
        if checks
            .iter()
            .any(|check| check.name == "binary_on_path" && check.state == "pass")
        {
            "pass"
        } else {
            "fail"
        },
        "CLI fallback uses shell commands such as `sifs search`.",
    ));
    let status = if checks
        .iter()
        .any(|check| check.name == "binary_on_path" && check.state == "pass")
    {
        "ready_fallback_only"
    } else {
        "not_ready"
    };
    AgentDoctorTarget {
        target,
        status: status.to_owned(),
        checks,
        next_actions: vec![format!(
            "Use CLI fallback: sifs search \"<query>\" --source <project>"
        )],
    }
}

fn skill_check(target: AgentTarget) -> AgentDoctorCheck {
    let Some(path) = target.default_skill_destination() else {
        return check(
            "skill_present",
            "unknown",
            "No default skill destination is known; pass --destination during install.",
        );
    };
    if !path.exists() {
        return check(
            "skill_present",
            "fail",
            format!("{} does not exist", path.display()),
        );
    }
    if path.is_dir() {
        let skill = path.join("SKILL.md");
        return content_check("skill_content_current", skill, target, AgentArtifact::Skill);
    }
    content_check("skill_content_current", path, target, AgentArtifact::Skill)
}

fn snippet_check(target: AgentTarget) -> AgentDoctorCheck {
    let Some(path) = target.default_snippet_file() else {
        return check("snippet_present", "unknown", "No default snippet file.");
    };
    if !path.exists() {
        return check(
            "snippet_present",
            "fail",
            format!("{} does not exist", path.display()),
        );
    }
    match fs::read_to_string(&path) {
        Ok(content) if content.contains(MANAGED_BLOCK_BEGIN_PREFIX) => check(
            "snippet_present",
            "pass",
            format!("{} contains a SIFS managed block", path.display()),
        ),
        Ok(_) => check(
            "snippet_present",
            "fail",
            format!("{} has no SIFS managed block", path.display()),
        ),
        Err(err) => check("snippet_present", "fail", err.to_string()),
    }
}

fn content_check(
    name: &str,
    path: PathBuf,
    target: AgentTarget,
    artifact: AgentArtifact,
) -> AgentDoctorCheck {
    match fs::read_to_string(&path) {
        Ok(content) => match render_artifact(target, artifact, None, None) {
            Ok(rendered)
                if content == rendered.content || content.contains("name: sifs-search") =>
            {
                check(
                    name,
                    "pass",
                    format!("{} is a SIFS skill artifact", path.display()),
                )
            }
            Ok(_) => check(
                name,
                "fail",
                format!("{} is stale or modified", path.display()),
            ),
            Err(err) => check(name, "unknown", err.to_string()),
        },
        Err(err) => check(name, "fail", err.to_string()),
    }
}

fn mcp_checks(target: AgentTarget) -> Vec<AgentDoctorCheck> {
    let config_present = match target {
        AgentTarget::Codex => std::env::var_os("HOME")
            .map(PathBuf::from)
            .map(|home| home.join(".codex/config.toml"))
            .filter(|path| path.exists())
            .map(|path| {
                fs::read_to_string(&path)
                    .map(|content| content.contains("[mcp_servers.sifs]"))
                    .unwrap_or(false)
            })
            .unwrap_or(false),
        AgentTarget::ClaudeCode if PathBuf::from(".mcp.json").exists() => {
            fs::read_to_string(".mcp.json")
                .map(|content| content.contains("\"sifs\""))
                .unwrap_or(false)
        }
        AgentTarget::ClaudeCode => false,
        _ => false,
    };
    vec![
        check(
            "mcp_config_present",
            if config_present { "pass" } else { "fail" },
            if config_present {
                "SIFS MCP config appears present."
            } else {
                "No SIFS MCP config was found for this target."
            },
        ),
        check(
            "mcp_handshake_ok",
            "unknown",
            "Run `sifs mcp doctor --json` for protocol handshake and BM25 smoke probes.",
        ),
        check(
            "mcp_tools_listed",
            "unknown",
            "Current-session MCP tools cannot be listed from this process.",
        ),
        check(
            "search_smoke_ok",
            "unknown",
            "Run `sifs mcp doctor --source <project> --offline --no-cache --json`.",
        ),
    ]
}

fn check(
    name: impl Into<String>,
    state: impl Into<String>,
    evidence: impl Into<String>,
) -> AgentDoctorCheck {
    AgentDoctorCheck {
        name: name.into(),
        state: state.into(),
        evidence: evidence.into(),
    }
}