domain

Project Delivery Graph

Top-level bounded context for the project graph. The Project Definition Graph is the first model: nodes, relationships, import runs, and the rematerialized summary view. Process Graph and Resource Graph are named and not modeled yet.

DomainStatus: Implemented in api-v1Hybrid Postgres + Airtable

Overview

ADR-0069 places this context at apps/api-v1/src/modules/project-delivery-graph. Project Delivery Graph is the module. Project Definition Graph is the designed-project model inside it.

There is no project-model module and no project-object-model folder. ADR-0069 retired “Project Object Model” as the name of what is being built. Cross-cutting POM language on the platform page still describes the idea of a rendered project graph; this domain owns the implemented definition-graph lifecycle.

The module is served by api-v1. There is no nested System. Revit Connector owns the desktop seam (add-in + panel) that produces the export artifact. This domain owns the model the artifact lands in, and the HTTP ingest on api-v1.

This domain sends four internal in-process events. Three live on the Node aggregate after steward assign/clear/change-class commands; TypeTakeoffPolicySet is emitted after a takeoff-policy write. They are not cross-context integration events — there is no outbox. GraphProjectionOnAssignmentChangedHandler rematerializes the summary view; BomRematerializeOnAssignmentChangedHandler rematerializes the Design BOM (summary first). Do not treat them like ProductVariantCatalogChanged.

Import and rematerialize paths do not add catalog-style integration events. Do not invent SourceImportRegistered or similar.

Implementation Status

Status: Implemented in api-v1

Hybrid persistence (ADR-0068 / ADR-0072):

  • Graph nodes, relationships, import runs, and pdg_summary_row live in Postgres
  • Airtable Design-base tables are a materialized summary view, not the graph store
  • Design revit_import_runs is an Airtable log projection of the same run (status word processing for Postgres in_progress)

Aggregates and supporting types

ConceptPersistence todayBadge
ImportRunPostgres pdg_import_run; Airtable revit_import_runs logImplemented in api-v1
NodePostgres graph nodes (kinds: space, level, phase, assembly_type, part_type, assembly_instance, part_instance). Optional ProductAssignment value object and object_class_domain_id on type nodes.Implemented in api-v1
RelationshipPostgres relationships; not part of the Node aggregateImplemented in api-v1
Graph summary viewPostgres pdg_summary_row; Airtable *_instance_summary (and type/space tables when written)Implemented in api-v1
Design BOMPostgres pdg_bom_contribution ledger + pdg_bom_line; Construction Airtable project_bom_lines count projectionImplemented in api-v1
Takeoff policyPostgres pdg_type_takeoff_policy (value object on a type node; defaults when absent)Implemented in api-v1
ArtifactReferenceValue object on ImportRun (object store key, gzip, SHA-256 of uncompressed JSONL)Implemented in api-v1

Node and Relationship exist in code. They do not have catalog entity pages yet — do not invent extra entity files to fill the tree.

Project is not an aggregate here. HTTP {id} is the project domain_id (for example project-c1). Design-management / projects_sync owns the canonical project record (ADR-0072; live SoR is an open question).

Boundary with Revit Connector

The add-in writes a contract-v1.1 JSONL artifact (phase on LOCATED_IN, sequence on phase nodes, phase on rooms, required baselinePhase in the manifest) and uploads it through object storage (ADR-0071). Bytes do not enter the API process except on the local-dev PUT stand-in. An artifact that omits baselinePhase is refused at intake, not guessed (ADR-0098).

This domain registers that object, processes it into the definition graph, and rematerializes the summary. Folding the graph into revit-connector would make the connector responsible for the model.

Not modeled yet

Do not invent catalog pages for:

  • Process Graph
  • Resource Graph
  • Construction BOM / work recipes (ADR-0078 — not this Design BOM)
  • Type composition (ADR-0075); expand stays reserved and rejected
  • A Project aggregate owned by this domain

Internal domain events

EventWhenConsumer
ProductVariantAssignedToTypeAssign command persistedGraphProjectionOnAssignmentChangedHandler (summary) and BomRematerializeOnAssignmentChangedHandler (BOM)
ProductAssignmentClearedFromTypeClear command persistedsame
TypeObjectClassChangedChange-class command persistedsame
TypeTakeoffPolicySetTakeoff-policy write persistedBomRematerializeOnAssignmentChangedHandler only

Payloads are primitive IDs (and policy scalars on TypeTakeoffPolicySet) only. Dispatch is in-process, best-effort, with handler idempotency — not an outbox.

Flows

  • Ingest Revit Export — upload URL → PUT artifact → register → process (or cron) → rematerialize → poll the import-run log and read the summary

HTTP surface

Canonical mount: /api/v1/projects/{id}/…. Authenticate with X-API-Key. Do not copy request/response schemas here; use generated OpenAPI.

Revit imports (ingest + log)

MethodPathOpenAPI operationIdBehavior
POST/revit-imports/upload-urlrequestRevitImportUploadUrlShort-lived signed URL. Artifact bytes stay out of the API
PUT/revit-imports/local-uploadputLocalRevitImportArtifactLocal-dev stand-in: gzip into the process-wide in-memory Map
POST/revit-importsregisterRevitImportRegister stored object; queue an ImportRun. Idempotent on key or ready hash
GET/revit-importslistRevitImportsImport-run log, newest first. API labels in_progress as processing
GET/revit-imports/{runId}getRevitImportOne run. 404 if missing or another project
POST/revit-imports/{runId}/processprocessRevitImportProcess now. Ready rematerializes once. Cron remains

Definition-graph summary

MethodPathOpenAPI operationIdBehavior
GET/definition-graph/summarygetProjectDefinitionGraphSummaryNested JSON from persisted pdg_summary_row. Does not rematerialize
POST/definition-graph/summary/rematerializerematerializeProjectDefinitionGraphSummaryPersist membership, upsert Airtable, return the nested summary. Does not accept phaseNodeId.

Graph reads

Tagged Project Delivery Graph / Graph Reads. No dedicated flow page.

Query ?phaseNodeId= means alive in that phase, placed where the occurrence stands in that phase — not “created in that phase”.

MethodPathOpenAPI operationId
GET/nodes/{nodeId}getProjectDefinitionGraphNode
GET/nodes/{nodeId}/relationshipsgetProjectDefinitionGraphNodeRelationships
GET/graph/subtree/{nodeId}getProjectDefinitionGraphSubtree
GET/graph/ancestors/{nodeId}getProjectDefinitionGraphAncestors
GET/graph/types/{nodeId}/instancesgetProjectDefinitionGraphTypeInstances
GET/graph/countsgetProjectDefinitionGraphCounts

Type actions (product assignment + object class)

OpenAPI: apps/api-v1/src/modules/project-delivery-graph/adapters/inbound/rest/graph/graphTypeActions.openapi.ts

MethodPathOpenAPI operationIdBehavior
POST/definition-graph/types/actions/assign-product-variantassignProductVariantToTypeAssign ProjectProductVariant to resolved type. Requires Idempotency-Key. Emits ProductVariantAssignedToType.
POST/definition-graph/types/actions/clear-product-assignmentclearProductAssignmentFromTypeClear assignment on resolved type. Emits ProductAssignmentClearedFromType.
POST/definition-graph/types/actions/change-object-classchangeTypeObjectClassChange object class when assignment compatible (409 otherwise). Emits TypeObjectClassChanged.
POST/definition-graph/types/actions/set-takeoff-policysetTypeTakeoffPolicySet takeoff rule, unit, waste factor, and procurement mode on the resolved type. Reserved members are rejected. Emits TypeTakeoffPolicySet.

Auth: X-API-Key + project-scoped design:update. Body userId is audit-only. AT-A1 script: pdg_graph_actions.js (see MUTATION_ROUTING.md). Airtable action dropdown still needs steward option Set Takeoff Policy (tealBright); REST works without it.

Design BOM

OpenAPI: apps/api-v1/src/modules/project-delivery-graph/adapters/inbound/rest/graph/graphBom.openapi.ts

MethodPathOpenAPI operationIdBehavior
GET/definition-graph/bomgetProjectDefinitionGraphBomRead persisted lines + contributions + resolution report. Does not rematerialize. Default omits orphaned lines.
POST/definition-graph/bom/rematerializerematerializeProjectDefinitionGraphBomRematerialize summary first, then BOM. Requires Idempotency-Key. Does not accept phaseNodeId. No PATCH or DELETE.

No ordered/received fields, no purchase-unit conversion, no quantity override.

Primary Code References

  • Module root: apps/api-v1/src/modules/project-delivery-graph
  • Composition: apps/api-v1/src/modules/project-delivery-graph/composition/project-delivery-graph.composition.ts
  • Import-run aggregate: apps/api-v1/src/modules/project-delivery-graph/core/domain/import-run/ImportRun.ts
  • OpenAPI: …/revit-imports/revitImports.openapi.ts, …/graph/graphSummary.openapi.ts, …/graph/graphReads.openapi.ts, …/graph/graphTypeActions.openapi.ts, …/graph/graphBom.openapi.ts
  • Contract notes: docs/api-v1/project-delivery-graph/REVIT_EXPORT_CONTRACT_V1.md

Open Questions

  • Whether the canonical project record stays with design-management (ADR-0072) or is projects_sync / party-directory. Today this module only stores projectId.
  • Process Graph and Resource Graph, when modeled, belong in this bounded context. The Design BOM is modeled (ledger + line + Construction count projection). The Construction BOM (recipes) is not.
  • api-v2 migration. The module lives in api-v1 only. No page here carries Being migrated from api-v1 to api-v2.