Object Class
The canonical kind of building object BuildPlan can classify, specify, and eventually build. Aggregate root of a single-parent tree, cross-referenced to Uniformat, Masterformat, ObjectClass codes, and one Revit category.
Purpose
An object class answers what kind of thing is this? It is not a product, a product model, or a Revit category — it is the classification node those things hang off. Classes carry the external classification cross-references (Uniformat, Masterformat, OmniClass, ObjectClass code), at most one Revit category link, and the parameter assignments that make a class specifiable.
The class graph is the spine of the module: ClassParameter assignments attach parameters to it, Revit category scope is resolved from it, and product-marketplace assigns a class to a product rather than defining its own taxonomy.
Lifecycle
draft → approved → archived, from OBJECT_CLASS_STATUSES.
| Transition | Rule |
|---|---|
draft → approved | Steward approve. Gated — see below |
draft → archived | Allowed |
approved → archived | Allowed |
approved → draft | Demotion only, via demoteToDraft(). Clearing a required unique classification with no remaining ancestor value demotes the class |
archived → draft | reinstate() only. Archived has no forward transition |
approved is the only usable tier (isUsableObjectClassStatus), and the only visible one when includeInactive is false. A new root is authored in draft (createDraftRoot).
Approve gate. Approving requires all four of Uniformat, Masterformat, ObjectClass code, and Revit category (REQUIRED_OBJECT_CLASS_APPROVE_KINDS) to be present — resolved along the ancestry, so an inherited value satisfies the gate exactly as a directly authored one does. It additionally requires every ancestor to already be approved (assertApprovedAncestorChain, ADR-0050); the error names the offending ancestor and tells the steward to approve it first. ApproveObjectClassUseCase runs the ancestor check before the classification check and is idempotent on an already-approved class.
Invariants
- Exactly one parent, or none (ADR-0046). A class may not be its own parent or its own child, and the graph is acyclic.
nameis required and non-empty.- At most one authored Revit category link. More than one on rehydrate is a domain error, not a silent pick.
- A class may be
approvedonly if every ancestor on its single path to the root isapproved. needs_reviewrequires areviewReason.- When the Revit category status is not
current, the projected bindable ids are null — astaleorneeds_reviewlink never hands out a category to bind against. depth,path,is_leaf, androot_object_classare derived. They are recomputed by theObjectClassParentGraphChangedhierarchy handler over the seeded subtree, never authored and never a source of truth.
root_object_class
root_object_class is the parentless ancestor at the top of a class’s lineage, derived and persisted on every row. It is explicitly not an input to Revit category resolution — that was a DAG-era rule retired with ADR-0046. Revit applicability comes from the effective assignment set, not from which tree a class happens to sit in.
Classification fill (ADR-0050)
Stewards author *_direct values only. Everything else is inferred:
- Make-unique is inferred, never authored. On the edited node, a direct value that differs from the value at the nearest make-unique ancestor becomes the new make-unique source; clearing it unsets make-unique and the node inherits again.
- Fill runs top-down. Walk up to the nearest make-unique ancestor, then fill that value down until the next one.
- Effective mirrors materialized
*_direct. Because the fill is materialized, no read-time ancestry walk happens.*_inheritedis always cleared under this rule. - Kinds are independent. Uniformat, Masterformat, ObjectClass code, and Revit category each fill separately. OmniClass is a flat, non-propagating link and is not part of the fill or the approve gate.
- More than one direct link for a kind is recorded as a
MULTIPLE_DIRECT_CLASSIFICATIONSviolation rather than resolved by guessing.
Revit category binding status
The link carries a health status — current, stale, needs_review, unresolved — and a provenance source of authored or inherited.
This is not a lifecycle. It says nothing about whether the class is approved and it never gates approve or archive on its own; it describes whether the class’s category link is currently trustworthy. Two words that look similar are also distinct: this status describes an object class classifying as a Revit category, whereas Revit category scope describes what a Revit project binding covers.
Domain Events
ObjectClassParentGraphChanged— published after parent tree edges change (move, merge, copy, delete, add-child, create-root), carryingaffectedObjectClassIdsandchangedAt. Four handlers are registered against it: Revit category reconciliation, hierarchy field recomputation, classification fill with demote-on-lost-classification, and rendered parameter template sync. Ids in the payload are de-duplicated.
No other object-class domain event exists — lifecycle transitions are persisted directly by the lifecycle use cases.
OpenAPI Operations
- listObjectClasses
- createObjectClass
- approveObjectClass
- archiveObjectClass
- moveObjectClasses
- mergeObjectClasses
- copyObjectClasses
- addObjectClassChild
- deleteObjectClasses
- setObjectClassRevitCategory
- syncObjectClassHierarchy
- syncObjectClassClassificationInheritance
- syncObjectClassRenderedParamTemplates
- syncAllObjectClasses
ADRs
The platform series lives in docs/adr/07_Architectural_Decision_Records.md. The per-file series is separate and the numbers collide — see docs/adr/README.md.
| ADR | Decision |
|---|---|
| Platform ADR-0046 | The object-class hierarchy is a single-parent tree; no primary-parent device |
| Platform ADR-0048 | domain_id is the public identifier; never a rec… id in a payload |
| Platform ADR-0050 | Object-class lifecycle plus the four-kind classification approve gate; inherit-by-default fill with make-unique |
| Platform ADR-0052 | active is displayable-but-unvetted and belongs to ParameterKey alone — which is why this class has no active tier |