Skip to content

Changelog

The canonical changelog lives at CHANGELOG.md in the repository root and follows Keep a Changelog. This page is synchronised from it at build time.

Unreleased

Added

  • ifc-alignment: HELMERTCURVE and SINECURVE horizontal transition segments lower exactly as Curve2::Intrinsic. Helmert is one curve carrying a CurvatureLaw::Piecewise with a seam at half length and two quadratic pieces, each written in its own rebased arc length; splitting it into two curves is impossible without the seam position, a non-elementary integral. Sine uses the kernel's sine_corrected_transition, matching IFC's published law exactly. VIENNESEBEND is now the only refused horizontal family: its law needs the cant swing from the separate IfcAlignmentCant layout. Still no quadrature, series, or sampling in the crate.

  • ifc-validate: WHERE rules for the relationship families ifc-spatial reads. IfcRelSpaceBoundary.CorrectPhysOrVirt ties declared physicality to the bounding element across all three concrete subtypes, NoSelfReference covers the four IfcRelAssigns subtypes, each naming its own relating attribute, and IfcRelConnectsPathElements priorities are bounded to 0..=100 with an empty list treated as conformant per the rule's own OR clause.

  • ifc-alignment: CLOTHOID, BLOSSCURVE and COSINECURVE transition spirals lower exactly as Curve2::Intrinsic, the natural-equation curve added in axiolid-curve v0.12.0. A spiral has no elementary parametric form, but its curvature is an elementary function of arc length, and a plane curve is fixed up to rigid motion by that law; anchoring it to a start frame fixes it absolutely. Storing the law is therefore lossless -- the crate still performs no quadrature, series expansion, or sampling anywhere. HELMERTCURVE, SINECURVE and VIENNESEBEND remain a typed refusal: their laws need terms an IfcAlignmentHorizontalSegment does not carry, and forcing them into a nearby law would be a silent approximation.

  • ifc-alignment: composite runs now end after a transition spiral rather than asserting a transition across it. Continuity into a spiral is provable from the predecessor's closed-form end point, but continuity out of one is a Fresnel-type integral, and Transition has no "unknown" member -- so the run ends instead of claiming a fact the crate cannot verify.

  • ifc-geometry: the IfcSameValue tolerance family (IfcSameValue, IfcSameCartesianPoint, IfcSameDirection, IfcSameAxis2Placement), plus IfcPointListDim, IfcOrthogonalComplement and IfcBuild2Axes, transcribed in resource::functions. The schema's comparison band is exclusive -- values exactly epsilon apart are not equal -- so a symmetric abs() <= eps would have been subtly wrong; a unit test pins the boundary. IfcSameAxis2Placement diverges deliberately: the published function compares ap1.Location with itself, never testing the location at all, and that defect is not reproduced.

  • ifc-spatial: space boundaries. relation::boundary reads IfcRelSpaceBoundary and both concrete subtypes, resolving the bounded space, the bounding element, PhysicalOrVirtualBoundary, InternalOrExternalBoundary, and the ParentBoundary / CorrespondingBoundary links that make second-level boundaries usable for heat transfer. Model::ids_of_type matches exact type names, so all three concrete types are queried: a lookup of the supertype alone misses every real BEM export and reports a building with no boundaries at all. The four EXTERNAL_* exposure members are kept apart from plain EXTERNAL rather than merged, since ground and water contact are different heat-transfer paths. CorrectPhysOrVirt agreement is reported through physical_matches_element, not enforced: this crate states what the file says and leaves rejection to ifc-validate.

  • ifc-spatial: coverings. IfcRelCoversBldgElements and IfcRelCoversSpaces join the generic relationship reader, so a finish is reachable from the element it clads and from the space it bounds. The two stay distinct: one suspended ceiling can cover a slab and bound a room, and collapsing them loses which question was asked. Layer order is preserved, since a build-up is ordered.

  • ifc-spatial: element connection and interference. IfcRelConnectsElements with its PathElements and WithRealizingElements subtypes, plus IfcRelInterferesElements. The connects family places ConnectionGeometry at slot 4, so its two ends sit at 5 and 6 rather than 4 and 5; reading the usual pair would name the connection geometry as the relating element. Interference is not a subtype of it and keeps the 4/5 layout. Both positions are asserted against the shipped schemas. Connection and interference stay separate kinds: a clash is not an adjacency.

  • ifc-spatial: assignment relationships. IfcRelAssignsToActor, ToProcess, ToProduct and ToGroupByFactor join the generic reader, answering who is responsible for an object, which task consumes it, and what it belongs to. Their two ends bracket RelatedObjectsType: the related list comes first at slot 4, the enumeration sits at 5, and the relating end is at 6. A reader assuming the usual "relating at 5" finds an enumeration rather than a reference and the assignment disappears with no error, so the positions are asserted against the schemas.

  • ifc-spatial: the last five relationship families. IfcRelDeclares (what a project context declares), IfcRelDefinesByObject (an occurrence defined by another), IfcRelFlowControlElements (controls governing a flow element), IfcRelServicesBuildings (which structures a system serves) and IfcRelConnectsWithEccentricity. These five do not share a layout: three put the relating end first, two put the related list first, and inverting either direction is silent. With these, all 42 concrete IfcRel* families in IFC4 are read.

Changed

  • ifc-alignment: kernel pin moved from axiolid v0.12.0 to v0.14.0, for CurvatureLaw::Piecewise and CurvatureLaw::Composite.

  • ifc-geometry: the EXPRESS function registry now records Implemented and NotApplicable alongside Scaffolded, and no row remains Scaffolded: 20 implemented, 6 native primitives, and IfcListToArray/IfcMakeArrayOfArray marked NotApplicable because they only re-index a LIST into an ARRAY, which a Vec already is.

  • ifc-geometry: five functions were implemented but filed under the wrong owner or left Scaffolded -- IfcCurveDim (rules::dimension), IfcBaseAxis (resource::axes), and IfcBuildAxes, IfcFirstProjAxis, IfcSecondProjAxis (all transform).

Fixed

  • CI now runs the schema-backed tests. references/ifc-spec is not committed (CC BY-ND 4.0), so every test loading it silently skipped -- including in CI, which is how an IFC2X3 slot-name bug reached main green. scripts/fetch-ifc-schemas.sh fetches the three normative schemas against pinned checksums of line-ending-normalised content, and IFC_SPEC_REQUIRED=1 turns a skip into a failure.

  • ifc-spatial: tests/slot_layout.rs asserted the IFC4 attribute name against every bundled schema. IFC2X3 spells IfcRelCoversSpaces slot 4 RelatedSpace; IFC4 renamed it to RelatingSpace. The slot position is 4 in both, so the reader was always correct -- only the test was wrong, and it failed on a schema the crate supports. The expected name now resolves per version, leaving the IFC4/IFC4X3 assertions intact.

  • ifc-geometry: the WHERE-rule coverage gate matched violations by rule label alone, so a violation raised by any entity satisfied any other entity's case for the same label. IfcBooleanResult.SameDim passed on a violation raised by IfcPolyline.SameDim. The gate now matches the entity type as well, accepting a declared subtype or a dimensionality-suffixed concrete form (IfcCartesianTransformationOperator3D for the operator supertype) and nothing wider. The equivalent hole in the naming gate had already been closed; the behavioural gate had kept it.

  • ifc-geometry: IfcBooleanResult.SameDim had two implementations. The older one resolved operands through a local operand_dim that answered Some(3) for every family it recognised and None otherwise, so its comparison was unreachable -- dead code that still read as enforcement. Removed it along with operand_dim; boolean_operands owns the rule and resolves dimensionality through dimension::dim_of.

  • ifc-geometry: resource::functions recorded eight functions as Scaffolded ("semantics remain to implement") that were already executing. Added FunctionStatus::Implemented and a manifest test asserting an Implemented row is named by its owner module, so the registry can no longer understate the crate.

  • ifc-geometry: a Scaffolded row in the EXPRESS function registry can no longer stay stale after its function is implemented. The manifest checked only that Implemented rows were backed by code, so the registry rotted downward instead: eight rows understated the crate for two commits. The inverse check is now enforced.

  • docs: the "Objectified relationship traversal" capability row said ifc-spatial::relation reads three IfcRel* families and that other families "are not interpreted". Sixteen crates read 26 of the schema.s 40 concrete families: assignments, definitions, connections, port connectivity, voiding and filling, sequencing, and associations. The row now lists them by crate. It sits outside the generated sentinel blocks, so nothing had checked it against the source; a new openbim-ifc/tests/relationship_census.rs re-counts the families and fails when the stated number drifts from what the crates actually read.

  • docs: the relationship census counted doc-comment mentions as readers, overstating it as 26 across 16 crates. A module doc that names a family to contrast its slot layout with one the crate does read is not a reader: IfcRelServicesBuildings and IfcRelSpaceBoundary were counted this way while no crate reads either. The real figure is 24 across 14 crates, with 16 families unread. relationship_census.rs now skips comment lines.

  • docs: two capability rows carried a literal ...[truncated] marker on the published site, one since 3e349cf. Both restored to full prose.

Added

  • ifc-alignment: lower_horizontal_layout_partial lowers a horizontal layout as far as exactness allows instead of failing the whole layout on the first transition spiral. Real railway and highway alignments interleave spirals between their lines and arcs, so the all-or-nothing entry point refused essentially every production file. The partial result keeps maximal runs of exactly-lowered consecutive segments and reports each refused segment with its entity id and authored PredefinedType, so a caller can say "3 of 5 lowered, CLOTHOID #103 and #107 refused" rather than only that something failed. A run ends at every refusal: continuity across a segment this crate did not lower is not a fact it is entitled to assert. Still no approximation anywhere -- lower_horizontal_layout keeps its exact all-or-nothing contract unchanged.

  • ifc-alignment: repinned the neutral kernel to axiolid v0.12.0, whose Curve2::Intrinsic carries a curvature-law-plus-start-frame natural equation. This is the representation the transition-spiral families need to be storable exactly; lowering them onto it is the follow-up to this change, not part of it.

  • ifc-geometry: the remaining 20 IFC4 geometry-resource WHERE rules are enforced, completing all 95. Two readers were added under their declared owners rather than inside the rules layer: surface::basis implements IfcGetBasisSurface/IfcAssociatedSurface, and solid::brep::non_advanced_faces walks shell faces. These feed SameSurface, DistinctSurfaces, HasAdvancedFaces and VoidsHaveAdvancedFaces; the rest cover boolean operands, composite-curve continuity, directrix bounding, trim value kinds, the revolution axis, and mapped representations.

  • ifc-geometry: FunctionStatus::Implemented records that a normative EXPRESS function is actually executed, and a manifest test rejects any row claiming it whose owner module does not name the function. Eight rows that understated the crate were corrected.

  • Enforced the schema's normative EXPRESS functions, taking executable WHERE rules from 63 to 75 of 95. src/rules/express.rs transcribes IfcConstraintsParamBSpline and IfcConsecutiveSegments and is unit-tested against the specification text itself, so the transcription is checkable line by line rather than only through the rules that call it. Covers B-spline curve and surface parametrisation, weight positivity, knot/ multiplicity correspondence, indexed poly-curve continuity, tapered profile correspondence, and IfcLocalPlacement.WR21.

  • IfcLocalPlacement.WR21 honours the function's three-valued result: the schema returns UNKNOWN for grid placements and unrecognised shapes, which EXPRESS treats as satisfied, so only the single explicit FALSE branch -- a 3D relative placement on a 2D parent -- is reported.

  • ifc-georef: GeorefView::for_model pins the declared schema to IFC4 or IFC4X3; IFC2X3 is refused with GeorefError::UnsupportedSchema since it declares no georeferencing entities at all (verified against IFC2X3_TC1.exp). resolve_project_to_map_in resolves an IfcMapConversion through a pinned view, distinguishing an IFC4X3-only coordinate-operation entity (IfcMapConversionScaled, IfcRigidOperation) read under IFC4 -- a schema mismatch -- from a genuinely wrong entity id.

  • ifc-georef: compose_project_frame chains a resolved project-to-map operation onto a separately supplied project frame (IfcLocalPlacement or equivalent, owned by ifc-geometry), producing one map-frame transform; refuses a singular (non-invertible) project frame rather than propagating a degenerate composed transform.

  • ifc-georef: NorthReference::{Project,True,Grid} distinguish IFC's three north references. resolve_true_north reads IfcGeometricRepresentationContext.TrueNorth when declared and falls back to IFC's documented default (the project Y axis) when absent, rather than silently treating "unspecified" as "equal to grid north". grid_north_direction derives grid north from the resolved map conversion's own rotation, so true, grid, and project north can all disagree simultaneously, matching real georeferenced files.

  • ifc-alignment: AlignmentView::for_model pins the declared schema to IFC4X3 (IFC4X3/IFC4X3_ADD2); IFC2X3 and IFC4 ADD2 TC1 are refused with AlignmentError::UnsupportedSchema since IfcAlignment* entities do not exist in either schema at all.

  • ifc-alignment: CantLayout resolves, orders, and C0-continuity-checks a full IfcAlignmentCant profile, with cant_at/cant_at_distance evaluating all seven IfcAlignmentCantSegmentTypeEnum closed-form base formulas (BLOSSCURVE, CONSTANTCANT, COSINECURVE, HELMERTCURVE, LINEARTRANSITION, SINECURVE, VIENNESEBEND) exactly -- cant states its elevation value directly as a function of arc-length, so every type is exactly representable with no integration and no approximation.

  • ifc-alignment: lower_horizontal_layout assembles an IfcAlignmentHorizontal's nested segment chain into one continuity-aware CurveRelation::Composite, observing (not assuming) the Transition between consecutive segments from exact endpoint equality.

  • ifc-alignment: resolve_linear_placement/resolve_point_by_distance resolve IfcLinearPlacement through IfcAxis2PlacementLinear to the mandatory IfcPointByDistanceExpression; station_equations resolves every IfcReferent carrying Pset_Stationing into its distance-along/station mapping, including station-equation IncomingStation discontinuities.

  • Enforced 47 more IFC4 geometry WHERE rules, taking the executable count from 16 to 63 of 95. New rule modules cover dimensionality (via a transcription of the schema's own IfcCurveDim derivation), positive scalars, list cardinality, type membership and surface degeneracy. Every newly enforced row carries a conforming and a violating model in tests/where_rule_inventory.rs, and the coverage assertion fails if a row claims implementation without one.

  • ifc-resource: IfcPerson, IfcOrganization, IfcOrganizationRelationship, IfcPersonAndOrganization, and IfcActorRole projections, enforcing IdentifiablePerson, ValidSetOfNames, and WR1 (USERDEFINED roles require UserDefinedRole).

  • ifc-resource: all six concrete IfcConstructionResourceType kinds, with IfcRelDefinesByType assignment resolution that refuses a second relation naming a different type for the same occurrence.

  • ifc-resource: IfcInventory metadata and IfcActorSelect jurisdiction projection, with IfcRelAssignsToGroup membership resolved in authored order.

  • ifc-resource: IfcPhysicalSimpleQuantity (all six concrete measure kinds) and IfcPhysicalComplexQuantity usage-quantity projections, enforcing the shared non-negative/finite value rule and NoSelfReference.

  • ifc-resource: IFC4X3 ADD2 accepted alongside IFC4 ADD2 TC1 for every resource, actor, inventory, and usage-quantity projection (entity shapes verified identical against IFC4X3_ADD2.exp). IFC2X3 remains an explicit UnsupportedSchema refusal: it does not declare IfcConstructionResourceType, IfcResourceTime, or PredefinedType on IfcConstructionEquipmentResource/ IfcCrewResource, so there is no normative behavior to project.

Fixed

  • IfcBooleanClippingResult.OperatorType was implemented under the label FirstOperandType, so the operator check reported the wrong rule and the real FirstOperandType -- the first operand must be a swept area, swept disc or nested clipping result -- was never checked at all. Both now exist under their schema names.
  • IfcSubedge now lowers instead of being refused. A subedge states its own EdgeStart/EdgeEnd and inherits the carrier curve from ParentEdge, which is reached by walking the parent chain: ParentEdge is typed IfcEdge, so a subedge of a subedge is legal and stopping at the first hop would leave the carved edge with no geometry.

Fixed

  • IfcGeometricRepresentationContext.WorldCoordinateSystem is now applied when lowering a product. It is a mandatory attribute defining model space, but was read and tested without ever reaching the lowered frame, so a file that surveys its site into a real coordinate system placed every product at the wrong location. Nearly all files write the identity, which is why a corpus pass never revealed it. The context frame composes above the placement chain, so a rotated context rotates the sited product.

  • Parameter-space lowering for IfcTrimmedCurve and IfcCompositeCurve p-curve reference curves; trim parameters stay unscaled because a (u, v) address is dimensionless.

  • Typed position() placement views on all four elementary surfaces.

  • select::subtype::TABLE_SCHEMA_VERSION names the IFC schema the compiled supertype chains encode, with tables_are_verified_for to ask before trusting them for another version. The version was previously prose only.

  • Membership tests for the four select families that resolved without them: CsgSelect, CurveOrEdgeCurve, SurfaceOrFaceSurface, PointOrVertexPoint.

  • CON-GRID: IfcGridAxis rule checking. WR1 reports an axis curve that is not 2D; WR2 reports an axis that is not in exactly one of a grid's U/V/W lists. WR2 is stated over inverse attributes, which Part 21 files do not carry, so it is counted from the owning grid instead.

  • RULE-REG: an executable inventory of every geometry-resource WHERE rule. data/ifc4-where-rules.tsv records all 95 rules the IFC4 ADD2 TC1 schema declares across 56 entities, each marked implemented (19) or inventoried (76), with the schema's own expression text. Four tests fail the build if the inventory drifts from the schema, invents a rule, or claims an implementation the rules module does not provide.

  • SEL-POSNEG: membership tests for CsgSelect, CurveOrEdgeCurve, SurfaceOrFaceSurface and PointOrVertexPoint, pinning the branch order that makes an IfcVertexPoint a vertex rather than a plain point.

Added

  • docs/schema-looseness.md documents refusals caused by the IFC schema permitting a combination the geometry cannot honour, separating them from unimplemented features. Covers the parameter-space conic on a 3D placement and the surface curve whose master names an absent side.

Fixed

  • The docs site rendered generated inline HTML as literal text: status badges appeared as <span class="status-implemented">Implemented</span> and the CAPABILITIES:*:BEGIN region markers were visible on the page. VitePress had markdown.html: false, which escapes both. The badge CSS was already present and styled, so rendering was always the intent. scripts/check-inline-html.py now fails the gate if the setting regresses.
  • IfcProfileDef was labelled Planned in the capability table, implying pending work. It is the bare supertype carrying no section geometry, so the refusal is permanent; it now reads Refused and its doc comment no longer claims it awaits a neutral contract.

Added

  • Parameter-space IfcIndexedPolyCurve now lowers explicit IfcArcIndex segments. A three-point arc composes exactly from a 2D circumcentre into Circle2 plus a Cartesian trim, mirroring the 3D path with no approximation and no unit conversion. The sweep sense follows the 2D cross-product sign, so a clockwise arc is not silently replaced by its complement.
  • Corrected two stale typed-refusal rationales. IfcSubedge and IfcVertexLoop claimed axiolid had "no neutral topology contract"; it exports Vertex, Edge, EdgeUse and Loop. Both stay refused, now for their real IFC-side reasons.
  • IfcPolygonalBoundedHalfSpace now lowers exactly, closing the last entry in the PLANNED family table. It maps to SolidOperation::BoundedHalfSpace with Position carried as the operation's own placement, independent of BaseSurface: the authored clipping prism is neither dropped (which removes material outside it) nor folded into the clip plane (which relocates it). The axiolid pin moves to v0.11.0.
  • IfcSurfaceCurve now lowers when MasterRepresentation names PCURVE_S2, and each associated p-curve is paired with the surface it lies on rather than kept in a flat list. PCURVE_S1/PCURVE_S2 are positional slots, so with an unordered list neither could be named without guessing, and the case was refused outright. Axiolid v0.10.0 records the pairing, so the neutral master names S1 and S2 exactly. A file naming PCURVE_S2 with only one p-curve names a side that does not exist and is refused as degenerate.
  • IfcPCurve reference curves now also admit the explicit-knot IfcBSplineCurveWithKnots and IfcRationalBSplineCurveWithKnots subtypes. Knots are curve parameters, not lengths -- the 3D path already passes them through unscaled -- and parameter-space control points are read as raw (u, v) pairs, so no length factor applies to either. Convention-only IfcBSplineCurve stays refused because it carries no authored knot vector, and parameter-space trimmed and composite curves stay refused pending a trim contract. The variant catalog and generated capability page were updated in the same change; the #27 runtime-checked gate caught the stale claim automatically. Evidence: a unit test, a corpus fixture case, and a mutation probe scaling the knots that the test catches.
  • Geometry interpretation now carries an aggregate-element budget. SessionLimits::max_aggregate_elements (default 16,777,216) bounds the file-declared aggregates that drive allocation: B-spline knot multiplicity totals, B-spline control grids (u x v), and triangulated face-set index counts. Over-budget input produces the new typed, locatable GeometryError::AggregateTooLarge naming the entity and the aggregate; nothing is ever truncated to fit. Products are computed in u128 so a huge declaration cannot wrap to a small usize and pass the check.
  • Fixed a reachable panic: KnotVector::total_multiplicity summed file-controlled multiplicities with an unchecked sum(), which aborts in debug and wraps in release. It and KnotVector::expanded now return Option and report overflow instead. Exact output below the limits is unchanged.
  • Lowering capability claims are now variant-accurate. A PARTIAL catalog in ifc-geometry/src/lower/dispatch.rs records, per authored form, whether a family admits or refuses it and why, so families that were reported as flatly "Implemented" while still refusing some inputs (IfcPCurve, IfcSurfaceCurve, IfcIndexedPolyCurve) now report as Partial with a per-variant table in docs/capabilities.md. The catalog is not documentation: declared_variant_support_matches_runtime_behaviour drives the real lowering path for every declared variant, so a wrong disposition -- in either direction -- fails CI. 4/4 mutation probes caught, including one that initially escaped and prompted the runtime cross-check.
  • IfcPCurve reference curves now admit the analytic IfcLine, IfcCircle and IfcEllipse families in addition to polylines. Parameter-space values are read verbatim: a p-curve conic's radius and semi-axes are displacements in the surface's own (u, v) domain, which for a cylinder mixes an angle with a length, so no length or angle factor may be applied. An IfcLine's Dir magnitude is preserved un-normalized because it sets the parameter scale. Only IfcAxis2Placement2D is admitted for a parameter-space conic; a 3D placement is a named typed refusal, since its axis has no meaning in a parameter domain. Parameter-space B-splines, trimmed and composite curves remain named typed refusals. Evidence: 8 unit tests, a hand-authored millimetre corpus fixture (synthetic_parameter_space_conics.ifc) whose unit choice is itself the assertion, and 5/5 mutation probes caught.
  • ifc-geometry repins Axiolid to c144808d (v0.9.1), superseding the previous 8401f908 pin; no production API changes, both feature columns build and the full test suite passes at the new pin. Confirms the fix for axiolid/kernel#83 (bounded_half_space agreement=false mirroring defect), independently re-verified with the original reproduction probe.
  • IfcPointOnCurve and IfcPointOnSurface lower to axiolid_model::PointOnCurve/PointOnSurface, preserving the basis reference and parameter(s) exactly. Parameters convert per the basis's own quantity kind (angle on a conic, dimensionless ordinal on a polyline, length otherwise) using the same conversion the trim-parameter path already established, rather than duplicating unit logic.
  • The p-curve reference-curve family (IfcPcurve's parameter-space geometry) now also accepts the implicit-order IfcIndexedPolyCurve form -- no explicit Segments, which the schema defaults to straight lines between consecutive points -- not only IfcPolyline. An indexed polycurve with an explicit arc segment remains a named typed refusal: this crate carries no parameter-space arc contract, and flattening the arc to a straight line would silently change the surface parameterisation.

Fixed

  • ifc-geometry (data/ifc4-representation-item-dispositions.tsv) no longer lists IfcPointOnCurve/IfcPointOnSurface as planned-exact; they are implemented and the disposition/IMPLEMENTED sets must stay disjoint.

  • ifc-schema can export deterministic, release-identified structural catalogs for IFC2x3 TC1, IFC4 ADD2 TC1, and IFC4X3 ADD2. The export preserves exact entity names, nearest-first ancestry, and inherited Part 21 attribute order without redistributing EXPRESS sources or specification prose. A direct mode emits only each entity's immediate parent and declared slots; reconstruction tests prove it expands byte-for-byte to every release's full structural rows.

  • ifc-geometry now lowers authored IfcArbitraryOpenProfileDef paths to Axiolid OpenProfile graph nodes without inventing area, width, or a closing segment. Area-only swept-solid callers continue to refuse them explicitly.

  • ifc-structural now transaction-stages selected members, connections, point/linear/planar actions, member-connection links, and activity assignments with projected-state validation; a facade test proves one physical-product EntityId composes with structural assignment and authored Body geometry while body-only plan selection remains None. Nine non-redundant mutants are killed.

  • ifc-structural now projects IFC4+ ordered load configurations, point reactions across all supported versions, IFC4+ curve/surface reactions, and strict result-group reaction assignments; focused tests kill 10/10 semantic mutants without interpreting or computing result values.

  • ifc-structural now projects typed cross-version boundary stiffness selectors, failure/slippage connection conditions, distinct varying-member identities, and IFC2X3 varying-surface thickness/location payloads without interpreting them as solver constraints; focused cross-version tests kill 8/8 semantic mutants.

  • Bounded IFC4 approval and constraint domains now provide strict borrowed projections, typed resource/object relationships, deterministic direct queries, and transaction-staged authoring; classification now owns the generic external- reference relationship. A facade/STEP test proves all three join by EntityId.

  • Domain completion foundations add transaction-staged IFC4 cost-value, item, schedule, ordered nesting, and schedule-assignment authoring with typed pre-staging refusal; a reusable reverse-index-backed spatial relationship index; and a facade proof that material semantics and geometry inputs join on the same EntityId without sibling dependencies or duplicated slot ownership.

  • ifc-resource now provides a bounded IFC4 ADD2 TC1 construction-resource slice: schema-resolved projections for six occurrence kinds and authored resource time, deterministic allocation lookup, budgeted authored-order composition, and transaction-staged creation of selected records and relationships. SELECT membership, enum values, inherited RelatedObjectsType category matching, finite positive ratios, aggregate bounds/uniqueness, references, model-wide GlobalId uniqueness, cycles, multiple parents, and traversal budgets are typed refusals; scheduling, levelling, calculations, actor/inventory, resource-type, IFC2X3, and IFC4X3 semantics remain outside the claim.

  • ifc-author::EntityEditor schema-checks the full projected record before staging updates through ifc-model::Transaction; invalid names, arity, required values, types, aggregates, references, and stale commits remain typed refusals with atomic non-mutation.

  • ifc-step proves deterministic model-order output and bit-exact finite-real round trips, preserves signed zero, and refuses nested non-finite values with entity/slot context before serialization.

  • ifc-xml adds an explicit strict IFC4 ADD2 TC1 namespace/profile mode and inspectable XmlPath diagnostics through entities, attributes, typed values, and list indexes, and refuses nested non-finite REAL writes before output. Its schema-less compatibility dialect remains available without being described as generic XSD conformance.

  • ifc-geometry lowers additional exact curve/surface families into neutral Axiolid graphs: ellipses, offset and indexed poly-curves, p-/surface curves, and curve-bounded surfaces; explicit Body/Plan selection prevents silent representation substitution and a committed IFC corpus exercises dispatch.

  • ifc-geometry now mechanically reconciles every concrete IFC4 representation item/profile family with an owner, exact route or typed refusal, and committed corpus behavior. It preserves composite-on-surface relations, refuses bounded half spaces whose polygonal cutter cannot yet be represented exactly, validates finite orthonormal right-handed neutral frames, and exposes geometry-only material profile/layer usage inputs.

  • ifc-georef resolves IFC4 projected CRS metadata and IfcMapConversion into a project-metre to map-metre neutral transform with strict unit, scale, and axis validation.

  • ifc-alignment resolves IFC4X3 horizontal, vertical, and cant segment parameters and emits exact neutral line, circular-arc, and constant-gradient curve graphs with schema-valid IFC4X3 corpus evidence, refusing unsupported transitions rather than approximating.

  • ifc-template-catalog embeds authenticated official PSD/QTO snapshots for IFC2X3 TC1, IFC4 ADD2 TC1, and IFC4X3 ADD2, while keeping corrected overlays IFC4-only. It exports deterministic, edition-explicit TSV applicability indexes with exact set/member types, release-scoped source GUIDs, selectors, source paths, and digests; IFC2X3's source snapshot contains no standardized QTO XML.

  • Public contributor workflow with a curated organization Project, contributor- ready plan-task promotion, issue forms, a pull-request template, and support, security, and conduct policies.

  • ifc-structural now provides schema-resolved IFC2X3/IFC4/IFC4X3 borrowed projections for analysis/load/result groups, analytical curve/surface members, point/curve/surface connections and actions, and four core static-load value families. Bounded assignment/connection queries validate aggregate bounds, canonical SET uniqueness, selects, dangling references, target ancestry, group self-reference, single-attachment activity semantics, relation-record file order, and IFC2X3 ProjectedOrTrue requiredness. Reads enforce canonical enum membership plus exposed USERDEFINED/ObjectType, action-load compatibility, concrete linear/planar CONST, projected-action, required-axis, positive-thickness, and finite-measure contracts. Selected analysis-model and load authoring is transaction-staged and rejects non-finite STEP-real values before staging. No solver, FEM, geometry evaluation, reaction-value interpretation or computed-results capability is claimed.

  • ifc-style now provides schema-resolved IFC2x3/IFC4/IFC4X3 views for colour/factor, curve/fill/text/surface styles, textures, presentation layers, styled items, and the requested IfcAnnotation, IfcTextLiteral, IfcTextLiteralWithExtent, and IfcAnnotationFillArea contracts. Core style and annotation graphs have transaction-staged authoring; direct styles resolve deterministically ahead of layer styles, and IFC2x3 wrappers remain explicit. A public 70-declaration appearance census distinguishes strict views from schema-only and structural-only support; no rendering capability is claimed.

  • ifc-classification implements strict borrowed IFC4 views for classification, document, and library information/references plus their three object association families. Queries provide bounded hierarchy traversal and keep occurrence/type classifications separate; matching helpers transactionally author all nine concrete records with pre-staging validation.

  • ifc-schema bundles IFC2x3 TC1 (653 entities, 327 types), IFC4 ADD2 TC1 (776 entities, 397 types), and IFC4X3 ADD2 (876 entities, 436 types) as independent generated artifacts. ifc2x3(), ifc4(), ifc4x3(), and for_version() route declared schemas without cross-version fallback.

  • ifc-validate checks STRING(n) FIXED widths from the schema, catching malformed IfcGloballyUniqueId values (STRING(22) FIXED) at any length.

  • ifc-validate corpus tests validate all 38 committed fixtures against their declared IFC2X3, IFC4, or IFC4X3 tables: 31 are model-level clean (including two raw-header-only defects normalized before validation) and all 7 observable known-bad fixtures are rejected. cargo run -p ifc-validate --example audit_corpus prints the same audit as a triage table.

  • ifc-validate implements structural and type validation plus eight selected native checks exposed as nine version-labelled rule IDs: single project, unique GlobalId, property/type assignment, external-reference identity, sequence endpoint inequality, aggregate/nesting self-reference, and material priority bounds. Aggregate bounds, arbitrary EXPRESS WHERE expressions, INVERSE derivation, geometry-dependent rules, and other known gaps remain explicitly unsupported, so a conformant verdict never claims full EXPRESS conformance.

  • ifc-schema delegates supertype chains and Part 21 positional attribute order to openbim-step 0.4.0; neither is IFC-specific. Version identity, independent bundled artifacts, and process caches stay here.

Removed

  • Removed the obsolete ifc_geometry::kernel compatibility facade and adapter Tolerance plumbing. Exact lowerers no longer advertise kernel execution or approximation policy; those remain neutral-provider concerns.

Changed

  • Replaced architecture ASCII sketches with accessible, dark/mobile Mermaid diagrams; IFC geometry docs now link to Axiolid's source-backed STL and equation guide. Progressive-context discovery ignores external node_modules trees that can contain foreign agent instructions.
  • Updated all user-facing Git dependency examples to the exact reviewed IFC revision that adopts Axiolid's nested package architecture.
  • ifc-geometry now follows Axiolid's nested package architecture. Direct test or tool consumers must replace axiolid-scalar / axiolid_scalar::ScalarCompiler with axiolid-reference / axiolid_reference::ReferenceMeshCompiler and use MeshCompiler::{compile_mesh, compile_mesh_batch, compile_mesh_batch_into} for graph-to-mesh projection. Model-only facade consumers remain on axiolid with default-features = false, features = ["model"].

Fixed

  • ifc-template-catalog source generation now canonicalizes the declared corpus root, rejects symbolic links and non-file/non-directory entries during XML traversal, and verifies every resolved candidate remains inside that root.

  • ifc-xml now binds the XSI namespace used by null values, independently resolves qualified attributes during tests, and rejects unmarked or namespace-spoofed self-closing typed values instead of silently decoding them as null.

  • Non-uniform mapped transforms now carry half-space plane normals through a scale-safe inverse transpose with scale-aware determinant orientation, preserving the authored cutting plane under ordinary and extreme finite affine scale and shear without overflow or underflow.

  • IfcPolygonalBoundedHalfSpace no longer drops its positioned polygonal bound and masquerades as an unbounded half space. It now returns a typed unsupported result until the neutral geometry contract can preserve the effective cutter.

  • ifc-geometry now lowers schema-valid IfcBoundaryCurve and IfcOuterBoundaryCurve members of IfcCurveBoundedSurface, preserves their parameter-space p-curves in committed corpus evidence, keeps IfcEdgeCurve p-curves on neutral edge uses instead of 3D carrier slots, and bounds malformed self-referential surface graphs with a typed cycle error, and rejects indexed-arc arithmetic that would emit non-finite circles.

  • ifc-georef defaults IfcMapConversion axis components independently as specified, rejects non-finite unit-normalized transforms, and explicitly refuses IfcMapConversionScaled until its unequal axis factors can be represented without loss.

  • ifc-alignment now rejects finite source arc parameters when derived circle frames or trim parameters overflow, and derives radial frame axes directly from source tangent and curvature sign so large global coordinates do not collapse them.

  • Contributor links now use the repository Discussions surface, omit unpublished docs.rs and crates.io references, provide a verified immutable Git dependency for current consumers, and document an actionable confidential conduct-reporting path.

  • The roadmap now reflects shipped tessellated, advanced B-rep, swept, half- space, CSG, and exact-curve lowering instead of presenting those families as unimplemented.

  • The invalid IFC4X3 declared-schema regression now parses a synthetic IFC4X3_ADD2 document, asserts exact IFC4X3 bundle routing, and checks the typed structure.required.missing finding. It previously loaded an IFC4 fixture while claiming IFC4X3 coverage.

  • Budget.max_findings now hard-caps report storage in Report::push and Report::extend; previously validation marked a report truncated only after a phase had already recorded an unbounded number of findings.

  • Duplicate GlobalIds are now reported once by the canonical global.UniqueGlobalId rule instead of once during structural validation and again during native-rule evaluation.

  • The IFC4X3 inventory guard now counts only TYPE declarations; a line-leading TYPEOF(...) expression had inflated the preliminary count from 436 to 437.

  • ifc-classification hierarchy budgets now count every followed ReferencedSource edge and every distinct resolved entity, including the terminal classification system; revisiting an already-counted reference reports a cycle before node exhaustion.

  • The ifc-schema-generate tool hardcoded IFC4's entity/type counts, so it could not produce another schema's artifact and its guard could not detect the wrong source file for one. It now takes a schema selector with per-schema expected counts, pinned to the committed artifacts by a test.

  • ifc-validate SELECT membership bounded the walk by loop iterations rather than distinct types visited. IFC4's value selects are wide -- reaching IfcMonetaryMeasure needs IfcAppliedValueSelect -> IfcValue -> IfcDerivedMeasureValue, whose 60 members exhausted the 32-iteration budget -- so legal typed values were reported as non-members. Eight false findings across three committed fixtures. The bound is now on visited types, and an exhausted walk returns "cannot answer" instead of "not a member".

  • test/fixtures/nurbs/ifc4_rational_bspline_curve_surface.ifc instantiated IfcBSplineCurve and IfcBSplineSurface, both ABSTRACT SUPERTYPE in IFC4 and confirmed invalid by ifcopenshell.validate. Moved to invalid_abstract_base_splines.ifc so the valid fixture is schema-clean and usable as validation ground truth; the lowering test that needs them now reads the invalid file.

  • The bundled IFC4 schema artifact was missing attributes on 124 entities. LIST [1:?] OF UNIQUE X contains the token UNIQUE, which the EXPRESS parser read as the start of a UNIQUE block, truncating the attribute list at that point. IfcTypeProduct lost RepresentationMaps and Tag, so every product type reported ElementType and PredefinedType two slots early. Also affected IfcGrid, IfcPolyLoop, IfcEdgeLoop, IfcPropertyEnumeration and IfcPropertyTableValue. Fixed in openbim-step 0.4.0; the artifact is regenerated and pinned by a test against known-correct layouts.

  • ifc-schedule implements every plan task: work plans and schedules, tasks with task times, sequencing with signed lag, work calendars with recurrence, events, and deterministic timeline queries.

  • ifc-cost completes rate/component trees, nesting and control assignment, currency agreement, and tree rollups over a validated fixture.

  • ifc-model gains transactional authoring: Transaction stages structural edits, validates them against a projected end state, and commits atomically. Removing an entity that a surviving entity still references is refused, and a transaction opened against a stale model revision will not commit.

  • ifc-properties completes PROP-EDIT: quantity authoring helpers stage onto a caller-owned transaction, preserving each quantity's declared measure type.

  • ifc-properties implements property sets: property sets with every value family, quantities, units, templates, occurrence/type precedence, and comparison against externally computed measurements. Property values keep their declared measure type, so a length stays distinguishable from a count. SI prefixes are carried as exact decimal exponents rather than rounded factors. WR21/WR22 quantity breaches are reported -- neither is checked by ifcopenshell.validate.

  • ifc-systems completes its plan: flow roles and direction semantics, zones with their WR1 membership rule, spatial containment vs referencing, and deterministic upstream/downstream queries oriented by port flow direction rather than by authoring order. Queries report when they crossed a port whose direction the file never stated, so an under-specified file cannot look authoritative.

  • Explicit-knot polynomial/rational IFC B-spline curves and surfaces now lower into exact neutral Axiolid data. A synthetic IFC4 fixture proves parsed degrees, controls, compact knots, multiplicities, weights, and scalar-oracle evaluation.

  • ifc-systems reads distribution ports and the connection network. Ports resolve through both IfcRelNests and the legacy IfcRelConnectsPortToElement, carrying flow direction and owning element. ConnectionGraph states what the file says; NetworkGraph adds through-element edges so a physical run is actually traversable. Both are cycle-safe, because ring mains are normal distribution topology.

  • ifc-systems implements SYS-ROOT: systems() returns every IfcSystem and subtype with its members resolved from IfcRelAssignsToGroup. Systems are found by schema ancestry rather than Model::ids_of_type, which is an exact-type index and reports no systems at all for a file whose systems are all IfcDistributionSystem. IfcZone is included because IFC4 makes it an IfcSystem subtype. Memberships naming absent entities, and assignments to groups that are not systems, are reported as anomalies rather than dropped.

  • test/fixtures/synthetic-systems/synthetic_systems.ifc, the first committed fixture stating systems at all, with its generator.

Added

  • IfcCenterLineProfileDef lowers to a centre-line profile: an open path plus the full Thickness across it, resolved into a constant-width boundary by the kernel's miter offsetting. It is read with an open-path reader rather than the closed-contour one, which would have invented a closing segment and turned a bent bar into a triangle. Corpus census 105 -> 106, and every concrete profile family in IfcProfileResource now lowers except IfcArbitraryOpenProfileDef, which encloses no area by definition.

Fixed

  • ifc-cost read the wrong attribute slots for IfcCostItem and IfcCostSchedule, skipping ObjectType and PredefinedType. Identification, cost values and quantities were read one or more slots early, so real files produced empty or wrong results. The crate's own fixtures encoded the same mistake, which is why its tests passed.

  • The published capability matrix is generated from the lowering source instead of maintained by hand, closing the ADR 0005 follow-up. It had drifted badly: eight shipped families were listed as planned, a warning claimed tessellated geometry was not lowered when both face sets are, ifc-geometry was understated by 5,472 lines, and ifc-author and ifc-spatial were missing from the census entirely.

  • UNLOWERED in the profile lowerer listed twelve families that in fact have live dispatch arms, so the crate understated its own profile support. The coverage gate now rejects a family that is both dispatched and declared unlowered, rather than accepting either mention as coverage.

Added

  • ifc-geometry lowers ten profile families: the I, asymmetric I, L, T, U, C and Z steel sections, plus ellipse, trapezium, composite and derived profiles. IfcMirroredProfileDef lowers to a mirroring transform even though its Operator is a DERIVED attribute no file can carry. Corpus census 93 -> 105.
  • Profile nesting is bounded: IfcCompositeProfileDef and IfcDerivedProfileDef reference other profiles, so a reference cycle is refused with a typed error instead of exhausting the stack.

Fixed

  • The schema coverage gate now enumerates IfcProfileResource, the fourth geometry schema. It previously covered three, so the 22 concrete profile families were never checked and coverage claims counted only what the fixture corpus happened to contain.
  • select::subtype gained the 22 profile rows it was missing, so is_a(.., "IFCPROFILEDEF") resolves profile families instead of answering false for all of them.

Added

  • lower/profile.rs declares each unlowered profile family with its reason, and tests/schema_coverage.rs fails if a concrete family is neither lowered nor declared. 13 families are currently declared unlowered.

Added

  • ifc-geometry lowers IfcExtrudedAreaSolidTapered, IfcRevolvedAreaSolidTapered, IfcFixedReferenceSweptAreaSolid, IfcSectionedSpine and IfcSweptDiskSolidPolygonal. The polygonal disk carries its FilletRadius, which the kernel's SweptDisk now models.

Fixed

  • Trim parameters on an IfcPolyline or IfcCompositeCurve directrix are segment indices, not lengths, and are no longer scaled by the length unit. In a millimetre file a parameter of 2.0 became 0.002, collapsing the trim onto the curve's start. This affected IfcSweptDiskSolid and IfcTrimmedCurve.

Added

  • ifc-geometry lowers IfcRectangularPyramid, IfcBoundingBox, IfcGeometricSet, IfcGeometricCurveSet, IfcShellBasedSurfaceModel and IfcFaceBasedSurfaceModel. Corpus census 82 -> 86.
  • lower/bbox.rs recomputes the world AABB from all eight transformed corners. IfcBoundingBox is aligned to its own representation's axes, which are routinely rotated; passing corner and extents straight into a world-aligned Aabb claims a box the file never described.
  • lower/collection.rs routes collection members per family. Curves and surfaces stay non-dispatchable as top-level items -- a bare curve must not stand in for a body -- but are the payload inside an IfcGeometricSet, so they route there and only there, via the generated is_a supertype table.
  • Two corpus inventory tests: one asserts every IMPLEMENTED family really lowers, the other walks the corpus by entity type and asserts anything that lowers is claimed. Previously the census only visited families already named in the lists, so deleting a name hid its instances instead of failing.

Fixed

  • IfcRectangularPyramid reads XLength, YLength, Height. It follows IfcBlock, not IfcRightCircularCone, which puts Height first; the cone ordering yields a pyramid with height and width swapped that still builds.

  • ifc-geometry lowers IfcAdvancedBrep and IfcAdvancedBrepWithVoids. IfcAdvancedFace attaches its support surface to Face::surface, IfcEdgeCurve attaches its support curve to Edge::curve, and IfcEdgeLoop/IfcOrientedEdge preserve edge sharing: the fixture's eight oriented-edge uses collapse onto four shared edges, so the manifold survives instead of becoming disconnected facets.

  • resource/topology.rs gains VertexPoint, EdgeCurve, OrientedEdge, EdgeLoop and FaceSurface views. IfcOrientedEdge.EdgeElement is slot 2, after the two inherited IfcEdge attributes STEP writes as *.

  • test/fixtures/synthetic-surfaces/synthetic_advanced_brep.ifc, generated by tools/gen_surface_fixtures.py: a half-cylinder plug with one curved lateral face and two planar caps.

Fixed

  • Both B-rep sense flags now compose. IfcEdgeCurve.SameSense sets the stored edge's intrinsic sense and each IfcOrientedEdge.Orientation flips its use of that edge. Applying only one leaves a curved solid whose face normals and edge directions disagree, which renders correctly and breaks booleans.

Added

  • ifc-geometry lowers the remaining exact surface families: IfcCylindricalSurface, IfcSphericalSurface, IfcToroidalSurface, IfcSurfaceOfRevolution, IfcRectangularTrimmedSurface, IfcBSplineSurfaceWithKnots and IfcCurveBoundedPlane. Radii and axis origins convert to metres while frame axes stay unit length, so a placement keeps the U/V parameterisation that trims are taken against.

  • A trim parameter is scaled by the basis surface's quantity kind: angle on a revolved or conic direction, length on a planar one. A file in degrees with a length factor applied turns a 90-degree patch into roughly 0.0016 of one, and nothing downstream reports it because the surface is still valid.

  • test/fixtures/synthetic-surfaces/ — 4 generated IFC4 fixtures, plus the tools/gen_surface_fixtures.py script that produces them. No licensed public corpus carries curved or B-spline surfaces: ifc-lite (MPL-2.0) has none, the buildingSMART sets (CC-BY-4.0) hold only faceted breps and extrusions, and the one repo that does have them publishes no licence at all. Generating our own is the only licence-clean route, and committing the generator keeps the fixtures reproducible instead of opaque. All four pass ifcopenshell.validate with zero issues.

Fixed

  • IfcSurfaceCurveSweptAreaSolid now lowers end to end. Its SweptCurve arrives wrapped in an IfcArbitraryOpenProfileDef; treating that wrapper as a profile demanded a closed contour it cannot supply. As a swept surface's generatrix it is a curve, not an area, so it is unwrapped to the curve it names. Used as an actual profile the entity is still refused with a stated reason -- closing it would fabricate a face the file never described. Corpus census rose 80 -> 81.

Added

  • ifc-geometry lowers IfcPlane and IfcSurfaceOfLinearExtrusion into GeometryNode::Surface and SurfaceRelation::LinearExtrusion, with a new Transform::to_geom_frame that carries a placement's own U/V axes into the kernel frame. A surface's x/y fix its parameterisation: rebuilding them from the normal picks an arbitrary rotation about it, so the surface still renders in the same place while every trim and pcurve taken against it lands somewhere else. Depth on a linear extrusion is deliberately dropped -- the surface is unbounded in the extrusion parameter and Depth is a drawing hint, so folding it into the direction would silently reparameterise the surface.

Changed

  • Relicensed repository-authored work from MIT to AGPL-3.0-or-later; historical releases remain under their published MIT terms, and third-party material retains its own terms.
  • IfcArbitraryOpenProfileDef now reports a stated reason ("the neutral profile model represents closed contours only") instead of a generic "profile subtype is not lowered yet". An open profile bounds no area; closing it would fabricate a face the file never described. This is the real blocker for IfcSurfaceCurveSweptAreaSolid, whose lowering is implemented and waiting on it.

Fixed

  • Removed duplicate direction normalization in surface lowering and to_geom_frame. resource::direction::resolve_unit already normalizes at the IFC boundary, which is where the crate's contract says it happens exactly once, so the second pass was unreachable code.

Added

  • ifc-geometry lowers exact curves: IfcPolyline, IfcLine, IfcCircle, IfcTrimmedCurve and IfcCompositeCurve. A trim parameter is not always a length. IfcTrimmedCurve carries values in the basis curve's own parameterisation, which is a length along an IfcLine but an angle on an IfcCircle. Applying the length factor to both silently rescales every arc: in swept_disk_composite_arc_crankbar.ifc the 0.082 rad arcs would become 8.2e-5 rad on a millimetre file, and the result still renders. An IfcVector's magnitude is likewise preserved rather than normalized away, because it scales the line's parameter rather than describing orientation.

  • IfcCsgSolid, IfcBlock, IfcSphere, IfcRightCircularCylinder, IfcRightCircularCone and IfcSweptDiskSolid lower. A CSG solid is a wrapper and resolves to whatever its TreeRootExpression resolves to. CSG primitives are local by kernel contract, so their Position rides on an Instance node instead of being folded into the extents, which would discard the origin offset and break any rotation. A swept disk keeps its InnerRadius -- dropping it turns every pipe into a solid bar -- and refuses a half-open parameter range rather than guessing the missing end.

  • With these families the committed corpus reaches 80 lowered items and an empty unsupported set: every representation item in every committed fixture now lowers into the neutral DAG.

  • IfcHalfSpaceSolid, IfcBoxedHalfSpace, and IfcPolygonalBoundedHalfSpace lower into GeometryNode::HalfSpace. A half space is the infinite cutting tool IFC uses to spell "clip this solid with a plane", so lowering it is what makes the enclosing boolean resolvable: IFCBOOLEANCLIPPINGRESULT left the unsupported set as a side effect, and the corpus census rose 67 -> 72 lowered items. The AgreementFlag is inverted on the way through: IFC .T. selects the side the base surface normal points away from, while the neutral HalfSpace.agreement selects the normal side. Passing the flag straight through keeps exactly the half that should have been removed, and nothing downstream reports it -- the boolean still evaluates and the mesh is still watertight, the wall simply has the wrong end missing. Curved base surfaces are reported as unsupported rather than flattened to a tangent plane, which would cut along the wrong shape. The two bounded subtypes lower to their underlying half space: their bounds are clipping hints, and building a prism from an unlowered 2D boundary curve would invent geometry.

  • ifc-geometry lowers IfcTriangulatedFaceSet and IfcPolygonalFaceSet into GeometryNode::TriMesh and GeometryNode::PolygonMesh. Corpus census rose 64 -> 67 lowered items and IFCTRIANGULATEDFACESET left the unsupported set. Authored n-gons and their voids survive verbatim: triangulating at read time would pick a fill rule and a tolerance on the kernel's behalf, and a face with its holes flattened into the outer loop tessellates into a solid slab, so a window silently becomes a wall. Face sets lower to meshes rather than BRep because a face set carries no adjacency -- recovering topology means inferring shared edges by comparing floats, which invents information the file never had. Two indexing traps are covered by tests because both produce meshes that still render: CoordIndex is 1-based in the file and 0-based in the mesh, and a PnIndex -- at set level or on a face -- is an extra hop that permutes vertices when skipped. Normals take the frame's linear part only; sending them through the full affine transform adds the translation and breaks lighting on every product away from the origin.

  • unreachable_products() on the facade, behind spatial + geometry-select: reports products a viewer will never draw, with the reason. Closes #5. A file can pass IfcOpenShell.validate with zero errors and still open blank -- validation asks whether the file is legal IFC, this asks whether the geometry is reachable. Three causes are detected: no IfcRelContainedInSpatialStructure (the spatial tree is how viewers reach geometry at all), a body authored only into a non-model context such as PlanView, and a representation whose context does not resolve. Openings, aggregated parts, spatial containers and representationless products are deliberately never reported: on AC20-FZK-Haus.ifc 20 of 127 products sit outside the containment tree and every one of them is legitimate, so the lint finds nothing there. It lives in the facade because containment and representation contexts are sibling domain crates that ADR 0003 forbids from depending on each other.

  • ifc-schema::ifc4(): the IFC4 ADD2 TC1 schema (776 entities, 397 types) bundled as a compiled binary artifact and cached in a OnceLock, on by default via the new ifc4 feature. Closes #4: consumers no longer source IFC4.exp themselves, hit the Latin-1 decode trap (Schema::from_express_bytes already fixed the decode half), or reparse 372 KB of EXPRESS on every process start -- the bundled artifact is a compiled 120 KB structural table with no normative source text or prose in it. ifc-schema-generate (the generation feature) regenerates the committed artifact from a user-supplied IFC4.exp; the normative file itself is never vendored into the crate or its published archive. Schema::from_express/from_express_bytes remain the path for schemas this crate does not bundle (IFC2x3, IFC4x3, custom).

  • product_world_transform and products_world_transforms are re-exported at the ifc-geometry crate root and from the facade under geometry-select. Resolving an IfcLocalPlacement chain is the most-reused operation in any IFC consumer and the one most often reimplemented incorrectly -- composition order and unit scaling are both easy to invert. The batch form shares one placement cache, which matters because products in a storey share their whole ancestor chain.

  • scripts/check-leakage.py runs in scripts/gate.sh. It has existed since the documentation work but ran only by hand, which is how a tracked ifc-geometry/references/ directory survived undetected. 3/3 mutation probes confirm it rejects a references/ path, XSD bytes under an innocuous filename, and a PDF payload.

  • ifc-geometry splits into two build sizes. The new default-on lowering feature carries the six axiolid-* dependencies; turning it off leaves representation contexts, plan/body selection, profiles, curves, surfaces, solids, units and placements, which read ifc-model slots and link no geometry code. Measured on the crate's own dependency graph: 26 crates with lowering, 17 without -- all eight axiolid-* crates and glam drop out. The facade exposes the same split as geometry-select (selection only) versus geometry (selection plus lowering). ifc-geometry/tests/ kernel_free_build.rs and two new cases in openbim-ifc/tests/thin_build.rs assert it against the resolved dependency graph, so a stray unconditional use axiolid_* fails the gate instead of silently relinking the kernel. Existing consumers are unaffected: lowering and geometry stay on by default. Closes #2.

  • Opt-in recovery from damaged exports. StepCodec::lenient() returns a StepReader that skips unreadable data records instead of failing the file, and Model::diagnostics() reports each dropped range, so a viewer can show "loaded, 1 record skipped" rather than silently losing data or refusing a 2.5 MB model over one truncated record. StepCodec stays strict: an authoring tool that drops entities corrupts the file it edits. Header structure and the physical-file marker remain fatal under both policies.

  • ifc_model::Diagnostic: codec-neutral non-fatal findings carried on the model, with an optional source byte range.

  • Advanced openbim-step to 0.3.2 for the recovery API.

Changed

  • geometric_products moved from ifc-geometry's lower module to input and is now re-exported at the crate root. Asking which entities carry a shape is a slot read, so it no longer disappears with --no-default-features; the old lower::context::geometric_products path still resolves.

  • Placement resolution moved from lower::context to constraint::placement. It was previously reachable only through the deep lower path, so it was undiscoverable, and after the lowering feature split it did not compile at all for kernel-free consumers -- exactly the 2D consumers that need world coordinates without a solid modeller. lower::context re-exports it, so the old path still resolves.

  • Committed schema-derived artifacts moved from ifc-geometry/references/ to ifc-geometry/data/, matching ifc-template-catalog/data/. The name references/ is reserved for the local, unredistributable schema checkout, so a published crate must never use it. The files themselves are unchanged and were never a licensing problem -- they carry structural facts (slot indices, declaration names) and this repository's own ownership mapping, with no EXPRESS source text -- but the directory name defeated the detector that exists to catch real leaks. data/NOTICE.md records the reasoning. The local references/ tree is now gitignored so the detector sees only the publishable tree.

  • Behavior change. select_plan_representation now requires a drawable identifier and a plan context, instead of letting the context win outright. ArchiCAD authors Box/BoundingBox shape representations inside a PLAN_VIEW sub-context, so the old context-first rule returned a bounding box and never consulted PLAN_IDENTIFIERS. On AC20-FZK-Haus.ifc that was 107 of 253 shape representations, and every plan lookup came back a box. Authorial intent now selects between drawable candidates rather than making a box drawable.

    This returns fewer answers, not just better ones: on that file, products resolving a plan representation drop from 121 to 34 (14 Annotation, 13 Axis, 7 FootPrint, and no non-drawable picks). The 87 products that lose an answer genuinely have only a bounding box, and None is the documented contract for "no drawable plan geometry" -- but a consumer that was drawing those boxes will now draw nothing for them.

  • ifc-geometry representation contexts: RepresentationContext reads IfcGeometricRepresentationContext and IfcGeometricRepresentationSubContext -- identifier, type, parent, target scale, and a typed TargetView that preserves unknown enumeration constants instead of flattening them. plan_contexts finds the sub-contexts a drawing is authored into.

  • DERIVED attribute inheritance: a sub-context redeclares six inherited attributes, which real files write as * meaning "read this from my parent". precision, world_coordinate_system, coordinate_space_dimension and true_north resolve the parent chain; reading the slot directly yields the marker and silently loses the project's precision and placement. See ADR 0009.

  • select_plan_representation: the inverse of select_shape_representation. Prefers an explicit PLAN_VIEW context, then Plan/Annotation/FootPrint/ Axis, and returns None for a solid-only product rather than offering a body to draw flat.

  • ifc-spatial: containment and objectified relationship traversal. Builds the project/site/building/storey/element tree from IfcRelAggregates, IfcRelContainedInSpatialStructure and IfcRelNests, answering "which elements are on this storey" and its inverse. Tolerates real exports -- omitted levels, elements on the building, duplicate storeys, dangling references and containment cycles -- reporting defects through orphans() and dangling(). Reached through the facade's new spatial feature. See ADR 0008.

  • ifc-model::ReverseIndex: on-demand target-to-referrer index recording the attribute slot each reference sits in, which is what distinguishes the two ends of an objectified relationship.

  • ifc-model bounded traversal: depth_first, breadth_first and find_cycle with explicit Budget/Stop reporting, so a malformed file truncates with a diagnosis instead of hanging the caller.

  • ifc-author: schema-checked entity construction. Build an entity by naming its attributes and let ifc-schema resolve STEP slot positions, instead of hand-placing positional values. Refuses unknown entities and attributes, duplicate sets, missing required attributes, declared-type and aggregate mismatches, and malformed GlobalIds. Reached through the facade's new author feature. See ADR 0007.

  • A documentation site (VitePress) published to GitHub Pages, covering architecture, a conservative capability matrix, ADRs, a roadmap, and end-to-end use-case guides.

  • Six architecture decision records covering the domain/codec-free entity graph, the codec trait, borrowed domain views, the Axiolid geometry boundary, scaffold-module semantics, and thin facade defaults.

  • openbim-ifc/tests/docs_examples.rs, which compiles and runs every Rust example shown in the documentation so published code cannot drift.

  • scripts/sync-changelog.py, generating the documentation changelog page from this file; scripts/gate.sh fails on drift.

  • scripts/check-leakage.py, rejecting standards material (XSD, PDF, references/) from the published site.

Fixed

  • Model::insert no longer corrupts the type index when it replaces an existing entity: the id was appended unconditionally, so re-inserting listed it twice under the same type, and replacing an entity with one of a different type left it listed under both. ids_of_type and type_histogram reported those duplicates.

Changed

  • Advanced openbim-step to 0.2.1 for strict mandatory-header validation, line/print-control handling, low-line keywords, and lossless string escapes.
  • Delegated generic ISO 10303-21 STEP syntax and ISO 10303-11 EXPRESS parsing to openbim-step; IFC retains thin model, schema-version, and validation adapters.
  • Extracted the IFC family from openbimrs/openbim into its canonical standalone repository while preserving relevant source history.
  • Added an independent Cargo workspace, CI workflow, verification gate, project documentation, and self-contained regression fixtures.
  • Made release-critical package metadata explicit across the nested-workspace boundary.

Released under the AGPL-3.0-or-later licence. ISO and CEN standards material is not redistributed.