@things-factory/headless-twin 10.0.8 → 10.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (381) hide show
  1. package/dist-server/engine/attention-digest.d.ts +52 -0
  2. package/dist-server/engine/attention-digest.js +76 -0
  3. package/dist-server/engine/attention-digest.js.map +1 -0
  4. package/dist-server/engine/canonical-ingest.d.ts +2 -2
  5. package/dist-server/engine/canonical-ingest.js +24 -2
  6. package/dist-server/engine/canonical-ingest.js.map +1 -1
  7. package/dist-server/engine/command-routing.d.ts +33 -0
  8. package/dist-server/engine/command-routing.js +53 -0
  9. package/dist-server/engine/command-routing.js.map +1 -0
  10. package/dist-server/engine/energy-topology.d.ts +86 -0
  11. package/dist-server/engine/energy-topology.js +144 -0
  12. package/dist-server/engine/energy-topology.js.map +1 -0
  13. package/dist-server/engine/index.d.ts +11 -0
  14. package/dist-server/engine/index.js +17 -0
  15. package/dist-server/engine/index.js.map +1 -1
  16. package/dist-server/engine/kpi-baseline.d.ts +78 -0
  17. package/dist-server/engine/kpi-baseline.js +123 -0
  18. package/dist-server/engine/kpi-baseline.js.map +1 -0
  19. package/dist-server/engine/kpi-fold.d.ts +137 -1
  20. package/dist-server/engine/kpi-fold.js +225 -0
  21. package/dist-server/engine/kpi-fold.js.map +1 -1
  22. package/dist-server/engine/kpi-query.d.ts +32 -1
  23. package/dist-server/engine/kpi-query.js +262 -20
  24. package/dist-server/engine/kpi-query.js.map +1 -1
  25. package/dist-server/engine/kpi-target.d.ts +17 -0
  26. package/dist-server/engine/kpi-target.js +24 -2
  27. package/dist-server/engine/kpi-target.js.map +1 -1
  28. package/dist-server/engine/live-attentions.d.ts +1 -0
  29. package/dist-server/engine/live-attentions.js +7 -1
  30. package/dist-server/engine/live-attentions.js.map +1 -1
  31. package/dist-server/engine/live-feed-registry.d.ts +25 -0
  32. package/dist-server/engine/live-feed-registry.js +51 -0
  33. package/dist-server/engine/live-feed-registry.js.map +1 -0
  34. package/dist-server/engine/load-meter.d.ts +181 -0
  35. package/dist-server/engine/load-meter.js +267 -0
  36. package/dist-server/engine/load-meter.js.map +1 -0
  37. package/dist-server/engine/local-declarations.d.ts +262 -0
  38. package/dist-server/engine/local-declarations.js +528 -0
  39. package/dist-server/engine/local-declarations.js.map +1 -0
  40. package/dist-server/engine/model-basis.d.ts +39 -6
  41. package/dist-server/engine/model-basis.js +69 -9
  42. package/dist-server/engine/model-basis.js.map +1 -1
  43. package/dist-server/engine/model-vocabulary.d.ts +17 -0
  44. package/dist-server/engine/model-vocabulary.js +81 -0
  45. package/dist-server/engine/model-vocabulary.js.map +1 -0
  46. package/dist-server/engine/oee-accumulator.d.ts +28 -0
  47. package/dist-server/engine/oee-accumulator.js +26 -1
  48. package/dist-server/engine/oee-accumulator.js.map +1 -1
  49. package/dist-server/engine/operation-basis.d.ts +32 -0
  50. package/dist-server/engine/operation-basis.js +87 -0
  51. package/dist-server/engine/operation-basis.js.map +1 -0
  52. package/dist-server/engine/property-effects.d.ts +30 -0
  53. package/dist-server/engine/property-effects.js +192 -0
  54. package/dist-server/engine/property-effects.js.map +1 -0
  55. package/dist-server/engine/runtime-key.d.ts +15 -0
  56. package/dist-server/engine/runtime-key.js +64 -0
  57. package/dist-server/engine/runtime-key.js.map +1 -0
  58. package/dist-server/engine/spec-coverage.js +3 -3
  59. package/dist-server/engine/spec-coverage.js.map +1 -1
  60. package/dist-server/engine/state-axes.d.ts +21 -0
  61. package/dist-server/engine/state-axes.js +59 -0
  62. package/dist-server/engine/state-axes.js.map +1 -0
  63. package/dist-server/engine/structure-diff.js +1 -1
  64. package/dist-server/engine/structure-diff.js.map +1 -1
  65. package/dist-server/engine/travel-estimator.d.ts +9 -2
  66. package/dist-server/engine/travel-estimator.js +11 -6
  67. package/dist-server/engine/travel-estimator.js.map +1 -1
  68. package/dist-server/engine/twin-engine.d.ts +418 -57
  69. package/dist-server/engine/twin-engine.js +1599 -306
  70. package/dist-server/engine/twin-engine.js.map +1 -1
  71. package/dist-server/engine/warm-start.d.ts +93 -13
  72. package/dist-server/engine/warm-start.js +114 -14
  73. package/dist-server/engine/warm-start.js.map +1 -1
  74. package/dist-server/index.d.ts +1 -0
  75. package/dist-server/index.js +17 -11
  76. package/dist-server/index.js.map +1 -1
  77. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.d.ts +5 -0
  78. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +48 -0
  79. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -0
  80. package/dist-server/migrations/index.d.ts +2 -0
  81. package/dist-server/migrations/index.js +10 -0
  82. package/dist-server/migrations/index.js.map +1 -0
  83. package/dist-server/service/index.d.ts +3 -2
  84. package/dist-server/service/index.js +25 -17
  85. package/dist-server/service/index.js.map +1 -1
  86. package/dist-server/service/reference/discovery-result.d.ts +34 -0
  87. package/dist-server/service/reference/discovery-result.js +84 -0
  88. package/dist-server/service/reference/discovery-result.js.map +1 -0
  89. package/dist-server/service/reference/ingest-space.d.ts +53 -0
  90. package/dist-server/service/reference/ingest-space.js +79 -0
  91. package/dist-server/service/reference/ingest-space.js.map +1 -0
  92. package/dist-server/service/reference/knob-defaults.d.ts +20 -0
  93. package/dist-server/service/reference/knob-defaults.js +59 -0
  94. package/dist-server/service/reference/knob-defaults.js.map +1 -0
  95. package/dist-server/service/reference/reference-live.d.ts +23 -0
  96. package/dist-server/service/reference/reference-live.js +123 -6
  97. package/dist-server/service/reference/reference-live.js.map +1 -1
  98. package/dist-server/service/reference/reference-master.d.ts +227 -11
  99. package/dist-server/service/reference/reference-master.js +206 -25
  100. package/dist-server/service/reference/reference-master.js.map +1 -1
  101. package/dist-server/service/reference/reference-resolver.d.ts +33 -3
  102. package/dist-server/service/reference/reference-resolver.js +290 -23
  103. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  104. package/dist-server/service/reference/template-registry.d.ts +19 -3
  105. package/dist-server/service/reference/template-registry.js.map +1 -1
  106. package/dist-server/service/twin-attention/twin-attention-query.d.ts +8 -1
  107. package/dist-server/service/twin-attention/twin-attention-query.js +39 -8
  108. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  109. package/dist-server/service/twin-audit/command-audit.d.ts +37 -0
  110. package/dist-server/service/twin-audit/command-audit.js +53 -0
  111. package/dist-server/service/twin-audit/command-audit.js.map +1 -0
  112. package/dist-server/service/twin-audit/index.d.ts +4 -0
  113. package/dist-server/service/twin-audit/index.js +8 -0
  114. package/dist-server/service/twin-audit/index.js.map +1 -0
  115. package/dist-server/service/twin-audit/twin-audit-event.d.ts +24 -0
  116. package/dist-server/service/twin-audit/twin-audit-event.js +125 -0
  117. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -0
  118. package/dist-server/service/twin-audit/twin-audit-query.d.ts +4 -0
  119. package/dist-server/service/twin-audit/twin-audit-query.js +76 -0
  120. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -0
  121. package/dist-server/service/twin-control/twin-control-mutation.d.ts +2 -0
  122. package/dist-server/service/twin-control/twin-control-mutation.js +50 -11
  123. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  124. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  125. package/dist-server/service/twin-event/twin-event-keys.js +1 -1
  126. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  127. package/dist-server/service/twin-event/twin-event.d.ts +14 -5
  128. package/dist-server/service/twin-event/twin-event.js +46 -14
  129. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  130. package/dist-server/service/twin-forecast/forecast-metrics.d.ts +13 -0
  131. package/dist-server/service/twin-forecast/forecast-metrics.js +57 -0
  132. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -0
  133. package/dist-server/service/twin-forecast/forecast-tuning.d.ts +12 -0
  134. package/dist-server/service/twin-forecast/forecast-tuning.js +44 -0
  135. package/dist-server/service/twin-forecast/forecast-tuning.js.map +1 -0
  136. package/dist-server/service/twin-forecast/gap-analytics.d.ts +28 -2
  137. package/dist-server/service/twin-forecast/gap-analytics.js +43 -17
  138. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  139. package/dist-server/service/twin-forecast/twin-forecast-query.js +219 -39
  140. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  141. package/dist-server/service/twin-instance/twin-instance.d.ts +38 -5
  142. package/dist-server/service/twin-instance/twin-instance.js +66 -18
  143. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  144. package/dist-server/service/twin-journal/twin-journal-query.d.ts +11 -4
  145. package/dist-server/service/twin-journal/twin-journal-query.js +95 -15
  146. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  147. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +11 -5
  148. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +46 -27
  149. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  150. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  151. package/dist-server/service/twin-metrics/twin-metrics-query.js +63 -1
  152. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  153. package/dist-server/service/twin-model/epcis-coverage.d.ts +14 -0
  154. package/dist-server/service/twin-model/epcis-coverage.js +174 -0
  155. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -0
  156. package/dist-server/service/twin-model/iec61850-coverage.d.ts +14 -0
  157. package/dist-server/service/twin-model/iec61850-coverage.js +98 -0
  158. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -0
  159. package/dist-server/service/twin-model/index.d.ts +13 -0
  160. package/dist-server/service/twin-model/index.js +29 -0
  161. package/dist-server/service/twin-model/index.js.map +1 -0
  162. package/dist-server/service/twin-model/isa95-coverage.d.ts +48 -0
  163. package/dist-server/service/twin-model/isa95-coverage.js +155 -0
  164. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -0
  165. package/dist-server/service/twin-model/project-structure.d.ts +16 -0
  166. package/dist-server/service/twin-model/project-structure.js +173 -0
  167. package/dist-server/service/twin-model/project-structure.js.map +1 -0
  168. package/dist-server/service/twin-model/standard-coverage.d.ts +9 -0
  169. package/dist-server/service/twin-model/standard-coverage.js +37 -0
  170. package/dist-server/service/twin-model/standard-coverage.js.map +1 -0
  171. package/dist-server/service/twin-model/twin-equipment.d.ts +64 -0
  172. package/dist-server/service/twin-model/twin-equipment.js +134 -0
  173. package/dist-server/service/twin-model/twin-equipment.js.map +1 -0
  174. package/dist-server/service/twin-model/twin-lineage-query.d.ts +3 -0
  175. package/dist-server/service/twin-model/twin-lineage-query.js +206 -0
  176. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -0
  177. package/dist-server/service/twin-model/twin-location.d.ts +58 -0
  178. package/dist-server/service/twin-model/twin-location.js +130 -0
  179. package/dist-server/service/twin-model/twin-location.js.map +1 -0
  180. package/dist-server/service/twin-model/twin-model-item-query.d.ts +141 -0
  181. package/dist-server/service/twin-model/twin-model-item-query.js +852 -0
  182. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -0
  183. package/dist-server/service/twin-model/twin-model-mutation.d.ts +57 -0
  184. package/dist-server/service/twin-model/twin-model-mutation.js +490 -0
  185. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -0
  186. package/dist-server/service/twin-model/twin-model-query.d.ts +43 -0
  187. package/dist-server/service/twin-model/twin-model-query.js +551 -0
  188. package/dist-server/service/twin-model/twin-model-query.js.map +1 -0
  189. package/dist-server/service/twin-model/twin-model-tree-query.d.ts +3 -0
  190. package/dist-server/service/twin-model/twin-model-tree-query.js +158 -0
  191. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -0
  192. package/dist-server/service/twin-model/twin-operation.d.ts +61 -0
  193. package/dist-server/service/twin-model/twin-operation.js +137 -0
  194. package/dist-server/service/twin-model/twin-operation.js.map +1 -0
  195. package/dist-server/service/twin-space/move-space.d.ts +65 -0
  196. package/dist-server/service/twin-space/move-space.js +166 -0
  197. package/dist-server/service/twin-space/move-space.js.map +1 -0
  198. package/dist-server/service/twin-space/space-integrity.d.ts +97 -0
  199. package/dist-server/service/twin-space/space-integrity.js +182 -0
  200. package/dist-server/service/twin-space/space-integrity.js.map +1 -0
  201. package/dist-server/service/twin-space/twin-space-area.js +4 -4
  202. package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
  203. package/dist-server/service/twin-space/twin-space-representation.js +6 -6
  204. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  205. package/dist-server/service/twin-space/twin-space-resolver.d.ts +63 -1
  206. package/dist-server/service/twin-space/twin-space-resolver.js +365 -18
  207. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  208. package/dist-server/service/twin-space/twin-space.d.ts +17 -1
  209. package/dist-server/service/twin-space/twin-space.js +15 -5
  210. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  211. package/dist-server/service/twin-state/twin-state-subscription.js +1 -1
  212. package/dist-server/service/twin-state/twin-state-subscription.js.map +1 -1
  213. package/dist-server/service/twin-structure/twin-structure.d.ts +1 -1
  214. package/dist-server/service/twin-structure/twin-structure.js +8 -7
  215. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  216. package/dist-server/service/twin-target/twin-target-resolver.js +21 -4
  217. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  218. package/dist-shared/axis-read.d.ts +12 -0
  219. package/dist-shared/axis-read.js +32 -0
  220. package/dist-shared/axis-read.js.map +1 -0
  221. package/dist-shared/entity-delta.d.ts +29 -0
  222. package/dist-shared/entity-delta.js +287 -0
  223. package/dist-shared/entity-delta.js.map +1 -0
  224. package/dist-shared/kpi-broadcast.d.ts +4 -0
  225. package/dist-shared/kpi-broadcast.js +16 -0
  226. package/dist-shared/kpi-broadcast.js.map +1 -0
  227. package/dist-shared/twin-level.d.ts +23 -0
  228. package/dist-shared/twin-level.js +52 -0
  229. package/dist-shared/twin-level.js.map +1 -0
  230. package/package.json +14 -12
  231. package/server/engine/attention-digest.ts +102 -0
  232. package/server/engine/canonical-ingest.ts +36 -4
  233. package/server/engine/command-routing.ts +67 -0
  234. package/server/engine/energy-topology.ts +189 -0
  235. package/server/engine/index.ts +17 -0
  236. package/server/engine/kpi-baseline.ts +202 -0
  237. package/server/engine/kpi-fold.ts +389 -1
  238. package/server/engine/kpi-query.ts +295 -22
  239. package/server/engine/kpi-target.ts +24 -2
  240. package/server/engine/live-attentions.ts +7 -2
  241. package/server/engine/live-feed-registry.ts +58 -0
  242. package/server/engine/load-meter.ts +384 -0
  243. package/server/engine/local-declarations.ts +700 -0
  244. package/server/engine/model-basis.ts +78 -10
  245. package/server/engine/model-vocabulary.ts +82 -0
  246. package/server/engine/oee-accumulator.ts +34 -1
  247. package/server/engine/operation-basis.ts +100 -0
  248. package/server/engine/property-effects.ts +199 -0
  249. package/server/engine/runtime-key.ts +58 -0
  250. package/server/engine/spec-coverage.ts +3 -3
  251. package/server/engine/state-axes.ts +60 -0
  252. package/server/engine/structure-diff.ts +1 -1
  253. package/server/engine/travel-estimator.ts +11 -6
  254. package/server/engine/twin-engine.ts +1710 -297
  255. package/server/engine/warm-start.ts +207 -22
  256. package/server/index.ts +13 -10
  257. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +43 -0
  258. package/server/migrations/index.ts +7 -0
  259. package/server/service/index.ts +9 -1
  260. package/server/service/reference/discovery-result.ts +95 -0
  261. package/server/service/reference/ingest-space.ts +104 -0
  262. package/server/service/reference/knob-defaults.ts +59 -0
  263. package/server/service/reference/reference-live.ts +121 -6
  264. package/server/service/reference/reference-master.ts +362 -35
  265. package/server/service/reference/reference-resolver.ts +307 -23
  266. package/server/service/reference/template-registry.ts +20 -5
  267. package/server/service/twin-attention/twin-attention-query.ts +43 -6
  268. package/server/service/twin-audit/command-audit.ts +81 -0
  269. package/server/service/twin-audit/index.ts +5 -0
  270. package/server/service/twin-audit/twin-audit-event.ts +112 -0
  271. package/server/service/twin-audit/twin-audit-query.ts +72 -0
  272. package/server/service/twin-control/twin-control-mutation.ts +53 -13
  273. package/server/service/twin-event/twin-event-keys.ts +1 -1
  274. package/server/service/twin-event/twin-event.ts +52 -15
  275. package/server/service/twin-forecast/forecast-metrics.ts +60 -0
  276. package/server/service/twin-forecast/forecast-tuning.ts +40 -0
  277. package/server/service/twin-forecast/gap-analytics.ts +48 -9
  278. package/server/service/twin-forecast/twin-forecast-query.ts +211 -38
  279. package/server/service/twin-instance/twin-instance.ts +127 -22
  280. package/server/service/twin-journal/twin-journal-query.ts +108 -14
  281. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +40 -23
  282. package/server/service/twin-metrics/twin-metrics-query.ts +60 -3
  283. package/server/service/twin-model/epcis-coverage.ts +195 -0
  284. package/server/service/twin-model/iec61850-coverage.ts +117 -0
  285. package/server/service/twin-model/index.ts +25 -0
  286. package/server/service/twin-model/isa95-coverage.ts +190 -0
  287. package/server/service/twin-model/project-structure.ts +199 -0
  288. package/server/service/twin-model/standard-coverage.ts +35 -0
  289. package/server/service/twin-model/twin-equipment.ts +150 -0
  290. package/server/service/twin-model/twin-lineage-query.ts +193 -0
  291. package/server/service/twin-model/twin-location.ts +140 -0
  292. package/server/service/twin-model/twin-model-item-query.ts +848 -0
  293. package/server/service/twin-model/twin-model-mutation.ts +503 -0
  294. package/server/service/twin-model/twin-model-query.ts +537 -0
  295. package/server/service/twin-model/twin-model-tree-query.ts +180 -0
  296. package/server/service/twin-model/twin-operation.ts +150 -0
  297. package/server/service/twin-space/move-space.ts +262 -0
  298. package/server/service/twin-space/space-integrity.ts +293 -0
  299. package/server/service/twin-space/twin-space-area.ts +4 -4
  300. package/server/service/twin-space/twin-space-representation.ts +6 -6
  301. package/server/service/twin-space/twin-space-resolver.ts +365 -22
  302. package/server/service/twin-space/twin-space.ts +40 -6
  303. package/server/service/twin-state/twin-state-subscription.ts +1 -1
  304. package/server/service/twin-structure/twin-structure.ts +10 -7
  305. package/server/service/twin-target/twin-target-resolver.ts +22 -4
  306. package/shared/axis-read.ts +28 -0
  307. package/shared/entity-delta.ts +286 -0
  308. package/shared/kpi-broadcast.ts +13 -0
  309. package/shared/twin-level.ts +48 -0
  310. package/test/adopt-structure-live.test.ts +133 -0
  311. package/test/attention-digest.test.ts +135 -0
  312. package/test/axis-read.test.ts +87 -0
  313. package/test/boot-resume.test.ts +205 -0
  314. package/test/canonical-ingest-vocabularies.test.ts +118 -0
  315. package/test/capability-mapping.test.ts +5 -5
  316. package/test/command-routing.test.ts +61 -0
  317. package/test/declaration-reaches-model.test.ts +178 -0
  318. package/test/discovery-result.test.ts +75 -0
  319. package/test/energy-topology.test.ts +113 -0
  320. package/test/entity-delta.test.ts +195 -25
  321. package/test/event-time-column.test.ts +67 -0
  322. package/test/forecast-metrics.test.ts +71 -0
  323. package/test/forecast-tuning.test.ts +42 -0
  324. package/test/gap-analytics.test.ts +34 -7
  325. package/test/ingest-bench.test.ts +9 -9
  326. package/test/ingest-running-guard.test.ts +136 -0
  327. package/test/ingest-space.test.ts +78 -0
  328. package/test/instance-cache-lifecycle.test.ts +110 -0
  329. package/test/kernel-kind-guard.test.ts +96 -0
  330. package/test/knob-defaults.test.ts +72 -0
  331. package/test/kpi-baseline-db.test.ts +216 -0
  332. package/test/kpi-baseline.test.ts +196 -0
  333. package/test/kpi-fold.test.ts +393 -2
  334. package/test/kpi-query-bench.test.ts +130 -0
  335. package/test/lineage-survives-restart.test.ts +203 -0
  336. package/test/live-feed-registry.test.ts +67 -0
  337. package/test/live-kernel-facts.test.ts +161 -0
  338. package/test/live-mirror-parity.test.ts +54 -14
  339. package/test/load-meter.test.ts +314 -0
  340. package/test/local-declarations.test.ts +554 -0
  341. package/test/master-to-twin.test.ts +128 -36
  342. package/test/model-basis.test.ts +56 -1
  343. package/test/model-vocabulary.test.ts +114 -0
  344. package/test/move-space.test.ts +149 -0
  345. package/test/mutation-gate.test.ts +122 -0
  346. package/test/oee-accumulator.test.ts +90 -7
  347. package/test/operation-basis.test.ts +100 -0
  348. package/test/operations-capability-db.test.ts +165 -0
  349. package/test/project-structure-db.test.ts +226 -0
  350. package/test/projection-reaches-screen.test.ts +272 -0
  351. package/test/property-effects.test.ts +96 -0
  352. package/test/registry-key-guard.test.ts +80 -0
  353. package/test/resync-origin-site.test.ts +77 -0
  354. package/test/runtime-key.test.ts +66 -0
  355. package/test/scale-twin-bench.test.ts +7 -7
  356. package/test/snapshot-freshness.test.ts +60 -0
  357. package/test/space-integrity.test.ts +223 -0
  358. package/test/standard-coverage.test.ts +94 -0
  359. package/test/state-axes.test.ts +74 -0
  360. package/test/streamline-e2e.test.ts +16 -16
  361. package/test/structure-revision-db.test.ts +16 -15
  362. package/test/tenant-registry-db.test.ts +149 -0
  363. package/test/twin-audit.test.ts +83 -0
  364. package/test/twin-model-item-db.test.ts +275 -0
  365. package/test/twin-model-tree-db.test.ts +176 -0
  366. package/test/twin-origin-resync.test.ts +114 -0
  367. package/test/warm-start-seam.test.ts +140 -0
  368. package/test/warm-start.test.ts +223 -4
  369. package/tsconfig.json +6 -1
  370. package/tsconfig.shared.json +23 -0
  371. package/tsconfig.shared.tsbuildinfo +1 -0
  372. package/tsconfig.tsbuildinfo +1 -0
  373. package/dist-server/engine/entity-delta.d.ts +0 -19
  374. package/dist-server/engine/entity-delta.js +0 -161
  375. package/dist-server/engine/entity-delta.js.map +0 -1
  376. package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
  377. package/dist-server/service/twin-event/backfill-keys.js +0 -63
  378. package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
  379. package/dist-server/tsconfig.tsbuildinfo +0 -1
  380. package/server/engine/entity-delta.ts +0 -169
  381. package/server/service/twin-event/backfill-keys.ts +0 -72
@@ -0,0 +1,52 @@
1
+ interface AttentionLike {
2
+ id?: string;
3
+ /** 어느 트윈(렌즈)에서 온 신호인가 — 공간에서 모을 때 붙는다. */
4
+ instanceId?: string;
5
+ severity?: string;
6
+ state?: string;
7
+ anchor?: {
8
+ locationId?: string;
9
+ };
10
+ }
11
+ export interface AttentionDigestResult {
12
+ /** 상위 N — 레일이 그리는 것. */
13
+ attentions: AttentionLike[];
14
+ /** 전체 개수 — 레일의 "N건 더" 가 이 값에서 나온다(자른 사실을 밝힌다). */
15
+ attentionTotal: number;
16
+ /**
17
+ * 자리별 최고 심각도 — **전량을 근거로** 접는다.
18
+ *
19
+ * 상위 N 만으로 접으면 20위 밖의 신호를 가진 자리가 색을 잃는다. 그것이 이 요약의 핵심 이유다:
20
+ * 레일은 잘라도 되지만 **지도 색은 잘라선 안 된다.**
21
+ */
22
+ severityByLocation: Record<string, string>;
23
+ }
24
+ /** 화면 정렬과 같은 순서 — 확인된 것은 아래로, 그다음 심각도 내림차순. */
25
+ export declare const compareAttentions: (a: AttentionLike, b: AttentionLike) => number;
26
+ /**
27
+ * 신호를 요약한다.
28
+ *
29
+ * `limit` 이 없거나 0 이하면 **자르지 않는다** — 자르는 쪽이 기본이면 이 값을 모르는 소비처가
30
+ * 조용히 일부만 받는다(요약을 도입하다 만드는 전형적인 사고다).
31
+ */
32
+ export declare function digestAttentions(attentions: readonly AttentionLike[] | undefined | null, limit?: number): AttentionDigestResult;
33
+ /** 렌즈 하나가 낸 신호 — 어느 트윈에서 왔는지와 함께. */
34
+ export interface LensAttentions {
35
+ instanceId: string;
36
+ attentions: readonly AttentionLike[] | undefined | null;
37
+ }
38
+ /**
39
+ * 여러 렌즈의 신호를 **한 현장의 목록으로** 모은다.
40
+ *
41
+ * 한 공장에 WMS·MES·YMS 가 함께 있으므로 "이 현장에서 봐야 할 것" 은 렌즈 하나로 답할 수 없다.
42
+ * 모으면서 지키는 것 셋:
43
+ *
44
+ * ① **어느 트윈에서 왔는지 붙인다**(`instanceId`) — 조치는 결국 그 트윈에 보내야 하고, 공간에서 보면
45
+ * 같은 자리 id 가 렌즈마다 다른 것을 가리킬 수 있다.
46
+ * ② **순서는 렌즈 순서가 아니라 급한 순서다** — 앞 렌즈의 낮은 신호가 뒤 렌즈의 심각한 신호를
47
+ * 밀어내면, 상위 N 만 보는 화면에서 정작 급한 것이 안 보인다.
48
+ * ③ **자리 색은 렌즈를 가로질러 가장 급한 것으로** 접는다 — 두 렌즈가 같은 자리를 가리키면 그 자리는
49
+ * 하나의 현실이므로 더 급한 쪽이 이긴다(둘 중 하나를 임의로 고르면 색이 렌즈 순서에 따라 달라진다).
50
+ */
51
+ export declare function mergeLensAttentions(lenses: readonly LensAttentions[], limit?: number): AttentionDigestResult;
52
+ export {};
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ /*
3
+ * 주의 신호 요약 — **순수**. "화면이 실제로 쓰는 두 가지만 값싸게 만든다."
4
+ *
5
+ * ── 왜 있나 (2026-08-05) ────────────────────────────────────────────────────
6
+ * 축 선택으로 리프레시를 80KB → 2KB 로 줄였는데, **신호가 많은 트윈은 거기서 막혔다**: `hatio-yard`
7
+ * 는 신호 397건이 108KB 라 5배 감소에 그쳤다. 그런데 화면이 그 397건으로 하는 일은 둘뿐이다.
8
+ *
9
+ * ① 레일에 **20건**만 보여주고 나머지는 "N건 더" 라고만 말한다.
10
+ * ② 자리(폴리곤)를 칠할 **자리별 최고 심각도**를 뽑는다 — 397건이 자리 7곳의 색 7개로 접힌다.
11
+ *
12
+ * 즉 전량을 내려보내 클라이언트가 접는 구조였다. 접는 것은 값싸므로 **서버가 접어서 보낸다.**
13
+ *
14
+ * ── 조용히 자르지 않는다 ────────────────────────────────────────────────────
15
+ * 잘랐다는 사실과 **전체 개수**를 함께 보낸다. 경고 목록에서 조용한 누락은 "괜찮은 줄 알았다" 로
16
+ * 이어진다 — 레일이 이미 "N건 더" 를 말하고 있었고, 그 N 은 전체 개수에서 나와야 맞다.
17
+ *
18
+ * ── 정렬을 한 곳에 둔다 ─────────────────────────────────────────────────────
19
+ * 상위 N 을 서버가 고르므로 **클라이언트와 같은 순서**여야 한다. 순서가 다르면 서버가 고른 20건이
20
+ * 화면이 고를 20건과 달라지고, 그 어긋남은 아무 데서도 오류로 드러나지 않는다.
21
+ * 순서 = 확인된 것은 아래로, 그다음 심각도 높은 것부터(클라이언트 정렬과 동일).
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.compareAttentions = void 0;
25
+ exports.digestAttentions = digestAttentions;
26
+ exports.mergeLensAttentions = mergeLensAttentions;
27
+ /** 알람 표준(ISA-18.2) 우선순위 — 큰 값이 급하다. */
28
+ const RANK = { low: 1, medium: 2, high: 3, critical: 4 };
29
+ /** 화면 정렬과 같은 순서 — 확인된 것은 아래로, 그다음 심각도 내림차순. */
30
+ const compareAttentions = (a, b) => (a.state === 'acknowledged' ? 1 : 0) - (b.state === 'acknowledged' ? 1 : 0) ||
31
+ (RANK[b.severity ?? ''] ?? 0) - (RANK[a.severity ?? ''] ?? 0);
32
+ exports.compareAttentions = compareAttentions;
33
+ /**
34
+ * 신호를 요약한다.
35
+ *
36
+ * `limit` 이 없거나 0 이하면 **자르지 않는다** — 자르는 쪽이 기본이면 이 값을 모르는 소비처가
37
+ * 조용히 일부만 받는다(요약을 도입하다 만드는 전형적인 사고다).
38
+ */
39
+ function digestAttentions(attentions, limit) {
40
+ const all = Array.isArray(attentions) ? attentions : [];
41
+ /* 자리 색은 전량으로 접는다 — 잘린 뒤에 접으면 색을 잃는 자리가 생긴다. */
42
+ const severityByLocation = {};
43
+ for (const a of all) {
44
+ const nid = a?.anchor?.locationId;
45
+ if (!nid)
46
+ continue;
47
+ const cur = severityByLocation[nid];
48
+ if (!cur || (RANK[a.severity ?? ''] ?? 0) > (RANK[cur] ?? 0))
49
+ severityByLocation[nid] = a.severity ?? '';
50
+ }
51
+ const sorted = [...all].sort(exports.compareAttentions);
52
+ const capped = limit && limit > 0 ? sorted.slice(0, limit) : sorted;
53
+ return { attentions: capped, attentionTotal: all.length, severityByLocation };
54
+ }
55
+ /**
56
+ * 여러 렌즈의 신호를 **한 현장의 목록으로** 모은다.
57
+ *
58
+ * 한 공장에 WMS·MES·YMS 가 함께 있으므로 "이 현장에서 봐야 할 것" 은 렌즈 하나로 답할 수 없다.
59
+ * 모으면서 지키는 것 셋:
60
+ *
61
+ * ① **어느 트윈에서 왔는지 붙인다**(`instanceId`) — 조치는 결국 그 트윈에 보내야 하고, 공간에서 보면
62
+ * 같은 자리 id 가 렌즈마다 다른 것을 가리킬 수 있다.
63
+ * ② **순서는 렌즈 순서가 아니라 급한 순서다** — 앞 렌즈의 낮은 신호가 뒤 렌즈의 심각한 신호를
64
+ * 밀어내면, 상위 N 만 보는 화면에서 정작 급한 것이 안 보인다.
65
+ * ③ **자리 색은 렌즈를 가로질러 가장 급한 것으로** 접는다 — 두 렌즈가 같은 자리를 가리키면 그 자리는
66
+ * 하나의 현실이므로 더 급한 쪽이 이긴다(둘 중 하나를 임의로 고르면 색이 렌즈 순서에 따라 달라진다).
67
+ */
68
+ function mergeLensAttentions(lenses, limit) {
69
+ const all = [];
70
+ for (const lens of lenses) {
71
+ for (const a of lens.attentions ?? [])
72
+ all.push({ ...a, instanceId: lens.instanceId });
73
+ }
74
+ return digestAttentions(all, limit);
75
+ }
76
+ //# sourceMappingURL=attention-digest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attention-digest.js","sourceRoot":"","sources":["../../server/engine/attention-digest.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;;AAuCH,4CAeC;AAqBD,kDAMC;AA/ED,uCAAuC;AACvC,MAAM,IAAI,GAA2B,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAA;AAyBhF,+CAA+C;AACxC,MAAM,iBAAiB,GAAG,CAAC,CAAgB,EAAE,CAAgB,EAAU,EAAE,CAC9E,CAAC,CAAC,CAAC,KAAK,KAAK,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,KAAK,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3E,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;AAFlD,QAAA,iBAAiB,qBAEiC;AAE/D;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,UAAuD,EAAE,KAAc;IACtG,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAA;IAEvD,+CAA+C;IAC/C,MAAM,kBAAkB,GAA2B,EAAE,CAAA;IACrD,KAAK,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC;QACpB,MAAM,GAAG,GAAG,CAAC,EAAE,MAAM,EAAE,UAAU,CAAA;QACjC,IAAI,CAAC,GAAG;YAAE,SAAQ;QAClB,MAAM,GAAG,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAA;QACnC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAAE,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAA;IAC1G,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,yBAAiB,CAAC,CAAA;IAC/C,MAAM,MAAM,GAAG,KAAK,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;IACnE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,EAAE,CAAA;AAC/E,CAAC;AAQD;;;;;;;;;;;;GAYG;AACH,SAAgB,mBAAmB,CAAC,MAAiC,EAAE,KAAc;IACnF,MAAM,GAAG,GAAoB,EAAE,CAAA;IAC/B,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,UAAU,IAAI,EAAE;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAmB,CAAC,CAAA;IACzG,CAAC;IACD,OAAO,gBAAgB,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;AACrC,CAAC","sourcesContent":["/*\n * 주의 신호 요약 — **순수**. \"화면이 실제로 쓰는 두 가지만 값싸게 만든다.\"\n *\n * ── 왜 있나 (2026-08-05) ────────────────────────────────────────────────────\n * 축 선택으로 리프레시를 80KB → 2KB 로 줄였는데, **신호가 많은 트윈은 거기서 막혔다**: `hatio-yard`\n * 는 신호 397건이 108KB 라 5배 감소에 그쳤다. 그런데 화면이 그 397건으로 하는 일은 둘뿐이다.\n *\n * ① 레일에 **20건**만 보여주고 나머지는 \"N건 더\" 라고만 말한다.\n * ② 자리(폴리곤)를 칠할 **자리별 최고 심각도**를 뽑는다 — 397건이 자리 7곳의 색 7개로 접힌다.\n *\n * 즉 전량을 내려보내 클라이언트가 접는 구조였다. 접는 것은 값싸므로 **서버가 접어서 보낸다.**\n *\n * ── 조용히 자르지 않는다 ────────────────────────────────────────────────────\n * 잘랐다는 사실과 **전체 개수**를 함께 보낸다. 경고 목록에서 조용한 누락은 \"괜찮은 줄 알았다\" 로\n * 이어진다 — 레일이 이미 \"N건 더\" 를 말하고 있었고, 그 N 은 전체 개수에서 나와야 맞다.\n *\n * ── 정렬을 한 곳에 둔다 ─────────────────────────────────────────────────────\n * 상위 N 을 서버가 고르므로 **클라이언트와 같은 순서**여야 한다. 순서가 다르면 서버가 고른 20건이\n * 화면이 고를 20건과 달라지고, 그 어긋남은 아무 데서도 오류로 드러나지 않는다.\n * 순서 = 확인된 것은 아래로, 그다음 심각도 높은 것부터(클라이언트 정렬과 동일).\n */\n\n/** 알람 표준(ISA-18.2) 우선순위 — 큰 값이 급하다. */\nconst RANK: Record<string, number> = { low: 1, medium: 2, high: 3, critical: 4 }\n\ninterface AttentionLike {\n id?: string\n /** 어느 트윈(렌즈)에서 온 신호인가 — 공간에서 모을 때 붙는다. */\n instanceId?: string\n severity?: string\n state?: string\n anchor?: { locationId?: string }\n}\n\nexport interface AttentionDigestResult {\n /** 상위 N — 레일이 그리는 것. */\n attentions: AttentionLike[]\n /** 전체 개수 — 레일의 \"N건 더\" 가 이 값에서 나온다(자른 사실을 밝힌다). */\n attentionTotal: number\n /**\n * 자리별 최고 심각도 — **전량을 근거로** 접는다.\n *\n * 상위 N 만으로 접으면 20위 밖의 신호를 가진 자리가 색을 잃는다. 그것이 이 요약의 핵심 이유다:\n * 레일은 잘라도 되지만 **지도 색은 잘라선 안 된다.**\n */\n severityByLocation: Record<string, string>\n}\n\n/** 화면 정렬과 같은 순서 — 확인된 것은 아래로, 그다음 심각도 내림차순. */\nexport const compareAttentions = (a: AttentionLike, b: AttentionLike): number =>\n (a.state === 'acknowledged' ? 1 : 0) - (b.state === 'acknowledged' ? 1 : 0) ||\n (RANK[b.severity ?? ''] ?? 0) - (RANK[a.severity ?? ''] ?? 0)\n\n/**\n * 신호를 요약한다.\n *\n * `limit` 이 없거나 0 이하면 **자르지 않는다** — 자르는 쪽이 기본이면 이 값을 모르는 소비처가\n * 조용히 일부만 받는다(요약을 도입하다 만드는 전형적인 사고다).\n */\nexport function digestAttentions(attentions: readonly AttentionLike[] | undefined | null, limit?: number): AttentionDigestResult {\n const all = Array.isArray(attentions) ? attentions : []\n\n /* 자리 색은 전량으로 접는다 — 잘린 뒤에 접으면 색을 잃는 자리가 생긴다. */\n const severityByLocation: Record<string, string> = {}\n for (const a of all) {\n const nid = a?.anchor?.locationId\n if (!nid) continue\n const cur = severityByLocation[nid]\n if (!cur || (RANK[a.severity ?? ''] ?? 0) > (RANK[cur] ?? 0)) severityByLocation[nid] = a.severity ?? ''\n }\n\n const sorted = [...all].sort(compareAttentions)\n const capped = limit && limit > 0 ? sorted.slice(0, limit) : sorted\n return { attentions: capped, attentionTotal: all.length, severityByLocation }\n}\n\n/** 렌즈 하나가 낸 신호 — 어느 트윈에서 왔는지와 함께. */\nexport interface LensAttentions {\n instanceId: string\n attentions: readonly AttentionLike[] | undefined | null\n}\n\n/**\n * 여러 렌즈의 신호를 **한 현장의 목록으로** 모은다.\n *\n * 한 공장에 WMS·MES·YMS 가 함께 있으므로 \"이 현장에서 봐야 할 것\" 은 렌즈 하나로 답할 수 없다.\n * 모으면서 지키는 것 셋:\n *\n * ① **어느 트윈에서 왔는지 붙인다**(`instanceId`) — 조치는 결국 그 트윈에 보내야 하고, 공간에서 보면\n * 같은 자리 id 가 렌즈마다 다른 것을 가리킬 수 있다.\n * ② **순서는 렌즈 순서가 아니라 급한 순서다** — 앞 렌즈의 낮은 신호가 뒤 렌즈의 심각한 신호를\n * 밀어내면, 상위 N 만 보는 화면에서 정작 급한 것이 안 보인다.\n * ③ **자리 색은 렌즈를 가로질러 가장 급한 것으로** 접는다 — 두 렌즈가 같은 자리를 가리키면 그 자리는\n * 하나의 현실이므로 더 급한 쪽이 이긴다(둘 중 하나를 임의로 고르면 색이 렌즈 순서에 따라 달라진다).\n */\nexport function mergeLensAttentions(lenses: readonly LensAttentions[], limit?: number): AttentionDigestResult {\n const all: AttentionLike[] = []\n for (const lens of lenses) {\n for (const a of lens.attentions ?? []) all.push({ ...a, instanceId: lens.instanceId } as AttentionLike)\n }\n return digestAttentions(all, limit)\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { type IngestResult } from '@operato/twin-kernel';
1
+ import { type EnergyRecord, type EnergyEquipmentRecord, type IngestResult } from '@operato/twin-kernel';
2
2
  export interface CanonicalRecord {
3
3
  epc: string;
4
4
  action: string;
@@ -8,4 +8,4 @@ export interface CanonicalRecord {
8
8
  bizLocation?: string;
9
9
  }
10
10
  /** 정규 레코드[] → 검증된 CanonicalEnvelope[] (+ 거부분). 순수 함수. 단일→객체도 배열로 정규화. */
11
- export declare function ingestCanonicalRecords(records: CanonicalRecord | CanonicalRecord[] | undefined | null, tenantId: string, defaultEventTime: string): IngestResult;
11
+ export declare function ingestCanonicalRecords(records: CanonicalRecord | EnergyRecord | EnergyEquipmentRecord | (CanonicalRecord | EnergyRecord | EnergyEquipmentRecord)[] | undefined | null, tenantId: string, defaultEventTime: string): IngestResult;
@@ -32,7 +32,29 @@ const TWIN_INGEST_RULES = [
32
32
  /** 정규 레코드[] → 검증된 CanonicalEnvelope[] (+ 거부분). 순수 함수. 단일→객체도 배열로 정규화. */
33
33
  function ingestCanonicalRecords(records, tenantId, defaultEventTime) {
34
34
  const arr = Array.isArray(records) ? records : records ? [records] : [];
35
- const tagged = arr.map(r => ({ ...r, sourceType: 'twin' }));
36
- return (0, twin_kernel_1.ingest)(tagged, TWIN_INGEST_RULES, { tenantId, defaultEventTime });
35
+ /*
36
+ * **어휘가 둘이다** — 물류(EPCIS) 에너지. 예전에는 함수가 무조건 `ObjectEvent` 를 만들었고,
37
+ * 그래서 계측을 이 길로 넣으면 `epc` 가 없어 거부되거나(그나마 나은 쪽) 엉뚱한 물품 관측이 됐다.
38
+ *
39
+ * 판정은 커널이 한 곳에서 한다(`isEnergyRecord`) — 여기서 다시 짐작하면 소비처마다 답이 달라진다.
40
+ * 봉투는 같은 것을 쓰므로 아래 소비처(미러·저널·방송)는 이 갈림을 알 필요가 없다.
41
+ */
42
+ const energy = arr.filter(twin_kernel_1.isEnergyRecord);
43
+ /* 세 번째 어휘 — 설비가 낸 자기 에너지 상태(발전·저장·감축 여지·개폐 위치). 계량과 갈라 두는
44
+ 이유는 커널에 적혀 있다: 계량은 구간에 누적되고, 이것들은 그 설비의 지금이다. */
45
+ const energyEquipment = arr.filter(twin_kernel_1.isEnergyEquipmentRecord);
46
+ const epcis = arr.filter(r => !(0, twin_kernel_1.isEnergyRecord)(r) && !(0, twin_kernel_1.isEnergyEquipmentRecord)(r));
47
+ const epcisResult = epcis.length
48
+ ? (0, twin_kernel_1.ingest)(epcis.map(r => ({ ...r, sourceType: 'twin' })), TWIN_INGEST_RULES, { tenantId, defaultEventTime })
49
+ : { accepted: [], rejected: [] };
50
+ const energyResult = energy.length ? (0, twin_kernel_1.ingestEnergyRecords)(energy, { tenantId, defaultEventTime }) : { accepted: [], rejected: [] };
51
+ const equipmentResult = energyEquipment.length
52
+ ? (0, twin_kernel_1.ingestEnergyEquipmentRecords)(energyEquipment, { tenantId, defaultEventTime })
53
+ : { accepted: [], rejected: [] };
54
+ /* 거부분을 합쳐 돌려준다 — 어느 어휘에서 거부됐는지는 이유 문구가 말한다. */
55
+ return {
56
+ accepted: [...epcisResult.accepted, ...energyResult.accepted, ...equipmentResult.accepted],
57
+ rejected: [...epcisResult.rejected, ...energyResult.rejected, ...equipmentResult.rejected]
58
+ };
37
59
  }
38
60
  //# sourceMappingURL=canonical-ingest.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"canonical-ingest.js","sourceRoot":"","sources":["../../server/engine/canonical-ingest.ts"],"names":[],"mappings":";;AAyCA,wDAQC;AAjDD,sDAAkF;AAElF;;;;;;;;;;;GAWG;AAEH,gEAAgE;AAChE,MAAM,iBAAiB,GAAkB;IACvC;QACE,UAAU,EAAE,MAAM;QAClB,OAAO,EAAE;YACP,IAAI,EAAE,aAAa;YACnB,MAAM,EAAE,UAAU;YAClB,OAAO,EAAE,WAAW;YACpB,WAAW,EAAE,eAAe;YAC5B,GAAG,EAAE,OAAO;YACZ,SAAS,EAAE,aAAa;YACxB,WAAW,EAAE,eAAe;SAC7B;KACF;CACF,CAAA;AAWD,yEAAyE;AACzE,SAAgB,sBAAsB,CACpC,OAA+D,EAC/D,QAAgB,EAChB,gBAAwB;IAExB,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACvE,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;IAC3D,OAAO,IAAA,oBAAM,EAAC,MAAM,EAAE,iBAAiB,EAAE,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC,CAAA;AAC1E,CAAC","sourcesContent":["import { ingest, type AdapterRule, type IngestResult } from '@operato/twin-kernel'\n\n/*\n * 정규 레코드 → 커널 검증 ACL → CanonicalEnvelope (P1 seam 코어).\n * 순수(커널만 의존; shell·integration-base 무의존 → node:test 검증 가능).\n * 매핑(레거시 스키마 → 정규)은 이 앞 단계(jsonata step 또는 커넥터 openLiveFeed)가 끝낸다.\n * 여기 도착하는 레코드는 이미 정규 필드다: { epc, action, bizStep, disposition?, readPoint?, bizLocation? }\n * 커널은 objectEvent 구성 + EPCIS 검증(위반=오염 차단) + 봉투 래핑만(방언 없음).\n *\n * 두 소비처가 이 단일 코어를 공유(중복 금지):\n * - sim/scenario 경로: operato-twin twin-ingest 태스크(http-get→jsonata→여기).\n * - live 커넥터 경로: reference-live(어댑터 openLiveFeed→여기).\n * SoT: design/integration/face2-adapters.md §5(seam)·§7(매핑=밖, 검증=커널).\n */\n\n/** 고정 정규 매핑(방언 아님 — 필드명 자체가 정규 계약). sourceType='twin' 으로 매칭. */\nconst TWIN_INGEST_RULES: AdapterRule[] = [\n {\n sourceType: 'twin',\n mapping: {\n type: 'ObjectEvent',\n action: '$.action',\n bizStep: '$.bizStep',\n disposition: '$.disposition',\n epc: '$.epc',\n readPoint: '$.readPoint',\n bizLocation: '$.bizLocation'\n }\n }\n]\n\nexport interface CanonicalRecord {\n epc: string\n action: string // ADD | OBSERVE | DELETE\n bizStep: string\n disposition?: string\n readPoint?: string\n bizLocation?: string\n}\n\n/** 정규 레코드[] → 검증된 CanonicalEnvelope[] (+ 거부분). 순수 함수. 단일→객체도 배열로 정규화. */\nexport function ingestCanonicalRecords(\n records: CanonicalRecord | CanonicalRecord[] | undefined | null,\n tenantId: string,\n defaultEventTime: string\n): IngestResult {\n const arr = Array.isArray(records) ? records : records ? [records] : []\n const tagged = arr.map(r => ({ ...r, sourceType: 'twin' }))\n return ingest(tagged, TWIN_INGEST_RULES, { tenantId, defaultEventTime })\n}\n"]}
1
+ {"version":3,"file":"canonical-ingest.js","sourceRoot":"","sources":["../../server/engine/canonical-ingest.ts"],"names":[],"mappings":";;AA2CA,wDAsCC;AAjFD,sDAE6E;AAE7E;;;;;;;;;;;GAWG;AAEH,gEAAgE;AAChE,MAAM,iBAAiB,GAAkB;IACvC;QACE,UAAU,EAAE,MAAM;QAClB,OAAO,EAAE;YACP,IAAI,EAAE,aAAa;YACnB,MAAM,EAAE,UAAU;YAClB,OAAO,EAAE,WAAW;YACpB,WAAW,EAAE,eAAe;YAC5B,GAAG,EAAE,OAAO;YACZ,SAAS,EAAE,aAAa;YACxB,WAAW,EAAE,eAAe;SAC7B;KACF;CACF,CAAA;AAWD,yEAAyE;AACzE,SAAgB,sBAAsB,CACpC,OAMQ,EACR,QAAgB,EAChB,gBAAwB;IAExB,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACvE;;;;;;OAMG;IACH,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,4BAAc,CAAmB,CAAA;IAC3D;uDACmD;IACnD,MAAM,eAAe,GAAG,GAAG,CAAC,MAAM,CAAC,qCAAuB,CAAuC,CAAA;IACjG,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAA,4BAAc,EAAC,CAAC,CAAC,IAAI,CAAC,IAAA,qCAAuB,EAAC,CAAC,CAAC,CAAsB,CAAA;IAErG,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM;QAC9B,CAAC,CAAC,IAAA,oBAAM,EAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,iBAAiB,EAAE,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC;QAC3G,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAA;IAClC,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAA,iCAAmB,EAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAA;IACjI,MAAM,eAAe,GAAG,eAAe,CAAC,MAAM;QAC5C,CAAC,CAAC,IAAA,0CAA4B,EAAC,eAAe,EAAE,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC;QAC/E,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAA;IAElC,+CAA+C;IAC/C,OAAO;QACL,QAAQ,EAAE,CAAC,GAAG,WAAW,CAAC,QAAQ,EAAE,GAAG,YAAY,CAAC,QAAQ,EAAE,GAAG,eAAe,CAAC,QAAQ,CAAC;QAC1F,QAAQ,EAAE,CAAC,GAAG,WAAW,CAAC,QAAQ,EAAE,GAAG,YAAY,CAAC,QAAQ,EAAE,GAAG,eAAe,CAAC,QAAQ,CAAC;KAC3F,CAAA;AACH,CAAC","sourcesContent":["import { ingest, ingestEnergyRecords, ingestEnergyEquipmentRecords, isEnergyRecord,\n isEnergyEquipmentRecord, type AdapterRule, type EnergyRecord,\n type EnergyEquipmentRecord, type IngestResult } from '@operato/twin-kernel'\n\n/*\n * 정규 레코드 → 커널 검증 ACL → CanonicalEnvelope (P1 seam 코어).\n * 순수(커널만 의존; shell·integration-base 무의존 → node:test 검증 가능).\n * 매핑(레거시 스키마 → 정규)은 이 앞 단계(jsonata step 또는 커넥터 openLiveFeed)가 끝낸다.\n * 여기 도착하는 레코드는 이미 정규 필드다: { epc, action, bizStep, disposition?, readPoint?, bizLocation? }\n * 커널은 objectEvent 구성 + EPCIS 검증(위반=오염 차단) + 봉투 래핑만(방언 없음).\n *\n * 두 소비처가 이 단일 코어를 공유(중복 금지):\n * - sim/scenario 경로: operato-twin twin-ingest 태스크(http-get→jsonata→여기).\n * - live 커넥터 경로: reference-live(어댑터 openLiveFeed→여기).\n * SoT: design/integration/face2-adapters.md §5(seam)·§7(매핑=밖, 검증=커널).\n */\n\n/** 고정 정규 매핑(방언 아님 — 필드명 자체가 정규 계약). sourceType='twin' 으로 매칭. */\nconst TWIN_INGEST_RULES: AdapterRule[] = [\n {\n sourceType: 'twin',\n mapping: {\n type: 'ObjectEvent',\n action: '$.action',\n bizStep: '$.bizStep',\n disposition: '$.disposition',\n epc: '$.epc',\n readPoint: '$.readPoint',\n bizLocation: '$.bizLocation'\n }\n }\n]\n\nexport interface CanonicalRecord {\n epc: string\n action: string // ADD | OBSERVE | DELETE\n bizStep: string\n disposition?: string\n readPoint?: string\n bizLocation?: string\n}\n\n/** 정규 레코드[] → 검증된 CanonicalEnvelope[] (+ 거부분). 순수 함수. 단일→객체도 배열로 정규화. */\nexport function ingestCanonicalRecords(\n records:\n | CanonicalRecord\n | EnergyRecord\n | EnergyEquipmentRecord\n | (CanonicalRecord | EnergyRecord | EnergyEquipmentRecord)[]\n | undefined\n | null,\n tenantId: string,\n defaultEventTime: string\n): IngestResult {\n const arr = Array.isArray(records) ? records : records ? [records] : []\n /*\n * **어휘가 둘이다** — 물류(EPCIS)와 에너지. 예전에는 이 함수가 무조건 `ObjectEvent` 를 만들었고,\n * 그래서 계측을 이 길로 넣으면 `epc` 가 없어 거부되거나(그나마 나은 쪽) 엉뚱한 물품 관측이 됐다.\n *\n * 판정은 커널이 한 곳에서 한다(`isEnergyRecord`) — 여기서 다시 짐작하면 소비처마다 답이 달라진다.\n * 봉투는 같은 것을 쓰므로 아래 소비처(미러·저널·방송)는 이 갈림을 알 필요가 없다.\n */\n const energy = arr.filter(isEnergyRecord) as EnergyRecord[]\n /* 세 번째 어휘 — 설비가 낸 자기 에너지 상태(발전·저장·감축 여지·개폐 위치). 계량과 갈라 두는\n 이유는 커널에 적혀 있다: 계량은 구간에 누적되고, 이것들은 그 설비의 지금이다. */\n const energyEquipment = arr.filter(isEnergyEquipmentRecord) as unknown as EnergyEquipmentRecord[]\n const epcis = arr.filter(r => !isEnergyRecord(r) && !isEnergyEquipmentRecord(r)) as CanonicalRecord[]\n\n const epcisResult = epcis.length\n ? ingest(epcis.map(r => ({ ...r, sourceType: 'twin' })), TWIN_INGEST_RULES, { tenantId, defaultEventTime })\n : { accepted: [], rejected: [] }\n const energyResult = energy.length ? ingestEnergyRecords(energy, { tenantId, defaultEventTime }) : { accepted: [], rejected: [] }\n const equipmentResult = energyEquipment.length\n ? ingestEnergyEquipmentRecords(energyEquipment, { tenantId, defaultEventTime })\n : { accepted: [], rejected: [] }\n\n /* 거부분을 합쳐 돌려준다 — 어느 어휘에서 거부됐는지는 이유 문구가 말한다. */\n return {\n accepted: [...epcisResult.accepted, ...energyResult.accepted, ...equipmentResult.accepted],\n rejected: [...epcisResult.rejected, ...energyResult.rejected, ...equipmentResult.rejected]\n }\n}\n"]}
@@ -0,0 +1,33 @@
1
+ /** 라우팅에 필요한 인스턴스 사실만 — 엔진 객체를 그대로 받지 않는다(순수하게 유지). */
2
+ export interface RoutableInstance {
3
+ /** 'live' 면 현실을 비추는 트윈(미러). 그 외는 시뮬. */
4
+ mode?: string;
5
+ /** 커널 런타임(tick/dispatch)이 붙어 있나 — 시뮬 경로에서만 만들어진다. */
6
+ hasRuntime: boolean;
7
+ /** 관측 커널이 dispatch 를 받을 수 있나. */
8
+ hasKernelDispatch: boolean;
9
+ }
10
+ export type CommandRoute =
11
+ /** 커널 런타임으로(시뮬 — 커널이 세계다). */
12
+ {
13
+ target: 'runtime';
14
+ }
15
+ /** 관측 커널로(미러 — 주석만). 방출된 사실은 저널 큐에 실어야 한다. */
16
+ | {
17
+ target: 'kernel';
18
+ }
19
+ /** 보내지 않는다. 사유는 언어 중립 코드 — 화면이 `twin.cmderr.<code>` 로 사람 말을 만든다. */
20
+ | {
21
+ target: 'reject';
22
+ errorCode: string;
23
+ errorParams?: Record<string, string | number>;
24
+ };
25
+ /** 이 커맨드가 실 프로세스를 바꾸는 것인가(= 액추에이션). */
26
+ export declare const isActuation: (type: string) => boolean;
27
+ /**
28
+ * 어디로 보낼지 정한다.
29
+ *
30
+ * `inst` 가 없거나 호출자 도메인 소유가 아니면 **같은 사유로** 거절한다 — 존재 여부를 알려 주면
31
+ * 다른 테넌트의 인스턴스 id 를 추측할 수 있다(있음/없음이 구별되면 그것이 정보다).
32
+ */
33
+ export declare function routeCommand(inst: RoutableInstance | undefined, ownedByCaller: boolean, type: string): CommandRoute;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /*
3
+ * 커맨드 라우팅 판정 — **순수**. "이 커맨드를 누구에게 보낼 것인가" 만 정한다.
4
+ * 실제 dispatch·저널 기록은 엔진이 한다(`design/plans/command-routing.md` §1·§4 P0).
5
+ *
6
+ * ── 왜 떼어냈나 ─────────────────────────────────────────────────────────────
7
+ * 예전에는 리졸버가 `inst.runtime.dispatch(...)` 를 곧바로 불렀다. `runtime` 은 **시뮬 경로에서만**
8
+ * 만들어지므로 실물을 비추는 트윈(mirror)에서는 그 한 줄이 `TypeError` 로 터졌다 — 조치 채널이
9
+ * 통째로 닿지 않았고, 사용자에게는 이유 있는 거절이 아니라 그냥 실패였다.
10
+ *
11
+ * 판정이 코드 한가운데 있으면 이런 부류를 테스트로 못 잡는다(엔진은 DB·TypeORM 을 물고 있어 단위
12
+ * 테스트가 불러올 수 없다). 그래서 판정만 순수하게 떼어 둔다 — `planWarmStart` 와 같은 이유다.
13
+ *
14
+ * ── 판정 규칙 = 권위 × 성격 ─────────────────────────────────────────────────
15
+ * | 성격 \ 모드 | 시뮬 | 미러(현실을 비춤) |
16
+ * | 액추에이션 | 커널 런타임 | **거절**(아웃바운드 어댑터 미결선) |
17
+ * | 주석(확인 ack) | 커널 런타임 | 관측 커널 |
18
+ *
19
+ * 미러에서 액추에이션을 그냥 허용하면 **새 거짓말이 생긴다**: 트윈은 "보류됨" 이라 말하고 실 시스템은
20
+ * 모른다. 조용히 커널만 바꾸지 않고, 이유를 밝혀 거절한다.
21
+ *
22
+ * 반면 확인(ack)은 액추에이션이 아니다 — 신호를 봤다는 트윈 쪽 주석이라 현실을 건드리지 않는다.
23
+ * 미러에서도 성립해야 하므로 관측 커널로 보낸다.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.isActuation = void 0;
27
+ exports.routeCommand = routeCommand;
28
+ /** 관측 쪽 주석 커맨드 — 현실을 바꾸지 않으므로 미러에서도 통과한다. */
29
+ const ANNOTATION_COMMANDS = new Set(['attention.ack']);
30
+ /** 이 커맨드가 실 프로세스를 바꾸는 것인가(= 액추에이션). */
31
+ const isActuation = (type) => !ANNOTATION_COMMANDS.has(type);
32
+ exports.isActuation = isActuation;
33
+ /**
34
+ * 어디로 보낼지 정한다.
35
+ *
36
+ * `inst` 가 없거나 호출자 도메인 소유가 아니면 **같은 사유로** 거절한다 — 존재 여부를 알려 주면
37
+ * 다른 테넌트의 인스턴스 id 를 추측할 수 있다(있음/없음이 구별되면 그것이 정보다).
38
+ */
39
+ function routeCommand(inst, ownedByCaller, type) {
40
+ if (!inst || !ownedByCaller)
41
+ return { target: 'reject', errorCode: 'no-instance' };
42
+ if (!type)
43
+ return { target: 'reject', errorCode: 'unknown-command' };
44
+ if (inst.mode !== 'live') {
45
+ /* 시뮬 — 커널이 세계다. 런타임이 없으면 예외를 던지지 않고 이유를 말한다. */
46
+ return inst.hasRuntime ? { target: 'runtime' } : { target: 'reject', errorCode: 'no-runtime' };
47
+ }
48
+ /* 미러 — 실 프로세스 변경은 아웃바운드 어댑터의 몫이고 아직 결선되지 않았다. */
49
+ if ((0, exports.isActuation)(type))
50
+ return { target: 'reject', errorCode: 'actuation-not-wired', errorParams: { type } };
51
+ return inst.hasKernelDispatch ? { target: 'kernel' } : { target: 'reject', errorCode: 'no-runtime' };
52
+ }
53
+ //# sourceMappingURL=command-routing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-routing.js","sourceRoot":"","sources":["../../server/engine/command-routing.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;;;AAgCH,oCAYC;AA1CD,6CAA6C;AAC7C,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAS,CAAC,eAAe,CAAC,CAAC,CAAA;AAoB9D,uCAAuC;AAChC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AAAvE,QAAA,WAAW,eAA4D;AAEpF;;;;;GAKG;AACH,SAAgB,YAAY,CAAC,IAAkC,EAAE,aAAsB,EAAE,IAAY;IACnG,IAAI,CAAC,IAAI,IAAI,CAAC,aAAa;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,CAAA;IAClF,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAA;IAEpE,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACzB,gDAAgD;QAChD,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,CAAA;IAChG,CAAC;IAED,iDAAiD;IACjD,IAAI,IAAA,mBAAW,EAAC,IAAI,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,qBAAqB,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,EAAE,CAAA;IAC3G,OAAO,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,CAAA;AACtG,CAAC","sourcesContent":["/*\n * 커맨드 라우팅 판정 — **순수**. \"이 커맨드를 누구에게 보낼 것인가\" 만 정한다.\n * 실제 dispatch·저널 기록은 엔진이 한다(`design/plans/command-routing.md` §1·§4 P0).\n *\n * ── 왜 떼어냈나 ─────────────────────────────────────────────────────────────\n * 예전에는 리졸버가 `inst.runtime.dispatch(...)` 를 곧바로 불렀다. `runtime` 은 **시뮬 경로에서만**\n * 만들어지므로 실물을 비추는 트윈(mirror)에서는 그 한 줄이 `TypeError` 로 터졌다 — 조치 채널이\n * 통째로 닿지 않았고, 사용자에게는 이유 있는 거절이 아니라 그냥 실패였다.\n *\n * 판정이 코드 한가운데 있으면 이런 부류를 테스트로 못 잡는다(엔진은 DB·TypeORM 을 물고 있어 단위\n * 테스트가 불러올 수 없다). 그래서 판정만 순수하게 떼어 둔다 — `planWarmStart` 와 같은 이유다.\n *\n * ── 판정 규칙 = 권위 × 성격 ─────────────────────────────────────────────────\n * | 성격 \\ 모드 | 시뮬 | 미러(현실을 비춤) |\n * | 액추에이션 | 커널 런타임 | **거절**(아웃바운드 어댑터 미결선) |\n * | 주석(확인 ack) | 커널 런타임 | 관측 커널 |\n *\n * 미러에서 액추에이션을 그냥 허용하면 **새 거짓말이 생긴다**: 트윈은 \"보류됨\" 이라 말하고 실 시스템은\n * 모른다. 조용히 커널만 바꾸지 않고, 이유를 밝혀 거절한다.\n *\n * 반면 확인(ack)은 액추에이션이 아니다 — 신호를 봤다는 트윈 쪽 주석이라 현실을 건드리지 않는다.\n * 미러에서도 성립해야 하므로 관측 커널로 보낸다.\n */\n\n/** 관측 쪽 주석 커맨드 — 현실을 바꾸지 않으므로 미러에서도 통과한다. */\nconst ANNOTATION_COMMANDS = new Set<string>(['attention.ack'])\n\n/** 라우팅에 필요한 인스턴스 사실만 — 엔진 객체를 그대로 받지 않는다(순수하게 유지). */\nexport interface RoutableInstance {\n /** 'live' 면 현실을 비추는 트윈(미러). 그 외는 시뮬. */\n mode?: string\n /** 커널 런타임(tick/dispatch)이 붙어 있나 — 시뮬 경로에서만 만들어진다. */\n hasRuntime: boolean\n /** 관측 커널이 dispatch 를 받을 수 있나. */\n hasKernelDispatch: boolean\n}\n\nexport type CommandRoute =\n /** 커널 런타임으로(시뮬 — 커널이 세계다). */\n | { target: 'runtime' }\n /** 관측 커널로(미러 — 주석만). 방출된 사실은 저널 큐에 실어야 한다. */\n | { target: 'kernel' }\n /** 보내지 않는다. 사유는 언어 중립 코드 — 화면이 `twin.cmderr.<code>` 로 사람 말을 만든다. */\n | { target: 'reject'; errorCode: string; errorParams?: Record<string, string | number> }\n\n/** 이 커맨드가 실 프로세스를 바꾸는 것인가(= 액추에이션). */\nexport const isActuation = (type: string): boolean => !ANNOTATION_COMMANDS.has(type)\n\n/**\n * 어디로 보낼지 정한다.\n *\n * `inst` 가 없거나 호출자 도메인 소유가 아니면 **같은 사유로** 거절한다 — 존재 여부를 알려 주면\n * 다른 테넌트의 인스턴스 id 를 추측할 수 있다(있음/없음이 구별되면 그것이 정보다).\n */\nexport function routeCommand(inst: RoutableInstance | undefined, ownedByCaller: boolean, type: string): CommandRoute {\n if (!inst || !ownedByCaller) return { target: 'reject', errorCode: 'no-instance' }\n if (!type) return { target: 'reject', errorCode: 'unknown-command' }\n\n if (inst.mode !== 'live') {\n /* 시뮬 — 커널이 세계다. 런타임이 없으면 예외를 던지지 않고 이유를 말한다. */\n return inst.hasRuntime ? { target: 'runtime' } : { target: 'reject', errorCode: 'no-runtime' }\n }\n\n /* 미러 — 실 프로세스 변경은 아웃바운드 어댑터의 몫이고 아직 결선되지 않았다. */\n if (isActuation(type)) return { target: 'reject', errorCode: 'actuation-not-wired', errorParams: { type } }\n return inst.hasKernelDispatch ? { target: 'kernel' } : { target: 'reject', errorCode: 'no-runtime' }\n}\n"]}
@@ -0,0 +1,86 @@
1
+ /** 계통의 한 구간(수전·분기·구역) — 자리 하나가 곧 구간이다. */
2
+ export interface TopologySegment {
3
+ id: string;
4
+ /** 카탈로그 자리 타입(`incoming`·`feeder`·`submeter-zone`). */
5
+ type: string;
6
+ label?: string;
7
+ /** 이 자리에 달린 계량 지점 id 들 — 비어 있으면 **어두운 구간**이다(값을 알 길이 없다). */
8
+ meters: string[];
9
+ /** 이 구간의 전력량과 근거 — 폴드가 낸 것만 싣는다(우리가 다시 계산하지 않는다). */
10
+ kWh?: number;
11
+ basis?: 'measured' | 'apportioned';
12
+ /**
13
+ * **피크가 찍힌 순간** 이 구간이 얼마였나(kW) — 요금이 걸린 수다.
14
+ *
15
+ * 전력량이 큰 구간과 피크를 만든 구간은 다를 수 있다(하루 종일 조금씩 쓰는 부하 vs 잠깐 크게 쓰는 부하).
16
+ * 그림에 둘을 함께 두면 「어느 분기를 깎아야 피크가 내려가나」가 바로 읽힌다.
17
+ */
18
+ peakKW?: number;
19
+ /** 이 자리에 매달린 부하(설비) — 종류를 그대로 싣는다. */
20
+ loads: {
21
+ id: string;
22
+ kind: string;
23
+ }[];
24
+ /**
25
+ * 계통 설비(차단기·태양광·축전지) — 부하가 아니라 계통의 일부다.
26
+ *
27
+ * `live` 는 **지금 값**이다(구간 값이 아니다): 태양광이 지금 몇 kW 를 만들고, 축전지가 얼마를 내보내며
28
+ * 충전율이 얼마인가. 원천이 보내 주는 사실이라 우리가 만들지 않는다 — 보내지 않은 필드는 없다.
29
+ *
30
+ * 시각(`measuredAt`)을 함께 나른다: 계측이 끊기면 **멈춘 값을 지금 값으로 그리게** 되고, 그것이 화면이
31
+ * 낼 수 있는 가장 나쁜 거짓이다(값이 그럴듯할수록 위험하다).
32
+ */
33
+ gear: {
34
+ id: string;
35
+ kind: string;
36
+ live?: {
37
+ generatedKW?: number;
38
+ exportKW?: number;
39
+ chargeKW?: number;
40
+ dischargeKW?: number;
41
+ soc?: number;
42
+ measuredAt?: string;
43
+ };
44
+ }[];
45
+ children: TopologySegment[];
46
+ }
47
+ export interface EnergyTopology {
48
+ roots: TopologySegment[];
49
+ /**
50
+ * 자리를 모르는 설비 — 집이 없거나, 없는 자리를 가리킨다. 숨기면 그림의 설비 수가 모델과 달라진다.
51
+ */
52
+ unplaced: {
53
+ id: string;
54
+ kind: string;
55
+ }[];
56
+ /** 계량기가 없는 자리 id 들 — 「여기에 달면 무엇을 알 수 있나」의 답이 되는 목록이다. */
57
+ unmetered: string[];
58
+ /**
59
+ * 결선을 모르는 부하가 매달린 자리 — 커넥터가 「어느 분기가 이 설비에 전기를 주는지 모른다」고 판단해
60
+ * 수전에 매단 경우다. 그 자리의 몫이 분기로 내려가지 않는 이유가 이것이다.
61
+ */
62
+ loadsOnIncoming: number;
63
+ }
64
+ /**
65
+ * 모델 + 폴드 결과 → 계통 트리.
66
+ *
67
+ * @param model 트윈 모델(자리 계층 `parentId`, 설비 소속 `homeLocation`).
68
+ * @param energy 폴드가 낸 분기별 몫(있으면 노드에 싣는다 — 없으면 값 없이 구조만 그린다).
69
+ */
70
+ export declare function buildEnergyTopology(model: {
71
+ locations?: any[];
72
+ equipment?: any[];
73
+ } | undefined | null, energy?: {
74
+ byFeeder?: {
75
+ key: string;
76
+ kWh: number;
77
+ basis?: 'measured' | 'apportioned';
78
+ peakKW?: number;
79
+ }[];
80
+ } | null,
81
+ /**
82
+ * 지금의 설비 상태(스냅샷의 `equipment`) — 발전·저장의 **지금 값**을 기호 옆에 적기 위해서다.
83
+ *
84
+ * 없으면 값 없이 구조만 그린다. 여기서 값을 만들지 않는다: 원천이 보내지 않은 것은 모르는 것이다.
85
+ */
86
+ equipmentState?: readonly any[] | null): EnergyTopology;
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildEnergyTopology = buildEnergyTopology;
4
+ /*
5
+ * 전력 계통도 — **선언된 것만 그린다.** 순수.
6
+ *
7
+ * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────
8
+ * 분기별 막대는 「어디가 많이 쓰나」에 답하지만 **어디에 걸려 있나**에 답하지 못한다. 실제로 부딪힌
9
+ * 세 가지가 전부 계통 질문이었다:
10
+ * · 「하위 계량이 덮지 않는 327 kWh」 — 그 몫이 수전 직결 부하인지, 계량 없는 구간인지 표로는 모른다.
11
+ * · 커넥터는 결선을 모르는 부하를 **수전에 매단다**(정직한 선택). 그런데 그 사실이 화면에 없었다.
12
+ * · 「계량기를 어디에 더 달아야 하나」 — 어두운 구간이 보여야 답할 수 있다.
13
+ *
14
+ * ── 무엇을 그리지 않나 ─────────────────────────────────────────────────────
15
+ * **보호 계통 단선도(SLD)가 아니다.** 차단기 정격·보호 협조·상(phase)·결선 방식은 모델에 없고, 그럴듯한
16
+ * 단선도를 그리면 갖지 않은 공학적 충실도를 주장하게 된다. 여기서 그리는 것은 **트윈이 선언한 관계**
17
+ * (자리의 계층 + 설비의 소속)와 **계량이 실제로 덮는 범위**뿐이다. 한계는 소비처가 화면에 적는다.
18
+ *
19
+ * ── 조용히 빠뜨리지 않는다 ─────────────────────────────────────────────────
20
+ * 자리를 모르는 설비, 계량기가 없는 자리, 계량이 덮지 않는 몫은 **각자 자기 칸으로** 나온다. 그림에서
21
+ * 사라지면 사용자는 자기 현장이 다 그려졌다고 믿는다.
22
+ */
23
+ const twin_kernel_1 = require("@operato/twin-kernel");
24
+ /**
25
+ * 이 설비의 **지금 값** — 스냅샷에서 그 설비를 찾아 에너지 축의 값만 옮긴다.
26
+ *
27
+ * 종류가 아는 필드만 옮긴다: 태양광에 충전율을 붙이거나 축전지에 발전량을 붙이면, 화면은 그것을 사실로
28
+ * 그리고 사람은 그 설비가 그런 일을 한다고 읽는다(커널이 종류별로 필드를 가려 받는 것과 같은 규율).
29
+ */
30
+ function liveOf(state, id, kind) {
31
+ const e = (state ?? []).find(x => String(x?.id ?? '') === id);
32
+ if (!e)
33
+ return undefined;
34
+ const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : undefined);
35
+ const live = {};
36
+ if (kind === 'pv-array') {
37
+ const g = num(e.generatedKW);
38
+ const x = num(e.exportKW);
39
+ if (g !== undefined)
40
+ live.generatedKW = g;
41
+ if (x !== undefined)
42
+ live.exportKW = x;
43
+ }
44
+ else if (kind === 'battery') {
45
+ const soc = num(e.soc);
46
+ const dis = num(e.dischargeKW);
47
+ const cha = num(e.chargeKW);
48
+ if (soc !== undefined)
49
+ live.soc = soc;
50
+ if (dis !== undefined)
51
+ live.dischargeKW = dis;
52
+ if (cha !== undefined)
53
+ live.chargeKW = cha;
54
+ }
55
+ if (!Object.keys(live).length)
56
+ return undefined;
57
+ /* 값이 있으면 시각도 함께 — 없으면 붙이지 않는다(모르는 시각을 지금으로 만들지 않는다). */
58
+ if (e.measuredAt)
59
+ live.measuredAt = String(e.measuredAt);
60
+ return { live };
61
+ }
62
+ const ELECTRICAL = new Set(twin_kernel_1.EMS_LOCATION_TYPES);
63
+ /** 부하가 아닌 계통 설비 — 이것들의 소비를 부하로 세면 총량이 이중으로 나뉜다. */
64
+ const GEAR = new Set(['meter', 'breaker', 'pv-array', 'battery']);
65
+ /**
66
+ * 모델 + 폴드 결과 → 계통 트리.
67
+ *
68
+ * @param model 트윈 모델(자리 계층 `parentId`, 설비 소속 `homeLocation`).
69
+ * @param energy 폴드가 낸 분기별 몫(있으면 노드에 싣는다 — 없으면 값 없이 구조만 그린다).
70
+ */
71
+ function buildEnergyTopology(model, energy,
72
+ /**
73
+ * 지금의 설비 상태(스냅샷의 `equipment`) — 발전·저장의 **지금 값**을 기호 옆에 적기 위해서다.
74
+ *
75
+ * 없으면 값 없이 구조만 그린다. 여기서 값을 만들지 않는다: 원천이 보내지 않은 것은 모르는 것이다.
76
+ */
77
+ equipmentState) {
78
+ const locations = (model?.locations ?? []).filter(n => n?.id && ELECTRICAL.has(String(n.type)));
79
+ const byId = new Map();
80
+ for (const n of locations) {
81
+ byId.set(String(n.id), {
82
+ id: String(n.id),
83
+ type: String(n.type),
84
+ ...(n.name ? { label: String(n.name) } : {}),
85
+ meters: [],
86
+ loads: [],
87
+ gear: [],
88
+ children: []
89
+ });
90
+ }
91
+ const share = new Map((energy?.byFeeder ?? []).map(f => [f.key, f]));
92
+ for (const [id, segment] of byId) {
93
+ const s = share.get(id);
94
+ if (s) {
95
+ segment.kWh = s.kWh;
96
+ if (s.basis)
97
+ segment.basis = s.basis;
98
+ if (typeof s.peakKW === 'number')
99
+ segment.peakKW = s.peakKW;
100
+ }
101
+ }
102
+ /* 설비를 자기 자리에 붙인다 — 계량기는 「이 구간을 잰다」는 뜻이라 따로 모은다. */
103
+ const unplaced = [];
104
+ let loadsOnIncoming = 0;
105
+ for (const e of (model?.equipment ?? [])) {
106
+ if (!e?.id)
107
+ continue;
108
+ const kind = String(e.kind ?? '');
109
+ const home = String(e.homeLocation ?? e.homeLocationId ?? '');
110
+ const at = byId.get(home);
111
+ if (!at) {
112
+ /* 전기 계통 밖의 자리에 사는 설비도 여기서는 「자리를 모른다」로 모은다 — 이 그림의 축은 계통이다. */
113
+ unplaced.push({ id: String(e.id), kind });
114
+ continue;
115
+ }
116
+ if (kind === 'meter')
117
+ at.meters.push(String(e.id));
118
+ else if (GEAR.has(kind))
119
+ at.gear.push({ id: String(e.id), kind, ...(liveOf(equipmentState, String(e.id), kind) ?? {}) });
120
+ else {
121
+ at.loads.push({ id: String(e.id), kind });
122
+ if (at.type === 'incoming')
123
+ loadsOnIncoming++;
124
+ }
125
+ }
126
+ /*
127
+ * 계층을 잇는 것은 **전기 상류**다 — 공간 부모(공장·구역)는 이 축이 아니다. 상류의 뜻은 커널이 읽는다
128
+ * (옛 세대는 그 안에서 흡수된다) — 여기서 다시 판정하면 화면과 계산이 다른 계통을 말하게 된다.
129
+ */
130
+ const roots = [];
131
+ const all = model?.locations ?? [];
132
+ for (const n of locations) {
133
+ const segment = byId.get(String(n.id));
134
+ const up = (0, twin_kernel_1.electricalUpstreamOf)(all, String(n.id));
135
+ const parent = up ? byId.get(up) : undefined;
136
+ if (parent && parent !== segment)
137
+ parent.children.push(segment);
138
+ else
139
+ roots.push(segment);
140
+ }
141
+ const unmetered = [...byId.values()].filter(n => !n.meters.length).map(n => n.id);
142
+ return { roots, unplaced, unmetered, loadsOnIncoming };
143
+ }
144
+ //# sourceMappingURL=energy-topology.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"energy-topology.js","sourceRoot":"","sources":["../../server/engine/energy-topology.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 전력 계통도 — **선언된 것만 그린다.** 순수.\n *\n * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────\n * 분기별 막대는 「어디가 많이 쓰나」에 답하지만 **어디에 걸려 있나**에 답하지 못한다. 실제로 부딪힌\n * 세 가지가 전부 계통 질문이었다:\n * · 「하위 계량이 덮지 않는 327 kWh」 — 그 몫이 수전 직결 부하인지, 계량 없는 구간인지 표로는 모른다.\n * · 커넥터는 결선을 모르는 부하를 **수전에 매단다**(정직한 선택). 그런데 그 사실이 화면에 없었다.\n * · 「계량기를 어디에 더 달아야 하나」 — 어두운 구간이 보여야 답할 수 있다.\n *\n * ── 무엇을 그리지 않나 ─────────────────────────────────────────────────────\n * **보호 계통 단선도(SLD)가 아니다.** 차단기 정격·보호 협조·상(phase)·결선 방식은 모델에 없고, 그럴듯한\n * 단선도를 그리면 갖지 않은 공학적 충실도를 주장하게 된다. 여기서 그리는 것은 **트윈이 선언한 관계**\n * (자리의 계층 + 설비의 소속)와 **계량이 실제로 덮는 범위**뿐이다. 한계는 소비처가 화면에 적는다.\n *\n * ── 조용히 빠뜨리지 않는다 ─────────────────────────────────────────────────\n * 자리를 모르는 설비, 계량기가 없는 자리, 계량이 덮지 않는 몫은 **각자 자기 칸으로** 나온다. 그림에서\n * 사라지면 사용자는 자기 현장이 다 그려졌다고 믿는다.\n */\nimport { EMS_LOCATION_TYPES, electricalUpstreamOf } from '@operato/twin-kernel'\n\n/** 계통의 한 구간(수전·분기·구역) — 자리 하나가 곧 구간이다. */\nexport interface TopologySegment {\n id: string\n /** 카탈로그 자리 타입(`incoming`·`feeder`·`submeter-zone`). */\n type: string\n label?: string\n /** 이 자리에 달린 계량 지점 id 들 — 비어 있으면 **어두운 구간**이다(값을 알 길이 없다). */\n meters: string[]\n /** 이 구간의 전력량과 근거 — 폴드가 낸 것만 싣는다(우리가 다시 계산하지 않는다). */\n kWh?: number\n basis?: 'measured' | 'apportioned'\n /**\n * **피크가 찍힌 순간** 이 구간이 얼마였나(kW) — 요금이 걸린 수다.\n *\n * 전력량이 큰 구간과 피크를 만든 구간은 다를 수 있다(하루 종일 조금씩 쓰는 부하 vs 잠깐 크게 쓰는 부하).\n * 그림에 둘을 함께 두면 「어느 분기를 깎아야 피크가 내려가나」가 바로 읽힌다.\n */\n peakKW?: number\n /** 이 자리에 매달린 부하(설비) — 종류를 그대로 싣는다. */\n loads: { id: string; kind: string }[]\n /**\n * 계통 설비(차단기·태양광·축전지) — 부하가 아니라 계통의 일부다.\n *\n * `live` 는 **지금 값**이다(구간 값이 아니다): 태양광이 지금 몇 kW 를 만들고, 축전지가 얼마를 내보내며\n * 충전율이 얼마인가. 원천이 보내 주는 사실이라 우리가 만들지 않는다 — 보내지 않은 필드는 없다.\n *\n * 시각(`measuredAt`)을 함께 나른다: 계측이 끊기면 **멈춘 값을 지금 값으로 그리게** 되고, 그것이 화면이\n * 낼 수 있는 가장 나쁜 거짓이다(값이 그럴듯할수록 위험하다).\n */\n gear: {\n id: string\n kind: string\n live?: { generatedKW?: number; exportKW?: number; chargeKW?: number; dischargeKW?: number; soc?: number; measuredAt?: string }\n }[]\n children: TopologySegment[]\n}\n\nexport interface EnergyTopology {\n roots: TopologySegment[]\n /**\n * 자리를 모르는 설비 — 집이 없거나, 없는 자리를 가리킨다. 숨기면 그림의 설비 수가 모델과 달라진다.\n */\n unplaced: { id: string; kind: string }[]\n /** 계량기가 없는 자리 id 들 — 「여기에 달면 무엇을 알 수 있나」의 답이 되는 목록이다. */\n unmetered: string[]\n /**\n * 결선을 모르는 부하가 매달린 자리 — 커넥터가 「어느 분기가 이 설비에 전기를 주는지 모른다」고 판단해\n * 수전에 매단 경우다. 그 자리의 몫이 분기로 내려가지 않는 이유가 이것이다.\n */\n loadsOnIncoming: number\n}\n\n/**\n * 이 설비의 **지금 값** — 스냅샷에서 그 설비를 찾아 에너지 축의 값만 옮긴다.\n *\n * 종류가 아는 필드만 옮긴다: 태양광에 충전율을 붙이거나 축전지에 발전량을 붙이면, 화면은 그것을 사실로\n * 그리고 사람은 그 설비가 그런 일을 한다고 읽는다(커널이 종류별로 필드를 가려 받는 것과 같은 규율).\n */\nfunction liveOf(\n state: readonly any[] | undefined | null,\n id: string,\n kind: string\n): { live: NonNullable<TopologySegment['gear'][number]['live']> } | undefined {\n const e = (state ?? []).find(x => String(x?.id ?? '') === id)\n if (!e) return undefined\n const num = (v: unknown): number | undefined => (Number.isFinite(Number(v)) ? Number(v) : undefined)\n const live: NonNullable<TopologySegment['gear'][number]['live']> = {}\n if (kind === 'pv-array') {\n const g = num(e.generatedKW)\n const x = num(e.exportKW)\n if (g !== undefined) live.generatedKW = g\n if (x !== undefined) live.exportKW = x\n } else if (kind === 'battery') {\n const soc = num(e.soc)\n const dis = num(e.dischargeKW)\n const cha = num(e.chargeKW)\n if (soc !== undefined) live.soc = soc\n if (dis !== undefined) live.dischargeKW = dis\n if (cha !== undefined) live.chargeKW = cha\n }\n if (!Object.keys(live).length) return undefined\n /* 값이 있으면 시각도 함께 — 없으면 붙이지 않는다(모르는 시각을 지금으로 만들지 않는다). */\n if (e.measuredAt) live.measuredAt = String(e.measuredAt)\n return { live }\n}\n\nconst ELECTRICAL = new Set(EMS_LOCATION_TYPES as readonly string[])\n/** 부하가 아닌 계통 설비 — 이것들의 소비를 부하로 세면 총량이 이중으로 나뉜다. */\nconst GEAR = new Set(['meter', 'breaker', 'pv-array', 'battery'])\n\n/**\n * 모델 + 폴드 결과 → 계통 트리.\n *\n * @param model 트윈 모델(자리 계층 `parentId`, 설비 소속 `homeLocation`).\n * @param energy 폴드가 낸 분기별 몫(있으면 노드에 싣는다 — 없으면 값 없이 구조만 그린다).\n */\nexport function buildEnergyTopology(\n model: { locations?: any[]; equipment?: any[] } | undefined | null,\n energy?: { byFeeder?: { key: string; kWh: number; basis?: 'measured' | 'apportioned'; peakKW?: number }[] } | null,\n /**\n * 지금의 설비 상태(스냅샷의 `equipment`) — 발전·저장의 **지금 값**을 기호 옆에 적기 위해서다.\n *\n * 없으면 값 없이 구조만 그린다. 여기서 값을 만들지 않는다: 원천이 보내지 않은 것은 모르는 것이다.\n */\n equipmentState?: readonly any[] | null\n): EnergyTopology {\n const locations: any[] = (model?.locations ?? []).filter(n => n?.id && ELECTRICAL.has(String(n.type)))\n const byId = new Map<string, TopologySegment>()\n for (const n of locations) {\n byId.set(String(n.id), {\n id: String(n.id),\n type: String(n.type),\n ...(n.name ? { label: String(n.name) } : {}),\n meters: [],\n loads: [],\n gear: [],\n children: []\n })\n }\n\n const share = new Map((energy?.byFeeder ?? []).map(f => [f.key, f]))\n for (const [id, segment] of byId) {\n const s = share.get(id)\n if (s) {\n segment.kWh = s.kWh\n if (s.basis) segment.basis = s.basis\n if (typeof s.peakKW === 'number') segment.peakKW = s.peakKW\n }\n }\n\n /* 설비를 자기 자리에 붙인다 — 계량기는 「이 구간을 잰다」는 뜻이라 따로 모은다. */\n const unplaced: { id: string; kind: string }[] = []\n let loadsOnIncoming = 0\n for (const e of (model?.equipment ?? []) as any[]) {\n if (!e?.id) continue\n const kind = String(e.kind ?? '')\n const home = String(e.homeLocation ?? e.homeLocationId ?? '')\n const at = byId.get(home)\n if (!at) {\n /* 전기 계통 밖의 자리에 사는 설비도 여기서는 「자리를 모른다」로 모은다 — 이 그림의 축은 계통이다. */\n unplaced.push({ id: String(e.id), kind })\n continue\n }\n if (kind === 'meter') at.meters.push(String(e.id))\n else if (GEAR.has(kind)) at.gear.push({ id: String(e.id), kind, ...(liveOf(equipmentState, String(e.id), kind) ?? {}) })\n else {\n at.loads.push({ id: String(e.id), kind })\n if (at.type === 'incoming') loadsOnIncoming++\n }\n }\n\n /*\n * 계층을 잇는 것은 **전기 상류**다 — 공간 부모(공장·구역)는 이 축이 아니다. 상류의 뜻은 커널이 읽는다\n * (옛 세대는 그 안에서 흡수된다) — 여기서 다시 판정하면 화면과 계산이 다른 계통을 말하게 된다.\n */\n const roots: TopologySegment[] = []\n const all = model?.locations ?? []\n for (const n of locations) {\n const segment = byId.get(String(n.id))!\n const up = electricalUpstreamOf(all as any, String(n.id))\n const parent = up ? byId.get(up) : undefined\n if (parent && parent !== segment) parent.children.push(segment)\n else roots.push(segment)\n }\n\n const unmetered = [...byId.values()].filter(n => !n.meters.length).map(n => n.id)\n return { roots, unplaced, unmetered, loadsOnIncoming }\n}\n"]}
@@ -1,8 +1,19 @@
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';
5
+ export * from './kpi-baseline.js';
6
+ export * from './kpi-target.js';
7
+ export * from '@things-factory/headless-twin/dist-shared/kpi-broadcast.js';
8
+ export * from '@things-factory/headless-twin/dist-shared/entity-delta.js';
9
+ export * from '@things-factory/headless-twin/dist-shared/twin-level.js';
10
+ export * from './load-meter.js';
11
+ export * from './runtime-key.js';
4
12
  export * from './kpi-query.js';
5
13
  export * from './spec-coverage.js';
6
14
  export * from './travel-estimator.js';
7
15
  export * from './measured-estimator.js';
8
16
  export * from './model-basis.js';
17
+ export * from './live-feed-registry.js';
18
+ export * from './local-declarations.js';
19
+ export * from './operation-basis.js';