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.
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.
| Transition | Rule |
|---|---|
create → draft | The 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 → approved | approve(), only after RevitProjectionApprovePolicy returns no unmet requirements. Idempotent when already approved |
approved → draft | demoteToDraft(). Not a rollback a steward asks for — the system does it when shared-parameter-file output would change |
any → archived | archive(). Idempotent when already archived |
archived → draft | reactivateFromArchive() 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 approved — rehydrateRevitParameterProjectionStatus 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_specorvariant_optionand which lacksbim_parameterreturnsnull. No projection, no error. PARAMETER_KEY_NOT_APPROVEDis returned both on create and on archived-reuse when the key is notapproved. The check runs before the reuse write, so an unapproved key cannot resurrect an archived projection.- Registry commons resolved by
CommonParameterProjectionPolicyprefer abuilt_indefinition; every other steward key defaults toshared.
Spec-type errors are hard, not silent
A key that cannot yield a Revit spec fails loudly rather than producing a half-formed draft:
| Code | Raised when |
|---|---|
PARAMETER_SPEC_ID_MISSING | A bim_parameter key has no ParameterSpec at all |
SPEC_TYPE_ID_MISSING | No Revit spec type is mapped to that spec, or the mapped spec has no forgeTypeId |
STORAGE_TYPE_MISSING | The 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.
| Requirement | Note |
|---|---|
parameterKey.approved | The linked key must be approved |
usage:bim_parameter | The key must carry that usage |
discipline, revitSpecType | Must resolve |
enabledApplication | At least one of family application or project binding, unless the definition is built_in (BO-RP-INV-11) |
bindingKindPreference | Required unless built_in |
revitPrimaryUsage | Always required |
BO-RP-INV-17 | A system projection requires revitPrimaryUsage of project |
BO-RP-INV-18 | revitPrimaryUsage must name an actually enabled usage |
sharedGuid | A shared definition requires its GUID |
objectClass.approved | Every ObjectClass the projection applies to must itself be approved |
status.not_archived | An 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 | unscoped — derived 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 recycled — updateAuthoredFacts 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.
| Form | Creatable | Notes |
|---|---|---|
shared | Yes | Owns the GUID. The only form that may carry a project binding (BO-RP-INV-07) |
family_local | Yes | Family application only; a project binding is refused (BO-RP-INV-10) |
built_in | Yes | Requires builtInParameterName; may carry neither application (BO-RP-INV-05) |
project_local | No | Read-only imported reference (BO-RP-INV-16, READ_ONLY_DEFINITION_FORM) |
legacy_unknown | No | Read-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_PROJECTIONSwhen the key links to more than one,REVIT_PROJECTION_STEWARDSHIP_REQUIREDwhen it links to an unresolved row. - A shared GUID is immutable (
BO-RP-INV-01) and globally unique (SHARED_GUID_CONFLICT);builtInParameterNameis 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):
| Event | Emitted by |
|---|---|
RevitParameterProjectionCreated | create, when a clock is supplied |
RevitParameterProjectionUpdated | updateAuthoredFacts |
RevitParameterProjectionApproved | approve |
RevitParameterProjectionDemoted | demoteToDraft |
RevitParameterProjectionReused | reactivateFromArchive |
RevitParameterProjectionArchived | archive |
RevitParameterProjectionResynced | resyncDerivedName, 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.
- listRevitParameterProjections
- getRevitParameterProjection
- updateRevitParameterProjection
- approveRevitParameterProjection
- lookupRevitParameterProjections
- resolveRevitParameterCategoryScope
- listRevitProjectionDerivationDrift
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:
- createRevitParameterProjection
- archiveRevitParameterProjection
- resyncDerivedRevitProjectionMetadata
- resyncRevitProjectionsForParameterKey
- resyncRevitProjectionsForParameterGroup
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.
| ADR | Decision |
|---|---|
| Platform ADR-0046 | The object-class hierarchy is a single-parent tree, so applicability follows one path to the root |
| Platform ADR-0048 | domain_id is the public identifier; never a rec… id in a payload |
| Platform ADR-0050 | The four-kind classification approve gate on ObjectClass, which is why an approved projection has a complete category set |
| Platform ADR-0052 | active belongs to ParameterKey alone; this aggregate’s active was renamed approved |
| Per-file ADR-0006 | Canonical parameter keys and values with per-level junctions |