cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! ULID-based adapters for the id-generation ports. Production sources the
//! system clock and the OS RNG here in the infra layer, then hands them to
//! the pure `Ulid::from_millis_with_random` constructor; tests use the
//! deterministic counters in the `test_support` modules.
//!
//! The TSID factory survives only as a migration helper: the v3→v4 step
//! seeds TSIDs from each entry's `created` event because that was the
//! identifier shape at v4. Fresh entries created by the current binary
//! always carry a ULID.

use std::time::{SystemTime, UNIX_EPOCH};

use crate::domain::model::record_ref::{DecisionRecordRef, IssueRef};
use crate::domain::model::ulid::Ulid;
use crate::domain::usecases::decision_record::DecisionRecordIdGenerator;
use crate::domain::usecases::issue::IssueIdGenerator;
use crate::domain::usecases::migrate::legacy::tsid::Tsid;

fn now_millis() -> i64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_millis() as i64)
        .unwrap_or(0)
}

fn random_u128() -> u128 {
    use rand::RngCore;
    let mut buf = [0u8; 16];
    rand::rngs::OsRng.fill_bytes(&mut buf);
    u128::from_le_bytes(buf)
}

/// Build a freshly-allocated TSID for a fixed millisecond timestamp. Used
/// by the v3→v4 migration to seed TSIDs from existing `created` events;
/// nothing in the production code path calls this.
pub fn tsid_from_millis(unix_ms: i64) -> Tsid {
    use rand::RngCore;
    let mask = (1u32 << crate::domain::usecases::migrate::legacy::tsid::RANDOM_BITS) - 1;
    Tsid::from_millis_with_random(unix_ms, rand::rngs::OsRng.next_u32() & mask)
}

fn fresh_ulid() -> Ulid {
    Ulid::from_millis_with_random(now_millis(), random_u128())
}

/// Build a freshly-allocated ULID for a fixed millisecond timestamp.
/// Used by the v7→v8 migration to mint a ULID from a chosen timestamp
/// source (TSID-derived, `created` event, or `date:` at midnight).
pub fn ulid_from_millis(unix_ms: i64) -> Ulid {
    Ulid::from_millis_with_random(unix_ms, random_u128())
}

fn format_id(prefix: &str) -> String {
    format!("{}-{}", prefix.trim_end_matches('-'), fresh_ulid())
}

/// Issue id generator emitting ULID suffixes. `id_prefix` is the
/// *resolved* prefix string the caller obtained from `cartulary.toml`
/// (defaulting to `"ISSUE"` at the config layer); this adapter does not
/// carry its own fallback.
pub struct UlidIssueIdGenerator {
    pub id_prefix: String,
}

impl IssueIdGenerator for UlidIssueIdGenerator {
    fn next_id(&self) -> anyhow::Result<IssueRef> {
        IssueRef::parse_v5(&format_id(&self.id_prefix))
    }
}

/// Decision-record id generator emitting ULID suffixes. `id_prefix` is
/// the resolved prefix string for this DR kind (the per-kind config
/// value, with the kind-name uppercase fallback applied by the caller).
pub struct UlidDecisionRecordIdGenerator {
    pub id_prefix: String,
}

impl DecisionRecordIdGenerator for UlidDecisionRecordIdGenerator {
    fn next_id(&self) -> anyhow::Result<DecisionRecordRef> {
        DecisionRecordRef::parse_v5(&format_id(&self.id_prefix))
    }
}