@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,352 @@
1
+ /*
2
+ * **κ΄€μΈ‘μ—μ„œ 쑰치λ₯Ό λ‚Έλ‹€** β€” κ·Έ νŒμ • ν•˜λ‚˜.
3
+ *
4
+ * ── 무엇이 μ—†μ—ˆλ‚˜ (2026-09-01) ───────────────────────────────────────────
5
+ * 승인 게이트λ₯Ό μ„Έμš°κ³  λ‚˜λ‹ˆ **λ‚Ό 수 μžˆλŠ” 것이 μ‚¬λžŒλΏ**μ΄μ—ˆλ‹€. 계약은 λ‚Έ μͺ½μ„ λ„€ κ°€μ§€λ‘œ λ‘λŠ”λ°
6
+ * (`operator` Β· `rule` Β· `ai` Β· `schedule`) μ‚¬λžŒ λ§κ³ λŠ” 길이 μ—†μ—ˆλ‹€. 게이트의 κ°’μ–΄μΉ˜λŠ” 트윈이 슀슀둜
7
+ * μ•Œμ•„μ±ˆ 것을 μ‚¬λžŒμ΄ νŒλ‹¨ν•˜λŠ” 데 μžˆλŠ”λ°, μ•Œμ•„μ±ˆ 것을 올릴 μžλ¦¬κ°€ μ—†μ—ˆλ‹€.
8
+ *
9
+ * ── 쑰건을 μƒˆλ‘œ λ§Œλ“€μ§€ μ•ŠλŠ”λ‹€ ────────────────────────────────────────────
10
+ * 컀널이 이미 μƒνƒœμ—μ„œ γ€Œλ΄μ•Ό ν•  것」을 νŒλ‹¨ν•΄ `Attention` 으둜 λ‚Έλ‹€ β€” μ•ˆμ •λœ μ’…λ₯˜ μ½”λ“œ(`kind`),
11
+ * 무엇에 λŒ€ν•œ 것인지(`anchor`), μ–Έμ œλΆ€ν„°μΈμ§€(`timeStamp`), ν‘œν˜„μš© κ°’(`params`). 쑰건 μ–Έμ–΄λ₯Ό μƒˆλ‘œ λ§Œλ“€λ©΄
12
+ * 같은 νŒλ‹¨μ΄ 두 벌이 되고, ν•œμͺ½λ§Œ κ³ μ³μ§€λŠ” 날이 μ˜¨λ‹€.
13
+ *
14
+ * κ·Έλž˜μ„œ κ·œμΉ™μ€ **μ–΄λŠ μ£Όμ˜μ— μ–΄λŠ μ§€μ‹œλ₯Ό λΆ™μ΄λŠ”κ°€**만 μ •ν•œλ‹€.
15
+ *
16
+ * ── 자극과 μ§€μ‹œλ₯Ό μž‡μ§€ μ•ŠλŠ”λ‹€ ────────────────────────────────────────────
17
+ * μ£Όμ˜μ—λŠ” `suggestedAction.command` κ°€ μ‹€λ € μžˆμ§€λ§Œ 그것은 **컀널 μ•ˆμ—μ„œ λ„λŠ” 자극**이닀
18
+ * (`resource.repair` = λͺ¨λΈμ—μ„œ 수리된 κ²ƒμœΌλ‘œ λ‘”λ‹€). μ—¬κΈ°μ„œ λ‚΄λŠ” 것은 ν˜„μž₯에 λ‚˜κ°€λŠ” μ§€μ‹œλ‹€. κ·Έ 값을
19
+ * κ·ΈλŒ€λ‘œ μ“°λ©΄ γ€Œλͺ¨λΈμ„ κ·Έλ ‡κ²Œ 두라」가 γ€Œμ‚¬λžŒμ„ 보내라」가 λœλ‹€(Β§`propose` 의 거절).
20
+ *
21
+ * ── λ¬΄λ„ˆμ§€λŠ” 것 λ„· ───────────────────────────────────────────────────────
22
+ * ν–‰λ³΅ν•œ 길은 μ§§λ‹€. μ‹€μ œλ‘œ λ¬΄λ„ˆμ§€λŠ” 것은 이것듀이닀.
23
+ *
24
+ * 같은 쑰건이 λ§€ μ£ΌκΈ° λ‹€μ‹œ μ˜¬λΌμ˜¨λ‹€ λŒ€κΈ° 쀄에 같은 μ§€μ‹œκ°€ μˆ˜μ‹­ 개 μ„ λ‹€
25
+ * 방금 생긴 쑰건에 λ°”λ‘œ μ˜¬λ¦°λ‹€ 깜빑인 것에 ν˜„μž₯ μ§€μ‹œκ°€ λ‚˜κ°„λ‹€
26
+ * 쑰건이 사라진 뒀에도 λŒ€κΈ°μ— λ‚¨λŠ”λ‹€ μŠΉμΈν•˜λŠ” μ‚¬λžŒμ΄ μ—†λŠ” 문제λ₯Ό νŒλ‹¨ν•œλ‹€
27
+ * ν•œ λ²ˆμ— 수백 κ°œκ°€ μ˜¬λΌμ˜¨λ‹€ μ‚¬λžŒμ΄ 읽을 수 μ—†κ³ , 읽지 μ•ŠμœΌλ©΄ κ²Œμ΄νŠΈκ°€ μ‹œλŠ‰μ΄ λœλ‹€
28
+ *
29
+ * λ„· λ‹€ 였λ₯˜κ°€ λ‚˜μ§€ μ•ŠλŠ”λ‹€. κ·Έλž˜μ„œ 이 파일이 넷을 닀룬닀.
30
+ */
31
+ import type { CommandTypeSpec } from '@operato/ops-contract'
32
+
33
+ /** μ–΄λŠ μ£Όμ˜μ— μ–΄λŠ μ§€μ‹œλ₯Ό λΆ™μ΄λŠ”κ°€ β€” μ„ μ–Έ ν•œ 쀄. */
34
+ export interface ActuationRuleSpec {
35
+ id: string
36
+ /** 이 νŠΈμœˆμ—λ§Œ μ μš©λœλ‹€. νŠΈμœˆλ§ˆλ‹€ 라인이 λ‹€λ₯΄λ―€λ‘œ μ „μ—­ κ·œμΉ™μ€ 두지 μ•ŠλŠ”λ‹€. */
37
+ instanceId: string
38
+ /** μ–΄λŠ μ£Όμ˜μΈκ°€ β€” μ»€λ„μ˜ μ•ˆμ • μ½”λ“œ(`Attention.kind`). */
39
+ attentionKind: string
40
+ /** 이 심각도 이상일 λ•Œλ§Œ β€” μ—†μœΌλ©΄ μ’…λ₯˜λ§Œ λ³Έλ‹€. */
41
+ minSeverity?: 'low' | 'medium' | 'high' | 'critical'
42
+ /**
43
+ * **쑰건이 이만큼 μ΄μ–΄μ‘Œμ„ λ•Œ** μ˜¬λ¦°λ‹€.
44
+ *
45
+ * μ—†μœΌλ©΄ 처음 λ³Έ 주기에 λ°”λ‘œ μ˜¬λ¦°λ‹€. κΉœλΉ‘μ΄λŠ” 쑰건에 ν˜„μž₯ μ§€μ‹œκ°€ λ‚˜κ°€λŠ” 것을 λ§‰μœΌλ €λ©΄ 이 값이 ν•„μš”ν•˜λ‹€
46
+ * β€” 컀널이 쑰건이 처음 μ„±λ¦½ν•œ μ‹œκ°μ„ κΈ°μ–΅ν•˜λ―€λ‘œ(`Attention.timeStamp`) μ—¬κΈ°μ„œ 잴 수 μžˆλ‹€.
47
+ */
48
+ minHeldMs?: number
49
+ /** 무엇을 λ‚΄λŠ”κ°€ β€” μ„ μ–Έλœ 쑰치 μ’…λ₯˜(`CommandTypeSpec.type`). 컀널 자극이 μ•„λ‹ˆλ‹€. */
50
+ commandType: string
51
+ /**
52
+ * μ§€μ‹œμ˜ λ‚΄μš© β€” `{이름}` 을 주의의 κ°’μœΌλ‘œ λ°”κΏ” λ„£λŠ”λ‹€.
53
+ *
54
+ * λ°”κΏ€ 값은 `Attention.params` 와 `anchor` μ—μ„œ μ˜¨λ‹€(`{locationId}` Β· `{ratioPct}`). ν•œ κ²Ή μ€‘κ΄„ν˜Έλ₯Ό
55
+ * μ“°λŠ” 것은 이 μ €μž₯μ†Œμ˜ i18n 관둀와 κ°™λ‹€.
56
+ *
57
+ * λ°”κΏ€ 것이 μ—†μœΌλ©΄ **κ·Έ 자리λ₯Ό λΉ„μš΄λ‹€** β€” μ§€μ–΄λ‚Έ 값을 λ„£μ§€ μ•ŠλŠ”λ‹€. μš”κ΅¬ν•˜λŠ” 칸이 λΉ„λ©΄ 계약이 κ±°μ ˆν•˜κ³ 
58
+ * (Β§`missingCommandPayload`) κ·Έ 거절이 μ‚¬λžŒμ—κ²Œ μ „ν•΄μ§„λ‹€.
59
+ */
60
+ payloadTemplate?: Record<string, unknown>
61
+ /**
62
+ * **ν•œ 번 λ‚Έ 근거에 λ‹€μ‹œ λ‚΄κΈ°κΉŒμ§€ κΈ°λ‹€λ¦¬λŠ” μ‹œκ°„.**
63
+ *
64
+ * 이것이 μ—†μœΌλ©΄ μŠΉμΈν•˜λŠ” μˆœκ°„ κ·œμΉ™μ΄ 같은 것을 또 λ‚Έλ‹€ β€” μ‹€μ œλ‘œ κ·Έλž¬λ‹€. γ€Œκ²°μ •μ„ κΈ°λ‹€λ¦¬λŠ” κ²ƒγ€λ§Œ 보고
65
+ * 쀑볡을 λ§‰μ•˜λŠ”λ°, μŠΉμΈν•˜λ©΄ κ·Έ μ»€λ§¨λ“œκ°€ λŒ€κΈ°μ—μ„œ λΉ μ§€κ³  쑰건은 κ·ΈλŒ€λ‘œ μ„œ 있기 λ•Œλ¬Έμ΄λ‹€. μ‚¬λžŒμ΄
66
+ * μŠΉμΈν• μˆ˜λ‘ λŒ€κΈ° 쀄이 λ‹€μ‹œ μ°¨λŠ” λͺ¨μ–‘이 λœλ‹€.
67
+ *
68
+ * 쑰건은 μ΄μ–΄μ§€λŠ” 것이고 μ‘°μΉ˜λŠ” μ‹œκ°„μ΄ κ±Έλ¦°λ‹€. λ‚Έ μ‘°μΉ˜κ°€ 효과λ₯Ό λ‚Ό λ•ŒκΉŒμ§€ κΈ°λ‹€λ¦¬λŠ” μ‹œκ°„μ΄ 이 값이닀.
69
+ */
70
+ cooldownMs?: number
71
+ /** κΊΌ λ‘” κ·œμΉ™μ€ νŒμ •ν•˜μ§€ μ•ŠλŠ”λ‹€. μ§€μš°μ§€ μ•Šκ³  끌 수 μžˆμ–΄μ•Ό ν•œλ‹€. */
72
+ enabled?: boolean
73
+ }
74
+
75
+ /** νŒμ •μ— λ“€μ–΄κ°€λŠ” 주의 β€” 컀널이 λ‚Έ λͺ¨μ–‘ κ·ΈλŒ€λ‘œ 쀑 ν•„μš”ν•œ κ²ƒλ§Œ. */
76
+ export interface AttentionLike {
77
+ id: string
78
+ kind: string
79
+ severity: string
80
+ state?: string
81
+ anchor?: { locationId?: string; moverId?: string; orderId?: string; operation?: string }
82
+ timeStamp?: string
83
+ params?: Record<string, string | number>
84
+ }
85
+
86
+ /** 올릴 것 ν•˜λ‚˜. */
87
+ export interface RuleProposal {
88
+ ruleId: string
89
+ attentionId: string
90
+ attentionKind: string
91
+ commandType: string
92
+ payload: Record<string, unknown>
93
+ }
94
+
95
+ /** μ˜¬λ¦¬μ§€ μ•Šμ€ 것과 κ·Έ 이유 β€” **λ§ν•˜μ§€ μ•ŠμœΌλ©΄ μ‚¬λžŒμ€ κ·œμΉ™μ΄ λ„λŠ” 쀄 μ•ˆλ‹€.** */
96
+ export interface RuleSkip {
97
+ ruleId: string
98
+ attentionId?: string
99
+ reason: string
100
+ }
101
+
102
+ export interface RuleVerdict {
103
+ proposals: RuleProposal[]
104
+ /** μ™œ μ•ˆ μ˜¬λ Έλ‚˜. 쑰용히 λ„˜μ–΄κ°€μ§€ μ•ŠλŠ”λ‹€. */
105
+ skips: RuleSkip[]
106
+ /** μƒν•œμ— λ‹Ώμ•„ μž˜λžλ‚˜ β€” 잘랐으면 λͺ‡ 개λ₯Ό 두고 μ™”λ‚˜. */
107
+ withheld?: number
108
+ }
109
+
110
+ const SEVERITY_ORDER = ['low', 'medium', 'high', 'critical'] as const
111
+
112
+ /** 심각도가 문턱에 λ‹Ώλ‚˜ β€” λͺ¨λ₯΄λŠ” 값은 λ‹Ώμ§€ μ•ŠλŠ” κ²ƒμœΌλ‘œ λ³Έλ‹€(λͺ¨λ¦„을 ν†΅κ³Όλ‘œ 읽지 μ•ŠλŠ”λ‹€). */
113
+ function severityMeets(actual: string, min: string | undefined): boolean {
114
+ if (!min) return true
115
+ const a = SEVERITY_ORDER.indexOf(actual as any)
116
+ const m = SEVERITY_ORDER.indexOf(min as any)
117
+ if (a < 0 || m < 0) return false
118
+ return a >= m
119
+ }
120
+
121
+ /**
122
+ * `{이름}` 을 λ°”κΏ” λ„£λŠ”λ‹€ β€” λ°”κΏ€ 값이 μ—†μœΌλ©΄ **κ·Έ 칸을 λΊ€λ‹€.**
123
+ *
124
+ * μ›λž˜ λ¬Έμžμ—΄μ„ κ·ΈλŒ€λ‘œ 두면 `{quantity}` 같은 κΈ€μžκ°€ μ§€μ‹œμ˜ μˆ˜λŸ‰μœΌλ‘œ λ“€μ–΄κ°€κ³ , κ·Έ μ§€μ‹œκ°€ ν˜„μž₯에 λ‚˜κ°„λ‹€.
125
+ * λΉΌλ©΄ 계약이 γ€Œμš”κ΅¬ν•˜λŠ” 칸이 μ—†λ‹€γ€λ‘œ κ±°μ ˆν•˜κ³  κ·Έ 거절이 μ‚¬λžŒμ—κ²Œ μ „ν•΄μ§„λ‹€.
126
+ */
127
+ function fill(template: Record<string, unknown> | undefined, values: Record<string, string | number>): {
128
+ payload: Record<string, unknown>
129
+ missing: string[]
130
+ } {
131
+ const payload: Record<string, unknown> = {}
132
+ const missing: string[] = []
133
+ if (!template) return { payload, missing }
134
+
135
+ for (const [key, raw] of Object.entries(template)) {
136
+ if (typeof raw !== 'string') {
137
+ payload[key] = raw
138
+ continue
139
+ }
140
+
141
+ /* κ°’ ν•˜λ‚˜λ₯Ό κ·ΈλŒ€λ‘œ λ‚˜λ₯΄λŠ” 자리 β€” 수λ₯Ό λ¬Έμžμ—΄λ‘œ λ§Œλ“€μ§€ μ•ŠλŠ”λ‹€(μˆ˜λŸ‰μ΄ λ¬Έμžμ—΄μ΄λ©΄ 계산이 λ¬΄λ„ˆμ§„λ‹€). */
142
+ const whole = /^\{([A-Za-z0-9_.]+)\}$/.exec(raw)
143
+ if (whole) {
144
+ const name = whole[1]!
145
+ if (!(name in values)) {
146
+ missing.push(name)
147
+ continue
148
+ }
149
+ payload[key] = values[name]
150
+ continue
151
+ }
152
+
153
+ let gap = false
154
+ const text = raw.replace(/\{([A-Za-z0-9_.]+)\}/g, (_m, name: string) => {
155
+ if (name in values) return String(values[name])
156
+ missing.push(name)
157
+ gap = true
158
+ return ''
159
+ })
160
+ if (!gap) payload[key] = text
161
+ }
162
+
163
+ return { payload, missing }
164
+ }
165
+
166
+ /** λ°”κΏ” 넣을 κ°’ β€” 주의의 `params` 와 `anchor` μ—μ„œ. */
167
+ function valuesOf(attention: AttentionLike): Record<string, string | number> {
168
+ const out: Record<string, string | number> = { ...(attention.params ?? {}) }
169
+ for (const [key, value] of Object.entries(attention.anchor ?? {})) {
170
+ if (value !== undefined && value !== null && value !== '') out[key] = value as string
171
+ }
172
+ out.attentionId = attention.id
173
+ out.attentionKind = attention.kind
174
+ return out
175
+ }
176
+
177
+ /**
178
+ * μ΄λ²ˆμ— 무엇을 μ˜¬λ¦΄μ§€ μ •ν•œλ‹€ β€” **순수 ν•¨μˆ˜**. μ €μž₯μ†Œλ„ μ‹œκ³„λ„ 보지 μ•ŠλŠ”λ‹€.
179
+ *
180
+ * μ‹œν—˜μ΄ μž¬κΈ°λ™κ³Ό μƒν•œκ³Ό 쀑볡을 λ‹€ 걸을 수 μžˆμ–΄μ•Ό ν•˜κ³ , 그러렀면 이 νŒμ •μ΄ λ°–μ—μ„œ 값을 λ°›μ•„μ•Ό ν•œλ‹€.
181
+ * 이 μ €μž₯μ†Œμ—μ„œ κ°€μž₯ 자주 λ‚˜λŠ” 결함이 γ€Œμžλ¦¬λŠ” 있고 길이 없닀」이고, κ²Œμ΄νŠΈμ— λ¬Όλ¦¬λŠ” κ²ƒμ΄λ―€λ‘œ μ—¬κΈ°μ„œ
182
+ * λ‚˜λ©΄ κ²°κ³Όκ°€ ν˜„μž₯의 μ§€μ‹œλ‹€.
183
+ */
184
+ export function evaluateRules(args: {
185
+ rules: readonly ActuationRuleSpec[]
186
+ attentions: readonly AttentionLike[]
187
+ /** μ„ μ–Έλœ 쑰치 μ’…λ₯˜ β€” μ—†λŠ” μ’…λ₯˜λ₯Ό κ°€λ¦¬ν‚€λŠ” κ·œμΉ™μ€ μ—¬κΈ°μ„œ κ±Έλ¦°λ‹€. */
188
+ specs: readonly CommandTypeSpec[]
189
+ /**
190
+ * **이미 닀루고 μžˆλŠ” κ·Όκ±°λ“€** β€” κ·Όκ±°λ§ˆλ‹€ λ§ˆμ§€λ§‰μœΌλ‘œ 쑰치λ₯Ό λ‚Έ μ‹œκ°.
191
+ *
192
+ * 결정을 κΈ°λ‹€λ¦¬λŠ” κ²ƒλ§Œ 보면 λΆ€μ‘±ν•˜λ‹€. μŠΉμΈν•˜λŠ” μˆœκ°„ κ·Έ μ»€λ§¨λ“œκ°€ λŒ€κΈ°μ—μ„œ λΉ μ§€κ³  쑰건은 κ·ΈλŒ€λ‘œ
193
+ * μ„œ μžˆμ–΄μ„œ, λ‹€μŒ 바퀴가 같은 것을 또 λ‚Έλ‹€ β€” κ±Έμ–΄ 보고 κ·Έ μƒνƒœλ₯Ό μ‹€μ œλ‘œ λ§Œλ“€μ—ˆλ‹€.
194
+ *
195
+ * κ·Έλž˜μ„œ **λ‚Έ μ‹œκ°**을 λ°›λŠ”λ‹€. 결정을 κΈ°λ‹€λ¦¬λŠ” 것은 μ‹œκ°κ³Ό λ¬΄κ΄€ν•˜κ²Œ λ§‰λŠ”λ‹€(Β§`openCauses`).
196
+ * μ €μž₯μ†Œμ—μ„œ 읽어 λ„˜κΈ΄λ‹€ β€” ν”„λ‘œμ„ΈμŠ€ 기얡에 두면 μž¬κΈ°λ™μ΄ κ³§ 쀑볡이 λœλ‹€.
197
+ */
198
+ openCauses: ReadonlySet<string>
199
+ /** κ·Όκ±° β†’ λ§ˆμ§€λ§‰μœΌλ‘œ 쑰치λ₯Ό λ‚Έ μ‹œκ°(ms). μ—†μœΌλ©΄ λ‚Έ 적이 μ—†λ‹€. */
200
+ lastProposedAt?: ReadonlyMap<string, number>
201
+ /** μ§€κΈˆ β€” 쑰건이 μ–Όλ§ˆλ‚˜ μ΄μ–΄μ‘ŒλŠ”μ§€ μž¬λŠ” κΈ°μ€€. */
202
+ nowMs: number
203
+ /** ν•œ λ²ˆμ— λͺ‡ κ°œκΉŒμ§€ μ˜¬λ¦¬λ‚˜. λ„˜λŠ” 것은 두고 였고, 두고 온 수λ₯Ό λ§ν•œλ‹€. */
204
+ maxPerRun?: number
205
+ }): RuleVerdict {
206
+ const { rules, attentions, specs, openCauses, nowMs } = args
207
+ const maxPerRun = args.maxPerRun ?? DEFAULT_MAX_PER_RUN
208
+
209
+ const proposals: RuleProposal[] = []
210
+ const skips: RuleSkip[] = []
211
+ let withheld = 0
212
+
213
+ /* ν•œ μ£Όμ˜μ— κ·œμΉ™μ΄ λ‘˜ λΆ™μ–΄ μžˆμ–΄λ„ μ‘°μΉ˜λŠ” ν•˜λ‚˜λ§Œ μ˜¬λ¦°λ‹€ β€” 같은 쑰건에 두 μ§€μ‹œκ°€ λ‚˜κ°€λ©΄ ν˜„μž₯이 두 번 움직인닀. */
214
+ const claimed = new Set<string>(openCauses)
215
+
216
+ for (const rule of rules) {
217
+ if (rule.enabled === false) continue
218
+
219
+ const gap = commandTypeGap(rule.commandType, specs)
220
+ if (gap) {
221
+ skips.push({ ruleId: rule.id, reason: gap })
222
+ continue
223
+ }
224
+ const spec = specs.find(s => s.type === rule.commandType)!
225
+
226
+ for (const attention of attentions) {
227
+ if (attention.kind !== rule.attentionKind) continue
228
+
229
+ /* ν™•μΈλœ 것과 사라진 κ²ƒμ—λŠ” μ˜¬λ¦¬μ§€ μ•ŠλŠ”λ‹€ β€” μ‚¬λžŒμ΄ 이미 λ³Έ 것이닀. */
230
+ if (attention.state && attention.state !== 'active') continue
231
+
232
+ if (!severityMeets(attention.severity, rule.minSeverity)) continue
233
+
234
+ if (claimed.has(attention.id)) continue
235
+
236
+ /*
237
+ * λ‚Έ μ§€ μ–Όλ§ˆ μ•ˆ 됐으면 κΈ°λ‹€λ¦°λ‹€. 쑰건은 μ΄μ–΄μ§€λŠ” 것이고 μ‘°μΉ˜λŠ” μ‹œκ°„μ΄ κ±Έλ¦°λ‹€ β€” 효과λ₯Ό 보기 전에
238
+ * 또 λ‚΄λ©΄ λŒ€κΈ° 쀄이 같은 μ§€μ‹œλ‘œ μ°¬λ‹€.
239
+ */
240
+ const lastAt = args.lastProposedAt?.get(attention.id)
241
+ const cooldown = rule.cooldownMs ?? DEFAULT_COOLDOWN_MS
242
+ if (lastAt !== undefined && cooldown > 0 && nowMs - lastAt < cooldown) continue
243
+
244
+ const held = heldMs(attention, nowMs)
245
+ if (rule.minHeldMs && rule.minHeldMs > 0) {
246
+ if (held === undefined) {
247
+ /*
248
+ * μ–Έμ œλΆ€ν„°μΈμ§€ λͺ¨λ₯΄λŠ” μ£Όμ˜λ‹€. **λͺ¨λ¦„을 γ€ŒμΆ©λΆ„νžˆ μ˜€λž˜λλ‹€γ€λ‘œ 읽지 μ•ŠλŠ”λ‹€** β€” κ·Έλ ‡κ²Œ 읽으면
249
+ * 방금 생긴 κΉœλΉ‘μž„μ— ν˜„μž₯ μ§€μ‹œκ°€ λ‚˜κ°„λ‹€.
250
+ */
251
+ skips.push({
252
+ ruleId: rule.id,
253
+ attentionId: attention.id,
254
+ reason: `이 μ£Όμ˜λŠ” μ–Έμ œλΆ€ν„°μΈμ§€ λ§ν•˜μ§€ μ•ŠλŠ”λ‹€ β€” ${rule.minHeldMs}ms μ΄μ–΄μ‘ŒλŠ”μ§€ μ•Œ 수 μ—†μ–΄ μ˜¬λ¦¬μ§€ μ•Šμ•˜λ‹€`
255
+ })
256
+ continue
257
+ }
258
+ if (held < rule.minHeldMs) continue
259
+ }
260
+
261
+ if (proposals.length >= maxPerRun) {
262
+ withheld++
263
+ continue
264
+ }
265
+
266
+ const { payload, missing } = fill(rule.payloadTemplate, valuesOf(attention))
267
+ if (missing.length) {
268
+ skips.push({
269
+ ruleId: rule.id,
270
+ attentionId: attention.id,
271
+ reason: `μ§€μ‹œμ— 넣을 값이 μ£Όμ˜μ— μ—†λ‹€ (${[...new Set(missing)].join(', ')}) β€” μ§€μ–΄λ‚Έ κ°’μœΌλ‘œ ν˜„μž₯ μ§€μ‹œλ₯Ό λ§Œλ“€μ§€ μ•Šμ•˜λ‹€`
272
+ })
273
+ continue
274
+ }
275
+
276
+ const gaps = missingRequired(spec, payload)
277
+ if (gaps.length) {
278
+ skips.push({
279
+ ruleId: rule.id,
280
+ attentionId: attention.id,
281
+ reason: `μ§€μ‹œκ°€ μš”κ΅¬ν•˜λŠ” 칸이 λΉ„μ—ˆλ‹€ (${gaps.join(', ')}) β€” 무엇을 μ§€μ‹œν•˜λŠ”μ§€ λͺ¨λ₯΄λ©΄ μŠΉμΈν•  κ·Όκ±°κ°€ μ—†λ‹€`
282
+ })
283
+ continue
284
+ }
285
+
286
+ claimed.add(attention.id)
287
+ proposals.push({
288
+ ruleId: rule.id,
289
+ attentionId: attention.id,
290
+ attentionKind: attention.kind,
291
+ commandType: rule.commandType,
292
+ payload
293
+ })
294
+ }
295
+ }
296
+
297
+ return { proposals, skips, ...(withheld ? { withheld } : {}) }
298
+ }
299
+
300
+ /** ν•œ λ²ˆμ— μ˜¬λ¦¬λŠ” μƒν•œ β€” μ‚¬λžŒμ΄ 읽을 수 μžˆλŠ” 수. 읽지 μ•ŠλŠ” λŒ€κΈ° 쀄은 κ²Œμ΄νŠΈκ°€ μ•„λ‹ˆλ‹€. */
301
+ export const DEFAULT_MAX_PER_RUN = 20
302
+
303
+ /**
304
+ * 같은 근거에 λ‹€μ‹œ λ‚΄κΈ°κΉŒμ§€ β€” **μ„ μ–Έν•˜μ§€ μ•Šμ•„λ„ κΈ°λ‹€λ¦°λ‹€.**
305
+ *
306
+ * 기본을 0 으둜 두면 μ„ μ–Έν•˜μ§€ μ•Šμ€ κ·œμΉ™μ΄ μŠΉμΈν•  λ•Œλ§ˆλ‹€ 같은 것을 또 λ‚Έλ‹€. κ·Έ 결함을 κΈ°λ³Έκ°’μœΌλ‘œ 두지
307
+ * μ•ŠλŠ”λ‹€. 더 빨리 λ‚΄μ•Ό ν•˜λŠ” κ·œμΉ™μ€ 슀슀둜 짧게 μ λŠ”λ‹€.
308
+ */
309
+ export const DEFAULT_COOLDOWN_MS = 60 * 60_000
310
+
311
+ /**
312
+ * 이 κ·œμΉ™μ΄ λ‚Ό 수 μžˆλŠ” μ’…λ₯˜μΈκ°€ β€” **νŒμ •μ΄ ν•œ μžλ¦¬μ—λ§Œ 있게.**
313
+ *
314
+ * μ €μž₯ν•  λ•Œμ™€ 돌 λ•Œκ°€ 같은 것을 봐야 ν•œλ‹€. 두 벌이면 μ €μž₯은 λ˜λŠ”λ° μ•ˆ λ„λŠ” κ·œμΉ™μ΄ 생기고, μ‚¬λžŒμ€ κ·œμΉ™μ΄
315
+ * κ±Έλ¦° μ€„λ‘œ μ½λŠ”λ‹€.
316
+ *
317
+ * `stimuli` λ₯Ό λ°›λŠ” 이유: 컀널 자극(`CMD`)을 μ μ—ˆμ„ λ•Œ **μ™œ μ•ˆ λ˜λŠ”μ§€**λ₯Ό λ§ν•˜λ €λ©΄ κ·Έ μ–΄νœ˜λ₯Ό μ•Œμ•„μ•Ό ν•œλ‹€.
318
+ * γ€Œμ„ μ–Έλ˜μ§€ μ•Šμ•˜λ‹€γ€λ‘œλ§Œ λ‹΅ν•˜λ©΄ λΆ€λ₯΄λŠ” μͺ½μ€ 선언이 λΉ μ§„ μ€„λ‘œ 읽고 선언을 μΆ”κ°€ν•œλ‹€ β€” κ·Έ μˆœκ°„ λͺ¨λΈ μ•ˆμ˜
319
+ * 자극이 ν˜„μž₯ μ§€μ‹œκ°€ λœλ‹€.
320
+ */
321
+ export function commandTypeGap(
322
+ commandType: string,
323
+ specs: readonly CommandTypeSpec[],
324
+ stimuli?: readonly string[]
325
+ ): string | undefined {
326
+ if (specs.some(s => s.type === commandType)) return undefined
327
+
328
+ if (stimuli?.includes(commandType)) {
329
+ return (
330
+ `그것은 컀널 μ•ˆμ—μ„œ λ„λŠ” μžκ·Ήμ΄λ‹€ (${commandType}) β€” ν˜„μž₯에 λ‚˜κ°€λŠ” μ§€μ‹œκ°€ μ•„λ‹ˆλ‹€. ` +
331
+ 'λͺ¨λΈ μƒνƒœλ₯Ό λ°”κΎΈλ €λ©΄ 컀널에 μžκ·Ήμ„ λ„£κ³ , ν˜„μž₯에 무언가λ₯Ό μ‹œν‚€λ €λ©΄ κ·Έ μ§€μ‹œλ₯Ό 쑰치 μ’…λ₯˜λ‘œ μ„ μ–Έν•œλ‹€'
332
+ )
333
+ }
334
+
335
+ return `μ„ μ–Έλ˜μ§€ μ•Šμ€ 쑰치 μ’…λ₯˜λ₯Ό 가리킨닀 (${commandType}) β€” μ„ μ–Έλœ 것: ${specs.map(s => s.type).join(', ') || '(μ—†μŒ)'}`
336
+ }
337
+
338
+ /** 쑰건이 μ–Όλ§ˆλ‚˜ μ΄μ–΄μ‘Œλ‚˜ β€” λͺ¨λ₯΄λ©΄ `undefined`(0 이 μ•„λ‹ˆλ‹€). */
339
+ function heldMs(attention: AttentionLike, nowMs: number): number | undefined {
340
+ if (!attention.timeStamp) return undefined
341
+ const at = Date.parse(attention.timeStamp)
342
+ if (!Number.isFinite(at)) return undefined
343
+ return Math.max(0, nowMs - at)
344
+ }
345
+
346
+ /** κ³„μ•½μ˜ μš”κ΅¬λ₯Ό κ·ΈλŒ€λ‘œ λ³Έλ‹€ β€” νŒμ •μ„ μ—¬κΈ°μ„œ λ‹€μ‹œ μ“°μ§€ μ•ŠλŠ”λ‹€. */
347
+ function missingRequired(spec: CommandTypeSpec, payload: Record<string, unknown>): string[] {
348
+ return (spec.required ?? []).filter(key => {
349
+ const v = payload[key]
350
+ return v === undefined || v === null || v === ''
351
+ })
352
+ }
@@ -0,0 +1,160 @@
1
+ /*
2
+ * κ·œμΉ™μ„ 주기둜 λŒλ¦¬λŠ” 타이머 ν•˜λ‚˜.
3
+ *
4
+ * ── μ™œ 컀널 틱이 μ•„λ‹Œκ°€ ──────────────────────────────────────────────────
5
+ * `tickGuarded` λŠ” μ˜ˆμ™Έκ°€ λ‚˜λ©΄ **κ·Έ νŠΈμœˆμ„ λ©ˆμΆ˜λ‹€.** κ·œμΉ™ ν•˜λ‚˜κ°€ 잘λͺ»λλ‹€κ³  곡μž₯이 λ©ˆμΆ”λ©΄ μ•ˆ λœλ‹€.
6
+ * 그리고 κ΅ΆκΉ€ κ°μ‹œκ°€ 느린 ν‹± 셋에 νŠΈμœˆμ„ λ‚΄λ¦¬λŠ”λ°, κ·œμΉ™μ€ DB λ₯Ό 두 번 κ±΄λ“œλ¦¬λ―€λ‘œ κ·Έ κ°μ‹œμ— κ±Έλ¦°λ‹€.
7
+ *
8
+ * ── μ™œ 이 간격인가 ───────────────────────────────────────────────────────
9
+ * κ·œμΉ™μ΄ λ³΄λŠ” 것은 **였래 이어진 쑰건**이닀. γ€Œλ³‘λͺ©μ΄ μ„Έ μ‹œκ°„μ§Έγ€λ₯Ό 1μ΄ˆλ§ˆλ‹€ λ‹€μ‹œ 물을 μ΄μœ κ°€ μ—†λ‹€.
10
+ * 짧게 작으면 μ–»λŠ” 것 없이 DB λ₯Ό 두 λ²ˆμ”© 더 μΉœλ‹€.
11
+ *
12
+ * ── 겹쳐 λŒμ§€ μ•ŠλŠ”λ‹€ ─────────────────────────────────────────────────────
13
+ * ν•œ 바퀴가 간격보닀 였래 걸리면 λ‹€μŒ λ°œν™”κ°€ κ²ΉμΉœλ‹€. 겹치면 두 바퀴가 같은 κ·Όκ±° 집합을 읽고 **같은
14
+ * 쑰치λ₯Ό 두 벌 λ‚Έλ‹€** β€” 쀑볡을 λ§‰λŠ” μž₯μΉ˜κ°€ κ·Έ μˆœκ°„ ν†΅κ³Όλœλ‹€.
15
+ */
16
+ import { MES_COMMANDS } from '@operato/ops-contract'
17
+
18
+ import { twinError, twinLog } from '../../engine/log.js'
19
+ import { TwinEngine } from '../../engine/twin-engine.js'
20
+
21
+ import { capabilitiesOf, getAdapter } from '../reference/reference-adapter.js'
22
+ import { actuationReadinessOf, actuationTargetOf, routeActuation } from '../reference/actuation-routing.js'
23
+ import { sweepPendingDispatch, type SweepReport } from './sweep-dispatch.js'
24
+ import { catchUpEffects } from './catch-up-effects.js'
25
+ import { databaseCommandStore } from './command-store.js'
26
+ import { runRulesOnce } from './rule-runner.js'
27
+ import { attentionsOf, modeOf, noteRun, rulesOf } from './rule-wiring.js'
28
+
29
+ /** λͺ‡ μ΄ˆλ§ˆλ‹€ λ„λ‚˜. 였래 이어진 쑰건을 λ³΄λŠ” 일이라 μ΄˜μ΄˜ν•  μ΄μœ κ°€ μ—†λ‹€. */
30
+ export const RULE_PERIOD_MS = 30_000
31
+
32
+ let timer: any
33
+ let running = false
34
+
35
+ /** μ£ΌκΈ°λ₯Ό μ„Έμš΄λ‹€ β€” 두 번 λΆˆλŸ¬λ„ ν•˜λ‚˜λ§Œ μ„ λ‹€. */
36
+ export function startActuationRuleLoop(periodMs = RULE_PERIOD_MS): void {
37
+ if (timer) return
38
+
39
+ timer = setInterval(() => {
40
+ /* μ•ž 바퀴가 아직 λˆλ‹€ β€” 겹치면 같은 쑰치λ₯Ό 두 벌 λ‚Έλ‹€. κ±΄λ„ˆλ›°λŠ” 것이 λ§žλ‹€. */
41
+ if (running) return
42
+ running = true
43
+
44
+ tickRules()
45
+ .catch(err => twinError(`[twin-rule] κ·œμΉ™ μ£ΌκΈ°κ°€ μ‹€νŒ¨ν–ˆλ‹€ β€” ${err?.message ?? err}`))
46
+ .finally(() => {
47
+ running = false
48
+ })
49
+ }, periodMs)
50
+
51
+ /* 이 타이머 λ•Œλ¬Έμ— ν”„λ‘œμ„ΈμŠ€κ°€ μ•ˆ μ£½μ§€ μ•Šκ²Œ. */
52
+ if (typeof timer?.unref === 'function') timer.unref()
53
+ }
54
+
55
+ export function stopActuationRuleLoop(): void {
56
+ if (!timer) return
57
+ clearInterval(timer)
58
+ timer = undefined
59
+ }
60
+
61
+ /**
62
+ * 일꾼이 ν•œ 바퀴에 무엇을 ν–ˆλ‚˜ β€” **λ°”λ€Œμ—ˆμ„ λ•Œλ§Œ** μ λŠ”λ‹€.
63
+ *
64
+ * ── μ™œ ν•„μš”ν•œκ°€ (2026-09-04) ────────────────────────────────────────────────
65
+ * μ²˜μŒμ— 보고λ₯Ό λ°›κ³  버렸닀. κ·ΈλŸ¬λ‹ˆ λŒ€κΈ° 쀄에 μ‘°μΉ˜κ°€ 앉아 μžˆλŠ”λ° 아무 일도 μ•ˆ 일어날 λ•Œ
66
+ * **이유λ₯Ό μ•Œ 길이 μ—†μ—ˆλ‹€** β€” μ‹€μ œλ‘œ κ·Έ μƒνƒœλ₯Ό λ§Œλ‚˜ 30뢄을 λ’€μ‘Œλ‹€. μ§‘μ§€ μ•ŠλŠ” 것은 λŒ€κ°œ μ˜³μ€
67
+ * νŒλ‹¨μ΄μ§€λ§Œ, μ˜³μ€ νŒλ‹¨λ„ 보이지 μ•ŠμœΌλ©΄ 결함과 κ΅¬λ³„λ˜μ§€ μ•ŠλŠ”λ‹€.
68
+ *
69
+ * ── μ™œ λ°”λ€Œμ—ˆμ„ λ•Œλ§ŒμΈκ°€ ────────────────────────────────────────────────────
70
+ * 30μ΄ˆλ§ˆλ‹€ 같은 쀄을 적으면 ν•˜λ£¨ 2,880쀄이고 κ·Έ μ‚¬μ΄μ˜ μ§„μ§œ λ³€ν™”κ°€ λ¬»νžŒλ‹€. λŒ€κΈ° μ€„μ˜ λͺ¨μ–‘이
71
+ * κ·ΈλŒ€λ‘œλ©΄ μƒˆλ‘œ 말할 것이 μ—†λ‹€ β€” `noteBlocked` μ—μ„œ 지킨 것과 같은 κ·œμœ¨μ΄λ‹€.
72
+ */
73
+ const lastSweep = new Map<string, string>()
74
+
75
+ function noteSweep(domainId: string, report: SweepReport): void {
76
+ if (!report.seen) {
77
+ lastSweep.delete(domainId)
78
+ return
79
+ }
80
+
81
+ const skips = Object.entries(report.skipped)
82
+ .sort(([a], [b]) => a.localeCompare(b))
83
+ .map(([why, n]) => `${why} ${n}`)
84
+ .join(' Β· ')
85
+ const line = `${report.seen}건 λŒ€κΈ° Β· λ„˜κΉ€ ${report.dispatched}${skips ? ` Β· ${skips}` : ''}`
86
+
87
+ if (lastSweep.get(domainId) === line) return
88
+ lastSweep.set(domainId, line)
89
+ twinLog(`[twin-sweep] ${line}`)
90
+ }
91
+
92
+ async function tickRules(): Promise<void> {
93
+ /*
94
+ * ── μœ μž… 창을 λ¨Όμ € κ΅΄λ¦°λ‹€ (2026-09-05) ─────────────────────────────────────
95
+ * 창이 λ‹«νžˆλŠ” μ΄μœ λŠ” **μ‹œκ°„μ΄ μ§€λ‚œ 것**이닀. 그런데 μ˜ˆμ „μ—λŠ” γ€Œλ‹€μŒ 사건이 왔을 λ•Œγ€μ™€ γ€Œλˆ„κ°€
96
+ * λ¬Όμ–΄λ³Ό λ•Œγ€λ§Œ κ΅΄λ €λ‹€ β€” λ‘˜ λ‹€ μ•ˆ 올 수 μžˆλ‹€.
97
+ *
98
+ * κ·Έλž˜μ„œ 사싀이 λ“œλ¬Έ νŠΈμœˆμ€ ν‘œμ— 아무것도 μ•ˆ λ‚¨μ•˜λ‹€. μ‹€μΈ‘: 46건이 62뢄에 흩어져 온 트윈의 창이
99
+ * 1개, 사싀 11건뿐인 트윈의 창이 48개 β€” **사싀 μˆ˜μ™€ μ°½ μˆ˜κ°€ 거꾸둜**μ˜€λ‹€. 창을 λ§Œλ“  것이 μœ μž…μ΄
100
+ * μ•„λ‹ˆλΌ λˆ„κ°€ λ¬Όμ–΄λ³Έ κ²ƒμ΄μ—ˆκΈ° λ•Œλ¬Έμ΄λ‹€.
101
+ *
102
+ * **아무도 μ•ˆ λ³΄λŠ” 트윈일수둝 기둝이 μ—†μ—ˆλ‹€.** 사싀이 쑰용히 μ‚¬λΌμ§€λŠ” 것을 μž‘μœΌλ €λŠ” μž₯λΆ€κ°€ μ •ν™•νžˆ
103
+ * κ·Έ λ°˜λŒ€λ‘œ 돌고 μžˆμ—ˆλ‹€(137λ΄‰νˆ¬κ°€ μž₯λΆ€ 없이 μ§€λ‚˜κ°”λ‹€ β€” μΈν‹°κ·Έλ ˆμ΄μ…˜ 레인 μ‹€μΈ‘).
104
+ *
105
+ * μ†Œμœ ν•œ 트윈이 없어도 κ΅΄λ¦°λ‹€ β€” μž₯λΆ€λŠ” ν”„λ‘œμ„ΈμŠ€κ°€ μ‚¬λŠ” λ™μ•ˆ 이어지고, κ·œμΉ™μ„ 돌 트윈이 μ—†λ‹€λŠ”
106
+ * 것과 기둝할 창이 μ—†λ‹€λŠ” 것은 λ‹€λ₯Έ 사싀이닀.
107
+ */
108
+ const rolled = TwinEngine.rollAllIngestWindows()
109
+ if (rolled) twinLog(`[twin-ingest] μ°½ ${rolled}개λ₯Ό λ‹«μ•„ μ μ—ˆλ‹€`)
110
+
111
+ const owned = TwinEngine.ownedTwins()
112
+ if (!owned.length) return
113
+
114
+ /*
115
+ * νŠΈλžœμž­μ…˜μ„ λ„˜κΈ°μ§€ μ•ŠλŠ”λ‹€ β€” μ—¬λŸ¬ νŠΈμœˆμ„ 돌고, ν•œ 트윈이 μ‹€νŒ¨ν•΄λ„ μ•žμ˜ 트윈이 λ‚Έ 것은 남아야 ν•œλ‹€.
116
+ * ν•œ λ©μ–΄λ¦¬λ‘œ 묢으면 λ§ˆμ§€λ§‰ 트윈의 μ‹€νŒ¨κ°€ μ•žμ˜ κ²°κ³Όλ₯Ό μ „λΆ€ λ¬Όλ¦°λ‹€.
117
+ */
118
+ /*
119
+ * **λ†“μΉœ 보고λ₯Ό ν•¨κ»˜ backfill ν•œλ‹€** β€” κ΄€μΈ‘ β†’ 쑰치의 λ°˜λŒ€ λ°©ν–₯이닀.
120
+ *
121
+ * 같은 μ£ΌκΈ°λ₯Ό μ“°λŠ” μ΄μœ λŠ” 값이닀: λ‘˜ λ‹€ λͺ‡ μ΄ˆλ§ˆλ‹€ λ³Ό 일이고, 타이머λ₯Ό λ‘˜ 두면 내리고 μ„ΈμšΈ μžλ¦¬κ°€
122
+ * λ‘˜μ΄ λœλ‹€. λ‹€λ§Œ **μ„œλ‘œ 막지 μ•ŠλŠ”λ‹€** β€” κ·œμΉ™μ΄ 터져도 backfill 은 λŒμ•„μ•Ό ν•œλ‹€. 쑰치λ₯Ό λͺ» λ‚΄λŠ” 것과
123
+ * λ‚˜κ°„ 쑰치의 κ²°κ³Όλ₯Ό λͺ¨λ₯΄λŠ” 것은 λ‹€λ₯Έ 사고닀.
124
+ */
125
+ for (const domainId of new Set(owned.map(t => t.domainId))) {
126
+ await catchUpEffects(domainId, databaseCommandStore())
127
+ /*
128
+ * **λ„˜κΈΈ μ°¨λ‘€λ₯Ό κΈ°λ‹€λ¦¬λŠ” 것듀도 ν•¨κ»˜ μ€λŠ”λ‹€** β€” 승인만 되고 μ•ˆ λ‚˜κ°„ 것, μ‹€νŒ¨ν•΄μ„œ λ‹€μ‹œ ν•΄ λ³Ό 것.
129
+ *
130
+ * 정상 κ²½λ‘œλŠ” 승인 직후에 이미 λλ‚œλ‹€(Β§`dispatchAfterApproval`, μ‹€μΈ‘ 282ms). 이 μžλ¦¬λŠ” **κ·Έ
131
+ * 사이에 ν”„λ‘œμ„ΈμŠ€κ°€ 죽은 경우**와 μž¬μ‹œλ„λ§Œ λ³Έλ‹€. κ·Έλž˜μ„œ 30초둜 μΆ©λΆ„ν•˜κ³  아무도 μ•ˆ κΈ°λ‹€λ¦°λ‹€.
132
+ */
133
+ const swept = await sweepPendingDispatch(domainId, {
134
+ store: databaseCommandStore(),
135
+ actuate: command => routeActuation(domainId, command.instanceId, command),
136
+ idempotent: async instanceId => {
137
+ const target = await actuationTargetOf(domainId, instanceId).catch(() => undefined)
138
+ const type = (target as any)?.adapterType
139
+ return type ? capabilitiesOf(getAdapter(type)).includes('idempotent-actuation') : false
140
+ },
141
+ localReadiness: async instanceId => {
142
+ const r = await actuationReadinessOf(domainId, instanceId).catch(() => undefined)
143
+ /* μ›κ²©μœΌλ‘œ λ¬Όμ–΄ λ³Έ 닡은 μ“°μ§€ μ•ŠλŠ”λ‹€ β€” 이 μžλ¦¬λŠ” μ €μͺ½μ„ κ±΄λ“œλ¦¬μ§€ μ•ŠλŠ” 것이 κ·œμ•½μ΄λ‹€. */
144
+ return r && (r as any).checked === 'local' ? (r as any) : undefined
145
+ }
146
+ })
147
+ noteSweep(domainId, swept)
148
+ }
149
+
150
+ await runRulesOnce({
151
+ ownedTwins: () => owned,
152
+ rulesOf,
153
+ attentionsOf,
154
+ modeOf,
155
+ store: databaseCommandStore(),
156
+ specs: MES_COMMANDS,
157
+ nowMs: () => Date.now(),
158
+ noteRun
159
+ })
160
+ }