@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,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
+ }
@@ -0,0 +1,210 @@
1
+ import { Arg, Ctx, Directive, Field, Int, Mutation, ObjectType, Query, Resolver } from 'type-graphql'
2
+ import { getRepository } from '@things-factory/shell'
3
+
4
+ import { TwinSubjectEvent } from './twin-subject-event.js'
5
+ import { SUBJECT_KINDS } from './event-subjects.js'
6
+ import { subjectWriteFailures } from './twin-subject-writer.js'
7
+ import { backfillSubjects } from './twin-subject-backfill.js'
8
+ import { hydratedDate } from '../../engine/read-time.js'
9
+
10
+ /*
11
+ * **대상별 사건 목록 조회** — 360 화면의 이력이 이 문으로 온다.
12
+ *
13
+ * ── 무엇이 달라지나 (2026-08-27 실측) ───────────────────────────────────────
14
+ * 지금 화면은 저널을 직접 묻고 그 조회가 64초다. 「무엇으로 걸러 시각순으로 정렬해 앞에서 30개」라는
15
+ * 모양에 맞는 목차가 저널에 없고, 얹어도 풀리지 않는다(정렬이 목차를 무력화한다 · 품목과 로트가 한
16
+ * 문자열에 붙어 있다).
17
+ *
18
+ * 이 표는 그 모양대로 생겼다. 목차가 `(도메인, 트윈, 종류, 식별자, 시각)` 이므로 위에서 30줄 읽고
19
+ * 끝난다 — 정렬이 없다.
20
+ *
21
+ * ── 준비되지 않은 구간을 말한다 ─────────────────────────────────────────────
22
+ * 이 표는 **앞으로만 채운다**(소급하지 않는다). 그래서 표가 만들어진 시점보다 앞의 사건은 여기에 없다.
23
+ * 그것을 「사건이 없다」로 그리면 화면이 거짓을 말한다. `coveredFrom` 이 그 경계를 함께 낸다 —
24
+ * 화면은 「이 목록은 …부터입니다. 그 앞은 기간을 지정해 조회하십시오」를 말할 수 있다.
25
+ *
26
+ * 적지 못한 줄이 있으면 그것도 함께 낸다(`writeFailures`). 알리지 않고 비면 화면이 「그 대상에는 사건이
27
+ * 없다」고 말하게 된다.
28
+ *
29
+ * 설계: `operato-twin/design/plans/read-models.md`
30
+ */
31
+
32
+ @ObjectType({ description: 'One row of a subject history list.' })
33
+ export class TwinSubjectEventView {
34
+ @Field({ description: 'Event type.' })
35
+ eventType: string
36
+
37
+ @Field({ description: 'When it happened, by the twin clock.' })
38
+ eventTime: Date
39
+
40
+ @Field(type => Int, { description: 'Journal revision — where this sits in the record.' })
41
+ revision: number
42
+
43
+ @Field({ nullable: true, description: 'Journal row id. Null when that row has been pruned.' })
44
+ eventId?: string
45
+ }
46
+
47
+ @ObjectType({
48
+ description:
49
+ 'History of one subject, newest first, plus how far back this list can speak for. The list is a read model built forward from the journal, so it does not cover events older than coveredFrom — say so on screen instead of drawing an empty list as "nothing happened".'
50
+ })
51
+ export class TwinSubjectHistory {
52
+ @Field(type => [TwinSubjectEventView], { description: 'The events on this page, newest first.' })
53
+ items: TwinSubjectEventView[]
54
+
55
+ @Field({
56
+ nullable: true,
57
+ description:
58
+ 'Oldest event time this list can speak for, for this twin. Events before it are in the journal but not in this list — the screen must say so rather than implying there were none. Null when the list holds nothing for this twin yet.'
59
+ })
60
+ coveredFrom?: Date
61
+
62
+ @Field({ nullable: true, description: 'Cursor for the next page: pass it back as before. Null when this page is the last.' })
63
+ nextBefore?: Date
64
+
65
+ @Field(type => Int, {
66
+ description:
67
+ 'Rows this twin failed to write into the read model since start. Greater than zero means this list is incomplete for reasons other than coverage — the journal still holds those events.'
68
+ })
69
+ writeFailures: number
70
+ }
71
+
72
+ const PAGE_MAX = 200
73
+
74
+ @Resolver()
75
+ export class TwinSubjectQuery {
76
+ @Query(returns => TwinSubjectHistory, {
77
+ description:
78
+ 'Events that refer to one subject (item, lot, order, location, equipment or task), newest first. Answered from a read model shaped for this question — one index lookup, no sort — instead of scanning the event journal. Paging is by time (before), not offset, because the list grows at the head. Always reports coveredFrom: this read model is built forward and does not contain events older than that, and an empty list before it means "not covered", not "nothing happened".'
79
+ })
80
+ async twinSubjectHistory(
81
+ @Ctx() context: any,
82
+ @Arg('instanceId', { description: 'The twin instance to read.' }) instanceId: string,
83
+ @Arg('subjectKind', { description: `One of: ${SUBJECT_KINDS.join(', ')}.` }) subjectKind: string,
84
+ @Arg('subjectId', { description: 'Identifier of the thing, within its kind.' }) subjectId: string,
85
+ @Arg('limit', type => Int, { nullable: true, description: `How many rows, at most ${PAGE_MAX}.` }) limit?: number,
86
+ @Arg('before', { nullable: true, description: 'Read events strictly older than this time — the previous page cursor.' })
87
+ before?: Date
88
+ ): Promise<TwinSubjectHistory> {
89
+ const domain = context.state.domain
90
+ const kind = String(subjectKind ?? '').trim()
91
+ /*
92
+ * 종류는 닫힌 목록이다. 모르는 값이면 빈 목록이 아니라 오류를 낸다 — 빈 목록으로 답하면 오타가
93
+ * 「그 대상에는 사건이 없다」로 보인다.
94
+ */
95
+ if (!(SUBJECT_KINDS as readonly string[]).includes(kind)) {
96
+ throw new Error(`unknown subjectKind "${subjectKind}" — one of: ${SUBJECT_KINDS.join(', ')}`)
97
+ }
98
+ const id = String(subjectId ?? '').trim()
99
+ if (!id) throw new Error('subjectId is required — an empty subject would read the whole twin')
100
+
101
+ const take = Math.min(Math.max(Number(limit) || 30, 1), PAGE_MAX)
102
+ const repo = getRepository(TwinSubjectEvent)
103
+
104
+ const qb = repo
105
+ .createQueryBuilder('s')
106
+ .where('s.domain = :domainId', { domainId: domain.id })
107
+ .andWhere('s.instanceId = :instanceId', { instanceId })
108
+ .andWhere('s.subjectKind = :kind', { kind })
109
+ .andWhere('s.subjectId = :id', { id })
110
+ /*
111
+ * 같은 시각에 사건이 여럿일 수 있다. 그때 순서를 리비전으로 가른다 — 그러지 않으면 같은 목록을
112
+ * 두 번 물었을 때 순서가 달라지고, 시각 커서로 넘기는 페이지에서 본 줄이 다시 나온다.
113
+ */
114
+ .orderBy('s.eventTime', 'DESC')
115
+ .addOrderBy('s.revision', 'DESC')
116
+ .take(take + 1)
117
+ /* 시각으로 페이지를 넘긴다 — 머리에 계속 쌓이는 목록에서 건너뛰기(offset)는 본 줄을 다시 낸다. */
118
+ if (before) qb.andWhere('s.eventTime < :before', { before })
119
+
120
+ const rows = await qb.getMany()
121
+ const items = rows.slice(0, take).map(r => ({
122
+ eventType: r.eventType,
123
+ eventTime: r.eventTime,
124
+ revision: Number(r.revision),
125
+ ...(r.eventId ? { eventId: r.eventId } : {})
126
+ }))
127
+ const nextBefore = rows.length > take ? items[items.length - 1]?.eventTime : undefined
128
+
129
+ return {
130
+ items,
131
+ ...(await coverage(domain.id, instanceId)),
132
+ ...(nextBefore ? { nextBefore } : {}),
133
+ writeFailures: subjectWriteFailures(domain.id, instanceId)
134
+ }
135
+ }
136
+ }
137
+
138
+ /**
139
+ * 이 표가 이 트윈에 대해 말할 수 있는 가장 오래된 시각 — 저장하지 않고 조회할 때 구한다.
140
+ *
141
+ * 저장하면 실제 자료와 어긋날 수 있고, 어긋나면 어느 쪽이 사실인지 알 수 없다. 목차
142
+ * `(도메인, 트윈, 시각)` 의 첫 줄 하나라서 값이 싸다.
143
+ */
144
+ async function coverage(domainId: string, instanceId: string): Promise<{ coveredFrom?: Date }> {
145
+ const first = await getRepository(TwinSubjectEvent)
146
+ .createQueryBuilder('s')
147
+ .select('s.eventTime', 'eventTime')
148
+ .where('s.domain = :domainId', { domainId })
149
+ .andWhere('s.instanceId = :instanceId', { instanceId })
150
+ .orderBy('s.eventTime', 'ASC')
151
+ .limit(1)
152
+ .getRawOne<{ eventTime: unknown }>()
153
+ .catch(() => null)
154
+ /* 원시 결과의 컬럼 값은 드라이버에게 해석시킨다(§`hydratedDate`). */
155
+ const at = hydratedDate(getRepository(TwinSubjectEvent), 'eventTime', first?.eventTime)
156
+ return at ? { coveredFrom: at } : {}
157
+ }
158
+
159
+ @ObjectType({ description: 'How far a backfill got.' })
160
+ export class TwinSubjectBackfillResult {
161
+ @Field({ description: 'The twin that was filled.' })
162
+ instanceId: string
163
+
164
+ @Field(type => Int, { description: 'Journal rows read.' })
165
+ read: number
166
+
167
+ @Field(type => Int, { description: 'Rows written into the read model.' })
168
+ written: number
169
+
170
+ /*
171
+ * 진행 상황을 리비전으로 말한다 — 옮기는 쪽이 저널을 리비전 순으로 읽고, 그 지점을 이 프로세스가 든다.
172
+ *
173
+ * 이 자리를 시각으로 적어 두었다가 옮기는 방식과 어긋났다. 응답의 칸과 실제로 드는 값이 다르면
174
+ * 부르는 쪽이 진행을 판단할 수 없다.
175
+ */
176
+ @Field(type => Int, { description: 'Journal revision reached so far.' })
177
+ upToRevision: number
178
+
179
+ @Field(type => Int, { description: 'The target: the journal tip at the moment this fill started. Reaching it means the twin is fully filled.' })
180
+ targetRevision: number
181
+
182
+ @Field({ description: 'True when upToRevision has reached the target.' })
183
+ complete: boolean
184
+
185
+ @Field({ description: 'True when the call stopped on its time budget rather than finishing.' })
186
+ paused: boolean
187
+ }
188
+
189
+ @Resolver()
190
+ export class TwinSubjectMutation {
191
+ /* 쓰기는 정본 게이트를 단다 — 로그인만 하면 누구나 부를 수 있는 문을 두지 않는다. */
192
+ @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
193
+ @Mutation(returns => TwinSubjectBackfillResult, {
194
+ description:
195
+ 'Move already-stored events of one twin into the subject read model. Not automatic: the read model is built forward, so filling old events is an explicit request for a twin that already has a journal. Works in slices on the time axis: each call fills events older than what the read model already covers, so calling it again continues downward with nothing to pass in. Blocks cannot split, and calling it twice does not duplicate rows.'
196
+ })
197
+ async backfillTwinSubjects(
198
+ @Ctx() context: any,
199
+ @Arg('instanceId', { description: 'The twin to fill.' }) instanceId: string,
200
+ @Arg('budgetMs', type => Int, { nullable: true, description: 'How long this call may run, in milliseconds. Default 30000.' })
201
+ budgetMs?: number
202
+ ): Promise<TwinSubjectBackfillResult> {
203
+ const domain = context.state.domain
204
+ /*
205
+ * 이어받기 인자가 없다 — 이 표의 현재 바닥(가장 오래된 시각)이 곧 이어받기 지점이다. 늘 그 아래만
206
+ * 채우므로 덩어리가 갈라지지 않고, 부르는 쪽이 아무것도 들고 있지 않아도 이어서 한다.
207
+ */
208
+ return await backfillSubjects(domain.id, instanceId, { budgetMs: budgetMs ?? undefined })
209
+ }
210
+ }