@things-factory/headless-twin 10.0.11 → 10.0.13

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 (369) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +81 -3
  2. package/dist-server/engine/canonical-ingest.js +89 -10
  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/declared-stimulus.d.ts +43 -0
  7. package/dist-server/engine/declared-stimulus.js +57 -0
  8. package/dist-server/engine/declared-stimulus.js.map +1 -0
  9. package/dist-server/engine/index.d.ts +5 -0
  10. package/dist-server/engine/index.js +6 -0
  11. package/dist-server/engine/index.js.map +1 -1
  12. package/dist-server/engine/ingest-health.d.ts +335 -0
  13. package/dist-server/engine/ingest-health.js +434 -0
  14. package/dist-server/engine/ingest-health.js.map +1 -0
  15. package/dist-server/engine/integration-coverage.d.ts +76 -0
  16. package/dist-server/engine/integration-coverage.js +73 -0
  17. package/dist-server/engine/integration-coverage.js.map +1 -0
  18. package/dist-server/engine/integration-probes.d.ts +65 -0
  19. package/dist-server/engine/integration-probes.js +100 -0
  20. package/dist-server/engine/integration-probes.js.map +1 -0
  21. package/dist-server/engine/integration-runner.d.ts +45 -0
  22. package/dist-server/engine/integration-runner.js +59 -0
  23. package/dist-server/engine/integration-runner.js.map +1 -0
  24. package/dist-server/engine/integration-target-profile.d.ts +57 -0
  25. package/dist-server/engine/integration-target-profile.js +79 -0
  26. package/dist-server/engine/integration-target-profile.js.map +1 -0
  27. package/dist-server/engine/kpi-fold.d.ts +39 -0
  28. package/dist-server/engine/kpi-fold.js +68 -5
  29. package/dist-server/engine/kpi-fold.js.map +1 -1
  30. package/dist-server/engine/kpi-query.d.ts +3 -3
  31. package/dist-server/engine/kpi-query.js +132 -18
  32. package/dist-server/engine/kpi-query.js.map +1 -1
  33. package/dist-server/engine/live-feed-registry.d.ts +1 -1
  34. package/dist-server/engine/live-feed-registry.js +3 -17
  35. package/dist-server/engine/live-feed-registry.js.map +1 -1
  36. package/dist-server/engine/load-meter.d.ts +1 -1
  37. package/dist-server/engine/load-meter.js +12 -3
  38. package/dist-server/engine/load-meter.js.map +1 -1
  39. package/dist-server/engine/local-declarations.d.ts +3 -6
  40. package/dist-server/engine/local-declarations.js +97 -16
  41. package/dist-server/engine/local-declarations.js.map +1 -1
  42. package/dist-server/engine/log.d.ts +18 -0
  43. package/dist-server/engine/log.js +80 -0
  44. package/dist-server/engine/log.js.map +1 -0
  45. package/dist-server/engine/loop-lag.d.ts +54 -0
  46. package/dist-server/engine/loop-lag.js +87 -0
  47. package/dist-server/engine/loop-lag.js.map +1 -0
  48. package/dist-server/engine/measured-yield.d.ts +42 -0
  49. package/dist-server/engine/measured-yield.js +75 -0
  50. package/dist-server/engine/measured-yield.js.map +1 -0
  51. package/dist-server/engine/model-gap.d.ts +108 -0
  52. package/dist-server/engine/model-gap.js +95 -0
  53. package/dist-server/engine/model-gap.js.map +1 -0
  54. package/dist-server/engine/operation-basis.d.ts +16 -0
  55. package/dist-server/engine/operation-basis.js +20 -2
  56. package/dist-server/engine/operation-basis.js.map +1 -1
  57. package/dist-server/engine/property-effects.js +17 -0
  58. package/dist-server/engine/property-effects.js.map +1 -1
  59. package/dist-server/engine/restart-policy.d.ts +13 -0
  60. package/dist-server/engine/restart-policy.js +52 -0
  61. package/dist-server/engine/restart-policy.js.map +1 -0
  62. package/dist-server/engine/runtime-key.js +1 -1
  63. package/dist-server/engine/runtime-key.js.map +1 -1
  64. package/dist-server/engine/spec-coverage.d.ts +8 -0
  65. package/dist-server/engine/spec-coverage.js +3 -1
  66. package/dist-server/engine/spec-coverage.js.map +1 -1
  67. package/dist-server/engine/stage-path.d.ts +83 -0
  68. package/dist-server/engine/stage-path.js +118 -0
  69. package/dist-server/engine/stage-path.js.map +1 -0
  70. package/dist-server/engine/twin-engine.d.ts +400 -31
  71. package/dist-server/engine/twin-engine.js +1198 -187
  72. package/dist-server/engine/twin-engine.js.map +1 -1
  73. package/dist-server/index.js +35 -3
  74. package/dist-server/index.js.map +1 -1
  75. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.d.ts +5 -0
  76. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +58 -0
  77. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -0
  78. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.d.ts +5 -0
  79. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +55 -0
  80. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -0
  81. package/dist-server/migrations/index.js +7 -1
  82. package/dist-server/migrations/index.js.map +1 -1
  83. package/dist-server/service/index.d.ts +5 -2
  84. package/dist-server/service/index.js +18 -7
  85. package/dist-server/service/index.js.map +1 -1
  86. package/dist-server/service/reference/control-routing.d.ts +14 -0
  87. package/dist-server/service/reference/control-routing.js +64 -0
  88. package/dist-server/service/reference/control-routing.js.map +1 -0
  89. package/dist-server/service/reference/discovery-result.d.ts +1 -1
  90. package/dist-server/service/reference/discovery-result.js +1 -1
  91. package/dist-server/service/reference/discovery-result.js.map +1 -1
  92. package/dist-server/service/reference/index.d.ts +5 -1
  93. package/dist-server/service/reference/index.js +8 -1
  94. package/dist-server/service/reference/index.js.map +1 -1
  95. package/dist-server/service/reference/reference-adapter.d.ts +251 -3
  96. package/dist-server/service/reference/reference-adapter.js +43 -1
  97. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  98. package/dist-server/service/reference/reference-assessment.d.ts +68 -0
  99. package/dist-server/service/reference/reference-assessment.js +136 -0
  100. package/dist-server/service/reference/reference-assessment.js.map +1 -0
  101. package/dist-server/service/reference/reference-live.d.ts +12 -1
  102. package/dist-server/service/reference/reference-live.js +119 -14
  103. package/dist-server/service/reference/reference-live.js.map +1 -1
  104. package/dist-server/service/reference/reference-master.d.ts +47 -1
  105. package/dist-server/service/reference/reference-master.js +34 -4
  106. package/dist-server/service/reference/reference-master.js.map +1 -1
  107. package/dist-server/service/reference/reference-probe.d.ts +92 -0
  108. package/dist-server/service/reference/reference-probe.js +186 -0
  109. package/dist-server/service/reference/reference-probe.js.map +1 -0
  110. package/dist-server/service/reference/reference-progress-subscription.d.ts +17 -0
  111. package/dist-server/service/reference/reference-progress-subscription.js +94 -0
  112. package/dist-server/service/reference/reference-progress-subscription.js.map +1 -0
  113. package/dist-server/service/reference/reference-progress.d.ts +38 -0
  114. package/dist-server/service/reference/reference-progress.js +71 -0
  115. package/dist-server/service/reference/reference-progress.js.map +1 -0
  116. package/dist-server/service/reference/reference-resolver.d.ts +3 -3
  117. package/dist-server/service/reference/reference-resolver.js +242 -58
  118. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  119. package/dist-server/service/reference/twin-reference.d.ts +32 -0
  120. package/dist-server/service/reference/twin-reference.js +10 -0
  121. package/dist-server/service/reference/twin-reference.js.map +1 -1
  122. package/dist-server/service/twin-audit/command-audit.d.ts +34 -0
  123. package/dist-server/service/twin-audit/command-audit.js +15 -1
  124. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  125. package/dist-server/service/twin-audit/twin-audit-event.d.ts +3 -0
  126. package/dist-server/service/twin-audit/twin-audit-event.js +10 -0
  127. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  128. package/dist-server/service/twin-control/twin-control-mutation.d.ts +15 -2
  129. package/dist-server/service/twin-control/twin-control-mutation.js +88 -36
  130. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  131. package/dist-server/service/twin-event/journal-count.d.ts +21 -0
  132. package/dist-server/service/twin-event/journal-count.js +26 -0
  133. package/dist-server/service/twin-event/journal-count.js.map +1 -0
  134. package/dist-server/service/twin-event/twin-event-keys.d.ts +9 -0
  135. package/dist-server/service/twin-event/twin-event-keys.js +15 -18
  136. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  137. package/dist-server/service/twin-event/twin-event-type.d.ts +3 -1
  138. package/dist-server/service/twin-event/twin-event-type.js +18 -1
  139. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  140. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  141. package/dist-server/service/twin-event/twin-event.js +9 -1
  142. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  143. package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
  144. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  145. package/dist-server/service/twin-ingest-window/index.d.ts +5 -0
  146. package/dist-server/service/twin-ingest-window/index.js +10 -0
  147. package/dist-server/service/twin-ingest-window/index.js.map +1 -0
  148. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +12 -0
  149. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +123 -0
  150. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -0
  151. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.d.ts +1 -0
  152. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +51 -0
  153. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -0
  154. package/dist-server/service/twin-ingest-window/twin-ingest-window.d.ts +16 -0
  155. package/dist-server/service/twin-ingest-window/twin-ingest-window.js +108 -0
  156. package/dist-server/service/twin-ingest-window/twin-ingest-window.js.map +1 -0
  157. package/dist-server/service/twin-instance/twin-instance.d.ts +1 -1
  158. package/dist-server/service/twin-instance/twin-instance.js +2 -2
  159. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  160. package/dist-server/service/twin-journal/twin-journal-query.js +58 -5
  161. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  162. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +14 -0
  163. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +74 -3
  164. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  165. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  166. package/dist-server/service/twin-metrics/twin-metrics-query.js +78 -3
  167. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  168. package/dist-server/service/twin-model/axis-journal-evidence.d.ts +2 -0
  169. package/dist-server/service/twin-model/axis-journal-evidence.js +89 -0
  170. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -0
  171. package/dist-server/service/twin-model/epcis-coverage.d.ts +1 -1
  172. package/dist-server/service/twin-model/epcis-coverage.js +19 -7
  173. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  174. package/dist-server/service/twin-model/iec61850-coverage.d.ts +1 -1
  175. package/dist-server/service/twin-model/iec61850-coverage.js +18 -7
  176. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  177. package/dist-server/service/twin-model/isa95-coverage.d.ts +14 -0
  178. package/dist-server/service/twin-model/isa95-coverage.js +23 -9
  179. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  180. package/dist-server/service/twin-model/item-ref.d.ts +24 -0
  181. package/dist-server/service/twin-model/item-ref.js +88 -0
  182. package/dist-server/service/twin-model/item-ref.js.map +1 -0
  183. package/dist-server/service/twin-model/name-index.d.ts +36 -0
  184. package/dist-server/service/twin-model/name-index.js +116 -0
  185. package/dist-server/service/twin-model/name-index.js.map +1 -0
  186. package/dist-server/service/twin-model/project-structure.js +1 -1
  187. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  188. package/dist-server/service/twin-model/status-tally.d.ts +9 -0
  189. package/dist-server/service/twin-model/status-tally.js +37 -0
  190. package/dist-server/service/twin-model/status-tally.js.map +1 -0
  191. package/dist-server/service/twin-model/twin-lineage-query.js +40 -12
  192. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  193. package/dist-server/service/twin-model/twin-model-item-query.js +38 -39
  194. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  195. package/dist-server/service/twin-model/twin-model-query.js +157 -5
  196. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  197. package/dist-server/service/twin-model/twin-model-tree-query.js +7 -0
  198. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  199. package/dist-server/service/twin-readiness/index.d.ts +2 -0
  200. package/dist-server/service/twin-readiness/index.js +6 -0
  201. package/dist-server/service/twin-readiness/index.js.map +1 -0
  202. package/dist-server/service/twin-readiness/twin-readiness-query.d.ts +3 -0
  203. package/dist-server/service/twin-readiness/twin-readiness-query.js +103 -0
  204. package/dist-server/service/twin-readiness/twin-readiness-query.js.map +1 -0
  205. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  206. package/dist-server/service/twin-space/twin-space-resolver.js +5 -4
  207. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  208. package/dist-shared/entity-delta.d.ts +23 -3
  209. package/dist-shared/entity-delta.js +12 -8
  210. package/dist-shared/entity-delta.js.map +1 -1
  211. package/dist-shared/kpi-broadcast.js +1 -1
  212. package/dist-shared/kpi-broadcast.js.map +1 -1
  213. package/dist-shared/touched-items.d.ts +7 -0
  214. package/dist-shared/touched-items.js +80 -0
  215. package/dist-shared/touched-items.js.map +1 -0
  216. package/package.json +7 -7
  217. package/server/engine/canonical-ingest.ts +164 -15
  218. package/server/engine/command-routing.ts +1 -1
  219. package/server/engine/declared-stimulus.ts +66 -0
  220. package/server/engine/index.ts +6 -0
  221. package/server/engine/ingest-health.ts +704 -0
  222. package/server/engine/integration-coverage.ts +147 -0
  223. package/server/engine/integration-probes.ts +144 -0
  224. package/server/engine/integration-runner.ts +95 -0
  225. package/server/engine/integration-target-profile.ts +103 -0
  226. package/server/engine/kpi-fold.ts +101 -5
  227. package/server/engine/kpi-query.ts +137 -23
  228. package/server/engine/live-feed-registry.ts +4 -3
  229. package/server/engine/load-meter.ts +12 -3
  230. package/server/engine/local-declarations.ts +97 -19
  231. package/server/engine/log.ts +72 -0
  232. package/server/engine/loop-lag.ts +120 -0
  233. package/server/engine/measured-yield.ts +89 -0
  234. package/server/engine/model-gap.ts +168 -0
  235. package/server/engine/operation-basis.ts +33 -2
  236. package/server/engine/property-effects.ts +17 -0
  237. package/server/engine/restart-policy.ts +55 -0
  238. package/server/engine/runtime-key.ts +1 -1
  239. package/server/engine/spec-coverage.ts +23 -3
  240. package/server/engine/stage-path.ts +172 -0
  241. package/server/engine/twin-engine.ts +1318 -186
  242. package/server/index.ts +36 -3
  243. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +54 -0
  244. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +53 -0
  245. package/server/migrations/index.ts +7 -1
  246. package/server/service/index.ts +11 -0
  247. package/server/service/reference/control-routing.ts +62 -0
  248. package/server/service/reference/discovery-result.ts +1 -1
  249. package/server/service/reference/index.ts +7 -1
  250. package/server/service/reference/reference-adapter.ts +275 -5
  251. package/server/service/reference/reference-assessment.ts +215 -0
  252. package/server/service/reference/reference-live.ts +126 -14
  253. package/server/service/reference/reference-master.ts +64 -4
  254. package/server/service/reference/reference-probe.ts +264 -0
  255. package/server/service/reference/reference-progress-subscription.ts +73 -0
  256. package/server/service/reference/reference-progress.ts +95 -0
  257. package/server/service/reference/reference-resolver.ts +246 -19
  258. package/server/service/reference/twin-reference.ts +39 -1
  259. package/server/service/twin-audit/command-audit.ts +34 -1
  260. package/server/service/twin-audit/twin-audit-event.ts +20 -0
  261. package/server/service/twin-control/twin-control-mutation.ts +78 -30
  262. package/server/service/twin-event/journal-count.ts +53 -0
  263. package/server/service/twin-event/twin-event-keys.ts +16 -1
  264. package/server/service/twin-event/twin-event-type.ts +26 -2
  265. package/server/service/twin-event/twin-event.ts +7 -0
  266. package/server/service/twin-forecast/twin-forecast-query.ts +2 -1
  267. package/server/service/twin-ingest-window/index.ts +7 -0
  268. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +125 -0
  269. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +57 -0
  270. package/server/service/twin-ingest-window/twin-ingest-window.ts +113 -0
  271. package/server/service/twin-instance/twin-instance.ts +16 -10
  272. package/server/service/twin-journal/twin-journal-query.ts +59 -5
  273. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +79 -4
  274. package/server/service/twin-metrics/twin-metrics-query.ts +81 -3
  275. package/server/service/twin-model/axis-journal-evidence.ts +57 -0
  276. package/server/service/twin-model/epcis-coverage.ts +2 -8
  277. package/server/service/twin-model/iec61850-coverage.ts +2 -8
  278. package/server/service/twin-model/isa95-coverage.ts +22 -9
  279. package/server/service/twin-model/item-ref.ts +80 -0
  280. package/server/service/twin-model/name-index.ts +94 -0
  281. package/server/service/twin-model/project-structure.ts +1 -1
  282. package/server/service/twin-model/status-tally.ts +37 -0
  283. package/server/service/twin-model/twin-lineage-query.ts +38 -9
  284. package/server/service/twin-model/twin-model-item-query.ts +26 -27
  285. package/server/service/twin-model/twin-model-query.ts +156 -5
  286. package/server/service/twin-model/twin-model-tree-query.ts +7 -0
  287. package/server/service/twin-readiness/index.ts +3 -0
  288. package/server/service/twin-readiness/twin-readiness-query.ts +96 -0
  289. package/server/service/twin-space/twin-space-resolver.ts +5 -4
  290. package/shared/entity-delta.ts +31 -7
  291. package/shared/kpi-broadcast.ts +1 -1
  292. package/shared/touched-items.ts +73 -0
  293. package/test/adopt-structure-live.test.ts +7 -7
  294. package/test/axis-journal-evidence.test.ts +71 -0
  295. package/test/axis-read.test.ts +101 -1
  296. package/test/boot-resume.test.ts +93 -27
  297. package/test/broadcast-cost-baseline.test.ts +200 -0
  298. package/test/broadcast-period.test.ts +58 -0
  299. package/test/canonical-ingest-vocabularies.test.ts +36 -1
  300. package/test/canonical-quantity-door.test.ts +61 -0
  301. package/test/command-routing.test.ts +1 -1
  302. package/test/control-capability.test.ts +103 -0
  303. package/test/declared-location-types.test.ts +89 -0
  304. package/test/declared-stimulus.test.ts +88 -0
  305. package/test/discovery-result.test.ts +1 -1
  306. package/test/duration-estimators.test.ts +1 -1
  307. package/test/entity-delta.test.ts +2 -2
  308. package/test/ingest-bench.test.ts +3 -3
  309. package/test/ingest-health-engine.test.ts +248 -0
  310. package/test/ingest-health-wiring.test.ts +119 -0
  311. package/test/ingest-health.test.ts +306 -0
  312. package/test/ingest-history.test.ts +247 -0
  313. package/test/ingest-running-guard.test.ts +5 -5
  314. package/test/instance-cache-lifecycle.test.ts +1 -1
  315. package/test/integration-probes.test.ts +103 -0
  316. package/test/integration-runner.test.ts +95 -0
  317. package/test/item-ref.test.ts +78 -0
  318. package/test/journal-read-discipline.test.ts +177 -0
  319. package/test/journal-retention.test.ts +133 -0
  320. package/test/journal-sort-axis.test.ts +142 -0
  321. package/test/journal-write-door.test.ts +110 -0
  322. package/test/journal-write-trend.test.ts +115 -0
  323. package/test/kernel-kind-guard.test.ts +4 -4
  324. package/test/kpi-baseline-db.test.ts +1 -1
  325. package/test/kpi-fold.test.ts +90 -3
  326. package/test/kpi-query-bench.test.ts +3 -3
  327. package/test/lineage-survives-restart.test.ts +20 -4
  328. package/test/live-cursor-wiring.test.ts +87 -0
  329. package/test/live-feed-registry.test.ts +15 -7
  330. package/test/live-kernel-facts.test.ts +7 -7
  331. package/test/live-mirror-parity.test.ts +6 -0
  332. package/test/load-meter.test.ts +29 -15
  333. package/test/local-declarations.test.ts +109 -2
  334. package/test/log-stamp.test.ts +59 -0
  335. package/test/loop-lag.test.ts +82 -0
  336. package/test/measured-yield.test.ts +90 -0
  337. package/test/model-gap.test.ts +153 -0
  338. package/test/oee-accumulator.test.ts +4 -0
  339. package/test/operation-basis.test.ts +28 -1
  340. package/test/operational-vocabulary.test.ts +108 -0
  341. package/test/operations-capability-db.test.ts +4 -4
  342. package/test/project-structure-db.test.ts +1 -1
  343. package/test/projection-reaches-screen.test.ts +1 -1
  344. package/test/property-effects.test.ts +28 -0
  345. package/test/read-failure-visible.test.ts +145 -0
  346. package/test/reference-grounding.test.ts +70 -0
  347. package/test/resolve-ts-siblings.mjs +52 -0
  348. package/test/restart-policy.test.ts +111 -0
  349. package/test/resync-origin-site.test.ts +7 -1
  350. package/test/revision-axis.test.ts +93 -0
  351. package/test/runtime-key.test.ts +2 -2
  352. package/test/scale-twin-bench.test.ts +2 -2
  353. package/test/source-outcome-audit.test.ts +104 -0
  354. package/test/spec-coverage.test.ts +1 -1
  355. package/test/stage-path.test.ts +95 -0
  356. package/test/standard-coverage.test.ts +15 -3
  357. package/test/status-tally.test.ts +55 -0
  358. package/test/structure-revision-db.test.ts +31 -27
  359. package/test/tenant-registry-db.test.ts +3 -3
  360. package/test/time-range.test.ts +152 -0
  361. package/test/touched-items.test.ts +61 -0
  362. package/test/twin-event-keys.test.ts +10 -2
  363. package/test/twin-model-item-db.test.ts +5 -3
  364. package/test/twin-model-tree-db.test.ts +7 -7
  365. package/test/twin-origin-resync.test.ts +4 -4
  366. package/test/warm-start-seam.test.ts +4 -0
  367. package/test/yield-loop.test.ts +199 -0
  368. package/tsconfig.shared.tsbuildinfo +1 -1
  369. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference-assessment.js","sourceRoot":"","sources":["../../../server/service/reference/reference-assessment.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IngestWarning, ReferenceMaster } from './reference-master.js'\n\n/*\n * **원본에서 읽은 뒤의 총평** — 사람이 한 장으로 읽는다.\n *\n * ── 왜 필요한가 (2026-08-22) ────────────────────────────────────────────────\n * 읽기가 끝나면 화면에 남는 것은 경고 열 줄이었고, 그 안에 이유가 문장으로 묻혀 있고 UUID 수백 개가\n * 붙어 있었다. 그래서 사람이 「가져올 것은 다 들어왔나」·「무엇이 비었나」·「다음에 무엇을 하면\n * 나아지나」를 스스로 세어야 했다 — 실제로 그 셋을 손으로 세는 대화가 하루를 채웠다.\n *\n * 그 셋은 이미 우리가 아는 사실이다. 세어 두었으니 **정리해서 내면 된다.**\n *\n * ── 두 층으로 나눈다 ────────────────────────────────────────────────────────\n * 여기는 **사실 층**이다: 센 것과 그 사유 코드만 담고 문장을 만들지 않는다. 화면이 표로 그리고,\n * 사람이 더 알고 싶으면 그때 AI 가 이 사실**만** 근거로 서술한다(말 층).\n *\n * **AI 는 로드베어링이 아니다.** 호출이 꺼져 있거나 실패해도 이 표가 그대로 화면에 남아야 한다 —\n * 그래서 사실 층이 스스로 완결이다.\n *\n * ── 무엇을 지어내지 않는가 ──────────────────────────────────────────────────\n * 「없음」의 뜻을 셋으로 갈라 둔다. 뭉치면 조치가 달라지는 것들이 한 칸에 섞인다.\n * · `gaps` — 원본에 있는데 우리가 옮기지 못했다. 고칠 대상이 있다(원본의 빈 칸이거나 우리 매핑).\n * · `absent` — 이 원본에 그 **개념이 없다.** 빠진 것이 아니므로 고칠 것이 없다.\n * · `grounding` — 값이 들어왔지만 **근거가 저장된 값이 아니다**(원본의 계산 규칙에서 왔다).\n */\n\n/** 센 것 — 화면이 그대로 표로 그린다. */\nexport interface AssessmentCounts {\n locations: number\n areas: number\n equipment: number\n materialDefinitions: number\n operations: number\n /** 생산 선언이 없으면 없다 — 0 과 구별한다(선언 자체가 없는 것과 비어 있는 것은 다르다). */\n materials?: number\n materialsWithHome?: number\n routes?: number\n recipes?: number\n recipesWithRoute?: number\n /** 공정을 말하는 투입 / 전체 투입 — 공정별 소요를 아는 정도. */\n taggedInputs?: number\n totalInputs?: number\n}\n\n/** 옮기지 못한 것 한 갈래 — 건수 큰 것부터. */\nexport interface AssessmentGap {\n /** 무엇을 옮기다 못 옮겼나 — 커넥터가 붙인 이름(`material home warehouse`). */\n what: string\n count: number\n /** 못 옮긴 원본 값 몇 개 — 전부가 아니라 표본이다(수백 개가 붙던 자리다). */\n sample?: string\n /** 그 자리에 대신 들어간 값. */\n fallback?: string\n}\n\nexport interface ImportAssessment {\n source: string\n siteId: string\n siteName?: string\n system?: string\n built: AssessmentCounts\n /**\n * **원본이 가지고 있던 수** — 커넥터가 말해 준 경우에만 있다.\n *\n * 이름을 `origin` 이라 하지 않는다 — 화면에서 그 낱말이 이미 **트윈의 출처**(`summary.origin`:\n * 템플릿인가 원본인가)를 뜻한다. 같은 이름을 두 뜻으로 쓰면 읽는 사람이 헷갈린다.\n *\n * `built` 와 견주어 「전부 옮겼는가」를 사람이 직접 셀 수 있게 한다. 커넥터가 말하지 않으면 이 자리는\n * 비어 있고, 화면은 분모 없이 옮긴 수만 보인다 — **분모를 지어내지 않는다.**\n */\n originCounts?: Record<string, number>\n gaps: AssessmentGap[]\n /** 이 원본에 개념이 없는 자리 — 커널 낱말로 적는다(화면이 번역한다). */\n absent: string[]\n /**\n * 값이 들어왔지만 **근거가 원본의 저장된 값이 아닌 것** — 파생·유추된 값이다.\n *\n * 이것을 `gaps` 에 섞으면 안 된다. 빈 자리가 아니라 **채워진 자리**이고, 사람이 물을 것은\n * 「왜 비었나」가 아니라 「그 값을 믿어도 되나」다. 조치도 다르다 — 원본을 채우는 것이 아니라\n * 그 규칙이 바뀌면 이 값도 바뀐다는 사실을 아는 것이다.\n */\n derived: AssessmentGap[]\n /** 되물은 횟수·잘린 목록 같은 운영 사실. */\n notes: IngestWarning[]\n}\n\n/** 이 마스터가 개념을 선언했나 — 「없다」와 「비어 있다」를 가른다. */\nconst declared = (v: unknown): boolean => Array.isArray(v) ? v.length > 0 : v !== undefined && v !== null\n\n/**\n * 마스터 한 벌 → 총평의 사실 층.\n *\n * 경고를 두 갈래로 나눈다: `connector-mapping-fallback` 은 **옮기지 못한 것**이므로 `gaps` 로,\n * 나머지(파생된 값·되물은 횟수·잘린 목록)는 `notes` 로 그대로 나른다. 판단하지 않는다 — 무엇이\n * 심각한지는 건수와 그 자리의 뜻이 말하고, 그 해석은 사람이나 말 층의 몫이다.\n */\nexport function assessMaster(master: ReferenceMaster, siteId: string): ImportAssessment {\n const m: any = master\n const d = m.productionSpec?.definition\n const inputs = (d?.recipes ?? []).flatMap((r: any) => r.inputs ?? [])\n\n const built: AssessmentCounts = {\n locations: (m.locations ?? []).length,\n areas: (m.locations ?? []).filter((l: any) => l.role === 'area' || l.role === 'both').length,\n equipment: (m.equipment ?? []).length,\n materialDefinitions: (m.materialDefinitions ?? []).length,\n operations: (m.operations ?? []).length,\n ...(d\n ? {\n materials: (d.materials ?? []).length,\n materialsWithHome: (d.materials ?? []).filter((x: any) => x.locationType).length,\n routes: (d.routes ?? []).length,\n recipes: (d.recipes ?? []).length,\n recipesWithRoute: (d.recipes ?? []).filter((x: any) => x.route).length,\n totalInputs: inputs.length,\n taggedInputs: inputs.filter((i: any) => i.operation).length\n }\n : {})\n }\n\n /* 원본의 수는 커넥터가 말한 그대로 나른다 — 여기서 고치거나 채우지 않는다. */\n const originCounts = m.originCounts && Object.keys(m.originCounts).length ? { ...m.originCounts } : undefined\n\n const gaps: AssessmentGap[] = []\n const derived: AssessmentGap[] = []\n const notes: IngestWarning[] = []\n /*\n * 커넥터가 「대체」 수집기로 남기는 것 중 일부는 **빈 자리가 아니라 파생된 값**이다(산출량이 원본의\n * 계산 규칙에서 온 경우처럼). 같은 통로로 오지만 뜻이 달라 갈라 담는다 — 이름에 그 사실이 적혀 있다.\n */\n const isDerived = (field: string) => /derived|not a stored field/i.test(field)\n for (const w of m.warnings ?? []) {\n if (w?.code === 'connector-mapping-fallback' && w.params?.field) {\n const values = String(w.params.values ?? '')\n const bin = isDerived(String(w.params.field)) ? derived : gaps\n bin.push({\n what: String(w.params.field),\n count: Number(w.params.count ?? 0),\n /* 값은 표본만 — 이 자리에 UUID 수백 개가 붙어 화면을 밀어냈다. 전체는 경고 원문에 남는다. */\n ...(values ? { sample: values.length > 120 ? `${values.slice(0, 120)}…` : values } : {}),\n ...(w.params.fallback ? { fallback: String(w.params.fallback) } : {})\n })\n continue\n }\n notes.push(w)\n }\n gaps.sort((a, b) => b.count - a.count)\n derived.sort((a, b) => b.count - a.count)\n\n /*\n * 이 원본에 개념이 없는 자리. **커널 낱말로 적는다** — 화면이 그것으로 번역하고, 개념 지도가 이미\n * 같은 낱말을 쓴다(두 화면이 다른 말을 하면 사람이 둘을 다른 사실로 읽는다).\n */\n const absent: string[] = []\n if (!declared(m.persons)) absent.push('persons')\n if (!declared(m.assets)) absent.push('assets')\n if (!declared(m.testSpecifications)) absent.push('testSpecifications')\n if (!declared(m.personnelClasses)) absent.push('personnelClasses')\n if (!declared(m.assetClasses)) absent.push('assetClasses')\n if (!declared(m.materialClasses)) absent.push('materialClasses')\n if (!d) absent.push('productionSpec')\n\n return {\n source: m.source,\n siteId,\n siteName: m.siteName,\n system: m.system,\n built,\n ...(originCounts ? { originCounts } : {}),\n gaps,\n derived,\n absent,\n notes\n }\n}\n\n/** 사람이 읽는 줄 — 터미널·로그가 읽는다(화면은 구조를 그대로 그린다). */\nexport function formatAssessment(a: ImportAssessment): string {\n const c = a.built\n const out: string[] = []\n /**\n * 분모가 있으면 함께 적는다 — 「1,261」과 「원본 1,408 중 1,261」은 다른 말이다.\n * 커넥터가 그 수를 말하지 않았으면 옮긴 수만 적는다(지어내지 않는다).\n */\n const n = (key: keyof AssessmentCounts, v?: number) => {\n if (v === undefined) return '없음'\n const o = a.originCounts?.[key as string]\n return o === undefined ? String(v) : o === v ? `${v}(원본 전부)` : `${v} / 원본 ${o}`\n }\n out.push(`총평 ${a.siteName ?? a.siteId} (${a.system ?? '?'}) — 원본 ${a.source}`)\n out.push(\n ` 선 것: 로케이션 ${n('locations', c.locations)}(존 ${c.areas}) · 설비 ${n('equipment', c.equipment)} · ` +\n `품목 정의 ${n('materialDefinitions', c.materialDefinitions)} · 공정 ${n('operations', c.operations)}`\n )\n if (c.materials !== undefined) {\n out.push(\n ` 자재 ${n('materials', c.materials)}(보관 로케이션 있는 것 ${c.materialsWithHome}) · ` +\n `경로 ${n('routes', c.routes)} · 레시피 ${n('recipes', c.recipes)}(경로 붙은 것 ${c.recipesWithRoute}) · ` +\n `투입 ${n('totalInputs', c.totalInputs)}(공정을 말하는 것 ${c.taggedInputs})`\n )\n } else {\n out.push(' 생산 선언 없음 — 레시피가 하나도 성립하지 않았다')\n }\n if (a.gaps.length) {\n out.push(' 옮기지 못한 것(건수 순):')\n for (const g of a.gaps) out.push(` ${String(g.count).padStart(6)} ${g.what}${g.sample ? ` — ${g.sample}` : ''}`)\n }\n if (a.derived.length) {\n out.push(' 파생된 값(저장된 값이 아니다):')\n for (const g of a.derived) out.push(` ${g.what}${g.sample ? ` — ${g.sample}` : ''}`)\n }\n if (a.absent.length) out.push(` 이 원본에 개념이 없는 자리: ${a.absent.join(', ')}`)\n for (const n of a.notes) out.push(` 참고 ${n.code}: ${n.message}`)\n return out.join('\\n')\n}\n"]}
@@ -3,7 +3,18 @@ import { type ConnectionConfig, type SiteDescriptor } from './reference-adapter.
3
3
  * 커넥터 라이브 기동 — 어댑터 openLiveFeed 를 인스턴스에 결선.
4
4
  * 반환: 라이브 결선 성공 여부(어댑터가 openLiveFeed 미지원이면 false → 호출측이 sim 폴백).
5
5
  */
6
- export declare function startReferenceLiveFeed(domainId: string, adapterType: string, cfg: ConnectionConfig, site: SiteDescriptor, instanceId: string): Promise<boolean>;
6
+ export declare function startReferenceLiveFeed(domainId: string, adapterType: string, cfg: ConnectionConfig, site: SiteDescriptor, instanceId: string,
7
+ /**
8
+ * **어느 레퍼런스에서 읽는 피드인가** — 읽기 커서를 저장할 자리를 가리킨다(§`loadLiveCursor`).
9
+ *
10
+ * 부르는 쪽이 `TwinReference` 를 이미 들고 있으므로 그 이름을 넘긴다. 처음에는 `cfg`·`site` 에서
11
+ * 찾으려 했는데 **둘 다 그 값을 가질 수 없다**: `cfg` 는 사람이 화면에서 입력한 접속 설정이고
12
+ * (`endpoint`·`token`…), `site` 는 커넥터가 원본에서 발견한 것이다 — **레퍼런스의 이름은 호스트가
13
+ * 붙이는 것이고 원본은 그것을 모른다.** 커넥터가 채울 수 없는 값을 커넥터에서 읽으려 한 것이 잘못이었다.
14
+ *
15
+ * 주지 않으면 커서를 잇지 않고 **그 사실을 말한다**(조용히 창이 미끄러지지 않게).
16
+ */
17
+ source?: string): Promise<boolean>;
7
18
  /** 라이브 피드 중지(unsubscribe). */
8
19
  export declare function stopReferenceLiveFeed(instanceId: string): boolean;
9
20
  /** 활성 피드 목록(관측). */
@@ -4,13 +4,14 @@ exports.startReferenceLiveFeed = startReferenceLiveFeed;
4
4
  exports.stopReferenceLiveFeed = stopReferenceLiveFeed;
5
5
  exports.listReferenceLiveFeeds = listReferenceLiveFeeds;
6
6
  exports.resumeReferenceLiveFeeds = resumeReferenceLiveFeeds;
7
+ const log_js_1 = require("../../engine/log.js");
7
8
  const shell_1 = require("@things-factory/shell");
8
9
  const reference_adapter_js_1 = require("./reference-adapter.js");
9
10
  const index_js_1 = require("../../engine/index.js");
10
11
  const twin_instance_js_1 = require("../twin-instance/twin-instance.js");
11
12
  const twin_reference_js_1 = require("./twin-reference.js");
12
13
  /*
13
- * 커넥터 기반 라이브 피드 결선(reference → live, §0 프레임 ① realityMode='mirror').
14
+ * 커넥터 기반 라이브 피드 결선(reference → live, §0 프레임 ① restartPolicy='resync').
14
15
  * 흐름: adapter.openLiveFeed(cfg, site, records) → ingestCanonicalRecords(정규→봉투) → TwinEngine.ingestLive(projector 미러).
15
16
  * 커넥터가 "벤더 이벤트 → 정규 레코드" 매핑을 소유(코드)한다 — preset 기반 startLiveFeed(operato-twin, http-get+jsonata)와
16
17
  * 대칭이되, 매핑 위치만 다르다. 실 시스템 전환은 connectionConfig.baseUrl 교체뿐(코드 경로 동일).
@@ -22,40 +23,144 @@ const feeds = new Map();
22
23
  * 커넥터 라이브 기동 — 어댑터 openLiveFeed 를 인스턴스에 결선.
23
24
  * 반환: 라이브 결선 성공 여부(어댑터가 openLiveFeed 미지원이면 false → 호출측이 sim 폴백).
24
25
  */
25
- async function startReferenceLiveFeed(domainId, adapterType, cfg, site, instanceId) {
26
+ async function startReferenceLiveFeed(domainId, adapterType, cfg, site, instanceId,
27
+ /**
28
+ * **어느 레퍼런스에서 읽는 피드인가** — 읽기 커서를 저장할 자리를 가리킨다(§`loadLiveCursor`).
29
+ *
30
+ * 부르는 쪽이 `TwinReference` 를 이미 들고 있으므로 그 이름을 넘긴다. 처음에는 `cfg`·`site` 에서
31
+ * 찾으려 했는데 **둘 다 그 값을 가질 수 없다**: `cfg` 는 사람이 화면에서 입력한 접속 설정이고
32
+ * (`endpoint`·`token`…), `site` 는 커넥터가 원본에서 발견한 것이다 — **레퍼런스의 이름은 호스트가
33
+ * 붙이는 것이고 원본은 그것을 모른다.** 커넥터가 채울 수 없는 값을 커넥터에서 읽으려 한 것이 잘못이었다.
34
+ *
35
+ * 주지 않으면 커서를 잇지 않고 **그 사실을 말한다**(조용히 창이 미끄러지지 않게).
36
+ */
37
+ source) {
26
38
  const adapter = (0, reference_adapter_js_1.getAdapter)(adapterType);
27
39
  if (!adapter?.openLiveFeed)
28
40
  return false;
29
41
  const det = await index_js_1.TwinEngine.detail(domainId, instanceId);
30
42
  if (!det?.model)
31
43
  throw new Error(`instance "${instanceId}" not provisioned (no model) — ingest master before live start`);
32
- // mirror 모드 기동 — 커널 tick 없이 projector 가 실 이벤트를 미러(startLive 가 realityMode='mirror' 설정).
44
+ // 관측 기동 — 커널 tick 없이 projector 가 실 이벤트를 반영한다(startLive 가 restartPolicy='resync' 로 적는다).
33
45
  // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.
34
46
  index_js_1.TwinEngine.startLive(instanceId, domainId, det.kind, await index_js_1.TwinEngine.withSpaceTimeBase(det.model, domainId));
35
47
  stopReferenceLiveFeed(instanceId); // 재기동 시 이전 피드 정리
48
+ /*
49
+ * **어디까지 읽었나를 이 층이 들고 건넨다**(§`LiveFeedContinuity`, 2026-08-23).
50
+ *
51
+ * 어댑터가 이 값을 자기 사물함에 두면 원본이 늘 때마다 저장 기제가 늘고, 그중 하나가 조용히 다르게
52
+ * 동작한다. 그리고 커널이 이미 갈라 두었다: 「원천이 애초에 다시 말해 주지 않는 축」은 재기동
53
+ * 연속성으로 이어받는다(§`hydrateContinuity`) — 읽기 커서가 정확히 그 성질이다.
54
+ *
55
+ * 없으면 `undefined` 를 건넨다 — 그것이 「첫 붙음」이라는 사실이고, 그때만 어댑터가 되돌아볼 날수로
56
+ * 첫 창을 만든다. 빈 객체로 메우지 않는다(그러면 첫 붙음과 이어 붙음이 같아진다).
57
+ */
58
+ const continuity = await loadLiveCursor(domainId, instanceId, source);
36
59
  const unsubscribe = adapter.openLiveFeed(cfg, site, (records) => {
37
60
  if (!index_js_1.TwinEngine.owns(domainId, instanceId)) {
38
61
  stopReferenceLiveFeed(instanceId); // 인스턴스 사라지면 자가 정지(안전망)
39
62
  return;
40
63
  }
64
+ const offered = Array.isArray(records) ? records.length : records ? 1 : 0;
41
65
  const { accepted, rejected } = (0, index_js_1.ingestCanonicalRecords)(records, domainId, new Date().toISOString());
42
- if (accepted.length)
43
- index_js_1.TwinEngine.ingestLive(domainId, instanceId, accepted);
66
+ /*
67
+ * 커널이 **몇 건을 실제로 받았나**를 함께 본다. 트윈이 라이브로 돌지 않으면 `ingestLive` 0 을
68
+ * 돌려주고, 그 차이가 곧 「바닥에 버려진 수」다. 이것을 통과로 세면 멈춘 트윈이 100% 로 보인다.
69
+ */
70
+ const applied = accepted.length ? index_js_1.TwinEngine.ingestLive(domainId, instanceId, accepted) : 0;
71
+ const undelivered = accepted.length - applied;
44
72
  /*
45
73
  * **거부한 것을 조용히 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이
46
74
  * 틀리면 트윈은 「아무 일도 일어나지 않는」 모습이 되고, 로그에도 아무 흔적이 없었다.
47
75
  *
48
- * 지금은 이유와 함께 남긴다. 화면까지 나르는 것은 다음 일이다(라이브 계측은 초당 여러 건이라
49
- * 건마다 방송하면 자체가 부하가 된다 집계해 자리가 필요하다).
76
+ * 그다음 자리였던 「화면까지 나르기」를 이제 채운다. 건마다 브로드캐스팅하지 않고 **집계**한다 통과분까지
77
+ * 함께 세는 이유는, 거부만 세면 「유입 없음」과 「정상」이 거부 0 으로 같아 보이기 때문이다.
78
+ * 조회는 `twinIngestHealth`.
79
+ */
80
+ /*
81
+ * **읽었다는 것 자체가 사실이다** — 빈 읽기도 성공이므로 단절 기록을 지운다(§`onReadFailure`).
82
+ * 그 둘을 섞으면 조용한 원본이 끊긴 원본으로 보인다.
50
83
  */
84
+ index_js_1.TwinEngine.clearIngestReadFailure(domainId, instanceId);
85
+ index_js_1.TwinEngine.recordIngestResult(domainId, instanceId, offered, rejected, Date.now(), undelivered);
51
86
  if (rejected.length) {
52
87
  const why = rejected[0]?.errors?.join(' · ') ?? 'unknown';
53
- console.warn(`[twin-live] "${instanceId}": ${rejected.length} record(s) rejected — e.g. ${why}`);
88
+ (0, log_js_1.twinWarn)(`[twin-live] "${instanceId}": ${rejected.length}/${offered} record(s) rejected — e.g. ${why}`);
54
89
  }
55
- });
90
+ /* 버려진 것은 로그에도 남긴다 — 화면을 보지 않는 사람도 알아야 한다(사실이 사라지는 중이다). */
91
+ if (undelivered > 0) {
92
+ (0, log_js_1.twinWarn)(`[twin-live] "${instanceId}": ${undelivered}/${offered} record(s) NOT delivered — the twin is not running live; ` +
93
+ `records are being discarded. Start the twin, or disconnect the feed.`);
94
+ }
95
+ }, continuity);
56
96
  feeds.set(instanceId, unsubscribe);
57
97
  return true;
58
98
  }
99
+ /**
100
+ * 그 원본의 읽기 커서를 불러오고, 움직이면 저장하는 통로를 만든다.
101
+ *
102
+ * 커서는 **원본(`TwinReference`)의 것**이지 인스턴스의 것이 아니다 — 한 원본이 여러 인스턴스를 낳고
103
+ * (`scopeSpec.produced`) 읽는 것은 한 번이다. 인스턴스마다 커서를 두면 같은 표를 여러 번 읽는다.
104
+ *
105
+ * 저장 실패는 **삼키지 않고 말한다.** 조용히 실패하면 다음 재기동에서 창이 다시 미끄러지고, 그 손실은
106
+ * 오류 없이 사라진다 — 이 컬럼을 만든 이유가 바로 그것이다.
107
+ */
108
+ async function loadLiveCursor(domainId,
109
+ /** 읽기 실패를 적을 트윈 — 커서는 레퍼런스의 것이지만 **건강은 인스턴스의 것**이다(한 레퍼런스가 여럿을 낳는다). */
110
+ instanceId, source) {
111
+ if (!source) {
112
+ /* 원본을 가리킬 수 없으면 저장할 자리도 없다 — 그 사실을 말한다(조용히 커서 없이 돌지 않게). */
113
+ (0, log_js_1.twinWarn)('[twin-live] live cursor not carried — the caller did not say which reference this feed reads from');
114
+ return undefined;
115
+ }
116
+ const repo = (0, shell_1.getRepository)(twin_reference_js_1.TwinReference);
117
+ const row = await repo.findOne({ where: { domain: { id: domainId }, source } }).catch(() => null);
118
+ if (!row) {
119
+ (0, log_js_1.twinWarn)(`[twin-live] live cursor not carried — no reference row for source "${source}"`);
120
+ return undefined;
121
+ }
122
+ const stored = row.liveCursor;
123
+ if (stored?.firstAttachedAt) {
124
+ (0, log_js_1.twinLog)(`[twin-live] "${source}": resuming from stored cursor — knowledge starts ${stored.firstAttachedAt}`);
125
+ }
126
+ return {
127
+ cursor: stored ?? undefined,
128
+ /*
129
+ * **원본에 닿지 못했다** — 어댑터가 알리면 상태에 적는다(§`onReadFailure`).
130
+ *
131
+ * 로그로는 어댑터가 이미 말하고 있었다(재시도 경고). 그러나 **로그는 사람이 볼 때만 값이 있다** —
132
+ * 화면이 말하려면 조회 가능한 상태에 있어야 한다.
133
+ *
134
+ * ── `status` 를 건드리지 않는다 (2026-08-23, 되돌린 자리) ────────────────────
135
+ * 처음에는 레퍼런스 행의 `status` 를 `'error'` 로도 적었다 — 「목록이 「연결됨」이라 말하면서 실제로는
136
+ * 못 읽는 상태를 없애려고」. **그것이 피드를 영구히 껐다.**
137
+ *
138
+ * 부팅 재부착은 `status: 'connected'` 인 레퍼런스만 되살린다(§`resumeReferenceLiveFeeds`). 그래서
139
+ * 원본이 **잠깐** 끊긴 것이 그 행을 `error` 로 만들고, 다음 재기동에서 그 피드는 아예 붙지 않았다 —
140
+ * 실측으로 그렇게 났다(09:25 판에 `[twin-live]` 줄이 한 줄도 없었다).
141
+ *
142
+ * 두 축을 한 칸에 섞은 것이 원인이다:
143
+ * `status` **연결 설정이 성립하나** — 접속 시험·마스터 동기가 정하는 축(재부착의 자격이다)
144
+ * 읽기 실패 **지금 닿나** — 오갈 수 있는 사실이고, 유입 장부가 그 축을 갖는다
145
+ *
146
+ * 그러니 여기서는 `lastError` 에만 적는다(사람이 목록에서 이유를 볼 수 있게). 「지금 닿나」는
147
+ * `ingestHealth().readFailure` 가 답하고, 성공한 주기가 그것을 지운다.
148
+ */
149
+ onReadFailure: ({ reason, stream }) => {
150
+ index_js_1.TwinEngine.recordIngestReadFailure(domainId, instanceId, reason, Date.now(), stream);
151
+ (0, log_js_1.twinWarn)(`[twin-live] "${source}": cannot read from the source${stream ? ` (${stream})` : ''} — ${reason}`);
152
+ repo
153
+ .update({ id: row.id }, { lastError: `live: ${reason}` })
154
+ .catch(err => (0, log_js_1.twinWarn)(`[twin-live] "${source}": read failure not recorded — ${err?.message ?? err}`));
155
+ },
156
+ onCursor: next => {
157
+ /* 어댑터가 준 모양을 그대로 적는다 — 이 층은 흐름의 뜻을 모른다(열쇠는 어댑터가 정한다). */
158
+ repo
159
+ .update({ id: row.id }, { liveCursor: next })
160
+ .catch(err => (0, log_js_1.twinWarn)(`[twin-live] "${source}": live cursor not saved — ${err?.message ?? err}`));
161
+ }
162
+ };
163
+ }
59
164
  /** 라이브 피드 중지(unsubscribe). */
60
165
  function stopReferenceLiveFeed(instanceId) {
61
166
  const unsub = feeds.get(instanceId);
@@ -117,7 +222,7 @@ async function resumeReferenceLiveFeeds() {
117
222
  const rows = await (0, shell_1.getRepository)(twin_instance_js_1.TwinInstance).find({
118
223
  where: produced.map((p) => ({ domain: { id: domainId }, instanceId: p.instanceId }))
119
224
  });
120
- const live = produced.filter((p) => rows.some(r => r.instanceId === p.instanceId && r.status === 'running' && r.realityMode === 'mirror'));
225
+ const live = produced.filter((p) => rows.some(r => r.instanceId === p.instanceId && r.status === 'running' && r.restartPolicy === 'resync'));
121
226
  if (!live.length)
122
227
  continue;
123
228
  let sites;
@@ -147,7 +252,7 @@ async function resumeReferenceLiveFeeds() {
147
252
  }
148
253
  attached.set(p.instanceId, ref.source);
149
254
  try {
150
- const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId);
255
+ const ok = await startReferenceLiveFeed(domainId, ref.adapterType, ref.connectionConfig ?? {}, site, p.instanceId, ref.source);
151
256
  if (ok)
152
257
  resumed.push(p.instanceId);
153
258
  else
@@ -160,13 +265,13 @@ async function resumeReferenceLiveFeeds() {
160
265
  }
161
266
  }
162
267
  catch (e) {
163
- console.error('[twin-live] feed resume scan failed', e?.message);
268
+ (0, log_js_1.twinError)('[twin-live] feed resume scan failed', e?.message);
164
269
  }
165
270
  if (resumed.length)
166
- console.log(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`);
271
+ (0, log_js_1.twinLog)(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`);
167
272
  /* **조용히 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */
168
273
  for (const f of failed)
169
- console.warn(`[twin-live] "${f.instanceId}" feed NOT reattached — ${f.error}`);
274
+ (0, log_js_1.twinWarn)(`[twin-live] "${f.instanceId}" feed NOT reattached — ${f.error}`);
170
275
  return { resumed, failed };
171
276
  }
172
277
  /*
@@ -1 +1 @@
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"]}
1
+ {"version":3,"file":"reference-live.js","sourceRoot":"","sources":["../../../server/service/reference/reference-live.ts"],"names":[],"mappings":"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","sourcesContent":["import { twinLog, twinWarn, twinError } from '../../engine/log.js'\nimport { getRepository } from '@things-factory/shell'\n\nimport { getAdapter, type ConnectionConfig, type LiveFeedContinuity, 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 프레임 ① restartPolicy='resync').\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 /**\n * **어느 레퍼런스에서 읽는 피드인가** — 읽기 커서를 저장할 자리를 가리킨다(§`loadLiveCursor`).\n *\n * 부르는 쪽이 `TwinReference` 를 이미 들고 있으므로 그 이름을 넘긴다. 처음에는 `cfg`·`site` 에서\n * 찾으려 했는데 **둘 다 그 값을 가질 수 없다**: `cfg` 는 사람이 화면에서 입력한 접속 설정이고\n * (`endpoint`·`token`…), `site` 는 커넥터가 원본에서 발견한 것이다 — **레퍼런스의 이름은 호스트가\n * 붙이는 것이고 원본은 그것을 모른다.** 커넥터가 채울 수 없는 값을 커넥터에서 읽으려 한 것이 잘못이었다.\n *\n * 주지 않으면 커서를 잇지 않고 **그 사실을 말한다**(조용히 창이 미끄러지지 않게).\n */\n source?: 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 // 관측 기동 — 커널 tick 없이 projector 가 실 이벤트를 반영한다(startLive 가 restartPolicy='resync' 로 적는다).\n // 시각 기준은 **공간**이 갖는다(테넌트 아님) — 교대의 HH:MM 을 어느 기준으로 읽나.\n TwinEngine.startLive(instanceId, domainId, det.kind, await TwinEngine.withSpaceTimeBase(det.model, domainId))\n\n stopReferenceLiveFeed(instanceId) // 재기동 시 이전 피드 정리\n /*\n * **어디까지 읽었나를 이 층이 들고 건넨다**(§`LiveFeedContinuity`, 2026-08-23).\n *\n * 어댑터가 이 값을 자기 사물함에 두면 원본이 늘 때마다 저장 기제가 늘고, 그중 하나가 조용히 다르게\n * 동작한다. 그리고 커널이 이미 갈라 두었다: 「원천이 애초에 다시 말해 주지 않는 축」은 재기동\n * 연속성으로 이어받는다(§`hydrateContinuity`) — 읽기 커서가 정확히 그 성질이다.\n *\n * 없으면 `undefined` 를 건넨다 — 그것이 「첫 붙음」이라는 사실이고, 그때만 어댑터가 되돌아볼 날수로\n * 첫 창을 만든다. 빈 객체로 메우지 않는다(그러면 첫 붙음과 이어 붙음이 같아진다).\n */\n const continuity = await loadLiveCursor(domainId, instanceId, source)\n const unsubscribe = adapter.openLiveFeed(cfg, site, (records: unknown[]) => {\n if (!TwinEngine.owns(domainId, instanceId)) {\n stopReferenceLiveFeed(instanceId) // 인스턴스 사라지면 자가 정지(안전망)\n return\n }\n const offered = Array.isArray(records) ? records.length : records ? 1 : 0\n const { accepted, rejected } = ingestCanonicalRecords(records as CanonicalRecord[], domainId, new Date().toISOString())\n /*\n * 커널이 **몇 건을 실제로 받았나**를 함께 본다. 트윈이 라이브로 돌지 않으면 `ingestLive` 가 0 을\n * 돌려주고, 그 차이가 곧 「바닥에 버려진 수」다. 이것을 통과로 세면 멈춘 트윈이 100% 로 보인다.\n */\n const applied = accepted.length ? TwinEngine.ingestLive(domainId, instanceId, accepted) : 0\n const undelivered = accepted.length - applied\n /*\n * **거부한 것을 조용히 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이\n * 틀리면 트윈은 「아무 일도 일어나지 않는」 모습이 되고, 로그에도 아무 흔적이 없었다.\n *\n * 그다음 자리였던 「화면까지 나르기」를 이제 채운다. 건마다 브로드캐스팅하지 않고 **집계**한다 — 통과분까지\n * 함께 세는 이유는, 거부만 세면 「유입 없음」과 「정상」이 둘 다 거부 0 으로 같아 보이기 때문이다.\n * 조회는 `twinIngestHealth`.\n */\n /*\n * **읽었다는 것 자체가 사실이다** — 빈 읽기도 성공이므로 단절 기록을 지운다(§`onReadFailure`).\n * 그 둘을 섞으면 조용한 원본이 끊긴 원본으로 보인다.\n */\n TwinEngine.clearIngestReadFailure(domainId, instanceId)\n TwinEngine.recordIngestResult(domainId, instanceId, offered, rejected, Date.now(), undelivered)\n if (rejected.length) {\n const why = rejected[0]?.errors?.join(' · ') ?? 'unknown'\n twinWarn(`[twin-live] \"${instanceId}\": ${rejected.length}/${offered} record(s) rejected — e.g. ${why}`)\n }\n /* 버려진 것은 로그에도 남긴다 — 화면을 보지 않는 사람도 알아야 한다(사실이 사라지는 중이다). */\n if (undelivered > 0) {\n twinWarn(\n `[twin-live] \"${instanceId}\": ${undelivered}/${offered} record(s) NOT delivered — the twin is not running live; ` +\n `records are being discarded. Start the twin, or disconnect the feed.`\n )\n }\n }, continuity)\n feeds.set(instanceId, unsubscribe)\n return true\n}\n\n/**\n * 그 원본의 읽기 커서를 불러오고, 움직이면 저장하는 통로를 만든다.\n *\n * 커서는 **원본(`TwinReference`)의 것**이지 인스턴스의 것이 아니다 — 한 원본이 여러 인스턴스를 낳고\n * (`scopeSpec.produced`) 읽는 것은 한 번이다. 인스턴스마다 커서를 두면 같은 표를 여러 번 읽는다.\n *\n * 저장 실패는 **삼키지 않고 말한다.** 조용히 실패하면 다음 재기동에서 창이 다시 미끄러지고, 그 손실은\n * 오류 없이 사라진다 — 이 컬럼을 만든 이유가 바로 그것이다.\n */\nasync function loadLiveCursor(\n domainId: string,\n /** 읽기 실패를 적을 트윈 — 커서는 레퍼런스의 것이지만 **건강은 인스턴스의 것**이다(한 레퍼런스가 여럿을 낳는다). */\n instanceId: string,\n source?: string\n): Promise<LiveFeedContinuity | undefined> {\n if (!source) {\n /* 원본을 가리킬 수 없으면 저장할 자리도 없다 — 그 사실을 말한다(조용히 커서 없이 돌지 않게). */\n twinWarn('[twin-live] live cursor not carried — the caller did not say which reference this feed reads from')\n return undefined\n }\n const repo = getRepository(TwinReference)\n const row = await repo.findOne({ where: { domain: { id: domainId } as any, source } }).catch(() => null)\n if (!row) {\n twinWarn(`[twin-live] live cursor not carried — no reference row for source \"${source}\"`)\n return undefined\n }\n const stored = row.liveCursor\n if (stored?.firstAttachedAt) {\n twinLog(`[twin-live] \"${source}\": resuming from stored cursor — knowledge starts ${stored.firstAttachedAt}`)\n }\n return {\n cursor: stored ?? undefined,\n /*\n * **원본에 닿지 못했다** — 어댑터가 알리면 상태에 적는다(§`onReadFailure`).\n *\n * 로그로는 어댑터가 이미 말하고 있었다(재시도 경고). 그러나 **로그는 사람이 볼 때만 값이 있다** —\n * 화면이 말하려면 조회 가능한 상태에 있어야 한다.\n *\n * ── `status` 를 건드리지 않는다 (2026-08-23, 되돌린 자리) ────────────────────\n * 처음에는 레퍼런스 행의 `status` 를 `'error'` 로도 적었다 — 「목록이 「연결됨」이라 말하면서 실제로는\n * 못 읽는 상태를 없애려고」. **그것이 피드를 영구히 껐다.**\n *\n * 부팅 재부착은 `status: 'connected'` 인 레퍼런스만 되살린다(§`resumeReferenceLiveFeeds`). 그래서\n * 원본이 **잠깐** 끊긴 것이 그 행을 `error` 로 만들고, 다음 재기동에서 그 피드는 아예 붙지 않았다 —\n * 실측으로 그렇게 났다(09:25 판에 `[twin-live]` 줄이 한 줄도 없었다).\n *\n * 두 축을 한 칸에 섞은 것이 원인이다:\n * `status` **연결 설정이 성립하나** — 접속 시험·마스터 동기가 정하는 축(재부착의 자격이다)\n * 읽기 실패 **지금 닿나** — 오갈 수 있는 사실이고, 유입 장부가 그 축을 갖는다\n *\n * 그러니 여기서는 `lastError` 에만 적는다(사람이 목록에서 이유를 볼 수 있게). 「지금 닿나」는\n * `ingestHealth().readFailure` 가 답하고, 성공한 주기가 그것을 지운다.\n */\n onReadFailure: ({ reason, stream }) => {\n TwinEngine.recordIngestReadFailure(domainId, instanceId, reason, Date.now(), stream)\n twinWarn(`[twin-live] \"${source}\": cannot read from the source${stream ? ` (${stream})` : ''} — ${reason}`)\n repo\n .update({ id: row.id }, { lastError: `live: ${reason}` } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": read failure not recorded — ${err?.message ?? err}`))\n },\n onCursor: next => {\n /* 어댑터가 준 모양을 그대로 적는다 — 이 층은 흐름의 뜻을 모른다(열쇠는 어댑터가 정한다). */\n repo\n .update({ id: row.id }, { liveCursor: next } as any)\n .catch(err => twinWarn(`[twin-live] \"${source}\": live cursor not saved — ${err?.message ?? err}`))\n }\n }\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.restartPolicy === 'resync')\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, ref.source)\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 twinError('[twin-live] feed resume scan failed', e?.message)\n }\n\n if (resumed.length) twinLog(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`)\n /* **조용히 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */\n for (const f of failed) twinWarn(`[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"]}
@@ -421,6 +421,21 @@ export interface ReferenceMaster {
421
421
  * BOM 을 설명 문서에만 적어 둔 채 키트를 못 만드는 트윈을 만든 원인). 근거는 커널 계약 주석에.
422
422
  */
423
423
  productionSpec?: unknown;
424
+ /**
425
+ * **이 원본이 식별자를 만드는 이름공간** — `TwinModelDef.identity` 로 통과한다.
426
+ *
427
+ * **선언이 없으면 커널이 거절한다** (0.7.45~). 예전에는 상수 프리픽스로 팔레트·트레일러·문서를
428
+ * 만들었는데(`0614141` — GS1 이 규격 예제로 쓰다 버린 값이고 `06…` 은 실 배정 대역이다), 그 값은
429
+ * 저널에 영원히 남으면서 지어낸 것이라고 말해 주는 데가 없었다. 그래서 커널이 만들지 않는다.
430
+ *
431
+ * 원본이 자기 도메인을 말하면 그 아래에서 만든다 — 그것이 표준 정합이다(CBV 2.0 §8.2.4·§8.5.5,
432
+ * 「그 인터넷 도메인 소유자가 배정한다」). GS1 키를 쓰려면 `documentTypes` + `companyPrefix` 이고,
433
+ * 그때 **자리 수가 맞아야 한다**(SGTIN 은 프리픽스 + 품목참조 = 13자리, GS1 TDS).
434
+ *
435
+ * 정체성은 생산과 무관하므로 `productionSpec` 이 아니라 **모델**이 갖는다 — 창고·야드는 아무것도
436
+ * 생산하지 않으면서 팔레트·트레일러를 식별한다.
437
+ */
438
+ identity?: unknown;
424
439
  /**
425
440
  * 사람 — 선언하지 않으면 **인원 제약이 없는 트윈**이 된다(설비만 있으면 언제나 가동되는 공장).
426
441
  * 공정이 인원을 요구하는데 사람이 없으면 그 작업은 시작되지 않는다 — 그것이 사실이므로 그대로 둔다.
@@ -449,6 +464,21 @@ export interface ReferenceMaster {
449
464
  * 놓고도 조용히 성공했다. 옮기는 사람만 아는 사실은 옮기는 자리에서 실어야 한다.
450
465
  */
451
466
  warnings?: IngestWarning[];
467
+ /**
468
+ * **원본이 가진 수** — 옮긴 수의 분모다.
469
+ *
470
+ * 총평은 도착한 모델만 볼 수 있어서 「레시피 1,261」까지만 말할 수 있었다. 그런데 사람이 묻는 것은
471
+ * 「그게 전부인가」다 — 원본에 1,408개가 있었고 147개를 옮기지 못했다면 그 사실이 총평의 첫 줄에
472
+ * 있어야 한다. 실제로 사용자가 품목 2,789 와 자재 1,789 의 차이를 화면에서 읽어 내지 못해 직접
473
+ * 물었다(2026-08-22).
474
+ *
475
+ * 분모를 아는 것은 **커넥터뿐이다.** 그래서 커넥터가 실어 보낸다. 보내지 않으면 총평은 분모 없이
476
+ * 옮긴 수만 말한다 — **지어내지 않는다.**
477
+ *
478
+ * 열쇠는 총평이 세는 이름과 같다(`recipes`·`materials`·`totalInputs`·…). 값은 **원본을 끝까지 읽어서
479
+ * 센 것**이어야 한다. 잘린 읽기의 수를 여기 담으면 「전부 옮겼다」는 거짓이 된다.
480
+ */
481
+ originCounts?: Record<string, number>;
452
482
  }
453
483
  export interface IngestedTwin {
454
484
  model: any;
@@ -495,8 +525,24 @@ export interface IngestWarning {
495
525
  * 그 가정 위에 선다) 대신 **그 조합을 말한다** — 공간에 시간대를 선언하면 사라지는 경고다.
496
526
  */
497
527
  export declare const generationWithoutTimeZone: (spaceId: string, equipmentId: string) => IngestWarning;
498
- /** 커널 카탈로그가 모르는 자리 타입 — 그 자리는 흐름에 참여하지 못할 수 있다. */
528
+ /**
529
+ * 아무도 선언하지 않은 자리 타입 — 그 자리는 흐름에 참여하지 못할 수 있다.
530
+ *
531
+ * **묻는 상대를 고쳤다 (2026-08-20).** 예전에는 내장 카탈로그에게만 물었다. 그런데 원본이 자기
532
+ * `DomainDefinition.locationTypes` 를 함께 보내면 **그 선언이 정본**이다 — 카탈로그는 우리가 아는
533
+ * 도메인들의 기본 어휘일 뿐이다. 물을 상대가 틀렸으니, 자기 어휘를 선언한 원본은 정상적인 타입마다
534
+ * 경고를 받았다(F&B 라인을 붙이면 자리마다 하나씩). 소음이 되면 사람은 경고를 읽지 않게 되고, 그때
535
+ * 진짜 경고도 함께 죽는다.
536
+ */
499
537
  export declare const unknownLocationType: (system: string, type: string, id: string) => IngestWarning;
538
+ /**
539
+ * 원본이 함께 보낸 자리 타입 선언 — 있으면 **이것이 정본**이다.
540
+ *
541
+ * 커널 계약의 `DomainDefinition.locationTypes` 는 `{ key, label, … }` 목록이다. 읽을 수 없는 모양이면
542
+ * `undefined` 를 돌려 **카탈로그로 떨어진다** — 빈 배열을 돌려주면 「아무 타입도 선언되지 않았다」가
543
+ * 되어 모든 자리가 경고를 받는다(없는 것과 못 읽은 것은 다르다).
544
+ */
545
+ export declare function declaredLocationTypes(productionSpec: unknown): string[] | undefined;
500
546
  /** 시간대 문자열을 못 읽었다 — 시각 해석이 UTC 로 떨어진다(조용히 현지 시각으로 가정하지 않는다). */
501
547
  export declare const unknownTimezone: (timezone: string) => IngestWarning;
502
548
  /** 작업 정의에 소요시간·파라미터가 없다 — 커널 기본값으로 동작한다. */
@@ -10,6 +10,7 @@
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.siteEquipmentAdopted = exports.knobDefaultsFilled = exports.describeWarnings = exports.connectorMappingFallback = exports.structureAdopted = exports.projectionFailed = exports.unresolvedReference = exports.noViewableRepresentation = exports.operationWithoutDuration = exports.unknownTimezone = exports.unknownLocationType = exports.generationWithoutTimeZone = void 0;
12
12
  exports.utcOffsetOf = utcOffsetOf;
13
+ exports.declaredLocationTypes = declaredLocationTypes;
13
14
  exports.masterToTwin = masterToTwin;
14
15
  exports.pickOriginSite = pickOriginSite;
15
16
  /**
@@ -44,7 +45,7 @@ function utcOffsetOf(timezone, at = new Date()) {
44
45
  return sign * (Number(m[2]) * 60 + Number(m[3] ?? 0));
45
46
  }
46
47
  catch {
47
- return undefined; // 알 수 없는 이름 — Intl 이 던진다
48
+ return undefined; // 알 수 없는 이름 — Intl 이 오류를 낸다
48
49
  }
49
50
  }
50
51
  /**
@@ -66,13 +67,36 @@ const generationWithoutTimeZone = (spaceId, equipmentId) => ({
66
67
  'so the generation peak will not land at local noon. Declare the space time zone (we do not guess one).'
67
68
  });
68
69
  exports.generationWithoutTimeZone = generationWithoutTimeZone;
69
- /** 커널 카탈로그가 모르는 자리 타입 — 그 자리는 흐름에 참여하지 못할 수 있다. */
70
+ /**
71
+ * 아무도 선언하지 않은 자리 타입 — 그 자리는 흐름에 참여하지 못할 수 있다.
72
+ *
73
+ * **묻는 상대를 고쳤다 (2026-08-20).** 예전에는 내장 카탈로그에게만 물었다. 그런데 원본이 자기
74
+ * `DomainDefinition.locationTypes` 를 함께 보내면 **그 선언이 정본**이다 — 카탈로그는 우리가 아는
75
+ * 도메인들의 기본 어휘일 뿐이다. 물을 상대가 틀렸으니, 자기 어휘를 선언한 원본은 정상적인 타입마다
76
+ * 경고를 받았다(F&B 라인을 붙이면 자리마다 하나씩). 소음이 되면 사람은 경고를 읽지 않게 되고, 그때
77
+ * 진짜 경고도 함께 죽는다.
78
+ */
70
79
  const unknownLocationType = (system, type, id) => ({
71
80
  code: 'unknown-location-type',
72
81
  params: { system, type, id },
73
- message: `"${type}" is not a known ${system} location type (${id}) — the kernel may not include this location in the flow`
82
+ message: `"${type}" is declared nowhere for ${system} (${id}) — neither in the master's own locationTypes nor in the built-in ` +
83
+ `catalog, so the kernel may not include this location in the flow`
74
84
  });
75
85
  exports.unknownLocationType = unknownLocationType;
86
+ /**
87
+ * 원본이 함께 보낸 자리 타입 선언 — 있으면 **이것이 정본**이다.
88
+ *
89
+ * 커널 계약의 `DomainDefinition.locationTypes` 는 `{ key, label, … }` 목록이다. 읽을 수 없는 모양이면
90
+ * `undefined` 를 돌려 **카탈로그로 떨어진다** — 빈 배열을 돌려주면 「아무 타입도 선언되지 않았다」가
91
+ * 되어 모든 자리가 경고를 받는다(없는 것과 못 읽은 것은 다르다).
92
+ */
93
+ function declaredLocationTypes(productionSpec) {
94
+ const list = productionSpec?.definition?.locationTypes;
95
+ if (!Array.isArray(list) || !list.length)
96
+ return undefined;
97
+ const keys = list.map((t) => (typeof t === 'string' ? t : t?.key)).filter((k) => typeof k === 'string' && k);
98
+ return keys.length ? keys : undefined;
99
+ }
76
100
  /** 시간대 문자열을 못 읽었다 — 시각 해석이 UTC 로 떨어진다(조용히 현지 시각으로 가정하지 않는다). */
77
101
  const unknownTimezone = (timezone) => ({
78
102
  code: 'unknown-timezone',
@@ -177,7 +201,11 @@ exports.siteEquipmentAdopted = siteEquipmentAdopted;
177
201
  function masterToTwin(master, catalog) {
178
202
  /* 커넥터가 옮기며 알게 된 것을 먼저 싣는다 — 원인이 앞에 오는 편이 읽기 쉽다(대체가 먼저, 그 결과가 뒤). */
179
203
  const warnings = [...(master.warnings ?? [])];
180
- const known = catalog?.[master.system]?.locationTypes ?? [];
204
+ /*
205
+ * 원본이 자기 어휘를 선언했으면 **그것에 대조한다.** 내장 카탈로그는 그 선언이 없을 때의 기본이다 —
206
+ * 상대를 바꾸지 않으면, 자기 어휘를 선언한 원본이 정상적인 자리마다 경고를 받는다.
207
+ */
208
+ const known = declaredLocationTypes(master.productionSpec) ?? catalog?.[master.system]?.locationTypes ?? [];
181
209
  const locationEntries = master.locations.filter(l => l.role === 'location' || l.role === 'both');
182
210
  const areaLocs = master.locations.filter(l => l.role === 'area' || l.role === 'both');
183
211
  const locations = locationEntries.map(l => {
@@ -276,6 +304,8 @@ function masterToTwin(master, catalog) {
276
304
  locations, equipment, layout, spaceId,
277
305
  /* 생산 정의도 손대지 않고 통과 — 경계에서 어휘를 갈지 않는다(커널이 그대로 소비한다). */
278
306
  ...(master.productionSpec ? { productionSpec: master.productionSpec } : {}),
307
+ /* 정체성 선언도 그대로 통과한다 — 없으면 만들지 않는다(커널이 레거시 상수로 떨어진다). */
308
+ ...(master.identity ? { identity: master.identity } : {}),
279
309
  ...(materialDefinitions?.length ? { materialDefinitions } : {}),
280
310
  ...(materialClasses?.length ? { materialClasses } : {}),
281
311
  ...(utcOffsetMinutes !== undefined ? { utcOffsetMinutes } : {}),