BREP_kernel 0.5.0

A boundary representation (BREP) geometry kernel for building CAD applications.
Documentation
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum DiagnosticSeverity {
    Info,
    Warning,
    Degraded,
    Error,
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum KernelStage {
    Collect,
    Classify,
    Intersect,
    Refine,
    Fragment,
    Select,
    Sew,
    Validate,
    Export,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DiagnosticEvent {
    pub severity: DiagnosticSeverity,
    pub stage: KernelStage,
    /// Stable machine-readable identifier suitable for tests and UI routing.
    pub code: String,
    pub message: String,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct KernelDiagnostics {
    pub events: Vec<DiagnosticEvent>,
    pub counters: BTreeMap<String, u64>,
    pub measurements: BTreeMap<String, f64>,
}

impl KernelDiagnostics {
    pub fn event(
        &mut self,
        severity: DiagnosticSeverity,
        stage: KernelStage,
        code: impl Into<String>,
        message: impl Into<String>,
    ) {
        self.events.push(DiagnosticEvent {
            severity,
            stage,
            code: code.into(),
            message: message.into(),
        });
    }

    pub fn count(&mut self, code: impl Into<String>) {
        *self.counters.entry(code.into()).or_default() += 1;
    }

    pub fn count_n(&mut self, code: impl Into<String>, amount: u64) {
        *self.counters.entry(code.into()).or_default() += amount;
    }

    pub fn measure_max(&mut self, code: impl Into<String>, value: f64) {
        let entry = self.measurements.entry(code.into()).or_insert(value);
        *entry = entry.max(value);
    }

    pub fn worst(&self) -> Option<DiagnosticSeverity> {
        self.events.iter().map(|event| event.severity).max()
    }

    pub fn shippable(&self) -> bool {
        self.events
            .iter()
            .all(|event| event.severity < DiagnosticSeverity::Error)
    }
}

/// A kernel REFUSAL: the fail-safe contract's carrier. The kernel never
/// degrades a result; where it cannot certify one it refuses, and this is the
/// refusal — a closed [`RefusalClass`] that consumers dispatch on (the boolean's
/// perturbation-retry gate, the CI baseline's per-class diff), the stage that
/// minted it, and the human text, which `Display` prints verbatim so every
/// message a user or a test saw before typing is unchanged.
///
/// There is deliberately no `From<String>` for this type: a refusal is minted
/// at its origin with a class, never re-derived from prose. The only string
/// conversion is the lossy exit (`From<KernelRefusal> for String`) that
/// stringly callers above the boolean stack still use; those callers are
/// retired stack by stack (offset/fillet/heal, then the feature pipeline).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct KernelRefusal {
    #[serde(flatten)]
    pub class: RefusalClass,
    pub stage: KernelStage,
    /// The human text, verbatim. `Display` prints exactly this.
    pub message: String,
}

/// The closed set of refusal classes. Dispatchers match on it exhaustively;
/// adding a variant is a deliberate taxonomy change, never a message edit.
///
/// The first five are ARRANGEMENT DEGENERACIES — a coincident or near-tangent
/// carrier pair made the exact arrangement structurally inconsistent — and are
/// the only classes the boolean's Simulation-of-Simplicity retry may act on
/// ([`RefusalClass::perturbation_eligible`]). The rest are honest refusals that
/// a perturbation cannot and must not "fix".
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "class", rename_all = "snake_case")]
pub enum RefusalClass {
    /// The assembled shell has open edges / invalid topology (the arrangement
    /// left a boundary unclosed).
    DegenerateArrangement { open_edges: u32, issues: u32 },
    /// Euler characteristic did not yield an integral genus.
    NonIntegralGenus { shells: u32, euler: i64 },
    /// The assembled solid's signed volume is not positive.
    NonPositiveVolume,
    /// A singular / tangent-node surface intersection the marcher declines.
    TangentNodeSingularity,
    /// The FINAL validation of the result found topology issues.
    InvalidResultTopology { issues: u32 },
    /// The operation produced no boundary faces while contact / graze
    /// evidence exists — refusing rather than blessing an empty result.
    ConservativeEmptyOverlap,
    /// An iterative lane (edge conformance, an SSI march, a Newton polish) did
    /// not converge within its budget.
    NonConvergence { what: String },
    /// A named deferral: geometry the kernel does not model yet.
    UnsupportedGeometry { what: String },
    /// A caller error: bad ids, non-finite parameters, an unusable policy.
    InvalidInput { what: String },
    /// The long tail — an internal consistency check tripped. Curated
    /// batteries assert zero of these among their expected refusals.
    Internal { what: String },
}

impl RefusalClass {
    /// Whether the boolean's perturbation retry may act on this refusal. The
    /// set is pinned by `refusal_retry_parity` against the former substring
    /// matcher; widening or narrowing it is a deliberate change.
    pub fn perturbation_eligible(&self) -> bool {
        matches!(
            self,
            Self::DegenerateArrangement { .. }
                | Self::NonIntegralGenus { .. }
                | Self::NonPositiveVolume
                | Self::TangentNodeSingularity
                | Self::InvalidResultTopology { .. }
        )
    }

    /// The snake_case tag serde emits — the name CI baselines and logs use.
    pub fn tag(&self) -> &'static str {
        match self {
            Self::DegenerateArrangement { .. } => "degenerate_arrangement",
            Self::NonIntegralGenus { .. } => "non_integral_genus",
            Self::NonPositiveVolume => "non_positive_volume",
            Self::TangentNodeSingularity => "tangent_node_singularity",
            Self::InvalidResultTopology { .. } => "invalid_result_topology",
            Self::ConservativeEmptyOverlap => "conservative_empty_overlap",
            Self::NonConvergence { .. } => "non_convergence",
            Self::UnsupportedGeometry { .. } => "unsupported_geometry",
            Self::InvalidInput { .. } => "invalid_input",
            Self::Internal { .. } => "internal",
        }
    }
}

impl KernelRefusal {
    pub fn new(class: RefusalClass, stage: KernelStage, message: impl Into<String>) -> Self {
        Self {
            class,
            stage,
            message: message.into(),
        }
    }

    /// An internal consistency failure. `what` is a short stable slug (the
    /// check that tripped), the message the full text.
    pub fn internal(
        stage: KernelStage,
        what: impl Into<String>,
        message: impl Into<String>,
    ) -> Self {
        Self::new(RefusalClass::Internal { what: what.into() }, stage, message)
    }

    /// A caller error.
    pub fn input(stage: KernelStage, what: impl Into<String>, message: impl Into<String>) -> Self {
        Self::new(
            RefusalClass::InvalidInput { what: what.into() },
            stage,
            message,
        )
    }

    /// A named deferral.
    pub fn unsupported(
        stage: KernelStage,
        what: impl Into<String>,
        message: impl Into<String>,
    ) -> Self {
        Self::new(
            RefusalClass::UnsupportedGeometry { what: what.into() },
            stage,
            message,
        )
    }

    /// An iterative lane that ran out of budget.
    pub fn non_convergence(
        stage: KernelStage,
        what: impl Into<String>,
        message: impl Into<String>,
    ) -> Self {
        Self::new(
            RefusalClass::NonConvergence { what: what.into() },
            stage,
            message,
        )
    }

    /// The same refusal with its message rewritten by `f` — the way a wrapping
    /// site adds context ("assembly failed: …") WITHOUT changing the class, so
    /// an inner degeneracy stays retry-eligible through every wrapper exactly
    /// as the substring matcher saw it in the wrapped text.
    pub fn with_message(mut self, f: impl FnOnce(&str) -> String) -> Self {
        self.message = f(&self.message);
        self
    }
}

impl std::fmt::Display for KernelRefusal {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.message)
    }
}

impl std::error::Error for KernelRefusal {}

/// The lossy exit for stringly callers: the class is dropped, the text kept.
/// Legal only ABOVE the typed stacks (never inside `csg/` or the healing
/// modules that mint refusals); each stack that adopts `KernelRefusal`
/// removes its uses.
impl From<KernelRefusal> for String {
    fn from(refusal: KernelRefusal) -> Self {
        refusal.message
    }
}

/// Mechanical conversion of a stringly LOWER-layer error consumed inside a
/// typed stack (geometry evaluation, tolerance policy checks): the text is
/// kept and the refusal is classed `Internal` under `what` at `stage`. This is
/// a constructor at the consuming site, not a blanket `From`.
pub trait OrRefuse<T> {
    fn or_refuse(self, stage: KernelStage, what: &'static str) -> Result<T, KernelRefusal>;
    fn or_input(self, stage: KernelStage, what: &'static str) -> Result<T, KernelRefusal>;
}

impl<T> OrRefuse<T> for Result<T, String> {
    fn or_refuse(self, stage: KernelStage, what: &'static str) -> Result<T, KernelRefusal> {
        self.map_err(|message| KernelRefusal::internal(stage, what, message))
    }

    fn or_input(self, stage: KernelStage, what: &'static str) -> Result<T, KernelRefusal> {
        self.map_err(|message| KernelRefusal::input(stage, what, message))
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct KernelOutcome<T> {
    pub value: T,
    pub diagnostics: KernelDiagnostics,
}

// BREP private tests: 05fb87b52e128ef2