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.
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_rowlive in Postgres - Airtable Design-base tables are a materialized summary view, not the graph store
- Design
revit_import_runsis an Airtable log projection of the same run (status wordprocessingfor Postgresin_progress)
Aggregates and supporting types
| Concept | Persistence today | Badge |
|---|---|---|
| ImportRun | Postgres pdg_import_run; Airtable revit_import_runs log | Implemented in api-v1 |
Node | Postgres 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 |
Relationship | Postgres relationships; not part of the Node aggregate | Implemented in api-v1 |
| Graph summary view | Postgres pdg_summary_row; Airtable *_instance_summary (and type/space tables when written) | Implemented in api-v1 |
| Design BOM | Postgres pdg_bom_contribution ledger + pdg_bom_line; Construction Airtable project_bom_lines count projection | Implemented in api-v1 |
| Takeoff policy | Postgres pdg_type_takeoff_policy (value object on a type node; defaults when absent) | Implemented in api-v1 |
ArtifactReference | Value 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);
expandstays reserved and rejected - A
Projectaggregate owned by this domain
Internal domain events
| Event | When | Consumer |
|---|---|---|
| ProductVariantAssignedToType | Assign command persisted | GraphProjectionOnAssignmentChangedHandler (summary) and BomRematerializeOnAssignmentChangedHandler (BOM) |
| ProductAssignmentClearedFromType | Clear command persisted | same |
| TypeObjectClassChanged | Change-class command persisted | same |
| TypeTakeoffPolicySet | Takeoff-policy write persisted | BomRematerializeOnAssignmentChangedHandler 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)
| Method | Path | OpenAPI operationId | Behavior |
|---|---|---|---|
POST | /revit-imports/upload-url | requestRevitImportUploadUrl | Short-lived signed URL. Artifact bytes stay out of the API |
PUT | /revit-imports/local-upload | putLocalRevitImportArtifact | Local-dev stand-in: gzip into the process-wide in-memory Map |
POST | /revit-imports | registerRevitImport | Register stored object; queue an ImportRun. Idempotent on key or ready hash |
GET | /revit-imports | listRevitImports | Import-run log, newest first. API labels in_progress as processing |
GET | /revit-imports/{runId} | getRevitImport | One run. 404 if missing or another project |
POST | /revit-imports/{runId}/process | processRevitImport | Process now. Ready rematerializes once. Cron remains |
Definition-graph summary
| Method | Path | OpenAPI operationId | Behavior |
|---|---|---|---|
GET | /definition-graph/summary | getProjectDefinitionGraphSummary | Nested JSON from persisted pdg_summary_row. Does not rematerialize |
POST | /definition-graph/summary/rematerialize | rematerializeProjectDefinitionGraphSummary | Persist 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”.
| Method | Path | OpenAPI operationId |
|---|---|---|
GET | /nodes/{nodeId} | getProjectDefinitionGraphNode |
GET | /nodes/{nodeId}/relationships | getProjectDefinitionGraphNodeRelationships |
GET | /graph/subtree/{nodeId} | getProjectDefinitionGraphSubtree |
GET | /graph/ancestors/{nodeId} | getProjectDefinitionGraphAncestors |
GET | /graph/types/{nodeId}/instances | getProjectDefinitionGraphTypeInstances |
GET | /graph/counts | getProjectDefinitionGraphCounts |
Type actions (product assignment + object class)
OpenAPI: apps/api-v1/src/modules/project-delivery-graph/adapters/inbound/rest/graph/graphTypeActions.openapi.ts
| Method | Path | OpenAPI operationId | Behavior |
|---|---|---|---|
POST | /definition-graph/types/actions/assign-product-variant | assignProductVariantToType | Assign ProjectProductVariant to resolved type. Requires Idempotency-Key. Emits ProductVariantAssignedToType. |
POST | /definition-graph/types/actions/clear-product-assignment | clearProductAssignmentFromType | Clear assignment on resolved type. Emits ProductAssignmentClearedFromType. |
POST | /definition-graph/types/actions/change-object-class | changeTypeObjectClass | Change object class when assignment compatible (409 otherwise). Emits TypeObjectClassChanged. |
POST | /definition-graph/types/actions/set-takeoff-policy | setTypeTakeoffPolicy | Set 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
| Method | Path | OpenAPI operationId | Behavior |
|---|---|---|---|
GET | /definition-graph/bom | getProjectDefinitionGraphBom | Read persisted lines + contributions + resolution report. Does not rematerialize. Default omits orphaned lines. |
POST | /definition-graph/bom/rematerialize | rematerializeProjectDefinitionGraphBom | Rematerialize 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
Documentation Links
- api-v1 service:
/docs/services/api-v1 - Ubiquitous language:
/docs/domains/project-delivery-graph/language - Revit Connector:
/docs/systems/revit-connector - Backend platform:
/docs/systems/backend-platform - ADRs:
docs/adr/README.md
Open Questions
- Whether the canonical project record stays with design-management (ADR-0072) or is
projects_sync/ party-directory. Today this module only storesprojectId. - 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.