@things-factory/headless-twin 10.0.14 → 10.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. package/dist-server/engine/attention-digest.d.ts +4 -4
  2. package/dist-server/engine/attention-digest.js +7 -7
  3. package/dist-server/engine/attention-digest.js.map +1 -1
  4. package/dist-server/engine/canonical-ingest.d.ts +11 -2
  5. package/dist-server/engine/canonical-ingest.js +137 -8
  6. package/dist-server/engine/canonical-ingest.js.map +1 -1
  7. package/dist-server/engine/command-routing.js +1 -1
  8. package/dist-server/engine/command-routing.js.map +1 -1
  9. package/dist-server/engine/declared-stimulus.js +1 -1
  10. package/dist-server/engine/declared-stimulus.js.map +1 -1
  11. package/dist-server/engine/energy-topology.js +1 -1
  12. package/dist-server/engine/energy-topology.js.map +1 -1
  13. package/dist-server/engine/ingest-dedupe.d.ts +38 -0
  14. package/dist-server/engine/ingest-dedupe.js +145 -0
  15. package/dist-server/engine/ingest-dedupe.js.map +1 -0
  16. package/dist-server/engine/ingest-health.d.ts +124 -9
  17. package/dist-server/engine/ingest-health.js +169 -15
  18. package/dist-server/engine/ingest-health.js.map +1 -1
  19. package/dist-server/engine/integration-probes.d.ts +3 -3
  20. package/dist-server/engine/integration-probes.js +6 -6
  21. package/dist-server/engine/integration-probes.js.map +1 -1
  22. package/dist-server/engine/kpi-baseline.js +4 -4
  23. package/dist-server/engine/kpi-baseline.js.map +1 -1
  24. package/dist-server/engine/kpi-fold.d.ts +122 -12
  25. package/dist-server/engine/kpi-fold.js +247 -18
  26. package/dist-server/engine/kpi-fold.js.map +1 -1
  27. package/dist-server/engine/kpi-query.d.ts +11 -11
  28. package/dist-server/engine/kpi-query.js +185 -28
  29. package/dist-server/engine/kpi-query.js.map +1 -1
  30. package/dist-server/engine/live-feed-registry.d.ts +1 -1
  31. package/dist-server/engine/live-feed-registry.js +1 -1
  32. package/dist-server/engine/live-feed-registry.js.map +1 -1
  33. package/dist-server/engine/load-meter.d.ts +1 -1
  34. package/dist-server/engine/load-meter.js.map +1 -1
  35. package/dist-server/engine/local-declarations.d.ts +7 -7
  36. package/dist-server/engine/local-declarations.js +9 -9
  37. package/dist-server/engine/local-declarations.js.map +1 -1
  38. package/dist-server/engine/log.d.ts +1 -1
  39. package/dist-server/engine/log.js +1 -1
  40. package/dist-server/engine/log.js.map +1 -1
  41. package/dist-server/engine/measured-estimator.js +1 -1
  42. package/dist-server/engine/measured-estimator.js.map +1 -1
  43. package/dist-server/engine/measured-yield.js +1 -1
  44. package/dist-server/engine/measured-yield.js.map +1 -1
  45. package/dist-server/engine/model-basis.d.ts +1 -1
  46. package/dist-server/engine/model-basis.js +1 -1
  47. package/dist-server/engine/model-basis.js.map +1 -1
  48. package/dist-server/engine/model-gap.d.ts +3 -3
  49. package/dist-server/engine/model-gap.js +1 -1
  50. package/dist-server/engine/model-gap.js.map +1 -1
  51. package/dist-server/engine/model-vocabulary.js +1 -1
  52. package/dist-server/engine/model-vocabulary.js.map +1 -1
  53. package/dist-server/engine/oee-accumulator.d.ts +4 -4
  54. package/dist-server/engine/oee-accumulator.js +3 -3
  55. package/dist-server/engine/oee-accumulator.js.map +1 -1
  56. package/dist-server/engine/property-effects.js +2 -2
  57. package/dist-server/engine/property-effects.js.map +1 -1
  58. package/dist-server/engine/read-time.d.ts +22 -0
  59. package/dist-server/engine/read-time.js +81 -0
  60. package/dist-server/engine/read-time.js.map +1 -0
  61. package/dist-server/engine/restart-policy.js +1 -1
  62. package/dist-server/engine/restart-policy.js.map +1 -1
  63. package/dist-server/engine/runtime-key.js +1 -1
  64. package/dist-server/engine/runtime-key.js.map +1 -1
  65. package/dist-server/engine/spec-coverage.d.ts +2 -2
  66. package/dist-server/engine/spec-coverage.js +1 -1
  67. package/dist-server/engine/spec-coverage.js.map +1 -1
  68. package/dist-server/engine/stage-path.d.ts +4 -4
  69. package/dist-server/engine/stage-path.js +1 -1
  70. package/dist-server/engine/stage-path.js.map +1 -1
  71. package/dist-server/engine/state-axes.d.ts +1 -1
  72. package/dist-server/engine/state-axes.js +2 -2
  73. package/dist-server/engine/state-axes.js.map +1 -1
  74. package/dist-server/engine/travel-estimator.d.ts +2 -2
  75. package/dist-server/engine/travel-estimator.js +1 -1
  76. package/dist-server/engine/travel-estimator.js.map +1 -1
  77. package/dist-server/engine/twin-engine.d.ts +243 -53
  78. package/dist-server/engine/twin-engine.js +690 -191
  79. package/dist-server/engine/twin-engine.js.map +1 -1
  80. package/dist-server/engine/warm-start.d.ts +2 -2
  81. package/dist-server/engine/warm-start.js +5 -5
  82. package/dist-server/engine/warm-start.js.map +1 -1
  83. package/dist-server/index.js +1 -1
  84. package/dist-server/index.js.map +1 -1
  85. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +1 -1
  86. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -1
  87. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +3 -3
  88. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -1
  89. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +2 -2
  90. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -1
  91. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.d.ts +5 -0
  92. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js +54 -0
  93. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js.map +1 -0
  94. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.d.ts +5 -0
  95. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js +44 -0
  96. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js.map +1 -0
  97. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.d.ts +6 -0
  98. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js +84 -0
  99. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js.map +1 -0
  100. package/dist-server/migrations/index.js +7 -1
  101. package/dist-server/migrations/index.js.map +1 -1
  102. package/dist-server/routes.d.ts +1 -0
  103. package/dist-server/routes.js +69 -5
  104. package/dist-server/routes.js.map +1 -1
  105. package/dist-server/service/index.d.ts +2 -2
  106. package/dist-server/service/index.js +27 -24
  107. package/dist-server/service/index.js.map +1 -1
  108. package/dist-server/service/reference/control-routing.d.ts +1 -1
  109. package/dist-server/service/reference/control-routing.js +1 -1
  110. package/dist-server/service/reference/control-routing.js.map +1 -1
  111. package/dist-server/service/reference/discovery-result.d.ts +2 -2
  112. package/dist-server/service/reference/discovery-result.js +3 -3
  113. package/dist-server/service/reference/discovery-result.js.map +1 -1
  114. package/dist-server/service/reference/hook-contract.d.ts +43 -0
  115. package/dist-server/service/reference/hook-contract.js +59 -0
  116. package/dist-server/service/reference/hook-contract.js.map +1 -0
  117. package/dist-server/service/reference/ingest-space.d.ts +1 -1
  118. package/dist-server/service/reference/ingest-space.js +4 -4
  119. package/dist-server/service/reference/ingest-space.js.map +1 -1
  120. package/dist-server/service/reference/knob-defaults.d.ts +1 -1
  121. package/dist-server/service/reference/knob-defaults.js +4 -4
  122. package/dist-server/service/reference/knob-defaults.js.map +1 -1
  123. package/dist-server/service/reference/reference-adapter.d.ts +140 -8
  124. package/dist-server/service/reference/reference-adapter.js +27 -0
  125. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  126. package/dist-server/service/reference/reference-hook.d.ts +20 -0
  127. package/dist-server/service/reference/reference-hook.js +133 -0
  128. package/dist-server/service/reference/reference-hook.js.map +1 -0
  129. package/dist-server/service/reference/reference-live.d.ts +2 -2
  130. package/dist-server/service/reference/reference-live.js +88 -13
  131. package/dist-server/service/reference/reference-live.js.map +1 -1
  132. package/dist-server/service/reference/reference-master.d.ts +14 -14
  133. package/dist-server/service/reference/reference-master.js +34 -12
  134. package/dist-server/service/reference/reference-master.js.map +1 -1
  135. package/dist-server/service/reference/reference-resolver.d.ts +1 -1
  136. package/dist-server/service/reference/reference-resolver.js +10 -10
  137. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  138. package/dist-server/service/reference/twin-reference.d.ts +3 -3
  139. package/dist-server/service/reference/twin-reference.js.map +1 -1
  140. package/dist-server/service/twin-attention/twin-attention-query.d.ts +1 -1
  141. package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
  142. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  143. package/dist-server/service/twin-audit/command-audit.d.ts +1 -1
  144. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  145. package/dist-server/service/twin-audit/twin-audit-event.d.ts +1 -1
  146. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  147. package/dist-server/service/twin-audit/twin-audit-query.js +1 -1
  148. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -1
  149. package/dist-server/service/twin-control/twin-control-mutation.js +1 -1
  150. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  151. package/dist-server/service/twin-event/twin-event-keys.d.ts +22 -2
  152. package/dist-server/service/twin-event/twin-event-keys.js +37 -5
  153. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  154. package/dist-server/service/twin-event/twin-event-type.d.ts +1 -0
  155. package/dist-server/service/twin-event/twin-event-type.js +8 -1
  156. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  157. package/dist-server/service/twin-event/twin-event.d.ts +2 -2
  158. package/dist-server/service/twin-event/twin-event.js +32 -4
  159. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  160. package/dist-server/service/twin-forecast/forecast-metrics.js +1 -1
  161. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -1
  162. package/dist-server/service/twin-forecast/gap-analytics.d.ts +1 -1
  163. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  164. package/dist-server/service/twin-forecast/twin-forecast-query.js +29 -11
  165. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  166. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +1 -1
  167. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +1 -1
  168. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -1
  169. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +1 -1
  170. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -1
  171. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  172. package/dist-server/service/twin-journal/event-type-split-shape.d.ts +50 -0
  173. package/dist-server/service/twin-journal/event-type-split-shape.js +111 -0
  174. package/dist-server/service/twin-journal/event-type-split-shape.js.map +1 -0
  175. package/dist-server/service/twin-journal/event-type-split.d.ts +15 -0
  176. package/dist-server/service/twin-journal/event-type-split.js +102 -0
  177. package/dist-server/service/twin-journal/event-type-split.js.map +1 -0
  178. package/dist-server/service/twin-journal/journal-order.d.ts +15 -0
  179. package/dist-server/service/twin-journal/journal-order.js +17 -0
  180. package/dist-server/service/twin-journal/journal-order.js.map +1 -0
  181. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -5
  182. package/dist-server/service/twin-journal/twin-journal-query.js +232 -21
  183. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  184. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +27 -1
  185. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +59 -3
  186. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  187. package/dist-server/service/twin-metrics/twin-metrics-query.js +14 -1
  188. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  189. package/dist-server/service/twin-model/axis-journal-evidence.js +1 -1
  190. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -1
  191. package/dist-server/service/twin-model/epcis-coverage.js +36 -1
  192. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  193. package/dist-server/service/twin-model/iec61850-coverage.js +1 -1
  194. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  195. package/dist-server/service/twin-model/isa95-coverage.d.ts +15 -2
  196. package/dist-server/service/twin-model/isa95-coverage.js +32 -8
  197. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  198. package/dist-server/service/twin-model/item-ref.js +2 -2
  199. package/dist-server/service/twin-model/item-ref.js.map +1 -1
  200. package/dist-server/service/twin-model/name-index.d.ts +1 -1
  201. package/dist-server/service/twin-model/name-index.js +1 -1
  202. package/dist-server/service/twin-model/name-index.js.map +1 -1
  203. package/dist-server/service/twin-model/project-structure.d.ts +1 -1
  204. package/dist-server/service/twin-model/project-structure.js +3 -3
  205. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  206. package/dist-server/service/twin-model/twin-equipment.d.ts +1 -1
  207. package/dist-server/service/twin-model/twin-equipment.js +1 -1
  208. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  209. package/dist-server/service/twin-model/twin-lineage-query.js +16 -5
  210. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  211. package/dist-server/service/twin-model/twin-location.d.ts +1 -1
  212. package/dist-server/service/twin-model/twin-location.js.map +1 -1
  213. package/dist-server/service/twin-model/twin-model-item-query.d.ts +3 -3
  214. package/dist-server/service/twin-model/twin-model-item-query.js +14 -14
  215. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  216. package/dist-server/service/twin-model/twin-model-mutation.d.ts +2 -2
  217. package/dist-server/service/twin-model/twin-model-mutation.js +4 -4
  218. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -1
  219. package/dist-server/service/twin-model/twin-model-query.js +117 -28
  220. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  221. package/dist-server/service/twin-model/twin-model-tree-query.js +22 -0
  222. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  223. package/dist-server/service/twin-model/twin-operation.d.ts +1 -1
  224. package/dist-server/service/twin-model/twin-operation.js +1 -1
  225. package/dist-server/service/twin-model/twin-operation.js.map +1 -1
  226. package/dist-server/service/twin-space/move-space.js +2 -2
  227. package/dist-server/service/twin-space/move-space.js.map +1 -1
  228. package/dist-server/service/twin-space/representation-role.d.ts +2 -0
  229. package/dist-server/service/twin-space/representation-role.js +18 -0
  230. package/dist-server/service/twin-space/representation-role.js.map +1 -0
  231. package/dist-server/service/twin-space/space-integrity.d.ts +1 -1
  232. package/dist-server/service/twin-space/space-integrity.js.map +1 -1
  233. package/dist-server/service/twin-space/twin-space-representation.d.ts +2 -0
  234. package/dist-server/service/twin-space/twin-space-representation.js +16 -0
  235. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  236. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  237. package/dist-server/service/twin-space/twin-space-resolver.js +36 -7
  238. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  239. package/dist-server/service/twin-structure/twin-structure.js +3 -3
  240. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  241. package/dist-server/service/twin-subject/event-subjects.d.ts +43 -0
  242. package/dist-server/service/twin-subject/event-subjects.js +137 -0
  243. package/dist-server/service/twin-subject/event-subjects.js.map +1 -0
  244. package/dist-server/service/twin-subject/index.d.ts +9 -0
  245. package/dist-server/service/twin-subject/index.js +16 -0
  246. package/dist-server/service/twin-subject/index.js.map +1 -0
  247. package/dist-server/service/twin-subject/item-resolver.d.ts +10 -0
  248. package/dist-server/service/twin-subject/item-resolver.js +93 -0
  249. package/dist-server/service/twin-subject/item-resolver.js.map +1 -0
  250. package/dist-server/service/twin-subject/subject-query-shape.d.ts +22 -0
  251. package/dist-server/service/twin-subject/subject-query-shape.js +88 -0
  252. package/dist-server/service/twin-subject/subject-query-shape.js.map +1 -0
  253. package/dist-server/service/twin-subject/twin-subject-backfill.d.ts +23 -0
  254. package/dist-server/service/twin-subject/twin-subject-backfill.js +159 -0
  255. package/dist-server/service/twin-subject/twin-subject-backfill.js.map +1 -0
  256. package/dist-server/service/twin-subject/twin-subject-event.d.ts +14 -0
  257. package/dist-server/service/twin-subject/twin-subject-event.js +111 -0
  258. package/dist-server/service/twin-subject/twin-subject-event.js.map +1 -0
  259. package/dist-server/service/twin-subject/twin-subject-fast-path.d.ts +39 -0
  260. package/dist-server/service/twin-subject/twin-subject-fast-path.js +295 -0
  261. package/dist-server/service/twin-subject/twin-subject-fast-path.js.map +1 -0
  262. package/dist-server/service/twin-subject/twin-subject-query.d.ts +27 -0
  263. package/dist-server/service/twin-subject/twin-subject-query.js +232 -0
  264. package/dist-server/service/twin-subject/twin-subject-query.js.map +1 -0
  265. package/dist-server/service/twin-subject/twin-subject-writer.d.ts +27 -0
  266. package/dist-server/service/twin-subject/twin-subject-writer.js +166 -0
  267. package/dist-server/service/twin-subject/twin-subject-writer.js.map +1 -0
  268. package/dist-server/service/twin-target/twin-target-resolver.js +1 -1
  269. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  270. package/dist-shared/entity-delta.d.ts +1 -1
  271. package/dist-shared/entity-delta.js +3 -3
  272. package/dist-shared/entity-delta.js.map +1 -1
  273. package/dist-shared/touched-items.js +2 -2
  274. package/dist-shared/touched-items.js.map +1 -1
  275. package/dist-shared/twin-level.js +1 -1
  276. package/dist-shared/twin-level.js.map +1 -1
  277. package/package.json +3 -3
  278. package/server/engine/attention-digest.ts +9 -9
  279. package/server/engine/canonical-ingest.ts +170 -13
  280. package/server/engine/command-routing.ts +1 -1
  281. package/server/engine/declared-stimulus.ts +1 -1
  282. package/server/engine/energy-topology.ts +1 -1
  283. package/server/engine/ingest-dedupe.ts +149 -0
  284. package/server/engine/ingest-health.ts +234 -20
  285. package/server/engine/integration-probes.ts +7 -7
  286. package/server/engine/kpi-baseline.ts +4 -4
  287. package/server/engine/kpi-fold.ts +395 -27
  288. package/server/engine/kpi-query.ts +212 -46
  289. package/server/engine/live-feed-registry.ts +1 -1
  290. package/server/engine/load-meter.ts +1 -1
  291. package/server/engine/local-declarations.ts +13 -13
  292. package/server/engine/log.ts +1 -1
  293. package/server/engine/measured-estimator.ts +1 -1
  294. package/server/engine/measured-yield.ts +1 -1
  295. package/server/engine/model-basis.ts +2 -2
  296. package/server/engine/model-gap.ts +3 -3
  297. package/server/engine/model-vocabulary.ts +1 -1
  298. package/server/engine/oee-accumulator.ts +4 -4
  299. package/server/engine/property-effects.ts +2 -2
  300. package/server/engine/read-time.ts +77 -0
  301. package/server/engine/restart-policy.ts +1 -1
  302. package/server/engine/runtime-key.ts +1 -1
  303. package/server/engine/spec-coverage.ts +3 -3
  304. package/server/engine/stage-path.ts +5 -5
  305. package/server/engine/state-axes.ts +2 -2
  306. package/server/engine/travel-estimator.ts +2 -2
  307. package/server/engine/twin-engine.ts +764 -201
  308. package/server/engine/warm-start.ts +6 -6
  309. package/server/index.ts +1 -1
  310. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +1 -1
  311. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +3 -3
  312. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +2 -2
  313. package/server/migrations/1786300000000-IndexEventTypeByTime.ts +48 -0
  314. package/server/migrations/1786400000000-IndexEventCreatedAt.ts +38 -0
  315. package/server/migrations/1786500000000-CreateTwinSubjectEvents.ts +93 -0
  316. package/server/migrations/index.ts +7 -1
  317. package/server/routes.ts +68 -5
  318. package/server/service/index.ts +4 -1
  319. package/server/service/reference/control-routing.ts +1 -1
  320. package/server/service/reference/discovery-result.ts +3 -3
  321. package/server/service/reference/hook-contract.ts +103 -0
  322. package/server/service/reference/ingest-space.ts +4 -4
  323. package/server/service/reference/knob-defaults.ts +4 -4
  324. package/server/service/reference/reference-adapter.ts +166 -9
  325. package/server/service/reference/reference-hook.ts +145 -0
  326. package/server/service/reference/reference-live.ts +92 -14
  327. package/server/service/reference/reference-master.ts +39 -17
  328. package/server/service/reference/reference-resolver.ts +10 -10
  329. package/server/service/reference/twin-reference.ts +3 -3
  330. package/server/service/twin-attention/twin-attention-query.ts +1 -1
  331. package/server/service/twin-audit/command-audit.ts +1 -1
  332. package/server/service/twin-audit/twin-audit-event.ts +2 -2
  333. package/server/service/twin-audit/twin-audit-query.ts +1 -1
  334. package/server/service/twin-control/twin-control-mutation.ts +1 -1
  335. package/server/service/twin-event/twin-event-keys.ts +37 -5
  336. package/server/service/twin-event/twin-event-type.ts +18 -1
  337. package/server/service/twin-event/twin-event.ts +32 -6
  338. package/server/service/twin-forecast/forecast-metrics.ts +1 -1
  339. package/server/service/twin-forecast/gap-analytics.ts +1 -1
  340. package/server/service/twin-forecast/twin-forecast-query.ts +30 -11
  341. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +1 -1
  342. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +1 -1
  343. package/server/service/twin-instance/twin-instance.ts +4 -4
  344. package/server/service/twin-journal/event-type-split-shape.ts +112 -0
  345. package/server/service/twin-journal/event-type-split.ts +144 -0
  346. package/server/service/twin-journal/journal-order.ts +20 -0
  347. package/server/service/twin-journal/twin-journal-query.ts +224 -20
  348. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +57 -3
  349. package/server/service/twin-metrics/twin-metrics-query.ts +14 -1
  350. package/server/service/twin-model/axis-journal-evidence.ts +1 -1
  351. package/server/service/twin-model/epcis-coverage.ts +36 -1
  352. package/server/service/twin-model/iec61850-coverage.ts +1 -1
  353. package/server/service/twin-model/isa95-coverage.ts +45 -8
  354. package/server/service/twin-model/item-ref.ts +2 -2
  355. package/server/service/twin-model/name-index.ts +1 -1
  356. package/server/service/twin-model/project-structure.ts +4 -4
  357. package/server/service/twin-model/twin-equipment.ts +2 -2
  358. package/server/service/twin-model/twin-lineage-query.ts +16 -5
  359. package/server/service/twin-model/twin-location.ts +1 -1
  360. package/server/service/twin-model/twin-model-item-query.ts +14 -14
  361. package/server/service/twin-model/twin-model-mutation.ts +4 -4
  362. package/server/service/twin-model/twin-model-query.ts +117 -28
  363. package/server/service/twin-model/twin-model-tree-query.ts +25 -1
  364. package/server/service/twin-model/twin-operation.ts +2 -2
  365. package/server/service/twin-space/move-space.ts +3 -3
  366. package/server/service/twin-space/representation-role.ts +15 -0
  367. package/server/service/twin-space/space-integrity.ts +1 -1
  368. package/server/service/twin-space/twin-space-representation.ts +37 -0
  369. package/server/service/twin-space/twin-space-resolver.ts +39 -7
  370. package/server/service/twin-structure/twin-structure.ts +3 -3
  371. package/server/service/twin-subject/event-subjects.ts +155 -0
  372. package/server/service/twin-subject/index.ts +10 -0
  373. package/server/service/twin-subject/item-resolver.ts +92 -0
  374. package/server/service/twin-subject/subject-query-shape.ts +84 -0
  375. package/server/service/twin-subject/twin-subject-backfill.ts +196 -0
  376. package/server/service/twin-subject/twin-subject-event.ts +101 -0
  377. package/server/service/twin-subject/twin-subject-fast-path.ts +346 -0
  378. package/server/service/twin-subject/twin-subject-query.ts +210 -0
  379. package/server/service/twin-subject/twin-subject-writer.ts +189 -0
  380. package/server/service/twin-target/twin-target-resolver.ts +1 -1
  381. package/shared/entity-delta.ts +3 -3
  382. package/shared/touched-items.ts +2 -2
  383. package/shared/twin-level.ts +1 -1
  384. package/test/adopt-structure-live.test.ts +3 -3
  385. package/test/aggregate-not-findone.test.ts +112 -0
  386. package/test/attention-digest.test.ts +6 -6
  387. package/test/axis-read.test.ts +2 -2
  388. package/test/boot-resume.test.ts +20 -7
  389. package/test/broadcast-period.test.ts +2 -2
  390. package/test/broadcast-queue-overflow.test.ts +244 -0
  391. package/test/canonical-ingest-vocabularies.test.ts +179 -7
  392. package/test/canonical-quantity-door.test.ts +2 -2
  393. package/test/checkpoint-refuses-empty.test.ts +2 -2
  394. package/test/connector-capability-declaration.test.ts +88 -0
  395. package/test/control-capability.test.ts +1 -1
  396. package/test/cursor-stall-not-read-failure.test.ts +2 -2
  397. package/test/declaration-reaches-model.test.ts +5 -5
  398. package/test/declared-stimulus.test.ts +4 -4
  399. package/test/discovery-result.test.ts +2 -2
  400. package/test/entity-delta.test.ts +4 -4
  401. package/test/event-subjects.test.ts +147 -0
  402. package/test/event-time-column.test.ts +3 -3
  403. package/test/event-type-split.test.ts +155 -0
  404. package/test/fold-gap-speaks.test.ts +53 -0
  405. package/test/forecast-metrics.test.ts +1 -1
  406. package/test/forecast-tuning.test.ts +1 -1
  407. package/test/ingest-health-engine.test.ts +2 -2
  408. package/test/ingest-health-wiring.test.ts +7 -5
  409. package/test/ingest-health.test.ts +6 -6
  410. package/test/ingest-history.test.ts +2 -2
  411. package/test/ingest-idempotent.test.ts +132 -0
  412. package/test/ingest-space.test.ts +3 -3
  413. package/test/integration-probes.test.ts +3 -3
  414. package/test/integration-runner.test.ts +1 -1
  415. package/test/item-ref.test.ts +1 -1
  416. package/test/journal-order-axis.test.ts +80 -0
  417. package/test/journal-read-discipline.test.ts +16 -6
  418. package/test/journal-retention.test.ts +171 -29
  419. package/test/journal-sort-axis.test.ts +1 -1
  420. package/test/journal-write-door.test.ts +3 -3
  421. package/test/journal-write-trend.test.ts +1 -1
  422. package/test/kernel-kind-guard.test.ts +3 -3
  423. package/test/knob-defaults.test.ts +1 -1
  424. package/test/kpi-baseline-db.test.ts +2 -2
  425. package/test/kpi-fold.test.ts +185 -14
  426. package/test/kpi-query-bench.test.ts +3 -3
  427. package/test/live-cursor-wiring.test.ts +6 -6
  428. package/test/live-kernel-facts.test.ts +11 -11
  429. package/test/live-mirror-parity.test.ts +3 -3
  430. package/test/load-meter.test.ts +1 -1
  431. package/test/local-declarations.test.ts +10 -10
  432. package/test/log-stamp.test.ts +1 -1
  433. package/test/measured-yield.test.ts +2 -2
  434. package/test/merge-streams.test.ts +70 -0
  435. package/test/mirror-resumes-from-checkpoint.test.ts +13 -13
  436. package/test/model-basis.test.ts +2 -2
  437. package/test/model-gap.test.ts +4 -4
  438. package/test/model-vocabulary.test.ts +2 -2
  439. package/test/move-space.test.ts +1 -1
  440. package/test/oee-accumulator.test.ts +2 -2
  441. package/test/operational-vocabulary.test.ts +2 -2
  442. package/test/project-structure-db.test.ts +2 -2
  443. package/test/projection-reaches-screen.test.ts +2 -2
  444. package/test/property-effects.test.ts +3 -3
  445. package/test/read-failure-ledger.test.ts +106 -0
  446. package/test/read-failure-visible.test.ts +7 -7
  447. package/test/read-time.test.ts +118 -0
  448. package/test/reference-grounding.test.ts +1 -1
  449. package/test/reference-hook.test.ts +86 -0
  450. package/test/registry-key-guard.test.ts +1 -1
  451. package/test/rename-twin.test.ts +81 -0
  452. package/test/restart-policy.test.ts +3 -3
  453. package/test/resync-origin-site.test.ts +3 -3
  454. package/test/revision-axis.test.ts +14 -8
  455. package/test/runtime-key.test.ts +3 -3
  456. package/test/snapshot-freshness.test.ts +4 -4
  457. package/test/source-outcome-audit.test.ts +1 -1
  458. package/test/space-integrity.test.ts +1 -1
  459. package/test/space-representation.test.ts +66 -0
  460. package/test/space-scope-includes-stopped.test.ts +55 -0
  461. package/test/spec-coverage.test.ts +1 -1
  462. package/test/standard-coverage.test.ts +43 -1
  463. package/test/status-tally.test.ts +1 -1
  464. package/test/structure-revision-db.test.ts +7 -7
  465. package/test/subject-fast-path.test.ts +148 -0
  466. package/test/subject-read-model.test.ts +175 -0
  467. package/test/touched-items.test.ts +2 -2
  468. package/test/twin-audit.test.ts +1 -1
  469. package/test/twin-event-keys.test.ts +49 -3
  470. package/test/twin-model-item-db.test.ts +5 -5
  471. package/test/twin-model-tree-db.test.ts +35 -2
  472. package/test/twin-origin-resync.test.ts +1 -1
  473. package/test/warm-start-seam.test.ts +1 -1
  474. package/test/warm-start.test.ts +6 -6
  475. package/test/withheld-door.test.ts +95 -0
  476. package/test/yield-loop.test.ts +4 -4
  477. package/tsconfig.shared.tsbuildinfo +1 -1
  478. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,196 @@
1
+ import { getRepository } from '@things-factory/shell'
2
+
3
+ import { twinLog, twinWarn } from '../../engine/log.js'
4
+ import { TwinEvent } from '../twin-event/twin-event.js'
5
+ import { TwinInstance } from '../twin-instance/twin-instance.js'
6
+ import { TwinSubjectEvent } from './twin-subject-event.js'
7
+ import { writeSubjectRows } from './twin-subject-writer.js'
8
+ import { itemResolverFor } from './item-resolver.js'
9
+
10
+ /*
11
+ * **이미 저장된 사건을 이 표로 옮긴다** — 요청받은 트윈만.
12
+ *
13
+ * ── 왜 기본이 아닌가 ────────────────────────────────────────────────────────
14
+ * 이 표의 규칙은 「앞으로만 채운다」다. 그래야 새 조회 축이 필요할 때마다 3,128만 줄을 다시 만드는
15
+ * 구조가 되지 않는다(설계: read-models.md §4).
16
+ *
17
+ * 그런데 이미 쌓인 트윈에서 이력을 보려면 그 구간이 있어야 한다. 그래서 **부르면 채우는 도구**로 둔다 —
18
+ * 마이그레이션이 아니고, 부팅에 자동으로 돌지도 않는다. 트윈을 지정해 부른다.
19
+ *
20
+ * ── 어디까지 옮겼는지 적어 둔다 (2026-08-27 · 세 번 틀린 뒤) ─────────────────
21
+ * 저장하지 않고 표의 내용으로 진행 지점을 알아내려 했고 세 번 틀렸다.
22
+ *
23
+ * ① 표의 최대 리비전 뒤부터 앞으로 채워진 줄의 최댓값이 곧 저널의 끝이라 소급을 다 건너뛴다
24
+ * ② 표의 최소 리비전 앞까지 소급이 1부터 적으면 최솟값이 1이 되어 구간이 0으로 접힌다
25
+ * ③ 표의 최소 시각 아래로 사건이 시각순으로 오지 않는 트윈에서 곧바로 끝난다
26
+ *
27
+ * ③이 결정적이었다. 승화푸드 커넥터는 과거 기록을 되읽으므로 방금 들어온 사건의 시각이 2024년 10월이다.
28
+ * 그래서 표의 가장 오래된 시각이 이미 저널의 시작과 같아진다.
29
+ *
30
+ * 원인은 하나다. **채워진 것은 구간이 아니라 집합이다.** 리비전으로 보든 시각으로 보든 「어디까지」를
31
+ * 하나의 경계로 말할 수 없다. 그래서 알아내려 하지 않고 **이 프로세스가 든다**(§`inFlight`).
32
+ *
33
+ * 리비전 1부터 순서대로 올라간다. 목표는 **채우기를 시작한 순간의 저널 끝**이다 — 그보다 뒤는 앞으로
34
+ * 채우는 쪽이 이미 적고 있다.
35
+ *
36
+ * ── 규율 ────────────────────────────────────────────────────────────────────
37
+ * **나눠서 옮긴다.** 한 번에 통째로 읽으면 그동안 호스트가 아무 일도 못 한다(sqlite 는 쓰기가 하나다).
38
+ * 배치 사이에 루프를 비워 준다.
39
+ *
40
+ * **같은 시각의 줄을 반쪽만 적지 않는다.** 한 배치의 가장 오래된 시각에 걸친 줄들은 다음 배치로 넘긴다 —
41
+ * 반쪽만 적으면 그 시각이 「채워진 것」으로 보이면서 일부가 빠진다.
42
+ *
43
+ * **몇 줄을 옮겼고 어디까지 왔는지 말한다.** 알리지 않고 절반만 채우면 화면이 「그 앞에는 사건이 없다」고
44
+ * 말하게 된다.
45
+ */
46
+
47
+ /** 한 번에 읽는 저널 행 수 — 저널 정리와 같은 규율. */
48
+ const READ_BATCH = 5_000
49
+
50
+ export interface BackfillResult {
51
+ instanceId: string
52
+ /** 읽은 저널 행 수. */
53
+ read: number
54
+ /** 이 표에 적은 줄 수. */
55
+ written: number
56
+ /** 여기까지 옮겼다(저널 리비전). */
57
+ upToRevision: number
58
+ /** 목표 — 채우기를 시작한 순간의 저널 끝. */
59
+ targetRevision: number
60
+ /** 다 옮겼나. */
61
+ complete: boolean
62
+ /** 예산을 다 써서 멈췄나. */
63
+ paused: boolean
64
+ }
65
+
66
+ /**
67
+ * 옮기는 중인 트윈의 목표와 지점 — **이 프로세스 안에서만** 든다.
68
+ *
69
+ * 테이블에 적지 않는다(사용자 판단, 2026-08-27). 서버가 내려가면 사라지고, 다음에 부르면 처음부터
70
+ * 다시 읽는다. 그 대가를 아는 채로 두는 것이다.
71
+ */
72
+ const inFlight = new Map<string, { upToRevision: number; targetRevision: number }>()
73
+
74
+ /**
75
+ * 한 트윈의 저장된 사건을 이 표로 옮긴다 — 예산 안에서.
76
+ *
77
+ * 되풀어 부르면 이어서 한다. 넘길 인자가 없다 — 진행 지점을 적어 두기 때문이다.
78
+ */
79
+ export async function backfillSubjects(
80
+ domainId: string,
81
+ instanceId: string,
82
+ opts: { budgetMs?: number } = {}
83
+ ): Promise<BackfillResult> {
84
+ const budgetMs = opts.budgetMs ?? 30_000
85
+ const startedMs = Date.now()
86
+
87
+ const reg = await getRepository(TwinInstance)
88
+ .findOne({ where: { domain: { id: domainId }, instanceId } })
89
+ .catch(() => null)
90
+ if (!reg?.model) {
91
+ throw new Error(`twin "${instanceId}" is not registered — no model to resolve declared items with`)
92
+ }
93
+ const itemOf = itemResolverFor(domainId, instanceId, reg.model)
94
+ if (!itemOf) {
95
+ twinWarn(
96
+ `[twin-subject] "${instanceId}" 선언된 품목이 없어 품목 줄은 만들지 않는다 — 개체·자리·설비 줄만 옮긴다.`
97
+ )
98
+ }
99
+
100
+ /*
101
+ * ── 진행 지점을 **판 안에서만** 든다 (2026-08-27 · 사용자 판단) ────────────────
102
+ *
103
+ * 처음에는 테이블에 적었다. 사용자 판단으로 없앴다 — 사건이 들어올 때 파생 테이블도 같이 적히므로,
104
+ * 옛 사건을 옮기는 일은 새 파생 테이블을 만들 때와 계산 규칙을 고쳐 다시 만들 때뿐이다. 그 두 경우가
105
+ * 흔하지 않다.
106
+ *
107
+ * 대가는 이것이다. 옮기는 중에 서버가 내려가면 그 몫을 다시 읽는다. 335만 건이면 20분쯤이다.
108
+ *
109
+ * 그래서 **목표와 지점을 이 프로세스가 든다.** 트윈마다 한 줄이고, 서버가 내려가면 사라진다.
110
+ */
111
+ const events = getRepository(TwinEvent)
112
+ const key = `${domainId}:${instanceId}`
113
+ let held = inFlight.get(key)
114
+ if (!held) {
115
+ /*
116
+ * 목표는 **처음 부를 때의 저널 끝**이다. 매번 다시 재면 그 사이에 새 사건이 들어와 끝나지 않는다.
117
+ * 그 뒤의 사건은 저널을 쓸 때 파생 테이블에도 같이 적히므로 이 작업의 몫이 아니다.
118
+ */
119
+ const tip = await events
120
+ .createQueryBuilder('e')
121
+ .select('MAX(e.revision)', 'max')
122
+ .where('e.domain = :domainId', { domainId })
123
+ .andWhere('e.instanceId = :instanceId', { instanceId })
124
+ .getRawOne<{ max: unknown }>()
125
+ held = { upToRevision: 0, targetRevision: Number(tip?.max ?? 0) }
126
+ inFlight.set(key, held)
127
+ }
128
+
129
+ let read = 0
130
+ let written = 0
131
+ let paused = false
132
+ let cursor = held.upToRevision
133
+ const target = held.targetRevision
134
+
135
+ for (;;) {
136
+ if (cursor >= target) break
137
+ if (Date.now() - startedMs >= budgetMs) {
138
+ paused = true
139
+ break
140
+ }
141
+ const rows = await events
142
+ .createQueryBuilder('e')
143
+ .where('e.domain = :domainId', { domainId })
144
+ .andWhere('e.instanceId = :instanceId', { instanceId })
145
+ .andWhere('e.revision > :cursor', { cursor })
146
+ .andWhere('e.revision <= :target', { target })
147
+ .orderBy('e.revision', 'ASC')
148
+ .take(READ_BATCH)
149
+ .getMany()
150
+ if (!rows.length) {
151
+ cursor = target
152
+ break
153
+ }
154
+
155
+ read += rows.length
156
+ written += await writeRows(domainId, instanceId, rows, itemOf)
157
+ cursor = Number(rows[rows.length - 1].revision)
158
+ /* 진행 지점을 그때그때 든다 — 중간에 멈춰도 다음 호출이 여기서 잇는다(같은 프로세스 안에서). */
159
+ held.upToRevision = cursor
160
+ /* 배치 사이에 루프를 비워 준다 — 이 한 줄이 없으면 나눈 뜻이 없다. */
161
+ await new Promise(resolve => setImmediate(resolve))
162
+ }
163
+
164
+ held.upToRevision = cursor
165
+ const complete = cursor >= target
166
+ /* 다 옮겼으면 들고 있을 이유가 없다 — 다시 부르면 목표를 새로 잡는다(그때는 옮길 것이 없다). */
167
+ if (complete) inFlight.delete(key)
168
+ twinLog(
169
+ `[twin-subject] backfill "${instanceId}" — 저널 ${read}행을 읽어 ${written}줄 적었다 · ` +
170
+ `리비전 ${cursor}/${target}` +
171
+ `${complete ? ' · 다 옮겼다' : paused ? ' · 예산을 다 써서 멈췄다 — 다시 부르면 이어서 한다' : ''}`
172
+ )
173
+
174
+ return { instanceId, read, written, upToRevision: cursor, targetRevision: target, complete, paused }
175
+ }
176
+
177
+ /** 저널 행들을 봉투로 되돌려 적는다 — `payload` 가 봉투 전체다. */
178
+ async function writeRows(domainId: string, instanceId: string, rows: TwinEvent[], itemOf?: any): Promise<number> {
179
+ return await writeSubjectRows(
180
+ domainId,
181
+ instanceId,
182
+ rows.map(r => ({
183
+ /*
184
+ * `payload` 가 봉투 전체다(`{ eventId, eventType, eventTime, tenantId, data: {…} }`). 한 겹 더
185
+ * 감싸면 키가 묻혀 아무것도 찾지 못한다 — 실측으로 825,000행을 읽어 0줄을 적었다.
186
+ *
187
+ * 시각과 종류는 승격 컬럼을 믿는다. payload 안의 값과 컬럼이 어긋난 옛 행이 있을 수 있고,
188
+ * 이 표의 정렬 축은 컬럼이 정본이다.
189
+ */
190
+ envelope: { ...((r as any).payload ?? {}), eventType: r.eventType, eventTime: r.eventTime },
191
+ revision: Number(r.revision),
192
+ eventId: r.id
193
+ })),
194
+ itemOf
195
+ )
196
+ }
@@ -0,0 +1,101 @@
1
+ import { Field, Int, ObjectType } from 'type-graphql'
2
+ import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId } from 'typeorm'
3
+ import { Domain } from '@things-factory/shell'
4
+
5
+ /*
6
+ * **대상별 사건 목록** — 조회용 파생 표. 사실의 원본은 `twin_events` 다.
7
+ *
8
+ * ── 왜 만드나 (2026-08-27 실측) ─────────────────────────────────────────────
9
+ * 360 화면이 「이 대상의 이력」을 묻고, 그 조회가 64초다. 원인은 「무엇으로 걸러 시각순으로 정렬해
10
+ * 앞에서 30개」라는 모양에 맞는 목차가 없다는 것이다. 그리고 목차를 얹는 방식으로는 풀리지 않는다.
11
+ *
12
+ * 정렬이 목차를 무력화한다 여러 값으로 거르면(IN) 합칠 때 다시 정렬해야 한다
13
+ * 축이 늘 때마다 목차가 늘어난다 360 의 축 넷을 다 빠르게 하면 `twin_events` 목차가 13개가 된다
14
+ * 소급 채우기가 매번 든다 목차 하나 만드는 데 3분 36초 · 파일 +2.4GB
15
+ * 품목 축은 그래도 안 풀린다 저장된 식별자에 품목과 로트가 한 문자열로 붙어 있다
16
+ *
17
+ * ── 이 표가 무엇을 담나 ─────────────────────────────────────────────────────
18
+ * 사건 하나가 가리키는 **대상마다 한 줄**이다. 품목 이동 사건이면 품목·로트·자리로 세 줄이다.
19
+ * 종류(`subjectKind`)를 값으로 밝히므로 읽는 쪽이 사건 종류표를 들고 있지 않아도 된다. 뜻이
20
+ * 달라지는 키 칸을 쓰지 않는 이유이고, 그 방식이 틀렸다는 뜻은 아니다 — 정의가 함께 있는
21
+ * 프레임워크에서는 합리적이다. 이 표는 볼 정의가 없다.
22
+ *
23
+ * ── 규칙 넷 (설계: read-models.md §4) ───────────────────────────────────────
24
+ * ① 조회는 이 표가 답한다. `twin_events` 에 목차를 얹어 답하지 않는다
25
+ * ② **앞으로만 채운다.** 소급하지 않는다 — 새 키가 필요할 때 3,128만 줄을 다시 만들지 않는다
26
+ * ③ 준비되지 않은 구간을 화면이 말한다. 비어 있는 것을 「없다」로 그리지 않는다
27
+ * ④ 언제든 버릴 수 있다. 사건에서 파생된 값이므로 지워도 사실이 사라지지 않는다
28
+ *
29
+ * ── 왜 사건 본문을 베끼지 않나 ──────────────────────────────────────────────
30
+ * `eventId` 로 원본을 가리키고, 목록을 그리는 데 필요한 값만 함께 둔다(종류·시각·리비전). 본문까지
31
+ * 베끼면 이 표가 원본만큼 커지고, 원본을 고칠 때 두 곳이 어긋난다.
32
+ */
33
+ @Entity()
34
+ /*
35
+ * 이 표의 유일한 목차 — 이 순서가 곧 이 표의 존재 이유다.
36
+ *
37
+ * 「이 트윈의 이 대상의 것을 최신순으로 30개」가 목차를 위에서 30줄 읽고 끝난다. 정렬이 없다 —
38
+ * 거르는 값이 하나이고 그 다음이 시각이기 때문이다.
39
+ */
40
+ @Index(
41
+ 'ix_twin_subject_event_0',
42
+ (e: TwinSubjectEvent) => [e.domain, e.instanceId, e.subjectKind, e.subjectId, e.eventTime],
43
+ { unique: false }
44
+ )
45
+ /* 보관 상한을 지우는 축 — 「이 트윈에서 오래된 줄」을 찾는다(§`twin-subject-writer` 의 정리). */
46
+ @Index('ix_twin_subject_event_1', (e: TwinSubjectEvent) => [e.domain, e.instanceId, e.eventTime], {
47
+ unique: false
48
+ })
49
+ @ObjectType({ description: 'One row per subject an event refers to — the read model behind entity history.' })
50
+ export class TwinSubjectEvent {
51
+ @PrimaryGeneratedColumn('uuid')
52
+ @Field(type => String, { description: 'Row id.' })
53
+ readonly id: string
54
+
55
+ @ManyToOne(type => Domain)
56
+ domain: Domain
57
+
58
+ @RelationId((e: TwinSubjectEvent) => e.domain)
59
+ domainId?: string
60
+
61
+ @Column()
62
+ @Field({ description: 'The twin instance this row belongs to.' })
63
+ instanceId: string
64
+
65
+ @Column()
66
+ @Field({
67
+ description:
68
+ 'What kind of thing this row points at: item, lot, order, location, equipment or task. The kind is stored as a value so a reader never has to know which event types put what where.'
69
+ })
70
+ subjectKind: string
71
+
72
+ @Column()
73
+ @Field({ description: 'Identifier of that thing, within its kind.' })
74
+ subjectId: string
75
+
76
+ /*
77
+ * **타입 이름을 적지 않는다** — `twin-event.ts` 의 `eventTime` 과 같은 이유다.
78
+ *
79
+ * `datetime` 은 sqlite·MySQL 의 이름이고 Postgres 는 `timestamp` 만 안다. 다섯 드라이버에 다
80
+ * 통하는 이름이 없어서, 이름을 고르는 일을 TypeORM 에 맡긴다(TS 타입 `Date` 에서 고른다).
81
+ */
82
+ @Column()
83
+ @Field({ description: 'When the event happened, by the twin clock. This is the axis the history list is ordered by.' })
84
+ eventTime: Date
85
+
86
+ @Column({ type: 'int' })
87
+ @Field(type => Int, { description: 'Journal revision of the event — ties this row to its place in the record.' })
88
+ revision: number
89
+
90
+ @Column()
91
+ @Field({ description: 'Event type, so the list can be drawn without reading the journal row.' })
92
+ eventType: string
93
+
94
+ @Column({ nullable: true })
95
+ @Field({ nullable: true, description: 'Journal row this points at. Null when the journal row has been pruned.' })
96
+ eventId?: string
97
+
98
+ @CreateDateColumn()
99
+ @Field({ description: 'When this row was written.' })
100
+ createdAt?: Date
101
+ }
@@ -0,0 +1,346 @@
1
+ import { decodeCursor, getRepository } from '@things-factory/shell'
2
+
3
+ import { TwinEvent } from '../twin-event/twin-event.js'
4
+ import { JOURNAL_COUNT_CAP as COUNT_CAP } from '../twin-event/journal-count.js'
5
+ import { TwinSubjectEvent } from './twin-subject-event.js'
6
+ import { subjectWriteFailures } from './twin-subject-writer.js'
7
+ import { itemResolverFor } from './item-resolver.js'
8
+ import type { SubjectKind } from './event-subjects.js'
9
+ import { readModelShape, type SubjectQueryShape as Shape } from './subject-query-shape.js'
10
+ import { absoluteTime } from '../../engine/read-time.js'
11
+ import { TwinEngine } from '../../engine/index.js'
12
+
13
+ /*
14
+ * **화면이 이미 부르는 문에서 파생 테이블을 쓴다** (2026-08-28).
15
+ *
16
+ * ── 무엇이 잘못되어 있었나 ──────────────────────────────────────────────────
17
+ * 대상별 사건 목록(`twin_subject_events`)을 만들고 조회도 새로 열었는데(`twinSubjectHistory`),
18
+ * 360 화면은 예전부터 `twinEventList` 를 부르고 있었다. 그래서 테이블은 8,382,017줄을 채우고도
19
+ * 쓰이지 않았고, 화면의 이력 조회는 그대로 64초였다. **자리는 있고 길이 없었다.**
20
+ *
21
+ * 새 문으로 옮기라고 화면에 요구하지 않는다. 화면이 이미 서 있는 문 안에서 답을 어디서 가져오는지만
22
+ * 바꾼다 — 계약(`TwinEventList`)도, 커서도, 응답 모양도 그대로다.
23
+ *
24
+ * ── 어떤 물음일 때만 쓰나 ───────────────────────────────────────────────────
25
+ * 파생 테이블은 「대상 하나를 시각순으로」에 맞춰 생겼다. 그 모양이 아닌 물음은 여기서 답하지 않고
26
+ * 저널로 보낸다(`null` 을 돌려준다). 좁게 잡는 이유는, 맞지 않는 물음에 억지로 답하면 목록이
27
+ * 알리지 않고 달라지기 때문이다.
28
+ *
29
+ * 쓴다 트윈 하나 · 축 하나(품목·오더·자리·설비) · 시각 내림차순 · 시각 상한(시간여행)까지
30
+ * 안 쓴다 여러 트윈 · 자유 검색 · 종류 필터 · 상관 식별자 · 그 밖의 정렬
31
+ *
32
+ * ── 어디까지 답할 수 있나 ───────────────────────────────────────────────────
33
+ * 파생 테이블은 만들어진 뒤부터 채워진다. 그보다 앞의 사건은 저널에는 있고 여기에는 없다. 그 경계를
34
+ * `coveredFrom`(이 트윈의 가장 오래된 파생 줄의 시각)이라 하고, **경계를 넘는 페이지는 저널로 보낸다.**
35
+ * 커서 모양이 저널 경로와 같으므로 넘기는 자리에서 그냥 이어진다 — 앞은 빠르고, 경계 아래는 느리지만
36
+ * 온전하다. 없는 것을 「사건이 없다」로 그리는 일은 생기지 않는다.
37
+ *
38
+ * 적지 못한 줄이 있으면(`subjectWriteFailures`) 그 창에 구멍이 있다는 뜻이므로 아예 저널로 보낸다.
39
+ *
40
+ * 설계: `operato-twin/design/plans/read-models.md`
41
+ */
42
+
43
+ export interface FastPathResult {
44
+ items: TwinEvent[]
45
+ nextCursor?: string
46
+ /** 이 트윈에서 파생 테이블이 말할 수 있는 가장 오래된 시각 — 화면이 경계를 말할 수 있게. */
47
+ coveredFrom?: Date
48
+ /** 파생 테이블이 저널의 처음까지 덮고 있나 — 아니면 전체 건수를 「모른다」로 낸다. */
49
+ fullyCovered: boolean
50
+ }
51
+
52
+ /**
53
+ * 화면이 건넨 축 값 → 파생 테이블의 키.
54
+ *
55
+ * 자리·오더·설비는 화면과 저장이 같은 값을 쓴다. **품목만 다르다**: 화면은 짧게 줄인 것을 들고 있고
56
+ * (`net/class/product-M1`) 저장된 것은 전체 식별자다. 그 되돌림을 적는 쪽과 **같은 함수**로 한다
57
+ * (`itemResolverFor`) — 두 벌로 적으면 어느 날 서로 다른 답을 내고, 그때 이력이 빈다.
58
+ */
59
+ async function canonicalize(domainId: string, instanceId: string, kind: SubjectKind, value: string): Promise<string | null> {
60
+ if (kind !== 'item') return value
61
+ const det = await TwinEngine.detail(domainId, instanceId).catch(() => null)
62
+ const itemOf = det?.model ? itemResolverFor(domainId, instanceId, det.model) : undefined
63
+ if (!itemOf) return null
64
+ return itemOf(value) ?? null
65
+ }
66
+
67
+ /**
68
+ * 이 트윈에서 파생 테이블이 말할 수 있는 경계와, 저널의 처음까지 덮는지.
69
+ *
70
+ * 둘 다 목차만 읽는 질의다(`(domain, instance, event_time)` 의 첫 줄). 그래서 부를 때마다 구해도
71
+ * 값이 싸고, 저장해 두지 않으므로 실제 데이터와 어긋날 일이 없다.
72
+ */
73
+ async function coverage(domainId: string, instanceId: string): Promise<{ from?: Date; full: boolean }> {
74
+ /*
75
+ * ── **원시 값을 직접 날짜로 읽지 않는다** (2026-08-28 실측) ──────────────────
76
+ *
77
+ * `getRawOne` 은 드라이버가 준 값을 그대로 준다. sqlite 의 그 값은 시간대가 없는 문자열
78
+ * (`2026-08-27 14:37:20.049`)이고, 그것을 `new Date()` 로 읽으면 **한국 시간으로 해석**해
79
+ * UTC 로 9시간을 뺀다. 실측: 커서의 시각이 9시간 이르게 나가 이어 읽기가 구간을 건너뛰었다.
80
+ *
81
+ * `getOne` 은 엔티티로 받으므로 컬럼 변환이 걸린다 — 드라이버가 무엇이든 같은 값이 된다.
82
+ * 조인이 없으므로 감싸지 않고, 상한이 안쪽에 걸린다(§`twinEvents` 의 실측).
83
+ */
84
+ const [sub, journal] = await Promise.all([
85
+ getRepository(TwinSubjectEvent)
86
+ .createQueryBuilder('s')
87
+ .where('s.domain = :domainId', { domainId })
88
+ .andWhere('s.instanceId = :instanceId', { instanceId })
89
+ .orderBy('s.eventTime', 'ASC')
90
+ .limit(1)
91
+ .getOne()
92
+ .catch(() => null),
93
+ getRepository(TwinEvent)
94
+ .createQueryBuilder('e')
95
+ .where('e.domain = :domainId', { domainId })
96
+ .andWhere('e.instanceId = :instanceId', { instanceId })
97
+ .orderBy('e.eventTime', 'ASC')
98
+ .limit(1)
99
+ .getOne()
100
+ .catch(() => null)
101
+ ])
102
+ const from = sub?.eventTime instanceof Date ? sub.eventTime : undefined
103
+ const oldest = journal?.eventTime instanceof Date ? journal.eventTime : undefined
104
+ if (!from || Number.isNaN(from.getTime())) return { from: undefined, full: false }
105
+ const full = !!oldest && !Number.isNaN(oldest.getTime()) && from.getTime() <= oldest.getTime()
106
+ return { from, full }
107
+ }
108
+
109
+ /**
110
+ * 파생 테이블로 한 페이지 — 답할 수 없으면 `null`(부르는 쪽이 저널로 간다).
111
+ *
112
+ * 두 걸음이다. ① 파생 테이블에서 리비전 번호를 페이지만큼 읽는다(목차만 읽으므로 정렬이 없다).
113
+ * ② 그 번호로 저널 줄을 가져온다(`(domain, instance, revision)` 목차의 키 조회 · 50줄).
114
+ * 두 번째 걸음이 필요한 이유는 화면이 저널 줄의 내용(`payload`·`bizStep`…)을 그리기 때문이다 —
115
+ * 파생 테이블은 목차이지 사실의 집이 아니다.
116
+ */
117
+ export async function subjectFastPage(
118
+ domainId: string,
119
+ instanceId: string,
120
+ shape: Shape,
121
+ limit: number,
122
+ after?: string
123
+ ): Promise<FastPathResult | null> {
124
+ /* 적지 못한 줄이 있으면 이 창에 구멍이 있다 — 그때는 온전한 쪽(저널)으로 보낸다. */
125
+ if (subjectWriteFailures(domainId, instanceId) > 0) return null
126
+
127
+ const subjectId = await canonicalize(domainId, instanceId, shape.kind, shape.value)
128
+ if (!subjectId) return null
129
+
130
+ const cov = await coverage(domainId, instanceId)
131
+ if (!cov.from) return null
132
+
133
+ /* 이어 읽기 지점 — 저널 경로와 **같은 커서**를 읽는다(`[eventTime, revision]`). */
134
+ let cursor: { at: Date; revision: number } | null = null
135
+ if (after) {
136
+ cursor = decodeJournalCursor(after)
137
+ if (!cursor) return null
138
+ }
139
+
140
+ /*
141
+ * 경계를 넘는 페이지는 여기서 답하지 않는다. 파생 테이블에는 그 아래가 없으므로, 답하면 목록이
142
+ * 「여기서 끝」이라고 말하게 된다. 저널로 보내면 커서가 그대로 이어진다.
143
+ *
144
+ * 이어 읽기 지점이 경계와 **같을** 때도 보낸다 — 그 커서는 아래에서 우리가 직접 만든 「경계 아래를
145
+ * 읽어라」는 표시이고(§`boundaryCursor`), 여기서 받으면 같은 페이지를 되풀게 된다.
146
+ */
147
+ if (cursor && cursor.at.getTime() <= cov.from.getTime()) return null
148
+ if (shape.untilTime && shape.untilTime.getTime() < cov.from.getTime()) return null
149
+
150
+ const qb = getRepository(TwinSubjectEvent)
151
+ .createQueryBuilder('s')
152
+ .select('s.revision', 'revision')
153
+ .where('s.domain = :domainId', { domainId })
154
+ .andWhere('s.instanceId = :instanceId', { instanceId })
155
+ .andWhere('s.subjectKind = :kind', { kind: shape.kind })
156
+ .andWhere('s.subjectId = :subjectId', { subjectId })
157
+ if (shape.fromTime) qb.andWhere('s.eventTime >= :fromTime', { fromTime: shape.fromTime })
158
+ if (shape.untilTime) qb.andWhere('s.eventTime <= :untilTime', { untilTime: shape.untilTime })
159
+ if (cursor) {
160
+ /* 시각이 같은 줄이 여럿이므로 리비전으로 갈라 이어 읽는다 — 건너뛰거나 되풀지 않게. */
161
+ qb.andWhere('(s.eventTime < :curAt OR (s.eventTime = :curAt AND s.revision < :curRev))', {
162
+ curAt: cursor.at,
163
+ curRev: cursor.revision
164
+ })
165
+ }
166
+ const picked = await qb
167
+ .orderBy('s.eventTime', 'DESC')
168
+ .addOrderBy('s.revision', 'DESC')
169
+ .limit(limit)
170
+ .getRawMany<{ revision: number }>()
171
+ .catch(() => null)
172
+ if (!picked) return null
173
+ if (!picked.length) {
174
+ /* 이 대상의 줄이 경계 안에 없다 — 경계 아래는 저널만 안다. 그 사실을 커서로 넘긴다. */
175
+ return { items: [], nextCursor: cov.full ? undefined : boundaryCursor(cov.from), coveredFrom: cov.from, fullyCovered: cov.full }
176
+ }
177
+
178
+ const revisions = picked.map(p => Number(p.revision)).filter(n => Number.isFinite(n))
179
+ const rows = await getRepository(TwinEvent)
180
+ .createQueryBuilder('e')
181
+ .where('e.domain = :domainId', { domainId })
182
+ .andWhere('e.instanceId = :instanceId', { instanceId })
183
+ .andWhere('e.revision IN (:...revisions)', { revisions })
184
+ .getMany()
185
+ .catch(() => null)
186
+ if (!rows) return null
187
+
188
+ /*
189
+ * 저널 줄을 파생 테이블이 정한 순서로 되돌린다. `IN` 조회는 순서를 약속하지 않으므로 여기서
190
+ * 세운다 — 50줄이라 값이 없다.
191
+ */
192
+ const byRevision = new Map(rows.map(r => [Number((r as any).revision), r]))
193
+ const items = revisions.map(r => byRevision.get(r)).filter(Boolean) as TwinEvent[]
194
+
195
+ /*
196
+ * 지워진 저널 줄은 목차에 남아 있을 수 있다(보관 정리가 저널을 먼저 지운 창). 그 차이를 세어
197
+ * 페이지가 짧아진 것을 알린다 — 그러나 여기서는 커서만 정확히 이어 주면 되고, 잃은 사실은 없다.
198
+ */
199
+ /*
200
+ * ── 다음 지점 ───────────────────────────────────────────────────────────────
201
+ * 페이지가 꽉 찼으면 이어 읽기 지점은 이번 페이지의 마지막 줄이다(저널 경로와 같은 축).
202
+ * **시각은 저널 줄에서 가져온다** — 목차의 원시 값을 직접 읽으면 시간대가 어긋난다(§`coverage`).
203
+ *
204
+ * 짧게 끝났으면 이 대상의 줄을 경계 안에서 다 냈다는 뜻이다. 그때 「없다」로 끝내면 안 된다 —
205
+ * 경계 아래에 저널만 아는 사건이 있을 수 있고, 그것을 끝으로 그리면 목록이 알리지 않고 잘린다.
206
+ * 그래서 **경계 아래를 읽으라는 커서**를 낸다. 저널이 그 다음 페이지를 답한다(느리지만 온전하다).
207
+ *
208
+ * 파생 테이블이 저널의 처음까지 덮고 있으면 아래에 아무것도 없으므로 그때만 끝이다.
209
+ */
210
+ let edge: { at: Date; revision: number } | null = null
211
+ if (picked.length === limit) {
212
+ /* 지워진 저널 줄은 목차에 남아 있을 수 있다 — 줄이 있는 마지막 것에서 지점을 만든다. */
213
+ for (let i = revisions.length - 1; i >= 0; i--) {
214
+ const row: any = byRevision.get(revisions[i])
215
+ if (row?.eventTime instanceof Date && !Number.isNaN(row.eventTime.getTime())) {
216
+ edge = { at: row.eventTime, revision: revisions[i] }
217
+ break
218
+ }
219
+ }
220
+ }
221
+ const nextCursor = edge ? encodeJournalCursor(edge.at, edge.revision) : cov.full ? undefined : boundaryCursor(cov.from)
222
+
223
+ return { items, nextCursor, coveredFrom: cov.from, fullyCovered: cov.full }
224
+ }
225
+
226
+ /**
227
+ * 「경계보다 오래된 것을 읽어라」는 지점.
228
+ *
229
+ * 저널 경로의 커서 조건은 `(시각 < v) 또는 (시각 = v 그리고 리비전 < v2)` 다. 그래서 리비전 자리에
230
+ * 0 을 두면 뒷절이 성립하지 않고 **시각이 경계보다 오래된 것만** 남는다 — 경계에 있는 줄은 이미
231
+ * 냈으므로 그것이 정확히 남은 구간이다. 이 커서를 다시 받으면 이 경로는 물러난다(위의 경계 검사).
232
+ */
233
+ function boundaryCursor(from: Date): string {
234
+ return encodeJournalCursor(from, 0)
235
+ }
236
+
237
+ /**
238
+ * 목록 계약 한 페이지를 파생 테이블로 답한다 — 답할 수 없으면 `null`(부르는 쪽이 저널로 간다).
239
+ *
240
+ * ── 전체 건수와 「더 있나」 ──────────────────────────────────────────────────
241
+ * 두 칸을 함께 정직하게 채워야 한다. 화면은 전체 건수를 모를 때 `capped` 로 「더 있나」를 판단한다
242
+ * (`hasMore`). 그래서 건수를 모르면서 `capped` 를 내리면 **목록이 알리지 않고 끝난 것처럼 보인다.**
243
+ *
244
+ * 파생 테이블이 저널의 처음까지 덮는다 → 정확한 전체 건수 · capped 내림
245
+ * 덮지 못한다(앞이 저널에만 있다) → 전체 건수 모름 · capped 올림(가진 수는 하한이다)
246
+ */
247
+ export async function readModelPage(domainId: string, instanceId: string, params: any, limit: number): Promise<TwinEventListShape | null> {
248
+ const shape = readModelShape(params, params?.sortings ?? [])
249
+ if (!shape) return null
250
+
251
+ const page = await subjectFastPage(domainId, instanceId, shape, limit, params?.pagination?.after ?? undefined)
252
+ if (!page) return null
253
+
254
+ let total: number | undefined
255
+ if (page.fullyCovered) {
256
+ const counted = await countSubjectRows(domainId, instanceId, shape)
257
+ if (counted !== null && counted <= COUNT_CAP) total = counted
258
+ }
259
+ /* 정확한 수를 낼 수 없으면 「모른다」로 내고, 가진 수가 하한임을 함께 말한다. */
260
+ const capped = total === undefined
261
+
262
+ return {
263
+ items: page.items,
264
+ total,
265
+ capped,
266
+ countCap: COUNT_CAP,
267
+ nextCursor: page.nextCursor,
268
+ oldestRevision: await oldestJournalRevision(domainId, instanceId)
269
+ }
270
+ }
271
+
272
+ /** 목록 계약이 돌려주는 모양 — 저널 경로와 같은 칸을 채운다. */
273
+ export interface TwinEventListShape {
274
+ items: TwinEvent[]
275
+ total?: number
276
+ capped: boolean
277
+ countCap: number
278
+ nextCursor?: string
279
+ oldestRevision?: number
280
+ }
281
+
282
+ /** 이 대상의 사건 수 — 목차만 읽는다(`(도메인, 트윈, 종류, 식별자, 시각)` 의 앞부분). */
283
+ async function countSubjectRows(domainId: string, instanceId: string, shape: Shape): Promise<number | null> {
284
+ const subjectId = await canonicalize(domainId, instanceId, shape.kind, shape.value)
285
+ if (!subjectId) return null
286
+ const qb = getRepository(TwinSubjectEvent)
287
+ .createQueryBuilder('s')
288
+ .where('s.domain = :domainId', { domainId })
289
+ .andWhere('s.instanceId = :instanceId', { instanceId })
290
+ .andWhere('s.subjectKind = :kind', { kind: shape.kind })
291
+ .andWhere('s.subjectId = :subjectId', { subjectId })
292
+ if (shape.fromTime) qb.andWhere('s.eventTime >= :fromTime', { fromTime: shape.fromTime })
293
+ if (shape.untilTime) qb.andWhere('s.eventTime <= :untilTime', { untilTime: shape.untilTime })
294
+ return qb.getCount().catch(() => null)
295
+ }
296
+
297
+ /**
298
+ * 이 트윈에 남아 있는 가장 오래된 리비전 — 보관 정리로 앞이 지워졌는지 화면이 말할 수 있게.
299
+ * 저널 경로가 내는 것과 같은 값이고 같은 이유다(없었던 것과 지워진 것은 다른 사실이다).
300
+ */
301
+ async function oldestJournalRevision(domainId: string, instanceId: string): Promise<number | undefined> {
302
+ const first = await getRepository(TwinEvent)
303
+ .createQueryBuilder('e')
304
+ .select('e.revision', 'revision')
305
+ .where('e.domain = :domainId', { domainId })
306
+ .andWhere('e.instanceId = :instanceId', { instanceId })
307
+ .orderBy('e.revision', 'ASC')
308
+ .limit(1)
309
+ .getRawOne<{ revision: unknown }>()
310
+ .catch(() => null)
311
+ const at = Number(first?.revision)
312
+ return Number.isFinite(at) && at > 0 ? at : undefined
313
+ }
314
+
315
+ /**
316
+ * 저널 경로와 **같은 축의** 커서를 만든다 — `[eventTime, revision]`.
317
+ *
318
+ * 시각이 같은 줄들 사이에서 무엇이 앞인지 가리는 축이 두 경로에서 같아야 한다. 다르면 한 목록을
319
+ * 나눠 답하는 자리에서 본 줄이 또 나온다(실측: 120건 중 15건). 저널 경로도 트윈 하나를 볼 때는
320
+ * `revision` 으로 가린다(§`cursorTiebreak`).
321
+ */
322
+ function encodeJournalCursor(at: Date, revision: number): string {
323
+ return Buffer.from(JSON.stringify([at, revision]), 'utf8').toString('base64url')
324
+ }
325
+
326
+ /**
327
+ * 커서를 파생 테이블이 쓸 수 있는 지점으로 읽는다 — `[eventTime, revision]`.
328
+ *
329
+ * 예전에는 커서에 저널 줄의 `id` 가 실려 있어서 그 줄을 한 번 읽어 리비전을 얻었다. 그 방식은
330
+ * **지워진 줄에서 어긋났다**: 보관 정리가 저널을 지우면 리비전을 얻지 못하고, 얻은 리비전이
331
+ * 이어 읽기 지점의 시각과 다른 줄의 것일 수 있었다. 이제 두 경로가 같은 축을 쓰므로 커서에 실린
332
+ * 값을 그대로 읽는다 — 조회도 필요 없다.
333
+ */
334
+ function decodeJournalCursor(cursor: string): { at: Date; revision: number } | null {
335
+ let values: unknown[]
336
+ try {
337
+ values = decodeCursor(cursor)
338
+ } catch {
339
+ return null
340
+ }
341
+ if (values.length !== 2) return null
342
+ const at = absoluteTime(values[0])
343
+ const revision = Number(values[1])
344
+ if (!at || !Number.isFinite(revision)) return null
345
+ return { at, revision }
346
+ }