use serde::{Deserialize, Serialize};
use crate::plan::operation::TargetPath;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum FindingKind {
Structural,
Budget,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Finding {
pub kind: FindingKind,
pub path: TargetPath,
pub rule: String,
pub statement: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub measurement: Option<Measurement>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Measurement {
pub dimension: String,
pub found: u64,
pub cap: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StyleCandidate {
pub path: TargetPath,
pub reason: String,
}
pub mod detector {
pub const FOREIGN_DOCS_ROOT: &str = "foreign-documentation-root";
pub const SPEC_WITHOUT_RULE_ID: &str = "spec-without-a-rule-id";
pub const ORDINAL_FILENAME: &str = "ordinal-filename";
pub const RECORD_OUTSIDE_DECISIONS: &str = "record-outside-the-decisions-directory";
pub const NO_SPECS_DIRECTORY: &str = "no-specifications-directory";
}
#[must_use]
pub fn is_ordinal_name(name: &str) -> bool {
let stem = name.strip_suffix(".md").unwrap_or(name);
let Some((head, rest)) = stem.split_once('-') else {
return false;
};
!head.is_empty() && head.bytes().all(|byte| byte.is_ascii_digit()) && !rest.is_empty()
}
#[must_use]
pub fn is_spec_shaped(name: &str) -> bool {
name.starts_with("SPEC-") && is_markdown(name)
}
fn is_markdown(name: &str) -> bool {
std::path::Path::new(name)
.extension()
.is_some_and(|extension| extension == "md")
}
#[must_use]
pub fn is_record_shaped(name: &str) -> bool {
name.starts_with("ADR-") && is_markdown(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_position_is_an_ordinal_name_and_a_subject_is_not() {
assert!(is_ordinal_name("01-intro.md"));
assert!(is_ordinal_name("0007-thing.md"));
assert!(!is_ordinal_name("roadmap-2026.md"));
assert!(!is_ordinal_name("intro.md"));
assert!(!is_ordinal_name("README.md"));
assert!(!is_ordinal_name("2026.md"));
assert!(!is_ordinal_name("01-.md"));
}
#[test]
fn only_this_conventions_shapes_are_recognized() {
assert!(is_spec_shaped("SPEC-distribution.md"));
assert!(!is_spec_shaped("notes.md"));
assert!(!is_spec_shaped("SPEC-distribution.rst"));
assert!(is_record_shaped("ADR-a-choice.md"));
assert!(!is_record_shaped("decision.md"));
}
}