use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReadinessStatus {
Complete,
Placeholder,
Missing,
NeedsReview,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum ReadinessCategory {
Critical,
Important,
NiceToHave,
}
impl std::fmt::Display for ReadinessCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReadinessCategory::Critical => write!(f, "Critical"),
ReadinessCategory::Important => write!(f, "Important"),
ReadinessCategory::NiceToHave => write!(f, "Nice-to-Have"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadinessRequirement {
pub id: String,
pub name: String,
pub description: String,
pub category: ReadinessCategory,
pub status: ReadinessStatus,
pub components: Vec<String>,
pub dependencies: Vec<String>,
pub effort_hours: Option<u32>,
pub priority: u8,
pub last_assessed: DateTime<Utc>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadinessReport {
pub project_name: String,
pub generated_at: DateTime<Utc>,
pub overall_score: f64,
pub by_category: BTreeMap<ReadinessCategory, CategoryReport>,
pub requirements: Vec<ReadinessRequirement>,
pub blocking_requirements: Vec<String>,
pub next_steps: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategoryReport {
pub category: ReadinessCategory,
pub total_requirements: usize,
pub completed: usize,
pub placeholders: usize,
pub missing: usize,
pub score: f64,
pub requirements: Vec<String>,
}
#[derive(Error, Debug)]
pub enum ProductionError {
#[error("Failed to load readiness configuration: {0}")]
ConfigLoad(#[from] std::io::Error),
#[error("Failed to parse readiness configuration: {0}")]
ConfigParse(#[from] toml::de::Error),
#[error("Failed to serialize readiness configuration: {0}")]
ConfigSerialize(#[from] toml::ser::Error),
#[error("Requirement not found: {0}")]
RequirementNotFound(String),
#[error("Circular dependency detected in requirements")]
CircularDependency,
#[error("Invalid requirement status transition")]
InvalidTransition,
#[error("Project analysis failed: {0}")]
AnalysisFailed(String),
}
pub type Result<T> = std::result::Result<T, ProductionError>;
pub struct ReadinessTracker {
project_root: std::path::PathBuf,
requirements: Vec<ReadinessRequirement>,
config_path: std::path::PathBuf,
}
impl ReadinessTracker {
pub fn new<P: AsRef<Path>>(project_root: P) -> Self {
let project_root = project_root.as_ref().to_path_buf();
let config_path = project_root.join(".ggen").join("production.toml");
Self {
project_root,
requirements: Vec::new(),
config_path,
}
}
pub fn load(&mut self) -> Result<()> {
if self.config_path.exists() {
let content = std::fs::read_to_string(&self.config_path)?;
let config: ProductionConfig = toml::from_str(&content)?;
self.requirements = config.requirements;
} else {
self.requirements = Self::default_requirements();
}
Ok(())
}
pub fn save(&self) -> Result<()> {
let config = ProductionConfig {
requirements: self.requirements.clone(),
};
let parent_dir = self.config_path.parent().ok_or_else(|| {
ProductionError::ConfigLoad(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Config path has no parent directory: {}",
self.config_path.display()
),
))
})?;
std::fs::create_dir_all(parent_dir)?;
let content = toml::to_string_pretty(&config)?;
std::fs::write(&self.config_path, content)?;
Ok(())
}
pub fn generate_report(&self) -> ReadinessReport {
let mut by_category = BTreeMap::new();
let mut blocking_requirements = Vec::new();
let mut next_steps = Vec::new();
for req in &self.requirements {
let category = req.category.clone();
let entry = by_category
.entry(category)
.or_insert_with(|| CategoryReport {
category: req.category.clone(),
total_requirements: 0,
completed: 0,
placeholders: 0,
missing: 0,
score: 0.0,
requirements: Vec::new(),
});
entry.total_requirements += 1;
entry.requirements.push(req.id.clone());
match req.status {
ReadinessStatus::Complete => entry.completed += 1,
ReadinessStatus::Placeholder => entry.placeholders += 1,
ReadinessStatus::Missing => entry.missing += 1,
ReadinessStatus::NeedsReview => {
if req.category == ReadinessCategory::Critical {
blocking_requirements.push(req.id.clone());
}
}
}
}
for report in by_category.values_mut() {
let total = report.total_requirements as f64;
if total > 0.0 {
let completed_ratio = report.completed as f64 / total;
let placeholder_ratio = report.placeholders as f64 / total;
report.score = (completed_ratio * 1.0 + placeholder_ratio * 0.5) * 100.0;
}
}
let critical_score = by_category
.get(&ReadinessCategory::Critical)
.map(|r| r.score)
.unwrap_or(0.0)
* 0.8; let important_score = by_category
.get(&ReadinessCategory::Important)
.map(|r| r.score)
.unwrap_or(0.0)
* 0.15; let nice_score = by_category
.get(&ReadinessCategory::NiceToHave)
.map(|r| r.score)
.unwrap_or(0.0)
* 0.05;
let overall_score = critical_score + important_score + nice_score;
for req in &self.requirements {
if req.category == ReadinessCategory::Critical
&& (req.status == ReadinessStatus::Missing
|| req.status == ReadinessStatus::NeedsReview)
{
next_steps.push(format!("Implement {}: {}", req.name, req.description));
}
}
ReadinessReport {
project_name: crate::lifecycle::model::defaults::DEFAULT_READINESS_PROJECT_NAME
.to_string(),
generated_at: Utc::now(),
overall_score,
by_category,
requirements: self.requirements.clone(),
blocking_requirements,
next_steps,
}
}
pub fn update_requirement(
&mut self, requirement_id: &str, status: ReadinessStatus,
) -> Result<()> {
let req_index = self
.requirements
.iter()
.position(|r| r.id == requirement_id)
.ok_or_else(|| ProductionError::RequirementNotFound(requirement_id.to_string()))?;
let current_status = self.requirements[req_index].status.clone();
Self::validate_transition_static(¤t_status, &status)?;
self.requirements[req_index].status = status;
self.requirements[req_index].last_assessed = Utc::now();
Ok(())
}
pub fn add_requirement(&mut self, requirement: ReadinessRequirement) -> Result<()> {
self.validate_dependencies(&requirement)?;
self.requirements.push(requirement);
Ok(())
}
pub fn get_by_status(&self, status: &ReadinessStatus) -> Vec<&ReadinessRequirement> {
self.requirements
.iter()
.filter(|r| &r.status == status)
.collect()
}
pub fn get_by_category(&self, category: &ReadinessCategory) -> Vec<&ReadinessRequirement> {
self.requirements
.iter()
.filter(|r| r.category == *category)
.collect()
}
fn validate_transition_static(from: &ReadinessStatus, to: &ReadinessStatus) -> Result<()> {
match (from, to) {
(ReadinessStatus::Missing, ReadinessStatus::Placeholder) => Ok(()),
(ReadinessStatus::Missing, ReadinessStatus::Complete) => Ok(()),
(ReadinessStatus::Placeholder, ReadinessStatus::Complete) => Ok(()),
(ReadinessStatus::Placeholder, ReadinessStatus::NeedsReview) => Ok(()),
(ReadinessStatus::Complete, ReadinessStatus::NeedsReview) => Ok(()),
_ => Err(ProductionError::InvalidTransition),
}
}
fn validate_dependencies(&self, requirement: &ReadinessRequirement) -> Result<()> {
let mut visited = std::collections::HashSet::new();
let mut path = Vec::new();
for dep_id in &requirement.dependencies {
if self.has_cycle(dep_id, &mut visited, &mut path) {
return Err(ProductionError::CircularDependency);
}
}
Ok(())
}
fn has_cycle(
&self, req_id: &str, visited: &mut std::collections::HashSet<String>,
path: &mut Vec<String>,
) -> bool {
if path.contains(&req_id.to_string()) {
return true; }
if visited.contains(req_id) {
return false; }
let req = match self.requirements.iter().find(|r| r.id == req_id) {
Some(r) => r,
None => return false,
};
visited.insert(req_id.to_string());
path.push(req_id.to_string());
for dep_id in &req.dependencies {
if self.has_cycle(dep_id, visited, path) {
return true;
}
}
path.pop();
false
}
fn default_requirements() -> Vec<ReadinessRequirement> {
vec![
ReadinessRequirement {
id: "auth-basic".to_string(),
name: "Basic Authentication".to_string(),
description: "User authentication system with login/logout".to_string(),
category: ReadinessCategory::Critical,
status: ReadinessStatus::Missing,
components: vec!["src/auth.rs".to_string(), "templates/auth.tmpl".to_string()],
dependencies: vec![],
effort_hours: Some(8),
priority: 10,
last_assessed: Utc::now(),
notes: Some("Core authentication is essential for production".to_string()),
},
ReadinessRequirement {
id: "error-handling".to_string(),
name: "Comprehensive Error Handling".to_string(),
description:
"Proper error handling with thiserror, no unwrap/expect in production code"
.to_string(),
category: ReadinessCategory::Critical,
status: ReadinessStatus::Missing,
components: vec![
"src/error.rs".to_string(),
"templates/error-handling.tmpl".to_string(),
],
dependencies: vec![],
effort_hours: Some(12),
priority: 10,
last_assessed: Utc::now(),
notes: Some("Production code must handle errors gracefully".to_string()),
},
ReadinessRequirement {
id: "logging-tracing".to_string(),
name: "Structured Logging & Tracing".to_string(),
description: "Comprehensive logging with tracing crate and structured output"
.to_string(),
category: ReadinessCategory::Critical,
status: ReadinessStatus::Missing,
components: vec![
"src/logging.rs".to_string(),
"config/tracing.toml".to_string(),
],
dependencies: vec![],
effort_hours: Some(6),
priority: 9,
last_assessed: Utc::now(),
notes: Some("Essential for debugging and monitoring in production".to_string()),
},
ReadinessRequirement {
id: "health-checks".to_string(),
name: "Health Check Endpoints".to_string(),
description: "HTTP health check endpoints for load balancer and monitoring"
.to_string(),
category: ReadinessCategory::Critical,
status: ReadinessStatus::Missing,
components: vec![
"src/health.rs".to_string(),
"templates/health.tmpl".to_string(),
],
dependencies: vec![],
effort_hours: Some(4),
priority: 8,
last_assessed: Utc::now(),
notes: Some("Required for container orchestration and monitoring".to_string()),
},
ReadinessRequirement {
id: "input-validation".to_string(),
name: "Input Validation & Sanitization".to_string(),
description:
"Comprehensive input validation and sanitization to prevent injection attacks"
.to_string(),
category: ReadinessCategory::Critical,
status: ReadinessStatus::Missing,
components: vec![
"src/validation.rs".to_string(),
"templates/validation.tmpl".to_string(),
],
dependencies: vec!["auth-basic".to_string()],
effort_hours: Some(10),
priority: 9,
last_assessed: Utc::now(),
notes: Some("Critical for security and data integrity".to_string()),
},
ReadinessRequirement {
id: "database-migrations".to_string(),
name: "Database Schema Migrations".to_string(),
description: "Automated database schema migrations with rollback capability"
.to_string(),
category: ReadinessCategory::Critical,
status: ReadinessStatus::Missing,
components: vec!["migrations/".to_string(), "src/database.rs".to_string()],
dependencies: vec![],
effort_hours: Some(8),
priority: 8,
last_assessed: Utc::now(),
notes: Some("Essential for zero-downtime deployments".to_string()),
},
ReadinessRequirement {
id: "api-documentation".to_string(),
name: "OpenAPI Documentation".to_string(),
description: "Complete OpenAPI/Swagger documentation for all endpoints".to_string(),
category: ReadinessCategory::Important,
status: ReadinessStatus::Missing,
components: vec!["docs/api.md".to_string(), "openapi.yaml".to_string()],
dependencies: vec![],
effort_hours: Some(12),
priority: 7,
last_assessed: Utc::now(),
notes: Some("Important for API consumers and automated testing".to_string()),
},
ReadinessRequirement {
id: "unit-tests".to_string(),
name: "Comprehensive Unit Tests".to_string(),
description: "Unit tests for all public functions with >80% coverage".to_string(),
category: ReadinessCategory::Important,
status: ReadinessStatus::Missing,
components: vec!["tests/unit/".to_string()],
dependencies: vec![],
effort_hours: Some(20),
priority: 7,
last_assessed: Utc::now(),
notes: Some("Essential for code quality and refactoring confidence".to_string()),
},
ReadinessRequirement {
id: "integration-tests".to_string(),
name: "Integration Tests".to_string(),
description: "Integration tests for component interactions".to_string(),
category: ReadinessCategory::Important,
status: ReadinessStatus::Missing,
components: vec!["tests/integration/".to_string()],
dependencies: vec!["unit-tests".to_string()],
effort_hours: Some(16),
priority: 6,
last_assessed: Utc::now(),
notes: Some("Validates component interactions work correctly".to_string()),
},
ReadinessRequirement {
id: "performance-monitoring".to_string(),
name: "Performance Monitoring".to_string(),
description: "Basic performance metrics collection and alerting".to_string(),
category: ReadinessCategory::Important,
status: ReadinessStatus::Missing,
components: vec![
"src/metrics.rs".to_string(),
"config/metrics.toml".to_string(),
],
dependencies: vec!["logging-tracing".to_string()],
effort_hours: Some(8),
priority: 6,
last_assessed: Utc::now(),
notes: Some("Essential for production performance management".to_string()),
},
ReadinessRequirement {
id: "docker-containerization".to_string(),
name: "Docker Containerization".to_string(),
description: "Production-ready Docker containers with multi-stage builds"
.to_string(),
category: ReadinessCategory::Important,
status: ReadinessStatus::Missing,
components: vec!["Dockerfile".to_string(), "docker-compose.yml".to_string()],
dependencies: vec!["health-checks".to_string()],
effort_hours: Some(6),
priority: 7,
last_assessed: Utc::now(),
notes: Some("Required for consistent deployment across environments".to_string()),
},
ReadinessRequirement {
id: "configuration-management".to_string(),
name: "Configuration Management".to_string(),
description: "Environment-based configuration with validation".to_string(),
category: ReadinessCategory::Important,
status: ReadinessStatus::Missing,
components: vec!["config/".to_string(), "src/config.rs".to_string()],
dependencies: vec![],
effort_hours: Some(8),
priority: 7,
last_assessed: Utc::now(),
notes: Some("Essential for multi-environment deployments".to_string()),
},
ReadinessRequirement {
id: "rate-limiting".to_string(),
name: "Rate Limiting".to_string(),
description: "API rate limiting to prevent abuse".to_string(),
category: ReadinessCategory::NiceToHave,
status: ReadinessStatus::Missing,
components: vec!["src/rate_limit.rs".to_string()],
dependencies: vec!["auth-basic".to_string()],
effort_hours: Some(12),
priority: 4,
last_assessed: Utc::now(),
notes: Some("Nice to have for high-traffic applications".to_string()),
},
ReadinessRequirement {
id: "caching-layer".to_string(),
name: "Advanced Caching".to_string(),
description: "Redis-based caching with cache invalidation strategies".to_string(),
category: ReadinessCategory::NiceToHave,
status: ReadinessStatus::Missing,
components: vec!["src/cache.rs".to_string(), "config/redis.toml".to_string()],
dependencies: vec!["performance-monitoring".to_string()],
effort_hours: Some(16),
priority: 3,
last_assessed: Utc::now(),
notes: Some("Improves performance but adds complexity".to_string()),
},
ReadinessRequirement {
id: "circuit-breaker".to_string(),
name: "Circuit Breaker Pattern".to_string(),
description: "Circuit breaker for external service calls".to_string(),
category: ReadinessCategory::NiceToHave,
status: ReadinessStatus::Missing,
components: vec!["src/circuit_breaker.rs".to_string()],
dependencies: vec!["error-handling".to_string()],
effort_hours: Some(10),
priority: 3,
last_assessed: Utc::now(),
notes: Some("Improves resilience but adds complexity".to_string()),
},
ReadinessRequirement {
id: "advanced-security".to_string(),
name: "Advanced Security Features".to_string(),
description:
"Advanced security features like CSRF protection, CORS, security headers"
.to_string(),
category: ReadinessCategory::NiceToHave,
status: ReadinessStatus::Missing,
components: vec![
"src/security.rs".to_string(),
"config/security.toml".to_string(),
],
dependencies: vec!["input-validation".to_string()],
effort_hours: Some(14),
priority: 4,
last_assessed: Utc::now(),
notes: Some("Additional security hardening for sensitive applications".to_string()),
},
ReadinessRequirement {
id: "monitoring-dashboard".to_string(),
name: "Monitoring Dashboard".to_string(),
description: "Grafana/Kibana dashboard for comprehensive monitoring".to_string(),
category: ReadinessCategory::NiceToHave,
status: ReadinessStatus::Missing,
components: vec!["docker/grafana/".to_string(), "docker/kibana/".to_string()],
dependencies: vec!["performance-monitoring".to_string()],
effort_hours: Some(20),
priority: 2,
last_assessed: Utc::now(),
notes: Some("Nice for observability but not essential for MVP".to_string()),
},
]
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductionConfig {
pub requirements: Vec<ReadinessRequirement>,
}
#[derive(Debug, Clone)]
pub struct Placeholder {
pub id: String,
pub description: String,
pub category: ReadinessCategory,
pub affects: Vec<String>,
pub guidance: String,
pub priority: u8,
}
impl Placeholder {
pub fn new(
id: String, description: String, category: ReadinessCategory, affects: Vec<String>,
guidance: String, priority: u8,
) -> Self {
Self {
id,
description,
category,
affects,
guidance,
priority,
}
}
pub fn to_comment(&self) -> String {
format!(
r#"// 🚧 PLACEHOLDER: {description}
// Category: {category:?}
// Priority: {priority}
// Guidance: {guidance}
// FUTURE: Implement this placeholder for production readiness"#,
description = self.description,
category = self.category,
priority = self.priority,
guidance = self.guidance
)
}
pub fn to_template_section(&self) -> String {
format!(
r#"{{{{!-- 🚧 PLACEHOLDER: {description} --}}
{{{{!-- Category: {category:?} --}}
{{{{!-- Priority: {priority} --}}
{{{{!-- Guidance: {guidance} --}}
{{{{!-- FUTURE: Implement this placeholder for production readiness --}}}}"#,
description = self.description,
category = self.category,
priority = self.priority,
guidance = self.guidance
)
}
}
impl ReadinessTracker {
pub fn analyze_project(&mut self) -> Result<()> {
self.scan_for_authentication()?;
self.scan_for_error_handling()?;
self.scan_for_logging()?;
self.scan_for_health_checks()?;
self.scan_for_input_validation()?;
self.scan_for_database_setup()?;
self.scan_for_tests()?;
self.scan_for_documentation()?;
self.scan_for_docker_setup()?;
self.scan_for_configuration()?;
Ok(())
}
fn scan_for_authentication(&mut self) -> Result<()> {
let auth_patterns = ["auth", "login", "authentication", "jwt", "session"];
let has_auth = self.scan_for_patterns(&auth_patterns);
if has_auth {
self.update_requirement("auth-basic", ReadinessStatus::Complete)?;
}
Ok(())
}
fn scan_for_error_handling(&mut self) -> Result<()> {
let error_patterns = ["thiserror", "Result<", "map_err", "anyhow"];
let has_error_handling = self.scan_for_patterns(&error_patterns);
if has_error_handling {
self.update_requirement("error-handling", ReadinessStatus::Complete)?;
}
Ok(())
}
fn scan_for_logging(&mut self) -> Result<()> {
let logging_patterns = ["tracing", "log::", "slog", "println!"];
let has_logging = self.scan_for_patterns(&logging_patterns);
if has_logging {
self.update_requirement("logging-tracing", ReadinessStatus::Complete)?;
}
Ok(())
}
fn scan_for_health_checks(&mut self) -> Result<()> {
let health_patterns = ["health", "/health", "health_check"];
let has_health = self.scan_for_patterns(&health_patterns);
if has_health {
self.update_requirement("health-checks", ReadinessStatus::Complete)?;
}
Ok(())
}
fn scan_for_input_validation(&mut self) -> Result<()> {
let validation_patterns = ["validate", "validator", "serde", "Deserialize"];
let has_validation = self.scan_for_patterns(&validation_patterns);
if has_validation {
self.update_requirement("input-validation", ReadinessStatus::Complete)?;
}
Ok(())
}
fn scan_for_database_setup(&mut self) -> Result<()> {
let db_patterns = ["sqlx", "diesel", "sea-orm", "migration", "schema"];
let has_db = self.scan_for_patterns(&db_patterns);
if has_db {
self.update_requirement("database-migrations", ReadinessStatus::Complete)?;
}
Ok(())
}
fn scan_for_tests(&mut self) -> Result<()> {
let test_patterns = ["#[test]", "#[cfg(test)]", "tests/"];
let has_tests = self.scan_for_patterns(&test_patterns);
if has_tests {
self.update_requirement("unit-tests", ReadinessStatus::Complete)?;
self.update_requirement("integration-tests", ReadinessStatus::Complete)?;
}
Ok(())
}
fn scan_for_documentation(&mut self) -> Result<()> {
let doc_patterns = ["README.md", "docs/", "openapi", "swagger"];
let has_docs = self.scan_for_patterns(&doc_patterns);
if has_docs {
self.update_requirement("api-documentation", ReadinessStatus::Complete)?;
}
Ok(())
}
fn scan_for_docker_setup(&mut self) -> Result<()> {
let docker_patterns = ["Dockerfile", "docker-compose", ".dockerignore"];
let has_docker = self.scan_for_patterns(&docker_patterns);
if has_docker {
self.update_requirement("docker-containerization", ReadinessStatus::Complete)?;
}
Ok(())
}
fn scan_for_configuration(&mut self) -> Result<()> {
let config_patterns = ["config.toml", "settings.toml", "app.toml"];
let has_config = self.scan_for_patterns(&config_patterns);
if has_config {
self.update_requirement("configuration-management", ReadinessStatus::Complete)?;
}
Ok(())
}
fn scan_for_patterns(&self, patterns: &[&str]) -> bool {
let src_dir = self.project_root.join("src");
let templates_dir = self.project_root.join("templates");
let config_dir = self.project_root.join("config");
let search_dirs = [src_dir, templates_dir, config_dir];
for dir in &search_dirs {
if !dir.exists() {
continue;
}
for pattern in patterns {
if self.directory_contains_pattern(dir, pattern) {
return true;
}
}
}
false
}
#[allow(clippy::only_used_in_recursion)]
fn directory_contains_pattern(&self, dir: &std::path::Path, pattern: &str) -> bool {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Ok(content) = std::fs::read_to_string(&path) {
if content.contains(pattern) {
return true;
}
}
} else if path.is_dir() {
if self.directory_contains_pattern(&path, pattern) {
return true;
}
}
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_requirements() {
let requirements = ReadinessTracker::default_requirements();
let critical = requirements
.iter()
.filter(|r| r.category == ReadinessCategory::Critical)
.count();
let important = requirements
.iter()
.filter(|r| r.category == ReadinessCategory::Important)
.count();
let nice = requirements
.iter()
.filter(|r| r.category == ReadinessCategory::NiceToHave)
.count();
assert!(critical > 0, "Should have critical requirements");
assert!(important > 0, "Should have important requirements");
assert!(nice > 0, "Should have nice-to-have requirements");
for req in &requirements {
if req.category == ReadinessCategory::Critical {
assert!(
req.priority >= 8,
"Critical requirements should have high priority"
);
}
}
}
#[test]
fn test_readiness_report_generation() {
let mut tracker = ReadinessTracker::new("/tmp/test");
tracker.load().unwrap(); let report = tracker.generate_report();
assert_eq!(
report.project_name,
crate::lifecycle::model::defaults::DEFAULT_READINESS_PROJECT_NAME
);
assert!(report.overall_score >= 0.0 && report.overall_score <= 100.0);
assert!(
!report.by_category.is_empty(),
"by_category should not be empty after loading defaults"
);
assert!(
!report.requirements.is_empty(),
"requirements should not be empty after loading defaults"
);
}
#[test]
fn test_status_transitions() {
let mut tracker = ReadinessTracker::new("/tmp/test");
tracker.load().unwrap();
assert!(tracker
.update_requirement("auth-basic", ReadinessStatus::Placeholder)
.is_ok());
assert!(tracker
.update_requirement("auth-basic", ReadinessStatus::Complete)
.is_ok());
assert!(tracker
.update_requirement("auth-basic", ReadinessStatus::Missing)
.is_err());
}
#[test]
fn test_readiness_category_ordering() {
assert!(ReadinessCategory::Critical < ReadinessCategory::Important);
assert!(ReadinessCategory::Important < ReadinessCategory::NiceToHave);
}
#[test]
fn test_readiness_status_valid_transitions() {
assert!(ReadinessTracker::validate_transition_static(
&ReadinessStatus::Missing,
&ReadinessStatus::Placeholder
)
.is_ok());
assert!(ReadinessTracker::validate_transition_static(
&ReadinessStatus::Missing,
&ReadinessStatus::Complete
)
.is_ok());
assert!(ReadinessTracker::validate_transition_static(
&ReadinessStatus::Placeholder,
&ReadinessStatus::Complete
)
.is_ok());
assert!(ReadinessTracker::validate_transition_static(
&ReadinessStatus::Placeholder,
&ReadinessStatus::NeedsReview
)
.is_ok());
assert!(ReadinessTracker::validate_transition_static(
&ReadinessStatus::Complete,
&ReadinessStatus::NeedsReview
)
.is_ok());
}
#[test]
fn test_readiness_status_invalid_transitions() {
assert!(ReadinessTracker::validate_transition_static(
&ReadinessStatus::Complete,
&ReadinessStatus::Missing
)
.is_err());
assert!(ReadinessTracker::validate_transition_static(
&ReadinessStatus::Complete,
&ReadinessStatus::Placeholder
)
.is_err());
}
#[test]
fn test_readiness_report_scoring() {
let mut tracker = ReadinessTracker::new("/tmp/test");
tracker.load().unwrap();
let report = tracker.generate_report();
assert!(report.overall_score >= 0.0 && report.overall_score <= 100.0);
assert!(report
.by_category
.contains_key(&ReadinessCategory::Critical));
assert!(report
.by_category
.contains_key(&ReadinessCategory::Important));
assert!(report
.by_category
.contains_key(&ReadinessCategory::NiceToHave));
}
#[test]
fn test_category_report_calculation() {
let mut tracker = ReadinessTracker::new("/tmp/test");
tracker.load().unwrap();
tracker
.update_requirement("auth-basic", ReadinessStatus::Complete)
.ok();
tracker
.update_requirement("error-handling", ReadinessStatus::Complete)
.ok();
let report = tracker.generate_report();
let critical_report = report
.by_category
.get(&ReadinessCategory::Critical)
.unwrap();
assert!(critical_report.completed > 0);
assert!(critical_report.score > 0.0);
}
#[test]
fn test_get_by_status() {
let mut tracker = ReadinessTracker::new("/tmp/test");
tracker.load().unwrap();
let missing = tracker.get_by_status(&ReadinessStatus::Missing);
assert!(!missing.is_empty());
tracker
.update_requirement("auth-basic", ReadinessStatus::Complete)
.ok();
let complete = tracker.get_by_status(&ReadinessStatus::Complete);
assert!(!complete.is_empty());
}
#[test]
fn test_get_by_category() {
let mut tracker = ReadinessTracker::new("/tmp/test");
tracker.load().unwrap();
let critical = tracker.get_by_category(&ReadinessCategory::Critical);
let important = tracker.get_by_category(&ReadinessCategory::Important);
let nice = tracker.get_by_category(&ReadinessCategory::NiceToHave);
assert!(!critical.is_empty());
assert!(!important.is_empty());
assert!(!nice.is_empty());
}
#[test]
fn test_requirement_not_found() {
let mut tracker = ReadinessTracker::new("/tmp/test");
tracker.load().unwrap();
let result = tracker.update_requirement("nonexistent", ReadinessStatus::Complete);
assert!(result.is_err());
}
#[test]
fn test_add_requirement_success() {
let mut tracker = ReadinessTracker::new("/tmp/test");
tracker.load().unwrap();
let req = ReadinessRequirement {
id: "custom-req".to_string(),
name: "Custom Requirement".to_string(),
description: "Test requirement".to_string(),
category: ReadinessCategory::Critical,
status: ReadinessStatus::Missing,
components: vec![],
dependencies: vec![],
effort_hours: Some(4),
priority: 8,
last_assessed: Utc::now(),
notes: None,
};
assert!(tracker.add_requirement(req).is_ok());
}
#[test]
fn test_dependency_validation() {
let mut tracker = ReadinessTracker::new("/tmp/test");
tracker.load().unwrap();
let req = ReadinessRequirement {
id: "req1".to_string(),
name: "Req 1".to_string(),
description: "Test".to_string(),
category: ReadinessCategory::Critical,
status: ReadinessStatus::Missing,
components: vec![],
dependencies: vec!["auth-basic".to_string()],
effort_hours: Some(4),
priority: 8,
last_assessed: Utc::now(),
notes: None,
};
assert!(tracker.add_requirement(req).is_ok());
}
#[test]
fn test_placeholder_creation() {
let placeholder = Placeholder::new(
"test-placeholder".to_string(),
"Test description".to_string(),
ReadinessCategory::Critical,
vec!["file.rs".to_string()],
"Implement this feature".to_string(),
9,
);
assert_eq!(placeholder.id, "test-placeholder");
assert_eq!(placeholder.priority, 9);
}
#[test]
fn test_placeholder_to_comment() {
let placeholder = Placeholder::new(
"test".to_string(),
"Test placeholder".to_string(),
ReadinessCategory::Critical,
vec![],
"Implement this".to_string(),
8,
);
let comment = placeholder.to_comment();
assert!(comment.contains("PLACEHOLDER"));
assert!(comment.contains("Test placeholder"));
assert!(comment.contains("Priority: 8"));
}
#[test]
fn test_placeholder_registry() {
let mut registry = PlaceholderRegistry::new();
let placeholder = Placeholder::new(
"test".to_string(),
"Test".to_string(),
ReadinessCategory::Critical,
vec![],
"Implement".to_string(),
8,
);
registry.register("test".to_string(), placeholder);
assert!(registry.get("test").is_some());
assert_eq!(registry.list().len(), 1);
}
#[test]
fn test_placeholder_registry_by_category() {
let mut registry = PlaceholderRegistry::new();
let p1 = Placeholder::new(
"critical".to_string(),
"Critical".to_string(),
ReadinessCategory::Critical,
vec![],
"Impl".to_string(),
9,
);
let p2 = Placeholder::new(
"important".to_string(),
"Important".to_string(),
ReadinessCategory::Important,
vec![],
"Impl".to_string(),
7,
);
registry.register("critical".to_string(), p1);
registry.register("important".to_string(), p2);
let critical = registry.get_by_category(&ReadinessCategory::Critical);
assert_eq!(critical.len(), 1);
let important = registry.get_by_category(&ReadinessCategory::Important);
assert_eq!(important.len(), 1);
}
#[test]
fn test_placeholder_processor() {
let mut processor = PlaceholderProcessor::new();
let placeholder = Placeholder::new(
"test".to_string(),
"Test".to_string(),
ReadinessCategory::Critical,
vec![],
"Impl".to_string(),
8,
);
processor
.registry_mut()
.register("test".to_string(), placeholder);
assert!(processor.process("test").is_ok());
assert!(processor.process("nonexistent").is_err());
}
}
#[derive(Debug, Clone, Default)]
pub struct PlaceholderRegistry {
placeholders: std::collections::HashMap<String, Placeholder>,
}
impl PlaceholderRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, id: String, placeholder: Placeholder) {
self.placeholders.insert(id, placeholder);
}
pub fn get(&self, id: &str) -> Option<&Placeholder> {
self.placeholders.get(id)
}
pub fn list(&self) -> Vec<&Placeholder> {
self.placeholders.values().collect()
}
pub fn get_by_category(&self, category: &ReadinessCategory) -> Vec<&Placeholder> {
self.placeholders
.values()
.filter(|p| &p.category == category)
.collect()
}
pub fn generate_summary(&self) -> String {
let mut summary = String::new();
summary.push_str("Placeholder Summary:\n");
for (id, placeholder) in &self.placeholders {
summary.push_str(&format!(
" {}: {} ({:?})\n",
id, placeholder.description, placeholder.category
));
}
summary
}
}
#[derive(Debug, Clone)]
pub struct PlaceholderProcessor {
registry: PlaceholderRegistry,
}
impl Default for PlaceholderProcessor {
fn default() -> Self {
Self::new()
}
}
impl PlaceholderProcessor {
pub fn new() -> Self {
Self {
registry: PlaceholderRegistry::new(),
}
}
pub fn process(&self, placeholder_id: &str) -> Result<()> {
if let Some(_placeholder) = self.registry.get(placeholder_id) {
tracing::info!("Processing placeholder: {}", placeholder_id);
Ok(())
} else {
Err(ProductionError::RequirementNotFound(
placeholder_id.to_string(),
))
}
}
pub fn registry(&self) -> &PlaceholderRegistry {
&self.registry
}
pub fn registry_mut(&mut self) -> &mut PlaceholderRegistry {
&mut self.registry
}
}