feat(explain): implement lore explain command for auto-generating issue/MR narratives

Adds the full explain command with 7 output sections: entity summary, description,
key decisions (heuristic event-note correlation), activity summary, open threads,
related entities (closing MRs, cross-references), and timeline excerpt (reuses
existing pipeline). Supports --sections filtering, --since time scoping,
--no-timeline, --max-decisions, and robot mode JSON output.

Closes: bd-2i3z, bd-a3j8, bd-wb0b, bd-3q5e, bd-nj7f, bd-9lbr
This commit is contained in:
teernisse
2026-03-10 14:24:35 -04:00
parent 16cc58b17f
commit 32134ea933
8 changed files with 2052 additions and 24 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
bd-2i3z bd-9lbr

View File

@@ -316,6 +316,17 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box<dyn std::e
"meta": {"elapsed_ms": "int"} "meta": {"elapsed_ms": "int"}
} }
}, },
"explain": {
"description": "Auto-generate a structured narrative of an issue or MR",
"flags": ["<entity_type: issues|mrs>", "<IID>", "-p/--project <path>", "--sections <comma-list>", "--no-timeline", "--max-decisions <N>", "--since <period>"],
"valid_sections": ["entity", "description", "key_decisions", "activity", "open_threads", "related", "timeline"],
"example": "lore --robot explain issues 42 --sections key_decisions,activity --since 30d",
"response_schema": {
"ok": "bool",
"data": {"entity": "{type:string, iid:int, title:string, state:string, author:string, assignees:[string], labels:[string], created_at:string, updated_at:string, url:string?, status_name:string?}", "description_excerpt": "string?", "key_decisions": "[{timestamp:string, actor:string, action:string, context_note:string}]?", "activity": "{state_changes:int, label_changes:int, notes:int, first_event:string?, last_event:string?}?", "open_threads": "[{discussion_id:string, started_by:string, started_at:string, note_count:int, last_note_at:string}]?", "related": "{closing_mrs:[{iid:int, title:string, state:string, web_url:string?}], related_issues:[{entity_type:string, iid:int, title:string?, reference_type:string}]}?", "timeline_excerpt": "[{timestamp:string, event_type:string, actor:string?, summary:string}]?"},
"meta": {"elapsed_ms": "int"}
}
},
"notes": { "notes": {
"description": "List notes from discussions with rich filtering", "description": "List notes from discussions with rich filtering",
"flags": ["--limit/-n <N>", "--author/-a <username>", "--note-type <type>", "--contains <text>", "--for-issue <iid>", "--for-mr <iid>", "-p/--project <path>", "--since <period>", "--until <period>", "--path <filepath>", "--resolution <any|unresolved|resolved>", "--sort <created|updated>", "--asc", "--include-system", "--note-id <id>", "--gitlab-note-id <id>", "--discussion-id <id>", "--fields <list|minimal>", "--open"], "flags": ["--limit/-n <N>", "--author/-a <username>", "--note-type <type>", "--contains <text>", "--for-issue <iid>", "--for-mr <iid>", "-p/--project <path>", "--since <period>", "--until <period>", "--path <filepath>", "--resolution <any|unresolved|resolved>", "--sort <created|updated>", "--asc", "--include-system", "--note-id <id>", "--gitlab-note-id <id>", "--discussion-id <id>", "--fields <list|minimal>", "--open"],

View File

@@ -209,6 +209,16 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
], ],
), ),
("drift", &["--threshold", "--project"]), ("drift", &["--threshold", "--project"]),
(
"explain",
&[
"--project",
"--sections",
"--no-timeline",
"--max-decisions",
"--since",
],
),
( (
"notes", "notes",
&[ &[
@@ -388,6 +398,7 @@ const CANONICAL_SUBCOMMANDS: &[&str] = &[
"file-history", "file-history",
"trace", "trace",
"drift", "drift",
"explain",
"related", "related",
"cron", "cron",
"token", "token",

1946
src/cli/commands/explain.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ pub mod cron;
pub mod doctor; pub mod doctor;
pub mod drift; pub mod drift;
pub mod embed; pub mod embed;
pub mod explain;
pub mod file_history; pub mod file_history;
pub mod generate_docs; pub mod generate_docs;
pub mod ingest; pub mod ingest;
@@ -35,6 +36,7 @@ pub use cron::{
pub use doctor::{DoctorChecks, print_doctor_results, run_doctor}; pub use doctor::{DoctorChecks, print_doctor_results, run_doctor};
pub use drift::{DriftResponse, print_drift_human, print_drift_json, run_drift}; pub use drift::{DriftResponse, print_drift_human, print_drift_json, run_drift};
pub use embed::{print_embed, print_embed_json, run_embed}; pub use embed::{print_embed, print_embed_json, run_embed};
pub use explain::{handle_explain, print_explain, print_explain_json, run_explain};
pub use file_history::{print_file_history, print_file_history_json, run_file_history}; pub use file_history::{print_file_history, print_file_history_json, run_file_history};
pub use generate_docs::{print_generate_docs, print_generate_docs_json, run_generate_docs}; pub use generate_docs::{print_generate_docs, print_generate_docs_json, run_generate_docs};
pub use ingest::{ pub use ingest::{

View File

@@ -277,6 +277,44 @@ pub enum Commands {
/// Trace why code was introduced: file -> MR -> issue -> discussion /// Trace why code was introduced: file -> MR -> issue -> discussion
Trace(TraceArgs), Trace(TraceArgs),
/// Auto-generate a structured narrative of an issue or MR
#[command(after_help = "\x1b[1mExamples:\x1b[0m
lore explain issues 42 # Narrative for issue #42
lore explain mrs 99 -p group/repo # Narrative for MR !99 in specific project
lore -J explain issues 42 # JSON output for automation
lore explain issues 42 --sections key_decisions,open_threads # Specific sections only
lore explain issues 42 --since 30d # Narrative scoped to last 30 days
lore explain issues 42 --no-timeline # Skip timeline (faster)")]
Explain {
/// Entity type: "issues" or "mrs" (singular forms also accepted)
#[arg(value_parser = ["issues", "mrs", "issue", "mr"])]
entity_type: String,
/// Entity IID
iid: i64,
/// Scope to project (fuzzy match)
#[arg(short, long)]
project: Option<String>,
/// Select specific sections (comma-separated)
/// Valid: entity, description, key_decisions, activity, open_threads, related, timeline
#[arg(long, value_delimiter = ',', help_heading = "Output")]
sections: Option<Vec<String>>,
/// Skip timeline excerpt (faster execution)
#[arg(long, help_heading = "Output")]
no_timeline: bool,
/// Maximum key decisions to include
#[arg(long, default_value = "10", help_heading = "Output")]
max_decisions: usize,
/// Time scope for events/notes (e.g. 7d, 2w, 1m, or YYYY-MM-DD)
#[arg(long, help_heading = "Filters")]
since: Option<String>,
},
/// Detect discussion divergence from original intent /// Detect discussion divergence from original intent
#[command(after_help = "\x1b[1mExamples:\x1b[0m #[command(after_help = "\x1b[1mExamples:\x1b[0m
lore drift issues 42 # Check drift on issue #42 lore drift issues 42 # Check drift on issue #42

View File

@@ -13,23 +13,24 @@ use lore::cli::autocorrect::{self, CorrectionResult};
use lore::cli::commands::{ use lore::cli::commands::{
IngestDisplay, InitInputs, InitOptions, InitResult, ListFilters, MrListFilters, IngestDisplay, InitInputs, InitOptions, InitResult, ListFilters, MrListFilters,
NoteListFilters, RefreshOptions, RefreshResult, SearchCliFilters, SyncOptions, TimelineParams, NoteListFilters, RefreshOptions, RefreshResult, SearchCliFilters, SyncOptions, TimelineParams,
delete_orphan_projects, open_issue_in_browser, open_mr_in_browser, parse_trace_path, delete_orphan_projects, handle_explain, open_issue_in_browser, open_mr_in_browser,
print_count, print_count_json, print_cron_install, print_cron_install_json, print_cron_status, parse_trace_path, print_count, print_count_json, print_cron_install, print_cron_install_json,
print_cron_status_json, print_cron_uninstall, print_cron_uninstall_json, print_doctor_results, print_cron_status, print_cron_status_json, print_cron_uninstall, print_cron_uninstall_json,
print_drift_human, print_drift_json, print_dry_run_preview, print_dry_run_preview_json, print_doctor_results, print_drift_human, print_drift_json, print_dry_run_preview,
print_embed, print_embed_json, print_event_count, print_event_count_json, print_file_history, print_dry_run_preview_json, print_embed, print_embed_json, print_event_count,
print_file_history_json, print_generate_docs, print_generate_docs_json, print_ingest_summary, print_event_count_json, print_file_history, print_file_history_json, print_generate_docs,
print_ingest_summary_json, print_list_issues, print_list_issues_json, print_list_mrs, print_generate_docs_json, print_ingest_summary, print_ingest_summary_json, print_list_issues,
print_list_mrs_json, print_list_notes, print_list_notes_json, print_related_human, print_list_issues_json, print_list_mrs, print_list_mrs_json, print_list_notes,
print_related_json, print_search_results, print_search_results_json, print_show_issue, print_list_notes_json, print_related_human, print_related_json, print_search_results,
print_show_issue_json, print_show_mr, print_show_mr_json, print_stats, print_stats_json, print_search_results_json, print_show_issue, print_show_issue_json, print_show_mr,
print_sync, print_sync_json, print_sync_status, print_sync_status_json, print_timeline, print_show_mr_json, print_stats, print_stats_json, print_sync, print_sync_json,
print_timeline_json_with_meta, print_trace, print_trace_json, print_who_human, print_who_json, print_sync_status, print_sync_status_json, print_timeline, print_timeline_json_with_meta,
query_notes, run_auth_test, run_count, run_count_events, run_cron_install, run_cron_status, print_trace, print_trace_json, print_who_human, print_who_json, query_notes, run_auth_test,
run_cron_uninstall, run_doctor, run_drift, run_embed, run_file_history, run_generate_docs, run_count, run_count_events, run_cron_install, run_cron_status, run_cron_uninstall, run_doctor,
run_ingest, run_ingest_dry_run, run_init, run_init_refresh, run_list_issues, run_list_mrs, run_drift, run_embed, run_file_history, run_generate_docs, run_ingest, run_ingest_dry_run,
run_me, run_related, run_search, run_show_issue, run_show_mr, run_stats, run_sync, run_init, run_init_refresh, run_list_issues, run_list_mrs, run_me, run_related, run_search,
run_sync_status, run_timeline, run_token_set, run_token_show, run_who, 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::render::{ColorMode, GlyphMode, Icons, LoreRenderer, Theme};
use lore::cli::robot::{RobotMeta, strip_schemas}; use lore::cli::robot::{RobotMeta, strip_schemas};
@@ -222,6 +223,25 @@ fn main() {
Some(Commands::Trace(args)) => handle_trace(cli.config.as_deref(), args, robot_mode), Some(Commands::Trace(args)) => handle_trace(cli.config.as_deref(), args, robot_mode),
Some(Commands::Cron(args)) => handle_cron(cli.config.as_deref(), args, robot_mode), Some(Commands::Cron(args)) => handle_cron(cli.config.as_deref(), args, robot_mode),
Some(Commands::Token(args)) => handle_token(cli.config.as_deref(), args, robot_mode).await, Some(Commands::Token(args)) => handle_token(cli.config.as_deref(), args, robot_mode).await,
Some(Commands::Explain {
entity_type,
iid,
project,
sections,
no_timeline,
max_decisions,
since,
}) => handle_explain(
cli.config.as_deref(),
&entity_type,
iid,
project.as_deref(),
sections,
no_timeline,
max_decisions,
since.as_deref(),
robot_mode,
),
Some(Commands::Drift { Some(Commands::Drift {
entity_type, entity_type,
iid, iid,