feat(cli): add 'lore related' semantic similarity command (bd-8con)
Adds 'lore related' / 'lore similar' command for discovering semantically related issues and MRs using vector embeddings. Two modes: - Entity mode: find entities similar to a specific issue/MR - Query mode: embed free text and find matching entities Includes distance-to-similarity conversion, label intersection, human and robot output formatters, and 11 unit tests.
This commit is contained in:
@@ -377,3 +377,120 @@ fn test_sync_completed_from_bootstrap_resets_navigation_and_state() {
|
||||
assert_eq!(app.navigation.depth(), 1);
|
||||
assert!(!app.state.bootstrap.sync_started);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_completed_flushes_entity_caches() {
|
||||
use crate::message::EntityKey;
|
||||
use crate::state::issue_detail::{IssueDetailData, IssueMetadata};
|
||||
use crate::state::mr_detail::{MrDetailData, MrMetadata};
|
||||
use crate::state::{CachedIssuePayload, CachedMrPayload};
|
||||
use crate::view::common::cross_ref::CrossRef;
|
||||
|
||||
let mut app = test_app();
|
||||
|
||||
// Populate caches with dummy data.
|
||||
let issue_key = EntityKey::issue(1, 42);
|
||||
app.state.issue_cache.put(
|
||||
issue_key,
|
||||
CachedIssuePayload {
|
||||
data: IssueDetailData {
|
||||
metadata: IssueMetadata {
|
||||
iid: 42,
|
||||
project_path: "g/p".into(),
|
||||
title: "Test".into(),
|
||||
description: String::new(),
|
||||
state: "opened".into(),
|
||||
author: "alice".into(),
|
||||
assignees: vec![],
|
||||
labels: vec![],
|
||||
milestone: None,
|
||||
due_date: None,
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
web_url: String::new(),
|
||||
discussion_count: 0,
|
||||
},
|
||||
cross_refs: Vec::<CrossRef>::new(),
|
||||
},
|
||||
discussions: vec![],
|
||||
},
|
||||
);
|
||||
|
||||
let mr_key = EntityKey::mr(1, 99);
|
||||
app.state.mr_cache.put(
|
||||
mr_key,
|
||||
CachedMrPayload {
|
||||
data: MrDetailData {
|
||||
metadata: MrMetadata {
|
||||
iid: 99,
|
||||
project_path: "g/p".into(),
|
||||
title: "MR".into(),
|
||||
description: String::new(),
|
||||
state: "opened".into(),
|
||||
draft: false,
|
||||
author: "bob".into(),
|
||||
assignees: vec![],
|
||||
reviewers: vec![],
|
||||
labels: vec![],
|
||||
source_branch: "feat".into(),
|
||||
target_branch: "main".into(),
|
||||
merge_status: String::new(),
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
merged_at: None,
|
||||
web_url: String::new(),
|
||||
discussion_count: 0,
|
||||
file_change_count: 0,
|
||||
},
|
||||
cross_refs: Vec::<CrossRef>::new(),
|
||||
file_changes: vec![],
|
||||
},
|
||||
discussions: vec![],
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(app.state.issue_cache.len(), 1);
|
||||
assert_eq!(app.state.mr_cache.len(), 1);
|
||||
|
||||
// Sync completes — caches should be flushed.
|
||||
app.update(Msg::SyncCompleted { elapsed_ms: 500 });
|
||||
|
||||
assert!(
|
||||
app.state.issue_cache.is_empty(),
|
||||
"issue cache should be flushed after sync"
|
||||
);
|
||||
assert!(
|
||||
app.state.mr_cache.is_empty(),
|
||||
"MR cache should be flushed after sync"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_completed_refreshes_current_detail_view() {
|
||||
use crate::message::EntityKey;
|
||||
use crate::state::LoadState;
|
||||
|
||||
let mut app = test_app();
|
||||
|
||||
// Navigate to an issue detail screen.
|
||||
let key = EntityKey::issue(1, 42);
|
||||
app.update(Msg::NavigateTo(Screen::IssueDetail(key)));
|
||||
|
||||
// Simulate load completion so LoadState goes to Idle.
|
||||
app.state.set_loading(
|
||||
Screen::IssueDetail(EntityKey::issue(1, 42)),
|
||||
LoadState::Idle,
|
||||
);
|
||||
|
||||
// Sync completes while viewing the detail.
|
||||
app.update(Msg::SyncCompleted { elapsed_ms: 300 });
|
||||
|
||||
// The detail screen should have been set to Refreshing.
|
||||
assert_eq!(
|
||||
*app.state
|
||||
.load_state
|
||||
.get(&Screen::IssueDetail(EntityKey::issue(1, 42))),
|
||||
LoadState::Refreshing,
|
||||
"detail view should refresh after sync"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -542,6 +542,11 @@ impl LoreApp {
|
||||
Msg::SyncCompleted { elapsed_ms } => {
|
||||
self.state.sync.complete(elapsed_ms);
|
||||
|
||||
// Flush entity caches — sync may have updated any entity's
|
||||
// metadata, discussions, or cross-refs in the DB.
|
||||
self.state.issue_cache.clear();
|
||||
self.state.mr_cache.clear();
|
||||
|
||||
// If we came from Bootstrap, replace nav history with Dashboard.
|
||||
if *self.navigation.current() == Screen::Bootstrap {
|
||||
self.state.bootstrap.sync_started = false;
|
||||
@@ -557,6 +562,19 @@ impl LoreApp {
|
||||
self.state.set_loading(dashboard.clone(), load_state);
|
||||
let _handle = self.supervisor.submit(TaskKey::LoadScreen(dashboard));
|
||||
}
|
||||
|
||||
// If currently on a detail view, refresh it so the user sees
|
||||
// updated data without navigating away and back.
|
||||
let current = self.navigation.current().clone();
|
||||
match ¤t {
|
||||
Screen::IssueDetail(_) | Screen::MrDetail(_) => {
|
||||
self.state
|
||||
.set_loading(current.clone(), LoadState::Refreshing);
|
||||
let _handle = self.supervisor.submit(TaskKey::LoadScreen(current));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Cmd::none()
|
||||
}
|
||||
Msg::SyncCancelled => {
|
||||
|
||||
Reference in New Issue
Block a user