@things-factory/headless-twin 10.0.9 → 10.0.11

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 (354) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +2 -2
  2. package/dist-server/engine/canonical-ingest.js +24 -2
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/command-routing.js +1 -1
  5. package/dist-server/engine/command-routing.js.map +1 -1
  6. package/dist-server/engine/energy-topology.d.ts +86 -0
  7. package/dist-server/engine/energy-topology.js +144 -0
  8. package/dist-server/engine/energy-topology.js.map +1 -0
  9. package/dist-server/engine/index.d.ts +8 -3
  10. package/dist-server/engine/index.js +10 -3
  11. package/dist-server/engine/index.js.map +1 -1
  12. package/dist-server/engine/kpi-baseline.d.ts +1 -1
  13. package/dist-server/engine/kpi-baseline.js.map +1 -1
  14. package/dist-server/engine/kpi-fold.d.ts +137 -1
  15. package/dist-server/engine/kpi-fold.js +225 -0
  16. package/dist-server/engine/kpi-fold.js.map +1 -1
  17. package/dist-server/engine/kpi-query.d.ts +1 -1
  18. package/dist-server/engine/kpi-query.js +212 -18
  19. package/dist-server/engine/kpi-query.js.map +1 -1
  20. package/dist-server/engine/kpi-target.d.ts +17 -0
  21. package/dist-server/engine/kpi-target.js +24 -2
  22. package/dist-server/engine/kpi-target.js.map +1 -1
  23. package/dist-server/engine/live-feed-registry.d.ts +25 -0
  24. package/dist-server/engine/live-feed-registry.js +51 -0
  25. package/dist-server/engine/live-feed-registry.js.map +1 -0
  26. package/dist-server/engine/load-meter.d.ts +181 -0
  27. package/dist-server/engine/load-meter.js +267 -0
  28. package/dist-server/engine/load-meter.js.map +1 -0
  29. package/dist-server/engine/local-declarations.d.ts +262 -0
  30. package/dist-server/engine/local-declarations.js +528 -0
  31. package/dist-server/engine/local-declarations.js.map +1 -0
  32. package/dist-server/engine/model-basis.d.ts +39 -6
  33. package/dist-server/engine/model-basis.js +69 -9
  34. package/dist-server/engine/model-basis.js.map +1 -1
  35. package/dist-server/engine/{board-vocabulary.d.ts → model-vocabulary.d.ts} +4 -4
  36. package/dist-server/engine/{board-vocabulary.js → model-vocabulary.js} +37 -19
  37. package/dist-server/engine/model-vocabulary.js.map +1 -0
  38. package/dist-server/engine/oee-accumulator.d.ts +28 -0
  39. package/dist-server/engine/oee-accumulator.js +26 -1
  40. package/dist-server/engine/oee-accumulator.js.map +1 -1
  41. package/dist-server/engine/operation-basis.d.ts +32 -0
  42. package/dist-server/engine/operation-basis.js +87 -0
  43. package/dist-server/engine/operation-basis.js.map +1 -0
  44. package/dist-server/engine/property-effects.d.ts +30 -0
  45. package/dist-server/engine/property-effects.js +192 -0
  46. package/dist-server/engine/property-effects.js.map +1 -0
  47. package/dist-server/engine/runtime-key.d.ts +1 -1
  48. package/dist-server/engine/runtime-key.js +1 -1
  49. package/dist-server/engine/runtime-key.js.map +1 -1
  50. package/dist-server/engine/spec-coverage.js +3 -3
  51. package/dist-server/engine/spec-coverage.js.map +1 -1
  52. package/dist-server/engine/state-axes.d.ts +7 -2
  53. package/dist-server/engine/state-axes.js +8 -3
  54. package/dist-server/engine/state-axes.js.map +1 -1
  55. package/dist-server/engine/structure-diff.js +1 -1
  56. package/dist-server/engine/structure-diff.js.map +1 -1
  57. package/dist-server/engine/travel-estimator.d.ts +9 -2
  58. package/dist-server/engine/travel-estimator.js +11 -6
  59. package/dist-server/engine/travel-estimator.js.map +1 -1
  60. package/dist-server/engine/twin-engine.d.ts +332 -47
  61. package/dist-server/engine/twin-engine.js +1314 -279
  62. package/dist-server/engine/twin-engine.js.map +1 -1
  63. package/dist-server/engine/warm-start.d.ts +38 -4
  64. package/dist-server/engine/warm-start.js +38 -9
  65. package/dist-server/engine/warm-start.js.map +1 -1
  66. package/dist-server/index.d.ts +1 -0
  67. package/dist-server/index.js +17 -11
  68. package/dist-server/index.js.map +1 -1
  69. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.d.ts +5 -0
  70. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +48 -0
  71. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -0
  72. package/dist-server/migrations/index.d.ts +2 -0
  73. package/dist-server/migrations/index.js +10 -0
  74. package/dist-server/migrations/index.js.map +1 -0
  75. package/dist-server/service/index.d.ts +3 -2
  76. package/dist-server/service/index.js +25 -17
  77. package/dist-server/service/index.js.map +1 -1
  78. package/dist-server/service/reference/discovery-result.d.ts +1 -1
  79. package/dist-server/service/reference/discovery-result.js +2 -2
  80. package/dist-server/service/reference/discovery-result.js.map +1 -1
  81. package/dist-server/service/reference/ingest-space.d.ts +24 -1
  82. package/dist-server/service/reference/ingest-space.js +17 -1
  83. package/dist-server/service/reference/ingest-space.js.map +1 -1
  84. package/dist-server/service/reference/reference-live.d.ts +23 -0
  85. package/dist-server/service/reference/reference-live.js +121 -4
  86. package/dist-server/service/reference/reference-live.js.map +1 -1
  87. package/dist-server/service/reference/reference-master.d.ts +194 -13
  88. package/dist-server/service/reference/reference-master.js +159 -28
  89. package/dist-server/service/reference/reference-master.js.map +1 -1
  90. package/dist-server/service/reference/reference-resolver.d.ts +31 -1
  91. package/dist-server/service/reference/reference-resolver.js +223 -12
  92. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  93. package/dist-server/service/reference/template-registry.d.ts +19 -3
  94. package/dist-server/service/reference/template-registry.js.map +1 -1
  95. package/dist-server/service/twin-audit/command-audit.d.ts +37 -0
  96. package/dist-server/service/twin-audit/command-audit.js +53 -0
  97. package/dist-server/service/twin-audit/command-audit.js.map +1 -0
  98. package/dist-server/service/twin-audit/index.d.ts +4 -0
  99. package/dist-server/service/twin-audit/index.js +8 -0
  100. package/dist-server/service/twin-audit/index.js.map +1 -0
  101. package/dist-server/service/twin-audit/twin-audit-event.d.ts +24 -0
  102. package/dist-server/service/twin-audit/twin-audit-event.js +141 -0
  103. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -0
  104. package/dist-server/service/twin-audit/twin-audit-query.d.ts +4 -0
  105. package/dist-server/service/twin-audit/twin-audit-query.js +76 -0
  106. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -0
  107. package/dist-server/service/twin-control/twin-control-mutation.js +21 -1
  108. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  109. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  110. package/dist-server/service/twin-event/twin-event-keys.js +1 -1
  111. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  112. package/dist-server/service/twin-event/twin-event.d.ts +14 -5
  113. package/dist-server/service/twin-event/twin-event.js +45 -13
  114. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  115. package/dist-server/service/twin-forecast/forecast-metrics.d.ts +13 -0
  116. package/dist-server/service/twin-forecast/forecast-metrics.js +57 -0
  117. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -0
  118. package/dist-server/service/twin-forecast/forecast-tuning.d.ts +12 -0
  119. package/dist-server/service/twin-forecast/forecast-tuning.js +44 -0
  120. package/dist-server/service/twin-forecast/forecast-tuning.js.map +1 -0
  121. package/dist-server/service/twin-forecast/gap-analytics.d.ts +28 -2
  122. package/dist-server/service/twin-forecast/gap-analytics.js +43 -17
  123. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  124. package/dist-server/service/twin-forecast/twin-forecast-query.js +217 -38
  125. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  126. package/dist-server/service/twin-instance/twin-instance.d.ts +38 -5
  127. package/dist-server/service/twin-instance/twin-instance.js +66 -20
  128. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  129. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -2
  130. package/dist-server/service/twin-journal/twin-journal-query.js +71 -9
  131. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  132. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +3 -5
  133. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +24 -27
  134. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  135. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  136. package/dist-server/service/twin-metrics/twin-metrics-query.js +62 -0
  137. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  138. package/dist-server/service/twin-model/epcis-coverage.d.ts +14 -0
  139. package/dist-server/service/twin-model/epcis-coverage.js +174 -0
  140. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -0
  141. package/dist-server/service/twin-model/iec61850-coverage.d.ts +14 -0
  142. package/dist-server/service/twin-model/iec61850-coverage.js +98 -0
  143. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -0
  144. package/dist-server/service/twin-model/index.d.ts +13 -0
  145. package/dist-server/service/twin-model/index.js +29 -0
  146. package/dist-server/service/twin-model/index.js.map +1 -0
  147. package/dist-server/service/twin-model/isa95-coverage.d.ts +48 -0
  148. package/dist-server/service/twin-model/isa95-coverage.js +155 -0
  149. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -0
  150. package/dist-server/service/twin-model/project-structure.d.ts +16 -0
  151. package/dist-server/service/twin-model/project-structure.js +173 -0
  152. package/dist-server/service/twin-model/project-structure.js.map +1 -0
  153. package/dist-server/service/twin-model/standard-coverage.d.ts +9 -0
  154. package/dist-server/service/twin-model/standard-coverage.js +37 -0
  155. package/dist-server/service/twin-model/standard-coverage.js.map +1 -0
  156. package/dist-server/service/twin-model/twin-equipment.d.ts +64 -0
  157. package/dist-server/service/twin-model/twin-equipment.js +134 -0
  158. package/dist-server/service/twin-model/twin-equipment.js.map +1 -0
  159. package/dist-server/service/twin-model/twin-lineage-query.d.ts +3 -0
  160. package/dist-server/service/twin-model/twin-lineage-query.js +206 -0
  161. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -0
  162. package/dist-server/service/twin-model/twin-location.d.ts +58 -0
  163. package/dist-server/service/twin-model/twin-location.js +130 -0
  164. package/dist-server/service/twin-model/twin-location.js.map +1 -0
  165. package/dist-server/service/twin-model/twin-model-item-query.d.ts +141 -0
  166. package/dist-server/service/twin-model/twin-model-item-query.js +852 -0
  167. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -0
  168. package/dist-server/service/twin-model/twin-model-mutation.d.ts +57 -0
  169. package/dist-server/service/twin-model/twin-model-mutation.js +490 -0
  170. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -0
  171. package/dist-server/service/twin-model/twin-model-query.d.ts +43 -0
  172. package/dist-server/service/twin-model/twin-model-query.js +551 -0
  173. package/dist-server/service/twin-model/twin-model-query.js.map +1 -0
  174. package/dist-server/service/twin-model/twin-model-tree-query.d.ts +3 -0
  175. package/dist-server/service/twin-model/twin-model-tree-query.js +158 -0
  176. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -0
  177. package/dist-server/service/twin-model/twin-operation.d.ts +61 -0
  178. package/dist-server/service/twin-model/twin-operation.js +137 -0
  179. package/dist-server/service/twin-model/twin-operation.js.map +1 -0
  180. package/dist-server/service/twin-space/move-space.d.ts +65 -0
  181. package/dist-server/service/twin-space/move-space.js +166 -0
  182. package/dist-server/service/twin-space/move-space.js.map +1 -0
  183. package/dist-server/service/twin-space/space-integrity.d.ts +97 -0
  184. package/dist-server/service/twin-space/space-integrity.js +182 -0
  185. package/dist-server/service/twin-space/space-integrity.js.map +1 -0
  186. package/dist-server/service/twin-space/twin-space-area.js +4 -4
  187. package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
  188. package/dist-server/service/twin-space/twin-space-representation.js +6 -6
  189. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  190. package/dist-server/service/twin-space/twin-space-resolver.d.ts +63 -1
  191. package/dist-server/service/twin-space/twin-space-resolver.js +356 -18
  192. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  193. package/dist-server/service/twin-space/twin-space.d.ts +17 -1
  194. package/dist-server/service/twin-space/twin-space.js +15 -5
  195. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  196. package/dist-server/service/twin-structure/twin-structure.d.ts +1 -1
  197. package/dist-server/service/twin-structure/twin-structure.js +8 -8
  198. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  199. package/dist-shared/axis-read.d.ts +12 -0
  200. package/dist-shared/axis-read.js +32 -0
  201. package/dist-shared/axis-read.js.map +1 -0
  202. package/{dist-server/engine → dist-shared}/entity-delta.d.ts +3 -0
  203. package/{dist-server/engine → dist-shared}/entity-delta.js +102 -2
  204. package/dist-shared/entity-delta.js.map +1 -0
  205. package/dist-shared/kpi-broadcast.js.map +1 -0
  206. package/dist-shared/twin-level.js.map +1 -0
  207. package/package.json +14 -12
  208. package/server/engine/canonical-ingest.ts +36 -4
  209. package/server/engine/command-routing.ts +1 -1
  210. package/server/engine/energy-topology.ts +189 -0
  211. package/server/engine/index.ts +10 -3
  212. package/server/engine/kpi-baseline.ts +1 -1
  213. package/server/engine/kpi-fold.ts +389 -1
  214. package/server/engine/kpi-query.ts +214 -19
  215. package/server/engine/kpi-target.ts +24 -2
  216. package/server/engine/live-feed-registry.ts +58 -0
  217. package/server/engine/load-meter.ts +384 -0
  218. package/server/engine/local-declarations.ts +700 -0
  219. package/server/engine/model-basis.ts +78 -10
  220. package/server/engine/{board-vocabulary.ts → model-vocabulary.ts} +36 -15
  221. package/server/engine/oee-accumulator.ts +34 -1
  222. package/server/engine/operation-basis.ts +100 -0
  223. package/server/engine/property-effects.ts +199 -0
  224. package/server/engine/runtime-key.ts +1 -1
  225. package/server/engine/spec-coverage.ts +3 -3
  226. package/server/engine/state-axes.ts +8 -3
  227. package/server/engine/structure-diff.ts +1 -1
  228. package/server/engine/travel-estimator.ts +11 -6
  229. package/server/engine/twin-engine.ts +1414 -274
  230. package/server/engine/warm-start.ts +83 -12
  231. package/server/index.ts +13 -10
  232. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +43 -0
  233. package/server/migrations/index.ts +7 -0
  234. package/server/service/index.ts +9 -1
  235. package/server/service/reference/discovery-result.ts +2 -2
  236. package/server/service/reference/ingest-space.ts +35 -1
  237. package/server/service/reference/reference-live.ts +119 -4
  238. package/server/service/reference/reference-master.ts +276 -35
  239. package/server/service/reference/reference-resolver.ts +232 -12
  240. package/server/service/reference/template-registry.ts +20 -5
  241. package/server/service/twin-audit/command-audit.ts +81 -0
  242. package/server/service/twin-audit/index.ts +5 -0
  243. package/server/service/twin-audit/twin-audit-event.ts +138 -0
  244. package/server/service/twin-audit/twin-audit-query.ts +72 -0
  245. package/server/service/twin-control/twin-control-mutation.ts +22 -1
  246. package/server/service/twin-event/twin-event-keys.ts +1 -1
  247. package/server/service/twin-event/twin-event.ts +59 -14
  248. package/server/service/twin-forecast/forecast-metrics.ts +60 -0
  249. package/server/service/twin-forecast/forecast-tuning.ts +40 -0
  250. package/server/service/twin-forecast/gap-analytics.ts +48 -9
  251. package/server/service/twin-forecast/twin-forecast-query.ts +208 -36
  252. package/server/service/twin-instance/twin-instance.ts +126 -25
  253. package/server/service/twin-journal/twin-journal-query.ts +74 -10
  254. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +23 -22
  255. package/server/service/twin-metrics/twin-metrics-query.ts +59 -2
  256. package/server/service/twin-model/epcis-coverage.ts +195 -0
  257. package/server/service/twin-model/iec61850-coverage.ts +117 -0
  258. package/server/service/twin-model/index.ts +25 -0
  259. package/server/service/twin-model/isa95-coverage.ts +190 -0
  260. package/server/service/twin-model/project-structure.ts +199 -0
  261. package/server/service/twin-model/standard-coverage.ts +35 -0
  262. package/server/service/twin-model/twin-equipment.ts +157 -0
  263. package/server/service/twin-model/twin-lineage-query.ts +193 -0
  264. package/server/service/twin-model/twin-location.ts +147 -0
  265. package/server/service/twin-model/twin-model-item-query.ts +848 -0
  266. package/server/service/twin-model/twin-model-mutation.ts +503 -0
  267. package/server/service/twin-model/twin-model-query.ts +537 -0
  268. package/server/service/twin-model/twin-model-tree-query.ts +180 -0
  269. package/server/service/twin-model/twin-operation.ts +157 -0
  270. package/server/service/twin-space/move-space.ts +262 -0
  271. package/server/service/twin-space/space-integrity.ts +293 -0
  272. package/server/service/twin-space/twin-space-area.ts +4 -4
  273. package/server/service/twin-space/twin-space-representation.ts +6 -6
  274. package/server/service/twin-space/twin-space-resolver.ts +355 -21
  275. package/server/service/twin-space/twin-space.ts +40 -6
  276. package/server/service/twin-structure/twin-structure.ts +10 -10
  277. package/shared/axis-read.ts +28 -0
  278. package/{server/engine → shared}/entity-delta.ts +96 -1
  279. package/test/adopt-structure-live.test.ts +133 -0
  280. package/test/attention-digest.test.ts +3 -3
  281. package/test/axis-read.test.ts +87 -0
  282. package/test/boot-resume.test.ts +205 -0
  283. package/test/canonical-ingest-vocabularies.test.ts +118 -0
  284. package/test/capability-mapping.test.ts +1 -1
  285. package/test/column-type-portability.test.ts +122 -0
  286. package/test/command-routing.test.ts +1 -1
  287. package/test/declaration-reaches-model.test.ts +178 -0
  288. package/test/discovery-result.test.ts +1 -1
  289. package/test/energy-topology.test.ts +113 -0
  290. package/test/entity-delta.test.ts +171 -1
  291. package/test/event-time-column.test.ts +67 -0
  292. package/test/forecast-metrics.test.ts +71 -0
  293. package/test/forecast-tuning.test.ts +42 -0
  294. package/test/gap-analytics.test.ts +34 -7
  295. package/test/ingest-bench.test.ts +6 -6
  296. package/test/ingest-running-guard.test.ts +136 -0
  297. package/test/ingest-space.test.ts +29 -1
  298. package/test/instance-cache-lifecycle.test.ts +110 -0
  299. package/test/kernel-kind-guard.test.ts +96 -0
  300. package/test/kpi-baseline-db.test.ts +3 -1
  301. package/test/kpi-baseline.test.ts +3 -3
  302. package/test/kpi-fold.test.ts +393 -2
  303. package/test/kpi-query-bench.test.ts +5 -3
  304. package/test/lineage-survives-restart.test.ts +203 -0
  305. package/test/live-feed-registry.test.ts +67 -0
  306. package/test/live-kernel-facts.test.ts +161 -0
  307. package/test/live-mirror-parity.test.ts +22 -15
  308. package/test/load-meter.test.ts +314 -0
  309. package/test/local-declarations.test.ts +554 -0
  310. package/test/master-to-twin.test.ts +121 -33
  311. package/test/model-basis.test.ts +56 -1
  312. package/test/{board-vocabulary.test.ts → model-vocabulary.test.ts} +25 -25
  313. package/test/move-space.test.ts +149 -0
  314. package/test/mutation-gate.test.ts +19 -5
  315. package/test/oee-accumulator.test.ts +26 -7
  316. package/test/operation-basis.test.ts +100 -0
  317. package/test/operations-capability-db.test.ts +165 -0
  318. package/test/project-structure-db.test.ts +226 -0
  319. package/test/projection-reaches-screen.test.ts +272 -0
  320. package/test/property-effects.test.ts +96 -0
  321. package/test/resync-origin-site.test.ts +77 -0
  322. package/test/scale-twin-bench.test.ts +5 -5
  323. package/test/snapshot-freshness.test.ts +60 -0
  324. package/test/space-integrity.test.ts +223 -0
  325. package/test/standard-coverage.test.ts +94 -0
  326. package/test/state-axes.test.ts +1 -1
  327. package/test/streamline-e2e.test.ts +14 -14
  328. package/test/structure-revision-db.test.ts +11 -11
  329. package/test/tenant-registry-db.test.ts +5 -5
  330. package/test/twin-audit.test.ts +83 -0
  331. package/test/twin-model-item-db.test.ts +275 -0
  332. package/test/twin-model-tree-db.test.ts +176 -0
  333. package/test/twin-origin-resync.test.ts +114 -0
  334. package/test/warm-start-seam.test.ts +6 -6
  335. package/test/warm-start.test.ts +88 -3
  336. package/tsconfig.json +6 -1
  337. package/tsconfig.shared.json +23 -0
  338. package/tsconfig.shared.tsbuildinfo +1 -0
  339. package/tsconfig.tsbuildinfo +1 -0
  340. package/dist-server/engine/board-vocabulary.js.map +0 -1
  341. package/dist-server/engine/entity-delta.js.map +0 -1
  342. package/dist-server/engine/kpi-broadcast.js.map +0 -1
  343. package/dist-server/engine/twin-level.js.map +0 -1
  344. package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
  345. package/dist-server/service/twin-event/backfill-keys.js +0 -63
  346. package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
  347. package/dist-server/tsconfig.tsbuildinfo +0 -1
  348. package/server/service/twin-event/backfill-keys.ts +0 -72
  349. /package/{dist-server/engine → dist-shared}/kpi-broadcast.d.ts +0 -0
  350. /package/{dist-server/engine → dist-shared}/kpi-broadcast.js +0 -0
  351. /package/{dist-server/engine → dist-shared}/twin-level.d.ts +0 -0
  352. /package/{dist-server/engine → dist-shared}/twin-level.js +0 -0
  353. /package/{server/engine → shared}/kpi-broadcast.ts +0 -0
  354. /package/{server/engine → shared}/twin-level.ts +0 -0
@@ -7,29 +7,64 @@ import { TwinSpaceRepresentation } from './twin-space-representation.js'
7
7
  import { TwinSpaceArea } from './twin-space-area.js'
8
8
  import { TwinArea } from './twin-area.js'
9
9
  import { TwinInstance } from '../twin-instance/twin-instance.js'
10
+ import { TwinAuditEvent } from '../twin-audit/twin-audit-event.js'
11
+ import { TwinEngine } from '../../engine/index.js'
12
+ import { judgeMoveTwinSpace, judgeDetachTwinSpace } from './move-space.js'
13
+ import { checkSpaceIntegrity, sortFindings, type IntegrityPolygon } from './space-integrity.js'
10
14
 
11
15
  /*
12
16
  * 공간 구성 채널(design/ui/space-composition.md) — Site(Space) + 복수 표현(Representation) + 영역(Area).
13
- * board 내용은 board-service Board 에(표현 config.boardId 참조, 중복 저장 금지). 모두 domain 스코프.
17
+ * model 내용은 board-service Board 에(표현 config.boardId 참조, 중복 저장 금지). 모두 domain 스코프.
14
18
  */
15
19
  @Resolver()
16
20
  export class TwinSpaceResolver {
17
21
  /* ── Space(Site) ────────────────────────────────────────────────── */
22
+ /**
23
+ * 이 테넌트의 현장 목록.
24
+ *
25
+ * ── 세 번만 묻는다 (2026-08-13) ──────────────────────────────────────────
26
+ * 예전에는 현장마다 표현 수를 세고 인스턴스를 조회했다 — 현장 13곳이면 **질의 27번**이고, 첫
27
+ * 호출이 2.4초였다(헤더가 뜰 때마다 이 목록을 읽는다). 규모가 커지면 그대로 자란다.
28
+ *
29
+ * 이제 세 번 묻고 메모리에서 맞춘다: 현장 · 표현(공간 id 만) · 인스턴스(필요한 컬럼만).
30
+ * 표현은 **id 만 실어 온다**(`loadRelationIds`) — 설정 blob 을 읽어 세는 것은 낭비다.
31
+ */
18
32
  @Query(returns => [ScalarObject], { description: 'List tenant Spaces (Sites) in the current domain.' })
19
33
  async twinSpaces(@Ctx() context: ResolverContext): Promise<any[]> {
20
34
  const domainId = context.state.domain.id
21
- const rows = await getRepository(TwinSpace).find({ where: { domain: { id: domainId } }, order: { createdAt: 'ASC' } })
22
- const out: any[] = []
23
- for (const r of rows) {
24
- const representationCount = await getRepository(TwinSpaceRepresentation).count({ where: { domain: { id: domainId } as any, space: { id: r.id } as any } })
25
- // 벤치 여부·원본 참조 — 인스턴스 purpose 근거(1급). 사본 공간을 헤더 선택기에서 구별·선택 가능하게.
26
- const insts = await getRepository(TwinInstance).find({ where: { domain: { id: domainId } as any, spaceId: r.spaceId } })
27
- const benchInst = insts.find(i => (i as any).purpose === 'bench')
28
- const bench = !!benchInst
29
- const copyOf = benchInst?.copyOf
30
- out.push({ spaceId: r.spaceId, name: r.name, description: r.description, geo: r.geo, timezone: r.timezone, primaryRepresentationId: r.primaryRepresentationId, content: r.content, representationCount, bench, copyOf })
35
+ const [rows, reps, insts] = await Promise.all([
36
+ getRepository(TwinSpace).find({ where: { domain: { id: domainId } }, order: { createdAt: 'ASC' } }),
37
+ /* 관계를 **조인해서** 읽는다 — `loadRelationIds`+`select` 조합은 관계 자리를 비워 두어
38
+ 표현 수가 전부 0 이 됐다(2026-08-13 실측으로 잡음). 조인 한 번이 현장마다 세는 것보다 싸다. */
39
+ getRepository(TwinSpaceRepresentation).find({ where: { domain: { id: domainId } }, relations: { space: true } }),
40
+ getRepository(TwinInstance).find({ where: { domain: { id: domainId } }, select: { instanceId: true, spaceId: true, purpose: true, copyOf: true } })
41
+ ])
42
+
43
+ const repCount = new Map<string, number>()
44
+ for (const rep of reps) {
45
+ const sid = rep.space?.id
46
+ if (sid) repCount.set(sid, (repCount.get(sid) ?? 0) + 1)
31
47
  }
32
- return out
48
+ /* 벤치 여부·원본 참조 — 인스턴스 purpose 근거(1급). 사본 현장을 헤더 선택기에서 구별·선택 가능하게. */
49
+ const benchOf = new Map<string, { copyOf?: string }>()
50
+ for (const i of insts) if ((i as any).purpose === 'bench' && i.spaceId && !benchOf.has(i.spaceId)) benchOf.set(i.spaceId, { copyOf: i.copyOf ?? undefined })
51
+
52
+ return rows.map(r => {
53
+ const bench = benchOf.get(r.spaceId)
54
+ return {
55
+ spaceId: r.spaceId,
56
+ name: r.name,
57
+ description: r.description,
58
+ geo: r.geo,
59
+ timezone: r.timezone,
60
+ primaryRepresentationId: r.primaryRepresentationId,
61
+ extentX: r.extentX,
62
+ extentY: r.extentY,
63
+ representationCount: repCount.get(r.id) ?? 0,
64
+ bench: !!bench,
65
+ copyOf: bench?.copyOf
66
+ }
67
+ })
33
68
  }
34
69
 
35
70
  /** 단건 — 표현(+영역) 트리 포함. */
@@ -44,7 +79,7 @@ export class TwinSpaceResolver {
44
79
  const areas = await getRepository(TwinSpaceArea).find({ where: { domain: { id: domainId }, representation: { id: rep.id } }, order: { createdAt: 'ASC' } })
45
80
  representations.push({ id: rep.id, type: rep.type, name: rep.name, seq: rep.seq, isPrimary: rep.isPrimary, config: rep.config, areas: areas.map(a => ({ id: a.id, name: a.name, type: a.type, geometry: a.geometry, drillTo: a.drillTo, binding: a.binding, style: a.style })) })
46
81
  }
47
- return { spaceId: r.spaceId, name: r.name, description: r.description, geo: r.geo, timezone: r.timezone, primaryRepresentationId: r.primaryRepresentationId, content: r.content, representations }
82
+ return { spaceId: r.spaceId, name: r.name, description: r.description, geo: r.geo, timezone: r.timezone, primaryRepresentationId: r.primaryRepresentationId, extentX: r.extentX, extentY: r.extentY, representations }
48
83
  }
49
84
 
50
85
  @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
@@ -63,17 +98,34 @@ export class TwinSpaceResolver {
63
98
  return true
64
99
  }
65
100
 
101
+ /**
102
+ * 공간 삭제 — **이 공간의 트윈이 있으면 거절한다.** 그래서 트윈이 현장을 잃는 일은 없다.
103
+ *
104
+ * ── 왜 모양을 바꿨나 (2026-08-13) ────────────────────────────────────────
105
+ * 예전에는 `Boolean` 을 돌려주고 거절은 **한국어 문장을 던졌다.** 두 가지가 틀렸다:
106
+ * ① 서버가 화면의 말을 정했다 — 일본어·스페인어 사용자에게 한국어가 새어 나간다.
107
+ * ② 던진 예외를 화면이 받지 않아 **아무 일도 없는 것처럼** 보였다. 사용자는 삭제를 눌렀고
108
+ * 화면은 그대로였다. 거절을 조용히 하는 것은 실패보다 나쁘다.
109
+ * 이제 `{ok, errorCode, errorParams}` 로 낸다 — 화면이 `twin.compose.del.<code>` 로 말을 만든다.
110
+ */
66
111
  @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
67
- @Mutation(returns => Boolean, { description: 'Delete a Space and its representations/areas.' })
68
- async deleteTwinSpace(@Arg('spaceId') spaceId: string, @Ctx() context: ResolverContext): Promise<boolean> {
112
+ @Mutation(returns => ScalarObject, {
113
+ description:
114
+ 'Delete a Space and its representations/areas. Refused while twins still live in it (a twin never loses its site this way). Returns {ok, errorCode, errorParams}.'
115
+ })
116
+ async deleteTwinSpace(@Arg('spaceId') spaceId: string, @Ctx() context: ResolverContext): Promise<any> {
69
117
  const domainId = context.state.domain.id
70
118
  // 라이프사이클 가드 — 이 공간에 바인딩된 트윈 인스턴스가 있으면 삭제 차단(dangling 방지).
71
119
  // spaceId 컬럼(space #3) 로 직접 질의 — 전 인스턴스 로드+JS필터 제거(효율·1급 질의).
72
120
  const bound = await getRepository(TwinInstance).find({ where: { domain: { id: domainId }, spaceId } })
73
121
  if (bound.length) {
74
- throw new Error(
75
- `공간 "${spaceId}" 에 트윈 인스턴스 ${bound.length}개가 바인딩돼 있어 삭제할 수 없습니다. 먼저 트윈을 제거하세요 (${bound.map(i => i.instanceId).join(', ')}).`
76
- )
122
+ const twins = bound.map(i => i.name || i.instanceId)
123
+ return {
124
+ ok: false,
125
+ errorCode: 'space-has-twins',
126
+ errorParams: { spaceId, count: bound.length, twins: twins.slice(0, 5).join(', ') },
127
+ error: `space "${spaceId}" still has ${bound.length} twin(s) — remove or move them first (${twins.join(', ')})`
128
+ }
77
129
  }
78
130
  const space = await getRepository(TwinSpace).findOne({ where: { domain: { id: domainId }, spaceId } })
79
131
  if (space) {
@@ -83,7 +135,289 @@ export class TwinSpaceResolver {
83
135
  await getRepository(TwinArea).delete({ domain: { id: domainId } as any, space: { id: space.id } as any }) // 논리 area 도 함께 정리
84
136
  }
85
137
  await getRepository(TwinSpace).delete({ domain: { id: domainId } as any, spaceId })
86
- return true
138
+ return { ok: true }
139
+ }
140
+
141
+ /**
142
+ * 이미 있는 트윈을 이 현장으로 가져온다 — **소속 갱신**(이력 이전이 아니다).
143
+ *
144
+ * 저널·투영 행은 `instanceId` 로 매여 있어 트윈을 따라오고, 구역·표현은 공간의 것이라 남는다
145
+ * (근거·판정: `move-space.ts`). 그래서 이 뮤테이션이 쓰는 것은 두 곳뿐이다 —
146
+ * 레지스트리의 `spaceId` 와 저장된 model 의 `spaceId`(커널·보드가 같은 공간을 가리켜야 한다).
147
+ * 새 공간의 시간대로 `utcOffsetMinutes` 도 다시 파생한다(공간이 시간의 기준이다).
148
+ *
149
+ * `dryRun` 은 **같은 판정을 미리** 보여 준다 — 화면이 잃을 것을 먼저 말하고 사용자가 정한다.
150
+ * 판정을 두 벌로 두면 미리보기와 실제가 어긋나므로 한 함수를 두 번 부른다.
151
+ */
152
+ @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
153
+ @Mutation(returns => ScalarObject, {
154
+ description:
155
+ 'Move an existing twin into a space (co-locate it as another lens). The journal and projected rows follow the twin; areas and representations stay with the old space. dryRun returns the same judgement without writing. Returns {ok, warnings, errorCode}.'
156
+ })
157
+ async moveTwinToSpace(
158
+ @Arg('instanceId') instanceId: string,
159
+ @Arg('spaceId') spaceId: string,
160
+ @Ctx() context: ResolverContext,
161
+ @Arg('dryRun', { nullable: true, description: 'Judge only — report refusal/warnings without moving.' }) dryRun?: boolean
162
+ ): Promise<any> {
163
+ const domainId = context.state.domain.id
164
+ const instRepo = getRepository(TwinInstance)
165
+ const spaceRepo = getRepository(TwinSpace)
166
+
167
+ const inst = await instRepo.findOne({ where: { domain: { id: domainId }, instanceId } })
168
+ const from = inst?.spaceId ? await spaceRepo.findOne({ where: { domain: { id: domainId }, spaceId: inst.spaceId } }) : null
169
+ const to = await spaceRepo.findOne({ where: { domain: { id: domainId }, spaceId } })
170
+
171
+ /* 판정에 필요한 사실만 모아 넘긴다 — 판정 자체는 순수 모듈이 한다. */
172
+ const toAreaIds = to
173
+ ? (await getRepository(TwinArea).find({ where: { domain: { id: domainId } as any, space: { id: to.id } as any } })).map(a => a.areaId)
174
+ : []
175
+ const fromRepresentationCount = from
176
+ ? await getRepository(TwinSpaceRepresentation).count({ where: { domain: { id: domainId } as any, space: { id: from.id } as any } })
177
+ : 0
178
+ const model: any = inst?.model ?? {}
179
+ const areaRefs: (string | undefined)[] = (model.locations ?? []).map((l: any) => l?.parentId)
180
+
181
+ const judgement = judgeMoveTwinSpace({
182
+ instanceId: inst?.instanceId,
183
+ fromSpaceId: inst?.spaceId,
184
+ toSpaceId: spaceId,
185
+ toSpaceExists: !!to,
186
+ purpose: (inst as any)?.purpose,
187
+ running: !!inst && !!TwinEngine.runtime(domainId, instanceId),
188
+ fromTimezone: from?.timezone ?? undefined,
189
+ toTimezone: to?.timezone ?? undefined,
190
+ areaRefs,
191
+ toAreaIds,
192
+ fromRepresentationCount
193
+ })
194
+ if (judgement.refusal) {
195
+ const r = judgement.refusal
196
+ return { ok: false, error: r.message, errorCode: r.code, errorParams: r.params, warnings: [] }
197
+ }
198
+ if (dryRun) return { ok: true, dryRun: true, warnings: judgement.warnings }
199
+
200
+ /* 두 곳을 함께 쓴다 — 레지스트리와 model 이 다른 공간을 가리키면 보드·커널이 갈라진다. */
201
+ const moved = { ...model, spaceId }
202
+ const withTime = await TwinEngine.withSpaceTimeBase(moved, domainId, spaceId)
203
+ await instRepo.save(instRepo.create({ ...(inst as any), spaceId, model: withTime }))
204
+
205
+ /*
206
+ * 감사 기록 — 소속을 바꾼 것은 사람의 결정이고, 나중에 "왜 이 트윈이 저 현장에 있나" 를 묻게 된다.
207
+ * 기록이 실패해도 이전 결과는 돌려준다(감사가 본업을 막지 않는다). 다만 삼키지 않고 로그에 남긴다.
208
+ */
209
+ try {
210
+ const repo = getRepository(TwinAuditEvent)
211
+ await repo.save(
212
+ repo.create({
213
+ domain: context.state.domain,
214
+ instanceId,
215
+ commandType: 'twin.moveToSpace',
216
+ accepted: true,
217
+ origin: 'user',
218
+ actorId: context.state.user?.id,
219
+ args: { from: inst?.spaceId ?? null, to: spaceId, warnings: judgement.warnings.map(w => w.code) }
220
+ } as any)
221
+ )
222
+ } catch (err) {
223
+ console.error(`[twin-space] move audit not recorded for "${instanceId}"`, err)
224
+ }
225
+
226
+ return { ok: true, warnings: judgement.warnings }
227
+ }
228
+
229
+ /**
230
+ * 트윈을 현장에서 뗀다 — **어느 현장에도 속하지 않음**으로 되돌린다.
231
+ *
232
+ * 더하기·옮기기만 있으면 잘못 넣은 것을 되돌릴 수 없다. 떼도 저널·로케이션·설비는 트윈에 남고
233
+ * (`instanceId` 로 매여 있다), 구역·표현은 옛 현장에 남는다. 잃는 것은 **볼 무대**다 —
234
+ * 판정이 그것을 말한다(`judgeDetachTwinSpace`). `dryRun` 은 같은 판정을 미리 낸다.
235
+ */
236
+ @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
237
+ @Mutation(returns => ScalarObject, {
238
+ description:
239
+ 'Detach a twin from its space (it ends up on no site). The journal and projected rows stay with the twin; areas and representations stay with the space. dryRun returns the same judgement without writing. Returns {ok, warnings, errorCode}.'
240
+ })
241
+ async detachTwinFromSpace(
242
+ @Arg('instanceId') instanceId: string,
243
+ @Ctx() context: ResolverContext,
244
+ @Arg('dryRun', { nullable: true, description: 'Judge only — report refusal/warnings without detaching.' }) dryRun?: boolean
245
+ ): Promise<any> {
246
+ const domainId = context.state.domain.id
247
+ const instRepo = getRepository(TwinInstance)
248
+ const inst = await instRepo.findOne({ where: { domain: { id: domainId }, instanceId } })
249
+ const from = inst?.spaceId
250
+ ? await getRepository(TwinSpace).findOne({ where: { domain: { id: domainId }, spaceId: inst.spaceId } })
251
+ : null
252
+ const fromRepresentationCount = from
253
+ ? await getRepository(TwinSpaceRepresentation).count({ where: { domain: { id: domainId } as any, space: { id: from.id } as any } })
254
+ : 0
255
+
256
+ const judgement = judgeDetachTwinSpace({
257
+ instanceId: inst?.instanceId,
258
+ fromSpaceId: inst?.spaceId,
259
+ purpose: (inst as any)?.purpose,
260
+ fromRepresentationCount,
261
+ fromTimezone: from?.timezone ?? undefined
262
+ })
263
+ if (judgement.refusal) {
264
+ const r = judgement.refusal
265
+ return { ok: false, error: r.message, errorCode: r.code, errorParams: r.params, warnings: [] }
266
+ }
267
+ if (dryRun) return { ok: true, dryRun: true, warnings: judgement.warnings }
268
+
269
+ /* 두 곳을 함께 비운다 — 레지스트리와 model 이 어긋나면 어느 쪽이 사실인지 알 수 없다.
270
+ 시간 기준(utcOffsetMinutes)은 **남긴다**: 지우면 조용히 UTC 로 읽힌다(판정이 그렇게 말했다). */
271
+ const { spaceId: _dropped, ...model } = (inst?.model as any) ?? {}
272
+ await instRepo.update({ id: (inst as any).id }, { spaceId: null as any, model })
273
+
274
+ try {
275
+ const repo = getRepository(TwinAuditEvent)
276
+ await repo.save(
277
+ repo.create({
278
+ domain: context.state.domain,
279
+ instanceId,
280
+ commandType: 'twin.detachFromSpace',
281
+ accepted: true,
282
+ origin: 'user',
283
+ actorId: context.state.user?.id,
284
+ args: { from: inst?.spaceId ?? null, warnings: judgement.warnings.map(w => w.code) }
285
+ } as any)
286
+ )
287
+ } catch (err) {
288
+ console.error(`[twin-space] detach audit not recorded for "${instanceId}"`, err)
289
+ }
290
+
291
+ return { ok: true, warnings: judgement.warnings }
292
+ }
293
+
294
+ /**
295
+ * 공간 ↔ 트윈 ↔ 가시화 정합성 점검 — **사실을 모아 순수 판정에 넘긴다.**
296
+ *
297
+ * 세 축은 서로를 id 로만 가리키고 DB 가 그 참조를 지켜 주지 않는다. 트윈을 옮기거나 떼거나 원본을
298
+ * 다시 읽으면 아무 오류 없이 **가리킬 것이 없는 도형**과 **소속 없는 로케이션**이 남는다. 사람이
299
+ * 눈으로 대조하던 일을 기계가 한다(판정·근거: `space-integrity.ts`).
300
+ *
301
+ * `spaceId` 를 주면 그 현장만, 없으면 테넌트 전체를 본다.
302
+ */
303
+ @Query(returns => [ScalarObject], {
304
+ description:
305
+ 'Check space ↔ twin ↔ visualization integrity: dangling site refs, registry/model mismatch, polygons binding areas or entities that are not there, primary representation, unbound board representations. Pass spaceId to check one site.'
306
+ })
307
+ async twinSpaceIntegrity(
308
+ @Ctx() context: ResolverContext,
309
+ @Arg('spaceId', { nullable: true }) spaceId?: string
310
+ ): Promise<any[]> {
311
+ const domainId = context.state.domain.id
312
+ const spaceRows = await getRepository(TwinSpace).find({ where: { domain: { id: domainId } } })
313
+ const instRows = await getRepository(TwinInstance).find({ where: { domain: { id: domainId } } })
314
+ const repRows = await getRepository(TwinSpaceRepresentation).find({ where: { domain: { id: domainId } }, relations: ['space'] })
315
+ const areaRows = await getRepository(TwinArea).find({ where: { domain: { id: domainId } }, relations: ['space'] })
316
+ const polyRows = await getRepository(TwinSpaceArea).find({ where: { domain: { id: domainId } }, relations: ['representation'] })
317
+
318
+ const areasBySpace: Record<string, string[]> = {}
319
+ for (const a of areaRows) {
320
+ const sid = (a as any).space?.spaceId
321
+ if (!sid) continue
322
+ ;(areasBySpace[sid] ??= []).push(a.areaId)
323
+ }
324
+
325
+ /*
326
+ * 폴리곤 바인딩의 실제 모양은 `{ groupId?, locationId?, locationIds? }` 다(지도 저작기 기준).
327
+ * 여러 로케이션을 묶은 도형은 **각각을 따로** 본다 — 하나만 끊겨도 그 도형은 반쯤 거짓이 된다.
328
+ */
329
+ const polysByRep: Record<string, IntegrityPolygon[]> = {}
330
+ for (const p of polyRows) {
331
+ const rid = (p as any).representation?.id
332
+ if (!rid) continue
333
+ const b: any = p.binding ?? {}
334
+ const list = polysByRep[rid] ??= []
335
+ if (b.groupId) list.push({ id: p.id, groupId: b.groupId })
336
+ const targets: string[] = [...(b.locationId ? [b.locationId] : []), ...(Array.isArray(b.locationIds) ? b.locationIds : [])]
337
+ for (const t of targets) list.push({ id: p.id, targetKind: 'location', targetId: t })
338
+ }
339
+
340
+ /* 보드 id 는 표현 config 가 들고 있다 — 존재 확인은 보드 서비스의 일이므로, 여기서는 표현이
341
+ 가리키는 값이 있는지만 본다(없는 보드까지 보려면 board-service 를 물어야 한다: 아래 주석). */
342
+ const boardIds = repRows.map(r => (r.config as any)?.boardId).filter(Boolean) as string[]
343
+
344
+ const findings = checkSpaceIntegrity({
345
+ spaces: spaceRows.map(s => ({ spaceId: s.spaceId, primaryRepresentationId: s.primaryRepresentationId })),
346
+ instances: instRows.map(i => {
347
+ const model: any = i.model ?? {}
348
+ /*
349
+ * 어휘가 맞는지는 **실행 중인 트윈만** 답할 수 있다 — 커널이 무엇을 버렸는지는 런타임의 사실이다.
350
+ * 정지한 트윈은 `undefined`(모른다)로 둔다. 저널을 다시 접어 알아내지는 않는다: 이 질의는
351
+ * 테넌트 전체를 훑으므로 트윈마다 폴드를 걸면 정합성 점검이 가장 무거운 질의가 된다.
352
+ */
353
+ const live: any = TwinEngine.isRunning(domainId, i.instanceId)
354
+ ? TwinEngine.snapshot(domainId, i.instanceId)
355
+ : undefined
356
+ return {
357
+ instanceId: i.instanceId,
358
+ spaceId: i.spaceId,
359
+ modelSpaceId: model.spaceId,
360
+ purpose: (i as any).purpose,
361
+ locationIds: (model.locations ?? []).map((l: any) => l?.id).filter(Boolean),
362
+ equipmentIds: (model.equipment ?? []).map((e: any) => e?.id).filter(Boolean),
363
+ areaRefs: (model.locations ?? []).map((l: any) => l?.parentId),
364
+ ...(Array.isArray(live?.unhandled) ? { unhandled: live.unhandled } : {})
365
+ }
366
+ }),
367
+ representations: repRows.map(r => ({
368
+ id: r.id,
369
+ spaceId: (r as any).space?.spaceId ?? '',
370
+ type: r.type,
371
+ boardId: (r.config as any)?.boardId ?? null,
372
+ polygons: polysByRep[r.id] ?? []
373
+ })),
374
+ areasBySpace,
375
+ boardIds
376
+ })
377
+
378
+ const scoped = spaceId ? findings.filter(f => f.spaceId === spaceId || (!f.spaceId && !spaceId)) : findings
379
+ return sortFindings(scoped)
380
+ }
381
+
382
+ /**
383
+ * 기계적으로 고칠 수 있는 어긋남만 고친다 — **사람의 판단이 필요한 것은 손대지 않는다.**
384
+ *
385
+ * 지금 고치는 것은 하나다: **대표 표현 포인터**. 공간이 없는 표현 id 를 가리키고 있으면, 행의
386
+ * `isPrimary` 가 켜진 표현(없으면 첫 표현)으로 다시 가리키고, 표현이 없으면 비운다. 근거가 있고
387
+ * 답이 하나뿐인 부류라 자동으로 고쳐도 사실을 바꾸지 않는다.
388
+ *
389
+ * 폴리곤이 없는 구역·개체를 가리키는 것은 **고치지 않는다.** 그 도형을 지워야 하는지, 트윈을 다시
390
+ * 데려와야 하는지, 구역을 다시 그려야 하는지는 사람이 정할 일이다 — 기계가 고르면 사용자가 그린
391
+ * 도형이 조용히 사라진다.
392
+ */
393
+ @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
394
+ @Mutation(returns => ScalarObject, {
395
+ description:
396
+ 'Repair mechanically fixable space integrity findings (currently: a primary representation pointer that points nowhere). Dangling polygon bindings are left alone — that is a human decision. Returns {repaired:[{spaceId, code}], skipped}.'
397
+ })
398
+ async repairTwinSpaceIntegrity(
399
+ @Ctx() context: ResolverContext,
400
+ @Arg('spaceId', { nullable: true }) spaceId?: string
401
+ ): Promise<any> {
402
+ const domainId = context.state.domain.id
403
+ const spaceRepo = getRepository(TwinSpace)
404
+ const repRepo = getRepository(TwinSpaceRepresentation)
405
+ const spaces = await spaceRepo.find({
406
+ where: spaceId ? { domain: { id: domainId }, spaceId } : { domain: { id: domainId } }
407
+ })
408
+ const repaired: any[] = []
409
+ for (const space of spaces) {
410
+ const reps = await repRepo.find({ where: { domain: { id: domainId } as any, space: { id: space.id } as any }, order: { seq: 'ASC' } })
411
+ const pointer = (space.primaryRepresentationId ?? '').trim()
412
+ if (pointer && reps.some(r => r.id === pointer)) continue /* 이미 옳다 */
413
+ const primary = reps.find(r => r.isPrimary) ?? reps[0]
414
+ const next = primary?.id ?? null
415
+ if (pointer === (next ?? '')) continue
416
+ await spaceRepo.save(spaceRepo.create({ ...space, primaryRepresentationId: next }))
417
+ if (primary) for (const r of reps) if (!!r.isPrimary !== (r.id === primary.id)) await repRepo.save(repRepo.create({ ...r, isPrimary: r.id === primary.id }))
418
+ repaired.push({ spaceId: space.spaceId, code: 'primary-representation-missing', from: pointer || null, to: next })
419
+ }
420
+ return { ok: true, repaired, count: repaired.length }
87
421
  }
88
422
 
89
423
  /* ── Area(논리 구역, 표현 무관·공유·계층) — area 단일화 space #2 ─────── */
@@ -151,7 +485,7 @@ export class TwinSpaceResolver {
151
485
 
152
486
  /* ── Representation ─────────────────────────────────────────────── */
153
487
  @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
154
- @Mutation(returns => ScalarObject, { description: 'Create/update a representation (map|image|board). Returns {id}.' })
488
+ @Mutation(returns => ScalarObject, { description: 'Create/update a representation (map|image|model). Returns {id}.' })
155
489
  async saveSpaceRepresentation(
156
490
  @Arg('spaceId') spaceId: string,
157
491
  @Arg('rep', type => ScalarObject) rep: any,
@@ -191,7 +525,7 @@ export class TwinSpaceResolver {
191
525
 
192
526
  /* ── Area (map/image 폴리곤) ────────────────────────────────────── */
193
527
  @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
194
- @Mutation(returns => ScalarObject, { description: 'Create/update a polygon area (geometry + drillTo board + binding). Returns {id}.' })
528
+ @Mutation(returns => ScalarObject, { description: 'Create/update a polygon area (geometry + drillTo model + binding). Returns {id}.' })
195
529
  async saveSpaceArea(
196
530
  @Arg('representationId') representationId: string,
197
531
  @Arg('area', type => ScalarObject) area: any,
@@ -5,8 +5,8 @@ import { Domain, ScalarObject } from '@things-factory/shell'
5
5
 
6
6
  /*
7
7
  * TwinSpace — 테넌트 1급 공간(Site) 정의(ADR-0016). 트윈보다 먼저 세우는 몰입의 무대.
8
- * 공간이 보이는 방식(레이아웃·배경·영역)은 정규화됨: TwinSpaceRepresentation(map|image|board —
9
- * 이미지 배경은 attachment-base 참조 `config.imageUrl`) + TwinSpaceArea. 인스턴스는 board.spaceId 로 바인딩.
8
+ * 공간이 보이는 방식(레이아웃·배경·영역)은 정규화됨: TwinSpaceRepresentation(map|image|model —
9
+ * 이미지 배경은 attachment-base 참조 `config.imageUrl`) + TwinSpaceArea. 인스턴스는 model.spaceId 로 바인딩.
10
10
  * - geo = 사이트 지오레퍼런스, primaryRepresentationId = 홈/모니터 대표 표현.
11
11
  * - content(simple-json) 는 정규화 이전 레거시 단일 blob(@deprecated, P6 제거) — 신규 저작은 representation/area 로.
12
12
  * (CLAUDE.md: 모든 @ObjectType/@Field 영문 description 필수.)
@@ -39,9 +39,30 @@ export class TwinSpace {
39
39
  description?: string
40
40
 
41
41
  @Column({ type: 'simple-json', nullable: true })
42
- @Field(type => ScalarObject, { nullable: true, description: 'Site georeference { lat, lon, rotation, unit } — aligns representations.' })
42
+ @Field(type => ScalarObject, { nullable: true, description: 'Site georeference { lat, lon, rotation } — pins the design frame to the earth so every representation lands in the same place. `rotation` is degrees clockwise from true north (bearing convention: 0 = north, 90 = east) by which the design frame is turned.' })
43
43
  geo?: any
44
44
 
45
+ /*
46
+ * 설계 좌표계의 범위 — **미터**. `geo` 와 한 쌍이다.
47
+ *
48
+ * 구역(TwinArea.layout)은 표현 중립 설계 좌표 `{x, y, w, h}` 로 놓인다. 그 좌표가 어디까지인지를
49
+ * 이 둘이 정하고, `geo` 가 그것을 지구에 붙인다 — 지도로 보든 도면 이미지로 보든 보드로 보든
50
+ * 구역이 같은 자리에 있어야 하므로, 범위도 지오레퍼런스도 **표현이 아니라 공간**이 갖는다.
51
+ *
52
+ * 설계 좌표 (x, y) ──[extentX/extentY 로 중심 이동]──▶ 미터 오프셋 ──[geo]──▶ 위경도
53
+ *
54
+ * `width`/`depth` 라 부르지 않는 이유: 컴포넌트 속성의 `depth` 는 높이 축(z)이다. 같은 화면에서
55
+ * 한 낱말이 두 축을 가리키면 읽는 사람이 매번 어느 쪽인지 물어야 한다.
56
+ * 단위 필드는 두지 않는다 — 미터로 고정이고, 있으면 "단위를 지원한다" 는 거짓 신호가 된다.
57
+ */
58
+ @Column({ type: 'float', nullable: true })
59
+ @Field({ nullable: true, description: 'Extent of the design frame along the east-west axis, in metres. Area layouts are placed within this frame.' })
60
+ extentX?: number
61
+
62
+ @Column({ type: 'float', nullable: true })
63
+ @Field({ nullable: true, description: 'Extent of the design frame along the north-south axis, in metres. Area layouts are placed within this frame.' })
64
+ extentY?: number
65
+
45
66
  /**
46
67
  * 이 현장의 시각 기준 — IANA 이름(`'America/Tijuana'`).
47
68
  *
@@ -60,10 +81,23 @@ export class TwinSpace {
60
81
  @Field({ nullable: true, description: 'Id of the primary representation shown on the home/monitor.' })
61
82
  primaryRepresentationId?: string
62
83
 
63
- /* @deprecated 전환기 — 정규화(representation/area) 전 단일 content. P6 에서 제거. */
84
+ /**
85
+ * 이 **현장**의 로컬 선언 겹 — 요금처럼 **트윈이 아니라 현장이 정하는 수**.
86
+ *
87
+ * ── 왜 트윈에 두면 안 되나 (2026-08-18) ────────────────────────────────────
88
+ * 요금 단가·통화는 그 현장의 계약이므로, 같은 수전을 보는 EMS 트윈이 둘이면 **같은 값을 두 번**
89
+ * 선언해야 했다. 두 번 적는 값은 반드시 갈라진다(하나만 고치는 날이 온다) — 그때 두 트윈이 다른
90
+ * 금액을 말하고, 어느 쪽이 맞는지 화면으로는 알 수 없다.
91
+ *
92
+ * 적용 순서는 **현장 먼저, 트윈이 나중**이다. 더 좁은 선언이 이긴다 — 한 현장 안에서 특정 트윈만
93
+ * 다른 값으로 실험하는 일이 있고(예: 인상된 단가로 what-if), 그때 현장 값을 고치게 만들면 나머지
94
+ * 트윈이 함께 흔들린다.
95
+ *
96
+ * 모양은 트윈 겹과 같다(`LocalDeclaration`) — 같은 규칙(검사·기억·철회)을 두 벌로 만들지 않는다.
97
+ */
64
98
  @Column({ type: 'simple-json', nullable: true })
65
- @Field(type => ScalarObject, { nullable: true, description: '[deprecated] legacy single-blob content; superseded by normalized representations/areas.' })
66
- content?: any
99
+ @Field(type => ScalarObject, { nullable: true, description: "Locally-declared values that belong to this facility rather than to one twin (tariff, currency). Applied to every twin in the space before the twin's own declarations, which win when both name the same property." })
100
+ localDeclarations?: any
67
101
 
68
102
  @CreateDateColumn()
69
103
  @Field({ nullable: true, description: 'Timestamp when the space was created.' })
@@ -3,7 +3,7 @@ import { ObjectType, Field, ID, Int } from 'type-graphql'
3
3
 
4
4
  import { Domain, ScalarObject } from '@things-factory/shell'
5
5
  import { User } from '@things-factory/auth-base'
6
- import { boardColumnTransformer } from '../../engine/board-vocabulary.js'
6
+ import { modelColumnTransformer } from '../../engine/model-vocabulary.js'
7
7
 
8
8
  /*
9
9
  * TwinStructure — **한 트윈이 거쳐 온 공장 구조들.** 리비전 하나가 그 시절의 공장이다.
@@ -17,11 +17,11 @@ import { boardColumnTransformer } from '../../engine/board-vocabulary.js'
17
17
  * 그래서 선택지가 둘뿐이었다: **역사를 잃거나, 거짓말을 하거나.**
18
18
  *
19
19
  * 셋째 길이 이 표다. 이벤트가 `structureRev` 로 자기 구조를 달고 다니고, 재생은 구조가 바뀌는
20
- * 지점에서 갈아탄 뒤 이어 접는다(`replaySegments`). 이력은 "그때 그 공장의 사실" 로 계속 읽힌다.
20
+ * 지점에서 전환한 뒤 이어 접는다(`replaySegments`). 이력은 "그때 그 공장의 사실" 로 계속 읽힌다.
21
21
  *
22
- * ── 왜 board 를 통째로 담나 ─────────────────────────────────────────────────
23
- * 재생하려면 **그때의 토폴로지 전체**가 필요하다. 차이(diff)만 담으면 리비전 1부터 되짚어 쌓아야
24
- * 하고, 그 되짚기 자체가 또 하나의 규칙이 되어 갈라진다. 구조는 이벤트에 비해 드물게 바뀌므로
22
+ * ── 왜 model 를 통째로 담나 ─────────────────────────────────────────────────
23
+ * 재생하려면 **그때의 토폴로지 전체**가 필요하다. 차이(diff)만 담으면 리비전 1부터 다시 계산해 쌓아야
24
+ * 하고, 그 재계산 자체가 또 하나의 규칙이 되어 어긋난다. 구조는 이벤트에 비해 드물게 바뀌므로
25
25
  * 스냅샷을 통째로 두는 편이 싸고 정직하다(보드 이력 `BoardHistory` 와 같은 선택).
26
26
  *
27
27
  * ── audit 컬럼을 다 두지 않는 이유 ──────────────────────────────────────────
@@ -72,11 +72,11 @@ export class TwinStructure {
72
72
  @Field({ description: 'Short hash of the structural signature (locations + equipment). A re-provision with the same signature does not create a new revision — otherwise every reboot would slice the history into meaningless segments.' })
73
73
  signature: string
74
74
 
75
- /* 읽을 때 어휘 세대를 흡수한다 — 옛 리비전의 보드로 지문을 다시 계산할 때 빈 공장으로 읽히면
76
- 구조 변경 감지가 죽는다(`engine/board-vocabulary.ts`). */
77
- @Column({ type: 'simple-json', transformer: boardColumnTransformer })
78
- @Field(type => ScalarObject, { description: 'The full BoardDef as it stood for this revision. Stored whole rather than as a diff: replay needs the entire topology, and rebuilding it from diffs would be a second rule that can drift.' })
79
- board: any
75
+ /* 읽을 때 어휘 세대를 흡수한다 — 옛 리비전의 모델로 지문을 다시 계산할 때 빈 공장으로 읽히면
76
+ 구조 변경 감지가 죽는다(`engine/model-vocabulary.ts`). */
77
+ @Column({ type: 'simple-json', transformer: modelColumnTransformer })
78
+ @Field(type => ScalarObject, { description: 'The full TwinModelDef as it stood for this revision. Stored whole rather than as a diff: replay needs the entire topology, and rebuilding it from diffs would be a second rule that can drift.' })
79
+ model: any
80
80
 
81
81
  /** 왜 바뀌었나 — 사람이 남기는 한 줄. 없을 수 있다(자동 프로비저닝은 이유를 모른다). */
82
82
  @Column({ nullable: true })
@@ -0,0 +1,28 @@
1
+ /*
2
+ * 축의 값을 **어디서 읽나** — 규칙 한 곳. (design/profiles/ems.md §10 6.5단계)
3
+ *
4
+ * ── 왜 함수로 빼나 ─────────────────────────────────────────────────────────
5
+ * 개념 지도는 상태 축을 `state[축이름]` 으로 읽고 있었다. 에너지의 수요 구간은 `state.energy.closed`
6
+ * 에 살아서 그 규칙으로는 **언제나 `undefined`** 다 — 카드가 「관측 없음」으로 굳고, 원인은 화면도
7
+ * 서버도 말해 주지 않는다(축은 선언돼 있고 상태에도 값이 있는데 이름이 안 맞을 뿐이다).
8
+ *
9
+ * 커널 선언이 이미 그 자리를 말한다(`TwinAxisInfo.path`). 그래서 규칙은 한 줄이다:
10
+ * **경로가 있으면 그 경로로, 없으면 축 이름으로.** 리졸버 안에 두면 시험이 DB 를 물어야 하므로
11
+ * 순수 함수로 빼 둔다.
12
+ */
13
+
14
+ /** 중첩 경로를 따라간다 — 중간이 없으면 `undefined`(빈 객체로 메우지 않는다). */
15
+ export function atPath(obj: any, path: string): any {
16
+ return path.split('.').reduce((o: any, k: string) => (o == null ? undefined : o[k]), obj)
17
+ }
18
+
19
+ /**
20
+ * 그 축의 값 — 배열이면 목록, 객체면 그릇, 없으면 `undefined`.
21
+ *
22
+ * `undefined` 와 빈 배열은 **다르다**: 앞은 「모른다·아직 안 옴」이고 뒤는 「없다」다. 이 함수는 그
23
+ * 구별을 지우지 않는다 — 소비처가 `emptyBecause` 를 그 차이로 만든다.
24
+ */
25
+ export function axisValueOf(state: any, axis: { axis: string; path?: string }): any {
26
+ if (!state || !axis?.axis) return undefined
27
+ return axis.path ? atPath(state, axis.path) : state[axis.axis]
28
+ }