1use objects::object::{
7 Annotation, AnnotationScope, AnnotationStatus, ContextSuggestionTier, ContextTarget,
8};
9
10pub fn annotation_status_label(status: AnnotationStatus) -> &'static str {
16 match status {
17 AnnotationStatus::Active => "active",
18 AnnotationStatus::Superseded => "superseded",
19 AnnotationStatus::Deleted => "deleted",
20 }
21}
22
23pub fn suggestion_tier_token(tier: &ContextSuggestionTier) -> &'static str {
25 match tier {
26 ContextSuggestionTier::Medium => "medium",
27 ContextSuggestionTier::High => "high",
28 }
29}
30
31pub fn suggestion_tier_human_label(tier: &ContextSuggestionTier) -> &'static str {
33 match tier {
34 ContextSuggestionTier::Medium => "may benefit",
35 ContextSuggestionTier::High => "recommended",
36 }
37}
38
39pub fn annotation_passes_filters(
47 annotation: &Annotation,
48 scope_filter: Option<&AnnotationScope>,
49 tag_filter: Option<&str>,
50 include_superseded: bool,
51) -> bool {
52 if annotation.status == AnnotationStatus::Deleted
53 || (!include_superseded && annotation.status == AnnotationStatus::Superseded)
54 {
55 return false;
56 }
57 if let Some(scope) = scope_filter
58 && !annotation.scope.matches(scope)
59 {
60 return false;
61 }
62 if let Some(tag) = tag_filter {
63 let Some(current) = annotation.current_revision() else {
64 return false;
65 };
66 if !current.tags.iter().any(|candidate| candidate == tag) {
67 return false;
68 }
69 }
70 true
71}
72
73pub fn filter_annotations<'a>(
75 annotations: &'a [Annotation],
76 scope_filter: Option<&AnnotationScope>,
77 tag_filter: Option<&str>,
78 include_superseded: bool,
79) -> Vec<&'a Annotation> {
80 annotations
81 .iter()
82 .filter(|annotation| {
83 annotation_passes_filters(annotation, scope_filter, tag_filter, include_superseded)
84 })
85 .collect()
86}
87
88pub fn count_active_annotations(annotations: &[Annotation]) -> usize {
90 annotations
91 .iter()
92 .filter(|annotation| annotation.status == AnnotationStatus::Active)
93 .count()
94}
95
96pub fn context_target_kind_and_label(target: &ContextTarget) -> (&'static str, String) {
102 match target {
103 ContextTarget::File { path } => ("file", path.clone()),
104 ContextTarget::State { state_id } => ("state", state_id.to_string_full()),
105 }
106}
107
108pub fn audit_target_key(target: &ContextTarget) -> String {
110 match target {
111 ContextTarget::File { path } => path.clone(),
112 ContextTarget::State { state_id } => state_id.to_string_full(),
113 }
114}
115
116pub fn audit_staleness_key(target: &ContextTarget, annotation: &Annotation) -> String {
118 match target {
119 ContextTarget::File { path } => format!("{path}:{}", annotation.scope),
120 ContextTarget::State { state_id } => {
121 format!(
122 "state:{}:{}",
123 state_id.to_string_full(),
124 annotation.annotation_id
125 )
126 }
127 }
128}
129
130pub fn audit_duplicate_count(signature_counts: impl IntoIterator<Item = u32>) -> u32 {
132 signature_counts
133 .into_iter()
134 .filter(|count| *count > 1)
135 .count() as u32
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum ContextContentPlanError {
145 Required,
147}
148
149impl ContextContentPlanError {
150 pub fn kind(self) -> &'static str {
151 match self {
152 Self::Required => "context_content_required",
153 }
154 }
155}
156
157pub fn plan_annotation_content_source(
161 has_message: bool,
162 has_file: bool,
163) -> Result<(), ContextContentPlanError> {
164 if has_message || has_file {
165 Ok(())
166 } else {
167 Err(ContextContentPlanError::Required)
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub enum ContextRmPlanError {
174 ScopeRequired,
176}
177
178impl ContextRmPlanError {
179 pub fn kind(self) -> &'static str {
180 match self {
181 Self::ScopeRequired => "context_remove_scope_required",
182 }
183 }
184}
185
186pub fn plan_context_rm(all: bool, scope_present: bool) -> Result<(), ContextRmPlanError> {
188 if !all && !scope_present {
189 Err(ContextRmPlanError::ScopeRequired)
190 } else {
191 Ok(())
192 }
193}
194
195pub fn supersede_reuses_original_target(path: Option<&str>, state: Option<&str>) -> bool {
197 path.is_none() && state.is_none()
198}
199
200pub fn supersede_reuses_original_scope(scope: Option<&str>) -> bool {
202 scope.is_none()
203}
204
205pub fn next_annotation_tags(current: &[String], override_tags: Vec<String>) -> Vec<String> {
207 if override_tags.is_empty() {
208 current.to_vec()
209 } else {
210 override_tags
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use objects::object::{Annotation, AnnotationKind, AnnotationScope, AnnotationStatus};
217
218 use super::*;
219
220 fn sample_annotation(
221 scope: AnnotationScope,
222 tags: Vec<String>,
223 status: AnnotationStatus,
224 ) -> Annotation {
225 let mut annotation = Annotation::new(
226 scope,
227 AnnotationKind::Rationale,
228 "body".into(),
229 tags,
230 "Test <t@example.com>".into(),
231 0,
232 None,
233 None,
234 );
235 annotation.status = status;
236 annotation
237 }
238
239 #[test]
240 fn status_and_tier_labels() {
241 assert_eq!(annotation_status_label(AnnotationStatus::Active), "active");
242 assert_eq!(
243 annotation_status_label(AnnotationStatus::Superseded),
244 "superseded"
245 );
246 assert_eq!(
247 suggestion_tier_token(&ContextSuggestionTier::Medium),
248 "medium"
249 );
250 assert_eq!(suggestion_tier_token(&ContextSuggestionTier::High), "high");
251 assert_eq!(
252 suggestion_tier_human_label(&ContextSuggestionTier::Medium),
253 "may benefit"
254 );
255 assert_eq!(
256 suggestion_tier_human_label(&ContextSuggestionTier::High),
257 "recommended"
258 );
259 }
260
261 #[test]
262 fn list_filters_status_scope_and_tag() {
263 let active = sample_annotation(
264 AnnotationScope::File,
265 vec!["a".into()],
266 AnnotationStatus::Active,
267 );
268 let superseded = sample_annotation(
269 AnnotationScope::File,
270 vec!["a".into()],
271 AnnotationStatus::Superseded,
272 );
273 let tagged = sample_annotation(
274 AnnotationScope::Lines(1, 2),
275 vec!["hot".into()],
276 AnnotationStatus::Active,
277 );
278
279 assert!(annotation_passes_filters(&active, None, None, false));
280 assert!(!annotation_passes_filters(&superseded, None, None, false));
281 assert!(annotation_passes_filters(&superseded, None, None, true));
282
283 assert!(!annotation_passes_filters(
284 &active,
285 Some(&AnnotationScope::Lines(1, 2)),
286 None,
287 false
288 ));
289 assert!(annotation_passes_filters(
290 &tagged,
291 Some(&AnnotationScope::Lines(1, 2)),
292 Some("hot"),
293 false
294 ));
295 assert!(!annotation_passes_filters(
296 &tagged,
297 None,
298 Some("cold"),
299 false
300 ));
301
302 let pool = [active.clone(), superseded, tagged];
303 let filtered = filter_annotations(&pool, None, Some("a"), false);
304 assert_eq!(filtered.len(), 1);
305 assert_eq!(filtered[0].annotation_id, active.annotation_id);
306 assert_eq!(count_active_annotations(&[active.clone(), active]), 2);
307 }
308
309 #[test]
310 fn content_and_rm_plans() {
311 assert!(plan_annotation_content_source(true, false).is_ok());
312 assert!(plan_annotation_content_source(false, true).is_ok());
313 assert_eq!(
314 plan_annotation_content_source(false, false),
315 Err(ContextContentPlanError::Required)
316 );
317 assert_eq!(
318 ContextContentPlanError::Required.kind(),
319 "context_content_required"
320 );
321
322 assert!(plan_context_rm(true, false).is_ok());
323 assert!(plan_context_rm(false, true).is_ok());
324 assert_eq!(
325 plan_context_rm(false, false),
326 Err(ContextRmPlanError::ScopeRequired)
327 );
328 assert_eq!(
329 ContextRmPlanError::ScopeRequired.kind(),
330 "context_remove_scope_required"
331 );
332 }
333
334 #[test]
335 fn supersede_and_edit_rules() {
336 assert!(supersede_reuses_original_target(None, None));
337 assert!(!supersede_reuses_original_target(Some("p"), None));
338 assert!(!supersede_reuses_original_target(None, Some("s")));
339 assert!(supersede_reuses_original_scope(None));
340 assert!(!supersede_reuses_original_scope(Some("file")));
341
342 assert_eq!(
343 next_annotation_tags(&["keep".into()], vec![]),
344 vec!["keep".to_string()]
345 );
346 assert_eq!(
347 next_annotation_tags(&["keep".into()], vec!["new".into()]),
348 vec!["new".to_string()]
349 );
350 }
351
352 #[test]
353 fn audit_duplicate_count_and_keys() {
354 assert_eq!(audit_duplicate_count([1, 1, 2, 3]), 2);
355 assert_eq!(audit_duplicate_count(std::iter::empty::<u32>()), 0);
356
357 let file = ContextTarget::file("src/a.rs").expect("file target");
358 let ann = sample_annotation(AnnotationScope::File, vec![], AnnotationStatus::Active);
359 assert_eq!(audit_target_key(&file), "src/a.rs");
360 assert_eq!(
361 audit_staleness_key(&file, &ann),
362 format!("src/a.rs:{}", ann.scope)
363 );
364 let (kind, label) = context_target_kind_and_label(&file);
365 assert_eq!(kind, "file");
366 assert_eq!(label, "src/a.rs");
367 }
368}