Honor ApplicableOccurrence in assign_type class validation

The class-pairing validation added in 10ee5aef4f rejected every typed
annotation with "IfcTypeProduct cannot type IfcAnnotation ... (allowed
occurrence classes: <none>)".

The check derived allowed occurrence classes solely from the
buildingSMART implementer-agreement map, which has no entry for the
abstract IfcTypeProduct that Bonsai uses for annotation types (IFC4 has
no IfcAnnotationType). The intended occurrence class is declared in the
type's ApplicableOccurrence attribute (e.g. "IfcAnnotation/TEXT"), the
schema-defined mechanism for exactly this purpose.

Augment the allow-list with the ApplicableOccurrence class, but only
when its leading token resolves to a real entity in the schema so
free-form text is not trusted blindly. Genuine mismatches (e.g.
IfcWallType -> IfcWindow) are still rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ryan Schultz
2026-06-26 16:51:16 -05:00
committed by Gorgious56
parent febde1bbbb
commit 65cd5701c3
@@ -193,15 +193,27 @@ class Usecase:
# The EXPRESS schema has no WHERE rule pairing RelatingType /
# RelatedObjects classes; the canonical class pairing per schema
# is a buildingSMART implementer agreement, enforced here.
allowed_occurrences = ifcopenshell.util.type.get_applicable_entities(
relating_type.is_a(), schema=self.file.schema
allowed_occurrences = set(
ifcopenshell.util.type.get_applicable_entities(relating_type.is_a(), schema=self.file.schema)
)
# The implementer agreement map has no entry for the abstract
# IfcTypeProduct, which Bonsai uses for annotation types. The schema
# itself defines IfcTypeProduct.ApplicableOccurrence for exactly this
# purpose, so honor it when the leading class token is a valid entity.
if applicable_occurrence := getattr(relating_type, "ApplicableOccurrence", None):
occurrence_class = applicable_occurrence.split("/", 1)[0]
schema = ifcopenshell.schema_by_name(self.file.schema)
try:
schema.declaration_by_name(occurrence_class)
allowed_occurrences.add(occurrence_class)
except RuntimeError:
pass
mismatched_classes = sorted({o.is_a() for o in related_objects if o.is_a() not in allowed_occurrences})
if mismatched_classes:
raise TypeError(
f"{relating_type.is_a()} cannot type {', '.join(mismatched_classes)} "
f"in schema {self.file.schema} (allowed occurrence classes: "
f"{allowed_occurrences or '<none>'})"
f"{', '.join(sorted(allowed_occurrences)) or '<none>'})"
)
ifc2x3 = self.file.schema == "IFC2X3"