@things-factory/headless-twin 10.0.9 → 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 (353) 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 +125 -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 +46 -14
  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 +112 -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 +52 -15
  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 +150 -0
  263. package/server/service/twin-model/twin-lineage-query.ts +193 -0
  264. package/server/service/twin-model/twin-location.ts +140 -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 +150 -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/command-routing.test.ts +1 -1
  286. package/test/declaration-reaches-model.test.ts +178 -0
  287. package/test/discovery-result.test.ts +1 -1
  288. package/test/energy-topology.test.ts +113 -0
  289. package/test/entity-delta.test.ts +171 -1
  290. package/test/event-time-column.test.ts +67 -0
  291. package/test/forecast-metrics.test.ts +71 -0
  292. package/test/forecast-tuning.test.ts +42 -0
  293. package/test/gap-analytics.test.ts +34 -7
  294. package/test/ingest-bench.test.ts +6 -6
  295. package/test/ingest-running-guard.test.ts +136 -0
  296. package/test/ingest-space.test.ts +29 -1
  297. package/test/instance-cache-lifecycle.test.ts +110 -0
  298. package/test/kernel-kind-guard.test.ts +96 -0
  299. package/test/kpi-baseline-db.test.ts +3 -1
  300. package/test/kpi-baseline.test.ts +3 -3
  301. package/test/kpi-fold.test.ts +393 -2
  302. package/test/kpi-query-bench.test.ts +5 -3
  303. package/test/lineage-survives-restart.test.ts +203 -0
  304. package/test/live-feed-registry.test.ts +67 -0
  305. package/test/live-kernel-facts.test.ts +161 -0
  306. package/test/live-mirror-parity.test.ts +22 -15
  307. package/test/load-meter.test.ts +314 -0
  308. package/test/local-declarations.test.ts +554 -0
  309. package/test/master-to-twin.test.ts +121 -33
  310. package/test/model-basis.test.ts +56 -1
  311. package/test/{board-vocabulary.test.ts → model-vocabulary.test.ts} +25 -25
  312. package/test/move-space.test.ts +149 -0
  313. package/test/mutation-gate.test.ts +19 -5
  314. package/test/oee-accumulator.test.ts +26 -7
  315. package/test/operation-basis.test.ts +100 -0
  316. package/test/operations-capability-db.test.ts +165 -0
  317. package/test/project-structure-db.test.ts +226 -0
  318. package/test/projection-reaches-screen.test.ts +272 -0
  319. package/test/property-effects.test.ts +96 -0
  320. package/test/resync-origin-site.test.ts +77 -0
  321. package/test/scale-twin-bench.test.ts +5 -5
  322. package/test/snapshot-freshness.test.ts +60 -0
  323. package/test/space-integrity.test.ts +223 -0
  324. package/test/standard-coverage.test.ts +94 -0
  325. package/test/state-axes.test.ts +1 -1
  326. package/test/streamline-e2e.test.ts +14 -14
  327. package/test/structure-revision-db.test.ts +11 -11
  328. package/test/tenant-registry-db.test.ts +5 -5
  329. package/test/twin-audit.test.ts +83 -0
  330. package/test/twin-model-item-db.test.ts +275 -0
  331. package/test/twin-model-tree-db.test.ts +176 -0
  332. package/test/twin-origin-resync.test.ts +114 -0
  333. package/test/warm-start-seam.test.ts +6 -6
  334. package/test/warm-start.test.ts +88 -3
  335. package/tsconfig.json +6 -1
  336. package/tsconfig.shared.json +23 -0
  337. package/tsconfig.shared.tsbuildinfo +1 -0
  338. package/tsconfig.tsbuildinfo +1 -0
  339. package/dist-server/engine/board-vocabulary.js.map +0 -1
  340. package/dist-server/engine/entity-delta.js.map +0 -1
  341. package/dist-server/engine/kpi-broadcast.js.map +0 -1
  342. package/dist-server/engine/twin-level.js.map +0 -1
  343. package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
  344. package/dist-server/service/twin-event/backfill-keys.js +0 -63
  345. package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
  346. package/dist-server/tsconfig.tsbuildinfo +0 -1
  347. package/server/service/twin-event/backfill-keys.ts +0 -72
  348. /package/{dist-server/engine → dist-shared}/kpi-broadcast.d.ts +0 -0
  349. /package/{dist-server/engine → dist-shared}/kpi-broadcast.js +0 -0
  350. /package/{dist-server/engine → dist-shared}/twin-level.d.ts +0 -0
  351. /package/{dist-server/engine → dist-shared}/twin-level.js +0 -0
  352. /package/{server/engine → shared}/kpi-broadcast.ts +0 -0
  353. /package/{server/engine → shared}/twin-level.ts +0 -0
@@ -45,6 +45,8 @@ export const TASKS_TAG_PREFIX = '__tasks__'
45
45
  export const ITEMS_TAG_PREFIX = '__items__'
46
46
  export const PERSONS_TAG_PREFIX = '__persons__'
47
47
  export const ASSETS_TAG_PREFIX = '__assets__'
48
+ /** 에너지 집약 — 수요 구간·피크·계약은 **트윈 하나의 사실**이라 개체 태그가 없다. */
49
+ export const ENERGY_TAG_PREFIX = '__energy__'
48
50
 
49
51
  /** 이 트윈의 집약 채널 태그. 트윈 식별자가 없으면 만들지 않는다 — 덮어쓰기의 원인이었다. */
50
52
  export function aggregateTag(prefix: string, instanceId: string): string {
@@ -58,6 +60,7 @@ export const tasksTag = (instanceId: string) => aggregateTag(TASKS_TAG_PREFIX, i
58
60
  export const itemsTag = (instanceId: string) => aggregateTag(ITEMS_TAG_PREFIX, instanceId)
59
61
  export const personsTag = (instanceId: string) => aggregateTag(PERSONS_TAG_PREFIX, instanceId)
60
62
  export const assetsTag = (instanceId: string) => aggregateTag(ASSETS_TAG_PREFIX, instanceId)
63
+ export const energyTag = (instanceId: string) => aggregateTag(ENERGY_TAG_PREFIX, instanceId)
61
64
 
62
65
  /**
63
66
  * 통과시키지 않는 필드 — **빼는 데는 이유가 필요하다.**
@@ -124,11 +127,58 @@ function aggregateItem(entity: any, idKey: 'id' | 'epc'): any {
124
127
  */
125
128
  export const ITEMS_AGGREGATE_LIMIT = 500
126
129
 
130
+ /**
131
+ * 자리별 에너지 — **그 자리의 계량기들이 잰 것**을 그 자리 태그에 실을 수 있게 모은다.
132
+ *
133
+ * ── 왜 자리에도 필요한가 (2026-08-14) ───────────────────────────────────────
134
+ * 카탈로그는 자리에도 `metered` 를 준다(수전·분기·구역 계량은 재는 자리다). 그런데 값은 계량기
135
+ * **설비** 태그로만 나갔다. 그래서 계통도의 분기 자리에 계량 컴포넌트를 놓으면 **영원히 「—」** 였다 —
136
+ * 능력은 「잰다」고 말하는데 값이 오지 않는 자리.
137
+ *
138
+ * 「이 구간이 얼마를 먹나」는 자리의 사실이고, 사람은 계통도의 자리에 붙여 본다.
139
+ *
140
+ * ── 계층은 여기서 문제가 되지 않는다 ────────────────────────────────────────
141
+ * 각 자리에는 **그 자리에 붙은 계량기만** 더한다(수전 자리에는 수전 계량기, 분기 자리에는 그 분기의
142
+ * 것). 커널이 부하를 뿌리로만 세는 것과 다른 층의 계산이다 — 여기서는 자리마다 자기 것을 말한다.
143
+ *
144
+ * 근거를 함께 낸다(`meters`) — 그 자리의 수가 어느 계량기에서 왔는지 되짚을 수 있어야 한다.
145
+ */
146
+ function energyByLocation(state: any): Map<string, Record<string, unknown>> {
147
+ const out = new Map<string, Record<string, unknown>>()
148
+ const points = state?.energy?.points
149
+ if (!Array.isArray(points) || !points.length) return out
150
+
151
+ /* 계량기 → 자리. 자리를 모르는 계량기는 자리 payload 에 실을 데가 없다(설비 태그로만 나간다). */
152
+ const locOf = new Map<string, string>()
153
+ for (const e of state?.equipment ?? []) if (e?.id && e?.homeLocation) locOf.set(e.id, e.homeLocation)
154
+
155
+ for (const pt of points) {
156
+ const loc = pt?.id ? locOf.get(pt.id) : undefined
157
+ if (!loc) continue
158
+ const acc = out.get(loc) ?? { meters: [] as string[], samplesInWindow: 0 }
159
+ ;(acc.meters as string[]).push(pt.id)
160
+ acc.samplesInWindow = Number(acc.samplesInWindow ?? 0) + (Number(pt.samplesInWindow) || 0)
161
+ /* 값은 **있는 것만** 더한다 — 하나도 없으면 키를 만들지 않는다(0 은 「안 썼다」가 된다). */
162
+ if (Number.isFinite(Number(pt.kW))) acc.kW = Number(acc.kW ?? 0) + Number(pt.kW)
163
+ if (Number.isFinite(Number(pt.kWh))) acc.kWh = Number(acc.kWh ?? 0) + Number(pt.kWh)
164
+ /* 나이는 **가장 최근**을 쓴다 — 자리에 계량기가 여럿이면 그중 마지막 관측이 그 자리의 나이다. */
165
+ if (pt.atMs !== undefined) {
166
+ const iso = new Date(Number(pt.atMs)).toISOString()
167
+ if (!acc.measuredAt || String(acc.measuredAt) < iso) acc.measuredAt = iso
168
+ }
169
+ out.set(loc, acc)
170
+ }
171
+ return out
172
+ }
173
+
127
174
  export function buildEntityDeltas(state: any, instanceId: string): EntityDelta[] {
128
175
  const out: EntityDelta[] = []
176
+ const locEnergy = energyByLocation(state)
129
177
 
130
178
  for (const n of state?.locations ?? []) {
131
- out.push({ tag: n.id, data: payloadOf('location', n) })
179
+ /* 에너지는 자리 payload 에 **합친다** — 같은 태그로 두 번 보내면 뒤엣것이 앞을 덮는다. */
180
+ const energy = locEnergy.get(n.id)
181
+ out.push({ tag: n.id, data: energy ? { ...payloadOf('location', n), ...energy } : payloadOf('location', n) })
132
182
  }
133
183
  for (const m of state?.equipment ?? []) {
134
184
  /* Mobile(location/motion) + Operable(status/held) + Processable(oee) — 한 payload 로.
@@ -187,5 +237,50 @@ export function buildEntityDeltas(state: any, instanceId: string): EntityDelta[]
187
237
  }
188
238
  })
189
239
 
240
+ /*
241
+ * ── 에너지 (EMS) ─────────────────────────────────────────────────────────
242
+ *
243
+ * 계측은 `state.energy` 에 산다 — 자원 축(`equipment`·`locations`)이 아니다. 그래서 이 매핑이 없으면
244
+ * 보드에 놓은 계량기가 **아무 값도 받지 못한다**(컴포넌트는 있는데 영원히 빈 모습).
245
+ *
246
+ * 두 갈래로 낸다:
247
+ * · **개체별** — 계량 지점 하나하나의 마지막 관측(kW·kWh·역률·시각). 태그는 그 설비 id 다.
248
+ * · **집약** — 수요 구간·피크·계약전력. 이것은 트윈 하나의 사실이라 개체에 실을 수 없다.
249
+ *
250
+ * 값이 없으면 **키를 넣지 않는다**(0 으로 만들지 않는다) — 「못 읽었다」와 「0 이었다」는 다른 사실이고,
251
+ * 그 구별이 이 트윈의 값어치다.
252
+ */
253
+ const energy = state?.energy
254
+ if (energy) {
255
+ for (const pt of energy.points ?? []) {
256
+ if (!pt?.id) continue
257
+ out.push({
258
+ tag: pt.id,
259
+ data: {
260
+ entity: 'meter',
261
+ ...(pt.kW !== undefined ? { kW: pt.kW } : {}),
262
+ ...(pt.kWh !== undefined ? { kWh: pt.kWh } : {}),
263
+ ...(pt.powerFactor !== undefined ? { powerFactor: pt.powerFactor } : {}),
264
+ /* 마지막 표본 시각 — 계측이 끊긴 것을 화면이 스스로 알 수 있게(우리가 판정하지 않는다). */
265
+ ...(pt.atMs !== undefined ? { measuredAt: new Date(pt.atMs).toISOString() } : {}),
266
+ samplesInWindow: pt.samplesInWindow ?? 0
267
+ }
268
+ })
269
+ }
270
+ out.push({
271
+ tag: energyTag(instanceId),
272
+ data: {
273
+ entity: 'energy',
274
+ ...(energy.contractKW !== undefined ? { contractKW: energy.contractKW } : {}),
275
+ ...(energy.open ? { open: energy.open } : {}),
276
+ ...(energy.peakSince ? { peakSince: energy.peakSince } : {}),
277
+ /* 마감된 구간은 **최근 것만** 낸다 — 하루치를 매 방송에 실으면 채널이 그것으로 찬다.
278
+ 전체 수는 함께 낸다(조용히 자르지 않는다). */
279
+ recent: (energy.closed ?? []).slice(-8),
280
+ closedTotal: energy.closedTotal ?? 0
281
+ }
282
+ })
283
+ }
284
+
190
285
  return out
191
286
  }
@@ -0,0 +1,133 @@
1
+ /*
2
+ * ★ 돌고 있는 미러에 새 구조를 넣는다 — **미러는 멈출 수 없다.**
3
+ *
4
+ * 미러가 비추는 현실은 24시간 돈다. 설비 한 대가 늘었다고 트윈을 멈췄다 세우면 그 사이의
5
+ * 사실을 잃고, 라이브에서 유실은 곧 거짓이다. 그래서 미러는 **돌면서** 갈아탄다.
6
+ *
7
+ * 갈아타기가 옳으려면 **세 층이 함께** 움직여야 한다. 하나라도 빠지면 조용히 어긋난다:
8
+ * ① 리비전 — 이후의 이벤트가 새 번호를 달고 다닌다(재생이 마디를 나눌 수 있게)
9
+ * ② 저장 model·구조 행 — 조회가 새 구조를 본다
10
+ * ③ 커널 — 지금 접히는 이벤트가 새 구조 위에서 접힌다
11
+ *
12
+ * 커널 단위 거동(무엇을 지키고 무엇을 버리나)은 커널 쪽 `adopt-structure.test.ts` 가 지킨다.
13
+ * 여기서 지키는 것은 **호스트 배선** — 세 층이 같은 인제스트에서 함께 움직이는가.
14
+ *
15
+ * 실 DataSource 로 돈다. 부분 적용은 정의상 "여러 표에 걸쳐 무엇이 남았나" 이므로.
16
+ */
17
+ import { test, before, after } from 'node:test'
18
+ import assert from 'node:assert/strict'
19
+ import { createRequire } from 'node:module'
20
+ import type { DataSource } from 'typeorm'
21
+
22
+ const req = createRequire(import.meta.url)
23
+ const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
24
+ const { entities: authEntities } = req('@things-factory/auth-base')
25
+ const { entities: twinEntities } = req('../dist-server/service/index.js')
26
+ const { TwinEngine } = req('../dist-server/engine/index.js')
27
+ const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js')
28
+ const { TwinStructure } = req('../dist-server/service/twin-structure/twin-structure.js')
29
+ const { TwinLocation } = req('../dist-server/service/twin-model/twin-location.js')
30
+ const { TwinEquipment } = req('../dist-server/service/twin-model/twin-equipment.js')
31
+ const { DataSource: TypeOrmDataSource } = req('typeorm')
32
+
33
+ let ds: DataSource
34
+ let domainId: string
35
+
36
+ const MIRROR = 'mirror-probe'
37
+ const SIM = 'sim-probe'
38
+
39
+ const master = (source: string, bins: number, forklifts: number): any => ({
40
+ source,
41
+ system: 'wms',
42
+ siteName: '미러 창고',
43
+ space: { width: 60, depth: 30, unit: 'm' },
44
+ locations: [
45
+ { id: 'dock-recv', type: 'dock', role: 'location', name: '입고 도크', capacity: 100 },
46
+ ...Array.from({ length: bins }, (_, i) => ({ id: `bin-${i + 1}`, type: 'storage', role: 'location', name: `빈${i + 1}`, capacity: 20 }))
47
+ ],
48
+ equipment: Array.from({ length: forklifts }, (_, i) => ({ id: `fk-${i + 1}`, kind: 'forklift', homeLocationId: 'dock-recv' }))
49
+ })
50
+
51
+ before(async () => {
52
+ ds = new TypeOrmDataSource({
53
+ type: 'sqlite',
54
+ database: ':memory:',
55
+ synchronize: true,
56
+ dropSchema: true,
57
+ namingStrategy: new NamingStrategy(),
58
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
59
+ logging: false
60
+ })
61
+ await ds.initialize()
62
+ addDataSource('default', ds)
63
+ const d = await ds.getRepository(Domain).save({ name: 'adopt-test', subdomain: 'adopt-test' } as any)
64
+ domainId = d.id
65
+
66
+ await TwinEngine.ingestMaster(domainId, master(MIRROR, 3, 2))
67
+ await TwinEngine.ingestMaster(domainId, master(SIM, 3, 2))
68
+ })
69
+
70
+ after(async () => {
71
+ for (const id of [MIRROR, SIM]) await TwinEngine.stop(domainId, id).catch(() => {})
72
+ await ds?.destroy()
73
+ })
74
+
75
+ test('돌고 있는 미러는 멈추지 않고 갈아탄다 — 세 층이 함께 움직인다', async () => {
76
+ /* 미러 기동은 선언 기반 부팅이 가른다 — `startFromRegistry` 는 시뮬 경로다(같은 함수가 아니다). */
77
+ const rt = await TwinEngine.bootDeclared(domainId, MIRROR, 'mirror')
78
+ assert.equal(rt.mode, 'live', '미러로 떠야 이 검사가 뜻이 있다')
79
+
80
+ const res = await TwinEngine.ingestMaster(domainId, master(MIRROR, 7, 2))
81
+
82
+ /* ① 리비전 — 이후 이벤트가 달고 다닐 번호가 올라간다 */
83
+ const revs = await ds.getRepository(TwinStructure).find({ where: { instanceId: MIRROR }, order: { rev: 'ASC' } })
84
+ assert.deepEqual(revs.map((r: any) => r.rev), [1, 2], '구조가 달라졌으니 새 리비전이 남는다')
85
+
86
+ /* ② 저장 model·구조 행 — 조회가 새 구조를 본다 */
87
+ const inst = await ds.getRepository(TwinInstance).findOne({ where: { instanceId: MIRROR } })
88
+ assert.equal(inst.model.locations.length, 8, '저장 model 가 새 구조다(도크 1 + 빈 7)')
89
+ assert.equal(await ds.getRepository(TwinLocation).countBy({ instance: { id: inst.id } } as any), 8, '구조 행도 함께 다시 그려진다')
90
+
91
+ /* ③ 커널 — 멈추지 않은 채로 새 구조를 들고 있다 */
92
+ assert.equal(TwinEngine.instances[`${domainId}:${MIRROR}`]?.mode, 'live', '갈아타기가 미러를 멈추지 않는다')
93
+ const snap = TwinEngine.snapshot(domainId, MIRROR)
94
+ assert.equal(snap.locations.filter((n: any) => n.type === 'storage').length, 7, '커널도 새 자리를 들고 있다')
95
+
96
+ /* 무엇이 달라졌는지 **말한다** — 멈췄다 세우는 마디가 없으므로 말하지 않으면 조용히 지나간다 */
97
+ const adopted = res.warnings.find((w: any) => w.code === 'structure-adopted')
98
+ assert.ok(adopted, '갈아타기는 말없이 성공하지 않는다')
99
+ assert.equal(adopted.params.locationsAdded, 4, '자리 4개가 늘었다(빈 3→7)')
100
+ assert.equal(adopted.params.rev, 2)
101
+ })
102
+
103
+ test('사라진 설비를 수로 알린다 — 자리가 수천인 현장에서 직접 세어 보게 하지 않는다', async () => {
104
+ const res = await TwinEngine.ingestMaster(domainId, master(MIRROR, 7, 1)) // 지게차 2 → 1
105
+ const adopted = res.warnings.find((w: any) => w.code === 'structure-adopted')
106
+ assert.equal(adopted.params.equipmentDropped, 1)
107
+
108
+ const inst = await ds.getRepository(TwinInstance).findOne({ where: { instanceId: MIRROR } })
109
+ assert.equal(await ds.getRepository(TwinEquipment).countBy({ instance: { id: inst.id } } as any), 1, '표에서도 사라진다')
110
+ assert.equal(TwinEngine.snapshot(domainId, MIRROR).equipment.length, 1, '커널에서도 사라진다')
111
+ })
112
+
113
+ test('돌고 있는 시뮬레이션은 여전히 거절한다 — 조건이 바뀐 실험은 다른 실험이다', async () => {
114
+ await TwinEngine.bootDeclared(domainId, SIM, 'sim-experiment')
115
+ const before = (await ds.getRepository(TwinStructure).find({ where: { instanceId: SIM } })).length
116
+
117
+ await assert.rejects(() => TwinEngine.ingestMaster(domainId, master(SIM, 9, 2)), /is running/)
118
+
119
+ assert.equal(
120
+ (await ds.getRepository(TwinStructure).find({ where: { instanceId: SIM } })).length,
121
+ before,
122
+ '거절된 인제스트는 흔적을 남기지 않는다'
123
+ )
124
+ })
125
+
126
+ test('멈춘 미러는 구조를 전환하지 않는다 — 정지한 대상을 전환하려 하지 않는다', async () => {
127
+ await TwinEngine.stop(domainId, MIRROR)
128
+ await assert.rejects(
129
+ () => TwinEngine.adoptStructure(domainId, MIRROR, { locations: [], equipment: [] } as any),
130
+ /is not running/,
131
+ '멈춘 트윈은 프로비저닝으로 세우는 것이지 갈아타는 것이 아니다'
132
+ )
133
+ })
@@ -46,7 +46,7 @@ test('순서는 화면과 같다 — 확인된 것은 아래로, 그다음 심
46
46
  assert.equal(d.attentionTotal, 3)
47
47
  })
48
48
 
49
- test('한도를 말하지 않으면 자르지 않는다 — 이 값을 모르는 소비처가 일부만 받으면 안 된다', () => {
49
+ test('한도를 지정하지 않으면 자르지 않는다 — 이 값을 모르는 소비처가 일부만 받으면 안 된다', () => {
50
50
  const all = Array.from({ length: 30 }, (_, i) => attn(`a${i}`, 'low'))
51
51
  assert.equal(digestAttentions(all).attentions.length, 30)
52
52
  assert.equal(digestAttentions(all, 0).attentions.length, 30)
@@ -103,7 +103,7 @@ test('★ 순서는 렌즈 순서가 아니라 급한 순서다 — 뒤 렌즈
103
103
  assert.equal(d.attentionTotal, 3)
104
104
  })
105
105
 
106
- test('★ 두 렌즈가 같은 자리를 가리키면 더 급한 쪽이 색을 정한다 — 하나의 현실이므로', () => {
106
+ test('★ 두 렌즈가 같은 대상을 지목하면 더 급한 쪽이 색을 정한다 — 하나의 현실이므로', () => {
107
107
  const d = mergeLensAttentions([
108
108
  { instanceId: 'wms', attentions: [attn('a', 'low', 'dock-1')] },
109
109
  { instanceId: 'yms', attentions: [attn('b', 'critical', 'dock-1')] }
@@ -127,7 +127,7 @@ test('★ 신호가 없는 렌즈는 아무것도 보태지 않는다 — 빈
127
127
  assert.deepEqual(d.attentions.map(a => a.id), ['a'])
128
128
  })
129
129
 
130
- test('★ 렌즈가 하나도 없으면 빈 요약 — 공간에 떠 있는 트윈이 없을 때', () => {
130
+ test('★ 렌즈가 하나도 없으면 빈 요약 — 공간에 배치된 트윈이 없을 때', () => {
131
131
  const d = mergeLensAttentions([], 20)
132
132
  assert.deepEqual(d.attentions, [])
133
133
  assert.equal(d.attentionTotal, 0)
@@ -0,0 +1,87 @@
1
+ /*
2
+ * 축의 값을 어디서 읽나 — **선언이 말한 자리에서** (§10 6.5단계)
3
+ *
4
+ * ── 왜 있나 ────────────────────────────────────────────────────────────────
5
+ * 개념 지도는 상태 축을 `state[축이름]` 으로 읽었다. 에너지의 수요 구간은 `state.energy.closed` 에
6
+ * 살아서 그 규칙으로는 **언제나 `undefined`** 다 — 카드가 「관측 없음」으로 굳는다. 축은 선언돼 있고
7
+ * 상태에도 값이 있는데 이름이 안 맞을 뿐이라, 화면도 서버도 그 원인을 말해 주지 않는다.
8
+ *
9
+ * 이 시험이 지키는 것은 **모름과 없음의 구별**이다: 경로 중간이 비면 `undefined`(모른다)이고, 배열이
10
+ * 비면 빈 배열(없다)이다. 그 둘을 섞으면 「아직 안 온 것」과 「하나도 없는 것」이 같아진다.
11
+ */
12
+ import { test } from 'node:test'
13
+ import assert from 'node:assert/strict'
14
+
15
+ import { atPath, axisValueOf } from '../shared/axis-read.ts'
16
+
17
+ test('경로가 없는 축은 이름으로 읽는다 — 지금까지의 거동', () => {
18
+ const state = { orders: [{ id: 'o1' }], tasks: [] }
19
+ assert.deepEqual(axisValueOf(state, { axis: 'orders' }), [{ id: 'o1' }])
20
+ assert.deepEqual(axisValueOf(state, { axis: 'tasks' }), [], '빈 배열은 「없다」다 — 모른다가 아니다')
21
+ })
22
+
23
+ test('경로가 있으면 그 경로로 읽는다 — 중첩된 축이 조용히 사라지지 않게', () => {
24
+ const state = { energy: { closed: [{ startMs: 1 }, { startMs: 2 }] } }
25
+ const value = axisValueOf(state, { axis: 'demandWindows', path: 'energy.closed' })
26
+ assert.equal(Array.isArray(value), true)
27
+ assert.equal(value.length, 2)
28
+ /* 축 이름으로 읽으면 없다 — 그것이 이 함수가 생긴 이유다. */
29
+ assert.equal((state as any).demandWindows, undefined)
30
+ })
31
+
32
+ test('경로 중간이 비면 모른다 — 빈 배열로 메우지 않는다', () => {
33
+ assert.equal(axisValueOf({}, { axis: 'demandWindows', path: 'energy.closed' }), undefined)
34
+ assert.equal(axisValueOf({ energy: {} }, { axis: 'demandWindows', path: 'energy.closed' }), undefined)
35
+ /* 에너지 트윈이 아직 표본을 못 받았을 때가 이 경우다 — 「구간 0」이 아니라 「모름」이다. */
36
+ })
37
+
38
+ test('상태가 없으면 모른다 — 커널이 멈춰 있으면 0 이 아니다', () => {
39
+ assert.equal(axisValueOf(null, { axis: 'orders' }), undefined)
40
+ assert.equal(axisValueOf(undefined, { axis: 'demandWindows', path: 'energy.closed' }), undefined)
41
+ })
42
+
43
+ test('경로 따라가기는 중간 부재에서 멈춘다 — 던지지 않는다', () => {
44
+ assert.equal(atPath({ a: { b: 1 } }, 'a.b'), 1)
45
+ assert.equal(atPath({ a: null }, 'a.b'), undefined)
46
+ assert.equal(atPath(undefined, 'a.b'), undefined)
47
+ })
48
+
49
+ test('커널 선언과 상태의 모양이 맞는다 — 선언한 경로에 정말 값이 있다', async () => {
50
+ /*
51
+ * 선언(`path`)과 실제 상태의 모양이 어긋나면 이 배선은 조용히 빈 카드를 만든다. 그래서 **커널이 낸
52
+ * 상태로** 확인한다 — 선언을 읽는 쪽과 값을 만드는 쪽이 같은 모양을 쓰는지 보는 것이 요점이다.
53
+ */
54
+ const { EmsKernel, axisInfo, ENERGY_EVENT, DEMAND_WINDOW_MS } = await import('@operato/twin-kernel')
55
+ const info = axisInfo('demandWindows')!
56
+ assert.ok(info, '커널이 그 축을 선언해야 한다')
57
+ assert.equal(info.path, 'energy.closed')
58
+
59
+ const k = new EmsKernel('t')
60
+ k.loadTwinModel({ locations: [{ id: 'i', type: 'incoming', capacity: 0 }], equipment: [] })
61
+ const at = Date.parse('2026-08-14T05:00:00.000Z')
62
+ const sample = (whenMs: number, kW: number) => ({
63
+ eventId: `e${whenMs}`,
64
+ eventType: ENERGY_EVENT.measured,
65
+ eventTime: new Date(whenMs).toISOString(),
66
+ tenantId: 't',
67
+ data: { meterId: 'm', kW, at: new Date(whenMs).toISOString() }
68
+ })
69
+
70
+ /*
71
+ * 표본이 오기 전에는 **빈 목록**이다 — 그리고 그것이 맞다.
72
+ *
73
+ * 처음에는 `undefined`(모름)를 기대했다가 이 시험에 걸렸다. 판단해 보면 빈 목록이 옳다: 커널은
74
+ * 관측 중이고, 수요 구간은 15분이 지나야 닫힌다. 「아직 하나도 마감되지 않았다」는 **참인 사실**이고
75
+ * 개념 지도가 그것을 `none-yet` 으로 그린다.
76
+ *
77
+ * 「모른다」는 다른 경우다 — 커널이 멈춰 있어 상태 자체가 없을 때(위 시험). 그 둘을 섞지 않는 것이
78
+ * 이 배선의 요점이다.
79
+ */
80
+ assert.deepEqual(axisValueOf(k.getSnapshot(), info), [], '관측 중인데 아직 마감이 없으면 빈 목록이다')
81
+
82
+ k.apply(sample(at + 60_000, 100) as any)
83
+ k.apply(sample(at + DEMAND_WINDOW_MS + 60_000, 120) as any) // 앞 구간 마감
84
+ const windows = axisValueOf(k.getSnapshot(), info)
85
+ assert.equal(Array.isArray(windows), true, '선언한 경로에 목록이 있어야 한다')
86
+ assert.equal(windows.length, 1, '마감된 구간 하나')
87
+ })
@@ -0,0 +1,205 @@
1
+ /*
2
+ * 재기동을 넘어 **도는 트윈이 계속 돈다** — 그리고 되살리지 못하면 그렇게 말한다.
3
+ *
4
+ * ── 실측 (2026-08-14) ───────────────────────────────────────────────────────
5
+ * 서버를 한 번 재기동하면 등록부는 `running` 이라 말하는데 **아무 커널도 돌지 않았다**.
6
+ * `bootstrap()` 은 상태만 되찾아 `recovered` 에 담았고, 커널을 세우는 것은 명시 mutation 뿐이었다.
7
+ * 미러 트윈에서는 계측까지 끊겼다 — `startReferenceLiveFeed` 를 부르는 곳이 `importTwinReference`
8
+ * 하나뿐이어서, 붙어 있던 커넥터가 조용히 떨어졌다(오류도 로그도 없이).
9
+ *
10
+ * 이 시험이 지키는 것 넷:
11
+ * ① 부팅이 런타임까지 되살린다(상태 복구만으로는 도는 트윈이 아니다)
12
+ * ② 모드를 지어내지 않는다 — 미러였던 것은 미러로(시뮬로 되살리면 없던 움직임을 만든다)
13
+ * ③ 되살리지 못한 행은 `stopped` 로 적는다(우리가 고치려던 거짓말이 남지 않게)
14
+ * ④ 부팅 훅이 **엔진 다음에** 피드를 다시 붙인다
15
+ *
16
+ * 엔진과 레퍼런스 계층은 DB·shell 을 물고 있어 단위 시험이 불러올 수 없다. 그래서 **배선의 모양**을
17
+ * 소스로 지킨다 — 이 부류의 결함은 「부르는 곳이 없다」는 모양이고, 그건 소스에서 보인다.
18
+ */
19
+ import { test } from 'node:test'
20
+ import assert from 'node:assert/strict'
21
+ import { readFileSync } from 'node:fs'
22
+ import { fileURLToPath } from 'node:url'
23
+
24
+ const engine = readFileSync(fileURLToPath(new URL('../server/engine/twin-engine.ts', import.meta.url)), 'utf-8')
25
+ const live = readFileSync(fileURLToPath(new URL('../server/service/reference/reference-live.ts', import.meta.url)), 'utf-8')
26
+ const boot = readFileSync(fileURLToPath(new URL('../server/index.ts', import.meta.url)), 'utf-8')
27
+
28
+ /** 그 정적 메서드의 본문만 — 다음 메서드 선언까지. */
29
+ function methodOf(src: string, name: string): string {
30
+ const at = [`static ${name}(`, `static async ${name}(`].map(sig => src.indexOf(sig)).filter(i => i >= 0).sort((a, b) => a - b)[0]
31
+ assert.ok(at !== undefined, `${name} 가 없다 — 이름이 바뀌었으면 이 검사도 함께 고친다`)
32
+ const ends = ['\n static ', '\n private static ', '\n /**'].map(m => src.indexOf(m, at + 10)).filter(i => i >= 0)
33
+ const end = ends.sort((a, b) => a - b)[0]
34
+ return src.slice(at, end === undefined ? undefined : end)
35
+ }
36
+
37
+ test('부팅이 런타임까지 되살린다 — 상태 복구만으로는 도는 트윈이 아니다', () => {
38
+ const body = methodOf(engine, 'bootstrap')
39
+ assert.match(body, /resumeRow\(row\)/, 'bootstrap 이 행마다 재개를 시도해야 한다')
40
+ /* 예전 모양: 스냅샷을 `recovered` 에 담고 `continue` 로 끝났다 — 그것으로 되돌아가지 않게. */
41
+ assert.match(body, /for \(const row of rows\) await this\.resumeRow\(row\)/, '복구 뒤 재개 루프가 있어야 한다')
42
+ })
43
+
44
+ test('모드를 지어내지 않는다 — 미러였던 것은 미러로 되살린다', () => {
45
+ const body = methodOf(engine, 'resumeRow')
46
+ assert.match(body, /realityMode === 'mirror'/, '선언된 모드를 보고 갈라야 한다')
47
+ assert.match(body, /this\.startLive\(/, '미러는 관측 구동으로 세운다')
48
+ assert.match(body, /this\.startFromRegistry\(/, '시뮬은 등록부의 모드 그대로 세운다')
49
+ /*
50
+ * 미러 가지 안을 그대로 떠서 본다 — 미러를 시뮬로 세우면 관측 트윈이 **스스로 물건을 옮긴다**
51
+ * (없던 움직임을 만드는 것이다). 단어의 거리로 재면 로그·주석에 걸려 헛돈다.
52
+ */
53
+ const at = body.indexOf("realityMode === 'mirror'")
54
+ const mirrorBranch = body.slice(at, body.indexOf('} else {', at))
55
+ assert.match(mirrorBranch, /startLive\(/, '미러 가지는 관측 구동으로 세워야 한다')
56
+ assert.equal(mirrorBranch.includes('startFromRegistry'), false, '미러 가지에서 시뮬 기동을 부른다 — 없던 움직임을 만든다')
57
+ })
58
+
59
+ test('되살리지 못한 행은 stopped 로 적는다 — 「도는 중」이라 말하는 채로 두지 않는다', () => {
60
+ const resume = methodOf(engine, 'resumeRow')
61
+ assert.match(resume, /catch[\s\S]{0,80}markStopped/, '실패를 삼키면 등록부가 계속 거짓을 말한다')
62
+ const mark = methodOf(engine, 'markStopped')
63
+ assert.match(mark, /status: 'stopped'/, '등록부에 실제로 적어야 한다')
64
+ assert.match(mark, /console\.(warn|error)/, '되살리지 못한 사실은 로그에도 남아야 한다')
65
+ })
66
+
67
+ test('벤치 사본은 무인으로 되살아나지 않는다 — 실험은 재기동을 넘기지 못한다', () => {
68
+ const body = methodOf(engine, 'resumeRow')
69
+ assert.match(body, /purpose === 'bench'/, '격리 사본을 갈라야 한다')
70
+ assert.match(body, /bench[\s\S]{0,200}markStopped/, '되살리지 않았으면 등록부도 그렇게 적어야 한다')
71
+ })
72
+
73
+ test('피드 재개는 등록부가 「도는 미러」라고 말하는 것만 — 멈춘 것을 다시 켜지 않는다', () => {
74
+ assert.match(live, /export async function resumeReferenceLiveFeeds/, '재개 함수가 있어야 한다')
75
+ assert.match(live, /r\.status === 'running' && r\.realityMode === 'mirror'/, '도는 미러만 골라야 한다')
76
+ assert.match(live, /discoverSites/, '사이트 서술자는 다시 찾아서 얻는다(hint 를 남기지 않았다)')
77
+ /* 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다 — 조용히 넘기지 않는다. */
78
+ assert.match(live, /NOT reattached/, '못 붙인 것은 이유와 함께 남겨야 한다')
79
+ })
80
+
81
+ test('부팅 훅이 엔진 다음에 피드를 붙인다 — 순서가 뒤집히면 없는 커널에 계측을 붓는다', () => {
82
+ assert.match(boot, /resumeReferenceLiveFeeds/, '부팅 훅이 재개를 불러야 한다(부르는 곳이 없으면 없는 기능이다)')
83
+ const engineAt = boot.indexOf('TwinEngine.bootstrap()')
84
+ const feedAt = boot.indexOf('resumeReferenceLiveFeeds()')
85
+ assert.ok(engineAt >= 0 && feedAt > engineAt, '엔진 복구가 피드 재개보다 앞이어야 한다')
86
+ })
87
+
88
+ /*
89
+ * ── 부팅 재개가 드러낸 것 (2026-08-14 실측) ─────────────────────────────────
90
+ * 트윈 14개가 실제로 돌기 시작하자 호스트가 죽었다. 원인은 재개가 아니라 **방송 두 가지**였다:
91
+ * ① 시뮬은 delta 마다 곧바로 전 상태를 투영해 방송했다 — 한 tick 이 사실을 수백 개 내면 그 반복이
92
+ * 이벤트 루프를 35초 막았고(단계 합은 32ms 였다), 그 사이 구독자는 아무것도 빼내지 못했다.
93
+ * ② 밀린 push 가 1024를 넘자 pubsub 이 던졌고, 타이머 콜백을 타고 올라온 그 예외가 프로세스를 끝냈다.
94
+ * 둘 다 재개 없이도 있던 결함이다 — 재개가 그것을 매일 아침 만나게 만들었을 뿐이다.
95
+ */
96
+ test('시뮬 방송도 모아서 한 번 — delta 마다 전 상태를 투영하지 않는다', () => {
97
+ const at = engine.indexOf("if (msg.kind === 'delta')")
98
+ assert.ok(at >= 0, 'sim delta 처리 자리가 없다')
99
+ const body = engine.slice(at, at + 1400)
100
+ assert.match(body, /inst\.dirty = true/, 'dirty 로 표시하고')
101
+ assert.match(body, /ensureBroadcastCoalescer\(\)/, '주기 flush 에 맡겨야 한다')
102
+ assert.equal(/this\.publishEntityData\(inst\)/.test(body), false, 'delta 마다 곧바로 방송하는 모양으로 되돌아갔다')
103
+ })
104
+
105
+ test('flush 가 시뮬도 본다 — 라이브만 보면 시뮬 방송이 멈춘 채 남는다', () => {
106
+ const body = methodOf(engine, 'flushLiveBroadcasts')
107
+ assert.equal(/mode !== 'live'\) continue/.test(body), false, '시뮬을 건너뛰는 모양으로 되돌아갔다')
108
+ assert.match(body, /const isLive = inst\.mode === 'live'/, '모드는 가르되 둘 다 방송해야 한다')
109
+ /* 저널·state 채널은 시뮬이 자기 콜백에서 이미 처리한다 — 여기서 또 하면 두 번 적힌다. */
110
+ assert.match(body, /if \(!isLive\) continue/, '시뮬은 엔티티 방송까지만')
111
+ })
112
+
113
+ test('구독자 하나가 호스트를 죽이지 못한다 — 그리고 못 보낸 것을 보낸 것으로 적지 않는다', () => {
114
+ /* 직접 방송은 **가드 안에 하나**여야 한다 — 그 밖의 호출은 예외가 그대로 올라가는 길이다. */
115
+ const guardBody = methodOf(engine, 'publishGuarded')
116
+ assert.equal((engine.match(/pubsub\.publish\(/g) ?? []).length, 1, '가드를 거치지 않는 직접 방송이 남아 있다')
117
+ assert.match(guardBody, /pubsub\.publish\(/, '그 하나는 가드 안에 있어야 한다')
118
+ assert.match(guardBody, /catch/, '방송 실패를 잡아야 한다(타이머 콜백에서 던지면 프로세스가 끝난다)')
119
+ assert.match(guardBody, /console\.warn/, '못 보낸 사실은 남아야 한다(삼키면 「보냈는데 낡았다」가 된다)')
120
+ /* 시그니처를 남겨 두면 다음 주기에 「무변화」로 건너뛰고 그 태그는 영원히 낡는다. */
121
+ assert.match(engine, /if \(!this\.publishGuarded\('data'[\s\S]{0,80}sigs\.delete\(tag\)/, '실패한 태그의 시그니처를 되돌려야 한다')
122
+ })
123
+
124
+ /*
125
+ * ── 굶김 안전망 (2026-08-14) ─────────────────────────────────────────────────
126
+ * 시뮬 틱은 메인 이벤트 루프에서 돈다 — 한 트윈의 틱이 길어지면 그 시간만큼 호스트 전체가 멈춘다
127
+ * (실측 34.9초). 방송 반복은 걷어냈지만 커널 틱 자체는 여전히 메인 루프에 있고, 근본 해결(분산)은
128
+ * 이연됐다. 그 사이의 안전망이 이것이다: **호스트를 굶기는 트윈은 멈춘다.**
129
+ */
130
+ test('굶김은 예산과 다른 문턱으로 판정한다 — 조금 느린 트윈을 내리지 않는다', () => {
131
+ const body = methodOf(engine, 'guardStarvation')
132
+ assert.match(body, /this\.TICK_MS \* this\.STARVE_FACTOR/, '굶김 문턱은 틱 간격의 배수다')
133
+ /* 예산(500ms)으로 내리면 계기판이 말해야 할 「조금 느림」에 트윈이 죽는다. */
134
+ assert.equal(/budgetMs/.test(body), false, '예산으로 내리는 모양 — 경고와 정지를 섞지 않는다')
135
+ assert.ok(/STARVE_FACTOR = \d+/.test(engine) && /STARVE_STREAK = \d+/.test(engine), '문턱과 연속 횟수가 상수로 서 있어야 한다')
136
+ })
137
+
138
+ test('한 번으로 내리지 않고, 한 번 내려오면 연속이 끊긴다', () => {
139
+ const body = methodOf(engine, 'guardStarvation')
140
+ assert.match(body, /streak < this\.STARVE_STREAK\) return/, '연속이 차기 전에는 멈추지 않는다')
141
+ /* 웜스타트 직후 첫 틱은 원래 무겁다(복구한 상태를 처음 접는다) — 그것으로 내리면 재개가 무의미해진다. */
142
+ assert.match(body, /this\.starveStreak\.delete\(key\)[\s\S]{0,40}return/, '문턱 아래면 연속을 끊어야 한다')
143
+ })
144
+
145
+ test('멈추는 길은 하나 — 타이머만 끄면 등록부가 계속 「도는 중」이라 말한다', () => {
146
+ const guard = methodOf(engine, 'stopWithNote')
147
+ assert.match(guard, /this\.stop\(domainId, id\)/, '공용 stop 을 써야 한다(스냅샷·피드 정리·등록부가 그 안에 있다)')
148
+ /* 예전 tick 실패 경로는 `clearInterval` 만 했다 — 그 모양으로 되돌아가지 않게. */
149
+ const tick = methodOf(engine, 'tickGuarded')
150
+ assert.equal(/clearInterval\(inst\.timer\)/.test(tick), false, '타이머만 끄는 모양으로 되돌아갔다')
151
+ assert.match(tick, /stopWithNote\(/, '실패도 이유와 함께 멈춰야 한다')
152
+ })
153
+
154
+ test('왜 멈췄는지 코드로 남고, 다시 세우면 지운다', () => {
155
+ assert.match(engine, /export interface StopNote/, '이유는 문장이 아니라 코드다(화면이 옮긴다)')
156
+ assert.match(engine, /code: 'starved', params: \{ streak/, '굶김은 근거 수치와 함께')
157
+ assert.match(engine, /code: 'tick-failed', params: \{ reason/, '틱 실패는 원문 그대로(옮길 어휘가 아니다)')
158
+ assert.match(methodOf(engine, 'list'), /stopNote: this\.stopNotes\.get\(/, '목록이 그 사실을 함께 내야 한다')
159
+ /* 남겨 두면 다시 돌고 있는 트윈이 「굶겨서 멈췄다」고 말한다. */
160
+ assert.match(engine, /this\.stopNotes\.delete\(key\)/, '다시 세울 때 지워야 한다')
161
+ })
162
+
163
+ /*
164
+ * ── 같은 현장 이웃 설비 상태 전달 (2026-08-14) ──────────────────────────────
165
+ * 시뮬 에너지 트윈은 계측이 없으므로 스스로 부하를 만든다. 그 재료 하나가 「무엇이 돌고 있나」인데,
166
+ * 그것은 같은 현장 공장 트윈이 아는 사실이다. 호스트는 **누가 이웃인지**만 알려 주고, 무엇을 읽어
167
+ * 어떻게 부하로 바꿀지는 커널이 정한다 — 호스트가 kW 를 계산해 주면 규칙이 어디에도 안 적힌 채
168
+ * 트윈마다 갈라진다.
169
+ */
170
+ test('이웃 상태는 커널이 요청할 때만, 같은 현장에서만 전달한다', () => {
171
+ const body = methodOf(engine, 'feedPeerEquipment')
172
+ assert.match(body, /observePeerEquipment !== 'function'\) return/, '이웃을 읽지 않는 커널에는 넘기지 않는다')
173
+ assert.match(body, /other\.spaceId !== inst\.spaceId\) continue/, '같은 현장만 이웃이다')
174
+ assert.match(body, /other === inst/, '자기 자신은 이웃이 아니다')
175
+ /* 상태만 넘긴다 — 스냅샷을 통째로 넘기면 매 tick 상태 크기에 비례한 비용을 치른다. */
176
+ assert.equal(/snapshot\(\)/.test(body), false, '스냅샷을 넘기는 모양으로 되돌아갔다')
177
+ assert.match(body, /status: String/, '상태만 실어 보낸다')
178
+ })
179
+
180
+ test('현장이 없으면 이웃 목록을 비운다 — 옛 상태가 남아 부하를 만들지 않게', () => {
181
+ const body = methodOf(engine, 'feedPeerEquipment')
182
+ assert.match(body, /!inst\.spaceId[\s\S]{0,120}observePeerEquipment\(\[\]\)/, '빈 목록으로 덮어야 한다')
183
+ })
184
+
185
+ test('tick 이 이웃 상태를 먼저 싣는다 — 커널이 그것을 읽어 부하를 만든다', () => {
186
+ const tick = methodOf(engine, 'tickGuarded')
187
+ const feedAt = tick.indexOf('feedPeerEquipment')
188
+ const tickAt = tick.indexOf('runtime.tick(')
189
+ assert.ok(feedAt >= 0 && tickAt > feedAt, '이웃 상태를 실은 뒤에 tick 해야 한다')
190
+ })
191
+
192
+ /*
193
+ * ── 시뮬 트윈의 시각축 (2026-08-15) ─────────────────────────────────────────
194
+ * 커널의 기본 기준점은 상수(2026-01-01)다. 호스트가 그것을 실제 시각으로 옮기지 않으면 시뮬 트윈의
195
+ * 저널 시각이 **재기동마다 되감긴다** — 며칠 치 사건이 전부 같은 분에 몰리고, 실 시각으로 창을 자르는
196
+ * 성과·이력 질의에는 그 트윈이 보이지 않는다. 수요 구간(15분)도 가짜 시각 위에 서게 된다.
197
+ */
198
+ test('시뮬 기동이 커널의 시각 기준점을 지금으로 맞춘다', () => {
199
+ const body = methodOf(engine, 'start')
200
+ assert.match(body, /setClockOrigin\?\.\(Date\.now\(\)\)/, '기동 순간의 실제 시각을 줘야 한다')
201
+ /* 커널을 세운 **직후**여야 한다 — 시계가 흐른 뒤에 옮기면 그전 사실과 시간축이 어긋난다. */
202
+ const at = body.indexOf('setClockOrigin')
203
+ const runtimeAt = body.indexOf('new TwinRuntime(')
204
+ assert.ok(at >= 0 && runtimeAt > at, '런타임을 세우기 전에 기준점을 맞춘다')
205
+ })