diff --git a/src/cli/commands/mod.rs b/src/cli/commands/mod.rs index d7a22f6..f83a35c 100644 --- a/src/cli/commands/mod.rs +++ b/src/cli/commands/mod.rs @@ -36,7 +36,7 @@ pub use list::{ ListFilters, MrListFilters, NoteListFilters, open_issue_in_browser, open_mr_in_browser, print_list_issues, print_list_issues_json, print_list_mrs, print_list_mrs_json, print_list_notes, print_list_notes_csv, print_list_notes_json, print_list_notes_jsonl, - query_notes, run_list_issues, run_list_mrs, + query_issues, query_mrs, query_notes, run_list_issues, run_list_mrs, }; pub use search::{ SearchCliFilters, SearchResponse, print_search_results, print_search_results_json, run_search, @@ -50,4 +50,7 @@ pub use sync::{SyncOptions, SyncResult, print_sync, print_sync_json, run_sync}; pub use sync_status::{print_sync_status, print_sync_status_json, run_sync_status}; pub use timeline::{TimelineParams, print_timeline, print_timeline_json_with_meta, run_timeline}; pub use trace::{parse_trace_path, print_trace, print_trace_json}; -pub use who::{WhoRun, print_who_human, print_who_json, run_who}; +pub use who::{ + WhoRun, half_life_decay, print_who_human, print_who_json, query_active, query_expert, + query_overlap, query_reviews, query_workload, run_who, +}; diff --git a/src/cli/commands/who.rs b/src/cli/commands/who.rs index c105685..0daa860 100644 --- a/src/cli/commands/who.rs +++ b/src/cli/commands/who.rs @@ -79,6 +79,15 @@ fn resolve_mode<'a>(args: &'a WhoArgs) -> Result> { } // ─── Result Types ──────────────────────────────────────────────────────────── +// +// Shared result types live in core::who_types. Re-exported here for +// backwards-compatible use within the CLI. + +pub use crate::core::who_types::{ + ActiveDiscussion, ActiveResult, Expert, ExpertMrDetail, ExpertResult, OverlapResult, + OverlapUser, ReviewCategory, ReviewsResult, ScoreComponents, WhoResult, WorkloadDiscussion, + WorkloadIssue, WorkloadMr, WorkloadResult, +}; /// Top-level run result: carries resolved inputs + the mode-specific result. pub struct WhoRun { @@ -98,166 +107,6 @@ pub struct WhoResolvedInput { pub limit: u16, } -/// Top-level result enum -- one variant per mode. -pub enum WhoResult { - Expert(ExpertResult), - Workload(WorkloadResult), - Reviews(ReviewsResult), - Active(ActiveResult), - Overlap(OverlapResult), -} - -// --- Expert --- - -pub struct ExpertResult { - pub path_query: String, - /// "exact" or "prefix" -- how the path was matched in SQL. - pub path_match: String, - pub experts: Vec, - pub truncated: bool, -} - -pub struct Expert { - pub username: String, - pub score: i64, - /// Unrounded f64 score (only populated when explain_score is set). - pub score_raw: Option, - /// Per-component score breakdown (only populated when explain_score is set). - pub components: Option, - pub review_mr_count: u32, - pub review_note_count: u32, - pub author_mr_count: u32, - pub last_seen_ms: i64, - /// Stable MR references like "group/project!123" - pub mr_refs: Vec, - pub mr_refs_total: u32, - pub mr_refs_truncated: bool, - /// Per-MR detail breakdown (only populated when --detail is set) - pub details: Option>, -} - -/// Per-component score breakdown for explain mode. -pub struct ScoreComponents { - pub author: f64, - pub reviewer_participated: f64, - pub reviewer_assigned: f64, - pub notes: f64, -} - -#[derive(Clone)] -pub struct ExpertMrDetail { - pub mr_ref: String, - pub title: String, - /// "R", "A", or "A+R" - pub role: String, - pub note_count: u32, - pub last_activity_ms: i64, -} - -// --- Workload --- - -pub struct WorkloadResult { - pub username: String, - pub assigned_issues: Vec, - pub authored_mrs: Vec, - pub reviewing_mrs: Vec, - pub unresolved_discussions: Vec, - pub assigned_issues_truncated: bool, - pub authored_mrs_truncated: bool, - pub reviewing_mrs_truncated: bool, - pub unresolved_discussions_truncated: bool, -} - -pub struct WorkloadIssue { - pub iid: i64, - /// Canonical reference: `group/project#iid` - pub ref_: String, - pub title: String, - pub project_path: String, - pub updated_at: i64, -} - -pub struct WorkloadMr { - pub iid: i64, - /// Canonical reference: `group/project!iid` - pub ref_: String, - pub title: String, - pub draft: bool, - pub project_path: String, - pub author_username: Option, - pub updated_at: i64, -} - -pub struct WorkloadDiscussion { - pub entity_type: String, - pub entity_iid: i64, - /// Canonical reference: `group/project!iid` or `group/project#iid` - pub ref_: String, - pub entity_title: String, - pub project_path: String, - pub last_note_at: i64, -} - -// --- Reviews --- - -pub struct ReviewsResult { - pub username: String, - pub total_diffnotes: u32, - pub categorized_count: u32, - pub mrs_reviewed: u32, - pub categories: Vec, -} - -pub struct ReviewCategory { - pub name: String, - pub count: u32, - pub percentage: f64, -} - -// --- Active --- - -pub struct ActiveResult { - pub discussions: Vec, - /// Count of unresolved discussions *within the time window*, not total across all time. - pub total_unresolved_in_window: u32, - pub truncated: bool, -} - -pub struct ActiveDiscussion { - pub discussion_id: i64, - pub entity_type: String, - pub entity_iid: i64, - pub entity_title: String, - pub project_path: String, - pub last_note_at: i64, - pub note_count: u32, - pub participants: Vec, - pub participants_total: u32, - pub participants_truncated: bool, -} - -// --- Overlap --- - -pub struct OverlapResult { - pub path_query: String, - /// "exact" or "prefix" -- how the path was matched in SQL. - pub path_match: String, - pub users: Vec, - pub truncated: bool, -} - -pub struct OverlapUser { - pub username: String, - pub author_touch_count: u32, - pub review_touch_count: u32, - pub touch_count: u32, - pub last_seen_at: i64, - /// Stable MR references like "group/project!123" - pub mr_refs: Vec, - pub mr_refs_total: u32, - pub mr_refs_truncated: bool, -} - /// Maximum MR references to retain per user in output (shared across modes). const MAX_MR_REFS_PER_USER: usize = 50; @@ -483,7 +332,7 @@ fn resolve_since_required(input: &str) -> Result { /// /// Returns `0.0` when `half_life_days` is zero (prevents division by zero). /// Negative elapsed values are clamped to zero (future events retain full weight). -fn half_life_decay(elapsed_ms: i64, half_life_days: u32) -> f64 { +pub fn half_life_decay(elapsed_ms: i64, half_life_days: u32) -> f64 { let days = (elapsed_ms as f64 / 86_400_000.0).max(0.0); let hl = f64::from(half_life_days); if hl <= 0.0 { @@ -495,7 +344,7 @@ fn half_life_decay(elapsed_ms: i64, half_life_days: u32) -> f64 { // ─── Query: Expert Mode ───────────────────────────────────────────────────── #[allow(clippy::too_many_arguments)] -fn query_expert( +pub fn query_expert( conn: &Connection, path: &str, project_id: Option, @@ -1150,7 +999,7 @@ fn query_expert_details( // ─── Query: Workload Mode ─────────────────────────────────────────────────── -fn query_workload( +pub fn query_workload( conn: &Connection, username: &str, project_id: Option, @@ -1336,7 +1185,7 @@ fn query_workload( // ─── Query: Reviews Mode ──────────────────────────────────────────────────── -fn query_reviews( +pub fn query_reviews( conn: &Connection, username: &str, project_id: Option, @@ -1432,7 +1281,7 @@ fn query_reviews( }) .collect(); - categories.sort_by(|a, b| b.count.cmp(&a.count)); + categories.sort_by_key(|cat| std::cmp::Reverse(cat.count)); Ok(ReviewsResult { username: username.to_string(), @@ -1463,7 +1312,7 @@ fn normalize_review_prefix(raw: &str) -> String { // ─── Query: Active Mode ───────────────────────────────────────────────────── -fn query_active( +pub fn query_active( conn: &Connection, project_id: Option, since_ms: i64, @@ -1686,7 +1535,7 @@ fn query_active( // ─── Query: Overlap Mode ──────────────────────────────────────────────────── -fn query_overlap( +pub fn query_overlap( conn: &Connection, path: &str, project_id: Option, diff --git a/src/core/mod.rs b/src/core/mod.rs index 9bef0f6..2893c5c 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -22,6 +22,7 @@ pub mod timeline_collect; pub mod timeline_expand; pub mod timeline_seed; pub mod trace; +pub mod who_types; pub use config::Config; pub use error::{LoreError, Result}; diff --git a/src/core/who_types.rs b/src/core/who_types.rs new file mode 100644 index 0000000..03cf2ce --- /dev/null +++ b/src/core/who_types.rs @@ -0,0 +1,180 @@ +//! Shared types for the `who` people-intelligence queries. +//! +//! These types are the data contract consumed by both the CLI renderer +//! and the TUI view layer. They contain no CLI or rendering logic. + +// ─── Top-Level Result ──────────────────────────────────────────────────────── + +/// Top-level result enum -- one variant per mode. +#[derive(Debug)] +pub enum WhoResult { + Expert(ExpertResult), + Workload(WorkloadResult), + Reviews(ReviewsResult), + Active(ActiveResult), + Overlap(OverlapResult), +} + +// ─── Expert ────────────────────────────────────────────────────────────────── + +#[derive(Debug)] +pub struct ExpertResult { + pub path_query: String, + /// "exact" or "prefix" -- how the path was matched in SQL. + pub path_match: String, + pub experts: Vec, + pub truncated: bool, +} + +#[derive(Debug)] +pub struct Expert { + pub username: String, + pub score: i64, + /// Unrounded f64 score (only populated when explain_score is set). + pub score_raw: Option, + /// Per-component score breakdown (only populated when explain_score is set). + pub components: Option, + pub review_mr_count: u32, + pub review_note_count: u32, + pub author_mr_count: u32, + pub last_seen_ms: i64, + /// Stable MR references like "group/project!123" + pub mr_refs: Vec, + pub mr_refs_total: u32, + pub mr_refs_truncated: bool, + /// Per-MR detail breakdown (only populated when --detail is set) + pub details: Option>, +} + +/// Per-component score breakdown for explain mode. +#[derive(Debug)] +pub struct ScoreComponents { + pub author: f64, + pub reviewer_participated: f64, + pub reviewer_assigned: f64, + pub notes: f64, +} + +#[derive(Debug, Clone)] +pub struct ExpertMrDetail { + pub mr_ref: String, + pub title: String, + /// "R", "A", or "A+R" + pub role: String, + pub note_count: u32, + pub last_activity_ms: i64, +} + +// ─── Workload ──────────────────────────────────────────────────────────────── + +#[derive(Debug)] +pub struct WorkloadResult { + pub username: String, + pub assigned_issues: Vec, + pub authored_mrs: Vec, + pub reviewing_mrs: Vec, + pub unresolved_discussions: Vec, + pub assigned_issues_truncated: bool, + pub authored_mrs_truncated: bool, + pub reviewing_mrs_truncated: bool, + pub unresolved_discussions_truncated: bool, +} + +#[derive(Debug)] +pub struct WorkloadIssue { + pub iid: i64, + /// Canonical reference: `group/project#iid` + pub ref_: String, + pub title: String, + pub project_path: String, + pub updated_at: i64, +} + +#[derive(Debug)] +pub struct WorkloadMr { + pub iid: i64, + /// Canonical reference: `group/project!iid` + pub ref_: String, + pub title: String, + pub draft: bool, + pub project_path: String, + pub author_username: Option, + pub updated_at: i64, +} + +#[derive(Debug)] +pub struct WorkloadDiscussion { + pub entity_type: String, + pub entity_iid: i64, + /// Canonical reference: `group/project!iid` or `group/project#iid` + pub ref_: String, + pub entity_title: String, + pub project_path: String, + pub last_note_at: i64, +} + +// ─── Reviews ───────────────────────────────────────────────────────────────── + +#[derive(Debug)] +pub struct ReviewsResult { + pub username: String, + pub total_diffnotes: u32, + pub categorized_count: u32, + pub mrs_reviewed: u32, + pub categories: Vec, +} + +#[derive(Debug)] +pub struct ReviewCategory { + pub name: String, + pub count: u32, + pub percentage: f64, +} + +// ─── Active ────────────────────────────────────────────────────────────────── + +#[derive(Debug)] +pub struct ActiveResult { + pub discussions: Vec, + /// Count of unresolved discussions *within the time window*, not total across all time. + pub total_unresolved_in_window: u32, + pub truncated: bool, +} + +#[derive(Debug)] +pub struct ActiveDiscussion { + pub discussion_id: i64, + pub entity_type: String, + pub entity_iid: i64, + pub entity_title: String, + pub project_path: String, + pub last_note_at: i64, + pub note_count: u32, + pub participants: Vec, + pub participants_total: u32, + pub participants_truncated: bool, +} + +// ─── Overlap ───────────────────────────────────────────────────────────────── + +#[derive(Debug)] +pub struct OverlapResult { + pub path_query: String, + /// "exact" or "prefix" -- how the path was matched in SQL. + pub path_match: String, + pub users: Vec, + pub truncated: bool, +} + +#[derive(Debug)] +pub struct OverlapUser { + pub username: String, + pub author_touch_count: u32, + pub review_touch_count: u32, + pub touch_count: u32, + pub last_seen_at: i64, + /// Stable MR references like "group/project!123" + pub mr_refs: Vec, + pub mr_refs_total: u32, + pub mr_refs_truncated: bool, +}