#![allow(clippy::doc_markdown)]
use duckdb::params;
use crate::facts::FactsDb;
use crate::{CodeLoreError, Options, Result};
use std::collections::HashMap;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KnowledgeIslandRow {
pub entity: String,
pub main_author: String,
pub ownership_pct: f64,
pub days_since_main_active: i32,
pub last_main_author_commit: String,
pub n_substantial_others: u32,
}
#[derive(Debug, Clone)]
pub struct OwnerActivity {
pub main_author: String,
pub ownership_pct: f64,
pub days_since_main_active: i32,
pub last_main_author_commit: String,
pub n_substantial_others: u32,
}
fn anchor_timestamp(opts: &Options) -> String {
if let Some(d) = opts.age_time_now {
format!(
"{:04}-{:02}-{:02} 23:59:59",
d.year(),
u8::from(d.month()),
d.day()
)
} else {
let n = time::OffsetDateTime::now_utc();
format!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
n.year(),
u8::from(n.month()),
n.day(),
n.hour(),
n.minute(),
n.second(),
)
}
}
const AUTHOR_LAST_COMMIT_CTE: &str = "author_last_commit AS (
SELECT canonical_author AS author, MAX(date) AS last_at
FROM commits
WHERE date <= CAST(? AS TIMESTAMP)
GROUP BY canonical_author
)";
const TOTALS_CTE: &str = "totals AS (
SELECT path, SUM(loc) AS total_loc
FROM per_path_author
GROUP BY path
HAVING SUM(loc) > 0
)";
const MAIN_PER_PATH_CTE: &str = "main_per_path AS (
SELECT
path,
first(author ORDER BY loc DESC, author ASC) AS author,
MAX(loc) AS loc
FROM per_path_author
GROUP BY path
)";
const SUBSTANTIAL_OTHERS_CTE: &str = "substantial_others AS (
SELECT
ppa.path,
CAST(SUM(CASE
WHEN ppa.author != m.author
AND t.total_loc > 0
AND CAST(ppa.loc AS DOUBLE) / t.total_loc >= ?
THEN 1 ELSE 0
END) AS UINTEGER) AS n_others
FROM per_path_author ppa
INNER JOIN totals t ON ppa.path = t.path
INNER JOIN main_per_path m ON m.path = ppa.path
GROUP BY ppa.path
)";
const OWNER_ACTIVITY_SELECT_CORE: &str = "SELECT
m.path AS entity,
m.author AS main_author,
100.0 * m.loc / NULLIF(t.total_loc, 0) AS ownership_pct,
DATE_DIFF('day', alc.last_at, CAST(? AS TIMESTAMP)) AS days_since_main_active,
CAST(CAST(alc.last_at AS DATE) AS TEXT) AS last_main_author_commit,
so.n_others AS n_substantial_others
FROM main_per_path m
INNER JOIN totals t ON t.path = m.path
INNER JOIN author_last_commit alc ON alc.author = m.author
INNER JOIN substantial_others so ON so.path = m.path";
fn per_path_author_cte(restrict_to: &str) -> String {
format!(
"per_path_author AS (
-- Filter bots BEFORE aggregating ownership: bots (dependabot,
-- renovate, etc.) don't have knowledge to lose — flagging a
-- dependabot-dominated lockfile as a 'knowledge island' is
-- exactly the false positive that destroys signal credibility.
SELECT
changes.path,
commits.canonical_author AS author,
SUM(COALESCE(changes.loc_added, 0)) AS loc
FROM changes
INNER JOIN commits ON changes.rev = commits.rev
INNER JOIN {restrict_to} USING (path)
LEFT JOIN (
-- Dedupe by canonical: author_aliases is keyed on
-- (raw_name, raw_email); canonical is N:1 (one person owns
-- several name+email identities). The LEFT JOIN tolerates
-- authors without an author_aliases row (legacy / pattern-only
-- classifications).
SELECT canonical, BOOL_OR(is_bot) AS is_bot
FROM author_aliases
GROUP BY canonical
) aa ON aa.canonical = commits.canonical_author
WHERE COALESCE(aa.is_bot, FALSE) = FALSE
AND commits.date <= CAST(? AS TIMESTAMP)
GROUP BY changes.path, commits.canonical_author
)"
)
}
fn knowledge_islands_sql() -> String {
let per_path_author = per_path_author_cte("live_paths");
format!(
"WITH {AUTHOR_LAST_COMMIT_CTE},
live_paths AS (
-- (rev, path) is the changes PK, so COUNT(c.rev) ==
-- COUNT(DISTINCT c.rev) per path — the same `--min-revs`
-- floor every other per-path analysis honors (see
-- hotspots.rs / revisions.rs), applied here so single-commit
-- files don't escape the floor just because this CTE's
-- primary job is liveness detection.
SELECT path FROM (
SELECT c.path,
arg_max(
c.change_type,
ROW(commits.date, -commits.rowid)
) AS change_type,
COUNT(c.rev) AS revs
FROM changes c
INNER JOIN commits ON commits.rev = c.rev
WHERE commits.date <= CAST(? AS TIMESTAMP)
GROUP BY c.path
HAVING revs >= ?
) WHERE change_type != 'deleted'
),
{per_path_author},
{TOTALS_CTE},
{MAIN_PER_PATH_CTE},
{SUBSTANTIAL_OTHERS_CTE}
{OWNER_ACTIVITY_SELECT_CORE}
WHERE DATE_DIFF('day', alc.last_at, CAST(? AS TIMESTAMP)) > ?
ORDER BY ownership_pct DESC, days_since_main_active DESC, entity ASC
LIMIT ?"
)
}
#[tracing::instrument(name = "knowledge-islands", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_knowledge_islands(db: &FactsDb, opts: &Options) -> Result<Vec<KnowledgeIslandRow>> {
let row_limit: i64 = opts.rows_limit.map_or(i64::MAX, i64::from);
let anchor_str = anchor_timestamp(opts);
let substantial_threshold = crate::constants::DEFAULT_SUBSTANTIAL_OWNER_THRESHOLD;
crate::analyses::lineage::materialize_if_needed(db, opts)?;
let sql = crate::analyses::lineage::rewrite(&knowledge_islands_sql(), opts);
super::query::explain_if_requested(
db,
&sql,
params![
anchor_str, anchor_str, opts.min_revs, anchor_str, substantial_threshold, anchor_str, anchor_str, opts.departed_threshold_days, row_limit, ],
"knowledge-islands",
opts,
)?;
super::query::query_map_collect(
db,
&sql,
params![
anchor_str, anchor_str, opts.min_revs, anchor_str, substantial_threshold, anchor_str, anchor_str, opts.departed_threshold_days, row_limit, ],
"knowledge-islands",
|r| {
Ok(KnowledgeIslandRow {
entity: r.get::<_, String>(0)?,
main_author: r.get::<_, String>(1)?,
ownership_pct: r.get::<_, f64>(2)?,
days_since_main_active: i32::try_from(r.get::<_, i64>(3)?).unwrap_or(i32::MAX),
last_main_author_commit: r.get::<_, String>(4)?,
n_substantial_others: r.get::<_, u32>(5)?,
})
},
)
}
pub fn count_live_files(db: &FactsDb) -> Result<u64> {
db.query_row(
"SELECT COUNT(*) FROM (
SELECT c.path,
arg_max(
c.change_type,
ROW(commits.date, -commits.rowid)
) AS change_type
FROM changes c
INNER JOIN commits ON commits.rev = c.rev
GROUP BY c.path
) WHERE change_type != 'deleted'",
[],
|r| r.get::<_, u64>(0),
)
}
pub fn owner_activity_for_paths(
db: &FactsDb,
opts: &Options,
paths: &[String],
) -> Result<HashMap<String, OwnerActivity>> {
if paths.is_empty() {
return Ok(HashMap::new());
}
crate::analyses::lineage::materialize_if_needed(db, opts)?;
let anchor_str = anchor_timestamp(opts);
let substantial_threshold = crate::constants::DEFAULT_SUBSTANTIAL_OWNER_THRESHOLD;
let placeholders = std::iter::repeat_n("(?)", paths.len())
.collect::<Vec<_>>()
.join(",");
let per_path_author = per_path_author_cte("requested_paths");
let sql = format!(
"WITH requested_paths(path) AS (VALUES {placeholders}),
{AUTHOR_LAST_COMMIT_CTE},
{per_path_author},
{TOTALS_CTE},
{MAIN_PER_PATH_CTE},
{SUBSTANTIAL_OTHERS_CTE}
{OWNER_ACTIVITY_SELECT_CORE}"
);
let sql = crate::analyses::lineage::rewrite(&sql, opts);
let mut stmt = db
.conn()
.prepare(&sql)
.map_err(|e| CodeLoreError::Analysis(format!("owner_activity_for_paths prepare: {e}")))?;
let mut bind_params: Vec<&dyn duckdb::ToSql> =
paths.iter().map(|p| p as &dyn duckdb::ToSql).collect();
bind_params.push(&anchor_str); bind_params.push(&anchor_str); bind_params.push(&substantial_threshold); bind_params.push(&anchor_str);
let rows = stmt
.query_map(bind_params.as_slice(), |r| {
Ok((
r.get::<_, String>(0)?,
OwnerActivity {
main_author: r.get::<_, String>(1)?,
ownership_pct: r.get::<_, f64>(2)?,
days_since_main_active: i32::try_from(r.get::<_, i64>(3)?).unwrap_or(i32::MAX),
last_main_author_commit: r.get::<_, String>(4)?,
n_substantial_others: r.get::<_, u32>(5)?,
},
))
})
.map_err(|e| CodeLoreError::Analysis(format!("owner_activity_for_paths query: {e}")))?;
rows.collect::<std::result::Result<HashMap<_, _>, _>>()
.map_err(|e| CodeLoreError::Analysis(format!("owner_activity_for_paths collect: {e}")))
}