@things-factory/headless-twin 10.0.15 → 10.0.18

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 (513) hide show
  1. package/dist-server/engine/attention-digest.d.ts +4 -4
  2. package/dist-server/engine/attention-digest.js +7 -7
  3. package/dist-server/engine/attention-digest.js.map +1 -1
  4. package/dist-server/engine/canonical-ingest.d.ts +18 -82
  5. package/dist-server/engine/canonical-ingest.js +183 -18
  6. package/dist-server/engine/canonical-ingest.js.map +1 -1
  7. package/dist-server/engine/command-routing.js +1 -1
  8. package/dist-server/engine/command-routing.js.map +1 -1
  9. package/dist-server/engine/declared-stimulus.js +1 -1
  10. package/dist-server/engine/declared-stimulus.js.map +1 -1
  11. package/dist-server/engine/energy-topology.js +4 -4
  12. package/dist-server/engine/energy-topology.js.map +1 -1
  13. package/dist-server/engine/ingest-dedupe.d.ts +38 -0
  14. package/dist-server/engine/ingest-dedupe.js +145 -0
  15. package/dist-server/engine/ingest-dedupe.js.map +1 -0
  16. package/dist-server/engine/ingest-health.d.ts +90 -10
  17. package/dist-server/engine/ingest-health.js +125 -16
  18. package/dist-server/engine/ingest-health.js.map +1 -1
  19. package/dist-server/engine/integration-probes.d.ts +3 -3
  20. package/dist-server/engine/integration-probes.js +6 -6
  21. package/dist-server/engine/integration-probes.js.map +1 -1
  22. package/dist-server/engine/kpi-baseline.js +4 -4
  23. package/dist-server/engine/kpi-baseline.js.map +1 -1
  24. package/dist-server/engine/kpi-fold.d.ts +212 -14
  25. package/dist-server/engine/kpi-fold.js +374 -21
  26. package/dist-server/engine/kpi-fold.js.map +1 -1
  27. package/dist-server/engine/kpi-query.d.ts +11 -11
  28. package/dist-server/engine/kpi-query.js +281 -36
  29. package/dist-server/engine/kpi-query.js.map +1 -1
  30. package/dist-server/engine/live-feed-registry.d.ts +1 -1
  31. package/dist-server/engine/live-feed-registry.js +1 -1
  32. package/dist-server/engine/live-feed-registry.js.map +1 -1
  33. package/dist-server/engine/load-meter.d.ts +1 -1
  34. package/dist-server/engine/load-meter.js.map +1 -1
  35. package/dist-server/engine/local-declarations.d.ts +7 -7
  36. package/dist-server/engine/local-declarations.js +15 -14
  37. package/dist-server/engine/local-declarations.js.map +1 -1
  38. package/dist-server/engine/log.d.ts +1 -1
  39. package/dist-server/engine/log.js +1 -1
  40. package/dist-server/engine/log.js.map +1 -1
  41. package/dist-server/engine/measured-estimator.js +1 -1
  42. package/dist-server/engine/measured-estimator.js.map +1 -1
  43. package/dist-server/engine/measured-yield.js +1 -1
  44. package/dist-server/engine/measured-yield.js.map +1 -1
  45. package/dist-server/engine/model-basis.d.ts +1 -1
  46. package/dist-server/engine/model-basis.js +1 -1
  47. package/dist-server/engine/model-basis.js.map +1 -1
  48. package/dist-server/engine/model-gap.d.ts +3 -3
  49. package/dist-server/engine/model-gap.js +1 -1
  50. package/dist-server/engine/model-gap.js.map +1 -1
  51. package/dist-server/engine/model-vocabulary.js +3 -3
  52. package/dist-server/engine/model-vocabulary.js.map +1 -1
  53. package/dist-server/engine/oee-accumulator.d.ts +6 -5
  54. package/dist-server/engine/oee-accumulator.js +6 -5
  55. package/dist-server/engine/oee-accumulator.js.map +1 -1
  56. package/dist-server/engine/property-effects.js +2 -2
  57. package/dist-server/engine/property-effects.js.map +1 -1
  58. package/dist-server/engine/read-time.d.ts +22 -0
  59. package/dist-server/engine/read-time.js +81 -0
  60. package/dist-server/engine/read-time.js.map +1 -0
  61. package/dist-server/engine/restart-policy.js +1 -1
  62. package/dist-server/engine/restart-policy.js.map +1 -1
  63. package/dist-server/engine/runtime-key.js +1 -1
  64. package/dist-server/engine/runtime-key.js.map +1 -1
  65. package/dist-server/engine/spec-coverage.d.ts +2 -2
  66. package/dist-server/engine/spec-coverage.js +1 -1
  67. package/dist-server/engine/spec-coverage.js.map +1 -1
  68. package/dist-server/engine/stage-path.d.ts +4 -4
  69. package/dist-server/engine/stage-path.js +1 -1
  70. package/dist-server/engine/stage-path.js.map +1 -1
  71. package/dist-server/engine/state-axes.d.ts +1 -1
  72. package/dist-server/engine/state-axes.js +2 -2
  73. package/dist-server/engine/state-axes.js.map +1 -1
  74. package/dist-server/engine/travel-estimator.d.ts +2 -2
  75. package/dist-server/engine/travel-estimator.js +1 -1
  76. package/dist-server/engine/travel-estimator.js.map +1 -1
  77. package/dist-server/engine/twin-engine.d.ts +307 -54
  78. package/dist-server/engine/twin-engine.js +822 -196
  79. package/dist-server/engine/twin-engine.js.map +1 -1
  80. package/dist-server/engine/warm-start.d.ts +2 -2
  81. package/dist-server/engine/warm-start.js +5 -5
  82. package/dist-server/engine/warm-start.js.map +1 -1
  83. package/dist-server/index.js +1 -1
  84. package/dist-server/index.js.map +1 -1
  85. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +1 -1
  86. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -1
  87. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +3 -3
  88. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -1
  89. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +2 -2
  90. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -1
  91. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.d.ts +5 -0
  92. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js +54 -0
  93. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js.map +1 -0
  94. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.d.ts +5 -0
  95. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js +44 -0
  96. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js.map +1 -0
  97. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.d.ts +6 -0
  98. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js +84 -0
  99. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js.map +1 -0
  100. package/dist-server/migrations/index.js +7 -1
  101. package/dist-server/migrations/index.js.map +1 -1
  102. package/dist-server/routes.d.ts +1 -0
  103. package/dist-server/routes.js +69 -5
  104. package/dist-server/routes.js.map +1 -1
  105. package/dist-server/service/index.d.ts +2 -2
  106. package/dist-server/service/index.js +30 -24
  107. package/dist-server/service/index.js.map +1 -1
  108. package/dist-server/service/reference/control-routing.d.ts +1 -1
  109. package/dist-server/service/reference/control-routing.js +1 -1
  110. package/dist-server/service/reference/control-routing.js.map +1 -1
  111. package/dist-server/service/reference/discovery-result.d.ts +2 -2
  112. package/dist-server/service/reference/discovery-result.js +3 -3
  113. package/dist-server/service/reference/discovery-result.js.map +1 -1
  114. package/dist-server/service/reference/hook-contract.d.ts +43 -0
  115. package/dist-server/service/reference/hook-contract.js +59 -0
  116. package/dist-server/service/reference/hook-contract.js.map +1 -0
  117. package/dist-server/service/reference/ingest-space.d.ts +1 -1
  118. package/dist-server/service/reference/ingest-space.js +4 -4
  119. package/dist-server/service/reference/ingest-space.js.map +1 -1
  120. package/dist-server/service/reference/knob-defaults.d.ts +1 -1
  121. package/dist-server/service/reference/knob-defaults.js +4 -4
  122. package/dist-server/service/reference/knob-defaults.js.map +1 -1
  123. package/dist-server/service/reference/reference-adapter.d.ts +116 -9
  124. package/dist-server/service/reference/reference-adapter.js +27 -0
  125. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  126. package/dist-server/service/reference/reference-hook.d.ts +20 -0
  127. package/dist-server/service/reference/reference-hook.js +133 -0
  128. package/dist-server/service/reference/reference-hook.js.map +1 -0
  129. package/dist-server/service/reference/reference-live.d.ts +2 -2
  130. package/dist-server/service/reference/reference-live.js +76 -13
  131. package/dist-server/service/reference/reference-live.js.map +1 -1
  132. package/dist-server/service/reference/reference-master.d.ts +25 -16
  133. package/dist-server/service/reference/reference-master.js +35 -13
  134. package/dist-server/service/reference/reference-master.js.map +1 -1
  135. package/dist-server/service/reference/reference-resolver.d.ts +1 -1
  136. package/dist-server/service/reference/reference-resolver.js +12 -12
  137. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  138. package/dist-server/service/reference/template-registry.d.ts +1 -1
  139. package/dist-server/service/reference/template-registry.js.map +1 -1
  140. package/dist-server/service/reference/twin-reference.d.ts +3 -3
  141. package/dist-server/service/reference/twin-reference.js.map +1 -1
  142. package/dist-server/service/twin-attention/twin-attention-query.d.ts +1 -1
  143. package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
  144. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  145. package/dist-server/service/twin-audit/command-audit.d.ts +1 -1
  146. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  147. package/dist-server/service/twin-audit/twin-audit-event.d.ts +1 -1
  148. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  149. package/dist-server/service/twin-audit/twin-audit-query.js +1 -1
  150. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -1
  151. package/dist-server/service/twin-backfill/backfill-period-facts.d.ts +24 -0
  152. package/dist-server/service/twin-backfill/backfill-period-facts.js +96 -0
  153. package/dist-server/service/twin-backfill/backfill-period-facts.js.map +1 -0
  154. package/dist-server/service/twin-backfill/backfill-shape.d.ts +34 -0
  155. package/dist-server/service/twin-backfill/backfill-shape.js +75 -0
  156. package/dist-server/service/twin-backfill/backfill-shape.js.map +1 -0
  157. package/dist-server/service/twin-backfill/index.d.ts +2 -0
  158. package/dist-server/service/twin-backfill/index.js +6 -0
  159. package/dist-server/service/twin-backfill/index.js.map +1 -0
  160. package/dist-server/service/twin-backfill/twin-backfill-resolver.d.ts +10 -0
  161. package/dist-server/service/twin-backfill/twin-backfill-resolver.js +44 -0
  162. package/dist-server/service/twin-backfill/twin-backfill-resolver.js.map +1 -0
  163. package/dist-server/service/twin-control/twin-control-mutation.js +3 -3
  164. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  165. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  166. package/dist-server/service/twin-event/twin-event-keys.js +2 -2
  167. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  168. package/dist-server/service/twin-event/twin-event-type.d.ts +1 -0
  169. package/dist-server/service/twin-event/twin-event-type.js +8 -1
  170. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  171. package/dist-server/service/twin-event/twin-event.d.ts +2 -2
  172. package/dist-server/service/twin-event/twin-event.js +32 -4
  173. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  174. package/dist-server/service/twin-forecast/forecast-metrics.js +1 -1
  175. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -1
  176. package/dist-server/service/twin-forecast/gap-analytics.d.ts +1 -1
  177. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  178. package/dist-server/service/twin-forecast/twin-forecast-query.js +29 -11
  179. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  180. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +1 -1
  181. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +1 -1
  182. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -1
  183. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +1 -1
  184. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -1
  185. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  186. package/dist-server/service/twin-journal/event-type-split-shape.d.ts +50 -0
  187. package/dist-server/service/twin-journal/event-type-split-shape.js +111 -0
  188. package/dist-server/service/twin-journal/event-type-split-shape.js.map +1 -0
  189. package/dist-server/service/twin-journal/event-type-split.d.ts +15 -0
  190. package/dist-server/service/twin-journal/event-type-split.js +102 -0
  191. package/dist-server/service/twin-journal/event-type-split.js.map +1 -0
  192. package/dist-server/service/twin-journal/journal-order.d.ts +15 -0
  193. package/dist-server/service/twin-journal/journal-order.js +17 -0
  194. package/dist-server/service/twin-journal/journal-order.js.map +1 -0
  195. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -5
  196. package/dist-server/service/twin-journal/twin-journal-query.js +232 -21
  197. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  198. package/dist-server/service/twin-lifecycle/domain-catalog.js +2 -1
  199. package/dist-server/service/twin-lifecycle/domain-catalog.js.map +1 -1
  200. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +27 -1
  201. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +59 -3
  202. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  203. package/dist-server/service/twin-metrics/twin-metrics-query.js +14 -1
  204. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  205. package/dist-server/service/twin-model/axis-journal-evidence.js +1 -1
  206. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -1
  207. package/dist-server/service/twin-model/epcis-coverage.js +34 -3
  208. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  209. package/dist-server/service/twin-model/iec61850-coverage.js +1 -1
  210. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  211. package/dist-server/service/twin-model/isa95-coverage.d.ts +12 -9
  212. package/dist-server/service/twin-model/isa95-coverage.js +33 -9
  213. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  214. package/dist-server/service/twin-model/item-ref.js +2 -2
  215. package/dist-server/service/twin-model/item-ref.js.map +1 -1
  216. package/dist-server/service/twin-model/name-index.d.ts +1 -1
  217. package/dist-server/service/twin-model/name-index.js +2 -2
  218. package/dist-server/service/twin-model/name-index.js.map +1 -1
  219. package/dist-server/service/twin-model/project-structure.d.ts +1 -1
  220. package/dist-server/service/twin-model/project-structure.js +7 -6
  221. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  222. package/dist-server/service/twin-model/twin-equipment.d.ts +2 -2
  223. package/dist-server/service/twin-model/twin-equipment.js +3 -3
  224. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  225. package/dist-server/service/twin-model/twin-lineage-query.js +16 -5
  226. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  227. package/dist-server/service/twin-model/twin-location.d.ts +1 -1
  228. package/dist-server/service/twin-model/twin-location.js.map +1 -1
  229. package/dist-server/service/twin-model/twin-model-item-query.d.ts +3 -3
  230. package/dist-server/service/twin-model/twin-model-item-query.js +15 -15
  231. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  232. package/dist-server/service/twin-model/twin-model-mutation.d.ts +2 -2
  233. package/dist-server/service/twin-model/twin-model-mutation.js +4 -4
  234. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -1
  235. package/dist-server/service/twin-model/twin-model-query.js +51 -30
  236. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  237. package/dist-server/service/twin-model/twin-model-tree-query.js +23 -1
  238. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  239. package/dist-server/service/twin-model/twin-operation.d.ts +1 -1
  240. package/dist-server/service/twin-model/twin-operation.js +1 -1
  241. package/dist-server/service/twin-model/twin-operation.js.map +1 -1
  242. package/dist-server/service/twin-space/move-space.js +2 -2
  243. package/dist-server/service/twin-space/move-space.js.map +1 -1
  244. package/dist-server/service/twin-space/representation-role.d.ts +2 -0
  245. package/dist-server/service/twin-space/representation-role.js +18 -0
  246. package/dist-server/service/twin-space/representation-role.js.map +1 -0
  247. package/dist-server/service/twin-space/space-integrity.d.ts +1 -1
  248. package/dist-server/service/twin-space/space-integrity.js.map +1 -1
  249. package/dist-server/service/twin-space/twin-space-representation.d.ts +2 -0
  250. package/dist-server/service/twin-space/twin-space-representation.js +16 -0
  251. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  252. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  253. package/dist-server/service/twin-space/twin-space-resolver.js +37 -8
  254. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  255. package/dist-server/service/twin-structure/twin-structure.js +3 -3
  256. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  257. package/dist-server/service/twin-subject/event-subjects.d.ts +43 -0
  258. package/dist-server/service/twin-subject/event-subjects.js +137 -0
  259. package/dist-server/service/twin-subject/event-subjects.js.map +1 -0
  260. package/dist-server/service/twin-subject/index.d.ts +9 -0
  261. package/dist-server/service/twin-subject/index.js +16 -0
  262. package/dist-server/service/twin-subject/index.js.map +1 -0
  263. package/dist-server/service/twin-subject/item-resolver.d.ts +10 -0
  264. package/dist-server/service/twin-subject/item-resolver.js +93 -0
  265. package/dist-server/service/twin-subject/item-resolver.js.map +1 -0
  266. package/dist-server/service/twin-subject/subject-query-shape.d.ts +22 -0
  267. package/dist-server/service/twin-subject/subject-query-shape.js +88 -0
  268. package/dist-server/service/twin-subject/subject-query-shape.js.map +1 -0
  269. package/dist-server/service/twin-subject/twin-subject-backfill.d.ts +23 -0
  270. package/dist-server/service/twin-subject/twin-subject-backfill.js +159 -0
  271. package/dist-server/service/twin-subject/twin-subject-backfill.js.map +1 -0
  272. package/dist-server/service/twin-subject/twin-subject-event.d.ts +14 -0
  273. package/dist-server/service/twin-subject/twin-subject-event.js +111 -0
  274. package/dist-server/service/twin-subject/twin-subject-event.js.map +1 -0
  275. package/dist-server/service/twin-subject/twin-subject-fast-path.d.ts +39 -0
  276. package/dist-server/service/twin-subject/twin-subject-fast-path.js +295 -0
  277. package/dist-server/service/twin-subject/twin-subject-fast-path.js.map +1 -0
  278. package/dist-server/service/twin-subject/twin-subject-query.d.ts +27 -0
  279. package/dist-server/service/twin-subject/twin-subject-query.js +232 -0
  280. package/dist-server/service/twin-subject/twin-subject-query.js.map +1 -0
  281. package/dist-server/service/twin-subject/twin-subject-writer.d.ts +27 -0
  282. package/dist-server/service/twin-subject/twin-subject-writer.js +166 -0
  283. package/dist-server/service/twin-subject/twin-subject-writer.js.map +1 -0
  284. package/dist-server/service/twin-target/twin-target-resolver.js +1 -1
  285. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  286. package/dist-shared/entity-delta.d.ts +1 -1
  287. package/dist-shared/entity-delta.js +4 -4
  288. package/dist-shared/entity-delta.js.map +1 -1
  289. package/dist-shared/touched-items.js +2 -2
  290. package/dist-shared/touched-items.js.map +1 -1
  291. package/dist-shared/twin-level.js +1 -1
  292. package/dist-shared/twin-level.js.map +1 -1
  293. package/package.json +7 -6
  294. package/server/engine/attention-digest.ts +9 -9
  295. package/server/engine/canonical-ingest.ts +202 -87
  296. package/server/engine/command-routing.ts +1 -1
  297. package/server/engine/declared-stimulus.ts +1 -1
  298. package/server/engine/energy-topology.ts +2 -2
  299. package/server/engine/ingest-dedupe.ts +149 -0
  300. package/server/engine/ingest-health.ts +171 -22
  301. package/server/engine/integration-probes.ts +7 -7
  302. package/server/engine/kpi-baseline.ts +4 -4
  303. package/server/engine/kpi-fold.ts +592 -41
  304. package/server/engine/kpi-query.ts +307 -48
  305. package/server/engine/live-feed-registry.ts +1 -1
  306. package/server/engine/load-meter.ts +1 -1
  307. package/server/engine/local-declarations.ts +15 -14
  308. package/server/engine/log.ts +1 -1
  309. package/server/engine/measured-estimator.ts +1 -1
  310. package/server/engine/measured-yield.ts +1 -1
  311. package/server/engine/model-basis.ts +2 -2
  312. package/server/engine/model-gap.ts +3 -3
  313. package/server/engine/model-vocabulary.ts +2 -2
  314. package/server/engine/oee-accumulator.ts +8 -6
  315. package/server/engine/property-effects.ts +2 -2
  316. package/server/engine/read-time.ts +77 -0
  317. package/server/engine/restart-policy.ts +1 -1
  318. package/server/engine/runtime-key.ts +1 -1
  319. package/server/engine/spec-coverage.ts +3 -3
  320. package/server/engine/stage-path.ts +5 -5
  321. package/server/engine/state-axes.ts +2 -2
  322. package/server/engine/travel-estimator.ts +2 -2
  323. package/server/engine/twin-engine.ts +906 -205
  324. package/server/engine/warm-start.ts +6 -6
  325. package/server/index.ts +1 -1
  326. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +1 -1
  327. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +3 -3
  328. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +2 -2
  329. package/server/migrations/1786300000000-IndexEventTypeByTime.ts +48 -0
  330. package/server/migrations/1786400000000-IndexEventCreatedAt.ts +38 -0
  331. package/server/migrations/1786500000000-CreateTwinSubjectEvents.ts +93 -0
  332. package/server/migrations/index.ts +7 -1
  333. package/server/routes.ts +73 -5
  334. package/server/service/index.ts +7 -1
  335. package/server/service/reference/control-routing.ts +1 -1
  336. package/server/service/reference/discovery-result.ts +3 -3
  337. package/server/service/reference/hook-contract.ts +103 -0
  338. package/server/service/reference/ingest-space.ts +4 -4
  339. package/server/service/reference/knob-defaults.ts +4 -4
  340. package/server/service/reference/reference-adapter.ts +145 -10
  341. package/server/service/reference/reference-hook.ts +145 -0
  342. package/server/service/reference/reference-live.ts +87 -15
  343. package/server/service/reference/reference-master.ts +51 -20
  344. package/server/service/reference/reference-resolver.ts +11 -11
  345. package/server/service/reference/template-registry.ts +1 -1
  346. package/server/service/reference/twin-reference.ts +3 -3
  347. package/server/service/twin-attention/twin-attention-query.ts +1 -1
  348. package/server/service/twin-audit/command-audit.ts +1 -1
  349. package/server/service/twin-audit/twin-audit-event.ts +2 -2
  350. package/server/service/twin-audit/twin-audit-query.ts +1 -1
  351. package/server/service/twin-backfill/backfill-period-facts.ts +168 -0
  352. package/server/service/twin-backfill/backfill-shape.ts +86 -0
  353. package/server/service/twin-backfill/index.ts +3 -0
  354. package/server/service/twin-backfill/twin-backfill-resolver.ts +35 -0
  355. package/server/service/twin-control/twin-control-mutation.ts +2 -2
  356. package/server/service/twin-event/twin-event-keys.ts +2 -2
  357. package/server/service/twin-event/twin-event-type.ts +18 -1
  358. package/server/service/twin-event/twin-event.ts +32 -6
  359. package/server/service/twin-forecast/forecast-metrics.ts +1 -1
  360. package/server/service/twin-forecast/gap-analytics.ts +1 -1
  361. package/server/service/twin-forecast/twin-forecast-query.ts +30 -11
  362. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +1 -1
  363. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +1 -1
  364. package/server/service/twin-instance/twin-instance.ts +4 -4
  365. package/server/service/twin-journal/event-type-split-shape.ts +112 -0
  366. package/server/service/twin-journal/event-type-split.ts +144 -0
  367. package/server/service/twin-journal/journal-order.ts +20 -0
  368. package/server/service/twin-journal/twin-journal-query.ts +224 -20
  369. package/server/service/twin-lifecycle/domain-catalog.ts +2 -1
  370. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +58 -4
  371. package/server/service/twin-metrics/twin-metrics-query.ts +14 -1
  372. package/server/service/twin-model/axis-journal-evidence.ts +1 -1
  373. package/server/service/twin-model/epcis-coverage.ts +34 -3
  374. package/server/service/twin-model/iec61850-coverage.ts +1 -1
  375. package/server/service/twin-model/isa95-coverage.ts +43 -16
  376. package/server/service/twin-model/item-ref.ts +2 -2
  377. package/server/service/twin-model/name-index.ts +2 -2
  378. package/server/service/twin-model/project-structure.ts +7 -6
  379. package/server/service/twin-model/twin-equipment.ts +16 -4
  380. package/server/service/twin-model/twin-lineage-query.ts +16 -5
  381. package/server/service/twin-model/twin-location.ts +1 -1
  382. package/server/service/twin-model/twin-model-item-query.ts +15 -15
  383. package/server/service/twin-model/twin-model-mutation.ts +4 -4
  384. package/server/service/twin-model/twin-model-query.ts +49 -28
  385. package/server/service/twin-model/twin-model-tree-query.ts +26 -2
  386. package/server/service/twin-model/twin-operation.ts +2 -2
  387. package/server/service/twin-space/move-space.ts +3 -3
  388. package/server/service/twin-space/representation-role.ts +15 -0
  389. package/server/service/twin-space/space-integrity.ts +1 -1
  390. package/server/service/twin-space/twin-space-representation.ts +37 -0
  391. package/server/service/twin-space/twin-space-resolver.ts +40 -8
  392. package/server/service/twin-structure/twin-structure.ts +3 -3
  393. package/server/service/twin-subject/event-subjects.ts +155 -0
  394. package/server/service/twin-subject/index.ts +10 -0
  395. package/server/service/twin-subject/item-resolver.ts +92 -0
  396. package/server/service/twin-subject/subject-query-shape.ts +84 -0
  397. package/server/service/twin-subject/twin-subject-backfill.ts +196 -0
  398. package/server/service/twin-subject/twin-subject-event.ts +101 -0
  399. package/server/service/twin-subject/twin-subject-fast-path.ts +346 -0
  400. package/server/service/twin-subject/twin-subject-query.ts +210 -0
  401. package/server/service/twin-subject/twin-subject-writer.ts +189 -0
  402. package/server/service/twin-target/twin-target-resolver.ts +1 -1
  403. package/shared/entity-delta.ts +4 -4
  404. package/shared/touched-items.ts +2 -2
  405. package/shared/twin-level.ts +1 -1
  406. package/test/adopt-structure-live.test.ts +3 -3
  407. package/test/aggregate-not-findone.test.ts +112 -0
  408. package/test/attention-digest.test.ts +6 -6
  409. package/test/axis-read.test.ts +6 -5
  410. package/test/backfill-shape.test.ts +89 -0
  411. package/test/boot-resume.test.ts +20 -7
  412. package/test/broadcast-cost-baseline.test.ts +2 -1
  413. package/test/broadcast-period.test.ts +2 -2
  414. package/test/broadcast-queue-overflow.test.ts +244 -0
  415. package/test/canonical-ingest-vocabularies.test.ts +259 -8
  416. package/test/canonical-quantity-door.test.ts +2 -2
  417. package/test/capability-mapping.test.ts +1 -1
  418. package/test/checkpoint-refuses-empty.test.ts +2 -2
  419. package/test/connector-capability-declaration.test.ts +88 -0
  420. package/test/contract-layer-guard.test.ts +48 -0
  421. package/test/control-capability.test.ts +1 -1
  422. package/test/cursor-stall-not-read-failure.test.ts +2 -2
  423. package/test/declaration-reaches-model.test.ts +7 -5
  424. package/test/declared-stimulus.test.ts +4 -4
  425. package/test/discovery-result.test.ts +2 -2
  426. package/test/entity-delta.test.ts +4 -4
  427. package/test/equipment-identity-reaches.test.ts +57 -0
  428. package/test/event-subjects.test.ts +147 -0
  429. package/test/event-time-column.test.ts +3 -3
  430. package/test/event-type-split.test.ts +155 -0
  431. package/test/fact-scope-wiring.test.ts +96 -0
  432. package/test/fold-gap-speaks.test.ts +53 -0
  433. package/test/forecast-metrics.test.ts +1 -1
  434. package/test/forecast-tuning.test.ts +1 -1
  435. package/test/generated-not-counted.test.ts +99 -0
  436. package/test/ingest-bench.test.ts +2 -1
  437. package/test/ingest-health-engine.test.ts +2 -2
  438. package/test/ingest-health-wiring.test.ts +7 -5
  439. package/test/ingest-health.test.ts +6 -6
  440. package/test/ingest-history.test.ts +2 -2
  441. package/test/ingest-idempotent.test.ts +132 -0
  442. package/test/ingest-space.test.ts +3 -3
  443. package/test/ingest-wiring-guard.test.ts +197 -0
  444. package/test/integration-probes.test.ts +3 -3
  445. package/test/integration-runner.test.ts +1 -1
  446. package/test/item-ref.test.ts +1 -1
  447. package/test/journal-order-axis.test.ts +80 -0
  448. package/test/journal-read-discipline.test.ts +16 -6
  449. package/test/journal-retention.test.ts +171 -29
  450. package/test/journal-sort-axis.test.ts +1 -1
  451. package/test/journal-write-door.test.ts +3 -3
  452. package/test/journal-write-trend.test.ts +1 -1
  453. package/test/kernel-kind-guard.test.ts +3 -3
  454. package/test/knob-defaults.test.ts +1 -1
  455. package/test/kpi-baseline-db.test.ts +2 -2
  456. package/test/kpi-fold.test.ts +266 -14
  457. package/test/kpi-query-bench.test.ts +3 -3
  458. package/test/live-cursor-wiring.test.ts +6 -6
  459. package/test/live-kernel-facts.test.ts +11 -11
  460. package/test/live-mirror-parity.test.ts +9 -6
  461. package/test/load-meter.test.ts +1 -1
  462. package/test/local-declarations.test.ts +13 -12
  463. package/test/log-stamp.test.ts +1 -1
  464. package/test/master-to-twin.test.ts +2 -2
  465. package/test/measured-yield.test.ts +2 -2
  466. package/test/merge-streams.test.ts +70 -0
  467. package/test/mirror-resumes-from-checkpoint.test.ts +13 -13
  468. package/test/model-basis.test.ts +2 -2
  469. package/test/model-gap.test.ts +4 -4
  470. package/test/model-vocabulary.test.ts +2 -2
  471. package/test/move-space.test.ts +1 -1
  472. package/test/oee-accumulator.test.ts +20 -5
  473. package/test/operational-vocabulary.test.ts +2 -2
  474. package/test/project-structure-db.test.ts +2 -2
  475. package/test/projection-reaches-screen.test.ts +7 -2
  476. package/test/projection-reads-declared.test.ts +109 -0
  477. package/test/property-effects.test.ts +10 -6
  478. package/test/read-failure-ledger.test.ts +106 -0
  479. package/test/read-failure-visible.test.ts +7 -7
  480. package/test/read-time.test.ts +118 -0
  481. package/test/reference-grounding.test.ts +1 -1
  482. package/test/reference-hook.test.ts +86 -0
  483. package/test/registry-key-guard.test.ts +1 -1
  484. package/test/rename-twin.test.ts +81 -0
  485. package/test/restart-policy.test.ts +3 -3
  486. package/test/resync-origin-site.test.ts +3 -3
  487. package/test/revision-axis.test.ts +14 -8
  488. package/test/runtime-key.test.ts +3 -3
  489. package/test/scale-twin-bench.test.ts +2 -1
  490. package/test/snapshot-freshness.test.ts +4 -4
  491. package/test/source-outcome-audit.test.ts +1 -1
  492. package/test/space-integrity.test.ts +1 -1
  493. package/test/space-representation.test.ts +66 -0
  494. package/test/space-scope-includes-stopped.test.ts +55 -0
  495. package/test/spec-coverage.test.ts +1 -1
  496. package/test/standard-coverage.test.ts +19 -21
  497. package/test/status-tally.test.ts +1 -1
  498. package/test/structure-revision-db.test.ts +7 -7
  499. package/test/subject-fast-path.test.ts +148 -0
  500. package/test/subject-read-model.test.ts +175 -0
  501. package/test/touched-items.test.ts +2 -2
  502. package/test/twin-audit.test.ts +1 -1
  503. package/test/twin-event-keys.test.ts +1 -1
  504. package/test/twin-model-item-db.test.ts +5 -5
  505. package/test/twin-model-tree-db.test.ts +35 -2
  506. package/test/twin-origin-resync.test.ts +1 -1
  507. package/test/vocabulary-guard.test.ts +1 -1
  508. package/test/warm-start-seam.test.ts +1 -1
  509. package/test/warm-start.test.ts +6 -6
  510. package/test/withheld-door.test.ts +2 -2
  511. package/test/yield-loop.test.ts +4 -4
  512. package/tsconfig.shared.tsbuildinfo +1 -1
  513. package/tsconfig.tsbuildinfo +1 -1
@@ -1,5 +1,5 @@
1
1
  /*
2
- * 업무 KPI 폴드 — **이벤트 저널을 접어** 시간창 지표를 만든다(순수).
2
+ * 업무 KPI 폴드 — **이벤트 저널을 계산해** 시간창 지표를 만든다(순수).
3
3
  *
4
4
  * ── 왜 필요한가 ─────────────────────────────────────────────────────────────
5
5
  * 지금 트윈이 답할 수 있는 것은 "이 순간의 값"(점유 92%)과 "인프라 부하"(초당 이벤트 수)뿐이다.
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * ── 원료는 이미 있다 ────────────────────────────────────────────────────────
10
10
  * 저널(`TwinEvent`)에 `task.status`·`order.status`·`equipment.status` 전이가 `eventTime`(ISO)과 함께
11
- * append-only 로 쌓인다. 그래서 새 계측을 심을 필요가 없다 — **접기만 하면 된다.**
12
- * 같은 모양의 선례가 이 폴더에 있다(`oee-accumulator.ts` — equipment 전이를 접어 OEE 카운터 생산).
11
+ * append-only 로 쌓인다. 그래서 새 계측을 심을 필요가 없다 — **계산만 하면 된다.**
12
+ * 같은 모양의 선례가 이 폴더에 있다(`oee-accumulator.ts` — equipment 전이를 계산해 OEE 카운터 생산).
13
13
  *
14
14
  * ── 이 파일이 하지 않는 것 ──────────────────────────────────────────────────
15
15
  * · 목표와 비교하지 않는다. "95% 목표 대비" 는 목표를 저장하는 별개 층(경영 KPI)의 몫이고,
@@ -23,27 +23,16 @@
23
23
  * 짝맞춤은 **커널이 소유한다**(`foldTaskRecords`). 완료는 작업당 하나·나중 것이 사실, 착수는 처음 것,
24
24
  * 종류·오더는 어느 전이에서든 — 그 규칙은 커널 상태 기계에서 나오는 지식이다. 여기서 다시 적었더니
25
25
  * 공정 타임라인과 **다른 답**을 냈다(재전송된 완료를 두 건으로 세어 처리량·점유가 부풀려졌다).
26
- * 이 파일은 이제 "접힌 기록 → 창 지표" 만 한다.
26
+ * 이 파일은 이제 "계산한 기록 → 창 지표" 만 한다.
27
27
  */
28
- import {
29
- ENERGY_EVENT,
30
- attributeEnergy,
31
- electricityCost,
32
- energyIntensity,
33
- energyOfWindows,
34
- foldTaskRecords,
35
- isOrderTerminal,
36
- activeShiftAt,
37
- type ElectricityCost,
38
- type TariffDeclaration,
39
- type TaskFacets,
40
- type WorkCalendarEntry
41
- } from '@operato/twin-kernel'
28
+ import { attributeEnergy, electricityCost, energyIntensity, energyOfWindows, foldTaskRecords, type ElectricityCost, type TariffDeclaration, type TaskFacets } from '@operato/twin-kernel'
29
+ import { type WorkCalendarEntry } from '@operato/ops-contract'
30
+ import { ENERGY_EVENT, isOrderTerminal, activeShiftAt } from '@operato/ops-contract'
42
31
 
43
32
  /** 저널 한 줄 — 필요한 것만(엔티티·typeorm 비의존). */
44
33
  export interface KpiEvent {
45
34
  eventType?: string
46
- /** ISO 시각. 파싱 불가하면 그 줄은 버린다(조용히 0 으로 세지 않는다). */
35
+ /** ISO 시각. 파싱 불가하면 그 줄은 버린다(알리지 않고 0 으로 세지 않는다). */
47
36
  eventTime?: string
48
37
  payload?: any
49
38
  }
@@ -58,7 +47,7 @@ export interface DurationStats {
58
47
  /** 짝이 맞아 계산된 건수. */
59
48
  count: number
60
49
  /**
61
- * 하위 꼬리 — **분포를 양쪽에서 접지하기 위해** 낸다.
50
+ * 하위 꼬리 — **분포를 양쪽에서 계산하지하기 위해** 낸다.
62
51
  * p50·p90 만 있으면 아래쪽 퍼짐을 위쪽에서 거울처럼 베껴 쓸 수밖에 없다(발명). 관측이 말하게 한다.
63
52
  */
64
53
  p10Ms: number
@@ -94,7 +83,7 @@ export interface KpiGroup {
94
83
  * **품질 판정이 있었던 건수만** 센다 (2026-08-19).
95
84
  *
96
85
  * 판정이 없는 작업(이동·체류·중간 스테이션)은 어느 쪽에도 세지 않는다 — 모름을 양품으로 세면 양품률이
97
- * 조용히 100% 로 올라간다. 그래서 `good + scrap` 은 `tasks` 보다 작을 수 있고, 그것이 정상이다.
86
+ * 알리지 않고 100% 로 올라간다. 그래서 `good + scrap` 은 `tasks` 보다 작을 수 있고, 그것이 정상이다.
98
87
  *
99
88
  * 판정이 하나도 없으면 **필드를 만들지 않는다**(0/0 을 「양품률 0%」로 읽히게 두지 않는다).
100
89
  */
@@ -135,9 +124,137 @@ export interface KpiResult {
135
124
  */
136
125
  peakGrossKW?: number
137
126
  shavedKW?: number
127
+ /**
128
+ * 이 창의 **발전량** — 커널이 마감한 기간 사실(`energy.generation.period`)에서 온다.
129
+ *
130
+ * 부하(`kWh`)와 더하지 않는다: 발전은 설비가 만든 양이고 부하는 계량 지점을 지난 양이다. 두 수를
131
+ * 더하면 어느 쪽도 아닌 숫자가 된다.
132
+ *
133
+ * 성과지표(ISO 50001 EnPI)와 태양광 성능비가 이 값을 분자로 쓴다. 발전 사실이 없으면 `undefined`
134
+ * 다(0 이 아니다 — 「발전이 없다」와 「발전 설비가 없다」는 다르다).
135
+ */
136
+ generated?: {
137
+ /** 창에 온전히 든 기간들의 합(kWh). */
138
+ kWh: number
139
+ /** 센 기간 수. */
140
+ counted: number
141
+ /**
142
+ * 그중 **관측이 기간을 다 덮지 못한** 기간 수.
143
+ *
144
+ * 커널이 기간마다 관측 구간을 함께 낸다. 마지막 표본이 기간 끝보다 이르면 그만큼 재지 않은
145
+ * 것이고, 그 수를 감추면 화면이 덜 잰 합을 온전한 합으로 말한다.
146
+ */
147
+ partial: number
148
+ /** 설비별 몫 — 이것은 **측정**이다(배분이 아니다). 각 설비의 적산에서 왔다. */
149
+ byEquipment: { id: string; kWh: number }[]
150
+ /**
151
+ * **이 창에 다 들어오지 않아 더하지 못한 기간** — 조용히 버리지 않는다.
152
+ *
153
+ * 더하는 것은 창에 온전히 든 기간뿐이다(걸친 기간을 더하면 다른 날 값이 이 창의 값인 척한다).
154
+ * 그런데 하루 단위 기간은 「지금부터 24시간 전」 창에 온전히 드는 일이 거의 없다 — 창이 한낮에서
155
+ * 시작하기 때문이다. 그래서 어제 발전량이 저널에 멀쩡히 있는데 화면이 비었고, 사용자는 그것을
156
+ * 「발전량이 없다」로 읽었다(2026-08-30 실측).
157
+ *
158
+ * 이 값이 있으면 화면은 **「이 구간에 다 들어오지 않은 기간이 있다」**를 말해야 한다.
159
+ */
160
+ notCounted?: { periods: number; kWh: number; earliestStart: string; latestEnd: string }
161
+ /**
162
+ * 이용률 — 만든 양 ÷ (정격 × 그 기간의 시간).
163
+ *
164
+ * **어느 정격으로 나눴는지를 함께 낸다.** 변환기를 낀 발전은 직류 정격이 교류보다 크고,
165
+ * 직류로 나눠 보여 주는 시스템이 실제로 있다. 밝히지 않으면 두 화면의 수가 두 배 달라도
166
+ * 왜 다른지 말할 수 없다.
167
+ *
168
+ * 창 전체가 아니라 **센 기간들의 길이**로 나눈다 — 하루치 하나만 센 24시간 창에서 창 길이로
169
+ * 나누면 그 하루가 창 전체에 걸쳐 있었던 것처럼 작아진다.
170
+ */
171
+ capacityFactor?: { value: number; ratedKW: number; basis: 'ac' | 'dc'; from: 'equipment' | 'location'; hours: number }
172
+ /**
173
+ * 낸 것의 값 — 기간마다 그때의 단가로 매긴다.
174
+ *
175
+ * **모든 기간에 단가가 있을 때만** 금액을 만든다. 일부만 매기고 더하면 그 합이 창 전체의 값인
176
+ * 척한다 — 못 매긴 기간 수를 함께 낸다.
177
+ */
178
+ revenue?: { amount: number; currency: string; priced: number; unpriced: number }
179
+ }
180
+ /**
181
+ * `kWh` 가 어느 회계에서 왔나.
182
+ *
183
+ * 'demand-window' 우리가 표본에서 마감한 구간 (계량이 오는 현장)
184
+ * 'usage-period' 밖에서 마감되어 온 구간 (계량 표본이 없는 현장)
185
+ *
186
+ * 둘을 더하지 않는다 — 같은 전기를 두 번 센다. 둘 다 있으면 계량 쪽을 쓰고 그 사실을 낸다.
187
+ */
188
+ kWhBasis?: 'demand-window' | 'usage-period'
189
+ /**
190
+ * 기본요금을 매긴 **요금적용전력과 그 근거**.
191
+ *
192
+ * 이 값은 매달 다시 정해진다. 선언에 박아 두면 사람이 갱신해야 하고, 안 하면 낡은 값으로
193
+ * 기본요금이 조용히 틀린다. 그래서 **청구서가 먼저다** — 원본이 매일 실어 보낸다.
194
+ *
195
+ * 'bill' 이 창을 덮는 청구서가 말한 값
196
+ * 'carried-forward' 가장 최근 청구서의 값을 이어 쓴다(요금 제도가 실제로 그렇게 군다)
197
+ * 'declaration' 청구가 오지 않는 현장 — 모델의 선언
198
+ *
199
+ * `declaredKW` 와 다르면 **선언이 낡았다**(`stale`). 그 사실을 감추면 아무도 다시 읽지 않는다.
200
+ */
201
+ billingDemand?: {
202
+ kW: number
203
+ from: 'tariff-basis' | 'bill' | 'carried-forward' | 'declaration'
204
+ declaredKW?: number
205
+ stale?: boolean
206
+ /**
207
+ * 이 창에서 관측한 최고 수요가 그 기준을 넘었다 — **다음 주기의 기본요금이 오른다.**
208
+ *
209
+ * 계약 초과와 다른 값이다. 계약 초과는 지금 줄이면 되고, 이것은 이미 정해진 결과다.
210
+ *
211
+ * `observedTo` 는 이 최고값이 언제까지의 것인지다. 최고 수요는 구간이 마감되어야 오므로
212
+ * 넘은 것을 뒤늦게 안다 — 그 한계를 화면이 말할 수 있어야 한다.
213
+ */
214
+ exceeded?: { peakKW: number; overKW: number; observedTo: string }
215
+ }
216
+ /**
217
+ * 밖에서 마감되어 온 사용 구간 — 계량 표본이 없는 현장의 유일한 근거다.
218
+ *
219
+ * `basis` 별 건수를 함께 낸다: 같은 kWh 라도 계량기가 잰 것과 공급자 화면에서 긁어온 것은 무게가
220
+ * 다르고, 실제로 긁어온 값이 망가져 있는 것을 본 적이 있다.
221
+ */
222
+ usage?: {
223
+ kWh: number
224
+ counted: number
225
+ /** 구간마다 제 단가로 매긴 사용량 요금 — 모든 구간이 단가를 실었을 때만. */
226
+ charge?: number
227
+ currency?: string
228
+ /** 단가가 없어 요금을 매기지 못한 구간 수 — 조용히 빼지 않는다. */
229
+ unpriced: number
230
+ byBasis: { basis: string; counted: number }[]
231
+ }
232
+ /**
233
+ * 이 창에 온전히 든 **청구서** — 공급자가 확정한 금액.
234
+ *
235
+ * 우리 계산을 덮지 않는다. 창이 청구 주기와 정확히 같을 때만 견줄 수 있으므로 그 사실을
236
+ * `matchesWindow` 로 함께 낸다 — 다른 창의 두 수를 빼면 그 차이는 아무것도 뜻하지 않는다.
237
+ */
238
+ bill?: {
239
+ from: string
240
+ to: string
241
+ total?: number
242
+ energyCharge?: number
243
+ demandCharge?: number
244
+ currency?: string
245
+ billingDemandKW?: number
246
+ matchesWindow: boolean
247
+ /**
248
+ * 창이 같을 때, **우리 계산에서 청구액을 뺀 값** — 0 이 아니면 우리 계산이 틀렸다는 뜻이다.
249
+ *
250
+ * 항목별로도 낸다(2026-08-29). 총액만 견주면 원본이 총액을 주지 않는 청구서에서는 비교가
251
+ * 영영 나오지 않는다 — 실제로 첫 청구서에 기본요금만 있었다. 양쪽에 있는 항목만 견준다.
252
+ */
253
+ diff?: { total?: number; demandCharge?: number; energyCharge?: number }
254
+ }
138
255
  /** 파생의 근거 — 구간 평균부하 × 길이(`mean-kw`). */
139
256
  basis: string
140
- /** 센 구간 수 · 못 센 구간 수(계측이 끊긴 구간) — 조용히 자르지 않는다. */
257
+ /** 센 구간 수 · 못 센 구간 수(계측이 끊긴 구간) — 알리지 않고 자르지 않는다. */
141
258
  counted: number
142
259
  skipped: number
143
260
  /** 완료 오더당 전력량 — 답할 수 없으면 `null` 과 이유. */
@@ -157,7 +274,7 @@ export interface KpiResult {
157
274
  * 붙지 않는 부하도 그 비율에 섞인다 — 화면이 그 문장을 함께 말해야 한다.
158
275
  */
159
276
  byFeeder?: {
160
- /** 자리(분기·수전) id. 자리를 모르는 자원은 `unknown` 으로 모인다(조용히 버리지 않는다). */
277
+ /** 자리(분기·수전) id. 자리를 모르는 자원은 `unknown` 으로 모인다(알리지 않고 버리지 않는다). */
161
278
  key: string
162
279
  kWh: number
163
280
  /**
@@ -186,7 +303,7 @@ export interface KpiResult {
186
303
  peakKW?: number
187
304
  }[]
188
305
  /**
189
- * 나누지 못한 양과 그 이유 — **조용히 0 으로 만들지 않는다.**
306
+ * 나누지 못한 양과 그 이유 — **알리지 않고 0 으로 만들지 않는다.**
190
307
  * `no-weights` = 그 창에 돌아간 자원이 없다(가동시간이 전부 0). 그러면 총량은 그대로 남는다.
191
308
  */
192
309
  unattributedKWh?: number
@@ -222,7 +339,7 @@ export interface KpiResult {
222
339
  utilization: { resourceId: string; busyMs: number; ratio: number }[]
223
340
  /**
224
341
  * **셈에서 빠진 것** — 완료는 창 안이지만 시작 이벤트를 못 찾은 건수.
225
- * 조회 구간이 짧아 시작이 잘렸거나 저널이 유실된 경우다. 이 값을 숨기면 평균이 조용히 거짓이 된다.
342
+ * 조회 구간이 짧아 시작이 잘렸거나 저널이 유실된 경우다. 이 값을 숨기면 평균이 알리지 않고 거짓이 된다.
226
343
  */
227
344
  unpaired: { leadTime: number; workTime: number }
228
345
  /**
@@ -372,6 +489,266 @@ function feederShares(
372
489
  * 분기 계량 합이 뿌리보다 크면(회계가 다른 두 계량기) **그 사실을 낸다** — 값을 깎아 맞추면 계량을
373
490
  * 우리가 고쳐 쓰는 것이다.
374
491
  */
492
+ /**
493
+ * 창의 **발전량** — 커널이 마감한 기간 사실을 합한다.
494
+ *
495
+ * ── 왜 합이 전부인가 ───────────────────────────────────────────────────────
496
+ * 기간 발전량은 커널이 이미 확정한 사실이다(적산의 종류마다 다른 규칙으로 얻는다). 여기서 다시
497
+ * 계산하면 규칙이 두 벌이 되고, 그때부터 두 화면이 다른 발전량을 말한다. 이 함수가 하는 일은
498
+ * **창에 든 것을 고르고, 설비별로 묶고, 근거가 얼마나 덮였는지 세는 것**이다.
499
+ *
500
+ * ── 걸친 기간은 세지 않는다 ─────────────────────────────────────────────────
501
+ * 구간 마감과 같은 규칙이다. 하루의 절반이 창에 들었다고 절반을 세면 그것은 관측이 아니라 우리가 나눈
502
+ * 값이고, 합이 창을 넘는다.
503
+ *
504
+ * ── 덜 덮인 기간을 센다 ─────────────────────────────────────────────────────
505
+ * 기간의 끝이 자정인데 마지막 표본이 저녁이었으면 그 사이는 재지 않은 것이다. 그 수를 함께 내야 화면이
506
+ * 「이 합은 이만큼 덜 잰 것」이라고 말할 수 있다. 얼마나 어긋나면 문제인지는 현장이 정한다.
507
+ */
508
+ function generatedOfPeriods(
509
+ periods: readonly {
510
+ equipmentId: string
511
+ kWh: number
512
+ startMs: number
513
+ endMs: number
514
+ observedFromMs: number
515
+ observedToMs: number
516
+ }[],
517
+ range: { startMs: number; endMs: number },
518
+ /** 이용률의 분모 — 없으면 이용률을 내지 않는다(0 이나 짐작을 만들지 않는다). */
519
+ rated?: { kW: number; basis: 'ac' | 'dc'; from: 'equipment' | 'location' },
520
+ /** 기간마다 적용할 단가 — 기간을 덮는 것을 쓴다. 없으면 금액을 만들지 않는다. */
521
+ prices?: readonly { startMs: number; endMs: number; unitPrice: number; currency: string }[]
522
+ ): Partial<NonNullable<KpiResult['energy']>> {
523
+ const inside = periods.filter(g => g.startMs >= range.startMs && g.endMs <= range.endMs)
524
+
525
+ /*
526
+ * ── **걸친 기간을 조용히 버리지 않는다** (2026-08-30) ──────────────────────
527
+ *
528
+ * 더하는 것은 창에 **온전히 들어온 기간**뿐이다. 그것이 맞는 셈이다 — 걸친 기간을 더하면 다른 날의
529
+ * 값이 이 창의 값인 척한다.
530
+ *
531
+ * 그런데 버린 것을 말하지 않았다. 하루 단위 기간은 「지금부터 24시간 전」 창에 온전히 들어오는 일이
532
+ * 거의 없다(창이 한낮에서 시작하므로). 그래서 어제 발전량이 저널에 멀쩡히 있는데 화면이 비었고,
533
+ * 사용자는 그것을 **「발전량이 없다」**로 읽었다. 실측으로 그 일이 났다.
534
+ *
535
+ * 이제 걸친 것을 세어 함께 낸다. 화면이 「이 창에 다 들어오지 않은 기간이 있다」를 말할 수 있다.
536
+ */
537
+ const straddling = periods.filter(
538
+ g => !(g.startMs >= range.startMs && g.endMs <= range.endMs) && g.endMs > range.startMs && g.startMs < range.endMs
539
+ )
540
+ const notCounted = straddling.length
541
+ ? {
542
+ periods: straddling.length,
543
+ kWh: straddling.reduce((a, g) => a + g.kWh, 0),
544
+ earliestStart: new Date(Math.min(...straddling.map(g => g.startMs))).toISOString(),
545
+ latestEnd: new Date(Math.max(...straddling.map(g => g.endMs))).toISOString()
546
+ }
547
+ : undefined
548
+
549
+ if (!inside.length) {
550
+ /* 온전히 들어온 것이 없다 — 그래도 걸친 것이 있으면 그 사실을 낸다(빈 값과 「없음」은 다르다). */
551
+ return notCounted ? { generated: { kWh: 0, counted: 0, partial: 0, byEquipment: [], notCounted } } : {}
552
+ }
553
+
554
+ const byEquipment = new Map<string, number>()
555
+ let partial = 0
556
+ for (const g of inside) {
557
+ byEquipment.set(g.equipmentId, (byEquipment.get(g.equipmentId) ?? 0) + g.kWh)
558
+ const coveredFrom = Number.isFinite(g.observedFromMs) ? g.observedFromMs <= g.startMs : false
559
+ const coveredTo = Number.isFinite(g.observedToMs) ? g.observedToMs >= g.endMs : false
560
+ if (!coveredFrom || !coveredTo) partial++
561
+ }
562
+
563
+ const kWh = inside.reduce((a, g) => a + g.kWh, 0)
564
+ /*
565
+ * 이용률의 분모는 **센 기간들의 길이**다. 창 길이로 나누면, 하루치 하나만 센 24시간 창에서 그 하루가
566
+ * 창 전체에 걸쳐 있었던 것처럼 작아진다.
567
+ */
568
+ const hours = inside.reduce((a, g) => a + (g.endMs - g.startMs) / 3_600_000, 0)
569
+ const capacityFactor =
570
+ rated && rated.kW > 0 && hours > 0
571
+ ? { value: kWh / (rated.kW * hours), ratedKW: rated.kW, basis: rated.basis, from: rated.from, hours }
572
+ : undefined
573
+
574
+ /*
575
+ * 낸 것의 값 — 기간마다 **그 기간을 덮는 단가**로 매긴다. 걸친 단가는 쓰지 않는다: 하루 단가로
576
+ * 이틀치를 매기면 그 절반은 다른 날의 값이 된다.
577
+ *
578
+ * 모든 기간에 단가가 있을 때만 금액을 만든다. 일부만 매기고 더하면 그 합이 창 전체의 값인 척한다.
579
+ */
580
+ let amount = 0
581
+ let priced = 0
582
+ let currency: string | undefined
583
+ for (const g of inside) {
584
+ const p = (prices ?? []).find(x => x.startMs <= g.startMs && x.endMs >= g.endMs)
585
+ if (!p) continue
586
+ amount += g.kWh * p.unitPrice
587
+ priced++
588
+ currency = currency ?? p.currency
589
+ }
590
+ const unpriced = inside.length - priced
591
+ const revenue = priced > 0 && unpriced === 0 && currency ? { amount, currency, priced, unpriced } : undefined
592
+
593
+ return {
594
+ generated: {
595
+ kWh,
596
+ counted: inside.length,
597
+ partial,
598
+ byEquipment: [...byEquipment.entries()]
599
+ .map(([id, kWh]) => ({ id, kWh }))
600
+ .sort((a, b) => b.kWh - a.kWh),
601
+ ...(capacityFactor ? { capacityFactor } : {}),
602
+ ...(revenue ? { revenue } : {}),
603
+ /* 이 창에 다 들어오지 않아 더하지 못한 기간 — 있으면 화면이 그것을 말한다. */
604
+ ...(notCounted ? { notCounted } : {})
605
+ }
606
+ }
607
+ }
608
+
609
+ /**
610
+ * 밖에서 마감되어 온 사용 구간을 합한다 — **구간 마감과 더하지 않는다.**
611
+ *
612
+ * 두 회계가 같은 전기를 말한다. 더하면 두 번 세고, 둘 다 있는 현장에서는 계량 쪽이 강한 근거이므로
613
+ * 그쪽을 쓴다. 여기서는 이쪽 몫만 계산하고, 무엇을 쓸지는 부르는 쪽이 정한다.
614
+ *
615
+ * 단가는 **모든 구간이 실었을 때만** 요금을 만든다. 일부만 실었는데 있는 것만 더하면 그 합이 창의
616
+ * 요금인 척한다 — 못 매긴 구간 수를 함께 낸다.
617
+ */
618
+ function usageOfPeriods(
619
+ periods: readonly { meterId: string; kWh: number; startMs: number; endMs: number; unitPrice: number; currency?: string; basis: string }[],
620
+ range: { startMs: number; endMs: number }
621
+ ): NonNullable<KpiResult['energy']>['usage'] | undefined {
622
+ const inside = periods.filter(u => u.startMs >= range.startMs && u.endMs <= range.endMs)
623
+ if (!inside.length) return undefined
624
+
625
+ const byBasis = new Map<string, number>()
626
+ let kWh = 0
627
+ let charge = 0
628
+ let unpriced = 0
629
+ let currency: string | undefined
630
+ for (const u of inside) {
631
+ kWh += u.kWh
632
+ byBasis.set(u.basis, (byBasis.get(u.basis) ?? 0) + 1)
633
+ if (Number.isFinite(u.unitPrice)) {
634
+ charge += u.kWh * u.unitPrice
635
+ if (!currency) currency = u.currency
636
+ } else unpriced++
637
+ }
638
+
639
+ return {
640
+ kWh,
641
+ counted: inside.length,
642
+ ...(unpriced === 0 && currency ? { charge, currency } : {}),
643
+ unpriced,
644
+ byBasis: [...byBasis.entries()].map(([basis, counted]) => ({ basis, counted })).sort((a, b) => b.counted - a.counted)
645
+ }
646
+ }
647
+
648
+ /**
649
+ * **요금적용전력을 고른다 — 청구서가 먼저다.**
650
+ *
651
+ * 순서에 뜻이 있다. 청구서는 그 기간에 실제로 매겨진 값이고, 선언은 사람이 옮겨 적은 값이다.
652
+ * 그 값이 매달 바뀌므로 옮겨 적은 것은 곧 낡는다.
653
+ */
654
+ function billingDemandOf(
655
+ billsInWindow: readonly { endMs: number; billingDemandKW: number }[],
656
+ tariffBasis: { billingDemandKW?: number } | undefined,
657
+ lastBill: { billingDemandKW?: number } | undefined,
658
+ declaredKW: number | undefined,
659
+ /** 이 창에서 관측한 최고 수요와 그 값이 언제까지의 것인지 — 사용 구간의 `maxKW` 에서 온다. */
660
+ observedPeak?: { kW: number; toMs: number }
661
+ ): NonNullable<KpiResult['energy']>['billingDemand'] | undefined {
662
+ const declared = Number.isFinite(Number(declaredKW)) ? Number(declaredKW) : undefined
663
+ const withValue = billsInWindow.filter(b => Number.isFinite(b.billingDemandKW))
664
+ const fromBill = withValue.length ? withValue.reduce((a, x) => (x.endMs > a.endMs ? x : a)).billingDemandKW : undefined
665
+ const carried = Number.isFinite(Number(lastBill?.billingDemandKW)) ? Number(lastBill?.billingDemandKW) : undefined
666
+ /* 이 주기에 적용되는 기준이 가장 먼저다 — 청구서는 끝난 기간의 것이다. */
667
+ const fromBasis = Number.isFinite(Number(tariffBasis?.billingDemandKW)) ? Number(tariffBasis?.billingDemandKW) : undefined
668
+
669
+ const picked =
670
+ fromBasis !== undefined
671
+ ? { kW: fromBasis, from: 'tariff-basis' as const }
672
+ : fromBill !== undefined
673
+ ? { kW: fromBill, from: 'bill' as const }
674
+ : carried !== undefined
675
+ ? { kW: carried, from: 'carried-forward' as const }
676
+ : declared !== undefined
677
+ ? { kW: declared, from: 'declaration' as const }
678
+ : undefined
679
+ if (!picked) return undefined
680
+
681
+ /* 선언이 있는데 청구서와 다르면 그 사실을 낸다 — 낡은 선언은 스스로 말해야 한다. */
682
+ const stale = picked.from !== 'declaration' && declared !== undefined && declared !== picked.kW
683
+ const exceeded =
684
+ observedPeak && observedPeak.kW > picked.kW
685
+ ? {
686
+ peakKW: observedPeak.kW,
687
+ overKW: observedPeak.kW - picked.kW,
688
+ observedTo: new Date(observedPeak.toMs).toISOString()
689
+ }
690
+ : undefined
691
+
692
+ return {
693
+ ...picked,
694
+ ...(declared !== undefined ? { declaredKW: declared } : {}),
695
+ ...(stale ? { stale: true } : {}),
696
+ ...(exceeded ? { exceeded } : {})
697
+ }
698
+ }
699
+
700
+ /**
701
+ * 창에 온전히 든 청구서 하나 — 가장 늦게 끝난 것.
702
+ *
703
+ * **창이 청구 주기와 같을 때만 견준다.** 한 달 청구서를 하루 창의 값과 빼면 그 차이는 아무것도 뜻하지
704
+ * 않는다. 그래서 같은지를 값과 함께 낸다.
705
+ */
706
+ function billOfWindow(
707
+ bills: readonly {
708
+ from: string
709
+ to: string
710
+ startMs: number
711
+ endMs: number
712
+ total: number
713
+ energyCharge: number
714
+ demandCharge: number
715
+ currency?: string
716
+ billingDemandKW: number
717
+ }[],
718
+ range: { startMs: number; endMs: number },
719
+ ours?: { total?: number; demandCharge?: number; energyCharge?: number }
720
+ ): NonNullable<KpiResult['energy']>['bill'] | undefined {
721
+ const inside = bills.filter(b => b.startMs >= range.startMs && b.endMs <= range.endMs)
722
+ if (!inside.length) return undefined
723
+ const b = inside.reduce((a, x) => (x.endMs > a.endMs ? x : a))
724
+ const matchesWindow = b.startMs === range.startMs && b.endMs === range.endMs
725
+ /* 양쪽에 있는 항목만 견준다 — 한쪽이 없는 것을 0 으로 두면 그 차이가 통째로 어긋남처럼 보인다. */
726
+ const gap = (mine: unknown, billed: number): number | undefined =>
727
+ Number.isFinite(Number(mine)) && Number.isFinite(billed) ? Number(mine) - billed : undefined
728
+ const diff = matchesWindow
729
+ ? {
730
+ ...(gap(ours?.total, b.total) !== undefined ? { total: gap(ours?.total, b.total) as number } : {}),
731
+ ...(gap(ours?.demandCharge, b.demandCharge) !== undefined
732
+ ? { demandCharge: gap(ours?.demandCharge, b.demandCharge) as number }
733
+ : {}),
734
+ ...(gap(ours?.energyCharge, b.energyCharge) !== undefined
735
+ ? { energyCharge: gap(ours?.energyCharge, b.energyCharge) as number }
736
+ : {})
737
+ }
738
+ : {}
739
+ return {
740
+ from: b.from,
741
+ to: b.to,
742
+ ...(Number.isFinite(b.total) ? { total: b.total } : {}),
743
+ ...(Number.isFinite(b.energyCharge) ? { energyCharge: b.energyCharge } : {}),
744
+ ...(Number.isFinite(b.demandCharge) ? { demandCharge: b.demandCharge } : {}),
745
+ ...(b.currency ? { currency: b.currency } : {}),
746
+ ...(Number.isFinite(b.billingDemandKW) ? { billingDemandKW: b.billingDemandKW } : {}),
747
+ matchesWindow,
748
+ ...(Object.keys(diff).length ? { diff } : {})
749
+ }
750
+ }
751
+
375
752
  function measuredFeeders(
376
753
  windows: readonly {
377
754
  startMs: number
@@ -453,11 +830,56 @@ function measuredFeeders(
453
830
  }
454
831
 
455
832
  /** 폴드 옵션 — 관점 축과, 그 축을 만들기 위해 이벤트 밖에서 와야 하는 지도. */
833
+ /*
834
+ * ── 같은 사실이 두 벌 있어도 한 번만 센다 (2026-08-30) ──────────────────────
835
+ *
836
+ * 마감된 구간 사실은 정체성이 내용에 있다(커널 0.7.81). 그런데 저널에 이미 여러 벌 적힌 것이 있다 —
837
+ * 쓰는 자리에서 막기 전에 들어간 것들이다(실측: 어느 날의 발전 단가가 세 벌).
838
+ *
839
+ * 지난 기록을 지우지 않는다 — 저널은 일어난 일의 기록이다. 대신 **읽을 때 한 번만 센다.**
840
+ * 그러지 않으면 그 구간의 수익·사용량이 그 횟수만큼 세어진다.
841
+ */
842
+ function onceById<T extends { payload?: any }>(rows: readonly T[]): T[] {
843
+ const seen = new Set<string>()
844
+ return rows.filter(r => {
845
+ const id = (r as any)?.payload?.eventId
846
+ if (typeof id !== 'string' || !id) return true
847
+ if (seen.has(id)) return false
848
+ seen.add(id)
849
+ return true
850
+ })
851
+ }
852
+
456
853
  export interface KpiFoldOptions {
457
854
  groupBy?: KpiGroupBy
855
+ /**
856
+ * 이 트윈이 이어받고 있는 **마지막 청구서** — 요금적용전력의 우선 근거(§`billingDemandOf`).
857
+ *
858
+ * 창 밖의 청구서는 저널 조회에 걸리지 않는다. 그런데 그 값은 이 창에도 적용되므로 호출부가
859
+ * 커널 상태에서 읽어 넘긴다.
860
+ */
861
+ lastBill?: { to: string; billingDemandKW?: number }
862
+ /**
863
+ * 이 트윈이 이어받고 있는 **요금 기준** — 이 주기에 적용되는 값(§`ENERGY_EVENT.tariffBasis`).
864
+ *
865
+ * 청구서보다 먼저다. 청구서는 끝난 기간의 정산이라 이번 주기의 기준을 말하지 않는다.
866
+ */
867
+ tariffBasis?: { from: string; billingDemandKW?: number }
868
+ /**
869
+ * 이 트윈의 **발전 정격**과 그 근거 — 이용률의 분모(§ 커널 `generationRated`).
870
+ *
871
+ * 모델의 선언에서 오므로 호출부가 읽어 넘긴다. 폴드는 저널만 본다.
872
+ */
873
+ generationRated?: { kW: number; basis: 'ac' | 'dc'; from: 'equipment' | 'location' }
874
+ /**
875
+ * 이 트윈이 이어받고 있는 **발전 단가** — 창 밖의 단가도 이 창에 적용될 수 있다.
876
+ *
877
+ * 창 안의 단가 사실이 먼저다. 이것은 그 창에 단가가 없을 때 쓴다.
878
+ */
879
+ generationPrice?: { from: string; to: string; unitPrice: number; currency: string }
458
880
  /**
459
881
  * 노드 → 구역 지도. **이벤트에는 구역이 없다**(노드까지만 있다) — 스냅샷·마스터가 아는 값이라
460
- * 호출부가 넘긴다. 없으면 구역 축은 'unknown' 으로 모인다(조용히 다른 축으로 바꾸지 않는다).
882
+ * 호출부가 넘긴다. 없으면 구역 축은 'unknown' 으로 모인다(알리지 않고 다른 축으로 바꾸지 않는다).
461
883
  */
462
884
  locationArea?: Record<string, string>
463
885
  /**
@@ -487,7 +909,7 @@ export interface KpiFoldOptions {
487
909
  tariff?: TariffDeclaration
488
910
  /**
489
911
  * 교대 선언 — **이벤트에는 교대가 없다**(시각만 있다). 현장 마스터가 아는 값이라 호출부가 넘긴다.
490
- * 없으면 교대 축은 'unknown' 으로 모인다(다른 축으로 조용히 바꾸지 않는다).
912
+ * 없으면 교대 축은 'unknown' 으로 모인다(다른 축으로 알리지 않고 바꾸지 않는다).
491
913
  */
492
914
  shifts?: WorkCalendarEntry[]
493
915
  /**
@@ -518,7 +940,7 @@ interface CompletionRecord {
518
940
  waitMs?: number
519
941
  }
520
942
 
521
- /** 이 기록이 요청한 축에서 어느 값에 속하는가. 값이 없으면 'unknown'(조용히 버리지 않는다). */
943
+ /** 이 기록이 요청한 축에서 어느 값에 속하는가. 값이 없으면 'unknown'(알리지 않고 버리지 않는다). */
522
944
  function facetKey(rec: CompletionRecord, options: KpiFoldOptions): string {
523
945
  const f = rec.facets
524
946
  switch (options.groupBy) {
@@ -547,7 +969,7 @@ function facetKey(rec: CompletionRecord, options: KpiFoldOptions): string {
547
969
  */
548
970
  if (!options.shifts?.length) return 'unknown'
549
971
  /* 시각까지 넘긴다 — **휴일에 일어난 일은 어느 교대에도 속하지 않는다.** 되풀이만 보면 그날 서지도
550
- 않은 교대에 집계돼, 교대별 성과가 조용히 거짓이 된다. */
972
+ 않은 교대에 집계돼, 교대별 성과가 알리지 않고 거짓이 된다. */
551
973
  return activeShiftAt(options.shifts, rec.at, options.utcOffsetMinutes) ?? 'unknown'
552
974
  default:
553
975
  return 'unknown'
@@ -576,7 +998,7 @@ function qualityOf(rows: CompletionRecord[]): { quality?: { good: number; scrap:
576
998
  * 기록을 축으로 묶어 축별 통계를 낸다 — **전체와 같은 규칙**으로(같은 stats 함수, 같은 창).
577
999
  *
578
1000
  * 축의 값이 아주 많을 수 있다(오더 축은 수천 개). 그래서 건수 큰 것부터 상한까지만 돌려주고
579
- * **잘라낸 수를 함께 알린다** — 조용한 절단은 "이게 전부" 라는 거짓을 만든다.
1001
+ * **잘라낸 수를 함께 알린다** — 들어온 것이 없는 절단은 "이게 전부" 라는 거짓을 만든다.
580
1002
  */
581
1003
  function groupRecords(
582
1004
  records: CompletionRecord[],
@@ -644,11 +1066,11 @@ function stats(values: number[]): DurationStats {
644
1066
  }
645
1067
 
646
1068
  /**
647
- * 이벤트를 접어 시간창 KPI 를 만든다.
1069
+ * 이벤트를 계산해 시간창 KPI 를 만든다.
648
1070
  *
649
1071
  * **호출부 규약**: `events` 는 창보다 **넉넉히 앞까지** 담아 넘긴다(예: 창 시작 − 리드타임 상한).
650
1072
  * 완료된 작업의 시작 이벤트가 창 밖에 있을 수 있기 때문이다. 못 찾은 것은 `unpaired` 로 알린다 —
651
- * 평균에서 조용히 빠지면 지표가 거짓이 된다.
1073
+ * 평균에서 오류 없이 빠지면 지표가 거짓이 된다.
652
1074
  *
653
1075
  * 시간축은 `eventTime`(ISO) 차분이다. sim 클록이 실제 시각과 오프셋이 있어도 **차분은 정확**하다
654
1076
  * (고정 오프셋은 상쇄된다). 파싱 불가한 줄은 버린다.
@@ -676,7 +1098,7 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
676
1098
  * **끝났나는 커널이 답한다** (2026-08-23) — `isOrderTerminal`.
677
1099
  *
678
1100
  * 예전에는 여기서 **동의어 목록**으로 추론했다(`completed`·`fulfilled`·`done`·`finished`).
679
- * 그 방식이 낸 대가가 둘이었고 둘 다 조용했다.
1101
+ * 그 방식이 낸 대가가 둘이었고 둘 다 들어온 것이 없었다.
680
1102
  *
681
1103
  * ① 원본의 낱말이 목록에 없으면 완료가 0 이 된다. 실제로 그랬다 — 첫 실 시스템의 `FINISHED`
682
1104
  * 가 빠져 있었고, 그때 목록에 낱말을 더한 것은 진짜 빈 자리(그 커넥터가 자기 상태 표를
@@ -693,7 +1115,7 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
693
1115
  if (at >= window.fromMs && at <= window.toMs) {
694
1116
  /*
695
1117
  * **오더당 한 번만 센다.** 폴링하는 원본은 같은 오더의 종결을 여러 주기에 걸쳐 다시 보낼 수
696
- * 있고(그것은 결함이 아니라 폴링의 성질이다), 사건마다 세면 처리량이 조용히 부풀려진다.
1118
+ * 있고(그것은 결함이 아니라 폴링의 성질이다), 사건마다 세면 처리량이 알리지 않고 부풀려진다.
697
1119
  * 작업 쪽은 커널의 짝맞춤이 이미 그 규율을 갖고 있다(「완료는 작업당 하나」) — 오더만 없었다.
698
1120
  */
699
1121
  if (done) {
@@ -701,7 +1123,7 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
701
1123
  if (id && !completedOrderIds.has(id)) {
702
1124
  completedOrderIds.add(id)
703
1125
  completedOrders++
704
- } else if (!id) completedOrders++ // 정체를 모르면 셈에서 빼지 않는다(조용히 줄이지 않는다)
1126
+ } else if (!id) completedOrders++ // 정체를 모르면 셈에서 빼지 않는다(알리지 않고 줄이지 않는다)
705
1127
  }
706
1128
  /*
707
1129
  * **완료로 세지 않은 낱말의 인구조사.** 「못 읽었다」가 아니다 — `picking` 처럼 완료가 아닌
@@ -744,6 +1166,76 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
744
1166
  })
745
1167
  .filter(w => Number.isFinite(w.startMs) && Number.isFinite(w.endMs))
746
1168
 
1169
+ /*
1170
+ * 발전 기간 사실 — 커널이 기간을 마감할 때 낸 것이다. 구간 마감과 같은 규칙으로 **창에 온전히 든
1171
+ * 것만** 센다(걸친 기간을 반만 세면 합이 창을 넘는다).
1172
+ *
1173
+ * 관측 구간(`observedFrom`·`observedTo`)을 함께 읽는다 — 값의 근거가 얼마나 덮였는지가 그 둘에 있다.
1174
+ */
1175
+ const generationPeriods = onceById(events.filter(e => e.eventType === ENERGY_EVENT.generationPeriod))
1176
+ .map(e => {
1177
+ const d = e.payload?.data ?? e.payload ?? {}
1178
+ return {
1179
+ equipmentId: String(d.equipmentId ?? ''),
1180
+ kWh: Number(d.kWh),
1181
+ startMs: Date.parse(String(d.periodStart ?? '')),
1182
+ endMs: Date.parse(String(d.periodEnd ?? '')),
1183
+ observedFromMs: Date.parse(String(d.observedFrom ?? '')),
1184
+ observedToMs: Date.parse(String(d.observedTo ?? ''))
1185
+ }
1186
+ })
1187
+ .filter(g => g.equipmentId && Number.isFinite(g.kWh) && Number.isFinite(g.startMs) && Number.isFinite(g.endMs))
1188
+
1189
+ /*
1190
+ * 밖에서 마감되어 온 사용 구간 — 구간 마감과 같은 규칙으로 **창에 온전히 든 것만** 센다.
1191
+ */
1192
+ const usagePeriods = onceById(events.filter(e => e.eventType === ENERGY_EVENT.usagePeriod))
1193
+ .map(e => {
1194
+ const d = e.payload?.data ?? e.payload ?? {}
1195
+ return {
1196
+ meterId: String(d.meterId ?? ''),
1197
+ kWh: Number(d.kWh),
1198
+ startMs: Date.parse(String(d.from ?? '')),
1199
+ endMs: Date.parse(String(d.to ?? '')),
1200
+ unitPrice: Number(d.unitPrice),
1201
+ maxKW: Number(d.maxKW),
1202
+ currency: d.currency ? String(d.currency) : undefined,
1203
+ basis: d.basis ? String(d.basis) : 'unstated'
1204
+ }
1205
+ })
1206
+ .filter(u => u.meterId && Number.isFinite(u.kWh) && Number.isFinite(u.startMs) && Number.isFinite(u.endMs))
1207
+
1208
+ /* 청구서 — 창에 온전히 든 것 중 가장 늦게 끝난 것 하나. */
1209
+ const bills = onceById(events.filter(e => e.eventType === ENERGY_EVENT.bill))
1210
+ .map(e => {
1211
+ const d = e.payload?.data ?? e.payload ?? {}
1212
+ return {
1213
+ from: String(d.from ?? ''),
1214
+ to: String(d.to ?? ''),
1215
+ startMs: Date.parse(String(d.from ?? '')),
1216
+ endMs: Date.parse(String(d.to ?? '')),
1217
+ total: Number(d.total),
1218
+ energyCharge: Number(d.energyCharge),
1219
+ demandCharge: Number(d.demandCharge),
1220
+ currency: d.currency ? String(d.currency) : undefined,
1221
+ billingDemandKW: Number(d.billingDemandKW)
1222
+ }
1223
+ })
1224
+ .filter(b => Number.isFinite(b.startMs) && Number.isFinite(b.endMs))
1225
+
1226
+ /* 발전 단가 — 기간마다 그때의 값이다(날마다 바뀐다). */
1227
+ const generationPrices = onceById(events.filter(e => e.eventType === ENERGY_EVENT.generationPrice))
1228
+ .map(e => {
1229
+ const d = e.payload?.data ?? e.payload ?? {}
1230
+ return {
1231
+ startMs: Date.parse(String(d.from ?? '')),
1232
+ endMs: Date.parse(String(d.to ?? '')),
1233
+ unitPrice: Number(d.unitPrice),
1234
+ currency: String(d.currency ?? '')
1235
+ }
1236
+ })
1237
+ .filter(p => Number.isFinite(p.startMs) && Number.isFinite(p.endMs) && Number.isFinite(p.unitPrice) && p.currency)
1238
+
747
1239
  const leads: number[] = []
748
1240
  const works: number[] = []
749
1241
  const waits: number[] = []
@@ -785,18 +1277,54 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
785
1277
  return {
786
1278
  window: { fromMs: window.fromMs, toMs: window.toMs },
787
1279
  throughput: { tasks: tasksInWindow, orders: completedOrders },
788
- ...(demandWindows.length
1280
+ /* 발전만 있는 현장(계량 없는 발전소)도 답이 나와야 한다 — 둘 중 하나만 있어도 블록을 낸다. */
1281
+ ...(demandWindows.length || generationPeriods.length || usagePeriods.length || bills.length
789
1282
  ? (() => {
790
1283
  const e = energyOfWindows(demandWindows, { startMs: window.fromMs, endMs: window.toMs })
1284
+ const usage = usageOfPeriods(usagePeriods, { startMs: window.fromMs, endMs: window.toMs })
1285
+ /*
1286
+ * 어느 회계를 쓸지 정한다 — **더하지 않는다.** 계량 표본에서 우리가 마감한 값이 있으면 그것을
1287
+ * 쓰고(강한 근거), 없으면 밖에서 마감되어 온 값을 쓴다. 무엇을 썼는지 함께 낸다.
1288
+ */
1289
+ const usedKWh = e.kWh !== undefined ? e.kWh : usage?.kWh
1290
+ const kWhBasis = e.kWh !== undefined ? 'demand-window' : usage ? 'usage-period' : undefined
791
1291
  /* 분모는 **완료 오더**다(대당 전력). 창에 완료가 없으면 커널 규칙이 「정의되지 않음」으로 답한다. */
792
1292
  const i = energyIntensity({
793
- kWh: e.kWh,
1293
+ kWh: usedKWh,
794
1294
  energyWindow: e.window,
795
1295
  denominator: { kind: 'output', value: completedOrders, window: e.window }
796
1296
  })
1297
+ /*
1298
+ * 요금은 **구간마다 제 단가로** 매긴 값이 있으면 그것을 쓴다. 단일 단가로 접으면 시간대마다
1299
+ * 다른 단가가 사라지고, 「피크를 깎아 얼마를 아끼나」가 그 차이 위에 선다.
1300
+ */
1301
+ /* 요금적용전력은 청구서가 먼저다 — 선언은 청구가 오지 않는 현장의 몫이다. */
1302
+ /* 이 창의 최고 수요 — 사용 구간이 실어 온 값 중 가장 큰 것(원본 계량기가 잰 값이다). */
1303
+ const peaks = usagePeriods
1304
+ .filter(u => u.startMs >= window.fromMs && u.endMs <= window.toMs && Number.isFinite(u.maxKW))
1305
+ .map(u => ({ kW: u.maxKW as number, toMs: u.endMs }))
1306
+ const observedPeak = peaks.length ? peaks.reduce((a, x) => (x.kW > a.kW ? x : a)) : undefined
1307
+ const billingDemand = billingDemandOf(
1308
+ bills.filter(b => Number.isFinite(b.billingDemandKW)).map(b => ({ endMs: b.endMs, billingDemandKW: b.billingDemandKW })),
1309
+ options.tariffBasis,
1310
+ options.lastBill,
1311
+ options.tariff?.billingDemandKW,
1312
+ observedPeak
1313
+ )
1314
+ const cost = options.tariff
1315
+ ? electricityCost({
1316
+ kWh: usedKWh,
1317
+ peakKW: e.peakKW,
1318
+ tariff: { ...options.tariff, ...(billingDemand ? { billingDemandKW: billingDemand.kW } : {}) },
1319
+ ...(usage?.charge !== undefined ? { energyChargeFromPeriods: usage.charge } : {})
1320
+ })
1321
+ : undefined
797
1322
  return {
798
1323
  energy: {
799
- ...(e.kWh !== undefined ? { kWh: e.kWh } : {}),
1324
+ ...(usedKWh !== undefined ? { kWh: usedKWh } : {}),
1325
+ ...(kWhBasis ? { kWhBasis } : {}),
1326
+ ...(billingDemand ? { billingDemand } : {}),
1327
+ ...(usage ? { usage } : {}),
800
1328
  /* 이 창의 최대수요 — 계약 대비 판정이 보는 수다(전력량과 같은 구간 집합에서 왔다). */
801
1329
  ...(e.peakKW !== undefined ? { peakKW: e.peakKW, peakStartMs: e.peakStartMs } : {}),
802
1330
  /* 깎인 양은 **같은 구간의 두 수**의 차이다 — 여기서 한 번만 계산해 두 곳이 갈라지지 않게. */
@@ -813,7 +1341,30 @@ export function foldKpi(events: KpiEvent[], window: KpiWindow, options: KpiFoldO
813
1341
  ...(measuredFeeders(demandWindows, { startMs: window.fromMs, endMs: window.toMs }, e.kWh, options.meterFeeder) ??
814
1342
  feederShares(e.kWh, utilization, options.resourceFeeder)),
815
1343
  /* 금액은 같은 창의 전력량·최대수요에서 나온다 — 창이 하나뿐이라 어긋날 수 없다. */
816
- ...(options.tariff ? { cost: electricityCost({ kWh: e.kWh, peakKW: e.peakKW, tariff: options.tariff }) } : {})
1344
+ ...(cost ? { cost } : {}),
1345
+ ...generatedOfPeriods(
1346
+ generationPeriods,
1347
+ { startMs: window.fromMs, endMs: window.toMs },
1348
+ options.generationRated,
1349
+ /* 창 안의 단가 사실이 먼저이고, 없으면 이어받은 마지막 단가를 쓴다. */
1350
+ generationPrices.length
1351
+ ? generationPrices
1352
+ : options.generationPrice
1353
+ ? [
1354
+ {
1355
+ startMs: Date.parse(options.generationPrice.from),
1356
+ endMs: Date.parse(options.generationPrice.to),
1357
+ unitPrice: options.generationPrice.unitPrice,
1358
+ currency: options.generationPrice.currency
1359
+ }
1360
+ ]
1361
+ : []
1362
+ ),
1363
+ /* 청구서는 사실이다 — 우리 계산을 덮지 않고 나란히 둔다(§`billOfWindow`). */
1364
+ ...(() => {
1365
+ const b = billOfWindow(bills, { startMs: window.fromMs, endMs: window.toMs }, cost as any)
1366
+ return b ? { bill: b } : {}
1367
+ })()
817
1368
  }
818
1369
  }
819
1370
  })()
@@ -876,7 +1427,7 @@ export function crossGroups(
876
1427
 
877
1428
  /* ── 시간 비교 ──────────────────────────────────────────────────────────────
878
1429
  * 숫자 하나로는 좋아졌는지 나빠졌는지 알 수 없다("29초" 가 개선인지 악화인지). 같은 길이의 창을 옮겨
879
- * 한 번 더 접으면 되므로(폴드는 순수) 값싸다. 창 옮기기와 차이 계산은 계산 규칙이라 여기 있다 —
1430
+ * 한 번 더 계산하면 되므로(폴드는 순수) 값싸다. 창 옮기기와 차이 계산은 계산 규칙이라 여기 있다 —
880
1431
  * 조회 층에 두면 테스트가 DB 를 끌고 와야 한다.
881
1432
  */
882
1433
 
@@ -913,7 +1464,7 @@ export interface KpiBucket {
913
1464
  }
914
1465
 
915
1466
  /**
916
- * 창을 N 등분해 구간별로 접는다 — **같은 이벤트를 창만 바꿔** 다시 접는다(조회 추가 없음).
1467
+ * 창을 N 등분해 구간별로 처리한다 — **같은 이벤트를 창만 바꿔** 다시 계산한다(조회 추가 없음).
917
1468
  *
918
1469
  * 마지막 구간은 나머지를 흡수한다(부동소수 나눗셈으로 끝이 밀리면 마지막 완료가 빠진다).
919
1470
  * `count <= 1` 이면 쪼갤 것이 없으므로 빈 배열 — 호출부가 "추세 없음" 으로 다룬다.