#![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,
pub total_loc: 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,
CAST(t.total_loc AS UINTEGER) AS total_loc
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.
-- Pair-granular: joins on the exact (raw_name, raw_email) that
-- made the commit, so a human sharing a canonical with a bot
-- (a --team-map fold, a name/email pattern hit, or the raw-email
-- canonical fallback) keeps their own commits' LoC counted while
-- the bot pair's LoC is dropped row-wise.
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)
INNER JOIN human_aliases ha
ON ha.raw_name = commits.author_name
AND ha.raw_email = commits.author_email
WHERE 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");
let human_aliases = super::query::HUMAN_ALIASES_CTE;
format!(
"WITH {human_aliases},
{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)?,
total_loc: r.get::<_, u32>(6)?,
})
},
)
}
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 human_aliases = super::query::HUMAN_ALIASES_CTE;
let sql = format!(
"WITH requested_paths(path) AS (VALUES {placeholders}),
{human_aliases},
{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}")))
}