@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 +1 @@
1
- {"version":3,"file":"canonical-ingest.js","sourceRoot":"","sources":["../../server/engine/canonical-ingest.ts"],"names":[],"mappings":"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","sourcesContent":["import { ingest, ingestEnergyRecords, ingestEnergyEquipmentRecords, ingestOperationalRecords, isAggregationRecord, isTransformationRecord,\n isEnergyRecord, isEnergyEquipmentRecord, isOperationalRecord, type AdapterRule, type EnergyRecord,\n type EnergyEquipmentRecord, type OperationalRecord, type IngestResult } from '@operato/twin-kernel'\n\n/*\n * 정규 레코드 → 커널 검증 ACL → CanonicalEnvelope (P1 seam 코어).\n * 순수(커널만 의존; shell·integration-base 무의존 → node:test 검증 가능).\n * 매핑(레거시 스키마 → 정규)은 이 앞 단계(jsonata step 또는 커넥터 openLiveFeed)가 끝낸다.\n * 여기 도착하는 레코드는 이미 정규 필드다 — 어휘마다 자기 필드 이름이 계약이다:\n * EPCIS 품목 { epc, action, bizStep, disposition?, readPoint?, bizLocation? }\n * 에너지 계량 { meterId, kW?, kWh?, … } · 설비 에너지 { equipmentId, generatedKW?, soc?, … }\n * 담김(적재) { parentID, childEPCs[] | childQuantityList[], action, bizStep, … }\n * 운영 사실 { taskId | moverId | personId | assetId | orderId, status, … } // vocabulary-guard: allow 저널 와이어 필드\n * 커널은 objectEvent 구성 + EPCIS 검증(위반=오염 차단) + 봉투 래핑만(방언 없음).\n *\n * 두 소비처가 이 단일 코어를 공유(중복 금지):\n * - sim/scenario 경로: operato-twin twin-ingest 태스크(http-get→jsonata→여기).\n * - live 커넥터 경로: reference-live(어댑터 openLiveFeed→여기).\n * SoT: design/integration/face2-adapters.md §5(seam)·§7(매핑=밖, 검증=커널).\n */\n\n/** 고정 정규 매핑(방언 아님 — 필드명 자체가 정규 계약). `sourceType` 으로 어느 품목 사실인지 고른다. */\nconst TWIN_INGEST_RULES: AdapterRule[] = [\n {\n sourceType: 'twin',\n mapping: {\n type: 'ObjectEvent',\n action: '$.action',\n bizStep: '$.bizStep',\n disposition: '$.disposition',\n epc: '$.epc',\n /* 비직렬 수량 — 개체 없이 「이 자리에 이 품목이 얼마 남았다」만 오는 관측이 있다. */\n quantityList: '$.quantityList',\n /*\n * **개체·로트의 마스터데이터**(로트 번호·소비기한 등) — 커널 0.7.58 에서 이 자리가 열렸다.\n *\n * 실측(포천): 원본 재고 1,704행 **전부**에 소비기한이 있는데 투영된 상태에는 **0건**이었다. 지난\n * 재고 950건·59,785kg 이 어디에도 나타나지 않았고, 빈 화면이 「이상 없음」으로 읽혔다.\n *\n * ★ **표준이 `action: 'ADD'` 일 때만 허용한다**(또는 변환). 마스터데이터는 로트가 **생길 때**\n * 말하는 사실이고, 주기 관측마다 실으면 커널이 거부한다 — 조용히 통과시키지 않는 것이 옳다\n * (커넥터가 첫 목격에 `ADD`, 그 뒤로 `OBSERVE` 를 내야 한다).\n */\n ilmd: '$.ilmd',\n readPoint: '$.readPoint',\n bizLocation: '$.bizLocation'\n }\n },\n {\n /* **변환의 사실** — 무엇으로 무엇이 되었나. 이름은 표준의 것을 그대로 쓴다(갈래 판정이 그 이름을 본다). */\n sourceType: 'twin-transformation',\n mapping: {\n type: 'TransformationEvent',\n bizStep: '$.bizStep',\n disposition: '$.disposition',\n inputEPCList: '$.inputEPCList',\n inputQuantityList: '$.inputQuantityList',\n outputEPCList: '$.outputEPCList',\n outputQuantityList: '$.outputQuantityList',\n transformationID: '$.transformationID',\n readPoint: '$.readPoint',\n bizLocation: '$.bizLocation'\n }\n },\n {\n /*\n * **담김의 사실** — 무엇이 무엇에 실렸나(팔레트·상자). 오래 비어 있던 자리다: 관측 리듀서는 이 사실을\n * 받으면 자식 물품에 `parent` 를 붙이는데, 문이 `ObjectEvent` 하나만 알아서 원본은 적재를 말할 길이\n * 없었다(시뮬 원본을 붙여 돌려 보고서야 로그로 드러났다).\n */\n sourceType: 'twin-aggregation',\n mapping: {\n type: 'AggregationEvent',\n action: '$.action',\n bizStep: '$.bizStep',\n disposition: '$.disposition',\n parentID: '$.parentID',\n childEPCs: '$.childEPCs',\n /* 낱개 식별자 없이 **수량으로** 담긴 자식(「이 품번 40개」) — 입고·포장이 이 모양으로 온다. */\n childQuantityList: '$.childQuantityList',\n readPoint: '$.readPoint',\n bizLocation: '$.bizLocation'\n }\n }\n]\n\n/** 개체 관측 — 물건 하나가 어디서 무엇이 됐나. */\nexport interface CanonicalRecord {\n /**\n * 개체 하나의 식별자. **`quantityList` 를 쓰면 없어도 된다** — 낱개 번호가 없는 자재(밀가루 3.5kg)는\n * 개체가 없다. 둘 다 없으면 무엇을 관측했는지 말하지 않은 것이므로 커널 매핑이 거부한다.\n */\n epc?: string\n /**\n * 클래스와 수량 — `{ epcClass, quantity, uom? }`. **잔량 절대값을 싣는다**(「얼마 뺐다」가 아니라\n * 「얼마 남았다」). 그래야 사건 하나를 놓쳐도 다음 사건이 정답을 다시 말해 주고 오차가 쌓이지 않는다.\n *\n * 담김(`childQuantityList`)이 하루 먼저 이 자리를 받았다 — 그때와 같은 이유다. 원본이 비직렬 수량을\n * 말할 길이 없어서, 그것이 주된 경로인 시스템(식품 제조 MES)은 라이브를 낼 수 없었다. 커널의\n * 관측 리듀서는 처음부터 이 값을 읽었고 문만 없었다.\n */\n quantityList?: { epcClass: string; quantity?: number; uom?: string }[]\n action: string // ADD | OBSERVE | DELETE\n bizStep: string\n disposition?: string\n readPoint?: string\n bizLocation?: string\n}\n\n/**\n * 담김 관측 — **부모와 자식**으로 말한다. 자리 필드는 개체 관측과 같은 이름이다(문자열).\n *\n * 이름은 표준의 것을 그대로 쓴다(`parentID`·`childEPCs`) — 여기서 다시 지으면 매핑이 번역이 되고,\n * 같은 사실에 두 어휘가 생긴다.\n */\n/**\n * 변환 관측 — **무엇으로 무엇이 되었나.** 입력과 출력이 **한 사실에 함께** 있어야 계보가 남는다.\n *\n * 따로 보내면(「이 로트가 없어졌다」와 「이 제품이 생겼다」) 그 둘을 이을 근거가 없다. 그래서 표준이\n * 한 사건에 둘을 담고, 우리 레코드도 같은 모양이다.\n *\n * 이름은 **표준의 것을 그대로** 쓴다(`inputQuantityList` 등). 갈래 판정이 이 이름으로 이루어지므로\n * 커넥터 낱말(`consumed`·`produced`)로 담으면 판정되지 않는다 — 커넥터가 정규 이름으로 담아 보내고\n * 매핑은 그 안에서 값을 고른다.\n *\n * **없는 쪽은 필드를 만들지 않는다.** 빈 배열을 실으면 「없다」고 말하는 것이 된다. 소실(N→0)과\n * 생성(0→N)도 표준이 인정하는 변환이다.\n *\n * `transformationID` 가 한 오더의 여러 단계를 잇는다 — 소비를 먼저 알고 산출을 나중에 알게 되는 원본에서\n * 그 둘을 묶는 유일한 자리다.\n */\nexport interface CanonicalTransformationRecord {\n /** 개체로 소비된 것. */\n inputEPCList?: string[]\n /** 클래스와 수량으로 소비된 것 — 로트 단위 소비가 이 자리다. */\n inputQuantityList?: { epcClass: string; quantity?: number; uom?: string }[]\n outputEPCList?: string[]\n outputQuantityList?: { epcClass: string; quantity?: number; uom?: string }[]\n /** 한 오더의 여러 단계를 잇는 식별자. */\n transformationID?: string\n bizStep: string\n disposition?: string\n readPoint?: string\n bizLocation?: string\n}\n\nexport interface CanonicalAggregationRecord {\n parentID: string\n /** 개체로 담긴 자식. 수량으로만 담겼으면 없다(둘 중 하나는 있어야 한다). */\n childEPCs?: string[]\n /** 수량으로 담긴 자식 — `{ epcClass, quantity, uom? }`. 값의 옳고 그름은 커널 검증이 본다. */\n childQuantityList?: { epcClass: string; quantity?: number; uom?: string }[]\n action: string // ADD(담다) | DELETE(풀다) | OBSERVE\n bizStep: string\n disposition?: string\n readPoint?: string\n bizLocation?: string\n}\n\n/** 정규 레코드[] → 검증된 CanonicalEnvelope[] (+ 거부분). 순수 함수. 단일→객체도 배열로 정규화. */\nexport function ingestCanonicalRecords(\n records:\n | CanonicalRecord\n | CanonicalAggregationRecord\n | CanonicalTransformationRecord\n | EnergyRecord\n | EnergyEquipmentRecord\n | OperationalRecord\n | (CanonicalRecord | CanonicalAggregationRecord | CanonicalTransformationRecord | EnergyRecord | EnergyEquipmentRecord | OperationalRecord)[]\n | undefined\n | null,\n tenantId: string,\n defaultEventTime: string\n): IngestResult {\n const arr = Array.isArray(records) ? records : records ? [records] : []\n /*\n * **어휘가 넷이다** — 물류(EPCIS)·에너지 계량·설비 에너지·운영 사실. 예전에는 이 함수가 무조건\n * `ObjectEvent` 를 만들었고, 그래서 계측을 이 길로 넣으면 `epc` 가 없어 거부되거나(그나마 나은 쪽)\n * 엉뚱한 물품 관측이 됐다.\n *\n * 물류 어휘 안에는 모양이 둘이다(개체 관측 `epc` / 담김 `parentID`+`childEPCs`).\n *\n * 판정은 커널이 한 곳에서 한다(`isEnergyRecord`·`isOperationalRecord`·`isAggregationRecord`) — 여기서 다시 짐작하면\n * 소비처마다 답이 달라진다. 봉투는 같은 것을 쓰므로 아래 소비처(미러·저널·브로드캐스팅)는 이 갈림을 알\n * 필요가 없다.\n */\n const energy = arr.filter(isEnergyRecord) as EnergyRecord[]\n /* 세 번째 어휘 — 설비가 낸 자기 에너지 상태(발전·저장·감축 여지·개폐 위치). 계량과 갈라 두는\n 이유는 커널에 적혀 있다: 계량은 구간에 누적되고, 이것들은 그 설비의 지금이다. */\n const energyEquipment = arr.filter(isEnergyEquipmentRecord) as unknown as EnergyEquipmentRecord[]\n /*\n * 네 번째 어휘 — **운영 사실**(작업·설비·사람·자산·오더·품질). 관측 리듀서는 이것을 오래전부터\n * 접었는데 **들어올 문이 없었다**: 원본이 「이 작업이 끝났다」를 말할 길이 없어 시뮬 커널만 그것을\n * 아는 상태가 남았다(같은 화면이 두 구동에서 다른 것을 말한다).\n */\n const operational = arr.filter(isOperationalRecord) as OperationalRecord[]\n const epcis = arr.filter(r => !isEnergyRecord(r) && !isEnergyEquipmentRecord(r) && !isOperationalRecord(r)) as (CanonicalRecord | CanonicalAggregationRecord | CanonicalTransformationRecord)[]\n\n /* 품목 어휘 안에서도 갈림이 있다(개체 관측 / 담김) — 그 판정도 커널의 것을 부른다. */\n const epcisResult = epcis.length\n ? ingest(\n epcis.map(r => ({\n ...r,\n /*\n * ── **시각의 이름을 둘 흡수한다** (2026-08-24 실측) ────────────────────\n * `eventTimePath` 는 **한 이름**만 받는다. 호스트가 `'eventTime'` 으로 못박아 두었는데, 뒤에\n * 생긴 커넥터는 `at` 을 싣는다(에너지 채널이 이미 `at` 을 쓰고 있어 그 어휘를 따랐다).\n * 그래서 그 커넥터의 사건은 **전부 폴링 순간**으로 찍혔다 — 저널 18,150건이 그랬다.\n *\n * 그 결과 둘이 함께 망가졌다: 재고 관측이 트윈의 시계를 밀지 못해 시계가 `task.status` 의\n * 최댓값(2026-04-15)에 **멈춰 있었고**, 이동 사건에서 「언제」가 사라져 순서만 남았다.\n *\n * 이름을 하나로 강요하지 않고 흡수한다 — `orderId`·`resourceRef`·`equipmentId` 를 함께 보는\n * 것과 같은 규율이다(§`twin-event-keys`). 세대가 섞이는 것은 이 이음새의 성질이다.\n *\n * **없으면 만들지 않는다**: 절대값 스냅샷 관측은 원본의 갱신 시각이 아니라 **폴링 순간**이\n * 맞는 관측 시각이다(「어제 값을 오늘 봤다」는 「어제 일어났다」가 아니다). 값을 말하는\n * 레코드만 자기 시각을 갖는다.\n */\n ...(((r as any).eventTime ?? (r as any).at) !== undefined\n ? { eventTime: (r as any).eventTime ?? (r as any).at }\n : {}),\n /* 판정은 커널이 한 곳에서 한다 — 여기서 다시 짐작하면 소비처마다 답이 달라진다. */\n sourceType: isTransformationRecord(r)\n ? 'twin-transformation'\n : isAggregationRecord(r)\n ? 'twin-aggregation'\n : 'twin'\n })),\n TWIN_INGEST_RULES,\n /*\n * **레코드가 시각을 말하면 그것을 쓴다** (2026-08-23).\n *\n * `eventTimePath` 를 주지 않던 동안 이 경로는 **언제나 폴링 시각**을 썼다(`defaultEventTime`).\n * 그래서 커넥터가 이미 싣고 있던 `eventTime`(변환 레코드의 `finishedAt` 등)이 **조용히\n * 무시됐다** — 커넥터를 붙인 쪽은 그것이 먹는다고 믿고 있었고, 어디에도 오류가 나지 않았다.\n *\n * 무엇이 걸렸나: 계보(변환)의 시각이다. 4월에 만든 로트가 8월에 만들어진 것으로 저널에 남았다.\n * 그리고 실측 소요를 재는 폴드가 봉투 시각을 쓰므로(§`kpi-fold`: `workMs = 완료 − 첫 착수`),\n * 폴링 간격을 작업시간으로 배울 수 있었다 — 못 배우는 것보다 나쁘다.\n *\n * 레코드에 `eventTime` 이 없으면 커널이 조용히 기본값으로 떨어진다(§`resolve` — 없는 값은\n * 오류가 아니다). 그래서 이 한 줄이 기존 경로를 막지 않는다: 시각을 말하는 레코드만 사실이 된다.\n */\n { tenantId, defaultEventTime, eventTimePath: 'eventTime' }\n )\n : { accepted: [], rejected: [] }\n const energyResult = energy.length ? ingestEnergyRecords(energy, { tenantId, defaultEventTime }) : { accepted: [], rejected: [] }\n const equipmentResult = energyEquipment.length\n ? ingestEnergyEquipmentRecords(energyEquipment, { tenantId, defaultEventTime })\n : { accepted: [], rejected: [] }\n const operationalResult = operational.length\n ? ingestOperationalRecords(operational, { tenantId, defaultEventTime })\n : { accepted: [], rejected: [] }\n\n /* 거부분을 합쳐 돌려준다 — 어느 어휘에서 거부됐는지는 이유 문구가 말한다. */\n return {\n accepted: [...epcisResult.accepted, ...energyResult.accepted, ...equipmentResult.accepted, ...operationalResult.accepted],\n rejected: [...epcisResult.rejected, ...energyResult.rejected, ...equipmentResult.rejected, ...operationalResult.rejected]\n }\n}\n"]}
1
+ {"version":3,"file":"canonical-ingest.js","sourceRoot":"","sources":["../../server/engine/canonical-ingest.ts"],"names":[],"mappings":"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","sourcesContent":["import { type CanonicalRecord, type CanonicalAggregationRecord, type CanonicalTransformationRecord, type AdapterRule, type EnergyRecord, type EnergyEquipmentRecord, type EnergyGenerationRecord, type EnergyUsagePeriodRecord, type EnergyBillRecord, type EnergyTariffBasisRecord, type EnergyGenerationPriceRecord, type EnergyGenerationPeriodRecord, type OperationalRecord, type IngestResult, type MasterDataRecord } from '@operato/ops-contract'\nimport { ingest, ingestEnergyRecords, ingestEnergyEquipmentRecords, ingestEnergyGenerationRecords, ingestEnergyUsagePeriodRecords, ingestEnergyBillRecords, ingestEnergyTariffBasisRecords, ingestEnergyGenerationPriceRecords, ingestEnergyGenerationPeriodRecords, ingestOperationalRecords, isAggregationRecord, isTransformationRecord, isEnergyRecord, isEnergyEquipmentRecord, isEnergyGenerationRecord, isEnergyUsagePeriodRecord, isEnergyBillRecord, isEnergyTariffBasisRecord, isEnergyGenerationPriceRecord, isEnergyGenerationPeriodRecord, isOperationalRecord, isMasterDataRecord, ingestMasterData } from '@operato/ops-contract'\n\n/*\n * 정규 레코드 → 커널 검증 ACL → CanonicalEnvelope (P1 seam 코어).\n * 순수(커널만 의존; shell·integration-base 무의존 → node:test 검증 가능).\n * 매핑(레거시 스키마 → 정규)은 이 앞 단계(jsonata step 또는 커넥터 openLiveFeed)가 끝낸다.\n * 여기 도착하는 레코드는 이미 정규 필드다 — 어휘마다 자기 필드 이름이 계약이다:\n * EPCIS 품목 { epc, action, bizStep, disposition?, readPoint?, bizLocation? }\n * 에너지 계량 { meterId, kW?, kWh?, … } · 설비 에너지 { equipmentId, generatedKW?, soc?, … }\n * 발전 적산 { equipmentId, kWh, kWhSince?, … }\n * 사용 구간 { meterId, from, to, kWh, unitPrice?, basis?, … } · 청구서 { from, to, total?, … }\n * 요금 기준 { from, to, billingDemandKW?, demandChargePerKW?, currency }\n * 담김(적재) { parentID, childEPCs[] | childQuantityList[], action, bizStep, … }\n * 운영 사실 { taskId | moverId | personId | assetId | orderId, status, … } // vocabulary-guard: allow 저널 와이어 필드\n * 커널은 objectEvent 구성 + EPCIS 검증(위반=오염 차단) + 봉투 래핑만(방언 없음).\n *\n * 두 소비처가 이 단일 코어를 공유(중복 금지):\n * - sim/scenario 경로: operato-twin twin-ingest 태스크(http-get→jsonata→여기).\n * - live 커넥터 경로: reference-live(어댑터 openLiveFeed→여기).\n * SoT: design/integration/face2-adapters.md §5(seam)·§7(매핑=밖, 검증=커널).\n */\n\n/** 고정 정규 매핑(방언 아님 — 필드명 자체가 정규 계약). `sourceType` 으로 어느 품목 사실인지 고른다. */\nconst TWIN_INGEST_RULES: AdapterRule[] = [\n {\n sourceType: 'twin',\n mapping: {\n type: 'ObjectEvent',\n action: '$.action',\n bizStep: '$.bizStep',\n disposition: '$.disposition',\n epc: '$.epc',\n /* 비직렬 수량 — 개체 없이 「이 자리에 이 품목이 얼마 남았다」만 오는 관측이 있다. */\n quantityList: '$.quantityList',\n /*\n * **개체·로트의 마스터데이터**(로트 번호·소비기한 등) — 커널 0.7.58 에서 이 자리가 열렸다.\n *\n * 실측(포천): 원본 재고 1,704행 **전부**에 소비기한이 있는데 투영된 상태에는 **0건**이었다. 지난\n * 재고 950건·59,785kg 이 어디에도 나타나지 않았고, 빈 화면이 「이상 없음」으로 읽혔다.\n *\n * ★ **표준이 `action: 'ADD'` 일 때만 허용한다**(또는 변환). 마스터데이터는 로트가 **생길 때**\n * 말하는 사실이고, 주기 관측마다 실으면 커널이 거부한다 — 알리지 않고 통과시키지 않는 것이 옳다\n * (커넥터가 첫 목격에 `ADD`, 그 뒤로 `OBSERVE` 를 내야 한다).\n */\n ilmd: '$.ilmd',\n /*\n * **정정 선언** — 「이 사건은 없던 일이 되었다」(커널 0.7.65 에서 이 자리가 열렸다).\n *\n * 세 값이 함께여야 뜻이 있어 묶음으로 온다. 커널이 선언 시각을 요구하고, 없으면 사건 시각으로\n * 메우지 않고 이유와 함께 돌려준다 — 「언제 정정했나」와 「언제 일어났나」가 같아지면 둘을 다시\n * 갈라낼 수 없다.\n *\n * 상태는 이것으로 흔들리지 않는다. 커널은 정정을 목록에 적고 재고를 그대로 둔다 — 무엇을 어떻게\n * 되돌릴지는 도메인 판단이고, 참 재고는 그 뒤의 잔량 관측이 다시 말해 준다.\n */\n errorDeclaration: {\n declarationTime: '$.errorDeclaration.declarationTime',\n reason: '$.errorDeclaration.reason',\n correctiveEventIDs: '$.errorDeclaration.correctiveEventIDs'\n },\n readPoint: '$.readPoint',\n bizLocation: '$.bizLocation'\n }\n },\n {\n /* **변환의 사실** — 무엇으로 무엇이 되었나. 이름은 표준의 것을 그대로 쓴다(갈래 판정이 그 이름을 본다). */\n sourceType: 'twin-transformation',\n mapping: {\n type: 'TransformationEvent',\n bizStep: '$.bizStep',\n disposition: '$.disposition',\n inputEPCList: '$.inputEPCList',\n inputQuantityList: '$.inputQuantityList',\n outputEPCList: '$.outputEPCList',\n outputQuantityList: '$.outputQuantityList',\n transformationID: '$.transformationID',\n errorDeclaration: {\n declarationTime: '$.errorDeclaration.declarationTime',\n reason: '$.errorDeclaration.reason',\n correctiveEventIDs: '$.errorDeclaration.correctiveEventIDs'\n },\n readPoint: '$.readPoint',\n bizLocation: '$.bizLocation'\n }\n },\n {\n /*\n * **담김의 사실** — 무엇이 무엇에 실렸나(팔레트·상자). 오래 비어 있던 자리다: 관측 리듀서는 이 사실을\n * 받으면 자식 물품에 `parent` 를 붙이는데, 문이 `ObjectEvent` 하나만 알아서 원본은 적재를 말할 길이\n * 없었다(시뮬 원본을 붙여 돌려 보고서야 로그로 드러났다).\n */\n sourceType: 'twin-aggregation',\n mapping: {\n type: 'AggregationEvent',\n action: '$.action',\n bizStep: '$.bizStep',\n disposition: '$.disposition',\n parentID: '$.parentID',\n childEPCs: '$.childEPCs',\n /* 낱개 식별자 없이 **수량으로** 담긴 자식(「이 품번 40개」) — 입고·포장이 이 모양으로 온다. */\n childQuantityList: '$.childQuantityList',\n errorDeclaration: {\n declarationTime: '$.errorDeclaration.declarationTime',\n reason: '$.errorDeclaration.reason',\n correctiveEventIDs: '$.errorDeclaration.correctiveEventIDs'\n },\n readPoint: '$.readPoint',\n bizLocation: '$.bizLocation'\n }\n }\n]\n\n/*\n * 정규 레코드 셋(`CanonicalRecord` · `CanonicalTransformationRecord` · `CanonicalAggregationRecord`)은\n * **계약으로 옮겼다**(2026-08-30, `@operato/ops-contract` 0.2.0).\n *\n * 여기 있던 이유는 소비 방식 때문이었다 — EPCIS 는 커널의 범용 어댑터를 지나서 타입을 요구하지 않았다.\n * 그런데 모양 자체는 EPCIS 2.0 의 필드 이름이고 호스트의 것이 아니다. 이제 MES 가 이 모양 그대로\n * 보낸다.\n *\n * **매핑 룰(`TWIN_INGEST_RULES`)은 여기 남는다** — 이 모양을 EPCIS 사건으로 옮기는 정책이지 모양이 아니다.\n */\n\n/** 정규 레코드[] → 검증된 CanonicalEnvelope[] (+ 거부분). 순수 함수. 단일→객체도 배열로 정규화. */\nexport function ingestCanonicalRecords(\n records:\n | CanonicalRecord\n | CanonicalAggregationRecord\n | CanonicalTransformationRecord\n | EnergyRecord\n | EnergyEquipmentRecord\n | OperationalRecord\n /* 다섯째 어휘 — 사건이 아닌 것(변하지 않는 속성). 저널로 가지 않는다(§`CanonicalIngestResult`). */\n | MasterDataRecord\n | (\n | CanonicalRecord\n | CanonicalAggregationRecord\n | CanonicalTransformationRecord\n | EnergyRecord\n | EnergyEquipmentRecord\n | OperationalRecord\n | MasterDataRecord\n )[]\n | undefined\n | null,\n tenantId: string,\n defaultEventTime: string,\n /**\n * **이 사실들이 어느 트윈의 것인가** — 마감된 구간 사실의 이름에 쓰인다(커널 §`SubjectBasis`).\n *\n * 커널은 도메인으로 서므로 자기가 어느 트윈인지 모른다. 그런데 설비 번호·계량 지점 번호는 트윈\n * 안에서만 통하는 이름표라, 한 도메인에 현장이 둘이면 서로 다른 설비의 같은 날이 한 사실이 된다.\n * 실측으로 그 일이 났다 — 두 발전소의 `002` 가 겹쳤다.\n *\n * 주지 않으면 이름표를 그대로 쓰고 `twin-local` 로 표시된다 — 겹칠 수 있다는 뜻이다.\n */\n scope?: { scopeId?: string; identityOf?: (kind: 'equipment' | 'meter', localId: string) => string | undefined }\n): CanonicalIngestResult {\n const arr = Array.isArray(records) ? records : records ? [records] : []\n /*\n * ── **다섯째 어휘는 사건이 아니다** (2026-08-28) ────────────────────────────\n *\n * 마스터데이터 — 변하지 않는 속성(유통기한·로트번호·자리의 성질). 표준은 이것을 사건이 아니라\n * **어휘 요소의 속성**으로 담고, 커널에 그 자리가 없어서 지금까지 사건에 실려 왔다.\n *\n * 그 결과가 유입에서 이렇게 났다. 전량을 읽는 원본이 유통기한을 실으려고 **들어오지 않은 것을\n * `ADD`** 라고 말했고, `ilmd` 가 사건 단위라 자리별로 묶을 수도 없어 낱개로 나갔다 — 재기동 한 번에\n * 2,665건이다.\n *\n * 그래서 여기서 갈라 낸다. 이것은 봉투가 되지 않고 저널에도 적히지 않는다(시각축이 없는 값이다).\n * 갈래 판정은 커널의 것을 부른다 — 다른 넷과 같은 규율이다.\n *\n * 설계: `operato-twin/design/plans/master-data.md`\n */\n const masterCandidates = arr.filter(isMasterDataRecord)\n const master = masterCandidates.length ? ingestMasterData(masterCandidates) : { accepted: [], rejected: [] }\n /*\n * **어휘가 넷이다** — 물류(EPCIS)·에너지 계량·설비 에너지·운영 사실. 예전에는 이 함수가 무조건\n * `ObjectEvent` 를 만들었고, 그래서 계측을 이 길로 넣으면 `epc` 가 없어 거부되거나(그나마 나은 쪽)\n * 엉뚱한 물품 관측이 됐다.\n *\n * 물류 어휘 안에는 모양이 둘이다(개체 관측 `epc` / 담김 `parentID`+`childEPCs`).\n *\n * 판정은 커널이 한 곳에서 한다(`isEnergyRecord`·`isOperationalRecord`·`isAggregationRecord`) — 여기서 다시 짐작하면\n * 소비처마다 답이 달라진다. 봉투는 같은 것을 쓰므로 아래 소비처(미러·저널·브로드캐스팅)는 이 갈림을 알\n * 필요가 없다.\n */\n /*\n * 여섯째·일곱째 어휘 — **마감된 사용 구간**과 **청구서**. 커널 0.7.72 가 이 문들을 열었다.\n *\n * ── ★ 사용 구간을 계량보다 **먼저** 가른다 ─────────────────────────────────\n * `isEnergyRecord` 는 `meterId` 가 있으면 참이므로 사용 구간에도 참이다. 순서를 정하지 않으면\n * 시간별 사용량이 계량 표본으로 들어가고, 그 문은 `kWh` 를 **적산 레지스터**로 읽는다. 그러면\n * 커널이 구간 전력량을 차분으로 구하는데 우리가 넣은 값이 이미 구간의 양이다 — 차분의 차분이 되어\n * 값이 조용히 틀린다. 거부되지도 않는다.\n */\n const usagePeriods = arr.filter(isEnergyUsagePeriodRecord) as unknown as EnergyUsagePeriodRecord[]\n const bills = arr.filter(isEnergyBillRecord) as unknown as EnergyBillRecord[]\n /*\n * 여덟째 어휘 — **그 주기에 적용되는 요금 기준**. 커널 0.7.76 이 이 문을 열었다.\n *\n * ★ 청구서와 겹친다. 청구서가 `billingDemandKW` 를 실을 수 있고, 그러면 두 판정이 다 참이다.\n * 그때 요금 기준 문으로 보내면 「금액은 이 문이 받지 않는다」로 거부된다 — 정산이 통째로\n * 사라진다. 그래서 청구서인 것을 뺀다. 커널에서 배타로 만들어 달라고 올렸다.\n */\n const tariffBases = arr.filter(r => isEnergyTariffBasisRecord(r) && !isEnergyBillRecord(r)) as unknown as EnergyTariffBasisRecord[]\n /*\n * 아홉째 어휘 — **발전 단가**. 낸 것에 매겨지는 1kWh 당 값이고 날마다 바뀐다(커널 0.7.79).\n *\n * 다른 문과 겹치지 않는다: 계량 지점이나 설비 id 가 있으면 이 문이 아니고(커널이 배타로 막는다),\n * 요금적용전력·기본요금 단가가 없으므로 요금 기준도 아니다.\n */\n const generationPrices = arr.filter(isEnergyGenerationPriceRecord) as unknown as EnergyGenerationPriceRecord[]\n /* 마감된 발전 기간 — 지난 기록 채우기가 쓴다. 적산의 문과 배타다(구간이 있으면 이쪽). */\n const generationPeriods = arr.filter(isEnergyGenerationPeriodRecord) as unknown as EnergyGenerationPeriodRecord[]\n const energy = arr.filter(r => isEnergyRecord(r) && !isEnergyUsagePeriodRecord(r)) as EnergyRecord[]\n /* 세 번째 어휘 — 설비가 낸 자기 에너지 상태(발전·저장·감축 여지·개폐 위치). 계량과 갈라 두는\n 이유는 커널에 적혀 있다: 계량은 구간에 누적되고, 이것들은 그 설비의 지금이다. */\n /*\n * 다섯 번째 어휘 — **발전 적산**(설비가 지금까지 만든 양). 커널 0.7.69 가 이 문을 열었다.\n *\n * ── ★ 설비 상태보다 **먼저** 가른다 ──────────────────────────────────────────\n * 두 판정이 겹친다. `isEnergyEquipmentRecord` 는 `equipmentId` 가 있고 물류·계량 어휘가 아니면\n * 참이므로 발전 적산도 함께 참이다. 그래서 순서가 갈림을 정한다.\n *\n * 순서를 잘못 두면 조용히 사라지는 것이 아니라 **엉뚱한 이유로 거부된다**: 설비 상태 문은 적산을\n * 값으로 세지 않아 「바꿀 상태가 하나도 없다」로 돌려보낸다. 커넥터는 보냈다고 믿고, 화면에는\n * 발전량이 없다.\n */\n const energyGeneration = arr.filter(isEnergyGenerationRecord) as unknown as EnergyGenerationRecord[]\n const energyEquipment = arr.filter(r => isEnergyEquipmentRecord(r) && !isEnergyGenerationRecord(r)) as unknown as EnergyEquipmentRecord[]\n /*\n * 네 번째 어휘 — **운영 사실**(작업·설비·사람·자산·오더·품질). 관측 리듀서는 이것을 오래전부터\n * 계산했는데 **들어올 문이 없었다**: 원본이 「이 작업이 끝났다」를 말할 길이 없어 시뮬 커널만 그것을\n * 아는 상태가 남았다(같은 화면이 두 구동에서 다른 것을 말한다).\n */\n const operational = arr.filter(isOperationalRecord) as OperationalRecord[]\n /*\n * ── 이 목록은 **어휘를 더할 때마다 함께 늘어야 한다** (2026-08-30) ─────────\n *\n * EPCIS 갈래는 「나머지 전부」다. 새 어휘를 빼 주지 않으면 그 레코드가 제 문으로 가면서\n * **EPCIS 문으로도 들어가** 거부된다. 받아들여지기도 하고 거부되기도 하는 상태가 되어,\n * 유입 장부의 거부 수가 늘고 사람이 그것을 진짜 결함으로 읽는다.\n *\n * 실제로 발전 단가를 더할 때 그렇게 됐다. 목록을 손으로 지키는 대신 시험이 지킨다\n * (§`canonical-ingest-vocabularies` — 어휘마다 한 문으로만 가는지).\n */\n const epcis = arr.filter(\n r =>\n !isEnergyRecord(r) &&\n !isEnergyEquipmentRecord(r) &&\n !isEnergyGenerationRecord(r) &&\n !isEnergyUsagePeriodRecord(r) &&\n !isEnergyBillRecord(r) &&\n !isEnergyTariffBasisRecord(r) &&\n !isEnergyGenerationPriceRecord(r) &&\n !isEnergyGenerationPeriodRecord(r) &&\n !isOperationalRecord(r) &&\n !isMasterDataRecord(r)\n ) as (CanonicalRecord | CanonicalAggregationRecord | CanonicalTransformationRecord)[]\n\n /* 품목 어휘 안에서도 갈림이 있다(개체 관측 / 담김) — 그 판정도 커널의 것을 부른다. */\n const epcisResult = epcis.length\n ? ingest(\n epcis.map(r => ({\n ...r,\n /*\n * ── **시각의 이름을 둘 흡수한다** (2026-08-24 실측) ────────────────────\n * `eventTimePath` 는 **한 이름**만 받는다. 호스트가 `'eventTime'` 으로 못박아 두었는데, 뒤에\n * 생긴 커넥터는 `at` 을 싣는다(에너지 채널이 이미 `at` 을 쓰고 있어 그 어휘를 따랐다).\n * 그래서 그 커넥터의 사건은 **전부 폴링 순간**으로 찍혔다 — 저널 18,150건이 그랬다.\n *\n * 그 결과 둘이 함께 망가졌다: 재고 관측이 트윈의 시계를 밀지 못해 시계가 `task.status` 의\n * 최댓값(2026-04-15)에 **멈춰 있었고**, 이동 사건에서 「언제」가 사라져 순서만 남았다.\n *\n * 이름을 하나로 강요하지 않고 흡수한다 — `orderId`·`resourceRef`·`equipmentId` 를 함께 보는\n * 것과 같은 규율이다(§`twin-event-keys`). 세대가 섞이는 것은 이 이음새의 성질이다.\n *\n * **없으면 만들지 않는다**: 절대값 스냅샷 관측은 원본의 갱신 시각이 아니라 **폴링 순간**이\n * 맞는 관측 시각이다(「어제 값을 오늘 봤다」는 「어제 일어났다」가 아니다). 값을 말하는\n * 레코드만 자기 시각을 갖는다.\n */\n ...(((r as any).eventTime ?? (r as any).at) !== undefined\n ? { eventTime: (r as any).eventTime ?? (r as any).at }\n : {}),\n /* 판정은 커널이 한 곳에서 한다 — 여기서 다시 짐작하면 소비처마다 답이 달라진다. */\n sourceType: isTransformationRecord(r)\n ? 'twin-transformation'\n : isAggregationRecord(r)\n ? 'twin-aggregation'\n : 'twin'\n })),\n TWIN_INGEST_RULES,\n /*\n * **레코드가 시각을 말하면 그것을 쓴다** (2026-08-23).\n *\n * `eventTimePath` 를 주지 않던 동안 이 경로는 **언제나 폴링 시각**을 썼다(`defaultEventTime`).\n * 그래서 커넥터가 이미 싣고 있던 `eventTime`(변환 레코드의 `finishedAt` 등)이 **알리지 않고\n * 무시됐다** — 커넥터를 붙인 쪽은 그것이 먹는다고 믿고 있었고, 어디에도 오류가 나지 않았다.\n *\n * 무엇이 걸렸나: 계보(변환)의 시각이다. 4월에 만든 로트가 8월에 만들어진 것으로 저널에 남았다.\n * 그리고 실측 소요를 재는 폴드가 봉투 시각을 쓰므로(§`kpi-fold`: `workMs = 완료 − 첫 착수`),\n * 폴링 간격을 작업시간으로 배울 수 있었다 — 못 배우는 것보다 나쁘다.\n *\n * 레코드에 `eventTime` 이 없으면 커널이 알리지 않고 기본값으로 떨어진다(§`resolve` — 없는 값은\n * 오류가 아니다). 그래서 이 한 줄이 기존 경로를 막지 않는다: 시각을 말하는 레코드만 사실이 된다.\n */\n { tenantId, defaultEventTime, eventTimePath: 'eventTime' }\n )\n : { accepted: [], rejected: [] }\n const energyResult = energy.length ? ingestEnergyRecords(energy, { tenantId, defaultEventTime, ...scope }) : { accepted: [], rejected: [] }\n const equipmentResult = energyEquipment.length\n ? ingestEnergyEquipmentRecords(energyEquipment, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const usageResult = usagePeriods.length\n ? ingestEnergyUsagePeriodRecords(usagePeriods, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const billResult = bills.length ? ingestEnergyBillRecords(bills, { tenantId, defaultEventTime, ...scope }) : { accepted: [], rejected: [] }\n const tariffBasisResult = tariffBases.length\n ? ingestEnergyTariffBasisRecords(tariffBases, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const generationPeriodResult = generationPeriods.length\n ? ingestEnergyGenerationPeriodRecords(generationPeriods, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const generationPriceResult = generationPrices.length\n ? ingestEnergyGenerationPriceRecords(generationPrices, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const generationResult = energyGeneration.length\n ? ingestEnergyGenerationRecords(energyGeneration, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const operationalResult = operational.length\n ? ingestOperationalRecords(operational, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n\n /* 거부분을 합쳐 돌려준다 — 어느 어휘에서 거부됐는지는 이유 문구가 말한다. */\n return {\n accepted: [\n ...epcisResult.accepted,\n ...energyResult.accepted,\n ...equipmentResult.accepted,\n ...generationResult.accepted,\n ...usageResult.accepted,\n ...generationPriceResult.accepted,\n ...generationPeriodResult.accepted,\n ...billResult.accepted,\n ...tariffBasisResult.accepted,\n ...operationalResult.accepted\n ],\n rejected: [\n ...epcisResult.rejected,\n ...energyResult.rejected,\n ...equipmentResult.rejected,\n ...generationResult.rejected,\n ...usageResult.rejected,\n ...billResult.rejected,\n ...tariffBasisResult.rejected,\n ...generationPriceResult.rejected,\n ...generationPeriodResult.rejected,\n ...operationalResult.rejected,\n ...master.rejected\n ],\n /* 사건이 아닌 것은 따로 낸다 — 부르는 쪽이 상태 세우는 문으로 보낸다(저널로 가지 않게). */\n masterData: master.accepted\n }\n}\n\n/**\n * 유입 결과 — 사건(봉투)과 **사건이 아닌 것**을 갈라 낸다.\n *\n * 마스터데이터를 `accepted` 에 섞지 않는 이유: 부르는 쪽이 그것을 저널에 적게 되고, 시각축이 없는 값이\n * 「그때 일어난 일」로 남는다. 갈래를 타입으로 갈라 두면 그 실수를 할 수 없다.\n */\nexport interface CanonicalIngestResult extends IngestResult {\n masterData: MasterDataRecord[]\n}\n"]}
@@ -17,7 +17,7 @@
17
17
  * | 주석(확인 ack) | 커널 런타임 | 관측 커널 |
18
18
  *
19
19
  * 미러에서 액추에이션을 그냥 허용하면 **새 거짓말이 생긴다**: 트윈은 "보류됨" 이라 말하고 실 시스템은
20
- * 모른다. 조용히 커널만 바꾸지 않고, 이유를 밝혀 거절한다.
20
+ * 모른다. 알리지 않고 커널만 바꾸지 않고, 이유를 밝혀 거절한다.
21
21
  *
22
22
  * 반면 확인(ack)은 액추에이션이 아니다 — 신호를 봤다는 트윈 쪽 주석이라 현실을 건드리지 않는다.
23
23
  * 미러에서도 성립해야 하므로 관측 커널로 보낸다.
@@ -1 +1 @@
1
- {"version":3,"file":"command-routing.js","sourceRoot":"","sources":["../../server/engine/command-routing.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;;;AAgCH,oCAYC;AA1CD,6CAA6C;AAC7C,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAS,CAAC,eAAe,CAAC,CAAC,CAAA;AAoB9D,uCAAuC;AAChC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AAAvE,QAAA,WAAW,eAA4D;AAEpF;;;;;GAKG;AACH,SAAgB,YAAY,CAAC,IAAkC,EAAE,aAAsB,EAAE,IAAY;IACnG,IAAI,CAAC,IAAI,IAAI,CAAC,aAAa;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,CAAA;IAClF,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAA;IAEpE,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACzB,+CAA+C;QAC/C,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,CAAA;IAChG,CAAC;IAED,iDAAiD;IACjD,IAAI,IAAA,mBAAW,EAAC,IAAI,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,qBAAqB,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,EAAE,CAAA;IAC3G,OAAO,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,CAAA;AACtG,CAAC","sourcesContent":["/*\n * 커맨드 라우팅 판정 — **순수**. \"이 커맨드를 누구에게 보낼 것인가\" 만 정한다.\n * 실제 dispatch·저널 기록은 엔진이 한다(`design/plans/command-routing.md` §1·§4 P0).\n *\n * ── 왜 떼어냈나 ─────────────────────────────────────────────────────────────\n * 예전에는 리졸버가 `inst.runtime.dispatch(...)` 를 곧바로 불렀다. `runtime` 은 **시뮬 경로에서만**\n * 만들어지므로 실물을 비추는 트윈(mirror)에서는 그 한 줄이 `TypeError` 로 터졌다 — 조치 채널이\n * 통째로 닿지 않았고, 사용자에게는 이유 있는 거절이 아니라 그냥 실패였다.\n *\n * 판정이 코드 한가운데 있으면 이런 부류를 테스트로 못 잡는다(엔진은 DB·TypeORM 을 물고 있어 단위\n * 테스트가 불러올 수 없다). 그래서 판정만 순수하게 떼어 둔다 — `planWarmStart` 와 같은 이유다.\n *\n * ── 판정 규칙 = 권위 × 성격 ─────────────────────────────────────────────────\n * | 성격 \\ 모드 | 시뮬 | 미러(현실을 비춤) |\n * | 액추에이션 | 커널 런타임 | **거절**(아웃바운드 어댑터 미결선) |\n * | 주석(확인 ack) | 커널 런타임 | 관측 커널 |\n *\n * 미러에서 액추에이션을 그냥 허용하면 **새 거짓말이 생긴다**: 트윈은 \"보류됨\" 이라 말하고 실 시스템은\n * 모른다. 조용히 커널만 바꾸지 않고, 이유를 밝혀 거절한다.\n *\n * 반면 확인(ack)은 액추에이션이 아니다 — 신호를 봤다는 트윈 쪽 주석이라 현실을 건드리지 않는다.\n * 미러에서도 성립해야 하므로 관측 커널로 보낸다.\n */\n\n/** 관측 쪽 주석 커맨드 — 현실을 바꾸지 않으므로 미러에서도 통과한다. */\nconst ANNOTATION_COMMANDS = new Set<string>(['attention.ack'])\n\n/** 라우팅에 필요한 인스턴스 사실만 — 엔진 객체를 그대로 받지 않는다(순수하게 유지). */\nexport interface RoutableInstance {\n /** 'live' 면 현실을 비추는 트윈(미러). 그 외는 시뮬. */\n mode?: string\n /** 커널 런타임(tick/dispatch)이 붙어 있나 — 시뮬 경로에서만 만들어진다. */\n hasRuntime: boolean\n /** 관측 커널이 dispatch 를 받을 수 있나. */\n hasKernelDispatch: boolean\n}\n\nexport type CommandRoute =\n /** 커널 런타임으로(시뮬 — 커널이 세계다). */\n | { target: 'runtime' }\n /** 관측 커널로(미러 — 주석만). 방출된 사실은 저널 큐에 실어야 한다. */\n | { target: 'kernel' }\n /** 보내지 않는다. 사유는 언어 중립 코드 — 화면이 `twin.cmderr.<code>` 로 사람 말을 만든다. */\n | { target: 'reject'; errorCode: string; errorParams?: Record<string, string | number> }\n\n/** 이 커맨드가 실 프로세스를 바꾸는 것인가(= 액추에이션). */\nexport const isActuation = (type: string): boolean => !ANNOTATION_COMMANDS.has(type)\n\n/**\n * 어디로 보낼지 정한다.\n *\n * `inst` 가 없거나 호출자 도메인 소유가 아니면 **같은 사유로** 거절한다 — 존재 여부를 알려 주면\n * 다른 테넌트의 인스턴스 id 를 추측할 수 있다(있음/없음이 구별되면 그것이 정보다).\n */\nexport function routeCommand(inst: RoutableInstance | undefined, ownedByCaller: boolean, type: string): CommandRoute {\n if (!inst || !ownedByCaller) return { target: 'reject', errorCode: 'no-instance' }\n if (!type) return { target: 'reject', errorCode: 'unknown-command' }\n\n if (inst.mode !== 'live') {\n /* 시뮬 — 커널이 세계다. 런타임이 없으면 오류를 내지 않고 이유를 말한다. */\n return inst.hasRuntime ? { target: 'runtime' } : { target: 'reject', errorCode: 'no-runtime' }\n }\n\n /* 미러 — 실 프로세스 변경은 아웃바운드 어댑터의 몫이고 아직 결선되지 않았다. */\n if (isActuation(type)) return { target: 'reject', errorCode: 'actuation-not-wired', errorParams: { type } }\n return inst.hasKernelDispatch ? { target: 'kernel' } : { target: 'reject', errorCode: 'no-runtime' }\n}\n"]}
1
+ {"version":3,"file":"command-routing.js","sourceRoot":"","sources":["../../server/engine/command-routing.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;;;AAgCH,oCAYC;AA1CD,6CAA6C;AAC7C,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAS,CAAC,eAAe,CAAC,CAAC,CAAA;AAoB9D,uCAAuC;AAChC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AAAvE,QAAA,WAAW,eAA4D;AAEpF;;;;;GAKG;AACH,SAAgB,YAAY,CAAC,IAAkC,EAAE,aAAsB,EAAE,IAAY;IACnG,IAAI,CAAC,IAAI,IAAI,CAAC,aAAa;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,EAAE,CAAA;IAClF,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAA;IAEpE,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACzB,+CAA+C;QAC/C,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,CAAA;IAChG,CAAC;IAED,iDAAiD;IACjD,IAAI,IAAA,mBAAW,EAAC,IAAI,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,qBAAqB,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,EAAE,CAAA;IAC3G,OAAO,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,CAAA;AACtG,CAAC","sourcesContent":["/*\n * 커맨드 라우팅 판정 — **순수**. \"이 커맨드를 누구에게 보낼 것인가\" 만 정한다.\n * 실제 dispatch·저널 기록은 엔진이 한다(`design/plans/command-routing.md` §1·§4 P0).\n *\n * ── 왜 떼어냈나 ─────────────────────────────────────────────────────────────\n * 예전에는 리졸버가 `inst.runtime.dispatch(...)` 를 곧바로 불렀다. `runtime` 은 **시뮬 경로에서만**\n * 만들어지므로 실물을 비추는 트윈(mirror)에서는 그 한 줄이 `TypeError` 로 터졌다 — 조치 채널이\n * 통째로 닿지 않았고, 사용자에게는 이유 있는 거절이 아니라 그냥 실패였다.\n *\n * 판정이 코드 한가운데 있으면 이런 부류를 테스트로 못 잡는다(엔진은 DB·TypeORM 을 물고 있어 단위\n * 테스트가 불러올 수 없다). 그래서 판정만 순수하게 떼어 둔다 — `planWarmStart` 와 같은 이유다.\n *\n * ── 판정 규칙 = 권위 × 성격 ─────────────────────────────────────────────────\n * | 성격 \\ 모드 | 시뮬 | 미러(현실을 비춤) |\n * | 액추에이션 | 커널 런타임 | **거절**(아웃바운드 어댑터 미결선) |\n * | 주석(확인 ack) | 커널 런타임 | 관측 커널 |\n *\n * 미러에서 액추에이션을 그냥 허용하면 **새 거짓말이 생긴다**: 트윈은 \"보류됨\" 이라 말하고 실 시스템은\n * 모른다. 알리지 않고 커널만 바꾸지 않고, 이유를 밝혀 거절한다.\n *\n * 반면 확인(ack)은 액추에이션이 아니다 — 신호를 봤다는 트윈 쪽 주석이라 현실을 건드리지 않는다.\n * 미러에서도 성립해야 하므로 관측 커널로 보낸다.\n */\n\n/** 관측 쪽 주석 커맨드 — 현실을 바꾸지 않으므로 미러에서도 통과한다. */\nconst ANNOTATION_COMMANDS = new Set<string>(['attention.ack'])\n\n/** 라우팅에 필요한 인스턴스 사실만 — 엔진 객체를 그대로 받지 않는다(순수하게 유지). */\nexport interface RoutableInstance {\n /** 'live' 면 현실을 비추는 트윈(미러). 그 외는 시뮬. */\n mode?: string\n /** 커널 런타임(tick/dispatch)이 붙어 있나 — 시뮬 경로에서만 만들어진다. */\n hasRuntime: boolean\n /** 관측 커널이 dispatch 를 받을 수 있나. */\n hasKernelDispatch: boolean\n}\n\nexport type CommandRoute =\n /** 커널 런타임으로(시뮬 — 커널이 세계다). */\n | { target: 'runtime' }\n /** 관측 커널로(미러 — 주석만). 방출된 사실은 저널 큐에 실어야 한다. */\n | { target: 'kernel' }\n /** 보내지 않는다. 사유는 언어 중립 코드 — 화면이 `twin.cmderr.<code>` 로 사람 말을 만든다. */\n | { target: 'reject'; errorCode: string; errorParams?: Record<string, string | number> }\n\n/** 이 커맨드가 실 프로세스를 바꾸는 것인가(= 액추에이션). */\nexport const isActuation = (type: string): boolean => !ANNOTATION_COMMANDS.has(type)\n\n/**\n * 어디로 보낼지 정한다.\n *\n * `inst` 가 없거나 호출자 도메인 소유가 아니면 **같은 사유로** 거절한다 — 존재 여부를 알려 주면\n * 다른 테넌트의 인스턴스 id 를 추측할 수 있다(있음/없음이 구별되면 그것이 정보다).\n */\nexport function routeCommand(inst: RoutableInstance | undefined, ownedByCaller: boolean, type: string): CommandRoute {\n if (!inst || !ownedByCaller) return { target: 'reject', errorCode: 'no-instance' }\n if (!type) return { target: 'reject', errorCode: 'unknown-command' }\n\n if (inst.mode !== 'live') {\n /* 시뮬 — 커널이 세계다. 런타임이 없으면 오류를 내지 않고 이유를 말한다. */\n return inst.hasRuntime ? { target: 'runtime' } : { target: 'reject', errorCode: 'no-runtime' }\n }\n\n /* 미러 — 실 프로세스 변경은 아웃바운드 어댑터의 몫이고 아직 결선되지 않았다. */\n if (isActuation(type)) return { target: 'reject', errorCode: 'actuation-not-wired', errorParams: { type } }\n return inst.hasKernelDispatch ? { target: 'kernel' } : { target: 'reject', errorCode: 'no-runtime' }\n}\n"]}
@@ -18,7 +18,7 @@
18
18
  * ── 판정을 순수 함수로 (이 파일) ────────────────────────────────────────────
19
19
  * 「이 트윈에 저장된 자극을 실을 것인가」에는 사람이 틀리기 쉬운 갈림이 셋 있다: 미러에 실으면 **현실을
20
20
  * 오염**시키고, 잘못된 선언을 실으면 **다음 틱에서 서버가 내려가고**(실제로 그 이력이 있다), 시나리오
21
- * 엔진이 없는 런타임에 실으면 조용히 아무 일도 없다. 그래서 판정을 값으로 만들고 시험한다.
21
+ * 엔진이 없는 런타임에 실으면 알리지 않고 아무 일도 없다. 그래서 판정을 값으로 만들고 시험한다.
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
24
  exports.planStimulus = planStimulus;
@@ -1 +1 @@
1
- {"version":3,"file":"declared-stimulus.js","sourceRoot":"","sources":["../../server/engine/declared-stimulus.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;AAsBH,oCAcC;AAOD,oCAEC;AA9BD;;;;;;GAMG;AACH,SAAgB,YAAY,CAC1B,MAAW,EACX,OAA8D,EAC9D,QAAuH;IAEvH,MAAM,QAAQ,GAAG,MAAM,EAAE,QAAQ,CAAA;IACjC,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAA;IACxF,+DAA+D;IAC/D,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;IAC1E,IAAI,CAAC,OAAO,CAAC,iBAAiB;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,CAAA;IAC9E,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,wBAAwB,EAAE,CAAA;IAC7F,MAAM,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAA;IAC5B,IAAI,CAAC,CAAC,EAAE,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAG,CAAS,EAAE,SAAS,IAAI,UAAU,EAAE,CAAA;IACrG,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;AACrC,CAAC;AAED;;;;GAIG;AACH,SAAgB,YAAY,CAAC,MAAW,EAAE,QAAa;IACrD,OAAO,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAA;AACxC,CAAC","sourcesContent":["/*\n * **자극은 원본이 선언한다** — 트윈이 아니라.\n *\n * ── 무엇이 났나 (2026-08-19) ────────────────────────────────────────────────\n * 데모의 자극(시나리오)은 시드 코드가 **메모리에만** 실었다: `runtime.scenario.load(...)`. 그래서 트윈을\n * 재기동하면 자극이 사라지고, 구조만 서 있는 트윈이 남는다 — 작업 0·오더 0. 「살아 있는 데모」가\n * **첫 재기동까지만** 사는 것이다(오늘 서버를 여러 번 띄우는 동안 실제로 그 상태였다).\n *\n * ── 왜 트윈에 저장하지 않나 (ADR-0029 · plans/simulator-as-source.md §4) ────\n * 자극은 **원본의 거동**이다: 무엇이 들어오고 무슨 주문이 나는지는 시뮬레이터(원본)가 정하는 사실이고,\n * 트윈은 그것을 반영한다. 트윈에 새 축을 만들면 ADR-0029 가 나중에 걷어내야 하는 표면이 하나 늘고\n * (그 계획서의 §2·§5 가 이미 그 부담을 적어 두었다), 원본에 두면 그 계획의 **첫 조각 앞자리**가 된다:\n * 지금은 호스트가 읽어 커널에 싣고, 나중에 `openLiveFeed` 뒤로 옮기면 같은 선언이 그대로 쓰인다.\n *\n * 그래서 집은 `TwinReference.connectionConfig.scenario` — 어댑터별 설정 자리(이미 `simple-json`)다.\n *\n * ── 판정을 순수 함수로 (이 파일) ────────────────────────────────────────────\n * 「이 트윈에 저장된 자극을 실을 것인가」에는 사람이 틀리기 쉬운 갈림이 셋 있다: 미러에 실으면 **현실을\n * 오염**시키고, 잘못된 선언을 실으면 **다음 틱에서 서버가 내려가고**(실제로 그 이력이 있다), 시나리오\n * 엔진이 없는 런타임에 실으면 조용히 아무 일도 없다. 그래서 판정을 값으로 만들고 시험한다.\n */\n\n/** 자극을 실을지 말지 — 그리고 왜. 부르는 쪽은 이 답만 보고 움직인다. */\nexport type StimulusPlan =\n /** 실어라 — 검사를 통과한 선언. */\n | { action: 'load'; scenario: any }\n /**\n * 싣지 않는다 — 그 이유.\n * · `none` — 원본이 자극을 선언하지 않았다(정상: 정적 구조만 반영하는 트윈)\n * · `mirror` — 미러다. 외부 실물이 진실인 트윈에 우리가 자극을 만들면 그것은 현실이 아니다\n * · `no-engine` — 이 런타임에 시나리오 엔진이 없다(관측 구동 커널)\n * · `invalid` — 선언이 커널 규약을 어긴다. **싣지 않고 말한다**(다음 틱에서 터지게 두지 않는다)\n */\n | { action: 'skip'; reason: 'none' | 'observed' | 'no-engine' | 'invalid'; detail?: string }\n\n/**\n * 저장된 자극 선언을 읽어 **실을지 판정한다** — 커널 검사기를 그대로 쓴다(규칙을 두 벌로 만들지 않는다).\n *\n * `validate` 는 커널의 `validateScenario` 를 넘긴다(호스트가 커널 타입에 묶이지 않게 함수로 받는다).\n * 검사기가 없으면 **싣지 않는다**: 검사할 수 없는 선언을 태우는 것이 가장 비싼 선택이다(서버가 내려간\n * 이력이 있다).\n */\nexport function planStimulus(\n config: any,\n runtime: { hasScenarioEngine: boolean; mode?: 'sim' | 'live' },\n validate?: (scenario: any) => { ok: boolean; errorCode?: string; errorParams?: any } | { ok: false; errorCode: string }\n): StimulusPlan {\n const scenario = config?.scenario\n if (!scenario || typeof scenario !== 'object') return { action: 'skip', reason: 'none' }\n /* 관측 트윈을 먼저 막는다 — 「엔진이 없다」로 답하면 이유가 흐려진다(애초에 실을 수 없는 트윈이다). */\n if (runtime.mode === 'live') return { action: 'skip', reason: 'observed' }\n if (!runtime.hasScenarioEngine) return { action: 'skip', reason: 'no-engine' }\n if (!validate) return { action: 'skip', reason: 'invalid', detail: 'no validator available' }\n const v = validate(scenario)\n if (!v?.ok) return { action: 'skip', reason: 'invalid', detail: (v as any)?.errorCode ?? 'rejected' }\n return { action: 'load', scenario }\n}\n\n/**\n * 선언을 원본 설정에 얹는다(순수) — **기존 설정을 지우지 않는다.**\n *\n * 어댑터 설정에는 접속 정보가 함께 산다. 자극을 넣으면서 그 옆을 덮으면 다음 재동기가 원본을 못 찾는다.\n */\nexport function withStimulus(config: any, scenario: any): any {\n return { ...(config ?? {}), scenario }\n}\n"]}
1
+ {"version":3,"file":"declared-stimulus.js","sourceRoot":"","sources":["../../server/engine/declared-stimulus.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;AAsBH,oCAcC;AAOD,oCAEC;AA9BD;;;;;;GAMG;AACH,SAAgB,YAAY,CAC1B,MAAW,EACX,OAA8D,EAC9D,QAAuH;IAEvH,MAAM,QAAQ,GAAG,MAAM,EAAE,QAAQ,CAAA;IACjC,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAA;IACxF,+DAA+D;IAC/D,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,CAAA;IAC1E,IAAI,CAAC,OAAO,CAAC,iBAAiB;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,CAAA;IAC9E,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,wBAAwB,EAAE,CAAA;IAC7F,MAAM,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAA;IAC5B,IAAI,CAAC,CAAC,EAAE,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAG,CAAS,EAAE,SAAS,IAAI,UAAU,EAAE,CAAA;IACrG,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;AACrC,CAAC;AAED;;;;GAIG;AACH,SAAgB,YAAY,CAAC,MAAW,EAAE,QAAa;IACrD,OAAO,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAA;AACxC,CAAC","sourcesContent":["/*\n * **자극은 원본이 선언한다** — 트윈이 아니라.\n *\n * ── 무엇이 났나 (2026-08-19) ────────────────────────────────────────────────\n * 데모의 자극(시나리오)은 시드 코드가 **메모리에만** 실었다: `runtime.scenario.load(...)`. 그래서 트윈을\n * 재기동하면 자극이 사라지고, 구조만 서 있는 트윈이 남는다 — 작업 0·오더 0. 「살아 있는 데모」가\n * **첫 재기동까지만** 사는 것이다(오늘 서버를 여러 번 띄우는 동안 실제로 그 상태였다).\n *\n * ── 왜 트윈에 저장하지 않나 (ADR-0029 · plans/simulator-as-source.md §4) ────\n * 자극은 **원본의 거동**이다: 무엇이 들어오고 무슨 주문이 나는지는 시뮬레이터(원본)가 정하는 사실이고,\n * 트윈은 그것을 반영한다. 트윈에 새 축을 만들면 ADR-0029 가 나중에 걷어내야 하는 표면이 하나 늘고\n * (그 계획서의 §2·§5 가 이미 그 부담을 적어 두었다), 원본에 두면 그 계획의 **첫 조각 앞자리**가 된다:\n * 지금은 호스트가 읽어 커널에 싣고, 나중에 `openLiveFeed` 뒤로 옮기면 같은 선언이 그대로 쓰인다.\n *\n * 그래서 집은 `TwinReference.connectionConfig.scenario` — 어댑터별 설정 자리(이미 `simple-json`)다.\n *\n * ── 판정을 순수 함수로 (이 파일) ────────────────────────────────────────────\n * 「이 트윈에 저장된 자극을 실을 것인가」에는 사람이 틀리기 쉬운 갈림이 셋 있다: 미러에 실으면 **현실을\n * 오염**시키고, 잘못된 선언을 실으면 **다음 틱에서 서버가 내려가고**(실제로 그 이력이 있다), 시나리오\n * 엔진이 없는 런타임에 실으면 알리지 않고 아무 일도 없다. 그래서 판정을 값으로 만들고 시험한다.\n */\n\n/** 자극을 실을지 말지 — 그리고 왜. 부르는 쪽은 이 답만 보고 움직인다. */\nexport type StimulusPlan =\n /** 실어라 — 검사를 통과한 선언. */\n | { action: 'load'; scenario: any }\n /**\n * 싣지 않는다 — 그 이유.\n * · `none` — 원본이 자극을 선언하지 않았다(정상: 정적 구조만 반영하는 트윈)\n * · `mirror` — 미러다. 외부 실물이 진실인 트윈에 우리가 자극을 만들면 그것은 현실이 아니다\n * · `no-engine` — 이 런타임에 시나리오 엔진이 없다(관측 구동 커널)\n * · `invalid` — 선언이 커널 규약을 어긴다. **싣지 않고 말한다**(다음 틱에서 터지게 두지 않는다)\n */\n | { action: 'skip'; reason: 'none' | 'observed' | 'no-engine' | 'invalid'; detail?: string }\n\n/**\n * 저장된 자극 선언을 읽어 **실을지 판정한다** — 커널 검사기를 그대로 쓴다(규칙을 두 벌로 만들지 않는다).\n *\n * `validate` 는 커널의 `validateScenario` 를 넘긴다(호스트가 커널 타입에 묶이지 않게 함수로 받는다).\n * 검사기가 없으면 **싣지 않는다**: 검사할 수 없는 선언을 태우는 것이 가장 비싼 선택이다(서버가 내려간\n * 이력이 있다).\n */\nexport function planStimulus(\n config: any,\n runtime: { hasScenarioEngine: boolean; mode?: 'sim' | 'live' },\n validate?: (scenario: any) => { ok: boolean; errorCode?: string; errorParams?: any } | { ok: false; errorCode: string }\n): StimulusPlan {\n const scenario = config?.scenario\n if (!scenario || typeof scenario !== 'object') return { action: 'skip', reason: 'none' }\n /* 관측 트윈을 먼저 막는다 — 「엔진이 없다」로 답하면 이유가 흐려진다(애초에 실을 수 없는 트윈이다). */\n if (runtime.mode === 'live') return { action: 'skip', reason: 'observed' }\n if (!runtime.hasScenarioEngine) return { action: 'skip', reason: 'no-engine' }\n if (!validate) return { action: 'skip', reason: 'invalid', detail: 'no validator available' }\n const v = validate(scenario)\n if (!v?.ok) return { action: 'skip', reason: 'invalid', detail: (v as any)?.errorCode ?? 'rejected' }\n return { action: 'load', scenario }\n}\n\n/**\n * 선언을 원본 설정에 얹는다(순수) — **기존 설정을 지우지 않는다.**\n *\n * 어댑터 설정에는 접속 정보가 함께 산다. 자극을 넣으면서 그 옆을 덮으면 다음 재동기가 원본을 못 찾는다.\n */\nexport function withStimulus(config: any, scenario: any): any {\n return { ...(config ?? {}), scenario }\n}\n"]}
@@ -16,11 +16,11 @@ exports.buildEnergyTopology = buildEnergyTopology;
16
16
  * 단선도를 그리면 갖지 않은 공학적 충실도를 주장하게 된다. 여기서 그리는 것은 **트윈이 선언한 관계**
17
17
  * (자리의 계층 + 설비의 소속)와 **계량이 실제로 덮는 범위**뿐이다. 한계는 소비처가 화면에 적는다.
18
18
  *
19
- * ── 조용히 빠뜨리지 않는다 ─────────────────────────────────────────────────
19
+ * ── 알리지 않고 빠뜨리지 않는다 ─────────────────────────────────────────────────
20
20
  * 자리를 모르는 설비, 계량기가 없는 자리, 계량이 덮지 않는 몫은 **각자 자기 칸으로** 나온다. 그림에서
21
21
  * 사라지면 사용자는 자기 현장이 다 그려졌다고 믿는다.
22
22
  */
23
- const twin_kernel_1 = require("@operato/twin-kernel");
23
+ const ops_contract_1 = require("@operato/ops-contract");
24
24
  /**
25
25
  * 이 설비의 **지금 값** — 스냅샷에서 그 설비를 찾아 에너지 축의 값만 옮긴다.
26
26
  *
@@ -59,7 +59,7 @@ function liveOf(state, id, kind) {
59
59
  live.measuredAt = String(e.measuredAt);
60
60
  return { live };
61
61
  }
62
- const ELECTRICAL = new Set(twin_kernel_1.EMS_LOCATION_TYPES);
62
+ const ELECTRICAL = new Set(ops_contract_1.EMS_LOCATION_TYPES);
63
63
  /** 부하가 아닌 계통 설비 — 이것들의 소비를 부하로 세면 총량이 이중으로 나뉜다. */
64
64
  const GEAR = new Set(['meter', 'breaker', 'pv-array', 'battery']);
65
65
  /**
@@ -131,7 +131,7 @@ equipmentState) {
131
131
  const all = model?.locations ?? [];
132
132
  for (const n of locations) {
133
133
  const segment = byId.get(String(n.id));
134
- const up = (0, twin_kernel_1.electricalUpstreamOf)(all, String(n.id));
134
+ const up = (0, ops_contract_1.electricalUpstreamOf)(all, String(n.id));
135
135
  const parent = up ? byId.get(up) : undefined;
136
136
  if (parent && parent !== segment)
137
137
  parent.children.push(segment);
@@ -1 +1 @@
1
- {"version":3,"file":"energy-topology.js","sourceRoot":"","sources":["../../server/engine/energy-topology.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 전력 계통도 — **선언된 것만 그린다.** 순수.\n *\n * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────\n * 분기별 막대는 「어디가 많이 쓰나」에 답하지만 **어디에 걸려 있나**에 답하지 못한다. 실제로 부딪힌\n * 세 가지가 전부 계통 질문이었다:\n * · 「하위 계량이 덮지 않는 327 kWh」 — 그 몫이 수전 직결 부하인지, 계량 없는 구간인지 표로는 모른다.\n * · 커넥터는 결선을 모르는 부하를 **수전에 매단다**(정직한 선택). 그런데 그 사실이 화면에 없었다.\n * · 「계량기를 어디에 더 달아야 하나」 — 어두운 구간이 보여야 답할 수 있다.\n *\n * ── 무엇을 그리지 않나 ─────────────────────────────────────────────────────\n * **보호 계통 단선도(SLD)가 아니다.** 차단기 정격·보호 협조·상(phase)·결선 방식은 모델에 없고, 그럴듯한\n * 단선도를 그리면 갖지 않은 공학적 충실도를 주장하게 된다. 여기서 그리는 것은 **트윈이 선언한 관계**\n * (자리의 계층 + 설비의 소속)와 **계량이 실제로 덮는 범위**뿐이다. 한계는 소비처가 화면에 적는다.\n *\n * ── 조용히 빠뜨리지 않는다 ─────────────────────────────────────────────────\n * 자리를 모르는 설비, 계량기가 없는 자리, 계량이 덮지 않는 몫은 **각자 자기 칸으로** 나온다. 그림에서\n * 사라지면 사용자는 자기 현장이 다 그려졌다고 믿는다.\n */\nimport { EMS_LOCATION_TYPES, electricalUpstreamOf } from '@operato/twin-kernel'\n\n/** 계통의 한 구간(수전·분기·구역) — 자리 하나가 곧 구간이다. */\nexport interface TopologySegment {\n id: string\n /** 카탈로그 자리 타입(`incoming`·`feeder`·`submeter-zone`). */\n type: string\n label?: string\n /** 이 자리에 달린 계량 지점 id 들 — 비어 있으면 **어두운 구간**이다(값을 알 길이 없다). */\n meters: string[]\n /** 이 구간의 전력량과 근거 — 폴드가 낸 것만 싣는다(우리가 다시 계산하지 않는다). */\n kWh?: number\n basis?: 'measured' | 'apportioned'\n /**\n * **피크가 찍힌 순간** 이 구간이 얼마였나(kW) — 요금이 걸린 수다.\n *\n * 전력량이 큰 구간과 피크를 만든 구간은 다를 수 있다(하루 종일 조금씩 쓰는 부하 vs 잠깐 크게 쓰는 부하).\n * 그림에 둘을 함께 두면 「어느 분기를 깎아야 피크가 내려가나」가 바로 읽힌다.\n */\n peakKW?: number\n /** 이 자리에 매달린 부하(설비) — 종류를 그대로 싣는다. */\n loads: { id: string; kind: string }[]\n /**\n * 계통 설비(차단기·태양광·축전지) — 부하가 아니라 계통의 일부다.\n *\n * `live` 는 **지금 값**이다(구간 값이 아니다): 태양광이 지금 몇 kW 를 만들고, 축전지가 얼마를 내보내며\n * 충전율이 얼마인가. 원천이 보내 주는 사실이라 우리가 만들지 않는다 — 보내지 않은 필드는 없다.\n *\n * 시각(`measuredAt`)을 함께 나른다: 계측이 끊기면 **멈춘 값을 지금 값으로 그리게** 되고, 그것이 화면이\n * 낼 수 있는 가장 나쁜 거짓이다(값이 그럴듯할수록 위험하다).\n */\n gear: {\n id: string\n kind: string\n live?: { generatedKW?: number; exportKW?: number; chargeKW?: number; dischargeKW?: number; soc?: number; measuredAt?: string }\n }[]\n children: TopologySegment[]\n}\n\nexport interface EnergyTopology {\n roots: TopologySegment[]\n /**\n * 자리를 모르는 설비 — 집이 없거나, 없는 자리를 가리킨다. 숨기면 그림의 설비 수가 모델과 달라진다.\n */\n unplaced: { id: string; kind: string }[]\n /** 계량기가 없는 자리 id 들 — 「여기에 달면 무엇을 알 수 있나」의 답이 되는 목록이다. */\n unmetered: string[]\n /**\n * 결선을 모르는 부하가 매달린 자리 — 커넥터가 「어느 분기가 이 설비에 전기를 주는지 모른다」고 판단해\n * 수전에 매단 경우다. 그 자리의 몫이 분기로 내려가지 않는 이유가 이것이다.\n */\n loadsOnIncoming: number\n}\n\n/**\n * 이 설비의 **지금 값** — 스냅샷에서 그 설비를 찾아 에너지 축의 값만 옮긴다.\n *\n * 종류가 아는 필드만 옮긴다: 태양광에 충전율을 붙이거나 축전지에 발전량을 붙이면, 화면은 그것을 사실로\n * 그리고 사람은 그 설비가 그런 일을 한다고 읽는다(커널이 종류별로 필드를 가려 받는 것과 같은 규율).\n */\nfunction liveOf(\n state: readonly any[] | undefined | null,\n id: string,\n kind: string\n): { live: NonNullable<TopologySegment['gear'][number]['live']> } | undefined {\n const e = (state ?? []).find(x => String(x?.id ?? '') === id)\n if (!e) return undefined\n const num = (v: unknown): number | undefined => (Number.isFinite(Number(v)) ? Number(v) : undefined)\n const live: NonNullable<TopologySegment['gear'][number]['live']> = {}\n if (kind === 'pv-array') {\n const g = num(e.generatedKW)\n const x = num(e.exportKW)\n if (g !== undefined) live.generatedKW = g\n if (x !== undefined) live.exportKW = x\n } else if (kind === 'battery') {\n const soc = num(e.soc)\n const dis = num(e.dischargeKW)\n const cha = num(e.chargeKW)\n if (soc !== undefined) live.soc = soc\n if (dis !== undefined) live.dischargeKW = dis\n if (cha !== undefined) live.chargeKW = cha\n }\n if (!Object.keys(live).length) return undefined\n /* 값이 있으면 시각도 함께 — 없으면 붙이지 않는다(모르는 시각을 지금으로 만들지 않는다). */\n if (e.measuredAt) live.measuredAt = String(e.measuredAt)\n return { live }\n}\n\nconst ELECTRICAL = new Set(EMS_LOCATION_TYPES as readonly string[])\n/** 부하가 아닌 계통 설비 — 이것들의 소비를 부하로 세면 총량이 이중으로 나뉜다. */\nconst GEAR = new Set(['meter', 'breaker', 'pv-array', 'battery'])\n\n/**\n * 모델 + 폴드 결과 → 계통 트리.\n *\n * @param model 트윈 모델(자리 계층 `parentId`, 설비 소속 `homeLocation`).\n * @param energy 폴드가 낸 분기별 몫(있으면 노드에 싣는다 — 없으면 값 없이 구조만 그린다).\n */\nexport function buildEnergyTopology(\n model: { locations?: any[]; equipment?: any[] } | undefined | null,\n energy?: { byFeeder?: { key: string; kWh: number; basis?: 'measured' | 'apportioned'; peakKW?: number }[] } | null,\n /**\n * 지금의 설비 상태(스냅샷의 `equipment`) — 발전·저장의 **지금 값**을 기호 옆에 적기 위해서다.\n *\n * 없으면 값 없이 구조만 그린다. 여기서 값을 만들지 않는다: 원천이 보내지 않은 것은 모르는 것이다.\n */\n equipmentState?: readonly any[] | null\n): EnergyTopology {\n const locations: any[] = (model?.locations ?? []).filter(n => n?.id && ELECTRICAL.has(String(n.type)))\n const byId = new Map<string, TopologySegment>()\n for (const n of locations) {\n byId.set(String(n.id), {\n id: String(n.id),\n type: String(n.type),\n ...(n.name ? { label: String(n.name) } : {}),\n meters: [],\n loads: [],\n gear: [],\n children: []\n })\n }\n\n const share = new Map((energy?.byFeeder ?? []).map(f => [f.key, f]))\n for (const [id, segment] of byId) {\n const s = share.get(id)\n if (s) {\n segment.kWh = s.kWh\n if (s.basis) segment.basis = s.basis\n if (typeof s.peakKW === 'number') segment.peakKW = s.peakKW\n }\n }\n\n /* 설비를 자기 자리에 붙인다 — 계량기는 「이 구간을 잰다」는 뜻이라 따로 모은다. */\n const unplaced: { id: string; kind: string }[] = []\n let loadsOnIncoming = 0\n for (const e of (model?.equipment ?? []) as any[]) {\n if (!e?.id) continue\n const kind = String(e.kind ?? '')\n const home = String(e.homeLocation ?? e.homeLocationId ?? '')\n const at = byId.get(home)\n if (!at) {\n /* 전기 계통 밖의 자리에 사는 설비도 여기서는 「자리를 모른다」로 모은다 — 이 그림의 축은 계통이다. */\n unplaced.push({ id: String(e.id), kind })\n continue\n }\n if (kind === 'meter') at.meters.push(String(e.id))\n else if (GEAR.has(kind)) at.gear.push({ id: String(e.id), kind, ...(liveOf(equipmentState, String(e.id), kind) ?? {}) })\n else {\n at.loads.push({ id: String(e.id), kind })\n if (at.type === 'incoming') loadsOnIncoming++\n }\n }\n\n /*\n * 계층을 잇는 것은 **전기 상류**다 — 공간 부모(공장·구역)는 이 축이 아니다. 상류의 뜻은 커널이 읽는다\n * (옛 세대는 그 안에서 흡수된다) — 여기서 다시 판정하면 화면과 계산이 다른 계통을 말하게 된다.\n */\n const roots: TopologySegment[] = []\n const all = model?.locations ?? []\n for (const n of locations) {\n const segment = byId.get(String(n.id))!\n const up = electricalUpstreamOf(all as any, String(n.id))\n const parent = up ? byId.get(up) : undefined\n if (parent && parent !== segment) parent.children.push(segment)\n else roots.push(segment)\n }\n\n const unmetered = [...byId.values()].filter(n => !n.meters.length).map(n => n.id)\n return { roots, unplaced, unmetered, loadsOnIncoming }\n}\n"]}
1
+ {"version":3,"file":"energy-topology.js","sourceRoot":"","sources":["../../server/engine/energy-topology.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 전력 계통도 — **선언된 것만 그린다.** 순수.\n *\n * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────\n * 분기별 막대는 「어디가 많이 쓰나」에 답하지만 **어디에 걸려 있나**에 답하지 못한다. 실제로 부딪힌\n * 세 가지가 전부 계통 질문이었다:\n * · 「하위 계량이 덮지 않는 327 kWh」 — 그 몫이 수전 직결 부하인지, 계량 없는 구간인지 표로는 모른다.\n * · 커넥터는 결선을 모르는 부하를 **수전에 매단다**(정직한 선택). 그런데 그 사실이 화면에 없었다.\n * · 「계량기를 어디에 더 달아야 하나」 — 어두운 구간이 보여야 답할 수 있다.\n *\n * ── 무엇을 그리지 않나 ─────────────────────────────────────────────────────\n * **보호 계통 단선도(SLD)가 아니다.** 차단기 정격·보호 협조·상(phase)·결선 방식은 모델에 없고, 그럴듯한\n * 단선도를 그리면 갖지 않은 공학적 충실도를 주장하게 된다. 여기서 그리는 것은 **트윈이 선언한 관계**\n * (자리의 계층 + 설비의 소속)와 **계량이 실제로 덮는 범위**뿐이다. 한계는 소비처가 화면에 적는다.\n *\n * ── 알리지 않고 빠뜨리지 않는다 ─────────────────────────────────────────────────\n * 자리를 모르는 설비, 계량기가 없는 자리, 계량이 덮지 않는 몫은 **각자 자기 칸으로** 나온다. 그림에서\n * 사라지면 사용자는 자기 현장이 다 그려졌다고 믿는다.\n */\nimport { EMS_LOCATION_TYPES, electricalUpstreamOf } from '@operato/ops-contract'\n\n/** 계통의 한 구간(수전·분기·구역) — 자리 하나가 곧 구간이다. */\nexport interface TopologySegment {\n id: string\n /** 카탈로그 자리 타입(`incoming`·`feeder`·`submeter-zone`). */\n type: string\n label?: string\n /** 이 자리에 달린 계량 지점 id 들 — 비어 있으면 **어두운 구간**이다(값을 알 길이 없다). */\n meters: string[]\n /** 이 구간의 전력량과 근거 — 폴드가 낸 것만 싣는다(우리가 다시 계산하지 않는다). */\n kWh?: number\n basis?: 'measured' | 'apportioned'\n /**\n * **피크가 찍힌 순간** 이 구간이 얼마였나(kW) — 요금이 걸린 수다.\n *\n * 전력량이 큰 구간과 피크를 만든 구간은 다를 수 있다(하루 종일 조금씩 쓰는 부하 vs 잠깐 크게 쓰는 부하).\n * 그림에 둘을 함께 두면 「어느 분기를 깎아야 피크가 내려가나」가 바로 읽힌다.\n */\n peakKW?: number\n /** 이 자리에 매달린 부하(설비) — 종류를 그대로 싣는다. */\n loads: { id: string; kind: string }[]\n /**\n * 계통 설비(차단기·태양광·축전지) — 부하가 아니라 계통의 일부다.\n *\n * `live` 는 **지금 값**이다(구간 값이 아니다): 태양광이 지금 몇 kW 를 만들고, 축전지가 얼마를 내보내며\n * 충전율이 얼마인가. 원천이 보내 주는 사실이라 우리가 만들지 않는다 — 보내지 않은 필드는 없다.\n *\n * 시각(`measuredAt`)을 함께 나른다: 계측이 끊기면 **멈춘 값을 지금 값으로 그리게** 되고, 그것이 화면이\n * 낼 수 있는 가장 나쁜 거짓이다(값이 그럴듯할수록 위험하다).\n */\n gear: {\n id: string\n kind: string\n live?: { generatedKW?: number; exportKW?: number; chargeKW?: number; dischargeKW?: number; soc?: number; measuredAt?: string }\n }[]\n children: TopologySegment[]\n}\n\nexport interface EnergyTopology {\n roots: TopologySegment[]\n /**\n * 자리를 모르는 설비 — 집이 없거나, 없는 자리를 가리킨다. 숨기면 그림의 설비 수가 모델과 달라진다.\n */\n unplaced: { id: string; kind: string }[]\n /** 계량기가 없는 자리 id 들 — 「여기에 달면 무엇을 알 수 있나」의 답이 되는 목록이다. */\n unmetered: string[]\n /**\n * 결선을 모르는 부하가 매달린 자리 — 커넥터가 「어느 분기가 이 설비에 전기를 주는지 모른다」고 판단해\n * 수전에 매단 경우다. 그 자리의 몫이 분기로 내려가지 않는 이유가 이것이다.\n */\n loadsOnIncoming: number\n}\n\n/**\n * 이 설비의 **지금 값** — 스냅샷에서 그 설비를 찾아 에너지 축의 값만 옮긴다.\n *\n * 종류가 아는 필드만 옮긴다: 태양광에 충전율을 붙이거나 축전지에 발전량을 붙이면, 화면은 그것을 사실로\n * 그리고 사람은 그 설비가 그런 일을 한다고 읽는다(커널이 종류별로 필드를 가려 받는 것과 같은 규율).\n */\nfunction liveOf(\n state: readonly any[] | undefined | null,\n id: string,\n kind: string\n): { live: NonNullable<TopologySegment['gear'][number]['live']> } | undefined {\n const e = (state ?? []).find(x => String(x?.id ?? '') === id)\n if (!e) return undefined\n const num = (v: unknown): number | undefined => (Number.isFinite(Number(v)) ? Number(v) : undefined)\n const live: NonNullable<TopologySegment['gear'][number]['live']> = {}\n if (kind === 'pv-array') {\n const g = num(e.generatedKW)\n const x = num(e.exportKW)\n if (g !== undefined) live.generatedKW = g\n if (x !== undefined) live.exportKW = x\n } else if (kind === 'battery') {\n const soc = num(e.soc)\n const dis = num(e.dischargeKW)\n const cha = num(e.chargeKW)\n if (soc !== undefined) live.soc = soc\n if (dis !== undefined) live.dischargeKW = dis\n if (cha !== undefined) live.chargeKW = cha\n }\n if (!Object.keys(live).length) return undefined\n /* 값이 있으면 시각도 함께 — 없으면 붙이지 않는다(모르는 시각을 지금으로 만들지 않는다). */\n if (e.measuredAt) live.measuredAt = String(e.measuredAt)\n return { live }\n}\n\nconst ELECTRICAL = new Set(EMS_LOCATION_TYPES as readonly string[])\n/** 부하가 아닌 계통 설비 — 이것들의 소비를 부하로 세면 총량이 이중으로 나뉜다. */\nconst GEAR = new Set(['meter', 'breaker', 'pv-array', 'battery'])\n\n/**\n * 모델 + 폴드 결과 → 계통 트리.\n *\n * @param model 트윈 모델(자리 계층 `parentId`, 설비 소속 `homeLocation`).\n * @param energy 폴드가 낸 분기별 몫(있으면 노드에 싣는다 — 없으면 값 없이 구조만 그린다).\n */\nexport function buildEnergyTopology(\n model: { locations?: any[]; equipment?: any[] } | undefined | null,\n energy?: { byFeeder?: { key: string; kWh: number; basis?: 'measured' | 'apportioned'; peakKW?: number }[] } | null,\n /**\n * 지금의 설비 상태(스냅샷의 `equipment`) — 발전·저장의 **지금 값**을 기호 옆에 적기 위해서다.\n *\n * 없으면 값 없이 구조만 그린다. 여기서 값을 만들지 않는다: 원천이 보내지 않은 것은 모르는 것이다.\n */\n equipmentState?: readonly any[] | null\n): EnergyTopology {\n const locations: any[] = (model?.locations ?? []).filter(n => n?.id && ELECTRICAL.has(String(n.type)))\n const byId = new Map<string, TopologySegment>()\n for (const n of locations) {\n byId.set(String(n.id), {\n id: String(n.id),\n type: String(n.type),\n ...(n.name ? { label: String(n.name) } : {}),\n meters: [],\n loads: [],\n gear: [],\n children: []\n })\n }\n\n const share = new Map((energy?.byFeeder ?? []).map(f => [f.key, f]))\n for (const [id, segment] of byId) {\n const s = share.get(id)\n if (s) {\n segment.kWh = s.kWh\n if (s.basis) segment.basis = s.basis\n if (typeof s.peakKW === 'number') segment.peakKW = s.peakKW\n }\n }\n\n /* 설비를 자기 자리에 붙인다 — 계량기는 「이 구간을 잰다」는 뜻이라 따로 모은다. */\n const unplaced: { id: string; kind: string }[] = []\n let loadsOnIncoming = 0\n for (const e of (model?.equipment ?? []) as any[]) {\n if (!e?.id) continue\n const kind = String(e.kind ?? '')\n const home = String(e.homeLocation ?? e.homeLocationId ?? '')\n const at = byId.get(home)\n if (!at) {\n /* 전기 계통 밖의 자리에 사는 설비도 여기서는 「자리를 모른다」로 모은다 — 이 그림의 축은 계통이다. */\n unplaced.push({ id: String(e.id), kind })\n continue\n }\n if (kind === 'meter') at.meters.push(String(e.id))\n else if (GEAR.has(kind)) at.gear.push({ id: String(e.id), kind, ...(liveOf(equipmentState, String(e.id), kind) ?? {}) })\n else {\n at.loads.push({ id: String(e.id), kind })\n if (at.type === 'incoming') loadsOnIncoming++\n }\n }\n\n /*\n * 계층을 잇는 것은 **전기 상류**다 — 공간 부모(공장·구역)는 이 축이 아니다. 상류의 뜻은 커널이 읽는다\n * (옛 세대는 그 안에서 흡수된다) — 여기서 다시 판정하면 화면과 계산이 다른 계통을 말하게 된다.\n */\n const roots: TopologySegment[] = []\n const all = model?.locations ?? []\n for (const n of locations) {\n const segment = byId.get(String(n.id))!\n const up = electricalUpstreamOf(all as any, String(n.id))\n const parent = up ? byId.get(up) : undefined\n if (parent && parent !== segment) parent.children.push(segment)\n else roots.push(segment)\n }\n\n const unmetered = [...byId.values()].filter(n => !n.meters.length).map(n => n.id)\n return { roots, unplaced, unmetered, loadsOnIncoming }\n}\n"]}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 이 봉투가 말하는 사실의 정체 — 같으면 같은 사실이다.
3
+ *
4
+ * 대상이나 시각을 알 수 없으면 **정체를 만들지 않는다**(`undefined`). 그때는 거르지 않고 통과시킨다 —
5
+ * 모르는 것을 같다고 판단하면 서로 다른 사실이 하나로 합쳐진다. 알리지 않고 잃는 것보다 두 번 세는 것이 낫다.
6
+ */
7
+ export declare function factIdentity(envelope: any): string | undefined;
8
+ /**
9
+ * 최근에 본 정체를 기억한다 — **크기에 상한을 둔다.**
10
+ *
11
+ * 전부 기억할 수는 없다(트윈 하나가 수천만 사건을 갖는다). 창을 넘으면 오래된 것부터 잊는다. 창 밖의
12
+ * 재전송은 다시 통과하는데, 그것은 절대값 사실이라 상태에 해가 없고 지난 기록에 한 줄 더 남는다.
13
+ * 창을 크게 잡으면 그만큼 메모리를 쓴다 — 그 값을 여기서 이름으로 못 박는다.
14
+ *
15
+ * **재기동을 건너지 못한다.** 프로세스가 새로 뜨면 기억이 비어 있으므로, 재기동 직후 첫 주기가 같은
16
+ * 값을 다시 보내면 걸러지지 않는다. 그것을 막으려면 이 기억이 저장돼야 하고, 그것은 다음 걸음이다.
17
+ * 지금은 그 사실을 여기 적어 둔다 — 적어 두지 않으면 「중복은 이제 없다」로 읽힌다.
18
+ */
19
+ export declare class FactDeduper {
20
+ /** 기억하는 정체의 수 — 창. */
21
+ static WINDOW: number;
22
+ private seen;
23
+ private order;
24
+ /** 이 사실을 본 적이 있나 — 없으면 기억하고 `false`. */
25
+ private known;
26
+ /**
27
+ * 처음 보는 것만 돌려준다 — 그리고 몇 개를 버렸는지 함께 낸다.
28
+ *
29
+ * 버린 수를 함께 내는 이유: 알리지 않고 버리면 「보낸 수」와 「반영된 수」가 어긋나는데 아무도 그
30
+ * 이유를 모른다. 그 수가 크면 보내는 쪽이 되풀이 보내고 있다는 뜻이고, 그것은 고칠 일이다.
31
+ */
32
+ filter<T>(envelopes: readonly T[]): {
33
+ fresh: T[];
34
+ duplicates: number;
35
+ };
36
+ /** 기억하고 있는 정체의 수 — 계기가 읽는다. */
37
+ size(): number;
38
+ }
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ /*
3
+ * 같은 사실을 두 번 받으면 한 번만 반영한다 — **유입 경계 한 곳에서.**
4
+ *
5
+ * ── 왜 필요한가 (2026-08-26) ────────────────────────────────────────────────
6
+ * 지금 중복을 막는 것은 커넥터마다 각자 하고 있다. 그래서 커넥터가 그것을 잊으면 트윈이 그대로 두 번
7
+ * 센다. 실제로 그랬다: 태양광 커넥터가 10초마다 물으면서 10분마다 바뀌는 값을 매번 다시 보냈고,
8
+ * 한 주기에 76건이 되풀이됐다(커넥터에서 고쳐서 2건이 됐다).
9
+ *
10
+ * 그리고 이것은 웹훅으로 받을 때 더 자주 난다. 밀어 주는 방식은 재전송이 정상이다 — 그쪽이 응답을
11
+ * 못 받으면 같은 것을 다시 보낸다.
12
+ *
13
+ * ── 무엇이 두 번 세어지나 ──────────────────────────────────────────────────
14
+ * 절대값으로 말하는 사실은 여러 번 반영해도 결과가 같다(잔량 · 상태 · 적산 · 순간 kW). 그런데 세는
15
+ * 값은 그렇지 않다.
16
+ *
17
+ * 구간의 표본 수 · 평균 부하(표본 수로 나눈다) · OEE 카운터 · 유입 계수
18
+ * 그리고 지난 기록에 같은 사실이 두 줄로 남는다 — 나중에 그 기록으로 다시 계산하면 또 두 번 센다
19
+ *
20
+ * ── 정체를 무엇으로 보나 ────────────────────────────────────────────────────
21
+ * 봉투의 `eventId` 로는 판단할 수 없다. 보낸 쪽이 매번 새로 만드는 순번이고(`…-energy-1` 처럼 호출마다
22
+ * 1부터 다시 시작한다), 같은 사실을 두 번 보내면 다른 번호가 붙는다. 다른 사실에 같은 번호가 붙는 일도
23
+ * 생긴다.
24
+ *
25
+ * 그래서 **사실 자체로** 판단한다.
26
+ *
27
+ * 무엇에 대한 것인가 설비 · 계량 지점 · 물품 · 작업 · 오더 · 자리
28
+ * 어떤 종류인가 사건 종류(eventType)
29
+ * 언제 잰 것인가 연결된 시스템이 준 시각
30
+ *
31
+ * 이 셋이 같으면 같은 사실이다. 태양광 커넥터가 `collectedAt` 으로 판단한 것과 같은 방식이고, 그것을
32
+ * 커넥터마다 하지 않도록 여기로 올린다.
33
+ *
34
+ * ── 정확히 같은 것만 버린다 ─────────────────────────────────────────────────
35
+ * 늦게 도착한 옛 사실은 버리지 않는다. 그것이 왔다는 것 자체가 사실이고, 상태에 반영되지 않는 것은
36
+ * 커널의 순서 판단이 이미 맡는다. 여기서 버리는 것은 **정체가 완전히 같은 것**뿐이다.
37
+ */
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.FactDeduper = void 0;
40
+ exports.factIdentity = factIdentity;
41
+ /** 시각을 밀리초로 — 연결된 시스템이 준 시각이 먼저, 없으면 봉투의 시각. */
42
+ function timeOf(envelope) {
43
+ for (const v of [envelope?.data?.at, envelope?.data?.eventTime, envelope?.eventTime]) {
44
+ const t = Date.parse(String(v ?? ''));
45
+ if (Number.isFinite(t))
46
+ return t;
47
+ }
48
+ return undefined;
49
+ }
50
+ /**
51
+ * 이 봉투가 말하는 대상 — 어휘마다 이름이 다르므로 아는 이름을 모두 본다.
52
+ *
53
+ * 순서가 뜻을 갖는다: 주체를 먼저 본다. 설비 사실은 「지금 붙어 있는 작업」을 함께 싣고, 작업 사실은
54
+ * 「소속 오더」를 함께 싣는다. 아무 이름이나 먼저 보면 참조를 주체로 읽는다(승격 키에서 겪은 것과 같다).
55
+ */
56
+ function targetOf(envelope) {
57
+ const d = envelope?.data ?? {};
58
+ const first = (...vals) => {
59
+ for (const v of vals) {
60
+ const s = String(v ?? '').trim();
61
+ if (s)
62
+ return s;
63
+ }
64
+ return undefined;
65
+ };
66
+ return first(d.meterId, d.equipmentId, d.moverId, d.taskId, d.orderId, d.personId, d.assetId, d.locationId, d.epcList?.[0], d.parentID, d.quantityList?.[0]?.epcClass, d.transformationID);
67
+ }
68
+ /**
69
+ * 이 봉투가 말하는 사실의 정체 — 같으면 같은 사실이다.
70
+ *
71
+ * 대상이나 시각을 알 수 없으면 **정체를 만들지 않는다**(`undefined`). 그때는 거르지 않고 통과시킨다 —
72
+ * 모르는 것을 같다고 판단하면 서로 다른 사실이 하나로 합쳐진다. 알리지 않고 잃는 것보다 두 번 세는 것이 낫다.
73
+ */
74
+ function factIdentity(envelope) {
75
+ const type = String(envelope?.eventType ?? '').trim();
76
+ if (!type)
77
+ return undefined;
78
+ const target = targetOf(envelope);
79
+ if (!target)
80
+ return undefined;
81
+ const at = timeOf(envelope);
82
+ if (at === undefined)
83
+ return undefined;
84
+ return `${type}|${target}|${at}`;
85
+ }
86
+ /**
87
+ * 최근에 본 정체를 기억한다 — **크기에 상한을 둔다.**
88
+ *
89
+ * 전부 기억할 수는 없다(트윈 하나가 수천만 사건을 갖는다). 창을 넘으면 오래된 것부터 잊는다. 창 밖의
90
+ * 재전송은 다시 통과하는데, 그것은 절대값 사실이라 상태에 해가 없고 지난 기록에 한 줄 더 남는다.
91
+ * 창을 크게 잡으면 그만큼 메모리를 쓴다 — 그 값을 여기서 이름으로 못 박는다.
92
+ *
93
+ * **재기동을 건너지 못한다.** 프로세스가 새로 뜨면 기억이 비어 있으므로, 재기동 직후 첫 주기가 같은
94
+ * 값을 다시 보내면 걸러지지 않는다. 그것을 막으려면 이 기억이 저장돼야 하고, 그것은 다음 걸음이다.
95
+ * 지금은 그 사실을 여기 적어 둔다 — 적어 두지 않으면 「중복은 이제 없다」로 읽힌다.
96
+ */
97
+ class FactDeduper {
98
+ constructor() {
99
+ this.seen = new Set();
100
+ this.order = [];
101
+ }
102
+ /** 기억하는 정체의 수 — 창. */
103
+ static { this.WINDOW = 20_000; }
104
+ /** 이 사실을 본 적이 있나 — 없으면 기억하고 `false`. */
105
+ known(identity) {
106
+ if (this.seen.has(identity))
107
+ return true;
108
+ this.seen.add(identity);
109
+ this.order.push(identity);
110
+ if (this.order.length > FactDeduper.WINDOW) {
111
+ const gone = this.order.splice(0, this.order.length - FactDeduper.WINDOW);
112
+ for (const g of gone)
113
+ this.seen.delete(g);
114
+ }
115
+ return false;
116
+ }
117
+ /**
118
+ * 처음 보는 것만 돌려준다 — 그리고 몇 개를 버렸는지 함께 낸다.
119
+ *
120
+ * 버린 수를 함께 내는 이유: 알리지 않고 버리면 「보낸 수」와 「반영된 수」가 어긋나는데 아무도 그
121
+ * 이유를 모른다. 그 수가 크면 보내는 쪽이 되풀이 보내고 있다는 뜻이고, 그것은 고칠 일이다.
122
+ */
123
+ filter(envelopes) {
124
+ const fresh = [];
125
+ let duplicates = 0;
126
+ for (const e of envelopes) {
127
+ const id = factIdentity(e);
128
+ if (id === undefined) {
129
+ fresh.push(e);
130
+ continue;
131
+ }
132
+ if (this.known(id))
133
+ duplicates++;
134
+ else
135
+ fresh.push(e);
136
+ }
137
+ return { fresh, duplicates };
138
+ }
139
+ /** 기억하고 있는 정체의 수 — 계기가 읽는다. */
140
+ size() {
141
+ return this.seen.size;
142
+ }
143
+ }
144
+ exports.FactDeduper = FactDeduper;
145
+ //# sourceMappingURL=ingest-dedupe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest-dedupe.js","sourceRoot":"","sources":["../../server/engine/ingest-dedupe.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 같은 사실을 두 번 받으면 한 번만 반영한다 — **유입 경계 한 곳에서.**\n *\n * ── 왜 필요한가 (2026-08-26) ────────────────────────────────────────────────\n * 지금 중복을 막는 것은 커넥터마다 각자 하고 있다. 그래서 커넥터가 그것을 잊으면 트윈이 그대로 두 번\n * 센다. 실제로 그랬다: 태양광 커넥터가 10초마다 물으면서 10분마다 바뀌는 값을 매번 다시 보냈고,\n * 한 주기에 76건이 되풀이됐다(커넥터에서 고쳐서 2건이 됐다).\n *\n * 그리고 이것은 웹훅으로 받을 때 더 자주 난다. 밀어 주는 방식은 재전송이 정상이다 — 그쪽이 응답을\n * 못 받으면 같은 것을 다시 보낸다.\n *\n * ── 무엇이 두 번 세어지나 ──────────────────────────────────────────────────\n * 절대값으로 말하는 사실은 여러 번 반영해도 결과가 같다(잔량 · 상태 · 적산 · 순간 kW). 그런데 세는\n * 값은 그렇지 않다.\n *\n * 구간의 표본 수 · 평균 부하(표본 수로 나눈다) · OEE 카운터 · 유입 계수\n * 그리고 지난 기록에 같은 사실이 두 줄로 남는다 — 나중에 그 기록으로 다시 계산하면 또 두 번 센다\n *\n * ── 정체를 무엇으로 보나 ────────────────────────────────────────────────────\n * 봉투의 `eventId` 로는 판단할 수 없다. 보낸 쪽이 매번 새로 만드는 순번이고(`…-energy-1` 처럼 호출마다\n * 1부터 다시 시작한다), 같은 사실을 두 번 보내면 다른 번호가 붙는다. 다른 사실에 같은 번호가 붙는 일도\n * 생긴다.\n *\n * 그래서 **사실 자체로** 판단한다.\n *\n * 무엇에 대한 것인가 설비 · 계량 지점 · 물품 · 작업 · 오더 · 자리\n * 어떤 종류인가 사건 종류(eventType)\n * 언제 잰 것인가 연결된 시스템이 준 시각\n *\n * 이 셋이 같으면 같은 사실이다. 태양광 커넥터가 `collectedAt` 으로 판단한 것과 같은 방식이고, 그것을\n * 커넥터마다 하지 않도록 여기로 올린다.\n *\n * ── 정확히 같은 것만 버린다 ─────────────────────────────────────────────────\n * 늦게 도착한 옛 사실은 버리지 않는다. 그것이 왔다는 것 자체가 사실이고, 상태에 반영되지 않는 것은\n * 커널의 순서 판단이 이미 맡는다. 여기서 버리는 것은 **정체가 완전히 같은 것**뿐이다.\n */\n\n/** 시각을 밀리초로 — 연결된 시스템이 준 시각이 먼저, 없으면 봉투의 시각. */\nfunction timeOf(envelope: any): number | undefined {\n for (const v of [envelope?.data?.at, envelope?.data?.eventTime, envelope?.eventTime]) {\n const t = Date.parse(String(v ?? ''))\n if (Number.isFinite(t)) return t\n }\n return undefined\n}\n\n/**\n * 이 봉투가 말하는 대상 — 어휘마다 이름이 다르므로 아는 이름을 모두 본다.\n *\n * 순서가 뜻을 갖는다: 주체를 먼저 본다. 설비 사실은 「지금 붙어 있는 작업」을 함께 싣고, 작업 사실은\n * 「소속 오더」를 함께 싣는다. 아무 이름이나 먼저 보면 참조를 주체로 읽는다(승격 키에서 겪은 것과 같다).\n */\nfunction targetOf(envelope: any): string | undefined {\n const d = envelope?.data ?? {}\n const first = (...vals: unknown[]) => {\n for (const v of vals) {\n const s = String(v ?? '').trim()\n if (s) return s\n }\n return undefined\n }\n return first(\n d.meterId,\n d.equipmentId,\n d.moverId,\n d.taskId,\n d.orderId,\n d.personId,\n d.assetId,\n d.locationId,\n d.epcList?.[0],\n d.parentID,\n d.quantityList?.[0]?.epcClass,\n d.transformationID\n )\n}\n\n/**\n * 이 봉투가 말하는 사실의 정체 — 같으면 같은 사실이다.\n *\n * 대상이나 시각을 알 수 없으면 **정체를 만들지 않는다**(`undefined`). 그때는 거르지 않고 통과시킨다 —\n * 모르는 것을 같다고 판단하면 서로 다른 사실이 하나로 합쳐진다. 알리지 않고 잃는 것보다 두 번 세는 것이 낫다.\n */\nexport function factIdentity(envelope: any): string | undefined {\n const type = String(envelope?.eventType ?? '').trim()\n if (!type) return undefined\n const target = targetOf(envelope)\n if (!target) return undefined\n const at = timeOf(envelope)\n if (at === undefined) return undefined\n return `${type}|${target}|${at}`\n}\n\n/**\n * 최근에 본 정체를 기억한다 — **크기에 상한을 둔다.**\n *\n * 전부 기억할 수는 없다(트윈 하나가 수천만 사건을 갖는다). 창을 넘으면 오래된 것부터 잊는다. 창 밖의\n * 재전송은 다시 통과하는데, 그것은 절대값 사실이라 상태에 해가 없고 지난 기록에 한 줄 더 남는다.\n * 창을 크게 잡으면 그만큼 메모리를 쓴다 — 그 값을 여기서 이름으로 못 박는다.\n *\n * **재기동을 건너지 못한다.** 프로세스가 새로 뜨면 기억이 비어 있으므로, 재기동 직후 첫 주기가 같은\n * 값을 다시 보내면 걸러지지 않는다. 그것을 막으려면 이 기억이 저장돼야 하고, 그것은 다음 걸음이다.\n * 지금은 그 사실을 여기 적어 둔다 — 적어 두지 않으면 「중복은 이제 없다」로 읽힌다.\n */\nexport class FactDeduper {\n /** 기억하는 정체의 수 — 창. */\n static WINDOW = 20_000\n\n private seen = new Set<string>()\n private order: string[] = []\n\n /** 이 사실을 본 적이 있나 — 없으면 기억하고 `false`. */\n private known(identity: string): boolean {\n if (this.seen.has(identity)) return true\n this.seen.add(identity)\n this.order.push(identity)\n if (this.order.length > FactDeduper.WINDOW) {\n const gone = this.order.splice(0, this.order.length - FactDeduper.WINDOW)\n for (const g of gone) this.seen.delete(g)\n }\n return false\n }\n\n /**\n * 처음 보는 것만 돌려준다 — 그리고 몇 개를 버렸는지 함께 낸다.\n *\n * 버린 수를 함께 내는 이유: 알리지 않고 버리면 「보낸 수」와 「반영된 수」가 어긋나는데 아무도 그\n * 이유를 모른다. 그 수가 크면 보내는 쪽이 되풀이 보내고 있다는 뜻이고, 그것은 고칠 일이다.\n */\n filter<T>(envelopes: readonly T[]): { fresh: T[]; duplicates: number } {\n const fresh: T[] = []\n let duplicates = 0\n for (const e of envelopes) {\n const id = factIdentity(e)\n if (id === undefined) {\n fresh.push(e)\n continue\n }\n if (this.known(id)) duplicates++\n else fresh.push(e)\n }\n return { fresh, duplicates }\n }\n\n /** 기억하고 있는 정체의 수 — 계기가 읽는다. */\n size(): number {\n return this.seen.size\n }\n}\n"]}