cordance-scan 0.1.1

Cordance repository scanners. Deterministic surface classification.
Documentation
//! Path → `SourceClass`. Pure function; no I/O.

use cordance_core::source::SourceClass;

/// Classify a repo-relative path into a `SourceClass` using ordered directory
/// and filename rules. First match wins.
#[must_use]
pub fn classify(rel_path: &str) -> SourceClass {
    let p = rel_path.replace('\\', "/");

    // Doctrine (engineering-doctrine sibling).
    if p.starts_with("doctrine/principles/") || p.contains("/doctrine/principles/") {
        return SourceClass::EngineeringDoctrinePrinciple;
    }
    if p.starts_with("doctrine/patterns/") || p.contains("/doctrine/patterns/") {
        return SourceClass::EngineeringDoctrinePattern;
    }
    if p.starts_with("doctrine/checklists/") || p.contains("/doctrine/checklists/") {
        return SourceClass::EngineeringDoctrineChecklist;
    }
    if p.starts_with("doctrine/tooling/") || p.contains("/doctrine/tooling/") {
        return SourceClass::EngineeringDoctrineTooling;
    }
    if p.ends_with("doctrine/glossary.md") {
        return SourceClass::EngineeringDoctrineGlossary;
    }
    if p.starts_with("doctrine/evolution/") {
        return SourceClass::EngineeringDoctrineEvolution;
    }

    // ADRs.
    if p.starts_with("docs/adr/")
        && std::path::Path::new(&p)
            .extension()
            .is_some_and(|e| e.eq_ignore_ascii_case("md"))
    {
        return SourceClass::ProjectAdr;
    }

    // Project schemas / contracts.
    if p.starts_with("contracts/") && p.ends_with(".schema.json") {
        return SourceClass::ProjectSchema;
    }
    if p.starts_with("contracts/") {
        return SourceClass::ProjectContract;
    }

    // Tests.
    if p.starts_with("tests/") || p.contains("/tests/") {
        return SourceClass::ProjectTest;
    }

    // CI / release gates.
    if p.starts_with(".github/workflows/")
        || p.starts_with(".pipelines/")
        || p == "deny.toml"
        || p == "rust-toolchain.toml"
        || p == "release-plz.toml"
        || p.ends_with(".taudit-suppressions.yml")
        || p.ends_with(".tsafe.yml")
        || p.ends_with(".gitleaksignore")
        || p.ends_with(".checkov.yaml")
    {
        return SourceClass::ProjectReleaseGate;
    }

    // Repo-tracked agent files.
    //
    // `.cordance/pack.json` was historically classified here so agents could
    // read it as a context source. Round-5 bughunt CRITICAL R5-bughunt-1
    // showed that letting pack.json appear in `pack.sources` creates a hash
    // self-reference loop: pack.json embeds the hash of pack.json. The
    // scanner now blocks the whole `.cordance/` tree (see
    // `blocked::PATH_SUBSTRINGS`), so this classifier branch is unreachable
    // — `.cordance/pack.json` is `BlockedSurface` before this code runs.
    if p == "AGENTS.md"
        || p == "CLAUDE.md"
        || p == "CLAUDE.md.template"
        || p.starts_with("agents/")
        || p.starts_with(".cursor/")
        || p == ".claude/settings.json"
    {
        return SourceClass::ProjectAgentFile;
    }

    // Readme + product spec at root.
    if p == "README.md"
        || p == "ENGINEERING.md"
        || p == "GOVERNANCE.md"
        || p == "PLAN.md"
        || p == "HORIZON.md"
        || p == "LAYERS.md"
        || p == "EXTENSIBILITY.md"
        || p == "IMPLEMENTATION_PLAN.md"
        || p == "CHANGELOG.md"
        || p == "CONTRIBUTING.md"
        || p == "SECURITY.md"
    {
        return SourceClass::ProjectReadme;
    }

    // Source code (broad).
    let ext_matches = std::path::Path::new(&p).extension().is_some_and(|e| {
        e.eq_ignore_ascii_case("rs")
            || e.eq_ignore_ascii_case("ts")
            || e.eq_ignore_ascii_case("py")
            || e.eq_ignore_ascii_case("go")
    });
    if p.starts_with("crates/") || p.starts_with("src/") || ext_matches {
        return SourceClass::ProjectSourceCode;
    }

    SourceClass::Unclassified
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn engineering_doctrine_principle_classified() {
        assert_eq!(
            classify("doctrine/principles/build.md"),
            SourceClass::EngineeringDoctrinePrinciple
        );
    }

    #[test]
    fn adr_classified() {
        assert_eq!(classify("docs/adr/0001-foo.md"), SourceClass::ProjectAdr);
    }

    #[test]
    fn release_gate_classified() {
        assert_eq!(classify("deny.toml"), SourceClass::ProjectReleaseGate);
        assert_eq!(
            classify(".github/workflows/quality.yml"),
            SourceClass::ProjectReleaseGate
        );
    }

    #[test]
    fn windows_paths_normalised() {
        assert_eq!(classify("docs\\adr\\0001-foo.md"), SourceClass::ProjectAdr);
    }
}