difflore-cli 0.5.0

Your AI coding agent learned public code, not your team's private decisions. difflore turns past PR reviews into source-backed local rules.
use difflore_core::memory_overview::{
    ActivityOverview, MemoryOverview, MemoryOverviewNextAction, MemoryOverviewOptions,
    OverviewReviewItem, OverviewRule, load_memory_overview,
};

use crate::runtime::CommandContext;
use crate::style;
use crate::support::util::json_compact_or;

use super::exit_structured_err;

const OVERVIEW_LATEST_LIMIT: usize = 5;
const OVERVIEW_ACTIVITY_DAYS: i64 = 30;

pub(crate) async fn handle_summary(ctx: &CommandContext, json: bool) {
    let repo_full_name = current_repo_full_name(ctx).await;
    let mut overview = load_memory_overview(
        &ctx.db,
        MemoryOverviewOptions {
            repo_full_name,
            latest_limit: OVERVIEW_LATEST_LIMIT,
            activity_days: OVERVIEW_ACTIVITY_DAYS,
        },
    )
    .await
    .unwrap_or_else(|err| {
        exit_structured_err(&format!("failed to load memory overview: {err}"), json)
    });
    overview.sync.logged_in = ctx.cloud().await.is_logged_in();

    if json {
        println!("{}", json_compact_or(&overview, "{}"));
        return;
    }

    print_overview(&overview);
}

async fn current_repo_full_name(ctx: &CommandContext) -> Option<String> {
    let configured_gitlab_hosts = difflore_core::ingest::gitlab::auth::configured_hosts().await;
    let project = ctx.project.to_string_lossy();
    difflore_core::infra::git::detect_repo_full_names_with_gitlab_hosts(
        project.as_ref(),
        &configured_gitlab_hosts,
    )
    .into_iter()
    .find_map(|repo| difflore_core::infra::git::RepoScope::canonical(&repo))
    .map(difflore_core::infra::git::RepoScope::into_string)
}

fn print_overview(overview: &MemoryOverview) {
    println!("{}", style::title("Memory"));
    print_remembered(overview);
    print_needs_review(overview);
    print_paused(overview);
    print_sync(overview);
    print_activity(&overview.activity);
    print_latest_remembered(&overview.remembered.latest);
    print_latest_review_items(&overview.needs_review.latest);
    print_next(&overview.next);
}

fn print_remembered(overview: &MemoryOverview) {
    let repo_label = match overview.remembered.active_for_repo {
        Some(count) => format!(
            ", {} for this repo",
            count_phrase(count, "active rule", "active rules")
        ),
        None => String::new(),
    };
    println!(
        "  remembered     {} available to agents{}",
        style::ident(&count_phrase(
            overview.remembered.active_total,
            "active rule",
            "active rules"
        )),
        repo_label
    );
}

fn print_needs_review(overview: &MemoryOverview) {
    let total = overview.needs_review.local_drafts
        + overview.needs_review.local_discoveries
        + overview.needs_review.autopilot_needs_review_groups;
    let rendered = if total > 0 {
        style::warn(&count_phrase(total, "item", "items")).to_string()
    } else {
        style::ident("0 items").to_string()
    };
    println!(
        "  needs review   {} ({}, {}, {})",
        rendered,
        count_phrase(
            overview.needs_review.local_drafts,
            "local draft",
            "local drafts"
        ),
        count_phrase(
            overview.needs_review.local_discoveries,
            "session discovery",
            "session discoveries"
        ),
        count_phrase(
            overview.needs_review.autopilot_needs_review_groups,
            "suggestion",
            "suggestions"
        )
    );
}

fn print_paused(overview: &MemoryOverview) {
    println!(
        "  paused         {} kept out of recall",
        style::ident(&count_phrase(
            overview.paused.count,
            "disabled rule",
            "disabled rules"
        ))
    );
}

fn print_sync(overview: &MemoryOverview) {
    let auth = if overview.sync.logged_in {
        style::ok("logged in").to_string()
    } else {
        style::pewter("local only").to_string()
    };
    let pending = overview.sync.approved_session_candidates_pending_upload
        + overview.sync.activity_records_pending_upload;
    println!(
        "  sync           {} | {} pending upload",
        auth,
        count_phrase(pending, "record", "records")
    );
}

fn print_activity(activity: &ActivityOverview) {
    let empty_note = if activity.recall_calls > 0 {
        format!(
            ", {} empty",
            count_phrase(activity.empty_recalls, "recall", "recalls")
        )
    } else {
        String::new()
    };
    println!(
        "  value          {}: {}, {} surfaced{}",
        format!("{}d", activity.window_days),
        count_phrase(activity.recall_calls, "recall", "recalls"),
        count_phrase(activity.rules_surfaced, "rule", "rules"),
        empty_note
    );
}

fn print_latest_remembered(items: &[OverviewRule]) {
    if items.is_empty() {
        return;
    }
    println!();
    println!("{}", style::title("Recently remembered"));
    for item in items.iter().take(OVERVIEW_LATEST_LIMIT) {
        let repo = item
            .source_repo
            .as_deref()
            .filter(|value| !value.trim().is_empty())
            .unwrap_or("global");
        println!(
            "  {} {} {}",
            style::pewter(style::sym::BULLET),
            style::ident(&item.title),
            style::pewter(&format!("{} | {}", item.item_id, repo))
        );
    }
}

fn print_latest_review_items(items: &[OverviewReviewItem]) {
    if items.is_empty() {
        return;
    }
    println!();
    println!("{}", style::title("Needs review"));
    for item in items.iter().take(OVERVIEW_LATEST_LIMIT) {
        println!(
            "  {} {} {}",
            style::warn(style::sym::WARN),
            style::ident(&item.title),
            style::pewter(&item.item_id)
        );
        println!("    inspect: {}", style::cmd(&item.commands.show));
    }
}

fn print_next(next: &MemoryOverviewNextAction) {
    println!();
    match next.command.as_deref() {
        Some(command) => println!("next: {}", style::cmd(command)),
        None => println!("next: {}", style::ok(&next.label)),
    }
    println!("      {}", style::pewter(&next.reason));
}

fn count_phrase(count: i64, singular: &str, plural_word: &str) -> String {
    let noun = if count == 1 { singular } else { plural_word };
    format!("{count} {noun}")
}