flow

Parameter Key to Revit Projection

Author a canonical ParameterKey, approve it, and let ParameterKeyApproved mint the draft Revit projection a steward then completes and approves.

FlowStatus: Implemented in api-v1

Overview

This is the parameter half of BIM Ontology, end to end: a steward defines a canonical concept, vets it, and the system responds by minting the Revit-facing row that only a human can finish. Two aggregates and two lifecycles are involved — ParameterKey (draft → active → approved → archived) and RevitParameterProjection (draft → approved → archived) — and the join between them is exactly one event.

The split of labour is the point. The system creates; the human completes. Auto-ensure can only know what the key knows, so it writes the key-derivable facts and leaves the Revit-specific decisions null. Nothing about GUIDs, binding kinds, or primary usage is knowable at the moment a key is approved, so the domain does not pretend otherwise — validateRuntimeShape accepts a draft projection with a null binding preference and a null primary usage, and the approve gate is where completeness is demanded.

The active tier and why it exists

ParameterStatus transitions are strictly linear: draft → active, active → approved | archived, approved → archived. A draft key cannot jump to approved, and it cannot be archived without being activated first.

active means displayable but unvetted. It exists for the scraping path: a scraped product attribute either maps onto an approved key or creates a new key in active, so the value is visible on the storefront while still flagged as needing steward mapping. An active key is deliberately not in Revit — see ubiquitous language.

OpenAPI

Routes are relative to the /api/v1/bim-ontology mount.

OperationRouteRole in this flow
createParameterKeyPOST /parameter-keysCreates the key in draft. Requires Idempotency-Key
activateParameterKeyPOST /parameter-keys/{id}/actions/activatedraft → active
approveParameterKeyPOST /parameter-keys/{id}/actions/approveactive → approved. The trigger for everything downstream
updateParameterKeyPOST /parameter-keys/{id}/actions/updateRaises ParameterKeyUpdated, which drives the guarded repair ensure
getRevitParameterProjectionGET /revit/parameter-projections/{key}Detail read, including missingForApproval from the same policy the approve gate uses
updateRevitParameterProjectionPATCH /revit/parameter-projections/{key}The steward completion step
approveRevitParameterProjectionPOST /revit/parameter-projections/{key}/actions/approvedraft → approved, gated
resolveRevitParameterPackagePOST /revit/parameter-packages/resolveManifest — approved projections only
generateRevitSharedParameterFilePOST /revit/parameter-packages/shared-parameter-fileSPF text from the same resolution
listCommonProjectParametersGET /revit/common-project-parametersInstallable system commons — approved projections only

:key on every projection route is the projection domain_id (ADR-0048), not an ObjectClass handle.

The approve gate on the key

collectApproveMissingRequirements returns all unmet items at once, and they are exactly two:

RequirementMissing-reference token
A ParameterSpec referenceparameterSpecId
A ParameterControl referenceparameterControlId

A non-empty list is a ParameterKeyApprovePreconditionError (PARAMETER_KEY_APPROVE_PRECONDITION_FAILED). Two further checks follow: the resolved spec and control must be the ones the key references (ParameterKeyResolutionMismatchError) and they must be compatible with each other (IncompatibleParameterSpecControlError). The same list is also enforced at rehydration — a stored row claiming approved without them throws ParameterKeyLifecycleInvariantViolatedError.

usage:bim_parameter is not an approve token. It remains the Revit gate: EnsureRevitProjectionForParameterKeyUseCase skips catalog-only keys, RevitProjectionApprovePolicy requires it on the linked key, and dropping it archives the projection.

The steward PATCH accepts five fields and nothing else

updateRevitProjectionBodySchema is .strict() and admits only bindingKindPreference, revitPrimaryUsage, familyApplication, projectBinding, and visibilityFlags. Anything derived or key-owned is refused with an HTTP 400 naming the field, from DERIVED_STEWARD_PATCH_FIELDS — which includes discipline, revitSpecTypeId, revitName, revitNameOverride, sharedGuid, status, and the removed explicitCategoryRefs.

All five writable fields are in changesSharedParameterFileOutput’s comparison set. The consequence is stronger than “a PATCH that changes SPF output demotes”: every PATCH that actually changes anything demotes an approved projection to draft, because there is no steward-writable field that does not affect Revit emission. A no-op PATCH changes nothing, saves nothing, and does not demote.

Event Path

ParameterKeyApproved is the only event that mints a projection.

EventRegistered handlersGuard
ParameterKeyApprovedParameterKeyApprovedProjectionHandlerNone beyond idempotency — ensure itself decides
ParameterKeyUpdatedParameterKeyUpdatedProjectionEnsureHandlerKey is approved and carries usage:bim_parameter
ParameterKeyUpdatedArchiveRevitProjectionOnParameterKeyRetirementHandlerKey is not approved or has lost usage:bim_parameter
ParameterKeyUpdatedCanonicalProjectionMetadataChangedHandlerNone — resyncs the derived Revit name
ParameterKeyArchivedArchiveRevitProjectionOnParameterKeyRetirementHandlerAlways archives
ParameterKeyCreated, ParameterKeyActivated, ParameterKeyDeletednone

The two ParameterKeyUpdated ensure/archive guards are exact complements, so any one update either repairs a projection or retires one, never both.

ParameterKeyCreated and ParameterKeyActivated have no registration anywhere in the module. Creating a key does not touch Revit; activating one does not either. The seven RevitParameterProjection* events are likewise emitted with no behavioural subscriber — only the wildcard DomainEventAuditLogger sees them.

What ensure actually writes

ensureRevitProjectionForParameterKey runs in this order, and the order is load-bearing:

  1. assertSingleProjectionLinkMULTIPLE_PARAMETER_PROJECTIONS when the key links to more than one row, REVIT_PROJECTION_STEWARDSHIP_REQUIRED when it links to an unresolved one.
  2. Load the key — PARAMETER_KEY_NOT_FOUND when it is gone.
  3. If a projection already exists: reuse it when it is archived and the key still carries bim_parameter (see Revit Projection Archive and Reuse); otherwise return it untouched.
  4. Catalog-only short-circuit — a key without bim_parameter returns ok(null). No projection, no error.
  5. requireApprovedParameterKeyPARAMETER_KEY_NOT_APPROVED.
  6. CommonParameterProjectionPolicy.resolve(handle) picks built_in for a registry common, shared for everything else.

The created projection carries only what the key can yield:

FieldValue at ensure
statusdraft — both ensure paths pass initialStatus: 'draft'
specTypeId, storageTypeResolved from the key’s ParameterSpec
revitName, nameSourceDerived from the canonical name (derived); the built_in path uses the enum name and override
disciplinecommand.discipline ?? spec?.discipline ?? 'Common'. Neither ensure path passes a discipline, so in practice the spec’s discipline or Common
visibilityFlagsshared: visible, user-modifiable, not hidden. built_in: visible, not user-modifiable
bindingKindPreferencenull on both paths
revitPrimaryUsagenull on the shared path. A registry built_in entry passes project, because CommonParameterRegistry requires revitPrimaryUsage: 'project' for every entry

Ensure fails loudly

A key that cannot yield a Revit spec produces an error, not a half-formed draft:

CodeFires when
PARAMETER_KEY_NOT_APPROVEDThe key is not approved, on the create path and on archived reuse
PARAMETER_SPEC_ID_MISSINGThe key has bim_parameter but no parameterSpecId. Shared path only — the built_in path resolves a Text spec from the catalog and never reads the key’s spec
SPEC_TYPE_ID_MISSINGNo RevitSpecType is mapped to the key’s spec, or the best mapped candidate has no forgeTypeId. Also raised on the built_in path when no complete Text spec exists
STORAGE_TYPE_MISSINGA mapped spec has a forgeTypeId but no storageType

When more than one spec type is mapped, ensureShared prefers the first candidate with both a forgeTypeId and a storageType; only if none qualifies does it report which half is missing. validateSpec raises the same two latter codes inside create and update, so a direct call cannot bypass them.

Handler failures are recorded through ForRecordingBimOntologyHandlerFailure and then rethrown. AirtableBimOntologyHandlerFailureAdapter creates a script_log row and stamps last_script_status: 'error' plus last_script_message on the parameter_keys row (ADR-0049), so a steward sees the reason on the record they were working on. The archive-on-retirement handler is the exception: it has no failure recorder and only logs before throwing.

The ObjectClass precondition

RevitProjectionApprovePolicy.unmetRequirements is the hard gate and the single source of the missingForApproval field on the detail read — the list is computed by the same unit, never copied.

Applicability is resolved, not authored: RevitCategoryScopeResolver.listApplicableObjectClasses derives the classes from the effective parameter set — direct plus inherited minus suppressed — so the gate covers the classes that actually carry the key. Each unmet class is reported by key and id.

Because ObjectClass approval already requires all four classifications (ADR-0050), an approved projection necessarily has a complete Revit category set. The reverse direction is the subject of Object Class Demotion Cascade.

Only approved projections reach Revit output

OutputFilter
Package manifest (resolveRevitParameterPackage)A projection that is not approved is a hard errorPARAMETER_PROJECTION_INACTIVE. With includeArchived: true, an archived projection is admitted; draft never is
Shared parameter file (generateRevitSharedParameterFile)Same resolution, then only SharedParameterDefinition entries with an SPF datatype survive; others are reported as exclusions rather than dropped silently
Common project parameters (listCommonProjectParameters)Filters status === 'approved' first, then requires ParameterKey.isSystem, an approved BIM key, and a shared definition
Projection list (listRevitParameterProjections)Not gated on approved — it is a steward list with a status filter, and it soft-excludes incomplete rows instead of failing

Airtable entry points

  • AT-B1create_parameter_key.js on ui_create_parameter_key, one row per requested key, POSTing /parameter-keys. It can optionally assign the new key onto object classes in the same run.
  • AT-A1parameter_key_actions.js on ui_parameter_key_actions, flag-based over parameter_keys, handling Activate, Approve, Archive, and Delete. Group assignment is a grid edit of parameter_keys.parameter_group plus AT-C1 parameter-key-projections resync — not an action.

No Airtable script creates, updates, or archives a revit_parameters row. Grepping apps/airtable-front-end/current/bim-ontology/ for revit returns only: comments explaining that scripts never write Revit rows, ObjectClass Revit-category handling (a classification link, not a projection), and two AT-C1 recompute calls in request_reconcile.jsby-parameter-key/{key}/resync and by-parameter-group/{key}/resync. Those are derived-metadata recomputes, not steward writes, and parameter_key_actions.js Delete relies on the API’s cascade. Stewardship of a projection reaches it only through the REST surface.

Code References

  • apps/api-v1/src/modules/bim-ontology/core/domain/parameter-key/ParameterKey.tsapprove, collectApproveMissingRequirements
  • apps/api-v1/src/modules/bim-ontology/core/domain/parameter-key/ParameterStatus.ts — the linear lifecycle
  • apps/api-v1/src/modules/bim-ontology/core/application/use-cases/ParameterKeyAuthoringUseCases.ts
  • apps/api-v1/src/modules/bim-ontology/core/application/use-cases/RevitProjectionAuthoringUseCases.ts — create, update, archive, ensure
  • apps/api-v1/src/modules/bim-ontology/core/application/use-cases/ApproveRevitParameterProjectionUseCase.ts
  • apps/api-v1/src/modules/bim-ontology/core/application/use-cases/demoteApprovedIfSpfOutputChanged.ts
  • apps/api-v1/src/modules/bim-ontology/core/application/use-cases/RevitProjectionReadUseCases.tsmissingForApproval
  • apps/api-v1/src/modules/bim-ontology/core/application/use-cases/ResolveRevitParameterPackageUseCase.ts
  • apps/api-v1/src/modules/bim-ontology/core/application/use-cases/ListCommonProjectParametersUseCase.ts
  • apps/api-v1/src/modules/bim-ontology/core/domain/revit/RevitProjectionApprovePolicy.ts
  • apps/api-v1/src/modules/bim-ontology/core/domain/revit/CommonParameterRegistry.tsCommonParameterProjectionPolicy
  • apps/api-v1/src/modules/bim-ontology/adapters/inbound/events/ParameterKeyApprovedProjectionHandler.ts
  • apps/api-v1/src/modules/bim-ontology/adapters/inbound/events/ParameterKeyUpdatedProjectionEnsureHandler.ts
  • apps/api-v1/src/modules/bim-ontology/adapters/inbound/rest/app/parameter-authoring/parameterAuthoring.openapi.ts
  • apps/api-v1/src/modules/bim-ontology/adapters/inbound/rest/app/revit-parameter-projections/revitParameterProjections.routes.ts
  • apps/api-v1/src/modules/bim-ontology/adapters/inbound/rest/app/revit-parameter-projections/revitParameterProjections.schemas.tsDERIVED_STEWARD_PATCH_FIELDS
  • apps/api-v1/src/modules/bim-ontology/adapters/outbound/persistence/airtable/AirtableBimOntologyHandlerFailureAdapter.ts
  • apps/api-v1/src/modules/bim-ontology/composition/bim-ontology.composition.ts — the ParameterKey* registrations
  • apps/airtable-front-end/current/bim-ontology/create_parameter_key.js, parameter_key_actions.js

ADRs

ADRBearing on this flow
Platform ADR-0047Airtable scripts call the API and never write domain state; handlers recompute seeded scope
Platform ADR-0048domain_id at the public boundary — the :key path parameter is never a rec… id
Platform ADR-0049Handler failures are stamped back onto the subject row as last_script_status / last_script_message
Platform ADR-0050The four-kind ObjectClass approve gate, which is why an approved projection has a complete category set
Platform ADR-0052active belongs to ParameterKey alone; the projection’s old active was renamed approved