entity

Revit Parameter Projection

The Revit-facing expression of exactly one ParameterKey — one definition identity plus optional placements. Lifecycle draft → approved → archived. System-created in draft, steward-completed, steward-approved. Archived, never deleted.

Entity(Aggregate Root)Status: Implemented in api-v1Aggregate Root

Purpose

A Revit parameter projection is the Revit-facing expression of exactly one ParameterKey: one definition identity, plus zero, one, or two placements. The key says what the parameter means; the projection says what Revit sees.

It is the only place Revit vocabulary is allowed to live. The key knows nothing about GUIDs, Forge spec types, binding kinds, or disciplines.

Airtable table revit_parameters.

Lifecycle

draft → approved → archived, from REVIT_PARAMETER_PROJECTION_STATUSES.

TransitionRule
create → draftThe aggregate’s create accepts an initialStatus of draft or approved, but initialStatus is not exposed on the HTTP contract and both ensure paths pass draft. In practice a projection is always born in draft
draft → approvedapprove(), only after RevitProjectionApprovePolicy returns no unmet requirements. Idempotent when already approved
approved → draftdemoteToDraft(). Not a rollback a steward asks for — the system does it when shared-parameter-file output would change
any → archivedarchive(). Idempotent when already archived
archived → draftreactivateFromArchive() only. Archived has no forward transition and cannot be edited or approved directly

There was no active tier and there is no longer one named that: the state now called approved was named active, renamed by platform ADR-0052. Legacy Airtable rows still holding active rehydrate as approvedrehydrateRevitParameterProjectionStatus normalizes the token before validating it. Nothing renders a projection to a customer ahead of review, so the displayable-but-unvetted tier that ParameterKey needs has no meaning here.

An archived projection is frozen: updateAuthoredFacts, approve, and demoteToDraft all refuse it with REVIT_PROJECTION_ARCHIVED (BO-RP-INV-06).

System-created, steward-completed

The projection is created by the system and finished by a human, and the split is deliberate.

Auto-ensure creates the projection in draft carrying only what the ParameterKey can yield: the resolved spec type and storage type, a derived name, default visibility flags, and a discipline. bindingKindPreference and revitPrimaryUsage are left null. The domain must not require Revit-specific facts at draft creation, because at that moment nobody has decided them — so validateRuntimeShape accepts a null binding preference and a null primary usage, and the comment in the source says so explicitly: BO-RP-INV-17 / BO-RP-INV-18 are approve-time only, and draft may persist incomplete primary-usage facts so dirty rows can rehydrate.

The steward then PATCHes the Revit-specific facts and approves. The approve gate returns every unmet requirement at once so that pass is a single round trip.

The ensure trigger is ParameterKeyApproved

The event that mints a projection is ParameterKeyApproved — not ParameterKeyCreated and not ParameterKeyActivated. Those two events exist on the key and have no handler registered anywhere in the module. An active key is deliberately not yet in Revit.

A secondary, guarded ensure runs on ParameterKeyUpdated through ParameterKeyUpdatedProjectionEnsureHandler, whose only purpose is repairing a projection that should exist and does not. It reloads the key and returns without acting unless the key is already approved and carries usage:bim_parameter.

Inside ensureRevitProjectionForParameterKey the ordering is: assert the key links to at most one projection, load the key, reuse an archived projection if one exists, short-circuit on catalog-only keys, then require approval.

  • Catalog-only keys short-circuit. A key whose usages are product_spec or variant_option and which lacks bim_parameter returns null. No projection, no error.
  • PARAMETER_KEY_NOT_APPROVED is returned both on create and on archived-reuse when the key is not approved. The check runs before the reuse write, so an unapproved key cannot resurrect an archived projection.
  • Registry commons resolved by CommonParameterProjectionPolicy prefer a built_in definition; every other steward key defaults to shared.

Spec-type errors are hard, not silent

A key that cannot yield a Revit spec fails loudly rather than producing a half-formed draft:

CodeRaised when
PARAMETER_SPEC_ID_MISSINGA bim_parameter key has no ParameterSpec at all
SPEC_TYPE_ID_MISSINGNo Revit spec type is mapped to that spec, or the mapped spec has no forgeTypeId
STORAGE_TYPE_MISSINGThe mapped spec has a forgeTypeId but no storage type

validateAuthoredStructure raises the same two latter codes at the aggregate boundary, so neither can be bypassed by a direct create.

Approve gate

RevitProjectionApprovePolicy.unmetRequirements is the hard gate and the single source for the missingForApproval field on the detail read — the list is never copied.

RequirementNote
parameterKey.approvedThe linked key must be approved
usage:bim_parameterThe key must carry that usage
discipline, revitSpecTypeMust resolve
enabledApplicationAt least one of family application or project binding, unless the definition is built_in (BO-RP-INV-11)
bindingKindPreferenceRequired unless built_in
revitPrimaryUsageAlways required
BO-RP-INV-17A system projection requires revitPrimaryUsage of project
BO-RP-INV-18revitPrimaryUsage must name an actually enabled usage
sharedGuidA shared definition requires its GUID
objectClass.approvedEvery ObjectClass the projection applies to must itself be approved
status.not_archivedAn archived projection must be reused into draft first

The ObjectClass gate is the load-bearing one. Applicability is resolved through class assignment — the effective parameter set, direct plus inherited minus suppressed — so it is the classes that actually carry the key, not an authored list. 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 category set: there is no way to reach an approved projection through an approved class that is missing its Revit category. Uniformat and Masterformat sit in that same gate deliberately even though Revit does not read them today — Revit will consume them later, and admitting a class without them would mean re-gating then.

Revit category scope is derived only

Category scope is the set of Revit categories a project binding covers. It is derived and only derived, resolved from the effective class assignments and the classes’ own Revit category links.

The steward override is gone. explicitCategoryRefs and the 'explicit' scope provenance were removed; the name survives in exactly one place, the DERIVED_STEWARD_PATCH_FIELDS reject-list, where sending it earns an HTTP 400 naming the field. Scope kinds are now derived | unscopedderived when a project binding resolves a scope, unscoped when there is no project binding or no resolved scope.

Categories are controlled through class assignments and the classes’ own categories, never by overriding the derived output. resolveRevitParameterCategoryScope computes the current scope with no writes at all; persisting the derived set is a separate use case.

The word scope in this module means this and nothing else — not parameter applicability, not graph traversal extent, and not a class’s own category classification, which is a link. See ubiquitous language.

Archive, never delete

Retirement archives. It does not delete. The one exception is cascade: when the ParameterKey itself is hard-deleted, DeleteParameterKeyUseCase cascade-deletes the linked projection and reports the deleted cascadedRevitProjectionId.

ArchiveRevitProjectionOnParameterKeyRetirementHandler is registered for both ParameterKeyUpdated and ParameterKeyArchived. On retirement the link to the key is preserved and the shared-parameter GUID is never recycledupdateAuthoredFacts refuses to remove or replace a shared GUID with REVIT_GUID_IMMUTABLE (BO-RP-INV-01), and archiving keeps the row.

That immutability is the whole reason for the archive-then-reuse shape. Re-adding bim_parameter to a key searches for an existing linked projection including archived ones and reactivates it into draft; it only creates when none exists. A steward who retires and later reinstates a parameter gets the same GUID back, so any Revit model already carrying it still resolves.

Canonical-name resync demotes

The effective Revit name is normally derived from the canonical ParameterKey name, so renaming the key drifts the projection. resyncDerivedName refreshes the stored cache, and demoteApprovedIfSpfOutputChanged decides what that costs.

A resync of an approved projection demotes it to draft when the shared-parameter-file output would change — the comparison is on revitName and nameSource, alongside binding kind preference, primary usage, the two application binding kinds, and visibility flags. Approved means vetted for Revit emission, so any actual change to what would be emitted sends the row back through the gate.

Override names no-op and do not demote. resyncDerivedName returns the same instance untouched when nameSource === 'override' or when the derived name already equals the stored one, so nothing is saved, no event fires, and the status is unchanged. The same is true of a no-op PATCH.

Drift that is not repaired silently is reported instead. listRevitProjectionDerivationDrift categorizes projection_missing, name_stale, name_override, missing_group, and revit_ui_group_missing_or_invalid. A canonical rename is also preflighted for Revit-name collisions before any Airtable write, failing with REVIT_PARAMETER_NAME_COLLISION rather than minting a duplicate.

Definition versus Reference

RevitDefinitionForm is shared, family_local, project_local, built_in, legacy_unknown.

The distinction the words carry is binding: a Definition is an identity BuildPlan authors and owns; a Reference is an external identity it reads but never authors. SharedParameterDefinition and FamilyLocalParameterDefinition are authored. ProjectLocalParameterReference, BuiltInParameterReference, and LegacyUnknownParameterReference are not BuildPlan’s to mint.

FormCreatableNotes
sharedYesOwns the GUID. The only form that may carry a project binding (BO-RP-INV-07)
family_localYesFamily application only; a project binding is refused (BO-RP-INV-10)
built_inYesRequires builtInParameterName; may carry neither application (BO-RP-INV-05)
project_localNoRead-only imported reference (BO-RP-INV-16, READ_ONLY_DEFINITION_FORM)
legacy_unknownNoRead-only; provenance was never recorded

Changing form after the fact is refused with DEFINITION_FORM_CHANGE_REQUIRES_ARCHIVE, because the Revit parameter identity would change. Archive and create a new one.

Invariants

  • Exactly one ParameterKey per projection, non-empty (BO-RP-INV-02), and at most one projection per key — MULTIPLE_PARAMETER_PROJECTIONS when the key links to more than one, REVIT_PROJECTION_STEWARDSHIP_REQUIRED when it links to an unresolved row.
  • A shared GUID is immutable (BO-RP-INV-01) and globally unique (SHARED_GUID_CONFLICT); builtInParameterName is likewise unique (BUILT_IN_PARAMETER_NAME_CONFLICT).
  • An authored projection requires a name, a name source, and visibility flags, plus a Forge spec type and a storage type.
  • Beyond draft, a non-built-in projection requires at least one enabled application (NO_ENABLED_REVIT_APPLICATION).
  • Rehydrating an unrecognized status fails; there is no default.
  • Every instance is frozen after construction — mutators return a new aggregate rather than mutating in place.

Domain Events

Exactly seven, all on one payload shape (projectionId, parameterKeyId, occurredAt):

EventEmitted by
RevitParameterProjectionCreatedcreate, when a clock is supplied
RevitParameterProjectionUpdatedupdateAuthoredFacts
RevitParameterProjectionApprovedapprove
RevitParameterProjectionDemoteddemoteToDraft
RevitParameterProjectionReusedreactivateFromArchive
RevitParameterProjectionArchivedarchive
RevitParameterProjectionResyncedresyncDerivedName, only when the name actually changed

None of the seven has a subscriber inside the module. They are dispatched and recorded; no handler is registered for any of them. Every consequence documented above — ensure, archive-on-retirement, resync-on-canonical-change — is driven by a ParameterKey* or ParameterGroupChanged event flowing into the projection side, never by a projection event flowing out. Treat these seven as emitted-but-unsubscribed until a consumer is registered.

OpenAPI Operations

The user-facing REST surface is deliberately narrow: a PATCH of Revit-specific fields, an approve action, and reads.

The PATCH accepts binding kind preference, Revit primary usage, the two applications and their binding kinds, and the visibility flags. Everything derived or key-owned is refused with an HTTP 400 naming the field.

Create, admin archive, and the three resync endpoints are maintenance and drift-repair paths, marked x-internal: true and tagged system-maintenance:

No Airtable script touches a Revit row. Every AT-C1 derivation in this base watches object_classes, class_parameters, class_parameter_allowed_values, parameter_keys, or parameter_groups — never revit_parameters — and the routing matrix marks the revit_parameters grid fields lock or monitor. Stewardship reaches the projection only through the API. See MUTATION_ROUTING.md.

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.

ADRDecision
Platform ADR-0046The object-class hierarchy is a single-parent tree, so applicability follows one path to the root
Platform ADR-0048domain_id is the public identifier; never a rec… id in a payload
Platform ADR-0050The four-kind classification approve gate on ObjectClass, which is why an approved projection has a complete category set
Platform ADR-0052active belongs to ParameterKey alone; this aggregate’s active was renamed approved
Per-file ADR-0006Canonical parameter keys and values with per-level junctions