@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
@@ -0,0 +1,180 @@
1
+ import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'
2
+
3
+ import { ScalarObject, getRepository } from '@things-factory/shell'
4
+
5
+ import { TwinInstance } from '../twin-instance/twin-instance.js'
6
+
7
+ const { TWIN_RELATIONS, axisInfo } = require('@operato/twin-kernel')
8
+
9
+ /**
10
+ * 재귀 전개 — **자기 축으로 돌아오는 선언을 따라간다.**
11
+ *
12
+ * ── 왜 자재가 자재를 품지 않는가 ───────────────────────────────────────────────
13
+ * 다단 BOM 을 "자재 안에 자재" 로 적으면 같은 부품이 여러 완제품에 들어갈 때마다 복제되고,
14
+ * 수량이 어디 붙는지도 애매해진다. 표준(ISA-95)이 택한 모양은 **자재와 레시피가 번갈아 나오는
15
+ * 그래프**다: 완제품 ←산출— 레시피 —투입→ 부품들, 그리고 그 부품이 또 어느 레시피의 산출이면
16
+ * 거기서 다시 내려간다. 우리 선언(`twin.rel.input`·`twin.rel.output`)이 이미 그 두 홉이다.
17
+ *
18
+ * ── 패턴을 코드가 알지 않는다 ──────────────────────────────────────────────────
19
+ * "BOM 은 산출→투입" 이라고 적어 두면 그 순간 이 파일이 도메인을 알게 된다(ADR-0031 위반).
20
+ * 대신 **선언 그래프에서 자기 축으로 돌아오는 두 홉 순환을 찾는다.** 자재만이 아니라 앞으로
21
+ * 같은 모양을 갖는 어떤 축이든 그대로 펼쳐진다.
22
+ *
23
+ * ── 조용히 고르지 않는다 ───────────────────────────────────────────────────────
24
+ * 순환이 여럿이면 **아무거나 집지 않는다.** 후보를 돌려주고 부르는 쪽이 고르게 한다 — 한쪽을
25
+ * 집으면 역전개(누가 나를 쓰나)를 전개(내가 무엇으로 되나)로 보여 주고도 아무도 모른다.
26
+ */
27
+
28
+ /** 자기 축으로 돌아오는 두 홉 — `axis ←down(via)— through —up(via)→ axis`. */
29
+ interface WalkPattern {
30
+ /** 중간에 거치는 축(레시피 등). */
31
+ through: string
32
+ /** 나로 들어오는 홉 — "나를 이것의 산출로 삼는" 쪽. */
33
+ inVia: string
34
+ /** 거기서 다시 나가는 홉 — "그것이 투입하는" 쪽. */
35
+ outVia: string
36
+ inField: string
37
+ outField: string
38
+ }
39
+
40
+ /** 선언 그래프에서 이 축으로 돌아오는 순환들을 찾는다. */
41
+ function patternsFor(axis: string): WalkPattern[] {
42
+ const toMe = TWIN_RELATIONS.filter((r: any) => r.target?.kind === 'axis' && r.target.axis === axis)
43
+ const out: WalkPattern[] = []
44
+ for (const a of toMe)
45
+ for (const b of toMe)
46
+ if (a !== b && a.from === b.from)
47
+ out.push({ through: a.from, inVia: a.via, outVia: b.via, inField: a.field, outField: b.field })
48
+ return out
49
+ }
50
+
51
+ const at = (o: any, path: string) => path.split('.').reduce((a: any, k: string) => (a == null ? undefined : a[k]), o)
52
+ const idOf = (o: any): string | null => (typeof o === 'string' ? o : o?.id ?? o?.key ?? o?.gtin ?? null)
53
+
54
+ /** `inputs[].material` — 배열을 지나는 선언 경로. 값과 **그 값을 담은 객체**를 함께 돌려준다(수량이 거기 있다). */
55
+ function entriesAt(obj: any, field: string): { value: any; holder: any }[] {
56
+ let cur: { value: any; holder: any }[] = [{ value: obj, holder: obj }]
57
+ for (const seg of field.split('.')) {
58
+ const key = seg.endsWith('[]') ? seg.slice(0, -2) : seg
59
+ const next: { value: any; holder: any }[] = []
60
+ for (const c of cur) {
61
+ const v = c.value?.[key]
62
+ if (v == null) continue
63
+ if (Array.isArray(v)) for (const x of v) next.push({ value: x, holder: x })
64
+ else next.push({ value: v, holder: c.value })
65
+ }
66
+ cur = next
67
+ }
68
+ return cur
69
+ }
70
+
71
+ /** 이 홉이 실어 나르는 수량 — 선언에 있으면 쓰고, 없으면 **없다고 한다**(1 로 메우지 않는다). */
72
+ function qtyOf(holder: any): number | null {
73
+ const q = holder?.qty ?? holder?.quantity
74
+ return typeof q === 'number' ? q : null
75
+ }
76
+
77
+ @Resolver()
78
+ export class TwinModelTreeQuery {
79
+ @Query(returns => ScalarObject, {
80
+ nullable: true,
81
+ description:
82
+ 'Recursively expand an item through a declared two-hop cycle (e.g. a multi-level BOM: product ←produced-by— recipe —consumes→ parts). The pattern is derived from the kernel relation declarations, never hardcoded. Reports the pattern used, any cycles found, and whether depth or item caps truncated the walk.'
83
+ })
84
+ async twinModelTree(
85
+ @Arg('instanceId') instanceId: string,
86
+ @Arg('axis') axis: string,
87
+ @Arg('itemId') itemId: string,
88
+ @Ctx() context: ResolverContext,
89
+ @Arg('inVia', { nullable: true }) inVia?: string,
90
+ @Arg('outVia', { nullable: true }) outVia?: string,
91
+ @Arg('maxDepth', type => Int, { nullable: true }) maxDepth?: number,
92
+ @Arg('maxItems', type => Int, { nullable: true }) maxItems?: number
93
+ ): Promise<any | null> {
94
+ const domainId = context.state.domain.id
95
+ const info = axisInfo(axis)
96
+ if (!info) throw new Error(`unknown concept axis: "${axis}" — the kernel catalog does not declare it`)
97
+
98
+ const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })
99
+ if (!inst) return null
100
+
101
+ const all = patternsFor(axis)
102
+ if (!all.length) return { axis, itemId, patterns: [], items: [], edges: [], reason: 'no-recursive-relation' }
103
+
104
+ const chosen = inVia && outVia ? all.find(p => p.inVia === inVia && p.outVia === outVia) : all.length === 1 ? all[0] : undefined
105
+ if (!chosen)
106
+ /* 여럿인데 안 고르면 **아무것도 펼치지 않는다.** 한쪽을 집으면 역전개를 전개로 보여 주고도
107
+ 아무도 모른다 — 방향이 뒤집힌 BOM 은 틀린 답이 아니라 반대의 답이다. */
108
+ return { axis, itemId, patterns: all, items: [], edges: [], reason: 'ambiguous-pattern' }
109
+
110
+ const model = inst.model ?? {}
111
+ const throughInfo = axisInfo(chosen.through)
112
+ const listOf = (i: any) => {
113
+ const raw = i ? at(model, i.path) : undefined
114
+ return Array.isArray(raw) ? raw : raw ? [raw] : []
115
+ }
116
+ const throughAll = listOf(throughInfo)
117
+ const mineAll = listOf(info)
118
+ const mineIds = new Set(mineAll.map(idOf).filter(Boolean) as string[])
119
+
120
+ const depthCap = Math.min(Math.max(maxDepth ?? 6, 1), 12)
121
+ const itemCap = Math.min(Math.max(maxItems ?? 200, 1), 2000)
122
+
123
+ const items: any[] = [{ id: itemId, axis, depth: 0, qty: 1, known: mineIds.has(itemId) }]
124
+ const edges: any[] = []
125
+ const seen = new Set<string>([itemId])
126
+ const cycles: string[] = []
127
+ let depthTruncated = false
128
+ let itemTruncated = false
129
+
130
+ /* 너비 우선 — 깊이 상한에서 끊기면 **얕은 층은 온전히** 남는다(깊이 우선이면 한 가지만 깊게 파고 끊긴다). */
131
+ let frontier: { id: string; depth: number; qty: number | null }[] = [{ id: itemId, depth: 0, qty: 1 }]
132
+ while (frontier.length) {
133
+ const nextFrontier: typeof frontier = []
134
+ for (const cur of frontier) {
135
+ if (cur.depth >= depthCap) { depthTruncated = true; continue }
136
+ /* 나를 만들어 내는 중간 항목들(레시피)을 찾는다 — 선언된 홉을 따라. */
137
+ for (const t of throughAll) {
138
+ const producesMe = entriesAt(t, chosen.inField).some(e => String(idOf(e.value) ?? e.value) === cur.id)
139
+ if (!producesMe) continue
140
+ for (const e of entriesAt(t, chosen.outField)) {
141
+ const childId = String(idOf(e.value) ?? e.value)
142
+ const step = qtyOf(e.holder)
143
+ /* 수량을 모르면 **곱하지 않는다.** 1 로 메우면 "1개 필요" 라는 사실 주장이 되고,
144
+ 윗단에서 곱해진 값이 전부 거짓이 된다. */
145
+ const qty = cur.qty != null && step != null ? cur.qty * step : null
146
+ edges.push({ from: cur.id, to: childId, through: idOf(t), depth: cur.depth + 1, step, qty })
147
+
148
+ if (seen.has(childId)) {
149
+ /* 이미 지난 항목으로 돌아왔다 — 사이클이거나 공유 부품이다. 어느 쪽이든 **다시 파지
150
+ 않고 말한다.** 파고 들어가면 돌아 나오지 못한다. */
151
+ if (edges.some(x => x.to === cur.id && x.from === childId)) cycles.push(`${cur.id} ↔ ${childId}`)
152
+ continue
153
+ }
154
+ if (items.length >= itemCap) { itemTruncated = true; continue }
155
+ seen.add(childId)
156
+ items.push({ id: childId, axis, depth: cur.depth + 1, qty, known: mineIds.has(childId) })
157
+ nextFrontier.push({ id: childId, depth: cur.depth + 1, qty })
158
+ }
159
+ }
160
+ }
161
+ frontier = nextFrontier
162
+ }
163
+
164
+ return {
165
+ axis,
166
+ itemId,
167
+ /* 어떤 순환을 따라 펼쳤는지 **밝힌다** — 방향이 뒤집힌 전개는 틀린 답이 아니라 반대의 답이라
168
+ 화면만 보고는 구별되지 않는다. */
169
+ pattern: chosen,
170
+ patterns: all,
171
+ items,
172
+ edges,
173
+ /* 수량을 하나라도 모르면 아래 합계가 성립하지 않는다는 사실을 그대로 낸다. */
174
+ qtyIncomplete: edges.some(e => e.step == null),
175
+ cycles: [...new Set(cycles)],
176
+ truncated: { depth: depthTruncated, items: itemTruncated, depthCap, itemCap },
177
+ basis: 'document'
178
+ }
179
+ }
180
+ }
@@ -0,0 +1,157 @@
1
+ import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
2
+ import { ObjectType, Field, ID, Int } from 'type-graphql'
3
+
4
+ import { Domain, ScalarObject } from '@things-factory/shell'
5
+
6
+ import { TwinInstance } from '../twin-instance/twin-instance.js'
7
+
8
+ /*
9
+ * TwinOperation — 이 트윈이 반영하는 **공정 명세**. ISA-95 `OperationsSegment`.
10
+ * 이 현장에서 각 작업이 **어디서·무엇으로·얼마나 걸리나**(선언값).
11
+ *
12
+ * ── 왜 이 표가 인스펙션의 중심인가 ──────────────────────────────────────────
13
+ * 인스펙션 화면의 존재 이유는 `선언 | 관측` 두 열이고(`plans/twin-inspection.md` §2.4),
14
+ * 그 첫 열이 여기서 온다. 둘째 열은 저널에서 접힌다(`kpi-fold.workTime`).
15
+ * 실적은 보이는데 기준이 안 보이면 "절단이 5분 걸렸다" 를 보고도 빠른지 느린지 알 수 없다.
16
+ *
17
+ * 자리·설비와 같은 규율 — **원본에서 온 캐시**, `sourceRef`·`ingestedAt` 필수,
18
+ * 쓰기 경로는 인제스트 하나(`twin-location.ts` 머리말).
19
+ */
20
+ @Entity({ name: 'twin_operations' })
21
+ @Index('ix_twin_operation_0', (e: TwinOperation) => [e.domain, e.instance, e.operationKey], { unique: true })
22
+ @ObjectType({ description: 'An operation spec reflected into the twin (ISA-95 OperationsSegment): where it runs, with what, and how long the source says it takes.' })
23
+ export class TwinOperation {
24
+ @PrimaryGeneratedColumn('uuid')
25
+ @Field(type => ID)
26
+ readonly id: string
27
+
28
+ @ManyToOne(type => Domain)
29
+ @Field(type => Domain, { nullable: true, description: 'Owning tenant domain.' })
30
+ domain?: Domain
31
+
32
+ @RelationId((e: TwinOperation) => e.domain)
33
+ domainId?: string
34
+
35
+ @ManyToOne(type => TwinInstance, { onDelete: 'CASCADE' })
36
+ @Field(type => TwinInstance, { nullable: true, description: 'Owning twin instance.' })
37
+ instance?: TwinInstance
38
+
39
+ @RelationId((e: TwinOperation) => e.instance)
40
+ instanceRowId?: string
41
+
42
+ /** 커널 계약의 `OperationDef.key` 그대로. 라우트의 단계가 이 키로 가리킨다. */
43
+ @Column()
44
+ @Field({ description: 'Operation key as declared by the source (kernel OperationDef.key). Route steps reference this.' })
45
+ operationKey: string
46
+
47
+ /**
48
+ * **사람이 부르는 이름** — 프레임워크 엔티티 규약의 `name`/`description`.
49
+ *
50
+ * ── 왜 이것이 빠져 있었고, 무엇이 망가졌나 ────────────────────────────────────
51
+ * 투영이 마스터의 `name` 을 버리고 있었다. 그래서 행에는 id 만 남고, 화면은 이름을 만들 재료가
52
+ * 없어 `cut-station` 을 그대로 찍었다 — 그 결함이 목록·표·관계 그림마다 따로 나타나서 표면을
53
+ * 하나씩 고치게 만들었다. 표면의 문제가 아니라 **행에 이름이 없던 문제**다.
54
+ *
55
+ * 원본이 이름을 말하지 않으면 비운다(id 를 이름으로 베끼지 않는다 — 그러면 "이름이 있다" 는
56
+ * 거짓이 되고, 화면이 이름 없음을 알아볼 수 없게 된다).
57
+ */
58
+ @Column({ nullable: true })
59
+ @Field({ nullable: true, description: 'Human name as declared by the source. Empty when the source does not name it (never copied from the id).' })
60
+ name?: string
61
+
62
+ @Column({ nullable: true })
63
+ @Field({ nullable: true, description: 'Longer description when the source provides one.' })
64
+ description?: string
65
+
66
+
67
+ @Column({ nullable: true })
68
+ @Field({ nullable: true, description: "Intent: 'transport' | 'process' | 'dwell'." })
69
+ intent?: string
70
+
71
+ /*
72
+ * 어디서·무엇으로 — 자리·설비의 **항목이 아니라 타입**을 가리킨다("절단은 cut-station 에서 수행된다").
73
+ * 그래서 FK 가 아니다. 화면은 이것을 "그 타입의 자리·설비 목록" 으로 푼다(관계 선언의 `type` 대상).
74
+ */
75
+ @Column({ nullable: true })
76
+ @Field({ nullable: true, description: 'Location type this operation runs at (a catalog type key, not an item reference).' })
77
+ locationType?: string
78
+
79
+ @Column({ nullable: true })
80
+ @Field({ nullable: true, description: 'Resource type that performs it (a catalog type key, not an item reference).' })
81
+ resourceType?: string
82
+
83
+ /**
84
+ * 소요시간 — **ISO 8601 기간 문자열이 정본**(`PT1H10M`).
85
+ *
86
+ * 분으로 저장하면 `PT1H10M30S` 의 초가 사라지고 그 손실은 조용하다. 사람이 읽을 분은
87
+ * 화면이 곁들여 보여 주고, 못 읽는 값은 `—` 로 낸다(0 으로 채우면 "즉시 끝나는 공정" 이라는 거짓).
88
+ */
89
+ @Column({ nullable: true })
90
+ @Field({ nullable: true, description: 'Declared duration as an ISO 8601 period (e.g. PT1H10M). The string is canonical — storing minutes would silently drop seconds.' })
91
+ duration?: string
92
+
93
+ /**
94
+ * 변동성·파라미터(수율·셋업)는 형이 도메인마다 달라 **원형 그대로** 싣는다.
95
+ * 여기를 컬럼으로 펴려면 커널이 그 형을 먼저 고정해야 한다 — 아직 아니다.
96
+ */
97
+ @Column({ type: 'simple-json', nullable: true })
98
+ @Field(type => ScalarObject, { nullable: true, description: 'Declared variability (distribution/min/max/mode) as given by the source.' })
99
+ variability?: any
100
+
101
+ @Column({ type: 'simple-json', nullable: true })
102
+ @Field(type => ScalarObject, { nullable: true, description: 'Declared parameters (yield, setup duration…) as given by the source.' })
103
+ parameters?: any
104
+
105
+ /**
106
+ * **네 자원 명세 — 선언한 것만 제약이 된다.**
107
+ *
108
+ * ISA-95 는 한 공정이 무엇을 필요로 하는지를 넷으로 말한다: 설비(`resourceType`)·인원·자산·자재.
109
+ * 커널도 그 넷을 그대로 본다 — 선언이 있으면 잡아야 작업이 시작되고, 없으면 그 자원을 보지 않는다.
110
+ *
111
+ * 이 셋이 투영에서 **떨어져 있었다.** 그래서 조회 화면의 "원본이 말한 값" 열에 넷 중 하나만
112
+ * 나왔고, 사람이 필요한 공정을 보고도 **사람이 필요 없다고 결론 내게** 되어 있었다. 원본이 말한
113
+ * 것을 화면이 말하지 않으면 그 화면은 원본을 반영하는 것이 아니다.
114
+ *
115
+ * 모양은 커널 계약 그대로 싣는다 — 인제스트 경계에서 모양을 바꾸면 그 자리에서 방언이 생긴다.
116
+ */
117
+ @Column({ type: 'simple-json', nullable: true })
118
+ @Field(type => ScalarObject, { nullable: true, description: 'Personnel this operation requires (ISA-95 PersonnelSpecification): class + quantity. Absent means the operation runs without people.' })
119
+ personnelSpecification?: any
120
+
121
+ @Column({ type: 'simple-json', nullable: true })
122
+ @Field(type => ScalarObject, { nullable: true, description: 'Physical assets this operation requires (ISA-95 PhysicalAssetSpecification). Absent means no asset constraint — an empty pallet is never the reason a job waits.' })
123
+ physicalAssetSpecification?: any
124
+
125
+ @Column({ type: 'simple-json', nullable: true })
126
+ @Field(type => ScalarObject, { nullable: true, description: 'Materials this operation consumes or produces (ISA-95 MaterialSpecification). Absent means material shortage never shows up in the forecast for this step.' })
127
+ materialSpecification?: any
128
+
129
+ /** 라우트 안에서의 순서 — 라우트가 이 공정을 몇 번째로 부르나(라우트 표가 서면 그리로 옮긴다). */
130
+ @Column({ type: 'int', nullable: true })
131
+ @Field(type => Int, { nullable: true, description: 'Sequence within its route, when the source declares one.' })
132
+ seq?: number
133
+
134
+ /* ── 캐시라는 사실을 구조로 (ADR-0032-D) ── */
135
+ @Column()
136
+ @Field({ description: 'Which reference (source system) this row came from. Required.' })
137
+ sourceRef: string
138
+
139
+ /*
140
+ * **타입 이름을 적지 않는다** (2026-08-19 Postgres 부팅 실패로 고침).
141
+ *
142
+ * `datetime` 은 sqlite·MySQL 의 이름이고 Postgres 는 `timestamp` 만 안다 — 거꾸로 `timestamp` 는
143
+ * sqlite 가 모른다. **다섯 드라이버에 다 통하는 이름이 없다.** 그래서 이름 고르는 일을 TypeORM 에
144
+ * 맡긴다(`emitDecoratorMetadata` 가 켜져 있어 `Date` 에서 드라이버별 타입을 고른다).
145
+ */
146
+ @Column()
147
+ @Field({ description: 'When this row was projected from the source.' })
148
+ ingestedAt: Date
149
+
150
+ @CreateDateColumn()
151
+ @Field({ nullable: true })
152
+ createdAt?: Date
153
+
154
+ @UpdateDateColumn()
155
+ @Field({ nullable: true })
156
+ updatedAt?: Date
157
+ }
@@ -0,0 +1,262 @@
1
+ /*
2
+ * 이미 있는 트윈을 다른 현장으로 옮긴다 — **판정만**. 순수.
3
+ *
4
+ * ── 왜 있나 (2026-08-13) ────────────────────────────────────────────────────
5
+ * 트윈↔현장 연결은 **만들 때만** 정할 수 있었다(템플릿 위자드·import 의 `spaceId`). 잘못 넣었거나,
6
+ * 나중에 "이 창고는 사실 저 공장과 같은 부지다" 를 알게 되면 지우고 다시 만드는 것이 유일한 길이었다 —
7
+ * 다시 만들면 그 트윈의 저널(일어난 일)이 사라진다. 고칠 수 있는 실수를 데이터 손실로 갚게 하는 설계다.
8
+ *
9
+ * ── 옮기는 것이 무엇을 옮기나 (엔티티 키를 보고 정했다) ──────────────────────
10
+ * 무엇이 트윈에 매여 있고 무엇이 공간에 매여 있는지가 답을 정한다:
11
+ *
12
+ * · `TwinEvent` `(domain, instanceId, …)` → **따라온다.** 저널을 다시 쓰지 않는다.
13
+ * · `TwinLocation/Equipment/Operation` `(domain, instance, …)` → **따라온다.**
14
+ * · `TwinArea` `(domain, space, areaId)` → **남는다.** 구역은 공간의 것이다.
15
+ * · `TwinSpaceRepresentation`(지도·이미지·보드) → **남는다.** 표현도 공간의 것이다.
16
+ *
17
+ * 그래서 옮기기는 이력 이전이 아니라 **소속 갱신**이다(`instance.spaceId` + `model.spaceId`).
18
+ * 대신 공간에 매인 것들과의 연결이 끊길 수 있고, 그것을 **조용히 넘기지 않는 것**이 이 모듈의 일이다.
19
+ *
20
+ * ── 거절과 경고를 가른다 ────────────────────────────────────────────────────
21
+ * 거절: 되돌릴 수 없거나 뜻이 성립하지 않는 것(없는 공간·같은 공간·벤치 사본).
22
+ * 경고: 사용자가 알고 나면 정당할 수 있는 것(구역 소속이 끊긴다·시간대가 다르다·옛 표현이 남는다).
23
+ * 경고를 거절로 올리면 정당한 이전을 막고, 거절을 경고로 낮추면 조용한 사고가 된다.
24
+ */
25
+
26
+ /** 옮길 수 없는 이유 — 언어 중립 코드(발견 오류·인제스트 거절과 같은 규약). */
27
+ export type MoveSpaceRefusalCode = 'twin-not-found' | 'space-not-found' | 'same-space' | 'bench-copy'
28
+
29
+ /** 옮겨도 되지만 알아야 하는 것 — 같은 규약. */
30
+ export type MoveSpaceWarningCode = 'areas-left-behind' | 'timezone-differs' | 'representations-left-behind' | 'running'
31
+
32
+ export interface MoveSpaceNote<C extends string> {
33
+ code: C
34
+ params?: Record<string, string | number>
35
+ /** 영어 canonical — 로그에 남고, 번역이 없으면 그대로 보인다. */
36
+ message: string
37
+ }
38
+
39
+ export type MoveSpaceRefusal = MoveSpaceNote<MoveSpaceRefusalCode>
40
+ export type MoveSpaceWarning = MoveSpaceNote<MoveSpaceWarningCode>
41
+
42
+ export interface MoveSpaceInput {
43
+ /** 옮길 트윈. 없으면 거절한다. */
44
+ instanceId?: string
45
+ /** 지금 속한 공간(없을 수 있다 — 옛 트윈). */
46
+ fromSpaceId?: string
47
+ /** 옮겨 갈 공간. */
48
+ toSpaceId: string
49
+ /** 그 공간이 이 테넌트에 실제로 있나(호출측이 조회해 넘긴다). */
50
+ toSpaceExists: boolean
51
+ /** 운영 트윈인가 벤치 사본인가 — 사본은 원본의 공간을 따라야 하므로 옮기지 않는다. */
52
+ purpose?: string
53
+ /** 지금 실행 중인가 — 시간대 변화는 재기동 뒤에 커널에 실린다. */
54
+ running?: boolean
55
+ /** 두 공간의 시간대(IANA). 다르면 앞으로의 시각 해석이 달라진다. */
56
+ fromTimezone?: string
57
+ toTimezone?: string
58
+ /** 이 트윈의 로케이션들이 소속으로 가리키는 구역 id 들(중복 포함 가능). */
59
+ areaRefs?: readonly (string | undefined | null)[]
60
+ /** 옮겨 갈 공간에 있는 구역 id 들. */
61
+ toAreaIds?: readonly string[]
62
+ /** 옛 공간의 표현 수 — 그 표현의 폴리곤이 이 트윈을 가리키고 있었을 수 있다. */
63
+ fromRepresentationCount?: number
64
+ }
65
+
66
+ export interface MoveSpaceJudgement {
67
+ /** 있으면 옮기지 않는다. */
68
+ refusal?: MoveSpaceRefusal
69
+ /** 옮기되 함께 알린다. 비어 있으면 잃는 것이 없다. */
70
+ warnings: MoveSpaceWarning[]
71
+ }
72
+
73
+ /** 옮겨도 되나 — 되면 무엇을 알려야 하나. */
74
+ export function judgeMoveTwinSpace(input: MoveSpaceInput): MoveSpaceJudgement {
75
+ const to = (input.toSpaceId ?? '').trim()
76
+ const from = (input.fromSpaceId ?? '').trim()
77
+
78
+ if (!input.instanceId) {
79
+ return {
80
+ refusal: { code: 'twin-not-found', message: 'no such twin in this tenant' },
81
+ warnings: []
82
+ }
83
+ }
84
+ if (!to || !input.toSpaceExists) {
85
+ return {
86
+ refusal: {
87
+ code: 'space-not-found',
88
+ params: { spaceId: to },
89
+ message: `space "${to}" not found — cannot move a twin into a space that does not exist`
90
+ },
91
+ warnings: []
92
+ }
93
+ }
94
+ if (from && from === to) {
95
+ return {
96
+ refusal: {
97
+ code: 'same-space',
98
+ params: { spaceId: to },
99
+ message: `twin "${input.instanceId}" is already in space "${to}"`
100
+ },
101
+ warnings: []
102
+ }
103
+ }
104
+ /* 벤치 사본은 "이 공장이 이만큼 커지면" 을 재는 사본이라 원본의 현장에 매여 있다. 옮기면 그 측정이
105
+ 무엇의 측정인지 말할 수 없게 된다 — 사본을 옮기는 대신 원본을 옮기고 사본을 다시 뜬다. */
106
+ if (input.purpose === 'bench') {
107
+ return {
108
+ refusal: {
109
+ code: 'bench-copy',
110
+ params: { instanceId: input.instanceId },
111
+ message: `twin "${input.instanceId}" is a bench copy — move its source twin instead`
112
+ },
113
+ warnings: []
114
+ }
115
+ }
116
+
117
+ const warnings: MoveSpaceWarning[] = []
118
+
119
+ /* 로케이션의 구역 소속 — 구역은 공간의 것이라 새 공간에 같은 id 가 없으면 소속이 끊긴다.
120
+ 막지는 않는다(새 공간에서 구역을 다시 그리는 것이 정상 경로다). 다만 몇 개가 끊기는지 말한다. */
121
+ const refs = [...new Set((input.areaRefs ?? []).filter((x): x is string => !!x && !!x.trim()))]
122
+ const have = new Set(input.toAreaIds ?? [])
123
+ const orphaned = refs.filter(r => !have.has(r)).sort()
124
+ if (orphaned.length) {
125
+ warnings.push({
126
+ code: 'areas-left-behind',
127
+ params: { count: orphaned.length, areas: orphaned.slice(0, 5).join(', ') },
128
+ message: `${orphaned.length} area reference(s) do not exist in the target space — those locations lose their grouping: ${orphaned
129
+ .slice(0, 5)
130
+ .join(', ')}`
131
+ })
132
+ }
133
+
134
+ /* 시간대 — 공간이 정하고 커널이 분 오프셋으로 받는다. 다르면 같은 저널이 다른 시각으로 읽힌다. */
135
+ const tzFrom = (input.fromTimezone ?? '').trim()
136
+ const tzTo = (input.toTimezone ?? '').trim()
137
+ if (tzFrom !== tzTo) {
138
+ warnings.push({
139
+ code: 'timezone-differs',
140
+ params: { from: tzFrom || '—', to: tzTo || '—' },
141
+ message: `time zone changes from "${tzFrom || 'unset'}" to "${tzTo || 'unset'}" — shift boundaries and day buckets will read differently`
142
+ })
143
+ }
144
+
145
+ /* 옛 공간의 표현 — 폴리곤 바인딩이 이 트윈의 로케이션·그룹을 가리키고 있었을 수 있다. 표현은
146
+ 공간에 남으므로, 그 화면에서 이 트윈이 사라진다. 몇 개가 남는지 말해 두면 사람이 찾아갈 수 있다. */
147
+ if ((input.fromRepresentationCount ?? 0) > 0) {
148
+ warnings.push({
149
+ code: 'representations-left-behind',
150
+ params: { count: input.fromRepresentationCount as number, spaceId: from || '—' },
151
+ message: `${input.fromRepresentationCount} representation(s) stay with space "${from}" — bindings that pointed at this twin will no longer resolve there`
152
+ })
153
+ }
154
+
155
+ /* 실행 중 — 소속은 즉시 바뀌지만 시간대는 커널을 세울 때 실린다. 시간대가 같으면 말할 것이 없다. */
156
+ if (input.running && tzFrom !== tzTo) {
157
+ warnings.push({
158
+ code: 'running',
159
+ params: { instanceId: input.instanceId },
160
+ message: `twin "${input.instanceId}" is running — the new time zone applies after it is restarted`
161
+ })
162
+ }
163
+
164
+ return { warnings }
165
+ }
166
+
167
+ /*
168
+ * ── 현장에서 떼기 (2026-08-13) ──────────────────────────────────────────────
169
+ *
170
+ * 더하기·옮기기만 있으면 잘못 넣은 것을 되돌릴 수 없다. 떼기는 "어느 현장에도 속하지 않음" 으로
171
+ * 되돌리는 일이고, 데이터 모델이 그 상태를 이미 허용한다(`TwinInstance.spaceId` 는 선택 컬럼).
172
+ *
173
+ * 떼면 무엇이 달라지나 — 옮기기와 같은 규칙이지만 **갈 곳이 없다**는 점이 다르다:
174
+ * · 저널·로케이션·설비는 그대로 트윈에 남는다(잃는 것 없음).
175
+ * · 구역·표현은 옛 현장에 남는다 → 그 화면에서 이 트윈이 사라진다.
176
+ * · **볼 무대가 없어진다** — 현장 현황(지도·보드)은 현장의 표현으로 그리므로, 뗀 트윈은 목록과
177
+ * 조회 화면에서만 보인다. 이것을 말하지 않으면 사용자는 트윈이 고장 났다고 읽는다.
178
+ * · 시간 기준(`utcOffsetMinutes`)은 **옛 현장의 것으로 남는다.** 지우면 조용히 UTC 로 읽히므로
179
+ * 그쪽이 더 위험하다 — 남기고 그 사실을 말한다.
180
+ */
181
+
182
+ /** 뗄 수 없는 이유 — 언어 중립 코드. */
183
+ export type DetachSpaceRefusalCode = 'twin-not-found' | 'no-space' | 'bench-copy'
184
+
185
+ /** 떼도 되지만 알아야 하는 것. */
186
+ export type DetachSpaceWarningCode = 'no-stage' | 'representations-left-behind' | 'timebase-kept'
187
+
188
+ export type DetachSpaceRefusal = MoveSpaceNote<DetachSpaceRefusalCode>
189
+ export type DetachSpaceWarning = MoveSpaceNote<DetachSpaceWarningCode>
190
+
191
+ export interface DetachSpaceInput {
192
+ instanceId?: string
193
+ /** 지금 속한 공간 — 없으면 뗄 것이 없다. */
194
+ fromSpaceId?: string
195
+ purpose?: string
196
+ /** 옛 공간의 표현 수. */
197
+ fromRepresentationCount?: number
198
+ /** 옛 공간의 시간대 — 있으면 그 기준이 model 에 남는다. */
199
+ fromTimezone?: string
200
+ }
201
+
202
+ export interface DetachSpaceJudgement {
203
+ refusal?: DetachSpaceRefusal
204
+ warnings: DetachSpaceWarning[]
205
+ }
206
+
207
+ /** 떼도 되나 — 되면 무엇을 알려야 하나. */
208
+ export function judgeDetachTwinSpace(input: DetachSpaceInput): DetachSpaceJudgement {
209
+ const from = (input.fromSpaceId ?? '').trim()
210
+
211
+ if (!input.instanceId) {
212
+ return { refusal: { code: 'twin-not-found', message: 'no such twin in this tenant' }, warnings: [] }
213
+ }
214
+ if (!from) {
215
+ return {
216
+ refusal: {
217
+ code: 'no-space',
218
+ params: { instanceId: input.instanceId },
219
+ message: `twin "${input.instanceId}" is not on any site — nothing to detach`
220
+ },
221
+ warnings: []
222
+ }
223
+ }
224
+ /* 벤치 사본은 원본의 현장을 따라야 측정이 무엇의 측정인지 말할 수 있다(옮기기와 같은 이유). */
225
+ if (input.purpose === 'bench') {
226
+ return {
227
+ refusal: {
228
+ code: 'bench-copy',
229
+ params: { instanceId: input.instanceId },
230
+ message: `twin "${input.instanceId}" is a bench copy — it must stay on the site it measures`
231
+ },
232
+ warnings: []
233
+ }
234
+ }
235
+
236
+ const warnings: DetachSpaceWarning[] = [
237
+ {
238
+ code: 'no-stage',
239
+ params: { instanceId: input.instanceId },
240
+ message: `twin "${input.instanceId}" will have no site to be drawn on — it stays visible in lists and inspection only`
241
+ }
242
+ ]
243
+
244
+ if ((input.fromRepresentationCount ?? 0) > 0) {
245
+ warnings.push({
246
+ code: 'representations-left-behind',
247
+ params: { count: input.fromRepresentationCount as number, spaceId: from },
248
+ message: `${input.fromRepresentationCount} representation(s) stay with space "${from}" — bindings that pointed at this twin will no longer resolve there`
249
+ })
250
+ }
251
+
252
+ const tz = (input.fromTimezone ?? '').trim()
253
+ if (tz) {
254
+ warnings.push({
255
+ code: 'timebase-kept',
256
+ params: { timezone: tz },
257
+ message: `the time base stays as "${tz}" (the site it came from) — clearing it would silently read times as UTC`
258
+ })
259
+ }
260
+
261
+ return { warnings }
262
+ }