chronicle/annotate/
mod.rs1pub 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
13pub fn run(
18 git_ops: &dyn GitOps,
19 provider: &dyn LlmProvider,
20 commit: &str,
21) -> Result<v2::Annotation> {
22 let context = gather::build_context(git_ops, commit)?;
24
25 let decision = filter::pre_llm_filter(&context);
27
28 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 let (collected, _summary) = crate::agent::run_agent_loop(provider, git_ops, &context)
90 .context(chronicle_error::AgentSnafu)?;
91
92 let mut narrative = collected.narrative.unwrap();
94
95 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 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}