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, v3};
11use snafu::ResultExt;
12
13pub fn run(
19 git_ops: &dyn GitOps,
20 provider: &dyn LlmProvider,
21 commit: &str,
22) -> Result<v3::Annotation> {
23 let context = gather::build_context(git_ops, commit)?;
25
26 let decision = filter::pre_llm_filter(&context);
28
29 let annotation = match decision {
30 filter::FilterDecision::Skip(reason) => {
31 tracing::info!("Skipping annotation: {}", reason);
32 v3::Annotation {
33 schema: "chronicle/v3".to_string(),
34 commit: context.commit_sha.clone(),
35 timestamp: context.timestamp.clone(),
36 summary: format!("Skipped: {reason}"),
37 wisdom: Vec::new(),
38 provenance: v3::Provenance {
39 source: v3::ProvenanceSource::Batch,
40 author: None,
41 derived_from: Vec::new(),
42 notes: Some(format!("Skipped: {reason}")),
43 },
44 }
45 }
46 filter::FilterDecision::Trivial(reason) => {
47 tracing::info!("Trivial commit: {}", reason);
48 v3::Annotation {
49 schema: "chronicle/v3".to_string(),
50 commit: context.commit_sha.clone(),
51 timestamp: context.timestamp.clone(),
52 summary: context.commit_message.clone(),
53 wisdom: Vec::new(),
54 provenance: v3::Provenance {
55 source: v3::ProvenanceSource::Batch,
56 author: None,
57 derived_from: Vec::new(),
58 notes: Some(format!("Trivial: {reason}")),
59 },
60 }
61 }
62 filter::FilterDecision::Annotate => {
63 let (collected, _summary) = crate::agent::run_agent_loop(provider, git_ops, &context)
65 .context(chronicle_error::AgentSnafu)?;
66
67 let narrative = collected.narrative.unwrap();
69
70 let mut wisdom = Vec::new();
72
73 for marker in &collected.markers {
75 let (category, content) = match &marker.kind {
76 v2::MarkerKind::Contract { description, .. } => {
77 (v3::WisdomCategory::Gotcha, description.clone())
78 }
79 v2::MarkerKind::Hazard { description } => {
80 (v3::WisdomCategory::Gotcha, description.clone())
81 }
82 v2::MarkerKind::Dependency {
83 target_file,
84 target_anchor,
85 assumption,
86 } => (
87 v3::WisdomCategory::Insight,
88 format!("Depends on {target_file}:{target_anchor} \u{2014} {assumption}"),
89 ),
90 v2::MarkerKind::Unstable { description, .. } => {
91 (v3::WisdomCategory::UnfinishedThread, description.clone())
92 }
93 v2::MarkerKind::Security { description } => {
94 (v3::WisdomCategory::Gotcha, description.clone())
95 }
96 v2::MarkerKind::Performance { description } => {
97 (v3::WisdomCategory::Gotcha, description.clone())
98 }
99 v2::MarkerKind::Deprecated { description, .. } => {
100 (v3::WisdomCategory::UnfinishedThread, description.clone())
101 }
102 v2::MarkerKind::TechDebt { description } => {
103 (v3::WisdomCategory::UnfinishedThread, description.clone())
104 }
105 v2::MarkerKind::TestCoverage { description } => {
106 (v3::WisdomCategory::Insight, description.clone())
107 }
108 };
109 wisdom.push(v3::WisdomEntry {
110 category,
111 content,
112 file: Some(marker.file.clone()),
113 lines: marker.lines,
114 });
115 }
116
117 for decision in &collected.decisions {
119 let file = decision
120 .scope
121 .first()
122 .map(|s| s.split(':').next().unwrap_or(s).to_string());
123 wisdom.push(v3::WisdomEntry {
124 category: v3::WisdomCategory::Insight,
125 content: format!("{}: {}", decision.what, decision.why),
126 file,
127 lines: None,
128 });
129 }
130
131 for ra in &narrative.rejected_alternatives {
133 let content = if ra.reason.is_empty() {
134 ra.approach.clone()
135 } else {
136 format!("{}: {}", ra.approach, ra.reason)
137 };
138 wisdom.push(v3::WisdomEntry {
139 category: v3::WisdomCategory::DeadEnd,
140 content,
141 file: None,
142 lines: None,
143 });
144 }
145
146 v3::Annotation {
147 schema: "chronicle/v3".to_string(),
148 commit: context.commit_sha.clone(),
149 timestamp: context.timestamp.clone(),
150 summary: narrative.summary,
151 wisdom,
152 provenance: v3::Provenance {
153 source: v3::ProvenanceSource::Batch,
154 author: None,
155 derived_from: Vec::new(),
156 notes: None,
157 },
158 }
159 }
160 };
161
162 let json = serde_json::to_string_pretty(&annotation).context(chronicle_error::JsonSnafu)?;
164 git_ops
165 .note_write(commit, &json)
166 .context(chronicle_error::GitSnafu)?;
167
168 Ok(annotation)
169}