cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! Linter-style rule abstraction.
//!
//! Each validation cartulary performs is a [`Rule`]. A rule runs a
//! single [`find`](IssueRule::find) pass and reports [`*Finding`]s; a
//! finding carries the [`CheckViolation`] to report and, when the
//! repair is well-defined, the [`*Edit`] that fixes it
//! (`fix: Option<*Edit>`). This mirrors ESLint / clippy / Ruff: one
//! analysis pass per rule, fix attached to the finding it resolves.
//!
//! Rules are autonomous: adding a new rule does not require editing
//! shared types or registry vocabulary — define its struct, implement
//! the trait, register it in the per-scope rule list.
//!
//! Scope is encoded by trait choice ([`DecisionRecordRule`] vs
//! [`IssueRule`]) because the underlying repositories differ. Each
//! trait method receives a *check context* that bundles the per-scope
//! repository with the workspace-level configuration the rule may need
//! (known refs, statuses, tag descriptors). The context never owns the
//! data — every field is a borrow.

use std::path::{Path, PathBuf};

use crate::domain::model::decision_record::DecisionRecord;
use crate::domain::model::entity_ref::KnownRefs;
use crate::domain::model::issue::Issue;
use crate::domain::model::record_ref::{DecisionRecordRef, IssueRef};
use crate::domain::model::status::StatusesConfig;
use crate::domain::model::tag_descriptor::TagDescriptors;
use crate::domain::usecases::decision_record::DecisionRecordRepository;
use crate::domain::usecases::issue::IssueRepository;

// Model data the trait surface references — re-exported so existing
// callers (and the rule files) keep their imports short.
pub use crate::domain::model::check::CheckViolation;
pub use crate::domain::model::decision_record::DecisionRecordFinding;
pub use crate::domain::model::issue::IssueFinding;

/// Context passed to every [`DecisionRecordRule`]. The CLI loads the
/// records once and bundles them as `(path, record)` pairs so rules
/// can observe duplicate ids (two paths, same record id) — a
/// deduplicated map would lose that.
///
/// Only successfully parsed records appear; parse errors are surfaced
/// by the engine, not by individual rules.
pub struct DrCheckCtx<'a> {
    pub repo: &'a dyn DecisionRecordRepository,
    pub records: &'a [(PathBuf, DecisionRecord)],
    pub known_refs: &'a KnownRefs,
    /// Tag descriptors that scope to the kind these records belong to —
    /// `config.tag_descriptors_for(kind)` at the CLI boundary.
    pub tag_descriptors: &'a TagDescriptors,
}

/// Context passed to every [`IssueRule`]. Issues need more workspace
/// configuration than DRs because statuses and tag descriptors are
/// authored in `cartulary.toml` rather than baked into the binary.
pub struct IssueCheckCtx<'a> {
    pub repo: &'a dyn IssueRepository,
    pub issues: &'a [(PathBuf, Issue)],
    pub known_refs: &'a KnownRefs,
    pub statuses: &'a StatusesConfig,
    pub tag_descriptors: &'a TagDescriptors,
}

impl DrCheckCtx<'_> {
    /// Convenience: look up the path of the record with the given id.
    /// First-occurrence wins on duplicate ids; rules that care about
    /// duplicates should walk `records` directly.
    pub fn path_of(&self, id: &DecisionRecordRef) -> Option<&Path> {
        self.records
            .iter()
            .find(|(_, r)| &r.id == id)
            .map(|(p, _)| p.as_path())
    }
}

impl IssueCheckCtx<'_> {
    /// Convenience: look up the path of the issue with the given id.
    /// See [`DrCheckCtx::path_of`].
    pub fn path_of(&self, id: &IssueRef) -> Option<&Path> {
        self.issues
            .iter()
            .find(|(_, i)| &i.id == id)
            .map(|(p, _)| p.as_path())
    }
}

/// A rule that validates decision records under a single configured kind.
pub trait DecisionRecordRule {
    /// Stable identifier used in rule output and (later) suppression rules.
    fn id(&self) -> &'static str;

    /// Run one analysis pass; return a finding per violation, with the
    /// `fix` populated when the repair is well-defined.
    fn find(&self, ctx: &DrCheckCtx<'_>) -> anyhow::Result<Vec<DecisionRecordFinding>>;
}

/// A rule that validates issues.
pub trait IssueRule {
    fn id(&self) -> &'static str;

    /// Run one analysis pass; return a finding per violation, with the
    /// `fix` populated when the repair is well-defined.
    fn find(&self, ctx: &IssueCheckCtx<'_>) -> anyhow::Result<Vec<IssueFinding>>;
}

// Re-export rule registries so the CLI sees one entry point per scope.
pub use crate::domain::usecases::decision_record::rules::dr_rules;
pub use crate::domain::usecases::issue::rules::issue_rules;

pub mod fix;
pub mod run;
pub use fix::{run_fix, FixDrSource, FixIssueSource, FixItem, FixMode, FixReport};
pub use run::{
    check_dr_kind, check_issue_corpus, run_check, CheckReport, CheckedEntry, DrSource, IssueSource,
};