use serde::Deserialize;
use crate::{CodeLoreError, Result};
#[derive(Debug, Clone, serde::Serialize, Deserialize)]
pub struct XRayEntry {
pub path: String,
pub function: String,
pub cognitive: f64,
pub start_line: u32,
pub end_line: u32,
}
#[derive(Debug, Clone, serde::Serialize, Deserialize)]
pub struct DailyCommit {
pub date: String,
pub count: u32,
}
#[derive(Debug, Clone, serde::Serialize, Deserialize)]
pub struct TrendPoint {
pub month: String,
pub path: String,
pub hotspot_score: f64,
}
#[derive(Debug, Clone, serde::Serialize, Deserialize)]
pub struct CloneSummary {
pub path: String,
pub groups: u32,
}
#[derive(Debug, Clone, serde::Serialize, Deserialize)]
pub struct ImportEdgeRow {
pub src_path: String,
pub target_path: String,
}
#[derive(Debug, Clone, serde::Serialize, Deserialize)]
pub struct KameiRiskRow {
pub rev: String,
pub date: String,
pub la: u32,
pub ld: u32,
pub nf: u32,
pub nd: u32,
pub ndev: u32,
pub nuc: u32,
pub exp: u32,
pub entropy: f64,
pub fix: bool,
}
#[tracing::instrument(name = "dashboard-imports", skip_all)]
pub fn run_imports_for_arch_graph(db: &crate::facts::FactsDb) -> Result<Vec<ImportEdgeRow>> {
let mut stmt = db
.conn()
.prepare(
"SELECT src_path, target_path FROM imports \
WHERE target_path IS NOT NULL \
ORDER BY src_path ASC, target_path ASC",
)
.map_err(|e| CodeLoreError::Analysis(format!("arch-imports prepare: {e}")))?;
let rows = stmt
.query_map([], |r| {
Ok(ImportEdgeRow {
src_path: r.get(0)?,
target_path: r.get(1)?,
})
})
.map_err(|e| CodeLoreError::Analysis(format!("arch-imports query: {e}")))?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| CodeLoreError::Analysis(format!("arch-imports collect: {e}")))
}
#[tracing::instrument(name = "dashboard-xray", skip_all)]
pub fn run_xray(db: &crate::facts::FactsDb, limit: i64) -> Result<Vec<XRayEntry>> {
let mut stmt = db
.conn()
.prepare(
"SELECT cm.path,
cm.name,
cm.cognitive,
CAST(e.start_line AS UINTEGER) AS s_line,
CAST(e.end_line AS UINTEGER) AS e_line
FROM complexity_metrics cm
INNER JOIN entities e
ON e.path = cm.path
AND e.name = cm.name
AND e.rev_last_seen = cm.rev
WHERE cm.cognitive > 0
ORDER BY cm.cognitive DESC, cm.path ASC, cm.name ASC
LIMIT ?",
)
.map_err(|e| CodeLoreError::Analysis(format!("xray prepare: {e}")))?;
let rows = stmt
.query_map([limit], |r| {
Ok(XRayEntry {
path: r.get(0)?,
function: r.get::<_, Option<String>>(1)?.unwrap_or_default(),
cognitive: r.get(2)?,
start_line: r.get(3)?,
end_line: r.get(4)?,
})
})
.map_err(|e| CodeLoreError::Analysis(format!("xray query: {e}")))?;
let out: std::result::Result<Vec<_>, _> = rows.collect();
out.map_err(|e| CodeLoreError::Analysis(format!("xray collect: {e}")))
}
#[tracing::instrument(name = "dashboard-clone-summary", skip_all)]
pub fn run_clone_summary(db: &crate::facts::FactsDb) -> Result<Vec<CloneSummary>> {
let mut stmt = db
.conn()
.prepare(
"SELECT path, COUNT(DISTINCT clone_group_id)::UINTEGER AS groups
FROM clones
GROUP BY path
ORDER BY groups DESC, path ASC",
)
.map_err(|e| CodeLoreError::Analysis(format!("clone_summary prepare: {e}")))?;
let rows = stmt
.query_map([], |r| {
Ok(CloneSummary {
path: r.get(0)?,
groups: r.get(1)?,
})
})
.map_err(|e| CodeLoreError::Analysis(format!("clone_summary query: {e}")))?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| CodeLoreError::Analysis(format!("clone_summary collect: {e}")))
}
#[tracing::instrument(name = "dashboard-trends", skip_all)]
pub fn run_trends(
db: &crate::facts::FactsDb,
opts: &crate::Options,
paths: &[String],
) -> Result<Vec<TrendPoint>> {
if paths.is_empty() {
return Ok(Vec::new());
}
crate::analyses::lineage::materialize_if_needed(db, opts)?;
let src = if opts.use_canonical_lineage && opts.time_bucket.is_none() {
"changes_lineage"
} else {
"changes"
};
let placeholders = std::iter::repeat_n("(?)", paths.len())
.collect::<Vec<_>>()
.join(",");
let sql = format!(
"WITH paths(path) AS (VALUES {placeholders})
SELECT strftime(date_trunc('month', c.date), '%Y-%m-%d') AS month,
ch.path,
CAST(COUNT(*) AS DOUBLE) AS score
FROM commits c
INNER JOIN {src} ch ON ch.rev = c.rev
INNER JOIN paths USING (path)
GROUP BY month, ch.path
ORDER BY month ASC, ch.path ASC"
);
let mut stmt = db
.conn()
.prepare(&sql)
.map_err(|e| CodeLoreError::Analysis(format!("trends prepare: {e}")))?;
let params: Vec<&dyn duckdb::ToSql> = paths.iter().map(|p| p as &dyn duckdb::ToSql).collect();
let rows = stmt
.query_map(params.as_slice(), |r| {
Ok(TrendPoint {
month: r.get(0)?,
path: r.get(1)?,
hotspot_score: r.get(2)?,
})
})
.map_err(|e| CodeLoreError::Analysis(format!("trends query: {e}")))?;
let out: std::result::Result<Vec<_>, _> = rows.collect();
out.map_err(|e| CodeLoreError::Analysis(format!("trends collect: {e}")))
}
#[tracing::instrument(name = "dashboard-daily-commits", skip_all)]
pub fn run_daily_commits(db: &crate::facts::FactsDb) -> Result<Vec<DailyCommit>> {
let mut stmt = db
.conn()
.prepare(
"SELECT CAST(CAST(date AS DATE) AS TEXT) AS d,
CAST(COUNT(*) AS UINTEGER) AS n
FROM commits
GROUP BY CAST(date AS DATE)
ORDER BY d ASC",
)
.map_err(|e| CodeLoreError::Analysis(format!("daily_commits prepare: {e}")))?;
let rows = stmt
.query_map([], |r| {
Ok(DailyCommit {
date: r.get(0)?,
count: r.get(1)?,
})
})
.map_err(|e| CodeLoreError::Analysis(format!("daily_commits query: {e}")))?;
let out: std::result::Result<Vec<_>, _> = rows.collect();
out.map_err(|e| CodeLoreError::Analysis(format!("daily_commits collect: {e}")))
}
#[tracing::instrument(name = "dashboard-kamei-risk", skip_all)]
pub fn run_kamei_risk(db: &crate::facts::FactsDb, limit: i64) -> Result<Vec<KameiRiskRow>> {
let sql = "
WITH recent AS (
SELECT rev, date, la, ld, nf, nd, ndev, nuc, exp, entropy, fix
FROM commits
WHERE is_merge = FALSE AND date IS NOT NULL
ORDER BY date DESC, rowid DESC
LIMIT ?
)
SELECT rev,
strftime(date, '%Y-%m-%d') AS date,
COALESCE(la, 0)::UINTEGER AS la,
COALESCE(ld, 0)::UINTEGER AS ld,
COALESCE(nf, 0)::UINTEGER AS nf,
COALESCE(nd, 0)::UINTEGER AS nd,
COALESCE(ndev, 0)::UINTEGER AS ndev,
COALESCE(nuc, 0)::UINTEGER AS nuc,
COALESCE(exp, 0)::UINTEGER AS exp,
COALESCE(entropy, 0.0) AS entropy,
COALESCE(fix, FALSE) AS fix
FROM recent
ORDER BY date ASC, rev ASC
";
let mut stmt = db
.conn()
.prepare(sql)
.map_err(|e| CodeLoreError::Analysis(format!("kamei_risk prepare: {e}")))?;
let rows = stmt
.query_map([limit], |r| {
Ok(KameiRiskRow {
rev: r.get(0)?,
date: r.get(1)?,
la: r.get(2)?,
ld: r.get(3)?,
nf: r.get(4)?,
nd: r.get(5)?,
ndev: r.get(6)?,
nuc: r.get(7)?,
exp: r.get(8)?,
entropy: r.get(9)?,
fix: r.get(10)?,
})
})
.map_err(|e| CodeLoreError::Analysis(format!("kamei_risk query: {e}")))?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| CodeLoreError::Analysis(format!("kamei_risk collect: {e}")))
}