@things-factory/headless-twin 10.0.19 β†’ 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +10 -1
  2. package/dist-server/engine/canonical-ingest.js +110 -33
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/event-time.d.ts +25 -0
  5. package/dist-server/engine/event-time.js +98 -0
  6. package/dist-server/engine/event-time.js.map +1 -0
  7. package/dist-server/engine/index.d.ts +2 -0
  8. package/dist-server/engine/index.js +23 -0
  9. package/dist-server/engine/index.js.map +1 -1
  10. package/dist-server/engine/ingest-dedupe.d.ts +18 -6
  11. package/dist-server/engine/ingest-dedupe.js +201 -15
  12. package/dist-server/engine/ingest-dedupe.js.map +1 -1
  13. package/dist-server/engine/ingest-health.d.ts +60 -1
  14. package/dist-server/engine/ingest-health.js +43 -8
  15. package/dist-server/engine/ingest-health.js.map +1 -1
  16. package/dist-server/engine/kpi-fold.d.ts +19 -0
  17. package/dist-server/engine/kpi-fold.js +4 -1
  18. package/dist-server/engine/kpi-fold.js.map +1 -1
  19. package/dist-server/engine/kpi-query.js +23 -3
  20. package/dist-server/engine/kpi-query.js.map +1 -1
  21. package/dist-server/engine/oee-accumulator.d.ts +35 -2
  22. package/dist-server/engine/oee-accumulator.js +56 -4
  23. package/dist-server/engine/oee-accumulator.js.map +1 -1
  24. package/dist-server/engine/twin-engine.d.ts +72 -3
  25. package/dist-server/engine/twin-engine.js +118 -5
  26. package/dist-server/engine/twin-engine.js.map +1 -1
  27. package/dist-server/index.js +21 -0
  28. package/dist-server/index.js.map +1 -1
  29. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.d.ts +5 -0
  30. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js +66 -0
  31. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js.map +1 -0
  32. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.d.ts +5 -0
  33. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js +79 -0
  34. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js.map +1 -0
  35. package/dist-server/migrations/index.js +5 -1
  36. package/dist-server/migrations/index.js.map +1 -1
  37. package/dist-server/routes.js +12 -20
  38. package/dist-server/routes.js.map +1 -1
  39. package/dist-server/service/actuation/actuation-advisor.d.ts +60 -0
  40. package/dist-server/service/actuation/actuation-advisor.js +53 -0
  41. package/dist-server/service/actuation/actuation-advisor.js.map +1 -0
  42. package/dist-server/service/actuation/actuation-outcome.d.ts +52 -0
  43. package/dist-server/service/actuation/actuation-outcome.js +34 -0
  44. package/dist-server/service/actuation/actuation-outcome.js.map +1 -0
  45. package/dist-server/service/actuation/actuation-rule-resolver.d.ts +19 -0
  46. package/dist-server/service/actuation/actuation-rule-resolver.js +161 -0
  47. package/dist-server/service/actuation/actuation-rule-resolver.js.map +1 -0
  48. package/dist-server/service/actuation/actuation-rule.d.ts +55 -0
  49. package/dist-server/service/actuation/actuation-rule.js +130 -0
  50. package/dist-server/service/actuation/actuation-rule.js.map +1 -0
  51. package/dist-server/service/actuation/approval-gateway.d.ts +20 -0
  52. package/dist-server/service/actuation/approval-gateway.js +14 -0
  53. package/dist-server/service/actuation/approval-gateway.js.map +1 -0
  54. package/dist-server/service/actuation/catch-up-effects.d.ts +16 -0
  55. package/dist-server/service/actuation/catch-up-effects.js +120 -0
  56. package/dist-server/service/actuation/catch-up-effects.js.map +1 -0
  57. package/dist-server/service/actuation/command-dispatcher.d.ts +206 -8
  58. package/dist-server/service/actuation/command-dispatcher.js +191 -14
  59. package/dist-server/service/actuation/command-dispatcher.js.map +1 -1
  60. package/dist-server/service/actuation/command-store.d.ts +10 -2
  61. package/dist-server/service/actuation/command-store.js +168 -5
  62. package/dist-server/service/actuation/command-store.js.map +1 -1
  63. package/dist-server/service/actuation/dispatch-after-approval.d.ts +11 -0
  64. package/dist-server/service/actuation/dispatch-after-approval.js +73 -0
  65. package/dist-server/service/actuation/dispatch-after-approval.js.map +1 -0
  66. package/dist-server/service/actuation/effect-match.d.ts +53 -0
  67. package/dist-server/service/actuation/effect-match.js +65 -0
  68. package/dist-server/service/actuation/effect-match.js.map +1 -0
  69. package/dist-server/service/actuation/index.d.ts +21 -2
  70. package/dist-server/service/actuation/index.js +21 -2
  71. package/dist-server/service/actuation/index.js.map +1 -1
  72. package/dist-server/service/actuation/retry-plan.d.ts +63 -0
  73. package/dist-server/service/actuation/retry-plan.js +99 -0
  74. package/dist-server/service/actuation/retry-plan.js.map +1 -0
  75. package/dist-server/service/actuation/rule-evaluate.d.ts +127 -0
  76. package/dist-server/service/actuation/rule-evaluate.js +205 -0
  77. package/dist-server/service/actuation/rule-evaluate.js.map +1 -0
  78. package/dist-server/service/actuation/rule-loop.d.ts +5 -0
  79. package/dist-server/service/actuation/rule-loop.js +156 -0
  80. package/dist-server/service/actuation/rule-loop.js.map +1 -0
  81. package/dist-server/service/actuation/rule-runner.d.ts +52 -0
  82. package/dist-server/service/actuation/rule-runner.js +135 -0
  83. package/dist-server/service/actuation/rule-runner.js.map +1 -0
  84. package/dist-server/service/actuation/rule-wiring.d.ts +24 -0
  85. package/dist-server/service/actuation/rule-wiring.js +80 -0
  86. package/dist-server/service/actuation/rule-wiring.js.map +1 -0
  87. package/dist-server/service/actuation/settle-effects.d.ts +21 -0
  88. package/dist-server/service/actuation/settle-effects.js +101 -0
  89. package/dist-server/service/actuation/settle-effects.js.map +1 -0
  90. package/dist-server/service/actuation/sweep-dispatch.d.ts +37 -0
  91. package/dist-server/service/actuation/sweep-dispatch.js +125 -0
  92. package/dist-server/service/actuation/sweep-dispatch.js.map +1 -0
  93. package/dist-server/service/actuation/twin-command-resolver.d.ts +120 -0
  94. package/dist-server/service/actuation/twin-command-resolver.js +406 -0
  95. package/dist-server/service/actuation/twin-command-resolver.js.map +1 -0
  96. package/dist-server/service/actuation/twin-command.d.ts +81 -0
  97. package/dist-server/service/actuation/twin-command.js +60 -0
  98. package/dist-server/service/actuation/twin-command.js.map +1 -1
  99. package/dist-server/service/actuation/twin-now.d.ts +41 -0
  100. package/dist-server/service/actuation/twin-now.js +65 -0
  101. package/dist-server/service/actuation/twin-now.js.map +1 -0
  102. package/dist-server/service/index.d.ts +5 -2
  103. package/dist-server/service/index.js +8 -0
  104. package/dist-server/service/index.js.map +1 -1
  105. package/dist-server/service/reference/actuation-routing.d.ts +60 -0
  106. package/dist-server/service/reference/actuation-routing.js +126 -0
  107. package/dist-server/service/reference/actuation-routing.js.map +1 -0
  108. package/dist-server/service/reference/actuation-target.d.ts +65 -0
  109. package/dist-server/service/reference/actuation-target.js +94 -0
  110. package/dist-server/service/reference/actuation-target.js.map +1 -0
  111. package/dist-server/service/reference/connection-portability-resolver.d.ts +29 -0
  112. package/dist-server/service/reference/connection-portability-resolver.js +181 -0
  113. package/dist-server/service/reference/connection-portability-resolver.js.map +1 -0
  114. package/dist-server/service/reference/connection-portability.d.ts +138 -0
  115. package/dist-server/service/reference/connection-portability.js +230 -0
  116. package/dist-server/service/reference/connection-portability.js.map +1 -0
  117. package/dist-server/service/reference/fill-cadence.d.ts +17 -0
  118. package/dist-server/service/reference/fill-cadence.js +31 -0
  119. package/dist-server/service/reference/fill-cadence.js.map +1 -0
  120. package/dist-server/service/reference/fill-dedupe.d.ts +16 -0
  121. package/dist-server/service/reference/fill-dedupe.js +63 -0
  122. package/dist-server/service/reference/fill-dedupe.js.map +1 -0
  123. package/dist-server/service/reference/fill-loop.d.ts +35 -0
  124. package/dist-server/service/reference/fill-loop.js +187 -0
  125. package/dist-server/service/reference/fill-loop.js.map +1 -0
  126. package/dist-server/service/reference/fill-span.d.ts +10 -0
  127. package/dist-server/service/reference/fill-span.js +37 -0
  128. package/dist-server/service/reference/fill-span.js.map +1 -0
  129. package/dist-server/service/reference/hook-contract.d.ts +88 -4
  130. package/dist-server/service/reference/hook-contract.js +105 -4
  131. package/dist-server/service/reference/hook-contract.js.map +1 -1
  132. package/dist-server/service/reference/hook-rejected.d.ts +25 -0
  133. package/dist-server/service/reference/hook-rejected.js +48 -0
  134. package/dist-server/service/reference/hook-rejected.js.map +1 -0
  135. package/dist-server/service/reference/index.d.ts +4 -1
  136. package/dist-server/service/reference/index.js +5 -1
  137. package/dist-server/service/reference/index.js.map +1 -1
  138. package/dist-server/service/reference/live-cadence.d.ts +68 -0
  139. package/dist-server/service/reference/live-cadence.js +44 -0
  140. package/dist-server/service/reference/live-cadence.js.map +1 -0
  141. package/dist-server/service/reference/live-feed-lease.d.ts +15 -0
  142. package/dist-server/service/reference/live-feed-lease.js +56 -0
  143. package/dist-server/service/reference/live-feed-lease.js.map +1 -0
  144. package/dist-server/service/reference/live-ingest.d.ts +21 -0
  145. package/dist-server/service/reference/live-ingest.js +66 -0
  146. package/dist-server/service/reference/live-ingest.js.map +1 -0
  147. package/dist-server/service/reference/reference-adapter.d.ts +291 -2
  148. package/dist-server/service/reference/reference-adapter.js +19 -1
  149. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  150. package/dist-server/service/reference/reference-fill-run.d.ts +50 -0
  151. package/dist-server/service/reference/reference-fill-run.js +142 -0
  152. package/dist-server/service/reference/reference-fill-run.js.map +1 -0
  153. package/dist-server/service/reference/reference-fill.d.ts +90 -0
  154. package/dist-server/service/reference/reference-fill.js +141 -0
  155. package/dist-server/service/reference/reference-fill.js.map +1 -0
  156. package/dist-server/service/reference/reference-hook.d.ts +22 -0
  157. package/dist-server/service/reference/reference-hook.js +135 -55
  158. package/dist-server/service/reference/reference-hook.js.map +1 -1
  159. package/dist-server/service/reference/reference-link-groups.d.ts +10 -0
  160. package/dist-server/service/reference/reference-link-groups.js +15 -0
  161. package/dist-server/service/reference/reference-link-groups.js.map +1 -0
  162. package/dist-server/service/reference/reference-links.d.ts +14 -0
  163. package/dist-server/service/reference/reference-links.js +73 -0
  164. package/dist-server/service/reference/reference-links.js.map +1 -0
  165. package/dist-server/service/reference/reference-live.d.ts +1 -0
  166. package/dist-server/service/reference/reference-live.js +32 -6
  167. package/dist-server/service/reference/reference-live.js.map +1 -1
  168. package/dist-server/service/reference/reference-master.d.ts +56 -2
  169. package/dist-server/service/reference/reference-master.js +33 -3
  170. package/dist-server/service/reference/reference-master.js.map +1 -1
  171. package/dist-server/service/reference/reference-resolver.d.ts +27 -0
  172. package/dist-server/service/reference/reference-resolver.js +79 -1
  173. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  174. package/dist-server/service/twin-event/twin-event-keys.d.ts +31 -3
  175. package/dist-server/service/twin-event/twin-event-keys.js +36 -4
  176. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  177. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  178. package/dist-server/service/twin-event/twin-event.js +11 -2
  179. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  180. package/dist-server/service/twin-model/choose-recipe.d.ts +15 -0
  181. package/dist-server/service/twin-model/choose-recipe.js +33 -0
  182. package/dist-server/service/twin-model/choose-recipe.js.map +1 -0
  183. package/dist-server/service/twin-model/external-resolver.d.ts +76 -0
  184. package/dist-server/service/twin-model/external-resolver.js +109 -0
  185. package/dist-server/service/twin-model/external-resolver.js.map +1 -0
  186. package/dist-server/service/twin-model/project-structure.js +16 -3
  187. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  188. package/dist-server/service/twin-model/twin-equipment.d.ts +17 -0
  189. package/dist-server/service/twin-model/twin-equipment.js +10 -0
  190. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  191. package/dist-server/service/twin-model/twin-lineage-query.js +75 -1
  192. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  193. package/dist-server/service/twin-model/twin-model-item-query.js +94 -5
  194. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  195. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  196. package/dist-server/service/twin-space/twin-space.d.ts +29 -0
  197. package/dist-server/service/twin-space/twin-space.js +13 -0
  198. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  199. package/dist-server/service/twin-subject/event-subjects.d.ts +3 -1
  200. package/dist-server/service/twin-subject/event-subjects.js +8 -1
  201. package/dist-server/service/twin-subject/event-subjects.js.map +1 -1
  202. package/dist-shared/entity-delta.js +1 -1
  203. package/dist-shared/entity-delta.js.map +1 -1
  204. package/package.json +7 -7
  205. package/server/engine/canonical-ingest.ts +119 -33
  206. package/server/engine/event-time.ts +96 -0
  207. package/server/engine/index.ts +20 -0
  208. package/server/engine/ingest-dedupe.ts +203 -27
  209. package/server/engine/ingest-health.ts +91 -6
  210. package/server/engine/kpi-fold.ts +24 -1
  211. package/server/engine/kpi-query.ts +23 -4
  212. package/server/engine/oee-accumulator.ts +80 -8
  213. package/server/engine/twin-engine.ts +119 -5
  214. package/server/index.ts +23 -0
  215. package/server/migrations/1786600000000-RenameOperatoMesAdapterType.ts +66 -0
  216. package/server/migrations/1786700000000-AddDispatchRetryColumns.ts +82 -0
  217. package/server/migrations/index.ts +5 -1
  218. package/server/routes.ts +12 -24
  219. package/server/service/actuation/actuation-advisor.ts +124 -0
  220. package/server/service/actuation/actuation-outcome.ts +97 -0
  221. package/server/service/actuation/actuation-rule-resolver.ts +147 -0
  222. package/server/service/actuation/actuation-rule.ts +139 -0
  223. package/server/service/actuation/approval-gateway.ts +44 -0
  224. package/server/service/actuation/catch-up-effects.ts +152 -0
  225. package/server/service/actuation/command-dispatcher.ts +401 -20
  226. package/server/service/actuation/command-store.ts +182 -8
  227. package/server/service/actuation/dispatch-after-approval.ts +71 -0
  228. package/server/service/actuation/effect-match.ts +124 -0
  229. package/server/service/actuation/index.ts +21 -2
  230. package/server/service/actuation/retry-plan.ts +145 -0
  231. package/server/service/actuation/rule-evaluate.ts +352 -0
  232. package/server/service/actuation/rule-loop.ts +160 -0
  233. package/server/service/actuation/rule-runner.ts +216 -0
  234. package/server/service/actuation/rule-wiring.ts +84 -0
  235. package/server/service/actuation/settle-effects.ts +115 -0
  236. package/server/service/actuation/sweep-dispatch.ts +179 -0
  237. package/server/service/actuation/twin-command-resolver.ts +387 -0
  238. package/server/service/actuation/twin-command.ts +128 -1
  239. package/server/service/actuation/twin-now.ts +97 -0
  240. package/server/service/index.ts +9 -1
  241. package/server/service/reference/actuation-routing.ts +165 -0
  242. package/server/service/reference/actuation-target.ts +163 -0
  243. package/server/service/reference/connection-portability-resolver.ts +171 -0
  244. package/server/service/reference/connection-portability.ts +285 -0
  245. package/server/service/reference/fill-cadence.ts +29 -0
  246. package/server/service/reference/fill-dedupe.ts +66 -0
  247. package/server/service/reference/fill-loop.ts +153 -0
  248. package/server/service/reference/fill-span.ts +31 -0
  249. package/server/service/reference/hook-contract.ts +185 -5
  250. package/server/service/reference/hook-rejected.ts +53 -0
  251. package/server/service/reference/index.ts +5 -1
  252. package/server/service/reference/live-cadence.ts +129 -0
  253. package/server/service/reference/live-feed-lease.ts +56 -0
  254. package/server/service/reference/live-ingest.ts +94 -0
  255. package/server/service/reference/reference-adapter.ts +327 -5
  256. package/server/service/reference/reference-fill-run.ts +165 -0
  257. package/server/service/reference/reference-fill.ts +223 -0
  258. package/server/service/reference/reference-hook.ts +162 -56
  259. package/server/service/reference/reference-link-groups.ts +21 -0
  260. package/server/service/reference/reference-links.ts +77 -0
  261. package/server/service/reference/reference-live.ts +30 -5
  262. package/server/service/reference/reference-master.ts +90 -5
  263. package/server/service/reference/reference-resolver.ts +77 -2
  264. package/server/service/twin-event/twin-event-keys.ts +37 -4
  265. package/server/service/twin-event/twin-event.ts +18 -1
  266. package/server/service/twin-model/choose-recipe.ts +31 -0
  267. package/server/service/twin-model/external-resolver.ts +161 -0
  268. package/server/service/twin-model/project-structure.ts +16 -3
  269. package/server/service/twin-model/twin-equipment.ts +22 -0
  270. package/server/service/twin-model/twin-lineage-query.ts +82 -1
  271. package/server/service/twin-model/twin-model-item-query.ts +95 -4
  272. package/server/service/twin-model/twin-model-tree-query.ts +1 -1
  273. package/server/service/twin-space/twin-space.ts +39 -0
  274. package/server/service/twin-subject/event-subjects.ts +8 -1
  275. package/shared/entity-delta.ts +1 -1
  276. package/test/ack-shape-alignment.test.ts +97 -0
  277. package/test/actuation-approval-door.test.ts +421 -0
  278. package/test/actuation-dispatch.test.ts +16 -3
  279. package/test/actuation-effect.test.ts +162 -0
  280. package/test/actuation-outcome.test.ts +152 -0
  281. package/test/actuation-rule-runner.test.ts +281 -0
  282. package/test/actuation-rule.test.ts +266 -0
  283. package/test/actuation-seam.test.ts +249 -0
  284. package/test/actuation-settle.test.ts +164 -0
  285. package/test/actuation-target.test.ts +145 -0
  286. package/test/canonical-ingest-vocabularies.test.ts +49 -2
  287. package/test/capability-mapping.test.ts +1 -1
  288. package/test/choose-recipe.test.ts +117 -0
  289. package/test/command-store-writes-columns.test.ts +126 -0
  290. package/test/connection-portability-doors.test.ts +87 -0
  291. package/test/connector-capability-declaration.test.ts +54 -4
  292. package/test/declaration-reaches-model.test.ts +43 -0
  293. package/test/dedupe-transformation.test.ts +112 -0
  294. package/test/entity-delta.test.ts +1 -1
  295. package/test/event-subjects.test.ts +2 -1
  296. package/test/external-resolver.test.ts +171 -0
  297. package/test/fill-dedupe.test.ts +59 -0
  298. package/test/fill-loop.test.ts +35 -0
  299. package/test/first-envelope.test.ts +153 -0
  300. package/test/fold-shift-dst.test.ts +110 -0
  301. package/test/home-in-area.test.ts +90 -0
  302. package/test/hook-rejected-shape.test.ts +86 -0
  303. package/test/hook-response-sum.test.ts +193 -0
  304. package/test/hook-sequence.test.ts +1 -1
  305. package/test/ingest-dedupe.test.ts +56 -0
  306. package/test/ingest-expected-quiet.test.ts +111 -0
  307. package/test/ingest-idempotent.test.ts +79 -0
  308. package/test/ingest-reconcile-four-ways.test.ts +97 -0
  309. package/test/ingest-rules-header.test.ts +100 -0
  310. package/test/ingest-window-rolls-unobserved.test.ts +72 -0
  311. package/test/intake-mapping-parity.test.ts +175 -0
  312. package/test/live-cadence.test.ts +170 -0
  313. package/test/live-feed-lease.test.ts +63 -0
  314. package/test/live-mirror-parity.test.ts +1 -1
  315. package/test/oee-accumulator.test.ts +3 -3
  316. package/test/oee-site-calendar.test.ts +146 -0
  317. package/test/order-and-transaction-split.test.ts +107 -0
  318. package/test/reference-fill.test.ts +235 -0
  319. package/test/reference-hook.test.ts +1 -1
  320. package/test/reference-links.test.ts +56 -0
  321. package/test/retry-plan.test.ts +139 -0
  322. package/test/sweep-dispatch.test.ts +191 -0
  323. package/test/twin-event-keys.test.ts +20 -6
  324. package/test/twin-model-item-db.test.ts +6 -6
  325. package/test/twin-model-tree-db.test.ts +6 -6
  326. package/tsconfig.shared.tsbuildinfo +1 -1
  327. package/tsconfig.tsbuildinfo +1 -1
@@ -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
+ }