use std::collections::{BTreeMap, BTreeSet};

use bone_kernel::{
    Arc3, BrepError, BrepSolid, Circle3, ContourSelection, Curve3Kind, DatumError, DatumFeature,
    DatumGap, DatumGeometry, EdgeCurve3, ExtrudeFeature, ExtrudeOperation, ExtrudeScene,
    FeatureScope, Line3, MergeResult, ReferenceGeometry, RevolveAxis, RevolveFeature, RevolveScene,
    SweepAlignment, SweepFeature, SweepPath, SweepProfileKind, SweepRails, evaluate_extrude_plan,
    evaluate_revolve, evaluate_sweep, next_face_distance, resolve_revolve, resolve_sweep,
};
use bone_types::{
    Aabb3, Angle, Axis3, BodyId, BodyOrdinal, BuildFailure, DatumTarget, EdgeLabel, EntityRef,
    FaceLabel, FaceRef, FeatureId, GeometryGeneration, Plane3, Point3, RebuildError, RebuildStatus,
    RollbackMarker, Sketch3EntityRef, SketchEntityId, SketchPlaneBasis, Tolerance, VertexLabel,
    radian,
};

use crate::document::{Document, FeatureNode, FeatureTree, PrincipalPlane, SketchPlaneRef};
use crate::evaluator::{
    EvaluatedExtrude, EvaluatedFeature, EvaluatedSketch, ExtrudeError, cut_from, evaluate_sketch,
    intersect_from, merge_into,
};
use crate::matcher::unique_face;
use crate::profile::{profile_from_sketch3, profiles_from_sketch3_regions};
use crate::sketch3::{Arc3Data, Sketch3, Sketch3Entity};

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RebuildPass(u64);

impl RebuildPass {
    #[must_use]
    const fn succ(self) -> Self {
        Self(self.0.saturating_add(1))
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum RecomputeScope {
    Full,
    Edited(FeatureId),
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RebuildCost(usize);

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RebuildBudget(usize);

impl RebuildBudget {
    pub const INTERACTIVE: Self = Self(2048);

    #[must_use]
    pub const fn new(limit: usize) -> Self {
        Self(limit)
    }
}

impl RebuildCost {
    #[must_use]
    pub const fn new(weight: usize) -> Self {
        Self(weight)
    }

    #[must_use]
    pub const fn within(self, budget: RebuildBudget) -> bool {
        self.0 <= budget.0
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum SkipState {
    Active,
    Suppressed,
    RolledBack,
}

type Consumed = BTreeSet<(FeatureId, BodyOrdinal)>;

#[derive(Clone)]
enum NodeOutput {
    Datum(Result<DatumGeometry, DatumError>),
    Sketch(EvaluatedSketch),
    Extrude(EvaluatedFeature),
    Imported(Box<BrepSolid>),
    Unbuilt,
}

#[derive(Clone, PartialEq)]
enum ParentDigest {
    Datum(Option<DatumGeometry>),
    Sketch(EvaluatedSketch),
    Geometry(GeometryGeneration),
    Absent,
}

#[derive(Clone, PartialEq)]
enum PayloadKey {
    Datum(Option<DatumFeature>),
    Sketch(Sketch3, Option<SketchPlaneRef>),
    Extrude(Option<ExtrudeFeature>),
    Revolve(Option<RevolveFeature>),
    Sweep(Option<SweepFeature>),
    Imported(BodyId),
}

#[derive(Clone, PartialEq)]
struct NodeFingerprint {
    payload: PayloadKey,
    parents: Vec<(FeatureId, ParentDigest)>,
}

struct NodeMemo {
    fingerprint: NodeFingerprint,
    output: NodeOutput,
    digest: ParentDigest,
    skip: SkipState,
    status: RebuildStatus,
    built_at: RebuildPass,
    consumed: Consumed,
}

type Memo = BTreeMap<FeatureId, NodeMemo>;

#[derive(Default)]
pub struct EvaluatedModel {
    memo: Memo,
    order: Vec<FeatureId>,
    pass: RebuildPass,
}

impl EvaluatedModel {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    pub fn recompute(
        &mut self,
        document: &Document,
        suppressed: &BTreeSet<FeatureId>,
        rollback: RollbackMarker,
        scope: RecomputeScope,
    ) {
        let tree = document.feature_tree();
        debug_assert!(
            tree.find_cycle().is_none(),
            "recompute requires an acyclic feature graph",
        );
        debug_assert!(
            tree.order_violation().is_none(),
            "recompute requires every parent to precede its child in feature order",
        );
        let live: BTreeSet<FeatureId> = tree.iter().map(|(id, _)| id).collect();
        let rolled = tree.rolled_back(rollback);
        let suppressed = closed_over_descendants(tree, suppressed);
        let pass = self.pass.succ();

        let retained: Memo = core::mem::take(&mut self.memo)
            .into_iter()
            .filter(|(id, _)| live.contains(id))
            .collect();
        let order = tree.topo_order();
        let seeded = dirty_set(tree, &live, &retained, &suppressed, &rolled, scope);
        let dirty = propagate_all_bodies(document, tree, &order, seeded);

        self.memo = order.iter().fold(retained, |mut memo, &id| {
            if !dirty.contains(&id) {
                return memo;
            }
            let Some(node) = tree.node(id) else {
                return memo;
            };
            let fingerprint = NodeFingerprint {
                payload: payload_key(document, node),
                parents: node_dependencies(document, tree, &order, id, node, &memo),
            };
            let skip = skip_state(id, &suppressed, &rolled);
            let unchanged = memo
                .get(&id)
                .is_some_and(|entry| entry.fingerprint == fingerprint && entry.skip == skip);
            if !unchanged {
                let entry = build_entry(document, id, node, &memo, fingerprint, skip, pass);
                memo.insert(id, entry);
            }
            memo
        });
        self.order = order;
        self.pass = pass;
    }

    #[must_use]
    pub fn status(&self, id: FeatureId) -> Option<RebuildStatus> {
        self.memo.get(&id).map(|entry| entry.status)
    }

    #[must_use]
    pub fn extrude_error(&self, id: FeatureId) -> Option<ExtrudeError> {
        match &self.memo.get(&id)?.output {
            NodeOutput::Extrude(feature) => feature.error().cloned(),
            NodeOutput::Datum(_)
            | NodeOutput::Sketch(_)
            | NodeOutput::Imported(_)
            | NodeOutput::Unbuilt => None,
        }
    }

    #[must_use]
    pub fn body(&self, id: FeatureId) -> Option<&BrepSolid> {
        self.body_at(id, BodyOrdinal::origin())
    }

    #[must_use]
    pub fn body_at(&self, id: FeatureId, ordinal: BodyOrdinal) -> Option<&BrepSolid> {
        self.memo
            .get(&id)
            .and_then(|entry| output_solids(&entry.output).get(usize::from(ordinal.value())))
    }

    #[must_use]
    pub fn datum(&self, id: FeatureId) -> Option<DatumGeometry> {
        self.memo
            .get(&id)
            .and_then(|entry| output_datum(&entry.output))
    }

    #[must_use]
    pub fn datum_preview(&self, feature: &DatumFeature) -> Option<DatumGeometry> {
        evaluate_datum(feature, &self.memo).ok()
    }

    #[must_use]
    pub fn sweep_preview(
        &self,
        document: &Document,
        feature: &SweepFeature,
    ) -> Option<EvaluatedExtrude> {
        let tree = document.feature_tree();
        let profile = sketch_profile(tree, feature.profile, &self.memo)?;
        let path = sweep_path_curves(feature.path, tree, &self.memo)?;
        let guides = feature
            .guides
            .iter()
            .map(|guide| resolve_guide(*guide, tree, &self.memo))
            .collect::<Option<Vec<_>>>()?;
        let bases: Vec<Base<'_>> = self.bodies().collect();
        Some(build_sweep_tool(
            FeatureId::default(),
            profile,
            &path,
            &guides,
            feature,
            &bases,
            &self.memo,
        ))
    }

    #[must_use]
    pub fn evaluated_sketch(&self, id: FeatureId) -> Option<&EvaluatedSketch> {
        match &self.memo.get(&id)?.output {
            NodeOutput::Sketch(sketch) => Some(sketch),
            NodeOutput::Datum(_)
            | NodeOutput::Extrude(_)
            | NodeOutput::Imported(_)
            | NodeOutput::Unbuilt => None,
        }
    }

    #[must_use]
    pub fn evaluated_sketch3(&self, id: FeatureId) -> Option<&Sketch3> {
        match self.evaluated_sketch(id)? {
            EvaluatedSketch::Solved(sketch) => Some(sketch),
            EvaluatedSketch::Failed(_) | EvaluatedSketch::PlaneUnresolved(_) => None,
        }
    }

    pub fn bodies(&self) -> impl Iterator<Item = (FeatureId, BodyOrdinal, &BrepSolid)> + '_ {
        let consumed = self.consumed_bodies();
        let live: Vec<(FeatureId, BodyOrdinal)> = self
            .order
            .iter()
            .flat_map(|&id| {
                let count = self
                    .memo
                    .get(&id)
                    .map_or(0, |entry| output_solids(&entry.output).len());
                (0..count).map(move |index| {
                    (
                        id,
                        BodyOrdinal::new(u16::try_from(index).unwrap_or(u16::MAX)),
                    )
                })
            })
            .filter(|pair| !consumed.contains(pair))
            .collect();
        live.into_iter().filter_map(move |(id, ordinal)| {
            self.body_at(id, ordinal).map(|solid| (id, ordinal, solid))
        })
    }

    fn consumed_bodies(&self) -> Consumed {
        self.memo
            .values()
            .flat_map(|entry| entry.consumed.iter().copied())
            .collect()
    }

    #[must_use]
    pub fn built_at(&self, id: FeatureId) -> Option<RebuildPass> {
        self.memo.get(&id).map(|entry| entry.built_at)
    }

    #[must_use]
    pub fn rebuild_cost(&self) -> RebuildCost {
        RebuildCost(
            self.bodies()
                .map(|(_, _, solid)| solid.iter_faces().count() + solid.iter_edges().count())
                .sum(),
        )
    }
}

fn closed_over_descendants(tree: &FeatureTree, seed: &BTreeSet<FeatureId>) -> BTreeSet<FeatureId> {
    seed.iter()
        .flat_map(|&feature| tree.descendants(feature))
        .chain(seed.iter().copied())
        .collect()
}

fn dirty_set(
    tree: &FeatureTree,
    live: &BTreeSet<FeatureId>,
    retained: &Memo,
    suppressed: &BTreeSet<FeatureId>,
    rolled: &BTreeSet<FeatureId>,
    scope: RecomputeScope,
) -> BTreeSet<FeatureId> {
    let scoped = match scope {
        RecomputeScope::Full => live.clone(),
        RecomputeScope::Edited(root) => subtree(tree, root),
    };
    let appeared = live.iter().copied().filter(|id| !retained.contains_key(id));
    let reskinned = retained
        .iter()
        .filter(|(id, entry)| entry.skip != skip_state(**id, suppressed, rolled))
        .flat_map(|(id, _)| subtree(tree, *id));
    appeared
        .chain(reskinned)
        .filter(|id| live.contains(id))
        .fold(scoped, |mut acc, id| {
            acc.insert(id);
            acc
        })
}

fn propagate_all_bodies(
    document: &Document,
    tree: &FeatureTree,
    order: &[FeatureId],
    seeded: BTreeSet<FeatureId>,
) -> BTreeSet<FeatureId> {
    order
        .iter()
        .fold((seeded, false), |(mut dirty, prior_dirty), &id| {
            let depends_on_prior = prior_dirty
                && tree.node(id).and_then(|node| feature_scope(document, node))
                    == Some(FeatureScope::AllBodies);
            if depends_on_prior {
                dirty.extend(subtree(tree, id));
            }
            let now_dirty = prior_dirty || dirty.contains(&id);
            (dirty, now_dirty)
        })
        .0
}

fn feature_scope(document: &Document, node: FeatureNode) -> Option<FeatureScope> {
    match node {
        FeatureNode::Extrude(extrude) => document.extrude(extrude).map(|feature| feature.scope),
        FeatureNode::Revolve(revolve) => document.revolve(revolve).map(|feature| feature.scope),
        FeatureNode::Sweep(sweep) => document.sweep(sweep).map(|feature| feature.scope),
        FeatureNode::Origin
        | FeatureNode::PrincipalPlane(_)
        | FeatureNode::Sketch(_)
        | FeatureNode::Datum(_)
        | FeatureNode::ImportedBody(_) => None,
    }
}

fn subtree(tree: &FeatureTree, root: FeatureId) -> BTreeSet<FeatureId> {
    tree.descendants(root).into_iter().chain([root]).collect()
}

fn skip_state(
    id: FeatureId,
    suppressed: &BTreeSet<FeatureId>,
    rolled: &BTreeSet<FeatureId>,
) -> SkipState {
    match (rolled.contains(&id), suppressed.contains(&id)) {
        (true, _) => SkipState::RolledBack,
        (false, true) => SkipState::Suppressed,
        (false, false) => SkipState::Active,
    }
}

fn payload_key(document: &Document, node: FeatureNode) -> PayloadKey {
    match node {
        FeatureNode::Origin | FeatureNode::PrincipalPlane(_) => PayloadKey::Datum(None),
        FeatureNode::Datum(datum) => PayloadKey::Datum(document.datum(datum).cloned()),
        FeatureNode::Sketch(sketch) => document
            .sketch3(sketch)
            .map_or(PayloadKey::Datum(None), |value| {
                PayloadKey::Sketch(value.clone(), document.sketch_plane_binding(sketch))
            }),
        FeatureNode::Extrude(extrude) => PayloadKey::Extrude(document.extrude(extrude).copied()),
        FeatureNode::Revolve(revolve) => PayloadKey::Revolve(document.revolve(revolve).copied()),
        FeatureNode::Sweep(sweep) => PayloadKey::Sweep(document.sweep(sweep).cloned()),
        FeatureNode::ImportedBody(body) => PayloadKey::Imported(body),
    }
}

fn parent_digests(
    tree: &FeatureTree,
    id: FeatureId,
    memo: &Memo,
) -> Vec<(FeatureId, ParentDigest)> {
    tree.parents(id)
        .into_iter()
        .map(|parent| {
            let digest = memo
                .get(&parent)
                .map_or(ParentDigest::Absent, |entry| entry.digest.clone());
            (parent, digest)
        })
        .collect()
}

fn node_dependencies(
    document: &Document,
    tree: &FeatureTree,
    order: &[FeatureId],
    id: FeatureId,
    node: FeatureNode,
    memo: &Memo,
) -> Vec<(FeatureId, ParentDigest)> {
    let mut digests = parent_digests(tree, id, memo);
    if feature_scope(document, node) == Some(FeatureScope::AllBodies) {
        digests.extend(prior_body_digests(order, id, memo));
    }
    digests
}

fn prior_body_digests(
    order: &[FeatureId],
    id: FeatureId,
    memo: &Memo,
) -> Vec<(FeatureId, ParentDigest)> {
    order
        .iter()
        .copied()
        .take_while(|&feature| feature != id)
        .filter_map(|feature| {
            let digest = memo.get(&feature)?.digest.clone();
            matches!(digest, ParentDigest::Geometry(_)).then_some((feature, digest))
        })
        .collect()
}

fn build_entry(
    document: &Document,
    id: FeatureId,
    node: FeatureNode,
    memo: &Memo,
    fingerprint: NodeFingerprint,
    skip: SkipState,
    pass: RebuildPass,
) -> NodeMemo {
    let (output, consumed) = match skip {
        SkipState::Active => node_output(document, id, node, memo),
        SkipState::Suppressed | SkipState::RolledBack => (NodeOutput::Unbuilt, Consumed::new()),
    };
    let digest = digest_of(&output, skip);
    let status = status_of(&output, skip);
    NodeMemo {
        fingerprint,
        output,
        digest,
        skip,
        status,
        built_at: pass,
        consumed,
    }
}

fn node_output(
    document: &Document,
    id: FeatureId,
    node: FeatureNode,
    memo: &Memo,
) -> (NodeOutput, Consumed) {
    match node {
        FeatureNode::Origin => (
            NodeOutput::Datum(Ok(DatumGeometry::Point(Point3::origin()))),
            Consumed::new(),
        ),
        FeatureNode::PrincipalPlane(plane) => (
            NodeOutput::Datum(Ok(DatumGeometry::Plane(principal_plane(plane)))),
            Consumed::new(),
        ),
        FeatureNode::Datum(datum) => {
            let output = match document.datum(datum) {
                None => NodeOutput::Datum(Err(DatumError::MissingReference)),
                Some(feature) => NodeOutput::Datum(evaluate_datum(feature, memo)),
            };
            (output, Consumed::new())
        }
        FeatureNode::Sketch(sketch) => {
            let output = document
                .sketch3(sketch)
                .map_or(NodeOutput::Unbuilt, |value| {
                    let evaluated = match document.sketch_plane_binding(sketch) {
                        None => evaluate_sketch_with_pierces(memo, value),
                        Some(SketchPlaneRef::Face(face)) => face_bound_sketch(memo, value, face),
                        Some(SketchPlaneRef::Datum(datum)) => {
                            datum_bound_sketch(memo, value, datum)
                        }
                    };
                    NodeOutput::Sketch(evaluated)
                });
            (output, Consumed::new())
        }
        FeatureNode::Extrude(extrude) => {
            let feature = document.extrude(extrude);
            let profile =
                feature.and_then(|f| sketch_profile(document.feature_tree(), f.sketch, memo));
            match (feature, profile) {
                (Some(feature), Some(profile)) => {
                    let bases = in_scope_bases(document, id, memo, feature.scope);
                    let tool = build_extrude_tool(id, profile, feature, &bases, memo);
                    apply_solid_operation(tool, &bases, feature.operation, feature.flip_side)
                }
                (Some(_), None) => unresolved_reference(),
                (None, _) => (NodeOutput::Unbuilt, Consumed::new()),
            }
        }
        FeatureNode::Revolve(revolve) => {
            let feature = document.revolve(revolve);
            let profile =
                feature.and_then(|f| sketch_profile(document.feature_tree(), f.sketch, memo));
            match (feature, profile) {
                (Some(feature), Some(profile)) => {
                    let bases = in_scope_bases(document, id, memo, feature.scope);
                    build_revolve_node(id, profile, feature, &bases, memo)
                }
                (Some(_), None) => unresolved_reference(),
                (None, _) => (NodeOutput::Unbuilt, Consumed::new()),
            }
        }
        FeatureNode::Sweep(sweep) => {
            let feature = document.sweep(sweep);
            let tree = document.feature_tree();
            let profile = feature.and_then(|f| sketch_profile(tree, f.profile, memo));
            let path = feature.and_then(|f| sweep_path_curves(f.path, tree, memo));
            let guides = feature.and_then(|f| {
                f.guides
                    .iter()
                    .map(|guide| resolve_guide(*guide, tree, memo))
                    .collect::<Option<Vec<_>>>()
            });
            match (feature, profile, path, guides) {
                (Some(feature), Some(profile), Some(path), Some(guides)) => {
                    let bases = in_scope_bases(document, id, memo, feature.scope);
                    build_sweep_node(id, profile, &path, &guides, feature, &bases, memo)
                }
                (Some(_), _, _, _) => unresolved_reference(),
                (None, _, _, _) => (NodeOutput::Unbuilt, Consumed::new()),
            }
        }
        FeatureNode::ImportedBody(body) => {
            let output = document
                .imported_body(body)
                .map_or(NodeOutput::Unbuilt, |solid| {
                    NodeOutput::Imported(Box::new(solid.clone()))
                });
            (output, Consumed::new())
        }
    }
}

fn principal_plane(plane: PrincipalPlane) -> Plane3 {
    Plane3::from(plane.basis())
}

fn evaluate_datum(feature: &DatumFeature, memo: &Memo) -> Result<DatumGeometry, DatumError> {
    let mut resolve = |target| resolve_datum_target(memo, target);
    feature.evaluate(&mut resolve)
}

fn resolve_datum_target(memo: &Memo, target: DatumTarget) -> Result<ReferenceGeometry, DatumError> {
    match target {
        DatumTarget::Feature(feature) => memo
            .get(&feature)
            .and_then(|entry| output_datum(&entry.output))
            .map(DatumGeometry::reference)
            .ok_or_else(|| DatumError::Unresolved(Box::new(target))),
        DatumTarget::Entity(entity) => resolve_entity_geometry(memo, entity, target),
        DatumTarget::Sketch3Entity(reference) => {
            resolve_sketch3_entity_geometry(memo, reference, target)
        }
    }
}

fn resolve_sketch3_entity_geometry(
    memo: &Memo,
    reference: Sketch3EntityRef,
    target: DatumTarget,
) -> Result<ReferenceGeometry, DatumError> {
    let absent = || DatumError::Unresolved(Box::new(target));
    let entry = memo.get(&reference.sketch).ok_or_else(absent)?;
    let NodeOutput::Sketch(EvaluatedSketch::Solved(sketch3)) = &entry.output else {
        return Err(absent());
    };
    sketch3
        .line_axis(reference.entity)
        .map(ReferenceGeometry::Axis)
        .or_else(|| {
            sketch3
                .point_position(reference.entity)
                .map(ReferenceGeometry::Point)
        })
        .ok_or_else(absent)
}

fn resolve_entity_geometry(
    memo: &Memo,
    entity: EntityRef,
    target: DatumTarget,
) -> Result<ReferenceGeometry, DatumError> {
    let absent = || DatumError::Unresolved(Box::new(target));
    match entity {
        EntityRef::Face(reference) => {
            let solid = body_in_memo(memo, reference.label.feature, reference.ordinal)
                .ok_or_else(absent)?;
            let face = unique_face(solid, reference.label).ok_or_else(absent)?;
            match solid.face_plane_basis(face.id()) {
                Some(basis) => Ok(ReferenceGeometry::Plane(basis.into())),
                None => match solid.face_axis(face.id()) {
                    Some(axis) => Ok(ReferenceGeometry::Axis(axis)),
                    None => Err(DatumError::Unsupported {
                        detail: DatumGap::CurvedFaceReference,
                    }),
                },
            }
        }
        EntityRef::Edge(reference) => {
            let solid = body_in_memo(memo, reference.label.feature, reference.ordinal)
                .ok_or_else(absent)?;
            let mut matches = solid
                .iter_edges()
                .filter(|edge| edge.label() == reference.label);
            let edge = matches.next().ok_or_else(absent)?;
            matches.next().is_none().then_some(()).ok_or_else(absent)?;
            match solid.edge_line_axis(edge.id()) {
                Some(axis) => Ok(ReferenceGeometry::Axis(axis)),
                None => Err(DatumError::Unsupported {
                    detail: DatumGap::CurvedEdgeReference,
                }),
            }
        }
        EntityRef::Vertex(reference) => {
            let solid = body_in_memo(memo, reference.label.feature, reference.ordinal)
                .ok_or_else(absent)?;
            let mut matches = solid
                .iter_vertices()
                .filter(|vertex| vertex.label() == reference.label);
            let vertex = matches.next().ok_or_else(absent)?;
            matches.next().is_none().then_some(()).ok_or_else(absent)?;
            let fingerprint = solid.vertex_fingerprint(vertex.id()).ok_or_else(absent)?;
            Ok(ReferenceGeometry::Point(fingerprint.point))
        }
    }
}

fn output_datum(output: &NodeOutput) -> Option<DatumGeometry> {
    match output {
        NodeOutput::Datum(result) => result.as_ref().ok().copied(),
        NodeOutput::Sketch(_)
        | NodeOutput::Extrude(_)
        | NodeOutput::Imported(_)
        | NodeOutput::Unbuilt => None,
    }
}

fn datum_rebuild_error(error: &DatumError) -> RebuildError {
    match error {
        DatumError::Unresolved(target) => match **target {
            DatumTarget::Entity(entity) => RebuildError::DanglingReference(entity),
            DatumTarget::Feature(_) | DatumTarget::Sketch3Entity(_) => {
                RebuildError::UpstreamUnresolved
            }
        },
        DatumError::WrongReferenceKind
        | DatumError::DegenerateInput
        | DatumError::MissingReference
        | DatumError::Unsupported { .. } => RebuildError::Build(BuildFailure::Kernel),
    }
}

fn evaluate_sketch_with_pierces(memo: &Memo, sketch: &Sketch3) -> EvaluatedSketch {
    match sketch
        .resolved_pierces(|reference| sketch3_axis(memo, reference.sketch, reference.entity))
    {
        Ok(resolved) => evaluate_sketch(&resolved),
        Err(unresolved) => {
            tracing::debug!(curve = %unresolved.curve, "pierce reference did not resolve");
            EvaluatedSketch::PlaneUnresolved(RebuildError::UpstreamUnresolved)
        }
    }
}

fn face_bound_sketch(memo: &Memo, value: &Sketch3, face: FaceRef) -> EvaluatedSketch {
    let Some(entry) = memo.get(&face.label.feature) else {
        return EvaluatedSketch::PlaneUnresolved(RebuildError::DanglingReference(
            face.entity_ref(),
        ));
    };
    let Some(solid) = output_solids(&entry.output).get(usize::from(face.ordinal.value())) else {
        return EvaluatedSketch::PlaneUnresolved(RebuildError::UpstreamUnresolved);
    };
    let Some(matched) = unique_face(solid, face.label) else {
        return EvaluatedSketch::PlaneUnresolved(RebuildError::DanglingReference(
            face.entity_ref(),
        ));
    };
    match solid.face_plane_basis(matched.id()) {
        Some(basis) => evaluate_sketch_with_pierces(memo, &value.rebased_onto(basis)),
        None => EvaluatedSketch::PlaneUnresolved(RebuildError::NonPlanarSketchTarget),
    }
}

fn datum_bound_sketch(memo: &Memo, value: &Sketch3, datum: FeatureId) -> EvaluatedSketch {
    let Some(entry) = memo.get(&datum) else {
        return EvaluatedSketch::PlaneUnresolved(RebuildError::UpstreamUnresolved);
    };
    match output_datum(&entry.output).and_then(DatumGeometry::plane) {
        Some(plane) => match SketchPlaneBasis::new(
            plane.origin(),
            plane.x_axis(),
            plane.y_axis(),
            Tolerance::new(1.0e-9),
        ) {
            Ok(basis) => evaluate_sketch_with_pierces(memo, &value.rebased_onto(basis)),
            Err(_) => EvaluatedSketch::PlaneUnresolved(RebuildError::NonPlanarSketchTarget),
        },
        None => EvaluatedSketch::PlaneUnresolved(RebuildError::NonPlanarSketchTarget),
    }
}

fn output_solids(output: &NodeOutput) -> &[BrepSolid] {
    match output {
        NodeOutput::Extrude(feature) => feature.solids(),
        NodeOutput::Imported(solid) => core::slice::from_ref(solid.as_ref()),
        NodeOutput::Datum(_) | NodeOutput::Sketch(_) | NodeOutput::Unbuilt => &[],
    }
}

fn output_solid(output: &NodeOutput) -> Option<&BrepSolid> {
    match output_solids(output) {
        [solid] => Some(solid),
        _ => None,
    }
}

fn body_in_memo(memo: &Memo, feature: FeatureId, ordinal: BodyOrdinal) -> Option<&BrepSolid> {
    output_solids(&memo.get(&feature)?.output).get(usize::from(ordinal.value()))
}

fn build_extrude_tool(
    extrude: FeatureId,
    sketch: &EvaluatedSketch,
    feature: &ExtrudeFeature,
    bases: &[Base<'_>],
    memo: &Memo,
) -> EvaluatedExtrude {
    let solved = match sketch {
        EvaluatedSketch::Solved(solved) => solved,
        EvaluatedSketch::Failed(error) => {
            return EvaluatedExtrude::failed(ExtrudeError::UnsolvedSketch(error.clone()));
        }
        EvaluatedSketch::PlaneUnresolved(error) => {
            return EvaluatedExtrude::failed(ExtrudeError::PlaneUnresolved(Box::new(*error)));
        }
    };
    let plane = Plane3::from(solved.plane());
    let base_bbox = union_bbox(bases);
    let base_solids: Vec<&BrepSolid> = bases.iter().map(|&(_, _, solid)| solid).collect();
    let face_plane = |face: FaceLabel| face_label_plane(memo, face);
    let vertex_point = |vertex: VertexLabel| vertex_label_point(memo, vertex);
    let body_bbox = |body: FeatureId| body_aabb(memo, body);
    let result = profile_from_sketch3(solved, feature.contours, None)
        .and_then(|profile| {
            let next_reach = |sign: f64, base_offset_mm: f64| {
                next_face_distance(&profile, &base_solids, sign, base_offset_mm)
            };
            let scene = ExtrudeScene::new(
                plane,
                base_bbox,
                &face_plane,
                &vertex_point,
                &body_bbox,
                &next_reach,
            );
            let plan = resolve_sweep(feature, &scene)?;
            evaluate_extrude_plan(extrude, &profile, &plan)
        })
        .map_err(ExtrudeError::Kernel);
    match result {
        Ok(solid) => EvaluatedExtrude::ok(solid),
        Err(error) => EvaluatedExtrude::failed(error),
    }
}

type Base<'memo> = (FeatureId, BodyOrdinal, &'memo BrepSolid);

fn unresolved_reference() -> (NodeOutput, Consumed) {
    (
        NodeOutput::Extrude(EvaluatedFeature::failed(ExtrudeError::UnresolvedReference)),
        Consumed::new(),
    )
}

fn apply_solid_operation(
    tool: EvaluatedExtrude,
    bases: &[Base<'_>],
    operation: ExtrudeOperation,
    flip_side: bool,
) -> (NodeOutput, Consumed) {
    match operation {
        ExtrudeOperation::Boss(MergeResult::Merge) => fold_merge(tool, bases),
        ExtrudeOperation::Boss(MergeResult::Separate) => (
            NodeOutput::Extrude(EvaluatedFeature::from_tool(tool)),
            Consumed::new(),
        ),
        ExtrudeOperation::Cut if flip_side => map_carve(&tool, bases, intersect_from),
        ExtrudeOperation::Cut => map_carve(&tool, bases, cut_from),
    }
}

fn fold_merge(tool: EvaluatedExtrude, bases: &[Base<'_>]) -> (NodeOutput, Consumed) {
    let folded = bases.iter().try_fold(
        (tool, Consumed::new()),
        |(accumulated, mut consumed), &(feature, ordinal, base)| {
            let (merged, did_merge) = merge_into(accumulated, base);
            let failure = (!did_merge)
                .then(|| merged.result().as_ref().err().cloned())
                .flatten();
            if let Some(error) = failure {
                Err(error)
            } else {
                if did_merge {
                    consumed.insert((feature, ordinal));
                }
                Ok((merged, consumed))
            }
        },
    );
    match folded {
        Ok((merged, consumed)) => (
            NodeOutput::Extrude(EvaluatedFeature::from_tool(merged)),
            consumed,
        ),
        Err(error) => (
            NodeOutput::Extrude(EvaluatedFeature::failed(error)),
            Consumed::new(),
        ),
    }
}

fn map_carve(
    tool: &EvaluatedExtrude,
    bases: &[Base<'_>],
    carve: impl Fn(EvaluatedExtrude, Option<&BrepSolid>) -> (EvaluatedExtrude, bool),
) -> (NodeOutput, Consumed) {
    let attempts: Vec<(EvaluatedExtrude, Option<(FeatureId, BodyOrdinal)>)> = if bases.is_empty() {
        vec![(carve(tool.clone(), None).0, None)]
    } else {
        bases
            .iter()
            .map(|&(feature, ordinal, base)| {
                let (result, carved) = carve(tool.clone(), Some(base));
                (result, carved.then_some((feature, ordinal)))
            })
            .collect()
    };
    let carved: Vec<BrepSolid> = attempts
        .iter()
        .filter_map(|(result, _)| result.solid().cloned())
        .collect();
    if carved.is_empty() {
        let error = attempts
            .iter()
            .find_map(|(result, _)| result.result().as_ref().err().cloned())
            .unwrap_or(ExtrudeError::CutWithoutBody);
        return (
            NodeOutput::Extrude(EvaluatedFeature::failed(error)),
            Consumed::new(),
        );
    }
    let consumed = attempts
        .iter()
        .filter_map(|(_, consumed)| *consumed)
        .collect();
    (
        NodeOutput::Extrude(EvaluatedFeature::bodies(carved)),
        consumed,
    )
}

fn build_revolve_node(
    revolve: FeatureId,
    sketch: &EvaluatedSketch,
    feature: &RevolveFeature,
    bases: &[Base<'_>],
    memo: &Memo,
) -> (NodeOutput, Consumed) {
    let separate_boss = matches!(
        feature.operation,
        ExtrudeOperation::Boss(MergeResult::Separate)
    );
    if separate_boss
        && matches!(feature.contours, ContourSelection::All)
        && let EvaluatedSketch::Solved(solved) = sketch
    {
        match revolve_region_solids(revolve, solved, feature, bases, memo) {
            Ok(solids) if !solids.is_empty() => {
                return (
                    NodeOutput::Extrude(EvaluatedFeature::bodies(solids)),
                    Consumed::new(),
                );
            }
            Ok(_) => {}
            Err(error) => {
                return (
                    NodeOutput::Extrude(EvaluatedFeature::failed(ExtrudeError::Kernel(error))),
                    Consumed::new(),
                );
            }
        }
    }
    let tool = build_revolve_tool(revolve, sketch, feature, bases, memo);
    apply_solid_operation(tool, bases, feature.operation, feature.flip_side)
}

fn build_revolve_tool(
    revolve: FeatureId,
    sketch: &EvaluatedSketch,
    feature: &RevolveFeature,
    bases: &[Base<'_>],
    memo: &Memo,
) -> EvaluatedExtrude {
    let solved = match sketch {
        EvaluatedSketch::Solved(solved) => solved,
        EvaluatedSketch::Failed(error) => {
            return EvaluatedExtrude::failed(ExtrudeError::UnsolvedSketch(error.clone()));
        }
        EvaluatedSketch::PlaneUnresolved(error) => {
            return EvaluatedExtrude::failed(ExtrudeError::PlaneUnresolved(Box::new(*error)));
        }
    };
    let face_plane = |face: FaceLabel| face_label_plane(memo, face);
    let vertex_point = |vertex: VertexLabel| vertex_label_point(memo, vertex);
    let result = resolve_revolve_axis(feature, solved, bases, memo)
        .and_then(|axis| revolve_solid(revolve, solved, feature, axis, &face_plane, &vertex_point))
        .map_err(ExtrudeError::Kernel);
    match result {
        Ok(solid) => EvaluatedExtrude::ok(solid),
        Err(error) => EvaluatedExtrude::failed(error),
    }
}

const PATH_TOLERANCE: Tolerance = Tolerance::new(1.0e-9);

fn build_sweep_node(
    sweep: FeatureId,
    profile_sketch: &EvaluatedSketch,
    path: &[Curve3Kind],
    guides: &[Vec<Curve3Kind>],
    feature: &SweepFeature,
    bases: &[Base<'_>],
    memo: &Memo,
) -> (NodeOutput, Consumed) {
    let tool = build_sweep_tool(sweep, profile_sketch, path, guides, feature, bases, memo);
    let tool_index = sweep_tool_index(feature, bases);
    let carve_bases: Vec<Base<'_>> = bases
        .iter()
        .enumerate()
        .filter(|&(index, _)| Some(index) != tool_index)
        .map(|(_, &base)| base)
        .collect();
    apply_solid_operation(tool, &carve_bases, feature.operation, feature.flip_side)
}

fn sweep_tool_index(feature: &SweepFeature, bases: &[Base<'_>]) -> Option<usize> {
    match feature.profile_kind {
        SweepProfileKind::Solid { tool } => bases.iter().position(|&(base_feature, ordinal, _)| {
            base_feature == tool.feature() && ordinal == tool.ordinal()
        }),
        SweepProfileKind::Sketch | SweepProfileKind::Circular { .. } => None,
    }
}

fn build_sweep_tool(
    sweep: FeatureId,
    profile_sketch: &EvaluatedSketch,
    path: &[Curve3Kind],
    guides: &[Vec<Curve3Kind>],
    feature: &SweepFeature,
    bases: &[Base<'_>],
    memo: &Memo,
) -> EvaluatedExtrude {
    let profile_solved = match solved_sketch(profile_sketch) {
        Ok(solved) => solved,
        Err(error) => return EvaluatedExtrude::failed(error),
    };
    let direction = match feature.alignment {
        SweepAlignment::DirectionVector { axis } => {
            match resolve_axis_ref(axis, profile_solved, bases, memo) {
                Ok(resolved) => Some(resolved.direction()),
                Err(error) => return EvaluatedExtrude::failed(ExtrudeError::Kernel(error)),
            }
        }
        _ => None,
    };
    let tool = sweep_tool_index(feature, bases)
        .and_then(|index| bases.get(index))
        .map(|&(_, _, solid)| solid);
    let body_refs: Vec<&BrepSolid> = bases.iter().map(|&(_, _, solid)| solid).collect();
    let rails = SweepRails {
        guides,
        direction,
        tool,
        bodies: &body_refs,
    };
    let result = profile_from_sketch3(profile_solved, ContourSelection::All, None)
        .and_then(|profile| evaluate_sweep(sweep, &profile, path, rails, feature))
        .map_err(ExtrudeError::Kernel);
    match result {
        Ok(solid) => EvaluatedExtrude::ok(solid),
        Err(error) => EvaluatedExtrude::failed(error),
    }
}

fn sweep_path_curves(path: SweepPath, tree: &FeatureTree, memo: &Memo) -> Option<Vec<Curve3Kind>> {
    let curves = match path {
        SweepPath::Sketch(sketch) => {
            path_curves3(solved_sketch(sketch_profile(tree, sketch, memo)?).ok()?)
        }
        SweepPath::Edge(label) => edge_path_curves(label, memo),
    };
    (!curves.is_empty()).then_some(curves)
}

fn resolve_guide(guide: SweepPath, tree: &FeatureTree, memo: &Memo) -> Option<Vec<Curve3Kind>> {
    match guide {
        SweepPath::Sketch(sketch) => Some(path_curves3(
            solved_sketch(sketch_profile(tree, sketch, memo)?).ok()?,
        )),
        SweepPath::Edge(label) => {
            let curves = edge_path_curves(label, memo);
            (!curves.is_empty()).then_some(curves)
        }
    }
}

fn edge_path_curves(label: EdgeLabel, memo: &Memo) -> Vec<Curve3Kind> {
    let Some(entry) = memo.get(&label.feature) else {
        return Vec::new();
    };
    output_solids(&entry.output)
        .iter()
        .flat_map(|solid| {
            solid
                .iter_edges()
                .filter(|edge| edge.label() == label)
                .map(|edge| edge_curve_kind(edge.curve()))
                .collect::<Vec<_>>()
        })
        .collect()
}

fn edge_curve_kind(curve: &EdgeCurve3) -> Curve3Kind {
    match curve {
        EdgeCurve3::Line(line) => line.as_kind(),
        EdgeCurve3::Arc(arc) => arc.as_kind(),
        EdgeCurve3::Circle(circle) => circle.as_kind(),
    }
}

fn solved_sketch(sketch: &EvaluatedSketch) -> Result<&Sketch3, ExtrudeError> {
    match sketch {
        EvaluatedSketch::Solved(solved) => Ok(solved),
        EvaluatedSketch::Failed(error) => Err(ExtrudeError::UnsolvedSketch(error.clone())),
        EvaluatedSketch::PlaneUnresolved(error) => {
            Err(ExtrudeError::PlaneUnresolved(Box::new(*error)))
        }
    }
}

fn path_curves3(solved: &Sketch3) -> Vec<Curve3Kind> {
    solved
        .entity_order()
        .iter()
        .filter_map(|&id| entity_curve3(solved, id))
        .collect()
}

fn entity_curve3(solved: &Sketch3, id: SketchEntityId) -> Option<Curve3Kind> {
    match solved.entities().get(id)? {
        Sketch3Entity::Line(line) if !line.for_construction() => {
            let from = solved.point_position(line.a())?;
            let to = solved.point_position(line.b())?;
            Line3::new(from, to, PATH_TOLERANCE)
                .ok()
                .map(Line3::as_kind)
        }
        Sketch3Entity::Circle(circle) if !circle.for_construction() => {
            let center = solved.point_position(circle.center())?;
            let (x, y) = circle.normal().orthonormal_basis();
            let plane = Plane3::new_unchecked(center, x, y);
            Circle3::new(plane, circle.radius(), PATH_TOLERANCE)
                .ok()
                .map(Circle3::as_kind)
        }
        Sketch3Entity::Arc(arc) if !arc.for_construction() => arc_curve3(solved, arc),
        Sketch3Entity::Point(_)
        | Sketch3Entity::Line(_)
        | Sketch3Entity::Circle(_)
        | Sketch3Entity::Arc(_)
        | Sketch3Entity::Ellipse(_)
        | Sketch3Entity::Spline(_) => None,
    }
}

fn arc_curve3(solved: &Sketch3, arc: &Arc3Data) -> Option<Curve3Kind> {
    let center = solved.point_position(arc.center())?;
    let start = solved.point_position(arc.start())?;
    let end = solved.point_position(arc.end())?;
    let normal = arc.normal();
    let radial = (start - center).try_normalize(PATH_TOLERANCE).ok()?;
    let tangent = normal.cross(radial, PATH_TOLERANCE).ok()?;
    let plane = Plane3::new_unchecked(center, radial, tangent);
    let radius = (start - center).norm();
    let from = start - center;
    let to = end - center;
    let cos = from.dot_mm2(to);
    let (nx, ny, nz) = normal.components();
    let (cx, cy, cz) = from.cross(to).coords_mm();
    let sin = nx * cx + ny * cy + nz * cz;
    let sweep = sin.atan2(cos).rem_euclid(core::f64::consts::TAU);
    Arc3::new(
        plane,
        radius,
        Angle::new::<radian>(0.0),
        Angle::new::<radian>(sweep),
        PATH_TOLERANCE,
    )
    .ok()
    .map(Arc3::as_kind)
}

fn revolve_axis_entity(feature: &RevolveFeature) -> Option<bone_types::SketchEntityId> {
    match feature.axis {
        RevolveAxis::SketchLine(entity) => Some(entity),
        RevolveAxis::Edge(_) | RevolveAxis::Datum(_) | RevolveAxis::Sketch3Line { .. } => None,
    }
}

fn revolve_one(
    revolve: FeatureId,
    feature: &RevolveFeature,
    profile: &bone_kernel::ExtrudeProfile,
    axis: Axis3,
    face_plane: &dyn Fn(FaceLabel) -> Option<Plane3>,
    vertex_point: &dyn Fn(VertexLabel) -> Option<Point3>,
) -> Result<BrepSolid, BrepError> {
    let scene = RevolveScene::new(axis, profile, face_plane, vertex_point);
    let plan = resolve_revolve(feature, &scene)?;
    evaluate_revolve(revolve, profile, &plan)
}

pub(crate) fn revolve_solid(
    revolve: FeatureId,
    solved: &Sketch3,
    feature: &RevolveFeature,
    axis: Axis3,
    face_plane: &dyn Fn(FaceLabel) -> Option<Plane3>,
    vertex_point: &dyn Fn(VertexLabel) -> Option<Point3>,
) -> Result<BrepSolid, BrepError> {
    let profile = profile_from_sketch3(solved, feature.contours, revolve_axis_entity(feature))?;
    revolve_one(revolve, feature, &profile, axis, face_plane, vertex_point)
}

fn revolve_region_solids(
    revolve: FeatureId,
    solved: &Sketch3,
    feature: &RevolveFeature,
    bases: &[Base<'_>],
    memo: &Memo,
) -> Result<Vec<BrepSolid>, BrepError> {
    let axis = resolve_revolve_axis(feature, solved, bases, memo)?;
    let face_plane = |face: FaceLabel| face_label_plane(memo, face);
    let vertex_point = |vertex: VertexLabel| vertex_label_point(memo, vertex);
    let profiles = profiles_from_sketch3_regions(solved, revolve_axis_entity(feature))?;
    profiles
        .iter()
        .map(|profile| revolve_one(revolve, feature, profile, axis, &face_plane, &vertex_point))
        .collect()
}

fn resolve_revolve_axis(
    feature: &RevolveFeature,
    solved: &Sketch3,
    bases: &[Base<'_>],
    memo: &Memo,
) -> Result<Axis3, BrepError> {
    resolve_axis_ref(feature.axis, solved, bases, memo)
}

fn resolve_axis_ref(
    axis: RevolveAxis,
    solved: &Sketch3,
    bases: &[Base<'_>],
    memo: &Memo,
) -> Result<Axis3, BrepError> {
    match axis {
        RevolveAxis::SketchLine(entity) => solved
            .line_axis(entity)
            .ok_or(BrepError::RevolveAxisUnresolved),
        RevolveAxis::Edge(label) => {
            edge_label_axis(bases, label).ok_or(BrepError::RevolveAxisUnresolved)
        }
        RevolveAxis::Datum(datum) => {
            datum_axis(memo, datum).ok_or(BrepError::RevolveAxisUnresolved)
        }
        RevolveAxis::Sketch3Line { sketch, entity } => {
            sketch3_axis(memo, sketch, entity).ok_or(BrepError::RevolveAxisUnresolved)
        }
    }
}

pub(crate) fn preview_revolve_axis(
    feature: &RevolveFeature,
    solved: &Sketch3,
) -> Result<Axis3, BrepError> {
    match feature.axis {
        RevolveAxis::SketchLine(entity) => solved
            .line_axis(entity)
            .ok_or(BrepError::RevolveAxisUnresolved),
        RevolveAxis::Edge(_) | RevolveAxis::Datum(_) | RevolveAxis::Sketch3Line { .. } => {
            Err(BrepError::RevolveAxisUnresolved)
        }
    }
}

fn edge_label_axis(bases: &[Base<'_>], target: EdgeLabel) -> Option<Axis3> {
    let candidates: Vec<Axis3> = bases
        .iter()
        .flat_map(|(_, _, solid)| {
            solid
                .iter_edges()
                .filter(|edge| edge.label() == target)
                .filter_map(|edge| solid.edge_line_axis(edge.id()))
                .collect::<Vec<_>>()
        })
        .collect();
    let [axis] = candidates.as_slice() else {
        return None;
    };
    Some(*axis)
}

fn datum_axis(memo: &Memo, datum: FeatureId) -> Option<Axis3> {
    match output_datum(&memo.get(&datum)?.output)?.reference() {
        ReferenceGeometry::Axis(axis) => Some(axis),
        ReferenceGeometry::Plane(_)
        | ReferenceGeometry::Point(_)
        | ReferenceGeometry::CoordinateSystem(_) => None,
    }
}

fn sketch3_axis(
    memo: &Memo,
    sketch: FeatureId,
    entity: bone_types::SketchEntityId,
) -> Option<Axis3> {
    match &memo.get(&sketch)?.output {
        NodeOutput::Sketch(EvaluatedSketch::Solved(sketch3)) => sketch3.line_axis(entity),
        _ => None,
    }
}

fn face_label_plane(memo: &Memo, face: FaceLabel) -> Option<Plane3> {
    let solid = output_solid(&memo.get(&face.feature)?.output)?;
    let matched = unique_face(solid, face)?;
    solid.face_plane_basis(matched.id()).map(Plane3::from)
}

fn vertex_label_point(memo: &Memo, label: VertexLabel) -> Option<Point3> {
    let solid = output_solid(&memo.get(&label.feature)?.output)?;
    let mut matches = solid
        .iter_vertices()
        .filter(|vertex| vertex.label() == label);
    let vertex = matches.next()?;
    matches.next().is_none().then_some(())?;
    Some(vertex.position())
}

fn body_aabb(memo: &Memo, feature: FeatureId) -> Option<Aabb3> {
    output_solid(&memo.get(&feature)?.output).and_then(BrepSolid::bounding_box)
}

fn in_scope_bases<'memo>(
    document: &Document,
    id: FeatureId,
    memo: &'memo Memo,
    scope: FeatureScope,
) -> Vec<Base<'memo>> {
    match scope {
        FeatureScope::AutoSelect => base_body(document, id, memo).into_iter().collect(),
        FeatureScope::AllBodies => live_bodies_before(document, id, memo),
    }
}

fn base_body<'memo>(document: &Document, id: FeatureId, memo: &'memo Memo) -> Option<Base<'memo>> {
    let tree = document.feature_tree();
    let binding = tree
        .parents(id)
        .into_iter()
        .find_map(|parent| match tree.node(parent)? {
            FeatureNode::Sketch(sketch) => match document.sketch_plane_binding(sketch)? {
                SketchPlaneRef::Face(face) => Some(face),
                SketchPlaneRef::Datum(_) => None,
            },
            _ => None,
        })?;
    let base = live_owner(memo, binding.label.feature);
    let solid = body_in_memo(memo, base, binding.ordinal)?;
    Some((base, binding.ordinal, solid))
}

fn live_bodies_before<'memo>(
    document: &Document,
    id: FeatureId,
    memo: &'memo Memo,
) -> Vec<Base<'memo>> {
    let before: Vec<FeatureId> = document
        .feature_tree()
        .topo_order()
        .into_iter()
        .take_while(|&feature| feature != id)
        .collect();
    let consumed: Consumed = before
        .iter()
        .filter_map(|feature| memo.get(feature))
        .flat_map(|entry| entry.consumed.iter().copied())
        .collect();
    before
        .iter()
        .filter_map(|&feature| Some((feature, memo.get(&feature)?)))
        .flat_map(|(feature, entry)| {
            output_solids(&entry.output)
                .iter()
                .enumerate()
                .map(move |(index, solid)| {
                    (
                        feature,
                        BodyOrdinal::new(u16::try_from(index).unwrap_or(u16::MAX)),
                        solid,
                    )
                })
        })
        .filter(|(feature, ordinal, _)| !consumed.contains(&(*feature, *ordinal)))
        .collect()
}

fn union_bbox(bases: &[Base<'_>]) -> Option<Aabb3> {
    bases
        .iter()
        .filter_map(|(_, _, solid)| solid.bounding_box())
        .reduce(Aabb3::union)
}

fn live_owner(memo: &Memo, origin: FeatureId) -> FeatureId {
    core::iter::successors(Some(origin), |&feature| {
        memo.iter().find_map(|(consumer, entry)| {
            entry
                .consumed
                .iter()
                .any(|&(consumed, _)| consumed == feature)
                .then_some(*consumer)
        })
    })
    .last()
    .unwrap_or(origin)
}

fn sketch_profile<'memo>(
    tree: &FeatureTree,
    sketch: bone_types::SketchId,
    memo: &'memo Memo,
) -> Option<&'memo EvaluatedSketch> {
    let feature = tree.feature_of_sketch(sketch)?;
    match &memo.get(&feature)?.output {
        NodeOutput::Sketch(sketch) => Some(sketch),
        NodeOutput::Datum(_)
        | NodeOutput::Extrude(_)
        | NodeOutput::Imported(_)
        | NodeOutput::Unbuilt => None,
    }
}

fn digest_of(output: &NodeOutput, skip: SkipState) -> ParentDigest {
    match skip {
        SkipState::Suppressed | SkipState::RolledBack => ParentDigest::Absent,
        SkipState::Active => match output {
            NodeOutput::Datum(result) => ParentDigest::Datum(result.as_ref().ok().copied()),
            NodeOutput::Sketch(sketch) => ParentDigest::Sketch(sketch.clone()),
            NodeOutput::Extrude(feature) => feature
                .generation()
                .map_or(ParentDigest::Absent, ParentDigest::Geometry),
            NodeOutput::Imported(solid) => {
                ParentDigest::Geometry(GeometryGeneration::from_solid_key(solid.content_key()))
            }
            NodeOutput::Unbuilt => ParentDigest::Absent,
        },
    }
}

fn status_of(output: &NodeOutput, skip: SkipState) -> RebuildStatus {
    match skip {
        SkipState::Suppressed | SkipState::RolledBack => RebuildStatus::UpToDate,
        SkipState::Active => match output {
            NodeOutput::Imported(_) => RebuildStatus::UpToDate,
            NodeOutput::Datum(result) => match result {
                Ok(_) => RebuildStatus::UpToDate,
                Err(error) => RebuildStatus::Error(datum_rebuild_error(error)),
            },
            NodeOutput::Unbuilt => RebuildStatus::NeedsRebuild,
            NodeOutput::Sketch(sketch) => match sketch {
                EvaluatedSketch::Solved(_) => RebuildStatus::UpToDate,
                EvaluatedSketch::Failed(_) => {
                    RebuildStatus::Error(RebuildError::Build(BuildFailure::UnsolvedSketch))
                }
                EvaluatedSketch::PlaneUnresolved(error) => RebuildStatus::Error(*error),
            },
            NodeOutput::Extrude(feature) => match feature.error() {
                None => RebuildStatus::UpToDate,
                Some(ExtrudeError::UnsolvedSketch(_)) => {
                    RebuildStatus::Error(RebuildError::Build(BuildFailure::UnsolvedSketch))
                }
                Some(ExtrudeError::Kernel(_) | ExtrudeError::FlipSideUnsupported) => {
                    RebuildStatus::Error(RebuildError::Build(BuildFailure::Kernel))
                }
                Some(ExtrudeError::CutWithoutBody) => {
                    RebuildStatus::Error(RebuildError::Build(BuildFailure::CutWithoutBody))
                }
                Some(ExtrudeError::PlaneUnresolved(_) | ExtrudeError::UnresolvedReference) => {
                    RebuildStatus::Error(RebuildError::UpstreamUnresolved)
                }
            },
        },
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeSet;

    use bone_kernel::{
        AxisRecipe, BrepFace, DatumFeature, DatumGeometry, ExtrudeFeature, FeatureScope,
        FromCondition, PlaneRecipe, RevolveAxis, RevolveFeature, SweepDirection, SweepFeature,
        SweepOrientation, SweepPath, SweepProfileKind,
    };
    use bone_types::{
        AngleTolerance, BodyOrdinal, BodyRef, BuildFailure, ChordHeightTolerance, DatumId,
        DatumTarget, DocumentId, EdgeRef, EntityRef, FaceRef, FeatureId, Length, PositiveLength,
        RebuildError, RebuildStatus, Resolution, RollbackMarker, millimeter,
    };
    use proptest::prelude::*;

    use super::{EvaluatedModel, RebuildBudget, RebuildCost, RecomputeScope};
    use crate::document::{DatumReferenceError, Document, PrincipalPlane};
    use crate::evaluator::ExtrudeError;
    use crate::sketch3::{Edit3Outcome, Sketch3, Sketch3Edit, Sketch3Entity, Sketch3Relation};
    use crate::test_support::{
        Chain, blind_extrude, blind_revolve, box_sketch, box_sketch_at, chain_handles,
        circle_sketch, contour_revolve, cut_extrude, draft_boss, end_cap_ref, from_offset_boss,
        full_model, merge_extrude, open_chain_sketch, placeholder_fingerprint, push_box,
        push_chain, push_face_bound_box_cut, push_face_bound_box_cut_flip,
        push_face_bound_box_cut_with, push_face_bound_box_merge, push_face_bound_chain,
        push_face_bound_circle_cut, revolve_axis_sketch, revolve_circle_profile_sketch,
        revolve_open_profile_sketch, revolve_plain_axis_sketch, revolve_single_line_sketch,
        revolve_two_contour_sketch, side_face_label, side_face_ref, start_cap_label,
        start_cap_vertex_label, thin_boss, thin_revolve, two_direction_boss,
    };
    use bone_kernel::ExtrudeEndCondition;
    use bone_types::FaceRole;
    use bone_types::{Point3, UnitVec3};

    fn doc() -> Document {
        Document::new(DocumentId::default(), "doc".to_owned())
    }

    fn sweep_scene(
        build: impl FnOnce(&mut Document) -> FeatureId,
    ) -> (Document, EvaluatedModel, FeatureId) {
        let mut document = doc();
        let sweep = build(&mut document);
        let model = full_model(&document);
        assert_eq!(
            model.status(sweep),
            Some(RebuildStatus::UpToDate),
            "the sweep resolves through the document: {:?}",
            model.extrude_error(sweep),
        );
        (document, model, sweep)
    }

    fn box_with_cap(side_mm: f64, depth_mm: f64) -> (Document, Chain, EvaluatedModel, FaceRef) {
        let mut document = doc();
        let base = push_box(&mut document, "Base", side_mm, depth_mm);
        let probe = full_model(&document);
        let cap = end_cap_ref(&probe, base.extrude);
        (document, base, probe, cap)
    }

    fn chain_with_cap(side_mm: f64, depth_mm: f64) -> (Document, Chain, EvaluatedModel, FaceRef) {
        let mut document = doc();
        let base = push_chain(&mut document, "Base", side_mm, depth_mm);
        let probe = full_model(&document);
        let cap = end_cap_ref(&probe, base.extrude);
        (document, base, probe, cap)
    }

    fn plate_scene(
        side_mm: f64,
        feature: impl FnOnce(bone_types::SketchId) -> ExtrudeFeature,
    ) -> (Document, EvaluatedModel, Chain) {
        let mut document = doc();
        let sketch_id = document.allocate_sketch();
        document.insert_sketch(sketch_id, "Plate".to_owned(), box_sketch(side_mm));
        let extrude_id = document.commit_extrude(feature(sketch_id));
        let chain = chain_handles(&document, sketch_id, extrude_id);
        let model = full_model(&document);
        (document, model, chain)
    }

    #[test]
    fn revolves_build_their_expected_face_topology() -> Result<(), Box<dyn std::error::Error>> {
        type RevolveTopologyCase = (
            &'static str,
            fn() -> (Sketch3, bone_types::SketchEntityId),
            fn(bone_types::SketchId, bone_types::SketchEntityId) -> RevolveFeature,
            fn(&bone_kernel::BrepSolid),
        );
        let cases: [RevolveTopologyCase; 6] = [
            (
                "full ring",
                revolve_axis_sketch,
                |s, a| blind_revolve(s, a, 360.0),
                |solid| {
                    let sides = solid
                        .iter_faces()
                        .filter(|face| matches!(face.label().role, FaceRole::Side { .. }))
                        .count();
                    assert_eq!(sides, 4, "a revolved rectangle ring has four side faces");
                    assert_eq!(
                        solid.iter_faces().count(),
                        4,
                        "a full revolve has no start or end caps"
                    );
                },
            ),
            (
                "quarter caps",
                revolve_axis_sketch,
                |s, a| blind_revolve(s, a, 90.0),
                |solid| {
                    assert_eq!(
                        solid.iter_faces().count(),
                        6,
                        "a partial revolve adds a start and end cap to the four sides"
                    );
                    assert!(
                        solid
                            .iter_faces()
                            .any(|face| matches!(face.label().role, FaceRole::StartCap)),
                        "a partial revolve carries a start cap"
                    );
                },
            ),
            (
                "thin closed ring",
                revolve_axis_sketch,
                |s, a| thin_revolve(s, a, 360.0, 0.5),
                |solid| {
                    assert_eq!(
                        solid.iter_faces().count(),
                        8,
                        "a full thin revolve walls the rectangle into outer and inner rings"
                    );
                },
            ),
            (
                "thin open shell",
                revolve_open_profile_sketch,
                |s, a| thin_revolve(s, a, 360.0, 0.4),
                |solid| {
                    assert!(
                        solid.iter_faces().count() >= 6,
                        "the open chain walls into at least its mitred segments plus caps"
                    );
                },
            ),
            (
                "open auto-close solid",
                revolve_open_profile_sketch,
                |s, a| blind_revolve(s, a, 360.0),
                |_solid| {},
            ),
            (
                "circle torus",
                revolve_circle_profile_sketch,
                |s, a| blind_revolve(s, a, 360.0),
                |solid| {
                    assert_eq!(
                        solid.iter_faces().count(),
                        1,
                        "a full torus is one periodic toroidal face"
                    );
                    assert!(
                        solid.bounding_box().is_some(),
                        "the edge-free torus still frames via its faces"
                    );
                },
            ),
        ];
        cases.iter().try_for_each(
            |&(label, profile, feature, check)| -> Result<(), Box<dyn std::error::Error>> {
                let mut document = doc();
                let (sketch, axis) = profile();
                let sketch_id = document.allocate_sketch();
                document.insert_sketch(sketch_id, "Profile".to_owned(), sketch);
                let revolve_id = document.commit_revolve(feature(sketch_id, axis));
                let revolve = document
                    .feature_tree()
                    .feature_of_revolve(revolve_id)
                    .ok_or_else(|| format!("{label}: revolve has a feature node"))?;
                let model = full_model(&document);
                assert_eq!(
                    model.status(revolve),
                    Some(RebuildStatus::UpToDate),
                    "{label}: the revolve rebuilds clean: {:?}",
                    model.extrude_error(revolve),
                );
                let solid = model
                    .body(revolve)
                    .ok_or_else(|| format!("{label}: the revolve body is built"))?;
                check(solid);
                Ok(())
            },
        )?;
        Ok(())
    }

    fn circle_path_about_y(radius_mm: f64) -> Sketch3 {
        let (sketch, center) =
            sketch3_entity(&Sketch3::new(), Sketch3Entity::point(Point3::origin()));
        let (sketch, _) = sketch3_entity(
            &sketch,
            Sketch3Entity::circle(
                center,
                Length::new::<millimeter>(radius_mm),
                UnitVec3::y_axis(),
                false,
            ),
        );
        sketch
    }

    fn z_line_path(length_mm: f64) -> Sketch3 {
        z_span_path(0.0, length_mm)
    }

    fn z_span_path(from_mm: f64, to_mm: f64) -> Sketch3 {
        z_line_path_at(0.0, 0.0, from_mm, to_mm)
    }

    fn z_line_path_at(x_mm: f64, y_mm: f64, from_mm: f64, to_mm: f64) -> Sketch3 {
        let (sketch, start) = sketch3_entity(
            &Sketch3::new(),
            Sketch3Entity::point(Point3::from_mm(x_mm, y_mm, from_mm)),
        );
        let (sketch, end) = sketch3_entity(
            &sketch,
            Sketch3Entity::point(Point3::from_mm(x_mm, y_mm, to_mm)),
        );
        let (sketch, _) = sketch3_entity(&sketch, Sketch3Entity::line(start, end, false));
        sketch
    }

    fn right_angle_path(arm_mm: f64) -> Sketch3 {
        let (sketch, corner) = sketch3_entity(
            &Sketch3::new(),
            Sketch3Entity::point(Point3::from_mm(0.0, 0.0, arm_mm)),
        );
        let (sketch, start) = sketch3_entity(&sketch, Sketch3Entity::point(Point3::origin()));
        let (sketch, elbow) = sketch3_entity(
            &sketch,
            Sketch3Entity::point(Point3::from_mm(arm_mm, 0.0, arm_mm)),
        );
        let (sketch, _) = sketch3_entity(&sketch, Sketch3Entity::line(start, corner, false));
        let (sketch, _) = sketch3_entity(&sketch, Sketch3Entity::line(corner, elbow, false));
        sketch
    }

    fn quarter_arc_path_from_origin(radius_mm: f64) -> Sketch3 {
        let (sketch, center) = sketch3_entity(
            &Sketch3::new(),
            Sketch3Entity::point(Point3::from_mm(radius_mm, 0.0, 0.0)),
        );
        let (sketch, start) = sketch3_entity(&sketch, Sketch3Entity::point(Point3::origin()));
        let (sketch, end) = sketch3_entity(
            &sketch,
            Sketch3Entity::point(Point3::from_mm(radius_mm, 0.0, radius_mm)),
        );
        let (sketch, _) = sketch3_entity(
            &sketch,
            Sketch3Entity::arc(center, start, end, UnitVec3::y_axis(), false),
        );
        sketch
    }

    fn commit_sweep_feature(
        document: &mut Document,
        profile: Sketch3,
        path: Sketch3,
        feature_of: impl FnOnce(bone_types::SketchId, bone_types::SketchId) -> SweepFeature,
    ) -> FeatureId {
        let path_id = document.allocate_sketch();
        document.insert_sketch(path_id, "Path".to_owned(), path);
        let profile_id = document.allocate_sketch();
        document.insert_sketch(profile_id, "Profile".to_owned(), profile);
        let sweep_id = document.commit_sweep(feature_of(profile_id, path_id));
        let Some(sweep) = document.feature_tree().feature_of_sweep(sweep_id) else {
            panic!("the sweep has a feature node");
        };
        sweep
    }

    fn commit_sweep_with(document: &mut Document, profile: Sketch3, path: Sketch3) -> FeatureId {
        commit_sweep_feature(document, profile, path, |profile, path| {
            SweepFeature::boss(profile, SweepPath::Sketch(path))
        })
    }

    fn pierced_profile(
        build_path: impl FnOnce() -> (Sketch3, bone_types::SketchEntityId),
    ) -> (Document, FeatureId, FeatureId, bone_types::SketchEntityId) {
        let mut document = doc();
        let (path, referenced) = build_path();
        let path_id = document.allocate_sketch();
        document.insert_sketch(path_id, "Path".to_owned(), path);
        let Some(path_feature) = document.feature_tree().feature_of_sketch(path_id) else {
            panic!("the path sketch has a feature node");
        };
        let (profile, pierced) = sketch3_entity(
            &Sketch3::new(),
            Sketch3Entity::point(Point3::from_mm(0.0, 0.0, 0.0)),
        );
        let curve = bone_types::Sketch3EntityRef::new(path_feature, referenced);
        let (profile, _rel) = profile.apply(Sketch3Edit::AddRelation(Sketch3Relation::Pierce {
            point: pierced,
            curve,
        }));
        let profile_id = document.allocate_sketch();
        document.insert_sketch(profile_id, "Profile".to_owned(), profile);
        let Some(profile_feature) = document.feature_tree().feature_of_sketch(profile_id) else {
            panic!("the profile sketch has a feature node");
        };
        (document, profile_feature, path_feature, pierced)
    }

    #[test]
    fn a_pierce_pins_the_crossing_or_errors_the_profile() -> Result<(), Box<dyn std::error::Error>>
    {
        type PierceRefCase = (&'static str, fn() -> (Sketch3, bone_types::SketchEntityId));
        let (document, profile_feature, path_feature, pierced) = pierced_profile(|| {
            let (path, pa) = sketch3_entity(
                &Sketch3::new(),
                Sketch3Entity::point(Point3::from_mm(5.0, 5.0, 0.0)),
            );
            let (path, pb) =
                sketch3_entity(&path, Sketch3Entity::point(Point3::from_mm(5.0, 5.0, 40.0)));
            sketch3_entity(&path, Sketch3Entity::line(pa, pb, false))
        });
        assert!(
            document
                .feature_tree()
                .parents(profile_feature)
                .contains(&path_feature),
            "the pierce makes the profile depend on the path for rebuild ordering",
        );
        let model = full_model(&document);
        assert_eq!(
            model.status(profile_feature),
            Some(RebuildStatus::UpToDate),
            "a pierced profile rebuilds clean",
        );
        let Some(crate::evaluator::EvaluatedSketch::Solved(solved)) =
            model.evaluated_sketch(profile_feature)
        else {
            panic!("the profile solves");
        };
        let position = solved
            .point_position(pierced)
            .ok_or("the pierced point resolves")?;
        let (x, y, z) = position.coords_mm();
        assert!(
            (x - 5.0).abs() < 1.0e-6 && (y - 5.0).abs() < 1.0e-6 && z.abs() < 1.0e-6,
            "the pierced point snaps to where the path crosses the sketch plane, got {position}",
        );

        let cases: [PierceRefCase; 2] = [
            ("its reference is a lone point, not a curve", || {
                sketch3_entity(
                    &Sketch3::new(),
                    Sketch3Entity::point(Point3::from_mm(5.0, 5.0, 10.0)),
                )
            }),
            (
                "the path lies in the sketch plane, so cannot pierce it",
                || {
                    let (path, pa) = sketch3_entity(
                        &Sketch3::new(),
                        Sketch3Entity::point(Point3::from_mm(-10.0, 0.0, 0.0)),
                    );
                    let (path, pb) = sketch3_entity(
                        &path,
                        Sketch3Entity::point(Point3::from_mm(10.0, 0.0, 0.0)),
                    );
                    sketch3_entity(&path, Sketch3Entity::line(pa, pb, false))
                },
            ),
        ];
        cases.iter().for_each(|&(reason, build_path)| {
            let (document, profile_feature, _path_feature, _pierced) = pierced_profile(build_path);
            let model = full_model(&document);
            assert_eq!(
                model.status(profile_feature),
                Some(RebuildStatus::Error(RebuildError::UpstreamUnresolved)),
                "a pierce errors the profile when {reason}",
            );
            assert!(
                model.evaluated_sketch3(profile_feature).is_none(),
                "the errored profile exposes no solved sketch downstream",
            );
        });
        Ok(())
    }

    #[test]
    fn a_plane_normal_to_a_sketch_line_lands_at_the_endpoint()
    -> Result<(), Box<dyn std::error::Error>> {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let (sketch, start) = sketch3_entity(
            &Sketch3::new(),
            Sketch3Entity::point(Point3::from_mm(0.0, 0.0, 0.0)),
        );
        let (sketch, end) = sketch3_entity(
            &sketch,
            Sketch3Entity::point(Point3::from_mm(0.0, 0.0, 40.0)),
        );
        let (sketch, line) = sketch3_entity(&sketch, Sketch3Entity::line(start, end, false));

        let path_id = document.allocate_sketch();
        document.insert_sketch(path_id, "Path".to_owned(), sketch);
        let path_feature = document
            .feature_tree()
            .feature_of_sketch(path_id)
            .ok_or("the path sketch has a feature node")?;

        let curve =
            DatumTarget::Sketch3Entity(bone_types::Sketch3EntityRef::new(path_feature, line));
        let point =
            DatumTarget::Sketch3Entity(bone_types::Sketch3EntityRef::new(path_feature, end));
        let datum = document.commit_datum(bone_kernel::DatumFeature::plane(
            bone_kernel::PlaneRecipe::NormalToCurve {
                curve,
                point,
                set_origin_on_curve: true,
            },
        ));
        let datum_feature = document
            .feature_tree()
            .feature_of_datum(datum)
            .ok_or("the datum has a feature node")?;

        let model = full_model(&document);
        assert_eq!(
            model.status(datum_feature),
            Some(RebuildStatus::UpToDate),
            "a plane normal to a sketch line rebuilds clean",
        );
        let Some(DatumGeometry::Plane(plane)) = model.datum(datum_feature) else {
            panic!("the datum evaluates to a plane");
        };
        let (x, y, z) = plane.origin().coords_mm();
        assert!(
            x.abs() < 1.0e-9 && y.abs() < 1.0e-9 && (z - 40.0).abs() < 1.0e-9,
            "the plane origin sits on the line endpoint",
        );
        assert!(
            (plane.normal().components().2.abs() - 1.0).abs() < 1.0e-9,
            "the plane normal points along the line",
        );
        Ok(())
    }

    #[test]
    fn sweeps_build_their_expected_bodies() {
        type SweepBodyCase = (
            &'static str,
            fn(&mut Document) -> FeatureId,
            fn(&EvaluatedModel, FeatureId),
        );
        let cases: [SweepBodyCase; 5] = [
            (
                "circular ring",
                |document| {
                    let (profile, _axis) = revolve_axis_sketch();
                    commit_sweep_with(document, profile, circle_path_about_y(3.0))
                },
                |model, sweep| {
                    let Some(solid) = model.body(sweep) else {
                        panic!("the sweep body is built");
                    };
                    let sides = solid
                        .iter_faces()
                        .filter(|face| matches!(face.label().role, FaceRole::Side { .. }))
                        .count();
                    assert_eq!(
                        sides, 4,
                        "a rectangle swept fully around a circular path is a four-sided ring"
                    );
                    assert_eq!(
                        solid.iter_faces().count(),
                        4,
                        "a full circular sweep has no start or end caps"
                    );
                },
            ),
            (
                "straight prism",
                |document| commit_sweep_with(document, box_sketch(4.0), z_line_path(6.0)),
                |model, sweep| {
                    let Some(solid) = model.body(sweep) else {
                        panic!("the straight sweep body is built");
                    };
                    assert_eq!(
                        solid.iter_faces().count(),
                        6,
                        "a square swept along a straight path is a six-faced prism"
                    );
                },
            ),
            (
                "bidirectional span",
                |document| {
                    commit_sweep_feature(
                        document,
                        box_sketch(4.0),
                        z_span_path(-4.0, 6.0),
                        |profile, path| SweepFeature {
                            direction: SweepDirection::Bidirectional,
                            ..SweepFeature::boss(profile, SweepPath::Sketch(path))
                        },
                    )
                },
                |model, sweep| {
                    assert!(
                        (body_volume(model, sweep) - 160.0).abs() < 0.2,
                        "a four-by-four profile spans the whole ten-unit path through its plane: got {}",
                        body_volume(model, sweep),
                    );
                },
            ),
            (
                "mitered elbow",
                |document| {
                    commit_sweep_with(
                        document,
                        box_sketch_at(-1.0, -1.0, 2.0),
                        right_angle_path(4.0),
                    )
                },
                |model, sweep| {
                    assert!(
                        body_is_manifold(model, sweep),
                        "the mitered elbow is a closed manifold body",
                    );
                    assert!(
                        (body_volume(model, sweep) - 32.0).abs() < 0.2,
                        "a centered two-by-two profile mitered around a right angle holds area times the eight unit centerline: got {}",
                        body_volume(model, sweep),
                    );
                },
            ),
            (
                "keep normal along an arc",
                |document| {
                    commit_sweep_feature(
                        document,
                        box_sketch_at(-1.0, -1.0, 2.0),
                        quarter_arc_path_from_origin(6.0),
                        |profile, path| SweepFeature {
                            orientation: SweepOrientation::KeepNormalConstant,
                            ..SweepFeature::boss(profile, SweepPath::Sketch(path))
                        },
                    )
                },
                |model, sweep| {
                    assert!(
                        model.body(sweep).is_some(),
                        "a keep-normal sweep along an open path produces a body"
                    );
                },
            ),
        ];
        cases.iter().for_each(|&(_label, build, check)| {
            let (_document, model, sweep) = sweep_scene(build);
            check(&model, sweep);
        });
    }

    #[test]
    fn sweep_preview_builds_the_committed_body_before_the_commit()
    -> Result<(), Box<dyn std::error::Error>> {
        type SweepPreviewCase = (&'static str, fn() -> (Sketch3, Sketch3), Option<usize>);
        let cases: [SweepPreviewCase; 2] = [
            (
                "a straight prism",
                || (box_sketch(4.0), z_line_path(6.0)),
                Some(6),
            ),
            (
                "a mitered corner elbow",
                || (box_sketch_at(-1.0, -1.0, 2.0), right_angle_path(4.0)),
                None,
            ),
        ];
        cases.iter().try_for_each(
            |&(label, build, expected_faces)| -> Result<(), Box<dyn std::error::Error>> {
                let mut document = Document::new(DocumentId::default(), "doc".to_owned());
                let (profile, path) = build();
                let path_id = document.allocate_sketch();
                document.insert_sketch(path_id, "Path".to_owned(), path);
                let profile_id = document.allocate_sketch();
                document.insert_sketch(profile_id, "Profile".to_owned(), profile);
                let model = full_model(&document);
                let feature = SweepFeature::boss(profile_id, SweepPath::Sketch(path_id));

                let preview = model
                    .sweep_preview(&document, &feature)
                    .ok_or_else(|| format!("{label}: the profile and path resolve to a preview"))?;
                let solid = preview.solid().ok_or_else(|| {
                    format!(
                        "{label}: the preview evaluates to a solid: {:?}",
                        preview.result().as_ref().err()
                    )
                })?;
                if let Some(faces) = expected_faces {
                    assert_eq!(
                        solid.iter_faces().count(),
                        faces,
                        "the uncommitted {label} preview matches the committed body",
                    );
                }
                Ok(())
            },
        )?;

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let path_id = document.allocate_sketch();
        document.insert_sketch(path_id, "Path".to_owned(), z_line_path(6.0));
        let profile_id = document.allocate_sketch();
        document.insert_sketch(profile_id, "Profile".to_owned(), box_sketch(4.0));
        let model = full_model(&document);
        let unresolved = SweepFeature::boss(
            profile_id,
            SweepPath::Sketch(bone_types::SketchId::default()),
        );
        assert!(
            model.sweep_preview(&document, &unresolved).is_none(),
            "an unresolved path yields no preview rather than a fake solid"
        );
        Ok(())
    }

    fn commit_two_plane_sweep(
        document: &mut Document,
    ) -> (bone_types::SketchId, bone_types::SketchId, FeatureId) {
        let path_id = document.allocate_sketch();
        document.insert_sketch(path_id, "Path".to_owned(), z_line_path(6.0));
        let profile_id = document.allocate_sketch();
        document.insert_sketch(profile_id, "Profile".to_owned(), box_sketch(4.0));
        let sweep_id =
            document.commit_sweep(SweepFeature::boss(profile_id, SweepPath::Sketch(path_id)));
        let Some(sweep) = document.feature_tree().feature_of_sweep(sweep_id) else {
            panic!("the sweep has a feature node");
        };
        (profile_id, path_id, sweep)
    }

    #[test]
    fn editing_a_sweep_input_sketch_rebuilds_the_dependent_sweep()
    -> Result<(), Box<dyn std::error::Error>> {
        type SweepEditCase = (
            &'static str,
            fn(bone_types::SketchId, bone_types::SketchId) -> (bone_types::SketchId, Sketch3),
        );
        let cases: [SweepEditCase; 2] = [
            ("path", |_profile, path| (path, z_line_path(10.0))),
            ("profile", |profile, _path| (profile, box_sketch(6.0))),
        ];
        cases
            .iter()
            .try_for_each(|&(input, edit)| -> Result<(), Box<dyn std::error::Error>> {
                let mut document = Document::new(DocumentId::default(), "doc".to_owned());
                let (profile_id, path_id, sweep) = commit_two_plane_sweep(&mut document);
                let (edited_id, replacement) = edit(profile_id, path_id);
                let edited_feature = document
                    .feature_tree()
                    .feature_of_sketch(edited_id)
                    .ok_or_else(|| format!("the {input} sketch has a feature node"))?;

                let mut model = full_model(&document);
                assert_eq!(model.status(sweep), Some(RebuildStatus::UpToDate));
                let built_at = model.built_at(sweep);

                document.replace_sketch(edited_id, replacement);
                model.recompute(
                    &document,
                    &BTreeSet::new(),
                    RollbackMarker::AtEnd,
                    RecomputeScope::Edited(edited_feature),
                );

                assert_ne!(
                    model.built_at(sweep),
                    built_at,
                    "editing the sweep {input} re-dirties the sweep that consumes it",
                );
                assert_eq!(
                    model.status(sweep),
                    Some(RebuildStatus::UpToDate),
                    "the sweep rebuilds with the edited {input}: {:?}",
                    model.extrude_error(sweep),
                );
                Ok(())
            })?;
        Ok(())
    }

    fn line3_sketch(from: Point3, to: Point3) -> Sketch3 {
        let (sketch, start) = sketch3_entity(&Sketch3::new(), Sketch3Entity::point(from));
        let (sketch, end) = sketch3_entity(&sketch, Sketch3Entity::point(to));
        let (sketch, _) = sketch3_entity(&sketch, Sketch3Entity::line(start, end, false));
        sketch
    }

    #[test]
    fn sweep_path_and_guide_dependencies() -> Result<(), Box<dyn std::error::Error>> {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let (profile, _axis) = revolve_axis_sketch();
        let path_id = document.allocate_sketch();
        document.insert_sketch(path_id, "Path".to_owned(), circle_path_about_y(3.0));
        let path_feature = document
            .feature_tree()
            .feature_of_sketch(path_id)
            .ok_or("the path sketch has a feature node")?;
        let profile_id = document.allocate_sketch();
        document.insert_sketch(profile_id, "Profile".to_owned(), profile);
        let sweep_id =
            document.commit_sweep(SweepFeature::boss(profile_id, SweepPath::Sketch(path_id)));
        let sweep = document
            .feature_tree()
            .feature_of_sweep(sweep_id)
            .ok_or("the sweep has a feature node")?;
        assert!(
            document
                .feature_tree()
                .parents(sweep)
                .contains(&path_feature),
            "the path sketch is recorded as a parent of the sweep"
        );

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let guide_id = document.allocate_sketch();
        document.insert_sketch(
            guide_id,
            "Guide".to_owned(),
            line3_sketch(
                Point3::from_mm(4.0, 4.0, 0.0),
                Point3::from_mm(2.0, 2.0, 8.0),
            ),
        );
        let sweep = commit_sweep_feature(
            &mut document,
            box_sketch(4.0),
            z_line_path(8.0),
            |profile, path| SweepFeature {
                guides: vec![SweepPath::Sketch(guide_id)],
                ..SweepFeature::boss(profile, SweepPath::Sketch(path))
            },
        );
        let model = full_model(&document);
        assert_eq!(
            model.status(sweep),
            Some(RebuildStatus::UpToDate),
            "the guided sweep resolves through the document: {:?}",
            model.extrude_error(sweep),
        );
        let expected = 128.0 * (1.0 - 0.125) / 1.5;
        assert!(
            (body_volume(&model, sweep) - expected).abs() / expected < 0.02,
            "the guide tapers the box to half size along the path: got {}, want {expected}",
            body_volume(&model, sweep),
        );
        let guide_feature = document
            .feature_tree()
            .feature_of_sketch(guide_id)
            .ok_or("the guide sketch has a feature node")?;
        assert!(
            document
                .feature_tree()
                .parents(sweep)
                .contains(&guide_feature),
            "the guide sketch is a dependency parent of the sweep",
        );

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let guide_id = document.allocate_sketch();
        document.insert_sketch(
            guide_id,
            "Guide".to_owned(),
            line3_sketch(
                Point3::from_mm(4.0, 4.0, 0.0),
                Point3::from_mm(2.0, 2.0, 8.0),
            ),
        );
        let path_id = document.allocate_sketch();
        document.insert_sketch(path_id, "Path".to_owned(), z_line_path(8.0));
        let profile_id = document.allocate_sketch();
        document.insert_sketch(profile_id, "Profile".to_owned(), box_sketch(4.0));
        let sweep_id = document.commit_sweep(SweepFeature {
            guides: vec![SweepPath::Sketch(guide_id)],
            ..SweepFeature::boss(profile_id, SweepPath::Sketch(path_id))
        });
        assert!(document.sweep(sweep_id).is_some());
        document.remove_sketch(guide_id);
        assert!(
            document.sweep(sweep_id).is_none(),
            "deleting a guide-only sketch removes the dependent sweep feature",
        );
        assert_eq!(document.feature_tree().feature_of_sweep(sweep_id), None);
        Ok(())
    }

    #[test]
    fn an_edge_path_sweep_builds_along_a_picked_base_edge() -> Result<(), Box<dyn std::error::Error>>
    {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let base = push_box(&mut document, "Box", 10.0, 8.0);
        let model = full_model(&document);
        let solid = model.body(base.extrude).ok_or("the base box builds")?;
        let vertical = |edge: &bone_kernel::BrepEdge| {
            solid.edge_line_axis(edge.id()).is_some_and(|axis| {
                let (dx, dy, _) = axis.direction().components();
                dx.abs() < 1.0e-9 && dy.abs() < 1.0e-9
            })
        };
        let label = solid
            .iter_edges()
            .filter(|edge| vertical(edge))
            .map(bone_kernel::BrepEdge::label)
            .find(|&candidate| {
                solid
                    .iter_edges()
                    .filter(|edge| edge.label() == candidate)
                    .count()
                    == 1
            })
            .ok_or("the box carries a uniquely labeled vertical edge to sweep along")?;

        let profile_id = document.allocate_sketch();
        document.insert_sketch(profile_id, "Profile".to_owned(), box_sketch(2.0));
        let feature = SweepFeature {
            operation: bone_kernel::ExtrudeOperation::Boss(bone_kernel::MergeResult::Separate),
            ..SweepFeature::boss(profile_id, SweepPath::Edge(label))
        };
        let sweep_id = document.commit_sweep(feature);
        let sweep = document
            .feature_tree()
            .feature_of_sweep(sweep_id)
            .ok_or("the edge-path sweep has a feature node")?;

        let model = full_model(&document);
        assert_eq!(
            model.status(sweep),
            Some(RebuildStatus::UpToDate),
            "the profile sweeps along the resolved base edge: {:?}",
            model.extrude_error(sweep),
        );
        let body = model.body(sweep).ok_or("the edge-path sweep body builds")?;
        assert_eq!(
            body.iter_faces().count(),
            6,
            "a square swept along a straight base edge is a six-faced prism",
        );
        Ok(())
    }

    fn commit_scoped_sweep(
        document: &mut Document,
        profile: Sketch3,
        path: Sketch3,
        operation: bone_kernel::ExtrudeOperation,
    ) -> FeatureId {
        let path_id = document.allocate_sketch();
        document.insert_sketch(path_id, "Path".to_owned(), path);
        let profile_id = document.allocate_sketch();
        document.insert_sketch(profile_id, "Profile".to_owned(), profile);
        let feature = SweepFeature {
            operation,
            scope: FeatureScope::AllBodies,
            ..SweepFeature::boss(profile_id, SweepPath::Sketch(path_id))
        };
        let sweep_id = document.commit_sweep(feature);
        let Some(sweep) = document.feature_tree().feature_of_sweep(sweep_id) else {
            panic!("the scoped sweep has a feature node");
        };
        sweep
    }

    #[test]
    fn a_scoped_sweep_boolean_modifies_the_base_body_in_place() {
        let cases: [(&str, bone_kernel::ExtrudeOperation, f64, bool); 2] = [
            (
                "cut carves a two-by-two tunnel",
                bone_kernel::ExtrudeOperation::Cut,
                24.0,
                true,
            ),
            (
                "boss merges a tower",
                bone_kernel::ExtrudeOperation::Boss(bone_kernel::MergeResult::Merge),
                48.0,
                false,
            ),
        ];
        cases
            .iter()
            .for_each(|&(label, operation, expected_volume, manifold)| {
                let mut document = Document::new(DocumentId::default(), "doc".to_owned());
                let _base = push_box(&mut document, "Base", 4.0, 2.0);
                let sweep = commit_scoped_sweep(
                    &mut document,
                    box_sketch_at(1.0, 1.0, 2.0),
                    z_line_path(6.0),
                    operation,
                );

                let model = full_model(&document);
                assert_eq!(
                    model.status(sweep),
                    Some(RebuildStatus::UpToDate),
                    "the scoped sweep {label} resolves: {:?}",
                    model.extrude_error(sweep),
                );
                assert_eq!(
                    model.bodies().count(),
                    1,
                    "a scoped sweep {label} stays one body rather than adding a second",
                );
                assert!(
                    !manifold || body_is_manifold(&model, sweep),
                    "the carved body is a closed manifold",
                );
                assert!(
                    (body_volume(&model, sweep) - expected_volume).abs() < 1.0e-4,
                    "the scoped sweep {label} lands its expected volume: got {}",
                    body_volume(&model, sweep),
                );
            });
    }

    #[test]
    fn a_solid_tool_sweep_cuts_a_tunnel() -> Result<(), Box<dyn std::error::Error>> {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let _base = push_box(&mut document, "Base", 4.0, 2.0);
        let diameter = PositiveLength::new(Length::new::<millimeter>(2.0))?;
        let tool_feature = commit_sweep_feature(
            &mut document,
            box_sketch(1.0),
            z_line_path_at(2.0, 2.0, -1.0, 4.0),
            |profile, path| SweepFeature {
                profile_kind: SweepProfileKind::Circular { diameter },
                operation: bone_kernel::ExtrudeOperation::Boss(bone_kernel::MergeResult::Separate),
                ..SweepFeature::boss(profile, SweepPath::Sketch(path))
            },
        );
        let cut = commit_sweep_feature(
            &mut document,
            box_sketch(1.0),
            z_line_path_at(2.0, 2.0, 0.0, 6.0),
            |profile, path| SweepFeature {
                profile_kind: SweepProfileKind::Solid {
                    tool: BodyRef::new(tool_feature, BodyOrdinal::origin()),
                },
                operation: bone_kernel::ExtrudeOperation::Cut,
                scope: FeatureScope::AllBodies,
                ..SweepFeature::boss(profile, SweepPath::Sketch(path))
            },
        );

        let model = full_model(&document);
        assert_eq!(
            model.status(cut),
            Some(RebuildStatus::UpToDate),
            "the solid tool sweep cut resolves: {:?}",
            model.extrude_error(cut),
        );
        assert_eq!(
            model.bodies().count(),
            2,
            "the carved base and the untouched tool cylinder both stay live",
        );
        assert!(
            body_is_manifold(&model, cut),
            "the carved body is a closed manifold",
        );
        let expected = 32.0 - core::f64::consts::PI * 2.0;
        assert!(
            (body_volume(&model, cut) - expected).abs() < 0.05,
            "the four-by-four-by-two block loses the tool cylinder bore: got {}, want {expected}",
            body_volume(&model, cut),
        );
        Ok(())
    }

    #[test]
    fn a_face_bound_sketch_survives_a_sweep_edit() -> Result<(), Box<dyn std::error::Error>> {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let path_id = document.allocate_sketch();
        document.insert_sketch(path_id, "Path".to_owned(), z_line_path(6.0));
        let profile_id = document.allocate_sketch();
        document.insert_sketch(profile_id, "Profile".to_owned(), box_sketch(4.0));
        let sweep_id =
            document.commit_sweep(SweepFeature::boss(profile_id, SweepPath::Sketch(path_id)));
        let sweep = document
            .feature_tree()
            .feature_of_sweep(sweep_id)
            .ok_or("the sweep has a feature node")?;
        let probe = full_model(&document);
        let side = side_face_ref(&probe, sweep);
        let bound = push_face_bound_chain(&mut document, "OnSide", side, 1.0, 2.0);
        let mut model = full_model(&document);
        assert_eq!(
            model.status(bound.sketch),
            Some(RebuildStatus::UpToDate),
            "the sketch binds to the swept side face",
        );
        assert!(
            model.body(bound.extrude).is_some(),
            "the face-bound boss builds before the edit",
        );

        document.insert_sweep(
            sweep_id,
            SweepFeature::boss(profile_id, SweepPath::Sketch(path_id)),
        );
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::AtEnd,
            RecomputeScope::Edited(sweep),
        );
        assert_eq!(
            model.status(sweep),
            Some(RebuildStatus::UpToDate),
            "re-inserting the same sweep rebuilds clean",
        );
        assert_eq!(
            model.status(bound.sketch),
            Some(RebuildStatus::UpToDate),
            "the bound sketch still resolves after the no-op sweep edit",
        );

        document.insert_sketch(path_id, "Path".to_owned(), z_line_path(9.0));
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::AtEnd,
            RecomputeScope::Full,
        );
        assert_eq!(
            model.status(sweep),
            Some(RebuildStatus::UpToDate),
            "the sweep rebuilds along the stretched path: {:?}",
            model.extrude_error(sweep),
        );
        assert!(
            (body_max_z(&model, sweep) - 9.0).abs() < 1.0e-6,
            "the stretched path lengthens the swept body: got {}",
            body_max_z(&model, sweep),
        );
        assert_eq!(
            model.status(bound.sketch),
            Some(RebuildStatus::UpToDate),
            "the bound sketch re-resolves its side face after the path edit",
        );
        assert!(
            model.body(bound.extrude).is_some(),
            "the face-bound boss rebuilds on the stretched sweep",
        );
        Ok(())
    }

    #[test]
    fn revolve_resolves_plain_axis_rejects_lines_and_selects_contours()
    -> Result<(), Box<dyn std::error::Error>> {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let sketch_id = document.allocate_sketch();
        document.insert_sketch(sketch_id, "Profile".to_owned(), revolve_plain_axis_sketch());
        let axis = document
            .sketch3(sketch_id)
            .and_then(crate::sketch3::Sketch3::axis_candidate)
            .ok_or("the free plain line beside the profile is the axis")?;
        let revolve_id = document.commit_revolve(blind_revolve(sketch_id, axis, 360.0));
        let revolve = document
            .feature_tree()
            .feature_of_revolve(revolve_id)
            .ok_or("revolve has a feature node")?;
        let model = full_model(&document);
        assert_eq!(
            model.status(revolve),
            Some(RebuildStatus::UpToDate),
            "the rectangle revolves about a plain sketch line: {:?}",
            model.extrude_error(revolve),
        );
        assert!(
            model.body(revolve).is_some(),
            "a non-construction line serves as the revolve axis and stays out of the profile",
        );

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let (sketch, axis) = revolve_single_line_sketch();
        let sketch_id = document.allocate_sketch();
        document.insert_sketch(sketch_id, "Profile".to_owned(), sketch);
        let revolve_id = document.commit_revolve(blind_revolve(sketch_id, axis, 360.0));
        let revolve = document
            .feature_tree()
            .feature_of_revolve(revolve_id)
            .ok_or("revolve has a feature node")?;
        let model = full_model(&document);
        assert!(
            matches!(
                model.extrude_error(revolve),
                Some(ExtrudeError::Kernel(
                    bone_kernel::BrepError::InvalidProfile {
                        reason: bone_kernel::ProfileDefect::ZeroArea
                    }
                ))
            ),
            "a lone line auto-closes onto itself and encloses no area: {:?}",
            model.extrude_error(revolve),
        );

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let (sketch, axis, first) = revolve_two_contour_sketch();
        let sketch_id = document.allocate_sketch();
        document.insert_sketch(sketch_id, "Profile".to_owned(), sketch);

        let whole = document.commit_revolve(blind_revolve(sketch_id, axis, 360.0));
        let whole_feature = document
            .feature_tree()
            .feature_of_revolve(whole)
            .ok_or("revolve has a feature node")?;
        let model = full_model(&document);
        let bodies = model
            .bodies()
            .filter(|(feature, _, _)| *feature == whole_feature)
            .count();
        assert_eq!(
            bodies, 2,
            "two disjoint contours revolve to a multibody output, one torus per region"
        );

        let picked = document.commit_revolve(contour_revolve(sketch_id, axis, 360.0, first));
        let picked_feature = document
            .feature_tree()
            .feature_of_revolve(picked)
            .ok_or("the contour revolve has a feature node")?;
        let model = full_model(&document);
        let solid = model
            .body(picked_feature)
            .ok_or("selecting one contour revolves that region to a torus")?;
        assert_eq!(
            solid.iter_faces().count(),
            1,
            "the selected circle alone revolves to a single torus"
        );
        Ok(())
    }

    fn principal_target(document: &Document, plane: PrincipalPlane) -> DatumTarget {
        let Some(feature) = document.feature_tree().principal_plane_feature(plane) else {
            panic!("the seeded tree carries the principal plane");
        };
        DatumTarget::Feature(feature)
    }

    #[test]
    fn a_revolve_about_a_datum_axis_builds_a_ring() -> Result<(), Box<dyn std::error::Error>> {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let xy = principal_target(&document, PrincipalPlane::Xy);
        let yz = principal_target(&document, PrincipalPlane::Yz);
        let datum =
            document.commit_datum(DatumFeature::axis(AxisRecipe::TwoPlanes { a: xy, b: yz }));
        let datum_feature = datum_feature_of(&document, datum);

        let (sketch, axis_line) = revolve_axis_sketch();
        let sketch_id = document.allocate_sketch();
        document.insert_sketch(sketch_id, "Profile".to_owned(), sketch);
        let feature = RevolveFeature {
            axis: RevolveAxis::Datum(datum_feature),
            ..blind_revolve(sketch_id, axis_line, 360.0)
        };
        let revolve_id = document.commit_revolve(feature);
        let revolve = document
            .feature_tree()
            .feature_of_revolve(revolve_id)
            .ok_or("revolve has a feature node")?;

        let model = full_model(&document);
        assert_eq!(
            model.status(revolve),
            Some(RebuildStatus::UpToDate),
            "the rectangle revolves about the datum Y axis: {:?}",
            model.extrude_error(revolve),
        );
        let solid = model
            .body(revolve)
            .ok_or("the datum-axis revolve body is built")?;
        let sides = solid
            .iter_faces()
            .filter(|face| matches!(face.label().role, FaceRole::Side { .. }))
            .count();
        assert_eq!(
            sides, 4,
            "a rectangle revolved fully about the datum axis is a four-sided ring"
        );
        Ok(())
    }

    fn along_y_sketch3(length_mm: f64) -> (Sketch3, bone_types::SketchEntityId) {
        let (sketch, start) =
            sketch3_entity(&Sketch3::new(), Sketch3Entity::point(Point3::origin()));
        let (sketch, end) = sketch3_entity(
            &sketch,
            Sketch3Entity::point(Point3::from_mm(3.0, 4.0, 1.0)),
        );
        let (sketch, segment) = sketch3_entity(&sketch, Sketch3Entity::line(start, end, false));
        let edits = [
            Sketch3Edit::AddRelation(Sketch3Relation::Fix(start)),
            Sketch3Edit::AddRelation(Sketch3Relation::AlongAxis {
                line: segment,
                axis: UnitVec3::y_axis(),
            }),
            Sketch3Edit::AddRelation(Sketch3Relation::Distance {
                a: start,
                b: end,
                value: Length::new::<millimeter>(length_mm),
                reference: false,
            }),
        ];
        let (sketch, _) = sketch.apply_all(edits);
        (sketch, segment)
    }

    fn revolve_about_sketch3(
        document: &mut Document,
        sketch3: Sketch3,
        line: bone_types::SketchEntityId,
    ) -> (bone_types::SketchId, FeatureId) {
        let axis_sketch = document.allocate_sketch();
        document.insert_sketch(axis_sketch, "Sketch3D".to_owned(), sketch3);
        let Some(sketch3_feature) = document.feature_tree().feature_of_sketch(axis_sketch) else {
            panic!("the sketch3 has a feature node");
        };
        let (sketch, _) = revolve_axis_sketch();
        let sketch_id = document.allocate_sketch();
        document.insert_sketch(sketch_id, "Profile".to_owned(), sketch);
        let feature = RevolveFeature {
            axis: RevolveAxis::Sketch3Line {
                sketch: sketch3_feature,
                entity: line,
            },
            ..blind_revolve(sketch_id, bone_types::SketchEntityId::default(), 360.0)
        };
        let revolve_id = document.commit_revolve(feature);
        let Some(revolve) = document.feature_tree().feature_of_revolve(revolve_id) else {
            panic!("revolve has a feature node");
        };
        (axis_sketch, revolve)
    }

    #[test]
    fn the_axis_sketch3_is_a_parent_and_edits_rebuild_the_revolve()
    -> Result<(), Box<dyn std::error::Error>> {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let (sketch3, line) = along_y_sketch3(10.0);
        let (axis_sketch, revolve) = revolve_about_sketch3(&mut document, sketch3, line);
        let sketch3_feature = document
            .feature_tree()
            .feature_of_sketch(axis_sketch)
            .ok_or("the sketch3 has a feature node")?;
        assert!(
            document
                .feature_tree()
                .parents(revolve)
                .contains(&sketch3_feature),
            "the 3D-sketch axis is a dependency parent of the revolve that consumes it",
        );

        let mut model = full_model(&document);
        assert_eq!(model.status(revolve), Some(RebuildStatus::UpToDate));
        let built_at = model.built_at(revolve);

        let (longer, _) = along_y_sketch3(30.0);
        document.replace_sketch(axis_sketch, longer);
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::AtEnd,
            RecomputeScope::Edited(sketch3_feature),
        );

        assert_ne!(
            model.built_at(revolve),
            built_at,
            "editing the 3D-sketch axis re-dirties the revolve that consumes it",
        );
        assert_eq!(
            model.status(revolve),
            Some(RebuildStatus::UpToDate),
            "the revolve rebuilds cleanly about the edited axis: {:?}",
            model.extrude_error(revolve),
        );
        Ok(())
    }

    #[test]
    fn edge_axis_resolves_a_unique_base_edge_to_its_line() -> Result<(), Box<dyn std::error::Error>>
    {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let chain = push_box(&mut document, "Box", 10.0, 4.0);
        let model = full_model(&document);
        let solid = model.body(chain.extrude).ok_or("the box body builds")?;
        let label = solid
            .iter_edges()
            .filter(|edge| solid.edge_line_axis(edge.id()).is_some())
            .map(bone_kernel::BrepEdge::label)
            .find(|&candidate| {
                solid
                    .iter_edges()
                    .filter(|edge| edge.label() == candidate)
                    .count()
                    == 1
            })
            .ok_or("a box carries a uniquely labeled straight edge")?;
        let expected = solid
            .iter_edges()
            .find(|edge| edge.label() == label)
            .and_then(|edge| solid.edge_line_axis(edge.id()))
            .ok_or("the chosen edge resolves to a line")?;
        let bases = [(chain.extrude, BodyOrdinal::origin(), solid)];
        let axis = super::edge_label_axis(&bases, label)
            .ok_or("the unique base edge resolves as a revolve axis")?;
        assert_eq!(
            axis, expected,
            "edge-axis resolution returns the base edge's own line"
        );
        Ok(())
    }

    fn xy_plane_target(document: &Document) -> DatumTarget {
        let Some(feature) = document
            .feature_tree()
            .principal_plane_feature(PrincipalPlane::Xy)
        else {
            panic!("the seeded tree carries the XY principal plane");
        };
        DatumTarget::Feature(feature)
    }

    fn commit_offset_plane(
        document: &mut Document,
        from: DatumTarget,
        distance_mm: f64,
    ) -> DatumId {
        document.commit_datum(DatumFeature::plane(PlaneRecipe::Offset {
            from,
            distance: Length::new::<millimeter>(distance_mm),
            flip: false,
        }))
    }

    fn datum_plane_sketch_chain(offset_mm: f64) -> (Document, DatumId, FeatureId, Chain) {
        let mut document = doc();
        let from = xy_plane_target(&document);
        let datum = commit_offset_plane(&mut document, from, offset_mm);
        let datum_feature = datum_feature_of(&document, datum);
        let sketch_id = document.allocate_sketch();
        document.insert_sketch(sketch_id, "Sketch1".to_owned(), circle_sketch(5.0));
        let Ok(()) = document.bind_sketch_to_datum_plane(sketch_id, datum_feature) else {
            panic!("binding a sketch onto a datum plane is acyclic");
        };
        let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0));
        let chain = chain_handles(&document, sketch_id, extrude_id);
        (document, datum, datum_feature, chain)
    }

    fn datum_plane_z(model: &EvaluatedModel, datum_feature: FeatureId) -> f64 {
        let Some(DatumGeometry::Plane(plane)) = model.datum(datum_feature) else {
            panic!("the datum feature evaluates to a plane");
        };
        plane.origin().z().get::<millimeter>()
    }

    fn datum_feature_of(document: &Document, datum: DatumId) -> FeatureId {
        let Some(feature) = document.feature_tree().feature_of_datum(datum) else {
            panic!("the datum resolves to a feature node");
        };
        feature
    }

    #[test]
    fn a_datum_plane_offsets_and_hosts_a_sketch_chain() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let from = xy_plane_target(&document);
        let datum = commit_offset_plane(&mut document, from, 25.0);
        let feature = datum_feature_of(&document, datum);
        let model = full_model(&document);
        assert!((datum_plane_z(&model, feature) - 25.0).abs() < 1.0e-9);
        assert_eq!(model.status(feature), Some(RebuildStatus::UpToDate));

        let (document, _datum, _datum_feature, chain) = datum_plane_sketch_chain(8.0);
        let model = full_model(&document);
        assert!(
            model.body(chain.extrude).is_some(),
            "the chain builds a body"
        );
        assert!(
            (body_min_z(&model, chain.extrude) - 8.0).abs() < 1.0e-6,
            "the sketch sits on the datum plane so the body rises from the offset",
        );
        assert!((body_max_z(&model, chain.extrude) - 12.0).abs() < 1.0e-6);

        let first = full_model(&document)
            .body(chain.extrude)
            .map(bone_kernel::BrepSolid::content_key);
        let second = full_model(&document)
            .body(chain.extrude)
            .map(bone_kernel::BrepSolid::content_key);
        assert!(first.is_some());
        assert_eq!(
            first, second,
            "a datum-plane sketch chain rebuilds byte for byte",
        );
    }

    #[test]
    fn editing_a_datum_offset_moves_the_downstream_body() -> Result<(), Box<dyn std::error::Error>>
    {
        let (mut document, datum, _datum_feature, chain) = datum_plane_sketch_chain(8.0);
        let from = xy_plane_target(&document);
        let mut model = full_model(&document);
        let before = model.built_at(chain.extrude);

        document.insert_datum(
            datum,
            DatumFeature::plane(PlaneRecipe::Offset {
                from,
                distance: Length::new::<millimeter>(20.0),
                flip: false,
            }),
        )?;
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::AtEnd,
            RecomputeScope::Full,
        );

        assert!(
            model.built_at(chain.extrude) > before,
            "moving the datum plane rebuilds the downstream body",
        );
        assert!(
            (body_min_z(&model, chain.extrude) - 20.0).abs() < 1.0e-6,
            "the body follows the datum plane to its new offset",
        );
        Ok(())
    }

    #[test]
    fn a_datum_referencing_a_deleted_body_dangles() {
        let (mut document, base, _probe, cap) = chain_with_cap(6.0, 4.0);
        let datum = commit_offset_plane(&mut document, DatumTarget::Entity(cap.entity_ref()), 5.0);
        let datum_feature = datum_feature_of(&document, datum);
        let model = full_model(&document);
        assert_eq!(model.status(datum_feature), Some(RebuildStatus::UpToDate));
        assert!(
            model.datum(datum_feature).is_some(),
            "the datum builds first"
        );

        document.remove_extrude(base.extrude_id);
        let mut model = model;
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::AtEnd,
            RecomputeScope::Full,
        );

        assert!(
            matches!(
                model.status(datum_feature),
                Some(RebuildStatus::Error(RebuildError::DanglingReference(_)))
            ),
            "a datum whose face reference was deleted dangles, never a silent wrong plane",
        );
        assert!(model.datum(datum_feature).is_none());
    }

    #[test]
    fn editing_a_datum_to_reference_a_later_body_is_refused() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let from = xy_plane_target(&document);
        let datum = commit_offset_plane(&mut document, from, 5.0);
        let base = push_chain(&mut document, "Base", 6.0, 4.0);
        let probe = full_model(&document);
        let cap = end_cap_ref(&probe, base.extrude);

        let outcome = document.insert_datum(
            datum,
            DatumFeature::plane(PlaneRecipe::Offset {
                from: DatumTarget::Entity(cap.entity_ref()),
                distance: Length::new::<millimeter>(5.0),
                flip: false,
            }),
        );

        assert!(
            matches!(outcome, Err(DatumReferenceError::WouldCycle(_))),
            "a datum cannot be edited to reference a body built after it",
        );
    }

    #[test]
    fn an_axis_datum_from_a_curved_edge_reports_a_kernel_gap()
    -> Result<(), Box<dyn std::error::Error>> {
        let (mut document, base, probe, _cap) = chain_with_cap(6.0, 4.0);
        let solid = probe
            .body(base.extrude)
            .ok_or("the cylinder builds a body")?;

        assert!(
            solid
                .iter_edges()
                .any(|edge| solid.edge_line_axis(edge.id()).is_some()),
            "the cylinder has straight seam edges",
        );
        let curved = solid
            .iter_edges()
            .find(|edge| solid.edge_line_axis(edge.id()).is_none())
            .ok_or("the cylinder has curved cap edges")?;
        let fingerprint = solid
            .edge_fingerprint(curved.id())
            .ok_or("the curved edge has a fingerprint")?;
        let target =
            DatumTarget::Entity(EntityRef::Edge(EdgeRef::new(curved.label(), fingerprint)));

        let datum = document.commit_datum(DatumFeature::axis(AxisRecipe::OneLine {
            reference: target,
        }));
        let datum_feature = datum_feature_of(&document, datum);
        let model = full_model(&document);

        assert_eq!(
            model.status(datum_feature),
            Some(RebuildStatus::Error(RebuildError::Build(
                BuildFailure::Kernel
            ))),
            "a curved edge cannot become an axis, so the kernel reports a gap instead of faking a tangent line",
        );
        assert!(model.datum(datum_feature).is_none());
        Ok(())
    }

    proptest! {
        #[test]
        fn a_datum_offset_places_the_plane_and_its_sketch_body(offset in 1.0f64..40.0) {
            let (document, _datum, datum_feature, chain) = datum_plane_sketch_chain(offset);
            let model = full_model(&document);

            prop_assert!((datum_plane_z(&model, datum_feature) - offset).abs() < 1.0e-9);
            prop_assert!(
                (body_min_z(&model, chain.extrude) - offset).abs() < 1.0e-6,
                "the datum-bound body always rises from the current offset",
            );
        }
    }

    fn body_max_z(model: &EvaluatedModel, body: FeatureId) -> f64 {
        let Some(solid) = model.body(body) else {
            panic!("the body is built");
        };
        let Some(aabb) = solid.bounding_box() else {
            panic!("a built body has a bounding box");
        };
        aabb.max().coords_mm().2
    }

    fn body_min_z(model: &EvaluatedModel, body: FeatureId) -> f64 {
        let Some(solid) = model.body(body) else {
            panic!("the body is built");
        };
        let Some(aabb) = solid.bounding_box() else {
            panic!("a built body has a bounding box");
        };
        aabb.min().coords_mm().2
    }

    fn solid_volume(solid: &bone_kernel::BrepSolid) -> f64 {
        let Ok(mesh) = solid.tessellate(
            ChordHeightTolerance::from_mm(0.001),
            AngleTolerance::from_radians(0.05),
        ) else {
            panic!("the body tessellates");
        };
        mesh.faces()
            .iter()
            .flat_map(|slab| {
                let positions = slab.positions().to_vec();
                slab.triangles().iter().map(move |triangle| {
                    let point = |index: u32| positions[index as usize].coords_mm();
                    let (ax, ay, az) = point(triangle[0]);
                    let (bx, by, bz) = point(triangle[1]);
                    let (cx, cy, cz) = point(triangle[2]);
                    let cross = (by * cz - bz * cy, bz * cx - bx * cz, bx * cy - by * cx);
                    (ax * cross.0 + ay * cross.1 + az * cross.2) / 6.0
                })
            })
            .sum()
    }

    fn solid_is_manifold(solid: &bone_kernel::BrepSolid) -> bool {
        solid
            .iter_edges()
            .all(|edge| solid.adjacent_faces(edge.id()).len() == 2)
    }

    fn body_volume(model: &EvaluatedModel, body: FeatureId) -> f64 {
        let Some(solid) = model.body(body) else {
            panic!("the body is built");
        };
        solid_volume(solid)
    }

    fn body_is_manifold(model: &EvaluatedModel, body: FeatureId) -> bool {
        let Some(solid) = model.body(body) else {
            panic!("the body is built");
        };
        solid_is_manifold(solid)
    }

    fn body_keys(model: &EvaluatedModel) -> Vec<(FeatureId, [u8; 16])> {
        model
            .bodies()
            .map(|(id, _, solid)| (id, solid.content_key().bytes()))
            .collect()
    }

    #[test]
    fn recompute_honors_build_suppression_and_rollback() {
        let single_chain = || {
            let mut document = Document::new(DocumentId::default(), "doc".to_owned());
            let chain = push_chain(&mut document, "Sketch1", 5.0, 4.0);
            (document, chain)
        };

        let (document, chain) = single_chain();
        let model = full_model(&document);
        assert!(
            model.body(chain.extrude).is_some(),
            "the extrude yields a body"
        );
        assert_eq!(model.status(chain.extrude), Some(RebuildStatus::UpToDate));
        assert_eq!(model.status(chain.sketch), Some(RebuildStatus::UpToDate));
        assert_eq!(model.bodies().count(), 1, "one extrude, one body");

        let (document, chain) = single_chain();
        let mut model = full_model(&document);
        let first = model.built_at(chain.extrude);
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::AtEnd,
            RecomputeScope::Edited(chain.sketch),
        );
        assert_eq!(
            model.built_at(chain.extrude),
            first,
            "an edit scope with no real input change must not re-evaluate the subtree",
        );

        let (document, chain) = single_chain();
        let mut model = EvaluatedModel::new();
        let suppressed = BTreeSet::from([chain.sketch]);
        model.recompute(
            &document,
            &suppressed,
            RollbackMarker::AtEnd,
            RecomputeScope::Full,
        );
        assert!(model.body(chain.extrude).is_none(), "no profile, no body");
        assert_eq!(
            model.status(chain.sketch),
            Some(RebuildStatus::UpToDate),
            "a suppressed feature is not an error",
        );
        assert_eq!(
            model.status(chain.extrude),
            Some(RebuildStatus::UpToDate),
            "recompute closes the suppressed set over descendants, so the child is suppressed not stranded",
        );

        let (mut document, chain) = single_chain();
        document.suppress(chain.sketch);
        let mut model = EvaluatedModel::new();
        model.recompute(
            &document,
            document.suppressed(),
            document.rollback(),
            RecomputeScope::Full,
        );
        assert!(
            model.body(chain.extrude).is_none(),
            "a suppressed chain builds no body",
        );
        assert_eq!(
            model.status(chain.sketch),
            Some(RebuildStatus::UpToDate),
            "a suppressed feature is not an error",
        );
        assert_eq!(
            model.status(chain.extrude),
            Some(RebuildStatus::UpToDate),
            "the cascaded child is suppressed, not a stranded needs-rebuild",
        );

        let (document, chain) = single_chain();
        let mut model = EvaluatedModel::new();
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::Above(chain.extrude),
            RecomputeScope::Full,
        );
        assert!(
            model.body(chain.extrude).is_none(),
            "a rolled-back extrude is not evaluated",
        );
        assert_eq!(model.bodies().count(), 0);
        assert_eq!(model.status(chain.sketch), Some(RebuildStatus::UpToDate));

        let (document, chain) = single_chain();
        let mut model = EvaluatedModel::new();
        let suppressed = BTreeSet::from([chain.extrude]);
        model.recompute(
            &document,
            &suppressed,
            RollbackMarker::Above(chain.extrude),
            RecomputeScope::Full,
        );
        assert!(
            model.body(chain.extrude).is_none(),
            "a feature that is both rolled back and suppressed builds nothing",
        );
        assert_eq!(
            model.status(chain.extrude),
            Some(RebuildStatus::UpToDate),
            "overlapping skip states collapse to a single up-to-date skip, never an error",
        );

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let a = push_chain(&mut document, "Sketch1", 5.0, 4.0);
        let b = push_chain(&mut document, "Sketch2", 6.0, 4.0);
        let mut model = full_model(&document);
        assert!(
            model.body(b.extrude).is_some(),
            "b builds before suppression"
        );
        document.replace_sketch(a.sketch_id, circle_sketch(7.0));
        let suppressed = BTreeSet::from([b.sketch]);
        model.recompute(
            &document,
            &suppressed,
            RollbackMarker::AtEnd,
            RecomputeScope::Edited(a.sketch),
        );
        assert!(
            model.body(b.extrude).is_none(),
            "suppressing a feature outside the edit scope must still strand its body",
        );
        assert_eq!(
            model.status(b.sketch),
            Some(RebuildStatus::UpToDate),
            "a suppressed feature is not an error",
        );
        assert!(
            model.body(a.extrude).is_some(),
            "the edited chain still builds"
        );

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let a = push_chain(&mut document, "Sketch1", 5.0, 4.0);
        let b = push_chain(&mut document, "Sketch2", 6.0, 4.0);
        let mut model = full_model(&document);
        assert!(model.body(b.extrude).is_some(), "b builds before rollback");
        document.replace_sketch(a.sketch_id, circle_sketch(7.0));
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::Above(b.extrude),
            RecomputeScope::Edited(a.sketch),
        );
        assert!(
            model.body(b.extrude).is_none(),
            "rolling back a feature outside the edit scope must still drop its body",
        );
    }

    #[test]
    fn an_unbuildable_profile_reports_a_build_error() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let sketch_id = document.allocate_sketch();
        document.insert_sketch(sketch_id, "Sketch1".to_owned(), open_chain_sketch());
        let extrude_id = document.commit_extrude(blind_extrude(sketch_id, 4.0));
        let chain = chain_handles(&document, sketch_id, extrude_id);
        let model = full_model(&document);

        assert_eq!(
            model.status(chain.sketch),
            Some(RebuildStatus::UpToDate),
            "the open chain solves, only the profile is unbuildable",
        );
        assert_eq!(
            model.status(chain.extrude),
            Some(RebuildStatus::Error(RebuildError::Build(
                BuildFailure::Kernel
            ))),
            "an open profile is a hard build error, not a stale mark",
        );
        assert!(model.body(chain.extrude).is_none());
    }

    proptest! {
        #[test]
        fn rebuilding_twice_is_bit_identical(radius in 1.0f64..20.0, depth in 1.0f64..20.0) {
            let mut document = Document::new(DocumentId::default(), "doc".to_owned());
            push_chain(&mut document, "Sketch1", radius, depth);
            let first = body_keys(&full_model(&document));
            let second = body_keys(&full_model(&document));
            prop_assert_eq!(first, second, "two rebuilds of one document must agree byte for byte");
        }

        #[test]
        fn editing_one_chain_leaves_the_other_untouched(
            radius_a in 1.0f64..10.0,
            radius_b in 1.0f64..10.0,
            edited in 11.0f64..20.0,
        ) {
            let mut document = Document::new(DocumentId::default(), "doc".to_owned());
            let a = push_chain(&mut document, "Sketch1", radius_a, 4.0);
            let b = push_chain(&mut document, "Sketch2", radius_b, 4.0);
            let mut model = full_model(&document);

            let b_sketch_before = model.built_at(b.sketch);
            let b_extrude_before = model.built_at(b.extrude);
            let b_body_before = model.body(b.extrude).map(bone_kernel::BrepSolid::content_key);
            let a_extrude_before = model.built_at(a.extrude);

            document.replace_sketch(a.sketch_id, circle_sketch(edited));
            model.recompute(
                &document,
                &BTreeSet::new(),
                RollbackMarker::AtEnd,
                RecomputeScope::Edited(a.sketch),
            );

            prop_assert_eq!(model.built_at(b.sketch), b_sketch_before, "sibling sketch must not re-evaluate");
            prop_assert_eq!(model.built_at(b.extrude), b_extrude_before, "sibling extrude must not re-evaluate");
            prop_assert_eq!(
                model.body(b.extrude).map(bone_kernel::BrepSolid::content_key),
                b_body_before,
                "the untouched body must stay byte-identical",
            );
            prop_assert!(
                model.built_at(a.extrude) > a_extrude_before,
                "the edited chain's extrude must rebuild",
            );
        }
    }

    #[test]
    #[cfg_attr(
        debug_assertions,
        ignore = "frame budget assertions are only meaningful in release builds"
    )]
    fn single_edit_rebuild_under_frame_budget() {
        use std::time::{Duration, Instant};

        const BASE_MM: f64 = 4.0;
        const STEP_MM: f64 = 0.5;
        const STEPS: u32 = 16;

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let chain = push_chain(&mut document, "Sketch1", 5.0, BASE_MM);
        let mut model = full_model(&document);

        let step = |document: &mut Document, model: &mut EvaluatedModel, radius_mm: f64| {
            document.replace_sketch(chain.sketch_id, circle_sketch(radius_mm));
            let started = Instant::now();
            model.recompute(
                document,
                &BTreeSet::new(),
                RollbackMarker::AtEnd,
                RecomputeScope::Edited(chain.sketch),
            );
            let elapsed = started.elapsed();
            assert!(
                model.body(chain.extrude).is_some(),
                "the edit rebuilds a body"
            );
            elapsed
        };

        let _warmup = step(&mut document, &mut model, 5.0);
        let durations: Vec<Duration> = (0..STEPS)
            .map(|i| step(&mut document, &mut model, 5.0 + STEP_MM * f64::from(i)))
            .collect();
        let sorted = {
            let mut v = durations.clone();
            v.sort_unstable();
            v
        };
        let median = sorted[sorted.len() / 2];
        let budget = Duration::from_millis(16);
        assert!(
            median <= budget,
            "median single-edit recompute {median:?} exceeds {budget:?} frame budget; samples {durations:?}",
        );
    }

    #[test]
    fn a_face_bound_chain_builds_then_tracks_and_dangles() {
        let (mut document, base, _probe, cap) = chain_with_cap(6.0, 4.0);
        let top = push_face_bound_chain(&mut document, "Top", cap, 3.0, 5.0);
        let mut model = full_model(&document);

        let descendants = document.feature_tree().descendants(base.extrude);
        assert!(
            descendants.contains(&top.sketch) && descendants.contains(&top.extrude),
            "the face binding records a dependency from the cap's body onto the new chain",
        );
        assert_eq!(model.status(top.sketch), Some(RebuildStatus::UpToDate));
        assert!(model.body(top.extrude).is_some());
        assert_eq!(model.bodies().count(), 2, "two separate bodies, no boolean");
        let cap_z = body_max_z(&model, base.extrude);
        assert!(
            (body_min_z(&model, top.extrude) - cap_z).abs() < 1.0e-6,
            "the derived plane sits on the cap so the second body rises from it",
        );

        let before = model.built_at(top.extrude);
        document.insert_extrude(base.extrude_id, blind_extrude(base.sketch_id, 7.0));
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::AtEnd,
            RecomputeScope::Edited(base.extrude),
        );
        assert!(
            model.built_at(top.extrude) > before,
            "deepening the base re-resolves the face and rebuilds the downstream body",
        );
        assert!((body_max_z(&model, base.extrude) - 7.0).abs() < 1.0e-6);
        assert!(
            (body_min_z(&model, top.extrude) - 7.0).abs() < 1.0e-6,
            "the face-bound body follows the cap to its new height",
        );

        assert!(model.body(top.extrude).is_some(), "the chain builds first");
        document.remove_extrude(base.extrude_id);
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::AtEnd,
            RecomputeScope::Full,
        );
        assert!(
            matches!(
                model.status(top.sketch),
                Some(RebuildStatus::Error(RebuildError::DanglingReference(_)))
            ),
            "a removed cap leaves the face-bound sketch dangling, never a silent wrong plane",
        );
        assert_eq!(
            model.status(top.extrude),
            Some(RebuildStatus::Error(RebuildError::UpstreamUnresolved)),
            "the downstream extrude fails as upstream-unresolved, not as a dangling owner",
        );
        assert!(model.body(top.extrude).is_none());
    }

    #[test]
    fn a_merge_boss_unions_into_the_base_body() {
        let (mut document, _base, _probe, cap) = box_with_cap(4.0, 2.0);
        let boss = push_face_bound_box_merge(&mut document, "Boss", cap, 4.0, 3.0);
        let model = full_model(&document);

        assert!(
            model.body(boss.extrude).is_some(),
            "the boss node owns the union"
        );
        assert_eq!(
            model.bodies().count(),
            1,
            "a merging boss unions into the in-scope body",
        );
        let tip = body_max_z(&model, boss.extrude);
        assert!((tip - 5.0).abs() < 1.0e-6, "the union spans both heights");
        assert!((body_min_z(&model, boss.extrude) - 0.0).abs() < 1.0e-6);
    }

    #[test]
    fn face_bound_cuts_carve_their_expected_volume() {
        use core::f64::consts::PI;
        type FaceBoundCutCase = (
            fn(&mut Document, FaceRef) -> Chain,
            fn(&Document, &EvaluatedModel, FeatureId),
        );
        let cases: [FaceBoundCutCase; 3] = [
            (
                |document, cap| push_face_bound_box_cut(document, "Pocket", cap, 1.0, 2.0, 1.0),
                |document, model, extrude| {
                    assert!(
                        model.body(extrude).is_some(),
                        "the cut node owns the carved body"
                    );
                    assert_eq!(
                        model.bodies().count(),
                        1,
                        "a cut subtracts into the in-scope body rather than adding a second",
                    );
                    assert!(
                        body_is_manifold(model, extrude),
                        "the carved body is a closed manifold",
                    );
                    assert!(
                        (body_volume(model, extrude) - 28.0).abs() < 1.0e-4,
                        "a thirty-two unit block loses a two-by-two-by-one pocket",
                    );
                    assert!((body_max_z(model, extrude) - 2.0).abs() < 1.0e-6);
                    assert!((body_min_z(model, extrude) - 0.0).abs() < 1.0e-6);
                    let again = full_model(document);
                    assert_eq!(
                        model.body(extrude).map(|s| s.content_key().bytes()),
                        again.body(extrude).map(|s| s.content_key().bytes()),
                        "the carved body is byte-identical across rebuilds",
                    );
                },
            ),
            (
                |document, cap| push_face_bound_box_cut_flip(document, "Keep", cap, 1.0, 2.0, 1.0),
                |_document, model, extrude| {
                    assert!(
                        model.body(extrude).is_some(),
                        "the flip-side cut owns the kept overlap"
                    );
                    assert_eq!(
                        model.bodies().count(),
                        1,
                        "a flip-side cut keeps the overlap in place of the base",
                    );
                    assert!(
                        body_is_manifold(model, extrude),
                        "the kept overlap is a closed manifold",
                    );
                    assert!(
                        (body_volume(model, extrude) - 4.0).abs() < 1.0e-4,
                        "flip side keeps only the two-by-two-by-one column inside the profile, got {}",
                        body_volume(model, extrude),
                    );
                },
            ),
            (
                |document, cap| {
                    push_face_bound_circle_cut(document, "Pocket", cap, 2.0, 2.0, 1.0, 1.0)
                },
                |document, model, extrude| {
                    assert_eq!(
                        model.bodies().count(),
                        1,
                        "a circle cut subtracts into the in-scope body",
                    );
                    assert!(
                        body_is_manifold(model, extrude),
                        "the carved body is a closed manifold",
                    );
                    assert!(
                        (body_volume(model, extrude) - (32.0 - PI)).abs() < 1.0e-2,
                        "a thirty-two unit block loses a unit-radius unit-deep round plug",
                    );
                    assert!((body_max_z(model, extrude) - 2.0).abs() < 1.0e-6);
                    assert!((body_min_z(model, extrude) - 0.0).abs() < 1.0e-6);
                    let again = full_model(document);
                    assert_eq!(
                        model.body(extrude).map(|s| s.content_key().bytes()),
                        again.body(extrude).map(|s| s.content_key().bytes()),
                        "the carved body is byte-identical across rebuilds",
                    );
                },
            ),
        ];
        cases.iter().for_each(|&(apply, check)| {
            let (mut document, _base, _probe, cap) = box_with_cap(4.0, 2.0);
            let cut = apply(&mut document, cap);
            let model = full_model(&document);
            check(&document, &model, cut.extrude);
        });
    }

    #[test]
    fn a_cut_on_a_face_inherited_through_a_merge_carves_the_live_body() {
        let (mut document, base, probe, top) = box_with_cap(4.0, 2.0);
        let side = side_face_ref(&probe, base.extrude);
        let _boss = push_face_bound_box_merge(&mut document, "Boss", top, 4.0, 3.0);
        let cut = push_face_bound_box_cut(&mut document, "Cut", side, 1.0, 1.0, 1.0);
        let model = full_model(&document);

        assert_eq!(
            model.bodies().count(),
            1,
            "the cut carves the merged tower, not a stale pre-merge copy of the base",
        );
        assert!(
            model.body(cut.extrude).is_some(),
            "the cut node owns the live carved body",
        );
        assert!(
            body_is_manifold(&model, cut.extrude),
            "the carved tower is a closed manifold",
        );
        assert!(
            (body_max_z(&model, cut.extrude) - 5.0).abs() < 1.0e-6,
            "the carved body is the five-tall tower, not the two-tall pre-merge base",
        );
    }

    fn push_stacked_bodies(document: &mut Document) -> (Chain, Chain) {
        let lower = push_box(document, "Lower", 4.0, 4.0);
        let upper_sketch = document.allocate_sketch();
        document.insert_sketch(upper_sketch, "Upper".to_owned(), box_sketch(4.0));
        let upper_id = document.commit_extrude(from_offset_boss(upper_sketch, 6.0, 4.0));
        let upper = chain_handles(document, upper_sketch, upper_id);
        (lower, upper)
    }

    fn push_side_by_side_bodies(document: &mut Document) -> (Chain, Chain) {
        let left = push_box(document, "Left", 4.0, 4.0);
        let right_sketch = document.allocate_sketch();
        document.insert_sketch(
            right_sketch,
            "Right".to_owned(),
            box_sketch_at(6.0, 0.0, 4.0),
        );
        let right_id = document.commit_extrude(blind_extrude(right_sketch, 4.0));
        let right = chain_handles(document, right_sketch, right_id);
        (left, right)
    }

    fn side_by_side_all_bodies_cut(document: &mut Document) -> Chain {
        use crate::test_support::rect_sketch_at;
        let cut_sketch = document.allocate_sketch();
        document.insert_sketch(
            cut_sketch,
            "Cut".to_owned(),
            rect_sketch_at(1.0, 1.0, 8.0, 2.0),
        );
        let cut_id = document.commit_extrude(ExtrudeFeature {
            scope: FeatureScope::AllBodies,
            from: FromCondition::Offset {
                distance: Length::new::<millimeter>(2.0),
            },
            ..cut_extrude(cut_sketch, 2.0)
        });
        chain_handles(document, cut_sketch, cut_id)
    }

    #[test]
    fn an_all_bodies_cut_carves_every_in_scope_body() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let (left, right) = push_side_by_side_bodies(&mut document);
        let cut = side_by_side_all_bodies_cut(&mut document);
        let model = full_model(&document);

        let owned: Vec<(FeatureId, BodyOrdinal)> = model
            .bodies()
            .map(|(feature, ordinal, _)| (feature, ordinal))
            .collect();
        assert_eq!(
            owned.len(),
            2,
            "an all-bodies cut spanning two disjoint bodies carves both, never just the one",
        );
        assert!(
            owned.iter().all(|(feature, _)| *feature == cut.extrude),
            "both carved bodies belong to the cut feature that produced them",
        );
        let ordinals: BTreeSet<u16> = owned.iter().map(|(_, ordinal)| ordinal.value()).collect();
        assert_eq!(
            ordinals,
            BTreeSet::from([0, 1]),
            "the two bodies of the one feature carry stable, distinct ordinals",
        );

        let live: BTreeSet<FeatureId> = owned.iter().map(|(feature, _)| *feature).collect();
        assert!(
            !live.contains(&left.extrude) && !live.contains(&right.extrude),
            "both base bodies are consumed by the cut, not left dangling as live arenas",
        );
        assert!(
            model.bodies().all(|(_, _, solid)| solid_is_manifold(solid)),
            "every carved body is a closed manifold",
        );
        let total: f64 = model
            .bodies()
            .map(|(_, _, solid)| solid_volume(solid))
            .sum();
        assert!(
            (total - 104.0).abs() < 1.0e-2,
            "two 64-unit blocks each lose a 12-unit pocket, leaving 104 total, got {total}",
        );
    }

    #[test]
    fn a_stacked_body_extrude_auto_selects_its_host_or_folds_every_body() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let (lower, upper) = push_stacked_bodies(&mut document);
        let cut_sketch = document.allocate_sketch();
        document.insert_sketch(cut_sketch, "Cut".to_owned(), box_sketch_at(1.0, 1.0, 2.0));
        let cut_id = document.commit_extrude(cut_extrude(cut_sketch, 12.0));
        let cut = chain_handles(&document, cut_sketch, cut_id);
        let model = full_model(&document);
        assert_eq!(
            model.status(cut.extrude),
            Some(RebuildStatus::Error(RebuildError::Build(
                BuildFailure::CutWithoutBody
            ))),
            "auto-select keeps the cut on its own host, so a plane-bound cut finds no body to carve",
        );
        let live: BTreeSet<FeatureId> = model.bodies().map(|(feature, _, _)| feature).collect();
        assert_eq!(
            live,
            BTreeSet::from([lower.extrude, upper.extrude]),
            "auto-select never reaches across to the other in-scope bodies the way all-bodies does",
        );

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let (lower, upper) = push_stacked_bodies(&mut document);
        let bridge_sketch = document.allocate_sketch();
        document.insert_sketch(bridge_sketch, "Bridge".to_owned(), box_sketch(4.0));
        let bridge_id = document.commit_extrude(ExtrudeFeature {
            scope: FeatureScope::AllBodies,
            from: FromCondition::Offset {
                distance: Length::new::<millimeter>(4.0),
            },
            ..merge_extrude(bridge_sketch, 2.0)
        });
        let bridge = chain_handles(&document, bridge_sketch, bridge_id);
        let model = full_model(&document);

        let owned: Vec<FeatureId> = model.bodies().map(|(feature, _, _)| feature).collect();
        assert_eq!(
            owned,
            vec![bridge.extrude],
            "an all-bodies merge folds the bridge and both stacked bodies into one body it owns",
        );
        assert!(
            !owned.contains(&lower.extrude) && !owned.contains(&upper.extrude),
            "both stacked base bodies are consumed by the fold",
        );
        assert!(
            body_is_manifold(&model, bridge.extrude),
            "the folded body is a closed manifold"
        );
        assert!(
            (body_volume(&model, bridge.extrude) - 160.0).abs() < 1.0e-1,
            "a 32-unit bridge fuses two 64-unit blocks across the gap into one 160-unit column",
        );
    }

    fn box_bored_by_revolved_tube(
        document: &mut Document,
        bore_label: &str,
        cut_of: impl FnOnce(bone_types::SketchId, bone_types::SketchEntityId, f64) -> RevolveFeature,
    ) -> (Chain, FeatureId) {
        use crate::test_support::{box_sketch_at, revolve_z_axis_tube_sketch};
        let base_sketch = document.allocate_sketch();
        document.insert_sketch(
            base_sketch,
            "Base".to_owned(),
            box_sketch_at(-3.0, -3.0, 6.0),
        );
        let base_id = document.commit_extrude(blind_extrude(base_sketch, 5.0));
        let base = chain_handles(document, base_sketch, base_id);
        let (tube, axis) = revolve_z_axis_tube_sketch(1.0, 2.0, -1.0, 3.0);
        let cut_sketch = document.allocate_sketch();
        document.insert_sketch(cut_sketch, bore_label.to_owned(), tube);
        let cut_id = document.commit_revolve(cut_of(cut_sketch, axis, 360.0));
        let Some(cut) = document.feature_tree().feature_of_revolve(cut_id) else {
            panic!("the revolve cut has a feature node");
        };
        (base, cut)
    }

    #[test]
    fn a_revolve_cut_across_bodies_carves_or_keeps_the_overlap() {
        use crate::test_support::{cut_revolve, flip_side_cut_revolve};
        use core::f64::consts::PI;

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let (base, cut) = box_bored_by_revolved_tube(&mut document, "Bore", cut_revolve);
        let model = full_model(&document);
        let owned: Vec<FeatureId> = model.bodies().map(|(feature, _, _)| feature).collect();
        assert_eq!(
            owned,
            vec![cut],
            "an all-bodies revolve cut consumes the box and owns the carved body",
        );
        assert!(
            !owned.contains(&base.extrude),
            "the base box is consumed by the revolve cut, not left dangling",
        );
        assert!(
            body_is_manifold(&model, cut),
            "the annular-bored box is a closed manifold",
        );
        let expected = 180.0 - PI * (2.0 * 2.0 - 1.0 * 1.0) * 3.0;
        assert!(
            (body_volume(&model, cut) - expected).abs() < 1.0e-1,
            "the box loses the annular groove the revolved tube sweeps: got {}, want {expected}",
            body_volume(&model, cut),
        );

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let (_base, cut) = box_bored_by_revolved_tube(&mut document, "Keep", flip_side_cut_revolve);
        let model = full_model(&document);
        assert_eq!(
            model.bodies().count(),
            1,
            "a flip-side revolve cut keeps the overlap in place of the base",
        );
        assert!(
            body_is_manifold(&model, cut),
            "the kept overlap ring is a closed manifold",
        );
        let expected = PI * (2.0 * 2.0 - 1.0 * 1.0) * 3.0;
        assert!(
            (body_volume(&model, cut) - expected).abs() < 1.0e-1,
            "flip side keeps only the annular ring the base and revolved tool share: got {}, want {expected}",
            body_volume(&model, cut),
        );
    }

    #[test]
    fn an_all_bodies_cut_face_resolves_to_the_body_its_ordinal_names()
    -> Result<(), Box<dyn std::error::Error>> {
        use crate::matcher::{ResolvedEntity, resolve};
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let _bases = push_side_by_side_bodies(&mut document);
        let cut = side_by_side_all_bodies_cut(&mut document);
        let model = full_model(&document);

        let first = BodyOrdinal::new(0);
        let second = BodyOrdinal::new(1);
        let solid0 = model
            .body_at(cut.extrude, first)
            .ok_or("the cut owns a first carved body")?;
        let solid1 = model
            .body_at(cut.extrude, second)
            .ok_or("the cut owns a second carved body")?;
        let labels1: Vec<_> = solid1.iter_faces().map(BrepFace::label).collect();
        let (shared, id0) = solid0
            .iter_faces()
            .find_map(|face| {
                labels1
                    .contains(&face.label())
                    .then_some((face.label(), face.id()))
            })
            .ok_or("a cut-tool face carries the identical label in both carved bodies")?;
        let id1 = solid1
            .iter_faces()
            .find_map(|face| (face.label() == shared).then_some(face.id()))
            .ok_or("the shared label names a face in the second body too")?;

        let ref0 = model
            .face_ref_at(cut.extrude, first, id0)
            .ok_or("the first carved-body face yields a reference")?;
        let ref1 = model
            .face_ref_at(cut.extrude, second, id1)
            .ok_or("the second carved-body face yields a reference")?;
        assert_eq!(
            ref0.label, ref1.label,
            "the two bodies share one label, so only the ordinal can tell them apart",
        );
        assert_eq!((ref0.ordinal, ref1.ordinal), (first, second));

        let Resolution::Resolved(ResolvedEntity::Face(at0)) = resolve(&model, ref0.entity_ref())
        else {
            panic!("the first-body reference resolves");
        };
        let Resolution::Resolved(ResolvedEntity::Face(at1)) = resolve(&model, ref1.entity_ref())
        else {
            panic!("the second-body reference resolves");
        };
        assert_eq!(
            (at0.ordinal(), at1.ordinal()),
            (first, second),
            "the same label under different ordinals resolves into the bodies the ordinals name",
        );

        let Resolution::Resolved(ResolvedEntity::Face(origin)) =
            resolve(&model, ref1.at(BodyOrdinal::origin()).entity_ref())
        else {
            panic!("an origin reference falls back to the first matching body");
        };
        assert_eq!(
            origin.ordinal(),
            first,
            "an origin ordinal lands in the first body, never dangling on the now-ambiguous label",
        );

        assert!(
            resolve(&model, ref1.at(BodyOrdinal::new(7)).entity_ref()).is_dangling(),
            "a label shared across bodies refuses to resolve under an ordinal that names neither",
        );
        Ok(())
    }

    #[test]
    fn an_all_bodies_cut_rebuilds_when_a_prior_body_changes()
    -> Result<(), Box<dyn std::error::Error>> {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let (left, _right) = push_side_by_side_bodies(&mut document);
        let cut = side_by_side_all_bodies_cut(&mut document);
        let mut model = full_model(&document);

        let before = model.built_at(cut.extrude).ok_or("the cut builds")?;
        let before_count = model
            .bodies()
            .filter(|(feature, _, _)| *feature == cut.extrude)
            .count();
        assert_eq!(
            before_count, 2,
            "the cut starts out owning both carved bodies"
        );

        document.replace_sketch(left.sketch_id, box_sketch(5.0));
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::AtEnd,
            RecomputeScope::Edited(left.sketch),
        );

        let after_cut = model.built_at(cut.extrude).ok_or("the cut still builds")?;
        assert_ne!(
            after_cut, before,
            "an all-bodies cut depends on every prior body, so editing one re-dirties the cut",
        );
        assert_eq!(
            Some(after_cut),
            model.built_at(left.extrude),
            "the cut rebuilds in the same pass as the prior body it carves, never one pass stale",
        );
        let after_count = model
            .bodies()
            .filter(|(feature, _, _)| *feature == cut.extrude)
            .count();
        assert_eq!(
            after_count, 2,
            "the rebuilt cut still owns both carved bodies"
        );
        Ok(())
    }

    #[test]
    fn boss_end_conditions_land_their_expected_solid() {
        type BossEndConditionCase = (
            f64,
            fn(bone_types::SketchId) -> ExtrudeFeature,
            fn(&Document, &EvaluatedModel, FeatureId),
        );
        let cases: [BossEndConditionCase; 4] = [
            (
                4.0,
                |sketch| two_direction_boss(sketch, 3.0, 2.0),
                |_document, model, extrude| {
                    assert!(model.body(extrude).is_some());
                    assert!((body_max_z(model, extrude) - 3.0).abs() < 1.0e-6);
                    assert!((body_min_z(model, extrude) + 2.0).abs() < 1.0e-6);
                    assert!(
                        (body_volume(model, extrude) - 80.0).abs() < 1.0e-4,
                        "a four-square plate over five units of total height is eighty units",
                    );
                },
            ),
            (
                4.0,
                |sketch| from_offset_boss(sketch, 1.0, 3.0),
                |_document, model, extrude| {
                    assert!((body_min_z(model, extrude) - 1.0).abs() < 1.0e-6);
                    assert!((body_max_z(model, extrude) - 4.0).abs() < 1.0e-6);
                    assert!((body_volume(model, extrude) - 48.0).abs() < 1.0e-4);
                },
            ),
            (
                10.0,
                |sketch| draft_boss(sketch, 6.0, 45.0),
                |document, model, extrude| {
                    assert!(model.body(extrude).is_some(), "the drafted boss builds");
                    assert!((body_min_z(model, extrude) - 0.0).abs() < 1.0e-6);
                    assert!((body_max_z(model, extrude) - 6.0).abs() < 1.0e-6);
                    assert!(
                        (body_volume(model, extrude) - 1608.0).abs() < 1.0e-3,
                        "outward 45-degree draft over depth 6 widens 10 to 22, a truncated pyramid",
                    );
                    let again = full_model(document);
                    assert_eq!(
                        model.body(extrude).map(|s| s.content_key().bytes()),
                        again.body(extrude).map(|s| s.content_key().bytes()),
                        "the drafted body is byte-identical across rebuilds",
                    );
                },
            ),
            (
                10.0,
                |sketch| thin_boss(sketch, 5.0, 2.0),
                |_document, model, extrude| {
                    assert!(model.body(extrude).is_some(), "the thin boss builds");
                    assert!(
                        body_is_manifold(model, extrude),
                        "the tube is a closed manifold",
                    );
                    assert!(
                        (body_volume(model, extrude) - 320.0).abs() < 1.0e-3,
                        "a 2 mm inward wall on a 10 mm square over depth 5 leaves a 64-area ring",
                    );
                },
            ),
        ];
        cases.iter().for_each(|&(side_mm, feature, check)| {
            let (document, model, chain) = plate_scene(side_mm, feature);
            check(&document, &model, chain.extrude);
        });
    }

    #[test]
    fn face_bound_cut_end_conditions_carve_or_refuse() {
        enum Outcome {
            Carves,
            Refused,
        }
        type FaceBoundEndCase = (
            &'static str,
            fn(&EvaluatedModel, FeatureId) -> ExtrudeEndCondition,
            Outcome,
        );
        let cases: [FaceBoundEndCase; 6] = [
            (
                "through all",
                |_probe, _base| ExtrudeEndCondition::ThroughAll,
                Outcome::Carves,
            ),
            (
                "up to the floor surface",
                |probe, base| ExtrudeEndCondition::UpToSurface {
                    surface: start_cap_label(probe, base),
                },
                Outcome::Carves,
            ),
            (
                "up to the next face",
                |_probe, _base| ExtrudeEndCondition::UpToNext,
                Outcome::Carves,
            ),
            (
                "up to a parallel wall surface",
                |probe, base| ExtrudeEndCondition::UpToSurface {
                    surface: side_face_label(probe, base),
                },
                Outcome::Refused,
            ),
            (
                "up to the floor vertex",
                |probe, base| ExtrudeEndCondition::UpToVertex {
                    vertex: start_cap_vertex_label(probe, base),
                },
                Outcome::Carves,
            ),
            (
                "through all both",
                |_probe, _base| ExtrudeEndCondition::ThroughAllBoth,
                Outcome::Carves,
            ),
        ];
        cases.into_iter().for_each(|(label, end, outcome)| {
            let (mut document, base, probe, cap) = box_with_cap(4.0, 2.0);
            let condition = end(&probe, base.extrude);
            let bore =
                push_face_bound_box_cut_with(&mut document, "Bore", cap, 1.0, 2.0, condition);
            let model = full_model(&document);
            match outcome {
                Outcome::Refused => {
                    assert_eq!(
                        model.status(bore.extrude),
                        Some(RebuildStatus::Error(RebuildError::Build(BuildFailure::Kernel))),
                        "{label}: a target face parallel to the extrude direction has no well-defined reach",
                    );
                    assert!(model.body(bore.extrude).is_none());
                }
                Outcome::Carves => {
                    assert!(
                        model.body(bore.extrude).is_some(),
                        "{label}: the cut owns a carved body",
                    );
                    assert_eq!(
                        model.bodies().count(),
                        1,
                        "{label}: the cut stays in the base body",
                    );
                    assert!(
                        body_is_manifold(&model, bore.extrude),
                        "{label}: the pierced body is a closed manifold",
                    );
                    assert!(
                        (body_volume(&model, bore.extrude) - 24.0).abs() < 1.0e-4,
                        "{label}: the cut clears a two-by-two column over the full height, got {}",
                        body_volume(&model, bore.extrude),
                    );
                    assert!((body_max_z(&model, bore.extrude) - 2.0).abs() < 1.0e-6);
                    assert!((body_min_z(&model, bore.extrude) - 0.0).abs() < 1.0e-6);
                    let again = full_model(&document);
                    assert_eq!(
                        model.body(bore.extrude).map(|s| s.content_key().bytes()),
                        again.body(bore.extrude).map(|s| s.content_key().bytes()),
                        "{label}: the carved body is byte-identical across rebuilds",
                    );
                }
            }
        });
    }

    #[test]
    fn an_extrude_contour_selection_builds_only_the_named_region()
    -> Result<(), Box<dyn std::error::Error>> {
        use core::f64::consts::PI;
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let (sketch, _axis, first) = revolve_two_contour_sketch();
        let sketch_id = document.allocate_sketch();
        document.insert_sketch(sketch_id, "Profile".to_owned(), sketch);
        let feature = ExtrudeFeature {
            contours: bone_kernel::ContourSelection::Contour(first),
            ..crate::test_support::blind_extrude(sketch_id, 4.0)
        };
        let extrude_id = document.commit_extrude(feature);
        let extrude = document
            .feature_tree()
            .feature_of_extrude(extrude_id)
            .ok_or("extrude has a feature node")?;
        let model = full_model(&document);

        assert!(
            model.body(extrude).is_some(),
            "a single picked contour builds where two disjoint regions cannot",
        );
        assert!(
            (body_volume(&model, extrude) - PI * 4.0).abs() < 0.05,
            "the one selected radius-1 circle extrudes four deep, got {}",
            body_volume(&model, extrude),
        );
        Ok(())
    }

    #[test]
    fn a_cut_without_a_base_body_reports_a_typed_failure() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let sketch_id = document.allocate_sketch();
        document.insert_sketch(
            sketch_id,
            "Cut".to_owned(),
            crate::test_support::box_sketch(2.0),
        );
        let extrude_id = document.commit_extrude(crate::test_support::cut_extrude(sketch_id, 1.0));
        let chain = chain_handles(&document, sketch_id, extrude_id);
        let model = full_model(&document);

        assert_eq!(
            model.status(chain.extrude),
            Some(RebuildStatus::Error(RebuildError::Build(
                BuildFailure::CutWithoutBody
            ))),
            "a cut with no body to remove material from fails honestly, never a phantom body",
        );
        assert!(model.body(chain.extrude).is_none());
        assert_eq!(model.bodies().count(), 0);
    }

    #[test]
    fn a_non_planar_target_is_a_typed_rebuild_error() {
        let (mut document, base, probe, _cap) = chain_with_cap(6.0, 4.0);
        let side = side_face_label(&probe, base.extrude);
        let top = push_face_bound_chain(
            &mut document,
            "Top",
            FaceRef::new(side, placeholder_fingerprint()),
            1.0,
            2.0,
        );
        let model = full_model(&document);

        assert_eq!(
            model.status(top.sketch),
            Some(RebuildStatus::Error(RebuildError::NonPlanarSketchTarget)),
            "a sketch bound to a cylindrical side fails with the non-planar contract",
        );
        assert!(model.body(top.extrude).is_none());
    }

    #[test]
    fn rebuild_cost_sums_topology_and_gates_on_the_budget() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        push_chain(&mut document, "Sketch1", 5.0, 4.0);
        let model = full_model(&document);
        let cost = model.rebuild_cost();

        assert!(
            cost > RebuildCost::default(),
            "a built body contributes faces and edges to the cost",
        );
        assert!(
            cost.within(RebuildBudget::INTERACTIVE),
            "a single extrude stays under the interactive budget, so the rebuild is eager",
        );
        assert!(
            !cost.within(RebuildBudget::new(0)),
            "a zero budget forces the deferred path for any non-empty model",
        );
        assert_eq!(
            EvaluatedModel::new().rebuild_cost(),
            RebuildCost::default(),
            "an empty model costs nothing to rebuild",
        );
    }

    proptest! {
        #[test]
        fn the_face_bound_body_tracks_the_cap_across_depth_edits(
            base_depth in 2.0f64..12.0,
            edited_depth in 2.0f64..12.0,
        ) {
            let (mut document, base, _probe, cap) = chain_with_cap(6.0, base_depth);
            let top = push_face_bound_chain(&mut document, "Top", cap, 3.0, 5.0);
            let mut model = full_model(&document);

            document.insert_extrude(base.extrude_id, blind_extrude(base.sketch_id, edited_depth));
            model.recompute(
                &document,
                &BTreeSet::new(),
                RollbackMarker::AtEnd,
                RecomputeScope::Edited(base.extrude),
            );

            prop_assert!(
                (body_min_z(&model, top.extrude) - edited_depth).abs() < 1.0e-6,
                "the face-bound body always rises from the current cap height",
            );
        }
    }

    fn sketch3_entity(
        sketch: &Sketch3,
        entity: Sketch3Entity,
    ) -> (Sketch3, bone_types::SketchEntityId) {
        let (next, outcome) = sketch.apply(Sketch3Edit::AddEntity(entity));
        let Edit3Outcome::Entity(id) = outcome else {
            panic!("adding an entity yields an entity id");
        };
        (next, id)
    }

    struct Sketch3Handles {
        sketch: Sketch3,
        start: bone_types::SketchEntityId,
        end: bone_types::SketchEntityId,
    }

    fn along_x_handles() -> Sketch3Handles {
        let (sketch, start) =
            sketch3_entity(&Sketch3::new(), Sketch3Entity::point(Point3::origin()));
        let (sketch, end) = sketch3_entity(
            &sketch,
            Sketch3Entity::point(Point3::from_mm(5.0, 2.7, -1.3)),
        );
        let (sketch, segment) = sketch3_entity(&sketch, Sketch3Entity::line(start, end, false));
        let edits = [
            Sketch3Edit::AddRelation(Sketch3Relation::Fix(start)),
            Sketch3Edit::AddRelation(Sketch3Relation::AlongAxis {
                line: segment,
                axis: UnitVec3::x_axis(),
            }),
        ];
        let (sketch, _) = sketch.apply_all(edits);
        Sketch3Handles { sketch, start, end }
    }

    fn along_x_sketch3(distance_mm: f64) -> (Sketch3, bone_types::SketchEntityId) {
        let handles = along_x_handles();
        let (sketch, _) =
            handles
                .sketch
                .apply(Sketch3Edit::AddRelation(Sketch3Relation::Distance {
                    a: handles.start,
                    b: handles.end,
                    value: Length::new::<millimeter>(distance_mm),
                    reference: false,
                }));
        (sketch, handles.end)
    }

    fn evaluated_end(
        model: &EvaluatedModel,
        feature: FeatureId,
        end: bone_types::SketchEntityId,
    ) -> (f64, f64, f64) {
        let Some(solved) = model.evaluated_sketch3(feature) else {
            panic!("the sketch3 feature solves");
        };
        let Some(point) = solved.point_position(end) else {
            panic!("the solved sketch3 carries the end point");
        };
        point.coords_mm()
    }

    #[test]
    fn a_sketch3_solves_deterministically_and_re_solves_on_edit()
    -> Result<(), Box<dyn std::error::Error>> {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let (sketch3, end) = along_x_sketch3(7.0);
        let id = document.allocate_sketch();
        document.insert_sketch(id, "Sketch3D".to_owned(), sketch3);
        let feature = document
            .feature_tree()
            .feature_of_sketch(id)
            .ok_or("the sketch3 has a feature node")?;
        let first = full_model(&document);
        assert_eq!(first.status(feature), Some(RebuildStatus::UpToDate));
        let (x, y, z) = evaluated_end(&first, feature, end);
        assert!((x - 7.0).abs() < 1.0e-6, "the line lays 7 mm along x: {x}");
        assert!(y.abs() < 1.0e-6, "the end snaps onto the start y: {y}");
        assert!(z.abs() < 1.0e-6, "the end snaps onto the start z: {z}");
        let second = full_model(&document);
        assert_eq!(
            evaluated_end(&first, feature, end),
            evaluated_end(&second, feature, end),
            "two full recomputes of the same sketch3 land the point identically",
        );

        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let handles = along_x_handles();
        let (under_constrained, start, end) = (handles.sketch, handles.start, handles.end);
        let id = document.allocate_sketch();
        document.insert_sketch(id, "Sketch3D".to_owned(), under_constrained.clone());
        let feature = document
            .feature_tree()
            .feature_of_sketch(id)
            .ok_or("the sketch3 has a feature node")?;
        let mut model = full_model(&document);
        let built_at = model.built_at(feature);
        let (before, _, _) = evaluated_end(&model, feature, end);
        assert!(
            (before - 5.0).abs() < 1.0e-6,
            "without a distance the free end keeps its seed x: {before}",
        );

        let (constrained, _) =
            under_constrained.apply(Sketch3Edit::AddRelation(Sketch3Relation::Distance {
                a: start,
                b: end,
                value: Length::new::<millimeter>(11.0),
                reference: false,
            }));
        document.replace_sketch(id, constrained);
        model.recompute(
            &document,
            &BTreeSet::new(),
            RollbackMarker::AtEnd,
            RecomputeScope::Edited(feature),
        );

        assert_ne!(
            model.built_at(feature),
            built_at,
            "adding a relation re-dirties the sketch3 node",
        );
        let (after, _, _) = evaluated_end(&model, feature, end);
        assert!(
            (after - 11.0).abs() < 1.0e-6,
            "the added distance relation re-solves the line to 11 mm: {after}",
        );
        Ok(())
    }
}
