Object Class Demotion Cascade
An ObjectClass that loses a required classification is demoted approved → draft. That records ObjectClassDemoted, and covering approved Revit projections are demoted to draft for re-review.
Overview
ObjectClass approval is a promise: the class is complete enough to publish. Under ADR-0050 that means all four classifications resolve — Uniformat, Masterformat, ObjectClass code, and Revit category — and every ancestor is already approved.
If that promise is broken, the class returns to draft. That records ObjectClassDemoted. A thin handler then demotes covering approved Revit projections to draft so they cannot keep shipping into Revit output until the class is fixed and both are re-approved. Demote is reversible re-review; archive is for ParameterKey retirement.
OpenAPI
| Operation | Route | Role in this flow |
|---|---|---|
| syncObjectClassClassificationInheritance | POST /object-classes/{key}/sync-classification-inheritance | Seeded refill plus demote. Declares ObjectClassDemoted |
| syncAllObjectClasses | POST /object-classes/sync-all | Whole-tree maintenance. Calls the same facade with recomputeWholeTree: true and no key, so demote is skipped |
| moveObjectClasses | POST /object-classes/actions/move | Re-parent. Dispatches ObjectClassParentGraphChanged; ObjectClassDemoted is downstream |
| approveObjectClass | POST /object-classes/{key}/actions/approve | The forward transition a demoted class must re-earn |
| approveRevitParameterProjection | POST /revit/parameter-projections/{key}/actions/approve | 409 while any applicable class is not approved |
| getRevitParameterProjection | GET /revit/parameter-projections/{key} | Surfaces objectClass.approved on missingForApproval even if a projection is still approved |
Event path
ObjectClassParentGraphChanged carries de-duplicated affectedObjectClassIds plus changedAt. Four handlers are registered against it; the third fills classification then demotes. See Object Class Graph Mutations for the full fan-out.
ObjectClassParentGraphChangedClassificationHandler does, per affected id:
syncClassificationInheritance({ categoryId, recomputeWholeTree: false }). A seed whose row is gone is skipped, matched on/not found/i. Any other failure throws.- For each of the four
REQUIRED_OBJECT_CLASS_APPROVE_KINDS,demoteWhenClassificationLost({ seedKey: categoryId, kind }).
The loop is per id and per kind, and it is not batched.
ObjectClassWorkflowUseCases.syncClassificationInheritance reaches the same demote loop directly, gated on a seed key. syncAllObjectClasses is seedless and skips demote. AT-C1 always supplies domain_id, so the steward-facing path always demotes.
What demotion records
DemoteObjectClassesThatLostClassificationUseCase still uses snapshot BFS to choose ids. It then loads each ObjectClass from snapshot.categories (already rehydrated aggregates — no rehydrate in the use case) and calls demoteToDraft(now, { kind }). Persist via setLifecycleStatuses, then pullDomainEvents() only if the write succeeded, then eventDispatcher.dispatch. Several demotions drain several ObjectClassDemoted events and dispatch together.
demoteToDraft is a no-op (no event) when already draft and InvalidObjectClassStatusTransitionError from archived.
Cascade into Revit
DemoteRevitProjectionOnObjectClassDemotionHandler translates the payload into DemoteRevitProjectionsCoveringObjectClassUseCase. That use case lists projections, resolves applicable object classes with RevitCategoryScopeResolver.listApplicableObjectClasses, and calls RevitParameterProjection.demoteToDraft(now) on approved rows that cover the demoted class.
Hex §7: the dispatcher swallows handler errors, so this reaction is not the only way to notice a stuck approved projection. GET already surfaces objectClass.approved on missingForApproval.
SPF steward PATCH demote (demoteApprovedIfSpfOutputChanged) is a separate path and is unchanged. ObjectClass archive cascade is out of scope.
Airtable entry points
- AT-C1 —
request_reconcile.jswithDERIVATION = 'classification-inheritance' - AT-A1 —
object_class_actions.jsonui_object_class_actions - AT-A2 —
object_class_approve.jsandobject_class_archive.js
Code references
apps/api-v1/src/modules/bim-ontology/core/domain/object-class/ObjectClass.ts—demoteToDraftrecordsObjectClassDemotedapps/api-v1/src/modules/bim-ontology/core/application/use-cases/ObjectClassLifecycleUseCases.ts— drain after persistapps/api-v1/src/modules/bim-ontology/core/application/use-cases/DemoteRevitProjectionsCoveringObjectClassUseCase.tsapps/api-v1/src/modules/bim-ontology/adapters/inbound/events/DemoteRevitProjectionOnObjectClassDemotionHandler.tsapps/api-v1/src/modules/bim-ontology/composition/bim-ontology.composition.ts
ADRs
| ADR | Bearing on this flow |
|---|---|
| Platform ADR-0046 | Single-parent tree, so ancestry is one path |
| Platform ADR-0047 | Seeded recompute on the event path |
| Platform ADR-0049 | Handler failures stamped onto object_classes rows |
| Platform ADR-0050 | Four-kind approve gate plus approved-ancestor chain |
| Platform ADR-0052 | approved naming across ObjectClass and the projection |
| Platform ADR-0056 | Class demotion records ObjectClassDemoted and covering approved projections return to draft |