1use crate::parse::Artifact;
14use crate::pycompat::py_round;
15use crate::spec::{specs, ArtifactSpec};
16
17pub const CONFIDENCE_THRESHOLD: f64 = 0.5;
18
19#[derive(Debug, Clone)]
21pub struct TypeScore {
22 pub name: String,
23 pub matched_required: Vec<String>,
24 pub matched_recommended: Vec<String>,
25 pub missing: Vec<String>,
26 pub points: f64,
27 pub ceiling: f64,
28 pub fit: f64,
29}
30
31#[derive(Debug, Clone)]
33pub struct Classification {
34 pub artifact_type: String,
36 pub confidence: f64,
38 pub present_sections: Vec<String>,
39 pub missing_sections: Vec<String>,
40}
41
42fn mapped<'a>(artifact: &'a Artifact, spec: &'a ArtifactSpec) -> Vec<&'a str> {
46 let mut out: Vec<&str> = Vec::new();
47 for (heading, _) in &artifact.product.sections {
48 let m = spec.synonym(heading).unwrap_or(heading.as_str());
49 if !out.contains(&m) {
50 out.push(m);
51 }
52 }
53 out
54}
55
56pub fn missing_sections(artifact: &Artifact, spec: &ArtifactSpec) -> (Vec<String>, Vec<String>) {
59 let m = mapped(artifact, spec);
60 let missing_required = spec
61 .required
62 .iter()
63 .filter(|s| !m.contains(&s.as_str()))
64 .cloned()
65 .collect();
66 let missing_recommended = spec
67 .recommended
68 .iter()
69 .filter(|s| !m.contains(&s.as_str()))
70 .cloned()
71 .collect();
72 (missing_required, missing_recommended)
73}
74
75pub fn score_artifacts(artifact: &Artifact) -> Vec<TypeScore> {
78 let mut scores: Vec<TypeScore> = Vec::new();
79 for spec in specs() {
80 let m = mapped(artifact, spec);
81 let matched_required: Vec<String> = spec
82 .required
83 .iter()
84 .filter(|s| m.contains(&s.as_str()))
85 .cloned()
86 .collect();
87 let matched_recommended: Vec<String> = spec
88 .recommended
89 .iter()
90 .filter(|s| m.contains(&s.as_str()))
91 .cloned()
92 .collect();
93 let missing: Vec<String> = spec
95 .required
96 .iter()
97 .chain(spec.recommended.iter())
98 .filter(|s| !m.contains(&s.as_str()))
99 .cloned()
100 .collect();
101 let points = matched_required.len() as f64 + 0.5 * matched_recommended.len() as f64;
102 let ceiling = spec.required.len() as f64 + 0.5 * spec.recommended.len() as f64;
103 let fit = if ceiling != 0.0 { points / ceiling } else { 0.0 };
104 scores.push(TypeScore {
105 name: spec.name.clone(),
106 matched_required,
107 matched_recommended,
108 missing,
109 points,
110 ceiling,
111 fit,
112 });
113 }
114 scores.sort_by(|a, b| {
119 b.fit
120 .partial_cmp(&a.fit)
121 .unwrap()
122 .then(b.matched_required.len().cmp(&a.matched_required.len()))
123 });
124 scores
125}
126
127pub fn classify(artifact: &Artifact) -> Classification {
129 let scores = score_artifacts(artifact);
130 let best = &scores[0]; if best.fit < CONFIDENCE_THRESHOLD || best.matched_required.is_empty() {
132 return Classification {
133 artifact_type: "unknown".to_string(),
134 confidence: py_round(best.fit, 2),
135 present_sections: artifact
136 .product
137 .sections
138 .iter()
139 .map(|(h, _)| h.clone())
140 .collect(),
141 missing_sections: Vec::new(),
142 };
143 }
144 let mut present = best.matched_required.clone();
145 present.extend(best.matched_recommended.iter().cloned());
146 Classification {
147 artifact_type: best.name.clone(),
148 confidence: py_round(best.fit, 2),
149 present_sections: present,
150 missing_sections: best.missing.clone(),
151 }
152}