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