ifc-geometry 0.10.0

IFC semantic views lowered into the format-neutral geometry DAG.
Documentation
//! Proof that the kernel-free build is real, not decorative.
//!
//! A 2D consumer selects a representation, applies a transform and reads
//! units. None of that needs a geometry kernel, but a single unconditional
//! `use axiolid_core` anywhere in the crate silently relinks all of it --
//! and nothing in a normal test run would notice, because the default build
//! links the kernel anyway.
//!
//! This checks the **resolved dependency graph** under
//! `--no-default-features`, not the manifest: an optional dependency can be
//! re-enabled by accident through a feature edge, and only the resolver
//! knows. Same technique as `openbim-ifc`'s `thin_build.rs`.

use std::process::Command;

/// Every crate linked into `ifc-geometry` for a given feature selection.
fn dependency_tree(extra: &[&str]) -> String {
    let manifest = concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml");
    let mut cmd = Command::new(env!("CARGO"));
    cmd.args([
        "tree",
        "--manifest-path",
        manifest,
        "--edges",
        "normal",
        "--prefix",
        "none",
    ]);
    cmd.args(extra);
    let out = cmd.output().expect("cargo tree should run");
    assert!(
        out.status.success(),
        "cargo tree failed: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    String::from_utf8(out.stdout).expect("tree output is utf-8")
}

fn links(tree: &str, crate_name: &str) -> bool {
    tree.lines()
        .filter_map(|line| line.split_whitespace().next())
        .any(|name| name == crate_name)
}

/// The neutral geometry crates, plus the math library they all pull.
///
/// `axiolid-mesh` and `axiolid-surface` are not direct dependencies but
/// arrive transitively through `axiolid-model`, so a check that only listed
/// the direct ones would miss most of the weight.
const KERNEL_CRATES: &[&str] = &[
    "axiolid-core",
    "axiolid-curve",
    "axiolid-mesh",
    "axiolid-model",
    "axiolid-primitive",
    "axiolid-profile",
    "axiolid-surface",
    "axiolid-topology",
    "glam",
];

/// Without `lowering`, not one geometry crate may be linked.
#[test]
fn the_kernel_free_build_links_no_geometry_crate() {
    let tree = dependency_tree(&["--no-default-features"]);

    for forbidden in KERNEL_CRATES {
        assert!(
            !links(&tree, forbidden),
            "a kernel-free build links {forbidden}. Something outside `lower` \
             uses the neutral vocabulary unconditionally; gate it behind \
             `#[cfg(feature = \"lowering\")]`.\n{tree}"
        );
    }

    // ...while keeping what representation selection actually needs.
    assert!(links(&tree, "ifc-model"), "the model is not optional");
    assert!(
        links(&tree, "ifc-schema"),
        "schema queries are not optional"
    );
}

/// With `lowering`, the neutral vocabulary must be present.
///
/// The inverse assertion: a feature table that gated *everything* off would
/// pass the test above while making the crate useless.
#[test]
fn the_default_build_still_links_the_neutral_vocabulary() {
    let tree = dependency_tree(&[]);

    for required in ["axiolid-core", "axiolid-model"] {
        assert!(
            links(&tree, required),
            "the default build lost {required}; lowering cannot work.\n{tree}"
        );
    }
}

/// The execution provider appears only when a backend is asked for.
///
/// Three columns, one contract: kernel-free links no geometry at all, the
/// default links representation crates but no engine, and only
/// `compile-reference-backend` pulls a provider. The middle assertion is the
/// one that matters -- it is what "off by default" means, and a feature edge
/// could silently break it while both other columns still passed.
#[test]
fn the_execution_provider_is_opt_in_only() {
    const PROVIDER: &[&str] = &[
        "axiolid-mesh-compile",
        "axiolid-mesh-boolean-boolmesh",
        "axiolid-construct",
    ];

    for column in [vec!["--no-default-features"], vec![]] {
        let tree = dependency_tree(&column);
        for crate_name in PROVIDER {
            assert!(
                !links(&tree, crate_name),
                "{column:?} links execution provider {crate_name}; compilation must stay opt-in"
            );
        }
    }

    // ADR 0012 split the seam from the engine: `compile` carries the
    // contracts a caller implements to bring their own kernel, and only
    // `compile-reference-backend` adds the reference provider. Asserting
    // the engine on `compile` would re-impose the coupling that split
    // removed; `backend_seam_linkage.rs` owns the finer claim.
    let compiled = dependency_tree(&["--features", "compile-reference-backend"]);
    for crate_name in PROVIDER {
        assert!(
            links(&compiled, crate_name),
            "--features compile-reference-backend lost {crate_name}; the feature cannot work"
        );
    }
}

/// Placement resolution must be reachable WITHOUT the geometry kernel.
///
/// This is the capability the split exists to protect. `product_world_transform`
/// used to live in `lower`, so a kernel-free consumer could not reach it at all
/// and had to hand-roll the `IfcLocalPlacement` walk -- which is how apps end
/// up with inverted composition order or a squared unit scale.
///
/// A compile-time reference is the proof: if the item stops existing in this
/// column, this test stops compiling.
#[test]
#[cfg(not(feature = "lowering"))]
fn placement_resolution_survives_without_the_kernel() {
    let resolve: fn(
        &ifc_model::Model,
        &ifc_geometry::units::UnitScale,
        ifc_model::EntityId,
    ) -> ifc_geometry::GeometryResult<ifc_geometry::Transform> =
        ifc_geometry::product_world_transform;

    // Exercise it, so the reference cannot be optimized into a no-op claim.
    let model = ifc_model::Model::new();
    let units = ifc_geometry::units::UnitScale::default();
    assert!(
        resolve(&model, &units, ifc_model::EntityId(1)).is_err(),
        "an absent product must report rather than silently resolve to origin"
    );
}

/// Authoring must work in the column that links no kernel (ADR 0011).
///
/// This is the claim that makes geometry authoring vendor-neutral: an
/// application computes vertices with CGAL, OCCT or Axiolid and hands over
/// plain numbers, so the writer must not itself depend on any of them. If a
/// future change makes an authoring signature name a kernel type, this
/// column stops compiling.
#[test]
#[cfg(not(feature = "lowering"))]
fn authoring_survives_without_the_kernel() {
    use ifc_model::{Model, Transaction};

    let model = Model::new();
    let mut tx = Transaction::new(&model);

    let origin = ifc_geometry::authoring::cartesian_point(&mut tx, &[0.0, 0.0, 0.0])
        .expect("a point needs no kernel");
    let up = ifc_geometry::authoring::direction(&mut tx, &[0.0, 0.0, 1.0])
        .expect("a direction needs no kernel");
    let place = ifc_geometry::authoring::axis2_placement_3d(&mut tx, origin, Some(up), None);
    let profile = ifc_geometry::authoring::rectangle_profile(&mut tx, None, None, 6.0, 0.3)
        .expect("a profile needs no kernel");
    let solid =
        ifc_geometry::authoring::extruded_area_solid(&mut tx, profile, Some(place), up, 2.4)
            .expect("an extrusion is a description, not an evaluation");

    let mut model = model;
    tx.commit(&mut model).expect("commit");
    assert_eq!(
        model.get(solid).expect("solid present").type_name.as_ref(),
        "IFCEXTRUDEDAREASOLID"
    );
}

/// Body description must be reachable WITHOUT the geometry kernel (#147).
///
/// A rule check asking "is this beam an extrusion of an HEA300" reads
/// profile parameters, not a mesh; linking a solid kernel for that answer is
/// the coupling the split exists to prevent. The typed reference is the
/// proof: if either function moves behind `lowering`, this stops compiling.
#[test]
#[cfg(not(feature = "lowering"))]
fn body_description_survives_without_the_kernel() {
    type Describe = fn(
        &ifc_model::Model,
        &ifc_geometry::units::UnitScale,
        ifc_model::EntityId,
    ) -> ifc_geometry::GeometryResult<Option<ifc_geometry::BodyDescription>>;
    type Profile = fn(
        &ifc_model::Model,
        &ifc_geometry::units::UnitScale,
        ifc_model::EntityId,
    ) -> ifc_geometry::GeometryResult<ifc_geometry::ProfileDescription>;
    let describe: Describe = ifc_geometry::body_description;
    let profile: Profile = ifc_geometry::describe_profile;

    let model = ifc_model::Model::new();
    let units = ifc_geometry::units::UnitScale::default();
    assert!(describe(&model, &units, ifc_model::EntityId(1)).is_err());
    assert!(profile(&model, &units, ifc_model::EntityId(1)).is_err());
}

/// The bridge-to-bridge exception (ADR 0003, amended 2026-09-15) must not
/// leak into the kernel-free column.
///
/// `ifc-alignment` links the kernel unconditionally, so if it ever became a
/// non-optional dependency of `ifc-geometry`, or escaped the `lowering`
/// feature, the 2D drawing consumer would start pulling a geometry kernel.
/// The dependency tree is the evidence, not the manifest.
#[test]
fn the_kernel_free_build_links_no_alignment_bridge() {
    let tree = dependency_tree(&["--no-default-features"]);
    assert!(
        !links(&tree, "ifc-alignment"),
        "kernel-free ifc-geometry must not link ifc-alignment:\\n{tree}"
    );
}