feat(related): add semantic similarity discovery command
Implement `lore related` command for discovering semantically similar entities using vector embeddings. Supports two modes: Entity mode: lore related issues 42 # Find entities similar to issue #42 lore related mrs 99 # Find entities similar to MR !99 Query mode: lore related "auth bug" # Find entities matching free text query Key features: - Uses existing embedding infrastructure (nomic-embed-text via Ollama) - Computes shared labels between source and results - Shows similarity scores as percentage (0-100%) - Warns when all results have low similarity (<30%) - Warns for short queries (<=2 words) that may produce noisy results - Filters out discussion/note documents, returning only issues and MRs - Handles orphaned documents gracefully (skips if entity deleted) - Robot mode JSON output with {ok, data, meta} envelope Implementation details: - distance_to_similarity() converts L2 distance to 0-1 score: 1/(1+distance) - Uses saturating_add/saturating_mul for overflow safety on limit parameter - Proper error handling for missing embeddings ("run lore embed first") - Project scoping via -p flag with fuzzy matching CLI integration: - Added to autocorrect.rs command registry - Added Related variant to Commands enum in cli/mod.rs - Wired into main.rs with handle_related() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
59
src/main.rs
59
src/main.rs
@@ -18,15 +18,16 @@ use lore::cli::commands::{
|
||||
print_event_count, print_event_count_json, print_file_history, print_file_history_json,
|
||||
print_generate_docs, print_generate_docs_json, print_ingest_summary, print_ingest_summary_json,
|
||||
print_list_issues, print_list_issues_json, print_list_mrs, print_list_mrs_json,
|
||||
print_list_notes, print_list_notes_json, print_search_results, print_search_results_json,
|
||||
print_show_issue, print_show_issue_json, print_show_mr, print_show_mr_json, print_stats,
|
||||
print_stats_json, print_sync, print_sync_json, print_sync_status, print_sync_status_json,
|
||||
print_timeline, print_timeline_json_with_meta, print_trace, print_trace_json, print_who_human,
|
||||
print_who_json, query_notes, run_auth_test, run_count, run_count_events, run_cron_install,
|
||||
run_cron_status, run_cron_uninstall, run_doctor, run_drift, run_embed, run_file_history,
|
||||
run_generate_docs, run_ingest, run_ingest_dry_run, run_init, run_list_issues, run_list_mrs,
|
||||
run_me, run_search, run_show_issue, run_show_mr, run_stats, run_sync, run_sync_status,
|
||||
run_timeline, run_token_set, run_token_show, run_who,
|
||||
print_list_notes, print_list_notes_json, print_related_human, print_related_json,
|
||||
print_search_results, print_search_results_json, print_show_issue, print_show_issue_json,
|
||||
print_show_mr, print_show_mr_json, print_stats, print_stats_json, print_sync, print_sync_json,
|
||||
print_sync_status, print_sync_status_json, print_timeline, print_timeline_json_with_meta,
|
||||
print_trace, print_trace_json, print_who_human, print_who_json, query_notes, run_auth_test,
|
||||
run_count, run_count_events, run_cron_install, run_cron_status, run_cron_uninstall, run_doctor,
|
||||
run_drift, run_embed, run_file_history, run_generate_docs, run_ingest, run_ingest_dry_run,
|
||||
run_init, run_list_issues, run_list_mrs, run_me, run_related, run_search, run_show_issue,
|
||||
run_show_mr, run_stats, run_sync, run_sync_status, run_timeline, run_token_set, run_token_show,
|
||||
run_who,
|
||||
};
|
||||
use lore::cli::render::{ColorMode, GlyphMode, Icons, LoreRenderer, Theme};
|
||||
use lore::cli::robot::{RobotMeta, strip_schemas};
|
||||
@@ -225,6 +226,22 @@ async fn main() {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Some(Commands::Related {
|
||||
query_or_type,
|
||||
iid,
|
||||
limit,
|
||||
project,
|
||||
}) => {
|
||||
handle_related(
|
||||
cli.config.as_deref(),
|
||||
&query_or_type,
|
||||
iid,
|
||||
limit,
|
||||
project.as_deref(),
|
||||
robot_mode,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Some(Commands::Stats(args)) => handle_stats(cli.config.as_deref(), args, robot_mode).await,
|
||||
Some(Commands::Embed(args)) => handle_embed(cli.config.as_deref(), args, robot_mode).await,
|
||||
Some(Commands::Sync(args)) => {
|
||||
@@ -1996,7 +2013,7 @@ async fn handle_timeline(
|
||||
if robot_mode {
|
||||
print_timeline_json_with_meta(
|
||||
&result,
|
||||
result.total_events_before_limit,
|
||||
result.total_filtered_events,
|
||||
params.depth,
|
||||
!params.no_mentions,
|
||||
args.fields.as_deref(),
|
||||
@@ -3256,6 +3273,28 @@ async fn handle_drift(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_related(
|
||||
config_override: Option<&str>,
|
||||
query_or_type: &str,
|
||||
iid: Option<i64>,
|
||||
limit: usize,
|
||||
project: Option<&str>,
|
||||
robot_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let start = std::time::Instant::now();
|
||||
let config = Config::load(config_override)?;
|
||||
let effective_project = config.effective_project(project);
|
||||
let response = run_related(&config, query_or_type, iid, limit, effective_project).await?;
|
||||
let elapsed_ms = start.elapsed().as_millis() as u64;
|
||||
|
||||
if robot_mode {
|
||||
print_related_json(&response, elapsed_ms);
|
||||
} else {
|
||||
print_related_human(&response);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn handle_list_compat(
|
||||
config_override: Option<&str>,
|
||||
|
||||
Reference in New Issue
Block a user