use clap::ValueEnum;
use serde::Deserialize;
use std::fmt;
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum AiProvider {
Claude,
Cursor,
Copilot,
Ollama,
Anthropic,
#[serde(alias = "openai")]
OpenAi,
Custom,
}
impl AiProvider {
#[allow(dead_code)]
pub fn default_command(&self) -> Option<&'static str> {
match self {
AiProvider::Claude => Some("claude -p --output-format text"),
AiProvider::Ollama => Some("ollama run llama3"),
AiProvider::Copilot => Some("gh copilot suggest -t shell"),
AiProvider::Cursor
| AiProvider::Custom
| AiProvider::Anthropic
| AiProvider::OpenAi => None,
}
}
pub fn binary_name(&self) -> &'static str {
match self {
AiProvider::Claude => "claude",
AiProvider::Cursor => "cursor",
AiProvider::Copilot => "gh",
AiProvider::Ollama => "ollama",
AiProvider::Anthropic | AiProvider::OpenAi | AiProvider::Custom => "",
}
}
pub fn is_api_provider(&self) -> bool {
matches!(self, AiProvider::Anthropic | AiProvider::OpenAi)
}
pub fn api_key_env_var(&self) -> Option<&'static str> {
match self {
AiProvider::Anthropic => Some("ANTHROPIC_API_KEY"),
AiProvider::OpenAi => Some("OPENAI_API_KEY"),
_ => None,
}
}
pub fn default_model(&self) -> Option<&'static str> {
match self {
AiProvider::Anthropic => Some("claude-sonnet-4-20250514"),
AiProvider::OpenAi => Some("gpt-4o"),
_ => None,
}
}
pub fn from_str_loose(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"claude" => Some(AiProvider::Claude),
"cursor" => Some(AiProvider::Cursor),
"copilot" | "gh-copilot" => Some(AiProvider::Copilot),
"ollama" => Some(AiProvider::Ollama),
"anthropic" | "anthropic-api" => Some(AiProvider::Anthropic),
"openai" | "openai-api" => Some(AiProvider::OpenAi),
_ => None,
}
}
pub fn detection_order() -> &'static [AiProvider] {
&[
AiProvider::Claude,
AiProvider::Ollama,
AiProvider::Copilot,
AiProvider::Anthropic,
AiProvider::OpenAi,
]
}
}
impl fmt::Display for AiProvider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AiProvider::Claude => write!(f, "claude"),
AiProvider::Cursor => write!(f, "cursor"),
AiProvider::Copilot => write!(f, "copilot"),
AiProvider::Ollama => write!(f, "ollama"),
AiProvider::Anthropic => write!(f, "anthropic"),
AiProvider::OpenAi => write!(f, "openai"),
AiProvider::Custom => write!(f, "custom"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
pub enum OutputFormat {
#[default]
Text,
Json,
Markdown,
}
#[derive(Debug, Default, Clone)]
pub struct Frontmatter {
pub module: Option<String>,
pub version: Option<String>,
pub status: Option<String>,
pub files: Vec<String>,
pub db_tables: Vec<String>,
pub depends_on: Vec<String>,
}
#[derive(Debug)]
pub struct ValidationResult {
pub spec_path: String,
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub export_summary: Option<String>,
pub fixes: Vec<String>,
}
impl ValidationResult {
pub fn new(spec_path: String) -> Self {
Self {
spec_path,
errors: Vec::new(),
warnings: Vec::new(),
export_summary: None,
fixes: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct CoverageReport {
pub total_source_files: usize,
pub specced_file_count: usize,
pub unspecced_files: Vec<String>,
pub unspecced_modules: Vec<String>,
pub coverage_percent: usize,
pub total_loc: usize,
pub specced_loc: usize,
pub loc_coverage_percent: usize,
pub unspecced_file_loc: Vec<(String, usize)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ExportLevel {
Type,
#[default]
Member,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpecSyncConfig {
#[serde(default = "default_specs_dir")]
pub specs_dir: String,
#[serde(default = "default_source_dirs")]
pub source_dirs: Vec<String>,
pub schema_dir: Option<String>,
pub schema_pattern: Option<String>,
#[serde(default = "default_required_sections")]
pub required_sections: Vec<String>,
#[serde(default = "default_exclude_dirs")]
pub exclude_dirs: Vec<String>,
#[serde(default = "default_exclude_patterns")]
pub exclude_patterns: Vec<String>,
#[serde(default)]
pub source_extensions: Vec<String>,
#[serde(default)]
pub export_level: ExportLevel,
#[serde(default)]
pub modules: std::collections::HashMap<String, ModuleDefinition>,
#[serde(default)]
pub ai_provider: Option<AiProvider>,
#[serde(default)]
pub ai_model: Option<String>,
#[serde(default)]
pub ai_command: Option<String>,
#[serde(default)]
pub ai_api_key: Option<String>,
#[serde(default)]
pub ai_base_url: Option<String>,
#[serde(default)]
pub ai_timeout: Option<u64>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
pub struct ModuleDefinition {
#[serde(default)]
pub files: Vec<String>,
#[serde(default)]
pub depends_on: Vec<String>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct RegistryEntry {
pub name: String,
pub specs: Vec<(String, String)>, }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Language {
TypeScript,
Rust,
Go,
Python,
Swift,
Kotlin,
Java,
CSharp,
Dart,
}
impl Language {
pub fn from_extension(ext: &str) -> Option<Self> {
match ext {
"ts" | "tsx" | "js" | "jsx" | "mts" | "cts" => Some(Language::TypeScript),
"rs" => Some(Language::Rust),
"go" => Some(Language::Go),
"py" => Some(Language::Python),
"swift" => Some(Language::Swift),
"kt" | "kts" => Some(Language::Kotlin),
"java" => Some(Language::Java),
"cs" => Some(Language::CSharp),
"dart" => Some(Language::Dart),
_ => None,
}
}
#[allow(dead_code)]
pub fn extensions(&self) -> &[&str] {
match self {
Language::TypeScript => &["ts", "tsx", "js", "jsx", "mts", "cts"],
Language::Rust => &["rs"],
Language::Go => &["go"],
Language::Python => &["py"],
Language::Swift => &["swift"],
Language::Kotlin => &["kt", "kts"],
Language::Java => &["java"],
Language::CSharp => &["cs"],
Language::Dart => &["dart"],
}
}
pub fn test_patterns(&self) -> &[&str] {
match self {
Language::TypeScript => &[".test.ts", ".spec.ts", ".test.tsx", ".spec.tsx", ".d.ts"],
Language::Rust => &[], Language::Go => &["_test.go"],
Language::Python => &["test_", "_test.py"],
Language::Swift => &[
"Tests.swift",
"Test.swift",
"Spec.swift",
"Specs.swift",
"Mock.swift",
"Mocks.swift",
"Stub.swift",
"Fake.swift",
],
Language::Kotlin => &[
"Test.kt", "Tests.kt", "Spec.kt", "Specs.kt", "Mock.kt", "Fake.kt",
],
Language::Java => &[
"Test.java",
"Tests.java",
"Spec.java",
"Mock.java",
"IT.java",
],
Language::CSharp => &["Tests.cs", "Test.cs", "Spec.cs", "Mock.cs"],
Language::Dart => &["_test.dart"],
}
}
}
fn default_specs_dir() -> String {
"specs".to_string()
}
fn default_source_dirs() -> Vec<String> {
vec!["src".to_string()]
}
fn default_required_sections() -> Vec<String> {
vec![
"Purpose".to_string(),
"Public API".to_string(),
"Invariants".to_string(),
"Behavioral Examples".to_string(),
"Error Cases".to_string(),
"Dependencies".to_string(),
"Change Log".to_string(),
]
}
fn default_exclude_dirs() -> Vec<String> {
vec!["__tests__".to_string()]
}
fn default_exclude_patterns() -> Vec<String> {
vec![
"**/__tests__/**".to_string(),
"**/*.test.ts".to_string(),
"**/*.spec.ts".to_string(),
]
}
impl Default for SpecSyncConfig {
fn default() -> Self {
Self {
specs_dir: default_specs_dir(),
source_dirs: default_source_dirs(),
schema_dir: None,
schema_pattern: None,
required_sections: default_required_sections(),
exclude_dirs: default_exclude_dirs(),
exclude_patterns: default_exclude_patterns(),
source_extensions: Vec::new(),
export_level: ExportLevel::default(),
modules: std::collections::HashMap::new(),
ai_provider: None,
ai_model: None,
ai_command: None,
ai_api_key: None,
ai_base_url: None,
ai_timeout: None,
}
}
}