1 line
34 KiB
JSON
1 line
34 KiB
JSON
{"repo": "arunachalamkalimuthu/merlin-ai-code-review", "files": {"config.example.toml": {"0729aaecc523a068c877c493ef72736856f63bcc": {"t": 1785745335.8266635, "n": 3589, "h": "5016cf6f28e64a1f", "c": "# Merlin configuration — copy to merlin.toml and customize\n# Secrets are loaded from environment variables (never put them here).\n\n[ai]\n# AI provider — choose one:\n#\n# Proprietary\n# \"anthropic\" — Claude via ANTHROPIC_API_KEY (default)\n# \"openai\" — GPT-4o via OPENAI_API_KEY\n# \"claude-code\" — Claude Code CLI (no API key needed; run `claude auth login` once)\n# \"gemini\" — Google Gemini via GEMINI_API_KEY\n# \"bedrock\" — Amazon Bedrock via AWS credentials\n# \"azure-openai\" — Azure OpenAI via AZURE_OPENAI_API_KEY\n#\n# Open-source (hosted)\n# \"groq\" — Llama 3 / Mixtral via GROQ_API_KEY (ultra-fast)\n# \"together-ai\" — 100+ open-source models via TOGETHER_API_KEY\n# \"deep-seek\" — DeepSeek Coder / Chat via DEEPSEEK_API_KEY\n# \"mistral\" — Mistral / Codestral via MISTRAL_API_KEY\n# \"open-router\" — 200+ models via OPENROUTER_API_KEY\n#\n# Self-hosted\n# \"ollama\" — Local Ollama instance (no API key required)\nprovider = \"anthropic\"\n\n# Model to use — override to pin a specific version\n# Anthropic: \"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5-20251001\"\n# OpenAI: \"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\"\n# Gemini: \"gemini-1.5-pro\", \"gemini-1.5-flash\", \"gemini-2.0-flash\"\n# Ollama: any model pulled with `ollama pull` (e.g. \"llama3.1\", \"codellama\", \"mistral\")\nmodel = \"claude-sonnet-4-6\"\n\n# Maximum tokens in the AI response\nmax_tokens = 4096\n\n# Temperature (lower = more deterministic reviews)\ntemperature = 0.2\n\n# (claude-code only) Token for CI headless auth: claude auth login --token <TOKEN>\n# claude_code_token = \"...\"\n\n# (ollama only) Base URL of your Ollama server. Default: http://localhost:11434\n# ollama_base_url = \"http://localhost:11434\"\n\n[review]\n# Which categories to focus on\nfocus = [\"bugs\", \"security\", \"style\", \"performance\"]\n\n# Cap total inline comments per review run (prevents PR spam)\nmax_comments = 30\n\n# Split diff files into chunks of this many lines before sending to AI\nchunk_lines = 200\n\n# Run a second AI pass to filter false positives and refine severity (default: false)\n# reflect = true\n\n[platform]\n# Usually auto-detected from CI environment variables:\n# GitHub Actions → GITHUB_ACTIONS=true\n# GitLab CI → GITLAB_CI=true\n# Bitbucket → BITBUCKET_PIPELINE_UUID\n# Azure DevOps → TF_BUILD=True\n# Gitea Actions → GITEA_ACTIONS=true\n#\n# Uncomment to force a specific platform:\n# type = \"github\" # or \"gitlab\", \"bitbucket\", \"azure-devops\", \"gitea\"\n\n[rag]\n# Enable retrieval-augmented generation for context-aware reviews\nenabled = false\n\n# Vector store backend: \"local\" | \"memory\" | \"qdrant\" | \"chroma\" | \"pinecone\"\nstore = \"local\"\n\n# Embedding model (via Ollama)\nembed_model = \"nomic-embed-text\"\n\n# Ollama server for embeddings\n# ollama_base_url = \"http://localhost:11434\"\n\n# Collection name in the vector store\n# collection = \"merlin\"\n\n# Number of results to retrieve per query\n# top_k = 5\n\n# Minimum similarity score (0.0–1.0)\n# min_score = 0.5\n\n# Path for local JSONL store\n# local_path = \"merlin-rag.jsonl\"\n\n# Qdrant configuration (when store = \"qdrant\")\n# qdrant_url = \"http://localhost:6333\"\n# qdrant_api_key = \"\" # optional for local Qdrant\n\n# ChromaDB configuration (when store = \"chroma\")\n# chroma_url = \"http://localhost:8000\"\n\n# Pinecone configuration (when store = \"pinecone\")\n# pinecone_api_key = \"\" # or set PINECONE_API_KEY env var\n# pinecone_host = \"https://your-index.svc.pinecone.io\"\n\n[agent]\n# Maximum ReAct loop iterations before giving up\n# max_iterations = 10\n\n# Port for Slack/Discord webhook server\n# port = 8090\n"}, "0475e029c6429f372acaf0036abdc52083766899": {"t": 1785750369.967014, "n": 3589, "h": "5016cf6f28e64a1f", "c": "# Merlin configuration — copy to merlin.toml and customize\n# Secrets are loaded from environment variables (never put them here).\n\n[ai]\n# AI provider — choose one:\n#\n# Proprietary\n# \"anthropic\" — Claude via ANTHROPIC_API_KEY (default)\n# \"openai\" — GPT-4o via OPENAI_API_KEY\n# \"claude-code\" — Claude Code CLI (no API key needed; run `claude auth login` once)\n# \"gemini\" — Google Gemini via GEMINI_API_KEY\n# \"bedrock\" — Amazon Bedrock via AWS credentials\n# \"azure-openai\" — Azure OpenAI via AZURE_OPENAI_API_KEY\n#\n# Open-source (hosted)\n# \"groq\" — Llama 3 / Mixtral via GROQ_API_KEY (ultra-fast)\n# \"together-ai\" — 100+ open-source models via TOGETHER_API_KEY\n# \"deep-seek\" — DeepSeek Coder / Chat via DEEPSEEK_API_KEY\n# \"mistral\" — Mistral / Codestral via MISTRAL_API_KEY\n# \"open-router\" — 200+ models via OPENROUTER_API_KEY\n#\n# Self-hosted\n# \"ollama\" — Local Ollama instance (no API key required)\nprovider = \"anthropic\"\n\n# Model to use — override to pin a specific version\n# Anthropic: \"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5-20251001\"\n# OpenAI: \"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\"\n# Gemini: \"gemini-1.5-pro\", \"gemini-1.5-flash\", \"gemini-2.0-flash\"\n# Ollama: any model pulled with `ollama pull` (e.g. \"llama3.1\", \"codellama\", \"mistral\")\nmodel = \"claude-sonnet-4-6\"\n\n# Maximum tokens in the AI response\nmax_tokens = 4096\n\n# Temperature (lower = more deterministic reviews)\ntemperature = 0.2\n\n# (claude-code only) Token for CI headless auth: claude auth login --token <TOKEN>\n# claude_code_token = \"...\"\n\n# (ollama only) Base URL of your Ollama server. Default: http://localhost:11434\n# ollama_base_url = \"http://localhost:11434\"\n\n[review]\n# Which categories to focus on\nfocus = [\"bugs\", \"security\", \"style\", \"performance\"]\n\n# Cap total inline comments per review run (prevents PR spam)\nmax_comments = 30\n\n# Split diff files into chunks of this many lines before sending to AI\nchunk_lines = 200\n\n# Run a second AI pass to filter false positives and refine severity (default: false)\n# reflect = true\n\n[platform]\n# Usually auto-detected from CI environment variables:\n# GitHub Actions → GITHUB_ACTIONS=true\n# GitLab CI → GITLAB_CI=true\n# Bitbucket → BITBUCKET_PIPELINE_UUID\n# Azure DevOps → TF_BUILD=True\n# Gitea Actions → GITEA_ACTIONS=true\n#\n# Uncomment to force a specific platform:\n# type = \"github\" # or \"gitlab\", \"bitbucket\", \"azure-devops\", \"gitea\"\n\n[rag]\n# Enable retrieval-augmented generation for context-aware reviews\nenabled = false\n\n# Vector store backend: \"local\" | \"memory\" | \"qdrant\" | \"chroma\" | \"pinecone\"\nstore = \"local\"\n\n# Embedding model (via Ollama)\nembed_model = \"nomic-embed-text\"\n\n# Ollama server for embeddings\n# ollama_base_url = \"http://localhost:11434\"\n\n# Collection name in the vector store\n# collection = \"merlin\"\n\n# Number of results to retrieve per query\n# top_k = 5\n\n# Minimum similarity score (0.0–1.0)\n# min_score = 0.5\n\n# Path for local JSONL store\n# local_path = \"merlin-rag.jsonl\"\n\n# Qdrant configuration (when store = \"qdrant\")\n# qdrant_url = \"http://localhost:6333\"\n# qdrant_api_key = \"\" # optional for local Qdrant\n\n# ChromaDB configuration (when store = \"chroma\")\n# chroma_url = \"http://localhost:8000\"\n\n# Pinecone configuration (when store = \"pinecone\")\n# pinecone_api_key = \"\" # or set PINECONE_API_KEY env var\n# pinecone_host = \"https://your-index.svc.pinecone.io\"\n\n[agent]\n# Maximum ReAct loop iterations before giving up\n# max_iterations = 10\n\n# Port for Slack/Discord webhook server\n# port = 8090\n"}, "e0b91775c8adc04ad8f1539fd927ab1cd2baf55b": {"t": 1785750371.07255, "n": 3090, "h": "1d8bdf0b44363ed9", "c": "# Merlin configuration — copy to merlin.toml and customize\n# Secrets are loaded from environment variables (never put them here).\n\n[ai]\n# AI provider — choose one:\n# \"anthropic\" — Claude via ANTHROPIC_API_KEY (default)\n# \"openai\" — GPT-4o via OPENAI_API_KEY\n# \"claude-code\" — Claude Code CLI (no API key needed; run `claude auth login` once)\n# \"gemini\" — Google Gemini via GEMINI_API_KEY\n# \"ollama\" — Local Ollama instance (no API key required)\nprovider = \"anthropic\"\n\n# Model to use — override to pin a specific version\n# Anthropic: \"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5-20251001\"\n# OpenAI: \"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\"\n# Gemini: \"gemini-1.5-pro\", \"gemini-1.5-flash\", \"gemini-2.0-flash\"\n# Ollama: any model pulled with `ollama pull` (e.g. \"llama3.1\", \"codellama\", \"mistral\")\nmodel = \"claude-sonnet-4-6\"\n\n# Maximum tokens in the AI response\nmax_tokens = 4096\n\n# Temperature (lower = more deterministic reviews)\ntemperature = 0.2\n\n# (claude-code only) Token for CI headless auth: claude auth login --token <TOKEN>\n# claude_code_token = \"...\"\n\n# (ollama only) Base URL of your Ollama server. Default: http://localhost:11434\n# ollama_base_url = \"http://localhost:11434\"\n\n[review]\n# Which categories to focus on\nfocus = [\"bugs\", \"security\", \"style\", \"performance\"]\n\n# Cap total inline comments per review run (prevents PR spam)\nmax_comments = 30\n\n# Split diff files into chunks of this many lines before sending to AI\nchunk_lines = 200\n\n# Run a second AI pass to filter false positives and refine severity (default: false)\n# reflect = true\n\n[platform]\n# Usually auto-detected from CI environment variables:\n# GitHub Actions → GITHUB_ACTIONS=true\n# GitLab CI → GITLAB_CI=true\n# Bitbucket → BITBUCKET_PIPELINE_UUID\n# Azure DevOps → TF_BUILD=True\n# Gitea Actions → GITEA_ACTIONS=true\n#\n# Uncomment to force a specific platform:\n# type = \"github\" # or \"gitlab\", \"bitbucket\", \"azure-devops\", \"gitea\"\n\n[rag]\n# Enable retrieval-augmented generation for context-aware reviews\nenabled = false\n\n# Vector store backend: \"local\" | \"memory\" | \"qdrant\" | \"chroma\" | \"pinecone\"\nstore = \"local\"\n\n# Embedding model (via Ollama)\nembed_model = \"nomic-embed-text\"\n\n# Ollama server for embeddings\n# ollama_base_url = \"http://localhost:11434\"\n\n# Collection name in the vector store\n# collection = \"merlin\"\n\n# Number of results to retrieve per query\n# top_k = 5\n\n# Minimum similarity score (0.0–1.0)\n# min_score = 0.5\n\n# Path for local JSONL store\n# local_path = \"merlin-rag.jsonl\"\n\n# Qdrant configuration (when store = \"qdrant\")\n# qdrant_url = \"http://localhost:6333\"\n# qdrant_api_key = \"\" # optional for local Qdrant\n\n# ChromaDB configuration (when store = \"chroma\")\n# chroma_url = \"http://localhost:8000\"\n\n# Pinecone configuration (when store = \"pinecone\")\n# pinecone_api_key = \"\" # or set PINECONE_API_KEY env var\n# pinecone_host = \"https://your-index.svc.pinecone.io\"\n\n[agent]\n# Maximum ReAct loop iterations before giving up\n# max_iterations = 10\n\n# Port for Slack/Discord webhook server\n# port = 8090\n"}}, "src/config/mod.rs": {"0729aaecc523a068c877c493ef72736856f63bcc": {"t": 1785745827.4353318, "n": 11152, "h": "fbd9e55bf617fb84", "c": "//! Configuration schema, TOML loading, and environment-variable credential helpers.\n//!\n//! [`Config`] is the root struct that maps directly to `merlin.toml`. Load it\n//! with [`Config::load`] or [`Config::load_default`]; all fields have sensible\n//! defaults so an empty (or missing) file is valid.\n//!\n//! Secrets are read from environment variables — never stored in the config\n//! file. The helper methods on [`Config`] (e.g. [`Config::anthropic_api_key`])\n//! return [`crate::error::MerlinError::EnvVar`] when the required variable is\n//! absent, so callers get a clear error message instead of a panic.\n//!\n//! # Example — minimal `merlin.toml`\n//!\n//! ```toml\n//! [ai]\n//! provider = \"anthropic\"\n//! model = \"claude-sonnet-4-6\"\n//! max_tokens = 4096\n//!\n//! [review]\n//! focus = [\"bugs\", \"security\"]\n//! max_comments = 20\n//! reflect = true\n//! ```\n\npub mod ai;\npub mod integrations;\npub mod platform;\npub mod rag;\npub mod review;\n\npub use ai::{AiConfig, AiProviderType, PersonaConfig};\npub use integrations::{\n AgentConfig, AuditConfig, CoverageConfig, JiraConfig, LinearConfig, SnykConfig,\n};\npub use platform::{PlatformConfig, PlatformType};\npub use rag::{EmbedderType, RagConfig, VectorStoreType};\npub use review::ReviewConfig;\n\nuse std::path::Path;\n\nuse crate::error::{MerlinError, Result};\n\n/// Root configuration struct — the complete contents of `merlin.toml`.\n///\n/// Load with [`Config::load`] or [`Config::load_default`]. All sub-structs\n/// have sensible defaults, so an empty file — or no file at all — is valid.\n///\n/// # Examples\n///\n/// ```no_run\n/// use merlin::config::Config;\n///\n/// let cfg = Config::load_default().unwrap();\n/// assert_eq!(cfg.ai.max_tokens, 4096);\n/// ```\n#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)]\npub struct Config {\n /// AI provider and model settings.\n #[serde(default)]\n pub ai: AiConfig,\n /// Review behaviour settings (focus, chunk size, comment cap).\n #[serde(default)]\n pub review: ReviewConfig,\n /// VCS platform connection settings.\n #[serde(default)]\n pub platform: PlatformConfig,\n /// Jira integration settings.\n #[serde(default)]\n pub jira: JiraConfig,\n /// Linear integration settings.\n #[serde(default)]\n pub linear: LinearConfig,\n /// Code coverage report settings.\n #[serde(default)]\n pub coverage: CoverageConfig,\n /// Audit log settings.\n #[serde(default)]\n pub audit: AuditConfig,\n /// Snyk vulnerability scanning settings.\n #[serde(default)]\n pub snyk: SnykConfig,\n /// Autonomous agent runtime settings.\n #[serde(default)]\n pub agent: AgentConfig,\n /// RAG pipeline settings.\n #[serde(default)]\n pub rag: RagConfig,\n}\n\nimpl Config {\n /// Load config from a TOML file, falling back to all-defaults if the file\n /// is not found.\n ///\n /// # Errors\n ///\n /// Returns [`MerlinError::Io`] if the file exists but cannot be read, or\n /// [`MerlinError::TomlDe`] if the file is not valid TOML.\n pub fn load(path: &Path) -> Result<Self> {\n if !path.exists() {\n tracing::debug!(\"Config file not found at {:?}, using defaults\", path);\n return Ok(Config::default());\n }\n let contents = std::fs::read_to_string(path)?;\n let config: Config = toml::from_str(&contents)?;\n Ok(config)\n }\n\n /// Load from the default path (`merlin.toml` in the current directory).\n ///\n /// Equivalent to `Config::load(Path::new(\"merlin.toml\"))`.\n ///\n /// # Errors\n ///\n /// See [`Config::load`].\n pub fn load_default() -> Result<Self> {\n Self::load(Path::new(\"merlin.toml\"))\n }\n\n /// Return the Anthropic API key from env (`ANTHROPIC_API_KEY`).\n pub fn anthropic_api_key() -> Result<String> {\n std::env::var(\"ANTHROPIC_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"ANTHROPIC_API_KEY\".to_string()))\n }\n\n /// Return the OpenAI API key from env (`OPENAI_API_KEY`).\n pub fn openai_api_key() -> Result<String> {\n std::env::var(\"OPENAI_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"OPENAI_API_KEY\".to_string()))\n }\n\n /// Return the Google Gemini API key from env (`GEMINI_API_KEY`).\n pub fn gemini_api_key() -> Result<String> {\n std::env::var(\"GEMINI_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"GEMINI_API_KEY\".to_string()))\n }\n\n /// Return the Azure OpenAI API key from env (`AZURE_OPENAI_API_KEY`).\n pub fn azure_openai_api_key() -> Result<String> {\n std::env::var(\"AZURE_OPENAI_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"AZURE_OPENAI_API_KEY\".to_string()))\n }\n\n /// Return the Groq API key from env (`GROQ_API_KEY`).\n pub fn groq_api_key() -> Result<String> {\n std::env::var(\"GROQ_API_KEY\").map_err(|_| MerlinError::EnvVar(\"GROQ_API_KEY\".to_string()))\n }\n\n /// Return the Together AI API key from env (`TOGETHER_API_KEY`).\n pub fn together_api_key() -> Result<String> {\n std::env::var(\"TOGETHER_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"TOGETHER_API_KEY\".to_string()))\n }\n\n /// Return the DeepSeek API key from env (`DEEPSEEK_API_KEY`).\n pub fn deepseek_api_key() -> Result<String> {\n std::env::var(\"DEEPSEEK_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"DEEPSEEK_API_KEY\".to_string()))\n }\n\n /// Return the Mistral AI API key from env (`MISTRAL_API_KEY`).\n pub fn mistral_api_key() -> Result<String> {\n std::env::var(\"MISTRAL_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"MISTRAL_API_KEY\".to_string()))\n }\n\n /// Return the OpenRouter API key from env (`OPENROUTER_API_KEY`).\n pub fn openrouter_api_key() -> Result<String> {\n std::env::var(\"OPENROUTER_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"OPENROUTER_API_KEY\".to_string()))\n }\n\n /// Return AWS credentials from env (access key, secret key, optional session token).\n pub fn aws_credentials() -> Result<(String, String, Option<String>)> {\n let access = std::env::var(\"AWS_ACCESS_KEY_ID\")\n .map_err(|_| MerlinError::EnvVar(\"AWS_ACCESS_KEY_ID\".to_string()))?;\n let secret = std::env::var(\"AWS_SECRET_ACCESS_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"AWS_SECRET_ACCESS_KEY\".to_string()))?;\n let token = std::env::var(\"AWS_SESSION_TOKEN\").ok();\n Ok((access, secret, token))\n }\n\n /// Return the Jira API token from env (`JIRA_TOKEN`).\n pub fn jira_token() -> Result<String> {\n std::env::var(\"JIRA_TOKEN\").map_err(|_| MerlinError::EnvVar(\"JIRA_TOKEN\".to_string()))\n }\n\n /// Return the Linear API key from env (`LINEAR_API_KEY`).\n pub fn linear_api_key() -> Result<String> {\n std::env::var(\"LINEAR_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"LINEAR_API_KEY\".to_string()))\n }\n\n /// Return the GitHub token from env (`GITHUB_TOKEN`).\n pub fn github_token() -> Result<String> {\n std::env::var(\"GITHUB_TOKEN\").map_err(|_| MerlinError::EnvVar(\"GITHUB_TOKEN\".to_string()))\n }\n\n /// Return the GitLab token from env (`GITLAB_TOKEN`).\n pub fn gitlab_token() -> Result<String> {\n std::env::var(\"GITLAB_TOKEN\").map_err(|_| MerlinError::EnvVar(\"GITLAB_TOKEN\".to_string()))\n }\n\n /// Return the Bitbucket token from env (`BITBUCKET_TOKEN` or `BITBUCKET_APP_PASSWORD`).\n pub fn bitbucket_token() -> Result<String> {\n std::env::var(\"BITBUCKET_TOKEN\")\n .or_else(|_| std::env::var(\"BITBUCKET_APP_PASSWORD\"))\n .map_err(|_| MerlinError::EnvVar(\"BITBUCKET_TOKEN\".to_string()))\n }\n\n /// Return the Azure DevOps PAT from env (`AZURE_DEVOPS_TOKEN` or `SYSTEM_ACCESSTOKEN`).\n pub fn azure_devops_token() -> Result<String> {\n std::env::var(\"AZURE_DEVOPS_TOKEN\")\n .or_else(|_| std::env::var(\"SYSTEM_ACCESSTOKEN\"))\n .map_err(|_| MerlinError::EnvVar(\"AZURE_DEVOPS_TOKEN\".to_string()))\n }\n\n /// Return the Gitea token from env (`GITEA_TOKEN`).\n pub fn gitea_token() -> Result<String> {\n std::env::var(\"GITEA_TOKEN\").map_err(|_| MerlinError::EnvVar(\"GITEA_TOKEN\".to_string()))\n }\n\n /// Return the Snyk API token from env (`SNYK_TOKEN`).\n pub fn snyk_token() -> Result<String> {\n std::env::var(\"SNYK_TOKEN\").map_err(|_| MerlinError::EnvVar(\"SNYK_TOKEN\".to_string()))\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use std::io::Write;\n\n #[test]\n fn test_default_config() {\n let cfg = Config::default();\n assert_eq!(cfg.ai.max_tokens, 4096);\n assert_eq!(cfg.review.max_comments, 30);\n assert_eq!(cfg.review.chunk_lines, 200);\n assert_eq!(cfg.review.focus.len(), 4);\n assert!(!cfg.review.reflect);\n assert!(cfg.jira.base_url.is_none());\n assert!(cfg.coverage.threshold == 0.0);\n assert!(cfg.audit.enabled);\n }\n\n #[test]\n fn test_load_toml() {\n let toml = r#\"\n[ai]\nprovider = \"openai\"\nmodel = \"gpt-4o\"\nmax_tokens = 2048\ntemperature = 0.5\n\n[review]\nmax_comments = 10\nchunk_lines = 100\nfocus = [\"bugs\", \"security\"]\nreflect = true\n\n[review.persona]\nname = \"security-focused\"\nsystem_prompt_extra = \"Be extra strict about security.\"\n\n[jira]\nbase_url = \"https://company.atlassian.net\"\nproject_key = \"PROJ\"\n\"#;\n let cfg: Config = toml::from_str(toml).expect(\"parse failed\");\n assert!(matches!(cfg.ai.provider, AiProviderType::Openai));\n assert_eq!(cfg.ai.model, \"gpt-4o\");\n assert_eq!(cfg.review.max_comments, 10);\n assert_eq!(cfg.review.focus, vec![\"bugs\", \"security\"]);\n assert!(cfg.review.reflect);\n assert_eq!(cfg.review.persona.name.as_deref(), Some(\"security-focused\"));\n assert_eq!(cfg.jira.project_key.as_deref(), Some(\"PROJ\"));\n }\n\n #[test]\n fn test_load_from_missing_file() {\n let result = Config::load(Path::new(\"/tmp/merlin_nonexistent_xyz.toml\"));\n assert!(result.is_ok());\n }\n\n #[test]\n fn test_load_from_file() {\n let mut f = tempfile::NamedTempFile::new().unwrap();\n write!(f, \"[ai]\\nprovider = \\\"anthropic\\\"\\n\").unwrap();\n let cfg = Config::load(f.path()).unwrap();\n assert!(matches!(cfg.ai.provider, AiProviderType::Anthropic));\n }\n\n #[test]\n fn test_azure_openai_config() {\n let toml = r#\"\n[ai]\nprovider = \"azure-openai\"\nmodel = \"gpt-4o\"\nazure_openai_endpoint = \"https://myresource.openai.azure.com\"\nazure_openai_api_version = \"2024-02-01\"\n\"#;\n let cfg: Config = toml::from_str(toml).expect(\"parse failed\");\n assert!(matches!(cfg.ai.provider, AiProviderType::AzureOpenai));\n assert_eq!(\n cfg.ai.azure_openai_endpoint.as_deref(),\n Some(\"https://myresource.openai.azure.com\")\n );\n }\n\n #[test]\n fn test_bedrock_config() {\n let toml = r#\"\n[ai]\nprovider = \"bedrock\"\nmodel = \"anthropic.claude-sonnet-4-6-20250514-v1:0\"\nbedrock_region = \"us-east-1\"\n\"#;\n let cfg: Config = toml::from_str(toml).expect(\"parse failed\");\n assert!(matches!(cfg.ai.provider, AiProviderType::Bedrock));\n assert_eq!(cfg.ai.bedrock_region.as_deref(), Some(\"us-east-1\"));\n }\n}\n"}, "c589e43d69671f053e8eaaf7813c544a6e3772c3": {"t": 1785750371.667212, "n": 11152, "h": "fbd9e55bf617fb84", "c": "//! Configuration schema, TOML loading, and environment-variable credential helpers.\n//!\n//! [`Config`] is the root struct that maps directly to `merlin.toml`. Load it\n//! with [`Config::load`] or [`Config::load_default`]; all fields have sensible\n//! defaults so an empty (or missing) file is valid.\n//!\n//! Secrets are read from environment variables — never stored in the config\n//! file. The helper methods on [`Config`] (e.g. [`Config::anthropic_api_key`])\n//! return [`crate::error::MerlinError::EnvVar`] when the required variable is\n//! absent, so callers get a clear error message instead of a panic.\n//!\n//! # Example — minimal `merlin.toml`\n//!\n//! ```toml\n//! [ai]\n//! provider = \"anthropic\"\n//! model = \"claude-sonnet-4-6\"\n//! max_tokens = 4096\n//!\n//! [review]\n//! focus = [\"bugs\", \"security\"]\n//! max_comments = 20\n//! reflect = true\n//! ```\n\npub mod ai;\npub mod integrations;\npub mod platform;\npub mod rag;\npub mod review;\n\npub use ai::{AiConfig, AiProviderType, PersonaConfig};\npub use integrations::{\n AgentConfig, AuditConfig, CoverageConfig, JiraConfig, LinearConfig, SnykConfig,\n};\npub use platform::{PlatformConfig, PlatformType};\npub use rag::{EmbedderType, RagConfig, VectorStoreType};\npub use review::ReviewConfig;\n\nuse std::path::Path;\n\nuse crate::error::{MerlinError, Result};\n\n/// Root configuration struct — the complete contents of `merlin.toml`.\n///\n/// Load with [`Config::load`] or [`Config::load_default`]. All sub-structs\n/// have sensible defaults, so an empty file — or no file at all — is valid.\n///\n/// # Examples\n///\n/// ```no_run\n/// use merlin::config::Config;\n///\n/// let cfg = Config::load_default().unwrap();\n/// assert_eq!(cfg.ai.max_tokens, 4096);\n/// ```\n#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)]\npub struct Config {\n /// AI provider and model settings.\n #[serde(default)]\n pub ai: AiConfig,\n /// Review behaviour settings (focus, chunk size, comment cap).\n #[serde(default)]\n pub review: ReviewConfig,\n /// VCS platform connection settings.\n #[serde(default)]\n pub platform: PlatformConfig,\n /// Jira integration settings.\n #[serde(default)]\n pub jira: JiraConfig,\n /// Linear integration settings.\n #[serde(default)]\n pub linear: LinearConfig,\n /// Code coverage report settings.\n #[serde(default)]\n pub coverage: CoverageConfig,\n /// Audit log settings.\n #[serde(default)]\n pub audit: AuditConfig,\n /// Snyk vulnerability scanning settings.\n #[serde(default)]\n pub snyk: SnykConfig,\n /// Autonomous agent runtime settings.\n #[serde(default)]\n pub agent: AgentConfig,\n /// RAG pipeline settings.\n #[serde(default)]\n pub rag: RagConfig,\n}\n\nimpl Config {\n /// Load config from a TOML file, falling back to all-defaults if the file\n /// is not found.\n ///\n /// # Errors\n ///\n /// Returns [`MerlinError::Io`] if the file exists but cannot be read, or\n /// [`MerlinError::TomlDe`] if the file is not valid TOML.\n pub fn load(path: &Path) -> Result<Self> {\n if !path.exists() {\n tracing::debug!(\"Config file not found at {:?}, using defaults\", path);\n return Ok(Config::default());\n }\n let contents = std::fs::read_to_string(path)?;\n let config: Config = toml::from_str(&contents)?;\n Ok(config)\n }\n\n /// Load from the default path (`merlin.toml` in the current directory).\n ///\n /// Equivalent to `Config::load(Path::new(\"merlin.toml\"))`.\n ///\n /// # Errors\n ///\n /// See [`Config::load`].\n pub fn load_default() -> Result<Self> {\n Self::load(Path::new(\"merlin.toml\"))\n }\n\n /// Return the Anthropic API key from env (`ANTHROPIC_API_KEY`).\n pub fn anthropic_api_key() -> Result<String> {\n std::env::var(\"ANTHROPIC_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"ANTHROPIC_API_KEY\".to_string()))\n }\n\n /// Return the OpenAI API key from env (`OPENAI_API_KEY`).\n pub fn openai_api_key() -> Result<String> {\n std::env::var(\"OPENAI_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"OPENAI_API_KEY\".to_string()))\n }\n\n /// Return the Google Gemini API key from env (`GEMINI_API_KEY`).\n pub fn gemini_api_key() -> Result<String> {\n std::env::var(\"GEMINI_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"GEMINI_API_KEY\".to_string()))\n }\n\n /// Return the Azure OpenAI API key from env (`AZURE_OPENAI_API_KEY`).\n pub fn azure_openai_api_key() -> Result<String> {\n std::env::var(\"AZURE_OPENAI_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"AZURE_OPENAI_API_KEY\".to_string()))\n }\n\n /// Return the Groq API key from env (`GROQ_API_KEY`).\n pub fn groq_api_key() -> Result<String> {\n std::env::var(\"GROQ_API_KEY\").map_err(|_| MerlinError::EnvVar(\"GROQ_API_KEY\".to_string()))\n }\n\n /// Return the Together AI API key from env (`TOGETHER_API_KEY`).\n pub fn together_api_key() -> Result<String> {\n std::env::var(\"TOGETHER_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"TOGETHER_API_KEY\".to_string()))\n }\n\n /// Return the DeepSeek API key from env (`DEEPSEEK_API_KEY`).\n pub fn deepseek_api_key() -> Result<String> {\n std::env::var(\"DEEPSEEK_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"DEEPSEEK_API_KEY\".to_string()))\n }\n\n /// Return the Mistral AI API key from env (`MISTRAL_API_KEY`).\n pub fn mistral_api_key() -> Result<String> {\n std::env::var(\"MISTRAL_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"MISTRAL_API_KEY\".to_string()))\n }\n\n /// Return the OpenRouter API key from env (`OPENROUTER_API_KEY`).\n pub fn openrouter_api_key() -> Result<String> {\n std::env::var(\"OPENROUTER_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"OPENROUTER_API_KEY\".to_string()))\n }\n\n /// Return AWS credentials from env (access key, secret key, optional session token).\n pub fn aws_credentials() -> Result<(String, String, Option<String>)> {\n let access = std::env::var(\"AWS_ACCESS_KEY_ID\")\n .map_err(|_| MerlinError::EnvVar(\"AWS_ACCESS_KEY_ID\".to_string()))?;\n let secret = std::env::var(\"AWS_SECRET_ACCESS_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"AWS_SECRET_ACCESS_KEY\".to_string()))?;\n let token = std::env::var(\"AWS_SESSION_TOKEN\").ok();\n Ok((access, secret, token))\n }\n\n /// Return the Jira API token from env (`JIRA_TOKEN`).\n pub fn jira_token() -> Result<String> {\n std::env::var(\"JIRA_TOKEN\").map_err(|_| MerlinError::EnvVar(\"JIRA_TOKEN\".to_string()))\n }\n\n /// Return the Linear API key from env (`LINEAR_API_KEY`).\n pub fn linear_api_key() -> Result<String> {\n std::env::var(\"LINEAR_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"LINEAR_API_KEY\".to_string()))\n }\n\n /// Return the GitHub token from env (`GITHUB_TOKEN`).\n pub fn github_token() -> Result<String> {\n std::env::var(\"GITHUB_TOKEN\").map_err(|_| MerlinError::EnvVar(\"GITHUB_TOKEN\".to_string()))\n }\n\n /// Return the GitLab token from env (`GITLAB_TOKEN`).\n pub fn gitlab_token() -> Result<String> {\n std::env::var(\"GITLAB_TOKEN\").map_err(|_| MerlinError::EnvVar(\"GITLAB_TOKEN\".to_string()))\n }\n\n /// Return the Bitbucket token from env (`BITBUCKET_TOKEN` or `BITBUCKET_APP_PASSWORD`).\n pub fn bitbucket_token() -> Result<String> {\n std::env::var(\"BITBUCKET_TOKEN\")\n .or_else(|_| std::env::var(\"BITBUCKET_APP_PASSWORD\"))\n .map_err(|_| MerlinError::EnvVar(\"BITBUCKET_TOKEN\".to_string()))\n }\n\n /// Return the Azure DevOps PAT from env (`AZURE_DEVOPS_TOKEN` or `SYSTEM_ACCESSTOKEN`).\n pub fn azure_devops_token() -> Result<String> {\n std::env::var(\"AZURE_DEVOPS_TOKEN\")\n .or_else(|_| std::env::var(\"SYSTEM_ACCESSTOKEN\"))\n .map_err(|_| MerlinError::EnvVar(\"AZURE_DEVOPS_TOKEN\".to_string()))\n }\n\n /// Return the Gitea token from env (`GITEA_TOKEN`).\n pub fn gitea_token() -> Result<String> {\n std::env::var(\"GITEA_TOKEN\").map_err(|_| MerlinError::EnvVar(\"GITEA_TOKEN\".to_string()))\n }\n\n /// Return the Snyk API token from env (`SNYK_TOKEN`).\n pub fn snyk_token() -> Result<String> {\n std::env::var(\"SNYK_TOKEN\").map_err(|_| MerlinError::EnvVar(\"SNYK_TOKEN\".to_string()))\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use std::io::Write;\n\n #[test]\n fn test_default_config() {\n let cfg = Config::default();\n assert_eq!(cfg.ai.max_tokens, 4096);\n assert_eq!(cfg.review.max_comments, 30);\n assert_eq!(cfg.review.chunk_lines, 200);\n assert_eq!(cfg.review.focus.len(), 4);\n assert!(!cfg.review.reflect);\n assert!(cfg.jira.base_url.is_none());\n assert!(cfg.coverage.threshold == 0.0);\n assert!(cfg.audit.enabled);\n }\n\n #[test]\n fn test_load_toml() {\n let toml = r#\"\n[ai]\nprovider = \"openai\"\nmodel = \"gpt-4o\"\nmax_tokens = 2048\ntemperature = 0.5\n\n[review]\nmax_comments = 10\nchunk_lines = 100\nfocus = [\"bugs\", \"security\"]\nreflect = true\n\n[review.persona]\nname = \"security-focused\"\nsystem_prompt_extra = \"Be extra strict about security.\"\n\n[jira]\nbase_url = \"https://company.atlassian.net\"\nproject_key = \"PROJ\"\n\"#;\n let cfg: Config = toml::from_str(toml).expect(\"parse failed\");\n assert!(matches!(cfg.ai.provider, AiProviderType::Openai));\n assert_eq!(cfg.ai.model, \"gpt-4o\");\n assert_eq!(cfg.review.max_comments, 10);\n assert_eq!(cfg.review.focus, vec![\"bugs\", \"security\"]);\n assert!(cfg.review.reflect);\n assert_eq!(cfg.review.persona.name.as_deref(), Some(\"security-focused\"));\n assert_eq!(cfg.jira.project_key.as_deref(), Some(\"PROJ\"));\n }\n\n #[test]\n fn test_load_from_missing_file() {\n let result = Config::load(Path::new(\"/tmp/merlin_nonexistent_xyz.toml\"));\n assert!(result.is_ok());\n }\n\n #[test]\n fn test_load_from_file() {\n let mut f = tempfile::NamedTempFile::new().unwrap();\n write!(f, \"[ai]\\nprovider = \\\"anthropic\\\"\\n\").unwrap();\n let cfg = Config::load(f.path()).unwrap();\n assert!(matches!(cfg.ai.provider, AiProviderType::Anthropic));\n }\n\n #[test]\n fn test_azure_openai_config() {\n let toml = r#\"\n[ai]\nprovider = \"azure-openai\"\nmodel = \"gpt-4o\"\nazure_openai_endpoint = \"https://myresource.openai.azure.com\"\nazure_openai_api_version = \"2024-02-01\"\n\"#;\n let cfg: Config = toml::from_str(toml).expect(\"parse failed\");\n assert!(matches!(cfg.ai.provider, AiProviderType::AzureOpenai));\n assert_eq!(\n cfg.ai.azure_openai_endpoint.as_deref(),\n Some(\"https://myresource.openai.azure.com\")\n );\n }\n\n #[test]\n fn test_bedrock_config() {\n let toml = r#\"\n[ai]\nprovider = \"bedrock\"\nmodel = \"anthropic.claude-sonnet-4-6-20250514-v1:0\"\nbedrock_region = \"us-east-1\"\n\"#;\n let cfg: Config = toml::from_str(toml).expect(\"parse failed\");\n assert!(matches!(cfg.ai.provider, AiProviderType::Bedrock));\n assert_eq!(cfg.ai.bedrock_region.as_deref(), Some(\"us-east-1\"));\n }\n}\n"}}}} |