polyc-eventlog-host 2026.9.0

Durable turn-persistence host: a dedicated-thread Commonware event-log bridge for the tokio control plane (#459).
//! Prometheus counter for the `#799` signed-root step's outcome.
//!
//! Registered into the process default registry — the same pattern
//! `polyc-agent`'s `metrics.rs` and `polyc-llm`'s follow: no separate scrape
//! endpoint and no registry plumbing.
//!
//! # Why this counter exists
//!
//! A batch can end up durable and unattested, and this counts how often.
//!
//! `append_batch_one` prepares the signed root BEFORE its first append, so a
//! preparation failure is definite: nothing of the batch is durable, the
//! caches are evicted, and the caller sees a plain refusal. That path is
//! counted as `rebuild_failed` or `extend_sign_failed`, and it counts a
//! refused attempt rather than durable content — a caller retrying the same
//! logical commit counts again.
//!
//! The marker append is different. It runs after every event is written, so a
//! failure there leaves content the best-effort commit may well have made
//! durable, with no root covering it. That is `marker_append_failed`.
//!
//! Nothing measured how often that happens. A partition holding no signed-root
//! marker verifies trivially — the forensics report says `NoMarkers`, not a
//! failure — so an unattested tail passes every check the journal ships. The
//! journal-format amendment records this as its risk 9, and states that
//! widening the leaf does not close it: a widened leaf attests up to the last
//! signed root, never past it.
//!
//! Deciding what to do about that needs the rate first. This counter is that
//! measurement, and it is deliberately independent of the amendment.
//!
//! # Reading it
//!
//! Both halves are counted, so a rate is a division at query time rather than
//! a pre-computed ratio here.
//!
//! Read the result precisely: it is the share of recorded batch OUTCOMES that
//! are not `signed`. It is NOT the share of durable content left unattested,
//! and the difference matters. A partial batch that fails and is retried
//! records `unattested_partial_batch` and then `signed`, so one logical commit
//! contributes two outcomes and reads as 50% — while the content it left
//! behind was attested by that very retry.
//!
//! ```promql
//! sum(rate(polychrome_eventlog_attestation_total{outcome!="signed"}[1h]))
//!   / sum(rate(polychrome_eventlog_attestation_total[1h]))
//! ```
//!
//! Zero for every failure label is the expected reading, and [`force`] is what
//! makes that zero visible rather than absent.
//!
//! This module is the INSTRUMENT for the amendment's risk 9, not the answer to
//! it. No overlay in this repository composes the scrape config, so nothing
//! reads these series by default. Risk 9 stays open until something does.
//!
//! A nonzero reading names a stage. Read the variants for what each covers —
//! the split is by where the failure surfaces, and a cold-start tree rebuild
//! surfaces under extend/sign rather than under rebuild.
//!
//! # What this does NOT measure
//!
//! This is not a census of unattested content. It counts the outcome of one
//! step on one path, and several other paths leave content unattested without
//! ever reaching that step:
//!
//! - A migration destination. `migrate_append_into` appends without signing,
//!   and evicts the cached tree so the next batch re-roots. Mechanically the
//!   same transient window `unattested_partial_batch` counts, and it is not
//!   counted here.
//! - A restore after a rewrite. `append_repair_copy` commits best-effort on a
//!   per-event failure and returns, so a prefix of the restored content can
//!   land with no trailing marker — onto a partition whose original content
//!   was already emptied. That one does not self-heal the same way.
//! - A batch whose signing SUCCEEDED and whose final commit failed. Nothing is
//!   recorded: `signed` is held until the commit lands, so a batch that never
//!   landed is never counted as attested.
//! - A partial batch whose best-effort commit ALSO failed. Earlier appends may
//!   still have reached the blob, and nothing is recorded.

use std::sync::OnceLock;

use prometheus::{IntCounterVec, register_int_counter_vec};

/// What the signed-root step did for one batch.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AttestationOutcome {
    /// The root was signed and its marker appended. The batch is attested.
    Signed,
    /// Rebuilding the cold-start tree failed, so nothing could be extended.
    RebuildFailed,
    /// Extending the tree or signing the root failed.
    ExtendSignFailed,
    /// The root was signed and appending its marker failed.
    MarkerAppendFailed,
    /// A batch failed partway, at least one event landed, and the commit of
    /// that prefix succeeded. The signing step never ran.
    ///
    /// It is not a signing failure — signing was never reached — so it carries
    /// its own label.
    ///
    /// **This window usually closes on its own.** The same path evicts the
    /// cached tree, so the next successful batch on that partition rebuilds
    /// from a full replay, which includes this prefix, and signs a root over
    /// it. In the ordinary case that is the caller's own retry. So a nonzero
    /// reading here counts how often the window OPENS, not how much content
    /// stays unattested.
    UnattestedPartialBatch,
}

impl AttestationOutcome {
    /// The metric label for this outcome.
    const fn label(self) -> &'static str {
        match self {
            Self::Signed => "signed",
            Self::RebuildFailed => "rebuild_failed",
            Self::ExtendSignFailed => "extend_sign_failed",
            Self::MarkerAppendFailed => "marker_append_failed",
            Self::UnattestedPartialBatch => "unattested_partial_batch",
        }
    }
}

/// Force-register [`attestations`] with every `outcome` label pre-created
/// (zero-valued), so each appears in a `/metrics` scrape before any batch
/// commits. See [`crate::init_metrics`].
///
/// An `IntCounterVec` produces NO scrape output for a label value that has
/// never been touched — registering the vec alone is not enough.
/// `with_label_values` creates the zero-valued child without incrementing it,
/// which is what makes it appear.
///
/// This matters more here than for most metrics. The expected healthy reading
/// is zero for every failure label, so without this the numerator of the query
/// above selects nothing, the division returns an EMPTY vector, and the panel
/// reads "no data" — indistinguishable from a build where the counter was
/// never wired. The reading this exists to produce is exactly the one that
/// would break.
pub(crate) fn force() {
    for outcome in [
        AttestationOutcome::Signed,
        AttestationOutcome::RebuildFailed,
        AttestationOutcome::ExtendSignFailed,
        AttestationOutcome::MarkerAppendFailed,
        AttestationOutcome::UnattestedPartialBatch,
    ] {
        attestations().with_label_values(&[outcome.label()]);
    }
}

/// Signed-root outcomes, labeled `outcome`.
fn attestations() -> &'static IntCounterVec {
    static V: OnceLock<IntCounterVec> = OnceLock::new();
    V.get_or_init(|| {
        register_int_counter_vec!(
            "polychrome_eventlog_attestation_total",
            "Signed-root outcomes for committed journal batches, by outcome. \
             A batch that does not reach `signed` is durable and unattested, \
             and a partition holding no marker verifies trivially.",
            &["outcome"]
        )
        .expect("the attestation counter registers once into the default registry")
    })
}

/// Count one batch's signed-root outcome.
///
/// Call this only AFTER the batch's commit succeeds. The signing step runs
/// before that commit, so counting at the signing site would report a batch
/// that never landed — and would count again on a caller's retry of the same
/// logical commit. Both bias the failure ratio downward, which is the one
/// direction that matters here.
///
/// Never fails and never blocks: a counter increment takes a read lock on the
/// vec's children map and nothing the write path itself needs.
pub(crate) fn record_attestation(outcome: AttestationOutcome) {
    attestations().with_label_values(&[outcome.label()]).inc();
}

/// Read one outcome's current count.
///
/// Exists so a test can prove the counter is WIRED to the append path, not
/// merely that it counts when called directly. A metric nothing increments is
/// the same defect as a guard nothing reaches.
#[cfg(test)]
pub(crate) fn attestation_count(outcome: AttestationOutcome) -> u64 {
    attestations().with_label_values(&[outcome.label()]).get()
}

#[cfg(test)]
mod tests {
    #![allow(clippy::pedantic, clippy::nursery, missing_docs)]
    use super::*;

    /// Each outcome carries its own label, so a failure names its stage.
    ///
    /// The failures have different causes — storage read, tree/sign,
    /// storage write — and collapsing them would hide which one fires.
    #[test]
    fn every_outcome_has_a_distinct_label() {
        let labels = [
            AttestationOutcome::Signed.label(),
            AttestationOutcome::RebuildFailed.label(),
            AttestationOutcome::ExtendSignFailed.label(),
            AttestationOutcome::MarkerAppendFailed.label(),
            AttestationOutcome::UnattestedPartialBatch.label(),
        ];
        let mut seen = labels.to_vec();
        seen.sort_unstable();
        seen.dedup();
        assert_eq!(seen.len(), labels.len(), "two outcomes share a label");
    }

    /// Every outcome appears in a scrape BEFORE anything increments it.
    ///
    /// This is the assertion the counter needs most, and the one it originally
    /// lacked. An `IntCounterVec` emits no line for a label value nothing has
    /// touched, and the expected healthy reading here is zero for every
    /// failure label — so without [`force`] the unattested-share query selects
    /// nothing, divides an empty vector, and renders "no data". That reads
    /// exactly like a build where the counter was never wired, which is the
    /// failure this whole module exists to prevent.
    #[test]
    fn every_outcome_is_in_the_scrape_before_anything_happens() {
        force();

        let mut buf = Vec::new();
        prometheus::Encoder::encode(
            &prometheus::TextEncoder::new(),
            &prometheus::default_registry().gather(),
            &mut buf,
        )
        .expect("encode the default registry");
        let text = String::from_utf8(buf).expect("the exposition format is utf-8");

        assert!(
            text.contains("polychrome_eventlog_attestation_total"),
            "the counter is absent from the scrape:\n{text}"
        );
        for outcome in [
            "signed",
            "rebuild_failed",
            "extend_sign_failed",
            "marker_append_failed",
            "unattested_partial_batch",
        ] {
            assert!(
                text.contains(&format!("outcome=\"{outcome}\"")),
                "outcome={outcome} has no series, so a query for it reads as \
                 no-data rather than zero:\n{text}"
            );
        }
    }

    /// Counting is observable, which is what the amendment's risk 9 asks for.
    #[test]
    fn recording_an_outcome_advances_its_own_counter_only() {
        let before_signed = attestations().with_label_values(&["signed"]).get();
        let before_failed = attestations()
            .with_label_values(&["extend_sign_failed"])
            .get();

        record_attestation(AttestationOutcome::Signed);

        assert_eq!(
            attestations().with_label_values(&["signed"]).get(),
            before_signed + 1,
            "the signed outcome counts"
        );
        assert_eq!(
            attestations()
                .with_label_values(&["extend_sign_failed"])
                .get(),
            before_failed,
            "counting one outcome must not move another"
        );
    }
}