@things-factory/headless-twin 10.0.19 → 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 (327) 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 +12 -20
  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 +206 -8
  58. package/dist-server/service/actuation/command-dispatcher.js +191 -14
  59. package/dist-server/service/actuation/command-dispatcher.js.map +1 -1
  60. package/dist-server/service/actuation/command-store.d.ts +10 -2
  61. package/dist-server/service/actuation/command-store.js +168 -5
  62. package/dist-server/service/actuation/command-store.js.map +1 -1
  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 +21 -2
  70. package/dist-server/service/actuation/index.js +21 -2
  71. package/dist-server/service/actuation/index.js.map +1 -1
  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 +81 -0
  97. package/dist-server/service/actuation/twin-command.js +60 -0
  98. package/dist-server/service/actuation/twin-command.js.map +1 -1
  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 +8 -0
  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 +88 -4
  130. package/dist-server/service/reference/hook-contract.js +105 -4
  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/index.d.ts +4 -1
  136. package/dist-server/service/reference/index.js +5 -1
  137. package/dist-server/service/reference/index.js.map +1 -1
  138. package/dist-server/service/reference/live-cadence.d.ts +68 -0
  139. package/dist-server/service/reference/live-cadence.js +44 -0
  140. package/dist-server/service/reference/live-cadence.js.map +1 -0
  141. package/dist-server/service/reference/live-feed-lease.d.ts +15 -0
  142. package/dist-server/service/reference/live-feed-lease.js +56 -0
  143. package/dist-server/service/reference/live-feed-lease.js.map +1 -0
  144. package/dist-server/service/reference/live-ingest.d.ts +21 -0
  145. package/dist-server/service/reference/live-ingest.js +66 -0
  146. package/dist-server/service/reference/live-ingest.js.map +1 -0
  147. package/dist-server/service/reference/reference-adapter.d.ts +291 -2
  148. package/dist-server/service/reference/reference-adapter.js +19 -1
  149. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  150. package/dist-server/service/reference/reference-fill-run.d.ts +50 -0
  151. package/dist-server/service/reference/reference-fill-run.js +142 -0
  152. package/dist-server/service/reference/reference-fill-run.js.map +1 -0
  153. package/dist-server/service/reference/reference-fill.d.ts +90 -0
  154. package/dist-server/service/reference/reference-fill.js +141 -0
  155. package/dist-server/service/reference/reference-fill.js.map +1 -0
  156. package/dist-server/service/reference/reference-hook.d.ts +22 -0
  157. package/dist-server/service/reference/reference-hook.js +135 -55
  158. package/dist-server/service/reference/reference-hook.js.map +1 -1
  159. package/dist-server/service/reference/reference-link-groups.d.ts +10 -0
  160. package/dist-server/service/reference/reference-link-groups.js +15 -0
  161. package/dist-server/service/reference/reference-link-groups.js.map +1 -0
  162. package/dist-server/service/reference/reference-links.d.ts +14 -0
  163. package/dist-server/service/reference/reference-links.js +73 -0
  164. package/dist-server/service/reference/reference-links.js.map +1 -0
  165. package/dist-server/service/reference/reference-live.d.ts +1 -0
  166. package/dist-server/service/reference/reference-live.js +32 -6
  167. package/dist-server/service/reference/reference-live.js.map +1 -1
  168. package/dist-server/service/reference/reference-master.d.ts +56 -2
  169. package/dist-server/service/reference/reference-master.js +33 -3
  170. package/dist-server/service/reference/reference-master.js.map +1 -1
  171. package/dist-server/service/reference/reference-resolver.d.ts +27 -0
  172. package/dist-server/service/reference/reference-resolver.js +79 -1
  173. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  174. package/dist-server/service/twin-event/twin-event-keys.d.ts +31 -3
  175. package/dist-server/service/twin-event/twin-event-keys.js +36 -4
  176. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  177. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  178. package/dist-server/service/twin-event/twin-event.js +11 -2
  179. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  180. package/dist-server/service/twin-model/choose-recipe.d.ts +15 -0
  181. package/dist-server/service/twin-model/choose-recipe.js +33 -0
  182. package/dist-server/service/twin-model/choose-recipe.js.map +1 -0
  183. package/dist-server/service/twin-model/external-resolver.d.ts +76 -0
  184. package/dist-server/service/twin-model/external-resolver.js +109 -0
  185. package/dist-server/service/twin-model/external-resolver.js.map +1 -0
  186. package/dist-server/service/twin-model/project-structure.js +16 -3
  187. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  188. package/dist-server/service/twin-model/twin-equipment.d.ts +17 -0
  189. package/dist-server/service/twin-model/twin-equipment.js +10 -0
  190. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  191. package/dist-server/service/twin-model/twin-lineage-query.js +75 -1
  192. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  193. package/dist-server/service/twin-model/twin-model-item-query.js +94 -5
  194. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  195. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  196. package/dist-server/service/twin-space/twin-space.d.ts +29 -0
  197. package/dist-server/service/twin-space/twin-space.js +13 -0
  198. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  199. package/dist-server/service/twin-subject/event-subjects.d.ts +3 -1
  200. package/dist-server/service/twin-subject/event-subjects.js +8 -1
  201. package/dist-server/service/twin-subject/event-subjects.js.map +1 -1
  202. package/dist-shared/entity-delta.js +1 -1
  203. package/dist-shared/entity-delta.js.map +1 -1
  204. package/package.json +7 -7
  205. package/server/engine/canonical-ingest.ts +119 -33
  206. package/server/engine/event-time.ts +96 -0
  207. package/server/engine/index.ts +20 -0
  208. package/server/engine/ingest-dedupe.ts +203 -27
  209. package/server/engine/ingest-health.ts +91 -6
  210. package/server/engine/kpi-fold.ts +24 -1
  211. package/server/engine/kpi-query.ts +23 -4
  212. package/server/engine/oee-accumulator.ts +80 -8
  213. package/server/engine/twin-engine.ts +119 -5
  214. package/server/index.ts +23 -0
  215. package/server/migrations/1786600000000-RenameOperatoMesAdapterType.ts +66 -0
  216. package/server/migrations/1786700000000-AddDispatchRetryColumns.ts +82 -0
  217. package/server/migrations/index.ts +5 -1
  218. package/server/routes.ts +12 -24
  219. package/server/service/actuation/actuation-advisor.ts +124 -0
  220. package/server/service/actuation/actuation-outcome.ts +97 -0
  221. package/server/service/actuation/actuation-rule-resolver.ts +147 -0
  222. package/server/service/actuation/actuation-rule.ts +139 -0
  223. package/server/service/actuation/approval-gateway.ts +44 -0
  224. package/server/service/actuation/catch-up-effects.ts +152 -0
  225. package/server/service/actuation/command-dispatcher.ts +401 -20
  226. package/server/service/actuation/command-store.ts +182 -8
  227. package/server/service/actuation/dispatch-after-approval.ts +71 -0
  228. package/server/service/actuation/effect-match.ts +124 -0
  229. package/server/service/actuation/index.ts +21 -2
  230. package/server/service/actuation/retry-plan.ts +145 -0
  231. package/server/service/actuation/rule-evaluate.ts +352 -0
  232. package/server/service/actuation/rule-loop.ts +160 -0
  233. package/server/service/actuation/rule-runner.ts +216 -0
  234. package/server/service/actuation/rule-wiring.ts +84 -0
  235. package/server/service/actuation/settle-effects.ts +115 -0
  236. package/server/service/actuation/sweep-dispatch.ts +179 -0
  237. package/server/service/actuation/twin-command-resolver.ts +387 -0
  238. package/server/service/actuation/twin-command.ts +128 -1
  239. package/server/service/actuation/twin-now.ts +97 -0
  240. package/server/service/index.ts +9 -1
  241. package/server/service/reference/actuation-routing.ts +165 -0
  242. package/server/service/reference/actuation-target.ts +163 -0
  243. package/server/service/reference/connection-portability-resolver.ts +171 -0
  244. package/server/service/reference/connection-portability.ts +285 -0
  245. package/server/service/reference/fill-cadence.ts +29 -0
  246. package/server/service/reference/fill-dedupe.ts +66 -0
  247. package/server/service/reference/fill-loop.ts +153 -0
  248. package/server/service/reference/fill-span.ts +31 -0
  249. package/server/service/reference/hook-contract.ts +185 -5
  250. package/server/service/reference/hook-rejected.ts +53 -0
  251. package/server/service/reference/index.ts +5 -1
  252. package/server/service/reference/live-cadence.ts +129 -0
  253. package/server/service/reference/live-feed-lease.ts +56 -0
  254. package/server/service/reference/live-ingest.ts +94 -0
  255. package/server/service/reference/reference-adapter.ts +327 -5
  256. package/server/service/reference/reference-fill-run.ts +165 -0
  257. package/server/service/reference/reference-fill.ts +223 -0
  258. package/server/service/reference/reference-hook.ts +162 -56
  259. package/server/service/reference/reference-link-groups.ts +21 -0
  260. package/server/service/reference/reference-links.ts +77 -0
  261. package/server/service/reference/reference-live.ts +30 -5
  262. package/server/service/reference/reference-master.ts +90 -5
  263. package/server/service/reference/reference-resolver.ts +77 -2
  264. package/server/service/twin-event/twin-event-keys.ts +37 -4
  265. package/server/service/twin-event/twin-event.ts +18 -1
  266. package/server/service/twin-model/choose-recipe.ts +31 -0
  267. package/server/service/twin-model/external-resolver.ts +161 -0
  268. package/server/service/twin-model/project-structure.ts +16 -3
  269. package/server/service/twin-model/twin-equipment.ts +22 -0
  270. package/server/service/twin-model/twin-lineage-query.ts +82 -1
  271. package/server/service/twin-model/twin-model-item-query.ts +95 -4
  272. package/server/service/twin-model/twin-model-tree-query.ts +1 -1
  273. package/server/service/twin-space/twin-space.ts +39 -0
  274. package/server/service/twin-subject/event-subjects.ts +8 -1
  275. package/shared/entity-delta.ts +1 -1
  276. package/test/ack-shape-alignment.test.ts +97 -0
  277. package/test/actuation-approval-door.test.ts +421 -0
  278. package/test/actuation-dispatch.test.ts +16 -3
  279. package/test/actuation-effect.test.ts +162 -0
  280. package/test/actuation-outcome.test.ts +152 -0
  281. package/test/actuation-rule-runner.test.ts +281 -0
  282. package/test/actuation-rule.test.ts +266 -0
  283. package/test/actuation-seam.test.ts +249 -0
  284. package/test/actuation-settle.test.ts +164 -0
  285. package/test/actuation-target.test.ts +145 -0
  286. package/test/canonical-ingest-vocabularies.test.ts +49 -2
  287. package/test/capability-mapping.test.ts +1 -1
  288. package/test/choose-recipe.test.ts +117 -0
  289. package/test/command-store-writes-columns.test.ts +126 -0
  290. package/test/connection-portability-doors.test.ts +87 -0
  291. package/test/connector-capability-declaration.test.ts +54 -4
  292. package/test/declaration-reaches-model.test.ts +43 -0
  293. package/test/dedupe-transformation.test.ts +112 -0
  294. package/test/entity-delta.test.ts +1 -1
  295. package/test/event-subjects.test.ts +2 -1
  296. package/test/external-resolver.test.ts +171 -0
  297. package/test/fill-dedupe.test.ts +59 -0
  298. package/test/fill-loop.test.ts +35 -0
  299. package/test/first-envelope.test.ts +153 -0
  300. package/test/fold-shift-dst.test.ts +110 -0
  301. package/test/home-in-area.test.ts +90 -0
  302. package/test/hook-rejected-shape.test.ts +86 -0
  303. package/test/hook-response-sum.test.ts +193 -0
  304. package/test/hook-sequence.test.ts +1 -1
  305. package/test/ingest-dedupe.test.ts +56 -0
  306. package/test/ingest-expected-quiet.test.ts +111 -0
  307. package/test/ingest-idempotent.test.ts +79 -0
  308. package/test/ingest-reconcile-four-ways.test.ts +97 -0
  309. package/test/ingest-rules-header.test.ts +100 -0
  310. package/test/ingest-window-rolls-unobserved.test.ts +72 -0
  311. package/test/intake-mapping-parity.test.ts +175 -0
  312. package/test/live-cadence.test.ts +170 -0
  313. package/test/live-feed-lease.test.ts +63 -0
  314. package/test/live-mirror-parity.test.ts +1 -1
  315. package/test/oee-accumulator.test.ts +3 -3
  316. package/test/oee-site-calendar.test.ts +146 -0
  317. package/test/order-and-transaction-split.test.ts +107 -0
  318. package/test/reference-fill.test.ts +235 -0
  319. package/test/reference-hook.test.ts +1 -1
  320. package/test/reference-links.test.ts +56 -0
  321. package/test/retry-plan.test.ts +139 -0
  322. package/test/sweep-dispatch.test.ts +191 -0
  323. package/test/twin-event-keys.test.ts +20 -6
  324. package/test/twin-model-item-db.test.ts +6 -6
  325. package/test/twin-model-tree-db.test.ts +6 -6
  326. package/tsconfig.shared.tsbuildinfo +1 -1
  327. package/tsconfig.tsbuildinfo +1 -1
@@ -2,6 +2,12 @@ import type { ApprovedCommand } from '@operato/ops-contract'
2
2
 
3
3
  import type { ReferenceStep } from './reference-progress.js'
4
4
  import type { ReferenceMaster } from './reference-master.js'
5
+ /*
6
+ * `fetchLinks` 가 돌려주는 관계 그룹. 정의는 이것을 소비하는 쪽에 하나만 둔다
7
+ * (`external-resolver.ts`). 여기서 같은 모양을 다시 선언하면 한쪽만 수정되는 날이 온다.
8
+ */
9
+ import type { ExternalIncoming as ReferenceLinkGroup } from '../twin-model/external-resolver.js'
10
+ export type { ReferenceLinkGroup }
5
11
  import { twinWarn } from '../../engine/log.js'
6
12
 
7
13
  /*
@@ -95,7 +101,37 @@ export interface AdapterMeta {
95
101
  * · `live` — 돌아가는 피드를 낸다(`openLiveFeed`).
96
102
  * · `control` — 구동을 받는다(`control`) — 자극·속도·정지·초기화.
97
103
  */
98
- export type ReferenceCapability = 'live' | 'control'
104
+ /**
105
+ * 이 원본에 무엇을 요구할 수 있나.
106
+ *
107
+ * · `live` — 관측을 이어서 낸다
108
+ * · `control` — 시뮬레이터를 움직인다(자극·배속·정지). 실 시스템은 갖지 않는다
109
+ * · `actuate` — **승인된 조치를 받는다.** 현장에 작업지시가 나가는 유일한 축이다
110
+ *
111
+ * ── `actuate` 를 늦게 넣은 이유 (2026-09-02) ────────────────────────────────
112
+ * 어댑터에 `actuate` 구현은 있었는데 이 축에 그 이름이 없었다. 그래서 셋이 동시에 조용했다.
113
+ *
114
+ * 화면 「이 원본이 지시를 받나」를 물을 수 없다 — 능력 목록에 그 이름이 없다
115
+ * 등록 구현했는데 선언 안 한 것을 알려 주지 않는다 — 검사 목록에 그 이름이 없다
116
+ * 넘김 리졸버가 어댑터를 찾지도 않고 무조건 거절했다 — 커넥터는 이미 만들어져 있었다
117
+ *
118
+ * 실제로 MES 커넥터가 `actuate` 를 다 만들어 두었는데, 넘기면 「구현이 없습니다」라고 답했다.
119
+ * 자리는 있고 길이 없는 상태였고, 이 저장소에서 가장 자주 나는 결함 부류다.
120
+ */
121
+ /**
122
+ * 커넥터가 선언하는 능력.
123
+ *
124
+ * `idempotent-actuation` 은 다른 셋과 성질이 다르다 — 무엇을 할 수 있나가 아니라 **두 번 해도
125
+ * 같은가**다. 일꾼이 실패한 조치를 다시 넘길지 정할 때 이것만 본다(§`retryDecisionOf`).
126
+ *
127
+ * 선언하지 않으면 **거짓으로 읽는다.** 이 한 자리에서만 「모르면 안전한 쪽」이 거짓이고, 그
128
+ * 안전한 쪽이 「재시도하지 않음」이다 — 참으로 가정하면 현장에 지시가 두 건 선다.
129
+ *
130
+ * 선언에는 근거가 있어야 한다. operato-plant 는 MES 가 `correlationId` 로 기존 행을 찾아 같은
131
+ * 이름을 돌려주기 때문에 참이다 — 우리가 조심해서가 아니라 저쪽 동작이 그래서다. 저쪽이 그것을
132
+ * 바꾸면 이 선언이 거짓이 된다.
133
+ */
134
+ export type ReferenceCapability = 'live' | 'control' | 'actuate' | 'idempotent-actuation'
99
135
 
100
136
  /** 원본에 보내는 구동 명령 — **시뮬레이터를 움직이는 말**이지 트윈을 고치는 말이 아니다. */
101
137
  export interface ControlCommand {
@@ -173,8 +209,30 @@ export interface ObservedItemFact {
173
209
  *
174
210
  * **`scope` 가 없으면 `seq` 도 없는 것으로 다룬다.** 번호만 있고 그 번호가 어느 줄의 것인지 모르면
175
211
  * 커서를 무엇으로 잡을지 짐작해야 하고, 짐작한 열쇠는 보내는 쪽이 줄을 하나 더 늘리는 날 어긋난다.
176
- * 실제로 operato-mes 가 `[domain, channel]` 마다 번호를 매기면서 봉투에 `channel` 을 싣지 않고 있었다.
212
+ * 실제로 operato-plant 가 `[domain, channel]` 마다 번호를 매기면서 봉투에 `channel` 을 싣지 않고 있었다.
177
213
  */
214
+ /** 번호 하나에 대해 원본이 아는 것. */
215
+ export interface SeqEntry {
216
+ seq: number
217
+ /**
218
+ * `unknown` 은 결함이 아니다 — 채번을 따로 들지 않는 원본은 「행이 없다」에서 잃은 것과 아직 안
219
+ * 간 것을 가를 수 없다. 그것을 `lost` 로 단정하면 사람이 없는 사실을 찾으러 간다.
220
+ */
221
+ state: 'present' | 'lost' | 'unissued' | 'unknown'
222
+ /** 그 봉투의 발생 시각 — `present` 일 때만. 무엇이었는지 사람이 알아보는 데 쓴다. */
223
+ at?: string
224
+ /** 그 봉투 자체 — `present` 이고 원본이 낼 수 있을 때만. 없어도 `state` 는 참이다. */
225
+ record?: unknown
226
+ }
227
+
228
+ /** 번호 구간에 대해 원본이 아는 것 — **원본이 아는 것만 낸다. 판정은 사람이 한다.** */
229
+ export interface SeqReport {
230
+ scope: string
231
+ entries: SeqEntry[]
232
+ /** 상한에 닿아 구간을 다 못 냈다 — 알리지 않고 자르지 않는다. */
233
+ truncated?: boolean
234
+ }
235
+
178
236
  export interface InboundBatch {
179
237
  /**
180
238
  * 옮긴 것들. 우리와 무관한 본문이면 빈 배열이다.
@@ -190,17 +248,66 @@ export interface InboundBatch {
190
248
  * 하나로 말할 수 없다.
191
249
  */
192
250
  scope?: string
251
+ /**
252
+ * (선택) **원본이 지금 가진 번호의 범위** — 우리 커서와 견주려면 이것이 있어야 한다.
253
+ *
254
+ * ── 왜 어댑터가 주나 (인티그레이션 레인 제안, 2026-09-06) ──────────────────
255
+ * 「가장 낮은 번호」를 묻는 방법이 원본마다 다르다. operato-plant 는
256
+ * `mesOutboxEvents(since: 0, limit: 1)` 이고, ppms·chef 는 아웃박스가 없어 번호 범위 자체가
257
+ * 없다. 프레임워크가 이것을 알 방법이 없다.
258
+ *
259
+ * ── 모르면 주지 않는다 ────────────────────────────────────────────────────
260
+ * **0 이나 추측을 넣지 않는다.** 0 을 넣으면 「원본이 1번부터 다 갖고 있다」로 읽히고, 그것이
261
+ * 거짓이면 「우리가 다 받았다」는 잘못된 결론이 나온다.
262
+ *
263
+ * `last` 에는 조건이 하나 붙는다. 쪽 상한에서 멈췄으면 그때의 번호는 **원본의 끝이 아니라
264
+ * 「여기까지 읽었다」**이므로 주지 않는다 — 주면 「원본에 더 없다」로 읽히는데 실제로는 더 있다.
265
+ */
266
+ sourceRange?: { first?: number; last?: number }
193
267
  }
194
268
 
195
269
  export interface InboundItem {
196
270
  /** 우리 어휘로 옮긴 레코드 하나. */
197
271
  record: unknown
272
+ /**
273
+ * 원본이 이 봉투에 붙인 id — **떨어졌을 때 그쪽이 자기 행을 찾는 열쇠다.**
274
+ *
275
+ * ── 왜 필요한가 (2026-09-07 측정) ──────────────────────────────────────────
276
+ * 훅 응답의 떨어진 목록이 `{ record, errors }` 였고, plant 은 그것을 **봉투 id 문자열 목록**으로
277
+ * 읽고 있었다. `Array.isArray` 는 통과하니 그쪽 guard 도 안 걸렸다 — id 를 맞추는데 객체라 하나도
278
+ * 안 맞고 **묶음 전체가 「보냈음」으로 찍혔다.** 422 와 `ok: false` 를 보냈는데도 그쪽 행은 재시도
279
+ * 0 · 오류 없음이었다.
280
+ *
281
+ * 이 값을 주면 응답이 `{ eventId, errors }` 로 나가고 그쪽이 그 행만 표시할 수 있다. 안 주면
282
+ * 레코드가 그대로 실려 나간다 — 무엇이 떨어졌는지 알 길이 그것뿐이고, 빈 id 를 실으면 보내는 쪽이
283
+ * 아무 행도 못 찾으면서 「알았다」고 여긴다.
284
+ */
285
+ eventId?: string
198
286
  /**
199
287
  * 이 봉투의 번호.
200
288
  *
201
289
  * **한 배치 안에서 있거나 없거나 하나로 통일한다.** 섞이면 빠진 것이 있는지 판단할 근거가 없다.
202
290
  */
203
291
  seq?: number
292
+ /**
293
+ * 이 봉투가 말하는 **발생 시각** — 레코드가 자기 시각을 안 말할 때 쓴다.
294
+ *
295
+ * ── 왜 필요한가 (2026-09-06 측정) ─────────────────────────────────────────
296
+ * 시각을 안 싣는 레코드는 `defaultEventTime`(= ingest 시각)으로 떨어진다. 그 값이 **실행할 때마다
297
+ * 다르다.** 그런데 fact identity 가 시각을 포함하므로, 같은 사실을 다시 받으면 dedupe 가 안 되고
298
+ * 새 사실로 앉는다.
299
+ *
300
+ * 실제로 그렇게 됐다. `fillTwinScope(from: 0)` 을 두 번 돌렸더니 mes-line-a 저널이
301
+ * 40 → 69건이 되고 중복이 27가지 생겼다. 시각을 말하는 레코드 12건만 되풀이로 걸러졌다.
302
+ *
303
+ * ── 봉투는 그 시각을 알고 있었다 ──────────────────────────────────────────
304
+ * plant 아웃박스가 `eventTime` 을 실어 보낸다. 그것이 갈 자리가 없어서 버려지고 있었다 —
305
+ * connector 주석이 그 위험을 적어 두고 자리를 요청해 두었다(`operato-plant.ts`).
306
+ *
307
+ * **레코드가 자기 시각을 말하면 그것이 이긴다.** 봉투의 시각은 「그 사실을 언제 보냈나」에 가깝고,
308
+ * 레코드의 시각은 「언제 일어났나」다. 둘이 다르면 뒤엣것이 맞다.
309
+ */
310
+ at?: string
204
311
  }
205
312
 
206
313
  /**
@@ -209,6 +316,30 @@ export interface InboundItem {
209
316
  * 흐름 열쇠는 **어댑터가 정한다**(`Record`). 계약이 이름을 닫으면 새 원본마다 계약을 고치게 된다.
210
317
  */
211
318
  export interface LiveFeedContinuity {
319
+ /**
320
+ * The name to take a task lease under, so only one instance polls this feed.
321
+ *
322
+ * ── Why this layer composes it ────────────────────────────────────────────
323
+ * `TaskLease` has no domain column on purpose: what is made exclusive is a
324
+ * loop, not a tenant's slice of one, and a task that does need to be
325
+ * per-tenant puts the tenant in the name so the unique index keeps meaning
326
+ * what it says.
327
+ *
328
+ * An adapter cannot follow that rule. It is handed `cfg` and `site`, and
329
+ * neither carries a domain — so a name built from `siteId` alone would let
330
+ * two domains that happen to use the same plant code block each other's
331
+ * channel. Silently, and looking exactly like "someone else holds it".
332
+ *
333
+ * So the name is composed here, where `domainId` and `instanceId` are known,
334
+ * and the rule lives in one place.
335
+ *
336
+ * ── Always present, unlike `cursor` ───────────────────────────────────────
337
+ * A feed needs its lease on the very first attach, before there is any
338
+ * cursor to carry. So this object is now handed over even when nothing has
339
+ * been read yet; `cursor` being absent is what still says "first attach",
340
+ * which is how every connector already reads it (`continuity?.cursor`).
341
+ */
342
+ leaseName: string
212
343
  /** 지난번에 어디까지 읽었나 — 없으면 첫 붙음이다(그때만 되돌아볼 날수로 창을 만든다). */
213
344
  cursor?: { streams?: Record<string, LiveFeedCursor>; firstAttachedAt?: string }
214
345
  /**
@@ -326,6 +457,25 @@ export interface LiveFeedContinuity {
326
457
  onWithheld?: (info: { reason: string; count: number }) => void
327
458
  }
328
459
 
460
+ /**
461
+ * 저쪽이 조치를 받을 준비가 됐나 — **「모른다」를 「안 된다」로 답하지 않는다.**
462
+ *
463
+ * `ready` 가 거짓이면 `reason` 이 사람의 말로 무엇이 없는지 말하고, `code` 가 화면이 다음 할 일을
464
+ * 가를 값이다(문장을 파싱하지 않게 — 번역하면 문장이 바뀐다).
465
+ */
466
+ export interface ActuationReadiness {
467
+ /** 받을 수 있어 보이나. **모르면 `undefined`** — 거짓이 아니다. */
468
+ ready?: boolean
469
+ /** 어디까지 봤나. `local` = 우리 설정만 봤다(망을 타지 않았다). `remote` = 저쪽에 물었다. */
470
+ checked: 'local' | 'remote'
471
+ /** 무엇이 없나 — 사람의 말로. */
472
+ reason?: string
473
+ /** 화면이 가를 값. 커넥터가 정한다(예: `no-endpoint` · `no-secret` · `unreachable` · `not-configured`). */
474
+ code?: string
475
+ }
476
+
477
+ import type { LiveCadence } from './live-cadence.js'
478
+
329
479
  export interface ReferenceAdapter {
330
480
  /** 레지스트리 키 (예: 'virtual' | 'sap-ewm' | 'custom-rest'). */
331
481
  type: string
@@ -346,6 +496,21 @@ export interface ReferenceAdapter {
346
496
  */
347
497
  masterSteps?: readonly ReferenceStep[]
348
498
 
499
+ /**
500
+ * 이 원천이 **축마다 언제·얼마나 자주 내놓나** — 유입 건강이 이 선언으로 견준다.
501
+ *
502
+ * 선언하지 않은 축은 **지금까지처럼** 창 하나(10분)로 견준다. 그러면 밤에 발전하지 않는
503
+ * 태양광과 업무시간만 도는 원천이 **정상인데 매일 밤 빨갛게** 난다 — 거짓 빨강이 쌓이면 진짜
504
+ * 빨강도 같이 안 읽힌다.
505
+ *
506
+ * `masterSteps` 와 같은 규율이다: **어댑터만 자기 시간의 결을 안다.** 호스트가 짐작하면 그것은
507
+ * 지어낸 판정이다.
508
+ *
509
+ * 자세한 것은 §`live-cadence.ts` — 특히 「해 있는 동안」을 **고정 시각으로 박지 말 것**
510
+ * (오늘 잰 일몰은 두 달 뒤에 틀리다).
511
+ */
512
+ liveCadence?: readonly LiveCadence[]
513
+
349
514
  /**
350
515
  * @param onStep 진행을 알리는 통로 — **선택이다.** 주지 않으면 예전과 같이 동작한다.
351
516
  *
@@ -432,7 +597,149 @@ export interface ReferenceAdapter {
432
597
  * `ref` 를 **반드시** 돌려준다. 그것이 없으면 넘긴 것이 저쪽에서 무엇이 되었는지 되짚을 수 없고,
433
598
  * 되돌릴 때 무엇을 되돌릴지 말할 수 없다.
434
599
  */
435
- actuate?(cfg: ConnectionConfig, site: SiteDescriptor, command: ApprovedCommand): Promise<{ ok: boolean; ref?: string; error?: string }>
600
+ actuate?(
601
+ cfg: ConnectionConfig,
602
+ site: SiteDescriptor,
603
+ command: ApprovedCommand
604
+ ): Promise<{
605
+ ok: boolean
606
+ ref?: string
607
+ /**
608
+ * **받아들였으나 남은 것이 있다** — 실패가 아니다. 사람이 저쪽에서 해야 할 일이 있으면 여기 적는다.
609
+ *
610
+ * `error` 에 적지 말 것 — 실패로 읽혀 커맨드가 `failed` 로 앉고, 다시 넘기게 된다. 실제로 MES 가
611
+ * 「지시서를 못 붙였다」를 답했을 때 커넥터가 적을 칸이 없어 로그로만 남겼고, 트윈 쪽에서는 성공한
612
+ * 조치와 구별되지 않았다.
613
+ */
614
+ note?: string
615
+ /**
616
+ * 그 말 중 **다음에 할 일** 한 줄 — 원인은 위 `note` 다.
617
+ *
618
+ * 붙여 보내지 말 것. 읽는 사람은 「그래서 내가 뭘 해야 하나」를 먼저 찾고, 한 문장으로 오면
619
+ * **화면이 자르게 되며 자르는 규칙이 화면마다 생긴다.** 어댑터는 이미 둘로 알고 있다.
620
+ */
621
+ noteNext?: string
622
+ error?: string
623
+ /**
624
+ * **다시 해서 될 일인가** — 실패했을 때만.
625
+ *
626
+ * again 그대로 다시 해 볼 만하다 못 닿았거나 저쪽이 잠깐 흔들렸다
627
+ * after-fix 사람이 고친 뒤 그대로 나간다 설정 문제
628
+ * never 이 조치로는 영원히 안 된다 지시 내용이 틀렸다
629
+ *
630
+ * 가르는 자리는 **「조치를 다시 낼 필요가 있나」**다. 설정이 틀린 것은 조치가 멀쩡하므로
631
+ * `after-fix`, 지시 내용이 틀린 것은 그 조치가 영원히 틀렸으므로 `never` 다.
632
+ *
633
+ * **문장에 담지 말 것** — 일꾼이 그것을 쓰려면 파싱해야 하고, 번역되면 깨진다.
634
+ *
635
+ * 말하지 않으면 일꾼이 **집지 않는다.** 모르는 것을 `never` 로 접으면 고칠 수 있는 것을 사람이
636
+ * 포기하고, `again` 으로 접으면 없는 자재를 끝없이 두드린다.
637
+ */
638
+ retry?: 'again' | 'after-fix' | 'never'
639
+ }>
640
+
641
+ /**
642
+ * **저쪽이 조치를 받을 준비가 됐나** — 보내기 전에 묻는다. 선택이다.
643
+ *
644
+ * ── 왜 필요한가 (2026-09-03 실측) ──────────────────────────────────────────
645
+ * 승인된 조치를 넘겼더니 저쪽이 503 을 답했다 — **저쪽 프로세스에 비밀값이 실려 있지 않았다.**
646
+ * 트윈 쪽 연결 설정에는 있었고, 주소도 맞았고, 서명도 맞았다. 저쪽이 재기동되면서 환경 변수에만
647
+ * 살던 값을 잃은 것이다.
648
+ *
649
+ * 그것을 보내 보기 전에는 알 수 없었다. 그래서 사람이 승인 화면까지 가서 누르고, 실패를 보고,
650
+ * 다시 로그인했다. **승인이 사라지지는 않는다**(`failed → dispatch` 가 열려 있다) — 없어진 것은
651
+ * 사람의 시간이다.
652
+ *
653
+ * ── 문이 아니다. 알림이다 ──────────────────────────────────────────────────
654
+ * **이 답으로 넘김을 막지 않는다.** 막으면 새 실패 방식이 생긴다 — 점검이 틀렸거나 잠깐 못 닿은
655
+ * 사이에, 성공할 수 있었던 조치가 못 나간다. 화면이 「지금 저쪽이 못 받는 것으로 보입니다」를
656
+ * 미리 말하는 데까지가 이 얼굴의 일이다.
657
+ *
658
+ * ── 두 겹을 구별한다 ──────────────────────────────────────────────────────
659
+ * 우리 쪽에 무엇이 없는 것(주소·비밀값을 설정하지 않았다)은 **망을 타지 않고** 알 수 있다.
660
+ * 저쪽이 받을 준비가 됐는지는 물어야 안다. 앞엣것만 답하고 뒤엣것을 안 물어도 되고, 그때
661
+ * `checked: 'local'` 로 그 사실을 말한다 — 「살아 있다」고 말한 적 없는 것과 「죽었다」는 다르다.
662
+ *
663
+ * 화면이 그릴 때마다 부를 수 있으므로 **망을 타는 구현은 짧게 끝내야 한다.**
664
+ */
665
+ actuationReadiness?(cfg: ConnectionConfig, site: SiteDescriptor): Promise<ActuationReadiness>
666
+
667
+ /**
668
+ * **놓친 구간을 backfill 한다** — 밀어 주는 길의 짝.
669
+ *
670
+ * ── 왜 필요한가 (2026-08-31, 인티그레이션 레인 지적) ──────────────────────
671
+ * 훅은 반드시 놓친다 — 우리가 내려가 있을 때, 그쪽이 못 보냈을 때, 네트워크가 끊겼을 때. 받는 쪽이
672
+ * 번호로 그것을 **알아채게** 되었지만(§`takeInSequence`), 알아챈 뒤 **그 사이를 채우는 길이 없었다.**
673
+ * 설계가 그 길을 적어 두었는데 만들지 않았다.
674
+ *
675
+ * ── 왜 `since` 가 번호인가 ────────────────────────────────────────────────
676
+ * 커서에 사는 것이 번호이고, 보내는 쪽이 되풀어 주는 단위도 번호다. 시각으로 두면 두 축이 섞이고,
677
+ * 같은 밀리초의 두 행이 갈리지 않는다.
678
+ *
679
+ * ── 왜 `scope` 를 프레임워크가 주나 ───────────────────────────────────────
680
+ * 그것이 커서 열쇠의 절반이다. 커넥터가 만들면 열쇠를 만드는 자리가 둘이 되고, 어긋난 그곳은 조용하다.
681
+ * 그래서 프레임워크가 커서에서 읽어 그대로 넘긴다.
682
+ *
683
+ * 답이 `InboundBatch` 인 이유도 하나다 — **backfill 한 것이 밀어 받은 것과 같은 번호 검사를 지난다.**
684
+ * 다른 모양으로 돌려주면 그 검사를 다시 만들게 되고, 두 길이 다른 규칙으로 받는다.
685
+ */
686
+ fetchSince?(cfg: ConnectionConfig, site: SiteDescriptor, scope: string, since: number): Promise<InboundBatch>
687
+
688
+ /**
689
+ * (선택) **원본에만 있는 관계를 조회한다** — 트윈으로 복제하지 않고 그때그때 물어본다.
690
+ *
691
+ * ── 왜 필요한가 ─────────────────────────────────────────────────────────────
692
+ * 트윈은 표준 어휘로 옮길 수 있는 것만 담는다. 원본에는 그 밖의 관계가 있다 — MES 의 생산 실적,
693
+ * 설비 상태 구간, 정비 계획. 설비 상세 화면에서 사람이 실제로 묻는 것이 그런 것들이다.
694
+ *
695
+ * 이것을 트윈으로 복제하면 원본의 표를 전부 미러링하게 된다. 조회용 참조는 복제하지 않고 그때
696
+ * 물어보는 것이 맞다.
697
+ *
698
+ * ── 왜 어댑터가 답하나 ──────────────────────────────────────────────────────
699
+ * 어느 테이블에 무엇이 있는지는 원본마다 다르다. 커널이 그것을 알면 원본 하나의 스키마에 묶이고,
700
+ * 다음 원본이 다른 구조를 쓰면 커널을 또 고친다.
701
+ *
702
+ * 어댑터는 이미 그 원본에 붙는 방법과 인증을 갖고 있다. 연결 설정이 한 자리에 있어야 한다는 것은
703
+ * `actuate` 를 별개 어댑터로 두지 않은 것과 같은 이유다.
704
+ *
705
+ * ── 실패하면 그것을 알린다 ──────────────────────────────────────────────────
706
+ * 예외를 던져도 됩니다. 호출하는 쪽이 잡아서 「조회 실패」로 표시하고, 나머지 관계는 그대로
707
+ * 표시합니다. 빈 배열은 「가리키는 것이 없다」는 뜻이고 실패와 다릅니다.
708
+ *
709
+ * @param axis `equipment` · `locations` 등 계약의 축 이름
710
+ * @param itemId 그 축에서의 식별자. 원본이 아는 이름이다(설비는 `ops_equipment.name`)
711
+ */
712
+ fetchLinks?(cfg: ConnectionConfig, site: SiteDescriptor | undefined, axis: string, itemId: string): Promise<ReferenceLinkGroup[]>
713
+
714
+ /**
715
+ * (선택) **그 번호가 무엇이었나** — 구멍을 만났을 때 묻는다.
716
+ *
717
+ * ── 왜 필요한가 (2026-09-06 실물) ──────────────────────────────────────────
718
+ * 커넥터가 구멍을 만나면 「418 다음이 420」이라고만 말한다. **419 가 무엇이었는지 알 방법이
719
+ * 없다.** 그날 plant 레인이 자기 아웃박스를 손으로 뒤져서 그것이 부하 시험의 흔적임을 찾았다.
720
+ *
721
+ * 구멍은 채널을 세운다 — 그 뒤의 사실이 통째로 못 온다. 그래서 「이게 무엇이었나」가 급한 물음인데
722
+ * 물을 자리가 없었다.
723
+ *
724
+ * ── 세 가지를 가른다. 넷째는 「모른다」다 ──────────────────────────────────
725
+ * ```
726
+ * present 그 행이 있다 — 봉투를 함께 낸다
727
+ * lost 번호는 나갔는데 행이 없다 — 쓰기 하나를 잃었다
728
+ * unissued 아직 그 번호까지 안 갔다 — 잃은 것이 아니다
729
+ * unknown 원본이 그 셋을 구별하지 못한다
730
+ * ```
731
+ *
732
+ * **`unknown` 이 있어야 한다.** 채번을 따로 들지 않는 원본은 「행이 없다」에서 `lost` 와
733
+ * `unissued` 를 가를 수 없다. 그때 `lost` 로 단정하면 사람이 없는 사실을 찾으러 간다.
734
+ *
735
+ * ── 판정을 여기서 하지 않는다 ─────────────────────────────────────────────
736
+ * 이 문은 **원본이 아는 것을 그대로 낸다.** 「그러니 이 번호는 포기해도 된다」는 판정은 사람이
737
+ * 한다 — 그 판단은 되돌릴 수 없어서(커서가 지나가면 끝이다) 코드가 대신할 자리가 아니다.
738
+ *
739
+ * @param from 이 번호부터(포함)
740
+ * @param to 이 번호까지(포함). 원본이 상한을 두면 그만큼만 내고 `truncated` 로 말한다
741
+ */
742
+ seqReport?(cfg: ConnectionConfig, site: SiteDescriptor | undefined, scope: string, from: number, to: number): Promise<SeqReport>
436
743
 
437
744
  openLiveFeed?(
438
745
  cfg: ConnectionConfig,
@@ -542,6 +849,12 @@ export function capabilitiesOf(adapter: ReferenceAdapter | undefined): Reference
542
849
  const out: ReferenceCapability[] = []
543
850
  if (declared.has('live') && typeof adapter.openLiveFeed === 'function') out.push('live')
544
851
  if (declared.has('control') && typeof adapter.control === 'function') out.push('control')
852
+ if (declared.has('actuate') && typeof adapter.actuate === 'function') out.push('actuate')
853
+ /*
854
+ * **선언만으로 성립한다** — 부를 메서드가 없다. 「두 번 해도 같은가」는 어댑터가 하는 일이 아니고
855
+ * 저쪽 시스템의 성질이다. 그래서 구현 검사를 하지 않는다.
856
+ */
857
+ if (declared.has('idempotent-actuation')) out.push('idempotent-actuation')
545
858
  return out
546
859
  }
547
860
 
@@ -569,8 +882,17 @@ const adapters = new Map<string, ReferenceAdapter>()
569
882
  */
570
883
  export function registerAdapterType(adapter: ReferenceAdapter): void {
571
884
  const declared = new Set(adapter.capabilities ?? [])
572
- const undeclared = (['live', 'control'] as const).filter(
573
- c => typeof (adapter as any)[c === 'live' ? 'openLiveFeed' : 'control'] === 'function' && !declared.has(c)
885
+ /* 능력 이름과 그것을 증명하는 메서드 — **한 자리에 적는다.** 두 벌이면 새 능력이 한쪽에만 늘어난다. */
886
+ const METHOD_OF: Record<ReferenceCapability, string> = {
887
+ live: 'openLiveFeed',
888
+ control: 'control',
889
+ actuate: 'actuate',
890
+ /* 성질 선언이라 부를 메서드가 없다 — 어댑터가 `capabilities` 에 적는 것으로 끝난다. */
891
+ 'idempotent-actuation': ''
892
+ }
893
+ const undeclared = (Object.keys(METHOD_OF) as ReferenceCapability[]).filter(
894
+ /* 메서드 이름이 없는 능력(선언만으로 성립하는 것)은 이 경고의 대상이 아니다. */
895
+ c => METHOD_OF[c] !== '' && typeof (adapter as any)[METHOD_OF[c]] === 'function' && !declared.has(c)
574
896
  )
575
897
  if (undeclared.length) {
576
898
  twinWarn(
@@ -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
+ }