Skip to main content

git_slop/
scoring.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use serde_json::{Value, json};
4
5use crate::config::{pointer_f64, pointer_u64};
6use crate::model::{FileAnalysis, FolderAnalysis, parent_folders};
7
8const DEFAULT_COMPACT_MAX_TOKENS: u64 = 3_072;
9const DEFAULT_HEALTHY_MAX_TOKENS: u64 = 8_000;
10const DEFAULT_WARNING_MAX_TOKENS: u64 = 10_000;
11const DEFAULT_AGE_HALF_LIFE_DAYS: f64 = 180.0;
12const DEFAULT_CONTEXT_WEIGHT: f64 = 0.60;
13const DEFAULT_AGE_WEIGHT: f64 = 0.20;
14const DEFAULT_CHURN_WEIGHT: f64 = 0.20;
15
16fn round_to(value: f64, digits: i32) -> f64 {
17    let factor = 10_f64.powi(digits);
18    (value * factor).round_ties_even() / factor
19}
20
21fn p95(values: impl IntoIterator<Item = f64>) -> f64 {
22    let mut sorted: Vec<f64> = values.into_iter().collect();
23    if sorted.is_empty() {
24        return 1.0;
25    }
26    sorted.sort_by(f64::total_cmp);
27    let rank = ((sorted.len() as f64 * 0.95).ceil() as usize).max(1);
28    sorted[rank.saturating_sub(1).min(sorted.len() - 1)]
29}
30
31pub fn context_band_for_tokens(tokens: usize, config: &Value) -> String {
32    let compact_max = pointer_u64(
33        config,
34        "/tokenization/context_bands/compact_max_tokens",
35        DEFAULT_COMPACT_MAX_TOKENS,
36    ) as usize;
37    let healthy_max = pointer_u64(
38        config,
39        "/tokenization/context_bands/healthy_max_tokens",
40        DEFAULT_HEALTHY_MAX_TOKENS,
41    ) as usize;
42    let warning_max = pointer_u64(
43        config,
44        "/tokenization/context_bands/warning_max_tokens",
45        DEFAULT_WARNING_MAX_TOKENS,
46    ) as usize;
47    if tokens <= compact_max {
48        "compact"
49    } else if tokens <= healthy_max {
50        "healthy"
51    } else if tokens <= warning_max {
52        "warning"
53    } else {
54        "critical"
55    }
56    .to_string()
57}
58
59pub fn context_pressure_for_tokens(tokens: usize, config: &Value) -> f64 {
60    let warning_max = pointer_u64(
61        config,
62        "/tokenization/context_bands/warning_max_tokens",
63        DEFAULT_WARNING_MAX_TOKENS,
64    )
65    .max(1) as f64;
66    (tokens as f64 / warning_max).min(1.0)
67}
68
69pub fn slop_band_for_score(score: f64) -> String {
70    if score >= 85.0 {
71        "critical"
72    } else if score >= 65.0 {
73        "high"
74    } else if score >= 50.0 {
75        "moderate"
76    } else {
77        "low"
78    }
79    .to_string()
80}
81
82fn age_pressure(age_days: u64, config: &Value) -> f64 {
83    if age_days == 0 {
84        return 0.0;
85    }
86    let half_life = pointer_f64(
87        config,
88        "/history/age_half_life_days",
89        DEFAULT_AGE_HALF_LIFE_DAYS,
90    );
91    if half_life <= 0.0 {
92        return 1.0;
93    }
94    1.0 - 2_f64.powf(-(age_days as f64 / half_life))
95}
96
97fn reason_codes(record: &FileAnalysis) -> Vec<String> {
98    let mut reasons = Vec::new();
99    match record.context_band.as_str() {
100        "critical" => reasons.push("critical_token_cost".to_string()),
101        "warning" => reasons.push("high_token_cost".to_string()),
102        _ => {}
103    }
104    if record.age_days >= 180 {
105        reasons.push("old_file".to_string());
106    }
107    if record.revision_norm >= 0.8 {
108        reasons.push("high_revision_frequency".to_string());
109    }
110    if record.relative_churn_norm >= 0.8 {
111        reasons.push("high_relative_churn".to_string());
112    }
113    if record.age_days >= 180 && record.churn_pressure >= 0.6 {
114        reasons.push("old_and_volatile".to_string());
115    }
116    reasons
117}
118
119/// Apply the stable 60/20/20 hotspot model in place.
120///
121/// Repository-relative churn components use nearest-rank p95 ordering.
122/// Intermediate metrics are rounded to six decimals and the final score to one
123/// decimal before banding, matching the public report contract.
124pub fn apply_scoring(records: &mut [FileAnalysis], config: &Value) {
125    let revision_p95 = p95(records.iter().map(|record| record.revisions_window as f64)).max(1.0);
126    let relative_churn_p95 = p95(records.iter().map(|record| record.relative_churn_window));
127    let relative_churn_denom = if relative_churn_p95 > 0.0 {
128        relative_churn_p95
129    } else {
130        1.0
131    };
132    let context_weight = pointer_f64(config, "/scoring/context_weight", DEFAULT_CONTEXT_WEIGHT);
133    let age_weight = pointer_f64(config, "/scoring/age_weight", DEFAULT_AGE_WEIGHT);
134    let churn_weight = pointer_f64(config, "/scoring/churn_weight", DEFAULT_CHURN_WEIGHT);
135
136    for record in records {
137        let raw_age_pressure = age_pressure(record.age_days, config);
138        let raw_revision_norm = (record.revisions_window as f64 / revision_p95).min(1.0);
139        let raw_relative_churn_norm =
140            (record.relative_churn_window / relative_churn_denom).min(1.0);
141        let raw_churn_pressure = 0.6 * raw_revision_norm + 0.4 * raw_relative_churn_norm;
142        let raw_score = 100.0
143            * (context_weight * record.context_pressure
144                + age_weight * raw_age_pressure
145                + churn_weight * raw_churn_pressure);
146
147        record.age_pressure = round_to(raw_age_pressure, 6);
148        record.revision_norm = round_to(raw_revision_norm, 6);
149        record.relative_churn_norm = round_to(raw_relative_churn_norm, 6);
150        record.churn_pressure = round_to(raw_churn_pressure, 6);
151        record.slop_score = round_to(raw_score, 1);
152        record.slop_band = slop_band_for_score(record.slop_score);
153        record.reason_codes = reason_codes(record);
154    }
155}
156
157pub fn folder_health_band(
158    direct_tokens: usize,
159    direct_file_count: usize,
160    config: &Value,
161) -> String {
162    let compact_max_tokens = pointer_u64(
163        config,
164        "/health/folder_bands/compact_max_direct_tokens",
165        31_999,
166    ) as usize;
167    let healthy_max_tokens = pointer_u64(
168        config,
169        "/health/folder_bands/healthy_max_direct_tokens",
170        128_000,
171    ) as usize;
172    let warning_max_tokens = pointer_u64(
173        config,
174        "/health/folder_bands/warning_max_direct_tokens",
175        256_000,
176    ) as usize;
177    let warning_max_files =
178        pointer_u64(config, "/health/folder_bands/warning_max_direct_files", 17) as usize;
179    let refactor_max_files = pointer_u64(
180        config,
181        "/health/folder_bands/refactor_required_max_direct_files",
182        37,
183    ) as usize;
184
185    if direct_tokens > warning_max_tokens || direct_file_count > refactor_max_files {
186        "refactor_required"
187    } else if direct_tokens > healthy_max_tokens || direct_file_count > warning_max_files {
188        "warning"
189    } else if direct_tokens > compact_max_tokens {
190        "healthy"
191    } else {
192        "compact"
193    }
194    .to_string()
195}
196
197fn direct_parent(path: &str) -> &str {
198    path.rsplit_once('/')
199        .map(|(parent, _)| parent)
200        .filter(|parent| !parent.is_empty())
201        .unwrap_or(".")
202}
203
204fn folder_classification(descendants: &[&FileAnalysis]) -> String {
205    let classifications: BTreeSet<&str> = descendants
206        .iter()
207        .map(|record| record.classification.as_str())
208        .collect();
209    if classifications.len() == 1 {
210        classifications
211            .into_iter()
212            .next()
213            .unwrap_or("other")
214            .to_string()
215    } else {
216        "mixed".to_string()
217    }
218}
219
220fn mean(values: impl IntoIterator<Item = f64>) -> f64 {
221    let values: Vec<f64> = values.into_iter().collect();
222    if values.is_empty() {
223        0.0
224    } else {
225        values.iter().sum::<f64>() / values.len() as f64
226    }
227}
228
229fn file_cost(record: &FileAnalysis, section: &str, key: &str) -> f64 {
230    record
231        .costs
232        .get(section)
233        .and_then(|value| value.get(key))
234        .and_then(Value::as_f64)
235        .unwrap_or_default()
236}
237
238fn folder_costs(descendants: &[&FileAnalysis], total_tokens: usize, config: &Value) -> Value {
239    let mut token_sizes: Vec<usize> = descendants.iter().map(|record| record.tokens).collect();
240    token_sizes.sort_unstable_by(|left, right| right.cmp(left));
241    let top_tokens = token_sizes.first().copied().unwrap_or(0);
242    json!({
243        "load": {
244            "file_token_count": top_tokens,
245            "folder_token_count": total_tokens,
246            "top_file_share": round_to(top_tokens as f64 / total_tokens.max(1) as f64, 6),
247            "top_3_file_share": round_to(
248                token_sizes.iter().take(3).sum::<usize>() as f64 / total_tokens.max(1) as f64,
249                6,
250            ),
251            "token_concentration_ratio": round_to(
252                top_tokens as f64 / total_tokens.max(1) as f64,
253                6,
254            ),
255            "context_band": context_band_for_tokens(total_tokens, config),
256            "load_pressure": round_to(context_pressure_for_tokens(total_tokens, config), 6),
257        },
258        "volatility": {
259            "commit_count_window": descendants
260                .iter()
261                .map(|record| record.revisions_window as f64)
262                .sum::<f64>(),
263            "recency_weighted_commits": round_to(
264                descendants.iter().map(|record| record.recency_weighted_commits).sum(),
265                6,
266            ),
267            "line_churn_window": round_to(
268                descendants
269                    .iter()
270                    .map(|record| record.line_churn_window as f64)
271                    .sum(),
272                6,
273            ),
274            "token_churn_window": descendants
275                .iter()
276                .map(|record| record.token_churn_window)
277                .sum::<usize>(),
278            "relative_token_churn": round_to(
279                mean(descendants.iter().map(|record| {
280                    record.token_churn_window as f64 / record.tokens.max(1) as f64
281                })),
282                6,
283            ),
284            "late_churn_spike": round_to(
285                mean(descendants.iter().map(|record| record.late_churn_spike)),
286                6,
287            ),
288            "volatility_pressure": round_to(
289                mean(descendants.iter().map(|record| record.churn_pressure)),
290                6,
291            ),
292        },
293        "coordination": {
294            "files_touched_per_change": round_to(
295                mean(descendants.iter().map(|record| {
296                    file_cost(record, "coordination", "files_touched_per_change")
297                })),
298                6,
299            ),
300            "folders_touched_per_change": round_to(
301                mean(descendants.iter().map(|record| {
302                    file_cost(record, "coordination", "folders_touched_per_change")
303                })),
304                6,
305            ),
306            "edit_hunks_per_change": round_to(
307                mean(descendants.iter().map(|record| {
308                    file_cost(record, "coordination", "edit_hunks_per_change")
309                })),
310                6,
311            ),
312            "cochange_degree": round_to(
313                mean(descendants.iter().map(|record| {
314                    file_cost(record, "coordination", "cochange_degree")
315                })),
316                6,
317            ),
318            "cochange_centrality": round_to(
319                mean(descendants.iter().map(|record| {
320                    file_cost(record, "coordination", "cochange_centrality")
321                })),
322                6,
323            ),
324            "cross_folder_cochange_ratio": round_to(
325                mean(descendants.iter().map(|record| {
326                    file_cost(record, "coordination", "cross_folder_cochange_ratio")
327                })),
328                6,
329            ),
330            "change_diffusion": round_to(
331                mean(descendants.iter().map(|record| {
332                    file_cost(record, "coordination", "change_diffusion")
333                })),
334                6,
335            ),
336            "coordination_pressure": round_to(
337                mean(descendants.iter().map(|record| {
338                    file_cost(record, "coordination", "coordination_pressure")
339                })),
340                6,
341            ),
342        },
343    })
344}
345
346fn aggregate_overlay_value(
347    folder_path: &str,
348    overlay_name: &str,
349    descendants: &[&FileAnalysis],
350) -> Value {
351    let overlay_values: Vec<&Value> = descendants
352        .iter()
353        .filter_map(|record| record.overlays.get(overlay_name))
354        .filter(|value| !value.is_null())
355        .collect();
356    if overlay_values.is_empty() {
357        return Value::Null;
358    }
359    let overlay_count = overlay_values.len();
360    let mut numeric: BTreeMap<String, Vec<f64>> = BTreeMap::new();
361    let mut booleans: BTreeMap<String, bool> = BTreeMap::new();
362    let mut lists: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
363    for value in overlay_values {
364        let Some(object) = value.as_object() else {
365            continue;
366        };
367        for (key, value) in object {
368            if key == "path" {
369                continue;
370            }
371            if let Some(boolean) = value.as_bool() {
372                booleans
373                    .entry(key.clone())
374                    .and_modify(|current| *current |= boolean)
375                    .or_insert(boolean);
376            } else if let Some(number) = value.as_f64() {
377                numeric.entry(key.clone()).or_default().push(number);
378            } else if let Some(items) = value.as_array() {
379                let values = lists.entry(key.clone()).or_default();
380                values.extend(
381                    items
382                        .iter()
383                        .filter_map(Value::as_str)
384                        .map(ToOwned::to_owned),
385                );
386            }
387        }
388    }
389    let mut result = serde_json::Map::new();
390    result.insert("path".to_string(), json!(folder_path));
391    result.insert("overlay_name".to_string(), json!(overlay_name));
392    for (key, values) in numeric {
393        result.insert(key, json!(round_to(mean(values), 6)));
394    }
395    for (key, value) in booleans {
396        result.insert(key, json!(value));
397    }
398    for (key, values) in lists {
399        result.insert(
400            key,
401            Value::Array(values.into_iter().take(20).map(Value::String).collect()),
402        );
403    }
404    result.insert("descendant_file_count".to_string(), json!(overlay_count));
405    Value::Object(result)
406}
407
408fn folder_overlays(folder_path: &str, descendants: &[&FileAnalysis]) -> Value {
409    let overlay_names = [
410        "organization_health",
411        "verification",
412        "navigation",
413        "blast_radius",
414        "stewardship",
415        "semantic_drift",
416    ];
417    let mut result = serde_json::Map::new();
418    for overlay_name in overlay_names {
419        result.insert(
420            overlay_name.to_string(),
421            aggregate_overlay_value(folder_path, overlay_name, descendants),
422        );
423    }
424    Value::Object(result)
425}
426
427pub fn build_folder_analysis(
428    path: &str,
429    descendants: &[&FileAnalysis],
430    config: &Value,
431) -> Option<FolderAnalysis> {
432    if descendants.is_empty() {
433        return None;
434    }
435    let mut ranked = descendants.to_vec();
436    ranked.sort_by(|left, right| {
437        right
438            .slop_score
439            .total_cmp(&left.slop_score)
440            .then_with(|| right.tokens.cmp(&left.tokens))
441            .then_with(|| left.path.cmp(&right.path))
442    });
443    let top = ranked[0];
444    let total_tokens = descendants.iter().map(|record| record.tokens).sum();
445    let direct: Vec<&FileAnalysis> = descendants
446        .iter()
447        .copied()
448        .filter(|record| direct_parent(&record.path) == path)
449        .collect();
450    let direct_tokens = direct.iter().map(|record| record.tokens).sum();
451    let mut seen_reasons = BTreeSet::new();
452    let mut reason_codes = Vec::new();
453    for record in &ranked {
454        for reason in &record.reason_codes {
455            if seen_reasons.insert(reason.clone()) {
456                reason_codes.push(reason.clone());
457            }
458        }
459    }
460    Some(FolderAnalysis {
461        path: path.to_string(),
462        descendant_file_count: descendants.len(),
463        direct_file_count: direct.len(),
464        bytes: descendants.iter().map(|record| record.bytes).sum(),
465        lines: descendants.iter().map(|record| record.lines).sum(),
466        tokens: total_tokens,
467        direct_tokens,
468        context_band: context_band_for_tokens(total_tokens, config),
469        health_band: folder_health_band(direct_tokens, direct.len(), config),
470        context_pressure: round_to(context_pressure_for_tokens(total_tokens, config), 6),
471        slop_score: top.slop_score,
472        slop_band: top.slop_band.clone(),
473        reason_codes,
474        top_file_path: top.path.clone(),
475        classification: folder_classification(descendants),
476        costs: folder_costs(descendants, total_tokens, config),
477        overlays: folder_overlays(path, descendants),
478    })
479}
480
481pub fn build_folder_analyses(files: &[FileAnalysis], config: &Value) -> Vec<FolderAnalysis> {
482    let mut grouped: BTreeMap<String, Vec<&FileAnalysis>> = BTreeMap::new();
483    for record in files {
484        for folder in parent_folders(&record.path) {
485            grouped.entry(folder).or_default().push(record);
486        }
487    }
488    let mut folders: Vec<FolderAnalysis> = grouped
489        .into_iter()
490        .filter_map(|(path, descendants)| build_folder_analysis(&path, &descendants, config))
491        .collect();
492    folders.sort_by(|left, right| {
493        (left.path != ".")
494            .cmp(&(right.path != "."))
495            .then_with(|| left.path.cmp(&right.path))
496    });
497    folders
498}
499
500pub fn aggregate_folders(files: &[FileAnalysis], config: &Value) -> Vec<FolderAnalysis> {
501    build_folder_analyses(files, config)
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507
508    fn file(path: &str, tokens: usize, revisions: usize, relative_churn: f64) -> FileAnalysis {
509        FileAnalysis {
510            path: path.to_string(),
511            bytes: tokens * 4,
512            lines: tokens / 2,
513            blank_lines: 0,
514            code_lines: tokens / 2,
515            comment_lines: 0,
516            language: "Rust".to_string(),
517            profile: "agent_context".to_string(),
518            classification: "source".to_string(),
519            tokens,
520            context_band: "compact".to_string(),
521            context_pressure: 0.0,
522            content_fingerprint: String::new(),
523            structural_tokens: Vec::new(),
524            structural_token_count: 0,
525            top_structural_terms: Vec::new(),
526            age_days: 0,
527            revisions_window: revisions,
528            recency_weighted_commits: 0.0,
529            added_window: 0,
530            deleted_window: 0,
531            churn_lines_window: 0,
532            line_churn_window: 0,
533            token_churn_window: 0,
534            relative_churn_window: relative_churn,
535            late_churn_spike: 0.0,
536            author_count_window: 0,
537            author_entropy: 0.0,
538            top_author_share: 0.0,
539            days_since_non_bot_edit: None,
540            recent_maintainer_diversity: 0,
541            age_pressure: 0.0,
542            revision_norm: 0.0,
543            relative_churn_norm: 0.0,
544            churn_pressure: 0.0,
545            slop_score: 0.0,
546            slop_band: String::new(),
547            reason_codes: Vec::new(),
548            costs: json!({}),
549            overlays: json!({}),
550        }
551    }
552
553    #[test]
554    fn context_and_slop_thresholds_match_public_contract() {
555        let config = json!({});
556        assert_eq!(context_band_for_tokens(3_072, &config), "compact");
557        assert_eq!(context_band_for_tokens(3_073, &config), "healthy");
558        assert_eq!(context_band_for_tokens(8_001, &config), "warning");
559        assert_eq!(context_band_for_tokens(10_001, &config), "critical");
560        assert_eq!(slop_band_for_score(49.9), "low");
561        assert_eq!(slop_band_for_score(50.0), "moderate");
562        assert_eq!(slop_band_for_score(65.0), "high");
563        assert_eq!(slop_band_for_score(85.0), "critical");
564    }
565
566    #[test]
567    fn scoring_uses_nearest_rank_p95_and_caps_outliers() {
568        let mut files: Vec<FileAnalysis> = (1..=20)
569            .map(|revision| file(&format!("src/{revision}.rs"), 100, revision, 0.0))
570            .collect();
571        apply_scoring(&mut files, &json!({}));
572        assert_eq!(files[17].revision_norm, round_to(18.0 / 19.0, 6));
573        assert_eq!(files[18].revision_norm, 1.0);
574        assert_eq!(files[19].revision_norm, 1.0);
575        assert_eq!(files[19].slop_score, 12.0);
576    }
577
578    #[test]
579    fn scoring_preserves_reason_order_and_rounded_band() {
580        let mut record = file("src/legacy.rs", 12_000, 10, 2.0);
581        record.context_band = "critical".to_string();
582        record.context_pressure = 1.0;
583        record.age_days = 180;
584        apply_scoring(std::slice::from_mut(&mut record), &json!({}));
585        assert_eq!(record.slop_score, 90.0);
586        assert_eq!(record.slop_band, "critical");
587        assert_eq!(
588            record.reason_codes,
589            vec![
590                "critical_token_cost",
591                "old_file",
592                "high_revision_frequency",
593                "high_relative_churn",
594                "old_and_volatile",
595            ]
596        );
597    }
598
599    #[test]
600    fn stable_relative_line_churn_drives_score_and_action_reason() {
601        let mut files = vec![
602            file("src/quiet.rs", 100, 1, 0.1),
603            file("src/volatile.rs", 100, 1, 2.0),
604        ];
605        apply_scoring(&mut files, &json!({}));
606
607        assert_eq!(files[0].relative_churn_norm, 0.05);
608        assert_eq!(files[1].relative_churn_norm, 1.0);
609        assert!(files[1].slop_score > files[0].slop_score);
610        assert!(
611            !files[0]
612                .reason_codes
613                .contains(&"high_relative_churn".to_string())
614        );
615        assert!(
616            files[1]
617                .reason_codes
618                .contains(&"high_relative_churn".to_string())
619        );
620    }
621
622    #[test]
623    fn stable_history_fields_keep_schema_four_integer_shapes() {
624        let mut record = file("src/history.rs", 100, 2, 0.5);
625        record.added_window = 7;
626        record.deleted_window = 3;
627        record.churn_lines_window = 10;
628        record.line_churn_window = 10;
629        record.token_churn_window = 24;
630
631        let value = serde_json::to_value(record).unwrap();
632        assert_eq!(value["added_window"].as_u64(), Some(7));
633        assert_eq!(value["deleted_window"].as_u64(), Some(3));
634        assert_eq!(value["churn_lines_window"].as_u64(), Some(10));
635        assert_eq!(value["token_churn_window"].as_u64(), Some(24));
636        assert_eq!(value["relative_churn_window"].as_f64(), Some(0.5));
637    }
638
639    #[test]
640    fn folder_aggregation_tracks_descendant_and_direct_pressure_separately() {
641        let mut top = file("src/top.rs", 20_000, 1, 0.0);
642        top.slop_score = 60.0;
643        top.slop_band = "moderate".to_string();
644        top.reason_codes = vec!["high_token_cost".to_string()];
645        let mut nested = file("src/nested/child.rs", 20_000, 1, 0.0);
646        nested.slop_score = 70.0;
647        nested.slop_band = "high".to_string();
648        nested.reason_codes = vec!["high_relative_churn".to_string()];
649        let folders = build_folder_analyses(&[top, nested], &json!({}));
650        let root = folders.iter().find(|folder| folder.path == ".").unwrap();
651        let src = folders.iter().find(|folder| folder.path == "src").unwrap();
652        let nested = folders
653            .iter()
654            .find(|folder| folder.path == "src/nested")
655            .unwrap();
656        assert_eq!(root.direct_file_count, 0);
657        assert_eq!(root.descendant_file_count, 2);
658        assert_eq!(src.direct_file_count, 1);
659        assert_eq!(src.direct_tokens, 20_000);
660        assert_eq!(src.tokens, 40_000);
661        assert_eq!(src.top_file_path, "src/nested/child.rs");
662        assert_eq!(src.reason_codes, ["high_relative_churn", "high_token_cost"]);
663        assert_eq!(nested.direct_file_count, 1);
664    }
665
666    #[test]
667    fn folder_health_uses_direct_token_and_file_limits() {
668        let config = json!({});
669        assert_eq!(folder_health_band(31_999, 17, &config), "compact");
670        assert_eq!(folder_health_band(32_000, 17, &config), "healthy");
671        assert_eq!(folder_health_band(128_001, 17, &config), "warning");
672        assert_eq!(folder_health_band(1, 18, &config), "warning");
673        assert_eq!(folder_health_band(256_001, 1, &config), "refactor_required");
674        assert_eq!(folder_health_band(1, 38, &config), "refactor_required");
675    }
676}