pub mod constants;
pub mod context;
pub mod index_resolver;
pub mod mapreduce;
pub mod role_models;
pub mod verification;
pub mod voice;
pub use index_resolver::{find_git_root, repo_root_from_cwd, resolve_index_from_list};
pub use mapreduce::{DiffStats, MapMode, MapReduceConfig, ReviewPath, select_review_mode};
pub use context::{ContextConfig, ContextFileConfig};
pub use role_models::{
FileModels, RoleCliOverrides, RoleConfig, RoleConfigOverride, RoleEnv, RoleModels,
};
pub use verification::{VerificationConfig, VerificationFileConfig};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tracing::warn;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Provider {
#[default]
OpenRouter,
Bedrock,
}
impl std::fmt::Display for Provider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Provider::OpenRouter => write!(f, "openrouter"),
Provider::Bedrock => write!(f, "bedrock"),
}
}
}
impl std::str::FromStr for Provider {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"openrouter" => Ok(Provider::OpenRouter),
"bedrock" => Ok(Provider::Bedrock),
other => Err(format!("unknown provider: {other}")),
}
}
}
#[derive(Debug, Default, Deserialize)]
struct TomlFile {
#[serde(default)]
models: FileModels,
#[serde(default)]
verification: VerificationFileConfig,
#[serde(default)]
context: ContextFileConfig,
#[serde(default)]
voice: voice::VoiceFileConfig,
#[serde(default)]
coverage: crate::coverage::CoverageFileConfig,
}
#[derive(Debug, Clone)]
pub struct ReviewConfig {
pub dry_run: bool,
pub enabled_repos: String,
pub excluded_repos: String,
pub excluded_authors: String,
pub log_dir: PathBuf,
pub openrouter_api_key: String,
pub search_url: String,
pub analyzer_url: String,
pub search_index: String,
pub search_index_explicit: bool,
pub github_app_id: Option<String>,
pub github_app_private_key: Option<String>,
pub github_token: String,
pub github_webhook_secret: String,
pub github_installations: Vec<(String, u64)>,
pub bot_username: String,
pub live_review_requesters: Vec<String>,
pub role_models: RoleModels,
pub verification: VerificationConfig,
pub context: ContextConfig,
pub context_sources: crate::integrations::context::ContextSourcesConfig,
pub apex_index: String,
pub apex_path_prefixes: Vec<String>,
pub voice_package: Option<String>,
pub voice_principles: bool,
pub coverage: crate::coverage::CoveragePolicy,
}
impl ReviewConfig {
pub fn from_env_and_file(
config_path: Option<&std::path::Path>,
cli_overrides: Option<&RoleCliOverrides>,
) -> Self {
let toml_file = load_toml_file(config_path);
let file_models = toml_file.as_ref().map(|f| f.models.clone());
let file_verification = toml_file.as_ref().map(|f| f.verification.clone());
let file_context = toml_file.as_ref().map(|f| f.context.clone());
let file_voice: Option<&voice::VoiceFileConfig> = toml_file.as_ref().map(|f| &f.voice);
let file_coverage: Option<&crate::coverage::CoverageFileConfig> =
toml_file.as_ref().map(|f| &f.coverage);
let env = RoleEnv::from_env();
let role_models = RoleModels::resolve(cli_overrides, &env, file_models.as_ref());
let verification = VerificationConfig::from_env_and_file(file_verification.as_ref());
let context = ContextConfig::from_env_and_file(file_context.as_ref());
let context_sources = crate::integrations::context::ContextSourcesConfig::from_env_and_file(
file_context.as_ref().map(|c| &c.sources),
);
let dry_run = std::env::var("PR_INTELLIGENCE_DRY_RUN")
.map(|v| v.to_lowercase() != "false")
.unwrap_or(true);
let log_dir = std::env::var("PR_INTELLIGENCE_LOG_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| {
dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("trusty-review")
.join("pr-reviews")
});
Self {
dry_run,
enabled_repos: std::env::var("PR_INTELLIGENCE_ENABLED_REPOS")
.unwrap_or_else(|_| "*".to_string()),
excluded_repos: std::env::var("PR_INTELLIGENCE_EXCLUDED_REPOS").unwrap_or_default(),
excluded_authors: std::env::var("PR_INTELLIGENCE_EXCLUDED_AUTHORS").unwrap_or_default(),
log_dir,
openrouter_api_key: std::env::var("OPENROUTER_API_KEY").unwrap_or_default(),
search_url: std::env::var("TRUSTY_SEARCH_URL")
.unwrap_or_else(|_| "http://localhost:7878".to_string()),
analyzer_url: std::env::var("PR_INTELLIGENCE_ANALYZER_URL")
.unwrap_or_else(|_| "http://localhost:7879".to_string()),
search_index: std::env::var("TRUSTY_SEARCH_INDEX")
.unwrap_or_else(|_| "main".to_string()),
search_index_explicit: std::env::var("TRUSTY_SEARCH_INDEX").is_ok(),
role_models,
github_app_id: std::env::var("GITHUB_APP_ID")
.ok()
.filter(|s| !s.is_empty()),
github_app_private_key: std::env::var("GITHUB_APP_PRIVATE_KEY")
.ok()
.filter(|s| !s.is_empty())
.map(|s| s.replace("\\n", "\n")), github_token: std::env::var("GITHUB_TOKEN").unwrap_or_default(),
github_webhook_secret: std::env::var("GITHUB_WEBHOOK_SECRET").unwrap_or_default(),
github_installations: load_github_installations(),
bot_username: std::env::var("PR_REVIEW_BOT_USERNAME")
.ok()
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| "trusty-review[bot]".to_string()),
live_review_requesters: load_live_review_requesters(),
verification,
context,
context_sources,
apex_index: std::env::var("TRUSTY_SEARCH_APEX_INDEX").unwrap_or_default(),
apex_path_prefixes: load_apex_path_prefixes(),
voice_package: voice::load_voice_package(file_voice),
voice_principles: voice::load_voice_principles(file_voice),
coverage: crate::coverage::CoveragePolicy::from_env_and_file(file_coverage),
}
}
pub fn load(cli_overrides: Option<&RoleCliOverrides>) -> Self {
let default_path = dirs::config_dir().map(|d| d.join("trusty-review").join("config.toml"));
Self::from_env_and_file(default_path.as_deref(), cli_overrides)
}
pub async fn resolve_index(
&mut self,
client: &dyn crate::integrations::search_client::SearchClient,
) {
if self.search_index_explicit {
return;
}
let repo_root = index_resolver::repo_root_from_cwd();
match client.list_indexes().await {
Ok(indexes) => {
if let Some(id) = index_resolver::resolve_index_from_list(&indexes, &repo_root) {
tracing::info!(
index = %id,
repo_root = %repo_root.display(),
"trusty-review: auto-derived search index from repo root"
);
self.search_index = id;
} else {
warn!(
repo_root = %repo_root.display(),
"trusty-review: could not auto-derive search index; \
using fallback \"main\". Set TRUSTY_SEARCH_INDEX to suppress."
);
}
}
Err(e) => {
warn!(
"trusty-review: index auto-derive failed (daemon unreachable?): {e}; \
using fallback \"main\""
);
}
}
}
}
fn load_github_installations() -> Vec<(String, u64)> {
let known = [
("GITHUB_INSTALLATION_ID_DUETTORESEARCH", "duettoresearch"),
("GITHUB_INSTALLATION_ID_HOTSTATS", "hotstats"),
];
let mut installations = Vec::new();
for (env_var, org_name) in &known {
if let Ok(val) = std::env::var(env_var)
&& let Ok(id) = val.trim().parse::<u64>()
{
installations.push((org_name.to_string(), id));
}
}
installations
}
fn load_live_review_requesters() -> Vec<String> {
std::env::var("PR_INTELLIGENCE_LIVE_REVIEW_REQUESTERS")
.ok()
.map(|raw| {
raw.split(',')
.map(|s| s.trim().to_lowercase())
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default()
}
fn load_apex_path_prefixes() -> Vec<String> {
std::env::var("TRUSTY_REVIEW_APEX_PATH_PREFIXES")
.ok()
.map(|raw| {
raw.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default()
}
fn load_toml_file(path: Option<&std::path::Path>) -> Option<TomlFile> {
let path = path?;
match std::fs::read_to_string(path) {
Err(_) => None,
Ok(s) => match toml::from_str::<TomlFile>(&s) {
Ok(f) => Some(f),
Err(e) => {
warn!(?path, "failed to parse config file: {e}");
None
}
},
}
}
#[cfg(test)]
#[path = "config_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "config_resolve_index_tests.rs"]
mod resolve_index_tests;