@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
@@ -0,0 +1,3 @@
1
+ import { TwinBackfillResolver } from './twin-backfill-resolver.js'
2
+
3
+ export const resolvers = [TwinBackfillResolver]
@@ -0,0 +1,35 @@
1
+ import { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'
2
+ import { ScalarObject } from '@things-factory/shell'
3
+
4
+ import { backfillPeriodFacts } from './backfill-period-facts.js'
5
+
6
+ /*
7
+ * 지난 기록 채우기의 **문** — 커넥터가 트윈이 없던 동안의 날들을 뒤늦게 넣는다.
8
+ *
9
+ * ── 왜 문을 따로 두나 (2026-08-30) ────────────────────────────────────────
10
+ * 라이브 유입과 판단이 다르다. 라이브는 트윈의 「지금」을 바꾸고, 이쪽은 저널에만 쓴다. 같은 문으로
11
+ * 받으면 옛 표본이 가드에 막히거나 트윈의 시계가 과거로 끌린다(§`backfill-period-facts`).
12
+ *
13
+ * 레코드의 **모양은 라이브와 같다** — 커넥터가 다른 어휘를 배울 필요가 없다. 다른 것은 받는 종류뿐이고,
14
+ * 그 판단은 커널의 유입과 `judgeBackfill` 이 한다.
15
+ *
16
+ * ── 왜 이 파일이 늦게 생겼나 ──────────────────────────────────────────────
17
+ * 채우기 자체는 만들어 두고 **부르는 곳을 만들지 않아** 쓸 수 없는 채로 있었다. 8월 29일 발전량을
18
+ * 되살리려다 드러났다. 같은 부류를 그날 두 번 겪었다(커널의 발전 기간 문도 없었다).
19
+ */
20
+ @Resolver()
21
+ export class TwinBackfillResolver {
22
+ /* 트윈의 모든 변경과 같은 게이트다(ADR-0027) — 저널에 사실을 적는 일이므로 변경이다. */
23
+ @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
24
+ @Mutation(returns => ScalarObject, {
25
+ description:
26
+ 'Backfill closed-period facts into a twin journal without touching its live state. Records use the same shape as live ingest; only closed-period facts are accepted (usage/generation periods, generation price, tariff basis, bill). Point-in-time observations are refused with a reason, as are times beyond journal retention. Re-running is safe: a period already present is skipped, because a closed period is identified by its content. Returns { written, skipped, refused }.'
27
+ })
28
+ async backfillTwinPeriodFacts(
29
+ @Arg('instanceId') instanceId: string,
30
+ @Arg('records', type => [ScalarObject]) records: unknown[],
31
+ @Ctx() context: ResolverContext
32
+ ): Promise<{ written: number; skipped: number; refused: { record: unknown; reason: string }[] }> {
33
+ return await backfillPeriodFacts({ domainId: context.state.domain.id, instanceId, records })
34
+ }
35
+ }
@@ -1,7 +1,7 @@
1
1
  import { twinError } from '../../engine/log.js'
2
2
  import { Arg, Ctx, Mutation, Query, Resolver, Directive } from 'type-graphql'
3
3
 
4
- import { validateScenario } from '@operato/twin-kernel'
4
+ import { validateScenario } from '@operato/ops-contract'
5
5
 
6
6
  import { ScalarObject } from '@things-factory/shell'
7
7
 
@@ -50,7 +50,7 @@ export class TwinControlMutation {
50
50
  * 증명할 수 있다. 커널이 거절한 것도 남긴다 — 무엇을 시도했는지가 감사의 절반이다.
51
51
  *
52
52
  * 기록이 실패해도 커맨드 결과는 그대로 돌려준다. 감사가 본업을 막으면 안 되지만, 실패를 삼키지도
53
- * 않는다(로그에 남긴다) — 조용히 안 남으면 나중에 "기록이 없다" 와 "일이 없었다" 를 구분 못 한다.
53
+ * 않는다(로그에 남긴다) — 알리지 않고 안 남으면 나중에 "기록이 없다" 와 "일이 없었다" 를 구분 못 한다.
54
54
  */
55
55
  try {
56
56
  const audit = buildCommandAudit({ command: cmdWithOrigin, ack, actor: context.state.user, at: new Date() })
@@ -29,7 +29,7 @@ export interface TwinEventKeys {
29
29
 
30
30
  /*
31
31
  * 컬럼 길이 상한. GS1 식별자(EPC URN·GDTI·SGLN)는 규격상 이보다 훨씬 짧다.
32
- * 넘치는 값이 오면 **조용히 자르지 않고** 경고를 남긴다 — 색인이 원본과 다르면 검색 결과가 거짓이 되는데,
32
+ * 넘치는 값이 오면 **알리지 않고 자르지 않고** 경고를 남긴다 — 색인이 원본과 다르면 검색 결과가 거짓이 되는데,
33
33
  * 그 사실이 어디에도 안 남으면 아무도 모른다.
34
34
  */
35
35
  const MAX_KEY = 255
@@ -128,7 +128,7 @@ export function equipmentIdOf(envelope: any): string | undefined {
128
128
  * EPCIS 행위 — `ADD`·`OBSERVE`·`DELETE`. 세 값이 아니면 `undefined`(원천의 잡값을 색인에 넣지 않는다).
129
129
  *
130
130
  * 이 키는 검색 축이 아니라 **판별식**이다: 되풀어 읽는 스냅샷은 원리적으로 `DELETE` 를 못 낸다.
131
- * 그래서 「이 트윈이 반출을 한 번이라도 접었는가」가 진짜 delta 피드의 증거인데, payload 안에 있는
131
+ * 그래서 「이 트윈이 반출을 한 번이라도 계산했는가」가 진짜 delta 피드의 증거인데, payload 안에 있는
132
132
  * 동안에는 5개 드라이버 어디에서도 그 질문을 할 수 없었다. 운영 델타에는 행위가 없다 — 비운다.
133
133
  */
134
134
  export function actionOf(envelope: any): string | undefined {
@@ -6,7 +6,7 @@ import { TwinEvent } from './twin-event.js'
6
6
  * 저널 목록 반환형 — things-factory 표준 `{ items, total }`(AttributeSetList·DomainList 등과 동형).
7
7
  *
8
8
  * `total` 이 이 타입의 존재 이유다. 기존 `twinEvents` 는 배열만 돌려줘서 **화면이 자기가 전체를 받은
9
- * 건지 잘린 건지 알 방법이 없었다** — 그래서 리스트가 조용히 잘린 채로 "이게 전부" 처럼 보였다.
9
+ * 건지 잘린 건지 알 방법이 없었다** — 그래서 리스트가 알리지 않고 잘린 채로 "이게 전부" 처럼 보였다.
10
10
  * 총건수를 함께 주면 화면은 "48 / 12,904" 라고 정직하게 말할 수 있고, 사용자는 좁혀야 한다는 걸 안다.
11
11
  */
12
12
  @ObjectType({ description: 'A page of twin journal events together with the total number of matching records.' })
@@ -48,4 +48,21 @@ export class TwinEventList {
48
48
  */
49
49
  @Field({ nullable: true, description: 'Opaque cursor for the next page. Pass it back as pagination.after to continue exactly where this page ended, without the duplicate or skipped rows that offset paging produces on a journal that keeps growing at the head. Null when this page is the last one.' })
50
50
  nextCursor?: string
51
+
52
+ /*
53
+ * **앞부분이 지워졌다** — 보관 기간 정리가 이 트윈의 오래된 사건을 지운 경우 (2026-08-27).
54
+ *
55
+ * 정리는 선언된 보관 기간(기본 7일)보다 오래된 사건을 지운다. 그러면 이 목록의 첫 페이지 뒤로 더
56
+ * 내려가도 어느 지점에서 기록이 끝난다. 화면이 그것을 말하지 않으면 사람은 「이 트윈은 그때부터
57
+ * 시작했다」로 읽는다. 없었던 것과 지워진 것은 다른 사실이다.
58
+ *
59
+ * 값은 저장하지 않고 조회할 때 구한다(남아 있는 가장 작은 리비전). 저장하면 실제 데이터와 어긋날 수
60
+ * 있고, 어긋나면 어느 쪽이 사실인지 알 수 없다.
61
+ */
62
+ @Field(type => Int, {
63
+ nullable: true,
64
+ description:
65
+ 'The oldest revision still stored for this twin. When it is greater than 1, older events were deleted by the journal retention policy: the screen must say the record starts here rather than implying the twin began at this point. NULL when the caller asked for more than one twin (the axis is per twin) or when nothing is stored.'
66
+ })
67
+ oldestRevision?: number
51
68
  }
@@ -34,8 +34,34 @@ import { Domain, ScalarObject } from '@things-factory/shell'
34
34
  @Index('ix_twin_event_4', (e: TwinEvent) => [e.domain, e.instanceId, e.orderId], { unique: false })
35
35
  /* ix_5 — "이 지시가 무엇을 일으켰나". 감사 기록의 commandId 로 저널을 다시 계산하는 축. */
36
36
  @Index('ix_twin_event_5', (e: TwinEvent) => [e.domain, e.correlationId], { unique: false })
37
- /* ix_6 — "반출을 한 번이라도 접었나". 없으면 그 질문이 인스턴스 저널 전량 주사가 된다. */
37
+ /* ix_6 — "반출을 한 번이라도 계산했나". 없으면 그 질문이 인스턴스 저널 전량 주사가 된다. */
38
38
  @Index('ix_twin_event_6', (e: TwinEvent) => [e.domain, e.instanceId, e.action], { unique: false })
39
+ /*
40
+ * ix_7 — **"이 종류의 사실을 시각순으로"** (2026-08-25 실측으로 추가)
41
+ *
42
+ * 이 축이 없어서 공정 타임라인이 22초였다. 종류로 거르고 시각으로 정렬하는 조회가 갈 곳이 없었다:
43
+ *
44
+ * ix_2 (…, eventType, revision) 종류는 걸러지고 정렬이 **적힌 순서**다 → 4월 사실을 최근으로 그렸다
45
+ * ix_1 (…, eventTime) 정렬은 맞고 **종류를 못 거른다** → 행마다 테이블을 열어 확인한다
46
+ *
47
+ * 뒤쪽으로 걸으면 그 트윈에서 찾는 종류가 멈춘 시점까지의 모든 행을 읽고 버린다. 승화푸드에서 작업
48
+ * 사실은 8월 22일에 멈춰 있고 그 뒤로 리비전이 110만 늘었으므로, 200건에 닿기까지 110만 행을 열었다.
49
+ *
50
+ * 위 주석이 「측정을 근거로 추가할 일이지 지레 얹을 일은 아니다」라고 적어 두었다. 그 측정이 나왔다.
51
+ */
52
+ @Index('ix_twin_event_7', (e: TwinEvent) => [e.domain, e.instanceId, e.eventType, e.eventTime], { unique: false })
53
+ /*
54
+ * ix_8 — **"이 트윈의 오래된 기록"** (2026-08-27 실측으로 추가)
55
+ *
56
+ * 보관 기간 정리가 `created_at` 으로 자르는데(보관은 저장 나이의 문제다) 그 축에 인덱스가 없었다.
57
+ * 그래서 정리가 매 배치마다 그 트윈의 저널을 전량 주사했다.
58
+ *
59
+ * hatio-mx2 의 7일 밖 행을 세기 208초 ← 1,029만 행을 전부 읽는다(7일 밖은 2,446건뿐이다)
60
+ * revision 으로 한 행 짚기 0.004초
61
+ *
62
+ * 정리는 한 창에 2초만 쓰도록 예산이 걸려 있어서, 그 2초를 주사에 다 쓰고 44건만 지우고 멈췄다.
63
+ */
64
+ @Index('ix_twin_event_8', (e: TwinEvent) => [e.domain, e.instanceId, e.createdAt], { unique: false })
39
65
  @ObjectType({ description: 'Append-only twin event journal record (EPCIS event or operational delta).' })
40
66
  export class TwinEvent {
41
67
  @PrimaryGeneratedColumn('uuid')
@@ -71,8 +97,8 @@ export class TwinEvent {
71
97
  * 이 사실이 **어느 공장에서** 일어났나 — `TwinStructure.rev`.
72
98
  *
73
99
  * 이것이 없던 시절에는 구조가 바뀌면 저널을 통째로 지우는 것이 유일한 길이었다(안 지우면 옛
74
- * 이벤트를 새 공장에 대고 접어 이력이 거짓말을 한다). 이 한 칸이 셋째 길을 연다 — 재생이
75
- * 구조가 바뀌는 지점에서 전환한 뒤 이어 접는다.
100
+ * 이벤트를 새 공장에 대고 계산해 이력이 거짓말을 한다). 이 한 칸이 셋째 길을 연다 — 재생이
101
+ * 구조가 바뀌는 지점에서 전환한 뒤 이어서 계산한다.
76
102
  *
77
103
  * **비어 있을 수 있다**: 컬럼이 생기기 전에 쓰인 행이다. 그 행들은 **가장 오래된 리비전**에
78
104
  * 속한다(0 을 채워 넣지 않는다 — 0 은 "모름" 이 아니라 유효한 번호처럼 보인다).
@@ -85,7 +111,7 @@ export class TwinEvent {
85
111
  * 이 사실이 일어난 시각(트윈의 시계). **날짜 타입이다.**
86
112
  *
87
113
  * 예전에는 문자열이었다. 읽을 때마다 `Date.parse` 했고, 정렬은 UTC ISO 만 들어온다는 전제 위에서
88
- * 사전식 비교가 우연히 맞았다 — `+09:00` 오프셋 표기가 하나 섞이면 조용히 순서가 틀린다.
114
+ * 사전식 비교가 우연히 맞았다 — `+09:00` 오프셋 표기가 하나 섞이면 알리지 않고 순서가 틀린다.
89
115
  * 그 전제는 컬럼 어디에도 적혀 있지 않았다.
90
116
  *
91
117
  * **타입 이름을 적지 않는다** (2026-08-19 Postgres 부팅 실패로 고침).
@@ -108,11 +134,11 @@ export class TwinEvent {
108
134
  * payload 가 여전히 정본이고 이것들은 **파생 색인**이다 — 새로운 진실이 아니라 찾을 수 있게 하는 장치.
109
135
  * simple-json(TEXT) 안의 값은 5개 DB 드라이버 공통으로 거를 방법이 없어서(멀티DB 호환 규칙상
110
136
  * DB별 JSON 연산자 금지) 승격 외의 선택지가 없다.
111
- * 길이 상한 255 는 GS1 식별자 규격 대비 충분하며, 넘치는 값은 조용히 잘리지 않고 경고를 남긴다.
137
+ * 길이 상한 255 는 GS1 식별자 규격 대비 충분하며, 넘치는 값은 알리지 않고 잘리지 않고 경고를 남긴다.
112
138
  *
113
139
  * ── 축을 새로 추가할 때 ──────────────────────────────────────────────────
114
140
  * 새 승격 컬럼을 만들면 **이미 쌓인 행은 비어 있다.** 그 행들은 새 축으로 검색되지 않고,
115
- * 사용자에게는 "과거가 없는 것" 으로 보인다(조용히 빠진 데이터).
141
+ * 사용자에게는 "과거가 없는 것" 으로 보인다(알리지 않고 빠진 데이터).
116
142
  *
117
143
  * 예전에는 기동 훅이 매번 테이블을 살펴 채웠다. 그건 두 가지로 틀렸다 — 저널이 커질수록 부팅이
118
144
  * 느려지고, "bizStep 이 비었으면 옛 행" 이라는 전제를 코드에 고정해 그 전제가 깨지는 순간
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * ── 왜 순수 모듈인가 (2026-08-15) ───────────────────────────────────────────
5
5
  * 이 표가 리졸버 안에 있어서 시험이 불러올 수 없었다(type-graphql·shell 을 물고 있다). 그런데 여기서
6
- * 틀리면 예측 그래프 전체가 조용히 다른 것을 그린다 — 오류 없이. 규칙은 시험이 닿는 자리에 둔다.
6
+ * 틀리면 예측 그래프 전체가 알리지 않고 다른 것을 그린다 — 오류 없이. 규칙은 시험이 닿는 자리에 둔다.
7
7
  *
8
8
  * ── 에너지 지표를 더한 이유 ─────────────────────────────────────────────────
9
9
  * 예측 화면의 지표가 물류 여섯(재고·점유·작업·오더·출고·백로그)으로 고정돼 있어서, 에너지 트윈을
@@ -23,7 +23,7 @@ export interface GapPoint {
23
23
  modelAbsError: number
24
24
  }
25
25
 
26
- /** 합산 결과 + **무엇을 못 넣었는지.** 조용히 줄이지 않는다. */
26
+ /** 합산 결과 + **무엇을 못 넣었는지.** 알리지 않고 줄이지 않는다. */
27
27
  export interface AggregateResult {
28
28
  points: GapPoint[]
29
29
  /** 한 트윈이라도 재지 못해 통째로 뺀 vantage 수. */
@@ -1,4 +1,5 @@
1
1
  import { twinLog } from '../../engine/log.js'
2
+ import { hydratedDate } from '../../engine/read-time.js'
2
3
  import { Arg, Ctx, Int, Mutation, Query, Resolver, Directive} from 'type-graphql'
3
4
 
4
5
  import { getRepository, ScalarObject } from '@things-factory/shell'
@@ -80,13 +81,31 @@ const calKey = (d: string, i: string, m: string, h: number) => `${d}:${i}:${m}:$
80
81
  * 지평 H 만큼 앞을 봐야 "그때 예측한 것"과 "실제로 벌어진 것"을 맞대 볼 수 있으므로 끝에서 H 를 뺀다.
81
82
  */
82
83
  async function gapWindow(domainId: string, instanceId: string, H: number): Promise<{ t0: number; lastVantage: number } | null> {
83
- const where = { domain: { id: domainId }, instanceId }
84
+ /*
85
+ * ── **양 끝을 집계로 묻는다** (2026-08-28) ──────────────────────────────────
86
+ *
87
+ * 예전에는 `findOne({ where: { domain: { id } }, order: { eventTime } })` 두 번이었다. 관계를 가진
88
+ * 엔티티에서 `findOne` 은 두 단계 질의가 되고 **안쪽에 상한이 없다** — 그 트윈의 저널을 전부 만든 뒤
89
+ * 한 줄을 고른다. 승화푸드는 337만 행이고, 그것을 두 번 했다.
90
+ *
91
+ * `MIN`/`MAX` 는 `(domain, instance, event_time)` 색인의 양 끝을 집으므로 각각 한 줄이다.
92
+ * 이 파일의 다른 자리와 같은 규율이다(§`twin-engine` 의 `tipOf`).
93
+ */
84
94
  const repo = getRepository(TwinEvent)
85
- const first = await repo.findOne({ where, order: { eventTime: 'ASC' } })
86
- const last = await repo.findOne({ where, order: { eventTime: 'DESC' } })
87
- if (!first?.eventTime || !last?.eventTime) return null
88
- const t0 = first.eventTime.getTime()
89
- const lastVantage = last.eventTime.getTime() - H
95
+ const edge = (agg: 'MIN' | 'MAX') =>
96
+ repo
97
+ .createQueryBuilder('e')
98
+ .select(`${agg}(e.eventTime)`, 'at')
99
+ .where('e.domain = :domainId', { domainId })
100
+ .andWhere('e.instanceId = :instanceId', { instanceId })
101
+ .getRawOne<{ at: unknown }>()
102
+ const [firstRow, lastRow] = await Promise.all([edge('MIN'), edge('MAX')])
103
+ /* 드라이버가 준 값은 시간대가 없을 수 있다 — 컬럼 변환을 지나게 한다(§`hydratedDate`). */
104
+ const first = hydratedDate(repo, 'eventTime', firstRow?.at)
105
+ const last = hydratedDate(repo, 'eventTime', lastRow?.at)
106
+ if (!first || !last) return null
107
+ const t0 = first.getTime()
108
+ const lastVantage = last.getTime() - H
90
109
  return lastVantage > t0 ? { t0, lastVantage } : null
91
110
  }
92
111
 
@@ -188,7 +207,7 @@ export class TwinForecastQuery {
188
207
  * **배운 모델에만 적용한다.** 보정은 "그때 모델이 이만큼 치우쳐 있다" 를 배운 것이다. 소요시간
189
208
  * 추정기(실측·거리)나 공정 명세가 바뀌어 모델이 달라지면 그 편향은 더 이상 사실이 아니고, 그대로
190
209
  * 밀어 주면 이중 보정이 된다. 그래서 지문을 비교하고, 다르면 적용하지 않되 **그 사실을 응답에
191
- * 싣는다** — 조용히 사라지면 사용자는 학습이 왜 안 듣는지 알 수 없다(다시 배우면 된다).
210
+ * 싣는다** — 오류 없이 사라지면 사용자는 학습이 왜 안 듣는지 알 수 없다(다시 배우면 된다).
192
211
  * 지문을 알 수 없을 때는 비교하지 않는다(모르는 것을 근거로 보정을 끊지 않는다).
193
212
  */
194
213
  const basis = await TwinEngine.modelBasis(domainId, instanceId)
@@ -312,7 +331,7 @@ export class TwinForecastQuery {
312
331
  /*
313
332
  * **가정이 어떻게 됐나** — 걸린 개입·거절된 개입·덮어쓴 선언.
314
333
  *
315
- * 이것을 감추면 두 가지가 조용해진다: 걸리지 않은 가정(「걸었는데 왜 안 바뀌나」)과, 덮어쓴
334
+ * 이것을 감추면 두 가지가 들어온 것이 없어진다: 걸리지 않은 가정(「걸었는데 왜 안 바뀌나」)과, 덮어쓴
316
335
  * 선언(사용자가 what-if 결과를 트윈의 현재 상태로 읽는다). 실제로 실행한 fork(`fc`)에서 읽는다 —
317
336
  * 가정을 겪은 것은 그쪽이다.
318
337
  */
@@ -342,10 +361,10 @@ export class TwinForecastQuery {
342
361
  *
343
362
  * 그런데 그 카운터는 **fork 안에** 있고 fork 는 버려진다. 여기서 싣지 않으면 아무에게도 닿지
344
363
  * 않는다 — 리듀서가 오래 세어 온 값을 스냅샷이 떨어뜨려 그것을 읽도록 쓰인 검사가 영원히
345
- * 조용했던 것과 **같은 실수**다. 세어 놓고 내보내지 않으면 안 센 것과 같다.
364
+ * 아무 신호도 내지 않았던 것과 **같은 실수**다. 세어 놓고 내보내지 않으면 안 센 것과 같다.
346
365
  *
347
366
  * 왜 화면이 이것을 알아야 하나: 계보는 **회수 범위**를 정하는 사슬이다. 구멍이 있는데 온전한
348
- * 것처럼 보이면 회수가 조용히 좁아진다. 식품에서 그것은 되돌릴 수 없다.
367
+ * 것처럼 보이면 회수가 알리지 않고 좁아진다. 식품에서 그것은 되돌릴 수 없다.
349
368
  *
350
369
  * 두 곳에서 읽어 **큰 값**을 낸다 — 분포(회차 R개의 최대)와 궤적 fork 는 **둘 다 실제로 실행됐다**.
351
370
  * 한쪽만 보면 다른 쪽이 겪은 구멍을 잃는다.
@@ -394,7 +413,7 @@ export class TwinForecastQuery {
394
413
  * 시각이 되어, 합산이 다른 시점의 값을 더한다(`aggregatePoints` 주석 참조).
395
414
  *
396
415
  * 저널이 없거나 지평 H 를 담을 만큼 길지 않은 트윈은 **백테스트에 참여할 수 없다.** 그런 트윈은
397
- * 빼되 누구를 뺐는지 돌려준다 — 조용히 빼면 화면은 그 트윈까지 포함한 공장 총량으로 읽는다.
416
+ * 빼되 누구를 뺐는지 돌려준다 — 알리지 않고 빼면 화면은 그 트윈까지 포함한 공장 총량으로 읽는다.
398
417
  */
399
418
  const windows = await Promise.all(ids.map(async id => ({ id, w: await gapWindow(domainId, id, H) })))
400
419
  const usable = windows.filter(x => x.w)
@@ -29,7 +29,7 @@ import { TwinIngestWindow } from './twin-ingest-window.js'
29
29
  * 한 번에 돌려주는 창 수 상한 — 144창 × 10분 = 24시간.
30
30
  *
31
31
  * 상한이 필요한 이유는 이 표가 **자란다**는 것이다(트윈 1,000개에 하루 14.4만 행). 상한에서 밀려난
32
- * 수는 **세어서 낸다** — 조용히 자르면 화면이 「그 앞에는 아무 일도 없었다」로 읽는다.
32
+ * 수는 **세어서 낸다** — 알리지 않고 자르면 화면이 「그 앞에는 아무 일도 없었다」로 읽는다.
33
33
  */
34
34
  export const HISTORY_WINDOW_LIMIT = 144
35
35
 
@@ -11,7 +11,7 @@ import { TwinIngestWindow } from './twin-ingest-window.js'
11
11
  * 두었고(`onIngestWindowClosed`), 이 파일이 부팅에서 한 번 등록한다. 그래서 기록 경로가 갈리지 않는다.
12
12
  *
13
13
  * ── 실패를 삼키지 않는다 ────────────────────────────────────────────────────
14
- * 쓰기가 터져도 메모리 장부는 계속 굴러간다(`rollIngestWindow` 가 훅을 감싼다). 다만 **조용히 넘기지는
14
+ * 쓰기가 터져도 메모리 장부는 계속 굴러간다(`rollIngestWindow` 가 훅을 감싼다). 다만 **알리지 않고 넘기지는
15
15
  * 않는다** — 실패를 로그로 남긴다. 그러지 않으면 추이 표에 구멍이 생긴 이유를 나중에 알 수 없다.
16
16
  *
17
17
  * ── 왜 await 하지 않나 ──────────────────────────────────────────────────────
@@ -73,7 +73,7 @@ export class TwinInstance {
73
73
  * 하나**였고, 그것은 원본의 성질이 아니라 **트윈과 자기 과거의 관계**다: 같은 시뮬레이터 원본을 보는 두
74
74
  * 트윈이 하나는 이어가고 하나는 매번 처음부터 재현할 수 있다. 그래서 축은 남고 이름만 바뀌었다.
75
75
  *
76
- * **비어 있을 수 없고, 기본값도 없다.** 예전에는 미선언이 조용히 `sim-experiment`(=저널 초기화)로
76
+ * **비어 있을 수 없고, 기본값도 없다.** 예전에는 미선언이 알리지 않고 `sim-experiment`(=저널 초기화)로
77
77
  * 떨어졌다 — 값이 하나 어긋나는 순간 이력을 지우는 길이다. 이제 읽는 쪽이 **오류를 낸다**
78
78
  * (`engine/restart-policy.ts`). 선언은 프로비저닝하는 쪽의 몫이다.
79
79
  */
@@ -83,7 +83,7 @@ export class TwinInstance {
83
83
 
84
84
  /* 읽을 때 어휘 세대를 흡수한다(`nodes`/`movers`/`equipmentList` → `locations`/`equipment`). (vocabulary-guard: allow — 세대 이름 없이는 설명할 수 없다.)
85
85
  자리마다 리더를 쓰게 하면 다음에 추가되는 자리가 또 빠지고, 빠진 것은 오류가 아니라 **빈 공장**이라
86
- 조용하다(2026-08-06: 모델러 엔티티 목록이 통째로 비었다). 상세 `engine/model-vocabulary.ts`. */
86
+ 들어온 것이 없다(2026-08-06: 모델러 엔티티 목록이 통째로 비었다). 상세 `engine/model-vocabulary.ts`. */
87
87
  @Column({ type: 'simple-json', nullable: true, transformer: modelColumnTransformer })
88
88
  @Field(type => ScalarObject, { nullable: true, description: 'TwinModelDef blueprint (locations/equipment) required to replay events into state. Older stored boards used nodes/movers/equipmentList; they are normalised on read so consumers see one vocabulary.' }) // vocabulary-guard: allow — 옛 어휘를 흡수하는 것이 이 코드의 일이다(세대 이름을 적어야 흡수할 수 있다)
89
89
  model?: any
@@ -92,7 +92,7 @@ export class TwinInstance {
92
92
  * 로컬 선언 겹 — **구조는 원천이, 수는 우리가.**
93
93
  *
94
94
  * 요금 단가·계약 정보·축전지 충전율처럼 원 시스템이 모르는 값을 넣는 자리다. `model` 에 직접 쓰지
95
- * 않는 이유는 그것이 **원천의 사본**이기 때문이다: 다시 읽기가 사본을 새로 그리면 직접 쓴 값은 조용히
95
+ * 않는 이유는 그것이 **원천의 사본**이기 때문이다: 다시 읽기가 사본을 새로 그리면 직접 쓴 값은 알리지 않고
96
96
  * 사라진다(요금이 사라지면 금액도 사라지고, 사용자는 이유를 알 수 없다).
97
97
  *
98
98
  * 그래서 여기 남기고, 모델을 그릴 때마다 그 위에 얹는다(`engine/local-declarations`). 얹힌 값에는
@@ -164,7 +164,7 @@ export class TwinInstance {
164
164
  * 이 세션에만 그 부류를 셋 겪었다(`model.operations` 두 집 · `materials` 두 집 · tenantId 중복).
165
165
  *
166
166
  * 복제한 값은 반드시 어긋난다 — 저널은 틱마다 늘고 이 컬럼은 저장할 때만 갱신되므로, 화면이
167
- * 어느 쪽을 읽느냐로 다른 숫자가 나온다. 그 조용한 어긋남을 만들 이유가 없다.
167
+ * 어느 쪽을 읽느냐로 다른 숫자가 나온다. 그 들어온 것이 없는 어긋남을 만들 이유가 없다.
168
168
  */
169
169
 
170
170
  @CreateDateColumn()
@@ -0,0 +1,112 @@
1
+ /*
2
+ * **이 물음이 「여러 종류를 시각순으로」인가** — 순수 판단만 여기 둔다.
3
+ *
4
+ * 데이터베이스를 부르지 않는다. 그래서 시험이 이 파일을 그대로 부를 수 있다(데코레이터가 없다).
5
+ * 판단이 조회 코드 안에 섞여 있으면 그 판단만 확인할 방법이 없고, 그러면 「어떤 물음이 나눠 묻는
6
+ * 갈래로 가는가」가 아무도 읽지 못하는 규칙이 된다.
7
+ *
8
+ * 실측 근거와 고침의 뜻은 `event-type-split.ts` 머리에 있다.
9
+ */
10
+ import { absoluteTime } from '../../engine/read-time.js'
11
+
12
+ /** 질의를 몇 개까지 나눌까 — 대상 × 종류. 이 이상이면 나누는 것이 손해다. */
13
+ export const MAX_SPLIT = 24
14
+
15
+ /** 종류 목록의 상한 — 이보다 많으면 「이 종류만」이 아니라 사실상 전체다. */
16
+ const MAX_TYPES = 12
17
+
18
+ export interface SplitShape {
19
+ eventTypes: string[]
20
+ /**
21
+ * 공정 단계 하나로 좁힌 경우 — 그 값을 갈래마다 함께 건다.
22
+ *
23
+ * 이 필터를 만나면 갈래를 포기하던 것을 고쳤다(2026-08-29). 원장은 종류를 담아 묻고 그 위에 단계
24
+ * 칩을 겹치는데, 칩을 고른 순간 예전 경로로 떨어져 **고르지 않은 쪽이 더 빨랐다.**
25
+ *
26
+ * 단계 컬럼에는 색인이 없다. 그래도 느려지지 않는다 — 종류 정확일치가 색인으로 시각 순서까지 주고,
27
+ * 단계는 그 안에서 행마다 확인하는 조건이다(걷는 행이 그 종류의 것으로 이미 좁혀져 있다).
28
+ */
29
+ bizStep?: string
30
+ untilTime?: Date
31
+ fromTime?: Date
32
+ }
33
+
34
+ /**
35
+ * 이 물음이 「여러 종류를 시각순으로」인가 — 아니면 `null`.
36
+ *
37
+ * 받는 모양
38
+ * `eventType` 을 `in` 으로 · 시각 상한이나 구간 · 시각 내림차순 정렬
39
+ *
40
+ * 보내는 모양(일반 경로로)
41
+ * 자유 검색어 · 그 밖의 필터(축·상관 식별자·업무 단계) · 그 밖의 정렬이나 연산자
42
+ */
43
+ export function splitShape(params: any, sortings: any[]): SplitShape | null {
44
+ const filters: any[] = Array.isArray(params?.filters) ? params.filters : []
45
+ if (typeof params?.search === 'string' && params.search.trim()) return null
46
+ if (sortings.length !== 1 || sortings[0]?.name !== 'eventTime' || sortings[0]?.desc !== true) return null
47
+
48
+ let eventTypes: string[] | undefined
49
+ let untilTime: Date | undefined
50
+ let fromTime: Date | undefined
51
+ let bizStep: string | undefined
52
+ for (const f of filters) {
53
+ if (f?.name === 'eventType') {
54
+ if (f.operator !== 'in' || !Array.isArray(f.value)) return null
55
+ const types: string[] = f.value.map((v: unknown) => String(v ?? '').trim()).filter((t: string) => !!t)
56
+ /* 하나면 나눌 것이 없다(이미 정확일치라 색인을 탄다). 너무 많으면 사실상 전체다. */
57
+ if (types.length < 2 || types.length > MAX_TYPES) return null
58
+ if (eventTypes) return null
59
+ eventTypes = [...new Set(types)]
60
+ } else if (f?.name === 'bizStep') {
61
+ /* 하나로 좁힌 경우만 받는다 — 여러 값이면 종류 × 단계로 갈래가 곱해진다. */
62
+ if (f.operator !== 'eq') return null
63
+ const v = String(f.value ?? '').trim()
64
+ if (!v) return null
65
+ if (bizStep) return null
66
+ bizStep = v
67
+ } else if (f?.name === 'eventTime') {
68
+ if (f.operator === 'lte' || f.operator === 'lt') {
69
+ untilTime = absoluteTime(f.value) ?? undefined
70
+ if (!untilTime) return null
71
+ } else if (f.operator === 'between' && Array.isArray(f.value)) {
72
+ fromTime = absoluteTime(f.value[0]) ?? undefined
73
+ untilTime = absoluteTime(f.value[1]) ?? undefined
74
+ if (!fromTime || !untilTime) return null
75
+ } else return null
76
+ } else return null
77
+ }
78
+ return eventTypes ? { eventTypes, untilTime, fromTime, bizStep } : null
79
+ }
80
+
81
+ /**
82
+ * **나눠 물은 갈래들을 합쳐 앞에서 `limit` 개** — 이 규율이 정확성의 근거다.
83
+ *
84
+ * ── 왜 갈래마다 `limit` 까지 받아야 하나 ────────────────────────────────────
85
+ * 갈래마다 `limit` 개를 받아 합치면, 합친 것의 앞 `limit` 개는 **전체에서도** 앞 `limit` 개다.
86
+ * 어떤 갈래에서 빠진 행은 그 갈래의 `limit` 번째보다 뒤이고, 우리는 전체에서 `limit` 개만 취하므로
87
+ * 그 행이 이 페이지에 들어올 수 없다.
88
+ *
89
+ * 갈래마다 **더 적게** 받으면 그 성질이 깨진다. 한 갈래가 앞을 다 차지하는 경우
90
+ * (종류 A 의 마지막이 종류 B 의 첫째보다 앞선다) 그 갈래에서 못 받은 것이 빠지고, 결과는 구간의
91
+ * 앞부분만 계산한 것이 된다 — 그런데 화면은 「일부만 읽었다」고만 말하고 어느 부분인지는 말하지 못한다.
92
+ * (2026-08-28 · UX 레인이 미리 짚어 준 함정이다.)
93
+ *
94
+ * 그래서 이 함수는 **`limit` 미만으로 받은 갈래가 있으면 그것을 말한다** — 부르는 쪽이 그 상태로
95
+ * 답하지 않게.
96
+ */
97
+ export function mergeStreams<T>(
98
+ streams: readonly T[][],
99
+ limit: number,
100
+ /** 앞에 와야 하는 것이 음수 — `Array.prototype.sort` 와 같은 규약. */
101
+ compare: (a: T, b: T) => number
102
+ ): { items: T[]; capped: boolean; underfilled: boolean } {
103
+ const underfilled = streams.some(s => s.length > limit)
104
+ const merged = streams.flat().sort(compare)
105
+ return {
106
+ items: merged.slice(0, limit),
107
+ /* 합친 것이 상한을 넘었으면 더 있었다는 뜻이다 — 잘랐다는 사실을 부르는 쪽이 말해야 한다. */
108
+ capped: merged.length > limit || streams.some(s => s.length >= limit),
109
+ /* 갈래가 상한보다 많이 받아 왔다 — 있을 수 없는 일이므로 부르는 쪽이 알아야 한다. */
110
+ underfilled
111
+ }
112
+ }
@@ -0,0 +1,144 @@
1
+ import { getRepository } from '@things-factory/shell'
2
+
3
+ import { TwinEvent } from '../twin-event/twin-event.js'
4
+ import { absoluteTime } from '../../engine/read-time.js'
5
+ import { MAX_SPLIT, type SplitShape } from './event-type-split-shape.js'
6
+
7
+ /*
8
+ * **여러 종류를 시각순으로 — 종류를 하나씩 나눠 묻는다** (2026-08-28 실측).
9
+ *
10
+ * ── 무엇이 느렸나 ───────────────────────────────────────────────────────────
11
+ * 에너지 원장은 「이 여덟 종류를 시각 내림차순으로 50건」을 묻는다. 그 모양에서 sqlite 는 종류 색인을
12
+ * **버린다**:
13
+ *
14
+ * 종류 IN + 시각순 SEARCH USING INDEX ix_twin_event_1 (domain, instance) 37.181초 · 결과 0건
15
+ * 같은 조건 · 정렬 없음 SEARCH USING INDEX ix_twin_event_7 (…, event_type=?) 0.003초
16
+ * 종류 하나 + 시각순 SEARCH USING INDEX ix_twin_event_7 (…, event_type=?) 0.003초
17
+ *
18
+ * 정렬을 붙이는 순간 여러 종류의 순서를 합칠 방법이 없어, 시각 색인으로 337만 행을 걸으며 행마다
19
+ * 종류를 확인한다. 승화푸드에 에너지 사건은 0건이다 — **없는 것을 찾느라 37초**를 쓴다.
20
+ *
21
+ * 360 이력이 64초였던 것과 같은 부류다: **거르는 축과 정렬 축이 어긋난다.**
22
+ *
23
+ * ── 고침 ────────────────────────────────────────────────────────────────────
24
+ * 종류를 **정확일치로 하나씩** 물으면 색인이 시각 순서까지 그대로 준다(정렬이 사라진다). 여덟 종류면
25
+ * 질의 여덟 개이고 각 0.003초다. 받은 것을 시각순으로 합쳐 한 페이지를 만든다.
26
+ *
27
+ * 새 색인도, 새 테이블도 필요 없다 — 있는 색인을 쓰게 하는 것이 전부다.
28
+ *
29
+ * ── 좁게 잡는다 ─────────────────────────────────────────────────────────────
30
+ * 이 모양이 아닌 물음은 답하지 않고(`null`) 저널의 일반 경로로 보낸다. 억지로 답하면 목록이 알리지
31
+ * 않고 달라진다.
32
+ *
33
+ * ── ★ **이 갈래를 걷을 조건** ────────────────────────────────────────────────
34
+ * 앱에서 종류를 여러 값으로 보내는 곳은 **에너지 원장 한 곳뿐**이다(2026-08-28 확인). 그 화면이
35
+ * 종류를 하나씩 고르는 모양으로 바뀌면 이 갈래를 부르는 데가 없어진다 — 그때 **지운다.**
36
+ *
37
+ * 부르는 데가 없는 갈래를 들고 있으면, 그것이 무언가를 받쳐 주는 것처럼 보이면서 실제로는 아무것도
38
+ * 하지 않는다. 지금 살려 두는 이유는 하나다: 실 고객 트윈에서 37초 질의가 돌고 있고 화면 수정은
39
+ * 다른 레인의 일이다.
40
+ */
41
+
42
+ export interface SplitPage {
43
+ items: TwinEvent[]
44
+ nextCursor?: string
45
+ }
46
+
47
+ /**
48
+ * 나눠 묻고 합쳐 한 페이지 — 답할 수 없으면 `null`.
49
+ *
50
+ * 이어 읽기 지점은 **모든 갈래에 같이** 건다. 그래야 합친 결과가 커서 아래에서만 나온다 — 갈래마다
51
+ * 다르게 걸면 어떤 종류는 이미 지나간 구간을 다시 낸다.
52
+ */
53
+ export async function eventTypeSplitPage(
54
+ domainId: string,
55
+ instanceIds: string[],
56
+ shape: SplitShape,
57
+ limit: number,
58
+ /** 동시각을 가리는 축 — 트윈 하나면 `revision`, 여럿이면 기본키(§`cursorTiebreak`). */
59
+ tiebreak: 'revision' | 'id',
60
+ after?: string
61
+ ): Promise<SplitPage | null> {
62
+ const pairs = instanceIds.length * shape.eventTypes.length
63
+ if (!pairs || pairs > MAX_SPLIT) return null
64
+
65
+ let cursor: { at: Date; tie: string | number } | null = null
66
+ if (after) {
67
+ cursor = decodeCursorPair(after)
68
+ if (!cursor) return null
69
+ }
70
+
71
+ const rows: TwinEvent[][] = []
72
+ for (const instanceId of instanceIds) {
73
+ for (const eventType of shape.eventTypes) {
74
+ const qb = getRepository(TwinEvent)
75
+ .createQueryBuilder('e')
76
+ .where('e.domain = :domainId', { domainId })
77
+ .andWhere('e.instanceId = :instanceId', { instanceId })
78
+ /* **정확일치**가 요점이다 — 그래야 색인이 시각 순서까지 준다(§ 위 실측). */
79
+ .andWhere('e.eventType = :eventType', { eventType })
80
+ /* 단계는 색인이 아니라 행 조건이다 — 종류 정확일치가 이미 걷는 범위를 좁혀 놓았다. */
81
+ if (shape.bizStep) qb.andWhere('e.bizStep = :bizStep', { bizStep: shape.bizStep })
82
+ if (shape.fromTime) qb.andWhere('e.eventTime >= :fromTime', { fromTime: shape.fromTime })
83
+ if (shape.untilTime) qb.andWhere('e.eventTime <= :untilTime', { untilTime: shape.untilTime })
84
+ if (cursor) {
85
+ qb.andWhere(`(e.eventTime < :curAt OR (e.eventTime = :curAt AND e.${tiebreak} < :curTie))`, {
86
+ curAt: cursor.at,
87
+ curTie: cursor.tie
88
+ })
89
+ }
90
+ const page = await qb
91
+ .orderBy('e.eventTime', 'DESC')
92
+ .addOrderBy(`e.${tiebreak}`, 'DESC')
93
+ .limit(limit)
94
+ .getMany()
95
+ .catch(() => null)
96
+ /* 한 갈래라도 못 읽으면 합친 결과가 반쪽이다 — 반쪽을 전부인 것처럼 내지 않는다. */
97
+ if (!page) return null
98
+ rows.push(page)
99
+ }
100
+ }
101
+
102
+ /*
103
+ * 합친다. 갈래마다 `limit` 까지 받았으므로, 합쳐서 앞의 `limit` 개는 **전체에서도** 앞의 `limit` 개다
104
+ * (각 갈래가 자기 안에서 정렬돼 있고 그보다 앞선 것을 빠뜨릴 수 없다).
105
+ */
106
+ const merged = rows
107
+ .flat()
108
+ .sort((a: any, b: any) => {
109
+ /* `getMany` 는 엔티티를 주므로 시각이 이미 날짜다 — 감싸면 드라이버 값을 손으로 읽는 모양이 된다. */
110
+ const at = (b.eventTime as Date).getTime() - (a.eventTime as Date).getTime()
111
+ if (at !== 0) return at
112
+ const x = a[tiebreak]
113
+ const y = b[tiebreak]
114
+ return x === y ? 0 : x < y ? 1 : -1
115
+ })
116
+ .slice(0, limit)
117
+
118
+ const last: any = merged[merged.length - 1]
119
+ const nextCursor =
120
+ merged.length === limit && last?.eventTime instanceof Date && last[tiebreak] !== undefined
121
+ ? encodeCursorPair(last.eventTime, last[tiebreak])
122
+ : undefined
123
+
124
+ return { items: merged, nextCursor }
125
+ }
126
+
127
+ /** 저널 경로와 같은 커서 — `[eventTime, 동시각 축]`(§`buildNextCursor`). */
128
+ function encodeCursorPair(at: Date, tie: string | number): string {
129
+ return Buffer.from(JSON.stringify([at, tie]), 'utf8').toString('base64url')
130
+ }
131
+
132
+ function decodeCursorPair(cursor: string): { at: Date; tie: string | number } | null {
133
+ let values: unknown
134
+ try {
135
+ values = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'))
136
+ } catch {
137
+ return null
138
+ }
139
+ if (!Array.isArray(values) || values.length !== 2) return null
140
+ const at = absoluteTime(values[0])
141
+ const tie = values[1]
142
+ if (!at || (typeof tie !== 'string' && typeof tie !== 'number')) return null
143
+ return { at, tie }
144
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 지난 기록 조회의 정렬 축 — **부르는 쪽이 무엇으로 물었는지에 맞춘다.**
3
+ *
4
+ * 리졸버 안에 두면 이 규칙을 확인할 방법이 소스를 글자로 전수 조회하는 것뿐이다. 규칙 자체를 부를 수 있게
5
+ * 밖으로 뺀다(`test/journal-order-axis.test.ts`).
6
+ */
7
+ export function journalOrder(q: {
8
+ untilTime?: string | null
9
+ fromTime?: string | null
10
+ fromRevision?: number | null
11
+ toRevision?: number | null
12
+ }): {
13
+ eventTime?: 'DESC'
14
+ revision: 'DESC'
15
+ } {
16
+ /* `revision` 으로 물었으면 그 순서가 물음의 답이다. 거르는 축과 정렬 축을 맞추는 규칙과도 같다. */
17
+ const byTime = q.untilTime != null || q.fromTime != null
18
+ if (!byTime && (q.fromRevision != null || q.toRevision != null)) return { revision: 'DESC' }
19
+ return { eventTime: 'DESC', revision: 'DESC' }
20
+ }