@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
@@ -26,7 +26,7 @@
26
26
  * 그리고 집약 태그(__orders__ · __tasks__ · __items__ · __persons__ · __assets__).
27
27
  */
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.ITEMS_AGGREGATE_LIMIT = exports.assetsTag = exports.personsTag = exports.itemsTag = exports.tasksTag = exports.ordersTag = exports.ASSETS_TAG_PREFIX = exports.PERSONS_TAG_PREFIX = exports.ITEMS_TAG_PREFIX = exports.TASKS_TAG_PREFIX = exports.ORDERS_TAG_PREFIX = void 0;
29
+ exports.ITEMS_AGGREGATE_LIMIT = exports.energyTag = exports.assetsTag = exports.personsTag = exports.itemsTag = exports.tasksTag = exports.ordersTag = exports.ENERGY_TAG_PREFIX = exports.ASSETS_TAG_PREFIX = exports.PERSONS_TAG_PREFIX = exports.ITEMS_TAG_PREFIX = exports.TASKS_TAG_PREFIX = exports.ORDERS_TAG_PREFIX = void 0;
30
30
  exports.aggregateTag = aggregateTag;
31
31
  exports.buildEntityDeltas = buildEntityDeltas;
32
32
  /** 집약 고정 태그 — 클라 provider 와 일치해야 한다(런타임 생성 엔티티는 개별 배치가 불가하므로 집약 뷰). */
@@ -44,6 +44,8 @@ exports.TASKS_TAG_PREFIX = '__tasks__';
44
44
  exports.ITEMS_TAG_PREFIX = '__items__';
45
45
  exports.PERSONS_TAG_PREFIX = '__persons__';
46
46
  exports.ASSETS_TAG_PREFIX = '__assets__';
47
+ /** 에너지 집약 — 수요 구간·피크·계약은 **트윈 하나의 사실**이라 개체 태그가 없다. */
48
+ exports.ENERGY_TAG_PREFIX = '__energy__';
47
49
  /** 이 트윈의 집약 채널 태그. 트윈 식별자가 없으면 만들지 않는다 — 덮어쓰기의 원인이었다. */
48
50
  function aggregateTag(prefix, instanceId) {
49
51
  if (!instanceId)
@@ -60,6 +62,8 @@ const personsTag = (instanceId) => aggregateTag(exports.PERSONS_TAG_PREFIX, inst
60
62
  exports.personsTag = personsTag;
61
63
  const assetsTag = (instanceId) => aggregateTag(exports.ASSETS_TAG_PREFIX, instanceId);
62
64
  exports.assetsTag = assetsTag;
65
+ const energyTag = (instanceId) => aggregateTag(exports.ENERGY_TAG_PREFIX, instanceId);
66
+ exports.energyTag = energyTag;
63
67
  /**
64
68
  * 통과시키지 않는 필드 — **빼는 데는 이유가 필요하다.**
65
69
  *
@@ -124,10 +128,61 @@ function aggregateItem(entity, idKey) {
124
128
  * 이 걸려 **변한 것만** 재방송된다.
125
129
  */
126
130
  exports.ITEMS_AGGREGATE_LIMIT = 500;
131
+ /**
132
+ * 자리별 에너지 — **그 자리의 계량기들이 잰 것**을 그 자리 태그에 실을 수 있게 모은다.
133
+ *
134
+ * ── 왜 자리에도 필요한가 (2026-08-14) ───────────────────────────────────────
135
+ * 카탈로그는 자리에도 `metered` 를 준다(수전·분기·구역 계량은 재는 자리다). 그런데 값은 계량기
136
+ * **설비** 태그로만 나갔다. 그래서 계통도의 분기 자리에 계량 컴포넌트를 놓으면 **영원히 「—」** 였다 —
137
+ * 능력은 「잰다」고 말하는데 값이 오지 않는 자리.
138
+ *
139
+ * 「이 구간이 얼마를 먹나」는 자리의 사실이고, 사람은 계통도의 자리에 붙여 본다.
140
+ *
141
+ * ── 계층은 여기서 문제가 되지 않는다 ────────────────────────────────────────
142
+ * 각 자리에는 **그 자리에 붙은 계량기만** 더한다(수전 자리에는 수전 계량기, 분기 자리에는 그 분기의
143
+ * 것). 커널이 부하를 뿌리로만 세는 것과 다른 층의 계산이다 — 여기서는 자리마다 자기 것을 말한다.
144
+ *
145
+ * 근거를 함께 낸다(`meters`) — 그 자리의 수가 어느 계량기에서 왔는지 되짚을 수 있어야 한다.
146
+ */
147
+ function energyByLocation(state) {
148
+ const out = new Map();
149
+ const points = state?.energy?.points;
150
+ if (!Array.isArray(points) || !points.length)
151
+ return out;
152
+ /* 계량기 → 자리. 자리를 모르는 계량기는 자리 payload 에 실을 데가 없다(설비 태그로만 나간다). */
153
+ const locOf = new Map();
154
+ for (const e of state?.equipment ?? [])
155
+ if (e?.id && e?.homeLocation)
156
+ locOf.set(e.id, e.homeLocation);
157
+ for (const pt of points) {
158
+ const loc = pt?.id ? locOf.get(pt.id) : undefined;
159
+ if (!loc)
160
+ continue;
161
+ const acc = out.get(loc) ?? { meters: [], samplesInWindow: 0 };
162
+ acc.meters.push(pt.id);
163
+ acc.samplesInWindow = Number(acc.samplesInWindow ?? 0) + (Number(pt.samplesInWindow) || 0);
164
+ /* 값은 **있는 것만** 더한다 — 하나도 없으면 키를 만들지 않는다(0 은 「안 썼다」가 된다). */
165
+ if (Number.isFinite(Number(pt.kW)))
166
+ acc.kW = Number(acc.kW ?? 0) + Number(pt.kW);
167
+ if (Number.isFinite(Number(pt.kWh)))
168
+ acc.kWh = Number(acc.kWh ?? 0) + Number(pt.kWh);
169
+ /* 나이는 **가장 최근**을 쓴다 — 자리에 계량기가 여럿이면 그중 마지막 관측이 그 자리의 나이다. */
170
+ if (pt.atMs !== undefined) {
171
+ const iso = new Date(Number(pt.atMs)).toISOString();
172
+ if (!acc.measuredAt || String(acc.measuredAt) < iso)
173
+ acc.measuredAt = iso;
174
+ }
175
+ out.set(loc, acc);
176
+ }
177
+ return out;
178
+ }
127
179
  function buildEntityDeltas(state, instanceId) {
128
180
  const out = [];
181
+ const locEnergy = energyByLocation(state);
129
182
  for (const n of state?.locations ?? []) {
130
- out.push({ tag: n.id, data: payloadOf('location', n) });
183
+ /* 에너지는 자리 payload 에 **합친다** — 같은 태그로 두 번 보내면 뒤엣것이 앞을 덮는다. */
184
+ const energy = locEnergy.get(n.id);
185
+ out.push({ tag: n.id, data: energy ? { ...payloadOf('location', n), ...energy } : payloadOf('location', n) });
131
186
  }
132
187
  for (const m of state?.equipment ?? []) {
133
188
  /* Mobile(location/motion) + Operable(status/held) + Processable(oee) — 한 payload 로.
@@ -182,6 +237,51 @@ function buildEntityDeltas(state, instanceId) {
182
237
  ...(items.length > shown.length ? { truncated: items.length - shown.length } : {})
183
238
  }
184
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)
257
+ continue;
258
+ out.push({
259
+ tag: pt.id,
260
+ data: {
261
+ entity: 'meter',
262
+ ...(pt.kW !== undefined ? { kW: pt.kW } : {}),
263
+ ...(pt.kWh !== undefined ? { kWh: pt.kWh } : {}),
264
+ ...(pt.powerFactor !== undefined ? { powerFactor: pt.powerFactor } : {}),
265
+ /* 마지막 표본 시각 — 계측이 끊긴 것을 화면이 스스로 알 수 있게(우리가 판정하지 않는다). */
266
+ ...(pt.atMs !== undefined ? { measuredAt: new Date(pt.atMs).toISOString() } : {}),
267
+ samplesInWindow: pt.samplesInWindow ?? 0
268
+ }
269
+ });
270
+ }
271
+ out.push({
272
+ tag: (0, exports.energyTag)(instanceId),
273
+ data: {
274
+ entity: 'energy',
275
+ ...(energy.contractKW !== undefined ? { contractKW: energy.contractKW } : {}),
276
+ ...(energy.open ? { open: energy.open } : {}),
277
+ ...(energy.peakSince ? { peakSince: energy.peakSince } : {}),
278
+ /* 마감된 구간은 **최근 것만** 낸다 — 하루치를 매 방송에 실으면 채널이 그것으로 찬다.
279
+ 전체 수는 함께 낸다(조용히 자르지 않는다). */
280
+ recent: (energy.closed ?? []).slice(-8),
281
+ closedTotal: energy.closedTotal ?? 0
282
+ }
283
+ });
284
+ }
185
285
  return out;
186
286
  }
187
287
  //# sourceMappingURL=entity-delta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-delta.js","sourceRoot":"","sources":["../shared/entity-delta.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 트윈 State 스냅샷 → 라이브 바인딩 delta 목록 (순수 매핑 — pubsub/도메인 무관 → 단위 테스트 가능).\n * publishEntityData 가 이 목록을 엔티티별 시그니처로 dedup 후 data(tag) 채널로 발행한다.\n *\n * ── 규율: 레퍼런스가 아는 것은 **전부** 내보낸다 ────────────────────────────\n * 트윈에서 사소한 속성은 없다. 소속 구역·소속 팔레트·수량·만료처럼 곁가지로 보이는 값이 실제로는\n * 구역 롤업·적재 표현·로트 추적의 전제다. 그래서 여기서 **필드를 손으로 골라 담지 않는다.**\n *\n * 예전에는 엔티티마다 필요할 것 같은 필드만 열거했고, 그 결과 계약에 있는 것이 조용히 누락됐다\n * (2026-08-01 감사: 노드 parentId 누락 · 작업 전량 누락 · 물품 전량 누락). 화이트리스트 방식은\n * **계약이 자라도 화면이 따라오지 못한다** — 커널이 속성을 늘릴 때마다 이 파일을 고쳐야 하고, 고치는\n * 것을 잊으면 아무 신호 없이 정보가 사라진다.\n *\n * 그래서 반대로 한다: **상태 객체가 가진 것을 그대로 통과**시키고, 통과시키지 **않을** 것만 이유와 함께\n * 열거한다(EXCLUDED). 새 속성은 자동으로 화면에 도달하고, 빼려면 이유를 적어야 한다.\n *\n * ── 표식은 도메인 낱말을 훔치지 않는다 ─────────────────────────────────────\n * 예전 payload 는 엔티티 종류 표식을 `kind` 로 실었다. 그런데 `kind` 는 **도메인의 낱말**이다 —\n * 무버 kind=cutter, 작업 kind=putaway, 오더 kind=workorder. 표식이 그 이름을 차지하면 도메인 속성을\n * 실을 자리가 없어진다(같은 병을 오늘 ox-timeline 의 `group` 에서도 고쳤다).\n * 그래서 표식은 `entity`(우리 이름)로 옮기고 `kind` 는 도메인에 돌려준다.\n *\n * payload 계약(씬 컴포넌트가 소비): `entity`(표식) + 그 엔티티의 모든 속성(도메인 `kind` 포함).\n * entity = location · equipment · person · asset · order · task · item,\n * 그리고 집약 태그(__orders__ · __tasks__ · __items__ · __persons__ · __assets__).\n */\n\nexport interface EntityDelta {\n tag: string\n data: any\n}\n\n/** 집약 고정 태그 — 클라 provider 와 일치해야 한다(런타임 생성 엔티티는 개별 배치가 불가하므로 집약 뷰). */\n/*\n * 집약 채널의 접두사 — **실제 태그는 트윈 식별자를 붙여 만든다.**\n *\n * 예전에는 이 상수가 곧 태그였다. 그런데 발행 키는 (도메인, 태그)이고, **한 공간에 트윈이 여럿**\n * 있으면(실측: 3곳 × 3개) 셋이 같은 태그로 발행해 서로 덮었다. 보드의 주문 목록판이 세 공장의 주문\n * 중 마지막에 도착한 것을 보여 준다 — 오류 없이.\n *\n * 구간 성과 태그가 이미 `__kpi__:<트윈>` 으로 트윈을 붙이고 있었다(`kpi-broadcast.ts`). 같은 규약으로 맞춘다.\n */\nexport const ORDERS_TAG_PREFIX = '__orders__'\nexport const TASKS_TAG_PREFIX = '__tasks__'\nexport const ITEMS_TAG_PREFIX = '__items__'\nexport const PERSONS_TAG_PREFIX = '__persons__'\nexport const ASSETS_TAG_PREFIX = '__assets__'\n/** 에너지 집약 — 수요 구간·피크·계약은 **트윈 하나의 사실**이라 개체 태그가 없다. */\nexport const ENERGY_TAG_PREFIX = '__energy__'\n\n/** 이 트윈의 집약 채널 태그. 트윈 식별자가 없으면 만들지 않는다 — 덮어쓰기의 원인이었다. */\nexport function aggregateTag(prefix: string, instanceId: string): string {\n if (!instanceId) throw new Error('aggregateTag: instanceId required — 트윈 구분이 없는 태그는 다른 트윈의 것을 덮는다')\n\n return `${prefix}:${instanceId}`\n}\n\nexport const ordersTag = (instanceId: string) => aggregateTag(ORDERS_TAG_PREFIX, instanceId)\nexport const tasksTag = (instanceId: string) => aggregateTag(TASKS_TAG_PREFIX, instanceId)\nexport const itemsTag = (instanceId: string) => aggregateTag(ITEMS_TAG_PREFIX, instanceId)\nexport const personsTag = (instanceId: string) => aggregateTag(PERSONS_TAG_PREFIX, instanceId)\nexport const assetsTag = (instanceId: string) => aggregateTag(ASSETS_TAG_PREFIX, instanceId)\nexport const energyTag = (instanceId: string) => aggregateTag(ENERGY_TAG_PREFIX, instanceId)\n\n/**\n * 통과시키지 않는 필드 — **빼는 데는 이유가 필요하다.**\n *\n * · `id`/`epc` — 태그로 이미 나가므로 개별 payload 에 중복해 싣지 않는다(집약 뷰에는 넣는다).\n * · `oee` — 원시 누적(idleMs 등)이 매 delta 증가해 \"무변화 생략\" 을 무력화한다. shapeOee 로 정형화해 얹는다.\n */\nconst EXCLUDED = new Set(['id', 'epc', 'oee'])\n\n/**\n * OEE 원시 비율/카운트 → 표시용 정수 퍼센트 + 카운트.\n * 원시 누적 ms(idleMs 등)는 매 delta 증가해 무변화-생략을 무력화하므로 제외(유휴 자원의 매초 재방송 억제).\n * oee 미측정이면 undefined.\n */\nfunction shapeOee(o: any): any {\n if (!o) return undefined\n return {\n overall: Math.round((o.overall ?? 0) * 100),\n availability: Math.round((o.availability ?? 0) * 100),\n performance: Math.round((o.performance ?? 0) * 100),\n quality: Math.round((o.quality ?? 0) * 100),\n good: o.goodCount ?? 0,\n scrap: o.scrapCount ?? 0\n }\n}\n\n/**\n * 엔티티 하나의 payload — `entity` 표식 + 나머지 전부(도메인 `kind` 포함).\n *\n * `undefined` 값은 싣지 않는다(키의 유무가 시그니처를 흔들지 않게). 값이 없는 것과 키가 없는 것은\n * 소비처에서 같으므로 안전하고, 무변화 생략이 잘 듣는다.\n */\nfunction payloadOf(entityKind: string, entity: any): any {\n /* 표식은 `entity` — 도메인의 `kind` 를 덮지 않는다. */\n const data: any = { entity: entityKind }\n for (const [k, v] of Object.entries(entity ?? {})) {\n if (EXCLUDED.has(k) || v === undefined) continue\n data[k] = v\n }\n return data\n}\n\n/**\n * 집약 뷰의 항목 — 개별 payload 와 같은 내용 + 식별자.\n * 집약은 \"전체를 한 컴포넌트가 추적\" 하는 용도라 식별자가 있어야 한다.\n */\nfunction aggregateItem(entity: any, idKey: 'id' | 'epc'): any {\n const out: any = { [idKey]: entity?.[idKey] }\n for (const [k, v] of Object.entries(entity ?? {})) {\n if (k === idKey || k === 'oee' || v === undefined) continue\n out[k] = v\n }\n const shaped = shapeOee(entity?.oee)\n if (shaped) out.oee = shaped\n return out\n}\n\n/**\n * 물품 집약의 상한 — 큰 현장은 물품이 수만 개라, 전량을 매 방송에 실으면 전달이 현장을 느리게 한다.\n *\n * **조용히 자르지 않는다**: 넘치면 `total`(전체 수)과 `truncated`(잘린 수)를 함께 실어, 소비처가\n * \"이게 전부\" 라고 오해하지 않게 한다. 개별 물품은 태그별 delta 로 따로 나가고 그쪽은 시그니처 dedup\n * 이 걸려 **변한 것만** 재방송된다.\n */\nexport const ITEMS_AGGREGATE_LIMIT = 500\n\n/**\n * 자리별 에너지 — **그 자리의 계량기들이 잰 것**을 그 자리 태그에 실을 수 있게 모은다.\n *\n * ── 왜 자리에도 필요한가 (2026-08-14) ───────────────────────────────────────\n * 카탈로그는 자리에도 `metered` 를 준다(수전·분기·구역 계량은 재는 자리다). 그런데 값은 계량기\n * **설비** 태그로만 나갔다. 그래서 계통도의 분기 자리에 계량 컴포넌트를 놓으면 **영원히 「—」** 였다 —\n * 능력은 「잰다」고 말하는데 값이 오지 않는 자리.\n *\n * 「이 구간이 얼마를 먹나」는 자리의 사실이고, 사람은 계통도의 자리에 붙여 본다.\n *\n * ── 계층은 여기서 문제가 되지 않는다 ────────────────────────────────────────\n * 각 자리에는 **그 자리에 붙은 계량기만** 더한다(수전 자리에는 수전 계량기, 분기 자리에는 그 분기의\n * 것). 커널이 부하를 뿌리로만 세는 것과 다른 층의 계산이다 — 여기서는 자리마다 자기 것을 말한다.\n *\n * 근거를 함께 낸다(`meters`) — 그 자리의 수가 어느 계량기에서 왔는지 되짚을 수 있어야 한다.\n */\nfunction energyByLocation(state: any): Map<string, Record<string, unknown>> {\n const out = new Map<string, Record<string, unknown>>()\n const points = state?.energy?.points\n if (!Array.isArray(points) || !points.length) return out\n\n /* 계량기 → 자리. 자리를 모르는 계량기는 자리 payload 에 실을 데가 없다(설비 태그로만 나간다). */\n const locOf = new Map<string, string>()\n for (const e of state?.equipment ?? []) if (e?.id && e?.homeLocation) locOf.set(e.id, e.homeLocation)\n\n for (const pt of points) {\n const loc = pt?.id ? locOf.get(pt.id) : undefined\n if (!loc) continue\n const acc = out.get(loc) ?? { meters: [] as string[], samplesInWindow: 0 }\n ;(acc.meters as string[]).push(pt.id)\n acc.samplesInWindow = Number(acc.samplesInWindow ?? 0) + (Number(pt.samplesInWindow) || 0)\n /* 값은 **있는 것만** 더한다 — 하나도 없으면 키를 만들지 않는다(0 은 「안 썼다」가 된다). */\n if (Number.isFinite(Number(pt.kW))) acc.kW = Number(acc.kW ?? 0) + Number(pt.kW)\n if (Number.isFinite(Number(pt.kWh))) acc.kWh = Number(acc.kWh ?? 0) + Number(pt.kWh)\n /* 나이는 **가장 최근**을 쓴다 — 자리에 계량기가 여럿이면 그중 마지막 관측이 그 자리의 나이다. */\n if (pt.atMs !== undefined) {\n const iso = new Date(Number(pt.atMs)).toISOString()\n if (!acc.measuredAt || String(acc.measuredAt) < iso) acc.measuredAt = iso\n }\n out.set(loc, acc)\n }\n return out\n}\n\nexport function buildEntityDeltas(state: any, instanceId: string): EntityDelta[] {\n const out: EntityDelta[] = []\n const locEnergy = energyByLocation(state)\n\n for (const n of state?.locations ?? []) {\n /* 에너지는 자리 payload 에 **합친다** — 같은 태그로 두 번 보내면 뒤엣것이 앞을 덮는다. */\n const energy = locEnergy.get(n.id)\n out.push({ tag: n.id, data: energy ? { ...payloadOf('location', n), ...energy } : payloadOf('location', n) })\n }\n for (const m of state?.equipment ?? []) {\n /* Mobile(location/motion) + Operable(status/held) + Processable(oee) — 한 payload 로.\n * oee 만 정형화가 필요해 따로 얹는다(나머지는 통과). */\n const data = payloadOf('equipment', m)\n const oee = shapeOee(m?.oee)\n if (oee) data.oee = oee\n out.push({ tag: m.id, data })\n }\n /* 사람 — 설비와 별개 엔티티다. 같은 통과 규칙(가진 것을 전부 내보낸다). */\n for (const p of state?.persons ?? []) {\n out.push({ tag: p.id, data: payloadOf('person', p) })\n }\n /* 물리 자산(반복사용) — 물품도 설비도 아닌 네 번째 자원. */\n for (const a of state?.assets ?? []) {\n out.push({ tag: a.id, data: payloadOf('asset', a) })\n }\n for (const o of state?.orders ?? []) {\n out.push({ tag: o.id, data: payloadOf('order', o) })\n }\n for (const t of state?.tasks ?? []) {\n out.push({ tag: t.id, data: payloadOf('task', t) })\n }\n for (const it of state?.items ?? []) {\n /* 물품의 태그는 epc(개체 식별자) — 노드·무버와 달리 id 가 아니다. */\n if (it?.epc) out.push({ tag: it.epc, data: payloadOf('item', it) })\n }\n\n /* ── 집약 뷰 ──────────────────────────────────────────────────────────────\n * 런타임에 생기고 사라지는 엔티티(오더·작업·물품)는 보드에 개별 배치할 수 없다 → 한 컴포넌트가\n * 전체를 추적하는 집약 태그를 항상 방출한다. */\n const orders = state?.orders ?? []\n out.push({ tag: ordersTag(instanceId), data: { entity: 'orders', orders: orders.map((o: any) => aggregateItem(o, 'id')) } })\n\n const tasks = state?.tasks ?? []\n out.push({ tag: tasksTag(instanceId), data: { entity: 'tasks', tasks: tasks.map((t: any) => aggregateItem(t, 'id')) } })\n\n /* 사람 집약 — 인원은 보드에 개별 배치할 수도 있지만(설비처럼), 전체를 한눈에 보는 소비처도 있다.\n * 인원을 선언하지 않은 트윈에서는 빈 배열이 나간다(없음과 모름을 구별). */\n const persons = state?.persons ?? []\n out.push({ tag: personsTag(instanceId), data: { entity: 'persons', persons: persons.map((p: any) => aggregateItem(p, 'id')) } })\n\n const assets = state?.assets ?? []\n out.push({ tag: assetsTag(instanceId), data: { entity: 'assets', assets: assets.map((a: any) => aggregateItem(a, 'id')) } })\n\n const items = state?.items ?? []\n const shown = items.slice(0, ITEMS_AGGREGATE_LIMIT)\n out.push({\n tag: itemsTag(instanceId),\n data: {\n entity: 'items',\n items: shown.map((it: any) => aggregateItem(it, 'epc')),\n total: items.length,\n /* 잘렸으면 그 사실을 말한다 — \"이게 전부\" 로 읽히면 재고를 과소 판단한다. */\n ...(items.length > shown.length ? { truncated: items.length - shown.length } : {})\n }\n })\n\n /*\n * ── 에너지 (EMS) ─────────────────────────────────────────────────────────\n *\n * 계측은 `state.energy` 에 산다 — 자원 축(`equipment`·`locations`)이 아니다. 그래서 이 매핑이 없으면\n * 보드에 놓은 계량기가 **아무 값도 받지 못한다**(컴포넌트는 있는데 영원히 빈 모습).\n *\n * 두 갈래로 낸다:\n * · **개체별** — 계량 지점 하나하나의 마지막 관측(kW·kWh·역률·시각). 태그는 그 설비 id 다.\n * · **집약** — 수요 구간·피크·계약전력. 이것은 트윈 하나의 사실이라 개체에 실을 수 없다.\n *\n * 값이 없으면 **키를 넣지 않는다**(0 으로 만들지 않는다) — 「못 읽었다」와 「0 이었다」는 다른 사실이고,\n * 그 구별이 이 트윈의 값어치다.\n */\n const energy = state?.energy\n if (energy) {\n for (const pt of energy.points ?? []) {\n if (!pt?.id) continue\n out.push({\n tag: pt.id,\n data: {\n entity: 'meter',\n ...(pt.kW !== undefined ? { kW: pt.kW } : {}),\n ...(pt.kWh !== undefined ? { kWh: pt.kWh } : {}),\n ...(pt.powerFactor !== undefined ? { powerFactor: pt.powerFactor } : {}),\n /* 마지막 표본 시각 — 계측이 끊긴 것을 화면이 스스로 알 수 있게(우리가 판정하지 않는다). */\n ...(pt.atMs !== undefined ? { measuredAt: new Date(pt.atMs).toISOString() } : {}),\n samplesInWindow: pt.samplesInWindow ?? 0\n }\n })\n }\n out.push({\n tag: energyTag(instanceId),\n data: {\n entity: 'energy',\n ...(energy.contractKW !== undefined ? { contractKW: energy.contractKW } : {}),\n ...(energy.open ? { open: energy.open } : {}),\n ...(energy.peakSince ? { peakSince: energy.peakSince } : {}),\n /* 마감된 구간은 **최근 것만** 낸다 — 하루치를 매 방송에 실으면 채널이 그것으로 찬다.\n 전체 수는 함께 낸다(조용히 자르지 않는다). */\n recent: (energy.closed ?? []).slice(-8),\n closedTotal: energy.closedTotal ?? 0\n }\n })\n }\n\n return out\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kpi-broadcast.js","sourceRoot":"","sources":["../shared/kpi-broadcast.ts"],"names":[],"mappings":";;;AAAA;;;;;;GAMG;AACU,QAAA,cAAc,GAAG,SAAS,CAAA;AAEvC,sBAAsB;AACf,MAAM,MAAM,GAAG,CAAC,UAAkB,EAAE,EAAE,CAAC,GAAG,sBAAc,IAAI,UAAU,EAAE,CAAA;AAAlE,QAAA,MAAM,UAA4D;AAE/E,kFAAkF","sourcesContent":["/*\n * 구간 성과 카드의 **대상 식별자**.\n *\n * 예전에는 이 파일이 방송 payload 도 만들었다(`shapeKpiBroadcast`). 2026-08-06 에 방송을 없애고 카드가\n * `twinKpi` 를 직접 묻게 바꿔서, 남은 것은 팔레트가 대상을 가리킬 때 쓰는 식별자뿐이다.\n * 왜 바꿨는지는 `twin-engine.ts` 의 주석과 `operato-twin/client/scene/twin-kpi-source.ts` 에 있다.\n */\nexport const KPI_TAG_PREFIX = '__kpi__'\n\n/** 이 트윈의 구간 성과 태그. */\nexport const kpiTag = (instanceId: string) => `${KPI_TAG_PREFIX}:${instanceId}`\n\n/** 표식은 `entity` 로 — `kind` 는 도메인의 낱말이라 표식이 훔치지 않는다(`entity-delta.ts` 와 같은 규약). */\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twin-level.js","sourceRoot":"","sources":["../shared/twin-level.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAWH,kCAEC;AAQD,8CAMC;AAQD,4CAEC;AAnCD;;;;;GAKG;AACU,QAAA,WAAW,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,CAAU,CAAA;AAGtF,SAAgB,WAAW,CAAC,CAAU;IACpC,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAK,mBAAiC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;AAChF,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,KAAgB;IAChD,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,MAAM,CAAA;IACnC,IAAI,KAAK,KAAK,UAAU;QAAE,OAAO,UAAU,CAAA;IAC3C,IAAI,KAAK,KAAK,WAAW;QAAE,OAAO,UAAU,CAAA;IAE5C,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,KAAgB;IAC/C,OAAO,iBAAiB,CAAC,KAAK,CAAC,KAAK,SAAS,CAAA;AAC/C,CAAC","sourcesContent":["/*\n * 공간 계층의 **단계** — 성과를 어느 단위로 보는가. 순수(import 없음).\n *\n * ── 왜 여기 있나 ───────────────────────────────────────────────────────────\n * 이 목록을 세 곳이 쓴다: 씬 표현이 \"나는 어느 단계에 붙는다\" 고 선언할 때(클라이언트), 성과 카드가\n * \"이 단계로 나눠 달라\" 고 물을 때(클라이언트), AI 가 \"구역별로 보여 줘\" 를 제안할 때(서버).\n * 세 곳이 각자 목록을 적으면 하나가 늘 때 나머지가 조용히 뒤처진다 — 집약 태그에서 이미 겪었다.\n *\n * 그래서 **한 곳**에 두고 셋이 가져다 쓴다. 이 파일은 import 가 없어 클라이언트 번들에 서버 코드를\n * 끌고 들어가지 않는다.\n */\n\n/**\n * 성과를 접을 수 있는 단계 — 좁은 것부터.\n *\n * `equipment` 는 커널 축 이름으로 `resource` 다(설비·인원·자산을 아우르는 이름). 화면과 AI 는\n * \"설비\" 로 말하고, 커널에 물을 때만 축 이름으로 바꾼다 — 그 변환이 아래 한 곳에 있다.\n */\nexport const TWIN_LEVELS = ['space', 'twin', 'area', 'location', 'equipment'] as const\nexport type TwinLevel = (typeof TWIN_LEVELS)[number]\n\nexport function isTwinLevel(v: unknown): v is TwinLevel {\n return typeof v === 'string' && (TWIN_LEVELS as readonly string[]).includes(v)\n}\n\n/**\n * 단계 → 커널의 나누는 축(`KpiGroupBy`). **전체를 보는 단계는 축이 없다** — 나누지 않고 통째로 접는다.\n *\n * `space` 와 `twin` 이 축 없이 같은 이유: 둘의 차이는 \"무엇을 대상으로 삼는가\"(공간 전체 대 트윈 하나)이지\n * \"어떻게 나누는가\" 가 아니다. 대상은 질의의 `spaceId`/`instanceId` 가 정한다.\n */\nexport function kpiGroupByOfLevel(level: TwinLevel): 'area' | 'location' | 'resource' | undefined {\n if (level === 'area') return 'area'\n if (level === 'location') return 'location'\n if (level === 'equipment') return 'resource'\n\n return undefined\n}\n\n/**\n * 그 단계의 카드가 **대상 하나마다 한 장**인가.\n *\n * 구역·자리·설비는 여럿이라 카드가 그 수만큼 생긴다. 공간·트윈은 하나뿐이다. 보드를 자동으로 조립할 때\n * 몇 장이 생길지 미리 알아야 한다 — 자리 200개 × 지표 4개면 800장이다.\n */\nexport function isPerTargetLevel(level: TwinLevel): boolean {\n return kpiGroupByOfLevel(level) !== undefined\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/headless-twin",
3
- "version": "10.0.9",
3
+ "version": "10.0.10",
4
4
  "main": "dist-server/index.js",
5
5
  "things-factory": true,
6
6
  "author": "heartyoh <heartyoh@hatiolab.com>",
@@ -16,20 +16,22 @@
16
16
  "directory": "packages/headless-twin"
17
17
  },
18
18
  "scripts": {
19
- "build": "npm run clean:server && tsc",
20
- "build:server": "npm run clean:server && tsc",
21
- "clean:server": "rm -rf dist-server",
22
- "clean": "npm run clean:server",
19
+ "build": "npm run build:shared && npm run clean:server && tsc",
20
+ "build:server": "npm run build:shared && npm run clean:server && tsc",
21
+ "clean:server": "rm -rf dist-server tsconfig.tsbuildinfo",
22
+ "clean": "npm run clean:server && npm run clean:shared",
23
23
  "migration:create": "node ../../node_modules/typeorm/cli.js migration:create ./server/migrations/migration",
24
- "typecheck": "tsc --noEmit",
25
- "test": "node --test test/*.test.ts"
24
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.shared.json --noEmit",
25
+ "test": "node --test test/*.test.ts",
26
+ "build:shared": "tsc -p tsconfig.shared.json",
27
+ "clean:shared": "rm -rf dist-shared tsconfig.shared.tsbuildinfo"
26
28
  },
27
29
  "dependencies": {
28
- "@operato/twin-kernel": "^0.4.0",
29
- "@things-factory/auth-base": "^10.0.9",
30
- "@things-factory/cache-service": "^10.0.9",
30
+ "@operato/twin-kernel": "^0.7.30",
31
+ "@things-factory/auth-base": "^10.0.10",
32
+ "@things-factory/cache-service": "^10.0.10",
31
33
  "@things-factory/env": "^10.0.8",
32
- "@things-factory/shell": "^10.0.9"
34
+ "@things-factory/shell": "^10.0.10"
33
35
  },
34
- "gitHead": "b7a6c48c213e7a504114397b752004a295c1df72"
36
+ "gitHead": "afb7bf76bb16e946c8e62d9e79199c3fa5e34174"
35
37
  }
@@ -1,4 +1,6 @@
1
- import { ingest, type AdapterRule, type IngestResult } from '@operato/twin-kernel'
1
+ import { ingest, ingestEnergyRecords, ingestEnergyEquipmentRecords, isEnergyRecord,
2
+ isEnergyEquipmentRecord, type AdapterRule, type EnergyRecord,
3
+ type EnergyEquipmentRecord, type IngestResult } from '@operato/twin-kernel'
2
4
 
3
5
  /*
4
6
  * 정규 레코드 → 커널 검증 ACL → CanonicalEnvelope (P1 seam 코어).
@@ -40,11 +42,41 @@ export interface CanonicalRecord {
40
42
 
41
43
  /** 정규 레코드[] → 검증된 CanonicalEnvelope[] (+ 거부분). 순수 함수. 단일→객체도 배열로 정규화. */
42
44
  export function ingestCanonicalRecords(
43
- records: CanonicalRecord | CanonicalRecord[] | undefined | null,
45
+ records:
46
+ | CanonicalRecord
47
+ | EnergyRecord
48
+ | EnergyEquipmentRecord
49
+ | (CanonicalRecord | EnergyRecord | EnergyEquipmentRecord)[]
50
+ | undefined
51
+ | null,
44
52
  tenantId: string,
45
53
  defaultEventTime: string
46
54
  ): IngestResult {
47
55
  const arr = Array.isArray(records) ? records : records ? [records] : []
48
- const tagged = arr.map(r => ({ ...r, sourceType: 'twin' }))
49
- return ingest(tagged, TWIN_INGEST_RULES, { tenantId, defaultEventTime })
56
+ /*
57
+ * **어휘가 둘이다** — 물류(EPCIS)와 에너지. 예전에는 이 함수가 무조건 `ObjectEvent` 를 만들었고,
58
+ * 그래서 계측을 이 길로 넣으면 `epc` 가 없어 거부되거나(그나마 나은 쪽) 엉뚱한 물품 관측이 됐다.
59
+ *
60
+ * 판정은 커널이 한 곳에서 한다(`isEnergyRecord`) — 여기서 다시 짐작하면 소비처마다 답이 달라진다.
61
+ * 봉투는 같은 것을 쓰므로 아래 소비처(미러·저널·방송)는 이 갈림을 알 필요가 없다.
62
+ */
63
+ const energy = arr.filter(isEnergyRecord) as EnergyRecord[]
64
+ /* 세 번째 어휘 — 설비가 낸 자기 에너지 상태(발전·저장·감축 여지·개폐 위치). 계량과 갈라 두는
65
+ 이유는 커널에 적혀 있다: 계량은 구간에 누적되고, 이것들은 그 설비의 지금이다. */
66
+ const energyEquipment = arr.filter(isEnergyEquipmentRecord) as unknown as EnergyEquipmentRecord[]
67
+ const epcis = arr.filter(r => !isEnergyRecord(r) && !isEnergyEquipmentRecord(r)) as CanonicalRecord[]
68
+
69
+ const epcisResult = epcis.length
70
+ ? ingest(epcis.map(r => ({ ...r, sourceType: 'twin' })), TWIN_INGEST_RULES, { tenantId, defaultEventTime })
71
+ : { accepted: [], rejected: [] }
72
+ const energyResult = energy.length ? ingestEnergyRecords(energy, { tenantId, defaultEventTime }) : { accepted: [], rejected: [] }
73
+ const equipmentResult = energyEquipment.length
74
+ ? ingestEnergyEquipmentRecords(energyEquipment, { tenantId, defaultEventTime })
75
+ : { accepted: [], rejected: [] }
76
+
77
+ /* 거부분을 합쳐 돌려준다 — 어느 어휘에서 거부됐는지는 이유 문구가 말한다. */
78
+ return {
79
+ accepted: [...epcisResult.accepted, ...energyResult.accepted, ...equipmentResult.accepted],
80
+ rejected: [...epcisResult.rejected, ...energyResult.rejected, ...equipmentResult.rejected]
81
+ }
50
82
  }
@@ -57,7 +57,7 @@ export function routeCommand(inst: RoutableInstance | undefined, ownedByCaller:
57
57
  if (!type) return { target: 'reject', errorCode: 'unknown-command' }
58
58
 
59
59
  if (inst.mode !== 'live') {
60
- /* 시뮬 — 커널이 세계다. 런타임이 없으면 터뜨리지 않고 이유를 말한다. */
60
+ /* 시뮬 — 커널이 세계다. 런타임이 없으면 예외를 던지지 않고 이유를 말한다. */
61
61
  return inst.hasRuntime ? { target: 'runtime' } : { target: 'reject', errorCode: 'no-runtime' }
62
62
  }
63
63
 
@@ -0,0 +1,189 @@
1
+ /*
2
+ * 전력 계통도 — **선언된 것만 그린다.** 순수.
3
+ *
4
+ * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────
5
+ * 분기별 막대는 「어디가 많이 쓰나」에 답하지만 **어디에 걸려 있나**에 답하지 못한다. 실제로 부딪힌
6
+ * 세 가지가 전부 계통 질문이었다:
7
+ * · 「하위 계량이 덮지 않는 327 kWh」 — 그 몫이 수전 직결 부하인지, 계량 없는 구간인지 표로는 모른다.
8
+ * · 커넥터는 결선을 모르는 부하를 **수전에 매단다**(정직한 선택). 그런데 그 사실이 화면에 없었다.
9
+ * · 「계량기를 어디에 더 달아야 하나」 — 어두운 구간이 보여야 답할 수 있다.
10
+ *
11
+ * ── 무엇을 그리지 않나 ─────────────────────────────────────────────────────
12
+ * **보호 계통 단선도(SLD)가 아니다.** 차단기 정격·보호 협조·상(phase)·결선 방식은 모델에 없고, 그럴듯한
13
+ * 단선도를 그리면 갖지 않은 공학적 충실도를 주장하게 된다. 여기서 그리는 것은 **트윈이 선언한 관계**
14
+ * (자리의 계층 + 설비의 소속)와 **계량이 실제로 덮는 범위**뿐이다. 한계는 소비처가 화면에 적는다.
15
+ *
16
+ * ── 조용히 빠뜨리지 않는다 ─────────────────────────────────────────────────
17
+ * 자리를 모르는 설비, 계량기가 없는 자리, 계량이 덮지 않는 몫은 **각자 자기 칸으로** 나온다. 그림에서
18
+ * 사라지면 사용자는 자기 현장이 다 그려졌다고 믿는다.
19
+ */
20
+ import { EMS_LOCATION_TYPES, electricalUpstreamOf } from '@operato/twin-kernel'
21
+
22
+ /** 계통의 한 구간(수전·분기·구역) — 자리 하나가 곧 구간이다. */
23
+ export interface TopologySegment {
24
+ id: string
25
+ /** 카탈로그 자리 타입(`incoming`·`feeder`·`submeter-zone`). */
26
+ type: string
27
+ label?: string
28
+ /** 이 자리에 달린 계량 지점 id 들 — 비어 있으면 **어두운 구간**이다(값을 알 길이 없다). */
29
+ meters: string[]
30
+ /** 이 구간의 전력량과 근거 — 폴드가 낸 것만 싣는다(우리가 다시 계산하지 않는다). */
31
+ kWh?: number
32
+ basis?: 'measured' | 'apportioned'
33
+ /**
34
+ * **피크가 찍힌 순간** 이 구간이 얼마였나(kW) — 요금이 걸린 수다.
35
+ *
36
+ * 전력량이 큰 구간과 피크를 만든 구간은 다를 수 있다(하루 종일 조금씩 쓰는 부하 vs 잠깐 크게 쓰는 부하).
37
+ * 그림에 둘을 함께 두면 「어느 분기를 깎아야 피크가 내려가나」가 바로 읽힌다.
38
+ */
39
+ peakKW?: number
40
+ /** 이 자리에 매달린 부하(설비) — 종류를 그대로 싣는다. */
41
+ loads: { id: string; kind: string }[]
42
+ /**
43
+ * 계통 설비(차단기·태양광·축전지) — 부하가 아니라 계통의 일부다.
44
+ *
45
+ * `live` 는 **지금 값**이다(구간 값이 아니다): 태양광이 지금 몇 kW 를 만들고, 축전지가 얼마를 내보내며
46
+ * 충전율이 얼마인가. 원천이 보내 주는 사실이라 우리가 만들지 않는다 — 보내지 않은 필드는 없다.
47
+ *
48
+ * 시각(`measuredAt`)을 함께 나른다: 계측이 끊기면 **멈춘 값을 지금 값으로 그리게** 되고, 그것이 화면이
49
+ * 낼 수 있는 가장 나쁜 거짓이다(값이 그럴듯할수록 위험하다).
50
+ */
51
+ gear: {
52
+ id: string
53
+ kind: string
54
+ live?: { generatedKW?: number; exportKW?: number; chargeKW?: number; dischargeKW?: number; soc?: number; measuredAt?: string }
55
+ }[]
56
+ children: TopologySegment[]
57
+ }
58
+
59
+ export interface EnergyTopology {
60
+ roots: TopologySegment[]
61
+ /**
62
+ * 자리를 모르는 설비 — 집이 없거나, 없는 자리를 가리킨다. 숨기면 그림의 설비 수가 모델과 달라진다.
63
+ */
64
+ unplaced: { id: string; kind: string }[]
65
+ /** 계량기가 없는 자리 id 들 — 「여기에 달면 무엇을 알 수 있나」의 답이 되는 목록이다. */
66
+ unmetered: string[]
67
+ /**
68
+ * 결선을 모르는 부하가 매달린 자리 — 커넥터가 「어느 분기가 이 설비에 전기를 주는지 모른다」고 판단해
69
+ * 수전에 매단 경우다. 그 자리의 몫이 분기로 내려가지 않는 이유가 이것이다.
70
+ */
71
+ loadsOnIncoming: number
72
+ }
73
+
74
+ /**
75
+ * 이 설비의 **지금 값** — 스냅샷에서 그 설비를 찾아 에너지 축의 값만 옮긴다.
76
+ *
77
+ * 종류가 아는 필드만 옮긴다: 태양광에 충전율을 붙이거나 축전지에 발전량을 붙이면, 화면은 그것을 사실로
78
+ * 그리고 사람은 그 설비가 그런 일을 한다고 읽는다(커널이 종류별로 필드를 가려 받는 것과 같은 규율).
79
+ */
80
+ function liveOf(
81
+ state: readonly any[] | undefined | null,
82
+ id: string,
83
+ kind: string
84
+ ): { live: NonNullable<TopologySegment['gear'][number]['live']> } | undefined {
85
+ const e = (state ?? []).find(x => String(x?.id ?? '') === id)
86
+ if (!e) return undefined
87
+ const num = (v: unknown): number | undefined => (Number.isFinite(Number(v)) ? Number(v) : undefined)
88
+ const live: NonNullable<TopologySegment['gear'][number]['live']> = {}
89
+ if (kind === 'pv-array') {
90
+ const g = num(e.generatedKW)
91
+ const x = num(e.exportKW)
92
+ if (g !== undefined) live.generatedKW = g
93
+ if (x !== undefined) live.exportKW = x
94
+ } else if (kind === 'battery') {
95
+ const soc = num(e.soc)
96
+ const dis = num(e.dischargeKW)
97
+ const cha = num(e.chargeKW)
98
+ if (soc !== undefined) live.soc = soc
99
+ if (dis !== undefined) live.dischargeKW = dis
100
+ if (cha !== undefined) live.chargeKW = cha
101
+ }
102
+ if (!Object.keys(live).length) return undefined
103
+ /* 값이 있으면 시각도 함께 — 없으면 붙이지 않는다(모르는 시각을 지금으로 만들지 않는다). */
104
+ if (e.measuredAt) live.measuredAt = String(e.measuredAt)
105
+ return { live }
106
+ }
107
+
108
+ const ELECTRICAL = new Set(EMS_LOCATION_TYPES as readonly string[])
109
+ /** 부하가 아닌 계통 설비 — 이것들의 소비를 부하로 세면 총량이 이중으로 나뉜다. */
110
+ const GEAR = new Set(['meter', 'breaker', 'pv-array', 'battery'])
111
+
112
+ /**
113
+ * 모델 + 폴드 결과 → 계통 트리.
114
+ *
115
+ * @param model 트윈 모델(자리 계층 `parentId`, 설비 소속 `homeLocation`).
116
+ * @param energy 폴드가 낸 분기별 몫(있으면 노드에 싣는다 — 없으면 값 없이 구조만 그린다).
117
+ */
118
+ export function buildEnergyTopology(
119
+ model: { locations?: any[]; equipment?: any[] } | undefined | null,
120
+ energy?: { byFeeder?: { key: string; kWh: number; basis?: 'measured' | 'apportioned'; peakKW?: number }[] } | null,
121
+ /**
122
+ * 지금의 설비 상태(스냅샷의 `equipment`) — 발전·저장의 **지금 값**을 기호 옆에 적기 위해서다.
123
+ *
124
+ * 없으면 값 없이 구조만 그린다. 여기서 값을 만들지 않는다: 원천이 보내지 않은 것은 모르는 것이다.
125
+ */
126
+ equipmentState?: readonly any[] | null
127
+ ): EnergyTopology {
128
+ const locations: any[] = (model?.locations ?? []).filter(n => n?.id && ELECTRICAL.has(String(n.type)))
129
+ const byId = new Map<string, TopologySegment>()
130
+ for (const n of locations) {
131
+ byId.set(String(n.id), {
132
+ id: String(n.id),
133
+ type: String(n.type),
134
+ ...(n.name ? { label: String(n.name) } : {}),
135
+ meters: [],
136
+ loads: [],
137
+ gear: [],
138
+ children: []
139
+ })
140
+ }
141
+
142
+ const share = new Map((energy?.byFeeder ?? []).map(f => [f.key, f]))
143
+ for (const [id, segment] of byId) {
144
+ const s = share.get(id)
145
+ if (s) {
146
+ segment.kWh = s.kWh
147
+ if (s.basis) segment.basis = s.basis
148
+ if (typeof s.peakKW === 'number') segment.peakKW = s.peakKW
149
+ }
150
+ }
151
+
152
+ /* 설비를 자기 자리에 붙인다 — 계량기는 「이 구간을 잰다」는 뜻이라 따로 모은다. */
153
+ const unplaced: { id: string; kind: string }[] = []
154
+ let loadsOnIncoming = 0
155
+ for (const e of (model?.equipment ?? []) as any[]) {
156
+ if (!e?.id) continue
157
+ const kind = String(e.kind ?? '')
158
+ const home = String(e.homeLocation ?? e.homeLocationId ?? '')
159
+ const at = byId.get(home)
160
+ if (!at) {
161
+ /* 전기 계통 밖의 자리에 사는 설비도 여기서는 「자리를 모른다」로 모은다 — 이 그림의 축은 계통이다. */
162
+ unplaced.push({ id: String(e.id), kind })
163
+ continue
164
+ }
165
+ if (kind === 'meter') at.meters.push(String(e.id))
166
+ else if (GEAR.has(kind)) at.gear.push({ id: String(e.id), kind, ...(liveOf(equipmentState, String(e.id), kind) ?? {}) })
167
+ else {
168
+ at.loads.push({ id: String(e.id), kind })
169
+ if (at.type === 'incoming') loadsOnIncoming++
170
+ }
171
+ }
172
+
173
+ /*
174
+ * 계층을 잇는 것은 **전기 상류**다 — 공간 부모(공장·구역)는 이 축이 아니다. 상류의 뜻은 커널이 읽는다
175
+ * (옛 세대는 그 안에서 흡수된다) — 여기서 다시 판정하면 화면과 계산이 다른 계통을 말하게 된다.
176
+ */
177
+ const roots: TopologySegment[] = []
178
+ const all = model?.locations ?? []
179
+ for (const n of locations) {
180
+ const segment = byId.get(String(n.id))!
181
+ const up = electricalUpstreamOf(all as any, String(n.id))
182
+ const parent = up ? byId.get(up) : undefined
183
+ if (parent && parent !== segment) parent.children.push(segment)
184
+ else roots.push(segment)
185
+ }
186
+
187
+ const unmetered = [...byId.values()].filter(n => !n.meters.length).map(n => n.id)
188
+ return { roots, unplaced, unmetered, loadsOnIncoming }
189
+ }
@@ -1,14 +1,17 @@
1
1
  export * from './twin-engine.js'
2
2
  export * from './canonical-ingest.js'
3
3
  export * from './kpi-fold.js'
4
+ export * from './property-effects.js'
4
5
  export * from './kpi-baseline.js'
5
6
  /* 지표 정본 표(TWIN_METRIC)·목표 판정 — 소비처(twin-ai 등)가 지표 이름을 다시 열거하지 않게. */
6
7
  export * from './kpi-target.js'
7
- export * from './kpi-broadcast.js'
8
+ export * from '@things-factory/headless-twin/dist-shared/kpi-broadcast.js'
8
9
  /* 집약 채널 태그(접두사 + 트윈별 태그 생성) — 소비처가 문자열을 다시 적지 않게 내보낸다. */
9
- export * from './entity-delta.js'
10
+ export * from '@things-factory/headless-twin/dist-shared/entity-delta.js'
10
11
  /* 공간 계층의 단계 — 씬 표현·성과 카드·AI 가 같은 목록을 쓴다. */
11
- export * from './twin-level.js'
12
+ export * from '@things-factory/headless-twin/dist-shared/twin-level.js'
13
+ /* 부하 계기판 — 어떤 작업이 이벤트 루프를 얼마나 쓰는지. */
14
+ export * from './load-meter.js'
12
15
  /* 런타임 레지스트리 키 규약 — 소비처(다른 키 캐시·테스트)가 `:` 를 다시 적지 않게 함께 내보낸다. */
13
16
  export * from './runtime-key.js'
14
17
  export * from './kpi-query.js'
@@ -16,3 +19,7 @@ export * from './spec-coverage.js'
16
19
  export * from './travel-estimator.js'
17
20
  export * from './measured-estimator.js'
18
21
  export * from './model-basis.js'
22
+ export * from './live-feed-registry.js'
23
+ /* 로컬 선언(현장·트윈) — 소비처(twin-ai 도구)가 명세·검사 규칙을 다시 적지 않게. */
24
+ export * from './local-declarations.js'
25
+ export * from './operation-basis.js'
@@ -94,7 +94,7 @@ export type BaselineNoPosition = 'no-current' | 'too-few-samples'
94
94
  * 분포 안에서 현재의 자리 — **방향을 적용한 읽기.**
95
95
  *
96
96
  * 임의의 임계가 없다: 표본 전부보다 좋으면 `best`, 전부보다 나쁘면 `worst`, 그 사이는 중앙값을 기준으로
97
- * 가른다. `typical` 은 중앙값과 **같을 때**만이다.
97
+ * 구분한다. `typical` 은 중앙값과 **같을 때**만이다.
98
98
  */
99
99
  export type BaselineStanding = 'best' | 'better-than-typical' | 'typical' | 'worse-than-typical' | 'worst'
100
100