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.
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.
| Operation | Route | Role in this flow |
|---|---|---|
| createParameterKey | POST /parameter-keys | Creates the key in draft. Requires Idempotency-Key |
| activateParameterKey | POST /parameter-keys/{id}/actions/activate | draft → active |
| approveParameterKey | POST /parameter-keys/{id}/actions/approve | active → approved. The trigger for everything downstream |
| updateParameterKey | POST /parameter-keys/{id}/actions/update | Raises ParameterKeyUpdated, which drives the guarded repair ensure |
| getRevitParameterProjection | GET /revit/parameter-projections/{key} | Detail read, including missingForApproval from the same policy the approve gate uses |
| updateRevitParameterProjection | PATCH /revit/parameter-projections/{key} | The steward completion step |
| approveRevitParameterProjection | POST /revit/parameter-projections/{key}/actions/approve | draft → approved, gated |
| resolveRevitParameterPackage | POST /revit/parameter-packages/resolve | Manifest — approved projections only |
| generateRevitSharedParameterFile | POST /revit/parameter-packages/shared-parameter-file | SPF text from the same resolution |
| listCommonProjectParameters | GET /revit/common-project-parameters | Installable 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:
| Requirement | Missing-reference token |
|---|---|
A ParameterSpec reference | parameterSpecId |
A ParameterControl reference | parameterControlId |
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.
| Event | Registered handlers | Guard |
|---|---|---|
ParameterKeyApproved | ParameterKeyApprovedProjectionHandler | None beyond idempotency — ensure itself decides |
ParameterKeyUpdated | ParameterKeyUpdatedProjectionEnsureHandler | Key is approved and carries usage:bim_parameter |
ParameterKeyUpdated | ArchiveRevitProjectionOnParameterKeyRetirementHandler | Key is not approved or has lost usage:bim_parameter |
ParameterKeyUpdated | CanonicalProjectionMetadataChangedHandler | None — resyncs the derived Revit name |
ParameterKeyArchived | ArchiveRevitProjectionOnParameterKeyRetirementHandler | Always archives |
ParameterKeyCreated, ParameterKeyActivated, ParameterKeyDeleted | none | — |
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:
assertSingleProjectionLink—MULTIPLE_PARAMETER_PROJECTIONSwhen the key links to more than one row,REVIT_PROJECTION_STEWARDSHIP_REQUIREDwhen it links to an unresolved one.- Load the key —
PARAMETER_KEY_NOT_FOUNDwhen it is gone. - If a projection already exists: reuse it when it is
archivedand the key still carriesbim_parameter(see Revit Projection Archive and Reuse); otherwise return it untouched. - Catalog-only short-circuit — a key without
bim_parameterreturnsok(null). No projection, no error. requireApprovedParameterKey—PARAMETER_KEY_NOT_APPROVED.CommonParameterProjectionPolicy.resolve(handle)picksbuilt_infor a registry common,sharedfor everything else.
The created projection carries only what the key can yield:
| Field | Value at ensure |
|---|---|
status | draft — both ensure paths pass initialStatus: 'draft' |
specTypeId, storageType | Resolved from the key’s ParameterSpec |
revitName, nameSource | Derived from the canonical name (derived); the built_in path uses the enum name and override |
discipline | command.discipline ?? spec?.discipline ?? 'Common'. Neither ensure path passes a discipline, so in practice the spec’s discipline or Common |
visibilityFlags | shared: visible, user-modifiable, not hidden. built_in: visible, not user-modifiable |
bindingKindPreference | null on both paths |
revitPrimaryUsage | null 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:
| Code | Fires when |
|---|---|
PARAMETER_KEY_NOT_APPROVED | The key is not approved, on the create path and on archived reuse |
PARAMETER_SPEC_ID_MISSING | The 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_MISSING | No 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_MISSING | A 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
| Output | Filter |
|---|---|
Package manifest (resolveRevitParameterPackage) | A projection that is not approved is a hard error — PARAMETER_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-B1 —
create_parameter_key.jsonui_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-A1 —
parameter_key_actions.jsonui_parameter_key_actions, flag-based overparameter_keys, handling Activate, Approve, Archive, and Delete. Group assignment is a grid edit ofparameter_keys.parameter_groupplus AT-C1parameter-key-projectionsresync — 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.js — by-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.ts—approve,collectApproveMissingRequirementsapps/api-v1/src/modules/bim-ontology/core/domain/parameter-key/ParameterStatus.ts— the linear lifecycleapps/api-v1/src/modules/bim-ontology/core/application/use-cases/ParameterKeyAuthoringUseCases.tsapps/api-v1/src/modules/bim-ontology/core/application/use-cases/RevitProjectionAuthoringUseCases.ts— create, update, archive, ensureapps/api-v1/src/modules/bim-ontology/core/application/use-cases/ApproveRevitParameterProjectionUseCase.tsapps/api-v1/src/modules/bim-ontology/core/application/use-cases/demoteApprovedIfSpfOutputChanged.tsapps/api-v1/src/modules/bim-ontology/core/application/use-cases/RevitProjectionReadUseCases.ts—missingForApprovalapps/api-v1/src/modules/bim-ontology/core/application/use-cases/ResolveRevitParameterPackageUseCase.tsapps/api-v1/src/modules/bim-ontology/core/application/use-cases/ListCommonProjectParametersUseCase.tsapps/api-v1/src/modules/bim-ontology/core/domain/revit/RevitProjectionApprovePolicy.tsapps/api-v1/src/modules/bim-ontology/core/domain/revit/CommonParameterRegistry.ts—CommonParameterProjectionPolicyapps/api-v1/src/modules/bim-ontology/adapters/inbound/events/ParameterKeyApprovedProjectionHandler.tsapps/api-v1/src/modules/bim-ontology/adapters/inbound/events/ParameterKeyUpdatedProjectionEnsureHandler.tsapps/api-v1/src/modules/bim-ontology/adapters/inbound/rest/app/parameter-authoring/parameterAuthoring.openapi.tsapps/api-v1/src/modules/bim-ontology/adapters/inbound/rest/app/revit-parameter-projections/revitParameterProjections.routes.tsapps/api-v1/src/modules/bim-ontology/adapters/inbound/rest/app/revit-parameter-projections/revitParameterProjections.schemas.ts—DERIVED_STEWARD_PATCH_FIELDSapps/api-v1/src/modules/bim-ontology/adapters/outbound/persistence/airtable/AirtableBimOntologyHandlerFailureAdapter.tsapps/api-v1/src/modules/bim-ontology/composition/bim-ontology.composition.ts— theParameterKey*registrationsapps/airtable-front-end/current/bim-ontology/create_parameter_key.js,parameter_key_actions.js
ADRs
| ADR | Bearing on this flow |
|---|---|
| Platform ADR-0047 | Airtable scripts call the API and never write domain state; handlers recompute seeded scope |
| Platform ADR-0048 | domain_id at the public boundary — the :key path parameter is never a rec… id |
| Platform ADR-0049 | Handler failures are stamped back onto the subject row as last_script_status / last_script_message |
| Platform ADR-0050 | The four-kind ObjectClass approve gate, which is why an approved projection has a complete category set |
| Platform ADR-0052 | active belongs to ParameterKey alone; the projection’s old active was renamed approved |