flow

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.

FlowStatus: Implemented in api-v1

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

OperationRouteRole in this flow
syncObjectClassClassificationInheritancePOST /object-classes/{key}/sync-classification-inheritanceSeeded refill plus demote. Declares ObjectClassDemoted
syncAllObjectClassesPOST /object-classes/sync-allWhole-tree maintenance. Calls the same facade with recomputeWholeTree: true and no key, so demote is skipped
moveObjectClassesPOST /object-classes/actions/moveRe-parent. Dispatches ObjectClassParentGraphChanged; ObjectClassDemoted is downstream
approveObjectClassPOST /object-classes/{key}/actions/approveThe forward transition a demoted class must re-earn
approveRevitParameterProjectionPOST /revit/parameter-projections/{key}/actions/approve409 while any applicable class is not approved
getRevitParameterProjectionGET /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:

  1. syncClassificationInheritance({ categoryId, recomputeWholeTree: false }). A seed whose row is gone is skipped, matched on /not found/i. Any other failure throws.
  2. 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-C1request_reconcile.js with DERIVATION = 'classification-inheritance'
  • AT-A1object_class_actions.js on ui_object_class_actions
  • AT-A2object_class_approve.js and object_class_archive.js

Code references

  • apps/api-v1/src/modules/bim-ontology/core/domain/object-class/ObjectClass.tsdemoteToDraft records ObjectClassDemoted
  • apps/api-v1/src/modules/bim-ontology/core/application/use-cases/ObjectClassLifecycleUseCases.ts — drain after persist
  • apps/api-v1/src/modules/bim-ontology/core/application/use-cases/DemoteRevitProjectionsCoveringObjectClassUseCase.ts
  • apps/api-v1/src/modules/bim-ontology/adapters/inbound/events/DemoteRevitProjectionOnObjectClassDemotionHandler.ts
  • apps/api-v1/src/modules/bim-ontology/composition/bim-ontology.composition.ts

ADRs

ADRBearing on this flow
Platform ADR-0046Single-parent tree, so ancestry is one path
Platform ADR-0047Seeded recompute on the event path
Platform ADR-0049Handler failures stamped onto object_classes rows
Platform ADR-0050Four-kind approve gate plus approved-ancestor chain
Platform ADR-0052approved naming across ObjectClass and the projection
Platform ADR-0056Class demotion records ObjectClassDemoted and covering approved projections return to draft