use crate::classify::classify;
use crate::parse::{parse_file, Artifact};
use crate::pycompat::py_strip;
use crate::relationships::extract_relationships;
use crate::spec::{canonical_value, spec_for, specs};
use crate::walk::find_markdown_files;
pub struct InspectionResult {
pub artifact_type: String,
pub confidence: f64,
pub present_sections: Vec<String>,
pub missing_sections: Vec<String>,
pub status: Option<String>,
pub category: Option<String>,
pub supersedes: Option<String>,
pub relationships: Vec<(String, Vec<String>)>,
}
pub struct FileInspection {
pub path: String,
pub artifact_type: String,
pub confidence: f64,
}
pub struct DirectoryInspection {
pub directory: String,
pub recursive: bool,
pub files: Vec<FileInspection>,
}
impl DirectoryInspection {
pub fn total_files(&self) -> usize {
self.files.len()
}
pub fn counts(&self) -> Vec<(&str, usize)> {
let mut counts: Vec<(&str, usize)> = specs().iter().map(|s| (s.name.as_str(), 0)).collect();
counts.push(("unknown", 0));
for f in &self.files {
if let Some(slot) = counts.iter_mut().find(|(name, _)| *name == f.artifact_type) {
slot.1 += 1;
} else {
counts.push((f.artifact_type.as_str(), 1));
}
}
counts
}
pub fn unknown_count(&self) -> usize {
self.counts()
.iter()
.find(|(name, _)| *name == "unknown")
.map(|(_, n)| *n)
.unwrap_or(0)
}
}
fn first_line(body: &str) -> &str {
for line in crate::pycompat::py_splitlines(body) {
let stripped = py_strip(line);
if !stripped.is_empty() {
return stripped;
}
}
""
}
fn attach_decision_metadata(result: &mut InspectionResult, artifact: &Artifact) {
let Some(spec) = spec_for("decision") else {
return;
};
for (field_name, allowed) in &spec.metadata {
let Some(body) = artifact.section(field_name) else {
continue;
};
if body.is_empty() {
continue;
}
let value = canonical_value(first_line(body), allowed);
match field_name.as_str() {
"status" => result.status = Some(value),
"category" => result.category = Some(value),
_ => {}
}
}
if let Some(supersedes) = artifact.section("supersedes") {
if !supersedes.is_empty() {
result.supersedes = Some(first_line(supersedes).to_string());
}
}
}
pub fn build_inspection(artifact: &Artifact) -> InspectionResult {
let c = classify(artifact);
let mut result = InspectionResult {
artifact_type: c.artifact_type.clone(),
confidence: c.confidence,
present_sections: c.present_sections,
missing_sections: c.missing_sections,
status: None,
category: None,
supersedes: None,
relationships: Vec::new(),
};
if c.artifact_type == "decision" {
attach_decision_metadata(&mut result, artifact);
}
if let Some(spec) = spec_for(&c.artifact_type) {
result.relationships = extract_relationships(artifact, spec);
}
result
}
pub fn inspect_directory(directory: &str, recursive: bool) -> DirectoryInspection {
use rayon::prelude::*;
let files: Vec<FileInspection> = find_markdown_files(directory, recursive)
.into_par_iter()
.map(|entry| {
let artifact = parse_file(&entry.display);
let c = classify(&artifact);
FileInspection {
path: entry.display,
artifact_type: c.artifact_type,
confidence: c.confidence,
}
})
.collect();
DirectoryInspection {
directory: directory.to_string(),
recursive,
files,
}
}