//! Run the brownfield onramp against jouleclaw-rs itself. //! //! Walks `crates/*/src/**/*.rs` + `crates/*/Cargo.toml`, //! produces a [`jouleclaw_brownfield::CodebaseMap`], runs //! [`jouleclaw_brownfield::reverse_compile`], and prints the //! markdown report. //! //! Run from the workspace root: //! //! ```bash //! cargo run -p jouleclaw-brownfield --example scan_self //! ``` //! //! Pipes cleanly into a file: //! //! ```bash //! cargo run -p jouleclaw-brownfield --example scan_self \ //! > BROWNFIELD-REPORT.md //! ``` use jouleclaw_brownfield::{ reverse_compile, reverse_compile_public_surface, reverse_compile_report, scan_corpus, ReasonConfidence, }; use std::path::{Path, PathBuf}; use walkdir::WalkDir; fn main() -> Result<(), Box> { let workspace_root = locate_workspace_root()?; let crates_dir = workspace_root.join("crates"); eprintln!("scanning {}", crates_dir.display()); let mut corpus: Vec<(PathBuf, String)> = Vec::new(); let mut skipped: u32 = 0; for entry in WalkDir::new(&crates_dir).into_iter().filter_map(Result::ok) { let path = entry.path(); if !path.is_file() { continue; } let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); let include = ext == "rs" || name == "Cargo.toml"; if !include { continue; } // Skip targets / .git just in case. if path .components() .any(|c| c.as_os_str() == "target" || c.as_os_str() == ".git") { skipped += 1; continue; } match std::fs::read_to_string(path) { Ok(content) => { let rel = path .strip_prefix(&workspace_root) .unwrap_or(path) .to_path_buf(); corpus.push((rel, content)); } Err(_) => skipped += 1, } } eprintln!( "collected {} files ({} skipped); running scan_corpus + reverse_compile", corpus.len(), skipped ); let map = scan_corpus(&corpus)?; let full = reverse_compile(&map, "jouleclaw-rs"); let public = reverse_compile_public_surface(&map, "jouleclaw-rs (public surface)"); let lost = full .module_purposes .iter() .filter(|r| r.confidence == ReasonConfidence::Lost) .count(); let documented = full .module_purposes .iter() .filter(|r| r.confidence == ReasonConfidence::Documented) .count(); eprintln!( "modules: {} | documented: {} | lost: {} | currency gaps: {}", full.module_purposes.len(), documented, lost, full.missing_currency.len() ); eprintln!( "missing reasons — all functions: {} | public surface only: {} | internal-only delta: {}", full.missing_reasons.len(), public.missing_reasons.len(), full.missing_reasons .len() .saturating_sub(public.missing_reasons.len()) ); // Print the public-surface report by default — that's the // actionable audit. The full report is much larger and // available via the `--full` arg. let full_mode = std::env::args().any(|a| a == "--full"); if full_mode { println!("{}", reverse_compile_report(&full)); } else { println!("{}", reverse_compile_report(&public)); } // CI gate: --fail-on-lost makes the process exit non-zero // when any public-surface Lost reason remains. Used by the // jouleclaw-public-surface-documented workflow as a // regression-prevention check. if std::env::args().any(|a| a == "--fail-on-lost") && !public.missing_reasons.is_empty() { eprintln!( "\nFAIL: {} public-surface Lost reasons remain. \ See the report above for the per-crate breakdown + full list. \ Close them with `///` doc comments or downgrade the function \ to `pub(crate)` if it isn't part of the consumer-visible surface.", public.missing_reasons.len() ); std::process::exit(1); } Ok(()) } /// Walk up from the example binary's location until we find a /// directory containing a `crates/` subdirectory + a top-level /// `Cargo.toml` — that's the workspace root jouleclaw-rs sits /// inside. fn locate_workspace_root() -> Result> { // Cargo runs examples from CARGO_MANIFEST_DIR (this // crate's dir); the workspace root is two levels up // (crates/jouleclaw-brownfield → crates → root). let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map(PathBuf::from)?; let candidate = manifest_dir .parent() .and_then(Path::parent) .map(Path::to_path_buf) .ok_or("manifest dir has no grandparent")?; if !candidate.join("crates").is_dir() { return Err(format!( "couldn't find `crates/` under workspace root {}", candidate.display() ) .into()); } Ok(candidate) }