crate-seq-ledger

Overview

crate-seq-ledger is the type foundation of the workspace. It defines the core domain types, the state machine for version lifecycle transitions, query helpers, file I/O, and tag-pattern utilities. Every other crate in the workspace imports from it.

Root config — CrateSeqLedger

The root struct mirrors the .crate-seq.toml file structure:

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct CrateSeqLedger {
    #[serde(rename = "crate")]
    pub crate_config: CrateConfig,
    pub settings: LedgerSettings,
    pub auth: LedgerAuth,
    #[serde(rename = "versions", default)]
    pub entries: Vec<LedgerEntry>,
}

CrateConfig

pub struct CrateConfig {
    pub name: String,               // crate name from Cargo.toml
    pub registry: Option<String>,    // registry alias, defaults to crates.io
}

LedgerSettings

pub struct LedgerSettings {
    pub mode: Option<String>,           // "git-tag" or "snapshot"
    pub tag_pattern: Option<String>,    // glob for tag matching, e.g. "v*"
    pub dry_run_default: Option<bool>,  // if true, dry-run by default
    pub backoff_base_ms: Option<u64>,   // base delay for exponential backoff
}

LedgerAuth

pub struct LedgerAuth {
    pub token_env: Option<String>,  // env var holding registry token
    pub token_cmd: Option<String>,  // shell command whose stdout is the token
}

All config structs derive Default and use #[serde(default)] on optional fields, so partial .crate-seq.toml files deserialize without error.

Entry types — types.rs

LedgerEntry

pub struct LedgerEntry {
    pub version: semver::Version,
    pub source: VersionSource,
    #[serde(rename = "ref")]
    pub ref_: String,       // tag name or snapshot SHA-256
    pub status: LedgerStatus,
}

VersionSource

#[serde(rename_all = "kebab-case")]
pub enum VersionSource {
    GitTag,
    Snapshot,
}

LedgerStatus

#[serde(rename_all = "kebab-case")]
pub enum LedgerStatus {
    Pending,
    Published,
    Skipped,
    Yanked,
}

All types derive Serialize and Deserialize with kebab-case renaming, ensuring TOML round-trip fidelity.

State machine — state.rs

Each method locates the entry by version, validates the current status, and transitions:

Method Valid from Target Error on invalid
mark_published(&mut self, version) Pending, Skipped Published InvalidTransition
mark_skipped(&mut self, version) Pending, Skipped, Yanked Skipped InvalidTransition
mark_yanked(&mut self, version) Published Yanked InvalidTransition

All methods return Result<(), Error>. If the version is not found in entries, they return Error::VersionNotFound. If the current status is not in the "Valid from" set, they return Error::InvalidTransition(version, current_state, target_state).

Transition rules

  • Published → Published is rejected (idempotent publishes go through the pipeline's AlreadyPublished path instead)
  • Yanked → Skipped is allowed — enables re-skipping a yanked version
  • Yanked → Yanked is rejected — no double-yank

Query methods — query.rs

/// entries with status Pending, sorted ascending by version.
#[must_use]
pub fn pending_versions(&self) -> Vec<&LedgerEntry>;

/// entries with status Published, sorted ascending by version.
#[must_use]
pub fn published_versions(&self) -> Vec<&LedgerEntry>;

/// first entry whose version equals `v`.
#[must_use]
pub fn find_version(&self, v: &semver::Version) -> Option<&LedgerEntry>;

Both pending_versions and published_versions return entries sorted ascending by SemVer.

I/O functions — io.rs

load

pub fn load(path: &Path) -> Result<CrateSeqLedger, Error>

Reads the file at path as a string, then deserializes via toml_edit::de::from_str. Returns Error::Io on read failure or Error::Deserialize on parse failure.

save

pub fn save(path: &Path, ledger: &CrateSeqLedger) -> Result<(), Error>

Serializes with toml_edit::ser::to_string_pretty, writes to a .tmp sidecar, then renames atomically. If rename fails, the .tmp file is cleaned up before returning the error.

Atomicity guarantee

The write-then-rename pattern ensures that a crash mid-write never leaves a corrupted .crate-seq.toml. The .tmp file is always cleaned up on rename failure.

Tag pattern utilities — tag.rs

detect_tag_pattern

pub fn detect_tag_pattern(crate_name: &str, is_workspace: bool) -> String

Returns "v*" for single-crate repos, "{crate_name}-v*" for workspaces.

extract_semver_from_tag

pub fn extract_semver_from_tag(tag: &str, pattern: &str) -> Option<semver::Version>

Strips the literal prefix (pattern minus trailing *) from tag, then attempts semver::Version::parse on the remainder. Returns None if the prefix doesn't match or the remainder isn't valid SemVer.

Error type — error.rs

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),

    #[error("TOML deserialize: {0}")]
    Deserialize(#[from] toml_edit::de::Error),

    #[error("TOML serialize: {0}")]
    Serialize(#[from] toml_edit::ser::Error),

    #[error("version {0} not found in ledger")]
    VersionNotFound(semver::Version),

    #[error("invalid state transition for {0}: {1}{2}")]
    InvalidTransition(semver::Version, &'static str, &'static str),
}

#[from] attributes enable automatic conversion via ? from std::io::Error, toml_edit::de::Error, and toml_edit::ser::Error.

Source files

File Responsibility
config.rs Root config structs (CrateSeqLedger, CrateConfig, LedgerSettings, LedgerAuth)
types.rs Entry types (LedgerEntry, VersionSource, LedgerStatus)
state.rs State machine transitions (mark_published, mark_skipped, mark_yanked)
query.rs Query helpers (pending_versions, published_versions, find_version)
io.rs Atomic TOML load/save
tag.rs Tag pattern detection and SemVer extraction
error.rs Crate error enum
lib.rs Module declarations and re-exports