1use crate::error::{ChronicleError, Result};
2use crate::git::GitOps;
3use crate::read::{self, MatchedAnnotation, ReadQuery};
4use crate::schema::common::{AstAnchor, LineRange};
5use crate::schema::v1::{
6 Constraint, ConstraintSource, ContextLevel, Provenance, ProvenanceOperation, RegionAnnotation,
7};
8use crate::schema::v3;
9
10#[derive(Debug, Clone)]
12pub struct RegionRef {
13 pub region: RegionAnnotation,
14 pub commit: String,
15 pub timestamp: String,
16 pub summary: String,
17 pub context_level: ContextLevel,
18 pub provenance: Provenance,
19}
20
21#[derive(Debug)]
23pub struct LineAnnotationMap {
24 coverage: Vec<Vec<usize>>,
26}
27
28impl LineAnnotationMap {
29 pub fn build_from_regions(regions: &[RegionRef], total_lines: usize) -> Self {
31 Self::build(regions, total_lines)
32 }
33
34 fn build(regions: &[RegionRef], total_lines: usize) -> Self {
35 let mut coverage = vec![Vec::new(); total_lines];
36 for (idx, r) in regions.iter().enumerate() {
37 let start = r.region.lines.start.saturating_sub(1) as usize;
38 let end = (r.region.lines.end as usize).min(total_lines);
39 for slot in &mut coverage[start..end] {
40 slot.push(idx);
41 }
42 }
43 Self { coverage }
44 }
45
46 pub fn regions_at_line(&self, line: u32) -> &[usize] {
48 let idx = line.saturating_sub(1) as usize;
49 self.coverage.get(idx).map(|v| v.as_slice()).unwrap_or(&[])
50 }
51
52 pub fn next_annotated_line(&self, from: u32) -> Option<u32> {
55 let start = from.saturating_sub(1) as usize;
56 for (i, regions) in self.coverage[start..].iter().enumerate() {
57 if !regions.is_empty() {
58 return Some((start + i) as u32 + 1);
59 }
60 }
61 None
62 }
63
64 pub fn prev_annotated_line(&self, from: u32) -> Option<u32> {
66 let end = (from as usize).min(self.coverage.len());
67 for i in (0..end).rev() {
68 if !self.coverage[i].is_empty() {
69 return Some(i as u32 + 1);
70 }
71 }
72 None
73 }
74}
75
76#[derive(Debug)]
78pub struct ShowData {
79 pub file_path: String,
80 pub commit: String,
81 pub source_lines: Vec<String>,
82 pub regions: Vec<RegionRef>,
83 pub annotation_map: LineAnnotationMap,
84}
85
86pub fn build_show_data(
88 git_ops: &dyn GitOps,
89 file_path: &str,
90 commit: &str,
91 anchor: Option<&str>,
92) -> Result<ShowData> {
93 let source = git_ops
95 .file_at_commit(std::path::Path::new(file_path), commit)
96 .map_err(|e| ChronicleError::Git {
97 source: e,
98 location: snafu::Location::default(),
99 })?;
100
101 let source_lines: Vec<String> = source.lines().map(String::from).collect();
102 let total_lines = source_lines.len();
103
104 let query = ReadQuery {
106 file: file_path.to_string(),
107 anchor: anchor.map(String::from),
108 lines: None,
109 };
110 let read_result = read::execute(git_ops, &query)?;
111
112 let regions = convert_to_region_refs(read_result.annotations, file_path);
114
115 let annotation_map = LineAnnotationMap::build(®ions, total_lines);
116
117 Ok(ShowData {
118 file_path: file_path.to_string(),
119 commit: commit.to_string(),
120 source_lines,
121 regions,
122 annotation_map,
123 })
124}
125
126fn convert_to_region_refs(annotations: Vec<MatchedAnnotation>, file_path: &str) -> Vec<RegionRef> {
131 use std::collections::HashMap;
132
133 let mut best: HashMap<String, RegionRef> = HashMap::new();
134
135 for ann in annotations {
136 if ann.wisdom.is_empty() {
137 let key = format!("{}:{}", file_path, "__commit_level__");
139 let region_ref = RegionRef {
140 region: RegionAnnotation {
141 file: file_path.to_string(),
142 ast_anchor: AstAnchor {
143 unit_type: "commit".to_string(),
144 name: "(commit-level)".to_string(),
145 signature: None,
146 },
147 lines: LineRange { start: 1, end: 1 },
148 intent: ann.summary.clone(),
149 reasoning: None,
150 constraints: vec![],
151 semantic_dependencies: vec![],
152 related_annotations: vec![],
153 tags: vec![],
154 risk_notes: None,
155 corrections: vec![],
156 },
157 commit: ann.commit.clone(),
158 timestamp: ann.timestamp.clone(),
159 summary: ann.summary.clone(),
160 context_level: ContextLevel::Inferred,
161 provenance: Provenance {
162 operation: ProvenanceOperation::Initial,
163 derived_from: vec![],
164 original_annotations_preserved: false,
165 synthesis_notes: None,
166 },
167 };
168 let existing = best.get(&key);
169 if existing.is_none() || region_ref.timestamp > existing.unwrap().timestamp {
170 best.insert(key, region_ref);
171 }
172 continue;
173 }
174
175 let mut wisdom_by_file: HashMap<String, Vec<&v3::WisdomEntry>> = HashMap::new();
177 for w in &ann.wisdom {
178 let f = w.file.clone().unwrap_or_else(|| file_path.to_string());
179 wisdom_by_file.entry(f).or_default().push(w);
180 }
181
182 for (wf, entries) in wisdom_by_file {
183 let key = format!("{}:{}", file_path, wf);
184
185 let mut line_start = u32::MAX;
187 let mut line_end = 0u32;
188 for w in &entries {
189 if let Some(ref lines) = w.lines {
190 line_start = line_start.min(lines.start);
191 line_end = line_end.max(lines.end);
192 }
193 }
194 if line_start == u32::MAX {
195 line_start = 1;
196 line_end = 1;
197 }
198
199 let mut constraints = Vec::new();
201 let mut risk_notes = Vec::new();
202
203 for w in &entries {
204 match w.category {
205 v3::WisdomCategory::Gotcha => {
206 constraints.push(Constraint {
207 text: w.content.clone(),
208 source: ConstraintSource::Inferred,
209 });
210 }
211 _ => {
212 risk_notes.push(w.content.clone());
213 }
214 }
215 }
216
217 let region_ref = RegionRef {
218 region: RegionAnnotation {
219 file: file_path.to_string(),
220 ast_anchor: AstAnchor {
221 unit_type: "file".to_string(),
222 name: wf.clone(),
223 signature: None,
224 },
225 lines: LineRange {
226 start: line_start,
227 end: line_end,
228 },
229 intent: ann.summary.clone(),
230 reasoning: None,
231 constraints,
232 semantic_dependencies: vec![],
233 related_annotations: vec![],
234 tags: vec![],
235 risk_notes: if risk_notes.is_empty() {
236 None
237 } else {
238 Some(risk_notes.join("; "))
239 },
240 corrections: vec![],
241 },
242 commit: ann.commit.clone(),
243 timestamp: ann.timestamp.clone(),
244 summary: ann.summary.clone(),
245 context_level: ContextLevel::Inferred,
246 provenance: Provenance {
247 operation: ProvenanceOperation::Initial,
248 derived_from: vec![],
249 original_annotations_preserved: false,
250 synthesis_notes: None,
251 },
252 };
253
254 let existing = best.get(&key);
255 if existing.is_none() || region_ref.timestamp > existing.unwrap().timestamp {
256 best.insert(key, region_ref);
257 }
258 }
259 }
260
261 let mut regions: Vec<RegionRef> = best.into_values().collect();
262 regions.sort_by_key(|r| r.region.lines.start);
263 regions
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_line_annotation_map_empty() {
272 let map = LineAnnotationMap::build(&[], 10);
273 assert!(map.regions_at_line(1).is_empty());
274 assert!(map.regions_at_line(5).is_empty());
275 assert!(map.next_annotated_line(1).is_none());
276 assert!(map.prev_annotated_line(10).is_none());
277 }
278
279 #[test]
280 fn test_line_annotation_map_coverage() {
281 use crate::schema::common::*;
282 use crate::schema::v1::*;
283
284 let regions = vec![RegionRef {
285 region: RegionAnnotation {
286 file: "test.rs".to_string(),
287 ast_anchor: AstAnchor {
288 unit_type: "function".to_string(),
289 name: "foo".to_string(),
290 signature: None,
291 },
292 lines: LineRange { start: 3, end: 5 },
293 intent: "test".to_string(),
294 reasoning: None,
295 constraints: vec![],
296 semantic_dependencies: vec![],
297 related_annotations: vec![],
298 tags: vec![],
299 risk_notes: None,
300 corrections: vec![],
301 },
302 commit: "abc".to_string(),
303 timestamp: "2025-01-01T00:00:00Z".to_string(),
304 summary: "test".to_string(),
305 context_level: ContextLevel::Inferred,
306 provenance: Provenance {
307 operation: ProvenanceOperation::Initial,
308 derived_from: vec![],
309 original_annotations_preserved: false,
310 synthesis_notes: None,
311 },
312 }];
313
314 let map = LineAnnotationMap::build(®ions, 10);
315 assert!(map.regions_at_line(1).is_empty());
316 assert!(map.regions_at_line(2).is_empty());
317 assert_eq!(map.regions_at_line(3), &[0]);
318 assert_eq!(map.regions_at_line(4), &[0]);
319 assert_eq!(map.regions_at_line(5), &[0]);
320 assert!(map.regions_at_line(6).is_empty());
321 }
322
323 #[test]
324 fn test_next_prev_annotated_line() {
325 use crate::schema::common::*;
326 use crate::schema::v1::*;
327
328 let regions = vec![RegionRef {
329 region: RegionAnnotation {
330 file: "test.rs".to_string(),
331 ast_anchor: AstAnchor {
332 unit_type: "function".to_string(),
333 name: "foo".to_string(),
334 signature: None,
335 },
336 lines: LineRange { start: 5, end: 8 },
337 intent: "test".to_string(),
338 reasoning: None,
339 constraints: vec![],
340 semantic_dependencies: vec![],
341 related_annotations: vec![],
342 tags: vec![],
343 risk_notes: None,
344 corrections: vec![],
345 },
346 commit: "abc".to_string(),
347 timestamp: "2025-01-01T00:00:00Z".to_string(),
348 summary: "test".to_string(),
349 context_level: ContextLevel::Inferred,
350 provenance: Provenance {
351 operation: ProvenanceOperation::Initial,
352 derived_from: vec![],
353 original_annotations_preserved: false,
354 synthesis_notes: None,
355 },
356 }];
357
358 let map = LineAnnotationMap::build(®ions, 15);
359 assert_eq!(map.next_annotated_line(1), Some(5));
360 assert_eq!(map.next_annotated_line(5), Some(5));
361 assert_eq!(map.next_annotated_line(9), None);
362 assert_eq!(map.prev_annotated_line(10), Some(8));
363 assert_eq!(map.prev_annotated_line(4), None);
364 }
365}