@things-factory/headless-twin 10.0.18 → 10.1.0

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 (331) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +10 -1
  2. package/dist-server/engine/canonical-ingest.js +110 -33
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/event-time.d.ts +25 -0
  5. package/dist-server/engine/event-time.js +98 -0
  6. package/dist-server/engine/event-time.js.map +1 -0
  7. package/dist-server/engine/index.d.ts +2 -0
  8. package/dist-server/engine/index.js +23 -0
  9. package/dist-server/engine/index.js.map +1 -1
  10. package/dist-server/engine/ingest-dedupe.d.ts +18 -6
  11. package/dist-server/engine/ingest-dedupe.js +201 -15
  12. package/dist-server/engine/ingest-dedupe.js.map +1 -1
  13. package/dist-server/engine/ingest-health.d.ts +60 -1
  14. package/dist-server/engine/ingest-health.js +43 -8
  15. package/dist-server/engine/ingest-health.js.map +1 -1
  16. package/dist-server/engine/kpi-fold.d.ts +19 -0
  17. package/dist-server/engine/kpi-fold.js +4 -1
  18. package/dist-server/engine/kpi-fold.js.map +1 -1
  19. package/dist-server/engine/kpi-query.js +23 -3
  20. package/dist-server/engine/kpi-query.js.map +1 -1
  21. package/dist-server/engine/oee-accumulator.d.ts +35 -2
  22. package/dist-server/engine/oee-accumulator.js +56 -4
  23. package/dist-server/engine/oee-accumulator.js.map +1 -1
  24. package/dist-server/engine/twin-engine.d.ts +72 -3
  25. package/dist-server/engine/twin-engine.js +118 -5
  26. package/dist-server/engine/twin-engine.js.map +1 -1
  27. package/dist-server/index.js +21 -0
  28. package/dist-server/index.js.map +1 -1
  29. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.d.ts +5 -0
  30. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js +66 -0
  31. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js.map +1 -0
  32. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.d.ts +5 -0
  33. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js +79 -0
  34. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js.map +1 -0
  35. package/dist-server/migrations/index.js +5 -1
  36. package/dist-server/migrations/index.js.map +1 -1
  37. package/dist-server/routes.js +19 -16
  38. package/dist-server/routes.js.map +1 -1
  39. package/dist-server/service/actuation/actuation-advisor.d.ts +60 -0
  40. package/dist-server/service/actuation/actuation-advisor.js +53 -0
  41. package/dist-server/service/actuation/actuation-advisor.js.map +1 -0
  42. package/dist-server/service/actuation/actuation-outcome.d.ts +52 -0
  43. package/dist-server/service/actuation/actuation-outcome.js +34 -0
  44. package/dist-server/service/actuation/actuation-outcome.js.map +1 -0
  45. package/dist-server/service/actuation/actuation-rule-resolver.d.ts +19 -0
  46. package/dist-server/service/actuation/actuation-rule-resolver.js +161 -0
  47. package/dist-server/service/actuation/actuation-rule-resolver.js.map +1 -0
  48. package/dist-server/service/actuation/actuation-rule.d.ts +55 -0
  49. package/dist-server/service/actuation/actuation-rule.js +130 -0
  50. package/dist-server/service/actuation/actuation-rule.js.map +1 -0
  51. package/dist-server/service/actuation/approval-gateway.d.ts +20 -0
  52. package/dist-server/service/actuation/approval-gateway.js +14 -0
  53. package/dist-server/service/actuation/approval-gateway.js.map +1 -0
  54. package/dist-server/service/actuation/catch-up-effects.d.ts +16 -0
  55. package/dist-server/service/actuation/catch-up-effects.js +120 -0
  56. package/dist-server/service/actuation/catch-up-effects.js.map +1 -0
  57. package/dist-server/service/actuation/command-dispatcher.d.ts +230 -0
  58. package/dist-server/service/actuation/command-dispatcher.js +269 -0
  59. package/dist-server/service/actuation/command-dispatcher.js.map +1 -0
  60. package/dist-server/service/actuation/command-store.d.ts +11 -0
  61. package/dist-server/service/actuation/command-store.js +219 -0
  62. package/dist-server/service/actuation/command-store.js.map +1 -0
  63. package/dist-server/service/actuation/dispatch-after-approval.d.ts +11 -0
  64. package/dist-server/service/actuation/dispatch-after-approval.js +73 -0
  65. package/dist-server/service/actuation/dispatch-after-approval.js.map +1 -0
  66. package/dist-server/service/actuation/effect-match.d.ts +53 -0
  67. package/dist-server/service/actuation/effect-match.js +65 -0
  68. package/dist-server/service/actuation/effect-match.js.map +1 -0
  69. package/dist-server/service/actuation/index.d.ts +25 -0
  70. package/dist-server/service/actuation/index.js +30 -0
  71. package/dist-server/service/actuation/index.js.map +1 -0
  72. package/dist-server/service/actuation/retry-plan.d.ts +63 -0
  73. package/dist-server/service/actuation/retry-plan.js +99 -0
  74. package/dist-server/service/actuation/retry-plan.js.map +1 -0
  75. package/dist-server/service/actuation/rule-evaluate.d.ts +127 -0
  76. package/dist-server/service/actuation/rule-evaluate.js +205 -0
  77. package/dist-server/service/actuation/rule-evaluate.js.map +1 -0
  78. package/dist-server/service/actuation/rule-loop.d.ts +5 -0
  79. package/dist-server/service/actuation/rule-loop.js +156 -0
  80. package/dist-server/service/actuation/rule-loop.js.map +1 -0
  81. package/dist-server/service/actuation/rule-runner.d.ts +52 -0
  82. package/dist-server/service/actuation/rule-runner.js +135 -0
  83. package/dist-server/service/actuation/rule-runner.js.map +1 -0
  84. package/dist-server/service/actuation/rule-wiring.d.ts +24 -0
  85. package/dist-server/service/actuation/rule-wiring.js +80 -0
  86. package/dist-server/service/actuation/rule-wiring.js.map +1 -0
  87. package/dist-server/service/actuation/settle-effects.d.ts +21 -0
  88. package/dist-server/service/actuation/settle-effects.js +101 -0
  89. package/dist-server/service/actuation/settle-effects.js.map +1 -0
  90. package/dist-server/service/actuation/sweep-dispatch.d.ts +37 -0
  91. package/dist-server/service/actuation/sweep-dispatch.js +125 -0
  92. package/dist-server/service/actuation/sweep-dispatch.js.map +1 -0
  93. package/dist-server/service/actuation/twin-command-resolver.d.ts +120 -0
  94. package/dist-server/service/actuation/twin-command-resolver.js +406 -0
  95. package/dist-server/service/actuation/twin-command-resolver.js.map +1 -0
  96. package/dist-server/service/actuation/twin-command.d.ts +111 -0
  97. package/dist-server/service/actuation/twin-command.js +170 -0
  98. package/dist-server/service/actuation/twin-command.js.map +1 -0
  99. package/dist-server/service/actuation/twin-now.d.ts +41 -0
  100. package/dist-server/service/actuation/twin-now.js +65 -0
  101. package/dist-server/service/actuation/twin-now.js.map +1 -0
  102. package/dist-server/service/index.d.ts +5 -2
  103. package/dist-server/service/index.js +28 -17
  104. package/dist-server/service/index.js.map +1 -1
  105. package/dist-server/service/reference/actuation-routing.d.ts +60 -0
  106. package/dist-server/service/reference/actuation-routing.js +126 -0
  107. package/dist-server/service/reference/actuation-routing.js.map +1 -0
  108. package/dist-server/service/reference/actuation-target.d.ts +65 -0
  109. package/dist-server/service/reference/actuation-target.js +94 -0
  110. package/dist-server/service/reference/actuation-target.js.map +1 -0
  111. package/dist-server/service/reference/connection-portability-resolver.d.ts +29 -0
  112. package/dist-server/service/reference/connection-portability-resolver.js +181 -0
  113. package/dist-server/service/reference/connection-portability-resolver.js.map +1 -0
  114. package/dist-server/service/reference/connection-portability.d.ts +138 -0
  115. package/dist-server/service/reference/connection-portability.js +230 -0
  116. package/dist-server/service/reference/connection-portability.js.map +1 -0
  117. package/dist-server/service/reference/fill-cadence.d.ts +17 -0
  118. package/dist-server/service/reference/fill-cadence.js +31 -0
  119. package/dist-server/service/reference/fill-cadence.js.map +1 -0
  120. package/dist-server/service/reference/fill-dedupe.d.ts +16 -0
  121. package/dist-server/service/reference/fill-dedupe.js +63 -0
  122. package/dist-server/service/reference/fill-dedupe.js.map +1 -0
  123. package/dist-server/service/reference/fill-loop.d.ts +35 -0
  124. package/dist-server/service/reference/fill-loop.js +187 -0
  125. package/dist-server/service/reference/fill-loop.js.map +1 -0
  126. package/dist-server/service/reference/fill-span.d.ts +10 -0
  127. package/dist-server/service/reference/fill-span.js +37 -0
  128. package/dist-server/service/reference/fill-span.js.map +1 -0
  129. package/dist-server/service/reference/hook-contract.d.ts +149 -22
  130. package/dist-server/service/reference/hook-contract.js +236 -23
  131. package/dist-server/service/reference/hook-contract.js.map +1 -1
  132. package/dist-server/service/reference/hook-rejected.d.ts +25 -0
  133. package/dist-server/service/reference/hook-rejected.js +48 -0
  134. package/dist-server/service/reference/hook-rejected.js.map +1 -0
  135. package/dist-server/service/reference/hook-store.d.ts +3 -0
  136. package/dist-server/service/reference/hook-store.js +28 -0
  137. package/dist-server/service/reference/hook-store.js.map +1 -0
  138. package/dist-server/service/reference/index.d.ts +4 -1
  139. package/dist-server/service/reference/index.js +5 -1
  140. package/dist-server/service/reference/index.js.map +1 -1
  141. package/dist-server/service/reference/live-cadence.d.ts +68 -0
  142. package/dist-server/service/reference/live-cadence.js +44 -0
  143. package/dist-server/service/reference/live-cadence.js.map +1 -0
  144. package/dist-server/service/reference/live-feed-lease.d.ts +15 -0
  145. package/dist-server/service/reference/live-feed-lease.js +56 -0
  146. package/dist-server/service/reference/live-feed-lease.js.map +1 -0
  147. package/dist-server/service/reference/live-ingest.d.ts +21 -0
  148. package/dist-server/service/reference/live-ingest.js +66 -0
  149. package/dist-server/service/reference/live-ingest.js.map +1 -0
  150. package/dist-server/service/reference/reference-adapter.d.ts +355 -2
  151. package/dist-server/service/reference/reference-adapter.js +19 -1
  152. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  153. package/dist-server/service/reference/reference-fill-run.d.ts +50 -0
  154. package/dist-server/service/reference/reference-fill-run.js +142 -0
  155. package/dist-server/service/reference/reference-fill-run.js.map +1 -0
  156. package/dist-server/service/reference/reference-fill.d.ts +90 -0
  157. package/dist-server/service/reference/reference-fill.js +141 -0
  158. package/dist-server/service/reference/reference-fill.js.map +1 -0
  159. package/dist-server/service/reference/reference-hook.d.ts +64 -6
  160. package/dist-server/service/reference/reference-hook.js +265 -27
  161. package/dist-server/service/reference/reference-hook.js.map +1 -1
  162. package/dist-server/service/reference/reference-link-groups.d.ts +10 -0
  163. package/dist-server/service/reference/reference-link-groups.js +15 -0
  164. package/dist-server/service/reference/reference-link-groups.js.map +1 -0
  165. package/dist-server/service/reference/reference-links.d.ts +14 -0
  166. package/dist-server/service/reference/reference-links.js +73 -0
  167. package/dist-server/service/reference/reference-links.js.map +1 -0
  168. package/dist-server/service/reference/reference-live.d.ts +1 -0
  169. package/dist-server/service/reference/reference-live.js +32 -6
  170. package/dist-server/service/reference/reference-live.js.map +1 -1
  171. package/dist-server/service/reference/reference-master.d.ts +56 -2
  172. package/dist-server/service/reference/reference-master.js +33 -3
  173. package/dist-server/service/reference/reference-master.js.map +1 -1
  174. package/dist-server/service/reference/reference-resolver.d.ts +27 -0
  175. package/dist-server/service/reference/reference-resolver.js +79 -1
  176. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  177. package/dist-server/service/twin-event/twin-event-keys.d.ts +31 -3
  178. package/dist-server/service/twin-event/twin-event-keys.js +36 -4
  179. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  180. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  181. package/dist-server/service/twin-event/twin-event.js +11 -2
  182. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  183. package/dist-server/service/twin-model/choose-recipe.d.ts +15 -0
  184. package/dist-server/service/twin-model/choose-recipe.js +33 -0
  185. package/dist-server/service/twin-model/choose-recipe.js.map +1 -0
  186. package/dist-server/service/twin-model/external-resolver.d.ts +76 -0
  187. package/dist-server/service/twin-model/external-resolver.js +109 -0
  188. package/dist-server/service/twin-model/external-resolver.js.map +1 -0
  189. package/dist-server/service/twin-model/project-structure.js +16 -3
  190. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  191. package/dist-server/service/twin-model/twin-equipment.d.ts +17 -0
  192. package/dist-server/service/twin-model/twin-equipment.js +10 -0
  193. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  194. package/dist-server/service/twin-model/twin-lineage-query.js +75 -1
  195. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  196. package/dist-server/service/twin-model/twin-model-item-query.js +94 -5
  197. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  198. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  199. package/dist-server/service/twin-space/twin-space.d.ts +29 -0
  200. package/dist-server/service/twin-space/twin-space.js +13 -0
  201. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  202. package/dist-server/service/twin-subject/event-subjects.d.ts +3 -1
  203. package/dist-server/service/twin-subject/event-subjects.js +8 -1
  204. package/dist-server/service/twin-subject/event-subjects.js.map +1 -1
  205. package/dist-shared/entity-delta.js +1 -1
  206. package/dist-shared/entity-delta.js.map +1 -1
  207. package/package.json +7 -7
  208. package/server/engine/canonical-ingest.ts +119 -33
  209. package/server/engine/event-time.ts +96 -0
  210. package/server/engine/index.ts +20 -0
  211. package/server/engine/ingest-dedupe.ts +203 -27
  212. package/server/engine/ingest-health.ts +91 -6
  213. package/server/engine/kpi-fold.ts +24 -1
  214. package/server/engine/kpi-query.ts +23 -4
  215. package/server/engine/oee-accumulator.ts +80 -8
  216. package/server/engine/twin-engine.ts +119 -5
  217. package/server/index.ts +23 -0
  218. package/server/migrations/1786600000000-RenameOperatoMesAdapterType.ts +66 -0
  219. package/server/migrations/1786700000000-AddDispatchRetryColumns.ts +82 -0
  220. package/server/migrations/index.ts +5 -1
  221. package/server/routes.ts +19 -20
  222. package/server/service/actuation/actuation-advisor.ts +124 -0
  223. package/server/service/actuation/actuation-outcome.ts +97 -0
  224. package/server/service/actuation/actuation-rule-resolver.ts +147 -0
  225. package/server/service/actuation/actuation-rule.ts +139 -0
  226. package/server/service/actuation/approval-gateway.ts +44 -0
  227. package/server/service/actuation/catch-up-effects.ts +152 -0
  228. package/server/service/actuation/command-dispatcher.ts +511 -0
  229. package/server/service/actuation/command-store.ts +236 -0
  230. package/server/service/actuation/dispatch-after-approval.ts +71 -0
  231. package/server/service/actuation/effect-match.ts +124 -0
  232. package/server/service/actuation/index.ts +27 -0
  233. package/server/service/actuation/retry-plan.ts +145 -0
  234. package/server/service/actuation/rule-evaluate.ts +352 -0
  235. package/server/service/actuation/rule-loop.ts +160 -0
  236. package/server/service/actuation/rule-runner.ts +216 -0
  237. package/server/service/actuation/rule-wiring.ts +84 -0
  238. package/server/service/actuation/settle-effects.ts +115 -0
  239. package/server/service/actuation/sweep-dispatch.ts +179 -0
  240. package/server/service/actuation/twin-command-resolver.ts +387 -0
  241. package/server/service/actuation/twin-command.ts +232 -0
  242. package/server/service/actuation/twin-now.ts +97 -0
  243. package/server/service/index.ts +11 -0
  244. package/server/service/reference/actuation-routing.ts +165 -0
  245. package/server/service/reference/actuation-target.ts +163 -0
  246. package/server/service/reference/connection-portability-resolver.ts +171 -0
  247. package/server/service/reference/connection-portability.ts +285 -0
  248. package/server/service/reference/fill-cadence.ts +29 -0
  249. package/server/service/reference/fill-dedupe.ts +66 -0
  250. package/server/service/reference/fill-loop.ts +153 -0
  251. package/server/service/reference/fill-span.ts +31 -0
  252. package/server/service/reference/hook-contract.ts +280 -22
  253. package/server/service/reference/hook-rejected.ts +53 -0
  254. package/server/service/reference/hook-store.ts +28 -0
  255. package/server/service/reference/index.ts +5 -1
  256. package/server/service/reference/live-cadence.ts +129 -0
  257. package/server/service/reference/live-feed-lease.ts +56 -0
  258. package/server/service/reference/live-ingest.ts +94 -0
  259. package/server/service/reference/reference-adapter.ts +392 -5
  260. package/server/service/reference/reference-fill-run.ts +165 -0
  261. package/server/service/reference/reference-fill.ts +223 -0
  262. package/server/service/reference/reference-hook.ts +355 -32
  263. package/server/service/reference/reference-link-groups.ts +21 -0
  264. package/server/service/reference/reference-links.ts +77 -0
  265. package/server/service/reference/reference-live.ts +30 -5
  266. package/server/service/reference/reference-master.ts +90 -5
  267. package/server/service/reference/reference-resolver.ts +77 -2
  268. package/server/service/twin-event/twin-event-keys.ts +37 -4
  269. package/server/service/twin-event/twin-event.ts +18 -1
  270. package/server/service/twin-model/choose-recipe.ts +31 -0
  271. package/server/service/twin-model/external-resolver.ts +161 -0
  272. package/server/service/twin-model/project-structure.ts +16 -3
  273. package/server/service/twin-model/twin-equipment.ts +22 -0
  274. package/server/service/twin-model/twin-lineage-query.ts +82 -1
  275. package/server/service/twin-model/twin-model-item-query.ts +95 -4
  276. package/server/service/twin-model/twin-model-tree-query.ts +1 -1
  277. package/server/service/twin-space/twin-space.ts +39 -0
  278. package/server/service/twin-subject/event-subjects.ts +8 -1
  279. package/shared/entity-delta.ts +1 -1
  280. package/test/ack-shape-alignment.test.ts +97 -0
  281. package/test/actuation-approval-door.test.ts +421 -0
  282. package/test/actuation-dispatch.test.ts +209 -0
  283. package/test/actuation-effect.test.ts +162 -0
  284. package/test/actuation-outcome.test.ts +152 -0
  285. package/test/actuation-rule-runner.test.ts +281 -0
  286. package/test/actuation-rule.test.ts +266 -0
  287. package/test/actuation-seam.test.ts +249 -0
  288. package/test/actuation-settle.test.ts +164 -0
  289. package/test/actuation-target.test.ts +145 -0
  290. package/test/canonical-ingest-vocabularies.test.ts +49 -2
  291. package/test/capability-mapping.test.ts +1 -1
  292. package/test/choose-recipe.test.ts +117 -0
  293. package/test/command-store-writes-columns.test.ts +126 -0
  294. package/test/connection-portability-doors.test.ts +87 -0
  295. package/test/connector-capability-declaration.test.ts +54 -4
  296. package/test/declaration-reaches-model.test.ts +43 -0
  297. package/test/dedupe-transformation.test.ts +112 -0
  298. package/test/entity-delta.test.ts +1 -1
  299. package/test/event-subjects.test.ts +2 -1
  300. package/test/external-resolver.test.ts +171 -0
  301. package/test/fill-dedupe.test.ts +59 -0
  302. package/test/fill-loop.test.ts +35 -0
  303. package/test/first-envelope.test.ts +153 -0
  304. package/test/fold-shift-dst.test.ts +110 -0
  305. package/test/home-in-area.test.ts +90 -0
  306. package/test/hook-rejected-shape.test.ts +86 -0
  307. package/test/hook-response-sum.test.ts +193 -0
  308. package/test/hook-sequence.test.ts +333 -0
  309. package/test/ingest-dedupe.test.ts +56 -0
  310. package/test/ingest-expected-quiet.test.ts +111 -0
  311. package/test/ingest-idempotent.test.ts +79 -0
  312. package/test/ingest-reconcile-four-ways.test.ts +97 -0
  313. package/test/ingest-rules-header.test.ts +100 -0
  314. package/test/ingest-window-rolls-unobserved.test.ts +72 -0
  315. package/test/intake-mapping-parity.test.ts +175 -0
  316. package/test/live-cadence.test.ts +170 -0
  317. package/test/live-feed-lease.test.ts +63 -0
  318. package/test/live-mirror-parity.test.ts +1 -1
  319. package/test/oee-accumulator.test.ts +3 -3
  320. package/test/oee-site-calendar.test.ts +146 -0
  321. package/test/order-and-transaction-split.test.ts +107 -0
  322. package/test/reference-fill.test.ts +235 -0
  323. package/test/reference-hook.test.ts +114 -23
  324. package/test/reference-links.test.ts +56 -0
  325. package/test/retry-plan.test.ts +139 -0
  326. package/test/sweep-dispatch.test.ts +191 -0
  327. package/test/twin-event-keys.test.ts +20 -6
  328. package/test/twin-model-item-db.test.ts +6 -6
  329. package/test/twin-model-tree-db.test.ts +6 -6
  330. package/tsconfig.shared.tsbuildinfo +1 -1
  331. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,165 @@
1
+ /*
2
+ * **빠진 구간을 다시 받는 일을 실제로 부른다** — `fillSince` 에 연결과 어댑터를 물려 주는 자리.
3
+ *
4
+ * ── 왜 이 파일이 없었나 ─────────────────────────────────────────────────────
5
+ * `fillSince` 는 2026-08-31 에 만들어졌고 **부르는 곳이 0곳이었습니다.** 2026-09-05 에 저는 그것을
6
+ * 못 보고 같은 일을 하는 것을 하나 더 만들었습니다(`backfillScope`). 두 벌이 되었고 둘 다 아무도
7
+ * 부르지 않았습니다.
8
+ *
9
+ * 그래서 이런 상태였습니다.
10
+ *
11
+ * ```
12
+ * 번호가 비면 어디서부터 다시 받을지 안다 ✔ takeInSequence 가 판정한다
13
+ * 그 빈 것을 메운다 ✖ 부를 곳이 없다
14
+ * ```
15
+ *
16
+ * 실제로 plant 아웃박스의 1~20 을 잃고 되찾을 방법이 없는 상태를 만났습니다.
17
+ *
18
+ * ── 같은 검사를 지난다 ──────────────────────────────────────────────────────
19
+ * `fillSince` 가 `takeInSequence` 를 지나고, 유입은 `liveIngest` 를 지납니다 — 밀어 받는 길과 같은
20
+ * 함수입니다. 여기서 따로 만들면 두 길이 다른 규칙으로 받고, 한쪽만 수정되는 날이 옵니다.
21
+ *
22
+ * ── 커서 열쇠는 프레임워크가 만든다 ─────────────────────────────────────────
23
+ * 열쇠는 `instanceId::scope` 이고 그것을 아는 것은 프레임워크입니다. 어댑터가 만들면 두 곳이 같은
24
+ * 규칙을 갖게 되고, 어긋나는 날 **다시 받기가 자기 커서를 0부터 새로 셉니다.**
25
+ */
26
+ import { getRepository } from '@things-factory/shell'
27
+
28
+ import { TwinInstance } from '../twin-instance/twin-instance.js'
29
+ import { TwinReference } from './twin-reference.js'
30
+ import { getAdapter, type SeqReport, type SiteDescriptor } from './reference-adapter.js'
31
+ import { pushCursorKey, withSeq } from './hook-contract.js'
32
+ import { fillSince, type FillOutcome } from './reference-fill.js'
33
+ import { liveIngest } from './live-ingest.js'
34
+ import { journalIdentities, spanOf } from './fill-dedupe.js'
35
+ import { TwinEngine } from '../../engine/index.js'
36
+ import { twinLog, twinWarn } from '../../engine/log.js'
37
+
38
+ /** 한 연결의 한 단위를 다시 받은 결과. */
39
+ export interface FillRunResult extends Partial<FillOutcome> {
40
+ instanceId: string
41
+ scope: string
42
+ /**
43
+ * 못 한 이유 — 성공이면 없습니다.
44
+ *
45
+ * `not-supported` 는 결함이 아닙니다. 주기로 물어보는 원본(chef · ppms)은 다음 주기가 곧 다시
46
+ * 받는 것입니다.
47
+ */
48
+ skipped?: 'unknown-twin' | 'no-source' | 'no-adapter' | 'not-supported'
49
+ }
50
+
51
+ /**
52
+ * 그 연결의 한 단위를 다시 받는다.
53
+ *
54
+ * **커서를 되돌리지 않습니다.** 받은 것이 번호 검사를 지나야 커서가 나아가고, 그 판정은 밀어 받는
55
+ * 길과 같은 함수가 합니다.
56
+ *
57
+ * @param from 여기서부터 다시 받는다 — 주면 커서를 무시합니다. 잃은 구간을 되찾을 때만 씁니다.
58
+ */
59
+ export async function runFill(args: {
60
+ domainId: string
61
+ instanceId: string
62
+ scope: string
63
+ from?: number
64
+ maxPages?: number
65
+ }): Promise<FillRunResult> {
66
+ const { domainId, instanceId, scope, from, maxPages } = args
67
+ const base: FillRunResult = { instanceId, scope }
68
+
69
+ const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })
70
+ if (!inst) return { ...base, skipped: 'unknown-twin' }
71
+
72
+ const repo = getRepository(TwinReference)
73
+ const ref = await repo.findOne({ where: { domain: { id: domainId }, source: instanceId } })
74
+ if (!ref) return { ...base, skipped: 'no-source' }
75
+
76
+ const adapter = getAdapter(ref.adapterType)
77
+ if (!adapter) return { ...base, skipped: 'no-adapter' }
78
+ /* 다시 받기를 안 내는 원본이 있습니다 — 주기로 물어보는 쪽은 다음 주기가 곧 그것입니다. */
79
+ if (typeof adapter.fetchSince !== 'function') return { ...base, skipped: 'not-supported' }
80
+
81
+ const siteId = (inst.origin as any)?.siteId
82
+ const site: SiteDescriptor = siteId ? { siteId } : { siteId: instanceId }
83
+ const cursorKey = pushCursorKey(instanceId, scope)
84
+
85
+ /*
86
+ * `from` 을 주면 커서를 그 값으로 갈아 끼웁니다 — **저장된 커서를 건드리지 않습니다.** `fillSince`
87
+ * 가 나아간 만큼만 적으므로, 잃은 구간을 되찾은 뒤 커서는 원래 자리보다 앞서지 않습니다.
88
+ */
89
+ const cursor = from === undefined ? ((ref as any).liveCursor ?? {}) : withSeq((ref as any).liveCursor, cursorKey, from)
90
+
91
+ const outcome = await fillSince({
92
+ cfg: ref.connectionConfig ?? {},
93
+ site,
94
+ scope,
95
+ cursorKey,
96
+ cursor,
97
+ fetch: (cfg, s, sc, since) => adapter.fetchSince!(cfg, s as SiteDescriptor, sc, since),
98
+ ingest: records => liveIngest(domainId, instanceId, records, 'fill'),
99
+ /*
100
+ * ── 넣기 전에 기억을 채운다 (2026-09-06) ──────────────────────────────────
101
+ * 중복 판정 기억은 프로세스 안에만 있다. 재기동하면 비워지고, 그러면 backfill 이 **이미 저널에
102
+ * 있는 사실도 다시 앉힌다.** 실측으로 39건이 통째로 다시 앉았다(저널 44 → 83).
103
+ *
104
+ * push 경로에서는 안 한다 — 거기서는 재전송이 창 안에 들어와 이미 걸리고, 봉투마다 저널을
105
+ * 조회하면 뜨거운 길이 느려진다.
106
+ */
107
+ beforeIngest: async records => {
108
+ const span = spanOf(records)
109
+ if (!span) return
110
+ const { identities, scanned, truncated } = await journalIdentities(domainId, instanceId, span)
111
+ const added = TwinEngine.warmDeduper(domainId, instanceId, identities)
112
+ if (truncated) {
113
+ twinWarn(
114
+ `[twin-fill] "${instanceId}" 의 저널을 ${scanned}줄까지만 읽었습니다 — 그보다 앞의 사실은 ` +
115
+ '다시 앉을 수 있습니다. 구간을 좁혀 부르거나 정체 색인이 필요합니다'
116
+ )
117
+ }
118
+ if (added) twinLog(`[twin-fill] "${instanceId}" — 저널에서 정체 ${added}개를 기억에 채웠습니다`)
119
+ },
120
+ saveCursor: async next => {
121
+ await repo.update({ id: (ref as any).id }, { liveCursor: next } as any)
122
+ },
123
+ ...(maxPages ? { maxPages } : {})
124
+ })
125
+
126
+ const say = `[twin-fill] "${instanceId}" · ${scope} — ${outcome.applied}건 넣음 · ${outcome.pages}쪽 · ${outcome.stopped}${outcome.detail ? ` (${outcome.detail})` : ''}`
127
+ /* 끝난 것과 막힌 것을 다른 소리로 남깁니다 — 구멍은 사람이 원본을 봐야 합니다. */
128
+ if (outcome.stopped === 'done' || outcome.stopped === 'no-progress') twinLog(say)
129
+ else twinWarn(say)
130
+
131
+ return { ...base, ...outcome }
132
+ }
133
+
134
+ /**
135
+ * **그 번호들이 무엇이었나** — 구멍을 만났을 때 사람이 묻는다.
136
+ *
137
+ * 커넥터는 「418 다음이 420」까지만 말한다. 그 419 가 무엇이었는지 알려면 원본에 물어야 하고,
138
+ * 그 길이 없어서 사람이 아웃박스를 손으로 뒤졌다(2026-09-06).
139
+ *
140
+ * **판정하지 않는다.** 원본이 아는 것을 그대로 낸다 — 「그러니 포기해도 된다」는 되돌릴 수 없는
141
+ * 판단이라 사람의 몫이다.
142
+ */
143
+ export async function seqReportOf(args: {
144
+ domainId: string
145
+ instanceId: string
146
+ scope: string
147
+ from: number
148
+ to: number
149
+ }): Promise<{ instanceId: string; scope: string; report?: SeqReport; skipped?: 'unknown-twin' | 'no-source' | 'no-adapter' | 'not-supported' }> {
150
+ const { domainId, instanceId, scope, from, to } = args
151
+ const base = { instanceId, scope }
152
+
153
+ const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })
154
+ if (!inst) return { ...base, skipped: 'unknown-twin' }
155
+ const ref = await getRepository(TwinReference).findOne({ where: { domain: { id: domainId }, source: instanceId } })
156
+ if (!ref) return { ...base, skipped: 'no-source' }
157
+ const adapter = getAdapter(ref.adapterType)
158
+ if (!adapter) return { ...base, skipped: 'no-adapter' }
159
+ /* 번호를 매기지 않는 원본은 물을 것이 없다 — 결함이 아니다. */
160
+ if (typeof adapter.seqReport !== 'function') return { ...base, skipped: 'not-supported' }
161
+
162
+ const siteId = (inst.origin as any)?.siteId
163
+ const site: SiteDescriptor | undefined = siteId ? { siteId } : undefined
164
+ return { ...base, report: await adapter.seqReport(ref.connectionConfig ?? {}, site, scope, from, to) }
165
+ }
@@ -0,0 +1,223 @@
1
+ /*
2
+ * **놓친 구간을 backfill 해 채운다** — 밀어 받는 길의 짝.
3
+ *
4
+ * ── 왜 필요한가 (2026-08-31, 인티그레이션 레인 지적) ──────────────────────
5
+ * 훅은 반드시 놓친다 — 우리가 내려가 있을 때, 그쪽이 못 보냈을 때, 네트워크가 끊겼을 때. 받는 쪽이
6
+ * 번호로 그것을 **알아채게** 되었지만(§`takeInSequence`), 알아챈 뒤 **그 사이를 채우는 길이 없었다.**
7
+ * 설계가 그 길을 적어 두었는데 만들지 않았다.
8
+ *
9
+ * ── 같은 검사를 지난다 ───────────────────────────────────────────────────
10
+ * backfill 한 것도 `takeInSequence` 를 지난다. 여기서 따로 만들면 두 길이 다른 규칙으로 받고, 한쪽만
11
+ * 고쳐지는 날 어긋난 그 길에서만 사실이 사라진다.
12
+ *
13
+ * ── 멈추는 조건을 셋 둔다 ────────────────────────────────────────────────
14
+ * 되풀이해서 요청하는 경로는 **끝나지 않는 것**이 가장 흔한 결함이다.
15
+ *
16
+ * 빈 답 더 줄 것이 없다. 끝
17
+ * 번호가 안 나아간다 같은 것을 다시 받았다 — 무한히 요청하는 것을 막는다
18
+ * 구멍 원본의 아웃박스에 구멍이 있다. 이어 받으면 커서가 그 번호를 지나쳐 적힌다
19
+ *
20
+ * 그리고 **쪽 상한**을 둔다. 상한에 닿았으면 조용히 끝내지 않고 말한다 — 조용히 자르면 사람은 다 받은
21
+ * 줄로 읽는다.
22
+ */
23
+ import { seqOf, takeInSequence, withSeq } from './hook-contract.js'
24
+ import type { InboundBatch } from './reference-adapter.js'
25
+ import { twinWarn } from '../../engine/log.js'
26
+
27
+ /** 한 번 채우고 나서 남는 사실. */
28
+ export interface FillOutcome {
29
+ /** 트윈에 넣은 레코드 수. */
30
+ applied: number
31
+ /**
32
+ * ingest 가 낸 네 갈래의 합 — **두 갈래로는 셈이 안 닫힌다.**
33
+ *
34
+ * ```
35
+ * 두 갈래 157 − 46 − 2 = 109 「설명 못 함」 진실은 되풀이 108 + 어휘 1
36
+ * 네 갈래 157 − 46 − 2 − 108 − 1 = 0
37
+ * ```
38
+ *
39
+ * 이 값이 없으면 원본 쪽에서 「보낸 것 20개 중 event 가 9개인데 나머지는 어디 갔나」를 셀 수
40
+ * 없다. 실제로 인티그레이션 레인이 그것을 물었고, 그 수가 트윈에 저장되지 않아 답을 못 했다.
41
+ */
42
+ duplicates: number
43
+ rejected: number
44
+ /**
45
+ * 왜 거절됐나 — **사유별 수.** 수만 내면 「26건이 안 들어온다」에서 멈춘다.
46
+ *
47
+ * 목록 전체가 아니라 사유별로 센다. 같은 이유로 거절된 26건이 26줄이면 읽는 사람이 그 줄을
48
+ * 세어야 하고, 다른 이유 하나가 그 안에 묻힌다.
49
+ */
50
+ rejectedReasons?: Record<string, number>
51
+ vocabulary: number
52
+ /** 시각을 못 읽어 ingest 시각으로 떨어진 것 — 종류별. 비어 있으면 전부 자기 시각을 말했다. */
53
+ undated?: Record<string, number>
54
+ /** 몇 쪽을 받았나. */
55
+ pages: number
56
+ /** 커서가 어디까지 갔나 — 아무것도 못 받았으면 없다. */
57
+ lastSeq?: number
58
+ /**
59
+ * 이 실행에서 **가장 낮게 받아들인 번호** — 밑에서부터 다시 받았을 때 이것이 곧 「우리가 가진
60
+ * 가장 낮은 번호」다.
61
+ */
62
+ lowestSeq?: number
63
+ /** 왜 멈췄나 — `done` · `gap` · `no-progress` · `page-limit` · `refused`. */
64
+ stopped: 'done' | 'gap' | 'no-progress' | 'page-limit' | 'refused'
65
+ /** 멈춘 이유의 사람 말. 알리지 않고 끝내지 않는다. */
66
+ detail?: string
67
+ /**
68
+ * 마지막으로 받은 묶음이 말한 **원본의 번호 범위** — 원본이 안 말하면 없다.
69
+ *
70
+ * 우리 커서와 견주는 자리가 이 값을 쓴다. 없으면 「맞다」가 아니라 **「모른다」**로 답해야 한다.
71
+ */
72
+ sourceRange?: { first?: number; last?: number }
73
+ }
74
+
75
+ /** 몇 쪽까지 받나 — 한 번의 채우기가 서버를 오래 붙잡지 않게. */
76
+ const MAX_PAGES = 50
77
+
78
+ /**
79
+ * 그 단위의 놓친 구간을 채운다.
80
+ *
81
+ * `ingest` 를 부르는 쪽이 넘겨 준다 — 훅과 같은 이유다(이 파일이 엔진을 직접 부르면 시험이 엔진을
82
+ * 세워야 한다).
83
+ */
84
+ export async function fillSince(args: {
85
+ cfg: Record<string, any>
86
+ site: { siteId: string }
87
+ /** 번호를 매기는 단위 — 커서 열쇠의 절반. 커넥터가 만들지 않는다. */
88
+ scope: string
89
+ /** 커서에 적힌 열쇠 — `pushCursorKey(instanceId, scope)`. */
90
+ cursorKey: string
91
+ /** 지금 커서 — 이 함수가 읽고, 나아간 만큼 되돌려 준다. */
92
+ cursor: unknown
93
+ fetch: (cfg: Record<string, any>, site: { siteId: string }, scope: string, since: number) => Promise<InboundBatch>
94
+ ingest: (records: unknown[]) => { applied: number; duplicates: number; rejected: unknown[] }
95
+ /**
96
+ * 넣기 **직전에** 부른다 — 이 쪽의 레코드를 넘긴다.
97
+ *
98
+ * 중복 판정 기억을 저널에서 채우는 자리다(§`fill-dedupe.ts`). 그 기억은 프로세스 안에만 있어서
99
+ * 재기동하면 비워지고, 그러면 이미 저널에 있는 사실도 「처음 본 것」이 된다.
100
+ *
101
+ * `ingest` 는 동기라 여기서 채울 수 없다. 이 훅은 비동기이므로 저널을 읽을 수 있다.
102
+ *
103
+ * 실패해도 넣기를 멈추지 않는다 — 기억을 못 채운 것이 사실을 못 받을 이유는 아니다.
104
+ */
105
+ beforeIngest?: (records: unknown[]) => Promise<void>
106
+ saveCursor: (cursor: Record<string, unknown>) => Promise<void>
107
+ maxPages?: number
108
+ }): Promise<FillOutcome> {
109
+ const { cfg, site, scope, cursorKey, fetch, ingest, saveCursor , beforeIngest } = args
110
+ const maxPages = args.maxPages ?? MAX_PAGES
111
+
112
+ let cursor = args.cursor
113
+ let applied = 0
114
+ let duplicates = 0
115
+ let rejectedCount = 0
116
+ const rejectedReasons: Record<string, number> = {}
117
+ let vocabulary = 0
118
+ const undated: Record<string, number> = {}
119
+ /* 원본이 말한 번호 범위 — 마지막으로 받은 묶음의 것을 들고 나간다. 안 말하면 끝까지 없다. */
120
+ let sourceRange: { first?: number; last?: number } | undefined
121
+ /* 이 실행에서 가장 낮게 받아들인 번호 — 커서에 firstSeq 로 적는다. */
122
+ let lowestSeq: number | undefined
123
+ let pages = 0
124
+
125
+ for (; pages < maxPages; pages++) {
126
+ const since = seqOf(cursor, cursorKey) ?? 0
127
+
128
+ let batch: InboundBatch
129
+ try {
130
+ batch = await fetch(cfg, site, scope, since)
131
+ } catch (e: any) {
132
+ return { applied, duplicates, rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}), vocabulary, ...(Object.keys(undated).length ? { undated } : {}), pages, ...seqPart(cursor, cursorKey), ...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}), stopped: 'refused', detail: String(e?.message ?? e) }
133
+ }
134
+
135
+ if (batch?.sourceRange) sourceRange = batch.sourceRange
136
+
137
+ const items = batch?.items ?? []
138
+ /* 빈 답은 더 줄 것이 없다는 뜻이다. 오류가 아니다. */
139
+ if (!items.length) return { applied, duplicates, rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}), vocabulary, ...(Object.keys(undated).length ? { undated } : {}), pages, ...seqPart(cursor, cursorKey), ...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}), stopped: 'done' }
140
+
141
+ const taken = takeInSequence({ items, scope, lastSeq: seqOf(cursor, cursorKey) })
142
+ if (taken.refused) {
143
+ return { applied, duplicates, rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}), vocabulary, ...(Object.keys(undated).length ? { undated } : {}), pages, ...seqPart(cursor, cursorKey), ...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}), stopped: 'refused', detail: taken.refused }
144
+ }
145
+
146
+ if (taken.records.length) {
147
+ if (beforeIngest) {
148
+ await beforeIngest(taken.records).catch(e =>
149
+ twinWarn(`[twin-fill] 중복 판정 기억을 못 채웠다 — ${String(e?.message ?? e)}. 넣기는 그대로 간다`)
150
+ )
151
+ }
152
+ const result = ingest(taken.records)
153
+ applied += result.applied
154
+ duplicates += result.duplicates ?? 0
155
+ rejectedCount += result.rejected?.length ?? 0
156
+ for (const r of result.rejected ?? []) {
157
+ /* 사유가 여럿이면 첫 줄로 묶는다 — 뒤는 대개 같은 원인의 곁가지다. */
158
+ const why = String((r as any)?.errors?.[0] ?? '(사유 없음)').slice(0, 120)
159
+ rejectedReasons[why] = (rejectedReasons[why] ?? 0) + 1
160
+ }
161
+ vocabulary += (result as any).vocabulary ?? 0
162
+ for (const [k, n] of Object.entries((result as any).undated ?? {})) undated[k] = (undated[k] ?? 0) + (n as number)
163
+ }
164
+
165
+ /*
166
+ * **번호가 나아가야 다음 쪽을 요청한다.** 원본이 빈 묶음에 물어본 번호를 그대로 돌려주는 계약이면,
167
+ * 나아감을 보지 않고 요청하면 무한히 돈다.
168
+ */
169
+ if (taken.advancedSeq === undefined || taken.advancedSeq <= since) {
170
+ if (taken.gap) return { applied, duplicates, rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}), vocabulary, ...(Object.keys(undated).length ? { undated } : {}), pages: pages + 1, ...seqPart(cursor, cursorKey), ...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}), stopped: 'gap', detail: gapWord(taken.gap) }
171
+ return {
172
+ applied,
173
+ duplicates,
174
+ rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}),
175
+ vocabulary,
176
+ ...(Object.keys(undated).length ? { undated } : {}),
177
+ pages: pages + 1,
178
+ ...seqPart(cursor, cursorKey),
179
+ ...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}),
180
+ stopped: 'no-progress',
181
+ detail: `번호가 ${since} 에서 나아가지 않았다 — 같은 것을 다시 받았다`
182
+ }
183
+ }
184
+
185
+ if (lowestSeq === undefined) lowestSeq = taken.lowestSeq
186
+ /* 밑에서부터 다시 받았으면 그 번호가 곧 우리가 가진 가장 낮은 번호다. */
187
+ cursor = withSeq(cursor, cursorKey, taken.advancedSeq, taken.firstSeq ?? lowestSeq)
188
+ await saveCursor(cursor as Record<string, unknown>).catch(err =>
189
+ twinWarn(`[twin-fill] 번호를 커서에 적지 못했다 (${cursorKey}=${taken.advancedSeq}) — ${err?.message ?? err}`)
190
+ )
191
+
192
+ /*
193
+ * **구멍이 있으면 멈춘다.** 원본의 아웃박스에 구멍이 있는 것이고, 이어 받으면 커서가 그 번호를
194
+ * 지나쳐 적혀 빠진 사실이 다시 오지 않는다.
195
+ */
196
+ if (taken.gap) {
197
+ return { applied, duplicates, rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}), vocabulary, ...(Object.keys(undated).length ? { undated } : {}), pages: pages + 1, ...seqPart(cursor, cursorKey), ...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}), stopped: 'gap', detail: gapWord(taken.gap) }
198
+ }
199
+ }
200
+
201
+ /* 상한에 닿았다 — 조용히 끝내지 않는다. 사람이 다 받은 줄로 읽으면 안 된다. */
202
+ return {
203
+ applied,
204
+ duplicates,
205
+ rejected: rejectedCount, ...(Object.keys(rejectedReasons).length ? { rejectedReasons } : {}),
206
+ vocabulary,
207
+ ...(Object.keys(undated).length ? { undated } : {}),
208
+ pages,
209
+ ...seqPart(cursor, cursorKey),
210
+ ...(sourceRange ? { sourceRange } : {}), ...(lowestSeq !== undefined ? { lowestSeq } : {}),
211
+ stopped: 'page-limit',
212
+ detail: `${maxPages}쪽까지 받고 멈췄다 — 남은 것이 있을 수 있다. 다음 주기가 이어 받는다`
213
+ }
214
+ }
215
+
216
+ function seqPart(cursor: unknown, key: string): { lastSeq?: number } {
217
+ const seq = seqOf(cursor, key)
218
+ return seq === undefined ? {} : { lastSeq: seq }
219
+ }
220
+
221
+ function gapWord(gap: { expectedSeq: number; lastSeq: number }): string {
222
+ return `번호가 비었다 — ${gap.lastSeq} 다음이 ${gap.expectedSeq} 이어야 한다. 원본의 아웃박스에 구멍이 있다`
223
+ }