@things-factory/headless-twin 10.0.14 → 10.0.17

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 (478) 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 +11 -2
  5. package/dist-server/engine/canonical-ingest.js +137 -8
  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 +1 -1
  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 +124 -9
  17. package/dist-server/engine/ingest-health.js +169 -15
  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 +122 -12
  25. package/dist-server/engine/kpi-fold.js +247 -18
  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 +185 -28
  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 +9 -9
  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 +1 -1
  52. package/dist-server/engine/model-vocabulary.js.map +1 -1
  53. package/dist-server/engine/oee-accumulator.d.ts +4 -4
  54. package/dist-server/engine/oee-accumulator.js +3 -3
  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 +243 -53
  78. package/dist-server/engine/twin-engine.js +690 -191
  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 +27 -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 +140 -8
  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 +88 -13
  131. package/dist-server/service/reference/reference-live.js.map +1 -1
  132. package/dist-server/service/reference/reference-master.d.ts +14 -14
  133. package/dist-server/service/reference/reference-master.js +34 -12
  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 +10 -10
  137. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  138. package/dist-server/service/reference/twin-reference.d.ts +3 -3
  139. package/dist-server/service/reference/twin-reference.js.map +1 -1
  140. package/dist-server/service/twin-attention/twin-attention-query.d.ts +1 -1
  141. package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
  142. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  143. package/dist-server/service/twin-audit/command-audit.d.ts +1 -1
  144. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  145. package/dist-server/service/twin-audit/twin-audit-event.d.ts +1 -1
  146. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  147. package/dist-server/service/twin-audit/twin-audit-query.js +1 -1
  148. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -1
  149. package/dist-server/service/twin-control/twin-control-mutation.js +1 -1
  150. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  151. package/dist-server/service/twin-event/twin-event-keys.d.ts +22 -2
  152. package/dist-server/service/twin-event/twin-event-keys.js +37 -5
  153. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  154. package/dist-server/service/twin-event/twin-event-type.d.ts +1 -0
  155. package/dist-server/service/twin-event/twin-event-type.js +8 -1
  156. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  157. package/dist-server/service/twin-event/twin-event.d.ts +2 -2
  158. package/dist-server/service/twin-event/twin-event.js +32 -4
  159. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  160. package/dist-server/service/twin-forecast/forecast-metrics.js +1 -1
  161. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -1
  162. package/dist-server/service/twin-forecast/gap-analytics.d.ts +1 -1
  163. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  164. package/dist-server/service/twin-forecast/twin-forecast-query.js +29 -11
  165. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  166. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +1 -1
  167. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +1 -1
  168. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -1
  169. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +1 -1
  170. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -1
  171. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  172. package/dist-server/service/twin-journal/event-type-split-shape.d.ts +50 -0
  173. package/dist-server/service/twin-journal/event-type-split-shape.js +111 -0
  174. package/dist-server/service/twin-journal/event-type-split-shape.js.map +1 -0
  175. package/dist-server/service/twin-journal/event-type-split.d.ts +15 -0
  176. package/dist-server/service/twin-journal/event-type-split.js +102 -0
  177. package/dist-server/service/twin-journal/event-type-split.js.map +1 -0
  178. package/dist-server/service/twin-journal/journal-order.d.ts +15 -0
  179. package/dist-server/service/twin-journal/journal-order.js +17 -0
  180. package/dist-server/service/twin-journal/journal-order.js.map +1 -0
  181. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -5
  182. package/dist-server/service/twin-journal/twin-journal-query.js +232 -21
  183. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  184. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +27 -1
  185. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +59 -3
  186. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  187. package/dist-server/service/twin-metrics/twin-metrics-query.js +14 -1
  188. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  189. package/dist-server/service/twin-model/axis-journal-evidence.js +1 -1
  190. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -1
  191. package/dist-server/service/twin-model/epcis-coverage.js +36 -1
  192. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  193. package/dist-server/service/twin-model/iec61850-coverage.js +1 -1
  194. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  195. package/dist-server/service/twin-model/isa95-coverage.d.ts +15 -2
  196. package/dist-server/service/twin-model/isa95-coverage.js +32 -8
  197. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  198. package/dist-server/service/twin-model/item-ref.js +2 -2
  199. package/dist-server/service/twin-model/item-ref.js.map +1 -1
  200. package/dist-server/service/twin-model/name-index.d.ts +1 -1
  201. package/dist-server/service/twin-model/name-index.js +1 -1
  202. package/dist-server/service/twin-model/name-index.js.map +1 -1
  203. package/dist-server/service/twin-model/project-structure.d.ts +1 -1
  204. package/dist-server/service/twin-model/project-structure.js +3 -3
  205. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  206. package/dist-server/service/twin-model/twin-equipment.d.ts +1 -1
  207. package/dist-server/service/twin-model/twin-equipment.js +1 -1
  208. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  209. package/dist-server/service/twin-model/twin-lineage-query.js +16 -5
  210. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  211. package/dist-server/service/twin-model/twin-location.d.ts +1 -1
  212. package/dist-server/service/twin-model/twin-location.js.map +1 -1
  213. package/dist-server/service/twin-model/twin-model-item-query.d.ts +3 -3
  214. package/dist-server/service/twin-model/twin-model-item-query.js +14 -14
  215. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  216. package/dist-server/service/twin-model/twin-model-mutation.d.ts +2 -2
  217. package/dist-server/service/twin-model/twin-model-mutation.js +4 -4
  218. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -1
  219. package/dist-server/service/twin-model/twin-model-query.js +117 -28
  220. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  221. package/dist-server/service/twin-model/twin-model-tree-query.js +22 -0
  222. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  223. package/dist-server/service/twin-model/twin-operation.d.ts +1 -1
  224. package/dist-server/service/twin-model/twin-operation.js +1 -1
  225. package/dist-server/service/twin-model/twin-operation.js.map +1 -1
  226. package/dist-server/service/twin-space/move-space.js +2 -2
  227. package/dist-server/service/twin-space/move-space.js.map +1 -1
  228. package/dist-server/service/twin-space/representation-role.d.ts +2 -0
  229. package/dist-server/service/twin-space/representation-role.js +18 -0
  230. package/dist-server/service/twin-space/representation-role.js.map +1 -0
  231. package/dist-server/service/twin-space/space-integrity.d.ts +1 -1
  232. package/dist-server/service/twin-space/space-integrity.js.map +1 -1
  233. package/dist-server/service/twin-space/twin-space-representation.d.ts +2 -0
  234. package/dist-server/service/twin-space/twin-space-representation.js +16 -0
  235. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  236. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  237. package/dist-server/service/twin-space/twin-space-resolver.js +36 -7
  238. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  239. package/dist-server/service/twin-structure/twin-structure.js +3 -3
  240. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  241. package/dist-server/service/twin-subject/event-subjects.d.ts +43 -0
  242. package/dist-server/service/twin-subject/event-subjects.js +137 -0
  243. package/dist-server/service/twin-subject/event-subjects.js.map +1 -0
  244. package/dist-server/service/twin-subject/index.d.ts +9 -0
  245. package/dist-server/service/twin-subject/index.js +16 -0
  246. package/dist-server/service/twin-subject/index.js.map +1 -0
  247. package/dist-server/service/twin-subject/item-resolver.d.ts +10 -0
  248. package/dist-server/service/twin-subject/item-resolver.js +93 -0
  249. package/dist-server/service/twin-subject/item-resolver.js.map +1 -0
  250. package/dist-server/service/twin-subject/subject-query-shape.d.ts +22 -0
  251. package/dist-server/service/twin-subject/subject-query-shape.js +88 -0
  252. package/dist-server/service/twin-subject/subject-query-shape.js.map +1 -0
  253. package/dist-server/service/twin-subject/twin-subject-backfill.d.ts +23 -0
  254. package/dist-server/service/twin-subject/twin-subject-backfill.js +159 -0
  255. package/dist-server/service/twin-subject/twin-subject-backfill.js.map +1 -0
  256. package/dist-server/service/twin-subject/twin-subject-event.d.ts +14 -0
  257. package/dist-server/service/twin-subject/twin-subject-event.js +111 -0
  258. package/dist-server/service/twin-subject/twin-subject-event.js.map +1 -0
  259. package/dist-server/service/twin-subject/twin-subject-fast-path.d.ts +39 -0
  260. package/dist-server/service/twin-subject/twin-subject-fast-path.js +295 -0
  261. package/dist-server/service/twin-subject/twin-subject-fast-path.js.map +1 -0
  262. package/dist-server/service/twin-subject/twin-subject-query.d.ts +27 -0
  263. package/dist-server/service/twin-subject/twin-subject-query.js +232 -0
  264. package/dist-server/service/twin-subject/twin-subject-query.js.map +1 -0
  265. package/dist-server/service/twin-subject/twin-subject-writer.d.ts +27 -0
  266. package/dist-server/service/twin-subject/twin-subject-writer.js +166 -0
  267. package/dist-server/service/twin-subject/twin-subject-writer.js.map +1 -0
  268. package/dist-server/service/twin-target/twin-target-resolver.js +1 -1
  269. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  270. package/dist-shared/entity-delta.d.ts +1 -1
  271. package/dist-shared/entity-delta.js +3 -3
  272. package/dist-shared/entity-delta.js.map +1 -1
  273. package/dist-shared/touched-items.js +2 -2
  274. package/dist-shared/touched-items.js.map +1 -1
  275. package/dist-shared/twin-level.js +1 -1
  276. package/dist-shared/twin-level.js.map +1 -1
  277. package/package.json +3 -3
  278. package/server/engine/attention-digest.ts +9 -9
  279. package/server/engine/canonical-ingest.ts +170 -13
  280. package/server/engine/command-routing.ts +1 -1
  281. package/server/engine/declared-stimulus.ts +1 -1
  282. package/server/engine/energy-topology.ts +1 -1
  283. package/server/engine/ingest-dedupe.ts +149 -0
  284. package/server/engine/ingest-health.ts +234 -20
  285. package/server/engine/integration-probes.ts +7 -7
  286. package/server/engine/kpi-baseline.ts +4 -4
  287. package/server/engine/kpi-fold.ts +395 -27
  288. package/server/engine/kpi-query.ts +212 -46
  289. package/server/engine/live-feed-registry.ts +1 -1
  290. package/server/engine/load-meter.ts +1 -1
  291. package/server/engine/local-declarations.ts +13 -13
  292. package/server/engine/log.ts +1 -1
  293. package/server/engine/measured-estimator.ts +1 -1
  294. package/server/engine/measured-yield.ts +1 -1
  295. package/server/engine/model-basis.ts +2 -2
  296. package/server/engine/model-gap.ts +3 -3
  297. package/server/engine/model-vocabulary.ts +1 -1
  298. package/server/engine/oee-accumulator.ts +4 -4
  299. package/server/engine/property-effects.ts +2 -2
  300. package/server/engine/read-time.ts +77 -0
  301. package/server/engine/restart-policy.ts +1 -1
  302. package/server/engine/runtime-key.ts +1 -1
  303. package/server/engine/spec-coverage.ts +3 -3
  304. package/server/engine/stage-path.ts +5 -5
  305. package/server/engine/state-axes.ts +2 -2
  306. package/server/engine/travel-estimator.ts +2 -2
  307. package/server/engine/twin-engine.ts +764 -201
  308. package/server/engine/warm-start.ts +6 -6
  309. package/server/index.ts +1 -1
  310. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +1 -1
  311. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +3 -3
  312. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +2 -2
  313. package/server/migrations/1786300000000-IndexEventTypeByTime.ts +48 -0
  314. package/server/migrations/1786400000000-IndexEventCreatedAt.ts +38 -0
  315. package/server/migrations/1786500000000-CreateTwinSubjectEvents.ts +93 -0
  316. package/server/migrations/index.ts +7 -1
  317. package/server/routes.ts +68 -5
  318. package/server/service/index.ts +4 -1
  319. package/server/service/reference/control-routing.ts +1 -1
  320. package/server/service/reference/discovery-result.ts +3 -3
  321. package/server/service/reference/hook-contract.ts +103 -0
  322. package/server/service/reference/ingest-space.ts +4 -4
  323. package/server/service/reference/knob-defaults.ts +4 -4
  324. package/server/service/reference/reference-adapter.ts +166 -9
  325. package/server/service/reference/reference-hook.ts +145 -0
  326. package/server/service/reference/reference-live.ts +92 -14
  327. package/server/service/reference/reference-master.ts +39 -17
  328. package/server/service/reference/reference-resolver.ts +10 -10
  329. package/server/service/reference/twin-reference.ts +3 -3
  330. package/server/service/twin-attention/twin-attention-query.ts +1 -1
  331. package/server/service/twin-audit/command-audit.ts +1 -1
  332. package/server/service/twin-audit/twin-audit-event.ts +2 -2
  333. package/server/service/twin-audit/twin-audit-query.ts +1 -1
  334. package/server/service/twin-control/twin-control-mutation.ts +1 -1
  335. package/server/service/twin-event/twin-event-keys.ts +37 -5
  336. package/server/service/twin-event/twin-event-type.ts +18 -1
  337. package/server/service/twin-event/twin-event.ts +32 -6
  338. package/server/service/twin-forecast/forecast-metrics.ts +1 -1
  339. package/server/service/twin-forecast/gap-analytics.ts +1 -1
  340. package/server/service/twin-forecast/twin-forecast-query.ts +30 -11
  341. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +1 -1
  342. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +1 -1
  343. package/server/service/twin-instance/twin-instance.ts +4 -4
  344. package/server/service/twin-journal/event-type-split-shape.ts +112 -0
  345. package/server/service/twin-journal/event-type-split.ts +144 -0
  346. package/server/service/twin-journal/journal-order.ts +20 -0
  347. package/server/service/twin-journal/twin-journal-query.ts +224 -20
  348. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +57 -3
  349. package/server/service/twin-metrics/twin-metrics-query.ts +14 -1
  350. package/server/service/twin-model/axis-journal-evidence.ts +1 -1
  351. package/server/service/twin-model/epcis-coverage.ts +36 -1
  352. package/server/service/twin-model/iec61850-coverage.ts +1 -1
  353. package/server/service/twin-model/isa95-coverage.ts +45 -8
  354. package/server/service/twin-model/item-ref.ts +2 -2
  355. package/server/service/twin-model/name-index.ts +1 -1
  356. package/server/service/twin-model/project-structure.ts +4 -4
  357. package/server/service/twin-model/twin-equipment.ts +2 -2
  358. package/server/service/twin-model/twin-lineage-query.ts +16 -5
  359. package/server/service/twin-model/twin-location.ts +1 -1
  360. package/server/service/twin-model/twin-model-item-query.ts +14 -14
  361. package/server/service/twin-model/twin-model-mutation.ts +4 -4
  362. package/server/service/twin-model/twin-model-query.ts +117 -28
  363. package/server/service/twin-model/twin-model-tree-query.ts +25 -1
  364. package/server/service/twin-model/twin-operation.ts +2 -2
  365. package/server/service/twin-space/move-space.ts +3 -3
  366. package/server/service/twin-space/representation-role.ts +15 -0
  367. package/server/service/twin-space/space-integrity.ts +1 -1
  368. package/server/service/twin-space/twin-space-representation.ts +37 -0
  369. package/server/service/twin-space/twin-space-resolver.ts +39 -7
  370. package/server/service/twin-structure/twin-structure.ts +3 -3
  371. package/server/service/twin-subject/event-subjects.ts +155 -0
  372. package/server/service/twin-subject/index.ts +10 -0
  373. package/server/service/twin-subject/item-resolver.ts +92 -0
  374. package/server/service/twin-subject/subject-query-shape.ts +84 -0
  375. package/server/service/twin-subject/twin-subject-backfill.ts +196 -0
  376. package/server/service/twin-subject/twin-subject-event.ts +101 -0
  377. package/server/service/twin-subject/twin-subject-fast-path.ts +346 -0
  378. package/server/service/twin-subject/twin-subject-query.ts +210 -0
  379. package/server/service/twin-subject/twin-subject-writer.ts +189 -0
  380. package/server/service/twin-target/twin-target-resolver.ts +1 -1
  381. package/shared/entity-delta.ts +3 -3
  382. package/shared/touched-items.ts +2 -2
  383. package/shared/twin-level.ts +1 -1
  384. package/test/adopt-structure-live.test.ts +3 -3
  385. package/test/aggregate-not-findone.test.ts +112 -0
  386. package/test/attention-digest.test.ts +6 -6
  387. package/test/axis-read.test.ts +2 -2
  388. package/test/boot-resume.test.ts +20 -7
  389. package/test/broadcast-period.test.ts +2 -2
  390. package/test/broadcast-queue-overflow.test.ts +244 -0
  391. package/test/canonical-ingest-vocabularies.test.ts +179 -7
  392. package/test/canonical-quantity-door.test.ts +2 -2
  393. package/test/checkpoint-refuses-empty.test.ts +2 -2
  394. package/test/connector-capability-declaration.test.ts +88 -0
  395. package/test/control-capability.test.ts +1 -1
  396. package/test/cursor-stall-not-read-failure.test.ts +2 -2
  397. package/test/declaration-reaches-model.test.ts +5 -5
  398. package/test/declared-stimulus.test.ts +4 -4
  399. package/test/discovery-result.test.ts +2 -2
  400. package/test/entity-delta.test.ts +4 -4
  401. package/test/event-subjects.test.ts +147 -0
  402. package/test/event-time-column.test.ts +3 -3
  403. package/test/event-type-split.test.ts +155 -0
  404. package/test/fold-gap-speaks.test.ts +53 -0
  405. package/test/forecast-metrics.test.ts +1 -1
  406. package/test/forecast-tuning.test.ts +1 -1
  407. package/test/ingest-health-engine.test.ts +2 -2
  408. package/test/ingest-health-wiring.test.ts +7 -5
  409. package/test/ingest-health.test.ts +6 -6
  410. package/test/ingest-history.test.ts +2 -2
  411. package/test/ingest-idempotent.test.ts +132 -0
  412. package/test/ingest-space.test.ts +3 -3
  413. package/test/integration-probes.test.ts +3 -3
  414. package/test/integration-runner.test.ts +1 -1
  415. package/test/item-ref.test.ts +1 -1
  416. package/test/journal-order-axis.test.ts +80 -0
  417. package/test/journal-read-discipline.test.ts +16 -6
  418. package/test/journal-retention.test.ts +171 -29
  419. package/test/journal-sort-axis.test.ts +1 -1
  420. package/test/journal-write-door.test.ts +3 -3
  421. package/test/journal-write-trend.test.ts +1 -1
  422. package/test/kernel-kind-guard.test.ts +3 -3
  423. package/test/knob-defaults.test.ts +1 -1
  424. package/test/kpi-baseline-db.test.ts +2 -2
  425. package/test/kpi-fold.test.ts +185 -14
  426. package/test/kpi-query-bench.test.ts +3 -3
  427. package/test/live-cursor-wiring.test.ts +6 -6
  428. package/test/live-kernel-facts.test.ts +11 -11
  429. package/test/live-mirror-parity.test.ts +3 -3
  430. package/test/load-meter.test.ts +1 -1
  431. package/test/local-declarations.test.ts +10 -10
  432. package/test/log-stamp.test.ts +1 -1
  433. package/test/measured-yield.test.ts +2 -2
  434. package/test/merge-streams.test.ts +70 -0
  435. package/test/mirror-resumes-from-checkpoint.test.ts +13 -13
  436. package/test/model-basis.test.ts +2 -2
  437. package/test/model-gap.test.ts +4 -4
  438. package/test/model-vocabulary.test.ts +2 -2
  439. package/test/move-space.test.ts +1 -1
  440. package/test/oee-accumulator.test.ts +2 -2
  441. package/test/operational-vocabulary.test.ts +2 -2
  442. package/test/project-structure-db.test.ts +2 -2
  443. package/test/projection-reaches-screen.test.ts +2 -2
  444. package/test/property-effects.test.ts +3 -3
  445. package/test/read-failure-ledger.test.ts +106 -0
  446. package/test/read-failure-visible.test.ts +7 -7
  447. package/test/read-time.test.ts +118 -0
  448. package/test/reference-grounding.test.ts +1 -1
  449. package/test/reference-hook.test.ts +86 -0
  450. package/test/registry-key-guard.test.ts +1 -1
  451. package/test/rename-twin.test.ts +81 -0
  452. package/test/restart-policy.test.ts +3 -3
  453. package/test/resync-origin-site.test.ts +3 -3
  454. package/test/revision-axis.test.ts +14 -8
  455. package/test/runtime-key.test.ts +3 -3
  456. package/test/snapshot-freshness.test.ts +4 -4
  457. package/test/source-outcome-audit.test.ts +1 -1
  458. package/test/space-integrity.test.ts +1 -1
  459. package/test/space-representation.test.ts +66 -0
  460. package/test/space-scope-includes-stopped.test.ts +55 -0
  461. package/test/spec-coverage.test.ts +1 -1
  462. package/test/standard-coverage.test.ts +43 -1
  463. package/test/status-tally.test.ts +1 -1
  464. package/test/structure-revision-db.test.ts +7 -7
  465. package/test/subject-fast-path.test.ts +148 -0
  466. package/test/subject-read-model.test.ts +175 -0
  467. package/test/touched-items.test.ts +2 -2
  468. package/test/twin-audit.test.ts +1 -1
  469. package/test/twin-event-keys.test.ts +49 -3
  470. package/test/twin-model-item-db.test.ts +5 -5
  471. package/test/twin-model-tree-db.test.ts +35 -2
  472. package/test/twin-origin-resync.test.ts +1 -1
  473. package/test/warm-start-seam.test.ts +1 -1
  474. package/test/warm-start.test.ts +6 -6
  475. package/test/withheld-door.test.ts +95 -0
  476. package/test/yield-loop.test.ts +4 -4
  477. package/tsconfig.shared.tsbuildinfo +1 -1
  478. package/tsconfig.tsbuildinfo +1 -1
@@ -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/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"]}
@@ -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"]}
@@ -12,7 +12,7 @@ export interface IngestWindow {
12
12
  * 매핑은 통과했지만 **트윈에 닿지 못한** 수 — 처방은 트윈을 세우는 것이다.
13
13
  *
14
14
  * ── 왜 거부와 따로 세나 (2026-08-20에 겪었다) ─────────────────────────────
15
- * 트윈이 라이브로 돌지 않으면 커널이 레코드를 **조용히 버린다**. 그런데 장부는 그것을 「통과」로 셌다.
15
+ * 트윈이 라이브로 돌지 않으면 커널이 레코드를 **알리지 않고 버린다**. 그런데 장부는 그것을 「통과」로 셌다.
16
16
  * 화면이 「150 들어옴 · 150 통과 · 100%」라고 말하는 동안 150건은 바닥에 버려지고 있었다. 이 기능이
17
17
  * 막으려던 바로 그것을 이 기능이 저지른 셈이다.
18
18
  *
@@ -72,6 +72,26 @@ export interface IngestLedger {
72
72
  */
73
73
  lastUndeliveredAtMs: number;
74
74
  /** 사유별 누적 건수와 **마지막으로 그 사유가 난 시각** — 고친 사유는 시각이 멈춘다. */
75
+ /**
76
+ * **어떤 길로 들어왔나** — 경로마다 건수와 마지막 시각.
77
+ *
78
+ * ── 왜 필요한가 (2026-08-26) ──────────────────────────────────────────────
79
+ * 트윈이 값을 받고 있다는 것은 계기가 말해 주는데, **무엇을 통해 받는지는 아무도 말하지 않았다.**
80
+ * 커넥터가 물어서 받는지, 연결된 시스템이 밀어 주는지, 시나리오가 넣는지, 부하 도구가 넣는지 —
81
+ * 화면에서 구별할 수 없었다. 그 구별이 없으면 「폴링을 웹훅으로 바꿨다」를 확인할 방법도 없다.
82
+ */
83
+ bySource: Record<string, {
84
+ count: number;
85
+ lastAtMs: number;
86
+ }>;
87
+ /**
88
+ * **같은 사실이 두 번 와서 버린 수** — 알리지 않고 버리지 않는다.
89
+ *
90
+ * 이 수가 크면 보내는 쪽이 되풀이 보내고 있다는 뜻이고, 그것은 고칠 일이다(태양광 커넥터가 한 주기에
91
+ * 76건을 되풀이 보내던 것을 이 수로 볼 수 있었어야 했다).
92
+ */
93
+ duplicates: number;
94
+ lastDuplicateAtMs: number;
75
95
  byReason: Record<string, {
76
96
  count: number;
77
97
  lastAtMs: number;
@@ -79,8 +99,8 @@ export interface IngestLedger {
79
99
  /**
80
100
  * 사유 종류가 상한에 닿았나.
81
101
  *
82
- * **접힌 종류 수는 세지 않는다** — 그것을 세려면 이미 본 사유를 모두 기억해야 하고, 그 집합이
83
- * 바로 이 상한으로 막으려던 무한한 집합이다. 접힌 **건수**는 `byReason[OTHER_REASON].count` 가 답한다.
102
+ * **모은 종류 수는 세지 않는다** — 그것을 세려면 이미 본 사유를 모두 기억해야 하고, 그 집합이
103
+ * 바로 이 상한으로 막으려던 무한한 집합이다. 모은 **건수**는 `byReason[OTHER_REASON].count` 가 답한다.
84
104
  */
85
105
  reasonsCapped: boolean;
86
106
  /** 표본을 이미 뜬 사유들 — 같은 사유를 다시 직렬화하지 않기 위한 색인(O(1) 조회). */
@@ -94,12 +114,12 @@ export interface IngestLedger {
94
114
  /**
95
115
  * **원본에 닿지 못한 사실** (2026-08-23) — 「받은 것이 없다」와 **다른 사실**이다.
96
116
  *
97
- * ── 무엇이 조용했나 ─────────────────────────────────────────────────────────
117
+ * ── 무엇이 아무 신호도 내지 않았나 ─────────────────────────────────────────────────────────
98
118
  * 실 원본이 끊겼을 때(접속 시간 초과) 트윈의 **조회 가능한 상태 어디에도** 그 사실이 없었다.
99
119
  * `recordIngest` 는 `onRecords` 가 불릴 때만 도달하고, 읽기가 실패하면 그 문에 오지 않는다.
100
120
  * `TwinReference.lastError` 는 마스터 동기·접속 시험에만 적힌다.
101
121
  *
102
- * 그래서 화면이 볼 수 있는 것은 「새 사실이 없다」뿐이었고, 그것은 **「원본이 조용하다」와 구별되지
122
+ * 그래서 화면이 볼 수 있는 것은 「새 사실이 없다」뿐이었고, 그것은 **「연결된 시스템에서 들어온 것이 없다」와 구별되지
103
123
  * 않는다.** 실증 데모 중에 원본이 끊기면 사용자가 원인을 찾을 수 없다.
104
124
  *
105
125
  * ── 왜 「몇 번」과 「언제부터」를 함께 두나 ──────────────────────────────────
@@ -108,6 +128,17 @@ export interface IngestLedger {
108
128
  *
109
129
  * 성공하면 지운다 — 고쳐진 단절이 화면에 남아 있으면 그것도 거짓이다.
110
130
  */
131
+ /**
132
+ * 마지막으로 끊겼다가 돌아온 것 — 없으면 끊긴 적이 없거나 아직 돌아오지 않은 것이다.
133
+ *
134
+ * 실패 기록(`readFailure`)은 돌아오면 지워진다. 그것만 지우면 「끊긴 적이 있었다」가 화면에서
135
+ * 사라지므로, 돌아온 사실을 따로 남긴다.
136
+ */
137
+ lastRecovery?: {
138
+ atMs: number;
139
+ afterFailures: number;
140
+ downMs: number;
141
+ };
111
142
  readFailure?: {
112
143
  /** 연속 실패 수 — 성공 한 번에 0 으로 돌아간다. */
113
144
  consecutive: number;
@@ -119,6 +150,8 @@ export interface IngestLedger {
119
150
  reason: string;
120
151
  /** 어느 흐름에서 났나(어댑터가 말해 주면). 원본마다 흐름이 다르므로 열려 있다. */
121
152
  stream?: string;
153
+ /** 다음에 다시 물을 때까지 남은 시간(ms) — 커넥터가 물러섰을 때만 있다. */
154
+ nextRetryMs?: number;
122
155
  };
123
156
  /**
124
157
  * **읽었는데 창을 넘길 수 없다** (2026-08-24) — 위와 **다른 사실**이다.
@@ -137,6 +170,31 @@ export interface IngestLedger {
137
170
  * 모양은 위와 같게 둔다 — 「몇 번」과 「언제부터」가 여기서도 같은 값을 한다(한 번의 밀림과 사흘째
138
171
  * 못 넘어가는 것은 다르다). 넘어가면 지운다.
139
172
  */
173
+ /**
174
+ * **원본이 말했는데 트윈에 세우지 않은 것** — 이유별로 센다.
175
+ *
176
+ * ── 왜 이 자리가 필요한가 (2026-08-24) ─────────────────────────────────────
177
+ * 커넥터가 원본의 사실 일부를 **일부러 받지 않는 일이 정상이다**: 입자가 맞지 않거나, 받으면 상태가
178
+ * 거짓이 되거나, 커널 어휘로 옮길 수 없다. 그때 커넥터는 경고를 낸다 — **그런데 그 경고가 프로비저닝
179
+ * 화면에서 한 번 스쳐 지나갈 뿐이었다.** 그 뒤로는 어디에서도 볼 수 없었다.
180
+ *
181
+ * 그래서 사용자가 「원본보다 계획이 적다」를 물으면 **답이 어디에도 없다.** 그 사실은 한 번의
182
+ * 사건이 아니라 **상태의 성질**이다 — 세우지 않은 것은 지금도 트윈에 없다.
183
+ *
184
+ * ── `unhandled` 의 형제다 ──────────────────────────────────────────────────
185
+ * `unhandled` 리듀서가 **받았는데** 담을 자리를 몰랐다
186
+ * 여기 커넥터가 **보지만 일부러 보내지 않았다** — 이유와 함께
187
+ *
188
+ * 둘 다 「사실이 오류 없이 사라지지 않게」 세는 것이고, 이유가 다르므로 자리도 다르다.
189
+ *
190
+ * 이유를 **문자열로 받는다**: 무엇을 왜 안 받는지는 원본마다 다르고, 계약이 그 목록을 닫으면 새 원본을
191
+ * 붙일 때마다 계약을 고치게 된다(흐름 열쇠와 같은 규율).
192
+ */
193
+ withheld?: {
194
+ reason: string;
195
+ count: number;
196
+ lastAtMs: number;
197
+ }[];
140
198
  cursorStall?: {
141
199
  /** 연속 정체 수 — 넘어가면 0 으로 돌아간다. */
142
200
  consecutive: number;
@@ -156,12 +214,30 @@ export interface IngestLedger {
156
214
  * 연속 실패의 시작 시각을 지키는 것이 이 함수의 요점이다: 매번 새로 적으면 「20분째 못 읽는다」를
157
215
  * 말할 수 없고, 화면은 늘 「방금 실패했다」만 보게 된다.
158
216
  */
159
- export declare function recordReadFailure(ledger: IngestLedger, reason: string, nowMs: number, stream?: string): void;
217
+ export declare function recordReadFailure(ledger: IngestLedger, reason: string, nowMs: number, stream?: string,
218
+ /**
219
+ * 다음에 다시 물을 때까지 남은 시간(ms) — 커넥터가 물러섰으면 그 값을 준다 (2026-08-27).
220
+ *
221
+ * 이것이 없으면 화면이 「닿지 못하고 있다」까지만 말하고, 사람은 **언제 다시 시도하는지** 알 수
222
+ * 없다. 그러면 기다려야 하는지 손을 써야 하는지 판단하지 못한다.
223
+ *
224
+ * **연달아 횟수와 시작 시각은 커넥터가 보내지 않는다** — 이 함수가 이미 센다. 두 곳에서 세면
225
+ * 갈라지고, 갈라지면 어느 쪽이 사실인지 알 수 없다.
226
+ */
227
+ nextRetryMs?: number): void;
228
+ /**
229
+ * **끊겼다가 돌아왔다** — 그 사실을 남긴다 (2026-08-27).
230
+ *
231
+ * 실패 기록만 지우면 「끊긴 적이 있었다」가 화면에서 사라진다. 그러면 밤새 두 시간 끊겼던 연결과 한
232
+ * 번도 끊기지 않은 연결이 아침에 똑같이 보인다. 몇 번 실패한 뒤에 돌아왔는지, 얼마나 끊겼는지를 함께
233
+ * 남긴다.
234
+ */
235
+ export declare function recordRecovered(ledger: IngestLedger, afterFailures: number, downMs: number, nowMs: number): void;
160
236
  /**
161
237
  * 읽기가 성공했다 — 단절 기록을 지운다.
162
238
  *
163
239
  * **빈 읽기도 성공이다**: 원본이 「새 것이 없다」고 답한 것은 닿았다는 뜻이다. 그 둘을 같게 두면
164
- * 조용한 원본이 끊긴 원본으로 보인다(고치려던 것의 반대 방향으로 틀린다).
240
+ * 들어온 것이 없는 연결이 끊긴 원본으로 보인다(고치려던 것의 반대 방향으로 틀린다).
165
241
  */
166
242
  export declare function clearReadFailure(ledger: IngestLedger): void;
167
243
  /**
@@ -172,6 +248,25 @@ export declare function clearReadFailure(ledger: IngestLedger): void;
172
248
  * 조치를 정한다.
173
249
  */
174
250
  export declare function recordCursorStall(ledger: IngestLedger, reason: string, nowMs: number, stream?: string): void;
251
+ /**
252
+ * **세우지 않은 것을 적는다** — 이유별 누적.
253
+ *
254
+ * 주기마다 다시 알려도 시끄러워지지 않게 **이유로 묶어 센다**(줄이 늘지 않는다). 그리고 마지막 시각을
255
+ * 함께 든다 — 「지금도 그런가」와 「한때 그랬나」는 다른 사실이고, 시각 없이는 구별할 수 없다.
256
+ *
257
+ * 이유가 같고 수가 달라지면 **마지막 수로 덮는다**: 이것은 누적 사건이 아니라 **지금 세우지 않은 것의
258
+ * 수**다. 주기마다 8건이면 8이고, 더해서 800이 되면 거짓이다.
259
+ */
260
+ /**
261
+ * 어떤 길로 몇 건이 들어왔나 — 경로 이름은 부르는 쪽이 정한다.
262
+ *
263
+ * 이름을 자유 문자열로 두는 이유: 길이 늘어난다(지금은 물어보기·시나리오·부하 도구, 다음은 웹훅).
264
+ * 열거로 못 박으면 새 길이 생길 때 이 파일을 함께 고쳐야 하고, 잊으면 그 길이 통계에서 사라진다.
265
+ */
266
+ export declare function recordIngestSource(ledger: IngestLedger, source: string, count: number, nowMs: number): void;
267
+ /** 같은 사실이 두 번 와서 버린 수 — 알리지 않고 버리지 않는다. */
268
+ export declare function recordDuplicates(ledger: IngestLedger, count: number, nowMs: number): void;
269
+ export declare function recordWithheld(ledger: IngestLedger, reason: string, count: number, nowMs: number): void;
175
270
  /** 창을 넘겼으면 지운다 — 풀린 정체가 화면에 남아 있으면 그것도 거짓이다. */
176
271
  export declare function clearCursorStall(ledger: IngestLedger): void;
177
272
  /** 창 길이 — 10분. 부하 계기판(10초)과 다른 값이라 이름을 따로 둔다(같은 이름이면 섞인다). */
@@ -192,7 +287,7 @@ export declare const TREND_LIMIT = 36;
192
287
  * (`unknown bizStep "PUTAWAY_X17"`, `…X18`, …) 키가 무한히 늘어난다. 초당 1만 건 규모에서 이것은
193
288
  * 미미한 오버헤드가 아니라 누수다.
194
289
  *
195
- * 넘치면 `OTHER_REASON` 한 칸에 모으고 **몇 종을 접었는지 함께 낸다**(조용히 자르지 않는다).
290
+ * 넘치면 `OTHER_REASON` 한 칸에 모으고 **몇 종을 모았는지 함께 낸다**(알리지 않고 자르지 않는다).
196
291
  * 30종이면 고칠 곳을 찾기에 충분하다 — 목록을 훑는 일이 아니라 매핑 몇 줄을 고치는 일이다.
197
292
  */
198
293
  export declare const REASON_LIMIT = 30;
@@ -291,6 +386,13 @@ export declare function syncVerdictOf(args: {
291
386
  nowMs?: number;
292
387
  /** 마지막으로 **버려진** 시각. 한 번도 없으면 `-1`. */
293
388
  lastUndeliveredAtMs?: number;
389
+ /**
390
+ * 지금까지 제시된 누적 수 — **「받고 있는 중」과 「오는 것이 없다」를 가르는 사실**이다.
391
+ *
392
+ * 닿은 시각(`lastAtMs`)으로는 가를 수 없다: 빈 읽기도 닿은 것이므로 그 시각이 선다. 창이 아직 닫히지
393
+ * 않았을 때 이 수가 0 이면 오는 것이 없는 것이고, 0 이 아니면 받고 있는 중이다.
394
+ */
395
+ offeredTotal?: number;
294
396
  threshold?: number;
295
397
  }): SyncVerdict;
296
398
  export interface IngestWindowView {
@@ -350,7 +452,7 @@ export interface IngestHealthView {
350
452
  *
351
453
  * 커서 정체(`cursorStall`)와 **섞이지 않는다**: 이쪽은 기다리면 풀리고 그쪽은 기다려도 안 풀린다.
352
454
  *
353
- * 「유입 없음」과 **다른 판정**이다: 유입 없음은 원본이 조용한 것일 수 있고, 이것은 우리가 묻지도
455
+ * 「유입 없음」과 **다른 판정**이다: 유입 없음은 연결된 시스템에서 들어온 것이 없는 것일 수 있고, 이것은 우리가 묻지도
354
456
  * 못한 것이다. 화면은 두 문장을 다르게 말해야 한다 — 실 원본이 끊겼을 때 사용자가 원인을 찾을 수
355
457
  * 있는지가 그 차이다.
356
458
  */
@@ -361,6 +463,19 @@ export interface IngestHealthView {
361
463
  lastAt: string;
362
464
  reason: string;
363
465
  stream?: string;
466
+ /** 다음에 다시 물을 때까지 남은 시간(ms) — 커넥터가 물러섰을 때만 있다. */
467
+ nextRetryMs?: number;
468
+ };
469
+ /**
470
+ * 마지막으로 끊겼다가 돌아온 것 — 없으면 끊긴 적이 없거나 아직 돌아오지 않은 것이다 (2026-08-27).
471
+ *
472
+ * `readFailure` 는 돌아오면 사라진다. 이 칸이 없으면 밤새 끊겼던 연결과 한 번도 끊기지 않은 연결이
473
+ * 아침에 똑같이 보인다.
474
+ */
475
+ lastRecovery?: {
476
+ at: string;
477
+ afterFailures: number;
478
+ downMs: number;
364
479
  };
365
480
  }
366
481
  /**