@things-factory/headless-twin 10.0.9 → 10.0.11

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 (354) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +2 -2
  2. package/dist-server/engine/canonical-ingest.js +24 -2
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/command-routing.js +1 -1
  5. package/dist-server/engine/command-routing.js.map +1 -1
  6. package/dist-server/engine/energy-topology.d.ts +86 -0
  7. package/dist-server/engine/energy-topology.js +144 -0
  8. package/dist-server/engine/energy-topology.js.map +1 -0
  9. package/dist-server/engine/index.d.ts +8 -3
  10. package/dist-server/engine/index.js +10 -3
  11. package/dist-server/engine/index.js.map +1 -1
  12. package/dist-server/engine/kpi-baseline.d.ts +1 -1
  13. package/dist-server/engine/kpi-baseline.js.map +1 -1
  14. package/dist-server/engine/kpi-fold.d.ts +137 -1
  15. package/dist-server/engine/kpi-fold.js +225 -0
  16. package/dist-server/engine/kpi-fold.js.map +1 -1
  17. package/dist-server/engine/kpi-query.d.ts +1 -1
  18. package/dist-server/engine/kpi-query.js +212 -18
  19. package/dist-server/engine/kpi-query.js.map +1 -1
  20. package/dist-server/engine/kpi-target.d.ts +17 -0
  21. package/dist-server/engine/kpi-target.js +24 -2
  22. package/dist-server/engine/kpi-target.js.map +1 -1
  23. package/dist-server/engine/live-feed-registry.d.ts +25 -0
  24. package/dist-server/engine/live-feed-registry.js +51 -0
  25. package/dist-server/engine/live-feed-registry.js.map +1 -0
  26. package/dist-server/engine/load-meter.d.ts +181 -0
  27. package/dist-server/engine/load-meter.js +267 -0
  28. package/dist-server/engine/load-meter.js.map +1 -0
  29. package/dist-server/engine/local-declarations.d.ts +262 -0
  30. package/dist-server/engine/local-declarations.js +528 -0
  31. package/dist-server/engine/local-declarations.js.map +1 -0
  32. package/dist-server/engine/model-basis.d.ts +39 -6
  33. package/dist-server/engine/model-basis.js +69 -9
  34. package/dist-server/engine/model-basis.js.map +1 -1
  35. package/dist-server/engine/{board-vocabulary.d.ts → model-vocabulary.d.ts} +4 -4
  36. package/dist-server/engine/{board-vocabulary.js → model-vocabulary.js} +37 -19
  37. package/dist-server/engine/model-vocabulary.js.map +1 -0
  38. package/dist-server/engine/oee-accumulator.d.ts +28 -0
  39. package/dist-server/engine/oee-accumulator.js +26 -1
  40. package/dist-server/engine/oee-accumulator.js.map +1 -1
  41. package/dist-server/engine/operation-basis.d.ts +32 -0
  42. package/dist-server/engine/operation-basis.js +87 -0
  43. package/dist-server/engine/operation-basis.js.map +1 -0
  44. package/dist-server/engine/property-effects.d.ts +30 -0
  45. package/dist-server/engine/property-effects.js +192 -0
  46. package/dist-server/engine/property-effects.js.map +1 -0
  47. package/dist-server/engine/runtime-key.d.ts +1 -1
  48. package/dist-server/engine/runtime-key.js +1 -1
  49. package/dist-server/engine/runtime-key.js.map +1 -1
  50. package/dist-server/engine/spec-coverage.js +3 -3
  51. package/dist-server/engine/spec-coverage.js.map +1 -1
  52. package/dist-server/engine/state-axes.d.ts +7 -2
  53. package/dist-server/engine/state-axes.js +8 -3
  54. package/dist-server/engine/state-axes.js.map +1 -1
  55. package/dist-server/engine/structure-diff.js +1 -1
  56. package/dist-server/engine/structure-diff.js.map +1 -1
  57. package/dist-server/engine/travel-estimator.d.ts +9 -2
  58. package/dist-server/engine/travel-estimator.js +11 -6
  59. package/dist-server/engine/travel-estimator.js.map +1 -1
  60. package/dist-server/engine/twin-engine.d.ts +332 -47
  61. package/dist-server/engine/twin-engine.js +1314 -279
  62. package/dist-server/engine/twin-engine.js.map +1 -1
  63. package/dist-server/engine/warm-start.d.ts +38 -4
  64. package/dist-server/engine/warm-start.js +38 -9
  65. package/dist-server/engine/warm-start.js.map +1 -1
  66. package/dist-server/index.d.ts +1 -0
  67. package/dist-server/index.js +17 -11
  68. package/dist-server/index.js.map +1 -1
  69. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.d.ts +5 -0
  70. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +48 -0
  71. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -0
  72. package/dist-server/migrations/index.d.ts +2 -0
  73. package/dist-server/migrations/index.js +10 -0
  74. package/dist-server/migrations/index.js.map +1 -0
  75. package/dist-server/service/index.d.ts +3 -2
  76. package/dist-server/service/index.js +25 -17
  77. package/dist-server/service/index.js.map +1 -1
  78. package/dist-server/service/reference/discovery-result.d.ts +1 -1
  79. package/dist-server/service/reference/discovery-result.js +2 -2
  80. package/dist-server/service/reference/discovery-result.js.map +1 -1
  81. package/dist-server/service/reference/ingest-space.d.ts +24 -1
  82. package/dist-server/service/reference/ingest-space.js +17 -1
  83. package/dist-server/service/reference/ingest-space.js.map +1 -1
  84. package/dist-server/service/reference/reference-live.d.ts +23 -0
  85. package/dist-server/service/reference/reference-live.js +121 -4
  86. package/dist-server/service/reference/reference-live.js.map +1 -1
  87. package/dist-server/service/reference/reference-master.d.ts +194 -13
  88. package/dist-server/service/reference/reference-master.js +159 -28
  89. package/dist-server/service/reference/reference-master.js.map +1 -1
  90. package/dist-server/service/reference/reference-resolver.d.ts +31 -1
  91. package/dist-server/service/reference/reference-resolver.js +223 -12
  92. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  93. package/dist-server/service/reference/template-registry.d.ts +19 -3
  94. package/dist-server/service/reference/template-registry.js.map +1 -1
  95. package/dist-server/service/twin-audit/command-audit.d.ts +37 -0
  96. package/dist-server/service/twin-audit/command-audit.js +53 -0
  97. package/dist-server/service/twin-audit/command-audit.js.map +1 -0
  98. package/dist-server/service/twin-audit/index.d.ts +4 -0
  99. package/dist-server/service/twin-audit/index.js +8 -0
  100. package/dist-server/service/twin-audit/index.js.map +1 -0
  101. package/dist-server/service/twin-audit/twin-audit-event.d.ts +24 -0
  102. package/dist-server/service/twin-audit/twin-audit-event.js +141 -0
  103. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -0
  104. package/dist-server/service/twin-audit/twin-audit-query.d.ts +4 -0
  105. package/dist-server/service/twin-audit/twin-audit-query.js +76 -0
  106. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -0
  107. package/dist-server/service/twin-control/twin-control-mutation.js +21 -1
  108. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  109. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  110. package/dist-server/service/twin-event/twin-event-keys.js +1 -1
  111. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  112. package/dist-server/service/twin-event/twin-event.d.ts +14 -5
  113. package/dist-server/service/twin-event/twin-event.js +45 -13
  114. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  115. package/dist-server/service/twin-forecast/forecast-metrics.d.ts +13 -0
  116. package/dist-server/service/twin-forecast/forecast-metrics.js +57 -0
  117. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -0
  118. package/dist-server/service/twin-forecast/forecast-tuning.d.ts +12 -0
  119. package/dist-server/service/twin-forecast/forecast-tuning.js +44 -0
  120. package/dist-server/service/twin-forecast/forecast-tuning.js.map +1 -0
  121. package/dist-server/service/twin-forecast/gap-analytics.d.ts +28 -2
  122. package/dist-server/service/twin-forecast/gap-analytics.js +43 -17
  123. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  124. package/dist-server/service/twin-forecast/twin-forecast-query.js +217 -38
  125. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  126. package/dist-server/service/twin-instance/twin-instance.d.ts +38 -5
  127. package/dist-server/service/twin-instance/twin-instance.js +66 -20
  128. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  129. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -2
  130. package/dist-server/service/twin-journal/twin-journal-query.js +71 -9
  131. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  132. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +3 -5
  133. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +24 -27
  134. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  135. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  136. package/dist-server/service/twin-metrics/twin-metrics-query.js +62 -0
  137. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  138. package/dist-server/service/twin-model/epcis-coverage.d.ts +14 -0
  139. package/dist-server/service/twin-model/epcis-coverage.js +174 -0
  140. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -0
  141. package/dist-server/service/twin-model/iec61850-coverage.d.ts +14 -0
  142. package/dist-server/service/twin-model/iec61850-coverage.js +98 -0
  143. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -0
  144. package/dist-server/service/twin-model/index.d.ts +13 -0
  145. package/dist-server/service/twin-model/index.js +29 -0
  146. package/dist-server/service/twin-model/index.js.map +1 -0
  147. package/dist-server/service/twin-model/isa95-coverage.d.ts +48 -0
  148. package/dist-server/service/twin-model/isa95-coverage.js +155 -0
  149. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -0
  150. package/dist-server/service/twin-model/project-structure.d.ts +16 -0
  151. package/dist-server/service/twin-model/project-structure.js +173 -0
  152. package/dist-server/service/twin-model/project-structure.js.map +1 -0
  153. package/dist-server/service/twin-model/standard-coverage.d.ts +9 -0
  154. package/dist-server/service/twin-model/standard-coverage.js +37 -0
  155. package/dist-server/service/twin-model/standard-coverage.js.map +1 -0
  156. package/dist-server/service/twin-model/twin-equipment.d.ts +64 -0
  157. package/dist-server/service/twin-model/twin-equipment.js +134 -0
  158. package/dist-server/service/twin-model/twin-equipment.js.map +1 -0
  159. package/dist-server/service/twin-model/twin-lineage-query.d.ts +3 -0
  160. package/dist-server/service/twin-model/twin-lineage-query.js +206 -0
  161. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -0
  162. package/dist-server/service/twin-model/twin-location.d.ts +58 -0
  163. package/dist-server/service/twin-model/twin-location.js +130 -0
  164. package/dist-server/service/twin-model/twin-location.js.map +1 -0
  165. package/dist-server/service/twin-model/twin-model-item-query.d.ts +141 -0
  166. package/dist-server/service/twin-model/twin-model-item-query.js +852 -0
  167. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -0
  168. package/dist-server/service/twin-model/twin-model-mutation.d.ts +57 -0
  169. package/dist-server/service/twin-model/twin-model-mutation.js +490 -0
  170. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -0
  171. package/dist-server/service/twin-model/twin-model-query.d.ts +43 -0
  172. package/dist-server/service/twin-model/twin-model-query.js +551 -0
  173. package/dist-server/service/twin-model/twin-model-query.js.map +1 -0
  174. package/dist-server/service/twin-model/twin-model-tree-query.d.ts +3 -0
  175. package/dist-server/service/twin-model/twin-model-tree-query.js +158 -0
  176. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -0
  177. package/dist-server/service/twin-model/twin-operation.d.ts +61 -0
  178. package/dist-server/service/twin-model/twin-operation.js +137 -0
  179. package/dist-server/service/twin-model/twin-operation.js.map +1 -0
  180. package/dist-server/service/twin-space/move-space.d.ts +65 -0
  181. package/dist-server/service/twin-space/move-space.js +166 -0
  182. package/dist-server/service/twin-space/move-space.js.map +1 -0
  183. package/dist-server/service/twin-space/space-integrity.d.ts +97 -0
  184. package/dist-server/service/twin-space/space-integrity.js +182 -0
  185. package/dist-server/service/twin-space/space-integrity.js.map +1 -0
  186. package/dist-server/service/twin-space/twin-space-area.js +4 -4
  187. package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
  188. package/dist-server/service/twin-space/twin-space-representation.js +6 -6
  189. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  190. package/dist-server/service/twin-space/twin-space-resolver.d.ts +63 -1
  191. package/dist-server/service/twin-space/twin-space-resolver.js +356 -18
  192. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  193. package/dist-server/service/twin-space/twin-space.d.ts +17 -1
  194. package/dist-server/service/twin-space/twin-space.js +15 -5
  195. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  196. package/dist-server/service/twin-structure/twin-structure.d.ts +1 -1
  197. package/dist-server/service/twin-structure/twin-structure.js +8 -8
  198. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  199. package/dist-shared/axis-read.d.ts +12 -0
  200. package/dist-shared/axis-read.js +32 -0
  201. package/dist-shared/axis-read.js.map +1 -0
  202. package/{dist-server/engine → dist-shared}/entity-delta.d.ts +3 -0
  203. package/{dist-server/engine → dist-shared}/entity-delta.js +102 -2
  204. package/dist-shared/entity-delta.js.map +1 -0
  205. package/dist-shared/kpi-broadcast.js.map +1 -0
  206. package/dist-shared/twin-level.js.map +1 -0
  207. package/package.json +14 -12
  208. package/server/engine/canonical-ingest.ts +36 -4
  209. package/server/engine/command-routing.ts +1 -1
  210. package/server/engine/energy-topology.ts +189 -0
  211. package/server/engine/index.ts +10 -3
  212. package/server/engine/kpi-baseline.ts +1 -1
  213. package/server/engine/kpi-fold.ts +389 -1
  214. package/server/engine/kpi-query.ts +214 -19
  215. package/server/engine/kpi-target.ts +24 -2
  216. package/server/engine/live-feed-registry.ts +58 -0
  217. package/server/engine/load-meter.ts +384 -0
  218. package/server/engine/local-declarations.ts +700 -0
  219. package/server/engine/model-basis.ts +78 -10
  220. package/server/engine/{board-vocabulary.ts → model-vocabulary.ts} +36 -15
  221. package/server/engine/oee-accumulator.ts +34 -1
  222. package/server/engine/operation-basis.ts +100 -0
  223. package/server/engine/property-effects.ts +199 -0
  224. package/server/engine/runtime-key.ts +1 -1
  225. package/server/engine/spec-coverage.ts +3 -3
  226. package/server/engine/state-axes.ts +8 -3
  227. package/server/engine/structure-diff.ts +1 -1
  228. package/server/engine/travel-estimator.ts +11 -6
  229. package/server/engine/twin-engine.ts +1414 -274
  230. package/server/engine/warm-start.ts +83 -12
  231. package/server/index.ts +13 -10
  232. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +43 -0
  233. package/server/migrations/index.ts +7 -0
  234. package/server/service/index.ts +9 -1
  235. package/server/service/reference/discovery-result.ts +2 -2
  236. package/server/service/reference/ingest-space.ts +35 -1
  237. package/server/service/reference/reference-live.ts +119 -4
  238. package/server/service/reference/reference-master.ts +276 -35
  239. package/server/service/reference/reference-resolver.ts +232 -12
  240. package/server/service/reference/template-registry.ts +20 -5
  241. package/server/service/twin-audit/command-audit.ts +81 -0
  242. package/server/service/twin-audit/index.ts +5 -0
  243. package/server/service/twin-audit/twin-audit-event.ts +138 -0
  244. package/server/service/twin-audit/twin-audit-query.ts +72 -0
  245. package/server/service/twin-control/twin-control-mutation.ts +22 -1
  246. package/server/service/twin-event/twin-event-keys.ts +1 -1
  247. package/server/service/twin-event/twin-event.ts +59 -14
  248. package/server/service/twin-forecast/forecast-metrics.ts +60 -0
  249. package/server/service/twin-forecast/forecast-tuning.ts +40 -0
  250. package/server/service/twin-forecast/gap-analytics.ts +48 -9
  251. package/server/service/twin-forecast/twin-forecast-query.ts +208 -36
  252. package/server/service/twin-instance/twin-instance.ts +126 -25
  253. package/server/service/twin-journal/twin-journal-query.ts +74 -10
  254. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +23 -22
  255. package/server/service/twin-metrics/twin-metrics-query.ts +59 -2
  256. package/server/service/twin-model/epcis-coverage.ts +195 -0
  257. package/server/service/twin-model/iec61850-coverage.ts +117 -0
  258. package/server/service/twin-model/index.ts +25 -0
  259. package/server/service/twin-model/isa95-coverage.ts +190 -0
  260. package/server/service/twin-model/project-structure.ts +199 -0
  261. package/server/service/twin-model/standard-coverage.ts +35 -0
  262. package/server/service/twin-model/twin-equipment.ts +157 -0
  263. package/server/service/twin-model/twin-lineage-query.ts +193 -0
  264. package/server/service/twin-model/twin-location.ts +147 -0
  265. package/server/service/twin-model/twin-model-item-query.ts +848 -0
  266. package/server/service/twin-model/twin-model-mutation.ts +503 -0
  267. package/server/service/twin-model/twin-model-query.ts +537 -0
  268. package/server/service/twin-model/twin-model-tree-query.ts +180 -0
  269. package/server/service/twin-model/twin-operation.ts +157 -0
  270. package/server/service/twin-space/move-space.ts +262 -0
  271. package/server/service/twin-space/space-integrity.ts +293 -0
  272. package/server/service/twin-space/twin-space-area.ts +4 -4
  273. package/server/service/twin-space/twin-space-representation.ts +6 -6
  274. package/server/service/twin-space/twin-space-resolver.ts +355 -21
  275. package/server/service/twin-space/twin-space.ts +40 -6
  276. package/server/service/twin-structure/twin-structure.ts +10 -10
  277. package/shared/axis-read.ts +28 -0
  278. package/{server/engine → shared}/entity-delta.ts +96 -1
  279. package/test/adopt-structure-live.test.ts +133 -0
  280. package/test/attention-digest.test.ts +3 -3
  281. package/test/axis-read.test.ts +87 -0
  282. package/test/boot-resume.test.ts +205 -0
  283. package/test/canonical-ingest-vocabularies.test.ts +118 -0
  284. package/test/capability-mapping.test.ts +1 -1
  285. package/test/column-type-portability.test.ts +122 -0
  286. package/test/command-routing.test.ts +1 -1
  287. package/test/declaration-reaches-model.test.ts +178 -0
  288. package/test/discovery-result.test.ts +1 -1
  289. package/test/energy-topology.test.ts +113 -0
  290. package/test/entity-delta.test.ts +171 -1
  291. package/test/event-time-column.test.ts +67 -0
  292. package/test/forecast-metrics.test.ts +71 -0
  293. package/test/forecast-tuning.test.ts +42 -0
  294. package/test/gap-analytics.test.ts +34 -7
  295. package/test/ingest-bench.test.ts +6 -6
  296. package/test/ingest-running-guard.test.ts +136 -0
  297. package/test/ingest-space.test.ts +29 -1
  298. package/test/instance-cache-lifecycle.test.ts +110 -0
  299. package/test/kernel-kind-guard.test.ts +96 -0
  300. package/test/kpi-baseline-db.test.ts +3 -1
  301. package/test/kpi-baseline.test.ts +3 -3
  302. package/test/kpi-fold.test.ts +393 -2
  303. package/test/kpi-query-bench.test.ts +5 -3
  304. package/test/lineage-survives-restart.test.ts +203 -0
  305. package/test/live-feed-registry.test.ts +67 -0
  306. package/test/live-kernel-facts.test.ts +161 -0
  307. package/test/live-mirror-parity.test.ts +22 -15
  308. package/test/load-meter.test.ts +314 -0
  309. package/test/local-declarations.test.ts +554 -0
  310. package/test/master-to-twin.test.ts +121 -33
  311. package/test/model-basis.test.ts +56 -1
  312. package/test/{board-vocabulary.test.ts → model-vocabulary.test.ts} +25 -25
  313. package/test/move-space.test.ts +149 -0
  314. package/test/mutation-gate.test.ts +19 -5
  315. package/test/oee-accumulator.test.ts +26 -7
  316. package/test/operation-basis.test.ts +100 -0
  317. package/test/operations-capability-db.test.ts +165 -0
  318. package/test/project-structure-db.test.ts +226 -0
  319. package/test/projection-reaches-screen.test.ts +272 -0
  320. package/test/property-effects.test.ts +96 -0
  321. package/test/resync-origin-site.test.ts +77 -0
  322. package/test/scale-twin-bench.test.ts +5 -5
  323. package/test/snapshot-freshness.test.ts +60 -0
  324. package/test/space-integrity.test.ts +223 -0
  325. package/test/standard-coverage.test.ts +94 -0
  326. package/test/state-axes.test.ts +1 -1
  327. package/test/streamline-e2e.test.ts +14 -14
  328. package/test/structure-revision-db.test.ts +11 -11
  329. package/test/tenant-registry-db.test.ts +5 -5
  330. package/test/twin-audit.test.ts +83 -0
  331. package/test/twin-model-item-db.test.ts +275 -0
  332. package/test/twin-model-tree-db.test.ts +176 -0
  333. package/test/twin-origin-resync.test.ts +114 -0
  334. package/test/warm-start-seam.test.ts +6 -6
  335. package/test/warm-start.test.ts +88 -3
  336. package/tsconfig.json +6 -1
  337. package/tsconfig.shared.json +23 -0
  338. package/tsconfig.shared.tsbuildinfo +1 -0
  339. package/tsconfig.tsbuildinfo +1 -0
  340. package/dist-server/engine/board-vocabulary.js.map +0 -1
  341. package/dist-server/engine/entity-delta.js.map +0 -1
  342. package/dist-server/engine/kpi-broadcast.js.map +0 -1
  343. package/dist-server/engine/twin-level.js.map +0 -1
  344. package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
  345. package/dist-server/service/twin-event/backfill-keys.js +0 -63
  346. package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
  347. package/dist-server/tsconfig.tsbuildinfo +0 -1
  348. package/server/service/twin-event/backfill-keys.ts +0 -72
  349. /package/{dist-server/engine → dist-shared}/kpi-broadcast.d.ts +0 -0
  350. /package/{dist-server/engine → dist-shared}/kpi-broadcast.js +0 -0
  351. /package/{dist-server/engine → dist-shared}/twin-level.d.ts +0 -0
  352. /package/{dist-server/engine → dist-shared}/twin-level.js +0 -0
  353. /package/{server/engine → shared}/kpi-broadcast.ts +0 -0
  354. /package/{server/engine → shared}/twin-level.ts +0 -0
@@ -1,11 +1,11 @@
1
1
  /*
2
- * 웜스타트 판정 — **순수**. "기동하는 커널에 직전 상태를 심을 것인가, 심는다면 무엇을" 만 정한다.
2
+ * 웜스타트 판정 — **순수**. "기동하는 커널에 직전 상태를 주입할 것인가, 주입한다면 무엇을" 만 정한다.
3
3
  * 실제 주입(hydrateObserved 호출)과 로그는 엔진이 한다.
4
4
  *
5
5
  * ── 왜 떼어냈나 ─────────────────────────────────────────────────────────────
6
- * 이 판정이 틀리면 증상이 정반대 두 방향으로 나온다: 심어야 할 때 안 심으면 **있는 재고가 0 으로**
6
+ * 이 판정이 틀리면 증상이 정반대 두 방향으로 나온다: 주입해야 할 때 안 주입하면 **있는 재고가 0 으로**
7
7
  * 보이고(2026-07-31 hatiolab-wms: 저널에 입고 540·출고 94 인데 재고 화면이 비어 있었다), 심지
8
- * 말아야 할 벤치에 심으면 **용량 측정이 오염된다.** 둘 다 조용히 틀리는 종류라 규칙을 고정한다.
8
+ * 말아야 할 벤치에 주입하면 **용량 측정이 오염된다.** 둘 다 조용히 틀리는 종류라 규칙을 고정한다.
9
9
  *
10
10
  * ── 오더를 버리던 전제가 낡았다 (2026-08-05) ────────────────────────────────
11
11
  * 예전 주석은 "스냅샷의 오더에는 progress 밖에 없어서 역산하면 없는 숫자를 지어내는 셈이므로
@@ -37,8 +37,15 @@ export interface ObservedSeed {
37
37
  assets?: unknown[]
38
38
  tasks?: unknown[]
39
39
  orders?: unknown[]
40
- /** 확인해 둔 주목 신호 id — 상태에서 파생되지 않는 유일한 축이라 반드시 함께 넘긴다. */
40
+ /** 확인해 둔 주목 신호 id — 상태에서 파생되지 않아 반드시 함께 넘긴다. */
41
41
  acked?: string[]
42
+ /**
43
+ * 주목 신호가 **처음 성립한 시각** — 확인과 같은 성질이다(계산으로 되살릴 수 없다).
44
+ *
45
+ * 넘기지 않으면 재기동 뒤 세 시간째 지속된 조건이 「0초째」로 되살아난다. 방금 찬 자리와 세 시간째
46
+ * 막힌 자리는 할 일이 다르므로, 그 값이 거짓이면 화면이 사람을 잘못된 순서로 움직인다.
47
+ */
48
+ attentionSince?: { id: string; since: string }[]
42
49
  }
43
50
 
44
51
  /** 심은 것의 내역 — 로그와 테스트가 같은 숫자를 본다. */
@@ -52,6 +59,8 @@ export interface WarmStartCounts {
52
59
  orderCount: number
53
60
  /** 이어받은 확인 처리 수 — 잃으면 확인해 둔 신호가 다시 빨개진다. */
54
61
  ackedCount: number
62
+ /** 이어받은 시작 시각 수 — 잃으면 지속된 조건이 모두 「방금」이 된다. */
63
+ attentionSinceCount: number
55
64
  /**
56
65
  * 원값이 없어 심지 못하는 오더 — **0 이 아니면 반드시 말한다.**
57
66
  * progress 만 있는 오더는 남은 수량을 알 수 없어 재계획의 근거가 되지 못한다. 지어내지 않고 뺀다.
@@ -61,14 +70,14 @@ export interface WarmStartCounts {
61
70
 
62
71
  export type WarmStartPlan =
63
72
  | ({ action: 'hydrate'; seed: ObservedSeed } & WarmStartCounts)
64
- /** 벤치 트윈 — 새 시작에서 용량을 재는 게 목적이라 현재 상태를 심으면 측정이 오염된다. */
73
+ /** 벤치 트윈 — 새 시작에서 용량을 재는 게 목적이라 현재 상태를 주입하면 측정이 오염된다. */
65
74
  | { action: 'skip'; reason: 'bench' }
66
75
  /** 심을 상태가 없다 — 처음 만든 트윈이거나 저널·체크포인트가 비었다. 정상이다. */
67
76
  | { action: 'skip'; reason: 'no-state' }
68
77
  /** 커널이 관측 주입을 지원하지 않는다 — 구조만으로 시작하므로 보유량은 0 으로 읽힌다(알려야 한다). */
69
78
  | { action: 'skip'; reason: 'unsupported' }
70
79
 
71
- /** 스냅샷이 들고 올 수 있는 관측 축 — 어느 것이든 있으면 심을 값어치가 있다. */
80
+ /** 스냅샷이 들고 올 수 있는 관측 축 — 어느 것이든 있으면 주입할 가치가 있다. */
72
81
  interface ObservedState {
73
82
  locations?: unknown[]
74
83
  items?: unknown[]
@@ -78,6 +87,7 @@ interface ObservedState {
78
87
  tasks?: unknown[]
79
88
  orders?: unknown[]
80
89
  acked?: string[]
90
+ attentionSince?: { id: string; since: string }[]
81
91
  }
82
92
 
83
93
  /** 관측 상태가 실려 있는지 — 축 하나라도 있으면 상태다(봉투에는 없다). */
@@ -111,11 +121,11 @@ const hasDemand = (o: unknown): boolean =>
111
121
  /**
112
122
  * 무엇을 할지 정한다.
113
123
  *
114
- * 오더·작업·사람·자산도 함께 심는다. 골라내는 규칙은 커널이 들고 있고(이행 완료 오더, 고아 작업),
124
+ * 오더·작업·사람·자산도 함께 주입한다. 골라내는 규칙은 커널이 들고 있고(이행 완료 오더, 고아 작업),
115
125
  * 여기서는 **심을 수 없는 오더의 수만 센다** — 호출부가 그 사실을 말할 수 있어야 하기 때문이다.
116
126
  */
117
127
  export function planWarmStart(state: ObservedState | null | undefined, purpose: string | undefined, canHydrate: boolean): WarmStartPlan {
118
- /* 벤치 판정이 먼저다 — 상태가 있든 없든 벤치에는 심지 않는다는 사실이 바뀌지 않는다. */
128
+ /* 벤치 판정이 먼저다 — 상태가 있든 없든 벤치에는 주입하지 않는다는 사실이 바뀌지 않는다. */
119
129
  if (purpose === 'bench') return { action: 'skip', reason: 'bench' }
120
130
  if (!state) return { action: 'skip', reason: 'no-state' }
121
131
  /* 봉투가 들어오면 스스로 벗는다 — 호출부가 한 곳이라도 잊으면 그 경로가 조용히 죽는다(실제로 그랬다). */
@@ -130,13 +140,18 @@ export function planWarmStart(state: ObservedState | null | undefined, purpose:
130
140
  const orders = arr(state.orders)
131
141
  /* 확인 처리는 문자열 id 목록이다 — 배열이 아니면 없는 것으로 본다(지어내지 않는다). */
132
142
  const acked = Array.isArray(state.acked) ? state.acked.filter((x): x is string => typeof x === 'string') : []
143
+ /* 시작 시각은 {id, since} 쌍 목록이다 — 모양이 아니면 없는 것으로 본다(지어낸 시각을 심으면 화면이
144
+ 「세 시간째」라고 거짓을 말한다. 모르면 그 신호는 이어붙는 순간이 새 시작이다). */
145
+ const attentionSince = Array.isArray(state.attentionSince)
146
+ ? state.attentionSince.filter((x: any): x is { id: string; since: string } => !!x && typeof x.id === 'string' && typeof x.since === 'string')
147
+ : []
133
148
 
134
149
  /* 어느 축도 없으면 심을 것이 없다 — 빈 주입으로 로그만 남기지 않는다. */
135
- if (!locations.length && !items.length && !equipment.length && !persons.length && !assets.length && !tasks.length && !orders.length && !acked.length) {
150
+ if (!locations.length && !items.length && !equipment.length && !persons.length && !assets.length && !tasks.length && !orders.length && !acked.length && !attentionSince.length) {
136
151
  return { action: 'skip', reason: 'no-state' }
137
152
  }
138
153
 
139
- /* 지원 여부는 마지막에 본다 — 심을 게 있는데 못 심는 상황이라야 경고할 값어치가 있다. */
154
+ /* 지원 여부는 마지막에 본다 — 주입할 것이 있는데 주입하지 못하는 상황이라야 경고할 가치가 있다. */
140
155
  if (!canHydrate) return { action: 'skip', reason: 'unsupported' }
141
156
 
142
157
  const seedable = orders.filter(hasDemand)
@@ -153,7 +168,9 @@ export function planWarmStart(state: ObservedState | null | undefined, purpose:
153
168
  /* 원값 없는 오더는 넘기지 않는다 — 커널도 걸러 내지만, 넘기지 않으면 세어 둔 수와 어긋날 일이 없다. */
154
169
  ...(seedable.length ? { orders: seedable } : {}),
155
170
  /* 확인 처리는 조건이 사라진 id 가 섞여 있어도 해롭지 않다 — 그 신호가 없으면 표시할 대상이 없다. */
156
- ...(acked.length ? { acked } : {})
171
+ ...(acked.length ? { acked } : {}),
172
+ /* 조건이 사라진 id 가 섞여 있어도 해롭지 않다 — 커널이 그 신호가 없으면 함께 지운다. */
173
+ ...(attentionSince.length ? { attentionSince } : {})
157
174
  },
158
175
  itemCount: items.length,
159
176
  equipmentCount: equipment.length,
@@ -162,6 +179,60 @@ export function planWarmStart(state: ObservedState | null | undefined, purpose:
162
179
  taskCount: tasks.length,
163
180
  orderCount: seedable.length,
164
181
  ordersWithoutDemand: orders.length - seedable.length,
165
- ackedCount: acked.length
182
+ ackedCount: acked.length,
183
+ attentionSinceCount: attentionSince.length
184
+ }
185
+ }
186
+
187
+ /*
188
+ * ── 미러(관측 구동) 재기동의 연속성 ─────────────────────────────────────────
189
+ *
190
+ * 미러의 진실은 원천이다. 그래서 재고·위치·설비 상태는 심지 않는다 — 다음 계측·다음 마스터 재읽기가
191
+ * 곧 정정하므로, 심으면 **떠난 물건이 되살아나는** 위험만 남는다(그래서 `startLive` 는 오랫동안 아무것도
192
+ * 심지 않았다).
193
+ *
194
+ * 그런데 원천이 **애초에 다시 말해 주지 않는 축**이 있다. 우리가 접어 만든 것들이다:
195
+ * · 확인(ack)·조건이 처음 성립한 시각 — 사람이 한 일과 우리가 기억한 시각
196
+ * · 에너지의 열린 구간 누적·적산 기준점·관측 이후 최대 — SCADA 는 「이번 15분에 지금까지 얼마」를 모른다
197
+ *
198
+ * 잃으면 **오류 없이 값이 작아진다**(실측 2026-08-18: 재기동 뒤 그 구간의 전력량·피크가 작아지고, 세
199
+ * 시간째 지속된 경보가 「0초째」가 됐다). 그래서 축을 갈라 되풀어 주지 않는 것만 이어받는다.
200
+ *
201
+ * 무엇을 이어받을지는 여기서 고르고, **어떻게 흡수할지는 커널이 정한다**(`hydrateContinuity`) — 도메인
202
+ * 규칙이 호스트로 새면 트윈마다 다른 답이 된다.
203
+ */
204
+ export interface ContinuitySeed {
205
+ acked?: string[]
206
+ attentionSince?: { id: string; since: string }[]
207
+ energy?: unknown
208
+ }
209
+
210
+ export interface ContinuityPlan {
211
+ seed: ContinuitySeed
212
+ /** 무엇을 이어받았나 — 로그와 시험이 같은 숫자를 본다(0 이면 이어받을 것이 없었다는 사실이다). */
213
+ ackedCount: number
214
+ attentionSinceCount: number
215
+ /** 에너지 연속성을 이어받았나 — 열린 구간이 있으면 그 구간의 요금 근거가 살아난다. */
216
+ energy: boolean
217
+ }
218
+
219
+ /** 미러가 이어받을 축만 골라낸다 — 관측 축(재고·위치·설비)은 일부러 넣지 않는다. */
220
+ export function planLiveContinuity(raw: unknown): ContinuityPlan {
221
+ const state = raw ? (unwrapState(raw as any) as any) : undefined
222
+ const acked = Array.isArray(state?.acked) ? state.acked.filter((x: any): x is string => typeof x === 'string') : []
223
+ const attentionSince = Array.isArray(state?.attentionSince)
224
+ ? state.attentionSince.filter((x: any): x is { id: string; since: string } => !!x && typeof x.id === 'string' && typeof x.since === 'string')
225
+ : []
226
+ /* 에너지는 커널이 자기 모양을 안다 — 호스트는 「있나」만 본다(골라 담으면 규칙이 두 곳으로 갈린다). */
227
+ const energy = state?.energy && typeof state.energy === 'object' ? state.energy : undefined
228
+ return {
229
+ seed: {
230
+ ...(acked.length ? { acked } : {}),
231
+ ...(attentionSince.length ? { attentionSince } : {}),
232
+ ...(energy ? { energy } : {})
233
+ },
234
+ ackedCount: acked.length,
235
+ attentionSinceCount: attentionSince.length,
236
+ energy: !!energy
166
237
  }
167
238
  }
package/server/index.ts CHANGED
@@ -1,25 +1,28 @@
1
1
  export * from './engine/index.js'
2
2
  export * from './service/index.js'
3
+ /* 스키마 이력 — 프레임워크가 `migrations` export 를 모아 실행한다(`run migration`). */
4
+ export { migrations } from './migrations/index.js'
3
5
 
4
6
  import './routes.js'
5
7
 
6
8
  import { TwinEngine } from './engine/index.js'
7
- import { backfillTwinEventKeys } from './service/twin-event/backfill-keys.js'
9
+ import { resumeReferenceLiveFeeds } from './service/reference/reference-live.js'
8
10
 
9
- /* 모듈 부팅 — 영속 인스턴스 복구 훅(향후). 지금은 명시 start(mutation/부팅 설정)로 인스턴스 생성. */
11
+ /*
12
+ * 모듈 부팅 — **도는 트윈을 되살린다.**
13
+ *
14
+ * 두 층이 순서대로다: ① 엔진이 등록부의 `running` 행을 선언된 `realityMode` 그대로 되살리고
15
+ * (`bootstrap`), ② 레퍼런스 계층이 미러 트윈의 커넥터 피드를 다시 붙인다. 순서가 뒤집히면 피드가
16
+ * 아직 없는 커널에 계측을 붓게 된다.
17
+ *
18
+ * 예전에는 ①도 ②도 없었다 — 재기동 한 번에 등록부만 「도는 중」이라 말하는 트윈이 남았다.
19
+ */
10
20
  process.on('bootstrap-module-start' as any, async ({ app, config, client }: any) => {
11
21
  try {
12
22
  await TwinEngine.bootstrap()
23
+ await resumeReferenceLiveFeeds()
13
24
  console.log('Headless Twin host has started.')
14
25
  } catch (ex) {
15
26
  console.error('Headless Twin host failed to start.', ex)
16
27
  }
17
-
18
- /*
19
- * 승격 검색 키 백필 — 컬럼이 생기기 전에 쌓인 저널을 채운다. 멱등·재개 가능이라 매 기동 불러도
20
- * 채울 게 없으면 조각 한 번 읽고 끝난다(로그도 남기지 않는다).
21
- * 기동을 막지 않는다 — 저널이 크면 오래 걸릴 수 있고, 그동안 트윈은 정상 동작해야 한다.
22
- * 실패해도 서비스는 계속된다: 못 채운 만큼 **과거 이력 검색이 덜 나올 뿐**이므로 조용히 삼키지 않고 알린다.
23
- */
24
- backfillTwinEventKeys().catch(ex => console.error('twin-event key backfill failed — search over older journal rows will be incomplete.', ex))
25
28
  })
@@ -0,0 +1,43 @@
1
+ import { MigrationInterface, QueryRunner } from 'typeorm'
2
+
3
+ /**
4
+ * `twin_instances.model` → `twin_instances.model`.
5
+ *
6
+ * ── 왜 개명하나 ────────────────────────────────────────────────────────────────
7
+ * 이 컬럼이 담는 것은 커널이 `TwinModelDef` 라 부르는 **트윈 모델**이다. `board` 는 저장 위치의 옛
8
+ * 이름이고, 이 앱에는 **진짜 board 가 따로 있다**(things-scene 보드 — 공간 표현의 `boardId`). 같은
9
+ * 낱말이 두 가지를 뜻하니 읽는 사람이 매번 어느 쪽인지 판단해야 했고, 코드와 주석이 "board(마스터
10
+ * 청사진)" 처럼 번역을 달아 설명해 왔다. 커널이 이미 옳은 이름을 갖고 있으므로 발명할 이름도 없다.
11
+ *
12
+ * ── 데이터를 잃지 않는다 ──────────────────────────────────────────────────────
13
+ * `renameColumn` 은 값을 그대로 둔 채 이름만 바꾼다. 지우고 다시 만들면(엔티티 변경 + synchronize)
14
+ * 기존 트윈의 모델이 통째로 사라지고, 원천이 없는 트윈은 되살릴 방법이 없다.
15
+ *
16
+ * `renameColumn` API 만 쓰므로 지원 대상 5개 드라이버가 각자의 DDL 로 처리한다(raw SQL 금지 규약).
17
+ *
18
+ * ── 멱등 ──────────────────────────────────────────────────────────────────────
19
+ * 이미 `model` 이 있으면(엔티티 선언 + synchronize 가 먼저 지나간 경우) 아무것도 하지 않는다.
20
+ * 그 경우 옛 컬럼이 남아 있을 수 있는데 **지우지 않는다** — 값이 든 컬럼을 마이그레이션이 조용히
21
+ * 버리면 안 된다. 남은 컬럼의 처분은 사람이 확인한 뒤의 일이다.
22
+ */
23
+ const TABLE_NAME = 'twin_instances'
24
+ const OLD = 'board'
25
+ const NEW = 'model'
26
+
27
+ export class RenameTwinInstanceBoardToModel1786000000000 implements MigrationInterface {
28
+ public async up(queryRunner: QueryRunner): Promise<void> {
29
+ const table = await queryRunner.getTable(TABLE_NAME)
30
+ if (!table) return
31
+ const has = (name: string) => table.columns.some(c => c.name === name)
32
+ if (has(NEW) || !has(OLD)) return
33
+ await queryRunner.renameColumn(table, OLD, NEW)
34
+ }
35
+
36
+ public async down(queryRunner: QueryRunner): Promise<void> {
37
+ const table = await queryRunner.getTable(TABLE_NAME)
38
+ if (!table) return
39
+ const has = (name: string) => table.columns.some(c => c.name === name)
40
+ if (has(OLD) || !has(NEW)) return
41
+ await queryRunner.renameColumn(table, NEW, OLD)
42
+ }
43
+ }
@@ -0,0 +1,7 @@
1
+ /*
2
+ * 마이그레이션 묶음 — 프레임워크 규약(`@things-factory/shell` · `auth-base` 와 같은 방식).
3
+ * 파일마다 하나씩 export 하고 여기서 모아 `server/index.ts` 가 내보낸다.
4
+ */
5
+ import { RenameTwinInstanceBoardToModel1786000000000 } from './1786000000000-RenameTwinInstanceBoardToModel.js'
6
+
7
+ export const migrations = [RenameTwinInstanceBoardToModel1786000000000]
@@ -1,5 +1,6 @@
1
1
  /* EXPORT ENTITY TYPES */
2
2
  export * from './twin-event/twin-event.js'
3
+ export * from './twin-audit/twin-audit-event.js'
3
4
  export * from './twin-instance/twin-instance.js'
4
5
  export * from './twin-space/twin-space.js'
5
6
  export * from './twin-space/twin-space-representation.js'
@@ -18,6 +19,7 @@ export * from './twin-lifecycle/domain-catalog.js'
18
19
 
19
20
  /* IMPORT ENTITIES AND RESOLVERS */
20
21
  import { entities as TwinEventEntities } from './twin-event/index.js'
22
+ import { entities as TwinAuditEntities, resolvers as TwinAuditResolvers } from './twin-audit/index.js'
21
23
  import { entities as TwinInstanceEntities } from './twin-instance/index.js'
22
24
  import { entities as TwinStructureEntities } from './twin-structure/index.js'
23
25
  import { entities as TwinSpaceEntities, resolvers as TwinSpaceResolvers } from './twin-space/index.js'
@@ -25,6 +27,7 @@ import { entities as TwinTargetEntities, resolvers as TwinTargetResolvers } from
25
27
  import { entities as TwinReferenceEntities, resolvers as TwinReferenceResolvers } from './reference/index.js'
26
28
  import { resolvers as TwinStateResolvers } from './twin-state/index.js'
27
29
  import { resolvers as TwinControlResolvers } from './twin-control/index.js'
30
+ import { entities as TwinModelEntities, resolvers as TwinModelResolvers } from './twin-model/index.js'
28
31
  import { resolvers as TwinLifecycleResolvers } from './twin-lifecycle/index.js'
29
32
  import { resolvers as TwinJournalResolvers } from './twin-journal/index.js'
30
33
  import { resolvers as TwinForecastResolvers } from './twin-forecast/index.js'
@@ -34,11 +37,14 @@ import { resolvers as TwinMetricsResolvers } from './twin-metrics/index.js'
34
37
  export const entities = [
35
38
  /* ENTITIES */
36
39
  ...TwinEventEntities,
40
+ ...TwinAuditEntities,
37
41
  ...TwinInstanceEntities,
38
42
  ...TwinStructureEntities,
39
43
  ...TwinSpaceEntities,
40
44
  ...TwinTargetEntities,
41
- ...TwinReferenceEntities
45
+ ...TwinReferenceEntities,
46
+ /* 구조 투영 — model JSON 에서 풀어낸 표준 엔티티(ADR-0032). 원본에서 온 캐시. */
47
+ ...TwinModelEntities
42
48
  ]
43
49
 
44
50
  export const subscribers = []
@@ -51,9 +57,11 @@ export const schema = {
51
57
  ...TwinControlResolvers,
52
58
  ...TwinLifecycleResolvers,
53
59
  ...TwinJournalResolvers,
60
+ ...TwinAuditResolvers,
54
61
  ...TwinForecastResolvers,
55
62
  ...TwinAttentionResolvers,
56
63
  ...TwinMetricsResolvers,
64
+ ...TwinModelResolvers,
57
65
  ...TwinSpaceResolvers,
58
66
  ...TwinTargetResolvers,
59
67
  ...TwinReferenceResolvers
@@ -1,5 +1,5 @@
1
1
  /*
2
- * 사이트 발견 결과 — **"못 찾았다" 와 "없다" 를 가른다.** 순수.
2
+ * 사이트 발견 결과 — **"못 찾았다" 와 "없다" 를 구분한다.** 순수.
3
3
  *
4
4
  * ── 왜 있나 (2026-08-06) ────────────────────────────────────────────────────
5
5
  * 실 시스템을 연결해 "거기 있는 것" 으로 트윈을 만드는 경로에서, 발견이 실패하면 화면이
@@ -75,7 +75,7 @@ export const discoveryTimedOut = (source: string, seconds: number): DiscoveryErr
75
75
  *
76
76
  * **사이트가 없는 것을 실패로 만들지 않는다** — 연결이 되고 조회가 성공했는데 공장이 하나도 없는
77
77
  * 시스템은 있을 수 있다(권한이 좁거나 아직 등록되지 않은 경우). 그것은 사실이고, 화면은 "없다" 고
78
- * 말해야 한다. 실패로 뭉개면 사용자가 있지도 않은 문제를 찾는다.
78
+ * 말해야 한다. 실패로 합치면 사용자가 있지도 않은 문제를 찾는다.
79
79
  */
80
80
  export function discovered<S>(sites: readonly S[] | undefined | null): DiscoveryResult<S> {
81
81
  return { sites: Array.isArray(sites) ? [...sites] : [] }
@@ -51,6 +51,40 @@ export function resolveIngestSpace(masterSpaceId: string, requested: string | un
51
51
  return { spaceId: want, joined: want !== masterSpaceId }
52
52
  }
53
53
 
54
+ /**
55
+ * 실 시스템 import 가 현장을 고를 때의 거절 판정 — **순수**.
56
+ *
57
+ * 레퍼런스 하나는 사이트를 여럿 발견할 수 있고(`discoverSites`), 그 사이트마다 트윈이 하나씩 생긴다.
58
+ * 그런데 "이 현장에 더한다" 는 **한 곳**을 가리키는 말이다. 사이트 여럿을 한 공간에 넣으면 서로 다른
59
+ * 현장의 구역·랜드마크가 id 로 합쳐지고(`unionById`), 그 사실을 알아차릴 때는 이미 이벤트가 양쪽에
60
+ * 쌓여 있다 — 손으로 되돌릴 수 없는 종류다. 그래서 **묻지 않고 합치지 않고, 거절한다.**
61
+ *
62
+ * 공간을 고르지 않으면 지금까지의 거동 그대로다: 사이트마다 마스터가 파생한 공간으로 들어간다.
63
+ */
64
+ export type ImportSpaceRefusalCode = 'space-with-multiple-sites'
65
+
66
+ export interface ImportSpaceRefusal {
67
+ code: ImportSpaceRefusalCode
68
+ params?: Record<string, string | number>
69
+ /** 영어 canonical — 로그에 남고, 번역이 없으면 그대로 보인다(발견 오류와 같은 규약). */
70
+ message: string
71
+ }
72
+
73
+ /**
74
+ * @param siteCount 실제로 만들 사이트 수(필터 적용 후).
75
+ * @param requested 사용자가 고른 기존 공간 id. 없으면 판정할 것이 없다.
76
+ * @returns 거절 사유. 없으면 진행해도 된다.
77
+ */
78
+ export function refuseImportSpace(siteCount: number, requested?: string | null): ImportSpaceRefusal | undefined {
79
+ const want = (requested ?? '').trim()
80
+ if (!want || siteCount <= 1) return undefined
81
+ return {
82
+ code: 'space-with-multiple-sites',
83
+ params: { spaceId: want, count: siteCount },
84
+ message: `cannot co-locate ${siteCount} sites into space "${want}" — pick one site, or omit the space so each site gets its own`
85
+ }
86
+ }
87
+
54
88
  /** 구역·랜드마크처럼 id 로 합쳐지는 것들. */
55
89
  interface Identified {
56
90
  id?: string
@@ -60,7 +94,7 @@ interface Identified {
60
94
  * 합치는 쪽과 기존 쪽이 같은 id 를 쓰는 것들을 찾아 경고 문장을 만든다.
61
95
  *
62
96
  * 정당한 경우(같은 현장의 같은 구역)와 사고(서로 다른 구역이 우연히 같은 이름)를 기계가 가릴 수는
63
- * 없다. 가릴 수 없는 것을 고르지 않고, **무엇이 합쳐졌는지 말한다.**
97
+ * 없다. 구분할 수 없는 것을 고르지 않고, **무엇이 합쳐졌는지 말한다.**
64
98
  */
65
99
  export function collidingIds(existing: readonly Identified[] | undefined | null, incoming: readonly Identified[] | undefined | null): string[] {
66
100
  const have = new Set((existing ?? []).map(x => x?.id).filter(Boolean) as string[])
@@ -1,5 +1,9 @@
1
+ import { getRepository } from '@things-factory/shell'
2
+
1
3
  import { getAdapter, type ConnectionConfig, type SiteDescriptor } from './reference-adapter.js'
2
- import { TwinEngine, ingestCanonicalRecords, type CanonicalRecord } from '../../engine/index.js'
4
+ import { TwinEngine, ingestCanonicalRecords, registerLiveFeedProbe, type CanonicalRecord } from '../../engine/index.js'
5
+ import { TwinInstance } from '../twin-instance/twin-instance.js'
6
+ import { TwinReference } from './twin-reference.js'
3
7
 
4
8
  /*
5
9
  * 커넥터 기반 라이브 피드 결선(reference → live, §0 프레임 ① realityMode='mirror').
@@ -27,11 +31,11 @@ export async function startReferenceLiveFeed(
27
31
  if (!adapter?.openLiveFeed) return false
28
32
 
29
33
  const det = await TwinEngine.detail(domainId, instanceId)
30
- if (!det?.board) throw new Error(`instance "${instanceId}" not provisioned (no board) — ingest master before live start`)
34
+ if (!det?.model) throw new Error(`instance "${instanceId}" not provisioned (no model) — ingest master before live start`)
31
35
 
32
36
  // mirror 모드 기동 — 커널 tick 없이 projector 가 실 이벤트를 미러(startLive 가 realityMode='mirror' 설정).
33
37
  // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.
34
- TwinEngine.startLive(instanceId, domainId, det.kind ?? 'wms', await TwinEngine.withSpaceTimeBase(det.board, domainId))
38
+ TwinEngine.startLive(instanceId, domainId, det.kind, await TwinEngine.withSpaceTimeBase(det.model, domainId))
35
39
 
36
40
  stopReferenceLiveFeed(instanceId) // 재기동 시 이전 피드 정리
37
41
  const unsubscribe = adapter.openLiveFeed(cfg, site, (records: unknown[]) => {
@@ -39,8 +43,19 @@ export async function startReferenceLiveFeed(
39
43
  stopReferenceLiveFeed(instanceId) // 인스턴스 사라지면 자가 정지(안전망)
40
44
  return
41
45
  }
42
- const { accepted } = ingestCanonicalRecords(records as CanonicalRecord[], domainId, new Date().toISOString())
46
+ const { accepted, rejected } = ingestCanonicalRecords(records as CanonicalRecord[], domainId, new Date().toISOString())
43
47
  if (accepted.length) TwinEngine.ingestLive(domainId, instanceId, accepted)
48
+ /*
49
+ * **거부한 것을 조용히 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이
50
+ * 틀리면 트윈은 「아무 일도 일어나지 않는」 모습이 되고, 로그에도 아무 흔적이 없었다.
51
+ *
52
+ * 지금은 이유와 함께 남긴다. 화면까지 나르는 것은 다음 일이다(라이브 계측은 초당 여러 건이라
53
+ * 건마다 방송하면 그 자체가 부하가 된다 — 집계해 낼 자리가 필요하다).
54
+ */
55
+ if (rejected.length) {
56
+ const why = rejected[0]?.errors?.join(' · ') ?? 'unknown'
57
+ console.warn(`[twin-live] "${instanceId}": ${rejected.length} record(s) rejected — e.g. ${why}`)
58
+ }
44
59
  })
45
60
  feeds.set(instanceId, unsubscribe)
46
61
  return true
@@ -60,5 +75,105 @@ export function listReferenceLiveFeeds(): string[] {
60
75
  return [...feeds.keys()]
61
76
  }
62
77
 
78
+ /**
79
+ * 부팅 때 **끊어진 피드를 다시 붙인다** — 도는 미러 트윈에 계측이 계속 들어오게.
80
+ *
81
+ * ── 무엇이 조용히 끊겼나 (2026-08-14) ───────────────────────────────────────
82
+ * `startReferenceLiveFeed` 를 부르는 곳은 `importTwinReference` **하나뿐**이었다. 그래서 서버를 한 번
83
+ * 재기동하면 미러 트윈은 「도는 중」이라 말하면서 아무 계측도 받지 않았다 — 오류도 로그도 없이,
84
+ * 사람은 「값이 안 변하네」로 알게 된다. 실 배포에서 재기동 한 번에 계측이 멈추는 셈이다.
85
+ *
86
+ * ── 되살릴 재료는 이미 영속돼 있다 ─────────────────────────────────────────
87
+ * `TwinReference` 가 어댑터 종류·접속 설정·그 레퍼런스가 만든 인스턴스(`scopeSpec.produced`)를 들고 있다.
88
+ * 사이트 서술자만 없는데(`hint` 는 남기지 않았다) **다시 찾아서** 얻는다(`discoverSites`) — 인제스트가
89
+ * 걷는 것과 같은 길이므로 규칙이 갈라지지 않는다. 되살릴 인스턴스가 없으면 그 호출도 하지 않는다.
90
+ *
91
+ * 재개 대상은 **등록부가 도는 미러라고 말하는 것**뿐이다: 멈춘 트윈을 되살리면 사람이 멈춘 것을
92
+ * 우리가 다시 켜는 것이고, 시뮬 트윈에 실 계측을 부으면 두 현실이 섞인다.
93
+ */
94
+ export async function resumeReferenceLiveFeeds(): Promise<{ resumed: string[]; failed: { instanceId: string; error: string }[] }> {
95
+ const resumed: string[] = []
96
+ const failed: { instanceId: string; error: string }[] = []
97
+ /*
98
+ * **한 트윈에 피드는 하나다** (2026-08-18).
99
+ *
100
+ * 같은 사이트를 두 레퍼런스가 각각 인제스트하면 둘 다 그 인스턴스를 자기가 만든 것(`produced`)으로
101
+ * 들고 있다. 기동 때 그 목록을 그대로 돌면 같은 트윈에 피드를 두 번 붙이는데, 붙이는 함수가 미러를
102
+ * 다시 세우므로(`startLive`) **먼저 쌓인 수요 구간이 리셋된다** — 실제로 그렇게 났다
103
+ * (로그: `reattached 2 feed(s): ems-plant-1, ems-plant-1`).
104
+ *
105
+ * 그러니 한 번만 붙이고, **두 레퍼런스가 같은 트윈을 주장한다는 사실은 말한다** — 조용히 하나를
106
+ * 고르면 어느 접속이 그 트윈을 먹이고 있는지 아무도 모르게 된다.
107
+ */
108
+ const attached = new Map<string, string>() // instanceId → 먼저 붙인 레퍼런스
109
+ try {
110
+ const refs = await getRepository(TwinReference).find({ where: { status: 'connected' }, relations: ['domain'] })
111
+ for (const ref of refs) {
112
+ const domainId = (ref as any).domain?.id ?? (ref as any).domainId
113
+ const adapter = getAdapter(ref.adapterType)
114
+ if (!domainId || !adapter?.openLiveFeed) continue // 관측만 하는 어댑터에는 되살릴 피드가 없다
115
+
116
+ const produced: any[] = ((ref.scopeSpec as any)?.produced ?? []).filter((p: any) => p?.instanceId && p?.siteId)
117
+ if (!produced.length) continue
118
+
119
+ /* 등록부가 「도는 미러」라고 말하는 것만 고른다. */
120
+ const rows = await getRepository(TwinInstance).find({
121
+ where: produced.map((p: any) => ({ domain: { id: domainId }, instanceId: p.instanceId })) as any
122
+ })
123
+ const live = produced.filter((p: any) =>
124
+ rows.some(r => r.instanceId === p.instanceId && r.status === 'running' && r.realityMode === 'mirror')
125
+ )
126
+ if (!live.length) continue
127
+
128
+ let sites: SiteDescriptor[]
129
+ try {
130
+ sites = await adapter.discoverSites(ref.connectionConfig ?? {})
131
+ } catch (e: any) {
132
+ /* 원 시스템이 아직 안 떴을 수 있다 — 지어내지 않고 그렇게 적는다(트윈은 계속 「도는 미러」다). */
133
+ for (const p of live) failed.push({ instanceId: p.instanceId, error: `discover failed: ${e?.message ?? 'unknown'}` })
134
+ continue
135
+ }
136
+
137
+ for (const p of live) {
138
+ const site = sites.find(s => s.siteId === p.siteId)
139
+ if (!site) {
140
+ failed.push({ instanceId: p.instanceId, error: `site "${p.siteId}" no longer discoverable` })
141
+ continue
142
+ }
143
+ const owner = attached.get(p.instanceId)
144
+ if (owner) {
145
+ /* 두 접속이 같은 트윈을 주장한다 — 먼저 붙은 것을 지키고, 그 사실을 남긴다(둘 다 붙이면 미러가 리셋된다). */
146
+ failed.push({
147
+ instanceId: p.instanceId,
148
+ error: `also claimed by reference "${ref.source}" — kept the feed from "${owner}" (two connections feeding one twin would reset the mirror)`
149
+ })
150
+ continue
151
+ }
152
+ attached.set(p.instanceId, ref.source)
153
+ try {
154
+ const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId)
155
+ if (ok) resumed.push(p.instanceId)
156
+ else failed.push({ instanceId: p.instanceId, error: 'adapter has no openLiveFeed' })
157
+ } catch (e: any) {
158
+ failed.push({ instanceId: p.instanceId, error: e?.message ?? 'reattach failed' })
159
+ }
160
+ }
161
+ }
162
+ } catch (e: any) {
163
+ console.error('[twin-live] feed resume scan failed', e?.message)
164
+ }
165
+
166
+ if (resumed.length) console.log(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`)
167
+ /* **조용히 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */
168
+ for (const f of failed) console.warn(`[twin-live] "${f.instanceId}" feed NOT reattached — ${f.error}`)
169
+ return { resumed, failed }
170
+ }
171
+
172
+ /*
173
+ * 이 계층의 피드 장부를 등록한다 — 화면이 「끊김」을 말할 근거는 **묻는 자리 하나**여야 한다.
174
+ * (프리셋 기반 피드는 앱이 자기 장부를 따로 등록한다. 한쪽만 보면 흐르는 트윈을 끊겼다고 부른다.)
175
+ */
176
+ registerLiveFeedProbe(() => [...feeds.keys()])
177
+
63
178
  /* 인스턴스 정지 시 피드 자동 정리(생명주기 대칭). */
64
179
  TwinEngine.onStop(instanceId => stopReferenceLiveFeed(instanceId))