1#![deny(warnings)]
45
46use std::collections::HashMap;
47use std::path::PathBuf;
48
49use crate::federation::scanner::InstalledAsset;
50use crate::federation::versions::{compare_versions, freshness_score, round_half_up_1dp};
51use crate::models::coverage::{AssetType, CellStatus, CoverageCell, CoverageMatrix, CoverageRow};
52use crate::models::provider::Provider;
53use crate::persistence::writer::{DbCommand, WriterHandle};
54
55pub async fn persist_coverage_scores(
61 tx: &WriterHandle,
62 scores: &[(String, String, f64)],
63 computed_at: &str,
64) {
65 for (workspace_path, workspace_name, coverage_score) in scores {
66 let _ = tx
67 .send(DbCommand::RecordCoverageScore {
68 workspace_path: workspace_path.clone(),
69 workspace_name: workspace_name.clone(),
70 coverage_score: *coverage_score,
71 computed_at: computed_at.to_string(),
72 })
73 .await;
74 }
75}
76
77pub struct CoverageEngine;
86
87impl CoverageEngine {
88 pub fn build_matrix(
109 canonical_asset_ids: &[String],
110 workspaces: &[(PathBuf, Vec<InstalledAsset>)],
111 canonical_hashes: &HashMap<String, String>,
112 canonical_versions: &HashMap<String, String>,
113 ) -> CoverageMatrix {
114 let mut install_index: HashMap<(PathBuf, String), &InstalledAsset> = HashMap::new();
117 for (ws_path, assets) in workspaces {
118 for asset in assets {
119 if asset.confirmed {
120 install_index.insert((ws_path.clone(), asset.asset_id.clone()), asset);
121 }
122 }
123 }
124
125 let workspace_names: Vec<String> = workspaces
128 .iter()
129 .map(|(path, _)| {
130 path.file_name()
131 .and_then(|n| n.to_str())
132 .map(|s| s.to_string())
133 .unwrap_or_else(|| path.display().to_string())
134 })
135 .collect();
136
137 let rows: Vec<CoverageRow> = canonical_asset_ids
139 .iter()
140 .map(|id| CoverageRow {
141 asset_id: id.clone(),
142 asset_type: infer_asset_type(id),
143 asset_name: asset_display_name(id),
144 provider: infer_provider(id),
145 })
146 .collect();
147
148 let mut cells: HashMap<(String, String), CoverageCell> = HashMap::new();
150
151 let mut ws_installed: Vec<usize> = vec![0; workspaces.len()];
153 let mut ws_total: Vec<usize> = vec![0; workspaces.len()];
154
155 for asset_id in canonical_asset_ids {
156 let canonical_hash = canonical_hashes.get(asset_id).map(|s| s.as_str());
157 let canonical_version = canonical_versions.get(asset_id).map(|s| s.as_str());
158
159 for (ws_idx, (ws_path, _)) in workspaces.iter().enumerate() {
160 let ws_name = &workspace_names[ws_idx];
161
162 ws_total[ws_idx] += 1; let cell =
165 if let Some(asset) = install_index.get(&(ws_path.clone(), asset_id.clone())) {
166 let status = classify_cell_status(asset, canonical_hash, canonical_version);
167 if status == CellStatus::Installed {
168 ws_installed[ws_idx] += 1;
169 }
170 CoverageCell {
171 status,
172 installed_version: asset.installed_version.clone(),
173 canonical_version: canonical_version.unwrap_or("").to_string(),
174 installed_hash: Some(asset.content_hash.clone()),
175 canonical_hash: canonical_hash.map(|s| s.to_string()),
176 }
177 } else {
178 CoverageCell {
179 status: CellStatus::NotInstalled,
180 installed_version: Option::None,
181 canonical_version: canonical_version.unwrap_or("").to_string(),
182 installed_hash: Option::None,
183 canonical_hash: canonical_hash.map(|s| s.to_string()),
184 }
185 };
186
187 cells.insert((asset_id.clone(), ws_name.clone()), cell);
188 }
189 }
190
191 let mut workspace_scores: HashMap<String, f64> = HashMap::new();
193 for (ws_idx, ws_name) in workspace_names.iter().enumerate() {
194 if let Some(score) =
195 Self::compute_coverage_score(ws_installed[ws_idx], ws_total[ws_idx])
196 {
197 workspace_scores.insert(ws_name.clone(), score);
198 }
199 }
201
202 CoverageMatrix {
203 rows,
204 columns: workspace_names,
205 cells,
206 workspace_scores,
207 }
208 }
209
210 pub fn compute_coverage_score(
222 installed_matching: usize,
223 total_applicable: usize,
224 ) -> Option<f64> {
225 if total_applicable == 0 {
226 return Option::None;
227 }
228 let ratio = installed_matching as f64 / total_applicable as f64;
229 Some(round_half_up_1dp(ratio * 100.0))
230 }
231
232 pub fn compute_freshness_score(current_count: usize, total_with_versions: usize) -> f64 {
242 freshness_score(current_count, total_with_versions)
243 }
244}
245
246fn classify_cell_status(
257 asset: &InstalledAsset,
258 canonical_hash: Option<&str>,
259 canonical_version: Option<&str>,
260) -> CellStatus {
261 if let Some(chash) = canonical_hash {
263 if asset.content_hash == chash {
264 return CellStatus::Installed;
265 }
266 } else {
267 if let (Some(iv), Some(cv)) = (asset.installed_version.as_deref(), canonical_version) {
271 if compare_versions(iv, cv) == std::cmp::Ordering::Less {
272 return CellStatus::Outdated;
273 }
274 }
275 return CellStatus::Installed;
276 }
277
278 if let (Some(iv), Some(cv)) = (asset.installed_version.as_deref(), canonical_version) {
280 if compare_versions(iv, cv) == std::cmp::Ordering::Less {
281 return CellStatus::Outdated;
282 }
283 }
284
285 CellStatus::Drifted
287}
288
289fn infer_asset_type(asset_id: &str) -> AssetType {
295 if asset_id.starts_with("agents/") {
296 AssetType::Agent
297 } else if asset_id.starts_with("skills/") {
298 AssetType::Skill
299 } else if asset_id.starts_with("rules/") {
300 AssetType::Rule
301 } else if asset_id.starts_with("mcp/") {
302 AssetType::McpRef
303 } else {
304 AssetType::Agent }
306}
307
308fn asset_display_name(asset_id: &str) -> String {
313 let last = asset_id.split('/').next_back().unwrap_or(asset_id);
314 last.split('-')
315 .map(|word| {
316 let mut c = word.chars();
317 match c.next() {
318 None => String::new(),
319 Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
320 }
321 })
322 .collect::<Vec<_>>()
323 .join(" ")
324}
325
326fn infer_provider(asset_id: &str) -> Provider {
340 let parts: Vec<&str> = asset_id.split('/').collect();
341 if parts.len() < 2 {
342 return Provider::Generic;
343 }
344 let component = parts[1].to_ascii_lowercase();
345 let key = match component.as_str() {
348 "k8s" => "kubernetes",
349 other => other,
350 };
351 serde_json::from_value::<Provider>(serde_json::Value::String(key.to_string()))
352 .unwrap_or(Provider::Generic)
353}
354
355#[cfg(test)]
360mod tests {
361 use super::*;
362 use crate::federation::scanner::DetectionMethod;
363 use proptest::prelude::*;
364
365 fn make_confirmed_asset(
370 path: &str,
371 asset_id: &str,
372 content_hash: &str,
373 installed_version: Option<&str>,
374 ) -> InstalledAsset {
375 InstalledAsset {
376 workspace_path: PathBuf::from(path),
377 asset_id: asset_id.to_string(),
378 installed_version: installed_version.map(|s| s.to_string()),
379 content_hash: content_hash.to_string(),
380 detection_methods: vec![DetectionMethod::Filename, DetectionMethod::MetadataComment],
381 confirmed: true,
382 harness: ".claude".to_string(),
383 }
384 }
385
386 #[test]
391 fn cell_installed_when_hash_matches() {
392 let asset = make_confirmed_asset("/ws/file.md", "agents/aws/cdk", "abc", Some("1.0.0"));
393 assert_eq!(
394 classify_cell_status(&asset, Some("abc"), Some("1.0.0")),
395 CellStatus::Installed
396 );
397 }
398
399 #[test]
400 fn cell_outdated_when_hash_mismatch_version_behind() {
401 let asset =
402 make_confirmed_asset("/ws/file.md", "agents/aws/cdk", "old_hash", Some("1.0.0"));
403 assert_eq!(
405 classify_cell_status(&asset, Some("new_hash"), Some("1.2.0")),
406 CellStatus::Outdated
407 );
408 }
409
410 #[test]
411 fn cell_drifted_when_hash_mismatch_version_same() {
412 let asset = make_confirmed_asset("/ws/file.md", "agents/aws/cdk", "actual", Some("1.0.0"));
413 assert_eq!(
414 classify_cell_status(&asset, Some("expected"), Some("1.0.0")),
415 CellStatus::Drifted
416 );
417 }
418
419 #[test]
420 fn cell_drifted_when_hash_mismatch_no_version() {
421 let asset = make_confirmed_asset("/ws/file.md", "agents/aws/cdk", "actual", None);
422 assert_eq!(
423 classify_cell_status(&asset, Some("expected"), None),
424 CellStatus::Drifted
425 );
426 }
427
428 #[test]
429 fn cell_drifted_when_hash_mismatch_installed_ahead() {
430 let asset =
432 make_confirmed_asset("/ws/file.md", "agents/aws/cdk", "new_hash", Some("2.0.0"));
433 assert_eq!(
434 classify_cell_status(&asset, Some("old_hash"), Some("1.0.0")),
435 CellStatus::Drifted
436 );
437 }
438
439 #[test]
444 fn coverage_score_none_when_zero_applicable() {
445 assert_eq!(CoverageEngine::compute_coverage_score(0, 0), Option::None);
446 }
447
448 #[test]
449 fn coverage_score_100_when_all_installed() {
450 assert_eq!(CoverageEngine::compute_coverage_score(5, 5), Some(100.0));
451 }
452
453 #[test]
454 fn coverage_score_0_when_none_installed() {
455 assert_eq!(CoverageEngine::compute_coverage_score(0, 5), Some(0.0));
456 }
457
458 #[test]
459 fn coverage_score_half() {
460 assert_eq!(CoverageEngine::compute_coverage_score(5, 10), Some(50.0));
461 }
462
463 #[test]
464 fn coverage_score_rounding_half_up() {
465 let s = CoverageEngine::compute_coverage_score(1, 3).unwrap();
467 assert!((s - 33.3).abs() < 1e-9, "expected 33.3 got {s}");
468 }
469
470 #[test]
471 fn coverage_score_x5_boundary() {
472 let s = CoverageEngine::compute_coverage_score(3, 20).unwrap();
474 assert!((s - 15.0).abs() < 1e-9, "expected 15.0 got {s}");
475 }
476
477 #[test]
482 fn build_matrix_empty_inputs() {
483 let matrix = CoverageEngine::build_matrix(&[], &[], &HashMap::new(), &HashMap::new());
484 assert!(matrix.rows.is_empty());
485 assert!(matrix.columns.is_empty());
486 assert!(matrix.cells.is_empty());
487 assert!(matrix.workspace_scores.is_empty());
488 }
489
490 #[test]
491 fn build_matrix_not_installed() {
492 let assets: Vec<InstalledAsset> = vec![];
493 let ws_path = PathBuf::from("/workspaces/team-a");
494 let workspaces = vec![(ws_path, assets)];
495 let canonical_ids = vec!["agents/aws/cdk".to_string()];
496 let mut hashes = HashMap::new();
497 hashes.insert("agents/aws/cdk".to_string(), "abc123".to_string());
498
499 let matrix =
500 CoverageEngine::build_matrix(&canonical_ids, &workspaces, &hashes, &HashMap::new());
501
502 assert_eq!(matrix.rows.len(), 1);
503 assert_eq!(matrix.columns.len(), 1);
504 let cell = matrix
505 .cells
506 .get(&("agents/aws/cdk".to_string(), "team-a".to_string()))
507 .expect("cell should exist");
508 assert_eq!(cell.status, CellStatus::NotInstalled);
509 assert_eq!(matrix.workspace_scores.get("team-a"), Some(&0.0));
511 }
512
513 #[test]
514 fn build_matrix_installed_hash_match() {
515 let ws_path = PathBuf::from("/workspaces/team-a");
516 let asset = make_confirmed_asset(
517 "/workspaces/team-a/.claude/cdk-agent.md",
518 "agents/aws/cdk",
519 "abc123",
520 Some("1.0.0"),
521 );
522 let workspaces = vec![(ws_path, vec![asset])];
523 let canonical_ids = vec!["agents/aws/cdk".to_string()];
524 let mut hashes = HashMap::new();
525 hashes.insert("agents/aws/cdk".to_string(), "abc123".to_string());
526 let mut versions = HashMap::new();
527 versions.insert("agents/aws/cdk".to_string(), "1.0.0".to_string());
528
529 let matrix = CoverageEngine::build_matrix(&canonical_ids, &workspaces, &hashes, &versions);
530
531 let cell = matrix
532 .cells
533 .get(&("agents/aws/cdk".to_string(), "team-a".to_string()))
534 .expect("cell should exist");
535 assert_eq!(cell.status, CellStatus::Installed);
536 assert_eq!(matrix.workspace_scores.get("team-a"), Some(&100.0));
537 }
538
539 #[test]
540 fn build_matrix_outdated_version_behind() {
541 let ws_path = PathBuf::from("/workspaces/team-a");
542 let asset = make_confirmed_asset(
543 "/workspaces/team-a/.claude/cdk-agent.md",
544 "agents/aws/cdk",
545 "old_hash",
546 Some("1.0.0"),
547 );
548 let workspaces = vec![(ws_path, vec![asset])];
549 let canonical_ids = vec!["agents/aws/cdk".to_string()];
550 let mut hashes = HashMap::new();
551 hashes.insert("agents/aws/cdk".to_string(), "new_hash".to_string());
552 let mut versions = HashMap::new();
553 versions.insert("agents/aws/cdk".to_string(), "1.2.0".to_string());
554
555 let matrix = CoverageEngine::build_matrix(&canonical_ids, &workspaces, &hashes, &versions);
556
557 let cell = matrix
558 .cells
559 .get(&("agents/aws/cdk".to_string(), "team-a".to_string()))
560 .expect("cell should exist");
561 assert_eq!(cell.status, CellStatus::Outdated);
562 assert_eq!(matrix.workspace_scores.get("team-a"), Some(&0.0));
564 }
565
566 #[test]
567 fn build_matrix_drifted() {
568 let ws_path = PathBuf::from("/workspaces/team-a");
569 let asset = make_confirmed_asset(
570 "/workspaces/team-a/.claude/cdk-agent.md",
571 "agents/aws/cdk",
572 "modified_hash",
573 Some("1.0.0"),
574 );
575 let workspaces = vec![(ws_path, vec![asset])];
576 let canonical_ids = vec!["agents/aws/cdk".to_string()];
577 let mut hashes = HashMap::new();
578 hashes.insert("agents/aws/cdk".to_string(), "canonical_hash".to_string());
579 let mut versions = HashMap::new();
580 versions.insert("agents/aws/cdk".to_string(), "1.0.0".to_string());
581
582 let matrix = CoverageEngine::build_matrix(&canonical_ids, &workspaces, &hashes, &versions);
583
584 let cell = matrix
585 .cells
586 .get(&("agents/aws/cdk".to_string(), "team-a".to_string()))
587 .expect("cell should exist");
588 assert_eq!(cell.status, CellStatus::Drifted);
589 assert_eq!(matrix.workspace_scores.get("team-a"), Some(&0.0));
590 }
591
592 #[test]
593 fn build_matrix_no_applicable_assets_score_omitted() {
594 let ws_path = PathBuf::from("/workspaces/team-a");
595 let workspaces: Vec<(PathBuf, Vec<InstalledAsset>)> = vec![(ws_path, vec![])];
596 let matrix =
598 CoverageEngine::build_matrix(&[], &workspaces, &HashMap::new(), &HashMap::new());
599 assert!(
601 !matrix.workspace_scores.contains_key("team-a"),
602 "workspace with no applicable assets should not have a score"
603 );
604 }
605
606 #[test]
607 fn build_matrix_unconfirmed_assets_excluded() {
608 use crate::federation::scanner::DetectionMethod;
609 let ws_path = PathBuf::from("/workspaces/team-a");
610 let mut asset = make_confirmed_asset(
611 "/workspaces/team-a/.claude/cdk-agent.md",
612 "agents/aws/cdk",
613 "abc123",
614 Some("1.0.0"),
615 );
616 asset.confirmed = false;
618 asset.detection_methods = vec![DetectionMethod::Filename];
619
620 let workspaces = vec![(ws_path, vec![asset])];
621 let canonical_ids = vec!["agents/aws/cdk".to_string()];
622 let mut hashes = HashMap::new();
623 hashes.insert("agents/aws/cdk".to_string(), "abc123".to_string());
624
625 let matrix =
626 CoverageEngine::build_matrix(&canonical_ids, &workspaces, &hashes, &HashMap::new());
627
628 let cell = matrix
629 .cells
630 .get(&("agents/aws/cdk".to_string(), "team-a".to_string()))
631 .expect("cell should exist");
632 assert_eq!(cell.status, CellStatus::NotInstalled);
634 }
635
636 #[test]
641 fn infer_provider_maps_java_board() {
642 assert_eq!(
645 infer_provider("agents/java/jdk-lifecycle-and-upgrade"),
646 Provider::Java
647 );
648 assert_eq!(
649 infer_provider("skills/java/java-jdk-lifecycle-and-upgrade"),
650 Provider::Java
651 );
652 }
653
654 #[test]
655 fn infer_provider_maps_typescript_board() {
656 assert_eq!(
662 infer_provider("agents/typescript/typescript-maestro-agent"),
663 Provider::Typescript
664 );
665 assert_eq!(
666 infer_provider("skills/typescript/typescript-runtime-boundary-contract"),
667 Provider::Typescript
668 );
669 }
670
671 #[test]
672 fn infer_provider_unknown_falls_back_to_generic() {
673 assert_eq!(
675 infer_provider("agents/not-a-real-provider/foo"),
676 Provider::Generic
677 );
678 }
679
680 #[test]
681 fn infer_provider_maps_databricks_board() {
682 assert_eq!(
683 infer_provider("agents/databricks/databricks-maestro-agent"),
684 Provider::Databricks
685 );
686 assert_eq!(
687 infer_provider("skills/databricks/databricks-unity-catalog-governance"),
688 Provider::Databricks
689 );
690 }
691
692 #[test]
693 fn infer_provider_keeps_k8s_directory_alias() {
694 assert_eq!(infer_provider("agents/k8s/foo"), Provider::Kubernetes);
696 assert_eq!(
697 infer_provider("agents/kubernetes/foo"),
698 Provider::Kubernetes
699 );
700 }
701
702 #[test]
711 fn infer_provider_covers_every_catalog_provider() {
712 let catalog =
713 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../catalog/agents.json");
714 let Ok(raw) = std::fs::read_to_string(&catalog) else {
715 eprintln!("skipping: {} not reachable", catalog.display());
716 return;
717 };
718 let agents: Vec<serde_json::Value> =
719 serde_json::from_str(&raw).expect("catalog/agents.json parses");
720 let mut providers: Vec<String> = agents
721 .iter()
722 .filter_map(|a| a.get("provider")?.as_str().map(str::to_string))
723 .collect();
724 providers.sort();
725 providers.dedup();
726 assert!(
727 !providers.is_empty(),
728 "catalog yielded no providers — the assertion below would be vacuous"
729 );
730 for provider in providers {
731 let inferred = infer_provider(&format!("agents/{provider}/some-agent"));
732 assert_eq!(
733 inferred.to_string(),
734 provider,
735 "provider {provider:?} in catalog/agents.json is not inferable from its \
736 asset path — add the matching variant to models::provider::Provider"
737 );
738 }
739 }
740
741 prop_compose! {
749 fn arb_hash()(s in "[0-9a-f]{10}") -> String { s }
750 }
751
752 prop_compose! {
753 fn arb_version_str()(
754 major in 0u8..5,
755 minor in 0u8..10,
756 patch in 0u8..10,
757 ) -> String {
758 format!("{major}.{minor}.{patch}")
759 }
760 }
761
762 proptest! {
763 #![proptest_config(proptest::test_runner::Config {
764 cases: 256,
765 ..Default::default()
766 })]
767
768 #[test]
770 fn prop12a_installed_iff_hash_matches(
771 hash in arb_hash(),
772 version in arb_version_str(),
773 ) {
774 let asset = make_confirmed_asset("/ws/file.md", "agents/test", &hash, Some(&version));
775 let status = classify_cell_status(&asset, Some(&hash), Some(&version));
777 prop_assert_eq!(status, CellStatus::Installed,
778 "hash match should be Installed");
779 }
780
781 #[test]
783 fn prop12b_outdated_when_hash_mismatch_version_behind(
784 hash_installed in arb_hash(),
785 hash_canonical in arb_hash(),
786 imajor in 0u8..5,
787 iminor in 0u8..8,
788 ipatch in 0u8..10,
789 extra_minor in 1u8..5,
790 ) {
791 prop_assume!(hash_installed != hash_canonical);
792 let canonical_minor = iminor + extra_minor;
793 prop_assume!(canonical_minor < 30);
794
795 let iv = format!("{imajor}.{iminor}.{ipatch}");
796 let cv = format!("{imajor}.{canonical_minor}.{ipatch}");
797
798 let asset = make_confirmed_asset("/ws/file.md", "agents/test", &hash_installed, Some(&iv));
799 let status = classify_cell_status(&asset, Some(&hash_canonical), Some(&cv));
800 prop_assert_eq!(status, CellStatus::Outdated,
801 "hash mismatch + installed behind → Outdated");
802 }
803
804 #[test]
806 fn prop12c_drifted_when_hash_mismatch_version_not_behind(
807 hash_installed in arb_hash(),
808 hash_canonical in arb_hash(),
809 version in arb_version_str(),
810 ) {
811 prop_assume!(hash_installed != hash_canonical);
812 let asset = make_confirmed_asset("/ws/file.md", "agents/test", &hash_installed, Some(&version));
814 let status = classify_cell_status(&asset, Some(&hash_canonical), Some(&version));
815 prop_assert_eq!(status, CellStatus::Drifted,
816 "hash mismatch + same version → Drifted");
817 }
818
819 #[test]
822 fn prop12d_absent_asset_is_not_installed(
823 hash in arb_hash(),
824 version in arb_version_str(),
825 ) {
826 let ws_path = PathBuf::from("/ws");
827 let workspaces: Vec<(PathBuf, Vec<InstalledAsset>)> = vec![(ws_path, vec![])];
829 let canonical_ids = vec!["agents/test".to_string()];
830 let mut hashes = HashMap::new();
831 hashes.insert("agents/test".to_string(), hash.clone());
832 let mut versions = HashMap::new();
833 versions.insert("agents/test".to_string(), version.clone());
834
835 let matrix = CoverageEngine::build_matrix(
836 &canonical_ids, &workspaces, &hashes, &versions,
837 );
838
839 let cell = matrix.cells.values().next().expect("should have a cell");
840 prop_assert_eq!(&cell.status, &CellStatus::NotInstalled);
841 prop_assert!(cell.installed_hash.is_none());
842 }
843 }
844
845 proptest! {
846 #![proptest_config(proptest::test_runner::Config {
847 cases: 256,
848 ..Default::default()
849 })]
850
851 #[test]
858 fn prop13a_coverage_score_range_and_none(
859 installed in 0usize..=500,
860 total in 0usize..=500,
861 ) {
862 let installed = installed.min(total); match CoverageEngine::compute_coverage_score(installed, total) {
864 Option::None => prop_assert_eq!(total, 0,
865 "None should only be returned when total==0"),
866 Some(score) => {
867 prop_assert!(total > 0, "Some score requires total > 0");
868 prop_assert!((0.0..=100.0).contains(&score),
869 "score {} out of range [0, 100]", score);
870 }
871 }
872 }
873
874 #[test]
877 fn prop13b_round_half_up_x5_boundary(
878 n in 1usize..=200,
879 ) {
880 let ratio = n as f64 / 200_f64;
883 let expected = round_half_up_1dp(ratio * 100.0);
884 let got = CoverageEngine::compute_coverage_score(n, 200).unwrap();
885 prop_assert!(
886 (got - expected).abs() < 1e-9,
887 "compute_coverage_score({}, 200) = {} but round_half_up gives {}",
888 n, got, expected
889 );
890 }
891
892 #[test]
894 fn prop13c_freshness_score_range(
895 current in 0usize..=1000,
896 total in 0usize..=1000,
897 ) {
898 let current = current.min(total);
899 let score = CoverageEngine::compute_freshness_score(current, total);
900 prop_assert!(
901 (0.0..=100.0).contains(&score),
902 "freshness_score({}, {}) = {} out of range",
903 current, total, score
904 );
905 }
906 }
907}