@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,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.orderObservationsIn = orderObservationsIn;
4
+ exports.settleEffects = settleEffects;
5
+ /*
6
+ * **관측이 들어올 때 나간 지시와 잇는다** — 폐루프의 배선. 판단은 `effect-match.ts` 가 한다.
7
+ *
8
+ * ── 무엇이 실측으로 확인됐나 (2026-09-02) ─────────────────────────────────────
9
+ * 인티그레이션 레인이 조치 둘을 MES 까지 걸었다. 트윈 쪽 두 행이 **구별되지 않았다.**
10
+ *
11
+ * 325e9bb1 acked WO-20260902-004 지시서 붙음 MES 에서 DRAFT
12
+ * 57428362 acked WO-20260902-005 지시서 못 붙음 MES 에서 DRAFT
13
+ *
14
+ * `state`·`dispatch_ref`·`error` 가 셋 다 같다. 그래서 화면은 둘 다 「나갔다」로 읽는데, 둘 다 아직
15
+ * 현장에 안 갔다. `acked` 의 뜻은 **「저쪽이 받았다」**까지다.
16
+ *
17
+ * 빠져나오는 것은 관측이다 — 저쪽이 그 오더를 현장에 내면 `order.status` 가 우리가 받은 식별자로
18
+ * 돌아온다. 그것을 이 파일이 잇는다.
19
+ *
20
+ * ── 왜 유입 자리인가 ─────────────────────────────────────────────────────────
21
+ * 관측의 **시각**이 봉투에만 있다. 나중에 커널 상태에서 읽으면 「지금」밖에 알 수 없고, 그것은 보고된
22
+ * 시각이 아니라 우리가 처리한 시각이다. 없는 사실을 만들지 않는다.
23
+ *
24
+ * ── 어디서 부르나 ───────────────────────────────────────────────────────────
25
+ *
26
+ * routes.ts 훅으로 밀려 들어온 것
27
+ * reference-live.ts 커넥터가 이어 내는 것 · 폴링으로 물어 온 것
28
+ *
29
+ * **백필(`backfill-period-facts.ts`)에서는 부르지 않는다.** 그것은 마감된 구간의 저널을 채우는
30
+ * 일이고, 이 자리가 답하는 것은 「방금 보낸 지시가 어떻게 됐나」다. 저널을 고치는 작업이 조치 기록을
31
+ * 바꾸게 두면, 백필을 다시 돌릴 때마다 같은 보고가 다시 세어진다(`seen` 이 는다).
32
+ *
33
+ * 그래서 지난 기록에서 효과가 확인되는 경우는 지금 다루지 않는다. 필요해지면 그때는 **백필 전용
34
+ * 경로**로 만들 일이다 — 몇 번 세었나가 아니라 「그때 이랬다」만 적는 자리다.
35
+ *
36
+ * ── 값은 얼마나 드나 ─────────────────────────────────────────────────────────
37
+ * 오더 보고가 든 묶음마다 질의 하나다. 조치가 하나도 안 나간 도메인에서도 그 질의가 돈다 — 지금
38
+ * 규모에서 그 값은 작지만 공짜라고 말하지 않는다. 더 줄이려면 「효과를 기다리는 식별자」를 프로세스가
39
+ * 들고 있어야 하고, 그러면 재기동 때 저장소에서 되세우는 자리가 하나 더 생긴다. 그것이 필요해지는
40
+ * 시점은 **오더 보고가 초당 수백 건 오는 도메인**이다.
41
+ */
42
+ const ops_contract_1 = require("@operato/ops-contract");
43
+ const log_js_1 = require("../../engine/log.js");
44
+ const effect_match_js_1 = require("./effect-match.js");
45
+ /** 봉투에서 오더 보고만 골라낸다 — 다른 어휘는 보지 않는다. */
46
+ function orderObservationsIn(envelopes) {
47
+ const out = [];
48
+ for (const e of envelopes ?? []) {
49
+ if (e?.eventType !== ops_contract_1.OP_EVENT.order)
50
+ continue;
51
+ const data = e?.data;
52
+ if (!data?.orderId)
53
+ continue;
54
+ /*
55
+ * 시각은 **봉투의 것**을 쓴다. 보고한 쪽이 말한 시각이고, 우리가 처리한 시각이 아니다.
56
+ * 없으면 `matchEffects` 가 거른다 — 여기서 「지금」으로 채우지 않는다.
57
+ */
58
+ out.push({ orderId: String(data.orderId), status: String(data.status ?? ''), at: e?.eventTime });
59
+ }
60
+ return out;
61
+ }
62
+ /**
63
+ * 이 묶음의 오더 보고를 나간 지시와 잇는다 — **아무것도 못 이어도 조용히 지난다.**
64
+ *
65
+ * 유입 경로에서 부른다. 그래서 **던지지 않는다** — 효과를 적다 실패해서 관측 유입이 멈추면, 고치는
66
+ * 동안 트윈이 눈을 감는다. 효과는 다음 보고에 다시 이을 수 있고 관측은 다시 오지 않는다.
67
+ */
68
+ async function settleEffects(args) {
69
+ const { domainId, envelopes, store } = args;
70
+ const none = { recorded: 0, unmatched: 0 };
71
+ const observations = orderObservationsIn(envelopes);
72
+ if (!observations.length)
73
+ return none;
74
+ /*
75
+ * 저장소가 두 자리를 안 갖추면 이을 길이 없다. **조용히 지나지 않고 말한다** — 이 상태에서는
76
+ * 넘긴 지시가 저쪽에서 어떻게 됐는지 영원히 모르고, 화면은 그 사실을 알 수 없다.
77
+ */
78
+ if (!store.byDispatchRef || !store.recordEffects) {
79
+ (0, log_js_1.twinWarn)('[actuation] 나간 지시와 관측을 이을 수 없다 — 저장소에 byDispatchRef 나 recordEffects 가 없다. ' +
80
+ '넘긴 조치가 저쪽에서 무엇이 되었는지 알 수 없는 상태다.');
81
+ return none;
82
+ }
83
+ try {
84
+ const byRef = await store.byDispatchRef(domainId, (0, effect_match_js_1.refsToLookUp)(observations));
85
+ if (!byRef.size)
86
+ return { recorded: 0, unmatched: observations.length };
87
+ const { updates, skips } = (0, effect_match_js_1.matchEffects)({
88
+ observations,
89
+ byRef: new Map([...byRef].map(([ref, c]) => [ref, { id: c.id, dispatchRef: ref, ...(c.effect ? { effect: c.effect } : {}) }]))
90
+ });
91
+ if (updates.length)
92
+ await store.recordEffects(domainId, updates);
93
+ return { recorded: updates.length, unmatched: skips.length };
94
+ }
95
+ catch (e) {
96
+ /* 관측 유입을 멈추지 않는다 — 위 §. 다만 조용히 넘기지도 않는다. */
97
+ (0, log_js_1.twinWarn)(`[actuation] 효과를 잇다 실패했다 — ${String(e?.message ?? e).slice(0, 200)}`);
98
+ return none;
99
+ }
100
+ }
101
+ //# sourceMappingURL=settle-effects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settle-effects.js","sourceRoot":"","sources":["../../../server/service/actuation/settle-effects.ts"],"names":[],"mappings":";;AA6CA,kDAaC;AAeD,sCAyCC;AAlHD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wDAAgD;AAEhD,gDAA8C;AAG9C,uDAAqF;AAErF,wCAAwC;AACxC,SAAgB,mBAAmB,CAAC,SAAyB;IAC3D,MAAM,GAAG,GAAuB,EAAE,CAAA;IAClC,KAAK,MAAM,CAAC,IAAI,SAAS,IAAI,EAAE,EAAE,CAAC;QAChC,IAAI,CAAC,EAAE,SAAS,KAAK,uBAAQ,CAAC,KAAK;YAAE,SAAQ;QAC7C,MAAM,IAAI,GAAG,CAAC,EAAE,IAAI,CAAA;QACpB,IAAI,CAAC,IAAI,EAAE,OAAO;YAAE,SAAQ;QAC5B;;;WAGG;QACH,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,CAAA;IAClG,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AASD;;;;;GAKG;AACI,KAAK,UAAU,aAAa,CAAC,IAInC;IACC,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,IAAI,CAAA;IAC3C,MAAM,IAAI,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAA;IAE1C,MAAM,YAAY,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAAA;IACnD,IAAI,CAAC,YAAY,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAErC;;;OAGG;IACH,IAAI,CAAC,KAAK,CAAC,aAAa,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC;QACjD,IAAA,iBAAQ,EACN,4EAA4E;YAC1E,kCAAkC,CACrC,CAAA;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAA,8BAAY,EAAC,YAAY,CAAC,CAAC,CAAA;QAC7E,IAAI,CAAC,KAAK,CAAC,IAAI;YAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,YAAY,CAAC,MAAM,EAAE,CAAA;QAEvE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,IAAA,8BAAY,EAAC;YACtC,YAAY;YACZ,KAAK,EAAE,IAAI,GAAG,CACZ,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAC/G;SACF,CAAC,CAAA;QAEF,IAAI,OAAO,CAAC,MAAM;YAAE,MAAM,KAAK,CAAC,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAChE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,MAAM,EAAE,CAAA;IAC9D,CAAC;IAAC,OAAO,CAAM,EAAE,CAAC;QAChB,4CAA4C;QAC5C,IAAA,iBAAQ,EAAC,6BAA6B,MAAM,CAAC,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAA;QAC9E,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC","sourcesContent":["/*\n * **관측이 들어올 때 나간 지시와 잇는다** — 폐루프의 배선. 판단은 `effect-match.ts` 가 한다.\n *\n * ── 무엇이 실측으로 확인됐나 (2026-09-02) ─────────────────────────────────────\n * 인티그레이션 레인이 조치 둘을 MES 까지 걸었다. 트윈 쪽 두 행이 **구별되지 않았다.**\n *\n * 325e9bb1 acked WO-20260902-004 지시서 붙음 MES 에서 DRAFT\n * 57428362 acked WO-20260902-005 지시서 못 붙음 MES 에서 DRAFT\n *\n * `state`·`dispatch_ref`·`error` 가 셋 다 같다. 그래서 화면은 둘 다 「나갔다」로 읽는데, 둘 다 아직\n * 현장에 안 갔다. `acked` 의 뜻은 **「저쪽이 받았다」**까지다.\n *\n * 빠져나오는 것은 관측이다 — 저쪽이 그 오더를 현장에 내면 `order.status` 가 우리가 받은 식별자로\n * 돌아온다. 그것을 이 파일이 잇는다.\n *\n * ── 왜 유입 자리인가 ─────────────────────────────────────────────────────────\n * 관측의 **시각**이 봉투에만 있다. 나중에 커널 상태에서 읽으면 「지금」밖에 알 수 없고, 그것은 보고된\n * 시각이 아니라 우리가 처리한 시각이다. 없는 사실을 만들지 않는다.\n *\n * ── 어디서 부르나 ───────────────────────────────────────────────────────────\n *\n * routes.ts 훅으로 밀려 들어온 것\n * reference-live.ts 커넥터가 이어 내는 것 · 폴링으로 물어 온 것\n *\n * **백필(`backfill-period-facts.ts`)에서는 부르지 않는다.** 그것은 마감된 구간의 저널을 채우는\n * 일이고, 이 자리가 답하는 것은 「방금 보낸 지시가 어떻게 됐나」다. 저널을 고치는 작업이 조치 기록을\n * 바꾸게 두면, 백필을 다시 돌릴 때마다 같은 보고가 다시 세어진다(`seen` 이 는다).\n *\n * 그래서 지난 기록에서 효과가 확인되는 경우는 지금 다루지 않는다. 필요해지면 그때는 **백필 전용\n * 경로**로 만들 일이다 — 몇 번 세었나가 아니라 「그때 이랬다」만 적는 자리다.\n *\n * ── 값은 얼마나 드나 ─────────────────────────────────────────────────────────\n * 오더 보고가 든 묶음마다 질의 하나다. 조치가 하나도 안 나간 도메인에서도 그 질의가 돈다 — 지금\n * 규모에서 그 값은 작지만 공짜라고 말하지 않는다. 더 줄이려면 「효과를 기다리는 식별자」를 프로세스가\n * 들고 있어야 하고, 그러면 재기동 때 저장소에서 되세우는 자리가 하나 더 생긴다. 그것이 필요해지는\n * 시점은 **오더 보고가 초당 수백 건 오는 도메인**이다.\n */\nimport { OP_EVENT } from '@operato/ops-contract'\n\nimport { twinWarn } from '../../engine/log.js'\n\nimport type { CommandStore } from './command-dispatcher.js'\nimport { matchEffects, refsToLookUp, type OrderObservation } from './effect-match.js'\n\n/** 봉투에서 오더 보고만 골라낸다 — 다른 어휘는 보지 않는다. */\nexport function orderObservationsIn(envelopes: readonly any[]): OrderObservation[] {\n const out: OrderObservation[] = []\n for (const e of envelopes ?? []) {\n if (e?.eventType !== OP_EVENT.order) continue\n const data = e?.data\n if (!data?.orderId) continue\n /*\n * 시각은 **봉투의 것**을 쓴다. 보고한 쪽이 말한 시각이고, 우리가 처리한 시각이 아니다.\n * 없으면 `matchEffects` 가 거른다 — 여기서 「지금」으로 채우지 않는다.\n */\n out.push({ orderId: String(data.orderId), status: String(data.status ?? ''), at: e?.eventTime })\n }\n return out\n}\n\nexport interface SettleEffectsResult {\n /** 효과를 적은 조치 수. */\n recorded: number\n /** 이을 수 없던 보고 수 — 대개 우리가 내지 않은 오더다. */\n unmatched: number\n}\n\n/**\n * 이 묶음의 오더 보고를 나간 지시와 잇는다 — **아무것도 못 이어도 조용히 지난다.**\n *\n * 유입 경로에서 부른다. 그래서 **던지지 않는다** — 효과를 적다 실패해서 관측 유입이 멈추면, 고치는\n * 동안 트윈이 눈을 감는다. 효과는 다음 보고에 다시 이을 수 있고 관측은 다시 오지 않는다.\n */\nexport async function settleEffects(args: {\n domainId: string\n envelopes: readonly any[]\n store: CommandStore\n}): Promise<SettleEffectsResult> {\n const { domainId, envelopes, store } = args\n const none = { recorded: 0, unmatched: 0 }\n\n const observations = orderObservationsIn(envelopes)\n if (!observations.length) return none\n\n /*\n * 저장소가 두 자리를 안 갖추면 이을 길이 없다. **조용히 지나지 않고 말한다** — 이 상태에서는\n * 넘긴 지시가 저쪽에서 어떻게 됐는지 영원히 모르고, 화면은 그 사실을 알 수 없다.\n */\n if (!store.byDispatchRef || !store.recordEffects) {\n twinWarn(\n '[actuation] 나간 지시와 관측을 이을 수 없다 — 저장소에 byDispatchRef 나 recordEffects 가 없다. ' +\n '넘긴 조치가 저쪽에서 무엇이 되었는지 알 수 없는 상태다.'\n )\n return none\n }\n\n try {\n const byRef = await store.byDispatchRef(domainId, refsToLookUp(observations))\n if (!byRef.size) return { recorded: 0, unmatched: observations.length }\n\n const { updates, skips } = matchEffects({\n observations,\n byRef: new Map(\n [...byRef].map(([ref, c]) => [ref, { id: c.id, dispatchRef: ref, ...(c.effect ? { effect: c.effect } : {}) }])\n )\n })\n\n if (updates.length) await store.recordEffects(domainId, updates)\n return { recorded: updates.length, unmatched: skips.length }\n } catch (e: any) {\n /* 관측 유입을 멈추지 않는다 — 위 §. 다만 조용히 넘기지도 않는다. */\n twinWarn(`[actuation] 효과를 잇다 실패했다 — ${String(e?.message ?? e).slice(0, 200)}`)\n return none\n }\n}\n"]}
@@ -0,0 +1,37 @@
1
+ import { type ApprovedCommand } from '@operato/ops-contract';
2
+ import { type CommandAck, type CommandStore } from './command-dispatcher.js';
3
+ /** 한 바퀴에 볼 최대 수 — 대기 줄이 커도 한 주기를 독차지하지 않게. */
4
+ export declare const SWEEP_LIMIT = 50;
5
+ export interface SweepReport {
6
+ /** 표에서 읽은 수. */
7
+ seen: number;
8
+ /** 실제로 넘긴 수. */
9
+ dispatched: number;
10
+ /** 넘기지 않은 이유별 수 — 목록이 그대로 보인다. */
11
+ skipped: Record<string, number>;
12
+ }
13
+ /**
14
+ * 이 테넌트의 대기 줄을 한 바퀴 훑는다.
15
+ *
16
+ * 저장소가 대기 줄을 못 내면 **아무것도 하지 않는다** — 그 능력이 없는 구현도 그대로 돌아야
17
+ * 한다(옛 거동).
18
+ */
19
+ export interface SweepDeps {
20
+ store: CommandStore;
21
+ /** 이 조치를 저쪽에 낸다 — 부르는 쪽이 커넥터 배선을 안다. */
22
+ actuate: (command: ApprovedCommand) => Promise<CommandAck>;
23
+ /** 이 연결이 두 번 보내도 지시가 하나인가 — **선언만 참으로 읽는다.** */
24
+ idempotent: (instanceId: string) => Promise<boolean>;
25
+ /**
26
+ * 우리 쪽 설정만으로 답할 수 있는 준비 상태. 저쪽을 두드리지 않는 답만 준다.
27
+ *
28
+ * 없으면 앞점검을 하지 않는다(그냥 넘긴다) — 모르는 것을 거절로 접지 않는 규율이다.
29
+ */
30
+ localReadiness?: (instanceId: string) => Promise<{
31
+ ready?: boolean;
32
+ reason?: string;
33
+ code?: string;
34
+ } | undefined>;
35
+ nowMs?: () => number;
36
+ }
37
+ export declare function sweepPendingDispatch(domainId: string, deps: SweepDeps): Promise<SweepReport>;
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SWEEP_LIMIT = void 0;
4
+ exports.sweepPendingDispatch = sweepPendingDispatch;
5
+ /*
6
+ * **남은 것을 줍는 그물** — 승인은 됐는데 안 나간 것, 실패해서 다시 해 볼 것.
7
+ *
8
+ * ── 왜 필요한가 (2026-09-04) ────────────────────────────────────────────────
9
+ * 승인 직후에 한 번 보내는 것만 있었다(§`dispatchAfterApproval`). 정상 경로는 그것으로 282ms 에
10
+ * 끝난다 — 실측이다. 그런데 두 자리가 비어 있었다.
11
+ *
12
+ * 커밋과 발신 사이에 프로세스가 죽으면 그 조치는 `approved` 로 남고 스스로 안 나간다
13
+ * 실패한 것을 다시 보내려면 사람이 시켜야 했다
14
+ *
15
+ * 2026-09-02 에 뒤엣것을 겪었다: 저쪽 인증 정보가 채워진 뒤 사람이 손으로 다시 넘겨서 나갔다.
16
+ *
17
+ * ── 왜 새 타이머를 세우지 않나 ──────────────────────────────────────────────
18
+ * 규칙 주기(`rule-loop`)에 얹는다. 그 파일이 이미 그 이유를 적어 두었다 — 「타이머를 둘 두면
19
+ * 내리고 세울 자리가 둘이 된다」. 놓친 보고를 backfill 하는 것(`catchUpEffects`)도 같은 자리에 있다.
20
+ *
21
+ * 30초면 충분하다. **아무도 안 기다린다** — 정상 경로가 이미 끝냈고, 이 그물은 남은 것만 본다.
22
+ *
23
+ * ── 판단은 여기 없다 ────────────────────────────────────────────────────────
24
+ * 「지금 넘길 것인가, 다음은 언제인가」는 순수 함수가 답한다(§`retryDecisionOf`). 이 파일은
25
+ * 표를 읽고 그 답대로 하고 결과를 적는 배선이다. 그렇게 갈라 둔 이유는 물러섬 규칙을 30분
26
+ * 기다리지 않고 값으로 잴 수 있어야 하기 때문이다.
27
+ *
28
+ * ── 던지지 않는다 ───────────────────────────────────────────────────────────
29
+ * 주기 안에서 돈다. 한 조치가 터져도 다음 조치를 봐야 하고, 이 그물이 터져도 규칙과 backfill 은
30
+ * 돌아야 한다. 조치를 못 넘기는 것과 트윈이 멈추는 것은 다른 사고다.
31
+ */
32
+ const ops_contract_1 = require("@operato/ops-contract");
33
+ const log_js_1 = require("../../engine/log.js");
34
+ const command_dispatcher_js_1 = require("./command-dispatcher.js");
35
+ const retry_plan_js_1 = require("./retry-plan.js");
36
+ /*
37
+ * **의존을 인자로 받는다** — 이 파일이 커넥터 등록소·연결 표를 직접 부르지 않는다.
38
+ *
39
+ * 규칙 실행이 이미 그 모양이다(`runRulesOnce({ store, rulesOf, attentionsOf … })`). 그렇게 두는
40
+ * 값이 둘이다: 배선을 시험이 굴릴 수 있고(직접 부르면 시험이 DB·모듈 부팅을 끌어온다 — 실제로
41
+ * 그렇게 났다), 그리고 이 파일이 무엇에 의존하는지가 서명에 적힌다.
42
+ */
43
+ /** 한 바퀴에 볼 최대 수 — 대기 줄이 커도 한 주기를 독차지하지 않게. */
44
+ exports.SWEEP_LIMIT = 50;
45
+ async function sweepPendingDispatch(domainId, deps) {
46
+ const { store } = deps;
47
+ const now = deps.nowMs ?? (() => Date.now());
48
+ const report = { seen: 0, dispatched: 0, skipped: {} };
49
+ if (!store.pendingDispatch)
50
+ return report;
51
+ const pending = await store.pendingDispatch(domainId).catch(() => []);
52
+ report.seen = pending.length;
53
+ const note = (why) => {
54
+ report.skipped[why] = (report.skipped[why] ?? 0) + 1;
55
+ };
56
+ for (const command of pending.slice(0, exports.SWEEP_LIMIT)) {
57
+ try {
58
+ const decision = (0, retry_plan_js_1.retryDecisionOf)({
59
+ state: command.state,
60
+ dispatchable: (0, ops_contract_1.nextCommandState)(command.state, 'dispatch') !== undefined,
61
+ retry: command.retry,
62
+ attempts: command.attempts,
63
+ nextAttemptAt: command.nextAttemptAt,
64
+ idempotent: await deps.idempotent(command.instanceId).catch(() => false),
65
+ nowMs: now()
66
+ });
67
+ if (!decision.take) {
68
+ note(decision.skip ?? 'unknown');
69
+ continue;
70
+ }
71
+ /*
72
+ * **넘기기 전에 우리 설정을 먼저 본다** — 아끼려는 것이 아니라 **이유를 지금 값으로
73
+ * 적으려는** 것이다(인티그레이션 레인이 제 첫 근거를 깼다: 그쪽 어댑터는 이미 `fetch` 전에
74
+ * 되돌아오므로 아낄 요청이 없다).
75
+ *
76
+ * 건너뛰면서 아무것도 안 적으면 **옛 `error` 가 남는다.** 저쪽 인증 정보 때문에 실패해 앉은
77
+ * 뒤 우리 쪽 설정이 지워지면, 목록에 계속 저쪽 얘기가 보이고 사람이 남의 서버를 보러 간다.
78
+ *
79
+ * 모르면(`undefined`) 그냥 넘긴다 — 모르는 것을 거절로 접지 않는다.
80
+ */
81
+ const readiness = await deps.localReadiness?.(command.instanceId).catch(() => undefined);
82
+ if (readiness && readiness.ready === false) {
83
+ await noteBlocked(store, domainId, command, decision.nextAttemptAt, readiness.reason, readiness.code);
84
+ note('not-ready');
85
+ continue;
86
+ }
87
+ const settled = await (0, command_dispatcher_js_1.dispatch)(store, domainId, command.id, c => deps.actuate(c));
88
+ await recordAttempt(store, domainId, settled, decision.attempt ?? 1, decision.nextAttemptAt);
89
+ if (settled.state === 'failed') {
90
+ note('failed');
91
+ (0, log_js_1.twinWarn)(`[twin-sweep] 조치 ${command.id} 를 넘기지 못했다: ${settled.error ?? '(사유 없음)'}`);
92
+ }
93
+ else {
94
+ report.dispatched++;
95
+ }
96
+ }
97
+ catch (e) {
98
+ /* 한 조치가 터져도 다음을 본다 — 가장 흔한 원인은 그 사이에 상태가 바뀐 것이다. */
99
+ note('threw');
100
+ (0, log_js_1.twinWarn)(`[twin-sweep] 조치 ${command.id} 에서 예외 — ${String(e?.message ?? e)}`);
101
+ }
102
+ }
103
+ return report;
104
+ }
105
+ /**
106
+ * 못 나가는 이유를 **지금 값으로** 적는다 — 넘기지 않았으므로 상태는 그대로다.
107
+ *
108
+ * **바뀌었을 때만 쓴다.** 10분마다 같은 이유를 다시 쓰면 `updatedAt` 이 계속 튀고, 목록의
109
+ * 「언제부터 이랬나」가 사라진다(§`catch-up-effects` 에서 같은 규율을 지켰다).
110
+ */
111
+ async function noteBlocked(store, domainId, command, nextAttemptAt, reason, code) {
112
+ const sentence = reason ?? code ?? '연결 설정이 조치를 낼 준비가 되지 않았습니다';
113
+ if (command.error === sentence && command.nextAttemptAt === nextAttemptAt)
114
+ return;
115
+ await store
116
+ .save(domainId, { ...command, error: sentence, ...(nextAttemptAt ? { nextAttemptAt } : {}) })
117
+ .catch(err => (0, log_js_1.twinWarn)(`[twin-sweep] 조치 ${command.id} 의 사유를 적지 못했다 — ${String(err?.message ?? err)}`));
118
+ }
119
+ /** 시도 횟수와 다음 시각을 적는다 — `dispatch` 가 쓴 상태를 덮지 않고 이 둘만 더한다. */
120
+ async function recordAttempt(store, domainId, settled, attempt, nextAttemptAt) {
121
+ await store
122
+ .save(domainId, { ...settled, attempts: attempt, ...(nextAttemptAt ? { nextAttemptAt } : {}) })
123
+ .catch(err => (0, log_js_1.twinWarn)(`[twin-sweep] 조치 ${settled.id} 의 시도 기록을 적지 못했다 — ${String(err?.message ?? err)}`));
124
+ }
125
+ //# sourceMappingURL=sweep-dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sweep-dispatch.js","sourceRoot":"","sources":["../../../server/service/actuation/sweep-dispatch.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **남은 것을 줍는 그물** — 승인은 됐는데 안 나간 것, 실패해서 다시 해 볼 것.\n *\n * ── 왜 필요한가 (2026-09-04) ────────────────────────────────────────────────\n * 승인 직후에 한 번 보내는 것만 있었다(§`dispatchAfterApproval`). 정상 경로는 그것으로 282ms 에\n * 끝난다 — 실측이다. 그런데 두 자리가 비어 있었다.\n *\n * 커밋과 발신 사이에 프로세스가 죽으면 그 조치는 `approved` 로 남고 스스로 안 나간다\n * 실패한 것을 다시 보내려면 사람이 시켜야 했다\n *\n * 2026-09-02 에 뒤엣것을 겪었다: 저쪽 인증 정보가 채워진 뒤 사람이 손으로 다시 넘겨서 나갔다.\n *\n * ── 왜 새 타이머를 세우지 않나 ──────────────────────────────────────────────\n * 규칙 주기(`rule-loop`)에 얹는다. 그 파일이 이미 그 이유를 적어 두었다 — 「타이머를 둘 두면\n * 내리고 세울 자리가 둘이 된다」. 놓친 보고를 backfill 하는 것(`catchUpEffects`)도 같은 자리에 있다.\n *\n * 30초면 충분하다. **아무도 안 기다린다** — 정상 경로가 이미 끝냈고, 이 그물은 남은 것만 본다.\n *\n * ── 판단은 여기 없다 ────────────────────────────────────────────────────────\n * 「지금 넘길 것인가, 다음은 언제인가」는 순수 함수가 답한다(§`retryDecisionOf`). 이 파일은\n * 표를 읽고 그 답대로 하고 결과를 적는 배선이다. 그렇게 갈라 둔 이유는 물러섬 규칙을 30분\n * 기다리지 않고 값으로 잴 수 있어야 하기 때문이다.\n *\n * ── 던지지 않는다 ───────────────────────────────────────────────────────────\n * 주기 안에서 돈다. 한 조치가 터져도 다음 조치를 봐야 하고, 이 그물이 터져도 규칙과 backfill 은\n * 돌아야 한다. 조치를 못 넘기는 것과 트윈이 멈추는 것은 다른 사고다.\n */\nimport { nextCommandState, type ApprovedCommand } from '@operato/ops-contract'\n\nimport { twinWarn } from '../../engine/log.js'\nimport { dispatch, type CommandAck, type CommandStore, type StoredCommand } from './command-dispatcher.js'\nimport { retryDecisionOf } from './retry-plan.js'\n\n/*\n * **의존을 인자로 받는다** — 이 파일이 커넥터 등록소·연결 표를 직접 부르지 않는다.\n *\n * 규칙 실행이 이미 그 모양이다(`runRulesOnce({ store, rulesOf, attentionsOf … })`). 그렇게 두는\n * 값이 둘이다: 배선을 시험이 굴릴 수 있고(직접 부르면 시험이 DB·모듈 부팅을 끌어온다 — 실제로\n * 그렇게 났다), 그리고 이 파일이 무엇에 의존하는지가 서명에 적힌다.\n */\n\n/** 한 바퀴에 볼 최대 수 — 대기 줄이 커도 한 주기를 독차지하지 않게. */\nexport const SWEEP_LIMIT = 50\n\nexport interface SweepReport {\n /** 표에서 읽은 수. */\n seen: number\n /** 실제로 넘긴 수. */\n dispatched: number\n /** 넘기지 않은 이유별 수 — 목록이 그대로 보인다. */\n skipped: Record<string, number>\n}\n\n/**\n * 이 테넌트의 대기 줄을 한 바퀴 훑는다.\n *\n * 저장소가 대기 줄을 못 내면 **아무것도 하지 않는다** — 그 능력이 없는 구현도 그대로 돌아야\n * 한다(옛 거동).\n */\nexport interface SweepDeps {\n store: CommandStore\n /** 이 조치를 저쪽에 낸다 — 부르는 쪽이 커넥터 배선을 안다. */\n /*\n * `dispatch` 가 승인 기록을 확인한 뒤의 커맨드를 넘겨 준다(`ApprovedCommand`) — 그 타입을 그대로\n * 받는다. 우리가 `StoredCommand` 로 넓히면 「승인 없이 나갈 수 있는 서명」이 된다.\n */\n actuate: (command: ApprovedCommand) => Promise<CommandAck>\n /** 이 연결이 두 번 보내도 지시가 하나인가 — **선언만 참으로 읽는다.** */\n idempotent: (instanceId: string) => Promise<boolean>\n /**\n * 우리 쪽 설정만으로 답할 수 있는 준비 상태. 저쪽을 두드리지 않는 답만 준다.\n *\n * 없으면 앞점검을 하지 않는다(그냥 넘긴다) — 모르는 것을 거절로 접지 않는 규율이다.\n */\n localReadiness?: (instanceId: string) => Promise<{ ready?: boolean; reason?: string; code?: string } | undefined>\n nowMs?: () => number\n}\n\nexport async function sweepPendingDispatch(domainId: string, deps: SweepDeps): Promise<SweepReport> {\n const { store } = deps\n const now = deps.nowMs ?? (() => Date.now())\n const report: SweepReport = { seen: 0, dispatched: 0, skipped: {} }\n if (!store.pendingDispatch) return report\n\n const pending = await store.pendingDispatch(domainId).catch(() => [] as readonly StoredCommand[])\n report.seen = pending.length\n\n const note = (why: string) => {\n report.skipped[why] = (report.skipped[why] ?? 0) + 1\n }\n\n for (const command of pending.slice(0, SWEEP_LIMIT)) {\n try {\n const decision = retryDecisionOf({\n state: command.state,\n dispatchable: nextCommandState(command.state, 'dispatch') !== undefined,\n retry: command.retry,\n attempts: command.attempts,\n nextAttemptAt: command.nextAttemptAt,\n idempotent: await deps.idempotent(command.instanceId).catch(() => false),\n nowMs: now()\n })\n\n if (!decision.take) {\n note(decision.skip ?? 'unknown')\n continue\n }\n\n /*\n * **넘기기 전에 우리 설정을 먼저 본다** — 아끼려는 것이 아니라 **이유를 지금 값으로\n * 적으려는** 것이다(인티그레이션 레인이 제 첫 근거를 깼다: 그쪽 어댑터는 이미 `fetch` 전에\n * 되돌아오므로 아낄 요청이 없다).\n *\n * 건너뛰면서 아무것도 안 적으면 **옛 `error` 가 남는다.** 저쪽 인증 정보 때문에 실패해 앉은\n * 뒤 우리 쪽 설정이 지워지면, 목록에 계속 저쪽 얘기가 보이고 사람이 남의 서버를 보러 간다.\n *\n * 모르면(`undefined`) 그냥 넘긴다 — 모르는 것을 거절로 접지 않는다.\n */\n const readiness = await deps.localReadiness?.(command.instanceId).catch(() => undefined)\n if (readiness && readiness.ready === false) {\n await noteBlocked(store, domainId, command, decision.nextAttemptAt, readiness.reason, readiness.code)\n note('not-ready')\n continue\n }\n\n const settled = await dispatch(store, domainId, command.id, c => deps.actuate(c))\n\n await recordAttempt(store, domainId, settled, decision.attempt ?? 1, decision.nextAttemptAt)\n\n if (settled.state === 'failed') {\n note('failed')\n twinWarn(`[twin-sweep] 조치 ${command.id} 를 넘기지 못했다: ${settled.error ?? '(사유 없음)'}`)\n } else {\n report.dispatched++\n }\n } catch (e: any) {\n /* 한 조치가 터져도 다음을 본다 — 가장 흔한 원인은 그 사이에 상태가 바뀐 것이다. */\n note('threw')\n twinWarn(`[twin-sweep] 조치 ${command.id} 에서 예외 — ${String(e?.message ?? e)}`)\n }\n }\n\n return report\n}\n\n/**\n * 못 나가는 이유를 **지금 값으로** 적는다 — 넘기지 않았으므로 상태는 그대로다.\n *\n * **바뀌었을 때만 쓴다.** 10분마다 같은 이유를 다시 쓰면 `updatedAt` 이 계속 튀고, 목록의\n * 「언제부터 이랬나」가 사라진다(§`catch-up-effects` 에서 같은 규율을 지켰다).\n */\nasync function noteBlocked(\n store: CommandStore,\n domainId: string,\n command: StoredCommand,\n nextAttemptAt: string | undefined,\n reason?: string,\n code?: string\n): Promise<void> {\n const sentence = reason ?? code ?? '연결 설정이 조치를 낼 준비가 되지 않았습니다'\n if (command.error === sentence && command.nextAttemptAt === nextAttemptAt) return\n\n await store\n .save(domainId, { ...command, error: sentence, ...(nextAttemptAt ? { nextAttemptAt } : {}) })\n .catch(err => twinWarn(`[twin-sweep] 조치 ${command.id} 의 사유를 적지 못했다 — ${String(err?.message ?? err)}`))\n}\n\n/** 시도 횟수와 다음 시각을 적는다 — `dispatch` 가 쓴 상태를 덮지 않고 이 둘만 더한다. */\nasync function recordAttempt(\n store: CommandStore,\n domainId: string,\n settled: StoredCommand,\n attempt: number,\n nextAttemptAt: string | undefined\n): Promise<void> {\n await store\n .save(domainId, { ...settled, attempts: attempt, ...(nextAttemptAt ? { nextAttemptAt } : {}) })\n .catch(err => twinWarn(`[twin-sweep] 조치 ${settled.id} 의 시도 기록을 적지 못했다 — ${String(err?.message ?? err)}`))\n}\n"]}
@@ -0,0 +1,120 @@
1
+ import { ListParam } from '@things-factory/shell';
2
+ export declare class TwinCommandResolver {
3
+ /**
4
+ * 커맨드 목록 — 기본은 **결정을 기다리는 것부터**.
5
+ *
6
+ * 정렬을 최근순으로 두지 않는다. 승인 대기 줄에서 사람이 찾는 것은 「새로 온 것」이 아니라
7
+ * **「오래 기다린 것」**이다 — 오래 기다린 조치가 뒤로 밀리면 그것이 가장 늦게 처리된다.
8
+ */
9
+ twinCommands(params: ListParam, context: ResolverContext): Promise<any>;
10
+ /**
11
+ * **이 조치에 대해 트윈이 지금 보는 것과 AI 의견** — 승인 화면이 부른다.
12
+ *
13
+ * ── 왜 조회인가 (2026-09-01) ───────────────────────────────────────────
14
+ * 결재선에 올릴 때 묻지 않는다. 그 자리는 트랜잭션 안이고 거기서 LLM 을 기다리면 DB 연결 하나를 몇 초
15
+ * 붙잡는다. 그리고 **승인하는 사람은 지금 판단한다** — 올린 시각의 의견은 낡았다.
16
+ *
17
+ * AI 는 판정하지 않는다. 이 값을 읽어 승인·거절을 막는 코드가 없다. 반대해도 승인되고 동의해도
18
+ * 거절된다 — 판정에 쓰면 AI 가 사실상 승인자가 되고, 그러면 사고 뒤에 책임을 물을 자리가 사라진다.
19
+ */
20
+ twinActuationOpinion(id: string,
21
+ /**
22
+ * AI 에게도 물을 것인가 — **누르면 묻는다.**
23
+ *
24
+ * 기본은 안 묻는다. 화면을 열 때마다 부르면 승인자 다섯 명이 두 번씩 열 때 한 건에 열 번이고,
25
+ * 대부분의 지시는 평범해서 그중 아홉 번은 같은 말을 다시 받는 것이다.
26
+ */
27
+ askAi: boolean, locale: string, context: ResolverContext): Promise<any>;
28
+ /**
29
+ * 조치를 낸다 — **대기 줄에 무언가가 들어오는 유일한 자리.**
30
+ *
31
+ * 결재선이 등록된 배포에서는 여기서 바로 결재선에 올라간다(§`ApprovalGateway`). 등록되지 않았으면
32
+ * 제안 상태로 남고, 이 리졸버의 직접 승인이 유일한 문이 된다. 어느 쪽인지는 답에 적어 준다 — 화면이
33
+ * 「어디서 승인하는가」를 사람에게 말해야 한다.
34
+ *
35
+ * 되돌릴 수 있는 조치는 결재선에 올리지 않는다. 계약이 그 판단을 든다(§`requiresApproval`).
36
+ */
37
+ proposeTwinCommand(instanceId: string, type: string, payload: any, context: ResolverContext): Promise<any>;
38
+ /**
39
+ * **사람이 결재선에 올린다** — 규칙이 낸 조치가 승인 앞에 서는 유일한 길.
40
+ *
41
+ * ── 왜 규칙이 직접 올리지 않나 ─────────────────────────────────────────
42
+ * 업무 목록의 결재선은 **올린 사람 기준**으로 풀린다 — `Myself` · `MyDepartment` · `MySupervisor` 가
43
+ * 전부 로그인한 사람을 본다. 배경 루프에는 「나」가 없으므로, 시스템 사용자를 지어내 올리면 그 셋이
44
+ * 시스템 사용자를 가리키고 결재선이 뜻을 잃는다.
45
+ *
46
+ * 그래서 규칙은 내는 것까지 하고, 여기서 사람이 올린다. **올린 사람이 「이것을 승인자들 앞에 놓았다」의
47
+ * 주체**가 되고, 그 사람 기준으로 결재선이 풀린다.
48
+ *
49
+ * 올린 사람은 자기가 올린 것을 승인할 수 있다 — 규칙이 낸 조치에는 낸 사람이 없으므로 승인자가 곧
50
+ * 두 번째 사람이다. 올린 것과 낸 것은 다르다.
51
+ */
52
+ raiseTwinCommand(id: string, context: ResolverContext): Promise<any>;
53
+ /**
54
+ * 승인한다 — **누가 승인했는지는 로그인한 사람에서 온다.**
55
+ *
56
+ * 인자로 받지 않는다. 받으면 다른 사람 이름으로 승인할 수 있고, 그러면 그 기록이 사고 뒤에 쓸 수
57
+ * 없어진다. 승인 기록의 값어치가 거기서 나온다.
58
+ */
59
+ approveTwinCommand(id: string, note: string, context: ResolverContext): Promise<any>;
60
+ /**
61
+ * 거절한다 — 여기서 끝난다.
62
+ *
63
+ * 사유를 요구한다. 거절은 현장이 기다리던 조치를 취소하는 것이고, 이유 없이 거절되면 낸 쪽이 다시
64
+ * 낸다. 그러면 같은 것이 대기 줄에 계속 돌아온다.
65
+ */
66
+ rejectTwinCommand(id: string, reason: string, context: ResolverContext): Promise<any>;
67
+ /**
68
+ * 어댑터에 넘긴다 — **승인을 지나지 않은 것은 여기서 막힌다.**
69
+ *
70
+ * ── 이름이 왜 `dispatchTwinCommand` 가 아닌가 (2026-09-01) ─────────────
71
+ * 그 이름은 이미 **커널 자극**이 쓴다(`TwinControlMutation.dispatchTwinCommand` —
72
+ * `order.hold`·`resource.repair` 를 커널에 넣는다). 같은 이름을 두 번 선언하면 type-graphql 이
73
+ * 오류 없이 하나만 남기고, 실제로 이 뮤테이션이 통째로 가려져 있었다 — 걸어 보고 알았다.
74
+ *
75
+ * 자극과 조치가 다른 어휘라는 것을 계약에서 갈라 놓고(§`propose` 의 거절), 정작 API 이름에서 겹쳐
76
+ * 두었다. 여기서는 조치임을 이름에 적는다.
77
+ *
78
+ * 화면이 이것을 따로 부르는 이유는, 승인과 넘김이 다른 순간이기 때문이다. 승인한 사람과 넘긴 시각이
79
+ * 같아야 할 이유가 없고, 어댑터가 죽어 있으면 승인은 남아 있어야 한다.
80
+ *
81
+ * ── 여기가 끊겨 있었다 (2026-09-02) ──────────────────────────────────
82
+ * 이 자리가 **어댑터를 찾지도 않고** 「구현이 없습니다」를 답했다. MES 커넥터는 그때 이미 `actuate`
83
+ * 를 다 만들어 두었다. 그 답을 사실로 읽어서 설계 문서에도, 다른 레인에도 그렇게 알렸다 — 둘 다
84
+ * 틀렸다.
85
+ *
86
+ * 이제 원본을 찾아 그 원본이 선언한 능력으로 판단한다(§`routeActuation`). 보내지 못하면 **무엇이
87
+ * 없어서인지** 말한다. 조용히 성공을 답하면 화면은 나간 줄로 읽고, 현장은 오지 않는 지시를 기다린다.
88
+ *
89
+ * ── 여기에는 트랜잭션을 걸지 않는다 ──────────────────────────────────
90
+ * 넘기기 전에 「넘기는 중」을 먼저 적는 것이 이 함수의 요점이다. 한 트랜잭션에 묶으면 그 기록이
91
+ * 커밋 전까지 아무에게도 보이지 않고, 넘기는 사이에 프로세스가 죽으면 그대로 사라진다. 그러면 저쪽은
92
+ * 받았는데 우리는 안 보낸 줄 알고 다시 보낸다 — 현장이 두 번 움직인다.
93
+ */
94
+ /**
95
+ * **이 조치를 넘길 수 있나** — 누르기 전에 답한다.
96
+ *
97
+ * ── 왜 이 자리가 필요한가 (2026-09-03) ────────────────────────────────────
98
+ * 판정 함수 둘을 만들어 두고 **부르는 자리를 안 만들었다**(`actuationTargetOf`·
99
+ * `actuationReadinessOf`). 그래서 화면이 물을 방법이 없었고, 못 넘기는 조치도 눌러 보고 알게 됐다.
100
+ * 인티그레이션 레인이 그 사실을 두 번 짚어 줬다 — 자리는 있고 길이 없는 그 부류다.
101
+ *
102
+ * 그동안 값이 실제로 없어졌다. 저쪽에 비밀값이 없던 날, 사람이 승인 화면까지 가서 누르고 실패를
103
+ * 보고 다시 로그인했다. **승인은 살아 있지만 사람의 시간은 돌아오지 않는다.**
104
+ *
105
+ * ── 둘을 함께 답한다 ──────────────────────────────────────────────────────
106
+ * 화면이 묻는 것은 하나다 — 「지금 넘겨도 되나」. 그 답이 두 겹이라 둘을 함께 싣는다.
107
+ *
108
+ * target 보낼 대상이 정해지나 — 망을 타지 않는다(원본·능력·사이트)
109
+ * readiness 저쪽이 받을 준비가 됐나 — 커넥터가 망을 탈 수 있다
110
+ *
111
+ * **문이 아니다.** 이 답으로 넘김을 막지 않는다(§`actuationReadinessOf`) — 화면이 미리 말하는
112
+ * 데까지다. 그래서 조회이고 뮤테이션이 아니다.
113
+ *
114
+ * 화면이 **목록을 그릴 때 부르지 말 것.** 커넥터가 저쪽에 물을 수 있어서 목록 한 줄마다 망을 탄다.
115
+ * 사람이 「넘기기」를 열 때 한 번이다.
116
+ */
117
+ twinActuationReadiness(id: string, context: ResolverContext): Promise<any>;
118
+ twinActuationOutcome(id: string, context: ResolverContext): Promise<any>;
119
+ dispatchTwinActuation(id: string, context: ResolverContext): Promise<any>;
120
+ }