use anyhow::{Context, Result};
use std::path::Path;
#[derive(Default)]
pub struct RebuildOptions {
pub scrape_only: bool,
pub oxidize_only: bool,
pub force: bool,
pub dry_run: bool,
}
struct ValidationResult {
has_git: bool,
session_count: usize,
projection_count: usize,
}
pub fn execute(options: RebuildOptions) -> Result<()> {
println!("🔄 Rebuilding .patina/ from layer/\n");
println!("📋 Validation");
let validation = validate()?;
if options.dry_run {
println!("\n🔍 Dry run - would execute:");
if !options.oxidize_only {
println!(" • scrape git (if .git/ exists)");
println!(
" • scrape layer ({} sessions + patterns)",
validation.session_count
);
println!(" • scrape code");
}
if !options.scrape_only {
println!(" • oxidize ({} projections)", validation.projection_count);
}
println!("\n✅ Dry run complete - no changes made");
return Ok(());
}
if options.force {
println!("\n🗑️ Force mode - clearing existing data...");
clear_data()?;
}
if !options.oxidize_only {
println!("\n📥 Scrape (Step 1/2)");
run_scrape(&validation)?;
}
if !options.scrape_only {
println!("\n🧪 Oxidize (Step 2/2)");
run_oxidize()?;
}
print_summary()?;
Ok(())
}
fn validate() -> Result<ValidationResult> {
if !Path::new("layer").exists() {
anyhow::bail!(
"❌ Not a Patina project (no layer/ found)\n\n\
Run 'patina init .' to initialize this project."
);
}
let session_count = count_sessions()?;
println!(" ✓ layer/ found ({} sessions)", session_count);
if !Path::new(".patina/oxidize.yaml").exists() {
anyhow::bail!(
"❌ No recipe found (.patina/oxidize.yaml)\n\n\
Run 'patina init .' to create the recipe file."
);
}
let projection_count = count_projections()?;
println!(" ✓ oxidize.yaml found ({} projections)", projection_count);
let has_git = Path::new(".git").exists();
if has_git {
let commit_count = count_commits()?;
println!(" ✓ .git/ found ({} commits)", commit_count);
} else {
println!(" ⚠️ .git/ not found (git scrape will be skipped)");
}
Ok(ValidationResult {
has_git,
session_count: count_sessions()?,
projection_count,
})
}
fn count_sessions() -> Result<usize> {
let sessions_dir = Path::new("layer/sessions");
if !sessions_dir.exists() {
return Ok(0);
}
let count = std::fs::read_dir(sessions_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map(|ext| ext == "md").unwrap_or(false))
.count();
Ok(count)
}
fn count_projections() -> Result<usize> {
use crate::commands::oxidize::recipe::OxidizeRecipe;
let recipe = OxidizeRecipe::load()?;
Ok(recipe.projections.len())
}
fn count_commits() -> Result<usize> {
let output = std::process::Command::new("git")
.args(["rev-list", "--count", "HEAD"])
.output()
.context("Failed to count git commits")?;
if !output.status.success() {
return Ok(0);
}
let count_str = String::from_utf8_lossy(&output.stdout);
count_str.trim().parse().unwrap_or(0).pipe(Ok)
}
fn clear_data() -> Result<()> {
let data_dir = Path::new(".patina/local/data");
if data_dir.exists() {
std::fs::remove_dir_all(data_dir).context("Failed to remove .patina/local/data/")?;
println!(" ✓ Cleared .patina/local/data/");
}
Ok(())
}
fn run_scrape(validation: &ValidationResult) -> Result<()> {
use crate::commands::scrape;
if validation.has_git {
print!(" • git: ");
let stats = scrape::git::run(false)?;
println!("{} commits", stats.items_processed);
}
print!(" • layer: ");
let stats = scrape::layer::run(false)?;
println!("{} items", stats.items_processed);
print!(" • code: ");
scrape::execute_code(false, false)?;
println!("complete");
let db_path = Path::new(".patina/local/data/patina.db");
if db_path.exists() {
let total = count_events(db_path)?;
println!(" ✓ patina.db: {} events", total);
}
Ok(())
}
fn count_events(db_path: &Path) -> Result<usize> {
let conn = rusqlite::Connection::open(db_path)?;
let count: usize = conn.query_row("SELECT COUNT(*) FROM eventlog", [], |row| row.get(0))?;
Ok(count)
}
fn run_oxidize() -> Result<()> {
use crate::commands::oxidize;
oxidize::oxidize()?;
Ok(())
}
fn print_summary() -> Result<()> {
println!("\n✅ Rebuild complete!");
let db_path = Path::new(".patina/local/data/patina.db");
if db_path.exists() {
let size_kb = std::fs::metadata(db_path)?.len() / 1024;
println!(" Database: .patina/local/data/patina.db ({} KB)", size_kb);
}
let embeddings_dir = Path::new(".patina/local/data/embeddings");
if embeddings_dir.exists() {
let size_kb = dir_size(embeddings_dir)? / 1024;
println!(
" Indices: .patina/local/data/embeddings/ ({} KB)",
size_kb
);
}
Ok(())
}
fn dir_size(path: &Path) -> Result<u64> {
let mut total = 0;
if path.is_dir() {
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
total += dir_size(&path)?;
} else {
total += std::fs::metadata(&path)?.len();
}
}
}
Ok(total)
}
trait Pipe: Sized {
fn pipe<F, R>(self, f: F) -> R
where
F: FnOnce(Self) -> R,
{
f(self)
}
}
impl<T> Pipe for T {}