refactor: extract who result types to core::who_types for TUI reuse

Move the 14 result structs and enums (WhoResult, ExpertResult, Expert,
ScoreComponents, ExpertMrDetail, WorkloadResult, WorkloadIssue, WorkloadMr,
WorkloadDiscussion, ReviewsResult, ReviewCategory, ActiveResult,
ActiveDiscussion, OverlapResult, OverlapUser) from cli::commands::who into
a new core::who_types module.

The TUI Who screen needs these types to render results, but importing from
the CLI layer would create a circular dependency (TUI -> CLI -> core). By
placing them in core, both the CLI and TUI can depend on them cleanly.

The CLI module re-exports all types via `pub use crate::core::who_types::*`
so existing consumers are unaffected.
This commit is contained in:
teernisse
2026-02-18 15:37:23 -05:00
parent 050e00345a
commit c1b1300675
4 changed files with 202 additions and 169 deletions

View File

@@ -36,7 +36,7 @@ pub use list::{
ListFilters, MrListFilters, NoteListFilters, open_issue_in_browser, open_mr_in_browser, 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_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, 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::{ pub use search::{
SearchCliFilters, SearchResponse, print_search_results, print_search_results_json, run_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 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 timeline::{TimelineParams, print_timeline, print_timeline_json_with_meta, run_timeline};
pub use trace::{parse_trace_path, print_trace, print_trace_json}; 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,
};

View File

@@ -79,6 +79,15 @@ fn resolve_mode<'a>(args: &'a WhoArgs) -> Result<WhoMode<'a>> {
} }
// ─── Result Types ──────────────────────────────────────────────────────────── // ─── 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. /// Top-level run result: carries resolved inputs + the mode-specific result.
pub struct WhoRun { pub struct WhoRun {
@@ -98,166 +107,6 @@ pub struct WhoResolvedInput {
pub limit: u16, 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<Expert>,
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<f64>,
/// Per-component score breakdown (only populated when explain_score is set).
pub components: Option<ScoreComponents>,
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<String>,
pub mr_refs_total: u32,
pub mr_refs_truncated: bool,
/// Per-MR detail breakdown (only populated when --detail is set)
pub details: Option<Vec<ExpertMrDetail>>,
}
/// 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<WorkloadIssue>,
pub authored_mrs: Vec<WorkloadMr>,
pub reviewing_mrs: Vec<WorkloadMr>,
pub unresolved_discussions: Vec<WorkloadDiscussion>,
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<String>,
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<ReviewCategory>,
}
pub struct ReviewCategory {
pub name: String,
pub count: u32,
pub percentage: f64,
}
// --- Active ---
pub struct ActiveResult {
pub discussions: Vec<ActiveDiscussion>,
/// 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<String>,
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<OverlapUser>,
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<String>,
pub mr_refs_total: u32,
pub mr_refs_truncated: bool,
}
/// Maximum MR references to retain per user in output (shared across modes). /// Maximum MR references to retain per user in output (shared across modes).
const MAX_MR_REFS_PER_USER: usize = 50; const MAX_MR_REFS_PER_USER: usize = 50;
@@ -483,7 +332,7 @@ fn resolve_since_required(input: &str) -> Result<i64> {
/// ///
/// Returns `0.0` when `half_life_days` is zero (prevents division by zero). /// 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). /// 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 days = (elapsed_ms as f64 / 86_400_000.0).max(0.0);
let hl = f64::from(half_life_days); let hl = f64::from(half_life_days);
if hl <= 0.0 { if hl <= 0.0 {
@@ -495,7 +344,7 @@ fn half_life_decay(elapsed_ms: i64, half_life_days: u32) -> f64 {
// ─── Query: Expert Mode ───────────────────────────────────────────────────── // ─── Query: Expert Mode ─────────────────────────────────────────────────────
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn query_expert( pub fn query_expert(
conn: &Connection, conn: &Connection,
path: &str, path: &str,
project_id: Option<i64>, project_id: Option<i64>,
@@ -1150,7 +999,7 @@ fn query_expert_details(
// ─── Query: Workload Mode ─────────────────────────────────────────────────── // ─── Query: Workload Mode ───────────────────────────────────────────────────
fn query_workload( pub fn query_workload(
conn: &Connection, conn: &Connection,
username: &str, username: &str,
project_id: Option<i64>, project_id: Option<i64>,
@@ -1336,7 +1185,7 @@ fn query_workload(
// ─── Query: Reviews Mode ──────────────────────────────────────────────────── // ─── Query: Reviews Mode ────────────────────────────────────────────────────
fn query_reviews( pub fn query_reviews(
conn: &Connection, conn: &Connection,
username: &str, username: &str,
project_id: Option<i64>, project_id: Option<i64>,
@@ -1432,7 +1281,7 @@ fn query_reviews(
}) })
.collect(); .collect();
categories.sort_by(|a, b| b.count.cmp(&a.count)); categories.sort_by_key(|cat| std::cmp::Reverse(cat.count));
Ok(ReviewsResult { Ok(ReviewsResult {
username: username.to_string(), username: username.to_string(),
@@ -1463,7 +1312,7 @@ fn normalize_review_prefix(raw: &str) -> String {
// ─── Query: Active Mode ───────────────────────────────────────────────────── // ─── Query: Active Mode ─────────────────────────────────────────────────────
fn query_active( pub fn query_active(
conn: &Connection, conn: &Connection,
project_id: Option<i64>, project_id: Option<i64>,
since_ms: i64, since_ms: i64,
@@ -1686,7 +1535,7 @@ fn query_active(
// ─── Query: Overlap Mode ──────────────────────────────────────────────────── // ─── Query: Overlap Mode ────────────────────────────────────────────────────
fn query_overlap( pub fn query_overlap(
conn: &Connection, conn: &Connection,
path: &str, path: &str,
project_id: Option<i64>, project_id: Option<i64>,

View File

@@ -22,6 +22,7 @@ pub mod timeline_collect;
pub mod timeline_expand; pub mod timeline_expand;
pub mod timeline_seed; pub mod timeline_seed;
pub mod trace; pub mod trace;
pub mod who_types;
pub use config::Config; pub use config::Config;
pub use error::{LoreError, Result}; pub use error::{LoreError, Result};

180
src/core/who_types.rs Normal file
View File

@@ -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<Expert>,
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<f64>,
/// Per-component score breakdown (only populated when explain_score is set).
pub components: Option<ScoreComponents>,
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<String>,
pub mr_refs_total: u32,
pub mr_refs_truncated: bool,
/// Per-MR detail breakdown (only populated when --detail is set)
pub details: Option<Vec<ExpertMrDetail>>,
}
/// 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<WorkloadIssue>,
pub authored_mrs: Vec<WorkloadMr>,
pub reviewing_mrs: Vec<WorkloadMr>,
pub unresolved_discussions: Vec<WorkloadDiscussion>,
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<String>,
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<ReviewCategory>,
}
#[derive(Debug)]
pub struct ReviewCategory {
pub name: String,
pub count: u32,
pub percentage: f64,
}
// ─── Active ──────────────────────────────────────────────────────────────────
#[derive(Debug)]
pub struct ActiveResult {
pub discussions: Vec<ActiveDiscussion>,
/// 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<String>,
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<OverlapUser>,
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<String>,
pub mr_refs_total: u32,
pub mr_refs_truncated: bool,
}