use anyhow::{Context, Result};
use codelore_lib::cli_api::facts::FactsDb;
use codelore_lib::cli_api::repo::GixRepo;
use codelore_lib::cli_api::{CodeLoreError, Options};
use crate::args::CalibrateArgs;
pub(crate) fn run_calibrate_cmd(args: &CalibrateArgs) -> Result<()> {
use codelore_lib::calibration::{self, LangObservations};
use codelore_lib::cli_api::cache::default_cache_root;
use codelore_lib::cli_api::quality_gates::ledger::now_utc_ts;
let cache_root = args.cache_dir.clone().unwrap_or_else(default_cache_root);
let generated_at = now_utc_ts();
let vintage = args
.vintage
.clone()
.unwrap_or_else(|| format!("corpus-{}", &generated_at[..7]));
let manifest = calibration::load_manifest(&args.repos).context("load corpus manifest")?;
let mut obs = LangObservations::new();
let mut pools = calibration::RepoMetrics::default();
let mut attempted: u32 = 0;
let mut included: u32 = 0;
for repo in &manifest.repos {
attempted += 1;
match calibrate_one_repo(&repo.source, &repo.sha, &cache_root, &mut obs, &mut pools) {
Ok(()) => included += 1,
Err(e) => eprintln!("calibrate: skip {} @ {}: {e:#}", repo.source, repo.sha),
}
}
if attempted > 0 && included == 0 {
return Err(anyhow::Error::new(CodeLoreError::Analysis(format!(
"all {attempted} repo(s) failed to fetch or ingest — no calibration data pooled \
(see the per-repo skip warnings above); refusing to write an empty artifact to {}",
args.output.display()
))));
}
let mut artifact = calibration::build_from_observations(&vintage, &generated_at, &obs);
artifact.repos_attempted = attempted;
artifact.repos_included = included;
calibration::attach_repo_metrics(&mut artifact, pools);
if let Some(merge_path) = &args.merge {
let base = calibration::load(merge_path)
.with_context(|| format!("load --merge artifact {}", merge_path.display()))?;
artifact = calibration::merge(base, artifact);
}
let json = serde_json::to_vec(&artifact).context("serialize calibration artifact")?;
if let Some(parent) = args.output.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent)
.with_context(|| format!("create output dir {}", parent.display()))?;
}
codelore_lib::cli_api::output::atomic_publish(&args.output, |tmp| {
std::fs::write(tmp, &json)
.with_context(|| format!("write artifact {}", args.output.display()))
})?;
eprintln!(
"calibrate: {included}/{attempted} repo(s) → {} ({} language(s))",
args.output.display(),
artifact.languages.len(),
);
Ok(())
}
fn calibrate_one_repo(
source: &str,
sha: &str,
cache_root: &std::path::Path,
obs: &mut codelore_lib::calibration::LangObservations,
pools: &mut codelore_lib::calibration::RepoMetrics,
) -> Result<()> {
let (checkout, mode) = checkout_pinned(source, sha)?;
eprintln!("calibrate: {source} @ {sha} ({})", mode.label());
let repo = GixRepo::open(checkout.path()).context("open pinned checkout")?;
let opts = Options {
repo_path: checkout.path().to_path_buf(),
head_only_ingest: true,
..Options::default()
};
let db = FactsDb::open_or_ingest_with_cache_root(&opts, &repo, cache_root).context("ingest")?;
pool_complexity(&db, obs).context("pool complexity metrics")?;
pool_repo_metrics(&db, pools).context("pool repo-level architecture metrics")?;
Ok(())
}
#[derive(Clone, Copy)]
enum CheckoutMode {
Shallow,
Full,
Worktree,
}
impl CheckoutMode {
fn label(self) -> &'static str {
match self {
Self::Shallow => "shallow",
Self::Full => "full",
Self::Worktree => "worktree",
}
}
}
enum PinnedCheckout {
Clone(tempfile::TempDir),
Worktree {
origin: std::path::PathBuf,
dir: tempfile::TempDir,
},
}
impl PinnedCheckout {
fn path(&self) -> &std::path::Path {
match self {
Self::Clone(dir) | Self::Worktree { dir, .. } => dir.path(),
}
}
}
impl Drop for PinnedCheckout {
fn drop(&mut self) {
if let Self::Worktree { origin, dir } = self
&& let Some(path) = dir.path().to_str()
{
let _ = std::process::Command::new("git")
.args([
"-C",
origin.to_str().unwrap_or("."),
"worktree",
"remove",
"--force",
path,
])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
}
}
}
fn checkout_pinned(source: &str, sha: &str) -> Result<(PinnedCheckout, CheckoutMode)> {
let is_url = source.contains("://") || source.starts_with("git@");
if is_url {
match shallow_pinned_clone(source, sha) {
Ok(dir) => return Ok((PinnedCheckout::Clone(dir), CheckoutMode::Shallow)),
Err(e) => tracing::warn!(
"calibrate: shallow fetch failed for {source} ({e:#}); falling back to full clone"
),
}
let dir = tempfile::tempdir().context("create clone tempdir")?;
run_git(
&["clone", "--quiet", source, path_str(dir.path())?],
"clone",
)?;
run_git(
&[
"-C",
path_str(dir.path())?,
"checkout",
"--quiet",
"--detach",
sha,
],
"checkout",
)?;
Ok((PinnedCheckout::Clone(dir), CheckoutMode::Full))
} else {
let origin = std::fs::canonicalize(source)
.with_context(|| format!("resolve local repo path {source}"))?;
run_git(
&[
"-C",
path_str(&origin)?,
"rev-parse",
"--verify",
"--quiet",
sha,
],
"rev-parse",
)?;
let dir = tempfile::tempdir().context("create worktree tempdir")?;
run_git(
&[
"-C",
path_str(&origin)?,
"worktree",
"add",
"--detach",
"--quiet",
path_str(dir.path())?,
sha,
],
"worktree add",
)?;
Ok((
PinnedCheckout::Worktree { origin, dir },
CheckoutMode::Worktree,
))
}
}
fn shallow_pinned_clone(source: &str, sha: &str) -> Result<tempfile::TempDir> {
let dir = tempfile::tempdir().context("create shallow clone tempdir")?;
let dir_str = path_str(dir.path())?;
run_git(&["init", "--quiet", dir_str], "init")?;
run_git(
&["-C", dir_str, "remote", "add", "origin", source],
"remote add",
)?;
run_git(
&[
"-C", dir_str, "fetch", "--quiet", "--depth", "1", "origin", sha,
],
"fetch --depth 1",
)?;
run_git(
&[
"-C",
dir_str,
"checkout",
"--quiet",
"--detach",
"FETCH_HEAD",
],
"checkout FETCH_HEAD",
)?;
Ok(dir)
}
fn run_git(git_args: &[&str], what: &str) -> Result<()> {
let out = std::process::Command::new("git")
.args(git_args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.output()
.with_context(|| format!("spawn git {what}"))?;
if out.status.success() {
Ok(())
} else {
Err(anyhow::anyhow!(
"git {what} failed: {}",
String::from_utf8_lossy(&out.stderr).trim()
))
}
}
fn path_str(p: &std::path::Path) -> Result<&str> {
p.to_str()
.with_context(|| format!("non-UTF-8 path {}", p.display()))
}
fn pool_complexity(
db: &FactsDb,
obs: &mut codelore_lib::calibration::LangObservations,
) -> Result<()> {
use codelore_lib::complexity::Tier1Language;
type Row = (
String,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
Option<i64>,
);
let mut stmt = db
.prepare(
"SELECT path, cyclomatic, cognitive, sloc, nargs, max_nesting \
FROM complexity_metrics",
)
.context("prepare complexity query")?;
let rows = stmt
.query_map([], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, Option<i64>>(1)?,
r.get::<_, Option<i64>>(2)?,
r.get::<_, Option<i64>>(3)?,
r.get::<_, Option<i64>>(4)?,
r.get::<_, Option<i64>>(5)?,
))
})
.context("run complexity query")?;
for row in rows {
let (path, cyclomatic, cognitive, sloc, nargs, max_nesting): Row =
row.context("read complexity row")?;
let Some(lang) = Tier1Language::from_path(&path) else {
continue;
};
let lang = lang.as_str();
for (metric, value) in [
("cyclomatic", cyclomatic),
("cognitive", cognitive),
("sloc", sloc),
("nargs", nargs),
("max_nesting", max_nesting),
] {
if let Some(value) = value {
obs.observe(lang, metric, value_to_f64(value));
}
}
}
Ok(())
}
#[allow(clippy::cast_precision_loss)]
fn value_to_f64(n: i64) -> f64 {
n as f64
}
fn pool_repo_metrics(
db: &FactsDb,
pools: &mut codelore_lib::calibration::RepoMetrics,
) -> Result<()> {
use codelore_lib::cli_api::analyses::import_graph::{build_import_graph, graph_metrics};
let graph = build_import_graph(db).context("build import graph")?;
if graph.is_empty() {
tracing::debug!(
"calibrate: empty import graph (no Tier-1 imports); skipping repo-level metric pooling"
);
return Ok(());
}
let m = graph_metrics(&graph);
let n = f64::from(u32::try_from(m.n.max(1)).unwrap_or(u32::MAX));
let cycle_file_share = f64::from(m.cyclic_nodes) / n;
pools
.values
.entry("propagation_cost".to_string())
.or_default()
.push(m.propagation_cost);
pools
.values
.entry("cycle_file_share".to_string())
.or_default()
.push(cycle_file_share);
Ok(())
}