haematite 0.6.1

Content-addressed, branchable, actor-native storage engine
Documentation
//! The vacuum report (§7): per-shard and total inventory, mark tallies,
//! metadata sources consulted, and every condition that would refuse a sweep.
//!
//! The report is the operator's artifact — the first thing this workstream
//! produces is a report that deletes nothing, whose numbers the operator can
//! check against what their own eyes told them (§9.5). Everything here is
//! serde-serialisable so the CLI can emit it as JSON alongside the
//! human-readable rendering.

use std::path::PathBuf;

use serde::Serialize;

/// Which vacuum operation produced this report.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum VacuumMode {
    /// Report-only: zero deletions, zero writes to the store.
    Stats,
}

/// Node counts and compressed bytes for one store (or the totals row).
/// `enumerated = marked + unmarked` by construction; the report shows all
/// three so the reconciliation is checkable by eye.
#[derive(Debug, Clone, Copy, Default, Serialize)]
pub struct NodeTotals {
    pub enumerated_nodes: usize,
    pub enumerated_bytes: u64,
    pub marked_nodes: usize,
    pub marked_bytes: u64,
    pub unmarked_nodes: usize,
    pub unmarked_bytes: u64,
}

impl NodeTotals {
    pub(super) const fn accumulate(&mut self, other: Self) {
        self.enumerated_nodes += other.enumerated_nodes;
        self.enumerated_bytes += other.enumerated_bytes;
        self.marked_nodes += other.marked_nodes;
        self.marked_bytes += other.marked_bytes;
        self.unmarked_nodes += other.unmarked_nodes;
        self.unmarked_bytes += other.unmarked_bytes;
    }
}

/// Whether a shard directory existed (lazy shards legitimately never
/// materialise — §2 M1 ⟨r2, B3⟩: only a WHOLLY absent `shard-{id}` directory
/// is a never-materialised lazy shard).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ShardPresence {
    Materialised,
    NeverMaterialised,
}

/// A store entry the strict inventory model does not accept (§4): wrong-length
/// names, foreign files, symlinks, unexpected directories. Reported; that
/// store's sweep refuses; stats still completes.
#[derive(Debug, Clone, Serialize)]
pub struct MalformedEntry {
    pub path: PathBuf,
    pub reason: String,
}

/// One shard's slice of the report.
#[derive(Debug, Serialize)]
pub struct ShardReport {
    pub shard_id: usize,
    pub presence: ShardPresence,
    /// Whether this shard's WAL carried a committed-root marker (M1).
    pub wal_committed_root: bool,
    pub nodes: NodeTotals,
    /// `.node-*.tmp` install debris: reported, never treated as a node,
    /// never deleted by sweep v1 (§4).
    pub temp_debris_files: usize,
    pub temp_debris_bytes: u64,
    pub malformed: Vec<MalformedEntry>,
}

/// How a consulted metadata source entered the run (§3).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MetadataOrigin {
    /// One of the blessed in-dir locations (`branches/`, `snapshots.hsr`).
    Canonical,
    /// Explicitly supplied via `VacuumOptions`.
    Supplied,
    /// Listed by the durable metadata manifest.
    Manifest,
}

/// What kind of metadata source was consulted.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MetadataSourceKind {
    RefsDir,
    SnapshotRegistry,
}

/// One consulted metadata source: the report records exactly which sources
/// were consulted (§3) — an operator who knows a refs dir exists and does not
/// see it here stops before sweeping.
#[derive(Debug, Serialize)]
pub struct MetadataSourceReport {
    pub path: PathBuf,
    pub kind: MetadataSourceKind,
    pub origin: MetadataOrigin,
    /// Decoded records (HBR1 branch records, or HSR1 snapshot entries).
    pub records: usize,
    /// `.branch-*.tmp` debris counted, never unlinked (§7 ⟨r2, M2⟩).
    pub temp_debris_files: usize,
    /// Entries the production reader would silently ignore (non-`.ref`
    /// files, non-UTF8 names); counted so growth there is visible.
    pub foreign_entries: usize,
}

/// One decoded manifest entry, reported with its durable state (§3 ⟨r7⟩:
/// stats reports each entry's state and whatever records are visible).
#[derive(Debug, Serialize)]
pub struct ManifestEntryReport {
    pub path: PathBuf,
    pub kind: MetadataSourceKind,
    pub state: String,
    /// Whether the listed path currently exists and is readable.
    pub source_readable: bool,
}

/// The manifest half of the metadata report.
#[derive(Debug, Serialize)]
pub struct ManifestReport {
    pub path: PathBuf,
    pub generation: u64,
    pub entries: Vec<ManifestEntryReport>,
}

/// The metadata picture: how the universe was (or was not) established.
#[derive(Debug, Serialize)]
pub struct MetadataReport {
    /// Present iff `<data_dir>/metadata-manifest.json` exists and decodes.
    pub manifest: Option<ManifestReport>,
    /// The `attest_metadata_complete` flag as supplied (ignored by stats,
    /// reported so the operator sees what the invocation claimed).
    pub attestation: bool,
    pub sources: Vec<MetadataSourceReport>,
    /// Explicitly supplied paths that do not exist — a sweep refusal
    /// (`MetadataPathMissing`), reported by stats ⟨r4, B4⟩.
    pub supplied_missing: Vec<PathBuf>,
}

/// Root tallies per mark source (§2, M1–M3), so the report shows where every
/// pin came from.
#[derive(Debug, Clone, Copy, Default, Serialize)]
pub struct MarkTallies {
    /// M1: WAL committed-root markers found.
    pub wal_roots: usize,
    /// M2: HBR1 fork anchors walked.
    pub branch_anchors: usize,
    /// M2: HBR1 committed heads walked.
    pub branch_heads: usize,
    /// M3: named snapshot roots walked.
    pub snapshot_roots: usize,
}

/// Filesystem entry kind for the top-level inventory.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EntryKind {
    File,
    Directory,
    Symlink,
    Other,
}

/// ⟨r15, C1⟩ a top-level `data_dir` entry the model does not account for:
/// enumerated by name, kind, and size — never assumed.
#[derive(Debug, Serialize)]
pub struct UninventoriedEntry {
    pub name: String,
    pub kind: EntryKind,
    /// Recursive byte size; `None` when unreadable (the entry is still
    /// enumerated — an unreadable foreign entry must not hide).
    pub size_bytes: Option<u64>,
}

/// A condition observed by stats that would refuse `vacuum_sweep`. Stats
/// completes and reports these; sweep will refuse on them (§3, §4).
#[derive(Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SweepBlocker {
    /// ⟨r15, C1⟩ a `shard-{id}` directory with `id >= shard_count`.
    ShardBeyondCount { id: usize, shard_count: usize },
    /// §4 malformed entries in a shard store: that store's sweep refuses.
    MalformedStoreEntries { shard_id: usize, count: usize },
    /// §3: no manifest and no attestation — the metadata universe is not
    /// positively established.
    MetadataUnattested,
    /// ⟨r4⟩ stale manifest: a canonical source exists that the manifest
    /// omits.
    StaleManifest { unlisted: PathBuf },
    /// ⟨r7⟩ a manifest entry in a non-terminal state.
    ManifestEntryUnsettled { path: PathBuf, state: String },
    /// ⟨r5, B5⟩ a manifest-listed source that is absent.
    ManifestSourceMissing { path: PathBuf },
    /// §3: a source that exists but could not be READ (permissions, mount
    /// access). Stats reports it and completes; sweep refuses. Distinct
    /// from codec corruption, which fails stats loud — a record that was
    /// read but does not decode is a dropped pin, not an access problem.
    SourceUnreadable { path: PathBuf, detail: String },
    /// ⟨r4, B4⟩ an explicitly supplied metadata path that does not exist.
    SuppliedPathMissing { path: PathBuf },
    /// ⟨r9⟩⟨r10⟩ this platform provides no real directory-entry durability
    /// barrier; every sweep mode refuses.
    UnsupportedDurability,
}

/// The vacuum report (§7).
#[derive(Debug, Serialize)]
pub struct VacuumReport {
    pub mode: VacuumMode,
    pub data_dir: PathBuf,
    pub shard_count: usize,
    /// Wall-clock duration of the run in milliseconds (lens Q4: the
    /// deliberate cost is offline downtime, so the run measures itself).
    pub duration_ms: u64,
    /// ⟨r4, M5⟩ the single named report-only write exception: true when this
    /// run created the advisory `writer.lock` anchor on a legacy dir that
    /// lacked one. When the anchor already existed, the run made no
    /// filesystem change at all.
    pub lock_anchor_created: bool,
    /// Shards whose `shard-{id}` entry is wholly absent (lazy, §2 M1
    /// ⟨r2, B3⟩) and which no mark source references: AGGREGATED as a count
    /// so the report stays `O(materialised shards + findings)`, never
    /// `O(shard_count)` — a huge persisted count must not inflate the report
    /// itself (Sol r4 follow-through).
    pub never_materialised_shards: usize,
    /// Per-shard detail for every MATERIALISED shard.
    pub shards: Vec<ShardReport>,
    pub totals: NodeTotals,
    pub metadata: MetadataReport,
    pub marks: MarkTallies,
    /// ⟨r2, B2⟩ content-hash verifications performed by the walk: every
    /// traversed node's hash was recomputed from its canonical serialised
    /// bytes before its children were trusted.
    pub verified_nodes: usize,
    /// ⟨r15, C1⟩ top-level `data_dir` entries not accounted for by the model.
    pub uninventoried: Vec<UninventoriedEntry>,
    /// Conditions that would refuse `vacuum_sweep`, observed by this run.
    pub sweep_blockers: Vec<SweepBlocker>,
    /// §6: the trust boundary named in output — what the lock does not
    /// exclude and the tool cannot see.
    pub trust_boundary: String,
}