feat(sync): fetch and store GitLab issue links (bd-343o)

Add end-to-end support for GitLab issue link fetching:
- New GitLabIssueLink type + fetch_issue_links API client method
- Migration 029: add issue_links job type and watermark column
- issue_links.rs: bidirectional entity_reference storage with
  self-link skip, cross-project fallback, idempotent upsert
- Drain pipeline in orchestrator following mr_closes_issues pattern
- Display related issues in 'lore show issues' output
- --no-issue-links CLI flag with config, autocorrect, robot-docs
- 6 unit tests for storage logic
This commit is contained in:
teernisse
2026-02-19 09:26:28 -05:00
parent 9a1dbda522
commit 1e679a6d72
18 changed files with 2051 additions and 8 deletions

View File

@@ -5,7 +5,7 @@
//! Built on FrankenTUI (Elm architecture): Model, update, view.
//! The `lore` CLI spawns `lore-tui` via PATH lookup at runtime.
use anyhow::Result;
use anyhow::{Context, Result};
// Phase 0 modules.
pub mod clock; // Clock trait: SystemClock + FakeClock (bd-2lg6)
@@ -71,9 +71,40 @@ pub struct LaunchOptions {
/// 2. **Data readiness** — check whether the database has any entity data.
/// If empty, start on the Bootstrap screen; otherwise start on Dashboard.
pub fn launch_tui(options: LaunchOptions) -> Result<()> {
let _options = options;
// Phase 1 will wire this to LoreApp + App::fullscreen().run()
eprintln!("lore-tui: browse mode not yet implemented (Phase 1)");
// 1. Resolve database path.
let db_path = lore::core::paths::get_db_path(None);
if !db_path.exists() {
anyhow::bail!(
"No lore database found at {}.\n\
Run 'lore init' to create a config, then 'lore sync' to fetch data.",
db_path.display()
);
}
// 2. Open DB and run schema preflight.
let db = db::DbManager::open(&db_path)
.with_context(|| format!("opening database at {}", db_path.display()))?;
db.with_reader(|conn| schema_preflight(conn))?;
// 3. Check data readiness — bootstrap screen if empty.
let start_on_bootstrap = db.with_reader(|conn| {
let readiness = action::check_data_readiness(conn)?;
Ok(!readiness.has_any_data())
})?;
// 4. Build the app model.
let mut app = app::LoreApp::new();
app.db = Some(db);
if start_on_bootstrap {
app.navigation.reset_to(message::Screen::Bootstrap);
}
// 5. Enter the FrankenTUI event loop.
ftui::App::fullscreen(app)
.with_mouse()
.run()
.context("running TUI event loop")?;
Ok(())
}