pub(crate) mod internal;
use anyhow::{Context, Result};
use internal::{
collect_inventory_json, execute_callees, execute_callers, execute_derive,
execute_derive_moments, execute_functions, execute_importers, execute_imports,
execute_inventory, execute_search,
};
use rusqlite::Connection;
#[allow(unused_imports)]
pub use internal::search::{assay_search, assay_search_json, SearchOptions, SearchResult};
const DB_PATH: &str = ".patina/local/data/patina.db";
#[derive(Debug, Clone, Default)]
pub enum QueryType {
#[default]
Inventory,
Imports,
Importers,
Functions,
Callers,
Callees,
Derive,
DeriveMoments,
Search {
query: String,
},
Cochange {
file: String,
},
Belief {
id: String,
},
}
#[derive(Debug, Clone, Default)]
pub struct AssayOptions {
pub query_type: QueryType,
pub pattern: Option<String>,
pub limit: usize,
pub json: bool,
pub repo: Option<String>,
pub all_repos: bool,
pub include_issues: bool,
}
pub fn execute(options: AssayOptions) -> Result<()> {
if let QueryType::Search { ref query } = options.query_type {
let search_opts = SearchOptions {
limit: options.limit,
include_issues: options.include_issues,
repo: options.repo.clone(),
};
if options.json {
let json = internal::search::assay_search_json(query, &search_opts)?;
println!("{}", json);
return Ok(());
}
return execute_search(query, &search_opts);
}
if let QueryType::Cochange { ref file } = options.query_type {
let db_path = match &options.repo {
Some(name) => crate::commands::repo::get_db_path(name)?,
None => DB_PATH.to_string(),
};
if options.json {
let json = internal::temporal::execute_cochange_json(file, options.limit, &db_path)?;
println!("{}", json);
return Ok(());
}
return internal::temporal::execute_cochange(file, options.limit, &db_path);
}
if let QueryType::Belief { ref id } = options.query_type {
return internal::belief::execute_belief_grounding(id, options.limit, options.json);
}
if options.all_repos {
return execute_all_repos(&options);
}
let db_path = match &options.repo {
Some(name) => crate::commands::repo::get_db_path(name)?,
None => DB_PATH.to_string(),
};
let conn = Connection::open(&db_path)
.with_context(|| format!("Failed to open database: {}", db_path))?;
if let Some(ref repo) = options.repo {
println!("Repository: {}\n", repo);
}
match options.query_type {
QueryType::Inventory => execute_inventory(&conn, &options, None),
QueryType::Imports => execute_imports(&conn, &options),
QueryType::Importers => execute_importers(&conn, &options),
QueryType::Functions => execute_functions(&conn, &options),
QueryType::Callers => execute_callers(&conn, &options),
QueryType::Callees => execute_callees(&conn, &options),
QueryType::Derive => execute_derive(&conn, &options),
QueryType::DeriveMoments => execute_derive_moments(&conn, &options),
QueryType::Search { .. } | QueryType::Cochange { .. } | QueryType::Belief { .. } => {
unreachable!("handled above")
}
}
}
pub fn execute_query(options: &AssayOptions) -> Result<String> {
if let QueryType::Search { ref query } = options.query_type {
let search_opts = SearchOptions {
limit: options.limit,
include_issues: options.include_issues,
repo: options.repo.clone(),
};
return internal::search::assay_search_json(query, &search_opts);
}
if let QueryType::Cochange { ref file } = options.query_type {
let db_path = match &options.repo {
Some(name) => crate::commands::repo::get_db_path(name)?,
None => DB_PATH.to_string(),
};
return internal::temporal::execute_cochange_json(file, options.limit, &db_path);
}
if let QueryType::Belief { ref id } = options.query_type {
return internal::belief::execute_belief_grounding_json(id, options.limit);
}
let db_path = match &options.repo {
Some(name) => crate::commands::repo::get_db_path(name)?,
None => DB_PATH.to_string(),
};
let conn = Connection::open(&db_path)
.with_context(|| format!("Failed to open database: {}", db_path))?;
match options.query_type {
QueryType::Inventory => {
let results = collect_inventory_json(&conn, options, None)?;
Ok(serde_json::to_string_pretty(&results)?)
}
_ => anyhow::bail!(
"assay query_type '{:?}' not yet supported via query interface; use CLI or MCP",
options.query_type
),
}
}
fn execute_all_repos(options: &AssayOptions) -> Result<()> {
let repos = crate::commands::repo::list()?;
if repos.is_empty() {
println!("No registered repos. Use 'patina repo add <url>' to add repos.");
return Ok(());
}
let current_has_db = std::path::Path::new(DB_PATH).exists();
if options.json {
let mut all_results: Vec<serde_json::Value> = Vec::new();
if current_has_db {
if let Ok(conn) = Connection::open(DB_PATH) {
if let Ok(results) = collect_inventory_json(&conn, options, Some("(current)")) {
all_results.extend(results);
}
}
}
for repo in &repos {
let db_path = std::path::Path::new(&repo.path).join(".patina/local/data/patina.db");
if let Ok(conn) = Connection::open(&db_path) {
if let Ok(results) = collect_inventory_json(&conn, options, Some(&repo.name)) {
all_results.extend(results);
}
}
}
println!("{}", serde_json::to_string_pretty(&all_results)?);
} else {
if current_has_db {
println!("━━━ (current) ━━━\n");
if let Ok(conn) = Connection::open(DB_PATH) {
let _ = execute_inventory(&conn, options, Some("(current)"));
}
println!();
}
for repo in &repos {
println!("━━━ {} ━━━\n", repo.name);
let db_path = std::path::Path::new(&repo.path).join(".patina/local/data/patina.db");
if let Ok(conn) = Connection::open(&db_path) {
let _ = execute_inventory(&conn, options, Some(&repo.name));
} else {
println!(" (database not found)\n");
}
println!();
}
}
Ok(())
}