@twinfinity/twin 0.0.0-feature.31596

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +277 -0
  2. package/dist/BuildInfo.d.ts +25 -0
  3. package/dist/BuildInfo.d.ts.map +1 -0
  4. package/dist/BuildInfo.js +15 -0
  5. package/dist/BuildInfo.js.map +1 -0
  6. package/dist/TwinClient.d.ts +823 -0
  7. package/dist/TwinClient.d.ts.map +1 -0
  8. package/dist/TwinClient.js +1487 -0
  9. package/dist/TwinClient.js.map +1 -0
  10. package/dist/errors.d.ts +289 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +357 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +4 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/tsconfig.tsbuildinfo +1 -0
  19. package/dist/types/Twin.d.ts +7 -0
  20. package/dist/types/Twin.d.ts.map +1 -0
  21. package/dist/types/Twin.js +6 -0
  22. package/dist/types/Twin.js.map +1 -0
  23. package/dist/types/TwinGeometryInfo.d.ts +7 -0
  24. package/dist/types/TwinGeometryInfo.d.ts.map +1 -0
  25. package/dist/types/TwinGeometryInfo.js +2 -0
  26. package/dist/types/TwinGeometryInfo.js.map +1 -0
  27. package/dist/types/asset-api.d.ts +66 -0
  28. package/dist/types/asset-api.d.ts.map +1 -0
  29. package/dist/types/asset-api.js +2 -0
  30. package/dist/types/asset-api.js.map +1 -0
  31. package/dist/types/asset-attachment-api.d.ts +51 -0
  32. package/dist/types/asset-attachment-api.d.ts.map +1 -0
  33. package/dist/types/asset-attachment-api.js +8 -0
  34. package/dist/types/asset-attachment-api.js.map +1 -0
  35. package/dist/types/asset-batch-api.d.ts +73 -0
  36. package/dist/types/asset-batch-api.d.ts.map +1 -0
  37. package/dist/types/asset-batch-api.js +2 -0
  38. package/dist/types/asset-batch-api.js.map +1 -0
  39. package/dist/types/asset-types-api.d.ts +76 -0
  40. package/dist/types/asset-types-api.d.ts.map +1 -0
  41. package/dist/types/asset-types-api.js +5 -0
  42. package/dist/types/asset-types-api.js.map +1 -0
  43. package/dist/types/common.d.ts +10 -0
  44. package/dist/types/common.d.ts.map +1 -0
  45. package/dist/types/common.js +2 -0
  46. package/dist/types/common.js.map +1 -0
  47. package/dist/types/generated.d.ts +458 -0
  48. package/dist/types/generated.d.ts.map +1 -0
  49. package/dist/types/generated.js +2 -0
  50. package/dist/types/generated.js.map +1 -0
  51. package/dist/types/index.d.ts +10 -0
  52. package/dist/types/index.d.ts.map +1 -0
  53. package/dist/types/index.js +10 -0
  54. package/dist/types/index.js.map +1 -0
  55. package/dist/types/twin-api.d.ts +141 -0
  56. package/dist/types/twin-api.d.ts.map +1 -0
  57. package/dist/types/twin-api.js +2 -0
  58. package/dist/types/twin-api.js.map +1 -0
  59. package/dist/types/twin-file-api.d.ts +61 -0
  60. package/dist/types/twin-file-api.d.ts.map +1 -0
  61. package/dist/types/twin-file-api.js +2 -0
  62. package/dist/types/twin-file-api.js.map +1 -0
  63. package/dist/types/twin-permissions-api.d.ts +240 -0
  64. package/dist/types/twin-permissions-api.d.ts.map +1 -0
  65. package/dist/types/twin-permissions-api.js +2 -0
  66. package/dist/types/twin-permissions-api.js.map +1 -0
  67. package/docs/.nojekyll +1 -0
  68. package/docs/assets/hierarchy.js +1 -0
  69. package/docs/assets/highlight.css +99 -0
  70. package/docs/assets/icons.js +18 -0
  71. package/docs/assets/icons.svg +1 -0
  72. package/docs/assets/main.js +60 -0
  73. package/docs/assets/navigation.js +1 -0
  74. package/docs/assets/search.js +1 -0
  75. package/docs/assets/style.css +1633 -0
  76. package/docs/classes/AssetAttachmentConflictError.html +3 -0
  77. package/docs/classes/AssetAttachmentNotFoundError.html +2 -0
  78. package/docs/classes/AssetBatchError.html +3 -0
  79. package/docs/classes/AssetCatalogTooLargeError.html +16 -0
  80. package/docs/classes/AssetCategoryConflictError.html +3 -0
  81. package/docs/classes/AssetCategoryForbiddenError.html +2 -0
  82. package/docs/classes/AssetCategoryNotFoundError.html +2 -0
  83. package/docs/classes/AssetCategoryValidationError.html +3 -0
  84. package/docs/classes/AssetConflictError.html +3 -0
  85. package/docs/classes/AssetForbiddenError.html +2 -0
  86. package/docs/classes/AssetNotFoundError.html +2 -0
  87. package/docs/classes/AssetPermissionsConflictError.html +16 -0
  88. package/docs/classes/AssetTypeConflictError.html +3 -0
  89. package/docs/classes/AssetTypeForbiddenError.html +2 -0
  90. package/docs/classes/AssetTypeNotFoundError.html +2 -0
  91. package/docs/classes/AssetTypeValidationError.html +3 -0
  92. package/docs/classes/AssetValidationError.html +3 -0
  93. package/docs/classes/ProcessorNotFoundError.html +2 -0
  94. package/docs/classes/TwinClient.html +409 -0
  95. package/docs/classes/TwinConflictError.html +3 -0
  96. package/docs/classes/TwinFileConflictError.html +3 -0
  97. package/docs/classes/TwinFileNotFoundError.html +2 -0
  98. package/docs/classes/TwinFileValidationError.html +3 -0
  99. package/docs/classes/TwinNotFoundError.html +4 -0
  100. package/docs/classes/TwinPermissionsConflictError.html +19 -0
  101. package/docs/classes/TwinPermissionsForbiddenError.html +12 -0
  102. package/docs/classes/TwinPrincipalNotFoundError.html +5 -0
  103. package/docs/classes/TwinTemporarilyUnavailableError.html +10 -0
  104. package/docs/classes/TwinUnauthenticatedError.html +6 -0
  105. package/docs/classes/TwinValidationError.html +3 -0
  106. package/docs/functions/isAssetCatalogTooLargeErrorBody.html +6 -0
  107. package/docs/functions/readErrorBody.html +10 -0
  108. package/docs/hierarchy.html +1 -0
  109. package/docs/index.html +168 -0
  110. package/docs/interfaces/ApiCreateUploadSessionResponse.html +7 -0
  111. package/docs/interfaces/AssetAttachmentPut.html +6 -0
  112. package/docs/interfaces/AssetBatchDeleteOperation.html +4 -0
  113. package/docs/interfaces/AssetBatchGetRequest.html +2 -0
  114. package/docs/interfaces/AssetBatchGetResponse.html +3 -0
  115. package/docs/interfaces/AssetBatchRequest.html +2 -0
  116. package/docs/interfaces/AssetBatchResponse.html +3 -0
  117. package/docs/interfaces/AssetBatchRestoreOperation.html +4 -0
  118. package/docs/interfaces/AssetBatchUpsertItem.html +12 -0
  119. package/docs/interfaces/AssetBatchUpsertOperation.html +4 -0
  120. package/docs/interfaces/AssetCatalogCategory.html +7 -0
  121. package/docs/interfaces/AssetCatalogResponse.html +6 -0
  122. package/docs/interfaces/AssetCatalogTooLargeErrorBody.html +15 -0
  123. package/docs/interfaces/AssetCategoryDelete.html +2 -0
  124. package/docs/interfaces/AssetCategoryGet.html +14 -0
  125. package/docs/interfaces/AssetCategoryPut.html +7 -0
  126. package/docs/interfaces/AssetGet.html +18 -0
  127. package/docs/interfaces/AssetGroupGet.html +9 -0
  128. package/docs/interfaces/AssetGroupsGet.html +3 -0
  129. package/docs/interfaces/AssetListItem.html +14 -0
  130. package/docs/interfaces/AssetPermissionsConflictErrorBody.html +12 -0
  131. package/docs/interfaces/AssetPermissionsPut.html +9 -0
  132. package/docs/interfaces/AssetPosition.html +4 -0
  133. package/docs/interfaces/AssetPost.html +12 -0
  134. package/docs/interfaces/AssetPrincipalPermissionsGet.html +23 -0
  135. package/docs/interfaces/AssetPut.html +12 -0
  136. package/docs/interfaces/AssetTypeDelete.html +2 -0
  137. package/docs/interfaces/AssetTypeGet.html +16 -0
  138. package/docs/interfaces/AssetTypePut.html +9 -0
  139. package/docs/interfaces/AttachmentGet.html +20 -0
  140. package/docs/interfaces/AttachmentPost.html +4 -0
  141. package/docs/interfaces/BatchItemError.html +4 -0
  142. package/docs/interfaces/BatchItemResult.html +6 -0
  143. package/docs/interfaces/BatchSummary.html +4 -0
  144. package/docs/interfaces/Coordinates.html +6 -0
  145. package/docs/interfaces/Diagnostics.html +4 -0
  146. package/docs/interfaces/DiagnosticsSummary.html +4 -0
  147. package/docs/interfaces/ElementQuantities.html +3 -0
  148. package/docs/interfaces/EntityCollection.html +15 -0
  149. package/docs/interfaces/EntityRepresentation.html +12 -0
  150. package/docs/interfaces/Facility.html +22 -0
  151. package/docs/interfaces/FacilityPart.html +18 -0
  152. package/docs/interfaces/FilePatch.html +7 -0
  153. package/docs/interfaces/FilePatchSet.html +5 -0
  154. package/docs/interfaces/GenericDiagnosticEntry.html +4 -0
  155. package/docs/interfaces/ITwinClient.html +404 -0
  156. package/docs/interfaces/IfcDiagnosticEntry.html +5 -0
  157. package/docs/interfaces/IfcDiagnosticNode.html +6 -0
  158. package/docs/interfaces/IfcDiagnosticNodeData.html +6 -0
  159. package/docs/interfaces/InheritedPermissions.html +8 -0
  160. package/docs/interfaces/ListTwinItem.html +30 -0
  161. package/docs/interfaces/Location.html +13 -0
  162. package/docs/interfaces/MyPermissions.html +7 -0
  163. package/docs/interfaces/MyTwinPermissions.html +5 -0
  164. package/docs/interfaces/PaginatedResponse.html +3 -0
  165. package/docs/interfaces/Pagination.html +4 -0
  166. package/docs/interfaces/Property.html +4 -0
  167. package/docs/interfaces/PropertySet.html +6 -0
  168. package/docs/interfaces/SimpleQuantity.html +6 -0
  169. package/docs/interfaces/SinglePropertyValue.html +5 -0
  170. package/docs/interfaces/Site.html +27 -0
  171. package/docs/interfaces/Space.html +16 -0
  172. package/docs/interfaces/TwinComputedPropertySetsResponse.html +4 -0
  173. package/docs/interfaces/TwinContentResponse.html +6 -0
  174. package/docs/interfaces/TwinDelete.html +2 -0
  175. package/docs/interfaces/TwinDiagnosticEntry.html +5 -0
  176. package/docs/interfaces/TwinDiagnosticNode.html +4 -0
  177. package/docs/interfaces/TwinDiagnosticNodeData.html +9 -0
  178. package/docs/interfaces/TwinDiagnosticsResponse.html +5 -0
  179. package/docs/interfaces/TwinErrorBody.html +4 -0
  180. package/docs/interfaces/TwinFileDelete.html +2 -0
  181. package/docs/interfaces/TwinFileGet.html +15 -0
  182. package/docs/interfaces/TwinFileListOptions.html +6 -0
  183. package/docs/interfaces/TwinFilePut.html +8 -0
  184. package/docs/interfaces/TwinFileReference.html +4 -0
  185. package/docs/interfaces/TwinFileRestore.html +2 -0
  186. package/docs/interfaces/TwinFileSource.html +4 -0
  187. package/docs/interfaces/TwinFileTransform.html +11 -0
  188. package/docs/interfaces/TwinFileVersionListOptions.html +4 -0
  189. package/docs/interfaces/TwinGet.html +48 -0
  190. package/docs/interfaces/TwinGroupGet.html +10 -0
  191. package/docs/interfaces/TwinGroupsGet.html +3 -0
  192. package/docs/interfaces/TwinPermissionsConflictErrorBody.html +8 -0
  193. package/docs/interfaces/TwinPermissionsPut.html +11 -0
  194. package/docs/interfaces/TwinPrincipalPermissionsGet.html +34 -0
  195. package/docs/interfaces/TwinProcessorEvent.html +7 -0
  196. package/docs/interfaces/TwinProcessorResponse.html +5 -0
  197. package/docs/interfaces/TwinPut.html +21 -0
  198. package/docs/interfaces/TwinReference.html +4 -0
  199. package/docs/interfaces/TwinSpecification.html +18 -0
  200. package/docs/interfaces/TwinVersionContentResponse.html +7 -0
  201. package/docs/interfaces/UploadStatusGet.html +3 -0
  202. package/docs/media/twin-management.md +399 -0
  203. package/docs/modules.html +1 -0
  204. package/docs/types/AssetBatchGetStatus.html +1 -0
  205. package/docs/types/AssetBatchOperation.html +1 -0
  206. package/docs/types/AssetBatchOperationStatus.html +1 -0
  207. package/docs/types/DiagnosticCategory.html +2 -0
  208. package/docs/types/DiagnosticLevel.html +2 -0
  209. package/docs/types/EpsgType.html +1 -0
  210. package/docs/types/FacilityClass.html +3 -0
  211. package/docs/types/FacilityPartClass.html +3 -0
  212. package/docs/types/IfcDiagnosticCode.html +1 -0
  213. package/docs/types/Op.html +2 -0
  214. package/docs/types/SinglePropertyValueType.html +3 -0
  215. package/docs/types/SingleQuantityValueType.html +3 -0
  216. package/docs/types/SiteType.html +3 -0
  217. package/docs/types/SpaceType.html +2 -0
  218. package/docs/types/TwinBoundCapabilityName.html +8 -0
  219. package/docs/types/TwinCapability.html +12 -0
  220. package/docs/types/TwinCapabilityName.html +8 -0
  221. package/docs/types/TwinDiagnosticCode.html +1 -0
  222. package/docs/types/TwinErrorCause.html +8 -0
  223. package/docs/types/TwinErrorItemKind.html +4 -0
  224. package/docs/types/TwinPermissionsConflictCause.html +6 -0
  225. package/docs/types/TwinScopedCapability.html +9 -0
  226. package/docs/types/TwinScopedCapabilityName.html +8 -0
  227. package/docs/types/UploadStatus.html +1 -0
  228. package/package.json +52 -0
  229. package/src/BuildInfo.ts +41 -0
  230. package/src/TwinClient.ts +3113 -0
  231. package/src/errors.ts +567 -0
  232. package/src/index.ts +3 -0
  233. package/src/tsconfig.json +13 -0
  234. package/src/types/Twin.ts +7 -0
  235. package/src/types/TwinGeometryInfo.ts +7 -0
  236. package/src/types/asset-api.ts +69 -0
  237. package/src/types/asset-attachment-api.ts +56 -0
  238. package/src/types/asset-batch-api.ts +90 -0
  239. package/src/types/asset-types-api.ts +86 -0
  240. package/src/types/common.ts +10 -0
  241. package/src/types/generated.ts +655 -0
  242. package/src/types/index.ts +9 -0
  243. package/src/types/twin-api.ts +153 -0
  244. package/src/types/twin-file-api.ts +67 -0
  245. package/src/types/twin-permissions-api.ts +257 -0
@@ -0,0 +1,399 @@
1
+ # Twin Management
2
+
3
+ ## Overview
4
+
5
+ Twins are the central entity in the Twin service. A twin represents a digital twin specification with versioned state, soft-delete support, and optimistic concurrency via etags. Twins reference containers and files, and their processed content (T8CM, T8GI formats) is available for download.
6
+
7
+ ## Storage and Versioning
8
+
9
+ Twins use a two-table versioning pattern:
10
+
11
+ - `twin.twins` — The `version` column points to the current version. The `etag` column provides optimistic concurrency control, regenerated on every write.
12
+ - `twin.twin_versions` — Immutable version history.
13
+
14
+ Every update creates a new row in `twin_versions` and bumps the version pointer in `twins`. Soft deletion sets `is_deleted = true` in a new version row rather than removing data.
15
+
16
+ The twin's `name`, its WGS84 `location`, and the set of entity `classes` it contains are persisted on the version row, so the list endpoint and the version-history endpoint can surface them without reading the specification blob. `name` and `location` are **derived from the twin's root entity** (see [Root Entity](#root-entity)) at write time; `classes` is derived from the specification's spatial hierarchy. Existing twins were backfilled across all versions (current and historical), so every version reports its own `name`, `location`, and `classes`.
17
+
18
+ ## REST API
19
+
20
+ Twins are managed through `TwinsController` at route `twins`.
21
+
22
+ | Operation | Method | Route | Permission |
23
+ | ------------- | ------ | ------------------------------------ | ---------- |
24
+ | List | GET | `/twins` | (filtered) |
25
+ | Get | GET | `/twins/{twinId}` | View |
26
+ | List versions | GET | `/twins/{twinId}/versions` | View |
27
+ | Get version | GET | `/twins/{twinId}/versions/{version}` | View |
28
+ | Create/Update | PUT | `/twins/{twinId}` | Edit |
29
+ | Delete | DELETE | `/twins/{twinId}` | Delete |
30
+ | Restore | PUT | `/twins/{twinId}/restore` | Delete |
31
+
32
+ Content and diagnostics are handled by `TwinContentController`:
33
+
34
+ | Operation | Method | Route | Permission |
35
+ | ------------------------ | ------ | -------------------------------------------------------------------- | ---------- |
36
+ | Get content (latest) | GET | `/twins/{twinId}/contents` | View |
37
+ | Get version content | GET | `/twins/{twinId}/versions/{version}/contents` | View |
38
+ | Download content blob | GET | `/twins/{twinId}/versions/{version}/contents/{contentId}/content` | View |
39
+ | Get diagnostics (latest) | GET | `/twins/{twinId}/diagnostics` | View |
40
+ | Get version diagnostics | GET | `/twins/{twinId}/versions/{version}/diagnostics` | View |
41
+ | Get version events | GET | `/twins/{twinId}/versions/{version}/events` | View |
42
+ | Get processor events | GET | `/twins/{twinId}/versions/{version}/processors/{processorId}/events` | View |
43
+
44
+ ### Request/Response Types
45
+
46
+ Request DTO (`TwinPut`): `rootEntity` (required; see [Root Entity](#root-entity)), `entities` (sites, facilities, spaces, and `twinReferences`), `propertySets`, `elementQuantities`, `relatedContainerIds`, `fileReferences` (array of `{fileId, fileVersion, twinNodeId?}`), `etag` (required for updates). The legacy top-level `site` field is deprecated; `rootEntity` is authoritative. `name` and `location` are **not** accepted on input — they are derived from the root entity (a client that still sends them has them ignored, not rejected).
47
+
48
+ Response DTO (`TwinGet`): extends `TwinSpecification` with `id`, `etag`, `relatedContainerIds`, `version`, `isDeleted`, `createdBy`, `createdUtc`, `modifiedBy`, `modifiedUtc`, `fileReferences`, `computedPropertySets`, the two computed nested-twin fields `aggregatedTwins` and `parentTwin` (see [Nested Twins](#nested-twins-twin-references)), and the derived `name` and full `location` (the latter omitted when the root entity declares no location). `name`/`location` are read-only projections of the root entity, not part of the stored specification body.
49
+
50
+ List item (`ListTwinItem`): subset with `id`, `name`, `etag`, `propertySets`, `computedPropertySets`, `elementQuantities`, `relatedContainerIds`, `version`, `isDeleted`, `createdBy`, `createdUtc`, `modifiedBy`, `modifiedUtc`, `aggregatedTwins`, `parentTwin`, `location`, `classes`.
51
+
52
+ The list `location` is a **coordinates-only** view: `{ epsg, latitude, longitude }`, where `epsg` is always `"4326"`. It is present only when the twin's specification declares a location, and is omitted entirely otherwise. `elevation` and `trueNorth` are intentionally left off the list to keep the payload small; the full single-twin GET (`TwinGet`) continues to return the complete `location` (including `elevation` and `trueNorth`) from the specification.
53
+
54
+ The list `classes` is the deduplicated, ordinally-sorted set of IFC entity classes the twin's specification declares across its spatial hierarchy (sites, facilities, facility parts, and spaces) — e.g. `["IfcBuilding", "IfcBuildingStorey", "IfcSite", "IfcSpace"]`. It lets applications see *what a twin contains* without reading the specification blob. It is **always present** as an array and is `[]` when the twin declares no such entities. The set is derived from the specification at write time; the full single-twin GET (`TwinGet`) does not repeat it, since that response already returns every entity with its `class` inline.
55
+
56
+ Delete/Restore request: `{ etag: string }`.
57
+
58
+ ### Query Parameters
59
+
60
+ **List twins:** `relatedTo` (container ID filter), `includeDeleted` (bool, default: false), `page` (pagination token), `limit` (default: 100, max: 1000).
61
+
62
+ **List versions:** `includeDeleted` (bool), `page`, `limit` (default: 100, max: 1000).
63
+
64
+ **Events:** `offset`, `limit` (default: 100, max: 500).
65
+
66
+ ## Optimistic Concurrency
67
+
68
+ All mutating operations use the `etag` field for conflict detection:
69
+
70
+ - **Create**: `etag` is omitted or null. If the twin already exists, the server treats it as an update and requires a matching etag.
71
+ - **Update**: `etag` must match the server's current etag. On mismatch, the server returns **409 Conflict** with the current twin state in the response body.
72
+ - **Delete/Restore**: `etag` is sent in the request body. Same 409 behavior on mismatch.
73
+
74
+ The 409 response containing the current state allows clients to implement merge-and-retry logic without an extra round-trip.
75
+
76
+ ## Soft Delete and Restore
77
+
78
+ - `DELETE /twins/{twinId}` marks the twin as deleted (new version with `isDeleted = true`).
79
+ - `PUT /twins/{twinId}/restore` restores a soft-deleted twin.
80
+ - Both operations require the `Delete` permission and the current etag.
81
+ - Deleted twins return redacted responses: list results (`TwinEntry`) redact the name to `"[Redacted]"`, omit properties and `location`, and empty `classes` to `[]` (it is always present, so it cannot be omitted), while GET-by-id (`TwinGet`) keeps the real name but strips the rest of the specification and relationships — emptying `entities` to `{}`, emptying `aggregatedTwins`, `aggregatedSpaces`, `elementQuantities`, `propertySets` and `fileReferences` to `[]`, setting `location` and `site` to `null`, and omitting `parentTwin`.
82
+ - List operations exclude deleted twins by default; use `includeDeleted=true` to include them.
83
+
84
+ ## Root Entity
85
+
86
+ Every twin declares a `rootEntity` (required) naming the entity that roots its model tree. The service enforces (`RootEntityValidator`):
87
+
88
+ - `rootEntity` must be a **site or facility** present in `entities`; facility parts and spaces are not allowed as roots. A value that is not a key in `entities.sites` or `entities.facilities` — including a `twinReferences` key — is rejected, so a nested twin reference can never be a root.
89
+ - A **facility-rooted** twin must not contain a site (neither in `entities.sites` nor the legacy `site` field) and cannot aggregate twins. The converter aggregates only the root under the project, so a site or reference on a facility root would be orphaned from the model tree.
90
+ - The root entity must have a **non-empty `name`**, for **both** site and facility roots. `Site.name` is required by the schema, but `Facility.name` is optional, so a nameless facility root is rejected — the root's name becomes the twin's name and a twin always has a name.
91
+
92
+ The legacy top-level `site` field is deprecated; `rootEntity` is authoritative. A failing root is rejected with cause `InvalidRootEntity`.
93
+
94
+ > **Back-compat note.** Requiring a name on the root is a behavioural tightening: before name/location moved onto the root, a client could author a top-level `name` and leave the root entity nameless. Such a write is now rejected — clients must put the name on the root entity (the site or facility named by `rootEntity`). This is intentional (the no-legacy-compat stance); there is no transition fallback that accepts a top-level name.
95
+
96
+ ### Name and location are derived from the root entity
97
+
98
+ A twin's `name` and `location` are **owned by its root entity**, not authored at the top level of the specification. `Site` and `Facility` each carry an optional `location` (WGS84); the root entity's `name` and `location` are extracted at write time, persisted on the version row, and surfaced on `TwinGet` (full `location`) and the list (`name` + coordinates-only `location`). They are read-only on input: a `TwinPut` cannot set them, mirroring the computed nested-twin fields (`aggregatedTwins`/`parentTwin`). Non-root sites/facilities may also carry a `location`, but only the root's is the twin's location.
99
+
100
+ The T8 client-model converter likewise reads the project name from the root entity. If a twin's `rootEntity` is unresolvable (names no site/facility in `entities` — an edge case the validator blocks on write, reachable only for pre-existing data), the converter produces a project with **no name** rather than falling back to a top-level value (there is none after migration). This graceful degradation only affects already-malformed twins.
101
+
102
+ ## Nested Twins (Twin References)
103
+
104
+ A twin can nest other twins as children, forming a parent/child tree. This is the **twin reference** feature (TWINS-3703). The behavioural rationale — especially how references that involve deleted twins are handled — is recorded in the ADR [Twin References Are Spec-Declared Edges With Computed Parentage](../../../../decisions/twin-reference-lifecycle.md); this section summarises the schema surface and the rules the service enforces.
105
+
106
+ ### Schema surface
107
+
108
+ A reference has two halves, both authored in the parent's specification blob (so both are versioned):
109
+
110
+ - **Declaration** — `entities.twinReferences` is a `{ refId → { twin: <twinId> } }` map. A `TwinReference` is intentionally minimal: it carries only the referenced twin's id (no transform, no name).
111
+ - **Placement** — a `Site`'s `aggregatedTwinReferences` lists the `refId`s nested directly under it. Placement is what gives a reference a position in the spatial tree.
112
+
113
+ A child twin must hang on a site: **every declared reference must be placed under exactly one site**. A reference declared in `twinReferences` but placed by no site (a dangling reference) is rejected, as is placing a `refId` that is not declared, or placing the same `refId` under more than one site. The holding site must also be **reachable from `rootEntity`** through the site aggregation chain (`site → aggregatedSites`). Only a site aggregates sites — facilities, facility parts and spaces never aggregate a site — so a site is reachable only along that chain; a reference under a site disconnected from the root is rejected, since the converter would orphan it from the model tree. Because a reference is placed under a `Site`, a **facility-rooted twin cannot aggregate twins** (it has no site). These cross-field rules are enforced in the service, since JSON Schema (draft-06) cannot express them.
114
+
115
+ ### Computed fields
116
+
117
+ From those declarations the service maintains two **computed** read-model fields, returned on `TwinGet` and `ListTwinItem`:
118
+
119
+ - **`aggregatedTwins`** (`string[]`) — the ids of the twins this twin parents (the flat projection of its `twinReferences`).
120
+ - **`parentTwin`** (`string?`) — the id of the twin that references this twin, or omitted for a root twin.
121
+
122
+ These are *projections of the actual edges, not authored input*. A `TwinPut` that sends them has them ignored. The authoritative relational edge is `twin.twins.parent_twin_id`, maintained transactionally by `upsert_twin`; `parentTwin` reads it directly.
123
+
124
+ ### Spec-versions-vs-computed-doesn't
125
+
126
+ This is the load-bearing rule:
127
+
128
+ - Changing **specification** data (name, location, `entities` including `twinReferences`/placement) creates a new version, a new etag, and emits a `TwinUpdated` event.
129
+ - Changing **computed** data (`parentTwin`, `aggregatedTwins`) is maintained in place — **no new version, no etag change, no event**.
130
+
131
+ Because parentage is computed, re-parenting a child is *not an edit of the child* and never bumps the child's version. A single PUT that adds N references versions exactly one twin (the parent), not N+1.
132
+
133
+ ### Enforced invariants
134
+
135
+ `upsert_twin` validates the references that are *newly added* in a write (references carried over from the previous version are left untouched) and enforces, transactionally:
136
+
137
+ | Rule | SQL violation | HTTP `cause` |
138
+ | ---- | --------- | --- |
139
+ | A twin must not reference itself | `self_reference` | `SelfTwinReference` |
140
+ | A referenced twin must exist | `referenced_row_not_found` | `ReferencedItemNotFound` |
141
+ | A referenced twin must not be deleted (a **new** reference to a deleted twin is rejected) | `referenced_row_deleted` | `ReferencedItemDeleted` |
142
+ | A twin has at most one parent — unless its current parent is itself deleted, in which case it may be re-parented | `twin_already_referenced` | `TwinAlreadyReferenced` |
143
+ | References must not form a cycle | `cycle` | `CyclicTwinReference` |
144
+
145
+ Further rules are enforced in C# *before* the transaction (returned as a 400 with the named `cause`), because the SQL function only sees a de-duplicated id array: a twin may be **referenced at most once** within a single specification (`DuplicateTwinReference`), and each `refId` must be **placed under exactly one site reachable from `rootEntity`** (`InvalidTwinReferencePlacement`). A `twinReferences` entry given as JSON `null` names no twin to place and is refused with the same `InvalidTwinReferencePlacement` cause, unless an earlier rule already refuses the write (`InvalidRootEntity` for a facility-rooted twin, `DuplicateTwinReference` for a duplicate among the other entries).
146
+
147
+ ### Permission to reference
148
+
149
+ Editing a twin requires `Edit` on that twin. Adding a reference additionally requires **`View` on the referenced twin**: a reference places the child in this twin's tree and repoints the child's computed parent, so the caller must be able to read it. The check applies only to references *newly added* by a write (those carried over from the current version are not re-checked, mirroring `upsert_twin`'s claim loop — re-saving a twin that still references a child it can no longer read is allowed). A newly referenced twin that is missing **or** not visible to the caller is reported identically, as `ReferencedItemNotFound`, so the response never reveals the existence of a twin the caller cannot read (consistent with the service's "no View → not found, not forbidden" convention).
150
+
151
+ ### Deleted twins
152
+
153
+ A reference to a twin that is deleted *after* it was first referenced is **kept** (the parent may keep and re-save it), but a **new** reference to an already-deleted twin is rejected. Consequently a live twin's `aggregatedTwins`, or a live child's `parentTwin`, may name a deleted (and therefore redacted) twin — consumers walking the tree must expect that.
154
+
155
+ A child whose current parent is *deleted* may be claimed by another twin (the "steal"). At steal time only the relational edge moves; the deleted parent's specification still declares the lost child, but this is invisible because deleted twins are redacted on read. The declaration is reconciled against the actual edges on **restore**. See the ADR for the full lifecycle.
156
+
157
+ ## Entity Identity
158
+
159
+ Every spatial entity — each entry of `entities.sites`, `entities.facilities`, `entities.facilityParts`
160
+ and `entities.spaces` — is identified by its `id`. `entities.twinReferences` is not covered: a nested
161
+ twin is identified by the twin it points at.
162
+
163
+ ### Identity rules enforced on write
164
+
165
+ `EntityIdentityValidator` runs on every `PUT /twins/{twinId}`, after the root-entity and twin-reference
166
+ checks and before the representation checks. A specification that breaks any of the four rules below
167
+ is rejected with `400` and cause `InvalidEntityId`:
168
+
169
+ | Rule | Also in the JSON schema |
170
+ | --- | --- |
171
+ | The entity has an `id` (a JSON `null` entity counts as having none) | Yes — all four entity types are objects that list `id` as required and declare it as a string, so an omitted or JSON `null` `id`, and a JSON `null` entity, never conform |
172
+ | The `id` equals the entity's map key, compared exactly | No |
173
+ | No `id` is used by more than one entity, across all four collections | No |
174
+ | The `id` is a canonical IFC GlobalId, `^[0-3][0-9A-Za-z_$]{21}$` | Yes — the `id` of all four entity types carries this pattern |
175
+
176
+ The rules are checked per entity in that order and the write fails on the first violation, so the
177
+ response `detail` names one entity (its collection and its key or id) even when several are wrong.
178
+
179
+ A missing id can answer with a different cause, because the request is deserialized before this gate
180
+ runs and a body the request model refuses never reaches it. The request model requires `id` on all
181
+ four entity types, so an `id` that is omitted or sent as JSON `null` is refused there, and that
182
+ refusal returns `400` with cause `ValidationFailed`, not `InvalidEntityId`.
183
+
184
+ The gate's missing-id rule catches what deserialization lets through — a JSON `null` entity or an
185
+ empty-string `id` — and those return `InvalidEntityId`. A client handling a missing id should
186
+ therefore expect either cause.
187
+
188
+ When a write carries ids that are not canonical GlobalIds, the service additionally logs a warning
189
+ with their count and the first ten, each truncated to 200 characters.
190
+
191
+ ### Previously accepted ids are now rejected
192
+
193
+ The GlobalId rule is a **breaking change to stored, caller-owned data**. Ids that earlier versions of
194
+ the service accepted — a readable id such as `site-001` or `Sovrum 1`, or a dashed uuid — now return
195
+ `400 InvalidEntityId`.
196
+
197
+ The gate runs only on `PUT /twins/{twinId}`, and it validates the whole specification being written,
198
+ not just what changed. A stored twin carrying such an id is therefore still returned by the read
199
+ endpoints, and can still be deleted and restored, but **every specification write to it fails until
200
+ all of its entity ids conform**.
201
+
202
+ Remediation is to replace each non-conforming id with a canonical IFC GlobalId, changing the map key
203
+ and the `id` together and updating everything in the specification that names the entity. This is
204
+ not a cosmetic rename. The `id` becomes the entity's GlobalId in the client model, so replacing it
205
+ changes the entity's identity: anything that recorded the old id, such as a consumer keyed on the
206
+ client model's GlobalId, no longer finds the entity under it. (No valid
207
+ `twin~twin:<twinId>.entity:<id>` reference can be lost this way, because a reference's `entity:`
208
+ component must itself be a GlobalId and so could never spell a non-conforming id.) Whatever produces
209
+ the specification is responsible for emitting conforming ids. See
210
+ [Object identity inside a twin](../../../entity-references.md#object-identity-inside-a-twin-is-the-client-model-gid)
211
+ for why the id must be a GlobalId.
212
+
213
+ ### A site must declare its class
214
+
215
+ `Site` lists `class` as required in the JSON schema, as `Facility`, `FacilityPart` and `Space` do, so
216
+ every spatial entity in a specification carries one. A `PUT /twins/{twinId}` whose body has a site
217
+ with `class` omitted or sent as JSON `null` is refused when the request is deserialized, before any
218
+ of the validators above run, and answers `400` with cause `ValidationFailed` — the same refusal a
219
+ missing `id` gets. A `class` that is not one of the schema's values for that entity type, on any of
220
+ the four entity types, is refused the same way, and `item.field` names the member, for example
221
+ `entities.sites.<id>.class`. So is any other value the request model cannot read against a length,
222
+ range or enum constraint of the schema — a representation relation's `relatingEntity` or
223
+ `representedBy` longer than 512 characters, for example, names
224
+ `entities.representations.<relationId>.relatingEntity`.
225
+
226
+ Unlike the GlobalId rule, this one is part of the model the service reads stored specifications
227
+ with, so it is not limited to writes. Twin Manager has always written a site's `class`, and stored
228
+ specifications were not checked for one before the rule was added. Should a stored specification
229
+ hold a site without a class:
230
+
231
+ - A live twin is still returned by `GET /twins/{twinId}`, which streams the stored specification
232
+ without deserializing it, and it is repaired by writing the specification back with the class set
233
+ and the current `etag`.
234
+ - A delete marks the twin deleted but answers `500`, because the response is built from the
235
+ deserialized specification. Once deleted, `GET /twins/{twinId}` and
236
+ `PUT /twins/{twinId}/restore` answer `500` as well. Repair such a twin before deleting it.
237
+ - A request with a stale `etag` answers `500` instead of `409` and changes nothing, because the
238
+ conflict body is built from the stored specification. This applies to `PUT /twins/{twinId}`
239
+ (the repair write included), `DELETE /twins/{twinId}` and `PUT /twins/{twinId}/restore` on a
240
+ live twin.
241
+
242
+ ## Representation Relations
243
+
244
+ A twin's spatial entities — sites, facilities, facility parts (floors) and spaces — are declared in
245
+ `entities`. They say *what the twin contains*, not *what draws it*. `entities.representations`
246
+ supplies the second half: an objectified relation declaring that a spatial entity of this twin is
247
+ drawn by a product inside one of its model files.
248
+
249
+ It is modelled as a relation rather than an inline attribute so it stays addressable and extensible,
250
+ mirroring how `twinReferences` objectifies twin-to-twin relations.
251
+
252
+ ```jsonc
253
+ "entities": {
254
+ "facilityParts": {
255
+ "1jK8pQrStUvWxYz0AbCdEf": { "class": "IfcBuildingStorey", "id": "1jK8pQrStUvWxYz0AbCdEf", "name": "Floor 01" }
256
+ },
257
+ "representations": {
258
+ "rep-floor-01": {
259
+ "relatingEntity": "twin~twin:550e8400-e29b-41d4-a716-446655440000.entity:1jK8pQrStUvWxYz0AbCdEf",
260
+ "representedBy": "twin~twin:550e8400-e29b-41d4-a716-446655440000.file:7c9e6679-7425-40de-944b-e07fc1f90ae7/entities/3Vu$pXAkz1Ee8OKmR3XBZZ"
261
+ }
262
+ }
263
+ }
264
+ ```
265
+
266
+ Both sides are [entity references](../../../entity-references.md). The map key (`rep-floor-01`) is
267
+ the relation's own id and carries no meaning beyond identifying the relation.
268
+
269
+ TwinBuilder reads the representation relations when it merges the twin. Each valid relation declares
270
+ a geometry reference, and the spatial entity is drawn with the entity representations (T8) of every
271
+ product its relations resolve to. A spatial entity named by a valid representation relation takes no
272
+ geometry reference from its `twinfinity` property set. `representedBy` must spell a canonical IFC
273
+ GlobalId, which most DWG products do not have, so the `twinfinity` property set remains the way to
274
+ draw a spatial entity from one; see
275
+ [Representation Relation Format](../../../formats/t8cm-geometry-references.md#representation-relation-format).
276
+ The merge rules and the diagnostics are specified in
277
+ [T8 Client Model — Geometry References](../../../formats/t8cm-geometry-references.md); this section
278
+ states what a write must satisfy.
279
+
280
+ ### `relatingEntity` — the entity being represented
281
+
282
+ ```
283
+ twin~twin:<thisTwinId>.entity:<entityId>
284
+ ```
285
+
286
+ It must name an entity of **this** twin, and `<entityId>` must equal the `id` of an entity in
287
+ `entities.sites`, `entities.facilities`, `entities.facilityParts` or `entities.spaces` (an entity's
288
+ `id` and its map key are themselves required to be equal). A `twinReferences` key denotes a nested
289
+ twin rather than a spatial entity and is rejected.
290
+
291
+ The entity is a **chain component**, not a path segment. That is what makes a floor addressable in
292
+ its own right — containment queries reach it, and it can carry events — which a path segment could
293
+ never do.
294
+
295
+ ### `representedBy` — the product that draws it
296
+
297
+ ```
298
+ twin~twin:<thisTwinId>.file:<fileId>/entities/<ifcGid>
299
+ ```
300
+
301
+ The product stays **path-addressed** because a GlobalId is only unique within a merged twin model,
302
+ not within a source file: the same gid can appear in several files, so resolving one returns a list.
303
+ The file itself is a chain component under its twin.
304
+
305
+ The validator checks the shape and that the file belongs to the twin being written: the twin
306
+ component must be this twin's id, and `<fileId>` must be one of the `fileReferences` stated in the
307
+ same write. An entity reference to another twin's file is rejected. Whether the file holds a product
308
+ with that GlobalId is not checked on write. TwinBuilder resolves the relation during the merge and
309
+ reports one that resolves nowhere as the `GeometryReferenceUnresolved` twin diagnostic; see
310
+ [Diagnostics](../../../formats/t8cm-geometry-references.md#diagnostics).
311
+
312
+ The write that creates a twin therefore cannot carry a representation relation. A file is uploaded
313
+ to a twin that already exists, so on that write no `fileReferences` entry is accepted: one naming a
314
+ file that does not exist answers `400` with cause `ReferencedItemNotFound`, and one naming another
315
+ twin's file answers `400` with cause `FileNotOwnedByTwin`. A relation whose file is left out of
316
+ `fileReferences` is refused first, with `InvalidRepresentation`. Create the twin, upload the file,
317
+ then write the relations with the file in `fileReferences`. An absent or empty `representations` is
318
+ accepted on every write, the creating one included.
319
+
320
+ ### What is validated on write
321
+
322
+ `RepresentationValidator` enforces the cross-field rules JSON Schema cannot express, returning `400`
323
+ with cause `InvalidRepresentation`:
324
+
325
+ | Rule | Applies to |
326
+ | --- | --- |
327
+ | Well-formed entity reference | both |
328
+ | Names this twin, as `twin~twin:<thisTwinId>` | `relatingEntity` |
329
+ | Addresses the entity directly, with no path | `relatingEntity` |
330
+ | Entity id is a site, facility, facility part or space in this specification | `relatingEntity` |
331
+ | Shaped as `twin~twin:<thisTwinId>.file:<fileId>/entities/<ifcGid>` | `representedBy` |
332
+ | Names this twin, as `twin~twin:<thisTwinId>` | `representedBy` |
333
+ | File id is one of this write's `fileReferences` | `representedBy` |
334
+ | Does not contain the character U+0000 | the relation's id (its key in `representations`) |
335
+
336
+ The relation id is otherwise free text. U+0000 is refused because TwinBuilder names a relation that
337
+ does not resolve by its id in the `GeometryReferenceUnresolved` diagnostic, and the Twin service
338
+ stores that diagnostic as Postgres `jsonb`, which cannot hold U+0000: the build result would be
339
+ refused on every attempt and the twin version would never finish building.
340
+
341
+ ### Referencing a floor from outside the specification
342
+
343
+ The same `relatingEntity` spelling is how anything else in the platform names that floor — an issue
344
+ filed against it, an event about it, a client resolving it against a loaded model:
345
+
346
+ ```
347
+ twin~twin:<twinId>.entity:<floorGid>
348
+ ```
349
+
350
+ `<floorGid>` is the entity's `id` as it appears in the merged client model. The specification's `id`
351
+ passes straight through to the T8 `GlobalId` attribute, so the id you author is the id the platform
352
+ addresses. Two consequences follow, and both are enforced on write (see
353
+ [Entity Identity](#entity-identity)): the entity's map key must equal its `id`, and the id must be a
354
+ canonical IFC GlobalId. See
355
+ [`docs/specs/entity-references.md`](../../../entity-references.md) for the identity rules and the
356
+ stability caveats — a gid is carried forward only while its product stays continuously present
357
+ across merges, so consumers must treat a dangling entity reference as normal.
358
+
359
+ ## Content and Processing
360
+
361
+ After a twin is updated, processors generate content in various formats:
362
+
363
+ - **t8cm** — T8 Client Model (BIM product hierarchy, properties, classifications)
364
+ - **t8cm-ext** — T8 Client Model extensions
365
+ - **t8gi** — T8 Geometry Info (mesh descriptors, styles, regions)
366
+ - **t8gd** — T8 Geometry Data
367
+
368
+ The content endpoint returns metadata about available content items and their associated processors. Content blobs are downloaded separately via the content blob endpoint, which supports HTTP range requests.
369
+
370
+ ### Version Content Response
371
+
372
+ ```typescript
373
+ interface TwinVersionContentResponse {
374
+ twinId: string;
375
+ twinVersion: number;
376
+ status: string;
377
+ processors: TwinProcessorResponse[];
378
+ content: TwinContentResponse[];
379
+ computedPropertySets: TwinComputedPropertySetsResponse[];
380
+ }
381
+ ```
382
+
383
+ ### Diagnostics
384
+
385
+ Each twin version may have diagnostics from processing, returned as a summary (fatal/error/warning counts) and detailed diagnostics data.
386
+
387
+ ### Processor Events
388
+
389
+ Processor events provide a timeline of processing activity for a twin version. Events can be queried for all processors or filtered to a specific processor.
390
+
391
+ ## Container Relationship
392
+
393
+ - `relatedContainerIds` links a twin to one or more containers (projects/folders).
394
+ - Container IDs are **immutable after creation** — they cannot be changed on subsequent updates.
395
+ - When creating a twin linked to multiple containers, the service computes intersected ACLs across all container scopes.
396
+
397
+ ## Client SDK
398
+
399
+ The `@twinfinity/twin` package (`packages/twinfinity-twin/`) provides a typed TypeScript client. See its [README](../../../../../packages/twinfinity-twin/README.md) for usage examples. For the conventions all TypeScript clients follow, see [TypeScript Client Conventions](../../../packages/client-conventions/typescript-client-conventions.md).