@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
@@ -552,7 +552,7 @@ test('방향은 지표의 성질이다 — 사용자가 반대로 적을 수 없
552
552
  assert.equal(faster.met, true, '실제 30초 ≤ 목표 60초')
553
553
  })
554
554
 
555
- test('미달을 미달이라 말한다', () => {
555
+ test('미달을 미달로 판정한다', () => {
556
556
  const kpi = kpiOf([...doneAtHour('t1', 7)])
557
557
  const [v] = judgeTargets(kpi, [{ metric: 'throughput.tasks', target: 5 }])
558
558
  assert.equal(v.met, false)
@@ -572,7 +572,7 @@ test('목표가 없으면 판정하지 않는다', () => {
572
572
  assert.deepEqual(judgeTargets(kpiOf([...doneAtHour('t1', 7)]), undefined), [])
573
573
  })
574
574
 
575
- test('값이 없는 지표는 판정하지 않는다 — 0 으로 놓으면 반대로 거짓말한다', () => {
575
+ test('값이 없는 지표는 판정하지 않는다 — 0 으로 놓으면 판정이 반대로 뒤집힌다', () => {
576
576
  const empty = foldKpi([], DAY)
577
577
  /* 이 테스트가 실제로 결함을 잡았다: 분위수는 표본이 없으면 p50Ms: 0 으로 나온다. 그것을 값으로 읽으면
578
578
  시간 지표는 "낮을수록 좋다" 라서 **일이 없던 구간이 목표를 완벽히 달성한 것으로** 보고된다 —
@@ -696,3 +696,394 @@ test('축 항목별로 판정한다 — 값이 없는 지표는 판정하지 않
696
696
  '대기시간은 표본이 없어 판정 안 함 · 자원 점유는 축 단위로 뜻이 없어 판정 안 함'
697
697
  )
698
698
  })
699
+
700
+ /*
701
+ * ── 에너지 원단위 (§10 7단계) ───────────────────────────────────────────────
702
+ *
703
+ * 원단위는 분자(전력량)와 분모(산출)를 **같은 창**에서 나눈 값이다. 그래서 별도 질의가 아니라 이 폴드
704
+ * 안에 있다 — 창이 하나뿐이면 어긋날 수 없다.
705
+ *
706
+ * 분자는 저널의 **구간 마감 사건**에서 온다(커널 상태가 아니라 저널이므로 멈춘 트윈·과거 구간에도
707
+ * 답한다). 그 값이 파생임을 `basis` 가 말하고, 못 센 구간 수를 함께 낸다.
708
+ */
709
+ const T = Date.parse('2026-08-14T00:00:00.000Z')
710
+ const q = 15 * 60_000
711
+ const win = (startMs: number, endMs: number, meanKW?: number) => ({
712
+ eventType: 'energy.demand.window',
713
+ eventTime: new Date(endMs).toISOString(),
714
+ payload: { data: { startMs, endMs, ...(meanKW !== undefined ? { meanKW } : {}) } }
715
+ })
716
+ const orderDone = (atMs: number) => ({
717
+ eventType: 'order.status',
718
+ eventTime: new Date(atMs).toISOString(),
719
+ payload: { data: { status: 'completed' } }
720
+ })
721
+
722
+ test('에너지 사건이 없으면 에너지 블록을 내지 않는다 — 없는 것을 만들지 않는다', () => {
723
+ const r = foldKpi([orderDone(T + 60_000)], { fromMs: T, toMs: T + q })
724
+ assert.equal(r.energy, undefined)
725
+ })
726
+
727
+ test('구간 마감에서 전력량과 대당 원단위를 낸다 — 같은 창이므로 어긋날 수 없다', () => {
728
+ const r = foldKpi(
729
+ [win(T, T + q, 400), win(T + q, T + 2 * q, 400), orderDone(T + 60_000), orderDone(T + q + 60_000)],
730
+ { fromMs: T, toMs: T + 2 * q }
731
+ )
732
+ assert.equal(r.energy?.kWh, 200, '400kW × 0.25h × 2구간')
733
+ assert.equal(r.energy?.basis, 'mean-kw', '파생임을 밝힌다')
734
+ assert.equal(r.energy?.counted, 2)
735
+ assert.equal(r.energy?.intensity.value, 100, '200kWh ÷ 완료 2건')
736
+ assert.equal(r.energy?.intensity.unit, 'kWh/unit')
737
+ })
738
+
739
+ test('그 창에 완료가 없으면 원단위는 정의되지 않는다 — 전력량은 그대로 낸다', () => {
740
+ const r = foldKpi([win(T, T + q, 400)], { fromMs: T, toMs: T + q })
741
+ assert.equal(r.energy?.kWh, 100, '전력량은 사실이다')
742
+ assert.equal(r.energy?.intensity.value, null)
743
+ assert.equal(r.energy?.intensity.reason, 'zero-denominator', '무한으로 내지 않는다')
744
+ })
745
+
746
+ test('계측이 끊긴 구간은 세지 않고 몇 개를 못 셌는지 말한다', () => {
747
+ const r = foldKpi([win(T, T + q, 400), win(T + q, T + 2 * q), orderDone(T + 60_000)], { fromMs: T, toMs: T + 2 * q })
748
+ assert.equal(r.energy?.counted, 1)
749
+ assert.equal(r.energy?.skipped, 1, '조용히 빼면 이 합이 그 창의 전부인 줄 읽는다')
750
+ })
751
+
752
+ test('창에 걸친 구간은 세지 않는다 — 부분을 비례로 자르면 추정이 하나 더 생긴다', () => {
753
+ const r = foldKpi([win(T, T + q, 400), win(T + q, T + 2 * q, 400)], { fromMs: T, toMs: T + q + 60_000 })
754
+ assert.equal(r.energy?.counted, 1)
755
+ assert.equal(r.energy?.kWh, 100)
756
+ })
757
+
758
+ /*
759
+ * ── 창의 최대수요 (2026-08-17) ──────────────────────────────────────────────
760
+ *
761
+ * 전력량은 요금의 사용량 부분이고, 최대수요는 **기본요금과 계약 위반**을 정한다. 평균으로 갈음하면
762
+ * 계약을 넘긴 창이 「안 넘겼다」로 보고된다 — 15분 평균 400kW 인 창의 최대가 1,200kW 일 수 있다.
763
+ */
764
+ const winMax = (startMs: number, endMs: number, meanKW: number, maxKW: number) => ({
765
+ eventType: 'energy.demand.window',
766
+ eventTime: new Date(endMs).toISOString(),
767
+ payload: { data: { startMs, endMs, meanKW, maxKW } }
768
+ })
769
+
770
+ test('창의 최대수요는 구간 최대들 중 가장 큰 값이다 — 평균으로 갈음하지 않는다', () => {
771
+ const r = foldKpi([winMax(T, T + q, 400, 620), winMax(T + q, T + 2 * q, 400, 1180)], { fromMs: T, toMs: T + 2 * q })
772
+ assert.equal(r.energy?.kWh, 200)
773
+ assert.equal(r.energy?.peakKW, 1180)
774
+ assert.equal(r.energy?.peakStartMs, T + q, '언제였는지도 답할 수 있어야 한다')
775
+ })
776
+
777
+ test('구간이 최대를 싣지 않으면 최대수요는 없다 — 0 도 평균도 아니다', () => {
778
+ const r = foldKpi([win(T, T + q, 400)], { fromMs: T, toMs: T + q })
779
+ assert.equal(r.energy?.kWh, 100)
780
+ assert.equal(r.energy?.peakKW, undefined)
781
+ })
782
+
783
+ /*
784
+ * ── 에너지 목표 (2026-08-17) ────────────────────────────────────────────────
785
+ *
786
+ * 값은 폴드가 내고 있었는데 목표를 걸 수 있는 어휘에 없었다 — 그래서 화면은 「1,240kWh」를 보이면서도
787
+ * 그것이 계약 안쪽인지 말하지 못했다. 방향은 셋 다 「낮을수록 좋다」로 못 박혀 있다.
788
+ */
789
+ test('최대수요 목표 — 계약 상한을 넘겼는지 판정한다', () => {
790
+ const kpi = foldKpi([winMax(T, T + q, 400, 1180)], { fromMs: T, toMs: T + q })
791
+ const [v] = judgeTargets(kpi, [{ metric: 'energy.peakKW', target: 800 }])
792
+ assert.equal(v.direction, 'lower', '많이 쓸수록 좋은 에너지 목표는 없다')
793
+ assert.equal(v.actual, 1180)
794
+ assert.equal(v.met, false, '계약 800kW 를 넘겼다')
795
+ })
796
+
797
+ test('전력량·원단위에도 목표를 건다', () => {
798
+ const kpi = foldKpi([win(T, T + q, 400), orderDone(T + 60_000)], { fromMs: T, toMs: T + q })
799
+ const [kwh] = judgeTargets(kpi, [{ metric: 'energy.kWh', target: 120 }])
800
+ assert.equal(kwh.actual, 100)
801
+ assert.equal(kwh.met, true)
802
+ const [per] = judgeTargets(kpi, [{ metric: 'energy.intensity', target: 80 }])
803
+ assert.equal(per.actual, 100, '100kWh ÷ 완료 1건')
804
+ assert.equal(per.met, false)
805
+ })
806
+
807
+ test('에너지를 못 잰 창은 판정하지 않는다 — 0 으로 읽으면 안 쓴 것이 만점이 된다', () => {
808
+ const kpi = foldKpi([orderDone(T + 60_000)], { fromMs: T, toMs: T + q })
809
+ assert.equal(judgeTargets(kpi, [{ metric: 'energy.kWh', target: 120 }]).length, 0)
810
+ assert.equal(judgeTargets(kpi, [{ metric: 'energy.peakKW', target: 800 }]).length, 0)
811
+ /* 원단위는 분모가 없어 `null` 로 온다 — 그것도 판정 대상이 아니다. */
812
+ const noOrders = foldKpi([win(T, T + q, 400)], { fromMs: T, toMs: T + q })
813
+ assert.equal(noOrders.energy?.intensity.value, null)
814
+ assert.equal(judgeTargets(noOrders, [{ metric: 'energy.intensity', target: 80 }]).length, 0)
815
+ })
816
+
817
+ /*
818
+ * ── 분기별 몫 (2026-08-17) ──────────────────────────────────────────────────
819
+ *
820
+ * 저널의 에너지 사실은 **현장 총량 한 줄**이다. 그래서 자리별 몫은 잰 것이 아니라 **나눈 것**이고,
821
+ * 그 사실을 값과 함께 내야 한다. 이 검사가 지키는 것: 합 보존 · 근거 표시 · 모르는 자리를 지어내지
822
+ * 않기 · 선언이 없으면 분해하지 않기.
823
+ */
824
+ const runs = (taskId: string, resource: string, fromSec: number, toSec: number) => [
825
+ { eventType: 'task.status', eventTime: new Date(T + fromSec * 1000).toISOString(), payload: { data: { taskId, status: 'in-progress', resourceRef: resource } } },
826
+ { eventType: 'task.status', eventTime: new Date(T + toSec * 1000).toISOString(), payload: { data: { taskId, status: 'completed', resourceRef: resource } } }
827
+ ]
828
+
829
+ test('분기별 몫 — 가동시간에 비례해 나누고, 합은 총량을 지킨다', () => {
830
+ /* press 300초 · weld 100초 → 3:1. 총량 100kWh. */
831
+ const events = [win(T, T + q, 400), ...runs('t1', 'press-1', 0, 300), ...runs('t2', 'weld-1', 0, 100)]
832
+ const r = foldKpi(events, { fromMs: T, toMs: T + q }, { resourceFeeder: { 'press-1': 'feeder-press', 'weld-1': 'feeder-weld' } })
833
+ assert.equal(r.energy?.kWh, 100)
834
+ const by = r.energy?.byFeeder ?? []
835
+ assert.equal(by.length, 2)
836
+ const press = by.find(x => x.key === 'feeder-press')!
837
+ const weld = by.find(x => x.key === 'feeder-weld')!
838
+ assert.ok(Math.abs(press.kWh - 75) < 1e-6, `press ${press.kWh}`)
839
+ assert.ok(Math.abs(weld.kWh - 25) < 1e-6, `weld ${weld.kWh}`)
840
+ /* 합 보존 — 이것이 깨지면 어딘가에서 전기가 사라지거나 생겨난 것이고, 그 위 모든 원단위가 거짓이다. */
841
+ const sum = by.reduce((a, x) => a + x.kWh, 0) + (r.energy?.unattributedKWh ?? 0)
842
+ assert.ok(Math.abs(sum - 100) < 1e-6, `합 ${sum}`)
843
+ })
844
+
845
+ test('몫에는 근거가 붙는다 — 여럿을 나눈 것이면 배분이라고 말한다', () => {
846
+ const events = [win(T, T + q, 400), ...runs('t1', 'press-1', 0, 300), ...runs('t2', 'weld-1', 0, 100)]
847
+ const r = foldKpi(events, { fromMs: T, toMs: T + q }, { resourceFeeder: { 'press-1': 'feeder-press', 'weld-1': 'feeder-weld' } })
848
+ const row = r.energy!.byFeeder![0]
849
+ assert.equal(row.basis, 'apportioned')
850
+ assert.equal(row.weightKind, 'runtimeMs')
851
+ assert.ok(row.weightShare > 0)
852
+ })
853
+
854
+ /*
855
+ * 선언된 부하가 **하나뿐인** 현장 — 커널은 「그 계량기는 그것만 잰다」고 읽는다(모델이 그렇게 말했다).
856
+ * 우리가 그것을 배분으로 낙인찍지 않는다: 처음에는 하드코딩으로 그렇게 했고, 그건 커널이 판정한 것을
857
+ * 화면에서 바꿔 말하는 짓이었다. 반대로 「돌아간 자원만 덮는다」고 거짓 선언을 넘기면 여럿인 현장에서도
858
+ * 이 경로로 빠져 배분이 측정으로 보고된다 — 그것이 실제로 처음의 결함이었다.
859
+ */
860
+ test('선언된 부하가 하나면 커널의 판정을 그대로 옮긴다', () => {
861
+ const events = [win(T, T + q, 400), ...runs('t1', 'press-1', 0, 300)]
862
+ const r = foldKpi(events, { fromMs: T, toMs: T + q }, { resourceFeeder: { 'press-1': 'feeder-press' } })
863
+ assert.equal(r.energy!.byFeeder![0].basis, 'measured')
864
+ })
865
+
866
+ test('돌지 않은 설비는 0kWh 를 받지 않고, 몇 대가 빠졌는지 센다', () => {
867
+ const events = [win(T, T + q, 400), ...runs('t1', 'press-1', 0, 300)]
868
+ const r = foldKpi(events, { fromMs: T, toMs: T + q }, {
869
+ resourceFeeder: { 'press-1': 'feeder-press', 'weld-1': 'feeder-weld', 'paint-1': 'feeder-paint' }
870
+ })
871
+ const keys = (r.energy!.byFeeder ?? []).map(x => x.key)
872
+ assert.deepEqual(keys, ['feeder-press'], '가동 기록이 없는 자리는 행을 만들지 않는다(0 을 지어내지 않는다)')
873
+ assert.equal(r.energy!.excludedResources, 2, '빠진 설비 수를 밝힌다')
874
+ })
875
+
876
+ test('자리를 모르는 자원은 unknown 으로 모인다 — 빼지도, 아무 분기에 넣지도 않는다', () => {
877
+ const events = [win(T, T + q, 400), ...runs('t1', 'press-1', 0, 200), ...runs('t2', 'ghost-1', 0, 200)]
878
+ const r = foldKpi(events, { fromMs: T, toMs: T + q }, { resourceFeeder: { 'press-1': 'feeder-press' } })
879
+ const by = r.energy!.byFeeder!
880
+ assert.ok(by.some(x => x.key === 'unknown'), '모르는 자리를 지어내지 않는다')
881
+ const sum = by.reduce((a, x) => a + x.kWh, 0)
882
+ assert.ok(Math.abs(sum - 100) < 1e-6, '모르는 몫까지 합해 총량을 지킨다')
883
+ })
884
+
885
+ test('선언이 없으면 분해하지 않는다 — 자리를 모르는 채 나눈 숫자는 아무 것의 몫도 아니다', () => {
886
+ const events = [win(T, T + q, 400), ...runs('t1', 'press-1', 0, 300)]
887
+ const r = foldKpi(events, { fromMs: T, toMs: T + q })
888
+ assert.equal(r.energy?.byFeeder, undefined)
889
+ assert.equal(r.energy?.kWh, 100, '총량은 그대로 낸다')
890
+ })
891
+
892
+ test('그 창에 아무도 돌지 않았으면 나누지 않고 이유를 낸다', () => {
893
+ /* 부하가 둘 이상 선언된 현장 — 나눌 대상은 있는데 몫이 없다. */
894
+ const r = foldKpi([win(T, T + q, 400)], { fromMs: T, toMs: T + q }, {
895
+ resourceFeeder: { 'press-1': 'feeder-press', 'weld-1': 'feeder-weld' }
896
+ })
897
+ assert.equal(r.energy?.byFeeder, undefined, '가동 기록이 없으면 나눌 근거가 없다')
898
+ assert.equal(r.energy?.unattributedReason, 'no-weights')
899
+ assert.equal(r.energy?.unattributedKWh, 100, '나누지 못한 양은 그대로 남는다(0 으로 만들지 않는다)')
900
+ assert.equal(r.energy?.kWh, 100)
901
+ })
902
+
903
+ /*
904
+ * ── 금액 (2026-08-17) ───────────────────────────────────────────────────────
905
+ *
906
+ * 트윈은 「최대수요 328kW」까지 말하고 멈춰 있었다. 피크를 깎는 일이 돈이 되는 이유가 요금의 구조에
907
+ * 있으므로(사용량 + 기본요금) 단가를 선언하면 금액이 나온다. 계산은 커널이 하고 폴드는 나른다 —
908
+ * 이 검사는 **같은 창의 값에서 나오는지**와 **단가가 없을 때 지어내지 않는지**를 본다.
909
+ */
910
+ test('단가를 선언하면 이 창의 금액이 나온다 — 전력량과 최대수요 각각에서', () => {
911
+ const r = foldKpi([winMax(T, T + q, 400, 1000)], { fromMs: T, toMs: T + q }, {
912
+ tariff: { energyChargePerKWh: 0.1, demandChargePerKW: 20, currency: 'USD' }
913
+ })
914
+ assert.equal(r.energy?.kWh, 100)
915
+ assert.equal(r.energy?.peakKW, 1000)
916
+ const c: any = r.energy?.cost
917
+ assert.equal(c.energyCharge, 10, '100kWh × 0.1')
918
+ assert.equal(c.demandCharge, 20_000, '1,000kW × 20')
919
+ assert.equal(c.currency, 'USD')
920
+ assert.equal(c.demandBasis, 'window-peak-as-period-peak', '기본요금의 조건이 값과 함께 온다')
921
+ })
922
+
923
+ test('단가를 선언하지 않으면 금액을 만들지 않는다', () => {
924
+ const r = foldKpi([winMax(T, T + q, 400, 1000)], { fromMs: T, toMs: T + q })
925
+ assert.equal(r.energy?.cost, undefined)
926
+ })
927
+
928
+ test('최대수요를 못 잰 창은 기본요금을 지어내지 않는다', () => {
929
+ const r = foldKpi([win(T, T + q, 400)], { fromMs: T, toMs: T + q }, { tariff: { demandChargePerKW: 20, energyChargePerKWh: 0.1 } })
930
+ const c: any = r.energy?.cost
931
+ assert.equal(c.energyCharge, 10)
932
+ assert.equal(c.demandCharge, undefined)
933
+ })
934
+
935
+ test('에너지 사실이 없는 창에는 금액도 없다 — 0 원이라고 말하지 않는다', () => {
936
+ const r = foldKpi([orderDone(T + 1000)], { fromMs: T, toMs: T + q }, { tariff: { demandChargePerKW: 20 } })
937
+ assert.equal(r.energy, undefined)
938
+ })
939
+
940
+ /*
941
+ * ── 깎인 양 (2026-08-17) ────────────────────────────────────────────────────
942
+ *
943
+ * 요금은 계통 수요로 매겨지고, 그 수요를 만든 것은 설비 부하다. 두 수의 간격이 곧 태양광·배터리가
944
+ * 덜어낸 몫이다 — 그것이 피크 셰이빙의 값이다. **같은 구간의 두 수**여야 차이가 뜻을 갖는다.
945
+ */
946
+ const winNet = (startMs: number, endMs: number, meanKW: number, maxKW: number, grossMaxKW?: number) => ({
947
+ eventType: 'energy.demand.window',
948
+ eventTime: new Date(endMs).toISOString(),
949
+ payload: { data: { startMs, endMs, meanKW, maxKW, ...(grossMaxKW !== undefined ? { grossMaxKW } : {}) } }
950
+ })
951
+
952
+ test('피크 구간의 총부하와 깎인 양을 낸다', () => {
953
+ const r = foldKpi([winNet(T, T + q, 300, 400, 600), winNet(T + q, T + 2 * q, 500, 900, 1200)], { fromMs: T, toMs: T + 2 * q })
954
+ assert.equal(r.energy?.peakKW, 900)
955
+ assert.equal(r.energy?.peakGrossKW, 1200)
956
+ assert.equal(r.energy?.shavedKW, 300, '1,200 − 900 — 그 순간 계통에서 덜어낸 몫')
957
+ })
958
+
959
+ test('깎인 것이 없으면 그 값을 만들지 않는다 — 0 을 성과로 적지 않는다', () => {
960
+ const r = foldKpi([winNet(T, T + q, 300, 400, 400)], { fromMs: T, toMs: T + q })
961
+ assert.equal(r.energy?.peakGrossKW, 400)
962
+ assert.equal(r.energy?.shavedKW, undefined)
963
+ })
964
+
965
+ test('총부하를 싣지 않은 옛 구간은 그대로 — 순수요와 같다고 단정하지 않는다', () => {
966
+ const r = foldKpi([winMax(T, T + q, 400, 900)], { fromMs: T, toMs: T + q })
967
+ assert.equal(r.energy?.peakKW, 900)
968
+ assert.equal(r.energy?.peakGrossKW, undefined)
969
+ assert.equal(r.energy?.shavedKW, undefined)
970
+ })
971
+
972
+ /*
973
+ * ── 분기 계량이 있으면 배분하지 않는다 (2026-08-18) ───────────────────────────
974
+ * 실 게이트웨이는 분기마다 계량기를 보낸다. 그런데 화면은 「나누지 못함」으로 답했다 — 폴드에 가동시간
975
+ * 배분 경로만 있었기 때문이다. 미러 트윈에는 설비 가동 기록조차 없어(원천이 계량만 준다) 그 경로로는
976
+ * 분기별 값이 영원히 나오지 않았다.
977
+ */
978
+ const windowFact = (startMs: number, points: { id: string; kWh: number; root?: boolean }[], meanKW = 400) => ({
979
+ eventType: 'energy.demand.window',
980
+ eventTime: new Date(startMs + 900_000).toISOString(),
981
+ instanceId: 'ems-1',
982
+ payload: {
983
+ data: {
984
+ startMs,
985
+ endMs: startMs + 900_000,
986
+ meanKW,
987
+ maxKW: meanKW,
988
+ samples: 30,
989
+ points: points.map(p => ({ id: p.id, kWh: p.kWh, ...(p.root ? { root: true } : {}) }))
990
+ }
991
+ }
992
+ })
993
+
994
+ test('분기별 값이 계량에서 온다 — 근거는 measured, 가동시간 이야기를 하지 않는다', () => {
995
+ const t0 = Date.parse('2026-08-18T00:00:00.000Z')
996
+ const r = foldKpi([windowFact(t0, [
997
+ { id: 'meter-incoming', kWh: 100, root: true },
998
+ { id: 'meter-a', kWh: 40 },
999
+ { id: 'meter-b', kWh: 30 }
1000
+ ]) as any], { fromMs: t0, toMs: t0 + 900_000 }, {
1001
+ meterFeeder: { 'meter-a': 'feeder-a', 'meter-b': 'feeder-b' }
1002
+ })
1003
+ const e = r.energy!
1004
+ const byKey = new Map((e.byFeeder ?? []).map(f => [f.key, f]))
1005
+ assert.equal(byKey.get('feeder-a')?.kWh, 40)
1006
+ assert.equal(byKey.get('feeder-a')?.basis, 'measured')
1007
+ assert.equal(byKey.get('feeder-a')?.weightKind, undefined, '나누지 않았으니 무엇으로 나눴다고 말하지 않는다')
1008
+ assert.equal(byKey.get('feeder-b')?.kWh, 30)
1009
+ })
1010
+
1011
+ test('하위 계량이 덮지 않는 나머지를 남긴다 — 어느 분기에 얹지 않는다', () => {
1012
+ const t0 = Date.parse('2026-08-18T01:00:00.000Z')
1013
+ /* 구간 평균 400kW × 0.25h = 100kWh 가 총량이고, 분기 합은 70 이다. */
1014
+ const r = foldKpi([windowFact(t0, [{ id: 'meter-a', kWh: 40 }, { id: 'meter-b', kWh: 30 }]) as any],
1015
+ { fromMs: t0, toMs: t0 + 900_000 }, { meterFeeder: { 'meter-a': 'feeder-a', 'meter-b': 'feeder-b' } })
1016
+ const e = r.energy!
1017
+ assert.equal(Math.round(e.kWh!), 100)
1018
+ assert.equal(Math.round(e.unattributedKWh!), 30)
1019
+ assert.equal(e.unattributedReason, 'not-submetered', '「선언이 없다」가 아니라 「계량이 덮지 않는다」')
1020
+ const sum = (e.byFeeder ?? []).reduce((s, f) => s + f.kWh, 0) + (e.unattributedKWh ?? 0)
1021
+ assert.ok(Math.abs(sum - e.kWh!) < 1e-6, `합이 보존된다(${sum} vs ${e.kWh})`)
1022
+ })
1023
+
1024
+ test('분기 합이 뿌리를 넘으면 그 사실을 낸다 — 깎아 맞추지 않는다', () => {
1025
+ const t0 = Date.parse('2026-08-18T02:00:00.000Z')
1026
+ const r = foldKpi([windowFact(t0, [{ id: 'meter-a', kWh: 90 }, { id: 'meter-b', kWh: 30 }]) as any],
1027
+ { fromMs: t0, toMs: t0 + 900_000 }, { meterFeeder: { 'meter-a': 'feeder-a', 'meter-b': 'feeder-b' } })
1028
+ const e = r.energy!
1029
+ assert.equal(Math.round(e.submeterExcessKWh!), 20, '120 − 100')
1030
+ assert.equal(e.byFeeder?.find(f => f.key === 'feeder-a')?.kWh, 90, '계량값은 그대로 남는다')
1031
+ })
1032
+
1033
+ test('계량 지도가 없으면 예전 배분 경로 그대로 — 거동을 바꾸지 않는다', () => {
1034
+ const t0 = Date.parse('2026-08-18T03:00:00.000Z')
1035
+ const r = foldKpi([windowFact(t0, [{ id: 'meter-a', kWh: 40 }]) as any],
1036
+ { fromMs: t0, toMs: t0 + 900_000 }, { resourceFeeder: { 'press-1': 'feeder-a', 'press-2': 'feeder-b' } })
1037
+ const e = r.energy!
1038
+ /* 선언된 부하가 둘인데 가동 기록이 없으니 나눌 몫이 없다 — 총량은 그대로 남고 이유가 붙는다(예전 거동). */
1039
+ assert.equal(e.unattributedReason, 'no-weights')
1040
+ assert.equal(e.byFeeder, undefined, '계량 경로를 타지 않았다')
1041
+ })
1042
+
1043
+ test('피크를 만든 분기를 그 순간의 kW 로 답한다 — 전력량 큰 분기와 다를 수 있다', () => {
1044
+ const t0 = Date.parse('2026-08-18T06:00:00.000Z')
1045
+ const t1 = t0 + 900_000
1046
+ /* 두 구간: 앞 구간이 전력량은 크지만(a 가 꾸준히), 피크는 뒤 구간에서 b 가 만든다. */
1047
+ const w0 = {
1048
+ eventType: 'energy.demand.window',
1049
+ eventTime: new Date(t1).toISOString(),
1050
+ instanceId: 'ems-1',
1051
+ payload: { data: { startMs: t0, endMs: t1, meanKW: 400, maxKW: 500, samples: 30,
1052
+ points: [{ id: 'meter-a', kWh: 80 }, { id: 'meter-b', kWh: 20 }],
1053
+ pointsAtPeak: [{ id: 'meter-a', kW: 450 }, { id: 'meter-b', kW: 50 }] } }
1054
+ }
1055
+ const w1 = {
1056
+ eventType: 'energy.demand.window',
1057
+ eventTime: new Date(t1 + 900_000).toISOString(),
1058
+ instanceId: 'ems-1',
1059
+ payload: { data: { startMs: t1, endMs: t1 + 900_000, meanKW: 300, maxKW: 900, samples: 30,
1060
+ points: [{ id: 'meter-a', kWh: 40 }, { id: 'meter-b', kWh: 35 }],
1061
+ pointsAtPeak: [{ id: 'meter-a', kW: 300 }, { id: 'meter-b', kW: 600 }] } }
1062
+ }
1063
+ const r = foldKpi([w0, w1] as any, { fromMs: t0, toMs: t1 + 900_000 }, {
1064
+ meterFeeder: { 'meter-a': 'feeder-a', 'meter-b': 'feeder-b' }
1065
+ })
1066
+ const byKey = new Map((r.energy?.byFeeder ?? []).map(f => [f.key, f]))
1067
+ assert.equal(byKey.get('feeder-a')?.kWh, 120, '전력량은 a 가 크다')
1068
+ /* 피크 값은 **최대수요를 낸 구간 하나**에서 온다 — 두 구간을 섞으면 다른 순간의 값을 더하는 것이다. */
1069
+ assert.equal(byKey.get('feeder-b')?.peakKW, 600, '피크를 만든 것은 b 다')
1070
+ assert.equal(byKey.get('feeder-a')?.peakKW, 300)
1071
+ })
1072
+
1073
+ test('피크 구간에 표본이 없던 분기는 피크 값을 만들지 않는다', () => {
1074
+ const t0 = Date.parse('2026-08-18T07:00:00.000Z')
1075
+ const w = {
1076
+ eventType: 'energy.demand.window',
1077
+ eventTime: new Date(t0 + 900_000).toISOString(),
1078
+ instanceId: 'ems-1',
1079
+ payload: { data: { startMs: t0, endMs: t0 + 900_000, meanKW: 200, maxKW: 300, samples: 10,
1080
+ points: [{ id: 'meter-a', kWh: 40 }, { id: 'meter-b', kWh: 10 }],
1081
+ pointsAtPeak: [{ id: 'meter-a', kW: 300 }] } }
1082
+ }
1083
+ const r = foldKpi([w] as any, { fromMs: t0, toMs: t0 + 900_000 }, {
1084
+ meterFeeder: { 'meter-a': 'feeder-a', 'meter-b': 'feeder-b' }
1085
+ })
1086
+ const byKey = new Map((r.energy?.byFeeder ?? []).map(f => [f.key, f]))
1087
+ assert.equal(byKey.get('feeder-a')?.peakKW, 300)
1088
+ assert.equal(byKey.get('feeder-b')?.peakKW, undefined, '그 순간 값을 모르면 만들지 않는다')
1089
+ })
@@ -0,0 +1,130 @@
1
+ /*
2
+ * 구간 성과 질의 비용 측정 — **방송이냐 질의냐를 숫자로 정한다.**
3
+ *
4
+ * ── 왜 재는가 (2026-08-06) ─────────────────────────────────────────────────
5
+ * 성과 카드를 여러 단계(공간·트윈·구역·자리·설비)에 붙이려 한다. 두 방법이 있다.
6
+ *
7
+ * · 방송 — 서버가 주기마다 전 단계를 계산해 밀어 준다. 자리가 트윈당 최대 200개고 지표가 6개라
8
+ * 태그가 1,200개가 된다. **모델에 카드를 하나도 안 놓아도 계산은 돈다.**
9
+ * · 질의 — 카드가 필요할 때 묻는다. **모델에 놓인 카드 수만큼만** 든다. 시각(`toTime`)도 물을 수 있어
10
+ * 타임라인과 맞고, 공간 단위 합산(여러 트윈을 한 번에 접기)은 질의에만 있다.
11
+ *
12
+ * 질의로 정하려면 **한 번의 비용**을 알아야 한다. 카드 20장이 30초마다 물으면 감당되는지가 결정을
13
+ * 가른다. 짐작하지 않고 잰다.
14
+ *
15
+ * 빌드 산출물을 실행한다 — **소스를 고친 뒤에는 `npm run build:server` 를 먼저 돌린다.**
16
+ */
17
+ import { test, before, after } from 'node:test'
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 { computeTwinKpi } = req('../dist-server/engine/index.js')
26
+ const { TwinEvent } = req('../dist-server/service/twin-event/twin-event.js')
27
+ const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js')
28
+ const { DataSource: TypeOrmDataSource } = req('typeorm')
29
+
30
+ let ds: DataSource
31
+ let domainId: string
32
+
33
+ const INSTANCE = 'kpi-bench'
34
+ const TO = Date.parse('2026-03-10T10:00:00.000Z')
35
+ const WINDOW_MIN = 60
36
+
37
+ /** 자리 200개 — 실측한 최대 규모(트윈당 자리 최대 200, 평균 19). */
38
+ const LOCATIONS = 200
39
+ /** 창 안의 완료 작업 수 — 한 건이 두 줄(시작·완료)이라 이벤트는 두 배가 된다. */
40
+ const TASK_COUNTS = [500, 2_000, 10_000]
41
+
42
+ function taskRows(count: number, revBase: number) {
43
+ const rows: any[] = []
44
+ for (let i = 0; i < count; i++) {
45
+ const done = TO - 60_000 - (i % (WINDOW_MIN * 60)) * 1000
46
+ const started = done - 30_000
47
+ const at = `loc-${i % LOCATIONS}`
48
+ rows.push(
49
+ {
50
+ domain: { id: domainId },
51
+ instanceId: INSTANCE,
52
+ revision: revBase + i * 2,
53
+ eventType: 'task.status',
54
+ eventTime: new Date(started).toISOString(),
55
+ payload: { data: { taskId: `t-${i}`, status: 'in-progress', resourceRef: 'fk-1', locationRef: at } }
56
+ },
57
+ {
58
+ domain: { id: domainId },
59
+ instanceId: INSTANCE,
60
+ revision: revBase + i * 2 + 1,
61
+ eventType: 'task.status',
62
+ eventTime: new Date(done).toISOString(),
63
+ payload: { data: { taskId: `t-${i}`, status: 'completed', resourceRef: 'fk-1', locationRef: at } }
64
+ }
65
+ )
66
+ }
67
+ return rows
68
+ }
69
+
70
+ before(async () => {
71
+ ds = new TypeOrmDataSource({
72
+ type: 'sqlite',
73
+ database: ':memory:',
74
+ synchronize: true,
75
+ dropSchema: true,
76
+ namingStrategy: new NamingStrategy(),
77
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
78
+ logging: false
79
+ })
80
+ await ds.initialize()
81
+ addDataSource('default', ds)
82
+ const d = await ds.getRepository(Domain).save({ name: 'kpi-bench', subdomain: 'kpi-bench' } as any)
83
+ domainId = d.id
84
+
85
+ /* 자리 계층 — 자리 200개를 구역 4개에 나눠 붙인다(구역 단위 접기 비용도 재려면 부모가 있어야 한다). */
86
+ const locations = Array.from({ length: LOCATIONS }, (_, i) => ({
87
+ id: `loc-${i}`,
88
+ type: 'storage',
89
+ capacity: 10,
90
+ parentId: `area-${i % 4}`
91
+ }))
92
+ await ds.getRepository(TwinInstance).save({
93
+ domain: { id: domainId },
94
+ instanceId: INSTANCE,
95
+ kind: 'wms',
96
+ realityMode: 'sim-experiment', /* 선언은 이제 필수 — 비워 두면 읽는 자리마다 기본값을 고른다 */
97
+ purpose: 'operational',
98
+ status: 'stopped',
99
+ model: { locations, equipment: [{ id: 'fk-1', kind: 'forklift', homeLocation: 'loc-0' }] }
100
+ } as any)
101
+ })
102
+
103
+ after(async () => {
104
+ await ds?.destroy()
105
+ })
106
+
107
+ const ms = async (fn: () => Promise<any>) => {
108
+ const t = process.hrtime.bigint()
109
+ await fn()
110
+ return Number(process.hrtime.bigint() - t) / 1e6
111
+ }
112
+
113
+ test('bench: 구간 성과 폴드 한 번의 비용 vs 창 안의 작업 수', async () => {
114
+ let seeded = 0
115
+ for (const count of TASK_COUNTS) {
116
+ await ds.getRepository(TwinEvent).save(taskRows(count - seeded, seeded * 2 + 1000))
117
+ seeded = count
118
+
119
+ const args = { domainId, instanceId: INSTANCE, toTime: new Date(TO).toISOString(), windowMinutes: WINDOW_MIN }
120
+ await computeTwinKpi(args) // 워밍업(연결·스키마 캐시)
121
+
122
+ const plain = await ms(() => computeTwinKpi(args))
123
+ const byArea = await ms(() => computeTwinKpi({ ...args, groupBy: 'area' }))
124
+
125
+ console.log(` 작업 ${count.toLocaleString()}건(이벤트 ${(count * 2).toLocaleString()}줄) · 자리 ${LOCATIONS}개`)
126
+ console.log(` 폴드 1회 ${plain.toFixed(1)}ms`)
127
+ console.log(` 구역별 폴드 1회 ${byArea.toFixed(1)}ms`)
128
+ console.log(` 카드 20장 동시 ${(plain * 20).toFixed(0)}ms (직렬 상한 — 실제로는 한 번 접어 나눠 쓸 수 있다)`)
129
+ }
130
+ })