1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
//! `lead-time` analysis.
//!
//! Computes the time elapsed between a commit's author-date (when
//! the change was authored) and committer-date (when it was merged
//! to mainline). Captures the "how long does code sit in review
//! before shipping?" question — a foundational DORA metric.
//!
//! ## Why per-commit, not per-PR?
//!
//! `CodeLore` is git-only by design — no GitHub PR metadata in scope
//! (see [`project_git_only_scope`](../../.devt/memory/lessons/project_git_only_scope.md)).
//! Git's commit object carries BOTH author-date and committer-date;
//! their delta is the in-flight time. On merge-via-squash workflows
//! this delta is small (the squash commit is born at merge); on
//! merge-via-rebase or merge-via-merge-commit workflows it's the
//! true review-time. Either way it's a defensible proxy.
//!
//! ## Output
//!
//! One row per commit, ordered by lead-time DESC. Useful for
//! identifying stragglers + computing org-wide percentiles.
use duckdb::params;
use crate::facts::FactsDb;
use crate::{CodeLoreError, Options, Result};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LeadTimeRow {
pub rev: String,
pub canonical_author: String,
pub author_date: String,
pub committer_date: String,
/// Seconds between author-date and committer-date. Negative
/// values can occur when the timestamps disagree (clock skew or
/// timezone artefacts); they're clamped to 0 in `lead_time_days`
/// but preserved here for forensic inspection.
pub lead_time_seconds: i64,
/// Convenience field: `max(0, lead_time_seconds) / 86400.0`.
pub lead_time_days: f64,
}
/// Run the `lead-time` analysis. Returns commits ordered by
/// lead-time DESC. Merge commits are excluded — their lead-time is
/// architecturally zero on most workflows.
///
/// ## `commits.date` vs `commits.committer_date` semantics
///
/// `commits.date` is the **author** date (when the commit was first
/// authored — `git commit`'s `%aI`); `commits.committer_date` is when
/// it last entered mainline (`%cI`). On linear-history workflows
/// (rebase, squash) these are often equal — the squash commit is born
/// at merge. On merge-via-merge-commit + review workflows the delta is
/// the true in-flight review time.
///
/// # Errors
///
/// Returns [`CodeLoreError::Analysis`] on `DuckDB` errors.
#[tracing::instrument(name = "lead-time", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_lead_time(db: &FactsDb, opts: &Options) -> Result<Vec<LeadTimeRow>> {
let row_limit: i64 = opts.rows_limit.map_or(i64::MAX, i64::from);
// `date` is the author date; `committer_date` is when the commit
// last entered mainline. Their delta in seconds is the lead time.
// `EPOCH` extraction returns DOUBLE; we cast to BIGINT for the
// typed-row binding.
let sql = "
SELECT
rev,
canonical_author,
CAST(CAST(date AS TIMESTAMP) AS TEXT) AS author_date,
CAST(CAST(committer_date AS TIMESTAMP) AS TEXT) AS committer_date,
CAST(
EXTRACT(EPOCH FROM committer_date) - EXTRACT(EPOCH FROM date)
AS BIGINT
) AS lead_time_seconds
FROM commits
WHERE is_merge = FALSE
AND date IS NOT NULL
AND committer_date IS NOT NULL
ORDER BY lead_time_seconds DESC, rev ASC
LIMIT ?
";
let mut stmt = db
.conn()
.prepare(sql)
.map_err(|e| CodeLoreError::Analysis(format!("prepare lead-time: {e}")))?;
let rows = stmt
.query_map(params![row_limit], |r| {
let lead_time_seconds: i64 = r.get(4)?;
#[allow(clippy::cast_precision_loss)]
// elapsed seconds in any real review cycle are far below 2^52; the i64→f64 cast preserves full precision
let lead_time_days = (lead_time_seconds.max(0) as f64) / 86_400.0;
Ok(LeadTimeRow {
rev: r.get(0)?,
canonical_author: r.get(1)?,
author_date: r.get(2)?,
committer_date: r.get(3)?,
lead_time_seconds,
lead_time_days,
})
})
.map_err(|e| CodeLoreError::Analysis(format!("query lead-time: {e}")))?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| CodeLoreError::Analysis(format!("collect lead-time: {e}")))
}