@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
@@ -11,6 +11,10 @@ import { TwinEngine } from '../../engine/index.js'
11
11
  import { pickAxes } from '../../engine/state-axes.js'
12
12
  import { digestAttentions } from '../../engine/attention-digest.js'
13
13
  import { computeTwinKpi, resolveTwinTargets } from '../../engine/kpi-query.js'
14
+ import { journalOrder } from './journal-order.js'
15
+ import { readModelPage } from '../twin-subject/twin-subject-fast-path.js'
16
+ import { eventTypeSplitPage } from './event-type-split.js'
17
+ import { splitShape } from './event-type-split-shape.js'
14
18
 
15
19
  /*
16
20
  * 저널 읽기 채널 — 상향 query.
@@ -41,7 +45,9 @@ function toDateValue(v: any): any {
41
45
  @Resolver()
42
46
  export class TwinJournalQuery {
43
47
  @Query(returns => [TwinEvent], {
44
- description: 'Read the append-only twin event journal (Ledger / History / audit), filtered and ordered by revision.'
48
+ description:
49
+ 'Read the append-only twin event journal (Ledger / History / audit). Newest first by event time; ' +
50
+ 'ordered by revision only when a revision range is given.'
45
51
  })
46
52
  async twinEvents(
47
53
  @Arg('instanceId') instanceId: string,
@@ -50,12 +56,33 @@ export class TwinJournalQuery {
50
56
  @Arg('eventType', { nullable: true }) eventType: string,
51
57
  @Arg('limit', type => Int, { nullable: true }) limit: number,
52
58
  @Ctx() context: ResolverContext,
53
- @Arg('untilTime', { nullable: true }) untilTime?: string
59
+ @Arg('untilTime', { nullable: true }) untilTime?: string,
60
+ @Arg('fromTime', { nullable: true }) fromTime?: string
54
61
  ): Promise<TwinEvent[]> {
55
62
  const where: any = { domain: { id: context.state.domain.id }, instanceId }
56
63
  if (eventType) where.eventType = eventType
57
- /* 시각 커서(untilTime, ISO) 우선 — eventTime ≤ T(공유 clock). 컬럼이 날짜라 **날짜로 넘긴다.** */
58
- if (untilTime != null) where.eventTime = LessThanOrEqual(new Date(untilTime))
64
+ /*
65
+ * ── 구간으로 물을 수 있게 한다 (2026-08-25 실측) ────────────────────────────
66
+ *
67
+ * `untilTime` 만 있으면 **끝만 정해진 조회**다. 그러면 위쪽 경계에서 뒤로 걸어오며 종류를 하나씩
68
+ * 확인해야 하고, 찾는 종류가 며칠 전에 멈춘 트윈에서는 그 사이 행을 전부 열어 본다. 실측:
69
+ *
70
+ * 공정 타임라인이 부르는 방식(종류만·200건) 22.0초
71
+ * 같은 조회에 `untilTime` 만 더한 것 9.7초
72
+ *
73
+ * 이유는 계획이 말한다. 종류별 시각순 인덱스가 없어서 `(domain, instanceId, eventTime)` 로 걷고
74
+ * 행마다 테이블을 열어 종류를 본다. 승화푸드에서 작업 사실은 8월 22일에 멈춰 있고 그 뒤로 리비전이
75
+ * 110만 늘었으므로, 200건에 닿기까지 그만큼을 읽고 버린다.
76
+ *
77
+ * **구간을 주면 그 범위만 걷는다**(`event_time>? AND event_time<?`). 화면은 자기가 그리는 구간을
78
+ * 알고 있으므로 그것을 그대로 물으면 된다 — 「전체」가 실제로는 상한에 걸린 6.7시간이었던 문제도
79
+ * 같은 자리에서 없어진다.
80
+ *
81
+ * 컬럼이 날짜라 **날짜로 넘긴다**(문자열로 비교하면 sqlite 에서 오류 없이 빈 결과가 나온다).
82
+ */
83
+ if (untilTime != null && fromTime != null) where.eventTime = Between(new Date(fromTime), new Date(untilTime))
84
+ else if (untilTime != null) where.eventTime = LessThanOrEqual(new Date(untilTime))
85
+ else if (fromTime != null) where.eventTime = MoreThanOrEqual(new Date(fromTime))
59
86
  else if (fromRevision != null && toRevision != null) where.revision = Between(fromRevision, toRevision)
60
87
  else if (fromRevision != null) where.revision = MoreThanOrEqual(fromRevision)
61
88
  else if (toRevision != null) where.revision = LessThanOrEqual(toRevision)
@@ -75,7 +102,7 @@ export class TwinJournalQuery {
75
102
  * `... FOR LAST TERM OF ORDER BY` — 시각은 인덱스(`ix_twin_event_1`)에서 순서대로 나오고
76
103
  * **동시각만** 임시 정렬한다.
77
104
  *
78
- * **리비전 경로는 그대로 둔다.** 그때는 거르는 축이 `revision` 이고 정렬도 `revision` 이라 이미
105
+ * **`revision` 경로는 그대로 둔다.** 그때는 거르는 축이 `revision` 이고 정렬도 `revision` 이라 이미
79
106
  * 맞아 있다(`ix_twin_event_0`). 축이 어긋난 경로만 고친다 — 맞는 자리를 건드리면 그쪽이 어긋난다.
80
107
  *
81
108
  * 순서의 뜻이 **완전히 같지는 않다**(측정해서 고친 문장): `revision` 순서와 `eventTime` 순서가
@@ -83,16 +110,77 @@ export class TwinJournalQuery {
83
110
  * 이 목록은 사람이 이력을 훑는 화면이고 **시각순으로 보는 것이 그 화면의 뜻**이므로, 그 어긋남은
84
111
  * 결함이 아니라 오히려 맞는 순서다. 동시각의 순서를 위해 `revision` 을 둘째 축으로 남긴다.
85
112
  */
86
- const order: any = untilTime != null ? { eventTime: 'DESC', revision: 'DESC' } : { revision: 'DESC' }
87
- return getRepository(TwinEvent).find({ where, order, take: limit ?? 200 })
113
+ /*
114
+ * ── 아무 필터도 없을 때도 시각순으로 낸다 (2026-08-25 실측) ────────────────
115
+ * 이 갈래가 `revision DESC` 였다. 공정 타임라인은 필터 없이 상한만 걸고 부르므로
116
+ * (`loadTwinEvents(id, { eventType: 'task.status', limit })`) 그 갈래로 떨어졌다.
117
+ *
118
+ * `revision` 은 **적힌 순서**다. 연결된 시스템을 되읽는 트윈에서는 그 순서가 일어난 순서와 다르다 —
119
+ * 커넥터가 원본의 갱신 순서를 걷기 때문에, 원본에서 나중에 손댄 행이 4월 사실을 담고 있으면 그
120
+ * 행이 큰 revision 을 받는다. 승화푸드 실측:
121
+ *
122
+ * 작업 축에 있는 가장 늦은 사실 2026-08-23 03:20
123
+ * revision 이 가장 큰 작업 사실 2026-04-15 10:49 ← 상한이 잡던 값
124
+ *
125
+ * 상한 안에 8월 두 달치가 들어오지 않았고, 화면은 4월 사실을 최근 것으로 그렸다.
126
+ *
127
+ * 아래 목록 계약(`twinEventList`)은 이미 시각순을 기본으로 골라 두었다(§`sortings`). 이 함수만
128
+ * 남아 있었다. 성능도 이유가 되지 않는다 — 시각 정렬이 인덱스를 순서대로 타는 것은 아래 실측이
129
+ * 말한다.
130
+ *
131
+ * `revision` 범위로 거를 때는 `revision DESC` 를 그대로 쓴다. 거르는 축과 정렬 축을 맞추는 규칙이고,
132
+ * 그때는 부르는 쪽이 리비전으로 말하고 있으므로 리비전 순서가 그 물음의 답이다.
133
+ */
134
+ const order: any = journalOrder({ untilTime, fromTime, fromRevision, toRevision })
135
+
136
+ /*
137
+ * ── `find` 이 아니라 질의 빌더로 묻는다 (2026-08-25 실측) ───────────────────
138
+ *
139
+ * `find({ where: { domain: { id } }, order, take })` 는 관계를 가진 엔티티에서 **두 단계 질의**가
140
+ * 된다. 실제로 나간 SQL 이 이렇다:
141
+ *
142
+ * SELECT DISTINCT "distinctAlias"."TwinEvent_id", …
143
+ * FROM (SELECT … 모든 컬럼 … FROM twin_events WHERE …) "distinctAlias"
144
+ * ORDER BY … LIMIT ?
145
+ *
146
+ * **안쪽에 상한이 없다.** 조건에 맞는 행을 전부 만들어 놓고 밖에서 자른다. 승화푸드의 작업 사실은
147
+ * 438,913건이고 `payload` 가 함께 실리므로, 200건을 얻는 데 그 전부를 읽었다.
148
+ *
149
+ * 실측: 같은 조회를 손으로 쓰면 0.024초, 이 함수를 통하면 12초였다. 인덱스를 새로 얹은 뒤에도
150
+ * 그대로였다 — 인덱스는 안쪽 질의를 도와줄 뿐이고, 문제는 안쪽이 전부를 만든다는 것이었다.
151
+ *
152
+ * 관계를 **컬럼으로** 물으면 감싸지 않는다. 이 파일의 다른 자리(`timeRange` · 최종 리비전)가 이미
153
+ * 그 방식이다. `getMany` 는 조인이 없으면 `DISTINCT` 를 붙이지 않는다.
154
+ */
155
+ const qb = getRepository(TwinEvent)
156
+ .createQueryBuilder('e')
157
+ .where('e.domain = :domainId', { domainId: context.state.domain.id })
158
+ .andWhere('e.instanceId = :instanceId', { instanceId })
159
+ if (eventType) qb.andWhere('e.eventType = :eventType', { eventType })
160
+ if (where.eventTime !== undefined) {
161
+ /* 조건은 위에서 이미 정했다 — 같은 판단을 두 벌로 적지 않는다. */
162
+ if (untilTime != null && fromTime != null) {
163
+ qb.andWhere('e.eventTime BETWEEN :fromTime AND :untilTime', { fromTime: new Date(fromTime), untilTime: new Date(untilTime) })
164
+ } else if (untilTime != null) {
165
+ qb.andWhere('e.eventTime <= :untilTime', { untilTime: new Date(untilTime) })
166
+ } else {
167
+ qb.andWhere('e.eventTime >= :fromTime', { fromTime: new Date(fromTime as string) })
168
+ }
169
+ } else if (where.revision !== undefined) {
170
+ if (fromRevision != null && toRevision != null) qb.andWhere('e.revision BETWEEN :from AND :to', { from: fromRevision, to: toRevision })
171
+ else if (fromRevision != null) qb.andWhere('e.revision >= :from', { from: fromRevision })
172
+ else qb.andWhere('e.revision <= :to', { to: toRevision })
173
+ }
174
+ for (const [column, direction] of Object.entries(order)) qb.addOrderBy(`e.${column}`, direction as 'DESC')
175
+ return qb.limit(limit ?? 200).getMany()
88
176
  }
89
177
 
90
178
  /**
91
- * 저널 목록 — things-factory 표준 목록 계약(`ListParam` → `{ items, total }`).
179
+ * 지난 기록 목록 — things-factory 표준 목록 계약(`ListParam` → `{ items, total }`).
92
180
  *
93
181
  * ── 왜 `twinEvents` 와 따로 두는가 ─────────────────────────────────────────
94
182
  * `twinEvents` 는 배열만 돌려준다. 화면은 자기가 받은 게 전부인지 잘린 건지 알 수 없었고, 그래서
95
- * 리스트가 **조용히 잘린 채 "이게 전부" 처럼** 보였다(원장 limit 120 이 대표적). 총건수를 함께
183
+ * 리스트가 **알리지 않고 잘린 채 "이게 전부" 처럼** 보였다(원장 limit 120 이 대표적). 총건수를 함께
96
184
  * 주면 "48 / 12,904" 라고 말할 수 있고, 사용자는 좁혀야 한다는 사실을 안다.
97
185
  * 기존 호출자를 깨지 않으려고 `twinEvents` 는 그대로 두고 목록 계약을 새로 연다.
98
186
  *
@@ -120,7 +208,7 @@ export class TwinJournalQuery {
120
208
  const { domain } = context.state
121
209
  if (!instanceId && !spaceId) throw new Error('either instanceId or spaceId is required')
122
210
  /* 테넌트 격리 — twinKpi 와 같은 규약. 남의 트윈은 빈 결과가 아니라 명시 실패로 알린다
123
- * (조용한 빈 목록은 "권한 없음" 과 "데이터 없음" 을 구분할 수 없게 만든다). */
211
+ * (들어온 것이 없는 빈 목록은 "권한 없음" 과 "데이터 없음" 을 구분할 수 없게 만든다). */
124
212
  if (instanceId && !(await TwinEngine.ownsRegistered(domain.id, instanceId))) {
125
213
  throw new Error(`twin instance not found in this tenant: ${instanceId}`)
126
214
  }
@@ -133,7 +221,16 @@ export class TwinJournalQuery {
133
221
  const limit = Math.min(params.pagination?.limit ?? 100, 500)
134
222
  const effective: ListParam = {
135
223
  ...params,
136
- pagination: { page: params.pagination?.page ?? 1, limit },
224
+ /*
225
+ * ── 이어 읽기 지점을 **잃지 않는다** (2026-08-28 실측) ─────────────────────
226
+ *
227
+ * 여기서 페이지 값을 다시 만들면서 `after` 를 빼고 있었다. 그래서 응답은 다음 지점(`nextCursor`)을
228
+ * 내주는데 다음 요청의 그 값은 아무 데도 쓰이지 않았고, 「더 보기」가 **1페이지를 다시 냈다.**
229
+ * 실측: 같은 품목의 30건을 세 번 이어 읽었더니 90건 중 60건이 되풀었다(같은 id).
230
+ *
231
+ * 오류가 나지 않는 결함이라 화면에서는 「이력이 30건뿐」이나 「같은 사건이 계속 나온다」로 보인다.
232
+ */
233
+ pagination: { page: params.pagination?.page ?? 1, limit, after: params.pagination?.after },
137
234
  /* 기본 정렬은 시각 내림차순 — 여러 트윈을 합칠 때 공통 축은 revision(트윈별 카운터)이 아니라
138
235
  * **시각**이다. revision 으로 섞으면 서로 다른 트윈의 무관한 카운터가 뒤엉킨다. */
139
236
  sortings: params.sortings?.length ? params.sortings : [{ name: 'eventTime', desc: true }],
@@ -148,6 +245,70 @@ export class TwinJournalQuery {
148
245
  filters: (params.filters ?? []).map(f => (f.name === 'eventTime' ? { ...f, value: toDateValue(f.value) } : f))
149
246
  }
150
247
 
248
+ /*
249
+ * ── 대상 하나의 이력은 **파생 테이블**이 답한다 (2026-08-28 실측) ───────────
250
+ *
251
+ * 360 화면의 이력이 이 문으로 온다. 물음은 「이 품목(오더·자리·설비)의 사건을 최신순으로 30건」
252
+ * 인데, 저널에는 그 모양의 목차가 없다: 축으로 걸러도 시각으로 정렬하는 순간 목차를 못 쓰고,
253
+ * 품목과 로트가 한 문자열에 붙어 있어 품목으로는 아예 찾지 못한다. 실측 **63.998초**였다.
254
+ *
255
+ * 그 물음의 모양대로 만든 테이블이 이미 있다(`twin_subject_events`). 같은 질의가 **0.078초**다.
256
+ * 화면을 고치지 않고 여기서 답을 가져오는 곳만 바꾼다 — 계약도 커서도 그대로다.
257
+ *
258
+ * 맞지 않는 물음(여러 트윈·자유 검색·종류 필터·다른 정렬)이거나 파생 테이블이 그 구간을 말할 수
259
+ * 없으면 `null` 이 오고, 그때는 아래 저널 경로로 간다. 느리지만 온전하다.
260
+ */
261
+ if (instanceIds.length === 1) {
262
+ const fast = await readModelPage(domain.id, instanceIds[0], effective, limit)
263
+ if (fast) return fast
264
+ }
265
+
266
+ /*
267
+ * ── **여러 종류를 시각순으로 물으면 나눠 묻는다** (2026-08-28 실측) ──────────
268
+ *
269
+ * 에너지 원장이 「이 여덟 종류를 시각 내림차순으로」를 묻는다. 그 모양에서 sqlite 는 종류 색인을
270
+ * 버리고 시각 색인으로 337만 행을 걸으며 행마다 종류를 확인한다 — **37.181초, 결과 0건**.
271
+ * 종류를 정확일치로 하나씩 물으면 색인이 시각 순서까지 주므로 **각 0.003초**다.
272
+ *
273
+ * 새 색인도 새 테이블도 필요 없다. 있는 색인(`ix_twin_event_7`)을 쓰게 하는 것이 전부다.
274
+ * 건수 세기는 이미 빠르다(정렬을 지우므로 같은 색인을 탄다) — 목록만 이 갈래를 탄다.
275
+ */
276
+ const tiebreak: 'revision' | 'id' = instanceIds.length === 1 ? 'revision' : 'id'
277
+ const split = splitShape(effective, effective.sortings ?? [])
278
+ if (split) {
279
+ const page = await eventTypeSplitPage(domain.id, instanceIds, split, limit, tiebreak, effective.pagination?.after)
280
+ if (page) {
281
+ /*
282
+ * 건수는 일반 경로와 같은 방식으로 센다 — 상한까지 키만 읽는다. 이 조건은 정렬이 없어 종류
283
+ * 색인을 그대로 타므로 값이 싸다(실측 0.003초).
284
+ */
285
+ const probe = getQueryBuilderFromListParams({
286
+ repository: getRepository(TwinEvent),
287
+ params: { ...effective, pagination: { page: 1, limit: COUNT_CAP + 1 } },
288
+ domain,
289
+ searchables: [],
290
+ sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],
291
+ defaultLimit: COUNT_CAP + 1,
292
+ maxLimit: COUNT_CAP + 1
293
+ })
294
+ probe.andWhere(`${probe.alias}.instanceId IN (:...instanceIds)`, { instanceIds })
295
+ const counted = await probe
296
+ .orderBy()
297
+ .select(`${probe.alias}.id`)
298
+ .getRawMany()
299
+ .catch(() => null)
300
+ const capped = !counted || counted.length > COUNT_CAP
301
+ return {
302
+ items: page.items,
303
+ total: capped ? undefined : counted!.length,
304
+ capped,
305
+ countCap: COUNT_CAP,
306
+ nextCursor: page.nextCursor,
307
+ oldestRevision: undefined
308
+ }
309
+ }
310
+ }
311
+
151
312
  const qb = getQueryBuilderFromListParams({
152
313
  repository: getRepository(TwinEvent),
153
314
  params: effective,
@@ -157,7 +318,22 @@ export class TwinJournalQuery {
157
318
  /* 정렬은 인덱스가 받쳐 주는 축으로만 — 큰 저널에서 임의 컬럼 정렬은 전체 정렬 스캔이다. */
158
319
  sortables: ['eventTime', 'revision', 'eventType', 'bizStep'],
159
320
  defaultLimit: 100,
160
- maxLimit: 500
321
+ maxLimit: 500,
322
+ /*
323
+ * ── 동시각을 가리는 축을 **두 경로가 같게** 쓴다 (2026-08-28 실측) ─────────
324
+ *
325
+ * 커서는 「이 행 다음부터」이므로 시각이 같은 행들 사이에서 무엇이 앞인지 정해야 한다. 기본값은
326
+ * 기본키(`id`)인데, 파생 테이블 경로는 `revision` 으로 가린다(그 테이블에 `id` 가 없다).
327
+ *
328
+ * 두 경로가 한 목록을 나눠 답하면 그 축이 달라지고, 시각이 같은 행이 많은 자리에서 **본 행이
329
+ * 또 나온다.** 실측: 재고 사건 2,665건이 같은 시각을 가진 트윈에서 30건씩 네 번 이어 읽어
330
+ * 120건 중 15건이 되풀었다.
331
+ *
332
+ * 그래서 트윈 하나를 볼 때는 `revision` 으로 가린다 — 그 트윈 안에서 유일하고 빈 값이 없다.
333
+ * 여러 트윈을 합쳐 보는 목록에서는 리비전이 트윈마다 따로 세는 번호라 유일하지 않으므로
334
+ * 기본키를 쓴다.
335
+ */
336
+ cursorTiebreak: tiebreak
161
337
  })
162
338
  /* 대상 범위는 호출자가 필터로 빼먹을 수 있는 값이 아니다 — 계약상 필수라 여기서 강제한다. */
163
339
  qb.andWhere(`${qb.alias}.instanceId IN (:...instanceIds)`, { instanceIds })
@@ -170,7 +346,7 @@ export class TwinJournalQuery {
170
346
  *
171
347
  * 상한을 두면 같은 답이 **0.04초**다(130배). 상한을 넘으면 정확한 수를 모르는 것이 사실이므로
172
348
  * 목록 규약의 「서버 상한에 부딪힘(total 모름)」 갈래로 낸다 — 화면은 「처음 N 건만」이라고 말하고
173
- * 좁히기를 권한다. **조용히 자르지 않는다**: 상한을 넘었다는 사실을 `capped` 로 함께 낸다.
349
+ * 좁히기를 권한다. **알리지 않고 자르지 않는다**: 상한을 넘었다는 사실을 `capped` 로 함께 낸다.
174
350
  *
175
351
  * 왜 하위 질의로 세지 않나: 5개 드라이버에서 같이 도는 형태여야 한다(이 저장소의 절대 규칙).
176
352
  * 그래서 **키 컬럼만 상한까지 읽어 세는** 방식을 쓴다 — 색인만 읽으므로 행 본문을 만들지 않는다.
@@ -188,20 +364,48 @@ export class TwinJournalQuery {
188
364
  const total = capped ? undefined : counted.length
189
365
 
190
366
  /* 다음 커서는 **이번 페이지의 마지막 행**에서 만든다. 페이지가 상한보다 짧으면 뒤가 없다. */
191
- const axes = cursorSortings(effective.sortings, 'id')
367
+ const axes = cursorSortings(effective.sortings, tiebreak)
192
368
  const nextCursor = items.length === limit ? buildNextCursor(items[items.length - 1], axes) : undefined
193
369
 
194
- return { items, total, capped, countCap: COUNT_CAP, nextCursor }
370
+ /*
371
+ * ── **앞부분이 지워졌는지 함께 낸다** (2026-08-27) ─────────────────────────
372
+ *
373
+ * 보관 기간 정리가 오래된 사건을 지운다(기본 7일). 그러면 이 목록을 아무리 내려가도 어느 지점에서
374
+ * 기록이 끝나는데, 화면이 그것을 말하지 않으면 사람은 「이 트윈은 그때 시작했다」로 읽는다.
375
+ * 없었던 것과 지워진 것은 다른 사실이다.
376
+ *
377
+ * 저장하지 않고 그때그때 구한다 — 저장한 값은 실제 데이터와 어긋날 수 있고, 어긋나면 어느 쪽이
378
+ * 사실인지 알 수 없다. 색인 `(domain, instanceId, revision)` 의 첫 행 하나라서 값이 싸다.
379
+ *
380
+ * 트윈 하나를 물었을 때만 낸다. 여러 트윈을 합쳐 보는 목록에서는 리비전이 트윈마다 따로 세는
381
+ * 번호라 하나의 답이 없다.
382
+ */
383
+ let oldestRevision: number | undefined
384
+ if (instanceIds.length === 1) {
385
+ const first = await getRepository(TwinEvent)
386
+ .createQueryBuilder('e')
387
+ .select('e.revision', 'revision')
388
+ .where('e.domain = :domainId', { domainId: domain.id })
389
+ .andWhere('e.instanceId = :instanceId', { instanceId: instanceIds[0] })
390
+ .orderBy('e.revision', 'ASC')
391
+ .limit(1)
392
+ .getRawOne<{ revision: unknown }>()
393
+ .catch(() => null)
394
+ const at = Number(first?.revision)
395
+ if (Number.isFinite(at) && at > 0) oldestRevision = at
396
+ }
397
+
398
+ return { items, total, capped, countCap: COUNT_CAP, nextCursor, oldestRevision }
195
399
  }
196
400
 
197
401
  /**
198
- * 업무 KPI — 시간창 처리량·소요시간·자원 점유. 저널을 **접어서** 만든다(새 계측을 주입하지 않는다).
402
+ * 업무 KPI — 시간창 처리량·소요시간·자원 점유. 저널을 **계산해서** 만든다(새 계측을 주입하지 않는다).
199
403
  *
200
404
  * `twinMetrics`(초당 이벤트 수 = 인프라 부하)와 **다른 것**이다: 이건 "지난 한 시간에 몇 건 처리했고
201
405
  * 한 건에 얼마나 걸렸나" 다. 이름이 비슷해 헷갈리기 쉬우므로 설명에 명시한다.
202
406
  *
203
407
  * 완료된 작업의 **시작 이벤트가 창 앞에 있을 수 있어** 조회 구간을 창보다 앞으로 넓힌다(lookback).
204
- * 그래도 못 찾은 것은 결과의 `unpaired` 로 드러난다 — 평균에서 조용히 빠지면 지표가 거짓이 된다.
408
+ * 그래도 못 찾은 것은 결과의 `unpaired` 로 드러난다 — 평균에서 오류 없이 빠지면 지표가 거짓이 된다.
205
409
  */
206
410
  @Query(returns => ScalarObject, {
207
411
  description:
@@ -233,7 +437,7 @@ export class TwinJournalQuery {
233
437
  @Arg('baselineStride', { nullable: true }) baselineStride?: string
234
438
  ): Promise<any> {
235
439
  /* 테넌트 격리 — 트윈 지정이면 소속을 확인한다. 공간 지정이면 대상 해소가 도메인 목록에서
236
- * 이뤄지므로(computeTwinKpi) 남의 트윈이 섞이지 않는다. 조용한 빈 결과가 아니라 명시 실패. */
440
+ * 이뤄지므로(computeTwinKpi) 남의 트윈이 섞이지 않는다. 들어온 것이 없는 빈 결과가 아니라 명시 실패. */
237
441
  if (instanceId && !(await TwinEngine.ownsRegistered(context.state.domain.id, instanceId))) {
238
442
  throw new Error(`twin instance not found in this tenant: ${instanceId}`)
239
443
  }
@@ -284,7 +488,7 @@ export class TwinJournalQuery {
284
488
  ): Promise<any> {
285
489
  const state = await TwinEngine.recover(context.state.domain.id, instanceId, untilRevision ?? undefined, untilTime ?? undefined)
286
490
  const picked = pickAxes(state, axes)
287
- /* 오타를 조용히 넘기지 않는다 — 화면이 빈 채로 뜨는 이유를 아무도 모르게 되는 것이 가장 나쁘다. */
491
+ /* 오타를 알리지 않고 넘기지 않는다 — 화면이 빈 채로 뜨는 이유를 아무도 모르게 되는 것이 가장 나쁘다. */
288
492
  if (picked.unknown.length) twinWarn(`[twin] twinReplay: unknown axes ignored for "${instanceId}": ${picked.unknown.join(', ')}`)
289
493
  if (!attentionLimit || !picked.state?.attentions) return picked.state
290
494
  const d = digestAttentions(picked.state.attentions, attentionLimit)
@@ -317,7 +521,7 @@ export class TwinJournalQuery {
317
521
 
318
522
  /*
319
523
  * 기준 주를 안 주면 **이번 주 월요일**을 쓴다. 그 주에 공휴일이 끼면 상한이 조금 낮게 나오는데,
320
- * 그것은 조용한 오류가 아니라 **이번 주의 실제 가용 시간**이라 답으로서 정직하다. 평상시 상한을
524
+ * 그것은 들어온 것이 없는 오류가 아니라 **이번 주의 실제 가용 시간**이라 답으로서 정직하다. 평상시 상한을
321
525
  * 보려는 호출자는 평상주를 지정한다.
322
526
  */
323
527
  const base = sampleWeekStart ? Date.parse(sampleWeekStart) : Date.now()
@@ -48,7 +48,7 @@ export class TwinLifecycleMutation {
48
48
  // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.
49
49
  if (model) {
50
50
  /* model 를 함께 주는 것은 프로비저닝이다 — 그러면 종류도 말해야 한다.
51
- 예전에는 `kind ?? 'wms'` 로 메웠는데, 종류를 빠뜨린 야드/생산 트윈이 **창고로 조용히 기동**했다. */
51
+ 예전에는 `kind ?? 'wms'` 로 메웠는데, 종류를 빠뜨린 야드/생산 트윈이 **창고로 알리지 않고 기동**했다. */
52
52
  if (!kind) throw new Error('kind is required when model is given — it decides which domain kernel runs this twin')
53
53
  TwinEngine.start(instanceId, domainId, kind, await TwinEngine.withSpaceTimeBase(model as TwinModelDef, domainId))
54
54
  }
@@ -74,7 +74,7 @@ export class TwinLifecycleMutation {
74
74
  *
75
75
  * ── 지금 도는 것에는 손대지 않는다 ─────────────────────────────────────────
76
76
  * 이 값은 **다음 기동**의 규칙이다. 돌고 있는 런타임의 구동 방식을 바꾸려면 정지·기동이 필요하고,
77
- * 그 판단은 사람의 것이다(도는 트윈을 조용히 갈아 끼우면 그 사이의 사실이 어디로 갔는지 알 수 없다).
77
+ * 그 판단은 사람의 것이다(도는 트윈을 알리지 않고 갈아 끼우면 그 사이의 사실이 어디로 갔는지 알 수 없다).
78
78
  * 그래서 결과에 「지금 도는 것과 다르다」는 사실을 함께 낸다.
79
79
  */
80
80
  @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
@@ -88,7 +88,7 @@ export class TwinLifecycleMutation {
88
88
  @Ctx() context: ResolverContext
89
89
  ): Promise<any> {
90
90
  const domainId = context.state.domain.id
91
- /* 값은 **엄격히** 읽는다 — 모르는 말을 기본값으로 메우면 그 트윈의 이력이 남을지가 조용히 결정된다. */
91
+ /* 값은 **엄격히** 읽는다 — 모르는 말을 기본값으로 메우면 그 트윈의 이력이 남을지가 알리지 않고 결정된다. */
92
92
  const policy = readRestartPolicy(restartPolicy, `twin "${instanceId}"`)
93
93
  const repo = getRepository(TwinInstance)
94
94
  const row = await repo.findOne({ where: { domain: { id: domainId }, instanceId } })
@@ -103,6 +103,60 @@ export class TwinLifecycleMutation {
103
103
  return { instanceId, restartPolicy: policy, previous: before, changed: true, appliesAtNextStart: running }
104
104
  }
105
105
 
106
+ /**
107
+ * 트윈의 **이름을 다시 붙인다** (2026-08-26).
108
+ *
109
+ * ── 왜 이 문이 필요한가 ────────────────────────────────────────────────────
110
+ * 이름은 트윈을 처음 세울 때 한 번 정해지고(`provision`), 다시 읽기는 그것을 건드리지 않는다. 그
111
+ * 자체는 안전하다 — 사람이 붙인 이름을 연결이 덮어쓰면 안 된다. 그런데 그래서 **첫날의 이름에
112
+ * 갇힌다.**
113
+ *
114
+ * 실제로 그런 일이 났다: 태양광 발전소를 붙일 때 커넥터가 아는 이름이 연결된 시스템의 코드
115
+ * (`ES202307202202`)뿐이었다. 뒤에 사람이 부르는 이름(`호현에너지`)이 어디 있는지 알아내 커넥터를
116
+ * 고쳤는데, 트윈 이름은 그대로였다. **연결된 시스템을 따라가는 트윈에서는 이름이 나중에 좋아진다** —
117
+ * 커넥터가 나아질 때마다 이 일이 난다.
118
+ *
119
+ * 그때 남은 유일한 길이 트윈을 지우고 다시 세우는 것이었다. 그러면 지난 기록이 사라진다 — **이름
120
+ * 하나 때문에 사실을 버리는 선택**이 된다. 그래서 이름만 바꾸는 문을 낸다.
121
+ *
122
+ * ── 연결이 따라오게 하지 않는다 ────────────────────────────────────────────
123
+ * 다시 읽기가 이름을 따라가게 하면 사람이 붙인 이름을 덮어쓴다. 「사람이 손대지 않았으면 따라간다」가
124
+ * 옳아 보이지만, 그러려면 「손댔다」는 표시를 새로 만들어야 하고 그것은 이름 하나 때문에 지기에 무거운
125
+ * 빚이다. 사람이 바꾸는 길만 낸다.
126
+ *
127
+ * ── 빈 이름은 받지 않는다 ──────────────────────────────────────────────────
128
+ * 이름을 지우는 것과 바꾸는 것은 다른 일이다. 빈 값을 받으면 그 트윈은 목록에서 식별자만 남는데,
129
+ * 그것을 「이름을 바꿨다」로 보고하면 거짓이다. 지우고 싶으면 그 뜻의 문이 따로 있어야 한다.
130
+ */
131
+ @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
132
+ @Mutation(returns => ScalarObject, {
133
+ description:
134
+ 'Rename a twin instance. The name is set when the twin is first provisioned and a re-read never touches it, ' +
135
+ 'so a twin stays with the name its connector knew on day one — often a code from the connected system. ' +
136
+ 'This changes the name only; nothing about the twin data, its journal or its runtime is affected. ' +
137
+ 'An empty name is refused: clearing a name and changing it are different acts.'
138
+ })
139
+ async renameTwinInstance(
140
+ @Arg('instanceId') instanceId: string,
141
+ @Arg('name') name: string,
142
+ @Ctx() context: ResolverContext
143
+ ): Promise<any> {
144
+ const domainId = context.state.domain.id
145
+ const next = String(name ?? '').trim()
146
+ if (!next) throw new Error(`a twin name cannot be empty — "${instanceId}" was left as it was`)
147
+
148
+ const repo = getRepository(TwinInstance)
149
+ const row = await repo.findOne({ where: { domain: { id: domainId }, instanceId } })
150
+ if (!row) throw new Error(`unknown twin "${instanceId}" in this domain — nothing was changed`)
151
+
152
+ const before = row.name ?? ''
153
+ if (before === next) return { instanceId, name: next, changed: false }
154
+
155
+ await repo.update({ id: row.id }, { name: next, updater: context.state.user })
156
+ /* 도는 런타임은 이름을 쓰지 않는다(식별자로 돈다) — 그래서 정지·기동이 필요하지 않다. */
157
+ return { instanceId, name: next, previous: before || null, changed: true }
158
+ }
159
+
106
160
  /** 삭제 — 정지 + 레지스트리 삭제 + 저널 purge(domain 스코프). */
107
161
  @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
108
162
  @Mutation(returns => Boolean, { description: 'Delete a twin instance: stop it, remove the registry entry and purge its event journal.' })
@@ -49,7 +49,7 @@ export class TwinMetricsQuery {
49
49
  * `twinMetrics` 는 **통과한** 이벤트의 초당 수를 답한다. 여기서 답하는 것은 **제시된 것 중 얼마가
50
50
  * 통과했나**이고, 통과하지 못한 것의 이유와 실제 모양까지 낸다.
51
51
  *
52
- * 왜 따로 있나: 거부된 레코드는 **저널에 없다**(거부의 뜻이 그것이다). 저널을 접어 나오는 것은 통과한
52
+ * 왜 따로 있나: 거부된 레코드는 **저널에 없다**(거부의 뜻이 그것이다). 저널을 계산해 나오는 것은 통과한
53
53
  * 쪽뿐이라, 거부의 추이는 세는 자리를 따로 두어야 나온다.
54
54
  *
55
55
  * 왜 도메인으로 자르나: 이것은 **테넌트의 데이터**다. 거부 표본에는 그 고객의 레코드 원문이 들어
@@ -66,6 +66,19 @@ export class TwinMetricsQuery {
66
66
  * 빈 배열로 두면 「거부된 것이 없다」로 읽힌다.
67
67
  */
68
68
  @Query(returns => ScalarObject, {
69
+ /*
70
+ * ── `null` 이 정답인 자리다 (2026-08-29 실측) ───────────────────────────────
71
+ * 이 조회는 세 경우에 `null` 을 낸다: 다른 도메인의 트윈 · 등록되지 않은 트윈 · 아직 돌지 않는
72
+ * 트윈. 설명문에도 그렇게 적혀 있었는데 **스키마가 그것을 금지하고 있었다.**
73
+ *
74
+ * 그래서 화면이 아직 돌지 않는 트윈을 열면 조회 전체가 이 오류로 죽었다.
75
+ *
76
+ * Cannot return null for non-nullable field Query.twinIngestHealth.
77
+ *
78
+ * 「없다」를 낼 수 있어야 화면이 「아직 잰 것이 없다」를 말한다. 빈 객체로 바꾸면 그것이
79
+ * 「재 봤는데 아무것도 없다」로 읽힌다 — 두 사실이 다르다.
80
+ */
81
+ nullable: true,
69
82
  description:
70
83
  'Answers one question: is this twin keeping in step with the floor? Returns a verdict — syncing | mapping-blocked | no-traffic | not-connected | not-applicable | unknown — together with the threshold it was judged by, so a screen can say why. `recent` is the last **closed** 10-minute window (null while the first window is still filling: a partially filled window under-counts, so it is not called "recent"), and `trend` carries up to 36 closed windows (6 hours) oldest-first with offered / accepted / rejected per window. Accepted and rejected are reported together because counting only rejections makes "nothing is arriving" and "everything is fine" both look like zero. `reasons` groups rejections by the reason the kernel gave, most frequent first, each with the last time that reason occurred — a reason whose time has stopped is one you already fixed. `samples` holds one rejected record per reason (the raw record, truncated with recordTruncated when long) because a reason alone does not tell you what the source actually sent. Counts that could not be shown are reported rather than dropped: sampleDropped, trendDropped, and reasonsCapped when distinct reasons hit their limit and the rest were folded into "(other reasons)". A twin that has never received anything reports null figures rather than zeros, since never-measured and measured-as-zero are different facts. This ledger lives in memory: it covers roughly the last 6 hours and resets when the process restarts. The verdict, trend and reasons are returned to anyone in the owning domain, but the raw records in `samples` are business data of that tenant, so they require the same privilege as changing the twin (twin/mutation, with domain owner and superuser granted); when the caller lacks it, `samples` comes back empty and `samplesWithheld` says how many were held back — an empty list alone would read as "nothing was rejected". `modelGap` answers a different question from the verdict: the verdict is about the feed (is anything arriving, does it pass), while modelGap is about the model (did we declare what the source knows). The ingest gate deliberately does not reject a record that references an id the master never declared — a source may legitimately report a new place before the structure sync catches up, and rejecting it would lose that measurement. The kernel absorbs it and marks it `origin: "observed"` instead, and modelGap counts those per axis (locations / equipment / persons) with a capped id list plus how many more were left out. It also carries the kernel\'s `unhandled` tally — event kinds the twin could not apply at all — and, for energy twins, how many state reports arrived from equipment the model does not know. `notCovered` names axes this cannot answer yet: meters carry no origin mark in the kernel, so a mistyped meterId silently becomes a new meter whose kW is accumulated into the site demand. Null while the twin is not running: there is nothing to compare against, and never-compared is not the same as none. Null if the instance is not in the current domain.'
71
84
  })
@@ -50,7 +50,7 @@ export async function axisSeenInJournal(
50
50
  /*
51
51
  * 접두사로 찾는 축(물품) — 한 건만 있으면 된다. `Like` 를 쓰는 대신 **낱말 목록**을 쓰지 않는 이유는
52
52
  * EPCIS 사건 이름이 커널에서 자라기 때문이다(객체·집약·변환·거래…). 이름을 여기 베끼면 커널이
53
- * 하나 늘릴 때 이 자리가 조용히 낡는다.
53
+ * 하나 늘릴 때 이 자리가 알리지 않고 낡는다.
54
54
  */
55
55
  const { Like } = await import('typeorm')
56
56
  return anyJournalRow(repo, { ...where, eventType: Like(`${spec.prefix}%`) })
@@ -76,6 +76,31 @@ const EVENTS: Isa95Concept[] = [
76
76
 
77
77
  /* Part: 사건이 답하는 다섯 가지 — 무엇을·어디서·언제·왜, 그리고 어떤 거래로. */
78
78
  const FIELDS: Isa95Concept[] = [
79
+ {
80
+ /*
81
+ * **무엇을 한 것인가** — 표준 `action`: `ADD`(생겼다) · `OBSERVE`(보았다) · `DELETE`(없어졌다).
82
+ *
83
+ * 이 줄이 표에 없었다. 표준 필드이고 우리가 계약·유입·검증에서 다 쓰는데도 세지 않았습니다.
84
+ *
85
+ * ── 2026-08-25 그 뜻이 커졌다 ─────────────────────────────────────────────
86
+ * `DELETE` 를 낱개 번호가 있는 물품에만 적용하고 있었다. 그래서 **낱개 번호가 없는 현장에서는
87
+ * 재고를 지울 방법이 하나도 없었다** — 승화푸드는 모든 재고가 수량으로 오는 현장이고, 한 번 본
88
+ * 재고가 영원히 남았다(실제 1,704건인데 트윈이 10,038건).
89
+ *
90
+ * 이제 수량으로 온 재고도 없어질 수 있다. 자리를 함께 말하면 그 자리의 것만, 말하지 않으면
91
+ * 그 로트를 모든 자리에서 지운다 — 짐작이 아니라 진술의 범위를 읽는다.
92
+ *
93
+ * 화면은 아직 없다. 「이것이 없어졌다」를 사람에게 보이는 자리를 만들지 않았다.
94
+ */
95
+ std: 'action',
96
+ part: 'fields',
97
+ label: 'twin.epcis.action',
98
+ axis: null,
99
+ shownOn: ['items'],
100
+ structure: 'full',
101
+ behavior: 'full',
102
+ surface: 'none'
103
+ },
79
104
  {
80
105
  std: 'epcList / quantityList',
81
106
  part: 'fields',
@@ -143,11 +168,15 @@ const FIELDS: Isa95Concept[] = [
143
168
  note: 'twin.epcis.note.ilmd'
144
169
  },
145
170
  {
171
+ /*
172
+ * `shownOn` 은 「어디에 속하나」다 — 정정 선언은 물품의 사실이다. 화면에 그려지는지는 `surface` 가
173
+ * 따로 답한다(지금은 `none` 이고 `note` 가 그 사실을 말한다). 둘은 다른 물음이다(§`shownOn`).
174
+ */
175
+ shownOn: ['items'],
146
176
  std: 'errorDeclaration',
147
177
  part: 'fields',
148
178
  label: 'twin.epcis.errorDeclaration',
149
179
  axis: null,
150
- shownOn: ['items'],
151
180
  structure: 'full',
152
181
  behavior: 'partial',
153
182
  surface: 'none',
@@ -155,6 +184,8 @@ const FIELDS: Isa95Concept[] = [
155
184
  },
156
185
  /* 모양만 있고 아무도 채우지 않는 것들 — 구조만 있음. */
157
186
  {
187
+ /* 어디에 속하나 — 소유·보관의 이전은 그 물품의 사실이다. 화면에 그려지는지는 `surface` 가 따로 답한다(§`shownOn`). */
188
+ shownOn: ['items'],
158
189
  std: 'sourceList / destinationList',
159
190
  part: 'fields',
160
191
  label: 'twin.epcis.sourceDestination',
@@ -165,6 +196,8 @@ const FIELDS: Isa95Concept[] = [
165
196
  note: 'twin.epcis.note.sourceDestination'
166
197
  },
167
198
  {
199
+ /* 어디에 속하나 — 지속 처분은 그 물품에 붙는다. 화면에 그려지는지는 `surface` 가 따로 답한다(§`shownOn`). */
200
+ shownOn: ['items'],
168
201
  std: 'persistentDisposition',
169
202
  part: 'fields',
170
203
  label: 'twin.epcis.persistentDisposition',
@@ -188,6 +221,8 @@ const FIELDS: Isa95Concept[] = [
188
221
  * 그 구별을 표가 말하지 않으면 「센서를 못 받는 트윈」으로 읽힌다.
189
222
  */
190
223
  {
224
+ /* 어디에 속하나 — 표준에서 센서 관측은 **개체에 붙는다** — 우리가 그것을 자리로 옮긴 것이 다른 이야기다. 화면에 그려지는지는 `surface` 가 따로 답한다(§`shownOn`). */
225
+ shownOn: ['items'],
191
226
  std: 'sensorElementList',
192
227
  part: 'fields',
193
228
  label: 'twin.epcis.sensorElement',
@@ -36,7 +36,7 @@ const LOGICAL_NODES: Isa95Concept[] = [ // vocabulary-guard: allow — IEC 61850
36
36
  note: 'twin.iec61850.note.MMTR'
37
37
  },
38
38
  /*
39
- * 계측(유효전력·역률) — 표본이 수요 구간으로 접히고, 구간·피크·계약 대비가 그 위에 선다.
39
+ * 계측(유효전력·역률) — 표본이 수요 구간에 합쳐지고, 구간·피크·계약 대비가 그 위에 선다.
40
40
  * 이 프로파일에서 가장 두껍게 채운 자리다.
41
41
  */
42
42
  {