@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,102 @@
1
+ /*
2
+ * 주의 신호 요약 — **순수**. "화면이 실제로 쓰는 두 가지만 값싸게 만든다."
3
+ *
4
+ * ── 왜 있나 (2026-08-05) ────────────────────────────────────────────────────
5
+ * 축 선택으로 리프레시를 80KB → 2KB 로 줄였는데, **신호가 많은 트윈은 거기서 막혔다**: `hatio-yard`
6
+ * 는 신호 397건이 108KB 라 5배 감소에 그쳤다. 그런데 화면이 그 397건으로 하는 일은 둘뿐이다.
7
+ *
8
+ * ① 레일에 **20건**만 보여주고 나머지는 "N건 더" 라고만 말한다.
9
+ * ② 자리(폴리곤)를 칠할 **자리별 최고 심각도**를 뽑는다 — 397건이 자리 7곳의 색 7개로 접힌다.
10
+ *
11
+ * 즉 전량을 내려보내 클라이언트가 접는 구조였다. 접는 것은 값싸므로 **서버가 접어서 보낸다.**
12
+ *
13
+ * ── 조용히 자르지 않는다 ────────────────────────────────────────────────────
14
+ * 잘랐다는 사실과 **전체 개수**를 함께 보낸다. 경고 목록에서 조용한 누락은 "괜찮은 줄 알았다" 로
15
+ * 이어진다 — 레일이 이미 "N건 더" 를 말하고 있었고, 그 N 은 전체 개수에서 나와야 맞다.
16
+ *
17
+ * ── 정렬을 한 곳에 둔다 ─────────────────────────────────────────────────────
18
+ * 상위 N 을 서버가 고르므로 **클라이언트와 같은 순서**여야 한다. 순서가 다르면 서버가 고른 20건이
19
+ * 화면이 고를 20건과 달라지고, 그 어긋남은 아무 데서도 오류로 드러나지 않는다.
20
+ * 순서 = 확인된 것은 아래로, 그다음 심각도 높은 것부터(클라이언트 정렬과 동일).
21
+ */
22
+
23
+ /** 알람 표준(ISA-18.2) 우선순위 — 큰 값이 급하다. */
24
+ const RANK: Record<string, number> = { low: 1, medium: 2, high: 3, critical: 4 }
25
+
26
+ interface AttentionLike {
27
+ id?: string
28
+ /** 어느 트윈(렌즈)에서 온 신호인가 — 공간에서 모을 때 붙는다. */
29
+ instanceId?: string
30
+ severity?: string
31
+ state?: string
32
+ anchor?: { locationId?: string }
33
+ }
34
+
35
+ export interface AttentionDigestResult {
36
+ /** 상위 N — 레일이 그리는 것. */
37
+ attentions: AttentionLike[]
38
+ /** 전체 개수 — 레일의 "N건 더" 가 이 값에서 나온다(자른 사실을 밝힌다). */
39
+ attentionTotal: number
40
+ /**
41
+ * 자리별 최고 심각도 — **전량을 근거로** 접는다.
42
+ *
43
+ * 상위 N 만으로 접으면 20위 밖의 신호를 가진 자리가 색을 잃는다. 그것이 이 요약의 핵심 이유다:
44
+ * 레일은 잘라도 되지만 **지도 색은 잘라선 안 된다.**
45
+ */
46
+ severityByLocation: Record<string, string>
47
+ }
48
+
49
+ /** 화면 정렬과 같은 순서 — 확인된 것은 아래로, 그다음 심각도 내림차순. */
50
+ export const compareAttentions = (a: AttentionLike, b: AttentionLike): number =>
51
+ (a.state === 'acknowledged' ? 1 : 0) - (b.state === 'acknowledged' ? 1 : 0) ||
52
+ (RANK[b.severity ?? ''] ?? 0) - (RANK[a.severity ?? ''] ?? 0)
53
+
54
+ /**
55
+ * 신호를 요약한다.
56
+ *
57
+ * `limit` 이 없거나 0 이하면 **자르지 않는다** — 자르는 쪽이 기본이면 이 값을 모르는 소비처가
58
+ * 조용히 일부만 받는다(요약을 도입하다 만드는 전형적인 사고다).
59
+ */
60
+ export function digestAttentions(attentions: readonly AttentionLike[] | undefined | null, limit?: number): AttentionDigestResult {
61
+ const all = Array.isArray(attentions) ? attentions : []
62
+
63
+ /* 자리 색은 전량으로 접는다 — 잘린 뒤에 접으면 색을 잃는 자리가 생긴다. */
64
+ const severityByLocation: Record<string, string> = {}
65
+ for (const a of all) {
66
+ const nid = a?.anchor?.locationId
67
+ if (!nid) continue
68
+ const cur = severityByLocation[nid]
69
+ if (!cur || (RANK[a.severity ?? ''] ?? 0) > (RANK[cur] ?? 0)) severityByLocation[nid] = a.severity ?? ''
70
+ }
71
+
72
+ const sorted = [...all].sort(compareAttentions)
73
+ const capped = limit && limit > 0 ? sorted.slice(0, limit) : sorted
74
+ return { attentions: capped, attentionTotal: all.length, severityByLocation }
75
+ }
76
+
77
+ /** 렌즈 하나가 낸 신호 — 어느 트윈에서 왔는지와 함께. */
78
+ export interface LensAttentions {
79
+ instanceId: string
80
+ attentions: readonly AttentionLike[] | undefined | null
81
+ }
82
+
83
+ /**
84
+ * 여러 렌즈의 신호를 **한 현장의 목록으로** 모은다.
85
+ *
86
+ * 한 공장에 WMS·MES·YMS 가 함께 있으므로 "이 현장에서 봐야 할 것" 은 렌즈 하나로 답할 수 없다.
87
+ * 모으면서 지키는 것 셋:
88
+ *
89
+ * ① **어느 트윈에서 왔는지 붙인다**(`instanceId`) — 조치는 결국 그 트윈에 보내야 하고, 공간에서 보면
90
+ * 같은 자리 id 가 렌즈마다 다른 것을 가리킬 수 있다.
91
+ * ② **순서는 렌즈 순서가 아니라 급한 순서다** — 앞 렌즈의 낮은 신호가 뒤 렌즈의 심각한 신호를
92
+ * 밀어내면, 상위 N 만 보는 화면에서 정작 급한 것이 안 보인다.
93
+ * ③ **자리 색은 렌즈를 가로질러 가장 급한 것으로** 접는다 — 두 렌즈가 같은 자리를 가리키면 그 자리는
94
+ * 하나의 현실이므로 더 급한 쪽이 이긴다(둘 중 하나를 임의로 고르면 색이 렌즈 순서에 따라 달라진다).
95
+ */
96
+ export function mergeLensAttentions(lenses: readonly LensAttentions[], limit?: number): AttentionDigestResult {
97
+ const all: AttentionLike[] = []
98
+ for (const lens of lenses) {
99
+ for (const a of lens.attentions ?? []) all.push({ ...a, instanceId: lens.instanceId } as AttentionLike)
100
+ }
101
+ return digestAttentions(all, limit)
102
+ }
@@ -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
  }
@@ -0,0 +1,67 @@
1
+ /*
2
+ * 커맨드 라우팅 판정 — **순수**. "이 커맨드를 누구에게 보낼 것인가" 만 정한다.
3
+ * 실제 dispatch·저널 기록은 엔진이 한다(`design/plans/command-routing.md` §1·§4 P0).
4
+ *
5
+ * ── 왜 떼어냈나 ─────────────────────────────────────────────────────────────
6
+ * 예전에는 리졸버가 `inst.runtime.dispatch(...)` 를 곧바로 불렀다. `runtime` 은 **시뮬 경로에서만**
7
+ * 만들어지므로 실물을 비추는 트윈(mirror)에서는 그 한 줄이 `TypeError` 로 터졌다 — 조치 채널이
8
+ * 통째로 닿지 않았고, 사용자에게는 이유 있는 거절이 아니라 그냥 실패였다.
9
+ *
10
+ * 판정이 코드 한가운데 있으면 이런 부류를 테스트로 못 잡는다(엔진은 DB·TypeORM 을 물고 있어 단위
11
+ * 테스트가 불러올 수 없다). 그래서 판정만 순수하게 떼어 둔다 — `planWarmStart` 와 같은 이유다.
12
+ *
13
+ * ── 판정 규칙 = 권위 × 성격 ─────────────────────────────────────────────────
14
+ * | 성격 \ 모드 | 시뮬 | 미러(현실을 비춤) |
15
+ * | 액추에이션 | 커널 런타임 | **거절**(아웃바운드 어댑터 미결선) |
16
+ * | 주석(확인 ack) | 커널 런타임 | 관측 커널 |
17
+ *
18
+ * 미러에서 액추에이션을 그냥 허용하면 **새 거짓말이 생긴다**: 트윈은 "보류됨" 이라 말하고 실 시스템은
19
+ * 모른다. 조용히 커널만 바꾸지 않고, 이유를 밝혀 거절한다.
20
+ *
21
+ * 반면 확인(ack)은 액추에이션이 아니다 — 신호를 봤다는 트윈 쪽 주석이라 현실을 건드리지 않는다.
22
+ * 미러에서도 성립해야 하므로 관측 커널로 보낸다.
23
+ */
24
+
25
+ /** 관측 쪽 주석 커맨드 — 현실을 바꾸지 않으므로 미러에서도 통과한다. */
26
+ const ANNOTATION_COMMANDS = new Set<string>(['attention.ack'])
27
+
28
+ /** 라우팅에 필요한 인스턴스 사실만 — 엔진 객체를 그대로 받지 않는다(순수하게 유지). */
29
+ export interface RoutableInstance {
30
+ /** 'live' 면 현실을 비추는 트윈(미러). 그 외는 시뮬. */
31
+ mode?: string
32
+ /** 커널 런타임(tick/dispatch)이 붙어 있나 — 시뮬 경로에서만 만들어진다. */
33
+ hasRuntime: boolean
34
+ /** 관측 커널이 dispatch 를 받을 수 있나. */
35
+ hasKernelDispatch: boolean
36
+ }
37
+
38
+ export type CommandRoute =
39
+ /** 커널 런타임으로(시뮬 — 커널이 세계다). */
40
+ | { target: 'runtime' }
41
+ /** 관측 커널로(미러 — 주석만). 방출된 사실은 저널 큐에 실어야 한다. */
42
+ | { target: 'kernel' }
43
+ /** 보내지 않는다. 사유는 언어 중립 코드 — 화면이 `twin.cmderr.<code>` 로 사람 말을 만든다. */
44
+ | { target: 'reject'; errorCode: string; errorParams?: Record<string, string | number> }
45
+
46
+ /** 이 커맨드가 실 프로세스를 바꾸는 것인가(= 액추에이션). */
47
+ export const isActuation = (type: string): boolean => !ANNOTATION_COMMANDS.has(type)
48
+
49
+ /**
50
+ * 어디로 보낼지 정한다.
51
+ *
52
+ * `inst` 가 없거나 호출자 도메인 소유가 아니면 **같은 사유로** 거절한다 — 존재 여부를 알려 주면
53
+ * 다른 테넌트의 인스턴스 id 를 추측할 수 있다(있음/없음이 구별되면 그것이 정보다).
54
+ */
55
+ export function routeCommand(inst: RoutableInstance | undefined, ownedByCaller: boolean, type: string): CommandRoute {
56
+ if (!inst || !ownedByCaller) return { target: 'reject', errorCode: 'no-instance' }
57
+ if (!type) return { target: 'reject', errorCode: 'unknown-command' }
58
+
59
+ if (inst.mode !== 'live') {
60
+ /* 시뮬 — 커널이 세계다. 런타임이 없으면 예외를 던지지 않고 이유를 말한다. */
61
+ return inst.hasRuntime ? { target: 'runtime' } : { target: 'reject', errorCode: 'no-runtime' }
62
+ }
63
+
64
+ /* 미러 — 실 프로세스 변경은 아웃바운드 어댑터의 몫이고 아직 결선되지 않았다. */
65
+ if (isActuation(type)) return { target: 'reject', errorCode: 'actuation-not-wired', errorParams: { type } }
66
+ return inst.hasKernelDispatch ? { target: 'kernel' } : { target: 'reject', errorCode: 'no-runtime' }
67
+ }
@@ -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,8 +1,25 @@
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
+ /* 지표 정본 표(TWIN_METRIC)·목표 판정 — 소비처(twin-ai 등)가 지표 이름을 다시 열거하지 않게. */
7
+ export * from './kpi-target.js'
8
+ export * from '@things-factory/headless-twin/dist-shared/kpi-broadcast.js'
9
+ /* 집약 채널 태그(접두사 + 트윈별 태그 생성) — 소비처가 문자열을 다시 적지 않게 내보낸다. */
10
+ export * from '@things-factory/headless-twin/dist-shared/entity-delta.js'
11
+ /* 공간 계층의 단계 — 씬 표현·성과 카드·AI 가 같은 목록을 쓴다. */
12
+ export * from '@things-factory/headless-twin/dist-shared/twin-level.js'
13
+ /* 부하 계기판 — 어떤 작업이 이벤트 루프를 얼마나 쓰는지. */
14
+ export * from './load-meter.js'
15
+ /* 런타임 레지스트리 키 규약 — 소비처(다른 키 캐시·테스트)가 `:` 를 다시 적지 않게 함께 내보낸다. */
16
+ export * from './runtime-key.js'
4
17
  export * from './kpi-query.js'
5
18
  export * from './spec-coverage.js'
6
19
  export * from './travel-estimator.js'
7
20
  export * from './measured-estimator.js'
8
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'
@@ -0,0 +1,202 @@
1
+ /*
2
+ * 여러 구간 동시 비교 — **"어제보다 나아졌다" 를 넘어선다.** 순수.
3
+ *
4
+ * ── 왜 필요한가 (성과 맵 ③) ─────────────────────────────────────────────────
5
+ * 지금 비교 축은 **한 구간**만 본다(직전 구간 또는 하루 전). 그래서 판단이 딱 한 문장에서 멈춘다:
6
+ * "어제보다 나아졌다." **어제도 나빴으면 그 말은 쓸모가 없다.** 목표(`kpi-target`)가 그 한계를 한쪽에서
7
+ * 풀지만, 목표를 아직 걸지 않은 현장은 여전히 자기 숫자가 평소와 같은지도 알 수 없다.
8
+ *
9
+ * 최근 며칠의 **같은 시간대**를 함께 접으면 목표 없이도 말할 수 있다 — "오늘 이 시간대는 지난 7일 중
10
+ * 가장 낮다." 그것이 이 파일이 만드는 것이다: 창 여러 개의 **분포**와 그 안에서 현재의 **자리**.
11
+ *
12
+ * ── 이 파일이 하지 않는 것 ──────────────────────────────────────────────────
13
+ * · **지표 어휘를 모른다.** 이름·방향·값 꺼내는 법은 `kpi-target.ts` 의 `TWIN_METRIC` 이 소유한다. 여기는
14
+ * **이미 꺼낸 값**만 받는다 — 어휘가 두 곳에 생기면 그 사이에 매핑 표가 자라고 그것이 곧 방언이다.
15
+ * (덤으로: 이 모듈은 런타임 import 가 없어 테스트가 소스를 그대로 든다.)
16
+ * · **DB·조회를 모른다.** 창 목록만 계산하고, 그 창을 읽는 것은 호출부의 일이다.
17
+ * · **좋고 나쁨의 임계를 발명하지 않는다.** "평소보다 나쁘다" 는 중앙값과의 비교로만 말한다 — 임의의
18
+ * 퍼센트 임계를 두면 그 숫자가 곧 숨은 목표가 된다.
19
+ *
20
+ * ── 정직성 규율 (이 도메인의 핵심) ──────────────────────────────────────────
21
+ * ① **결측 창을 조용히 빼지 않는다.** 요청한 창 전부를 `measured` 와 함께 돌려주고, 분포는 측정된
22
+ * 것만으로 만든다. 빼고 말하면 "지난 7일" 이 실제로는 3일이었다는 것을 아무도 모른다.
23
+ * ② **표본이 둘 미만이면 자리를 말하지 않는다.** 하나를 분포라 부를 수 없다.
24
+ * ③ **현재 값이 없으면 자리를 말하지 않는다.** 결측을 0 으로 읽으면 시간 지표는 "낮을수록 좋다" 라서
25
+ * **일이 없던 구간이 최고 성적**으로 보고된다(목표 판정에서 이미 겪은 결함이다).
26
+ * ④ 왜 말하지 않는지를 `reason` 으로 밝힌다 — 조용히 빈 값을 주면 화면이 "분포 없음" 을 "평범함" 으로
27
+ * 그린다.
28
+ */
29
+
30
+ /** 창 하나 — 폴드의 창과 같은 모양(ms). */
31
+ export interface BaselineWindow {
32
+ fromMs: number
33
+ toMs: number
34
+ }
35
+
36
+ /** 며칠 전 같은 시간대인가, 몇 주 전 같은 요일·시간대인가. */
37
+ export type BaselineStride = 'day' | 'week'
38
+
39
+ /** 한 번에 볼 수 있는 과거 구간 수 — 창마다 조회가 한 번 더 붙으므로 상한을 둔다. */
40
+ export const MAX_BASELINE_PERIODS = 14
41
+
42
+ const MS_DAY = 24 * 60 * 60_000
43
+
44
+ /**
45
+ * 비교할 과거 창들 — 현재 창을 **같은 길이로** 하루(또는 한 주)씩 뒤로 옮긴다.
46
+ *
47
+ * 가까운 것부터 준다(어제 → 그제 → …). 화면이 시간 순서를 다시 정하지 않아도 되게.
48
+ *
49
+ * **한계를 밝힌다**: 옮기는 것은 ms 산술이다. 트윈 공간은 고정 오프셋(`utcOffsetMinutes`)을 선언하므로
50
+ * 현재 모델에서는 "같은 시간대" 가 정확하다. 일광절약시간을 쓰는 시간대를 나중에 지원하면 전환이 있는
51
+ * 주의 하루가 한 시간 밀린다 — 그때는 오프셋 기준 캘린더 산술로 바꿔야 한다(지금 지어내지 않는다).
52
+ */
53
+ export function baselineWindows(
54
+ fromMs: number,
55
+ toMs: number,
56
+ periods: number,
57
+ stride: BaselineStride = 'day'
58
+ ): BaselineWindow[] {
59
+ if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || toMs <= fromMs) return []
60
+ const count = Math.max(0, Math.min(MAX_BASELINE_PERIODS, Math.floor(periods)))
61
+ const step = stride === 'week' ? 7 * MS_DAY : MS_DAY
62
+
63
+ return Array.from({ length: count }, (_, i) => ({
64
+ fromMs: fromMs - step * (i + 1),
65
+ toMs: toMs - step * (i + 1)
66
+ }))
67
+ }
68
+
69
+ /** 지표의 성질 — 이름과 값 꺼내기는 호출부(`TWIN_METRIC`)가 소유한다. */
70
+ export interface MetricSpecLike {
71
+ direction: 'higher' | 'lower'
72
+ unit: string
73
+ }
74
+
75
+ /** 과거 창 하나에서 꺼낸 값들 — 그 창에 기록이 없으면 값이 `undefined` 다. */
76
+ export interface BaselineSampleInput<M extends string> {
77
+ fromMs: number
78
+ toMs: number
79
+ values: Partial<Record<M, number | undefined>>
80
+ }
81
+
82
+ /** 과거 창 하나의 결과 — **결측도 남긴다**(빼면 "지난 7일" 이 거짓이 된다). */
83
+ export interface BaselineSample {
84
+ fromMs: number
85
+ toMs: number
86
+ measured: boolean
87
+ value?: number
88
+ }
89
+
90
+ /** 왜 자리를 말하지 않는지. */
91
+ export type BaselineNoPosition = 'no-current' | 'too-few-samples'
92
+
93
+ /**
94
+ * 분포 안에서 현재의 자리 — **방향을 적용한 읽기.**
95
+ *
96
+ * 임의의 임계가 없다: 표본 전부보다 좋으면 `best`, 전부보다 나쁘면 `worst`, 그 사이는 중앙값을 기준으로
97
+ * 구분한다. `typical` 은 중앙값과 **같을 때**만이다.
98
+ */
99
+ export type BaselineStanding = 'best' | 'better-than-typical' | 'typical' | 'worse-than-typical' | 'worst'
100
+
101
+ export interface MetricBaseline<M extends string = string> {
102
+ metric: M
103
+ direction: 'higher' | 'lower'
104
+ unit: string
105
+ /** 지금 창의 값 — 없으면 `undefined`(0 이 아니다). */
106
+ current?: number
107
+ /** 요청한 과거 창 전부(결측 포함, 가까운 것부터). */
108
+ samples: BaselineSample[]
109
+ /** 요청한 창 수 / 그중 실제로 측정된 수 — 둘을 함께 봐야 "지난 7일" 을 믿을 수 있다. */
110
+ requested: number
111
+ measuredCount: number
112
+ /** 측정된 표본만의 요약. 표본이 없으면 전부 `undefined`. */
113
+ min?: number
114
+ median?: number
115
+ max?: number
116
+ /** 측정 표본을 오름차순으로 놓았을 때 현재 값이 몇 번째인가(1 = 가장 작음) — **방향 없는 사실**. */
117
+ rank?: number
118
+ /** 그 순위를 셀 때의 표본 수(+현재 1). 순위만 보면 표본이 둘이었는지 열이었는지 알 수 없다. */
119
+ rankOf?: number
120
+ /** 방향을 적용한 읽기. 말할 수 없으면 없다. */
121
+ standing?: BaselineStanding
122
+ /** 자리를 말하지 않은 이유. */
123
+ noPosition?: BaselineNoPosition
124
+ }
125
+
126
+ /** 중앙값 — 짝수 개면 가운데 둘의 평균(폴드의 분위수 규약과 달리 여기 표본은 아주 작다). */
127
+ function medianOf(sorted: readonly number[]): number | undefined {
128
+ if (!sorted.length) return undefined
129
+ const mid = sorted.length >> 1
130
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2
131
+ }
132
+
133
+ /**
134
+ * 지표별로 분포와 자리를 만든다.
135
+ *
136
+ * @param specs 지표 성질(방향·단위). 열거된 지표만 낸다 — 모르는 이름을 짐작하지 않는다.
137
+ * @param current 지금 창에서 꺼낸 값들(결측은 `undefined`).
138
+ * @param inputs 과거 창들에서 꺼낸 값들(가까운 것부터).
139
+ */
140
+ export function summarizeBaseline<M extends string>(
141
+ specs: Readonly<Record<M, MetricSpecLike>>,
142
+ current: Partial<Record<M, number | undefined>>,
143
+ inputs: readonly BaselineSampleInput<M>[]
144
+ ): MetricBaseline<M>[] {
145
+ const out: MetricBaseline<M>[] = []
146
+
147
+ for (const metric of Object.keys(specs) as M[]) {
148
+ const spec = specs[metric]
149
+ const samples: BaselineSample[] = inputs.map(s => {
150
+ const v = s.values[metric]
151
+ const measured = typeof v === 'number' && Number.isFinite(v)
152
+ return measured ? { fromMs: s.fromMs, toMs: s.toMs, measured: true, value: v as number } : { fromMs: s.fromMs, toMs: s.toMs, measured: false }
153
+ })
154
+
155
+ const values = samples.filter(s => s.measured).map(s => s.value as number)
156
+ const sorted = [...values].sort((a, b) => a - b)
157
+ const cur = current[metric]
158
+ const hasCurrent = typeof cur === 'number' && Number.isFinite(cur)
159
+
160
+ const row: MetricBaseline<M> = {
161
+ metric,
162
+ direction: spec.direction,
163
+ unit: spec.unit,
164
+ ...(hasCurrent ? { current: cur as number } : {}),
165
+ samples,
166
+ requested: inputs.length,
167
+ measuredCount: values.length,
168
+ ...(sorted.length ? { min: sorted[0], median: medianOf(sorted), max: sorted[sorted.length - 1] } : {})
169
+ }
170
+
171
+ /* 자리를 말할 수 없는 두 경우 — 이유를 밝힌다(조용한 빈 값이 "평범함" 으로 읽힌다). */
172
+ if (!hasCurrent) row.noPosition = 'no-current'
173
+ else if (sorted.length < 2) row.noPosition = 'too-few-samples'
174
+ else {
175
+ const value = cur as number
176
+ const median = medianOf(sorted) as number
177
+ /* 오름차순에서 몇 번째인가 — 같은 값은 뒤에 놓는다(동률이면 "더 큰 쪽" 으로 세지 않는다). */
178
+ row.rank = sorted.filter(v => v < value).length + 1
179
+ row.rankOf = sorted.length + 1
180
+
181
+ const higherIsBetter = spec.direction === 'higher'
182
+ const beatsAll = higherIsBetter ? value > sorted[sorted.length - 1] : value < sorted[0]
183
+ const losesToAll = higherIsBetter ? value < sorted[0] : value > sorted[sorted.length - 1]
184
+ /* 중앙값보다 큰 것이 "좋은" 쪽인가 — 방향을 여기 한 번만 적용한다. */
185
+ const aboveMedianIsBetter = higherIsBetter === value > median
186
+
187
+ row.standing = beatsAll
188
+ ? 'best'
189
+ : losesToAll
190
+ ? 'worst'
191
+ : value === median
192
+ ? 'typical'
193
+ : aboveMedianIsBetter
194
+ ? 'better-than-typical'
195
+ : 'worse-than-typical'
196
+ }
197
+
198
+ out.push(row)
199
+ }
200
+
201
+ return out
202
+ }