@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
@@ -11,7 +11,7 @@
11
11
  import { test } from 'node:test'
12
12
  import assert from 'node:assert/strict'
13
13
 
14
- import { buildEntityDeltas, ITEMS_AGGREGATE_LIMIT, itemsTag, ordersTag, personsTag, tasksTag } from '../server/engine/entity-delta.ts'
14
+ import { buildEntityDeltas, ITEMS_AGGREGATE_LIMIT, itemsTag, ordersTag, personsTag, tasksTag, energyTag } from '../shared/entity-delta.ts'
15
15
 
16
16
  const byTag = (arr: any[], tag: string) => arr.find(x => x.tag === tag)
17
17
 
@@ -180,3 +180,173 @@ test('사람 집약 태그가 항상 나간다 — 선언하지 않은 트윈은
180
180
  const withTwo = buildEntityDeltas({ persons: [{ id: 'w1' }, { id: 'w2' }] }, 't1')
181
181
  assert.equal(withTwo.find(d => d.tag === personsTag('t1'))!.data.persons.length, 2)
182
182
  })
183
+
184
+ /*
185
+ * ── 에너지 (EMS 6단계) ──────────────────────────────────────────────────────
186
+ *
187
+ * 계측은 `state.energy` 에 산다 — 자원 축이 아니다. 이 매핑이 없으면 보드에 놓은 계량기가 값을
188
+ * 받지 못하고 **영원히 빈 모습**이 된다(컴포넌트는 있는데 데이터가 없는, 눈에 안 보이는 결함).
189
+ */
190
+ test('계량 지점의 마지막 관측이 그 설비 태그로 나간다', () => {
191
+ const deltas = buildEntityDeltas(
192
+ {
193
+ locations: [],
194
+ equipment: [],
195
+ energy: {
196
+ points: [
197
+ { id: 'meter-incoming', kW: 1200.5, kWh: 98_000, powerFactor: 0.95, atMs: Date.parse('2026-08-14T09:01:00.000Z'), samplesInWindow: 3 },
198
+ { id: 'meter-quiet', samplesInWindow: 0 }
199
+ ],
200
+ closed: [],
201
+ closedTotal: 0
202
+ }
203
+ },
204
+ 'ems-1'
205
+ )
206
+ const m = deltas.find(d => d.tag === 'meter-incoming')!
207
+ assert.equal(m.data.entity, 'meter')
208
+ assert.equal(m.data.kW, 1200.5)
209
+ assert.equal(m.data.measuredAt, '2026-08-14T09:01:00.000Z', '시각을 함께 낸다 — 화면이 계측 끊김을 스스로 안다')
210
+
211
+ /* 값이 없는 지점: 키를 넣지 않는다(0 으로 만들면 「전기를 안 썼다」가 된다). */
212
+ const q = deltas.find(d => d.tag === 'meter-quiet')!
213
+ assert.equal('kW' in q.data, false)
214
+ assert.equal(q.data.samplesInWindow, 0, '받은 표본 수는 사실이다')
215
+ })
216
+
217
+ test('수요 구간·피크·계약은 집약 태그로 나간다 — 개체의 사실이 아니다', () => {
218
+ const deltas = buildEntityDeltas(
219
+ {
220
+ locations: [],
221
+ equipment: [],
222
+ energy: {
223
+ points: [],
224
+ contractKW: 4000,
225
+ open: { startMs: 1, endMs: 2, samples: 1, maxKW: 4200 },
226
+ peakSince: { kW: 4200, windowStartMs: 1 },
227
+ closed: Array.from({ length: 20 }, (_, i) => ({ startMs: i, endMs: i + 1, samples: 1, maxKW: 100 + i })),
228
+ closedTotal: 137
229
+ }
230
+ },
231
+ 'ems-1'
232
+ )
233
+ const agg = deltas.find(d => d.tag === energyTag('ems-1'))!
234
+ assert.ok(agg, '집약 태그가 있어야 한다')
235
+ assert.equal(agg.data.contractKW, 4000)
236
+ assert.equal(agg.data.peakSince.kW, 4200)
237
+ /* 하루치를 매 방송에 실으면 채널이 그것으로 찬다 — 최근 것만 내고 전체 수를 함께 낸다. */
238
+ assert.equal(agg.data.recent.length, 8)
239
+ assert.equal(agg.data.closedTotal, 137, '자른 사실을 조용히 두지 않는다')
240
+ })
241
+
242
+ test('에너지가 없는 트윈은 에너지 태그를 내지 않는다 — 없는 것을 만들지 않는다', () => {
243
+ const deltas = buildEntityDeltas({ locations: [], equipment: [] }, 'wms-1')
244
+ assert.equal(deltas.some(d => d.tag === energyTag('wms-1')), false)
245
+ assert.equal(deltas.some(d => d.data?.entity === 'meter'), false)
246
+ })
247
+
248
+ test('자리에도 에너지가 실린다 — 능력이 「잰다」고 말하는데 값이 없던 자리', () => {
249
+ const deltas = buildEntityDeltas(
250
+ {
251
+ locations: [
252
+ { id: 'incoming-1', type: 'incoming' },
253
+ { id: 'feeder-a', type: 'feeder' },
254
+ { id: 'feeder-b', type: 'feeder' }
255
+ ],
256
+ equipment: [
257
+ { id: 'm-main', kind: 'meter', homeLocation: 'incoming-1' },
258
+ { id: 'm-a1', kind: 'meter', homeLocation: 'feeder-a' },
259
+ { id: 'm-a2', kind: 'meter', homeLocation: 'feeder-a' }
260
+ ],
261
+ energy: {
262
+ points: [
263
+ { id: 'm-main', kW: 3800, kWh: 1000, atMs: Date.parse('2026-08-14T09:00:00.000Z'), samplesInWindow: 3 },
264
+ { id: 'm-a1', kW: 1000, atMs: Date.parse('2026-08-14T09:01:00.000Z'), samplesInWindow: 2 },
265
+ { id: 'm-a2', kW: 600, atMs: Date.parse('2026-08-14T09:02:00.000Z'), samplesInWindow: 1 }
266
+ ],
267
+ closed: [],
268
+ closedTotal: 0
269
+ }
270
+ },
271
+ 'ems-1'
272
+ )
273
+
274
+ const incoming = deltas.find(d => d.tag === 'incoming-1')!.data
275
+ assert.equal(incoming.entity, 'location', '자리 payload 를 덮지 않고 합친다')
276
+ assert.equal(incoming.kW, 3800)
277
+ assert.deepEqual(incoming.meters, ['m-main'], '어느 계량기에서 온 수인지 되짚을 수 있어야 한다')
278
+
279
+ /* 자리에 계량기가 둘이면 그 자리의 수는 둘의 합이고, 나이는 **가장 최근**이다. */
280
+ const a = deltas.find(d => d.tag === 'feeder-a')!.data
281
+ assert.equal(a.kW, 1600)
282
+ assert.equal(a.samplesInWindow, 3)
283
+ assert.equal(a.measuredAt, '2026-08-14T09:02:00.000Z')
284
+ assert.deepEqual(a.meters, ['m-a1', 'm-a2'])
285
+
286
+ /* 계량기가 없는 자리는 예전 그대로 — 에너지 키를 만들지 않는다(0 은 「안 썼다」가 된다). */
287
+ const b = deltas.find(d => d.tag === 'feeder-b')!.data
288
+ assert.equal('kW' in b, false)
289
+ assert.equal('meters' in b, false)
290
+ })
291
+
292
+ test('자리를 모르는 계량기는 자리에 실리지 않는다 — 설비 태그로만 나간다', () => {
293
+ const deltas = buildEntityDeltas(
294
+ {
295
+ locations: [{ id: 'incoming-1', type: 'incoming' }],
296
+ equipment: [],
297
+ energy: { points: [{ id: 'm-orphan', kW: 500, samplesInWindow: 1 }], closed: [], closedTotal: 0 }
298
+ },
299
+ 'ems-1'
300
+ )
301
+ assert.equal('kW' in deltas.find(d => d.tag === 'incoming-1')!.data, false)
302
+ assert.equal(deltas.find(d => d.tag === 'm-orphan')!.data.kW, 500, '설비 태그로는 나간다')
303
+ })
304
+
305
+ /*
306
+ * ── 능력 넷의 값이 컴포넌트까지 간다 (2026-08-15) ───────────────────────────
307
+ * 보드에 놓인 컴포넌트는 `data(tag)` 로만 값을 받는다. 커널이 값을 들고 있어도 이 페이로드에 실리지
308
+ * 않으면 화면은 영원히 「—」다 — 실제로 그 부류를 두 번 겪었다(스냅샷 투영이 필드를 빠뜨렸고,
309
+ * 값의 시각을 버렸다).
310
+ *
311
+ * 그래서 **설비 태그의 payload** 를 직접 못 박는다: 값과 그 값의 나이가 함께 실려야 한다.
312
+ */
313
+ test('발전·저장·감축·개폐 값과 그 나이가 설비 태그로 나간다', () => {
314
+ const deltas = buildEntityDeltas(
315
+ {
316
+ locations: [{ id: 'incoming-1', type: 'incoming' }],
317
+ equipment: [
318
+ { id: 'pv-1', kind: 'pv-array', homeLocation: 'incoming-1', status: 'idle', generatedKW: 210, exportKW: 40, measuredAt: '2026-08-15T09:00:00.000Z' },
319
+ { id: 'ess-1', kind: 'battery', homeLocation: 'incoming-1', status: 'idle', soc: 62, chargeKW: 0, dischargeKW: 85, measuredAt: '2026-08-15T09:00:00.000Z' },
320
+ { id: 'load-1', kind: 'curtailable-load', homeLocation: 'incoming-1', status: 'busy', curtailable: true, minKW: 30, measuredAt: '2026-08-15T09:00:00.000Z' },
321
+ { id: 'cb-1', kind: 'breaker', homeLocation: 'incoming-1', status: 'idle', position: 'closed', measuredAt: '2026-08-15T09:00:00.000Z' }
322
+ ]
323
+ },
324
+ 'ems-1'
325
+ )
326
+ const by = Object.fromEntries(deltas.map(d => [d.tag, d.data as any]))
327
+
328
+ assert.equal(by['pv-1'].generatedKW, 210)
329
+ assert.equal(by['pv-1'].exportKW, 40)
330
+ assert.equal(by['ess-1'].soc, 62)
331
+ assert.equal(by['ess-1'].dischargeKW, 85)
332
+ assert.equal(by['load-1'].curtailable, true)
333
+ assert.equal(by['load-1'].minKW, 30)
334
+ assert.equal(by['cb-1'].position, 'closed')
335
+ /* 나이가 없으면 화면이 멈춘 값을 지금 값처럼 그린다 — 네 컴포넌트가 모두 이 키를 읽는다. */
336
+ for (const id of ['pv-1', 'ess-1', 'load-1', 'cb-1']) {
337
+ assert.equal(by[id].measuredAt, '2026-08-15T09:00:00.000Z', `${id}: 값의 나이가 함께 가야 한다`)
338
+ }
339
+ })
340
+
341
+ test('값이 없는 설비에는 그 키를 만들지 않는다 — 0 으로 그리면 「안 쓴다」가 된다', () => {
342
+ const deltas = buildEntityDeltas(
343
+ {
344
+ locations: [{ id: 'incoming-1', type: 'incoming' }],
345
+ equipment: [{ id: 'pv-2', kind: 'pv-array', homeLocation: 'incoming-1', status: 'idle' }]
346
+ },
347
+ 'ems-1'
348
+ )
349
+ const pv = deltas.find(d => d.tag === 'pv-2')!.data as any
350
+ assert.equal('generatedKW' in pv, false)
351
+ assert.equal('measuredAt' in pv, false)
352
+ })
@@ -0,0 +1,67 @@
1
+ /*
2
+ * eventTime 이 **날짜 컬럼**이라는 사실을 지킨다.
3
+ *
4
+ * ── 왜 있나 (2026-08-08) ────────────────────────────────────────────────────
5
+ * 예전에는 문자열 컬럼이었고, "UTC ISO 만 들어오니 사전식 비교가 맞는다" 는 전제 위에서 정렬·범위
6
+ * 질의가 돌았다. 그 전제는 컬럼 어디에도 적혀 있지 않았다.
7
+ *
8
+ * 날짜 컬럼으로 바꾸면서 **조용한 함정**이 하나 드러났다: sqlite 는 datetime 을 공백 구분 형식
9
+ * (`2026-08-07 11:00:00.000`)으로 저장하는데, 화면은 ISO 문자열(`…T11:00:00.000Z`)을 보낸다.
10
+ * 문자열 그대로 비교하면 사전식으로 어긋나 **오류 없이 0건**이 나온다 — 원장 시간창이 조용히
11
+ * 비어 버리고, 사용자는 "그 시간에 아무 일도 없었다" 로 읽는다.
12
+ *
13
+ * 그래서 두 가지를 못 박는다: 저장 형식이 실제로 그렇다는 것과, 경계에서 날짜로 옮기면 걸린다는 것.
14
+ * 이 검사가 깨지면 typeorm/드라이버의 거동이 바뀐 것이므로 변환 지점을 다시 봐야 한다.
15
+ */
16
+ import { test } from 'node:test'
17
+ import assert from 'node:assert/strict'
18
+ import { createRequire } from 'node:module'
19
+
20
+ const req = createRequire(import.meta.url)
21
+ const { DataSource, EntitySchema, Between } = req('typeorm')
22
+
23
+ const Ev = new EntitySchema({
24
+ name: 'ev',
25
+ tableName: 'ev',
26
+ columns: {
27
+ id: { primary: true, type: 'int', generated: true },
28
+ eventTime: { type: 'datetime', nullable: true }
29
+ }
30
+ })
31
+
32
+ async function seeded() {
33
+ const ds = new DataSource({ type: 'sqlite', database: ':memory:', entities: [Ev], synchronize: true })
34
+ await ds.initialize()
35
+ const repo = ds.getRepository('ev')
36
+ await repo.save([
37
+ { eventTime: new Date('2026-08-07T11:00:00.000Z') },
38
+ { eventTime: new Date('2026-08-07T13:00:00.000Z') }
39
+ ])
40
+ return { ds, repo }
41
+ }
42
+
43
+ test('sqlite 는 datetime 을 공백 구분 형식으로 저장한다 — ISO 문자열과 사전식으로 어긋난다', async () => {
44
+ const { ds } = await seeded()
45
+ const raw = await ds.query('select eventTime from ev order by id')
46
+ assert.match(raw[0].eventTime, /^\d{4}-\d{2}-\d{2} /, '공백 구분이 아니면 이 검사의 전제가 바뀐 것이다')
47
+ assert.ok(!String(raw[0].eventTime).includes('T'), 'T 가 들어가면 ISO 와 같은 형식이므로 함정이 사라진 것이다')
48
+ await ds.destroy()
49
+ })
50
+
51
+ test('ISO 문자열을 그대로 넘기면 **조용히 0건** — 오류가 아니라 빈 결과라 더 위험하다', async () => {
52
+ const { ds, repo } = await seeded()
53
+ const rows = await repo.find({
54
+ where: { eventTime: Between('2026-08-07T10:00:00.000Z', '2026-08-07T12:00:00.000Z') }
55
+ })
56
+ assert.equal(rows.length, 0, '이 검사가 깨졌다면 드라이버가 문자열을 받아 주게 된 것 — 변환 지점을 재검토할 것')
57
+ await ds.destroy()
58
+ })
59
+
60
+ test('경계에서 날짜로 옮기면 제대로 걸린다 — 질의 전에 반드시 변환한다', async () => {
61
+ const { ds, repo } = await seeded()
62
+ const rows = await repo.find({
63
+ where: { eventTime: Between(new Date('2026-08-07T10:00:00.000Z'), new Date('2026-08-07T12:00:00.000Z')) }
64
+ })
65
+ assert.equal(rows.length, 1)
66
+ await ds.destroy()
67
+ })
@@ -0,0 +1,71 @@
1
+ /*
2
+ * 예측 지표 — **스냅샷에서 무엇을 세는가.**
3
+ *
4
+ * 여기서 틀리면 예측 그래프가 조용히 다른 것을 그린다(오류 없이 곡선이 나온다). 특히 에너지 지표는
5
+ * 「없음」과 「0」이 같은 그림이 되기 쉬워서, 그 경계를 명시로 잡아 둔다.
6
+ */
7
+ import { test } from 'node:test'
8
+ import assert from 'node:assert/strict'
9
+
10
+ import { FORECAST_METRICS, metricsForKind } from '../dist-server/service/twin-forecast/forecast-metrics.js'
11
+
12
+ test('물류 지표 — 기존 여섯은 그대로 센다', () => {
13
+ const s = {
14
+ items: [1, 2, 3],
15
+ locations: [{ occupancy: 2 }, { occupancy: 5 }],
16
+ tasks: [{}, {}],
17
+ orders: [{ status: 'shipped' }, { status: 'picking' }, { status: 'cancelled' }]
18
+ }
19
+ assert.equal(FORECAST_METRICS.items(s), 3)
20
+ assert.equal(FORECAST_METRICS.occupancy(s), 7)
21
+ assert.equal(FORECAST_METRICS.tasks(s), 2)
22
+ assert.equal(FORECAST_METRICS.orders(s), 3)
23
+ assert.equal(FORECAST_METRICS.shipped(s), 1)
24
+ /* 취소는 백로그가 아니다 — 대기와 취소를 같이 세면 「안 빠지는 일감」이 부풀려진다. */
25
+ assert.equal(FORECAST_METRICS.backlog(s), 1)
26
+ })
27
+
28
+ test('에너지 지표 — 커널이 낸 값을 그대로 읽는다(다시 계산하지 않는다)', () => {
29
+ const s = {
30
+ energy: {
31
+ peakSince: { kW: 812 },
32
+ open: { meanKW: 300, projectedKW: 455, projectionBasis: 'mean-so-far' },
33
+ closed: [{ overContract: true }, { overContract: false }, { overContract: true }]
34
+ }
35
+ }
36
+ assert.equal(FORECAST_METRICS.peakKW(s), 812)
37
+ assert.equal(FORECAST_METRICS.projectedKW(s), 455)
38
+ assert.equal(FORECAST_METRICS.overWindows(s), 2)
39
+ })
40
+
41
+ test('투영이 아직 없으면 지금까지의 평균으로 — 0 으로 떨어뜨리지 않는다', () => {
42
+ assert.equal(FORECAST_METRICS.projectedKW({ energy: { open: { meanKW: 120 } } }), 120)
43
+ })
44
+
45
+ test('에너지가 없는 스냅샷 — 세 지표 모두 0(그리지 않을 값이지, 관측한 0 이 아니다)', () => {
46
+ assert.equal(FORECAST_METRICS.peakKW({ items: [] }), 0)
47
+ assert.equal(FORECAST_METRICS.projectedKW({ items: [] }), 0)
48
+ assert.equal(FORECAST_METRICS.overWindows({ items: [] }), 0)
49
+ })
50
+
51
+ test('계약을 모르면 초과 구간은 0 — overContract 가 없는 구간을 초과로 세지 않는다', () => {
52
+ const s = { energy: { closed: [{ maxKW: 900 }, { maxKW: 1200 }] } }
53
+ assert.equal(FORECAST_METRICS.overWindows(s), 0)
54
+ })
55
+
56
+ test('종류별 지표 목록 — 에너지 트윈에는 물류 지표가 섞이지 않는다', () => {
57
+ const ems = metricsForKind('ems')
58
+ assert.deepEqual(ems, ['projectedKW', 'peakKW', 'overWindows'])
59
+ assert.equal(ems.includes('items' as any), false)
60
+ assert.equal(metricsForKind('wms').includes('items'), true)
61
+ /* 종류를 모르면 물류로 — 기존 트윈(kind 미선언)이 지표를 잃지 않는다. */
62
+ assert.equal(metricsForKind(undefined).includes('items'), true)
63
+ })
64
+
65
+ test('목록의 모든 키가 실제로 계산 가능하다 — 화면에 뜨는데 서버가 모르는 지표는 없다', () => {
66
+ for (const kind of ['ems', 'wms', 'mes', 'yms']) {
67
+ for (const key of metricsForKind(kind)) {
68
+ assert.equal(typeof (FORECAST_METRICS as any)[key], 'function', `${kind}:${key}`)
69
+ }
70
+ }
71
+ })
@@ -0,0 +1,42 @@
1
+ /*
2
+ * 예측 틱 크기 — **지평선이 정하고, 양끝은 묶인다.**
3
+ *
4
+ * 이 규칙이 틀리면 예측이 조용히 비싸지거나(1초 고정 시절 9,000틱·28.5초) 조용히 성글어진다
5
+ * (짧은 작업이 한 틱에 삼켜진다). 둘 다 화면에는 그럴듯한 숫자로 나오므로 시험이 지킨다.
6
+ */
7
+ import { test } from 'node:test'
8
+ import assert from 'node:assert/strict'
9
+
10
+ import { forecastStepMs, TARGET_TICKS_PER_RUN, MAX_STEP_MS, MIN_STEP_MS } from '../dist-server/service/twin-forecast/forecast-tuning.js'
11
+
12
+ test('기본 지평선(5분)은 5초 단위 — 실측으로 1초와 답이 같고 5배 싸다', () => {
13
+ assert.equal(forecastStepMs(300_000), 5_000)
14
+ })
15
+
16
+ test('짧은 지평선은 더 잘게 — 1분이면 1초', () => {
17
+ assert.equal(forecastStepMs(60_000), 1_000)
18
+ })
19
+
20
+ test('아주 짧아도 커널 시각 단위 아래로 내려가지 않는다', () => {
21
+ assert.equal(forecastStepMs(5_000), MIN_STEP_MS)
22
+ assert.equal(forecastStepMs(0), MIN_STEP_MS)
23
+ assert.equal(forecastStepMs(Number.NaN), MIN_STEP_MS)
24
+ })
25
+
26
+ test('아무리 길어도 상한을 넘지 않는다 — 짧은 작업이 한 틱에 삼켜진다', () => {
27
+ assert.equal(forecastStepMs(2 * 60 * 60_000), MAX_STEP_MS)
28
+ assert.equal(forecastStepMs(24 * 60 * 60_000), MAX_STEP_MS)
29
+ })
30
+
31
+ test('틱 크기는 초 단위로 떨어진다 — 3,333ms 는 사람도 화면도 설명할 수 없다', () => {
32
+ for (const h of [100_000, 200_000, 333_000, 777_000]) {
33
+ assert.equal(forecastStepMs(h) % 1000, 0, `${h}ms`)
34
+ }
35
+ })
36
+
37
+ test('상한에 걸리지 않는 구간에서는 목표 틱 수 근처로 돈다', () => {
38
+ for (const h of [60_000, 120_000, 300_000, 600_000]) {
39
+ const ticks = h / forecastStepMs(h)
40
+ assert.ok(ticks >= TARGET_TICKS_PER_RUN * 0.8 && ticks <= TARGET_TICKS_PER_RUN * 1.25, `${h}ms → ${ticks}틱`)
41
+ }
42
+ })
@@ -52,11 +52,38 @@ test('learnedCalibration — biasShift=평균오차, 편향 제거로 정확도
52
52
  assert.equal(learn.accAfter, 100, '일정 편향을 완전 제거 → 잔차 0 → 100%')
53
53
  })
54
54
 
55
- test('aggregatePoints — 공동배치 인덱스별 합산(공장 총량)', () => {
56
- const a = [pt(2, 30), pt(4, 30)]
57
- const b = [pt(1, 20), pt(2, 20)]
58
- const agg = aggregatePoints([a, b])
59
- assert.equal(agg.length, 2)
60
- assert.equal(agg[0].actual, 50, '실제 30+20')
61
- assert.equal(agg[0].error, agg[0].actual - agg[0].p50, 'error = 실제−예측(합산 후 재계산)')
55
+ /* 같은 격자 위의 점 — vantage 를 명시한다(예전 헬퍼는 t 를 늘 0 으로 두어 시간축을 전혀 시험하지 않았다). */
56
+ const at = (t: number, error: number, actual = 100) => ({ ...pt(error, actual), t })
57
+
58
+ test('aggregatePoints — 공동배치 합산(공장 총량)', () => {
59
+ const a = [at(10, 2, 30), at(20, 4, 30)]
60
+ const b = [at(10, 1, 20), at(20, 2, 20)]
61
+ const { points, droppedVantages } = aggregatePoints([a, b])
62
+ assert.equal(points.length, 2)
63
+ assert.equal(droppedVantages, 0)
64
+ assert.equal(points[0].actual, 50, '실제 30+20')
65
+ assert.equal(points[0].t, 10, '합산점의 시각은 그 vantage 다')
66
+ assert.equal(points[0].error, points[0].actual - points[0].p50, 'error = 실제−예측(합산 후 재계산)')
67
+ })
68
+
69
+ test('한 트윈이 못 잰 vantage 는 통째로 뺀다 — 빠진 트윈을 0 으로 세지 않는다', () => {
70
+ /*
71
+ * 예전에는 `max` 길이로 돌며 있는 것만 더했다. 그래서 이 경우 t=20 의 공장 총량이 20(b 뿐)으로
72
+ * 나왔다 — a 가 그 시각에 아무것도 안 했다는 뜻이 아니라, **못 쟀다**는 뜻인데도.
73
+ * 공장 합계 곡선이 꼬리에서 아래로 꺾이고, 그 곡선으로 예측 정확도를 판정했다.
74
+ */
75
+ const a = [at(10, 2, 30), null]
76
+ const b = [at(10, 1, 20), at(20, 2, 20)]
77
+ const { points, droppedVantages } = aggregatePoints([a as any, b])
78
+ assert.equal(points.length, 1, '모두가 값을 가진 vantage 만 남는다')
79
+ assert.equal(points[0].t, 10)
80
+ assert.equal(droppedVantages, 1, '뺀 개수를 돌려줘야 화면이 "2점 중 1점" 이라고 말할 수 있다')
81
+ })
82
+
83
+ test('길이가 다르면 뒤쪽은 뺀다 — 남은 트윈만의 합을 공장 총량이라 부르지 않는다', () => {
84
+ const a = [at(10, 2, 30)]
85
+ const b = [at(10, 1, 20), at(20, 2, 20)]
86
+ const { points, droppedVantages } = aggregatePoints([a, b])
87
+ assert.equal(points.length, 1)
88
+ assert.equal(droppedVantages, 1)
62
89
  })
@@ -8,10 +8,10 @@ import { test } from 'node:test'
8
8
  import assert from 'node:assert/strict'
9
9
 
10
10
  import { StateProjector, objectEvent, BIZSTEP, DISP } from '@operato/twin-kernel'
11
- import { buildEntityDeltas } from '../server/engine/entity-delta.ts'
11
+ import { buildEntityDeltas } from '../shared/entity-delta.ts'
12
12
 
13
13
  const T0 = '2026-07-19T00:00:00.000Z'
14
- const board = (nNodes: number) => ({
14
+ const model = (nNodes: number) => ({
15
15
  locations: Array.from({ length: nNodes }, (_, i) => ({ id: `bin-${i}`, type: 'storage', capacity: 100000 })),
16
16
  equipment: []
17
17
  })
@@ -23,7 +23,7 @@ function envelope(epc: string, location: string): any {
23
23
  test('bench: apply 처리량(이벤트 흡수)', () => {
24
24
  const nNodes = 200
25
25
  const M = 200_000
26
- const proj = new StateProjector(board(nNodes))
26
+ const proj = new StateProjector(model(nNodes))
27
27
  const t0 = performance.now()
28
28
  for (let i = 0; i < M; i++) proj.apply(envelope(`urn:epc:id:sscc:0614141.${i}`, `bin-${i % nNodes}`))
29
29
  const dt = performance.now() - t0
@@ -33,7 +33,7 @@ test('bench: apply 처리량(이벤트 흡수)', () => {
33
33
 
34
34
  test('bench: snapshot 비용 vs item 수 (전체 O(N) 재구성)', () => {
35
35
  for (const N of [1000, 10000, 50000]) {
36
- const proj = new StateProjector(board(200))
36
+ const proj = new StateProjector(model(200))
37
37
  for (let i = 0; i < N; i++) proj.apply(envelope(`urn:epc:id:sscc:0614141.${i}`, `bin-${i % 200}`))
38
38
  const iters = 200
39
39
  const t0 = performance.now()
@@ -45,7 +45,7 @@ test('bench: snapshot 비용 vs item 수 (전체 O(N) 재구성)', () => {
45
45
 
46
46
  test('bench: buildEntityDeltas 비용 vs 엔티티 수 (전 엔티티 매번 방출)', () => {
47
47
  for (const N of [100, 1000, 5000]) {
48
- const proj = new StateProjector(board(N))
48
+ const proj = new StateProjector(model(N))
49
49
  for (let i = 0; i < N * 5; i++) proj.apply(envelope(`urn:epc:id:sscc:0614141.${i}`, `bin-${i % N}`))
50
50
  const snap = proj.snapshot()
51
51
  const iters = 200
@@ -58,7 +58,7 @@ test('bench: buildEntityDeltas 비용 vs 엔티티 수 (전 엔티티 매번 방
58
58
 
59
59
  test('bench: 방송 1회 총비용(snapshot + buildEntityDeltas) vs 상태 규모', () => {
60
60
  for (const [locations, items] of [[200, 5000], [1000, 50000], [2000, 100000]]) {
61
- const proj = new StateProjector(board(locations))
61
+ const proj = new StateProjector(model(locations))
62
62
  for (let i = 0; i < items; i++) proj.apply(envelope(`urn:epc:id:sscc:0614141.${i}`, `bin-${i % locations}`))
63
63
  const iters = 100
64
64
  const t0 = performance.now()
@@ -0,0 +1,136 @@
1
+ /*
2
+ * ★ 가동 중 인제스트 — **거절할 것이면 아무것도 쓰기 전에 거절한다.**
3
+ *
4
+ * 가동 중 재프로비저닝은 `provision` 이 막는다. 그런데 그 호출은 `ingestMaster` 의 **맨 끝**이었고,
5
+ * 그때까지 공간·표현·구역(TwinArea)은 이미 저장된 뒤였다. 사용자 화면에는 "인제스트 실패" 가
6
+ * 뜨는데 실제로는 절반이 적용된 상태다 — **실패라고 말하면서 데이터를 바꾸는 것**이 가장 나쁘다.
7
+ *
8
+ * 이 검사는 그 경계 하나를 지킨다: 거절된 인제스트는 **흔적을 남기지 않는다.**
9
+ *
10
+ * 순수 함수로는 확인되지 않는다. 부분 쓰기는 정의상 "여러 표에 걸쳐 무엇이 남았나" 이므로 실
11
+ * DataSource 가 있어야 한다(멀티DB 규약상 sqlite 가 5개 드라이버 공통의 최소선이다).
12
+ *
13
+ * 이 제약 자체는 최종형이 아니다 — 커널에는 가동 중 구조 교체(`adoptStructure`)가 이미 있고
14
+ * 재생은 리비전 경계마다 그것을 쓴다. 라이브에 배선되면 이 거절은 사라지고 이 검사도 함께 간다.
15
+ */
16
+ import { test, before, after } from 'node:test'
17
+ import assert from 'node:assert/strict'
18
+ import { createRequire } from 'node:module'
19
+ import type { DataSource } from 'typeorm'
20
+
21
+ const req = createRequire(import.meta.url)
22
+ const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
23
+ const { entities: authEntities } = req('@things-factory/auth-base')
24
+ const { entities: twinEntities } = req('../dist-server/service/index.js')
25
+ const { TwinEngine } = req('../dist-server/engine/index.js')
26
+ const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js')
27
+ const { TwinStructure } = req('../dist-server/service/twin-structure/twin-structure.js')
28
+ const { TwinArea } = req('../dist-server/service/twin-space/twin-area.js')
29
+ const { TwinSpace } = req('../dist-server/service/twin-space/twin-space.js')
30
+ const { TwinLocation } = req('../dist-server/service/twin-model/twin-location.js')
31
+ const { DataSource: TypeOrmDataSource } = req('typeorm')
32
+
33
+ let ds: DataSource
34
+ let domainId: string
35
+
36
+ const INSTANCE = 'guard-probe'
37
+
38
+ /** WMS 마스터 — `bins` 로 규모를, `siteName`·구역 이름으로 "무엇이 바뀌려 했나" 를 다이얼한다. */
39
+ const master = (bins: number, siteName: string, areaName: string): any => ({
40
+ source: INSTANCE,
41
+ system: 'wms',
42
+ siteName,
43
+ space: { width: 60, depth: 30, unit: 'm' },
44
+ locations: [
45
+ { id: 'a-dc', type: 'warehouse', role: 'area', name: areaName },
46
+ { id: 'dock-recv', type: 'dock', role: 'location', name: '입고 도크', capacity: 100 },
47
+ ...Array.from({ length: bins }, (_, i) => ({
48
+ id: `bin-${i + 1}`,
49
+ type: 'storage',
50
+ role: 'location',
51
+ name: `빈${i + 1}`,
52
+ parentId: 'a-dc',
53
+ capacity: 20
54
+ }))
55
+ ],
56
+ equipment: [{ id: 'fk-1', kind: 'forklift', homeLocationId: 'dock-recv' }]
57
+ })
58
+
59
+ /** 지금 DB 에 남아 있는 모든 흔적 — 인제스트가 건드리는 표를 한 줌으로 뜬다. */
60
+ async function trace() {
61
+ const inst = await ds.getRepository(TwinInstance).findOne({ where: { instanceId: INSTANCE } })
62
+ const space = await ds.getRepository(TwinSpace).findOne({ where: { spaceId: `${INSTANCE}-site` } })
63
+ const areas = await ds.getRepository(TwinArea).find({ order: { areaId: 'ASC' } })
64
+ const revs = await ds.getRepository(TwinStructure).find({ order: { rev: 'ASC' } })
65
+ return {
66
+ boardLocations: (inst?.model?.locations ?? []).length,
67
+ spaceName: space?.name,
68
+ areaNames: areas.map((a: any) => a.name),
69
+ revs: revs.map((r: any) => r.rev),
70
+ rows: await ds.getRepository(TwinLocation).count()
71
+ }
72
+ }
73
+
74
+ before(async () => {
75
+ ds = new TypeOrmDataSource({
76
+ type: 'sqlite',
77
+ database: ':memory:',
78
+ synchronize: true,
79
+ dropSchema: true,
80
+ namingStrategy: new NamingStrategy(),
81
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
82
+ logging: false
83
+ })
84
+ await ds.initialize()
85
+ addDataSource('default', ds)
86
+ const d = await ds.getRepository(Domain).save({ name: 'guard-test', subdomain: 'guard-test' } as any)
87
+ domainId = d.id
88
+
89
+ await TwinEngine.ingestMaster(domainId, master(3, '서울 창고', '보관 구역'))
90
+ })
91
+
92
+ after(async () => {
93
+ await TwinEngine.stop(domainId, INSTANCE).catch(() => {})
94
+ await ds?.destroy()
95
+ })
96
+
97
+ test('멈춰 있으면 재인제스트가 통한다 — 막는 것은 가동 중일 때뿐이다', async () => {
98
+ await TwinEngine.ingestMaster(domainId, master(5, '서울 창고', '보관 구역'))
99
+ const t = await trace()
100
+ assert.equal(t.boardLocations, 6, '자리 6개(도크 1 + 빈 5)로 갱신된다')
101
+ assert.deepEqual(t.revs, [1, 2], '구조가 달라졌으니 리비전이 하나 늘어난다')
102
+ assert.equal(t.rows, 6, '표도 함께 다시 그려진다')
103
+ })
104
+
105
+ test('가동 중이면 거절하고 — **아무것도 바꾸지 않는다**', async () => {
106
+ await TwinEngine.startFromRegistry(domainId, INSTANCE, 'sim-experiment')
107
+ const before = await trace()
108
+
109
+ /*
110
+ * 바뀔 것을 **일부러 전부** 흔든다. 구조(빈 5→9)뿐 아니라 공간 이름·구역 이름까지 —
111
+ * 부분 쓰기는 앞쪽(공간·구역)에서 일어났으므로, 구조만 확인하면 예전 결함을 못 잡는다.
112
+ */
113
+ await assert.rejects(
114
+ () => TwinEngine.ingestMaster(domainId, master(9, '부산 창고', '다른 구역')),
115
+ /is running/,
116
+ '가동 중에는 거절해야 한다'
117
+ )
118
+
119
+ assert.deepEqual(await trace(), before, '거절된 인제스트는 어느 표에도 흔적을 남기지 않는다')
120
+ })
121
+
122
+ test('멈추면 다시 통한다 — 거절은 잠금이 아니라 순서다', async () => {
123
+ await TwinEngine.stop(domainId, INSTANCE)
124
+ await TwinEngine.ingestMaster(domainId, master(9, '부산 창고', '다른 구역'))
125
+
126
+ const t = await trace()
127
+ assert.equal(t.boardLocations, 10, '아까 거절당한 그 구조가 이제 들어간다')
128
+ assert.deepEqual(t.revs, [1, 2, 3])
129
+ assert.equal(t.rows, 10)
130
+ /*
131
+ * 공간 이름은 **안 바뀐다** — 이미 있는 공간의 이름은 인제스트가 덮지 않는다(공유 공간에서
132
+ * 먼저 쓴 값이 이긴다). 거절 검사가 "안 바뀌었다" 를 확인할 때 이 성질에 기대지 않도록,
133
+ * 여기서 그것이 별개의 규칙임을 못 박아 둔다.
134
+ */
135
+ assert.equal(t.spaceName, '서울 창고', '기존 공간 이름은 재인제스트가 덮지 않는다(별개 규칙)')
136
+ })
@@ -7,7 +7,7 @@
7
7
  import { test } from 'node:test'
8
8
  import assert from 'node:assert/strict'
9
9
 
10
- import { resolveIngestSpace, collidingIds, INGEST_SPACE_NOT_FOUND } from '../server/service/reference/ingest-space.ts'
10
+ import { resolveIngestSpace, refuseImportSpace, collidingIds, INGEST_SPACE_NOT_FOUND } from '../server/service/reference/ingest-space.ts'
11
11
 
12
12
  test('고르지 않으면 마스터가 파생한 공간으로 — 지금까지의 거동을 바꾸지 않는다', () => {
13
13
  for (const req of [undefined, null, '', ' ']) {
@@ -48,3 +48,31 @@ test('같은 id 를 쓰는 것들을 찾아낸다 — 합쳐지는 것을 모르
48
48
  assert.deepEqual(collidingIds(undefined, undefined), [], '없는 쪽은 빈 것으로 본다')
49
49
  assert.deepEqual(collidingIds([{}, { id: '' }], [{ id: '' }]), [], 'id 없는 항목은 세지 않는다')
50
50
  })
51
+
52
+ /* ── 실 시스템 import 의 현장 선택 (2026-08-13) ───────────────────────────── */
53
+
54
+ test('현장을 고르지 않으면 사이트가 몇이든 진행한다 — 지금까지의 거동', () => {
55
+ for (const n of [0, 1, 5, 40]) {
56
+ assert.equal(refuseImportSpace(n, undefined), undefined)
57
+ assert.equal(refuseImportSpace(n, ''), undefined)
58
+ }
59
+ })
60
+
61
+ test('사이트 하나면 그 현장에 넣는다 — 렌즈를 더하는 정상 경로', () => {
62
+ assert.equal(refuseImportSpace(1, 'rosarito-site'), undefined)
63
+ })
64
+
65
+ test('사이트 여럿을 한 현장에 넣는 것은 거절한다 — 서로 다른 현장이 조용히 합쳐진다', () => {
66
+ const r = refuseImportSpace(3, 'rosarito-site')
67
+ assert.ok(r, '거절해야 한다')
68
+ assert.equal(r.code, 'space-with-multiple-sites')
69
+ assert.equal(r.params?.count, 3)
70
+ assert.equal(r.params?.spaceId, 'rosarito-site')
71
+ assert.match(r.message, /pick one site/)
72
+ })
73
+
74
+ test('거절 사유는 코드로 낸다 — 화면이 다섯 언어로 번역할 수 있게', () => {
75
+ const r = refuseImportSpace(2, ' rosarito-site ')
76
+ assert.equal(r?.code, 'space-with-multiple-sites')
77
+ assert.equal(r?.params?.spaceId, 'rosarito-site', '앞뒤 공백은 다듬어 낸다')
78
+ })