@things-factory/headless-twin 10.0.8 → 10.0.10

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 (381) hide show
  1. package/dist-server/engine/attention-digest.d.ts +52 -0
  2. package/dist-server/engine/attention-digest.js +76 -0
  3. package/dist-server/engine/attention-digest.js.map +1 -0
  4. package/dist-server/engine/canonical-ingest.d.ts +2 -2
  5. package/dist-server/engine/canonical-ingest.js +24 -2
  6. package/dist-server/engine/canonical-ingest.js.map +1 -1
  7. package/dist-server/engine/command-routing.d.ts +33 -0
  8. package/dist-server/engine/command-routing.js +53 -0
  9. package/dist-server/engine/command-routing.js.map +1 -0
  10. package/dist-server/engine/energy-topology.d.ts +86 -0
  11. package/dist-server/engine/energy-topology.js +144 -0
  12. package/dist-server/engine/energy-topology.js.map +1 -0
  13. package/dist-server/engine/index.d.ts +11 -0
  14. package/dist-server/engine/index.js +17 -0
  15. package/dist-server/engine/index.js.map +1 -1
  16. package/dist-server/engine/kpi-baseline.d.ts +78 -0
  17. package/dist-server/engine/kpi-baseline.js +123 -0
  18. package/dist-server/engine/kpi-baseline.js.map +1 -0
  19. package/dist-server/engine/kpi-fold.d.ts +137 -1
  20. package/dist-server/engine/kpi-fold.js +225 -0
  21. package/dist-server/engine/kpi-fold.js.map +1 -1
  22. package/dist-server/engine/kpi-query.d.ts +32 -1
  23. package/dist-server/engine/kpi-query.js +262 -20
  24. package/dist-server/engine/kpi-query.js.map +1 -1
  25. package/dist-server/engine/kpi-target.d.ts +17 -0
  26. package/dist-server/engine/kpi-target.js +24 -2
  27. package/dist-server/engine/kpi-target.js.map +1 -1
  28. package/dist-server/engine/live-attentions.d.ts +1 -0
  29. package/dist-server/engine/live-attentions.js +7 -1
  30. package/dist-server/engine/live-attentions.js.map +1 -1
  31. package/dist-server/engine/live-feed-registry.d.ts +25 -0
  32. package/dist-server/engine/live-feed-registry.js +51 -0
  33. package/dist-server/engine/live-feed-registry.js.map +1 -0
  34. package/dist-server/engine/load-meter.d.ts +181 -0
  35. package/dist-server/engine/load-meter.js +267 -0
  36. package/dist-server/engine/load-meter.js.map +1 -0
  37. package/dist-server/engine/local-declarations.d.ts +262 -0
  38. package/dist-server/engine/local-declarations.js +528 -0
  39. package/dist-server/engine/local-declarations.js.map +1 -0
  40. package/dist-server/engine/model-basis.d.ts +39 -6
  41. package/dist-server/engine/model-basis.js +69 -9
  42. package/dist-server/engine/model-basis.js.map +1 -1
  43. package/dist-server/engine/model-vocabulary.d.ts +17 -0
  44. package/dist-server/engine/model-vocabulary.js +81 -0
  45. package/dist-server/engine/model-vocabulary.js.map +1 -0
  46. package/dist-server/engine/oee-accumulator.d.ts +28 -0
  47. package/dist-server/engine/oee-accumulator.js +26 -1
  48. package/dist-server/engine/oee-accumulator.js.map +1 -1
  49. package/dist-server/engine/operation-basis.d.ts +32 -0
  50. package/dist-server/engine/operation-basis.js +87 -0
  51. package/dist-server/engine/operation-basis.js.map +1 -0
  52. package/dist-server/engine/property-effects.d.ts +30 -0
  53. package/dist-server/engine/property-effects.js +192 -0
  54. package/dist-server/engine/property-effects.js.map +1 -0
  55. package/dist-server/engine/runtime-key.d.ts +15 -0
  56. package/dist-server/engine/runtime-key.js +64 -0
  57. package/dist-server/engine/runtime-key.js.map +1 -0
  58. package/dist-server/engine/spec-coverage.js +3 -3
  59. package/dist-server/engine/spec-coverage.js.map +1 -1
  60. package/dist-server/engine/state-axes.d.ts +21 -0
  61. package/dist-server/engine/state-axes.js +59 -0
  62. package/dist-server/engine/state-axes.js.map +1 -0
  63. package/dist-server/engine/structure-diff.js +1 -1
  64. package/dist-server/engine/structure-diff.js.map +1 -1
  65. package/dist-server/engine/travel-estimator.d.ts +9 -2
  66. package/dist-server/engine/travel-estimator.js +11 -6
  67. package/dist-server/engine/travel-estimator.js.map +1 -1
  68. package/dist-server/engine/twin-engine.d.ts +418 -57
  69. package/dist-server/engine/twin-engine.js +1599 -306
  70. package/dist-server/engine/twin-engine.js.map +1 -1
  71. package/dist-server/engine/warm-start.d.ts +93 -13
  72. package/dist-server/engine/warm-start.js +114 -14
  73. package/dist-server/engine/warm-start.js.map +1 -1
  74. package/dist-server/index.d.ts +1 -0
  75. package/dist-server/index.js +17 -11
  76. package/dist-server/index.js.map +1 -1
  77. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.d.ts +5 -0
  78. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +48 -0
  79. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -0
  80. package/dist-server/migrations/index.d.ts +2 -0
  81. package/dist-server/migrations/index.js +10 -0
  82. package/dist-server/migrations/index.js.map +1 -0
  83. package/dist-server/service/index.d.ts +3 -2
  84. package/dist-server/service/index.js +25 -17
  85. package/dist-server/service/index.js.map +1 -1
  86. package/dist-server/service/reference/discovery-result.d.ts +34 -0
  87. package/dist-server/service/reference/discovery-result.js +84 -0
  88. package/dist-server/service/reference/discovery-result.js.map +1 -0
  89. package/dist-server/service/reference/ingest-space.d.ts +53 -0
  90. package/dist-server/service/reference/ingest-space.js +79 -0
  91. package/dist-server/service/reference/ingest-space.js.map +1 -0
  92. package/dist-server/service/reference/knob-defaults.d.ts +20 -0
  93. package/dist-server/service/reference/knob-defaults.js +59 -0
  94. package/dist-server/service/reference/knob-defaults.js.map +1 -0
  95. package/dist-server/service/reference/reference-live.d.ts +23 -0
  96. package/dist-server/service/reference/reference-live.js +123 -6
  97. package/dist-server/service/reference/reference-live.js.map +1 -1
  98. package/dist-server/service/reference/reference-master.d.ts +227 -11
  99. package/dist-server/service/reference/reference-master.js +206 -25
  100. package/dist-server/service/reference/reference-master.js.map +1 -1
  101. package/dist-server/service/reference/reference-resolver.d.ts +33 -3
  102. package/dist-server/service/reference/reference-resolver.js +290 -23
  103. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  104. package/dist-server/service/reference/template-registry.d.ts +19 -3
  105. package/dist-server/service/reference/template-registry.js.map +1 -1
  106. package/dist-server/service/twin-attention/twin-attention-query.d.ts +8 -1
  107. package/dist-server/service/twin-attention/twin-attention-query.js +39 -8
  108. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  109. package/dist-server/service/twin-audit/command-audit.d.ts +37 -0
  110. package/dist-server/service/twin-audit/command-audit.js +53 -0
  111. package/dist-server/service/twin-audit/command-audit.js.map +1 -0
  112. package/dist-server/service/twin-audit/index.d.ts +4 -0
  113. package/dist-server/service/twin-audit/index.js +8 -0
  114. package/dist-server/service/twin-audit/index.js.map +1 -0
  115. package/dist-server/service/twin-audit/twin-audit-event.d.ts +24 -0
  116. package/dist-server/service/twin-audit/twin-audit-event.js +125 -0
  117. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -0
  118. package/dist-server/service/twin-audit/twin-audit-query.d.ts +4 -0
  119. package/dist-server/service/twin-audit/twin-audit-query.js +76 -0
  120. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -0
  121. package/dist-server/service/twin-control/twin-control-mutation.d.ts +2 -0
  122. package/dist-server/service/twin-control/twin-control-mutation.js +50 -11
  123. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  124. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  125. package/dist-server/service/twin-event/twin-event-keys.js +1 -1
  126. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  127. package/dist-server/service/twin-event/twin-event.d.ts +14 -5
  128. package/dist-server/service/twin-event/twin-event.js +46 -14
  129. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  130. package/dist-server/service/twin-forecast/forecast-metrics.d.ts +13 -0
  131. package/dist-server/service/twin-forecast/forecast-metrics.js +57 -0
  132. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -0
  133. package/dist-server/service/twin-forecast/forecast-tuning.d.ts +12 -0
  134. package/dist-server/service/twin-forecast/forecast-tuning.js +44 -0
  135. package/dist-server/service/twin-forecast/forecast-tuning.js.map +1 -0
  136. package/dist-server/service/twin-forecast/gap-analytics.d.ts +28 -2
  137. package/dist-server/service/twin-forecast/gap-analytics.js +43 -17
  138. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  139. package/dist-server/service/twin-forecast/twin-forecast-query.js +219 -39
  140. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  141. package/dist-server/service/twin-instance/twin-instance.d.ts +38 -5
  142. package/dist-server/service/twin-instance/twin-instance.js +66 -18
  143. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  144. package/dist-server/service/twin-journal/twin-journal-query.d.ts +11 -4
  145. package/dist-server/service/twin-journal/twin-journal-query.js +95 -15
  146. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  147. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +11 -5
  148. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +46 -27
  149. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  150. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  151. package/dist-server/service/twin-metrics/twin-metrics-query.js +63 -1
  152. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  153. package/dist-server/service/twin-model/epcis-coverage.d.ts +14 -0
  154. package/dist-server/service/twin-model/epcis-coverage.js +174 -0
  155. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -0
  156. package/dist-server/service/twin-model/iec61850-coverage.d.ts +14 -0
  157. package/dist-server/service/twin-model/iec61850-coverage.js +98 -0
  158. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -0
  159. package/dist-server/service/twin-model/index.d.ts +13 -0
  160. package/dist-server/service/twin-model/index.js +29 -0
  161. package/dist-server/service/twin-model/index.js.map +1 -0
  162. package/dist-server/service/twin-model/isa95-coverage.d.ts +48 -0
  163. package/dist-server/service/twin-model/isa95-coverage.js +155 -0
  164. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -0
  165. package/dist-server/service/twin-model/project-structure.d.ts +16 -0
  166. package/dist-server/service/twin-model/project-structure.js +173 -0
  167. package/dist-server/service/twin-model/project-structure.js.map +1 -0
  168. package/dist-server/service/twin-model/standard-coverage.d.ts +9 -0
  169. package/dist-server/service/twin-model/standard-coverage.js +37 -0
  170. package/dist-server/service/twin-model/standard-coverage.js.map +1 -0
  171. package/dist-server/service/twin-model/twin-equipment.d.ts +64 -0
  172. package/dist-server/service/twin-model/twin-equipment.js +134 -0
  173. package/dist-server/service/twin-model/twin-equipment.js.map +1 -0
  174. package/dist-server/service/twin-model/twin-lineage-query.d.ts +3 -0
  175. package/dist-server/service/twin-model/twin-lineage-query.js +206 -0
  176. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -0
  177. package/dist-server/service/twin-model/twin-location.d.ts +58 -0
  178. package/dist-server/service/twin-model/twin-location.js +130 -0
  179. package/dist-server/service/twin-model/twin-location.js.map +1 -0
  180. package/dist-server/service/twin-model/twin-model-item-query.d.ts +141 -0
  181. package/dist-server/service/twin-model/twin-model-item-query.js +852 -0
  182. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -0
  183. package/dist-server/service/twin-model/twin-model-mutation.d.ts +57 -0
  184. package/dist-server/service/twin-model/twin-model-mutation.js +490 -0
  185. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -0
  186. package/dist-server/service/twin-model/twin-model-query.d.ts +43 -0
  187. package/dist-server/service/twin-model/twin-model-query.js +551 -0
  188. package/dist-server/service/twin-model/twin-model-query.js.map +1 -0
  189. package/dist-server/service/twin-model/twin-model-tree-query.d.ts +3 -0
  190. package/dist-server/service/twin-model/twin-model-tree-query.js +158 -0
  191. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -0
  192. package/dist-server/service/twin-model/twin-operation.d.ts +61 -0
  193. package/dist-server/service/twin-model/twin-operation.js +137 -0
  194. package/dist-server/service/twin-model/twin-operation.js.map +1 -0
  195. package/dist-server/service/twin-space/move-space.d.ts +65 -0
  196. package/dist-server/service/twin-space/move-space.js +166 -0
  197. package/dist-server/service/twin-space/move-space.js.map +1 -0
  198. package/dist-server/service/twin-space/space-integrity.d.ts +97 -0
  199. package/dist-server/service/twin-space/space-integrity.js +182 -0
  200. package/dist-server/service/twin-space/space-integrity.js.map +1 -0
  201. package/dist-server/service/twin-space/twin-space-area.js +4 -4
  202. package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
  203. package/dist-server/service/twin-space/twin-space-representation.js +6 -6
  204. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  205. package/dist-server/service/twin-space/twin-space-resolver.d.ts +63 -1
  206. package/dist-server/service/twin-space/twin-space-resolver.js +365 -18
  207. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  208. package/dist-server/service/twin-space/twin-space.d.ts +17 -1
  209. package/dist-server/service/twin-space/twin-space.js +15 -5
  210. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  211. package/dist-server/service/twin-state/twin-state-subscription.js +1 -1
  212. package/dist-server/service/twin-state/twin-state-subscription.js.map +1 -1
  213. package/dist-server/service/twin-structure/twin-structure.d.ts +1 -1
  214. package/dist-server/service/twin-structure/twin-structure.js +8 -7
  215. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  216. package/dist-server/service/twin-target/twin-target-resolver.js +21 -4
  217. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  218. package/dist-shared/axis-read.d.ts +12 -0
  219. package/dist-shared/axis-read.js +32 -0
  220. package/dist-shared/axis-read.js.map +1 -0
  221. package/dist-shared/entity-delta.d.ts +29 -0
  222. package/dist-shared/entity-delta.js +287 -0
  223. package/dist-shared/entity-delta.js.map +1 -0
  224. package/dist-shared/kpi-broadcast.d.ts +4 -0
  225. package/dist-shared/kpi-broadcast.js +16 -0
  226. package/dist-shared/kpi-broadcast.js.map +1 -0
  227. package/dist-shared/twin-level.d.ts +23 -0
  228. package/dist-shared/twin-level.js +52 -0
  229. package/dist-shared/twin-level.js.map +1 -0
  230. package/package.json +14 -12
  231. package/server/engine/attention-digest.ts +102 -0
  232. package/server/engine/canonical-ingest.ts +36 -4
  233. package/server/engine/command-routing.ts +67 -0
  234. package/server/engine/energy-topology.ts +189 -0
  235. package/server/engine/index.ts +17 -0
  236. package/server/engine/kpi-baseline.ts +202 -0
  237. package/server/engine/kpi-fold.ts +389 -1
  238. package/server/engine/kpi-query.ts +295 -22
  239. package/server/engine/kpi-target.ts +24 -2
  240. package/server/engine/live-attentions.ts +7 -2
  241. package/server/engine/live-feed-registry.ts +58 -0
  242. package/server/engine/load-meter.ts +384 -0
  243. package/server/engine/local-declarations.ts +700 -0
  244. package/server/engine/model-basis.ts +78 -10
  245. package/server/engine/model-vocabulary.ts +82 -0
  246. package/server/engine/oee-accumulator.ts +34 -1
  247. package/server/engine/operation-basis.ts +100 -0
  248. package/server/engine/property-effects.ts +199 -0
  249. package/server/engine/runtime-key.ts +58 -0
  250. package/server/engine/spec-coverage.ts +3 -3
  251. package/server/engine/state-axes.ts +60 -0
  252. package/server/engine/structure-diff.ts +1 -1
  253. package/server/engine/travel-estimator.ts +11 -6
  254. package/server/engine/twin-engine.ts +1710 -297
  255. package/server/engine/warm-start.ts +207 -22
  256. package/server/index.ts +13 -10
  257. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +43 -0
  258. package/server/migrations/index.ts +7 -0
  259. package/server/service/index.ts +9 -1
  260. package/server/service/reference/discovery-result.ts +95 -0
  261. package/server/service/reference/ingest-space.ts +104 -0
  262. package/server/service/reference/knob-defaults.ts +59 -0
  263. package/server/service/reference/reference-live.ts +121 -6
  264. package/server/service/reference/reference-master.ts +362 -35
  265. package/server/service/reference/reference-resolver.ts +307 -23
  266. package/server/service/reference/template-registry.ts +20 -5
  267. package/server/service/twin-attention/twin-attention-query.ts +43 -6
  268. package/server/service/twin-audit/command-audit.ts +81 -0
  269. package/server/service/twin-audit/index.ts +5 -0
  270. package/server/service/twin-audit/twin-audit-event.ts +112 -0
  271. package/server/service/twin-audit/twin-audit-query.ts +72 -0
  272. package/server/service/twin-control/twin-control-mutation.ts +53 -13
  273. package/server/service/twin-event/twin-event-keys.ts +1 -1
  274. package/server/service/twin-event/twin-event.ts +52 -15
  275. package/server/service/twin-forecast/forecast-metrics.ts +60 -0
  276. package/server/service/twin-forecast/forecast-tuning.ts +40 -0
  277. package/server/service/twin-forecast/gap-analytics.ts +48 -9
  278. package/server/service/twin-forecast/twin-forecast-query.ts +211 -38
  279. package/server/service/twin-instance/twin-instance.ts +127 -22
  280. package/server/service/twin-journal/twin-journal-query.ts +108 -14
  281. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +40 -23
  282. package/server/service/twin-metrics/twin-metrics-query.ts +60 -3
  283. package/server/service/twin-model/epcis-coverage.ts +195 -0
  284. package/server/service/twin-model/iec61850-coverage.ts +117 -0
  285. package/server/service/twin-model/index.ts +25 -0
  286. package/server/service/twin-model/isa95-coverage.ts +190 -0
  287. package/server/service/twin-model/project-structure.ts +199 -0
  288. package/server/service/twin-model/standard-coverage.ts +35 -0
  289. package/server/service/twin-model/twin-equipment.ts +150 -0
  290. package/server/service/twin-model/twin-lineage-query.ts +193 -0
  291. package/server/service/twin-model/twin-location.ts +140 -0
  292. package/server/service/twin-model/twin-model-item-query.ts +848 -0
  293. package/server/service/twin-model/twin-model-mutation.ts +503 -0
  294. package/server/service/twin-model/twin-model-query.ts +537 -0
  295. package/server/service/twin-model/twin-model-tree-query.ts +180 -0
  296. package/server/service/twin-model/twin-operation.ts +150 -0
  297. package/server/service/twin-space/move-space.ts +262 -0
  298. package/server/service/twin-space/space-integrity.ts +293 -0
  299. package/server/service/twin-space/twin-space-area.ts +4 -4
  300. package/server/service/twin-space/twin-space-representation.ts +6 -6
  301. package/server/service/twin-space/twin-space-resolver.ts +365 -22
  302. package/server/service/twin-space/twin-space.ts +40 -6
  303. package/server/service/twin-state/twin-state-subscription.ts +1 -1
  304. package/server/service/twin-structure/twin-structure.ts +10 -7
  305. package/server/service/twin-target/twin-target-resolver.ts +22 -4
  306. package/shared/axis-read.ts +28 -0
  307. package/shared/entity-delta.ts +286 -0
  308. package/shared/kpi-broadcast.ts +13 -0
  309. package/shared/twin-level.ts +48 -0
  310. package/test/adopt-structure-live.test.ts +133 -0
  311. package/test/attention-digest.test.ts +135 -0
  312. package/test/axis-read.test.ts +87 -0
  313. package/test/boot-resume.test.ts +205 -0
  314. package/test/canonical-ingest-vocabularies.test.ts +118 -0
  315. package/test/capability-mapping.test.ts +5 -5
  316. package/test/command-routing.test.ts +61 -0
  317. package/test/declaration-reaches-model.test.ts +178 -0
  318. package/test/discovery-result.test.ts +75 -0
  319. package/test/energy-topology.test.ts +113 -0
  320. package/test/entity-delta.test.ts +195 -25
  321. package/test/event-time-column.test.ts +67 -0
  322. package/test/forecast-metrics.test.ts +71 -0
  323. package/test/forecast-tuning.test.ts +42 -0
  324. package/test/gap-analytics.test.ts +34 -7
  325. package/test/ingest-bench.test.ts +9 -9
  326. package/test/ingest-running-guard.test.ts +136 -0
  327. package/test/ingest-space.test.ts +78 -0
  328. package/test/instance-cache-lifecycle.test.ts +110 -0
  329. package/test/kernel-kind-guard.test.ts +96 -0
  330. package/test/knob-defaults.test.ts +72 -0
  331. package/test/kpi-baseline-db.test.ts +216 -0
  332. package/test/kpi-baseline.test.ts +196 -0
  333. package/test/kpi-fold.test.ts +393 -2
  334. package/test/kpi-query-bench.test.ts +130 -0
  335. package/test/lineage-survives-restart.test.ts +203 -0
  336. package/test/live-feed-registry.test.ts +67 -0
  337. package/test/live-kernel-facts.test.ts +161 -0
  338. package/test/live-mirror-parity.test.ts +54 -14
  339. package/test/load-meter.test.ts +314 -0
  340. package/test/local-declarations.test.ts +554 -0
  341. package/test/master-to-twin.test.ts +128 -36
  342. package/test/model-basis.test.ts +56 -1
  343. package/test/model-vocabulary.test.ts +114 -0
  344. package/test/move-space.test.ts +149 -0
  345. package/test/mutation-gate.test.ts +122 -0
  346. package/test/oee-accumulator.test.ts +90 -7
  347. package/test/operation-basis.test.ts +100 -0
  348. package/test/operations-capability-db.test.ts +165 -0
  349. package/test/project-structure-db.test.ts +226 -0
  350. package/test/projection-reaches-screen.test.ts +272 -0
  351. package/test/property-effects.test.ts +96 -0
  352. package/test/registry-key-guard.test.ts +80 -0
  353. package/test/resync-origin-site.test.ts +77 -0
  354. package/test/runtime-key.test.ts +66 -0
  355. package/test/scale-twin-bench.test.ts +7 -7
  356. package/test/snapshot-freshness.test.ts +60 -0
  357. package/test/space-integrity.test.ts +223 -0
  358. package/test/standard-coverage.test.ts +94 -0
  359. package/test/state-axes.test.ts +74 -0
  360. package/test/streamline-e2e.test.ts +16 -16
  361. package/test/structure-revision-db.test.ts +16 -15
  362. package/test/tenant-registry-db.test.ts +149 -0
  363. package/test/twin-audit.test.ts +83 -0
  364. package/test/twin-model-item-db.test.ts +275 -0
  365. package/test/twin-model-tree-db.test.ts +176 -0
  366. package/test/twin-origin-resync.test.ts +114 -0
  367. package/test/warm-start-seam.test.ts +140 -0
  368. package/test/warm-start.test.ts +223 -4
  369. package/tsconfig.json +6 -1
  370. package/tsconfig.shared.json +23 -0
  371. package/tsconfig.shared.tsbuildinfo +1 -0
  372. package/tsconfig.tsbuildinfo +1 -0
  373. package/dist-server/engine/entity-delta.d.ts +0 -19
  374. package/dist-server/engine/entity-delta.js +0 -161
  375. package/dist-server/engine/entity-delta.js.map +0 -1
  376. package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
  377. package/dist-server/service/twin-event/backfill-keys.js +0 -63
  378. package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
  379. package/dist-server/tsconfig.tsbuildinfo +0 -1
  380. package/server/engine/entity-delta.ts +0 -169
  381. package/server/service/twin-event/backfill-keys.ts +0 -72
@@ -11,9 +11,27 @@ import { TWIN_METRIC, isTwinMetric } from '../../engine/kpi-target.js'
11
11
  * 경영 KPI 관리메뉴가 아니라 여기인 이유: 목표가 숫자 옆에 있어야 맥락이 살고, 운영 임계는 자주
12
12
  * 조정된다. 조직 단위 기간 평가는 `@things-factory/kpi` 의 일이고 이쪽으로 베껴 오지 않는다.
13
13
  */
14
+ /*
15
+ * **쓰기는 막고 읽기는 연다** (2026-08-06 · ADR-0027).
16
+ *
17
+ * 이 리졸버는 트윈 리졸버 중 **유일하게** 권한 게이트를 갖는데, 처음에는 우회 표시가 없었다
18
+ * (`@privilege(category:"twin", privilege:"query")`). 그러면 `twin/query`·`twin/mutation` 권한을
19
+ * 실제로 부여받은 사용자만 쓸 수 있는데 — **저장소 어디에도 그 권한을 만드는 곳이 없다.** 결과는
20
+ * 도메인 소유자도 슈퍼유저도 목표를 걸 수 없는 상태였다: 성과 화면의 목표 버튼이 "Unauthorized" 로
21
+ * 떨어지고, 이 관점의 최상위 기능(§5 목표 프레임)이 **아무도 쓸 수 없는 채로** 놓여 있었다.
22
+ *
23
+ * 그래서 프레임워크의 통용 관용구를 따른다 — 가장 가까운 형제인 `@things-factory/kpi`(목표·지표 관리)가
24
+ * 74곳 전부 `domainOwnerGranted: true, superUserGranted: true` 를 쓴다. 같은 성격의 조작(무엇을 잘한
25
+ * 것으로 볼지 정하는 일)이므로 같은 자세를 쓴다: **현장을 소유한 사람은 바로 쓰고**, 다른 사람에게
26
+ * 맡기려면 `twin` 권한을 부여한다.
27
+ *
28
+ * 이제 트윈의 **모든 변경**에 같은 게이트가 있다(ADR-0027) — 처음에는 이 리졸버 하나만 게이트를 갖는
29
+ * 반쪽이었다. 조회 게이트는 걷었다: 같은 판정이 `twinKpi` 로 이미 열려 있어 지키는 것이 없었다.
30
+ */
14
31
  @Resolver()
15
32
  export class TwinTargetResolver {
16
- @Directive('@privilege(category: "twin", privilege: "query")')
33
+ /* 조회에는 게이트를 두지 않는다 — 같은 판정이 `twinKpi.targets` 로 이미 열려 있어(그쪽은 게이트가
34
+ 없다) 여기만 막으면 지키는 것 없이 화면만 깨진다. 읽기의 경계는 **테넌트 소속**이다. */
17
35
  @Query(returns => [String], {
18
36
  description:
19
37
  'The metric ids a twin target may refer to. Fixed in one place so a target and the fact it judges can never drift into two names.'
@@ -22,7 +40,7 @@ export class TwinTargetResolver {
22
40
  return Object.keys(TWIN_METRIC)
23
41
  }
24
42
 
25
- @Directive('@privilege(category: "twin", privilege: "query")')
43
+
26
44
  @Query(returns => [TwinTarget], {
27
45
  description:
28
46
  'Operational targets in scope — site-wide targets for the space plus targets bound to the given instance. Callers judge with the narrower one when both exist.'
@@ -37,7 +55,7 @@ export class TwinTargetResolver {
37
55
  return rows.filter(r => (spaceId && r.spaceId === spaceId) || (instanceId && r.instanceId === instanceId))
38
56
  }
39
57
 
40
- @Directive('@privilege(category: "twin", privilege: "mutation")')
58
+ @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
41
59
  @Mutation(returns => Boolean, {
42
60
  description:
43
61
  'Create or update one operational target. The metric must be one of twinTargetMetrics — an unknown name is rejected rather than stored, because a target nobody can evaluate reads as "no target" and hides the gap.'
@@ -79,7 +97,7 @@ export class TwinTargetResolver {
79
97
  return true
80
98
  }
81
99
 
82
- @Directive('@privilege(category: "twin", privilege: "mutation")')
100
+ @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
83
101
  @Mutation(returns => Boolean, { description: 'Remove an operational target. Removing a target removes the judgement, not the fact — the metric keeps being reported without a verdict.' })
84
102
  async deleteTwinTarget(
85
103
  @Arg('metric') metric: string,
@@ -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
+ }
@@ -0,0 +1,286 @@
1
+ /*
2
+ * 트윈 State 스냅샷 → 라이브 바인딩 delta 목록 (순수 매핑 — pubsub/도메인 무관 → 단위 테스트 가능).
3
+ * publishEntityData 가 이 목록을 엔티티별 시그니처로 dedup 후 data(tag) 채널로 발행한다.
4
+ *
5
+ * ── 규율: 레퍼런스가 아는 것은 **전부** 내보낸다 ────────────────────────────
6
+ * 트윈에서 사소한 속성은 없다. 소속 구역·소속 팔레트·수량·만료처럼 곁가지로 보이는 값이 실제로는
7
+ * 구역 롤업·적재 표현·로트 추적의 전제다. 그래서 여기서 **필드를 손으로 골라 담지 않는다.**
8
+ *
9
+ * 예전에는 엔티티마다 필요할 것 같은 필드만 열거했고, 그 결과 계약에 있는 것이 조용히 누락됐다
10
+ * (2026-08-01 감사: 노드 parentId 누락 · 작업 전량 누락 · 물품 전량 누락). 화이트리스트 방식은
11
+ * **계약이 자라도 화면이 따라오지 못한다** — 커널이 속성을 늘릴 때마다 이 파일을 고쳐야 하고, 고치는
12
+ * 것을 잊으면 아무 신호 없이 정보가 사라진다.
13
+ *
14
+ * 그래서 반대로 한다: **상태 객체가 가진 것을 그대로 통과**시키고, 통과시키지 **않을** 것만 이유와 함께
15
+ * 열거한다(EXCLUDED). 새 속성은 자동으로 화면에 도달하고, 빼려면 이유를 적어야 한다.
16
+ *
17
+ * ── 표식은 도메인 낱말을 훔치지 않는다 ─────────────────────────────────────
18
+ * 예전 payload 는 엔티티 종류 표식을 `kind` 로 실었다. 그런데 `kind` 는 **도메인의 낱말**이다 —
19
+ * 무버 kind=cutter, 작업 kind=putaway, 오더 kind=workorder. 표식이 그 이름을 차지하면 도메인 속성을
20
+ * 실을 자리가 없어진다(같은 병을 오늘 ox-timeline 의 `group` 에서도 고쳤다).
21
+ * 그래서 표식은 `entity`(우리 이름)로 옮기고 `kind` 는 도메인에 돌려준다.
22
+ *
23
+ * payload 계약(씬 컴포넌트가 소비): `entity`(표식) + 그 엔티티의 모든 속성(도메인 `kind` 포함).
24
+ * entity = location · equipment · person · asset · order · task · item,
25
+ * 그리고 집약 태그(__orders__ · __tasks__ · __items__ · __persons__ · __assets__).
26
+ */
27
+
28
+ export interface EntityDelta {
29
+ tag: string
30
+ data: any
31
+ }
32
+
33
+ /** 집약 고정 태그 — 클라 provider 와 일치해야 한다(런타임 생성 엔티티는 개별 배치가 불가하므로 집약 뷰). */
34
+ /*
35
+ * 집약 채널의 접두사 — **실제 태그는 트윈 식별자를 붙여 만든다.**
36
+ *
37
+ * 예전에는 이 상수가 곧 태그였다. 그런데 발행 키는 (도메인, 태그)이고, **한 공간에 트윈이 여럿**
38
+ * 있으면(실측: 3곳 × 3개) 셋이 같은 태그로 발행해 서로 덮었다. 보드의 주문 목록판이 세 공장의 주문
39
+ * 중 마지막에 도착한 것을 보여 준다 — 오류 없이.
40
+ *
41
+ * 구간 성과 태그가 이미 `__kpi__:<트윈>` 으로 트윈을 붙이고 있었다(`kpi-broadcast.ts`). 같은 규약으로 맞춘다.
42
+ */
43
+ export const ORDERS_TAG_PREFIX = '__orders__'
44
+ export const TASKS_TAG_PREFIX = '__tasks__'
45
+ export const ITEMS_TAG_PREFIX = '__items__'
46
+ export const PERSONS_TAG_PREFIX = '__persons__'
47
+ export const ASSETS_TAG_PREFIX = '__assets__'
48
+ /** 에너지 집약 — 수요 구간·피크·계약은 **트윈 하나의 사실**이라 개체 태그가 없다. */
49
+ export const ENERGY_TAG_PREFIX = '__energy__'
50
+
51
+ /** 이 트윈의 집약 채널 태그. 트윈 식별자가 없으면 만들지 않는다 — 덮어쓰기의 원인이었다. */
52
+ export function aggregateTag(prefix: string, instanceId: string): string {
53
+ if (!instanceId) throw new Error('aggregateTag: instanceId required — 트윈 구분이 없는 태그는 다른 트윈의 것을 덮는다')
54
+
55
+ return `${prefix}:${instanceId}`
56
+ }
57
+
58
+ export const ordersTag = (instanceId: string) => aggregateTag(ORDERS_TAG_PREFIX, instanceId)
59
+ export const tasksTag = (instanceId: string) => aggregateTag(TASKS_TAG_PREFIX, instanceId)
60
+ export const itemsTag = (instanceId: string) => aggregateTag(ITEMS_TAG_PREFIX, instanceId)
61
+ export const personsTag = (instanceId: string) => aggregateTag(PERSONS_TAG_PREFIX, instanceId)
62
+ export const assetsTag = (instanceId: string) => aggregateTag(ASSETS_TAG_PREFIX, instanceId)
63
+ export const energyTag = (instanceId: string) => aggregateTag(ENERGY_TAG_PREFIX, instanceId)
64
+
65
+ /**
66
+ * 통과시키지 않는 필드 — **빼는 데는 이유가 필요하다.**
67
+ *
68
+ * · `id`/`epc` — 태그로 이미 나가므로 개별 payload 에 중복해 싣지 않는다(집약 뷰에는 넣는다).
69
+ * · `oee` — 원시 누적(idleMs 등)이 매 delta 증가해 "무변화 생략" 을 무력화한다. shapeOee 로 정형화해 얹는다.
70
+ */
71
+ const EXCLUDED = new Set(['id', 'epc', 'oee'])
72
+
73
+ /**
74
+ * OEE 원시 비율/카운트 → 표시용 정수 퍼센트 + 카운트.
75
+ * 원시 누적 ms(idleMs 등)는 매 delta 증가해 무변화-생략을 무력화하므로 제외(유휴 자원의 매초 재방송 억제).
76
+ * oee 미측정이면 undefined.
77
+ */
78
+ function shapeOee(o: any): any {
79
+ if (!o) return undefined
80
+ return {
81
+ overall: Math.round((o.overall ?? 0) * 100),
82
+ availability: Math.round((o.availability ?? 0) * 100),
83
+ performance: Math.round((o.performance ?? 0) * 100),
84
+ quality: Math.round((o.quality ?? 0) * 100),
85
+ good: o.goodCount ?? 0,
86
+ scrap: o.scrapCount ?? 0
87
+ }
88
+ }
89
+
90
+ /**
91
+ * 엔티티 하나의 payload — `entity` 표식 + 나머지 전부(도메인 `kind` 포함).
92
+ *
93
+ * `undefined` 값은 싣지 않는다(키의 유무가 시그니처를 흔들지 않게). 값이 없는 것과 키가 없는 것은
94
+ * 소비처에서 같으므로 안전하고, 무변화 생략이 잘 듣는다.
95
+ */
96
+ function payloadOf(entityKind: string, entity: any): any {
97
+ /* 표식은 `entity` — 도메인의 `kind` 를 덮지 않는다. */
98
+ const data: any = { entity: entityKind }
99
+ for (const [k, v] of Object.entries(entity ?? {})) {
100
+ if (EXCLUDED.has(k) || v === undefined) continue
101
+ data[k] = v
102
+ }
103
+ return data
104
+ }
105
+
106
+ /**
107
+ * 집약 뷰의 항목 — 개별 payload 와 같은 내용 + 식별자.
108
+ * 집약은 "전체를 한 컴포넌트가 추적" 하는 용도라 식별자가 있어야 한다.
109
+ */
110
+ function aggregateItem(entity: any, idKey: 'id' | 'epc'): any {
111
+ const out: any = { [idKey]: entity?.[idKey] }
112
+ for (const [k, v] of Object.entries(entity ?? {})) {
113
+ if (k === idKey || k === 'oee' || v === undefined) continue
114
+ out[k] = v
115
+ }
116
+ const shaped = shapeOee(entity?.oee)
117
+ if (shaped) out.oee = shaped
118
+ return out
119
+ }
120
+
121
+ /**
122
+ * 물품 집약의 상한 — 큰 현장은 물품이 수만 개라, 전량을 매 방송에 실으면 전달이 현장을 느리게 한다.
123
+ *
124
+ * **조용히 자르지 않는다**: 넘치면 `total`(전체 수)과 `truncated`(잘린 수)를 함께 실어, 소비처가
125
+ * "이게 전부" 라고 오해하지 않게 한다. 개별 물품은 태그별 delta 로 따로 나가고 그쪽은 시그니처 dedup
126
+ * 이 걸려 **변한 것만** 재방송된다.
127
+ */
128
+ export const ITEMS_AGGREGATE_LIMIT = 500
129
+
130
+ /**
131
+ * 자리별 에너지 — **그 자리의 계량기들이 잰 것**을 그 자리 태그에 실을 수 있게 모은다.
132
+ *
133
+ * ── 왜 자리에도 필요한가 (2026-08-14) ───────────────────────────────────────
134
+ * 카탈로그는 자리에도 `metered` 를 준다(수전·분기·구역 계량은 재는 자리다). 그런데 값은 계량기
135
+ * **설비** 태그로만 나갔다. 그래서 계통도의 분기 자리에 계량 컴포넌트를 놓으면 **영원히 「—」** 였다 —
136
+ * 능력은 「잰다」고 말하는데 값이 오지 않는 자리.
137
+ *
138
+ * 「이 구간이 얼마를 먹나」는 자리의 사실이고, 사람은 계통도의 자리에 붙여 본다.
139
+ *
140
+ * ── 계층은 여기서 문제가 되지 않는다 ────────────────────────────────────────
141
+ * 각 자리에는 **그 자리에 붙은 계량기만** 더한다(수전 자리에는 수전 계량기, 분기 자리에는 그 분기의
142
+ * 것). 커널이 부하를 뿌리로만 세는 것과 다른 층의 계산이다 — 여기서는 자리마다 자기 것을 말한다.
143
+ *
144
+ * 근거를 함께 낸다(`meters`) — 그 자리의 수가 어느 계량기에서 왔는지 되짚을 수 있어야 한다.
145
+ */
146
+ function energyByLocation(state: any): Map<string, Record<string, unknown>> {
147
+ const out = new Map<string, Record<string, unknown>>()
148
+ const points = state?.energy?.points
149
+ if (!Array.isArray(points) || !points.length) return out
150
+
151
+ /* 계량기 → 자리. 자리를 모르는 계량기는 자리 payload 에 실을 데가 없다(설비 태그로만 나간다). */
152
+ const locOf = new Map<string, string>()
153
+ for (const e of state?.equipment ?? []) if (e?.id && e?.homeLocation) locOf.set(e.id, e.homeLocation)
154
+
155
+ for (const pt of points) {
156
+ const loc = pt?.id ? locOf.get(pt.id) : undefined
157
+ if (!loc) continue
158
+ const acc = out.get(loc) ?? { meters: [] as string[], samplesInWindow: 0 }
159
+ ;(acc.meters as string[]).push(pt.id)
160
+ acc.samplesInWindow = Number(acc.samplesInWindow ?? 0) + (Number(pt.samplesInWindow) || 0)
161
+ /* 값은 **있는 것만** 더한다 — 하나도 없으면 키를 만들지 않는다(0 은 「안 썼다」가 된다). */
162
+ if (Number.isFinite(Number(pt.kW))) acc.kW = Number(acc.kW ?? 0) + Number(pt.kW)
163
+ if (Number.isFinite(Number(pt.kWh))) acc.kWh = Number(acc.kWh ?? 0) + Number(pt.kWh)
164
+ /* 나이는 **가장 최근**을 쓴다 — 자리에 계량기가 여럿이면 그중 마지막 관측이 그 자리의 나이다. */
165
+ if (pt.atMs !== undefined) {
166
+ const iso = new Date(Number(pt.atMs)).toISOString()
167
+ if (!acc.measuredAt || String(acc.measuredAt) < iso) acc.measuredAt = iso
168
+ }
169
+ out.set(loc, acc)
170
+ }
171
+ return out
172
+ }
173
+
174
+ export function buildEntityDeltas(state: any, instanceId: string): EntityDelta[] {
175
+ const out: EntityDelta[] = []
176
+ const locEnergy = energyByLocation(state)
177
+
178
+ for (const n of state?.locations ?? []) {
179
+ /* 에너지는 자리 payload 에 **합친다** — 같은 태그로 두 번 보내면 뒤엣것이 앞을 덮는다. */
180
+ const energy = locEnergy.get(n.id)
181
+ out.push({ tag: n.id, data: energy ? { ...payloadOf('location', n), ...energy } : payloadOf('location', n) })
182
+ }
183
+ for (const m of state?.equipment ?? []) {
184
+ /* Mobile(location/motion) + Operable(status/held) + Processable(oee) — 한 payload 로.
185
+ * oee 만 정형화가 필요해 따로 얹는다(나머지는 통과). */
186
+ const data = payloadOf('equipment', m)
187
+ const oee = shapeOee(m?.oee)
188
+ if (oee) data.oee = oee
189
+ out.push({ tag: m.id, data })
190
+ }
191
+ /* 사람 — 설비와 별개 엔티티다. 같은 통과 규칙(가진 것을 전부 내보낸다). */
192
+ for (const p of state?.persons ?? []) {
193
+ out.push({ tag: p.id, data: payloadOf('person', p) })
194
+ }
195
+ /* 물리 자산(반복사용) — 물품도 설비도 아닌 네 번째 자원. */
196
+ for (const a of state?.assets ?? []) {
197
+ out.push({ tag: a.id, data: payloadOf('asset', a) })
198
+ }
199
+ for (const o of state?.orders ?? []) {
200
+ out.push({ tag: o.id, data: payloadOf('order', o) })
201
+ }
202
+ for (const t of state?.tasks ?? []) {
203
+ out.push({ tag: t.id, data: payloadOf('task', t) })
204
+ }
205
+ for (const it of state?.items ?? []) {
206
+ /* 물품의 태그는 epc(개체 식별자) — 노드·무버와 달리 id 가 아니다. */
207
+ if (it?.epc) out.push({ tag: it.epc, data: payloadOf('item', it) })
208
+ }
209
+
210
+ /* ── 집약 뷰 ──────────────────────────────────────────────────────────────
211
+ * 런타임에 생기고 사라지는 엔티티(오더·작업·물품)는 보드에 개별 배치할 수 없다 → 한 컴포넌트가
212
+ * 전체를 추적하는 집약 태그를 항상 방출한다. */
213
+ const orders = state?.orders ?? []
214
+ out.push({ tag: ordersTag(instanceId), data: { entity: 'orders', orders: orders.map((o: any) => aggregateItem(o, 'id')) } })
215
+
216
+ const tasks = state?.tasks ?? []
217
+ out.push({ tag: tasksTag(instanceId), data: { entity: 'tasks', tasks: tasks.map((t: any) => aggregateItem(t, 'id')) } })
218
+
219
+ /* 사람 집약 — 인원은 보드에 개별 배치할 수도 있지만(설비처럼), 전체를 한눈에 보는 소비처도 있다.
220
+ * 인원을 선언하지 않은 트윈에서는 빈 배열이 나간다(없음과 모름을 구별). */
221
+ const persons = state?.persons ?? []
222
+ out.push({ tag: personsTag(instanceId), data: { entity: 'persons', persons: persons.map((p: any) => aggregateItem(p, 'id')) } })
223
+
224
+ const assets = state?.assets ?? []
225
+ out.push({ tag: assetsTag(instanceId), data: { entity: 'assets', assets: assets.map((a: any) => aggregateItem(a, 'id')) } })
226
+
227
+ const items = state?.items ?? []
228
+ const shown = items.slice(0, ITEMS_AGGREGATE_LIMIT)
229
+ out.push({
230
+ tag: itemsTag(instanceId),
231
+ data: {
232
+ entity: 'items',
233
+ items: shown.map((it: any) => aggregateItem(it, 'epc')),
234
+ total: items.length,
235
+ /* 잘렸으면 그 사실을 말한다 — "이게 전부" 로 읽히면 재고를 과소 판단한다. */
236
+ ...(items.length > shown.length ? { truncated: items.length - shown.length } : {})
237
+ }
238
+ })
239
+
240
+ /*
241
+ * ── 에너지 (EMS) ─────────────────────────────────────────────────────────
242
+ *
243
+ * 계측은 `state.energy` 에 산다 — 자원 축(`equipment`·`locations`)이 아니다. 그래서 이 매핑이 없으면
244
+ * 보드에 놓은 계량기가 **아무 값도 받지 못한다**(컴포넌트는 있는데 영원히 빈 모습).
245
+ *
246
+ * 두 갈래로 낸다:
247
+ * · **개체별** — 계량 지점 하나하나의 마지막 관측(kW·kWh·역률·시각). 태그는 그 설비 id 다.
248
+ * · **집약** — 수요 구간·피크·계약전력. 이것은 트윈 하나의 사실이라 개체에 실을 수 없다.
249
+ *
250
+ * 값이 없으면 **키를 넣지 않는다**(0 으로 만들지 않는다) — 「못 읽었다」와 「0 이었다」는 다른 사실이고,
251
+ * 그 구별이 이 트윈의 값어치다.
252
+ */
253
+ const energy = state?.energy
254
+ if (energy) {
255
+ for (const pt of energy.points ?? []) {
256
+ if (!pt?.id) continue
257
+ out.push({
258
+ tag: pt.id,
259
+ data: {
260
+ entity: 'meter',
261
+ ...(pt.kW !== undefined ? { kW: pt.kW } : {}),
262
+ ...(pt.kWh !== undefined ? { kWh: pt.kWh } : {}),
263
+ ...(pt.powerFactor !== undefined ? { powerFactor: pt.powerFactor } : {}),
264
+ /* 마지막 표본 시각 — 계측이 끊긴 것을 화면이 스스로 알 수 있게(우리가 판정하지 않는다). */
265
+ ...(pt.atMs !== undefined ? { measuredAt: new Date(pt.atMs).toISOString() } : {}),
266
+ samplesInWindow: pt.samplesInWindow ?? 0
267
+ }
268
+ })
269
+ }
270
+ out.push({
271
+ tag: energyTag(instanceId),
272
+ data: {
273
+ entity: 'energy',
274
+ ...(energy.contractKW !== undefined ? { contractKW: energy.contractKW } : {}),
275
+ ...(energy.open ? { open: energy.open } : {}),
276
+ ...(energy.peakSince ? { peakSince: energy.peakSince } : {}),
277
+ /* 마감된 구간은 **최근 것만** 낸다 — 하루치를 매 방송에 실으면 채널이 그것으로 찬다.
278
+ 전체 수는 함께 낸다(조용히 자르지 않는다). */
279
+ recent: (energy.closed ?? []).slice(-8),
280
+ closedTotal: energy.closedTotal ?? 0
281
+ }
282
+ })
283
+ }
284
+
285
+ return out
286
+ }
@@ -0,0 +1,13 @@
1
+ /*
2
+ * 구간 성과 카드의 **대상 식별자**.
3
+ *
4
+ * 예전에는 이 파일이 방송 payload 도 만들었다(`shapeKpiBroadcast`). 2026-08-06 에 방송을 없애고 카드가
5
+ * `twinKpi` 를 직접 묻게 바꿔서, 남은 것은 팔레트가 대상을 가리킬 때 쓰는 식별자뿐이다.
6
+ * 왜 바꿨는지는 `twin-engine.ts` 의 주석과 `operato-twin/client/scene/twin-kpi-source.ts` 에 있다.
7
+ */
8
+ export const KPI_TAG_PREFIX = '__kpi__'
9
+
10
+ /** 이 트윈의 구간 성과 태그. */
11
+ export const kpiTag = (instanceId: string) => `${KPI_TAG_PREFIX}:${instanceId}`
12
+
13
+ /** 표식은 `entity` 로 — `kind` 는 도메인의 낱말이라 표식이 훔치지 않는다(`entity-delta.ts` 와 같은 규약). */
@@ -0,0 +1,48 @@
1
+ /*
2
+ * 공간 계층의 **단계** — 성과를 어느 단위로 보는가. 순수(import 없음).
3
+ *
4
+ * ── 왜 여기 있나 ───────────────────────────────────────────────────────────
5
+ * 이 목록을 세 곳이 쓴다: 씬 표현이 "나는 어느 단계에 붙는다" 고 선언할 때(클라이언트), 성과 카드가
6
+ * "이 단계로 나눠 달라" 고 물을 때(클라이언트), AI 가 "구역별로 보여 줘" 를 제안할 때(서버).
7
+ * 세 곳이 각자 목록을 적으면 하나가 늘 때 나머지가 조용히 뒤처진다 — 집약 태그에서 이미 겪었다.
8
+ *
9
+ * 그래서 **한 곳**에 두고 셋이 가져다 쓴다. 이 파일은 import 가 없어 클라이언트 번들에 서버 코드를
10
+ * 끌고 들어가지 않는다.
11
+ */
12
+
13
+ /**
14
+ * 성과를 접을 수 있는 단계 — 좁은 것부터.
15
+ *
16
+ * `equipment` 는 커널 축 이름으로 `resource` 다(설비·인원·자산을 아우르는 이름). 화면과 AI 는
17
+ * "설비" 로 말하고, 커널에 물을 때만 축 이름으로 바꾼다 — 그 변환이 아래 한 곳에 있다.
18
+ */
19
+ export const TWIN_LEVELS = ['space', 'twin', 'area', 'location', 'equipment'] as const
20
+ export type TwinLevel = (typeof TWIN_LEVELS)[number]
21
+
22
+ export function isTwinLevel(v: unknown): v is TwinLevel {
23
+ return typeof v === 'string' && (TWIN_LEVELS as readonly string[]).includes(v)
24
+ }
25
+
26
+ /**
27
+ * 단계 → 커널의 나누는 축(`KpiGroupBy`). **전체를 보는 단계는 축이 없다** — 나누지 않고 통째로 접는다.
28
+ *
29
+ * `space` 와 `twin` 이 축 없이 같은 이유: 둘의 차이는 "무엇을 대상으로 삼는가"(공간 전체 대 트윈 하나)이지
30
+ * "어떻게 나누는가" 가 아니다. 대상은 질의의 `spaceId`/`instanceId` 가 정한다.
31
+ */
32
+ export function kpiGroupByOfLevel(level: TwinLevel): 'area' | 'location' | 'resource' | undefined {
33
+ if (level === 'area') return 'area'
34
+ if (level === 'location') return 'location'
35
+ if (level === 'equipment') return 'resource'
36
+
37
+ return undefined
38
+ }
39
+
40
+ /**
41
+ * 그 단계의 카드가 **대상 하나마다 한 장**인가.
42
+ *
43
+ * 구역·자리·설비는 여럿이라 카드가 그 수만큼 생긴다. 공간·트윈은 하나뿐이다. 보드를 자동으로 조립할 때
44
+ * 몇 장이 생길지 미리 알아야 한다 — 자리 200개 × 지표 4개면 800장이다.
45
+ */
46
+ export function isPerTargetLevel(level: TwinLevel): boolean {
47
+ return kpiGroupByOfLevel(level) !== undefined
48
+ }
@@ -0,0 +1,133 @@
1
+ /*
2
+ * ★ 돌고 있는 미러에 새 구조를 넣는다 — **미러는 멈출 수 없다.**
3
+ *
4
+ * 미러가 비추는 현실은 24시간 돈다. 설비 한 대가 늘었다고 트윈을 멈췄다 세우면 그 사이의
5
+ * 사실을 잃고, 라이브에서 유실은 곧 거짓이다. 그래서 미러는 **돌면서** 갈아탄다.
6
+ *
7
+ * 갈아타기가 옳으려면 **세 층이 함께** 움직여야 한다. 하나라도 빠지면 조용히 어긋난다:
8
+ * ① 리비전 — 이후의 이벤트가 새 번호를 달고 다닌다(재생이 마디를 나눌 수 있게)
9
+ * ② 저장 model·구조 행 — 조회가 새 구조를 본다
10
+ * ③ 커널 — 지금 접히는 이벤트가 새 구조 위에서 접힌다
11
+ *
12
+ * 커널 단위 거동(무엇을 지키고 무엇을 버리나)은 커널 쪽 `adopt-structure.test.ts` 가 지킨다.
13
+ * 여기서 지키는 것은 **호스트 배선** — 세 층이 같은 인제스트에서 함께 움직이는가.
14
+ *
15
+ * 실 DataSource 로 돈다. 부분 적용은 정의상 "여러 표에 걸쳐 무엇이 남았나" 이므로.
16
+ */
17
+ import { test, before, after } from 'node:test'
18
+ import assert from 'node:assert/strict'
19
+ import { createRequire } from 'node:module'
20
+ import type { DataSource } from 'typeorm'
21
+
22
+ const req = createRequire(import.meta.url)
23
+ const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
24
+ const { entities: authEntities } = req('@things-factory/auth-base')
25
+ const { entities: twinEntities } = req('../dist-server/service/index.js')
26
+ const { TwinEngine } = req('../dist-server/engine/index.js')
27
+ const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js')
28
+ const { TwinStructure } = req('../dist-server/service/twin-structure/twin-structure.js')
29
+ const { TwinLocation } = req('../dist-server/service/twin-model/twin-location.js')
30
+ const { TwinEquipment } = req('../dist-server/service/twin-model/twin-equipment.js')
31
+ const { DataSource: TypeOrmDataSource } = req('typeorm')
32
+
33
+ let ds: DataSource
34
+ let domainId: string
35
+
36
+ const MIRROR = 'mirror-probe'
37
+ const SIM = 'sim-probe'
38
+
39
+ const master = (source: string, bins: number, forklifts: number): any => ({
40
+ source,
41
+ system: 'wms',
42
+ siteName: '미러 창고',
43
+ space: { width: 60, depth: 30, unit: 'm' },
44
+ locations: [
45
+ { id: 'dock-recv', type: 'dock', role: 'location', name: '입고 도크', capacity: 100 },
46
+ ...Array.from({ length: bins }, (_, i) => ({ id: `bin-${i + 1}`, type: 'storage', role: 'location', name: `빈${i + 1}`, capacity: 20 }))
47
+ ],
48
+ equipment: Array.from({ length: forklifts }, (_, i) => ({ id: `fk-${i + 1}`, kind: 'forklift', homeLocationId: 'dock-recv' }))
49
+ })
50
+
51
+ before(async () => {
52
+ ds = new TypeOrmDataSource({
53
+ type: 'sqlite',
54
+ database: ':memory:',
55
+ synchronize: true,
56
+ dropSchema: true,
57
+ namingStrategy: new NamingStrategy(),
58
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
59
+ logging: false
60
+ })
61
+ await ds.initialize()
62
+ addDataSource('default', ds)
63
+ const d = await ds.getRepository(Domain).save({ name: 'adopt-test', subdomain: 'adopt-test' } as any)
64
+ domainId = d.id
65
+
66
+ await TwinEngine.ingestMaster(domainId, master(MIRROR, 3, 2))
67
+ await TwinEngine.ingestMaster(domainId, master(SIM, 3, 2))
68
+ })
69
+
70
+ after(async () => {
71
+ for (const id of [MIRROR, SIM]) await TwinEngine.stop(domainId, id).catch(() => {})
72
+ await ds?.destroy()
73
+ })
74
+
75
+ test('돌고 있는 미러는 멈추지 않고 갈아탄다 — 세 층이 함께 움직인다', async () => {
76
+ /* 미러 기동은 선언 기반 부팅이 가른다 — `startFromRegistry` 는 시뮬 경로다(같은 함수가 아니다). */
77
+ const rt = await TwinEngine.bootDeclared(domainId, MIRROR, 'mirror')
78
+ assert.equal(rt.mode, 'live', '미러로 떠야 이 검사가 뜻이 있다')
79
+
80
+ const res = await TwinEngine.ingestMaster(domainId, master(MIRROR, 7, 2))
81
+
82
+ /* ① 리비전 — 이후 이벤트가 달고 다닐 번호가 올라간다 */
83
+ const revs = await ds.getRepository(TwinStructure).find({ where: { instanceId: MIRROR }, order: { rev: 'ASC' } })
84
+ assert.deepEqual(revs.map((r: any) => r.rev), [1, 2], '구조가 달라졌으니 새 리비전이 남는다')
85
+
86
+ /* ② 저장 model·구조 행 — 조회가 새 구조를 본다 */
87
+ const inst = await ds.getRepository(TwinInstance).findOne({ where: { instanceId: MIRROR } })
88
+ assert.equal(inst.model.locations.length, 8, '저장 model 가 새 구조다(도크 1 + 빈 7)')
89
+ assert.equal(await ds.getRepository(TwinLocation).countBy({ instance: { id: inst.id } } as any), 8, '구조 행도 함께 다시 그려진다')
90
+
91
+ /* ③ 커널 — 멈추지 않은 채로 새 구조를 들고 있다 */
92
+ assert.equal(TwinEngine.instances[`${domainId}:${MIRROR}`]?.mode, 'live', '갈아타기가 미러를 멈추지 않는다')
93
+ const snap = TwinEngine.snapshot(domainId, MIRROR)
94
+ assert.equal(snap.locations.filter((n: any) => n.type === 'storage').length, 7, '커널도 새 자리를 들고 있다')
95
+
96
+ /* 무엇이 달라졌는지 **말한다** — 멈췄다 세우는 마디가 없으므로 말하지 않으면 조용히 지나간다 */
97
+ const adopted = res.warnings.find((w: any) => w.code === 'structure-adopted')
98
+ assert.ok(adopted, '갈아타기는 말없이 성공하지 않는다')
99
+ assert.equal(adopted.params.locationsAdded, 4, '자리 4개가 늘었다(빈 3→7)')
100
+ assert.equal(adopted.params.rev, 2)
101
+ })
102
+
103
+ test('사라진 설비를 수로 알린다 — 자리가 수천인 현장에서 직접 세어 보게 하지 않는다', async () => {
104
+ const res = await TwinEngine.ingestMaster(domainId, master(MIRROR, 7, 1)) // 지게차 2 → 1
105
+ const adopted = res.warnings.find((w: any) => w.code === 'structure-adopted')
106
+ assert.equal(adopted.params.equipmentDropped, 1)
107
+
108
+ const inst = await ds.getRepository(TwinInstance).findOne({ where: { instanceId: MIRROR } })
109
+ assert.equal(await ds.getRepository(TwinEquipment).countBy({ instance: { id: inst.id } } as any), 1, '표에서도 사라진다')
110
+ assert.equal(TwinEngine.snapshot(domainId, MIRROR).equipment.length, 1, '커널에서도 사라진다')
111
+ })
112
+
113
+ test('돌고 있는 시뮬레이션은 여전히 거절한다 — 조건이 바뀐 실험은 다른 실험이다', async () => {
114
+ await TwinEngine.bootDeclared(domainId, SIM, 'sim-experiment')
115
+ const before = (await ds.getRepository(TwinStructure).find({ where: { instanceId: SIM } })).length
116
+
117
+ await assert.rejects(() => TwinEngine.ingestMaster(domainId, master(SIM, 9, 2)), /is running/)
118
+
119
+ assert.equal(
120
+ (await ds.getRepository(TwinStructure).find({ where: { instanceId: SIM } })).length,
121
+ before,
122
+ '거절된 인제스트는 흔적을 남기지 않는다'
123
+ )
124
+ })
125
+
126
+ test('멈춘 미러는 구조를 전환하지 않는다 — 정지한 대상을 전환하려 하지 않는다', async () => {
127
+ await TwinEngine.stop(domainId, MIRROR)
128
+ await assert.rejects(
129
+ () => TwinEngine.adoptStructure(domainId, MIRROR, { locations: [], equipment: [] } as any),
130
+ /is not running/,
131
+ '멈춘 트윈은 프로비저닝으로 세우는 것이지 갈아타는 것이 아니다'
132
+ )
133
+ })