@things-factory/headless-twin 10.0.8 → 10.0.10

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 (381) hide show
  1. package/dist-server/engine/attention-digest.d.ts +52 -0
  2. package/dist-server/engine/attention-digest.js +76 -0
  3. package/dist-server/engine/attention-digest.js.map +1 -0
  4. package/dist-server/engine/canonical-ingest.d.ts +2 -2
  5. package/dist-server/engine/canonical-ingest.js +24 -2
  6. package/dist-server/engine/canonical-ingest.js.map +1 -1
  7. package/dist-server/engine/command-routing.d.ts +33 -0
  8. package/dist-server/engine/command-routing.js +53 -0
  9. package/dist-server/engine/command-routing.js.map +1 -0
  10. package/dist-server/engine/energy-topology.d.ts +86 -0
  11. package/dist-server/engine/energy-topology.js +144 -0
  12. package/dist-server/engine/energy-topology.js.map +1 -0
  13. package/dist-server/engine/index.d.ts +11 -0
  14. package/dist-server/engine/index.js +17 -0
  15. package/dist-server/engine/index.js.map +1 -1
  16. package/dist-server/engine/kpi-baseline.d.ts +78 -0
  17. package/dist-server/engine/kpi-baseline.js +123 -0
  18. package/dist-server/engine/kpi-baseline.js.map +1 -0
  19. package/dist-server/engine/kpi-fold.d.ts +137 -1
  20. package/dist-server/engine/kpi-fold.js +225 -0
  21. package/dist-server/engine/kpi-fold.js.map +1 -1
  22. package/dist-server/engine/kpi-query.d.ts +32 -1
  23. package/dist-server/engine/kpi-query.js +262 -20
  24. package/dist-server/engine/kpi-query.js.map +1 -1
  25. package/dist-server/engine/kpi-target.d.ts +17 -0
  26. package/dist-server/engine/kpi-target.js +24 -2
  27. package/dist-server/engine/kpi-target.js.map +1 -1
  28. package/dist-server/engine/live-attentions.d.ts +1 -0
  29. package/dist-server/engine/live-attentions.js +7 -1
  30. package/dist-server/engine/live-attentions.js.map +1 -1
  31. package/dist-server/engine/live-feed-registry.d.ts +25 -0
  32. package/dist-server/engine/live-feed-registry.js +51 -0
  33. package/dist-server/engine/live-feed-registry.js.map +1 -0
  34. package/dist-server/engine/load-meter.d.ts +181 -0
  35. package/dist-server/engine/load-meter.js +267 -0
  36. package/dist-server/engine/load-meter.js.map +1 -0
  37. package/dist-server/engine/local-declarations.d.ts +262 -0
  38. package/dist-server/engine/local-declarations.js +528 -0
  39. package/dist-server/engine/local-declarations.js.map +1 -0
  40. package/dist-server/engine/model-basis.d.ts +39 -6
  41. package/dist-server/engine/model-basis.js +69 -9
  42. package/dist-server/engine/model-basis.js.map +1 -1
  43. package/dist-server/engine/model-vocabulary.d.ts +17 -0
  44. package/dist-server/engine/model-vocabulary.js +81 -0
  45. package/dist-server/engine/model-vocabulary.js.map +1 -0
  46. package/dist-server/engine/oee-accumulator.d.ts +28 -0
  47. package/dist-server/engine/oee-accumulator.js +26 -1
  48. package/dist-server/engine/oee-accumulator.js.map +1 -1
  49. package/dist-server/engine/operation-basis.d.ts +32 -0
  50. package/dist-server/engine/operation-basis.js +87 -0
  51. package/dist-server/engine/operation-basis.js.map +1 -0
  52. package/dist-server/engine/property-effects.d.ts +30 -0
  53. package/dist-server/engine/property-effects.js +192 -0
  54. package/dist-server/engine/property-effects.js.map +1 -0
  55. package/dist-server/engine/runtime-key.d.ts +15 -0
  56. package/dist-server/engine/runtime-key.js +64 -0
  57. package/dist-server/engine/runtime-key.js.map +1 -0
  58. package/dist-server/engine/spec-coverage.js +3 -3
  59. package/dist-server/engine/spec-coverage.js.map +1 -1
  60. package/dist-server/engine/state-axes.d.ts +21 -0
  61. package/dist-server/engine/state-axes.js +59 -0
  62. package/dist-server/engine/state-axes.js.map +1 -0
  63. package/dist-server/engine/structure-diff.js +1 -1
  64. package/dist-server/engine/structure-diff.js.map +1 -1
  65. package/dist-server/engine/travel-estimator.d.ts +9 -2
  66. package/dist-server/engine/travel-estimator.js +11 -6
  67. package/dist-server/engine/travel-estimator.js.map +1 -1
  68. package/dist-server/engine/twin-engine.d.ts +418 -57
  69. package/dist-server/engine/twin-engine.js +1599 -306
  70. package/dist-server/engine/twin-engine.js.map +1 -1
  71. package/dist-server/engine/warm-start.d.ts +93 -13
  72. package/dist-server/engine/warm-start.js +114 -14
  73. package/dist-server/engine/warm-start.js.map +1 -1
  74. package/dist-server/index.d.ts +1 -0
  75. package/dist-server/index.js +17 -11
  76. package/dist-server/index.js.map +1 -1
  77. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.d.ts +5 -0
  78. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +48 -0
  79. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -0
  80. package/dist-server/migrations/index.d.ts +2 -0
  81. package/dist-server/migrations/index.js +10 -0
  82. package/dist-server/migrations/index.js.map +1 -0
  83. package/dist-server/service/index.d.ts +3 -2
  84. package/dist-server/service/index.js +25 -17
  85. package/dist-server/service/index.js.map +1 -1
  86. package/dist-server/service/reference/discovery-result.d.ts +34 -0
  87. package/dist-server/service/reference/discovery-result.js +84 -0
  88. package/dist-server/service/reference/discovery-result.js.map +1 -0
  89. package/dist-server/service/reference/ingest-space.d.ts +53 -0
  90. package/dist-server/service/reference/ingest-space.js +79 -0
  91. package/dist-server/service/reference/ingest-space.js.map +1 -0
  92. package/dist-server/service/reference/knob-defaults.d.ts +20 -0
  93. package/dist-server/service/reference/knob-defaults.js +59 -0
  94. package/dist-server/service/reference/knob-defaults.js.map +1 -0
  95. package/dist-server/service/reference/reference-live.d.ts +23 -0
  96. package/dist-server/service/reference/reference-live.js +123 -6
  97. package/dist-server/service/reference/reference-live.js.map +1 -1
  98. package/dist-server/service/reference/reference-master.d.ts +227 -11
  99. package/dist-server/service/reference/reference-master.js +206 -25
  100. package/dist-server/service/reference/reference-master.js.map +1 -1
  101. package/dist-server/service/reference/reference-resolver.d.ts +33 -3
  102. package/dist-server/service/reference/reference-resolver.js +290 -23
  103. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  104. package/dist-server/service/reference/template-registry.d.ts +19 -3
  105. package/dist-server/service/reference/template-registry.js.map +1 -1
  106. package/dist-server/service/twin-attention/twin-attention-query.d.ts +8 -1
  107. package/dist-server/service/twin-attention/twin-attention-query.js +39 -8
  108. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  109. package/dist-server/service/twin-audit/command-audit.d.ts +37 -0
  110. package/dist-server/service/twin-audit/command-audit.js +53 -0
  111. package/dist-server/service/twin-audit/command-audit.js.map +1 -0
  112. package/dist-server/service/twin-audit/index.d.ts +4 -0
  113. package/dist-server/service/twin-audit/index.js +8 -0
  114. package/dist-server/service/twin-audit/index.js.map +1 -0
  115. package/dist-server/service/twin-audit/twin-audit-event.d.ts +24 -0
  116. package/dist-server/service/twin-audit/twin-audit-event.js +125 -0
  117. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -0
  118. package/dist-server/service/twin-audit/twin-audit-query.d.ts +4 -0
  119. package/dist-server/service/twin-audit/twin-audit-query.js +76 -0
  120. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -0
  121. package/dist-server/service/twin-control/twin-control-mutation.d.ts +2 -0
  122. package/dist-server/service/twin-control/twin-control-mutation.js +50 -11
  123. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  124. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  125. package/dist-server/service/twin-event/twin-event-keys.js +1 -1
  126. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  127. package/dist-server/service/twin-event/twin-event.d.ts +14 -5
  128. package/dist-server/service/twin-event/twin-event.js +46 -14
  129. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  130. package/dist-server/service/twin-forecast/forecast-metrics.d.ts +13 -0
  131. package/dist-server/service/twin-forecast/forecast-metrics.js +57 -0
  132. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -0
  133. package/dist-server/service/twin-forecast/forecast-tuning.d.ts +12 -0
  134. package/dist-server/service/twin-forecast/forecast-tuning.js +44 -0
  135. package/dist-server/service/twin-forecast/forecast-tuning.js.map +1 -0
  136. package/dist-server/service/twin-forecast/gap-analytics.d.ts +28 -2
  137. package/dist-server/service/twin-forecast/gap-analytics.js +43 -17
  138. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  139. package/dist-server/service/twin-forecast/twin-forecast-query.js +219 -39
  140. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  141. package/dist-server/service/twin-instance/twin-instance.d.ts +38 -5
  142. package/dist-server/service/twin-instance/twin-instance.js +66 -18
  143. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  144. package/dist-server/service/twin-journal/twin-journal-query.d.ts +11 -4
  145. package/dist-server/service/twin-journal/twin-journal-query.js +95 -15
  146. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  147. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +11 -5
  148. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +46 -27
  149. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  150. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  151. package/dist-server/service/twin-metrics/twin-metrics-query.js +63 -1
  152. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  153. package/dist-server/service/twin-model/epcis-coverage.d.ts +14 -0
  154. package/dist-server/service/twin-model/epcis-coverage.js +174 -0
  155. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -0
  156. package/dist-server/service/twin-model/iec61850-coverage.d.ts +14 -0
  157. package/dist-server/service/twin-model/iec61850-coverage.js +98 -0
  158. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -0
  159. package/dist-server/service/twin-model/index.d.ts +13 -0
  160. package/dist-server/service/twin-model/index.js +29 -0
  161. package/dist-server/service/twin-model/index.js.map +1 -0
  162. package/dist-server/service/twin-model/isa95-coverage.d.ts +48 -0
  163. package/dist-server/service/twin-model/isa95-coverage.js +155 -0
  164. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -0
  165. package/dist-server/service/twin-model/project-structure.d.ts +16 -0
  166. package/dist-server/service/twin-model/project-structure.js +173 -0
  167. package/dist-server/service/twin-model/project-structure.js.map +1 -0
  168. package/dist-server/service/twin-model/standard-coverage.d.ts +9 -0
  169. package/dist-server/service/twin-model/standard-coverage.js +37 -0
  170. package/dist-server/service/twin-model/standard-coverage.js.map +1 -0
  171. package/dist-server/service/twin-model/twin-equipment.d.ts +64 -0
  172. package/dist-server/service/twin-model/twin-equipment.js +134 -0
  173. package/dist-server/service/twin-model/twin-equipment.js.map +1 -0
  174. package/dist-server/service/twin-model/twin-lineage-query.d.ts +3 -0
  175. package/dist-server/service/twin-model/twin-lineage-query.js +206 -0
  176. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -0
  177. package/dist-server/service/twin-model/twin-location.d.ts +58 -0
  178. package/dist-server/service/twin-model/twin-location.js +130 -0
  179. package/dist-server/service/twin-model/twin-location.js.map +1 -0
  180. package/dist-server/service/twin-model/twin-model-item-query.d.ts +141 -0
  181. package/dist-server/service/twin-model/twin-model-item-query.js +852 -0
  182. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -0
  183. package/dist-server/service/twin-model/twin-model-mutation.d.ts +57 -0
  184. package/dist-server/service/twin-model/twin-model-mutation.js +490 -0
  185. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -0
  186. package/dist-server/service/twin-model/twin-model-query.d.ts +43 -0
  187. package/dist-server/service/twin-model/twin-model-query.js +551 -0
  188. package/dist-server/service/twin-model/twin-model-query.js.map +1 -0
  189. package/dist-server/service/twin-model/twin-model-tree-query.d.ts +3 -0
  190. package/dist-server/service/twin-model/twin-model-tree-query.js +158 -0
  191. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -0
  192. package/dist-server/service/twin-model/twin-operation.d.ts +61 -0
  193. package/dist-server/service/twin-model/twin-operation.js +137 -0
  194. package/dist-server/service/twin-model/twin-operation.js.map +1 -0
  195. package/dist-server/service/twin-space/move-space.d.ts +65 -0
  196. package/dist-server/service/twin-space/move-space.js +166 -0
  197. package/dist-server/service/twin-space/move-space.js.map +1 -0
  198. package/dist-server/service/twin-space/space-integrity.d.ts +97 -0
  199. package/dist-server/service/twin-space/space-integrity.js +182 -0
  200. package/dist-server/service/twin-space/space-integrity.js.map +1 -0
  201. package/dist-server/service/twin-space/twin-space-area.js +4 -4
  202. package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
  203. package/dist-server/service/twin-space/twin-space-representation.js +6 -6
  204. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  205. package/dist-server/service/twin-space/twin-space-resolver.d.ts +63 -1
  206. package/dist-server/service/twin-space/twin-space-resolver.js +365 -18
  207. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  208. package/dist-server/service/twin-space/twin-space.d.ts +17 -1
  209. package/dist-server/service/twin-space/twin-space.js +15 -5
  210. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  211. package/dist-server/service/twin-state/twin-state-subscription.js +1 -1
  212. package/dist-server/service/twin-state/twin-state-subscription.js.map +1 -1
  213. package/dist-server/service/twin-structure/twin-structure.d.ts +1 -1
  214. package/dist-server/service/twin-structure/twin-structure.js +8 -7
  215. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  216. package/dist-server/service/twin-target/twin-target-resolver.js +21 -4
  217. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  218. package/dist-shared/axis-read.d.ts +12 -0
  219. package/dist-shared/axis-read.js +32 -0
  220. package/dist-shared/axis-read.js.map +1 -0
  221. package/dist-shared/entity-delta.d.ts +29 -0
  222. package/dist-shared/entity-delta.js +287 -0
  223. package/dist-shared/entity-delta.js.map +1 -0
  224. package/dist-shared/kpi-broadcast.d.ts +4 -0
  225. package/dist-shared/kpi-broadcast.js +16 -0
  226. package/dist-shared/kpi-broadcast.js.map +1 -0
  227. package/dist-shared/twin-level.d.ts +23 -0
  228. package/dist-shared/twin-level.js +52 -0
  229. package/dist-shared/twin-level.js.map +1 -0
  230. package/package.json +14 -12
  231. package/server/engine/attention-digest.ts +102 -0
  232. package/server/engine/canonical-ingest.ts +36 -4
  233. package/server/engine/command-routing.ts +67 -0
  234. package/server/engine/energy-topology.ts +189 -0
  235. package/server/engine/index.ts +17 -0
  236. package/server/engine/kpi-baseline.ts +202 -0
  237. package/server/engine/kpi-fold.ts +389 -1
  238. package/server/engine/kpi-query.ts +295 -22
  239. package/server/engine/kpi-target.ts +24 -2
  240. package/server/engine/live-attentions.ts +7 -2
  241. package/server/engine/live-feed-registry.ts +58 -0
  242. package/server/engine/load-meter.ts +384 -0
  243. package/server/engine/local-declarations.ts +700 -0
  244. package/server/engine/model-basis.ts +78 -10
  245. package/server/engine/model-vocabulary.ts +82 -0
  246. package/server/engine/oee-accumulator.ts +34 -1
  247. package/server/engine/operation-basis.ts +100 -0
  248. package/server/engine/property-effects.ts +199 -0
  249. package/server/engine/runtime-key.ts +58 -0
  250. package/server/engine/spec-coverage.ts +3 -3
  251. package/server/engine/state-axes.ts +60 -0
  252. package/server/engine/structure-diff.ts +1 -1
  253. package/server/engine/travel-estimator.ts +11 -6
  254. package/server/engine/twin-engine.ts +1710 -297
  255. package/server/engine/warm-start.ts +207 -22
  256. package/server/index.ts +13 -10
  257. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +43 -0
  258. package/server/migrations/index.ts +7 -0
  259. package/server/service/index.ts +9 -1
  260. package/server/service/reference/discovery-result.ts +95 -0
  261. package/server/service/reference/ingest-space.ts +104 -0
  262. package/server/service/reference/knob-defaults.ts +59 -0
  263. package/server/service/reference/reference-live.ts +121 -6
  264. package/server/service/reference/reference-master.ts +362 -35
  265. package/server/service/reference/reference-resolver.ts +307 -23
  266. package/server/service/reference/template-registry.ts +20 -5
  267. package/server/service/twin-attention/twin-attention-query.ts +43 -6
  268. package/server/service/twin-audit/command-audit.ts +81 -0
  269. package/server/service/twin-audit/index.ts +5 -0
  270. package/server/service/twin-audit/twin-audit-event.ts +112 -0
  271. package/server/service/twin-audit/twin-audit-query.ts +72 -0
  272. package/server/service/twin-control/twin-control-mutation.ts +53 -13
  273. package/server/service/twin-event/twin-event-keys.ts +1 -1
  274. package/server/service/twin-event/twin-event.ts +52 -15
  275. package/server/service/twin-forecast/forecast-metrics.ts +60 -0
  276. package/server/service/twin-forecast/forecast-tuning.ts +40 -0
  277. package/server/service/twin-forecast/gap-analytics.ts +48 -9
  278. package/server/service/twin-forecast/twin-forecast-query.ts +211 -38
  279. package/server/service/twin-instance/twin-instance.ts +127 -22
  280. package/server/service/twin-journal/twin-journal-query.ts +108 -14
  281. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +40 -23
  282. package/server/service/twin-metrics/twin-metrics-query.ts +60 -3
  283. package/server/service/twin-model/epcis-coverage.ts +195 -0
  284. package/server/service/twin-model/iec61850-coverage.ts +117 -0
  285. package/server/service/twin-model/index.ts +25 -0
  286. package/server/service/twin-model/isa95-coverage.ts +190 -0
  287. package/server/service/twin-model/project-structure.ts +199 -0
  288. package/server/service/twin-model/standard-coverage.ts +35 -0
  289. package/server/service/twin-model/twin-equipment.ts +150 -0
  290. package/server/service/twin-model/twin-lineage-query.ts +193 -0
  291. package/server/service/twin-model/twin-location.ts +140 -0
  292. package/server/service/twin-model/twin-model-item-query.ts +848 -0
  293. package/server/service/twin-model/twin-model-mutation.ts +503 -0
  294. package/server/service/twin-model/twin-model-query.ts +537 -0
  295. package/server/service/twin-model/twin-model-tree-query.ts +180 -0
  296. package/server/service/twin-model/twin-operation.ts +150 -0
  297. package/server/service/twin-space/move-space.ts +262 -0
  298. package/server/service/twin-space/space-integrity.ts +293 -0
  299. package/server/service/twin-space/twin-space-area.ts +4 -4
  300. package/server/service/twin-space/twin-space-representation.ts +6 -6
  301. package/server/service/twin-space/twin-space-resolver.ts +365 -22
  302. package/server/service/twin-space/twin-space.ts +40 -6
  303. package/server/service/twin-state/twin-state-subscription.ts +1 -1
  304. package/server/service/twin-structure/twin-structure.ts +10 -7
  305. package/server/service/twin-target/twin-target-resolver.ts +22 -4
  306. package/shared/axis-read.ts +28 -0
  307. package/shared/entity-delta.ts +286 -0
  308. package/shared/kpi-broadcast.ts +13 -0
  309. package/shared/twin-level.ts +48 -0
  310. package/test/adopt-structure-live.test.ts +133 -0
  311. package/test/attention-digest.test.ts +135 -0
  312. package/test/axis-read.test.ts +87 -0
  313. package/test/boot-resume.test.ts +205 -0
  314. package/test/canonical-ingest-vocabularies.test.ts +118 -0
  315. package/test/capability-mapping.test.ts +5 -5
  316. package/test/command-routing.test.ts +61 -0
  317. package/test/declaration-reaches-model.test.ts +178 -0
  318. package/test/discovery-result.test.ts +75 -0
  319. package/test/energy-topology.test.ts +113 -0
  320. package/test/entity-delta.test.ts +195 -25
  321. package/test/event-time-column.test.ts +67 -0
  322. package/test/forecast-metrics.test.ts +71 -0
  323. package/test/forecast-tuning.test.ts +42 -0
  324. package/test/gap-analytics.test.ts +34 -7
  325. package/test/ingest-bench.test.ts +9 -9
  326. package/test/ingest-running-guard.test.ts +136 -0
  327. package/test/ingest-space.test.ts +78 -0
  328. package/test/instance-cache-lifecycle.test.ts +110 -0
  329. package/test/kernel-kind-guard.test.ts +96 -0
  330. package/test/knob-defaults.test.ts +72 -0
  331. package/test/kpi-baseline-db.test.ts +216 -0
  332. package/test/kpi-baseline.test.ts +196 -0
  333. package/test/kpi-fold.test.ts +393 -2
  334. package/test/kpi-query-bench.test.ts +130 -0
  335. package/test/lineage-survives-restart.test.ts +203 -0
  336. package/test/live-feed-registry.test.ts +67 -0
  337. package/test/live-kernel-facts.test.ts +161 -0
  338. package/test/live-mirror-parity.test.ts +54 -14
  339. package/test/load-meter.test.ts +314 -0
  340. package/test/local-declarations.test.ts +554 -0
  341. package/test/master-to-twin.test.ts +128 -36
  342. package/test/model-basis.test.ts +56 -1
  343. package/test/model-vocabulary.test.ts +114 -0
  344. package/test/move-space.test.ts +149 -0
  345. package/test/mutation-gate.test.ts +122 -0
  346. package/test/oee-accumulator.test.ts +90 -7
  347. package/test/operation-basis.test.ts +100 -0
  348. package/test/operations-capability-db.test.ts +165 -0
  349. package/test/project-structure-db.test.ts +226 -0
  350. package/test/projection-reaches-screen.test.ts +272 -0
  351. package/test/property-effects.test.ts +96 -0
  352. package/test/registry-key-guard.test.ts +80 -0
  353. package/test/resync-origin-site.test.ts +77 -0
  354. package/test/runtime-key.test.ts +66 -0
  355. package/test/scale-twin-bench.test.ts +7 -7
  356. package/test/snapshot-freshness.test.ts +60 -0
  357. package/test/space-integrity.test.ts +223 -0
  358. package/test/standard-coverage.test.ts +94 -0
  359. package/test/state-axes.test.ts +74 -0
  360. package/test/streamline-e2e.test.ts +16 -16
  361. package/test/structure-revision-db.test.ts +16 -15
  362. package/test/tenant-registry-db.test.ts +149 -0
  363. package/test/twin-audit.test.ts +83 -0
  364. package/test/twin-model-item-db.test.ts +275 -0
  365. package/test/twin-model-tree-db.test.ts +176 -0
  366. package/test/twin-origin-resync.test.ts +114 -0
  367. package/test/warm-start-seam.test.ts +140 -0
  368. package/test/warm-start.test.ts +223 -4
  369. package/tsconfig.json +6 -1
  370. package/tsconfig.shared.json +23 -0
  371. package/tsconfig.shared.tsbuildinfo +1 -0
  372. package/tsconfig.tsbuildinfo +1 -0
  373. package/dist-server/engine/entity-delta.d.ts +0 -19
  374. package/dist-server/engine/entity-delta.js +0 -161
  375. package/dist-server/engine/entity-delta.js.map +0 -1
  376. package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
  377. package/dist-server/service/twin-event/backfill-keys.js +0 -63
  378. package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
  379. package/dist-server/tsconfig.tsbuildinfo +0 -1
  380. package/server/engine/entity-delta.ts +0 -169
  381. package/server/service/twin-event/backfill-keys.ts +0 -72
@@ -0,0 +1,34 @@
1
+ /** 발견이 왜 아무것도 주지 못했나 — 언어 중립 코드. */
2
+ export type DiscoveryErrorCode = 'reference-not-found' | 'adapter-unknown' | 'discovery-failed' | 'discovery-timeout';
3
+ export interface DiscoveryError {
4
+ code: DiscoveryErrorCode;
5
+ params?: Record<string, string | number>;
6
+ /** 영어 canonical — 로그에 남고, 번역이 없으면 그대로 보인다. */
7
+ message: string;
8
+ }
9
+ export interface DiscoveryResult<S = unknown> {
10
+ /** 발견된 사이트들. 실패했으면 빈 배열이고 `error` 가 이유를 말한다. */
11
+ sites: S[];
12
+ /** 실패 사유. **없으면 성공이다** — 빈 배열 + 사유 없음 = "연결은 됐지만 사이트가 없다". */
13
+ error?: DiscoveryError;
14
+ }
15
+ /** 등록된 레퍼런스가 없다 — 목록이 낡았거나 다른 테넌트의 것이다. */
16
+ export declare const referenceNotFound: (source: string) => DiscoveryError;
17
+ /** 어댑터 종류를 모른다 — 배포에 그 커넥터가 없다(패키지 미설치·오타). */
18
+ export declare const adapterUnknown: (adapterType: string, source: string) => DiscoveryError;
19
+ /** 연결·조회가 실패했다 — 원 시스템이 준 사유를 그대로 싣는다. */
20
+ export declare const discoveryFailed: (source: string, detail: string) => DiscoveryError;
21
+ /** 기다려도 답이 없었다 — 무한히 도는 화면보다 사유가 낫다. */
22
+ export declare const discoveryTimedOut: (source: string, seconds: number) => DiscoveryError;
23
+ /**
24
+ * 어댑터가 준 것을 결과로 접는다.
25
+ *
26
+ * **사이트가 없는 것을 실패로 만들지 않는다** — 연결이 되고 조회가 성공했는데 공장이 하나도 없는
27
+ * 시스템은 있을 수 있다(권한이 좁거나 아직 등록되지 않은 경우). 그것은 사실이고, 화면은 "없다" 고
28
+ * 말해야 한다. 실패로 합치면 사용자가 있지도 않은 문제를 찾는다.
29
+ */
30
+ export declare function discovered<S>(sites: readonly S[] | undefined | null): DiscoveryResult<S>;
31
+ /** 실패를 결과로 접는다 — 사이트는 비어 있다(반쪽 목록을 성공처럼 보이게 하지 않는다). */
32
+ export declare function failed<S>(error: DiscoveryError): DiscoveryResult<S>;
33
+ /** 어댑터가 던진 것에서 사람이 읽을 사유를 뽑는다 — 형태를 가정하지 않는다. */
34
+ export declare function detailOf(e: unknown): string;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ /*
3
+ * 사이트 발견 결과 — **"못 찾았다" 와 "없다" 를 구분한다.** 순수.
4
+ *
5
+ * ── 왜 있나 (2026-08-06) ────────────────────────────────────────────────────
6
+ * 실 시스템을 연결해 "거기 있는 것" 으로 트윈을 만드는 경로에서, 발견이 실패하면 화면이
7
+ * **"사이트를 찾지 못했습니다"** 라고만 말했다. 네 가지가 그 한 문장으로 접혀 있었다.
8
+ *
9
+ * ① 그 레퍼런스가 등록돼 있지 않다 (`!ref → []`)
10
+ * ② 어댑터 종류를 모른다 (`!adapter → []`)
11
+ * ③ 연결·조회가 실패했다 (`catch { return [] }`)
12
+ * ④ 정말로 사이트가 없다 (빈 배열)
13
+ *
14
+ * ①②③ 은 **고칠 수 있는 문제**이고 ④ 는 사실이다. 그런데 화면이 같은 말을 하므로 사용자는 자격이
15
+ * 틀렸는지, 주소가 틀렸는지, 정말 공장이 없는지 알 수 없다 — 실 시스템 연결에서 가장 자주 막히는
16
+ * 자리인데 거기서 아무 단서도 주지 않았다.
17
+ *
18
+ * ── 왜 문장이 아니라 코드인가 ───────────────────────────────────────────────
19
+ * 이 프로젝트가 이미 쓰는 규약을 따른다(주의 신호·커맨드 거절·인제스트 경고와 같은 모양):
20
+ * `code`(언어 중립) + `params`(문장에 끼울 값) + `message`(영어 canonical, 로그·폴백).
21
+ * 화면은 `twin.connect.discover.<code>` 로 사람 말을 만든다.
22
+ *
23
+ * ── 원 시스템의 오류 문장을 감추지 않는다 ────────────────────────────────────
24
+ * 어댑터가 준 사유(자격 거부·타임아웃·404)는 **그 시스템만 아는 사실**이라 우리가 다시 쓸 수 없다.
25
+ * `detail` 로 그대로 실어 보내 화면이 보조 문장으로 보인다 — 원문이 있으면 사용자가 자기 관리자에게
26
+ * 물어볼 수 있고, 없으면 우리 짐작만 남는다.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.discoveryTimedOut = exports.discoveryFailed = exports.adapterUnknown = exports.referenceNotFound = void 0;
30
+ exports.discovered = discovered;
31
+ exports.failed = failed;
32
+ exports.detailOf = detailOf;
33
+ /** 등록된 레퍼런스가 없다 — 목록이 낡았거나 다른 테넌트의 것이다. */
34
+ const referenceNotFound = (source) => ({
35
+ code: 'reference-not-found',
36
+ params: { source },
37
+ message: `reference "${source}" is not registered in this tenant`
38
+ });
39
+ exports.referenceNotFound = referenceNotFound;
40
+ /** 어댑터 종류를 모른다 — 배포에 그 커넥터가 없다(패키지 미설치·오타). */
41
+ const adapterUnknown = (adapterType, source) => ({
42
+ code: 'adapter-unknown',
43
+ params: { adapterType, source },
44
+ message: `no connector of type "${adapterType}" is available for reference "${source}"`
45
+ });
46
+ exports.adapterUnknown = adapterUnknown;
47
+ /** 연결·조회가 실패했다 — 원 시스템이 준 사유를 그대로 싣는다. */
48
+ const discoveryFailed = (source, detail) => ({
49
+ code: 'discovery-failed',
50
+ params: { source, detail },
51
+ message: `could not read sites from "${source}": ${detail}`
52
+ });
53
+ exports.discoveryFailed = discoveryFailed;
54
+ /** 기다려도 답이 없었다 — 무한히 도는 화면보다 사유가 낫다. */
55
+ const discoveryTimedOut = (source, seconds) => ({
56
+ code: 'discovery-timeout',
57
+ params: { source, seconds },
58
+ message: `"${source}" did not answer within ${seconds}s`
59
+ });
60
+ exports.discoveryTimedOut = discoveryTimedOut;
61
+ /**
62
+ * 어댑터가 준 것을 결과로 접는다.
63
+ *
64
+ * **사이트가 없는 것을 실패로 만들지 않는다** — 연결이 되고 조회가 성공했는데 공장이 하나도 없는
65
+ * 시스템은 있을 수 있다(권한이 좁거나 아직 등록되지 않은 경우). 그것은 사실이고, 화면은 "없다" 고
66
+ * 말해야 한다. 실패로 합치면 사용자가 있지도 않은 문제를 찾는다.
67
+ */
68
+ function discovered(sites) {
69
+ return { sites: Array.isArray(sites) ? [...sites] : [] };
70
+ }
71
+ /** 실패를 결과로 접는다 — 사이트는 비어 있다(반쪽 목록을 성공처럼 보이게 하지 않는다). */
72
+ function failed(error) {
73
+ return { sites: [], error };
74
+ }
75
+ /** 어댑터가 던진 것에서 사람이 읽을 사유를 뽑는다 — 형태를 가정하지 않는다. */
76
+ function detailOf(e) {
77
+ if (e instanceof Error && e.message)
78
+ return e.message;
79
+ if (typeof e === 'string' && e)
80
+ return e;
81
+ /* 무엇이 왔는지 모르면 그렇게 말한다 — 빈 문장으로 "이유 없음" 처럼 보이게 하지 않는다. */
82
+ return 'the connector failed without a message';
83
+ }
84
+ //# sourceMappingURL=discovery-result.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery-result.js","sourceRoot":"","sources":["../../../server/service/reference/discovery-result.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;;;AAsDH,gCAEC;AAGD,wBAEC;AAGD,4BAKC;AAlDD,2CAA2C;AACpC,MAAM,iBAAiB,GAAG,CAAC,MAAc,EAAkB,EAAE,CAAC,CAAC;IACpE,IAAI,EAAE,qBAAqB;IAC3B,MAAM,EAAE,EAAE,MAAM,EAAE;IAClB,OAAO,EAAE,cAAc,MAAM,oCAAoC;CAClE,CAAC,CAAA;AAJW,QAAA,iBAAiB,qBAI5B;AAEF,+CAA+C;AACxC,MAAM,cAAc,GAAG,CAAC,WAAmB,EAAE,MAAc,EAAkB,EAAE,CAAC,CAAC;IACtF,IAAI,EAAE,iBAAiB;IACvB,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE;IAC/B,OAAO,EAAE,yBAAyB,WAAW,iCAAiC,MAAM,GAAG;CACxF,CAAC,CAAA;AAJW,QAAA,cAAc,kBAIzB;AAEF,0CAA0C;AACnC,MAAM,eAAe,GAAG,CAAC,MAAc,EAAE,MAAc,EAAkB,EAAE,CAAC,CAAC;IAClF,IAAI,EAAE,kBAAkB;IACxB,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;IAC1B,OAAO,EAAE,8BAA8B,MAAM,MAAM,MAAM,EAAE;CAC5D,CAAC,CAAA;AAJW,QAAA,eAAe,mBAI1B;AAEF,wCAAwC;AACjC,MAAM,iBAAiB,GAAG,CAAC,MAAc,EAAE,OAAe,EAAkB,EAAE,CAAC,CAAC;IACrF,IAAI,EAAE,mBAAmB;IACzB,MAAM,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;IAC3B,OAAO,EAAE,IAAI,MAAM,2BAA2B,OAAO,GAAG;CACzD,CAAC,CAAA;AAJW,QAAA,iBAAiB,qBAI5B;AAEF;;;;;;GAMG;AACH,SAAgB,UAAU,CAAI,KAAsC;IAClE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAA;AAC1D,CAAC;AAED,wDAAwD;AACxD,SAAgB,MAAM,CAAI,KAAqB;IAC7C,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,CAAA;AAC7B,CAAC;AAED,iDAAiD;AACjD,SAAgB,QAAQ,CAAC,CAAU;IACjC,IAAI,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,OAAO;QAAE,OAAO,CAAC,CAAC,OAAO,CAAA;IACrD,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC;QAAE,OAAO,CAAC,CAAA;IACxC,yDAAyD;IACzD,OAAO,wCAAwC,CAAA;AACjD,CAAC","sourcesContent":["/*\n * 사이트 발견 결과 — **\"못 찾았다\" 와 \"없다\" 를 구분한다.** 순수.\n *\n * ── 왜 있나 (2026-08-06) ────────────────────────────────────────────────────\n * 실 시스템을 연결해 \"거기 있는 것\" 으로 트윈을 만드는 경로에서, 발견이 실패하면 화면이\n * **\"사이트를 찾지 못했습니다\"** 라고만 말했다. 네 가지가 그 한 문장으로 접혀 있었다.\n *\n * ① 그 레퍼런스가 등록돼 있지 않다 (`!ref → []`)\n * ② 어댑터 종류를 모른다 (`!adapter → []`)\n * ③ 연결·조회가 실패했다 (`catch { return [] }`)\n * ④ 정말로 사이트가 없다 (빈 배열)\n *\n * ①②③ 은 **고칠 수 있는 문제**이고 ④ 는 사실이다. 그런데 화면이 같은 말을 하므로 사용자는 자격이\n * 틀렸는지, 주소가 틀렸는지, 정말 공장이 없는지 알 수 없다 — 실 시스템 연결에서 가장 자주 막히는\n * 자리인데 거기서 아무 단서도 주지 않았다.\n *\n * ── 왜 문장이 아니라 코드인가 ───────────────────────────────────────────────\n * 이 프로젝트가 이미 쓰는 규약을 따른다(주의 신호·커맨드 거절·인제스트 경고와 같은 모양):\n * `code`(언어 중립) + `params`(문장에 끼울 값) + `message`(영어 canonical, 로그·폴백).\n * 화면은 `twin.connect.discover.<code>` 로 사람 말을 만든다.\n *\n * ── 원 시스템의 오류 문장을 감추지 않는다 ────────────────────────────────────\n * 어댑터가 준 사유(자격 거부·타임아웃·404)는 **그 시스템만 아는 사실**이라 우리가 다시 쓸 수 없다.\n * `detail` 로 그대로 실어 보내 화면이 보조 문장으로 보인다 — 원문이 있으면 사용자가 자기 관리자에게\n * 물어볼 수 있고, 없으면 우리 짐작만 남는다.\n */\n\n/** 발견이 왜 아무것도 주지 못했나 — 언어 중립 코드. */\nexport type DiscoveryErrorCode = 'reference-not-found' | 'adapter-unknown' | 'discovery-failed' | 'discovery-timeout'\n\nexport interface DiscoveryError {\n code: DiscoveryErrorCode\n params?: Record<string, string | number>\n /** 영어 canonical — 로그에 남고, 번역이 없으면 그대로 보인다. */\n message: string\n}\n\nexport interface DiscoveryResult<S = unknown> {\n /** 발견된 사이트들. 실패했으면 빈 배열이고 `error` 가 이유를 말한다. */\n sites: S[]\n /** 실패 사유. **없으면 성공이다** — 빈 배열 + 사유 없음 = \"연결은 됐지만 사이트가 없다\". */\n error?: DiscoveryError\n}\n\n/** 등록된 레퍼런스가 없다 — 목록이 낡았거나 다른 테넌트의 것이다. */\nexport const referenceNotFound = (source: string): DiscoveryError => ({\n code: 'reference-not-found',\n params: { source },\n message: `reference \"${source}\" is not registered in this tenant`\n})\n\n/** 어댑터 종류를 모른다 — 배포에 그 커넥터가 없다(패키지 미설치·오타). */\nexport const adapterUnknown = (adapterType: string, source: string): DiscoveryError => ({\n code: 'adapter-unknown',\n params: { adapterType, source },\n message: `no connector of type \"${adapterType}\" is available for reference \"${source}\"`\n})\n\n/** 연결·조회가 실패했다 — 원 시스템이 준 사유를 그대로 싣는다. */\nexport const discoveryFailed = (source: string, detail: string): DiscoveryError => ({\n code: 'discovery-failed',\n params: { source, detail },\n message: `could not read sites from \"${source}\": ${detail}`\n})\n\n/** 기다려도 답이 없었다 — 무한히 도는 화면보다 사유가 낫다. */\nexport const discoveryTimedOut = (source: string, seconds: number): DiscoveryError => ({\n code: 'discovery-timeout',\n params: { source, seconds },\n message: `\"${source}\" did not answer within ${seconds}s`\n})\n\n/**\n * 어댑터가 준 것을 결과로 접는다.\n *\n * **사이트가 없는 것을 실패로 만들지 않는다** — 연결이 되고 조회가 성공했는데 공장이 하나도 없는\n * 시스템은 있을 수 있다(권한이 좁거나 아직 등록되지 않은 경우). 그것은 사실이고, 화면은 \"없다\" 고\n * 말해야 한다. 실패로 합치면 사용자가 있지도 않은 문제를 찾는다.\n */\nexport function discovered<S>(sites: readonly S[] | undefined | null): DiscoveryResult<S> {\n return { sites: Array.isArray(sites) ? [...sites] : [] }\n}\n\n/** 실패를 결과로 접는다 — 사이트는 비어 있다(반쪽 목록을 성공처럼 보이게 하지 않는다). */\nexport function failed<S>(error: DiscoveryError): DiscoveryResult<S> {\n return { sites: [], error }\n}\n\n/** 어댑터가 던진 것에서 사람이 읽을 사유를 뽑는다 — 형태를 가정하지 않는다. */\nexport function detailOf(e: unknown): string {\n if (e instanceof Error && e.message) return e.message\n if (typeof e === 'string' && e) return e\n /* 무엇이 왔는지 모르면 그렇게 말한다 — 빈 문장으로 \"이유 없음\" 처럼 보이게 하지 않는다. */\n return 'the connector failed without a message'\n}\n"]}
@@ -0,0 +1,53 @@
1
+ /** 공간 판정 결과. */
2
+ export interface IngestSpaceChoice {
3
+ /** 실제로 쓸 공간 id. */
4
+ spaceId: string;
5
+ /** 사용자가 기존 현장에 합치기를 골랐나(마스터가 파생한 공간과 다른 곳으로 들어가나). */
6
+ joined: boolean;
7
+ }
8
+ /** 거절 사유 — 호출측이 문장을 만들 때 쓴다(언어 중립 코드). */
9
+ export declare const INGEST_SPACE_NOT_FOUND = "space-not-found";
10
+ /**
11
+ * 요청받은 공간과 마스터가 파생한 공간 중 무엇으로 들어갈지 정한다.
12
+ *
13
+ * @param masterSpaceId 마스터에서 파생된 공간 id(요청이 없을 때의 기본).
14
+ * @param requested 사용자가 고른 기존 공간 id. 없으면 빈 값/undefined.
15
+ * @param requestedExists 그 공간이 이 테넌트에 실제로 있나(호출측이 조회해 넘긴다).
16
+ * @throws 요청한 공간이 없으면 — 조용히 새로 만들지 않는다.
17
+ */
18
+ export declare function resolveIngestSpace(masterSpaceId: string, requested: string | undefined | null, requestedExists: boolean): IngestSpaceChoice;
19
+ /**
20
+ * 실 시스템 import 가 현장을 고를 때의 거절 판정 — **순수**.
21
+ *
22
+ * 레퍼런스 하나는 사이트를 여럿 발견할 수 있고(`discoverSites`), 그 사이트마다 트윈이 하나씩 생긴다.
23
+ * 그런데 "이 현장에 더한다" 는 **한 곳**을 가리키는 말이다. 사이트 여럿을 한 공간에 넣으면 서로 다른
24
+ * 현장의 구역·랜드마크가 id 로 합쳐지고(`unionById`), 그 사실을 알아차릴 때는 이미 이벤트가 양쪽에
25
+ * 쌓여 있다 — 손으로 되돌릴 수 없는 종류다. 그래서 **묻지 않고 합치지 않고, 거절한다.**
26
+ *
27
+ * 공간을 고르지 않으면 지금까지의 거동 그대로다: 사이트마다 마스터가 파생한 공간으로 들어간다.
28
+ */
29
+ export type ImportSpaceRefusalCode = 'space-with-multiple-sites';
30
+ export interface ImportSpaceRefusal {
31
+ code: ImportSpaceRefusalCode;
32
+ params?: Record<string, string | number>;
33
+ /** 영어 canonical — 로그에 남고, 번역이 없으면 그대로 보인다(발견 오류와 같은 규약). */
34
+ message: string;
35
+ }
36
+ /**
37
+ * @param siteCount 실제로 만들 사이트 수(필터 적용 후).
38
+ * @param requested 사용자가 고른 기존 공간 id. 없으면 판정할 것이 없다.
39
+ * @returns 거절 사유. 없으면 진행해도 된다.
40
+ */
41
+ export declare function refuseImportSpace(siteCount: number, requested?: string | null): ImportSpaceRefusal | undefined;
42
+ /** 구역·랜드마크처럼 id 로 합쳐지는 것들. */
43
+ interface Identified {
44
+ id?: string;
45
+ }
46
+ /**
47
+ * 합치는 쪽과 기존 쪽이 같은 id 를 쓰는 것들을 찾아 경고 문장을 만든다.
48
+ *
49
+ * 정당한 경우(같은 현장의 같은 구역)와 사고(서로 다른 구역이 우연히 같은 이름)를 기계가 가릴 수는
50
+ * 없다. 구분할 수 없는 것을 고르지 않고, **무엇이 합쳐졌는지 말한다.**
51
+ */
52
+ export declare function collidingIds(existing: readonly Identified[] | undefined | null, incoming: readonly Identified[] | undefined | null): string[];
53
+ export {};
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /*
3
+ * 인제스트가 어느 공간으로 들어갈지 정한다 — **순수**.
4
+ *
5
+ * ── 왜 있나 (2026-08-05) ────────────────────────────────────────────────────
6
+ * 한 현실(공장·물류센터)을 여러 렌즈가 비춘다: 창고(WMS)·공정(MES)·야드(YMS) 가 같은 부지에 있다.
7
+ * 엔진은 이미 그것을 지원한다 — 같은 `spaceId` 로 들어오면 공간 content 를 병합하고 기존 값을 이긴다.
8
+ *
9
+ * 그런데 **만드는 흐름에 그 선택이 없었다.** 템플릿은 `space.spaceId` 를 정하지 않으므로 항상
10
+ * `<source>-site` 로 파생돼, 새 트윈은 늘 새 현장처럼 생겼다. 데이터로는 가능한 일이 저작으로는
11
+ * 불가능했다.
12
+ *
13
+ * ── 없는 공간에 조용히 새 공간을 만들지 않는다 ────────────────────────────────
14
+ * "이 현장에 더한다" 는 요청이 오타·낡은 목록으로 빗나갔을 때 새 공간을 만들어 주면, 사용자는
15
+ * 합쳤다고 믿고 화면에서는 따로 놀게 된다 — 나중에 두 현장을 손으로 합쳐야 하고, 그때는 이미
16
+ * 이벤트가 양쪽에 쌓여 있다. 그래서 **모르는 공간이면 거절한다.**
17
+ *
18
+ * ── 같은 id 를 쓰는 구역은 조용히 합쳐지므로 알린다 ──────────────────────────
19
+ * 공간 content 의 구역·랜드마크는 id 로 합집합을 만든다(`unionById`). 두 렌즈가 우연히 같은 구역
20
+ * id(`a-yard` 따위)를 쓰면 서로 다른 구역이 하나로 접힌다. 막을 일은 아니다(같은 현장의 같은 구역을
21
+ * 가리키는 정당한 경우가 많다) — 다만 **모르고 지나가지 않게** 경고로 돌려준다.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.INGEST_SPACE_NOT_FOUND = void 0;
25
+ exports.resolveIngestSpace = resolveIngestSpace;
26
+ exports.refuseImportSpace = refuseImportSpace;
27
+ exports.collidingIds = collidingIds;
28
+ /** 거절 사유 — 호출측이 문장을 만들 때 쓴다(언어 중립 코드). */
29
+ exports.INGEST_SPACE_NOT_FOUND = 'space-not-found';
30
+ /**
31
+ * 요청받은 공간과 마스터가 파생한 공간 중 무엇으로 들어갈지 정한다.
32
+ *
33
+ * @param masterSpaceId 마스터에서 파생된 공간 id(요청이 없을 때의 기본).
34
+ * @param requested 사용자가 고른 기존 공간 id. 없으면 빈 값/undefined.
35
+ * @param requestedExists 그 공간이 이 테넌트에 실제로 있나(호출측이 조회해 넘긴다).
36
+ * @throws 요청한 공간이 없으면 — 조용히 새로 만들지 않는다.
37
+ */
38
+ function resolveIngestSpace(masterSpaceId, requested, requestedExists) {
39
+ const want = (requested ?? '').trim();
40
+ if (!want)
41
+ return { spaceId: masterSpaceId, joined: false };
42
+ if (!requestedExists) {
43
+ const e = new Error(`space "${want}" not found — cannot co-locate into a space that does not exist`);
44
+ e.code = exports.INGEST_SPACE_NOT_FOUND;
45
+ throw e;
46
+ }
47
+ /* 마스터가 이미 그 공간을 가리키면 합치기가 아니다 — 같은 공간의 재인제스트다. */
48
+ return { spaceId: want, joined: want !== masterSpaceId };
49
+ }
50
+ /**
51
+ * @param siteCount 실제로 만들 사이트 수(필터 적용 후).
52
+ * @param requested 사용자가 고른 기존 공간 id. 없으면 판정할 것이 없다.
53
+ * @returns 거절 사유. 없으면 진행해도 된다.
54
+ */
55
+ function refuseImportSpace(siteCount, requested) {
56
+ const want = (requested ?? '').trim();
57
+ if (!want || siteCount <= 1)
58
+ return undefined;
59
+ return {
60
+ code: 'space-with-multiple-sites',
61
+ params: { spaceId: want, count: siteCount },
62
+ message: `cannot co-locate ${siteCount} sites into space "${want}" — pick one site, or omit the space so each site gets its own`
63
+ };
64
+ }
65
+ /**
66
+ * 합치는 쪽과 기존 쪽이 같은 id 를 쓰는 것들을 찾아 경고 문장을 만든다.
67
+ *
68
+ * 정당한 경우(같은 현장의 같은 구역)와 사고(서로 다른 구역이 우연히 같은 이름)를 기계가 가릴 수는
69
+ * 없다. 구분할 수 없는 것을 고르지 않고, **무엇이 합쳐졌는지 말한다.**
70
+ */
71
+ function collidingIds(existing, incoming) {
72
+ const have = new Set((existing ?? []).map(x => x?.id).filter(Boolean));
73
+ const hits = new Set();
74
+ for (const x of incoming ?? [])
75
+ if (x?.id && have.has(x.id))
76
+ hits.add(x.id);
77
+ return [...hits].sort();
78
+ }
79
+ //# sourceMappingURL=ingest-space.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest-space.js","sourceRoot":"","sources":["../../../server/service/reference/ingest-space.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;;AAqBH,gDAUC;AA0BD,8CAQC;AAaD,oCAKC;AAzED,0CAA0C;AAC7B,QAAA,sBAAsB,GAAG,iBAAiB,CAAA;AAEvD;;;;;;;GAOG;AACH,SAAgB,kBAAkB,CAAC,aAAqB,EAAE,SAAoC,EAAE,eAAwB;IACtH,MAAM,IAAI,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IACrC,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;IAC3D,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,MAAM,CAAC,GAAG,IAAI,KAAK,CAAC,UAAU,IAAI,iEAAiE,CAAC,CACnG;QAAC,CAAS,CAAC,IAAI,GAAG,8BAAsB,CAAA;QACzC,MAAM,CAAC,CAAA;IACT,CAAC;IACD,kDAAkD;IAClD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,KAAK,aAAa,EAAE,CAAA;AAC1D,CAAC;AAqBD;;;;GAIG;AACH,SAAgB,iBAAiB,CAAC,SAAiB,EAAE,SAAyB;IAC5E,MAAM,IAAI,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IACrC,IAAI,CAAC,IAAI,IAAI,SAAS,IAAI,CAAC;QAAE,OAAO,SAAS,CAAA;IAC7C,OAAO;QACL,IAAI,EAAE,2BAA2B;QACjC,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE;QAC3C,OAAO,EAAE,oBAAoB,SAAS,sBAAsB,IAAI,gEAAgE;KACjI,CAAA;AACH,CAAC;AAOD;;;;;GAKG;AACH,SAAgB,YAAY,CAAC,QAAkD,EAAE,QAAkD;IACjI,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAa,CAAC,CAAA;IAClF,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,KAAK,MAAM,CAAC,IAAI,QAAQ,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAC3E,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,EAAE,CAAA;AACzB,CAAC","sourcesContent":["/*\n * 인제스트가 어느 공간으로 들어갈지 정한다 — **순수**.\n *\n * ── 왜 있나 (2026-08-05) ────────────────────────────────────────────────────\n * 한 현실(공장·물류센터)을 여러 렌즈가 비춘다: 창고(WMS)·공정(MES)·야드(YMS) 가 같은 부지에 있다.\n * 엔진은 이미 그것을 지원한다 — 같은 `spaceId` 로 들어오면 공간 content 를 병합하고 기존 값을 이긴다.\n *\n * 그런데 **만드는 흐름에 그 선택이 없었다.** 템플릿은 `space.spaceId` 를 정하지 않으므로 항상\n * `<source>-site` 로 파생돼, 새 트윈은 늘 새 현장처럼 생겼다. 데이터로는 가능한 일이 저작으로는\n * 불가능했다.\n *\n * ── 없는 공간에 조용히 새 공간을 만들지 않는다 ────────────────────────────────\n * \"이 현장에 더한다\" 는 요청이 오타·낡은 목록으로 빗나갔을 때 새 공간을 만들어 주면, 사용자는\n * 합쳤다고 믿고 화면에서는 따로 놀게 된다 — 나중에 두 현장을 손으로 합쳐야 하고, 그때는 이미\n * 이벤트가 양쪽에 쌓여 있다. 그래서 **모르는 공간이면 거절한다.**\n *\n * ── 같은 id 를 쓰는 구역은 조용히 합쳐지므로 알린다 ──────────────────────────\n * 공간 content 의 구역·랜드마크는 id 로 합집합을 만든다(`unionById`). 두 렌즈가 우연히 같은 구역\n * id(`a-yard` 따위)를 쓰면 서로 다른 구역이 하나로 접힌다. 막을 일은 아니다(같은 현장의 같은 구역을\n * 가리키는 정당한 경우가 많다) — 다만 **모르고 지나가지 않게** 경고로 돌려준다.\n */\n\n/** 공간 판정 결과. */\nexport interface IngestSpaceChoice {\n /** 실제로 쓸 공간 id. */\n spaceId: string\n /** 사용자가 기존 현장에 합치기를 골랐나(마스터가 파생한 공간과 다른 곳으로 들어가나). */\n joined: boolean\n}\n\n/** 거절 사유 — 호출측이 문장을 만들 때 쓴다(언어 중립 코드). */\nexport const INGEST_SPACE_NOT_FOUND = 'space-not-found'\n\n/**\n * 요청받은 공간과 마스터가 파생한 공간 중 무엇으로 들어갈지 정한다.\n *\n * @param masterSpaceId 마스터에서 파생된 공간 id(요청이 없을 때의 기본).\n * @param requested 사용자가 고른 기존 공간 id. 없으면 빈 값/undefined.\n * @param requestedExists 그 공간이 이 테넌트에 실제로 있나(호출측이 조회해 넘긴다).\n * @throws 요청한 공간이 없으면 — 조용히 새로 만들지 않는다.\n */\nexport function resolveIngestSpace(masterSpaceId: string, requested: string | undefined | null, requestedExists: boolean): IngestSpaceChoice {\n const want = (requested ?? '').trim()\n if (!want) return { spaceId: masterSpaceId, joined: false }\n if (!requestedExists) {\n const e = new Error(`space \"${want}\" not found — cannot co-locate into a space that does not exist`)\n ;(e as any).code = INGEST_SPACE_NOT_FOUND\n throw e\n }\n /* 마스터가 이미 그 공간을 가리키면 합치기가 아니다 — 같은 공간의 재인제스트다. */\n return { spaceId: want, joined: want !== masterSpaceId }\n}\n\n/**\n * 실 시스템 import 가 현장을 고를 때의 거절 판정 — **순수**.\n *\n * 레퍼런스 하나는 사이트를 여럿 발견할 수 있고(`discoverSites`), 그 사이트마다 트윈이 하나씩 생긴다.\n * 그런데 \"이 현장에 더한다\" 는 **한 곳**을 가리키는 말이다. 사이트 여럿을 한 공간에 넣으면 서로 다른\n * 현장의 구역·랜드마크가 id 로 합쳐지고(`unionById`), 그 사실을 알아차릴 때는 이미 이벤트가 양쪽에\n * 쌓여 있다 — 손으로 되돌릴 수 없는 종류다. 그래서 **묻지 않고 합치지 않고, 거절한다.**\n *\n * 공간을 고르지 않으면 지금까지의 거동 그대로다: 사이트마다 마스터가 파생한 공간으로 들어간다.\n */\nexport type ImportSpaceRefusalCode = 'space-with-multiple-sites'\n\nexport interface ImportSpaceRefusal {\n code: ImportSpaceRefusalCode\n params?: Record<string, string | number>\n /** 영어 canonical — 로그에 남고, 번역이 없으면 그대로 보인다(발견 오류와 같은 규약). */\n message: string\n}\n\n/**\n * @param siteCount 실제로 만들 사이트 수(필터 적용 후).\n * @param requested 사용자가 고른 기존 공간 id. 없으면 판정할 것이 없다.\n * @returns 거절 사유. 없으면 진행해도 된다.\n */\nexport function refuseImportSpace(siteCount: number, requested?: string | null): ImportSpaceRefusal | undefined {\n const want = (requested ?? '').trim()\n if (!want || siteCount <= 1) return undefined\n return {\n code: 'space-with-multiple-sites',\n params: { spaceId: want, count: siteCount },\n message: `cannot co-locate ${siteCount} sites into space \"${want}\" — pick one site, or omit the space so each site gets its own`\n }\n}\n\n/** 구역·랜드마크처럼 id 로 합쳐지는 것들. */\ninterface Identified {\n id?: string\n}\n\n/**\n * 합치는 쪽과 기존 쪽이 같은 id 를 쓰는 것들을 찾아 경고 문장을 만든다.\n *\n * 정당한 경우(같은 현장의 같은 구역)와 사고(서로 다른 구역이 우연히 같은 이름)를 기계가 가릴 수는\n * 없다. 구분할 수 없는 것을 고르지 않고, **무엇이 합쳐졌는지 말한다.**\n */\nexport function collidingIds(existing: readonly Identified[] | undefined | null, incoming: readonly Identified[] | undefined | null): string[] {\n const have = new Set((existing ?? []).map(x => x?.id).filter(Boolean) as string[])\n const hits = new Set<string>()\n for (const x of incoming ?? []) if (x?.id && have.has(x.id)) hits.add(x.id)\n return [...hits].sort()\n}\n"]}
@@ -0,0 +1,20 @@
1
+ /** 템플릿 노브 — 기본값 적용에 필요한 만큼만 본다. */
2
+ export interface KnobLike {
3
+ key?: string;
4
+ default?: unknown;
5
+ }
6
+ /**
7
+ * 빠진 노브를 기본값으로 채운다.
8
+ *
9
+ * @param knobs 템플릿이 선언한 노브들(없으면 파라미터를 그대로 돌려준다).
10
+ * @param params 호출자가 준 파라미터.
11
+ * @returns 새 객체 — 입력을 바꾸지 않는다(호출자의 객체가 몰래 변하면 추적이 어렵다).
12
+ */
13
+ export declare function applyKnobDefaults(knobs: readonly KnobLike[] | undefined | null, params: Record<string, unknown> | undefined | null): Record<string, unknown>;
14
+ /**
15
+ * 기본값으로 채운 노브 이름들 — 호출자에게 "무엇을 내가 정해 줬는지" 알릴 때 쓴다.
16
+ *
17
+ * 조용히 채우는 것도 조용히 비우는 것만큼 위험하다: 사용자가 준 적 없는 값으로 트윈이 만들어졌는데
18
+ * 아무 말이 없으면, 나중에 "왜 이 수치인가" 를 아무도 답할 수 없다.
19
+ */
20
+ export declare function filledKnobKeys(knobs: readonly KnobLike[] | undefined | null, params: Record<string, unknown> | undefined | null): string[];
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /*
3
+ * 노브 기본값 적용 — **순수**. "안 준 값은 템플릿이 선언한 기본값으로."
4
+ *
5
+ * ── 왜 있나 (2026-08-05) ────────────────────────────────────────────────────
6
+ * 템플릿은 노브마다 기본값을 선언한다(`knobs[].default`). 그런데 그 기본값을 채우는 일을 **위자드만**
7
+ * 하고 있었다 — 서버는 받은 파라미터를 그대로 `build(p)` 에 넘긴다. 그래서 파라미터를 일부만 주고
8
+ * 부르면(AI·스크립트·API 호출자) 템플릿 안에서 `Number(p.inboundRate) || 0` 이 **0** 이 되고,
9
+ * 발생률 0 = 도착 없음 = **아무것도 하지 않는 트윈**이 조용히 만들어진다.
10
+ *
11
+ * 실제로 그 함정을 밟았다: 유통가공 창고를 파라미터 일부만 주고 만들었더니 이벤트가 0건이었고,
12
+ * 처음에는 템플릿이 깨진 것으로 오진했다. 기본값을 아는 곳이 계약(노브 스키마)인데 그것을 적용하는
13
+ * 곳이 화면뿐이면, 화면을 거치지 않는 모든 경로가 함정이 된다.
14
+ *
15
+ * ── 0 을 기본값으로 덮지 않는다 ─────────────────────────────────────────────
16
+ * 발생률 `0` 은 "도착 없음" 이라는 **뜻 있는 값**이다. 없는 것과 0 은 다르다 — falsy 로 판정해 덮으면
17
+ * "자극 없이 구조만 보겠다" 는 선언이 조용히 뒤집힌다. 그래서 **키가 없을 때만** 채운다.
18
+ * `null` 은 없는 것으로 본다(JSON·GraphQL 경로에서 "주지 않음" 이 null 로 오는 일이 흔하다).
19
+ *
20
+ * ── 선언 밖 파라미터는 손대지 않는다 ────────────────────────────────────────
21
+ * 템플릿은 노브에 없는 값도 읽는다(예: `geo` 대신 `lat`/`lon` 폴백). 모르는 키를 걷어내면 그 경로가
22
+ * 조용히 끊긴다. 그대로 통과시킨다.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.applyKnobDefaults = applyKnobDefaults;
26
+ exports.filledKnobKeys = filledKnobKeys;
27
+ /**
28
+ * 빠진 노브를 기본값으로 채운다.
29
+ *
30
+ * @param knobs 템플릿이 선언한 노브들(없으면 파라미터를 그대로 돌려준다).
31
+ * @param params 호출자가 준 파라미터.
32
+ * @returns 새 객체 — 입력을 바꾸지 않는다(호출자의 객체가 몰래 변하면 추적이 어렵다).
33
+ */
34
+ function applyKnobDefaults(knobs, params) {
35
+ const out = { ...(params ?? {}) };
36
+ for (const k of knobs ?? []) {
37
+ if (!k?.key)
38
+ continue;
39
+ if (k.default === undefined)
40
+ continue; // 기본값을 선언하지 않은 노브는 채울 것이 없다
41
+ const given = out[k.key];
42
+ if (given === undefined || given === null)
43
+ out[k.key] = k.default;
44
+ }
45
+ return out;
46
+ }
47
+ /**
48
+ * 기본값으로 채운 노브 이름들 — 호출자에게 "무엇을 내가 정해 줬는지" 알릴 때 쓴다.
49
+ *
50
+ * 조용히 채우는 것도 조용히 비우는 것만큼 위험하다: 사용자가 준 적 없는 값으로 트윈이 만들어졌는데
51
+ * 아무 말이 없으면, 나중에 "왜 이 수치인가" 를 아무도 답할 수 없다.
52
+ */
53
+ function filledKnobKeys(knobs, params) {
54
+ const given = params ?? {};
55
+ return (knobs ?? [])
56
+ .filter(k => k?.key && k.default !== undefined && (given[k.key] === undefined || given[k.key] === null))
57
+ .map(k => k.key);
58
+ }
59
+ //# sourceMappingURL=knob-defaults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knob-defaults.js","sourceRoot":"","sources":["../../../server/service/reference/knob-defaults.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;AAeH,8CASC;AAQD,wCAKC;AA7BD;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,KAA6C,EAAE,MAAkD;IACjI,MAAM,GAAG,GAA4B,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAA;IAC1D,KAAK,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,EAAE,CAAC;QAC5B,IAAI,CAAC,CAAC,EAAE,GAAG;YAAE,SAAQ;QACrB,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS;YAAE,SAAQ,CAAC,4BAA4B;QAClE,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;QACxB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAA;IACnE,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;;GAKG;AACH,SAAgB,cAAc,CAAC,KAA6C,EAAE,MAAkD;IAC9H,MAAM,KAAK,GAAG,MAAM,IAAI,EAAE,CAAA;IAC1B,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;SACjB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAI,CAAC,KAAK,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC,GAAI,CAAC,KAAK,IAAI,CAAC,CAAC;SACzG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAI,CAAC,CAAA;AACrB,CAAC","sourcesContent":["/*\n * 노브 기본값 적용 — **순수**. \"안 준 값은 템플릿이 선언한 기본값으로.\"\n *\n * ── 왜 있나 (2026-08-05) ────────────────────────────────────────────────────\n * 템플릿은 노브마다 기본값을 선언한다(`knobs[].default`). 그런데 그 기본값을 채우는 일을 **위자드만**\n * 하고 있었다 — 서버는 받은 파라미터를 그대로 `build(p)` 에 넘긴다. 그래서 파라미터를 일부만 주고\n * 부르면(AI·스크립트·API 호출자) 템플릿 안에서 `Number(p.inboundRate) || 0` 이 **0** 이 되고,\n * 발생률 0 = 도착 없음 = **아무것도 하지 않는 트윈**이 조용히 만들어진다.\n *\n * 실제로 그 함정을 밟았다: 유통가공 창고를 파라미터 일부만 주고 만들었더니 이벤트가 0건이었고,\n * 처음에는 템플릿이 깨진 것으로 오진했다. 기본값을 아는 곳이 계약(노브 스키마)인데 그것을 적용하는\n * 곳이 화면뿐이면, 화면을 거치지 않는 모든 경로가 함정이 된다.\n *\n * ── 0 을 기본값으로 덮지 않는다 ─────────────────────────────────────────────\n * 발생률 `0` 은 \"도착 없음\" 이라는 **뜻 있는 값**이다. 없는 것과 0 은 다르다 — falsy 로 판정해 덮으면\n * \"자극 없이 구조만 보겠다\" 는 선언이 조용히 뒤집힌다. 그래서 **키가 없을 때만** 채운다.\n * `null` 은 없는 것으로 본다(JSON·GraphQL 경로에서 \"주지 않음\" 이 null 로 오는 일이 흔하다).\n *\n * ── 선언 밖 파라미터는 손대지 않는다 ────────────────────────────────────────\n * 템플릿은 노브에 없는 값도 읽는다(예: `geo` 대신 `lat`/`lon` 폴백). 모르는 키를 걷어내면 그 경로가\n * 조용히 끊긴다. 그대로 통과시킨다.\n */\n\n/** 템플릿 노브 — 기본값 적용에 필요한 만큼만 본다. */\nexport interface KnobLike {\n key?: string\n default?: unknown\n}\n\n/**\n * 빠진 노브를 기본값으로 채운다.\n *\n * @param knobs 템플릿이 선언한 노브들(없으면 파라미터를 그대로 돌려준다).\n * @param params 호출자가 준 파라미터.\n * @returns 새 객체 — 입력을 바꾸지 않는다(호출자의 객체가 몰래 변하면 추적이 어렵다).\n */\nexport function applyKnobDefaults(knobs: readonly KnobLike[] | undefined | null, params: Record<string, unknown> | undefined | null): Record<string, unknown> {\n const out: Record<string, unknown> = { ...(params ?? {}) }\n for (const k of knobs ?? []) {\n if (!k?.key) continue\n if (k.default === undefined) continue // 기본값을 선언하지 않은 노브는 채울 것이 없다\n const given = out[k.key]\n if (given === undefined || given === null) out[k.key] = k.default\n }\n return out\n}\n\n/**\n * 기본값으로 채운 노브 이름들 — 호출자에게 \"무엇을 내가 정해 줬는지\" 알릴 때 쓴다.\n *\n * 조용히 채우는 것도 조용히 비우는 것만큼 위험하다: 사용자가 준 적 없는 값으로 트윈이 만들어졌는데\n * 아무 말이 없으면, 나중에 \"왜 이 수치인가\" 를 아무도 답할 수 없다.\n */\nexport function filledKnobKeys(knobs: readonly KnobLike[] | undefined | null, params: Record<string, unknown> | undefined | null): string[] {\n const given = params ?? {}\n return (knobs ?? [])\n .filter(k => k?.key && k.default !== undefined && (given[k.key!] === undefined || given[k.key!] === null))\n .map(k => k.key!)\n}\n"]}
@@ -8,3 +8,26 @@ export declare function startReferenceLiveFeed(domainId: string, adapterType: st
8
8
  export declare function stopReferenceLiveFeed(instanceId: string): boolean;
9
9
  /** 활성 피드 목록(관측). */
10
10
  export declare function listReferenceLiveFeeds(): string[];
11
+ /**
12
+ * 부팅 때 **끊어진 피드를 다시 붙인다** — 도는 미러 트윈에 계측이 계속 들어오게.
13
+ *
14
+ * ── 무엇이 조용히 끊겼나 (2026-08-14) ───────────────────────────────────────
15
+ * `startReferenceLiveFeed` 를 부르는 곳은 `importTwinReference` **하나뿐**이었다. 그래서 서버를 한 번
16
+ * 재기동하면 미러 트윈은 「도는 중」이라 말하면서 아무 계측도 받지 않았다 — 오류도 로그도 없이,
17
+ * 사람은 「값이 안 변하네」로 알게 된다. 실 배포에서 재기동 한 번에 계측이 멈추는 셈이다.
18
+ *
19
+ * ── 되살릴 재료는 이미 영속돼 있다 ─────────────────────────────────────────
20
+ * `TwinReference` 가 어댑터 종류·접속 설정·그 레퍼런스가 만든 인스턴스(`scopeSpec.produced`)를 들고 있다.
21
+ * 사이트 서술자만 없는데(`hint` 는 남기지 않았다) **다시 찾아서** 얻는다(`discoverSites`) — 인제스트가
22
+ * 걷는 것과 같은 길이므로 규칙이 갈라지지 않는다. 되살릴 인스턴스가 없으면 그 호출도 하지 않는다.
23
+ *
24
+ * 재개 대상은 **등록부가 도는 미러라고 말하는 것**뿐이다: 멈춘 트윈을 되살리면 사람이 멈춘 것을
25
+ * 우리가 다시 켜는 것이고, 시뮬 트윈에 실 계측을 부으면 두 현실이 섞인다.
26
+ */
27
+ export declare function resumeReferenceLiveFeeds(): Promise<{
28
+ resumed: string[];
29
+ failed: {
30
+ instanceId: string;
31
+ error: string;
32
+ }[];
33
+ }>;
@@ -3,8 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.startReferenceLiveFeed = startReferenceLiveFeed;
4
4
  exports.stopReferenceLiveFeed = stopReferenceLiveFeed;
5
5
  exports.listReferenceLiveFeeds = listReferenceLiveFeeds;
6
+ exports.resumeReferenceLiveFeeds = resumeReferenceLiveFeeds;
7
+ const shell_1 = require("@things-factory/shell");
6
8
  const reference_adapter_js_1 = require("./reference-adapter.js");
7
9
  const index_js_1 = require("../../engine/index.js");
10
+ const twin_instance_js_1 = require("../twin-instance/twin-instance.js");
11
+ const twin_reference_js_1 = require("./twin-reference.js");
8
12
  /*
9
13
  * 커넥터 기반 라이브 피드 결선(reference → live, §0 프레임 ① realityMode='mirror').
10
14
  * 흐름: adapter.openLiveFeed(cfg, site, records) → ingestCanonicalRecords(정규→봉투) → TwinEngine.ingestLive(projector 미러).
@@ -23,20 +27,31 @@ async function startReferenceLiveFeed(domainId, adapterType, cfg, site, instance
23
27
  if (!adapter?.openLiveFeed)
24
28
  return false;
25
29
  const det = await index_js_1.TwinEngine.detail(domainId, instanceId);
26
- if (!det?.board)
27
- throw new Error(`instance "${instanceId}" not provisioned (no board) — ingest master before live start`);
30
+ if (!det?.model)
31
+ throw new Error(`instance "${instanceId}" not provisioned (no model) — ingest master before live start`);
28
32
  // mirror 모드 기동 — 커널 tick 없이 projector 가 실 이벤트를 미러(startLive 가 realityMode='mirror' 설정).
29
33
  // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.
30
- index_js_1.TwinEngine.startLive(instanceId, domainId, det.kind ?? 'wms', await index_js_1.TwinEngine.withSpaceTimeBase(det.board, domainId));
34
+ index_js_1.TwinEngine.startLive(instanceId, domainId, det.kind, await index_js_1.TwinEngine.withSpaceTimeBase(det.model, domainId));
31
35
  stopReferenceLiveFeed(instanceId); // 재기동 시 이전 피드 정리
32
36
  const unsubscribe = adapter.openLiveFeed(cfg, site, (records) => {
33
- if (!index_js_1.TwinEngine.instances[instanceId]) {
37
+ if (!index_js_1.TwinEngine.owns(domainId, instanceId)) {
34
38
  stopReferenceLiveFeed(instanceId); // 인스턴스 사라지면 자가 정지(안전망)
35
39
  return;
36
40
  }
37
- const { accepted } = (0, index_js_1.ingestCanonicalRecords)(records, domainId, new Date().toISOString());
41
+ const { accepted, rejected } = (0, index_js_1.ingestCanonicalRecords)(records, domainId, new Date().toISOString());
38
42
  if (accepted.length)
39
- index_js_1.TwinEngine.ingestLive(instanceId, accepted);
43
+ index_js_1.TwinEngine.ingestLive(domainId, instanceId, accepted);
44
+ /*
45
+ * **거부한 것을 조용히 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이
46
+ * 틀리면 트윈은 「아무 일도 일어나지 않는」 모습이 되고, 로그에도 아무 흔적이 없었다.
47
+ *
48
+ * 지금은 이유와 함께 남긴다. 화면까지 나르는 것은 다음 일이다(라이브 계측은 초당 여러 건이라
49
+ * 건마다 방송하면 그 자체가 부하가 된다 — 집계해 낼 자리가 필요하다).
50
+ */
51
+ if (rejected.length) {
52
+ const why = rejected[0]?.errors?.join(' · ') ?? 'unknown';
53
+ console.warn(`[twin-live] "${instanceId}": ${rejected.length} record(s) rejected — e.g. ${why}`);
54
+ }
40
55
  });
41
56
  feeds.set(instanceId, unsubscribe);
42
57
  return true;
@@ -57,6 +72,108 @@ function stopReferenceLiveFeed(instanceId) {
57
72
  function listReferenceLiveFeeds() {
58
73
  return [...feeds.keys()];
59
74
  }
75
+ /**
76
+ * 부팅 때 **끊어진 피드를 다시 붙인다** — 도는 미러 트윈에 계측이 계속 들어오게.
77
+ *
78
+ * ── 무엇이 조용히 끊겼나 (2026-08-14) ───────────────────────────────────────
79
+ * `startReferenceLiveFeed` 를 부르는 곳은 `importTwinReference` **하나뿐**이었다. 그래서 서버를 한 번
80
+ * 재기동하면 미러 트윈은 「도는 중」이라 말하면서 아무 계측도 받지 않았다 — 오류도 로그도 없이,
81
+ * 사람은 「값이 안 변하네」로 알게 된다. 실 배포에서 재기동 한 번에 계측이 멈추는 셈이다.
82
+ *
83
+ * ── 되살릴 재료는 이미 영속돼 있다 ─────────────────────────────────────────
84
+ * `TwinReference` 가 어댑터 종류·접속 설정·그 레퍼런스가 만든 인스턴스(`scopeSpec.produced`)를 들고 있다.
85
+ * 사이트 서술자만 없는데(`hint` 는 남기지 않았다) **다시 찾아서** 얻는다(`discoverSites`) — 인제스트가
86
+ * 걷는 것과 같은 길이므로 규칙이 갈라지지 않는다. 되살릴 인스턴스가 없으면 그 호출도 하지 않는다.
87
+ *
88
+ * 재개 대상은 **등록부가 도는 미러라고 말하는 것**뿐이다: 멈춘 트윈을 되살리면 사람이 멈춘 것을
89
+ * 우리가 다시 켜는 것이고, 시뮬 트윈에 실 계측을 부으면 두 현실이 섞인다.
90
+ */
91
+ async function resumeReferenceLiveFeeds() {
92
+ const resumed = [];
93
+ const failed = [];
94
+ /*
95
+ * **한 트윈에 피드는 하나다** (2026-08-18).
96
+ *
97
+ * 같은 사이트를 두 레퍼런스가 각각 인제스트하면 둘 다 그 인스턴스를 자기가 만든 것(`produced`)으로
98
+ * 들고 있다. 기동 때 그 목록을 그대로 돌면 같은 트윈에 피드를 두 번 붙이는데, 붙이는 함수가 미러를
99
+ * 다시 세우므로(`startLive`) **먼저 쌓인 수요 구간이 리셋된다** — 실제로 그렇게 났다
100
+ * (로그: `reattached 2 feed(s): ems-plant-1, ems-plant-1`).
101
+ *
102
+ * 그러니 한 번만 붙이고, **두 레퍼런스가 같은 트윈을 주장한다는 사실은 말한다** — 조용히 하나를
103
+ * 고르면 어느 접속이 그 트윈을 먹이고 있는지 아무도 모르게 된다.
104
+ */
105
+ const attached = new Map(); // instanceId → 먼저 붙인 레퍼런스
106
+ try {
107
+ const refs = await (0, shell_1.getRepository)(twin_reference_js_1.TwinReference).find({ where: { status: 'connected' }, relations: ['domain'] });
108
+ for (const ref of refs) {
109
+ const domainId = ref.domain?.id ?? ref.domainId;
110
+ const adapter = (0, reference_adapter_js_1.getAdapter)(ref.adapterType);
111
+ if (!domainId || !adapter?.openLiveFeed)
112
+ continue; // 관측만 하는 어댑터에는 되살릴 피드가 없다
113
+ const produced = (ref.scopeSpec?.produced ?? []).filter((p) => p?.instanceId && p?.siteId);
114
+ if (!produced.length)
115
+ continue;
116
+ /* 등록부가 「도는 미러」라고 말하는 것만 고른다. */
117
+ const rows = await (0, shell_1.getRepository)(twin_instance_js_1.TwinInstance).find({
118
+ where: produced.map((p) => ({ domain: { id: domainId }, instanceId: p.instanceId }))
119
+ });
120
+ const live = produced.filter((p) => rows.some(r => r.instanceId === p.instanceId && r.status === 'running' && r.realityMode === 'mirror'));
121
+ if (!live.length)
122
+ continue;
123
+ let sites;
124
+ try {
125
+ sites = await adapter.discoverSites(ref.connectionConfig ?? {});
126
+ }
127
+ catch (e) {
128
+ /* 원 시스템이 아직 안 떴을 수 있다 — 지어내지 않고 그렇게 적는다(트윈은 계속 「도는 미러」다). */
129
+ for (const p of live)
130
+ failed.push({ instanceId: p.instanceId, error: `discover failed: ${e?.message ?? 'unknown'}` });
131
+ continue;
132
+ }
133
+ for (const p of live) {
134
+ const site = sites.find(s => s.siteId === p.siteId);
135
+ if (!site) {
136
+ failed.push({ instanceId: p.instanceId, error: `site "${p.siteId}" no longer discoverable` });
137
+ continue;
138
+ }
139
+ const owner = attached.get(p.instanceId);
140
+ if (owner) {
141
+ /* 두 접속이 같은 트윈을 주장한다 — 먼저 붙은 것을 지키고, 그 사실을 남긴다(둘 다 붙이면 미러가 리셋된다). */
142
+ failed.push({
143
+ instanceId: p.instanceId,
144
+ error: `also claimed by reference "${ref.source}" — kept the feed from "${owner}" (two connections feeding one twin would reset the mirror)`
145
+ });
146
+ continue;
147
+ }
148
+ attached.set(p.instanceId, ref.source);
149
+ try {
150
+ const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId);
151
+ if (ok)
152
+ resumed.push(p.instanceId);
153
+ else
154
+ failed.push({ instanceId: p.instanceId, error: 'adapter has no openLiveFeed' });
155
+ }
156
+ catch (e) {
157
+ failed.push({ instanceId: p.instanceId, error: e?.message ?? 'reattach failed' });
158
+ }
159
+ }
160
+ }
161
+ }
162
+ catch (e) {
163
+ console.error('[twin-live] feed resume scan failed', e?.message);
164
+ }
165
+ if (resumed.length)
166
+ console.log(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`);
167
+ /* **조용히 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */
168
+ for (const f of failed)
169
+ console.warn(`[twin-live] "${f.instanceId}" feed NOT reattached — ${f.error}`);
170
+ return { resumed, failed };
171
+ }
172
+ /*
173
+ * 이 계층의 피드 장부를 등록한다 — 화면이 「끊김」을 말할 근거는 **묻는 자리 하나**여야 한다.
174
+ * (프리셋 기반 피드는 앱이 자기 장부를 따로 등록한다. 한쪽만 보면 흐르는 트윈을 끊겼다고 부른다.)
175
+ */
176
+ (0, index_js_1.registerLiveFeedProbe)(() => [...feeds.keys()]);
60
177
  /* 인스턴스 정지 시 피드 자동 정리(생명주기 대칭). */
61
178
  index_js_1.TwinEngine.onStop(instanceId => stopReferenceLiveFeed(instanceId));
62
179
  //# sourceMappingURL=reference-live.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"reference-live.js","sourceRoot":"","sources":["../../../server/service/reference/reference-live.ts"],"names":[],"mappings":";;AAkBA,wDA4BC;AAGD,sDAMC;AAGD,wDAEC;AA5DD,iEAA+F;AAC/F,oDAAgG;AAEhG;;;;;;GAMG;AAEH,iEAAiE;AACjE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAA;AAE3C;;;GAGG;AACI,KAAK,UAAU,sBAAsB,CAC1C,QAAgB,EAChB,WAAmB,EACnB,GAAqB,EACrB,IAAoB,EACpB,UAAkB;IAElB,MAAM,OAAO,GAAG,IAAA,iCAAU,EAAC,WAAW,CAAC,CAAA;IACvC,IAAI,CAAC,OAAO,EAAE,YAAY;QAAE,OAAO,KAAK,CAAA;IAExC,MAAM,GAAG,GAAG,MAAM,qBAAU,CAAC,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAA;IACzD,IAAI,CAAC,GAAG,EAAE,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,aAAa,UAAU,gEAAgE,CAAC,CAAA;IAEzH,wFAAwF;IACxF,uDAAuD;IACvD,qBAAU,CAAC,SAAS,CAAC,UAAU,EAAE,QAAQ,EAAE,GAAG,CAAC,IAAI,IAAI,KAAK,EAAE,MAAM,qBAAU,CAAC,iBAAiB,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAA;IAEtH,qBAAqB,CAAC,UAAU,CAAC,CAAA,CAAC,iBAAiB;IACnD,MAAM,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,OAAkB,EAAE,EAAE;QACzE,IAAI,CAAC,qBAAU,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,CAAC;YACtC,qBAAqB,CAAC,UAAU,CAAC,CAAA,CAAC,uBAAuB;YACzD,OAAM;QACR,CAAC;QACD,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAA,iCAAsB,EAAC,OAA4B,EAAE,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAA;QAC7G,IAAI,QAAQ,CAAC,MAAM;YAAE,qBAAU,CAAC,UAAU,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;IAClE,CAAC,CAAC,CAAA;IACF,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,CAAA;IAClC,OAAO,IAAI,CAAA;AACb,CAAC;AAED,8BAA8B;AAC9B,SAAgB,qBAAqB,CAAC,UAAkB;IACtD,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IACnC,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAA;IACxB,IAAI,CAAC;QAAC,KAAK,EAAE,CAAA;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC;IACpC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IACxB,OAAO,IAAI,CAAA;AACb,CAAC;AAED,oBAAoB;AACpB,SAAgB,sBAAsB;IACpC,OAAO,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAA;AAC1B,CAAC;AAED,kCAAkC;AAClC,qBAAU,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,qBAAqB,CAAC,UAAU,CAAC,CAAC,CAAA","sourcesContent":["import { getAdapter, type ConnectionConfig, type SiteDescriptor } from './reference-adapter.js'\nimport { TwinEngine, ingestCanonicalRecords, type CanonicalRecord } from '../../engine/index.js'\n\n/*\n * 커넥터 기반 라이브 피드 결선(reference → live, §0 프레임 ① realityMode='mirror').\n * 흐름: adapter.openLiveFeed(cfg, site, records) → ingestCanonicalRecords(정규→봉투) → TwinEngine.ingestLive(projector 미러).\n * 커넥터가 \"벤더 이벤트 → 정규 레코드\" 매핑을 소유(코드)한다 — preset 기반 startLiveFeed(operato-twin, http-get+jsonata)와\n * 대칭이되, 매핑 위치만 다르다. 실 시스템 전환은 connectionConfig.baseUrl 교체뿐(코드 경로 동일).\n * 생명주기: 인스턴스 정지(TwinEngine.stop)에 훅으로 묶여 피드도 함께 unsubscribe.\n */\n\n/** 활성 라이브 피드 — instanceId → unsubscribe(어댑터 openLiveFeed 반환). */\nconst feeds = new Map<string, () => void>()\n\n/**\n * 커넥터 라이브 기동 — 어댑터 openLiveFeed 를 인스턴스에 결선.\n * 반환: 라이브 결선 성공 여부(어댑터가 openLiveFeed 미지원이면 false → 호출측이 sim 폴백).\n */\nexport async function startReferenceLiveFeed(\n domainId: string,\n adapterType: string,\n cfg: ConnectionConfig,\n site: SiteDescriptor,\n instanceId: string\n): Promise<boolean> {\n const adapter = getAdapter(adapterType)\n if (!adapter?.openLiveFeed) return false\n\n const det = await TwinEngine.detail(domainId, instanceId)\n if (!det?.board) throw new Error(`instance \"${instanceId}\" not provisioned (no board) — ingest master before live start`)\n\n // mirror 모드 기동 — 커널 tick 없이 projector 가 실 이벤트를 미러(startLive 가 realityMode='mirror' 설정).\n // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.\n TwinEngine.startLive(instanceId, domainId, det.kind ?? 'wms', await TwinEngine.withSpaceTimeBase(det.board, domainId))\n\n stopReferenceLiveFeed(instanceId) // 재기동 시 이전 피드 정리\n const unsubscribe = adapter.openLiveFeed(cfg, site, (records: unknown[]) => {\n if (!TwinEngine.instances[instanceId]) {\n stopReferenceLiveFeed(instanceId) // 인스턴스 사라지면 자가 정지(안전망)\n return\n }\n const { accepted } = ingestCanonicalRecords(records as CanonicalRecord[], domainId, new Date().toISOString())\n if (accepted.length) TwinEngine.ingestLive(instanceId, accepted)\n })\n feeds.set(instanceId, unsubscribe)\n return true\n}\n\n/** 라이브 피드 중지(unsubscribe). */\nexport function stopReferenceLiveFeed(instanceId: string): boolean {\n const unsub = feeds.get(instanceId)\n if (!unsub) return false\n try { unsub() } catch { /* noop */ }\n feeds.delete(instanceId)\n return true\n}\n\n/** 활성 피드 목록(관측). */\nexport function listReferenceLiveFeeds(): string[] {\n return [...feeds.keys()]\n}\n\n/* 인스턴스 정지 시 피드 자동 정리(생명주기 대칭). */\nTwinEngine.onStop(instanceId => stopReferenceLiveFeed(instanceId))\n"]}
1
+ {"version":3,"file":"reference-live.js","sourceRoot":"","sources":["../../../server/service/reference/reference-live.ts"],"names":[],"mappings":"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","sourcesContent":["import { getRepository } from '@things-factory/shell'\n\nimport { getAdapter, type ConnectionConfig, type SiteDescriptor } from './reference-adapter.js'\nimport { TwinEngine, ingestCanonicalRecords, registerLiveFeedProbe, type CanonicalRecord } from '../../engine/index.js'\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinReference } from './twin-reference.js'\n\n/*\n * 커넥터 기반 라이브 피드 결선(reference → live, §0 프레임 ① realityMode='mirror').\n * 흐름: adapter.openLiveFeed(cfg, site, records) → ingestCanonicalRecords(정규→봉투) → TwinEngine.ingestLive(projector 미러).\n * 커넥터가 \"벤더 이벤트 → 정규 레코드\" 매핑을 소유(코드)한다 — preset 기반 startLiveFeed(operato-twin, http-get+jsonata)와\n * 대칭이되, 매핑 위치만 다르다. 실 시스템 전환은 connectionConfig.baseUrl 교체뿐(코드 경로 동일).\n * 생명주기: 인스턴스 정지(TwinEngine.stop)에 훅으로 묶여 피드도 함께 unsubscribe.\n */\n\n/** 활성 라이브 피드 — instanceId → unsubscribe(어댑터 openLiveFeed 반환). */\nconst feeds = new Map<string, () => void>()\n\n/**\n * 커넥터 라이브 기동 — 어댑터 openLiveFeed 를 인스턴스에 결선.\n * 반환: 라이브 결선 성공 여부(어댑터가 openLiveFeed 미지원이면 false → 호출측이 sim 폴백).\n */\nexport async function startReferenceLiveFeed(\n domainId: string,\n adapterType: string,\n cfg: ConnectionConfig,\n site: SiteDescriptor,\n instanceId: string\n): Promise<boolean> {\n const adapter = getAdapter(adapterType)\n if (!adapter?.openLiveFeed) return false\n\n const det = await TwinEngine.detail(domainId, instanceId)\n if (!det?.model) throw new Error(`instance \"${instanceId}\" not provisioned (no model) — ingest master before live start`)\n\n // mirror 모드 기동 — 커널 tick 없이 projector 가 실 이벤트를 미러(startLive 가 realityMode='mirror' 설정).\n // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.\n TwinEngine.startLive(instanceId, domainId, det.kind, await TwinEngine.withSpaceTimeBase(det.model, domainId))\n\n stopReferenceLiveFeed(instanceId) // 재기동 시 이전 피드 정리\n const unsubscribe = adapter.openLiveFeed(cfg, site, (records: unknown[]) => {\n if (!TwinEngine.owns(domainId, instanceId)) {\n stopReferenceLiveFeed(instanceId) // 인스턴스 사라지면 자가 정지(안전망)\n return\n }\n const { accepted, rejected } = ingestCanonicalRecords(records as CanonicalRecord[], domainId, new Date().toISOString())\n if (accepted.length) TwinEngine.ingestLive(domainId, instanceId, accepted)\n /*\n * **거부한 것을 조용히 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이\n * 틀리면 트윈은 「아무 일도 일어나지 않는」 모습이 되고, 로그에도 아무 흔적이 없었다.\n *\n * 지금은 이유와 함께 남긴다. 화면까지 나르는 것은 다음 일이다(라이브 계측은 초당 여러 건이라\n * 건마다 방송하면 그 자체가 부하가 된다 — 집계해 낼 자리가 필요하다).\n */\n if (rejected.length) {\n const why = rejected[0]?.errors?.join(' · ') ?? 'unknown'\n console.warn(`[twin-live] \"${instanceId}\": ${rejected.length} record(s) rejected — e.g. ${why}`)\n }\n })\n feeds.set(instanceId, unsubscribe)\n return true\n}\n\n/** 라이브 피드 중지(unsubscribe). */\nexport function stopReferenceLiveFeed(instanceId: string): boolean {\n const unsub = feeds.get(instanceId)\n if (!unsub) return false\n try { unsub() } catch { /* noop */ }\n feeds.delete(instanceId)\n return true\n}\n\n/** 활성 피드 목록(관측). */\nexport function listReferenceLiveFeeds(): string[] {\n return [...feeds.keys()]\n}\n\n/**\n * 부팅 때 **끊어진 피드를 다시 붙인다** — 도는 미러 트윈에 계측이 계속 들어오게.\n *\n * ── 무엇이 조용히 끊겼나 (2026-08-14) ───────────────────────────────────────\n * `startReferenceLiveFeed` 를 부르는 곳은 `importTwinReference` **하나뿐**이었다. 그래서 서버를 한 번\n * 재기동하면 미러 트윈은 「도는 중」이라 말하면서 아무 계측도 받지 않았다 — 오류도 로그도 없이,\n * 사람은 「값이 안 변하네」로 알게 된다. 실 배포에서 재기동 한 번에 계측이 멈추는 셈이다.\n *\n * ── 되살릴 재료는 이미 영속돼 있다 ─────────────────────────────────────────\n * `TwinReference` 가 어댑터 종류·접속 설정·그 레퍼런스가 만든 인스턴스(`scopeSpec.produced`)를 들고 있다.\n * 사이트 서술자만 없는데(`hint` 는 남기지 않았다) **다시 찾아서** 얻는다(`discoverSites`) — 인제스트가\n * 걷는 것과 같은 길이므로 규칙이 갈라지지 않는다. 되살릴 인스턴스가 없으면 그 호출도 하지 않는다.\n *\n * 재개 대상은 **등록부가 도는 미러라고 말하는 것**뿐이다: 멈춘 트윈을 되살리면 사람이 멈춘 것을\n * 우리가 다시 켜는 것이고, 시뮬 트윈에 실 계측을 부으면 두 현실이 섞인다.\n */\nexport async function resumeReferenceLiveFeeds(): Promise<{ resumed: string[]; failed: { instanceId: string; error: string }[] }> {\n const resumed: string[] = []\n const failed: { instanceId: string; error: string }[] = []\n /*\n * **한 트윈에 피드는 하나다** (2026-08-18).\n *\n * 같은 사이트를 두 레퍼런스가 각각 인제스트하면 둘 다 그 인스턴스를 자기가 만든 것(`produced`)으로\n * 들고 있다. 기동 때 그 목록을 그대로 돌면 같은 트윈에 피드를 두 번 붙이는데, 붙이는 함수가 미러를\n * 다시 세우므로(`startLive`) **먼저 쌓인 수요 구간이 리셋된다** — 실제로 그렇게 났다\n * (로그: `reattached 2 feed(s): ems-plant-1, ems-plant-1`).\n *\n * 그러니 한 번만 붙이고, **두 레퍼런스가 같은 트윈을 주장한다는 사실은 말한다** — 조용히 하나를\n * 고르면 어느 접속이 그 트윈을 먹이고 있는지 아무도 모르게 된다.\n */\n const attached = new Map<string, string>() // instanceId → 먼저 붙인 레퍼런스\n try {\n const refs = await getRepository(TwinReference).find({ where: { status: 'connected' }, relations: ['domain'] })\n for (const ref of refs) {\n const domainId = (ref as any).domain?.id ?? (ref as any).domainId\n const adapter = getAdapter(ref.adapterType)\n if (!domainId || !adapter?.openLiveFeed) continue // 관측만 하는 어댑터에는 되살릴 피드가 없다\n\n const produced: any[] = ((ref.scopeSpec as any)?.produced ?? []).filter((p: any) => p?.instanceId && p?.siteId)\n if (!produced.length) continue\n\n /* 등록부가 「도는 미러」라고 말하는 것만 고른다. */\n const rows = await getRepository(TwinInstance).find({\n where: produced.map((p: any) => ({ domain: { id: domainId }, instanceId: p.instanceId })) as any\n })\n const live = produced.filter((p: any) =>\n rows.some(r => r.instanceId === p.instanceId && r.status === 'running' && r.realityMode === 'mirror')\n )\n if (!live.length) continue\n\n let sites: SiteDescriptor[]\n try {\n sites = await adapter.discoverSites(ref.connectionConfig ?? {})\n } catch (e: any) {\n /* 원 시스템이 아직 안 떴을 수 있다 — 지어내지 않고 그렇게 적는다(트윈은 계속 「도는 미러」다). */\n for (const p of live) failed.push({ instanceId: p.instanceId, error: `discover failed: ${e?.message ?? 'unknown'}` })\n continue\n }\n\n for (const p of live) {\n const site = sites.find(s => s.siteId === p.siteId)\n if (!site) {\n failed.push({ instanceId: p.instanceId, error: `site \"${p.siteId}\" no longer discoverable` })\n continue\n }\n const owner = attached.get(p.instanceId)\n if (owner) {\n /* 두 접속이 같은 트윈을 주장한다 — 먼저 붙은 것을 지키고, 그 사실을 남긴다(둘 다 붙이면 미러가 리셋된다). */\n failed.push({\n instanceId: p.instanceId,\n error: `also claimed by reference \"${ref.source}\" — kept the feed from \"${owner}\" (two connections feeding one twin would reset the mirror)`\n })\n continue\n }\n attached.set(p.instanceId, ref.source)\n try {\n const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId)\n if (ok) resumed.push(p.instanceId)\n else failed.push({ instanceId: p.instanceId, error: 'adapter has no openLiveFeed' })\n } catch (e: any) {\n failed.push({ instanceId: p.instanceId, error: e?.message ?? 'reattach failed' })\n }\n }\n }\n } catch (e: any) {\n console.error('[twin-live] feed resume scan failed', e?.message)\n }\n\n if (resumed.length) console.log(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`)\n /* **조용히 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */\n for (const f of failed) console.warn(`[twin-live] \"${f.instanceId}\" feed NOT reattached — ${f.error}`)\n return { resumed, failed }\n}\n\n/*\n * 이 계층의 피드 장부를 등록한다 — 화면이 「끊김」을 말할 근거는 **묻는 자리 하나**여야 한다.\n * (프리셋 기반 피드는 앱이 자기 장부를 따로 등록한다. 한쪽만 보면 흐르는 트윈을 끊겼다고 부른다.)\n */\nregisterLiveFeedProbe(() => [...feeds.keys()])\n\n/* 인스턴스 정지 시 피드 자동 정리(생명주기 대칭). */\nTwinEngine.onStop(instanceId => stopReferenceLiveFeed(instanceId))\n"]}