@things-factory/headless-twin 10.0.9 → 10.0.11

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 (354) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +2 -2
  2. package/dist-server/engine/canonical-ingest.js +24 -2
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/command-routing.js +1 -1
  5. package/dist-server/engine/command-routing.js.map +1 -1
  6. package/dist-server/engine/energy-topology.d.ts +86 -0
  7. package/dist-server/engine/energy-topology.js +144 -0
  8. package/dist-server/engine/energy-topology.js.map +1 -0
  9. package/dist-server/engine/index.d.ts +8 -3
  10. package/dist-server/engine/index.js +10 -3
  11. package/dist-server/engine/index.js.map +1 -1
  12. package/dist-server/engine/kpi-baseline.d.ts +1 -1
  13. package/dist-server/engine/kpi-baseline.js.map +1 -1
  14. package/dist-server/engine/kpi-fold.d.ts +137 -1
  15. package/dist-server/engine/kpi-fold.js +225 -0
  16. package/dist-server/engine/kpi-fold.js.map +1 -1
  17. package/dist-server/engine/kpi-query.d.ts +1 -1
  18. package/dist-server/engine/kpi-query.js +212 -18
  19. package/dist-server/engine/kpi-query.js.map +1 -1
  20. package/dist-server/engine/kpi-target.d.ts +17 -0
  21. package/dist-server/engine/kpi-target.js +24 -2
  22. package/dist-server/engine/kpi-target.js.map +1 -1
  23. package/dist-server/engine/live-feed-registry.d.ts +25 -0
  24. package/dist-server/engine/live-feed-registry.js +51 -0
  25. package/dist-server/engine/live-feed-registry.js.map +1 -0
  26. package/dist-server/engine/load-meter.d.ts +181 -0
  27. package/dist-server/engine/load-meter.js +267 -0
  28. package/dist-server/engine/load-meter.js.map +1 -0
  29. package/dist-server/engine/local-declarations.d.ts +262 -0
  30. package/dist-server/engine/local-declarations.js +528 -0
  31. package/dist-server/engine/local-declarations.js.map +1 -0
  32. package/dist-server/engine/model-basis.d.ts +39 -6
  33. package/dist-server/engine/model-basis.js +69 -9
  34. package/dist-server/engine/model-basis.js.map +1 -1
  35. package/dist-server/engine/{board-vocabulary.d.ts → model-vocabulary.d.ts} +4 -4
  36. package/dist-server/engine/{board-vocabulary.js → model-vocabulary.js} +37 -19
  37. package/dist-server/engine/model-vocabulary.js.map +1 -0
  38. package/dist-server/engine/oee-accumulator.d.ts +28 -0
  39. package/dist-server/engine/oee-accumulator.js +26 -1
  40. package/dist-server/engine/oee-accumulator.js.map +1 -1
  41. package/dist-server/engine/operation-basis.d.ts +32 -0
  42. package/dist-server/engine/operation-basis.js +87 -0
  43. package/dist-server/engine/operation-basis.js.map +1 -0
  44. package/dist-server/engine/property-effects.d.ts +30 -0
  45. package/dist-server/engine/property-effects.js +192 -0
  46. package/dist-server/engine/property-effects.js.map +1 -0
  47. package/dist-server/engine/runtime-key.d.ts +1 -1
  48. package/dist-server/engine/runtime-key.js +1 -1
  49. package/dist-server/engine/runtime-key.js.map +1 -1
  50. package/dist-server/engine/spec-coverage.js +3 -3
  51. package/dist-server/engine/spec-coverage.js.map +1 -1
  52. package/dist-server/engine/state-axes.d.ts +7 -2
  53. package/dist-server/engine/state-axes.js +8 -3
  54. package/dist-server/engine/state-axes.js.map +1 -1
  55. package/dist-server/engine/structure-diff.js +1 -1
  56. package/dist-server/engine/structure-diff.js.map +1 -1
  57. package/dist-server/engine/travel-estimator.d.ts +9 -2
  58. package/dist-server/engine/travel-estimator.js +11 -6
  59. package/dist-server/engine/travel-estimator.js.map +1 -1
  60. package/dist-server/engine/twin-engine.d.ts +332 -47
  61. package/dist-server/engine/twin-engine.js +1314 -279
  62. package/dist-server/engine/twin-engine.js.map +1 -1
  63. package/dist-server/engine/warm-start.d.ts +38 -4
  64. package/dist-server/engine/warm-start.js +38 -9
  65. package/dist-server/engine/warm-start.js.map +1 -1
  66. package/dist-server/index.d.ts +1 -0
  67. package/dist-server/index.js +17 -11
  68. package/dist-server/index.js.map +1 -1
  69. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.d.ts +5 -0
  70. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +48 -0
  71. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -0
  72. package/dist-server/migrations/index.d.ts +2 -0
  73. package/dist-server/migrations/index.js +10 -0
  74. package/dist-server/migrations/index.js.map +1 -0
  75. package/dist-server/service/index.d.ts +3 -2
  76. package/dist-server/service/index.js +25 -17
  77. package/dist-server/service/index.js.map +1 -1
  78. package/dist-server/service/reference/discovery-result.d.ts +1 -1
  79. package/dist-server/service/reference/discovery-result.js +2 -2
  80. package/dist-server/service/reference/discovery-result.js.map +1 -1
  81. package/dist-server/service/reference/ingest-space.d.ts +24 -1
  82. package/dist-server/service/reference/ingest-space.js +17 -1
  83. package/dist-server/service/reference/ingest-space.js.map +1 -1
  84. package/dist-server/service/reference/reference-live.d.ts +23 -0
  85. package/dist-server/service/reference/reference-live.js +121 -4
  86. package/dist-server/service/reference/reference-live.js.map +1 -1
  87. package/dist-server/service/reference/reference-master.d.ts +194 -13
  88. package/dist-server/service/reference/reference-master.js +159 -28
  89. package/dist-server/service/reference/reference-master.js.map +1 -1
  90. package/dist-server/service/reference/reference-resolver.d.ts +31 -1
  91. package/dist-server/service/reference/reference-resolver.js +223 -12
  92. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  93. package/dist-server/service/reference/template-registry.d.ts +19 -3
  94. package/dist-server/service/reference/template-registry.js.map +1 -1
  95. package/dist-server/service/twin-audit/command-audit.d.ts +37 -0
  96. package/dist-server/service/twin-audit/command-audit.js +53 -0
  97. package/dist-server/service/twin-audit/command-audit.js.map +1 -0
  98. package/dist-server/service/twin-audit/index.d.ts +4 -0
  99. package/dist-server/service/twin-audit/index.js +8 -0
  100. package/dist-server/service/twin-audit/index.js.map +1 -0
  101. package/dist-server/service/twin-audit/twin-audit-event.d.ts +24 -0
  102. package/dist-server/service/twin-audit/twin-audit-event.js +141 -0
  103. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -0
  104. package/dist-server/service/twin-audit/twin-audit-query.d.ts +4 -0
  105. package/dist-server/service/twin-audit/twin-audit-query.js +76 -0
  106. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -0
  107. package/dist-server/service/twin-control/twin-control-mutation.js +21 -1
  108. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  109. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  110. package/dist-server/service/twin-event/twin-event-keys.js +1 -1
  111. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  112. package/dist-server/service/twin-event/twin-event.d.ts +14 -5
  113. package/dist-server/service/twin-event/twin-event.js +45 -13
  114. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  115. package/dist-server/service/twin-forecast/forecast-metrics.d.ts +13 -0
  116. package/dist-server/service/twin-forecast/forecast-metrics.js +57 -0
  117. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -0
  118. package/dist-server/service/twin-forecast/forecast-tuning.d.ts +12 -0
  119. package/dist-server/service/twin-forecast/forecast-tuning.js +44 -0
  120. package/dist-server/service/twin-forecast/forecast-tuning.js.map +1 -0
  121. package/dist-server/service/twin-forecast/gap-analytics.d.ts +28 -2
  122. package/dist-server/service/twin-forecast/gap-analytics.js +43 -17
  123. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  124. package/dist-server/service/twin-forecast/twin-forecast-query.js +217 -38
  125. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  126. package/dist-server/service/twin-instance/twin-instance.d.ts +38 -5
  127. package/dist-server/service/twin-instance/twin-instance.js +66 -20
  128. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  129. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -2
  130. package/dist-server/service/twin-journal/twin-journal-query.js +71 -9
  131. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  132. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +3 -5
  133. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +24 -27
  134. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  135. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  136. package/dist-server/service/twin-metrics/twin-metrics-query.js +62 -0
  137. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  138. package/dist-server/service/twin-model/epcis-coverage.d.ts +14 -0
  139. package/dist-server/service/twin-model/epcis-coverage.js +174 -0
  140. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -0
  141. package/dist-server/service/twin-model/iec61850-coverage.d.ts +14 -0
  142. package/dist-server/service/twin-model/iec61850-coverage.js +98 -0
  143. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -0
  144. package/dist-server/service/twin-model/index.d.ts +13 -0
  145. package/dist-server/service/twin-model/index.js +29 -0
  146. package/dist-server/service/twin-model/index.js.map +1 -0
  147. package/dist-server/service/twin-model/isa95-coverage.d.ts +48 -0
  148. package/dist-server/service/twin-model/isa95-coverage.js +155 -0
  149. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -0
  150. package/dist-server/service/twin-model/project-structure.d.ts +16 -0
  151. package/dist-server/service/twin-model/project-structure.js +173 -0
  152. package/dist-server/service/twin-model/project-structure.js.map +1 -0
  153. package/dist-server/service/twin-model/standard-coverage.d.ts +9 -0
  154. package/dist-server/service/twin-model/standard-coverage.js +37 -0
  155. package/dist-server/service/twin-model/standard-coverage.js.map +1 -0
  156. package/dist-server/service/twin-model/twin-equipment.d.ts +64 -0
  157. package/dist-server/service/twin-model/twin-equipment.js +134 -0
  158. package/dist-server/service/twin-model/twin-equipment.js.map +1 -0
  159. package/dist-server/service/twin-model/twin-lineage-query.d.ts +3 -0
  160. package/dist-server/service/twin-model/twin-lineage-query.js +206 -0
  161. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -0
  162. package/dist-server/service/twin-model/twin-location.d.ts +58 -0
  163. package/dist-server/service/twin-model/twin-location.js +130 -0
  164. package/dist-server/service/twin-model/twin-location.js.map +1 -0
  165. package/dist-server/service/twin-model/twin-model-item-query.d.ts +141 -0
  166. package/dist-server/service/twin-model/twin-model-item-query.js +852 -0
  167. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -0
  168. package/dist-server/service/twin-model/twin-model-mutation.d.ts +57 -0
  169. package/dist-server/service/twin-model/twin-model-mutation.js +490 -0
  170. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -0
  171. package/dist-server/service/twin-model/twin-model-query.d.ts +43 -0
  172. package/dist-server/service/twin-model/twin-model-query.js +551 -0
  173. package/dist-server/service/twin-model/twin-model-query.js.map +1 -0
  174. package/dist-server/service/twin-model/twin-model-tree-query.d.ts +3 -0
  175. package/dist-server/service/twin-model/twin-model-tree-query.js +158 -0
  176. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -0
  177. package/dist-server/service/twin-model/twin-operation.d.ts +61 -0
  178. package/dist-server/service/twin-model/twin-operation.js +137 -0
  179. package/dist-server/service/twin-model/twin-operation.js.map +1 -0
  180. package/dist-server/service/twin-space/move-space.d.ts +65 -0
  181. package/dist-server/service/twin-space/move-space.js +166 -0
  182. package/dist-server/service/twin-space/move-space.js.map +1 -0
  183. package/dist-server/service/twin-space/space-integrity.d.ts +97 -0
  184. package/dist-server/service/twin-space/space-integrity.js +182 -0
  185. package/dist-server/service/twin-space/space-integrity.js.map +1 -0
  186. package/dist-server/service/twin-space/twin-space-area.js +4 -4
  187. package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
  188. package/dist-server/service/twin-space/twin-space-representation.js +6 -6
  189. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  190. package/dist-server/service/twin-space/twin-space-resolver.d.ts +63 -1
  191. package/dist-server/service/twin-space/twin-space-resolver.js +356 -18
  192. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  193. package/dist-server/service/twin-space/twin-space.d.ts +17 -1
  194. package/dist-server/service/twin-space/twin-space.js +15 -5
  195. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  196. package/dist-server/service/twin-structure/twin-structure.d.ts +1 -1
  197. package/dist-server/service/twin-structure/twin-structure.js +8 -8
  198. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  199. package/dist-shared/axis-read.d.ts +12 -0
  200. package/dist-shared/axis-read.js +32 -0
  201. package/dist-shared/axis-read.js.map +1 -0
  202. package/{dist-server/engine → dist-shared}/entity-delta.d.ts +3 -0
  203. package/{dist-server/engine → dist-shared}/entity-delta.js +102 -2
  204. package/dist-shared/entity-delta.js.map +1 -0
  205. package/dist-shared/kpi-broadcast.js.map +1 -0
  206. package/dist-shared/twin-level.js.map +1 -0
  207. package/package.json +14 -12
  208. package/server/engine/canonical-ingest.ts +36 -4
  209. package/server/engine/command-routing.ts +1 -1
  210. package/server/engine/energy-topology.ts +189 -0
  211. package/server/engine/index.ts +10 -3
  212. package/server/engine/kpi-baseline.ts +1 -1
  213. package/server/engine/kpi-fold.ts +389 -1
  214. package/server/engine/kpi-query.ts +214 -19
  215. package/server/engine/kpi-target.ts +24 -2
  216. package/server/engine/live-feed-registry.ts +58 -0
  217. package/server/engine/load-meter.ts +384 -0
  218. package/server/engine/local-declarations.ts +700 -0
  219. package/server/engine/model-basis.ts +78 -10
  220. package/server/engine/{board-vocabulary.ts → model-vocabulary.ts} +36 -15
  221. package/server/engine/oee-accumulator.ts +34 -1
  222. package/server/engine/operation-basis.ts +100 -0
  223. package/server/engine/property-effects.ts +199 -0
  224. package/server/engine/runtime-key.ts +1 -1
  225. package/server/engine/spec-coverage.ts +3 -3
  226. package/server/engine/state-axes.ts +8 -3
  227. package/server/engine/structure-diff.ts +1 -1
  228. package/server/engine/travel-estimator.ts +11 -6
  229. package/server/engine/twin-engine.ts +1414 -274
  230. package/server/engine/warm-start.ts +83 -12
  231. package/server/index.ts +13 -10
  232. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +43 -0
  233. package/server/migrations/index.ts +7 -0
  234. package/server/service/index.ts +9 -1
  235. package/server/service/reference/discovery-result.ts +2 -2
  236. package/server/service/reference/ingest-space.ts +35 -1
  237. package/server/service/reference/reference-live.ts +119 -4
  238. package/server/service/reference/reference-master.ts +276 -35
  239. package/server/service/reference/reference-resolver.ts +232 -12
  240. package/server/service/reference/template-registry.ts +20 -5
  241. package/server/service/twin-audit/command-audit.ts +81 -0
  242. package/server/service/twin-audit/index.ts +5 -0
  243. package/server/service/twin-audit/twin-audit-event.ts +138 -0
  244. package/server/service/twin-audit/twin-audit-query.ts +72 -0
  245. package/server/service/twin-control/twin-control-mutation.ts +22 -1
  246. package/server/service/twin-event/twin-event-keys.ts +1 -1
  247. package/server/service/twin-event/twin-event.ts +59 -14
  248. package/server/service/twin-forecast/forecast-metrics.ts +60 -0
  249. package/server/service/twin-forecast/forecast-tuning.ts +40 -0
  250. package/server/service/twin-forecast/gap-analytics.ts +48 -9
  251. package/server/service/twin-forecast/twin-forecast-query.ts +208 -36
  252. package/server/service/twin-instance/twin-instance.ts +126 -25
  253. package/server/service/twin-journal/twin-journal-query.ts +74 -10
  254. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +23 -22
  255. package/server/service/twin-metrics/twin-metrics-query.ts +59 -2
  256. package/server/service/twin-model/epcis-coverage.ts +195 -0
  257. package/server/service/twin-model/iec61850-coverage.ts +117 -0
  258. package/server/service/twin-model/index.ts +25 -0
  259. package/server/service/twin-model/isa95-coverage.ts +190 -0
  260. package/server/service/twin-model/project-structure.ts +199 -0
  261. package/server/service/twin-model/standard-coverage.ts +35 -0
  262. package/server/service/twin-model/twin-equipment.ts +157 -0
  263. package/server/service/twin-model/twin-lineage-query.ts +193 -0
  264. package/server/service/twin-model/twin-location.ts +147 -0
  265. package/server/service/twin-model/twin-model-item-query.ts +848 -0
  266. package/server/service/twin-model/twin-model-mutation.ts +503 -0
  267. package/server/service/twin-model/twin-model-query.ts +537 -0
  268. package/server/service/twin-model/twin-model-tree-query.ts +180 -0
  269. package/server/service/twin-model/twin-operation.ts +157 -0
  270. package/server/service/twin-space/move-space.ts +262 -0
  271. package/server/service/twin-space/space-integrity.ts +293 -0
  272. package/server/service/twin-space/twin-space-area.ts +4 -4
  273. package/server/service/twin-space/twin-space-representation.ts +6 -6
  274. package/server/service/twin-space/twin-space-resolver.ts +355 -21
  275. package/server/service/twin-space/twin-space.ts +40 -6
  276. package/server/service/twin-structure/twin-structure.ts +10 -10
  277. package/shared/axis-read.ts +28 -0
  278. package/{server/engine → shared}/entity-delta.ts +96 -1
  279. package/test/adopt-structure-live.test.ts +133 -0
  280. package/test/attention-digest.test.ts +3 -3
  281. package/test/axis-read.test.ts +87 -0
  282. package/test/boot-resume.test.ts +205 -0
  283. package/test/canonical-ingest-vocabularies.test.ts +118 -0
  284. package/test/capability-mapping.test.ts +1 -1
  285. package/test/column-type-portability.test.ts +122 -0
  286. package/test/command-routing.test.ts +1 -1
  287. package/test/declaration-reaches-model.test.ts +178 -0
  288. package/test/discovery-result.test.ts +1 -1
  289. package/test/energy-topology.test.ts +113 -0
  290. package/test/entity-delta.test.ts +171 -1
  291. package/test/event-time-column.test.ts +67 -0
  292. package/test/forecast-metrics.test.ts +71 -0
  293. package/test/forecast-tuning.test.ts +42 -0
  294. package/test/gap-analytics.test.ts +34 -7
  295. package/test/ingest-bench.test.ts +6 -6
  296. package/test/ingest-running-guard.test.ts +136 -0
  297. package/test/ingest-space.test.ts +29 -1
  298. package/test/instance-cache-lifecycle.test.ts +110 -0
  299. package/test/kernel-kind-guard.test.ts +96 -0
  300. package/test/kpi-baseline-db.test.ts +3 -1
  301. package/test/kpi-baseline.test.ts +3 -3
  302. package/test/kpi-fold.test.ts +393 -2
  303. package/test/kpi-query-bench.test.ts +5 -3
  304. package/test/lineage-survives-restart.test.ts +203 -0
  305. package/test/live-feed-registry.test.ts +67 -0
  306. package/test/live-kernel-facts.test.ts +161 -0
  307. package/test/live-mirror-parity.test.ts +22 -15
  308. package/test/load-meter.test.ts +314 -0
  309. package/test/local-declarations.test.ts +554 -0
  310. package/test/master-to-twin.test.ts +121 -33
  311. package/test/model-basis.test.ts +56 -1
  312. package/test/{board-vocabulary.test.ts → model-vocabulary.test.ts} +25 -25
  313. package/test/move-space.test.ts +149 -0
  314. package/test/mutation-gate.test.ts +19 -5
  315. package/test/oee-accumulator.test.ts +26 -7
  316. package/test/operation-basis.test.ts +100 -0
  317. package/test/operations-capability-db.test.ts +165 -0
  318. package/test/project-structure-db.test.ts +226 -0
  319. package/test/projection-reaches-screen.test.ts +272 -0
  320. package/test/property-effects.test.ts +96 -0
  321. package/test/resync-origin-site.test.ts +77 -0
  322. package/test/scale-twin-bench.test.ts +5 -5
  323. package/test/snapshot-freshness.test.ts +60 -0
  324. package/test/space-integrity.test.ts +223 -0
  325. package/test/standard-coverage.test.ts +94 -0
  326. package/test/state-axes.test.ts +1 -1
  327. package/test/streamline-e2e.test.ts +14 -14
  328. package/test/structure-revision-db.test.ts +11 -11
  329. package/test/tenant-registry-db.test.ts +5 -5
  330. package/test/twin-audit.test.ts +83 -0
  331. package/test/twin-model-item-db.test.ts +275 -0
  332. package/test/twin-model-tree-db.test.ts +176 -0
  333. package/test/twin-origin-resync.test.ts +114 -0
  334. package/test/warm-start-seam.test.ts +6 -6
  335. package/test/warm-start.test.ts +88 -3
  336. package/tsconfig.json +6 -1
  337. package/tsconfig.shared.json +23 -0
  338. package/tsconfig.shared.tsbuildinfo +1 -0
  339. package/tsconfig.tsbuildinfo +1 -0
  340. package/dist-server/engine/board-vocabulary.js.map +0 -1
  341. package/dist-server/engine/entity-delta.js.map +0 -1
  342. package/dist-server/engine/kpi-broadcast.js.map +0 -1
  343. package/dist-server/engine/twin-level.js.map +0 -1
  344. package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
  345. package/dist-server/service/twin-event/backfill-keys.js +0 -63
  346. package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
  347. package/dist-server/tsconfig.tsbuildinfo +0 -1
  348. package/server/service/twin-event/backfill-keys.ts +0 -72
  349. /package/{dist-server/engine → dist-shared}/kpi-broadcast.d.ts +0 -0
  350. /package/{dist-server/engine → dist-shared}/kpi-broadcast.js +0 -0
  351. /package/{dist-server/engine → dist-shared}/twin-level.d.ts +0 -0
  352. /package/{dist-server/engine → dist-shared}/twin-level.js +0 -0
  353. /package/{server/engine → shared}/kpi-broadcast.ts +0 -0
  354. /package/{server/engine → shared}/twin-level.ts +0 -0
@@ -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"]}
@@ -42,7 +42,7 @@ function routeCommand(inst, ownedByCaller, type) {
42
42
  if (!type)
43
43
  return { target: 'reject', errorCode: 'unknown-command' };
44
44
  if (inst.mode !== 'live') {
45
- /* 시뮬 — 커널이 세계다. 런타임이 없으면 터뜨리지 않고 이유를 말한다. */
45
+ /* 시뮬 — 커널이 세계다. 런타임이 없으면 예외를 던지지 않고 이유를 말한다. */
46
46
  return inst.hasRuntime ? { target: 'runtime' } : { target: 'reject', errorCode: 'no-runtime' };
47
47
  }
48
48
  /* 미러 — 실 프로세스 변경은 아웃바운드 어댑터의 몫이고 아직 결선되지 않았다. */
@@ -1 +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,6CAA6C;QAC7C,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"]}
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,14 +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';
4
5
  export * from './kpi-baseline.js';
5
6
  export * from './kpi-target.js';
6
- export * from './kpi-broadcast.js';
7
- export * from './entity-delta.js';
8
- export * from './twin-level.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';
9
11
  export * from './runtime-key.js';
10
12
  export * from './kpi-query.js';
11
13
  export * from './spec-coverage.js';
12
14
  export * from './travel-estimator.js';
13
15
  export * from './measured-estimator.js';
14
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';
@@ -4,14 +4,17 @@ const tslib_1 = require("tslib");
4
4
  tslib_1.__exportStar(require("./twin-engine.js"), exports);
5
5
  tslib_1.__exportStar(require("./canonical-ingest.js"), exports);
6
6
  tslib_1.__exportStar(require("./kpi-fold.js"), exports);
7
+ tslib_1.__exportStar(require("./property-effects.js"), exports);
7
8
  tslib_1.__exportStar(require("./kpi-baseline.js"), exports);
8
9
  /* 지표 정본 표(TWIN_METRIC)·목표 판정 — 소비처(twin-ai 등)가 지표 이름을 다시 열거하지 않게. */
9
10
  tslib_1.__exportStar(require("./kpi-target.js"), exports);
10
- tslib_1.__exportStar(require("./kpi-broadcast.js"), exports);
11
+ tslib_1.__exportStar(require("@things-factory/headless-twin/dist-shared/kpi-broadcast.js"), exports);
11
12
  /* 집약 채널 태그(접두사 + 트윈별 태그 생성) — 소비처가 문자열을 다시 적지 않게 내보낸다. */
12
- tslib_1.__exportStar(require("./entity-delta.js"), exports);
13
+ tslib_1.__exportStar(require("@things-factory/headless-twin/dist-shared/entity-delta.js"), exports);
13
14
  /* 공간 계층의 단계 — 씬 표현·성과 카드·AI 가 같은 목록을 쓴다. */
14
- tslib_1.__exportStar(require("./twin-level.js"), exports);
15
+ tslib_1.__exportStar(require("@things-factory/headless-twin/dist-shared/twin-level.js"), exports);
16
+ /* 부하 계기판 — 어떤 작업이 이벤트 루프를 얼마나 쓰는지. */
17
+ tslib_1.__exportStar(require("./load-meter.js"), exports);
15
18
  /* 런타임 레지스트리 키 규약 — 소비처(다른 키 캐시·테스트)가 `:` 를 다시 적지 않게 함께 내보낸다. */
16
19
  tslib_1.__exportStar(require("./runtime-key.js"), exports);
17
20
  tslib_1.__exportStar(require("./kpi-query.js"), exports);
@@ -19,4 +22,8 @@ tslib_1.__exportStar(require("./spec-coverage.js"), exports);
19
22
  tslib_1.__exportStar(require("./travel-estimator.js"), exports);
20
23
  tslib_1.__exportStar(require("./measured-estimator.js"), exports);
21
24
  tslib_1.__exportStar(require("./model-basis.js"), exports);
25
+ tslib_1.__exportStar(require("./live-feed-registry.js"), exports);
26
+ /* 로컬 선언(현장·트윈) — 소비처(twin-ai 도구)가 명세·검사 규칙을 다시 적지 않게. */
27
+ tslib_1.__exportStar(require("./local-declarations.js"), exports);
28
+ tslib_1.__exportStar(require("./operation-basis.js"), exports);
22
29
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/engine/index.ts"],"names":[],"mappings":";;;AAAA,2DAAgC;AAChC,gEAAqC;AACrC,wDAA6B;AAC7B,4DAAiC;AACjC,qEAAqE;AACrE,0DAA+B;AAC/B,6DAAkC;AAClC,0DAA0D;AAC1D,4DAAiC;AACjC,4CAA4C;AAC5C,0DAA+B;AAC/B,gEAAgE;AAChE,2DAAgC;AAChC,yDAA8B;AAC9B,6DAAkC;AAClC,gEAAqC;AACrC,kEAAuC;AACvC,2DAAgC","sourcesContent":["export * from './twin-engine.js'\nexport * from './canonical-ingest.js'\nexport * from './kpi-fold.js'\nexport * from './kpi-baseline.js'\n/* 지표 정본 표(TWIN_METRIC)·목표 판정 — 소비처(twin-ai 등)가 지표 이름을 다시 열거하지 않게. */\nexport * from './kpi-target.js'\nexport * from './kpi-broadcast.js'\n/* 집약 채널 태그(접두사 + 트윈별 태그 생성) — 소비처가 문자열을 다시 적지 않게 내보낸다. */\nexport * from './entity-delta.js'\n/* 공간 계층의 단계 — 씬 표현·성과 카드·AI 가 같은 목록을 쓴다. */\nexport * from './twin-level.js'\n/* 런타임 레지스트리 키 규약 — 소비처(다른 키 캐시·테스트)가 `:` 를 다시 적지 않게 함께 내보낸다. */\nexport * from './runtime-key.js'\nexport * from './kpi-query.js'\nexport * from './spec-coverage.js'\nexport * from './travel-estimator.js'\nexport * from './measured-estimator.js'\nexport * from './model-basis.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/engine/index.ts"],"names":[],"mappings":";;;AAAA,2DAAgC;AAChC,gEAAqC;AACrC,wDAA6B;AAC7B,gEAAqC;AACrC,4DAAiC;AACjC,qEAAqE;AACrE,0DAA+B;AAC/B,qGAA0E;AAC1E,0DAA0D;AAC1D,oGAAyE;AACzE,4CAA4C;AAC5C,kGAAuE;AACvE,sCAAsC;AACtC,0DAA+B;AAC/B,gEAAgE;AAChE,2DAAgC;AAChC,yDAA8B;AAC9B,6DAAkC;AAClC,gEAAqC;AACrC,kEAAuC;AACvC,2DAAgC;AAChC,kEAAuC;AACvC,yDAAyD;AACzD,kEAAuC;AACvC,+DAAoC","sourcesContent":["export * from './twin-engine.js'\nexport * from './canonical-ingest.js'\nexport * from './kpi-fold.js'\nexport * from './property-effects.js'\nexport * from './kpi-baseline.js'\n/* 지표 정본 표(TWIN_METRIC)·목표 판정 — 소비처(twin-ai 등)가 지표 이름을 다시 열거하지 않게. */\nexport * from './kpi-target.js'\nexport * from '@things-factory/headless-twin/dist-shared/kpi-broadcast.js'\n/* 집약 채널 태그(접두사 + 트윈별 태그 생성) — 소비처가 문자열을 다시 적지 않게 내보낸다. */\nexport * from '@things-factory/headless-twin/dist-shared/entity-delta.js'\n/* 공간 계층의 단계 — 씬 표현·성과 카드·AI 가 같은 목록을 쓴다. */\nexport * from '@things-factory/headless-twin/dist-shared/twin-level.js'\n/* 부하 계기판 — 어떤 작업이 이벤트 루프를 얼마나 쓰는지. */\nexport * from './load-meter.js'\n/* 런타임 레지스트리 키 규약 — 소비처(다른 키 캐시·테스트)가 `:` 를 다시 적지 않게 함께 내보낸다. */\nexport * from './runtime-key.js'\nexport * from './kpi-query.js'\nexport * from './spec-coverage.js'\nexport * from './travel-estimator.js'\nexport * from './measured-estimator.js'\nexport * from './model-basis.js'\nexport * from './live-feed-registry.js'\n/* 로컬 선언(현장·트윈) — 소비처(twin-ai 도구)가 명세·검사 규칙을 다시 적지 않게. */\nexport * from './local-declarations.js'\nexport * from './operation-basis.js'\n"]}
@@ -41,7 +41,7 @@ export type BaselineNoPosition = 'no-current' | 'too-few-samples';
41
41
  * 분포 안에서 현재의 자리 — **방향을 적용한 읽기.**
42
42
  *
43
43
  * 임의의 임계가 없다: 표본 전부보다 좋으면 `best`, 전부보다 나쁘면 `worst`, 그 사이는 중앙값을 기준으로
44
- * 가른다. `typical` 은 중앙값과 **같을 때**만이다.
44
+ * 구분한다. `typical` 은 중앙값과 **같을 때**만이다.
45
45
  */
46
46
  export type BaselineStanding = 'best' | 'better-than-typical' | 'typical' | 'worse-than-typical' | 'worst';
47
47
  export interface MetricBaseline<M extends string = string> {
@@ -1 +1 @@
1
- {"version":3,"file":"kpi-baseline.js","sourceRoot":"","sources":["../../server/engine/kpi-baseline.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;;;AAyBH,0CAcC;AAyED,8CA8DC;AAnKD,uDAAuD;AAC1C,QAAA,oBAAoB,GAAG,EAAE,CAAA;AAEtC,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,MAAM,CAAA;AAE/B;;;;;;;;GAQG;AACH,SAAgB,eAAe,CAC7B,MAAc,EACd,IAAY,EACZ,OAAe,EACf,SAAyB,KAAK;IAE9B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,MAAM;QAAE,OAAO,EAAE,CAAA;IACnF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,4BAAoB,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;IAC9E,MAAM,IAAI,GAAG,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;IAEpD,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9C,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;KAC5B,CAAC,CAAC,CAAA;AACL,CAAC;AA2DD,0DAA0D;AAC1D,SAAS,QAAQ,CAAC,MAAyB;IACzC,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IACpC,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,IAAI,CAAC,CAAA;IAC9B,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;AAC9E,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAC/B,KAA0C,EAC1C,OAA+C,EAC/C,MAAyC;IAEzC,MAAM,GAAG,GAAwB,EAAE,CAAA;IAEnC,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAQ,EAAE,CAAC;QAC/C,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAA;QAC1B,MAAM,OAAO,GAAqB,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;YAC/C,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YAC1B,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;YAC5D,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAW,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;QAChJ,CAAC,CAAC,CAAA;QAEF,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAe,CAAC,CAAA;QAC1E,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;QAChD,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;QAC3B,MAAM,UAAU,GAAG,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;QAElE,MAAM,GAAG,GAAsB;YAC7B,MAAM;YACN,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACjD,OAAO;YACP,SAAS,EAAE,MAAM,CAAC,MAAM;YACxB,aAAa,EAAE,MAAM,CAAC,MAAM;YAC5B,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvG,CAAA;QAED,wDAAwD;QACxD,IAAI,CAAC,UAAU;YAAE,GAAG,CAAC,UAAU,GAAG,YAAY,CAAA;aACzC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,GAAG,CAAC,UAAU,GAAG,iBAAiB,CAAA;aACzD,CAAC;YACJ,MAAM,KAAK,GAAG,GAAa,CAAA;YAC3B,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAW,CAAA;YACzC,2DAA2D;YAC3D,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,CAAA;YACnD,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;YAE9B,MAAM,cAAc,GAAG,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAA;YAClD,MAAM,QAAQ,GAAG,cAAc,CAAC,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;YACvF,MAAM,UAAU,GAAG,cAAc,CAAC,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;YACzF,6CAA6C;YAC7C,MAAM,mBAAmB,GAAG,cAAc,KAAK,KAAK,GAAG,MAAM,CAAA;YAE7D,GAAG,CAAC,QAAQ,GAAG,QAAQ;gBACrB,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,UAAU;oBACV,CAAC,CAAC,OAAO;oBACT,CAAC,CAAC,KAAK,KAAK,MAAM;wBAChB,CAAC,CAAC,SAAS;wBACX,CAAC,CAAC,mBAAmB;4BACnB,CAAC,CAAC,qBAAqB;4BACvB,CAAC,CAAC,oBAAoB,CAAA;QAChC,CAAC;QAED,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACf,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC","sourcesContent":["/*\n * 여러 구간 동시 비교 — **\"어제보다 나아졌다\" 를 넘어선다.** 순수.\n *\n * ── 왜 필요한가 (성과 맵 ③) ─────────────────────────────────────────────────\n * 지금 비교 축은 **한 구간**만 본다(직전 구간 또는 하루 전). 그래서 판단이 딱 한 문장에서 멈춘다:\n * \"어제보다 나아졌다.\" **어제도 나빴으면 그 말은 쓸모가 없다.** 목표(`kpi-target`)가 그 한계를 한쪽에서\n * 풀지만, 목표를 아직 걸지 않은 현장은 여전히 자기 숫자가 평소와 같은지도 알 수 없다.\n *\n * 최근 며칠의 **같은 시간대**를 함께 접으면 목표 없이도 말할 수 있다 — \"오늘 이 시간대는 지난 7일 중\n * 가장 낮다.\" 그것이 이 파일이 만드는 것이다: 창 여러 개의 **분포**와 그 안에서 현재의 **자리**.\n *\n * ── 이 파일이 하지 않는 것 ──────────────────────────────────────────────────\n * · **지표 어휘를 모른다.** 이름·방향·값 꺼내는 법은 `kpi-target.ts` 의 `TWIN_METRIC` 이 소유한다. 여기는\n * **이미 꺼낸 값**만 받는다 — 어휘가 두 곳에 생기면 그 사이에 매핑 표가 자라고 그것이 곧 방언이다.\n * (덤으로: 이 모듈은 런타임 import 가 없어 테스트가 소스를 그대로 든다.)\n * · **DB·조회를 모른다.** 창 목록만 계산하고, 그 창을 읽는 것은 호출부의 일이다.\n * · **좋고 나쁨의 임계를 발명하지 않는다.** \"평소보다 나쁘다\" 는 중앙값과의 비교로만 말한다 — 임의의\n * 퍼센트 임계를 두면 그 숫자가 곧 숨은 목표가 된다.\n *\n * ── 정직성 규율 (이 도메인의 핵심) ──────────────────────────────────────────\n * ① **결측 창을 조용히 빼지 않는다.** 요청한 창 전부를 `measured` 와 함께 돌려주고, 분포는 측정된\n * 것만으로 만든다. 빼고 말하면 \"지난 7일\" 이 실제로는 3일이었다는 것을 아무도 모른다.\n * ② **표본이 둘 미만이면 자리를 말하지 않는다.** 하나를 분포라 부를 수 없다.\n * ③ **현재 값이 없으면 자리를 말하지 않는다.** 결측을 0 으로 읽으면 시간 지표는 \"낮을수록 좋다\" 라서\n * **일이 없던 구간이 최고 성적**으로 보고된다(목표 판정에서 이미 겪은 결함이다).\n * ④ 왜 말하지 않는지를 `reason` 으로 밝힌다 — 조용히 빈 값을 주면 화면이 \"분포 없음\" 을 \"평범함\" 으로\n * 그린다.\n */\n\n/** 창 하나 — 폴드의 창과 같은 모양(ms). */\nexport interface BaselineWindow {\n fromMs: number\n toMs: number\n}\n\n/** 며칠 전 같은 시간대인가, 몇 주 전 같은 요일·시간대인가. */\nexport type BaselineStride = 'day' | 'week'\n\n/** 한 번에 볼 수 있는 과거 구간 수 — 창마다 조회가 한 번 더 붙으므로 상한을 둔다. */\nexport const MAX_BASELINE_PERIODS = 14\n\nconst MS_DAY = 24 * 60 * 60_000\n\n/**\n * 비교할 과거 창들 — 현재 창을 **같은 길이로** 하루(또는 한 주)씩 뒤로 옮긴다.\n *\n * 가까운 것부터 준다(어제 → 그제 → …). 화면이 시간 순서를 다시 정하지 않아도 되게.\n *\n * **한계를 밝힌다**: 옮기는 것은 ms 산술이다. 트윈 공간은 고정 오프셋(`utcOffsetMinutes`)을 선언하므로\n * 현재 모델에서는 \"같은 시간대\" 가 정확하다. 일광절약시간을 쓰는 시간대를 나중에 지원하면 전환이 있는\n * 주의 하루가 한 시간 밀린다 — 그때는 오프셋 기준 캘린더 산술로 바꿔야 한다(지금 지어내지 않는다).\n */\nexport function baselineWindows(\n fromMs: number,\n toMs: number,\n periods: number,\n stride: BaselineStride = 'day'\n): BaselineWindow[] {\n if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || toMs <= fromMs) return []\n const count = Math.max(0, Math.min(MAX_BASELINE_PERIODS, Math.floor(periods)))\n const step = stride === 'week' ? 7 * MS_DAY : MS_DAY\n\n return Array.from({ length: count }, (_, i) => ({\n fromMs: fromMs - step * (i + 1),\n toMs: toMs - step * (i + 1)\n }))\n}\n\n/** 지표의 성질 — 이름과 값 꺼내기는 호출부(`TWIN_METRIC`)가 소유한다. */\nexport interface MetricSpecLike {\n direction: 'higher' | 'lower'\n unit: string\n}\n\n/** 과거 창 하나에서 꺼낸 값들 — 그 창에 기록이 없으면 값이 `undefined` 다. */\nexport interface BaselineSampleInput<M extends string> {\n fromMs: number\n toMs: number\n values: Partial<Record<M, number | undefined>>\n}\n\n/** 과거 창 하나의 결과 — **결측도 남긴다**(빼면 \"지난 7일\" 이 거짓이 된다). */\nexport interface BaselineSample {\n fromMs: number\n toMs: number\n measured: boolean\n value?: number\n}\n\n/** 왜 자리를 말하지 않는지. */\nexport type BaselineNoPosition = 'no-current' | 'too-few-samples'\n\n/**\n * 분포 안에서 현재의 자리 — **방향을 적용한 읽기.**\n *\n * 임의의 임계가 없다: 표본 전부보다 좋으면 `best`, 전부보다 나쁘면 `worst`, 그 사이는 중앙값을 기준으로\n * 가른다. `typical` 은 중앙값과 **같을 때**만이다.\n */\nexport type BaselineStanding = 'best' | 'better-than-typical' | 'typical' | 'worse-than-typical' | 'worst'\n\nexport interface MetricBaseline<M extends string = string> {\n metric: M\n direction: 'higher' | 'lower'\n unit: string\n /** 지금 창의 값 — 없으면 `undefined`(0 이 아니다). */\n current?: number\n /** 요청한 과거 창 전부(결측 포함, 가까운 것부터). */\n samples: BaselineSample[]\n /** 요청한 창 수 / 그중 실제로 측정된 수 — 둘을 함께 봐야 \"지난 7일\" 을 믿을 수 있다. */\n requested: number\n measuredCount: number\n /** 측정된 표본만의 요약. 표본이 없으면 전부 `undefined`. */\n min?: number\n median?: number\n max?: number\n /** 측정 표본을 오름차순으로 놓았을 때 현재 값이 몇 번째인가(1 = 가장 작음) — **방향 없는 사실**. */\n rank?: number\n /** 그 순위를 셀 때의 표본 수(+현재 1). 순위만 보면 표본이 둘이었는지 열이었는지 알 수 없다. */\n rankOf?: number\n /** 방향을 적용한 읽기. 말할 수 없으면 없다. */\n standing?: BaselineStanding\n /** 자리를 말하지 않은 이유. */\n noPosition?: BaselineNoPosition\n}\n\n/** 중앙값 — 짝수 개면 가운데 둘의 평균(폴드의 분위수 규약과 달리 여기 표본은 아주 작다). */\nfunction medianOf(sorted: readonly number[]): number | undefined {\n if (!sorted.length) return undefined\n const mid = sorted.length >> 1\n return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2\n}\n\n/**\n * 지표별로 분포와 자리를 만든다.\n *\n * @param specs 지표 성질(방향·단위). 열거된 지표만 낸다 — 모르는 이름을 짐작하지 않는다.\n * @param current 지금 창에서 꺼낸 값들(결측은 `undefined`).\n * @param inputs 과거 창들에서 꺼낸 값들(가까운 것부터).\n */\nexport function summarizeBaseline<M extends string>(\n specs: Readonly<Record<M, MetricSpecLike>>,\n current: Partial<Record<M, number | undefined>>,\n inputs: readonly BaselineSampleInput<M>[]\n): MetricBaseline<M>[] {\n const out: MetricBaseline<M>[] = []\n\n for (const metric of Object.keys(specs) as M[]) {\n const spec = specs[metric]\n const samples: BaselineSample[] = inputs.map(s => {\n const v = s.values[metric]\n const measured = typeof v === 'number' && Number.isFinite(v)\n return measured ? { fromMs: s.fromMs, toMs: s.toMs, measured: true, value: v as number } : { fromMs: s.fromMs, toMs: s.toMs, measured: false }\n })\n\n const values = samples.filter(s => s.measured).map(s => s.value as number)\n const sorted = [...values].sort((a, b) => a - b)\n const cur = current[metric]\n const hasCurrent = typeof cur === 'number' && Number.isFinite(cur)\n\n const row: MetricBaseline<M> = {\n metric,\n direction: spec.direction,\n unit: spec.unit,\n ...(hasCurrent ? { current: cur as number } : {}),\n samples,\n requested: inputs.length,\n measuredCount: values.length,\n ...(sorted.length ? { min: sorted[0], median: medianOf(sorted), max: sorted[sorted.length - 1] } : {})\n }\n\n /* 자리를 말할 수 없는 두 경우 — 이유를 밝힌다(조용한 빈 값이 \"평범함\" 으로 읽힌다). */\n if (!hasCurrent) row.noPosition = 'no-current'\n else if (sorted.length < 2) row.noPosition = 'too-few-samples'\n else {\n const value = cur as number\n const median = medianOf(sorted) as number\n /* 오름차순에서 몇 번째인가 — 같은 값은 뒤에 놓는다(동률이면 \"더 큰 쪽\" 으로 세지 않는다). */\n row.rank = sorted.filter(v => v < value).length + 1\n row.rankOf = sorted.length + 1\n\n const higherIsBetter = spec.direction === 'higher'\n const beatsAll = higherIsBetter ? value > sorted[sorted.length - 1] : value < sorted[0]\n const losesToAll = higherIsBetter ? value < sorted[0] : value > sorted[sorted.length - 1]\n /* 중앙값보다 큰 것이 \"좋은\" 쪽인가 — 방향을 여기 한 번만 적용한다. */\n const aboveMedianIsBetter = higherIsBetter === value > median\n\n row.standing = beatsAll\n ? 'best'\n : losesToAll\n ? 'worst'\n : value === median\n ? 'typical'\n : aboveMedianIsBetter\n ? 'better-than-typical'\n : 'worse-than-typical'\n }\n\n out.push(row)\n }\n\n return out\n}\n"]}
1
+ {"version":3,"file":"kpi-baseline.js","sourceRoot":"","sources":["../../server/engine/kpi-baseline.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 여러 구간 동시 비교 — **\"어제보다 나아졌다\" 를 넘어선다.** 순수.\n *\n * ── 왜 필요한가 (성과 맵 ③) ─────────────────────────────────────────────────\n * 지금 비교 축은 **한 구간**만 본다(직전 구간 또는 하루 전). 그래서 판단이 딱 한 문장에서 멈춘다:\n * \"어제보다 나아졌다.\" **어제도 나빴으면 그 말은 쓸모가 없다.** 목표(`kpi-target`)가 그 한계를 한쪽에서\n * 풀지만, 목표를 아직 걸지 않은 현장은 여전히 자기 숫자가 평소와 같은지도 알 수 없다.\n *\n * 최근 며칠의 **같은 시간대**를 함께 접으면 목표 없이도 말할 수 있다 — \"오늘 이 시간대는 지난 7일 중\n * 가장 낮다.\" 그것이 이 파일이 만드는 것이다: 창 여러 개의 **분포**와 그 안에서 현재의 **자리**.\n *\n * ── 이 파일이 하지 않는 것 ──────────────────────────────────────────────────\n * · **지표 어휘를 모른다.** 이름·방향·값 꺼내는 법은 `kpi-target.ts` 의 `TWIN_METRIC` 이 소유한다. 여기는\n * **이미 꺼낸 값**만 받는다 — 어휘가 두 곳에 생기면 그 사이에 매핑 표가 자라고 그것이 곧 방언이다.\n * (덤으로: 이 모듈은 런타임 import 가 없어 테스트가 소스를 그대로 든다.)\n * · **DB·조회를 모른다.** 창 목록만 계산하고, 그 창을 읽는 것은 호출부의 일이다.\n * · **좋고 나쁨의 임계를 발명하지 않는다.** \"평소보다 나쁘다\" 는 중앙값과의 비교로만 말한다 — 임의의\n * 퍼센트 임계를 두면 그 숫자가 곧 숨은 목표가 된다.\n *\n * ── 정직성 규율 (이 도메인의 핵심) ──────────────────────────────────────────\n * ① **결측 창을 조용히 빼지 않는다.** 요청한 창 전부를 `measured` 와 함께 돌려주고, 분포는 측정된\n * 것만으로 만든다. 빼고 말하면 \"지난 7일\" 이 실제로는 3일이었다는 것을 아무도 모른다.\n * ② **표본이 둘 미만이면 자리를 말하지 않는다.** 하나를 분포라 부를 수 없다.\n * ③ **현재 값이 없으면 자리를 말하지 않는다.** 결측을 0 으로 읽으면 시간 지표는 \"낮을수록 좋다\" 라서\n * **일이 없던 구간이 최고 성적**으로 보고된다(목표 판정에서 이미 겪은 결함이다).\n * ④ 왜 말하지 않는지를 `reason` 으로 밝힌다 — 조용히 빈 값을 주면 화면이 \"분포 없음\" 을 \"평범함\" 으로\n * 그린다.\n */\n\n/** 창 하나 — 폴드의 창과 같은 모양(ms). */\nexport interface BaselineWindow {\n fromMs: number\n toMs: number\n}\n\n/** 며칠 전 같은 시간대인가, 몇 주 전 같은 요일·시간대인가. */\nexport type BaselineStride = 'day' | 'week'\n\n/** 한 번에 볼 수 있는 과거 구간 수 — 창마다 조회가 한 번 더 붙으므로 상한을 둔다. */\nexport const MAX_BASELINE_PERIODS = 14\n\nconst MS_DAY = 24 * 60 * 60_000\n\n/**\n * 비교할 과거 창들 — 현재 창을 **같은 길이로** 하루(또는 한 주)씩 뒤로 옮긴다.\n *\n * 가까운 것부터 준다(어제 → 그제 → …). 화면이 시간 순서를 다시 정하지 않아도 되게.\n *\n * **한계를 밝힌다**: 옮기는 것은 ms 산술이다. 트윈 공간은 고정 오프셋(`utcOffsetMinutes`)을 선언하므로\n * 현재 모델에서는 \"같은 시간대\" 가 정확하다. 일광절약시간을 쓰는 시간대를 나중에 지원하면 전환이 있는\n * 주의 하루가 한 시간 밀린다 — 그때는 오프셋 기준 캘린더 산술로 바꿔야 한다(지금 지어내지 않는다).\n */\nexport function baselineWindows(\n fromMs: number,\n toMs: number,\n periods: number,\n stride: BaselineStride = 'day'\n): BaselineWindow[] {\n if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || toMs <= fromMs) return []\n const count = Math.max(0, Math.min(MAX_BASELINE_PERIODS, Math.floor(periods)))\n const step = stride === 'week' ? 7 * MS_DAY : MS_DAY\n\n return Array.from({ length: count }, (_, i) => ({\n fromMs: fromMs - step * (i + 1),\n toMs: toMs - step * (i + 1)\n }))\n}\n\n/** 지표의 성질 — 이름과 값 꺼내기는 호출부(`TWIN_METRIC`)가 소유한다. */\nexport interface MetricSpecLike {\n direction: 'higher' | 'lower'\n unit: string\n}\n\n/** 과거 창 하나에서 꺼낸 값들 — 그 창에 기록이 없으면 값이 `undefined` 다. */\nexport interface BaselineSampleInput<M extends string> {\n fromMs: number\n toMs: number\n values: Partial<Record<M, number | undefined>>\n}\n\n/** 과거 창 하나의 결과 — **결측도 남긴다**(빼면 \"지난 7일\" 이 거짓이 된다). */\nexport interface BaselineSample {\n fromMs: number\n toMs: number\n measured: boolean\n value?: number\n}\n\n/** 왜 자리를 말하지 않는지. */\nexport type BaselineNoPosition = 'no-current' | 'too-few-samples'\n\n/**\n * 분포 안에서 현재의 자리 — **방향을 적용한 읽기.**\n *\n * 임의의 임계가 없다: 표본 전부보다 좋으면 `best`, 전부보다 나쁘면 `worst`, 그 사이는 중앙값을 기준으로\n * 구분한다. `typical` 은 중앙값과 **같을 때**만이다.\n */\nexport type BaselineStanding = 'best' | 'better-than-typical' | 'typical' | 'worse-than-typical' | 'worst'\n\nexport interface MetricBaseline<M extends string = string> {\n metric: M\n direction: 'higher' | 'lower'\n unit: string\n /** 지금 창의 값 — 없으면 `undefined`(0 이 아니다). */\n current?: number\n /** 요청한 과거 창 전부(결측 포함, 가까운 것부터). */\n samples: BaselineSample[]\n /** 요청한 창 수 / 그중 실제로 측정된 수 — 둘을 함께 봐야 \"지난 7일\" 을 믿을 수 있다. */\n requested: number\n measuredCount: number\n /** 측정된 표본만의 요약. 표본이 없으면 전부 `undefined`. */\n min?: number\n median?: number\n max?: number\n /** 측정 표본을 오름차순으로 놓았을 때 현재 값이 몇 번째인가(1 = 가장 작음) — **방향 없는 사실**. */\n rank?: number\n /** 그 순위를 셀 때의 표본 수(+현재 1). 순위만 보면 표본이 둘이었는지 열이었는지 알 수 없다. */\n rankOf?: number\n /** 방향을 적용한 읽기. 말할 수 없으면 없다. */\n standing?: BaselineStanding\n /** 자리를 말하지 않은 이유. */\n noPosition?: BaselineNoPosition\n}\n\n/** 중앙값 — 짝수 개면 가운데 둘의 평균(폴드의 분위수 규약과 달리 여기 표본은 아주 작다). */\nfunction medianOf(sorted: readonly number[]): number | undefined {\n if (!sorted.length) return undefined\n const mid = sorted.length >> 1\n return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2\n}\n\n/**\n * 지표별로 분포와 자리를 만든다.\n *\n * @param specs 지표 성질(방향·단위). 열거된 지표만 낸다 — 모르는 이름을 짐작하지 않는다.\n * @param current 지금 창에서 꺼낸 값들(결측은 `undefined`).\n * @param inputs 과거 창들에서 꺼낸 값들(가까운 것부터).\n */\nexport function summarizeBaseline<M extends string>(\n specs: Readonly<Record<M, MetricSpecLike>>,\n current: Partial<Record<M, number | undefined>>,\n inputs: readonly BaselineSampleInput<M>[]\n): MetricBaseline<M>[] {\n const out: MetricBaseline<M>[] = []\n\n for (const metric of Object.keys(specs) as M[]) {\n const spec = specs[metric]\n const samples: BaselineSample[] = inputs.map(s => {\n const v = s.values[metric]\n const measured = typeof v === 'number' && Number.isFinite(v)\n return measured ? { fromMs: s.fromMs, toMs: s.toMs, measured: true, value: v as number } : { fromMs: s.fromMs, toMs: s.toMs, measured: false }\n })\n\n const values = samples.filter(s => s.measured).map(s => s.value as number)\n const sorted = [...values].sort((a, b) => a - b)\n const cur = current[metric]\n const hasCurrent = typeof cur === 'number' && Number.isFinite(cur)\n\n const row: MetricBaseline<M> = {\n metric,\n direction: spec.direction,\n unit: spec.unit,\n ...(hasCurrent ? { current: cur as number } : {}),\n samples,\n requested: inputs.length,\n measuredCount: values.length,\n ...(sorted.length ? { min: sorted[0], median: medianOf(sorted), max: sorted[sorted.length - 1] } : {})\n }\n\n /* 자리를 말할 수 없는 두 경우 — 이유를 밝힌다(조용한 빈 값이 \"평범함\" 으로 읽힌다). */\n if (!hasCurrent) row.noPosition = 'no-current'\n else if (sorted.length < 2) row.noPosition = 'too-few-samples'\n else {\n const value = cur as number\n const median = medianOf(sorted) as number\n /* 오름차순에서 몇 번째인가 — 같은 값은 뒤에 놓는다(동률이면 \"더 큰 쪽\" 으로 세지 않는다). */\n row.rank = sorted.filter(v => v < value).length + 1\n row.rankOf = sorted.length + 1\n\n const higherIsBetter = spec.direction === 'higher'\n const beatsAll = higherIsBetter ? value > sorted[sorted.length - 1] : value < sorted[0]\n const losesToAll = higherIsBetter ? value < sorted[0] : value > sorted[sorted.length - 1]\n /* 중앙값보다 큰 것이 \"좋은\" 쪽인가 — 방향을 여기 한 번만 적용한다. */\n const aboveMedianIsBetter = higherIsBetter === value > median\n\n row.standing = beatsAll\n ? 'best'\n : losesToAll\n ? 'worst'\n : value === median\n ? 'typical'\n : aboveMedianIsBetter\n ? 'better-than-typical'\n : 'worse-than-typical'\n }\n\n out.push(row)\n }\n\n return out\n}\n"]}