@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
@@ -1,8 +1,12 @@
1
+ import { pubsub } from '@things-factory/shell'
2
+ import { HOST_STEPS, type ReferenceProgress } from './reference-progress.js'
3
+ import { assessMaster } from './reference-assessment.js'
1
4
  import { Arg, Ctx, Mutation, Query, Resolver, Directive} from 'type-graphql'
2
5
 
3
6
  import { ScalarObject, getRepository } from '@things-factory/shell'
4
7
 
5
8
  import { TwinEngine } from '../../engine/index.js'
9
+ import { readRestartPolicy } from '../../engine/restart-policy.js'
6
10
  import { readBoardEquipment } from '@operato/twin-kernel'
7
11
 
8
12
  import { applyKnobDefaults, filledKnobKeys } from './knob-defaults.js'
@@ -140,9 +144,13 @@ export class TwinReferenceResolver {
140
144
 
141
145
  /** 소스를 인제스트 → 공간 + 인스턴스 생성(provision). start 면 기동까지. */
142
146
  @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
143
- @Mutation(returns => ScalarObject, { description: 'Ingest a registered reference source into a twin (space + instance). Optionally start it.' })
147
+ @Mutation(returns => ScalarObject, {
148
+ description:
149
+ "Ingest a registered reference source into a twin (space + instance). Optionally start it. restartPolicy declares how the new twin treats its own past on reboot: 'resync' (read the source again) | 'resume' (continue its journal) | 'reset' (wipe and replay from the seed). It has no default — a twin cannot exist without the declaration."
150
+ })
144
151
  async importFromReference(
145
152
  @Arg('source') source: string,
153
+ @Arg('restartPolicy') restartPolicy: string,
146
154
  @Ctx() context: ResolverContext,
147
155
  @Arg('start', { nullable: true }) start?: boolean
148
156
  ): Promise<any> {
@@ -153,7 +161,13 @@ export class TwinReferenceResolver {
153
161
  /* 어디서 왔는지 남긴다 — 이것이 없으면 이 트윈은 두 번 다시 읽을 수 없다.
154
162
  이 경로는 **메모리에 등록된 마스터 하나**를 그대로 쓰므로 사이트 서술자가 없다(사이트 정체를
155
163
  적을 것이 없다). 다시 읽기도 같은 등록본을 집으므로 사이트를 찾을 필요가 없다. */
156
- const res = await TwinEngine.ingestMaster(domainId, { ...master, origin: { kind: 'reference', source } }, undefined, context.state.user)
164
+ const res = await TwinEngine.ingestMaster(
165
+ domainId,
166
+ { ...master, origin: { kind: 'reference', source } },
167
+ readRestartPolicy(restartPolicy, 'importFromReference'),
168
+ undefined,
169
+ context.state.user
170
+ )
157
171
  if (start) await TwinEngine.startFromRegistry(domainId, res.instanceId)
158
172
  return { ok: true, instanceId: res.instanceId, spaceId: res.spaceId, warnings: res.warnings }
159
173
  } catch (e: any) {
@@ -196,23 +210,96 @@ export class TwinReferenceResolver {
196
210
  error: `twin "${instanceId}" does not remember where it came from — it cannot be re-read. Re-create it from its template or reference so the origin is recorded.`
197
211
  }
198
212
 
213
+ /*
214
+ * 진행을 알리는 통로 — 만들 때와 **같은 것**이다. 레퍼런스에서 온 트윈만 알린다(템플릿에서 온
215
+ * 트윈은 원본을 읽지 않으므로 알릴 단계가 없다).
216
+ *
217
+ * 분모는 그 어댑터가 선언한 단계 수다(`masterSteps`) + 호스트 단계. 선언하지 않은 어댑터는
218
+ * `total` 이 없고, 그때 화면은 퍼센트를 만들지 않는다 — 모르는 진행률을 지어내지 않는다.
219
+ */
220
+ const progressSource: string | undefined = origin.kind === 'reference' ? origin.source : undefined
221
+ const refRow = progressSource
222
+ ? await getRepository(TwinReference).findOne({ where: { domain: { id: domainId }, source: progressSource } })
223
+ : null
224
+ const adapterForSteps = refRow ? getAdapter(refRow.adapterType) : undefined
225
+ const declaredSteps = adapterForSteps?.masterSteps?.length
226
+ const stepTotal = declaredSteps === undefined ? undefined : declaredSteps + HOST_STEPS.length
227
+ const siteId = origin.siteId ?? instanceId
228
+ let lastDone = 0
229
+ let lastStep: ReferenceProgress['step'] = (adapterForSteps?.masterSteps?.[0] ?? 'provision') as ReferenceProgress['step']
230
+ const emit = (p: Omit<ReferenceProgress, 'siteCount' | 'siteId' | 'siteIndex'>) => {
231
+ if (!progressSource) return
232
+ lastDone = p.done
233
+ lastStep = p.step
234
+ pubsub.publish('twin-reference-progress', {
235
+ twinReferenceProgress: { ...p, siteId, siteIndex: 0, siteCount: 1, total: p.total ?? stepTotal, source: progressSource }
236
+ })
237
+ }
238
+
199
239
  try {
200
- const master = await this.masterFromOrigin(domainId, origin, instanceId)
240
+ const master = await this.masterFromOrigin(domainId, origin, instanceId, st =>
241
+ emit({ step: st.key, done: st.done, total: stepTotal, detail: st.detail })
242
+ )
243
+ emit({ step: 'provision', done: declaredSteps ?? 0, total: stepTotal })
201
244
  /*
202
245
  * **어느 현장의 트윈이었나를 지킨다** — 다시 읽기는 구조만 새로 그리는 일이고, 현장 소속은 그때
203
246
  * 사람이 고른 것이다. 기억이 없는 옛 트윈은 **지금 있는 현장**을 그대로 쓴다(빈손으로 두면 템플릿
204
247
  * 기본 공간으로 옮겨 간다 — 조용히 현장을 떠나고 그 현장의 값도 잃는다).
205
248
  */
206
249
  const keepSpace = origin.spaceId || inst.spaceId
207
- const res = await TwinEngine.ingestMaster(domainId, master, keepSpace ? { spaceId: keepSpace } : undefined, context.state.user)
250
+ /* 다시 읽기는 **구조만** 새로 그린다 재기동 정책은 트윈의 선언이므로 그대로 지킨다. */
251
+ const res = await TwinEngine.ingestMaster(
252
+ domainId,
253
+ master,
254
+ readRestartPolicy(inst.restartPolicy, `resync of twin "${instanceId}"`),
255
+ keepSpace ? { spaceId: keepSpace } : undefined,
256
+ context.state.user
257
+ )
258
+ emit({ step: 'start', done: (declaredSteps ?? 0) + 1, total: stepTotal })
259
+ /*
260
+ * **총평도 새로 남긴다.** 다시 읽었는데 총평이 옛것이면 화면이 지난 읽기를 현재로 보여 준다 —
261
+ * 그 표를 보고 사람이 원본을 고치러 간다.
262
+ */
263
+ if (refRow && progressSource) {
264
+ const prevSpec = (refRow.scopeSpec as any) ?? {}
265
+ await getRepository(TwinReference).update(
266
+ { id: refRow.id },
267
+ {
268
+ scopeSpec: { ...prevSpec, assessments: { ...(prevSpec.assessments ?? {}), [siteId]: assessMaster(master, siteId) } }
269
+ } as any
270
+ )
271
+ }
208
272
  return { ok: true, instanceId: res.instanceId, spaceId: res.spaceId, warnings: res.warnings }
209
273
  } catch (e: any) {
274
+ /* 막힌 자리를 그대로 알린다 — 「끝낸 수」의 다음 단계다(만들 때와 같은 규칙). */
275
+ emit({
276
+ step: (adapterForSteps?.masterSteps?.[lastDone] ?? lastStep) as ReferenceProgress['step'],
277
+ done: lastDone,
278
+ total: stepTotal,
279
+ failed: true,
280
+ error: e?.message ?? String(e)
281
+ })
210
282
  return refusal(context, e, 'resync failed')
211
283
  }
212
284
  }
213
285
 
214
286
  /** 기억해 둔 원천에서 마스터를 다시 얻는다 — 어댑터든 템플릿이든 이 한 곳에서 갈린다. */
215
- private async masterFromOrigin(domainId: string, origin: any, instanceId: string): Promise<any> {
287
+ private async masterFromOrigin(
288
+ domainId: string,
289
+ origin: any,
290
+ instanceId: string,
291
+ /*
292
+ * **다시 읽기도 진행을 보여야 한다** (2026-08-22).
293
+ *
294
+ * 만들 때는 8단계가 화면에 흐르는데 다시 읽을 때는 아무것도 없었다 — 사용자가 버튼을 누르고
295
+ * 「진행되고 있는지 확인할 수가 없다」고 했다. 실제로 chef 는 원본 읽기가 14~21초이고 그 사이
296
+ * 화면에 아무 변화가 없다.
297
+ *
298
+ * 통로는 만들 때와 **같은 것**이다(`twin-reference-progress`). 화면도 같은 표시를 쓴다 —
299
+ * 두 벌을 만들면 한쪽만 고쳐지고 사람은 같은 일이 다르게 보이는 것을 결함으로 읽는다.
300
+ */
301
+ onStep?: (s: { key: any; done: number; total?: number; detail?: string }) => void
302
+ ): Promise<any> {
216
303
  if (origin.kind === 'template') {
217
304
  const tpl = getTemplate(origin.templateId)
218
305
  if (!tpl) throw new Error(`unknown template: ${origin.templateId} — this twin cannot be rebuilt`)
@@ -249,7 +336,7 @@ export class TwinReferenceResolver {
249
336
  * 다른 현장의 구조를 이 트윈에 덮는 것은 오류가 아니라 조용히 틀린 공장으로 나타난다.
250
337
  */
251
338
  const picked = pickOriginSite(sites, origin)
252
- if (picked.site) return { ...(await adapter.fetchMaster(cfg, picked.site)), origin }
339
+ if (picked.site) return { ...(await adapter.fetchMaster(cfg, picked.site, onStep)), origin }
253
340
  if (picked.reason === 'site-gone')
254
341
  throw new Error(
255
342
  `reference "${origin.source}" no longer reports site "${origin.siteId}" — refusing to overwrite this twin with a different site`
@@ -273,10 +360,14 @@ export class TwinReferenceResolver {
273
360
 
274
361
  /** 템플릿 + 노브 → 합성 마스터 → 인제스트(+기동+시나리오). 레퍼런스 import 와 같은 경로, 원천만 합성. */
275
362
  @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
276
- @Mutation(returns => ScalarObject, { description: 'Instantiate a twin template with params → synthetic master → ingest (+start+scenario). Returns {ok, instanceId, spaceId}.' })
363
+ @Mutation(returns => ScalarObject, {
364
+ description:
365
+ "Instantiate a twin template with params → synthetic master → ingest (+start+scenario). restartPolicy declares how the new twin treats its own past on reboot ('resync' | 'resume' | 'reset') — there is no default. Returns {ok, instanceId, spaceId}."
366
+ })
277
367
  async createFromTemplate(
278
368
  @Arg('templateId') templateId: string,
279
369
  @Arg('params', type => ScalarObject) params: any,
370
+ @Arg('restartPolicy') restartPolicy: string,
280
371
  @Ctx() context: ResolverContext,
281
372
  @Arg('start', { nullable: true }) start?: boolean,
282
373
  @Arg('spaceId', { nullable: true, description: 'Existing space to co-locate this twin into (add a lens to a site). Omit to create its own space.' }) spaceId?: string
@@ -310,7 +401,13 @@ export class TwinReferenceResolver {
310
401
  master.origin = { kind: 'template', templateId, params: effective, ...(spaceId ? { spaceId } : {}) }
311
402
  /* 공간 선택은 템플릿을 거치지 않는다 — 템플릿마다 이 노브를 알고 있어야 한다면 새 템플릿이
312
403
  조용히 무시하게 된다. 합성된 마스터에 한 곳에서 얹는다(레퍼런스 인제스트와 같은 경로). */
313
- const res = await TwinEngine.ingestMaster(domainId, master, { spaceId }, context.state.user)
404
+ const res = await TwinEngine.ingestMaster(
405
+ domainId,
406
+ master,
407
+ readRestartPolicy(restartPolicy, `createFromTemplate("${templateId}")`),
408
+ { spaceId },
409
+ context.state.user
410
+ )
314
411
  if (start !== false) {
315
412
  await TwinEngine.startFromRegistry(domainId, res.instanceId)
316
413
  if (scenario) {
@@ -340,8 +437,19 @@ export class TwinReferenceResolver {
340
437
  async twinReferences(@Ctx() context: ResolverContext): Promise<any[]> {
341
438
  const domainId = context.state.domain.id
342
439
  const rows = await getRepository(TwinReference).find({ where: { domain: { id: domainId } } })
343
- // provenance 라이브 대조 — 지워진 트윈은 producedCount 에서 자동 제외(상태 동기의 lazy 방식).
344
- const liveIds = new Set((await getRepository(TwinInstance).find({ where: { domain: { id: domainId } } })).map(i => i.instanceId))
440
+ /*
441
+ * provenance 라이브 대조 지워진 트윈은 producedCount 에서 자동 제외(상태 동기의 lazy 방식).
442
+ *
443
+ * **id 만 읽는다** (2026-08-21). 예전에는 인스턴스 행 전체를 읽었고, 그 행에는 **모델 JSON** 이
444
+ * 들어 있다(자리 수백 개 + 발자국 좌표). 그래서 이 목록을 여는 데 수 메가바이트를 읽고 파싱했고,
445
+ * 화면은 그동안 「아직 연결된 시스템이 없습니다」를 보이고 있었다 — 사용자에게는 「없다」는 답으로
446
+ * 읽힌다. 여기서 필요한 것은 살아 있는 id 집합 하나다.
447
+ */
448
+ const liveIds = new Set(
449
+ (await getRepository(TwinInstance).find({ where: { domain: { id: domainId } }, select: ['instanceId'] })).map(
450
+ i => i.instanceId
451
+ )
452
+ )
345
453
  return rows.map(r => {
346
454
  const produced = (((r.scopeSpec as any)?.produced ?? []) as any[]).filter(p => liveIds.has(p.instanceId))
347
455
  return {
@@ -417,15 +525,37 @@ export class TwinReferenceResolver {
417
525
  const secretKeys = new Set((getAdapter(r.adapterType)?.meta?.configSchema ?? []).filter(f => f.secret).map(f => f.key))
418
526
  const cfg: any = {}
419
527
  for (const [k, v] of Object.entries((r.connectionConfig as any) ?? {})) cfg[k] = secretKeys.has(k) ? (v ? '••••••' : '') : v
528
+ /*
529
+ * **이 연결이 만든 트윈 id 를 함께 낸다.** 상세 화면이 「이 연결이 잘 동기화되고 있나」를 물으려면
530
+ * 어느 트윈을 물어야 하는지 알아야 한다. 예전에는 개수(`producedCount`)만 있어서 화면이 id 를
531
+ * 알 방법이 없었다.
532
+ */
533
+ const producedInstanceIds = [
534
+ ...new Set(
535
+ ((r.scopeSpec as any)?.produced ?? [])
536
+ .map((p: any) => p?.instanceId)
537
+ .filter((x: any): x is string => typeof x === 'string' && !!x)
538
+ )
539
+ ]
420
540
  return {
421
541
  source: r.source, system: r.system, siteName: r.siteName, description: r.description,
422
- adapterType: r.adapterType, connectionConfig: cfg, status: r.status, lastSyncedAt: r.lastSyncedAt, lastError: r.lastError
542
+ adapterType: r.adapterType, connectionConfig: cfg, status: r.status, lastSyncedAt: r.lastSyncedAt, lastError: r.lastError,
543
+ producedInstanceIds
423
544
  }
424
545
  }
425
546
 
426
- @Query(returns => [ScalarObject], { description: 'List reference connector metadata (label, domain, config schema, availability) for the connect UI.' })
547
+ @Query(returns => [ScalarObject], { description: 'List reference connector metadata (label, domain, config schema, availability, capabilities) for the connect UI.' })
427
548
  referenceAdapters(): any[] {
428
- return listAdapters().map(a => ({ type: a.type, ...(a.meta ?? {}) }))
549
+ /*
550
+ * **능력을 함께 낸다** (2026-08-21).
551
+ *
552
+ * `capabilities` 는 어댑터 뿌리에 있고 `meta` 에는 없었다. 그래서 연결 화면이 「이 원본이 라이브를
553
+ * 낼 수 있나」를 알 수 없었고, 트윈을 만들 때 `live` 를 말하지 못해 **재기동 정책이 없다고 거절**됐다
554
+ * (실 연동에서 그것에 막혔다 — 미러는 정의상 `resync` 이므로 `live` 하나로 정해진다).
555
+ *
556
+ * 화면이 그 사실을 알아야 정책을 사람에게 물을지 스스로 정할지 가를 수 있다.
557
+ */
558
+ return listAdapters().map(a => ({ type: a.type, ...(a.meta ?? {}), capabilities: a.capabilities ?? [] }))
429
559
  }
430
560
 
431
561
  @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
@@ -545,14 +675,18 @@ export class TwinReferenceResolver {
545
675
  * 넣는 것도 거절(`refuseImportSpace`) — 서로 다른 현장이 조용히 합쳐지는 것을 막는다.
546
676
  */
547
677
  @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
548
- @Mutation(returns => ScalarObject, { description: 'Create twins from a reference: discover sites → filter by siteIds (if given) → fetch master(s) → provision. spaceId co-locates the twin into an existing space (one site only). start=false to only provision; live=true to run as a mirror (adapter openLiveFeed → live projector) instead of sim.' })
678
+ @Mutation(returns => ScalarObject, {
679
+ description:
680
+ "Create twins from a reference: discover sites → filter by siteIds (if given) → fetch master(s) → provision. spaceId co-locates the twin into an existing space (one site only). start=false to only provision; live=true to run as a mirror (adapter openLiveFeed → live projector) instead of sim. restartPolicy declares how each new twin treats its own past on reboot ('resync' | 'resume' | 'reset'); omit it and live=true implies 'resync' — a mirror reads the source again — while a simulation must declare 'resume' or 'reset' for itself."
681
+ })
549
682
  async importTwinReference(
550
683
  @Arg('source') source: string,
551
684
  @Ctx() context: ResolverContext,
552
685
  @Arg('siteIds', type => [String], { nullable: true }) siteIds?: string[],
553
686
  @Arg('start', { nullable: true }) start?: boolean,
554
687
  @Arg('live', { nullable: true }) live?: boolean,
555
- @Arg('spaceId', { nullable: true, description: 'Existing space to co-locate this twin into (add a lens to a site). Only when importing a single site. Omit to give each site its own space.' }) spaceId?: string
688
+ @Arg('spaceId', { nullable: true, description: 'Existing space to co-locate this twin into (add a lens to a site). Only when importing a single site. Omit to give each site its own space.' }) spaceId?: string,
689
+ @Arg('restartPolicy', { nullable: true, description: "How each new twin treats its own past on reboot: 'resync' | 'resume' | 'reset'. Required unless live=true (a mirror is 'resync' by definition)." }) restartPolicy?: string
556
690
  ): Promise<any> {
557
691
  const domainId = context.state.domain.id
558
692
  const repo = getRepository(TwinReference)
@@ -569,26 +703,98 @@ export class TwinReferenceResolver {
569
703
  if (refusal) return { ok: false, error: refusal.message, errorCode: refusal.code, errorParams: refusal.params }
570
704
  const instances: any[] = []
571
705
  const failed: any[] = []
572
- for (const site of sites) {
706
+ /*
707
+ * **진행을 구독으로 흘린다** (2026-08-21).
708
+ *
709
+ * 마스터 읽기는 원본에 여러 질의를 보내는 긴 작업이고, 화면은 그 사이 아무것도 알 수 없었다.
710
+ * 단계를 아는 것은 어댑터뿐이므로 어댑터가 말하고(표준 낱말), 호스트가 자기 두 단계(생성·기동)를
711
+ * 더해 흘린다.
712
+ *
713
+ * **분모는 어댑터가 선언한 것**이다(`masterSteps`). 선언하지 않은 어댑터는 `total` 이 없고, 그때
714
+ * 화면은 퍼센트를 만들지 않는다 — 모르는 진행률을 지어내지 않는다.
715
+ */
716
+ const declared = adapter.masterSteps?.length
717
+ const stepTotal = declared === undefined ? undefined : declared + HOST_STEPS.length
718
+ const emit = (p: Omit<ReferenceProgress, 'siteCount'>) =>
719
+ pubsub.publish('twin-reference-progress', {
720
+ twinReferenceProgress: { ...p, siteCount: sites.length, total: p.total ?? stepTotal, source }
721
+ })
722
+
723
+ /*
724
+ * **읽은 뒤의 총평을 사이트마다 남긴다** (2026-08-22).
725
+ *
726
+ * 총평은 읽는 순간에만 있었다 — 대화상자를 닫으면 사라지고, 다시 보려면 같은 읽기를 또 해야 했다.
727
+ * 그런데 사람이 그것을 묻는 자리는 「모델 살펴보기」다(트윈을 보다가 「이건 왜 비었나」를 묻는다).
728
+ * 그래서 레퍼런스 행에 남겨 그 화면이 언제든 읽게 한다.
729
+ *
730
+ * 사실만 담는다(`assessMaster`) — 문장은 화면이 만들고, 더 알고 싶을 때 AI 가 이 사실만 근거로
731
+ * 서술한다. 다시 읽으면 그 사이트의 총평이 새것으로 바뀐다(지난 읽기의 총평을 현재로 보이게
732
+ * 두지 않는다).
733
+ */
734
+ const assessments: Record<string, unknown> = {}
735
+
736
+ for (const [siteIndex, site] of sites.entries()) {
737
+ /*
738
+ * **실패를 실제로 막힌 단계에 적는다** (2026-08-22).
739
+ *
740
+ * 예전에는 실패를 무조건 `provision` 으로 적었다. 그래서 원본을 읽다 끊긴 것도 화면에는
741
+ * 「트윈 생성에서 실패」로 떴다 — 사람이 원인을 엉뚱한 자리에서 찾는다(실제로 그렇게 됐다).
742
+ *
743
+ * 규약이 답을 준다: `done` 은 **끝낸 단계 수**다(`ReferenceProgress.done`). 그러면 지금 막힌 것은
744
+ * 그 다음 단계, 즉 `masterSteps[done]` 이다. 그 표를 넘어서면(호스트 단계) 마지막으로 알린 것을 쓴다.
745
+ *
746
+ * 이 규칙은 두 가지 보고 방식에 함께 맞는다 — 단계 **시작**에 알리는 어댑터(`done` 이 아직 0)와
747
+ * 단계 **완료**에 알리는 어댑터(`done` 이 1 늘어난다) 모두 「끝낸 수」만 지키면 된다.
748
+ */
749
+ let lastDone = 0
750
+ let lastStep: ReferenceProgress['step'] = (adapter.masterSteps?.[0] ?? 'provision') as ReferenceProgress['step']
751
+ const track = (p: Omit<ReferenceProgress, 'siteCount'>) => {
752
+ lastDone = p.done
753
+ lastStep = p.step
754
+ emit(p)
755
+ }
573
756
  try {
574
- const master = await adapter.fetchMaster(cfg, site)
757
+ const master = await adapter.fetchMaster(cfg, site, st =>
758
+ track({ siteId: site.siteId, siteIndex, step: st.key, done: st.done, total: stepTotal, detail: st.detail })
759
+ )
575
760
  /* 어느 레퍼런스의 **어느 사이트**에서 왔는지 남긴다 — 다시 읽기가 같은 어댑터·같은 사이트로 돌아오려면
576
761
  사이트 정체가 있어야 한다(`source` 는 참조 이름이라 사이트 전부에 같다). */
762
+ /*
763
+ * 정책은 **선언이거나 의도에서 곧바로 따라오는 것**이다: `live=true` 는 미러를 띄우겠다는 뜻이고
764
+ * 미러는 정의상 `resync` 다(외부가 진실이므로 우리 과거를 이어 붙이지 않는다). 시뮬로 띄우려면
765
+ * 스스로 선언해야 한다 — 그 자리에서 우리가 고르면 재기동마다 이력을 지우는 트윈이 조용히 생긴다.
766
+ */
767
+ assessments[site.siteId] = assessMaster(master, site.siteId)
768
+ const policy = readRestartPolicy(restartPolicy ?? (live ? 'resync' : undefined), `importTwinReference("${source}")`)
769
+ track({ siteId: site.siteId, siteIndex, step: 'provision', done: declared ?? 0, total: stepTotal })
577
770
  const res = await TwinEngine.ingestMaster(
578
771
  domainId,
579
772
  { ...master, origin: { kind: 'reference', source, siteId: site.siteId, ...(spaceId ? { spaceId } : {}) } },
773
+ policy,
580
774
  { spaceId },
581
775
  context.state.user
582
776
  )
777
+ track({ siteId: site.siteId, siteIndex, step: 'start', done: (declared ?? 0) + 1, total: stepTotal })
583
778
  if (start !== false) {
584
779
  // live=true + 어댑터가 openLiveFeed 지원 → mirror(실 이벤트 미러). 아니면 sim 기동(fallback).
585
- const wentLive = live ? await startReferenceLiveFeed(domainId, ref.adapterType, cfg, site, res.instanceId) : false
780
+ /* 읽기 커서를 저장할 자리를 함께 넘긴다 레퍼런스의 이름은 호스트가 붙이는 것이고
781
+ 커넥터는 그것을 알 수 없다(§`startReferenceLiveFeed`). */
782
+ const wentLive = live
783
+ ? await startReferenceLiveFeed(domainId, ref.adapterType, cfg, site, res.instanceId, ref.source)
784
+ : false
586
785
  if (!wentLive) await TwinEngine.startFromRegistry(domainId, res.instanceId)
587
786
  instances.push({ siteId: site.siteId, instanceId: res.instanceId, spaceId: res.spaceId, mode: wentLive ? 'live' : 'sim', warnings: res.warnings })
588
787
  } else {
589
788
  instances.push({ siteId: site.siteId, instanceId: res.instanceId, spaceId: res.spaceId, mode: 'provisioned', warnings: res.warnings })
590
789
  }
591
790
  } catch (e: any) {
791
+ /*
792
+ * **실패한 단계를 그대로 알린다.** 화면이 진행 표시를 지우지 않고 그 자리를 붉게 두어야
793
+ * 사람이 「어디까지 갔는지」를 안다. 막힌 자리는 「끝낸 수」의 다음 단계다(위 주석).
794
+ */
795
+ const at = (adapter.masterSteps?.[lastDone] ?? lastStep) as ReferenceProgress['step']
796
+ emit({ siteId: site.siteId, siteIndex, step: at, done: lastDone, total: stepTotal, failed: true, error: e?.message ?? String(e) })
797
+
592
798
  failed.push({ siteId: site.siteId, error: e?.message ?? 'failed' })
593
799
  }
594
800
  }
@@ -600,6 +806,10 @@ export class TwinReferenceResolver {
600
806
  {
601
807
  status: 'connected',
602
808
  lastSyncedAt: new Date().toISOString(),
809
+ /* 못 옮긴 값을 **세어 남긴다**. 아무 인스턴스도 보고하지 않았으면 `null` — 「없었다」로 위장하지 않는다. */
810
+ lastWarningCount: instances.some((i: any) => Array.isArray(i.warnings))
811
+ ? instances.reduce((n: number, i: any) => n + (Array.isArray(i.warnings) ? i.warnings.length : 0), 0)
812
+ : null,
603
813
  /*
604
814
  * **영어 canonical 로 남긴다.** 이 값은 DB 에 저장되고 연결 화면이 그대로 뿌린다 — 예전에는
605
815
  * 한글 산문(`N개 사이트 실패`)이라 다섯 언어 화면에 한글이 새어 나갔다. 어댑터가 주는 사유도
@@ -607,7 +817,24 @@ export class TwinReferenceResolver {
607
817
  * 어느 사이트가 왜 실패했는지는 `failed[]` 가 답한다 — 화면이 그것을 보여 준다.
608
818
  */
609
819
  lastError: failed.length ? `${failed.length} of ${sites.length} site(s) failed: ${failed.map((f: any) => f.siteId).join(', ')}` : null,
610
- scopeSpec: { ...((ref.scopeSpec as any) ?? {}), produced: [...prev, ...producedNew] }
820
+ /*
821
+ * ── 실패 사유를 **남긴다** (2026-08-21) ────────────────────────────────
822
+ *
823
+ * 사유는 대화상자에만 있었다. 닫으면 사라지고, 다시 보려면 같은 실패를 다시 만들어야 했다 —
824
+ * 실제로 「승화푸드」 연결이 그 상태였다(요약 한 줄만 남고 원인은 어디에도 없었다).
825
+ *
826
+ * `scopeSpec` 은 이미 이 레퍼런스의 부속 사실을 담는 자리이므로(`produced`) 스키마를 늘리지 않고
827
+ * 여기 적는다. 성공한 회차에는 **비운다** — 지나간 실패를 현재 상태처럼 보이게 두지 않는다.
828
+ */
829
+ scopeSpec: {
830
+ ...((ref.scopeSpec as any) ?? {}),
831
+ produced: [...prev, ...producedNew],
832
+ /* 이번에 읽은 사이트의 총평만 갈아 끼운다 — 읽지 않은 사이트의 것은 그대로 둔다. */
833
+ assessments: { ...(((ref.scopeSpec as any) ?? {}).assessments ?? {}), ...assessments },
834
+ ...(failed.length
835
+ ? { lastFailures: failed.map((f: any) => ({ siteId: f.siteId, error: f.error, at: new Date().toISOString() })) }
836
+ : { lastFailures: [] })
837
+ }
611
838
  } as any
612
839
  )
613
840
  return { ok: instances.length > 0, instances, failed }
@@ -1,5 +1,5 @@
1
1
  import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
2
- import { ObjectType, Field, ID } from 'type-graphql'
2
+ import { ObjectType, Field, ID, Int } from 'type-graphql'
3
3
 
4
4
  import { Domain, ScalarObject } from '@things-factory/shell'
5
5
 
@@ -70,6 +70,44 @@ export class TwinReference {
70
70
  @Field({ nullable: true, description: 'Last connection/sync error message, if any.' })
71
71
  lastError?: string
72
72
 
73
+ /**
74
+ * 마지막 동기에서 **못 옮긴 값의 수**. `null` = 세지 않았다(0 과 다르다).
75
+ *
76
+ * 세지 않은 구조 인제스트는 도크를 랙으로 바꿔 놓고도 조용히 성공한다. 그 사실을 사후에 물을 수
77
+ * 있어야 단계 판정이 「구조는 됐다」를 말할 수 있다 — 없는 동안 판정은 늘 `unmeasured` 였다.
78
+ * 기본값을 두지 않는다: 0 을 기본으로 깔면 「경고 없었다」와 「세지 않았다」가 같아진다.
79
+ */
80
+ @Column({ type: 'int', nullable: true })
81
+ @Field(type => Int, { nullable: true, description: 'Number of values the last sync could not carry over (mapping fallbacks). Null means they were not counted at all, which is different from zero.' })
82
+ lastWarningCount?: number
83
+
84
+ /**
85
+ * **어디까지 읽었나** — 라이브 피드의 읽기 커서(§`LiveFeedContinuity`).
86
+ *
87
+ * ── 왜 저장하나 (2026-08-23 실측) ──────────────────────────────────────────
88
+ * 어댑터가 붙을 때마다 `Date.now() − 되돌아볼 날수` 로 창을 새로 만들고 있었다. 그래서 **재기동마다
89
+ * 미러의 과거가 잘렸다** — 작업 2,855 → 2,820(8시간 흐른 만큼). 미러가 아는 것이 「원본의 사실」이
90
+ * 아니라 「창의 함수」였고, 그 사실을 아무도 말하지 않았다.
91
+ *
92
+ * 커널이 그 갈림을 이미 적어 두었다(§`hydrateContinuity`): 「원천이 애초에 다시 말해 주지 않는 축」은
93
+ * 재기동 연속성으로 이어받는다. 「우리가 어디까지 읽었나」가 정확히 그 성질이므로 **어댑터의 사물함이
94
+ * 아니라 이 층**에 있다 — 원본이 늘 때마다 저장 기제가 늘고 그중 하나가 조용히 다르게 동작하지 않게.
95
+ *
96
+ * ── 왜 캐시가 아니라 표인가 ────────────────────────────────────────────────
97
+ * 만료되면 창이 다시 미끄러지고, 그 손실은 오류 없이 조용하다. 스냅샷 체크포인트와 성질이 다르다
98
+ * (그쪽은 잃어도 원천이 정정해 준다 — 이 값은 **잃으면 원천에 묻지 않게 된다**).
99
+ *
100
+ * 모양은 `{ streams: { [흐름]: { since?, seen[] } }, firstAttachedAt? }` 다. **흐름 이름은 어댑터가
101
+ * 정한다** — 원본마다 흐름 수와 뜻이 다르므로 이 층은 열쇠로만 다룬다. `simple-json` 이라 드라이버
102
+ * 다섯을 그대로 지난다.
103
+ *
104
+ * `firstAttachedAt` 은 「언제부터 아는가」다. 커널은 상한만 안다(`nowTime` = 마지막으로 들은 시각).
105
+ * 둘이 함께 「이 트윈이 아는 구간」이고, 화면이 수를 보일 때 그 구간을 말해야 한다.
106
+ */
107
+ @Column({ type: 'simple-json', nullable: true })
108
+ @Field(type => ScalarObject, { nullable: true, description: 'Live feed read cursor carried across restarts, keyed by adapter-defined stream. Null means the feed has never attached, and the next attach decides its first window.' })
109
+ liveCursor?: any
110
+
73
111
  @CreateDateColumn()
74
112
  @Field({ nullable: true, description: 'Timestamp when the reference was first created.' })
75
113
  createdAt?: Date
@@ -17,6 +17,19 @@ export type CommandAuditInput = {
17
17
  command: any
18
18
  /** 커널 판정. */
19
19
  ack: { accepted?: boolean; errorCode?: string; error?: string }
20
+ /**
21
+ * **원본(어댑터)이 준 답** — 허락과 일어남 사이에 끼어 있는 것을 같은 기록에 닫는다 (ADR-0029 §1 결정 C).
22
+ *
23
+ * ── 왜 이 칸이 필요한가 ──────────────────────────────────────────────────
24
+ * 명령이 원본으로 가면 감사는 「허락했다」까지만 증명한다 — 어댑터가 조용히 실패하면 원장에는 승인만
25
+ * 남는다. 그래서 어댑터의 답을 **같은 행**에 되적는다.
26
+ *
27
+ * 셋을 구별한다:
28
+ * · 없음(`undefined`) — **원본에 묻지 않았다**(커널이 받은 명령이다). 「모른다」와 다르다.
29
+ * · `{ answered: false }` — 물었는데 **답을 못 받았다** → `unknown` 으로 남긴다. 성공으로 적지 않는다.
30
+ * · `{ answered: true, ok, code? }` — 원본이 답했다(적용·거절 모두 사실이다).
31
+ */
32
+ source?: { answered: boolean; ok?: boolean; code?: string }
20
33
  /** 세션이 아는 사용자 — **서버만 채운다.** */
21
34
  actor?: { id?: string; email?: string; name?: string }
22
35
  /** 서버 시각. 호출부가 주입해 테스트가 시간을 고정할 수 있게 한다. */
@@ -35,6 +48,13 @@ export type CommandAuditRow = {
35
48
  actorName?: string
36
49
  accepted: boolean
37
50
  errorCode?: string
51
+ /**
52
+ * 원본이 그래서 어떻게 했나 — `applied` · `refused` · `unknown`(물었지만 답이 없다).
53
+ * **없으면 묻지 않은 것이다**(커널이 받은 명령). 0 이나 성공으로 채우지 않는다.
54
+ */
55
+ sourceOutcome?: 'applied' | 'refused' | 'unknown'
56
+ /** 원본이 준 사유 코드 — 있으면 그대로. 우리가 지어내지 않는다. */
57
+ sourceCode?: string
38
58
  args?: any
39
59
  argsTruncated: boolean
40
60
  }
@@ -48,7 +68,7 @@ export function normalizeOrigin(v: unknown): CommandOrigin {
48
68
  const ARGS_MAX = 4000
49
69
 
50
70
  export function buildCommandAudit(input: CommandAuditInput): CommandAuditRow {
51
- const { command, ack, actor, at } = input
71
+ const { command, ack, actor, at, source } = input
52
72
  const argsText = safeJson(command?.args)
53
73
  const truncated = argsText !== null && argsText.length > ARGS_MAX
54
74
  return {
@@ -57,6 +77,8 @@ export function buildCommandAudit(input: CommandAuditInput): CommandAuditRow {
57
77
  commandType: str(command?.type),
58
78
  origin: normalizeOrigin(command?.origin),
59
79
  reason: str(command?.reason),
80
+ /* 원본의 답 — 묻지 않았으면 칸을 만들지 않는다(모름과 구별한다). */
81
+ ...(source ? { sourceOutcome: sourceOutcomeOf(source), ...(source.code ? { sourceCode: source.code } : {}) } : {}),
60
82
  /* 신원 — 세션 값만. 커맨드에 실려 온 신원 주장은 쳐다보지 않는다. */
61
83
  actorId: str(actor?.id),
62
84
  actorEmail: str(actor?.email),
@@ -79,3 +101,14 @@ function safeJson(v: unknown): string | null {
79
101
  return null /* 순환 참조 등 — 못 재면 자르지 않는다(원본을 그대로 둔다) */
80
102
  }
81
103
  }
104
+
105
+ /**
106
+ * 원본의 답을 세 값으로 — **답이 없으면 `unknown`**(성공으로 적지 않는다).
107
+ *
108
+ * 이 함수가 있는 이유: 「물었지만 답이 없다」를 실패로도 성공으로도 적을 수 없다. 실패로 적으면 일어난 일을
109
+ * 없던 일로 만들고, 성공으로 적으면 없던 일을 일어난 일로 만든다. 셋째 값이 사실이다.
110
+ */
111
+ export function sourceOutcomeOf(source: { answered: boolean; ok?: boolean }): 'applied' | 'refused' | 'unknown' {
112
+ if (!source.answered || typeof source.ok !== 'boolean') return 'unknown'
113
+ return source.ok ? 'applied' : 'refused'
114
+ }
@@ -99,6 +99,26 @@ export class TwinAuditEvent {
99
99
  reason?: string
100
100
 
101
101
  /* ── 신원: 세션에서만 온다 ── */
102
+ /*
103
+ * **원본이 그래서 어떻게 했나** — 허락과 일어남 사이에 끼어 있는 것을 같은 행에 닫는다
104
+ * (ADR-0029 §1 결정 C, 2026-08-19).
105
+ *
106
+ * 명령이 원본으로 가면 감사는 「허락했다」까지만 증명한다 — 어댑터가 조용히 실패하면 원장에는 승인만
107
+ * 남는다. 그래서 어댑터의 답을 되적는다.
108
+ *
109
+ * · 비어 있음 — **원본에 묻지 않았다**(커널이 받은 명령이다). 「모른다」와 다르다.
110
+ * · `unknown` — 물었는데 **답을 못 받았다**. 성공으로 적지 않는다.
111
+ * · `applied` · `refused` — 원본이 답했다(둘 다 사실이다).
112
+ */
113
+ @Column({ nullable: true })
114
+ @Field({ nullable: true, description: "What the source did with it: 'applied' | 'refused' | 'unknown' (asked but no answer). Empty means the source was not asked — the kernel took the command." })
115
+ sourceOutcome?: string
116
+
117
+ /** 원본이 준 사유 코드 — 있으면 그대로 적는다(우리가 지어내지 않는다). */
118
+ @Column({ nullable: true })
119
+ @Field({ nullable: true, description: 'Reason code the source returned, as it gave it.' })
120
+ sourceCode?: string
121
+
102
122
  @Column({ nullable: true })
103
123
  @Field({ nullable: true, description: 'Approving user id, stamped by the server from the session.' })
104
124
  actorId?: string