cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! Serde-driven mirror of `cartulary.toml`.
//!
//! Every Raw type lives in infra so the domain model stays serde-free
//! (the precedent is `RawEventAction` mirroring `EventAction`). The
//! conversion to domain types is in [`super::from_raw`].
//!
//! Strictness rules:
//! - `#[serde(deny_unknown_fields)]` on every closed table — a typo in
//!   a key fails the load with the offending path.
//! - Tables with dynamic keys (`[sources.<name>]`, `[decisions.<kind>]`,
//!   `[issues.statuses.<name>]`, `[tags.<name>]`) cannot use
//!   `deny_unknown_fields` on the outer container by serde's design;
//!   the inner Raw type carries it instead.
//! - Optional fields use `#[serde(default)]`; required fields have no
//!   default and surface a missing-field error.
//! - Per-field defaults that are not just `None` / `false` / empty live
//!   in this file via `#[serde(default = "fn")]`, so the doc-gen
//!   schema walker (ISSUE-017DAX6CX3Q9M) can introspect them.
//!   ISSUE-01951GBF7ES6W tracks the migration; defaults that span
//!   several fields, depend on dynamic keys, or distinguish "absent"
//!   from "explicit value" stay in `from_raw.rs` with a comment.
//!
//! Compatibility carve-outs:
//! - `RawDecisionKind.statuses` / `.preset` are accepted-and-ignored —
//!   the DR workflow is hardcoded (DDR-018QWJVHRH35B), and `cartu
//!   migrate v5→v6` strips them, but a config that never migrated
//!   should still load.

use std::collections::BTreeMap;

use serde::Deserialize;

#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawConfig {
    #[serde(default)]
    pub version: Option<u32>,
    #[serde(default)]
    pub issues: Option<RawIssues>,
    #[serde(default)]
    pub decisions: Option<RawDecisions>,
    #[serde(default)]
    pub sources: BTreeMap<String, RawSource>,
    #[serde(default)]
    pub docs: BTreeMap<String, RawDocsEntry>,
    #[serde(default)]
    pub site: Option<RawSite>,
    #[serde(default)]
    pub query: Option<RawQuery>,
    /// Tag descriptors at the config root. Each entry's `applies_to`
    /// declares which record kinds it scopes (`"issues"`, `"adr"`, …).
    #[serde(default)]
    pub tags: BTreeMap<String, RawTagDescriptor>,
}

#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawQuery {
    #[serde(default)]
    pub dir: Option<String>,
}

#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawSite {
    #[serde(default)]
    pub title: Option<String>,
    #[serde(default)]
    pub theme: Option<String>,
    #[serde(default)]
    pub out: Option<String>,
    #[serde(default)]
    pub nav: Vec<RawNavEntry>,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawNavEntry {
    pub label: String,
    pub url: String,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawIssues {
    #[serde(default = "default_issues_dir")]
    pub dir: String,
    /// Additional read-only directories merged in for reads. See
    /// ISSUE-01F8K0WCA2BNB.
    #[serde(default)]
    pub union: Vec<String>,
    #[serde(default)]
    pub id_prefix: Option<String>,
    #[serde(default)]
    pub preset: Option<String>,
    #[serde(default)]
    pub initial: Option<String>,
    #[serde(default)]
    pub statuses: BTreeMap<String, RawStatusConfig>,
}

impl Default for RawIssues {
    fn default() -> Self {
        Self {
            dir: default_issues_dir(),
            union: Vec::new(),
            id_prefix: None,
            preset: None,
            initial: None,
            statuses: BTreeMap::new(),
        }
    }
}

fn default_issues_dir() -> String {
    "docs/issues".to_string()
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawStatusConfig {
    #[serde(default)]
    pub next: Vec<String>,
    #[serde(default)]
    pub active: bool,
    #[serde(default)]
    pub terminal: bool,
    #[serde(default)]
    pub label: Option<String>,
    #[serde(default = "default_status_category")]
    pub category: String,
}

fn default_status_category() -> String {
    "unknown".to_string()
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawTagDescriptor {
    #[serde(default)]
    pub levels: Vec<String>,
    #[serde(default = "default_cardinality")]
    pub cardinality: String,
    #[serde(default)]
    pub ordered: bool,
    #[serde(default)]
    pub weights: Option<Vec<u32>>,
    /// Rollup aggregation: `or`, `and`, `max`, `min`, or absent for
    /// no rollup. `max`/`min` require `ordered = true`.
    #[serde(default)]
    pub aggregate: Option<String>,
    /// Record kinds this descriptor scopes to. Empty = inert.
    #[serde(default)]
    pub applies_to: Vec<String>,
}

fn default_cardinality() -> String {
    "any".to_string()
}

#[derive(Debug, Default, Deserialize)]
pub struct RawDecisions {
    #[serde(default)]
    pub types: Vec<String>,
    /// Captures every `[decisions.<kind>]` sub-table by its dynamic key.
    /// `serde(flatten)` is incompatible with `deny_unknown_fields`, so
    /// strictness on individual kind tables is enforced by
    /// [`RawDecisionKind`] instead.
    #[serde(flatten)]
    pub kinds: BTreeMap<String, RawDecisionKind>,
}

#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawDecisionKind {
    #[serde(default)]
    pub dir: Option<String>,
    /// Additional read-only directories merged in for reads. See
    /// ISSUE-01F8K0WCA2BNB.
    #[serde(default)]
    pub union: Vec<String>,
    #[serde(default)]
    pub id_prefix: Option<String>,
    /// Accepted-and-ignored: the DR workflow is hardcoded.
    /// `cartu migrate v5→v6` strips this key, but older configs may
    /// still carry it.
    #[serde(default)]
    pub statuses: Option<toml::Value>,
    /// Accepted-and-ignored, same rationale as `statuses`.
    #[serde(default)]
    pub preset: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawDocsEntry {
    #[serde(rename = "type")]
    pub kind: String,
    pub source: String,
    pub publish: String,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawSource {
    #[serde(rename = "type")]
    pub source_type: String,
    pub url: String,
    pub project: String,
    pub token_env: String,
    #[serde(default)]
    pub status_map: BTreeMap<String, String>,
}