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 objects::object::VisibilityTier::Public,
235 );
236 annotation.status = status;
237 annotation
238 }
239
240 #[test]
241 fn status_and_tier_labels() {
242 assert_eq!(annotation_status_label(AnnotationStatus::Active), "active");
243 assert_eq!(
244 annotation_status_label(AnnotationStatus::Superseded),
245 "superseded"
246 );
247 assert_eq!(
248 suggestion_tier_token(&ContextSuggestionTier::Medium),
249 "medium"
250 );
251 assert_eq!(suggestion_tier_token(&ContextSuggestionTier::High), "high");
252 assert_eq!(
253 suggestion_tier_human_label(&ContextSuggestionTier::Medium),
254 "may benefit"
255 );
256 assert_eq!(
257 suggestion_tier_human_label(&ContextSuggestionTier::High),
258 "recommended"
259 );
260 }
261
262 #[test]
263 fn list_filters_status_scope_and_tag() {
264 let active = sample_annotation(
265 AnnotationScope::File,
266 vec!["a".into()],
267 AnnotationStatus::Active,
268 );
269 let superseded = sample_annotation(
270 AnnotationScope::File,
271 vec!["a".into()],
272 AnnotationStatus::Superseded,
273 );
274 let tagged = sample_annotation(
275 AnnotationScope::Lines(1, 2),
276 vec!["hot".into()],
277 AnnotationStatus::Active,
278 );
279
280 assert!(annotation_passes_filters(&active, None, None, false));
281 assert!(!annotation_passes_filters(&superseded, None, None, false));
282 assert!(annotation_passes_filters(&superseded, None, None, true));
283
284 assert!(!annotation_passes_filters(
285 &active,
286 Some(&AnnotationScope::Lines(1, 2)),
287 None,
288 false
289 ));
290 assert!(annotation_passes_filters(
291 &tagged,
292 Some(&AnnotationScope::Lines(1, 2)),
293 Some("hot"),
294 false
295 ));
296 assert!(!annotation_passes_filters(
297 &tagged,
298 None,
299 Some("cold"),
300 false
301 ));
302
303 let pool = [active.clone(), superseded, tagged];
304 let filtered = filter_annotations(&pool, None, Some("a"), false);
305 assert_eq!(filtered.len(), 1);
306 assert_eq!(filtered[0].annotation_id, active.annotation_id);
307 assert_eq!(count_active_annotations(&[active.clone(), active]), 2);
308 }
309
310 #[test]
311 fn content_and_rm_plans() {
312 assert!(plan_annotation_content_source(true, false).is_ok());
313 assert!(plan_annotation_content_source(false, true).is_ok());
314 assert_eq!(
315 plan_annotation_content_source(false, false),
316 Err(ContextContentPlanError::Required)
317 );
318 assert_eq!(
319 ContextContentPlanError::Required.kind(),
320 "context_content_required"
321 );
322
323 assert!(plan_context_rm(true, false).is_ok());
324 assert!(plan_context_rm(false, true).is_ok());
325 assert_eq!(
326 plan_context_rm(false, false),
327 Err(ContextRmPlanError::ScopeRequired)
328 );
329 assert_eq!(
330 ContextRmPlanError::ScopeRequired.kind(),
331 "context_remove_scope_required"
332 );
333 }
334
335 #[test]
336 fn supersede_and_edit_rules() {
337 assert!(supersede_reuses_original_target(None, None));
338 assert!(!supersede_reuses_original_target(Some("p"), None));
339 assert!(!supersede_reuses_original_target(None, Some("s")));
340 assert!(supersede_reuses_original_scope(None));
341 assert!(!supersede_reuses_original_scope(Some("file")));
342
343 assert_eq!(
344 next_annotation_tags(&["keep".into()], vec![]),
345 vec!["keep".to_string()]
346 );
347 assert_eq!(
348 next_annotation_tags(&["keep".into()], vec!["new".into()]),
349 vec!["new".to_string()]
350 );
351 }
352
353 #[test]
354 fn audit_duplicate_count_and_keys() {
355 assert_eq!(audit_duplicate_count([1, 1, 2, 3]), 2);
356 assert_eq!(audit_duplicate_count(std::iter::empty::<u32>()), 0);
357
358 let file = ContextTarget::file("src/a.rs").expect("file target");
359 let ann = sample_annotation(AnnotationScope::File, vec![], AnnotationStatus::Active);
360 assert_eq!(audit_target_key(&file), "src/a.rs");
361 assert_eq!(
362 audit_staleness_key(&file, &ann),
363 format!("src/a.rs:{}", ann.scope)
364 );
365 let (kind, label) = context_target_kind_and_label(&file);
366 assert_eq!(kind, "file");
367 assert_eq!(label, "src/a.rs");
368 }
369}