cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
use std::path::{Path, PathBuf};

use crate::infra::driven::cozo::adapter::CozoAdapter;
use crate::infra::driven::cozo::store::CozoFileStore;
use crate::infra::driven::fs::config::{CartularyConfig, KindConfig};
use crate::infra::driven::fs::decision_record_repository::FsDecisionRecordRepository;
use crate::infra::driven::fs::id_generator::{UlidDecisionRecordIdGenerator, UlidIssueIdGenerator};
use crate::infra::driven::fs::issue_repository::FsIssueRepository;
use crate::infra::driving::cli::OutputFormat;

use crate::domain::model::status::StatusesConfig;

/// Shared context threaded through every CLI command.
///
/// Built once in `dispatch()` from the loaded config and global flags.
/// Passed by shared reference to every subcommand dispatcher and leaf
/// `execute()` function, replacing the current ad-hoc parameter lists.
pub(crate) struct Context<'cfg> {
    /// Output format requested by the user (`--output` / `-o`).
    pub output_fmt: OutputFormat,

    /// Root directory of the target project.
    pub root_dir: PathBuf,

    /// Configured statuses for issues.
    ///
    /// Available to commands that need to display or filter by status
    /// (e.g. `list`, `show`, `stats`).
    pub issues_statuses: &'cfg StatusesConfig,

    // ── Private fields ────────────────────────────────────────────────────────
    issues_dir: PathBuf,
    issues_id_prefix: Option<String>,

    config: &'cfg CartularyConfig,
}

impl<'cfg> Context<'cfg> {
    /// Build a `Context` from the loaded config and global flags.
    pub fn new(config: &'cfg CartularyConfig, root_dir: PathBuf, output_fmt: OutputFormat) -> Self {
        Self {
            output_fmt,
            root_dir,
            issues_statuses: &config.issues_statuses,
            issues_dir: config.issues_dir.clone(),
            issues_id_prefix: config.issues_id_prefix.clone(),
            config,
        }
    }

    /// Build a ready-to-use issue repository.
    pub fn issue_repository(&self) -> FsIssueRepository {
        FsIssueRepository {
            dir: self.issues_dir.clone(),
            union: self.config.issues_union.clone(),
            id_prefix: self.issues_id_prefix.clone(),
            statuses: self.issues_statuses.clone(),
            schema_version: self.config.schema_version,
        }
    }

    /// Build the ULID-based id generator for issues. The prefix falls
    /// back to `"ISSUE"` when none is configured (matches the CLI display
    /// default).
    pub fn issue_id_generator(&self) -> UlidIssueIdGenerator {
        UlidIssueIdGenerator {
            id_prefix: self
                .issues_id_prefix
                .clone()
                .unwrap_or_else(|| "ISSUE".to_string()),
        }
    }

    /// Build the ULID-based id generator for a DR kind. The prefix falls
    /// back to the uppercase kind name when none is configured.
    pub fn decision_record_id_generator(
        &self,
        kind_cfg: &KindConfig,
    ) -> UlidDecisionRecordIdGenerator {
        UlidDecisionRecordIdGenerator {
            id_prefix: kind_cfg
                .id_prefix
                .clone()
                .unwrap_or_else(|| kind_cfg.kind.to_uppercase()),
        }
    }

    /// Build a ready-to-use decision record repository for `kind_cfg`.
    pub fn decision_record_repository(&self, kind_cfg: &KindConfig) -> FsDecisionRecordRepository {
        FsDecisionRecordRepository {
            dir: kind_cfg.dir.clone(),
            union: kind_cfg.union.clone(),
            kind: kind_cfg.kind.clone(),
            id_prefix: kind_cfg.id_prefix.clone(),
            schema_version: self.config.schema_version,
        }
    }

    /// Build a defect scanner over the issue corpus — surfaces load-time
    /// failures that the repository skips.
    pub fn issue_defect_scanner(
        &self,
    ) -> crate::infra::driven::fs::issue_defect_scanner::FsIssueDefectScanner {
        crate::infra::driven::fs::issue_defect_scanner::FsIssueDefectScanner {
            dir: self.issues_dir.clone(),
            union: self.config.issues_union.clone(),
            id_prefix: self.issues_id_prefix.clone(),
            statuses: self.issues_statuses.clone(),
            schema_version: self.config.schema_version,
        }
    }

    /// Build a defect scanner over the decision-record corpus of one kind.
    pub fn decision_record_defect_scanner(
        &self,
        kind_cfg: &KindConfig,
    ) -> crate::infra::driven::fs::decision_record_defect_scanner::FsDecisionRecordDefectScanner
    {
        crate::infra::driven::fs::decision_record_defect_scanner::FsDecisionRecordDefectScanner {
            dir: kind_cfg.dir.clone(),
            union: kind_cfg.union.clone(),
            kind: kind_cfg.kind.clone(),
            id_prefix: kind_cfg.id_prefix.clone(),
            schema_version: self.config.schema_version,
        }
    }

    /// Return the issues directory path (e.g. for editor invocation).
    pub fn issues_dir(&self) -> &Path {
        &self.issues_dir
    }

    /// Return the issues ID prefix (e.g. `"ISSUE-"`), if configured.
    pub fn issues_id_prefix(&self) -> Option<&str> {
        self.issues_id_prefix.as_deref()
    }

    /// Build a `QueryStore` over the configured query directory.
    pub fn query_store(&self) -> CozoFileStore {
        CozoFileStore::new(self.config.query_dir.clone())
    }

    /// Build a `QueryRunner` populated with the workspace's facts
    /// (issues, decision records, tags). Heavy to construct — the
    /// in-memory db loads every record on creation.
    pub fn query_runner(&self) -> anyhow::Result<CozoAdapter> {
        let issue_repo = self.issue_repository();
        let dr_owned: Vec<(String, FsDecisionRecordRepository)> = self
            .config
            .decision_kinds
            .iter()
            .map(|k| (k.kind.clone(), self.decision_record_repository(k)))
            .collect();
        use crate::domain::usecases::decision_record::DecisionRecordRepository;
        let dr_refs: Vec<(String, &dyn DecisionRecordRepository)> = dr_owned
            .iter()
            .map(|(k, r)| (k.clone(), r as &dyn DecisionRecordRepository))
            .collect();
        CozoAdapter::from_workspace(
            &issue_repo,
            &dr_refs,
            &self.config.tag_descriptors_for("issues"),
        )
    }

    /// Return the full loaded config (needed by `check` and `completions`).
    pub fn config(&self) -> &'cfg CartularyConfig {
        self.config
    }

    /// Build a populated `KnownRefs` covering every entry across every
    /// configured decision kind plus issues. Used by `check` to validate
    /// cross-entry link targets (ISSUE-018D373JDKYFA).
    pub fn load_known_refs(&self) -> anyhow::Result<crate::domain::model::entity_ref::KnownRefs> {
        let dr_repos: Vec<
            crate::infra::driven::fs::decision_record_repository::FsDecisionRecordRepository,
        > = self
            .config
            .decision_kinds
            .iter()
            .map(|k| self.decision_record_repository(k))
            .collect();
        let dr_refs: Vec<&dyn crate::domain::usecases::decision_record::DecisionRecordRepository> =
            dr_repos
                .iter()
                .map(|r| {
                    r as &dyn crate::domain::usecases::decision_record::DecisionRecordRepository
                })
                .collect();
        let issue_repo = self.issue_repository();
        crate::domain::usecases::links::load_known_refs(&dr_refs, &issue_repo)
    }
}