Skip to main content

chronicle/annotate/
mod.rs

1pub mod filter;
2pub mod gather;
3pub mod live;
4pub mod squash;
5pub mod staging;
6
7use crate::error::{chronicle_error, Result};
8use crate::git::GitOps;
9use crate::provider::LlmProvider;
10use crate::schema::v2;
11use snafu::ResultExt;
12
13/// The main annotation entry point. Gathers context, checks filters,
14/// runs the agent, and writes the annotation as a git note.
15///
16/// Produces v2 annotations (narrative-first).
17pub fn run(
18    git_ops: &dyn GitOps,
19    provider: &dyn LlmProvider,
20    commit: &str,
21) -> Result<v2::Annotation> {
22    // 1. Gather context
23    let context = gather::build_context(git_ops, commit)?;
24
25    // 2. Pre-LLM filter
26    let decision = filter::pre_llm_filter(&context);
27
28    // Collect files_changed from diffs
29    let files_changed: Vec<String> = context.diffs.iter().map(|d| d.path.clone()).collect();
30
31    let annotation = match decision {
32        filter::FilterDecision::Skip(reason) => {
33            tracing::info!("Skipping annotation: {}", reason);
34            v2::Annotation {
35                schema: "chronicle/v2".to_string(),
36                commit: context.commit_sha.clone(),
37                timestamp: context.timestamp.clone(),
38                narrative: v2::Narrative {
39                    summary: format!("Skipped: {reason}"),
40                    motivation: None,
41                    rejected_alternatives: Vec::new(),
42                    follow_up: None,
43                    files_changed,
44                },
45                decisions: Vec::new(),
46                markers: Vec::new(),
47                effort: None,
48                provenance: v2::Provenance {
49                    source: v2::ProvenanceSource::Batch,
50                    author: None,
51                    derived_from: Vec::new(),
52                    notes: Some(format!("Skipped: {reason}")),
53                },
54            }
55        }
56        filter::FilterDecision::Trivial(reason) => {
57            tracing::info!("Trivial commit: {}", reason);
58            let effort = context.author_context.as_ref().and_then(|ac| {
59                ac.task.as_ref().map(|task| v2::EffortLink {
60                    id: task.clone(),
61                    description: task.clone(),
62                    phase: v2::EffortPhase::InProgress,
63                })
64            });
65            v2::Annotation {
66                schema: "chronicle/v2".to_string(),
67                commit: context.commit_sha.clone(),
68                timestamp: context.timestamp.clone(),
69                narrative: v2::Narrative {
70                    summary: context.commit_message.clone(),
71                    motivation: None,
72                    rejected_alternatives: Vec::new(),
73                    follow_up: None,
74                    files_changed,
75                },
76                decisions: Vec::new(),
77                markers: Vec::new(),
78                effort,
79                provenance: v2::Provenance {
80                    source: v2::ProvenanceSource::Batch,
81                    author: None,
82                    derived_from: Vec::new(),
83                    notes: Some(format!("Trivial: {reason}")),
84                },
85            }
86        }
87        filter::FilterDecision::Annotate => {
88            // Call the agent loop for full LLM annotation
89            let (collected, _summary) = crate::agent::run_agent_loop(provider, git_ops, &context)
90                .context(chronicle_error::AgentSnafu)?;
91
92            // The narrative is required (agent loop guarantees it's Some)
93            let mut narrative = collected.narrative.unwrap();
94
95            // Auto-populate files_changed from diffs
96            narrative.files_changed = files_changed;
97
98            let effort = context.author_context.as_ref().and_then(|ac| {
99                ac.task.as_ref().map(|task| v2::EffortLink {
100                    id: task.clone(),
101                    description: task.clone(),
102                    phase: v2::EffortPhase::InProgress,
103                })
104            });
105
106            v2::Annotation {
107                schema: "chronicle/v2".to_string(),
108                commit: context.commit_sha.clone(),
109                timestamp: context.timestamp.clone(),
110                narrative,
111                decisions: collected.decisions,
112                markers: collected.markers,
113                effort,
114                provenance: v2::Provenance {
115                    source: v2::ProvenanceSource::Batch,
116                    author: None,
117                    derived_from: Vec::new(),
118                    notes: None,
119                },
120            }
121        }
122    };
123
124    // 3. Serialize and write as git note
125    let json = serde_json::to_string_pretty(&annotation).context(chronicle_error::JsonSnafu)?;
126    git_ops
127        .note_write(commit, &json)
128        .context(chronicle_error::GitSnafu)?;
129
130    Ok(annotation)
131}