@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,9 +1,9 @@
1
1
  /*
2
- * 업무 KPI 폴드 검증 — 저널을 접어 시간창 지표를 만드는 규칙을 고정한다.
2
+ * 업무 KPI 폴드 검증 — 저널을 계산해 시간창 지표를 만드는 규칙을 고정한다.
3
3
  *
4
4
  * 무엇을 지키는가:
5
5
  * ① **성과는 완료 시점으로 센다** — 시작 시점으로 세면 진행 중인 것이 섞여 처리량이 부풀려진다.
6
- * ② **셈에서 빠진 것을 밝힌다**(unpaired) — 시작 이벤트를 못 찾은 완료를 조용히 버리면 평균이 거짓이 된다.
6
+ * ② **셈에서 빠진 것을 밝힌다**(unpaired) — 시작 이벤트를 못 찾은 완료를 알리지 않고 버리면 평균이 거짓이 된다.
7
7
  * ③ **없는 것을 0 으로 꾸미지 않는다** — 저널이 비었으면 eventsSeen 0 으로 그 사실이 보인다.
8
8
  * ④ 시간축은 eventTime 차분 — sim 클록 오프셋이 있어도 차분은 정확하다.
9
9
  */
@@ -82,7 +82,7 @@ test('분위수: p50·p90 을 낸다(작은 표본에서 과장하지 않는다)
82
82
  assert.equal(r.workTime.p90Ms, 10000)
83
83
  })
84
84
 
85
- test('시작 이벤트를 못 찾은 완료는 unpaired 로 밝힌다 — 평균에서 조용히 빠지지 않게', () => {
85
+ test('시작 이벤트를 못 찾은 완료는 unpaired 로 밝힌다 — 평균에서 오류 없이 빠지지 않게', () => {
86
86
  const r = foldKpi([task('t1', 'completed', 30)], WINDOW)
87
87
  assert.equal(r.throughput.tasks, 1, '완료는 센다')
88
88
  assert.equal(r.leadTime.count, 0, '리드타임은 계산할 수 없다')
@@ -159,7 +159,7 @@ test('오더 완료: 양이 답한다 — 낱말이 진행 중이라 말해도',
159
159
  test('오더 완료: 같은 오더를 한 번만 센다 — 폴링 원본이 다시 보내도', () => {
160
160
  /*
161
161
  * 폴링하는 원본은 같은 오더의 종결을 여러 주기에 걸쳐 다시 보낼 수 있다(결함이 아니라 폴링의 성질).
162
- * 사건마다 세면 처리량이 조용히 부풀려진다 — 작업 쪽은 커널의 짝맞춤이 이미 그 규율을 갖고 있었고
162
+ * 사건마다 세면 처리량이 알리지 않고 부풀려진다 — 작업 쪽은 커널의 짝맞춤이 이미 그 규율을 갖고 있었고
163
163
  * 오더만 없었다.
164
164
  */
165
165
  const r = foldKpi([order('o1', 'completed', 10), order('o1', 'completed', 20), order('o2', 'completed', 30)], WINDOW)
@@ -233,7 +233,7 @@ test('같은 전이가 두 번 와도 착수는 처음 것, 생성은 마지막
233
233
  })
234
234
 
235
235
  /* ── 구간 쪼개기(추세) ─────────────────────────────────────────────────────
236
- * 화면의 추세선이 여기서 나온다. 조회는 한 번이고 창만 바꿔 다시 접는다.
236
+ * 화면의 추세선이 여기서 나온다. 조회는 한 번이고 창만 바꿔 다시 계산한다.
237
237
  */
238
238
  test('구간 쪼개기: N 등분해 구간별 처리량을 낸다', () => {
239
239
  /* 1시간 창을 4등분(15분씩). 각 구간에 한 건씩 완료되게 놓는다. */
@@ -336,7 +336,7 @@ test('축의 합이 전체 처리량과 같다 — 어느 축에서도 건이
336
336
  }
337
337
  })
338
338
 
339
- test('축의 값이 없던 건은 unknown 으로 모인다 — 조용히 버리지 않는다', () => {
339
+ test('축의 값이 없던 건은 unknown 으로 모인다 — 알리지 않고 버리지 않는다', () => {
340
340
  const events = [...three('a', 100, { kind: 'move', resourceRef: 'fk-1' }), ...three('b', 200, { kind: 'move' })]
341
341
  const r = foldKpi(events, WINDOW, { groupBy: 'resource' })
342
342
  assert.deepEqual(r.groups?.items.map(g => g.key).sort(), ['fk-1', 'unknown'])
@@ -374,7 +374,7 @@ test('구역 축은 노드→구역 지도를 받아야 낸다 — 지도가 없
374
374
  assert.deepEqual(mapped.groups?.items.map(g => [g.key, g.tasks]), [['zone-a', 2]], '두 노드가 한 구역으로 모인다')
375
375
  })
376
376
 
377
- test('축이 많으면 잘라내고 잘린 축의 수와 그 안의 건수를 함께 알린다 — 조용한 절단 금지', () => {
377
+ test('축이 많으면 잘라내고 잘린 축의 수와 그 안의 건수를 함께 알린다 — 들어온 것이 없는 절단 금지', () => {
378
378
  /* 축마다 건수를 다르게 둔다: 축의 수만 알리면 "그 외 20개" 가 20건인지 60건인지 알 수 없다. */
379
379
  const events: KpiEvent[] = []
380
380
  let t = 0
@@ -403,7 +403,7 @@ test('짝을 못 찾은 건도 축에서 센다 — 건수는 세고 소요만
403
403
  })
404
404
 
405
405
  /* ── 시간 비교 ──────────────────────────────────────────────────────────────
406
- * "29초" 만으로는 좋아졌는지 나빠졌는지 알 수 없다. 창을 옮겨 한 번 더 접는 규칙을 고정한다.
406
+ * "29초" 만으로는 좋아졌는지 나빠졌는지 알 수 없다. 창을 옮겨 한 번 더 계산 규칙을 고정한다.
407
407
  */
408
408
  test('직전 구간: 같은 길이로 바로 앞 — 경계가 맞물린다(겹치거나 벌어지지 않게)', () => {
409
409
  const w = shiftWindow(WINDOW.fromMs, WINDOW.toMs, 'previous')
@@ -431,7 +431,7 @@ test('중앙값 차이: 한쪽이 결측이면 차이는 없다 — 0 으로 내
431
431
  assert.equal(deltaP50(missing, measured), undefined)
432
432
  })
433
433
 
434
- test('비교: 옮긴 창으로 접으면 앞 구간의 성과가 나온다 — 같은 폴드, 창만 다르다', () => {
434
+ test('비교: 옮긴 창으로 계산하면 앞 구간의 성과가 나온다 — 같은 폴드, 창만 다르다', () => {
435
435
  /* 현재 구간에 2건, 직전 구간에 1건. */
436
436
  const events = [
437
437
  ...three('now-1', 100, { kind: 'move' }),
@@ -500,7 +500,7 @@ test('교차: 지금만 있는 축은 isNew — delta 를 건수와 같게 적
500
500
  assert.equal(nw.prevTasks, undefined)
501
501
  })
502
502
 
503
- test('교차: 사라진 축을 따로 알린다 — 멈춘 구역이 표에서 조용히 빠지지 않게', () => {
503
+ test('교차: 사라진 축을 따로 알린다 — 멈춘 구역이 표에서 오류 없이 빠지지 않게', () => {
504
504
  const c = crossGroups([g('pick', 5)], [g('pick', 5), g('putaway', 12)])
505
505
  assert.deepEqual(c.disappeared, [{ key: 'putaway', tasks: 12 }])
506
506
  })
@@ -547,7 +547,7 @@ test('교대별로 쪼갠다 — 완료 시각에서 계산한다(저널에 없
547
547
  assert.equal(r.groups?.items.reduce((s, g) => s + g.tasks, 0), r.throughput.tasks, '축의 합이 전체와 같다')
548
548
  })
549
549
 
550
- test('현장의 시각 기준으로 읽는다 — UTC 로 조용히 가정하지 않는다', () => {
550
+ test('현장의 시각 기준으로 읽는다 — UTC 로 알리지 않고 가정하지 않는다', () => {
551
551
  const events = [...doneAtHour('t1', 20)] // 절대 20시
552
552
  const utc = foldKpi(events, DAY, { groupBy: 'shift', shifts: SHIFTS_3 })
553
553
  const local = foldKpi(events, DAY, { groupBy: 'shift', shifts: SHIFTS_3, utcOffsetMinutes: -420 })
@@ -630,7 +630,7 @@ test('모르는 지표 이름은 짐작해 매핑하지 않는다', () => {
630
630
  /* ── 목표의 범위 (공간 vs 인스턴스) ──────────────────────────────────────────
631
631
  *
632
632
  * 이 규칙은 원래 DB 접근 뒤에 숨어 있어 **화면을 눌러 봐야만** 확인할 수 있었다. 로그인 뒤 화면을
633
- * 자동으로 검증할 방법이 없는 프로젝트라 그런 규칙은 조용히 미검증으로 쌓인다 — 그래서 밖으로 뺐다.
633
+ * 자동으로 검증할 방법이 없는 프로젝트라 그런 규칙은 알리지 않고 미검증으로 쌓인다 — 그래서 밖으로 뺐다.
634
634
  */
635
635
  const row = (metric: string, target: number, scope: { spaceId?: string; instanceId?: string }) => ({
636
636
  metric,
@@ -788,7 +788,7 @@ test('그 창에 완료가 없으면 원단위는 정의되지 않는다 — 전
788
788
  test('계측이 끊긴 구간은 세지 않고 몇 개를 못 셌는지 말한다', () => {
789
789
  const r = foldKpi([win(T, T + q, 400), win(T + q, T + 2 * q), orderDone(T + 60_000)], { fromMs: T, toMs: T + 2 * q })
790
790
  assert.equal(r.energy?.counted, 1)
791
- assert.equal(r.energy?.skipped, 1, '조용히 빼면 이 합이 그 창의 전부인 줄 읽는다')
791
+ assert.equal(r.energy?.skipped, 1, '알리지 않고 빼면 이 합이 그 창의 전부인 줄 읽는다')
792
792
  })
793
793
 
794
794
  test('창에 걸친 구간은 세지 않는다 — 부분을 비례로 자르면 추정이 하나 더 생긴다', () => {
@@ -1134,7 +1134,7 @@ test('피크 구간에 표본이 없던 분기는 피크 값을 만들지 않는
1134
1134
  * ── 완료로 읽지 못한 오더 상태 낱말을 **센다** (2026-08-23) ──────────────────
1135
1135
  *
1136
1136
  * 오더의 상태 어휘는 계약이 도메인에 넘긴 것이라(`picking`·`shipped`·`FINISHED`…) 이 폴드는 「완료로
1137
- * 읽히는 표현」을 동의어 목록으로 받는다. 그 대가는 **목록에 없는 낱말이 조용히 0 으로 세어진다**는 것이다.
1137
+ * 읽히는 표현」을 동의어 목록으로 받는다. 그 대가는 **목록에 없는 낱말이 알리지 않고 0 으로 세어진다**는 것이다.
1138
1138
  *
1139
1139
  * 실제로 그랬다: `finished` 가 빠져 있어 첫 실 시스템의 완료 오더가 전부 0 이었고, 사람이 화면을 보고
1140
1140
  * 의심해서야 드러났다. 목록을 넓히는 것으로는 다음 원본을 막지 못한다 — 그래서 **못 읽은 것을 센다.**
@@ -1174,3 +1174,255 @@ test('상태를 말하지 않은 사건은 여기 오지 않는다 — 그것은
1174
1174
  const r = foldKpi([{ eventType: 'order.status', eventTime: at(10), payload: { data: { orderId: 'o-1' } } }], WINDOW)
1175
1175
  assert.deepEqual(r.orderStatusNotCounted, {})
1176
1176
  })
1177
+
1178
+ /* ── 밖에서 마감되어 온 사용 구간과 청구서 (2026-08-29) ────────────────────
1179
+ *
1180
+ * ★ 계량 표본이 오지 않는 현장이 있다. 원본이 「이 구간에 X 썼다」를 이미 마감해 준다.
1181
+ * 그 값을 쓰지 않으면 그 현장의 화면은 영영 빈손이다.
1182
+ *
1183
+ * 두 회계를 **더하지 않는다** — 같은 전기를 두 번 센다.
1184
+ */
1185
+
1186
+ const usagePeriod = (kWh: number, fromSec: number, toSec: number, extra: Record<string, unknown> = {}): KpiEvent => ({
1187
+ eventType: 'energy.usage.period',
1188
+ eventTime: at(toSec),
1189
+ payload: { data: { meterId: 'incoming-1', from: at(fromSec), to: at(toSec), kWh, ...extra } }
1190
+ })
1191
+
1192
+ test('★ 계량 표본이 없어도 사용 구간으로 답한다 — 근거를 밝힌다', () => {
1193
+ const k = foldKpi([usagePeriod(474.48, 0, 1800, { basis: 'provider' }), usagePeriod(471.6, 1800, 3600, { basis: 'provider' })], WINDOW)
1194
+ assert.equal(k.energy?.kWh, 474.48 + 471.6)
1195
+ assert.equal(k.energy?.kWhBasis, 'usage-period')
1196
+ assert.equal(k.energy?.usage?.counted, 2)
1197
+ assert.deepEqual(k.energy?.usage?.byBasis, [{ basis: 'provider', counted: 2 }])
1198
+ })
1199
+
1200
+ test('창에 걸친 구간은 세지 않는다 — 반만 세면 합이 창을 넘는다', () => {
1201
+ const k = foldKpi([usagePeriod(100, -1800, 1800)], WINDOW)
1202
+ assert.equal(k.energy?.usage, undefined)
1203
+ })
1204
+
1205
+ test('★ 구간마다 제 단가로 매긴다 — 단일 단가로 접지 않는다', () => {
1206
+ /* 새벽 87.3 · 낮 222.3 — 하나로 접으면 「피크를 깎아 얼마를 아끼나」가 사라진다. */
1207
+ const k = foldKpi(
1208
+ [
1209
+ usagePeriod(100, 0, 1800, { unitPrice: 87.3, currency: 'KRW', basis: 'provider' }),
1210
+ usagePeriod(100, 1800, 3600, { unitPrice: 222.3, currency: 'KRW', basis: 'provider' })
1211
+ ],
1212
+ WINDOW,
1213
+ { tariff: { energyChargePerKWh: 140.2, currency: 'KRW' } } as any
1214
+ )
1215
+ assert.equal(k.energy?.usage?.charge, 100 * 87.3 + 100 * 222.3)
1216
+ assert.equal((k.energy?.cost as any)?.energyCharge, 100 * 87.3 + 100 * 222.3, '구간별 단가가 이긴다')
1217
+ assert.ok(!(k.energy?.cost as any)?.notModeled.includes('tou'))
1218
+ })
1219
+
1220
+ test('일부만 단가를 실었으면 요금을 만들지 않고 그 수를 낸다', () => {
1221
+ const k = foldKpi(
1222
+ [usagePeriod(100, 0, 1800, { unitPrice: 87.3, currency: 'KRW' }), usagePeriod(100, 1800, 3600)],
1223
+ WINDOW
1224
+ )
1225
+ assert.equal(k.energy?.usage?.charge, undefined, '있는 것만 더하면 그 합이 창의 요금인 척한다')
1226
+ assert.equal(k.energy?.usage?.unpriced, 1)
1227
+ })
1228
+
1229
+ test('★ 청구서는 사실로 나란히 둔다 — 창이 다르면 견주지 않는다', () => {
1230
+ const bill: KpiEvent = {
1231
+ eventType: 'energy.bill',
1232
+ eventTime: at(3600),
1233
+ payload: { data: { from: at(600), to: at(3000), total: 1_000_000, currency: 'KRW', billingDemandKW: 1650 } }
1234
+ }
1235
+ const k = foldKpi([bill], WINDOW)
1236
+ assert.equal(k.energy?.bill?.total, 1_000_000)
1237
+ assert.equal(k.energy?.bill?.billingDemandKW, 1650)
1238
+ assert.equal(k.energy?.bill?.matchesWindow, false, '한 달 청구서를 하루 창의 값과 빼면 아무것도 뜻하지 않는다')
1239
+ assert.equal(k.energy?.bill?.ourTotalMinusBilled, undefined)
1240
+ })
1241
+
1242
+ test('★ 창이 청구 주기와 같으면 우리 계산과의 차이를 낸다 — 0 이 아니면 우리가 틀렸다', () => {
1243
+ const bill: KpiEvent = {
1244
+ eventType: 'energy.bill',
1245
+ eventTime: at(3600),
1246
+ payload: { data: { from: at(0), to: at(3600), total: 100_000, currency: 'KRW' } }
1247
+ }
1248
+ const k = foldKpi([bill, usagePeriod(1_000, 0, 3600, { unitPrice: 87.3, currency: 'KRW' })], WINDOW, {
1249
+ tariff: { energyChargePerKWh: 87.3, currency: 'KRW' }
1250
+ } as any)
1251
+ assert.equal(k.energy?.bill?.matchesWindow, true)
1252
+ assert.equal(k.energy?.bill?.diff?.total, 1_000 * 87.3 - 100_000)
1253
+ })
1254
+
1255
+ test('★ 총액이 없는 청구서도 항목별로 견준다 — 실제 첫 청구서에 기본요금만 있었다', () => {
1256
+ const bill: KpiEvent = {
1257
+ eventType: 'energy.bill',
1258
+ eventTime: at(3600),
1259
+ payload: { data: { from: at(0), to: at(3600), demandCharge: 13_728_000, currency: 'KRW', billingDemandKW: 1650 } }
1260
+ }
1261
+ const k = foldKpi([bill], WINDOW, {
1262
+ tariff: { demandChargePerKW: 8_320, billingDemandKW: 1_650, currency: 'KRW' }
1263
+ } as any)
1264
+ assert.equal(k.energy?.bill?.diff?.demandCharge, 0, '요금적용전력으로 매기면 청구서와 같다')
1265
+ assert.equal(k.energy?.bill?.diff?.total, undefined, '한쪽에 없는 항목은 견주지 않는다')
1266
+ })
1267
+
1268
+ test('요금적용전력을 선언하지 않으면 기본요금이 어긋나는 것이 드러난다', () => {
1269
+ const bill: KpiEvent = {
1270
+ eventType: 'energy.bill',
1271
+ eventTime: at(3600),
1272
+ payload: { data: { from: at(0), to: at(3600), demandCharge: 13_728_000, currency: 'KRW' } }
1273
+ }
1274
+ /* 선언이 없으면 커널은 창의 피크로 조건부 값을 낸다 — 그 차이가 그대로 나온다. */
1275
+ const peakWindow: KpiEvent = {
1276
+ eventType: 'energy.demand.window',
1277
+ eventTime: at(1800),
1278
+ payload: { data: { startMs: T0 + 900_000, endMs: T0 + 1_800_000, maxKW: 600, meanKW: 600, samples: 4 } }
1279
+ }
1280
+ const k = foldKpi([bill, peakWindow], WINDOW, {
1281
+ tariff: { demandChargePerKW: 8_320, currency: 'KRW' }
1282
+ } as any)
1283
+ const d = k.energy?.bill?.diff?.demandCharge
1284
+ assert.ok(d !== undefined && d !== 0, `어긋남이 드러나야 한다: ${d}`)
1285
+ })
1286
+
1287
+ /* ── 요금적용전력은 청구서가 먼저다 (2026-08-29) ───────────────────────────
1288
+ *
1289
+ * ★ 그 값은 최근 몇 달의 최고 수요로 **매달 다시 정해진다.** 선언에 박아 두면 사람이 매달
1290
+ * 갱신해야 하고, 안 하면 낡은 값으로 기본요금이 조용히 틀린다.
1291
+ */
1292
+
1293
+ const billEvent = (fromSec: number, toSec: number, data: Record<string, unknown>): KpiEvent => ({
1294
+ eventType: 'energy.bill',
1295
+ eventTime: at(toSec),
1296
+ payload: { data: { from: at(fromSec), to: at(toSec), currency: 'KRW', ...data } }
1297
+ })
1298
+
1299
+ test('★ 창을 덮는 청구서가 있으면 그 값으로 매긴다', () => {
1300
+ const k = foldKpi([billEvent(0, 3600, { demandCharge: 1, billingDemandKW: 1650 })], WINDOW, {
1301
+ tariff: { demandChargePerKW: 8_320, billingDemandKW: 1200, currency: 'KRW' }
1302
+ } as any)
1303
+ assert.equal(k.energy?.billingDemand?.kW, 1650)
1304
+ assert.equal(k.energy?.billingDemand?.from, 'bill')
1305
+ assert.equal((k.energy?.cost as any)?.demandCharge, 1650 * 8_320, '선언 1200 이 아니라 청구서 1650 으로 매긴다')
1306
+ })
1307
+
1308
+ test('★ 선언이 청구서와 다르면 낡았다고 말한다 — 감추면 아무도 다시 읽지 않는다', () => {
1309
+ const k = foldKpi([billEvent(0, 3600, { demandCharge: 1, billingDemandKW: 1650 })], WINDOW, {
1310
+ tariff: { demandChargePerKW: 8_320, billingDemandKW: 1200, currency: 'KRW' }
1311
+ } as any)
1312
+ assert.equal(k.energy?.billingDemand?.declaredKW, 1200)
1313
+ assert.equal(k.energy?.billingDemand?.stale, true)
1314
+ })
1315
+
1316
+ test('선언과 청구서가 같으면 낡았다고 하지 않는다', () => {
1317
+ const k = foldKpi([billEvent(0, 3600, { demandCharge: 1, billingDemandKW: 1650 })], WINDOW, {
1318
+ tariff: { demandChargePerKW: 8_320, billingDemandKW: 1650, currency: 'KRW' }
1319
+ } as any)
1320
+ assert.equal(k.energy?.billingDemand?.stale, undefined)
1321
+ })
1322
+
1323
+ test('★ 창에 청구서가 없으면 마지막 청구서의 값을 이어 쓴다 — 요금 제도가 그렇게 군다', () => {
1324
+ const k = foldKpi([usagePeriod(100, 0, 3600, { unitPrice: 87.3, currency: 'KRW' })], WINDOW, {
1325
+ tariff: { demandChargePerKW: 8_320, currency: 'KRW' },
1326
+ lastBill: { to: at(-86400), billingDemandKW: 1650 }
1327
+ } as any)
1328
+ assert.equal(k.energy?.billingDemand?.kW, 1650)
1329
+ assert.equal(k.energy?.billingDemand?.from, 'carried-forward')
1330
+ assert.equal((k.energy?.cost as any)?.demandBasis, 'billing-demand')
1331
+ })
1332
+
1333
+ test('청구가 오지 않는 현장은 선언을 쓴다', () => {
1334
+ const k = foldKpi([usagePeriod(100, 0, 3600, { unitPrice: 87.3, currency: 'KRW' })], WINDOW, {
1335
+ tariff: { demandChargePerKW: 8_320, billingDemandKW: 900, currency: 'KRW' }
1336
+ } as any)
1337
+ assert.equal(k.energy?.billingDemand?.from, 'declaration')
1338
+ assert.equal(k.energy?.billingDemand?.kW, 900)
1339
+ assert.equal(k.energy?.billingDemand?.stale, undefined)
1340
+ })
1341
+
1342
+ test('셋 다 없으면 이 칸을 만들지 않는다 — 빈 값을 지어내지 않는다', () => {
1343
+ const k = foldKpi([usagePeriod(100, 0, 3600, { unitPrice: 87.3, currency: 'KRW' })], WINDOW, {
1344
+ tariff: { demandChargePerKW: 8_320, currency: 'KRW' }
1345
+ } as any)
1346
+ assert.equal(k.energy?.billingDemand, undefined)
1347
+ })
1348
+
1349
+ test('★ 관측한 최고가 청구 기준을 넘으면 얼마나 넘었는지와 언제까지의 값인지를 낸다', () => {
1350
+ /* 기본요금은 이 값으로 다시 정해진다 — 지금 줄여도 되돌아가지 않는다. */
1351
+ const k = foldKpi([usagePeriod(400, 0, 1800, { maxKW: 1200 }), usagePeriod(400, 1800, 3600, { maxKW: 1700 })], WINDOW, {
1352
+ tariff: { demandChargePerKW: 8_320, billingDemandKW: 1650, currency: 'KRW' }
1353
+ } as any)
1354
+ const e = k.energy?.billingDemand?.exceeded
1355
+ assert.equal(e?.peakKW, 1700)
1356
+ assert.equal(e?.overKW, 50)
1357
+ assert.equal(e?.observedTo, at(3600), '이 최고값이 언제까지의 것인지')
1358
+ })
1359
+
1360
+ test('넘지 않으면 그 칸을 만들지 않는다', () => {
1361
+ const k = foldKpi([usagePeriod(400, 0, 3600, { maxKW: 1200 })], WINDOW, {
1362
+ tariff: { demandChargePerKW: 8_320, billingDemandKW: 1650, currency: 'KRW' }
1363
+ } as any)
1364
+ assert.equal(k.energy?.billingDemand?.exceeded, undefined)
1365
+ })
1366
+
1367
+ test('청구서가 말한 기준으로 넘었는지 본다 — 선언이 아니다', () => {
1368
+ const k = foldKpi([usagePeriod(400, 0, 3600, { maxKW: 1300 })], WINDOW, {
1369
+ tariff: { demandChargePerKW: 8_320, billingDemandKW: 1650, currency: 'KRW' },
1370
+ lastBill: { to: at(-86400), billingDemandKW: 1200 }
1371
+ } as any)
1372
+ assert.equal(k.energy?.billingDemand?.kW, 1200)
1373
+ assert.equal(k.energy?.billingDemand?.exceeded?.overKW, 100)
1374
+ })
1375
+
1376
+ /* ── 이용률 (2026-08-30) ───────────────────────────────────────────────────
1377
+ *
1378
+ * ★ 분모가 어느 정격인지가 값의 뜻을 바꾼다. 붙어 있는 원본은 직류로 나눠 2.2% 를 보여 주는데,
1379
+ * 교류로 나누면 4.08% 다. 같은 발전소를 두 화면에서 보는 사람이 두 배 다른 수를 본다.
1380
+ *
1381
+ * 그래서 커널은 교류를 먼저 쓰되(계통으로 나가는 양은 교류 정격을 넘을 수 없다) **어느 쪽으로
1382
+ * 나눴는지를 값과 함께 낸다.**
1383
+ */
1384
+
1385
+ const generationPeriod = (kWh: number, fromSec: number, toSec: number): KpiEvent => ({
1386
+ eventType: 'energy.generation.period',
1387
+ eventTime: at(toSec),
1388
+ payload: {
1389
+ data: { equipmentId: 'inv-1', kWh, periodStart: at(fromSec), periodEnd: at(toSec), observedFrom: at(fromSec), observedTo: at(toSec) }
1390
+ }
1391
+ })
1392
+
1393
+ test('★ 이용률은 만든 양 ÷ (정격 × 그 기간의 시간)', () => {
1394
+ /* 한 시간에 63.8kWh · 정격 127.6kW → 50% */
1395
+ const k = foldKpi([generationPeriod(63.8, 0, 3600)], WINDOW, {
1396
+ generationRated: { kW: 127.6, basis: 'ac', from: 'location' }
1397
+ } as any)
1398
+ const cf = k.energy?.generated?.capacityFactor
1399
+ assert.equal(cf?.value, 0.5)
1400
+ assert.equal(cf?.ratedKW, 127.6)
1401
+ assert.equal(cf?.hours, 1)
1402
+ })
1403
+
1404
+ test('★ 어느 정격으로 나눴는지 함께 낸다 — 두 화면의 수가 달라도 이유를 말할 수 있게', () => {
1405
+ const k = foldKpi([generationPeriod(63.8, 0, 3600)], WINDOW, {
1406
+ generationRated: { kW: 234.35, basis: 'dc', from: 'location' }
1407
+ } as any)
1408
+ assert.equal(k.energy?.generated?.capacityFactor?.basis, 'dc')
1409
+ assert.equal(k.energy?.generated?.capacityFactor?.from, 'location')
1410
+ })
1411
+
1412
+ test('★ 창 길이가 아니라 센 기간들의 길이로 나눈다', () => {
1413
+ /*
1414
+ * 한 시간짜리 기간 하나만 센 창에서 창 길이(1시간)로 나누는 것과 같지만, 기간이 창보다 짧으면
1415
+ * 달라진다. 창 길이로 나누면 그 기간이 창 전체에 걸쳐 있었던 것처럼 작아진다.
1416
+ */
1417
+ const k = foldKpi([generationPeriod(63.8, 0, 1800)], WINDOW, {
1418
+ generationRated: { kW: 127.6, basis: 'ac', from: 'location' }
1419
+ } as any)
1420
+ assert.equal(k.energy?.generated?.capacityFactor?.hours, 0.5)
1421
+ assert.equal(k.energy?.generated?.capacityFactor?.value, 1, '30분에 정격만큼 만들었으면 100%다')
1422
+ })
1423
+
1424
+ test('정격이 없으면 이용률을 내지 않는다 — 0 이나 짐작을 만들지 않는다', () => {
1425
+ const k = foldKpi([generationPeriod(63.8, 0, 3600)], WINDOW)
1426
+ assert.equal(k.energy?.generated?.capacityFactor, undefined)
1427
+ assert.equal(k.energy?.generated?.kWh, 63.8, '발전량 자체는 그대로 낸다')
1428
+ })
@@ -7,7 +7,7 @@
7
7
  * · 브로드캐스팅 — 서버가 주기마다 전 단계를 계산해 밀어 준다. 자리가 트윈당 최대 200개고 지표가 6개라
8
8
  * 태그가 1,200개가 된다. **모델에 카드를 하나도 안 놓아도 계산은 돈다.**
9
9
  * · 질의 — 카드가 필요할 때 묻는다. **모델에 놓인 카드 수만큼만** 든다. 시각(`toTime`)도 물을 수 있어
10
- * 타임라인과 맞고, 공간 단위 합산(여러 트윈을 한 번에 접기)은 질의에만 있다.
10
+ * 타임라인과 맞고, 공간 단위 합산(여러 트윈을 한 번에 계산)은 질의에만 있다.
11
11
  *
12
12
  * 질의로 정하려면 **한 번의 비용**을 알아야 한다. 카드 20장이 30초마다 물으면 감당되는지가 결정을
13
13
  * 가른다. 짐작하지 않고 잰다.
@@ -82,7 +82,7 @@ before(async () => {
82
82
  const d = await ds.getRepository(Domain).save({ name: 'kpi-bench', subdomain: 'kpi-bench' } as any)
83
83
  domainId = d.id
84
84
 
85
- /* 자리 계층 — 자리 200개를 구역 4개에 나눠 붙인다(구역 단위 접기 비용도 재려면 부모가 있어야 한다). */
85
+ /* 자리 계층 — 자리 200개를 구역 4개에 나눠 붙인다(구역 단위 계산 비용도 재려면 부모가 있어야 한다). */
86
86
  const locations = Array.from({ length: LOCATIONS }, (_, i) => ({
87
87
  id: `loc-${i}`,
88
88
  type: 'storage',
@@ -125,6 +125,6 @@ test('bench: 구간 성과 폴드 한 번의 비용 vs 창 안의 작업 수', a
125
125
  console.log(` 작업 ${count.toLocaleString()}건(이벤트 ${(count * 2).toLocaleString()}줄) · 자리 ${LOCATIONS}개`)
126
126
  console.log(` 폴드 1회 ${plain.toFixed(1)}ms`)
127
127
  console.log(` 구역별 폴드 1회 ${byArea.toFixed(1)}ms`)
128
- console.log(` 카드 20장 동시 ${(plain * 20).toFixed(0)}ms (직렬 상한 — 실제로는 한 번 접어 나눠 쓸 수 있다)`)
128
+ console.log(` 카드 20장 동시 ${(plain * 20).toFixed(0)}ms (직렬 상한 — 실제로는 한 번 계산해 나눠 쓸 수 있다)`)
129
129
  }
130
130
  })
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * ★ 읽기 커서는 **부르는 쪽이 자리를 가리켜야** 이어진다 — 커넥터는 그 이름을 알 수 없다.
3
3
  *
4
- * ── 무엇이 조용히 끊겨 있었나 (2026-08-23) ──────────────────────────────────
4
+ * ── 무엇이 알리지 않고 끊겨 있었나 (2026-08-23) ──────────────────────────────────
5
5
  * 커서를 저장할 컬럼과 계약을 만들고 배선까지 했는데, **한 번도 저장되지 않았다.** 저장할 자리를
6
6
  * `cfg`·`site` 에서 찾으려 했기 때문이다.
7
7
  *
@@ -12,7 +12,7 @@
12
12
  * 원본은 그것을 모른다. 커넥터가 채울 수 없는 값을 커넥터에서 읽으려 한 것이 잘못이었다.
13
13
  *
14
14
  * 그리고 그 실패는 **로그로는 말했다** — 「this feed does not say which reference it reads from」.
15
- * 그 한 줄로 다른 레인이 바로 짚었다(조용히 넘기지 않은 것이 값을 했다). 그러나 로그는 사람이 볼 때만
15
+ * 그 한 줄로 다른 레인이 바로 짚었다(알리지 않고 넘기지 않은 것이 값을 했다). 그러나 로그는 사람이 볼 때만
16
16
  * 값이 있다. 배선이 다시 끊기면 이 시험이 먼저 말한다.
17
17
  *
18
18
  * ── 왜 「늘었나」로 확인하면 안 되나 ────────────────────────────────────────
@@ -44,10 +44,10 @@ test('커서를 저장할 자리는 부르는 쪽이 가리킨다 — cfg·site
44
44
  assert.match(code, /startReferenceLiveFeed/, '대상 함수가 남아 있다')
45
45
  })
46
46
 
47
- test('모든 호출처가 그 이름을 넘긴다 — 한 곳만 빠져도 그 경로는 조용히 창이 미끄러진다', () => {
47
+ test('모든 호출처가 그 이름을 넘긴다 — 한 곳만 빠져도 그 경로는 알리지 않고 창이 미끄러진다', () => {
48
48
  /*
49
49
  * 호출처가 둘이고 **둘 다** `TwinReference` 를 들고 있다(부팅 재개 · 인제스트 직후 기동).
50
- * 하나만 넘기면 그 경로로 세워진 트윈만 커서를 잃고, 그 차이는 오류 없이 조용하다.
50
+ * 하나만 넘기면 그 경로로 세워진 트윈만 커서를 잃고, 그 차이는 오류 없이 들어온 것이 없다.
51
51
  */
52
52
  const calls = [...codeOf(LIVE).matchAll(/startReferenceLiveFeed\(([^)]*)\)/g)].map(m => m[1])
53
53
  const resolverCalls = [...codeOf(RESOLVER).matchAll(/startReferenceLiveFeed\([\s\S]{0,200}?\)/g)].map(m => m[0])
@@ -58,7 +58,7 @@ test('모든 호출처가 그 이름을 넘긴다 — 한 곳만 빠져도 그
58
58
  }
59
59
  })
60
60
 
61
- test('이름이 없으면 조용히 돌지 않는다 — 말한다', () => {
61
+ test('이름이 없으면 알리지 않고 돌지 않는다 — 말한다', () => {
62
62
  /*
63
63
  * 커서 없이 도는 것은 「창이 매번 미끄러진다」는 뜻이고, 그 손실은 오류 없이 사라진다. 그래서 그
64
64
  * 사실을 로그로 말한다 — 실제로 그 한 줄이 이 결함을 찾아냈다.
@@ -82,6 +82,6 @@ test('커서는 레퍼런스의 것이다 — 인스턴스마다 두지 않는
82
82
  })
83
83
 
84
84
  test('저장 실패를 삼키지 않는다', () => {
85
- /* 조용히 실패하면 다음 재기동에서 창이 다시 미끄러지고, 이 컬럼을 만든 이유가 사라진다. */
85
+ /* 오류 없이 실패하면 다음 재기동에서 창이 다시 미끄러지고, 이 컬럼을 만든 이유가 사라진다. */
86
86
  assert.match(codeOf(LIVE), /live cursor not saved/, '저장 실패를 말한다')
87
87
  })
@@ -65,7 +65,7 @@ test('커널이 낸 사실도 브로드캐스팅을 예약한다 — 피크·마
65
65
 
66
66
  /*
67
67
  * ── 구조 전환도 브로드캐스팅한다 (2026-08-18) ───────────────────────────────────────
68
- * 화면이 보는 것 상당수가 구조에서 파생된다(계약 대비 판정·주목 신호·자리 색). 구조 전환이 조용히
68
+ * 화면이 보는 것 상당수가 구조에서 파생된다(계약 대비 판정·주목 신호·자리 색). 구조 전환이 알리지 않고
69
69
  * 끝나면 지도 레일은 다음 계측 표본이 올 때까지 옛 화면을 들고 있고, 헤더 배지는 폴링으로 먼저 알아
70
70
  * 「배지엔 있고 목록엔 없는」 어긋난 화면이 된다(실화면에서 봤다).
71
71
  */
@@ -101,18 +101,18 @@ test('과거 화면의 「지금」은 잘라 낸 뒤의 마지막 사실이다
101
101
  const body = methodOf('recover')
102
102
  /* 예전에는 전량(rows)의 마지막 시각을 썼다 — 물어본 시점보다 뒤의 이벤트라 과거 화면이 미래를
103
103
  기준으로 「늦었다」를 판정할 수 있었다. 이제 rows 는 이미 잘린 것이다. */
104
- assert.match(body, /const wanted = rows/, '자른 것과 접는 것이 같아야 한다')
104
+ assert.match(body, /const wanted = rows/, '자른 것과 계산하는 것이 같아야 한다')
105
105
  })
106
106
 
107
107
  /*
108
- * ── 재개점에서 이어 접는다 (2026-08-18) ──────────────────────────────────────
109
- * 접은 결과는 이미 남기고 있었지만, 그 지점의 **재개점**(리듀서 내부 상태 전부 + 가동 누적기)이 없어서
110
- * 이벤트가 하나만 늘어도 0부터 다시 접어야 했다. 동치는 커널 시험이 증명한다(0부터 == 재개점 + 꼬리).
111
- * 여기서는 **호스트가 그 씨앗을 실제로 쓰는지**를 지킨다 — 배선이 사라지면 지름길은 조용히 사라진다.
108
+ * ── 재개점에서 이어서 계산한다 (2026-08-18) ──────────────────────────────────────
109
+ * 계산한 결과는 이미 남기고 있었지만, 그 지점의 **재개점**(리듀서 내부 상태 전부 + 가동 누적기)이 없어서
110
+ * 이벤트가 하나만 늘어도 0부터 다시 계산해야 했다. 동치는 커널 시험이 증명한다(0부터 == 재개점 + 꼬리).
111
+ * 여기서는 **호스트가 그 씨앗을 실제로 쓰는지**를 지킨다 — 배선이 사라지면 지름길은 오류 없이 사라진다.
112
112
  */
113
113
  test('폴드가 재개점을 쓰고, 새 재개점을 함께 남긴다', () => {
114
114
  const body = methodOf('recover')
115
- assert.match(body, /replayFrom\(model, resume\.fold\.reducer/, '씨앗이 있으면 꼬리만 접는다')
115
+ assert.match(body, /replayFrom\(model, resume\.fold\.reducer/, '씨앗이 있으면 꼬리만 계산한다')
116
116
  assert.match(body, /revision: MoreThan\(from\)/, '읽는 것도 그 뒤만')
117
117
  assert.match(body, /oee\.restore\(resume\.fold\.oee\)/, '가동 누적기도 이어받는다(없으면 가용률이 작아진다)')
118
118
  assert.match(body, /replayWithCheckpoint\(/, '씨앗이 없을 때도 새 재개점을 만든다')
@@ -122,7 +122,7 @@ test('폴드가 재개점을 쓰고, 새 재개점을 함께 남긴다', () => {
122
122
  test('씨앗을 좁게 쓴다 — 캐시가 사실을 이기지 않는다', () => {
123
123
  const body = methodOf('recover')
124
124
  /* 지금을 물었을 때만·구조가 그대로일 때만. 시간여행은 목표 이전 재개점이 필요하고(사슬은 다음 단계),
125
- 구조가 바뀌면 그 경계에서 갈라 접어야 한다. */
125
+ 구조가 바뀌면 그 경계에서 갈라 계산해야 한다. */
126
126
  assert.match(body, /asOfNowRead && tip && cachedForResume && cachedForResume\.fold/, '지금 읽기에만 쓴다')
127
127
  assert.match(body, /\(cachedForResume\.structureRev \?\? null\) === tip\.structureRev/, '구조가 같을 때만 쓴다')
128
128
  assert.match(body, /segments\.length <= 1/, '구조가 갈린 폴드에는 쓰지 않는다')
@@ -131,10 +131,10 @@ test('씨앗을 좁게 쓴다 — 캐시가 사실을 이기지 않는다', () =
131
131
  /*
132
132
  * ── 재개점 **사슬** (2026-08-18) ─────────────────────────────────────────────
133
133
  * 최신 재개점 하나로는 과거를 도울 수 없다 — 그것은 언제나 목표보다 뒤에 있다. 그래서 지점을 여러 개
134
- * 남기고, 과거를 물으면 **목표 직전** 지점에서 접는다. 지점은 각각 상태 전체를 들고 있어 무겁기 때문에
135
- * 눈금(리비전 간격)을 넘을 때만 남기고 최근 몇 개만 든다 — 더 과거는 0부터 접는다(알려진 한계).
134
+ * 남기고, 과거를 물으면 **목표 직전** 지점에서 계산한다. 지점은 각각 상태 전체를 들고 있어 무겁기 때문에
135
+ * 눈금(리비전 간격)을 넘을 때만 남기고 최근 몇 개만 든다 — 더 과거는 0부터 계산한다(알려진 한계).
136
136
  */
137
- test('과거 읽기는 목표 직전 지점에서 접는다 — 사슬을 쓴다', () => {
137
+ test('과거 읽기는 목표 직전 지점에서 계산한다 — 사슬을 쓴다', () => {
138
138
  const body = methodOf('recover')
139
139
  assert.match(body, /this\.chainSeedFor\(domainId, instanceId, \{/, '과거 씨앗을 사슬에서 고른다')
140
140
  assert.match(body, /structureCount <= 1/, '구조가 갈린 트윈에서는 쓰지 않는다')
@@ -11,7 +11,9 @@
11
11
  import { test } from 'node:test'
12
12
  import assert from 'node:assert/strict'
13
13
 
14
- import { WmsKernel, StateProjector, deriveAttentions, computeOee } from '@operato/twin-kernel'
14
+ import { WmsKernel, StateProjector, deriveAttentions } from '@operato/twin-kernel'
15
+ /* OEE 공식은 계약에 있다 — 시뮬·미러·MES 셋이 같은 것을 부른다. */
16
+ import { computeOee } from '@operato/ops-contract'
15
17
  import { masterToTwin } from '../server/service/reference/reference-master.ts'
16
18
  import { buildEntityDeltas } from '../shared/entity-delta.ts'
17
19
  import { withLiveAttentions } from '../server/engine/live-attentions.ts'
@@ -209,12 +211,13 @@ test('★ OEE derive: 공유 공식 computeOee 가 공급된 카운터로 OEE
209
211
  * 없앴다. 이제 창을 모르면 잰 시간이 없으므로, 재생산에는 창이 함께 와야 한다. 라이브 공급자가
210
212
  * 실제로 하는 일이 그것이다(누적기는 첫 이벤트 시각을 창으로 낸다).
211
213
  */
212
- const planned = o.runMs + o.setupMs + o.downMs + o.idleMs
214
+ const planned = o.runMs + o.setupMs + o.downMs + o.delayMs
213
215
  const derived = computeOee(
214
216
  { runMs: o.runMs, setupMs: o.setupMs, downMs: o.downMs, goodCount: o.goodCount, scrapCount: o.scrapCount, metricsSinceMs: 0 },
215
217
  planned
216
218
  )
217
- const near = (a: number, b: number) => Math.abs(a - b) < 1e-6
219
+ /* 세 비율은 근거가 없으면 없다 — 양쪽 다 없는 것도 재생산이다(계약 0.5.0). */
220
+ const near = (a?: number, b?: number) => (a == null && b == null) || (a != null && b != null && Math.abs(a - b) < 1e-6)
218
221
  assert.ok(near(derived.availability, o.availability), `availability 재생산 (${derived.availability} vs ${o.availability})`)
219
222
  assert.ok(near(derived.performance, o.performance), 'performance 재생산')
220
223
  assert.ok(near(derived.quality, o.quality), 'quality 재생산')
@@ -247,7 +250,7 @@ test('★ withLiveAttentions 는 채널을 하나도 떨어뜨리지 않는다(
247
250
 
248
251
  /*
249
252
  * ── 되짚은 상태에도 신호가 있어야 한다 (2026-08-05) ────────────────────────
250
- * 라이브·시뮬은 커널이 신호를 스스로 내지만, 저널을 접는 경로는 프로젝터 상태만 낸다 — 신호 키가
253
+ * 라이브·시뮬은 커널이 신호를 스스로 내지만, 저널을 계산하는 경로는 프로젝터 상태만 낸다 — 신호 키가
251
254
  * 아예 없었다. 지도는 `snap.attentions` 를 읽으므로 **과거를 되짚으면 주의 레일이 텅 비었고**,
252
255
  * 기동돼 있지 않은 트윈을 보는 화면도 같았다. 실 저널로 재 보니 `rosarito-wms` 는 246건이 비어 있었다.
253
256
  *
@@ -260,7 +263,7 @@ test('★ 프로젝터 폴드 상태에는 신호 키가 없다 — 보강이
260
263
  const proj = new StateProjector(model)
261
264
  for (const env of events) proj.apply(env)
262
265
  const raw: any = proj.snapshot()
263
- assert.equal(raw.attentions, undefined, '접은 상태는 신호를 스스로 내지 않는다(커널이 아니다)')
266
+ assert.equal(raw.attentions, undefined, '계산한 상태는 신호를 스스로 내지 않는다(커널이 아니다)')
264
267
  })
265
268
 
266
269
  test('★ 보강하면 다시 계산한 상태에도 신호가 있다 — 지도의 주의 레일이 비지 않게', () => {
@@ -274,6 +277,6 @@ test('★ 보강하면 다시 계산한 상태에도 신호가 있다 — 지도
274
277
  tasks: []
275
278
  }
276
279
  const out = withLiveAttentions(st)
277
- assert.ok((out.attentions ?? []).length > 0, '접은 상태에서도 규칙이 신호를 낸다')
280
+ assert.ok((out.attentions ?? []).length > 0, '계산한 상태에서도 규칙이 신호를 낸다')
278
281
  assert.equal(out.attentions![0].kind, 'bottleneck')
279
282
  })
@@ -84,7 +84,7 @@ test('경고는 쿨다운을 둔다 — 느린 트윈이 매 틱 경고하면
84
84
  const slow = 900
85
85
 
86
86
  assert.equal(judgeCycle(m, slow, TICK, 1000).warn, true, '처음엔 말한다')
87
- assert.equal(judgeCycle(m, slow, TICK, 2000).warn, false, '쿨다운 안에는 조용히')
87
+ assert.equal(judgeCycle(m, slow, TICK, 2000).warn, false, '쿨다운 안에는 알리지 않고')
88
88
  assert.equal(judgeCycle(m, slow, TICK, 1000 + WARN_COOLDOWN_MS).warn, true, '쿨다운이 지나면 다시')
89
89
 
90
90
  assert.equal(m.overBudget, 3, '말하지 않아도 세기는 한다')