feat(cli): implement 'lore trace' command (bd-2n4, bd-9dd)
Gate 5 Code Trace - Tier 1 (API-only, no git blame). Answers 'Why was this code introduced?' by building file -> MR -> issue -> discussion chains. New files: - src/core/trace.rs: run_trace() query logic with rename-aware path resolution, entity_reference-based issue linking, and DiffNote discussion extraction - src/core/trace_tests.rs: 7 unit tests for query logic - src/cli/commands/trace.rs: CLI command with human output, robot JSON output, and :line suffix parsing (5 tests) Human output shows full content (no truncation). Robot JSON truncates discussion bodies to 500 chars for token efficiency. Wiring: - TraceArgs + Commands::Trace in cli/mod.rs - handle_trace in main.rs - VALID_COMMANDS + robot-docs manifest entry - COMMAND_FLAGS autocorrect registry entry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
78
src/main.rs
78
src/main.rs
@@ -11,26 +11,26 @@ use lore::cli::autocorrect::{self, CorrectionResult};
|
||||
use lore::cli::commands::{
|
||||
IngestDisplay, InitInputs, InitOptions, InitResult, ListFilters, MrListFilters,
|
||||
NoteListFilters, SearchCliFilters, SyncOptions, TimelineParams, open_issue_in_browser,
|
||||
open_mr_in_browser, print_count, print_count_json, print_doctor_results, print_drift_human,
|
||||
print_drift_json, print_dry_run_preview, print_dry_run_preview_json, print_embed,
|
||||
print_embed_json, print_event_count, print_event_count_json, print_file_history,
|
||||
open_mr_in_browser, parse_trace_path, print_count, print_count_json, print_doctor_results,
|
||||
print_drift_human, print_drift_json, print_dry_run_preview, print_dry_run_preview_json,
|
||||
print_embed, print_embed_json, 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_csv, print_list_notes_json,
|
||||
print_list_notes_jsonl, 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_who_human, print_who_json, query_notes, run_auth_test,
|
||||
run_count, run_count_events, 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_search, run_show_issue, run_show_mr, run_stats, run_sync, run_sync_status, run_timeline,
|
||||
run_who,
|
||||
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_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_search, run_show_issue, run_show_mr, run_stats, run_sync, run_sync_status,
|
||||
run_timeline, run_who,
|
||||
};
|
||||
use lore::cli::render::{ColorMode, GlyphMode, Icons, LoreRenderer, Theme};
|
||||
use lore::cli::robot::{RobotMeta, strip_schemas};
|
||||
use lore::cli::{
|
||||
Cli, Commands, CountArgs, EmbedArgs, FileHistoryArgs, GenerateDocsArgs, IngestArgs, IssuesArgs,
|
||||
MrsArgs, NotesArgs, SearchArgs, StatsArgs, SyncArgs, TimelineArgs, WhoArgs,
|
||||
MrsArgs, NotesArgs, SearchArgs, StatsArgs, SyncArgs, TimelineArgs, TraceArgs, WhoArgs,
|
||||
};
|
||||
use lore::core::db::{
|
||||
LATEST_SCHEMA_VERSION, create_connection, get_schema_version, run_migrations,
|
||||
@@ -40,8 +40,10 @@ use lore::core::error::{LoreError, RobotErrorOutput};
|
||||
use lore::core::logging;
|
||||
use lore::core::metrics::MetricsLayer;
|
||||
use lore::core::paths::{get_config_path, get_db_path, get_log_dir};
|
||||
use lore::core::project::resolve_project;
|
||||
use lore::core::shutdown::ShutdownSignal;
|
||||
use lore::core::sync_run::SyncRunRecorder;
|
||||
use lore::core::trace::run_trace;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -199,6 +201,7 @@ async fn main() {
|
||||
Some(Commands::FileHistory(args)) => {
|
||||
handle_file_history(cli.config.as_deref(), args, robot_mode)
|
||||
}
|
||||
Some(Commands::Trace(args)) => handle_trace(cli.config.as_deref(), args, robot_mode),
|
||||
Some(Commands::Drift {
|
||||
entity_type,
|
||||
iid,
|
||||
@@ -725,6 +728,7 @@ fn suggest_similar_command(invalid: &str) -> String {
|
||||
("note", "notes"),
|
||||
("drift", "drift"),
|
||||
("file-history", "file-history"),
|
||||
("trace", "trace"),
|
||||
];
|
||||
|
||||
let invalid_lower = invalid.to_lowercase();
|
||||
@@ -766,6 +770,7 @@ fn command_example(cmd: &str) -> &'static str {
|
||||
"generate-docs" => "lore --robot generate-docs",
|
||||
"embed" => "lore --robot embed",
|
||||
"robot-docs" => "lore robot-docs",
|
||||
"trace" => "lore --robot trace src/main.rs",
|
||||
"init" => "lore init",
|
||||
_ => "lore --robot <command>",
|
||||
}
|
||||
@@ -1888,6 +1893,51 @@ fn handle_file_history(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_trace(
|
||||
config_override: Option<&str>,
|
||||
args: TraceArgs,
|
||||
robot_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let start = std::time::Instant::now();
|
||||
let config = Config::load(config_override)?;
|
||||
|
||||
let (path, line_requested) = parse_trace_path(&args.path);
|
||||
|
||||
if line_requested.is_some() && !robot_mode {
|
||||
eprintln!(
|
||||
"Note: Line-level tracing requires Tier 2 (git blame). Showing file-level results."
|
||||
);
|
||||
}
|
||||
|
||||
let project = config
|
||||
.effective_project(args.project.as_deref())
|
||||
.map(String::from);
|
||||
|
||||
let db_path = get_db_path(config.storage.db_path.as_deref());
|
||||
let conn = create_connection(&db_path)?;
|
||||
let project_id = project
|
||||
.as_deref()
|
||||
.map(|p| resolve_project(&conn, p))
|
||||
.transpose()?;
|
||||
|
||||
let result = run_trace(
|
||||
&conn,
|
||||
project_id,
|
||||
&path,
|
||||
!args.no_follow_renames,
|
||||
args.discussions,
|
||||
args.limit,
|
||||
)?;
|
||||
|
||||
if robot_mode {
|
||||
let elapsed_ms = start.elapsed().as_millis() as u64;
|
||||
print_trace_json(&result, elapsed_ms, line_requested);
|
||||
} else {
|
||||
print_trace(&result);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_timeline(
|
||||
config_override: Option<&str>,
|
||||
args: TimelineArgs,
|
||||
@@ -2556,6 +2606,16 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box<dyn std::e
|
||||
"active_minimal": ["entity_type", "iid", "title", "participants"]
|
||||
}
|
||||
},
|
||||
"trace": {
|
||||
"description": "Trace why code was introduced: file -> MR -> issue -> discussion. Follows rename chains by default.",
|
||||
"flags": ["<path>", "-p/--project <path>", "--discussions", "--no-follow-renames", "-n/--limit <N>"],
|
||||
"example": "lore --robot trace src/main.rs -p group/repo",
|
||||
"response_schema": {
|
||||
"ok": "bool",
|
||||
"data": {"path": "string", "resolved_paths": "[string]", "trace_chains": "[{mr_iid:int, mr_title:string, mr_state:string, mr_author:string, change_type:string, merged_at_iso:string?, updated_at_iso:string, web_url:string?, issues:[{iid:int, title:string, state:string, reference_type:string, web_url:string?}], discussions:[{discussion_id:string, mr_iid:int, author_username:string, body_snippet:string, path:string, created_at_iso:string}]}]"},
|
||||
"meta": {"tier": "string (api_only)", "line_requested": "int?", "elapsed_ms": "int", "total_chains": "int", "renames_followed": "bool"}
|
||||
}
|
||||
},
|
||||
"file-history": {
|
||||
"description": "Show MRs that touched a file, with rename chain resolution and optional DiffNote discussions",
|
||||
"flags": ["<path>", "-p/--project <path>", "--discussions", "--no-follow-renames", "--merged", "-n/--limit <N>"],
|
||||
|
||||
Reference in New Issue
Block a user