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 (imported by type, or by type and space; authored lines inserted). Construction project_bom_lines is the line projection (ADR-0131)Implemented 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 party-directory project domain_id (for example project-c1). Party-directory projects is the system of record for existence and the plugin picker. Design projects_sync is a read-only mirror used to fill Airtable project links once domain_id matches.

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; local PUT-is-submit and dev-revit-imports in ADR-0103). An artifact that omits baselinePhase is refused at intake, not guessed (ADR-0098). {id} and manifest.projectId must be a known party-directory domain_id; the document title is never a project id.

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 (submit) → process (or cron) → rematerialize → poll. Register is fallback only. Delete-graph / same-run retry: ADR-0103.

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 URL. Production signed URL keeps bytes out of the API
PUT/revit-imports/local-uploadputLocalRevitImportArtifactLocal-dev stand-in: PUT is submit; live composition writes dev-revit-imports
POST/revit-importsregisterRevitImportFallback when storage PUT did not queue. Idempotent on key (skips deleted)
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 summary first. Cron remains in production
POST/revit-imports/{runId}/actions/delete-graphdeleteRevitImportGraphWipe imported_from_source nodes; mark run deleted; keep gzip
POST/revit-imports/{runId}/actions/retryretryRevitImportSame-run requeue from failed or deleted

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, including origin, spaces, prices, and product code. Does not rematerialize. Default omits orphaned lines.
POST/definition-graph/bom/linesaddProjectDefinitionGraphBomLinesInsert authored lines and one unbound product code each, then project them. Requires Idempotency-Key. Taken requestedOrdinal is 409.
POST/definition-graph/bom/lines/{domainId}/actions/deletedeleteAuthoredBomLineDelete an authored line. Refuses an imported line. Requires Idempotency-Key.
POST/definition-graph/bom/lines/{domainId}/actions/assign-codeassignProductCodeToAuthoredBomLineRename or issue the line’s unbound code. Refuses an imported line and a code bound to a type. Requires Idempotency-Key.
POST/definition-graph/bom/lines/{domainId}/actions/reconcile-takeoffreconcileAuthoredBomLineTakeoffBody { derivation: "authored-takeoff" } only. Authored lines adopt Construction quantity and unit. Requires Idempotency-Key.
POST/definition-graph/bom/rematerializerematerializeProjectDefinitionGraphBomRematerialize summary first, then imported lines. Query grain is collapsed (default) or by_space. Requires Idempotency-Key. Does not accept phaseNodeId.

No ordered/received fields, no purchase-unit conversion, no quantity field on the takeoff reconcile. Hand-added lines are not orphaned by rematerialize. Product-code rename and retire (POST /api/v1/design/product-codes/{productCodeId}/actions/rename and .../retire) update every BOM line that stores that code.

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

  • Process Graph and Resource Graph, when modeled, belong in this bounded context. The Design BOM is modeled (ledger + imported and authored lines + Construction projection, ADR-0131). 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.