feat(me): add lore me personal work dashboard command

Implement a personal work dashboard that shows everything relevant to the
configured GitLab user: open issues assigned to them, MRs they authored,
MRs they are reviewing, and a chronological activity feed.

Design decisions:
- Attention state computed from GitLab interaction data (comments, reviews)
  with no local state tracking -- purely derived from existing synced data
- Username resolution: --user flag > config.gitlab.username > actionable error
- Project scoping: --project (fuzzy) | --all | default_project | all
- Section filtering: --issues, --mrs, --activity (combinable, default = all)
- Activity feed controlled by --since (default 30d); work item sections
  always show all open items regardless of --since

Architecture (src/cli/commands/me/):
- types.rs: MeDashboard, MeSummary, AttentionState data types
- queries.rs: 4 SQL queries (open_issues, authored_mrs, reviewing_mrs,
  activity) using existing issue_assignees, mr_reviewers, notes tables
- render_human.rs: colored terminal output with attention state indicators
- render_robot.rs: {ok, data, meta} JSON envelope with field selection
- mod.rs: orchestration (resolve_username, resolve_project_scope, run_me)
- me_tests.rs: comprehensive unit tests covering all query paths

Config additions:
- New optional gitlab.username field in config.json
- Tests for config with/without username
- Existing test configs updated with username: None

CLI wiring:
- MeArgs struct with section filter, since, project, all, user, fields flags
- Autocorrect support for me command flags
- LoreRenderer::try_get() for safe renderer access in me module
- Robot mode field selection presets (me_items, me_activity)
- handle_me() in main.rs command dispatch

Also fixes duplicate assertions in surgical sync tests (removed 6
duplicate assert! lines that were copy-paste artifacts).

Spec: docs/lore-me-spec.md
This commit is contained in:
teernisse
2026-02-20 14:25:08 -05:00
parent a5c2589c7d
commit 9c1a9bfe5d
16 changed files with 3060 additions and 10 deletions

View File

@@ -30,6 +30,7 @@ fn test_config() -> Config {
base_url: "https://gitlab.example.com".to_string(),
token_env_var: "GITLAB_TOKEN".to_string(),
token: None,
username: None,
},
projects: vec![ProjectConfig {
path: "group/repo".to_string(),
@@ -167,7 +168,6 @@ fn test_ingest_issue_by_iid_upserts_and_marks_dirty() {
let result = ingest_issue_by_iid(&conn, &config, 1, &issue).unwrap();
assert!(!result.skipped_stale);
assert!(!result.skipped_stale);
assert!(!result.dirty_source_keys.is_empty());
@@ -199,7 +199,6 @@ fn test_toctou_skips_stale_issue() {
// Second ingest with same timestamp should be skipped
let r2 = ingest_issue_by_iid(&conn, &config, 1, &issue).unwrap();
assert!(r2.skipped_stale);
assert!(r2.skipped_stale);
assert!(r2.dirty_source_keys.is_empty());
// No new dirty mark
@@ -223,7 +222,6 @@ fn test_toctou_allows_newer_issue() {
let result = ingest_issue_by_iid(&conn, &config, 1, &issue_t2).unwrap();
assert!(!result.skipped_stale);
assert!(!result.skipped_stale);
}
#[test]
@@ -271,7 +269,6 @@ fn test_ingest_mr_by_iid_upserts_and_marks_dirty() {
let result = ingest_mr_by_iid(&conn, &config, 1, &mr).unwrap();
assert!(!result.skipped_stale);
assert!(!result.skipped_stale);
assert!(!result.dirty_source_keys.is_empty());
@@ -298,7 +295,6 @@ fn test_toctou_skips_stale_mr() {
let r2 = ingest_mr_by_iid(&conn, &config, 1, &mr).unwrap();
assert!(r2.skipped_stale);
assert!(r2.skipped_stale);
assert!(r2.dirty_source_keys.is_empty());
}
@@ -316,7 +312,6 @@ fn test_toctou_allows_newer_mr() {
let result = ingest_mr_by_iid(&conn, &config, 1, &mr_t2).unwrap();
assert!(!result.skipped_stale);
assert!(!result.skipped_stale);
}
#[test]