@things-factory/headless-twin 10.0.15 → 10.0.18

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 (513) hide show
  1. package/dist-server/engine/attention-digest.d.ts +4 -4
  2. package/dist-server/engine/attention-digest.js +7 -7
  3. package/dist-server/engine/attention-digest.js.map +1 -1
  4. package/dist-server/engine/canonical-ingest.d.ts +18 -82
  5. package/dist-server/engine/canonical-ingest.js +183 -18
  6. package/dist-server/engine/canonical-ingest.js.map +1 -1
  7. package/dist-server/engine/command-routing.js +1 -1
  8. package/dist-server/engine/command-routing.js.map +1 -1
  9. package/dist-server/engine/declared-stimulus.js +1 -1
  10. package/dist-server/engine/declared-stimulus.js.map +1 -1
  11. package/dist-server/engine/energy-topology.js +4 -4
  12. package/dist-server/engine/energy-topology.js.map +1 -1
  13. package/dist-server/engine/ingest-dedupe.d.ts +38 -0
  14. package/dist-server/engine/ingest-dedupe.js +145 -0
  15. package/dist-server/engine/ingest-dedupe.js.map +1 -0
  16. package/dist-server/engine/ingest-health.d.ts +90 -10
  17. package/dist-server/engine/ingest-health.js +125 -16
  18. package/dist-server/engine/ingest-health.js.map +1 -1
  19. package/dist-server/engine/integration-probes.d.ts +3 -3
  20. package/dist-server/engine/integration-probes.js +6 -6
  21. package/dist-server/engine/integration-probes.js.map +1 -1
  22. package/dist-server/engine/kpi-baseline.js +4 -4
  23. package/dist-server/engine/kpi-baseline.js.map +1 -1
  24. package/dist-server/engine/kpi-fold.d.ts +212 -14
  25. package/dist-server/engine/kpi-fold.js +374 -21
  26. package/dist-server/engine/kpi-fold.js.map +1 -1
  27. package/dist-server/engine/kpi-query.d.ts +11 -11
  28. package/dist-server/engine/kpi-query.js +281 -36
  29. package/dist-server/engine/kpi-query.js.map +1 -1
  30. package/dist-server/engine/live-feed-registry.d.ts +1 -1
  31. package/dist-server/engine/live-feed-registry.js +1 -1
  32. package/dist-server/engine/live-feed-registry.js.map +1 -1
  33. package/dist-server/engine/load-meter.d.ts +1 -1
  34. package/dist-server/engine/load-meter.js.map +1 -1
  35. package/dist-server/engine/local-declarations.d.ts +7 -7
  36. package/dist-server/engine/local-declarations.js +15 -14
  37. package/dist-server/engine/local-declarations.js.map +1 -1
  38. package/dist-server/engine/log.d.ts +1 -1
  39. package/dist-server/engine/log.js +1 -1
  40. package/dist-server/engine/log.js.map +1 -1
  41. package/dist-server/engine/measured-estimator.js +1 -1
  42. package/dist-server/engine/measured-estimator.js.map +1 -1
  43. package/dist-server/engine/measured-yield.js +1 -1
  44. package/dist-server/engine/measured-yield.js.map +1 -1
  45. package/dist-server/engine/model-basis.d.ts +1 -1
  46. package/dist-server/engine/model-basis.js +1 -1
  47. package/dist-server/engine/model-basis.js.map +1 -1
  48. package/dist-server/engine/model-gap.d.ts +3 -3
  49. package/dist-server/engine/model-gap.js +1 -1
  50. package/dist-server/engine/model-gap.js.map +1 -1
  51. package/dist-server/engine/model-vocabulary.js +3 -3
  52. package/dist-server/engine/model-vocabulary.js.map +1 -1
  53. package/dist-server/engine/oee-accumulator.d.ts +6 -5
  54. package/dist-server/engine/oee-accumulator.js +6 -5
  55. package/dist-server/engine/oee-accumulator.js.map +1 -1
  56. package/dist-server/engine/property-effects.js +2 -2
  57. package/dist-server/engine/property-effects.js.map +1 -1
  58. package/dist-server/engine/read-time.d.ts +22 -0
  59. package/dist-server/engine/read-time.js +81 -0
  60. package/dist-server/engine/read-time.js.map +1 -0
  61. package/dist-server/engine/restart-policy.js +1 -1
  62. package/dist-server/engine/restart-policy.js.map +1 -1
  63. package/dist-server/engine/runtime-key.js +1 -1
  64. package/dist-server/engine/runtime-key.js.map +1 -1
  65. package/dist-server/engine/spec-coverage.d.ts +2 -2
  66. package/dist-server/engine/spec-coverage.js +1 -1
  67. package/dist-server/engine/spec-coverage.js.map +1 -1
  68. package/dist-server/engine/stage-path.d.ts +4 -4
  69. package/dist-server/engine/stage-path.js +1 -1
  70. package/dist-server/engine/stage-path.js.map +1 -1
  71. package/dist-server/engine/state-axes.d.ts +1 -1
  72. package/dist-server/engine/state-axes.js +2 -2
  73. package/dist-server/engine/state-axes.js.map +1 -1
  74. package/dist-server/engine/travel-estimator.d.ts +2 -2
  75. package/dist-server/engine/travel-estimator.js +1 -1
  76. package/dist-server/engine/travel-estimator.js.map +1 -1
  77. package/dist-server/engine/twin-engine.d.ts +307 -54
  78. package/dist-server/engine/twin-engine.js +822 -196
  79. package/dist-server/engine/twin-engine.js.map +1 -1
  80. package/dist-server/engine/warm-start.d.ts +2 -2
  81. package/dist-server/engine/warm-start.js +5 -5
  82. package/dist-server/engine/warm-start.js.map +1 -1
  83. package/dist-server/index.js +1 -1
  84. package/dist-server/index.js.map +1 -1
  85. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +1 -1
  86. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -1
  87. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +3 -3
  88. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -1
  89. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +2 -2
  90. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -1
  91. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.d.ts +5 -0
  92. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js +54 -0
  93. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js.map +1 -0
  94. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.d.ts +5 -0
  95. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js +44 -0
  96. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js.map +1 -0
  97. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.d.ts +6 -0
  98. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js +84 -0
  99. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js.map +1 -0
  100. package/dist-server/migrations/index.js +7 -1
  101. package/dist-server/migrations/index.js.map +1 -1
  102. package/dist-server/routes.d.ts +1 -0
  103. package/dist-server/routes.js +69 -5
  104. package/dist-server/routes.js.map +1 -1
  105. package/dist-server/service/index.d.ts +2 -2
  106. package/dist-server/service/index.js +30 -24
  107. package/dist-server/service/index.js.map +1 -1
  108. package/dist-server/service/reference/control-routing.d.ts +1 -1
  109. package/dist-server/service/reference/control-routing.js +1 -1
  110. package/dist-server/service/reference/control-routing.js.map +1 -1
  111. package/dist-server/service/reference/discovery-result.d.ts +2 -2
  112. package/dist-server/service/reference/discovery-result.js +3 -3
  113. package/dist-server/service/reference/discovery-result.js.map +1 -1
  114. package/dist-server/service/reference/hook-contract.d.ts +43 -0
  115. package/dist-server/service/reference/hook-contract.js +59 -0
  116. package/dist-server/service/reference/hook-contract.js.map +1 -0
  117. package/dist-server/service/reference/ingest-space.d.ts +1 -1
  118. package/dist-server/service/reference/ingest-space.js +4 -4
  119. package/dist-server/service/reference/ingest-space.js.map +1 -1
  120. package/dist-server/service/reference/knob-defaults.d.ts +1 -1
  121. package/dist-server/service/reference/knob-defaults.js +4 -4
  122. package/dist-server/service/reference/knob-defaults.js.map +1 -1
  123. package/dist-server/service/reference/reference-adapter.d.ts +116 -9
  124. package/dist-server/service/reference/reference-adapter.js +27 -0
  125. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  126. package/dist-server/service/reference/reference-hook.d.ts +20 -0
  127. package/dist-server/service/reference/reference-hook.js +133 -0
  128. package/dist-server/service/reference/reference-hook.js.map +1 -0
  129. package/dist-server/service/reference/reference-live.d.ts +2 -2
  130. package/dist-server/service/reference/reference-live.js +76 -13
  131. package/dist-server/service/reference/reference-live.js.map +1 -1
  132. package/dist-server/service/reference/reference-master.d.ts +25 -16
  133. package/dist-server/service/reference/reference-master.js +35 -13
  134. package/dist-server/service/reference/reference-master.js.map +1 -1
  135. package/dist-server/service/reference/reference-resolver.d.ts +1 -1
  136. package/dist-server/service/reference/reference-resolver.js +12 -12
  137. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  138. package/dist-server/service/reference/template-registry.d.ts +1 -1
  139. package/dist-server/service/reference/template-registry.js.map +1 -1
  140. package/dist-server/service/reference/twin-reference.d.ts +3 -3
  141. package/dist-server/service/reference/twin-reference.js.map +1 -1
  142. package/dist-server/service/twin-attention/twin-attention-query.d.ts +1 -1
  143. package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
  144. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  145. package/dist-server/service/twin-audit/command-audit.d.ts +1 -1
  146. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  147. package/dist-server/service/twin-audit/twin-audit-event.d.ts +1 -1
  148. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  149. package/dist-server/service/twin-audit/twin-audit-query.js +1 -1
  150. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -1
  151. package/dist-server/service/twin-backfill/backfill-period-facts.d.ts +24 -0
  152. package/dist-server/service/twin-backfill/backfill-period-facts.js +96 -0
  153. package/dist-server/service/twin-backfill/backfill-period-facts.js.map +1 -0
  154. package/dist-server/service/twin-backfill/backfill-shape.d.ts +34 -0
  155. package/dist-server/service/twin-backfill/backfill-shape.js +75 -0
  156. package/dist-server/service/twin-backfill/backfill-shape.js.map +1 -0
  157. package/dist-server/service/twin-backfill/index.d.ts +2 -0
  158. package/dist-server/service/twin-backfill/index.js +6 -0
  159. package/dist-server/service/twin-backfill/index.js.map +1 -0
  160. package/dist-server/service/twin-backfill/twin-backfill-resolver.d.ts +10 -0
  161. package/dist-server/service/twin-backfill/twin-backfill-resolver.js +44 -0
  162. package/dist-server/service/twin-backfill/twin-backfill-resolver.js.map +1 -0
  163. package/dist-server/service/twin-control/twin-control-mutation.js +3 -3
  164. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  165. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  166. package/dist-server/service/twin-event/twin-event-keys.js +2 -2
  167. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  168. package/dist-server/service/twin-event/twin-event-type.d.ts +1 -0
  169. package/dist-server/service/twin-event/twin-event-type.js +8 -1
  170. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  171. package/dist-server/service/twin-event/twin-event.d.ts +2 -2
  172. package/dist-server/service/twin-event/twin-event.js +32 -4
  173. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  174. package/dist-server/service/twin-forecast/forecast-metrics.js +1 -1
  175. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -1
  176. package/dist-server/service/twin-forecast/gap-analytics.d.ts +1 -1
  177. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  178. package/dist-server/service/twin-forecast/twin-forecast-query.js +29 -11
  179. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  180. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +1 -1
  181. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +1 -1
  182. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -1
  183. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +1 -1
  184. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -1
  185. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  186. package/dist-server/service/twin-journal/event-type-split-shape.d.ts +50 -0
  187. package/dist-server/service/twin-journal/event-type-split-shape.js +111 -0
  188. package/dist-server/service/twin-journal/event-type-split-shape.js.map +1 -0
  189. package/dist-server/service/twin-journal/event-type-split.d.ts +15 -0
  190. package/dist-server/service/twin-journal/event-type-split.js +102 -0
  191. package/dist-server/service/twin-journal/event-type-split.js.map +1 -0
  192. package/dist-server/service/twin-journal/journal-order.d.ts +15 -0
  193. package/dist-server/service/twin-journal/journal-order.js +17 -0
  194. package/dist-server/service/twin-journal/journal-order.js.map +1 -0
  195. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -5
  196. package/dist-server/service/twin-journal/twin-journal-query.js +232 -21
  197. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  198. package/dist-server/service/twin-lifecycle/domain-catalog.js +2 -1
  199. package/dist-server/service/twin-lifecycle/domain-catalog.js.map +1 -1
  200. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +27 -1
  201. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +59 -3
  202. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  203. package/dist-server/service/twin-metrics/twin-metrics-query.js +14 -1
  204. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  205. package/dist-server/service/twin-model/axis-journal-evidence.js +1 -1
  206. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -1
  207. package/dist-server/service/twin-model/epcis-coverage.js +34 -3
  208. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  209. package/dist-server/service/twin-model/iec61850-coverage.js +1 -1
  210. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  211. package/dist-server/service/twin-model/isa95-coverage.d.ts +12 -9
  212. package/dist-server/service/twin-model/isa95-coverage.js +33 -9
  213. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  214. package/dist-server/service/twin-model/item-ref.js +2 -2
  215. package/dist-server/service/twin-model/item-ref.js.map +1 -1
  216. package/dist-server/service/twin-model/name-index.d.ts +1 -1
  217. package/dist-server/service/twin-model/name-index.js +2 -2
  218. package/dist-server/service/twin-model/name-index.js.map +1 -1
  219. package/dist-server/service/twin-model/project-structure.d.ts +1 -1
  220. package/dist-server/service/twin-model/project-structure.js +7 -6
  221. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  222. package/dist-server/service/twin-model/twin-equipment.d.ts +2 -2
  223. package/dist-server/service/twin-model/twin-equipment.js +3 -3
  224. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  225. package/dist-server/service/twin-model/twin-lineage-query.js +16 -5
  226. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  227. package/dist-server/service/twin-model/twin-location.d.ts +1 -1
  228. package/dist-server/service/twin-model/twin-location.js.map +1 -1
  229. package/dist-server/service/twin-model/twin-model-item-query.d.ts +3 -3
  230. package/dist-server/service/twin-model/twin-model-item-query.js +15 -15
  231. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  232. package/dist-server/service/twin-model/twin-model-mutation.d.ts +2 -2
  233. package/dist-server/service/twin-model/twin-model-mutation.js +4 -4
  234. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -1
  235. package/dist-server/service/twin-model/twin-model-query.js +51 -30
  236. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  237. package/dist-server/service/twin-model/twin-model-tree-query.js +23 -1
  238. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  239. package/dist-server/service/twin-model/twin-operation.d.ts +1 -1
  240. package/dist-server/service/twin-model/twin-operation.js +1 -1
  241. package/dist-server/service/twin-model/twin-operation.js.map +1 -1
  242. package/dist-server/service/twin-space/move-space.js +2 -2
  243. package/dist-server/service/twin-space/move-space.js.map +1 -1
  244. package/dist-server/service/twin-space/representation-role.d.ts +2 -0
  245. package/dist-server/service/twin-space/representation-role.js +18 -0
  246. package/dist-server/service/twin-space/representation-role.js.map +1 -0
  247. package/dist-server/service/twin-space/space-integrity.d.ts +1 -1
  248. package/dist-server/service/twin-space/space-integrity.js.map +1 -1
  249. package/dist-server/service/twin-space/twin-space-representation.d.ts +2 -0
  250. package/dist-server/service/twin-space/twin-space-representation.js +16 -0
  251. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  252. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  253. package/dist-server/service/twin-space/twin-space-resolver.js +37 -8
  254. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  255. package/dist-server/service/twin-structure/twin-structure.js +3 -3
  256. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  257. package/dist-server/service/twin-subject/event-subjects.d.ts +43 -0
  258. package/dist-server/service/twin-subject/event-subjects.js +137 -0
  259. package/dist-server/service/twin-subject/event-subjects.js.map +1 -0
  260. package/dist-server/service/twin-subject/index.d.ts +9 -0
  261. package/dist-server/service/twin-subject/index.js +16 -0
  262. package/dist-server/service/twin-subject/index.js.map +1 -0
  263. package/dist-server/service/twin-subject/item-resolver.d.ts +10 -0
  264. package/dist-server/service/twin-subject/item-resolver.js +93 -0
  265. package/dist-server/service/twin-subject/item-resolver.js.map +1 -0
  266. package/dist-server/service/twin-subject/subject-query-shape.d.ts +22 -0
  267. package/dist-server/service/twin-subject/subject-query-shape.js +88 -0
  268. package/dist-server/service/twin-subject/subject-query-shape.js.map +1 -0
  269. package/dist-server/service/twin-subject/twin-subject-backfill.d.ts +23 -0
  270. package/dist-server/service/twin-subject/twin-subject-backfill.js +159 -0
  271. package/dist-server/service/twin-subject/twin-subject-backfill.js.map +1 -0
  272. package/dist-server/service/twin-subject/twin-subject-event.d.ts +14 -0
  273. package/dist-server/service/twin-subject/twin-subject-event.js +111 -0
  274. package/dist-server/service/twin-subject/twin-subject-event.js.map +1 -0
  275. package/dist-server/service/twin-subject/twin-subject-fast-path.d.ts +39 -0
  276. package/dist-server/service/twin-subject/twin-subject-fast-path.js +295 -0
  277. package/dist-server/service/twin-subject/twin-subject-fast-path.js.map +1 -0
  278. package/dist-server/service/twin-subject/twin-subject-query.d.ts +27 -0
  279. package/dist-server/service/twin-subject/twin-subject-query.js +232 -0
  280. package/dist-server/service/twin-subject/twin-subject-query.js.map +1 -0
  281. package/dist-server/service/twin-subject/twin-subject-writer.d.ts +27 -0
  282. package/dist-server/service/twin-subject/twin-subject-writer.js +166 -0
  283. package/dist-server/service/twin-subject/twin-subject-writer.js.map +1 -0
  284. package/dist-server/service/twin-target/twin-target-resolver.js +1 -1
  285. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  286. package/dist-shared/entity-delta.d.ts +1 -1
  287. package/dist-shared/entity-delta.js +4 -4
  288. package/dist-shared/entity-delta.js.map +1 -1
  289. package/dist-shared/touched-items.js +2 -2
  290. package/dist-shared/touched-items.js.map +1 -1
  291. package/dist-shared/twin-level.js +1 -1
  292. package/dist-shared/twin-level.js.map +1 -1
  293. package/package.json +7 -6
  294. package/server/engine/attention-digest.ts +9 -9
  295. package/server/engine/canonical-ingest.ts +202 -87
  296. package/server/engine/command-routing.ts +1 -1
  297. package/server/engine/declared-stimulus.ts +1 -1
  298. package/server/engine/energy-topology.ts +2 -2
  299. package/server/engine/ingest-dedupe.ts +149 -0
  300. package/server/engine/ingest-health.ts +171 -22
  301. package/server/engine/integration-probes.ts +7 -7
  302. package/server/engine/kpi-baseline.ts +4 -4
  303. package/server/engine/kpi-fold.ts +592 -41
  304. package/server/engine/kpi-query.ts +307 -48
  305. package/server/engine/live-feed-registry.ts +1 -1
  306. package/server/engine/load-meter.ts +1 -1
  307. package/server/engine/local-declarations.ts +15 -14
  308. package/server/engine/log.ts +1 -1
  309. package/server/engine/measured-estimator.ts +1 -1
  310. package/server/engine/measured-yield.ts +1 -1
  311. package/server/engine/model-basis.ts +2 -2
  312. package/server/engine/model-gap.ts +3 -3
  313. package/server/engine/model-vocabulary.ts +2 -2
  314. package/server/engine/oee-accumulator.ts +8 -6
  315. package/server/engine/property-effects.ts +2 -2
  316. package/server/engine/read-time.ts +77 -0
  317. package/server/engine/restart-policy.ts +1 -1
  318. package/server/engine/runtime-key.ts +1 -1
  319. package/server/engine/spec-coverage.ts +3 -3
  320. package/server/engine/stage-path.ts +5 -5
  321. package/server/engine/state-axes.ts +2 -2
  322. package/server/engine/travel-estimator.ts +2 -2
  323. package/server/engine/twin-engine.ts +906 -205
  324. package/server/engine/warm-start.ts +6 -6
  325. package/server/index.ts +1 -1
  326. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +1 -1
  327. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +3 -3
  328. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +2 -2
  329. package/server/migrations/1786300000000-IndexEventTypeByTime.ts +48 -0
  330. package/server/migrations/1786400000000-IndexEventCreatedAt.ts +38 -0
  331. package/server/migrations/1786500000000-CreateTwinSubjectEvents.ts +93 -0
  332. package/server/migrations/index.ts +7 -1
  333. package/server/routes.ts +73 -5
  334. package/server/service/index.ts +7 -1
  335. package/server/service/reference/control-routing.ts +1 -1
  336. package/server/service/reference/discovery-result.ts +3 -3
  337. package/server/service/reference/hook-contract.ts +103 -0
  338. package/server/service/reference/ingest-space.ts +4 -4
  339. package/server/service/reference/knob-defaults.ts +4 -4
  340. package/server/service/reference/reference-adapter.ts +145 -10
  341. package/server/service/reference/reference-hook.ts +145 -0
  342. package/server/service/reference/reference-live.ts +87 -15
  343. package/server/service/reference/reference-master.ts +51 -20
  344. package/server/service/reference/reference-resolver.ts +11 -11
  345. package/server/service/reference/template-registry.ts +1 -1
  346. package/server/service/reference/twin-reference.ts +3 -3
  347. package/server/service/twin-attention/twin-attention-query.ts +1 -1
  348. package/server/service/twin-audit/command-audit.ts +1 -1
  349. package/server/service/twin-audit/twin-audit-event.ts +2 -2
  350. package/server/service/twin-audit/twin-audit-query.ts +1 -1
  351. package/server/service/twin-backfill/backfill-period-facts.ts +168 -0
  352. package/server/service/twin-backfill/backfill-shape.ts +86 -0
  353. package/server/service/twin-backfill/index.ts +3 -0
  354. package/server/service/twin-backfill/twin-backfill-resolver.ts +35 -0
  355. package/server/service/twin-control/twin-control-mutation.ts +2 -2
  356. package/server/service/twin-event/twin-event-keys.ts +2 -2
  357. package/server/service/twin-event/twin-event-type.ts +18 -1
  358. package/server/service/twin-event/twin-event.ts +32 -6
  359. package/server/service/twin-forecast/forecast-metrics.ts +1 -1
  360. package/server/service/twin-forecast/gap-analytics.ts +1 -1
  361. package/server/service/twin-forecast/twin-forecast-query.ts +30 -11
  362. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +1 -1
  363. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +1 -1
  364. package/server/service/twin-instance/twin-instance.ts +4 -4
  365. package/server/service/twin-journal/event-type-split-shape.ts +112 -0
  366. package/server/service/twin-journal/event-type-split.ts +144 -0
  367. package/server/service/twin-journal/journal-order.ts +20 -0
  368. package/server/service/twin-journal/twin-journal-query.ts +224 -20
  369. package/server/service/twin-lifecycle/domain-catalog.ts +2 -1
  370. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +58 -4
  371. package/server/service/twin-metrics/twin-metrics-query.ts +14 -1
  372. package/server/service/twin-model/axis-journal-evidence.ts +1 -1
  373. package/server/service/twin-model/epcis-coverage.ts +34 -3
  374. package/server/service/twin-model/iec61850-coverage.ts +1 -1
  375. package/server/service/twin-model/isa95-coverage.ts +43 -16
  376. package/server/service/twin-model/item-ref.ts +2 -2
  377. package/server/service/twin-model/name-index.ts +2 -2
  378. package/server/service/twin-model/project-structure.ts +7 -6
  379. package/server/service/twin-model/twin-equipment.ts +16 -4
  380. package/server/service/twin-model/twin-lineage-query.ts +16 -5
  381. package/server/service/twin-model/twin-location.ts +1 -1
  382. package/server/service/twin-model/twin-model-item-query.ts +15 -15
  383. package/server/service/twin-model/twin-model-mutation.ts +4 -4
  384. package/server/service/twin-model/twin-model-query.ts +49 -28
  385. package/server/service/twin-model/twin-model-tree-query.ts +26 -2
  386. package/server/service/twin-model/twin-operation.ts +2 -2
  387. package/server/service/twin-space/move-space.ts +3 -3
  388. package/server/service/twin-space/representation-role.ts +15 -0
  389. package/server/service/twin-space/space-integrity.ts +1 -1
  390. package/server/service/twin-space/twin-space-representation.ts +37 -0
  391. package/server/service/twin-space/twin-space-resolver.ts +40 -8
  392. package/server/service/twin-structure/twin-structure.ts +3 -3
  393. package/server/service/twin-subject/event-subjects.ts +155 -0
  394. package/server/service/twin-subject/index.ts +10 -0
  395. package/server/service/twin-subject/item-resolver.ts +92 -0
  396. package/server/service/twin-subject/subject-query-shape.ts +84 -0
  397. package/server/service/twin-subject/twin-subject-backfill.ts +196 -0
  398. package/server/service/twin-subject/twin-subject-event.ts +101 -0
  399. package/server/service/twin-subject/twin-subject-fast-path.ts +346 -0
  400. package/server/service/twin-subject/twin-subject-query.ts +210 -0
  401. package/server/service/twin-subject/twin-subject-writer.ts +189 -0
  402. package/server/service/twin-target/twin-target-resolver.ts +1 -1
  403. package/shared/entity-delta.ts +4 -4
  404. package/shared/touched-items.ts +2 -2
  405. package/shared/twin-level.ts +1 -1
  406. package/test/adopt-structure-live.test.ts +3 -3
  407. package/test/aggregate-not-findone.test.ts +112 -0
  408. package/test/attention-digest.test.ts +6 -6
  409. package/test/axis-read.test.ts +6 -5
  410. package/test/backfill-shape.test.ts +89 -0
  411. package/test/boot-resume.test.ts +20 -7
  412. package/test/broadcast-cost-baseline.test.ts +2 -1
  413. package/test/broadcast-period.test.ts +2 -2
  414. package/test/broadcast-queue-overflow.test.ts +244 -0
  415. package/test/canonical-ingest-vocabularies.test.ts +259 -8
  416. package/test/canonical-quantity-door.test.ts +2 -2
  417. package/test/capability-mapping.test.ts +1 -1
  418. package/test/checkpoint-refuses-empty.test.ts +2 -2
  419. package/test/connector-capability-declaration.test.ts +88 -0
  420. package/test/contract-layer-guard.test.ts +48 -0
  421. package/test/control-capability.test.ts +1 -1
  422. package/test/cursor-stall-not-read-failure.test.ts +2 -2
  423. package/test/declaration-reaches-model.test.ts +7 -5
  424. package/test/declared-stimulus.test.ts +4 -4
  425. package/test/discovery-result.test.ts +2 -2
  426. package/test/entity-delta.test.ts +4 -4
  427. package/test/equipment-identity-reaches.test.ts +57 -0
  428. package/test/event-subjects.test.ts +147 -0
  429. package/test/event-time-column.test.ts +3 -3
  430. package/test/event-type-split.test.ts +155 -0
  431. package/test/fact-scope-wiring.test.ts +96 -0
  432. package/test/fold-gap-speaks.test.ts +53 -0
  433. package/test/forecast-metrics.test.ts +1 -1
  434. package/test/forecast-tuning.test.ts +1 -1
  435. package/test/generated-not-counted.test.ts +99 -0
  436. package/test/ingest-bench.test.ts +2 -1
  437. package/test/ingest-health-engine.test.ts +2 -2
  438. package/test/ingest-health-wiring.test.ts +7 -5
  439. package/test/ingest-health.test.ts +6 -6
  440. package/test/ingest-history.test.ts +2 -2
  441. package/test/ingest-idempotent.test.ts +132 -0
  442. package/test/ingest-space.test.ts +3 -3
  443. package/test/ingest-wiring-guard.test.ts +197 -0
  444. package/test/integration-probes.test.ts +3 -3
  445. package/test/integration-runner.test.ts +1 -1
  446. package/test/item-ref.test.ts +1 -1
  447. package/test/journal-order-axis.test.ts +80 -0
  448. package/test/journal-read-discipline.test.ts +16 -6
  449. package/test/journal-retention.test.ts +171 -29
  450. package/test/journal-sort-axis.test.ts +1 -1
  451. package/test/journal-write-door.test.ts +3 -3
  452. package/test/journal-write-trend.test.ts +1 -1
  453. package/test/kernel-kind-guard.test.ts +3 -3
  454. package/test/knob-defaults.test.ts +1 -1
  455. package/test/kpi-baseline-db.test.ts +2 -2
  456. package/test/kpi-fold.test.ts +266 -14
  457. package/test/kpi-query-bench.test.ts +3 -3
  458. package/test/live-cursor-wiring.test.ts +6 -6
  459. package/test/live-kernel-facts.test.ts +11 -11
  460. package/test/live-mirror-parity.test.ts +9 -6
  461. package/test/load-meter.test.ts +1 -1
  462. package/test/local-declarations.test.ts +13 -12
  463. package/test/log-stamp.test.ts +1 -1
  464. package/test/master-to-twin.test.ts +2 -2
  465. package/test/measured-yield.test.ts +2 -2
  466. package/test/merge-streams.test.ts +70 -0
  467. package/test/mirror-resumes-from-checkpoint.test.ts +13 -13
  468. package/test/model-basis.test.ts +2 -2
  469. package/test/model-gap.test.ts +4 -4
  470. package/test/model-vocabulary.test.ts +2 -2
  471. package/test/move-space.test.ts +1 -1
  472. package/test/oee-accumulator.test.ts +20 -5
  473. package/test/operational-vocabulary.test.ts +2 -2
  474. package/test/project-structure-db.test.ts +2 -2
  475. package/test/projection-reaches-screen.test.ts +7 -2
  476. package/test/projection-reads-declared.test.ts +109 -0
  477. package/test/property-effects.test.ts +10 -6
  478. package/test/read-failure-ledger.test.ts +106 -0
  479. package/test/read-failure-visible.test.ts +7 -7
  480. package/test/read-time.test.ts +118 -0
  481. package/test/reference-grounding.test.ts +1 -1
  482. package/test/reference-hook.test.ts +86 -0
  483. package/test/registry-key-guard.test.ts +1 -1
  484. package/test/rename-twin.test.ts +81 -0
  485. package/test/restart-policy.test.ts +3 -3
  486. package/test/resync-origin-site.test.ts +3 -3
  487. package/test/revision-axis.test.ts +14 -8
  488. package/test/runtime-key.test.ts +3 -3
  489. package/test/scale-twin-bench.test.ts +2 -1
  490. package/test/snapshot-freshness.test.ts +4 -4
  491. package/test/source-outcome-audit.test.ts +1 -1
  492. package/test/space-integrity.test.ts +1 -1
  493. package/test/space-representation.test.ts +66 -0
  494. package/test/space-scope-includes-stopped.test.ts +55 -0
  495. package/test/spec-coverage.test.ts +1 -1
  496. package/test/standard-coverage.test.ts +19 -21
  497. package/test/status-tally.test.ts +1 -1
  498. package/test/structure-revision-db.test.ts +7 -7
  499. package/test/subject-fast-path.test.ts +148 -0
  500. package/test/subject-read-model.test.ts +175 -0
  501. package/test/touched-items.test.ts +2 -2
  502. package/test/twin-audit.test.ts +1 -1
  503. package/test/twin-event-keys.test.ts +1 -1
  504. package/test/twin-model-item-db.test.ts +5 -5
  505. package/test/twin-model-tree-db.test.ts +35 -2
  506. package/test/twin-origin-resync.test.ts +1 -1
  507. package/test/vocabulary-guard.test.ts +1 -1
  508. package/test/warm-start-seam.test.ts +1 -1
  509. package/test/warm-start.test.ts +6 -6
  510. package/test/withheld-door.test.ts +2 -2
  511. package/test/yield-loop.test.ts +4 -4
  512. package/tsconfig.shared.tsbuildinfo +1 -1
  513. package/tsconfig.tsbuildinfo +1 -1
@@ -5,7 +5,7 @@
5
5
  * 템플릿은 노브마다 기본값을 선언한다(`knobs[].default`). 그런데 그 기본값을 채우는 일을 **위자드만**
6
6
  * 하고 있었다 — 서버는 받은 파라미터를 그대로 `build(p)` 에 넘긴다. 그래서 파라미터를 일부만 주고
7
7
  * 부르면(AI·스크립트·API 호출자) 템플릿 안에서 `Number(p.inboundRate) || 0` 이 **0** 이 되고,
8
- * 발생률 0 = 도착 없음 = **아무것도 하지 않는 트윈**이 조용히 만들어진다.
8
+ * 발생률 0 = 도착 없음 = **아무것도 하지 않는 트윈**이 알리지 않고 만들어진다.
9
9
  *
10
10
  * 실제로 그 함정을 밟았다: 유통가공 창고를 파라미터 일부만 주고 만들었더니 이벤트가 0건이었고,
11
11
  * 처음에는 템플릿이 깨진 것으로 오진했다. 기본값을 아는 곳이 계약(노브 스키마)인데 그것을 적용하는
@@ -13,12 +13,12 @@
13
13
  *
14
14
  * ── 0 을 기본값으로 덮지 않는다 ─────────────────────────────────────────────
15
15
  * 발생률 `0` 은 "도착 없음" 이라는 **뜻 있는 값**이다. 없는 것과 0 은 다르다 — falsy 로 판정해 덮으면
16
- * "자극 없이 구조만 보겠다" 는 선언이 조용히 뒤집힌다. 그래서 **키가 없을 때만** 채운다.
16
+ * "자극 없이 구조만 보겠다" 는 선언이 알리지 않고 뒤집힌다. 그래서 **키가 없을 때만** 채운다.
17
17
  * `null` 은 없는 것으로 본다(JSON·GraphQL 경로에서 "주지 않음" 이 null 로 오는 일이 흔하다).
18
18
  *
19
19
  * ── 선언 밖 파라미터는 손대지 않는다 ────────────────────────────────────────
20
20
  * 템플릿은 노브에 없는 값도 읽는다(예: `geo` 대신 `lat`/`lon` 폴백). 모르는 키를 걷어내면 그 경로가
21
- * 조용히 끊긴다. 그대로 통과시킨다.
21
+ * 알리지 않고 끊긴다. 그대로 통과시킨다.
22
22
  */
23
23
 
24
24
  /** 템플릿 노브 — 기본값 적용에 필요한 만큼만 본다. */
@@ -48,7 +48,7 @@ export function applyKnobDefaults(knobs: readonly KnobLike[] | undefined | null,
48
48
  /**
49
49
  * 기본값으로 채운 노브 이름들 — 호출자에게 "무엇을 내가 정해 줬는지" 알릴 때 쓴다.
50
50
  *
51
- * 조용히 채우는 것도 조용히 비우는 것만큼 위험하다: 사용자가 준 적 없는 값으로 트윈이 만들어졌는데
51
+ * 알리지 않고 채우는 것도 알리지 않고 비우는 것만큼 위험하다: 사용자가 준 적 없는 값으로 트윈이 만들어졌는데
52
52
  * 아무 말이 없으면, 나중에 "왜 이 수치인가" 를 아무도 답할 수 없다.
53
53
  */
54
54
  export function filledKnobKeys(knobs: readonly KnobLike[] | undefined | null, params: Record<string, unknown> | undefined | null): string[] {
@@ -1,5 +1,6 @@
1
1
  import type { ReferenceStep } from './reference-progress.js'
2
2
  import type { ReferenceMaster } from './reference-master.js'
3
+ import { twinWarn } from '../../engine/log.js'
3
4
 
4
5
  /*
5
6
  * 레퍼런스 어댑터 계약 (reference-management P1) — 정적 마스터 등록(`registerReference`)을
@@ -34,6 +35,27 @@ export interface AdapterConfigField {
34
35
  multiline?: boolean // JSON/매핑 등 여러 줄 입력(textarea)
35
36
  help?: string // 필드 아래 도움말
36
37
  default?: string // 폼 프리필 기본값(예시 매핑/URL 등)
38
+ /*
39
+ * 값이 정해진 목록 안에 있을 때 — 화면이 고르게 그린다(타이핑하지 않는다).
40
+ *
41
+ * 손으로 적는 칸에 닫힌 값을 받으면 오타가 그대로 저장되고, 그 오타는 오류로 나오지 않는다.
42
+ * 발전소 종류(`sell` | `self-consumption`)가 그렇다 — 틀리면 계량 방향이 반대가 되는데 어디에도
43
+ * 표시되지 않는다.
44
+ *
45
+ * `label` 은 번역 키를 쓸 수 있다(화면이 `meta()` 로 푼다). `value` 는 저장되는 값이라 번역하지 않는다.
46
+ */
47
+ options?: { value: string; label: string }[]
48
+ /*
49
+ * 값의 종류가 정해져 있지만 목록이 너무 커서 `options` 로 실을 수 없을 때 — 화면이 그 종류에 맞는
50
+ * 고르는 칸을 그린다.
51
+ *
52
+ * 'timezone' IANA 시간대. 화면이 `Intl.supportedValuesOf('timeZone')` 로 목록을 만든다.
53
+ * 400개가 넘고 표준이 갱신되므로 우리가 목록을 들고 있지 않는다.
54
+ * 이름이 곧 값이자 표시라 번역하지 않는다.
55
+ *
56
+ * 자유 입력으로 두면 `Asia/seoul` 같은 오타가 저장되고, 그 오타는 오류로 나오지 않는다.
57
+ */
58
+ kind?: 'timezone'
37
59
  }
38
60
  /** 커넥터 메타데이터 — picker·연결 폼용 표시 정보. */
39
61
  export interface AdapterMeta {
@@ -107,10 +129,10 @@ export interface ControlResult {
107
129
  * 한 흐름의 읽기 커서 — **어디까지 읽었나.**
108
130
  *
109
131
  * `since` 와 `seen` 이 **함께** 있어야 한다. 이어 읽기는 경계 시각을 겹쳐 읽고(`gte`), 그 겹침을
110
- * `seen` 이 걸러 낸다. `gt` 로 좁히면 같은 밀리초의 다른 행이 사라진다 — 조용한 누락이다.
132
+ * `seen` 이 걸러 낸다. `gt` 로 좁히면 같은 밀리초의 다른 행이 사라진다 — 들어온 것이 없는 누락이다.
111
133
  *
112
134
  * `seen` 이 예상보다 커지면(한 시각에 몰린 행 수가 늘면) 그것은 원본 쪽 신호다. 어댑터가 그 크기를
113
- * 말해야 한다 — 조용히 커지게 두면 이어 읽기가 막히는 순간까지 아무도 모른다.
135
+ * 말해야 한다 — 알리지 않고 커지게 두면 이어 읽기가 막히는 순간까지 아무도 모른다.
114
136
  */
115
137
  export interface LiveFeedCursor {
116
138
  /** 그 흐름에서 마지막으로 본 시각(원본의 시각, ISO). */
@@ -119,6 +141,28 @@ export interface LiveFeedCursor {
119
141
  seen: string[]
120
142
  }
121
143
 
144
+ /**
145
+ * **트윈이 지금 들고 있는 물품 하나** — 붙는 커넥터가 견줄 근거(§`LiveFeedContinuity.observedItems`).
146
+ *
147
+ * 커널 어휘로 낸다. 원본의 낱말(그 시스템의 상태 코드)로 내지 않는 이유가 있다: 원본 낱말은 여러 개가
148
+ * 커널의 한 값으로 모이므로(예: 네 상태가 「진행 중」 하나가 된다) 되돌릴 수 없다. 그리고 되돌릴
149
+ * 필요도 없다 — 두 원본 상태가 커널에서 같은 값이면 **트윈이 든 사실은 바뀌지 않았다.**
150
+ */
151
+ export interface ObservedItemFact {
152
+ /** 트윈이 든 물품의 키 — 비직렬 재고는 `클래스@자리`. */
153
+ id: string
154
+ /** 클래스 식별자(원문). */
155
+ epcClass?: string
156
+ location?: string
157
+ qty?: number
158
+ uom?: string
159
+ /** 표준 처분(CBV) — 원본 상태 코드가 아니다. */
160
+ disposition?: string
161
+ /** 유통기한(밀리초). */
162
+ expiry?: number
163
+ lot?: string
164
+ }
165
+
122
166
  /**
123
167
  * 재기동을 넘어 이어지는 읽기 상태 — 참조 계층이 소유하고 어댑터에 건넨다(§`openLiveFeed`).
124
168
  *
@@ -127,6 +171,21 @@ export interface LiveFeedCursor {
127
171
  export interface LiveFeedContinuity {
128
172
  /** 지난번에 어디까지 읽었나 — 없으면 첫 붙음이다(그때만 되돌아볼 날수로 창을 만든다). */
129
173
  cursor?: { streams?: Record<string, LiveFeedCursor>; firstAttachedAt?: string }
174
+ /**
175
+ * **트윈이 지금 들고 있는 물품** — 붙는 커넥터가 「무엇이 바뀌었나」를 견줄 근거 (2026-08-28).
176
+ *
177
+ * ── 무엇을 고치나 ───────────────────────────────────────────────────────────
178
+ * 전량 절대값을 읽는 커넥터는 지난 주기와 견주어 **바뀐 것만** 낸다. 그 비교 표가 프로세스 안에만
179
+ * 있어서, 재기동하면 커넥터가 자기 기억을 잃고 **전량을 「바뀐 것」으로 판정**한다. 승화푸드에서 그
180
+ * 한 번이 2,665건이었고, 재기동마다 지난 기록을 덮었다.
181
+ *
182
+ * **트윈은 잃지 않는다** — 저장본에서 되세우고 뜬다(실측 로그: `items 2665 · orders 4794`). 잃는
183
+ * 쪽은 커넥터다. 그래서 트윈이 아는 것을 건네면 첫 주기부터 바뀐 것만 나간다.
184
+ *
185
+ * 없으면 `undefined` 다 — 그것이 「트윈이 아무것도 모른다」는 사실이고, 그때만 전량이 새 사실이다.
186
+ * 빈 배열로 메우지 않는다(그러면 「모른다」와 「없다」가 같아진다).
187
+ */
188
+ observedItems?: ObservedItemFact[]
130
189
  /**
131
190
  * 커서가 움직였다고 알린다 — 이 층이 저장한다.
132
191
  *
@@ -137,11 +196,11 @@ export interface LiveFeedContinuity {
137
196
  /**
138
197
  * **원본에 닿지 못했다** — 어댑터가 읽기 실패를 알린다(2026-08-23).
139
198
  *
140
- * ── 무엇이 조용했나 ─────────────────────────────────────────────────────────
199
+ * ── 무엇이 아무 신호도 내지 않았나 ─────────────────────────────────────────────────────────
141
200
  * 실 원본이 끊겼을 때(접속 시간 초과) 트윈의 **조회 가능한 상태 어디에도** 그 사실이 없었다.
142
201
  * `onRecords` 가 불리지 않으면 유입 장부에 아무 일도 일어나지 않고, `TwinReference.lastError` 는
143
202
  * 마스터 동기·접속 시험에만 적힌다. 그래서 화면이 볼 수 있는 것은 「새 사실이 없다」뿐이었고,
144
- * 그것은 **「원본이 조용하다」와 구별되지 않는다.**
203
+ * 그것은 **「연결된 시스템에서 들어온 것이 없다」와 구별되지 않는다.**
145
204
  *
146
205
  * 어댑터는 로그로 말하고 있었다(재시도 경고). 그러나 **로그는 사람이 볼 때만 값이 있다** — 화면이
147
206
  * 말하려면 상태에 있어야 한다. 이 통로가 그 자리다.
@@ -151,9 +210,35 @@ export interface LiveFeedContinuity {
151
210
  * 흐름 여럿을 읽는 원본은 흐름 이름을 함께 준다(어느 표가 막혔나가 원인 찾기의 절반이다).
152
211
  *
153
212
  * **빈 읽기는 실패가 아니다** — 원본이 「새 것이 없다」고 답한 것은 닿았다는 뜻이므로 `onRecords([])`
154
- * 로 알린다. 그 둘을 섞으면 조용한 원본이 끊긴 원본으로 보인다(고치려던 것의 반대 방향으로 틀린다).
213
+ * 로 알린다. 그 둘을 섞으면 들어온 것이 없는 연결이 끊긴 원본으로 보인다(고치려던 것의 반대 방향으로 틀린다).
214
+ *
215
+ * ── 무엇을 어댑터가 보내고 무엇을 이 층이 세나 (2026-08-27) ─────────────────
216
+ * 어댑터는 **이유**만 보낸다 — 그리고 물러섰다면 다음에 다시 물을 때까지의 시간을 함께 보낸다.
217
+ *
218
+ * 연달아 몇 번 실패했는지와 언제부터 실패하고 있는지는 **이 층이 이미 센다**(`recordReadFailure`).
219
+ * 그것을 어댑터가 함께 보내면 같은 수를 두 곳에서 세게 되고, 갈라지면 어느 쪽이 사실인지 알 수 없다.
220
+ *
221
+ * `reason` 에 **완성된 문장을 담지 않는다.** 이 값은 화면에 그려지고, 화면은 다섯 언어로 그린다.
222
+ * 「3회 연달아 …부터 읽지 못했습니다」처럼 한 언어의 문장을 넣으면 그 문장은 번역되지 않고, 이 층이
223
+ * 세는 수와 어긋날 수도 있다. 이유 하나만 짧게 준다(접속 시간 초과 · 401 · 형식 오류).
224
+ */
225
+ onReadFailure?: (info: {
226
+ reason: string
227
+ stream?: string
228
+ /** 다음에 다시 물을 때까지 남은 시간(ms) — 물러섰을 때만 준다. */
229
+ nextRetryMs?: number
230
+ }) => void
231
+ /**
232
+ * **끊겼다가 돌아왔다** — 어댑터가 회복을 알린다 (2026-08-27).
233
+ *
234
+ * ── 왜 따로 알려야 하나 ─────────────────────────────────────────────────────
235
+ * 읽기가 다시 성공하면 실패 기록은 지워진다. 그것만 하면 「끊긴 적이 있었다」가 화면에서 사라지고,
236
+ * 밤새 두 시간 끊겼던 연결과 한 번도 끊기지 않은 연결이 아침에 똑같이 보인다.
237
+ *
238
+ * 읽기 성공(`onRecords`)만으로 이 층이 회복을 유추하지 않는다 — 몇 번 실패한 뒤였는지와 얼마나
239
+ * 끊겨 있었는지는 재시도를 관리하는 어댑터가 안다.
155
240
  */
156
- onReadFailure?: (info: { reason: string; stream?: string }) => void
241
+ onRecovered?: (info: { afterFailures: number; downMs: number }) => void
157
242
  /**
158
243
  * **읽었는데 창을 넘길 수 없다** — 어댑터가 커서 정체를 알린다 (2026-08-24).
159
244
  *
@@ -193,7 +278,7 @@ export interface LiveFeedContinuity {
193
278
  *
194
279
  * **풀리면 부르지 않는 것으로 끝나지 않는다** — 이유가 사라졌으면 `count: 0` 으로 부르는 것이 아니라
195
280
  * 그 이유를 더 이상 보내지 않으면 된다… 가 **아니다.** 장부는 마지막 값을 들고 있으므로, 풀린 것을
196
- * 알리려면 그 이유로 `count: 0` 을 한 번 보내라(그때 줄이 사라진다). 조용히 그치면 낡은 수가 남는다.
281
+ * 알리려면 그 이유로 `count: 0` 을 한 번 보내라(그때 줄이 사라진다). 알리지 않고 그치면 낡은 수가 남는다.
197
282
  *
198
283
  * 이유는 **자유 문자열**이다 — 무엇을 왜 안 받는지는 원본마다 다르고, 계약이 목록을 닫으면 새 원본을
199
284
  * 붙일 때마다 계약을 고치게 된다(흐름 열쇠와 같은 규율). 사람이 읽을 문장으로 적어라.
@@ -249,7 +334,7 @@ export interface ReferenceAdapter {
249
334
  * 것이 「원본의 사실」이 아니라 「창의 함수」였다.
250
335
  *
251
336
  * 그런데 미러의 상태는 **원천으로 서야 한다**(같은 절: 「미러의 진실은 원천이다 … 심으면 떠난 물건이
252
- * 되살아난다」). 그러니 저널을 접어 상태를 세우는 것으로 고칠 일이 아니다 — 그 선언의 **전제**가
337
+ * 되살아난다」). 그러니 저널을 계산해 상태를 세우는 것으로 고칠 일이 아니다 — 그 선언의 **전제**가
253
338
  * 「원천이 다시 말해 준다」이고, 창이 좁아지면 그 전제가 깨진다. 창을 고치는 것이 그 원칙을 지키는 길이다.
254
339
  *
255
340
  * ── 계약의 모양 ─────────────────────────────────────────────────────────────
@@ -259,11 +344,31 @@ export interface ReferenceAdapter {
259
344
  * `Set` 을 받으면 이 층이 직렬화를 알게 되고, 커서 모양이 바뀔 때 두 곳을 고친다.
260
345
  * · `firstAttachedAt` — **첫 창을 만든 시각.** 이후 재기동에서 바뀌지 않는다. 커널은 아는 구간의
261
346
  * 상한만 안다(`nowTime` = 마지막으로 들은 시각). 이 값이 하한이고, 둘이 「이 트윈이 아는 구간」이다.
262
- * 화면이 수를 보일 때 그 구간을 함께 말해야 한다 — 말하지 않으면 조용히 자르는 것이 된다.
347
+ * 화면이 수를 보일 때 그 구간을 함께 말해야 한다 — 말하지 않으면 알리지 않고 자르는 것이 된다.
263
348
  * · **커서가 없을 때만** 되돌아볼 날수를 쓴다. 그 순간이 `firstAttachedAt` 이다.
264
349
  *
265
350
  * 주지 않는 호출자와도 함께 선다(선택 인자) — 커서를 모르는 배포는 예전처럼 동작한다.
266
351
  */
352
+ /**
353
+ * (선택) **연결된 시스템이 밀어 준 것**을 우리 레코드로 옮긴다 — 웹훅.
354
+ *
355
+ * ── 왜 `openLiveFeed` 와 나누나 (2026-08-26) ───────────────────────────────
356
+ * `openLiveFeed` 는 「가져오기」와 「옮기기」를 한 함수에 담고 있다. 그래서 가져오는 방식이 바뀌면
357
+ * (그쪽이 밀어 주면) 옮기는 코드를 쓸 수 없었다 — 같은 파일 안에 있는데 밖에서 부를 수 없다.
358
+ *
359
+ * 이 함수는 **옮기기만** 한다. 밖에서 받은 본문을 받아 우리 레코드 목록을 돌려준다. 입구·인증·유입은
360
+ * 프레임워크가 맡는다(§`reference-hook`).
361
+ *
362
+ * ── 옮길 수 없으면 던진다 ─────────────────────────────────────────────────
363
+ * 빈 목록과 「옮길 수 없다」는 다르다. 빈 목록은 「우리와 무관한 것이 왔다」이고, 던지는 것은 「그쪽
364
+ * 모양이 우리가 아는 것과 다르다」다. 그 둘을 같게 답하면 그쪽 모양이 바뀐 것을 아무도 모른다.
365
+ *
366
+ * ── 판단은 여기서 한다 ────────────────────────────────────────────────────
367
+ * 통신이 끊긴 설비의 값을 보내지 않는 것, 잰 시각이 나아가지 않은 줄을 거르는 것 — 폴링에서 하던
368
+ * 판단을 여기서도 한다. 그래야 두 길이 같은 답을 낸다.
369
+ */
370
+ handleInbound?(cfg: ConnectionConfig, site: { siteId: string }, body: unknown, headers: Record<string, unknown>): unknown[]
371
+
267
372
  openLiveFeed?(
268
373
  cfg: ConnectionConfig,
269
374
  site: SiteDescriptor,
@@ -322,7 +427,7 @@ export interface ReferenceAdapter {
322
427
  * `product` 실 시스템 **여러 배포**에 붙여 확인했다 — 그때 비로소 제품의 성질이라 말할 수 있다.
323
428
  *
324
429
  * `instance` 와 `product` 를 가르는 이유: 첫 실 연동에서 그 배포만의 성질이 넷 드러났고(관계 리솔버가
325
- * 다른 테넌트의 행을 답한다 · 모르는 필터를 조용히 무시한다 · 지워진 품목의 BOM 이 남아 있다 ·
430
+ * 다른 테넌트의 행을 답한다 · 모르는 필터를 알리지 않고 무시한다 · 지워진 품목의 BOM 이 남아 있다 ·
326
431
  * 사실상 무한을 뜻하는 표식) **그것이 그 제품의 성질인지 그 인스턴스의 성질인지 아무도 모른다.**
327
432
  * 두 번째 배포가 붙는 날 갈린다. 그 사실을 어휘가 담는다.
328
433
  */
@@ -378,7 +483,37 @@ export function capabilitiesOf(adapter: ReferenceAdapter | undefined): Reference
378
483
  /* 어댑터 "타입" 레지스트리(전역·인메모리) — 레퍼런스 데이터는 DB(TwinReference), 어댑터 동작은 여기. */
379
484
  const adapters = new Map<string, ReferenceAdapter>()
380
485
 
486
+ /**
487
+ * 어댑터를 등록한다 — 그리고 **선언과 구현이 어긋나면 그 자리에서 말한다.**
488
+ *
489
+ * ── 왜 필요한가 (2026-08-26 실측) ──────────────────────────────────────────
490
+ * 능력 판정(`capabilitiesOf`)은 **한 방향만** 본다: 선언했는데 구현이 없는 것은 걸러 내고, **구현했는데
491
+ * 선언이 없는 것은 아무 말도 하지 않는다.** 그래서 다음이 오류 없이 일어난다.
492
+ *
493
+ * 커넥터가 `openLiveFeed` 를 구현한다 → `capabilities: ['live']` 를 잊는다
494
+ * → 연결 화면이 「이 원본은 라이브를 못 낸다」로 읽는다
495
+ * → 재기동 정책을 정할 수 없어 트윈 생성이 거절된다
496
+ * → 사람이 보는 것은 「재기동 정책이 없습니다」뿐이다
497
+ *
498
+ * 실제로 그렇게 막혔고(태양광 발전소), 원인을 찾는 데 코드를 뒤져야 했다. 지금 저장소에서 같은 상태인
499
+ * 커넥터가 일곱이다.
500
+ *
501
+ * ── 왜 막지 않고 알리기만 하나 ──────────────────────────────────────────────
502
+ * 등록에서 던지면 커넥터 하나 때문에 호스트가 못 뜬다. 만들다 만 커넥터를 들고 있는 사람이 아무것도
503
+ * 못 하게 된다. 그리고 이 어긋남은 **고칠 수 있는 것**이지 위험한 것이 아니다 — 말해 주면 된다.
504
+ */
381
505
  export function registerAdapterType(adapter: ReferenceAdapter): void {
506
+ const declared = new Set(adapter.capabilities ?? [])
507
+ const undeclared = (['live', 'control'] as const).filter(
508
+ c => typeof (adapter as any)[c === 'live' ? 'openLiveFeed' : 'control'] === 'function' && !declared.has(c)
509
+ )
510
+ if (undeclared.length) {
511
+ twinWarn(
512
+ `[reference] connector "${adapter.type}" implements ${undeclared.join(' and ')} but does not declare it — ` +
513
+ `add capabilities: [${undeclared.map(c => `'${c}'`).join(', ')}] or remove the implementation. ` +
514
+ 'Until then the screens treat this connector as unable to do it, and a live twin cannot be created from it.'
515
+ )
516
+ }
382
517
  adapters.set(adapter.type, adapter)
383
518
  }
384
519
  export function getAdapter(type: string): ReferenceAdapter | undefined {
@@ -0,0 +1,145 @@
1
+ /*
2
+ * 연결된 시스템이 **밀어 주는** 사실을 받는 자리 — 웹훅.
3
+ *
4
+ * 규약(상태 코드 · 비밀값 · 대상 확인)은 `hook-contract.ts` 에 있다. 여기는 그것을 써서 찾고 · 확인하고 ·
5
+ * 커넥터에게 옮기게 하고 · 유입으로 넘기는 순서만 맡는다.
6
+ *
7
+ * ── 왜 필요한가 ─────────────────────────────────────────────────────────────
8
+ * 지금 모든 커넥터가 주기적으로 물어봅니다(폴링). 그 방식은 두 가지를 치릅니다. 연결된 시스템이 값을
9
+ * 갱신하는 주기보다 자주 물으면 같은 값을 되풀이 받고, 그쪽 서버에 그만큼 부담을 줍니다. 실제로
10
+ * 태양광 발전소에 10초마다 물으면서 10분마다 바뀌는 값을 받아 왔습니다.
11
+ *
12
+ * 연결된 시스템이 「바뀌었다」를 말해 줄 수 있으면 그것을 받는 것이 낫습니다. 이 파일이 그 입구입니다.
13
+ *
14
+ * ── 규약 ────────────────────────────────────────────────────────────────────
15
+ *
16
+ * 주소 POST /domain/<도메인>/twin/hook/<연결이름>/<트윈>
17
+ * 인증 연결 설정에 저장한 비밀값(`hookSecret`)을 헤더로 보낸다
18
+ * X-Twin-Hook-Secret: <비밀값> 또는 Authorization: Bearer <비밀값>
19
+ * 본문 연결된 시스템이 정한 모양 그대로. 우리 어휘로 옮기는 것은 커넥터의 일이다
20
+ * 응답 아래 §`hookStatus`
21
+ *
22
+ * ── 왜 사람 인증(JWT)이 아닌가 ──────────────────────────────────────────────
23
+ * 밀어 주는 쪽은 사람이 아닙니다. 우리 토큰을 발급받아 갱신하며 관리하라고 요구하면 대부분의 현장에서
24
+ * 연동이 서지 않습니다. 그래서 연결마다 비밀값을 두고 그것으로 확인합니다.
25
+ *
26
+ * **비밀값이 없는 연결은 훅을 받지 않습니다.** 기본이 거부입니다 — 인증 없는 입구를 열어 두는 것보다
27
+ * 훅을 못 쓰는 것이 낫습니다.
28
+ *
29
+ * ── 왜 커넥터가 옮기나 (시나리오가 아니라) ──────────────────────────────────
30
+ * 연결된 시스템의 웹훅은 그쪽이 정한 고정된 모양으로 옵니다. 그 모양을 아는 것은 그 커넥터이고,
31
+ * 옮기는 판단은 코드여야 테스트로 지킬 수 있습니다. 실제로 지금 커넥터들이 코드로 판단합니다 —
32
+ * 통신이 끊긴 설비의 값은 보내지 않고, 잰 시각이 나아가지 않은 줄은 거릅니다. 그 판단을 변환식으로
33
+ * 옮기면 약해지고 테스트가 없어집니다.
34
+ *
35
+ * ── 밀어 주기만으로는 안 된다 ───────────────────────────────────────────────
36
+ * 훅은 반드시 놓칩니다 — 우리가 내려가 있을 때, 그쪽이 못 보냈을 때, 네트워크가 끊겼을 때. 그래서
37
+ * 훅을 쓰는 연결도 **드문 폴링을 유지합니다.** 폴링의 역할이 「값을 가져오는 것」에서 「놓친 것이 있나
38
+ * 확인하는 것」으로 바뀌는 것입니다. 그 주기는 연결 설정이 정합니다.
39
+ *
40
+ * ── 같은 것이 두 번 오는 것 ─────────────────────────────────────────────────
41
+ * 밀어 주는 방식은 재전송이 정상입니다(그쪽이 우리 응답을 못 받으면 다시 보냅니다). 그것은 유입
42
+ * 경계가 이미 거릅니다(§`FactDeduper`) — 이 파일이 따로 하지 않습니다. 응답에 몇 건이 중복이었는지
43
+ * 함께 알립니다.
44
+ */
45
+ import { getRepository } from '@things-factory/shell'
46
+
47
+ import { TwinReference } from './twin-reference.js'
48
+ import { getAdapter } from './reference-adapter.js'
49
+ import { HOOK_STATUS, producedInstance, secretFromHeaders, secretMatches, type HookOutcome } from './hook-contract.js'
50
+
51
+ /**
52
+ * 훅 하나를 처리한다 — 찾고 · 확인하고 · 커넥터에게 옮기게 하고 · 유입으로 넘긴다.
53
+ *
54
+ * 유입 자체(`ingest`)는 부르는 쪽이 넘겨 준다: 이 파일이 엔진을 직접 부르면 시험할 때 엔진이 필요해지고,
55
+ * 그러면 규약을 확인하는 시험을 쓸 수 없다.
56
+ */
57
+ export async function handleHook(args: {
58
+ domainId: string
59
+ source: string
60
+ instanceId: string
61
+ headers: Record<string, unknown>
62
+ body: unknown
63
+ /** 레코드를 트윈에 넣는다 — 받은 수와 버린 수를 돌려준다. */
64
+ ingest: (instanceId: string, records: unknown[]) => { applied: number; duplicates: number; rejected: number }
65
+ }): Promise<HookOutcome> {
66
+ const { domainId, source, instanceId, headers, body, ingest } = args
67
+
68
+ const ref = await getRepository(TwinReference)
69
+ .findOne({ where: { domain: { id: domainId }, source } })
70
+ .catch(() => null)
71
+ if (!ref) {
72
+ return { status: HOOK_STATUS.unknownTarget, body: { ok: false, error: `unknown connection "${source}"` } }
73
+ }
74
+
75
+ const cfg: any = (ref as any).connectionConfig ?? {}
76
+ const expected = String(cfg.hookSecret ?? '').trim()
77
+ if (!expected) {
78
+ /* 비밀값을 선언하지 않은 연결은 훅을 받지 않는다 — 기본이 거부다. */
79
+ return {
80
+ status: HOOK_STATUS.badSecret,
81
+ body: {
82
+ ok: false,
83
+ error: `connection "${source}" declares no hookSecret — set one in the connection settings before pushing to it`
84
+ }
85
+ }
86
+ }
87
+ if (!secretMatches(secretFromHeaders(headers), expected)) {
88
+ return { status: HOOK_STATUS.badSecret, body: { ok: false, error: 'hook secret does not match' } }
89
+ }
90
+
91
+ const site = producedInstance(ref as any, instanceId)
92
+ if (!site) {
93
+ return {
94
+ status: HOOK_STATUS.unknownTarget,
95
+ body: { ok: false, error: `twin "${instanceId}" was not created by connection "${source}"` }
96
+ }
97
+ }
98
+
99
+ const adapter: any = getAdapter(String((ref as any).adapterType ?? ''))
100
+ if (typeof adapter?.handleInbound !== 'function') {
101
+ return {
102
+ status: HOOK_STATUS.unsupported,
103
+ body: { ok: false, error: `connector "${(ref as any).adapterType}" cannot take pushed events (no handleInbound)` }
104
+ }
105
+ }
106
+
107
+ let records: unknown[]
108
+ try {
109
+ records = adapter.handleInbound(cfg, site, body, headers) ?? []
110
+ } catch (e: any) {
111
+ /* 옮기지 못한 것은 그쪽 모양이 바뀐 것이다 — 다시 보내도 같으므로 재시도를 부르지 않는다. */
112
+ return { status: HOOK_STATUS.badPayload, body: { ok: false, error: e?.message ?? 'cannot translate the payload' } }
113
+ }
114
+ if (!Array.isArray(records)) {
115
+ return { status: HOOK_STATUS.badPayload, body: { ok: false, error: 'the connector did not return a list of records' } }
116
+ }
117
+ if (!records.length) {
118
+ /* 옮길 것이 없는 것은 오류가 아니다 — 그쪽이 우리와 무관한 것을 보낸 것일 수 있다. */
119
+ return { status: HOOK_STATUS.ok, body: { ok: true, offered: 0, applied: 0, duplicates: 0, rejected: 0 } }
120
+ }
121
+
122
+ let result: { applied: number; duplicates: number; rejected: number }
123
+ try {
124
+ result = ingest(instanceId, records)
125
+ } catch (e: any) {
126
+ return { status: HOOK_STATUS.failed, body: { ok: false, error: e?.message ?? 'ingest failed' } }
127
+ }
128
+
129
+ if (result.applied === 0 && result.duplicates === 0 && records.length > 0 && result.rejected === 0) {
130
+ /*
131
+ * 넣었는데 한 건도 반영되지 않았고 중복도 거부도 아니면, 그 트윈이 실시간으로 돌지 않는 것이다.
132
+ * 200 을 주면 그쪽은 전달된 줄 알고 그 사실을 버린다. 그래서 재시도로 풀리지 않는다는 것을 코드로
133
+ * 말한다 — 사람이 트윈을 띄워야 한다.
134
+ */
135
+ return {
136
+ status: HOOK_STATUS.notLive,
137
+ body: { ok: false, error: `twin "${instanceId}" is not running live — nothing was applied`, offered: records.length }
138
+ }
139
+ }
140
+
141
+ return {
142
+ status: HOOK_STATUS.ok,
143
+ body: { ok: true, offered: records.length, applied: result.applied, duplicates: result.duplicates, rejected: result.rejected }
144
+ }
145
+ }
@@ -1,8 +1,9 @@
1
1
  import { twinLog, twinWarn, twinError } from '../../engine/log.js'
2
2
  import { getRepository } from '@things-factory/shell'
3
3
 
4
- import { getAdapter, type ConnectionConfig, type LiveFeedContinuity, type SiteDescriptor } from './reference-adapter.js'
5
- import { TwinEngine, ingestCanonicalRecords, registerLiveFeedProbe, type CanonicalRecord } from '../../engine/index.js'
4
+ import { getAdapter, type ConnectionConfig, type LiveFeedContinuity, type ObservedItemFact, type SiteDescriptor } from './reference-adapter.js'
5
+ import { TwinEngine, ingestCanonicalRecords, registerLiveFeedProbe } from '../../engine/index.js'
6
+ import { type CanonicalRecord } from '@operato/ops-contract'
6
7
  import { TwinInstance } from '../twin-instance/twin-instance.js'
7
8
  import { TwinReference } from './twin-reference.js'
8
9
 
@@ -35,7 +36,7 @@ export async function startReferenceLiveFeed(
35
36
  * (`endpoint`·`token`…), `site` 는 커넥터가 원본에서 발견한 것이다 — **레퍼런스의 이름은 호스트가
36
37
  * 붙이는 것이고 원본은 그것을 모른다.** 커넥터가 채울 수 없는 값을 커넥터에서 읽으려 한 것이 잘못이었다.
37
38
  *
38
- * 주지 않으면 커서를 잇지 않고 **그 사실을 말한다**(조용히 창이 미끄러지지 않게).
39
+ * 주지 않으면 커서를 잇지 않고 **그 사실을 말한다**(알리지 않고 창이 미끄러지지 않게).
39
40
  */
40
41
  source?: string
41
42
  ): Promise<boolean> {
@@ -53,7 +54,7 @@ export async function startReferenceLiveFeed(
53
54
  /*
54
55
  * **어디까지 읽었나를 이 층이 들고 건넨다**(§`LiveFeedContinuity`, 2026-08-23).
55
56
  *
56
- * 어댑터가 이 값을 자기 사물함에 두면 원본이 늘 때마다 저장 기제가 늘고, 그중 하나가 조용히 다르게
57
+ * 어댑터가 이 값을 자기 사물함에 두면 원본이 늘 때마다 저장 기제가 늘고, 그중 하나가 알리지 않고 다르게
57
58
  * 동작한다. 그리고 커널이 이미 갈라 두었다: 「원천이 애초에 다시 말해 주지 않는 축」은 재기동
58
59
  * 연속성으로 이어받는다(§`hydrateContinuity`) — 읽기 커서가 정확히 그 성질이다.
59
60
  *
@@ -61,21 +62,53 @@ export async function startReferenceLiveFeed(
61
62
  * 첫 창을 만든다. 빈 객체로 메우지 않는다(그러면 첫 붙음과 이어 붙음이 같아진다).
62
63
  */
63
64
  const continuity = await loadLiveCursor(domainId, instanceId, source)
65
+ /*
66
+ * ── **트윈이 아는 것을 함께 건넨다** (2026-08-28) ───────────────────────────
67
+ *
68
+ * 전량 절대값을 읽는 커넥터는 지난 주기와 견주어 바뀐 것만 낸다. 그 비교 표가 프로세스 안에만
69
+ * 있어서 재기동하면 커넥터가 전량을 「바뀐 것」으로 판정했다 — 승화푸드에서 2,665건이다.
70
+ *
71
+ * 트윈은 저장본에서 되세우므로 잃지 않았다. 그래서 그 사실을 건네면 첫 주기부터 바뀐 것만 나간다.
72
+ * 커널 어휘로 건넨다 — 원본 낱말로 되돌리는 일은 여러 낱말이 한 값으로 모여 불가능하고, 필요도 없다.
73
+ */
74
+ if (continuity) continuity.observedItems = observedItemsOf(domainId, instanceId)
64
75
  const unsubscribe = adapter.openLiveFeed(cfg, site, (records: unknown[]) => {
65
76
  if (!TwinEngine.owns(domainId, instanceId)) {
66
77
  stopReferenceLiveFeed(instanceId) // 인스턴스 사라지면 자가 정지(안전망)
67
78
  return
68
79
  }
69
80
  const offered = Array.isArray(records) ? records.length : records ? 1 : 0
70
- const { accepted, rejected } = ingestCanonicalRecords(records as CanonicalRecord[], domainId, new Date().toISOString())
81
+ const { accepted, rejected, masterData } = ingestCanonicalRecords(
82
+ records as CanonicalRecord[],
83
+ domainId,
84
+ new Date().toISOString(),
85
+ TwinEngine.factScope(domainId, instanceId)
86
+ )
87
+ /*
88
+ * ── **사건이 아닌 것은 상태만 세운다** (2026-08-28) ─────────────────────────
89
+ *
90
+ * 마스터데이터 — 변하지 않는 속성(유통기한·로트번호). 시각축이 없는 값이므로 지난 기록에 적지
91
+ * 않는다(§`applyMasterData`). 이 문이 없던 동안 그 값은 사건에 실려 왔고, 표준이 그것을
92
+ * 「들어왔다」에만 허용하므로 전량을 읽는 원본은 **들어오지 않은 것을 들어왔다고** 말해야 했다.
93
+ *
94
+ * 반영된 건수를 함께 센다 — 라이브로 돌지 않는 트윈은 0 을 내므로, 그 차이가 「바닥에 버려진 수」다.
95
+ */
96
+ const masterApplied = masterData.length ? TwinEngine.applyMasterData(domainId, instanceId, masterData) : 0
97
+ if (masterData.length && masterApplied < masterData.length) {
98
+ twinWarn(
99
+ `[twin-live] "${instanceId}": ${masterData.length - masterApplied}/${masterData.length} master data element(s) NOT applied — ` +
100
+ 'the twin is not running live; lot attributes such as expiry will stay empty.'
101
+ )
102
+ }
71
103
  /*
72
104
  * 커널이 **몇 건을 실제로 받았나**를 함께 본다. 트윈이 라이브로 돌지 않으면 `ingestLive` 가 0 을
73
105
  * 돌려주고, 그 차이가 곧 「바닥에 버려진 수」다. 이것을 통과로 세면 멈춘 트윈이 100% 로 보인다.
74
106
  */
75
- const applied = accepted.length ? TwinEngine.ingestLive(domainId, instanceId, accepted) : 0
107
+ /* 어떤 길로 들어왔는지 함께 적는다 — 화면이 「이 트윈은 무엇을 통해 받나」를 말할 수 있게. */
108
+ const applied = accepted.length ? TwinEngine.ingestLive(domainId, instanceId, accepted, 'connector') : 0
76
109
  const undelivered = accepted.length - applied
77
110
  /*
78
- * **거부한 것을 조용히 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이
111
+ * **거부한 것을 알리지 않고 버리지 않는다.** 예전에는 `rejected` 를 받지도 않았다 — 커넥터의 매핑이
79
112
  * 틀리면 트윈은 「아무 일도 일어나지 않는」 모습이 되고, 로그에도 아무 흔적이 없었다.
80
113
  *
81
114
  * 그다음 자리였던 「화면까지 나르기」를 이제 채운다. 건마다 브로드캐스팅하지 않고 **집계**한다 — 통과분까지
@@ -84,7 +117,7 @@ export async function startReferenceLiveFeed(
84
117
  */
85
118
  /*
86
119
  * **읽었다는 것 자체가 사실이다** — 빈 읽기도 성공이므로 단절 기록을 지운다(§`onReadFailure`).
87
- * 그 둘을 섞으면 조용한 원본이 끊긴 원본으로 보인다.
120
+ * 그 둘을 섞으면 들어온 것이 없는 연결이 끊긴 원본으로 보인다.
88
121
  */
89
122
  TwinEngine.clearIngestReadFailure(domainId, instanceId)
90
123
  TwinEngine.recordIngestResult(domainId, instanceId, offered, rejected, Date.now(), undelivered)
@@ -104,13 +137,35 @@ export async function startReferenceLiveFeed(
104
137
  return true
105
138
  }
106
139
 
140
+ /**
141
+ * 트윈이 지금 들고 있는 물품을 커널 어휘로 낸다 — 없으면 `undefined`.
142
+ *
143
+ * 상태가 없으면(트윈이 아직 아무것도 모르면) `undefined` 다. 빈 배열로 메우지 않는다 — 「모른다」와
144
+ * 「하나도 없다」는 다른 사실이고, 커넥터가 그 둘에 다르게 반응해야 한다.
145
+ */
146
+ function observedItemsOf(domainId: string, instanceId: string): ObservedItemFact[] | undefined {
147
+ const state = TwinEngine.snapshot(domainId, instanceId)
148
+ const items = (state as any)?.items
149
+ if (!Array.isArray(items)) return undefined
150
+ return items.map((i: any) => ({
151
+ id: String(i?.subLotId ?? i?.epc ?? ''),
152
+ ...(i?.gtin ? { epcClass: String(i.gtin) } : {}),
153
+ ...(i?.location ? { location: String(i.location) } : {}),
154
+ ...(typeof i?.qty === 'number' ? { qty: i.qty } : {}),
155
+ ...(i?.uom ? { uom: String(i.uom) } : {}),
156
+ ...(i?.disposition ? { disposition: String(i.disposition) } : {}),
157
+ ...(typeof i?.expiry === 'number' ? { expiry: i.expiry } : {}),
158
+ ...(i?.lot ? { lot: String(i.lot) } : {})
159
+ })).filter((i: ObservedItemFact) => !!i.id)
160
+ }
161
+
107
162
  /**
108
163
  * 그 원본의 읽기 커서를 불러오고, 움직이면 저장하는 통로를 만든다.
109
164
  *
110
165
  * 커서는 **원본(`TwinReference`)의 것**이지 인스턴스의 것이 아니다 — 한 원본이 여러 인스턴스를 낳고
111
166
  * (`scopeSpec.produced`) 읽는 것은 한 번이다. 인스턴스마다 커서를 두면 같은 표를 여러 번 읽는다.
112
167
  *
113
- * 저장 실패는 **삼키지 않고 말한다.** 조용히 실패하면 다음 재기동에서 창이 다시 미끄러지고, 그 손실은
168
+ * 저장 실패는 **삼키지 않고 말한다.** 오류 없이 실패하면 다음 재기동에서 창이 다시 미끄러지고, 그 손실은
114
169
  * 오류 없이 사라진다 — 이 컬럼을 만든 이유가 바로 그것이다.
115
170
  */
116
171
  async function loadLiveCursor(
@@ -120,7 +175,7 @@ async function loadLiveCursor(
120
175
  source?: string
121
176
  ): Promise<LiveFeedContinuity | undefined> {
122
177
  if (!source) {
123
- /* 원본을 가리킬 수 없으면 저장할 자리도 없다 — 그 사실을 말한다(조용히 커서 없이 돌지 않게). */
178
+ /* 원본을 가리킬 수 없으면 저장할 자리도 없다 — 그 사실을 말한다(알리지 않고 커서 없이 돌지 않게). */
124
179
  twinWarn('[twin-live] live cursor not carried — the caller did not say which reference this feed reads from')
125
180
  return undefined
126
181
  }
@@ -157,13 +212,30 @@ async function loadLiveCursor(
157
212
  * 그러니 여기서는 `lastError` 에만 적는다(사람이 목록에서 이유를 볼 수 있게). 「지금 닿나」는
158
213
  * `ingestHealth().readFailure` 가 답하고, 성공한 주기가 그것을 지운다.
159
214
  */
160
- onReadFailure: ({ reason, stream }) => {
161
- TwinEngine.recordIngestReadFailure(domainId, instanceId, reason, Date.now(), stream)
215
+ onReadFailure: ({ reason, stream, nextRetryMs }) => {
216
+ TwinEngine.recordIngestReadFailure(domainId, instanceId, reason, Date.now(), stream, nextRetryMs)
162
217
  twinWarn(`[twin-live] "${source}": cannot read from the source${stream ? ` (${stream})` : ''} — ${reason}`)
163
218
  repo
164
219
  .update({ id: row.id }, { lastError: `live: ${reason}` } as any)
165
220
  .catch(err => twinWarn(`[twin-live] "${source}": read failure not recorded — ${err?.message ?? err}`))
166
221
  },
222
+ /*
223
+ * **끊겼다가 돌아왔다** — 그 사실을 장부에 남긴다 (2026-08-27).
224
+ *
225
+ * 실패 기록을 지우기만 하면 「끊긴 적이 있었다」가 화면에서 사라진다. 그러면 밤새 두 시간 끊겼던
226
+ * 연결과 한 번도 끊기지 않은 연결이 아침에 똑같이 보인다.
227
+ *
228
+ * `lastError` 도 함께 지운다 — 목록에 남은 옛 이유가 지금의 상태로 읽힌다.
229
+ */
230
+ onRecovered: ({ afterFailures, downMs }) => {
231
+ TwinEngine.recordIngestRecovered(domainId, instanceId, afterFailures, downMs, Date.now())
232
+ twinLog(
233
+ `[twin-live] "${source}": reading again after ${afterFailures} failed attempt(s) — it was out for ${Math.round(downMs / 1000)}s`
234
+ )
235
+ repo
236
+ .update({ id: row.id }, { lastError: null } as any)
237
+ .catch(err => twinWarn(`[twin-live] "${source}": recovery not recorded — ${err?.message ?? err}`))
238
+ },
167
239
  /**
168
240
  * **읽었는데 창을 넘길 수 없다** — 위와 조치가 반대인 사실이다 (2026-08-24).
169
241
  *
@@ -222,7 +294,7 @@ export function listReferenceLiveFeeds(): string[] {
222
294
  /**
223
295
  * 부팅 때 **끊어진 피드를 다시 붙인다** — 도는 미러 트윈에 계측이 계속 들어오게.
224
296
  *
225
- * ── 무엇이 조용히 끊겼나 (2026-08-14) ───────────────────────────────────────
297
+ * ── 무엇이 알리지 않고 끊겼나 (2026-08-14) ───────────────────────────────────────
226
298
  * `startReferenceLiveFeed` 를 부르는 곳은 `importTwinReference` **하나뿐**이었다. 그래서 서버를 한 번
227
299
  * 재기동하면 미러 트윈은 「도는 중」이라 말하면서 아무 계측도 받지 않았다 — 오류도 로그도 없이,
228
300
  * 사람은 「값이 안 변하네」로 알게 된다. 실 배포에서 재기동 한 번에 계측이 멈추는 셈이다.
@@ -246,7 +318,7 @@ export async function resumeReferenceLiveFeeds(): Promise<{ resumed: string[]; f
246
318
  * 다시 세우므로(`startLive`) **먼저 쌓인 수요 구간이 리셋된다** — 실제로 그렇게 났다
247
319
  * (로그: `reattached 2 feed(s): ems-plant-1, ems-plant-1`).
248
320
  *
249
- * 그러니 한 번만 붙이고, **두 레퍼런스가 같은 트윈을 주장한다는 사실은 말한다** — 조용히 하나를
321
+ * 그러니 한 번만 붙이고, **두 레퍼런스가 같은 트윈을 주장한다는 사실은 말한다** — 알리지 않고 하나를
250
322
  * 고르면 어느 접속이 그 트윈을 먹이고 있는지 아무도 모르게 된다.
251
323
  */
252
324
  const attached = new Map<string, string>() // instanceId → 먼저 붙인 레퍼런스
@@ -308,7 +380,7 @@ export async function resumeReferenceLiveFeeds(): Promise<{ resumed: string[]; f
308
380
  }
309
381
 
310
382
  if (resumed.length) twinLog(`[twin-live] reattached ${resumed.length} feed(s): ${resumed.join(', ')}`)
311
- /* **조용히 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */
383
+ /* **알리지 않고 넘기지 않는다** — 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다. */
312
384
  for (const f of failed) twinWarn(`[twin-live] "${f.instanceId}" feed NOT reattached — ${f.error}`)
313
385
  return { resumed, failed }
314
386
  }