@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
@@ -22,7 +22,8 @@
22
22
  import { test } from 'node:test'
23
23
  import assert from 'node:assert/strict'
24
24
 
25
- import { MesKernel, type CanonicalEnvelope, type ProductionSpec, type ScenarioDef, type TwinModelDef } from '@operato/twin-kernel'
25
+ import { MesKernel } from '@operato/twin-kernel'
26
+ import { type CanonicalEnvelope, type ProductionSpec, type ScenarioDef, type TwinModelDef } from '@operato/ops-contract'
26
27
 
27
28
  import { buildEntityDeltas } from '../shared/entity-delta.ts'
28
29
 
@@ -9,7 +9,7 @@
9
9
  * ① 무거우면 늘린다 — 상한까지만.
10
10
  * ② 가벼워지면 되돌린다 — 원래 주기 아래로는 내려가지 않는다.
11
11
  * ③ 경계에서 늘리고 줄이기를 반복하지 않는다(문턱이 둘이다).
12
- * ④ **늘렸다는 사실을 센다** — 조용히 늦추면 사람은 화면이 낡은 이유를 알 수 없다.
12
+ * ④ **늘렸다는 사실을 센다** — 알리지 않고 늦추면 사람은 화면이 낡은 이유를 알 수 없다.
13
13
  */
14
14
  import { test, beforeEach } from 'node:test'
15
15
  import assert from 'node:assert/strict'
@@ -35,7 +35,7 @@ test('브로드캐스팅이 주기의 몫을 넘게 쓰면 주기를 늘린다
35
35
  assert.equal(TwinEngine.broadcastPeriodMs, TwinEngine.BROADCAST_MAX_COALESCE_MS, '상한 위로는 올라가지 않는다')
36
36
  })
37
37
 
38
- test('늘렸다는 사실을 센다 — 조용히 늦추지 않는다', () => {
38
+ test('늘렸다는 사실을 센다 — 알리지 않고 늦추지 않는다', () => {
39
39
  adjust(TwinEngine.BROADCAST_COALESCE_MS)
40
40
  assert.equal(TwinEngine.broadcastBackoffs, 1)
41
41
  })
@@ -0,0 +1,244 @@
1
+ /*
2
+ * ★ 브로드캐스팅이 서버를 죽였다 — 그 기전을 실제 라이브러리로 재고, 우리 코드가 그것을 넘기지 않는지 본다.
3
+ *
4
+ * ── 무엇이 났나 (2026-08-27 10:24 · 실측) ───────────────────────────────────
5
+ * `RepeaterOverflowError: No more than 1024 pending calls to push are allowed on a single repeater.`
6
+ * 가 브로드캐스팅 타이머 안에서 나서 프로세스가 내려갔다. 같은 예외로 2026-08-14 에도 죽었다.
7
+ *
8
+ * ── 기전 ────────────────────────────────────────────────────────────────────
9
+ * 구독의 대기열에는 버퍼가 없다. `pubsub.subscribe` 가 `new Repeater(fn)` 을 버퍼 인자 없이 만든다.
10
+ * 그래서 발행하는 순간에 「다음 값을 달라」는 요청이 이미 걸려 있어야 바로 전달되고, 아니면 대기열에
11
+ * 쌓인다. 상한은 1024 이고 넘으면 예외가 난다.
12
+ *
13
+ * 값을 꺼내는 쪽은 비동기다. 그래서 **동기 반복문이 도는 중에는 한 건도 꺼내지 못한다.** 반복문으로
14
+ * N 건을 발행하면 첫 건만 전달되고 나머지 N−1 건이 전부 쌓인다. 브라우저가 아무리 빨라도 넘는다 —
15
+ * 속도 문제가 아니라 꺼낼 틈을 주지 않는 것이 원인이다.
16
+ *
17
+ * ── 왜 try/catch 로 못 막나 ─────────────────────────────────────────────────
18
+ * 2026-08-14 에 `publishGuarded` 가 `pubsub.publish` 를 try/catch 로 감쌌고, 커밋 제목이 「방송이
19
+ * 호스트를 죽이지 못하게」였다. 그것이 무력하다. 예외는 `EventTarget` 이 부른 리스너 안에서 나고,
20
+ * 런타임은 리스너 예외를 호출자에게 주지 않고 uncaughtException 으로 보낸다. 이 테스트가 그것을
21
+ * 값으로 못 박는다 — 그때 검증하지 않아서 3주 동안 막힌 줄 알고 있었다.
22
+ *
23
+ * ── 왜 오늘 났나 ────────────────────────────────────────────────────────────
24
+ * 오늘 아침 chef 커넥터가 묻는 간격을 5초에서 30초로 올렸다(상대 서버 부담을 줄이려고). 요청 횟수는
25
+ * 줄지만 한 번에 오는 양이 6배가 된다. 초당 130건이 들어오는 원본에서 한 묶음이 3,900건이 되고, 그
26
+ * 묶음이 한 창에 몰려 반복문 하나가 1024 를 넘겼다.
27
+ */
28
+ import { test } from 'node:test'
29
+ import assert from 'node:assert/strict'
30
+ import { readFileSync } from 'node:fs'
31
+ import { join } from 'node:path'
32
+ import { spawnSync } from 'node:child_process'
33
+
34
+ const ENGINE = join(import.meta.dirname, '..', 'server', 'engine', 'twin-engine.ts')
35
+ const src = readFileSync(ENGINE, 'utf8')
36
+
37
+ /** 저장소 뿌리 — 자식이 `graphql-yoga` 를 찾을 수 있어야 한다. */
38
+ const REPO = join(import.meta.dirname, '..', '..', '..')
39
+
40
+ /**
41
+ * 발행을 **자식 프로세스**에서 한다 — 「프로세스가 죽는다」를 종료 코드로 확인하려면 그래야 한다.
42
+ *
43
+ * 같은 프로세스에서 재면 예외가 테스트 러너에 잡혀 이 테스트의 실패로 보고된다. 그것도 사실이지만
44
+ * 확인하려는 것은 「감싸도 못 막고 프로세스가 내려간다」이므로 자식의 종료 코드로 판정한다.
45
+ */
46
+ function runChild(
47
+ count: number,
48
+ opts: { consume: boolean; chunk?: number; subscribers?: number; watchAll?: boolean }
49
+ ) {
50
+ const subscribers = opts.subscribers ?? 1
51
+ const code = `
52
+ import { createPubSub, pipe, filter } from 'graphql-yoga'
53
+ const pubsub = createPubSub()
54
+ const watchAll = ${!!opts.watchAll}
55
+ let pulled = 0, ordered = true, lastSeen = -1
56
+ if (${opts.consume}) {
57
+ for (let s = 0; s < ${subscribers}; s++) {
58
+ const stream = pipe(pubsub.subscribe('data'), filter(p => watchAll || p.tag === 'watched'))
59
+ ;(async () => {
60
+ for await (const p of stream) {
61
+ /* 순서 확인은 첫 구독만 한다 — 여럿이 세면 서로의 값이 섞인다. */
62
+ if (s === 0) {
63
+ pulled++
64
+ if (watchAll) {
65
+ if (p.i !== lastSeen + 1) ordered = false
66
+ lastSeen = p.i
67
+ }
68
+ }
69
+ }
70
+ })().catch(() => {})
71
+ }
72
+ /* 소비자들이 첫 요청을 걸 틈을 준다 — 이것이 없으면 리스너조차 등록되지 않는다(지연 구독). */
73
+ await new Promise(r => setImmediate(r))
74
+ }
75
+ let thrownToCaller = 0, sinceYield = 0
76
+ for (let i = 0; i < ${count}; i++) {
77
+ try { pubsub.publish('data', { tag: (watchAll || i === 0) ? 'watched' : 'other-' + i, i }) }
78
+ catch { thrownToCaller++ }
79
+ const chunk = ${opts.chunk ?? 0}
80
+ if (chunk && ++sinceYield >= chunk) { sinceYield = 0; await new Promise(r => setImmediate(r)) }
81
+ }
82
+ /* 남은 것을 다 꺼낼 틈을 준다 — 여기서 짧게 끊으면 「빠졌다」로 오판한다. */
83
+ for (let k = 0; k < 50; k++) await new Promise(r => setImmediate(r))
84
+ await new Promise(r => setTimeout(r, 100))
85
+ console.log(JSON.stringify({ thrownToCaller, pulled, ordered }))
86
+ `
87
+ const r = spawnSync(process.execPath, ['--input-type=module', '-e', code], {
88
+ cwd: REPO,
89
+ encoding: 'utf8'
90
+ })
91
+ const line = (r.stdout ?? '').trim().split('\n').filter(Boolean).pop()
92
+ return {
93
+ status: r.status,
94
+ overflowed: /No more than 1024 pending calls to push/.test(r.stderr ?? ''),
95
+ result: line ? (JSON.parse(line) as { thrownToCaller: number; pulled: number; ordered: boolean }) : null
96
+ }
97
+ }
98
+
99
+ test('★ 구독자가 있고 한 차례에 1024건을 넘게 발행하면 프로세스가 죽는다 — 브라우저 속도와 무관하다', () => {
100
+ const r = runChild(2000, { consume: true })
101
+ assert.ok(r.overflowed, '대기열이 넘쳐 예외가 나야 한다(이것이 서버를 죽인 그 예외다)')
102
+ assert.notEqual(r.status, 0, '그 예외로 프로세스가 내려간다')
103
+ })
104
+
105
+ test('★ 그 예외는 publish() 를 감싼 try/catch 로 잡히지 않는다 — 08-14 의 가드가 무력한 이유', () => {
106
+ const r = runChild(2000, { consume: true })
107
+ assert.equal(r.result, null, '완주하지 못한다 — 결과를 낼 자리에 닿지 못한다')
108
+ assert.ok(r.overflowed, '예외는 감싼 곳이 아니라 uncaughtException 으로 간다')
109
+ })
110
+
111
+ test('★ 상한마다 루프를 비워 주면 넘지 않는다 — 이것이 고친 방법이다', () => {
112
+ const r = runChild(5000, { consume: true, chunk: 200 })
113
+ assert.equal(r.status, 0, '청크마다 양보하면 프로세스가 살아 있다')
114
+ assert.equal(r.overflowed, false, '대기열이 넘지 않는다')
115
+ assert.ok((r.result?.pulled ?? 0) > 0, '소비자가 실제로 꺼내 간다')
116
+ })
117
+
118
+ test('★ 동기 반복문 중에는 소비자가 꺼내지 못한다 — 속도가 아니라 틈이 없는 것이 원인이다', () => {
119
+ /* 1024 아래로 두어 죽지 않게 하고, 그 사이에 몇 건이 꺼내졌는지 본다. */
120
+ const r = runChild(1000, { consume: true })
121
+ assert.equal(r.status, 0)
122
+ assert.ok((r.result?.pulled ?? -1) <= 2, `꺼낸 것이 ${r.result?.pulled}건뿐이다 — 1000건을 발행하는 동안`)
123
+ })
124
+
125
+ test('구독자가 없으면 아무것도 쌓이지 않는다 — 리스너가 없다', () => {
126
+ const r = runChild(2000, { consume: false })
127
+ assert.equal(r.status, 0, '들을 사람이 없으면 대기열도 없다')
128
+ assert.equal(r.overflowed, false)
129
+ })
130
+
131
+
132
+
133
+
134
+
135
+
136
+ test('★ 엔진의 발행 반복문이 상한마다 루프를 비워 준다', () => {
137
+ const at = src.indexOf('static async publishEntityData')
138
+ assert.ok(at > 0, 'publishEntityData 가 async 여야 한다 — 양보할 수 없으면 상한을 지킬 수 없다')
139
+ const body = src.slice(at, at + 6000)
140
+ assert.match(body, /PUBLISH_CHUNK/, '한 차례의 상한에 이름이 있어야 한다')
141
+ assert.match(
142
+ body,
143
+ /sinceYield \+= 0|\+\+sinceYield >= this\.PUBLISH_CHUNK[\s\S]{0,120}?setImmediate/,
144
+ '상한에 닿으면 루프를 비워 줘야 한다'
145
+ )
146
+ /* 상한은 1024 보다 넉넉히 작아야 한다 — 한 청크를 다 꺼내지 못해도 여유가 남게. */
147
+ const cap = Number(/static PUBLISH_CHUNK = ([0-9_]+)/.exec(src)?.[1]?.replace(/_/g, ''))
148
+ assert.ok(cap > 0 && cap <= 512, `상한이 ${cap} 이다 — 1024 의 절반 아래여야 한다`)
149
+ })
150
+
151
+ test('★ 발행을 중간에 그만두지 않는다 — 그만두면 그 태그가 낡은 값으로 남는다', () => {
152
+ const at = src.indexOf('static async publishEntityData')
153
+ const body = src.slice(at, at + 6000)
154
+ /* 상한에 닿았을 때 `break`·`return` 으로 빠지면 남은 태그는 이 창에서 발행되지 않는다. */
155
+ assert.doesNotMatch(body, />= this\.PUBLISH_CHUNK[\s\S]{0,80}?(break|return)/, '상한에서 빠져나가지 않는다')
156
+ })
157
+
158
+ test('★ 한 창이 겹쳐 돌지 않는다 — 같은 태그가 두 순서로 나가지 않게', () => {
159
+ const at = src.indexOf('static flushLiveBroadcasts')
160
+ assert.ok(at > 0, 'flushLiveBroadcasts 가 있어야 한다')
161
+ const body = src.slice(at, at + 1200)
162
+ assert.match(body, /if \(this\.flushInFlight\)/, '앞 창이 돌고 있으면 들어가지 않는다')
163
+ assert.match(body, /flushOverruns\+\+/, '겹친 횟수를 센다 — 알리지 않고 넘기지 않는다')
164
+ /* 발행을 기다려야 청크 사이의 양보가 뜻을 갖는다. */
165
+ assert.match(src, /await this\.publishEntityData\(inst\)/, '창이 발행을 기다린다')
166
+ })
167
+
168
+ test('★ 발행 건수를 남긴다 — 다음에 이 판단을 추측으로 하지 않게', () => {
169
+ const at = src.indexOf('static async publishEntityData')
170
+ const body = src.slice(at, at + 6000)
171
+ assert.match(body, /publishedTotal/, '누적 발행 건수를 든다')
172
+ assert.match(body, /건 발행/, '상한을 넘긴 창은 건수를 로그에 남긴다')
173
+ })
174
+
175
+ /*
176
+ * ── 여기서부터: 상한 근처와 그 너머를 더 넓게 잰다 ────────────────────────────
177
+ *
178
+ * 위 테스트가 「1024 를 넘으면 죽고, 200마다 양보하면 산다」를 못 박았다. 그런데 실제 창은 18,426건이고
179
+ * 구독은 하나가 아니다(`board-provider` 가 컴포넌트마다 하나씩 연다). 그 조건에서도 서는지 잰다.
180
+ */
181
+
182
+ test('★ 실제 규모에서 선다 — 한 창 18,426건 · 청크 200', () => {
183
+ /* 2026-08-27 11:11 실측값 그대로. 이 수가 상한의 18배라서 청크 없이는 반드시 죽는다. */
184
+ const r = runChild(18426, { consume: true, chunk: 200 })
185
+ assert.equal(r.status, 0, '실측 규모에서 프로세스가 살아 있어야 한다')
186
+ assert.equal(r.overflowed, false)
187
+ })
188
+
189
+ test('★ 구독이 여러 개여도 선다 — 화면은 컴포넌트마다 구독을 하나씩 연다', () => {
190
+ /*
191
+ * 지금 구조에서는 발행 하나가 **구독마다** 큐를 지나간다. 그래서 구독 수가 늘면 밀리는 총량도
192
+ * 그만큼 늘고, 양보 한 번에 소비자 여럿이 나눠 꺼내야 한다. 그 조건에서도 상한을 넘지 않아야 한다.
193
+ */
194
+ const r = runChild(5000, { consume: true, chunk: 200, subscribers: 20 })
195
+ assert.equal(r.status, 0, '구독 20개에서도 살아 있어야 한다')
196
+ assert.equal(r.overflowed, false)
197
+ })
198
+
199
+ test('★ 한 실행 차례의 경계는 1,025건이다 — 재서 확인한 값이다', () => {
200
+ /*
201
+ * ── 측정값 (2026-08-27) ─────────────────────────────────────────────────────
202
+ * 청크 1,024 · 1,025 산다
203
+ * 청크 1,026 죽는다
204
+ * 구독 수(1·5·20) 경계를 바꾸지 않는다 — 구독마다 자기 대기열이 따로다
205
+ *
206
+ * 1,025 까지 사는 이유: 청크가 시작될 때 소비자의 요청이 하나 걸려 있어 첫 건은 바로 전달되고,
207
+ * 그 뒤 1,024건이 쌓인 상태에서 검사를 통과한다. 1,026번째에 상한에 닿는다.
208
+ *
209
+ * 처음에 「청크가 상한과 같으면 위험하다」고 단정했는데 틀렸다. 재서 고쳤다.
210
+ */
211
+ assert.equal(runChild(5000, { consume: true, chunk: 1025 }).status, 0, '1,025 는 산다')
212
+ const over = runChild(5000, { consume: true, chunk: 1026 })
213
+ assert.ok(over.overflowed, '1,026 은 넘친다')
214
+ assert.notEqual(over.status, 0, '그리고 프로세스가 내려간다')
215
+ })
216
+
217
+ test('★ 그래도 여유를 둔다 — 같은 실행 차례에 다른 발행처가 함께 넣는다', () => {
218
+ /*
219
+ * 경계가 1,025 라도 그것에 붙여 쓰지 않는다. 이 채널에 발행하는 곳이 우리만이 아니다 —
220
+ * `integration-base` 의 상태 등록 변경과 `shell` 의 진행률 보고가 같은 채널을 쓴다. 그것들이
221
+ * 같은 실행 차례에 끼어들면 우리 몫에 더해진다.
222
+ *
223
+ * 엔진의 값이 200 이다(경계의 1/5). 이 테스트는 그 값이 여유 있는 쪽에 있는지만 본다.
224
+ */
225
+ const cap = Number(/static PUBLISH_CHUNK = ([0-9_]+)/.exec(src)?.[1]?.replace(/_/g, ''))
226
+ assert.ok(cap > 0 && cap <= 512, `상한이 ${cap} 이다 — 경계(1,025)의 절반 아래여야 한다`)
227
+ })
228
+
229
+ test('청크가 넉넉히 작으면 구독이 늘어도 여유가 남는다 — 엔진의 200이 그 자리에 있다', () => {
230
+ const r = runChild(5000, { consume: true, chunk: 200, subscribers: 50 })
231
+ assert.equal(r.status, 0)
232
+ assert.equal(r.overflowed, false)
233
+ })
234
+
235
+ test('★ 발행한 것이 하나도 빠지지 않는다 — 청크로 나눈 것이 값을 잃지 않았음을 확인한다', () => {
236
+ /*
237
+ * 청크는 시점만 나눈다. 건수·내용·순서가 그대로여야 한다. 그것을 확인하지 않으면 「죽지 않는다」만
238
+ * 알고 「제대로 전달된다」는 모르는 상태가 된다.
239
+ */
240
+ const r = runChild(3000, { consume: true, chunk: 200, watchAll: true })
241
+ assert.equal(r.status, 0)
242
+ assert.equal(r.result?.pulled, 3000, '발행한 3,000건이 그대로 도착해야 한다')
243
+ assert.equal(r.result?.ordered, true, '순서도 그대로여야 한다')
244
+ })
@@ -15,7 +15,12 @@ import assert from 'node:assert/strict'
15
15
 
16
16
  import { ingestCanonicalRecords } from '../server/engine/canonical-ingest.ts'
17
17
 
18
- const NOW = '2026-08-14T09:00:00.000Z'
18
+ /*
19
+ * 이 파일의 **시계**. 아래 픽스처들이 8월 말을 쓰는데 시계가 8월 14일이었다 — 자기 시계보다 뒤의
20
+ * 사실을 표본으로 두고 있었다. 커널이 「아직 오지 않은 시각의 사실」을 거부하게 되면서 드러났다
21
+ * (2026-08-30). 시계를 픽스처 뒤로 옮긴다.
22
+ */
23
+ const NOW = '2026-09-01T00:00:00.000Z'
19
24
 
20
25
  const epcisRecord = {
21
26
  epc: 'urn:epc:id:sgtin:0614141.107346.2017',
@@ -57,11 +62,11 @@ test('거부분이 사라지지 않는다 — 커넥터 매핑이 틀린 것을
57
62
  for (const r of rejected) assert.ok(r.errors.length > 0, '이유 없는 거부가 없다')
58
63
  })
59
64
 
60
- test('빈 입력에서 두 경로 모두 조용하다 — 없는 것을 만들지 않는다', () => {
61
- const empty = ingestCanonicalRecords([], 't', NOW)
62
- assert.deepEqual(empty, { accepted: [], rejected: [] })
63
- const nothing = ingestCanonicalRecords(null, 't', NOW)
64
- assert.deepEqual(nothing, { accepted: [], rejected: [] })
65
+ test('빈 입력에서 모든 갈래가 들어온 것이 없다 — 없는 것을 만들지 않는다', () => {
66
+ /* 어휘가 다섯이다 — 사건 넷과 사건이 아닌 것 하나(마스터데이터). 어느 갈래도 값을 지어내지 않는다. */
67
+ const none = { accepted: [], rejected: [], masterData: [] }
68
+ assert.deepEqual(ingestCanonicalRecords([], 't', NOW), none)
69
+ assert.deepEqual(ingestCanonicalRecords(null, 't', NOW), none)
65
70
  })
66
71
 
67
72
  test('시각이 없는 계측은 호출자의 기준 시각을 쓴다 — 도착 시각이 근거다', () => {
@@ -121,7 +126,7 @@ test('개폐 위치는 표준의 네 값만 받는다 — 과도·불량을 열
121
126
  * ── 레코드가 말한 시각이 봉투의 시각이 된다 (2026-08-23) ────────────────────
122
127
  *
123
128
  * 이 경로는 `eventTimePath` 를 주지 않아 **언제나 폴링 시각**을 썼다. 그래서 커넥터가 이미 싣고 있던
124
- * `eventTime` 이 조용히 무시됐다 — 어디에도 오류가 나지 않고, 커넥터를 붙인 쪽은 그것이 먹는다고
129
+ * `eventTime` 이 알리지 않고 무시됐다 — 어디에도 오류가 나지 않고, 커넥터를 붙인 쪽은 그것이 먹는다고
125
130
  * 믿고 있었다.
126
131
  *
127
132
  * 무엇이 걸렸나: **계보의 시각**이다. 4월에 만든 로트가 8월에 만들어진 것으로 저널에 남았다. 그리고
@@ -144,10 +149,256 @@ test('레코드가 시각을 말하면 그것이 봉투의 시각이다 — 폴
144
149
  test('레코드가 시각을 말하지 않으면 폴링 시각으로 떨어진다 — 이 한 줄이 기존 경로를 막지 않는다', () => {
145
150
  /*
146
151
  * 커널의 경로 해석은 없는 값을 **오류로 만들지 않는다**(§`resolve`). 그래서 시각을 말하지 않는
147
- * 레코드는 예전처럼 기본값을 쓴다. 이 단정이 없으면 위 한 줄이 조용히 대량 거부를 낼 수 있다.
152
+ * 레코드는 예전처럼 기본값을 쓴다. 이 단정이 없으면 위 한 줄이 알리지 않고 대량 거부를 낼 수 있다.
148
153
  */
149
154
  const POLLED = '2026-08-23T00:00:00.000Z'
150
155
  const { accepted, rejected } = ingestCanonicalRecords([epcisRecord] as any, 't', POLLED)
151
156
  assert.deepEqual(rejected, [])
152
157
  assert.equal(accepted[0].eventTime, POLLED)
153
158
  })
159
+
160
+ /*
161
+ * ── 다섯 번째 어휘 — 발전 적산 (2026-08-28, 커널 0.7.69) ──────────────────────
162
+ *
163
+ * 커널이 `energy.generated` 를 오래 선언만 하고 있었고, 그 문이 0.7.69 에 열렸다. 그런데 문이 열린
164
+ * 것과 길이 난 것은 다르다 — 이 seam 이 발전 적산을 **설비 상태 문으로 보내고 있었다.**
165
+ *
166
+ * 두 판정이 겹치기 때문이다. `isEnergyEquipmentRecord` 는 `equipmentId` 만 보므로 발전 적산에도
167
+ * 참이다. 그래서 순서를 정하지 않으면 적산이 설비 상태 문에 닿고, 그 문은 적산을 값으로 세지 않아
168
+ * 「바꿀 상태가 하나도 없다」로 돌려보낸다.
169
+ *
170
+ * 이 저장소에서 가장 자주 나는 결함 부류다 — 자리는 선언되어 있고 거기까지 가는 길이 없다.
171
+ */
172
+ const generationRecord = {
173
+ equipmentId: 'inv-001',
174
+ kWh: 574.8,
175
+ kWhSince: '2026-08-13T15:00:00.000Z',
176
+ at: '2026-08-14T09:00:30.000Z'
177
+ }
178
+
179
+ test('발전 적산은 설비 상태가 아니라 자기 문으로 들어온다', () => {
180
+ const { accepted, rejected } = ingestCanonicalRecords([generationRecord] as any, 't', NOW)
181
+ assert.deepEqual(rejected, [], `거부 없음이어야 한다: ${JSON.stringify(rejected)}`)
182
+ assert.equal(accepted.length, 1)
183
+ assert.equal(accepted[0].eventType, 'energy.generated', '설비 상태 문으로 가면 여기서 거부됐을 것이다')
184
+ })
185
+
186
+ test('적산이 쌓이기 시작한 시각이 봉투까지 간다 — 이것이 없으면 자정이 계기 교체로 남는다', () => {
187
+ const { accepted } = ingestCanonicalRecords([generationRecord] as any, 't', NOW)
188
+ assert.equal((accepted[0].data as any).since, '2026-08-13T15:00:00.000Z')
189
+ assert.equal((accepted[0].data as any).kWh, 574.8)
190
+ })
191
+
192
+ test('기준점을 말하지 않는 원본도 그대로 통과한다 — 없는 칸을 만들지 않는다', () => {
193
+ const { accepted, rejected } = ingestCanonicalRecords(
194
+ [{ equipmentId: 'inv-002', kWh: 120.5, at: '2026-08-14T09:00:30.000Z' }] as any,
195
+ 't',
196
+ NOW
197
+ )
198
+ assert.deepEqual(rejected, [])
199
+ assert.equal(accepted[0].eventType, 'energy.generated')
200
+ assert.equal((accepted[0].data as any).since, undefined, '말하지 않은 것을 지어내지 않는다')
201
+ })
202
+
203
+ test('한 배치에 설비 상태와 발전 적산이 섞여도 각자의 문으로 간다', () => {
204
+ const { accepted, rejected } = ingestCanonicalRecords(
205
+ [{ equipmentId: 'inv-001', generatedKW: 8.2, at: '2026-08-14T09:00:30.000Z' }, generationRecord] as any,
206
+ 't',
207
+ NOW
208
+ )
209
+ assert.deepEqual(rejected, [], JSON.stringify(rejected))
210
+ assert.deepEqual(accepted.map(e => e.eventType).sort(), ['energy.equipment', 'energy.generated'])
211
+ })
212
+
213
+ test('★ 상태까지 걷는다 — 자정에 되돌아간 적산이 계기 교체로 남지 않는다', async () => {
214
+ /*
215
+ * 문이 열린 것과 길이 난 것은 다르다. 그래서 봉투에서 멈추지 않고 **상태까지** 걷는다.
216
+ *
217
+ * PPMS 인버터의 `acc` 는 하루 누적이다(수집 코드가 전날까지 총량을 빼고, 그 값을 하루 만에
218
+ * 만료되는 자리에 둔다). 그래서 자정마다 574.8 → 165.2 로 되돌아간다. 기준점을 말하지 않으면
219
+ * 커널이 그것을 계기 교체로 읽고, 「계기 교체가 하루에 한 번 일어난다」가 지난 기록에 남는다.
220
+ */
221
+ const { EmsKernel } = await import('@operato/twin-kernel')
222
+ const k = new EmsKernel('t')
223
+ k.loadTwinModel({
224
+ locations: [{ id: 'incoming', type: 'incoming', capacity: 0 }],
225
+ equipment: [{ id: 'inv-001', kind: 'pv-array', locationId: 'incoming' }]
226
+ } as any)
227
+
228
+ const send = (kWh: number, since: string | undefined, at: string) => {
229
+ const { accepted, rejected } = ingestCanonicalRecords(
230
+ [{ equipmentId: 'inv-001', kWh, ...(since ? { kWhSince: since } : {}), at }] as any,
231
+ 't',
232
+ at
233
+ )
234
+ assert.deepEqual(rejected, [], JSON.stringify(rejected))
235
+ for (const e of accepted) k.apply(e as any)
236
+ }
237
+ const inverter = () => (k.getSnapshot() as any).equipment.find((e: any) => e.id === 'inv-001')
238
+
239
+ /* 첫날 저녁 — 그날 자정부터 574.8kWh */
240
+ send(574.8, '2026-08-26T15:00:00.000Z', '2026-08-27T14:37:00.000Z')
241
+ assert.equal(inverter().generatedKWh, 574.8, '적산이 상태에 선다')
242
+
243
+ /* 다음 날 아침 — 값이 줄었지만 기준점이 나아갔다 */
244
+ send(165.2, '2026-08-27T15:00:00.000Z', '2026-08-28T01:12:00.000Z')
245
+ const after = inverter()
246
+ assert.equal(after.generatedKWh, 165.2)
247
+ assert.equal(after.generatedKWhSince, '2026-08-27T15:00:00.000Z', '어느 구간의 적산인지 남는다')
248
+ assert.equal(after.generatedKWhResetAt, undefined, '구간이 새로 시작한 것은 계기의 이상이 아니다')
249
+ })
250
+
251
+ test('기준점 없이 값이 줄면 모른다고 표시한다 — 짐작을 사실로 적지 않는다', async () => {
252
+ const { EmsKernel } = await import('@operato/twin-kernel')
253
+ const k = new EmsKernel('t')
254
+ k.loadTwinModel({
255
+ locations: [{ id: 'incoming', type: 'incoming', capacity: 0 }],
256
+ equipment: [{ id: 'inv-002', kind: 'pv-array', locationId: 'incoming' }]
257
+ } as any)
258
+ const send = (kWh: number, at: string) => {
259
+ const { accepted } = ingestCanonicalRecords([{ equipmentId: 'inv-002', kWh, at }] as any, 't', at)
260
+ for (const e of accepted) k.apply(e as any)
261
+ }
262
+ send(574.8, '2026-08-27T14:37:00.000Z')
263
+ send(165.2, '2026-08-28T01:12:00.000Z')
264
+ const eq = (k.getSnapshot() as any).equipment.find((e: any) => e.id === 'inv-002')
265
+ assert.ok(eq.generatedKWhResetAt, '되돌아간 사실은 남긴다')
266
+ assert.equal(eq.generatedKWhBasis, 'inferred', '그 판정이 짐작이라는 것도 함께 남긴다')
267
+ })
268
+
269
+ /*
270
+ * ── 여섯째·일곱째 어휘 — 사용 구간과 청구서 (2026-08-29, 커널 0.7.72) ──────────
271
+ *
272
+ * 커널이 문을 열었고, 이 seam 은 또 그 어휘를 몰랐다. 발전 적산 때와 같은 자리다.
273
+ *
274
+ * 이번 겹침이 더 나쁘다. `isEnergyRecord` 는 `meterId` 가 있으면 참이므로 사용 구간에도 참이다.
275
+ * 순서를 정하지 않으면 시간별 사용량이 계량 표본으로 들어가는데, 그 문은 `kWh` 를 적산
276
+ * 레지스터로 읽는다. 우리가 넣는 값은 이미 구간의 양이라 차분의 차분이 된다 — **거부되지 않고
277
+ * 값만 틀린다.**
278
+ */
279
+ const usageRecord = {
280
+ meterId: 'incoming',
281
+ from: '2026-08-28T15:00:00.000Z',
282
+ to: '2026-08-28T16:00:00.000Z',
283
+ kWh: 474.48,
284
+ maxKW: 478.08,
285
+ unitPrice: 87.3,
286
+ currency: 'KRW',
287
+ basis: 'provider'
288
+ }
289
+ const billRecord = {
290
+ from: '2026-06-30T15:00:00.000Z',
291
+ to: '2026-07-31T15:00:00.000Z',
292
+ demandCharge: 13728000,
293
+ currency: 'KRW',
294
+ billingDemandKW: 1650
295
+ }
296
+
297
+ test('★ 사용 구간은 계량 표본이 아니라 자기 문으로 들어온다', () => {
298
+ const { accepted, rejected } = ingestCanonicalRecords([usageRecord] as any, 't', NOW)
299
+ assert.deepEqual(rejected, [], JSON.stringify(rejected))
300
+ assert.equal(accepted.length, 1)
301
+ assert.notEqual(accepted[0].eventType, 'energy.measured', '계량 문으로 가면 kWh 가 적산으로 읽힌다')
302
+ })
303
+
304
+ test('청구서도 자기 문으로 들어온다', () => {
305
+ const { accepted, rejected } = ingestCanonicalRecords([billRecord] as any, 't', NOW)
306
+ assert.deepEqual(rejected, [], JSON.stringify(rejected))
307
+ assert.equal(accepted.length, 1)
308
+ assert.notEqual(accepted[0].eventType, 'epcis.ObjectEvent', '물품 관측으로 만들지 않는다')
309
+ })
310
+
311
+ test('계량 표본은 그대로 계량 문으로 간다 — 이 갈림이 기존 경로를 막지 않는다', () => {
312
+ const { accepted, rejected } = ingestCanonicalRecords(
313
+ [{ meterId: 'incoming', kW: 1200, at: '2026-08-14T09:00:30.000Z' }] as any,
314
+ 't',
315
+ NOW
316
+ )
317
+ assert.deepEqual(rejected, [])
318
+ assert.equal(accepted[0].eventType, 'energy.measured')
319
+ })
320
+
321
+ test('한 배치에 넷이 섞여도 각자의 문으로 간다', () => {
322
+ const { accepted, rejected } = ingestCanonicalRecords(
323
+ [energyRecord, generationRecord, usageRecord, billRecord] as any,
324
+ 't',
325
+ NOW
326
+ )
327
+ assert.deepEqual(rejected, [], JSON.stringify(rejected))
328
+ assert.equal(accepted.length, 4, '넷 다 받아들인다')
329
+ assert.equal(new Set(accepted.map(e => e.eventType)).size, 4, '넷이 서로 다른 사건이다')
330
+ })
331
+
332
+ /*
333
+ * ── 여덟째 어휘 — 요금 기준 (2026-08-29, 커널 0.7.76) ─────────────────────────
334
+ *
335
+ * 문이 열렸는데 유입 갈림이 또 몰랐다. 오늘 세 번째다.
336
+ */
337
+ const tariffBasisRecord = {
338
+ from: '2026-07-31T15:00:00.000Z',
339
+ to: '2026-08-31T15:00:00.000Z',
340
+ billingDemandKW: 1650,
341
+ demandChargePerKW: 8320,
342
+ currency: 'KRW'
343
+ }
344
+
345
+ test('★ 요금 기준이 자기 문으로 들어온다', () => {
346
+ const { accepted, rejected } = ingestCanonicalRecords([tariffBasisRecord] as any, 't', NOW)
347
+ assert.deepEqual(rejected, [], JSON.stringify(rejected))
348
+ assert.equal(accepted.length, 1)
349
+ assert.equal(accepted[0].eventType, 'energy.tariff.basis')
350
+ })
351
+
352
+ test('요금 기준의 사건 시각은 기간의 시작이다 — 진행 중인 주기도 미래에 적히지 않는다', () => {
353
+ const { accepted } = ingestCanonicalRecords([tariffBasisRecord] as any, 't', NOW)
354
+ assert.equal(accepted[0].eventTime, '2026-07-31T15:00:00.000Z', '청구서와 달리 to 가 아니다')
355
+ })
356
+
357
+ test('요금 기준과 청구서를 섞어도 각자의 문으로 간다', () => {
358
+ const { accepted, rejected } = ingestCanonicalRecords([tariffBasisRecord, billRecord] as any, 't', NOW)
359
+ assert.deepEqual(rejected, [], JSON.stringify(rejected))
360
+ assert.deepEqual(accepted.map(e => e.eventType).sort(), ['energy.bill', 'energy.tariff.basis'])
361
+ })
362
+
363
+ test('설비의 전기 계측값은 설비 상태 문으로 간다 — 발전 출력이 없어도 받는다', () => {
364
+ const { accepted, rejected } = ingestCanonicalRecords(
365
+ [{ equipmentId: '002', dcVoltage: 469.13, dcCurrent: 68.7, acVoltage: [377.8, 379.9, 377.7], acCurrent: [37, 37.2, 37] }] as any,
366
+ 't',
367
+ NOW
368
+ )
369
+ assert.deepEqual(rejected, [], JSON.stringify(rejected))
370
+ assert.equal(accepted[0].eventType, 'energy.equipment')
371
+ })
372
+
373
+ /* ── 발전 단가가 지나갈 길 (2026-08-30) ────────────────────────────────────
374
+ *
375
+ * ★ 오늘 다섯 번째다. 커널에 문을 열고 이 갈림에 길을 내지 않으면, 레코드가 아무 문에도
376
+ * 들어가지 못하고 조용히 사라진다 — 거부도 되지 않으므로 아무도 모른다.
377
+ */
378
+ test('발전 단가가 그 문으로 간다', () => {
379
+ const r = ingestCanonicalRecords(
380
+ [{ from: '2026-08-29T15:00:00.000Z', to: '2026-08-30T15:00:00.000Z', unitPrice: 148.61, currency: 'KRW' }] as any,
381
+ 't',
382
+ NOW
383
+ )
384
+ assert.equal(r.rejected.length, 0, `거부되면 안 된다: ${JSON.stringify(r.rejected)}`)
385
+ assert.equal(r.accepted.length, 1)
386
+ assert.equal((r.accepted[0] as any).eventType, 'energy.generation.price')
387
+ assert.equal((r.accepted[0] as any).eventTime, '2026-08-29T15:00:00.000Z', '사건 시각은 기간의 시작이다')
388
+ })
389
+
390
+ test('★ 단가·사용 구간·요금 기준·청구서가 한 배치에 섞여 와도 각자의 문으로 간다', () => {
391
+ const r = ingestCanonicalRecords(
392
+ [
393
+ { from: '2026-08-29T15:00:00.000Z', to: '2026-08-30T15:00:00.000Z', unitPrice: 148.61, currency: 'KRW' },
394
+ { meterId: 'incoming', from: '2026-08-29T15:00:00.000Z', to: '2026-08-29T16:00:00.000Z', kWh: 474.48 },
395
+ { from: '2026-07-31T15:00:00.000Z', to: '2026-08-31T15:00:00.000Z', billingDemandKW: 1650, demandChargePerKW: 8320, currency: 'KRW' },
396
+ { from: '2026-06-30T15:00:00.000Z', to: '2026-07-31T15:00:00.000Z', demandCharge: 13_728_000, currency: 'KRW' }
397
+ ] as any,
398
+ 't',
399
+ NOW
400
+ )
401
+ assert.equal(r.rejected.length, 0, JSON.stringify(r.rejected))
402
+ const types = (r.accepted as any[]).map(e => e.eventType).sort()
403
+ assert.deepEqual(types, ['energy.bill', 'energy.generation.price', 'energy.tariff.basis', 'energy.usage.period'])
404
+ })
@@ -1,5 +1,5 @@
1
1
  /*
2
- * ★ 비직렬 수량이 들어오는 문 — **조용히 사라지지 않는다.**
2
+ * ★ 비직렬 수량이 들어오는 문 — **오류 없이 사라지지 않는다.**
3
3
  *
4
4
  * `CanonicalRecord` 에 `quantityList` 자리를 냈다(담김의 `childQuantityList` 가 하루 먼저 같은 이유로
5
5
  * 받은 자리다). 원본이 「이 자리에 이 품목이 얼마 남았다」를 말할 길이 없어서, 그것이 주된 경로인
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * ── 배포 뒤 (2026-08-21) ────────────────────────────────────────────────────
9
9
  * 커널 0.7.40 에 `ObjectEventMapping.quantityList` 가 들어와 **수량만 오는 레코드가 통과한다.**
10
- * 그 전까지는 `epcList` 와 `quantityList` 가 둘 다 비어 검증이 거부했고, 이 시험은 그 거부가 **조용하지
10
+ * 그 전까지는 `epcList` 와 `quantityList` 가 둘 다 비어 검증이 거부했고, 이 시험은 그 거부가 **아무 말도 없지
11
11
  * 않다**(이유와 함께 계수된다)는 것을 지켰다.
12
12
  *
13
13
  * 이제 통과 쪽으로 고정한다 — 통과와 거부를 둘 다 받아들이면 다시 막혔을 때 초록으로 남는다.
@@ -8,7 +8,7 @@
8
8
  import { test } from 'node:test'
9
9
  import assert from 'node:assert/strict'
10
10
 
11
- import { DOMAIN_CATALOG, CAPABILITIES, CAPABILITY_KEYS, capabilitiesForType, stateFieldsOf } from '@operato/twin-kernel'
11
+ import { DOMAIN_CATALOG, CAPABILITIES, CAPABILITY_KEYS, capabilitiesForType, stateFieldsOf } from '@operato/ops-contract'
12
12
  import { buildEntityDeltas } from '../shared/entity-delta.ts'
13
13
 
14
14
  const SYSTEMS = ['wms', 'yms', 'mes'] as const
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * 실측(포천, `operato-chef-v1-pocheon`):
10
10
  *
11
- * 저널을 접으면 items 741 · orders 2,856 · tasks 9,250 · nowTime 2026-04-15
11
+ * 저널을 계산하면 items 741 · orders 2,856 · tasks 9,250 · nowTime 2026-04-15
12
12
  * 라이브 미러 items 0 · orders 0 · tasks 0 · nowTime 2026-01-01
13
13
  * 저장된 스냅샷 rev 221,884 · items 741 · orders 2,780 · tasks 6,353 (어제 것)
14
14
  * 그런데 이 판이 같은 키에 **빈 상태**를 쓰고 있었다
@@ -98,6 +98,6 @@ test('엔진이 실제로 이 판정을 한다 — 식만 시험하면 엔진이
98
98
  const body = code.slice(at, at + 2000)
99
99
  assert.match(body, /ingestedTotal/, '「들은 것이 있나」를 본다')
100
100
  assert.match(body, /items\?\.length[\s\S]{0,80}orders\?\.length[\s\S]{0,80}tasks\?\.length/, '관측 축 셋을 합쳐 센다')
101
- assert.match(body, /checkpoint refused/, '거절을 말한다 — 조용히 거절하지 않는다')
101
+ assert.match(body, /checkpoint refused/, '거절을 말한다 — 알리지 않고 거절하지 않는다')
102
102
  assert.doesNotMatch(body.slice(0, body.indexOf('checkpoint refused')), /revision >|revision </, '리비전 비교로 판정하지 않는다')
103
103
  })